初始化完成后,就直接到了任务的执行,继续往下看
//方法有些长,我们一行一行往下看 @Override public TaskResult call() throws TaskExecuteException { try { //根据任务类型,结果类型,获取对应的 TaskResult对象,最终是TaskJsonResult或者TaskHtmlResult //这里默认 TaskResult taskResult = TaskResultFactory.factory(getType(), TASK_RESULT_TYPE_JSON, config); if (taskResult == null) { return null; } //记录开始时间 long startTime = System.currentTimeMillis(); JsonArray jsonArray = new JsonArray(); //这里就开始循环初始化时加入的dex文件列表,也就是RandomAccessFile随机访问对象 for (int i = 0; i < dexFileList.size(); i++) { RandomAccessFile dexFile = dexFileList.get(i); //详见下面函数分析 countDex(dexFile); //关闭文件流,防止泄漏 dexFile.close(); //计算内部方法数 int totalInternalMethods = sumOfValue(classInternalMethod); //计算外部方法数 int totalExternalMethods = sumOfValue(classExternalMethod); JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("dex-file", dexFileNameList.get(i)); //按配置进行分组 if (JobConstants.GROUP_CLASS.equals(group)) { List<String> sortList = sortKeyByValue(classInternalMethod); JsonArray classes = new JsonArray(); for (String className : sortList) { JsonObject classObj = new JsonObject(); classObj.addProperty("name", className); classObj.addProperty("methods", classInternalMethod.get(className)); classes.add(classObj); } jsonObject.add("internal-classes", classes); } else if (JobConstants.GROUP_PACKAGE.equals(group)) { String packageName; for (Map.Entry<String, Integer> entry : classInternalMethod.entrySet()) { packageName = ApkUtil.getPackageName(entry.getKey()); if (!Util.isNullOrNil(packageName)) { if (!pkgInternalRefMethod.containsKey(packageName)) { pkgInternalRefMethod.put(packageName, entry.getValue()); } else { pkgInternalRefMethod.put(packageName, pkgInternalRefMethod.get(packageName) + entry.getValue()); } } } List<String> sortList = sortKeyByValue(pkgInternalRefMethod); JsonArray packages = new JsonArray(); for (String pkgName : sortList) { JsonObject pkgObj = new JsonObject(); pkgObj.addProperty("name", pkgName); pkgObj.addProperty("methods", pkgInternalRefMethod.get(pkgName)); packages.add(pkgObj); } jsonObject.add("internal-packages", packages); } jsonObject.addProperty("total-internal-classes", classInternalMethod.size()); jsonObject.addProperty("total-internal-methods", totalInternalMethods); if (JobConstants.GROUP_CLASS.equals(group)) { List<String> sortList = sortKeyByValue(classExternalMethod); JsonArray classes = new JsonArray(); for (String className : sortList) { JsonObject classObj = new JsonObject(); classObj.addProperty("name", className); classObj.addProperty("methods", classExternalMethod.get(className)); classes.add(classObj); } jsonObject.add("external-classes", classes); } else if (JobConstants.GROUP_PACKAGE.equals(group)) { String packageName = ""; for (Map.Entry<String, Integer> entry : classExternalMethod.entrySet()) { packageName = ApkUtil.getPackageName(entry.getKey()); if (!Util.isNullOrNil(packageName)) { if (!pkgExternalMethod.containsKey(packageName)) { pkgExternalMethod.put(packageName, entry.getValue()); } else { pkgExternalMethod.put(packageName, pkgExternalMethod.get(packageName) + entry.getValue()); } } } List<String> sortList = sortKeyByValue(pkgExternalMethod); JsonArray packages = new JsonArray(); for (String pkgName : sortList) { JsonObject pkgObj = new JsonObject(); pkgObj.addProperty("name", pkgName); pkgObj.addProperty("methods", pkgExternalMethod.get(pkgName)); packages.add(pkgObj); } jsonObject.add("external-packages", packages); } jsonObject.addProperty("total-external-classes", classExternalMethod.size()); jsonObject.addProperty("total-external-methods", totalExternalMethods); jsonArray.add(jsonObject); } ((TaskJsonResult) taskResult).add("dex-files", jsonArray); taskResult.setStartTime(startTime); taskResult.setEndTime(System.currentTimeMillis()); //返回结果 return taskResult; } catch (Exception e) { throw new TaskExecuteException(e.getMessage(), e); } }
countDex(dexFile) 函数分析
private void countDex(RandomAccessFile dexFile) throws IOException { //按类分组的,内部方法Map,清除掉缓存 classInternalMethod.clear(); //按类分组的,外部依赖方法Map,清除掉缓存 classExternalMethod.clear(); //按包分组的,同上 pkgInternalRefMethod.clear(); pkgExternalMethod.clear(); //使用的com.android.dexdeps 包下的DexData类, DexData dexData = new DexData(dexFile); dexData.load(); //获取所有方法索引对象,包括内部方法和外部索引的方法 MethodRef[] methodRefs = dexData.getMethodRefs(); //获取所有外部类的索引数据 ClassRef[] externalClassRefs = dexData.getExternalReferences(); //获取混淆过的类 Map<String, String> proguardClassMap = config.getProguardClassMap(); String className = null; for (ClassRef classRef : externalClassRefs) { //获取类名 className = ApkUtil.getNormalClassName(classRef.getName()); if (proguardClassMap.containsKey(className)) { //匹配并赋值为混淆前的类名 className = proguardClassMap.get(className); } if (className.indexOf('.') == -1) { continue; } //将类名放入到外部方法Map中,供下面匹配外部方法时使用 classExternalMethod.put(className, 0); } //遍历所有方法,找到外部和内部方法,并分别加入到classExternalMethod、classInternalMethod Map中 for (MethodRef methodRef : methodRefs) { //获取该方法的类名 className = ApkUtil.getNormalClassName(methodRef.getDeclClassName()); //匹配混淆前的类名 if (proguardClassMap.containsKey(className)) { className = proguardClassMap.get(className); } if (!Util.isNullOrNil(className)) { if (className.indexOf('.') == -1) { continue; } //根据类名和外部方法存储的类信息匹配,匹配上就说明是外部方法引用。 if (classExternalMethod.containsKey(className)) { classExternalMethod.put(className, classExternalMethod.get(className) + 1); } else if (classInternalMethod.containsKey(className)) { classInternalMethod.put(className, classInternalMethod.get(className) + 1); } else { classInternalMethod.put(className, 1); } } } //删除没有方法引用的类 Iterator<String> iterator = classExternalMethod.keySet().iterator(); while (iterator.hasNext()) { if (classExternalMethod.get(iterator.next()) == 0) { iterator.remove(); } } }
通过源码的分析,其实原理就是使用DexData对象加载dexFile,最后getMethodRefs、getExternalReferences方法获取相关信息,最后通过外部的classExternalMethod 将所有的方法分类成内部方法和外部方法集合。
CountClassTask
经过上面的分析经验,我们再分析CountClassTask就简单了许多,直接找到核心代码如下:
DexData dexData = new DexData(dexFile); dexData.load(); dexFile.close(); ClassRef[] defClassRefs = dexData.getInternalReferences(); Set<String> classNameSet = new HashSet<>(); for (ClassRef classRef : defClassRefs) { String className = ApkUtil.getNormalClassName(classRef.getName()); if (classProguardMap.containsKey(className)) { className = classProguardMap.get(className); } if (className.indexOf('.') == -1) { continue; } classNameSet.add(className); }
上面使用了getExternalReferences,而这里直接使用getInternalReferences,获取所有内部类的索引数据,最终加入到一个HashSet中。整个过程简单明了。难道所有的Task的实现都是用到DexData对象处理的吗?并不是,我们来看下一个
UnusedAssetsTask
看类名,简单解释为未被使用的资源任务,其实就是找到assets文件夹下没有被依赖的资源,知道该任务的目的后,我们就来看看,它是如何实现的,先看下init中都做了什么准备
//初始化方法 @Override public void init() throws TaskInitException { super.init(); //先拿到解压后的apk文件路径 String inputPath = config.getUnzipPath(); if (Util.isNullOrNil(inputPath)) { throw new TaskInitException(TAG + "---APK-UNZIP-PATH can not be null!"); } inputFile = new File(inputPath); //同样的检测文件是否存在和检查文件的属性是否是文件夹 if (!inputFile.exists()) { throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not exist!"); } else if (!inputFile.isDirectory()) { throw new TaskInitException(TAG + "---APK-UNZIP-PATH '" + inputPath + "' is not directory!"); } //根据配置文件中忽略资源列表,放入到ignoreSet中,为了忽略一些文件的检查,比如确定资源是有用的,就不需要被检查,缩小范围。 if (params.containsKey(JobConstants.PARAM_IGNORE_ASSETS_LIST) && !Util.isNullOrNil(params.get(JobConstants.PARAM_IGNORE_ASSETS_LIST))) { String[] ignoreAssets = params.get(JobConstants.PARAM_IGNORE_ASSETS_LIST).split(","); Log.i(TAG, "ignore assets %d", ignoreAssets.length); for (String ignore : ignoreAssets) { ignoreSet.add(Util.globToRegexp(ignore)); } } File[] files = inputFile.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getName().endsWith(ApkConstants.DEX_FILE_SUFFIX)) { //同样的将dex文件,筛选出来,放入到dexFileNameList中 dexFileNameList.add(file.getName()); } } } }
初始化中,执行了常规的文件检查,params参数的整理,再将dex文件缓存到一个list中,待处理,再来看下call函数,看它如何找到了未被依赖资源。
@Override public TaskResult call() throws TaskExecuteException { try { TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config); long startTime = System.currentTimeMillis(); //创建assets目录的文件对象 File assetDir = new File(inputFile, ApkConstants.ASSETS_DIR_NAME); //这里递归找到所有的文件,存储到assetsPathSet中,比较简单就不贴代码 findAssetsFile(assetDir); //将忽略的文件,从assetsPathSet中剔除 generateAssetsSet(assetDir.getAbsolutePath()); Log.i(TAG, "find all assets count: %d", assetsPathSet.size()); //核心的实现,解析代码中的assets引用,看下面贴的代码 decodeCode(); Log.i(TAG, "find reference assets count: %d", assetRefSet.size()); assetsPathSet.removeAll(assetRefSet); JsonArray jsonArray = new JsonArray(); for (String name : assetsPathSet) { jsonArray.add(name); } ((TaskJsonResult) taskResult).add("unused-assets", jsonArray); taskResult.setStartTime(startTime); taskResult.setEndTime(System.currentTimeMillis()); return taskResult; } catch (Exception e) { throw new TaskExecuteException(e.getMessage(), e); } } //解析代码中的assets引用 private void decodeCode() throws IOException { for (String dexFileName : dexFileNameList) { //这里用到之前 apktool-lib-2.4.0.jar 包中的类,根据dex文件获取DexBackedDexFile对象 DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(inputFile, dexFileName), Opcodes.forApi(15)); BaksmaliOptions options = new BaksmaliOptions(); //apktool 的api,拿到排序好的类引用集合 List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses()); for (ClassDef classDef : classDefs) { // 按空格将类里面的所有代码组合成一个数组 String[] lines = ApkUtil.disassembleClass(classDef, options); if (lines != null) { //匹配资源文件的引用 readSmaliLines(lines); } } } } private void readSmaliLines(String[] lines) { if (lines == null) { return; } for (String line : lines) { line = line.trim(); //找到常量字符 if (!Util.isNullOrNil(line) && line.startsWith("const-string")) { String[] columns = line.split(","); if (columns.length == 2) { //拿到常量字符中的资源名字 String assetFileName = columns[1].trim(); assetFileName = assetFileName.substring(1, assetFileName.length() - 1); if (!Util.isNullOrNil(assetFileName)) { for (String path : assetsPathSet) { //循环匹配,匹配上后就加入到assetRefSet中 if (assetFileName.endsWith(path)) { assetRefSet.add(path); } } } } } } }
代码分析完,我们已经知道,原理其实就是对类里面的常量字符与assets目录中文件名做了匹配处理,能匹配上说明有被引用到,如果匹配失败,那就是没有被引用。分析到这里,你是不是对其他的Task也有些好奇且有了分析的方法了呢?由于篇幅原因我们就不再详细展开,有问题欢迎评论区提问。
小结
本期就先到这里,后面再补充详细的实战示例,毕竟实践才能出真理,由于时间的原因,后面还有8篇,当然我们肯定会在后续持续更新的,敬请谅解。