本文主要描述了怎么样提高一个客户端开发排查和定位的效率,并且动手写了一个小工具的实践和思考,以及团队中其他合作者能够提高了定位问题效率,验证功能是否准确的效率。
作者/马杰 中国大学 MOOC 团队
编辑/刘振宇
一、前言
中国大学 MOOC 是由网易与高教社携手推出的在线教育平台,承接教育部国家精品开放课程任务,向大众提供中国知名高校的 MOOC 课程。目前,无论是课程数量、质量还是社会影响力,中国大学 MOOC 都已成为全球领先的中文慕课平台。
在日常的 Android 开发中,我们经常会遇到以下的一些问题:测试、运营、产品同学跑过来说这个页面出了问题,赶紧看下。这时候客户端开发同学就需要赶紧定位到具体的某个页面。
据观察,大部分的情况下对于一个突发页面的问题定位,或者业务方想让开发者确认这个页面的业务逻辑的时候,客户端开发者,往往需要花费比较长的时间去给业务方答复。如果近期业务可能还能记得,但是客户端的页面比较多,想要快速定位到具体业务页面,那么就需要花更多的时间去找相关的页面。
所以本文的想法是怎么快速找到对应的页面,帮助开发快速的进入业务代码,快速的回复业务方提出的问题。
二、方案实施
在探讨方案的时候,我们需要对比目前有哪些方案,对比之后再选择一种更加有效的方法。
2.1 解决问题的常用方式
在 Android 开发中解决上述提供的问题,常用的有以下 3 种方式:
- 打开 Android studio,靠着源码记忆,文案记忆去搜索;
- 使用 adb 命令来过滤当前的 activity;
// windows
adb shell dumpsys window windows | Select-String -Pattern 'mCurrentFocus|mFocusedApp|mLastOpeningApp|mObscuringWindow'
// Mac
adb shell dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp|mLastOpeningApp'
- 全局搜索关键文案。
上面 3 种方式是能够解决问题,但从时间效率上分析,可以估算一下每一种方式大概需要多长时间:
第一种,按照个人经验,熟悉项目代码的同学最快也要几十秒左右,慢的话 10 几分钟;
第二种,使用 adb 能够几秒就定位到页面,但是需要记住命令,或者提前设置命令快捷方式;
第三种,如果有很多相同文案,需要多搜索几遍,时间也可能是10几秒到1分钟不等。
所有上面几种方式得出的时间效率就是几秒到几分钟不等,而且基本都是需要代码或者 adb 的开发工具,依赖于开发环境。
既然需要花的时间也不少,那么是不是应该做一个工具来提升更快的定位速度,提升定位效率呢?
2.2 更加高效的方法
其实思路很简单,就是写一个开发的SDK,用来实时关注当前的页面信息。这个页面信息主要包含如下的信息:
- 当前 Activity 是哪个?
- 当前的 Fragment 是哪个?
- 当前页面的参数传递,如:intent 中的各种参数是什么?
效果图如下:
从上面信息就能够很快的定位到当前的页面;当一个页面的深度到非常深的时候,这样的小工具就特别好用;最快速度只要几秒就能快速定位到页面,效率提升快几十倍不止,而且能够当着测试和产品的面,能够把当前关键的参数给他看,如:xxxId、埋点信息等。
2.3 主要实现原理
上面的小工具,主要的工作是获得当前的Activity。获得当前Activity的方式主要有以下几种方式:
- 通过 RunningTaskInfo的 topActivity,该方法在后续的一些版本已经被禁用
- 手写代码管理Activity,这个方法比较粗暴,维护比较麻烦;
- 通过反射 ActivityThread获得 currentActivityThread 从 mActivities 中查询获得;
- 使用AccessibilityService 这个辅助功能,这个方法获得的信息比较少;
- 通过 ActivityLifecycleCallback 监听来获得。
经过对比,选择使用AccessibilityService和ActivityLifecycleCallback 这2种方式去尝试。以下就简单的说下这2种方法的实现,并进行二者之间的对比以及最后做出的选择。
2.3.1 ActivityLifecycleCallback 方式
private static Activity topActivity;
@Override
public void onActivityResumed(Activity activity) {
topActivity = activity;
}
是不是很简单?为了避免内存泄漏,可以在 onDestroy 的时候把 topActivity 设置成 null。这种方式简单快速,不需要申请权限。
2.3.2 AccessibilityService 方式
- 继承 AccessibilityService
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.d(TAG, "onAccessibilityEvent");
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
if (event.getPackageName() != null && event.getClassName() != null) {
ComponentName componentName = new ComponentName(
event.getPackageName().toString(),
event.getClassName().toString()
);
ActivityInfo activityInfo = tryGetActivity(componentName);
boolean isActivity = activityInfo != null;
if (isActivity) {
Log.d(TAG, "CurentActivity " + componentName.flattenToShortString());
Log.d(TAG, "CurentActivity " + event.getPackageName().toString());
}
}
}
}
private ActivityInfo tryGetActivity(ComponentName componentName) {
try {
return getPackageManager().getActivityInfo(componentName, 0);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
- manifest 配置
<service
android:name=".WindowChangeDetectingService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibilityservice"/>
</service>
- 需要 res/xm/accessibilityservice.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<!-- These options MUST be specified here in order for the events to be received on first
start in Android 4.1.1 -->
<accessibility-service
xmlns:tools="http://schemas.android.com/tools"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_service_description"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="UnusedAttribute"/>
2.3.3 2种方式的对比
从实现角度对比,使用 ActivityLifecycleCallback 比 AccessibilityService 更加简单。但是 AccessibilityService 有个优势就是可以不用集成到自己的 app 里面,可以独立运行,可以查看所有的当前页面是属于哪个 Activity,可以跨进程使用。使用 ActivityLifecycleCallback 必须要集成到自己的 app 中。
在实践过程中,其实我们不只是想获得当前的 Activity ,我们还想知道当前的 Activity 中有哪些当前的 fragment, 当前的 Activity 从上一个 Activity 中获得了哪些参数,当前的fragment 中有哪些参数等细节信息,那么只能集成到 app 中去的时候才会比较容易获得。所以最后选择了使用 ActivityLifecycleCallback 的方式。
2.3.4 页面更详细的信息
一般页面上的信息开发,简单一点的就是一个 Activity 然后简单布局;复杂一点的基本都是 Activity + (ViewPager)Adatper + fragment, 有时候 fragment 里面还会有 ViewPager 装载着 fragment, 对于不熟悉代码的人找对应的业务逻辑页面和代码,还是需要花费不少时间的。所以页面信息 fragment 也很重要。
topActivity.getIntent().getExtras();// 获得 activity 的页面参数
topActivity.getSupportFragmentManager().getFragments(); // 获得 activity 一级中的 fragments
fragment.getChildFragmentManager().getFragments();// 对应 fragment 中的 fragments
fragment.getArguments() // 获得 fragment的页面参数
三、 提高效率举例
关于页面信息采集,在这里列举了几个使用场景,来证明效率得到了提高:
- 对于客户端开发者,能够快速的定位到当前出错的页面,特别是刚来的开发,或者不熟悉这块业务的,或者业务页面深度比较深的时候;
- 页面核心参数的确认。比如详情页面需要一些 id,这些详细参数就不需要客户端同事打断点的方式去获取,运营和测试自己可以去查看;
- 在精品课和云课堂集成的时候,能够让测试同学快速的区分哪个是精品课里面的页面,哪个是云课堂里面的页面,这样方便测试知道当前页面是属于哪个业务端的;(这个场景是网易内部融合项目)
- 页面全链路参数传递验证场景。比如:首页点击需要传递转化率的参数一直传递到下单页面,平时都是开发自己验证,有这个工具后,产品也能在提测后,从测试包上自己查看验证。
四、 页面信息其他想法
关于页面信息场景增强,以下几种页面信息方式,认为可以进行扩充的:
- 可以获得 RecyclerView 中的 adapter;(有很多布局逻辑,放到了 adapter 里面的 ViewHolder)
- webview 当前信息的监控;(前端同事调试)
- 网络看板的监控;(当前页面的网络请求信息)
- 不连接电脑 Logcat 日志查看看板;(不用连接电脑,获得 adb 信息)
- 参考线,界面元素位置,对应元素的颜色。(UI 走查的验证)
以上的几点,主要是按照自身 app 的情况去判断是否需要实现,判断哪些实现性价比比较高。目前已经实现的,基本都是个人认为性价比是比较高的东西。
五、 总结
当我们遇见一个问题的时候,首先思考这个问题是不是自己比较难受的点,然后观察其他人是否有类似的情况。这个问题常用的解决方案什么?有没有工具方法去替代?如果没有,可不可以用比较低的成本去制造一个工具?最后来提升自己的效率,这个工具如果能够帮助其他人,那么能效就更加好了。
-END-
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。