0%

Android自动化测试冷门知识

问题

  • AndroidJUnitRunner 原理 (https://yuque.antfin.com/xiaobao.spf/ad2gcp/ktqa9u
  • 编译产物 (自动化测试生成的2个apk对比)
  • 启动方式 (主要研究AndroidJUnitRunner如何被调起)
  • 运行方式 (研究 espresso和 uiautomator原理,如何模拟操作)
  • espresso和uiautomator是否可以打包到主apk

编译产物

执行自动化测试会编译出两个apk,我看到之后是很懵逼的,心里会涌现出了无数个🦙,这么多年竟然忽略了这个细节。
image.png

1
2
3
4
5
6
7
8
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

1.app-debug.apk

1.1 Manifest.xml

1
2
3
4
5
6
7
8
9
10
11
   <manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:compileSdkVersion="30"
android:compileSdkVersionCodename="11"
package="com.demo.sample"
platformBuildVersionCode="30"
platformBuildVersionName="11">
...
</manifest>

package为com.demo.sample

1.2 依赖
正常的依赖了gradle中配置的依赖

2.app-debug-androidTest.apk

2.1Manifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:compileSdkVersion="30"
android:compileSdkVersionCodename="11"
package="com.demo.sample.test"
platformBuildVersionCode="30"
platformBuildVersionName="11">

<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="30" />

<instrumentation
android:label="Tests for com.demo.sample"
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.demo.sample"
android:handleProfiling="false"
android:functionalTest="false" />
...
</manifest>

这里有个细节,manifest中package是com.demo.sample.test,和实际apk相比多了.test

2.2 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
debugAndroidTestRuntimeClasspath - Resolved configuration for runtime for variant: debugAndroidTest
+--- androidx.test.ext:junit:1.1.3
| +--- junit:junit:4.12
| | \--- org.hamcrest:hamcrest-core:1.3
| +--- androidx.test:core:1.4.0
| | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0
| | +--- androidx.test:monitor:1.4.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.2.0
| | \--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.3.1
| | \--- androidx.annotation:annotation:1.1.0 -> 1.2.0
| +--- androidx.test:monitor:1.4.0 (*)
| \--- androidx.annotation:annotation:1.0.0 -> 1.2.0
+--- androidx.test.espresso:espresso-core:3.4.0
| +--- androidx.test:runner:1.4.0
| | +--- androidx.annotation:annotation:1.0.0 -> 1.2.0
| | +--- androidx.test:monitor:1.4.0 (*)
| | +--- androidx.test.services:storage:1.4.0
| | | +--- androidx.test:monitor:1.4.0 (*)
| | | \--- com.google.code.findbugs:jsr305:2.0.1
| | \--- junit:junit:4.12 (*)
| +--- androidx.test.espresso:espresso-idling-resource:3.4.0
| +--- com.squareup:javawriter:2.1.1
| +--- javax.inject:javax.inject:1
| +--- org.hamcrest:hamcrest-library:1.3
| | \--- org.hamcrest:hamcrest-core:1.3
| +--- org.hamcrest:hamcrest-integration:1.3
| | \--- org.hamcrest:hamcrest-library:1.3 (*)
| \--- com.google.code.findbugs:jsr305:2.0.1
+--- androidx.annotation:annotation:{strictly 1.2.0} -> 1.2.0 (c) //这可真是一万个🦙呀,根本不知道为啥被引入的,
\--- androidx.lifecycle:lifecycle-common:{strictly 2.3.1} -> 2.3.1 (c) // 只能看源码,这个任务留给以后吧

综上,不同的地方基本只有Manifest中的不同,其他资源正常。

启动方式

正确流程:
am可执行文件 ->
app_process可执行文件 ->
AM.java ->
Instrument.java ->
AMS ->
Process ->
ActivityThread.java ->
Instrumentation.java

一般am 直接调用AMS服务,但是am instrument 时为特殊场景

1
adb shell am instrument -w -m    -e debug false -e class 'com.demo.sample.ExampleInstrumentedTest' com.demo.sample.test/androidx.test.runner.AndroidJUnitRunner

根据aosp经验,adb shell am 会调用AMS中的onShellCommand方法

ActivityManagerService.java

1
2
3
4
5
6
7
8
9
10
public class ActivityManagerService extends IActivityManager.Stub
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
(new ActivityManagerShellCommand(this, false)).exec(
this, in, out, err, args, callback, resultReceiver);
}
}

ActivityManagerShellCommand.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final class ActivityManagerShellCommand extends ShellCommand {
@Override
public int onCommand(String cmd) {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
final PrintWriter pw = getOutPrintWriter();
try {
switch (cmd) {
...

case "instrument":
getOutPrintWriter().println("Error: must be invoked through 'am instrument'.");
return -1;
...
default:
return handleDefaultCommands(cmd);
}
} catch (RemoteException e) {
pw.println("Remote exception: " + e);
}
return -1;
}
}

又一个🦙出现在我的眼前,这儿直接是打印一个日志,那肯定不是走这里了,这样的话只能从cmd 去跟代码了。
如果不懂cmd参考:https://yuque.antfin.com/xiaobao.spf/ad2gcp/gm5kr7

1
adb shell cmd -l

上面的命令可以列出手机支持的服务,可惜列出来的服务没有am,现在我们应该去手机/system/bin目录下去找am
image.png
am就是一个shell脚本

1
2
3
4
5
6
7
8
9
#!/system/bin/sh

if [ "$1" != "instrument" ] ; then
cmd activity "$@"
else //我们关注点是else这个分支
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"
fi

这段代码不难理解, args = [am instrument ],如果$1是instrument,则走else,这里直接执行了/system/bin/app_process,这里我们直接看com.android.commands.am.Am代码吧,这里给出com.android.commands.am.Am文件吧
如果不懂app_process参考:https://yuque.antfin.com/xiaobao.spf/ad2gcp/inweh2
image.png

com.android.commands.am.Am.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class Am extends BaseCommand {


public static void main(String[] args) {
(new Am()).run(args);
}

@Override
public void onRun() throws Exception {

mAm = ActivityManager.getService();
if (mAm == null) {
System.err.println(NO_SYSTEM_ERROR_CODE);
throw new AndroidException("Can't connect to activity manager; is the system running?");
}

mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
if (mPm == null) {
System.err.println(NO_SYSTEM_ERROR_CODE);
throw new AndroidException("Can't connect to package manager; is the system running?");
}

String op = nextArgRequired(); //获取args

if (op.equals("instrument")) {
runInstrument(); //执行这里
} else {
runAmCmd(getRawArgs());//调回了ams
}
}

public void runInstrument() throws Exception {
Instrument instrument = new Instrument(mAm, mPm);
// adb shell am instrument
// -w
// -m
// -e debug false
// -e class 'com.demo.sample.ExampleInstrumentedTest'
// com.demo.sample.test/androidx.test.runner.AndroidJUnitRunner
String opt;
while ((opt=nextOption()) != null) {
if (opt.equals("-w")) {
instrument.wait = true;
} else if (opt.equals("-m")) {
instrument.protoStd = true;
} else if (opt.equals("-e")) {
final String argKey = nextArgRequired();
final String argValue = nextArgRequired();
instrument.args.putString(argKey, argValue);
}
}

if (instrument.userId == UserHandle.USER_ALL) {
System.err.println("Error: Can't start instrumentation with user 'all'");
return;
}
// componentNameArg = com.demo.sample.test/androidx.test.runner.AndroidJUnitRunner
instrument.componentNameArg = nextArgRequired();

instrument.run();
}
}

