目录
  • 正文
  • 1. 构建输出的产物
  • 2. js bundle分析
  • 3. 图片source拼接
    • 3.1 如果bundle放在服务器(本地开发)
    • 3.2 bundle内置在app中(app下载bundle和assets后执行)
  • 4. Image style的witdh和height没有声明会发生什么?

    正文

    我们知道,在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
    

    构建结果如下:

    react native图片解析流程详解

    react native图片解析流程详解

    ios会将图片输出到assets目录下,且图片保留图片目录层次结构。

    android中,drawable-mdpidrawable-hdpidrawable-xhdpidrawable-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 +
            '&amp;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 资源系统会选择正确的比例)
    • 文件系统

    react native图片解析流程详解

    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图的宽高(无论你的手机屏幕尺寸如何)。

    以上就是react native图片解析流程详解的详细内容,更多关于react native图片解析的资料请关注其它相关文章!

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。