AndResGuard 源码解析

news2025/1/11 16:59:58

背景

抖音包体积优化提出的“键常量池裁剪”是基于腾讯的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步进行操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/558516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

推荐一款好用的Idea热部署插件

目录 1.什么是热部署 2.为什么需要热部署 3.热部署产品 3.1.JRebel 3.2.IDEA HotSwap 3.3.HotSwapAgent 3.4.Spring Boot DevTools 3.5.FastHotSwapper 4.FastHotSwapper安装使用 参考&#xff1a; 1.什么是热部署 热部署&#xff08;Hot Deployment&#xff09;是指…

chatgpt赋能Python-python_iloc_loc

使用Python中的iloc和loc方法对数据进行索引 如果你正在使用Python来进行数据处理或者数据分析&#xff0c;那么你肯定会遇到需要对数据进行索引的情况。Python提供了两个非常有用的方法——iloc和loc&#xff0c;对于数据的索引和切片操作非常实用。在本文中&#xff0c;我们…

yolov5-7.0 添加BiFPN

1. BiFPN特征融合 BiFPN是目标检测中神经网络架构设计的选择之一&#xff0c;为了优化目标检测性能而提出。主要用来进行多尺度特征融合&#xff0c;对神经网络性能进行优化。来自EfficientDet: Scalable and Efficient Object Detection这篇论文。 在这篇论文中&#xff0c;作…

算法27:从暴力递归到动态规划(2)

上一题比较简单&#xff0c;下面来一道比较难的题目。 假设有排成一行的N个位置&#xff0c;记为1~N&#xff0c;N 一定大于或等于 2 开始时机器人在其中的M位置上(M 一定是 1~N 中的一个) 如果机器人来到1位置&#xff0c;那么下一步只能往右来到2位置&#xff1b; 如果机…

初级程序员如何快速晋升为技术大牛

[请搜索公众号“云智AI助手”、“云智博瑞”关注我们 │ 谢谢支持 ] Cloud wisdom, AI assistant 作为初级程序员&#xff0c;你是否常常遇到代码优Bug调试的难题&#xff1f;幸运的是&#xff0c;ChatGPT可以助你一臂之力。本文将通过多个实例展示&#xff0c;如何借ChatGPT的…

【微信支付】分享一个失败的项目

这个项目是去年做的&#xff0c;开始客户还在推广&#xff0c;几个月后发现服务器已经关掉了。这是一个发图片猜谜语的应用&#xff0c;用户猜对了分红包&#xff0c;所得奖金可以提现。开发的时候对需求都不太看好&#xff0c;觉得用户粘性太低了。今天就把所有的程序拿了出来…

[抢先看] 全平台数据 (数据库) 管理工具 DataCap 1.10.0

推荐一个基于 SpringBoot 开发的全平台数据 (数据库管理工具) 功能比较完善&#xff0c;建议下载使用: github.com/EdurtIO/datacap 目前已经支持 40 多种数据源。国内首个应用 ChatGPT 到数据管理系统中项目。 在 DataCap v1.10.0 中我们主要核心修改了数据编辑器&#xff0c;…

多线程处理有序集合

文章目录 前言一、多线程处理有序集合&#xff1f;总结 前言 通过多线程,处理数据是一个快速提高处理的手段,那么当用多线程处理的时候,如果遇到有序集合怎么办?例如: 我想爬取一本小说,那么爬取完成后,需要的是 一个有序的章节小说,而非混乱的 章节,如何做呢? 一、多线程处…

.Net8顶级技术:边界检查之IR解析(慎入)

前言 C#这种语言之所以号称安全的&#xff0c;面向对象的语言。这个安全两个字可不是瞎叫的哦。因为JIT会检查任何可能超出分配范围的数值&#xff0c;以便使其保持在安全边界内。这里有两个概念&#xff0c;其一边界检查&#xff0c;其二IR解析。后者的生成是前者的功能的保证…

音视频使用qt测试ffmpeg接口时无法运行

仅仅时把自己过程中遇到的稍微阻塞疑惑问题做出整理&#xff0c;疑惑的是拿到的ffmpeg包中没有dll文件&#xff0c;导致自己研究了一系列。 使用qt加载音视频ffmpeg对应的相关lib库&#xff0c;进行接口&#xff0c;源码的研究。 1&#xff1a;使用源码安装的方式获取相关的动…

