你应该知道的 Android 网络访问方式的演化史

154 阅读7分钟
原文链接: mp.weixin.qq.com

作者:Elye 编译者:Kotlin之巅

众所周知,Android 不知不觉间已经存在超过 10 年了,与此同时,Android 开发者们访问网络的方式也在不断演化中。本文我将通过一个例子来简单分享我对这个演化的认识。

恐龙时代:直接访问

在 Android 2.2 版本之前,网络访问相关的代码和其他代码一样,可以在主线程中直接使用,不会导致任何异常:

  1. String result = Network.fetchHttp(searchText);

  2. view.updateScreen(result);

然而,这种用法体验并不理想,因为网络访问相对 UI 交互来说通常很慢,视具体的接口而定,通常需要几秒钟时间,这段时间内 UI 界面会卡住,用户体验比较糟糕。因此,在后面的版本中,如果你在主线程中访问网络,Android应用在运行时将会毫不留情的抛出 NetworkOnMainThreadException 异常,导致应用闪退。

当然也还是有一些办法可以强制 Android 允许在主线程中直接访问网络,就像本文这个例子中一样,但这种操作仅仅用于演示目的,千万不要在任何线上产品中使用。

石器时代:使用子线程

稍微有点经验的开发者应该都知道,为了不阻塞 UI 交互,我们可以把网络访问部分代码放到后台线程中,这样就能够在访问网络获取数据的同时能够有流畅的 UI 响应。示例代码如下所示:

  1. thread = new Thread(new Runnable() {

  2.    @Override

  3.    public void run() {

  4.        final String result = Network.fetchHttp(searchText);

  5.        // Run the result on Main UI Thread

  6.        new Handler(Looper.getMainLooper()).post(new Runnable() {

  7.            @Override

  8.            public void run() {

  9.                view.updateScreen(result);

  10.            }

  11.        });

  12.    }

  13. });

  14. thread.start();

上面的代码是在子线程中访问网络的基本方式,需要注意的一点是,更新 UI 界面的代码必须是放到主线程中,也就是通过使用主线程的 Looper.getMainLooper() 来创建 Handler,否则会出现 CalledFromWrongThreadException 异常。

这种方式有很多不足之处,因为线程的管理通常比较复杂,让开发者直接处理这种线程问题可能会导致写出来的 App 存在出错的风险,因此,Google 做了进一步的优化。

青铜时代:AsyncTask

由于主线程已经禁用直接访问网络,因此,Google 提出了一种推荐的方法,称为 AsyncTask。

AsyncTask 带来了很多不错的特性,例如在后台线程中执行网络请求之前和之后,可以允许我们在主线程执行某些操作,同时,它也支持渐进式的更新等。示例代码如下所示:

  1. private static class MyAsyncTask extends AsyncTask<String, Void, String> {

  2.    private WeakReference<MainView> view;

  3.    // only retain a weak reference to the activity

  4.    MyAsyncTask(MainView view) {

  5.        this.view = new WeakReference<>(view);

  6.    }

  7.    @Override

  8.    protected String doInBackground(String... params) {

  9.        String result = Network.fetchHttp(params[0]);

  10.        return result;

  11.    }

  12.    @Override

  13.    protected void onPostExecute(String result) {

  14.        if (view.get() != null) {

  15.            view.get().updateScreen(result);

  16.        }

  17.    }

  18. }

曾几何时,这种方式被称为标准的网络访问方式,网络上也能找到相关的教程。

尽管 AsyncTask 是 Google 引入的,但开发者们陆续发现它的一些缺陷,特别是跟 Android 应用生命周期相关的,具体可以参考 The Hidden Pitfalls of AsyncTask 这篇文章。

因此,尽管曾经是标准方式,但现在已经没什么人用它了。

中世纪:IntentService

由于 AsyncTask 不尽如人意,另外一个流行的方式出现了,它就是 IntentService。

当然,IntentService 并不是 Google 专门引入用来解决 AsyncTask 缺点的,它最初是为了后台长时间运行的服务(例如文件下载)而设计的。

随着社区对 AsyncTask 的摒弃,开发者们开始考虑将 IntentService 作为替代方案。

使用简单的代码片段很难讲清楚这种方式,因此我们来看下面这张图。IntentService 位于 Activity 之外,它本质上是一个 Service,需要注册到 AndroidManifest 中。

我们可以把 IntentService 当作一个线程,网络访问代码如下所示:

  1. @Override

  2. protected void onHandleIntent(@Nullable Intent intent) {

  3.    String searchText = intent.getStringExtra(PARAM_IN_MSG);

  4.    String result = Network.fetchHttp(searchText);

  5.    Intent broadcastIntent = new Intent();

  6.    broadcastIntent.setAction(ACTION_RESP);

  7.    broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT);

  8.    broadcastIntent.putExtra(PARAM_OUT_MSG, result);

  9.    sendBroadcast(broadcastIntent);

  10. }

可以看到,当网络访问完成后,它通过 BroadcastReceiver 发送消息给 Activity 来实现结果信息的传递。

