React Native iOS原生模块开发实战|教程|心得|如何创建React Native iOS原生模块

尊重版权,未经授权不得转载
本文出自:贾鹏辉的技术博客(http://www.jb51.cc/article/p-ooangwyu-ty.html)

前言

一直想写一下我在React Native原生模块封装方面的一些经验和心得,来分享给大家,但实在抽不开身,今天看了一下日历发现马上就春节了,所以就赶在春节之前将这篇博文写好并发布(其实是两篇:要看Android篇的点这里《React Native Android原生模块开发》)。

我平时在用React Native开发App时会用到一些原生模块,比如:在做社会化分享、第三方登录、扫描、通信录,日历等等,想必大家也是一样。

关于在React Native中使用原生模块,在这里引用React Native官方文档的一段话:

有时候App需要访问平台API,但在React Native可能还没有相应的模块。或者你需要复用一些Java代码,而不想用JavaScript再重新实现一遍;又或者你需要实现某些高性能的、多线程的代码,譬如图片处理、数据库、或者一些高级扩展等等。
我们把React Native设计为可以在其基础上编写真正的原生代码,并且可以访问平台所有的能力。这是一个相对高级的特性,我们并不期望它应当在日常开发的过程中经常出现,但它确实必不可少,而且是存在的。如果React Native还不支持某个你需要的原生特性,你应当可以自己实现对该特性的封装。

上面是我翻译React Native官方文档上的一段话,大家如果想看英文版可以点这里:Native Modules
在这文章中呢,我会带着大家来开发一个从相册获取照片并裁切照片的项目,并结合这个项目来具体讲解一下如何一步步开发React Native iOS原生模块的。

提示:本文中所开发的项目的源码已开源到GitHub,供大家学习使用。

首先,让我们先看一下,开发iOS原生模块的主要流程。

开发iOS原生模块的主要流程

在这里我把构建React Native iOS原生模块的流程概括为以下三大步:

  1. 编写原生模块的相关iOS代码
  2. 暴露接口与数据交互;
  3. 导出React Native原生模块;

接下来让我们一起来看一下每一步所需要做的一些事情。

原生模块开发实战

在这里我们就以开发一个从相册获取照片并裁切照片的实战项目,来具体讲解一下如何开发React Native iOS原生模块的。

编写原生模块的相关iOS代码

这一步我们需要用到XCode。
首先我们用XCode打开React Native项目根目录下的iOS项目,如图:

接下来呢,我们就可以编写iOS代码了。

首先呢,我们先来实现一个Crop接口:

@interface Crop:NSObject<UIImagePickerControllerDelegate,UINavigationControllerDelegate>
-(instancetype)initWithViewController:(UIViewController *)vc;
typedef void(^PickSuccess)(NSDictionary *resultDic);
typedef void(^PickFailure)(Nsstring* message);
@property (nonatomic,retain) NSMutableDictionary *response;
@property (nonatomic,copy)PickSuccess pickSuccess;
@property (nonatomic,copy)PickFailure pickFailure;
@property(nonatomic,strong)UIViewController *viewController;

-(void)selectimage:(NSDictionary*)option successs:(PickSuccess)success failure:(PickFailure)failure;
@end

我们创建一个Crop.m,在这个类中呢,我们实现了从相册选择照片以及裁切照片的功能

/** * React Native iOS原生模块开发 * Author: CrazyCodeBoy * 技术博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:crazycodeboy@gmail.com */

#import "Crop.h"
#import <AssetsLibrary/AssetsLibrary.h>
@interface Crop ()
@property(strong,nonatomic)NSDictionary*option;
@end

@implementation Crop
-(instancetype)initWithViewController:(UIViewController *)vc{
  self=[super init];
  self.viewController=vc;
  return self;
}

-(void)selectimage:(NSDictionary*)option successs:(PickSuccess)success failure:(PickFailure)failure{
  self.pickSuccess=success;
  self.pickFailure=failure;
  self.option=option;
  UIImagePickerController *pickerController = [[UIImagePickerController alloc] init];
  pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
  pickerController.delegate = self;
  pickerController.allowsEditing = YES;
  [self.viewController presentViewController:pickerController animated:YES completion:nil];
}

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediawithInfo:(NSDictionary<Nsstring *,id> *)info{
  UIImage*image=[info objectForKey:@"UIImagePickerControllerEditedImage"];
  image=[self scaletoSize:image size:CGSizeMake([[self.option objectForKey:@"aspectX"]integerValue],[[self.option objectForKey:@"aspectY"]integerValue])];
  if(image){
    [self writetoFileWithImage:image outPut:[self getTempFile:[self getFileName:info]] handler:^(Nsstring *path) {
      [picker dismissViewControllerAnimated:YES completion:nil];
      self.pickSuccess(@{@"imageUrl": path});
    }];

  }else{
    self.pickFailure(@"获取照片失败");
    [picker dismissViewControllerAnimated:YES completion:nil];
  }

}
#pragma mark 裁剪照片
-(UIImage *)scaletoSize:(UIImage *)image size:(CGSize)size
{
  //并把他设置成当前的context
  UIGraphicsBeginImageContext(size);
  //绘制图片的大小
  [image drawInRect:CGRectMake(0,0,size.width,size.height)];
  //从当前context中创建一个改变大小后的图片
  UIImage *endImage=UIGraphicsGetimageFromCurrentimageContext();

  UIGraphicsEndImageContext();
  return endImage;
}
#pragma mark 将image写入沙盒
-(void)writetoFileWithImage:(UIImage*)image outPut:(Nsstring*)imagePath handler:(void(^)(Nsstring *path))handler{
  NSData *data= UIImageJPEGRepresentation(image,1);
  [data writetoFile:imagePath atomically:YES];
  dispatch_async(dispatch_get_main_queue(),^{
    handler(imagePath);
  });

}
#pragma mark 将指定url对于的图片写入沙盒
-(void)writetoFileWithUrl:(NSURL*)url outPut:(Nsstring*)imagePath handler:(void(^)(Nsstring *path))handler{
  ALAssetsLibrary *assetLibrary = [[ALAssetsLibrary alloc] init];
  if (url) {
    [assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
      ALAssetRepresentation *rep = [asset defaultRepresentation];
      Byte *buffer = (Byte*)malloc((unsigned long)rep.size);
      NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:((unsigned long)rep.size) error:nil];
      NSData *data = [NSData dataWithBytesNocopy:buffer length:buffered freeWhenDone:YES];
      [data writetoFile:imagePath atomically:YES];
      dispatch_async(dispatch_get_main_queue(),^{
        handler(imagePath);
      });
    } failureBlock:^(NSError *error) {
      dispatch_async(dispatch_get_main_queue(),^{
        handler(@"获取图片失败");
      });
    }];
  }
}
#pragma mark 获取临时文件路径
-(Nsstring*)getTempFile:(Nsstring*)fileName{
  Nsstring *imageContent=[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES) objectAtIndex:0] stringByAppendingString:@"/temp"];
  NSFileManager * fileManager = [NSFileManager defaultManager];
  if (![fileManager fileExistsAtPath:imageContent]) {
    [fileManager createDirectoryAtPath:imageContent withIntermediateDirectories:YES attributes:nil error:nil];
  }
  return [imageContent stringByAppendingPathComponent:fileName];
}
-(Nsstring*)getFileName:(NSDictionary*)info{
  Nsstring *fileName;
  Nsstring *tempFileName = [[NSUUID UUID] UUIDString];
  fileName = [tempFileName stringByAppendingString:@".jpg"];
  return fileName;
}
@end

