<LinearLayout xmlns:android=“http://schemas.andro
id.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”>
<TextView
android:id="@+id/mCreateBug"
android:layout_width=“match_parent”
android:layout_height=“45dp”
android:layout_margin=“20dp”
android:background="@color/colorPrimary"
android:gravity=“center”
android:text=“生成BUG”
android:textColor="#fff" />
<TextView
android:id="@+id/mFixBug"
android:layout_width=“match_parent”
android:layout_height=“45dp”
android:layout_marginLeft=“20dp”
android:layout_marginRight=“20dp”
android:background="@color/colorPrimary"
android:gravity=“center”
android:text=“修复BUG”
android:textColor="#fff" />
界面如下:当我们点击生成BUG按钮时,我们的程序会发生崩溃Crash掉:
②、编写业务代码
首先是对差异包的后缀名、存放路径等的一个初始化操作:
private static final String TAG = MainActivity.class.getSimpleName();
//定义差异包文件的后缀名
private static final String FILE_SUFFIX = “.apatch”;
//定义差异包文件的存放路径
private String mPatchDir;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化差异包文件路径
mPatchDir = getExternalCacheDir().getAbsolutePath()+"/apatch/";
Log.e(TAG, “完整路径—>”+mPatchDir);
//创建文件夹
File file = new File(mPatchDir);
if (file == null || !file.exists()){
file.mkdir();
}
}
然后是构造apatch文件的完整路径,当点击修复BUG的时候,调用PatchManager的addPath方法加载文件:
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.mFixBug: //修复Bug
AndFixPatchManager.getInstance().addPatch(getPatchName());
break;
}
}
//构造patch文件名
private String getPatchName(){
return mPatchDir.concat(“jaqandfix”).concat(FILE_SUFFIX);
}
③、模拟BUG产生
在产生BUG按钮的点击事件的方法中我们模拟一次Crash的产生:
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.mCreateBug: //生成Bug
Utils.printLog();
break;
}
}
然后在Utils类中的**printLog()**方法中给它制造Crash,这里就简单的让它产生空指针异常:
//构造异常方法
public static void printLog() {
String info = null;
Log.e(“jarchie-andfix”, info);
}
④、Build异常APK
在构建APK时,我们需要构建带签名的版本,这是为了接下来生成apatch文件用的。关于如何构建release版本的APK我就不多说了,相信没有不知道的吧,构建完了之后,你可以把它弄到你的手机上,通过adb push或者文件传输工具都可以,只要安装到你手机上就行,注意这里需要将这个有bug的apk保存一份,因为后面要用到。
2.3.2、构建正常APK
①、修改空指针异常
public static void printLog() {
String info = “Jarchie”; //修复空指针
Log.e(“jarchie-andfix”, info);
}
②、构建修复后的APK
将修改后的代码重新打包,生成新的release包,这里也将新的apk包保存一份。
2.4、修复BUG
①、生成apatch文件
生成apatch文件主要是用到了apkpatch这个命令行工具,这个工具包在github上有,大家下载到自己电脑上就行了:
里面就3个文件,windows用户使用.bat的这个,Linux或者MAC OS的用户使用.sh的这个。
然后我将之前Build的两个apk和jks文件都复制到这个文件夹中,并且新建了一个文件夹outputs作为apatch文件的输出目录:
然后打开控制台,进入到apkpatch这个目录下,执行apkpatch命令来看一下这个命令的用法介绍:
上面的是用来生成apatch文件,下面的是用来合并多个patch文件为一个的时候用的,具体的参数下面也都给出了,并且也都有注释说明(虽然都是英文,但相信你都能看的懂)。
然后我们就来使用apkpatch命令来生成我们的.apatch差异包:
执行完这个命令就生成了我们的差异包,并且它还会告诉你哪个类的哪个方法做了修改,正好就是我们的printLog()方法修改了。
进到本地目录中可以看到确实生成了apatch文件,我将它重命名为 jaqandfix.apatch。
②、push apatch文件
在生成了apatch文件之后,就可以将它放到手机对应的目录中,这一步操作同样也没有限制具体的方法,你可以通过文件传输工具,也可以直接通过adb命令将文件push到对应的目录,我这里使用adb命令的方式进行:
可以看到,我们手机中对应的目录下面已经有了push进来的jaqandfix.apatch文件。
③、修复BUG
再次进入App,然后首先点击修复BUG,它会去load这个补丁文件,当你再次点击产生BUG时,你会发现BUG已经被修复了。
注意:官网上给出的是2.1-7.0的版本,如果你各种操作步骤都是正确的,但是没有效果,那就换一台手机试一下,因为毕竟这个东西并不是所有机型都适配的,这里主要是学习它的方法。还有一点是,实际应用中,补丁文件是肯定不可能通过adb push这种方式进入用户手机中的,基本上都是通过服务端下发,客户端是一个下载文件的过程,这一点也需要注意。
到这里就已经说完了AndFix的修复流程,整个流程总结下来就是下面这张简化的图:
三、AndFix源码解析
============
首先找到之前封装的AndFixPatchManager类,然后找到initPatch()方法:
//初始化AndFix方法
public void initPatch(Context context){
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getVersionName(context));
mPatchManager.loadPatch();
}
从代码中可以看到,所有的操作都是通过AndFix的PatchManager类来完成的,很明显是外观模式,将所有的API都包含在了PatchManger中,所以不需要关注AndFix其他模块的作用。这里需要说明一点,阅读源码我们不可能把每一个类的每一行代码都完全弄懂,我们读源码是为了了解这个框架的实现过程,所以最好的方式就是结合在应用层我们自己的业务代码中调用它的那些类和方法,按照顺序一一跟进阅读,把整个调用流程串起来就OK了。
好,现在来打开PatchManager类,首先看一下它里面几个比较重要的成员变量:
/**
- context
*/
private final Context mContext;
/**
- AndFix manager
*/
private final AndFixManager mAndFixManager;
/**
- patch directory
*/
private final File mPatchDir;
/**
- patchs
*/
private final SortedSet mPatchs;
/**
- classloaders
*/
private final Map<String, ClassLoader> mLoaders;
-
AndFixManager:所有的方法替换、BUG修复都是由AndFixManager来完成的
-
SortedSet:经过排序后的Set集合,包含应用所有的Patch文件
接着来看一下它的构造方法,因为我们在应用层最先调用的就是它的构造方法:
/**
-
@param context
-
context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);
mPatchDir = new File(mContext.getFilesDir(), DIR);
mPatchs = new ConcurrentSkipListSet();
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
可以看到构造方法主要就是进行了一系列的初始化:上下文、AndFixManager、文件夹、数据结构等等的初始化操作。
接着来看我们应用层调用的第一个方法init()方法:
/**
-
initialize
-
@param appVersion
-
App version
*/
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, “patch dir create error.”);
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
initPatchs();
}
}
入参是需要传入当前应用的版本号,然后内部一开始是进行了文件夹的判断,满足了条件之后,它会从AndFix的SharedPreferences中拿到之前保存的版本号,然后通过这个版本号和入参中传入的版本号去做一个判断,如果不同,表明我们的应用已经做了升级,然后就会调用cleanPatch()去删除所有的Patch文件,同时更新版本号,用于下一次的比较,如果版本号相同,表明没有升级,则会调用**initPatchs()**方法,接下来,跟进这个initPatchs()方法:
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
这个方法很简单,就是遍历指定Patch文件夹下的所有文件,然后将它们通过_addPatch()_方法添加到mPatchs这个PatchList中,跟进addPatch()方法看一下:
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);
mPatchs.add(patch);
} catch (IOException e) {
Log.e(TAG, “addPatch”, e);
}
}
return patch;
}
这个方法内部首先是判断传入的文件后缀名是否符合.apatch格式,如果符合,将其转化为Patch文件,然后将文件添加到PatchList中,所以这里的mPatchs内部就是保存了所有的Patch文件。然后点击Patch类进入到这个类中看一下它是如何将一个File转化为Patch类的?这个Patch类就相当于是一个实体类,这个类中定义了一些成员变量:
/**
- patch file
*/
private final File mFile;
/**
- name
*/
private String mName;
/**
- create time
*/
private Date mTime;
/**
- classes of patch
*/
private Map<String, List> mClassesMap;
主要有传入的文件、文件名、mClassMap等,mClassMap是存储了本次Patch文件所有要修复的class的字符串,然后会调用类中的init()方法完成解析:
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);
mTime = new Date(main.getValue(CREATED_TIME));
mClassesMap = new HashMap<String, List>();
Attributes.Name attrName;
String name;
List strings;
for (Iterator<?> it = main.keySet().iterator(); it.hasNext()😉 {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// “-Classes”
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
这个方法是首先把文件转化成jar文件,然后解析jar文件中的所有字段比如:PATCH_NAME、CREATED_TIME等,这些字段是我们之前通过apatch命令行工具生成apatch文件的时候添加的,所以在这里可以直接解析了。然后来说mClassMap是如何初始化的,它会找到所有的Class,然后判断一下是不是自己要解析的PATCH_CLASS,如果是就添加到以当前Patch文件名为key的Map中,添加进来之后当你后续使用的时候,就可以直接通过getClasses传入当前的Patch文件名获取这个Patch文件中所有要修复的Class的绝对路径:
public List getClasses(String patchName) {
return mClassesMap.get(patchName);
}
现在我们应该清楚了这个Patch文件的作用了,它就是将普通磁盘上的File转化成PatchFile方便使用。OK,到这里这个PatchManager的init()方法就说完了,总结一下它的作用就是对Patch文件的删除和添加。
应用层中在我们下载完Patch文件之后,我们调用了addPatch()方法还记得吗?mPatchManager.addPatch(path); 现在就来看一下这个addPatch()方法是如何实现的?
/**
-
add patch at runtime
-
@param path
-
patch path
-
@throws IOException
*/
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if(!src.exists()){
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, “patch [” + path + “] has be loaded.”);
return;
}
FileUtil.copyFile(src, dest);// copy to patch’s directory
Patch patch = addPatch(dest);
if (patch != null) {
loadPatch(patch);
}
}
前面就是一些判断和文件的创建,它会先把磁盘上的文件拷贝到mPatchDir下面,拷贝完成之后会将文件解析成Patch类,然后会添加到mPatchs这个PatchList中,添加完以后,最后调用了loadPatch()方法,正是因为调用了loadPatch()方法所以可以完成BUG的修复,在loadPatch()方法内部调用了AndFixManager去完成了方法的替换,所以接着来看一下loadPatch()方法的实现过程。
/**
-
load patch,call when application start
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set patchNames;
List classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
/**
-
load specific patch
-
@param patch
-
patch
*/
private void loadPatch(Patch patch) {
Set patchNames = patch.getPatchNames();
ClassLoader cl;
List classes;
for (String patchName : patchNames) {
if (mLoaders.containsKey("*")) {
cl = mContext.getClassLoader();
} else {
cl = mLoaders.get(patchName);
}
if (cl != null) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), cl, classes);
}
}
}
loadPatch()方法有两个重载的方法,上面的没有参数的方法会遍历mPatchs这个集合,对所有的Patch文件中的Class都调用一次AndFixManager的fix()方法,下面的有参数的方法就是单一的修复指定Patch文件中的Class字节码,无论是有参还是无参的方法都调用了mAndFixManager.fix()方法,接着来看一下这个方法内部又是如何实现的?
public synchronized void fix(File file, ClassLoader classLoader,
List classes) {
if (!mSupport) {
return;
}
der(),
classes);
}
}
}
/**
-
load specific patch
-
@param patch
-
patch
*/
private void loadPatch(Patch patch) {
Set patchNames = patch.getPatchNames();
ClassLoader cl;
List classes;
for (String patchName : patchNames) {
if (mLoaders.containsKey("*")) {
cl = mContext.getClassLoader();
} else {
cl = mLoaders.get(patchName);
}
if (cl != null) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), cl, classes);
}
}
}
loadPatch()方法有两个重载的方法,上面的没有参数的方法会遍历mPatchs这个集合,对所有的Patch文件中的Class都调用一次AndFixManager的fix()方法,下面的有参数的方法就是单一的修复指定Patch文件中的Class字节码,无论是有参还是无参的方法都调用了mAndFixManager.fix()方法,接着来看一下这个方法内部又是如何实现的?
public synchronized void fix(File file, ClassLoader classLoader,
List classes) {
if (!mSupport) {
return;
}