我们接着分析阿里开源的AndFix库,上次留下了三个坑,一个方法,两个类,不知道你们是否想急切了解呢? loadPatch()方法和AndFixManagerPatch类。

分析loadPatch()方法的时候离不开AndFixManager这个类,所以,我会在分析loadPatch()方法的时候分析AndFixManager这个类。 Patch类相当于一个容器,把修复bug所需的信息放在其中,Patch类相对来说比较独立,不需要牵扯到另外两个坑,所以,就先把这个坑埋了。

要分析Patch类,就不能不分析阿里提供的打包.apatch的工具apkpatch-1.0.2.jarPatch获取的信息其实就是apkpatch打包时放入其中的信息。 我下面对apkpatch.jar的分析以这个版本的源码为准。

分析源码,那些try-catch-finally和逻辑无关,所以,我会把这些代码从源码中删掉的,除非有提示用户的操作

我们先来看看Patch

class Patch

从前一篇的分析中,我们看到调用了Patch的构造函数,那我们就从构造函数开始看。

Patch Constructor

public Patch(File file) throws IOException {this.mFile = file;this.init();
}

Patch init()

将传入的文件保存在类变量中,并调用init()函数,那么init()函数干什么呢? 代码里异常和清理的代码我给删掉了,毕竟,这和我们对源码的分析关系不大,那么我们看一下剩下的代码

public void init(){JarFile jarFile = new JarFile(this.mFile);JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);InputStream inputStream = jarFile.getInputStream(entry);Manifest manifest = new Manifest(inputStream);Attributes main = manifest.getMainAttributes();this.mName = main.getValue(PATCH_NAME);this.mTime = new Date(main.getValue(CREATED_TIME));this.mClassesMap = new HashMap();Iterator it = main.keySet().iterator();while(it.hasNext()) {Name attrName = (Name)it.next();String name = attrName.toString();if(name.endsWith(CLASSES)) {List strings = Arrays.asList(main.getValue(attrName).split(","));if(name.equalsIgnoreCase(PATCH_CLASSES)) {this.mClassesMap.put(this.mName, strings);} else {this.mClassesMap.put(name.trim().substring(0, name.length() - 8), strings);}}}
}

先分析上面的代码,通过JarFile, JarEntry, Manifest, Attributes一层层的获取jar文件中值,并放入对应的类变量中 这些值是从哪里来的呢?是从生成这个.apatch文件的时候写入的,即apkpatch.jar文件中写入的。 虽然这个文件后缀名是apatch,不是jar,但是它生成的时候是使用Attributes,Manifest,jarEntry将数据写入的, 其实依旧是jar格式,修改了扩展名而已

上面的那些常量都是什么呢,我们来看看这个类的类变量,就是一些对应的字符串。

private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
private final File mFile;
private String mName;
private Date mTime;
private Map<String, List<String>> mClassesMap;

看完了Patch类,是不是还一头雾水呢, 其他的好理解, mClassesMap里面的各种类名是从哪里来,里面的类作用又是什么呢? 这里,我们就需要进入apkpatch.jar一探究竟。

apkpatch.jar

对于Jar文件,我们可以使用jd-gui文件打开,阿里这里并没有对这个jar文件加密,所以我们可以直接看,对于Jar文件,我们从Main()方法开始看起。 在package com.euler.patch;里面,有一个Main类,其中有那个我们常见的main()方法,那么我们就进入看一看。 代码比较长,我们一段一段来看

Main Main()

//CommandLineParser,CommandLine, Option, Options,OptionBuilder,  PosixParser等等类都是
//org.apache.commons.cli这个包中的类,负责解析命令行传给程序的参数,所以下面的很多内容应该就不用我过多解释了
CommandLineParser parser = new PosixParser();
CommandLine commandLine = null;
//option()方法就是将命令行需要解析的参数加入其中,有三种解析方式
//private static final Options allOptions = new Options();
//private static final Options patchOptions = new Options();
//private static final Options mergeOptions = new Options();
option();
try {commandLine = parser.parse(allOptions, args);
} catch (ParseException e) {System.err.println(e.getMessage());//提示用户如何使用这个jar包usage(commandLine);return;
}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;
}

这一部分主要是查看命令行是否有那些参数,如果没有要求的参数,则提示用户需要哪些参数。

接下来注意一下你下载apkpatch这个文件的时间,根据github的历史提交记录,如果你2015年10月以后下载的,可以忽视掉这个提醒,如果你是10月之前下载的那个工具,建议你更新一下这个工具,否则可能会导致-o所在的文件夹被清空。

好了,提醒完了,我们继续

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');
}

对于我们的release版应用来说,我们会指定storeFile, storePassword, keyAlias, keyPassword, 这些即是上面keystore, password, alias, entry对应的值。至于name,这是最后生成的文件的名称,基本可以不用管。

下面,我们看一下main()函数中最后的一个if-else

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];
}

在此,我选择不分析MergePatch这个部分,毕竟,你只有先生成了,后面才可能需要做merge操作。 from指修复bug后的apk,to指之前有bug的apk。所以这部分的分析就结束了。

main()函数里最后一部分内容

ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore,password, alias, entry);
apkPatch.doPatch();

将那些参数传给ApkPatch去初始化,然后调用doPatch()方法

ApkPatch

ApkPatch Constructor

我们一步步来看,首先是调用构造函数

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

Build Constructor

public class ApkPatch extends Build ApkPatch的函数头,说明它是Build类的子类

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.");try{FileUtils.cleanDirectory(out);} catch (IOException e) {throw new RuntimeException(e);}
}

在这里,看到了我之前提到的-o操作的问题,会清空那个文件夹内的内容。

好了,我们接下来就可以看看最后一个方法,doPatch()方法了。

ApkPatch doPatch()
public void doPatch() {File smaliDir = new File(this.out, "smali");smaliDir.mkdir();File dexFile = new File(this.out, "diff.dex");File outFile = new File(this.out, "diff.apatch");//DiffInfo是一个容器类,主要保存了新加的和修改的 类,方法和字段。DiffInfo info = new DexDiffer().diff(this.from, this.to);this.classes = buildCode(smaliDir, dexFile, info);build(outFile, dexFile);release(this.out, dexFile, outFile);
}

