An overview of the

Two years ago, wechat opened the Matrix project and provided APM implementation schemes for Android and ios. For Android, it mainly includes APK Checker, Resource Canary, Trace Canary, SQLite Lint, and IO Canary. This article mainly introduces the source code implementation of APK Checker, and the rest of the source code analysis will follow.

Code framework analysis

The overall code structure is relatively clear, mainly including three parts: ApkJob, Task, Result. ApkJob indicates the overall APK detection Task, Task indicates the subdivided detection Task, and Result indicates the Result of the detection Task. The overall process is as follows: ApkJob reads configuration information and instantiates related tasks. Output the Result to a file (default: MMTaskJsonResult) after the related Task is executed.

Task Task implementation analysis

UnZipTask

Purpose: Decompress THE Apk, parse the Class obfuscating rules and Res obfuscating rules, and output the original size of each entry in the Apk and the compressed size in the ZIP package. Some raw data is stored to prepare for subsequent tasks.

@Override
    public TaskResult call(a) throws TaskExecuteException {

        try {
            / / the apk
            ZipFile zipFile = newZipFile(inputFile); .//Result Output objectTaskResult taskResult = TaskResultFactory.factory(getType(), TASK_RESULT_TYPE_JSON, config); ./ / apk total size
            ((TaskJsonResult) taskResult).add("total-size", inputFile.length());
            // Read the Class mapping rule and store it in the config object
            readMappingTxtFile();
            config.setProguardClassMap(proguardClassMap);
            // Read the Res mapping rule and store it in the config object
            readResMappingTxtFile();
            config.setResguardMap(resguardMap);

            Enumeration entries = zipFile.entries();
            JsonArray jsonArray = new JsonArray();
            String outEntryName = "";
            while (entries.hasMoreElements()) {
                ZipEntry entry = (ZipEntry) entries.nextElement();
                outEntryName = writeEntry(zipFile, entry);
                if(! Util.isNullOrNil(outEntryName)) { JsonObject fileItem =new JsonObject();
                    // Print the name and compressed size of each item in Apk
                    fileItem.addProperty("entry-name", outEntryName);
                    fileItem.addProperty("entry-size", entry.getCompressedSize());
                    jsonArray.add(fileItem);
                    //Map: uncompressed file (relative path) -> (uncompressed Size, compressed Size)
                    entrySizeMap.put(outEntryName, Pair.of(entry.getSize(), entry.getCompressedSize()));
                    //Map: Apk file name -> : decompressed file (relative path)entryNameMap.put(entry.getName(), outEntryName); }}// Store to the config object
            config.setEntrySizeMap(entrySizeMap);
            config.setEntryNameMap(entryNameMap);
            // Output to Result
            ((TaskJsonResult) taskResult).add("entries", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

Mapping file parsing rules for Task mapping files:

  • Class Mapping

Class Mapping file snippet:

. android.arch.core.executor.ArchTaskExecutorThe $1 -> android.arch.a.a.aThe $1:
    42:42:void <init>() -> <init>
    45:46:void execute(java.lang.Runnable) -> execute
android.arch.core.executor.ArchTaskExecutor$2 -> android.arch.a.a.a$2:
    50:50:void <init>() -> <init>
    53:54:void execute(java.lang.Runnable) -> execute
android.arch.core.executor.DefaultTaskExecutor -> android.arch.a.a.b:
    java.lang.Object mLock -> a
    java.util.concurrent.ExecutorService mDiskIO -> b
    android.os.Handler mMainHandler -> c
    31:33:void <init>() -> <init>
    40:41:void executeOnDiskIO(java.lang.Runnable) -> a
    45:54:void postToMainThread(java.lang.Runnable) -> b
    58:58:boolean isMainThread() -> b
    ...
Copy the code
* Original class name -> obfuscated class name (top) * Original field name -> obfuscated field name (first Tab reserved) * Original function name -> Obfuscated function name (first Tab reserved)Copy the code
  • Res Mapping

Excerpt from res Mapping file:

res path mapping:
    res/layout-v22 -> r/a
    res/drawable -> r/b
    res/color-night-v8 -> r/c
    res/xml -> r/d
    res/layout -> r/e
  ...
  
res id mapping:
    com.example.app.R.attr.avatar_border_color -> com.example.app.R.attr.a
    com.example.app.R.attr.actualImageScaleType -> com.example.app.R.attr.b
    com.example.app.R.attr.backgroundImage -> com.example.app.R.attr.c
    com.example.app.R.attr.fadeDuration -> com.example.app.R.attr.d
    com.example.app.R.attr.failureImage -> com.example.app.R.attr.e
Copy the code
* Original resource directory -> Confounded resource directory * Original resource name -> Confounded resource nameCopy the code

ManifestAnalyzeTask

Purpose: Parse Manifest files and ARSC files

public TaskResult call(a) throws TaskExecuteException {
        try {
            ManifestParser manifestParser = null;
            // Create the Manifest object
            if(! FileUtil.isLegalFile(arscFile)) { manifestParser =new ManifestParser(inputFile);
            } else {
                manifestParser = new ManifestParser(inputFile, arscFile);
            }
            TaskResult taskResult = TaskResultFactory.factory(getType(), TASK_RESULT_TYPE_JSON, config);
            if (taskResult == null) {
                return null;
            }
            long startTime = System.currentTimeMillis();
            JsonObject jsonObject = manifestParser.parse();
            // Output the Manifest result
            ((TaskJsonResult) taskResult).add("manifest", jsonObject);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

Here’s a look at the ARSC file. Arsc files exist in binary form and store index information of resources. The basic file format is shown as follows (image source network) :

Use binary tools to view the contents of arSC files:

The details of the ARSC file format are not expanded at this time, but refer to the article. Here is a brief analysis of some of the information presented visually in the binary tool.

  • ResTable_header part

  • ResStringPool part

  • ResTablePackage

See article for more information about ARSC file parsing

ShowFileSizeTask

Objective: Collect statistics about the files that exceed the threshold.

public TaskResult call(a) throws TaskExecuteException {...long startTime = System.currentTimeMillis();
            // Get the file name recorded in UnZipTask -> (file size after compression, file size before compression) map
            Map<String, Pair<Long, Long>> entrySizeMap = config.getEntrySizeMap();
            if(! entrySizeMap.isEmpty()) {for (Map.Entry<String, Pair<Long, Long>> entry : entrySizeMap.entrySet()) {
                    final String suffix = getSuffix(entry.getKey());
                    Pair<Long, Long> size = entry.getValue();
                    // Record files that exceed the threshold
                    if (size.getFirst() >= downLimit * ApkConstants.K1024) {
                        if(filterSuffix.isEmpty() || filterSuffix.contains(suffix)) { entryList.add(Pair.of(entry.getKey(), size.getFirst())); }}}}.../ / sorting

            JsonArray jsonArray = new JsonArray();
            for (Pair<String, Long> sortFile : entryList) {
                JsonObject fileItem = new JsonObject();
                fileItem.addProperty("entry-name", sortFile.getFirst());
                fileItem.addProperty("entry-size", sortFile.getSecond());
                jsonArray.add(fileItem);
            }
            // Output to the result
            ((TaskJsonResult) taskResult).add("files", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

MethodCountTask

Objective: Count the number of methods defined in this DEX file and methods not defined in this DEX file.

public TaskResult call(a) throws TaskExecuteException {
        try{...long startTime = System.currentTimeMillis();
            JsonArray jsonArray = new JsonArray();
            for (int i = 0; i < dexFileList.size(); i++) {
                RandomAccessFile dexFile = dexFileList.get(i);
                // Calculate method information in dex
                countDex(dexFile);
                // The defined method can be found in the dex
                int totalInternalMethods = sumOfValue(classInternalMethod);
                // Cross-dex method
                int totalExternalMethods = sumOfValue(classExternalMethod);
                JsonObject jsonObject = new JsonObject();
                jsonObject.addProperty("dex-file", dexFileNameList.get(i));
                // Aggregate by Class dimension
                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);
                    // Aggregate by package dimension
                } 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 newTaskExecuteException(e.getMessage(), e); }}Copy the code

The focus of this code is how to statically analyze the DEX file

private void countDex(RandomAccessFile dexFile) throws IOException {
        classInternalMethod.clear();
        classExternalMethod.clear();
        pkgInternalRefMethod.clear();
        pkgExternalMethod.clear();
        DexData dexData = new DexData(dexFile);
        // Load dex data
        dexData.load();
        MethodRef[] methodRefs = dexData.getMethodRefs();
        ClassRef[] externalClassRefs = dexData.getExternalReferences();
        // Get confused Class maping rules
        Map<String, String> proguardClassMap = config.getProguardClassMap();
        String className = null;
        for (ClassRef classRef : externalClassRefs) {
            className = ApkUtil.getNormalClassName(classRef.getName());
            if (proguardClassMap.containsKey(className)) {
                // The original className before the obfuscation
                className = proguardClassMap.get(className);
            }
            if (className.indexOf('. ') = = -1) {
                continue;
            }
            classExternalMethod.put(className, 0);
        }
        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); }}}//remove 0-method referenced class
        Iterator<String> iterator = classExternalMethod.keySet().iterator();
        while (iterator.hasNext()) {
            if (classExternalMethod.get(iterator.next()) == 0) { iterator.remove(); }}}Copy the code

Before we understand the code above, let’s look at the dex file format. The DEX file can be divided into Header part, String index table, type index table, method prototype index table, field index table, method index table, class definition and Data Data area.

  • The Header section

  • String index table

  • Type index table

  • Method prototype index table

  • Field index table

  • Method index table

  • The class definition

Through binary tools, roughly explained the dex file format. Back in the code, there’s a difference between classInternalMethod and classExternalMethod; A TypeId is first parsed with an internal field indicating whether the type is defined in the dex file.

  /** * Holds the contents of a type_id_item. * * This is chiefly a list of indices into the string table. We need * some additional bits of data, such as whether or not the type ID * represents a class defined in this DEX, so we use an object for * each instead of a simple integer. (Could use a parallel array, but * since this is a desktop app it's not essential.) */
    static class TypeIdItem {
        public int descriptorIdx;       // index into string_ids

        public boolean internal;        // defined within this DEX file?
    }
Copy the code

The internal field assignment is as follows:

 /**
     * Sets the "internal" flag on type IDs which are defined in the
     * DEX file or within the VM (e.g. primitive classes and arrays).
     */
    void markInternalClasses() {
        for (int i = mClassDefs.length - 1; i >= 0; i--) {
            mTypeIds[mClassDefs[i].classIdx].internal = true;
        }

        for (int i = 0; i < mTypeIds.length; i++) {
            String className = mStrings[mTypeIds[i].descriptorIdx];

            if (className.length() == 1) {
                // primitive class
                mTypeIds[i].internal = true;
            } else if (className.charAt(0) == '[') {
                mTypeIds[i].internal = true;
            }

            //System.out.println(i + "" +
            //    (mTypeIds[i].internal ? "INTERNAL" : "external") + "-"+ // mStrings[mTypeIds[i].descriptorIdx]); }}Copy the code

All types defined in ClassDef are internal, while the converted className type of length 1 (the underlying data type) is interal, and the last array type is internal.

The specific classification rules of classInternalMethod and classExternalMethod are as follows:

  private void countDex(RandomAccessFile dexFile) throws IOException {... .for (ClassRef classRef : externalClassRefs) {
            className = ApkUtil.getNormalClassName(classRef.getName());
            if (proguardClassMap.containsKey(className)) {
                className = proguardClassMap.get(className);
            }
            if (className.indexOf('. ') = = -1) {
                continue;
            }
            // Add class names whose class definitions are not in this dex file to map
            classExternalMethod.put(className, 0);
        }
        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;
                }
                // Add different categories according to the class name
                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); }}}//remove 0-method referenced class
        Iterator<String> iterator = classExternalMethod.keySet().iterator();
        while (iterator.hasNext()) {
            if (classExternalMethod.get(iterator.next()) == 0) { iterator.remove(); }}}Copy the code

ResProguardCheckTask

Purpose: Determine whether APK performs resource obfuscation.

 @Override
    public TaskResult call(a) throws TaskExecuteException {
        File resDir = newFile(inputFile, ApkConstants.RESOURCE_DIR_PROGUARD_NAME); .if (resDir.exists() && resDir.isDirectory()) {
                Log.d(TAG, "find resource directory " + resDir.getAbsolutePath());
                // the folder named r performs support obfuscation
                ((TaskJsonResult) taskResult).add("hasResProguard".true);
            } else {
                resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_NAME);
                if (resDir.exists() && resDir.isDirectory()) {
                    File[] dirs = resDir.listFiles();
                    boolean hasProguard = true;
                    for (File dir : dirs) {
                        // If any folder does not comply with the naming rules for resource obfuscation, resource obfuscation is not performed
                        if(dir.isDirectory() && ! fileNamePattern.matcher(dir.getName()).matches()) { hasProguard =false;
                            Log.i(TAG, "directory " + dir.getName() + " has a non-proguard name!");
                            break;
                        }
                    }
                    ((TaskJsonResult) taskResult).add("hasResProguard", hasProguard); . }Copy the code

FindNonAlphaPngTask

Purpose: Detect PNG files with no transparency (should be replaced with JPG to take up less space)

private void findNonAlphaPng(File file) throws IOException {
        if(file ! =null) {
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for(File tempFile : files) { findNonAlphaPng(tempFile); }}else if(file.isFile() && file.getName().endsWith(ApkConstants.PNG_FILE_SUFFIX) && ! file.getName().endsWith(ApkConstants.NINE_PNG)) { BufferedImage bufferedImage = ImageIO.read(file);// No alpha information
                if(! bufferedImage.getColorModel().hasAlpha()) { String filename = file.getAbsolutePath().substring(inputFile.getAbsolutePath().length() +1);
                    if (entryNameMap.containsKey(filename)) {
                        filename = entryNameMap.get(filename);
                    }
                    long size = file.length();
                    if (entrySizeMap.containsKey(filename)) {
                        size = entrySizeMap.get(filename).getFirst();
                    }
                    if (size >= downLimitSize * ApkConstants.K1024) {
                        nonAlphaPngList.add(Pair.of(filename, file.length()));
                    }
                }
            }
        }
    }
Copy the code

MultiLibCheckTask

Purpose: Check if multiple folders exist in the lib folder.

    @Override
    public TaskResult call(a) throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(getType(), TASK_RESULT_TYPE_JSON, config);
            if (taskResult == null) {
                return null;
            }
            long startTime = System.currentTimeMillis();
            JsonArray jsonArray = new JsonArray();
            if (libDir.exists() && libDir.isDirectory()) {
                File[] dirs = libDir.listFiles();
                for (File dir : dirs) {
                    if (dir.isDirectory()) {
                        jsonArray.add(dir.getName());
                    }
                }
            }
            ((TaskJsonResult) taskResult).add("lib-dirs", jsonArray);
            if (jsonArray.size() > 1) {
                ((TaskJsonResult) taskResult).add("multi-lib".true);
            } else {
                ((TaskJsonResult) taskResult).add("multi-lib".false);
            }
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

UncompressedFileTask

Objective: To compare the size of each entry in apK compression package after compression and before compression. If the sizes are the same, the files are not compressed.

@Override
    public TaskResult call(a) throws TaskExecuteException {
        try{...if(! entrySizeMap.isEmpty()) {//take advantage of the result of UnzipTask.
                for (Map.Entry<String, Pair<Long, Long>> entry : entrySizeMap.entrySet()) {
                    final String suffix = getSuffix(entry.getKey());
                    Pair<Long, Long> size = entry.getValue();
                    if (filterSuffix.isEmpty() || filterSuffix.contains(suffix)) {
                        if(! uncompressSizeMap.containsKey(suffix)) { uncompressSizeMap.put(suffix, size.getFirst()); }else {
                            uncompressSizeMap.put(suffix, uncompressSizeMap.get(suffix) + size.getFirst());
                        }
                        if(! compressSizeMap.containsKey(suffix)) { compressSizeMap.put(suffix, size.getSecond()); }else{ compressSizeMap.put(suffix, compressSizeMap.get(suffix) + size.getSecond()); }}else {
// Log.d(TAG, "file: %s, filter by suffix.", entry.getKey());}}}for (String suffix : uncompressSizeMap.keySet()) {
            // Size comparison
                if (uncompressSizeMap.get(suffix).equals(compressSizeMap.get(suffix))) {
                    JsonObject fileItem = new JsonObject();
                    fileItem.addProperty("suffix", suffix);
                    fileItem.addProperty("total-size", uncompressSizeMap.get(suffix));
                    jsonArray.add(fileItem);
                }
            }
            ((TaskJsonResult) taskResult).add("files", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

CountRTask

Objective: Count the number of R files.

  @Override
    public TaskResult call(a) throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            Map<String, String> classProguardMap = config.getProguardClassMap();
            for (RandomAccessFile dexFile : dexFileList) {
                DexData dexData = new DexData(dexFile);
                dexData.load();
                ClassRef[] defClassRefs = dexData.getInternalReferences();
                for (ClassRef classRef : defClassRefs) {
                    String className = ApkUtil.getNormalClassName(classRef.getName());
                    if (classProguardMap.containsKey(className)) {
                        className = classProguardMap.get(className);
                    }
                    // Remove the inner class
                    String pureClassName = getOuterClassName(className);
                    // Identify R files
                    if (pureClassName.endsWith(".R") | |"R".equals(pureClassName)) {
                        if(! classesMap.containsKey(pureClassName)) { classesMap.put(pureClassName, classRef.getFieldArray().length); }else {
                            classesMap.put(pureClassName, classesMap.get(pureClassName) + classRef.getFieldArray().length);
                        }
                    }
                }
            }

            JsonArray jsonArray = new JsonArray();
            long totalSize = 0;
            Map<String, String> proguardClassMap = config.getProguardClassMap();
            for (Map.Entry<String, Integer> entry : classesMap.entrySet()) {
                JsonObject jsonObject = new JsonObject();
                if (proguardClassMap.containsKey(entry.getKey())) {
                    jsonObject.addProperty("name", proguardClassMap.get(entry.getKey()));
                } else {
                    jsonObject.addProperty("name", entry.getKey());
                }
                jsonObject.addProperty("field-count", entry.getValue());
                totalSize += entry.getValue();
                jsonArray.add(jsonObject);
            }
            ((TaskJsonResult) taskResult).add("R-count", jsonArray.size());
            ((TaskJsonResult) taskResult).add("Field-counts", totalSize);

            ((TaskJsonResult) taskResult).add("R-classes", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

DuplicateFileTask

Purpose: Determine whether identical files exist in APK by calculating MD5.

private void computeMD5(File file) throws NoSuchAlgorithmException, IOException {
        if(file ! =null) {
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                for(File resFile : files) { computeMD5(resFile); }}else {
                MessageDigest msgDigest = MessageDigest.getInstance("MD5");
                BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
                byte[] buffer = new byte[512];
                int readSize = 0;
                long totalRead = 0;
                while ((readSize = inputStream.read(buffer)) > 0) {
                    msgDigest.update(buffer, 0, readSize);
                    totalRead += readSize;
                }
                inputStream.close();
                if (totalRead > 0) {
                    final String md5 = Util.byteArrayToHex(msgDigest.digest());
                    String filename = file.getAbsolutePath().substring(inputFile.getAbsolutePath().length() + 1);
                    if (entryNameMap.containsKey(filename)) {
                        filename = entryNameMap.get(filename);
                    }
                    if(! md5Map.containsKey(md5)) { md5Map.put(md5,new ArrayList<String>());
                        if (entrySizeMap.containsKey(filename)) {
                            fileSizeList.add(Pair.of(md5, entrySizeMap.get(filename).getFirst()));
                        } else{ fileSizeList.add(Pair.of(md5, totalRead)); }}//md5 the same file listmd5Map.get(md5).add(filename); }}}}Copy the code
@Override
    public TaskResult call(a) throws TaskExecuteException {... .for (Pair<String, Long> entry : fileSizeList) {
                //md5 the same file
                if (md5Map.get(entry.getFirst()).size() > 1) {
                    JsonObject jsonObject = new JsonObject();
                    jsonObject.addProperty("md5", entry.getFirst());
                    jsonObject.addProperty("size", entry.getSecond());
                    JsonArray jsonFiles = new JsonArray();
                    for (String filename : md5Map.get(entry.getFirst())) {
                        jsonFiles.add(filename);
                    }
                    jsonObject.add("files", jsonFiles);
                    jsonArray.add(jsonObject);
                }
            }
            ((TaskJsonResult) taskResult).add("files", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
        } catch (Exception e) {
            throw new TaskExecuteException(e.getMessage(), e);
        }
        return taskResult;
    }
Copy the code

MultiSTLCheckTask

Objective: To determine whether SO has multiple copies of STL standard library.

@Override
    public TaskResult call(a) throws TaskExecuteException {
        try{...for (File libFile : libFiles) {
                if (isStlLinked(libFile)) {
                    Log.d(TAG, "lib: %s has stl link", libFile.getName());

                    jsonArray.add(libFile.getName());
                }
            }
            ((TaskJsonResult) taskResult).add("stl-lib", jsonArray);
            if (jsonArray.size() > 1) {
                ((TaskJsonResult) taskResult).add("multi-stl".true);
            } else {
                ((TaskJsonResult) taskResult).add("multi-stl".false);
            }
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code
private boolean isStlLinked(File libFile) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder(toolnmPath, "-D"."-C", libFile.getAbsolutePath());
        Process process = processBuilder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line = reader.readLine();
        while(line ! =null) {
            String[] columns = line.split("");
// Log.d(TAG, "%s", line);
            if (columns.length >= 3 && columns[1].equals("T") && columns[2].startsWith("std::")) {
                return true;
            }
            line = reader.readLine();
        }
        reader.close();
        process.waitFor();
        return false;
    }
Copy the code

UnusedResourcesTask

Purpose: To detect unreferenced resources in code, resource files.

 @Override
    public TaskResult call(a) throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            readMappingTxtFile();
            readResourceTxtFile();
            // Add all declared resources
            unusedResSet.addAll(resourceDefMap.values());
            Log.d(TAG, "find resource declarations %d items.", unusedResSet.size());
            // Find all the resources used in the code
            decodeCode();
            Log.d(TAG, "find resource references in classes: %d items.", resourceRefSet.size());
            // Find the resource referenced in all resources
            decodeResources();
            Log.d(TAG, "find resource references %d items.", resourceRefSet.size());
            // Remove the referenced resource
            unusedResSet.removeAll(resourceRefSet);
            Log.d(TAG, "find unused references %d items", unusedResSet.size());

            JsonArray jsonArray = new JsonArray();
            for (String name : unusedResSet) {
                jsonArray.add(name);
            }
            ((TaskJsonResult) taskResult).add("unused-resources", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code
private void readMappingTxtFile(a) throws IOException {
        // com.tencent.mm.R$string -> com.tencent.mm.R$l:
        // int fade_in_property_anim -> aRW

        if(mappingTxt ! =null) {
            BufferedReader bufferedReader = new BufferedReader(new FileReader(mappingTxt));
            String line = bufferedReader.readLine();
            boolean readRField = false;
            String beforeClass = "", afterClass = "";
            try {
                while(line ! =null) {
                    if(! line.startsWith("")) {
                        String[] pair = line.split("- >");
                        if (pair.length == 2) {
                            beforeClass = pair[0].trim();
                            afterClass = pair[1].trim();
                            afterClass = afterClass.substring(0, afterClass.length() - 1);
                            if(! Util.isNullOrNil(beforeClass) && ! Util.isNullOrNil(afterClass) && ApkUtil.isRClassName(ApkUtil.getPureClassName(beforeClass))) {// Log.d(TAG, "before:%s,after:%s", beforeClass, afterClass);
                                readRField = true;
                            } else {
                                readRField = false; }}else {
                            readRField = false; }}else {
                        if (readRField) {
                            String[] entry = line.split("- >");
                            if (entry.length == 2) {
                                String key = entry[0].trim();
                                String value = entry[1].trim();
                                if(! Util.isNullOrNil(key) && ! Util.isNullOrNil(value)) { String[] field = key.split("");
                                    if (field.length == 2) {
// Log.d(TAG, "%s -> %s", afterClass.replace('$', '.') + "." + value, getPureClassName(beforeClass).replace('$', '.') + "." + field[1]);
                                        // add r.java full path field after confusion -> r.java full path field before confusion
                                        rclassProguardMap.put(afterClass.replace('$'.'. ') + "." + value, ApkUtil.getPureClassName(beforeClass).replace('$'.'. ') + "." + field[1]); } } } } } line = bufferedReader.readLine(); }}finally{ bufferedReader.close(); }}}Copy the code
private void readResourceTxtFile(a) throws IOException {
        / / read R.t xt
        BufferedReader bufferedReader = new BufferedReader(new FileReader(resourceTxt));
        String line = bufferedReader.readLine();
        try {
            while(line ! =null) {
                String[] columns = line.split("");
                if (columns.length >= 4) {
                    final String resourceName = "R." + columns[1] + "." + columns[2];
                    if(! columns[0].endsWith("[]") && columns[3].startsWith("0x")) {
                        //int styleable ActionBar_title 27
                        if (columns[3].startsWith("0x01")) {
                            Log.d(TAG, "ignore system resource %s", resourceName);
                        } else {
                            final String resId = parseResourceId(columns[3]);
                            if(! Util.isNullOrNil(resId)) {// Resource ID Resource name mappingresourceDefMap.put(resId, resourceName); }}}else {
                        //int[] styleable ActionMode { 0x7f030034, 0x7f030036, 0x7f030056, 0x7f0300ad, 0x7f030168, 0x7f03019e }
                        Log.d(TAG, "ignore resource %s", resourceName);
                        if (columns[0].endsWith("[]") && columns.length > 5) {
                            Set<String> attrReferences = new HashSet<String>();
                            for (int i = 4; i < columns.length; i++) {
                                if (columns[i].endsWith(",")) {
                                    attrReferences.add(columns[i].substring(0, columns[i].length() - 1));
                                } else{ attrReferences.add(columns[i]); }}/ / style mappingstyleableMap.put(resourceName, attrReferences); } } } line = bufferedReader.readLine(); }}finally{ bufferedReader.close(); }}Copy the code

Parse smali code in dex file:

private void decodeCode(a) throws IOException {
        for (String dexFileName : dexFileNameList) {
            DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(inputFile, dexFileName), Opcodes.forApi(15));

            BaksmaliOptions options = new BaksmaliOptions();
            List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());

            for (ClassDef classDef : classDefs) {
                String[] lines = ApkUtil.disassembleClass(classDef, options);
                if(lines ! =null) { readSmaliLines(lines); }}}}Copy the code
private void readSmaliLines(String[] lines) {
        if (lines == null) {
            return;
        }
        for (String line : lines) {
            line = line.trim();
            if(! Util.isNullOrNil(line)) {if (line.startsWith("const")) {
                    String[] columns = line.split(",");
                    if (columns.length == 2) {
                        final String resId = parseResourceId(columns[1].trim());
                        // Get the resource name from id
                        if(! Util.isNullOrNil(resId) && resourceDefMap.containsKey(resId)) { resourceRefSet.add(resourceDefMap.get(resId)); }}}else if (line.startsWith("sget")) {
                    String[] columns = line.split("");
                    if (columns.length == 3) {
                        // Get the resource name
                        final String resourceRef = parseResourceNameFromProguard(columns[2]);
                        if(! Util.isNullOrNil(resourceRef)) {//Log.d(TAG, "find resource reference %s", resourceRef);
                            if (styleableMap.containsKey(resourceRef)) {
                                //reference of R.styleable.XXX
                                for(String attr : styleableMap.get(resourceRef)) { resourceRefSet.add(resourceDefMap.get(attr)); }}else {
                                resourceRefSet.add(resourceRef);
                            }
                        }
                    }
                }
            }
        }
    }
Copy the code
private String parseResourceNameFromProguard(String entry) {
        if(! Util.isNullOrNil(entry)) {// sget v6, Lcom/tencent/mm/R$string; ->chatting_long_click_menu_revoke_msg:I
            // sget v1, Lcom/tencent/mm/libmmui/R$id; ->property_anim:I
            // sput-object v0, Lcom/tencent/mm/plugin_welab_api/R$styleable; ->ActionBar:[I
            // const v6, 0x7f0c0061
            String[] columns = entry.split("- >");
            if (columns.length == 2) {
                int index = columns[1].indexOf(':');
                if (index >= 0) {
                    final String className = ApkUtil.getNormalClassName(columns[0]);
                    final String fieldName = columns[1].substring(0, index);
                    if(! rclassProguardMap.isEmpty()) { String resource = className.replace('$'.'. ') + "." + fieldName;
                        if (rclassProguardMap.containsKey(resource)) {
                            return rclassProguardMap.get(resource);
                        } else {
                            return ""; }}else {
                        if (ApkUtil.isRClassName(ApkUtil.getPureClassName(className))) {
                            return (ApkUtil.getPureClassName(className) + "." + fieldName).replace('$'.'. ');
                        }
                    }
                }
            }
        }
        return "";
    }
Copy the code

UnusedAssetsTask

Purpose: Detect asset resources in APK that are not being used (there are legacies if the code implementation only overwrites string constants).

 @Override
    public TaskResult call(a) throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            File assetDir = new File(inputFile, ApkConstants.ASSETS_DIR_NAME);
            // Find all asset files
            findAssetsFile(assetDir);
            generateAssetsSet(assetDir.getAbsolutePath());
            Log.d(TAG, "find all assets count: %d", assetsPathSet.size());
            // Parse the asset reference in the code
            decodeCode();
            Log.d(TAG, "find reference assets count: %d", assetRefSet.size());
            // Remove the referenced resource
            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 newTaskExecuteException(e.getMessage(), e); }}Copy the code
private void generateAssetsSet(String rootPath) {
        HashSet<String> relativeAssetsSet = new HashSet<String>();
        for (String path : assetsPathSet) {
            int index = path.indexOf(rootPath);
            if (index >= 0) {
                String relativePath = path.substring(index + rootPath.length() + 1);
                //Log.d(TAG, "assets %s", relativePath);
                relativeAssetsSet.add(relativePath);
                if (ignoreAsset(relativePath)) {
                    Log.d(TAG, "ignore assets %s", relativePath);
                    // Get the relative path in which the asset is used
                    assetRefSet.add(relativePath);
                }
            }
        }
        assetsPathSet.clear();
        assetsPathSet.addAll(relativeAssetsSet);
    }
Copy the code
private void readSmaliLines(String[] lines) {
        if (lines == null) {
            return;
        }
        for (String line : lines) {
            line = line.trim();
            // invoke-virtual {p0}, Lcom/ss/android/alog/App; ->getAssets()Landroid/content/res/AssetManager;

            //move-result-object v1

            //const-string v2, "video"

            //invoke-virtual {v1, v2}, Landroid/content/res/AssetManager; ->open(Ljava/lang/String;) Ljava/io/InputStream;
            //:try_end_13
            //.catch Ljava/io/IOException; {:try_start_a .. :try_end_13} :catch_1a
            // This const-string judgment is not very good, can only be written to dead values
            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)) {// Check if the constant is in the asset file name collection
                        for (String path : assetsPathSet) {
                            if (path.endsWith(assetFileName)) {
                                assetRefSet.add(path);
                            }
                        }
                    }
                }
            }
        }
    }
Copy the code

UnStrippedSoCheckTask

Objective: To detect untrimmed SO in APK.

@Override
    public TaskResult call(a) throws TaskExecuteException {
        try{...if (libDir.exists() && libDir.isDirectory()) {
                File[] dirs = libDir.listFiles();
                for (File dir : dirs) {
                    if (dir.isDirectory()) {
                        File[] libs = dir.listFiles();
                        for (File libFile : libs) {
                            if (libFile.isFile() && libFile.getName().endsWith(ApkConstants.DYNAMIC_LIB_FILE_SUFFIX)) {
                                libFiles.add(libFile);
                            }
                        }
                    }
                }
            }
            for (File libFile : libFiles) {
                // Determine whether to crop
                if(! isSoStripped(libFile)) { Log.d(TAG,"lib: %s is not stripped", libFile.getName());

                    jsonArray.add(libFile.getName());
                }
            }
            ((TaskJsonResult) taskResult).add("unstripped-lib", jsonArray);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

Determine whether so is clipped from the command line

private boolean isSoStripped(File libFile) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder(toolnmPath, libFile.getAbsolutePath());
        Process process = processBuilder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
        String line = reader.readLine();
        if(! Util.isNullOrNil(line)) {//Log.d(TAG, "%s", line);
            String[] columns = line.split(":");
            if (columns.length == 3 && columns[2].trim().equalsIgnoreCase("no symbols")) {
                return true;
            }
        }
        reader.close();
        process.waitFor();
        return false;
    }
Copy the code

CountClassTask

Objective: Count the number of classes.

@Override
    public TaskResult call(a) throws TaskExecuteException {
        try {
            TaskResult taskResult = TaskResultFactory.factory(type, TaskResultFactory.TASK_RESULT_TYPE_JSON, config);
            long startTime = System.currentTimeMillis();
            Map<String, String> classProguardMap = config.getProguardClassMap();
            JsonArray dexFiles = new JsonArray();

            for (int i = 0; i < dexFileList.size(); i++) {
                RandomAccessFile dexFile = dexFileList.get(i);
                DexData dexData = new DexData(dexFile);
                dexData.load();
                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);
                }
                JsonObject jsonObject = new JsonObject();
                jsonObject.addProperty("dex-file", dexFileNameList.get(i));
                //Log.d(TAG, "dex %s, classes %s", dexFileNameList.get(i), classNameSet.toString());

                Map<String, Set<String>> packageClass = new HashMap<>();
                if (JobConstants.GROUP_PACKAGE.equals(group)) {
                    String packageName = "";
                    for (String clazzName : classNameSet) {
                        packageName = ApkUtil.getPackageName(clazzName);
                        if(! Util.isNullOrNil(packageName)) {if(! packageClass.containsKey(packageName)) { packageClass.put(packageName,new HashSet<String>());
                            }
                            // Aggregate as package
                            packageClass.get(packageName).add(clazzName);
                        }
                    }
                    JsonArray packages = new JsonArray();
                    for (Map.Entry<String, Set<String>> pkg : packageClass.entrySet()) {
                        JsonObject pkgObj = new JsonObject();
                        pkgObj.addProperty("package", pkg.getKey());
                        JsonArray classArray = new JsonArray();
                        for (String clazz : pkg.getValue()) {
                            classArray.add(clazz);
                        }
                        // All classes in a single package
                        pkgObj.add("classes", classArray);
                        packages.add(pkgObj);
                    }
                    jsonObject.add("packages", packages);
                }
                dexFiles.add(jsonObject);
            }

            ((TaskJsonResult) taskResult).add("dex-files", dexFiles);
            taskResult.setStartTime(startTime);
            taskResult.setEndTime(System.currentTimeMillis());
            return taskResult;
        } catch (Exception e) {
            throw newTaskExecuteException(e.getMessage(), e); }}Copy the code

conclusion

The code logic of Matrix static APK scanning part is relatively simple. After a preliminary understanding of the DEX file format, arSC file format, code understanding should not be too much of a problem.