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(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 viewif (mReactInstanceManager != null && !mIsAttachedToInstance) {  // Enqueue it to UIThread not to block onMeasure waiting for the catalyst instance creation  UiThreadUtil.runOnUiThread(new Runnable() {@Overridepublic void run() {  attachToReactInstanceManager();}  });}  }

onMeasure()方法确定了组件的大小,我们看到这里应该考虑,我们在JS中设置了那个Text,Text的属性值,例如颜色,宽高,是怎么传递到android原生的。这里先简单说一下Android原生View的绘制需要一个MeasureSpec类参数,MeasureSpec 由 SpecMode和SpecSize组成,SpecMode有三种类型:

  1. EXACTLY
    表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
  2. AT_MOST
    表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
  3. 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 设置为 truemIsAttachedToInstance = 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

更多相关文章

  1. android 版本号比较大小
  2. Android SDK Manager 更新失败的解决方法
  3. android获取屏幕大小
  4. Android三种方法设置ImageView的图片
  5. Android为每个应用程序分配的内存大小是多
  6. 全志A64 Android7.1屏蔽使用按键进入安全模式的方法
  7. Android使用AttributeSet自定义控件的方法

随机推荐

  1. Android 实际项目架构提炼开篇一
  2. android中的强指针和弱指针
  3. Android 与 H5 之间的互调
  4. 详解Android增量更新
  5. 一篇文章教你读懂UI绘制流程
  6. Looper,Handler,Message
  7. Google董事长:望Android遍布世界 智能机明
  8. Android目录结构(详解)
  9. 五步学会Android的ListView控件
  10. Android Studio:10分钟教会你做百度地图定