Instrument.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Instrument {

public void run() throws Exception {
StatusReporter reporter = null;
float[] oldAnims = null;

try {

// Choose whether we have to wait for the results.
InstrumentationWatcher watcher = null;
// 这里竟然有 UiAutomationConnection,一定要记住,后续会再次提到
UiAutomationConnection connection = null;
if (reporter != null) {
watcher = new InstrumentationWatcher(reporter);
connection = new UiAutomationConnection();
}

// 这里通过PMS 查询 instrument,原理就是通过手机里面安装的所有Manifest.xml 寻找
final ComponentName cn = parseComponentName(componentNameArg);


// 通过ams 启动 startInstrumentation ,这样就启动了
if (!mAm.startInstrumentation(cn, profileFile, flags, args, watcher, connection, userId,
abi)) {
throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString());
}

} catch (Exception ex) {

} finally {

}
}
}

下一步应该在AMS中找 startInstrumentation方法,有个自己编译的aosp手机是如此的重要,下面截图是AMS调试
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class ActivityManagerService extends IActivityManager.Stub
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {

public boolean startInstrumentation(ComponentName className,
String profileFile, int flags, Bundle arguments,
IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection,
int userId, String abiOverride) {

synchronized(this) {

ActiveInstrumentation activeInstr = new ActiveInstrumentation(this);
activeInstr.mClass = className;
String defProcess = ai.processName;;
if (ii.targetProcesses == null) {
activeInstr.mTargetProcesses = new String[]{ai.processName};
} else if (ii.targetProcesses.equals("*")) {
activeInstr.mTargetProcesses = new String[0];
} else {
activeInstr.mTargetProcesses = ii.targetProcesses.split(",");
defProcess = activeInstr.mTargetProcesses[0];
}
activeInstr.mTargetInfo = ai;
activeInstr.mProfileFile = profileFile;
activeInstr.mArguments = arguments;
activeInstr.mWatcher = watcher;
activeInstr.mUiAutomationConnection = uiAutomationConnection;
activeInstr.mResultClass = className;
activeInstr.mHasBackgroundActivityStartsPermission = checkPermission(
START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid)
== PackageManager.PERMISSION_GRANTED;

boolean disableHiddenApiChecks = ai.usesNonSdkApi()
|| (flags & INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS) != 0;
if (disableHiddenApiChecks) {
enforceCallingPermission(android.Manifest.permission.DISABLE_HIDDEN_API_CHECKS,
"disable hidden API checks");
}
final boolean mountExtStorageFull = isCallerShell()
&& (flags & INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL) != 0;

final long origId = Binder.clearCallingIdentity();
// Instrumentation can kill and relaunch even persistent processes
forceStopPackageLocked(ii.targetPackage, -1, true, false, true, true, false, userId,
"start instr");
// Inform usage stats to make the target package active
if (mUsageStatsService != null) {
mUsageStatsService.reportEvent(ii.targetPackage, userId,
UsageEvents.Event.SYSTEM_INTERACTION);
}
//<instrumentation
// android:label="Tests for com.demo.sample"
// android:name="androidx.test.runner.AndroidJUnitRunner"
// android:targetPackage="com.demo.sample"
// android:handleProfiling="false"
// android:functionalTest="false" />

// ProcessRecord 中的ai是 targetPackage,所以启动的进程是非 test apk的进程


//启动一个进程,这里的ai很重要,因为这是从instrumentation取出来的targetPackage
//看图吧
ProcessRecord app = addAppLocked(ai, defProcess, false, disableHiddenApiChecks,
mountExtStorageFull, abiOverride);

// 这里是关键
app.setActiveInstrumentation(activeInstr);
activeInstr.mFinished = false;
activeInstr.mRunningProcesses.add(app);
if (!mActiveInstrumentation.contains(activeInstr)) {
mActiveInstrumentation.add(activeInstr);
}
Binder.restoreCallingIdentity(origId);
}

return true;
}
}

image.png
这里的链路比较长,我这里直接放出代码,有兴趣可以自己跟一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class ActivityThread extends ClientTransactionHandler {

private void handleBindApplication(AppBindData data) {
// 这里启动了 com.demo.sample.test/androidx.test.runner.AndroidJUnitRunner
try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate instrumentation "
+ data.instrumentationName + ": " + e.toString(), e);
}

