尊重版权,未经授权不得转载
本文出自:贾鹏辉的技术博客(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原生模块的。
首先,让我们先看一下,开发iOS原生模块的主要流程。
开发iOS原生模块的主要流程
在这里我把构建React Native iOS原生模块的流程概括为以下三大步:
- 编写原生模块的相关iOS代码;
- 暴露接口与数据交互;
- 导出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@H_182_403@
/** * 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@H_182_403@
/** * 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
被回收我们就无法收到选择照片之后的回调了,也就无法获取到照片。
暴露接口@H_182_403@
在上述代码中我们通过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
标注的方法支持如下几种数据类型。
string (NSString)
number (NSInteger,float,double,CGFloat,NSNumber)
boolean (BOOL,NSNumber)
array (NSArray) 包含本列表中任意类型
object (NSDictionary) 包含string类型的键和本列表中任意类型的值
function (RCTResponseSenderBlock)
原生模块向JS传递数据:
原生模块向JS传递数据我们可以借助Callbacks与Promises,接下来就讲一下如何通过他们两个进行数据传递的。
Callbacks@H_182_403@
原生模块支持一个特殊类型的参数-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@H_182_403@
除了上文所讲的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原生模块的多线程无外乎这些东西。
如果,大家在开发原生模块中遇到问题可以在本文的下方进行留言,我看到了后会及时回复的哦。
另外也可以关注的新浪微博@CrazyCodeBoy,或者关注我的Github来获取更多有关React Native开发的技术干货。