zoukankan      html  css  js  c++  java
  • 热修复 RocooFix篇(一)

    吐槽之前先放一张大帅图.

    (md 这张图貌似有点小 不纠结这个了==)

    有时候项目刚刚上线或者迭代 测试或者在线上使用测出一个bug来 真让人蛋疼 不得不重新改bug测试 打包混淆上线感觉就向findviewById一样让你无法忍受

    热修复从15年开始火起来 关于热修复的理论知识基于QQ空间热修复的一片文章(后面我会附上这几天学习的了解 不想看吐槽的可以滑到最后面 没办法为了凑字

    数不够150个字数不允许发表 难道这就可以阻挡我吐槽的 呸 是学习的热情了吗)

    其实在学习热修复之前 我们还是有必要了解一下热修复的原理 下面开始正经(后面均有链接):

    热修复大致分为两种解决方式:

    PathClassLoader:

       

    DexClassLoader:

      

    官方文档说的很明白了:(我也没看明白 接着查==)

    参考一下stack overflow的回答:

    两者的区别PathClassLoader只能加载本地的classes 而DexClassLoader可以加载apk或者jar压缩包中的dex文件

    需要注意的是:

    就是说DexClassLoader不提倡从sd卡加载 ,(This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getCodeCacheDir() to create such a directory: 需要私人的,可写的目录缓存优化类 也是一些热修复一些生成jar 需要md5加密的原因?)关于这个问题 农民伯伯11年就写了一篇博文:

    Android动态加载jar/dex :http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html

    以及这位 后来没坚持写博客了 http://blog.csdn.net/quaful/article/details/6096951

    (这里忍不住吐槽一下: 我靠 农名伯伯是有多屌啊!可惜不能发表情包 吓得我下巴就要掉下来了!应用这么多图和链接 只是帮助我们了解一下 现在大部分博文不严谨 百度一下全是一样的。写到这里感觉 如果接着写下去的话 这个题目要改为热修复初探! 当然不! 分享才知道自己的不足)

    ...关于热修复 目前大概有以下几种(不严谨 有错误欢迎补充 谢谢. 关于以下几种分别附上链接 以及 demo个人使用总结 部分还在学习中):

    基于Xposed的AOP框架的淘宝的 Dexposed,支付宝的AndFix,QZone的超级热补丁方案(Nuwa 采用了相同的方式),以及微信的Tinker,

    RocooFix(是基于扣扣空间的方案,稳不稳定看下面微信对qq空间方案的评价 本篇后面使用Tomat 完整演示),饿了么的Amigo,以及掌阅的Zeus Plugin等等吧。

    ================以上内容 是初探 ===========================

    关于RocooFix

    在使用RocooFix之前 我们很快找到两种方法:

    静态修复:

    RocooFix.applyPatch(Context context, String dexPath);

    动态修复:

    RocooFix.applyPatchRuntime(Context context, String dexPath);   

    思路:

        出现bug之后 我们使用RocooFix集成 生成patch.jar文件 给后台让其上传到服务器(获取向后台要三个接口 一个上传patch.jar文件 一个用来修改json数据

    一个用来获取到json数据)

      静态修复的话 我们 直接从服务器拿到jar数据 放在sdcard某个位置 app重启自动修复

      动态修复的话 我们可能要多思考几步 就是动态修复完成之后 如何避免重复 下载 需要json权限判断 (代码中会具体有)

      如果同时使用静态修复和动态修复的话 可能会崩溃

    Tomcat 解压之后在webapps目录下新建文件夹(我的是HotFix) .复制ROOT目录下 WEB-INF,丢进HotFix中。

    patch.jar是我们在第二次编译Android Studio version 2 debug目录下生成的jar文件 我们复制到HotFix 目录下

    并且新建b.txt文件 存储json字符串 下载到本地:

    {"md5":"json","patch":"10.0.2.2/HotFix/patch.jar","Root":"wifi","download":"ok"}

    md5 用于txt文本加密 patch模拟服务器patch.jar生成的位置 root 个人感觉是否需要判断 在wifi条件下自动修复 download 本地下载txt文件之后 判断是否是ok 如果ok 动态修复 修复之后 将ok 的值改为其他值 覆盖sdcard的txt文件 避免静态修复 之后启用动态修复 这样会崩掉

    具体见代码:

      App文件:

    package com.example.administrator.myapplication;

    import android.app.Application;
    import android.content.Context;
    import android.os.Environment;
    import android.util.Log;

    import com.dodola.rocoofix.RocooFix;

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;

    import okhttp3.Call;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.Response;

    /**
    * Created by One on 2016/8/31.
    */
    public class App extends Application {

    private String path = "/One1988/data";//项目目录
    private String urlTest = "http://10.0.2.2/HotFix/b.txt";//Tomcat上面的 text文件json数据
    private String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
    path + "/patch.jar";//jar文件 位于 sdcard/One1988/data 目录下

    @Override
    protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    RocooFix.init(base);


    try {
    if (patchPath != null) {
    File file = new File(patchPath);
    if (file.exists()) {
    /**
    * 存在的话修复从制定目录加载静态修复
    */
    try {
    RocooFix.applyPatch(base, patchPath);
    } catch (Exception e) {
    Log.d("file.exist", "热修复出现异常: ");
    }
    } else {
    /**
    * 没有的话下载写入 读取
    */
    String apath = Environment.getExternalStorageDirectory().getAbsolutePath() + path;
    File fileP = new File(apath);
    if(!fileP.getParentFile().exists()){
    fileP.getParentFile().mkdirs();
    }
    getHttp(urlTest);
    }
    }
    } catch (Exception e) {
    Log.d("RocooFix热修复出现问题", e.toString());
    }
    }


    /**
    * 下载写入sdcard json数据 用于控制
    */
    OkHttpClient mOkHttpClient = new OkHttpClient();

    private void getHttp(String url) {

    Request request = new Request.Builder()
    .url(url)
    .build();

    mOkHttpClient.newCall(request).enqueue(new okhttp3.Callback() {

    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    if (response.isSuccessful()) {
    writeSdcard(response, "download.txt");//写入txt文件
    }
    }
    });
    }



    /**
    * 写入txt文件进sdcard实际考虑加密什么的
    * @param response
    */
    private void writeSdcard(Response response, String filePath) {
    InputStream is = null;
    byte[] buf = new byte[2048];
    int len = 0;
    FileOutputStream fos = null;
    String SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data";
    try {
    is = response.body().byteStream();
    File file = new File(SDPath, filePath);
    fos = new FileOutputStream(file);
    long sum = 0;
    while ((len = is.read(buf)) != -1) {
    fos.write(buf, 0, len);
    sum += len;
    }
    fos.flush();
    Log.d("文件下载", "txt文件下载成功");
    } catch (Exception e) {
    Log.d("文件下载", "文件下载失败");
    } finally {
    try {
    if (is != null)
    is.close();
    } catch (IOException e) {
    }
    try {
    if (fos != null)
    fos.close();
    } catch (IOException e) {
    }
    }
    }
    }

    MainActivity文件:package com.example.administrator.myapplication;

    import android.os.Bundle;
    import android.os.Environment;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.util.Log;
    import android.widget.Button;
    import android.widget.Toast;

    import com.dodola.rocoofix.RocooFix;
    import com.example.administrator.myapplication.bean.FileUtils;

    import org.json.JSONObject;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;

    import okhttp3.Call;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.Response;

    public class MainActivity extends AppCompatActivity {

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

    /**
    * 测试Ui
    */
    private String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data/patch.jar";
    private void initUi() {
    Button button = (Button) findViewById(R.id.button);
    Button buttonRun = (Button) findViewById(R.id.buttonRun);
    final Test test = new Test();

    /**
    * 测试立即生效
    */
    button.setOnClickListener(v -> {
    Toast.makeText(MainActivity.this, test.show(), Toast.LENGTH_SHORT).show();
    });

    /**
    * button run测试
    */
    buttonRun.setOnClickListener(v -> {
    try {
    TestRun run = new TestRun();
    String json = read();
    JSONObject obj = new JSONObject(json);
    String download = obj.optString("download");
    Log.d("download字段", "json: " + json);
    Log.d("download字段", "download: " + download);
    if (download.equals("ok")) {
    //下载修复
    //getHttp(urlPatch);
    updateJson();

    } else {
    Toast.makeText(this, run.run(), Toast.LENGTH_SHORT).show();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    });
    }


    /**
    * 插件位于sdcard位置
    */
    private String urlPatch = "http://10.0.2.2/HotFix/patch.jar";

    /**
    * 读取字符串
    * @return
    * @throws Exception
    */
    private String read() throws Exception {//读取sdcard中的json字符串
    String SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data";

    File file = new File(SDPath, "/download.txt");

    StringBuffer sb = new StringBuffer();

    FileInputStream fis = new FileInputStream(file);
    int c;
    while ((c = fis.read()) != -1) {
    sb.append((char) c);
    }
    fis.close();
    String fileString = sb.toString();
    Log.d("FileInputStream:", "file: " + sb.toString());
    return fileString;
    }

    /**
    * Test 测试
    */
    public class Test {

    public String show(){
    return "出现bug!";
    //return "来点不一样的!";
    }
    }

    /**
    * 测试
    */
    public class TestRun {

    public String run(){
    return "测试修复前!";
    //return "bug已经修复了欧耶!";
    }
    }


    /**
    * 下载jar文件立即修复
    */
    private void writeSdcard(Response response, String filePath) {
    InputStream is = null;
    byte[] buf = new byte[2048];
    int len = 0;
    FileOutputStream fos = null;
    String SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data";
    try {
    is = response.body().byteStream();
    File file = new File(SDPath, filePath);
    fos = new FileOutputStream(file);
    long sum = 0;
    while ((len = is.read(buf)) != -1) {
    fos.write(buf, 0, len);
    sum += len;
    }
    fos.flush();
    Log.d("文件下载", "jar文件下载成功");
    RocooFix.applyPatchRuntime(this, patchPath);
    mHandler.sendEmptyMessage(0);
    } catch (Exception e) {
    Log.d("文件下载", "文件下载失败");
    } finally {
    try {
    if (is != null)
    is.close();
    } catch (IOException e) {
    }
    try {
    if (fos != null)
    fos.close();
    } catch (IOException e) {
    }
    }
    }

    /**
    * 下载
    */
    OkHttpClient mOkHttpClient = new OkHttpClient();

    private void getHttp(String url) {

    Request request = new Request.Builder()
    .url(url)
    .build();

    mOkHttpClient.newCall(request).enqueue(new okhttp3.Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    if (response.isSuccessful()) {
    writeSdcard(response, "patch.jar");
    }
    }
    });
    }

    /**
    * Handler
    */
    private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what){
    case 0:
    TestRun run = new TestRun();
    Toast.makeText(MainActivity.this, run.run(), Toast.LENGTH_SHORT).show();

    /**
    * 我的想法是:
    */
    try {
    String json = read();
    if(json.contains("ok")){
    json.replaceAll("ok","nohttp");
    }

    File file = new File(patchPath);
    if(file.exists()){
    FileUtils.writeFileFromString(file,json,false);
    }
    Log.d("读取到的字符串:", "handleMessage: "+read());
    } catch (Exception e) {
    e.printStackTrace();
    }
    break;
    default:
    break;
    }
    }
    };

    /**
    * 测试更改sdcard中的字符串
    */
    String download = Environment.getExternalStorageDirectory().getAbsolutePath() + "/One1988/data/download.txt";
    private void updateJson() {
    try {
    String read = read();
    Log.d("写入字符串", "之前: "+read);
    /*Gson gson = new Gson();
    Bean bean = gson.fromJson(read, Bean.class);
    if(bean.getDownload().equals("ok")){
    bean.setDownload("哇咔咔");
    }
    String write = gson.toJson(bean);*/

    /**
    * 我尝试用上面方法修改json字符串但是 加入gson依赖之后
    * 添加混淆会出错 这种比较麻烦用于学习 后面会学习一下其他的几种方式
    * 选择一种最好的混淆方式 如果这里你解决了 请告诉我 谢谢!
    */

    JSONObject object = new JSONObject();
    object.put("md5","json");
    object.put("patch","10.0.2.2/HotFix/patch.jar");
    object.put("Root","wifi");
    object.put("download","成功修改后!");
    String write = String.valueOf(object);
    Log.d("写入字符串", "之前: ======");
    Log.d("写入字符串", "之前: "+write);
    write(write,download);
    Log.d("写入字符串", "之后: "+read());
    } catch (Exception e) {
    e.printStackTrace();
    }
    }


    /**
    * 写入字符串到txt文件
    * @param toSaveString
    * @param filePath
    */
    public static void write(String toSaveString, String filePath) {
    try{
    File saveFile = new File(filePath);
    if(!saveFile.exists()){
    File dir = new File(saveFile.getParent());
    dir.mkdirs();
    saveFile.createNewFile();
    }
    FileOutputStream outputStream = new FileOutputStream(saveFile);
    outputStream.write(toSaveString.getBytes());
    outputStream.close();
    }catch (Exception e){
    Log.d("字符串写入失败", "saveFile: "+e.toString());
    }
    }

    }其他配置 等具体见demo这种比较麻烦的是 考虑混淆问题 后面学习Amigo的使用比较几种热修复的优点。记录一下。
      

    QQ空间终端开发团队:(引发热潮的一篇文章 文中的图片我就不引用了 大神请绕道)

    https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a

    PathClassLoader和DexClassLoader官方文档:

    https://developer.android.com/reference/dalvik/system/PathClassLoader.html

    https://developer.android.com/reference/dalvik/system/DexClassLoader.html

    stack overflow 上关于PathClassLoader和DexClassLoader的不同

    http://stackoverflow.com/questions/37296192/what-are-differences-between-dexclassloader-and-pathclassloader

    饿了么:https://github.com/eleme/Amigo 

    掌阅:https://github.com/iReaderAndroid/ZeusPlugin 

    女娲: https://github.com/jasonross/Nuwa

    RocooFix:https://github.com/dodola/RocooFix

    Tomact 8.5.4 windows 64:

    http://download.csdn.net/detail/onebelowzero2012/9626103

    demo 以及txt文件:

    http://download.csdn.net/detail/onebelowzero2012/9628341

    最后:欢迎给出意见 一起学习 加入群Android&Go,Let's go! 521039620 (感觉自己像个拉皮条的 ==)

    =======2017/06/06 补记 现在回过头来看 感觉写的太浅 希望不要误导读者 自己多尝试 关于修复这块 建议大家用微信Tinker 或者 参考腾讯Bugly 的Tinker集成使用 谢谢大家。

  • 相关阅读:
    Dynamics 365 多租户?多实例?
    接口接收gzip压缩数据并解压
    系统检测到在一个调用中尝试使用指针参数时的无效指针地址 问题
    PBI DAX 中GroupBy
    将sql 查询结果导出到excel
    自动生成数据库表分区脚本
    快速生成导入亿级测试数据到sqlserver
    powershell 版本问题
    运行powershell 脚本 在此系统上禁止运行脚本
    python网站收集
  • 原文地址:https://www.cnblogs.com/yizuochengchi2012/p/5864423.html
Copyright © 2011-2022 走看看