安卓经常需要打多个渠道包,当二次打包时,资源ID会重新生成。如果代码中有第三方SDK通过直接引用R文件的方式来获取资源ID,就会出现资源ID不匹配的问题。
本文主要介绍解决此类问题的三种方法。
一 背景
为什么要二次打包
大家都知道,国内安卓渠道众多,游戏想要上架渠道就要接入他们的sdk。这对于游戏开发商(CP)来说是一个不小的工作量。
通过接入我们的聚合SDK,CP只需要提供一个母包,然后使用我们的打包工具就可以打出几十个渠道包,非常的高效。
二次打包的基本原理
打包工具的基本原理就是通过反编译,把SDK的代码和资源文件打入到游戏母包,然后重新打包签名,生成对应的渠道包。
当然这其中会涉及到许多细节方面的东西,不是本文重点,就不展开了。
二次打包之资源文件ID
打包时,会生成两个文件。
一个是resources.arsc.里面包含了所有资源文件的索引ID
示例:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public type="drawable" name="btn_login" id="2130837507" />
<public type="layout" name="paydialog" id="2130903044" />
</resources>
另外一个是R文件,里面提供了代码中引用的name和resources.arsc中对应的ID
示例:
public final class R {
public static final class drawable {
public static final int btn_login = 2130837507;
}
public static final class layout {
public static final int paydialog = 2130903049;
}
}
这样我们通过引用R文件就可以找到对应的资源。
但是,正如前面所说,resources.arsc和R.java是打包的时候才会生成。因此,二次打包会重新生成资源ID,如果我们通过直接引用R文件的方式来寻找资源,就会出现问题。
二 解决方法
1. 常规操作:动态获取资源ID
这是最简单最通用的解决方法。
比如说我们可以通过如下方式获取一个布局文件的ID:
public static int getLayoutId(Context context,String paramString) {
mContext = context;
return getResourceId("layout", paramString);
}
protected static int getResourceId(String paramString1, String paramString2) {
return mContext.getResources().getIdentifier(paramString2, paramString1,
mContext.getPackageName());
}
2. 奇技淫巧:替换掉第三方SDK中R文件的引用
可是,如果第三方SDK就是通过R文件直接引用资源ID怎么办?
比如说,他们引用了com.corpA.sdk.R,重新打包之后,会在我们的包名(com.corpB.demo)文件夹下生成新的R文件。因此,我们可以让SDK直接引用我们包名文件夹下的R文件。
具体咋做呢?
2.1 SDK代码反编译为smali文件
使用apktool将第三方SDK的所有java代码反编译为smali文件。
2.2 查找替换所有R文件的引用
将smali文件中所有com/corpA/sdk/R替换为com/corp/demo/R。这样,重新打包后,第三方SDK在运行时会去我们包名下最新的R文件中寻找资源ID
这种方法简单粗暴,不需要手动生成R文件,也不需要更改SDK中原来的R文件,直接通过打包脚本在反编译阶段改掉第三方SDK的代码。
3. 曲线救国:直接修改第三方SDK中引用的R文件
凡是总有例外,不是所有的SDK都能反编译的。比如有些SDK会将Java代码打成一个加密的Dex文件,放到asset文件夹下面。这种加密的Dex文件我们是无法反编译的,所以上面的方法2就行不通了。
由于无法反编译,这个Dex里面的内容对于我们就是一个黑盒,我们无法知道它里面是否有直接引用R文件的情况,以及引用了哪里的R文件。
这个其实没有很好地办法,只能先按照方法2来处理掉可以反编译的Java代码,然后打出一个包来进行测试。如果加密Dex中有直接引用到R文件,那么就会出现崩溃,从日志中我们就可以找到引用R文件的位置。然后我们就可以修改指定位置的R文件啦。
过程有些繁琐,下面详细说明:
3.1 找到引用R文件的位置
通过崩溃日志,我们可以找到Dex文件中所引用的R文件的位置,假设该R文件的路径是com.corpA.sdk.R
3.2 使用aapt手动生成R文件
aapt(Android Asset Packaging Tool)是安卓sdk中负责将资源文件和代码进行打包的工具。基本上所有的打包工具都是在aapt的基础上进行封装和修改的,打包的过程比较繁琐,总结下来大概有如下几个步骤:
- 通过aapt工具,生成R.java和.arsc文件
- 通过aidl工具,把aidl文件打包成java文件
- 通过javac工具,将Java文件编译成.class文件
- 通过dx工具,把class文件和第三方jar打包成dex文件
- 通过apkbuilder工具,把dex和资源文件打包成apk文件
- 通过JarSinger工具,对apk进行签名
- 通过zipAlign工具,对apk做对齐处理
我们要改的就是第一步:aapt生成R文件!步骤如下
反编译游戏母包和接入渠道SDK的插件包,将代码和资源文件合并
修改AndroidManifest.xml中的包名为最终渠道包的包名
-
使用Android sdk的aapt工具手动生成R文件
前提是需要首先安装Andorid sdk相应的工具,并配置好环境变量。aapt指令比较复杂,核心就是下面这个:
os.getenv('ANDROID_BUILD_TOOL') + "/aapt package -f -m -J " + temp_path + " -S " + res_path + " -I " + os.getenv('ANDROID_PLATFORM') + "/android.jar -M " + manifest_path
其中temp_path是生成的R文件输出路径;res_path是res文件夹的路径;manifest_path是manifest的路径
3.3 将R文件转换为Smali文件
这个过程大概4个步骤: java --> class --> jar --> dex --> smali
示例:
# 1. build_r_class
r_java_path = temp_path + os.sep + package_name.replace('.', os.sep) + os.sep + "R.java"
cmd_build_r_class = "javac -source 1.6 -target 1.6 " + r_java_path
execCmd(cmd_build_r_class)
file_util.deleteFile(r_java_path)
# 2. generate jar
os.chdir("/data/soft/jenkins/workspace/Packaging_Tools-All" + os.sep + temp_path)
cmd_generate_r_jar = "jar cvf " + "r.jar " + "com"
execCmd(cmd_generate_r_jar)
# 3. generate dex
cmd_generate_dex = os.getenv('ANDROID_BUILD_TOOL') + "/dx --dex --output=" + "r.dex " + "r.jar"
execCmd(cmd_generate_dex)
# 4. generate smali
cmd_generate_smali = "java -jar " + "/data/soft/jenkins/workspace/Packaging_Tools-All" + os.sep + "baksmali-2.0.jar " + "r.dex"
execCmd(cmd_generate_smali)
3.4 拷贝smali文件到对应位置
我们要拷贝一份smali文件到包名下对应目录。
然后同样拷贝到3.1步骤中R文件的路径(com.corpA.sdk.R)。同时,我们需要修改smali中R文件的引用为com.corpA.sdk.R。这样才能保持更Asset下面的Dex文件中的引用一致。
至此,关于R文件的处理已经完成,然后重新打包就可以啦~