Talk about ReactNative Image Component

迁移老文章到掘金javascript

相关系列文章html

最近好像唠叨了好多RN的东西╮(╯_╰)╭,唠叨的我都以为有点贫,就当随手记笔记吧java

关于ReactNative的Image组件,我一直很好奇他内部具体的工做过程,这里面有不少有意思的东西,毕竟Image这个东西即使是纯源生开发也能够作的很复杂很精妙,好比SDWebImage的无比强大的网络缓存,网图控制,好比ASDK里面的asyncDisplay,好比YYWebImage中身兼网络缓存控制与异步高效解码绘制。今天咱们来看看ReactNative是如何处理Image的node

ImageComponent的基本用法

Facebook的官方文档:ImageComponentreact

从文档中咱们能够看到ImageComponent一共能够读取三种图片,不管用那种方式,只要把他们赋值给source,好像图片就能天然生效了ios

  • 与jsbundle一块儿打包packag的资源文件(require方式)
  • iOS源生APP的ImageAsset内部被iOS管理的资源文件({uri:name}方式)
  • 网络上的图片({uri:url}方式)

静态图片资源

随着jsbundle一块儿打包的资源文件,在文档中以这种方式require('./img/check.png')使用,其中的路径是图片小相对于index.ios.js这个文件的路径。git

静态图片资源是什么意思呢?咱们用RN确定是指望热更新的,确定指望rn的js代码与功能所须要用到的图片,一块儿随着网络下载的客户端本地,从而生效,从而展示,因此这些图片须要随着js一块儿被打包,当执行了node.js的打包命令后,会生成一个bundle目录,这里面有最核心的jsbundl也就是js代码包,同时这里面还有个asset目录,里面放着全部一块儿打包的资源文件,这就是RN的静态图片资源的概念github

APP的图片资源

这里要提到iOS的图片资源管理,iOS会把全部的图片打包进入app本身独有的资源文件包之中,这部分图片属于APP管理,是随着每一个app的包一块儿提审,一块儿发版,简单地说这部分图片的管理,不能随着网络下载随意存放和读取和更新,是纯源生iOSAPP的资源管理与读取的方案chrome

若是想在RN里面,显示这种源生APP资源的话就要经过{uri:name}的方式,其中name是资源文件在源生管理器里面的名字,这样就能够在RN的环境里,读取出native环境里的资源数据库

网络图片

这个就很好理解了,恩,不从APP本地不管是RN包仍是native包里面读图,直接从网络里拉图,{uri:url}其中url是图片的网络地址

图片是如何读取的

读了以前的文章,咱们应该清楚,全部的RN的ImageComponent最终都会经过源生的UIModule,实现最终的源生的展示效果,那么这个UIModule就是RCTImageView,你们能够从源码中看到这个类

关注一下这个类的- (void)setSource:(RCTImageSource *)source方法,看起来全部在JS里赋值给Image的Source的属性,都会传过来经过这个方法传给RCTImageView,而后再经过RCTImageView的reloadImage方法去读取图片内容,这部分后面还会讲。

但我很惊讶与这里面的代码,刚才咱们讲到,RN是有三种大相径庭的图片读取方式的,传入的是三种大相径庭的数据,并且是读取大相径庭的三种类型的图片

在个人认知里面,彻底不一样的三种方案,在setSourcereloadImage里面应该会按着三种方式,至少有个if else之类的差别化处理,但出乎个人意料,RN在这两处的代码是彻底一致而且统一的,代码一鼓作气没有任何分支处理

咱们写这样一段JS代码,在一个页面里同时展示3种图片

render() {
    return (
      <View>
        <Image source={require('./res/kakaka.jpg')}/>
        <Image source={{uri: 'ScreenCover_night'}}
              style={{width: 40, height: 40}}/>
        <Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}}
              style={{width: 50, height: 50}}/>
      </View>
    );
  }
复制代码

这3个文件真正执行的时候,在OC的setSource处打断点却发现大相径庭的景象

本来的输入参数

  • require('./res/kakaka.jpg')
  • {uri: 'ScreenCover_night'}
  • {uri: 'https://facebook.github.io/react/img/logo_og.png'}

在setSource断点里彻底变了,彻底变成了imageURL

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • file:///Users/Awhisper/Library/Developer/CoreSimulator/Devices/2D84E82E-AEDD-4B7B-A59A-F44C3B6721F2/data/Containers/Bundle/Application/B80C514C-639F-42FE-812F-3ECF457BFEC8/yuedu.app/ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

这太不符合个人认知了,为何输入参数会如此整齐划一的统统变成了iOS下的URL?不管是本地URL仍是远程URL,由于他们彻底被统一成了同一种URL类型,从而iOS的这两处OC代码彻底不须要if分支就能一个逻辑处理全部图片

