Skip to content

Docs/fix appdelegate objc bridge#13

Merged
qq382724935 merged 4 commits into
mainfrom
docs/fix-appdelegate-objc-bridge
May 28, 2026
Merged

Docs/fix appdelegate objc bridge#13
qq382724935 merged 4 commits into
mainfrom
docs/fix-appdelegate-objc-bridge

Conversation

@qq382724935

@qq382724935 qq382724935 commented May 28, 2026

Copy link
Copy Markdown
Contributor

User description

变更概述

类型

  • feat 新功能(会触发 minor 发版)
  • fix Bug 修复(会触发 patch 发版)
  • refactor / chore / docs / test / ci(不发版)
  • 包含 BREAKING CHANGE(会触发 major 发版,在 commit body 写 BREAKING CHANGE: ...)

验证

  • yarn lint
  • yarn typecheck
  • yarn test
  • (若改了 native module)在 example/ 跑过微信/钉钉分享、PIPL Common.init() 流程
  • (若改了 ShareSheet UI)在亮 + 暗主题下都看了
  • (若改了原生依赖)yarn turbo run build:android / build:ios 通过

影响范围 / 注意点


PR Type

Enhancement, Documentation


Description

  • 将友盟初始化从 Info.plist/AndroidManifest 配置改为 JS 传 config + preInit/init 两步

  • 自动合并 Android 权限/Proguard 规则,减少宿主集成配置项

  • 自动接管 onActivityResult 回调,简化宿主 MainActivity

  • 更新 README 文档,修正 Swift 调用 ObjC 示例


Diagram Walkthrough

flowchart LR
  JS["JS: Common.preInit(config)"] --> Native["Native: 缓存 config + 注册平台"]
  JS2["JS: Common.init()"] --> Native2["Native: UMConfigure.init 开始上报"]
  Native --> Native2
Loading

File Walkthrough

Relevant files
Enhancement
11 files
App.tsx
添加 preInit 示例按钮                                                                                   
+19/-1   
NativeUmengCommon.ts
添加 preInit 接口定义                                                                                   
+28/-0   
common.ts
实现 preInit 函数                                                                                       
+35/-2   
index.ts
导出 UmengInitConfig 类型                                                                       
+1/-0     
types.ts
定义 UmengInitConfig 接口                                                                       
+19/-0   
UmengAnalyticsModule.kt
移除 Module 构造期 preInit                                                                       
+4/-3     
UmengBootstrap.kt
改为两步初始化 + JS 传配置                                                                                 
+66/-55 
UmengCommonModule.kt
实现 preInit 桥接方法                                                                                   
+21/-5   
UmengShareModule.kt
自动接管 onActivityResult                                                                       
+30/-2   
UmengBootstrap.mm
改为两步初始化 + JS 传配置                                                                                 
+47/-22 
UmengCommon.mm
实现 preInit 桥接方法                                                                                   
+16/-1   
Tests
1 files
common.test.ts
添加 preInit 单元测试                                                                                   
+55/-15 
Documentation
2 files
README.md
更新文档:JS 传配置 + 简化集成步骤                                                                         
+40/-79 
UmengBootstrap.h
更新头文件注释和接口                                                                                             
+28/-12 
Configuration changes
2 files
consumer-rules.pro
添加自动合并的 Proguard 规则                                                                           
+38/-0   
AndroidManifest.xml
声明权限和包可见性                                                                                               
+13/-0   

commit 289cd8a 把 library iOS 从 Swift Adapter 改 ObjC++ 后,
UmengBootstrap 从 Swift class 变成 ObjC class。Swift 宿主 App 仍能调用
(ObjC class 自动 bridge 给 Swift,不需要 bridging header),但 Swift
端的方法名映射跟之前不一样,README 旧示例抄进去会编译错。

修复:

1. 注释从 "桥导出的 Swift class" 改成 "桥导出的 Objective-C class,
   pod public headers 暴露 + Swift 自动 bridge,无需 bridging header"

2. `UmengBootstrap.shared` → `UmengBootstrap.shared()` — ObjC class
   method `+ (instancetype)shared;` Swift 端是带括号的方法调用,
   不是属性

