Android安装后首次启动MainActivity的onCreate()为何被调用两次?
应用的启动页Activity(MainActivity)的onCreate()方法会被调用两次,但仅在安装后首次启动应用时出现(从Google Play内部应用分享链接安装或安装签名APK文件到设备)。此行为会导致同时创建多个MainActivity实例。
描述
已全面检查代码,确认不存在意外调用startActivity或任何会导致onCreate()被多次调用的显式代码。查看过相关旧bug报告,但该bug似乎已过时。
观察结果
- 仅在安装后首次启动应用时出现此问题
- 后续启动应用时不会出现该行为,
onCreate()仅按预期调用一次 - 问题与安装后的首次启动强相关
- 测试设备为Android 13版本
相关代码
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xyz"> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Required to maintain app compatibility. --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <application android:name=".framework.presentation.BaseApplication" android:allowBackup="false" android:fullBackupOnly="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <meta-data android:name="firebase_crashlytics_collection_enabled" android:value="${enableCrashReporting}" /> <activity android:name="xyz.framework.presentation.MainActivity" android:windowSoftInputMode="adjustResize" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="xyz.framework.presentation.main.RecipeActivity" android:windowSoftInputMode="adjustResize" android:exported="true" /> <activity android:name="com.facebook.FacebookActivity" android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" android:theme="@style/com_facebook_activity_theme" android:exported="false" /> <activity android:name="com.facebook.CustomTabMainActivity" android:exported="false" /> <activity android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity" android:theme="@android:style/Theme" android:exported="false"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> </activity> <activity android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity" android:theme="@android:style/Theme" android:exported="false"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> </activity> <activity android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyFloatingActivity" android:theme="@android:style/Theme.Dialog" android:exported="false"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> </activity> </application> </manifest>
BaseApplication
@FlowPreview @ExperimentalCoroutinesApi @HiltAndroidApp open class BaseApplication : Application()
MainActivity
@AndroidEntryPoint class MainActivity : AppCompatActivity(), UIController { @Inject lateinit var editor: SharedPreferences.Editor @Inject lateinit var sharedPreferences: SharedPreferences @Inject lateinit var viewModelFactory: ViewModelProvider.Factory private val viewModel: SplashViewModel by viewModels { viewModelFactory } private val signInLauncher = registerForActivityResult( FirebaseAuthUIActivityResultContract() ) { res -> this.onSignInResult(res) } @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Toast.makeText(this, "oncreate called", Toast.LENGTH_SHORT).show() setContent { SplashProvider() } viewModel.hasSyncBeenExecuted() .observe(this) { hasSyncBeenExecuted -> if (hasSyncBeenExecuted) { startActivity(Intent(this, RecipeActivity::class.java)) // Use lifecycleScope for the coroutine lifecycleScope.launch { delay(2000) // Adjust the delay time as needed finish() } } } if (sharedPreferences?.getString( PreferenceKeys.USER_UID, null ) != null && FirebaseAuth.getInstance().currentUser != null ) { viewModel.syncCacheWithNetwork() } else { createSignInIntent() } } private fun createSignInIntent() { // [START auth_fui_create_intent] // Choose authentication providers val providers = arrayListOf( AuthUI.IdpConfig.EmailBuilder().build(), AuthUI.IdpConfig.GoogleBuilder().build() ) Toast.makeText(this, "createSignInIntent called", Toast.LENGTH_SHORT).show() // Create and launch sign-in intent val signInIntent = AuthUI.getInstance() .createSignInIntentBuilder() .setAvailableProviders(providers) .setTheme(R.style.LoginTheme) .setLogo(R.drawable.handshake) // Set logo drawable .build() signInLauncher.launch(signInIntent) // [END auth_fui_create_intent] } override fun hideSoftKeyboard() { if (currentFocus != null) { val inputMethodManager = getSystemService( Context.INPUT_METHOD_SERVICE ) as InputMethodManager inputMethodManager .hideSoftInputFromWindow(currentFocus!!.windowToken, 0) } } private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) { val response = result.idpResponse if (result.resultCode == AppCompatActivity.RESULT_OK) { // Successfully signed in val user = FirebaseAuth.getInstance().currentUser // ... editor?.putString(PreferenceKeys.USER_UID, user?.uid) editor?.apply() viewModel.syncCacheWithNetwork() } else if (response != null) { Toast.makeText( this, response.error?.errorCode.toString().plus(response.error?.message), Toast.LENGTH_LONG ).show() } else { finish() } } }
SplashProvider
@Composable fun SplashProvider( ) { val images = listOf(R.drawable.splash1, R.drawable.splash2, R.drawable.splash3) val imageIndex = Random.nextInt(images.size) Splash(images[imageIndex]) } @Composable fun Splash(img: Int) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Image( contentDescription = "Recipe", painter = painterResource(img), modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) Column( modifier = Modifier .wrapContentSize() .clip(CircleShape) .background(Color.Black.copy(alpha = 0.6f)) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator( modifier = Modifier.size(50.dp), strokeWidth = 4.dp, color = Color.White ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Loading...", color = Color.White, fontSize = 16.sp ) } } }
1. 限制Activity启动模式
在AndroidManifest.xml中给MainActivity添加launchMode="singleTop",确保同类型实例不会重复创建:
<activity android:name="xyz.framework.presentation.MainActivity" android:windowSoftInputMode="adjustResize" android:exported="true" android:launchMode="singleTop"> <!-- intent-filter 保持不变 --> </activity>
singleTop模式下,当Activity处于任务栈顶部时,系统会复用现有实例而非创建新的,适配启动页的场景需求。
2. 拦截重复启动的Intent
在MainActivity的onCreate()方法开头添加判断,若当前实例不是任务栈根且是从桌面启动的,直接销毁避免重复:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 拦截重复启动的实例 if (!isTaskRoot) { val intent = intent if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN == intent.action) { finish() return } } // 原有业务代码 }
安装后首次启动时,系统可能因安装完成的广播和用户点击图标发送两个启动Intent,这段代码可以过滤掉重复的实例。
3. 优化页面跳转逻辑
移除startActivity后的延迟finish()逻辑,直接跳转后销毁当前Activity,避免实例堆积:
if (hasSyncBeenExecuted) { startActivity(Intent(this, RecipeActivity::class.java)) finish() }
延迟销毁没有实际意义,反而会导致原Activity未销毁时新实例被创建。
4. 调整Auth启动时机
将Firebase Auth的启动逻辑移到onResume()中,确保Activity完全初始化后再执行,避免干扰系统启动流程:
private var isFirstResume = true override fun onResume() { super.onResume() if (isFirstResume) { isFirstResume = false if (sharedPreferences?.getString(PreferenceKeys.USER_UID, null) != null && FirebaseAuth.getInstance().currentUser != null) { viewModel.syncCacheWithNetwork() } else { createSignInIntent() } } }
同时删除onCreate()中对应的判断代码,减少Activity创建阶段的异步操作。
5. 移除测试组件
正式包中移除androidx.test.core.app相关的Activity,测试组件可能干扰系统的启动逻辑,建议通过构建变体(productFlavors)或构建类型(buildType)在正式包中排除这些代码。
内容的提问来源于stack exchange,提问作者Android Developer

