AndResGuard 源码解析

背景

抖音包体积优化提出的“键常量池裁剪”是基于腾讯的AndResGuard资源混淆之后做的进一步处理,也就是对resources.arsc文件的处理。而资源混淆,就是对resources.arsc文件进行修改。那么我们可以尝试基于这个思路,对AndResGuard插件源码进行解析,获悉他对resources.arsc文件的处理详情。

入口

在这里插入图片描述

下载AndResGuard源码后,项目结构如上。作为一个gradle插件,打开他的入口AndResGuardPlugin,首先会执行的是apply方法

class AndResGuardPlugin implements Plugin<Project> {

  public static final String USE_APK_TASK_NAME = "UseApk"

  @Override
  void apply(Project project) {
    ...

    project.afterEvaluate {
      ...
      createTask(project, USE_APK_TASK_NAME)
      ...
    }
  }

  private static void createTask(Project project, variantName) {
    def taskName = "resguard${variantName}"
    if (project.tasks.findByPath(taskName) == null) {
      def task = project.task(taskName, type: AndResGuardTask)
      if (variantName != USE_APK_TASK_NAME) {
        task.dependsOn "assemble${variantName}"
      }
    }
  }
}

此处创建了AndResGuardTask,并在打包后执行

assemble:打包命令,具体详情可以参考 https://www.jianshu.com/p/db62617cbbff

那么接下来解析AndResGuardTask类,他是一个task,那么直接看run方法

  run() {
    ...

    buildConfigs.each { config ->
      ...
        RunGradleTask(config, config.file.getAbsolutePath(), config.minSDKVersion, config.targetSDKVersion)
      ...
    }
  }

此处执行了RunGradleTask函数

  def RunGradleTask(config, String absPath, int minSDKVersion, int targetSDKVersion) {
    ...
    configuration.whiteList.each { res ->
      if (res.startsWith("R")) {
        whiteListFullName.add(packageName + "." + res)
      } else {
        whiteListFullName.add(res)
      }
    }

    InputParam.Builder builder = new InputParam.Builder()
        .setMappingFile(configuration.mappingFile)
        .setWhiteList(whiteListFullName)
        .setUse7zip(configuration.use7zip)
        .setMetaName(configuration.metaName)
        .setFixedResName(configuration.fixedResName)
        .setKeepRoot(configuration.keepRoot)
        .setMergeDuplicatedRes(configuration.mergeDuplicatedRes)
        .setCompressFilePattern(configuration.compressFilePattern)
        .setZipAlign(getZipAlignPath())
        .setSevenZipPath(sevenzip.path)
        .setOutBuilder(useFolder(config.file))
        .setApkPath(absPath)
        .setUseSign(configuration.useSign)
        .setDigestAlg(configuration.digestalg)
        .setMinSDKVersion(minSDKVersion)
        .setTargetSDKVersion(targetSDKVersion)

    if (configuration.finalApkBackupPath != null && configuration.finalApkBackupPath.length() > 0) {
      builder.setFinalApkBackupPath(configuration.finalApkBackupPath)
    } else {
      builder.setFinalApkBackupPath(absPath)
    }

    if (configuration.useSign) {
      if (signConfig == null) {
        throw new GradleException("can't the get signConfig for release build")
      }
      builder.setSignFile(signConfig.storeFile)
          .setKeypass(signConfig.keyPassword)
          .setStorealias(signConfig.keyAlias)
          .setStorepass(signConfig.storePassword)
      if (signConfig.hasProperty('v3SigningEnabled') && signConfig.v3SigningEnabled) {
        builder.setSignatureType(InputParam.SignatureType.SchemaV3)
      } else if (signConfig.hasProperty('v2SigningEnabled') && signConfig.v2SigningEnabled) {
        builder.setSignatureType(InputParam.SignatureType.SchemaV2)
      }
    }
    InputParam inputParam = builder.create()
    Main.gradleRun(inputParam)
  }

这里主要做了两件事

  • 获取构建参数,也就是在使用gradle时,注册的那些参数
  • 调用Main.gradleRun,这是一个java函数,也就是说具体的逻辑最终都是用java实现的

实现

