应用宝(MediaRouteProviderService)媒体路由保活思路研究
现象
gms通过android.media.MediaRouteProviderService拉活应用宝的现象
日志
07-29 18:00:55.625 1442 2258 D IntentFirewall: service:Block = false intent = Intent { act=android.media.MediaRouteProviderService cmp=com.tencent.android.qqdownloader/com.tencent.assistant.syscomponent.MediaRouteProviderService } callerUid = 10152 callerPid = 2330 callerPackage = com.google.android.gms calledPackage = com.tencent.android.qqdownloader calledUid = 10265
07-29 18:00:56.207 1442 1496 D IntentFirewall: provider:Block = false intent is NULL! callerUid = 10066 callerPid = -1 callerPackage = com.android.providers.blockednumber calledPackage = com.tencent.android.qqdownloader calledUid = 10265
07-29 18:00:56.682 1442 2314 D IntentFirewall: provider:Block = false intent is NULL! callerUid = 10265 callerPid = -1 callerPackage = com.tencent.android.qqdownloader calledPackage = com.tencent.android.qqdownloader calledUid = 10265
07-29 18:00:56.717 1442 1918 D IntentFirewall: provider:Block = false intent is NULL! callerUid = 10265 callerPid = -1 callerPackage = com.tencent.android.qqdownloader calledPackage = com.tencent.mm calledUid = 10283
07-29 18:00:57.388 1442 2155 D IntentFirewall: service:Block = false intent = Intent { cmp=com.tencent.android.qqdownloader/com.tencent.assistant.daemon.CoreService } callerUid = 10265 callerPid = 5625 callerPackage = com.tencent.android.qqdownloader calledPackage = com.tencent.android.qqdownloader calledUid = 10265
系统自启动的拦截方案
方案1:动态拦截
原则是不影响兼容性或用户体验的条件下,对非可感知的后台启动行为可以进行拦截
判断启动方和被启动方的应用标签状态,如果都非可见且被启动方(应用宝)也没有被fork,但是由于caller是系统进程想要冷启动第三方App,还是需要慎重判断拦截(例如加一些灭屏条件或剔除gms类型拉起或Media路由非真实被使用等更精细化条件辅助判断是否拦截)
方案2:rule拦截
如下AOSP配置就行,可以简单包名也可以精确到called,caller,action和component,从配置文件我们也可以看出来,手机厂商拦截应用后台自启动,真的可以很随心所欲啊。第三方应用好不容易辛苦研究了一个保活技术,直接就被厂商配置文件给拦截了。这也就是后面app工程师,不怎么想研究保活技术的原因了哈。
<service block="true" called="com.tencent.android.qqdownloader" caller="com.google.android.gms"
interaction="*" log="true">
<intent-filter>
<action name="android.media.MediaRouteProviderService" />
</intent-filter>
<component-filter
name="com.tencent.android.qqdownloader/com.tencent.assistant.syscomponent.MediaRouteProviderService" />
</service>
<service block="true" called="com.tencent.android.qqdownloader" caller="*"
interaction="*" log="true">
<intent-filter>
<action name="android.media.MediaRoute2ProviderService" />
</intent-filter>
<component-filter
name="com.tencent.android.qqdownloader/com.tencent.assistant.syscomponent.MediaRouteProviderService2" />
</service>
这种配置文件拦截的,可能是一些大厂的作风为主。因为这样不破坏整体拦截策略稳定性。即为了交付进行拦截
应用宝保活思路研究
应用宝通过以下策略增强保活效果:
高频绑定:媒体路由发现是周期性行为(如设备网络变化时),触发多次绑定。
多进程守护::live 进程与主进程分离,避免全量被杀。
系统白名单:媒体路由服务属于系统功能依赖,绑定请求通常不受后台限制
1. 应用宝如何使用SDK拉活自己呢
反编译一下应用宝原理就知道了哈
1. 清单文件注册(AndroidManifest.xml)
应用宝需在清单文件中声明自定义的 MediaRouteProviderService/MediaRoute2ProviderService,并注册系统标准的媒体路由 Action:
<service
android:name="com.tencent.assistant.syscomponent.MediaRoute2ProviderService"
android:label="@string/media_route_service_name"
android:process=":live" <!-- 使用独立进程保活 -->
android:exported="true"> <!-- 允许系统绑定 -->
<intent-filter>
<action android:name="android.media.MediaRouteProviderService" />
</intent-filter>
</service>
<service android:name="com.tencent.assistant.syscomponent.MediaRouteProviderService2" android:enabled="true" android:exported="true" android:process=":live">
<intent-filter>
<action android:name="android.media.MediaRoute2ProviderService"/>
</intent-filter>
</service>
保活关键点:
1.android.media.MediaRouteProviderService 是系统媒体路由器框架的固定 Action,用于发现服务
2.:live 进程后缀用于隔离并提高优先级,避免主进程被杀时连带失效
2. 服务实现类
应用宝其实也没做啥事情哈
package com.tencent.assistant.syscomponent;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import com.tencent.assistant.Settings;
import com.tencent.assistant.utils.SysComponentHelper;
/* compiled from: ProGuard */
/* loaded from: classes2.dex */
public class MediaRouteProviderService2 extends BaseSysComponentService {
@Override // com.tencent.assistant.syscomponent.BaseSysComponentService
public SysComponentHelper.SysComponentType a() {
return SysComponentHelper.SysComponentType.MediaRouteProviderService2;
}
@Override // com.tencent.assistant.syscomponent.BaseSysComponentService, android.app.Service
@Nullable
public IBinder onBind(Intent intent) {
Settings.get().setAsync("key_live_media_service_on_bind", Long.valueOf(SystemClock.elapsedRealtime()));
SysComponentHelper.e(SysComponentHelper.SysComponentType.MediaRouteProviderService2, "service_bind");
return new Binder();
}
@Override // com.tencent.assistant.syscomponent.BaseSysComponentService, android.app.Service
public void onCreate() {
super.onCreate();
}
}
Google Play 服务(com.google.android.gms)会主动绑定此服务以发现投屏设备,从而拉活应用宝进程。
2. 系统侧的冷启动堆栈
本质还是bindService哈,MediaRoute2ProviderServiceProxy的bindService冷启动了应用宝
01-26 03:00:16.621 7461 7461 I ActivityManager: startProcessLocked com.tencent.android.qqdownloader:live
01-26 03:00:16.621 7461 7461 I ActivityManager: java.lang.Throwable
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ProcessList.startProcessLocked(ProcessList.java:1697)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ProcessList.startProcessLocked(ProcessList.java:2418)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ProcessList.startProcessLocked(ProcessList.java:2558)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:2858)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ActiveServices.bringUpServiceLocked(ActiveServices.java:4278)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ActiveServices.bindServiceLocked(ActiveServices.java:2956)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ActivityManagerService.bindServiceInstance(ActivityManagerService.java:12782)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.am.ActivityManagerService.bindServiceInstance(ActivityManagerService.java:12729)
01-26 03:00:16.621 7461 7461 I ActivityManager: at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:2035)
01-26 03:00:16.621 7461 7461 I ActivityManager: at android.app.ContextImpl.bindServiceAsUser(ContextImpl.java:1974)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.media.MediaRoute2ProviderServiceProxy.bind(MediaRoute2ProviderServiceProxy.java:243)
01-26 03:00:16.621 7461 7461 I ActivityManager: at com.android.server.media.MediaRoute2ProviderServiceProxy.onBindingDied(MediaRoute2ProviderServiceProxy.java:312)
01-26 03:00:16.621 7461 7461 I ActivityManager: at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:2184)
01-26 03:00:16.621 7461 7461 I ActivityManager: at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:2221)
典型调用链:
MediaRouter2 → MediaRoute2ProviderServiceProxy.bind() → 目标服务进程启动 → getControllerName() 获取展示名称
/**
* 媒体路由提供者服务的代理类,用于绑定和管理实际的 MediaRoute2ProviderService 服务。
* 通过 Binder 机制与系统媒体路由器框架交互,负责路由发现和会话控制。
*
* @see MediaRoute2ProviderService 系统媒体路由服务的基类
*/
final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
// 系统定义的媒体路由服务接口 Action
public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService";
/**
* 绑定目标媒体路由提供者服务。
* 绑定成功后,通过 mServiceConnection 回调处理服务连接状态。
* 绑定优先级为前台服务(BIND_FOREGROUND_SERVICE),确保进程不被轻易回收。
*
* @throws SecurityException 若缺少绑定权限或目标服务未声明导出
*/
private void bind() {
if (!mBound) {
if (DEBUG) {
Slog.d(TAG, this + ": Binding");
}
// 构建指向目标服务的 Intent,使用系统预定义 Action
Intent service = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
service.setComponent(mComponentName);
try {
// 绑定服务并设置自动创建和前台优先级标志
mBound = mContext.bindServiceAsUser(
service,
mServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
new UserHandle(mUserId));
if (!mBound && DEBUG) {
Slog.d(TAG, this + ": Bind failed");
}
} catch (SecurityException ex) {
// 捕获权限异常(如目标服务未声明 exported=true)
if (DEBUG) {
Slog.d(TAG, this + ": Bind failed", ex);
}
}
}
}
}
/**
* 媒体会话管理器,负责处理媒体控制器(MediaController)的元数据获取和展示逻辑。
* 包含从应用或服务中提取显示名称的功能。
*/
public class MediaSessions {
/**
* 获取媒体控制器的显示名称,优先从 MediaRouteProviderService 的服务标签中获取,
* 若未找到则回退到应用标签,最后返回包名。
*
* @param controller 目标媒体控制器实例
* @return 控制器友好名称(如“应用宝投屏”),默认返回包名
*/
protected String getControllerName(MediaController controller) {
final PackageManager pm = mContext.getPackageManager();
final String pkg = controller.getPackageName();
try {
// 优先从 MediaRouteProviderService 的服务标签获取名称
if (USE_SERVICE_LABEL) {
// 查询所有注册了 MediaRouteProviderService 的服务
final List<ResolveInfo> ris = pm.queryIntentServices(
new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0);
if (ris != null) {
for (ResolveInfo ri : ris) {
if (ri.serviceInfo == null) continue;
// 确保服务属于当前包
if (pkg.equals(ri.serviceInfo.packageName)) {
final String serviceLabel =
Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim();
if (serviceLabel.length() > 0) {
return serviceLabel; // 返回服务标签(如“应用宝投屏”)
}
}
}
}
}
// 回退到应用标签
final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim();
if (appLabel.length() > 0) {
return appLabel; // 返回应用名称(如“应用宝”)
}
} catch (NameNotFoundException e) {
// 忽略异常,返回包名
}
return pkg; // 默认返回包名(如“com.tencent.android.qqdownloader”)
}
}
MediaRoute2ProviderService:系统级服务,用于发布和管理媒体路由(如 Chromecast 设备),绑定后触发进程拉活。
进程保活:应用宝通过声明该服务并设置独立进程(:live),借助 Google Play 服务的绑定请求实现自启动
3. 那么媒体路由保活方案有用吗?
答:肯定是有用的,不然应用宝也不会搞这个保活技术。那为啥有些手机又没用呢?因为啊华米OV等一些大厂都有自己的自启动模块或功耗开发工程师啊,很多年前啊,他们在大数据中或用户反馈中发现了这一后台启动行为,进行了拦截处理哈。但是啊,我们手机其实不止华米OV,总有一些非主流的牌子,不一定有对应拦截媒体路由保活的策略哈。反正看着选择就行哈。但是其实应用后台保活的趋势不强了,尽量还是按需来就行了。能不能常驻后台运行,用户决定就行了。当然这块我们程序员决定不了,看公司商业良心了。