react native图片解析流程详解

目录
  • 正文
  • 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

构建结果如下:

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 资源系统会选择正确的比例)
  • 文件系统

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图片解析的资料请关注我们其它相关文章!

(0)

相关推荐

  • react native reanimated实现动画示例详解

    目录 背景 动画拆分 实现抖动 定义动画 实现缩放动画 改变内容 Reanimated 原理浅析 总结 背景 在一次 App 迭代中,UI 想要给按钮添加一个动画效果,在对接的过程中,UI 表示直接用 .gif 就好,因为感觉开发出来的效果应该不会很好. 听到这里,一个技术人的自尊心仿佛被踩在了地上,我当即表示想用 react-native-reanimated(下文简称 Reanimated) 试一试. 动画拆分 首先,从最外层来看,动画有一个抖动效果:先向左,再向右.可以利用 rotate

  • 一文详解ReactNative状态管理rematch使用

    目录 引言 React 和 rematch 创建Todo List App 创建一个 models.ts 文件 创建一个 todo.ts 文件 使用 rematch 的 init 函数创建 store RematchTodoApp 要创建UI 组件 总结 引言 有同学反馈开发 ReactNative 应用时状态管理不是很明白,接下来几篇文章我们来对比下 React 及 ReactNative 状态管理常用的几种框架的使用和优缺点. 上一篇文章介绍了 redux 的升级版 redux-toolki

  • ReactNative 状态管理redux使用详解

    目录 正文 安装和配置开发环境 定义数据结构 然后创建行为处理函数 todoReducer 创建 UI 组件: 正文 有同学反馈开发 ReactNative 应用时状态管理不是很明白,接下来几篇文章我们来对比下 React 及 ReactNative 状态管理常用的几种框架的使用和优缺点. 首先来看下 redux 怎么使用. 以下是使用 React 和 Redux 创建 todo list 的一般过程,完整代码见文章末尾: 安装和配置开发环境 安装 Node.js 和 create-react-

  • react-native只保留3x图原理解析

    目录 引言 1. 输出构建产物 2. RN如何决定加载哪张scale图片? 3. repo中是否可以只保留3x图? 3.1 资源上传 3.2 资源下载 4. 结论 引言 我们的react-native项目中,一张图片一般会存在1x, 1.5x, 2x, 3x几种尺寸(1.5x是android特有的),以便在不同屏幕尺寸的手机加载对应尺寸的图片. 1. 输出构建产物 如果我们在代码中引入了一张图片,例如 // index.js import bg from './bg.png'; . ├── in

  • 一文详解ReactNative状态管理redux-toolkit使用

    目录 正文 React 和 Redux-Toolkit 创建 Todo List App 新建一个React应用 创建一个 todoSlice.ts 文件 创建 store 上层组件通过 Provider 分发给组件树 通过useSelector 和 useDispatch 获取数据和分发行为 正文 有同学反馈开发 ReactNative 应用时状态管理不是很明白,接下来几篇文章我们来对比下 React 及 ReactNative 状态管理常用的几种框架的使用和优缺点. 上一篇文章介绍了 red

  • ReactNative错误采集原理在Android中实现详解

    目录 引言 1 JS错误 1.1 Error 1.2 常见的错误 SyntaxError:语法错误 ReferenceError:引用错误 TypeError:类型错误 RangeError:边界错误 URIError:URI错误 1.3 自定义错误 2 RN错误处理 2.1 JS部分 2.1.1 MessageQueue 2.1.2 ErrorUtils 2.1.3 ExceptionsManager 2.2 Native部分 2.2.1 ExceptionsManagerModule 2.2

  • React Native react-navigation 导航使用详解

    一.开源库介绍 今年1月份,新开源的react-natvigation库备受瞩目.在短短不到3个月的时间,github上星数已达4000+.Fb推荐使用库,并且在React Native当前最新版本0.44中将Navigator删除.react-navigation据称有原生般的性能体验效果.可能会成为未来React Native导航组件的主流军.本篇内容基于[ ^1.0.0-beta.9 ]版本来介绍关于该库的使用和实战技巧.可以看到,虽然是beta版本,不过基本稳定,大家可放心在项目中使用.

  • react电商商品列表的实现流程详解

    目录 整体页面效果 项目技术点 拦截器的配置 主页面 添加商品 分页与搜索 修改商品 删除商品 完整代码 整体页面效果 项目技术点 antd组件库,@ant-design/icons antd的图标库 axios 接口请求,拦截器配置 node-sass sass-loader css样式的一个嵌套 react-router-dom react路由使用 react-redux redux hooks:大多数我们用的是函数组件,函数组件没有state属性,所以我们使用hooks来初始化数据,并且函

  • react组件的创建与更新实现流程详解

    目录 React源码执行流程图 legacyRenderSubtreeIntoContainer legacyCreateRootFromDOMContainer createLegacyRoot ReactDOMBlockingRoot createRootImpl createContainer createFiberRoot createHostRootFiber createFiber updateContainer 总结 这一章节就来讲讲ReactDOM.render()方法的内部实现

  • SpringBoot解析yml全流程详解

    目录 背景 加载监听器 执行run方法 加载配置文件 封装Node 调用构造器 思考 背景 前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml文件中配置了一个属性作为开关,再配合nacos就可以随时改变这个值达到我们的目的,yml文件中是这样写的: switch: turnOn: on 程序中的代码也很简单,大致的逻辑就是下面这样,如果取到的开关字段是on的话,那么就执行if判断中的代码,否则就不执行: @Value("${switch.turnOn}

  • react时间分片实现流程详解

    目录 什么是时间分片 为什么需要时间分片 实现分片开启 - 固定 为什么用performance.now()而不用Date.now() 实现分片中断.重启 - 连续 分片中断 分片重启 实现延迟执行 - 有间隔 为什么选择宏任务实现异步执行 时间分片异步执行方案的演进 时间分片简单实现 总结 我们常说的调度,可以分为两大模块,时间分片和优先级调度 时间分片的异步渲染是优先级调度实现的前提 优先级调度在异步渲染的基础上引入优先级机制控制任务的打断.替换. 本节将从时间分片的实现剖析react的异步

  • React实现卡片拖拽效果流程详解

    前提摘要: 学习宋一玮 React 新版本 + 函数组件 &Hooks 优先 开篇就是函数组件+Hooks 实现的效果如下: 学到第11篇了 照葫芦画瓢,不过老师在讲解的过程中没有考虑拖拽目标项边界问题,我稍微处理了下这样就实现拖拽流畅了 下面就是主要的代码了,实现拖拽(src/App.js): 核心在于标记当前项,来源项,目标项,并且在拖拽完成时对数据处理,更新每一组数据(useState): /** @jsxImportSource @emotion/react */ // 上面代码是使用e

  • 基于PHP的微信公众号的开发流程详解

    微信公众号开发分傻瓜模式和开发者模式两种,前者不要考虑调用某些接口,只要根据后台提示傻瓜式操作即可,适用于非专业开发人员. 开发模式当然就是懂程序开发的人员使用的. 下面简单说一下微信公众号开发的简易流程,新手看看会有帮助,高手请一笑而过. 1.配置服务器: A.首先在本机建立如下结构的文件夹(这里是我自己的习惯,仅供参考) MMPN:总目录mro message public number 微信公众号 backup:备份目录,主要用于备份php文件,每次修改时将原稿备份到里面去. images

  • Android zygote启动流程详解

    对zygote的理解 在Android系统中,zygote是一个native进程,是所有应用进程的父进程.而zygote则是Linux系统用户空间的第一个进程--init进程,通过fork的方式创建并启动的. 作用 zygote进程在启动时,会创建一个Dalvik虚拟机实例,每次孵化新的应用进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面,从而使得每个应用程序进程都有一个独立的Dalvik虚拟机实例. zygote进程的主要作用有两个: 启动SystemServer. 孵化应用

  • React团队测试并发特性详解

    目录 引言 遇到的困境 1. 如何表达渲染结果? 2. 如何测试并发环境? React的应对策略 实现一个渲染器 如何测试并发环境? 总结 引言 React18进入大家视野已经有一段时间了,不知道各位有没有尝试并发特性呢? 当启用并发特性后,React会从同步更新变为异步.带优先级.可中断的更新. 这也为编写单元测试带来了一些难度. 本文来聊聊React团队如何测试并发特性. 遇到的困境 主要有两个问题需要面对. 1. 如何表达渲染结果? React可以对接不同宿主环境的渲染器,大家最熟悉的渲染

  • React中react-redux和路由详解

    目录 观察者模式解决组件间通信问题 react-redux connect方法 Provider组件 路由 使用路由 默认路由 路由重定向 获取路由参数 路由导航 观察者模式解决组件间通信问题 使用观察者解决组件间通信,分成两步 在一个组件中,订阅消息 在另一个组件中,发布消息 发布消息之后,订阅的消息回调函数会执行,在函数中,我们修改状态,这样就可以实现组件间通信了.这就是reflux框架的实现. react-redux redux早期被设计成可以在各个框架中使用,因此在不同的框架中使用的时候

随机推荐