因此,虽然这种方式可以作为替代方案,但相对来说属于重量级的方式,如果我们只是想要发起一个简单的网络请求,这种方式显得太过笨重。

工业时代:RxJava

随着函数式编程和响应式编程的流行,RxJava 也被引入了 Android 社区。

很快的,网络访问的标准方式变成了 RxJava。

  1. disposable = Single.fromCallable(new Callable<String>() {

  2.    @Override

  3.    public String call() throws Exception {

  4.        return Network.fetchHttp(searchText);

  5.    }

  6. })

  7.        .subscribeOn(Schedulers.io())

  8.        .observeOn(AndroidSchedulers.mainThread())

  9.        .subscribe(new Consumer<String>() {

  10.            @Override

  11.            public void accept(String s) throws Exception {

  12.                view.updateScreen(s);

  13.            }

  14.        });

这种方式之所以流行,是因为它通过链式调用的方式把主线程和子线程的操作串联起来。

我想如果有人声称自己在使用 RxJava,那么很大可能他只是在项目中网络访问部分的代码中使用 RxJava,其他地方的代码根本就没有使用 RxJava 这种函数响应式编程范式。

这种现象的原因是响应式编程本身是一个相对比较复杂的概念,不好掌握,而且需要使用匿名类(因为当时 Android 开发中只能使用 Java 6 和 Java 7),因此代码写起来并不是很顺畅。

现代:RxJava + Kotlin

2017 年,Google 宣布在 Android Studio 中对 Kotlin 语言提供一级公民形式的支持,这使得使用 Kotlin 开发 Android 迅速流行起来。使用 Kotlin 中的 lambda 表达式代替 Java 中匿名类,使得 RxJava 的使用更加简洁。

让我们来看看实现相同功能的网络访问代码使用 Kotlin 改写后的形式:

  1. disposable = Single.fromCallable { Network.fetchHttp(searchText) }

  2.        .subscribeOn(Schedulers.io())

  3.        .observeOn(AndroidSchedulers.mainThread())

  4.        .subscribe { result -> view.updateScreen(result) }

可以看到,实现相同功能的代码简练了很多。因此,在 Kotlin 中,RxJava 方式访问网络变得更加流行,出现了越来越多有趣且优雅的使用 RxJava 访问网络的方式。具体可以参考这两篇文章:《RxJava: Clean way of prefetching data and use later》,《Managing Network State using RxJava》。

未来:Coroutines

尽管 RxJava 解决了很多问题,但它的学习曲线对很多初学者来说还是很陡峭的。响应式编程范式对多数使用者来说会显得有点别扭,源头(网络获取的数据)和结果(发送给 UI 界面显示的数据)之间的联系看起来并不直接。

最新版本的 Kotlin 引入了 Coroutines,虽然现在还处于试验阶段,但它看起来前景广阔。

Coroutines 最大的亮点是使得异步代码看起来像同步代码,如下所示:

  1. job = launch {

  2.    val result = Network.fetchHttp(searchText)

  3.    launch(UI) {

  4.        view.updateScreen(result)

  5.    }

  6. }

如果你想代码变得更优雅,例如使用 future 或者 promises 之类的概念使得代码更具响应式的特点,那么也可以通过 async-await 的方式来编写:

  1. val defer = async {

  2.    Network.fetchHttp(searchText)

  3. }

  4. job = launch(UI) {

  5.    view.updateScreen(defer.await())

  6. }

因此,在我看来,Coroutines 很好的桥接了初学者社区和资深的专家。这里有一篇不错的文章(《Simple asynchronous loading with Kotlin Coroutines》)将 Kotlin 的 Coroutines 和 Android 的生命周期 API 相结合。结合后代码示例如下:

  1. load { restApi.fetchData(query) } then { adapter.display(it) }

看起来前景广阔吧,以上是我对未来几个月 Android 网络访问方式的预测,如果你有其他想法,欢迎交流。

参考文档

  1. 原文链接:https://medium.com/@elye.project/the-evolution-of-android-network-access-1e199fc6e9a2

  2. 本文示例代码地址:https://github.com/elye/demoandroidnetwork_evolution

  3. AsyncTask:https://developer.android.com/reference/android/os/AsyncTask.html

  4. The Hidden Pitfalls of AsyncTask:http://blog.danlew.net/2014/06/21/the-hidden-pitfalls-of-asynctask/

  5. RxJava: Clean way of prefetching data and use later:https://medium.com/@elye.project/rxjava-clean-way-of-prefetching-data-and-use-later-54800f2652d4

  6. Managing Network State using RxJava:https://medium.com/@elye.project/managing-network-state-using-rxjava-79cdaed88d5d

  7. Simple asynchronous loading with Kotlin Coroutines:https://hellsoft.se/simple-asynchronous-loading-with-kotlin-coroutines-f26408f97f46

  8. Android 生命周期 API:https://developer.android.com/topic/libraries/architecture/lifecycle.html