final ComponentName component = new ComponentName(ii.packageName, ii.name);
mInstrumentation.init(this, instrContext, appContext, component,
data.instrumentationWatcher, data.instrumentationUiAutomationConnection);
}
}

启动的是app-debug.apk,为什么可以加载app-debug-androidTest.apk中的AndroidJUnitRunner ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public final class ActivityThread extends ClientTransactionHandler {

private void handleBindApplication(AppBindData data) {
final InstrumentationInfo ii;
if (data.instrumentationName != null) {
try {
ii = new ApplicationPackageManager(null, getPackageManager())
.getInstrumentationInfo(data.instrumentationName, 0);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(
"Unable to find instrumentation info for: " + data.instrumentationName);
}

// Warn of potential ABI mismatches.
if (!Objects.equals(data.appInfo.primaryCpuAbi, ii.primaryCpuAbi)
|| !Objects.equals(data.appInfo.secondaryCpuAbi, ii.secondaryCpuAbi)) {
Slog.w(TAG, "Package uses different ABI(s) than its instrumentation: "
+ "package[" + data.appInfo.packageName + "]: "
+ data.appInfo.primaryCpuAbi + ", " + data.appInfo.secondaryCpuAbi
+ " instrumentation[" + ii.packageName + "]: "
+ ii.primaryCpuAbi + ", " + ii.secondaryCpuAbi);
}

mInstrumentationPackageName = ii.packageName;
mInstrumentationAppDir = ii.sourceDir;
mInstrumentationSplitAppDirs = ii.splitSourceDirs;
mInstrumentationLibDir = getInstrumentationLibrary(data.appInfo, ii);
mInstrumentedAppDir = data.info.getAppDir();
mInstrumentedSplitAppDirs = data.info.getSplitAppDirs();
mInstrumentedLibDir = data.info.getLibDir();
} else {
ii = null;
}

// Continue loading instrumentation.
if (ii != null) {
ApplicationInfo instrApp;
try {
instrApp = getPackageManager().getApplicationInfo(ii.packageName, 0,
UserHandle.myUserId());
} catch (RemoteException e) {
instrApp = null;
}
if (instrApp == null) {
instrApp = new ApplicationInfo();
}
ii.copyTo(instrApp);
instrApp.initForUser(UserHandle.myUserId());

// instrApp 为 instrumentation 的applicationInfo,即 test apk
final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
appContext.getClassLoader(), false, true, false);

// The test context's op package name == the target app's op package name, because
// the app ops manager checks the op package name against the real calling UID,
// which is what the target package name is associated with.
final ContextImpl instrContext = ContextImpl.createAppContext(this, pi,
appContext.getOpPackageName());

try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate instrumentation "
+ data.instrumentationName + ": " + e.toString(), e);
}

final ComponentName component = new ComponentName(ii.packageName, ii.name);
mInstrumentation.init(this, instrContext, appContext, component,
data.instrumentationWatcher, data.instrumentationUiAutomationConnection);

}
}

总结为一句话,LoadedApk会帮我们把app-debug-androidTest.apk进行装载。很多插件化的原理也是如此,我知道的ACDD就是这样。

紧接着AndroidJUnitRunner就被启动了,
AndroidJUnitRunner原理看着这个 https://yuque.antfin.com/xiaobao.spf/ad2gcp/ktqa9u


运行方式