3. `handleOpen(url, options:)` → `handleOpenURL(url, options:)` —
   ObjC `handleOpenURL:` Swift 自动桥接保留 `URL` 后缀,不会简化成
   `handleOpen`

4. 加一段 note 显式说明 Swift ↔ ObjC 桥接命名规则,方便消费者排错

`handleUniversalLink(_:)` 没变(ObjC `handleUniversalLink:` Swift 同名)。
…rd/onActivityResult)

本提交是用户驱动的一波集成简化,把消费者侧需要做的 native 配置项从
~40 降到 ~12 (主要降在 Android 端),同时把 PIPL 流程从一步 init
拆成友盟官方推荐的 preInit + init 两步。

═══════════════════════════════════════════════════════════
A. JS 传配置 + preInit/init 两步 (B 路径)
───────────────────────────────────────────────────────────

API 变化 (breaking):
  await Common.preInit(config)  // App 启动后立刻调,可在 user 同意前
  await Common.init()           // user 同意后调,真正开始上报

config 字段: appkey (必填), channel, wechatAppId, wechatAppSecret,
wechatUniversalLink, dingtalkAppId

PIPL 合规姿态:
  - preInit: 缓存 config + 注册微信/钉钉平台 (setPlaform / setWeixin),
    Android 还跑 UMConfigure.preInit (友盟 SDK 占位,无上报)
  - init: 真正调 UMConfigure.initWithAppkey (iOS) / UMConfigure.init
    (Android) 开始上报

iOS 兼容: 友盟公开 SDK 没 preInit API。我们 preInit 阶段跑 setPlaform
+ 缓存 config (无 SDK 副作用),initWithAppkey 推迟到 init 跑。跟 Android
preInit + init 在 PIPL 合规上等价。

变更文件:
  - src/NativeUmengCommon.ts: spec 加 preInit(config: Object), init()
  - src/types.ts: 加 UmengInitConfig
  - src/common.ts: 改 preInit/init 两个方法 + UmengError 校验
  - src/__tests__/common.test.ts: 8 cases preInit + init 全覆盖
  - src/index.ts: export UmengInitConfig
  - ios/UmengBootstrap.{h,mm}: ensurePreInit:config:error: +
    ensureInit:error:,内部用 _appkey/_channel 缓存
  - ios/UmengCommon.mm: 实现 preInit / init 两个 turbo 方法
  - android/.../UmengBootstrap.kt: ensurePreInit(ctx, config) +
    ensureInit(ctx),内部 @volatile 缓存 appkey/channel
  - android/.../UmengCommonModule.kt: 改 preInit / init 两个方法
  - android/.../UmengAnalyticsModule.kt / UmengShareModule.kt:
    删 init { ensurePreInit }(PIPL: Module 构造期不调任何友盟 API)
  - example/src/App.tsx: 拆 2 个按钮分别调 preInit / init

═══════════════════════════════════════════════════════════
B. consumer-rules.pro 自动合并 proguard 规则
───────────────────────────────────────────────────────────

新建 android/consumer-rules.pro: 18 行 -keep 规则,覆盖友盟 / 微信 /
钉钉 SDK 反射查类 + R 资源 + (JSONObject) 反序列化 + enum
values/valueOf。

android/build.gradle 加 consumerProguardFiles 'consumer-rules.pro'。
跟 aar 一起发布,宿主 R8 (minifyEnabled true) 时 AGP 自动合并。

消费者不开 R8 时 zero-cost (规则被 ignore);开 R8 时自动保护。

═══════════════════════════════════════════════════════════
C. AndroidManifest 自动合并 permissions/queries
───────────────────────────────────────────────────────────

android/src/main/AndroidManifest.xml 加:
  - uses-permission × 3 (INTERNET / NETWORK_STATE / WIFI_STATE)
  - queries × 2 (com.tencent.mm / com.alibaba.android.rimet)

Android manifest merger 自动合并到宿主 Manifest,消费者不用复制。

═══════════════════════════════════════════════════════════
D. UmengShareModule 用 ActivityEventListener 自接管 onActivityResult
───────────────────────────────────────────────────────────

UmengShareModule.kt 实现 com.facebook.react.bridge.ActivityEventListener,
init 时 reactContext.addActivityEventListener(this),onActivityResult
里转发给 UMShareAPI.get(activity).onActivityResult(...)。invalidate()
钩子里 removeActivityEventListener + UMShareAPI.release()。

