本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

0x1、引言

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

Hi,我是杰哥,上节咱们学习了AccessibilityService无障碍的基础知识,并写了一个简略的微信主动登录的小案例。信任咱们都意犹未尽,所以本节组织一波实战 —— 微信僵尸老友检测

啥是 僵尸老友

在微信里,对方把你删去/拉黑了,并不会从你的老友列表消失,只需你给他/她发音讯,看到赤色感叹号才知道。而他/她假如把 加老友验证选项封闭,你发音讯不会有赤色感叹号,而对方却能看到你发的音讯:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

他/她顺手点这个把你加回来,你这边是不会有任何提示的,所以被删这件事你可能永远都不知道~

对于我这种有强迫症的人来说,已然对方删了我,那我也要删了他/她。直接粗暴地给每个老友 群发音讯看会不会呈现赤色感叹号 的计划显然不太行,浪费自己时刻不说,还打扰了别人,万一发给了一些不得不加,但平常无天可聊的人,就为难了。

顺手搜了一下,看到一个 拉群 的计划:拉群时被删的老友会提示不是老友联系

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

但每次只能检测50人 (如同是这个值),超过50需求对方同意才干够入群,就是会 收到约请通知。而少于50的话,只需不往群里发音讯,被拉的人是不知道群存在的。当然,要是被别人知道了的话,就是社死了~

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

又顺手搜了一下:主动整理微信僵尸老友

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

九块九解君愁?也有些免费帮你整理的公号 (哪有那么多天上掉馅饼的好事),看了下演示视频,需求扫码登录,猜想用的是PC端协议。登录后,他们能够用你的账号随意发音讯,这就存在 风险 了,万一他们:以患病、出车祸、急用钱等各种理由向你的朋友 群发欺诈信息,又或许 群发色情信息 给你造成不良影响,导致封号呢?

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

所以,但凡触及到 要你登录 的,都请不要尝试,以免造成不必要的丢失。那,有没有 免费安全又好用 的东西呢?还真有,网上许多文章都提到了它李跳跳的 实在老友,界面长这样:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

用法简略

翻开无障碍权限,点击开端检测,晾一边等它主动检测完,最终会输出正常/反常老友到列表。点击能够仿制微信号,翻开微信自行搜索,按需删去联系反常的老友即可。

笔者简略体验了一下,很赞,虽然没开源,但APP没恳求任何权限(不联网),所以你不需求担心隐私泄露啥的。假如懒得折腾,完全能够放心使用,当然,建议到官方公号「大小姐李跳跳」下载。毕竟破解APP后加点广告、引流信息等恶意内容很常见,比方我就见过越过广告的APP反而被加入了开屏广告,23333~

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

em… 如同扯得有点远了,本文的意图不是教会咱们使用这款软件,而是 借(chao)鉴(xi) 它, 使用上节所学的AccessibilityService基础,自己完成一个检测微信僵尸老友的东西!本节某些东西代码get√了,也能够为你开发其它无障碍服务脚本供给一些助力哦~ 话不多说,赶紧开端!!!


0x2、怎么判别被删去/拉黑?—— 假转账法

上面说了 群发音讯拉群 验证老友联系都不太靠谱,所以这儿选用实在老友用的—— 假转账法,无感,不打扰对方,也不会产生实在的转账行为。它的断定流程如下:

  • 进入老友转账页,呢称后边呈现实在姓名,阐明是 正常老友联系
  • 呢称后没有实在姓名,进行 “假转账” 进一步承认联系,可能会呈现四种状况:
  • ① 提示:你不是收款方老友,对方增加你为老友后才干建议转账 → 阐明被删去了;
  • ② 提示:请承认你和他(她)的老友联系是否正常 → 阐明被拉黑了;
  • ③ 提示:对方微信号已被限制登录,为保障你的资金安全,暂时无法完结交易 → 对方账号反常;
  • ④ 弹出:输入付出暗码 界面,阐明是正常老友联系

核心难点解决了,记者就是编写脚原本完成主动化了~


0x3、实战环节

① 界面设计

设置页 直接复用上节的熊猫头,增加一个 去整理的Button,点击跳转到 整理僵尸老友页,根本UI款式如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

