首页 | 学习笔记

767 阅读38分钟

1. 概述

效果类似下图。

微信图片_20250107112312.jpg

2. 什么是Fragment

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

image.png

2.1 Activity和Fragment的关系

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

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

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

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

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

  • 通信方式:Activity通常通过Intent与其他Activity进行通信或跳转。Fragment需要通过宿主 ActivityViewModel(也可使用专门的接口回调等方式)与其他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()

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

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对象来启动一个ActivityIntent使用当前上下文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

image.png

简单来说,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的生命周期方法,在FragmentActivity中移除或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对象的创建

image.png

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

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

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

LayoutInflaterAndroid提供的类,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 获取父类的参数化类型
 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<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成员变量)。

image.png

image.png

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

3.4.2 为什么要在onCreate中创建binding

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

    @Override
    protected View getLayoutView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return binding.getRoot();
    }

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

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

4 实现首页Fragment

4.1 视图设置

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

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

image.png

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

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()));
    }
}

AdpaterAndroid中的一种设计模式,负责将数据源绑定到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

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

BaseViewHolder的缓存机制

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

image.png

image.png

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

image.png

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;
}

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

最后,给RecyclerView设置Adpter

binding.list.setAdapter(adapter);

4.3 从网络加载数据

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

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

整体流程概括:

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

Retrofit是一个用于AndroidHTTP客户端,开发者通过接口定义请求方式和请求参数,Retrofit根据这些定义生成一个代理对象,并通过OkHttp执行实际的网络请求。当请求完成时,返回的数据会根据指定的Converter(比如GsonMoshi)自动转换为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 封装RetrofitOkHttp

首先,我们先封装好RetrofitOkHttp,以提供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中,将HTTPHTTPS响应缓存到文件系统中,以便它们可以被重用,从而节省时间和带宽。注意这里配置的是磁盘缓存,而不是内存缓存,并且缓存的是后端返回的HTTP响应报文,不是音乐和图片本身,音乐缓存将由ExoPlayer管理,图片会持久化到数据库中,以后的文章会说。

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

image.png

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

配置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数字证书体系验证服务器身份(可选验证客户端身份)。

完整安全链路的工作流程

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

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

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

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

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 Clientapi地址,RxJava,并使用换javaJSON

4.3.2 创建服务接口

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

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

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

image.png

image.png

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

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

@GET("/songs")
Observable<ListResponse<Song>> songs(@Query("limit") int limit);

image.png

最后观察者订阅事件源,当事件源发出事件,将被观察者捕获,并做出响应(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则立即改变下一个线程。 image.png

回到我们的代码:

@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通过RxJavaCallAdapterFactoryCall对象包装成了一个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

回到我们的代码

@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

image.png

当接收到网络请求后,切换为主线程,并开始执行观察者里的方法,首先是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抛向下游。

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

public Observable<ListResponse<Song>> 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 OnErrorretry接着会在子订阅调用它们。否则当前Observable会被重订阅。”

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

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

image.png

可以看到它使用Function<? super Observable<Throwable>, ? extends ObservableSource<?>>作为参数,也就是一个handlerFunction又是啥?

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

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

  • onNextretryWhen重新订阅源Observable,也就是重试。
  • onErroronComplete信号: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方法:

image.png

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

flatMap内部有个lambda表达式,对捕获的exception进行逻辑判断(转换),并返回一个新的Observable:小于重试次数并属于网络异常,则发出一个onNextObservable.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 无法被垃圾回收,从而引发内存泄漏。

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

image.png

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()),当主题状态改变时被调用。

例子:

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(或者类似RxJavaObservabe):

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);
}

整体流程:

  1. 创建Subject
  2. 使用Lambda表达式创建Observer,本身就实现了Observer的接口方法
  3. Subject通知所有Observer,执行它们自己的方法