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

时间:2023-02-14 20:37:34

转载请注明出处:

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

地址:http://blog.csdn.net/qq_22744433/article/details/78297635

目录

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

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来开启一个后台。
相比直接使用线程开启后台,service的优先级更高,更不容易被销毁。

5. 使用线程池

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