文章记录了作者逆向修改「慧生活798」3.1.4 的过程:定位驾考页信息流广告与穿山甲 SDK 的加载链路,发现其会被预加载、重复请求、默认外放并拖慢启动,且广告开关形同虚设。作者借助 JADX 与 Apktool 短路广告方法、移除 SDK 预热、强制关闭推荐开关,并直接隐藏驾考 Tab,最终实现去广告与优化启动体验。
AI摘要

起因:这广告到底有多恶心

「慧生活798」是在学校要用到的 App,用来饮水、沐浴、洗衣服、吹头发什么的。3.1.4 版本的底部导航栏多了一个驾考

这个驾考页面本身倒也无所谓,反正不点就是了——直到他们往里面塞了穿山甲(字节跳动广告 SDK)的信息流视频广告。问题不只是“有广告”这么简单,而是这个广告的实现方式堪称教科书级别的反面案例:

  1. 一进 App 就加载。驾考模块的 DrivingHomeFragment 在主页面初始化时就被 ViewPager 预加载了,广告跟着一起触发,根本不需要你点进驾考 Tab。
  2. 加载两次。驾考首页有两个训练页 Fragment(DrivingTrainingFragment),每个都独立调用 FeedExpressAdUtil.loadFeedAd(),所以广告会请求两遍。
  3. 默认开声音。这条广告加载链路完全没有做 App 层的静音控制,视频素材默认带声音播放。两个 Fragment 各加载一次,于是你会听到两个视频广告的声音重叠在一起。打开 App 的瞬间,手机突然外放两段重叠的广告音,在公共场合社死效果拉满。
  4. 严重拖慢启动。穿山甲 SDK 的预热(TTAdHelper.startTTAdSdk)被塞在了 SplashActivity(启动页)、MainActivity(主页面)、甚至事件流回调 MainActivity$subscribeEvent$3$1DrivingHomeFragment 里,四个位置都在做 SDK 初始化。结果就是进 App 之后卡半天才能操作,启动体验被广告 SDK 彻底拖垮。

更离谱的是,设置页里有个“个性化信息推荐”入口,关掉这个本来能关掉整个 APP 内的广告。但是驾考首页的信息流广告走的是完全独立的加载链路,根本不读这个开关的状态。就算你把开关关了,广告照样加载、照样播。而且这个开关还有个 14 天自动恢复逻辑(TTAdManager.shouldRecommend() 里的时间阈值判断),关了过两周自己又开回来。

行吧。既然你不让我关,那我就自己动手拆了。

工具准备

  • JADX:反编译 APK 为 Java 源码,用来快速定位逻辑和阅读代码
  • Apktool:解包/重打包 APK,修改 smali 和资源文件

解包命令:

apktool d 慧生活798_3.1.4.apk -o hsh798_3.1.4_apktool

逆向分析

定位广告加载链路

用 JADX 搜索广告相关关键词,很快定位到核心链路:

MainActivity 启动
  └─ ViewPager 预加载 DrivingHomeFragment
       └─ 初始化两个 DrivingTrainingFragment
            └─ SDK ready 后调用 I() 方法
                 └─ FeedExpressAdUtil.loadFeedAd(...)
                      └─ 广告位 INFORMATION_FLOW (ID: 103345090)

DrivingTrainingFragmentI() 方法是广告的实际加载点。它获取广告容器 mLlAd,然后调用 FeedExpressAdUtil.loadFeedAd() 请求穿山甲信息流广告。两个训练页 Fragment 各自持有独立的 FeedExpressAdUtil 实例,所以会发出两次广告请求。

广告 SDK 枚举

TTAdIdEnum.smali 可以看到这个 App 注册了一整套广告位:

枚举值 广告位 ID 类型
SPLASH 103344598 开屏广告
INFORMATION_FLOW 103345090 信息流广告(就是驾考页这个)
BANNER 103344599 横幅广告
REWARD_VIDEO 103357192 激励视频
INTERSTITIAL_FULL_SCREEN 103345182 全屏插屏
DOOR_LOCK_INTERSTITIAL_FULL_SCREEN 103939417 门锁全屏插屏

SDK 预热位置

TTAdHelper 的初始化调用散布在四个地方:

  1. SplashActivity —— 启动页,构造函数里就 new 了 TTAdHelper
  2. MainActivity —— 主页面,启动阶段检查 SDK ready
  3. MainActivity$subscribeEvent$3$1 —— 事件流回调,又来一次
  4. DrivingHomeFragment —— 驾考模块初始化,再来一次

每次初始化都要走网络请求、初始化渲染引擎,叠加起来就是启动时的严重卡顿。

广告开关的骗局

RecommendSettingActivity 的逻辑:

  1. 读取 TTAdManager.shouldRecommend() 的返回值来决定开关显示状态
  2. shouldRecommend() 的原始实现是检查一个时间戳——关闭后 14~28 天(随机)自动恢复为 true
  3. 驾考首页的信息流广告加载链路完全不检查这个方法的返回值,直接加载

