微信小程序分析

Published: 2023年06月04日

In Reverse.

小程序架构

它是一个多线程模型,小程序的核心功能由两个线程承载,即渲染层(View Thread)逻辑层(App Service Thread)

img

小程序的JS代码运行在逻辑层,这是一个纯JS环境,没有DOM/BOM对象,除了JS本身内置对象外只存在微信逻辑层注入的接口可用。小程序的页面视图会在渲染层里,这是一个完整的浏览器环境(WebView),然而这里面的JS是小程序开发者无法控制的,甚至可以说里面的DOM都不是开发者完全掌控的,开发者使用WXMLWXSS制作的页面会有开发者工具转换为JS再由渲染层的运行时渲染为DOM节点(类似VDOM),渲染层和逻辑层可通过Native通信,如逻辑层可通过数据形式有限的修改渲染层节点,渲染层也可以给逻辑层发送很多事件。渲染层相对好调试,但在分析小程序时逻辑层才是核心,下面列下各种环境的底层:

平台 渲染层 逻辑层
iOS WKWebView JavaScriptCore -
Android/WMPF xwalk(Chromium) V8 -
Windows CEF V8 -
macOS WKWebView JavaScriptCore -
开发者工具 NW.js NW.js (V8) 两个层其实都是webview,其实都是很完整的运行环境,但官方为了保持与真机一致用JS限制了逻辑层的能力(可越狱,无意义),逻辑层和渲染层通过WS通信

在通信时有两种情况:

1.Native->JS:Native向逻辑层上下文执行JS评估(V8为evaluateJavascript,JSC为evaluateJavaScript:completionHandler:),这里通常是给JS回调的结果

2.JS->Native:Native向JS注入全局对象(WeixinJSBridge),JS直接调用它的invokepublish接口即可,wx.xxx是对它的封装

更:在新版Android apk中WeixinJSBridge不再是被底层注入,下文会详细说明

源代码结构

典型源码布局如下:

my-mini-program/
├── app.js               // 小程序逻辑入口文件,用于初始化全局配置
├── app.json             // 项目的全局配置文件,如页面路径、窗口表现等
├── app.wxss             // 全局样式表,影响所有页面的样式
├── project.config.json  // 项目配置文件,包含开发者工具相关设置
├── pages/               // 存放各个页面的目录
│   ├── index/           // 首页
│   │   ├── index.wxml   // 页面结构文件
│   │   ├── index.wxss   // 页面样式表
│   │   ├── index.js     // 页面逻辑文件
│   │   └── index.json   // 页面配置文件(可选)
│   ├── logs/            // 日志页面
│   │   ├── logs.wxml
│   │   ├── logs.wxss
│   │   ├── logs.js
│   │   └── logs.json    // (可选)
│   └── ...              // 更多页面
├── utils/               // 工具类库或公共函数
│   └── util.js          // 提供一些常用的工具方法
├── images/              // 图片资源
│   └── logo.png         // 示例图片
└── components/          // 自定义组件
    └── my-component/    // 示例自定义组件
        ├── my-component.wxml
        ├── my-component.wxss
        ├── my-component.js
        └── my-component.json

这里面关键的是三类文件:WXML、WXSS、JS,他们分别对应于网页开发中的 HTMLCSSJavaScript。下面我将详细说明这三类文件的作用及其与 HTMLCSSJavaScript 的类比。

WXML (WeiXin Markup Language)

WXML 是微信小程序的页面结构描述语言,类似于 HTML。它定义了页面的布局和结构,包括各种标签、属性以及数据绑定等。如:

<view class="container">
  <text>{{message}}</text>
  <button bindtap="handleTap">点击我</button>
</view>

WXSS (WeiXin Style Sheets)

WXSS 是微信小程序的样式表语言,类似于 CSS。它用于定义页面的样式,包括颜色、字体、布局等。如:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20rpx;
}

button {
  background-color: #1aad19;
  color: white;
  border: none;
  padding: 10rpx 20rpx;
  border-radius: 5rpx;
}

JS (JavaScript)

