因工作需要,开始接触了热更新的实现,通过对网上各种热更新原理的了解了,我选择了阿里巴巴的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

下载下来的格式如下

android自研热更新 安卓开发热更新_热更新




其中,zx.keystore、old.apk、new.apk、keystore.txt是我的文件

密码存在了keystore里面

打开cmd
输入命令打开这些文件所在的文件夹,比如我的是F盘的apkpatch,先打开这个文件夹

然后输入

android自研热更新 安卓开发热更新_apkpatch_02

完整如下:

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文件夹

可以看到

android自研热更新 安卓开发热更新_android自研热更新_03

其中那一串命名乱码的就是打包出来的apatch文件

重命名为out.apatch,至于为什么重命名为这个,因为,我们在项目MyApplication里面设置了

private static final String APATCH_PATH = "/out.apatch";

所以如果你需要可以随便改,设置好就行。

然后就可以了,首先安装“old版本”

如图

android自研热更新 安卓开发热更新_andfix_04


然后,将刚刚打出的补丁包,复制粘贴到手机上,当然正式使用时使用下载到手机上是一样的

关闭应用,重新打开,就会变成下面这样

android自研热更新 安卓开发热更新_android自研热更新_05

我们没有对应用进行重新安装,是通过安装补丁包,将代码进行了修改,实现了热更新

这就是我这边初步实现的热更新方案,据了解,使用这个方法无法实现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);
        }

后面如果遇到其他问题,会继续更新,谢谢。