在 android 原生View的绘制流程,可以参考以下郭霖大神的博客: Android视图绘制流程完全解析,带你一步步深入了解View(二)
在之前的认知中,在android原生显示的都是原生的 View,即使是显示html也是通 WebView 去解析的渲染 html 从而显示出网页内容。那么在React Native的应用场景下,是如何将 JS代码生成视图的呢?
在React-Native/React 的官网上,没找到相关的说明,那么我们只好自己找源码了,可以通过编写简单的测试代码来验证,下面是我的入口 JS代码,在EasyView.js中,这个js文件的完整路径是:项目主目录/rnjs/view_draw。
import React,{ Component} from 'react'
import {View, Text, AppRegistry} from 'react-native'
class EasyView extends Component {
render() {
return(
How to draw a view!
)
}
}
AppRegistry.registerComponent("TestRN",() =>EasyView); // TextRN是注册的入口Component名称,默认和项目名称一样
然后在 android.index.js 中只有一句代码(这样的话,方便自己写不同场景的测试代码):
require('./rnjs/view_draw/EasyView')
这样,我们去到 android原生,查看一下代码。
首先去到主页(入口和显示)的 Activity 类中,我们直接从 Activity 的生命周期看起,默认生成的 Activity 中继承了 ReactActivity,实现了 DefaultHardwareBackBtnHandler和PermissionAwareActivity接口,这两个接口主要是处理一些//todo
我们直接从 ReactActivity 看起吧,在 ReactActivity 的 onCreate()方法中,这里完成了视图的绘制:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 如果是开发者支持显示悬浮窗(默认是返回true,支持按下菜单栏或者摇一摇就显示开发者菜单)
//并且是android6.0上,android 6.0需要手动开启一些权限,由于android 6.0 使用了新的权限管理机制,动态权限管理机制,和ios很类型。
if (getUseDeveloperSupport() && Build.VERSION.SDK_INT >= 23) {
// Get permission to show redbox in dev builds.
if (!Settings.canDrawOverlays(this)) {
//Settings.ACTION_MANAGE_OVERLAY_PERMISSION是申请SYSTEM_ALERT_WINDOW权限对应的intent action,也就是悬浮窗权限
Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(serviceIntent);
FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
Toast.makeText(this, REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
}
}
//这个 RootView 就是我们今天关注的重点了
mReactRootView = createRootView();
mReactRootView.startReactApplication(
getReactNativeHost().getReactInstanceManager(),
getMainComponentName(),
getLaunchOptions());
// 所以我们清楚的知道了,显示的就是这个mReactRootView了
setContentView(mReactRootView);
mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}
我们看一下 createRootView()方法里面做了什么。
/**
* A subclass may override this method if it needs to use a custom {@link ReactRootView}.
*/
protected ReactRootView createRootView() {
return new ReactRootView(this);
}
这个方法只是简单的 New 了一个 ReactRootView 出来,然后方法的说明是吗,一个子类可以覆盖这个方法实现自定义的 ReactRootView。且不说这个,看到这里,我们可以推测,这个 RootView 肯定具有我们常用的 View 一些共同点,因为我们经常也是 new一个button出来什么的。
接着我们去看一下 ReactRootView 类的源码
/**
* Default root view for catalyst apps. Provides the ability to listen for size changes so that a UI
* manager can re-layout its elements.
* It delegates handling touch events for itself and child views and sending those events to JS by
* using JSTouchDispatcher.
* This view is overriding {@link ViewGroup#onInterceptTouchEvent} method in order to be notified
* about the events for all of it's children and it's also overriding
* {@link ViewGroup#requestDisallowInterceptTouchEvent} to make sure that
* {@link ViewGroup#onInterceptTouchEvent} will get events even when some child view start
* intercepting it. In case when no child view is interested in handling some particular
* touch event this view's {@link View#onTouchEvent} will still return true in order to be notified
* about all subsequent touch events related to that gesture (in case when JS code want to handle
* that gesture).
*/
public class ReactRootView extends SizeMonitoringFrameLayout implements RootView {
.......
.......
public ReactRootView(Context context) {
super(context);
}
public ReactRootView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReactRootView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
简单的翻译一下就是:构建app的默认 root view,提供了监听大小改变的能力,所以一个UI manager可以重新layout它的元素。它为自己和它的child view 代处理所有的触摸事件,并且会通过 JSTouchDisoatcher 发送事件到JS。这个类重写了 ViewGroup的onInterceptTouchEvent()方法,和ViewGroup的requestDisallowInterceptTouchEvent()方法,保证所有的触摸事件能够被ViewGroup的onInterceptTouchEvent()能够正常分发和接收。如果child view 对当前的触摸事件不感兴趣,当前 child view的 onTouchEvent()方法依旧必须返回 true,这样才能保证可以接收到一些系列的Touch 事件(也可能JS想要接收这些事件)。
简单的来说这个类,它继承自 SizeMonitoringFrameLayout 类,而 SizeMonitoringFrameLayout 继承自 FrameLayout布局类,实现了一个View改变大小的接口回调类,用于监听 View的 onSizeChanged()方法,实现比较简单,所以我们把注意力集中在 ReactRootView 本身。
所以看到这里,看到构造方法,熟悉自定义组件的同学,应该很容易看出来,这个 ReactRootView 肯定是个自定义组件了。那么我们着重的来看一下 View 类型组件的相关方法:
onMeasure()方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 如SpecMode 为UNSPECIFIED,则直接抛出异常,UNSPECIFIED也就是说高和宽都不确定
if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
throw new IllegalStateException(
"The root catalyst view must have a width and height given to it by it's parent view. " +
"You can do this by specifying MATCH_PARENT or explicit width and height in the layout.");
}
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
mWasMeasured = true;
// Check if we were waiting for onMeasure to attach the root view
if (mReactInstanceManager != null && !mIsAttachedToInstance) {
// Enqueue it to UIThread not to block onMeasure waiting for the catalyst instance creation
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
attachToReactInstanceManager();
}
});
}
}
onMeasure()方法确定了组件的大小,我们看到这里应该考虑,我们在JS中设置了那个Text,Text的属性值,例如颜色,宽高,是怎么传递到android原生的。这里先简单说一下Android原生View的绘制需要一个MeasureSpec类参数,MeasureSpec 由 SpecMode和SpecSize组成,SpecMode有三种类型:
而 SpecSize则记录了组件的大小信息。
在上面的 onMeasure()方法中,前两行获得了widthMode和heightMode,然后判断它们两个的数值是不是等于 UNSPECIFIED,如果其中一个等于 UNSPECIFIED,则抛出异常。调用 View的setMeasuredDimension()方法,设置View的宽和高。接着讲 mWasMeasured 设置 true,这个是标志这个View已经完成了Measure操作的标志。再接着跑判断是否为空,并且是还没attached到Instance上去的,然后在UI主线程执行一个 runnable,执行attachToReactInstanceManager()方法。
我们进入 attachToReactInstanceManager()方法看一下:
private void attachToReactInstanceManager() {
if (mIsAttachedToInstance) {
return;
}
// 这里将 mIsAttachedToInstance 设置为 true
mIsAttachedToInstance = true;
Assertions.assertNotNull(mReactInstanceManager).attachMeasuredRootView(this);// Asserions框架,判断 mReactInstanceManager 是否为空。不为空的话 attach它。
getViewTreeObserver().addOnGlobalLayoutListener(getKeyboardListener());
}
这个方法里面,调用了 ReactInstanceManager 类的 attachMeasuredRootView()方法,我们看一下这个方法做了什么,
/**
* Attach given {@param rootView} to a catalyst instance manager and start JS application using
* JS module provided by {@link ReactRootView#getJSModuleName}. If the react context is currently
* being (re)-created, or if react context has not been created yet, the JS application associated
* with the provided root view will be started asynchronously, i.e this method won't block.
* This view will then be tracked by this manager and in case of catalyst instance restart it will
* be re-attached.
*/
public abstract void attachMeasuredRootView(ReactRootView rootView);
我们可以看到 ReactInstanceManager 是在 startReactApplication()方法里被赋值,所以我们又回到了 ReactActivity 类的 onCreate()方法中了,这里对 ReactInstanceManager 进行了赋值调用的是:
getReactNativeHost().getReactInstanceManager()
这个 ReactInstanceManager 类又是用来干嘛的呢?
ReactInstanceManager 类是在 ReactNativeHost 类的 getReactInstanceManager() 方法下被创建的。
public ReactInstanceManager getReactInstanceManager() {
if (mReactInstanceManager == null) {
mReactInstanceManager = createReactInstanceManager();
}
return mReactInstanceManager;
}
而 ReactNativeHost 类是在 Application 类被创建的,ReactNativeHost 类其实
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List getPackages() {
return Arrays.asList(
new MainReactPackage(),
new ReactNativePackager()
);
}
};
看到这里,我们在回到 ReactActivity 的 onCreate()方法,似乎就有点峰回路转了。我们找到 ReactInstanceManager 的实现类 XReactInstanceManagerImpl(旧版本是使用ReactInstanceManagerImpl,这两者之间略有不同,后续再分析)。我们先看一下 XReactInstanceManagerImpl 类的 attachMeasuredRootView()方法:
@Override
public void attachMeasuredRootView(ReactRootView rootView) {
UiThreadUtil.assertOnUiThread();
mAttachedRootViews.add(rootView);
// If react context is being created in the background, JS application will be started
// automatically when creation completes, as root view is part of the attached root view list.
if (mReactContextInitAsyncTask == null && mCurrentReactContext != null) {
attachMeasuredRootViewToInstance(rootView, mCurrentReactContext.getCatalystInstance());
}
}
JSBundleLoader.createFileLoader(mApplication, mJSBundleFile) 去加载 JSbundle 文件的。
调用了View的getViewTreeObserver()方法,获取了当前View的 ViewTreeObserver,ViewTreeObserver 是一个视图树的监听类,会在View树重新 layout,draw,measure的时候发送和处理通知。那么这里为什么为这个 ViewTreeObserver 添加一个 Listener 呢?
我们继续往下看,SizeMonitoringFrameLayout类 是继承 FrameLayout的,实现了 RootView 接口,而RootView 接口只是为了RN中,操作了原生事件的时候能够回调,先看下 RootView 的代码:
import android.view.MotionEvent;
/**
* Interface for the root native view of a React native application.
*/
public interface RootView {
/**
* Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called
* from the child's onTouchIntercepted implementation.
*/
void onChildStartedNativeGesture(MotionEvent androidEvent);
}
我们尝试在源码中找出它的调用时机 // todo