进入Main类,解析他的gradleRun函数

public class Main {

  ...

  private void run(InputParam inputParam) {
    ...
      resourceProguard(
          new File(inputParam.outFolder),
          finalApkFile,
          inputParam.apkPath,
          inputParam.signatureType,
          inputParam.minSDKVersion
      );
    ...
  }

  protected void resourceProguard(
      File outputDir, File outputFile, String apkFilePath, InputParam.SignatureType signatureType, int minSDKVersoin) {
    ...
    try {
      ApkDecoder decoder = new ApkDecoder(config, apkFile);
      /* 默认使用V1签名 */
      decodeResource(outputDir, decoder, apkFile);
      buildApk(decoder, apkFile, outputFile, signatureType, minSDKVersoin);
    } catch (Exception e) {
      e.printStackTrace();
      goToError();
    }
  }

  private void decodeResource(File outputFile, ApkDecoder decoder, File apkFile)
      throws AndrolibException, IOException, DirectoryException {
    if (outputFile == null) {
      mOutDir = new File(mRunningLocation, apkFile.getName().substring(0, apkFile.getName().indexOf(".apk")));
    } else {
      mOutDir = outputFile;
    }
    decoder.setOutDir(mOutDir.getAbsoluteFile());
    decoder.decode();
  }

  private void buildApk(
      ApkDecoder decoder, File apkFile, File outputFile, InputParam.SignatureType signatureType, int minSDKVersion)
      throws Exception {
			...
    }
  }

  protected void goToError() {
    System.exit(ERRNO_USAGE);
  }
}

跟着方法读取gradleRun==>run==>resourceProguard==>decodeResource & buildApk

此处做了两件事

  • 解析APK资源,并对文件进行修改 - decodeResource
  • 重新构建APK - buildApk

解析修改APK资源

读到decodeResource方法,可知他是交给了ApkDecoder#decode方法,代码如下

  public void decode() throws AndrolibException, IOException, DirectoryException {
    if (hasResources()) {
      ensureFilePath();
      // read the resources.arsc checking for STORED vs DEFLATE compression
      // this will determine whether we compress on rebuild or not.
      System.out.printf("decoding resources.arsc\n");
      RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"));
      ResPackage[] pkgs = ARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"), this);

      //把没有纪录在resources.arsc的资源文件也拷进dest目录
      copyOtherResFiles();

      ARSCDecoder.write(apkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
    }
  }

此处总共执行了四件事

  • 确定文件路径,解压APK - ensureFilePath
  • 第一次解析resources.arsc文件,读取资源文件信息并保存 - RawARSCDecoder.decode
  • 第二次解析resources.arsc文件,进行混淆 - ARSCDecoder.decode
  • 重新生成resources.arsc - ARSCDecoder.write
确定文件信息

跟踪 ensureFilePath 可得

  private void ensureFilePath() throws IOException {
    Utils.cleanDir(mOutDir);

    String unZipDest = new File(mOutDir, TypedValue.UNZIP_FILE_PATH).getAbsolutePath();
    System.out.printf("unziping apk to %s\n", unZipDest);
    mCompressData = FileOperation.unZipAPk(apkFile.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");
    }

    //这个需要混淆各个文件夹
    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());

    if (!mRawResFile.exists() || !mRawResFile.isDirectory()) {
      throw new IOException("can not found res dir in the apk or it is not a dir");
    }

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

    String basename = apkFile.getName().substring(0, apkFile.getName().indexOf(".apk"));
    mResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath()
                               + File.separator
                               + TypedValue.RES_MAPPING_FILE
                               + basename
                               + TypedValue.TXT_FILE);
    mMergeDuplicatedResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath()
                             + File.separator
                             + TypedValue.MERGE_DUPLICATED_RES_MAPPING_FILE
                             + basename
                             + TypedValue.TXT_FILE);
  }

此处总共执行了这几件事

  • 解压APK - FileOperation.unZipAPk
  • 压缩APK资源 - dealWithCompressConfig

APK中很多资源是以stored方式存储的,这些资源都是没被压缩的,通过修改他的压缩方式达到压缩的目的

在使用AndResGuard时通过compressFilePattern参数配置

未被压缩的资源包含如下

static const char* kNoCompressExt[] = {
 ".jpg", ".jpeg", ".png", ".gif",
 ".wav", ".mp2", ".mp3", ".ogg", ".aac",
 ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
 ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
 ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
 ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
}
  • 将res混淆成r
  • 纪录原始res目录的文件
  • 创建新的resources.arsc输出文件和mapping文件
第一次解析文件,存储原资源信息
RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"))

可见此处解析的是apk的resources.arsc文件,进入该方法

  public static ResPackage[] decode(InputStream arscStream) throws AndrolibException {
    try {
      RawARSCDecoder decoder = new RawARSCDecoder(arscStream);
      System.out.printf("parse to get the exist names in the resouces.arsc first\n");
      return decoder.readTable();
    } catch (IOException ex) {
      throw new AndrolibException("Could not decode arsc file", ex);
    }
  }

继续看decoder.readTable()方法

  private ResPackage[] readTable() throws IOException, AndrolibException {
    nextChunkCheckType(Header.TYPE_TABLE);
    int packageCount = mIn.readInt();
    StringBlock.read(mIn);
    ResPackage[] packages = new ResPackage[packageCount];
    nextChunk();
    for (int i = 0; i < packageCount; i++) {
      packages[i] = readTablePackage();
    }
    return packages;
  }

读到readTablePackage方法

  private ResPackage readTablePackage() throws IOException, AndrolibException {
    ...
    while (mHeader.type == Header.TYPE_LIBRARY) {
      readLibraryType();
    }
    while (mHeader.type == Header.TYPE_SPEC_TYPE) {
      readTableTypeSpec();
    }
    ...
  }

看到此处有readLibraryType和readTableTypeSpec方法,我们先看readLibraryType方法

  private void readLibraryType() throws AndrolibException, IOException {
    ...
    while (mHeader.type == Header.TYPE_TYPE) {
      readTableTypeSpec();
    }
  }

此处调用了readTableTypeSpec方法,所以直接读readTableTypeSpec方法即可

  private void readTableTypeSpec() throws AndrolibException, IOException {
    ...
    while (mHeader.type == Header.TYPE_TYPE) {
      readConfig();
      nextChunk();
    }
  }

继续readConfig方法分析

  private void readConfig() throws IOException, AndrolibException {
    ...
    int[] entryOffsets = mIn.readIntArray(entryCount);
    for (int i = 0; i < entryOffsets.length; i++) {
      if (entryOffsets[i] != -1) {
        mResId = (mResId & 0xffff0000) | i;
        readEntry();
      }
    }
  }

readEntry

   */
  private void readEntry() throws IOException, AndrolibException {
    /* size */
    mIn.skipBytes(2);
    short flags = mIn.readShort();
    int specNamesId = mIn.readInt();
    putTypeSpecNameStrings(mCurTypeID, mSpecNames.getString(specNamesId));
    boolean readDirect = false;
    if ((flags & ENTRY_FLAG_COMPLEX) == 0) {
      readDirect = true;
      readValue(readDirect, specNamesId);
    } else {
      readDirect = false;
      readComplexEntry(readDirect, specNamesId);
    }
  }

  private void putTypeSpecNameStrings(int type, String name) {
    Set<String> names = mExistTypeNames.get(type);
    if (names == null) {
      names = new HashSet<>();
    }
    names.add(name);
    mExistTypeNames.put(type, names);
  }
  • 将资源类型的名称存在mExistTypeNames里,key为资源类型,value为名称集合 - putTypeSpecNameStrings
  • 避免混淆后的名称与混淆前的名称出现相同的情况
  • 需要防止由于某些非常恶心的白名单,导致出现重复id
第二次解析,进行混淆处理
ResPackage[] pkgs = ARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"), this);

