本文旨在从Android系统源码出发,简单梳理Instrumentation框架的行为及逻辑结构,供有兴趣的同学一起学习

从am instrument谈起

am instrument命令的执行

我们知道,命令行运行Android测试的命令是adb shell am instrument,这个命令是如何调起我们的测试代码来进行测试的呢,让我们从am命令的处理源码来开始一步步的查看吧。

am.java是android系统处理am命令的类,其位于/frameworks/base/cmds/am/src/com/android/commands/am/下,有Android源码的同学可以到相关目录下自行查看

onRun方法是am处理各个不同命令的分发处,我们可以看到am命令有多种用法,其中am instrumentation命令会调用runInstrument()方法

public void onRun() throws Exception {mAm = ActivityManagerNative.getDefault();if (mAm == null) {System.err.println(NO_SYSTEM_ERROR_CODE);throw new AndroidException("Can't connect to activity manager; is the system running?");}String op = nextArgRequired();if (op.equals("start")) {runStart();} else if (op.equals("startservice")) {runStartService();} else if (op.equals("stopservice")) {runStopService();} else if (op.equals("force-stop")) {runForceStop();} else if (op.equals("kill")) {runKill();} else if (op.equals("kill-all")) {runKillAll();} else if (op.equals("instrument")) {runInstrument();} else if (op.equals("broadcast")) {sendBroadcast();} else if (op.equals("profile")) {runProfile();} else if (op.equals("dumpheap")) {runDumpHeap();} else if (op.equals("set-debug-app")) {runSetDebugApp();} else if (op.equals("clear-debug-app")) {runClearDebugApp();} else if (op.equals("bug-report")) {runBugReport();} else if (op.equals("monitor")) {runMonitor();} else if (op.equals("hang")) {runHang();} else if (op.equals("restart")) {runRestart();} else if (op.equals("idle-maintenance")) {runIdleMaintenance();} else if (op.equals("screen-compat")) {runScreenCompat();} else if (op.equals("to-uri")) {runToUri(0);} else if (op.equals("to-intent-uri")) {runToUri(Intent.URI_INTENT_SCHEME);} else if (op.equals("to-app-uri")) {runToUri(Intent.URI_ANDROID_APP_SCHEME);} else if (op.equals("switch-user")) {runSwitchUser();} else if (op.equals("start-user")) {runStartUserInBackground();} else if (op.equals("stop-user")) {runStopUser();} else if (op.equals("stack")) {runStack();} else if (op.equals("lock-task")) {runLockTask();} else if (op.equals("get-config")) {runGetConfig();} else {showError("Error: unknown command '" + op + "'");}
}

以下是runInsturmentation方法的源码

private void runInstrument() throws Exception {String profileFile = null;boolean wait = false;boolean rawMode = false;boolean no_window_animation = false;int userId = UserHandle.USER_CURRENT;Bundle args = new Bundle();String argKey = null, argValue = null;IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));String abi = null;String opt;while ((opt=nextOption()) != null) {if (opt.equals("-p")) {profileFile = nextArgRequired();} else if (opt.equals("-w")) {wait = true;} else if (opt.equals("-r")) {rawMode = true;} else if (opt.equals("-e")) {argKey = nextArgRequired();argValue = nextArgRequired();args.putString(argKey, argValue);} else if (opt.equals("--no_window_animation")|| opt.equals("--no-window-animation")) {no_window_animation = true;} else if (opt.equals("--user")) {userId = parseUserArg(nextArgRequired());} else if (opt.equals("--abi")) {abi = nextArgRequired();} else {System.err.println("Error: Unknown option: " + opt);return;}}if (userId == UserHandle.USER_ALL) {System.err.println("Error: Can't start instrumentation with user 'all'");return;}String cnArg = nextArgRequired();ComponentName cn = ComponentName.unflattenFromString(cnArg);if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg);InstrumentationWatcher watcher = null;UiAutomationConnection connection = null;if (wait) {watcher = new InstrumentationWatcher();watcher.setRawOutput(rawMode);connection = new UiAutomationConnection();}float[] oldAnims = null;if (no_window_animation) {oldAnims = wm.getAnimationScales();wm.setAnimationScale(0, 0.0f);wm.setAnimationScale(1, 0.0f);}if (abi != null) {final String[] supportedAbis = Build.SUPPORTED_ABIS;boolean matched = false;for (String supportedAbi : supportedAbis) {if (supportedAbi.equals(abi)) {matched = true;break;}}if (!matched) {throw new AndroidException("INSTRUMENTATION_FAILED: Unsupported instruction set " + abi);}}if (!mAm.startInstrumentation(cn, profileFile, 0, args, watcher, connection, userId, abi)) {throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString());}if (watcher != null) {if (!watcher.waitForFinish()) {System.out.println("INSTRUMENTATION_ABORTED: System has crashed.");}}if (oldAnims != null) {wm.setAnimationScales(oldAnims);}
}

该方法主要做了这么几件事:

  1. 解析参数并处理异常,目前支持的参数为(-w,-p,-r,-e,–no_window_animation,–no-window-animation,–user,–abi)
  2. 获取测试包名和TestRunner,格式为测试包名/TestRunner
  3. 进行一些参数的逻辑处理(通常没有使用到,可以暂不关注)
  4. 启动TestRunner进行测试(mAm.startInstrumentation(cn, profileFile, 0, args, watcher, connection, userId, abi))
  5. 如果附带了-w参数,会等待至执行完成,否则直接结束处理

各个指令含义解析:

  • -w, 等待执行完成后才返回,否则直接返回(Instrumentation的执行在不同线程,不管是否带该参数都会正确执行)
  • -p, 带1个参数,将一些配置写入指定文件(具体用处还未研究,后续有需要再补充)
  • -r, 输出原始的数据(具体用处还未研究,后续有需要再补充)
  • -e, 带两个参数,将这两个参数作为键值对传递给TestRunner,由TestRunner处理(后面会提到)
  • –no_window_animation或–no-window-animation,执行Instrumentation过程中禁用动画效果,执行完后会恢复
  • –user, 带1个参数,使用指定的uid运行(具体用处还未研究,后续有需要再补充)
  • –abi, 带1个参数,使用指定的abi运行(具体用处还未研究,后续有需要再补充)

mAm是一个IActivityManager的对象,调用其startInstrumentation方法开始处理Instrumentation,下面我们来看看ActivityManager相关的知识

ActivityManager相关知识

ActivityManager是android框架的一个重要部分,它负责一新ActivityThread进程创建,Activity生命周期的维护,下图为这几个类之间的层次关系:

在这张图中,绿色的部分是在SDK中开放给应用程序开发人员的接口,蓝色的部分是一个典型的Proxy模式,红色的部分是底层的服务实现,是真正的动作执行者。这里的一个核心思想是Proxy模式,关于代理模式相关知识,请参考(暂却,后续补上)。以上仅是简单的介绍了下者几个类的关系,随着我们上文的步伐,我们会一点点分析出am命令是如何让Android系统跑起来测试用例的。

获取ActivityManager

还记得之前在am命令中启动Instrumentation的命令么?对的就是这个mAm.startInstrumentation(cn, profileFile, 0, args, watcher, connection, userId, abi)
其中的mAm为mAm = ActivityManagerNative.getDefault();
接下来便是要研究ActivityManagerNative.getDefault()了:

static public IActivityManager getDefault() {return gDefault.get();
}

gDefault的定义是IActivityManager的一个单例对象

private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {protected IActivityManager create() {IBinder b = ServiceManager.getService("activity");if (false) {Log.v("ActivityManager", "default service binder = " + b);}IActivityManager am = asInterface(b);if (false) {Log.v("ActivityManager", "default service = " + am);}return am;}
};

获取到名为activity的服务后,调用asInterface方法:

static public IActivityManager asInterface(IBinder obj) {if (obj == null) {return null;}IActivityManager in =(IActivityManager)obj.queryLocalInterface(descriptor);if (in != null) {return in;}return new ActivityManagerProxy(obj);
}

返回的是一个ActivityManagerProxy对象,然后按照原来的流程应该执行的是startInstrumentation方法

public boolean startInstrumentation(ComponentName className, String profileFile,int flags, Bundle arguments, IInstrumentationWatcher watcher,IUiAutomationConnection connection, int userId, String instructionSet)throws RemoteException {Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);ComponentName.writeToParcel(className, data);data.writeString(profileFile);data.writeInt(flags);data.writeBundle(arguments);data.writeStrongBinder(watcher != null ? watcher.asBinder() : null);data.writeStrongBinder(connection != null ? connection.asBinder() : null);data.writeInt(userId);data.writeString(instructionSet);mRemote.transact(START_INSTRUMENTATION_TRANSACTION, data, reply, 0);reply.readException();boolean res = reply.readInt() != 0;reply.recycle();data.recycle();return res;
}

将相关参数写入打包后调用mRemote.transact方法,这个mRemote即初始化ActivityManagerProxy时传入的IBinder对象,即ServiceManager.getService(“activity”)

public static IBinder getService(String name) {try {IBinder service = sCache.get(name);if (service != null) {return service;} else {return getIServiceManager().getService(name);}} catch (RemoteException e) {Log.e(TAG, "error in getService", e);}return null;
}

