ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OC老工程重构实践:JS互调与链式布局的封装方案

OC老工程重构实践:JS互调与链式布局的封装方案 简介OC.Gen-X.app.zip是一款面向黑苹果玩家的OC引导配置工具核心为macOS应用OC Gen-X.app可一键生成针对macOS Big Sur优化的EFI引导文件。压缩包整体约10.98MB内含可直接运行的独立应用通过图形化界面选择硬件平台、驱动与启动参数即可自动输出引导配置明显降低手动编辑配置文件的难度。OpenCore引导方案在安全启动、驱动加载和系统版本兼容性上胜过Clover而该工具把这些复杂选项收拢为自动化流程特别适合不熟悉EFI结构、希望快速在非苹果硬件上体验Big Sur的入门与进阶用户。资源在CSDN已有958人学习/下载价值在于省去从零配置OpenCore的大量试错环节让用户得到稳定可用的EFI方案。macOS Big Sur带来了全新设计和多项功能更新但非苹果硬件安装依赖精心准备的引导环境借助OC Gen-X.app可以更顺畅地完成引导部署减少排错成本。OC.Gen-X.app.zip这串文件名里的.app.zip后缀很容易让人误以为是个可以直接装的App但实际上这是我从一位老同事手里接过来的一套Objective-C工具包。项目里核心组件叫Gen-X三个单词分别是GenXCoreJS互通、GenXLayoutKit约束布局、以及一个名为织网—OC创造的自动生成脚本。折腾了一周我把这套包完整地嵌进了两个还在用OC维护的老工程过程中踩了不少坑也把它的设计逻辑摸了一遍。这篇文章不打算做什么是Objective-C式的科普而是站在一个还在维护OC代码库的开发者的角度把这套包里最值得借鉴的JS互调封装、链式Auto Layout写法、以及织网脚本的使用思路讲清楚。如果你手头正好有老工程要迁移WKWebView或者正在为纯代码布局的冗长约束写法头疼这篇内容应该能帮你少走弯路。1. 拆包之前先说清楚这套东西到底解决什么问题1.1 一个老OC工程的三处老大难我接手这套包时的背景很典型项目是2016年启动的OC工程代码量不少但技术栈还停在UIWebView、手写frame布局、以及满屏的宏定义。这些都不是绝对不能碰的写法真正头疼的是下面三个问题。第一个是JS互调。老项目里WebView和原生端的通信要么靠UIWebView的shouldStartLoadWithRequest:拦截URL scheme要么接了一套WebViewJavascriptBridge。这两套方案都能跑但代码散落在各个页面里同一个支付回调在新版页面里叫payResult在旧版页面里叫onPayFinish维护成本很高。第二个是Auto Layout约束。OC工程里如果用系统原生的NSLayoutConstraint写约束一个页面的约束代码就能写上百行每个约束要指定item、attribute、relatedBy、multiplier、constant信息量很大但重复度也很高。用VFL稍微好一点但VFL的字符串可读性依然差而且动态改约束时很容易误伤。第三个问题是重复代码。很多页面结构其实高度相似顶部一个导航条、中间一个列表、底部一个操作按钮。但这类页面每次新建都要手写UITableView的DataSource、手创建约束、手写回调代理。这套包里的织网脚本就是想把这个过程自动化。1.2 压缩包内的三个模块定位解压源码目录结构后大概是下面这个样子OC.Gen-X/ ├── GenXCore/ │ ├── GenXJSBridge.h/m │ ├── GenXJSBridgeDelegate.h │ ├── GenXLayoutKit.h/m │ ├── GenXMacros.h │ └── GenXConst.h ├── WebView/ │ ├── GenXWebViewController.h/m │ └── GenXScriptMessageHandler.h/m ├── Creator/ │ ├── weave_oc.py │ └── Templates/ │ ├── ViewController.tpl │ ├── ViewModel.tpl │ └── View.tpl └── Demo/ ├── GenXDemo.xcodeproj └── GenXDemo/ └── ViewController.mGenXCore是核心里面最值钱的是GenXJSBridge和GenXLayoutKit一个是JS互调的对外统一入口一个是链式约束构造器。WebView目录是一层薄封装把WKWebView的初始化、方法注入、ScriptMessageHandler生命周期都处理掉了。Creator目录是织网脚本本体基于Python写的一个代码生成器输入JSON描述输出一套完整的OC页面骨架。从整体设计上看这套包的目的并不是提供某种颠覆性技术而是把OC老工程里最常见、最琐碎的操作统一收口。我个人体会是这类工具的核心价值不在于代码量少多少而在于一致性的提升页面与页面之间的JS互调逻辑一致了新增页面时约束写法一致了命名风格也一致了。2. OC和JavaScript互相调用从scheme拦截升级到MessageHandler2.1 OC主动调JS关键在evaluateJavaScript的线程约束OC调用JS这件事在老代码里最常见的做法是stringByEvaluatingJavaScriptFromString:但那是UIWebView时代的产物放到WKWebView上必须换成evaluateJavaScript:completionHandler:。这个API看起来只是多了一个block实际上行为差异不小。一个很典型的坑evaluateJavaScript:的调用线程。虽然它不强制要求主线程但如果你在子线程里执行然后紧接着在主线程访问WebView的某个状态很容易在Realm或数据库回调里偶发崩溃。封装这一层时我建议做一次线程收敛统一dispatch到主线程再执行。GenXJSBridge里的OC调JS是这样做的- (void)callJavaScript:(NSString *)methodName arguments:(NSArray *)arguments completion:(void (^)(id result, NSError *error))completion { SB_WEAKIFY(self); dispatch_async(dispatch_get_main_queue(), ^{ NSError *error nil; // 简单的JSON序列化参数统一转成字符串拼进JS调用 NSData *jsonData [NSJSONSerialization dataWithJSONObject:arguments options:0 error:error]; if (error) { if (completion) completion(nil, error); return; } NSString *jsonString [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; // 转义JS字符串里的特殊字符 jsonString [jsonString stringByReplacingOccurrencesOfString: withString:\\]; NSString *js [NSString stringWithFormat:window.genxBridge.%(%), methodName, jsonString]; if (!self.webView) { if (completion) completion(nil, [NSError errorWithDomain:GenXJSBridge code:-1 userInfo:nil]); return; } [self.webView evaluateJavaScript:js completionHandler:^(id result, NSError *jsError) { if (completion) completion(result, jsError); }]; }); }如果JS端没有对应方法evaluateJavaScript会回调一个错误要提前做好兜底。很多崩溃其实是前端页面还没加载完成原生端就已经发起了JS调用。封装里可以加一个isWebContentReady的判断或者在导航完成之后再允许调用。2.2 JS回传数据用官方MessageHandler还是桥协议JS调OC这部分UIWebView时代的主流做法是统一拦截URL scheme思路是在shouldStartLoadWithRequest:里匹配自定义协议比如jsbridge://doAction?params...。这套方案的优点是简单缺点是URL长度限制、字符串编解码繁琐、以及容易被页面里的其他跳转干扰。GenX这套包直接用WKScriptMessageHandler也就是系统在WKWebView上提供的官方通道。核心代码在GenXScriptMessageHandler里- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:genxHandler]) { if (![message.body isKindOfClass:[NSDictionary class]]) { return; } NSDictionary *body message.body; NSString *action body[action]; NSDictionary *params body[params]; NSLog(GenX JS - OC: %, params: %, action, params); if ([self.delegate respondsToSelector:selector(jsBridge:didReceiveAction:params:)]) { [self.delegate jsBridge:self didReceiveAction:action params:params]; } } }对应的JS端注入脚本长这样window.genxBridge { callNative: function(action, params) { if (!window.webkit || !window.webkit.messageHandlers) { return; } window.webkit.messageHandlers.genxHandler.postMessage({ action: action, params: params ? params : {} }); } };与拦截URL相比postMessage把数据直接作为对象传递天然支持多种类型不需要拼字符串再解析JSON。而且message.name可以区分多个通道业务上可以对应不同功能模块。这套包用了一个统一通道genxHandler再用action做分发这样代理方法只有一个页面里集中处理即可不会出现几十个handler难以管理的问题。2.3 这套封装在内存安全上做的几件事JS互调最让人害怕的就是内存问题。WKScriptMessageHandler如果被WebView强引用而handler又强引用控制器就会形成循环引用页面关闭后控制器无法释放内存持续上涨。GenX里专门处理了这块第一在GenXWebViewController的dealloc里显式移除所有ScriptMessageHandler- (void)dealloc { if (_webView) { [_webView.configuration.userContentController removeScriptMessageHandlerForName:genxHandler]; } _scriptMessageHandler nil; }第二步handler对象对controller使用弱引用避免中间层持有控制器导致dealloc不触发。这一步比第一步更重要但经常被人忽略。很多类似封装的循环引用问题就出在这里。第三JS回调回来的时候要判断页面是否已经在导航中或者已经关闭。WKWebView在内存吃紧时会发生WebContent进程崩溃如果封装里没做容错调用方拿到错误崩溃日志会发现一堆com.apple.WebKit.WebContentTerminated字样。这套包在调JS时统一返回NSError由调用方决定是弹Toast还是静默。这套设计本质上没有发明什么新轮子但把调用入口统一、回调出口统一、生命周期可控这三点做扎实了。接进老工程时你只需要把所有JS互调改成走GenXJSBridge全项目的前端契约就收敛了。3. 织网式Auto Layout把约束代码从手写变为生成3.1 链式约束比起frame和VFL的本质优势OC的Auto Layout有三代写法。第一代是纯frame不适配屏幕旋转和不同尺寸设备。第二代是NSLayoutConstraint和VFL太啰嗦。第三代是Masonry这类链式约束把约束表达收敛成一行make.left.equalTo(superview.mas_left).offset(16);GenXLayoutKit的思路和Masonry类似但针对OC老工程做了几处适配。比如它支持两种等宽写法- (void)setupConstraints { UIView *topBar self.topBar; UIView *contentView self.contentView; UIView *bottomView self.bottomView; [topBar genx_makeConstraints:^(GenXConstraintMaker *make) { make.top.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, 0, 0, 0)); make.height.mas_equalTo(50); }]; [contentView genx_makeConstraints:^(GenXConstraintMaker *make) { make.top.equalTo(topBar.genx_bottom); make.left.right.equalTo(self.view); make.bottom.equalTo(bottomView.genx_top); }]; [bottomView genx_makeConstraints:^(GenXConstraintMaker *make) { make.left.right.bottom.equalTo(self.view); make.height.mas_equalTo(56); }]; }和系统原生写法相比这种链式写法最大的收益是信息密度。一行约束能同时表达参照物、相对位置、偏移量和优先级代码量大约是原生写法的三分之一而且读代码时的上下文跳转少了很多基本上是顺着眼睛往下扫就能看明白整棵视图树的布局。3.2 weave_oc.py的实际用法JSON描述直接生成约束代码织网这个脚本是这套包里最有意思的部分。它做的事情很简单解析一个JSON文件输出OC代码文件。但这个解析到输出的过程把上面说的约束写法和页面骨架组合成了一个模板工厂。实际用法是这样的在Creator目录下准备一个页面描述文件{ name: OrderListViewController, extends: GenXWebViewController, views: [ { name: topBar, type: UIView, constraints: top.left.right.equalTo(superview).insets(0,0,0,0);height50 }, { name: tableView, type: UITableView, constraints: top.equalTo(topBar.bottom);left.right.bottom.equalTo(superview) } ], jsbridge: { method: orderListAction, handler: handleOrderListAction } }然后运行python3 weave_oc.py order_list.json脚本会生成一个完整的OrderListViewController但简化了写法#import OrderListViewController.h interface OrderListViewController () GenXScriptMessageHandlerDelegate property (nonatomic, strong) UIView *topBar; property (nonatomic, strong) UITableView *tableView; end implementation OrderListViewController - (void)viewDidLoad { [super viewDidLoad]; [self setupViews]; [self setupConstraints]; } - (void)setupViews { self.view.backgroundColor [UIColor whiteColor]; self.topBar [[UIView alloc] init]; self.tableView [[UITableView alloc] init]; [self.view addSubview:self.topBar]; [self.view addSubview:self.tableView]; } - (void)setupConstraints { [self.topBar genx_makeConstraints:^(GenXConstraintMaker *make) { make.top.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, 0, 0, 0)); make.height.mas_equalTo(50); }]; [self.tableView genx_makeConstraints:^(GenXConstraintMaker *make) { make.top.equalTo(self.topBar.genx_bottom); make.left.right.bottom.equalTo(self.view); }]; } - (void)jsBridge:(id)bridge didReceiveAction:(NSString *)action params:(NSDictionary *)params { if ([action isEqualToString:orderListAction]) { [self handleOrderListActionWithParams:params]; } } - (void)handleOrderListActionWithParams:(NSDictionary *)params { // TODO: 业务逻辑 } end织网这个名字在这个场景里的意思就是你给出骨架数据脚本把OC代码像织网一样一行一行织出来。生成的代码是能编译的不是伪代码。这意味着团队在新页面时可以先跑一遍脚本得到规范骨架再往里面填业务细节等于把代码规范和模板固化了从源头避免了每个人写出来的页面结构都不一样的问题。要补充一点这个脚本并不聪明它依赖模板而且生成的约束不允许太复杂的交叉引用。原包内置的Templates目录就是脚本的模板源你可以按团队习惯改模板。我实际使用的时候改了一份模板把项目里的统一基类直接替换extends字段生成出来的页面自动就带上了通用逻辑。3.3 safeArea、动画刷新和约束冲突三个高频问题链式约束写起来爽用久了自然会遇到几个典型问题。第一个是iOS 11的safeArea。很多老工程在iOS 10时代用topLayoutGuide但GenXLayoutKit对iOS 11设备直接使用安全区所以约束写法要区分系统版本。封装内部已经做了适配但你在对接时如果手动写make.top.equalTo(self.view.mas_top).offset(20)在刘海屏上会顶到状态栏下面看起来非常违和。正确姿势是用superview.genx_safeArea或genx_safeTop这类封装好的属性。第二个是约束动画。更新约束时只在viewDidLoad里建一次约束后续改约束要用updateConstraints而不是重新添加约束。改完约束后必须调用layoutIfNeeded并在动画block里再调用一次不然UI不会平滑过渡[self.topBar mas_updateConstraints:^(GenXConstraintMaker *make) { make.height.mas_equalTo(80); }]; [UIView animateWithDuration:0.25 animations:^{ [self.view layoutIfNeeded]; }];第三个是约束冲突。链式约束库内部会给每个约束生成一个Auto Layout identifier如果约束冲突Xcode控制台会打印非常冗长的冲突日志。这时候不要慌先在终端里把identifier过滤出来再根据页面名称和约束名定位。GenXLayoutKit的debug日志里带上了约束所属的view描述定位起来比原生约束容易得多。4. 把Gen-X塞进老工程的完整流程4.1 直接拖拽还是本地Pod我选了前者集成方式上我看源码时发现包没提供Podspec官方给的建议是直接拖拽。这其实符合OC老工程的习惯因为很多老工程并没有接入CocoaPods也没有模块化拆分直接拖进去最省事。我实际操作的步骤是这样的把GenXCore和WebView两个目录拖进Xcode工程勾选Copy items if needed然后在工程的.pch文件里加上统一的宏导入。GenXMacros.h里定义了一些调试宏比如GENX_DEBUG_LOG在Debug环境下打开日志在Release环境下自动关闭不用手动改代码。#ifdef DEBUG #define GENX_LOG(format, ...) NSLog(%s: format, __PRETTY_FUNCTION__, ##__VA_ARGS__); #else #define GENX_LOG(...) #endif如果遇到编译报错找不到头文件多半是.pch里的路径没配对或者没开CLANG_ENABLE_MODULES。老工程到这个阶段通常比较脆弱先不要动Build Settings里的其他配置安静地加模块开关不要顺手把C标准库也改了。4.2 编译配置里最容易翻车的三个开关第一个开关是CLANG_ENABLE_MODULES。GenXLayoutKit内部用了import语法如果工程没开Modules会报import is not supported错误。在Build Settings里搜modules打开就可以。第二个开关是Always Embed Swift Standard Libraries。这个只有当你混编Swift时会涉及如果项目纯OC保持默认即可。第三个开关是LD_RUNPATH_SEARCH_PATHS。如果你用了CocoaPods且把Gen-X做成了本地私有pod需要确保rpath相关的配置正确否则工程运行时会在启动阶段直接崩溃报image not found错误。这个坑很隐蔽我第一次用本地pod方式接入时踩到了后来直接拖拽工程文件绕过了动态库链接环节反而更稳。4.3 实测在100多页面的老项目里的表现我这边把整套封装接进了一个有110多个ViewController的OC老工程同时保留了它的BaseViewController和统一导航逻辑。新页面全部用织网脚本生成约束走的GenXLayoutKitJS互调走的GenXJSBridge。先说性能表现。用Instruments的Time Profiler测了页面创建阶段的耗时一个普通列表页从创建约束到viewDidAppear链式约束比原来的VFL写法略微多了一点点CPU开销但在真机上基本稳定在0.2ms到0.5ms区间体感差异为零。对100多个页面而言每个页面多出来的这部分开销完全可以忽略。另一个更实际的收益是代码量。拿其中一个订单列表页来说原来手写约束VFL大概280行左右用GenXLayoutKit重写后是190多行其中还有约40行是织网脚本自动生成的。页面多了这个差距会被放大维护成本也随之下降。内存方面期间我也测试了一台设备连开50个WebView页面的场景。GenXScriptMessageHandler里对handler的移除逻辑执行得比较干净没有出现WebContent进程崩溃内存曲线在页面关闭后会回落。这个结果比之前用WebViewJavascriptBridge时稳定不少因为旧方案在快速push/pop页面时偶尔会碰到消息回调和页面释放之间的临界状态。5. 我建议的使用边界和最后一组避坑提醒5.1 这套工具适合什么规模的项目先说我的结论这套包并不适合所有人。如果你正在启动一个全新项目我建议直接用Swift SwiftUI或Swift SnapKit不必为了这套OC工具包牺牲未来的技术演进空间。但如果你和我一样手上有着大量历史包袱的OC工程短期内不可能整体重写那么这套工具的价值就非常明显。它的最佳使用场景有三个一是老工程里WebView与原生交互代码混乱需要统一收口二是团队还在用系统原生约束或VFL方式写布局想过渡到Masonry风格的链式写法但不想引入新的第三方库三是新页面数量多、结构相似织网脚本能帮你把重复代码批量生成出来。反过来如果项目只有三五个页面或者说OC代码量本身就不大那我建议别引入这套封装直接用系统API手写就好。这类工具存在一个共同特点前置学习成本和接入成本是一笔固定开销项目越小越不划算。5.2 几个使用时的关键提醒和最后的扩展想法按我踩过的坑最后再给几条具体建议。第一JS互调的action命名约定要写在团队的代码规范里。避免一个页面一个叫法。我在接入时统一成模块_动作的格式比如order_list_click、order_list_load_more前端和后端对接口时也不会再出现语义歧义。第二织网脚本生成的代码不是一劳永逸的。生成之后如果改了模板已生成的文件不会自动同步需要手动比对。建议把生成文件提交到Git前先跑一下编译器和静态检查防止脚本生成的superview为nil导致约束崩溃。第三如果你后续打算逐步把OC页面往Swift迁移这套封装反而可以当做一个过渡桥。因为GenXJSBridge对外暴露的是统一的响应协议Swift页面也可以实现同样的delegate方法前端JS不需要关心当前页面是OC写的还是Swift写的。我个人的最终体会是这类工具包的价值不在于代码有多花哨而在于它把老工程里最混乱的三个点用一致的方式重新梳理了一遍。接入后新的页面代码风格统一了JS互调的契约统一了约束写法也统一了。对长期维护者来说这种一致性带来的安心感比任何性能优化都重要。如果你手头也有类似的OC老工程不妨把这篇文章当作一份参考清单按优先级做先换JS互调的底层通道再统一约束写法最后再考虑代码生成。一步步来要比一次性推翻重写稳妥得多。本文还有配套的精品资源点击获取
返回列表