程序性能優化之APK大小優化(六)下篇

 

阿里P7移動互聯網架構師進階視頻(每日更新中)免費學習請點擊:https://space.bilibili.com/474380680html

本篇文章將繼續從微信資源混淆AndResGuard原理來介紹APK大小優化:
微信的AndResGuard工具是用於Android資源的混淆,做用有兩點:一是經過混淆資源ID長度同時利用7z深度壓縮,減少了apk包大小;二是混淆後在安全性方面有一點提高,提升了逆向破解難度。本文從源碼角度,來探尋AndResGuard實現原理。linux

閱讀本文須要前提知識:掌握Android應用程序打包編譯過程,尤爲是對資源的編譯和打包過程;熟悉resource.arsc文件格式。git

推薦羅昇陽文章:http://blog.csdn.net/luoshengyang/article/details/8744683
微信資源混淆工具源碼地址:https://github.com/shwenzhang/AndResGuard
附上來自網絡神圖:github

 
19956127-5ebe6cb98b1d0dfb.png
 

 

0、程序入口CliMain.main()
該函數處理命令行參數、並解析自定義配置文件,混淆工具能夠根據配置項進行特定處理,具體參考config.xml內容,針對其中特定內容,咱們會在後面提到。而後進入真正混淆的入口函數resourceProgurad()正則表達式

特別說明一下解析Configuration中關鍵點,處理複用舊的mapping文件:
一、processOldMappingFile()緩存

private void processOldMappingFile() throws IOException {
        ...
        try {
            String line = br.readLine();

            while (line != null) {
                if (line.length() > 0) {
                    Matcher mat = MAP_PATTERN.matcher(line);

                    if (mat.find()) {
                        String nameAfter = mat.group(2);
                        String nameBefore = mat.group(1);
                        nameAfter = nameAfter.trim();
                        nameBefore = nameBefore.trim();

                        //若是有這個的話,那就是mOldFileMapping
                        if (line.contains("/")) {
                            mOldFileMapping.put(nameBefore, nameAfter);
                        } else {
                            //這裏是resid的mapping
                            int packagePos = nameBefore.indexOf(".R.");
                            if (packagePos == -1) {
                                throw new IOException(
                                    String.format(
                                        "the old mapping file packagename is malformed, " +
                                            "it should be like com.tencent.mm.R.attr.test, yours %s\n", nameBefore)
                                );

                            }
                            String packageName = nameBefore.substring(0, packagePos);
                            int nextDot = nameBefore.indexOf(".", packagePos + 3);
                            String typeName = nameBefore.substring(packagePos + 3, nextDot);

                            String beforename = nameBefore.substring(nextDot + 1);
                            String aftername = nameAfter.substring(nameAfter.indexOf(".", packagePos + 3) + 1);

                            HashMap<String, HashMap<String, String>> typeMap;

                            if (mOldResMapping.containsKey(packageName)) {
                                typeMap = mOldResMapping.get(packageName);
                            } else {
                                typeMap = new HashMap<>();
                            }

                            HashMap<String, String> namesMap;
                            if (typeMap.containsKey(typeName)) {
                                namesMap = typeMap.get(typeName);
                            } else {
                                namesMap = new HashMap<>();
                            }
                            namesMap.put(beforename, aftername);

                            typeMap.put(typeName, namesMap);
                            mOldResMapping.put(packageName, typeMap);
                        }
                    }

                }
                line = br.readLine();
            }
        }
        ...
    }
}

該函數主要功能是:對oldmapping文件處理是按照正則表達式把「->」分隔提取兩邊字符串,進行hashmap緩存:安全

其1、若是有這個「/」的話,那就是res path mapping即mOldFileMapping的hashmap中:
mOldFileMapping.put(nameBefore, nameAfter);
(例如res/drawable -> r/c,最終mOldFileMapping是(「res/drawable」,」r/c」))微信

其2、不然判斷若是包含「.R.」,則是resid的mapping,最後按照類別、package保存到oldResMapping的hashmap中:
namesMap.put(beforename, aftername);
(例如com.basket24.demo.R.attr.progress -> com.basket24.demo.R.attr.a,最終namesMap是(「progress」,」a」))
typeMap.put(typeName, namesMap);
(例如com.basket24.demo.R.attr.progress -> com.basket24.demo.R.attr.a,最終typeMap是(「attr」,namesMap))
mOldResMapping.put(packageName, typeMap);
(例如com.basket24.demo.R.attr.progress -> com.basket24.demo.R.attr.a,最終mOldResMapping是(「com.basket24.demo」,typeMap))網絡

