# iOS 原生插件开发指引

# 一、开发环境准备

  1. 根据原生插件工具操作指引生成一个多端插件工程项目

  2. 安装 Xcode,准备好 iOS 开发环境

  3. iOS SDK >= 1.1.0

  4. 开发者工具 Nightly >= 1.06.2311282

# 二、插件工程介绍

  • 下面以工具生成的多端插件工程项目为例:

  • 在 Finder 中打开多端插件工程目录下的 ios 文件夹,双击点击 NativePlugin.xcodeproj 即可打开插件工程

  • 打开后的效果如下:

# 工程目录结构说明


    |-- demo // 开发插件时的示例 App 工程,但在多端插件开发中并无实际使用到,开发者无需改动此文件夹的内容
    |-- MyPlugin // 插件主要内容,开发者的改动应在此目录下进行
        |-- WeAppNativePlugin.framework // 多端插件与多端核心 SDK 通信依赖的静态库
        |-- MyPlugin.h
        |-- MyPlugin.mm
    |-- resources // 调试插件时需要用到的资源目录
        |-- arm64 // 用于真机调试
        |-- x86 // 用于模拟器调试
        |-- script // 用于开发调试的辅助脚本
    

# 工程 Xcode 配置说明

以下配置在创建 iOS 插件工程时均已设置完成,开发者不应随意改动

  • Demo BundleId:需与对应的多端应用绑定的移动应用的 iOS 信息匹配

    多端应用绑定的移动应用的 iOS 信息如下图:

    插件的 bundleId 在项目创建时已自动生成,开发者可自定义修改

  • Scheme:scheme 名称固定为 plugin 以及插件构建产物名称固定格式为 ${pluginId}.framework

  • Build Phases:Run Script 阶段处理了使用插件构建产物的 demo App,这里的顺序与脚本内容开发者都不应该改动。

# 运行

按照上述的指引完成配置后,即可在微信开发者工具中开始运行和调试。

  1. 在微信开发者工具切换至「多端插件模式」后,选择「iOS」点击「运行。工具将自动打开 Xcode 工程。

  2. 在打开的 Xcode 中点击「运行」按钮,即可看到示例工程运行效果

  3. 在 Xcode 模拟器上,先点击「加载多端插件」,加载成功后再点击「调用多端插件」,即可通过 vconsole 看到插件方法的返回。

    注:如果遇到 M1 电脑模拟器按钮点击无响应,可查看指引

  4. 后续开发者修改小程序代码、project.miniapp.json 中的配置,仅需保持工具开启状态;工具会与 Xcode 通信完成对应的资源替换,开发者只需要专注在 Xcode 重新编译运行即可。在 Xcode build log 中,可以看到 Xcode 与工具通信的日志。

    工具的构建面板也有相关的日志输出:

    如果开发者发现资源替换未生效,Xcode build log 中与没有工具通信的日志、或者与工具通信失败,请自查:

    • 工具版本 Nightly >= 1.06.2311282
    • 工具「设置」->「安全设置」中已开启多端插件服务端口
    • 插件模板已更新至最新(可重新创建插件项目)

# 三、插件开发

# 多端应用与多端插件的调用时序介绍

在开始 iOS 多端插件的开发前,理解多端应用与多端插件的调用时序将更好帮助开发者进行开发。

sequenceDiagram
    participant app as 多端应用
    participant plugin as 多端插件

    app->>app:wx.miniapp.loadNativePlugin 加载插件
    app->>plugin: 加载插件 framework
    plugin->>plugin: 载入时即实例化插件对象
    app->>plugin: 加载插件,进行一系列初始化操作(包括 id 注册、插件方法注册、插件 initPlugin 方法调用)
    app->>app: 持有插件实例 pluginInstance
    note over app,plugin: 插件加载完成
    app->>plugin: pluginInstance.func() 进行插件方法的调用

# 插件开发

在环境准备中创建的 iOS 插件工程模板,已替开发者完成:

以下只有 插件实例化注册插件 Id 是必需操作,剩余操作开发者可按自身需求进行调整。

# 1. 插件实例化

__attribute__((constructor))
static void initPlugin() {
    [MyPlugin registerPluginAndInit:[[MyPlugin alloc] init]];
};

# 2. 注册插件 Id

使用宏方法 WEAPP_DEFINE_PLUGIN_ID(pluginId) 注册插件 Id

WEAPP_DEFINE_PLUGIN_ID(YOUR_PLUGIN_ID)

# 3. 定义插件实例方法 initPlugin

// 插件初始化方法,在注册插件后会被自动调用
- (void)initPlugin {
    NSLog(@"initPlugin");
}

# 4. 注册同步方法

  • 注册方法

使用宏方法 WEAPP_EXPORT_PLUGIN_METHOD_SYNC(methodName, methodSelector) 注册同步方法。

参数 类型 必填 说明
methodName 在 js 层可用获取的 pluginInstance.methodName 进行方法调用
methodSelector SEL MyPlugin 定义的插件实例方法 Selector
  • 同步方法的入参与出参
参数 类型 必填 说明
入参 NSDictionary * 在 js 层调用 pluginInstance.methodName 时传入的 Object 类型参数
出参 可序列化类型 在 js 层调用 pluginInstance.methodName 的返回
  • 代码示例

OC 侧的插件同步方法实现

WEAPP_EXPORT_PLUGIN_METHOD_SYNC(mySyncFunc, @selector(mySyncFunc:))

- (id)mySyncFunc:(NSDictionary *)param {
    NSLog(@"mySyncFunc %@", param);
        
    return @"mySyncFunc";
}

JS 侧的方法调用


wx.miniapp.loadNativePlugin({
    pluginId: 'YOUR_PLUGIN_ID',
    success: (plugin) => {
        console.log('load plugin success')
        const ret = plugin.mySyncFunc({ a: 'hello', b: [1,2] })
        console.log('mySyncFunc ret:', ret)
    },
    fail: (e) => {
        console.log('load plugin fail', e)
    }
})

# 5. 注册异步方法

  • 注册方法

使用宏方法 WEAPP_EXPORT_PLUGIN_METHOD_ASYNC(methodName, methodSelector) 注册异步方法。

参数 类型 必填 说明
methodName 在 js 层可用获取的 pluginInstance.methodName 进行方法调用
methodSelector SEL MyPlugin 定义的插件实例方法 Selector
  • 异步方法的入参与出参
参数 类型 必填 说明
入参1 NSDictionary * 在 js 层调用 pluginInstance.methodName 时传入的第 1 个 Object 类型参数
入参2 WeAppNativePluginCallback 在 js 层调用 pluginInstance.methodName 时传入的第 2 个 Function 类型参数,在 OC 侧可通过该入参回调给 js 层。该回调方法支持一个可序列化类型的参数
  • 代码示例

OC 侧的插件异步方法实现

WEAPP_EXPORT_PLUGIN_METHOD_ASYNC(myAsyncFuncwithCallback, @selector(myAsyncFunc:withCallback:))

- (void)myAsyncFunc:(NSDictionary *)param withCallback:(WeAppNativePluginCallback)callback {
    NSLog(@"myAsyncFunc %@", param);

    callback(@{ @"a": @"1", @"b": @[@1, @2], @"c": @3 });
}

JS 侧的方法调用


wx.miniapp.loadNativePlugin({
    pluginId: 'YOUR_PLUGIN_ID',
    success: (plugin) => {
        console.log('load plugin success')
        plugin.myAsyncFuncwithCallback({ a: 'hello', b: [1,2] }, (ret) => {
            console.log('myAsyncFuncwithCallback ret:', ret)
        })
    },
    fail: (e) => {
        console.log('load plugin fail', e)
    }
})