可见ServiceManager会先从sCache缓存中查看是否有对应的Binder对象,有则返回,没有则调用getIServiceManager().getService(name),那么要获取这个以activity命名的Service,它是在哪里创建的呢?通过全局搜索,我们找到这个调用关系,由于中间的方法实在是太太太太太长了,大家有兴趣的自己去看源码吧,其调用过程如下:zygote->main->new SystemServer().run()->[SystemServer]startBootstrapServices()->[SystemServer]mActivityManagerService.setSystemProcess()->[ActivityManagerService]ServiceManager.addService(Context.ACTIVITY_SERVICE, this, true)

由此可见,这个名为mRemote的Binder对应的是ActivityManagerService,ActivityManagerService的transact方法继承了Binder的实现:

public final boolean transact(int code, Parcel data, Parcel reply,int flags) throws RemoteException {if (false) Log.v("Binder", "Transact: " + code + " to " + this);if (data != null) {data.setDataPosition(0);}boolean r = onTransact(code, data, reply, flags);if (reply != null) {reply.setDataPosition(0);}return r;
}

会调用onTransact方法:

public boolean onTransact(int code, Parcel data, Parcel reply, int flags)throws RemoteException {if (code == SYSPROPS_TRANSACTION) {// We need to tell all apps about the system property change.ArrayList<IBinder> procs = new ArrayList<IBinder>();synchronized(this) {final int NP = mProcessNames.getMap().size();for (int ip=0; ip<NP; ip++) {SparseArray<ProcessRecord> apps = mProcessNames.getMap().valueAt(ip);final int NA = apps.size();for (int ia=0; ia<NA; ia++) {ProcessRecord app = apps.valueAt(ia);if (app.thread != null) {procs.add(app.thread.asBinder());}}}}int N = procs.size();for (int i=0; i<N; i++) {Parcel data2 = Parcel.obtain();try {procs.get(i).transact(IBinder.SYSPROPS_TRANSACTION, data2, null, 0);} catch (RemoteException e) {}data2.recycle();}}try {return super.onTransact(code, data, reply, flags);} catch (RuntimeException e) {// The activity manager only throws security exceptions, so let's// log all others.if (!(e instanceof SecurityException)) {Slog.wtf(TAG, "Activity Manager Crash", e);}throw e;}
}

由于statusCode不为SYSPROPS_TRANSACTION会调用父类ActivityManagerNative的onTransact方法,方法由于statusCode很多,我们只挑选了符合我们要求的部分的源码:

case START_INSTRUMENTATION_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);ComponentName className = ComponentName.readFromParcel(data);String profileFile = data.readString();int fl = data.readInt();Bundle arguments = data.readBundle();IBinder b = data.readStrongBinder();IInstrumentationWatcher w = IInstrumentationWatcher.Stub.asInterface(b);b = data.readStrongBinder();IUiAutomationConnection c = IUiAutomationConnection.Stub.asInterface(b);int userId = data.readInt();String abiOverride = data.readString();boolean res = startInstrumentation(className, profileFile, fl, arguments, w, c, userId,abiOverride);reply.writeNoException();reply.writeInt(res ? 1 : 0);return true;
}

在读取出相应数据后调用startInstrumentation方法,开始执行Instrumentation

启动Instrumentation

所以回到之前am命令的处理,实际调用的是ActivityManagerService的startInstrumentation方法。所以Instrumentation的启动是由ActivityManagerService.startInstrumentation()方法完成的

public boolean startInstrumentation(ComponentName className,String profileFile, int flags, Bundle arguments,IInstrumentationWatcher watcher, IUiAutomationConnection uiAutomationConnection,int userId, String abiOverride) {enforceNotIsolatedCaller("startInstrumentation");userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),userId, false, ALLOW_FULL_ONLY, "startInstrumentation", null);// Refuse possible leaked file descriptorsif (arguments != null && arguments.hasFileDescriptors()) {throw new IllegalArgumentException("File descriptors passed in Bundle");}synchronized(this) {InstrumentationInfo ii = null;ApplicationInfo ai = null;try {ii = mContext.getPackageManager().getInstrumentationInfo(className, STOCK_PM_FLAGS);ai = AppGlobals.getPackageManager().getApplicationInfo(ii.targetPackage, STOCK_PM_FLAGS, userId);} catch (PackageManager.NameNotFoundException e) {} catch (RemoteException e) {}if (ii == null) {reportStartInstrumentationFailure(watcher, className,"Unable to find instrumentation info for: " + className);return false;}if (ai == null) {reportStartInstrumentationFailure(watcher, className,"Unable to find instrumentation target package: " + ii.targetPackage);return false;}int match = mContext.getPackageManager().checkSignatures(ii.targetPackage, ii.packageName);if (match < 0 && match != PackageManager.SIGNATURE_FIRST_NOT_SIGNED) {String msg = "Permission Denial: starting instrumentation "+ className + " from pid="+ Binder.getCallingPid()+ ", uid=" + Binder.getCallingPid()+ " not allowed because package " + ii.packageName+ " does not have a signature matching the target "+ ii.targetPackage;reportStartInstrumentationFailure(watcher, className, msg);throw new SecurityException(msg);}final long origId = Binder.clearCallingIdentity();// Instrumentation can kill and relaunch even persistent processesforceStopPackageLocked(ii.targetPackage, -1, true, false, true, true, false, userId,"start instr");ProcessRecord app = addAppLocked(ai, false, abiOverride);app.instrumentationClass = className;app.instrumentationInfo = ai;app.instrumentationProfileFile = profileFile;app.instrumentationArguments = arguments;app.instrumentationWatcher = watcher;app.instrumentationUiAutomationConnection = uiAutomationConnection;app.instrumentationResultClass = className;Binder.restoreCallingIdentity(origId);}return true;
}

该方法做了如下的事情:

  • 检查TestRunner是否存在
  • 检查TargetPackage是否存在
  • 检测签名是否一致
  • 强制关闭被测包
  • 通过addAppLocked方法创建ProcessRecord

addAppLocked方法的源码如下:

final ProcessRecord addAppLocked(ApplicationInfo info, boolean isolated,String abiOverride) {ProcessRecord app;if (!isolated) {app = getProcessRecordLocked(info.processName, info.uid, true);} else {app = null;}if (app == null) {app = newProcessRecordLocked(info, null, isolated, 0);mProcessNames.put(info.processName, app.uid, app);if (isolated) {mIsolatedProcesses.put(app.uid, app);}updateLruProcessLocked(app, false, null);updateOomAdjLocked();}// This package really, really can not be stopped.try {AppGlobals.getPackageManager().setPackageStoppedState(info.packageName, false, UserHandle.getUserId(app.uid));} catch (RemoteException e) {} catch (IllegalArgumentException e) {Slog.w(TAG, "Failed trying to unstop package "+ info.packageName + ": " + e);}if ((info.flags&(ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PERSISTENT))== (ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PERSISTENT)) {app.persistent = true;app.maxAdj = ProcessList.PERSISTENT_PROC_ADJ;}if (app.thread == null && mPersistentStartingProcesses.indexOf(app) < 0) {mPersistentStartingProcesses.add(app);startProcessLocked(app, "added application", app.processName, abiOverride,null /* entryPoint */, null /* entryPointArgs */);}return app;
}

之后会调用startProcessLocked方法启动进程,启动进程的过程就比较复杂了,暂时不去分析了,具体调用流程如下:startProcessLocked->Process.start->startViaZygote->zygoteSendArgsAndGetResult,zygoteSendArgsAndGetResult函数最终实现的,是向socket服务端写书据,把创建进程的请求通过socket通讯方式让framework的进程孵化类zygote创建新进程。而数据就是argsForZygote(一个以字符串List形式的把Process.start()所有调用参数都包含在里面的变量),具体的启动过程可以参考:android进程创建分析

socket服务端收到创建新进程的请求,ZygoteConnection.runOnce()接收到新进程的参数,然后调用Zygote.forkAndSpecialize()来fork一个子进程,在子进程中会接着关闭socket,调用ZygoteInit.invokeStaticMain(cloader, className, mainArgs),即调用ActivityThread.main()。 新的应用进程会从ActivityThread 的 main()函数处开始执行。

ActivityThread,新的进程

首先来看入口,main函数:

public static void main(String[] args) {SamplingProfilerIntegration.start();// CloseGuard defaults to true and can be quite spammy.  We// disable it here, but selectively enable it later (via// StrictMode) on debug builds, but using DropBox, not logs.CloseGuard.setEnabled(false);Environment.initForCurrentUser();// Set the reporter for event logging in libcoreEventLogger.setReporter(new EventLoggingReporter());Security.addProvider(new AndroidKeyStoreProvider());// Make sure TrustedCertificateStore looks in the right place for CA certificatesfinal File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());TrustedCertificateStore.setDefaultUserDirectory(configDir);Process.setArgV0("<pre-initialized>");Looper.prepareMainLooper();ActivityThread thread = new ActivityThread();thread.attach(false);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();}if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");
}

