一、前言
终于毕业开始工作啦。感觉工作跟学校还是有挺大的区别的~还是学校比较自由,看到什么新鲜东西就可以随时折腾,工作相对就严肃很多了,毕竟为了求稳。
研究热修复技术是因为前两天组长突然跟我提起这个,建议可以学习一下。于是就有了这篇东西。(本篇文章仅对新手有用哈)
二、热修复原理
看了鸿洋大神的文章,大概清楚了热修复技术的原理。这边就简单介绍一下,我们需要知道的是Android的ClassLoader体系。主要分为PathClassLoader和DexClassLoader。DexClassLoader主要从jar包、apk文件中加载classes.dex文件,用来执行非安装的程序代码。而PathClassLoader主要用来加载已经安装到系统中的apk文件,Android系统也主要使用它来作为类加载器。在这俩类的父类BaseDexClassLoader里有个叫findClass的方法,源码如下:
#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
#DexPathList
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
大概意思就是用类加载器遍历pathList集合,去加载对应的类。所以热修复的基本原理也就出来啦,我们可以在补丁中放上修复好的类文件,然后在加载的时候选择补丁中的类来代替原apk中的类文件。这样就可以达到修复bug的目的啦!
三、AndFix简单实践
目前已有的热修复技术有很多种,这里就选用AndFix作为示例。首先去github上下载AndFix源码,解压导入工程
(这里我删掉了samples、docs和tools文件夹,无任何影响)我们需要用这个工程生成aar文件作为依赖,我们只需要rebuild下project就可以了。生成的aar文件在build文件夹下的output里。
之后就可以新建自己的工程啦,将生成的aar文件放置在libs目录下,并在build.gradle里进行如下配置:
repositories {
flatDir {
dirs 'libs'
}
compile(name:'andfix0.5.0', ext:'aar')
完整配置如图:
待所有配置完成后,点击重新编译一次工程,完成后,若成功可在External Libraries内看到我们的相关依赖。
至此我们所有的准备工作都已完成,可以开始尝试使用AndFix了,首先根据官方文档
我们需要在Application中进行初始化操作,并且一开始就loadPath
public class MyApplication extends Application {
public PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
mPatchManager = new PatchManager(this);
mPatchManager.init(VERSION_NAME);
mPatchManager.loadPatch();
}
}
然后我们在需要的地方进行addPath(path)操作就可以了。path为补丁存放路径,我暂时将他放在了根目录上
private static final String APATCH_PATH = "/fix.apatch"; // 下载下来的apatch的路径
String patchFilePath = Environment.getExternalStorageDirectory()
.getAbsolutePath() + APATCH_PATH;
try {
mPatchManager.addPatch(patchFilePath);
} catch (IOException e) {
e.printStackTrace();
}
接下来就要生成补丁文件进行试验了,在MainActivity里就放了一个TextView,设置文本为”old version”,这就是我们的1.0版本。接下来打上签名,这里顺便为像我这样的新手们普及下如何签名,点击Build->Generate Signed APK
点击Create New
依次填写
完成后应该是这样的
输入两个密码后点击next
选择release版,点击finish就会完成签名,在对应的目录下就能找到签名后的apk,重命名为1.apk。接下来我们将textview中的文本设为”new version”,再按照上述步骤生成新的签名apk,重命名为2.apk。
接下来就要用阿里提供的apkpatch工具生成补丁文件了,首先把两个补丁文件和签名文件放到tools文件下,然后用cmd指令进入到tools目录下,输入例如如下指令:
apkpatch -f 2.apk -t 1.apk -o output -k abc.jks -p 123456 -a abc -e 1234567
usage: apkpatch -m <apatch_path...> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore entry password.
-k,--keystore <loc> keystore path.
-m,--merge <loc...> path of .apatch files.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
完成后如下图所示
完成后我们可以在目录下的output文件夹里找到一个以.apatch结尾的文件,这就是我们需要的补丁文件。
现在可以来验证我们的热修复是否有效了,先将1.0apk安装至手机,然后将补丁文件放置手机根目录,这里我推荐用adb指令,简单举例
adb push D:\out.apatch /system/sdcard0/
之后再打开应用,发现已经变成”new version”了。
四、使用WampServer构建局域网服务器
到这里我们已经感受到andfix的效果了,为了更好地模拟真实打补丁的环境,我打算让手机从服务器上下载补丁文件进行热修复,这里同学推荐使用WampServer构建了局域网的服务器,不得不说WampServer用起来确实很方便。
首先去官网http://www.wampserver.com/ 下载WampServer,安装完成后,启动WampServer,看不惯英文的同学可以右键->language->chinese。
在浏览器中输入localhost,如果显示下图就说明成功了
然后左键打开www目录,我们把补丁文件拷贝到这里,为了更好的模拟,我将补丁重新命名为1.1.apatch,图中的test.php为我们的接口
test.php源码
<?php
echo '{"downloadUrl":"http://192.168.1.100/1.1.apatch","version":"1.1"}';
?>
至此,我们服务器环境已经搭建完成。用手机或者另一台电脑访问这个文件(记得关闭防火墙),如果访问不成功,将apache中的httpd-vhost.conf配置如下
# Virtual Hosts
#
<VirtualHost *:80>
ServerName localhost
DocumentRoot D:/wamp64/www
<Directory "D:/wamp64/www/">
Options +Indexes +Includes +FollowSymLinks +MultiViews
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
#
如果还是不行,可以上网查查httpd的一些配置,基本上目的就是允许其他地址进行访问。
上述都完成后,我们开始对客户端进行代码修改,这里使用xutils框架进行补丁下载。大概逻辑是这样的,我们将从服务器给的接口中获取一个补丁版本号,如果该版本号与本地读取的不同,则下载补丁,补丁下载并修复完成后删除补丁并将最新的版本号写入本地SP中。具体代码如下:
package com.example.administrator.andfixdemo.application;
import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Environment;
import com.alipay.euler.andfix.patch.PatchManager;
import com.example.administrator.andfixdemo.Bean.PatchBean;
import com.example.administrator.andfixdemo.Constants.DownloadConstants;
import com.example.administrator.andfixdemo.utils.SPUtils;
import com.google.gson.Gson;
import org.xutils.common.Callback;
import org.xutils.http.RequestParams;
import org.xutils.x;
import java.io.File;
import java.io.IOException;
/**
* Created by yaochen on 2016/9/22.
*/
public class MyApplication extends Application {
public static final String TAG = "MyApplication";
private static final String UPDATE_URL = "http://192.168.1.100/test.php";
private static final String BASE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/";
private static final String SUFFIX = ".apatch";
public static String VERSION_NAME = "";
public static PatchManager mPatchManager;
private String currentVersion;
private String latestVersion;
private String patchFilePath;
private Gson gson;
@Override
public void onCreate() {
super.onCreate();
x.Ext.init(this);
x.Ext.setDebug(false);
gson = new Gson();
try {
PackageInfo packageInfo = getPackageManager()
.getPackageInfo(getPackageName(), 0);
VERSION_NAME = packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
initAndFix();
checkPatch();
}
private void initAndFix() {
mPatchManager = new PatchManager(this);
mPatchManager.init(VERSION_NAME);
mPatchManager.loadPatch();
}
private void checkPatch() {
final RequestParams params = new RequestParams(UPDATE_URL);
x.http().get(params, new Callback.CommonCallback<String>() {
@Override
public void onSuccess(String s) {
// Log.d(TAG, "onSuccess");
PatchBean patch = gson.fromJson(s, PatchBean.class);
// 获取当前版本号
currentVersion = (String) SPUtils.get(x.app(), null, DownloadConstants.PATCH_VERSION, VERSION_NAME);
// Log.d(TAG, "currentVersion:" + currentVersion);
if (!currentVersion.equals(patch.getVersion())) { // 如果不是最新版本则进行补丁下载
latestVersion = patch.getVersion(); // 暂存最新版本号
downloadPatch(patch.getVersion(), patch.getDownloadUrl());
}
}
@Override
public void onError(Throwable throwable, boolean b) {
// Log.d(TAG, "onError:" + throwable.getMessage());
}
@Override
public void onCancelled(CancelledException e) {
// Log.d(TAG, "onCancelled");
}
@Override
public void onFinished() {
// Log.d(TAG, "onFinished");
}
});
}
private void downloadPatch(String patchVersion, String downloadUrl) {
RequestParams requestParams = new RequestParams(downloadUrl);
requestParams.setSaveFilePath(BASE_PATH + patchVersion + SUFFIX);
// Log.d(TAG, "path:" + BASE_PATH + patchVersion + SUFFIX);
x.http().get(requestParams, new Callback.ProgressCallback<File>() {
@Override
public void onWaiting() {
// Log.d(TAG, "onWaiting");
}
@Override
public void onStarted() {
// Log.d(TAG, "onStarted");
}
@Override
public void onLoading(long total, long current, boolean isDownloading) {
// Log.d(TAG, "onStarted");
}
@Override
public void onSuccess(File file) {
// Log.d(TAG, "onSuccess");
patchFilePath = BASE_PATH + latestVersion + SUFFIX;
try {
mPatchManager.addPatch(patchFilePath);
} catch (IOException e) {
e.printStackTrace();
}
File downloadFile = new File(patchFilePath);
if (downloadFile.exists()) {
downloadFile.delete();
}
// 存储最新版本号
SPUtils.put(x.app(), null, DownloadConstants.PATCH_VERSION, latestVersion);
}
@Override
public void onError(Throwable throwable, boolean b) {
// Log.d(TAG, "onError");
}
@Override
public void onCancelled(CancelledException e) {
// Log.d(TAG, "onCancelled");
}
@Override
public void onFinished() {
// Log.d(TAG, "onFinished");
}
});
}
}
至此,我们已经完成了从服务器下载补丁并且进行热修复的过程。
源码地址 http://download.csdn.net/detail/cc65431362/9641293