我很好奇究竟是哪段代码处理了这种统一化?

图片源Source输入参数在JS里的归一化处理

若是想弄明白rn是如何把这三种方案统一的,那天然得从JS源码入手看起,咱们将要很大程度的关注/node_module/react-native/Libraries/Image这个目录下的几个关键JS文件。

想弄明白这里面的运做过程,最好的办法就是利用RN的chrome-debug方案,在关键位置上打上断点,看看到底代码调用栈是如何一步步执行的

那咱们的焦点就落在了目录下这个Image.ios.js的文件上,能够看出来没错,这就是Image组件的JS源码,咱们会看到这么几行

render: function() {
    var source = resolveAssetSource(this.props.source) || {};

    //balabalabala
    //....中间代码略去
    }

    return (
      <RawImage {...this.props} style={style} resizeMode={resizeMode} tintColor={tintColor} source={source} /> ); }, 复制代码

能够知道,咱们传给JS的Source属性的输入参数this.props.source,到底是如何处理的 他看起来就是resolveAssetSource()处理了一下原封不动的就进入后面的流程了,传递数据给native进行渲染的流程

这部分流程在这里,咱们须要看一下/node_module/react-native/Libraries/ReactNative/ReactNativeBaseComponent.js文件,全部的RN界面组件,不管是标签文字,仍是图片,地图,转菊花,都是经过这个BaseComponent来调用UIManger(也就是APIModule RCTUIManager的JS侧入口)绘制到native上的。你们只关注mountComponent这个方法就行了

mountComponent: function(rootID, transaction, context) {
    //balabalabala
    //....中间代码略去

	//...用来获取Component的props
    var updatePayload = ReactNativeAttributePayload.create(
      this._currentElement.props,
      this.viewConfig.validAttributes
    );

    //balabalabala
    //....中间代码略去 用来获取nativeTopRootID
    
    //... call OC 建立native界面组件
    UIManager.createView(
      tag,
      this.viewConfig.uiViewClassName,
      ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID],
      updatePayload
    );

    //balabalabala
    //....中间代码略去
    
    return {
      rootNodeID: rootID,
      tag: tag
    };
  }
复制代码

咱们梳理一下过程

首先咱们三种example的输入参数是

  • require('./res/kakaka.jpg')
  • {uri: 'ScreenCover_night'}
  • {uri: 'https://facebook.github.io/react/img/logo_og.png'}

在最终mountComponent的时候,updatePayload获取到的props里面,咱们打断点查看一下,看看通过了无数的JS代码处理后,对于Image组件这块的内存数据是怎样的,实践事后会发现,这里的props必定会还有一个uri属性,三种example此时的uri属性分别是

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

JS代码就到此为止,走过UIManager.createView以后,就进入了OC的代码逻辑了,这个放在后面细说。

因而咱们会发现除了require的静态图片资源,输入参数和输出结果变化很是大之外,另外两种example基本没啥变化,咱们先从简单的下手,看一看后两种example是如何简单处理的

{uri:xxx}的处理过程

顺着刚才贴出的代码能够知道,Image.ios.js只是简单的把{uri:xxx}的输入参数传给resolveAssetSource.jsresolveAssetSource方法,处理一下,而后添加了几个额外属性,而后直接复制给this.props.source,以后就传给了ReactNativeBaseComponent.js了。

resolveAssetSource.jsresolveAssetSource方法更是简单粗暴,由于咱们输入的是{uri:xx}他就是一个对象,这方法什么也不作直接返回

if (typeof source === 'object') {
    return source;
}
//其余处理
复制代码

因此咱们在UIManager最终传值的时候,会看到一个跟咱们输入数据没啥变化的一个JS Object,只是多了几个属性而已

require(imagepath)的处理过程

这个过程就比较复杂了,并且这个过程会涵盖rn的打包,执行,两大重要环节

  • 一. RN的打包环节

rn的打包是经过在rn根目录下,执行node.js的一行打包脚本命令,最终把咱们编辑过程当中的js业务文件,js框架文件,res资源文件,总体打包到bundle目录之下,对于图片来讲,我本觉得只是把图片换个打包目录另存为而已,但当我一步一步追踪源码的时候,我发现我错了。

require('./res/kakaka.jpg') 举例来讲,在这个目录下的kakaka.jpg文件会另存为到bundle/asset/res/kakaka.jpg这个位置,成为rn包中的一部分。

但毫不仅仅是另存为,打包脚本还会在图片文件中植入一行js代码,若是你在AssetRegistry.jsregisterAsset方法打上断点,去查看调用栈,你会发现,竟然调用栈里的一行JS代码,来自这个图片文件,有图为证(床说中贴吧各类往图片里藏老司机开车的种子链接,就是用这种方式╮(╯_╰)╭)

chrome 断点

这行JS代码是

module.exports = require("react-native/Libraries/Image/AssetRegistry").registerAsset({"__packager_asset":true,"fileSystemLocation":"/Users/Awhisper/Desktop/yuedu_RN_BRANCH/Main/YDReactNative/res","httpServerLocation":"/assets/res","width":1242,"height":150,"scales":[1],"files":["/Users/Awhisper/Desktop/yuedu_RN_BRANCH/Main/YDReactNative/res/kakaka.jpg"],"hash":"319fbd6959f45c18b1843e71d3bdd991","name":"kakaka","type":"jpg"});
复制代码

这说明,在打包脚本执行的时候,打包脚本会把这个图片的全部信息,包括打包前原来的绝对路径,打包后的相对路径,打包后的host路径,打包后的文件hash,打包后的文件名全都以源码js写入的方式,写进图片文件里。而且这个图片文件还执行了一行代码AssetRegistry. registerAsset

这个图片虽然被植入了JS代码,可是他并无马上生效,但正由于图片内部存在JS代码,因此他能够经过require(xxx)的方式进行加载(其实RN也扩写了require.js这个库),也就是说当咱们在RN运行环节,一旦执行了require(图片)这行代码,AssetRegistry. registerAsset就会马上被执行

  • 二. RN的运行环节

打包完成了,图片已经被植入了JS代码,如今RN该开始运行了,一旦运行到<Image source={require('./res/kakaka.jpg')}/>这句话时候,就至关于require了图片内的js代码,因而就执行了AssetRegistry. registerAsset

这个函数干了些啥呢?他把图片内被植入的js代码中的一大堆图片信息参数,全都push进了一个全局的数组里,而且返回了一个索引值index

function registerAsset(asset: PackagerAsset): number {
  return assets.push(asset);
}
复制代码

当咱们Image.ios.js获取this.props.source的时候,咱们断点查看var source的值你会发现他是一个数字也就是1!这就是index

回到resolveAssetSource.jsresolveAssetSource方法,此时咱们的输入参数已经不是一个JS Object了,而是一个数字1,因而天然也就没有直接return,而是进一步处理

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();
复制代码

没错他从全局的资源数组里,按着index取出来那个含有资源详细信息的字典,而且稍加处理和改造,返回给了Image.ios.js

这就是为啥咱们从这个JS层的输入参数

  • require('./res/kakaka.jpg')

变成了这样的JS层的输出参数

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991

图片源Source输入参数在OC里的归一化处理

上文提到三种example在JS层最终输出参数是这样的

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

他们会经过UIManager传给OC的RCTUIManager从而进行建立和绘制,此时此刻你会发现,(1)与(3)已经变成了URL的样式,已经能够直接进行URLLoad了,可是(2)还不是一个URL,说明(2)还须要在OC层面进行转换统一,这个转换过程又发生在哪呢?

这咱们就要顺着OC的RCTUIManager去追踪了,在createView方法里面(上一篇源码分析提到过RCTUIManager与RCTComponentData的关系,我就不细讲了)

  • RCTUIManager-createView:xxxx方法
    • RCTComponentData-setProps:forView:方法(RCTUIManager line:910)
      • RCTComponentData-propBlockForKey:inDictionary:方法(RCTComponentData line:343) (此处代码有点绕,函数的做用是取出block,真正要看的是函数后面的括号执行block)
      • invocation(invocation去执行setter,真正的setSource,而且伴随着复杂切晦涩的宏处理,在宏的处理过程当中,把js传过来的json字典会经过RCTConvert转换成RCTImageSource,推荐直接在下面的位置断点看效果)
        • RCTImageSource-RCTImageSource方法
          • RCTImageSource-NSURL方法

没错,RCTImageSource-NSURL就是关键,在这个函数里若是发现url字符串能够被转化成NSURL,则直接return该NSURL(因此例子1,3没有任何变化直接return),若是传来的是像2例子那样一个名字ScreenCover_night,在这段代码里,会主动向iOS独有的资源管理类[NSBundle mainBundle].resourcePath来申请iOS本地资源路径,从而将ScreenCover_night转化成真正意义上的URL

file:///Users/Awhisper/Library/Developer/CoreSimulator/Devices/2D84E82E-AEDD-4B7B-A59A-F44C3B6721F2/data/Containers/Bundle/Application/B80C514C-639F-42FE-812F-3ECF457BFEC8/yuedu.app/ScreenCover_night
复制代码