我们看到main方法初始化了主线程的消息队列,实例化了一个ActivityThread对象,然后调用了它的attach方法:

private void attach(boolean system) {sCurrentActivityThread = this;mSystemThread = system;if (!system) {ViewRootImpl.addFirstDrawHandler(new Runnable() {@Overridepublic void run() {ensureJitEnabled();}});android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",UserHandle.myUserId());RuntimeInit.setApplicationObject(mAppThread.asBinder());final IActivityManager mgr = ActivityManagerNative.getDefault();try {mgr.attachApplication(mAppThread);} catch (RemoteException ex) {// Ignore}// Watch for getting close to heap limit.BinderInternal.addGcWatcher(new Runnable() {@Override public void run() {if (!mSomeActivitiesChanged) {return;}Runtime runtime = Runtime.getRuntime();long dalvikMax = runtime.maxMemory();long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();if (dalvikUsed > ((3*dalvikMax)/4)) {if (DEBUG_MEMORY_TRIM) Slog.d(TAG, "Dalvik max=" + (dalvikMax/1024)+ " total=" + (runtime.totalMemory()/1024)+ " used=" + (dalvikUsed/1024));mSomeActivitiesChanged = false;try {mgr.releaseSomeActivities(mAppThread);} catch (RemoteException e) {}}}});} else {// Don't set application object here -- if the system crashes,// we can't display an alert, we just want to die die die.android.ddm.DdmHandleAppName.setAppName("system_process",UserHandle.myUserId());try {mInstrumentation = new Instrumentation();ContextImpl context = ContextImpl.createAppContext(this, getSystemContext().mPackageInfo);mInitialApplication = context.mPackageInfo.makeApplication(true, null);mInitialApplication.onCreate();} catch (Exception e) {throw new RuntimeException("Unable to instantiate Application():" + e.toString(), e);}}// add dropbox logging to libcoreDropBox.setReporter(new DropBoxReporter());ViewRootImpl.addConfigCallback(new ComponentCallbacks2() {@Overridepublic void onConfigurationChanged(Configuration newConfig) {synchronized (mResourcesManager) {// We need to apply this change to the resources// immediately, because upon returning the view// hierarchy will be informed about it.if (mResourcesManager.applyConfigurationToResourcesLocked(newConfig, null)) {// This actually changed the resources!  Tell// everyone about it.if (mPendingConfiguration == null ||mPendingConfiguration.isOtherSeqNewer(newConfig)) {mPendingConfiguration = newConfig;sendMessage(H.CONFIGURATION_CHANGED, newConfig);}}}}@Overridepublic void onLowMemory() {}@Overridepublic void onTrimMemory(int level) {}});
}

我们看到由于是非系统初始化(不是系统启动时启动的进程),传入的参数为false,我们重点关注前面一半的逻辑。这里又出现了

final IActivityManager mgr = ActivityManagerNative.getDefault();

有了之前的经验我们已经知道这是指向的ActivityManagerService,然后调用了

mgr.attachApplication(mAppThread);

调用ActivityManagerService.attachApplication

public final void attachApplication(IApplicationThread thread) {synchronized (this) {int callingPid = Binder.getCallingPid();final long origId = Binder.clearCallingIdentity();attachApplicationLocked(thread, callingPid);Binder.restoreCallingIdentity(origId);}
}

接着走到attachApplicationLocked,这个方法比较长,为了节约篇幅,不贴源码了,会调用ActivityThread的bindApplication方法

thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,isRestrictedBackupMode || !normalMode, app.persistent,new Configuration(mConfiguration), app.compat,getCommonServicesLocked(app.isolated),mCoreSettingsObserver.getCoreSettingsLocked());

而bindApplication做的事情是数据绑定并发送消息(源码部分节选)

AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
data.providers = providers;
data.instrumentationName = instrumentationName;
data.instrumentationArgs = instrumentationArgs;
data.instrumentationWatcher = instrumentationWatcher;
data.instrumentationUiAutomationConnection = instrumentationUiConnection;
data.debugMode = debugMode;
data.enableOpenGlTrace = enableOpenGlTrace;
data.restrictedBackupMode = isRestrictedBackupMode;
data.persistent = persistent;
data.config = config;
data.compatInfo = compatInfo;
data.initProfilerInfo = profilerInfo;
sendMessage(H.BIND_APPLICATION, data);

在handleMessage方法中可以看到接到Message后的处理

case BIND_APPLICATION:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");AppBindData data = (AppBindData)msg.obj;handleBindApplication(data);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;

handleBindApplication方法实在是太长了,我们就截取和Instrumentation相关的部分吧

if (data.instrumentationName != null) {InstrumentationInfo ii = null;try {ii = appContext.getPackageManager().getInstrumentationInfo(data.instrumentationName, 0);} catch (PackageManager.NameNotFoundException e) {}if (ii == null) {throw new RuntimeException("Unable to find instrumentation info for: "+ data.instrumentationName);}mInstrumentationPackageName = ii.packageName;mInstrumentationAppDir = ii.sourceDir;mInstrumentationSplitAppDirs = ii.splitSourceDirs;mInstrumentationLibDir = ii.nativeLibraryDir;mInstrumentedAppDir = data.info.getAppDir();mInstrumentedSplitAppDirs = data.info.getSplitAppDirs();mInstrumentedLibDir = data.info.getLibDir();ApplicationInfo instrApp = new ApplicationInfo();instrApp.packageName = ii.packageName;instrApp.sourceDir = ii.sourceDir;instrApp.publicSourceDir = ii.publicSourceDir;instrApp.splitSourceDirs = ii.splitSourceDirs;instrApp.splitPublicSourceDirs = ii.splitPublicSourceDirs;instrApp.dataDir = ii.dataDir;instrApp.nativeLibraryDir = ii.nativeLibraryDir;LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,appContext.getClassLoader(), false, true, false);ContextImpl instrContext = ContextImpl.createAppContext(this, pi);try {java.lang.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);}mInstrumentation.init(this, instrContext, appContext,new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,data.instrumentationUiAutomationConnection);if (mProfiler.profileFile != null && !ii.handleProfiling&& mProfiler.profileFd == null) {mProfiler.handlingProfiling = true;File file = new File(mProfiler.profileFile);file.getParentFile().mkdirs();Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);}} else {mInstrumentation = new Instrumentation();
}if ((data.appInfo.flags&ApplicationInfo.FLAG_LARGE_HEAP) != 0) {dalvik.system.VMRuntime.getRuntime().clearGrowthLimit();
}// Allow disk access during application and provider setup. This could
// block processing ordered broadcasts, but later processing would
// probably end up doing the same disk access.
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
try {// If the app is being launched for full backup or restore, bring it up in// a restricted environment with the base application class.Application app = data.info.makeApplication(data.restrictedBackupMode, null);mInitialApplication = app;// don't bring up providers in restricted mode; they may depend on the// app's custom Application classif (!data.restrictedBackupMode) {List<ProviderInfo> providers = data.providers;if (providers != null) {installContentProviders(app, providers);// For process that contains content providers, we want to// ensure that the JIT is enabled "at some point".mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);}}// Do this after providers, since instrumentation tests generally start their// test thread at this point, and we don't want that racing.try {mInstrumentation.onCreate(data.instrumentationArgs);}catch (Exception e) {throw new RuntimeException("Exception thrown in onCreate() of "+ data.instrumentationName + ": " + e.toString(), e);}try {mInstrumentation.callApplicationOnCreate(app);} catch (Exception e) {if (!mInstrumentation.onException(app, e)) {throw new RuntimeException("Unable to create application " + app.getClass().getName()+ ": " + e.toString(), e);}}
} finally {StrictMode.setThreadPolicy(savedPolicy);
}

方法中首先初始化了mInstrumentation,此处Load的Class即am instrument命令传入的TestRunner

java.lang.ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)cl.loadClass(data.instrumentationName.getClassName()).newInstance();

然后对mInstrumentation进行了初始化

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

调用mInstrumentation的onCreate方法进行执行(参考下文的InstrumentationTestRunner介绍)

mInstrumentation.onCreate(data.instrumentationArgs);

最后调用mInstrumentation的callApplicationOnCreate方法启动测试包的Application

mInstrumentation.callApplicationOnCreate(app);

至此从命令到启动测试的流程就分析完了,下面我们就将从Instrumentation类出发,看看我们常见的Instrumentation,InstrumentationTestRunner,ActivityInstrumentationTestCase2类都分别做了哪些事

Instrumentation类源码分析

弄清楚了Instrumentation的启动之后,我们来分析下Instrumentation这个类及其功能吧

Instrumentation 流程控制函数分析

在Instrumentation类中有几个关键的流程控制函数,我们先来分析下这些方法:

onCreate方法

在Instrumentation中是个空方法,之前我们有提到ActivityThread的handleBindApplication方法会调用InstrumentationTestRunner的onCreate方法来启动测试代码,InstrumentationTestRunner重写了这个类(下文分析InstrumentationTestRunner时会分析)

start方法

start方法会创建一个新的线程来执行instrumentation,通常由继承Instrumentation的类来调用该方法,InstrumentationTestRunner在onCreate方法的最后面调用了这个方法执行instrumentation,下面是Instrumentation中start方法的源码

