简单例子教你理解ImageLoader

时间:2022-11-30 20:50:18

图文并茂是现代app的发展趋势,而随着移动互联网的发达,大家也不可能把图片写死在客户端,所以动态加载服务器上的图片成为了app起码的能力之一。当图片较少时,一般使用AsyncTask就能解决问题,但是当同屏包含大量图片的时候,简单的单线程单图片的解决方案就会遇到性能和效率的瓶颈,这时候ImageLoader框架就应运而生了。Github上成熟的框架很多,这里不会纠结于具体的功能,主要通过简单的demo让没接触过框架的童鞋最效率的理解其核心思想。

先画个图:
简单例子教你理解ImageLoader
请各位保持冷静,不要吐槽我的图片。大概讲解一下,界面层会在某个时刻同步产生大量的图片请求,这些请求会被封装成Request对象,丢到产线,也就是BlockingQueue上去,后端会有一个线程池根据app需求产生定制数量的线程(可理解为工人)然后从queue中获取任务并进行处理。当没有任务的时候,线程会阻塞等待bolckingqueue中新的任务的到来。

好了,最重要的就是这个思想,代码其实并不重要,下面上代码。

/**
* Created by Amuro on 16/10/10.
*/

public class SDKImageLoader
{

private SDKImageLoader()
{}

private static SDKImageLoader instance;

public static SDKImageLoader getInstance()
{
if(instance == null)
{
synchronized (SDKImageLoader.class)
{
if(instance == null)
{
instance = new SDKImageLoader();
}
}
}

return instance;
}

private ImageLoaderConfig config;
private RequestQueue requestQueue;

public void init(ImageLoaderConfig config)
{
this.config = config;
requestQueue = new RequestQueue();
requestQueue.invoke();
}

public void displayImage(ImageView imageView, String url)
{
displayImage(imageView, url, config.getFailedResId(), config.getLoadingResId());
}

public void displayImage(ImageView imageView, String url, int failedResId, int loadingResId)
{
Request request = new Request(
imageView, url, failedResId, loadingResId, config.getLoadPolicy());
requestQueue.addRequest(request);
}

public void destroy()
{
if(requestQueue != null)
{
requestQueue.stop();
}
}
}

外部调用的接口核心类,初始化的时候需要传入一个Config参数,看下config类:


/**
* Created by Amuro on 16/10/10.
*/

public class ImageLoaderConfig
{

private int failedResId;
private int loadingResId;
private LoadPolicy loadPolicy;

public int getFailedResId()
{
return failedResId;
}

public int getLoadingResId()
{
return loadingResId;
}

public LoadPolicy getLoadPolicy()
{
return loadPolicy;
}

public static class Builder
{

private int failedResId;
private int loadingResId;
private LoadPolicy loadPolicy = new SerialPolicy();

public Builder setFailedResId(int id)
{
failedResId = id;
return this;
}

public Builder setLoadingResId(int id)
{
loadingResId = id;
return this;
}

public Builder setLoadPolicy(LoadPolicy loadPolicy)
{
this.loadPolicy = loadPolicy;
return this;
}

public ImageLoaderConfig build()
{
ImageLoaderConfig config = new ImageLoaderConfig();
config.failedResId = failedResId;
config.loadingResId = loadingResId;
config.loadPolicy = loadPolicy;

return config;
}
}
}

这里用到了变体的建造者模式,可参考Android源码里的AlertDialog,这里我们也跟风玩一下,三个参数分别是默认加载失败时展示的图片,载入是加载的图片,已经加载的策略也就是Request之间的优先级处理策略。看一下初始化我们ImageLoader的代码:

ImageLoaderConfig config =
new ImageLoaderConfig.Builder().
setFailedResId(R.mipmap.failed).
setLoadingResId(R.mipmap.loading).
build();

SDKImageLoader.getInstance().init(config);

没错,就是这样风骚。

初始化主要就是读取配置,然后把queue启动起来,我们来看一下RequestQueue这个类:


/**
* Created by Amuro on 16/10/10.
*/

public class RequestQueue
{

private static final int DEFAULT_THREAD_COUNT =
Runtime.getRuntime().availableProcessors() + 1;

private BlockingQueue<Request> queue = new PriorityBlockingQueue<Request>();
private List<RequestDispatcher> dispatcherList;
private int threadCount;

public RequestQueue()
{
this(DEFAULT_THREAD_COUNT);
}

public RequestQueue(int threadCount)
{
this.threadCount = threadCount;
this.dispatcherList = new ArrayList<>(threadCount);
}

public void addRequest(Request request)
{
if(!queue.contains(request))
{
this.queue.add(request);
}
}

public void invoke()
{
stop();
for(int i = 0; i < threadCount; i++)
{
RequestDispatcher dispatcher = new RequestDispatcher(queue);
dispatcherList.add(dispatcher);
dispatcher.start();
}
}

public void stop()
{
if(dispatcherList != null && dispatcherList.size() == threadCount)
{

for (int i = 0; i < threadCount; i++)
{
RequestDispatcher dispatcher = dispatcherList.get(i);
if (dispatcher != null)
{
dispatcher.interrupt();
}

}
}
}
}

没错,这个类启动的时候,其实就是初始化了我们的BlockingQueue队列,然后初始化了处理任务的线程池,其中的线程数量默认是CPU核心数加1,当然也可以通过外部传入指定的线程数。重点在invoke方法,这里根据线程数量初始化了同样数量的RequestDispatcher,并start,我们来看一下这个类:

/**
* Created by Amuro on 16/10/10.
*/

public class RequestDispatcher extends Thread
{

private BlockingQueue<Request> queue;

public RequestDispatcher(BlockingQueue<Request> queue)
{
this.queue = queue;
}

@Override
public void run()
{
while (!interrupted())
{
try
{
Request request = queue.take();
disposeRequest(request);

}
catch (Exception e)
{
e.printStackTrace();
}
}

}

private void disposeRequest(final Request request) throws Exception
{
final ImageView imageView = request.imageViewReference.get();

imageView.post(new Runnable()
{
@Override
public void run()
{
imageView.setImageResource(request.loadingResId);
}
});

int random = new Random().nextInt(2000);
sleep(random);

int success = new Random().nextInt(2);
if(success == 0)
{
imageView.post(new Runnable()
{
@Override
public void run()
{
imageView.setImageResource(R.mipmap.success);
}
});
}
else
{
imageView.post(new Runnable()
{
@Override
public void run()
{
imageView.setImageResource(request.failedResId);
}
});
}
}
}

注意,这里只是模拟,并没有真正的发起网络请求,但本质是一样的,就是一个耗时异步操作而已。在加载ing的时候,加载失败的时候和加载成功的时候,分别给ImageView设置对应的图片即可。这里还有一个功能点没有深入写,就是加载网络图片等时候可以根据加载策略判断本地缓存的情况,有缓存就加载缓存的,没有,就去网络上拿,这个代码很好写,就不赘述了,这篇的重点在思想。

初始化完成后,核心方法其实就是Manager里的displayImage方法,其本质就是把View层加载的请求,丢到queue中去就完了,我们的生产线会自动处理这一系列请求的,是不是轻松愉快。

最后,别忘了在程序退出时销毁我们的ImageLoader。

@Override
protected void onDestroy()
{
super.onDestroy();
SDKImageLoader.getInstance().destroy();
}

无论多复杂的框架,都一定是在某个核心思想的主干上,开枝散叶罢了。
就酱~