查看源码

实现了从相册选择照片以及裁切照片的功能之后呢,接下来我们需要将iOS原生模块暴露给React Native,以供js调用

暴露接口与数据交互

接下了我们就向React Native暴露接口以及做一些数据交互部分的操作。为了暴露接口以及进行数据交互我们需要借助React Native的React/RCTBridgeModule.h在这里我们创建一个ImageCrop类让它实现RCTBridgeModule协议。

创建一个ImageCrop

ImageCrop.h

/** * React Native iOS原生模块开发 * Author: CrazyCodeBoy * 技术博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:crazycodeboy@gmail.com */


#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface  ImageCrop: NSObject <RCTBridgeModule>

@end

查看源码

ImageCrop.m

/** * React Native iOS原生模块开发 * Author: CrazyCodeBoy * 技术博文:http://www.devio.org * GitHub:https://github.com/crazycodeboy * Email:crazycodeboy@gmail.com */


#import "ImageCrop.h"
#import "Crop.h"

@interface ImageCrop ()
@property(strong,nonatomic)Crop *crop;
@end

@implementation ImageCrop


RCT_EXPORT_MODULE();

- (dispatch_queue_t)methodQueue 
{
  return dispatch_get_main_queue();
}
RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (root.presentedViewController != nil) {
      root = root.presentedViewController;
    }
  Nsstring*aspectXStr=[Nsstring stringWithFormat: @"%ld",aspectX];
  Nsstring*aspectYStr=[Nsstring stringWithFormat: @"%ld",aspectY];
  [[self _crop:root] selectimage:@{@"aspectX":aspectXStr,@"aspectY":aspectYStr} successs:^(NSDictionary *resultDic) {
    resolve(resultDic);
  } failure:^(Nsstring *message) {
    reject(@"fail",message,nil);
  }];
}
-(CroP*)_crop:(UIViewController*)vc{
  if(self.crop==nil){
    self.crop=[[Crop alloc] initWithViewController:vc];
  }
  return self.crop;
}