AndroidJUnitRunner的运行这里不讲,主要看uiautomator的运行。

  • UiDevice

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class UiDevice implements Searchable {
    /**
    * Returns the first object to match the {@code selector} criteria,
    * or null if no matching objects are found.
    */
    public UiObject2 findObject(BySelector selector) {
    AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
    return node != null ? new UiObject2(this, selector, node) : null;
    }

    AccessibilityNodeInfo[] getWindowRoots() {
    waitForIdle();

    Set<AccessibilityNodeInfo> roots = new HashSet();

    // Start with the active window, which seems to sometimes be missing from the list returned
    // by the UiAutomation.
    AccessibilityNodeInfo activeRoot = getUiAutomation().getRootInActiveWindow();
    if (activeRoot != null) {
    roots.add(activeRoot);
    }

    // Support multi-window searches for API level 21 and up.
    if (UiDevice.API_LEVEL_ACTUAL >= Build.VERSION_CODES.LOLLIPOP) {
    for (AccessibilityWindowInfo window : getUiAutomation().getWindows()) {
    AccessibilityNodeInfo root = window.getRoot();
    if (root == null) {
    Log.w(LOG_TAG, String.format("Skipping null root node for window: %s",
    window.toString()));
    continue;
    }
    roots.add(root);
    }
    }
    return roots.toArray(new AccessibilityNodeInfo[roots.size()]);
    }

    }

    很重要的方法:getWindowRoots 调用了UiAutomation的getRootInActiveWindow方法

  • UiAutomation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public final class UiAutomation {
    // window 根
    public AccessibilityNodeInfo getRootInActiveWindow() {
    final int connectionId;
    synchronized (mLock) {
    throwIfNotConnectedLocked();
    connectionId = mConnectionId;
    }
    // Calling out without a lock held.
    // 链接 Acc服务的时候会存下来
    return AccessibilityInteractionClient.getInstance()
    .getRootInActiveWindow(connectionId);
    }

    public void connect(int flags) {
    synchronized (mLock) {
    // 接受Acc的回调 binder stub
    mClient = new IAccessibilityServiceClientImpl(mRemoteCallbackThread.getLooper());
    }
    try {
    // binder proxy 链接ACC服务,将mClient传递给Acc服务
    mUiAutomationConnection.connect(mClient, flags);
    mFlags = flags;
    } catch (RemoteException re) {
    throw new RuntimeException("Error while connecting " + this, re);
    }
    }



    }
  • UiAutomationConnection binder Proxy 帮助链接ACC

  • IAccessibilityServiceClientImpl binder Stub 接受acc的回调

  • Instrumentation 你没有看错 ActivityThread里面的也是它,自动化测试中AndroidJUnitRunner继承它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class Instrumentation {
    // 有Instrumentation时调用,目前自动化测试会设置
    final void init(ActivityThread thread,
    Context instrContext, Context appContext, ComponentName component,
    IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection) {
    mThread = thread;
    mMessageQueue = mThread.getLooper().myQueue();
    mInstrContext = instrContext;
    mAppContext = appContext;
    mComponent = component;
    mWatcher = watcher;
    mUiAutomationConnection = uiAutomationConnection; // 上面有提到这个
    }

    //正常调用
    final void basicInit(ActivityThread thread) {
    mThread = thread;
    }

    // 触发 Acc服务 连接,上面获取 AccessibilityNodeInfo时也会触发
    public UiAutomation getUiAutomation(@UiAutomationFlags int flags) {
    boolean mustCreateNewAutomation = (mUiAutomation == null) || (mUiAutomation.isDestroyed());

    if (mUiAutomationConnection != null) {
    if (!mustCreateNewAutomation && (mUiAutomation.getFlags() == flags)) {
    return mUiAutomation;
    }
    if (mustCreateNewAutomation) {
    mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(),
    mUiAutomationConnection);
    } else {
    mUiAutomation.disconnect();
    }
    //链接Acc服务
    mUiAutomation.connect(flags);
    return mUiAutomation;
    }
    return null;
    }



    }

espresso和uiautomator是否可以打包到主apk

根据上面启动方式,最终是启动了1个apk,通过LoadedApk装载了另一个apk,所以无论espresso和uiautomator在哪个apk中,都不会影响自动化测试运行。

利用Intrumentation是否可以搞黑科技

是否可以通过一个apk去运行另一个apk的instrument ,然后运行自动化测试的模拟操作?

第一个apk中手动注册一个 MyInstrumentation

1
2
3
4
5
6
<instrumentation
android:label="Tests for com.demo.sample"
android:name=".MyInstrumentation"
android:targetPackage="com.demo.sample"
android:handleProfiling="false"
android:functionalTest="false" />

第二个apk中使用Runtime.getRuntime().exec()

1
am instrument -w -m -e debug false com.demo.sample/com.demo.sample.MyInstrumentation

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Permission Denial: startInstrumentation asks to run as user -2 but is calling from uid u0a164; this requires android.permission.INTERACT_ACROSS_USERS_FULL	
at android.os.Parcel.createException(Parcel.java:2071)
at android.os.Parcel.readException(Parcel.java:2039)
at android.os.Parcel.readException(Parcel.java:1987)
at android.app.IActivityManager$Stub$Proxy.startInstrumentation(IActivityManager.java:5443)
at com.android.commands.am.Instrument.run(Instrument.java:512)
at com.android.commands.am.Am.runInstrument(Am.java:196)
at com.android.commands.am.Am.onRun(Am.java:80)
at com.android.internal.os.BaseCommand.run(BaseCommand.java:56)
at com.android.commands.am.Am.main(Am.java:50)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:338)
Caused by: android.os.RemoteException: Remote stack trace:
at com.android.server.am.UserController.handleIncomingUser(UserController.java:1683)
at com.android.server.am.ActivityManagerService.startInstrumentation(ActivityManagerService.java:15696)
at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:2350)
at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2738)
at android.os.Binder.execTransactInternal(Binder.java:1021)

通过查看代码 userid可以外部传入

1
2
3
if (opt.equals("--user")) {
instrument.userId = parseUserArg(nextArgRequired());
}
1
2
3
4
am instrument -w -m --user 0 -e debug false com.demo.sample/com.demo.sample.MyInstrumentation
//或者
app_process -Djava.class.path=/system/framework/am.jar /data/local/tmp com.android.commands.am.Am instrument -w -m --user 0 -e debug false com.demo.sample/com.demo.sample.MyInstrumentation

新的问题又来了

1
2
3
4
5
6
7
8
9
10
java.lang.RuntimeException: Exception thrown in onCreate() of ComponentInfo{com.demo.sample/com.demo.sample.MyInstrumentation}: java.lang.SecurityException: You do not have android.permission.RETRIEVE_WINDOW_CONTENT required to call registerUiTestAutomationService from pid=14037, uid=10164
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6457)
at android.app.ActivityThread.access$1300(ActivityThread.java:219)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1859)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

缺少 android.permission.RETRIEVE_WINDOW_CONTENT权限,但是这个权限是系统应用的,暂时没有想到如果规避这个权限,我这边尝试了很多种方式都绕不过去。

这里解释下为什么必须要用Uiautomator,因为正常的Acc辅助功能需要授权,而Uiautomator中的通过adb shell启动的自动化测试无需授权。

假如一个apk中可以调起另一个apk中的Uiautomator,我们可以想象一个场景,支付宝中有一个instrumentation,里面可以使用uiautomator,高德app中可以调起这个instrumentation,就可以实现模拟用户的行为了。

异常分析:registerUiTestAutomationService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
@Override
public void connect(IAccessibilityServiceClient client, int flags) {
if (client == null) {
throw new IllegalArgumentException("Client cannot be null!");
}
synchronized (mLock) {
throwIfShutdownLocked();
if (isConnectedLocked()) {
throw new IllegalStateException("Already connected.");
}
mOwningUid = Binder.getCallingUid();
//这里会进行权限判断
registerUiTestAutomationServiceLocked(client, flags);
storeRotationStateLocked();
}
}
}

UiAutomationConnection是在app_process 进程中创建的,而app_process 是另一个apk创建的,这样的情况就是 UiAutomationConnection的group process 是 另一个apk;可以尝试启动app_process设置一些参数。adb shell 之所以可以是因为是通过adb shell 创建的app_process

instrumentation 这个可以使用,但是没有应用场景
uiautomator 只能通过adb shell am instrument …调用,否则将有权限问题;因为uiautomator使用了acc辅助权限,当链接AccessibilityService时,无法获取权限,所以脱离adb,无法实现自动模拟操作的行为。