在out文件夹内生成了一个smali文件夹,还有diff.dexdiff.apatch文件。 看到diff()方法,应该能想到就是比较两个文件的不同,所以DiffInfo就是储存两个文件不同的一个容器类, 由于篇幅原因,这里就不深入其中了,有兴趣的同学可以深入其中看一下。

但是,在这个diff()方法中,有一个重要的问题需要大家注意,就是其中只针对classes.dex做了diff,如果你使用了Google的Multidex,那么结果就是你其它dex文件中的任何bug,依旧无法修复,因为这个生成的DiffInfo中没有其它dex的信息,这个时候就需要大家使用JavaAssist之类的工具修改阿里的这个jar文件,然后达到你自己的修复非classes.dex文件中bug的目的,问题出在DexFileFactory.class类中。

接下来,我们可以看到调用了三个方法,buildCode(smaliDir, dexFile, info);build(outFile, dexFile);release(this.out, dexFile, outFile); 一个个的跟进去看一看。

ApkPatch buildCode(smaliDir, dexFile, info)
private static Set<String> buildCode(File smaliDir, File dexFile, DiffInfo info)throws IOException, RecognitionException, FileNotFoundException{Set classes = new HashSet();Set list = new HashSet();//从这里可以看出,list保存了DiffInfo容器中的新添加的classes和被修改过的classeslist.addAll(info.getAddedClasses());list.addAll(info.getModifiedClasses());baksmaliOptions options = new baksmaliOptions();options.deodex = false;options.noParameterRegisters = false;options.useLocalsDirective = true;options.useSequentialLabels = true;options.outputDebugInfo = true;options.addCodeOffsets = false;options.jobs = -1;options.noAccessorComments = false;options.registerInfo = 0;options.ignoreErrors = false;options.inlineResolver = null;options.checkPackagePrivateAccess = false;if (!options.noAccessorComments) {options.syntheticAccessorResolver = new SyntheticAccessorResolver(list);}ClassFileNameHandler outFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");ClassFileNameHandler inFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");DexBuilder dexBuilder = DexBuilder.makeDexBuilder();for (DexBackedClassDef classDef : list) {String className = classDef.getType();//将相关的类信息写入outFileNameHandlerbaksmali.disassembleClass(classDef, outFileNameHandler, options);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;
}

看到baksmali,反编译过apk的同学一定不陌生,这就是dex的打包工具,还有对应的解包工具smali,就到这里,这方面不继续深入了。 如果想深入了解dex打包解包工具的源码,参见This Blog

可以看到,这个方法的返回值将DiffInfo中新添加的classes和修改过的classes做了一个重命名,然后保存了起来,同时,将相关内容写入smali文件中。 这个重命名是一个怎样的重命名呢,看一下生成的smali文件夹里任意一个smali文件,我就拿Demo里的MainActivity.smali来说明 这个类在文件中的类名是Lcom/euler/andfix/MainActivity;,看到这个名字,再看下面这个方法就很清晰了

classes.add(TypeGenUtil.newType(className)
.substring(1, TypeGenUtil.newType(className).length() - 1)
.replace('/', '.'));

就是把Lcom/euler/andfix/MainActivity;替换成com.euler.andfix.MainActivity_CF; 那个_CF哪里来的呢,就是那个TypeGenUtil类做的操作了。 这个类就一个方法,该方法进对String进行了操作,我们来看一看源码

public static String newType(String type){return type.substring(0, type.length() - 1) + "_CF;";
}

可以看到,去掉了类名多余的那个’;’,然后在后面加了个_CF后缀。重命名应该是为了不合之前安装的dex文件的名字冲突。

(new FileDataStore(file) 作用就是清空file里的内容) 最后,将dexFile文件清空,把dexBuilder的内容写入其中。

到这里,buildCode(smaliDir, dexFile, info)方法就结束了, 看看下一个方法build(outFile, dexFile);

ApkPatch build(outFile, dexFile)

上一个方法已经把内容填充到dexFile内了,我们来看看它的源码。

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);//将getMeta()中获取的Manifest内容写入"META-INF/PATCH.MF"文件中builder.writeMeta(getMeta());//单纯调用了close()命令, 将异常处理放在子函数中。//这里做了一个异常分层,即不同抽象层次的子函数需要处理的异常不一样//具体请参阅《Code Complete》(代码大全)在恰当的抽象层次抛出异常builder.sealPatch();
}

要打包了,打包完是要签名的,所以需要获取签名的相关信息,这一部分就不详细讲解了。 这里提一下getMeta()函数,因为开头我们提到的Patch init() 函数内 这句List strings = Arrays.asList(main.getValue(attrName).split(","));中的那个strings就是从这里写入的

 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;
}

那个classes就是我们那个将类名修改后保存起来的SetFormater.dotStringList(this.classes));这个方法就是遍历Set,并将其中用','作为分隔符,将整个Set中保存的String拼接 所以在Patch init()方法内,就可以反过来组成一个List

那么,我们来看看中间那条没注释的语句,通过PatchBuilder()的构造函数生成PatchBuilder,我们来看看PatchBuilder的构造函数

PatchBuilder Constructor
public PatchBuilder(File outFile, File dexFile, KeyStore.PrivateKeyEntry key, PrintStream verboseStream){this.mBuilder = new SignedJarBuilder(new FileOutputStream(outFile, false), key.getPrivateKey(),(X509Certificate)key.getCertificate());this.mBuilder.writeFile(dexFile, "classes.dex");
}

这个就是把dexFile里的内容,经过修改,加上签名,写入classes.dex文件中,但是这里实在看不出来其中的细节, 所以,我们要进入SignedJarBuilder类一探究竟

先看它的构造函数。

SignedJarBuilder Constructor
public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)throws IOException, NoSuchAlgorithmException{this.mOutputJar = new JarOutputStream(new BufferedOutputStream(out));//设置压缩级别this.mOutputJar.setLevel(9);this.mKey = key;this.mCertificate = certificate;if ((this.mKey != null) && (this.mCertificate != null)) {this.mManifest = new Manifest();Attributes main = this.mManifest.getMainAttributes();main.putValue("Manifest-Version", "1.0");main.putValue("Created-By", "1.0 (ApkPatch)");this.mBase64Encoder = new BASE64Encoder();this.mMessageDigest = MessageDigest.getInstance("SHA1");}
}

这个方法做了一些初始化,做了一些赋值操作,大家经常写类似的代码,就不进行分析了,只是为了让大家看writeFile(File, String)的时候容易理解一些。

SignedJarBuilder writeFile(File, String)

writeFile(File, String)做了什么呢?

public void writeFile(File inputFile, String jarPath){FileInputStream fis = new FileInputStream(inputFile);JarEntry entry = new JarEntry(jarPath);entry.setTime(inputFile.lastModified());writeEntry(fis, entry);
}

这个方法调用了writeEntry(InputStream, JarEntry)方法, 来看一看它的源码

SignedJarBuilder writeEntry(InputStream, JarEntry)
private void writeEntry(InputStream input, JarEntry entry)throws IOException{this.mOutputJar.putNextEntry(entry);int count;while ((count = input.read(this.mBuffer)) != -1){int count;this.mOutputJar.write(this.mBuffer, 0, count);if (this.mMessageDigest != null) {//将mBuffer中的内容放入mMessageDigest内部数组this.mMessageDigest.update(this.mBuffer, 0, count);}}this.mOutputJar.closeEntry();if (this.mManifest != null){Attributes attr = this.mManifest.getAttributes(entry.getName());if (attr == null) {attr = new Attributes();this.mManifest.getEntries().put(entry.getName(), attr);}attr.putValue("SHA1-Digest", this.mBase64Encoder.encode(this.mMessageDigest.digest()));}
}

mBuffer是4096 bytes的数组。 这段代码主要从input中读取一个buffer的数据,然后写入entry中,这里应该就可以理解我上面说的dexFile里的内容,写入classes.dex文件中了吧。

build(outFile, dexFile);结束了,看看最后一个方法release(this.out, dexFile, outFile)

ApkPatch release(this.out, dexFile, outFile)
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"));
}

最后就是把dexFile进行了md5加密,并把build(outFile, dexFile);函数中生成的outFile重命名。这样,AndFix框架所需要的补丁文件就生成了。

还记得上面提到的Patch类中读取Manifest的那些属性吗?在build(outFile, dexFile);函数中,那个getMeta()函数把它读取的属性写到了文件中。

到这里,第二篇分析也结束了,下一篇将会进入Android代码中一探它做了什么的究竟。

原文地址:

我们接着分析阿里开源的AndFix库,上次留下了三个坑,一个方法,两个类,不知道你们是否想急切了解呢? loadPatch()方法和AndFixManagerPatch类。

分析loadPatch()方法的时候离不开AndFixManager这个类,所以,我会在分析loadPatch()方法的时候分析AndFixManager这个类。 Patch类相当于一个容器,把修复bug所需的信息放在其中,Patch类相对来说比较独立,不需要牵扯到另外两个坑,所以,就先把这个坑埋了。

要分析Patch类,就不能不分析阿里提供的打包.apatch的工具apkpatch-1.0.2.jarPatch获取的信息其实就是apkpatch打包时放入其中的信息。 我下面对apkpatch.jar的分析以这个版本的源码为准。

分析源码,那些try-catch-finally和逻辑无关,所以,我会把这些代码从源码中删掉的,除非有提示用户的操作

我们先来看看Patch

class Patch

从前一篇的分析中,我们看到调用了Patch的构造函数,那我们就从构造函数开始看。

Patch Constructor

public Patch(File file) throws IOException {this.mFile = file;this.init();
}

Patch init()

将传入的文件保存在类变量中,并调用init()函数,那么init()函数干什么呢? 代码里异常和清理的代码我给删掉了,毕竟,这和我们对源码的分析关系不大,那么我们看一下剩下的代码

public void init(){JarFile jarFile = new JarFile(this.mFile);JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);InputStream inputStream = jarFile.getInputStream(entry);Manifest manifest = new Manifest(inputStream);Attributes main = manifest.getMainAttributes();this.mName = main.getValue(PATCH_NAME);this.mTime = new Date(main.getValue(CREATED_TIME));this.mClassesMap = new HashMap();Iterator it = main.keySet().iterator();while(it.hasNext()) {Name attrName = (Name)it.next();String name = attrName.toString();if(name.endsWith(CLASSES)) {List strings = Arrays.asList(main.getValue(attrName).split(","));if(name.equalsIgnoreCase(PATCH_CLASSES)) {this.mClassesMap.put(this.mName, strings);} else {this.mClassesMap.put(name.trim().substring(0, name.length() - 8), strings);}}}
}

先分析上面的代码,通过JarFile, JarEntry, Manifest, Attributes一层层的获取jar文件中值,并放入对应的类变量中 这些值是从哪里来的呢?是从生成这个.apatch文件的时候写入的,即apkpatch.jar文件中写入的。 虽然这个文件后缀名是apatch,不是jar,但是它生成的时候是使用Attributes,Manifest,jarEntry将数据写入的, 其实依旧是jar格式,修改了扩展名而已

上面的那些常量都是什么呢,我们来看看这个类的类变量,就是一些对应的字符串。

private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
private final File mFile;
private String mName;
private Date mTime;
private Map<String, List<String>> mClassesMap;

看完了Patch类,是不是还一头雾水呢, 其他的好理解, mClassesMap里面的各种类名是从哪里来,里面的类作用又是什么呢? 这里,我们就需要进入apkpatch.jar一探究竟。

apkpatch.jar

对于Jar文件,我们可以使用jd-gui文件打开,阿里这里并没有对这个jar文件加密,所以我们可以直接看,对于Jar文件,我们从Main()方法开始看起。 在package com.euler.patch;里面,有一个Main类,其中有那个我们常见的main()方法,那么我们就进入看一看。 代码比较长,我们一段一段来看

Main Main()