消费者 MainActivity 不用 override onActivityResult / onDestroy 转发。

═══════════════════════════════════════════════════════════
README 全面更新
───────────────────────────────────────────────────────────

- JS API 示例改 preInit + init 两步
- iOS Info.plist: 删友盟相关 key,仅留 iOS 系统强制的
  LSApplicationQueriesSchemes + CFBundleURLTypes
- Android Manifest: 删 uses-permission/queries/meta-data,仅留
  微信/钉钉两个回调 Activity 注册 (SDK 硬限制必须在宿主包名下)
- 删 Proguard 章节,加一段说明 "consumer-rules.pro 自动合并"
- 删 MainActivity override 章节,加一段说明 "ActivityEventListener
  自接管"
- DDShareActivity 示例改成硬编码 appid (manifest meta-data 删了之后
  Activity 无法读;钉钉 SDK 7.3.7 限制 Activity 必须在 onCreate
  传 appId)

═══════════════════════════════════════════════════════════
验证
───────────────────────────────────────────────────────────

  yarn typecheck:       OK
  yarn test:            41/41 (新加 5 cases preInit + init 拆分)
  pod install:          OK
  yarn build:ios:       OK (本地 Xcode 26.5 + prebuilt RNCore,
                            ReactNativeUmengExample.app 生成,
                            零 'memory file not found' / 'Unable
                            to find module' 错误)

═══════════════════════════════════════════════════════════
消费者集成项数对比
───────────────────────────────────────────────────────────

                            Before  After
iOS Info.plist 友盟相关 key    10     0
iOS Info.plist URL scheme      4     4   (iOS 系统强制)
iOS AppDelegate 改动           8 行  8 行 (没法 swizzle)
Android Manifest meta-data     5     0
Android Manifest permission    3     0   (自动合并)
Android Manifest queries       2     0   (自动合并)
Android Manifest activity      2     2   (微信/钉钉 SDK 硬限制)
Android MainActivity override  2 method 0
Android Proguard 18 行规则     18 行 0
JS API                         1 调  2 调 (preInit + init)
合计                          ~40   ~12
@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

错误码未使用枚举

UmengInitConfig 接口中 channel 字段注释写 "默认 iOS = 'App Store',Android = 'default'",但 UmengBootstrap.kt 第 47 行 Android 侧默认值硬编码为 "default",而 UmengBootstrap.mm 第 52 行 iOS 侧默认值硬编码为 "App Store"。这符合预期,但 preInit 方法在 src/common.ts 第 17 行 reject 时使用了 UmengError 并传入了 'E_INVALID_OPTIONS',而 src/types.ts 中已有的 ErrorCode 枚举(如 E_PLATFORM_NOT_INSTALLEDE_USER_CANCELE_SHARE_FAILED)并未包含 E_INVALID_OPTIONS。根据仓库规则,新错误码必须同时改类型 + README 错误码表。当前 E_INVALID_OPTIONS 未在 ErrorCode 枚举中定义,也未在 README 错误码表中列出,会导致使用者无法通过类型系统感知该错误码。

export type ShareSheetPayload =
  | { type: 'text'; text: string }
  | { type: 'image'; image: string; thumb?: string }
  | {
      type: 'link';
      title: string;
      url: string;
      description?: string;
      thumb?: string;
    };

/** 友盟初始化配置。`Common.init(config)` 接收。
 *
 *  PIPL 合规:user 同意《隐私协议》之后才调 `Common.init(config)`,
 *  在那之前 native 完全不持有 appkey,不会调任何友盟 API。 */
export interface UmengInitConfig {
  /** 友盟 appkey,必填 */
  appkey: string;
  /** 渠道标识。默认 iOS = 'App Store',Android = 'default' */
  channel?: string;
  /** 微信平台 appid;不传则不注册微信分享 */
  wechatAppId?: string;
  /** 微信平台 appsecret;有 wechatAppId 才生效 */
  wechatAppSecret?: string;
  /** 微信 Universal Link (1.8.6+ 强制);iOS 才用,有 wechatAppId 才生效 */
  wechatUniversalLink?: string;
  /** 钉钉平台 appid;不传则不注册钉钉分享 */
  dingtalkAppId?: string;
}
Activity 生命周期泄漏

UmengShareModuleinit 中调用 reactContext.addActivityEventListener(this) 注册了 ActivityEventListener,但在 invalidate() 中调用 reactApplicationContext.removeActivityEventListener(this)release() UMShareAPI。然而 invalidate() 在 React Native 的 TurboModule 生命周期中可能不会被调用(例如宿主 Activity 被销毁但 JS 引擎未重启时),导致 ActivityEventListener 泄漏,进而可能持有已销毁的 Activity 引用,引发内存泄漏或 ActivityNotFoundException。建议在 onHostDestroy 回调中同步清理。

init {
  // 自动接管宿主 Activity 的 onActivityResult 回调,转发给 UMShareAPI。
  // 避免宿主 MainActivity 自己 override onActivityResult 手动转发。
  reactContext.addActivityEventListener(this)
}

override fun invalidate() {
  super.invalidate()
  reactApplicationContext.removeActivityEventListener(this)
  currentActivity?.let { UMShareAPI.get(it).release() }
}
线程安全问题

ensurePreInit:config:error:ensureInit:error: 方法使用 NSLock 进行同步,但 _preInited_inited 是实例变量,而 shared 方法返回单例。在 ensurePreInit 中,第 33-34 行先检查 _preInited 再加锁,这是正确的双重检查锁定模式。但第 56-57 行在加锁后直接赋值 _appkey_channel,而 ensureInit 第 112 行读取这些变量时也在锁内,因此线程安全。然而,ensurePreInit 第 67-68 行在锁外检查 wxIdwxSecretisKindOfClass,这些检查是只读的,不会导致问题。但第 69-70 行调用 setPlaform: 时仍在锁内,而 setPlaform: 内部可能调用其他线程的回调,存在潜在死锁风险。建议将 setPlaform: 调用移出锁范围。

if ([wxId isKindOfClass:[NSString class]] && wxId.length > 0 &&
    [wxSecret isKindOfClass:[NSString class]] && wxSecret.length > 0) {
  // 友盟 SDK 拼写遵循原始错误: setPlaform 少一个 t
  [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession

PR review 提到的真问题:invalidate() 在 react native turbo-module 生命周期
中只在 catalyst instance destroy 时调 (app reload / 关闭),而宿主 Activity
在 JS 引擎不重启时也可能销毁,这时:

  - addActivityEventListener 注册的 listener 不被清 (但 ReactContext
    自己管理 listener 引用,实际不会泄漏 Activity)
  - 友盟 UMShareAPI.get(activity) 按 activity 缓存的 instance 不被 release
    → SDK 内部 dict 持有已销毁 Activity 引用 → 内存泄漏 /
    ActivityNotFoundException

修复: UmengShareModule 额外实现 LifecycleEventListener,init 时 addLifecycle,
onHostDestroy() 调 UMShareAPI.get(currentActivity).release()。

LifecycleEventListener 的 onHostDestroy 在 ReactContext 关联的 host Activity
(MainActivity) onDestroy 时触发,正是清 SDK 缓存的时机。

保留 invalidate() 的清理作为兜底 (catalyst destroy 时也清一次)。

verify: yarn typecheck + 41/41 jest 通过。
ci kotlin 编译报:
  'onActivityResult' overrides nothing. Potential signatures for overriding:
    fun onActivityResult(activity: Activity, requestCode: Int, ...): Unit
  'onNewIntent' overrides nothing. Potential signatures for overriding:
    fun onNewIntent(intent: Intent): Unit

rn 0.85 com.facebook.react.bridge.ActivityEventListener 接口签名是
non-null (没 `?`),我之前写成 Activity? / Intent? 导致 override 失败。

修复:
- onActivityResult: Activity? -> Activity (去掉 nullable 标记 + 简化
  activity?.let { ... } 到直接调)
- onNewIntent: Intent? -> Intent

verify: 本地 yarn typecheck 干净。
@qq382724935 qq382724935 merged commit 6ca08c1 into main May 28, 2026
11 checks passed
@qq382724935 qq382724935 deleted the docs/fix-appdelegate-objc-bridge branch May 28, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant