React-Native在android原生上的绘制流程
在 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(<View> <Text>How to draw a view!</Text> </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有三种类型:
- EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 - AT_MOST
表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。 - UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。
而 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<ReactPackage> getPackages() { return Arrays.<ReactPackage>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