//CommandLineParser,CommandLine, Option, Options,OptionBuilder,  PosixParser等等类都是
//org.apache.commons.cli这个包中的类,负责解析命令行传给程序的参数,所以下面的很多内容应该就不用我过多解释了
CommandLineParser parser = new PosixParser();
CommandLine commandLine = null;
//option()方法就是将命令行需要解析的参数加入其中,有三种解析方式
//private static final Options allOptions = new Options();
//private static final Options patchOptions = new Options();
//private static final Options mergeOptions = new Options();
option();
try {commandLine = parser.parse(allOptions, args);
} catch (ParseException e) {System.err.println(e.getMessage());//提示用户如何使用这个jar包usage(commandLine);return;
}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;
}

这一部分主要是查看命令行是否有那些参数,如果没有要求的参数,则提示用户需要哪些参数。

接下来注意一下你下载apkpatch这个文件的时间,根据github的历史提交记录,如果你2015年10月以后下载的,可以忽视掉这个提醒,如果你是10月之前下载的那个工具,建议你更新一下这个工具,否则可能会导致-o所在的文件夹被清空。

好了,提醒完了,我们继续

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');
}

对于我们的release版应用来说,我们会指定storeFile, storePassword, keyAlias, keyPassword, 这些即是上面keystore, password, alias, entry对应的值。至于name,这是最后生成的文件的名称,基本可以不用管。

下面,我们看一下main()函数中最后的一个if-else

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];
}

在此,我选择不分析MergePatch这个部分,毕竟,你只有先生成了,后面才可能需要做merge操作。 from指修复bug后的apk,to指之前有bug的apk。所以这部分的分析就结束了。

main()函数里最后一部分内容

ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore,password, alias, entry);
apkPatch.doPatch();

将那些参数传给ApkPatch去初始化,然后调用doPatch()方法

ApkPatch

ApkPatch Constructor

我们一步步来看,首先是调用构造函数

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

Build Constructor

public class ApkPatch extends Build ApkPatch的函数头,说明它是Build类的子类

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.");try{FileUtils.cleanDirectory(out);} catch (IOException e) {throw new RuntimeException(e);}
}

在这里,看到了我之前提到的-o操作的问题,会清空那个文件夹内的内容。

好了,我们接下来就可以看看最后一个方法,doPatch()方法了。

ApkPatch doPatch()
public void doPatch() {File smaliDir = new File(this.out, "smali");smaliDir.mkdir();File dexFile = new File(this.out, "diff.dex");File outFile = new File(this.out, "diff.apatch");//DiffInfo是一个容器类,主要保存了新加的和修改的 类,方法和字段。DiffInfo info = new DexDiffer().diff(this.from, this.to);this.classes = buildCode(smaliDir, dexFile, info);build(outFile, dexFile);release(this.out, dexFile, outFile);
}

在out文件夹内生成了一个smali文件夹,还有diff.dexdiff.apatch文件。 看到diff()方法,应该能想到就是比较两个文件的不同,所以DiffInfo就是储存两个文件不同的一个容器类, 由于篇幅原因,这里就不深入其中了,有兴趣的同学可以深入其中看一下。

但是,在这个diff()方法中,有一个重要的问题需要大家注意,就是其中只针对classes.dex做了diff,如果你使用了Google的Multidex,那么结果就是你其它dex文件中的任何bug,依旧无法修复,因为这个生成的DiffInfo中没有其它dex的信息,这个时候就需要大家使用JavaAssist之类的工具修改阿里的这个jar文件,然后达到你自己的修复非classes.dex文件中bug的目的,问题出在DexFileFactory.class类中。

接下来,我们可以看到调用了三个方法,buildCode(smaliDir, dexFile, info);build(outFile, dexFile);release(this.out, dexFile, outFile); 一个个的跟进去看一看。

ApkPatch buildCode(smaliDir, dexFile, info)
private static Set<String> buildCode(File smaliDir, File dexFile, DiffInfo info)throws IOException, RecognitionException, FileNotFoundException{Set classes = new HashSet();Set list = new HashSet();//从这里可以看出,list保存了DiffInfo容器中的新添加的classes和被修改过的classeslist.addAll(info.getAddedClasses());list.addAll(info.getModifiedClasses());baksmaliOptions options = new baksmaliOptions();options.deodex = false;options.noParameterRegisters = false;options.useLocalsDirective = true;options.useSequentialLabels = true;options.outputDebugInfo = true;options.addCodeOffsets = false;options.jobs = -1;options.noAccessorComments = false;options.registerInfo = 0;options.ignoreErrors = false;options.inlineResolver = null;options.checkPackagePrivateAccess = false;if (!options.noAccessorComments) {options.syntheticAccessorResolver = new SyntheticAccessorResolver(list);}ClassFileNameHandler outFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");ClassFileNameHandler inFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");DexBuilder dexBuilder = DexBuilder.makeDexBuilder();for (DexBackedClassDef classDef : list) {String className = classDef.getType();//将相关的类信息写入outFileNameHandlerbaksmali.disassembleClass(classDef, outFileNameHandler, options);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;
}

看到baksmali,反编译过apk的同学一定不陌生,这就是dex的打包工具,还有对应的解包工具smali,就到这里,这方面不继续深入了。 如果想深入了解dex打包解包工具的源码,参见This Blog

可以看到,这个方法的返回值将DiffInfo中新添加的classes和修改过的classes做了一个重命名,然后保存了起来,同时,将相关内容写入smali文件中。 这个重命名是一个怎样的重命名呢,看一下生成的smali文件夹里任意一个smali文件,我就拿Demo里的MainActivity.smali来说明 这个类在文件中的类名是Lcom/euler/andfix/MainActivity;,看到这个名字,再看下面这个方法就很清晰了

classes.add(TypeGenUtil.newType(className)
.substring(1, TypeGenUtil.newType(className).length() - 1)
.replace('/', '.'));

就是把Lcom/euler/andfix/MainActivity;替换成com.euler.andfix.MainActivity_CF; 那个_CF哪里来的呢,就是那个TypeGenUtil类做的操作了。 这个类就一个方法,该方法进对String进行了操作,我们来看一看源码

public static String newType(String type){return type.substring(0, type.length() - 1) + "_CF;";
}

