拒绝重复进程!Android开发者必须掌握的单实例设计全攻略

"面对系统多任务机制带来的多实例问题,本文提供从标准API到系统级hack的渐进式解决方案,帮你构建坚如磐石的单实例应用!"

针对 Android 应用的单实例需求,这里提供分层解决方案:


一、基础方案(推荐优先实现)

1. 在入口 Activity 声明单任务栈

xml 复制代码
<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"
    android:taskAffinity="你的包名" <!-- 关键:强制绑定到应用专属任务栈 -->
    android:clearTaskOnLaunch="true"/> <!-- 可选:每次启动清除非根Activity -->

2. 全局实例检测(Application 级)

kotlin 复制代码
class MyApp : Application() {
    private var isRunning = false

    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                if (activity is MainActivity) {
                    if (isRunning) {
                        activity.finish()
                        return
                    }
                    isRunning = true
                }
            }

            override fun onActivityDestroyed(activity: Activity) {
                if (activity is MainActivity) {
                    isRunning = false
                }
            }
            // 其他生命周期方法留空实现
        })
    }
}

二、增强方案(应对特殊启动方式)

1. 防止多实例穿透

kotlin 复制代码
abstract class SingleInstanceActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        if (!isTaskRoot) { // 检测是否处于任务栈底部
            if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
                // 处理从桌面图标重复点击启动
                finish()
                startActivity(Intent.makeMainActivity(componentName))
                return
            }
            finish()
            return
        }
        super.onCreate(savedInstanceState)
    }
}

2. 强制前台唤醒

kotlin 复制代码
fun bringToFrontIfRunning(context: Context) {
    val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
    val tasks = activityManager.getRunningTasks(100)
    
    tasks.firstOrNull { it.baseActivity?.packageName == context.packageName }?.let {
        val intent = Intent(context, MainActivity::class.java).apply {
            addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }
        context.startActivity(intent)
    }
}

三、终极方案(跨进程锁)

适用于需要绝对单实例的场景(需注意性能损耗)

1. 文件锁实现

kotlin 复制代码
private const val LOCK_FILE = "single_instance.lock"

fun acquireAppLock(context: Context): Boolean {
    return try {
        val lockFile = File(context.filesDir, LOCK_FILE)
        val channel = RandomAccessFile(lockFile, "rw").channel
        val lock = channel.tryLock()
        lock != null && lock.isValid
    } catch (e: Exception) {
        false
    }
}

2. 结合 IPC 通信

kotlin 复制代码
// 在 Application 中
private val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        // 通过 binder 判断是否已有实例
    }

    override fun onServiceDisconnected(name: ComponentName?) {}
}

fun checkExistingInstance() {
    val bindResult = bindService(
        Intent(this, InstanceCheckService::class.java),
        serviceConnection,
        Context.BIND_ABOVE_CLIENT
    )
    if (!bindResult) {
        // 无已有实例
    }
}

四、特别场景处理

1. 多窗口模式适配

xml 复制代码
<activity android:name=".MainActivity"
    android:resizeableActivity="false" <!-- 禁用分屏 -->
    android:supportsPictureInPicture="false"/> <!-- 禁用画中画 -->

2. 通知栏点击处理

ini 复制代码
val pendingIntent = PendingIntent.getActivity(
    context, 
    0, 
    Intent(context, MainActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
    },
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

五、验证方法

  1. 通过 ADB 模拟多启动:
sql 复制代码
adb shell am start -n your.package.name/.MainActivity
adb shell am start -a android.intent.action.VIEW -d "your://deep/link"
  1. 使用开发者选项中的「不保留活动」进行压力测试
  2. 监控 Activity 生命周期:
go 复制代码
adb shell dumpsys activity activities | grep your.package.name

根据实际业务复杂度选择方案层级,建议优先采用基础方案 + 增强方案组合实现,既能覆盖大部分场景,又不会引入过度设计。

相关推荐
Starry-sky(jing)8 小时前
Vidda ADB 开启:R Pro 65 找不到开发者选项的处理与连接验收
android·adb·远程控制·电视·vidda
雨白8 小时前
从原理到实践:使用 Kotlin KSP 仿写 ButterKnifeLite
android·kotlin
疯华整猫9 小时前
把二十七个 init 从首帧前面搬走:用 Trae 把启动优化钉进 CI
android·性能优化·ai编程
传奇开心果10 小时前
【探秘 Jetpack Compose 框架深水区】 第8课:rememberSaveable 的边界在哪里?——Bundle、Saver 与什么值得跨进程存活
android·kotlin
Frankie_stand11 小时前
测试自动化实战指南:从脚本化、自动判据到 CI 集成的完整落地路径
android·ci/cd·自动化
恋猫de小郭12 小时前
Flutter 大小核优化又被回滚,然后又开新提案?
android·前端·flutter
Kapaseker12 小时前
领导问我 Kotlin 的 Nothing 是什么?
android·kotlin
韩曙亮13 小时前
【错误记录】安卓 adb 调试 5037 端口占用排查 ( adb 的三层结构 | 找出本机占用 5037 的进程 )
android·数据库·adb·安卓·错误记录
三少爷的鞋14 小时前
不改变异步本质,只改变代码组织:Telegram 项目的 Kotlin 协程实践
android
好的佩奇1 天前
蓝牙之广播与扫描
android·flutter·蓝牙