CodeCrafts之断点调试

1,089 阅读4分钟

CodeCrafts是什么?

这么好的Android开发辅助工具App不白嫖可惜了

CodeCrafts下载安装:

Github地址下载, Gitee地址下载(需要登录gitee) 

 或者去Google Play安装

如果你的App有多个进程,你是否被子进程如何使用断点来调试Application.onCreate() 等进程启动前期的初始化代码困扰过?

我就是被这个问题困扰过很长一段时间,起初我都是在想要断点的地方手动加上Debug.waitForDebugger(),然后重新编译,最后再完成调试后再手动删除Debug.waitForDebugger()。繁琐低效不说,如果一不小心把它提交到代码仓库那就很尴尬了。

CodeCrafts的断点调试可以在进程启动前,提前将进程设置为断点调试状态,这样就能解决子进程的一些初始化代码不方便调试的问题。这是Android Studio无法做到的,先看图

实现原理

原理其实很简单,就是利用am set-debug-app,

set-debug-app这个名称并不准确,导致这个命令容易被用错。

set-debug-app和clear-debug-app

set-debug-app: 设置待调试的App, 实际上这个命令的名字和描述都不太准确,应该叫set-debug-process才对,因为set-debug-app 命令最后的参数是processName 并不是packageName。

clear-debug-app: 相反,是清除待调试的App

set-debug-appclear-debug-app是am下面的一个子命令, 看下am的定义

# adb shell am 
...
  set-debug-app [-w] [--persistent] <PACKAGE>  // <PACKAGE> 应该是Process, 不是Package
      Set application <PACKAGE> to debug.  Options are:
      -w: wait for debugger when application starts
      --persistent: retain this value
  clear-debug-app
      Clear the previously set-debug-app.
...

为什么说set-debug-app 命令的最后的参数是processName而不是packageName, 看一下它在源码中的实现就知道了

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

private boolean attachApplicationLocked(@NonNull IApplicationThread thread, int pid, int callingUid, long startSeq) {    ....
        try {            
            int testMode = ApplicationThreadConstants.DEBUG_OFF;

            // mDebugApp是命令后的入参,与之判断相等的是processName,而不是packageName
            if (mDebugApp != null && mDebugApp.equals(processName)) {
                  testMode = mWaitForDebugger
                    ? ApplicationThreadConstants.DEBUG_WAIT
                    : ApplicationThreadConstants.DEBUG_ON;

              // app是个ProcessRecord对象,这里将这个进程打上Debugging标记
                app.setDebugging(true); 
                if (mDebugTransient) {
                    mDebugApp = mOrigDebugApp;
                    mWaitForDebugger = mOrigWaitForDebugger;
                }
            }


     ....
}

如果你真的输入packageName, 那么就没办法为子进程设置调试了。

CodeCrafts的断点调试实现

CodeCrafts有Shell权限,当然可以直接执行am set-debug-app来设置要对App的哪个进程来进行断点。 但这样有点简单粗暴,执行结果也很难控制。

set-debug-appam下的一个子命令,因此推断它的实现肯定是在ActivityManagerService(后面简称AMS)中, 最终很容易找到了AMSsetDebugApp方法。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

 public void setDebugApp(String packageName, boolean waitForDebugger,
            boolean persistent) {
        enforceCallingPermission(android.Manifest.permission.SET_DEBUG_APP,
                "setDebugApp()");

        final long ident = Binder.clearCallingIdentity();
        try {
            // Note that this is not really thread safe if there are multiple
            // callers into it at the same time, but that's not a situation we
            // care about.
            if (persistent) {
                final ContentResolver resolver = mContext.getContentResolver();
                Settings.Global.putString(
                    resolver, Settings.Global.DEBUG_APP,
                    packageName);
                Settings.Global.putInt(
                    resolver, Settings.Global.WAIT_FOR_DEBUGGER,
                    waitForDebugger ? 1 : 0);
            }

            synchronized (this) {
                if (!persistent) {
                    mOrigDebugApp = mDebugApp;
                    mOrigWaitForDebugger = mWaitForDebugger;
                }
                mDebugApp = packageName;
                mWaitForDebugger = waitForDebugger;
                mDebugTransient = !persistent;
                if (packageName != null) {
                    forceStopPackageLocked(packageName, -1, false, false, true, true,
                            false, UserHandle.USER_ALL, "set debug app");
                }
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

但是ActivityManager(后面简称AM)中并没有开放这个接口, 普通的App没有办法直接调用这个系统未公开的API。

那有没有间接一点的方法呢?

我们知道AMAMS在应用进程中的代理,他们之间是通过Binder通信的,Binder通信一般需要定义一个aidl,IActivityManager.aidl 就是他们之间的接口定义, 而且IActivityManager.aidl中也有定义setDebugApp方法。

frameworks/base/core/java/android/app/IActivityManager.aidl 

interface IActivityManager {
    ....

    void setDebugApp(in String packageName, boolean waitForDebugger, boolean persistent);
    ....
}

IActivityManager.aidl 会在编译期间会生成

IActivityManager.java

,IActivityManager.java中当然也会有setDebugApp接口方法。

虽然AM没有开放setDebugApp接口,但是根据aidl的实现规则, IActivityManager.java中必定有个Proxy类,并且Proxy必定实现了IActivityManager接口,我们只要拿到这个Proxy类的对象就能调用到setDebugApp方法。

如何拿到这个Proxy类的对象呢?

frameworks/base/core/java/android/app/ActivityManagerNative.java

public abstract class ActivityManagerNative {
....

    /**
     * Cast a Binder object into an activity manager interface, generating
     * a proxy if needed.
     *
     * @deprecated use IActivityManager.Stub.asInterface instead.
     */
    @UnsupportedAppUsage
    static public IActivityManager asInterface(IBinder obj) {
        return IActivityManager.Stub.asInterface(obj);
    }

    /**
     * Retrieve the system's default/global activity manager.
     *
     * @deprecated use ActivityManager.getService instead.
     */
    @UnsupportedAppUsage
    static public IActivityManager getDefault() {
        return ActivityManager.getService();
    }

....}

ActivityManagerNative提供的getDefault()方法获取的正是这个Proxy类。

虽然ActivityManagerNative和IActivityManager都是隐藏类App无法直接访问,但是我们只需要定义这两个类的同包名、同类名的文件即可解决编译问题。 根据类加载双亲委派机制,会自动忽略这两个文件。这块不是重点,有空再细讲。

使用说明

断点调试功能是CodeCrafts v1.0.15中新增功能

CodeCrafts下载安装:

Github地址下载, Gitee地址下载(需要登录gitee)

或者去Google Play安装