JS文件是微信小程序的逻辑处理文件,类似于网页开发中的JavaScript`。它负责处理页面的数据逻辑、事件处理、网络请求等。如:

Page({
  data: {
    message: 'Hello, World!'
  },

  onLoad: function() {
    // 页面加载时执行的初始化操作
    console.log('Page loaded');
  },

  handleTap: function() {
    // 处理按钮点击事件
    wx.showToast({
      title: '你点击了按钮',
      icon: 'none'
    });
  }
});

注:要学习,可以去官网申请账号,但更简单的就是直接去用测试号,只需登录微信,之后到首页就是小程序发布流程一把梭

包结构

文件结构

现在常见的有两种后缀:wxvpkg和 wxapkg,它们使用完全相同的二进制格式,只是用途不同: - wxvpkg: 微信开发者工具核心包 (Vendor Package) - wxapkg: 微信小程序包 (Application Package)

它们的结构如下(Version 0):

┌─────────────────────────────────────────────────────────────┐
 Header (14 bytes)                                           
   ┌───────────────────────────────────────────────────────┐ 
    0x00: 0xBE (Magic Start)                               
    0x01-0x04: Version (4 bytes, Big-Endian)               
    0x05-0x08: Info Length (4 bytes, BE)                   
    0x09-0x0C: Data Length (4 bytes, BE)                   
    0x0D: 0xED (Magic End)                                 
   └───────────────────────────────────────────────────────┘ 
├─────────────────────────────────────────────────────────────┤
 File Count (4 bytes)                                        
   ┌───────────────────────────────────────────────────────┐ 
    0x0E-0x11: File Count (4 bytes, BE)                    
   └───────────────────────────────────────────────────────┘ 
├─────────────────────────────────────────────────────────────┤
 File Index Section                                          
   ┌───────────────────────────────────────────────────────┐ 
    For each file:                                         
      [4 bytes] Name Length                                
      [N bytes] Name (UTF-8 string)                        
      [4 bytes] Offset (absolute offset in package)        
      [4 bytes] Length                                     
   └───────────────────────────────────────────────────────┘ 
├─────────────────────────────────────────────────────────────┤
 Data Section                                                
   ┌───────────────────────────────────────────────────────┐ 
    File 1 Data                                            
    File 2 Data                                            
    ...                                                    
   └───────────────────────────────────────────────────────┘ 
└─────────────────────────────────────────────────────────────┘

下面简单说明下:

Header (14 bytes)

偏移 长度 字段 说明
0x00 1 Magic Start 0xBE (190) 固定魔数
0x01 4 Version 0x00000000 Version 0
0x05 4 Info Length 变长 File Count + File Index 总长度
0x09 4 Data Length 变长 数据区总长度(所有文件内容总和)
0x0D 1 Magic End 0xED (237) 固定魔数

File Count (4 bytes)

偏移 长度 字段 说明
0x0E 4 File Count 包含的文件总数

File Index Entry (变长)

每个文件的索引结构:

[4 bytes]  Name Length      - 文件名字节长度
[N bytes]  Name             - 文件名 (UTF-8 编码)
[4 bytes]  Offset           - 文件数据在包文件中的绝对偏移相对 0x00
[4 bytes]  Length           - 文件数据长度
  • 固定部分: 12 bytes (Name Length + Offset + Length)
  • 变长部分: N bytes (文件名)

Data Section

文件数据按索引顺序紧密排列,无填充字节。关键关系(由 pack.js 可直接推导):

  • InfoLength = 4 + Σ(4 + nameLen + 4 + 4)
  • DataStart = 14 + InfoLength
  • 首个文件的 Offset = DataStart
  • DataLength = Σ(file.length)

从开发者工具中可见,还有个Version 10,它的头部多了个索引长度,且每个文件索引多了两个字段:

  • [1 byte] Enc Type (加密类型)
  • [2 bytes] Mode (文件权限模式)

另外它的偏移有两个实现,一个是绝对地址,一个是相对地址,具体用哪个需要根据代码实际来,这个俺没深入分析了~

在解包后,是编译好的文件,还需要再对单个文件处理,这里不再分析了,可见下面提到的解包工具~

不同版本

其实没啥特别的,但一些解包工具做了很精细化的处理,例如分了小程序和小游戏、插件、运行公共库、子包等,这主要根据是否含有某文件区分,可忽略~

加密结构

在windows和mac上它们是加密存放的,且加密方式存在差异,如下

Windows

它的前六字节是V1MMWX,接下来1024字节由AES加密,剩下是异或编码,且加密的salt和iv是固定值,key由wxid决定,具体如下:

const wxid = process.argv[2];
const iv = 'the iv: 16 bytes';
const salt = 'saltiest';
const wxapkgPath = '__APP__.wxapkg';
const decWxapkgPath = 'dec.wxapkg';

const data = fs.readFileSync(wxapkgPath);
// 生成密钥
const dk = crypto.pbkdf2Sync(Buffer.from(wxid), Buffer.from(salt), 1000, 32, 'sha1');
// 创建AES解密器
const decipher = crypto.createDecipheriv('aes-256-cbc', dk, Buffer.from(iv, 'utf8'));
// 解密前1024字节
let originData = decipher.update(data.slice(6, 1024 + 6));
// 处理剩余部分
const xorKey = wxid.length >= 2 ? wxid.charCodeAt(wxid.length - 2) : 0x66;
const afData = data.slice(1024 + 6).map(b => b ^ xorKey);
// 合并解密数据
originData = Buffer.concat([originData, Buffer.from(afData)]);
// 写入解密后的文件
fs.writeFileSync(decWxapkgPath, originData);

Mac

只有老版本有加密,新版本没了。老版本加密方式是AES-ECB加密前1024字节,并且尾部会有WAPkgEncryptedTagForMac标记,可参考mac_wxapkg_decrypt获取密钥,它是账户级的:

// 拦截 AccountService 的 GetEncryptKey 方法
function interceptAccountService(accountService: NativePointer) {
    Interceptor.attach(accountService.implementation, {
        onLeave: (ret: NativePointer) => {
            const keyObject = new ObjC.Object(ret);
            const key = keyObject.bytes().readByteArray(keyObject.length());
            console.log(hexdump(key, { offset: 0, length: 16, header: true, ansi: false })); 
        }
    });
}

const accountService = ObjC.classes.AccountService['- GetEncryptKey'];
interceptAccountService(accountService);

分析方式

解包

先获取小程序的代码,分为手机端和PC端:

# 安卓 需root
/data/data/com.tencent.mm/MicroMsg/{用户ID}/appbrand/pkg/ # 旧版
/data/data/com.tencent.mm/MicroMsg/appbrand/pkg/general # 新版
# IOS 需越狱
/var/mobile/Containers/Data/Application/{微信App的UUID}/Library/WechatPrivate/{用户ID}/WeApp/LocalCache/release/{小程序AppID}/
# windows
C:\Users\{UserName}\Documents\WeChat Files\Applet\{小程序AppID}\
# mac
/Users/{UserName}/Library/Group Containers/xxxx.com.tencent.xinWeChat/Library/Caches/xinWeChat/{用户ID}/WeApp/LocalCache/release/# 旧版
/Users/{UserName}/Library/Containers/com.tencent.xinWeChat/Data/.wxapplet/packages/ # 新版

也可以通过抓包获取...

有了文件后,存在大量工具可以使用,它们分为正则表达式提取和AST分析的,推荐后者,下面是工具列表:

解密后用这里的工具就可以解包了~

调试

1.使用开发者工具抓包(直接在Console->Networks查看或者用专业工具抓),记得要勾选不校验证书,还有个wx.setEnableDebug没分析过~

2.windows版可使用WeChatOpenDevTools-Python强制开启调试,这个工具也可以hook macOS版的,其实macOS上的老版本直接开WebKit的调试即可~

3.安卓可用http://debugxweb.qq.com/?inspector=true去开启渲染层的调试,逻辑层得另寻它法~

注:要开启macOS的微信小程序调试,hook的点是一样的(enable vconsole/Devtools/app->web),但是mac需要特权,可以通过关闭SIP或重签名微信来实现

抓包

微信小程序能发出四种包:httpwebsockettcpudp,对于没有开启微信网关的http流量可以直接通过设置系统代理实现,websocket尽管也是基于http但和剩下三者一样,需要强制转发流量,详见抓包相关文章。

分析策略

我们只关注它的实现方式,不涉及风控相关代码,不涉及深度的逆向对抗,所以分析起来只是工作量大,可配合llm+ida-mcp/jeb-mcp去分析,效率x10!大体方向是从js层的js api开始入手向下追,wx.xxx是关键的函数,通常需要hook观察调用情况,之前的版本是Java实现的,它使用J2V8 (它改名为mmj2v8/mmv8),故通过registerJavaMethod/com.tencent.mm.appbrand.commonjni.AppBrandJsBridgeBinding即可找到所有的js bridge注册,后来估计是安全/性能考虑,移走了,现在关键点在如下so文件:

libappbrandcommon.so
libcronet.119.0.6045.214.so
libilink_protobuf.so
libmmj2v8.so
libMMProtocalJni.so
libmmv8.so
libwxa-runtime-binding.so
libmmwcwss.so

由于它的JS运行时主要是V8所以也可以根据V8注入函数/类的方式去定位关键点。

架构分析

总览

要理解它的架构,需要弄懂它里面一些重要类的作用与关系,下面放出一个类的相互关系图(它并不完整),先总览了解一下:

现在俺逐个说明

Container族

这里包含AppBrandRuntime、AppBrandService、AppBrandPage三个重要类,它们一起组成了完整的小程序进程:

AppBrandRuntimeContainer -> AppBrandRuntimeContainerLU -> AppBrandRuntimeContainerStandaloneImpl -> WxaRuntimeContainer    # 管理AppBrandRuntime,包括创建、查找、销毁等
    AppBrandRuntime -> AppBrandRuntimeLU                            # 代表单个小程序
        AppBrandService -> AppBrandServiceLU -> AppBrandServiceWC   # Service上下文
        AppBrandPageContainer -> WxaPageContainer                   # 所有页面
            AppBrandPageView -> AppBrandPageViewLU                  # 单个Page上下文

这里只说AppBrandRuntime,它代表了一个正在运行的小程序实例,负责:

  • 生命周期管理: 负责小程序的启动 (onLaunch)、显示 (onResume)、隐藏 (onPause) 和销毁 (onDestroy)
  • 配置管理: 持有并管理全局配置,如 AppBrandSysConfigLU (系统配置) 和 AppBrandInitConfigLU (启动配置)
  • 组件持有: 持有逻辑层 (AppBrandService) 和视图层容器 (AppBrandPageContainer)
  • 权限控制: 通过 mPermissionController 管理 API 调用权限
  • 性能监控: 集成 mPerformancePanel (性能面板) 和 AppBrandIDKeyReporter (打点上报)

AppBrandComponent族

主要有如下类:

1.AppBrandComponent: 它是组件最顶层接口,定义了获取AppID、JS运行时、事件分发与线程调度等接口,这是渲染层和逻辑层的顶层

2.AppBrandComponentImpl: 实现了通用逻辑,管理JS API池、JS运行时创建销毁及拦截器等

3.AppBrandComponentWxaSharedKT: 在Impl基础上增加Kotlin支持(语法糖),负责注入__wxConfig和生成预加载配置

4.AppBrandComponentWxaShared: 逻辑层和视图层的共同父类,封装了文件系统、基础库、小程序包读取等底层资源访问能力,处理环境上下文和运行时生命周期

5.AppBrandService: 代表逻辑线程,专注运行app-service.js、管理Worker和全局生命周期,不含UI操作

6.AppBrandComponentViewWxa: 代表视图层,增加UI能力,负责View的显示隐藏、UI线程调度及视图生命周期管理

7.AppBrandPageView/LU/WC: 具体页面实现,持有WebView渲染引擎,处理页面的加载、显隐状态及页面级API

8.HTMLWebViewComponentImpl: 对应<web-view>组件,用于加载外部HTML页面,渲染机制区别于普通小程序页

它们的继承关系如下:

AppBrandService类

这是我们最关注的部分,逻辑层!它可以分逻辑层主体和业务逻辑策略两部分,先看第一部分:

1.AppBrandService:它继承自 AppBrandComponentWxaShared,是小程序逻辑层的宿主,它是一个抽象的概念,代表了Service 层它是 JS 代码运行的容器,但不直接执行 JS,而是委托给 AppBrandJsRuntime,通过 mWorkerContainer 管理Worker线程,提供 dispatch 方法将事件(如 onAppEnterForeground)分发给 JS 层,负责生成并注入 __wxConfig 到 JS 环境

2.AppBrandServiceLU:它继承自 AppBrandService,它本身不包含复杂的业务逻辑,而是将逻辑委托给 mLogicImp (BaseAppBrandServiceLogic),它根据初始化参数(如是否是远程调试),选择不同的 mLogicImp 实现,将 onInit, onRuntimeReady 等生命周期事件转发给 mLogicImp,创建 AppBrandComponentInterceptor 来拦截和处理 JS API 调用

3.AppBrandWorkerContainer:是多线程Worker的管理者,使用createWorkerInternal创建worker,支持创建普通 Worker 和音频/处理 Worker,提供 postMsgToWorkeronWorkerMsg 实现主线程与Worker线程的通信,每个 Worker 都有独立的 V8EngineWorkerManager 和上下文

第二部分是逻辑实现:

1.BaseAppBrandServiceLogic:定义了Service逻辑的标准接口,它管理 AppBrandCommonBinding,这是JS调用Native能力(如 JNI)的桥梁,管理 MMWebRTCBinding,提供WebRTC能力,并支持 AppServiceExtensionWithLifecycle,允许外部插件监听Service生命周期,它通过createCustomJsRuntime()让子类决定创建什么类型的JS引擎

2.AppBrandMiniProgramServiceLogicImp:是正常运行模式下的逻辑实现,它创建 V8JsEngine(基于 V8 的高性能 JS 运行环境),负责加载各种库(execInternalInitScript加载基础库 WAService.jsexecExternalInitScript加载业务代码app-service.jsloadModuleInternal实现分包加载、并支持 TCP, UDP, WebAudio, SkiaCanvas等组件的懒加载初始化,在初始化时,先注入__wxConfig,再加载WAService.js,最后加载app-service.js

3.MPRemoteDebugServiceLogic:是真机调试模式下的逻辑实现,它创建 RemoteDebugJsEngine,这实际上是一个空壳引擎,它不执行具体的JS代码,而是拦截所有的API调用 shouldInterceptCallbackHandler和事件分发shouldInterceptDispatchHandler,通过 WebSocket 将这些指令发送给开发者工具(IDE),由 IDE 中的 JS 引擎执行,然后将结果回传

AppBrandServiceAppBrandServiceLogic 的分离使得微信可以在不修改Service主体代码的情况下,通过替换Logic实现来支持完全不同的运行模式(本地运行 vs 远程调试),极大地提高了系统的灵活性和可维护性:

AppBrandPageView类

这是渲染层的东西,小程序开发者无法直接编写执行在渲染层的代码,他只能写WXML和WXSS然后将其编译后注入到渲染层,一个小程序可能会有很多页面,每个页面会有一个AppBrandPage实例并被统一维护,关键类如下:

1.AppBrandPageContainer: 它管理小程序所有页面,维护页面栈与路由跳转(如navigatexx/reLaunch/switchTab等),调度页面生命周期(如onPagexx),作为FrameLayout承载视图,注意它只负责页面切换而非具体渲染

2.AppBrandPage: 代表逻辑页面Window,持有PageView,负责处理页面生命周期回调及导航栏样式配置,作为PageView的容器

3.AppBrandPageView: 继承自视图组件基类,持有IAppBrandWebView(通常是WebView)来渲染HTML内容,处理用户交互与JSBridge,管理导航栏等UI,通过Renderer隔离具体渲染实现,它是真正展示给用户看的内容区域

4.AppBrandComponentViewWxa: PageView的父类,提供show/hide等通用UI操作及生命周期监听,剥离具体页面逻辑只保留通用视图能力

5.AppBrandPageViewRenderer: 抽象渲染逻辑,负责创建WebView和加载URL,利用策略模式支持WebView或Skyline等不同渲染引擎切换

6.IAppBrandWebView: 底层WebView封装,负责加载URL与执行JS,屏蔽系统WebView或XWeb等的内核差异

在单个页面中会有三个关键角色,Webview负责最终的渲染行为、Renderer负责业务逻辑(如什么时候要执行什么操作)、RenderEngine是一个中间层负责连接Webview和Renderer(保证Webview在还未初始化时Renderer下达命令不会出错且在后续合适的时机会被执行),下面是Render类的说明:

1.MPPageViewRenderer: 它是页面渲染的高层管理者,继承自AbstractMPPageViewRenderer,负责协调WebView的生命周期、事件分发(如 custom_event_vdSync)以及与AppBrandPageView组件的交互,他通过内部类RendererDelegate实现,负责向WebView注入基础环境,包括 __wxConfigWAVConsole.jsWAWebview.js等基础库脚本,监听并处理关键的渲染事件(如 GenerateFuncReady),确保逻辑层和视图层的同步;在onCreateCustomWebView中它会创建具体的MPWebViewRenderEngine实现(如MPWebViewRenderEngineLegacyImpl)

2.MPWebViewRenderEngine: 定义了WebView渲染引擎的标准行为接口,继承自IAppBrandWebView,它屏蔽了底层WebView的具体实现细节,提供统一的接口给MPPageViewRenderer调用,它定义了初始化 (dispatchInit)、预加载 (dispatchPreload)、脚本执行 (executeOrDeferEvaluation) 和页面重载 (dispatchPageReload) 等能力并提供查询页面框架是否就绪 (isPageFrameReady) 和是否正在预加载 (isDoingPreload) 的方法

3.MPWebViewRenderEngineLegacyImpl: 是MPWebViewRenderEngine的具体实现类,包装了底层的WebView,它维护一个mDeferredEvaluations队列,当WebView的PageFrame还没准备好时,所有 JS执行请求都会被暂存,直到onPageFinished后统一执行,它还会处理WebView的Trim内存清理)和Reload(重载)逻辑,当收到内存警告时,会清理状态;再次进入时会触发重载,通过 RendererDelegate 回调 MPPageViewRenderer 来获取 Host URL、注入环境变量或加载 PageFrame。

JSEngine族

它支持多种JS引擎,包括V8NodeWebview(实现IAppBrandWebView接口,渲染层使用的)、RemoteDebug等版本,这里主要关注逻辑层使用的两个版本:

V8JsEngine: # V8引擎实现,也就是服务层的JS引擎
Lcom/tencent/mm/plugin/appbrand/jsruntime/V8JsEngine
    
    └── extends AbstractMultiContextJsEngine # 新增多上下文管理(上下文的创建、销毁、获取等),自定义异常处理器、控制台输出重定向、配置状态管理等
            
            └── extends AppBrandJ2V8Context # 抽象V8上下文基类,封装了V8引擎的底层调用,如生命周期管理、调试与回调处理、JS线程管理等
                    
                    └── implements:
                        AppBrandJsRuntime # JS运行时主接口,提供JS执行和生命周期管理
                        AppBrandJsRuntimeAddonDestroyListener # 销毁监听器Addon,在引擎销毁时释放资源
                        AppBrandJsRuntimeAddonV8 # V8引擎Addon,提供V8特有的JS执行能力,比如批量执行,还暴露出了isolate/context/uvlooper等供其他模块使用
                        NativeBufferManager (extends AppBrandJsRuntimeAddon) # Native缓冲区管理Addon,负责JS与Native间的高效二进制数据传递
                        InitializationListener (extends AppBrandJsRuntimeAddon) # 初始化监听Addon,在JS环境初始化完成时回调
                        MultiContextManager (extends AppBrandJsRuntimeAddon) # 多上下文管理Addon,管理多个独立的JS执行上下文,比如新的SDK支持主上下文,Worker和插件在单独的上下文
                        PausableInterface (extends AppBrandJsRuntimeAddon) # 暂停/恢复Addon,支持暂停JS引擎执行



RemoteDebugJsEngine: # 远程调试引擎,这应该是老版的真机调试引擎,会将常规js发给开发者工具执行,真机只执行JS Bridge以下的
Lcom/tencent/mm/plugin/appbrand/debugger/RemoteDebugJsEngine
    
    └── implements AppBrandJsRuntime
        
        └── 包含:
             RemoteDebugSocket - WebSocket管理
             RemoteDebugMessageManager - 消息管理
             RemoteDebugView - 调试视图
             DebugEnvironment - 调试环境

初始化过程

在微信中,它通过startActivity去启动小程序,它主要需要构造下面参数,放在intent中:

public class LuggageStartParams {
    private final String appId; // 小程序的唯一标识符
    private final boolean isGame; // 是否为小游戏 (true: 小游戏, false: 小程序)
    private final int versionType; // 版本类型 (0: 正式版, 1: 开发版, 2: 体验版)
}

public final class WxaStartParams extends LuggageStartParams {
    private final WxaAttrSyncRequest action; // 核心启动动作请求,包含版本、场景、调试等详细配置
    private final AppBrandLaunchReferrer referrer; // 启动来源信息,描述了"从哪里来" (如从另一个小程序跳转、从扫码等)
    private final AppBrandStatObject statObject; // 统计对象,用于数据上报和分析
}

public class WxaAttrSyncRequest extends ZPVZX {
    public String appId; // 小程序 AppID
    public boolean needVersionInfo;  // 是否需要后台返回版本信息
    public String lastVersion; // 本地缓存的最后一个版本号 (用于增量更新)
    public String instanceId;  // 运行实例 ID (区分同一 AppID 的不同运行实例),实例管理用的,多开找它附近
    public int scene; // 启动场景值 (如 1001: 发现栏小程序)
    public LinkedList moduleList; // 需要加载的模块列表 (分包加载用)
    public boolean isDev; // 是否为开发版环境
    public boolean isDebug; // 是否开启调试模式
    public int clientVersion; // 客户端版本号
    public int netType; // 网络类型 (如 WiFi, 4G)
    public String extInfo; // 扩展信息 JSON 字符串 (包含远程调试配置、第三方平台配置等)
}

public final class AppBrandLaunchReferrer implements Parcelable {
    public int launchScene; // 启动来源场景值
    public String appId; // 来源 AppID (例如从 A 小程序跳转到 B 小程序,这里是 A 的 AppID)
    public String extraData; // 来源携带的公开额外数据
    public String privateExtraData; // 来源携带的私有额外数据
    public String messageExtraData;// 消息相关的额外数据 (如从聊天消息卡片启动)
    public String url;// 来源 URL (如从 H5 页面跳转)
    public int sourceType;// 来源类型
    public String businessType;// 业务类型标识
    public String transitiveData;// 传递性数据 (可能需要透传给下一个页面)
}

public final class AppBrandStatObject implements Parcelable {
    public int preScene; // 上一个场景值 (用户进入当前场景之前的场景)
    public String preSceneNote; // 上一个场景的备注信息
    public int scene; // 当前启动场景值
    public int sceneType; // 场景类型
    public String sceneNote; // 当前场景备注信息
    public String appId; // 小程序 AppID
    public int actionType; // 动作类型
    public String extraInfo; // 额外统计信息
    public AppBrandRecommendStatObj recommendStatObj; // 推荐相关的统计对象 (如从推荐列表启动)
}

在小程序端它最多可以起5个进程,即平时看到的com.tencent.mm:appbrand0~4,在实现上它粗暴的创了5个一样的类,命名ContainerActivity0~4来对应各自的页面,咱直接从它开始看,在收到intent后它会找运行时或创建新的运行时并初始化:

ContainerActivityLifecycleDispatcherImpl.onCreate(Bundle)
├── AppBrandRuntimeContainerStandaloneImpl.performLaunch(Intent, "onCreate")
   ├── WxaLaunchParameters.toLaunchParcel(WxaAttrSyncRequest)  # 解析启动参数
   ├── getRuntimeByAppId(appId)                                # 尝试获取已存在的 Runtime,这里如果存在下面会走loadExisted
   ├── WxaLaunchPreconditionProcess.start()                    # 启动预加载流程(获取属性、检查封禁等)
      └── loadStandaloneImpl(AppBrandInitConfigLU, ...)       # 预加载成功回调
          ├── new AppBrandInitConfig(config)                  # 创建独立运行时配置
          └── AppBrandRuntimeContainerLU.load(config, stat)   # 加载运行时
              └── runOnUiThread
                  ├── createRuntime(config)                   # 这是运行时不存在的情况,会创建新的Runtime
                  └── loadNew(null, runtime, config)          # 加载新Runtime
                      └── AppBrandRuntimeContainer.loadNew(old, new, config)
                          ├── AppBrandRuntime.init(config)    # 初始化 Runtime 状态
                             ├── mInitConfig = config
                             └── mRunningStateController.start()
                          ├── mRuntimeStack.push(newRuntime)  # 入栈
                          ├── attachRuntimeViewToTree(view)   # 挂载视图
                          └── AppBrandRuntime.launch()        # 启动 Runtime
                              ├── onLaunch()
                              ├── onCreatePrivate()           # 注册核心服务
                                 ├── IPCInvoker.register...  # 注册跨进程通信
                                 ├── registerService(...)    # 注册基础服务(ImageLoader等)
                                 └── AppBrandLifeCycle.notifyOnCreate()
                              ├── prepare(PrepareAllDoneInitNotify) # 执行异步准备任务
                              └── showSplash()                # 显示启动页
                                  └── PrepareAllDoneInitNotify.done() # 准备完成回调
                                      └── initRuntime()       # 初始化运行时核心组件

AppBrandRuntime在初始化时会创建逻辑层和渲染层并初始化,之后导航到启动参数指定的页面,过程如下:

AppBrandRuntime.initRuntime()
├── initRuntimeImpl()
   ├── installFileSystem(false)                    # 安装文件系统,它实现了自己的VFS,每个小程序读写文件会透过它
   ├── onInitBeforeComponentsInstalled()           # 组件安装前初始化
   ├── createService()                             # 创建AppBrandService
      └── new AppBrandService()                   # 创建逻辑层,下面会马上初始化它
   ├── attachService(mService)                     # 绑定服务到运行时
      ├── onPreAttachService(appBrandService0)    # 服务预绑定,它采用的是服务注册机制,之后会根据接口找是否存在某个服务并调用,且采用后进先出的优先级来支持功能重写
         ├── registerService(IImageReaderUrlBuilder.class, ...)
         ├── registerService(IImageLoader.class, ...)
         └── 注册其他服务配置
      └── mService.attachRuntime(this)            # 服务绑定运行时,现在实现了双向绑定了,你中有我我中有你(
          └── onRuntimeReady(appBrandRuntime0)    # 运行时就绪回调
              ├── installJsRuntime()              # 安装JS运行时
                 └── createJsRuntime()           # 创建JS运行时
                     ├── createCustomJsRuntime() # 优先尝试自定义创建运行时
                        ├── AppBrandMiniProgramServiceLogicImp  new V8JsEngine(null, null)
                        └── MPRemoteDebugServiceLogic  new RemoteDebugJsEngine()
                     └── new V8JsEngine(null, null) # 默认创建,注意这里的图有问题:createCustomJsRuntime和默认创建是二选一,不会创建多个
              ├── installWorkerContainer()        # 安装Worker容器
                 └── createWorkerContainer()     # 创建Worker容器
                     └── new AppBrandWorkerContainer(this)
              └── WorkerContainer.onRuntimeReady() # Worker容器就绪
                  ├── workerEngineManager.onRuntimeReady()
                  └── dispatchConfigToWorker()    # 下发配置到Worker
   ├── performInitService()                        # 初始化服务
      └── mService.init()                         # 服务初始化
          ├── super(AppBrandComponentImpl).init() # 父类初始化
             ├── installJsRuntime()              # 安装JS运行时
                ├── createJsRuntime()           # 创建JS运行时
                ├── addJavascriptInterface(this.mJsInterface, "WeixinJSCore")
                └── postJsRuntimeCreated()      # JS运行时创建后处理
                    ├── postCreateJsRuntime()   # 子类后处理
                       └── notifyCreate()      # 通知创建完成,这是在AppBrandCommonBindingJni里哦
                    └── run pending tasks       # 执行等待任务
             ├── installJsApis()                 # 安装JS API
                └── createJsApiPool()           # 创建JS API池
                    ├── createForAppService()   # 为应用服务创建
                    └── createForPage()         # 为页面创建
             └── installWorkerContainer()        # 安装Worker容器
          ├── attachConfig(configStore)           # 附加配置
          └── onInit()                            # 初始化完成回调
              ├── injectConfig()                  # 注入配置
|   |           |      ├── super.injectConfig() 
                        ├── generateWxConfig()          # 生成WX配置
                        ├── attachCommonConfig()    # 附加通用配置
                        └── mRuntime.getAppConfig().getInjectConfig()
                        ├── subscribeHandler("__native_custom_event__wxConfig_inject", ...)    # 这里会用两种方式注入配置,包括订阅事件和直接注入js代码
                        └── evaluateJavascript("__wxConfig=%s", ...)
                    ├── initDebugger()            # 如果启用了远程调试,则在这里会初始化

              └── clearEventQueue()               # 清空事件队列
   ├── performInitPageContainer()                  # 初始化页面容器
      ├── createPageContainer()                   # 创建页面容器
         └── new AppBrandPageContainer(context, this)
      └── mPageContainer.init(enterPath)         # 初始化页面容器
   ├── 创建PIP管理器和其他组件                        # 即画中画管理
   └── onPostInit()                                # 初始化后处理
├── mInitialized = true                             # 标记为已初始化
└── runPendingTasksOnRuntimeInitialized()           # 执行运行时初始化后的等待任务

自此,一个完整的启动流程结束。

JS Bridge

JS层

先看JS层,开发者使用的是wx.xxx,在之前的版本中它最终会调用到WeixinJSBridge.invoke,但在后续的更新中它根据平台的不同有了些差别,这里只看Android,它里面WeixinJSBridge并不是Native注入的,而是在JS里定义并封装了其它对象,实际上Native会向全局注入多个全局变量/类/方法,如WeixinJSCore/WeixinJSCoreAndroid负责JS Bridge的调用回调与事件订阅,WeixinJsContext负责加载JS库等,NativeGlobal更全能,它不仅也能处理JS Bridge相关调用,Native还会向它注入很多原生库,如Request/Download/UploadTaskUDP/TCP/WXAUDIO/WSSOffscreenCanvas/WebRTC/Box2D等。

注:在之前的小程序中,逻辑层主代码在WAService.js中,后来小程序支持了Worker机制,就用WAMainContextService.jsWAWorker.js取代了它,使它更专一,其实他俩里有大部分代码是重合的。

在Android里小程序要执行的JS除了小程序自身代码、小程序SDK外,还有android公共库里的,这里主要看android.js,它会被注入逻辑层,对应的还有android-webview.js会被注入渲染层,android.js文件没有混淆这里粘贴部分说明:

;if (this.setTimeout && this.clearTimeout && this.setInterval && this.clearInterval && this.NativeGlobal) {
    this.NativeGlobal.setTimeout = this.setTimeout; 
    // ... 如果当前环境已经注入了 setTimeout/clearTimeout 等定时器方法和 NativeGlobal,则将这些方法挂载回 NativeGlobal,确保原生侧能通过 NativeGlobal 调用 JS 的定时器。
};
// 封装 WeixinJSCore 接口,用于调用原生 API(bridge)
var WeixinJSCore = (function(global) {
    var _WeixinJSCore = global.WeixinJSCore
        // 优先使用更现代的 invokeHandler(来自 NativeGlobal 或 workerInvokeJsApi)
    var __invokeHandler__ = _WeixinJSCore.invokeHandler
    var __invokeHandler2__ = _WeixinJSCore.invokeHandler2
    // 优先级:NativeGlobal.invokeHandler > workerInvokeJsApi > 原始 invokeHandler
    if (global.NativeGlobal && global.NativeGlobal.invokeHandler) { 
        __invokeHandler2__ = global.NativeGlobal.invokeHandler
    } else if (global.workerInvokeJsApi) {
        __invokeHandler2__ = global.workerInvokeJsApi
    }
    var ret = {};
    ret.publishHandler = function(event, data, dst) {_WeixinJSCore.publishHandler(event, data, dst)} // // 事件发布接口直接用weixinjscore(用于原生向 JS 发送事件)
    ret.invokeHandler = function(api, args, callbackId, privateArgs) {/*..*/} // 这两个调用js api的根据上面的优先级,实际会走NativeGlobal的invokeHandler
    if (global.workerInvokeJsApi) { /*..*/ } 
    return ret
})(this);

// 封装 Android 平台特有的回调和事件订阅处理
var WeixinJSCoreAndroid = (function(global) {
    var ret = {};
    ret.invokeCallbackHandler = function(callbackId, data, ext) {  // 原生调用 JS 回调的入口
            WeixinJSBridge.invokeCallbackHandler(callbackId, data, ext);
    };
    ret.subscribeHandler = function(event, data, src, ext) {// 原生向 JS 发送事件(如配置注入、错误等)
        if ('__native_custom_event__wxConfig_inject' === event) { // 这个事件会特殊处理,在下面的微信网关部分会再提到
            global.NativeGlobal.hasInitializedWxConfig = true;
            Object.assign(global.__wxConfig, JSON.parse(data))
            return
        }
                // 其它事件解析参数后发送
        WeixinJSBridge.subscribeHandler(event, data, src, ext);
    };
    return ret;
})(this);

//  修复 WeixinWorker.create 的多参调用兼容性
if (typeof this.WeixinWorker !== 'undefined') {/*...*/};
// 删除 NativeGlobal 上的 invokeHandler,防止被意外使用
;(function(global){ delete global.NativeGlobal.invokeHandler; })(this);
// 封装共享内存缓冲区接口(用于高性能数据传输,如音视频)
var WeixinSharedBuffer = (function(global) {/*...*/})(this);;
// 为多个大型原生模块(如 Canvas、WebRTC、AR 等)实现懒加载,即进行封装,在访问时才会调用NativeGlobal.initModule去实际加载
if (this.NativeGlobal && this.NativeGlobal.initModule) {/*...*/};;
// 为构造函数类模块(如 WXAUDIO、TCP、UDP)实现懒加载代理
(function(globalThis) {/**/})(this);

Native层

C代码

现在要看Native是怎么为JS提供这些能力的,它由Java代码和C代码共同实现,先看C的吧,在libwxa-runtime-binding.so里,它的AppBrandCommonBindingJni.nativeBindTo调用注入了NativeGlobalWeixinJSContext

void __fastcall Java_com_tencent_mm_appbrand_commonjni_AppBrandCommonBindingJni_nativeBindTo(int a1,int a2,AppBrandRuntime *a3,v8::Isolate *a4,_QWORD **a5,const void *a6)
{
  BindTo(a3, v11, (unsigned __int64)a6);
}
void __fastcall BindTo(AppBrandRuntime *a1, char *a2, unsigned __int64 a3)
{

  InstallNativeGlobal(v5, a1, &v14);
  BindClass(v5, a1, &v14);
}

__int64 __fastcall BindClass(v8::Isolate *a1, AppBrandRuntime *a2, __int64 *a3)
{
  WeixinJSContext_init(&v24);  // 初始化WeixinJSContext类定义结构体,v24是一个ClassDefinition结构体,用于定义JavaScript类的属性和方法
  // 添加多个方法到类定义
  ClassDefinition_AddMethod(&v24, "allocEmpty", AllocEmptyCallback);
  ClassDefinition_AddMethod(&v24, "loadLibFiles", LoadLibFilesCallback);
  ClassDefinition_AddMethod(&v24, "loadJsFiles", LoadJsFilesCallback);
  InitClassTemplate(&v24, "WeixinJSContext"); // 初始化类模板
  CurrentContext = v8::Isolate::GetCurrentContext(a1);
  v8 = v8::External::New(a1, (v8::Isolate *)a2, v6);
  v9 = v8::FunctionTemplate::New(a1, WeixinJSContextConstructorCallback, v8, 0LL, 0LL, 1LL, 0LL, 0LL, v21, v22, v23); // 创建WeixinJSContext类的实例对象
  Function = v8::FunctionTemplate::GetFunction(v9, CurrentContext); 
  v11 = v8::Function::NewInstance(Function, CurrentContext, 0LL, &v24.padding);
  v12 = *a3;
  // 将WeixinJSContext类添加到全局对象
  v13 = v8::Isolate::GetCurrentContext(a1);
  v14 = v8::String::NewFromUtf8(a1, "WeixinJSContext");
  v15 = (v8::api_internal *)v8::Object::Set(v12, v13, v14, v11);
}
/* for NativeGlobal */
__int64 __fastcall InstallNativeGlobal(v8::Isolate *a1, v8::Isolate *a2, __int64 *a3)
{
  // 检查是否存在,存在则不新建
  native_global_property = (v8::Value *)get_native_global_property(a1, *a3, "NativeGlobal");   
  if ( native_global_property && (v8 = (__int64)native_global_property, (v8::Value::IsObject(native_global_property) & 1) != 0) )  {  }  else
  {
    v8 = v8::Object::New(a1, v9);
  }
  v16[0] = v8;
  // 注册各种功能:Signal/Worker和invokeHandler initModule等方法
  registerFunction_43E38(a1, v8, "invokeHandler", (__int64)js_to_native_invoke_handler_wrapper, a2);
  registerFunction_43E38(a1, v8, "initModule", (__int64)init_module_callback, a2);
  registerFunction_43E38(a1, v8, "testV8Crash", (__int64)test_v8_crash_callback, a2);
  init_reporter_binding(a1, a2, v16);
  init_native_buffer_binding(a1, a2, v16);
  if ( *((int *)a2 + 84) <= 0 )
  {
    init_worker_binding(a1);
    init_runtime_binding();
    registerFunction_43E38(a1, v16[0], "getCurrentThreadTime", (__int64)get_current_thread_time_callback, a2);
    registerFunction_43E38(a1, v16[0], "createSignal", (__int64)create_signal_callback, a2);
  }
  v10 = *a3;
  v11 = v16[0];
  // 设置为全局对象
  CurrentContext = v8::Isolate::GetCurrentContext(a1);
  v13 = v8::String::NewFromUtf8(a1, "NativeGlobal");
  v14 = (v8::api_internal *)v8::Object::Set(v10, CurrentContext, v13, v11);
}

AppBrandJsBridgeBinding.nativeCreateRuntime调用注入了WeixinJSCoreAndroid

__int64 __fastcall Java_com_tencent_mm_appbrand_commonjni_AppBrandJsBridgeBinding_nativeCreateRuntime(JNIEnv *a1, __int64 a2, v8::Isolate *a3, _QWORD **a4, void *a5, char a6)
{

    // 获取 Java 传入的 Bridge 名称 (参数 a5, 例如 "WeixinJSCoreAndroid")
    const char *bridge_name_cstr = env->GetStringUTFChars(a5, 0);
    // 将 C 字符串转换为 C++ std::string
    std::string bridge_name_cpp_str(bridge_name_cstr); 
    // 分配 AppBrandJsBridgeBinding 对象的内存 (大小 0x38 字节)
    AppBrandJsBridgeBinding *binding_instance = (AppBrandJsBridgeBinding *)operator new(0x38);
    // 调用核心构造函数进行初始化
    AppBrandJsBridgeBinding_ctor(binding_instance, isolate, context, bridge_name_cpp_str, jni_manager);
    return (jlong)binding_instance;
}

void AppBrandJsBridgeBinding_ctor(AppBrandJsBridgeBinding* this_ptr, v8::Isolate* isolate, void* context_holder, std::string* bridge_name_str, void* jni_mgr) {

    // 初始化结构体成员
    this_ptr->jni_manager = jni_mgr;       // Offset 0x20
    this_ptr->isolate = isolate;           // Offset 0x28
    this_ptr->context = context_holder;    // Offset 0x30

    // 进入 V8 环境 (标准 V8 嵌入流程) ... 获取 JS 全局对象 (Global Object)
    v8::Local<v8::Object> global = context->Global();

    const char* name = bridge_name_str->is_long() ? bridge_name_str->ptr : bridge_name_str->buf; // 处理 std::string 的短字符串优化(SSO)逻辑以获取 char*
    v8::Local<v8::Value> bridge_obj = get_native_global_property(isolate, global, name); // 调用辅助函数在 Global 中查找属性
    this_ptr->bridge_ref = CreatePersistentRef(isolate, bridge_obj); // 持久化 Bridge 对象引用: 创建一个包装结构 { Isolate*, GlobalRef } 并存入 this_ptr

    // 查找并绑定invokeCallbackHandler / subscribeHandler
    v8::Local<v8::Value> invoke_func = get_native_global_property(isolate, bridge_obj, "invokeCallbackHandler");
    this_ptr->invoke_handler_ref = CreatePersistentRef(isolate, invoke_func);
    v8::Local<v8::Value> sub_func = get_native_global_property(isolate, bridge_obj, "subscribeHandler");
    this_ptr->subscribe_handler_ref = CreatePersistentRef(isolate, sub_func);

    // 退出V8环境,析构Scope对象,释放局部句柄...
}
Java代码

从上面的小程序初始化过程可见,它为渲染层和逻辑层注入了不同的JS API:

public class JsApiPool {
    private Map pageAPIs;
    private Map serviceAPIs;

    protected void addToServicePool(AppBrandJsApi appBrandJsApi0) {
        if(appBrandJsApi0 != null && !Util.isNullOrNil(appBrandJsApi0.getName())) {
            this.serviceAPIs.put(appBrandJsApi0.getName(), appBrandJsApi0);
        }
    }
    public Map createForAppService() {
        this.serviceAPIs = new HashMap();
        this.initServicePool();
        return this.serviceAPIs;
    }
    // 这是为渲染层注入的Js API,主要供渲染层框架使用,开发者好像是用不了
    protected void initPagePool() {
        this.addToPagePool(new JsApiShowStatusBar());
        this.addToPagePool(new JsApiHideStatusBar());
        // ... 
    }
    // 这是逻辑层的,为开发者使用的
    protected void initServicePool() {
        this.addToServicePool(new JsApiBatchGetStorage());
        this.addToServicePool(new JsApiBatchGetStorageSync());
        // ...
    }
}

这里的JS API通常会有同步和异步的版本:

BaseJsApi (抽象类,实现ConstantsAppBrandJsApiMsg接口,该接口定义了常见的错误字符串)
  ↑
AppBrandJsApi (抽象类,提供结果生成和错误处理,比如去打包为json等)
  ├── AppBrandSyncJsApi (抽象类,同步API基类)
  │     └── 同步API实现类,比如JsApiBatchGetStorageSync继承自它
  └── AppBrandAsyncJsApi (抽象类,异步API基类)
        └── 异步API实现类,比如JsApiBatchGetStorage继承自它

现在JS API已经完成注册了,看看它们是怎么被调用的,在运行时初始化过层有看到它使用addJavascriptInterface(this.mJsInterface, "WeixinJSCore")去注册到JS运行时里,这里的mJsInterface是下面类的实例:

public class AppBrandJSInterface {
    private volatile AppBrandComponentImpl mEnv;

    public AppBrandJSInterface(AppBrandComponentImpl appBrandComponentImpl0) { this.mEnv = appBrandComponentImpl0; }
    public void cleanup() { his.mEnv = null; }
    private int[] extractJsonArray(String s) {/**/ }

    // 下面三个是暴露给JS的函数(当然了其实不会被调用,它走的是native的路子)
    @JavascriptInterface // 这是Android Webview的JS Bridge注入方式
    public String invokeHandler(String apiName, String data, int callbackId) {
        return this.mEnv == null ? "" : this.mEnv.invoke(apiName, data, "", callbackId);
    }
    @JavascriptInterface
    public String invokeHandler2(String apiName, String data, int callbackId, String privateData) {
        return this.mEnv == null ? "" : this.mEnv.invoke(apiName, data, privateData, callbackId);
    }
    @JavascriptInterface
    public void publishHandler(String eventName, String eventData, String targetIdsJson) {
        this.mEnv.publish(eventName, eventData, this.parseIntArrayFromJson(targetIdsJson));
    }
}

它实际只是为了满足JS Bridge的要求做了简单封装,最终调用的是AppBrandComponentImpl.invoke,而且根据上面的分析它不会直接从WeixinJSCore.invokeHandler去调用JS API了,而是走NativeGlobal,但它其实也会走到同样的位置:

public class AppBrandCommonBindingJni {
    @Keep
    protected String nativeInvokeHandler(String apiName, String data, String privateData, int callbackId, boolean isAsyncThread) { // 还有两个x,y参数未知
        return this.mAppBrandDelegate.nativeInvokeHandler(apiName, data, privateData, callbackId, isAsyncThread);
    }
}
public class AppBrandCommonBinding {
        @Override  // 从Native调过来的
        public String nativeInvokeHandler(String apiName, String data, String privateData, int callbackId, boolean isAsyncThread) {
            return ((AppBrandComponentImpl)this.appBrandComponentImpl.get()).invoke(apiName, data, privateData, callbackId, isAsyncThread, null);
        }
    }
}

它的调用过程代码量依然很大,它会先去上面注册的ApiPool里找到API的实现实例、判断是否有权调用(CTRL_Index号与位图比较)、是否有拦截器要在调用前/调用后处理它、解析参数为JSON对象,然后再根据是同步还是异步API处理它,同步则直接调用api的invoke接口并直接返回结果,异步还会根据它指定的执行位置扔给对应线程去执行,执行完后调用AppBrandComponentImpl.callback将结果返回,在callback时也有两种方式,如果支持会走AppBrandJsBridgeBinding.invokeCallbackHandler(callbackId, result),否则会直接通过AppBrandJsRuntimeAddonExecutable.evaluateJavascript("WeixinJSCoreAndroid.invokeCallbackHandler(callbackId, result)")去触发回调,时序图如下:

其它类型

上面分析了常规JS API注册/调用流程,但有一些接口是例外,包括下面会详细分析的wx.request,这里再提一嘴另一类,以websocket为例,它支持懒加载,无论是否是懒加载它都是另外的注册流程:

// 创建加载助手
WcWssNativeInstallHelper wcWssNativeInstallHelper0 = new WcWssNativeInstallHelper();
this.mWcWssNativeInstallHelper = wcWssNativeInstallHelper0;
// 建立绑定:加载owl、ssl、crypto、wcwss(核心实现)等库,创建WebSocket绑定,设置回调监听器
wcWssNativeInstallHelper0.createWcWssBinding(((AppBrandServiceLU)this.getComponent()).getJsRuntime(), this.getComponent(), 0);
// 处理证书验证和网络配置
this.mWcWssNativeInstallHelper.initConfigWcWss(this.getJsRuntime(), ((AppBrandComponentWithExtra)this.getComponent()));

怎么hook/rpc? 通过上面的分析,大部分JS API可以直接hookAppBrandCommonBindingJni类的nativeInvokeHandler方法获取调用参数和同步调用的结果,而异步api需要hookAppBrandJsBridgeBindinginvokeCallbackHandler方法获取结果,rpc也是hook这两个,不过在使用Java.chooseAppBrandCommonBindingJni实例时会找到多个,需要通过一些方法筛选出正确的实例。而剩下的JS API,例如Websocket的,就需要自己在对应的so里做hook了。

调试机制

小程序支持真机调试1.0和2.0两个版本,1.0只会在真机上执行js api,其他的js(如小程序业务代码)会在ide上执行,大概是因为那时js引擎对调试的支持还不到位,而2.0会直接使用js引擎的调试能力,所有js代码都在真机上运行。

现在先来看看JS层,在启用调试后,小程序会向渲染层注入WARemoteDebug.jsWAVConsole.js、向逻辑层注入WAServiceRemoteDebug.js来提供调试能力,因为小程序不只有DOM/Application/Console等域,还有Wxml/AppData等独有的域,且小程序逻辑层的请求也不是走BOM标准的xhr/fetch,存储也不是用的cookie/localstorage等机制,所以Network/Storage域也要特殊处理,去hook这些js api操作将其转换为cdp事件才能被ide的调试器捕获到,这些都是由注入JS实现的,它们依赖另一个被注入的对象DebuggerConnection通信,这是在Java层注入的,可看作类似Websocket的连接。

可以自行看js,咱直接转到Java层,先来看看和调试相关的类: + RemoteDebugJsEngine: 上面已经提过,远程调试的核心引擎,实现了AppBrandJsRuntime接口,它充当JS运行时的代理,在1.0模式下它替换了本地的V8/Node引擎,管理WebSocket连接(RemoteDebugSocket),处理来自IDE的指令(如登录、断点、执行代码),将本地的事件(Event)和API调用结果转发给IDE,维护调试环境 (DebugEnvironment) 和消息管理器 (RemoteDebugMessageManager);在2.0时依然使用了它,但不再用它去执行全部js代码了,而是作为一个旁路,只使用剩余功能 + DebugEnvironment: 它是调试会话的上下文状态容器,存储会话信息(debugIdsessionIdappBrandService的引用)、管理连接状态(connectionStateisBusyisBreakpoint等)、消息队列管理(如维护发送队列 sendingMessages、接收队列receivedMessages和回调映射callbackMap)和统计信息(记录流量totalDataSize、时间戳lastSendTime, lastReceiveTime)等 + RemoteDebugMessageManager: 是消息处理中心,负责构造和解析Protobuf消息、处理消息的发送策略(如同步消息syncMessageRange)、分发接收到的消息到对应的处理器(如处理网络头信息onReceiveNetworkHeader)及管理消息ID生成和确认机制 + RemoteDebugSocket: WebSocket通信层封装,它封装了WebSocketClient,负责与调试服务器(wss://wxagame.weixin.qq.com/remote/或局域网地址)建立连接、处理 SSL/TLS 配置 (SSLSocketFactory)、处理 WebSocket 的分片帧 (onFragment) 和重组、提供发送 (sendSocketMsg) 和关闭连接的能力 + DebugConnection: 定义调试连接的标准行为,被AppBrandMiniProgramServiceLogicImpMPRemoteDebugServiceLogic实现,提供onRemoteDebugInfo方法,用于接收和处理调试信息、提供getConfigScript获取配置脚本

在1.0模式下,它使用MPRemoteDebugServiceLogic作为AppBrandService的逻辑实现类,在调试时流程如下:

而在2.0下,它在小程序创建时,在AppBrandMiniProgramServiceLogicImp中初始化:

initDebugger
    initRemoteDebug # 这之前会检查 AppBrandRuntimeLU.isRemoteDebug && RemoteDebugUtils.isNewRemoteDebugType 即调试类型为1
        new RemoteDebugJsEngine # 在主线程运行
        new DebugEnvironment()
        mRemoteDebugEnv.init
            RemoteDebugUtils.setUin                     # 设置uin
            RemoteDebugUtils.parseRemoteDebugInfo       # 解析调试信息
            appBrandSysConfigLU0.isRemoteDebug          # 设置远程调试标志
            appBrandNetworkConfig0.shouldCheckDomains   # 根据调试配置禁用url/域名检查
        mRemoteDebugEnv.setDebugId
        mRemoteDebugJsEngine.init
            new RemoteDebugView                         # 设置调试视图(显示在UI)
            connect                                     # 连接调试器,根据配置去通过局域网直连还是通过腾讯服务器做中转
                new RemoteDebugSocket       
                mSocketMrg.connectSocket
        debugJSContextInterface.setupDebugEngine        # DebuggerConnection设置调试RemoteDebugJsEnginejs就能调sendCustomMessage向它发消息了
    V8.setBreakOnStart(Boolean.FALSE)                   # 不要暂停否则不会执行到正确流程
    JsValidationInjectorWC.waitForDebug                 # 等待调试器连接
    JsValidationInjectorWC.breakProgram                 # 执行"breakprogram();

它实现了DebugConnection所以可以处理调试的消息。

HTTP请求分析

直接从wx.request开始,在WAServiceMainContext.js中会一路到:

// request -> WY -> Z -> Y

Y = (e)=>{
    var a = new K({ args: t, options: e });
    if ((a.catch((e) => {s.YG("request",t,e.message || e.errMsg,e.errno);})
        .use(normalizeParams(r))    // 将请求头转小写,方法转大些,body序列化,初始化时间戳重试次数等
        .use(parseUrl({options}) // 将 URL 字符串拆解为结构化数据,方便后续 DNS 解析和连接
        .use(checkHttpDNSPolyfill) // 针对 iOS 等特定环境,检查是否需要开启 DNS Polyfill,必要时还会发预检
        .use(validateHost) // 检查 Host 是否合法,例如不能以 "." 开头,不能包含连续的 ".."
        .use(skipDNS) // 如果用户在 wx.request 中直接传入了 'ip' 参数并且配置了 skipDNSRequest,可用IP直连,绕过系统DNS解析
        .use(determineAcceleration) // 智能判断是否将请求路由到微信的加速网关
        .use(handleFreeHttpDNS) // 如果之前的请求失败了,或者配置了 __retryHttpDNS__这里会尝试获取 IP
        .use(resolveHttpDNS)    // 如果开启了enableHttpDNS,使用微信的 HttpDNS 服务解析域名,防止DNS 劫持
        .use(forceCellular)  // 强制使用蜂窝网络,比如在Wi-Fi差时
        .use(tryGateway)    // 这里是一个核心,尝试使用微信网关加速/加固
        .useAfterAsyncTasks(S.rk),
      a.catchFn)
    )
      return a.execute(t); // 执行请求
}

注:本文演示用的sdk版本为3.12.1,有些混淆的名称可以跟着它看。

传统请求流程

这里先不看中间件,在正常的流程下它会到execute->createRequestTask->B=L extends o3=we,这里关键在we,它是任务请求(Request/Download/Upload)的基类,它里面有两条路径,正常在真机下调用q获取taskInvoker,其他(可能是兼容)情况调用WeixinJSBridge到老版的路径,这里看taskInvoker,它调用G.Request.jsBinding,这里面就是new g.Z.RequestTask(args)RequestTask就是libcronet.so注册给NativeGlobal的:

v8::Isolate *__fastcall InitNetworkJSEnv(__int64 cronetJsContext,int a2,__int64 a3,v8::Isolate *a4,_QWORD *a5,__int64 isInit)
{
  // 获取/创建NativeGlobal
  CurrentContext = (v8::Context *)v8::Isolate::GetCurrentContext(a4);
  v19 = v8::Context::Global(CurrentContext);
  Property = JS_GetProperty(a4, v19, "NativeGlobal");
  if ( !Property )
  {
    Property = v8::Object::New(a4, v20);
    JS_SetProperty(a4, v19, "NativeGlobal", Property);
  }
  // 获取Cronet配置,创建Cronet对象管理器等
  CronetJsConfigBuilder_Constructor(v26, (__int64)v105, &v104, (__int64)&v108, (__int64)v107, (__int64)v106);
  v27 = BuildAndConsume_CronetJsConfig((__int64 *)&v99);
  CronetJsContext_Constructor(v29, JsObjectManager, ArrayBufferAllocator, v27, (__int64)&v108);
  if ( isInit )
  {
    // 开始定义任务相关的三个类,并将其放到 NativeGlobal 里 
    JS_Define_RequestTask_Class();        // 定义类,各种属性/方法访问器等
    JS_Define_DownloadTask_Class();
    JS_Define_UploadTask_Class();
    JS_RegisterRequestTask(a4, Property, (__int64)"RequestTask", (v8::Isolate *)v29); // 注册类,里面有它的构造函数
    JS_RegisterDownloadTask(a4, Property, (__int64)"DownloadTask", (v8::Isolate *)v29);
    JS_RegisterUploadTask(a4, Property, (__int64)"UploadTask", (v8::Isolate *)v29);
  }
  // ...
}

所以,如果要hook获取它的请求,可以根据构造函数的方式解析,例如:

__int64 __fastcall RequestTask_Init(RequestTask *a1, v8::Isolate *a2, __int64 a3)
{
  // ...
  if ( *(int *)(a3 + 16) <= 0 )
    v14 = *(_QWORD *)(*(_QWORD *)a3 + 8LL) + 632LL;
  else
    v14 = *(_QWORD *)(a3 + 8);
  // 开始一个一个读对象的属性,即构造函数的参数
  v8_url = JS_GetProperty_Value(a2, v14, "url", v54);
  V8_Value_ToString(&url, a2, v8_url);
  // ...
}

微信网关流程

回到tryGateWay这个中间件,它会根据运行时配置、环境信息和请求信息决定使用轻量级网关、微信网关拦截,还是继续由RequestTask处理,这里它要满足多个条件才会使用微信网关:

function tryGateWay(ctx) {
  const globalRuntimeConfig = i; // 运行时配置 (auth_info, prefetch_ip 等)
  const projectConfig = n;       // 项目配置 (domain, env_version 等)
  const systemInfo = l.default;  // 系统信息 (envVersion 等)
  const libInfo = t;             // 基础库信息 (version)
  const liteGatewayRules = o;    // Lite Gateway 规则配置
  const isCloudGatewayEnabled = P; // 是否启用云/网关特性
  let hasGlobalRetryFallback = A;  // 全局重试标记

  if (ctx.args.disableGateway) { // 如果请求参数明确禁用了网关,直接退出
    return;
  }

  if (liteGatewayRules) { // 如果有加速规则会尝试匹配,本文不关注这种情况,简单说下
    try {
      const accelerationResult = tryCalculateLiteGatewayOptions(ctx); // 尝试计算加速配置,它会匹配当前url的转发规则,检查是否满足加速条件(大量检查),如果都满足则会构造加速请求(修改url,注入header等)
      if (accelerationResult && accelerationResult.accelerateOptions.accelerateType === 'liteGatewayAcc') { // 如果成功生成了 Lite Gateway 加速选项,直接返回修改后的请求配置I.JL.liteGatewayAcc
        return accelerationResult;
      }
    } catch (err) {
      // 加速逻辑出错时不阻断流程,静默失败降级
    }
  }

  if (!isCloudGatewayEnabled) { // 如果全局未启用云/网关特性,直接退出
    return;
  }

  if (ctx.host !== projectConfig.domain) { // 分支 A: 请求目标不是网关域名 (可能是云函数或其他请求)
    if (!ctx.options.__skipDomainCheck__ && typeof __cloudSDKMain__ !== "undefined") { // 如果没有跳过域名检查,且云开发SDK(W)存在
      const cloudMiddleware = __cloudSDKMain__.getRequestMiddleware?.(systemInfo)(ctx.args); // 尝试获取云开发的请求中间件

      if (cloudMiddleware) { // 如果中间件返回了处理函数,将其包装为 Promise 返回,用于拦截 wx.request 并转为云函数调用
        return new Promise((resolve, reject) => cloudMiddleware(resolve, reject));
      }
    }
    return;
  }

可见会判断是否要走网关,这里以及接下来网关涉及到的配置会通过__WxConfig/__native_custom_event__wxConfig_inject事件注入:

{
  "appContactInfo": {
    "passThroughInfo": {
      "wxcloud_gateway": { "user_selected": true },
      "realtime_wxa_report_ob_config": {
        "partital_config_list": [],
        "gateway_config": {
          "auth_info": {
            "key": "8n...Ssw==",
            "token": "99_Tqn...Q",
            "iat": 0,
            "exp": 0,
            "rid": 3388496851
          }
        }
      }
    }
    "stablePassThroughInfo": {
      "wxcloud_gateway_conf": {
        "enabled": true,
        "gateway_info": {
          "space_appid": "wx621112590b635086",
          "gateway_id": "1d5e",
          "access_id": "33a7"
        },
        "access_mode": 1,
        "domain": "a1d5e33a7-wx621112590b635086.sh.wxgateway.com",
        "url_prefixes": ["https://api.m.jd.com"],
        "enable_http2": true,
        "enable_quic": true,
        "env_version": "release",
        "gray_scale": 1000,
        "conf_version": "1767076878",
        "use_high_performance_mode": true,
        "url_prefix_configs": [],
        "user_ctrl_list": [],
        "gray_type": "kGrayTypeScale",
        "expt": { "name": "", "value": "" },
        "disable_timeout_retry": true,
        "mini_lib_ver": "3.7.4"
      }
    }
  }
}

注:要对网关进行降级,除了一些传统方法外,还能在这里hook它的配置项,或者拦截请求注入降级响应。

好像在使用微信网关时(响应头返回x-wx-conf-update=1)还能更新配置,没仔细分析。现在继续到WXCloud.js里,微信网关在代码里有v1、v2和v3三个版本,v1主要用来获取配置信息,v3版本只会在初始化时走operatewxdata得到token,之后用这个token直连网关,此时数据会使用AES加密,而v2版本全程走operatewxdata,它是把请求跨进程给微信主进程,由主进程走私有协议发送到微信网关,现在直接看v2,它整个调用过程很复杂,下面简单描述:

// getRequestMiddleware: () => Zd
function Zd(e) {
    // 先判断是否开启了网关(enabled)、环境(开发/正式/体验版)匹配、基础库版本满足、请求的URL前缀匹配
    // 调用LP(e) => { Gateway: { initRequired: !1, fn: ah(e) } }; 
    Jd = Lp(Yd).Gateway.fn({ ...t, authorization: d })
    // 
}
//  // ah调了sh,sh是微信网关的主要实现
class sh extends tu {
    constructor(e) { } // 初始化网关实例,创建 Token 管理器,配置导出 API 和初始授权信息
    refreshToken(e) {} // 刷新网关会话 (Session) 或 Token,通常在 Token 过期或失效时触发
    callV2(e) { } // 使用 V2 协议(隧道模式)发起请求,通过 operateWXData 走微信底层通道透传数据
    callV3(e, t) { } // 使用 V3 协议(直连模式)发起请求,直接向网关 URL (/__wx__/call) 发送封装后的数据
    runDeferTasks() { } // 执行在网关初始化期间或等待 Token 时被挂起(Defer)的任务队列
    formatErrorMessage(e, t) { } // 格式化错误信息,在错误消息后附加当前网关的状态机状态 (FSM) 以便调试
    initGateway() { } // 启动网关初始化流程,包括获取初始 Token、建立 Keepalive 保活连接和预取资源
    call(e) { } // 网关请求的统一入口,根据配置(除非明确指定否则默认v3)和 Token 状态(不存在则v2)智能选择使用 V2 或 V3 协议
    connectSocket(e, t) { } // 通过网关建立 WebSocket 连接 (/__wx__/wss),处理握手鉴权及数据帧的封装与解包
}

// 在callV2里继续跟pn->An->gn->vn->i.request 这里的requets是Je({ cloud: this })),它会调用标准云开发We函数发包,即调用operatewxdata

接着我们跟入Java层,看看operateWXData怎么实现的, operateWXData API处理流程如下:

// JsApiOperateWXData.java
public class JsApiOperateWXData extends BaseAuthJsApi {
    public static final String NAME = "operateWXData";

    @Override
    public void handleAuthRequest(JsInvokeContext ce0, AuthQueueCallback o0) {
        boolean z;
        String s;
        AppBrandComponentBase y0 = (AppBrandComponentBase)ce0.invokeEnv;
        JSONObject jSONObject0 = ce0.data;
        int v = ce0.callbackId;
        try {
            jSONObject0.put("wxdataDequeueTimestamp", System.currentTimeMillis());
            s = jSONObject0.getString("data");
        }
        catch(Exception exception0) {
            n2.e("MicroMsg.AppBrand.JsApiOperateWXData", "Exception %s", new Object[]{exception0.getMessage()});
            y0.a(v, this.o("fail"));
            o0.onQueueStopped();
            return;
        }

        RequestRouteType c0 = RequestRouteType.parseRequestRoutType(jSONObject0);
        // 创建跨进程任务
        JsApiOperateWXData.OperateWXDataTask jsApiOperateWXData$OperateWXDataTask0 = new JsApiOperateWXData.OperateWXDataTask();
        jsApiOperateWXData$OperateWXDataTask0.appId = y0.getAppId();
        jsApiOperateWXData$OperateWXDataTask0.methodName = "operateWXData";
        o0 o00 = y0.getRuntime().g0();
        if(o00 != null) {
            jsApiOperateWXData$OperateWXDataTask0.debugType = o00.r.d;
        }

        AppBrandRuntime appBrandRuntime0 = y0.getRuntime();
        try {
            z = appBrandRuntime0.t0(new JSONObject(s).optString("api_name"));
        }
        catch(JSONException unused_ex) {
            n2.e("MicroMsg.AppBrand.JsApiOperateWXData", "illegal arguments", null);
            z = false;
        }

        jsApiOperateWXData$OperateWXDataTask0.isAsync = z;
        jsApiOperateWXData$OperateWXDataTask0.responseBuilder = this;
        jsApiOperateWXData$OperateWXDataTask0.appBrandContext = y0;
        jsApiOperateWXData$OperateWXDataTask0.data = s;
        jsApiOperateWXData$OperateWXDataTask0.requestRouteType = c0;
        jsApiOperateWXData$OperateWXDataTask0.callbackId = v;
        jsApiOperateWXData$OperateWXDataTask0.taskCallback = new OperateWXDataTaskCallback(jsApiOperateWXData$OperateWXDataTask0, o0);
        jsApiOperateWXData$OperateWXDataTask0.dataMap = new HashMap();
        AppBrandStatObject appBrandStatObject0 = AppBrandBridge.getStatObject(jsApiOperateWXData$OperateWXDataTask0.appId);
        if(appBrandStatObject0 != null) {
            jsApiOperateWXData$OperateWXDataTask0.versionCode = appBrandStatObject0.scene;
        }
        // 根据调用环境:小程序/H5设置客户端版本
        if(y0 instanceof AppBrandService) {
            jsApiOperateWXData$OperateWXDataTask0.clientVersion = 1;
        }
        else if(y0 instanceof AppBrandPageView) {
            jsApiOperateWXData$OperateWXDataTask0.clientVersion = 2;
        }

        int v1 = jSONObject0.optInt("queueLength", -1);
        long v2 = jSONObject0.optLong("wxdataQueueTimestamp", 0x7FFFFFFFFFFFFFFFL);
        long v3 = jSONObject0.optLong("wxdataDequeueTimestamp", 0x7FFFFFFFFFFFFFFFL);
        jsApiOperateWXData$OperateWXDataTask0.queueLength = v1;
        jsApiOperateWXData$OperateWXDataTask0.wxdataQueueTimestamp = v2;
        jsApiOperateWXData$OperateWXDataTask0.wxdataDequeueTimestamp = v3;     
        // 执行跨进程任务
        jsApiOperateWXData$OperateWXDataTask0.executeAsync();
    }
}

这里JsApiOperateWXData.OperateWXDataTask会使用它的IPC跨进程调度,由微信主进程执行它,现在咱直接跳到主进程:

// OperateWXDataTask.java - runTask() 中的网络调用
@Override
public void runTask() {
    // 记录开始时间
    if (this.isConfirmOperation) {
        this.beginCgiTimestampAfterConfirm = System.currentTimeMillis();
    } else {
        this.beginCgiTimestamp = System.currentTimeMillis();
    }

    // 创建回调
    OperateWXDataCallbackImpl callback = new OperateWXDataCallbackImpl(this);

    // 执行网络请求
    if (this.methodName.equals("operateWXData")) {
        this.executeNetScene(this.appId, this.data, "", this.debugType, 
                            this.param2, 0, callback, this.requestRouteType, 
                            this.isSecure, this.isAsync);
    } else if (this.methodName.equals("operateWXDataConfirm")) {
        this.executeNetScene(this.appId, this.data, this.confirmData, 
                            this.debugType, this.param2, this.confirmStatus, 
                            callback, this.requestRouteType, this.isSecure, 
                            this.isAsync);
    }
}

public void executeNetScene(String appId, String data, String confirmData, 
                           int debugType, int param1, int param2, 
                           OperateWXDataCallback callback, 
                           RequestRouteType routeType, 
                           boolean isSecure, boolean isAsync) {
    // 1. 创建网络场景
    NetSceneJSOperateWxData scene = new NetSceneJSOperateWxData(
        appId, data, confirmData, debugType, param2, param1, 
        this.versionCode, routeType, this.sessionId, this.userId, 
        isSecure, isAsync, new OperateWxDataSceneCallback(this, data, callback));

    // 2. 设置客户端版本
    int clientVersion = this.clientVersion;
    JSOperateWxDataRequest request = (JSOperateWxDataRequest)
        scene.request.requestWrapper.requestData;

    if (request.header == null) {
        request.header = new JSRequestHeader();
    }
    request.header.clientVersion = clientVersion;

    // 3. 提交到网络队列
    MMKernel.getNetSceneQueue().doScene(scene);
}

这会交给底层,即libwechatnetwork.so(Mars)库处理:

public class NetSceneQueue {
    public void doScene(NetSceneBase scene) {
        this.checkNetwork() // 1. 检查网络状态
        INetworkDispatcher dispatcher = this.getNetworkDispatcher(); // 2. 获取网络分发器
        dispatcher.dispatch(scene);// 3. 分发网络请求
    }
}

处理完会将结果跨进程返回给小程序。

附:IPC 流程可以总结为以下架构:

┌─────────────────────────────────────────────────────────────────────┐
│                   微信小程序进程 (com.tencent.mm:appbrandN)           │
├─────────────────────────────────────────────────────────────────────┤
│ 1. JsApiOperateWXData.handleAuthRequest()                          │
│ 2. 创建 OperateWXDataTask                                           │
│ 3. OperateWXDataTask.executeAsync()                                │
│ 4. MainProcessTask.executeAsync()                                  │
│ 5. IPCInvoker.invokeAsyncWithTag()                                 │
│ 6. IPCBridgeManager.getIPCBridge()                                 │
│ 7. IPCServiceConnection.bindService()                              │
└────────────────────────────────┬────────────────────────────────────┘
                                 │ Binder IPC
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│                   微信主进程 (com.tencent.mm)                        │
├─────────────────────────────────────────────────────────────────────┤
│ 1. IPCInvokeBridgeStubImpl.Sa()                                    │
│ 2. AsyncInvocationTask.run()                                       │
│ 3. MainProcessTask$AsyncTaskInvoker.invoke()                       │
│ 4. OperateWXDataTask.runTask()                                     │
│ 5. executeNetScene() → NetSceneJSOperateWxData                     │
│ 6. MMKernel.getNetSceneQueue().doScene()                           │
│ 7. NativeNetworkDispatcher.dispatch() → JNI → Native               │
│ 8. 网络请求执行 → 结果返回                                            │
│ 9. OperateWXDataCallbackImpl.onSuccess()/onError()                 │
│10. OperateWXDataTask.onSyncComplete()                              │
│11. 结果通过Binder IPC返回小程序进程                                    │
└─────────────────────────────────────────────────────────────────────┘

风控

小程序运行在一个极端受限的环境里,视图层和逻辑层是完全分开的,JS只能在受限的环境中执行,该环境没有DOM/BOM等,因此大多数浏览器相关的指纹在纯小程序环境里是拿不到的(当然可以通过关键页,如验证码页跳Webview去获取),这里就列下小程序本身提供的可供用作风控指纹的API:

系统信息

wx.getSystemInfo    // brand(品牌) model(型号) pixelRatio(像素比) 屏幕和窗口信息 language(微信设置的语言) version(微信版本) system(操作系统及版本) platform(客户端平台 如ios) fontSizeSetting(用户字体大小) SDKVersion(基础库版本) benchmarkLevel(性能等级) safeArea(安全区域) locationReducedAccuracy(模糊定位) theme(暗黑主题?) host(宿主环境) enableDebug(开启调试) deviceOrientation(设备方向) xxAuthorized(授权状态) xxEnabled(系统开关)...
wx.getDeviceInfo    // platform(客户端平台) abi(微信APP二进制接口类型) deviceAbi(设备二进制接口类型) brand(设备品牌) model(设备型号) cpuType(设备 CPU 型号) system(操作系统及版本) memorySize(设备内存大小) benchmarkLevel(设备性能等级)
wx.getDeviceBenchmarkInfo // benchmarkLevel modelLevel(设备档位,和BenchmarkLevel有映射关系)
wx.getAccountInfoSync   // 当前小程序的appid 版本 版本类型(release) 插件名称和版本
wx.getPerformance   // 获取性能数据
wx.getWindowInfo    // 窗口/屏幕信息
wx.getSkylineInfo   // skyline 支持度/版本
wx.getRendererUserAgent // webview ua
wx.env  // USER_DATA_PATH(用户目录路径 (本地路径))
__wxConfig  // 里面有很多信息,例如envVersion==develop
wx.getLaunchOptionsSync // scene(场景值) path(启动路径) apiCategory query(启动参数)
wx.getAppBaseInfo       // 基础库/微信版本 是否开启调试 语言 字体 ...
wx.getAvailableAudioSources // 支持的音频输入源,如buildInMic手机麦克风 headsetMic耳机麦克风

用户活动信息

包括传感器,是否在录屏,是否在截屏,用户位置,网络信息,连接的Wi-Fi信息,生命周期监控

wx.getAppAuthorizeSetting   // 授权信息 album(相册) bluetooth camera location locationReduce microphone notificationAlert notification notificationBadge notificationSound phoneCalender
wx.getScreenBrightness  // 屏幕亮度
wx.getSystemSetting // 手机的设置 开没开相关功能 bluetooth location wifi deviceOrientation(设备方向)
wx.getLocation  // 经纬度 速度 高度 这些信息的精度
wx.onLocationChange // 实时地理位置变化事件
wx.startLocationUpdate  // 开启小程序进入前台时接收位置消息
wx.startLocationUpdateBackground  // 开启小程序进入前后台时均接收位置消息
wx.checkIsOpenAccessibility // 是否开启了辅助功能

wx.getBeacons   // 周边的蓝牙信息(准确的说是iBeacon设备)
wx.startBeaconDiscovery // 开始搜索附近的iBeacon设备
wx.onBeaconUpdate   // 监听iBeacon设备更新事件
wx.getBluetoothAdapterState // 是否可用 是否正在搜索设备
wx.startBluetoothDevicesDiscovery   // 开始搜寻附近的蓝牙外围设备
wx.onBluetoothDeviceFound   // 寻找到新设备的事件回调
wx.getConnectedBluetoothDevices //根据uuid获取处于已连接状态的设备


wx.getNetworkType   // 网络类型 信号强度 是否有系统代理 是否是弱网环境
wx.onNetworkStatusChange    // 监听网络状态变化
wx.getConnectedWifi // 连接的Wi-Fi的信息
wx.getWifiList  // 获取周边Wi-Fi列表
wx.onGetWifiList    // 监听获取到Wi-Fi列表数据事件
wx.onWifiConnected  // 监听连接上Wi-Fi的事件
wx.onWifiConnectedWithPartialInfo   // 监听连接上Wi-Fi的事件(包含部分信息)
wx.startWifi    // 初始化Wi-Fi模块,为获取Wi-Fi信息做准备
wx.getLocalIPAddress  // 获取局域网IP地址

wx.getHCEState  // 是否支持HCE(NFC)
wx.startHCE // 
wx.getNFCAdapter    // 获取NFC实例

wx.getBatteryInfo // 获取电量/充电状态
App()Page()    // 路由/生命周期等,这里有很多 

wx.getClipboardData //剪切板

wx.startAccelerometer   // 加速度计
wx.onAccelerometerChange
wx.startCompass // 罗盘
wx.onCompassChange
wx.startDeviceMotionListening   // 设备方向
wx.onDeviceMotionChange
wx.startGyroscope   // 陀螺仪
wx.onGyroscopeChange

wx.onUserCaptureScreen  // 截屏
wx.getScreenRecordingState  // 录屏
wx.onScreenRecordingStateChanged

wx.onCopyUrl    // 监听用户点击右上角菜单的「复制链接」按钮时触发的事件
wx.onAppShow
wx.onAppHide
wx.onThemeChange

wx.getExtConfig  // 获取第三方平台自定义的数据字段,用于环境校验

wx.getWeRunData // 运动数据 这个一般获取不到

// 还有就是渲染层绑定的各种输入事件,如触控 输入等

用户信息

小程序本身的信息,账户信息

wx.checkSession // 检查登录态是否过期,这是微信维护的一个关联wx code的会话,如果经常用小程序就不容易过期
wx.getUserInfo  // 用户的各种信息 如昵称 头像 性别 语言等 含签名
wx.getUserProfile   // 获取用户信息(需弹窗授权)

存储

// storage 存储 存储信息
wx.xxStorage[XX]
wx.getStorageInfo   // 所有存的key,存储空间大小
// file 可存放cookie等信息
wx.getFileSystemManager
// 预取
wx.getBackgroundFetchToken

Canvas

// 常量定义
const CANVAS_ID = "fingerprintCanvas";

/**
 * 绘制Canvas并生成MD5指纹
 * @param {Object} canvasParams - 画布参数对象
 * @param {Function} successCallback - 成功回调函数
 * @param {Function} failCallback - 失败回调函数
 * @param {Function} completeCallback - 完成回调函数
 */
function generateCanvasFingerprint(canvasParams, successCallback, failCallback, completeCallback) {
  const performanceMetrics = { startTime: new Date().getTime() };
  const canvasContext = wx.createCanvasContext(CANVAS_ID, this);

  if (!canvasContext) {
    if (failCallback && typeof failCallback === 'function') {
      failCallback({
        errMsg: "Canvas creation failed, Please check if canvasId is consistent"
      });
    }
    return;
  }

  // 设置画布属性
  const platform = canvasParams.pl || "";
  canvasContext.globalCompositeOperation = platform === "android" ? "xor" : "multiply";

  // 绘制基本图形
  drawBasicShapes(canvasContext);
  performanceMetrics.basicShapesTime = new Date().getTime();

  // 绘制文本
  drawTextElements(canvasContext);
  performanceMetrics.textDrawingTime = new Date().getTime();

  // 绘制圆形
  drawCircles(canvasContext);
  performanceMetrics.circlesTime = new Date().getTime();

  // 绘制参数信息
  drawParameterInfo(canvasContext, canvasParams);
  performanceMetrics.parameterDrawingTime = new Date().getTime();

  // 生成图像并计算MD5
  let fingerprintHash = "";
  canvasContext.draw(false, () => {
    wx.canvasToTempFilePath({
      canvasId: CANVAS_ID,
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      destWidth: 100,
      destHeight: 100,
      fileType: "png",
      success: (result) => {
        try {
          performanceMetrics.drawTime = new Date().getTime();
          const fileData = wx.getFileSystemManager()
                            .readFileSync(result.tempFilePath, "base64");
          fingerprintHash = md5.hexMD5(fileData);
          performanceMetrics.md5Time = new Date().getTime();
          canvasParams.canvasPerformance = performanceMetrics;

          if (successCallback) {
            successCallback(fingerprintHash);
          }
        } catch (error) {
          if (successCallback) {
            successCallback("");
          }
        }
      },
      fail: () => {
        if (successCallback) {
          successCallback("");
        }
      },
      complete: (result) => {
        if (completeCallback) {
          completeCallback(result);
        }
      }
    });
  });
}

// 辅助函数:绘制基本图形
function drawBasicShapes(context) {
  context.rect(0, 0, 10, 10);
  context.rect(2, 2, 6, 6);
  context.textBaseline = "alphabetic";
  context.fillStyle = "#f60";
  context.fillRect(32, 1, 62, 20);
}

// 辅助函数:绘制文本元素
function drawTextElements(context) {
  context.fillStyle = "#069";
  context.fillText("Cwwm aa fjorddbank glbyphs veext qtuiz, 😃", 2, 15);

  context.fillStyle = "rgba(102, 204, 0, 0.2)";
  context.font = "18pt Arial";
  context.fillText("Cwwm aa fjorddbank glbyphs veext qtuiz, 😃", 4, 45);

  context.fillStyle = "rgb(255,0,255)";
}

// 辅助函数:绘制圆形
function drawCircles(context) {
  let x = 16, y = 16;
  context.beginPath();
  context.arc(x, y, 16, 0, 2 * Math.PI, true);
  context.closePath();
  context.fill();

  x = 32, y = 16;
  context.fillStyle = "rgb(0,255,255)";
  context.beginPath();
  context.arc(x, y, 16, 0, 2 * Math.PI, true);
  context.closePath();
  context.fill();

  x = 24, y = 32;
  context.fillStyle = "rgb(255,255,0)";
  context.beginPath();
  context.arc(x, y, 16, 0, 2 * Math.PI, true);
  context.closePath();
  context.fill();

  x = 24, y = 24;
  context.fillStyle = "rgb(255,0,255)";
  context.arc(x, y, 24, 0, 2 * Math.PI, true);
  context.arc(x, y, 8, 0, 2 * Math.PI, true);
  context.fill();

  context.beginPath();
  context.textBaseline = "top";
  context.font = "14px Arial";
  context.fillStyle = "#000";
  context.closePath();
}

// 辅助函数:绘制参数信息
function drawParameterInfo(context, params) {
  const parameterKeys = ["cpi", "br", "mo", "pr", "pl", "sh", "sw", "hh"];

  parameterKeys.forEach((key, index) => {
    const value = params[key] || "";
    context.fillText(value, 0, 15 * (index + 1));
  });
}

Webgl

function getWebGLRendererInfo(infoKey) {
  try {
    // 创建离屏WebGL上下文
    const offscreenCanvas = wx.createOffscreenCanvas({ 
      type: "webgl", 
      width: 0, 
      height: 0 
    });
    const glContext = offscreenCanvas.getContext("webgl");

    if (!glContext) {
      console.error("Failed to get WebGL context");
      return;
    }

    // 获取调试扩展
    const debugExtension = glContext.getExtension("WEBGL_debug_renderer_info");
    if (!debugExtension) {
      console.error("WEBGL_debug_renderer_info extension not supported");
      return;
    }

    // 获取渲染器信息 如GPU类型
    const rendererInfo = glContext.getParameter(
      debugExtension.UNMASKED_RENDERER_WEBGL
    );

    // 存储信息
    storeDeviceInfo({ 
      value: rendererInfo, 
      key: infoKey 
    });

  } catch (error) {
    console.error("Error while getting WebGL renderer info:", error);
  }
}

Audio

wx.createWebAudioContext // 同浏览器

加密管理器

wx.getUserCryptoManager可获取用户加密模块,它能用于建立加密网络通道,简单的说它由微信官方维护key和iv(getLatestUserKey),客户端利用该信息加密数据,服务端再获取到加密数据后同时向微信官方获取key和iv来解密数据,这能(一定程度)被用于对抗一些接码方案。

安全网关

小程序可一键配置微信网关,配置后原始wx.request等的请求将不会再直接到目标服务器,而是通过专有协议到网关服务器*wxgateway*,再由网关添加一些设备信息、用户信息、风控信息与私有数据后再转交给目标服务器,这会导致抓包困难,对抗也会升级到微信APP级别。

关于它的介绍可以看微信网关-产品简介,按文档它主要有两个作用,在弱网环境提升可用性与安全防护,其实俺觉得安全防护才是使用者最看重的能力,使用微信网关后,请求流量路径如下:

可以看到它不是直接到业务服务器,而是先走网关服务器做中转再发出;上面已经提到小程序本身提供的、可用于风控分析的能力十分有限,而走网关服务器后,就可以借助微信主体的风控能力加强业务的安全性: 1.链路加密:在之前小程序直接向业务服务器发包,这个流量很容易被中间人劫持,只能依靠业务自建数据加密,但小程序本身破解难度较小所以整个链路安全上限较低,而使用微信网关后,不再有明文流量直接给业务服务器了,具体来说: - v2版本中,HTTP请求不再通过传统的接口发送(如cronet的RequestTask),而是使用operatewxdataapi,它会使用binder将数据发送给微信主体,微信主体会使用mars框架发送,这里面会使用它私有的协议(mmtls),存抓包难以解密 - v3版本中,尽管它会使用传统接口发HTTP请求,但是整个请求和响应都会使用AES加密,而加密密钥会使用operatewxdata传递,又回到上一步了

2.风控信息增强:小程序在受限的环境中执行,它只能获取很有限的信息,这很容易被黑灰产对抗掉;相反微信是在正常的android/ios等中执行,它本身能收集大量信息,并且这已经被建设为微信风控系统了。现在请求由网关转发,网关就可以作为一个防火墙配合微信风控系统的能力对请求做过滤或是加上额外的风控建议信息再发给业务服务器,此时的对抗一定程度上变为和微信风控系统做对抗,难度极具上升

3.对抗身份伪造:在传统的小程序授权机制中,是小程序调用wx.login获取wxcode并发送给业务服务器,业务服务器根据此code向微信服务器获取用户信息,这个过程伪造难度较小;在使用微信网关时可通过x-wx-include-creadentials: openid,unionid请求头让网关自动带上微信所对应用户id,增大伪造难度

环境真实性

  1. hook检测,检测关键函数是否被hook
  2. 版本与平台检测,不同版本和平台对API的支持情况不同,可通过canIUse等检测,基础库版本可直接对比JS

网络流量

就是JA3,Akamai等协议上的指纹特征咯~

群控方案

群控小程序,需要关注:多开、抓包、自动化、指纹等,WX的端有很多,所以有丰富的选择:

1.PC端微信运行小程序,特别是Mac上的HOOK起来特别方便(ObjC),可直接单微信多开小程序,而且退一步直接多开微信也很简单

2.安卓上,由于前台只能运行一个应用,需要安卓群控(如魔云腾),可对WX魔改直接起小程序,省去了登录的麻烦

3.基于微信开发者工具:好处当然是完全js实现,修改方便易于定制化,但缺点是特征太多需要对抗,而且它本身只能运行有源码的程序,这里也得打补丁

4.基于wmpf开发:wmpf本身目的就是为了让小程序运行时脱离微信,以便拓展其使用范围,但当前其官方只放开了Android版的实现,我们需要对其进行修改(不必对抗微信的风控了),以便实现多开与绕过权限限制,在多开时可考虑windows的安卓子系统

image-20230923165623316

可学习wechat-web-devtools-linux

其它

打开小程序的指定url

1.利用小程序跳小程序,跳转完到指定小程序可生成短链接

2.利用微信浏览器(H5)跳小程序,利用wx-open-launch-weapp跳转

3.利用app直接生成相关卡片(需要APP实现了该功能)

4.直接在小程序里打开指定域名(需要指定域名可控,不现实)

5.利用微信直接生成卡片(需要分析微信:HOOK/改数据库)

6.利用小程序本身调用服务端的生成短链接能力(需要分析它怎么调用的,已知服务端利用getUnlimitedQRCode接口实现的)

参考

[0] A Measurement Study of Wechat Mini-Apps

[1] 微信小程序源码阅读笔记1

[2] 企业微信超大型工程-跨全平台UI框架最佳实践

[3] 微信小程序技术原理分析

social