public void start() {if (mRunner != null) {throw new RuntimeException("Instrumentation already started");}mRunner = new InstrumentationThread("Instr: " + getClass().getName());mRunner.start();
}

其中mRunner是一个InstrumentationThread对象,是负责运行Instrumentation的线程对象,运行该方法会触发Instrumentation的onStart方法

private final class InstrumentationThread extends Thread {public InstrumentationThread(String name) {super(name);}public void run() {try {Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);} catch (RuntimeException e) {Log.w(TAG, "Exception setting priority of instrumentation thread "                                            + Process.myTid(), e);                                                                             }if (mAutomaticPerformanceSnapshots) {startPerformanceSnapshot();}onStart();}
}

onStart方法

同onCreate方法一样,在Instrumentation中是个空方法,通过重写该方法能够在测试使执行时产生一些阻塞性动作,Google的原文是这样的

/*** Method where the instrumentation thread enters execution.  This allows* you to run your instrumentation code in a separate thread than the* application, so that it can perform blocking operation such as* {@link #sendKeySync} or {@link #startActivitySync}.* * <p>You will typically want to call finish() when this function is done,* to end your instrumentation.*/

onException方法

onException方法会在系统catch到Exception时由ActivityThread调用,在Instrumentation中它仅仅返回false,通过重写该方法可以在需要时返回true,来自定义异常的处理,此时发生异常的被测工程会继续执行下去而忽略该异常的发生,转交给自定义实现处理。

sendStatus方法

sendStatus方法是在测试执行过程中状态改变时会调用的方法,Instrumentation中已定义了如下四种状态,用户也可以定义自己需要的状态并在合适的地方调用sendStatus方法发送自定义的状态

/*** The test is starting.*/
public static final int REPORT_VALUE_RESULT_START = 1;
/*** The test completed successfully.*/
public static final int REPORT_VALUE_RESULT_OK = 0;
/*** The test completed with an error.*/
public static final int REPORT_VALUE_RESULT_ERROR = -1;
/*** The test completed with a failure.*/
public static final int REPORT_VALUE_RESULT_FAILURE = -2;

finish方法

finish方法的调用会终止Instrumentation的执行,使被测应用退出,其源码如下:

public void finish(int resultCode, Bundle results) {if (mAutomaticPerformanceSnapshots) {endPerformanceSnapshot();}if (mPerfMetrics != null) {if (results == null) {results = new Bundle();}results.putAll(mPerfMetrics);}if (mUiAutomation != null) {mUiAutomation.disconnect();mUiAutomation = null;}mThread.finishInstrumentation(resultCode, results);
}

Instrumentation中的几个内部类

Instrumentation定义了几个内部类,为了能够更好的阅读后文,我们先来学习以下这些内部类及其作用

ActivityResult

定义了Activity向源Activity传递的执行结果,有两个成员变量,一个是

int mResultCode;
Intent mResultData;

ActivityMonitor

ActivityMonitor是用来监视应用中单个活动的,它可以用来监视一些指定的Intent。创建好ActivityMonitor的实例后,通过调用Instrumentation.addMonitor函数来添加这个实例。当Activity启动后,系统会匹配Instrumentation中的ActivityMonitory实例列表,如果匹配,就会累加计数器。

ActivityMonitor同样可以被用于获取新创建的Activity,通过waitForActivity方法,可以返回一个符合IntentFilter的Activity对象

ActivityMonitor有6个成员变量
private final IntentFilter mWhich;    //IntentFilter,被监视的Acitivity的条件
private final String mClass;    //被监视的Acitivity类名
private final ActivityResult mResult;    //如果monitor符合条件,返回的ActivityResult
private final boolean mBlock;    //决定monitor是否会阻止activity的启动(使用mResult启动,true时阻止),或者继续activity的启动
int mHits = 0;    //记录被监视的Activity的启动次数
Activity mLastActivity = null;    //最后匹配成功的Activity
ActivityMonitor的两种构造函数

使用IntentFilter做筛选条件

public ActivityMonitor(IntentFilter which, ActivityResult result, boolean block) {mWhich = which;mClass = null;mResult = result;mBlock = block;
}

使用Activity类名做筛选条件

public ActivityMonitor(String cls, ActivityResult result, boolean block) {mWhich = null;mClass = cls;mResult = result;mBlock = block;
}
其他关键方法

match方法在Instrumentation启动Activity时会被调用,根据初始化ActivityMonitor时使用的是IntentFilter还是Activity类名有不同的处理,但都是用于检查被添加的ActivityMonitor对象是否和新启动的Activity匹配,如果不一致则返回false,比较一致就把新的Activity写入ActivityMonitor对象的最后匹配成功Activity属性中

final boolean match(Context who, Activity activity, Intent intent) {synchronized (this) {if (mWhich != null&& mWhich.match(who.getContentResolver(), intent,true, "Instrumentation") < 0) {return false;}if (mClass != null) {String cls = null;if (activity != null) {cls = activity.getClass().getName();} else if (intent.getComponent() != null) {cls = intent.getComponent().getClassName();}if (cls == null || !mClass.equals(cls)) {return false;}}if (activity != null) {mLastActivity = activity;notifyAll();}return true;}
}

waitForActivity方法是一个阻塞方法,会一直等待直至有启动的Acitivity成功匹配,否则会一直阻塞

/*** Block until an Activity is created that matches this monitor,* returning the resulting activity.** @return Activity*/
public final Activity waitForActivity() {synchronized (this) {while (mLastActivity == null) {try {wait();} catch (InterruptedException e) {}}Activity res = mLastActivity;mLastActivity = null;return res;}
}

waitForActivityWithTimeout方法作用同waitForActivity一致,但是有一个timeout,超时后就不会继续阻塞Instrumentation的执行了。

/*** Block until an Activity is created that matches this monitor, * returning the resulting activity or till the timeOut period expires.* If the timeOut expires before the activity is started, return null. * * @param timeOut Time to wait before the activity is created.* * @return Activity*/
public final Activity waitForActivityWithTimeout(long timeOut) {synchronized (this) {if (mLastActivity == null) {try {wait(timeOut);} catch (InterruptedException e) {}}if (mLastActivity == null) {return null;} else {Activity res = mLastActivity;mLastActivity = null;return res;}}
}

InstrumentationThread

前文已介绍,负责运行Instrumentation的线程对象

EmptyRunnable

一个run方法为空的Runnable

SyncRunnable

同步任务类,提供一个方法waitForComplete,使任务完成前会一直阻塞,在Instrumentation.runOnMainSync方法中会被使用到,在主线程做操作时会阻塞Instrumentation的执行

private static final class SyncRunnable implements Runnable {private final Runnable mTarget;private boolean mComplete;public SyncRunnable(Runnable target) {mTarget = target;}public void run() {mTarget.run();synchronized (this) {mComplete = true;notifyAll();}}public void waitForComplete() {synchronized (this) {while (!mComplete) {try {wait();} catch (InterruptedException e) {}}}}
}

ActivityWaiter

一个数据结构,包含了一个Intent和一个Activity,主要作用是用于判断Intent是否能被Activity处理

ActivityGoing

实现了MessageQueue.IdleHandler接口,带有一个ActivityWaiter的成员,queueIdle方法的实现是将这个ActivityWaiter的从mWaitingActivities列表中移除

private final class ActivityGoing implements MessageQueue.IdleHandler {private final ActivityWaiter mWaiter;public ActivityGoing(ActivityWaiter waiter) {mWaiter = waiter;}public final boolean queueIdle() {synchronized (mSync) {mWaitingActivities.remove(mWaiter);mSync.notifyAll();}return false;}
}

Idler

实现了MessageQueue.IdleHandler接口,检测线程是否处于idle状态并做相应操作

Instrumentation中一些公有方法的介绍

性能相关

Profiling是一个Android系统自带的用于性能分析的系统,这里暂不介绍,仅记录功能入口,如果后期有性能同学对此做了深入研究会链接到相关页面

  • public boolean isProfiling():判断是否开启了Profiling模式
  • public void startProfiling():生成Profiling相关文件,并开始记录每个方法执行
  • public void stopProfiling():停止Profiling模式
    PerformanceSnapshots没有找到相关的文档信息,从其关键类PerformanceCollector的注释来看是记录一些性能数据用的
  • public void setAutomaticPerformanceSnapshots():
  • public void startPerformanceSnapshot():
  • public void endPerformanceSnapshot():

线程相关

以下三个方法贴了原文注释,主要是不知道如何合适的翻译,大体上前两个方法用于等待被测Activity处于idle状态(一个同步,一个异步),然后做相应操作,第三个方法是在被测Activity主线程做指定的操作

  • public void waitForIdle(Runnable recipient):Schedule a callback for when the application’s main thread goes idle(has no more events to process)
  • public void waitForIdleSync():Synchronously wait for the application to be idle. Can not be called from the main application thread – use start() to execute instrumentation in its own thread.
  • public void runOnMainSync(Runnable runner):Execute a call on the application’s main thread, blocking until it is complete. Useful for doing things that are not thread-safe, such as looking at or modifying the view hierarchy.