图片源Source输入参数归一

输入

  • require('./res/kakaka.jpg')
  • {uri: 'ScreenCover_night'}
  • {uri: 'https://facebook.github.io/react/img/logo_og.png'}

通过了JS层的初步归一,归一处理了example(1)的状况,变成了

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

通过RCTConvert,OC层的二次归一,归一处理了example(2)的状况,变成了

  • http://localhost:8081/assets/res/kakaka.jpg?platform=ios&hash=319fbd6959f45c18b1843e71d3bdd991
  • file:///Users/Awhisper/Library/Developer/CoreSimulator/Devices/2D84E82E-AEDD-4B7B-A59A-F44C3B6721F2/data/Containers/Bundle/Application/B80C514C-639F-42FE-812F-3ECF457BFEC8/yuedu.app/ScreenCover_night
  • https://facebook.github.io/react/img/logo_og.png

如今全部的输入source都已经不折不扣统一成了url,不管是远程url,仍是本地磁盘文件url,因此后续的loadImage过程,就无需特别针对处理了,直接能够进行load了

关于RCTImageView的缓存

这是一个颇有趣的事情!

咱们都知道RN的网络图片是有缓存的,可是今天在群里讨论的时候,却发现了一个颇有意思的事情,我发现RN内部,不止一种图片缓存的方案。

RN的源生native缓存方案(存在感极低)

对于源生客户端来讲,一般会使用SDWebImage这样的第三方库去处理网络图片,由于他有着很是强大的内存缓存,磁盘缓存,有着灵活的缓存管理手段,以图片url为key,统一在一个字典表里进行存储,不管是内存仍是磁盘。

RN也不例外,你能够找到一个RCTImageStoreManager的类,干着相似的事情,以字典+urlKey的方式管理一堆UIImage,但奇怪的是,这个类竟然没有任何方法调用。

RCTImageStoreManager是一个APIModule,他含有RCT_EXPORT_MODULE()代码。也就是说JS层是能够直接操做RCTImageStoreManager的,可是顺藤摸瓜寻找JS层是如何使用却发现,只有2个JS文件使用了它,ImageStore.jsImageEditor.js,有趣的事情来了,这两个JS组件就好端端的躺在rn框架代码里,可是并无被任何人使用,没有被Image组件直接使用,网上google了一下发现相关内容很是之少,只有极个别人会用一下。RN的英文官网也搜不到这两个组件的介绍。

但正如我说,这一整套源生native缓存方案,不管是OC侧的源码仍是JS测的源码就这么好端端的呆在RN的框架源码里面,等待着被人使用,虽然几乎没有。若是你尝试一下,发现一切都运做正常

第三方扩展的native缓存方案

reactnative image cache相关字样的时候,你能够发现github上有不少第三方重新写的一套相似ImageStore.jsImageEditor.js的解决方案,看来要么是有人以为facebook写好的现成的不够牛逼,基于数据库从新封装了一套,要么是有人压根都不知道facebook写好了一个,因而本身重写了一遍,哈哈

总之fb本身写好的那一套native缓存方案,存在感异常的低啊哈哈哈哈哈

RN的http图片缓存方案

都说了,fb本身的native缓存方案存在感如此之低,太多的人都不知道,github上的开源的解决方案其实普及率也没那么大,不少人用RN也没用github上的三方缓存也没用fb提供的缓存,但RN的图片依然仍是有缓存功能的,这是为啥?

这就回到了RCTImageView的reloadImage函数了,它里面会起一个NSURLSession去拉取网络图片数据,当拉取到图片缓存数据后,会使用OC源生的NSURLCache缓存整个URLRequest

//RCTImageLoader-loadImageOrDataWithTag:xxx(line:390)

dispatch_async(_URLCacheQueue, ^{
    // Cache the response
    // TODO: move URL cache out of RCTImageLoader into its own module
    BOOL isHTTPRequest = [request.URL.scheme hasPrefix:@"http"];
    [strongSelf->_URLCache storeCachedResponse:
     [[NSCachedURLResponse alloc] initWithResponse:response
                                              data:data
                                          userInfo:nil
                                     storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
                                    forRequest:request];
    // Process image data
    processResponse(response, data, nil);

    //clean up
    [weakSelf dequeueTasks];

});
复制代码

因此说,若是你没有使用任何的native图片cache方案,不管是fb提供的仍是三方的,rn依然会帮助你进行图片缓存,使用的方法就是系统级NSURLCache的整个URLRequest的缓存,这个缓存是系统级的,会和你其余的非rn的native的http缓存请求混在一块儿处理(具体看NSURLCache的使用,native能够自由的单开和共用)

相关文章
相关标签/搜索