React-Native在android原生上的绘制流程

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有三种类型:

  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 设置为 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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值