【蓝桥杯省赛真题40】Scratch报数游戏 蓝桥杯少儿编程scratch图形化编程 蓝桥杯省赛真题讲解

目录 scratch报数游戏 一、题目要求 编程实现 二、案例分析 1、角色分析

OKR是什么意思啊

一、OKR是什么意思&#xff1f; OKR是"Objective and Key Results"的缩写&#xff0c;即目标和关键结果。它是一种目标管理框架&#xff0c;旨在帮助组织和团队设定明确的目标&#xff0c;并通过关键结果来衡量和追踪目标的实现情况。 为了让大家快速了解什么是OKR…

基于变长频带选择的JPEG图像可逆数据隐藏-文献学习

论文学习 原文题目&#xff1a; Reversible Data Hiding of JPEG Image Based on Adaptive Frequency Band Length 发表期刊&#xff1a; TCSVT 2023&#xff08;中科院1区&#xff09; 作者&#xff1a; Ningxiong Mao, Hongjie He, Fan Chen, Yuan Yuan, Lingfeng Qu 摘要 J…

SolVES模型应用(基于多源环境QGIS\PostgreSQL\ARCGIS\MAXENT\R语言支持下模型应用)

生态系统服务是人类从自然界中获得的直接或间接惠益&#xff0c;可分为供给服务、文化服务、调节服务和支持服务4类&#xff0c;对提升人类福祉具有重大意义&#xff0c;且被视为连接社会与生态系统的桥梁。自从启动千年生态系统评估项目&#xff08;Millennium Ecosystem Asse…

又双叒叕入选!腾讯安全NDR连续四年获Gartner认可

近日&#xff0c;全球权威研究机构 Gartner发布了2023年《Emerging Tech: Security — Adoption Growth Insights for Network Detection and Response》&#xff08;《新兴技术&#xff1a;安全-网络检测与响应的采用增长洞察》&#xff09;&#xff0c;腾讯安全连续四年被列为…

内卷把同事逼成了“扫地僧”,把Git上所有面试题整理成足足24W字测试八股文

互联网大厂更多的是看重学历还是技术&#xff1f; 毫无疑问&#xff0c;是技术&#xff0c;技术水平相近的情况下&#xff0c;肯定学历高/好的会优先一点&#xff0c;这点大家肯定都理解。 说实话&#xff0c;学弟学妹们找工作难&#xff0c;作为面试官招人也难呀&#xff01…

使用laf云开发三分钟上线你自己的Midjourney

文章尾部有demo 江湖惯例&#xff1a;先来一波感谢&#xff0c;感谢laf&#xff0c;让我们可以不使用魔法、免费接入Midjourney&#xff0c;不了解laf的请猛戳 Laf介绍 一、写这篇博客的背景 laf官方最近发布了一个活动&#xff0c;活动链接&#xff0c;新手也可以接入哦&…

云渲染平台为什么越来越多的效果图公司开始使用?

随着3dmax版本的不断更迭&#xff0c;包括常用的V-Ray渲染器和Corona渲染器的不断更新&#xff0c;室内设计行业对于 效果图的渲染要求越来越高。而要求更高的渲染精度和更真实的渲染效果&#xff0c;所需要付出的代价则是不断增长的参数&#xff0c;这会使渲染一张效果图的时间…

chatgpt赋能Python-python_lamba

Python Lambda: 什么是Lambda表达式&#xff1f; Python是一种强大的编程语言&#xff0c;它支持许多编程范式&#xff0c;包括函数式编程。Lambda表达式是函数式编程的一个重要概念。许多人对Lambda表达式感到困惑&#xff0c;认为它们很难理解。本文将介绍Python的Lambda表达…

如何对Windows剪切板里的内容进行取证分析 Windows剪切板取证

前言 无论是在现实中对设备进行取证分析&#xff0c;还是在ctf中做取证类的题目&#xff0c;剪切板里的内容都需要去查看&#xff0c;以免遗漏什么重要信息 剪切板位置 剪切板是计算机操作系统提供的一个临时存储区域&#xff0c;用于在不同应用程序之间复制和粘贴文本、图像…