Skip to content
目录

iOS 进阶

一、Hummer 拦截器

与 Android 不同,iOS 的拦截器通过注解的方式来实现,相关注解定义在 HMInterceptor 中,下面已 log 为例,实现在 LogInterceptor 文件中。

拦截器示例

objc
// 首先注册拦截器
HM_EXPORT_INTERCEPTOR(LogInterceptor)
// 实现拦截器方法 HMLoggerProtocol
- (BOOL)handleJSLog:(NSString *)log level:(HMLogLevel)level {
    NSLog(@"----- >> js log:%@",log);
    return YES;
}

- (BOOL)handleNativeLog:(NSString *)log level:(HMLogLevel)level {
    NSLog(@"----- >> native log:%@",log);
    return YES;
}

注意:部分拦截器存在“阻断”概念:一组同一类型的拦截器,如果位于前面的拦截器处理了拦截事件(返回 YES),则后面的拦截器不会被调用。

目前支持的拦截器如下:

拦截器类型调用时机是否阻断
HMInterceptorTypeEventTrack触发 eventLisenter 回调
HMInterceptorTypeImageimage 组件,即将设置图片
HMInterceptorTypeJSLoadhummer.evaluateScriptWithUrl
HMInterceptorTypeLogconsole 相关 api
HMInterceptorTypeNetwork创建 request,构造 resonse
HMInterceptorTypeReporterjs 执行异常
HMInterceptorTypeRouter页面跳转

Namespace

namespace 的作用与 android 一致。
iOS 通过对 context 设置达到一样的效果。代码见下一节容器内部实现。

二、Hummer 容器高级特性


1. 单页面容器

开发者可选择是否继承 HMViewController,继承则有以下能力:

  • 容器级别的生命周期管理。
  • 自动加载,执行 js。
  • 自动管理原生对象的生命周期(如 context,rootView 等)。
JS 示例
javascript
class RootView extends View {
    function onAppear() {
    
    }
    function onDisappear() {
    
    }
    function onDestroy() {
    
    }    
}
Hummer.render(new RootView());

INFO

使用 HMViewController 实例或子类加载上述 js,则默认支持生命周期函数。注意,该生命周期为 iOS ViewController 的生命周期!避免在 onDestroy 做过多的事情。

Native 示例
objc
@interface ()
// context,pageView,hmRootView 应成对应关系。
@property (nonatomic, weak) HMJSContext  *context;
@property (nonatomic, strong) UIView *hmRootView;
@property (nonatomic, weak) UIView * pageView;// hummer.render()传入的视图对象。

@end
@implementation HMViewController
// 生命周期管理
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self callJSWithFunc:@"onAppear" arguments:@[]];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self callJSWithFunc:@"onDisappear" arguments:@[]];
}

- (void)dealloc {
    [self callJSWithFunc:@"onDestroy" arguments:@[]];
}
// 单 hummer 视图容器
- (void)initHMRootView {
    /** hummer渲染view */
    self.hmRootView = [[UIView alloc] initWithFrame:self.view.bounds];
    self.hmRootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:self.hmRootView];
}
- (void)viewDidLoad {
    // 加载 hummer 根视图
    [self initHMRootView];
    // 执行 脚本
    [self renderWithScript:script];
}
#pragma mark -渲染脚本
- (void)renderWithScript:(NSString *)script {
    if (script.length == 0) {
        return;
    }
    
    //设置页面参数
    NSMutableDictionary * pData = [NSMutableDictionary dictionary];
    if (self.URL) {
        pData[@"url"]=self.URL;
    }
    pData[@"params"] = self.params ?: @{};
    HMJSGlobal.globalObject.pageInfo = pData;
    
    //渲染脚本之前 注册bridge
    HMJSContext *context = [HMJSContext contextInRootView:self.hmRootView];
    context.nameSpace = @"test_namespace";
    HM_SafeRunBlock(self.registerJSBridgeBlock,context);
    
    //执行脚本
    [context evaluateScript:script fileName:self.URL];
    self.pageView = self.hmRootView.subviews.firstObject;
    self.context = [[HMJSGlobal globalObject] currentContext:self.pageView.hmContext];
    
    //发送加载完成消息
    [self callJSWithFunc:@"onCreate" arguments:@[]];
}
@end

2. 嵌入式页面容器

在单容器,多 hummer 视图的场景下,只需要再声明一个 hmRootView 和 context,分别执行各自的 JS 即可。

objc
@interface ()
@property (nonatomic, strong) UIView *hmRootView1;
@property (nonatomic, weak) HMJSContext  *context1;
@property (nonatomic, weak) UIView * pageView1;

@property (nonatomic, strong) UIView *hmRootView2;
@property (nonatomic, weak) HMJSContext  *context2;
@property (nonatomic, weak) UIView * pageView2;

@end

@implementation HMViewController

// ....
// 多 hummer 视图容器
- (void)initHMRootView {
    /** hummer渲染view */
    self.hmRootView1 = [[UIView alloc] initWithFrame:frame1];
    [self.view addSubview:self.hmRootView1];
    
    self.hmRootView2 = [[UIView alloc] initWithFrame:frame2];
    [self.view addSubview:self.hmRootView2];
}

#pragma mark -渲染脚本
- (void)renderWithScript:(NSString *)script1  script2:(NSString *)script2{
   
    // ....
    // 执行第一个脚本   
    HMJSContext *context = [HMJSContext contextInRootView:self.hmRootView1];    
    [context evaluateScript:script1 fileName:self.URL];
    self.pageView1 = self.hmRootView1.subviews.firstObject;
    self.context1 = [[HMJSGlobal globalObject] currentContext:self.pageView1.hmContext];

    // 执行第二个脚本  
    HMJSContext *context2 = [HMJSContext contextInRootView:self.hmRootView2];    
    [context2 evaluateScript:script2 fileName:self.URL];
    self.pageView2 = self.hmRootView2.subviews.firstObject;
    self.context2 = [[HMJSGlobal globalObject] currentContext:self.pageView2.hmContext]; 
}    

// ...
@end

注意

无论是单容器单页面,还是单容器嵌套多 Hummer 页面,容器的生命周期还是一个。并且依赖 iOS 原生的生命周期机制。

三、实现自定义导出组件

iOS 同样使用注解的方式实现 js 和 native 的对应关系。 比如我们要实现一个 MyComponent 组件,其中包括如下:

属性
  • style(实例属性)
  • version(类属性)
方法
  • show(实例方法)
  • className(类方法)
  • doSomething(方法带参数,返回值)

则示例代码如下:

Naive 示例

objc
@interface ()
@property (nonatomic,strong) NSDictionary *nativeStyle;
@end

@implementation MyComponent
// 第一个参数为 js 方法名,第二个参数为原生的 sel,
HM_EXPORT_METHOD(show, __show)

- (void)__show{
    //...
}
HM_EXPORT_CLASS_METHOD(className, __className)

+ (NSString *)__className{
    return @"MyComponent";
}

HM_EXPORT_METHOD(doSomething, __doSomething:)

- (BOOL)__doSomething:(HMBaseValue *)value {
    //需要 自己实现类型转换,可参考 JavascriptCore 的 JSValue 类型对应及转换。
    NSString *expectType = value.toString;

    //hummer 会根据返回值类型转换对应的js 类型。与 JSC 规则保持一致。
    return YES;
}

HM_EXPORT_PROPERTY(style, __style, __setStyle:)
- (NSDictionary *)__style {

    return self.nativeStyle;
}

- (void)__setStyle:(HMBaseValue *)value {
    self.nativeStyle = value.toDictionary;
}

HM_EXPORT_CLASS_PROPERTY(version, __version, __setVersion:)
static NSString *version;
+ (NSString *)__version {

    return version;
}

+ (void)__setVersion:(HMBaseValue *)value {
    version = value.toString;
}
@end

JS 侧使用示例

javascript
let component = new MyComponent()
component.style = {key:value}
let result:bool = component.doSomething('param')
MyComponent.className()
let version = MyComponent.version;

其他说明

  • 组件之间可以存在继承关系。例如想导出新的视图,则 iOS 侧需要继承 UIView。
  • 方法、属性中的参数都为 HMBaseValue 类型,需要 iOS 侧根据约定调用对应类型转换api,类型转换 api 和 JSC 一致。

四、Bridge 用法

在通过 Bridge 通信之前,需要明确一个概念:所有 JS 侧的对象(类)都对应 Native 的 HMBaseValue。

Native 向 JS 注入方法


动态注入方法 API
objc
@implementation HMBaseValue
- (void)setValue:(id)value forProperty:(NSString *)property;
@end

iOS 侧的方法注入类似 runtime 的动态添加机制:给对象添加方法则传入类,给类添加则传入类对象。
在此基础上,如果我们想要注入自定义的方法,则需要先获取对应的对象(类)。

1. Native 向 JS 静态类注册方法
objc
// 向 MyComponent 注入 类方法:version。。
// 通过导出方法回去到 MyComponent 对应的 js 类对象。
HM_EXPORT_METHOD(setClass, __setClass:)
+ (void)__setClass:(HMBaseValue *)value {
    
    HMFunctionType func = ^(NSArray *_Nullable argumentArray) {
    
        return @"1.0";
    };
    // 获取到js 类对象,通过 setValue 注入 HMFunctionType 闭包。
    [value setValue:func forProperty:@"version"];
}

INFO

js 中调用:

  • MyComponent.setClass(MyComponent);
  • MyComponent.version();
2. Native 向 JS 对象注册方法

跟上述类似,只是 HMBaseValue 为 js对象,而非实例。

Native 调用 JS 侧方法


Native 调用 JS 侧 API
objc
- (nullable HMBaseValue *)callWithArguments:(nullable NSArray *)arguments;

根据上面一节 HMBaseValue 的对象定义,要调用 JS 侧的方法。需要如下两步:

  1. 获取对应对象或类。
  2. 使用 callWithArguments: api
1. Native 调用 JS 侧类方法
javascript
class MyComponent {
    function version(){

    }
}

同样 将 MyComponent 传递到 Native 获取到对应的 HMBaseValue 后使用:

[HMBaseValue callWithArguments:@[]]

2. Native 调用 JS 某个对象的方法

同上。

最后说明

每一个 js 值(对象或类),在 native 侧都使用 HMBaseValue 表示,因此,只要可以拿到 HMBaseValue, 则可以进行注入操作。