可以看到,去掉了类名多余的那个’;’,然后在后面加了个_CF后缀。重命名应该是为了不合之前安装的dex文件的名字冲突。

(new FileDataStore(file) 作用就是清空file里的内容) 最后,将dexFile文件清空,把dexBuilder的内容写入其中。

到这里,buildCode(smaliDir, dexFile, info)方法就结束了, 看看下一个方法build(outFile, dexFile);

ApkPatch build(outFile, dexFile)

上一个方法已经把内容填充到dexFile内了,我们来看看它的源码。

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);//将getMeta()中获取的Manifest内容写入"META-INF/PATCH.MF"文件中builder.writeMeta(getMeta());//单纯调用了close()命令, 将异常处理放在子函数中。//这里做了一个异常分层,即不同抽象层次的子函数需要处理的异常不一样//具体请参阅《Code Complete》(代码大全)在恰当的抽象层次抛出异常builder.sealPatch();
}

要打包了,打包完是要签名的,所以需要获取签名的相关信息,这一部分就不详细讲解了。 这里提一下getMeta()函数,因为开头我们提到的Patch init() 函数内 这句List strings = Arrays.asList(main.getValue(attrName).split(","));中的那个strings就是从这里写入的

 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;
}

那个classes就是我们那个将类名修改后保存起来的SetFormater.dotStringList(this.classes));这个方法就是遍历Set,并将其中用','作为分隔符,将整个Set中保存的String拼接 所以在Patch init()方法内,就可以反过来组成一个List

那么,我们来看看中间那条没注释的语句,通过PatchBuilder()的构造函数生成PatchBuilder,我们来看看PatchBuilder的构造函数

PatchBuilder Constructor
public PatchBuilder(File outFile, File dexFile, KeyStore.PrivateKeyEntry key, PrintStream verboseStream){this.mBuilder = new SignedJarBuilder(new FileOutputStream(outFile, false), key.getPrivateKey(),(X509Certificate)key.getCertificate());this.mBuilder.writeFile(dexFile, "classes.dex");
}

这个就是把dexFile里的内容,经过修改,加上签名,写入classes.dex文件中,但是这里实在看不出来其中的细节, 所以,我们要进入SignedJarBuilder类一探究竟

先看它的构造函数。

SignedJarBuilder Constructor
public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)throws IOException, NoSuchAlgorithmException{this.mOutputJar = new JarOutputStream(new BufferedOutputStream(out));//设置压缩级别this.mOutputJar.setLevel(9);this.mKey = key;this.mCertificate = certificate;if ((this.mKey != null) && (this.mCertificate != null)) {this.mManifest = new Manifest();Attributes main = this.mManifest.getMainAttributes();main.putValue("Manifest-Version", "1.0");main.putValue("Created-By", "1.0 (ApkPatch)");this.mBase64Encoder = new BASE64Encoder();this.mMessageDigest = MessageDigest.getInstance("SHA1");}
}

这个方法做了一些初始化,做了一些赋值操作,大家经常写类似的代码,就不进行分析了,只是为了让大家看writeFile(File, String)的时候容易理解一些。

SignedJarBuilder writeFile(File, String)

writeFile(File, String)做了什么呢?

public void writeFile(File inputFile, String jarPath){FileInputStream fis = new FileInputStream(inputFile);JarEntry entry = new JarEntry(jarPath);entry.setTime(inputFile.lastModified());writeEntry(fis, entry);
}

这个方法调用了writeEntry(InputStream, JarEntry)方法, 来看一看它的源码

SignedJarBuilder writeEntry(InputStream, JarEntry)
private void writeEntry(InputStream input, JarEntry entry)throws IOException{this.mOutputJar.putNextEntry(entry);int count;while ((count = input.read(this.mBuffer)) != -1){int count;this.mOutputJar.write(this.mBuffer, 0, count);if (this.mMessageDigest != null) {//将mBuffer中的内容放入mMessageDigest内部数组this.mMessageDigest.update(this.mBuffer, 0, count);}}this.mOutputJar.closeEntry();if (this.mManifest != null){Attributes attr = this.mManifest.getAttributes(entry.getName());if (attr == null) {attr = new Attributes();this.mManifest.getEntries().put(entry.getName(), attr);}attr.putValue("SHA1-Digest", this.mBase64Encoder.encode(this.mMessageDigest.digest()));}
}

mBuffer是4096 bytes的数组。 这段代码主要从input中读取一个buffer的数据,然后写入entry中,这里应该就可以理解我上面说的dexFile里的内容,写入classes.dex文件中了吧。

build(outFile, dexFile);结束了,看看最后一个方法release(this.out, dexFile, outFile)

ApkPatch release(this.out, dexFile, outFile)
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"));
}

最后就是把dexFile进行了md5加密,并把build(outFile, dexFile);函数中生成的outFile重命名。这样,AndFix框架所需要的补丁文件就生成了。

还记得上面提到的Patch类中读取Manifest的那些属性吗?在build(outFile, dexFile);函数中,那个getMeta()函数把它读取的属性写到了文件中。

到这里,第二篇分析也结束了,下一篇将会进入Android代码中一探它做了什么的究竟。

原文地址:

我们接着分析阿里开源的AndFix库,上次留下了三个坑,一个方法,两个类,不知道你们是否想急切了解呢? loadPatch()方法和AndFixManagerPatch类。

分析loadPatch()方法的时候离不开AndFixManager这个类,所以,我会在分析loadPatch()方法的时候分析AndFixManager这个类。 Patch类相当于一个容器,把修复bug所需的信息放在其中,Patch类相对来说比较独立,不需要牵扯到另外两个坑,所以,就先把这个坑埋了。

要分析Patch类,就不能不分析阿里提供的打包.apatch的工具apkpatch-1.0.2.jarPatch获取的信息其实就是apkpatch打包时放入其中的信息。 我下面对apkpatch.jar的分析以这个版本的源码为准。

分析源码,那些try-catch-finally和逻辑无关,所以,我会把这些代码从源码中删掉的,除非有提示用户的操作

我们先来看看Patch

class Patch

从前一篇的分析中,我们看到调用了Patch的构造函数,那我们就从构造函数开始看。

Patch Constructor

public Patch(File file) throws IOException {this.mFile = file;this.init();
}

Patch init()

将传入的文件保存在类变量中,并调用init()函数,那么init()函数干什么呢? 代码里异常和清理的代码我给删掉了,毕竟,这和我们对源码的分析关系不大,那么我们看一下剩下的代码

public void init(){JarFile jarFile = new JarFile(this.mFile);JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);InputStream inputStream = jarFile.getInputStream(entry);Manifest manifest = new Manifest(inputStream);Attributes main = manifest.getMainAttributes();this.mName = main.getValue(PATCH_NAME);this.mTime = new Date(main.getValue(CREATED_TIME));this.mClassesMap = new HashMap();Iterator it = main.keySet().iterator();while(it.hasNext()) {Name attrName = (Name)it.next();String name = attrName.toString();if(name.endsWith(CLASSES)) {List strings = Arrays.asList(main.getValue(attrName).split(","));if(name.equalsIgnoreCase(PATCH_CLASSES)) {this.mClassesMap.put(this.mName, strings);} else {this.mClassesMap.put(name.trim().substring(0, name.length() - 8), strings);}}}
}

先分析上面的代码,通过JarFile, JarEntry, Manifest, Attributes一层层的获取jar文件中值,并放入对应的类变量中 这些值是从哪里来的呢?是从生成这个.apatch文件的时候写入的,即apkpatch.jar文件中写入的。 虽然这个文件后缀名是apatch,不是jar,但是它生成的时候是使用Attributes,Manifest,jarEntry将数据写入的, 其实依旧是jar格式,修改了扩展名而已

上面的那些常量都是什么呢,我们来看看这个类的类变量,就是一些对应的字符串。

private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
private final File mFile;
private String mName;
private Date mTime;
private Map<String, List<String>> mClassesMap;

看完了Patch类,是不是还一头雾水呢, 其他的好理解, mClassesMap里面的各种类名是从哪里来,里面的类作用又是什么呢? 这里,我们就需要进入apkpatch.jar一探究竟。

apkpatch.jar

对于Jar文件,我们可以使用jd-gui文件打开,阿里这里并没有对这个jar文件加密,所以我们可以直接看,对于Jar文件,我们从Main()方法开始看起。 在package com.euler.patch;里面,有一个Main类,其中有那个我们常见的main()方法,那么我们就进入看一看。 代码比较长,我们一段一段来看

Main Main()

//CommandLineParser,CommandLine, Option, Options,OptionBuilder,  PosixParser等等类都是
//org.apache.commons.cli这个包中的类,负责解析命令行传给程序的参数,所以下面的很多内容应该就不用我过多解释了
CommandLineParser parser = new PosixParser();
CommandLine commandLine = null;
//option()方法就是将命令行需要解析的参数加入其中,有三种解析方式
//private static final Options allOptions = new Options();
//private static final Options patchOptions = new Options();
//private static final Options mergeOptions = new Options();
option();
try {commandLine = parser.parse(allOptions, args);
} catch (ParseException e) {System.err.println(e.getMessage());//提示用户如何使用这个jar包usage(commandLine);return;
}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;
}

这一部分主要是查看命令行是否有那些参数,如果没有要求的参数,则提示用户需要哪些参数。

接下来注意一下你下载apkpatch这个文件的时间,根据github的历史提交记录,如果你2015年10月以后下载的,可以忽视掉这个提醒,如果你是10月之前下载的那个工具,建议你更新一下这个工具,否则可能会导致-o所在的文件夹被清空。

好了,提醒完了,我们继续

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');
}

对于我们的release版应用来说,我们会指定storeFile, storePassword, keyAlias, keyPassword, 这些即是上面keystore, password, alias, entry对应的值。至于name,这是最后生成的文件的名称,基本可以不用管。

下面,我们看一下main()函数中最后的一个if-else

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];
}

在此,我选择不分析MergePatch这个部分,毕竟,你只有先生成了,后面才可能需要做merge操作。 from指修复bug后的apk,to指之前有bug的apk。所以这部分的分析就结束了。

main()函数里最后一部分内容

ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore,password, alias, entry);
apkPatch.doPatch();

将那些参数传给ApkPatch去初始化,然后调用doPatch()方法

ApkPatch

ApkPatch Constructor

我们一步步来看,首先是调用构造函数

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

Build Constructor

public class ApkPatch extends Build ApkPatch的函数头,说明它是Build类的子类

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.");try{FileUtils.cleanDirectory(out);} catch (IOException e) {throw new RuntimeException(e);}
}

在这里,看到了我之前提到的-o操作的问题,会清空那个文件夹内的内容。

好了,我们接下来就可以看看最后一个方法,doPatch()方法了。

ApkPatch doPatch()
public void doPatch() {File smaliDir = new File(this.out, "smali");smaliDir.mkdir();File dexFile = new File(this.out, "diff.dex");File outFile = new File(this.out, "diff.apatch");//DiffInfo是一个容器类,主要保存了新加的和修改的 类,方法和字段。DiffInfo info = new DexDiffer().diff(this.from, this.to);this.classes = buildCode(smaliDir, dexFile, info);build(outFile, dexFile);release(this.out, dexFile, outFile);
}

在out文件夹内生成了一个smali文件夹,还有diff.dexdiff.apatch文件。 看到diff()方法,应该能想到就是比较两个文件的不同,所以DiffInfo就是储存两个文件不同的一个容器类, 由于篇幅原因,这里就不深入其中了,有兴趣的同学可以深入其中看一下。

但是,在这个diff()方法中,有一个重要的问题需要大家注意,就是其中只针对classes.dex做了diff,如果你使用了Google的Multidex,那么结果就是你其它dex文件中的任何bug,依旧无法修复,因为这个生成的DiffInfo中没有其它dex的信息,这个时候就需要大家使用JavaAssist之类的工具修改阿里的这个jar文件,然后达到你自己的修复非classes.dex文件中bug的目的,问题出在DexFileFactory.class类中。

