当前位置: 首页 > 科技观察

Android多线程技术哪家强?

时间:2023-03-12 13:16:25 科技观察

TradeOff今天我想说一个英文单词,叫做TradeOff。中文翻译可以叫做平衡和妥协,但这样干巴巴的翻译未必能体现出这个词的厉害之处。让我举一个例子。比如迪丽热巴和谢娜同时追求我。虽然迪丽热巴的颜值更好,但考虑到谢娜在湖南卫视的地位,和她在一起后给我带来的曝光率,我还是选择了谢娜。...(以上纯属玩笑)反正。..这就是TradeOff,一个非常艰难的选择,但最终人们往往会以自身利益最大化来做最后的决定。TradeOff这个词贯穿于软件开发的所有过程,也体现在多线程的选择上。Google在2018年的IO大会上官方放了这样一张图,我先翻译一下这张图:横轴从左到右分别是Best-Effort(可以理解为尽力而为)和GuaranteedExecution(保证执行)。纵轴从上到下分别是ExactTiming(准确的时间点)和Deferrable(可以延迟)。该图根据多线程下执行的代码的可执行性和执行时间将框架分为两部分。四个维度。其中,我想先说说我个人的理解:对于Android中的任何代码,都绕不开生命周期这个话题。因为Android的四大组件中有两个是有生命周期的,而对于用户来说,可见的Activity或者Fragment才是他们最关心的app的一部分。因此,一段代码能否在异步框架下执行,同时保证不发生内存泄漏,取决于代码所在的载体(Activity/Fragment)的生命周期。比如我们上一期提到的RxJava例子:@OverrideprotectedvoidonDestroy(){super.onDestroy();//在onDestroy中取消订阅RxJavastream防止内存泄露subscription.unsubscribe();}这段代码可能会阻止我们调用API中的可观察的。那么在Android的生命周期的背景下,这段代码就是BestEffort,尽力而为。能跑就跑,活动没了就拉下来。..所以把上面例子中的代码换成图中的ThreadPool,你就明白了。那么保证执行呢?很明显,就是用图中的Foreground服务来完成的。与Activity或Fragment不同,Service虽然也有生命周期,但它的生命周期不像前两者那样由用户控制。Service的生命周期可以由开发者决定,所以我们可以使用Foregroundservice+ThreadPool来保证代码可以执行。使用ForegroundService是因为Android在Oreo之后修改了Service的优先级,在app进入后台空闲超过一分钟后会自动杀死任何后台Service。但是使用ForegroundService需要开发者开启一个Notification。@OverridepublicvoidonCreate(){super.onCreate();startForeground(1,notification);Log.d(TAG_FOREGROUND_SERVICE,"MyforegroundserviceonCreate().");}这样就可以了,虽然保证程序能正常运行,但是我们的UX有改变了现在,你必须向设计狮子解释,这都是Android和Google的错!我不想在状态栏中出现一个突然的图标。..还记得去年修改我们产品下载音乐的服务时,为了防止服务被破坏,把notification改成了前台服务的notification,我们的产品经理来问我为什么notification可以不被划掉。...对产品经理的教育也花了很长时间。你看,这就是TradeOff,从竭尽全力到想要保证代码必须运行。中间有这样的取舍。然后我们又开始思考,既然ForegroundService这么痛苦,能不能要求一个框架,既能保证执行又不改变我们app的UX?当当当当!WorkManager登场。说起这个框架真是棒极了。使用它可以轻松实现异步任务的调度和运行。当然,仅仅只是普通的执行异步任务似乎并没有那么吸引人,毕竟还有很多其他优秀的异步框架也可以实现。我们来看看官方的解释:WorkManagerAPI可以很容易地安排可延迟的、异步的任务,这些任务即使在应用程序退出或设备重启时也应该运行。focus,即使app退出或设备重启,意味着即使app退出或重启,你也可以保证你的异步任务被完整执行。这样就完美的解决了我们使用ForegroundService或者ThreadPool的问题。可以保证任务的完整执行,不需要因为启动前台服务而改变UX!WorkManager的实现细节和源码这里就不详细解释了。.直接说上一个youtube退订的例子(这个例子用的是kotlin,因为懒得重写一个java版本。。。)!我们定义一个Worker:classMakeSubscriptionWorker:Worker{constructor(context:Context,parameterName:WorkerParameters):super(context,parameterName)overridefundoWork():Result{//unsubscribe的API调用在这里完成valapi=API()varresponse=api.unSubscribe()if(response!=null){returnResult.success(response)}else{returnResult.failure()}}}复制代码Worker实际上执行我们的取消订阅API调用。然后听听我们的退订成功//1。创建我们的Worker实例并开始执行!WorkManager.getInstance().enqueue(OneTimeWorkRequest.Builder(MakeSubscriptionWorker::class.java!!).addTag(MakeSubscriptionWorker::class.simpleName!!).build())//2.将API调用的结果转换为JetpackLiveData,并开始监听结果WorkManager.getInstance().getWorkInfosByTagLiveData(MakeSubscriptionWorker::class.simpleName!!).observe(this,purchaseObservaer)//3.如果用户退出Activity,停止监听结果WorkManager.getInstance().getWorkInfosByTagLiveData(MakeSubscriptionWorker::class.simpleName!!).removeObserver(purchaseObservaer)关注第三步。虽然我们停止监听,但这并不意味着这个异步任务会被取消。它将继续执行。不过这和我们使用线程池+非匿名内部类Runnable似乎没有本质区别。毕竟在上面的例子中,kotlin的内部类本身就是静态的。没有内存泄漏。回到开头说的,WorkManager可以保证任务一定会执行,即使你退出了app!WorkManager会将你的任务执行id和相关信息保存在数据库中,App重新打开后,会根据你在任务限制中设置的设置(比如有些任务必须在Wi-Fi下执行,WorkManager提供这样的API)来重新打开你未完成的任务。也就是说,即使我们在点击取消订阅后立即强行关闭App,WorkManager下次打开时也可以重启任务!!!那。..为什么我们不马上开始使用这么酷的功能????还记得我反复提到的权衡这个词吗?WorkManager也有其权衡取舍。首先,虽然官方的重点是确保任务执行,但也提到:WorkManager是为可延迟的任务——即不需要立即运行的任务而设计的。换句话说,WorkManager的主要目的是针对那些允许/可以容忍为任务设计的延迟的异步任务。能够忍受延迟很有趣。谁会想要无目的地延迟他们想要运行的异步任务?这个问题的答案其实就是安卓用户一直关心的续航问题。Android在经历了最初的大度之后,开始越来越在意用户体验。由于App开发者不遵守游戏规则(是的,我说的是那些无耻的xxkeepalive应用程序),那么Google就制定了自己的规则。在新的操作系统中,谷歌进一步降低了后台任务可以执行的条件。具体限制(https://developer.android.com/guide/background/)上图中,简单的说就是当APP进入后台时,异步任务被严格限制。嗯,作为谷歌自己开发的WorkManager,一个号称能够在应用关闭后重启异步任务的框架当然要遵循这个规则。所以,所谓的延迟并没有那么可怕。笔者亲测,当App还在前台执行WorkManager时,异步任务基本会立即进入调度执行,但是当App进入后台时,WorkManager会尝试挂起Task。所以在我们上面的例子中,WorkManager也是可用的。但!TradeOff又来了。WorkManager虽然和Activity的生命周期没有关系,但是关系到整个App的前后台状态。App的退出可以暂停WorkManager中的任务,也就是说,控制他能不能执行的关键已经从开发者手里转到了用户手里。...这个已经讲了大部分章节的WorkManager了,怎么又回来了。说了这么多,从ThreadPool到ForegroundService,再到WorkManager。我们似乎每次解决一个问题后都会遇到新的问题,好像没有完美的解决方案。没错,这些都是取舍,取舍,软件开发没有完美的答案,杀吸血鬼才有灵丹妙药,软件开发?不存在。..上面TradeOff的复杂性部分,我是从Google官方的解释,即从执行时间和任务能否完全执行的角度来回顾我们现有的解决方案。接下来,我想从代码复杂度的角度来谈谈。2015年开始接触RxJava,刚开始学习RxJava的时候,有点难懂,尤其是flatMap操作符,花了我整整一周的时间才消化。但是在越来越熟悉之后,我渐渐爱上了RxJava。当时觉得函数式编程的运算符真的很酷。酷炫的干员们堆在一起,简直就是疯狂、酷酷、霸道。况且团队里懂RxJava的人不多,每个人都有问题。他们都会找我,我的虚荣心很快膨胀到月亮。..记得当时在重构一个app冷启动任务调度的代码。当时任务的依赖图大概是这样的:当时队友还在用LacthCoundown,走投无路。我使用RxJava的mergeWith和ConcatMap轻松解决了它:B.mergeWith(C).concatMap(E).concatMap(F).mergeWith(A.concatMap(D))。!这进一步坚定了我的信念,即RxJava是世界上最好的异步任务框架。...直到我从一家初创公司来到AmazonMusic,从一个只有3人的Android团队变成了一个有四个大团队同时开发一个产品的组织。突然发现,推广RxJava的时间成本,还有团队学习的成本,已经不能和之前的创业公司相比了。一开始,每次看到队友的codereview,我都喜欢说:“你知道的,如果我们这里用RxJava……”,直到有一次组里的前辈问我:“为什么RxJava更好?》,我才发现自己好像从来没有系统地总结过RxJava的优缺点,一时间有些无语。我什至发现有时候一些简单的集合处理使用RxJava就变得复杂了,RxJava的可读性还是以团队熟悉为前提,更何况学习成本导致产品迭代变慢。那一刻,我仿佛失去了灵魂。我引以为豪的RxJava退化为无!这不,我们的RxJava显然对异步任务的组合和连接有很强的支持!mergeWith,concatMap,太牛逼了强制运算符不是使用RxJava的最佳理由!我就这样和前辈反击。..直到我看到协程。...Coroutine的算子也可以用同样的方式实现上面的例子,更容易理解和阅读。..如果你想同时执行上面的四个异步任务,下面的伪代码可以轻松实现。//DispatchcodeinMainthread,unlessweswithctoantoehrvarjob=GlobalScope.launch(Dispatchers.Main){//forcetaskABCDtoruninIOthreadvarA=async(Dispatchers.IO){//dosomethinginIOthreadpool}varB=async(Dispatchers.IO){//dosomethinginIOthreadpool}varC=async.IOthread(){//dosomethinginIOthreadpool}varD=async(Dispatchers.IO){//dosomethinginIOthreadpool}//join4tasks(similartomergeconceptinRxJava),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这三个框架连接合并来自强大的异步任务它们被整齐地排列成功能受限的程度,但同样,实现的复杂度也是从高到低排列的。这又回到了我们一开始谈到的TradeOff。如何从团队实力、代码复杂度和功能性上做出直接的权衡。综上所述,写到最后,我想稍微说明一下。可能这两篇文章的标题有点党,号称是最全的选型指南。这也是我想吸引眼球的一种方式。最后,我没有给读者一个结论是用什么框架?如果有读者因此而感到受骗,在此表示歉意。不过,相信看完这篇文章,你可能也会发现,模型选择的问题需要了解框架本身使用的TradeOff。不要仅仅因为我喜欢它或我的想法就做出决定或试图说服你的反对者或你的老板。如果我的文章能让大家对多线程技术选型多一点思考,我想我已经达到了写这两篇文章的初衷。