前段时间,突然有朋友询问,自己写的SingleInstance Activity在按home键的时分被毁掉了,刚听到这个问题的时分,我直觉怀疑是Activity在onPause或者onStop中发生了Crash导致闪退了,可是装置apk检查现象,没有发现异常日志,这究竟是怎么回事呢?编写测试Demo来详细探究下

Demo代码阐明

Demo日志很简单,包含MainActivity和SingleInstanceActivity两个页面,在MainActivity中的TextView点击事情中发动SingleInstanceActivity,在SingleInstanceActivity中的TextView点击事情中调用moveTaskToBack(true)切回后台,随后在MainActivity界面按Home键返回桌面,就能够看到SingleInstanceActivity被毁掉了,示例代码如下所示:

// MainActivity.kt
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      MyApplicationTheme {
        // A surface container using the 'background' color from the theme
        Surface(
          modifier = Modifier
             .fillMaxSize()
             .clickable { onBtnClick() },
          color = MaterialTheme.colorScheme.background
         ) {
          Greeting("Android")
         }
       }
     }
   }
  fun onBtnClick() {
     startActivity(Intent(this,                 SingleInstanceActivity::class.java))
   }
}
​
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
  Text(
    text = "Hello $name!",
    modifier = modifier
   )
}
​
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
  MyApplicationTheme {
    Greeting("Android")
   }
}
// SingleInstanceActivity.java
public class SingleInstanceActivity extends ComponentActivity {
  private static final String TAG = "SingleInstanceActivity";
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  Log.d(TAG,"SingleInstanceActivity onCreate method called",new Exception());
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_single_instance);
  findViewById(R.id.move_back).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      moveTaskToBack(true);
     }
   });
  }
​
 @Override
 protected void onDestroy() {
  Log.d(TAG,"SingleInstanceActivity onDestroy method called",new Exception());
  super.onDestroy();
  }
}
<!-- AndroidManifest.xml文件中application节点的内容-->
<application
  android:allowBackup="true"
  android:dataExtractionRules="@xml/data_extraction_rules"
  android:fullBackupContent="@xml/backup_rules"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/Theme.MyApplication"
  tools:targetApi="31">
  <activity
    android:name=".SingleInstanceActivity"
    android:launchMode="singleInstance"
    android:exported="true" />
  <activity
    android:name=".MainActivity"
    android:exported="true"
    android:label="@string/app_name"
    android:theme="@style/Theme.MyApplication">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />
​
      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
</application>

调用栈回溯

即然SingleInstanceActivity被毁掉了,那么咱们只需求在Activity生命周期中增加日志,来看下onDestroyed函数是怎么驱动调用的即可,从Activity生命周期可知,在Framework中框架经过ClientLifecycleManager类来管理Activity的生命周期变化,在该类的scheduleTransaction函数中,Activity的每一种生命周期类型均被包装成一个ClientTransaction来处理,在该函数中增加日志,打印调用栈,即可确定是那个地方毁掉了SingleInstanceActivity,增加日志的代码如下:

按Home键时SingleInstance Activity销毁了???

随后编译framework.jar并push到设备上,检查日志,能够看到SingleInstanceActivity是在Task类的removeActivities办法中被毁掉的,日志如下:

按Home键时SingleInstance Activity销毁了???

依照如上的思路,逐步类推,增加日志,检查调用栈,咱们终究追溯到ActivityThread的handleResumeActivity,在该函数的最终,增加的IdlerHandler里面会执行RecentTasks的onActivityIdle办法,在该函数的调用流程里,会判别当时resume的Activity是不是桌面,是的话在HiddenTask不为空的情况下,就会执行removeUnreachableHiddenTasks的逻辑,毁掉SingleInstanceActivity(这儿的代码分支为android-13.0.0_r31)。

完整的正向调用流程如下图所示:

按Home键时SingleInstance Activity销毁了???

remove-hidden-task机制

前文中咱们已经盯梢到Activity毁掉的调用流程,那么为什么要毁掉SingleInstanceActivity呢?咱们继续看前文中的日志,能够看出Activity毁掉的原因是:remove-hidden-task。

按Home键时SingleInstance Activity销毁了???

那么这个remove-hidden-task到底是用来干嘛的呢?咱们来看下代码提交信息:

按Home键时SingleInstance Activity销毁了???

从代码提交阐明不难看出,这儿的意思是:当咱们向最近使命列表中增加一个使命时,会移除已不可达/未激活的Task,这儿咱们的SingleInstanceActivity所在的Task被判定为不可达/未激活状况,所以被这套机制移除了。

不可达/未激活的Task

那么为什么SingleInstanceActivity被认为是不可达的呢?咱们进一步追寻代码,能够看到RencentTasks.removeUnreachableHiddenTasks移除的是mHiddenTasks中的使命,代码如下:

按Home键时SingleInstance Activity销毁了???

这样咱们就只需求搞清楚什么样的Task会被参加mHiddenTasks中即可,mHiddenTasks.add的调用代码如下所示:

按Home键时SingleInstance Activity销毁了???

按Home键时SingleInstance Activity销毁了???

从上述代码可知,在removeForAddTask中经过findRemoveIndexForAddTask来查找当给定Task增加到最近使命列表时,需求被移除的Task,在findRemoveIndexForAddTask中最典型的一种场景就是当两个Task的TaskAffinity相一起,当后来的Task被增加到最近使命列表时,前一个Task会被毁掉,这也就意味着在SingleInstanceActivity按Home键,MainActivity也会被毁掉,经过实践,确实是这样。

解决方案

前文中已探讨了remove-hidden-task的运行机制,那么解决方案也就很简单了,给SingleInstanceActivity增加独立的TaskAffinity即可(留意:此刻SingleInstanceActivity会显现在最近使命中,假如不想显现,请指定android:excludeFromRecents=”true”)。

影响范围

经排查,Google Pixel记性从Android 12开始支撑该特性,针对国内定制厂商而言,大多数应该是在Android 13跟进的,大家能够测试看看。