背景
之前面试一些校招同学,聊到微信小程序是什么launchMode,其任务栈是如何实现的?很多同学只提到singleInstance,这是不合适的。
今天我们就猜测并解析一下微信主程序与小程序的关系与大致实现,最后给出源码,可以给大家作一个简单参考。
初探
既然要研究微信,那么我们就先打开几个小程序,再用adb命令看看任务栈信息。
在终端使用 adb shell dumpsys activity activities 命令后,可以找到最近任务列表的Activity信息:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | Running activities (most recent first):TaskRecord{caccd90 #3239 A=.AppBrandUI3 U=0 StackId=1 sz=1}
 Run #4: ActivityRecord{bb162b8 u0 com.tencent.mm/.plugin.appbrand.ui.AppBrandUI3 t3239}
 TaskRecord{d6c62d6 #3190 A=com.tencent.mm U=0 StackId=1 sz=1}
 Run #3: ActivityRecord{7f2d805 u0 com.tencent.mm/.ui.LauncherUI t3190}
 TaskRecord{34a386a #3238 A=.AppBrandUI2 U=0 StackId=1 sz=1}
 Run #2: ActivityRecord{16cfede u0 com.tencent.mm/.plugin.appbrand.ui.AppBrandUI2 t3238}
 TaskRecord{7ade2d1 #3237 A=.AppBrandUI U=0 StackId=1 sz=1}
 Run #1: ActivityRecord{ccfd8ae u0 com.tencent.mm/.plugin.appbrand.ui.AppBrandUI t3237}
 ...
 
 | 
可以发现这里的#3是微信主Activity,4、2、1都是我开的小程序,且位于不同的任务栈中,Activity名称都是AppBrandUI+数字的形式。
然后再看看其他关键信息(这里我单独筛出来):
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | packageName=com.tencent.mm processName=com.tencent.mmtaskAffinity=com.tencent.mm
 
 packageName=com.tencent.mm processName=com.tencent.mm:appbrand3
 taskAffinity=.AppBrandUI3
 
 packageName=com.tencent.mm processName=com.tencent.mm:appbrand2
 taskAffinity=.AppBrandUI2
 
 packageName=com.tencent.mm processName=com.tencent.mm:appbrand
 taskAffinity=.AppBrandUI
 
 | 
很简单,和我们平时实现多进程差不多,说明是给Activity设置了process属性。
思考
转念一想,小程序那么多,难道这些不同后缀的Activity都写死在代码里吗?
显然不能这么干,只能两种途径可以达成目的:
- 不在Manifest里面静态注册Activity,使用类似Hook的方式动态创建进程和Activity
- 动静结合,设计一个Activity池,在本地写死有限数量的Activity,通过复用的方式承载小程序
对于第一种,我查阅了一些资料,理论上讲是可以做到的,涉及到NDK开发,需要我们对AMS的源码很熟悉,不走常规流程启动Activity,且小程序是多进程的,可能还需要手动fork进程。
这种方式显然具有较大的风险,属于黑科技范畴,而且谷歌官方是不推荐的,微信作为十几亿用户的常驻App,几乎不太可能使用这一方案。
那么只剩第二种了,预先在本地写死n个一样的Activity(当然也可以通过继承形式),同时在Manifest中注册好。
然后打开一个小程序就占用一个Activity,当打开第n+1个小程序时,覆盖第1个小程序所在的Activity,这样就相当于第1个小程序被顶掉了。
分析到此,就很明显了,如果真的是第二种方案,那么小程序就不能无限数量地打开咯?果断打开微信试了一下,果然,最多只能开5个!当你启动第6个小程序时,第1个就被销毁了。
其实这也是符合我们上述预期的,每个小程序的进程不一样,taskAffinity也不一样,类名也不一样。原生API是不支持动态设置taskAffinity和进程名的。
简单实现
小程序所在的Activity:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | public class SmallActivity extends AppCompatActivity {
 public static class Small0 extends SmallActivity {}
 public static class Small1 extends SmallActivity {}
 public static class Small2 extends SmallActivity {}
 public static class Small3 extends SmallActivity {}
 public static class Small4 extends SmallActivity {}
 
 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_small);
 
 
 
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 int iconRes = 0;
 setTaskDescription(new ActivityManager.TaskDescription("小程序名", iconRes));
 } else {
 Bitmap iconBmp = null;
 setTaskDescription(new ActivityManager.TaskDescription("小程序名", iconBmp));
 }
 }
 }
 
 | 
Manifest:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 
 | <?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 package="com.ysy.smallapp">
 
 <application
 ...>
 
 <activity android:name=".MainActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 
 <activity
 android:name=".SmallActivity$Small0"
 android:label="Small0"
 android:launchMode="singleTask"
 android:process=":Small0"
 android:taskAffinity=".Small0" />
 
 <activity
 android:name=".SmallActivity$Small1"
 android:label="Small1"
 android:launchMode="singleTask"
 android:process=":Small1"
 android:taskAffinity=".Small1" />
 
 <activity
 android:name=".SmallActivity$Small2"
 android:label="Small2"
 android:launchMode="singleTask"
 android:process=":Small2"
 android:taskAffinity=".Small2" />
 
 <activity
 android:name=".SmallActivity$Small3"
 android:label="Small3"
 android:launchMode="singleTask"
 android:process=":Small3"
 android:taskAffinity=".Small3" />
 
 <activity
 android:name=".SmallActivity$Small4"
 android:label="Small4"
 android:launchMode="singleTask"
 android:process=":Small4"
 android:taskAffinity=".Small4" />
 
 </application>
 
 </manifest>
 
 | 
具体的复用逻辑这里暂时就这样简单地实现了,实际情况肯定比此复杂:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | class MainActivity : AppCompatActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 
 val edtText = findViewById<EditText>(R.id.edt_main)
 
 findViewById<View>(R.id.btn_main).setOnClickListener {
 startActivity(Intent().apply {
 val id = edtText.text.toString().toInt() % 5
 setClassName(this@MainActivity, "com.ysy.smallapp.SmallActivity\$Small$id")
 })
 }
 }
 }
 
 | 
完整源码:
https://github.com/ysy950803/SmallApp