首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐
2025年6月17日 星期二 3:42am

首页 | 学习笔记

  • 25-04-18 08:00
  • 3378
  • 5906
juejin.cn

1. 概述

效果类似下图。

微信图片_20250107112312.jpg

2. 什么是Fragment

我们来看官方文档对fragment的解释,我的理解是activity适合用来做一个整体的界面,而fragment适合做线性布局的界面。 image.png

image.png

2.1 Activity和Fragment的关系

  • 存在方式:Activity可以独立存在,是应用程序界面的主要入口和承载者。Fragment不能独立存在,必须由 Activity或其他Fragment托管,自己的视图会附加到宿主的视图层次结构中。

  • 作用范围:Activity通常用于放置全局的、具有整体结构意义的界面元素,例如抽屉式导航栏、工具栏等。它是一个更高层级的容器,可以管理整个应用或某个模块的所有界面切换。Fragment更适合定义单个“局部”屏幕的布局或功能部分。把界面拆分成多个Fragment可以实现更灵活的模块化和重复使用。

  • 生命周期:Activity具有独立且完整的生命周期(如onCreate、onStart、onResume、onPause、onStop、onDestroy)。应用被启动后,Activity可以完全管理自身的状态。Fragment虽然有自己的一套类似生命周期方法(如onCreateView、onViewCreated、onDestroyView等),但它必须依附在宿主Activity的生命周期之上,Fragment的生存状态直接受到其所依附的Activity的影响。

  • 动态管理:Activity在大多数情况下并不会在运行时被动态添加、替换或移除,而是通过启动或关闭一个 Activity来切换页面。Fragment可以在Activity处于STARTED或更高的生命周期状态时被动态添加、替换或移除,并且可以利用返回堆栈来记录和撤销这些更改,实现更灵活的界面切换和管理。

  • 复用性:Activity通常是应用中的一个功能或一个大模块的“入口”,很少被多处“嵌套”或复用。Fragment可以在同一 Activity或不同Activity中多次使用,甚至可以嵌套在其他Fragment中。这样的复用性使得界面更加灵活、模块化。

  • 通信方式:Activity通常通过Intent与其他Activity进行通信或跳转。Fragment需要通过宿主 Activity或ViewModel(也可使用专门的接口回调等方式)与其他Fragment通信,但是Fragment间不建议直接互相操作,最好保持松耦合。

3 Fragment分层架构模式

分层架构模式(Layered Architecture Pattern)在开发中是一种常见的做法,在中大型项目中,通常将Fragment抽象为四个层级结构,类似于:
BaseFragment <- BaseCommonFragment <- BaseLogicFragment <- BaseViewModelFragment

这样做的好处主要是模块化,高复用性,易维护,易扩展(开放封闭原则)。

  • 模块化:每一层专注于某一个功能,各司其责,比如上面四个Fragment类各自负责基础功能,通用功能,业务逻辑,视图绑定。
  • 高复用性:基类方法子类共享,避免重复代码编写。
  • 易维护:新增需求时,只需修改对应的层,并且方便快速定位错误。
  • 易扩展:主要是开放封闭原则(对扩展开放,对修改封闭)的体现。比如要新增一个View初始化逻辑,只需在对应的子类对initView()进行修改,无需修改父类。

再来看每个Fragment类的实现:

3.1 BaseFragment

抽象类,继承自Fragment,负责视图的创建和之后的初始化操作。在这个项目中,重写了两个Fragment生命周期方法,onCreateView()和onViewCreated()。

less
代码解读
复制代码
public abstract class BaseFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return getLayoutView(inflater, container, savedInstanceState); } protected abstract View getLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState); public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initViews(); initDatum(); initListeners(); } protected void initViews() {} protected void initDatum() {} protected void initListeners() {} }

3.1.1 视图创建:onCreateView()

image.png

onCreateView()是Fragment的生命周期方法之一,它在onCreate()方法后调用,它负责加载布局并返回视图。重写该方法并调用getLayoutView()抽象方法,子类需要实现这个方法,后面会讲到,在BaseViewModelFragment类中实现了该方法,使用ViewBinding代替传统的LayoutInflater来简化视图的绑定。

3.1.2 初始化操作:onViewCreated()

image.png

onViewCreated()在onCreateView()返回后立即调用,在视图创建后进行额外的初始化操作,重写该方法加入了三个初始化步骤供子类实现:initViews()初始化视图,initDatum()数据加载,initListeners()设置化监听器。 这三个方法被称为钩子方法(Hook Method)。钩子方法是设计模式中的一种概念,通常是指在库、父类中中预留而不执行实际操作,供子类扩展,覆盖并实现的方法。三个初始化方法职责分明,并且将这些通用的初始化逻辑封装到这个父类中,可以确保每个Fragment按照统一的流程执行初始化。

3.2 BaseCommonFragment

scala
代码解读
复制代码
public abstract class BaseCommonFragment extends BaseFragment { protected void startActivity(Class clazz) { Intent intent = new Intent(getHostActivity(), clazz); startActivity(intent); } protected BaseCommonActivity getHostActivity() { return (BaseCommonActivity) getActivity(); } }

通用Fragment类, 继承自BaseFragment,封装了两个通用方法,分别负责启动目标Activity,以及返回宿主Activity。

image.png

getHostActivity()将宿主Activity返回,并强转为通用的BaseCommonActivity,使得调用者可以使用BaseCommonActivity中的方法。startActivity()使用Intent对象来启动一个Activity,Intent使用当前上下文getHostActivity()(相当于从哪里启动)和目标Activity作为参数。

3.3 BaseLogicFragment

这个Fragment类抽象为负责处理与业务相关的逻辑。

