(4.2.32.5)android热修复之Andfix方式:Andfix的补丁生成方法分析
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了(4.2.32.5)android热修复之Andfix方式:Andfix的补丁生成方法分析,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含11559字,纯文字阅读大概需要17分钟。
内容图文
在前文中,我们知道,如果需要生成补丁.patch文件需要借助apkpatch ,在本章节我们分析下该工具的内部原理。
apkpatch 是一个jar包,并没有开源出来,但是我们可以用 JD-G UI 或者 procyon 来看下它的 源码 ,版本1.0.3。
重要:
- 只提取出了 classes.dex 这个文件,所以源生工具并 不支持multidex ,如果使用了 multidex 方案,并且修复的类不在同一个 dex 文件中,那么补丁就不会生效。所以这里并不像作者在issue中提到的支持 multidex 那样,不过我们可以通过 JavaAssist 工具 修改 apkpatch 这个jar包,来达到支持multidex的目的
- AndFix 不支持增加成员变量,但是支持在新增方法中增加的局部变量 。 也不支持修改成员变量
- 对比方法过程中对比两个 dex 文件中同时存在的方法,如果方法实现不同则 存储为修改过的方法 ;如果方法名不同, 存储为新增的方法 ,也就是说 AndFix支持增加新的方法 ,这一点已经测试证明
1-首先找到 Main.class
位于 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();
}
}
1.1 第一部分
我们前面介绍如何使用命令行打补丁包的命令,检查命令行是否有那些参数,如果没有要求的参数,就给用户相应的提示
1.2 第二部分
我们在打正式包的时候,会指定keystore,password,alias,entry相关参数。另外name就是最后生成的文件,可以忽略
1.3 第三部分
上面的参数传给ApkPatch进行初始化,调用其构造函数
final ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, password, alias, entry);
1.4 第四部分
调用ApkPatch的doPatch()方法。
apkPatch.doPatch();
2-ApkPatch
2.1 构造函数
public ApkPatch(File from, Fileto, String name, Fileout, 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;
publicBuild(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();
elseif (!out.isDirectory())
thrownew RuntimeException("output path must be directory.");
}
2.2 doPatch 方法
可以简单描述为两步:
- 对比apk文件,得到需要的信息
- 将结果打包为apatch文件
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文件对比,对比信息写入DiffInfofinal 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();
}
}
2.2.1 第一步,对比并记录差异:DexDiffer().diff() 方法返回DiffInfo差异对象
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;
}
2.2.1.1提取dex
就是提取 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));
}
2.2.1.2 对比变量compareField
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);
}
2.2.1.3 对比方法compareMethod
对比方法过程中对比两个 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);
}
2.2.2 第二步,将对比结果info写入.smali文件中,然后打包成dex文件:buildCode()
将上一步得到的 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;
}
2.2.3 第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件:build(outFile, dexFile)
- 首先从keystone里面获取应用相关签名
- 实例化PatchBuilder,然后调用writeMeta(getMeta())
- 将getMeta()中获取的Manifest内容写入”META-INF/PATCH.MF”文件中。
- sealPatch——从input输入流中读取buffer数据然后写入到entry。然后联系到我上面提到的将dexfile和签名相关信息写入到classes.dex里面
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();
}
protectedabstract Manifest getMeta();
2.2.3.1 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;
}
2.2.4 第四步,dex转.apatch
将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 = newbyte[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"));
}
原文:http://blog.csdn.net/fei20121106/article/details/51892358
内容总结
以上是互联网集市为您收集整理的(4.2.32.5)android热修复之Andfix方式:Andfix的补丁生成方法分析全部内容,希望文章能够帮你解决(4.2.32.5)android热修复之Andfix方式:Andfix的补丁生成方法分析所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。