react native圖片解析流程詳解
正文
我們知道,在react-native中加載一張圖片需要使用Image組件,其有兩種使用方式
import bg from './bg.png'; // 1. 加載本地圖片資源 <Image source={bg}/> <Image source={require('./bg.png)}/> // 2. 加載網(wǎng)絡(luò)圖片資源 <Image source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }}/>
1. 構(gòu)建輸出的產(chǎn)物
如果代碼里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中到底長啥樣呢? 先通過以下命令構(gòu)建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
構(gòu)建結(jié)果如下:
ios會將圖片輸出到assets目錄下,且圖片保留圖片目錄層次結(jié)構(gòu)。
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作為參數(shù),也就是__d
所注冊的模塊id,其調(diào)用了在__d
中注冊的工廠方法。
function metroRequire(moduleId) { var moduleIdReallyIsNumber = moduleId; var module = modules[moduleIdReallyIsNumber]; return module && module.isInitialized // 如果已經(jīng)初始化過,直接返回緩存 ? module.publicModule.exports // 這里其實就是 module.exports // 如果沒有初始化過,則內(nèi)部調(diào)用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進行處理,內(nèi)部調(diào)用了react-native提供的registerAsset方法來注冊資源。
資源信息包括了以下幾個重要字段:
httpServerLocation
:圖片文件夾在http server中的地址。如果我們在本地開發(fā),metro內(nèi)部會啟動一個http server,這個字段就是告訴server圖片文件夾在哪。scales
:圖片有哪些尺寸。因為bg.png存在 1x,1.5x,2x,3x 4種尺寸,所以這里scales就為[1, 1.5, 2, 3]
。如果你的圖片只有3x,那么scales就為[3]
。type
: 圖片后綴。width
:圖片寬度height
:圖片高度
經(jīng)過測試發(fā)現(xiàn),圖片有哪些尺寸,始終都是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' );
在注冊圖片時會調(diào)用registerAsset
方法,registerAsset
將圖片module注冊到一個全局assets數(shù)組中,然后返回當前assets數(shù)組的長度,也表示圖片模塊id。getAssetByID
方法會根據(jù)傳入的id,從全局assets數(shù)組中取出已經(jīng)注冊的圖片信息。
需要注意這里的圖片信息只包含本地圖片資源,而不包含網(wǎng)絡(luò)圖片資源
所以我們在代碼中寫的import bg from './bg.png'
, 經(jīng)過打包后bg就是一個數(shù)字(模塊注冊時的assets.length)。因此<Image source={xxx}/>
加載本地圖片資源時,source prop其實傳入的是一個數(shù)字。
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組件一開始會調(diào)用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, ); // 如果存在自定義處理函數(shù)_customSourceTransformer,就返回它的執(zhí)行結(jié)果。 // 可以通過setCustomSourceTransformer來設(shè)置。 if (_customSourceTransformer) { return _customSourceTransformer(resolver); } return resolver.defaultAsset(); }
通過注釋和源碼得知,傳入的source有兩種形式:
- object形式,包含一個uri網(wǎng)絡(luò)圖片地址
- 數(shù)字形式,即上文所說在AssetRegistry中注冊返回的模塊id
如果是source是一個object直接返回。否則會通過AssetRegistry.getAssetByID
將之前注冊的圖片信息提取出來,然后經(jīng)過AssetSourceResolver
解析,形成最終的source并返回。
而在初始化AssetSourceResolver
時傳入了三個參數(shù),分別是服務(wù)器地址(類似 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 { // 如果是本地開發(fā) if (this.isLoadedFromServer()) { return this.assetServerURL(); } // 非本地開發(fā),Native內(nèi)嵌 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()), }; } }
/** * 返回圖片在服務(wù)器中的路徑,比如 '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根據(jù)當前手機的ratio加載對應(yīng)的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放在服務(wù)器(本地開發(fā))
圖片source由serverUrl + 圖片在服務(wù)器中的地址拼接組成
this.serverUrl + getScaledAssetPath(this.asset) + '?platform=' + Platform.OS + '&hash=' + this.asset.hash,
比如上述的bg圖片在本地開發(fā)時會最終返回
http://localhost:8081/assets/src/bg@3x.png?platform=ios&hash=615a107224f6f73b539078be1c162c6c
3.2 bundle內(nèi)置在app中(app下載bundle和assets后執(zhí)行)
這里不同平臺的處理方式又不一樣。
ios
直接從文件系統(tǒng)讀取
android
分為兩種:
- 資源標識符(Android 資源系統(tǒng)會選擇正確的比例)
- 文件系統(tǒng)
4. Image style的witdh和height沒有聲明會發(fā)生什么?
有時候在我們在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會默認設(shè)置為1x圖的寬高(無論你的手機屏幕尺寸如何)。
以上就是react native圖片解析流程詳解的詳細內(nèi)容,更多關(guān)于react native圖片解析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
reset.css瀏覽器默認樣式表重置(user?agent?stylesheet)的示例代碼
這篇文章主要介紹了reset.css瀏覽器默認樣式表重置(user?agent?stylesheet),本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-12-12React在Dva項目中創(chuàng)建并引用頁面局部組件的方式
這篇文章主要介紹了React在Dva項目中創(chuàng)建并引用頁面局部組件的方式,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07react中使用echarts,并實現(xiàn)tooltip循環(huán)輪播方式
這篇文章主要介紹了react中使用echarts,并實現(xiàn)tooltip循環(huán)輪播方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01詳解webpack2+node+react+babel實現(xiàn)熱加載(hmr)
這篇文章主要介紹了詳解webpack2+node+react+babel實現(xiàn)熱加載(hmr) ,非常具有實用價值,需要的朋友可以參考下2017-08-08ReactNative中使用Redux架構(gòu)總結(jié)
本篇文章主要介紹了ReactNative中使用Redux架構(gòu)總結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12