• 自动跳过APP启动页广告


    背景

    • 我们都知道现在随便打开一个APP,启动页都是充斥着各种各样的广告,一般都是要等三到五秒钟才会自己关闭,或者用户手动点击跳过按钮直接进入首页,其实这件事在以前更为泛滥,已经到了无法忍受的地步,不知道是哪位变态产品想出来的摇一摇进入广告,问题的关键在于TMD的设置的传感器参数非常灵敏,手机稍微动一下就触发打开广告的操作了,简直没人性

    • 其实网上已经有很多人写过类似的文章了,那么为什么我还要重复写一遍呐,虽然实现的整体思路大家都基本一致,但是具体的实现细节或多或少的有些不一样的,我也是抱着学习的态度去实现一遍,算是是对之前写的自动化系列文章的一种延续吧。

    实现思路

    • 所有的实现方案都是基于Android无障碍模式做的,加上我们之前已经对微信的各种自动化方案已经很熟悉了,所以这里实现起来也就顺手拈来了。

    • 我们要想自动点击跳过按钮就要找到触发的时机,一般APP启动的时候都会先进入启动页广告,然后在进入首页,所以我们的问题就变成怎么判断APP启动。

    • 我们知道在onAccessibilityEvent方法的回调中是可以看到当前event的一些具体信息的,比如包名,类名等。大致内容如下:

    EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 333079282; PackageName: com.lygttpod.android.auto;WindowChangeTypes: [] [ ClassName: com.lygttpod.android.auto.MainActivity; Text: [Android自动化]; 
    
    • 1

    我们只需要找到需要的包名,然后去遍历当前页面的元素,找到带有跳过或者关闭字样的节点然后触发点击事件即可。

    经过我们的大量尝试后会发现其实只需要监听typeWindowStateChanged就可以了,这样不仅可以减少频繁触发的次数也会过滤掉很多无用的event

    • 怎么根据当前事件的包名去找到我们需要的呐,这里我们可以定义一个配置文件fuck_ad_app_config.json,文件中添加我们需要跳过广告的APP信息,然后在服务链接的时候解析文件取出来数据进行匹配就ok了。
    {  
        "fuckAd":{  
            "apps":[  
                {  
                    "appName":"XX财富",  
                    "packageName":"com.eastmoney.android.berlin",  
                    "launcher":"com.eastmoney.android.berlin.activity.MainActivity",  
                    "adNodes":[  
                        {  
                            "action":"跳过"  
                        }  
                    ]  
                }  
            ]  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 看到了包名和启动页就相当于检测到APP启动了,然后用我们之前写好的方法遍历找到指定节点触发点击就好了。
        private fun loadAdConfig(): FuckAdApps? {
            return try {
                val json = AppContext.loadAsset("fuck_ad_app_config.json")
                if (json.isNullOrEmpty()) {
                    null
                } else {
                    val data: FuckAdApps =
                        Gson().fromJson(json, object : TypeToken() {}.type)
                    data
                }
            } catch (e: Exception) {
                e.printStackTrace()
                null
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 拿到配置文件中的数据后就判断触发event的应用是否在配置文件中,是的话就触发点击跳过操作,大致代码如下
        fun fuckAD(event: AccessibilityEvent) {
            fuckADTaskScope.launch {
                val packageName = event.packageName.default()
                val className = event.className.default()
                val adApp =
                    fuckAdApps?.fuckAd?.apps?.find { it.packageName == packageName && it.launcher == className }
                adApp?.let {
                    Log.d("FuckADTask", "打开了【${it.appName}】的【${it.launcher}】")
                    skipAd(it)
                }
            }
        }
        
        
        private suspend fun skipAd(adApp: AdApp) {
                val acc = FuckADAccessibility.fuckADAccessibility ?: return
                val actions = adApp.adNode.action.split(",")
                    val skipResult = actions.find { acc.clickByText(it) } != null
                    if (skipResult) {
                        withContext(Dispatchers.Main) {
                            Log.d("FuckADTask", "自动跳过广告啦")
                        }
                    }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 这样我们就实现了自动点击【跳过】的功能了,但是这样写的会有很大的局限性,就是我们必须先获取到所有需要跳过广告的APP的包名和启动页类名,这是一件非常麻烦的事情,当然我也尝试这么做了,首先我下载了应用市场top50的APP,一个一个的测试,在打印的log中寻找对应的信息,在我尝试找了二十多个APP的时候我放弃了,因为我发现这是一件非常笨重的做法,即使我把下载的这50个APP都配置好了,也不能保证别人能下载和我一模一样的APP啊,市场上APP那么多怎么可能搞得完。

    优化

    • 既然每个用户手机中安装的APP都不一样,我们能不能先获取手机中已安装的APP呐,然后再对其处理不就好了么,这样就可以针对当前手机设备中的APP进行操作了,不仅减少了非自己安装的APP遍历过程,也可以覆盖到每个用户的设备。而且Android也提供的有相应的API去获取应用列表。于是就有了如下代码
    fun Context.queryAllInstallApp(): MutableList {
        val packageManager = this.packageManager
        // 创建一个 Intent,用于查询所有启动的应用程序
        val intent = Intent(Intent.ACTION_MAIN, null)
        intent.addCategory(Intent.CATEGORY_LAUNCHER)
        // 使用 queryIntentActivities 获取所有匹配的应用列表
        val appInfoList =
            packageManager.queryIntentActivities(intent, 0)
                .filterNot { isSystemApp(it) || isSelf(it) }
                .map {
                    val appName = it.loadLabel(packageManager).toString()
                    val packageName = it.activityInfo.packageName
                    val className = it.activityInfo.name
                    AdApp(appName, packageName, className)
                }
                .toMutableList()
    
        return appInfoList
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 这样就可以全面覆盖到每个用户安装的APP了,但是在测试中会发现还会有点问题,就是手机系统中内置的APP也会在里边,有很多其实我们是不需要去监听的,于是我们就把系统内置APP给过滤掉,顺手再把自己的APP也过滤了。当然其实想要做的更好就在添加一个配置菜单,让用户自己去选择需要过滤的APP也是一种不错的选择(后边有需要再加吧),这里我就先自动过滤系统内置了
    // 检查应用是否为系统应用
    private fun isSystemApp(resolveInfo: ResolveInfo): Boolean {
        val regex = "com\.android\.[^.]+" // 匹配系统应用 com.android. 后面跟着任意不包含 . 的字符
        val result = resolveInfo.activityInfo.packageName.matches(Regex(regex))
        Log.d("isSystemApp", "isSystemApp: ${resolveInfo.activityInfo.packageName} : $result")
        return result
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 这样我们就真正实现了自动跳过自己设备安装的非系统APP的启动页广告了,让我们变一下看一下效果吧。
    打开了【知乎】的【com.zhihu.android.app.ui.activity.LauncherActivity】
    自动跳过【知乎】的广告啦
    打开了【知乎】的【com.zhihu.android.app.ui.activity.LauncherActivity】
    isSkipped: 5秒内已经成功跳过一次广告,无需重复检测
    打开了【网易云音乐】的【com.netease.cloudmusic.activity.IconChangeDefaultAlias】
    自动跳过【网易云音乐】的广告啦
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    进阶

    在我自己使用的过程中遇到误判的情况,比如某个页面的文本中带有跳过的字眼也会触发,所以我们还需要对误判的情况处理一下。这里我分了几种情况。

    1、action的长度

    • 广告页的跳过有一定的规则,一般都会有字数限制,比如跳过关闭跳过(5)5s | 跳过等等,我们这里可以添加一个最大字数长度限制的判断,大部分字数都在五个字以内,所谓我们设置默认最大长度是5,个别APP字数长的可以单独在进行设置。

    2、action所在类的类型

    • 广告页的跳过一般都是文本消息,如果如你在EditText中输入了跳过两个字,按照原来的判断逻辑也会触发我们的判断,所以我们只需要TextView类型的文本。当然还有极个别APP的广告跳过两个字是图片,真TM想方设法的搞事哦,遇到这种情况可以单独设置id也能做兼容,有的APP是没有text值的,只有id(12306APP就是只有id),如果连ID都TM没有那就直接向工信部举报吧(哈哈,开玩笑。。。)。

    3、action是否在列表内的内容

    • 大部分情况下广告页的内容不会是在列表中展示的,为了避免某些内容中的有跳过字眼触发误判,还可以加上判断是否在ListView或RecyclerView中等等,这样就避免了大部分内容区域带有关键字眼导致的误判

    经过上面这些判断后就会大大降低的误触的概率,当然有遇到特殊的情况可以继续加判断

    val skipResult = acc.clickByCustomRule {
        if (id.isNotBlank()) {
            it.viewIdResourceName == id
        } else {
            if (it.isTextView()) {
                val text = it.text.default()
                text.length <= adApp.actionMaxLength()
                        && actions.find { action -> text.contains(action) } != null
                        && it.inListView().not()
            } else {
                false
            }
        }
    }
    if (skipResult) {
        withContext(Dispatchers.Main) {
            Log.d("FuckADTask", "自动跳过【${adApp.appName}】的广告啦")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 当然也可以针对每个APP单独设置自己的规则,设置内容如下

    • 至此自动跳过启动页广告的自动化工具算是完成了,中间其实有很多优化的细节就不在文章中展开讲解了,需要详细了解的可以阅读一下源码

    适配

    • 【铁路12306】获取到的text是空,所以就设置ID为com.MobileTicket:id/tv_skip 进行适配,className = android.widget.TextView → id = com.MobileTicket:id/tv_skip → isClickable = true
    +--- className = android.widget.FrameLayout
    |  --- className = android.widget.LinearLayout
    |    --- className = android.widget.FrameLayout → id = android:id/content
    |      --- className = android.widget.LinearLayout → id = com.MobileTicket:id/ll_splash_ad_container
    |        +--- className = android.widget.FrameLayout → id = com.MobileTicket:id/fl_adContent_container
    |        |  +--- className = android.widget.ImageView → id = com.MobileTicket:id/img_adContent → description = 广告 → isClickable = true
    |        |  +--- className = android.widget.FrameLayout → id = com.MobileTicket:id/fl_skip_wrong → isClickable = true
    |        |  |  --- className = android.widget.TextView → id = com.MobileTicket:id/tv_skip → isClickable = true
    |        |  --- className = android.view.ViewGroup
    |        |    +--- className = android.widget.ImageView → id = com.MobileTicket:id/lottie_scroll_view
    |        |    --- className = android.view.ViewGroup → id = com.MobileTicket:id/cl_button_container → isClickable = true
    |        |      +--- className = android.widget.ImageView → id = com.MobileTicket:id/lottie_button_view
    |        |      +--- className = android.widget.TextView → text = 上滑或点击跳转至详情页 → id = com.MobileTicket:id/tv_button_text
    |        |      --- className = android.widget.ImageView → id = com.MobileTicket:id/iv_right_arrow
    |   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 【中国联通】获取到的文本是5s | 跳过,action长度是7,所以设置actionMaxLength为7即可
    +--- className = android.widget.FrameLayout
    |  --- className = android.widget.LinearLayout
    |    --- className = android.widget.FrameLayout
    |      --- className = android.widget.LinearLayout → id = com.sinovatech.unicom.ui:id/action_bar_root
    |        --- className = android.widget.FrameLayout → id = android:id/content
    |          --- className = android.widget.RelativeLayout
    |            +--- className = android.widget.FrameLayout → id = com.sinovatech.unicom.ui:id/splash_container2
    |            |  --- className = android.widget.RelativeLayout
    |            |    +--- className = android.widget.ImageView → id = com.sinovatech.unicom.ui:id/welcome_bg
    |            |    +--- className = android.widget.TextView → text = 5s | 跳过 → id = com.sinovatech.unicom.ui:id/welcome_advertise_close → isClickable = true
    |            |    --- className = android.widget.ImageView → id = com.sinovatech.unicom.ui:id/adv_bottom_detail → isClickable = true
    |            --- className = android.widget.ImageView → id = com.sinovatech.unicom.ui:id/welcome_bottom_logo
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在使用过程中如有发现其他特殊的APP欢迎提 issues 或者打在评论区,我们一起去维护,造福更多人。

    最后

    • 在具体的代码实现中已经做了很多优化工作,不会影响APP正常使用,也不会导致耗电增多或导致系统卡顿之类的问题,完全可以放心使用。

    • 为了防止我们的广告服务被系统杀死,可以设置我们的APP为系统白名单或者允许后台运行等一系列保活方法让它多活一会吧。

    为了帮助到大家更好的全面清晰的掌握好启动优化,准备了性能优化相关的核心笔记(包含了启动优化、内存优化、网络优化……等,还该底层逻辑):https://qr18.cn/FVlo89

    性能优化核心笔记:https://qr18.cn/FVlo89

    启动优化

    内存优化

    UI优化

    网络优化

    Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

    多线程并发优化与数据传输效率优化

    体积包优化

    《Android 性能监控框架》:https://qr18.cn/FVlo89

    《Android Framework学习手册》:https://qr18.cn/AQpN4J

    1. 开机Init 进程
    2. 开机启动 Zygote 进程
    3. 开机启动 SystemServer 进程
    4. Binder 驱动
    5. AMS 的启动过程
    6. PMS 的启动过程
    7. Launcher 的启动过程
    8. Android 四大组件
    9. Android 系统服务 - Input 事件的分发过程
    10. Android 底层渲染 - 屏幕刷新机制源码分析
    11. Android 源码分析实战

  • 相关阅读:
    Python 网络爬虫:深入解析 Scrapy
    HTTP请求数据、响应数据格式,Tomcat基本使用、IDEA中创建Maven Web项目、集成本地Tomcat
    WEB自动化测试需掌握的基础技能知识总结
    民安智库(第三方市场调研公司)哪家残疾人服务满意度调研公司比较专业
    腾讯云轻量数据库是什么?轻量数据库测评详细介绍
    网站优化搜索引擎与关键词
    Android 屏幕适配
    in git how to blame who delete this line code
    HarmonyOS之自选股App
    订单超时自动取消3种方案——我们用这种!
  • 原文地址:https://blog.csdn.net/weixin_61845324/article/details/134036246