分析这个方法,进入ARSCDecoder.decode

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

  private ResPackage[] readTable() throws IOException, AndrolibException {
    nextChunkCheckType(Header.TYPE_TABLE);
    int packageCount = mIn.readInt();
    mTableStrings = StringBlock.read(mIn);
    ResPackage[] packages = new ResPackage[packageCount];
    nextChunk();
    for (int i = 0; i < packageCount; i++) {
      packages[i] = readPackage();
    }
    mMappingWriter.close();
   ...
    mMergeDuplicatedResMappingWriter.close();
   ...
    return packages;
  }
  • 这里实际上是读取resource.arsc的package部分
  • mMappingWriter - mapping文件写入类
  • mMergeDuplicatedResMappingWriter - 合并旧mapping文件写入类

主要的逻辑在readPackage

  private ResPackage readPackage() throws IOException, AndrolibException {
    ...
    mPkg = new ResPackage(id, name);
    // 系统包名不混淆
    if (mPkg.getName().equals("android")) {
      mPkg.setCanResguard(false);
    } else {
      mPkg.setCanResguard(true);
    }
    nextChunk();
    while (mHeader.type == Header.TYPE_LIBRARY) {
      readLibraryType();
    }
    while (mHeader.type == Header.TYPE_SPEC_TYPE) {
      readTableTypeSpec();
    }
    return mPkg;
  }
  • 这里有个处理,就是判断系统包名不混淆
  • 然后是核心的readLibraryType和readTableTypeSpec方法,readLibraryType里面调用的也是readTableTypeSpec方法,所以此处直接看readTableTypeSpec方法
  private void readTableTypeSpec() throws AndrolibException, IOException {
    ...
    // first meet a type of resource
    if (mCurrTypeID != id) {
      mCurrTypeID = id;
      initResGuardBuild(mCurrTypeID);
    }
    // 是否混淆文件路径
    mShouldResguardForType = isToResguardFile(mTypeNames.getString(id - 1));

    // 对,这里是用来描述差异性的!!!
    mIn.skipBytes(entryCount * 4);
    mResId = (0xff000000 & mResId) | id << 16;

    while (nextChunk().type == Header.TYPE_TYPE) {
      readConfig();
    }
  }
  • 在白名单里的不需要混淆 - initResGuardBuild
  • 某些文件路径不需要混淆,比如string,array - isToResguardFile
  • 进行混淆处理 - readConfig

看下 initResGuardBuild 方法

  private void initResGuardBuild(int resTypeId) {
    // we need remove string from resguard candidate list if it exists in white list
    HashSet<Pattern> whiteListPatterns = getWhiteList(mType.getName());
    // init resguard builder
    mResguardBuilder.reset(whiteListPatterns);
    mResguardBuilder.removeStrings(RawARSCDecoder.getExistTypeSpecNameStrings(resTypeId));
    // 如果是保持mapping的话,需要去掉某部分已经用过的mapping
    reduceFromOldMappingFile();
  }

  /**
   * 如果是保持mapping的话,需要去掉某部分已经用过的mapping
   */
  private void reduceFromOldMappingFile() {
    if (mPkg.isCanResguard()) {
      if (mApkDecoder.getConfig().mUseKeepMapping) {
        // 判断是否走keepmapping
        HashMap<String, HashMap<String, HashMap<String, String>>> resMapping = mApkDecoder.getConfig().mOldResMapping;
        String packName = mPkg.getName();
        if (resMapping.containsKey(packName)) {
          HashMap<String, HashMap<String, String>> typeMaps = resMapping.get(packName);
          String typeName = mType.getName();

          if (typeMaps.containsKey(typeName)) {
            HashMap<String, String> proguard = typeMaps.get(typeName);
            // 去掉所有之前保留的命名,为了简单操作,mapping里面有的都去掉
            mResguardBuilder.removeStrings(proguard.values());
          }
        }
      }
    }
  }

此处主要做了两件事

  • 白名单不混淆
  • 如果保持之前的mapping的话,需要去掉这些已经用掉的mapping,新的文件用新的mapping

接着看isToResguardFile方法

  /**
   * 为了加速,不需要处理string,id,array,这几个是肯定不是的
   */
  private boolean isToResguardFile(String name) {
    return (!name.equals("string") && !name.equals("id") && !name.equals("array"));
  }

看注释,基本上已经可以明白他的用途