一个从头检测的Button + 一个显现成果的RecyclerView,十分简练(lou)~


② 跳转微信

跳转外部APP的方法有两种:Intent指定启动APP包名和Activity名URL Scheme恳求,直接给出东西代码:

/**
 * 跳转其它APP
 * @param packageName 跳转APP包名
 * @param activityName 跳转APP的Activity名
 * @param errorTips 跳转页面不存在时的提示
 * */
fun Context.startApp(packageName: String, activityName: String, errorTips: String) {
    try {
        startActivity(Intent(Intent.ACTION_VIEW).apply {
            component = ComponentName(packageName, activityName)
            flags = Intent.FLAG_ACTIVITY_NEW_TASK
        })
    } catch (e: ActivityNotFoundException) {
        shortToast(errorTips)
    } catch (e: Exception) {
        e.message?.let { logD(it) }
    }
}
/**
 * 跳转其它APP
 * @param urlScheme URL Scheme恳求字符串
 * @param errorTips 跳转页面不存在时的提示
 * */
fun Context.startApp(urlScheme: String, errorTips: String) {
    try {
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(urlScheme)))
    } catch (e: ActivityNotFoundException) {
        shortToast(errorTips)
    } catch (e: Exception) {
        e.message?.let { logD(it) }
    }
}

读者可能对这儿的捕获 ActivityNotFoundException 感到古怪,为啥不经过 getPackageManager().getInstalledPackages(0) 读已装置应用列表,然后再遍历判别?

答:由于这样不只需求权限,还触及到了隐私,为了简化处理,直接捕获这个反常,然后给出未装置的提示。由于假如设备装置了,一般只需你不写错包名啥的,是不会触发这个反常的!调用示例如下:

startApp("com.tencent.mm", "com.tencent.mm.ui.LauncherUI", "未装置微信")
startApp("weixin://", "未装置微信")

另外,URL Scheme对于一些内嵌浏览器页面的APP跳转有奇效,比方之前某东双11活动页的跳转的scheme如下:

"openApp.jdMobile://virtual?params={"category":"jump","action":"to","des":"m","sourceValue":"JSHOP_SOURCE_VALUE","sourceType":"JSHOP_SOURCE_TYPE","url":"https://u.jd.com/kIrrQ3H","M_sourceFrom":"mxz","msf_type":"auto"}'})"

履行后会跳转到下述页面:(活动已过期,正常状况下你是进不了这个页面的~)

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测


③ 搞清Event的触发链条

按照上节所说,能够先把无障碍配置文件里的 android:accessibilityEventTypes 设置为 typeAllMask,监听一切类型的Event。在 onAccessibilityEvent() 里把日志打印出来,然后挑选自己重视的Event类型,最终再把 android:accessibilityFeedbackType 设置为这些类型。

点击从头检测,跳转微信,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.ui.LauncherUI
  • 阐明:进入微信主页

此刻点击底部的 通讯录,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

当:EventTypeTYPE_VIEW_CLICKEDText通讯录 时阐明点击了通讯录。接着随意 点击一个联系人,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.plugin.profile.ui.ContactInfoUI
  • 阐明:进入联系人信息页

接着 点击发音讯,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.ui.chatting.ChattingUI
  • 阐明:进入谈天页

接着 点击加号更多按钮,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

当:EventTypeTYPE_VIEW_CLICKEDText更多功用按钮,已折叠 阐明:点击了更多按钮,接着 点击转账按钮,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNameom.tencent.mm.plugin.remittance.ui.RemittanceUI
  • 阐明:进入转账页,输入0.01,接着 点击转账按钮,输出日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNamecom.tencent.mm.ui.widget.dialog.f
  • 阐明:呈现反常弹窗,老友联系不正常,比方这儿的Text就显现:你不是收款方老友,对方增加你为老友后才干建议转账, 我知道了

接着再试试正常转账,需求输入付出暗码的状况,输入日志如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

  • 当:EventTypeTYPE_WINDOW_STATE_CHANGEDClassNameandroid.widget.LinearLayout – 阐明:呈现输入付出暗码的页面

由于LinearLayout不是特别的Activity或许类,所以等下还得特别处理一下。当然,这只是大约的Event触发流程,实践开发过程可能呈现某些Event不触发的景象,见机行事咯。接着就是在恰当的Event,获取相应节点,履行对应的交互,如点击、滑动等。不过再次之前,还得先改动下咱们的 无障碍配置文件~。

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测


④ 修改无障碍配置文件

笔者忽然有点猎奇 实在老友 的配置,那就开扒,定位到它的配置文件:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

咦,跟我的配置不一样,没设置 android:packageNames,上节说过不设置这个属性的话,是监听一切App的,检测僵尸老友,不是只应该监听 com.tencent.mm 微信的吗?还有监听的事情类型只监听 typeWindowsChanged,关于这种类型,官方文档中这样介绍到:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

API 21新增,体系窗口事情改动会触发,难不成这中event类型更高效?写一个顶几个?所以我Copy了它的配置,并加上 android:packageNames=”com.tencent.mm”,运转后却发现没有日志输出。

接着把它删掉再试,此刻有日志信息输出:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

但packageName和className都是null,这样能区别哪个APP?哪个页面?真是老友是咋做的?

简略脱下壳导出dex,丢电脑里用jadx反编译成java,直接定位到它的无障碍服务类 → MyAccessibilityService,搜 setServiceInfo(),我感觉它是不是在代码里又进行了动态配置,成果没找着,接着搜 onAccessibilityEvent():

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

往线程池里丢了线程实例t,跟下t的代码完成:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

榜首段不难看出大约得逻辑:

getRootInActiveWindow() 取得节点树,然后判别packageName是否为com.tencent.mm,是履行微信相关校验逻辑

第二段略微难猜一点,应该是用来 判别用户是否退出微信,只在 onServiceConnected() 调用一次,断点了一下for循环:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

这儿拿到了Launcher(桌面启动器) 和 设置的包名信息,此后的com.android.incallui一般是拨号软件的包名,再加上实在老友的包名。假如包名和这四个匹配,阐明用户退出微信页面,中断使命履行。

另外,在阅读源码时还发现了作者不同版本的兼容方法,需求经过id定位节点的,把每个版本节点对应id存一个数组中,遍历查找:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

虽然没细看完整代码,不过大约能猜到作者的意图,这样处理的好处,不必区别Event类型,依据页面特征点进行匹配,当时处于哪一步,履行对应的处理逻辑,实属牛啤!

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

但咱们这儿不这样做,毕竟练手,2333,仍是特意区别event来游玩,后边再改善亦可,给出咱们的无障碍服务配置如下:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/accessibility_desc"
    android:accessibilityEventTypes="typeWindowStateChanged|typeViewClicked"
    android:accessibilityFeedbackType="feedbackSpoken"
    android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="100"
    android:canPerformGestures="true"
    android:packageNames="com.tencent.mm"
    android:settingsActivity="cn.coderpig.clearcorpse.SettingActivity" />

另外,实在老友配置文件中的 android:accessibilityFlags=”flagRetrieveInteractiveWindows” 这个是用来搭配 TYPE_WINDOWS_CHANGE 事情类型使用的:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测


⑤ 点击通讯录Tab

跳转微信后,定位到通讯录节点,触发点击,运转打印节点树的python脚本,输出成果如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

能够看到id未f2s得节点有多个,故这儿经过文本匹配,而它的clickable为false,阐明是不行点击的,得调用 parent() 取得他的父节点才干点击。

这种状况很常见,获取到的节点不支持点击,有时得连续调用好几个 parent() 才干拿到可点击的节点,跟连体蜈蚣一样。所以这儿封装下点击的方法,递归获取能点击的父节点,具体代码如下:

// 点击
fun AccessibilityNodeInfo?.click() {
    if (this == null) return
    if (this.isClickable) {
        this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
        return
    } else {
        this.parent.click()
    }
}
// 长按
fun AccessibilityNodeInfo?.longClick() {
    if (this == null) return
    if (this.isClickable) {
        this.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
        return
    } else {
        this.parent.longClick()
    }
}

补全下点击代码:

class ClearCorpseAccessibilityService : AccessibilityService() {
    companion object {
        const val LAUNCHER_UI = "com.tencent.mm.ui.LauncherUI"  // 主页
    }
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        if (event.eventType == TYPE_WINDOW_STATE_CHANGED) {
            when (event.className.toString()) {
                LAUNCHER_UI -> {
                    event.source?.let { source ->
                        source.getNodeByText("通讯录").click()
                    }
                }
            }
        }
    }
    override fun onInterrupt() { }
}

能够,运转后主动点击通讯录了。

关于结点查找的两个方法: findAccessibilityNodeInfosByViewId()findAccessibilityNodeInfosByText() 的回来类型都是 List<AccessibilityNodeInfo>,而咱们大部分时分只需榜首个结点,每次得写一堆判空然后取榜首个的重复代码显得不太漂亮,同样封装下,顺带加上轮询,由于有时页面可能还没load完,此刻拿不到节点,过一瞬间就能拿到了,封装后的代码如下:

/**
 * 依据id查找单个节点
 * @param id 控件id
 * @return 对应id的节点
 * */
fun AccessibilityNodeInfo.getNodeById(id: String): AccessibilityNodeInfo? {
    var count = 0
    while (count < 10) {
        findAccessibilityNodeInfosByViewId(id).let {
            if (!it.isNullOrEmpty()) return it[0]
        }
        sleep(100)
        count++
    }
    return null
}
/**
 * 依据id查找多个节点
 * @param id 控件id
 * @return 对应id的节点列表
 * */
fun AccessibilityNodeInfo.getNodesById(id: String): List<AccessibilityNodeInfo>? {
    var count = 0
    while (count < 10) {
        findAccessibilityNodeInfosByViewId(id).let {
            if (!it.isNullOrEmpty()) return it
        }
        sleep(100)
        count++
    }
    return null
}
/**
 * 依据文本查找单个节点
 * @param text 匹配文本
 * @param allMatch 是否全匹配,默许false,contains()方法的匹配
 * @return 匹配文本的节点
 * */
fun AccessibilityNodeInfo.getNodeByText(
    text: String,
    allMatch: Boolean = false
): AccessibilityNodeInfo? {
    var count = 0
    while (count < 10) {
        findAccessibilityNodeInfosByText(text).let {
            if (!it.isNullOrEmpty()) {
                if (allMatch) {
                    it.forEach { node -> if (node.text == text) return node }
                } else {
                    return it[0]
                }
            }
            sleep(100)
            count++
        }
    }
    return null
}
/**
 * 依据文本查找多个节点
 * @param text 匹配文本
 * @param allMatch 是否全匹配,默许false,contains()方法的匹配
 * @return 匹配文本的节点列表
 * */
fun AccessibilityNodeInfo.getNodesByText(
    text: String,
    allMatch: Boolean = false
): List<AccessibilityNodeInfo>? {
    var count = 0
    while (count < 10) {
        findAccessibilityNodeInfosByText(text).let {
            if (!it.isNullOrEmpty()) {
                return if (allMatch) {
                    val tempList = arrayListOf<AccessibilityNodeInfo>()
                    it.forEach { node -> if (node.text == text) tempList.add(node) }
                    if (tempList.isEmpty()) null else tempList
                } else {
                    it
                }
            }
            sleep(100)
            count++
        }
    }
    return null
}
/**
 * 获取结点的文本
 * */
fun AccessibilityNodeInfo?.text(): String {
    return this?.text?.toString() ?: ""
}

封装好的代码等下直接调,乐滋滋~


⑥ 老友列表点击

来到老友列表,仍是运转打印节点树的python脚本:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

直接就定位到了列表节点,但我忽然有点厌烦这种获取方法了,每次找节点都得运转一次脚本,得想办法简化下~

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

忽然心生一计,我直接写个递归遍历结点的方法,把要用到的信息打印出来不就好了?说干就干:

/**
 * 遍历打印结点
 * */
