4

Android无用代码、资源扫描的其他思路

 2 years ago
source link: http://www.androidchina.net/12504.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

之前一直是用Android自带的 Analyze -> Run inspection by name… 但缺点也很多。 后来在stackoverflow看到一种基于minifyEnabled,和shrinkResources的思路

一、基于minifyEnabled 结果获取无用代码

1、我们都知道minifyEnabled=true会开启代码缩减,那如果知道minifyEnabled都删了哪些代码,就知道哪些代码是没用的了

在最新官方文档上关于minifyEnabled的描述中 minifyEnabled 属性设为 true,系统会默认启用 R8 代码缩减功能,在排查R8问题的文档中我们发现官方提供了一个R8 移除的(或保留的)代码的报告的功能 developer.android.com/studio/buil…
这个生成的报告usage.txt 大概长这样子:

image.png 这个文件列出了minifyEnabled开启后,缩减掉的代码内容:最大粒度为类,到类的成员变量、方法;
移除的内容包含三方Jar包的无用代码,以及工程中自己的无用代码

2、如何生成usage.txt

官方文档中提到了minifyEnabled开启后,默认用是R8做代码缩减,但在R8之前呢? 又是由谁来做缩减的工作的呢,其实是proguard! 关于两者的区别可以参考这篇文章

下面说这两种情况下分别怎么生成:

1、不想开启R8,生成usage.txt
设置minifyEnabled=true进行编译即可,
生成的文件位于build/outputs/mapping/release(或debug)/usage.txt
2、开启R8,生成usage.txt
1、设置minifyEnabled=true
2. 指定生成路径,在proguard-rules.pro文件中添加:
   -printusage <output-dir>/usage.txt
3. 编译即可

笔者对比过这两种方式的代码缩减效果,相比之下开启R8后被删掉的代码要比proguard的稍微多一些,但整体相差不大。如下图:左边是proguard,4万1千行,右边是R8,4万4千行

image.png

3、基于usage文件内容,我们根据包名进行过滤,可以拿到当前工程中被缩减那部分的代码,文章第三部分实践,可以参考

二、基于shrinkResources结果获取无用资源

获取无用资源相对容易些,将shrinkResources置为true,编译后shrinkResources的结果位于build/outputs/mapping/release(或debug)/resources.txt。内容大概长这样:

image.png

除此之外,官方还提供了一个开启严苛引用检查的开关。开启了之后,扫描出的无用资源数量大大增加,但需要注意是否会影响业务

开启严苛检查方法:在res/raw/目录下新增keep.xml文件

编译后基于usage.txt 和 resources.txt 的结果,可以通过task来过滤,排序处理。可参考以下:

task codeScan(dependsOn: assembleRelease)  {
        ...
        doLast {
            if (project.getBuildDir().exists()) {
            String basePath = project.getBuildDir().path + "/outputs/mapping/release/"
            //无用Class
            File uoUseClassRecode = new File(basePath + "usage.txt")
            if (uoUseClassRecode.exists()) {
                FileReader fr = new FileReader(uoUseClassRecode)
                BufferedReader reader = new BufferedReader(fr)
                List<ClassRecorder> classList = new ArrayList<>()
                ClassRecorder recorder = null
                String packageName = "${project.android.defaultConfig.applicationId}"
                if (packageName == null || packageName.size() == 0) {
                    throw new IllegalArgumentException(
                            "packageName为空,请检查是否在build.gradle的defaultConfig中配置applicationId属性")
                }
                while(reader.ready()){
                    String line = reader.readLine()
                    //新的类
                    if (!line.startsWith("  ")) {
                        if (isBusinessCode(recorder, packageName)){ //如果是业务代码,记录下来
                            classList.add(recorder)
                        }
                        recorder = new ClassRecorder()
                        recorder.className = line
                    } else {
                        recorder.classMethodList.add(line)
                    }
                }
                reader.close()
                fr.close()
                //读取结束,排序整理
                List<ClassRecorder> result = sortByClassName(classList, packageName.size()+1)
                //排序完,输出到文件
                File outPutFile = new File(basePath + "unusedClass.txt")
                if (outPutFile.exists()) outPutFile.createNewFile()
                BufferedWriter bw = new BufferedWriter(new FileWriter(outPutFile))
                for (ClassRecorder cr : result) {
                    bw.writeLine(cr.className)
                }
                bw.close()
            } else {
                throw new IllegalArgumentException("编译产物文件不存在")
            }

            boolean checkResPrefix = true
            //无用资源
            File uoUsedRes = new File(basePath + "resources.txt")
            if (uoUseClassRecode.exists()) {
                FileReader fr = new FileReader(uoUsedRes)
                BufferedReader reader = new BufferedReader(fr)
                List<String> resList = new ArrayList<>()
                while(reader.ready()){
                    String line = reader.readLine()
                    if (line.startsWith("Skipped unused resource")) {
                        String name = line.split(" ")[3]
                        name = name.substring(0, name.size()-1)
                        resList.add(name)
                    }
                }
                reader.close()
                fr.close()
                File outPutFile = new File(basePath + "unusedRes.txt")
                if (outPutFile.exists()) outPutFile.createNewFile()
                BufferedWriter bw = new BufferedWriter(new FileWriter(outPutFile))
                for (String name : resList) {
                    bw.writeLine(name)
                }
                bw.close()
            }
        }
    }

    /**
     * 是否是业务代码,是否是含有包名
     */
    static boolean isBusinessCode(ClassRecorder recorder, String packageName) {
        if (recorder == null) return false
        return recorder.className.contains(packageName)
    }
    /**
    * 排序,按类名 —— 高位优先字符串排序
    */
    static List<ClassRecorder> sortByClassName(List<ClassRecorder> list, int defaultStartLength){
        List<ClassRecorder> result = new ArrayList<>(list.size())
        result.addAll(list)
        sortByClassName(result, 0, result.size()-1, defaultStartLength)
        return result
    }

    static sortByClassName(List<ClassRecorder> list, int begin, int end, int d){
        if(begin >= end){return }
        int[] count = new int[258]
        for (int i = 0; i < 256+2; i++) {
            count[i] = 0;
        }
        for(int i = begin; i <= end; i++){ //attention 这个起始的位置是begin,end,每次只处理这一部分
            int index = charAt(list.get(i).className, d) + 2;
            count[index]+=1;
        }
        for(int i = 0; i < count.length-1; i++){
            count[i+1] += count[i];
        }
        List<ClassRecorder> result = new ArrayList<>(list.size());
        for(int i = begin; i <= end; i++){
            int index = charAt(list[i].className ,d) + 1
            result[count[index]++] = list.get(i);
        }
        for(int i = begin; i <= end; i++){
            list[i] = result[i - begin];
        }
        //当前按d位的排序已完成
        for(int r = 0; r < count.length-2; r++){
            sortByClassName(list, begin + count[r], begin + count[r+1]-1, d+1);
        }
    }

    static int charAt(string, d) {
        if (d < string.size()){
            return Character.codePointAt(string, d)
        } else {
            return -1;
        }
    }

    class ClassRecorder {
        String className
        List<String> classMethodList = new ArrayList<>()
    }

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK