一、 iOS原生基础
- Swift/OC语言特性
问题1:Swift中的struct和class有什么区别?在混合开发中何时用struct?
答案:
· 核心区别: · class是引用类型,传递的是指针;struct是值类型,传递的是副本。 · class支持继承,struct不支持。 · class有析构函数(deinit),struct没有。 · class的实例可以被多次引用,struct的实例在赋值给新变量时会被复制。 · 混合开发中的使用场景: · 使用 struct:当需要封装简单的数据模型在RN和原生之间传递时(例如,配置对象)。因为它的值类型特性可以避免意外的数据修改,并且线程安全。 · 使用 class:当需要封装一个有状态、有生命周期的原生模块或UI组件时(例如,一个视频播放器管理器),因为引用类型更符合对象的概念。
问题2:什么是内存管理(ARC)?在RN混合开发中需要注意什么?
答案:
· ARC(自动引用计数): Swift/OC通过计算对象的引用数量来管理内存。当引用计数为0时,对象会被自动销毁。 · RN混合开发中的注意事项: · 循环引用:这是最常见的问题。当原生模块持有了RN的RCTBridge或回调Block,而RN又持有了该原生模块时,就会形成循环引用,导致内存泄漏。 · 解决方案: 1. 在Block或闭包内使用 [weak self] 来弱引用self。 2. 避免在原生模块中强引用RCTBridge,如果需要,使用weak引用。 3. 确保从RN传递过来的回调Block不会长期被原生端强持有。
- UI与视图系统
问题1:如何封装一个原生的UI组件给RN使用?
答案: 核心是创建一个继承自RCTViewManager 的类。
步骤(以Swift为例):
- 创建ViewManager:
// MyCustomViewManager.swift @objc(MyCustomViewManager) class MyCustomViewManager: RCTViewManager { override func view() -> UIView! { return MyCustomNativeView() // 返回你的原生UIView子类 } // 导出属性,让JS可以设置 @objc func setText(_ node: NSNumber, text: String) { DispatchQueue.main.async { // UI操作必须在主线程 let component = self.bridge.uiManager.view(forReactTag: node) as! MyCustomNativeView component.text = text } } } - 导出模块和属性:
// 需要Objective-C宏来导出 #import <React/RCTViewManager.h> @interface RCT_EXTERN_MODULE(MyCustomViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(isEnabled, BOOL) // 导出简单属性 RCT_EXTERN_METHOD(setText:(nonnull NSNumber *)node text:(NSString *)text) // 导出方法 @end - 在JS中使用:
import { requireNativeComponent } from 'react-native'; const MyCustomView = requireNativeComponent('MyCustomView'); // ... <MyCustomView isEnabled={true} style={...} />
问题2:如何封装一个原生的模块(非UI)给RN调用?
答案: 核心是创建一个遵循RCTBridgeModule 协议的类。
步骤(以Swift为例):
- 创建原生模块:
// CalendarManager.swift @objc(CalendarManager) class CalendarManager: NSObject, RCTBridgeModule { static func moduleName() -> String! { return "CalendarManager" // 模块名 } // 导出方法给JS调用,默认在主队列执行 static func requiresMainQueueSetup() -> Bool { return false // 如果初始化不需要主线程,返回false性能更好 } // 导出一个方法给JS @objc func addEvent(_ name: String, location: String, callback: RCTResponseSenderBlock) -> Void { // 在这里执行原生操作... let result = ["success": true, "eventId": "123"] callback([result]) // 回调给JS,参数是一个数组 } } - 导出模块和方法(如果需要):
// CalendarManager.m (桥接文件) #import <React/RCTBridgeModule.h> @interface RCT_EXTERN_REMAP_MODULE(CalendarManager, CalendarManager, NSObject) RCT_EXTERN_METHOD(addEvent:(NSString *)name location:(NSString *)location callback:(RCTResponseSenderBlock)callback) @end - 在JS中使用:
import { NativeModules } from 'react-native'; const { CalendarManager } = NativeModules; CalendarManager.addEvent('生日派对', '家里', (error, events) => { if (error) { console.error(error); } else { console.log('事件创建成功', events); } });
二、 React Native核心
- 基础与原理
问题1:React Native的通信机制(Bridge/JSI)是怎样的?
答案:
· 旧架构(Bridge): · 异步序列化: JS和原生之间的所有通信都是异步的。 · 桥接: 数据被序列化成JSON消息,通过一个“桥”(Bridge)进行传递。 · 瓶颈: 频繁的、大量的数据传输会成为性能瓶颈(如滚动列表),因为序列化和反序列化开销大。 · 新架构(JSI + Fabric + TurboModules): · JSI(JavaScript Interface): 允许JS直接调用原生方法,无需序列化。它用一个C++层作为中间件,让JS可以持有原生对象的引用。 · Fabric: 新的渲染系统,允许同步渲染。JS和UI线程可以直接通信,减少了异步带来的白屏和闪烁。 · TurboModules: 新的原生模块系统,支持懒加载,类型安全,并且通过JSI直接调用,性能更高。
问题2:RN新架构相比旧的Bridge架构有哪些优势?
答案:
特性 旧架构 (Bridge) 新架构 (JSI/Fabric/TurboModules) 通信方式 异步,序列化 同步,直接调用 性能 较差,有序列化开销 显著提升,尤其启动速度和交互响应 类型安全 无,靠文档约定 有,通过Codegen生成接口 模块加载 启动时全部加载 懒加载,启动更快 渲染 异步,可能导致闪烁 同步,更流畅
- 开发与生态
问题1:常用的性能优化手段有哪些?
答案:
· 减少重渲染: · 使用 React.memo 包装函数组件。 · 使用 useMemo 缓存昂贵的计算结果。 · 使用 useCallback 缓存函数,避免子组件不必要的重渲染。 · 列表优化: · 始终使用 FlatList 或 SectionList。 · 实现 getItemLayout 避免动态测量内容高度(用于固定高度项)。 · 使用 keyExtractor 提供稳定的key。 · 合理使用 windowSize 和 maxToRenderPerBatch。 · 图片优化: · 使用合适尺寸的图片。 · 使用 react-native-fast-image 库进行缓存和预加载。 · JS线程优化: · 避免在JS线程进行大量计算,可将复杂任务移到原生端或使用 Worklets(React Native Reanimated)。
问题2:如何管理RN项目的依赖?autolinking是什么?
答案:
· 管理工具: 主要使用 npm 或 yarn 来管理JS依赖,使用 CocoaPods(iOS)和 Gradle(Android)来管理原生依赖。 · react-native link(旧): 手动将库的原生代码链接到iOS和Android项目。 · autolinking(新): RN 0.60+ 引入的自动链接机制。 · 工作原理: 当通过npm安装一个包含原生代码的库时,库的 package.json 中有一个 "react-native" 配置指向其原生代码。 · iOS: 运行 pod install,CocoaPods会自动读取所有已安装库的 *.podspec 文件并将其集成到项目中。 · 开发者只需: npm install -> cd ios && pod install。
三、 混合开发实战与架构
- 通信与集成
问题1:请描述在RN中调用原生模块(如相机)的完整步骤。
答案:
- 封装原生模块: 如上所述,创建一个 RCTBridgeModule,例如 CameraManager。
- 实现原生功能: 在 CameraManager 中,使用 AVFoundation 等原生API打开相机、拍照。
- 暴露方法给JS: 使用 RCT_EXTERN_METHOD 暴露一个如 openCamera 的方法。
- 处理回调/Promise: · 回调: 使用 RCTResponseSenderBlock。 · Promise: 使用 RCTPromiseResolveBlock 和 RCTPromiseRejectBlock。
- 线程处理: 确保UI操作在主线程(DispatchQueue.main.async)。
- 在JS中调用:
import { NativeModules } from 'react-native'; const { CameraManager } = NativeModules; const takePhoto = async () => { try { const photoUrl = await CameraManager.openCamera(); console.log('照片路径:', photoUrl); } catch (error) { console.error('打开相机失败:', error); } };
问题2:如何从原生代码向RN发送事件?
答案: 使用RCTEventEmitter。
-
原生端: 让你的模块继承自 RCTEventEmitter。
@objc(EventEmitterManager) class EventEmitterManager: RCTEventEmitter { override func supportedEvents() -> [String]! { return ["onScanResult"] // 定义支持的事件名 } @objc func sendResultToJS(_ result: String) { // 发送事件给JS sendEvent(withName: "onScanResult", body: ["result": result]) } } -
JS端: 创建一个 NativeEventEmitter 实例并订阅事件。
import { NativeEventEmitter, NativeModules } from 'react-native'; const { EventEmitterManager } = NativeModules; const eventEmitter = new NativeEventEmitter(EventEmitterManager); useEffect(() => { const subscription = eventEmitter.addListener('onScanResult', (data) => { console.log('收到扫描结果:', data.result); }); // 清理订阅 return () => subscription.remove(); }, []); -
导航集成
问题:在混合App中,如何实现RN和原生的混合导航?
答案: 有两种主流方案:
-
RN主导导航(推荐用于RN内容多的App): · 使用 react-native-navigation (Wix) 或 react-navigation 的原生栈。 · 原理: 每个原生屏幕(如ViewController)都是一个容器,里面承载一个完整的RN导航栈。 · 实现: 通过原生模块打开一个新的原生容器,并传递componentName等参数给RN,让RN内部进行页面跳转。
-
原生主导导航(推荐用于以原生为主,嵌入RN页面的App): · 原理: 使用原生的 UINavigationController。 · 实现: · 创建一个 RCTRootView,将其设置为一个 UIViewController 的view。 · 将这个 UIViewController push 到原生的 UINavigationController 栈中。 · 从RN跳回原生,通过原生模块调用 popViewController。
-
部署与热更新
问题:CodePush的原理和流程是怎样的?Apple允许吗?
答案:
· 原理: CodePush是一个云服务,允许App从服务器动态下载并更新JS Bundle和资源文件,而无需通过App Store审核。 · 流程:
- 开发者在CodePush服务器上发布新的JS Bundle。
- App启动或从后台恢复时,会向CodePush服务器查询更新。
- 如果发现更新,则在后台静默下载。
- 下载完成后,在下次App启动时立即生效(或根据配置立即重启)。 · Apple审核指南: · 允许 使用热更新来修复Bug或更新业务逻辑。 · 禁止 用于修改App的核心功能和行为,或分发违反App Store规定的内容(如赌博、色情)。不能绕过Apple的审核流程来发布本质上是一个新App的更新。
四、 非技术问题(软实力与项目经验)
问题1:在混合开发中遇到的最大挑战是什么?如何解决的?
参考答案(请根据自身经历修改):
· 挑战: RN页面在低端Android设备上启动白屏时间过长。 · 分析与解决:
- 定位问题: 使用性能分析工具(如Flipper、Systrace)发现,JS Bundle加载、解析和执行是主要耗时点。
- 解决方案: · 拆包: 使用 metro 进行JS Bundle拆分,将基础库和业务代码分离,实现按需加载。 · 预加载: 在App启动后,在空闲时间预加载RN运行环境。 · 优化图片: 对RN内的图片进行压缩和格式转换(WebP)。 · 升级RN版本: 利用新架构(JSI)的性能优势。
- 结果: 白屏时间从 ~3秒 减少到 ~1.2秒,用户体验显著提升。
问题2:你是如何决定一个功能用原生开发还是RN开发的?
参考答案: 这是一个权衡的过程,主要考虑以下几点:
考虑维度 推荐使用原生 推荐使用React Native 性能要求 极高(复杂动画、大量计算) 一般业务场景,性能足够 原生特性 深度依赖新发布的系统API 已有成熟的RN社区库封装 开发效率 不追求跨平台,或功能单一 核心优势:一套代码,跨平台,快速迭代 团队技能 原生开发人员充足 JS/React开发人员为主 UI复杂度 高度定制、非标准UI 标准UI或可通过RN实现
通用策略: 用RN实现App的主体业务流和UI,用原生开发性能瓶颈模块(如视频编辑)、深度系统集成模块(如蓝牙通信)或复用已有的原生库。