Android内存优化之图片查重

本文聚焦Android图片重复问题,分为APP运行时加载相同图片对象致内存浪费、APK包中存在相同图片文件影响包大小两类。分别介绍内存图片查重和APK文件查重思路,前者用HAHA库分析hprof文件,后者遍历项目目录以图片MD5为Key值,还推荐了冗余代码查重工具。

本文将图片重复分为两种类型:

1.APP运行时加载了多个相同的图片对象,造成了内存浪费

2.APK包中存在多个相同的图片文件,影响了APK包大小

下面分别进行讨论:

---------------------------------------------------------------------------------------------------

一.内存图片查重:

目的:降低运行时内存,防止程序发生OOM异常,以及降低程序由于内存过大被LMK机制杀死的概率。另一方面,不合理的内存使用会使GC大大增多,从而导致程序变卡。

思路:通过分析内存文件hprof快速判断内存中是否存在重复的图片,并且将这些重复图片的PNG、堆栈等信息输出

内存文件hprof快速判断内存中是否存在重复的图片,并且将这些重复图片的PNG、堆栈等信息输出

1.首先是获取我们需要分析的 hprof 文件,我们加载两张相同图片:

Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);

imageView1.setImageBitmap(bitmap1);
imageView2.setImageBitmap(bitmap2);

2.生成 hprof 文件

// 手动触发 GC
Runtime.getRuntime().gc();
System.runFinalization();
Debug.dumpHprofData(file.getAbsolutePath());

3.利用HAHA 库进行文件分析的核心代码:

主要思路是遍历所有的bitmap对象,用HashCode-List<AnalyserResults>,作为key-value,最后如果判断value的list的size大于1,则说明内存中存在重复图片

File heapDumpFile = new File(args[0]);
            //打开hprof文件
            HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
            HprofParser parser = new HprofParser(buffer);
            //解析获得快照
            com.squareup.haha.perflib.Snapshot snapshot = parser.parse();
            snapshot.computeDominators();

            //获得Bitmap Class
            Collection<ClassObj> bitmapClasses = snapshot.findClasses("android.graphics.Bitmap");
            //获取堆数据,这里包括项目app、系统、default heap的信息,需要进行过滤
            Collection<Heap> heaps = snapshot.getHeaps();
//            Tools.print("bitmapClasses size = " + bitmapClasses.size());
//            Tools.print("all heaps size in snapshot = " + heaps.size());
            
            //这里有一个坑,其实snapshot也是从每个heap上获取他的ClassObj列表的,但是可能出现这个heap上的
            //ClassObj对象出现在了另一个heap中的情况,因此我们不能直接获取heap的ClassObj列表,
            //需要直接从snapshot总获取ClassObj列表.
            long startTime = System.currentTimeMillis();
            Tools.print("---------------------- START ----------------------- ");
            for (Heap heap : heaps) {
                // 只需要分析app和default heap即可
                Tools.print("HeapName:" + heap.getName());
                if (!heap.getName().equals("app") && !heap.getName().equals("default")) {
                    continue;
                }
                //  Tools.print("HeapName:" + heap.getName());

                Map<Integer, List<AnalyzerResult>> map = new HashMap<>();
                
                for (ClassObj clazz : bitmapClasses) {
                    //从heap中获得所有的Bitmap实例
                    List<Instance> instances = clazz.getHeapInstances(heap.getId());

                    for (int i = 0; i < instances.size(); i++) {
                        //从GcRoot开始遍历搜索,Integer.MAX_VALUE代表无法被搜索到,说明对象没被引用可以被回收
                        if (instances.get(i).getDistanceToGcRoot() == Integer.MAX_VALUE) {
                            continue;
                        }
                        List<AnalyzerResult> analyzerResults;
                        int curHashCode = Tools.getHashCodeByInstance(instances.get(i));
                        AnalyzerResult result = Tools.getAnalyzerResult(instances.get(i));
                        result.setInstance(instances.get(i));
                        if (map.get(curHashCode) == null){
                            analyzerResults = new ArrayList<>();
                        }else {
                            analyzerResults = map.get(curHashCode);
                        }
                        analyzerResults.add(result);
                        map.put(curHashCode, analyzerResults);
                    }
                }

                if (map.isEmpty()){
                    Tools.print("current head has no bitmap object");
                }

                for (Map.Entry<Integer, List<AnalyzerResult>> entry : map.entrySet()){
                    List<AnalyzerResult> analyzerResults = entry.getValue();
                    //去除size小于2的,剩余的为重复图片。
                    if (analyzerResults.size() < 2){
                        continue;
                    }
                    Tools.print("============================================================");
                    Tools.print("duplcateCount:" + analyzerResults.size());
                    Tools.print("stacks:[");
                    for (AnalyzerResult result : analyzerResults){
                        Tools.print("   [");
                        Tools.getStackInfo(result.getInstance());
                        Tools.print("   ]");
                    }
                    Tools.print("]");
                    Tools.print(analyzerResults.get(0).toString());
                    Tools.print("============================================================");
                }
              
            }
            long endTime = System.currentTimeMillis();
            Tools.print("---------------------- END ----------------------- ");
            Tools.print("Total cost time:" + (endTime - startTime) + "ms");

