吐槽之前先放一张大帅图.
(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 (感觉自己像个拉皮条的 ==)