接下来,我们可以看到调用了三个方法,buildCode(smaliDir, dexFile, info);build(outFile, dexFile);release(this.out, dexFile, outFile); 一个个的跟进去看一看。

ApkPatch buildCode(smaliDir, dexFile, info)
private static Set<String> buildCode(File smaliDir, File dexFile, DiffInfo info)throws IOException, RecognitionException, FileNotFoundException{Set classes = new HashSet();Set list = new HashSet();//从这里可以看出,list保存了DiffInfo容器中的新添加的classes和被修改过的classeslist.addAll(info.getAddedClasses());list.addAll(info.getModifiedClasses());baksmaliOptions options = new baksmaliOptions();options.deodex = false;options.noParameterRegisters = false;options.useLocalsDirective = true;options.useSequentialLabels = true;options.outputDebugInfo = true;options.addCodeOffsets = false;options.jobs = -1;options.noAccessorComments = false;options.registerInfo = 0;options.ignoreErrors = false;options.inlineResolver = null;options.checkPackagePrivateAccess = false;if (!options.noAccessorComments) {options.syntheticAccessorResolver = new SyntheticAccessorResolver(list);}ClassFileNameHandler outFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");ClassFileNameHandler inFileNameHandler = new ClassFileNameHandler(smaliDir, ".smali");DexBuilder dexBuilder = DexBuilder.makeDexBuilder();for (DexBackedClassDef classDef : list) {String className = classDef.getType();//将相关的类信息写入outFileNameHandlerbaksmali.disassembleClass(classDef, outFileNameHandler, options);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;
}

看到baksmali,反编译过apk的同学一定不陌生,这就是dex的打包工具,还有对应的解包工具smali,就到这里,这方面不继续深入了。 如果想深入了解dex打包解包工具的源码,参见This Blog

可以看到,这个方法的返回值将DiffInfo中新添加的classes和修改过的classes做了一个重命名,然后保存了起来,同时,将相关内容写入smali文件中。 这个重命名是一个怎样的重命名呢,看一下生成的smali文件夹里任意一个smali文件,我就拿Demo里的MainActivity.smali来说明 这个类在文件中的类名是Lcom/euler/andfix/MainActivity;,看到这个名字,再看下面这个方法就很清晰了

classes.add(TypeGenUtil.newType(className)
.substring(1, TypeGenUtil.newType(className).length() - 1)
.replace('/', '.'));

就是把Lcom/euler/andfix/MainActivity;替换成com.euler.andfix.MainActivity_CF; 那个_CF哪里来的呢,就是那个TypeGenUtil类做的操作了。 这个类就一个方法,该方法进对String进行了操作,我们来看一看源码

public static String newType(String type){return type.substring(0, type.length() - 1) + "_CF;";
}

可以看到,去掉了类名多余的那个’;’,然后在后面加了个_CF后缀。重命名应该是为了不合之前安装的dex文件的名字冲突。

(new FileDataStore(file) 作用就是清空file里的内容) 最后,将dexFile文件清空,把dexBuilder的内容写入其中。

到这里,buildCode(smaliDir, dexFile, info)方法就结束了, 看看下一个方法build(outFile, dexFile);

ApkPatch build(outFile, dexFile)

上一个方法已经把内容填充到dexFile内了,我们来看看它的源码。

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);//将getMeta()中获取的Manifest内容写入"META-INF/PATCH.MF"文件中builder.writeMeta(getMeta());//单纯调用了close()命令, 将异常处理放在子函数中。//这里做了一个异常分层,即不同抽象层次的子函数需要处理的异常不一样//具体请参阅《Code Complete》(代码大全)在恰当的抽象层次抛出异常builder.sealPatch();
}

要打包了,打包完是要签名的,所以需要获取签名的相关信息,这一部分就不详细讲解了。 这里提一下getMeta()函数,因为开头我们提到的Patch init() 函数内 这句List strings = Arrays.asList(main.getValue(attrName).split(","));中的那个strings就是从这里写入的

 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;
}

那个classes就是我们那个将类名修改后保存起来的SetFormater.dotStringList(this.classes));这个方法就是遍历Set,并将其中用','作为分隔符,将整个Set中保存的String拼接 所以在Patch init()方法内,就可以反过来组成一个List

那么,我们来看看中间那条没注释的语句,通过PatchBuilder()的构造函数生成PatchBuilder,我们来看看PatchBuilder的构造函数

PatchBuilder Constructor
public PatchBuilder(File outFile, File dexFile, KeyStore.PrivateKeyEntry key, PrintStream verboseStream){this.mBuilder = new SignedJarBuilder(new FileOutputStream(outFile, false), key.getPrivateKey(),(X509Certificate)key.getCertificate());this.mBuilder.writeFile(dexFile, "classes.dex");
}

这个就是把dexFile里的内容,经过修改,加上签名,写入classes.dex文件中,但是这里实在看不出来其中的细节, 所以,我们要进入SignedJarBuilder类一探究竟

先看它的构造函数。

SignedJarBuilder Constructor
public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)throws IOException, NoSuchAlgorithmException{this.mOutputJar = new JarOutputStream(new BufferedOutputStream(out));//设置压缩级别this.mOutputJar.setLevel(9);this.mKey = key;this.mCertificate = certificate;if ((this.mKey != null) && (this.mCertificate != null)) {this.mManifest = new Manifest();Attributes main = this.mManifest.getMainAttributes();main.putValue("Manifest-Version", "1.0");main.putValue("Created-By", "1.0 (ApkPatch)");this.mBase64Encoder = new BASE64Encoder();this.mMessageDigest = MessageDigest.getInstance("SHA1");}
}

这个方法做了一些初始化,做了一些赋值操作,大家经常写类似的代码,就不进行分析了,只是为了让大家看writeFile(File, String)的时候容易理解一些。

SignedJarBuilder writeFile(File, String)

writeFile(File, String)做了什么呢?

public void writeFile(File inputFile, String jarPath){FileInputStream fis = new FileInputStream(inputFile);JarEntry entry = new JarEntry(jarPath);entry.setTime(inputFile.lastModified());writeEntry(fis, entry);
}

这个方法调用了writeEntry(InputStream, JarEntry)方法, 来看一看它的源码

