源码解读RCTImageView(iOS)

[TOC]

源码解读RCTImageView(iOS)

查看官网自定义UI组件的文档, 可以知道每个自定义的UI组件CustomView需要对应一个CustomViewManagerCustomViewManager继承RCTViewManager.

我们可以跟踪查看requireNativeComponent的实现, 最终发现调用的是UIManager.createView(...), 对应原生RCTUIManager的createView方法。

createView.png

一、RCTUIManager的原理

原生

  • RCTUIManager也是一个module, 在setBridge的时候, 获取bridge中所有RCTViewManager的子类保存下来
- (void)setBridge:(RCTBridge *)bridge {
    ...
      for (Class moduleClass in _bridge.moduleClasses) {
    if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) {
      RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass
                                                                                bridge:_bridge];
      componentDataByName[componentData.name] = componentData;
    }
  }

  _componentDataByName = [componentDataByName copy];
  ...
}
  • constantsToExport把所有相关ViewManager的信息导出
- (NSDictionary<NSString *, id> *)constantsToExport
{
    ...
  return constants;
}

具体信息格式为:

{
    ...
    RCTImageView =     {
        Manager = ImageViewManager;
        NativeProps =         {
            blurRadius = CGFloat;
            capInsets = UIEdgeInsets;
            defaultSource = UIImage;
            onError = BOOL;
            onLoad = BOOL;
            onLoadEnd = BOOL;
            onLoadStart = BOOL;
            onPartialLoad = BOOL;
            onProgress = BOOL;
            resizeMode = RCTResizeMode;
            source = "NSArray<RCTImageSource *>";
            tintColor = UIColor;
        };
    };
    RCTWebView =     {
        Manager = WebViewManager;
        NativeProps =         {
            allowsInlineMediaPlayback = BOOL;
            automaticallyAdjustContentInsets = BOOL;
            bounces = BOOL;
            contentInset = UIEdgeInsets;
            dataDetectorTypes = UIDataDetectorTypes;
            decelerationRate = CGFloat;
            injectedJavaScript = NSString;
            mediaPlaybackRequiresUserAction = BOOL;
            messagingEnabled = BOOL;
            onLoadingError = BOOL;
            onLoadingFinish = BOOL;
            onLoadingStart = BOOL;
            onMessage = BOOL;
            onShouldStartLoadWithRequest = BOOL;
            scalesPageToFit = BOOL;
            scrollEnabled = BOOL;
            source = NSDictionary;
        };
    };
    customBubblingEventTypes =     {
        topBlur =         {
            phasedRegistrationNames =             {
                bubbled = onBlur;
                captured = onBlurCapture;
            };
        };
        topChange =         {
            phasedRegistrationNames =             {
                bubbled = onChange;
                captured = onChangeCapture;
            };
        };
        ...
    };
    customDirectEventTypes =     {
        topAccessibilityTap =         {
            registrationName = onAccessibilityTap;
        };
        topError =         {
            registrationName = onError;
        };
        ...
    };
    ...
}

每个对应的UI组件 xxxView有manager、NativeProps俩个属性。分别表示对应的ViewManager及属性。

JS

//UIManager.js
// 经过映射 UIManager是一个带有上述jsonkey的对象。 同时 会增加Constants 与 UIManager的属性。

UIManager 数据结构

UIManagerjs.png

RCTImageView

原生类的关系图:

RCTImageView.png

js部分

//Image.ios.js
const Image = createReactClass({...,
    render:function() {
        ...
            return (
              <RCTImageView
                {...this.props}
                style={style}
                resizeMode={resizeMode}
                tintColor={tintColor}
                source={sources}
              />
            );
    }
});

... 

const RCTImageView = requireNativeComponent('RCTImageView', Image);

module.exports = Image;

图片定位

Image的图片浏览主要通过source设置, js传递source信息体到原生, 原生根据source信息体生成对应的请求, 通过遍历所有实现的RCTImageURLLoader协议的模块,选择最优的loader获取image,如果没有则尝试使用网络接口获取,失败则报错。 流程如下

getImage.png

source的解析

RN的Image控件,支持2种形式的source

  • require('filepath') //PropTypes.number
  • {uri:'fileURI', bundle:'bundleName', width:100, height:100, ... } //ImageURISourcePropType

图片控件的JS代码对应Image.ios.js, 原生代码对应RCTImageView

require方式

require方式返回的是一个整型, 对应一个define函数, 在bundle中体现为

//引用的地方  require方式
_react2.default.createElement(_reactNative.Image, { source: require(305                                      ), __source: { // 305 = ./Images/diary_mood_icon_alcohol_32.png
            fileName: _jsxFileName,
            lineNumber: 30
          }
        }),
 // uri 方式
_react2.default.createElement(_reactNative.Image, { source: { uir: 'https://www.baidu.com/img/bd_logo1.png', width: 100, height: 100 }, __source: {
            fileName: _jsxFileName,
            lineNumber: 31
          }
        })
//define地方
__d(/* RN472/Images/diary_mood_icon_alcohol_32.png */function(global, require, module, exports) {module.exports=require(161                                         ).registerAsset({"__packager_asset":true,"httpServerLocation":"/assets/Images","width":16,"height":16,"scales":[2,3],"hash":"7824b2f2a263b0bb181ff482a88fb813","name":"diary_mood_icon_alcohol_32","type":"png"}); // 161 = react-native/Libraries/Image/AssetRegistry
}, 305, null, "RN472/Images/diary_mood_icon_alcohol_32.png");

我们看到打包的时候,require图片会转换成如下格式的对象保存:

{
    "__packager_asset":true,  //是否是asset目录下的资源
    "httpServerLocation":"/assets/Images", //server目录地址
    "width":16, 
    "height":16,
    "scales":[2,3], //图片scales   
    "hash":"7824b2f2a263b0bb181ff482a88fb813", //文件hash值
    "name":"diary_mood_icon_alcohol_32", //文件名
    "type":"png" //文件类型
}

我们看到引用的地方require(305)其实是执行了require(161)的registerAsset的方法。查看161的define

__d(/* AssetRegistry */function(global, require, module, exports) {
'use strict';

var assets = [];

function registerAsset(asset) {
  return assets.push(asset);
}

function getAssetByID(assetId) {
  return assets[assetId - 1];
}

module.exports = { registerAsset: registerAsset, getAssetByID: getAssetByID };
}, 161, null, "AssetRegistry");

161对应的就是AssetRegistry, AssetRegistry.registerAsset把图片信息保存在成员数组assets中。
查看Image.ios.js的render函数

  render: function() {
        const source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined };
    ...
    return (
      <RCTImageView
        {...this.props}
        style={style}
        resizeMode={resizeMode}
        tintColor={tintColor}
        source={sources}
      />
    );

通过resolveAssetSource函数

function resolveAssetSource(source: any): ?ResolvedAssetSource {
  if (typeof source === 'object') {
    return source;
  }

  var asset = AssetRegistry.getAssetByID(source);
  if (!asset) {
    return null;
  }

  const resolver = new AssetSourceResolver(getDevServerURL(), getBundleSourcePath(), asset);
  if (_customSourceTransformer) {
    return _customSourceTransformer(resolver);
  }
  
  return resolver.defaultAsset();

调用AssetRegistry.getAssetByID方法取出对应的信息,传递到原生。

//传递到原生的source信息格式
{
    "__packager_asset" = 1;
    height = 16;
    scale = 2;
    uri = "//Users/huangmingwei/Library/Developer/CoreSimulator/Devices/2A0C4BE4-807B-4000-83EB-342B720A14DE/data/Containers/Bundle/Application/F84F1359-CBCD-4184-B3FD-2C7833B83A60/RN472.app/react-app/assets/Images/diary_mood_icon_alcohol_32@2x.png";
    width = 16;
}

原生通过解析uri信息,获取对应的图片

//RCTImageView.m
- (void)setImageSources:(NSArray<RCTImageSource *> *)imageSources
{
  if (![imageSources isEqual:_imageSources]) {
    _imageSources = [imageSources copy];
    [self reloadImage];
  }
}

原生加载过程涉到及几个类

  • RCTImageSource, 通过js传递的source信息,转换出图片地址
  • RCTImageViewManager 管理RCTImageView
  • RCTImageVIew 对应每个Image实例
  • RCTImageLoader bridge的成员,控制图片的加载逻辑
  • RCTImageURLLoader 协议,具体图片的定位执行者需要实现此协议
  • RCTLocalAssetImageLoader 实现bundle统计目录asset下的图片访问 loader

重点在RCTImageURLLoader协议

//判断能否处理
- (BOOL)canLoadImageURL:(NSURL *)requestURL;

//获取图片, 如果是异步处理返回一个可以取消的block
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
                                              size:(CGSize)size
                                             scale:(CGFloat)scale
                                        resizeMode:(RCTResizeMode)resizeMode
                                   progressHandler:(RCTImageLoaderProgressBlock)progressHandler
                                partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
                                 completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;

URI方式

这种方式的加载流程与require相同,只是source的获取信息方式比较简单,js端设置的是什么,到原生端也是一样

function resolveAssetSource(source: any): ?ResolvedAssetSource {
  if (typeof source === 'object') {  // uri 方式直接返回
    return source;
  }
  

原生通过RCTImageSource解析流程是一样的。

Array方式

js端最终将source转换为一个Array,传递到原生。如果有多个uri,可设置为不同尺寸的图片,在原生会选择大小最适合的一个图片展示。 当只有一张图片的时候,默认会转为只有一个元素的Array。目前都是只有一张哦~

RCTImageView.m

- (RCTImageSource *)imageSourceForSize:(CGSize)size
{
  if (![self hasMultipleSources]) {
    return _imageSources.firstObject;
  }

  // Need to wait for layout pass before deciding.
  if (CGSizeEqualToSize(size, CGSizeZero)) {
    return nil;
  }

  const CGFloat scale = RCTScreenScale();
  const CGFloat targetImagePixels = size.width * size.height * scale * scale;

  RCTImageSource *bestSource = nil;
  CGFloat bestFit = CGFLOAT_MAX;
  for (RCTImageSource *source in _imageSources) {
    CGSize imgSize = source.size;
    const CGFloat imagePixels =
      imgSize.width * imgSize.height * source.scale * source.scale;
    const CGFloat fit = ABS(1 - (imagePixels / targetImagePixels));

    if (fit < bestFit) {
      bestFit = fit;
      bestSource = source;
    }
  }
  return bestSource;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 我不知道你是否和我一样,辗转反侧,无法自拔地陷入后悔中,常常带着几许“羞愧”去细想 一时冲动或一时无意而做下的“蠢...
    夙澜息阅读 897评论 12 40
  • 啦啦啦
    周航666阅读 218评论 0 0
  • 昨天是活动日,敏敏的主场。这是一个什么主场呢,就是发泄,骂人。 开始得很突然,我刚送完孩子去上学,走路回去开车上班...
    真冉阅读 423评论 0 1