diff --git a/.env.example b/.env.example
index 19d9ef8..02e90e6 100644
--- a/.env.example
+++ b/.env.example
@@ -1,23 +1,22 @@
# === iOS 自动打包服务配置 ===
# 复制此文件为 .env 并修改为实际值
-# ---- 项目路径 ----
-# iOS 源码项目根目录(也用于 AutoPacking 脚本路径)
-PROJECT_ROOT=/Users/shen/Work/Code/Readoor
+# ---- 目录配置 ----
+# 分支源码根目录,每个分支一个子目录(如 ReadoorBranches/main、ReadoorBranches/dev)
+GIT_SOURCE_BASE=./ReadoorBranches
# 打包输出基础目录(每次打包会在此目录下创建子目录)
-BUILD_BASE_DIR=/Users/shen/Documents
+BUILD_BASE_DIR=./build
# ---- 分支源码管理 ----
-# 分支源码根目录,每个分支一个子目录(如 ReadoorBranches/main、ReadoorBranches/dev)
-GIT_SOURCE_BASE=/Users/shen/Work/Code/ReadoorBranches
-
# Git 远程仓库地址,分支目录不存在时自动 clone
-GIT_REMOTE_URL=git@github.com:your-org/readoor.git
+GIT_REMOTE_URL=https://pineapple.readoor.cn:9080/trn/triapp.git
+GIT_USERNAME=shenlei
+GIT_PASSWORD='&s9U5fbreA'
# ---- 服务端口 ----
-BACKEND_PORT=8000
-FRONTEND_PORT=3000
+BACKEND_PORT=5002
+FRONTEND_PORT=5999
# ---- 管理员账号 ----
ADMIN_USERNAME=admin
diff --git a/.gitignore b/.gitignore
index e41d212..f718dd9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ __pycache__/
# 运行时数据
backend/data/*.bak
+config.json
+ReadoorBranches/
+build/
# 日志
logs/
diff --git a/README.md b/README.md
index 9788511..c9a420f 100644
--- a/README.md
+++ b/README.md
@@ -44,14 +44,17 @@ cp .env.example .env
vim .env
```
-**必须修改的配置项:**
+**建议优先检查的配置项:**
```bash
-# iOS 源码项目路径(包含 readoor.xcworkspace 的目录)
-PROJECT_ROOT=/path/to/your/Readoor
+# 分支源码根目录,默认在当前项目下
+GIT_SOURCE_BASE=/path/to/iOSBuildServer/ReadoorBranches
-# 打包输出目录
-BUILD_BASE_DIR=/path/to/build/output
+# 打包输出目录,默认在当前项目下
+BUILD_BASE_DIR=/path/to/iOSBuildServer/build
+
+# Git 远程仓库地址(配置后可自动拉取分支)
+GIT_REMOTE_URL=git@github.com:org/repo.git
# 管理员密码(建议修改)
ADMIN_PASSWORD=your_secure_password
@@ -71,17 +74,19 @@ ADMIN_PASSWORD=your_secure_password
### 4.(可选)初始化分支源码目录
-如果需要按分支打包,先准备分支源码目录:
+系统现在默认直接从分支源码目录打包,每个分支对应 `GIT_SOURCE_BASE` 下的一个子目录。
+
+如果已配置 `GIT_REMOTE_URL`,首次打包某个分支时会自动 clone;如果未配置,则需要先手动准备分支目录:
```bash
# 创建分支源码根目录
mkdir -p /path/to/ReadoorBranches
-# 手动 clone 主分支(后续自动 pull 更新)
+# 手动 clone 主分支(后续可继续手动维护)
git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
```
-其他分支无需手动 clone,首次打包时会自动从远程仓库 clone。
+如果已配置 `GIT_REMOTE_URL`,其他分支无需手动 clone,首次打包时会自动拉取。
### 5.(可选)启用健康监测
@@ -101,13 +106,12 @@ git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
所有配置通过项目根目录的 `.env` 文件管理,修改后重启服务生效。
-### 项目路径
+### 目录配置
| 变量 | 示例 | 说明 |
|------|------|------|
-| `PROJECT_ROOT` | `/Users/shen/Work/Code/Readoor` | iOS 源码项目根目录(AutoPacking 脚本路径) |
-| `BUILD_BASE_DIR` | `/Users/shen/Documents` | 打包产物输出基础目录 |
-| `GIT_SOURCE_BASE` | `/path/to/ReadoorBranches` | 分支源码根目录,每个分支一个子目录 |
+| `GIT_SOURCE_BASE` | `/Users/shen/Work/Code/iOSBuildServer/ReadoorBranches` | 分支源码根目录,每个分支一个子目录,仅提供 iOS 工程源码 |
+| `BUILD_BASE_DIR` | `/Users/shen/Work/Code/iOSBuildServer/build` | 打包产物输出基础目录 |
| `GIT_REMOTE_URL` | `git@github.com:org/repo.git` | 远程仓库地址,分支目录不存在时自动 clone |
### 服务配置
@@ -131,14 +135,20 @@ git clone -b main git@github.com:org/repo.git /path/to/ReadoorBranches/main
以下配置通过管理后台 Web 界面管理(存储在 `config.json`):
+首次部署时,如果项目根目录下存在 `config.json`,系统会自动导入其中的 `apps` 和 `schemes` 作为初始配置;导入后即可继续在后台增删改。
+
| 配置项 | 说明 |
|--------|------|
| App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 |
| Scheme 管理 | 打包 Scheme 配置 |
| 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 |
| 分支管理 | 可打包的代码分支列表 |
-| 版本号 | App_Ver 和 Build_Ver(写入 AutoPacking 脚本) |
-| 上传配置 | OSS / WebDAV 文件上传设置 |
+| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码 |
+| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 |
+
+### 服务端自动化资源
+
+项目内的 `backend/automation/` 是 Web 打包服务唯一使用的自动化资源:默认皮肤和混淆工具都在此处维护。分支源码中的 `AutoPacking/` 不会被复制、执行或修改,仍可供开发者手动打包使用。
### Watchdog 配置
@@ -496,7 +506,7 @@ tail -50 logs/server.log
```
常见原因:
-- `PROJECT_ROOT` 路径不存在
+- `GIT_SOURCE_BASE` 下对应分支目录不存在,且未配置 `GIT_REMOTE_URL`
- 磁盘空间不足
- Python 依赖缺失(重新执行 `./deploy.sh build`)
diff --git a/backend/automation/obfuscation/generate_image_whitelist.py b/backend/automation/obfuscation/generate_image_whitelist.py
new file mode 100644
index 0000000..ce28695
--- /dev/null
+++ b/backend/automation/obfuscation/generate_image_whitelist.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python3
+"""
+构建期 dylib 白名单生成脚本
+
+优先扫描最终 .app Bundle 内实际嵌入并会被 dyld 加载的镜像,生成:
+1. RDKnownImages.plist — 白名单 + HMAC-SHA256 摘要
+2. Generated/RDWhitelistSecret.swift — 仅在缺失时补生成
+
+在 Xcode Build Phase 中调用时,会复用现有密钥并更新最终 app bundle 内的 plist;
+手动执行时若无法定位 app bundle,则退回扫描仓库中的 framework/dylib 来源。
+"""
+
+import os
+import sys
+import hmac
+import hashlib
+import plistlib
+import secrets
+import re
+from pathlib import Path
+
+# 所有 framework 来源目录(相对于项目根目录)
+FRAMEWORK_SEARCH_PATHS = [
+ 'Pods',
+ 'readoor/BookShelf/Classes/Factorys',
+ 'readoor/3.0/Factorys',
+ 'readoor/3.0/Features/ThirdPartyService',
+]
+
+# 输出路径
+PLIST_OUTPUT = 'readoor/3.0/Base/Security/RDKnownImages.plist'
+SECRET_OUTPUT = 'readoor/3.0/Base/Security/Generated/RDWhitelistSecret.swift'
+def scan_source_frameworks(search_paths, app_name):
+ """扫描仓库中的 framework 和 dylib 来源,作为无法定位 app bundle 时的兜底方案。"""
+ known = []
+ seen = set()
+ for search_dir in search_paths:
+ if not os.path.isdir(search_dir):
+ continue
+ for root, dirs, files in os.walk(search_dir):
+ # .framework 在 macOS 上是目录,出现在 dirs 中
+ for d in dirs:
+ if d.endswith('.framework'):
+ basename = d.replace('.framework', '')
+ rel_path = os.path.relpath(os.path.join(root, d), '.')
+ key = (basename, rel_path)
+ if key not in seen:
+ seen.add(key)
+ known.append({'name': basename, 'path': rel_path})
+ for f in files:
+ if f.endswith('.dylib'):
+ basename = f.replace('.dylib', '')
+ rel_path = os.path.relpath(os.path.join(root, f), '.')
+ key = (basename, rel_path)
+ if key not in seen:
+ seen.add(key)
+ known.append({'name': basename, 'path': rel_path})
+
+ # App 自身
+ app_key = (app_name, app_name)
+ if app_key not in seen:
+ known.append({'name': app_name, 'path': app_name})
+
+ # 按 name 排序保证稳定性
+ known.sort(key=lambda x: x['name'])
+ return known
+
+
+def scan_bundled_images(app_bundle_path, executable_path):
+ """扫描最终 app bundle 中会实际被加载的镜像,路径使用 bundle 相对路径。"""
+ known = []
+ seen = set()
+
+ executable_name = Path(executable_path).name if executable_path else Path(app_bundle_path).stem
+ main_key = (executable_name, executable_path or executable_name)
+ seen.add(main_key)
+ known.append({'name': executable_name, 'path': executable_path or executable_name})
+
+ for root, dirs, files in os.walk(app_bundle_path):
+ dirs.sort()
+ files.sort()
+
+ for d in dirs:
+ if not d.endswith('.framework'):
+ continue
+ framework_dir = os.path.join(root, d)
+ basename = d.replace('.framework', '')
+
+ # if basename == 'MJExtension': # 故意排除
+ # continue
+
+ binary_path = os.path.join(framework_dir, basename)
+ if not os.path.isfile(binary_path):
+ continue
+ rel_path = os.path.relpath(binary_path, app_bundle_path)
+ key = (basename, rel_path)
+ if key not in seen:
+ seen.add(key)
+ known.append({'name': basename, 'path': rel_path})
+
+ for f in files:
+ if not f.endswith('.dylib'):
+ continue
+ dylib_path = os.path.join(root, f)
+ basename = f.replace('.dylib', '')
+ rel_path = os.path.relpath(dylib_path, app_bundle_path)
+ key = (basename, rel_path)
+ if key not in seen:
+ seen.add(key)
+ known.append({'name': basename, 'path': rel_path})
+
+ known.sort(key=lambda x: (x['name'], x['path']))
+ return known
+
+
+def compute_digest(entries, secret):
+ """对排序后的白名单内容计算 HMAC-SHA256 摘要"""
+ parts = [f"{e['name']}|{e['path']}" for e in entries]
+ message = '\n'.join(parts).encode('utf-8')
+ return hmac.new(secret, message, hashlib.sha256).hexdigest()
+
+
+def generate_swift_secret(secret_bytes):
+ """将构建期密钥写入 Swift 源码"""
+ hex_bytes = ', '.join(f'0x{b:02x}' for b in secret_bytes)
+ source = f"""// Auto-generated by generate_image_whitelist.py — do not edit manually
+import Foundation
+
+enum RDWhitelistSecret {{
+ static let key: [UInt8] = [{hex_bytes}]
+}}
+"""
+ os.makedirs(os.path.dirname(SECRET_OUTPUT), exist_ok=True)
+ with open(SECRET_OUTPUT, 'w') as f:
+ f.write(source)
+
+
+def load_existing_secret():
+ if not os.path.isfile(SECRET_OUTPUT):
+ return None
+ with open(SECRET_OUTPUT, 'r', encoding='utf-8') as f:
+ content = f.read()
+ hex_values = re.findall(r'0x([0-9a-fA-F]{2})', content)
+ if not hex_values:
+ return None
+ return bytes(int(value, 16) for value in hex_values)
+
+
+def resolve_app_bundle_path():
+ target_build_dir = os.environ.get('TARGET_BUILD_DIR')
+ wrapper_name = os.environ.get('WRAPPER_NAME')
+ if target_build_dir and wrapper_name:
+ app_bundle_path = os.path.join(target_build_dir, wrapper_name)
+ if os.path.isdir(app_bundle_path):
+ return app_bundle_path
+ return None
+
+
+def main():
+ root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ os.chdir(root)
+
+ build_secret = load_existing_secret()
+ if build_secret is None:
+ build_secret = secrets.token_bytes(32)
+ generate_swift_secret(build_secret)
+ print(f'[whitelist] Generated {SECRET_OUTPUT}')
+
+ app_bundle_path = resolve_app_bundle_path()
+ executable_path = os.environ.get('EXECUTABLE_PATH', 'readoor')
+ if app_bundle_path:
+ frameworks = scan_bundled_images(app_bundle_path, executable_path)
+ print(f'[whitelist] Scanned app bundle images from {app_bundle_path}')
+ else:
+ frameworks = scan_source_frameworks(FRAMEWORK_SEARCH_PATHS, 'readoor')
+ print('[whitelist] App bundle not found, fallback to source framework scan')
+
+ digest = compute_digest(frameworks, build_secret)
+
+ output = {
+ 'known_images': frameworks,
+ 'digest': digest
+ }
+
+ if app_bundle_path:
+ destination = os.path.join(app_bundle_path, os.path.basename(PLIST_OUTPUT))
+ os.makedirs(os.path.dirname(destination), exist_ok=True)
+ with open(destination, 'wb') as f:
+ plistlib.dump(output, f)
+ print(f'[whitelist] Generated app bundle plist {destination} with {len(frameworks)} dynamic entries')
+ else:
+ os.makedirs(os.path.dirname(PLIST_OUTPUT), exist_ok=True)
+ with open(PLIST_OUTPUT, 'wb') as f:
+ plistlib.dump(output, f)
+ print(f'[whitelist] Generated {PLIST_OUTPUT} with {len(frameworks)} dynamic entries')
+
+ print(f'[whitelist] digest={digest[:16]}...')
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/backend/automation/obfuscation/obfuscate_symbols.py b/backend/automation/obfuscation/obfuscate_symbols.py
new file mode 100644
index 0000000..7c04da4
--- /dev/null
+++ b/backend/automation/obfuscation/obfuscate_symbols.py
@@ -0,0 +1,4293 @@
+#!/usr/bin/env python3
+"""
+obfuscate_symbols.py — Swift 类型名符号混淆工具
+用法:
+ python3 AutoPacking/obfuscation/obfuscate_symbols.py --scan-dir readoor/3.0 # 仅预览,不修改
+ python3 AutoPacking/obfuscation/obfuscate_symbols.py --scan-dir readoor/3.0 --apply # 真正替换
+ python3 AutoPacking/obfuscation/obfuscate_symbols.py --deobfuscate crash.log # 还原 crash 符号
+"""
+
+import os
+import re
+import json
+import hashlib
+import argparse
+import sys
+import secrets
+import time
+import shutil
+import tempfile
+import multiprocessing
+import subprocess
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from pathlib import Path
+
+from obfuscation_runtime.canonical import (
+ build_canonical_symbol_tables,
+ build_method_mapping_from_canonical_table,
+ build_type_mappings_from_canonical_table,
+)
+from obfuscation_runtime.discovery import discover_files, prefilter_files_with_rg
+from obfuscation_runtime.json_cache import IncrementalAnalysisCache, load_json_file, save_json_file
+
+# ── 配置 ──────────────────────────────────────────────────────────────────────
+
+# 扫描这些目录(相对于脚本所在目录)。默认改为项目根以覆盖整个仓库,
+# 也可通过命令行的 --scan-dir 覆盖为更精确的子目录。
+DEFAULT_SCAN_DIRS = ["."]
+
+# 排除目录(含这些路径片段的文件跳过)
+EXCLUDE_PATH_FRAGMENTS = [
+ ".build/",
+ ".git/",
+ "Pods/",
+ "Carthage/",
+ "Tests/",
+ "readoorTests/",
+ "DerivedData/",
+ "RDObfuscateMacro/", # 我们自己的宏包,不混淆
+ ".framework/",
+ ".xcframework/",
+]
+THIRD_PARTY_PROTOCOL_EXCLUDE_PATH_FRAGMENTS = [
+ frag for frag in EXCLUDE_PATH_FRAGMENTS
+ if frag not in {"Pods/", ".framework/", ".xcframework/"}
+]
+
+# 目录名白名单:完全匹配这些目录名则在提取阶段忽略(例如第三方/工具目录)
+EXCLUDE_DIR_NAMES = ("Factorys",)
+METHOD_REPLACEMENT_SKIP_DIR_NAMES = ("Pods",)
+# 始终保留(白名单)的符号,完全匹配
+SYMBOL_WHITELIST = {
+ # AppKit / UIKit 入口
+ "AppDelegate", "SceneDelegate",
+ # 常见系统协议实现类型
+ "Error", "Result", "Event",
+ # Swift 内建类型
+ "String", "Int", "Bool", "Double", "Float", "Data", "URL", "Date",
+ "Array", "Dictionary", "Set", "Optional",
+ # JSON 编解码保留(必须与 JSON key 名一致,混淆会导致解码失败)
+ "CodingKeys",
+ # StoreKit / 内购 相关类型(保留,避免混淆第三方/系统 IAP 协议/模型)
+ "SKProduct", "SKProductsRequest", "SKProductsResponse", "SKPayment", "SKPaymentQueue",
+ "SKPaymentTransaction", "SKPaymentTransactionObserver", "SKProductsRequestDelegate",
+ "SKReceiptRefreshRequest", "SKStoreProductViewController", "SKMutablePayment",
+ # StoreKit 2 及 App Store 相关通用类型
+ "Product", "Transaction", "StoreKit", "AppStore",
+}
+
+# 前缀白名单:含这些前缀的符号跳过(第三方/系统)
+SYMBOL_PREFIX_WHITELIST = ("NS", "UI", "AV", "CL", "MK", "SK", "CG", "CA", "WK")
+
+# 一些语言/运行时保留标识符,绝对不能被替换(例如 Objective-C/Swift 的 `self`/`super` 等)
+RESERVED_IDENTIFIERS = {
+ # Obj-C instance/reference keywords
+ "self", "super",
+ # Obj-C literals / common macros
+ "nil", "NULL", "YES", "NO",
+ # Swift / JSON booleans
+ "true", "false",
+}
+
+# 仅混淆这些 Swift 声明关键字(保守策略,先不动 func/var)
+DECL_KEYWORDS = ("class", "struct", "enum", "protocol")
+
+# 混淆后名称前缀(让 crash 还原时还能快速识别来源)
+# 类型名使用 YMH;方法名使用 ymh,避免 Swift 导入 Obj-C 时出现
+# “'YMHxxxx()' has been renamed to 'ymhxxxx()'” 提示。
+TYPE_OBFUSCATED_PREFIX = "YMH"
+METHOD_OBFUSCATED_PREFIX = "ymh"
+OBFUSCATED_PREFIX = TYPE_OBFUSCATED_PREFIX
+# 混淆哈希长度(hex 字符数)。增大此值可显著降低不同运行间重复概率。
+OBFUSCATED_HASH_LEN = 10
+LEGACY_OBFUSCATED_HASH_LENGTHS = (6, 8, 10)
+ALREADY_OBFUSCATED_PREFIXES = (TYPE_OBFUSCATED_PREFIX, METHOD_OBFUSCATED_PREFIX)
+
+# 仅对这些文件名前缀进行混淆(基于文件名,不含扩展)
+# 例如 Swift 文件 `RDWereadOAuthWebViewController.swift` 会匹配 'RD'
+SWIFT_FILENAME_PREFIXES = ("RD", "RB")
+# 对 Obj-C 文件匹配的前缀(例如 `STS... .m/.h`)
+OBJC_FILENAME_PREFIXES = ("STS", "RB")
+
+# 仅混淆这些符号名前缀(避免在 RD 文件中误混淆第三方或通用类)
+SWIFT_SYMBOL_PREFIXES = ("RD", "RB")
+OBJC_SYMBOL_PREFIXES = ("STS", "RB")
+
+# 映射表保存路径
+MAP_OUTPUT_PATH = "obfuscation_map.json"
+APPLY_STATE_PATH = ".obfuscate_apply_state.json"
+SCRIPT_DIR = Path(__file__).resolve().parent
+PROJECT_METHOD_IGNORE_FILE_PATH = str(SCRIPT_DIR / "system_methods_ignore.txt")
+AUTO_DETECTED_METHOD_IGNORE_BEGIN = "# BEGIN AUTO-DETECTED-OBJC-DOT-SYNTAX"
+AUTO_DETECTED_METHOD_IGNORE_END = "# END AUTO-DETECTED-OBJC-DOT-SYNTAX"
+SOURCE_FILE_EXTENSIONS = (".swift", ".m", ".mm", ".h", ".pch")
+# 方法映射输出(dry-run)
+METHOD_MAP_OUTPUT_PATH = "tmp_method_map.json"
+DEFAULT_WORKERS = max(1, min(8, os.cpu_count() or 4))
+IOS_SYSTEM_SYMBOL_CACHE_PATH = ".ios_system_symbols_cache.json"
+IOS_SYSTEM_SYMBOL_CACHE_VERSION = 2
+EXTRACTION_CACHE_PATH = ".obfuscate_extraction_cache.json"
+EXTRACTION_CACHE_VERSION = 3
+IOS_SYSTEM_SYMBOL_SCAN_DIRS = (
+ "System/Library/Frameworks",
+ "usr/include",
+)
+THIRD_PARTY_PROTOCOL_ROOT_NAMES = ("Pods", "Factorys")
+METHOD_CANDIDATE_PREFIXES = ("STS", "RD", "RB")
+METHOD_CANDIDATE_EXTENSIONS = (".swift", ".m", ".mm", ".h")
+RG_DISCOVERY_EXTENSIONS = SOURCE_FILE_EXTENSIONS
+RG_CHUNK_SIZE = 256
+PROJECT_SOURCE_ANALYSIS_KIND = "project_source"
+THIRD_PARTY_MEMBER_ANALYSIS_KIND = "third_party_member"
+_EXTRACTION_ANALYSIS_CACHE = IncrementalAnalysisCache(EXTRACTION_CACHE_PATH, EXTRACTION_CACHE_VERSION)
+
+TYPE_DECLARATION_RG_PATTERN = r"\b(?:class|struct|enum|protocol)\s+[A-Za-z_]|@(?:interface|implementation|protocol)\s+[A-Za-z_]"
+METHOD_DECLARATION_RG_PATTERN = r"\bfunc\s+[A-Za-z_]|^[[:space:]]*[+-][[:space:]]*\("
+PROPERTY_DECLARATION_RG_PATTERN = r"@property\b|\b(?:var|let)\s+[A-Za-z_]"
+SELECTOR_LITERAL_RG_PATTERN = r'@"[A-Za-z_][A-Za-z0-9_:@]*"'
+OBJC_DOT_SYNTAX_MEMBER_RG_PATTERN = r"\.\s*[A-Za-z_]"
+
+# 常见系统成员名兜底(即使 SDK 扫描失败,也不要混淆这些高风险名字)
+COMMON_SYSTEM_MEMBER_NAMES = {
+ # 来源: Apple Developer Documentation (常用 UIKit / Delegate / DataSource / WebKit / UserNotifications API)
+ # - https://developer.apple.com/documentation/uikit/uiviewcontroller
+ # - https://developer.apple.com/documentation/uikit/uiview
+ # - https://developer.apple.com/documentation/uikit/uislider
+ # - https://developer.apple.com/documentation/uikit/uiscrollviewdelegate
+ # - https://developer.apple.com/documentation/uikit/uitableviewdelegate
+ # - https://developer.apple.com/documentation/uikit/uitableviewdatasource
+ # - https://developer.apple.com/documentation/uikit/uicollectionviewdelegate
+ # - https://developer.apple.com/documentation/uikit/uicollectionviewdatasource
+ # - https://developer.apple.com/documentation/uikit/uiapplicationdelegate
+ # - https://developer.apple.com/documentation/uikit/uiscenedelegate
+ # - https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate
+ # - https://developer.apple.com/documentation/uikit/uigesturerecognizerdelegate
+ # - https://developer.apple.com/documentation/webkit/wkwebview
+ # - https://developer.apple.com/documentation/webkit/wknavigationdelegate
+
+ # 基本 Obj-C/Swift setter/getter 名
+ "set",
+ "get",
+
+ # UIViewController lifecycle / layout
+ "viewDidLoad", "viewWillAppear", "viewDidAppear", "viewWillDisappear", "viewDidDisappear",
+ "viewWillLayoutSubviews", "viewDidLayoutSubviews", "viewSafeAreaInsetsDidChange",
+ "viewWillLayoutSubviews", "viewDidLayoutSubviews", "viewWillLayoutSubviews",
+ "didReceiveMemoryWarning", "traitCollectionDidChange", "awakeFromNib", "deinit",
+ "prepareForSegue", "loadView", "loadViewIfNeeded", "setNeedsUpdateProperties",
+
+ # UIView / view hierarchy
+ "layoutSubviews", "layoutIfNeeded", "setNeedsLayout", "setNeedsDisplay",
+ "addSubview", "removeFromSuperview", "willMoveToSuperview", "didMoveToSuperview",
+ "willMoveToWindow", "didMoveToWindow", "didAddSubview", "willRemoveSubview",
+ "draw", "drawRect", "setNeedsDisplay", "tintColorDidChange",
+ "systemLayoutSizeFittingSize", "systemLayoutSizeFittingSize:",
+ "systemLayoutSizeFitting", "systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:)",
+
+ # UIResponder / touch handling
+ "touchesBegan", "touchesMoved", "touchesEnded", "touchesCancelled",
+
+ # Gesture recognizer
+ "gestureRecognizerShouldBegin", "gestureRecognizer", "gestureRecognizer:shouldReceiveTouch:",
+ "gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)",
+
+ # Scroll / Zoom delegate
+ "scrollViewDidScroll", "scrollViewWillBeginDragging", "scrollViewWillEndDragging",
+ "scrollViewDidEndDragging", "scrollViewDidEndDecelerating", "scrollViewDidEndScrollingAnimation",
+ "scrollViewDidZoom", "scrollViewWillBeginZooming", "viewForZooming(in:)",
+ "scrollViewDidChangeAdjustedContentInset",
+
+ # TableView delegate / datasource (常见签名关键词)
+ "tableView", "didSelectRowAt", "didDeselectRowAt", "willSelectRowAt", "willDeselectRowAt",
+ "cellForRowAt", "numberOfRowsInSection", "numberOfSections", "heightForRowAt", "estimatedHeightForRowAt",
+ "willDisplayCell", "didEndDisplayingCell", "commit", "canEditRowAt", "moveRowAt",
+ "accessoryButtonTappedForRowWith", "leadingSwipeActionsConfigurationForRowAt", "trailingSwipeActionsConfigurationForRowAt",
+
+ # CollectionView delegate / datasource
+ "collectionView", "cellForItemAt", "numberOfItemsInSection", "didSelectItemAt", "didDeselectItemAt",
+ "willDisplayCellForItemAt", "didEndDisplayingCellForItemAt", "targetContentOffsetForProposedContentOffset",
+
+ # App / Scene lifecycle
+ "application:didFinishLaunchingWithOptions:", "application:open:options:",
+ "applicationDidBecomeActive", "applicationWillResignActive", "applicationDidEnterBackground",
+ "applicationWillEnterForeground", "applicationWillTerminate",
+ "application:didRegisterForRemoteNotificationsWithDeviceToken:",
+ "application:didFailToRegisterForRemoteNotificationsWithError:",
+ "application:didReceiveRemoteNotification:fetchCompletionHandler:",
+
+ # Scene delegate
+ "scene(_:willConnectTo:options:)", "sceneDidDisconnect", "sceneWillEnterForeground",
+ "sceneDidBecomeActive", "sceneWillResignActive", "sceneDidEnterBackground",
+
+ # Notifications / UNUserNotificationCenterDelegate
+ "userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:",
+ "userNotificationCenter:willPresentNotification:withCompletionHandler:",
+ "userNotificationCenter:openSettingsFor:",
+
+ # WebKit / WKNavigationDelegate
+ "webView(_:decidePolicyFor:decisionHandler:)", "webView(_:didStartProvisionalNavigation:)",
+ "webView(_:didFinish:)", "webView(_:didFail:withError:)",
+
+ # WKWebView documented instance methods (avoid obfuscating WebKit entry points used directly in app code)
+ "loadHTMLString", "loadFileURL", "loadFileRequest", "loadSimulatedRequest",
+ "reloadFromOrigin", "stopLoading", "goBack", "goForward", "go(to:)",
+ "evaluateJavaScript", "callAsyncJavaScript", "takeSnapshot", "createPDF",
+ "createWebArchiveData", "startDownload", "resumeDownload", "setMagnification",
+ "pauseAllMediaPlayback", "requestMediaPlaybackState", "setAllMediaPlaybackSuspended",
+ "closeAllMediaPresentations", "setCameraCaptureState", "setMicrophoneCaptureState",
+ "find", "setMinimumViewportInset", "fetchData", "restoreData",
+
+ # UISlider documented methods and subclass override points
+ "setValue", "minimumTrackImage", "setMinimumTrackImage",
+ "maximumTrackImage", "setMaximumTrackImage", "thumbImage", "setThumbImage",
+ "minimumValueImageRect", "maximumValueImageRect", "trackRect", "thumbRect",
+
+ # StoreKit / In-App Purchase callbacks (Obj-C selectors / Swift method names)
+ "paymentQueue:updatedTransactions:", "paymentQueueRestoreCompletedTransactionsFinished",
+ "paymentQueue:restoreCompletedTransactionsFailedWithError:", "productsRequest:didReceiveResponse:",
+ "request:didFailWithError:", "paymentQueue:removedTransactions:", "paymentQueue:updatedDownloads:",
+
+ # Text input delegates
+ "textFieldShouldBeginEditing", "textFieldDidBeginEditing", "textFieldShouldReturn",
+ "textField(_:shouldChangeCharactersIn:replacementString:)",
+ "textViewDidChange", "textViewDidBeginEditing", "textViewDidEndEditing",
+
+ # Common focus / state restoration hooks
+ "encodeRestorableState", "decodeRestorableState", "applicationFinishedRestoringState",
+ "archivedValue",
+ "responds", "forwardingTarget", "respondsToSelector", "forwardingTargetForSelector:",
+
+ # SwiftUI lightweight hooks (common closure names)
+ "onAppear", "onDisappear",
+ # Project-specific API exemptions
+ "consigneeCreate", "consigneeUpdate",
+ "addRightHandleClick",
+ "topModel",
+ "topPageVc",
+ "modelList",
+ "bookTypeToItemType",
+ "vocabularyArray",
+ "letterArray",
+}
+
+# UINavigationController 常用方法/属性(来源:Apple Developer Documentation)
+COMMON_SYSTEM_MEMBER_NAMES.update({
+ # Properties
+ "topViewController", "visibleViewController", "viewControllers",
+ "navigationBar", "toolbar", "isNavigationBarHidden", "isToolbarHidden",
+ "interactivePopGestureRecognizer", "interactiveContentPopGestureRecognizer",
+ "hidesBarsOnTap", "hidesBarsOnSwipe", "hidesBarsWhenVerticallyCompact",
+ "hidesBarsWhenKeyboardAppears", "barHideOnTapGestureRecognizer", "barHideOnSwipeGestureRecognizer",
+ "hideShowBarDuration",
+
+ # Swift-style method bases
+ "pushViewController", "popViewController", "popToViewController", "popToRootViewController",
+ "setViewControllers", "setNavigationBarHidden", "setToolbarHidden", "show", "showDetailViewController",
+
+ # Obj-C selectors / Obj-C-style names (保守兼容)
+ "pushViewController:animated:", "popViewControllerAnimated:", "popToViewController:animated:",
+ "popToRootViewControllerAnimated:", "setViewControllers:animated:",
+ "setNavigationBarHidden:animated:", "setToolbarHidden:animated:",
+})
+
+IOS_SYSTEM_MEMBER_NAMES = set()
+IOS_SYSTEM_SYMBOLS_LOADED = False
+# If enabled, allow rewriting `#import "X.h"` lines and rename corresponding header files.
+# This is risky: filenames may diverge from disk unless headers are renamed accordingly.
+REWRITE_IMPORTS_ALLOWED = False
+# Pods / Factorys 中声明过的第三方成员名。保守处理:这些名字一律不参与方法混淆,
+# 以避免继承第三方类、实现第三方 Delegate/DataSource 时把回调或重写方法改坏。
+THIRD_PARTY_PROTOCOL_METHOD_NAMES = set()
+THIRD_PARTY_PROTOCOL_METHODS_LOADED = False
+PROJECT_PROPERTY_MEMBER_NAMES = set()
+PROJECT_PROPERTY_MEMBERS_LOADED = False
+PROJECT_METHOD_IGNORE_NAMES = set()
+PROJECT_METHOD_IGNORE_PATTERNS = []
+
+OBJC_PROPERTY_DECL_RE = re.compile(r"@property\b(?P
.*?);", re.S)
+OBJC_PROPERTY_GETTER_ATTR_RE = re.compile(r"\bgetter\s*=\s*([A-Za-z_][A-Za-z0-9_]*)")
+OBJC_PROPERTY_SETTER_ATTR_RE = re.compile(r"\bsetter\s*=\s*([A-Za-z_][A-Za-z0-9_]*:)")
+OBJC_SYNTHESIZE_IVAR_RE = re.compile(r"@synthesize\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*_([A-Za-z_][A-Za-z0-9_]*)\s*;")
+SWIFT_INTERFACE_FUNC_RE = re.compile(r"\bfunc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(")
+SWIFT_INTERFACE_PROPERTY_RE = re.compile(r"\b(?:var|let)\s+([A-Za-z_][A-Za-z0-9_]*)\s*:")
+SWIFT_PROPERTY_DECL_RE = re.compile(
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^\n]*?\))?\s*)*"
+ r"(?:(?:public|private|fileprivate|internal|open|final|weak|unowned|lazy|static|class|override|required|optional|dynamic|mutating|nonmutating|convenience|indirect)\s+)*"
+ r"(?:var|let)\s+([A-Za-z_][A-Za-z0-9_]*)\b",
+ re.M,
+)
+THIRD_PARTY_PROTOCOL_NAME_RE = re.compile(r"(?:Delegate|DataSource)\b", re.I)
+SWIFT_PROTOCOL_DECL_RE = re.compile(r"\bprotocol\s+([A-Za-z_][A-Za-z0-9_]*)\b[^\{]*\{")
+
+# ── 工具函数 ──────────────────────────────────────────────────────────────────
+
+def short_hash(name: str, salt: str) -> str:
+ """生成可复现的伪随机 ID,长度由 OBFUSCATED_HASH_LEN 控制。"""
+ h = hashlib.sha256(f"{salt}{name}".encode()).hexdigest()[:OBFUSCATED_HASH_LEN]
+ return h
+
+def obfuscated_name(original: str, salt: str, nonce: int = 0, prefix: str = TYPE_OBFUSCATED_PREFIX) -> str:
+ # nonce 用于极端哈希冲突时重试,保证本次映射唯一
+ payload = original if nonce == 0 else f"{original}:{nonce}"
+ return f"{prefix}{short_hash(payload, salt)}"
+
+def build_unique_mapping(symbols: list, salt: str, prefix: str = TYPE_OBFUSCATED_PREFIX) -> dict:
+ mapping = {}
+ used_obf = set()
+ for name in symbols:
+ nonce = 0
+ while True:
+ obf = obfuscated_name(name, salt, nonce, prefix=prefix)
+ if obf not in used_obf:
+ mapping[name] = obf
+ used_obf.add(obf)
+ break
+ nonce += 1
+ return mapping
+
+def should_exclude_path(path: str) -> bool:
+ return any(frag in path for frag in EXCLUDE_PATH_FRAGMENTS)
+
+def collect_swift_files(scan_dirs: list) -> list:
+ return discover_files(scan_dirs, (".swift",), EXCLUDE_PATH_FRAGMENTS, skip_dir_names=EXCLUDE_DIR_NAMES)
+
+def collect_objc_files(scan_dirs: list) -> list:
+ return discover_files(scan_dirs, (".m", ".mm", ".h"), EXCLUDE_PATH_FRAGMENTS, skip_dir_names=EXCLUDE_DIR_NAMES)
+
+def collect_source_files(scan_dirs: list) -> list:
+ """收集所有可恢复源码文件,不受 RD/STS 文件名前缀限制。"""
+ return discover_files(scan_dirs, SOURCE_FILE_EXTENSIONS, EXCLUDE_PATH_FRAGMENTS)
+
+def collect_method_candidate_files(scan_dirs: list) -> list:
+ """仅收集用于方法提取的候选文件:文件名以 STS 或 RD 开头,忽略 Factorys 等目录。"""
+ files = discover_files(
+ scan_dirs,
+ METHOD_CANDIDATE_EXTENSIONS,
+ EXCLUDE_PATH_FRAGMENTS,
+ skip_dir_names=EXCLUDE_DIR_NAMES,
+ basename_prefixes=METHOD_CANDIDATE_PREFIXES,
+ )
+ return prefilter_files_with_rg(files, METHOD_DECLARATION_RG_PATTERN, chunk_size=RG_CHUNK_SIZE)
+
+def collect_apply_target_files(scan_dirs: list, enable_objc_symbols: bool) -> list:
+ """收集实际执行替换的源码文件。
+
+ 定义提取仍可只看 RD/STS 前缀文件,但符号引用可能出现在任意业务源码文件中,
+ 因此替换阶段需要覆盖全部源码文件,否则会遗漏 String+xxx 之类的引用文件。
+ """
+ return discover_files(scan_dirs, SOURCE_FILE_EXTENSIONS, EXCLUDE_PATH_FRAGMENTS)
+
+def collect_resource_strings(scan_dirs: list) -> set:
+ """扫描 storyboard/xib 等资源,收集 class/outlet/selector 等字符串作为白名单。"""
+ strs = set()
+ pattern_attr = re.compile(r'(?:customClass|customModule|placeholder|storyboardIdentifier|reuseIdentifier|userLabel|selector)\s*=\s*"([A-Za-z_][A-Za-z0-9_:@]*)"')
+ pattern_tag = re.compile(r'<[^>]+\bclass\s*=\s*"([A-Za-z_][A-Za-z0-9_]*)"')
+ resource_files = discover_files(scan_dirs, ('.storyboard', '.xib', '.xml'), EXCLUDE_PATH_FRAGMENTS)
+ for full in resource_files:
+ try:
+ text = Path(full).read_text(encoding='utf-8')
+ except Exception:
+ continue
+ for m in pattern_attr.finditer(text):
+ strs.add(m.group(1))
+ for m in pattern_tag.finditer(text):
+ strs.add(m.group(1))
+ return strs
+
+
+def _discover_third_party_protocol_roots(scan_dirs: list) -> list:
+ roots = set()
+ excluded_dir_names = {"Pods", ".git", ".build", "Carthage", "Tests", "readoorTests", "DerivedData", "RDObfuscateMacro"}
+
+ search_roots = {str(Path(os.getcwd()).resolve())}
+ for scan_dir in scan_dirs:
+ try:
+ resolved_scan_dir = Path(scan_dir).resolve()
+ except Exception:
+ continue
+ if resolved_scan_dir.exists():
+ search_roots.add(str(resolved_scan_dir))
+
+ for search_root in sorted(search_roots):
+ base_path = Path(search_root)
+ if not base_path.exists():
+ continue
+ if base_path.name in THIRD_PARTY_PROTOCOL_ROOT_NAMES:
+ roots.add(str(base_path.resolve()))
+ for root, dirs, _ in os.walk(base_path):
+ current_root = Path(root)
+ for directory in list(dirs):
+ if directory in THIRD_PARTY_PROTOCOL_ROOT_NAMES:
+ roots.add(str((current_root / directory).resolve()))
+ dirs[:] = [
+ directory for directory in dirs
+ if directory not in excluded_dir_names and directory not in THIRD_PARTY_PROTOCOL_ROOT_NAMES
+ ]
+
+ return sorted(roots)
+
+
+def _iter_protocol_source_files(root_dirs: list):
+ seen = set()
+ candidates = discover_files(root_dirs, (".h", ".swift"), THIRD_PARTY_PROTOCOL_EXCLUDE_PATH_FRAGMENTS)
+ prefetched = prefilter_files_with_rg(candidates, PROPERTY_DECLARATION_RG_PATTERN + "|" + METHOD_DECLARATION_RG_PATTERN)
+ for file_path in prefetched:
+ full_path = os.path.realpath(file_path)
+ if full_path in seen:
+ continue
+ seen.add(full_path)
+ yield file_path
+
+
+def _extract_swift_property_members(text: str) -> set:
+ return {match.group(1) for match in SWIFT_PROPERTY_DECL_RE.finditer(text)}
+
+
+def _empty_project_source_analysis() -> dict:
+ return {
+ "swift_symbols": [],
+ "objc_symbols": [],
+ "raw_direct_names": [],
+ "raw_objc_selectors": [],
+ "raw_swift_signatures": {},
+ "property_members": [],
+ "selector_literals": [],
+ }
+
+
+def _empty_third_party_member_analysis() -> dict:
+ return {
+ "member_names": [],
+ }
+
+
+def load_extraction_cache() -> dict:
+ return _EXTRACTION_ANALYSIS_CACHE.load_state()
+
+
+def save_extraction_cache():
+ _EXTRACTION_ANALYSIS_CACHE.save_if_dirty()
+
+
+def _build_analysis_cache_key(analysis_kind: str, path: str) -> str:
+ return IncrementalAnalysisCache.build_cache_key(analysis_kind, path)
+
+
+def _read_text_with_hash(path: str):
+ return IncrementalAnalysisCache._read_text_with_hash(path)
+
+
+def _get_cached_file_analysis(path: str, analysis_kind: str, analyzer):
+ return _EXTRACTION_ANALYSIS_CACHE.get_or_compute(path, analysis_kind, analyzer)
+
+
+def _analyze_project_source_text(text: str, path: str) -> dict:
+ analysis = _empty_project_source_analysis()
+ if not text:
+ return analysis
+
+ if path.endswith(".swift"):
+ analysis["swift_symbols"] = sorted(_extract_swift_symbols_from_text(text))
+ direct_names, swift_signatures = _extract_swift_methods_from_text(text)
+ analysis["raw_direct_names"] = sorted(direct_names)
+ analysis["raw_swift_signatures"] = swift_signatures
+ analysis["property_members"] = sorted(_extract_swift_property_members(text))
+ return analysis
+
+ if path.endswith((".h", ".m", ".mm", ".pch")):
+ direct_names, objc_selectors = _extract_objc_methods_from_text(text)
+ analysis["objc_symbols"] = sorted(_extract_objc_symbols_from_text(text))
+ analysis["raw_direct_names"] = sorted(direct_names)
+ analysis["raw_objc_selectors"] = sorted(objc_selectors)
+ analysis["property_members"] = sorted(_extract_objc_property_members(text))
+ analysis["selector_literals"] = sorted(_collect_selector_literals_from_text(text))
+
+ return analysis
+
+
+def _analyze_third_party_member_text(text: str, path: str) -> dict:
+ analysis = _empty_third_party_member_analysis()
+ if not text:
+ return analysis
+
+ if path.endswith(".swift"):
+ analysis["member_names"] = sorted(_extract_delegate_protocol_methods_from_swift_text(text))
+ else:
+ analysis["member_names"] = sorted(_extract_delegate_protocol_methods_from_objc_text(text))
+ return analysis
+
+
+def ensure_project_property_members_loaded(scan_dirs: list) -> set:
+ global PROJECT_PROPERTY_MEMBERS_LOADED
+
+ if PROJECT_PROPERTY_MEMBERS_LOADED:
+ return PROJECT_PROPERTY_MEMBER_NAMES
+
+ property_members = set()
+ source_files = collect_apply_target_files(scan_dirs, enable_objc_symbols=True)
+ property_files = prefilter_files_with_rg(source_files, PROPERTY_DECLARATION_RG_PATTERN)
+ for file_path in property_files:
+ analysis = _get_cached_file_analysis(file_path, PROJECT_SOURCE_ANALYSIS_KIND, _analyze_project_source_text)
+ property_members.update(analysis.get("property_members", []))
+
+ PROJECT_PROPERTY_MEMBER_NAMES.clear()
+ PROJECT_PROPERTY_MEMBER_NAMES.update(property_members)
+ PROJECT_PROPERTY_MEMBERS_LOADED = True
+ save_extraction_cache()
+ return PROJECT_PROPERTY_MEMBER_NAMES
+
+
+def _protocol_name_is_delegate_like(name: str) -> bool:
+ return bool(THIRD_PARTY_PROTOCOL_NAME_RE.search(name or ""))
+
+
+def _extract_delegate_protocol_methods_from_objc_text(text: str) -> set:
+ protected = set()
+ protected.update(_extract_objc_property_members(text))
+
+ for method_match in OBJC_METHOD_DECL_RE.finditer(text):
+ declaration = method_match.group(1).strip()
+ selector_parts = re.findall(r'([A-Za-z_][A-Za-z0-9_]*:)', declaration)
+ if selector_parts:
+ # Obj-C 带参数方法在 Swift 中实现时通常会以首段 selector 作为 base name,
+ # 例如 `tencentDidNotLogin:` -> `func tencentDidNotLogin(_:)`。
+ # 这里同时保护完整 selector 和 Swift 可见的 base name,
+ # 避免实现第三方 Delegate/DataSource 或重写第三方类方法时被误混淆。
+ protected.add("".join(selector_parts))
+ protected.add(selector_parts[0][:-1])
+ continue
+
+ simple_match = re.match(r'([A-Za-z_][A-Za-z0-9_]*)', declaration)
+ if simple_match:
+ protected.add(simple_match.group(1))
+
+ return protected
+
+
+def _extract_delegate_protocol_methods_from_swift_text(text: str) -> set:
+ protected = set(_extract_swift_property_members(text))
+ for method_match in SWIFT_METHOD_DECL_RE.finditer(text):
+ protected.add(method_match.group(1))
+
+ return protected
+
+
+def ensure_third_party_protocol_methods_loaded(scan_dirs: list) -> set:
+ global THIRD_PARTY_PROTOCOL_METHODS_LOADED
+
+ if THIRD_PARTY_PROTOCOL_METHODS_LOADED:
+ return THIRD_PARTY_PROTOCOL_METHOD_NAMES
+
+ # 保守策略:Pods / Factorys 里声明过的成员名全部视为受保护,
+ # 这样第三方基类重写、第三方 Delegate / DataSource 实现都不会进入方法映射。
+ protected = set()
+ protocol_roots = _discover_third_party_protocol_roots(scan_dirs)
+ for file_path in _iter_protocol_source_files(protocol_roots):
+ analysis = _get_cached_file_analysis(file_path, THIRD_PARTY_MEMBER_ANALYSIS_KIND, _analyze_third_party_member_text)
+ protected.update(analysis.get("member_names", []))
+
+ THIRD_PARTY_PROTOCOL_METHOD_NAMES.clear()
+ THIRD_PARTY_PROTOCOL_METHOD_NAMES.update(protected)
+ THIRD_PARTY_PROTOCOL_METHODS_LOADED = True
+ save_extraction_cache()
+ return THIRD_PARTY_PROTOCOL_METHOD_NAMES
+
+
+def _path_contains_dir_name(path: str, dir_names: tuple) -> bool:
+ wanted = {name.lower() for name in dir_names}
+ return any(part.lower() in wanted for part in Path(path).parts)
+
+
+def should_skip_method_replacements(path: str) -> bool:
+ return _path_contains_dir_name(path, METHOD_REPLACEMENT_SKIP_DIR_NAMES)
+
+
+def _run_command_capture_stdout(args: list) -> str:
+ try:
+ completed = subprocess.run(
+ args,
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ except Exception:
+ return ""
+ return (completed.stdout or "").strip()
+
+
+def _discover_ios_sdk_paths() -> list:
+ sdk_paths = []
+
+ env_sdk_root = os.environ.get("SDKROOT")
+ if env_sdk_root and os.path.exists(env_sdk_root):
+ sdk_paths.append(os.path.realpath(env_sdk_root))
+
+ for sdk_name in ("iphoneos", "iphonesimulator"):
+ sdk_path = _run_command_capture_stdout(["xcrun", "--sdk", sdk_name, "--show-sdk-path"])
+ if sdk_path and os.path.exists(sdk_path):
+ sdk_paths.append(os.path.realpath(sdk_path))
+
+ unique_paths = []
+ seen = set()
+ for sdk_path in sdk_paths:
+ if sdk_path in seen:
+ continue
+ seen.add(sdk_path)
+ unique_paths.append(sdk_path)
+ return unique_paths
+
+
+def _load_cached_ios_system_symbols(sdk_paths: list):
+ payload = load_json_file(IOS_SYSTEM_SYMBOL_CACHE_PATH, {})
+ if not isinstance(payload, dict):
+ return None
+
+ cache_version = payload.get("cache_version")
+ cached_sdk_paths = payload.get("sdk_paths")
+ cached_symbols = payload.get("symbols")
+ if cache_version != IOS_SYSTEM_SYMBOL_CACHE_VERSION or cached_sdk_paths != sdk_paths or not isinstance(cached_symbols, list):
+ return None
+
+ return set(symbol for symbol in cached_symbols if isinstance(symbol, str) and symbol)
+
+
+def _save_ios_system_symbols_cache(sdk_paths: list, symbols: set):
+ payload = {
+ "cache_version": IOS_SYSTEM_SYMBOL_CACHE_VERSION,
+ "sdk_paths": sdk_paths,
+ "symbols": sorted(symbols),
+ }
+ try:
+ save_json_file(IOS_SYSTEM_SYMBOL_CACHE_PATH, payload)
+ except Exception:
+ pass
+
+
+def _iter_ios_sdk_symbol_files(sdk_path: str):
+ seen = set()
+ sdk_root = Path(sdk_path)
+ for relative_dir in IOS_SYSTEM_SYMBOL_SCAN_DIRS:
+ base_dir = sdk_root / relative_dir
+ if not base_dir.exists():
+ continue
+ for root, dirs, files in os.walk(base_dir):
+ dirs[:] = [directory for directory in dirs if not directory.endswith(".docc")]
+ for filename in files:
+ if not filename.endswith((".h", ".swiftinterface")):
+ continue
+ full_path = os.path.realpath(os.path.join(root, filename))
+ if full_path in seen:
+ continue
+ seen.add(full_path)
+ yield full_path
+
+
+def _default_objc_setter_name(property_name: str) -> str:
+ if property_name.startswith("is") and len(property_name) > 2 and property_name[2].isupper():
+ setter_base = property_name[2:]
+ else:
+ setter_base = property_name[:1].upper() + property_name[1:]
+ return f"set{setter_base}:"
+
+
+def _extract_objc_property_members(text: str) -> set:
+ members = set()
+ for match in OBJC_PROPERTY_DECL_RE.finditer(text):
+ body = " ".join(match.group("body").split())
+ if not body:
+ continue
+
+ name_match = re.search(r"([A-Za-z_][A-Za-z0-9_]*)\s*$", body)
+ if not name_match:
+ continue
+
+ property_name = name_match.group(1)
+ members.add(property_name)
+
+ getter_match = OBJC_PROPERTY_GETTER_ATTR_RE.search(body)
+ if getter_match:
+ members.add(getter_match.group(1))
+ else:
+ members.add(property_name)
+
+ if "readonly" in body:
+ continue
+
+ setter_match = OBJC_PROPERTY_SETTER_ATTR_RE.search(body)
+ if setter_match:
+ members.add(setter_match.group(1))
+ else:
+ members.add(_default_objc_setter_name(property_name))
+
+ for match in OBJC_SYNTHESIZE_IVAR_RE.finditer(text):
+ property_name = match.group(1)
+ members.add(property_name)
+ members.add(_default_objc_setter_name(property_name))
+
+ return members
+
+
+def _extract_objc_sdk_members(text: str) -> set:
+ members = set()
+ members.update(_extract_objc_property_members(text))
+
+ for match in OBJC_METHOD_DECL_RE.finditer(text):
+ declaration = match.group(1).strip()
+ selector_parts = re.findall(r'([A-Za-z_][A-Za-z0-9_]*:)', declaration)
+ if selector_parts:
+ members.add("".join(selector_parts))
+ continue
+
+ simple_match = re.match(r'([A-Za-z_][A-Za-z0-9_]*)', declaration)
+ if simple_match:
+ members.add(simple_match.group(1))
+
+ return members
+
+
+def _extract_swift_sdk_members(text: str) -> set:
+ members = set()
+ for match in SWIFT_INTERFACE_FUNC_RE.finditer(text):
+ members.add(match.group(1))
+ for match in SWIFT_INTERFACE_PROPERTY_RE.finditer(text):
+ members.add(match.group(1))
+ return members
+
+
+def _extract_ios_system_symbols_from_sdk(sdk_path: str) -> set:
+ members = set(COMMON_SYSTEM_MEMBER_NAMES)
+ for file_path in _iter_ios_sdk_symbol_files(sdk_path):
+ try:
+ text = Path(file_path).read_text(encoding="utf-8", errors="ignore")
+ except Exception:
+ continue
+
+ if file_path.endswith(".swiftinterface"):
+ members.update(_extract_swift_sdk_members(text))
+ else:
+ members.update(_extract_objc_sdk_members(text))
+
+ return members
+
+
+def ensure_ios_system_symbols_loaded(verbose: bool = True) -> set:
+ global IOS_SYSTEM_SYMBOLS_LOADED
+
+ if IOS_SYSTEM_SYMBOLS_LOADED:
+ return IOS_SYSTEM_MEMBER_NAMES
+
+ sdk_paths = _discover_ios_sdk_paths()
+ symbols = _load_cached_ios_system_symbols(sdk_paths)
+ if symbols is None:
+ symbols = set(COMMON_SYSTEM_MEMBER_NAMES)
+ for sdk_path in sdk_paths:
+ symbols.update(_extract_ios_system_symbols_from_sdk(sdk_path))
+ if sdk_paths:
+ _save_ios_system_symbols_cache(sdk_paths, symbols)
+
+ IOS_SYSTEM_MEMBER_NAMES.clear()
+ IOS_SYSTEM_MEMBER_NAMES.update(symbols)
+ IOS_SYSTEM_SYMBOLS_LOADED = True
+
+ if verbose:
+ if sdk_paths:
+ print(f"🛡️ 已加载 {len(IOS_SYSTEM_MEMBER_NAMES)} 个 iOS SDK 系统方法/属性名")
+ else:
+ print("⚠️ 未找到 iOS SDK,当前仅使用内置系统方法保护列表")
+
+ return IOS_SYSTEM_MEMBER_NAMES
+
+# ── 提取阶段 ─────────────────────────────────────────────────────────────────
+
+# 匹配声明前修饰符(注解行或同行),用于过滤不应混淆的符号
+UNSAFE_MODIFIERS_RE = re.compile(
+ r"@objc\b|@objcMembers\b|@IBAction\b|@IBOutlet\b|@IBInspectable\b"
+ r"|@NSManaged\b|@NSApplicationMain\b|@UIApplicationMain\b"
+ r"|\bopen\b|\bpublic\b"
+)
+
+# 捕获类型声明: [class|struct|enum|protocol] TypeName
+DECL_RE = re.compile(
+ r"(? re.Pattern:
+ lengths = sorted(set(LEGACY_OBFUSCATED_HASH_LENGTHS))
+ suffix = "|".join(f"[a-f0-9]{{{length}}}" for length in lengths)
+ prefixes = "|".join(re.escape(prefix) for prefix in sorted(set(ALREADY_OBFUSCATED_PREFIXES), key=len, reverse=True))
+ return re.compile(rf"^(?:{prefixes})(?:{suffix})$")
+
+
+ALREADY_OBFUSCATED_RE = build_already_obfuscated_re()
+
+# 方法抽取正则(Swift / Obj-C)
+SWIFT_FUNC_RE = re.compile(r"^\s*(?:@[^\n]+\s*)*(?:public|private|fileprivate|internal|open|static|class|override|final|mutating|nonmutating|async|throws|rethrows|lazy|weak|unowned|convenience|required|optional|override|protocol|extension|extension\s+|actor|struct|enum|actor\s+)?\s*func\s+([a-zA-Z_][a-zA-Z0-9_]+)")
+SWIFT_METHOD_DECL_RE = re.compile(r"\bfunc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(")
+SWIFT_PARAM_LABEL_RE = re.compile(
+ r"^\s*(?:@[A-Za-z_][A-Za-z0-9_.]*(?:\([^)]*\))?\s+)*"
+ r"(?:(?:inout|borrowing|consuming|isolated|__shared|__owned|sending)\s+)*"
+ r"(?P