二、Main.resourceProguard()是混淆真正的入口。架構

protected void resourceProguard(File outputFile, String apkFilePath, InputParam.SignatureType signatureType) {
        ApkDecoder decoder = new ApkDecoder(config);
        File apkFile = new File(apkFilePath);
        ...
        mRawApkSize = FileOperation.getFileSizes(apkFile);
        try {
            /* 默認使用V1簽名 */
            decodeResource(outputFile, decoder, apkFile);
            buildApk(decoder, apkFile, signatureType);
        } catch (Exception e) {
            e.printStackTrace();
            goToError();
        }
    }

混淆入口resourceProguard裏功能:
其一:decodeResource();//進行混淆資源相關功能。
其二:buildApk(decoder, apkFile, signatureType);//最後buildApk生成簽名包。

三、Main.decodeResource()

private void decodeResource(File outputFile, ApkDecoder decoder, File apkFile) {
    decoder.setApkFile(apkFile);
    ...
    decoder.setOutDir(mOutDir.getAbsoluteFile());
    decoder.decode();//混淆資源功能
}

decodeResource核心功能就是設置相關變量,並執行ApkDecoder.decode()。

四、ApkDecoder.decode()

public void decode(){
    if (hasResources()) {
        ensureFilePath();       
        RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"));
        ResPackage[] pkgs = ARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"), this);

        //把沒有紀錄在resources.arsc的資源文件也拷進dest目錄
        copyOtherResFiles();

        /*把整個arsc從新修改其中幾個字符串池和對應大小,造成新的arsc文件。*/
        ARSCDecoder.write(mApkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
      }
}

五、ensureFilePath()

ensureFilePath(){
    Utils.cleanDir(mOutDir);//mOutDir就是outapk目錄

    //temp目錄,用於解壓apk
    String unZipDest = new File(mOutDir, TypedValue.UNZIP_FILE_PATH).getAbsolutePath();

    mCompressData = FileOperation.unZipAPk(mApkFile.getAbsoluteFile().getAbsolutePath(), unZipDest);
    dealWithCompressConfig();//
    //將res混淆成r
    if (!config.mKeepRoot) {
        mOutResFile = new File(mOutDir.getAbsolutePath() + File.separator + TypedValue.RES_FILE_PATH);
    } else {
        mOutResFile = new File(mOutDir.getAbsolutePath() + File.separator + "res");
    }

    //這個須要混淆各個文件夾
    // TypedValue.UNZIP_FILE_PATH指"temp"
    mRawResFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.UNZIP_FILE_PATH + File.separator + "res");
    mOutTempDir = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.UNZIP_FILE_PATH);

    //這裏遍歷獲取原始res目錄的文件
    Files.walkFileTree(mRawResFile.toPath(), new ResourceFilesVisitor());

    mOutTempARSCFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + "resources_temp.arsc");
    mOutARSCFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + "resources.arsc");

    String basename = mApkFile.getName().substring(0, mApkFile.getName().indexOf(".apk"));

    //RES_MAPPING_FILE = "resource_mapping_";
    //mResMappingFile名稱如「resource_mapping_imfun.txt"
    mResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator
        + TypedValue.RES_MAPPING_FILE + basename + TypedValue.TXT_FILE);
}

ensureFilePath主要功能以下:
其1、在輸出目錄下,創建一個temp目錄,用於apk解壓的目錄。unZipAPk解壓apk,獲得mCompressData壓縮條目集合[compress.put(name,entry.getMethod());]
其2、根據config來修改壓縮的值,將知足config的壓縮類型,進行修改壓縮標記爲ZIP_DEFLATED
其3、判斷是否將將res混淆成r
其4、建立須要混淆的temp目錄(apk被解壓到temp目錄)等、使用FileVisitor對目錄進行遍歷,將原始res(」temp/res」)下路徑保存到HashSet中。
其5、建立resources_temp.arsc 和最終resources.arsc等文件及最終mapping命名:resource_mapping_apkname.txt

下面回到第4步ApkDecoder.decode()中繼續執行:

六、RawARSCDecoder.decode()
這一步就是解析原始resources.arsc文件,獲得文件結構並緩存相關數據,如資源類型字符串池mExistTypeNames等。代碼較長,且關鍵步驟較少,故略去代碼。

繼續在第4步ApkDecoder.decode()中往下執行:

七、ARSCDecoder.decode()

public static ResPackage[] decode(InputStream arscStream, ApkDecoder apkDecoder){
    try {
         //proguardFileName混淆文件名
         ARSCDecoder decoder = new ARSCDecoder(arscStream, apkDecoder);
         ResPackage[] pkgs = decoder.readTable();
         return pkgs;
     } catch (IOException ex) {
         throw new AndrolibException("Could not decode arsc file", ex);
     }
}

八、ARSCDecoder的構造函數中執行proguardFileName()

proguardFileName(){

    //其中初始化ProguardStringBuilder,創建各類被映射爲的字符集合標記集合
    mProguardBuilder = new ProguardStringBuilder();
    mProguardBuilder.reset();

    final Configuration config = mApkDecoder.getConfig();
    File rawResFile = mApkDecoder.getRawResFile();
    File[] resFiles = rawResFile.listFiles();
    if (!config.mKeepRoot) {
        //須要保持以前的命名方式
        if (config.mUseKeepMapping) {
            mOldFileMapping提取values部分即"r/c"保存到keepFileNames,而後從mProguardBuilder生成的混淆字符池中刪除掉這些names.
            for (File resFile : resFiles) {
                String raw = "res" + "/" + resFile.getName();
                if (fileMapping.containsKey(raw)) {
                    mOldFileName.put(raw, fileMapping.get(raw));
                } else {
                    mOldFileName.put(raw, resRoot + "/" + mProguardBuilder.getReplaceString());
                }
            }
            /*上面mOldFileName保存的是用舊混淆(沒有的話重新的混淆池中獲取)文件處理過的File混淆映射.
            (mOldFileName("res/attr"," r/h"))*/
        }else{//不然
            for (int i = 0; i < resFiles.length; i++) {
                //這裏也要用linux的分隔符,若是普通的話,就是r
                mOldFileName.put("res" + "/" + resFiles[i].getName(), TypedValue.RES_FILE_PATH + "/" + mProguardBuilder.getReplaceString());
            }
        }

        generalFileResMapping();//資源目錄File映射
    }
}

/**
*對資源目錄File映射。
*/
generalFileResMapping(){
    mMappingWriter.write("res path mapping:\n");
    for (String raw : mOldFileName.keySet()) {
        mMappingWriter.write("    " + raw + " -> " + mOldFileName.get(raw));
        mMappingWriter.write("\n");
    }
    mMappingWriter.write("\n\n");
    mMappingWriter.write("res id mapping:\n");
    mMappingWriter.flush();
}

這裏第8步主要功能是:
其1、其中初始化ProguardStringBuilder,創建混淆字符串池和標記集合。
其2、獲取配置config內容,判斷是否keeproot,是否沿用舊的mapping文件等,進行映射。
其3、generalFileResMapping把緩存的映射hashmap寫入文件,造成mapping文件,其中目前只有資源目錄path映射。

回到第7步中繼續執行decoder.readTable()進行真正混淆

九、decoder.readTable()

ResPackage[] readTable(){
    mTableStrings = StringBlock.read(mIn);
    ResPackage[] packages = new ResPackage[packageCount];
    packages[i] = readPackage();
    return packages;
}

readPackage()解析resources.arsc文件,其中關鍵步驟readEntry()以下:

十、readEntry()

readEntry(){
    if (config.mUseWhiteList) {
        //判斷是否走whitelist
        HashMap<String, HashMap<String, HashSet<Pattern>>> whiteList = config.mWhiteList;
        String packName = mPkg.getName();
        if (whiteList.containsKey(packName)) {

            HashMap<String, HashSet<Pattern>> typeMaps = whiteList.get(packName);
            String typeName = mType.getName();

            if (typeMaps.containsKey(typeName)) {
                String specName = mSpecNames.get(specNamesId).toString();
                HashSet<Pattern> patterns = typeMaps.get(typeName);
                for (Iterator<Pattern> it = patterns.iterator(); it.hasNext(); ) {
                    Pattern p = it.next();
                    if (p.matcher(specName).matches()) {
                        mPkg.putSpecNamesReplace(mResId, specName);//緩存設置package中spec替換項
                        mPkg.putSpecNamesblock(specName);
                        mProguardBuilder.setInWhiteList(mCurEntryID, true);//當前資源項ID標示爲白名單

                        mType.putSpecProguardName(specName);//設置spec的proguard的名稱爲原始資源項名稱
                        isWhiteList = true;
                        break;
                    }
                }
            }

        }


    }


    if (!isWhiteList) {
        boolean keepMapping = false;
        if (config.mUseKeepMapping) {//判斷舊的mapping文件也複用,獲得replaceString
            HashMap<String, HashMap<String, HashMap<String, String>>> resMapping = config.mOldResMapping;
            String packName = mPkg.getName();
            //resMapping是指res Id的映射
            if (resMapping.containsKey(packName)) {
                HashMap<String, HashMap<String, String>> typeMaps = resMapping.get(packName);
                String typeName = mType.getName();
                if (typeMaps.containsKey(typeName)) {
                    //這裏面的東東已經提早去掉,請放心使用
                    /*(例如com.basket24.demo.R.attr.progress -> com.basket24.demo.R.attr.a,最終proguard是("progress","a"))*/
                    HashMap<String, String> proguard = typeMaps.get(typeName);
                    String specName = mSpecNames.get(specNamesId).toString();
                    if (proguard.containsKey(specName)) {
                        keepMapping = true;
                        /*獲取舊的混淆id映射中specname對應的混淆字符串,繼續使用。*/
                        replaceString = proguard.get(specName);
                    }
                }
            }
        }

        //沒有通過舊的混淆文件處理,則直接從混淆池中獲取一個混淆字符串
        if (!keepMapping) {
            replaceString = mProguardBuilder.getReplaceString();
        }

        /*設置混淆池中對應資源項id的位置爲「已混淆」的標記。*/
        mProguardBuilder.setInReplaceList(mCurEntryID, true);
        if (replaceString == null) {
            throw new AndrolibException("readEntry replaceString == null");
        }
        //根據新的混淆字符串,生成相應的id映射。
        generalResIDMapping(mPkg.getName(), mType.getName(), mSpecNames.get(specNamesId).toString(), replaceString);

        //如下對混淆字符串進行相應對象的緩存。
        mPkg.putSpecNamesReplace(mResId, replaceString);
        mPkg.putSpecNamesblock(replaceString);
        mType.putSpecProguardName(replaceString);
    }
}  

/*根據新的混淆字符串,生成相應的id映射。輸出到新的混淆mapping文件中(裏面已經文件file的映射關係)。*/
generalResIDMapping(){
    mMappingWriter.write("    " + packagename + ".R." + typename + "." + specname + " -> " + packagename + ".R." + typename + "." + replace);
}

readEntry函數主要實現了:
其1、判斷是否啓用whitelist,若是有的話,設置specname的混淆字符串爲原始字符串,即不進行混淆,進行相應對象緩存。
其2、判斷是否複用舊的mapping文件中id的映射,已有的繼續使用舊的映射關係中的混淆字符串,不然從混淆池中獲取一個新的字符串,即獲得replaceString。
其3、根據新的混淆字符串,生成相應的id映射。輸出到新的混淆mapping文件中(裏面已經文件file的映射關係)。

readEntry繼續解析arsc文件,執行到關鍵步驟readValue:

十一、readValue()

