源码分析glide对线程中断的优化

转载请注明出处:

源码分析glide对线程中断的优化

地址:http://www.jianshu.com/p/e0190611c25c

目录

android中我们需要很小心对待线程的创建取、监听、取消。如果不小心处理,可能就会引入内存泄漏,监听的生命周期与宿主不一致导致crash,频繁创建线程对资源的消耗,线程无意义的运行等问题。那么这里对于线程中断,源码分析一下glide对其的优化。对于线程创建/监听的选择,总结了一些知识点。

1. 线程中断

1. 线程中断(取消)的方法

我们知道,如果一个线程已经被废弃了(没有监听者了),那么线程就没有继续运行的必要了。如果只是取消监听者,这么做肯定是不够的,因为线程还在运行。所以我们需要中断线程的运行来节省CPU,而线程的中断并不是一件容易的事。大体的方法是:

如果使用Thread.interrupt(),那么当线程处于阻塞状态时(比如wait住,sleep),那么线程会抛出InterruptException异常,退出循环。我们需要捕获这个异常,进行相应处理,不然就crash了。

如果是非阻塞情况下,Thread.interrupt()是不能把线程中断的。这时候只能设置volitie关键字来中断线程。

这两种情况需要配合使用来中断已经废弃且还在运行的线程。
Glide是一个很优秀的图片加载库。在处理大量图片上,其做了很多的优化,那么我们看下其对线程的取消(取消图片的网络加载)做了哪些事情:

2.源码分析glide对线程中断的优化

glide中EngineJob中:

public void removeCallback(ResourceCallback cb) {
Util.assertMainThread();
if (hasResource || hasException) {
    addIgnoredCallback(cb);
} else {
    cbs.remove(cb);
    if (cbs.isEmpty()) {
        cancel();
    }
}
}

void cancel() {
if (hasException || hasResource || isCancelled) {
    return;
}
engineRunnable.cancel();
Future currentFuture = future;
if (currentFuture != null) {
    currentFuture.cancel(true);
}
isCancelled = true;
listener.onEngineJobCancelled(this, key);
}

当一个图片加载任务EngineJob已经没有监听者时,会调用future的cancel()方法。future是提交给线程池任务返回的。当调用future的cancel(true)时,如果任务还没执行,那么就取消任务。如果任务已经执行,但被阻塞了,那么会调用Thread的interrupt()方法中断线程。
EngineJob中往线程池抛的task是:EngineRunnable,当调用其cancel()时:

    private volatile boolean isCancelled;


   public void cancel() {
    isCancelled = true;
    decodeJob.cancel();
}

@Override
public void run() {
    if (isCancelled) {
        return;
    }

    Exception exception = null;
    Resource<?> resource = null;
    try {
        resource = decode();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Exception decoding", e);
        }
        exception = e;
    }

    if (isCancelled) {
        if (resource != null) {
            resource.recycle();
        }
        return;
    }

    if (resource == null) {
        onLoadFailed(exception);
    } else {
        onLoadComplete(resource);
    }
}

使用了volatile关键字来让runnable的方法不再执行。当task还在队列中还没有执行的话(这应该经常发生,如果发送的图片请求太多),那么直接return。如果已经请求执行,请求获取了数据后,也会做一次判断。

那么我们看下进行请求过程中

     resource = decode();,

是不是就不能中断了呢?网络请求在DecodeJob的decodeSource()方法:

  private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
        long startTime = LogTime.getLogTime();
        final A data = fetcher.loadData(priority);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Fetched data", startTime);
        }
        if (isCancelled) {
            return null;
        }
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}

其中isCancelled也是volatie关键字,在 decodeJob.cancel();中会被置为true。前面代码中EngineRunnable#cancle()会调用这个方法。所以在请求完网络数据时,还会判断一下,是不是需要中断。如果需要,那么就不用解码了(解码是很耗时的操作)。那网络请求有没有在这方面做判断呢?真正的网络请求在DataFetcher#load()中。DataFetcher类的存在能够解耦图片加载的具体实现。比如你是使用android原生的http加载的(生成httpUrlConnection...),还是使用其他的协议加载的,还是使用第三方库加载的(比如okhttp)。这里我们以HttpUrlFetcher为例:

    private volatile boolean isCancelled;

     @Override
public InputStream loadData(Priority priority) throws Exception {
    return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
}

private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
        throws IOException {
    if (redirects >= MAXIMUM_REDIRECTS) {
        throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
    } else {
        // Comparing the URLs using .equals performs additional network I/O and is generally broken.
        // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
        try {
            if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
                throw new IOException("In re-direct loop");
            }
        } catch (URISyntaxException e) {
            // Do nothing, this is best effort.
        }
    }
    urlConnection = connectionFactory.build(url);
    for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
      urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
    }
    urlConnection.setConnectTimeout(2500);
    urlConnection.setReadTimeout(2500);
    urlConnection.setUseCaches(false);
    urlConnection.setDoInput(true);

    // Connect explicitly to avoid errors in decoders if connection fails.
    urlConnection.connect();
    if (isCancelled) {
        return null;
    }
    final int statusCode = urlConnection.getResponseCode();
    if (statusCode / 100 == 2) {
        return getStreamForSuccessfulRequest(urlConnection);
    } else if (statusCode / 100 == 3) {
        String redirectUrlString = urlConnection.getHeaderField("Location");
        if (TextUtils.isEmpty(redirectUrlString)) {
            throw new IOException("Received empty or null redirect url");
        }
        URL redirectUrl = new URL(url, redirectUrlString);
        return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
    } else {
        if (statusCode == -1) {
            throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
        }
        throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
    }
}