# 6. 注册 AppDelegate 方法

多端 iOS 插件现在支持监听 AppDelegate 方法

  • application:openURL:options:
  • application:continueUserActivity:restorationHandler:
  • application:didFinishLaunchingWithOptions:
  • application:didRegisterForRemoteNotificationsWithDeviceToken:
  • application:didFailToRegisterForRemoteNotificationsWithError

使用方法

  1. 开发者可通过调用继承于 WeAppNativePlugin 的方法 registerAppDelegateMethod: 来注册监听。
  2. 监听方法是插件对象的实例方法,其中方法签名与需监听方法保持一致,
// 在 initPlugin 中注册监听
- (void)initPlugin {
    NSLog(@"initPlugin");
    [self registerAppDelegateMethod:@selector(application:openURL:options:)];
    [self registerAppDelegateMethod:@selector(application:continueUserActivity:restorationHandler:)];
}

// 当 App 通过 URL Scheme 被打开时,将回调到此处
- (void)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
    NSLog(@"url scheme");
}

// 当 App 通过 Universal Link 被打开时,将回调到此处
- (void)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *__nullable restorableObjects))restorationHandler {
    NSLog(@"universal link");
}

# 7. 插件事件监听

从 iOS SDK >= 1.2.5 开始,支持在 JS 侧监听来自 Native 的事件。

  • 在 Native 侧通过调用继承于 WeAppNativePlugin 的方法 sendMiniPluginEvent: 可向 JS 侧发送事件。
参数 类型 必填 说明
param NSDictionary * 在 js 侧的监听方法获得的入参
  • 在 JS 侧可使用插件实例的 onMiniPluginEvent 方法注册监听
参数 类型 必填 说明
callback Function Native 侧向 JS 侧发送事件时触发的回调,支持注册多个回调
  • 在 JS 侧可使用插件实例的 offMiniPluginEvent 取消监听。
参数 类型 必填 说明
callback Function 取消监听;当未指定需要取消的回调时,取消所有监听回调
  • 代码示例

OC 侧的插件异步方法实现


- (void)sendMsg {
    [self sendMiniPluginEvent:@{ @"msg": @"this is an event from plugin" }];
}

JS 侧的方法调用


const listener1 = (param) => {
  console.log('onMiniPluginEvent listener1', param)
}

const listener2 = (param) => {
  console.log('onMiniPluginEvent listener2', param)
}

wx.miniapp.loadNativePlugin({
    pluginId: 'YOUR_PLUGIN_ID',
    success: (plugin) => {
        plugin.onMiniPluginEvent(listener1)
        plugin.onMiniPluginEvent(listener2)
    }
})

# 8. 插件资源拷贝到主包

一般情况下,插件依赖的资源文件是一并打包放置在动态库中。如果开发者有需求将资源文件直接放置在主包中,可在插件项目的 MiniPlugin.bundle/PluginConfig.plist 里填写配置项 CopyResourcesToMainBundle。如下图所示,在多端应用构建时会把 MiniPlugin.bundle/resource.txt 拷贝到构建生成的 IPA 的一级目录下。

注:需要拷贝到主包的资源文件需放置在 MiniPlugin.bundle 中。

# 9. 将小程序的资源拷贝到插件中

使用插件的时候,可以将小程序目录下的资源拷贝到使用的插件内。

project.miniapp.json文件的可视化配置中,在使用的插件下可以看到“将文件添加到原生插件中”,配置相对于小程序项目根路径的相对路径。

# 10. 启动即加载插件

一般使用插件的时候,只有当loadNativePlugin才会动态连接插件。但是部分场景需要app一启动就加载插件。

在开发阶段, 在插件的小程序项目project.miniapp.json中,配置loadWhenStart:true。即可模拟App启动即加载

  "mini-plugin": {
    "ios": {
      "loadWhenStart": true
    }
  }

