React Native作为facebook的大作,一直收到开发者的喜爱。facebook最初设想React Native独立 完成应用,但是我们开发者更多开发在已有应用基础上,引入React Native帮助我们完成部分功能的开发。 接下来我来描述下我在引入React Native后,遇到的有关图片资源管理问题。
React Native Bundle 打包 通过以下命令打包:1 react-native bundle --entry-file index.android.js --platform android --dev false --bundle-output build/index.bundle --assets-dest build
输出为:
React Native在bundle后,会为开发者把资源整理到不同的drawable下。
使用Bundle和图片资源 React Native支持两种使用bundle和图片资源的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this ) { @Override protected List<ReactPackage> getPackages () { return Arrays.<ReactPackage>asList( new MainReactPackage() ); } @Nullable @Override protected String getBundleAssetName () { return "****" ; } @Nullable @Override protected String getJSBundleFile () { return "****" ; } }
getJSBundleFile指定内存和存储设备bundle的filePath; getBundleAssetName指定assets中bundle的filePath; 其中优先使用getJSBundleFile。
可以看到我们只为React Native指定bundle文件路径,并没有指定资源路径。那么React Native 如何使用图片资源呢?
源码分析 分析图片资源访问问题,我们先找到node_modules/react-native/Libraries/Image/image.android.js。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 render: function ( ) { const source = resolveAssetSource(this .props.source); const loadingIndicatorSource = resolveAssetSource(this .props.loadingIndicatorSource); ... if (source && (source.uri || Array .isArray(source))) { let style; let sources; if (source.uri) { const {width, height} = source; style = flattenStyle([{width, height}, styles.base, this .props.style]); sources = [{uri: source.uri}]; } else { style = flattenStyle([styles.base, this .props.style]); sources = source; } const {onLoadStart, onLoad, onLoadEnd} = this .props; const nativeProps = merge(this .props, { style, shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd), src: sources, loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null , }); ... } return null ; } });
可以看到render中通过resolveAssetSource对是source进行了处理。
继续去resolveAssetSource看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 function getBundleSourcePath ( ): ?string { if (_bundleSourcePath === undefined ) { const scriptURL = SourceCode.scriptURL; if (!scriptURL) { _bundleSourcePath = null ; return _bundleSourcePath; } if (scriptURL.startsWith('assets://' )) { _bundleSourcePath = null ; return _bundleSourcePath; } if (scriptURL.startsWith('file://' )) { _bundleSourcePath = scriptURL.substring(7 , scriptURL.lastIndexOf('/' ) + 1 ); } else { _bundleSourcePath = scriptURL.substring(0 , scriptURL.lastIndexOf('/' ) + 1 ); } } return _bundleSourcePath; } function setCustomSourceTransformer ( transformer: (resolver: AssetSourceResolver ) => ResolvedAssetSource ,): void { _customSourceTransformer = transformer; } 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(); }
构造方法中创建了AssetSourceResolver,默认通过defaultAsset对asset进行处理,注意到:在构建AssetSourceResolver时,第二个参数getBundleSourcePath()中通过assets://和file://对bundle路径进行了区分处理,assets://是返回null。
继续去AssetSourceResolver中看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 isLoadedFromFileSystem(): boolean { return !!this .bundlePath; } defaultAsset(): ResolvedAssetSource { if (this .isLoadedFromServer()) { return this .assetServerURL(); } if (Platform.OS === 'android' ) { return this .isLoadedFromFileSystem() ? this .drawableFolderInBundle() : this .resourceIdentifierWithoutScale(); } else { return this .scaledAssetPathInBundle(); } }
重点来啦,AssetSourceResolver是通过this.isLoadedFromFileSystem区分加载内存和存储设备或者res中drawable资源。在resolveAssetSource中,内置assets://时,this.isLoadedFromFileSystem为false,所以从res中加载图片资源。
分析总结 React Native图片资源加载机制,依赖bundle文件路径加载图片资源。
bundle文件内置assets时,加载res中的drawable资源,故内置bundle时,需要把bundle依赖的资源copy到res中;、
bundle文件在内存或者存储设备时,加载bundle文件同目录下的图片资源。
问题来啦 当我们内置bundle文件到assets中时,React Native要求把图片资源拷贝到res下才能访问到图片资源,以提升bundle文件访问图片资源的效率。但是这些资源在我们应用中只有bundle文件需要我们把bundle文件需要的图片资源和宿主应用的图片资源混在res中,不利于项目资源管理。
理想的情况是bundle文件和图片资源文件可以一起内置到assets中,那么我们怎么解决呢?
解决方案 在resolveAssetSource中,我们可以看到resolveAssetSource的构造方法中优先_customSourceTransformer对assets处理,不存在时,才使用resolver.defaultAsset()。看来我们需要自定义一个Transformer来搞定。
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { AppRegistry, Image, NativeModules, } from 'react-native' import Root from './app/root' const { setCustomSourceTransformer } = require ('resolveAssetSource' );const PixelRatio = require ('PixelRatio' );const AssetSourceResolver = require ('AssetSourceResolver' );const assetPathUtils = require ('./node_modules/react-native/local-cli/bundle/assetPathUtils' );function getAssetPathInDrawableFolder (asset ) { const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get()); const drawbleFolder = assetPathUtils.getAndroidDrawableFolderName(asset, scale); const fileName = assetPathUtils.getAndroidResourceIdentifier(asset); return `${drawbleFolder} /${fileName} .${asset.type} ` ; } setCustomSourceTransformer((resolver) => { const { SourceCode } = NativeModules; let bundlePath = SourceCode.scriptURL; if (bundlePath.startsWith('assets://' )) { bundlePath = bundlePath.substring(9 , bundlePath.lastIndexOf('/' ) + 1 ); const rootURL = `asset:///${bundlePath} ` ; const asset = resolver.asset; return resolver.fromSource( rootURL + getAssetPathInDrawableFolder(asset) ); } return resolver.defaultAsset(); }) AppRegistry.registerComponent('Root' , () => Root)