标签:
在前文中,我们知道,如果需要生成补丁.patch文件需要借助apkpatch ,在本章节我们分析下该工具的内部原理。
apkpatch 是一个jar包,并没有开源出来,但是我们可以用 JD-G UI 或者 procyon 来看下它的 源码 ,版本1.0.3。
重要:
位于 com.euler.patch 包下,找到 Main() 方法
public static void main(String[] args) {
CommandLineParser parser = new PosixParser();
CommandLine commandLine = null;
option();
try {
commandLine = parser.parse(allOptions, args);
} catch (ParseException e) {
System.err.println(e.getMessage());
usage(commandLine);
return;
}
//*************************1 start******************************
if ((!commandLine.hasOption(‘k‘)) && (!commandLine.hasOption("keystore"))) {
usage(commandLine);
return;
}
if ((!commandLine.hasOption(‘p‘)) && (!commandLine.hasOption("kpassword"))) {
usage(commandLine);
return;
}
if ((!commandLine.hasOption(‘a‘)) && (!commandLine.hasOption("alias"))) {
usage(commandLine);
return;
}
if ((!commandLine.hasOption(‘e‘)) && (!commandLine.hasOption("epassword"))) {
usage(commandLine);
return;
}
File out = null;
if ((!commandLine.hasOption(‘o‘)) && (!commandLine.hasOption("out")))
out = new File("");
else {
out = new File(commandLine.getOptionValue(‘o‘));
}
//***********************1 End********************************
//***********************2 start********************************
String keystore = commandLine.getOptionValue(‘k‘);
String password = commandLine.getOptionValue(‘p‘);
String alias = commandLine.getOptionValue(‘a‘);
String entry = commandLine.getOptionValue(‘e‘);
String name = "main";
if ((commandLine.hasOption(‘n‘)) || (commandLine.hasOption("name")))
{
name = commandLine.getOptionValue(‘n‘);
}
if ((commandLine.hasOption(‘m‘)) || (commandLine.hasOption("merge"))) {
String[] merges = commandLine.getOptionValues(‘m‘);
File[] files = new File[merges.length];
for (int i = 0; i < merges.length; i++) {
files[i] = new File(merges[i]);
}
MergePatch mergePatch = new MergePatch(files, name, out, keystore,
password, alias, entry);
mergePatch.doMerge();
} else {
if ((!commandLine.hasOption(‘f‘)) && (!commandLine.hasOption("from"))) {
usage(commandLine);
return;
}
if ((!commandLine.hasOption(‘t‘)) && (!commandLine.hasOption("to"))) {
usage(commandLine);
return;
}
File from = new File(commandLine.getOptionValue("f"));
File to = new File(commandLine.getOptionValue(‘t‘));
if ((!commandLine.hasOption(‘n‘)) && (!commandLine.hasOption("name"))) {
name = from.getName().split("\\.")[0];
}
//***********************2 End********************************
//***********************3 start********************************
ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore,
password, alias, entry);
//***********************3 End********************************
apkPatch.doPatch();
}
}
我们前面介绍如何使用命令行打补丁包的命令,检查命令行是否有那些参数,如果没有要求的参数,就给用户相应的提示
我们在打正式包的时候,会指定keystore,password,alias,entry相关参数。另外name就是最后生成的文件,可以忽略
上面的参数传给ApkPatch进行初始化,调用其构造函数
final ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, password, alias, entry);
调用ApkPatch的doPatch()方法。
apkPatch.doPatch();
public ApkPatch(File from, File to, String name, File out, String keystore, String password, String alias, String entry)
{
super(name, out, keystore, password, alias, entry);
this.from = from;
this.to = to;
}
调用了父类Build的构造函数,干的事情其实比较简单,就是给变量进行赋值。可以看到out,我们的输出文件就是这么来的,没有的话,它会自己创建一个。
protected static final String SUFFIX = ".apatch";
protected String name;
private String keystore;
private String password;
private String alias;
private String entry;
protected File out;
public Build(String name, File out, String keystore, String password, String alias, String entry)
{
this.name = name;
this.out = out;
this.keystore = keystore;
this.password = password;
this.alias = alias;
this.entry = entry;
if (!out.exists())
out.mkdirs();
else if (!out.isDirectory())
throw new RuntimeException("output path must be directory.");
}
可以简单描述为两步:
public void doPatch() {
try {
//生成smali文件夹
final File smaliDir = new File(this.out, "smali");
if (!smaliDir.exists()) {
smaliDir.mkdir();
}
//新建diff.dex文件
final File dexFile = new File(this.out, "diff.dex");
//新建diff.apatch文件
final File outFile = new File(this.out, "diff.apatch");
//第一步,拿到两个apk文件对比,对比信息写入DiffInfo
final DiffInfo info = new DexDiffer().diff(this.from, this.to);
//第二步,将对比结果info写入.smali文件中,然后打包成dex文件
this.classes = buildCode(smaliDir, dexFile, info);
//第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件
this.build(outFile, dexFile);
//第四步,将diff.apatch文件重命名,结束
this.release(this.out, dexFile, outFile);
}
catch (Exception e2) {
e2.printStackTrace();
}
}
public DiffInfo diff(final File newFile, final File oldFile) throws IOException {
//提取新apk的dex文件
final DexBackedDexFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);
//提取旧apk的dex文件
final DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);
final DiffInfo info = DiffInfo.getInstance();
boolean contains = false;
for (final DexBackedClassDef newClazz : newDexFile.getClasses()) {//一层for循环,新的所有类
final Set<? extends DexBackedClassDef> oldclasses = oldDexFile.getClasses();
for (final DexBackedClassDef oldClazz : oldclasses) {//二层for循环,旧的所有类
//对比相同名的类,存储为修改的方法
if (newClazz.equals(oldClazz)) {
//对比class文件的变量
this.compareField(newClazz, oldClazz, info);
//对比class文件的方法
this.compareMethod(newClazz, oldClazz, info);
contains = true;
break;
}
}
if (!contains) {
//否则是新增的方法
info.addAddedClasses(newClazz);
}
}
//返回包含diff信息的DiffInfo对象
return info;
}
就是提取 dex 文件的地方,在 DexFileFactory 类中
可以看到,只提取出了 classes.dex 这个文件,所以源生工具并 不支持multidex ,如果使用了 multidex 方案,并且修复的类不在同一个 dex 文件中,那么补丁就不会生效。所以这里并不像作者在issue中提到的支持 multidex 那样,不过我们可以通过 JavaAssist 工具 修改 apkpatch 这个jar包,来达到支持multidex的目的
public static DexBackedDexFile loadDexFile(File dexFile, int api, boolean experimental) throws IOException
{
return loadDexFile(dexFile, "classes.dex", new Opcodes(api, experimental));
}
AndFix 不支持增加成员变量,但是支持在新增方法中增加的局部变量 。 也不支持修改成员变量
public void compareField(DexBackedField object, Iterable<? extends DexBackedField> olds, DiffInfo info)
{
for (DexBackedField reference : olds) {
if (reference.equals(object)) {
if ((reference.getInitialValue() == null) &&
(object.getInitialValue() != null)) {
info.addModifiedFields(object);
return;
}
if ((reference.getInitialValue() != null) &&
(object.getInitialValue() == null)) {
info.addModifiedFields(object);
return;
}
if ((reference.getInitialValue() == null) &&
(object.getInitialValue() == null)) {
return;
}
if (reference.getInitialValue().compareTo(
object.getInitialValue()) != 0) {
info.addModifiedFields(object);
return;
}
return;
}
}
info.addAddedFields(object);
}
对比方法过程中对比两个 dex 文件中同时存在的方法,如果方法实现不同则 存储为修改过的方法 ;如果方法名不同, 存储为新增的方法 ,也就是说 AndFix支持增加新的方法 ,这一点已经测试证明
public void compareMethod(DexBackedMethod object, Iterable<? extends DexBackedMethod> olds, DiffInfo info)
{
for (DexBackedMethod reference : olds) {
if (reference.equals(object))
{
if ((reference.getImplementation() == null) &&
(object.getImplementation() != null)) {
info.addModifiedMethods(object);
return;
}
if ((reference.getImplementation() != null) &&
(object.getImplementation() == null)) {
info.addModifiedMethods(object);
return;
}
if ((reference.getImplementation() == null) &&
(object.getImplementation() == null)) {
return;
}
if (!reference.getImplementation().equals(
object.getImplementation())) {
info.addModifiedMethods(object);
return;
}
return;
}
}
info.addAddedMethods(object);
}
将上一步得到的 diff 信息写入 smali 文件,并且生成 diff.dex 文件。 smali 文件的命名以 _CF.smali 结尾,并且在修改的地方用自定义的 Annotation ( MethodReplace )标注,用于在替换之前查找修复的变量或方法
private static Set<String> buildCode(final File smaliDir, final File dexFile, final DiffInfo info) throws IOException, RecognitionException, FileNotFoundException {
final ClassFileNameHandler outFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");
final ClassFileNameHandler inFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");
final DexBuilder dexBuilder = DexBuilder.makeDexBuilder();
for (final DexBackedClassDef classDef : list) {
final String className = classDef.getType();
baksmali.disassembleClass(classDef, outFileNameHandler, options);
final File smaliFile = inFileNameHandler.getUniqueFilenameForClass(TypeGenUtil.newType(className));
classes.add(TypeGenUtil.newType(className).substring(1, TypeGenUtil.newType(className).length() - 1).replace(‘/‘, ‘.‘));
SmaliMod.assembleSmaliFile(smaliFile, dexBuilder, true, true);
}
dexBuilder.writeTo(new FileDataStore(dexFile));
return classes;
}
protected void build(File outFile, File dexFile)
throws KeyStoreException, FileNotFoundException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableEntryException
{
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore.PrivateKeyEntry privateKeyEntry = null;
InputStream is = new FileInputStream(this.keystore);
keyStore.load(is, this.password.toCharArray());
privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(this.alias,
new KeyStore.PasswordProtection(this.entry.toCharArray()));
PatchBuilder builder = new PatchBuilder(outFile, dexFile,
privateKeyEntry, System.out);
builder.writeMeta(getMeta());
builder.sealPatch();
}
protected abstract Manifest getMeta();
protected Manifest getMeta()
{
Manifest manifest = new Manifest();
Attributes main = manifest.getMainAttributes();
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (ApkPatch)");
main.putValue("Created-Time",
new Date(System.currentTimeMillis()).toGMTString());
main.putValue("From-File", this.from.getName());
main.putValue("To-File", this.to.getName());
main.putValue("Patch-Name", this.name);
main.putValue("Patch-Classes", Formater.dotStringList(this.classes));
return manifest;
}
将dexFile进行md5加密,把build(outFile, dexFile);函数中生成的outFile重命名。哈哈,看到”.patch”有没有很激动!!我们的补丁包一开始的命名就是一长串。好了,到这里,补丁文件就生成了
protected void release(File outDir, File dexFile, File outFile)
throws NoSuchAlgorithmException, FileNotFoundException, IOException
{
MessageDigest messageDigest = MessageDigest.getInstance("md5");
FileInputStream fileInputStream = new FileInputStream(dexFile);
byte[] buffer = new byte[8192];
int len = 0;
while ((len = fileInputStream.read(buffer)) > 0) {
messageDigest.update(buffer, 0, len);
}
String md5 = HexUtil.hex(messageDigest.digest());
fileInputStream.close();
outFile.renameTo(new File(outDir, this.name + "-" + md5 + ".apatch"));
}
(4.2.32.5)android热修复之Andfix方式:Andfix的补丁生成方法分析
标签:
原文地址:http://blog.csdn.net/fei20121106/article/details/51892358