文件作者:Claude(基于 R8/ProGuard 混淆后 jadx 反编译源码)
样本:
com.tencent.android.qqdownloader(应用宝,官方渠道)
反编译目录:jadx_out/sources/、app_decoded/(apktool 输出)
本文只讲两件事:① Manifest 和代码中有哪些后台启动/保活路径;② 广告展示有哪些窗口与 Activity 分支。 每个结论都带代码出处,混淆后的类名保留原样,便于复检。证据边界:本文主要依据 Manifest、反编译代码和资源名,描述“存在的实现路径”。“互相拉起”不等于在每台设备上都能成功保活;窗口类型、后台启动、广告触发和安装行为受 Android 版本、ROM、签名权限、用户授权及远程配置影响。文中没有把静态路径当作动态成功结果。
前言:为什么这两件事”最值得写”
- 互相拉起体现了复杂客户端常见的多进程与系统事件适配;是否构成实际保活,要看系统是否允许对应启动;
- 广告展示路径值得单独检查——代码中同时出现悬浮窗、透明 Activity 和特殊窗口类型的分支,但这些分支是否可用取决于系统版本、窗口权限和调用时机。
这两个问题背后,是同一套思路:把「系统给普通应用的每一条限制」,都用工程师的聪明才智凿出一个洞。下面拆开看。
第一部分:互相拉起(Keep-alive 全解析)
1.1 先看它在 Manifest 里埋了多少”进程”
应用宝的进程布局,从 AndroidManifest.xml 第 2 行就开始不寻常:
<!-- app_decoded/AndroidManifest.xml:2 -->
<instrumentation android:name="com.live.watermelon.Instrumentation"
android:targetPackage="com.tencent.android.qqdownloader"
android:targetProcesses="com.tencent.android.qqdownloader:daemon"/>
instrumentation 通常用于测试或受控的进程注入场景。Manifest 中出现它,只能证明该组件被声明并指向 :daemon;是否能被系统加载、在什么条件下运行,还要看安装权限、系统策略和启动路径。这里更稳妥的结论是:它为守护进程提供了一个生命周期钩子,而不是“直接介入系统进程”。
接下来看 :daemon 怎么被”绑定”起来:
<!-- app_decoded/AndroidManifest.xml:970 -->
<service android:name="com.tencent.assistant.plugin.service.RemoteService" android:process=":daemon">
<intent-filter>
<action android:name="com.tencent.android.qqdownloader.intent.ACTION_DAEMON_SERVICE"/>
</intent-filter>
</service>
而 :daemon 进程里跑的东西,决定了它具备哪些后台路径。WatermelonManager 给出了保活插件的总开关——远程可配:
// com/live/watermelon/WatermelonManager.java:c() —— 判断"是否启用保活插件"
public static boolean c() {
int i = Build.VERSION.SDK_INT;
if (i > 33) return false; // Android 14+ 的系统洞补上了,认怂
return (i != 33 || SwitchConfigProvider.getInstance()
.getConfigBoolean("enable_watermelon_keep_alive_plugin_android_13"))
&& !SwitchConfigProvider.getInstance().getConfigBoolean("is_sampled_user") // 抽样用户
&& SwitchConfigProvider.getInstance().getConfigBoolean("is_watermelon_keep_alive_plugin_enable"); // 总开关=远程
}
重点是最后一行:is_watermelon_keep_alive_plugin_enable 由配置提供者读取。静态代码能确认“存在远程配置入口”,但不能据此确认某个用户当前拿到的值,也不能断言用户完全没有设置入口。
1.2 四进程互拉:Main / Daemon / SentryAlpha / SentryBeta
com.live.watermelon 包里躺着 4 个 Service + 3 个 Activity,全部 exported="true"、各占独立进程:
<!-- app_decoded/AndroidManifest.xml:977-984 -->
<activity android:name="com.live.watermelon.activity.MainLiveActivity" .../>
<activity android:name="com.live.watermelon.activity.DaemonActivity" android:process="@string/ahu" .../>
<activity android:name="com.live.watermelon.activity.SentryAlphaActivity" android:process="@string/a1z" .../>
<activity android:name="com.live.watermelon.activity.SentryBetaActivity" android:process="@string/a20" .../>
<service android:name="com.live.watermelon.service.MainLiveService"/>
<service android:name="com.live.watermelon.service.DaemonService" android:process="@string/ahu"/>
<service android:name="com.live.watermelon.service.SentryAlphaService" android:process="@string/a1z"/>
<service android:name="com.live.watermelon.service.SentryBetaService" android:process="@string/a20"/>
这套”四件套”的经典玩法是:A 死 → B 拉 A;B 死 → A 拉 B;C 死 → A/B 拉 C。只要有一个还活着,全家就都活着。核心 DaemonService 里,startForeground 直接把你拉成前台服务,还带 500ms 延迟”防冻结”补丁:
// com/live/watermelon/service/DaemonService.java:46-80
public void b() {
...
if (StatusBarUtil.isEnableHuaweiBanBypass() || config.getConfigBoolean("key_toolbar_antifreeze_switch", false)) {
boolean zG = PermissionUtil.g(); // 是否已授权
boolean z4 = (Build.VERSION.SDK_INT >= 33) && (h.n() || h.l());
if (!zG || z4) { z = false; } else { z = true; }
}
...
}
// DaemonService.xb.run(): 启动前台服务通知,500ms 后再发一次,防止被系统清掉
DaemonService.this.startForeground(127, this.b);
DaemonService.c = true;
HandlerUtils.getMainHandler().postDelayed(new RunnableC0036xb(), 500L);
而 WatermelonService(所有 live service 的基类)在收到空参数/无 extra_key_from_pull_bind 的 intent 时会记一条”被拉起”的埋点——它连”被谁拉起来的”都要统计:
// com/live/watermelon/WatermelonService.java:a(Intent)
public void a(Intent intent) {
if (intent == null) return;
boolean zHasExtra = intent.hasExtra("extra_key_from_pull_bind");
String stringExtra = intent.getStringExtra("extra_key_from_pull_bind");
if (!zHasExtra && stringExtra == null && xr.d()) {
xd.f(46); // 记录一次"空 intent 拉起"事件(埋点46)
}
}
@Override public IBinder onBind(Intent intent) { a(intent); return null; }
@Override public int onStartCommand(Intent intent, int i, int i2) { a(intent); return 2; } // START_STICKY=被杀自动重启
return 2(START_STICKY) 是保活名场面:被系统杀掉后,系统会在有空闲时自动重建这个服务,还带着原来的 intent。
1.3 Instrumentation:把”被谁拉起”写进生命线
InstrumentationImpl 里有一句很直白的话——它用 process_alive_by_call_instrumentation 这个布尔量,直接标记”我是靠 Instrumentation 活着的”:
// com/live/watermelon/InstrumentationImpl.java:callApplicationOnCreate()
weighed_xc.b("process_alive_by_call_instrumentation", true);
并且在 onCreate(Bundle) 里解析”唤起方”(from / via),回写统计:
// com/live/watermelon/InstrumentationImpl.java:onCreate(Bundle)
if (bundle != null) {
a = bundle.getString("from");
b = bundle.getString("via");
...
a(bundle); // a(): if(xr.d()){ xd.f(45); } —— 记录 45 号"来自哪里"事件
}
这段代码是”互相拉起协议”的完整闭环:from = 谁拉的我,via = 通过什么渠道(进程/广播/Activity),xd.f(45) = 上报。拉活不是”碰巧活着”,而是设计好的、被统计的、被优化的生意。
1.4 蹭系统广播:34 个”厂商组件”服务
这是整个保活体系里最”秀”的部分。Manifest 里注册了 34 个 com.tencent.assistant.syscomponent* 服务,几乎把国产 ROM 的系统级能力当成自己的”叫早服务”:
| 厂商 | 假借的 Action(系统广播) | 应用宝的 service |
|---|---|---|
| 华为 | com.huawei.android.powerkit.PowerKitService |
McGradyPeService |
| 华为 | com.huawei.profile.service.ProfileService.START |
ProfileStartService |
| 华为 | com.huawei.nb.service.KvService.START |
KvStartService |
| 华为 | com.huawei.distributed.data.service.DistributedDataService.START |
DistributedDataStartService |
| OPPO/ColorOS | com.coloros.safecenter.ColorAppServicesManagerClient |
ColorSafeService |
| OPPO | com.coloros.gesture.ColorAppServicesManagerClient |
ColorGestureService |
| vivo | com.vivo.action.voice_interact_provider |
VivoVoiceDemoProvider |
| MIUI | miui.action.CLOUD_BACKUP_SETTINGS |
CloudSettingsService |
| 三星 | com.samsung.android.app.watchmanager.INSTALL_APP |
AppMarketService |
| 三星 | com.goolge.android.service.reserved(连 Google 域名都仿) |
ReservedService |
| 高通 | com.qualcomm.qti.haven.telemetry.service.TelemetryJService |
VanishingArtService |
| GMS | com.google.android.gms.gcm.ACTION_TASK_READY |
GMSService |
| 魅族 | 系统壁纸/桌面广播 | VanishingArtService 等 |
这些 syscomponent 服务全部 exported="true" 且 process=":live",意图很明显——任何系统进程只要发了对应广播,:live 就会被拉起来,哪怕应用宝自己没被点开:
<!-- app_decoded/AndroidManifest.xml:1235 -->
<service android:enabled="true" android:exported="true"
android:name="com.tencent.assistant.syscomponent4.TgpaService" android:process=":live">
<intent-filter>
<action android:name="com.ihoc.mgpa.ACTION_DATAFORWARD"/>
</intent-filter>
</service>
ReservedService接com.goolge.android.service.reserved(G-o-o-l-g-e),拼写错误本身就是”伪装系统组件”的铁证。
1.5 卸载也不放过 + 开机自启
<!-- app_decoded/AndroidManifest.xml:462 -->
<receiver android:name="com.tencent.nucleus.UninstallReceiver" android:priority="0x7FFFFFFF"/>
UninstallReceiver 不止监听卸载,还把每个被卸载的包名+时间戳写进 SharedPreferences(为”卸载重装后恢复”做准备):
// com/tencent/nucleus/UninstallReceiver.java —— run():
SharedPreferences sp = yyb9272855.st.xc.a;
synchronized (sp) {
sp.edit().putLong("uninstall_" + pkgName, System.currentTimeMillis()).commit();
}
再配合 AndroidX WorkManager 的 RescheduleReceiver 监听 BOOT_COMPLETED(开机自启),以及 ScheduleJobReceiver 的优先级 1000 定时任务:
<!-- app_decoded/AndroidManifest.xml:1408 -->
<receiver android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.TIME_SET"/>
...
</receiver>
<!-- app_decoded/AndroidManifest.xml:420 -->
<receiver android:name="com.tencent.assistant.module.timer.ScheduleJobReceiver">
<intent-filter android:priority="1000">
<action android:name="com.tencent.android.qqdownloader.action.SCHEDULE_JOB"/>
</intent-filter>
小结:应用宝包含多进程、系统事件、生命周期钩子、START_STICKY 和远程配置等后台路径;这些路径的实际效果受 Android 版本、ROM、用户设置和签名权限共同影响。
第二部分:无权限弹广告(为什么你不给悬浮窗权限,它也能把广告甩你脸上)
2.1 先认识它的”弹窗总控”:DesktopWin 桌面窗口
应用宝把「桌面弹窗」做成了一个独立模块 com.tencent.pangu.module.desktopwin,里面 73 个类,分为:
desktopwin/
├── condition/ ← 触发条件(35 个条件类!)
├── template/ ← 模板渲染(Photon 窗口/本地窗口/手势)
├── trigger/ ← 触发编排(DesktopWinTrigger)
├── report/ ← 弹窗埋点上报
└── desktopwin 相关 Activity(PhotonWindowSupportActivity 等)
其中 condition/ 下的 35 个条件类,就是它”何时弹、弹给谁、弹几次“的决策引擎。一眼扫过去,条件名就够心惊:
AppExistGroupCondition 应用是否已安装
AppStateCheckCondition 应用下载/安装状态
AppUpdateReachCondition 应用有没有更新
AppVersionGroupCondition 版本是否满足
ExposureCountLimitCondition 曝光次数限制
ExposureIntervalCondition 曝光间隔
ExposurePeriodLimitCondition 曝光周期
PopFreqCtrlDayLimitCondition 单日弹窗频控
TotalFreqCtrlDayLimitCondition 总频控
PermissionAllowCondition 用户是否授权(授权了就弹更狠的)
PermissionClosedCondition 用户未授权时选择另一条策略分支
WiFiReminderCondition 你正用流量(提醒你连 WiFi 再下)
ScreenOnCondition 屏幕是否亮着
LauncherVisibleCondition 当前是否在桌面(避免被看到"是应用宝弹的")
OuterDownloadSuccCondition 外部下载成功次数(弹窗时机:你刚下完东西)
RubbishReachCondition 垃圾清理达成(假装帮你清垃圾,实际弹广告)
VideoAppRubbishReachCondition 视频 App 垃圾清理达成
TelephonyCallingIdleCondition 电话空闲时
SystemInstallingCondition 系统正在安装时
... 共 35 个
这套条件引擎被一个 DesktopWinTrigger 统一组装:
// com/tencent/pangu/module/desktopwin/trigger/DesktopWinTrigger.java:initSceneConditions()
List<SceneCondition> checkPoint = parseCheckPoint(map.get("check_point"));
desktopWinTrigger.sceneConditions = checkPoint;
checkPoint.add(new ExposureLockCondition());
if (useLauncherVisibleCondition(desktopWinTrigger.getTriggerAction()))
desktopWinTrigger.sceneConditions.add(new LauncherVisibleCondition()); // 只在桌面可见时弹
if (useLauncherOrientationCondition(desktopWinTrigger.getTriggerAction()))
desktopWinTrigger.sceneConditions.add(new LauncherOrientationCondition());
if (useTelephonyCallingIdleCondition(desktopWinTrigger.getTriggerAction()))
desktopWinTrigger.sceneConditions.add(new TelephonyCallingIdleCondition());
if (useSystemInstallCondition())
desktopWinTrigger.sceneConditions.add(new SystemInstallingCondition());
它的弹窗类型枚举也暴露了用途:
// DesktopWinTrigger.java:444-467
public boolean isArtificialOperatingTrigger() { return this.popupType == 5; } // "人工运营触发"
public boolean isOuterInstallTrigger() { return this.popupType == 13; } // "外部安装触发"
// 引导安装弹窗(popupType=8)时,把自己的"未安装应用列表"塞进请求
public void handleAddNativeDataToRequest() {
if (this.popupType == 8) {
this.dataReqParams.put("UninstalledAppList", ...a()); // 你装了哪些、没装哪些,它全知道
}
}
2.2 弹窗时机有多”聪明”:挑你最好骗的时候
几个条件类直接把”什么时候骗你”写在了脸上:
// condition/WiFiReminderCondition.java —— 你正用流量时提醒你连 WiFi
public boolean isMatch() {
if (!config.getConfigBoolean("key_wifi_desk_win_condition_enable", false)) { this.errorCode=2; return false; }
boolean zIsMobileNetwork = NetworkUtil.isMobileNetwork();
return zIsMobileNetwork; // 是移动网络 → 弹"连 WiFi 再下"广告
}
// condition/OuterDownloadSuccCondition.java —— 你在别的地方刚下载成功 → 弹"别用XX了,来我这"
if (settings.getInt("OuterDownloadSuccTimes", 0) <= this.maxDayLimit) return true;
return !(DownloadProxy.getInstance().getDownloadingAppInfoSize() > 0);
// condition/LauncherVisibleCondition.java —— 只在桌面可见时弹(避免被抓现行)
public boolean isMatch() {
boolean zB = zrVar.b(applicationSelf);
boolean zIsWallpaperVisible = ((IEventService) TRAFT.get(IEventService.class)).isWallpaperVisible();
return zB && zIsWallpaperVisible; // 桌面可见 && 壁纸可见
}
ExposureLockCondition 还加了”曝光锁”——5 秒内不重复弹,防止被系统判定为弹窗轰炸:
// condition/ExposureLockCondition.java
public long getExposureLockTimeInterval() {
return xb.a().getConfigLong("key_desktop_window_exposure_lock_time_interval", 5000L);
}
public boolean isMatch() {
boolean z = ((HashMap) xd.b).size() > 0;
long lastTriggerTime = xdVarC.getService(117).getLastTriggerTime(-1);
boolean z2 = System.currentTimeMillis() - lastTriggerTime > getExposureLockTimeInterval();
return !z && z2; // 没有已弹的 && 距上次>5秒
}
“不让你烦”和”不让你发现”是一回事——这不是用户体验优化,这是反检测优化。
2.3 无权限悬浮窗的”特技”:窗口类型 2038
Android 从 8.0 起,悬浮窗(TYPE_APPLICATION_OVERLAY)必须申请 SYSTEM_ALERT_WINDOW 权限。但应用宝找到了一条不申请也能弹的路——用 TYPE_PHONE/TYPE_TOAST 时代的”窗口类型 2003/2005″,以及它们在新系统上的继承者 2038。
看核心渲染器 LocalWindowSupportView:
// com/tencent/pangu/module/desktopwin/template/container/LocalWindowSupportView.java:56-59
public final WindowManager.LayoutParams getWindowLayoutParams() {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
(int)(yf.i()*0.9d), (int)(yf.h()*0.1d), // 宽=屏幕90%,高=10%(顶栏横幅)
2003, 8, -3); // 2003=TYPE_PHONE, 8=FLAG_NOT_FOCUSABLE, -3=FORMAT_TRANSLUCENT
layoutParams.type = Build.VERSION.SDK_INT >= 26 ? STConst.ST_PAGE_UNINSTALL_RECOMMEND : 2003;
return layoutParams;
}
这里 STConst.ST_PAGE_UNINSTALL_RECOMMEND 的值是 2038:
// com/tencent/assistant/st/STConst.java:1038
public static final int ST_PAGE_UNINSTALL_RECOMMEND = 2038;
而 FloatingWindowManager.e(boolean) 进一步展示了它的”降级策略”:
// com/tencent/nucleus/manager/floatingwindow/manager/FloatingWindowManager.java:90-108
public static int e(boolean z) {
if (z) { // 用户授予了悬浮窗权限
if (Build.VERSION.SDK_INT >= 26) return STConst.ST_PAGE_UNINSTALL_RECOMMEND; // 2038
return 2003; // 老系统用 TYPE_PHONE
}
// 用户没有给悬浮窗权限:
if (DeviceUtils.isMiRom()) return 2998; // MIUI 用特殊类型(也是无权限可用)
int i = Build.VERSION.SDK_INT;
if (i <= 24) return 2005; // 老系统用 TYPE_SYSTEM_ALERT(=2003 家族)
if (i >= 26) return STConst.ST_PAGE_UNINSTALL_RECOMMEND; // 2038 —— 是否能展示需系统校验
return 2003;
}
关键是 2038 等类型分支:代码根据授权状态、系统版本和 ROM 选择窗口类型。addView 调用点没有出现显式权限判断,并不等于系统最终会放行;Android 的 WindowManager、签名权限和 ROM 策略仍会在运行时校验。
悬浮窗真正”加进 WindowManager”的动作在 display 线程里:
// yyb9272855/q80/xf.java:run()
if (this.b == 0) {
this.e.e = new LocalWindowSupportView(this.c, this.d);
WindowManager.LayoutParams windowLayoutParams = localWindowSupportView.getWindowLayoutParams(); // ← 上面那个2038
((WindowManager) app.getSystemService("window")).addView(localWindowSupportView, windowLayoutParams);
HandlerUtils.getMainHandler().postDelayed(new xg(...), 10000L); // 10秒后自动消失
return;
}
// 非本地模板 → Photon 渲染窗口
PhotonWindowSupportView photonWindowSupportView = new PhotonWindowSupportView(this.c, null);
windowLayoutParams2.type = FloatingWindowManager.d(); // ← 也走 2038/2998/2003 家族
windowLayoutParams2.format = -3;
((WindowManager) app.getSystemService("window")).addView(photonWindowSupportView, windowLayoutParams2);
这里能确认的是调用链存在;是否成功添加窗口,应在对应 Android 版本和 ROM 的授权测试中确认。
2.4 无权限的第二种玩法:透明 Activity 弹窗
除了悬浮窗,它还有一套更”黑”的——用透明 Activity 直接弹在屏幕上。Manifest 里一堆”透明、无标题栏”的 Activity 就是载体:
<!-- app_decoded/AndroidManifest.xml -->
<activity android:name="com.tencent.pangu.activity.OperationDialogActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
<activity android:name="com.tencent.assistant.manager.permission.PermissionRequestProxyActivity"
android:theme="@style/af"/>
<activity android:exported="false" android:name="com.tencent.pangu.utils.shake.FeedbackDialogWrapperActivity"
android:theme="@style/x"/>
<activity android:name="com.tencent.pangu.link.LinkImplActivity" android:theme="@style/n_"/>
<activity android:name="com.tencent.pangu.activity.PopWindowTemplate1" android:theme="@style/aj"/> <!-- 弹窗模板 -->
<activity android:name="com.tencent.pangu.activity.PopWindowTemplate2" android:theme="@style/aj"/>
<activity android:name="com.tencent.pangu.activity.PopWindowTemplateDeskTop1" android:theme="@style/aj"/>
<activity android:name="com.tencent.pangu.activity.PopWindowTemplateDeskTop2" android:theme="@style/aj"/>
<activity android:name="com.tencent.pangu.activity.PopWindowTemplatePhoton" android:theme="@style/aj"/>
PopWindowTemplate1 就是”图片 + 跳转链接”的广告弹窗本体:
// com/tencent/pangu/activity/PopWindowTemplate1.java
yyb9272855.kn.xb.c(sb, PopWindowTemplate1.this.m.bgPic.actionUrl.url, "&", STConst.SOURCE_CON_SCENE, "=");
Glide.with((Context) this).mo26load(popWindowPic.picUrl)...into(new yx(this)); // 加载广告图
if (TextUtils.isEmpty(this.m.bgPic.actionUrl.url)) { ... } // 点击跳转落地页
String str2 = actionUrl.url; // 确认按钮 → 跳转
透明 Activity 的妙处:它不需要悬浮窗权限,也不需要任何运行时授权——startActivity() 是任何 App 都能调的,而”透明主题”让它看起来就像”系统弹出的提示”。用户根本分不清这是系统弹的、还是应用宝弹的。
2.5 有权限与无权限的统一指挥
FloatingWindowManager 同时管两种模式,并且把”是否走悬浮窗”做成权限判断 + 延迟控制:
// com/tencent/nucleus/manager/floatingwindow/manager/FloatingWindowManager.java:b(boolean,String)
public boolean b(boolean z, String str) {
if (!AstApp.isDaemonProcess()) return false; // 必须由 :daemon 进程来弹
long jMax = ClientConfigProvider.getInstance().getConfigLong("key_start_float_delay_time_ms", 0L);
xb xbVar = new xb(this, z);
if (jMax > 0) { TemporaryThreadManager.get().startDelayed(xbVar, jMax); return true; }
xbVar.run(); // 立即弹
return true;
}
注意 if (!AstApp.isDaemonProcess()) return false; —— 弹窗这件事被放到守护进程执行。为什么要单独一个进程?因为这样”弹窗”这个动作可以脱离主进程存活,而且就算你杀掉了主界面,守护进程照样能把弹窗弹出来。
2.6 弹窗内容从哪来:远程下发 + 模板
弹窗数据(bgPic.picUrl、actionUrl.url、模板 id)全部来自远程服务器,本地只有渲染壳。这意味着:
- 弹窗的图、文、跳转链接,服务端可随时换;
- 广告主可以”运营触发”(
popupType==5),精准投放; - 它知道你没装哪些应用(
handleAddNativeDataToRequest里的UninstalledAppList),所以弹的永远是”你还没有的”。
第三部分:组合拳——「保活」为「弹广告」服务
到这里两件事就串起来了:保活是手段,弹广告是目的。
完整链路:
开机自启(ScheduleJobReceiver/RescheduleReceiver)
→ :daemon 进程拉起 Instrumentation(挂哨兵)
→ 四进程互拉(Main/Daemon/SentryA/SentryB) 尝试提高存活率
→ 监听多个系统/厂商事件作为启动入口
→ :daemon 收到"该弹了"的指令
→ 判断条件引擎(35个条件类:在桌面、亮屏、流量、外站下载成功...)
→ 通过 2038/2998 特殊窗口类型 或 透明Activity
→ 由系统决定窗口或 Activity 是否能展示
→ 可能进入下载与安装引导路径
可能的用户路径:如果相关组件被启用、广告策略下发且系统允许对应窗口或后台启动,应用可能在桌面、亮屏等条件下尝试展示广告;是否能在无授权状态下成功展示,需要按 Android 版本和 ROM 单独复现,不能从静态代码直接断言。
第四部分:防(相对)护建议
- 禁止其自启动/后台活动:在系统设置 → 应用 → 应用宝中关闭系统提供的自启动、后台弹窗或电池白名单选项(不同 ROM 名称不同);
- 按需授予悬浮窗和无障碍权限:代码中虽有特殊窗口类型和透明 Activity 分支,但能否绕过权限取决于系统,不能把窗口类型编号当成通用绕过方案;
- 用完即卸:这是最有效的。应用宝不是系统组件,卸了它一样能装 App;
- 如果必须用:装完 → 设置里关掉「全部权限」→ 断网使用(它的条件引擎依赖远程配置,断网后触发条件会大量失效);
- 识别透明 Activity:看到「突然弹出来的、没有应用图标来源的窗口」,长按或下拉通知栏查”最近弹出的应用”——多半就是应用宝。
本文所有代码引用均可从
jadx_out/sources/与app_decoded/逐条复检。请勿将文中技术用于恶意用途——分析的目的,是让你知道手机里那些”你以为只是商店”的 App,到底能做什么。