2019-03-11
Image 组件是 React Native 中的最常用组件之一,在本文中我们来看它在 Android 下的实现原理。
对于 Image 的使用,可以参考官方文档。
在 React Native 中使用方式很简单:
<Image source={require('./my-icon.png')} />
这是如何实现的呢?它是如何映射到 Android 的 ImageView 呢?
Image 组件定义在 image.android.js(Libraries/Image/Image.android.js)中。
在这个文件中定义了:
其中:
下面我们来详细看 Image 组件,它是一个函数式组件,一开始对 Props 进行一系列的校验。我们着重看它的 return 逻辑:
return ( <TextAncestor.Consumer> {hasTextAncestor => hasTextAncestor ? ( <TextInlineImageNativeComponent {...nativeProps} /> ) : ( <ImageViewNativeComponent {...nativeProps} /> ) } </TextAncestor.Consumer> );
其中:包含一个判断逻辑,这里我们直接看 ImageViewNativeComponent。它位于:Libraries/Image/TextInlineImageNativeComponent.js:
'use strict'; const requireNativeComponent = require('requireNativeComponent'); const ImageViewNativeComponent = requireNativeComponent('RCTImageView'); module.exports = ImageViewNativeComponent;
上面代码中的 RCTImageView 对应于 Android Native 侧的 com.facebook.react.views.image.ReactImageManager。
ReactImageManager 有一个 createViewInstance,React Native 通过这个方法创建 ReactImageView 实例。
同时 ReactImageManager 还有一系列 setter 方法:
这些方法的参数都满足一定的模式:第一个参数为 ReactImageView 实例,第二个参数为属性的值。比如:
// In JS this is Image.props.source @ReactProp(name = "src") public void setSource(ReactImageView view, @Nullable ReadableArray sources) { view.setSource(sources); }
每个 setter 方法都调用了 ReactImageView 的对应 setter 方法。
需要注意的一点是,ReactImageView 每次被 set 属性后,不会立刻更新。它内部有一个 mIsDirty 状态,ReactImageView 会将这个状态先置为 true,之后统一根据是否 dirty 统一更新。
现在我们知道,React Native JS 侧的 Image 组件对应于 Android 的 ReactImageView。那么 ReactImageView 是如何把图片展示出来的呢?
前面我们说到 ReactImageView 采用 dirty-update 的机制。update 方法为 com.facebook.react.views.image.ReactImageView#maybeUpdateView。
它会首先判断是否 dirty,如果不 dirty 就直接退出方法:
if (!mIsDirty) { return; }
经过一系列的参数设置,最终创建出 Fresco 的图片加载请求:
ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri()) .setPostprocessor(postprocessor) .setResizeOptions(resizeOptions) .setAutoRotateEnabled(true) .setProgressiveRenderingEnabled(mProgressiveRenderingEnabled); ImageRequest imageRequest = ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, mHeaders); // This builder is reused mDraweeControllerBuilder.reset(); mDraweeControllerBuilder .setAutoPlayAnimations(true) .setCallerContext(mCallerContext) .setOldController(getController()) .setImageRequest(imageRequest); setController(mDraweeControllerBuilder.build()); mIsDirty = false; mDraweeControllerBuilder.reset();
其中:
这里我们不深入 Fresco 库内部的实现原理。在上面的代码中,我们知道了通过上述代码,调用 Fresco 库进行了图片加载。
同时在 maybeUpdateView 方法中,还有很多对 Prop 细节的解析,比如圆角、背景、边框、Scale 方式等等的解析与配置。这些操作都是基于 Fresco 库进行的二次封装,感兴趣的同学可以再进一步看一下。
在我们的 JavaScript 代码中:
<Image source={require('./my-icon.png')} />
指定了 source 属性,指定图片的位置。 source 属性是 Image 组件最终的属性,它是如何解析的呢?
从 Image 使用文档中我们知道,React Native 的 Image 组件支持多种数据来源:JS 相对路径导入、Hybrid 资源引用、网络数据导入、Uri Data 都是可以的。
这么多种类的来源如何进行处理呢?
思考这个问题的思路如下:
因此就找到了 com.facebook.react.views.image.ReactImageManager#setSource 方法:
// In JS this is Image.props.source @ReactProp(name = "src") public void setSource(ReactImageView view, @Nullable ReadableArray sources) { view.setSource(sources); }
之后来到 com.facebook.react.views.image.ReactImageView#setSource 方法,它的核心实现如下:
首先获取 source Props:
String uri = source.getString("uri"); ImageSource imageSource = new ImageSource(getContext(), uri);
其中:
./my-icon.png
如何判定 source 是来源自网络、Hybrid 还是 Uri Data 呢?这就需要借助于 ImageSource 这个类,它专门用于解析来源。
ImageSource 解析来源的核心代码如下:
private Uri computeUri(Context context) { try { Uri uri = Uri.parse(mSource); // Verify scheme is set, so that relative uri (used by static resources) are not handled. return uri.getScheme() == null ? computeLocalUri(context) : uri; } catch (Exception e) { return computeLocalUri(context); } } private Uri computeLocalUri(Context context) { isResource = true; return ResourceDrawableIdHelper.getInstance().getResourceDrawableUri(context, mSource); }
其中:
Image 组件有四个回调方法:
它们是从哪里发出,如何在 JS 层接到的呢?
答案在 ReactImageManager 的 getExportedCustomDirectEventTypeConstants 方法中:
@Override public @Nullable Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_START), MapBuilder.of("registrationName", "onLoadStart"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD), MapBuilder.of("registrationName", "onLoad"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_ERROR), MapBuilder.of("registrationName", "onError"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_END), MapBuilder.of("registrationName", "onLoadEnd")); }
这个方法中创建了一个 Map,对 ImageLoadEvent 枚举和名称进行了映射。
这几个回调对应于 com.facebook.react.views.image.ImageLoadEvent 中的几个常量:
// Currently ON_PROGRESS is not implemented, these can be added // easily once support exists in fresco. public static final int ON_ERROR = 1; public static final int ON_LOAD = 2; public static final int ON_LOAD_END = 3; public static final int ON_LOAD_START = 4; public static final int ON_PROGRESS = 5;
这些事件是在哪里发出的呢?答案是在 ReactImageView 的 setShouldNotifyLoadEvents 方法中。
这个方法的定义如下:
@ReactProp(name = "shouldNotifyLoadEvents") public void setLoadHandlersRegistered( ReactImageView view, boolean shouldNotifyLoadEvents) { view.setShouldNotifyLoadEvents(shouldNotifyLoadEvents); }
其中,可以看出:
下面我们看 setShouldNotifyLoadEvents 的内部实现,核心代码如下:
final EventDispatcher mEventDispatcher = ((ReactContext) getContext()).getNativeModule(UIManagerModule.class).getEventDispatcher(); mControllerListener = new BaseControllerListener<ImageInfo>() { @Override public void onSubmit(String id, Object callerContext) { mEventDispatcher.dispatchEvent( new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD_START)); } @Override public void onFinalImageSet( String id, @Nullable final ImageInfo imageInfo, @Nullable Animatable animatable) { if (imageInfo != null) { mEventDispatcher.dispatchEvent( new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD, mImageSource.getSource(), imageInfo.getWidth(), imageInfo.getHeight())); mEventDispatcher.dispatchEvent( new ImageLoadEvent(getId(), ImageLoadEvent.ON_LOAD_END)); } } @Override public void onFailure(String id, Throwable throwable) { mEventDispatcher.dispatchEvent( new ImageLoadEvent(getId(), ImageLoadEvent.ON_ERROR, true, throwable.getMessage())); } }; }
其中:
对于 EventDispatcher 的内部机制,限于篇幅我们暂不在本文中展开,点到为止。
在查看 React Native 的 Bug 列表时,发现有一个说 Image.getSize 方法在 Android 失效了。我想修复这个 Bug,因此需要研究这个方法的实现原理。
Issue 详情参见这里。需要注意的是,这个 Bug 只在加载来自 Drawable 的图片时会出现,加载其他来源的图片没有问题。
Image.getSize 方法位于 js 侧 Image 组件,它的代码在 Libraries/Image/Image.android.js。
其代码如下:
function getSize( url: string, success: (width: number, height: number) => void, failure?: (error: any) => void, ) { return ImageLoader.getSize(url) .then(function(sizes) { success(sizes.width, sizes.height); }) .catch( failure || function() { console.warn('Failed to get size for image: ' + url); }, ); }
其中:
在上面的代码中,ImageLoader 是一个 Native Module,被映射到了 ImageLoaderModule 类,位于 com/facebook/react/modules/image/ImageLoaderModule.java。
下面我们来看这个方法的实现。这个类的完整代码参见这里。
首先方法签名:
@ReactMethod public void getSize( final String uriString, final Promise promise) {
其中:
例如,drawable 中有一个图片名为 tv_banner
,在 js 侧会调用 Image.getSize('tv_banner')
,传到上面 Java 侧的方法时,uriString 的值也是 tv_banner
。
下面我们来看 getSize 方法内部:
Uri uri = Uri.parse(uriString);
首先,Uri.parse 会将 uriString 转为 Android 中的 Uri。
由于这里的信息非常少,解析得到的 Uri 里面许多字段都为空:
这里可能是导致问题的原因,我们先记住这里接着往下看。
ImageRequest 属于 Facebook 的 imagepipeline 库。这是 Fresco 项目的一个子项目。
好在社区中有一篇官方文档直接使用Image Pipeline,看起来跟我们的场景很像,快速学习一下。文档中的内容这里就不赘述了。
我们回到 getSize 方法。
首先是创建一个 ImageRequest:
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri).build();
之后是获取数据源:
DataSource<CloseableReference<CloseableImage>> dataSource = Fresco.getImagePipeline().fetchDecodedImage(request, mCallerContext);
之后是订阅数据:
DataSubscriber<CloseableReference<CloseableImage>> dataSubscriber = new BaseDataSubscriber<CloseableReference<CloseableImage>>() { @Override protected void onNewResultImpl( DataSource<CloseableReference<CloseableImage>> dataSource) { if (!dataSource.isFinished()) { return; } CloseableReference<CloseableImage> ref = dataSource.getResult(); if (ref != null) { try { CloseableImage image = ref.get(); WritableMap sizes = Arguments.createMap(); sizes.putInt("width", image.getWidth()); sizes.putInt("height", image.getHeight()); promise.resolve(sizes); } catch (Exception e) { promise.reject(ERROR_GET_SIZE_FAILURE, e); } finally { CloseableReference.closeSafely(ref); } } else { promise.reject(ERROR_GET_SIZE_FAILURE); } } @Override protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) { promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()); } };
最后是实际执行订阅操作:
dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance());
在上面的大代码块中,有多处异常逻辑,都会导致 promise.reject,最终是哪个导致的呢?
我们都下断点来 Debug 一下,发现最终落入了 onFailureImpl。
onFailureImpl 是在哪个步骤出错的呢?这个要看图片实际加载的过程,这个过程位于 com.facebook.imagepipeline.core.ImagePipeline#fetchDecodedImage(com.facebook.imagepipeline.request.ImageRequest, java.lang.Object, com.facebook.imagepipeline.request.ImageRequest.RequestLevel, com.facebook.imagepipeline.listener.RequestListener) 方法。
发现异常是从这一句抛出来的:
Producer<CloseableReference<CloseableImage>> producerSequence = mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest);
异常原因是什么呢?
java.lang.IllegalArgumentException: Unsupported uri scheme! Uri is: tv_banner
从中可以看出,问题确实出在这个 Uri 身上。
Android 的 Drawable uri 应该是什么样的呢?
它的格式可以参考下面文章:
在本文中,我们深入分析了 Image 组件的实现原理。包括:
同时 Image.getSize 是以跟踪问题的形式进行调试,并最终找到了问题所在。