最终的输出结果:

$ java -jar DuplicatedBitmapAnalyzer-1.0.jar dump.hprof
---------------------- START -----------------------
HeapName:default
current head has no bitmap object
HeapName:app
============================================================
duplcateCount:2
stacks:[
   [
      android.graphics.drawable.BitmapDrawable$BitmapState@315598040 (0x12cfa4d8)
      android.graphics.drawable.BitmapDrawable@315232112 (0x12ca0f70)
      android.support.v7.widget.AppCompatImageView@315614208 (0x12cfe400)
      android.view.View[12]@315579008 (0x12cf5a80)
      android.widget.LinearLayout@315612160 (0x12cfdc00)
      android.support.v7.widget.AppCompatButton@315615232 (0x12cfe800)
   ]
   [
      android.graphics.drawable.BitmapDrawable$BitmapState@315597984 (0x12cfa4a0)
      android.graphics.drawable.BitmapDrawable@315232040 (0x12ca0f28)
      android.support.v7.widget.AppCompatImageView@315613184 (0x12cfe000)
      android.view.View[12]@315579008 (0x12cf5a80)
      android.widget.LinearLayout@315612160 (0x12cfdc00)
      android.support.v7.widget.AppCompatButton@315615232 (0x12cfe800)
   ]
]
bufferHash:fac3180b7dd77454bb6c81bc02827945
width:90
height:90
bufferSize:32400
============================================================
---------------------- END -----------------------
Total cost time:49ms

用AS打开Hprof文件,发现drawable对象一样:

-----------------------------------------------------------------------------------------------------

二.APK文件查重(可以包括图片,布局,资源等等)

目的:降低程序占用的空间,防止由于ROM空间不足导致程序无法安装;另外在用到插件化技术时,不注意控制插件包大小,尤其会影响插件加载速度

思路:和上面类似,遍历项目目录下的图片资源,将图片MD5作为Key值,如果存在多个相同文件,则对应的value存储多个,最后输出重复结果。

可以考虑用脚本实现,在代码入库前做强制检查,具体代码就不贴了。

说到文件查重,顺便推荐一下冗余代码查重工具:Simian,重构的时候可以使用,实测效果不错。

 

最后:

好的架构,不要相信大家的口头约定,应该考虑将能自动化的工作,全部自动化。

只有保证实验室监控和线上监控的持续有效实施,才能够保证项目的线上质量。