启动Activity

  • public Activity startActivitySync(Intent intent)
    这是是通过Instrumentation启动Activity的主要方法,我们先来看看源码及注释是怎样的,再来对着源码分析下Activity是如何启动的
/*** Start a new activity and wait for it to begin running before returning.* In addition to being synchronous, this method as some semantic* differences from the standard {@link Context#startActivity} call: the* activity component is resolved before talking with the activity manager* (its class name is specified in the Intent that this method ultimately* starts), and it does not allow you to start activities that run in a* different process.  In addition, if the given Intent resolves to* multiple activities, instead of displaying a dialog for the user to* select an activity, an exception will be thrown.* * <p>The function returns as soon as the activity goes idle following the* call to its {@link Activity#onCreate}.  Generally this means it has gone* through the full initialization including {@link Activity#onResume} and* drawn and displayed its initial window.* * @param intent Description of the activity to start.* * @see Context#startActivity*/
public Activity startActivitySync(Intent intent) {validateNotAppThread();synchronized (mSync) {intent = new Intent(intent);ActivityInfo ai = intent.resolveActivityInfo(getTargetContext().getPackageManager(), 0);if (ai == null) {throw new RuntimeException("Unable to resolve activity for: " + intent);}String myProc = mThread.getProcessName();if (!ai.processName.equals(myProc)) {// todo: if this intent is ambiguous, look here to see if// there is a single match that is in our package.throw new RuntimeException("Intent in process "+ myProc + " resolved to different process "+ ai.processName + ": " + intent);}intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));final ActivityWaiter aw = new ActivityWaiter(intent);if (mWaitingActivities == null) {mWaitingActivities = new ArrayList();}mWaitingActivities.add(aw);getTargetContext().startActivity(intent);do {try {mSync.wait();} catch (InterruptedException e) {}} while (mWaitingActivities.contains(aw));return aw.activity;}
}

从官方注释我们可以看到,startActivitySync方法的功能也是启动Activity,同Context.startActivity方法不同的是,startActivitySync是无法启动进程外的Activity的。另外,如果用于启动的Intent可以被多个Activity接受,该方法会直接抛出异常而不是弹出选择对话框让用户选择需要使用的程序。本方法作为一个同步方法,在Activity启动后处于idle状态时返回,意味着Activity会走完onCreate,onStart,onResume的方法,并完成UI初始化。

下面我们看看这个方法的具体实现

  • 首先会获取到被测APK的ActivityInfo(可以通过其获取到相关的Activity)
  • 然后判断是否是运行在同一进程(具体实现不是很明白,有兴趣的调研下补充上来?)
  • 再将从Activity中提取的Component信息设置给Intent(决定谁来处理Intent)
  • 之后使用该Intent初始化一个ActivityWaiter对象,并加入到mWaitingActivities列表中
  • 获取目标的Context并通过intent启动Acticity
  • 在之前生成的ActivityWaiter对象从mWaitingActivities中移除之前一直会处于阻塞状态
    那么问题来了,这个ActivityWaiter对象是何时被移除的呢?Instrumentation提供了操作Activity生命周期的方法(下面也会提到),使用Instrumentation启动Activity时会调用callActivityOnCreate方法,处理Acticity的onCreate,而callActivityOnCreate分成了3个步骤
prePerformCreate(activity);
activity.performCreate(icicle);
postPerformCreate(activity);

在prePerformCreate方法中

private void prePerformCreate(Activity activity) {if (mWaitingActivities != null) {synchronized (mSync) {final int N = mWaitingActivities.size();for (int i=0; i<N; i++) {final ActivityWaiter aw = mWaitingActivities.get(i);final Intent intent = aw.intent;if (intent.filterEquals(activity.getIntent())) {aw.activity = activity;mMessageQueue.addIdleHandler(new ActivityGoing(aw));}}}}
}

针对mWaitingActivities列表中已加入的每一个ActivityWaiter,判断intent是否符合目标Activity,如果符合则将目标Activity绑定到ActivityWaiter对象。然后发送消息,在空闲时调用这个以aw为参数的ActivityGoing对象,上文中我们介绍过ActivityGoing类,其功能是在目标Activity空闲后将指定的ActivityWaiter从mWaitingActivities列表中移除。

至此就实现了启动Activity并阻塞到Activity启动的全过程

下面的一系列方法是通过Instrumentation启动Activity的,具体使用场景暂未分析

  • public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)
  • public void execStartActivities(Context who, IBinder contextThread, IBinder token, Activity target, Intent[] intents, Bundle options)
  • public void execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, Intent[] intents, Bundle options, int userId)
  • public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment target, Intent intent, int requestCode, Bundle options)
  • public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options, UserHandle user)
  • public ActivityResult execStartActivityAsCaller(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options, int userId)
  • public void execStartActivityFromAppTask(Context who, IBinder contextThread, IAppTask appTask,Intent intent, Bundle options)

ActivityMonitor相关

addMonitor共有三种重载,作用都是增加向当前Instrumentation添加一个ActivityMonitor,其中后两种会同时返回构造出的ActivityMonitor对象

  • public void addMonitor(ActivityMonitor monitor)
  • public ActivityMonitor addMonitor(IntentFilter filter, ActivityResult result, boolean block)
  • public ActivityMonitor addMonitor(String cls, ActivityResult result, boolean block)

其他相关的还有如下的方法,其中waitForMonitorWithTimeout(waitForMonitor,可能会造成程序阻塞,不推荐使用)可以用于检测是否启动了符合指定ActivityMonitor的Activity,来验证某些操作是否启动了正确的Activity:

  • public boolean checkMonitorHit(ActivityMonitor monitor, int minHits):检查指定的ActivityMonitor的匹配次数是否达到minHits,minHits会在各种execStartActivity(上一节)中intent和ActivityMonitor匹配时+1。
  • public Activity waitForMonitor(ActivityMonitor monitor):等待指定的ActivityMonitor被命中(符合ActivityMonitor条件的Activity被启动),并返回启动的Activity对象,同时将该ActivityMonitor从mActivityMonitors列表中移除,如果没有等到会一直阻塞,代码如下:
public Activity waitForMonitor(ActivityMonitor monitor) {Activity activity = monitor.waitForActivity();synchronized (mSync) {mActivityMonitors.remove(monitor);}return activity;
}
  • public Activity waitForMonitorWithTimeout(ActivityMonitor monitor, long timeOut):等待指定的ActivityMonitor被命中(符合ActivityMonitor条件的Activity被启动),并返回启动的Activity对象,同时将该ActivityMonitor从mActivityMonitors列表中移除,在计时器结束前没有等到会一直阻塞,代码类似,不贴了。
  • public void removeMonitor(ActivityMonitor monitor):从mActivityMonitors列表中移除指定的ActivityMonitor

操作按键

以下这个方法提供了在主线程中的一些操作按键的方法,方法名都很直白,不用一一解释了吧

  • public boolean invokeMenuActionSync(Activity targetActivity, int id, int flag)
  • public boolean invokeContextMenuAction(Activity targetActivity, int id, int flag)
  • public void sendStringSync(String text)
  • public void sendKeySync(KeyEvent event)
  • public void sendKeyDownUpSync(int key)
  • public void sendCharacterSync(int keyCode)
  • public void sendPointerSync(MotionEvent event)
  • public void sendTrackballEventSync(MotionEvent event)

处理Activity生命周期相关

以下的方法提供了各种操纵Activity生命周期的方法,通过重写这些方法可以修改Activity在每个生命周期方法被调用时的行为。

  • public void callActivityOnCreate(Activity activity, Bundle icicle)
  • public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState)
  • public void callActivityOnDestroy(Activity activity)
  • public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState)
  • public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState, PersistableBundle persistentState)
  • public void callActivityOnPostCreate(Activity activity, Bundle icicle)
  • public void callActivityOnPostCreate(Activity activity, Bundle icicle, PersistableBundle persistentState)
  • public void callActivityOnNewIntent(Activity activity, Intent intent)
  • public void callActivityOnNewIntent(Activity activity, ReferrerIntent intent)
  • public void callActivityOnStart(Activity activity)
  • public void callActivityOnRestart(Activity activity)
  • public void callActivityOnResume(Activity activity)
  • public void callActivityOnStop(Activity activity)
  • public void callActivityOnSaveInstanceState(Activity activity, Bundle outState)
  • public void callActivityOnSaveInstanceState(Activity activity, Bundle outState, PersistableBundle outPersistentState)
  • public void callActivityOnPause(Activity activity)
  • public void callActivityOnUserLeaving(Activity activity)