接着看 readConfig 方法

  private void readConfig() throws IOException, AndrolibException {
    ...
    int[] entryOffsets = mIn.readIntArray(entryCount);
    for (int i = 0; i < entryOffsets.length; i++) {
      mCurEntryID = i;
      if (entryOffsets[i] != -1) {
        mResId = (mResId & 0xffff0000) | i;
        readEntry();
      }
    }
  }


  private void readEntry() throws IOException, AndrolibException {
    mIn.skipBytes(2);
    short flags = mIn.readShort();
    int specNamesId = mIn.readInt();

    if (mPkg.isCanResguard()) {
      // 混淆过或者已经添加到白名单的都不需要再处理了
      if (!mResguardBuilder.isReplaced(mCurEntryID) && !mResguardBuilder.isInWhiteList(mCurEntryID)) {
        Configuration config = mApkDecoder.getConfig();
        boolean isWhiteList = false;
        if (config.mUseWhiteList) {
          isWhiteList = dealWithWhiteList(specNamesId, config);
        }

        if (!isWhiteList) {
          dealWithNonWhiteList(specNamesId, config);
        }
      }
    }

    if ((flags & ENTRY_FLAG_COMPLEX) == 0) {
      readValue(true, specNamesId);
    } else {
      readComplexEntry(false, specNamesId);
    }
  }

通过以上代码可知

  • 已经混淆过或者已经添加到白名单的都不需要再处理了 - dealWithWhiteList
  • 具体的混淆操作 - dealWithNonWhiteList

接着看dealWithNonWhiteList方法

  private void dealWithNonWhiteList(int specNamesId, Configuration config) throws AndrolibException, IOException {
    String replaceString = null;
    boolean keepMapping = false;
    if (config.mUseKeepMapping) {
      String packName = mPkg.getName();
      if (config.mOldResMapping.containsKey(packName)) {
        HashMap<String, HashMap<String, String>> typeMaps = config.mOldResMapping.get(packName);
        String typeName = mType.getName();
        if (typeMaps.containsKey(typeName)) {
          HashMap<String, String> nameMap = typeMaps.get(typeName);
          String specName = mSpecNames.get(specNamesId).toString();
          if (nameMap.containsKey(specName)) {
            keepMapping = true;
            replaceString = nameMap.get(specName);
          }
        }
      }
    }

    if (!keepMapping) {
      replaceString = mResguardBuilder.getReplaceString();
    }

    mResguardBuilder.setInReplaceList(mCurEntryID);
    if (replaceString == null) {
      throw new AndrolibException("readEntry replaceString == null");
    }
    generalResIDMapping(mPkg.getName(), mType.getName(), mSpecNames.get(specNamesId).toString(), replaceString);
    mPkg.putSpecNamesReplace(mResId, replaceString);
    // arsc name列混淆成固定名字, 减少string pool大小
    boolean useFixedName = config.mFixedResName != null && config.mFixedResName.length() > 0;
    String fixedName = useFixedName ? config.mFixedResName : replaceString;
    mPkg.putSpecNamesblock(fixedName, replaceString);
    mType.putSpecResguardName(replaceString);
  }

  • config.mUseKeepMapping - 如果设置了config.mUseKeepMapping为true,就用老的mapping文件混淆
  • 新的或者没设置mUseKeepMapping,就通过mResguardBuilder.getReplaceString()取,它实际上是组装的混淆字符串数组
 public String getReplaceString() throws AndrolibException {
   if (mReplaceStringBuffer.isEmpty()) {
     throw new AndrolibException(String.format("now can only proguard less than 35594 in a single type\n"));
   }
   return mReplaceStringBuffer.remove(0);
 }

可以看到他取的是mReplaceStringBuffer,取出后移除该元素,防止重复。

