一、背景
犹记得当初刚接触android的时候,遇到一个不太会UI效果的时,都会先在项目中寻找类似的代码实现,然后照葫芦画瓢的做需求。那时,一件比较苦恼的事情是,虽然找到了可以参考的页面,却不知道这个页面叫什么名字,往往得花费一点时间才能找到对应的Activity代码。
起初,我是在基类Activity中加入日志,将activity名称打印出来,但遇到没有继承基类的Activity就尴尬了。
后来的某一天,同事丢给我一个叫Activity.apk的安装包,安装完打开,竟然直接粗暴地将Activity名字和App包名显示在了当前页面上。
时至今天,难得有空,有必要捋一捋观测Activity名称的实现方案。
二、使用adb命令查看
Android Device bridge (adb) 命令可以查看activity栈,执行指令
adb shell dumpsys activity,将得到一串非常长也是非常全的一串讯息。讯息是分段展示的,为了方便查找信息,可以全局搜索"dumpsys activity",然后分段查找。其中"dumpsys activity recents"这段,打印出了当前手机后台中运行的app任务栈。
而
dumpsys activity activities这段,会一一罗列各个app任务的activity栈,比如下面对于美团app中的解读就可以看出,包名com.sankuai.meituan,LuancherActivity是com.meituan.android.pt.homepage.activity.MainActivity,当前栈顶activity是com.meituan.android.pt.bike.app.ui.MobikeMainActivity(美团单车),后台activity还有首页com.meituan.android.pt.homepage.activity.MainActivity。
在执行dumpsys命令时,也可以添加过滤参数,比如执行
adb shell dumpsys activity activities,那么dump结果中就只会打印中“dumpsys activity activities”这段。
三、使用LifecycleCallbacks生命周期组件
如果你只是想观测自己App的Activity,那可以借助LifecycleCallback组件观测活动。Application提供的registerActivityLifecycleCallbacks()方法,允许app中所有的activity的生命周期发生变化时,给予回调。
class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}
@Override
public void onActivityStarted(@NonNull Activity activity) { }
@Override
public void onActivityResumed(@NonNull Activity activity) { }
@Override
public void onActivityPaused(@NonNull Activity activity) { }
@Override
public void onActivityStopped(@NonNull Activity activity) { }
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { }
@Override
public void onActivityDestroyed(@NonNull Activity activity) { }
});
}
简单分析下LifecycleCallbacks源码,在Application中维护了一个callbacks数组,注册时,就是把ActivityLifecycleCallbacks添加进数组中,
public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
synchronized (mActivityLifecycleCallbacks) {
mActivityLifecycleCallbacks.add(callback);
}
}
然后,当activity的生命周期方法执行时,以Activity.onCreate()方法为例,会调用dispatchActivityCreated(),
protected void onCreate(@android.annotation.Nullable Bundle savedInstanceState) {
...
dispatchActivityCreated(savedInstanceState);
...
}
activity.dispatchActivityCreated()方法中,会执行Application.dispatchActivityCreated方法,
private void dispatchActivityCreated(@Nullable Bundle savedInstanceState) {
getApplication().dispatchActivityCreated(this, savedInstanceState);
...
}
Application.dispatchActivityCreated()方法主要就是取出callbacks数组,并遍历取出ActivityLifecycleCallbacks对象,然后一一回调ActivityLifecycleCallbacks.onActivityCreated()方法。
void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) {
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i=0; i<callbacks.length; i++) {
((ActivityLifecycleCallbacks)callbacks[i]).onActivityCreated(activity,
savedInstanceState);
}
}
}
private Object[] collectActivityLifecycleCallbacks() {
Object[] callbacks = null;
synchronized (mActivityLifecycleCallbacks) {
if (mActivityLifecycleCallbacks.size() > 0) {
callbacks = mActivityLifecycleCallbacks.toArray();
}
}
return callbacks;
}
四、使用辅助功能服务AccessibilityService
AccessibilityService根据android官方介绍,设计初衷是为了帮助残障人士更好的使用app而设置的辅助功能,但是由于它过于“强大”,能够接收到前台app的各种event事件,常常被开发者用在一些特殊功能上,比如著名的微信抢红包助手,就是在accessibilityService中监听到微信红包的事件后,进行了拆红包的响应处理。
我们可以使用AccessibilityService来观测Activity,首先得自定义一个继承AcessibilityService的实现类,并onAcessibilityEvents()和onInterrupt()方法,其中,前台app的事件将通过onAcessibilityEvents()方法进行通知。
class WatchingAccessibilityService : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent) {
}
override fun onInterrupt() {
}
}
对Service的注册必须可不少,其中resource中指定配置文件名。
<service
android:name=".WatchingAccessibilityService"
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/accessibility" />
</service>
配置文件,标签为,用于设置一些指定的属性和限制监听的events范围,配置好后,AccessibilityService就可以使用了。
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds"
android:canRetrieveWindowContent="true"
android:description="@string/app_slogan"
android:notificationTimeout="100" />
开启辅助功能,AccessibilityService具有很高的系统权限,配置好的service不会自动开启,还必须让用户在系统设置的辅助服务中开启权限才可以。这时,需要检查权限是否开启,可用下面的方法完成检查,
private fun isAccessibilitySettingsOn(): Boolean {
var accessibilityEnabled = 0
val service: String =
this.packageName + "/" + WatchingAccessibilityService::class.java.canonicalName
try {
accessibilityEnabled = Settings.Secure.getInt(
applicationContext.contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED
)
Log.v(logTag, "accessibilityEnabled = $accessibilityEnabled")
} catch (e: Settings.SettingNotFoundException) {
Log.e(
logTag, "Error finding setting, default accessibility to not found: "
+ e.message
)
}
val mStringColonSplitter = TextUtils.SimpleStringSplitter(':')
if (accessibilityEnabled == 1) {
val settingValue = Settings.Secure.getString(
applicationContext.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
if (settingValue != null) {
mStringColonSplitter.setString(settingValue)
while (mStringColonSplitter.hasNext()) {
val accessibilityService = mStringColonSplitter.next()
Log.v(
logTag,
"-------------- > accessibilityService :: $accessibilityService $service"
)
if (accessibilityService.equals(service, ignoreCase = true)) {
Log.v(
logTag,
"We've found the correct setting - accessibility is switched on!"
)
return true
}
}
}
} else {
Log.v(logTag, "***ACCESSIBILITY IS DISABLED***")
}
return false
}
如果没开启,就引导用户去设置页中开启,
val intent = Intent()
intent.action = "android.settings.ACCESSIBILITY_SETTINGS"
startActivity(intent)
dialog.dismiss()
权限开启后,AccessibilityService就正常开始工作了,所有events都会通过onAccessibilityEvent()回调,从events中筛选中前台Acttivity,解析出activity名称。注意,app每次开启的时候都需要重新去开启权限。
internal class WatchingAccessibilityService : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
Log.i("Watching event", "$event\n==================================================\n")
ShowTopActivityWindowManager.window?.show(getInfo(event))
}
}
private var currentActivityName: String? = null
private var currentViewName: String? = null
private fun getInfo(event: AccessibilityEvent): String {
if (event.className.isNullOrEmpty()) {
return ""
}
if (isActivity(event)) {
currentActivityName = event.className.toString()
currentViewName = null
} else {
currentViewName = event.className.toString()
}
return if (currentViewName.isNullOrEmpty()) {
"${event.packageName}\n$currentActivityName"
} else {
"${event.packageName}\n$currentActivityName\n$currentViewName"
}
}
private fun isActivity(event: AccessibilityEvent): Boolean {
val component = ComponentName(event.packageName.toString(), event.className.toString())
return try {
packageManager.getActivityInfo(component, 0)
true
} catch (e: Exception) {
false
}
}
override fun onInterrupt() {
}
}
五、使用AppUsager应用数据
Google从 API 21 新增了接口 android.app.usage , 通过这个api让我们获取到手机中各个app的使用情况统计数据,包括进程信息、启动次数,启动时间等。 首先得在Manifest中声明权限,
<!-- 访问应用使用情况 -->
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
使用UsagerStatsManager查询应用使用数据,查询时需指定时间范围。开启一个定时Service,每间隔一段时间,就去查询最近1秒钟内的ACTIVITY_RESUMED事件,从event中取出app的包名+activity名,从而能够实时观测Activity。
class WatchingService : Service() {
private val mHandler = Handler()
private var mActivityManager: ActivityManager? = null
private var timer: Timer? = null
private val logTag = WatchingService::class.java.canonicalName
override fun onCreate() {
super.onCreate()
mActivityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(logTag, "Watching Service start")
if (timer == null) {
timer = Timer()
timer!!.scheduleAtFixedRate(RefreshTask(), 0, 500)
}
return super.onStartCommand(intent, flags, startId)
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
override fun onTaskRemoved(rootIntent: Intent) {
Log.d(logTag, ServiceInfo.FLAG_STOP_WITH_TASK.toString() + "")
val restartServiceIntent = Intent(applicationContext, this.javaClass)
restartServiceIntent.setPackage(packageName)
val restartServicePendingIntent = PendingIntent.getService(
applicationContext, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT
)
val alarmService =
applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService[AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 500] =
restartServicePendingIntent
super.onTaskRemoved(rootIntent)
}
internal inner class RefreshTask : TimerTask() {
override fun run() {
val name = getCurrentActivityName()
if (name.isNullOrEmpty()) {
return
}
Log.i(logTag, "top running app is : $name")
mHandler.post {
MainActivity.topActivityWindow?.show(name)
}
}
}
private fun getCurrentActivityName(): String? {
var topActivity = ""
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
val mUsageStatsManager =
getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val now = System.currentTimeMillis()
val events = mUsageStatsManager.queryEvents(now - 1000, now)
while (events.hasNextEvent()) {
val event = UsageEvents.Event()
events.getNextEvent(event)
when (event.eventType) {
UsageEvents.Event.ACTIVITY_RESUMED -> {
topActivity = "${event.packageName}\n${event.className}"
}
}
}
} else {
val activityManager =
applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val forGroundActivity = activityManager.getRunningTasks(1)
topActivity =
forGroundActivity[0].topActivity!!.packageName + "\n" + forGroundActivity[0].topActivity!!.className
}
return topActivity
}
}
开启权限后才可以使用,检查是否具有权限,
/**
* 检查访问应用数据权限
*/
fun checkUsageStatsPermissionInner(context: Context): Boolean {
context.getSystemService(Context.APP_OPS_SERVICE)?.let {
val appOps = it as AppOpsManager
val mode = appOps.checkOpNoThrow("android:get_usage_stats",Process.myUid(), context.packageName)
return mode == AppOpsManager.MODE_ALLOWED
}
return false
}
如果发现没有权限,可以引导用户去系统设置页开启权限,
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
startActivityForResult(intent, requestCode)
那5.0以前都是怎么拿统计数据呢,事实上5.0以前更简单,不需要这么多权限,直接同AMS.getRunningTask()就可以拿到后台运行的进程,比如getRunningTask(1)就是获取前台进程。
val activityManager = applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val forGroundActivity = activityManager.getRunningTasks(1)
topActivity = forGroundActivity[0].topActivity!!.packageName + "\n" + forGroundActivity[0].topActivity!!.className
六、设计显示Activity名字的悬浮窗
用一个Window来显示观测到Activity名称,对页面本身不产生任何影响。在manifest中声明悬浮窗权限。
<!-- 悬浮窗 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
6.0以下声明后就有权限了,6.0以后需要用户授权。与普通权限不同,不能使用requestPermissions()动态申请悬浮窗权限,只能让用户自己去应用设置中开启权限。需要时,可以引导用户去设置页,
/**
* 申请悬浮窗
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private fun requestAlertWindow(requestCode: Int) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + context!!.packageName)
)
startActivityForResult(intent, requestCode)
}
用于显示观测到的前台活动名称的Window完整类。
/**
* desc: 显示栈顶activity名称的window
*/
class TopActivityWindow(var mContext: Context) {
private var sWindowParams: WindowManager.LayoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_TOAST else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
0x18,
PixelFormat.TRANSLUCENT
)
private var sWindowManager: WindowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private var sView: View
private var textView: TextView
private var isShowing = false
init {
sWindowParams.gravity = Gravity.START + Gravity.TOP
sView = LayoutInflater.from(mContext).inflate(R.layout.debug_top_activity_window, null)
textView = sView.findViewById(R.id.text)
}
fun show(text: String?) {
if (text.isNullOrEmpty()) {
return
}
textView.text = text
if (isShowing) {
return
}
if (PermissionUtil.checkFloatPermission(mContext)) {
isShowing = true
sWindowManager.addView(sView, sWindowParams)
} else {
dismiss()
}
}
fun dismiss() {
if (!isShowing) {
return
}
sWindowManager.removeView(sView)
isShowing = false
}
七、总结
本文总结了几种观测Android活动的方法,包括:
- 使用adb工具抓活动日志
- 使用LifecycleCallback组件观测活动生命周期
- 使用辅助功能AccessibilityService观测活动Events
- 使用应用数据AppUsager定时扫描观测活动
在项目中灵活运用这些工具方法,能够为开发提供方便,提高排查问题的效率。