在做ReactNative的业务过程中,由于碰到了不少奇(keng)妙(die)的问题,所以不免要从源码入手,探究一下各个方面的原理。这一篇文章就是由一个图片路径问题引出的总结。
我们知道,ReactNative
中创建一个图片组件有两种方式:
本文主要分析了从这几行 js 代码,到最终在 Native 生成 UIImageView
的过程。
jsbundle中的图片
上一篇文章讲到,无论是真机调试,还是模拟器调试,都是执行打包生成的 jsbundle
。jsbundle
中主要有两个东西比较值得注意:
__d
:即define
,其定义在node_modules/metro-bundler/src/Resolver/polyfills/require.js
中,可以理解为将一个模块以一个唯一的模块 ID 进行注册。require
,require
的方法参数为模块 ID,也就是__d
所注册的模块 ID,其调用了在__d
中注册的工厂方法。
有关
jsbundle
更加具体的解读,请参考我师父的文章^w^
我们以 source={require('../../../images_QRMedalHallRN/ranklinel.png')}
指定的本地图片资源,会在 jsbundle
中生成一段如下代码:
|
|
这里可以看到,一个本地图片资源文件,在RN中是当做一个模块来对待、处理的,有自己的 __d
方法。在这段代码中,function
就是该模块所注册的工厂方法,在 require
该模块的时候会被调用。432
即该模块的模块 ID。
|
|
这段代码在经过编译,打包后,在 jsbundle
中的存在形态如下:
|
|
可以看到,我们使用JSX写出的 <Image/>
控件,在经过编译后,变成了标准的js函数调用 createElement
(这也是需要进行编译、打包的原因之一)。其中,调用了 require(432)
,其实也就是去执行了上面 代码2
中注册的 function
。
在 function
中,调用了 require(169)
,从注释可以看出,169
是 react-native/Libraries/Image/AssetRegistry
的模块 ID。也就是这里调用了 AssetRegistry
的 registerAsset
方法。
AssetRegistry
AssetRegistry
为本地图片资源的一个注册站。它的代码很简单:
|
|
registerAsset
函数会将图片信息(一个map)保存到全局的数组中,并返回一个从1开始的索引。getAssetByID
函数会根据索引,取出在全局数组中保存的图片信息。注意,如果是指定本地图片的话,这里的图片信息只包含相对路径,即 '../../../images_QRMedalHallRN/ranklinel.png'
这种。
因此,在 <Image source=...><Image/>
中,我们以 source={require()}
指定的图片,会变成 source=<1>
这样的格式,而以 「uri」 指定的网络地址则不会被改变。
image.ios.js
我们在RN中使用的 <Image/>
控件,其源码位于 image.ios.js
:
|
|
其中,第4行代码,调用了 resolveAssetSource
对传入的 source
进行了解析,并最终作为一个参数传入 RCTImageView
中,而 RCTImageView
就是 Native 端的 RCTImageView
类,下文会讲到。
进入 resolveAssetSource
。其定义在 resolveAssetSource.js
中:
|
|
由注释和源码,我们很明显看出,这里传入的 source
,要么是一个数字,即上文所述,在 AssetRegistry
中注册的索引,要么是以 uri
传入的地址,即一个字符串。如果是字符串,那么将直接返回,否则会将之前在 AssetRegistry
中注册的图片信息提取出来,经过 AssetSourceResolver
的加工,形成最终的 source
返回。而AssetSourceResolver
的初始化中,传入了三个参数,分别是「网络服务器地址」,「本地图片路径」,「图片信息」。其中,前两个参数获取方式如下:
|
|
可以看到,resolveAssetSource
中从 Native 端获取了 scriptURL
,也就是 jsbundle
的 URL,并进行了判断:是否是网络地址,是否是本地 asset 地址,是否是沙盒文件地址等:
|
|
如果是从本地服务器动态进行打包,那么获取到的就是 http://localhost:8081/index.ios.bundle?platform=ios&dev=true&minify=false
这种网络地址。如果是采用了本地静态资源 bundle 的形式,那么这里获取到的是本地 asset 地址或沙盒文件路径。
再看AssetSourceResolver
。 AssetSourceResolver
会将初始化时传入的三个参数进行整合,并加上图片分辨率等信息,返回一个包含完整图片资源路径的 JSON 格式的图片信息:
|
|
这也就是最终传入到 Native 端,用于初始化 RCTImageView
的 JSON 数据。至此,js 端的工作结束了。
Native端
Native 端入口,自然是 RCTUIManager
。有关 RCTUIManager
,在我师父的另外一篇文章^w^中已经讲解的很清楚了,这里不再赘述了。
我们上面生成的 JSON 数据,会被解析成 RCTImageSource
类的对象,这段代码在 RCTImageSource
中:
|
|
RCTImageSource
会把图片信息进行整合,创建出一个 NSURLRequest
。其中,逻辑1对应 require()
指定本地图片地址的方式,逻辑2对应 uri
指定网络图片地址的方式。
最终,RCTImageSource
会传到 RCTImageView
,RCTImageView
是 UIImageView
的子类:
|
|
可以看到,RCTImageView
集中控制了图片的获取,下载(如果是网络图片的话),缓存,展示等等任务,能保证图片展示的高效性和流畅度,具体在此不再赘述了。另外,传入的是一个 RCTImageSource
的数组,这是为了 Native 根据不同分辨率的设备,选取合适的图片进行展示。
总结
说了这么多,引发这一串源码分析的,其实是一个图片资源路径错误的问题。因为我们采取了分包加载的方案,我们的 common.js
一开始放在了 appbundle
中,但我们业务的图片资源是下发后,保存在用户沙盒目录下,因此,在 代码8
中,返回的其实是 appbundle
的路径,自然取不到沙盒路径下的图片了。由此可见,弄懂源码好像才是解决问题最彻底的手段。