mReplaceStringBuffer是一个集合,它的组装如下

 private String[] mAToZ = {
    "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
    "w", "x", "y", "z"
 };
 private String[] mAToAll = {
    "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
    "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
 };
 /**
     * 在window上面有些关键字是不能作为文件名的
     * CON, PRN, AUX, CLOCK$, NUL
     * COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9
     * LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9.
     */
    private HashSet<String> mFileNameBlackList;

    public ResguardStringBuilder() {
      mFileNameBlackList = new HashSet<>();
      mFileNameBlackList.add("con");
      mFileNameBlackList.add("prn");
      mFileNameBlackList.add("aux");
      mFileNameBlackList.add("nul");
      mReplaceStringBuffer = new ArrayList<>();
      mIsReplaced = new HashSet<>();
      mIsWhiteList = new HashSet<>();
    }

    public void reset(HashSet<Pattern> blacklistPatterns) {
      mReplaceStringBuffer.clear();
      mIsReplaced.clear();
      mIsWhiteList.clear();

      for (int i = 0; i < mAToZ.length; i++) {
        String str = mAToZ[i];
        if (!Utils.match(str, blacklistPatterns)) {
          mReplaceStringBuffer.add(str);
        }
      }

      for (int i = 0; i < mAToZ.length; i++) {
        String first = mAToZ[i];
        for (int j = 0; j < mAToAll.length; j++) {
          String str = first + mAToAll[j];
          if (!Utils.match(str, blacklistPatterns)) {
            mReplaceStringBuffer.add(str);
          }
        }
      }

      for (int i = 0; i < mAToZ.length; i++) {
        String first = mAToZ[i];
        for (int j = 0; j < mAToAll.length; j++) {
          String second = mAToAll[j];
          for (int k = 0; k < mAToAll.length; k++) {
            String third = mAToAll[k];
            String str = first + second + third;
            if (!mFileNameBlackList.contains(str) && !Utils.match(str, blacklistPatterns)) {
              mReplaceStringBuffer.add(str);
            }
          }
        }
      }
    }
  • 替换后的id放到mIsReplaced set中
  • 写入mapping文件 - generalResIDMapping
  • 设置替换后的名称 - putSpecNamesReplace

继续读之后的readValue方法

/**
   * @param flags whether read direct
   */
  private void readValue(boolean flags, int specNamesId) throws IOException, AndrolibException {
   ...

    //这里面有几个限制,一对于string ,id, array我们是知道肯定不用改的,第二看要那个type是否对应有文件路径
    if (mPkg.isCanResguard()
       && flags
       && type == TypedValue.TYPE_STRING
       && mShouldResguardForType
       && mShouldResguardTypeSet.contains(mType.getName())) {
      if (mTableStringsResguard.get(data) == null) {
        ...

        File resRawFile = new File(mApkDecoder.getOutTempDir().getAbsolutePath() + File.separator + compatibaleraw);
        File resDestFile = new File(mApkDecoder.getOutDir().getAbsolutePath() + File.separator + compatibaleresult);

        MergeDuplicatedResInfo filterInfo = null;
        boolean mergeDuplicatedRes = mApkDecoder.getConfig().mMergeDuplicatedRes;
        if (mergeDuplicatedRes) {
          filterInfo = mergeDuplicated(resRawFile, resDestFile, compatibaleraw, result);
          if (filterInfo != null) {
            resDestFile = new File(filterInfo.filePath);
            result = filterInfo.fileName;
          }
        }

       ...

        if (!resRawFile.exists()) {
          System.err.printf("can not find res file, you delete it? path: resFile=%s\n", resRawFile.getAbsolutePath());
        } else {
          ...
          if (filterInfo == null) {
            FileOperation.copyFileUsingStream(resRawFile, resDestFile);
          }
          //already copied
          mApkDecoder.removeCopiedResFile(resRawFile.toPath());
          mTableStringsResguard.put(data, result);
        }
      }
    }
  }

  • resRawFile - 原始文件
  • resDestFile - 混淆后的文件
  • mergeDuplicated - 资源过滤,过滤重复资源,减少apk体积
  • copyFileUsingStream - 讲原文件内容复制给混淆后的文件
  • mTableStringsResguard - 混淆后的全局经放到mTableStringsResguard字典里
重新生成resources.arsc
ARSCDecoder.write(apkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);