####一、前言 在做系统应用的时候,一般都在linus 服务器上面搭建编译环境,编译apk。跟大厂(vivo)合作的时候,验收的最后一关往往是内存泄漏测试,想要保证项目按期验收,我们最好是能在平时开发的时候发现并解决内存泄漏问题,而源码环境无法像gradle 一行代码依赖leakcanary,而是需要通过aar或者jar形式添加依赖。刚好前段时间抽空将leakcanary集成到项目中,现在将过程记录下来,为大家踩个坑。 ####二、撸起袖子 老规矩,百度一下... ![image.png](https://upload-images.jianshu.io/upload_images/11562793-8b8621df077a06ea.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 搜到一篇文章,嗯,感觉有我要的东西 ![image.png](https://upload-images.jianshu.io/upload_images/11562793-446a980a54bf60a7.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 点进去一看,额,只是记录了依赖,没给出相应的jar包和arr,版本1.5也不是最新的,所以我还是自己动手吧。 ##### 下载leakcanary源码 传送门 https://github.com/square/leakcanary ##### 编译出 aar 选择assembleDebug ![image.png](https://upload-images.jianshu.io/upload_images/11562793-43e389295da509e0.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 生成的aar在目录 /build/outputs/aar 下 ![image.png](https://upload-images.jianshu.io/upload_images/11562793-2360141aa17b6f5e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) leakcanary-android 依赖 leakcanary-analyzer,所以把leakcanary-analyzer-debug.aar也取出来 ![image.png](https://upload-images.jianshu.io/upload_images/11562793-7120eb74d6b5bc0e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) leakcanary-analyzer 依赖 com.squareup.haha:haha:2.0.4 和 leakcanary-watcher 我们把haha:2.0.4的jar包取出来 ![image.png](https://upload-images.jianshu.io/upload_images/11562793-c1dd44406966df29.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 同理把leakcanary-watcher 的jar包取出来。 #### 依赖 整理一下需要依赖的jar包和arr haha-2.0.4.jar leakcanary-analyzer-1.6.2.aar leakcanary-android-1.6.2.aar leakcanary-watcher-1.6.2.jar 把上面的aar和jar放到项目的libs目录下,然后打开Android.mk,开始添加依赖 ``` #依赖申明,冒号前是别名,冒号后是jar包路径 LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := haha:libs/haha-2.0.4.jar \ leakcanary-watcher:libs/leakcanary-watcher-1.6.2.jar \ leakcanary-android:libs/leakcanary-android-1.6.2.aar \ leakcanary-analyzer:libs/leakcanary-analyzer-1.6.2.aar \ include $(BUILD_MULTI_PREBUILT) ...省略其它 #依赖jar包 ...省略其它jar包依赖 LOCAL_STATIC_JAVA_LIBRARIES += haha LOCAL_STATIC_JAVA_LIBRARIES += leakcanary-watcher #依赖aar LOCAL_STATIC_JAVA_AAR_LIBRARIES := leakcanary-analyzer LOCAL_STATIC_JAVA_AAR_LIBRARIES += leakcanary-android ...省略其它 #添加包名 LOCAL_AAPT_FLAGS += --extra-packages com.squareup.leakcanary LOCAL_AAPT_FLAGS += --extra-packages gnu.trove ``` 然后在Application中添加初始化代码 ``` import com.squareup.leakcanary.LeakCanary; #oncreate中 if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); ``` 感觉应该没啥问题,编译一下试试 开始mm 等待中... ####编译失败 ![image.png](https://upload-images.jianshu.io/upload_images/11562793-c905b8d4a55e53c7.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 非法字符 '\$' ?什么鬼,非法字符哪里来的,肯定是LeakCanary 的,看看去。 ![image.png](https://upload-images.jianshu.io/upload_images/11562793-2ae94c9b68676610.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 原来是占位符导致的,这个好办,直接把applicationId 写死不就行了 这里就把 ${applicationId} 换成hello_world,然后编译一下,把aar替换成新编译的,然后mm 等待一分钟... ![image.png](https://upload-images.jianshu.io/upload_images/11562793-b36ea1fbd4f18da8.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 编译成功,install 一下,运行没问题。 那搞个内存泄漏出来看看?![screen.png](https://upload-images.jianshu.io/upload_images/11562793-e92d1a3a1c05738e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 嗯,没毛病,那本次源码环境集成LeakCanary工作到此就结束了。 总结: 1.下载LeakCanary 源码,修改manifest中的非法字符’\$‘,然后编译,取出需要的aar和jar 2.然后在Android.mk中添加依赖 为何方便大家集成,依赖我已经整理出来了
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值