scala
代码解读
复制代码
public abstract class BaseLogicFragment extends BaseCommonFragment { protected PreferenceUtil sp; protected void initDatum() { super.initDatum(); if (isRegisterEventBus()) { EventBus.getDefault().register(this); } sp = PreferenceUtil.getInstance(getHostActivity()); } protected boolean isRegisterEventBus() { return false; } //......

initDatum()负责数据初始化,同时也可以完成EventBus的注册(注册了才可以接收事件),以及用户偏好工具类(记录最后一首播放的音乐等等)的初始化。

3.3.1 EventBus

image.png

简单来说,EventBus是一个事件总线库,使用发布/订阅模式(publish-subscribe pattern)在Activity, Fragment, Service之间收发事件。发布:post()方法,订阅:带有@Subscribe注解的方法,方法的参数类型就是要就订阅的事件类型,事件可以是任何类型的java对象。事件会在主线程或者后台线程中处理。

scss
代码解读
复制代码
//...... public void onDestroy() { super.onDestroy(); if (isRegisterEventBus()) { EventBus.getDefault().unregister(this); } } public void startMusicPlayerActivity() { ((BaseLogicActivity) getHostActivity()).startMusicPlayerActivity(); } }

3.3.2 onDestroy()

onDestroy()是Fragment的生命周期方法,在Fragment从Activity中移除或Activity销毁时被调用,完成销毁和清理工作。这里覆盖并注销EventBus。

startMusicPlayerActivity()启动进入音乐播放界面的Activity。

3.4 BaseViewModelFragment

scala
代码解读
复制代码
public abstract class BaseViewModelFragment extends ViewBinding> extends BaseLogicFragment { protected VB binding; public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ReflectUtil.newViewBinding(getLayoutInflater(), getClass()); } protected View getLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return binding.getRoot(); } public void onDestroyView() { super.onDestroyView(); binding = null; } }

这个Fragment类负责视图绑定,其中VB是一个泛型变量,继承自ViewBinding。继承BaseViewModelFragment的子类在其泛型上会自动根据布局文件xxx.xml生成名为xxxbind的类(subFragment extends BaseViewModelFragment),那么binding就是subFragment的一个实例,可以在subFragment中使用它来访问视图控件。

3.4.1 ViewBinding对象的创建

image.png

ViewBinding是Android提供的一种视图绑定机制,直接使用上文的bninding即可访问xml的控件,避免了每次使用findViewById()的麻烦。

bninding是通过ReflectUtil.newViewBinding(getLayoutInflater(), getClass())创建的,其中:

  • getLayoutInflater()返回当前Fragment(也就是subFragment)的LayoutInflater,可以将布局XML文件实例化为相应的View对象。

LayoutInflater是Android提供的类,LayoutInflater类将布局XML文件实例化为相应的视图对象。它本身不能直接使用。应该通过 android.app.Activity.getLayoutInflater()或Context.getSystemService()获取一个标准的 LayoutInflater实例,该实例已经与当前上下文绑定,并且正确配置了设备环境。

3.4.1.2 getClass()返回的是什么?

image.png

getClass()返回的是当前实例的运行时类,也就是说,如果有一个子类继承自BaseViewModelFragment,即便getClass()是在BaseViewModelFragment中使用,但是实际调用的还是它的子类,这里是基于java的动态绑定机制。动态绑定机制的核心是,方法调用在运行时根据对象的实际类型来决定。也就是说,即便方法调用发生在父类,调用的行为也是根据子类的类型来执行。具体的,某个子类subFragment继承自BaseViewModelFragment,所以是subFragment的实例调用getClass(),并返回subFragment.class。

ReflectUtil.newViewBinding(getLayoutInflater(), getClass())这个方法的核心是通过反射获得ViewBinding对象,具体如下:

3.4.1.3 获取父类的参数化类型
typescript
代码解读
复制代码
public static <VB extends ViewBinding> VB newViewBinding(LayoutInflater layoutInflater, Class clazz) { ParameterizedType type; try { type = (ParameterizedType) clazz.getGenericSuperclass(); } catch (ClassCastException e) { type = (ParameterizedType) Objects.requireNonNull(clazz.getSuperclass()).getGenericSuperclass(); } //......

image.png

image.png

ParameterizedType是一个接口,表示一个参数化的类型,例如Collection,也就是带有泛型的类型,我们可以用它来保存父类的泛型类型。getGenericSuperclass返回该实体的直接父类的Type,如果父类是一个带有类型参数的参数化类型,则返回的Type对象必须准确反映源代码中使用的实际类型参数。clazz就是BaseViewModelFragment的子类subFragment,那么clazz.getGenericSuperclass()返回的是subFragment的直接父类实例的Type,也就是BaseViewModelFragment。接着转换为ParameterizedType,它是Type的子接口,专门表示带有类型参数的泛型类型,转换后便可以使用ParameterizedType中的方法。

3.4.1.4 获取泛型参数
ini
代码解读
复制代码
assert type != null; Class clazzVB = (Class) type.getActualTypeArguments()[0];

type.getActualTypeArguments()[0]返回的是BaseViewModelFragment的泛型参数数组的第一个参数,也就是subFragmentBinding。接着转换为Class,以使用Class中的getMethod方法。

3.4.1.5 获取绑定了布局的ViewBinding对象
ini
代码解读
复制代码
Method inflateMethod = clazzVB.getMethod("inflate", LayoutInflater.class); return (VB) inflateMethod.invoke(null, layoutInflater); }

这段代码的意思是查找subFragmentBinding中的名为inflate,参数是LayoutInflater类型的方法,并返回该方法的Method对象,用于运行时调用该方法。

要注意的是,ViewBindging会自动生成数个inflate方法(public static VB inflate(LayoutInflater inflater)),传进来的LayoutInflater会调用LayoutInflater中的inflater方法,对象将xml文件转换为根视图(View),接着,ViewBindging自动生成的inflate方法再将其转换为ViewBinding子类对象(包含两个RecyclerView成员变量)。

image.png

image.png

最后通过反射调用(VB) inflateMethod.invoke(null, layoutInflater),inflate是静态方法,所以第一个参数是null。最后将返回的Object转换为ViewBinding对象。

3.4.2 为什么要在onCreate中创建binding

在onCreate中创建好binding后,我们继续实现之前BaseFragment中onCreateView里的getLayoutView抽象方法,也就是返回绑定的xml文件的根视图。

typescript
代码解读
复制代码
@Override protected View getLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return binding.getRoot(); }

onCreate是Fragment生命周期的早期方法,在onAttach后,onCreateView之前调用,为了确保在执行onCreateView时已经绑定好视图,所以binding必须在onCreate中初始化好,以防止空指针异常。

最后在onDestroyView中将binding设置为null,避免内存泄露。

