Android 热修复值 AndFix

723 阅读7分钟

            最近我看到身边的小伙伴在看android热修复相关的文章,正好趁着休息的时间我在掘金社区看到了一篇讲android热修复的文章,于是我仔细读完了那篇文章。在读完那边文章之后,我觉得写一个实现热修复的测试demo很简单呀!于是我又多找了几篇文章度了一下,加深了一下印象,我看到身边的小伙伴被一个cmd命令折磨得怒抓头皮的样子,于是决定自己也来写一个demo测试玩玩,也就有了下面的这篇文章。

            我使用的是阿里团队做的一套热修复框架AndFix,在这里我就不讲热修复到底是怎么实现的,也不探讨QQ空间团队给出的热修复解决方案。

            AndFix框架地址:github.com/alibaba/And…

接下来,我就讲讲我的尝试过程。

一、  创建新的android应用,我使用的是AndroidStudio,然后将框架配置到应用之中,配置方式有两种:

1、       通过build.gradle里导入andfix

compile 'com.alipay.euler:andfix:0.3.1'

2、       通过module的方式添加andfix

此处我使用的是第二种方法,因为可以直接查看和编辑源码,我也在一些博客中看到使用gradle有一个bug,下面再细讲这个bug以及怎么处理这个问题。

二、  建议使用导入module的方式,我们需要在app所在module中创建jniLibs文件夹,然后将armeabix86两个文件夹拷贝到刚刚创建的文件夹中,目录结构如下图所示:


当然从github上下载的andfix文件中的toolsdoc等文件可以不用导入。

三、  我们需要自定义Application,在自定义的Application中我们需要添加对补丁文件的加载实现,源码如下:

public class MyApplication extends Application{

    private static final String TAG = "MyApplication";

    /**
     * apatch文件
     */
    private static final String APATCH_PATH= "/Out.apatch";

    private PatchManagermPatchManager;

    @Override
    public void onCreate(){
        super.onCreate();
        // 初始化
        mPatchManager = new PatchManager(this);
        mPatchManager.init("1.0"); // 版本号

        // 加载 apatch
        mPatchManager.loadPatch();

        //apatch文件的目录
        StringpatchFileString = Environment.getExternalStorageDirectory().getAbsolutePath()+ APATCH_PATH;
        File apatchPath = new File(patchFileString);

        if (apatchPath.exists()){
            Log.e(TAG, "补丁文件存在");
            Toast.makeText(getApplicationContext(),"补丁文件存在", Toast.LENGTH_SHORT).show();
            try {
                //添加apatch文件
                mPatchManager.addPatch(patchFileString);
            } catch (IOExceptione) {
                Log.e(TAG, "打补丁出错了");
                error =e.toString();
                Toast.makeText(getApplicationContext(),"打补丁出错了"+e.toString(), Toast.LENGTH_SHORT).show();
                e.printStackTrace();
            }
        } else {
            Log.e(TAG, "补丁文件不存在");
            Toast.makeText(getApplicationContext(),"补丁文件不存在", Toast.LENGTH_SHORT).show();
        }

    }
}

四、  接下来,我们需要模拟一下应用出bug的场景,我们就以简单的TextView提示内容来当作应用的出bug和正常运行的情况,源码如下所示:

public class MainActivity extends AppCompatActivity {

    private Button button, patchBtn;
    private TextView textTV;
    private MyApplication app;
    private static final String APATCH_PATH = "Out.apatch";

    private final String url = "http://192.168.31.163/Out.apatch";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        app = (MyApplication) getApplication();
        button = (Button) findViewById(R.id.button);
        patchBtn = (Button) findViewById(R.id.getpathc);
        textTV = (TextView) findViewById(R.id.text);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                textTV.setText("APP出bug啦!");
                textTV.setText("已经修复bug啦!");
            }
        });

        patchBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                downloadPatch(url);
            }
        });
    }

    private void downloadPatch(final String url) {
        Toast.makeText(MainActivity.this, "开始下载", Toast.LENGTH_SHORT).show();
        new Thread() {
            @Override
            public void run() {
                HttpClient client = new DefaultHttpClient();
                HttpGet get = new HttpGet(url);
                HttpResponse response;
                try {
                    response = client.execute(get);
                    HttpEntity entity = response.getEntity();
                    int fileSize = Integer.parseInt(entity.getContentLength() + "");
                    int downloadSize = 0;
                    InputStream is = entity.getContent();
                    FileOutputStream fos = null;
                    if (is != null) {
                        File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), APATCH_PATH);
                        fos = new FileOutputStream(file);
                        byte[] buf = new byte[1024];
                        int ch = -1;
                        while ((ch = is.read(buf)) != -1) {
                            fos.write(buf, 0, ch);
                            downloadSize += ch;
                        }

                        fos.flush();
                        if (fos != null) {
                            if (fileSize == downloadSize) {
                                sendMsg(0);
                            }
                            fos.close();
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    private Handler update_handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (!Thread.currentThread().isInterrupted()) {
                switch (msg.what) {
                    case 0:
                        //设置进度条最大值
                        Toast.makeText(MainActivity.this, "补丁下载完成", Toast.LENGTH_SHORT).show();
                        break;
                    default:
                        break;
                }
            }
            super.handleMessage(msg);
        }
    };

    private void sendMsg(int flag) {
        Message msg = new Message();
        msg.what = flag;
        update_handler.sendMessage(msg);
    }
}

小伙伴们一眼就能看出来,我是通过TextView提示的内容来判断应用有没有bug。其中的downloadPatch()方法就是我用来实现从服务器下载补丁包到手机sdcard上的方法,如果小伙伴要运行就需要自己搭建一个简单的服务器,功能不用多复杂只要能成功下载文件就ok!这里我就不讲怎么搭建服务器,网上教程多得是,小伙伴动动手指就能找到很多教程。

五、  前面我们说了在gradle里导入andfix会有个问题,是在原来的项目中,加载一次补丁后,out.apatch文件会copygetFilesDir目录下的/apatch文件夹中,在下次补丁更新时,会检测补丁是否已经添加在apatch文件夹下,已存在就不会复制加载sdcardout.apatch,所以我们需要对框架中patch文件下的PatchManager类中的addPatch()方法进行修改。原来的方法如下:

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);
    }
}

