Android多线程技术选型最全指南(part 2 - 认识解决方案)

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

维度的Trade Off

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

Trade

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

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

0_d6E-nXIUTOdBkpE_.png

我先来翻译翻译这张图。

横轴从左往右分别是Best-Effort(可以理解为尽力而为)还有Guaranteed Execution(保证执行). 竖轴从上往下分别是Exact Timing(准确的时间点)还有Deferrable(可以被延迟).
这张图分别从在多线程下执行的代码的可执行性和执行时间来把框架分成了四个维度。其中我想先说说个人的理解:
对于安卓里面的里面的任何代码,都逃不开生命周期这个话题。因为安卓的四大组件有两个都是有生命周期的,而且对于用户来说,可见的Activity或者Fragment才是他们最关心app的部分。所以一段代码,在保证没有内存泄漏的情况下,能不能在异步框架下执行完毕,就得取决于代码所在载体(Activity/Fragment)的生命周期了。比如上一期我们说到的RxJava的例子:

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

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

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

images.jpeg

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

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

    @Override
    public 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闪亮登场。

xAndroid-Jetpack.png.pagespeed.ic.MpRuNNWmpe.png

说起这个框架就屌了。使用它可以轻松的实现异步任务的调度,运行。当然仅仅是普通的执行异步任务好像没那么吸引人,毕竟很多其他的优秀异步框架也可以实现。我们看看官方的解释:
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),那么谷歌就自己制定规则,在新的操作系统中,谷歌进一步缩减后台任务可以执行的条件。

具体限制

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

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

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

download (3).jpeg

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

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

复杂度的Trade Off

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

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

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

Screen Shot 2019-07-20 at 12.17.11 PM.png

当我的队友还在用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的操作符也可以同样的实现上面的例子,还更容易理解和阅读。。。


Screen Shot 2019-07-20 at 12.37.55 PM.png

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

//Dispatch code in Main thread , unless we swithc to antoehr
var job = GlobalScope.launch(Dispatchers.Main) {
//force task A B C D to run in IO thread
var 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也有同样的功能。。。
比如下面的串行执行异步任务


Screen Shot 2019-07-20 at 12.44.22 PM.png
   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。不能因为喜欢,或者觉得就轻易的做决定或者尝试说服你的反对者或者老板。如果我的文章可以让你稍微对多线程做技术选型的时候能多做一丢丢的思考,我想我也就达到了我写这两篇文章的初衷。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容