4 实现首页Fragment

4.1 视图设置

我们的首页其实就是一个简单音乐列表,所以可以使用RecyclerView来实现布局。RecyclerView是 Android中用于展示长列表或网格数据的控件。RecyclerView的特点是视图回收机制,它可以节省内存和提高性能。简单来说,但你在滑动音乐列表时,滑出界面的视图并不会销毁然后重建,而是放入回收池(Recycled View Pool)重复利用。

RecyclerView布局长这样,每个item可以加载数据:

image.png

覆盖BaseFragment中的initViews,设置RecyclerView为固定宽高,在大数据量下就不会每次计算布局大小,提高性能。其中binding.list中的list是RecyclerView的xml控件,这里不再赘述。

scala
代码解读
复制代码
public class HomeFragment extends BaseViewModelFragment { @Override protected void initViews() { super.initViews(); setHasOptionsMenu(true); binding.list.setHasFixedSize(true); //......

设置LinearLayoutManager为布局管理器,默认为垂直排列。

ini
代码解读
复制代码
LinearLayoutManager layoutManager = new LinearLayoutManager(getHostActivity()); binding.list.setLayoutManager(layoutManager);

添加垂直方向的水平分割线。

ini
代码解读
复制代码
DividerItemDecoration decoration = new DividerItemDecoration(binding.list.getContext(), RecyclerView.VERTICAL); binding.list.addItemDecoration(decoration);

4.2 加载数据

在这个部分,我们覆盖initDatum,为RecyclerView绑定一个Adapter,告诉RecyclerView应该如何显示数据;随后发送网络请求获得音乐数据,更新Adapter数据并渲染在RecyclerView上。

scss
代码解读
复制代码
@Override protected void initDatum() { super.initDatum(); adapter = new HomeAdapter(R.layout.item_song); binding.list.setAdapter(adapter); loadData(); }

4.2.1 什么是Adapter

less
代码解读
复制代码
public class HomeAdapter extends BaseQuickAdapter<Song, BaseViewHolder> { public HomeAdapter(int layoutResId) { super(layoutResId); } @Override protected void convert(@NonNull BaseViewHolder holder, Song data) { ImageUtil.show(holder.getView(R.id.icon), data.getIcon()); holder.setText(R.id.title, data.getTitle()); holder.setText(R.id.info, String.format("%s-%s", data.getSinger(), data.getAlbum())); } }

Adpater是Android中的一种设计模式,负责将数据源绑定到RecyclerView,比如将一首音乐绑定到item,并有新数据时自动更新UI。这里使用BaseQuickAdapter,它是一个第三方库,继承并简化RecyclerView.Adapter的开发,它仅需要重写convert方法就能实现数据绑定。

4.2.1.1 convert调用时机

当你滚动屏幕时,如果一个视图是第一次出现,RecyclerView会调用onCreateViewHolder来创建新的ViewHolder,紧接着,BaseQuickAdapter会在onBindViewHolder方法中调用convert来绑定数据,正如上文所说,这个过程是BaseQuickAdapter自动完成的,我们仅需要重写convert方法就能实现数据绑定。

4.2.1.2 什么是BaseViewHolder

BaseViewHolder对RecyclerView.ViewHolder进行封装和扩展,简化了对RecyclerView的item元素进行访问和操作,比如代码中的setText。它拥有所有视图控件(如 TextView、ImageView 等),可以通过holder来获取和操作这些控件。

BaseViewHolder的缓存机制

BaseViewHolder的核心是它的缓存机制,可以提高视图的访问效率。每个BaseViewHolder都拥有一个SparseArray()器,用来缓存当前item的视图元素。在访问某个视图时,getView方法会检查该视图是否已经被缓存,如果缓存中有该视图,则直接返回缓存的视图;如果没有,则调用findViewById查找视图并缓存起来。

image.png

image.png

SparseArray()使用两个int数组mKeys和mValues,mKeys保存viewId,mValues保存View。存放键值时先使用二分法找出key的下标,然后再把key和value放入各自数组的相同下标。SparseArray()相比于更节省空间。

image.png

4.2.1.3 使用Glide显示图片

Glide是现代Android开发中加载图像的首选库,提供自动管理内存、磁盘缓存和生命周期等功能。根据上文所讲,我们应该把Glide的使用放在convert方法中:

scss
代码解读
复制代码
//封装好的类 RequestOptions options = getCommonRequestOptions(); Glide.with(view.getContext()) .load(data) .apply(options) .into(view); @SuppressLint("CheckResult") private static RequestOptions getCommonRequestOptions() { //为图片加载提供配置 RequestOptions options = new RequestOptions() //设置错误图(Error Image) .error(R.drawable.placeholder_error); //设置占位符图(Placeholder) //.placeholder(R.drawable.placeholder) //设置图像裁剪方式 //.centerCrop() //控制缓存行为,例如,指定不缓存任何图像: //.diskCacheStrategy(DiskCacheStrategy.NONE) return options; }

RequestOptions是Glide中用于配置图像加载选项的一个类,它可以定制图像加载过程中的各种行为,比如占位符、错误图、变换、裁剪等。

最后,给RecyclerView设置Adpter

ini
代码解读
复制代码
binding.list.setAdapter(adapter);

4.3 从网络加载数据

设置好Adpter后,就可以从网络获取音乐数据,并通过Adapter加载到RecyclerView。

在这里,我们使用OkHttp,Retrofit,RxJava来完成网络服务。

整体流程概括:

  1. OkHttp配置HTTP请求,包括连接管理、请求头处理、缓存、超时设置等。
  2. Retrofit构建网络请求,并将响应数据自动转换成Java对象,它的底层使用OkHttp发送HTTP请求。
  3. 为Retrofit配置RxJava,异步管理Retrofit的HTTP请求和响应。

Retrofit是一个用于Android的HTTP客户端,开发者通过接口定义请求方式和请求参数,Retrofit根据这些定义生成一个代理对象,并通过OkHttp执行实际的网络请求。当请求完成时,返回的数据会根据指定的Converter(比如Gson或Moshi)自动转换为Java对象。 image.png

OkHttp是一个高效的、功能强大的HTTP客户端库,它为网络通信提供了低层次的实现,支持同步和异步请求、连接池、缓存、重试、请求拦截器等特性。 image.png

image.png

RxJava是一个基于响应式编程(Reactive Programming)理念的Java库,它通过使用观察者模式(Observer Pattern)来简化异步和事件驱动的编程。借助RxJava,开发者可以轻松地处理数据流和异步操作,例如网络请求、用户界面事件等,通过“可观察的”数据序列(Observable)和“观察者”(Observer)之间的协作,实现数据的实时处理与响应。RxJava提供了丰富的操作符(Operators),如过滤、转换、合并等,使得复杂的数据处理逻辑变得简洁且易于维护。此外,RxJava还支持线程调度(Schedulers),帮助开发者在不同的线程间高效切换,优化应用的性能和响应速度。总体而言,RxJava极大地提升了Java应用的可扩展性和可维护性,是现代 Android和后端开发中广泛使用的强大工具。 image.png

4.3.1 封装Retrofit和OkHttp

首先,我们先封装好Retrofit和OkHttp,以提供OkHttp Client实例和Retrofit实例,并在里面配置好该配置的东西。

4.3.1.1 OkHttp Client配置

虽然Retrofit默认使用OkHttp作为HTTP客户端,但要实现一些自定义功能还是需要手动配置OkHttp。比如说:连接池,也就是相同地址的HTTP请求可能会共享一个连接,默认情况下最多保留5个空闲连接,这些连接将在5分钟无活动后被驱逐。对于我们的场景来说一般情况下很少会有高并发,所以使用默认连接池即可,无需手动配置。设置超时时间,设置连接超时时间,读写超时时间,这里使用默认的10秒。

ini
代码解读
复制代码
Context context = AppContext.getInstance(); OkHttpClient.Builder builder = new OkHttpClient.Builder(); // 基础配置 builder.connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS)
配置缓存
less
代码解读
复制代码
.cache(new Cache(context.getCacheDir(), Config.NETWORK_CACHE_SIZE));

在OkHttp Client中,将HTTP和HTTPS响应缓存到文件系统中,以便它们可以被重用,从而节省时间和带宽。注意这里配置的是磁盘缓存,而不是内存缓存,并且缓存的是后端返回的HTTP响应报文,不是音乐和图片本身,音乐缓存将由ExoPlayer管理,图片会持久化到数据库中,以后的文章会说。

这里要注意的是,虽然OkHttp提供了拦截器以及并可以让开发者修改响应头的缓存配置(过期时间,是否使用缓存等),但是OkHttp的官方文档并不建议客户端修改响应头:

image.png

所以这里设置好缓存目录和大小即可,让后端去配置缓存策略。

拦截器

网络拦截器

”观察单个网络请求和响应。这些拦截器必须调用Interceptor.Chain.proceed恰好一次。“

image.png

  1. 能够操作中间响应,如重定向和重试。 网络拦截器可以拦截并处理在请求过程中发生的中间响应。例如,当服务器返回一个重定向(如HTTP 301或302状态码)时,拦截器可以捕捉到这个响应并决定是否跟随重定向。此外,如果请求失败,拦截器还可以实现重试机制,以再次尝试发送请求。

  2. 不会在缓存响应(绕过网络)时被调用。 如果一个响应是从缓存中直接获取的,而不是通过网络请求获得的,网络拦截器不会被触发。这意味着网络拦截器只作用于实际通过网络发送和接收的数据,而不会影响缓存的数据。

  3. 观察数据的传输方式,正如它将在网络上进行传输一样。 网络拦截器能够查看和操作数据流,就像数据在网络上传输时一样。这意味着拦截器可以访问到即将发送到服务器的数据以及从服务器接收到的响应数据,从而可以进行监控、修改或记录。

  4. 可以访问承载请求的连接。 网络拦截器有权限访问用于发送请求的具体网络连接。这使得拦截器能够获取关于连接的详细信息,如连接的地址、端口、协议等,甚至可以对连接进行一些高级操作。

image.png

应用拦截器

拦截器观察每次调用的整个过程:从连接建立之前(如果有)直到选择响应源之后(无论是来自源服务器、缓存,还是两者)。 image.png

  1. 无需担心中间响应,如重定向和重试。 应用拦截器不需要处理在请求过程中可能出现的中间响应,例如服务器返回的重定向(如HTTP 301或302状态码)或自动重试机制。这些中间响应由网络拦截器或HTTP客户端本身处理,应用拦截器只需关注最终的请求和响应。

  2. 即使HTTP响应来自缓存,也始终只调用一次。 无论响应是从网络获取的还是从缓存中读取的,应用拦截器都会被调用一次。这意味着应用拦截器能够一致地处理所有响应,而不需要关心响应的来源。

  3. 观察应用程序的原始意图。不关心OkHttp注入的头信息,如If-None-Match。 应用拦截器专注于应用程序发出的原始请求,不会关心由OkHttp或其他底层库自动添加的头信息(例如用于缓存验证的If-None-Match)。这使得应用拦截器能够更专注于应用层面的逻辑,而不被底层实现细节干扰。

  4. 允许短路并且不调用Chain.proceed()。 应用拦截器有权决定是否继续执行请求链。如果拦截器决定不继续传递请求(即“短路”),它可以直接返回一个响应,而无需调用Chain.proceed()方法。这在某些情况下非常有用,例如在满足特定条件时直接返回缓存响应,而不进行实际的网络请求。

  5. 允许重试并多次调用Chain.proceed()。 应用拦截器可以选择重试请求,通过多次调用Chain.proceed()方法来重新发送请求。这在处理临时性错误或需要根据特定逻辑重试请求时非常有用。例如,如果第一次请求失败,拦截器可以自动重试一次或多次。