修改后,判断apatch下的out.apatch存在即删除掉,重新复制加载sdcard下的out.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 [" + src.getName() + "] has be loaded.");
        boolean deleteResult = dest.delete();
        if (deleteResult)
            Log.e(TAG, "patch [" + dest.getPath() + "] has be delete.");
        else {
            Log.e(TAG, "patch [" + dest.getPath() + "] delete error");
            return;
        }
    }
    FileUtil.copyFile(src, dest);// copy to patch's directory
    Patch patch = addPatch(dest);
    if (patch != null) {
        loadPatch(patch);
    }
}

六、  我们需要将应用打包,需要对包进行签名,同时这里需要两个应用包,一个是有bug的应用包,一个是修复之后的应用包。然后,我们需要用到apkpatch这个工具,这个工具在框架的github地址里面有。


         apkpatch.sh是在Linux下使用的脚本文件,apkpatch.bat是在window的命令行中使用的批处理文件。

七、  准备好两个安装包和apkpatch工具,将两个安装包都放到apkpatch这个文件夹里面,方便我们使用命令生成补丁包,我使用的是mac,所以使用apkpatch.sh来生成补丁包,完整命令如下:

./apkpatch.sh -f [新包]–t [旧包] –o [输出文件名称]–k [签名文件] –p [签名文件密码]–a [签名文件别名] –e [别名密码]

例如:

./apkpatch.sh -f NoBug.apk -t Bug.apk -o Out-k /Users/cyril/Desktop/key/andfix.jks -p cyril998 -a andfix -e cyril998

当你的命令行出现如下截图的内容,并且在apkpatch文件夹生成如下的文件夹,就表明补丁文件生成成功。



我们需要将.apatch文件改名为Out.apatch

八、  不要忘了把补丁文件放到服务器的下载地址里面去。接着我们在手机上安装有bug的应用,运行截图如下:


看到了吗?程序已经出错了,然后我们点击下载补丁按钮,从实现写定的服务端下载补丁包,出现如下截图之后,我们就可以将应用完全退出,最好是清一下后台。



为了确保下载成功,我们进入应用的内存空间去看看,有没有我们下载的补丁包,如下图所示:


上图表明我们已经下载成功了。接下来,我们退出应用并清空后台,然后再次打开应用,点击运行异常按钮,运行结果如下图所示:


上图告诉我们我们的热修复实验成功了!

九、  一些不细心的小伙伴会发现怎么我点击下载补丁并没有看到下载成功的Toast呢?怎么我讲补丁包手动放到内存里面还是提示bug呢?

1、不要忘了涉及到网络请求、内存读写需要设置权限的,因为小编就是从这个坑爬起来的,我当时忘了加权限,反复测试了很久,最后突然想起来我没有添加权限,在AndroidManifest.xml文件中添加如下权限



         2、因为需要使用HttpClient,所以需要导入相应的jar包,需要在applib中导入如下的jar包,


同时还需要在build.gradle中配置

compile files('libs/org.apache.http.legacy.jar')

十、  总结

1、       andfix只能对一些逻辑进行修复,不能修改资源和布局文件;

2、       我测试过加固的情况,加固不应用热修复的使用,但是有一点用来生成.apatch包的两个安装包都不能加壳,也就是在加壳之前生成,如果应用出了问题需要打补丁,就必须找到有问题并且未加固的安装包。另外,在加固前制作的补丁可以很容易的被反编译出源码,也就说增加了新的安全性问题。

3、       我使用了4.4.4系统的手机测试是没有问题的,但是5.0以上的手机没有一次成功过,我使用了一部5.1和一部6.0系统的手机全都crash掉了,错误如下:

 java.lang.UnsatisfiedLinkError:No implementation found for booleancom.alipay.euler.andfix.AndFix.setup(boolean, int)

(triedJava_com_alipay_euler_andfix_AndFix_setup andJava_com_alipay_euler_andfix_AndFix_setup__ZI)。

 

其实框架中.so文件只提供了两个版本,并没有提供适用于arm64-v8aarmebia-v7.so文件,所以在这些类型的cpu下当然找不到相关的方法。