android多线程回调,Android 踩过的几个误区 | 多线程篇

原标题:Android 踩过的几个误区 | 多线程篇

本文作者

作者:qing的世界

链接:

https://juejin.im/post/5d1eb4acf265da1bb003de71

前段时间在组内做了一下现有的代码分析,发现很多以前的legacy code多线程的使用都不算是最佳实践,而且坏事的地方在于,刚毕业的学生,因为没有别的参照物,往往会复制粘贴以前的旧代码,这就造成了坏习惯不停的扩散。

所以本人就总结分析了一下Android的多线程技术选型,还有应用场景。借着和组内分享的机会也在简书上总结一下。因为自己的技术水平有限,有不对的地方还希望大家能多多指正。(代码的例子方面,肯定不能用我们自己组内产品的源代码,都是我修改过的).

这篇文章我会先分析一些大家可能踩过的雷区,然后再列出一些可以改进的地方。

1.在代码中直接创建新的Thread.

newThread( newRunnable {

@Override

publicvoidrun{

}

}).start;

以上的做法是非常不可取的,缺点非常的多,想必大部分朋友面试的时候都会遇到这种问题,分析一下为啥不可以。浪费线程资源是第一,最重要的是我们无法控制该线程的执行,因此可能会造成不必要的内存泄漏。

在Activity或者Fragment这种有生命周期的控件里面直接执行这段代码,相信大部分人都知道会可能有内存泄漏。

但是就算在其他的设计模式,比如MVP,同样也可能会遇到这个问题。

//runnable->presenter->view

publicclassPresenter{

//持有view引用

privateIView view;

publicPresenter(IView v){

this.view = v;

}

publicvoiddoSomething(String[] args){

newThread( newRunnable {

@ Override

publicvoidrun{

/**

** 持有presenter引用

**/

//do something

}

}).start;

}

publicstaticinterfaceIView{}

}

比如图中的一段代码(我标记了引用方向),通常MVP里面的View都是一个接口,但是接口的实现可能是Activity。那么在代码中就可能存在内存泄漏了。

Thread的runnable是匿名内部类,持有presenter的引用,presenter持有view的引用。这里的引用链就会造成内存泄漏了。关键是,就算你持有线程的句柄,也无法把这个引用关系给解除。

所以优秀的设计模式也阻止不了内存泄漏。。。。。

2.频繁使用HandlerThread

虽然HandlerThread是安卓framework的亲儿子,但是在实际的开发过程中却很少能有他的适用之处。HandlerThread继承于Thread类,所以每次开启一个HandlerThread就和开启一个普通Thread一样,很浪费资源。我们可以通过使用HandlerThread的例子来分析他最大的作用是什么。

staticHandlerThread thread = newHandlerThread( "test");

static{

thread.start;

}

publicvoidtestHandlerThread{

Handler handler = newHandler(thread.getLooper);

handler.post( newRunnable {

@Override

publicvoidrun{

//do something

}

});

//如果不需要了就remove handler's message

handler.removeCallbacksAndMessages( null);

}

publicvoidtest{

//如果我还想利用HandlerThread,但是已经丢失了handler的句柄,那么我们利用handler thread再构建一个handler

Handler handler = newHandler(thread.getLooper);

handler.post( newRunnable {

@Override

publicvoidrun{

//do something

}

});

}

综上所述,HandlerThread最屌的地方就在于,只要你还有它的句柄,你可以随时拿到在该线程下创建的Looper对象,用于生成一个Handler。

之后post的所有runnable都可以在该HandlerThread下运行。然而。。

66ed4e89595057e96a6b8cabaa69fa39.png

在实际的开发中,我们好像很难找到这么一个需求,要在指定的一个线程下执行某些任务。

注意了是指定的一个,不是一些(线程池)。唯一比Thread厉害的地方恐怕就是可以取消未执行的任务,减少内存泄漏的情况了吧。

不过个人观点是线程池好像也可以做到。所以并没有察觉 HandlerThread有任何的优势。而且其实实现也很简单,我们可以随时手写一个简陋版的HandlerThread.

publicstaticclassDemoThreadextendsThread{

privateLinkedBlockingQueue queue = newLinkedBlockingQueue<>;

@Override

publicvoidrun{

super.run;

while( true){

if(!queue.isEmpty){

Runnable runnable;

synchronized( this){

runnable = queue.poll;

}

if(runnable!= null) {

runnable.run;

}

}

}

}

publicsynchronizedvoidpost(Runnable runnable){

queue.add(runnable);

}

publicsynchronizedvoidclearAllMessage{

queue.clear;

}

publicsynchronizedvoidclearOneMessage(Runnable runnable){

for(Runnable runnable1 : queue){

if(runnable == runnable1){

queue.remove(runnable);

}

}

}

}

publicvoidtestDemoThread{

DemoThread thread = newDemoThread;

thread.start;

//发一个消息

Runnable r = newRunnable {

@Override

publicvoidrun{

}

};

thread.post(r);

//不想执行了。。。。删掉

thread.clearOneMessage(r);

}

看分分钟完成HandlerThread能做到的一切。。。。是不是很简单。

3.直接使用AsyncTask.execute

AsyncTask.execute( newRunnable {

@Override

publicvoidrun{

}

});

个人认为AsyncTask的设计暴露了这个接口方法谷歌做的非常不恰当。它这样允许开发者直接使用AsyncTask本身的线程池,我们可以看看源代码做验证

@MainThread

publicstaticvoidexecute(Runnable runnable){

sDefaultExecutor.execute(runnable);

}

果不其然,execute直接访问了executor。

这样的问题在于,这样使用完全丧失了AsyncTask本身的意图。个人的观点是,AsyncTask提供了一个后台任务切换到主线程的通道,就像RxJava的subscribeOn/observeOn一样,同时提供cancel方法,可以取消掉切换回主线程执行的代码,从而防止内存泄漏。

AsyncTask asyncTask = newAsyncTask {

@Override

protectedObject doInBackground(Object[] objects){

returnnull;

}

@Override

protectedvoidonPostExecute(Object o){

//1.提供了后台线程切换回主线程的方法

super.onPostExecute(o);

}

};

//2.可以随时取消

asyncTask.cancel( true);

But!如果直接使用execute方法的话,我们完全没有利用到AsyncTask本身设计的初衷下的优势,和直接自己创建一个线程池没有任何区别,还存在内存泄漏的风险。这样的用法,肯定不能称之为best practice.

4.以为RxJava的unsubscribe能包治百病

d8531136c0751dbe1f6df59a56883a8a.png

我们以一个实际的app例子开始,让我们看看youtube的app退订频道功能:

e012761e63a5d560ebe274d399952ace.png

用户点击退订按钮之后,app发出api call,告诉后台我们停止订阅该频道,同时把UI更新为progress bar,当api call结束,在api的回调里面我们更新UI控件显示已退订UI。

我们写一个示例代码看看:

但是万一用户在点击退订按钮,但是api call还没发出去之前就退出了app呢?

publicclassYoutubePlayerActivityextendsActivity{

privateSubion subion;

publicvoidsetUnSubscribeListner{

unsubscribeButton.setOnClickListener( newView.OnClickListener {

@Override

publicvoidonClick(View v){

subion = Observable.create( newObservable.OnSubscribe {

@Override

publicvoidcall(Subscriber superVoid> subscriber){

try{

//在这里我们做取消订阅的API, http

API api = newAPI;

api.unSubscribe;

}

catch(Exception e){

subscriber.(e);

}

subscriber.onNext( null);

subscriber.onCompleted;

}

})

.subscribeOn(Schedulers.io)

.observeOn(AndroidSchedulers.mainThread)

.subscribe( newAction1 {

@Override

publicvoidcall(Void aVoid){

//API call成功!,在这里更新订阅button的ui

unsubscribeButton.toggleSubionStatus;

}

});

}

});

}

@Override

protectedvoidonDestroy{

super.onDestroy;

//onDestroy 里面对RxJava stream进行unsubscribe,防止内存泄漏

subion.unsubscribe;

}

}

看似好像没啥问题,没有内存泄漏,可以后台线程和主线程直接灵活切换,更新UI不会crash。而且我们使用了Schedulers.io调度器,看似也没有浪费线程资源。

BUT!!!!!!

我们先仔细想想一个问题。我们在点击button之后,我们的Observable

API api = newAPI;

api.unSubscribe;

答案是NO。

因为我们的Observable是subscribeOn io线程池。

如果该线程池现在非常拥挤,这段代码,这个Observable是不会立刻执行的。该段代码会华丽丽的躺在线程池的队列中,安安静静的等待轮到自己执行。

那么如果用户点击按钮,同时退出app,我们unubscribe了这个RxJava 的observable 我们就存在一个不会执行api call的风险。也就是用户点击退订按钮,退出app,返回app的时候,会发现,咦,怎么明明点了退订,竟然还是订阅状态?

是不是所有异步调用,都需要和Activity或者fragment的生命周期绑定?

答案同样是NO,在很多应用场景下,当用户做出一个行为的时候,我们必须坚定不移的执行该行为背后的一切操作,至于异步操作完成之后的UI更新,则视当前Activity或者fragment的生命周期决定。

也就是异步操作和生命周期无关,UI更新和生命周期有关。

简单点说,很多情况下,写操作不能取消,读操作可以。

很多情况下,比如支付,订阅等等这种用户场景,需要涉及到异步操作的都是会有以上的问题。在这些场景下,我们需要遵循以下流程。

9107236431fb5a50825e4578602026db.png

最最重点的部分,就是当用户退出的时候虽然我们停止更新UI,但当用户重新进入的时候,app需要主动的重新向后台发送请求,查看当前订阅状态。这样,才是一个健康的app。

所以很遗憾,RxJava并没有很好的支持这一场景,至于怎么解决,有什么框架比较合适,下一章再介绍。返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值