其他

  • public void setInTouchMode(boolean inTouch):Set to true to be in touch mode, false to be in focus mode.
  • public void startAllocCounting(): Starts allocation counting. This triggers a gc and resets the counts.
  • public void stopAllocCounting(): Stops allocation counting.
  • public Bundle getAllocCounts(): Returns a bundle with the current results from the allocation counting.
  • public Bundle getBinderCounts(): Returns a bundle with the counts for various binder counts for this process. Currently the only two that are reported are the number of send and the number of received transactions.
  • public UiAutomation getUiAutomation():获取UiAutomation对象,UI自动化测试相关
  • public Application newApplication(ClassLoader cl, String className, Context context):新建Application,测试Application时可用,我们通常不会使用到
  • static public Application newApplication(Class

InstrumentationTestRunner源码分析

InstrumentationTestRunner实际上是继承自Instrumentation,所以上面所有对Instrumentation类的分析都适用于它,我们在这里主要要看的是它多了些什么新玩意,按照之前的逻辑我们先来看流程控制函数

InstrumentationTestRunner流程控制函数分析

onCreate方法

InstrumentationTestRunner重写了onCreate,在之前的启动流程分析中我们也知道,Instrumentation启动的入口即是onCreate方法:

public void onCreate(Bundle arguments) {super.onCreate(arguments);mArguments = arguments;// Apk paths used to search for test classes when using TestSuiteBuilders.String[] apkPaths ={getTargetContext().getPackageCodePath(), getContext().getPackageCodePath()};ClassPathPackageInfoSource.setApkPaths(apkPaths);Predicate<TestMethod> testSizePredicate = null;Predicate<TestMethod> testAnnotationPredicate = null;Predicate<TestMethod> testNotAnnotationPredicate = null;String testClassesArg = null;boolean logOnly = false;if (arguments != null) {// Test class name passed as an argument should override any meta-data declaration.testClassesArg = arguments.getString(ARGUMENT_TEST_CLASS);mDebug = getBooleanArgument(arguments, "debug");mJustCount = getBooleanArgument(arguments, "count");mSuiteAssignmentMode = getBooleanArgument(arguments, "suiteAssignment");mPackageOfTests = arguments.getString(ARGUMENT_TEST_PACKAGE);testSizePredicate = getSizePredicateFromArg(arguments.getString(ARGUMENT_TEST_SIZE_PREDICATE));testAnnotationPredicate = getAnnotationPredicate(arguments.getString(ARGUMENT_ANNOTATION));testNotAnnotationPredicate = getNotAnnotationPredicate(arguments.getString(ARGUMENT_NOT_ANNOTATION));logOnly = getBooleanArgument(arguments, ARGUMENT_LOG_ONLY);mCoverage = getBooleanArgument(arguments, "coverage");mCoverageFilePath = arguments.getString("coverageFile");try {Object delay = arguments.get(ARGUMENT_DELAY_MSEC);  // Accept either string or intif (delay != null) mDelayMsec = Integer.parseInt(delay.toString());} catch (NumberFormatException e) {Log.e(LOG_TAG, "Invalid delay_msec parameter", e);}}TestSuiteBuilder testSuiteBuilder = new TestSuiteBuilder(getClass().getName(),getTargetContext().getClassLoader());if (testSizePredicate != null) {testSuiteBuilder.addRequirements(testSizePredicate);}if (testAnnotationPredicate != null) {testSuiteBuilder.addRequirements(testAnnotationPredicate);}if (testNotAnnotationPredicate != null) {testSuiteBuilder.addRequirements(testNotAnnotationPredicate);}//判断是否传入了参数指定测试类,并做相应处理if (testClassesArg == null) {if (mPackageOfTests != null) {testSuiteBuilder.includePackages(mPackageOfTests);} else {TestSuite testSuite = getTestSuite();if (testSuite != null) {testSuiteBuilder.addTestSuite(testSuite);} else {// no package or class bundle arguments were supplied, and no test suite// provided so add all tests in applicationtestSuiteBuilder.includePackages("");}}} else {parseTestClasses(testClassesArg, testSuiteBuilder);}testSuiteBuilder.addRequirements(getBuilderRequirements());mTestRunner = getAndroidTestRunner();mTestRunner.setContext(getTargetContext());mTestRunner.setInstrumentation(this);mTestRunner.setSkipExecution(logOnly);mTestRunner.setTest(testSuiteBuilder.build());mTestCount = mTestRunner.getTestCases().size();if (mSuiteAssignmentMode) {mTestRunner.addTestListener(new SuiteAssignmentPrinter());} else {WatcherResultPrinter resultPrinter = new WatcherResultPrinter(mTestCount);mTestRunner.addTestListener(new TestPrinter("TestRunner", false));mTestRunner.addTestListener(resultPrinter);mTestRunner.setPerformanceResultsWriter(resultPrinter);}start();
}

可以看到方法根据的参数arguments做了一系列的处理,这些arguments即是在am instrument命令中传入的各种-e后的键值对参数,初始化mTestRunner,然后调用start函数

mTestRunner是一个AndroidTestRunner对象,主要用于记录TestCase和TestResult,同时维护了一个TestListener类型的List,然后用mTestRunner.setTest(testSuiteBuilder.build())获取测试用例集并传递给mTestRunner。关于AndroidTestRunner和TestSuite相关的内容我们后续分析。

onStart方法

InstrumentationTestRunner并没有重写start方法,所以start方法会调用父类Instrumentation的start,启动一个InstrumentationThread,调用onStart方法,onStart方法是被重写过了的:

public void onStart() {prepareLooper();if (mJustCount) {mResults.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);mResults.putInt(REPORT_KEY_NUM_TOTAL, mTestCount);finish(Activity.RESULT_OK, mResults);} else {if (mDebug) {Debug.waitForDebugger();}ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();PrintStream writer = new PrintStream(byteArrayOutputStream);try {StringResultPrinter resultPrinter = new StringResultPrinter(writer);mTestRunner.addTestListener(resultPrinter);long startTime = System.currentTimeMillis();mTestRunner.runTest();long runTime = System.currentTimeMillis() - startTime;resultPrinter.printResult(mTestRunner.getTestResult(), runTime);} catch (Throwable t) {// catch all exceptions so a more verbose error message can be outputtedwriter.println(String.format("Test run aborted due to unexpected exception: %s",t.getMessage()));t.printStackTrace(writer);} finally {mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT,String.format("\nTest results for %s=%s",mTestRunner.getTestClassName(),byteArrayOutputStream.toString()));if (mCoverage) {generateCoverageReport();}writer.close();finish(Activity.RESULT_OK, mResults);}}
}

可以看到方法中先是增加了一个StringResultPrinter类型的TestListener,然后调用了mTestRunner的runTest方法执行测试用例,如果设置生成覆盖率报告则调用generateCoverageReport方法生成覆盖率报告,最后调用finish方法返回测试结果并停止被测app的运行。

测试集与测试用例(TestSuite相关)

在前文中我们看到了Instrumentation执行测试用例的过程,那么这些测试用例是如何被框架识别的呢?

我们记得在InstrumentationTestRunner的onCreate方法中有这样一句:

mTestRunner.setTest(testSuiteBuilder.build());

是用来设定测试用例集的,我们就从这里出发看看如何从测试脚本中找到测试用例集的:

TestGrouping类

TestGrouping类的成员变量

private static final String LOG_TAG = "TestGrouping";
SortedSet<Class<? extends TestCase>> testCaseClasses;
protected String firstIncludedPackage = null;
private ClassLoader classLoader;

TestGrouping类的构造函数

InstrumentationTestRunner中调用的构造函数,其中SORT_BY_c_QUALIFIED_NAME是一个实现了Comparotor接口的SortByFullyQualifiedName对象,提供根据Class类名排序的功能:

private final TestGrouping testGrouping = new TestGrouping(SORT_BY_c_QUALIFIED_NAME);

对应的构造函数,使用指定的comparator初始化一个TreeSet用于存储TestCase

public TestGrouping(Comparator<Class<? extends TestCase>> comparator) {testCaseClasses = new TreeSet<Class<? extends TestCase>>(comparator);
}

TestGrouping类的共有函数

getTests方法

将所有的testcase以List的形式返回

public List<TestMethod> getTests() {List<TestMethod> testMethods = new ArrayList<TestMethod>();for (Class<? extends TestCase> testCase : testCaseClasses) {for (Method testMethod : getTestMethods(testCase)) {testMethods.add(new TestMethod(testMethod, testCase));}}return testMethods;
}

遍历所有的测试类,然后将每个测试方法加入到testMethods列表中,getTestMethods用于筛选测试方法

protected List<Method> getTestMethods(Class<? extends TestCase> testCaseClass) {List<Method> methods = Arrays.asList(testCaseClass.getMethods());return select(methods, new TestMethodPredicate());
}

通过反射获得测试类的所有方法后使用selcet方法进行过滤,过滤器为TestMethodPredicate,我们先看selcet方法:

private <T> List<T> select(Collection<T> items, Predicate<T> predicate) {ArrayList<T> selectedItems = new ArrayList<T>();for (T item : items) {if (predicate.apply(item)) {selectedItems.add(item);}}return selectedItems;
}

可以看到实际上是针对所有的方法使用过滤器的aplly方法做检验,通过的就可以加入到返回的列表中,那么TestMethodPredicate的apply方法是怎么过滤的呢?

public boolean apply(Method method) {return ((method.getParameterTypes().length == 0) &&(method.getName().startsWith("test")) &&(method.getReturnType().getSimpleName().equals("void")));
}

可以看到设定了3个条件:

  • 无参数
  • 方法名以test开头
  • 返回值类型为void
    至此为什么在写测试用例时有这些要求就找到根源了,通过一顿扫描会把所有符合要求的测试方法形成一个TestMethod类型的List
addPackagesRecursive方法

将指定的package(包含其子package)中的所有测试类加入到当前TestGrouping中,每个给出的package至少要包含一个测试类,同时将第一个处理的package包名赋给firstIncludedPackage

public TestGrouping addPackagesRecursive(String... packageNames) {for (String packageName : packageNames) {List<Class<? extends TestCase>> addedClasses = testCaseClassesInPackage(packageName);if (addedClasses.isEmpty()) {Log.w(LOG_TAG, "Invalid Package: '" + packageName+ "' could not be found or has no tests");}testCaseClasses.addAll(addedClasses);if (firstIncludedPackage == null) {firstIncludedPackage = packageName;}}return this;
}

下面是其中的私有方法testCaseClassesInPackage,其中ClassPathPackageInfo一个用于列举给定package内的类和子package的工具类,ClassPathPackageInfoSource类负责通过扫描APK路径,获取ClassPathPackageInfo。

private List<Class<? extends TestCase>> testCaseClassesInPackage(String packageName) {ClassPathPackageInfoSource source = PackageInfoSources.forClassPath(classLoader);ClassPathPackageInfo packageInfo = source.getPackageInfo(packageName);return selectTestClasses(packageInfo.getTopLevelClassesRecursive());
}

其中的私有方法selectTestClasses,从给定的测试类集合中挑选出符合要求的测试类列表:

private List<Class<? extends TestCase>> selectTestClasses(Set<Class<?>> allClasses) {List<Class<? extends TestCase>> testClasses = new ArrayList<Class<? extends TestCase>>();for (Class<?> testClass : select(allClasses,new TestCasePredicate())) {testClasses.add((Class<? extends TestCase>) testClass);}return testClasses;
}

其中的select方法,按照指定的要求(predicate)挑选item并返回列表:

private <T> List<T> select(Collection<T> items, Predicate<T> predicate) {ArrayList<T> selectedItems = new ArrayList<T>();for (T item : items) {if (predicate.apply(item)) {selectedItems.add(item);}}return selectedItems;
}

Predicate是一个只定义了apply一个方法的接口,实现该接口能够使对象能够用于判断目标是否满足指定的要求,我们看下TestCasePredicate的具体实现:

private static class TestCasePredicate implements Predicate<Class<?>> {public boolean apply(Class aClass) {int modifiers = ((Class<?>) aClass).getModifiers();return TestCase.class.isAssignableFrom((Class<?>) aClass)&& Modifier.isPublic(modifiers)&& !Modifier.isAbstract(modifiers)&& hasValidConstructor((Class<?>) aClass);}@SuppressWarnings("unchecked")private boolean hasValidConstructor(java.lang.Class<?> aClass) {// The cast below is not necessary with the Java 5 compiler, but necessary with the Java 6 compiler,// where the return type of Class.getDeclaredConstructors() was changed// from Constructor<T>[] to Constructor<?>[]Constructor<? extends TestCase>[] constructors= (Constructor<? extends TestCase>[]) aClass.getConstructors();for (Constructor<? extends TestCase> constructor : constructors) {if (Modifier.isPublic(constructor.getModifiers())) {java.lang.Class[] parameterTypes = constructor.getParameterTypes();if (parameterTypes.length == 0 ||(parameterTypes.length == 1 && parameterTypes[0] == String.class)) {return true;}}}Log.i(LOG_TAG, String.format("TestCase class %s is missing a public constructor with no parameters " +"or a single String parameter - skipping",aClass.getName()));return false;}
}

我们看到在这个条件限制有有4个:

  • 目标class可以被转换为TestCase
  • 是一个public类
  • 不是abstrarct类
  • 有符合要求的构造函数(存在至少一个public构造方法,没有参数或者有一个参数也参数类型为String)
removePackagesRecursive方法

从当前TestGrouping中移除指定package及其子package内的测试类

public TestGrouping removePackagesRecursive(String... packageNames) {for (String packageName : packageNames) {testCaseClasses.removeAll(testCaseClassesInPackage(packageName));}return this;
}

调用的私有方法testCaseClassesInPackage,同selectTestClasses只不过这次的源是package包名:

private List<Class<? extends TestCase>> testCaseClassesInPackage(String packageName) {ClassPathPackageInfoSource source = PackageInfoSources.forClassPath(classLoader);ClassPathPackageInfo packageInfo = source.getPackageInfo(packageName);return selectTestClasses(packageInfo.getTopLevelClassesRecursive());
}

TestSuiteBuilder类

TestSuiteBuilder类的成员变量

private Context context;    //
private final TestGrouping testGrouping = new TestGrouping(SORT_BY_FULLY_QUALIFIED_NAME);    //
private final Set<Predicate<TestMethod>> predicates = new HashSet<Predicate<TestMethod>>();    //筛选条件集合
private List<TestCase> testCases;    //测试用例集
private TestSuite rootSuite;    //
private TestSuite suiteForCurrentClass;    //
private String currentClassname;    //
private String suiteName;    //Suite名称

onCreate方法中调用的构造函数

TestSuiteBuilder testSuiteBuilder = new TestSuiteBuilder(getClass().getName(),getTargetContext().getClassLoader());

对应的构造函数,将suiteName设置为TestRunner的类名,初始化testCases列表:

public TestSuiteBuilder(String name, ClassLoader classLoader) {this.suiteName = name;this.testGrouping.setClassLoader(classLoader);this.testCases = Lists.newArrayList();addRequirements(REJECT_SUPPRESSED);
}

其他公有方法

build方法

TestCase分成了两部分,一部分是获取TestGrouping后,根据每一个TestMethod是否能够满足所有的Predicates筛选出后加入列表,另一部分是通过addTestClassByName和addTestSuite直接加入的case(存储于testCases中),同样会判断是否满足所有的Predicates

public final TestSuite build() {rootSuite = new TestSuite(getSuiteName());// Keep track of current class so we know when to create a new sub-suite.currentClassname = null;try {for (TestMethod test : testGrouping.getTests()) {if (satisfiesAllPredicates(test)) {addTest(test);}}if (testCases.size() > 0) {for (TestCase testCase : testCases) {if (satisfiesAllPredicates(new TestMethod(testCase))) {addTest(testCase);}}}} catch (Exception exception) {Log.i("TestSuiteBuilder", "Failed to create test.", exception);TestSuite suite = new TestSuite(getSuiteName());suite.addTest(new FailedToCreateTests(exception));return suite;}return rootSuite;
}
addTestClassByName方法

通过指定的类名和方法名添加测试类

public TestSuiteBuilder addTestClassByName(String testClassName, String testMethodName,Context context) {AndroidTestRunner atr = new AndroidTestRunner();atr.setContext(context);atr.setTestClassName(testClassName, testMethodName);this.testCases.addAll(atr.getTestCases());return this;
}
addTestSuite方法

将testSuite中的测试类加入进来

public TestSuiteBuilder addTestSuite(TestSuite testSuite) {for (TestCase testCase : (List<TestCase>) TestCaseUtil.getTests(testSuite, true)) {this.testCases.add(testCase);}return this;
}

AndroidTestRunner

AndroidTestRunner类

成员变量

private TestResult mTestResult;    //存储测试结果
private String mTestClassName;    //当前测试的名字
private List<TestCase> mTestCases;    //TestCase集合
private Context mContext;    //测试目标APK的Context
private boolean mSkipExecution = false;    //当出现异常时是否终止执行private List<TestListener> mTestListeners = Lists.newArrayList();    //TestListener列表
private Instrumentation mInstrumentation;    //Instrumentation对象
private PerformanceResultsWriter mPerfWriter;    //PerformanceResultsWriter对象,性能相关

几个常用的公有方法

setTest方法
public void setTest(Test test) {setTest(test, test.getClass());
}

调用私有方法

private void setTest(Test test, Class<? extends Test> testClass) {mTestCases = (List<TestCase>) TestCaseUtil.getTests(test, true);if (TestSuite.class.isAssignableFrom(testClass)) {mTestClassName = TestCaseUtil.getTestName(test);} else {mTestClassName = testClass.getSimpleName();}
}

从给定的Test中获取Cases,分为两种情况:单独的Test和TestSuite

runTest方法

runTest方法是调用执行测试用例的入口

public void runTest() {runTest(createTestResult());
}

调用私有方法

public void runTest(TestResult testResult) {mTestResult = testResult;for (TestListener testListener : mTestListeners) {mTestResult.addListener(testListener);}Context testContext = mInstrumentation == null ? mContext : mInstrumentation.getContext();for (TestCase testCase : mTestCases) {setContextIfAndroidTestCase(testCase, mContext, testContext);setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);setPerformanceWriterIfPerformanceCollectorTestCase(testCase, mPerfWriter);testCase.run(mTestResult);}
}

设定了mTestResult,并将所有已注册的TestListener传递给mTestResult(关于TestListener如何被使用我们下面再聊),然后又是三个私有方法:

private void setContextIfAndroidTestCase(Test test, Context context, Context testContext) {if (AndroidTestCase.class.isAssignableFrom(test.getClass())) {((AndroidTestCase) test).setContext(context);((AndroidTestCase) test).setTestContext(testContext);}
}

setContextIfAndroidTestCase方法用于对每一个可以转化为AndroidTestCase的Case设定context

private void setInstrumentationIfInstrumentationTestCase(Test test, Instrumentation instrumentation) {if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {((InstrumentationTestCase) test).injectInstrumentation(instrumentation);}
}

setInstrumentationIfInstrumentationTestCase方法用于对每一个可以转化为InstrumentationTestCase的Case注入instrumentation

private void setPerformanceWriterIfPerformanceCollectorTestCase(Test test, PerformanceResultsWriter writer) {if (PerformanceCollectorTestCase.class.isAssignableFrom(test.getClass())) {((PerformanceCollectorTestCase) test).setPerformanceResultsWriter(writer);}
}

setPerformanceWriterIfPerformanceCollectorTestCase方法用于对每一个可以转化为PerformanceCollectorTestCase的Case注入PerformanceResultsWriter

最后执行testCase的run方法,执行测试并将结果存储在mTestResult中返回,查看TestCase源码找到run方法,发现是实际调用的传入的TestResult的run方法:

public void run(TestResult result) {result.run(this);
}

转回头看看我们传入的TestResult:

protected TestResult createTestResult() {if (mSkipExecution) {return new NoExecTestResult();}return new TestResult();
}

即如果设定了忽略异常会使用NoExecTestResult,否则使用TestResult,我们分别看一下这两个类

TestResult类

执行测试会调用TestResult的run方法,我们先来看下这个方法:

protected void run(final TestCase test) {startTest(test);Protectable p= new Protectable() {public void protect() throws Throwable {test.runBare();}};runProtected(test, p);endTest(test);
}

分别调用了startTest,runProtected,endTest完成预处理,执行和收尾工作,依次看下做了什么

startTest方法

public void startTest(Test test) {final int count= test.countTestCases();synchronized(this) {fRunTests+= count;}for (TestListener each : cloneListeners())each.startTest(test);
}

可以看到实现中将测试类中的测试用例数,并针对每一个已注册的TestListener执行startTest方法

runProtected方法

public void runProtected(final Test test, Protectable p) {try {p.protect();} catch (AssertionFailedError e) {addFailure(test, e);}catch (ThreadDeath e) { // don't catch ThreadDeath by accidentthrow e;}catch (Throwable e) {addError(test, e);}
}

这里通过runProtected方法执行test.runBare(),并处理异常分发给addFailure和addError方法,最后看test.runBare()的执行,先来看下这两个异常处理做了什么

addFailure方法
public synchronized void addFailure(Test test, AssertionFailedError t) {fFailures.add(new TestFailure(test, t));for (TestListener each : cloneListeners())each.addFailure(test, t);
}

实际上同startTest,针对每一个已注册的TestListener执行addFailure方法

addError方法
public synchronized void addError(Test test, Throwable t) {fErrors.add(new TestFailure(test, t));for (TestListener each : cloneListeners())each.addError(test, t);
}
test.runBare()

实际上这是JUINT的TestCase的实现了,理论上和Android无关了,我们也可以看看:

public void runBare() throws Throwable {Throwable exception= null;setUp();try {runTest();} catch (Throwable running) {exception= running;}finally {try {tearDown();} catch (Throwable tearingDown) {if (exception == null) exception= tearingDown;}}if (exception != null) throw exception;
}

会分别调用setUp,runTest,tearDown方法,是不是很熟悉啊,那runTest做了什么呢,只是通过反射运行testMethod啦:

protected void runTest() throws Throwable {assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null);Method runMethod= null;try {// use getMethod to get all public inherited// methods. getDeclaredMethods returns all// methods of this class but excludes the// inherited ones.runMethod= getClass().getMethod(fName, (Class[])null);} catch (NoSuchMethodException e) {fail("Method \""+fName+"\" not found");}if (!Modifier.isPublic(runMethod.getModifiers())) {fail("Method \""+fName+"\" should be public");}try {runMethod.invoke(this);}catch (InvocationTargetException e) {e.fillInStackTrace();throw e.getTargetException();}catch (IllegalAccessException e) {e.fillInStackTrace();throw e;}
}

endTest方法

实际也上同startTest,针对每一个已注册的TestListener执行endTest方法

public void endTest(Test test) {for (TestListener each : cloneListeners())each.endTest(test);
}

NoExecTestResult类

NoExecTestResult继承自TestResult类,区别是重写了run方法,并不会执行Test,仅仅检查startTest和endTest能否正常通过

protected void run(final TestCase test) {startTest(test);endTest(test);
}

Instrumentation框架分析及其使用相关推荐

  1. Flutter框架分析(五)-- 动画

    Flutter框架分析分析系列文章: <Flutter框架分析(一)-- 总览和Window> <Flutter框架分析(二)-- 初始化> <Flutter框架分析(三 ...

  2. hdfs文档存储服务器,一文读懂HDFS分布式存储框架分析

    一文读懂HDFS分布式存储框架分析 HDFS是一套基于区块链技术的个人的数据存储系统,利用无处不在的私人PC存储空间及便捷的网络为个人提供数据加密存储服务,将闲置的存储空间利用起来,服务于正处于爆发期 ...

  3. 需求评审五个维度框架分析及其带来的启示-3-典型需求评审

    典型情境是指软件开发的常见情境,本文选择如下来进行分析: 1. 传统瀑布模型开发下的需求评审 2. 使用IEEE Std. 1028的需求评审 3. 敏捷开发下的需求评审 传统瀑布模型下的需求评审 对 ...

  4. java连接linux服务器执行shell命令(框架分析+推荐)

    java连接linux服务器执行shell命令(框架分析+推荐) 一.分类+连接方式 程序打成jar包,在本地服务器上执行shell命令.这种使用MyRuntimeUtil工具类 java程序远程li ...

  5. Linux USB驱动框架分析 【转】

    转自:http://blog.chinaunix.net/uid-11848011-id-96188.html 初次接触与OS相关的设备驱动编写,感觉还挺有意思的,为了不至于忘掉看过的东西,笔记跟总结 ...

  6. linux音频框架分析,Alsa音频子系统Codec---al5623.c内核代码框架分析

    驱动代码位于: sound/soc/codec/alc5623.c 随便找个Linux内核都会有. 1.首先进行i2c总线驱动加载在: static int __init alc5623_modini ...

  7. Janus流媒体服务器框架分析

    Janus流媒体服务器框架分析 目录 webrtc多方通信架构 Janus流媒体服务器 1. webrtc多方通信架构 1. Mesh 方案 Mesh方案即多个终端之间两两进行连接,形成一个网状结构. ...

  8. FFmpeg过滤器框架分析

    FFmpeg过滤器框架分析 目录 主要结构体和API介绍 AVFilterGraph-对filters系统的整体管理 AVFilter-定义filter本身的能⼒ AVFilterContext-fi ...

  9. Prototype 框架分析(一)

    Prototype 框架分析(一) Class 关于javascript的面向对象设计可以参看MSDN上的文章<JavaScript使用面向对象的技术创建高级 Web 应用程序>,这样理解 ...

最新文章

  1. webGIS(离线版)研究路线归总
  2. matlab 写excel 慢_吐槽一下MATLAB的workspace
  3. linux futex 进程同步,Linux的新式线程同步原语——Futex
  4. java中抽象类,abstract关键字
  5. San 3.7.4 发布,百度开源的 MVVM 组件框架
  6. 手游接入Facebook的那些坑
  7. Python中MySQL查询结果返回类型
  8. 6 款好用到爆的 JSON 处理工具,极大提高效率!
  9. 安卓pdf阅读器_2020年双十一有哪些电纸书、电子书阅读器值得买?Kindle、掌阅、文石、科大讯飞哪个好?...
  10. 昂达平板装linux系统下载,昂达平板用U盘启动方式安装Ubuntu Uudgie 16.10 Linux操作系统...
  11. Comparing JSON and XML? Stop it!
  12. android深入浅出binder机制,Android Binder机制,跨进程机制深入理解精华总结
  13. A4988与42步进电机
  14. 电脑网易云音乐,网易云音乐的话题区到底有多魔性?
  15. 数独算法c语言,数独求解算法C.
  16. DPP Substrate 1中英文说明书
  17. 广州优漫动游公司:人物海报怎样设计才不显得低端?
  18. 软件无线电,虚拟无线电
  19. Free software是什么?
  20. android 彩信处理

热门文章

  1. 复数四则运算(Web)
  2. 媲美阿里P7的前端技术架构图,你要不要试一试?
  3. 代码实现raft共识算法,并进行Demo展示
  4. 初识CCS,创建工程、选择芯片、简单调试及烧录
  5. 西门子Smart200 PLC恒压供水程序
  6. Redis中事务用法详解
  7. iOS系统中判断设备类型
  8. Linux系统中设备主要分为哪几类?
  9. php的mvc三层架构,MVC三层架构
  10. 从六一宝宝节“共情消费”看苏宁易购“专注好服务”理念