序言
对于android学习者,对于网络请求势必都经历这样的一个过程,通过HttpClient或者HttpUrlConnection,来发其请求然后通过Handler进行数据的传递,非常的麻烦,然后后来你知道了有Volley,OKHttp,来让我们尝试动手写个网络请求的小工具吧,来对其进行一个剖析。
图片请求网络框架
对于图片的请求,我们需要设置一个缓存,通过缓存策略来减少网络请求,从而减少电量消耗和流量消耗,缓存策略通过二级缓存策略,内存作为一级缓存,磁盘作为二级缓存,缓存采用LRU的方式来进行管理,到得不到指定的内容之后,向网络发起请求来获得图片。这么一听貌似很简单的呀,来我们一个坑一个坑的踩过去。
现在我要根据URL来找一个图片,那么先从内存中取。
public Bitmap loadBitmap(String uri,int reqWidth,int reqHeight){
Bitmap bitmap = loadBitmapFromMemCache(uri);
if(bitmap!=null)
return bitmap;
try{
bitmap = loadBitmapFromDiskCache(uri,reqWidth,reqHeight);
if(bitmap!=null)
return bitmap;
bitmap = loadBitmapFromHttp(uri,reqWidth,reqHeight);
}catch (IOException e){
e.printStackTrace();
}
if(bitmap==null&&mIsDiskLruCacheCreated){
bitmap = downloadBitmapFromUrl(uri);
}
}
loadBitmapFromMemCache:先是从内存中拉取
private Bitmap loadBitmapFromMemCache(String url){
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
return bitmap;
}
通过Url我们向内存中缓存进行查找,我们首先将url进行一个哈希,对其哈希的原因很明显,我们的url中可能还有一个特殊字符,影响我们的使用,所以我们一般采用其MD5值来作为key,这个函数这里我们先不去看,现在知道其作用即可。我们所关心的重点是getBitmapFromMemcache(),如何来实现这个方法。
private Bitmap getBitmapFromMemCache(String key){
return mMemoryCache.get(key);
}
从一个对象中来拿,这个就是我们用来管理内存的LruCache,如何创建这个对象呢?我们创建的时候,还需要重写它的sizeOf方法。
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
显然,我们可以通过put方法将我们的图片缓存进去,到此,我们关于从内存缓存中如何取图片已经完成了,然后是当我们的在内存中找不到图片从磁盘的缓存中查找的时候。
loadBitmapFromDiskCache
从磁盘加载
private Bitmap loadBitmapFromDiskCache(String url,int reqWidth,int reqHeight) throws IOException{
if(Looper.myLooper()==Looper.getMainLooper()){
Log.w(TAG,"load bitmap from the UI Thread is not recommended!");
}
if(mDiskLruCache ==null){
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot!=null){
FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mBitmapHelper.decodeBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
if(bitmap!=null){
addBitmapToMemoryCache(key,bitmap);
}
}
return bitmap;
}
首先判断是否在是在主线程,从磁盘读取文件,不建议在主线程中对其读取,容易导致ANR。接着我们来看下我们的DisKLruCache的实现,将在下篇文章中进行剖析。从磁盘中查找图片得不到的时候,我们会发起网络请求。
loadBitmapFromHttp
从网络中获取图片
private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException{
if(Looper.myLooper()==Looper.getMainLooper()){
throw new RuntimeException("can not visit network from UI thread");
}
if(mDiskLruCache == null){
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor!=null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if(downloadUrlToStream(url,outputStream)){
editor.commit();
}else{
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url,reqWidth,reqHeight);
}
首先我们要进行线程的检测,判断是否处于主线程,然后调用了downloadUrlToStream()来从网络中获取数据流,然后将该数据流转交给DiskLruCache,也就是将图片文件写进我们的磁盘缓存中,然后调用从磁盘缓存中加载图片。来看下如何实现根据提供的url来获取一个数据流的。
downloadUrlToStream
//将网络流转化为数据流
public boolean downloadUrlToStream(String urlString,OutputStream outputStream){
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try{
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection)url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
int b;
while((b=in.read())!=-1){
out.write(b);
}
return true;
}catch (IOException e){
Log.e(TAG,"downloadBitmap failed"+e);
}finally {
if(urlConnection!=null){
urlConnection.disconnect();
}
IOUtils.close(out);
IOUtils.close(in);
}
return false;
}
我们向该函数传递了我们所需要的两个参数,一个url,一个是输出流,我们通过UrlConnection来获取了一个和网络的连接,获取了数据流,然后根据流来读取,之后写入到我们的输出流,如果你对Java研究并不是很深入,可能听到流,会有写模糊,对其底层的细节也想了解,别急,后续文章会继续来讲。这样我们实现了将数据写入到我们的磁盘缓存。再回到我们最初,我们对于这次湖区图片后面还做了一个判断,你可能会感觉到疑惑了,为什么还要做这么一次操作。不应该是100%的可以从网络中获取到图片的吗?其实不然,这个时候,我们可能会在网络加载等各方面问题上出现了状况,这个时候,我们选择从网络重新进行加载。
//根据Url下载图片
private Bitmap downloadBitmapFromUrl(String urlString){
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try{
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
}catch (final IOException e){
}finally {
if(urlConnection!=null){
urlConnection.disconnect();
}
IOUtils.close(in);
}
if(bitmap!=null)
return bitmap;
}
如此一个图片缓存的框架结束了,当然从网络加载过来的图片我们的不可能是将其全部加载到内存,我饿们需要根据其大小做一个显示的处理,处理方式。
获取图片的宽高,根据我们需要的宽高进行一个缩放比对,修改了其属性之后,然后将其设置该Bitmap的属性。从而减小其体积。
public static Bitmap decodeBitmapFromFileDescriptor(FileDescriptor fd,int reqWidth,int reqHeight){
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd,null,options);
}
public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if(height>reqHeight||width>reqHeight){
final int halfHeight = height/2;
final int halfWidth = width/2;
while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
inSampleSize *=2;
}
}
return inSampleSize;
}
这里我们提供了另一个方式,支持ImageView绑定一个View
//实现异步加载
public void bindBitmap(final String uri,final ImageView imageView,final int reqWidth,final int reqHeight){
imageView.setTag(uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if(bitmap!=null){
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri,reqWidth,reqHeight);
if(bitmap!=null){
LoaderResult result = new LoaderResult(imageView,uri,bitmap);
//get the message from messge pool avoid to create new message
mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
这个方法,先是从内存中查找,如果找到,则进行设置,如果没有找到,则从网络发起请求,创建了一个Runnable,然后将我们上述的从内存,网络中的请求封装到其中,丢给线程池来处理,这个时候问题就来了,丢给线程池之后,我们和UI线程就不在一个线程了,这个时候,需要我们进行线程的切换,如何来操纵我们view,或者是将我们的结果传递出去,实现方式是。对结果类进行了一个封装,将我们的view和结果封装为一个结果类。
private static class LoaderResult{
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView,String uri,Bitmap bitmap){
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
得到了结果,将其通过消息发送的方式,传递给我们的主线程,然后在Handler中进行处理。处理方式。
// handler实现交互
private Handler mMainHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult)msg.obj;
ImageView imageView = result.imageView;
//imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag();
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
}else{
//set a default background
}
}
};
这里,我们给ImageView设置一个tag用来标记,然后通过对其进行判断,然后将图片设置在上面。至此我们一个图片请求的框架就写好了,当然还是需要在进行一些优化的。接下来我们进行普通网络请求(非图片)库的一个封装封装。然后是对于两个缓存工具类的剖析。