您的位置:首页 > 移动开发 > Android开发

Android开发,热更新的实现与探讨(AndFix)

2016-11-21 16:54 316 查看
因工作需要,开始接触了热更新的实现,通过对网上各种热更新原理的了解了,我选择了阿里巴巴的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);
}
后面如果遇到其他问题,会继续更新,谢谢。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