SmallApp

Project Url: ysy950803/SmallApp
Introduction: 微信小程序任务栈实现原理。
More: Author   ReportBugs   
Tags:

之前面试一些校招同学,聊到微信小程序是什么 launchMode,其任务栈是如何实现的?很多同学只提到 singleInstance,这是不合适的。

今天我们就猜测并解析一下微信主程序与小程序的关系与大致实现,最后给出源码,可以给大家作一个简单参考。

初探

既然要研究微信,那么我们就先打开几个小程序,再用 adb 命令看看任务栈信息。

在终端使用 adb shell dumpsys activity activities 命令后,可以找到最近任务列表的 Activity 信息:

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+数字的形式。

然后再看看其他关键信息(这里我单独筛出来):

packageName=com.tencent.mm processName=com.tencent.mm
taskAffinity=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:

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);

        // 动态地给小程序 Activity 设置名称和图标,下面代码只是举例,实际信息肯定是动态获取的
        // 由于 iconRes 这个构造参数的 API 28 才加入的,所以建议区分版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            int iconRes = 0; // 这里应该是小程序图标的资源索引
            setTaskDescription(new ActivityManager.TaskDescription("小程序名", iconRes));
        } else {
            Bitmap iconBmp = null; // 这里应该是小程序图标的 bitmap
            setTaskDescription(new ActivityManager.TaskDescription("小程序名", iconBmp));
        }
    }
}

Manifest:

<?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>

具体的复用逻辑这里暂时就这样简单地实现了,实际情况肯定比此复杂:

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")
            })
        }
    }
}
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools