ReactNative Image组件的原理,源码分析
1.简介
image组件是iOS中常用组件之一,是一个由UIImageView导出来的RN控件。那么必然就包括oc端的代码导出与js端的代码配合,我们将从源码角度查看下这个控件是怎么工作的。
2.问题
<Image source={{uri:'http://*****/soft/collection/240/20160705/20160705121955903778.jpg'}} />
<Image source={require('./img/test.png')}/>
为什么配置source就可以下载网络图片呢?为什么可以支持网络图片有可以支持本地图片呢?他们究竟是怎么工作的,接下来我们将一一介绍。
3.源码结构
3.1 image OC层
RCTImageView,RCTImageViewManager配合负责oc端导出。
查看RCTImageView代码会发现,带有一个source,如下
@property(nonatomic,strong)RCTImageSource*source;
而这个source也就是对应的类为RCTImageSource,主要包括了图片的路径imageURL,图片scale,图片大小。
imageURL:需要主要的是imageURL不一定是http类型,在获取bundel包图片资源的时候为file://格式在调试模式下的本地图片和网络图片下载模式下imageURL为http类型。
scale:为图片生成模式 从1 到 3的范围分别对应@1x @2x @3x 图。js端会根据机型进行选择如在iphone6上 调试模式下加载本地图的情况下imageURL为http://*****/image@3x.png(如果以有3x图), scale为3。
总之,js端在代用require图片后就会经过一系列处理转换为一个包含图片信息的RCTImageSource,用来加载图片解码图片。
大体流程是这样:
JS source require --> RCTImageSource -->loadImage
外部输入假设
JS source require --> RCTImageSource 已经获取到了source.
我们先关注: RCTImageSource -->loadImage
- (void)setSource:(RCTImageSource *)source
{
if (![source isEqual:_source]) {
_source = source;
[self reloadImage];
}
}
- (void)reloadImage{
[selfcancelImageLoad];
if(_source&&self.frame.size.width>0&&self.frame.size.height>0) {
if(_onLoadStart) {
_onLoadStart(nil);
}
RCTImageLoaderProgressBlockprogressHandler =nil;
if(_onProgress) {
progressHandler = ^(int64_tloaded,int64_ttotal) {
_onProgress(@{
@"loaded":@((double)loaded),
@"total":@((double)total),
});
};
}
CGSizeimageSize =self.bounds.size;
CGFloatimageScale =RCTScreenScale();
if(!UIEdgeInsetsEqualToEdgeInsets(_capInsets,UIEdgeInsetsZero)) {
// Don't resize images that use capInsets
imageSize =CGSizeZero;
imageScale =_source.scale;
}
RCTImageSource*source =_source;
CGFloatblurRadius =_blurRadius;
__weakRCTImageView*weakSelf =self;
这里为图片真实获取地址
_reloadImageCancellationBlock= [_bridge.imageLoaderloadImageWithoutClipping:_source.imageURL.absoluteString
size:imageSize
scale:imageScale
resizeMode:(RCTResizeMode)self.contentMode
progressBlock:progressHandler
completionBlock:^(NSError*error,UIImage*image) {
RCTImageView*strongSelf = weakSelf;
if(blurRadius >__FLT_EPSILON__) {
// Do this on the background thread to avoid blocking interaction
image =RCTBlurredImageWithRadius(image, blurRadius);
}
dispatch_async(dispatch_get_main_queue(), ^{
if(![sourceisEqual:strongSelf.source]) {
// Bail out if source has changed since we started loading
return;
}
if(image.reactKeyframeAnimation) {
[strongSelf.layeraddAnimation:image.reactKeyframeAnimationforKey:@"contents"];
}else{
[strongSelf.layerremoveAnimationForKey:@"contents"];
strongSelf.image= image;
}
if(error) {
if(strongSelf->_onError) {
strongSelf->_onError(@{@"error": error.localizedDescription});
}
}else{
if(strongSelf->_onLoad) {
strongSelf->_onLoad(nil);
}
}
if(strongSelf->_onLoadEnd) {
strongSelf->_onLoadEnd(nil);
}
});
}];
}else{
[selfclearImage];
}
}
//imageLoaderloadImageWithoutClipping 函数就就会进行图片下载等内如处理包括缓冲图片界面图片识别是不是本地图片或者还是网络图片,如是本地图片就直接imageName 或者用绝对路径加载,网络图片启动网络下载流程,这样就到达区分到底是要加载本地还是网络图片了,下载成功后设置 strongSelf.image= image;
3.1 image js层
刚刚描述了oc端拿到数据结构后的图片下载流程,那么这个RCTImageSource是如何被处js端处理的呢?也就是回答下这个流程:JS source require --> RCTImageSource 。
主要文件为:image.ios.js ,我们对齐分析发现
var source = resolveAssetSource(this.props.source) || {};
对我们传进来的require('./img/test.png'),实际为一个数字,进行了处理。
resolveAssetSource,这是干嘛用的我们接着往下跟,路径为:[Libraries/Image/resolveAssetSource.js]
其中一个重要函数如下
/**
* `source` is either a number (opaque type returned by require('./foo.png'))
* `source` 就是个数字(从require('./foo.png')返回来的)或者是像
* { uri: '<http location || file path>' }本地和网络都支持的字典数据格式
*/
function resolveAssetSource(source: any): ?ResolvedAssetSource {
if (typeof source === 'object') {
return source;
}
var asset = AssetRegistry.getAssetByID(source);
if (!asset) {
return null;
}
//这里模块里面将进行 1x 2x 3x图的选择
const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
}
我们分析下AssetSourceResolver组件里面的主要函数, 1x 2x 3x图的选择是由他来完成的
路径:[Libraries/Image/AssetSourceResolver.js]
/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset): string {
var scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
var scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
var assetDir = assetPathUtils.getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
这样js端就处理完成:resolveAssetSource 返回结果为一个字典给OC用,在RCTImageViewManager中会
RCT_EXPORT_VIEW_PROPERTY(source, RCTImageSource),当然字典怎么导出为source的,也不难理解,
@implementation RCTConvert (ImageSource)
+ (RCTImageSource *)RCTImageSource:(id)json
{
if (!json) {
return nil;
}
NSURL *imageURL;
CGSize size = CGSizeZero;
CGFloat scale = 1.0;
BOOL packagerAsset = NO;
if ([json isKindOfClass:[NSDictionary class]]) {
if (!(imageURL = [self NSURL:RCTNilIfNull(json[@"uri"])])) {
return nil;
}
size = [self CGSize:json];
scale = [self CGFloat:json[@"scale"]] ?: [self BOOL:json[@"deprecated"]] ? 0.0 : 1.0;
packagerAsset = [self BOOL:json[@"__packager_asset"]];
} else if ([json isKindOfClass:[NSString class]]) {
imageURL = [self NSURL:json];
} else {
RCTLogConvertError(json, @"an image. Did you forget to call resolveAssetSource() on the JS side?");
return nil;
}
RCTImageSource *imageSource = [[RCTImageSource alloc] initWithURL:imageURL
size:size
scale:scale];
imageSource.packagerAsset = packagerAsset;
return imageSource;
}
@end
总结:至此一个完整的调用流程就实现了,当然还有很多细节,由此可以看出RN的实现是复杂的,但却合理的,之所以要看明白这些,好比你想把RN的image组件换成SDWebImage的下载逻辑,又比如你想自己造一个类似的控件加载图片,要模拟这个过程,所以了解源码是很有必要的。