经常会用到 网络文件 比如查看大图片数据 资源优化的问题,当然用开源的项目 Android-Universal-Image-Loader 是个很好的选择。
在这里把原来 写过的优化的代码直接拿出来,经过测试千张图片效果还是不错的。
工程目录:
至于 Activity 就是加载了 1个网格布局
/** * 实现 异步加载 和 2级缓存 */ public class ImagedownActivity extends Activity { public static String filepath; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); filepath = this.getCacheDir().getAbsolutePath(); GridView gv=(GridView)findViewById(R.id.gridview01); //设置列数 gv.setNumColumns(3); //配置适配器 gv.setAdapter(new Myadapter(this)); } @Override protected void onDestroy() { // TODO Auto-generated method stub //activity 销毁时,清除缓存 MyImageLoader.removeCache(filepath); super.onDestroy(); } }
接下来 Myadapter.java(给网格每个item塞入图片 )在生成每个 item 异步请求网络获取image
public class Myadapter extends BaseAdapter { private Context context; private String root ="http://192.168.0.100:8080/Android_list/"; private String[] URLS; private final MyImageLoader myImageLoader = new MyImageLoader(context);; /** * adapter 初始化的时候早一堆数据 * 这里我请求的是自己搭的服务器 * @param context */ public Myadapter(Context context){ this.context =context; URLS = new String[999]; for (int i = 0; i < 999; i++) { URLS[i] = root + (i+1)+".jpg"; } } @Override public int getCount() { return URLS.length; } @Override public Object getItem(int position) { return URLS[position]; } @Override public long getItemId(int position) { return URLS[position].hashCode(); } @Override public View getView(int position, View view, ViewGroup parent) { ImageView imageView; if(view==null){ imageView=new ImageView(context); imageView.setLayoutParams(new GridView.LayoutParams(200,190)); imageView.setAdjustViewBounds(false); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setPadding(5, 5, 5, 5); }else{ imageView=(ImageView)view; } myImageLoader.downLoad(URLS[position], (ImageView)imageView , context); return imageView; } }
MyImageLoader.java
public class MyImageLoader { //最大内存 final static int memClass = (int) Runtime.getRuntime().maxMemory(); private Context context; // 是否缓存到硬盘 private boolean diskcache = true; // 定义一级 缓存的图片数 private static final int catch_num = 10; // 定义二级缓存 容器 软引用 private static ConcurrentHashMap<String, SoftReference<Bitmap>> current_hashmap = new ConcurrentHashMap<String, SoftReference<Bitmap>>(); // 定义一级缓存容器 强引用 (catch_num ,0.75f,true) 默认参数 2.加载因子默认 3.排序模式 true private static LinkedHashMap<String, Bitmap> link_hashmap = new LinkedHashMap<String, Bitmap>(catch_num ,0.75f,true) { // 必须实现的方法 protected boolean removeEldestEntry(java.util.Map.Entry<String, Bitmap> eldest) { /** 当一级缓存中 图片数量大于 定义的数量 放入二级缓存中 */ if (this.size() > catch_num) { // 软连接的方法 存进二级缓存中 current_hashmap.put(eldest.getKey(), new SoftReference<Bitmap>( eldest.getValue())); //缓存到本地 cancheToDisk(eldest.getKey(),eldest.getValue() ); return true; } return false; }; }; public MyImageLoader(Context context) { } /** * 外部调用此方法 进行下载图片 */ public void downLoad(String key , ImageView imageView,Context context){ // 先从缓存中找 。 context = this.context; Bitmap bitmap = getBitmapFromCache(key); if( null!= bitmap){ imageView.setImageBitmap(bitmap); cancleDownload(key, imageView); //取消下载 return ; } // 缓存中 没有 把当前的 imageView 给他 得到 task if(cancleDownload(key, imageView)){ //没有任务进行。,。。开始下载 ImageDownloadTask task = new ImageDownloadTask(imageView); Zhanwei_Image zhanwei_image = new Zhanwei_Image(task); //先把占位的图片放进去 imageView.setImageDrawable(zhanwei_image); // task执行任务 task.execute(key); } } /** 此方法 用于优化 : 用户直接 翻到 哪个 就先加载 哪个、 * @param key - URL * @param imageView - imageView * core: 给当前的 imageView 得到给他下载的 task */ private boolean cancleDownload(String key,ImageView imageView){ // 给当前的 imageView 得到给他下载的 task ImageDownloadTask task = getImageDownloadTask(imageView); if(null != task){ String down_key = task.key; if( null == down_key || !down_key.equals(key)){ task.cancel(true); // imageview 和 url 的key不一样 取消下载 }else{ return false; //正在下载: } } return true; //没有正在下载 } // public void getThisProcessMemeryInfo() { // int pid = android.os.Process.myPid(); // android.os.Debug.MemoryInfo[] memoryInfoArray = activityManager.getProcessMemoryInfo(new int[] {pid}); // System.out.println("本应用当前使用了" + (float)memoryInfoArray[0].getTotalPrivateDirty() / 1024 + "mb的内存"); // } /** * 从缓存中得到 图片的方法 1.先从一级 缓存找 linkhashmap 不是线程安全的 必须要加同步 */ public Bitmap getBitmapFromCache(String key) { //1.先在一级缓存中找 synchronized (link_hashmap) { Bitmap bitmap = link_hashmap.get(key); if (null != bitmap) { link_hashmap.remove(key); // 按照 LRU是Least Recently Used 近期最少使用算法 内存算法 就近 就 原则 放到首位 link_hashmap.put(key, bitmap); System.out.println(" 在缓存1中找图片了 =" +key); return bitmap; } } // 2. 到二级 缓存找 SoftReference<Bitmap> soft = current_hashmap.get(key); if (soft != null) { //得到 软连接 中的图片 Bitmap soft_bitmap = soft.get(); if (null != soft_bitmap) { System.out.println(" 在缓存2中找图片了 =" +key); return soft_bitmap; } } else { // 没有图片的话 把这个key删除 current_hashmap.remove(key); } //3.都没有的话去从外部缓存文件读取 if(diskcache){ Bitmap bitmap = getBitmapFromFile(key); if(bitmap!= null){ link_hashmap.put(key, bitmap); //将图片放到一级缓存首位 return bitmap; } } return null; } /** * 缓存到本地文件 * @param key * @param bitmap */ public static void cancheToDisk(String key ,Bitmap bitmap ){ //2.缓存bitmap至/data/data/packageName/cache/文件夹中 try { String fileName = getMD5Str(key); String filePath = ImagedownActivity.filepath + "/" + fileName; System.out.println("缓存到本地===" + filePath); FileOutputStream fos = new FileOutputStream(filePath); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); } catch (Exception e) { } } /** * 从外部文件缓存中获取bitmap * @param url * @return */ private Bitmap getBitmapFromFile(String url){ Bitmap bitmap = null; String fileName = getMD5Str(url); if(fileName == null){ return null; } String filePath = ImagedownActivity.filepath + "/" + fileName; try { FileInputStream fis = new FileInputStream(filePath); bitmap = BitmapFactory.decodeStream(fis); System.out.println("在本地缓存中找到图片==="+ filePath); } catch (FileNotFoundException e) { System.out.println("getBitmapFromFile==="+ e.toString()); e.printStackTrace(); bitmap = null; } return bitmap; } /** * 清理文件缓存 * @param dirPath * @return */ public static boolean removeCache(String dirPath) { File dir = new File(dirPath); File[] files = dir.listFiles(); if(files == null || files.length == 0) { return true; } int dirSize = 0; //这里删除所有的缓存 int all_ = (int) ( 1 * files.length + 1); //对files 进行排序 Arrays.sort(files, new FileLastModifiedSort()); for (int i = 0; i < all_ ; i++) { files[i].delete(); } return true; } /** * 根据文件最后修改时间进行排序 */ private static class FileLastModifiedSort implements Comparator<File> { @Override public int compare(File lhs, File rhs) { if(lhs.lastModified() > rhs.lastModified()) { return 1; } else if(lhs.lastModified() == rhs.lastModified()) { return 0; } else { return -1; } } } /** * MD5 加密 */ private static String getMD5Str(String str) { MessageDigest messageDigest = null; try { messageDigest = MessageDigest.getInstance("MD5"); messageDigest.reset(); messageDigest.update(str.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { System.out.println("NoSuchAlgorithmException caught!"); return null; } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } byte[] byteArray = messageDigest.digest(); StringBuffer md5StrBuff = new StringBuffer(); for (int i = 0; i < byteArray.length; i++) { if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) md5StrBuff.append("0").append(Integer.toHexString(0xFF & byteArray[i])); else md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i])); } return md5StrBuff.toString(); } // ------------------------ 异步加载---------------------------- /** * 占位的 图片 或者 颜色 用来绑定 相应的图片 */ class Zhanwei_Image extends ColorDrawable{ //里面存放 相应 的异步 处理时加载好的图片 ----- 相应的 task private final WeakReference<ImageDownloadTask> taskReference; public Zhanwei_Image(ImageDownloadTask task){ super(Color.BLUE); taskReference = new WeakReference<MyImageLoader.ImageDownloadTask>(task); } // 返回去这个 task 用于比较 public ImageDownloadTask getImageDownloadTask(){ return taskReference.get(); } } // 根据 给 的 iamgeView、 得到里面的 task 用于和当前的 task比较是不是同1个 private ImageDownloadTask getImageDownloadTask(ImageView imageView){ if( null != imageView){ Drawable drawable = imageView.getDrawable(); if( drawable instanceof Zhanwei_Image) return ((Zhanwei_Image)drawable).getImageDownloadTask(); } return null; } /** * 把图片 添加到缓存中 */ public void addBitmap(String key, Bitmap bitmap) { if (null != bitmap) { synchronized (link_hashmap) { // 添加到一级 缓存中 link_hashmap.put(key, bitmap); } } } /** 在后台 加载每个图片 * 第一个参数 第2个要进度条不 第三个返回结果 bitmap */ class ImageDownloadTask extends AsyncTask<String, Void, Bitmap> { private String key; private WeakReference<ImageView> imgViReference; public ImageDownloadTask(ImageView imageView) { //imageView 传进来 。。要给哪个iamgeView加载图片 imgViReference = new WeakReference<ImageView>( imageView); } @Override protected Bitmap doInBackground(String... params){ key = params[0]; //调用下载函数 根据 url 下载 return downloadBitmap(key); } @Override protected void onPostExecute(Bitmap result) { if(isCancelled()){ result = null; } System.out.println("result=="+ result.getByteCount()+"---memClassmemery="+memClass); if(null!= result){ //保存到缓存中 addBitmap(key, result); ImageView imageView = imgViReference.get(); if( null != imageView){ //向 imageView 里面放入 bitmap ImageDownloadTask task = getImageDownloadTask(imageView); /** * 判断 是不是 同一个 task( ) * 如果当前这个 task == imageView 里面的那个 task 就是同1个 */ if( this == task ){ imageView.setImageBitmap(result); } } } } } /** * 连接网络 客户端 下载图片 */ private Bitmap downloadBitmap(String url) { final HttpClient client = AndroidHttpClient.newInstance("Android"); final HttpGet getRequest = new HttpGet(url); try { HttpResponse response = client.execute(getRequest); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url); return null; } final HttpEntity entity = response.getEntity(); if (entity != null) { InputStream inputStream = null; try { inputStream = entity.getContent(); /** * 1.没有压缩直接将生成的bitmap返回去 */ // return BitmapFactory.decodeStream(inputStream); /** * 2.得到data后在这里把图片进行压缩 */ byte[] data = StreamTool.read(inputStream); return BitmapManager.scaleBitmap(context, data, 0.3f); // return BitmapFactory.decodeStream(new FlushedInputStream(inputStream)); } finally { if (inputStream != null) { inputStream.close(); } entity.consumeContent(); } } } catch (IOException e) { getRequest.abort(); } catch (IllegalStateException e) { getRequest.abort(); } catch (Exception e) { getRequest.abort(); } finally { if ((client instanceof AndroidHttpClient)) { ((AndroidHttpClient) client).close(); } } return null; } }
StreamTool.java
public class StreamTool { public static byte[] read(InputStream in) throws Exception{ ByteArrayOutputStream out_byte = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int len=0; while((len = in.read(buff))!= -1){ //写到内存中 字节流 out_byte.write( buff, 0 , len); } out_byte.close(); // 把内存数据返回 return out_byte.toByteArray(); } }
BitmapManager.java ( 这个类里面对 网络资源的图片 进行了优化)
public class BitmapManager { /** * 按屏幕适配Bitmap */ public static Bitmap scaleBitmap(Context context, byte[] data , float percent) { //这里我不获取了,假设是下面这个分辨率 int screenWidth = 540; int screenrHeight = 950; //设置 options BitmapFactory.Options options = new BitmapFactory.Options(); /** * BitmapFactory.Options这个类,有一个字段叫做 inJustDecodeBounds.SDK中对这个成员的说明是这样的: * If set to true, the decoder will return null (no bitmap), but the out… * 也就是说,如果我们把它设为true,那么BitmapFactory.decodeFile(String path, Options opt)并不会真的返回一个Bitmap给你, * 它仅仅会把它的宽,高取回来给你,这样就不会占用太多的内存,也就不会那么频繁的发生OOM了。 */ options.inJustDecodeBounds = true; //读取 Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options); int imgWidth = options.outWidth; int imgHeight = options.outHeight; //如果比你设置的宽高大 就进行缩放, if(imgWidth > screenWidth * percent || imgHeight > screenrHeight * percent) { options.inSampleSize = calculateInSampleSize(options, screenWidth, screenrHeight, percent); } /** * If set to true, the decoder will return null (no bitmap), but the out... fields will still be set, allowing the caller * to query the bitmap without having to allocate the memory for its pixels. * * 如果设置成 true,这个编码将会返回1个null , 但是那个区域仍将被设置(也就是存在),允许(调用者)去查询那个没有分配 内存的像素 bitmap */ options.inJustDecodeBounds = false; /** * Android的Bitmap.Config给出了bitmap的一个像素所对应的存储方式, * 有RGB_565,ARGB_8888,ARGB_4444,ALPHA_8四种。RGB_565表示的是红绿蓝三色分别用5,6,5个比特来存储, * 一个像素占用了5+6+5=16个比特。ARGB_8888表示红绿蓝和半透明分别用8,8,8,8个比特来存储, * 一个像素占用了8+8+8+8=32个比特。这样的话如果图片是以RGB_8888读入的,那么占用内存的大小将是RGB_565读入方式的2倍。 * 通常我们给Imagview加载图片是通过setDrawable或者在xml文件中用android:src来设置 * 默认的加载图片大小的方式是以RGB_8888读入的。 * */ options.inPreferredConfig = Bitmap.Config.RGB_565; /** * If this is set to true, then the resulting bitmap will allocate its pixels such that they can be purged * if the system needs to reclaim memory. * * 如果设置成 true, 这个结果bitmap 将会被分配像素,这样他们就能被 系统回收了,当系统需要回收内存的时候 */ options.inPurgeable = true; /** * This field works in conjuction with inPurgeable. * 这个方法是在 inPurgeable 的基础上工作的 */ options.inInputShareable = true; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options); System.out.println("data==="+ data.length +" change == bitmap byte "+ bitmap.getByteCount()); return bitmap; } // options reqWidth 屏幕宽 reqHeight屏幕高 你的view是屏幕的多大 public static int calculateInSampleSize(BitmapFactory.Options options, int screenWidth, int screenHeight ,float percent) { // 原始图片宽高 final int height = options.outHeight; final int width = options.outWidth; // 倍数 int inSampleSize = 1; if (height > screenHeight * percent || width > screenWidth * percent) { // 计算目标宽高与原始宽高的比值 final int inSampleSize_h = Math.round((float) height / (float)( screenHeight * percent)); final int inSampleSize_w = Math.round((float) width / (float)( screenWidth * percent)); // 选择两个比值中较小的作为inSampleSize的 inSampleSize = inSampleSize_h < inSampleSize_w ? inSampleSize_h : inSampleSize_w; System.out.println("inSampleSize===="+ inSampleSize); // if(inSampleSize < 1) { inSampleSize = 1; } } //简单说这个数字就是 缩小为原来的几倍,根据你的image需要占屏幕多大动态算的(比如你用的权重设置layout) return inSampleSize; } }
这个是代码输出的最多给这个进程分配的内存 128M
可以看到我上面的bitmapManager 里面有个 options.inPreferredConfig 注释写的很清楚,可以上去看一下,接下来贴几种格式的效果图
rgb565 和 argb_444 所占的内存 (54000)
看一下 argb_8888 ( 108000)
当然可能仔细看的人会看到我一开始截的 鸣人的效果图 上半部分 和 下半部分的颜色会有点问题。上面的rgb_565 生成的,和原图色彩可能会有点出入。
但是内存真心少了一半,所以各种取舍就看个人了,代码注释都谢的很清楚了。
先贴出代码不同地方的代码 : 就是在强引用的地方 把 LinkedHashMap 换成了 LruCache
// 获取单个进程可用内存的最大值 // 方式一:使用ActivityManager服务(计量单位为M) /*int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();*/ // 方式二:使用Runtime类(计量单位为Byte) final static int memClass = (int) Runtime.getRuntime().maxMemory(); // 3. 定义一级缓存容器 强引用 (catch_num /2,0.75f,true) 默认参数 2.加载因子默认 3.排序模式 true final static int max = memClass/5; // LruCache 用强引用将 图片放入 LinkedHashMap private static LruCache<String, Bitmap> lrucache = new LruCache<String, Bitmap>(max) { protected int sizeOf(String key, Bitmap value) { if(value != null) { // 计算存储bitmap所占用的字节数 return value.getRowBytes() * value.getHeight(); } else { return 0; } } @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { if(oldValue != null) { // 当硬引用缓存容量已满时,会使用LRU算法将最近没有被使用的图片转入软引用缓存 current_hashmap.put(key, new SoftReference<Bitmap>(oldValue)); } } };
1. 强引用:LruCache 后面再说,其实他内的内部封装的就是1个 LinkedHashMap 。LinkedHashMap 是线程不安全的,所以上面都会用到同步。
2. 软引用:ConcurrentHashMap 是线程安全的,并且支持高并发很有效率,这个后面也会说到。
3. 磁盘缓存: 这个经常会看到网易新闻等,应用有些界面你看了很多图片,为了保证不出现oom 又不用再次访问网络,会将部分image缓存在sdcard里。
4. 其中1个优化: 当比如用户快速滑动到 最底部,其实是最先加载显示给用户的部分的内容的。
原理: 当用户进入界面加载图片 ,首先会从1级缓存强引用中找,找不到回去2级缓存软引用中找,找不到再去sdcard中找,再找不到才会去请求网络加载资源。
当然sdcard的缓存 看个人需求是否需要。
注: android 4.0 后 对 SoftReference 的回收机制进行了改变,所以你是可以不用 2级缓存的,直接去掉就好了。
只要控制好你的 lrucache 或者 linkedhashmap就好了。