修改方案

第一刀:短路广告加载方法

目标文件:DrivingTrainingFragment.smali

原始的 I() 方法会调用 FeedExpressAdUtil.loadFeedAd() 加载广告。修改后直接短路:只执行隐藏广告容器 mLlAd 的操作,然后 return-void,不再调用任何广告加载方法。

修改后的 I() 方法核心逻辑(smali):

.method public final I()V
    .locals 3

    # 获取广告容器 mLlAd
    sget-object v0, Lcom/ilife/lib/common/util/u0;->a:Lcom/ilife/lib/common/util/u0;
    invoke-virtual {p0}, Lcom/ilife/lib/common/base/BaseFragment;->m()Landroidx/viewbinding/ViewBinding;
    move-result-object v1
    check-cast v1, Lcom/ilife/module/car/databinding/FragmentDrivingTrainingBinding;
    iget-object v1, v1, Lcom/ilife/module/car/databinding/FragmentDrivingTrainingBinding;->z:Landroid/widget/LinearLayout;
    const-string v2, "binding.mLlAd"
    invoke-static {v1, v2}, Lkotlin/jvm/internal/x;->f(Ljava/lang/Object;Ljava/lang/String;)V

    # 隐藏容器,然后结束——不再调用 loadFeedAd
    invoke-virtual {v0, v1}, Lcom/ilife/lib/common/util/u0;->a(Landroid/view/View;)V

    return-void
.end method

同时把被短路后不再被调用的 J() 方法也清空为直接 return:

.method public final J()V
    .locals 0
    return-void
.end method

第二刀:干掉 SDK 预热

四个位置的 TTAdHelper 初始化/预热调用全部替换为 nop 或短路,涉及文件:

  • DrivingHomeFragment.smali
  • MainActivity.smali
  • MainActivity$subscribeEvent$3$1.smali
  • SplashActivity.smali

不再在启动阶段主动初始化穿山甲 SDK,启动速度明显改善。

第三刀:广告开关永久关闭

修改 TTAdManager.smalishouldRecommend() 方法,直接返回 false

.method public final shouldRecommend()Z
    .locals 1

    const/4 v0, 0x0

    return v0
.end method

原来的 14~28 天恢复逻辑全部失效。

同时修改 RecommendSettingActivity.smali,让设置页的开关永远显示为关闭状态,并通过 setEnabled(false) 禁用交互,防止误点恢复。

第四刀:直接砍掉驾考 Tab

既然驾考模块是广告的载体,而且对我来说毫无用处,干脆整个砍掉。

布局层activity_main.xml):

驾考 Tab 的 mLlDrivingExam 设为 visibility="gone"、宽高压为 0dplayout_weight 设为 0

<LinearLayout android:id="@id/mLlDrivingExam"
    android:visibility="gone"
    android:layout_width="0.0dp"
    android:layout_height="0.0dp"
    android:layout_weight="0.0"
    android:padding="0.0dp">

逻辑层MainActivity.smali):

  • ViewPager 不再装载 DrivingHomeFragment,只装三个页面:首页、积分、我的
  • 底部导航点击事件的页面索引重新映射:0=首页1=积分2=我的
  • onPageSelected 的高亮状态同步修正,避免视觉错位
  • 所有与驾考 Tab 可见性相关的方法统一改为彻底隐藏

重编译

$env:_JAVA_OPTIONS = '-Xmx4096m'
apktool b "hsh798_3.1.4_apktool" -o "hsh798_3.1.4_noad_notab_unsigned.apk"

产出的是未签名 APK,需要签名后才能安装。

最终效果

  • 驾考首页信息流广告不再加载,不再有任何声音
  • 启动阶段穿山甲 SDK 预热已去除,启动速度回归正常
  • 广告推荐开关永久关闭,不再有 14 天自动恢复
  • 主页面底部不再显示驾考 Tab,ViewPager 不再装载驾考模块
  • 原来的四 Tab 布局变成干净的两 Tab:首页 / 我的

吐槽

这个驾考广告的实现甚至称不上“正常的恶心”——加载两次导致声音重叠都不修,默认不静音都不管,挂在 ViewPager 预加载上一进 App 就播都不控制。要么是开发者根本没测试过这条链路,要么就是故意的。无论哪种,用户体验都是零分。

所幸 smali 层面的修改并不复杂。短路几个方法调用、改几个返回值、调整一下 ViewPager 的页面映射,一个下午就能把这坨东西清理干净。工具链也很成熟,JADX 负责阅读理解,Apktool 负责手术,流程顺滑。

如果你也在用类似的被广告塞满的 App,不妨试试同样的思路:用 JADX 定位广告入口,用 Apktool 解包改 smali,短路加载方法,重编译签名。大部分国产 App 的广告集成都比较粗暴,逆向难度不高,效果立竿见影。