接着分析此方法

  public static void write(InputStream arscStream, ApkDecoder decoder, ResPackage[] pkgs) throws AndrolibException {
    ...
    for (int i = 0; i < packageCount; i++) {
      mCurPackageID = i;
      writePackage();
    }
    // 最后需要把整个的size重写回去
    reWriteTable();
  }

  private void writePackage() throws IOException, AndrolibException {
   ...

    if (mPkgs[mCurPackageID].isCanResguard()) {
      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();
    }
  }

  private void writeTableTypeSpec() throws AndrolibException, IOException {
    ...
    while (writeNextChunk(0).type == Header.TYPE_TYPE) {
      writeConfig();
    }
  }

  private void writeConfig() throws IOException, AndrolibException {
    ...

    for (int i = 0; i < entryOffsets.length; i++) {
      if (entryOffsets[i] != -1) {
        mResId = (mResId & 0xffff0000) | i;
        writeEntry();
      }
    }
  }

  private void writeEntry() throws IOException, AndrolibException {
    ...
    if (pkg.isCanResguard()) {
      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();
    }
  }

  private void writeValue() throws IOException, AndrolibException {
    /* size */
    mOut.writeCheckShort(mIn.readShort(), (short) 8);
    /* zero */
    mOut.writeCheckByte(mIn.readByte(), (byte) 0);
    byte type = mIn.readByte();
    mOut.writeByte(type);
    int data = mIn.readInt();
    mOut.writeInt(data);
  }
  • writeTableNameStringBlock - 重写全局字符串池,计算混淆后全局字符串池长度与混淆前的差值。后面 reWriteTable() 方法会用到 mTableLenghtChange
  • 重写package
  • 最后需要把整个的size重写回去

可以看到此处重写了resources.arsc 文件,如果想按照自己的方式构建此文件,在此处可以尝试添加代码

重新构建APK

  private void buildApk(
      ApkDecoder decoder, File apkFile, File outputFile, InputParam.SignatureType signatureType, int minSDKVersion)
      throws Exception {
    ResourceApkBuilder builder = new ResourceApkBuilder(config);
    String apkBasename = apkFile.getName();
    apkBasename = apkBasename.substring(0, apkBasename.indexOf(".apk"));
    builder.setOutDir(mOutDir, apkBasename, outputFile);
    System.out.printf("[AndResGuard] buildApk signatureType: %s\n", signatureType);
    switch (signatureType) {
      case SchemaV1:
        builder.buildApkWithV1sign(decoder.getCompressData());
        break;
      case SchemaV2:
      case SchemaV3:
        builder.buildApkWithV2V3Sign(decoder.getCompressData(), minSDKVersion, signatureType);
        break;
    }
  }

此处简单分析,根据不同签名方式进行打包操作

总结

整个资源混淆流程如下

  1. 解压APK,混淆res目录为r
  2. 第一次解析resources.arsc,保存原来的资源信息,为mapping文件做准备
  3. 第二次解析resources.arsc,生成混淆信息
  4. 重新生成resources.arsc
  5. 重新打包成APK

**从以上流程可知,如果需要对键常量池进行裁剪,可以尝试在第4步进行操作

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Kotlin中进行App瘦身可以采取以下几种方法: 1. 使用ProGuard进行代码混淆和优化。ProGuard是一个Java字节码优化器,可以删除未使用的代码和资源,减小应用的体积。在Kotlin项目中,可以通过在build.gradle文件中配置ProGuard规则来启用它。例如: ```kotlin android { buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } ``` 2. 使用R8进行代码压缩。R8是Google推出的一种代码压缩工具,可以在编译时删除未使用的代码和资源,减小应用的体积。在Kotlin项目中,默认情况下,R8已经启用,无需额外配置。 3. 优化资源文件。可以通过使用WebP格式替换PNG格式的图片,使用Vector Drawable替换多个分辨率的位图,以及压缩和优化其他资源文件来减小应用的体积。 4. 使用动态特性模块化。将应用的功能模块化,只在需要的时候下载和安装相应的模块,可以减小应用的初始安装包体积。 5. 使用APK分包。将应用的代码和资源分成多个APK文件,按需下载和安装,可以减小应用的初始安装包体积。 6. 使用资源压缩工具。可以使用工具如AndResGuard对资源文件进行压缩和优化,减小应用的体积。 7. 使用动态加载技术。将一部分代码和资源放在服务器上,通过动态加载的方式在运行时下载和加载,可以减小应用的初始安装包体积。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值