终于勉强有一个调试环境了。因为完整的 android 源码下载下来太慢了,所以我采用远程调试 uiautomator 脚本的方法进行调试,缺少的 IAccessibilityServiceConnection 文件也在GrepCode找到了(是编译后的.java 文件,对于这种调试基本足够了)。
先附上 uiautomator 脚本的关键代码:
...
UiDevice device = getUiDevice();
device.waitForIdle();
//set compress, the same as what --compress did
device.setCompressedLayoutHeirarchy(true);
//dump Hierarchy file
device.dumpWindowHierarchy("dumpFromEclipse.xml");
...
开始调试过程。这里我主要把关键过程的调试信息附上:
device.setCompressedLayoutHeirarchy(true);
方法代码:
/**
* Enables or disables layout hierarchy compression.
*
* If compression is enabled, the layout hierarchy derived from the Acessibility
* framework will only contain nodes that are important for uiautomator
* testing. Any unnecessary surrounding layout nodes that make viewing
* and searching the hierarchy inefficient are removed.
*
* @param compressed true to enable compression; else, false to disable
* @since API Level 18
*/
public void setCompressedLayoutHeirarchy(boolean compressed) {
getAutomatorBridge().setCompressedLayoutHierarchy(compressed);
}
这里getAutomatorBridge()
获取了对QueryController
和InteractionController
的访问连接,然后调用了它的setCompressedLayoutHierarchy(compressed)
方法。
public void setCompressedLayoutHierarchy(boolean compressed) {
AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
if (compressed)
info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
else
info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
mUiAutomation.setServiceInfo(info);
}
在调试时看到:
AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
标志位前,info.flags=18
,16
(这里使用的是位运算。关于位运算的资料可以查看位运算简介及实用技巧(一):基础篇)setServiceInfo
方法会把添加标志位后的info
发给AccessibilityInteractionClient
:
/**
* Sets the {@link AccessibilityServiceInfo} that describes how this
* UiAutomation will be handled by the platform accessibility layer.
*
* @param info The info.
*
* @see AccessibilityServiceInfo
*/
public final void setServiceInfo(AccessibilityServiceInfo info) {
final IAccessibilityServiceConnection connection;
synchronized (mLock) {
throwIfNotConnectedLocked();
AccessibilityInteractionClient.getInstance().clearCache();
connection = AccessibilityInteractionClient.getInstance()
.getConnection(mConnectionId);
}
// Calling out without a lock held.
if (connection != null) {
try {
connection.setServiceInfo(info);
} catch (RemoteException re) {
Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
}
}
}
getConnection()
的返回值类型为IAccessibilityServiceConnection
,通过 find in path,得知com.android.server.accessibility.AccessibilityManagerService
中的Service
类继承并实现了IAccessibilityServiceConnection.Stub
。其中setService
源码如下:
@Override
public void setServiceInfo(AccessibilityServiceInfo info) {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
// If the XML manifest had data to configure the service its info
// should be already set. In such a case update only the dynamically
// configurable properties.
AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo;
if (oldInfo != null) {
oldInfo.updateDynamicallyConfigurableProperties(info);
setDynamicallyConfigurableProperties(oldInfo);
} else {
setDynamicallyConfigurableProperties(info);
}
UserState userState = getUserStateLocked(mUserId);
onUserStateChangedLocked(userState);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
这里主要做的事情就是执行了setDynamicallyConfigurableProperties
方法来更新info
。setDynamicallyConfigurableProperties
方法源码如下:
public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) {
mEventTypes = info.eventTypes;
mFeedbackType = info.feedbackType;
String[] packageNames = info.packageNames;
if (packageNames != null) {
mPackageNames.addAll(Arrays.asList(packageNames));
}
mNotificationTimeout = info.notificationTimeout;
mIsDefault = (info.flags & DEFAULT) != 0;
if (mIsAutomation || info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion
>= Build.VERSION_CODES.JELLY_BEAN) {
if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) {
mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
} else {
mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
}
}
if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) {
mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
} else {
mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
}
mRequestTouchExplorationMode = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
mRequestEnhancedWebAccessibility = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY) != 0;
mRequestFilterKeyEvents = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0;
}
这里做的事情就是根据 info 更新局部变量,如mEventTyes
,mFetchFlags
。其中mFetchFlags
同样检查了AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
。当info.flag
中含有FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
时,执行mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
,把这个标志位添加到mFetchFlags
中。
至此,device.setCompressedLayoutHeirarchy(true);
完成任务了,AccessibilityInteractionClient
已经知道后面执行的操作都基于设定了AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
标志的前提下
首先进入dumpWindowHierarchy
:
/**
* Helper method used for debugging to dump the current window's layout hierarchy.
* The file root location is /data/local/tmp
*
* @param fileName
* @since API Level 16
*/
public void dumpWindowHierarchy(String fileName) {
Tracer.trace(fileName);
AccessibilityNodeInfo root =
getAutomatorBridge().getQueryController().getAccessibilityRootNode();
if(root != null) {
Display display = getAutomatorBridge().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
AccessibilityNodeInfoDumper.dumpWindowToFile(root,
new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName),
display.getRotation(), size.x, size.y);
}
}
Tracer.trace(fileName);
与找 node 的过程无关,略过,然后我们执行到AccessibilityNodeInfo root =getAutomatorBridge().getQueryController().getAccessibilityRootNode();
getAutomatorBridge()
和第一步一样,获取连接getQueryController()
获取QueryController
实例getAccessibilityRootNode()
这是重点。咱们进去看看:源码:
public AccessibilityNodeInfo getAccessibilityRootNode() {
return mUiAutomatorBridge.getRootInActiveWindow();
}
这里开始和我上面的getRootInActiveWindow
到connection.findAccessibilityNodeInfoByAccessibilityId
的过程是一致的。其中findAccessibilityNodeInfoByAccessibilityId
方法比较复杂,因此从这里开始详述:
/**
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
* to start from the root.
* @param bypassCache Whether to bypass the cache while looking for the node.
* @param prefetchFlags flags to guide prefetching.
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
int prefetchFlags) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
if (!bypassCache) {
AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
accessibilityNodeId);
if (cachedInfo != null) {
return cachedInfo;
}
}
final int interactionId = mInteractionIdCounter.getAndIncrement();
final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (success) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
if (infos != null && !infos.isEmpty()) {
return infos.get(0);
}
}
} else {
if (DEBUG) {
Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
}
}
} catch (RemoteException re) {
if (DEBUG) {
Log.w(LOG_TAG, "Error while calling remote"
+ " findAccessibilityNodeInfoByAccessibilityId", re);
}
}
return null;
}
咱们一行一行来:
IAccessibilityServiceConnection connection = getConnection(connectionId);
获取连接。获取后 connection 不是 null,且 bypassCache 为 false,因此跑到这一行:
AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
accessibilityNodeId);
调试过程中发现缓存中没有对应信息,cachedInfo 为 null,因此进入这一行:
final int interactionId = mInteractionIdCounter.getAndIncrement();
获取了 interactionId,值为 0。进入下一行
final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId());
这里做的事情比较多。为方便后面说明,先把目前的各变量及对应值贴一下:
然后咱们进入之前卡住了的connection.findAccessibilityNodeInfoByAccessibilityId
:
@Override
public boolean findAccessibilityNodeInfoByAccessibilityId(
int accessibilityWindowId, long accessibilityNodeId,
int interactionId,
android.view.accessibility.IAccessibilityInteractionConnectionCallback callback,
int flags, long threadId) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(accessibilityWindowId);
_data.writeLong(accessibilityNodeId);
_data.writeInt(interactionId);
_data.writeStrongBinder((((callback != null))? (callback.asBinder()) : (null)));
_data.writeInt(flags);
_data.writeLong(threadId);
mRemote.transact(Stub.TRANSACTION_findAccessibilityNodeInfoByAccessibilityId,
_data, _reply, 0);
_reply.readException();
_result = (0 != _reply.readInt());
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
开始三行获取了 2 个Parcel实例 (Parcel 是一个存储通过 IBinder 传输的消息的容器)_data
和_reply
,创建了一个 boolean 类型的变量_result
。
然后开始往_data
写入数据:
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(accessibilityWindowId);
_data.writeLong(accessibilityNodeId);
_data.writeInt(interactionId);
_data.writeStrongBinder((((callback != null))? (callback.asBinder()) : (null)));
_data.writeInt(flags);
_data.writeLong(threadId);
执行完后各数据的值:
其中DESCRIPTOR
的值如下:
private static final java.lang.String DESCRIPTOR = "android.accessibilityservice.IAccessibilityServiceClient";
然后执行mRemote.transact(Stub.TRANSACTION_findAccessibilityNodeInfoByAccessibilityId, _data, _reply, 0);
。
此时 step into 进入不了这个方法(我用的是 remote debug,这部分代码的内容已经不在 build path 里面了,所以跟不进去),直接去到下一个语句_reply.readException();
。此时 callback 的内部变量值有了变化,增加了两个变量:
可以看到mFindAccessibilityNodeInfosResult
里面已经含有各个 node 的相关信息了。而且这里的信息已经 compress 过了,所以过滤是在mRemote.transact
里面做的。
接下来我们看看这里实际做了什么了。
还是在IAccessibilityServiceConnection
里面:
@Override
public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags)
throws android.os.RemoteException {
switch (code) {
...
case TRANSACTION_findAccessibilityNodeInfoByAccessibilityId: {
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
long _arg1;
_arg1 = data.readLong();
int _arg2;
_arg2 = data.readInt();
android.view.accessibility.IAccessibilityInteractionConnectionCallback _arg3;
_arg3 = android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());
int _arg4;
_arg4 = data.readInt();
long _arg5;
_arg5 = data.readLong();
boolean _result = this.findAccessibilityNodeInfoByAccessibilityId(_arg0,
_arg1, _arg2, _arg3, _arg4, _arg5);
reply.writeNoException();
reply.writeInt(((_result) ? (1) : (0)));
return true;
}
这里data.enforceInterface(DESCRIPTOR);
进行了数据的解包,然后把各个数据分别放到_arg0
~_arg5
中,其中_arg3=android.view.accessibility.IAccessibilityInteractionConnectionCallback.Stub.asInterface(data.readStrongBinder());
把 callback(getRootInActiveWindow
的findAccessibilityNodeInfoByAccessibilityId
)转变成IAccessibilityInteractionConnectionCallback
的 instance(相当于findAccessibilityNodeInfoByAccessibilityId
是IAccessibilityInteractionConnectionCallback
的实现)。然后调用了findAccessibilityNodeInfoByAccessibilityId
(此处调用的应该是继承了IAccessibilityInteractionConnection.Stub
的实现类,通过查找源码发现在com.android.server.accessibility.AccessibilityManagerService
里面),并把结果写到 reply 中。
查看com.android.server.accessibility.AccessibilityManagerService
相关源码:
@Override
public boolean findAccessibilityNodeInfoByAccessibilityId(
int accessibilityWindowId, long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
long interrogatingTid) throws RemoteException {
final int resolvedWindowId;
IAccessibilityInteractionConnection connection = null;
synchronized (mLock) {
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(
UserHandle.getCallingUserId());
if (resolvedUserId != mCurrentUserId) {
return false;
}
mSecurityPolicy.enforceCanRetrieveWindowContent(this);
resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
return false;
} else {
connection = getConnectionLocked(resolvedWindowId);
if (connection == null) {
return false;
}
}
}
final int interrogatingPid = Binder.getCallingPid();
final long identityToken = Binder.clearCallingIdentity();
MagnificationSpec spec = getCompatibleMagnificationSpec(resolvedWindowId);
try {
connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId,
interactionId, callback, mFetchFlags | flags, interrogatingPid,
interrogatingTid, spec);
return true;
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
}
} finally {
Binder.restoreCallingIdentity(identityToken);
}
return false;
}
这里的connection
和之前的不一样,它是通过getConnectionLocked
获得的。查看getConnectionLocked
:
private IAccessibilityInteractionConnection getConnectionLocked(int windowId) {
if (DEBUG) {
Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
}
AccessibilityConnectionWrapper wrapper = mGlobalInteractionConnections.get(windowId);
if (wrapper == null) {
wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
}
if (wrapper != null && wrapper.mConnection != null) {
return wrapper.mConnection;
}
if (DEBUG) {
Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
}
return null;
}
这里的 connection 是IAccessibilityInteractionConnection
类型的,而前面的 connection 则是IAccessibilityServiceConnection
类型的。那么,这里的findAccessibilityNodeInfoByAccessibilityId
具体做了什么?留待搭建完完整的 android source code 调试环境后继续研究。
@doctorq 谢谢指导,之前还真没了解过 aidl。今晚研究一下。
看了一下源码,但还没搭建好调试环境,所以跟踪到某一步后就跟踪不下去了。在此仅分享一下找的过程:
先接着 doctorq 的思路,去找 android 源码中的getRootInActiveWindow
:
android.app.UiAutomation
/**
* Gets the root {@link AccessibilityNodeInfo} in the active window.
*
* @return The root info.
*/
public AccessibilityNodeInfo getRootInActiveWindow() {
final int connectionId;
synchronized (mLock) {
throwIfNotConnectedLocked();
connectionId = mConnectionId;
}
// Calling out without a lock held.
return AccessibilityInteractionClient.getInstance()
.getRootInActiveWindow(connectionId);
}
接着找 return 中的getRootInActiveWindow
:
android.view.acessibility.AccessibilityInteractionClient
/**
* Gets the root {@link AccessibilityNodeInfo} in the currently active window.
*
* @param connectionId The id of a connection for interacting with the system.
* @return The root {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) {
return findAccessibilityNodeInfoByAccessibilityId(connectionId,
AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID,
false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS);
}
继续找findAccessibilityNodeInfoByAccessibilityId
:
android.view.acessibility.AccessibilityInteractionClient
/**
* Finds an {@link AccessibilityNodeInfo} by accessibility id.
*
* @param connectionId The id of a connection for interacting with the system.
* @param accessibilityWindowId A unique window id. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ACTIVE_WINDOW_ID}
* to query the currently active window.
* @param accessibilityNodeId A unique view id or virtual descendant id from
* where to start the search. Use
* {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID}
* to start from the root.
* @param bypassCache Whether to bypass the cache while looking for the node.
* @param prefetchFlags flags to guide prefetching.
* @return An {@link AccessibilityNodeInfo} if found, null otherwise.
*/
public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId,
int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache,
int prefetchFlags) {
try {
IAccessibilityServiceConnection connection = getConnection(connectionId);
if (connection != null) {
if (!bypassCache) {
AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get(
accessibilityNodeId);
if (cachedInfo != null) {
return cachedInfo;
}
}
final int interactionId = mInteractionIdCounter.getAndIncrement();
final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId(
accessibilityWindowId, accessibilityNodeId, interactionId, this,
prefetchFlags, Thread.currentThread().getId());
// If the scale is zero the call has failed.
if (success) {
List<AccessibilityNodeInfo> infos = getFindAccessibilityNodeInfosResultAndClear(
interactionId);
finalizeAndCacheAccessibilityNodeInfos(infos, connectionId);
if (infos != null && !infos.isEmpty()) {
return infos.get(0);
}
}
} else {
if (DEBUG) {
Log.w(LOG_TAG, "No connection for connection id: " + connectionId);
}
}
} catch (RemoteException re) {
if (DEBUG) {
Log.w(LOG_TAG, "Error while calling remote"
+ " findAccessibilityNodeInfoByAccessibilityId", re);
}
}
return null;
}
注意中间的connection.findAccessibilityNodeInfoByAccessibilityId
,我用的 IDE 是 Android Studio,findAccessibilityNodeInfoByAccessibilityId
方法显示为红色,表示找不到它的声明位置。
然后我们看看 connection 的声明:
·IAccessibilityServiceConnection connection = getConnection(connectionId);·
它是通过getConnection(connectionId)
获得的实例,继续找:
public IAccessibilityServiceConnection getConnection(int connectionId) {
synchronized (sConnectionCache) {
return sConnectionCache.get(connectionId);
}
}
再找sConnectionCache
:
// The connection cache is shared between all interrogating threads.
private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
new SparseArray<IAccessibilityServiceConnection>();
继续IAccessibilityServiceConnection
,这时候只能找到一个相关的 import 语句了:
import android.accessibilityservice.IAccessibilityServiceConnection;
到这里为止,寻找方法声明位置的这种方法找不下去了,因为源码里找不到IAccessibilityServiceConnection
声明的位置。
大致猜测是本来每个 Node 其实都带有一定的信息 (具体有哪些信息可以在android.accessibilityservice.AccessibilityServiceInfo
找到) 表示它是否影响显示,--compress 插入的标志位会在存储 Node 时不存储这些不影响显示的 Node。具体是如何实现的等研究好如何搭建调试环境后继续研究。
看完以后,我觉得我压根就没学会设计模式。用过 singleton,但没试过探究到内存这么深入。赞一个
@ice87875494 这个库是用来给其他应用调用的。你应该下载下来后用其他 py 文件 import pylib 来使用。
举个例子:
folder
├─pylib(pylib源码)
│ test.py
此时 test.py 里面使用import pylib
就能 import。
在 module xxx 里面 import xxx 是会提示" No module named xxx"的,因为这个 module 不在查找范围内。具体 import 过程可以看看import system
12 年实习,一开始主要做政府部门的验收测试(公司拿到了对应的资质证书),2~3 天把项目的招标、需求、概设、详设等看完并根据需求写用例,然后又 2~3 天到客户现场把用例执行完(功能测试和压力测试,使用 LoadRunner 录脚本然后根据需求确认性能点是否达标),最后出报告。基本上 1 周 1 个项目的节奏。后面公司需要人手做公司的操作系统的兼容性测试,于是学习了 linux,开始用 shell 脚本来跑一部分测试(脚本都是别人写好的,当时对 linux 的东西还不是太了解),测试安装、驱动性能等。
13 年毕业,进入一个做海外外包的外企,进入了一个产品线包括嵌入式、web、android、ios 的项目。第一年先是做 ios 测试,然后 android,然后 web,都是纯手工测试。业余学了一些 ios 和 android 调试工具的使用,知道了 appium 的存在(当时 appium 刚出来,还是挺热门的),学会了用 python 写一些简单的监控脚本。正准备开始自己弄一些脚本来让手工测试不那么枯燥的时候,公司需要开发一个测试第三方配件的测试工具,刚好用的是 python,leader 和我也比较熟,然后很幸运地就加入了。现在基本完成了工具的开发,正在写对应这个工具用例作为演示。
过程中也有人建议我转做开发,因为我有一定的编程经验(以前在学校搞过 J2EE 和 web 的一些东西),但我还是更想做测试,一方面做开发做久了也会很枯燥(开发也有重复性的工作,也有不少 copy/paste),会让我失去对计算机的兴趣,另一方面如同《Google 软件测试之道》里面 Chrome 测试工程经理 Joel Hynoski 所说,“测试是开发过程里面工程师能涉及的最远的地方”。我喜欢对所有事情都一探究竟,直到我完全理解,测试正好可以做到。
后面还会继续专注自动化测试,当然作为基础的开发也会继续学习。
根据 log 的描述,无法创建新 session 的原因是旧 session 没有被关闭。
个人建议:
@lihuazhang github 账号:chenhengjie123
@doctorq 有兴趣,求加入,学习 appium 同时改进它。
希望留下,虽然主要都是潜水,但是视野比以前开阔了很多。
一般公司隔一段时间会组织一些活动的吧,多参加这些活动就有一定锻炼效果了。
最好还是有一项运动方面的爱好,坚持每周至少参加一次,那么身体相对就好一些。