readValue() {
    //這裏面有幾個限制,一對於string ,id, array咱們是知道確定不用改的,第二看要那個type是否對應有文件路徑
    if (mPkg.isCanProguard() && flags && type == TypedValue.TYPE_STRING && mShouldProguardForType && mShouldProguardTypeSet.contains(mType.getName())) {
        //mTableStringsProguard是要存放混淆的資源項值
        if (mTableStringsProguard.get(data) == null) {
            String raw = mTableStrings.get(data).toString();//mTableStrings是解析原始arsc文件獲得資源項值字符串池
            String proguard = mPkg.getSpecRepplace(mResId);//獲取前面已緩存下的specName對應的混淆字符串
            //這個要寫死這個,由於resources.arsc裏面就是用這個"/"
            int secondSlash = raw.lastIndexOf("/");
            ...
            String newFilePath = raw.substring(0, secondSlash);//得到原始資源項值的path部分

            if (!mApkDecoder.getConfig().mKeepRoot) {
                //如在(「res/drawable「,」r/c」)中找到newFilePath=」r/c」
                newFilePath = mOldFileName.get(raw.substring(0, secondSlash));//mOldFileName是已生成的混淆文件映射
            }
            ...
            //同理這裏不能用File.separator,由於resources.arsc裏面就是用這個

            /***********************
            *結果result如」r/c/a」
            ************************/
            String result = newFilePath + "/" + proguard; 
            ...
            String compatibaleraw = new String(raw);
            String compatibaleresult = new String(result);

            //爲了適配window要作一次轉換
            if (!File.separator.contains("/")) {
                compatibaleresult = compatibaleresult.replace("/", File.separator);
                compatibaleraw = compatibaleraw.replace("/", File.separator);
            }

            //下面很關鍵,建立了原始res文件和混淆後的res文件
            File resRawFile = new File(mApkDecoder.getOutTempDir().getAbsolutePath() + File.separator + compatibaleraw);
            File resDestFile = new File(mApkDecoder.getOutDir().getAbsolutePath() + File.separator + compatibaleresult);

            //這裏用的是linux的分隔符
            HashMap<String, Integer> compressData = mApkDecoder.getCompressData();
            if (compressData.containsKey(raw)) {
                compressData.put(result, compressData.get(raw));//替換壓縮的文件名爲混淆後的字符串
            } else {
                System.err.printf("can not find the compress dataresFile=%s\n", raw);
            }

            if (!resRawFile.exists()) {
                System.err.printf("can not find res file, you delete it? path: resFile=%s\n", resRawFile.getAbsolutePath());
                return;
            } else {
                if (resDestFile.exists()) {
                    throw new AndrolibException(
                        String.format("res dest file is already  found: destFile=%s", resDestFile.getAbsolutePath())
                    );
                }
                /**************************************************************
                *關鍵點:把舊的資源文件內容copy到新的混淆後的資源文件中
                **************************************************************/
                FileOperation.copyFileUsingStream(resRawFile, resDestFile);
                //already copied
                //從原始資源目錄mRawResourceFiles中刪除掉該已混淆的文件Path
                mApkDecoder.removeCopiedResFile(resRawFile.toPath());

                /**********************
                *按照data的index順序,保存resutl(result如」r/c/a」),
                *即把混淆後的資源項的值緩存下來
                **********************/
                mTableStringsProguard.put(data, result);
            }
        }
    }
}

readValue主要實現了:
其1、mPkg.getSpecRepplace獲取前面已緩存下的specName對應的混淆字符串如「a」
其2、從mOldFileName中如在(「res/drawable「,」r/c」)中找到newFilePath=」r/c」
其3、生成result如」r/c/a」
其4、建立了混淆後的res文件,把舊的資源文件內容copy到新的混淆後的資源文件中。
其5、從原始資源目錄mRawResourceFiles中刪除掉該已混淆的文件Path
其6、按照Value的index順序,保存result(如」r/c/a」),即把混淆後的資源項的值緩存下來

下面回到第4步中,繼續執行copyOtherResFiles():

十二、 copyOtherResFiles()

copyOtherResFiles(){
    ...
    Path resPath = mRawResFile.toPath();
    Path destPath = mOutResFile.toPath();

    //mRawResourceFiles中是剩下的
    for (Path path : mRawResourceFiles) {
        //copy文件內容到dest中
        FileOperation.copyFileUsingStream(path.toFile(), dest.toFile());

    }
}

該函數主要實現了把沒有紀錄在resources.arsc的資源文件也拷進dest目錄。

回到第4步中,繼續執行ARSCDecoder.write():

1三、ARSCDecoder.write()

write(){
    ARSCDecoder writer = new ARSCDecoder(arscStream, decoder, pkgs);
    writer.writeTable();
}

writeTable(){
    System.out.printf("writing new resources.arsc \n");
    mTableLenghtChange = 0;
    writeNextChunkCheck(Header.TYPE_TABLE, 0);
    int packageCount = mIn.readInt();
    mOut.writeInt(packageCount);

    //mTableStringsProguard就是上面產生的已混淆的資源項值的字符串池
    mTableLenghtChange += StringBlock.writeTableNameStringBlock(mIn, mOut, mTableStringsProguard);
    ...
    for (int i = 0; i < packageCount; i++) {
        mCurPackageID = i;
        writePackage();
    }
    //最後須要把整個的size重寫回去
    reWriteTable();
}