  6. 可以使用withConnectTimeout、withReadTimeout、withWriteTimeout调整调用的超时设置。 应用拦截器可以动态调整网络请求的超时设置,包括连接超时 (withConnectTimeout)、读取超时 (withReadTimeout) 和写入超时 (withWriteTimeout)。这允许根据不同的请求需求或网络状况,灵活地配置超时时间,以提高应用的健壮性和用户体验。

image.png

配置HTTPS

SSLContext和X509TrustManager是HTTPS安全通信的核心组件。

X509TrustManager是证书验证的核心接口,它负责验证服务器证书链的有效性,证书是否过期,域名是否匹配,证书颁发机构 (CA) 是否受信任。

getDefaultAlgorithm()返回默认算法(通常是PKIX)。

init(null)表示加载系统默认的信任库(即Android系统预装的CA证书),如Let's Encrypt、DigiCert 等,这些证书存储在系统级信任库中,应用默认会信任这些 CA 颁发的证书。

java
代码解读
复制代码
static X509TrustManager getSystemTrustManager() throws NoSuchAlgorithmException, KeyStoreException { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { if (trustManager instanceof X509TrustManager) { return (X509TrustManager) trustManager; } } throw new IllegalStateException("No X509TrustManager found"); }

SSLContext是Java中用于配置SSL/TLS协议的核心类,它定义了加密套件、协议版本、密钥管理和信任管理等参数。

"TLS"表示使用TLS协议的最新兼容版本,Android 会根据系统版本自动选择(如TLSv1.2或TLSv1.3)。

第一个参数null表示不指定密钥管理器 (KeyManager)。密钥管理器用于客户端身份认证(如双向 TLS),在此场景中不需要。

第二个参数TrustManager数组通过getSystemTrustManager()获取系统默认的信任管理器,用于验证服务器证书链的有效性。

第三个参数SecureRandom提供加密强随机数生成器,用于SSL/TLS握手过程中的随机数生成。

csharp
代码解读
复制代码
static SSLContext getSSLContext() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{getSystemTrustManager()}, new java.security.SecureRandom()); return sslContext; }

配置OkHttp的SSL

ini
代码解读
复制代码
SSLContext sslContext = getSSLContext(); X509TrustManager trustManager = getSystemTrustManager(); builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager);

TLS是一种密码学协议,作用于计算机网络模型的传输层(OSI第4层/TCP/IP模型),核心目标是为客户端-服务器通信提供:

数据机密性(Confidentiality) 通过对称加密算法(如AES、ChaCha20)加密传输内容。

数据完整性(Integrity) 使用MAC(消息认证码)或AEAD(认证加密关联数据)机制,防止数据被篡改。

端点身份认证(Authentication) 基于X.509数字证书体系验证服务器身份(可选验证客户端身份)。

完整安全链路的工作流程

  1. 客户端发起HTTPS请求。OkHttp使用配置的SSLSocketFactory创建SSL套接字。

  2. TLS握手。协商加密协议版本(如 TLSv1.3);协商加密套件(如 AES_256_GCM_SHA384);交换随机数、生成会话密钥。

  3. 证书验证。服务器发送证书链;X509TrustManager检查证书链是否由受信任的CA签发,证书是否过期,域名是否匹配(通过SNI扩展)。

  4. 加密通信。握手成功后,所有数据传输使用对称加密。

Token配置

我们把Token保存在EncryptedSharedPreferences中,它是一个对键和值进行加密的SharedPreferences。

scss
代码解读
复制代码
public void setAccessToken(String token) { preference.edit().putString(KEY_ACCESS_TOKEN, token).apply(); } public String getAccessToken() { return preference.getString(KEY_ACCESS_TOKEN, null); } public void setRefreshToken(String token) { preference.edit() .putString(KEY_REFRESH_TOKEN, token) .apply(); } public String getRefreshToken() { return preference.getString(KEY_REFRESH_TOKEN, null); } // expirationTime表示Access Token的过期时间 public void setTokenExpiration(long expiresIn) { long expirationTime = System.currentTimeMillis() + (expiresIn * 1000); preference.edit() .putLong(KEY_TOKEN_EXPIRATION, expirationTime) .apply(); } public boolean isTokenValid() { long expirationTime = preference.getLong(KEY_TOKEN_EXPIRATION, 0); return System.currentTimeMillis() < expirationTime; } public boolean isLoggedIn() { return getAccessToken() != null && isTokenValid(); } // 登出时清理 public void clearAuth() { preference.edit() .remove(KEY_ACCESS_TOKEN) .remove(KEY_REFRESH_TOKEN) .remove(KEY_TOKEN_EXPIRATION) .apply(); }
Token拦截器

用户登录后,需要把所有API请求注入令牌,服务端通过JWT解析用户身份。

arduino
代码解读
复制代码
builder.addInterceptor(new AuthInterceptor(context))

使用Bearer Token认证方案,符合OAuth2标准:

java
代码解读
复制代码
public class AuthInterceptor implements Interceptor { private final PreferenceUtil preference; AuthInterceptor(Context context) throws GeneralSecurityException, IOException { this.preference = PreferenceUtil.getInstance(context); } @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { Request original = chain.request(); if (!preference.isLoggedIn()) { return chain.proceed(original); } Request request = original.newBuilder() .header("Authorization", "Bearer " + preference.getAccessToken()) .build(); return chain.proceed(request); } }
Token刷新机制

当客户端向服务端发送请求,如果服务端返回HTTP 401 Unauthorized(通常表示Token过期或失效),比如Token每小时自动过期,或Token在离线期间过期,我们又应该如何处理呢?我们可以实现OkHttp的Authenticator类,在Token失效时,自动刷新Token(Refresh Token)获取新Token,实现无感知续期。

rust
代码解读
复制代码
sequenceDiagram participant Client as 客户端 participant OkHttp as OkHttp participant AuthServer as 认证服务器 Client->>OkHttp: 发起API请求(携带旧access_token) OkHttp->>AuthServer: 请求受保护资源 AuthServer-->>OkHttp: 返回401 Unauthorized OkHttp->>TokenRefreshAuthenticator: 触发authenticate()方法 TokenRefreshAuthenticator->>AuthServer: 发送refresh_token换取新令牌 AuthServer-->>TokenRefreshAuthenticator: 返回新access_token/refresh_token TokenRefreshAuthenticator->>Client: 更新本地令牌存储 TokenRefreshAuthenticator->>OkHttp: 修改请求头(注入新令牌) OkHttp->>AuthServer: 自动重试原始请求 AuthServer-->>Client: 返回正常响应

首先需要一个控制器控制刷新次数,防止因服务端故障导致无限重试循环:

less
代码解读
复制代码
// 添加独立的重试计数器 private int retryCount = 0; @Nullable @Override public Request authenticate(@Nullable Route route, @NonNull okhttp3.Response response) throws IOException { // 最大重试次数限制 if (retryCount++ >= 2) { clearAuthState(); return null; } // ...

Token刷新流程:

refresh_token(刷新Token或长期Token):仅允许通过HTTPS传输。由授权服务器(Authorization Server)颁发的长期Token(通常有效期数天至数月),专门用于获取新的access_token。它具有单次有效性,多数实现采用Refresh Token Rotation机制,每次刷新后旧refresh_token立即失效,防止重复使用。并且与特定client_id绑定,防止跨客户端滥用。

csharp
代码解读
复制代码
Map params = new HashMap<>(); params.put("grant_type", "refresh_token"); // OAuth 2.0 标准参数 params.put("refresh_token", refreshToken); // 长期有效令牌 params.put("client_id", Config.CLIENT_ID); // 客户端身份标识

配置独立网络,避免业务API的拦截器(如日志、缓存)影响认证请求:

scss
代码解读
复制代码
/** * 创建专用认证服务实例 * 与常规API服务隔离,实现更严格的安全策略 */ private AuthService createAuthService() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { // 创建独立OkHttpClient实例 OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(NetworkModule.getSSLContext().getSocketFactory(), NetworkModule.getSystemTrustManager()) .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .build(); // 构建专用Retrofit例 return new Retrofit.Builder() .baseUrl(Config.ENDPOINT) .client(client) .addConverterFactory(GsonConverterFactory.create(JSONUtil.createGson())) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) // 要用异步适配器 .build() .create(AuthService.class); }

返回新access_token + 新refresh_token:

ini
代码解读
复制代码
Response response = authService.refreshToken(params).execute(); if (response.isSuccessful()) { TokenResponse tokenResponse = response.body(); // 防止多线程同时修改令牌导致状态不一致 assert tokenResponse != null; synchronized (TokenRefreshAuthenticator.class) { preference.setAccessToken(tokenResponse.getAccessToken()); preference.setRefreshToken(tokenResponse.getRefreshToken()); } preference.setTokenExpiration(tokenResponse.getExpiresIn()); return tokenResponse.getAccessToken(); } else { //处理错误响应,清除认证状态等 }

刷新成功获取新AccessToken:

scss
代码解读
复制代码
// 保存新 Token preference.setAccessToken(newToken); // 重新构建请求 return response.request().newBuilder() .header("Authorization", "Bearer " + newToken) .build();
4.3.1.2 配置Retrofit

主要为Retrofit设置刚刚配置好的OkHttp Client,api地址,RxJava,并使用换java和JSON。

4.3.2 创建服务接口

Retrofit还需要一个API接口来创建实例,这个接口就是使用GET,POST之类的注解来定义请求。

less
代码解读
复制代码
public interface DefaultService { @GET("/songs") Observable> songs(@Query("limit") int limit); }

相当于https://api.example.com/songs?limit=10

接着,使用动态代理为retrofit配置这个service接口。当调用DefaultService中的方法时,Retrofit通过动态代理生成一个实现了DefaultService接口的代理类。

ini
代码解读
复制代码
service = retrofit.create(DefaultService.class);

4.3.3 RxJava

可以看到,上面的service接口返回的是一个Observable,它是RxJava的一个核心类,从service接口返回到为adapter设置数据的过程这样的:

less
代码解读
复制代码
@GET("/songs") Observable> songs(@Query("limit") int limit); service.songs(10) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new HttpObserver>() { @Override public void onSucceeded(ListResponse data) { adapter.setNewInstance(data.getData().getData()); } });
4.3.3.1 Observable

先来看看官方文档的解释:

image.png

image.png

简单概括,我们定义一个观察者Observer,对应于上面的代码new HttpObserver<>()就是一个自定义的 Observer实现。里面有四个方法onSubscribe,onNext onError,onComplete如下图,当然还可以自定义自己的方法。

接着,我们定义一个事件源(Observable),对应于:

less
代码解读
复制代码
@GET("/songs") Observable> songs(@Query("limit") int limit);

image.png

最后观察者订阅事件源,当事件源发出事件,将被观察者捕获,并做出响应(onNext or onError ?):

scss
代码解读
复制代码
service.songs(10) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new HttpObserver>() { // 对事件做出响应 @Override public void onSucceeded(ListResponse data) { adapter.setNewInstance(data.getData().getData()); } }
4.3.3.2 subscribeOn, observeOn

官方文档是这么说的:"SubscribeOn操作符决定了Observable的初始运行线程,无论该操作符在操作符链中的何处调用。而 ObserveOn则影响的是在其调用位置之后的线程环境。这意味着你可以在操作符链中的多个位置调用 ObserveOn,以更改特定操作符运行的线程。"

也就是说,如图中即便在下方调用subscribeOn,影响的也是顶部的线程(变成了蓝色);而ObserveOn则立即改变下一个线程。 image.png

回到我们的代码:

less
代码解读
复制代码
@GET("/songs") 这个songs↓ Observable> songs(@Query("limit") int limit); service.songs(10) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread());

service.songs(10),执行到这里时,网络请求并没发出(Cold observale),Retrofit只是创建了一个Observable>对象。在Retrofit中,Observable本质上是Call的一个封装,Retrofit通过RxJavaCallAdapterFactory将Call对象包装成了一个Observable对象。

当调用service.songs(10)时,Retrofit通过代理方法将请求的详细信息(例如请求的URL、参数)封装到一个Call对象中,然后通过RxJavaCallAdapterFactory转换为Observable,并交给RxJava管理。

如上图所说,subscribeOn(Schedulers.io())决定了Observable,也就是网络请求最开始将在哪个线程执行,而Schedulers.io()指定了将在IO线程池中执行,这个线程池的线程数量是动态的,线程池中的线程会根据需要自动创建和销毁,因此可以支持大量的并发IO操作。线程池中的线程会被复用,而不会频繁创建新的线程。

接着,Observable接收到网络相响应,.observeOn(AndroidSchedulers.mainThread())将下一步的操作切换到主线程(如上图的变色),因为Android系统规定了所有涉及界面更新的操作都只能在主线程执行。

4.3.3.3 subscribe

“订阅 操作符用于处理Observable发射的事件和通知。它是将 观察者(Observer)与Observable连接起来的“纽带”。为了使观察者能够看到Observable发射的项,或者接收到Observable发出的错误通知或完成通知,必须首先使用 订阅 操作符订阅该Observable。” image.png

回到我们的代码

less
代码解读
复制代码
@GET("/songs") Observable> songs(@Query("limit") int limit); service.songs(10) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new HttpObserver>() { @Override public void onSucceeded(ListResponse data) { adapter.setNewInstance(data.getData().getData()); } });

只有被订阅(subscribe)后,songs(10)返回的Observable>才会真正开始执行网络请求,因为这是一个冷(Cold)的Observable:

image.png

当接收到网络请求后,切换为主线程,并开始执行观察者里的方法,首先是onNext

scss
代码解读
复制代码
@Override public void onNext(T t) { super.onNext(t); if (isSucceeded(t)) { //请求正常,并执行后续任务 onSucceeded(t); } else { //请求出错,处理业务逻辑错误 handleRequest(t, null); } }

其中,onSucceeded(t)为adapter设置新的数据实例,替换原有内存引用。

接着,来看看网络请求失败/错误的处理

onFailed默认返回false,如果要在HttpObserver重写该方法来处理错误,记得改为返回true。

vbnet
代码解读
复制代码
private void handleRequest(T data, Throwable error) { if (onFailed(data, error)) { // onFailed默认返回 false,所以直接进入else语句 } else { ExceptionHandlerUtil.handlerRequest(data, error); } }

如果只是业务逻辑错误,比如用户未认证等,就会触发onNext中的handleRequest(t, null),注意Throwable参数为null;如果是网络异常,也就是Observable遇到了错误,将不会调用onNext方法,而是调用onError,注意handleRequest第一个参数为null。

scss
代码解读
复制代码
@Override public void onNext(T t) { super.onNext(t); if (isSucceeded(t)) { //请求正常 onSucceeded(t); } else { //请求出错了,处理业务逻辑错误 handleRequest(t, null); } } @Override public void onError(Throwable e) { super.onError(e); // 处理网络或系统异常 handleRequest(null, e); }
typescript
代码解读
复制代码
public static void handlerRequest(T data, Throwable error) { if (error != null) { // 处理exception handleException(error); } else{ // ... }

处理exception,不同类型的错误会根据异常的类型显示相应的错误信息:

scss
代码解读
复制代码
public static void handleException(Throwable error) { if (error instanceof UnknownHostException) { // 无法连接到主机(如`DNS`解析失败) SuperToast.show(R.string.error_network_unknown_host); } else if (error instanceof ConnectException) { // 网络连接失败 SuperToast.show(R.string.network_error); } else if (error instanceof SocketTimeoutException) { // 请求超时 SuperToast.show(R.string.error_network_timeout); } else if (error instanceof HttpException) { // HTTP 错误 HttpException exception = (HttpException) error; int code = exception.code(); handleHttpError(code); } else if (error instanceof IllegalArgumentException) { // 本地参数错误 SuperToast.show(R.string.error_parameter); }else{ // 其他未知错误则显示通用错误信息 String message = error.getLocalizedMessage(); if (StringUtils.isNotBlank(message)) { message = AppContext.getInstance().getString(R.string.error_unknown_format,message); }else{ message = AppContext.getInstance().getString(R.string.error_unknown); } SuperToast.show(message); } }

接着处理非exception,这里是HTTP错误处理:

csharp
代码解读
复制代码
private static void handleHttpError(int code) { if (code == 401) { // 未授权,触发用户登出 SuperToast.show(R.string.error_network_not_auth); AppContext.getInstance().logout(); } else if (code == 403) { // 无权限 SuperToast.show(R.string.error_network_not_permission); } else if (code == 404) { // 未找到 SuperToast.show(R.string.error_network_not_found); } else if (code >= 500) { // 服务器错误 SuperToast.show(R.string.error_network_server); } else { SuperToast.show(R.string.error_unknown); } }
4.3.3.4 完善网络错误处理

以上处理只是弹框提示用户出错了,我i们还可以再完善一下,比如网络错误时自动重试,未登录时自动跳转登录页面。

网络错误时自动重试

首先梳理一下逻辑,在进行网络请求时,如果抛出网络异常(UnknownHostException,ConnectException等),我们可以捕获这个异常并进行3次延迟重试,如果还是无法成功,则将onError抛向下游。

我们可以使用RxJava的retryWhen操作符,并自定义辅助类来实现。

scss
代码解读
复制代码
public Observable> songs() { return service.songs(10) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .retryWhen(new RetryWithDelay(MAX_RETRIES, RETRY_DELAY)); }
retryWhen

先来读读它的API文档:

image.png

image.png

老外老是喜欢搞这种长难句就问你服不服,我来试试翻译下:“返回一个Observable,这个Observable与源Observable发射相同值除了OnError。源Observable发出onError通知,会导致一个Throwable发送给Observable,并且这个Observable会作为notificationHandler函数的参数。如果这个Observable调用OnComplete or OnError,retry接着会在子订阅调用它们。否则当前Observable会被重订阅。”

“请注意,handler函数返回的内部ObservableSource应该对接收到的Throwable发出onNext、onError或onComplete信号,以指示操作符是否应该重试或终止。如果操作符的上游是异步的,那么立即发出onNext然后紧接着发出onComplete可能会导致序列立即完成。类似地,如果这个内部的 ObservableSource在上游仍然处于活动状态时发出onError或onComplete,序列将立即以相同的信号终止。”

看不懂正常,我们再来看看它的签名:

image.png

可以看到它使用Function, ? extends ObservableSource>作为参数,也就是一个handler。Function又是啥?

可以看到,Function接口有两个类型参数,第一个是input,第二个是output,apply对input进行一些计算并返回output。 image.png

总结一下retryWhen,它接受一个实现了Function接口的handler,当源Observable发出onError,抛出的Throwable封装成Observable并在apply接口方法里进行逻辑处理,根据处理结果返回一个新的ObservableSource(Observable实现了ObservableSource接口)。如果这个ObservableSource发出:

  • onNext: retryWhen重新订阅源Observable,也就是重试。
  • onError或onComplete信号:retryWhen会将相同的信号传递给下游观察者,终止整个数据流。

接着,我们来看看retryWhen里的handler怎么实现:

java
代码解读
复制代码
public class RetryWithDelay implements Function, Observable> { // 最大重试次数 private final int maxRetries; // 重试延迟 private final int retryDelaySeconds; private int retryCount; public RetryWithDelay(int maxRetries, int retryDelaySeconds) { this.maxRetries = maxRetries; this.retryDelaySeconds = retryDelaySeconds; this.retryCount = 0; } @Override public Observable apply(Observable exception) { return exception.flatMap((Function>) throwable -> { // 判断重试次数与是否是网络异常 if (++retryCount <= maxRetries && isNetworkError(throwable)) { // 进行重试 return Observable.timer(retryDelaySeconds, TimeUnit.SECONDS); } // 超过重试次数或不是网络错误,终止重试 return Observable.error(throwable); }); } private boolean isNetworkError(Throwable throwable) { return throwable instanceof UnknownHostException || throwable instanceof ConnectException || throwable instanceof SocketTimeoutException; } }

这个handler的核心是apply方法。它可以捕获源Observable发出的onError,也就是Observable exception,接着,对它使用flatMap方法:

image.png

FlatMap是一种高级的转换操作符,用于将一个Observable发出的每个项目转换为另一个Observable,然后将这些内部Observables的发射结果合并到一个单一的Observable中。

flatMap内部有个lambda表达式,对捕获的exception进行逻辑判断(转换),并返回一个新的Observable:小于重试次数并属于网络异常,则发出一个onNext(Observable.timer(...))给retryWhen重新订阅源Observable:

  • 如果网络请求成功,源Observable发出onNext信号,随后发出onComplete信号。因为没有onError 信号,retryWhen不再介入,整个数据流正常结束。
  • 如果超过重试次数还没成功,flatMap发出onError(Observable.error),retryWhen将错误传递给下游观察者,终止整个数据流,执行handleRequest(null, e)。
登录过期自动跳转登录页面

这部分比较简单,登录过期属于业务逻辑错误,而不是网络异常,所以在handleHttpError判断响应码为401,就使用EventBus发送全局事件:

scss
代码解读
复制代码
EventBus.getDefault().post(new AuthErrorEvent());

并在BaseLogicFragment跳转即可:

scss
代码解读
复制代码
@Subscribe(threadMode = ThreadMode.MAIN) public void onAuthErrorEvent(AuthErrorEvent event) { // 跳转到登录界面 ((BaseLogicActivity) getHostActivity()).startLoginActivity(); }
4.3.3.5 取消订阅

最后,我们的代码是存在内存泄露风险的,原因在于HttpObserver 是一个匿名内部类,它会会隐式持有其外部类 (DiscoveryFragment) 的引用,当 DiscoveryFragment被销毁时,订阅仍然持有对它的引用,导致DiscoveryFragment 无法被垃圾回收,从而引发内存泄漏。

为防止存泄漏,需要在生命周期内取消订阅。这里我们使用RxJava的CompositeDisposable容器中的clear来销毁所有disposable容器。

image.png

java
代码解读
复制代码
private CompositeDisposable compositeDisposable = new CompositeDisposable(); @Override public void onDestroyView() { super.onDestroyView(); // 取消所有订阅,避免内存泄漏 compositeDisposable.clear(); }
4.3.3.6 RxJava总结

image.png

RxJava基于ReactiveX:

“ReactiveX是一个用于通过使用可观察序列来组合异步和基于事件的程序的库。 它扩展了观察者模式,以支持数据和/或事件的序列,并添加了操作符,使您能够以声明式的方式组合这些序列,同时抽象掉诸如低级线程管理、同步、线程安全、并发数据结构和非阻塞 I/O 等方面的关注点。”

观察者模式

观察者模式(Observer Pattern)是一种设计模式,用于在对象之间建立一种一对多的依赖关系。当一个对象(称为“主题”或“被观察者”)的状态发生变化时,所有依赖于它的对象(称为“观察者”或“订阅者”)都会自动收到通知并更新。

观察者模式的核心组成部分

主题(Subject):

  • 维护一组观察者。
  • 提供方法让观察者注册(订阅)和注销(取消订阅)。
  • 在自身状态发生变化时,通知所有注册的观察者。

观察者(Observer):

  • 接收并处理来自主题的通知。
  • 通常包含一个更新方法(如update()),当主题状态改变时被调用。

例子:

csharp
代码解读
复制代码
interface Observer { void update(String event); } class EventSource { List observers = new ArrayList<>(); public void notifyObservers(String event) { observers.forEach(observer -> observer.update(event)); } public void addObserver(Observer observer) { observers.add(observer); } public void scanSystemIn() { Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String line = scanner.nextLine(); notifyObservers(line); } } } public class ObserverDemo { public static void main(String[] args) { System.out.println("Enter Text: "); EventSource eventSource = new EventSource(); eventSource.addObserver(event -> System.out.println("Received response: " + event)); eventSource.scanSystemIn(); } }

Subject(或者类似RxJava的Observabe):

EventSource类,维护观察者列表,有事件发出时通知观察者

typescript
代码解读
复制代码
class EventSource { List<Observer> observers = new ArrayList<>(); public void notifyObservers(String event) { observers.forEach(observer -> observer.update(event)); } public void addObserver(Observer observer) { observers.add(observer); } public void scanSystemIn() { Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String line = scanner.nextLine(); notifyObservers(line); } } }

观察者(Observer):

通过Lambda表达式定义了一个观察者:

csharp
代码解读
复制代码
eventSource.addObserver(event -> System.out.println("Received response: " + event));

其中addObserver的参数是一个Observer接口,Lambda表达式实现了这个接口的updata方法:

csharp
代码解读
复制代码
public void addObserver(Observer observer) { observers.add(observer); }

整体流程:

  1. 创建Subject
  2. 使用Lambda表达式创建Observer,本身就实现了Observer的接口方法
  3. Subject通知所有Observer,执行它们自己的方法
注:本文转载自juejin.cn的yyh888的文章"https://juejin.cn/post/7457566084138631178"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

140
Android
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top