date: 2025-09-30 tags:
- Android
- 架构
- 启动优化
- AppWidget
- AIDL
- 多证书签名
- Key Rotation
面向客户端与平台工程团队的实施手册:启动路由与“已启动跳转”、服务型应用(无图标)、App Widget 托管的“近似内嵌”体验,以及多证书签名与密钥轮换流水线。
1)启动路由:RouterActivity + SplashScreen
Manifest(仅一个入口)
<activity
android:name=".RouterActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/Theme.Router.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp"/>
</intent-filter>
</activity>Kotlin
class RouterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() // 仅影响启动期
super.onCreate(savedInstanceState)
route(intent)
}
override fun onNewIntent(i: Intent) { super.onNewIntent(i); setIntent(i); route(i) }
private fun route(i: Intent) {
val loggedIn = /* 读取登录态 */
val route = i.getStringExtra("route") ?: i.data?.host
val target = when {
!loggedIn -> Intent(this, LoginActivity::class.java)
route == "chat" -> Intent(this, ChatActivity::class.java)
.putExtra("convId", i.getStringExtra("convId") ?: i.data?.getQueryParameter("convId"))
else -> Intent(this, HomeActivity::class.java)
}
target.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(target); finish()
}
}要点
- 已启动时跳转:靠
onNewIntent();系统 Splash 仅在启动期生效。 - 避免白屏:入口主题使用 NoDisplay/透明预览禁用。
- 从通知/外链重置返回栈:
NEW_TASK | CLEAR_TASK。
2)无图标“服务型”应用(只提供能力,不出现在启动器)
Manifest
<service
android:name=".ApiService"
android:exported="true"
android:permission="com.example.permission.USE_MY_SERVICE">
<intent-filter>
<action android:name="com.example.MY_SERVICE"/>
</intent-filter>
</service>
<permission
android:name="com.example.permission.USE_MY_SERVICE"
android:protectionLevel="signature"/>Service
class ApiService : Service() {
private val binder = object : IMyApi.Stub() {
override fun ping(): String = "pong"
}
override fun onBind(intent: Intent): IBinder = binder
}注意
- 不声明
MAIN/LAUNCHER的 Activity ⇒ 启动器无图标。 - Android 12+ 所有带
intent-filter的组件必须显式android:exported。 - 建议使用 signature 级权限 与显式绑定(包名+类名);多应用协作时最稳。
3)“把别的 App 页面嵌进来”的现实替代:App Widget 托管
为何选择 App Widget
- 公开 API 支持的跨进程 UI方式;无需共享代码;托管方将对方的 RemoteViews 以
AppWidgetHostView形式嵌入到自身页面。 - 限制:控件集受限、动画/手势有限、不可直接长文本输入;但按钮、开关、列表、跳转足够覆盖多数“嵌卡片”需求。
提供方(Widget App)
AppWidgetProvider+ 多尺寸布局(小/中/大);- 列表用
RemoteViewsService+RemoteViewsFactory; - 点击交互:
setOnClickPendingIntent(),或 Android 12+ 的响应 API。
托管方(主 App)
val appWidgetId = appWidgetHost.allocateAppWidgetId()
val ok = appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, providerComponent)
if (!ok) {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerComponent)
}
startActivityForResult(intent, REQ_BIND) // 一次授权
}
val view = appWidgetHost.createView(this, appWidgetId, appWidgetManager.getAppWidgetInfo(appWidgetId))
container.addView(view)体验优化
- 刷新策略事件驱动(数据变更时主动
updateAppWidget或notifyAppWidgetViewDataChanged),避免固定updatePeriodMillis。 - 图片统一缩放与缓存,避免大 Bitmap 传输;深色/动态配色(Material You)与主 App 视觉对齐。
4)一个 APK 多证书签名 & 密钥轮换
目标:同一 APK 同时由多把证书签(多签),或平滑更换证书(轮换)。
多签(multi-signer)
步骤:构建未签名 APK → apksigner 添加多个签名者。
apksigner sign \
--ks ks1.jks --ks-key-alias alias1 --ks-pass pass:*** --key-pass pass:*** \
--next-signer \
--ks ks2.jks --ks-key-alias alias2 --ks-pass pass:*** --key-pass pass:*** \
--v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \
app-release-unsigned.apk
apksigner verify --print-certs --verbose app-release-unsigned.apkGradle 后处理
tasks.register("addSecondSigner", Exec) {
dependsOn("assembleRelease")
doFirst {
def apk = file("$buildDir/outputs/apk/release/app-release.apk")
commandLine(
"${android.sdkDirectory}/build-tools/34.0.0/apksigner", "sign",
"--next-signer",
"--ks", file("keystores/ks2.jks"), "--ks-key-alias", "alias2",
"--ks-pass", "pass:***", "--key-pass", "pass:***",
"--v1-signing-enabled", "true", "--v2-signing-enabled", "true", "--v3-signing-enabled", "true",
apk.absolutePath
)
}
}注意
- 确保 v1 与 v2/v3 使用相同的签名者集合(兼容老系统);
- 后续版本保持相同集合,或使用 v3 的轮换机制。
密钥轮换(Key Rotation / v3 lineage)
- 使用 v3 签名谱系(
lineage.bin)声明新旧钥的承接关系; - 过渡期可同时含旧/新证书签名,之后仅用新证书 + 同一 lineage;
- 若使用 Google Play App Signing(AAB):最终安装包由 Play 的密钥签名,多签策略改为按 Play 的密钥升级流程执行。
5)实践清单
- Router 入口
singleTask+onNewIntent()处理已启动跳转; - Splash 仅限启动期;NoDisplay 主题避免白屏;
- 无图标服务:无
MAIN/LAUNCHER,组件声明exported,signature 权限隔离; - App Widget:事件驱动刷新、列表用 RemoteViewsService、点击深链回主 App;
- 多签:统一 v1/v2/v3 的签名者集合;轮换使用 v3 lineage;
- Play 分发:遵循 App Signing 密钥升级流程,而非本地多签。