Android App调试一个奇巧淫技

《Android App调试一个奇巧淫技》

前言

不知道同学们有没有遇到这些时候:

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目录。

《Android App调试一个奇巧淫技》 单元测试目录

那么,编译后的单元测试class文件,在build/intermediates/classes/test/debug目录。

《Android App调试一个奇巧淫技》 单元测试class文件目录

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存在这两个文件:

《Android App调试一个奇巧淫技》

(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$

《Android App调试一个奇巧淫技》

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发送代码)

调试最终效果:

《Android App调试一个奇巧淫技》 demo.gif

调试代码

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工程师,猥琐文艺码农。每天谋划砍死产品经理。喜欢科学、历史,玩玩投资,偶尔旅行。

    原文作者:键盘男
    原文地址: https://www.jianshu.com/p/a039fd5046c2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