在使用阶段,project.miniapp.json文件的可视化配置中,在使用的插件下可以看到“是否在App启动的时候就自动加载“。

工具在1.06.2405222版本开始支持

# 11. App Extensions

多端插件开发支持App Extensions。比如消息通知扩展,分享组件,桌面小控件等。xcode 的原生开发项目中,本身支持开发 appex 产物

但是为了能在多端框架中使用,需要将 appex 编译的 schemetarget 名字设置为plugin id 开头的内容。在构建plugin的时候会将编译的appex产物上传。

schemes

targets

在最终使用的时候,这些 appex 都需要进行签名。所以使用插件的小程序项目需要去苹果后台配置对应的 bundle id 和 profiles,并添加到project.miniapp.json中。

  "mini-plugin": {
    "ios": [
      {
        "open": true,
        "pluginId": "wxaaaaaaaaaaaa",
        "pluginVersion": "1.0.0",
        "isFromLocal": false,
        "loadWhenStart": true,
        "appexProfiles": {
          "NSE": {
            "enable": true,
            "bundleID": "xxxx.xxx.xxx.xxx",
            "profilePath": "xxxx/xxxx/开发用的.mobileprovision",
            "distributeProfilePath": "xxxx/xxxx/分发用的证书.mobileprovision"
          },
          "ShareExt": {
            "enable": true,
            "bundleID": "xxxx.xxx.xxx.xxx",
            "profilePath": "xxxx/xxxx/开发用的.mobileprovision",
            "distributeProfilePath": "xxxx/xxxx/分发用的证书.mobileprovision"
          }
        },
        "resourcePath": "pluginResources/wxaaaaaaaaaaaa.json"
      }
    ],
    "android": []
  }

工具在1.06.2405222版本开始支持

# 构建

插件调试完毕之后,开发者需要构建出 ${pluginId}.framework 动态库供多端应用使用。

工具中提供了「构建 iOS 插件产物」的按钮,用于帮助开发者快速构建支持 arm64 的动态库到规定的上传目录中。iOS 插件产物规定放置于目录 build/ios 下,用于后续上传动态库。

如开发者有更加个性化的构建流程,可自行构建 iOS 插件产物,并将插件产物置于规定目录下。

# 构建失败定位

在工具中使用「构建 iOS 插件产物」,本质是调用 xcode 命令帮助开发者构建。当遇到工具构建失败时,开发者可在 ios 目录下使用以下构建脚本自行定位失败原因:

# 修改成自己的插件id
PLUGIN_ID="YOUR_PLUGIN_ID"

TEMP_BUILD_DIR=$(mktemp -d)

xcodebuild clean -project NativePlugin.xcodeproj -scheme plugin -derivedDataPath $TEMP_BUILD_DIR

xcodebuild -project NativePlugin.xcodeproj -scheme plugin -destination generic/platform=iOS -derivedDataPath $TEMP_BUILD_DIR -configuration Release ARCHS=arm64 ONLY_ACTIVE_ARCH=NO

xcodebuild -project NativePlugin.xcodeproj -scheme plugin -destination "generic/platform=iOS Simulator" -derivedDataPath $TEMP_BUILD_DIR -configuration Release ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO

FAT_PATH=$TEMP_BUILD_DIR/Build/Products
LINKMAP_PATH=$TEMP_BUILD_DIR/Build/Intermediates.noindex
SIMULATOR_LIB_PATH=$FAT_PATH/Release-iphonesimulator
IPHONE_LIB_PATH=$FAT_PATH/Release-iphoneos

lipo -create $IPHONE_LIB_PATH/$PLUGIN_ID.framework/$PLUGIN_ID $SIMULATOR_LIB_PATH/$PLUGIN_ID.framework/$PLUGIN_ID -output $FAT_PATH/$PLUGIN_ID

# 四、使用 CocoaPods 管理项目依赖

开发者可参考使用 CocoaPods 开发 iOS 插件

# 五、注意事项