因工作需要,开始接触了热更新的实现,通过对网上各种热更新原理的了解了,我选择了阿里巴巴的AndFix这个个热更新的实现,因为在我的了解上,这个比较简单适用,在手机端代码上的量比较少。如果不对,欢迎指正,别打脸。
好,现在开始流程,我使用的是Android Studio
先进行相关包的导入
compile 'com.alipay.euler:andfix:0.3.1@aar'
然后配置MyApplication类
/**
* Created by Xiangb on 2016/11/21.
* 功能:
*/
public class MyApplication extends Application {
private PatchManager patchManager;
private static final String TAG = "euler";
private static final String APATCH_PATH = "/out.apatch";
private static final String DIR = "apatch";//补丁文件夹
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onCreate() {
super.onCreate();
String version = "";
try {
String Version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
patchManager = new PatchManager(getApplicationContext());
patchManager.init(version);
patchManager.loadPatch();
try {
// .apatch file path
// String patchFileString = "/mnt/sdcard" + APATCH_PATH;
String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
patchManager.addPatch(patchFileString);
Log.d(TAG, "apatch:" + patchFileString + " added.");
//这里我加了个方法,复制加载补丁成功后,删除sdcard的补丁,避免每次进入程序都重新加载一次
File f = new File(this.getFilesDir(), DIR + APATCH_PATH);
if (f.exists()) {
boolean result = new File(patchFileString).delete();
if (!result)
Log.e(TAG, patchFileString + " delete fail");
}
} catch (IOException e) {
Log.e(TAG, "", e);
}
}
}
请注意其中的
String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
这句话,因为我的手机是公司的开发手机,是没有内存卡的,补丁文件存放的位置不好设置,请根据你开发的具体情况,设置一个可以获取到的地址。
以及上面的
String version = "";
try {
String Version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
patchManager = new PatchManager(getApplicationContext());
patchManager.init(version);
这一段,是先获取到版本号,系统会判断版本号,只有相同的版本号的时候会执行热更新
对了,注意要写权限,要写权限,权限。
因为我是直接新建了一个项目来做demo,结果忘记了添加文件读写的权限,结果在处理文件流的时候各种出问题。
然后,我们在MainActivity中写一个方法,比如
private void toast() {
Toast.makeText(this, "old", Toast.LENGTH_SHORT).show();
}
然后打包,重命名为old.apk
然后再改为
private void toast() {
Toast.makeText(this, "new", Toast.LENGTH_SHORT).show();
}
命名为new.apk
好,接下来就是最关键的步骤了,先下载apkpatch
下载下来的格式如下
其中,zx.keystore、old.apk、new.apk、keystore.txt是我的文件
密码存在了keystore里面
打开cmd
输入命令打开这些文件所在的文件夹,比如我的是F盘的apkpatch,先打开这个文件夹
然后输入
完整如下:
apkpatch.bat -f new.apk -t old.apk -o output -k zx.keystore -p zxdl.digitalcq.com -a zxandroidkey -e zxdl.digitalcq.com
其中,
-f 是新apk的名字
-t 是旧apk的名字
-o 是输出补丁的文件夹位置
-k 是keystore文件的名称
-p 是keystore文件的密码
-a 是项目的别名
-e 是项目的打包的另一个密码
后面四个是在你进行apk签名打包的时候设置的参数,另外如果没有密码的时候怎么设置的我还没有研究过,你们可以研究一下,你们可以暂时用我的就好了
敲击回车就会出现最后一排的那句提示,如果没有报错,就说明,补丁打包成功
打包成功后,打开文件夹里面的output文件夹
可以看到
其中那一串命名乱码的就是打包出来的apatch文件
重命名为out.apatch,至于为什么重命名为这个,因为,我们在项目MyApplication里面设置了
private static final String APATCH_PATH = "/out.apatch";
所以如果你需要可以随便改,设置好就行。
然后就可以了,首先安装“old版本”
如图
然后,将刚刚打出的补丁包,复制粘贴到手机上,当然正式使用时使用下载到手机上是一样的
关闭应用,重新打开,就会变成下面这样
我们没有对应用进行重新安装,是通过安装补丁包,将代码进行了修改,实现了热更新
这就是我这边初步实现的热更新方案,据了解,使用这个方法无法实现res文件的更新,所以,这个适用于小型代码bug的修改。
亲测是可以用的,里面遇到的最多的问题就是文件路径以及补丁包打包的实现。
如果有问题欢迎提出。
---------------------------------------------2016.11.22更新--------------------------------------------------
以下是应用测试
方法的修改 成功
方法的修改 成功
方法的增加 成功
方法的删除 成功
新建类文件 失败
删除类文件 失败
删除字段 成功
新建内部类 失败
更改res文件 失败
在方法中调用资源文件(图片、id等) 成功
总结:
无法新增和R文件相关的包括类和字段以及res文件
无法修改res文件
可以调用已有的R相关文件,包括mipmap、drawable、layout等
类似于id已存在的情况下可以使用findviewbyid来获取控件并设置属性
新增类时打补丁不会报错,运行会报错
综上所述,该热更新适用于代码bug的修改(不涉及新的类或字段)
---------------------------------------------2016.11.23更新--------------------------------------------------
针对多次代码更新出错,系统无法 更新第二次的解决方案
因为手机在进行过一个更新后,会把.apatch文件复制到data/data文件夹,下一次启动后会检查是否存在该.apatch文件,如果存在,就不会进行更新,但是这也就导致了,第二次打补丁如果文件名和原有.apatch文件相同,就会出现不更新的问题,比较直观的解决办法就是每次补丁都采用不同的命名,但是这样就需要在程序中做相关变动,且极其容易产生更多的问题,比如程序中名称与文件名称不同,导致更新失败的问题
我们查看热更新的源码可以看到如下的代码:
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);
}
}
其中的dest就是第一次热更新的时候,将sdcard中的.apatch文件copy到了data/data中的位置
在中间有个判断是否dest这个文件存在
如果存在就会直接做一个return的操作,而不会继续进行下面的loadPatch的操作
在也就导致了,如果更新了一次后,再次进行热更新,如果.apatch的文件名不变,应用不会进行第二次修改
经过测试,两次更新如果采用不同的.apatch命名,就可以进行第二次更新,可以证实上面的判断
虽然依赖库中提供了removeAllPatch,但是调用这个方法会导致里面的共享参数也被清除了
public void removeAllPatch() {
cleanPatch();
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
sp.edit().clear().commit();
}
所以导致了即使调用了这个方法,但如果命名还是不变,仍然会出问题。
我预想了以下的解决方案
打开应用,先调用接口,或者后台提供的补丁信息,需要补丁下载位置以及补丁的名称,这个名称需要每个补丁都是单独的命名,比如fix1.apatch,fix2.apatch,每次将这个补丁名保存到SharedPreference上,然后重启应用,在MyApplication中加载补丁的时候,提取这个命名,去获取对应的补丁,调用removeAllPatch,再加载这个补丁,这样就相当于应用第一次加载补丁。
目前还没有进行测试,后期会慢慢的进行一个测试。
---------------------------------------------2016.11.24更新--------------------------------------------------
经过我的测试,预想成立,过程如下
在测试的类中,创建一个字符数组
private String[] texts = {"初始","第一","第二","第三"};
我的layout上有三个控件,一个TextView,一个EditText,一个Button
TextView是用于显示初始、第一、第二、第三这几个字符,用于判断应用是否实现了热更新
EditText用于输出补丁的版本号,比如输入1,2,3,分别对应了out1.apatch,out2.apatch,out3.apatch
Button用于将输入的信息存入到SharedPreference中
如下
fixBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
editor.putString("fixNum", fixEdit.getText().toString()).commit();
Toast.makeText(ClipDrawable.this, "修改成功", Toast.LENGTH_SHORT).show();
}
});
在MyApplication中,我做了如下的变化
APATCH_PATH = "/out"+preferences.getString("fixNum", "")+".apatch";
从SharedPreference中获取补丁的版本号
String patchFileString = "/mnt/sdcard" + APATCH_PATH;
if (new File(patchFileString).exists()) {
Log.e("path存在", patchFileString);
patchManager.removeAllPatch();
patchManager.addPatch(patchFileString);
} else {
Log.e("path不存在", patchFileString);
}
判断如果对应文件存在,先清空所有已存在的apatch文件
再进行addPatch的操作,此时加载的补丁号就是我们在EditText中输入的信息。
好,现在开始,首先将
fixText.setText(texts[0]);
这句代码中的0依次变成1,2,3
打出各自对应的apk文件,再分别与原始版本0打成三个补丁文件out1.apatch,out2.apatch,out3.apatch
再将这三个文件都放进手机中
手机上的应用为0这个版本,也就是显示的为”初始“
重启发现也没有任何变化,这是因为,目前得到的路径获取的补丁号为空
然后在EditText中输入1,点击确定,SharedPreference中的fixNum被修改成了1,再次重启应用
应用会去寻找out1.apatch这个文件,成功找到!
进行热更新
应用中可以看到显示为“第一”
这是第一步,因为这只是第一次更新,本来就可以成功
接下来就是关键,测试第二次,甚至第三次更新能否成功
在原来,我们如果进行第二次更新,是无法成功的
好,我们再打开应用,输入2,重启应用
打开发现,界面显示为第二
依法炮制,界面显示为第三
至此,我们可以肯定,这个方法可以成功的实现应用的多次更新
下面,稍微贴一下相关代码
fixEdit = (EditText) findViewById(R.id.fixEdit);
fixText = (TextView) findViewById(R.id.fixText);
fixText.setText(texts[0]);
fixBtn = (Button) findViewById(R.id.fixBtn);
fixEdit.setText(preferences.getString("fixNum", ""));
fixBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
editor.putString("fixNum", fixEdit.getText().toString()).commit();
Toast.makeText(ClipDrawable.this, "修改成功", Toast.LENGTH_SHORT).show();
}
});
SharedPreferences preferences = getSharedPreferences("fix", MODE_APPEND);
APATCH_PATH = "/out"+preferences.getString("fixNum", "")+".apatch";
try {
// .apatch file path
String patchFileString = "/mnt/sdcard" + APATCH_PATH;
// String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
if (new File(patchFileString).exists()) {
Log.e("path存在", patchFileString);
patchManager.removeAllPatch();
patchManager.addPatch(patchFileString);
} else {
Log.e("path不存在", patchFileString);
}
//这里我加了个方法,复制加载补丁成功后,删除sdcard的补丁,避免每次进入程序都重新加载一次
File f = new File(this.getFilesDir(), DIR + APATCH_PATH);
if (f.exists()) {
boolean result = new File(patchFileString).delete();
if (!result) {
Log.e(TAG, patchFileString + " delete fail");
}
}
} catch (IOException e) {
Log.e(TAG, "", e);
}
后面如果遇到其他问题,会继续更新,谢谢。