精品!Android 多线程技术哪家强?

开发者全社区

共 7248字,需浏览 15分钟

 ·

2020-07-31 23:42

作者:qing的世界

链接https://juejin.im/post/5d3374cee51d4556bb4cd469

上一篇文章我介绍了一些使用安卓多线程框架们的一些误区,那既然已经介绍了那么多坑,这一篇我就来详细说说一些方案。同样的,这些总结下来的方案都是我自己个人的心得体会,本人水平有限,有什么不对或者意见不同的欢迎大家讨论或者吐槽。


Trade Off 


今天我想先说一个英文单词,叫Trade Off。中文翻译过来可以说叫权衡,妥协,但是这么干巴巴的翻译可能不能体现这个词的牛逼之处,我来举个例子。比如迪丽热巴和谢娜同时追求我,虽然迪丽热巴颜值更高,但是考虑到谢娜在湖南台的地位以及和她在一起之后能给我带来的曝光度,我选择了谢娜。。。。(以上纯属段子)


Anyway。。。这就是Trade Off,一个很艰难的选择,但是最后人都是趋于自己的利益最大化做出最后的决定。Trade Off这个词贯穿了软件开发的所有流程,在多线程的选择下面也是有一样的体现。


谷歌官方在18年的IO大会上放了这么一张图

我先来翻译翻译这张图:横轴从左往右分别是Best-Effort(可以理解为尽力而为)还有Guaranteed Execution(保证执行). 竖轴从上往下分别是Exact Timing(准确的时间点)还有Deferrable(可以被延迟).这张图分别从在多线程下执行的代码的可执行性和执行时间来把框架分成了四个维度。


其中我想先说说个人的理解: 对于安卓里面的里面的任何代码,都逃不开生命周期这个话题。因为安卓的四大组件有两个都是有生命周期的,而且对于用户来说,可见的Activity或者Fragment才是他们最关心app的部分。所以一段代码,在保证没有内存泄漏的情况下,能不能在异步框架下执行完毕,就得取决于代码所在载体(Activity/Fragment)的生命周期了。比如上一期我们说到的RxJava的例子:

@Overrideprotected void onDestroy() {    super.onDestroy();
//onDestroy 里面对RxJava stream进行unsubscribe,防止内存泄漏    subscription.unsubscribe();}

这段代码有可能会阻止我们在Observable里面的API进行调用。


那么在安卓的生命周期的背景下,这段代码就是Best Effort,尽力而为了。能跑就跑,要是activity没了,那就拉倒。。。

所以把以上例子中的代码换成图中的ThreadPool想必你就理解了。


那么Guaranteed Execution呢?很显然在图中是用Foreground service来做。不像Activity或者Fragment,Service虽然也有生命周期,但是他的生命周期不像前两者是被用户操控。Service的生命周期可以由开发者来决定,因此我们可以使用Foreground service + ThreadPool,来保证代码一定可以被执行。用Foreground Service是因为Android在Oreo之后修改了Service的优先级,在app 进入后台idle超过一分钟之后会自动杀死任何后台Service。但是,使用Foreground Service,要求开发者一定要开启一个Notification。

@Overridepublic void onCreate() {    super.onCreate();
startForeground(1, notification);    Log.d(TAG_FOREGROUND_SERVICE, "My foreground service onCreate().");}



这下好了,虽然保证程序正常运行了,我们的UX却变了,你还得和设计狮们苦口婆心的解释,这都是安卓谷歌的锅!我也不想有个突兀的图标出现在状态栏里。。。我还记得我去年在修改我们产品下载音乐的Service时候,为了让Service不被销毁把Notification变成Foreground service 的notification,我们的产品经理还跑来问我为啥这个notification不能划掉。。。。也是花了很长时间来给产品经理科普。


你看这就是Trade Off,从尽力而为到想保证代码必须运行。中间有这么一个需要权衡的地方。


那么咱又开始琢磨了,既然Foreground Service这么蛋疼,能不能要一个可以保证执行,但是不改变咱app的UX的框架呢。


当当当当!WorkManager闪亮登场。

说起这个框架就屌了。使用它可以轻松的实现异步任务的调度,运行。当然仅仅是普通的执行异步任务好像没那么吸引人,毕竟很多其他的优秀异步框架也可以实现。我们看看官方的解释:

The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.

划重点,even if the app exits or device restarts,意思是即使app退出或者重启,也可以保证你的异步任务完整的执行完毕。这个就完美的解决了我们用Foreground Service或者ThreadPool的问题,它既可以保证任务完整执行,也不需要以为启动前台服务而导致需要UX的改变!


我这里就不详细解释WorkManager的实现细节和源码了。我们直接以上次的youtube 取消订阅的例子说话(这个例子用kotlin因为我懒得重新写一个java版本的了。。。)!我们先定义一个Worker:

class MakeSubscriptionWorker : Worker{    constructor(context: Context, parameterName: WorkerParameters):super(context,parameterName)
override fun doWork(): Result {      //unsubscribe 的API call在这里做 val api = API() var response = api.unSubscribe() if(response != null){       return Result.success(response)      }else{ return Result.failure()       }    }}


复制代码Worker里面其实就是执行我们的取消订阅的API call。


接着监听我们取消订阅的成功与否

//1. 创建我们Worker的实例并且开始执行!WorkManager.getInstance()          .enqueue(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!)          .addTag(MakeSubscriptionWorker::class.simpleName!!)          .build())//2. 把API call的结果转化成Jetpack里面的LiveData,并且开始监听结果WorkManager.getInstance()    .getWorkInfosByTagLiveData(MakeSubscriptionWorker::class.simpleName!!)    .observe(this,purchaseObservaer)
 //3. 如果用户退出了Activity,那么停止监听结果WorkManager.getInstance() .getWorkInfosByTagLiveData(MakeSubscriptionWorker::class.simpleName!!) .removeObserver(purchaseObservaer)


重点在第三步,虽然我们停止监听了,但是不代表这个异步任务会取消。它还会继续执行。


可是这和我们用线程池+非匿名内部类Runnable好像没啥本质区别,毕竟在上面的例子里面,kotlin的内部class本身就是静态的。不存在内存泄漏。


回到开头我说的,WorkManager可以保证任务一定执行,即使你把app退出!

WorkManager会把你的任务序执行id和相关信息保存在一个数据库中,在App重新打开之后会根据你在任务中设置的限制(比如有的任务限制必须在Wifi下执行,WorkManager提供这样的API)来重新开启你未完成任务。


也就是说,即使我们在点击取消订阅之后马上把App强行关闭,下一次打开的时候WorkManager也可以重新启动这个任务!!!


那。。。这么屌的功能为啥我们不马上开始使用呢????

还记得我反复提到的Trade Off这个词么,WorkManager也有它需要取舍的地方。

首先官方虽然重点说到了保证任务执行,但同时也提到了:

WorkManager is intended for tasks that are deferrable—that is, not required to run immediately

也就是说,WorkManager主要目的是为了那些允许/可以忍受延迟的异步任务而设计的。这个可以忍受延迟就很玩味了。有谁会想要无目的的延迟自己想要运行的异步任务的?这个问题的答案其实也是安卓用户一直关心的电池续航。


安卓在经历了初期的大开大方之后,开始越来越关心用户体验。既然App的开发者不遵守游戏规则(没错我说的就是那些不要脸的xx保活app),那么谷歌就自己制定规则,在新的操作系统中,谷歌进一步缩减后台任务可以执行的条件。


具体限制(https://developer.android.com/guide/background/

上图中,简洁的来说,当APP进入后台之后,异步任务被限制的很死。那么作为谷歌自己研制的WorkManager,一个号称app关掉之后还能重启异步任务的这么吊炸天的框架当然也要遵循这个规则。


所以,所谓的延迟,并不是那么的吓人,笔者亲测,在App还在前台的时候执行WorkManager,异步任务基本上还是马上会进入调度执行的,但是当app进入后台之后,WorkManager就会尝试暂停任务。所以在我们上面的例子里面,WorkManager也是可以使用的。


但是!Trade Off又来了。虽然WorkManager和Activity的生命周期无关了,但是却和整个App的前后台状态相关了。app的退出可以暂停WorkManager里面的任务,也就是说控制他能否执行的这个钥匙,又从开发者手中跑到用户的手里了。。。。

这说了大半章节的WorkManager,怎么又绕回来了呢。说了这么多,从ThreadPool到Foreground Service,再到WorkManager。我们好像每次都在解决一个问题之后又遇到了新的问题,好像没有完美的方案。

没错,这些就是Trade Off,权衡,软件开发本就没有完美的答案,silver bullet只在杀吸血鬼的时候存在,软件开发?不存在的。。。

复杂度的Trade Off 


上面的篇幅我都在从谷歌官方的解释,也就是从执行时间,和能否保证任务完整执行的维度来审视我们现有的解决方案。接下来我想从代码的复杂角度来聊聊。


我在2015年开始接触RxJava,刚开始学习RxJava的时候的确有点难懂,尤其是flatMap这个操作符消耗了我整整一周的时间去消化。但是在越来越熟悉之后,我就渐渐的爱上了RxJava。那个时候我就觉得,函数式编程的操作符实在太屌了,酷炫的操作符叠在一起,简直是狂炫酷霸拽有没有,加上团队中懂RxJava的人不多,大家有问题都会找我,我的虚荣心也迅速膨胀到了月球。。。我记得当时我在重构一个app冷启动的任务调度的代码。


当时任务的依赖图大概长这个样子:

当我的队友还在用LacthCoundown,焦头烂额的时候。我轻松的用RxJava的mergeWith和ConcatMap解决了:

B  .mergeWith(C)  .concatMap(E)  .concatMap(F)  .mergeWith(A            .concatMap(D))

复制代码啥也不说了,屌就一个字!这更加坚定了我RxJava就是世界上最好的异步任务框架的信念了。。。。


直到我从创业公司来到Amazon Music,从一个只有3个人的安卓团队到了一个四个大组同时做一个产品的Org。我突然发现,推广RxJava的时间成本,还有团队学习的成本,已经不能和以前在创业公司同日而语了。刚开始的时候,每次看到队友的code review我都喜欢插上一嘴:"you know , if we use RxJava here......", 直到团队的Senior有一次和我问我:"Why RxJava is better?"的时候,我才意识到,我好像从来没有系统性的总结过RxJava的优缺点,一时间有点语塞。我甚至发现, 有时候一些简单的集合处理,用RxJava反而还显得复杂了,况且RxJava的可读性还是在基于团队都熟悉的条件下,更不说因为学习成本导致产品迭代的减速了。那一刻,我仿佛丢了灵魂,我引以为傲的RxJava竟然被贬的一文不值!!!

不对啊,我们RxJava明明对异步任务的组合,连接有强大的支持!mergeWith,concatMap,这么牛逼的操作符,不就是使用RxJava最好的理由么!我这样和Senior反击到。。。


直到我看到了Coroutine。。。。


Coroutine的操作符也可以同样的实现上面的例子,还更容易理解和阅读。。。

如果想实现上面的四个异步任务同时执行,下面的伪代码可以轻松实现。

//Dispatch code in Main thread , unless we swithc to antoehrvar job = GlobalScope.launch(Dispatchers.Main) {
//force task A B C D to run in IO threadvar A = async (Dispatchers.IO){//do something in IO thread pool }   var B = async (Dispatchers.IO){//do something in IO thread pool }   var C = async (Dispatchers.IO){//do something in IO thread pool } var D = async (Dispatchers.IO){//do something in IO thread pool }
//join 4 tasks (similar to merge concept in RxJava),A.await()B.await()C.await()D.await()


复制代码这一刻我崩溃了,这个世界上竟然还有除了RxJava之外的框架可以做到组合连接。


也可能我高估了自己的预判能力,在学习WorkManager之后,我发现,WorkManager也有同样的功能。。。比如下面的串行执行异步任务

WorkManager.getInstance()          .beginWith(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).build())          .then(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).build())          .then(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).build())


RxJava -> Coroutine -> WorkManager


这三个框架对异步任务的连接,合并等等逻辑操作从强大到功能有所局限整齐的排列着,但同样的,实现的复杂度也从高到底排列。


这又回到了我们开头讲的Trade Off。怎么样从团队,代码复杂度和功能的强大与否直接做权衡。


总结一下


写到最后我想稍微解释一下,可能本身这两篇文章有些许的标题党,号称最全的选型指南,这也是我想吸引眼球的一种方式,结果到最后也没有给读者一个结论到底用哪个框架,如果有读者因为这个原因感觉被欺骗了,那在此我想说一声抱歉。不过我相信,在读完这篇文章之后,你可能也会发现选型这个问题需要先了解框架本身使用的Trade Off。不能因为我喜欢,或者我觉得就轻易的做决定或者尝试说服你的反对者或者老板。如果我的文章可以让你稍微对多线程做技术选型的时候能多做一丢丢的思考,我想我也就达到了我写这两篇文章的初衷。

浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报