@end

ImageCrop类中,我们调用Crop类来实现从iOS相册中获取图片并裁切图片功能,在调用Crop的时候我们用的是懒加载的方式。为什么要用懒加载呢?这是为了避免当我们多次调用原生模块从相册选择照片的时候创建多个Crop实例情况的发生。

另外,需要特别提到的是,我们对Crop实例设置了强引用,这是为了防止在我们调用相册的时候Crop被回收,如果Crop被回收我们就无法收到选择照片之后的回调了,也就无法获取到照片。

暴露接口

在上述代码中我们通过RCT_EXPORT_METHOD宏来声明向React Native暴露的接口,这样以来我们就可以在js文件中通过ImageCrop.selectWithCrop调用我们所暴露给React Native的接口了。

接下来呢,我们来看一下原生模块和JS模块是如何进行数据交互的?

原生模块和JS进行数据交互

在我们要实现的从相册选择照片并裁切的项目中,JS模块需要告诉原生模块照片裁切的比例,等照片裁切完成后,原生模块需要对JS模块进行回调来告诉JS模块照片裁切的结果,在这里我们需要将照片裁切后生成图片的路径告诉JS模块。

提示:在所有的情况下js和原生模块之前进行通信都是在异步的情况下进行的。

接下来我们就来看下一JS是如何向原生模块传递数据的?

JS向原生模块传递数据:

为了实现JS向原生模块进行传递数据,我们可以直接通过调用原生模块所暴露出来的接口,来为接口方法设置参数。这样以来我们就可以将数据通过接口参数传递到原生模块中,如:

RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

通过上述代码我们可以看出,JS模块可以通过selectWithCrop方法来告诉原生模块要裁切照片的宽高比,最后两个参数是一个Promise,照片裁剪完成之后呢,原生模块可以通过Promise来对JS模块进行回调,来告诉裁切结果。

既然是js和Object-c进行数据传递,那么他们两者之间是如何进行类型转换的呢:
在上述例子中我们通过RCT_EXPORT_METHOD宏来声明需要暴露的接口,被 RCT_EXPORT_METHOD标注的方法支持如下几种数据类型。

RCT_EXPORT_METHOD标注的方法支持如下几种数据类型的参数:

string (Nsstring)
number (NSInteger,float,double,CGFloat,NSNumber)
boolean (BOOL,NSNumber)
array (NSArray) 包含本列表中任意类型
object (NSDictionary) 包含string类型的键和本列表中任意类型的值
function (RCTResponseSenderBlock)

原生模块向JS传递数据:

原生模块向JS传递数据我们可以借助Callbacks与Promises,接下来就讲一下如何通过他们两个进行数据传递的。

Callbacks

原生模块支持一个特殊类型的参数-Callbacks,我们可以通过它来对js进行回调,以告诉js调用原生模块方法的结果。
将我们selectWithCrop的参数改为Callbacks之后:

RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY success:(RCTResponseSenderBlock)success failure:(RCTResponseErrorBlock)failure)
{
    UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
    while (root.presentedViewController != nil) {
      root = root.presentedViewController;
    }
  Nsstring*aspectXStr=[Nsstring stringWithFormat: @"%ld",@"aspectY":aspectYStr} successs:^(NSDictionary *resultDic) {
    success(@[[NSNull null],@[@"imageUrl",[resultDic objectForKey:@"imageUrl"]]]);
  } failure:^(Nsstring *message) {
    failure(nil);
  }];
}

在上述代码中我们实现了通过Callback来对js进行回调。

接下来呢,我们在js中就可以这样来调用我们所暴露的接口:

ImageCrop.selectWithCrop(parseInt(x),parseInt(y),(error,result)=>{
 if (error) {
    console.error(error);
  } else {
    console.log(result);
  }
})

提示:另外要告诉大家的是,无论是Callback还是我接下来要讲的Promise,我们只能调用一次,也就是”you call me once,I can only call you once”。

Promises

除了上文所讲的Callback之外React Native还为了我们提供了另外一种回调js的方式叫-Promise。如果我们暴露的接口方法的最后一个参数是Promise时,如:

RCT_EXPORT_METHOD(selectWithCrop:(NSInteger)aspectX aspectY:(NSInteger)aspectY resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

那么当js调用它的时候将会返回一个Promsie:

ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> {
    this.setState({
        result: result
    })
}).catch(e=> {
    this.setState({
        result: e
    })
});

另外,我们也可以使用ES2016的 async/await语法,来简化我们的代码

async onSelectCrop() {
    var result=await ImageCrop.selectWithCrop(parseInt(x),parseInt(y));
}

这样以来代码就简化了很多。

因为,基于回调的数据传递无论是Callback还是Promise,都只能调用一次。但,在实际项目开发中我们有时会向js多次传递数据,比如二维码扫描原生模块,针对这种多次数据传递的情况我们该怎么实现呢?

接下来我就为大家介绍一种原生模块可以向js多次传递数据的方式:

向js发送事件

在原生模块中我们可以向js发送多次事件,即使原生模块没有被直接的调用。为了向js传递事件我们需要用到RCTEventdispatcher,它是原生模块和js之间的一个事件调度管理器。

#import "RCTBridge.h"
#import "RCTEventdispatcher.h"

@implementation CalendarManager

@synthesize bridge = _bridge;

- (void)calendarEventReminderReceived:(NSNotification *)notification
{
  Nsstring *eventName = notification.userInfo[@"name"];
  [self.bridge.eventdispatcher sendAppEventWithName:@"EventReminder"
                                               body:@{@"name": eventName}];
}

在上述方法中我们可以向JS模块发送任意次数的事件,其中eventName是我们要发送事件的事件名,params是此次事件所携带的数据,接下来呢我们就可以在JS模块中监听这个事件了:

import { NativeAppEventEmitter } from 'react-native';

var subscription = NativeAppEventEmitter.addListener(
  'EventReminder',(reminder) => console.log(reminder.name)
);
...

另外,不要忘记在组件被卸载的时候移除监听:

componentwillUnmount(){
    subscription.remove();
}

到现在呢,暴露接口以及数据传递已经进行完了,接下来呢,我们就需要导出React Native原生模块了。

导出React Native原生模块

为了方面我们使用刚才创建的原生模块,我们需要为它导出一个相应的JS模块。

我们创建一个ImageCrop.js文件,然后添加如下代码

import { NativeModules } from 'react-native';
export default NativeModules.ImageCrop;

这样以来呢,我们就可以在其他地方通过下面方式来使用我们所导出的这个模块了:

import ImageCrop from './ImageCrop' //导入ImageCrop.js
//...省略部分代码

    onSelectCrop() {
        let x=this.aspectX?this.aspectX:ASPECT_X;
        let y=this.aspectY?this.aspectY:ASPECT_Y;
        ImageCrop.selectWithCrop(parseInt(x),parseInt(y)).then(result=> {
            this.setState({
                result: result
            })
        }).catch(e=> {
            this.setState({
                result: e
            })
        });
    }
//...省略部分代码
}

现在呢,我们这个原生模块就开发好了,而且我们也使用了我们的这个原生模块。

关于线程

React Native在一个独立的串行GCD队列中调用原生模块的方法。在我们为React Native开发原生模块的时候,如果有耗时的操作比如:文件读写、网络操作等,我们需要新开辟一个线程,不然的话,这些耗时的操作会阻塞JS线程。通过实现方法- (dispatch_queue_t)methodQueue,原生模块可以指定自己想在哪个队列中被执行。
具体来说,如果模块需要调用一些必须在主线程才能使用的API,那应当这样指定:

- (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }

类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:

- (dispatch_queue_t)methodQueue { return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue",disPATCH_QUEUE_SERIAL); }

指定的methodQueue会被模块里的所有方法共享。如果你的方法中“只有一个”是耗时较长的(或者是由于某种原因必须在不同的队列中运行的),你可以在函数体内用dispatch_async方法来在另一个队列执行,而不影响其他方法

dispatch_async(dispatch_get_global_queue(disPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
    if (url) {
      [assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
        ALAssetRepresentation *rep = [asset defaultRepresentation];
        Byte *buffer = (Byte*)malloc((unsigned long)rep.size);
        NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:((unsigned long)rep.size) error:nil];
        NSData *data = [NSData dataWithBytesNocopy:buffer length:buffered freeWhenDone:YES];
        Nsstring * imagePath = [imageContent stringByAppendingPathComponent:fileName];
        [data writetoFile:imagePath atomically:YES];
        handler(imagePath);
      } failureBlock:^(NSError *error) {
        handler(@"获取图片失败");
      }];
    }
  });

在上述代码中我们将文件写入操作放到了一个独立的线程队列中,这样以来即使文件写入的时间再长也不会阻塞其他线程。

还有一个需要告诉大家的是,如果原生模块中需要更新UI,我们需要获取主线程,然后在主线程中更新UI,如:

dispatch_async(dispatch_get_main_queue(),^{
        [picker dismissViewControllerAnimated:YES completion:dismissCompletionBlock];
    });

关于React Native iOS原生模块的多线程无外乎这些东西。

另外,告诉大家一个好消息,我已经本博文所用到的项目的源码放到了Github上供大家学习使用:项目源码

如果,大家在开发原生模块中遇到问题可以在本文的下方进行留言,我看到了后会及时回复的哦。
另外也可以关注的新浪微博@CrazyCodeBoy,或者关注我的Github获取更多有关React Native开发的技术干货

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom