前段时间在组内作了一下现有的代码分析,发现不少之前的legacy code多线程的使用都不算是最佳实践,并且坏事的地方在于,刚毕业的学生,由于没有别的参照物,每每会复制粘贴之前的旧代码,这就形成了坏习惯不停的扩散。因此本人就总结分析了一下Android的多线程技术选型,还有应用场景。借着和组内分享的机会也在简书上总结一下。由于本身的技术水平有限,有不对的地方还但愿你们能多多指正。(代码的例子方面,确定不能用咱们本身组内产品的源代码,简书上的都是我修改过的)java
这篇文章我会先分析一些你们可能踩过的雷区,而后再列出一些能够改进的地方。面试
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
复制代码
以上的作法是很是不可取的,缺点很是的多,想必大部分朋友面试的时候都会遇到这种问题,分析一下为啥不能够。浪费线程资源是第一,最重要的是咱们没法控制该线程的执行,所以可能会形成没必要要的内存泄漏。在Activity或者Fragment这种有生命周期的控件里面直接执行这段代码,相信大部分人都知道会可能有内存泄漏。可是就算在其余的设计模式,好比MVP,一样也可能会遇到这个问题。设计模式
//runnable->presenter->view
public class Presenter {
//持有view引用
private IView view;
public Presenter(IView v){
this.view = v;
}
public void doSomething(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
/** ** 持有presenter引用 **/
//do something
}
}).start();
}
public static interface IView{}
}
复制代码
好比图中的一段代码(我标记了引用方向),一般MVP里面的View都是一个接口,可是接口的实现多是Activity。那么在代码中就可能存在内存泄漏了。Thread的runnable是匿名内部类,持有presenter的引用,presenter持有view的引用。这里的引用链就会形成内存泄漏了。关键是,就算你持有线程的句柄,也没法把这个引用关系给解除。api
因此优秀的设计模式也阻止不了内存泄漏。。。。。bash
虽然HandlerThread是安卓framework的亲儿子,可是在实际的开发过程当中却不多能有他的适用之处。HandlerThread继承于Thread类,因此每次开启一个HandlerThread就和开启一个普通Thread同样,很浪费资源。咱们能够经过使用HandlerThread的例子来分析他最大的做用是什么。多线程
static HandlerThread thread = new HandlerThread("test");
static {
thread.start();
}
public void testHandlerThread(){
Handler handler = new Handler(thread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
//do something
}
});
//若是不须要了就remove handler's message
handler.removeCallbacksAndMessages(null);
}
public void test(){
//若是我还想利用HandlerThread,可是已经丢失了handler的句柄,那么咱们利用handler thread再构建一个handler
Handler handler = new Handler(thread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
//do something
}
});
}
复制代码
综上所述,HandlerThread
最屌的地方就在于,只要你还有它的句柄,你能够随时拿到在该线程下建立的Looper对象,用于生成一个Handler。以后post的全部runnable均可以在该HandlerThread下运行。 然而。。 app
HandlerThread
有任何的优点。并且其实实现也很简单,咱们能够随时手写一个简陋版的HandlerThread.
public static class DemoThread extends Thread{
private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
@Override
public void run() {
super.run();
while(true){
if(!queue.isEmpty()){
Runnable runnable;
synchronized (this){
runnable = queue.poll();
}
if(runnable!= null) {
runnable.run();
}
}
}
}
public synchronized void post(Runnable runnable){
queue.add(runnable);
}
public synchronized void clearAllMessage(){
queue.clear();
}
public synchronized void clearOneMessage(Runnable runnable){
for(Runnable runnable1 : queue){
if(runnable == runnable1){
queue.remove(runnable);
}
}
}
}
public void testDemoThread(){
DemoThread thread = new DemoThread();
thread.start();
//发一个消息
Runnable r = new Runnable() {
@Override
public void run() {
}
};
thread.post(r);
//不想执行了。。。。删掉
thread.clearOneMessage(r);
}
复制代码
看分分钟完成HandlerThread能作到的一切。。。。是否是很简单。框架
AsyncTask.execute(new Runnable() {
@Override
public void run() {
}
});
复制代码
我的认为AsyncTask的设计暴露了这个接口方法谷歌作的很是不恰当。它这样容许开发者直接使用AsyncTask自己的线程池,咱们能够看看源代码作验证异步
@MainThread
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
复制代码
果不其然,execute直接访问了executor。async
这样的问题在于,这样使用彻底丧失了AsyncTask自己的意图。我的的观点是,AsyncTask提供了一个后台任务切换到主线程的通道,就像RxJava的subscribeOn/observeOn同样,同时提供cancel方法,能够取消掉切换回主线程执行的代码,从而防止内存泄漏。
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Object doInBackground(Object[] objects) {
return null;
}
@Override
protected void onPostExecute(Object o) {
//1.提供了后台线程切换回主线程的方法
super.onPostExecute(o);
}
};
//2.能够随时取消
asyncTask.cancel(true);
复制代码
But!若是直接使用execute方法的话,咱们彻底没有利用到AsyncTask自己设计的初衷下的优点,和直接本身建立一个线程池没有任何区别,还存在内存泄漏的风险。这样的用法,确定不能称之为best practice
.
这个误区标题起的有点模糊,这个没办法,由于例子有点点复杂。让我来慢慢解释。
咱们以一个实际的app例子开始,让咱们看看youtube的app退订频道功能:
用户点击退订按钮以后,app发出api call,告诉后台咱们中止订阅该频道,同时把UI更新为progress bar,当api call结束,在api的回调里面咱们更新UI控件显示已退订UI。咱们写一个示例代码看看:
完美!
可是万一用户在点击退订按钮,可是api call还没发出去以前就退出了app呢?
public class YoutubePlayerActivity extends Activity {
private Subscription subscription;
public void setUnSubscribeListner(){
unsubscribeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
subscription = Observable.create(new Observable.OnSubscribe<Void>() {
@Override
public void call(Subscriber<? super Void> subscriber) {
try {
//在这里咱们作取消订阅的API, http
API api = new API();
api.unSubscribe();
}
catch (Exception e){
subscriber.onError(e);
}
subscriber.onNext(null);
subscriber.onCompleted();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Void>() {
@Override
public void call(Void aVoid) {
//API call成功!,在这里更新订阅button的ui
unsubscribeButton.toggleSubscriptionStatus();
}
});
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
//onDestroy 里面对RxJava stream进行unsubscribe,防止内存泄漏
subscription.unsubscribe();
}
}
复制代码
看似好像没啥问题,没有内存泄漏,能够后台线程和主线程直接灵活切换,更新UI不会crash。并且咱们使用了Schedulers.io()调度器,看似也没有浪费线程资源。
BUT!!!!!!
咱们先仔细想一想一个问题。咱们在点击button以后,咱们的Observable
API api = new API();
api.unSubscribe();
复制代码
会马上执行么?
答案是NO。由于咱们的Observable是subscribeOn io线程池。若是该线程池如今很是拥挤,这段代码,这个Observable是不会马上执行的。该段代码会华丽丽的躺在线程池的队列中,安安静静的等待轮到本身执行。
那么若是用户点击按钮,同时退出app,咱们unubscribe了这个RxJava 的observable 咱们就存在一个不会执行api call的风险。也就是用户点击退订按钮,退出app,返回app的时候,会发现,咦,怎么明明点了退订,居然仍是订阅状态?
这就回到了一个本质问题,来自灵魂的拷问。是否是全部异步调用,都须要和Activity或者fragment的生命周期绑定?
答案一样是NO,在不少应用场景下,当用户作出一个行为的时候,咱们必须坚决不移的执行该行为背后的一切操做,至于异步操做完成以后的UI更新,则视当前Activity或者fragment的生命周期决定。也就是异步操做和生命周期无关,UI更新和生命周期有关。简单点说,不少状况下,写操做不能取消,读操做能够。
不少状况下,好比支付,订阅等等这种用户场景,须要涉及到异步操做的都是会有以上的问题。在这些场景下,咱们须要遵循如下流程。
因此很遗憾,RxJava并无很好的支持这一场景,至于怎么解决,有什么框架比较合适,下一章再介绍。