writePackage(){
    checkChunkType(Header.TYPE_PACKAGE);
    int id = (byte) mIn.readInt();
    mOut.writeInt(id);
    mResId = id << 24;
    //char_16的,一共256byte
    mOut.writeBytes(mIn, 256);
    /* typeNameStrings */
    mOut.writeInt(mIn.readInt());
    /* typeNameCount */
    mOut.writeInt(mIn.readInt());
    /* specNameStrings */
    mOut.writeInt(mIn.readInt());
    /* specNameCount */
    mOut.writeInt(mIn.readInt());
    StringBlock.writeAll(mIn, mOut);

    if (mPkgs[mCurPackageID].isCanProguard()) {
        //writeSpecNameStringBlock把混淆後specname從新寫入arsc文件
        //其中mCurSpecNameToPos是混淆的specname對應位置
        int specSizeChange = StringBlock.writeSpecNameStringBlock(
            mIn,
            mOut,
            mPkgs[mCurPackageID].getSpecNamesBlock(),
            mCurSpecNameToPos
        );
        mPkgsLenghtChange[mCurPackageID] += specSizeChange;
        mTableLenghtChange += specSizeChange;//從新記錄大小
    } else {
        StringBlock.writeAll(mIn, mOut);
    }
    writeNextChunk(0);
    while (mHeader.type == Header.TYPE_LIBRARY) {
        writeLibraryType();
    }
    while (mHeader.type == Header.TYPE_SPEC_TYPE) {
        writeTableTypeSpec();
    }
}

/**
*修改混淆資源項specname對應位置
*/
writeEntry(){
        /* size */
        mOut.writeBytes(mIn, 2);
        short flags = mIn.readShort();
        mOut.writeShort(flags);
        int specNamesId = mIn.readInt();
        ResPackage pkg = mPkgs[mCurPackageID];
        if (pkg.isCanProguard()) {

            //獲取資源項specname對應位置
            specNamesId = mCurSpecNameToPos.get(pkg.getSpecRepplace(mResId));
            if (specNamesId < 0) {
                throw new AndrolibException(String.format(
                    "writeEntry new specNamesId < 0 %d", specNamesId));
            }
        }
        //重寫位置
        mOut.writeInt(specNamesId);

        if ((flags & ENTRY_FLAG_COMPLEX) == 0) {
            writeValue();
        } else {
            writeComplexEntry();
        }
    }

這一步一樣是解析resource.arsc,從新修改arsc文件其中幾個字符串池和對應大小,造成新的arsc文件。主要包括:
其1、資源項值字符串池修改,咱們須要把文件指向路徑改變,例如res/layout/test.xml,改成res/layout/a.xml
其2、資源項key池修改,即specsname除了白名單部分所有廢棄,替換成全部咱們混淆方案中用到的字符。
其3、每一個資源項entry中指向的specsname中的id修正。因爲specname已混淆,咱們須要用混淆後的資源項specname的位置改寫。

回到最開始第2步中,執行 buildApk(decoder, apkFile, signatureType);

1四、buildApk()
從新打包生成新的apk並簽名等,這一步再也不贅述。

以上完成了對apk資源混淆的過程分析。

總結:

資源混淆核心處理過程以下:
一、生成新的資源文件目錄,裏面對資源文件路徑進行混淆(其中涉及如何複用舊的mapping文件),例如將res/drawable/hello.png混淆爲r/s/a.png,並將映射關係輸出到mapping文件中。
二、對資源id進行混淆(其中涉及如何複用舊的mapping文件),並將映射關係輸出到mapping文件中。
三、生成新的resources.arsc文件,裏面對資源項值字符串池、資源項key字符串池、進行混淆替換,對資源項entry中引用的資源項字符串池位置進行修正、並更改相應大小,並打包生成新的apk。

原文連接https://blog.csdn.net/cg_wang/article/details/70183864
阿里P7移動互聯網架構師進階視頻(每日更新中)免費學習請點擊:https://space.bilibili.com/474380680

相關文章
相關標籤/搜索