react native图片解析流程详解
lcwlucky 人气:0正文
我们知道,在react-native中加载一张图片需要使用Image组件,其有两种使用方式
import bg from './bg.png'; // 1. 加载本地图片资源 <Image source={bg}/> <Image source={require('./bg.png)}/> // 2. 加载网络图片资源 <Image source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }}/>
1. 构建输出的产物
如果代码里import了一张图片
// src/index.js import { Image } from 'react-native'; import bg from './bg.png'; const jsx = <Image source={bg}/>
. └── src/ ├── index.js ├── bg.png ├── bg@1.5x.png ├── bg@2x.png └── bg@3x.png
那么通过metro打包后图片在js bundle中到底长啥样呢? 先通过以下命令构建bundle
// ios react-native bundle --entry-file src/index.ts --platform ios --bundle-output dist/ios/ios.bundle.js --assets-dest dist/ios // android react-native bundle --entry-file src/index.ts --platform android --bundle-output dist/android/android.bundle.js --assets-dest dist/android
构建结果如下:
ios会将图片输出到assets目录下,且图片保留图片目录层次结构。
android中,drawable-mdpi
,drawable-hdpi
,drawable-xhdpi
,drawable-xxhdpi
文件夹存放不同分辨率屏幕下的图片,文件名由目录和图片名称通过_
拼接组成。
drawable-mdpi: 1x
drawable-hdpi: 1.5x
drawable-xhdpi: 2x
drawable-xxhdpi: 3x
2. js bundle分析
打开ios.bundle.js,首先看一下bundle中的两个重要的方法:
- __d: 即
define
。 注册一个模块到全局modules中,且这个模块的id是唯一的,大致源码如下
modules = Object.create(null); function define(factory, moduleId, dependencyMap) { if (modules[moduleId] != null) { return; } var mod = { dependencyMap: dependencyMap, factory: factory, hasError: false, importedAll: EMPTY, importedDefault: EMPTY, isInitialized: false, publicModule: { exports: {}, }, }; modules[moduleId] = mod; }
- __r: 即
metroRequire
, 它接收一个模块id作为参数,也就是__d
所注册的模块id,其调用了在__d
中注册的工厂方法。
function metroRequire(moduleId) { var moduleIdReallyIsNumber = moduleId; var module = modules[moduleIdReallyIsNumber]; return module && module.isInitialized // 如果已经初始化过,直接返回缓存 ? module.publicModule.exports // 这里其实就是 module.exports // 如果没有初始化过,则内部调用module的factory方法初始化 : guardedLoadModule(moduleIdReallyIsNumber, module); }
我们import的图片最终生成了这样一段代码
__d( // factory function ( global, _$$_REQUIRE, //__r _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap ) { module.exports = _$$_REQUIRE( _dependencyMap[0], 'react-native/Libraries/Image/AssetRegistry' ).registerAsset({ __packager_asset: true, httpServerLocation: '/assets/src', width: 295, height: 153, scales: [1, 1.5, 2, 3], hash: '615a107224f6f73b539078be1c162c6c', name: 'bg', type: 'png', }); }, 479, [223], // 223 就是react-native/Libraries/Image/AssetRegistry 模块 'src/bg.png' );
由代码得知,我们在代码中import的图片被当做一个module进行处理,内部调用了react-native提供的registerAsset方法来注册资源。
资源信息包括了以下几个重要字段:
httpServerLocation
:图片文件夹在http server中的地址。如果我们在本地开发,metro内部会启动一个http server,这个字段就是告诉server图片文件夹在哪。scales
:图片有哪些尺寸。因为bg.png存在 1x,1.5x,2x,3x 4种尺寸,所以这里scales就为[1, 1.5, 2, 3]
。如果你的图片只有3x,那么scales就为[3]
。type
: 图片后缀。width
:图片宽度height
:图片高度
经过测试发现,图片有哪些尺寸,始终都是1x图的宽高。比如一张图片只有3x尺寸,那么metro在打包时会通过当前3x图的宽高计算出1x图的宽高,但是scales仍为 [3]。
// react-native/Libraries/Image/AssetRegistry __d( function ( global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap ) { 'use strict'; var assets = []; function registerAsset(asset) { return assets.push(asset); } function getAssetByID(assetId) { return assets[assetId - 1]; } module.exports = { registerAsset: registerAsset, getAssetByID: getAssetByID, }; }, 223, [], 'node_modules/react-native/Libraries/Image/AssetRegistry.js' );
在注册图片时会调用registerAsset
方法,registerAsset
将图片module注册到一个全局assets数组中,然后返回当前assets数组的长度,也表示图片模块id。getAssetByID
方法会根据传入的id,从全局assets数组中取出已经注册的图片信息。
需要注意这里的图片信息只包含本地图片资源,而不包含网络图片资源
所以我们在代码中写的import bg from './bg.png'
, 经过打包后bg就是一个数字(模块注册时的assets.length)。因此<Image source={xxx}/>
加载本地图片资源时,source prop其实传入的是一个数字。
3. 图片source拼接
我们来看看Image组件是如何通过图片模块id来拼接source的
// react-native/Libraries/Image/Image.ios.js 代码有删减 const BaseImage = (props) => { const source = getImageSourcesFromImageProps(props) || { uri: undefined, width: undefined, height: undefined, }; let sources; let style: ImageStyleProp; if (Array.isArray(source)) { style = flattenStyle([styles.base, props.style]) || {}; sources = source; } else { const {width = props.width, height = props.height, uri} = source; style = flattenStyle([{width, height}, styles.base, props.style]) || {}; sources = [source]; if (uri === '') { console.warn('source.uri should not be an empty string'); } } const objectFit = style && style.objectFit ? convertObjectFitToResizeMode(style.objectFit) : null; const resizeMode = objectFit || props.resizeMode || (style && style.resizeMode) || 'cover'; const { height, width, ...restProps } = props; return ( return ( <ImageViewNativeComponent {...restProps} style={style} resizeMode={resizeMode} source={sources} /> ); ); };
Image组件一开始会调用getImageSourcesFromImageProps
来解析传入的source, 然后传入到native提供的组件(RCTImageView)进而显示。
// 代码有删减 function getImageSourcesFromImageProps(imageProps) { return resolveAssetSource(imageProps.source); }
进入resolveAssetSource
/** * `source` is either a number (opaque type returned by require('./foo.png')) * or an `ImageSource` like { uri: '<http location || file path>' } */ function resolveAssetSource(source: any): ?ResolvedAssetSource { if (typeof source === 'object') { return source; } const asset = AssetRegistry.getAssetByID(source); if (!asset) { return null; } const resolver = new AssetSourceResolver( getDevServerURL(), getScriptURL(), asset, ); // 如果存在自定义处理函数_customSourceTransformer,就返回它的执行结果。 // 可以通过setCustomSourceTransformer来设置。 if (_customSourceTransformer) { return _customSourceTransformer(resolver); } return resolver.defaultAsset(); }
通过注释和源码得知,传入的source有两种形式:
- object形式,包含一个uri网络图片地址
- 数字形式,即上文所说在AssetRegistry中注册返回的模块id
如果是source是一个object直接返回。否则会通过AssetRegistry.getAssetByID
将之前注册的图片信息提取出来,然后经过AssetSourceResolver
解析,形成最终的source并返回。
而在初始化AssetSourceResolver
时传入了三个参数,分别是服务器地址(类似 http://localhost:8081) ,bundle所在位置和注册的图片信息。
defaultAsset
包含了最终返回source的逻辑
// 代码有删减 class AssetSourceResolver { constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) { this.serverUrl = serverUrl; this.jsbundleUrl = jsbundleUrl; this.asset = asset; } isLoadedFromServer(): boolean { return !!this.serverUrl; } isLoadedFromFileSystem(): boolean { return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://')); } defaultAsset(): ResolvedAssetSource { // 如果是本地开发 if (this.isLoadedFromServer()) { return this.assetServerURL(); } // 非本地开发,Native内嵌 if (Platform.OS === 'android') { return this.isLoadedFromFileSystem() ? this.drawableFolderInBundle() : this.resourceIdentifierWithoutScale(); } else { return this.scaledAssetURLNearBundle(); } } assetServerURL(): ResolvedAssetSource { return this.fromSource( this.serverUrl + getScaledAssetPath(this.asset) + '?platform=' + Platform.OS + '&hash=' + this.asset.hash, ); } /** * If the jsbundle is running from a sideload location, this resolves assets * relative to its location * E.g. 'file:///sdcard/xxx/drawable-xxhdpi/src_bg.png' */ drawableFolderInBundle(): ResolvedAssetSource { const path = this.jsbundleUrl || 'file://'; return this.fromSource(path + getAssetPathInDrawableFolder(this.asset)); } /** * The default location of assets bundled with the app, located by * resource identifier * The Android resource system picks the correct scale. * E.g. 'src_bg' */ resourceIdentifierWithoutScale(): ResolvedAssetSource { return this.fromSource(getAndroidResourceIdentifier(this.asset)); } /** * Resolves to where the bundle is running from, with a scaled asset filename * E.g. 'file:///sdcard/bundle/assets/src/bg@3x.png' */ scaledAssetURLNearBundle(): ResolvedAssetSource { const path = this.jsbundleUrl || 'file://'; return this.fromSource( // Assets can have relative paths outside of the project root. // When bundling them we replace `../` with `_` to make sure they // don't end up outside of the expected assets directory. path + getScaledAssetPath(this.asset).replace(/\.\.\//g, '_') ); } fromSource(source: string): ResolvedAssetSource { return { __packager_asset: true, width: this.asset.width, height: this.asset.height, uri: source, scale: pickScale(this.asset.scales, PixelRatio.get()), }; } }
/** * 返回图片在服务器中的路径,比如 'assets/src/bg@3x.png' */ function getScaledAssetPath(asset: PackagerAsset): string { const scale = pickScale(asset.scales, PixelRatio.get()); const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x'; // 这里的assetDir其实就是 之前通过__d定义的图片信息中的httpServerLocation,即assets_src const assetDir = getBasePath(asset); return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type; }
// 判断选择哪种尺寸的图片 // RN根据当前手机的ratio加载对应的scale图片。如果当前手机的ratio没有匹配到正确的scale图片,则会获取第一个大于当前手机ratio的scale图片。 // 例如当前手机的scale为2,如果存在2x图片,则返回2x图片。如果没有2x图,则会向上获取3x图 export function pickScale(scales: Array<number>, deviceScale?: number): number { if (deviceScale == null) { deviceScale = PixelRatio.get(); } // Packager guarantees that `scales` array is sorted for (let i = 0; i < scales.length; i++) { if (scales[i] >= deviceScale) { return scales[i]; } } // If nothing matches, device scale is larger than any available // scales, so we return the biggest one. Unless the array is empty, // in which case we default to 1 return scales[scales.length - 1] || 1; }
通过分析代码可知有两种情况:
3.1 如果bundle放在服务器(本地开发)
图片source由serverUrl + 图片在服务器中的地址拼接组成
this.serverUrl + getScaledAssetPath(this.asset) + '?platform=' + Platform.OS + '&hash=' + this.asset.hash,
比如上述的bg图片在本地开发时会最终返回
http://localhost:8081/assets/src/bg@3x.png?platform=ios&hash=615a107224f6f73b539078be1c162c6c
3.2 bundle内置在app中(app下载bundle和assets后执行)
这里不同平台的处理方式又不一样。
ios
直接从文件系统读取
android
分为两种:
- 资源标识符(Android 资源系统会选择正确的比例)
- 文件系统
4. Image style的witdh和height没有声明会发生什么?
有时候在我们在Image组件中没有传入style,或者并没有在style中声明width和height,那么图片实际展示的宽高为多少呢?
//image.ios.js const source = getImageSourcesFromImageProps(props) || { uri: undefined, width: undefined, height: undefined, }; const {width, height, uri} = source; style = flattenStyle([{width, height}, styles.base, props.style]) || {};
由Image组件源码得知, 此时会使用注册图片模块时的width和height。
registerAsset({ __packager_asset: true, httpServerLocation: '/assets/src', width: 295, height: 153, scales: [1, 1.5, 2, 3], hash: '615a107224f6f73b539078be1c162c6c', name: 'bg', type: 'png', });
前面提到,无论图片有哪些尺寸,注册时的宽高始终是1x图的宽高。所以当我们在Image组件没有写style 宽高时,RN会默认设置为1x图的宽高(无论你的手机屏幕尺寸如何)。
加载全部内容