这里也有一个volitile关键字isCancelled。当http链接已经建立了,这时候在进行请求(code/data)之前判断一下,如果已经取消了,那么就不进行请求数据或code了。isCancelled关键字会在其cancel()方法会置为true。而cancel()方法在DecodeJob的cancel()方法中会被调用。

所以当取消一个任务时,网络请求如果还没加载数据,会进行相应的中断。

通过上面的分析:当一个图片网络任务没有任何监听时,线程处于阻塞状态下、任务还没执行、网络连接后还没请求数据、数据请求结束还没解码、还没发送给监听者这些状态时,都会进行中断取消判断。所以glide在网络请求的取消这块做的真的很棒。

2. 线程的使用

上面说完了线程的取消。在android中使用线程,我们还需要注意是不是会导致内存泄漏,串行/并发,与UI线程交互,线程的创建销毁等问题,那么我这里总结一些知识点。并没有源码分析。

1. 直接new 一个线程

直接

new  thread(new runnable{ 
  @override
  public void run{
  ...
  } 
 }).start();

缺点:

  • 匿名内部类持有外部类的引用,会造成内存泄漏。

  • 线程优先级和ui线程一样高。

  • 需要自己使用handler处理与ui线程的通信。同时由于handler写法如果不规范,handler也会持有外部类的引用,造成内存泄漏。但是,如果handler写成静态内部类,那么如果handler的handleMessage(){//逻辑..}逻辑中使用到了activty中的某些view或成员变量,那么如果activty已经消失了(虽然持有了activty的成员变量,但静态handler并没有持有activty,所以activty还是可能被销毁。导致里面的view为空),那么这时候再操作这些view,就会报空指针。所以只能在逻辑的最前面加一些撇脚的 fragment!=null&&fragment.isAdd()的判断。这是因为这里使用handler处理与ui线程的通信并没有使用观察者模式。所以并没有取消订阅这些操作。导致很可能crash增加。AsyncTask同理。

  • 不好管理线程的取消。

2. 使用AsyncTask

不用自己去处理与ui线程的同步。
缺点:

  • 如果使用匿名内部类,会持有外部类的引用,会造成内存泄漏。
  • 直接使用不含有参数的execute()启动task,那么task是串行执行的。这点虽然不用考虑同步问题,但是如果task多的话,会有性能影响。如果想并发执行task,那么需要使用带参数的execute(ExecutorService),即指定线程池。
  • AsyncTask需要使用cancel()取消订阅(有时候可能会不起作用),不然可能造成crash。与上面同理。

3.使用 HandlerThread

handlerThread 是含有一套looper,handler,messageQueue的线程。如果我们有一个业务场景:需要一个持久的后台线程,且该线程与ui线程需要相互通信。使用handlerThread会比较方便。

缺点

  • 如果activty界面消失了,那么不容易找到这个handlerThread,并且这个thread的优先级并没有那么高,很可能在内存吃紧的时候被销毁。所以HandlerThread一般是在其他组件内部使用,比如IntentService、ThreadPoolExecutor的coreThread都是基于HandlerThread形成的。
  • 使用HandlerThread的handler向HandlerThread抛任务时,是串行执行的。

4. 使用IntentService

前面说了,IntentService内部的工作原理就是service+HandlerThread。我们一般使用service时,一般启动有两种方式:
一种:startService(Intent)通过intent来指派执行任务(service的onStartCommand(intent)会被回调)。生命周期比较长,如果不手调用selfStop()/stopService,那么service会一直存在(即使activty销毁了)。
第二种:如果使用bindService(intent)(service的onBind(ServiceConnection)会被回调),如果所有页面的地方都unBindService(),那么service就会被停止。并且,不像开启一个线程后,我们基本不能再对线程做什么了。我们可以通过binder获取到service的实例(startService()或bindService()也只能回调service的某些生命周期方法,并不能得到service实例本身),进而调用service实例的某些方法来调控后台。

虽然上面两种方式我们都可以在service中创建新线程来执行新的任务。如果我们的任务不紧急,我们也不想操心线程创建的事情。我们就可以直接使用IntentService来开启一个后台。
我们可以实现IntentService的handleIntent(intent)方法。handleIntent默认是在异步线程工作的。IntentService使用context.startService(intent)来启动(类似handler的作用)。会将intent到IntentService中的一个intent队列中,等待IntentService的handleIntent()方法的回调。intent任务串行执行。

相比直接使用线程开启后台,service的优先级更高,更不容易被销毁。

5. 使用线程池

线程池的worker线程(线程池中,线程被封装成了work类)其实和HandlerThread差不多。都是一个无线循环的线程。task执行完了,再到线程池workQueue阻塞队列中拿task来执行。coreThread核心线程即使没有任务,也不会被回收。当task超过了核心线程的数量,那么就放到阻塞队列中。如果阻塞队列也塞满了任务,那么就继续开启worker,直到所有worker的数目到MaxThreadNum,这时候使用某些策略来回应,比如停止接收,报错等。

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

推荐阅读更多精彩内容