前言
不知道同学们有没有遇到这些时候:
1.需要在某个时刻,获取某个本地数据,而重新走流程debug又比较麻烦;
2.你需要临时清理一个数据,但app当前流程,并不提供这样的操作;
3.想在程序加段代码,代码要依赖app当前状态,但不知道代码跑不跑得通,于是在某处加入代码,编译运行,走流程...如果代码失败,还得重复上述步骤;
4.等等...
简单地说,就是想在app运行的某个时刻,运行一小段代码,于是不得不重新编译、运行、走流程。当然场景还有很多很多,不一一而足。
本篇笔者介绍一个方法,可以让你遇到这些情况,有更轻松、灵活地调试app。
一个场景
调试需求:
1.浏览一个app页面,先显示本地缓存,再请求接口更新数据,并显示;
2.当有bug发生,需要重新走一遍这个流程debug;
3.必须先清空本地缓存,再重新走流程。
问题:
如何清空本地缓存?
方案:
1.在系统清除整个app数据,并重新登录、走流程;
2.在代码中加入清空缓存逻辑,重新编译运行,并在某个事件下触发这段逻辑,并且判断BuildConfig.DEBUG==true才能执行(或者上线前注释掉);
3.写一段清空缓存代码,能立即在app上执行,并不影响原来代码。
探讨方案:
1.方案一,最笨拙的方法,好处是不需要写任何代码。坏处是每次需要重新登陆、走流程。如果流程太长,花费的时间很多。
2.方案二,好处是不用重新登陆、走流程,写一次代码,以后调试都能用,多次调试效率相对高。坏处一,第一次调试时,比方案一多花时间,并且重新编译运行app,这里也花费时间,并且不知道调试代码是否有bug,如果有bug还得改代码、编译、运行。坏处二,调试代码入侵到原代码,必须小心对待,以防在上线时有影响。
3.方案三,好处是调试代码不入侵原代码,不需要重新编译运行app(当然编译调试代码也要耗几秒钟)。不足,需要做一点准备工作。
方案三就是笔者今天介绍的“奇巧淫技”。
实现思路
我们的需求是:
临时在app上跑一段代码,并且不入侵原代码,不需要重新编译运行app。
实现思路:
1.调试代码写成单元测试(或者java main函数);
2.编译、打包成dex文件;
3.发送dex给app;
4.app执行代码。
相信聪明的同学,看到思路已经廓然开朗了;同时,笔者也相信很多同学直接滚到下面点demo链接.....下面给大家讲讲代码。
代码
1.写调试代码
在.../test/com/example/dex
,写DexTask
类,继承Runnable
:
package com.example.dex;
public class DexTask implements Runnable {
@Override
public void run() {
System.out.println("DexTask running...");
}
}
run()
会被app执行。
2.编译、打包dex
例如,工程包名com.example.dex
。单元测试代码在src/main/test/java
目录。
那么,编译后的单元测试class文件,在build/intermediates/classes/test/debug
目录。
class打包jar
用shell命令,将build/intermediates/classes/test/debug/
目录打包成myjar.jar
:
String dir = new File("build/intermediates/classes/test/debug").getAbsolutePath();
Bash bash = new Bash();
bash.cd(dir);
bash.exec("jar -cvf myjar.jar .");
(Bash是笔者写的一个工具类)
jar编译成dex
使用android sdk的Dx工具命令,将myjar.jar
编译成dex.jar
:
> $ANDROID_HOME/build-tools/27.0.1/dx --dex --output=dex.jar myjar.jar
注意,更改目录为sdk存在的build-tools版本dx路径。笔者最新到27.0.1,读者可能是其他版本(demo中会自动获取本地最新build-tools版本)。
java代码:
Dx dx = new Dx();
String dexPath = dx.dx(dir + "/myjar.jar", "dex.jar");
Dx是笔者封装的dx工具类。
编译jar、dex后,build/intermediates/classes/test/debug
存在这两个文件:
(demo中,每次执行完就删掉myjar.jar和dex.jar)
3.发送dex到app
app监听端口
app启动一个Service,用ServerSocket监听某端口(demo用10086端口做例子):
ServerSocket mServer = new ServerSocket(10086);
Socket socket = mServer.accept();
// 从socket流读取数据,写入本地
InputStream is = socket.getInputStream();
FileOutputStream fos = new FileOutputStream(context.getCacheDir() + "dex.jar");
// 详细代码不写了,看demo
...
执行单元测试,发送dex文件
Socket socket = new Socket();
socket.setSoTimeout(10 * 1000);
socket.connect(new InetSocketAddress("192.168.1.*", 10086));
OutputStream os = socket.getOutputStream();
// 写流操作,详细代码看demo
...
4.app执行dex代码
app加载dex,并执行DexTask.run()
try {
File dexFile = new File(context.getCacheDir(), "dex.jar");
DexClassLoader cl = new DexClassLoader(dexFile, context.getCacheDir(), null, getClassLoader());
String taskName = "com.example.dex.DexTask";
Class clazz = cl.loadClass(taskName);
Runnable runnable = (Runnable) clazz.newInstance();
runnable.run();
// 执行完后,删除dex文件
dexFile.delete();
} catch (Exception e) {
e.printStackTrace();
}
这样几个步骤就完成了。
调试
1.修改Working Directory
Run -> Edit Configurations -> Defaults -> Android Junit -> Working Directory
配置成$MODULE_DIR$
2.执行单元测试RPCTest
:
public class RPCTest {
@Test
public void rpc() throws Exception {
Bash.DEBUG = false;
RPC rpc = new RPC("192.168.1.154", 10086);
rpc.remoteRun();
}
}
(RPC
封装了上述编译、打包dex、socket发送代码)
调试最终效果:
调试代码
DexTask
调试代码,EventBus发送String:
public class DexTask implements Runnable {
@Override
public void run() {
System.out.println("DexTask running...");
EventBus.getDefault().post("收到dex并执行");
}
}
MainActivity
接受到String事件,在TextView
显示:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_hello)
TextView tv_hello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
EventBus.getDefault().register(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMsgEvent(String msg) {
tv_hello.setText(msg);
}
}
demo
https://github.com/kkmike999/DexRpcDemo
demo的代码,与本篇介绍有所出入,因为demo注重代码解耦、可读性,文章注重理解。
推荐阅读:《Android 面试指南》
关于作者
我是键盘男。
在广州生活,悦跑圈Android工程师,猥琐文艺码农。每天谋划砍死产品经理。喜欢科学、历史,玩玩投资,偶尔旅行。