fun AccessibilityNodeInfo?.fullPrintNode(
    tag: String,
    spaceCount: Int = 0
) {
    if (this == null) return
    val spaceSb = StringBuilder().apply { repeat(spaceCount) { append("  ") } }
    logD("$tag: $spaceSb$text | $viewIdResourceName | $className | Clickable: $isClickable")
    if (childCount == 0) return
    for (i in 0 until childCount) getChild(i).fullPrintNode(tag, spaceCount + 1)
}
// 调用下
source.fullPrintNode("主页")

运转后,输出日志信息如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

舒服了啊,列表项的id也get√了,持续完善下代码:

const val CONTACT_LIST_ID = "js"
const val CONTACT_ITEM_ID = "hg4"
    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        when (event.eventType) {
            TYPE_WINDOW_STATE_CHANGED -> {
                when (event.className.toString()) {
                    LAUNCHER_UI -> {
                        event.source?.let { source -> source.getNodeByText("通讯录").click() }
                    }
                }
            }
            TYPE_VIEW_CLICKED -> {
                if (event.text[0] == "通讯录") {
                    // 这儿不能用event的getSource(),只能获取到发生改动的节点
                    // 需求调用getRootInActiveWindow()取得一切结点
                    rootInActiveWindow?.let { source ->
                        val contactList = source.getNodeById(wxNodeId(CONTACT_LIST_ID))
                        if (contactList != null) {
                            contactList.getNodeById(wxNodeId(CONTACT_ITEM_ID)).click()
                        } else {
                            logD("未能获取老友列表")
                        }
                    }
                }
            }
            else -> logD("$event")
        }
    }

运转看下效果:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

杠杠滴!有了上面的东西代码,后续的开发也变得简略了许多~


⑦ 联系人信息页点击发音讯

来到联系人信息页,获取联系人微信号,然后点击发音讯,代码如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

运转后正常点击,控制台看到联系人微信号也打印出来了~


⑧ 谈天页点击加号+转账

来到谈天页,点击更多按钮,底部弹出窗口点击转账。但这儿有些古怪,并没有走到上面的ChattingUI,所以这儿换成监听点击了发音讯,然后再履行这些操作。

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

理论上是这样,但实践上并没有点击转账,打断点发现,点击确实实是clickable的父节点。应该是微信做了什么防护,阻拦了节点的点击行为。这种状况得变通下了,用手势的方法来完成模仿点击,同样给出直接就能用的东西代码:

/**
 * 使用手势模仿点击
 * @param node: 需求点击的节点
 * */
fun AccessibilityService.gestureClick(node: AccessibilityNodeInfo?) {
    if (node == null) return
    val tempRect = Rect()
    node.getBoundsInScreen(tempRect)
    val x = ((tempRect.left + tempRect.right) / 2).toFloat()
    val y = ((tempRect.top + tempRect.bottom) / 2).toFloat()
    dispatchGesture(
        GestureDescription.Builder().apply {
            addStroke(GestureDescription.StrokeDescription(Path().apply { moveTo(x, y) }, 0L, 200L))
        }.build(),
        object : AccessibilityService.GestureResultCallback() {
            override fun onCompleted(gestureDescription: GestureDescription?) {
                super.onCompleted(gestureDescription)
                logD("手势点击完结: 【$x - $y】")
            }
        },
        null
    )
}

修改下调用处:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

运转后看看效果:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

能够,手势模仿点击正常~


⑨ 转账页处理逻辑

来到转账页,判别昵称后边是否有实在姓名,是阐明老友联系正常。没有的话,转账0.01,呈现反常状况弹窗(被删、拉黑、对方账号反常),呈现输入暗码的弹窗阐明联系正常。逻辑十分清楚,就直接给出代码吧:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

运转下看效果,先是反常老友:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

紧接着是正常老友:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

日志输出如下:

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测

能够,到此整条检测链条的根本流程就完成啦~


0x4、小结

不知不觉又到文尾,限于篇幅,并没有完成完整功用,现在还差:遍历一切老友履行上述逻辑和检测成果保存了,当然可能还有一些bug,后续会完善下更新到Github上:ClearCorpse,感兴趣的能够先Star,也能够自己续着写,师傅领进门,修行靠本身,多练多总结才是真,感谢,咱们下节再会~

【杰哥带你玩转Android自动化】AccessibilityService实战-微信僵尸好友检测