四种方法让你尽情观测Activity活动

1,396 阅读5分钟

查看github上对应的demo

一、背景

犹记得当初刚接触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定时扫描观测活动

在项目中灵活运用这些工具方法,能够为开发提供方便,提高排查问题的效率。