SignedJarBuilder writeEntry(InputStream, JarEntry)
private void writeEntry(InputStream input, JarEntry entry)throws IOException{this.mOutputJar.putNextEntry(entry);int count;while ((count = input.read(this.mBuffer)) != -1){int count;this.mOutputJar.write(this.mBuffer, 0, count);if (this.mMessageDigest != null) {//将mBuffer中的内容放入mMessageDigest内部数组this.mMessageDigest.update(this.mBuffer, 0, count);}}this.mOutputJar.closeEntry();if (this.mManifest != null){Attributes attr = this.mManifest.getAttributes(entry.getName());if (attr == null) {attr = new Attributes();this.mManifest.getEntries().put(entry.getName(), attr);}attr.putValue("SHA1-Digest", this.mBase64Encoder.encode(this.mMessageDigest.digest()));}
}

mBuffer是4096 bytes的数组。 这段代码主要从input中读取一个buffer的数据,然后写入entry中,这里应该就可以理解我上面说的dexFile里的内容,写入classes.dex文件中了吧。

build(outFile, dexFile);结束了,看看最后一个方法release(this.out, dexFile, outFile)

ApkPatch release(this.out, dexFile, outFile)
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"));
}

最后就是把dexFile进行了md5加密,并把build(outFile, dexFile);函数中生成的outFile重命名。这样,AndFix框架所需要的补丁文件就生成了。

还记得上面提到的Patch类中读取Manifest的那些属性吗?在build(outFile, dexFile);函数中,那个getMeta()函数把它读取的属性写到了文件中。

到这里,第二篇分析也结束了,下一篇将会进入Android代码中一探它做了什么的究竟。

原文地址: http://yunair.github.io/blog/2015/10/10/AndFix-%E8%A7%A3%E6%9E%90(%E4%B8%AD).html

AndFix解析——(中)相关推荐

  1. WR:微生物污染源解析中宿主特异性标记物在中国的表现特征

    WR:微生物污染源解析中宿主特异性标记物在中国的表现特征 微生物污染源解析中宿主特异性标记物在中国的表现特征 Performance of host-associated genetic marker ...

  2. ios网址解析中,中文部分如何处理

    在网络解析中,中文出现的时候,在解析数据是中文会显示为 %E7%81%AB%E5%BD%B1%E5%BF%8D%E8%80%85 这样的形式但是如果我们之间用字符串拼接键值对的时候但多数出现的不是错误 ...

  3. 时间约束的实体解析中记录对排序研究

    时间约束的实体解析中记录对排序研究 人工智能技术与咨询 来源:<软件学报> ,作者孙琛琛等 摘 要:实体解析是数据集成和数据清洗的重要组成部分,也是大数据分析与挖掘的必要预处理步骤.传统的 ...

  4. Spring的XML解析中关于DTD的路径问题-

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 在Spr ...

  5. 应用计算机散热的原理是什么,计算机散热的原理与技术解析[中].doc

    散热的原理与技术解析-中(1) 在本文的第一部分,我们主要探讨了如何快速将热量带离热源,主要涉及热传递三种基本方式中的热传导方面.但对一个完整的散热器而言,这是远远不够的,因为这样只是将热量转移到散热 ...

  6. DNS解析中的A记录、AAAA记录、CNAME记录、MX记录、NS记录、TXT记录、SRV记录、URL转发等

    A A记录: 将域名指向一个IPv4地址(例如:100.100.100.100),需要增加A记录 NS NS记录: 域名解析服务器记录,如果要将子域名指定某个域名服务器来解析,需要设置NS记录 SOA ...

  7. 源码解析中看到的奇淫巧技

    源码解析中看到的奇淫巧技 一. 数组重置 let arr = [123,123] arr.length // 2 arr.length = 0 arr // [] 当我们给数组的length 属性设置 ...

  8. AndFix解析——(下)

    我们接着分析阿里开源的AndFix库, 上一篇分析了Patch类,这个类相当于我们提供补丁的容器,容器里有了东西,我们要对容器进行操作了, 于是开始了我们这次的分析. 在第二篇里,我们添了Patch类 ...

  9. AndFix解析——(上)

    阿里巴巴前一段时间开源了他们用来解决线上紧急bug的一款Android库--AndFix 对Android开发者来说真是一个很好的消息. 基于自己的经验,太长的文字很少有人可以一口气看下来的,所以我打 ...

最新文章

  1. pgjdbc源码分析
  2. poj 1852 Ants_贪心
  3. Thinkphp下嵌套UEditor富文本WEB编辑器
  4. 线性时间复杂度求数组中第K大数
  5. 人声处理_10款免费的人声处理工具
  6. code dairy
  7. commons-lang3-RandomUtils
  8. 是时候重构下自己的博客了
  9. shell脚本分析 nginx日志访问次数最多及最耗时的页面(慢查询)
  10. 未捕获的错误:始终违反:元素类型无效:预期为字符串(对于内置组件)或类/函数,但得到了:对象
  11. mysql数据库工程师考证题_100道MySQL常见面试题总结
  12. 用java画网状图_如何在背景中绘制一个带网格线的漂亮条形图?
  13. 数据质量管理有哪些方法
  14. 【渝粤教育】国家开放大学2018年春季 0314-21T兽医基础 参考试题
  15. 基于Java的项目--酒店客房管理系统
  16. 2022年茶艺师(初级)考试试题及在线模拟考试
  17. 3-8 查询水果价格 (15 分)
  18. 信号卷积和图像卷积滤波
  19. [个人开发者赚钱五]植入广告等获取收益
  20. DTI数据处理: from scanner to statistics

热门文章

  1. HDU OJ Matrix Swapping II
  2. 风讯dotNETCMS源码分析—数据存取篇
  3. CUDA从入门到精通(零):写在前面
  4. Linux/Unix下的任务管理器-top命令
  5. 在idea中使用构造方法
  6. 《Neural network and deep learning》学习笔记(一)
  7. A humble heart2019-11-09
  8. 3DSlicer18:Layouts
  9. 大富翁已成过去-我的一些感想
  10. lseek函数实现对打开文件的定位