文章记录了作者逆向修改「慧生活798」3.1.4 的过程:定位驾考页信息流广告与穿山甲 SDK 的加载链路,发现其会被预加载、重复请求、默认外放并拖慢启动,且广告开关形同虚设。作者借助 JADX 与 Apktool 短路广告方法、移除 SDK 预热、强制关闭推荐开关,并直接隐藏驾考 Tab,最终实现去广告与优化启动体验。
起因:这广告到底有多恶心
「慧生活798」是在学校要用到的 App,用来饮水、沐浴、洗衣服、吹头发什么的。3.1.4 版本的底部导航栏多了一个驾考。
这个驾考页面本身倒也无所谓,反正不点就是了——直到他们往里面塞了穿山甲(字节跳动广告 SDK)的信息流视频广告。问题不只是“有广告”这么简单,而是这个广告的实现方式堪称教科书级别的反面案例:
- 一进 App 就加载。驾考模块的
DrivingHomeFragment在主页面初始化时就被 ViewPager 预加载了,广告跟着一起触发,根本不需要你点进驾考 Tab。 - 加载两次。驾考首页有两个训练页 Fragment(
DrivingTrainingFragment),每个都独立调用FeedExpressAdUtil.loadFeedAd(),所以广告会请求两遍。 - 默认开声音。这条广告加载链路完全没有做 App 层的静音控制,视频素材默认带声音播放。两个 Fragment 各加载一次,于是你会听到两个视频广告的声音重叠在一起。打开 App 的瞬间,手机突然外放两段重叠的广告音,在公共场合社死效果拉满。
- 严重拖慢启动。穿山甲 SDK 的预热(
TTAdHelper.startTTAdSdk)被塞在了SplashActivity(启动页)、MainActivity(主页面)、甚至事件流回调MainActivity$subscribeEvent$3$1和DrivingHomeFragment里,四个位置都在做 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)
DrivingTrainingFragment 的 I() 方法是广告的实际加载点。它获取广告容器 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 的初始化调用散布在四个地方:
SplashActivity—— 启动页,构造函数里就 new 了TTAdHelperMainActivity—— 主页面,启动阶段检查 SDK readyMainActivity$subscribeEvent$3$1—— 事件流回调,又来一次DrivingHomeFragment—— 驾考模块初始化,再来一次
每次初始化都要走网络请求、初始化渲染引擎,叠加起来就是启动时的严重卡顿。
广告开关的骗局
RecommendSettingActivity 的逻辑:
- 读取
TTAdManager.shouldRecommend()的返回值来决定开关显示状态 shouldRecommend()的原始实现是检查一个时间戳——关闭后 14~28 天(随机)自动恢复为true- 驾考首页的信息流广告加载链路完全不检查这个方法的返回值,直接加载
修改方案
第一刀:短路广告加载方法
目标文件: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.smaliMainActivity.smaliMainActivity$subscribeEvent$3$1.smaliSplashActivity.smali
不再在启动阶段主动初始化穿山甲 SDK,启动速度明显改善。
第三刀:广告开关永久关闭
修改 TTAdManager.smali 的 shouldRecommend() 方法,直接返回 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"、宽高压为 0dp、layout_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 的广告集成都比较粗暴,逆向难度不高,效果立竿见影。
微信