前言
不知道同學們有沒有遇到這些時候:
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工程師,猥瑣文藝碼農。每天謀劃砍死產品經理。喜歡科學、歷史,玩玩投資,偶爾旅行。