1. 概述
效果类似下图。
2. 什么是Fragment
我们来看官方文档对fragment的解释,我的理解是activity适合用来做一个整体的界面,而fragment适合做线性布局的界面。
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()。
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()
onCreateView()是Fragment的生命周期方法之一,它在onCreate()方法后调用,它负责加载布局并返回视图。重写该方法并调用getLayoutView()抽象方法,子类需要实现这个方法,后面会讲到,在BaseViewModelFragment类中实现了该方法,使用ViewBinding代替传统的LayoutInflater来简化视图的绑定。
3.1.2 初始化操作:onViewCreated()
onViewCreated()在onCreateView()返回后立即调用,在视图创建后进行额外的初始化操作,重写该方法加入了三个初始化步骤供子类实现:initViews()初始化视图,initDatum()数据加载,initListeners()设置化监听器。
这三个方法被称为钩子方法(Hook Method)。钩子方法是设计模式中的一种概念,通常是指在库、父类中中预留而不执行实际操作,供子类扩展,覆盖并实现的方法。三个初始化方法职责分明,并且将这些通用的初始化逻辑封装到这个父类中,可以确保每个Fragment按照统一的流程执行初始化。
3.2 BaseCommonFragment
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。
getHostActivity()将宿主Activity返回,并强转为通用的BaseCommonActivity,使得调用者可以使用BaseCommonActivity中的方法。startActivity()使用Intent对象来启动一个Activity,Intent使用当前上下文getHostActivity()(相当于从哪里启动)和目标Activity作为参数。
3.3 BaseLogicFragment
这个Fragment类抽象为负责处理与业务相关的逻辑。
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
简单来说,EventBus是一个事件总线库,使用发布/订阅模式(publish-subscribe pattern)在Activity, Fragment, Service之间收发事件。发布:post()方法,订阅:带有@Subscribe注解的方法,方法的参数类型就是要就订阅的事件类型,事件可以是任何类型的java对象。事件会在主线程或者后台线程中处理。
//......
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
public abstract class BaseViewModelFragment<VB 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<subFragment>),那么binding就是subFragment的一个实例,可以在subFragment中使用它来访问视图控件。
3.4.1 ViewBinding对象的创建
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()返回的是什么?
getClass()返回的是当前实例的运行时类,也就是说,如果有一个子类继承自BaseViewModelFragment,即便getClass()是在BaseViewModelFragment中使用,但是实际调用的还是它的子类,这里是基于java的动态绑定机制。动态绑定机制的核心是,方法调用在运行时根据对象的实际类型来决定。也就是说,即便方法调用发生在父类,调用的行为也是根据子类的类型来执行。具体的,某个子类subFragment继承自BaseViewModelFragment,所以是subFragment的实例调用getClass(),并返回subFragment.class。
ReflectUtil.newViewBinding(getLayoutInflater(), getClass())这个方法的核心是通过反射获得ViewBinding对象,具体如下:
3.4.1.3 获取父类的参数化类型
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();
}
//......
ParameterizedType是一个接口,表示一个参数化的类型,例如Collection<String>,也就是带有泛型的类型,我们可以用它来保存父类的泛型类型。getGenericSuperclass返回该实体的直接父类的Type,如果父类是一个带有类型参数的参数化类型,则返回的Type对象必须准确反映源代码中使用的实际类型参数。clazz就是BaseViewModelFragment的子类subFragment,那么clazz.getGenericSuperclass()返回的是subFragment的直接父类实例的Type,也就是BaseViewModelFragment<subFragmentBinding>。接着转换为ParameterizedType,它是Type的子接口,专门表示带有类型参数的泛型类型,转换后便可以使用ParameterizedType中的方法。
3.4.1.4 获取泛型参数
assert type != null;
Class<VB> clazzVB = (Class<VB>) type.getActualTypeArguments()[0];
type.getActualTypeArguments()[0]返回的是BaseViewModelFragment<subFragmentBinding>的泛型参数数组的第一个参数,也就是subFragmentBinding。接着转换为Class<VB>,以使用Class中的getMethod方法。
3.4.1.5 获取绑定了布局的ViewBinding对象
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成员变量)。
最后通过反射调用(VB) inflateMethod.invoke(null, layoutInflater),inflate是静态方法,所以第一个参数是null。最后将返回的Object转换为ViewBinding对象。
3.4.2 为什么要在onCreate中创建binding
在onCreate中创建好binding后,我们继续实现之前BaseFragment中onCreateView里的getLayoutView抽象方法,也就是返回绑定的xml文件的根视图。
@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可以加载数据:
覆盖BaseFragment中的initViews,设置RecyclerView为固定宽高,在大数据量下就不会每次计算布局大小,提高性能。其中binding.list中的list是RecyclerView的xml控件,这里不再赘述。
public class HomeFragment extends BaseViewModelFragment<FragmentHomeBinding> {
@Override
protected void initViews() {
super.initViews();
setHasOptionsMenu(true);
binding.list.setHasFixedSize(true);
//......
设置LinearLayoutManager为布局管理器,默认为垂直排列。
LinearLayoutManager layoutManager = new LinearLayoutManager(getHostActivity());
binding.list.setLayoutManager(layoutManager);
添加垂直方向的水平分割线。
DividerItemDecoration decoration = new DividerItemDecoration(binding.list.getContext(), RecyclerView.VERTICAL);
binding.list.addItemDecoration(decoration);
4.2 加载数据
在这个部分,我们覆盖initDatum,为RecyclerView绑定一个Adapter,告诉RecyclerView应该如何显示数据;随后发送网络请求获得音乐数据,更新Adapter数据并渲染在RecyclerView上。
@Override
protected void initDatum() {
super.initDatum();
adapter = new HomeAdapter(R.layout.item_song);
binding.list.setAdapter(adapter);
loadData();
}
4.2.1 什么是Adapter
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查找视图并缓存起来。
SparseArray()使用两个int数组mKeys和mValues,mKeys保存viewId,mValues保存View。存放键值时先使用二分法找出key的下标,然后再把key和value放入各自数组的相同下标。SparseArray()相比于更节省空间。
4.2.1.3 使用Glide显示图片
Glide是现代Android开发中加载图像的首选库,提供自动管理内存、磁盘缓存和生命周期等功能。根据上文所讲,我们应该把Glide的使用放在convert方法中:
//封装好的类
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
binding.list.setAdapter(adapter);
4.3 从网络加载数据
设置好Adpter后,就可以从网络获取音乐数据,并通过Adapter加载到RecyclerView。
在这里,我们使用OkHttp,Retrofit,RxJava来完成网络服务。
整体流程概括:
OkHttp配置HTTP请求,包括连接管理、请求头处理、缓存、超时设置等。Retrofit构建网络请求,并将响应数据自动转换成Java对象,它的底层使用OkHttp发送HTTP请求。- 为
Retrofit配置RxJava,异步管理Retrofit的HTTP请求和响应。
Retrofit是一个用于Android的HTTP客户端,开发者通过接口定义请求方式和请求参数,Retrofit根据这些定义生成一个代理对象,并通过OkHttp执行实际的网络请求。当请求完成时,返回的数据会根据指定的Converter(比如Gson或Moshi)自动转换为Java对象。
OkHttp是一个高效的、功能强大的HTTP客户端库,它为网络通信提供了低层次的实现,支持同步和异步请求、连接池、缓存、重试、请求拦截器等特性。
RxJava是一个基于响应式编程(Reactive Programming)理念的Java库,它通过使用观察者模式(Observer Pattern)来简化异步和事件驱动的编程。借助RxJava,开发者可以轻松地处理数据流和异步操作,例如网络请求、用户界面事件等,通过“可观察的”数据序列(Observable)和“观察者”(Observer)之间的协作,实现数据的实时处理与响应。RxJava提供了丰富的操作符(Operators),如过滤、转换、合并等,使得复杂的数据处理逻辑变得简洁且易于维护。此外,RxJava还支持线程调度(Schedulers),帮助开发者在不同的线程间高效切换,优化应用的性能和响应速度。总体而言,RxJava极大地提升了Java应用的可扩展性和可维护性,是现代 Android和后端开发中广泛使用的强大工具。
4.3.1 封装Retrofit和OkHttp
首先,我们先封装好Retrofit和OkHttp,以提供OkHttp Client实例和Retrofit实例,并在里面配置好该配置的东西。
4.3.1.1 OkHttp Client配置
虽然Retrofit默认使用OkHttp作为HTTP客户端,但要实现一些自定义功能还是需要手动配置OkHttp。比如说:连接池,也就是相同地址的HTTP请求可能会共享一个连接,默认情况下最多保留5个空闲连接,这些连接将在5分钟无活动后被驱逐。对于我们的场景来说一般情况下很少会有高并发,所以使用默认连接池即可,无需手动配置。设置超时时间,设置连接超时时间,读写超时时间,这里使用默认的10秒。
Context context = AppContext.getInstance();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// 基础配置
builder.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
配置缓存
.cache(new Cache(context.getCacheDir(), Config.NETWORK_CACHE_SIZE));
在OkHttp Client中,将HTTP和HTTPS响应缓存到文件系统中,以便它们可以被重用,从而节省时间和带宽。注意这里配置的是磁盘缓存,而不是内存缓存,并且缓存的是后端返回的HTTP响应报文,不是音乐和图片本身,音乐缓存将由ExoPlayer管理,图片会持久化到数据库中,以后的文章会说。
这里要注意的是,虽然OkHttp提供了拦截器以及并可以让开发者修改响应头的缓存配置(过期时间,是否使用缓存等),但是OkHttp的官方文档并不建议客户端修改响应头:
所以这里设置好缓存目录和大小即可,让后端去配置缓存策略。
配置HTTPS
SSLContext和X509TrustManager是HTTPS安全通信的核心组件。
X509TrustManager是证书验证的核心接口,它负责验证服务器证书链的有效性,证书是否过期,域名是否匹配,证书颁发机构 (CA) 是否受信任。
getDefaultAlgorithm()返回默认算法(通常是PKIX)。
init(null)表示加载系统默认的信任库(即Android系统预装的CA证书),如Let's Encrypt、DigiCert 等,这些证书存储在系统级信任库中,应用默认会信任这些 CA 颁发的证书。
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握手过程中的随机数生成。
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
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数字证书体系验证服务器身份(可选验证客户端身份)。
完整安全链路的工作流程
-
客户端发起HTTPS请求。OkHttp使用配置的SSLSocketFactory创建SSL套接字。
-
TLS握手。协商加密协议版本(如 TLSv1.3);协商加密套件(如 AES_256_GCM_SHA384);交换随机数、生成会话密钥。
-
证书验证。服务器发送证书链;X509TrustManager检查证书链是否由受信任的CA签发,证书是否过期,域名是否匹配(通过SNI扩展)。
-
加密通信。握手成功后,所有数据传输使用对称加密。
Token配置
Token 的核心作用:
Token 是一种凭证,用于在客户端和服务器之间传递身份验证信息和/或授权信息,而不需要每次都发送用户名和密码。
流程:
用户登录成功后,服务器会给你一个 Token。
将这个 Token 存储在 PreferenceUtil 中。
每次发起需要认证的 API 请求时,AuthInterceptor 会从 PreferenceUtil 中读取 Token,并将其放入请求的 Authorization 头部,发送给服务器。
服务器收到请求后,验证这个 Token 的有效性,然后决定是否处理你的请求。
我们把Token保存在EncryptedSharedPreferences中,它是一个对键和值进行加密的SharedPreferences。
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解析用户身份。
builder.addInterceptor(new AuthInterceptor(context))
使用Bearer Token认证方案,符合OAuth2标准:
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刷新机制
我们知道,为了安全,API的访问令牌(Access Token)通常都有一个较短的有效期。当它过期后,如果App继续使用这个过期的Token去请求API,服务器就会返回一个错误,通常是401 Unauthorized。为了避免用户频繁需要重新登录,我们就引入了Token刷新机制。
这个机制的核心是OkHttp框架提供的一个叫做 Authenticator 的接口。
1. 触发机制:
当我们的任何一个API请求因为Token失效而收到服务器返回的401错误时,OkHttp会自动回调我们实现的这个 Authenticator。
2. 核心刷新逻辑:
-
单一刷新点: 这是非常关键的一点。如果同一时间有多个API请求都因为Token过期而失败,我们不希望每个请求都去尝试刷新Token。这不仅浪费资源,还可能导致并发问题。所以,我们设计了一个机制来确保同一时间只有一个线程在执行刷新Token的操作。我们用了一个原子布尔型变量(AtomicBoolean)作为“正在刷新”的标记,并配合一个同步锁(synchronized块)。
- 第一个进入的线程会检查这个标记,如果没人在刷新,它就把标记设为“正在刷新”,然后开始执行实际的Token刷新网络请求。
- 其他后续因为401进来的线程,如果发现标记是“正在刷新”,它们就会进入等待状态,直到第一个线程刷新完成。
-
执行刷新: 获得刷新权限的那个线程,会拿出之前登录时保存的长期有效的刷新令牌(Refresh Token),向认证服务器发起一个特定的API请求(比如 /oauth/token),请求用这个Refresh Token换取一个新的Access Token和可能更新的Refresh Token。这是一个同步的网络调用。
-
处理刷新结果:
- 刷新成功: 如果服务器返回了新的Access Token (和 Refresh Token),我们会立即把这些新的Token更新到本地持久化存储中(比如SharedPreferences)。然后,用这个新的Access Token重新构建刚才失败的那个原始API请求,并把它返回给OkHttp。OkHttp会用这个带有新Token的请求自动进行重试。
- 刷新失败 - Refresh Token无效: 这是一个比较严重的情况。如果服务器告诉我们Refresh Token本身也无效了(比如返回错误码invalid_grant),这意味着用户必须重新登录。此时,我们会清除本地所有认证信息(Access Token, Refresh Token, Cookies等),并发出一个全局事件(比如通过EventBus)通知App的其他部分,比如UI层面,引导用户到登录页面。
- 刷新失败 - 其他原因: 如果是网络问题或其他临时服务器错误导致刷新失败,我们通常不会立即让用户登出,因为问题可能是暂时的。这次原始请求会失败,但后续的请求可能再次触发刷新并成功。
-
唤醒等待者: 无论刷新成功还是失败,那个执行刷新操作的线程在完成后,会清除“正在刷新”的标记,并唤醒所有之前因为这个锁而等待的线程。
3. 等待线程的处理:
那些被唤醒的线程,它们醒来后会先去本地存储里获取最新的Access Token(这个Token可能是刚刚被刷新成功的,也可能因为刷新失败还是旧的或者空的)。
- 如果获取到了有效的、新的Access Token,它们也会用这个新Token重新构建自己的原始请求,并交给OkHttp重试。
- 如果获取不到有效的新Token(比如刷新彻底失败且认证信息被清除了),那么这些请求也就自然失败了,不会再进行重试。
4. 防止无限循环的一个小优化:
我们还加了一个检查:如果在 Authenticator 中,发现当前失败的请求 已经 是带着我们认为的最新Access Token发出去的,但它还是返回了401,那就说明这个Access Token(即使是刚刷新的)也有问题,或者服务端逻辑有变。这时我们会放弃重试,并可能直接清理认证状态,避免陷入不断尝试刷新的死循环。
总结一下,这个机制的目标是:
- 用户体验: 在Access Token过期时,用户无感知地自动刷新,继续使用App。
- 效率与健壮性: 通过同步机制,避免并发刷新Token带来的混乱和资源浪费。
- 错误处理: 明确处理Refresh Token失效等关键错误,引导用户重新认证。
- 集中管理: 将Token刷新逻辑集中在 Authenticator 中,对业务代码透明。
通过这样的设计,我们就能比较优雅地处理Token过期和刷新的问题了。
public class TokenRefreshAuthenticator implements Authenticator { // 移除了 HttpObserver 继承,如果不需要
private static final String TAG = "TokenRefreshAuth";
private final PreferenceUtil preference;
private final Context applicationContext;
// 用于防止多个线程同时尝试刷新Token
private final AtomicBoolean isRefreshing = new AtomicBoolean(false);
// 用于同步访问 isRefreshing 和等待/通知机制
private final Object refreshLock = new Object();
public TokenRefreshAuthenticator(Context context) {
// 假设 PreferenceUtil.getInstance 不会抛出 GeneralSecurityException, IOException
// 如果会,需要在调用处处理或在此处声明并处理
this.applicationContext = context.getApplicationContext();
this.preference = PreferenceUtil.getInstance(this.applicationContext);
}
@Nullable
@Override
public Request authenticate(@Nullable Route route, @NonNull okhttp3.Response response) throws IOException {
Log.d(TAG, "authenticate: Received 401, current access token: " + preference.getAccessToken());
// 1. 如果请求已经携带了我们刚刚刷新的Token,但仍然401,说明新Token也无效,放弃。
// 这可以防止在Token刷新后立即再次触发认证(如果服务器立即判定新Token也无效)。
String requestAuthorizationHeader = response.request().header("Authorization");
if (requestAuthorizationHeader != null && requestAuthorizationHeader.endsWith(preference.getAccessToken())) {
Log.w(TAG, "authenticate: Request with current access token failed. Giving up.");
// 此时可以认为刷新令牌也可能失效,或者服务端有问题
clearAuthStateAndNotify();
return null; // 放弃,不再尝试
}
// 2. 使用双重检查锁定和原子变量来确保只有一个线程执行刷新操作
if (!isRefreshing.get()) {
synchronized (refreshLock) {
if (!isRefreshing.get()) {
isRefreshing.set(true); // 标记正在刷新
Log.d(TAG, "authenticate: Starting token refresh...");
String newAccessToken = null;
try {
newAccessToken = performTokenRefresh();
} catch (IOException e) {
Log.e(TAG, "authenticate: Token refresh failed with IOException", e);
// 刷新失败,可能是网络问题或服务器错误
// 不清除认证状态,允许后续请求再次尝试(除非特定错误表明需要登出)
} catch (TokenRefreshException e) {
Log.e(TAG, "authenticate: Token refresh failed with TokenRefreshException", e);
if (e.isInvalidGrant()) { // 例如,刷新令牌本身无效
clearAuthStateAndNotify();
}
// 其他 TokenRefreshException 类型可以根据需要处理
} finally {
isRefreshing.set(false); // 清除刷新标记
refreshLock.notifyAll(); // 唤醒其他等待的线程
Log.d(TAG, "authenticate: Token refresh finished. New token: " + (newAccessToken != null));
}
if (newAccessToken != null) {
return response.request().newBuilder()
.header("Authorization", "Bearer " + newAccessToken)
.build();
} else {
// 如果 newAccessToken 为 null (刷新失败或特定错误),则不重试
// 如果不是因为 invalid_grant 导致的刷新失败,之前的 clearAuthStateAndNotify 不会被调用
// 这里可以根据具体失败原因决定是否清除状态
// 为简单起见,如果刷新操作最终没有得到新token,就放弃
return null;
}
}
}
}
// 3. 如果其他线程正在刷新,则当前线程等待
Log.d(TAG, "authenticate: Another thread is refreshing, waiting...");
synchronized (refreshLock) {
// 再次检查 isRefreshing,因为可能在进入 synchronized 块之前刷新已完成
while (isRefreshing.get()) {
try {
refreshLock.wait(); // 等待刷新完成的通知
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.w(TAG, "authenticate: Waiting thread interrupted", e);
throw new IOException("Token refresh waiting interrupted", e);
}
}
}
Log.d(TAG, "authenticate: Woke up. Current access token after wait: " + preference.getAccessToken());
// 4. 刷新完成后,使用最新的Token(可能由其他线程刷新)构建新请求
// 需要检查Token是否真的被刷新了(即非空且与旧的不同)
String potentiallyNewAccessToken = preference.getAccessToken();
if (potentiallyNewAccessToken != null && !potentiallyNewAccessToken.isEmpty() &&
(requestAuthorizationHeader == null || !requestAuthorizationHeader.endsWith(potentiallyNewAccessToken))) {
Log.d(TAG, "authenticate: Retrying with potentially new token from preferences.");
return response.request().newBuilder()
.header("Authorization", "Bearer " + potentiallyNewAccessToken)
.build();
} else {
Log.w(TAG, "authenticate: No valid new token found after waiting or refresh. Giving up.");
// 如果等待后没有有效的Token,或Token未改变,则放弃
return null;
}
}
/**
* 执行实际的Token刷新网络调用。
*
* @return 新的 Access Token,如果刷新成功。
* @throws IOException 如果发生网络或解析错误。
* @throws TokenRefreshException 如果发生特定于Token刷新的业务逻辑错误 (如 invalid_grant)。
*/
private String performTokenRefresh() throws IOException, TokenRefreshException {
String currentRefreshToken = preference.getRefreshToken();
if (currentRefreshToken == null || currentRefreshToken.isEmpty()) {
Log.w(TAG, "performTokenRefresh: No refresh token available.");
throw new TokenRefreshException("Refresh token is missing.", false); // 不是 invalid_grant
}
AuthService authService;
try {
authService = createAuthService();
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
Log.e(TAG, "performTokenRefresh: Failed to create AuthService", e);
throw new IOException("Failed to initialize SSL for auth service", e);
}
Map<String, String> params = new HashMap<>();
params.put("grant_type", "refresh_token");
params.put("refresh_token", currentRefreshToken);
params.put("client_id", Config.CLIENT_ID); // 确保Config.CLIENT_ID是正确的
Log.d(TAG, "performTokenRefresh: Calling refresh token API...");
Response<TokenResponse> retrofitResponse = authService.refreshToken(params).execute(); // 同步执行
if (retrofitResponse.isSuccessful()) {
TokenResponse tokenResponse = retrofitResponse.body();
if (tokenResponse == null || tokenResponse.getAccessToken() == null || tokenResponse.getRefreshToken() == null) {
Log.e(TAG, "performTokenRefresh: Successful response but token data is missing. Body: " + retrofitResponse.body());
throw new IOException("Token refresh response body or token data is null.");
}
// 同步更新 SharedPreferences (PreferenceUtil 的方法应设计为线程安全或在此处额外同步)
// 由于整个 performTokenRefresh 被 isRefreshing 保护,这里的并发写入竞争会小很多
// 但 PreferenceUtil 内部最好还是线程安全的
preference.setAccessToken(tokenResponse.getAccessToken())
.setRefreshToken(tokenResponse.getRefreshToken())
.setTokenExpiration(tokenResponse.getExpiresIn());
Log.i(TAG, "performTokenRefresh: Token refreshed successfully. New AccessToken: " + tokenResponse.getAccessToken());
return tokenResponse.getAccessToken();
} else {
// 处理错误响应
String errorBodyString = null;
if (retrofitResponse.errorBody() != null) {
try {
// 注意:errorBody().string() 只能调用一次
errorBodyString = retrofitResponse.errorBody().string();
} catch (IOException e) {
Log.e(TAG, "performTokenRefresh: Failed to read error body", e);
}
}
Log.e(TAG, "performTokenRefresh: Token refresh API call failed. Code: " + retrofitResponse.code() + ", Message: " + retrofitResponse.message() + ", ErrorBody: " + errorBodyString);
// 解析错误,判断是否是 invalid_grant
if (errorBodyString != null) {
try {
// 假设 ErrorResponse 结构和你的 JSONUtil.fromJson
ErrorResponse error = JSONUtil.fromJson(errorBodyString, ErrorResponse.class);
if (error != null && "invalid_grant".equals(error.getError())) {
Log.w(TAG, "performTokenRefresh: Refresh token is invalid (invalid_grant).");
throw new TokenRefreshException("Refresh token is invalid (invalid_grant). Error: " + error.getDescription(), true);
} else if (error != null) {
throw new TokenRefreshException("Token refresh failed with server error: " + error.getDescription(), false);
}
} catch (JsonSyntaxException e) {
Log.e(TAG, "performTokenRefresh: Failed to parse error response JSON.", e);
}
}
// 通用错误
throw new IOException("Token refresh failed with HTTP status: " + retrofitResponse.code());
}
}
private AuthService createAuthService() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
// 确保 NetworkModule.getSSLContext() 和 NetworkModule.getSystemTrustManager() 是线程安全的
// 或者在这里创建它们的实例(如果它们本身是轻量级的)
// 假设它们是安全的
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(NetworkModule.getSSLContext().getSocketFactory(), NetworkModule.getSystemTrustManager())
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS) // 增加读取超时,刷新Token可能稍慢
.writeTimeout(15, TimeUnit.SECONDS)
// 重要:不要添加会再次触发认证的拦截器 (如 AuthInterceptor 或这个 Authenticator 自身)
.build();
return new Retrofit.Builder()
.baseUrl(Config.ENDPOINT) // 确保这是认证服务器的地址
.client(client)
.addConverterFactory(GsonConverterFactory.create(JSONUtil.createGson()))
// refreshToken 通常不需要 RxJavaCallAdapterFactory,因为我们在 Authenticator 中需要同步执行
// 但如果 AuthService 也用于其他异步调用,保留它也可以
.build()
.create(AuthService.class);
}
private void clearAuthStateAndNotify() {
Log.i(TAG, "clearAuthStateAndNotify: Clearing authentication state and notifying listeners.");
// 确保 preference.clearAuth() 是线程安全的
preference.clearAuth();
CookieManager.getInstance().removeAllCookies(null); // 简单的移除,回调可选
EventBus.getDefault().post(new LoginStatusChangedEvent(false));
}
/**
* 自定义异常,用于区分Token刷新过程中的特定错误。
*/
private static class TokenRefreshException extends Exception {
private final boolean invalidGrant;
public TokenRefreshException(String message, boolean isInvalidGrant) {
super(message);
this.invalidGrant = isInvalidGrant;
}
public TokenRefreshException(String message, Throwable cause, boolean isInvalidGrant) {
super(message, cause);
this.invalidGrant = isInvalidGrant;
}
public boolean isInvalidGrant() {
return invalidGrant;
}
}
// 假设你有一个这样的 ErrorResponse 类
// private static class ErrorResponse {
// String error;
// String error_description;
// public String getError() { return error; }
// public String getDescription() { return error_description; }
// }
// 假设你的 AuthService 接口包含:
// interface AuthService {
// @FormUrlEncoded
// @POST("oauth/token") // 假设这是你的刷新Token端点
// Call<TokenResponse> refreshToken(@FieldMap Map<String, String> params);
// }
// 假设你的 TokenResponse 类:
// class TokenResponse {
// String access_token;
// String refresh_token;
// long expires_in;
// public String getAccessToken() { return access_token; }
// public String getRefreshToken() { return refresh_token; }
// public long getExpiresIn() { return expires_in; }
// }
}
4.3.1.2 配置Retrofit
主要为Retrofit设置刚刚配置好的OkHttp Client,api地址,RxJava,并使用换java和JSON。
4.3.2 创建服务接口
Retrofit还需要一个API接口来创建实例,这个接口就是使用GET,POST之类的注解来定义请求。
public interface DefaultService {
@GET("/songs")
Observable<ListResponse<Song>> songs(@Query("limit") int limit);
}
相当于https://api.example.com/songs?limit=10
接着,使用动态代理为retrofit配置这个service接口。当调用DefaultService中的方法时,Retrofit通过动态代理生成一个实现了DefaultService接口的代理类。
service = retrofit.create(DefaultService.class);
4.3.3 RxJava
可以看到,上面的service接口返回的是一个Observable,它是RxJava的一个核心类,从service接口返回到为adapter设置数据的过程这样的:
@GET("/songs")
Observable<ListResponse<Song>> songs(@Query("limit") int limit);
service.songs(10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new HttpObserver<ListResponse<Song>>() {
@Override
public void onSucceeded(ListResponse<Song> data) {
adapter.setNewInstance(data.getData().getData());
}
});
4.3.3.1 Observable
先来看看官方文档的解释:
简单概括,我们定义一个观察者Observer,对应于上面的代码new HttpObserver<>()就是一个自定义的 Observer实现。里面有四个方法onSubscribe,onNext onError,onComplete如下图,当然还可以自定义自己的方法。
接着,我们定义一个事件源(Observable),对应于:
@GET("/songs")
Observable<ListResponse<Song>> songs(@Query("limit") int limit);
最后观察者订阅事件源,当事件源发出事件,将被观察者捕获,并做出响应(onNext or onError ?):
service.songs(10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new HttpObserver<ListResponse<Song>>() {
// 对事件做出响应
@Override
public void onSucceeded(ListResponse<Song> data) {
adapter.setNewInstance(data.getData().getData());
}
}
4.3.3.2 subscribeOn, observeOn
官方文档是这么说的:"SubscribeOn操作符决定了Observable的初始运行线程,无论该操作符在操作符链中的何处调用。而 ObserveOn则影响的是在其调用位置之后的线程环境。这意味着你可以在操作符链中的多个位置调用 ObserveOn,以更改特定操作符运行的线程。"
也就是说,如图中即便在下方调用subscribeOn,影响的也是顶部的线程(变成了蓝色);而ObserveOn则立即改变下一个线程。
回到我们的代码:
@GET("/songs") 这个songs↓
Observable<ListResponse<Song>> songs(@Query("limit") int limit);
service.songs(10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
service.songs(10),执行到这里时,网络请求并没发出(Cold observale),Retrofit只是创建了一个Observable<ListResponse<Song>>对象。在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。”
回到我们的代码
@GET("/songs")
Observable<ListResponse<Song>> songs(@Query("limit") int limit);
service.songs(10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new HttpObserver<ListResponse<Song>>() {
@Override
public void onSucceeded(ListResponse<Song> data) {
adapter.setNewInstance(data.getData().getData());
}
});
只有被订阅(subscribe)后,songs(10)返回的Observable<ListResponse<Song>>才会真正开始执行网络请求,因为这是一个冷(Cold)的Observable:
当接收到网络请求后,切换为主线程,并开始执行观察者里的方法,首先是onNext
@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。
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。
@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);
}
public static <T> void handlerRequest(T data, Throwable error) {
if (error != null) {
// 处理exception
handleException(error);
} else{
// ...
}
处理exception,不同类型的错误会根据异常的类型显示相应的错误信息:
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错误处理:
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操作符,并自定义辅助类来实现。
public Observable<ListResponse<Song>> songs() {
return service.songs(10)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retryWhen(new RetryWithDelay(MAX_RETRIES, RETRY_DELAY));
}
retryWhen
先来读读它的API文档:
老外老是喜欢搞这种长难句就问你服不服,我来试试翻译下:“返回一个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,序列将立即以相同的信号终止。”
看不懂正常,我们再来看看它的签名:
可以看到它使用Function<? super Observable<Throwable>, ? extends ObservableSource<?>>作为参数,也就是一个handler。Function又是啥?
可以看到,Function接口有两个类型参数,第一个是input,第二个是output,apply对input进行一些计算并返回output。
总结一下retryWhen,它接受一个实现了Function接口的handler,当源Observable发出onError,抛出的Throwable封装成Observable<Throwable>并在apply接口方法里进行逻辑处理,根据处理结果返回一个新的ObservableSource<?>(Observable实现了ObservableSource接口)。如果这个ObservableSource发出:
onNext:retryWhen重新订阅源Observable,也就是重试。onError或onComplete信号:retryWhen会将相同的信号传递给下游观察者,终止整个数据流。
接着,我们来看看retryWhen里的handler怎么实现:
public class RetryWithDelay implements Function<Observable<Throwable>, 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<Throwable> exception) {
return exception.flatMap((Function<Throwable, Observable<?>>) 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<Throwable> exception,接着,对它使用flatMap方法:
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发送全局事件:
EventBus.getDefault().post(new AuthErrorEvent());
并在BaseLogicFragment跳转即可:
@Subscribe(threadMode = ThreadMode.MAIN)
public void onAuthErrorEvent(AuthErrorEvent event) {
// 跳转到登录界面
((BaseLogicActivity) getHostActivity()).startLoginActivity();
}
4.3.3.5 取消订阅
最后,我们的代码是存在内存泄露风险的,原因在于HttpObserver 是一个匿名内部类,它会会隐式持有其外部类 (DiscoveryFragment) 的引用,当 DiscoveryFragment被销毁时,订阅仍然持有对它的引用,导致DiscoveryFragment 无法被垃圾回收,从而引发内存泄漏。
为防止存泄漏,需要在生命周期内取消订阅。这里我们使用RxJava的CompositeDisposable容器中的clear来销毁所有disposable容器。
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Override
public void onDestroyView() {
super.onDestroyView();
// 取消所有订阅,避免内存泄漏
compositeDisposable.clear();
}
4.3.3.6 RxJava总结
RxJava基于ReactiveX:
“ReactiveX是一个用于通过使用可观察序列来组合异步和基于事件的程序的库。
它扩展了观察者模式,以支持数据和/或事件的序列,并添加了操作符,使您能够以声明式的方式组合这些序列,同时抽象掉诸如低级线程管理、同步、线程安全、并发数据结构和非阻塞 I/O 等方面的关注点。”
观察者模式
观察者模式(Observer Pattern)是一种设计模式,用于在对象之间建立一种一对多的依赖关系。当一个对象(称为“主题”或“被观察者”)的状态发生变化时,所有依赖于它的对象(称为“观察者”或“订阅者”)都会自动收到通知并更新。
观察者模式的核心组成部分
主题(Subject):
- 维护一组观察者。
- 提供方法让观察者注册(订阅)和注销(取消订阅)。
- 在自身状态发生变化时,通知所有注册的观察者。
观察者(Observer):
- 接收并处理来自主题的通知。
- 通常包含一个更新方法(如
update()),当主题状态改变时被调用。
例子:
interface Observer {
void update(String event);
}
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);
}
}
}
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类,维护观察者列表,有事件发出时通知观察者
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表达式定义了一个观察者:
eventSource.addObserver(event -> System.out.println("Received response: " + event));
其中addObserver的参数是一个Observer接口,Lambda表达式实现了这个接口的updata方法:
public void addObserver(Observer observer) {
observers.add(observer);
}
整体流程:
- 创建
Subject - 使用
Lambda表达式创建Observer,本身就实现了Observer的接口方法 Subject通知所有Observer,执行它们自己的方法