Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android--ViewTreeObserver介绍

$
0
0

1、ViewTree

首先我们来介绍一下 ViewTree 也就是视图树是什么。

View 和 ViewGroup 是 Android UI 的基本组件, 而 ViewGroup 作为容器,可以包含一组 View, 并且 ViewGroup 其本身就是 View 的扩展。

而各种不同的 Widgets 像 TextView,Button 等等也是View的扩展,只不过是放在各种 Layout 里,比如 LinearLayout,RelativeLayout。而 Layout 却是 ViewGroup 的子类。

而 ViewTree 就是各种 View 和 ViewGroup 放在一个 Layout 里组成的树形结构。我们用 XML 编写的布局就是依照 ViewTree 结构层层叠加的。

2、ViewTreeObserver

ViewTreeObserver 是一个注册监听视图树的观察者(observer),会监听视图树发生全局变化时发出的通知。这个全局事件包括整个树的布局,从绘画过程开始,触摸模式的改变等等。

ViewTreeObserver不能够被应用程序实例化,因为它是由视图提供,通过 view.getViewTreeObserver() 方法获取。

简单的说,这是个 view 事件的观察者。要注意的是它的初始化就是调用View.getViewTreeObserver(),所以要使用 ViewTreeObserver 的方法也是使用这样来获得 ViewTreeObserver 对象再调用方法。

3、内部接口

名字 说明
OnDrawListener 当在一个视图树绘制时,所要调用的回调函数的接口类,与OnPreDrawListener不同的是,不能在这个接口中取消绘制过程
OnGlobalFocusChangeListener 当在一个视图树中的焦点状态发生改变时,所要调用的回调函数的接口类
OnGlobalLayoutListener 当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时,所要调用的回调函数的接口类
OnPreDrawListener 当一个视图树将要绘制时,所要调用的回调函数的接口类
OnScrollChangedListener 当一个视图树中的一些组件发生滚动时,所要调用的回调函数的接口类
OnTouchModeChangeListener 当一个视图树的触摸模式发生改变时,所要调用的回调函数的接口

4、公共方法

1.

/*
 * 注册一个回调函数,当在一个视图树绘制时调用这个回调函数。
 */
public void addOnDrawListener (ViewTreeObserver.OnDrawListener listener)

2.

/*
 * 注册一个回调函数,当在一个视图树中的焦点状态发生改变时调用这个回调函数。
 */
public void addOnGlobalFocusChangeListener (ViewTreeObserver.OnGlobalFocusChangeListener listener)

3.

/*
 * 注册一个回调函数,当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时调用这个回调函数。
 */
public void addOnGlobalLayoutListener (ViewTreeObserver.OnGlobalLayoutListener listener)

4.

/*
 * 注册一个回调函数,当一个视图树将要绘制时调用这个回调函数。
 */
public void addOnPreDrawListener (ViewTreeObserver.OnPreDrawListener listener)

5.

/*
 * 注册一个回调函数,当一个视图发生滚动时调用这个回调函数。
 */
public void addOnScrollChangedListener (ViewTreeObserver.OnScrollChangedListener listener)

6.

/*
 * 注册一个回调函数,当一个触摸模式发生改变时调用这个回调函数。
 */
public void addOnTouchModeChangeListener (ViewTreeObserver.OnTouchModeChangeListener listener)

7.

//告知相应监听器,视图绘制开始了
public final void dispatchOnDraw()

8.

/** 当整个布局发生改变时通知相应的注册监听器。
如果你强制对视图布局或者在一个没有附加到一个窗口的视图的层次结构或者在GONE状态下,它可以被手动的调用 **/
public final void dispatchOnGlobalLayout()

9.

/*
 * 当一个视图树将要绘制时通知相应的注册监听器。
 * 如果这个监听器返回true,则这个绘制将被取消并重新计划。
 * 如果你强制对视图布局或者在一个没有附加到一个窗口的视图的层次结构或者在一个GONE状态下,它可以被手动的调用。
 * 返回值  当前绘制能够取消并重新计划则返回true,否则返回false。
 */
public final boolean dispatchOnPreDraw ()

10.

/*
 * 指示当前的ViewTreeObserver是否可用(alive)。
 * 当observer不可用时,任何方法的调用(除了这个方法)都将抛出一个异常。
 * (异常 IllegalStateException       如果isAlive() 返回false)
 * 如果一个应用程序保持和ViewTreeObserver一个历时较长的引用,它应该总是需要在调用别的方法之前去检测这个方法的返回值。
 * 返回值 但这个对象可用则返回true,否则返回false   
 */
public boolean isAlive ()

11.

/*
 * 移除之前已经注册的全局布局回调函数。
 * 参数 victim 将要被移除的回调函数  
 */
public void removeGlobalOnLayoutListener (ViewTreeObserver.OnGlobalLayoutListener victim)

12.

/*
 * 移除之前已经注册的视图绘制回调函数。   
 */
public void removeOnDrawListener (ViewTreeObserver.OnDrawListener victim)

13.


/*
 * 移除之前已经注册的焦点改变回调函数。
 */
public void removeOnGlobalFocusChangeListener (ViewTreeObserver.OnGlobalFocusChangeListener victim)

14.


/*
 * 移除之前已经注册的预绘制回调函数。
 */
public void removeOnPreDrawListener (ViewTreeObserver.OnPreDrawListener victim)

15.

/*
 * 移除之前已经注册的滚动改变回调函数。
 */
public void removeOnScrollChangedListener (ViewTreeObserver.OnScrollChangedListener victim)

16.

/*
 * 移除之前已经注册的触摸模式改变回调函数
 */
public void removeOnTouchModeChangeListener (ViewTreeObserver.OnTouchModeChangeListener victim)

5、实例

1、获取控件宽高

public class MainActivity extends AppCompatActivity {

    private MyImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (MyImageView) findViewById(R.id.id_iv);
        mImageView.setImageResource(R.drawable.t1);

        //使用完必须撤销监听(只测量一次),否则,会一直不停的不定时的测量
        ViewTreeObserver observer = mImageView.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                final int w = mImageView.getMeasuredWidth();
                final int h = mImageView.getMeasuredHeight();

                Log.i("TAG", "width=" + w + " height=" + h);
            }
        });
    }
}

TAG: width=768 height=1110

当前的 ImageView 占据了整个屏幕,而我的虚拟机设置的分辨率是 768 * 1366 去掉标题栏正好是1100。

这种方法无法像第一种方法那样通过一个函数返回值,因为它是基于 listener 的,OnGlobalLayoutListener 的 onGlobalLayout() 被回调之前是没有值的。由于布局状态可能会发生多次改变,因此onGlobalLayout() 可能被回调多次,所以我们在第一次获得值之后就将 listener 注销掉。

2、启动帧动画

mImageView = (MyImageView) findViewById(R.id.id_iv);

final AnimationDrawable anim = (AnimationDrawable) mImageView.getBackground();

OnPreDrawListener opd=new OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        anim.start();
        return true; //注意此行返回的值
    }
};

mImageView.getViewTreeObserver().addOnPreDrawListener(opd);
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/t1"
        android:duration="1000" />
    <item
        android:drawable="@drawable/t2"
        android:duration="1000" />
    <item
        android:drawable="@drawable/t3"
        android:duration="1000" />
</animation-list>
<com.ht.motiontest.MyImageView
    android:id="@+id/id_iv"
    android:background="@drawable/anim"
    android:layout_gravity="center"
    android:layout_width="match_parent"
    android:layout_height="200dp" />

关于帧动画的使用,不了解的朋友可以看我的博客Android–各种Drawable介绍

3、监听焦点的变化

public class MainActivity extends AppCompatActivity {

    private TextView mText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mText = (TextView) findViewById(R.id.id_tv);

        OnGlobalFocusChangeListener ogl = new OnGlobalFocusChangeListener() {
            @Override
            public void onGlobalFocusChanged(View oldFocus, View newFocus) {

                if (oldFocus != null && newFocus != null) {
                    mText.setText("Focus \nFROM:\t" + oldFocus.toString() + "\n    TO:\t" + newFocus.toString());
                }

            }
        };

        mText.getViewTreeObserver().addOnGlobalFocusChangeListener(ogl);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.ht.motiontest.MainActivity">

    <EditText
        android:id="@+id/id_et"
        android:text="hello world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <EditText
        android:text="hi"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:text=""
        android:id="@+id/id_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

onGlobalFocusChanged() 是接口 OnGlobalFocusChangeListener 中定义的方法。当焦点发生变化时,会触发这个方法的执行。需要注意的是,布局中的任何一个view添加了这个监听,该布局的任意控件的焦点变化都能够监听的到。

所以可以看到当我们为 TextView 添加 listener 后,无论是第一个还是第二个 EditView,焦点一变化 TextView 的文字就会改变,说明被监听器监听到了。

结束语:本文仅用来学习记录,参考查阅。

作者:HardWorkingAnt 发表于2017/8/19 13:27:36 原文链接
阅读:65 评论:1 查看评论

Android--获取View的宽高的几种方法

$
0
0

1、getHeight()无效

我们先来看看在 onCreate() 中用控件的 getHeight() 和 getWidth() 方法会出现什么情况。

public class MainActivity extends AppCompatActivity {

    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (ImageView) findViewById(R.id.id_iv);
        Log.i("TAG", "height=" + mImageView.getHeight() +
                "  width=" + mImageView.getWidth());
    }
}

TAG:height=0 width=0

在 onCreate() 中获取到的控件宽高为0,这是为什么呢?我们来重写下控件的 onMeasure(),看看控件的绘制测量与 Activity 的 onCreate() 的先后关系:

public class MyImageView extends AppCompatImageView {

    public MyImageView(Context context) {
        this(context, null);
    }

    public MyImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i("TAG", "onMeasure()被调用了" + System.currentTimeMillis());
    }
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.i("TAG", "onCreate()执行完毕" + System.currentTimeMillis());
}

TAG: height=0 width=0
TAG: onCreate()执行完毕 1502893100701
TAG: onMeasure()被调用了1502893100781
TAG: onMeasure()被调用了1502893101006
TAG: onMeasure()被调用了1502893101066

由此可见,onCreate() 执行完了,我们定义的控件才会被测量,所以我们在 onCreate() 里面通过 view.getHeight() 获取控件的高度肯定是0,因为它还没有被测量,也就是说它自己都不知道自己有多高,而我们这时候去获取它的尺寸,肯定是不行的。

2、调用View的measure方法

从上面的分析我们可以得出,之所以无法测量,是因为控件是在 onCreate() 执行完后才被测量的,所以我们要想得到结果可以主动调用方法去测量它。

public class MainActivity extends AppCompatActivity {

    private MyImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (MyImageView) findViewById(R.id.id_iv);
        mImageView.setImageResource(R.drawable.t3);
        Log.i("TAG", "width=" + getViewWidth(mImageView));
    }

    public int getViewWidth(ImageView view) {
        view.measure(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        return view.getMeasuredWidth();
    }
}

TAG: width=2048

我们在 onCreate() 中要用到控件的宽高的时候可以主动去为它测量,也就是直接调用一个 view 或者 viewgroup 的 measure() 去测量。

测量之后该 view 的 getMeasuredHeight() 就会返回刚才测量所得的高,getMeasuredWidth() 返回测量所得宽。

本来在布局加载的过程中,view的 measure方法一定会被系统调用(在onResume() 中已经调用了 measure方法),但这发生在我们所不知道的某个时间点,为了在这之前提前得到测量结果,我们主动调用 measure方法,但是这样做的好处是可以立即获得宽和高,坏处是多了一次测量过程。

该方法测量的宽度和高度可能与视图绘制完成后的真实的宽度和高度不一致。

3、OnGlobalLayoutListener获取

另一种方法是利用 ViewTreeObserver 的 OnGlobalLayoutListener 来测量。关于 ViewTreeObserver 我在我的博客Android–ViewTreeObserver介绍有详细介绍,不了解的朋友可以看看。

在布局发生改变或者某个视图的可视状态发生改变时调用该事件,会被多次调用,因此需要在获取到视图的宽度和高度后执行 remove 方法移除该监听事件。

ViewTreeObserver observer = mImageView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        mImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        final int w = mImageView.getMeasuredWidth();
        final int h = mImageView.getMeasuredHeight();
    }
});

4、OnPreDrawListener获取

在视图将要绘制时调用该监听事件,会被调用多次,因此获取到视图的宽度和高度后要移除该监听事件。这同样是 ViewTreeObserver 的接口。

mText.getViewTreeObserver().addOnPreDrawListener(
        new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {

                mText.getViewTreeObserver().removeOnPreDrawListener(this);
                int w = mText.getWidth();
                int h = mText.getHeight();

                return true;
            }
        }
);

5、OnLayoutChangeListener获取

在视图的 layout 改变时调用该事件,会被多次调用,因此需要在获取到视图的宽度和高度后执行 remove 方法移除该监听事件。

mText.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom,
                int oldLeft, int oldTop, int oldRight, int oldBottom) {

        mText.removeOnLayoutChangeListener(this);
        int w = mText.getWidth();
        int h = mText.getHeight();
    }
});

6、重写View的onSizeChanged()

在视图的大小发生改变时调用该方法,会被多次调用,因此获取到宽度和高度后需要考虑禁用掉代码。

该实现方法需要继承 View,且多次被调用,不建议使用。

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    Log.i("TAG", "width = " + getWidth() + "height = " + getHeight());
}

7、重写View的onLayout()

该方法会被多次调用,获取到宽度和高度后需要考虑禁用掉代码。
该实现方法需要继承 View,且多次被调用,不建议使用。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);

    Log.i("TAG", "width = " + getWidth() + "height = " + getHeight());

}

8、使用View.post()方法

Runnable 对象中的方法会在 View 的 measure()、layout() 等事件完成后触发。

UI 事件队列会按顺序处理事件,在 setContentView() 被调用后,事件队列中会包含一个要求重新 layout 的 message,所以任何 post 到队列中的 Runnable 对象都会在 Layout 发生变化后执行。

该方法只会执行一次,且逻辑简单,建议使用。

mImageView.post(new Runnable() {
    @Override
    public void run() {

        Log.i("TAG", "width = " + mImageView.getWidth() +
              "height = " + mImageView.getHeight());

    }
});

以上就是各种获取 View 的宽高的方法,充分了解它们适用的场合,我们去做布局会更加简单、轻松。

结束语:本文仅用来学习记录,参考查阅。

作者:HardWorkingAnt 发表于2017/8/19 14:12:27 原文链接
阅读:57 评论:0 查看评论

Unity 按指定路线移动

$
0
0

按照路线指定各个拐角处的点,注意使用空对象这样不会再Game显示点如下图,,如果想做弯道那就多做几个点就可以了,,大家注意此时的z轴的值是没有变化的,要保证点和物体是同一个z轴的值



上图代码:
public class Path : MonoBehaviour {

    public GameObject[] gos; //获取每个目标点,,注意数组顺序不能乱
    public float speed = 1;  //用于控制移动速度
    int i = 0;             //用于记录是第几个目标点
    float des;             //用于存储与目标点的距离     
	// Use this for initialization
	void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
        
        //看向目标点
        this.transform.LookAt(gos[i].transform);
        //计算与目标点间的距离
        des = Vector3.Distance(this.transform.position, gos[i].transform.position);
        //移向目标
        transform.position = Vector3.MoveTowards(this.transform.position, gos[i].transform.position, Time.deltaTime* speed);
        //如果移动到当前目标点,就移动向下个目标
        if (des < 0.1f && i < 9)
        {
            i++;
        }
       
    }
}



作者:Czhenya 发表于2017/8/19 14:33:29 原文链接
阅读:54 评论:0 查看评论

iOS 工程自动化 - 思路整理

$
0
0

4 月份参加 2017@Swift 大会的时候有幸听到了 @zesming 大佬关于美团组件化的 Topic,有一张图印象特别深刻。

来自 @zesming 大佬

后来跟 @zesming 大佬沟通怎么去整理组件自动构建发布思路的时候他也跟我提到了这张图。所以我准备围绕这张图来整理一下 iOS 工程自动化的思路。

基础知识

首先,我们需要掌握一些自动构建发布的基础知识,主要包含如下几个方面。

GitFlow - 规范 git 操作流程

GitFlow 是由 Vincent Driessen 提出的一个 Git 操作流程标准。包含如下几个关键分支:

名称 说明
master 主分支
develop 主开发分支,包含确定即将发布的代码
feature 新功能分支,一般一个新功能对应一个分支,对于功能的拆分需要比较合理,以避免一些后面不必要的代码冲突
release 发布分支,发布时候用的分支,一般测试时候发现的 bug 在这个分支进行修复
hotfix 热修复分支,紧急修 bug 的时候用

GitFlow 的优势有如下几点:

  • 并行开发:GitFlow 可以很方便的实现并行开发:每个新功能都会建立一个新的 feature 分支,从而和已经完成的功能隔离开来,而且只有在新功能完成开发的情况下,其对应的 feature 分支才会合并到主开发分支上(也就是我们经常说的 develop 分支)。另外,如果你正在开发某个功能,同时又有一个新的功能需要开发,你只需要提交当前 feature 的代码,然后创建另外一个 feature 分支并完成新功能开发。然后再切回之前的 feature 分支即可继续完成之前功能的开发。
  • 协作开发:GitFlow 还支持多人协同开发,因为每个 feature 分支上改动的代码都只是为了让某个新的 feature 可以独立运行。同时我们也很容易知道每个人都在干啥。
  • 发布阶段:当一个新 feature 开发完成的时候,它会被合并到 develop 分支,这个分支主要用来暂时保存那些还没有发布的内容,所以如果需要再开发新的 feature,我们只需要从 develop 分支创建新分支,即可包含所有已经完成的 feature
  • 支持紧急修复:GitFlow 还包含了 hotfix 分支。这种类型的分支是从某个已经发布的 tag 上创建出来并做一个紧急的修复,而且这个紧急修复只影响这个已经发布的 tag,而不会影响到你正在开发的新 feature

Glow flow 是如何工作的

新功能都是在 feature 分支上进行开发

feature 分支都是从 develop 分支创建,完成后再合并到 develop 分支上,等待发布。

当需要发布时,我们从 develop 分支创建一个 release 分支

然后这个 release 分支会发布到测试环境进行测试,如果发现问题就在这个分支直接进行修复。在所有问题修复之前,我们会不停的重复发布->测试->修复->重新发布->重新测试这个流程。

发布结束后,这个 release 分支会合并到 developmaster 分支,从而保证不会有代码丢失。

master 分支只跟踪已经发布的代码,合并到 master 上的 commit 只能来自 release 分支和 hotfix 分支。

hotfix 分支的作用是紧急修复一些 Bug。

它们都是从 master 分支上的某个 tag 建立,修复结束后再合并到 developmaster 分支上。

GitFlow 工具

如果要在项目中引入 GitFlow,推荐使用 SourceTree 来做客户端工具,它包含了所有 GitFlow 的流程,可视化操作,很方便。

gitignore - 干掉那些干扰代码

为什么提到 gitignore,审过 PR(Pull request) 或者 MR(Merge request)的同学应该深有体会,当一个带着一大堆 Pods 库代码的 PR/MR 袭来的时候,你的内心应该是绝望的……所以我们要了解下怎么把一些非模块相关的代码 ignore 掉。

  创建项目仓库中的 .gitignore

如果你在项目仓库内创建一个 .gitignore 文件,Git 会根据这个文件来决定哪些文件和目录是需要忽略的。

注意:如果你想把一个已经被跟踪的文件 ignore 掉,这是时候新增的规则并不会对这个文件产生作用,你需要先用下面的指令把这个文件设置为不跟踪:

git rm --cached FILENAME

创建全局的 .gitignore

这个其实没啥意思,不过还是看一下怎么设置吧:

git config --global core.excludesfile ~/.gitignore_global

iOSer 需要的 .gitignore

Github 官方模版给出的建议如下:

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

通过这个文件我们可以看到,忽略的内容主要包含下面几种:

  • 构建产生的目录和文件;
  • 各种临时配置文件;
  • Obj-C / Swift 相关的特定文件;
  • CocoaPods 第三方 Pods 库目录;
  • Carthage 构建目录;
  • fastlane 构建产生的文件;
  • 代码注入工具产生的目录及文件。

我们可以根据自身项目的情况来对这些配置进行相应的调整和定制。

githooks - 背后的“男人”

注:本段主要翻译自 githooks 官方文档。

githooks 简介

githooks 是指 git 在执行某些特定操作时会触发的一系列程序,有点类似数据库中的触发器,由用户编写。默认情况下,这些程序都被放置在 $GIT_DIR/hooks 目录下,当然,我们也可以通过 git 的环境配置变量 core.hooksPath 来改变这个目录。

githooks 的类型有很多中,这些 hook 会在各个特定操作时帮你自动完成一些你想要做的操作,比如:在提交代码到 develop 分支的时候自动打包。

目前支持的 githooks

git commit 流程

pre-commit

git commit 触发,可以通过 --no-verify 选项来跳过,这个 hook 不需要参数,在得到提交消息并开始提交(commit)前被调用,如果返回非 0,则会导致 git commit 失败。

当默认的 pre-commit 钩子被启用时,如果它发现文件尾部有空白行,此次提交就会被终止。

如果进行 git commit 的命令没有指定一个编辑器来修改提交信息(commit message),任何的 git commit hook 被调用时都会带上环境变量 GIT_EDITOR=:

prepare-commit-msg

执行 git commit 命令后,在默认提交消息准备好后但编辑器启动前,这个 hook 就会被调用。

它接受 1 到 3 个参数。第 1 个是包含了提交消息的文件的名字。第 2 个是提交消息的来源,它可以是:

  • message(如果指定了 -m 或者 -F 选项)
  • template(如果指定了 -t 选项,或者在 git config中开启了 commit.template 选项)
  • merge(如果本次提交是一次合并,或者存在文件 .git/MERGE_MSG
  • squash(如果存在文件 .git/SQUASH_MSG
  • commit 并且第 3 个参数是一个提交的 SHA1 值(如果指定了 -c,-C 或者 --amend 选项)

如果返回值不是 0,那么 git commit 命令就会被中止。

这个 hook 的目的是用来在工作时编辑信息文件,并且不会被 --no-verify 选项跳过。一个非 0 值意味着 hook 工作失败,会终止提交。它不应该用来作为 pre-commit 钩子的替代。

commit-msg

git commit 触发,可以通过 --no-verify 选项来跳过,接受 1 个参数,这个参数包含了提交消息的文件的名字,如果返回非 0,则会中止 git commit 命令。

这个 hook 可以用来规范提交信息,比如把信息格式化成项目定制的标准格式,或者发现提交信息不符合格式时拒绝这次提交。

post-commit

git commit 触发,在提交后被调用,不能影响 git commit 的结果。

git push 流程

pre-push

git push 运行期间,更新了远程引用但尚未传送对象时执行。如果返回非 0,将中止推送过程。它接受远程分支的名字和位置作为参数,同时从标准输入中读取一系列待更新的引用。 你可以在推送开始之前,用它验证对引用的更新操作。

pre-receive

服务端处理来自客户端的推送操作时,最先被调用的 hook。它从标准输入获取一系列被推送的引用。如果它以非 0 值退出,所有的推送内容都不会被接受。你可以用这个 hook 阻止对 non-fast-forward 的更新,或者对该推送所修改的所有引用和文件进行访问控制。

update

pre-receive hook 十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。它不会从标准输入读取内容,而是接受三个参数:引用的分支名;推送前引用所指向内容的 SHA-1 值;以及用户准备推送内容的 SHA-1 值。如果这个 hook 以非 0 值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新。

post-receive

在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。它接受与 pre-receive 相同的标准输入数据。它的用途包括给某个邮件列表发信,通知持续集成服务器,或者更新缺陷追踪系统 —— 甚至可以通过分析提交信息来决定某个问题是否应该被开启、修改或者关闭。该脚本无法中止 push 进程,不过客户端在它结束运行之前将保持连接状态,所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。

post-update

由远程仓库的 git-receive-pack 调用,也是就是本地仓库完成 git push 时。这个指令只能用来做通知,不能改变 git-receive-pack 的结果。

push-to-checkout

由远程仓库的 git-receive-pack 调用,也是就是本地仓库完成 git push 时。如果这个 hook 返回非 0 值,则会中断 git push 操作。

applypatch-msg & pre-applypatch & post-applypatch

git am 触发,主要用于引入第三方 patch 的时候用。因为项目中暂时没有这样的使用场景,所以不做具体研究。

pre-rebase

git rebase 触发,可以用来防止某个分支被 rebase。这个 hook 接受 1 到 2 个参数。第 1 个参数是上游分支,第 2 个参数是将要执行 rebase 的分支。

post-checkout

git checkout 成功运行后执行。你可以根据你的项目环境用它调整你的工作目录。包括放入大的二进制文件、自动生成文档或进行其他类似这样的操作。

post-merge

git merge 成功运行后执行。你可以用它恢复 Git 无法跟踪的工作区数据,比如权限数据。这个 hook 也可以用来验证某些在 Git 控制之外的文件是否存在,这样你就能在工作区改变时,把这些文件复制进来。

post-rewrite

这个 hook 被那些会替换提交记录的命令调用,比如 git commit --amendgit rebase(不过不包括 git filter-branch)。 它唯一的参数是触发重写的命令名,同时从标准输入中接受一系列重写的提交记录。 这个 hook 的用途很大程度上跟 post-checkoutpost-merge 差不多。

sendemail-validate

这个 hook 由 git send-email 触发,它接受一个参数:包含 e-mail 接受者邮箱的文件名。如果这个 hook 返回非 0 值,git send-email 就会被中止。

pre-auto-gc

Git 的一些日常操作在运行时,偶尔会调用 git gc --auto 进行垃圾回收。这个 hook 会在垃圾回收开始之前被调用,可以用它来提醒你现在要回收垃圾了,或者依情形判断是否要中断回收。

Cocoapods - 大管家

做为一个第三方库依赖管理工具,Cocoapods 在模块化开发中扮演了非常核心的角色。关于 Cocoapods 的功能和介绍这里就不一一陈述了,主要看一下和模块化开发过程中相关的一些东西。

自定义 Cocoapods 模板

我们一般用 pod lib create 这个指令来创建一个模块,其实这个指令还有一个选项:--template-url=URL,用来指定生成库的模板,官方模板地址是:https://github.com/CocoaPods/pod-template,根据这个模板生成的工程目录结构如下:

MyLib
  ├── .travis.yml
  ├── _Pods.xcproject
  ├── Example
  │   ├── MyLib
  │   ├── MyLib.xcodeproj
  │   ├── MyLib.xcworkspace
  │   ├── Podfile
  │   ├── Podfile.lock
  │   ├── Pods
  │   └── Tests
  ├── LICENSE
  ├── MyLib.podspec
  ├── Pod
  │   ├── Assets
  │   └── Classes
  │     └── RemoveMe.[swift/m]
  └── README.md

也就是说,我们可以 fork 这份官方模版地址,然后定制我们自己的模板(包括主工程和业务模块),增加自定义的功能。比如:

  • 添加所有基础库依赖。
  • 添加私有 Cocoapods 仓库。
  • 添加 .gitignore 文件。
  • 添加自定义脚本。

私有 Cocoapods 仓库

和主工程一样,模块也是需要做版本管理的,目前来看比较好的方式就是通过 Cocoapods 来发布版本。所以我们需要准备一个自己的私有 Cocoapods 仓库,其实很简单,首先在内网 git 上建立一个空仓库,然后在本地执行一下下面的指令即可:

pod repo add [Spec name] [Git url]

然后就是在发布库的时候注意一下,用如下指令即可发布到私有仓库内:

pod repo push [Spec name] [Lib name].podspec

Cocoapods 引用第三方库的几种方式

使用过 Cocoapods 的童鞋应该都知道,Cocoapods 的引用方式有三种:

方式 例子 说明
版本号引用 pod ‘Alamofire’, ‘~> 3.0’ 这种方式引用的是已经发布的版本,包含了 >``>=``<``<=``~> 几种版本限制符号,其中~>符号代表只更新最新的小版本号,比如 ~> 1.0.0 则只会更新到 1.0.x 的最新版本,而不会更新 1.x.0 以上的版本
本地路径引用 pod ‘Alamofire’, :path => ‘~/Documents/Alamofire’ 这种方式直接引用本地的代码,这种方式下对引用库的修改仍然会提交到引用库的 git 上,而不会提交到主工程。
远程 git 路径引用 pod ‘Alamofire’, :git => ‘https://github.com/Alamofire/Alamofire.git 这种方式直接引用远程 git 代码,不需要引用的库进行发布,而且还支持 :branch =>:tag =>:commit => 三种选项

流程分析

下面就是我对 @zesming 大佬分享的流程图中各个过程的一些思考。

业务方需求到开发

根据 GitFlow 的规范,新的需求走的是 feature 的流程。这里我们应该可以开发一些辅助开发的脚本。

一键新建业务模块和主工程 feature 分支

组件化到了一个完整阶段的时候,主工程应该是没有代码的,只是一个壳。但是在发展阶段的时候,主工程还会包含一些业务代码,所以我们在开发某个 feature 的时候,往往是模块内有一些具体业务代码,主工程还有一些调用代码,这个时候就需要在主工程和业务模块都新建同样的 feature 分支,所以我们在主工程增加一个脚本。这个脚本的参数包含:

  • 业务模块名;
  • 业务模块本地路径;
  • feature 分支名。

执行过程如下:

  1. 为主工程创建 feature 分支;
  2. 进入业务模块所在目录,为业务模块创建 feature 分支;
  3. 设置主工程 Podfile 中业务模块引用方式为本地路径引用;
  4. 打开主工程和业务模块 Example 工程。

一键切换主工程开发调试状态

一键切换主工程开发状态是指某些业务模块需要依赖主工程来进行调试的时候(PS:当然,比较理想的状态是业务模块可以独立运行,不过一般情况下,理想很美好,现实很残酷��),需要将 Podfile 中这个模块的引用方式修改为本地路径的引用方式。这样主工程代码的修改和业务模块代码的修改会分别提交到各自的 git 仓库,从而实现边调试边开发边提交代码。

这个功能考虑用脚本的方式来实现,放置在主工程的常用脚本目录下。参数应该包含:

  • 业务模块名;
  • 业务模块本地路径;
  • 业务模块 feature 分支名 [可选]。

执行的操作应该包含:

  1. 如果指定了业务模块 feature 分支名,则需要先给业务模块新建分支,否则直接执行第 2 步;
  2. 修改主工程 Podfile 中业务模块的引用方式为本地路径引用;
  3. pod install
  4. 关闭主工程并重新打开。

一键切换主工程提交状态

业务模块开发调试完成之后,需要将主工程恢复到正常的状态并提交。

这个功能还是用脚本的方式实现,放置在主工程的常用脚本目录下。参数应该包含:

  • 业务模块名;
  • 业务模块远程 git 地址;
  • 业务模块 feature 分支名。

执行的操作应该包含:

  1. 根据当前业务模块本地路径,提交业务模块代码;
  2. 修改主工程 Podfile 中业务模块的引用方式为远程 git 引用;
  3. pod install
  4. 关闭主工程并重新打开。

提交代码到业务代码仓库

这个过程我们主要通过 githooks 来做一些一些自动检测。包括:

  • OCLint
  • 单元测试

发布组件到内网 Pods

准备阶段

这个阶段做的事情主要是检测当前需要发布的所有分支是否都已经提交 PR / MR 并合并到了 develop 分支。

注:这里对分支的命名会有一些规则上的要求比。如在分支名内需要带上当前需要发布的版本号,从而可以通过这个版本号匹配到当前需要发布的所有分支。

发布 -> 测试 -> bug fix -> 再次发布阶段

这里需要把 GitFlow 和 Cocoapods 的发布流程结合起来。

考虑到一般需要依赖主工程来进行测试,我们需要在主工程增加一个本地脚本来辅助发布,包含的参数如下:

  • 当前版本号;
  • 业务模块本地路径;
  • 主工程 feature 分支名[可选]。

实现的功能如下:

  1. 如果提供了主工程 feature 分支名,需要先切换主工程分支;否则跳过这一步;
  2. 根据传入的当前版本号从 develop 分支建立 release 分支。如果已经存在,则跳过这一步;
  3. 将主工程 git 仓库中 Podfile 引用该模块的方式替换为引用远程 git 仓库的 release 分支;
  4. 然后在主工程执行 pod update [模块名] 更新代码;
  5. 推送主工程代码到远程 git 仓库。
  6. 通过 githooks 的方式打包主工程并提交测试;
  7. 测试过程中如果存在问题,则通过一键切换主工程开发调试状态脚本直接切换开发状态并进行 bug fix;
  8. bug fix 之后重新执行第 1 步。

测试完成发布到内网 Pods 阶段

测试完成后就可以发布业务模块到内网 Pods 了。这里我们在业务模块工程内准备一个脚本,参数如下:

  • 当前版本号。
  • 主工程本地路径。

实现的功能如下:

  1. 首先我们要修改 .podspec 文件中的版本号为传入的当前版本号,并提交 push 到 release 分支。
  2. 然后根据 GitFlow 的 release 流程,合并 release 分支到 develop 分支和 master 分支,然后在 master 分支建立对应版本号的 tag 并 push 到远程 git 仓库。
  3. 然后就可以发布业务模块了,发布完成之后切换到主工程路径将主工程 Podfile 中对该模块的引用方式修改为版本号引用。

集成组件到主工程

因为之前发布组件的时候已经将主工程对各业务模块的引用方式修改为版本号引用。所以这个阶段我们只需要验证一下当前主工程引用的是否是各业务模块的最新发布版本即可。参数如下:

  • 主工程依赖的所有业务模块列表

完成的功能如下:

检测主工程依赖模块的所有业务模块的最新版本是否和 Podfile 中指定的一致,如果不一致,报错。

发布主工程

准备阶段

这个阶段做的事情有两点:

  1. 检测主工程当前需要发布的所有分支是否都已经提交 PR / MR 并合并到了 develop 分支。
  2. 检测主工程当前引用的所有业务模块是否都为版本号引用。

发布 -> 测试 -> bug fix -> 再次发布阶段

这个阶段可以理解为集成测试阶段。同样是主工程中的一个脚本。参数如下:

  • 当前版本号。

    1. 根据传入的当前版本号从 develop 分支建立 release 分支。如果已经存在,则跳过这一步;
    2. 推送主工程代码到远程 git 仓库;
    3. 通过 githooks 的方式打包主工程并提交测试;
    4. 测试过程中如果存在问题,则通过一键切换主工程开发调试状态脚本直接切换开发状态并进行 bug fix;
    5. bug fix 之后重新执行第 1 步。

测试完成发布

  1. 根据 GitFlow 的 release 流程,合并 release 分支到 develop 分支和 master 分支,然后在 master 分支建立对应版本号的 tag 并 push 到远程 git 仓库;
  2. 通过 githooks 的方式打包主工程并发布。

总结

本文只是一个不成熟的思考,后续实施的过程中可能会对一些细节进行改善,也欢迎大家对我思路中不合理的地方进行指正,我会非常感激。

下一篇的内容应该是业务方需求到开发这个过程中的一些具体的实践记录。敬请期待!

参考资料

Introducing GitFlow:https://datasift.github.io/gitflow/IntroducingGitFlow.html

Using Git / Ignoring files:
https://help.github.com/articles/ignoring-files/

githooks:https://git-scm.com/docs/githooks

自定义 Git - Git 钩子:https://git-scm.com/book/zh/v2/%E8%87%AA%E5%AE%9A%E4%B9%89-Git-Git-%E9%92%A9%E5%AD%90

作者:mmoaay 发表于2017/8/20 16:58:12 原文链接
阅读:0 评论:0 查看评论

谈谈 sizeToFit 与 sizeThatFit

$
0
0

先看看苹果官方文档对这连个的方法的解释:

- (CGSize)sizeThatFits:(CGSize)size;   

return 'best' size to fit given size. does not actually resize view. Default is return existing view size

- (void)sizeToFit;     

calls sizeThatFits: with current view bounds and changes bounds size.

意思是说,sizeThatFits: 会计算出最优的 size 但是不会改变 自己的 size,而 sizeToFit: 会计算出最优的 size 而且会改变自己的 size。那么两者的联系是什么呢?

实际上,当调用 sizeToFit 后会调用 sizeThatFits 方法来计算 UIView 的 bounds.size 然后改变 frame.size。也就是说,其实我们也可以不使用 [ label sizeToFit] 来计算 label 内容的 size ,首先调用 sizeThatFits 方法或者一个 CGSize 然后改变 label.frame.size 就可以得到 [label sizeToFit]一样的效果。下面用实例加以说明:

UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 100, 20)];
    testLabel.text = @"欢迎关注黄飞的csdn博客";
    testLabel.font = [UIFont systemFontOfSize:14];
    testLabel.textAlignment=NSTextAlignmentCenter;
    //使用sizeThatFit计算lable大小
    CGSize sizeThatFit=[testLabel sizeThatFits:CGSizeZero];
    NSLog(@"%f-----%f", sizeThatFit.width, sizeThatFit.height);
    NSLog(@"%f-----%f", testLabel.frame.size.width, testLabel.frame.size.height);
    // 调用sizeToFit
    [testLabel sizeToFit];
    NSLog(@"%f-----%f", testLabel.frame.size.width, testLabel.frame.size.height);
    testLabel.textColor=[UIColor blackColor];
    testLabel.backgroundColor=[UIColor yellowColor];
    [self.view addSubview:testLabel];

运行结果如下所示:
这里写图片描述

这也验证了官方的对其的解释:sizeThatFits: 会计算出最优的 size 但是不会改变 自己的 size,而 sizeToFit: 会计算出最优的 size 而且会改变自己的 size。

上述说的是 Label 单行显示时的情形,当 Label 文本较长以至于不能单行显示时,两者也是有区别的。

sizeThatFit:

UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 50, 20)];
    testLabel.text = @"欢迎关注黄飞的csdn博客,这里有你想要的东西,欢迎关注!!";
    testLabel.font = [UIFont systemFontOfSize:14];
    testLabel.textAlignment=NSTextAlignmentCenter;
    testLabel.numberOfLines = 0;
    //使用sizeThatFit计算lable大小
    CGSize sizeThatFit = [testLabel sizeThatFits:CGSizeZero];
    testLabel.frame    = CGRectMake(testLabel.frame.origin.x, testLabel.frame.origin.y, sizeThatFit.width, sizeThatFit.height);

    testLabel.textColor=[UIColor blackColor];
    testLabel.backgroundColor=[UIColor yellowColor];
    [self.view addSubview:testLabel];

sizeToFit:

UILabel *testLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 100, 20)];
    testLabel.text = @"欢迎关注黄飞的csdn博客,这里有你想要的东西,欢迎关注!!";
    testLabel.font = [UIFont systemFontOfSize:14];
    testLabel.textAlignment=NSTextAlignmentCenter;
    testLabel.numberOfLines = 0;

    [testLabel sizeToFit];

    testLabel.textColor=[UIColor blackColor];
    testLabel.backgroundColor=[UIColor yellowColor];
    [self.view addSubview:testLabel];

两者的效果对比如下:
这里写图片描述

当设置多行显示时,两者又体现出区别,sizeThatFits并不会折行显示,sizeToFits会在设置的宽度内这行显示。这实际上又从另一方面验证了官方的解释。

知其然,更要知其所以然,学习没有捷径,坚持和专研是硬道理,多分享则乐趣无穷!欢迎关注后续博文!

作者:huangfei711 发表于2017/8/20 23:10:45 原文链接
阅读:70 评论:0 查看评论

FlexboxLayout

$
0
0

1. 简介

Flexbox-layout,是Google推出的流式布局。具体的基本介绍可以参考Github上的Wiki。看一下类的英文说明:

/**
* A layout that arranges its children in a way its attributes can be specified like the
* CSS Flexible Box Layout Module.
* This class extends the {@link ViewGroup} like other layout classes such as {@link LinearLayout}
* or {@link RelativeLayout}, the attributes can be specified from a layout XML or from code.
*
* The supported attributes that you can use are:
* <ul>
* <li>{@code flexDirection}</li>
* <li>{@code flexWrap}</li>
* <li>{@code justifyContent}</li>
* <li>{@code alignItems}</li>
* <li>{@code alignContent}</li>
* <li>{@code showDivider}</li>
* <li>{@code showDividerHorizontal}</li>
* <li>{@code showDividerVertical}</li>
* <li>{@code dividerDrawable}</li>
* <li>{@code dividerDrawableHorizontal}</li>
* <li>{@code dividerDrawableVertical}</li>
* </ul>
* for the FlexboxLayout.
*
* And for the children of the FlexboxLayout, you can use:
* <ul>
* <li>{@code layout_order}</li>
* <li>{@code layout_flexGrow}</li>
* <li>{@code layout_flexShrink}</li>
* <li>{@code layout_flexBasisPercent}</li>
* <li>{@code layout_alignSelf}</li>
* <li>{@code layout_minWidth}</li>
* <li>{@code layout_minHeight}</li>
* <li>{@code layout_maxWidth}</li>
* <li>{@code layout_maxHeight}</li>
* <li>{@code layout_wrapBefore}</li>
* </ul>
*/

应该很容易看懂,主要是提供类似于CSS中Flexible Box Layout的能力。


2. 属性值

针对FlexboxLayout

属性
flexDirection
flexWrap
justifyContent
alignItems
alignContent
showDivider
showDividerHorizontal
showDividerVertical
dividerDrawable
dividerDrawableHorizontal
dividerDrawableVertical


针对FlexboxLayout中的子元素

属性
layout_order
layout_flexGrow
layout_flexShrink
layout_flexBasisPercent
layout_alignSelf
layout_minWidth
layout_minHeight
layout_maxWidth
layout_maxHeight
layout_wrapBefore

3. FlexboxLayout属性自测

3.1 flexDirection

flexDirection属性支持row , row_reverse , column , column_reverse 四个取值。分别代表左右,右左,上下,下上。

flexDIrection 效果图
row 这里写图片描述
row_reverse 这里写图片描述
column 这里写图片描述
column_reverse 这里写图片描述

3.2 flexWrap

flexWrap, 表示换行与否,支持wrap , nowrap , wrap_reverse。默认为noWrap,表示不换行,wrap表示自动换行,还有一个wrap_reverse 表示副轴反转。
以flexDirection为row时为例

flexWrap
wrap 这里写图片描述
nowrap 这里写图片描述
wrap_reverse 这里写图片描述

3.3 justifyContent

justifyContent,表示控件沿主轴对齐方向。支持flex_start , flex_end , center , space_between , space_around五种取值。

下面以flexDirection为row , flexWrap为wrap为例,注意比较效果之间的差异,就可以很直观的了解到每个属性值的作用。

justifyContent 效果图
flex_start 这里写图片描述
flex_end 这里写图片描述
center 这里写图片描述
space_between 这里写图片描述
space_around 这里写图片描述

3.4 alignItems

alignItems , 描述元素在副轴上的对齐方向(针对单行) , 支持flex_start , flex_end , center , baseline , stretch五种取值。

以flexDirection为row , flexWrap为nowrap为例,注意比较不同属性之间的效果差异。由于上面示例中我们针对flexboxlayout的高度使用的时wrap_content , 下面这个示例中我们给flexboxlayout一个合适的固定高度活着改成match_parent更容易看出效果

alignItems 效果图
flex_start 这里写图片描述
flex_end 这里写图片描述
center 这里写图片描述
baseline 这里写图片描述
stretch 这里写图片描述

3.5 alignContent

alignContent , 表示控件在副轴上的对齐方向(针对多行元素)。支持flex_start , flex_end , center , space_between , space_around , stretch六种取值

flexboxlayout改成match_parent , 以flexDirection为row , flexWrap为wrap为例, 看看效果

alignContent 效果图
flex_start 这里写图片描述
flex_end 这里写图片描述
center 这里写图片描述
space_between 这里写图片描述
space_around 这里写图片描述
stretch 这里写图片描述

3.6 showDivider and dividerDrawable

showDivider , 显示分割线。支持none , beginning , middle , end 四种取值

绘制一条分割线,然后flexDirection为row , flexWrap为wrap,dividerDrawable为@drawable/divider为例,看看效果

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/colorAccent"></solid>
    <size android:width="2dp"/>
</shape>
showDivider 效果图
none 这里写图片描述
beginning 这里写图片描述
middle 这里写图片描述
end 这里写图片描述

3.7 showDividerHorizontal and dividerDrawableHorizontal

横向分割线,支持none , beginning , middle , end 四种取值,四种取值和上面的使用差不多,我们只举一个例子

分割线

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/colorAccent"></solid>
    <size  android:height="2dp"/>
</shape>

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.flexbox.FlexboxLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#EBEBEB"
        app:flexDirection="column"
        app:showDividerHorizontal="middle"
        app:dividerDrawableHorizontal="@drawable/divider"
        app:flexWrap="wrap">

        <TextView
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:text="Android" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="android studio" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="Android安全" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="Android框架" />
    </com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

这里写图片描述

3.8 showDividerVertical and dividerDrawableVertical

和上面的用法相似,看下效果把

分割线

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/colorAccent"></solid>
    <size  android:width="2dp"/>
</shape>

这里写图片描述


4. FlexboxLayout子元素属性自测

4.1 layout_order

默认情况

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.flexbox.FlexboxLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#EBEBEB"
        app:flexDirection="row"
        app:flexWrap="wrap">

        <TextView
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:gravity="center"
            android:text="Android" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="android studio" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="Android安全" />
    </com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

这里写图片描述

顺序排一下 2,3,1

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.flexbox.FlexboxLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#EBEBEB"
        app:flexDirection="row"
        app:flexWrap="wrap">

        <TextView
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            app:layout_order="2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:gravity="center"
            android:text="Android" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            app:layout_order="3"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="android studio" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            app:layout_order="1"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="Android安全" />
     </com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

这里写图片描述

4.2 layout_flexGrow

layout_flexGrow ,子元素的放大比例, 决定如何分配剩余空间(如果存在剩余空间的话),默认值为0,不会分配剩余空间,如果某一行中有一个item的 layout_flexGrow 是一个正值,那么会将这行全部剩余空间分配给这个Item , 如果这一行有多个Item这个属性都为正值,那么剩余空间的按照layout_flexGrow定义的比例(有点像LinearLayout的layout_weight属性)进行分配

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.flexbox.FlexboxLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#EBEBEB"
        app:flexDirection="row"
        app:flexWrap="wrap">

        <TextView
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            app:layout_flexGrow="10.0"
            android:text="Android" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            android:text="android studio" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="4dp"
            android:background="@drawable/item_bg"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="4dp"
            android:paddingBottom="4dp"
            app:layout_flexGrow="10.0"
            android:text="Android安全" />
     </com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

这里写图片描述
可以看到黄色框内的两个元素(也就是第一个子元素和第三个子元素)按照1:1 的比例分配了这一行中剩余的空间。

4.3 layout_flexShrink

layout_flexShrink , 子元素缩小比例 , 与上面的属性值有点对应关系,当空间不足时,子元素需要缩小(针对单行),默认值为1,如果所有子元素的layout_flexShrink 值为1,空间不足时,都等比缩小,如果有一个为0,其他为1,空间不足时,为0的不缩小,负值无效。

这里引用Github上的一个图,看看效果
缩小比例

4.4 layout_alignSelf

layout_alignSelf 属性可以给子元素设置对齐方式,上面讲的alignItems属性可以设置对齐,这个属性的功能和alignItems一样,只不过alignItems作用于所有子元素,而layout_alignSelf 作用于单个子元素。默认值为auto, 表示继承alignItems属性,如果为auto以外的值,则会覆盖alignItems属性。支持auto , flex_start , flex_end , center , baseline , stretch六种取值。除了auto以外,其他和alignItems属性一样,而auto表示继承alignItems的属性,所以基本上效果相同,这里不做过多解释。

4.5 layout_flexBasisPercent

layout_flexBasisPercen , 表示设置子元素的长度为它父容器长度的百分比。这个值只有设置了父容器的长度时才有效(也就是MeasureSpec mode 是 MeasureSpec.EXACTLY)。默认值时-1。

设置第一个元素
app:layout_flexBasisPercent=”50%”

这里写图片描述

4.6 layout_minWidth / layout_minHeight

强制限制 FlexboxLayout的子元素(宽或高)不会小于最小值,不管layout_flexShrink这个属性的值为多少,子元素不会被缩小到小于设置的这个最小值。

4.7 layout_maxWidth / layout_maxHeight

这个和上面的刚好相反,强制限制FlexboxLayout子元素不会大于这个最大值, 不管layout_flexGrow的值为多少,子元素不会被放大到超过这个最大值。

4.8 layout_wrapBefore

layout_wrapBefore 属性控制强制换行,默认值为false,如果将一个子元素的这个属性设置为true,那么这个子元素将会成为一行的第一个元素。这个属性将忽略flex_wrap 设置的 noWrap值。

设置第三个元素
app:layout_wrapBefore=”true”

这里写图片描述

可以看到第三个元素另起了一行

作者:poorkick 发表于2017/8/19 17:03:46 原文链接
阅读:734 评论:2 查看评论

React Native自定义Button

$
0
0


效果:





引用文件代码:


import React, { Component } from 'react';
import {
   AppRegistry,
  Image,
    Text,
    View,
  StyleSheet,
} from 'react-native';

var ZYButton = require('./ZYButton')

class RNHybrid extends Component {
  render() {
    return(
        <View style={{marginTop:100,alignItems:'center'}}>
              <ZYButton
          clickBtn={()=>this.buttonClick()}
          btnStyle={styles.btnStyle}
          btnInnerTextStyle={styles.btnStyle}
           title="ZYButton"
           />
        </View>
      );   
  }

  buttonClick(){
    alert('buttonClick');
  }
};



const styles = StyleSheet.create({
    btnInnerTextStyle:{
       
    },

    btnStyle:{

    }
});


AppRegistry.registerComponent('RNHybrid', () => RNHybrid);


ZYButton定制代码:


import React, { Component, PropTypes} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    TouchableOpacity
} from 'react-native';


export default class ZYButton extends Component {
    // 对外开放属性
    static propTypes = {
        // 常用的属性
        title: PropTypes.string,
        bgImage:PropTypes.func,
        btnStyle: View.propTypes.style,
        btnInnerTextStyle:Text.propTypes.style,

        // 响应事件
        clickBtn: PropTypes.func
    };

    static defaultProps = {
        clickBtn(){},
        bgImage(){}
    };


    constructor(props){
        super(props);

        this.state = {
            selected:this.props.selected
        }
    }


    render() {
        return (
           <TouchableOpacity
               style={[styles.btnViewStyle, this.props.btnStyle]}
               onPress={()=>this.props.clickBtn()}
           >
               {this.props.bgImage()}
               <Text style={[styles.btnTextStyle, this.props.btnInnerTextStyle]}>{this.props.title}</Text>
           </TouchableOpacity>
        );
    }
}

const styles = StyleSheet.create({
    btnViewStyle:{
        backgroundColor:'red',
        width:120,
        height:40,
        justifyContent:'center',
        alignItems:'center',
        borderRadius:5
    },

    btnTextStyle:{
        color:'#fff',
        fontSize:16,
        backgroundColor:'transparent'
    }
});

module.exports = ZYButton;



作者:ZY_FlyWay 发表于2017/8/21 13:19:40 原文链接
阅读:45 评论:0 查看评论

iOS开发入门笔记

$
0
0

iOS开发入门笔记

本文面向已有其它语言(如Java,C,PHP,Javascript)编程经验的iOS开发初学者,初衷在于让我的同事一小时内了解如何开始开发iOS App,学习目标包括:

  • 能使用Xcode IDE、模拟器
  • 能修改、调试已有iOS App
  • 能在已有应用内创建新模块
  • 能创建新应用
  • 能发布应用到App Store

本文不包含任何高级的iOS开发知识,已学会iOS开发的同学不要看,看完这篇文章学会了的同学也不用再看了。

不仅是学习一门新语言

有过脚本开发经验的人(如Javascript,PHP,Shell)在刚开始学习iOS开发的时候,会觉得iOS开发的学习曲线比脚本语言要高,是的,这种感觉是对的。因为学iOS开发,不仅是学习一门新语言,它包括:

  • 一门语言:Objective-C
  • 一个框架:Cocoa Touch
  • 一个IDE:Xcode

初学脚本语言通常不会来绘制图形界面、与人交互,iOS如果不做图形界面,像脚本语言一样处理文本操作数据库,就没啥意思了。

所以,过去我写别的新手入门教程,通常都是写《XXX入门15分钟教程》,而iOS就要花数倍的时间来写了。

环境准备

做iOS开发一定要有苹果的软件环境:Mac OS操作系统、Objective-C编译器、设备模拟器等,开发工具倒不一定要用Xcode,只要是个源代码编辑工具就行(vim都行,只是没Xcode那么多功能)。

Mac OS

拥有Mac OS环境最简单的方法是找一台苹果电脑,包括iMac, MacBook Pro, MacBook Air, Mac Mini,但不包括苹果的移动设备(iPod Touch, iPhone, iPad, iPad Mini,它们运行的是iOS系统,不是Mac OS),苹果电脑在出厂的时候就会预装Mac OS,目前最新版本是Mac OS X 10.8,主流的版本还有Mac OS X 10.6、Max OS X 10.7。

如果囊中羞涩,可以借一台,或者上淘宝买个二手的。

黑苹果

提到iOS开发入门,似乎没办法不说黑苹果。所谓黑苹果,就是把Mac OS改造后安装在非苹果的硬件上,这是违反DMCA法案的,黑苹果的更多资料,可以在维基上找到

苹果电脑价格高,国内软件开发者生存压力大,所以黑苹果在国内也有一些真实的存在,国外当然也有啦。

黑苹果基本可以胜任iOS开发,但有一些问题:

  • 安装黑苹果是非法的
  • 个人行为苹果公司一般不会追究,但会遭同行的鄙视
  • 黑苹果超级难装,挑硬件。即使完全相同的型号,相同的批次,也有可能A机器装上了,B机器装不上
  • 黑苹果系统多少都存在一些使用上的问题,像驱动Bug啦、待机恢复蓝屏啦、上网浏览有问题啦
  • 黑苹果不能随意升级,可能升级一次safari就导致整个系统崩溃了

上面这些虽然不会直接影响Xcode写代码、模拟器测试,但写着写着想上网查个东西的时候,safari不能翻页,确实挺影响心情的。所以,钱包允许的前提下,还是搞个苹果电脑省心一些。

Xcode 和 模拟器

Xcode可以在苹果官网免费下载:Xcode下载地址

安装Xcode时会自动安装iOS SDK和模拟器。

这么强大的IDE居然是免费的,还是挺让人开心的。

从改一个现成的应用开始吧

学一门新软件开发技能,能够第一时间做出一个可运行的产品非常重要,有助于给自己正面激励,我上大学的时候,有很多次想学一门新语言,往往花了半个月,还沉浸在数据类型和语法字典里,连第一个Hello World都没做出来。

这一次,就让我们从改一个现成的应用开始吧。

下载

首先,我们从苹果开发者中心下载一个示例代码回来。我选了ToolBarSearch

在本文档的末尾,还有一些其它的网址可以下载开源iOS产品或者代码段,但我试了一下,还是Apple Sample Code最容易成功。

下载回来的zip文件最好保存在”下载”或者”文稿”目录里,因为在Mac OS 10.8以前,有些目录(例如/var/private/tmp)在Finder中是看不到的,要通过Finder的“前往 > 前往文件夹”功能才能进入。

打开

有三种方式可以打开一个iOS Project

双击project文件

打开Finder,进入刚刚下载解压的ToolBarSearch目录,找到ToolBarSearch.Xcodeproj文件,双击之,Xcode会自动启动,并打开这个项目

在Xcode里选择Project打开

  • 在Xcode没启动的情况下(如果Xcode已经启动了,就先按Command Q退出),启动Xcode,会弹出“Welcome to Xcode”的欢迎页,点击左下角的“Open Other”按钮,找到ToolBarSearch目录,双击ToolBarSearch目录,或者双击ToolBarSearch.Xcodeproj文件都可以

  • 如果Xcode处于打开状态,可以点击其菜单栏的File -> Open,或者File -> Open Recent,然后再选择要打开的项目

通过命令行打开

在Mac OS 10.8以前,有些目录(例如/var/private/tmp),在Finder和Xcode的File > Open对话框中,点击鼠标是找不到的,这时候就要通过命令行终端来打开了。

打开终端,执行:

cd /ToolBarSearch的父目录/ToolBarSearch
open -a Xcode

open -a是mac os的系统命令,除了iOS项目,别的项目也可以这样打开。

运行刚下载的应用

点击Xcode左上角的Run按钮(或者同时按下Comman和R键),Xcode会编译源码并在模拟器中运行这个应用。

编译成功会在屏幕上淡淡地显示“Build Succeeded”。反之,失败就显示“Build Failed”且不启动模拟器。

修改

在模拟器上看到“Performed search using…”了吧,下面我们改掉它。

  • 在Xcode左上角的Run按钮下方,有一排小按钮,从左到右第三个是一个放大镜图标,鼠标移上去会显示“Show the Search Navigator”,点一下它,打开搜索界面,在它下方出现的Find输入框中输入“performed”

  • 搜索结果只有一条:ToolbarSearchViewController.m,点文件名下方被高亮的“Performed”字串,右侧代码编辑区会自动打开这个文件,并滚动屏幕,使包含“Performed”的这一行出现在编辑区的中间。

  • 修改双引号里的字串,随便改成啥,然后按“Command S”保存。

当然,这些操作,你也可以在终端下通过grep和vim完成。

运行修改后的应用

按Command R运行,看看,是不是看到效果啦?

是的,修改一个应用就这么简单。

Objective-C

Objective-C是苹果应用软件(包括苹果电脑上的Mac OS App和移动设备上的iOS App)的开发语言。它是一种面向对象的编程语言。

苹果公司还提供了一个软件,叫Interface Builder,简称IB,用于可视化的界面制作,就像用Dreamweaver做网页,或者像Visual Basic做桌面软件一样。后来IB就整合进了Xcode,成了Xcode的一部分。这篇文档不讲IB,只讲Objective-C,因为:

  • 基本上,每一本讲iOS开发的书(纸质书、电子书),都有大量的截图一步一步教如何用IB开发iOS应用,而讲Objective-C开发应用的书却没有那么多。
  • IB可以用来直观方便地画界面、设置控件属性、建立代码与控件的联系,但后台的业务逻辑和数据处理仍然要靠Objective-C,可见,不管用不用IB,Objective-C都是绕不过去的。

C的超集

Objective-C扩展了ANSI C,是C的超集,也就是说:

  • 任何C源程序,不经修改,即可通过Objective-C编译器成功编译
  • Objective-C源程序中可以直接使用任何C语言代码

除了面向对象有语法是SmallTalk风格的(下面会讲到),其它非面向对象的语法、数据类型,与C完全相同,所以本文就不再赘述。
来看一个经典的Hello World示例吧:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]){
    @autoreleasepool{
        NSLog(@"Hello World!");
    }
    return 0;
}

是不是仿佛穿越回了大一学习C语言的时代,看起来和C几乎没有区别,是吧?是的,因为还没用到它的面向对象特性,哈哈!

SmallTalk的消息传递语法风格

Objective-C的面向对象语法源自SmallTalk,消息传递(Message Passing)风格。在源码风格方面,这是它与C Family语言(包括C/C++、Java、PHP)差别最大的地方。

在Java、C++世界,我们调用一个对象的某方法,在Objective-C里,这称作给类型发送一个消息,这可不仅仅是文字游戏,他们的技术细节也是不同的。

在Java里,对象和方法关系非常严格,一个方法必须属于一个类/对象,否则编译是要报错的。而在Objective-C里,类型和消息的关系比较松散,消息处理到运行时(runtime)才会动态决定,给类型发送一个它无法处理的消息,也只会抛出一个异常,而不会挂掉。

[obj undefinedMethod];

在代码里调用没定义的方法(这是Java世界的习惯说法啊,专业的叫法是,给obj对象传递它无法处理的消息),Xcode会警告,但编译能成功,运行的时候会出错。它会输出这样一个错误:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSObject undefinedMethod]: unrecognized selector sent to instance 0x8871710'

类似Java的OOP概念

Objective-C中一些面向对象的概念,也可以在Java中找到类似的实现(只能说是类似,不是完全相同),我的读者基本都是Java和PHP程序员,我会在下文中尽量用Java的概念来类比。

GoogleCode上有人整理了Java和Objective-C的概念、数据类型对应表,参见这里

字符串

Objective-C里有字符串是由双引号包裹,并在引号前加一个@符号,例如:

title = @"Hello";
if(title == @"hello") {}

PHP程序员要注意,在这里不能用单引号,即使只有一个字符也不能用。Objective-C与Java、C一样,双引号表示字符串。

函数调用

前文述及,不涉及面向对象时,它和C是完全一样的。以下是几个函数调用的示例:

不带参数

startedBlock();

带参数

NSLog(@"decrypted string: %@", str);
CGRectMake(0,0,0,0);

传递消息给类/实例方法

不带参数

[obj method];

对应的Java版本

obj.method();

带一个参数:

[counter increase:1];

对应的Java版本

counter.increase(1);

带多个参数

对C Family程序员来说,这是最难接受的,最反人类的:

- (void) setColorToRed: (float)red Green: (float)green Blue:(float)blue {...} //定义方法
[myObj setColorToRed: 1.0 Green: 0.8 Blue: 0.2]; //调用方法

对应的Java版

public void setColorToRedGreenBlue(float red, float green, float blue) {...}
myObj.setColorToRedGreenBlue(1.0, 0.8, 0.2);

消息嵌套

UINavigationBar *bar = [[[UINavigationBar alloc] init] autorelease];

对应的Java版

UINavigationBar bar = UINavigationBar.alloc().init().autorelease();//Java没有指针,所以星号去掉了

接口和实现

Objective-C的类分为接口定义和实现两个部分。接口定义(Interface)放在头文件中,文件扩展名是.h,实现(implementation)放在实现文件中,文件扩展名是.m(也有.mm的扩展名,表示Objective-C和C++混编的代码)。

接口定义也可以写在.m文件中,但最好不要这么干

需要注意的是,与Objective-C的interface概念最接近的是C和C++里的头文件,它与implementation是成双成对出现的,作用是声明类的成员变量和方法。它与Java的interface概念完全不同:

  • Objective-C里,interface有且只有一个实现,Java的interface可以有0-N个实现
  • Objective-C里,interface可以定义成员属性,Java里不可以

在Objective-C里,和Java的Interface概念相似的是Protocol,下文会讲到。

请看示例:

Interface

@interface MyClass {
    int memberVar1;
    id  memberVar2;
}

-(return_type) instance_method1; 
-(return_type) instance_method2: (int) p1;
-(return_type) instance_method3: (int) p1 andPar: (int) p2;
@end

Implementation

@implementation MyClass {
    int memberVar3;
}

-(return_type) instance_method1 {
    ....
}
-(return_type) instance_method2: (int) p1 {
    ....
}
-(return_type) instance_method3: (int) p1 andPar: (int) p2 {
    ....
}
@end

接口和实现以@interface、@implementation开头,都以@end结束。“@”符号在Objective-C中是个很神奇的符号。

冒号也是方法名的一部分,method和method:是两个不同的方法名,不是overload,第二个带参数。

上述代码对应的Java版:

public class MyClass {
    protected int memberVar1;
    protected pointer memberVar2;
    private int memberVar3;

    public (return_type) instance_method1() {
        ....
    }

    public (return_type) instance_method2(int p1) {
        ....
    }

    public (return_type) instance_method3andPar(int p1, int p2) {
        ....
    }
}

私有方法和公开方法

写在.h头文件里的方法都是公开的,Objective-C里没有私有方法的概念(没有你说个蛋啊,哈哈哈哈)。

官方并没有提到Objective-C怎么实现私有方法,我查阅了stackoverflow,统一的答案是,要实现私有方法的效果只能借助Category,不过,根据我的测试,即使采用了Category,也不能阻止外部的代码调用这个“私有方法”,只是Xcode不支持“私有方法”的自动完成,并会有警告提示,运行的时候,还是会成功的。各位看官知道有这么回事就可以了,这里不深讲。

变量和属性

类方法和实例方法

类方法

类方法就是Java、PHP里的Static Method,不用实例化就能调。类方法有一个加号前缀。
示例:

类定义

@interface MyClass
    +(void) sayHello;
@end

@implementation MyClass

+(void) sayHello {
    NSLog(@"Hello, World");
}
@end

使用

[MyClass sayHello];
实例方法

实例方法就是Java、PHP里的普通方法,必须实例化才能调。实例方法有一个减号前缀。
示例:

类定义

@interface MyClass : NSObject
-(void) sayHello;
@end

@implementation MyClass

-(void) sayHello {
    NSLog(@"Hello, World");
}
@end

使用

mycls = [MyClass new];
[mycls sayHello];

Selector

selector就是一个方法指针,类似PHP里的动态方法名:

<?php
class Hello {
    public function sayHello() {}

    public function test() {
        $fun_name = "sayHello";
        $this->$fun_name();
    }
}

在Objective-C里,selector主要用来做两类事情:

绑定控件触发的动作
@implementation DemoViewController
- (void)downButtonPressed:(id)sender {//响应“按钮被按下事件”的方法
    UIButton *button = (UIButton*)sender;
    [button setSelected:YES];
}

- (void)drawAnButton {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; 
    btn.frame = _frame; 
    btn.tag = 1;
    btn.backgroundColor = [UIColor clearColor];
    [btn addTarget: self
         action: @selector(downButtonPressed:)
         forControlEvents: UIControlEventTouchUpInside];//当这个按钮被按下时,触发downButtonPressed:方法
}
@end
延时异步执行
@implementation ETHotDealViewController
- (void)viewDidLoad {

    //获取数据源
    HotDealDataSource *ds = [[HotDealDataSource alloc]init];
    [ds reload];
    _items = ds.items;

    [self performSelector: @selector(refreshTable)
          withObject: self
          afterDelay: 0.5];//延迟0.5秒调用refreshTable方法
}

-(void)refreshTable
{
    [self.tableView reloadData];
}
@end

这个例子中,获取数据源是通过ASIHTTP组件异步调用服务端HTTP接口,refreshTable要用到数据源返回回来的数据,如果不延迟0.5秒,就会立刻执行,执行的时候数据还在路上呢,页面就要变空白了。

继承

继承是写在Interface定义里面的。语法为:子类名在左,父类名在右,中间用冒号分隔。
示例:

@interface MyClass : NSObject
@end

对应的Java版本是:

public class MyClass extends NSObject {
}

协议(Protocol)

就是Java、PHP里的Interface。

协议的定义

协议的定义用@protocol关键字:

@protocol Printable
    -(void)print:(NSString)str;
@end

对应的Java版本是:

publilc interface Printable {
    public void print(String str);
}
协议的继承

协议本身也可以继承别的协议:

@protocol Printable <NSObject>
    -(void)print:(NSString)str;
@end

对应的Java版本:

public interface Printable extends NSObject {
    public void print (String str);
}
可选方法

协议可以包含可选方法,顾名思义,可选方法可以不被类实现:

@protocol Printable
@optional
    -(void)print:(NSString)str;
@end

加了@optional关键字,一个类在implements这个协议时,便可以不实现print:方法。

Java里没有类似的实现,除了Collection里会有一些方法带有optional的注释,但Collection是个特例。

协议的实现

一个类实现某些协议是写在Interface定义里面的。语法为:协议名用尖括号包裹,多个协议名用逗号隔开,协议写在父类的右边(如果没有父类就直接写在子类右边)。

示例:

@interface  class MyClass : NSObject <Printable, Drawable>
@end

Printable, Drawablw就是两个协议。

对应的Java版本是:

public class MyClass extends NSObject implements Printable, Drawable {
}

分类(Category)

分类可以给一个已经存在的类增加方法,而不用去改它的源码。Java和PHP中都没有类似的特性。

比如说,NSObject是一个Objective-C内置的系统类,我们想给它增加toJson方法,就像这样:

头文件:NSObject+Json.h

@interface NSObject (Json)
    -(NSString)toJson;
@end

实现文件:NSObject+Json.m

@implementation NSObject (Json)
    -(NSString)toJson {
        //...
    }
@end

使用的时候,只要包含NSObject+Json.h,实例化NSObject类,就可以使用toJson方法了:

import "NSObject+Json.h"
@implatementation XYZController
    -(void)test {
        NSObject *obj = [[NSObject alloc]init];
        NSString *str = [obj toJson];
    }
@end

当然了,NSObject本来的那些方法依然还是可以用的,什么都没变,只是多了个toJson方法。看起来是不是和继承没太多差别呢(除了使用的时候实例化的是NSObject,而不是JsonObject)?再看一个继承实现不了的例子:

头文件:NSObject+Json+XML.h

@interface NSObject (Json)
    -(NSString)toJson;
@end

@interface NSObject (XML)
    -(NSString)toXML;
@end

实现文件:NSObject+Json+XML.m

@implementation NSObject (Json)
    -(NSString)toJson {
        //...
    }
@end

@implementation NSObject (XML)
    -(NSString)toXML {
        //...
    }
@end

使用:

import "NSObject+Json+XML.h"
@implatementation XYZController
    -(void)test {
        NSObject *obj = [[NSObject alloc]init];
        NSString *json = [obj toJson];
        NSString *xml = [obj toXML];
    }
@end

Cocoa Touch

Cocoa是Mac OS App的开发框架,Cocoa Touch是iOS开发用的框架,Cocoa Touch和Cocoa大部分是一样的,只是Cocoa Touch多了一些移动设备特有的东西,如:触摸屏、加速度传感器、GPS定位。Cocoa中多任务、多窗口的特性,在Cocoa Touch中也是没有的(或者跟Cocoa不完全一样的)。

就像学了Java语言还要再学一些Spring、Hibernate、Struts(或者其它类似的Java类库)才能开始做J2EE应用一样,学过Objective-C语言之后,也要再学习Cocoa Touch框架才能顺利地开发iOS应用。

最常用设计模式之Delegate

Cocoa Touch大量使用Delegate(委派)设计模式。

常用控件:按钮、文本块、图片、输入框

TableView

WebView

导航条

Xcode

运行

快捷键:Comman R

搜索

搜索文本

搜索文件

新建文件/目录

推荐在Finder中新建好的再添加进来

断点

模拟器和真机测试

模拟器测试

在Xcode中打开你的项目,在Xcode顶部工具栏的Stop按钮(Run按钮右边那个黑色正方形按钮)右边,有个下拉菜单,显示着 “ToolBarSearch > iPhone 5.0 Simulator” (即 你的应用英文名 > 当前选中的调试 ),点击这个下拉菜单,选中iPhone 5.0 Simulator(这里的5.0是指iOS版本,不是iPhone5的意思,如果你的项目是iPad应用,请选iPad 5.0 Simulator),再按“Run”按钮,Xcode就会自动把当前正在编辑开发的应用编译并安装到模拟器上。

在模拟器上操作时,如果执行过程中遇到了你在Xcode里设置的断点,模拟器会暂停运行,并将当前活动窗口切换回Xcode,供你调试。

在Xcode里增加或者取消了断点,不需要重新编译和安装应用即可生效。

切换被模拟的设备

模拟器的“硬件”菜单,可以选择想要模拟什么设备,有iPad、iPhone可选。

  • Retina:表示视网膜屏,iPhone(Retina)代表iPhone4,iPhone4S
  • 4-Inch:表示4英寸的iPhone,iPhone(Retina 4-Inch)就是iPhone 5

切换模拟的iOS版本

在模拟器的“版本”菜单,可以选择要模拟什么版本的iOS。设备和版本是彼此独立的,iPhone 4S可以有5.0,5.1,6.1几种iOS版本,当然了,iPhone 5不可能有4.3的iOS版本。

触摸屏

用鼠标点击(不区分左右键)模拟器上的iPhone、iPad屏幕,就是在模拟用手指触摸iPhone,iPad的屏幕,可以实现一些触摸效果比如:

  • 鼠标单击 等于 手指轻触
  • 鼠标长按 等于 手指长按(例如你可以在模拟器上长按应用icon调出删除应用的确认框)
  • 鼠标按住拖动 等于 手指拖动
  • 双击和单击模拟器的Home键也等于双击和单击真机的Home键
多指手势

多指手势比较复杂,在白苹果笔记本上可以模拟简单的双指手势,白苹果的触控板天然支持多指触摸,但要定位到模拟器的区域再响应多指手势就需要借助一些额外的键啦:

  • 按住Option键,再用两个手指去操作触摸板,可模拟双指拖动、旋转
  • 按住Option+Shift,可模拟双指合拢

输入法和键盘

输入中文

手机上特有的输入法(比如九宫格输入法)不能模拟。模拟器默认的iOS软键盘只有英文输入,在测试应用的时候,我们要用到中文,有两个办法:

  • 使用剪贴板,在Mac OS里复制,再到模拟器运行的应用中的输入框上长按鼠标(模拟手指长按)3秒以上,等弹出“粘贴”的时候选择之,即可。
  • 在模拟器里,按Home键,找到Setting那个App icon(不是Mac OS顶部的模拟器菜单啊,那里没有Setting的),打开被模拟iOS设备的设置,依次点击”General - Keyboard - International Keyboards - Add New Keyboard…”,加个中文键盘,以后就可以使用被模拟iOS设备软件盘输入中文了,跟在iPhone/iPad真机上一样。

使用Mac电脑的键盘

如果要输入大量文本,使用模拟器里的软键盘效率太低,这时候可以使用物理键盘,方法是:在Mac OS顶部的模拟器菜单栏,点击”硬件”菜单,勾选下拉菜单中的“模拟硬件键盘”。以后再用模拟器运行iOS应用时,点击iOS应用中的输入框,软键盘就不弹出来了,可直接使用Mac电脑的物理键盘输入。

注意

  • 模拟器中的iOS接管了物理键盘输入,所以,调用的是模拟器iOS的输入法,不是你的Mac电脑的输入法。打个比方,你的Mac OS装的是搜狗五笔,模拟器中iOS加了个拼音输入法(Add New Keyboard),那么,在iOS应用中输入中文会调用拼音输入法。
  • 要切换模拟器中iOS的中英文输入法,也只能按iOS设备软键盘上的小地球图标,按Mac电脑上的Command+空格键是不行的。

地理位置

但Mac电脑没有定位用的硬件(GPS)和软件基础,因此模拟器不能自动获得当前的地理位置,不能用模拟器测试定位功能。(注意,虽然WiFi也可以独立定位——iPad WiFi版没有GPS也可以定位,但Mac电脑的WiFi不具备定位相关的软件)

要在模拟器里测试依赖地理位置的功能(如”我附近的xx”),可以手工指定一个经纬度给模拟器,方法:在Mac电脑顶部的模拟器菜单,点击”调试 - 位置 - 自定位置”,会弹出一个对话框,在弹出的框内填入经纬度即可。

如何获得经纬度?
上谷歌地图(ditu.google.cn),在地图上找到你想要的位置(比如你想知道杭州大厦的位置,就在通过搜索框找到杭州大厦),点击右键,选择“这儿是什么”,搜索框中就会出现这个位置的经纬度了,前面是纬度,后面是经度。咱们天朝的版图,都是北纬和东经。

摄像头

Mac电脑有摄像头,但Mac OS没有设计API给iOS模拟器调用,所以,不能用模拟器测试对焦闪光灯等功能。

要在模拟器上测试依赖照片的功能,可以在代码里做一个workaround,即当代码检测到摄像头不可用时,弹出一个照片选择器,让测试人员从相册里选择一幅照片,来进行后续的操作(如照片美化、人脸识别、条码扫描)。

真机测试

模拟器能验证你开发的iOS应用的大部分功能,但有些Mac设备上不具备的硬件,模拟器是不能模拟的。前文提到了一个绕过这些限制的办法,但获取当前位置、拍照、加速度感应这些是模拟不了的,一款应用发布给消费者之前,必须要在真实设备上验证过。

将未提交App Store审核通过的应用安装到iOS设备上测试,有三种办法:

  • 加入苹果的Developer Program,成为付费会员,有了这个付费会员资格,就可以直接在Xcode中点击”Run”将刚刚改过的代码编译打包安装到开发测试用的iOS设备上。在iOS真机上操作被测试的程序能激活Xcode中设置的断点。
  • 越狱iOS设备。将iPhone和iPad越狱后,可以通过SSH直接上传Xcode编译好的ipa包(一个iOS App本质上就是一个ipa包)。
  • 越狱的iOS设备,配合破解过的Xcode,甚至可以实现和付费开发者计划一样的功能:在Xcode上点击”Run”,就自动编译安装到iOS设备上去运行了
  • 企业部署方案。就像阿里巴巴的轩辕剑一样,用iPhone/iPad访问这个网址,点击里面的轩辕剑链接就可以安装轩辕剑这个应用了。

破解Xcode是违法行为(越狱是合法的),而且挑版本挑得厉害,不是所有Xcode版本都能破解,也不是所有Xcode的破解版都能和越狱的iOS配合好。越狱+SSH上传跟企业部署一样效率低(部署效率低,无法激活Xcode中的断点),只能用于QA验收,不适合开发自测。综上所述,最适合开发实时测试的就是第一个正规途径了。下面重点讲这个:

拥有一个开发者账号

苹果的Developer Program分为个人开发者和公司开发者,分别是每年99美元和每年299美元,分别可以注册100台和500台苹果测试设备。这个台数限制在一个付费年度内不会清空,比如说,2013年4月1日付费成功的,付费会员资格在2014年3月31日之前有效,这期间,注册一台就少一个名额,哪怕这个设备注册进来用了之后一分钟马上又删掉了,减少的这个名额也不会回来。

在交钱之前,最好问一下,周围的同事,有没有已经交了钱的。如果有,你只需要注册一个免费的Apple ID(就是你在App Store安装软件用的Apple ID),请他发个邀请邮件给你,把你的Apple ID加入他的团队就可以了,苹果会认为你们两个人是一个团队的,你们分别用自己的账号,共享100台设备的限额,这是合法的。

安装证书和私钥

证书

不想看下面各种点击各种页面跳转的直接用浏览器访问证书管理,你要登录你就用Apple ID登录(前提是交过钱,或者找交了钱的人把你加入团队了)。
>
不嫌烦,或者想知道下次没我这个文档的时候怎么进证书管理吗?按这个步骤操作:

页面上有一个“Your Certificate”区域,下方有个Download圆角按钮,这是你的个人证书,下载下来。再下面一行,有一句“If you do not have the WWDR intermediate certificate installed, click here to download now”,这个是苹果的公共证书,也下下来。

双击下载回来的证书,装证书时,会提示你输入密码,这是【钥匙串访问工具】在问你要你的Mac OS账号开机密码(相当于linux里面的sudo),不是Apple ID的密码,不要搞错了。

安装私钥

如果你是和其它同事公用的账号,让他给你一个私钥即可,就是一个扩展名为p12的文件,双击之,钥匙串访问会自动出来,需要你输入一个密码,这个密码问给你p12文件的人要,不是你的Mac OS系统开机密码,也不是你的Apple ID密码。

将设备注册到Provisioning Portal

  • 打开Xcode,从Xcode的Window菜单中找到Organizer,打开之(Shift Command 2)。
  • 把iOS设备连上电脑,Organizer会自动识别出你的设备,并显示在左侧边栏。
  • 在Organizer左侧边栏找到你的设备,右键,点击“Add Device to Provisioning Portal”,然后等Organizer提示你操作成功即可。(选中设备后,右边设备详情区域会显示一个按钮“Use for Development”,点它也可以)。

到iOS真机上运行测试版程序

回到Xcode主界面,在Stop按钮(Run按钮右边那个黑色正方形按钮)右边,有个下拉菜单,显示着 “ToolBarSearch > iPhone 5.0 Simulator” (即 你的应用英文名 > 当前选中的调试 ),点击这个下拉菜单,选中你的真机设备名,再按“Run”按钮,Xcode就会自动把当前正在编辑开发的应用编译并安装到真机上测试啦!

发布到App Store

打IPA包

IPA包本质上是一个ZIP压缩包,只不过它有着特殊的目录结构,扩展名是ipa,制作方法如下:

  • 在Xcode中Build项目,快捷键Command B
  • 在左侧项目导航器中,展开Products文件夹,找到你要打包的应用,你的应用名.app,右键,选择show in finder
  • 到Finder中Copy这个.app目录(选中,按Command C),复制到一个你新建的名为Payload(区分大小写)的文件夹中
  • 找到你的应用Logo,即一个512 * 512像素的PNG文件,copy到与Payload一起(与Payload并列,不要放进Payload了),并重命名为iTunesArtwork(区分大小写,没有扩展名)
  • 将Payload目录、ItunesArtwork文件打成一个zip包,并更改扩展名为ipa
  • 双击这个ipa文件,会用iTunes打开,如果打开成功,且在iTunes里有应用Logo显示,就成功了

批量自动打包

除App Store外,还有许多其它的iOS应用市场(如91助手,同步推等等),如果一个应用需要发布到很多个应用市场,且他们的代码略有不同(比如说,统计代码不同),按上述方法手工修改源码再打包,再还原,比较容易出错。好消息是,Xcode是有命令行的,我们可以写一个shell脚本,先用se自动修改源码,再调用Xcode的命令行来编译以得到your——app.app目录,最后调用zip、mv等命令把上一个章节讲的ipa打包动作自动执行。

阅读应用代码

从头新建一个应用:Hello World

其它

代码里的控件尺寸

iOS App里的控件尺寸和字体大小都是指Point,Retina设备(iPhone 4,4S,5;the new Pad)和非Retina设备(iPhone 3GS,iPad,iPad 2)的Point数是一样的,尽管iPhone 4的分辨率是3GS的2倍。比如说,10point在Retina设备里是20 pixel,在非Retina设备(iPhone 3G)上则是10 pixel。

项目成员间交流时,应使用Point,不要使用pixel。

SVN操作含有@符号的文件

iOS应用中经常出现xxxx@2x.png这样的文件名,它们是给retina设备用的高分辨率大图,用svn命令行操作它们的时候会被@符号干扰,解决方案是在svn命令末尾加上一个@符号,如:

svn del icon@2x.png@
svn info Default@2x.png@

如果一次移动了几十个png文件再svn commit的,可以用shell批处理:

svn status | awk '($1=="!"){print $2}' | grep -v @ | xargs svn del

上面这个命令是将文件名不包含@符号的,且已经不在硬盘上的文件从svn version controll中删掉

for file in `svn status | awk '($1=="!"){print $2}' `; do svn del $file"@"; done     

上面这个命令是将文件名包含@符号的,且已经不在硬盘上的文件从svn version controll中删掉

svn add同上, 如法炮制即可.

Xcode中的代码结构与操作系统上的文件系统并不一致

推荐在Finder里建好目录再到Xcode的Project Navigator中点击“Add Files to”添加到项目中

iPhone 5适配

iPhone 5与之前的iPhone不一样,采用了4寸Retina屏,所以它的Point数变成了320 * 568 points

开源代码

Objective-C教程

作者:xiangzhihong8 发表于2017/8/21 13:52:42 原文链接
阅读:0 评论:0 查看评论

Android优化之Hardware Layer

$
0
0

项目中越来越多的动画,越来越多的效果导致了应用性能越来越低。该如何提升。

简介

View播放动画的过程中每一帧都需要被重绘。如果使用view layers,就不用每帧都去重绘,因为View渲染一旦离开屏幕缓冲区就可以被重用。

而且,hardware layers会在GPU上缓存,这样就会让一些动画过程中的操作变得更快。通过hardware layers可以快速的渲染一些简单的转变(位移、选中、缩放、颜色渐变)。由于很多动画都是这些动作的结合,所以hardware layers可以显著的提高动画性能。

View当中提供了三种类型的Layer type:

  • LAYER_TYPE_HARDWARE

    Indicates that the view has a hardware layer. A hardware layer is backed by a hardware specific texture (generally Frame Buffer Objects or FBO on OpenGL hardware) and causes the view to be rendered using Android’s hardware rendering pipeline, but only if hardware acceleration is turned on for the view hierarchy. When hardware acceleration is turned off, hardware layers behave exactly as software layers.

    A hardware layer is useful to apply a specific color filter and/or blending mode and/or translucency to a view and all its children.

    A hardware layer can be used to cache a complex view tree into a texture and reduce the complexity of drawing operations. For instance, when animating a complex view tree with a translation, a hardware layer can be used to render the view tree only once.

    A hardware layer can also be used to increase the rendering quality when rotation transformations are applied on a view. It can also be used to prevent potential clipping issues when applying 3D transforms on a view.

  • LAYER_TYPE_SOFTWARE

    Indicates that the view has a software layer. A software layer is backed by a bitmap and causes the view to be rendered using Android’s software rendering pipeline, even if hardware acceleration is enabled.

    Software layers have various usages:

    When the application is not using hardware acceleration, a software layer is useful to apply a specific color filter and/or blending mode and/or translucency to a view and all its children.

    When the application is using hardware acceleration, a software layer is useful to render drawing primitives not supported by the hardware accelerated pipeline. It can also be used to cache a complex view tree into a texture and reduce the complexity of drawing operations. For instance, when animating a complex view tree with a translation, a software layer can be used to render the view tree only once.

    Software layers should be avoided when the affected view tree updates often. Every update will require to re-render the software layer, which can potentially be slow (particularly when hardware acceleration is turned on since the layer will have to be uploaded into a hardware texture after every update.)

  • LAYER_TYPE_NONE

    Indicates that the view does not have a layer.
    默认值。

使用

首先使用的前提是在清单文件中开启了硬件加速。否则将无法使用hardware layer。这一点在上面的文档中也有说明。

API也是非常简单的,直接使用View.setLayerType()就好。使用时应该只是暂时的设置Hardware Layer,因为它们无法自动释放。
基本的使用步骤:

  • 对每个想要在动画过程中进行缓存的view调用View.setLayerType(View.LAYER_TYPE_HARDWARE, null)方法。
  • 执行动画。
  • 在动画执行结束后调用View.setLayerType(View.LAYER_TYPE_NONE, null)方法来进行清除。

示例:

mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);

animator.addListener(new AnimatorListenerAdapter() {  
  @Override
  public void onAnimationEnd(Animator animation) {
    mView.setLayerType(View.LAYER_TYPE_NONE, null);
  }
});

animator.start();  

但是如果在4.0.x的版本中使用上面的代码会本亏,必须要把setLayerType放到Runnable中。如下:

mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);

animator.addListener(new AnimatorListenerAdapter() {  
  @Override
  public void onAnimationEnd(Animator animation) {
    //This will work successfully
    post(new Runnable() {
        @Override
        public void run () {
            setLayerType(LAYER_TYPE_NONE, null);
        }
    }
  }
});

animator.start();  

如果你基于minSdkVersion 16以上并且使用ViewPropertyAnimator时,你可以使用withLayer()方法替代如上的操作:

mView.animate().translationX(150).withLayer().start();

或者在api 14以上时使用ViewCompat.animate().withLayer()
这样做,你的动画就会变得更流畅!

注意事项

你应该知道,事情没那么简单。
Hardware layers有着惊人的提升动画性能的能力。然而,如果滥用,它的危害更大。不要盲目的使用layers

  • 首先,在有些情况下,hardware layers除了view渲染外还会执行更多的工作。缓存layer将会需要时间,因为首选第一步就需要两个过程: 先将这些view渲染到GPU的一个layer中然后GPU再渲染该layerWindow上。如果要渲染的View非常简单(例如一个纯色值),那么这样在初始化的时候就会增加Hardware Layer不必要的开销。

  • 其次,对所有的缓存来讲,都有一个缓存失效的可能性。任何时候如果在动画过程中调用view.invalidate(),那么layer就必须要重新渲染。经常的废弃hardware layers会比没有layers的情况下更糟糕,因为如同上面讲到的hardware layers在设置缓存时会有额外的开销。如果你需要经常的重新缓存layer,那就会有极大的损害。

    这个问题也是非常容易出现的,因为动画经常有多个移动的部分。假如现在有一个三个部分移动的动画:

    Parent ViewGroup
    —-> Child View1 (往左移动)
    —-> Child View2 (往右移动)
    —-> Child View3 (往上移动)

    如果你只在父布局ViewGroup上设置一个layer,那就将经常的缓存失效,因为ViewGroup会随着子View不断地改变。然而对每个单独的子Views而言,他们只是在位移。这种情况下,最好是对每个子View上设置Hardware Layer(而不是在父布局上)。

    再次重申,通常是对多个子View上适当的设置Hardware Layer,这样他们就不会在动画运行时失效。

    在手机开发者选项中的显示硬件层更新(Show hardware layers updates)功能是追踪这个问题的开发利器。当View渲染Hardware Layer的时候闪烁绿色,它应该在动画开始的时候闪烁一次(也就是Layer渲染初始化的时候),然而,如果你的View在整个动画期间都是绿色,那就是遇到失效的问题了。

  • 最后,hardware layers使用GPU内存,你当然不想出现内存泄漏的问题。所以你应该在必要的时候再去使用hardware layers,就想播放动画时。

这里也没有硬性规则。Android渲染系统是非常复杂的。就像所有性能问题一样,测试才是关键。通过使用“显示硬件层更新”开发者选项来确定layers问题之后,再优化。

作者:xiangzhihong8 发表于2017/8/21 14:05:13 原文链接
阅读:0 评论:0 查看评论

React Native入门(十)之导航组件React Navigation(2)TabNavigator

$
0
0

前言

在上一篇博客,了解了React Navigation导航库的StackNavigator的用法,主要用来页面的跳转和标题栏的设置!本篇就来了解一下这个导航库中第二个组件TabNavigator的用法!

使用

TabNavigator组件呢,看名字Tab就大致可以知道它是一个标签式的导航组件。这个组件呢在Android和iOS平台有不同的显示效果,而且呢在Android平台默认是在上边,(非常类似TabLayout),在iOS平台是在下边显示!
好,下边来看一下简单的使用:

import {TabNavigator} from "react-navigation";//先引入组件

class NewsScreen extends Component {
  render() {
    return <FlatList
      style={{padding: 10}}
      data={[{key: '第一条新闻'},
        {key: '第二条新闻'},
        {key: '第三条新闻'},
        {key: '第四条新闻'},
        {key: '第五条新闻'},
      ]}
      renderItem={({item}) => {
        return (
          <Text style={{padding: 5, fontSize: 15}}>{item.key}</Text>
        )
      }}
    />
  }
}

class VideoScreen extends Component {
  render() {
    return <Text>视频列表</Text>
  }
}

//设置TabNavigator
const MainScreenNavigator = TabNavigator({
  新闻: {screen: NewsScreen},
  视频: {screen: VideoScreen},
})

AppRegistry.registerComponent('AwesomeProject', () => MainScreenNavigator );

使用呢,非常简单,首先引入组件,然后写两个Tab页面,然后使用TabNavigator()设置路由即可!
这里写图片描述
一个简单的TabNavigator导航就完成了!

配置

当然,我们在具体使用的时候不会就这么简单的使用默认的样式等内容,我们可以自己配置需要的内容!

TabNavigator(RouteConfigs, TabNavigatorConfig)

RouteConfigs路由设置

路由设置呢,这里就不再提了,具体的跟前边 StackNavigator中的路由设置配置一样!
需要了解的可以参考上一篇文章!或者到官网查看相关API!

TabNavigatorConfig标签导航设置

  • tabBarComponent - 自定义组件来设置自己的tab bar
  • tabBarPosition - 设置tab bar的位置, 可以是 ‘top’ 或者 ‘bottom’
  • swipeEnabled - 是否允许滑动切换tab
  • animationEnabled - 在切换tab时是否展示动画
  • lazy - 是否懒加载
  • tabBarOptions - tab bar的设置选项

下边的可选项可传递到潜在路由来改变导航逻辑:

  • initialRouteName - 第一次加载时初始化tab的路由名字
  • order - 定义tabs顺序的路由名字数组
  • paths - 重写routeConfigs.
  • backBehavior-设置返回键的行为

tabBarOptions选项

在我们自己没有设置tabBarComponent的情况下,默认iOS平台下是TabBarBottom , Android平台下是TabBarTop !两个平台的tabbar设置的参数不同,这里说一下Android平台的TabBarTop可以设置的参数,iOS平台的设置参数可以到官网查看:tabBarOptions for TabBarBottom (default tab bar on iOS)

下面是Android平台默认的TabBarTop可以设置的参数:

  • activeTintColor - 选中标签的label和icon颜色
  • inactiveTintColor - 非选中标签的label和icon颜色
  • showIcon - 是否显示tab的icon,默认为false
  • showLabel - 是否显示tab的label,默认为true
  • upperCaseLabel - 是否将label显示为大写,默认为true(这里要是英文的话,会全部大写)
  • pressColor - 按压tab时的波纹效果颜色(Android >= 5.0 only)
  • pressOpacity - 按压tab时的透明度(iOS and Android < 5.0 only)
  • scrollEnabled - 设置是否可以滑动
  • tabStyle - 设置每一个tab的样式
  • indicatorStyle - 设置tab下边指示器的样式
  • labelStyle - 设置tab的label的样式
  • iconStyle - 设置tab的icon的样式
  • style - 设置整个tab bar的样式

好了,上边基本上就是TabNavigator全部的内容了!
下面,我们结合StackNavigator来看一个完整的例子:
先来看一下效果:
这里写图片描述

demo:

import {TabNavigator} from "react-navigation";//引入

class NewsScreen extends Component {
  render() {
    const {navigate} = this.props.navigation;
    return <FlatList
      style={{padding: 10}}
      data={[{key: '第一条新闻'},
        {key: '第二条新闻'},
        {key: '第三条新闻'},
        {key: '第四条新闻'},
        {key: '第五条新闻'},
      ]}
      renderItem={({item}) => {
        return (
          <TouchableOpacity
            activeOpacity={0.5}
            //点击列表,跳转页面且传值
            onPress={() => navigate('NewsContent', {hello: item.key})}>
            <Text style={{padding: 5, fontSize: 15}}>{item.key}</Text>
          </TouchableOpacity>
        )
      }}
    />
  }
}

class NewsContentScreen extends Component {
  render() {
    const {params} = this.props.navigation.state;
    return (
      <View style={{padding: 10}}>
        //得到传递的数据
        <Text style={{fontSize: 20, fontWeight: 'bold', alignSelf: 'center'}}>{params.hello}</Text>
        <Text>
          我是具体的新闻内容!{'\n'}
          啊啊啊啊啊啊啊啊啊啊啊啊{'\n'}
          ...
        </Text>
      </View>
    );
  }
}

class VideoScreen extends Component {
  render() {
    return <Text>视频列表</Text>
  }
}

//设置TabNavigator
const MainScreenNavigator = TabNavigator({
  新闻: {screen: NewsScreen},
  视频: {screen: VideoScreen},
},{
  tabBarPosition:'top',//设置位置
  tabBarOptions: {
    activeTintColor:'firebrick',//设置选中标签颜色
    inactiveTintColor:'gray',//设置非选择标签颜色
    labelStyle: {//设置标签label样式
      fontSize: 15,
    },
    indicatorStyle:{//设置标签指示器样式
      backgroundColor:'firebrick'
    },
    /*tabStyle: {
      width: 100,//设置每一个tab的宽度,如果不设置则默认均分
    },*/
    style: {//设置整个tab bar的样式
      backgroundColor: 'white',
    },
  }
});

//设置StackNavigator
const SimpleApp = StackNavigator({
  Home: {
    screen: MainScreenNavigator,
    navigationOptions: {
      title: '今日头条',//设置导航栏的标题
      headerTintColor:'white',//设置header的渲染颜色
      /*headerTitleStyle:{
        color:'white'  //这里和上边设置颜色是一样的,用其中一个即可
      },*/
      headerStyle:{//设置header的样式
        backgroundColor:'firebrick',
      }
    }
  },
  NewsContent: {
    screen: NewsContentScreen,
    navigationOptions: ({navigation}) => ({
      title: `${navigation.state.params.hello}`,
    }),
  },
});

AppRegistry.registerComponent('AwesomeProject', () => SimpleApp);

结语

本篇文章了解了导航组件库React Navigation中TabNavigator标签组件的使用,并结合上一篇中的StackNavigator写了一个简单的demo。下一篇文章会来了解一下最后一个DrawerNavigator的用法!
好了,先这样,下一篇再见!

作者:aiynmimi 发表于2017/8/21 14:33:06 原文链接
阅读:10 评论:0 查看评论

React Native组件(四)TextInput组件解析

$
0
0

相关文章
React Native探索系列
React Native组件系列

1 概述

TextInput组件和Text组件类似,内部都没有使用FlexBox布局,不同的是TextInput组件支持文字的输入,因为支持文字输入, TextInput组件要比Text组件多了一些属性和方法。TextInput组件支持Text组件所有的Style属性,而TextInput组件本身是没有特有的Style属性的。

2 属性

TextInput组件支持所有的View组件的属性,除此之外,它还有许多其他属性。

2.1 onChangeText

当输入框的内容发生变化时,就会调用onChangeText。
index.android.js

import React, {Component} from 'react';
import {AppRegistry, StyleSheet, View, TextInput, Button,Alert} from 'react-native';
class TextApp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            searchText: ""
        }
    }
    render() {
        return (
            <View style={styles.container}>
                <View style={styles.searchBar}>
                    <TextInput style={styles.input} placeholder='请输入内容'
                               onChangeText={(text) => {
                                   this.setState({searchText: text});
                               }}/>
                    <Button style={styles.button} title='搜索'
                            onPress={ () => {
                                Alert.alert('输入的内容为:' + this.state.searchText);
                            }
                            }/>
                </View>
            </View>
        );
    }
}
const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    searchBar: {
        flexDirection: 'row',
        height: 45,
        justifyContent: 'center',
        alignItems: 'center'
    },
    input: {
        flex: 1,
        borderColor: 'gray'
    },
    button: {
        flex: 1
    }
});
AppRegistry.registerComponent('ViewSample', () => TextApp);

上面的例子我们用到了TextInput组件的onChangeText属性,当我们在TextInput中输入内容时,这个内容就会通过onChangeText的参数text传递回来,在onChangeText中将text的内容保存到state中。当我们点击Button时,通过Alert将state中保存的内容展现出来。
运行程序效果如下图所示。

在输入框中输入Android,点击搜索Button,可以看到输入的Android展示到了Alert中。

2.2 onChange

当输入框的内容发生变化时,也会调用onChange,只不过它所返回的参数是一个event,我们来改写2.1的代码:

...
 <TextInput style={styles.input} placeholder='请输入内容' keyboardType='default'
                               onChange={(event) => {
                                   this.setState({searchText: event.nativeEvent.text});
                               }}/>
...                               

通过event.nativeEvent.text可以得到用户输入的内容,如果只是想要得到用户输入的内容,还是用onChangeText比较合适。

2.3 keyboardType

keyboardType用于设置弹出软键盘的类型。它的取值为范围为: enum(‘default’, ‘email-address’, ‘numeric’, ‘phone-pad’, ‘ascii-capable’, ‘numbers-and-punctuation’, ‘url’, ‘number-pad’, ‘name-phone-pad’, ‘decimal-pad’, ‘twitter’, ‘web-search’) ,其中default、numeric、email-address和phone-pad是跨平台。

...
 <TextInput style={styles.input} placeholder='请输入内容' keyboardType='phone-pad'
                               onChangeText={(text) => {
                                   this.setState({searchText: text}); 
                               }}/>
...   

将keyboardType的值设置为phone-pad,效果如下图所示。

2.4 blurOnSubmit

如果blurOnSubmit值为true,文本框会在按下提交键时失去焦点。对于单行输入框,blurOnSubmit默认值为true,多行则为false。
在单行的情况下,点击键盘上的提交按钮时,TextInput的效果如下图所示。


将blurOnSubmit设置为false:

...
 <TextInput style={styles.input} placeholder='请输入内容' blurOnSubmit={false} 
                               onChangeText={(text) => {
                                   this.setState({searchText: text});
                               }}/>
...                               

点击键盘上的提交按钮时,TextInput的效果如下图所示。

2.5 onSubmitEditing

当提交键被按下时会调用onSubmitEditing,如果multiline等于true,则此属性不可用。

...
 <TextInput style={styles.input} placeholder='请输入内容' blurOnSubmit={true} multiline={false}
                               onChangeText={(text) => {
                                   this.setState({searchText: text});
                               }}
                               onSubmitEditing={(event) => {
                                   console.log(event.nativeEvent.text);
                               }}
                    />
...                    

运行程序并在App的开发菜单中选择Debug JS Remotely,这时我们输入Android并按下提交键,在Console控制台中就会输出结果。(笔者用的是WebStorm)

2.6 returnKeyType

用于设置软键盘回车键的样式,Android平台可以使用returnKeyLabel来设置软键盘回车键的内容。
returnKeyType的取值为enum(‘done’, ‘go’, ‘next’, ‘search’, ‘send’, ‘none’, ‘previous’, ‘default’, ‘emergency-call’, ‘google’, ‘join’, ‘route’, ‘yahoo’)。
其中跨平台的取值有:done、next、search、send。
Android平台独有:none、previous。
iOS平台独有:default、emergency-call、google、join、route、yahoo。
如果我们将returnKeyType设置为go时,效果如下图所示。

returnKeyType设置为send时,效果如下图所示。

2.7 其他跨平台属性

属性名 取值 说明
autoCapitalize enum(‘none’, ‘sentences’, ‘words’, ‘characters’) 设置英文字母自动大写规则,取值分别表示:不自动大写、每句话首字母自动大写、每个单词首字母大写、全部字母自动大写
autoCorrect bool 是否会自动检测用户输入的英语单词正确性,默认值为true
autoFocus bool 如果为true,在componentDidMount后会获得焦点。默认值为false。
defaultValue string 字符初始值,当用户开始输入时,该值将改变
placeholder node 文本输入之前将呈现的字符串,多用于提示用户应该输入什么
placeholderTextColor color 文本输入之前将呈现的字符串的颜色
editable bool 是否允许修改字符,默认值为true
maxLength number 最多允许用户输入多少字符
caretHidden bool 如果为true,则隐藏光标
multiline bool 如果为true,则文本输入可以是多行的,默认值为false
secureTextEntry bool 文本框是否用于输入密码,默认值为false
selectTextOnFocus bool 如果为true,则文本框获取焦点时,组件中的内容会被自动选中
onFocus function 当文本框获得焦点的时候调用此回调函数
onEndEditing function 当文本输入结束后调用此回调函数
onLayout function 当组件挂载或者布局变化的时候调用,参数为{x, y, width, height}
onScroll function 在内容滚动时持续调用,传回参数的格式形如{ nativeEvent: { contentOffset: { x, y } } }
onSelectionChange function 长按选择文本时,选择范围变化时调用此函数,传回参数的格式形如 { nativeEvent: { selection: { start, end } } }
value string 文本框中的文字内容

2.8 Android平台独有属性

属性名 取值 说明
inlineImageLeft string 指定一个图片放置在左侧
inlineImagePadding number 左侧图片的Padding(如果有的话),以及文本框本身的Padding
numberOfLines number TextInput的行数
underlineColorAndroid string TextInput的下划线颜色
returnKeyLabel string 设置软键盘回车键的内容,优先级高于returnKeyType
disableFullscreenUI bool 值为false时(默认值),如果TextInput的输入空间小,系统可能会进入全屏文本输入模式

2.9 iOS平台独有属性

属性名 取值 说明
clearButtonMode enum(‘never’, ‘while-editing’, ‘unless-editing’, ‘always’) 何时在文本框右侧显示清除按钮
clearTextOnFocus bool 如果为true,每次开始输入的时候都会清除文本框的内容
keyboardAppearance enum(‘default’, ‘light’, ‘dark’) 键盘的颜色
onKeyPress function 一个键被按下的时候调用此回调,传递给回调函数的参数为{ nativeEvent: { key: keyValue } }
spellCheck bool 如果为false,则禁用拼写检查的样式(比如红色下划线)
enablesReturnKeyAutomatically bool 如果为true,键盘会在文本框内没有文字的时候禁用确认按钮 ,默认值为false

3 方法

clear()

clear用于清空输入框的内容。
想要使用组件的方法则需要使用组件的引用,例子如下所示。

...
render() {
        return (
            <View style={styles.container}>
                <View style={styles.searchBar}>
                    <TextInput style={styles.input} ref="textInputRefer" placeholder='请输入内容' blurOnSubmit={true}
                               returnKeyType='send'
                               onChangeText={(text) => {
                                   this.setState({searchText: text});
                               }}
                    />
                    <Button style={styles.button} title='清除'
                            onPress={ () => {
                                this.refs.textInputRefer.clear();
                            }
                            }/>
                </View>
            </View>
        );
    }
 ...   

在TextInput标签中定义引用的名称:ref="textInputRefer",这样我们通过 this.refs.textInputRefer就可以得到TextInput 组件的引用。在Button的onPress函数中,调用了TextInput的clear方法,这样当我们点击“清除”按钮时,文本框中的内容就会被清除。

isFocused(): boolean

返回值表明当前输入框是否获得了焦点。

好了,到这里TextInput组件就介绍到这里,还有一些没有列出的属性请查看官方文档

参考资料
官方文档
《React Native跨平台移动应用开发》第二版
《React Native 移动开发实战》
React Native-组件的引用


欢迎关注我的微信公众号,第一时间获得博客更新提醒,以及更多成体系的Android相关原创技术干货。
扫一扫下方二维码或者长按识别二维码,即可关注。

作者:itachi85 发表于2017/8/21 12:41:44 原文链接
阅读:31 评论:0 查看评论

Android开发学习(14)Popupwindow右上角弹出菜单

$
0
0

很多安卓app的右上角都有弹出菜单,今天我们继续丰富我们的计算机app,前几篇博客我们为他增加了历史记录的功能,今天我们使用Popupwindow做一个右上角弹出的按钮。
效果:
pop

布局

按钮主界面

menu_popwindow.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <ImageView
            android:layout_width="18sp"
            android:src="@drawable/ic_up"
            android:layout_gravity="right"
            android:layout_marginBottom="-3sp"
            android:layout_marginRight="10sp"
            android:layout_height="18sp"/>
    <ListView
            android:id="@+id/lv_toptitle_menu"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/background_ground" />


</LinearLayout>

menu_popwindow_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <ImageView
            android:id="@+id/iv_menu_item"
            android:layout_width="18dp"
            android:layout_height="18dp"
            android:layout_gravity="center_vertical"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:src="@drawable/ic_setting" />
    <TextView
            android:id="@+id/tv_menu_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_gravity="center_vertical"
            android:text="测试"
            android:textColor="@color/background_ground"
            android:textSize="18sp" />


</LinearLayout>

按钮动画

style.xml

<style name="PopupAnimation" parent="android:Animation" mce_bogus="1">
        <item name="android:windowEnterAnimation">@anim/popup_enter</item>
        <item name="android:windowExitAnimation">@anim/popup_exit</item>
    </style>

弹出:
popup_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale android:fromXScale="0.6" android:toXScale="1.0"
           android:fromYScale="0.6" android:toYScale="1.0" android:pivotX="50%"
           android:pivotY="50%" android:duration="150" />
    <alpha android:interpolator="@android:anim/decelerate_interpolator"
           android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="50" />
</set>

退出:
popup_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
            android:fromXScale="1.0"
            android:toXScale="0.5"
            android:fromYScale="1.0"
            android:toYScale="0.5"
            android:pivotX="50%"
            android:pivotY="50%"
            android:duration="150" />
    <alpha
            android:interpolator="@android:anim/accelerate_interpolator"
            android:fromAlpha="1.0"
            android:toAlpha="0.0"
            android:duration="150" />
</set>

自定义Popupwindow

MenuPopwindow:

package com.xvshu.android.window;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import com.xvshu.android.R;
import com.xvshu.android.model.MenuPopwindowBean;

import java.util.List;

/**
 * Created by xvshu on 2017/8/21.
 */
public class MenuPopwindow extends PopupWindow {
    private View conentView;
    private ListView lvContent;
    public MenuPopwindow(Activity context, List<MenuPopwindowBean> list) {
        LayoutInflater inflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        conentView = inflater.inflate(R.layout.menu_popwindow, null);
        lvContent = (ListView) conentView.findViewById(R.id.lv_toptitle_menu);
        lvContent.setAdapter(new MyAdapter(context, list));
        int h = context.getWindowManager().getDefaultDisplay().getHeight();
        int w = context.getWindowManager().getDefaultDisplay().getWidth();
        // 设置SelectPicPopupWindow的View
        this.setContentView(conentView);
        // 设置SelectPicPopupWindow弹出窗体的宽
        this.setWidth(w / 3-30);
        // 设置SelectPicPopupWindow弹出窗体的高
        this.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        // 设置SelectPicPopupWindow弹出窗体可点击
        this.setFocusable(true);
        this.setOutsideTouchable(true);
        // 刷新状态
        this.update();
        // 实例化一个ColorDrawable颜色为半透明
        ColorDrawable dw = new ColorDrawable(0000000000);
        // 点back键和其他地方使其消失,设置了这个才能触发OnDismisslistener ,设置其他控件变化等操作
        this.setBackgroundDrawable(dw);
        // 设置SelectPicPopupWindow弹出窗体动画效果
        this.setAnimationStyle(R.style.PopupAnimation);
    }

    public void setOnItemClick(AdapterView.OnItemClickListener myOnItemClickListener) {
        lvContent.setOnItemClickListener(myOnItemClickListener);
    }

    class MyAdapter extends BaseAdapter {
        private List<MenuPopwindowBean> list;
        private LayoutInflater inflater;
        public MyAdapter(Context context, List<MenuPopwindowBean> list) {
            inflater = LayoutInflater.from(context);
            this.list = list;
        }
        @Override
        public int getCount() {
            return list == null ? 0 : list.size();
        }
        @Override
        public Object getItem(int position) {
            return list.get(position);
        }
        @Override
        public long getItemId(int position) {
            return position;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            Holder holder = null;
            if (convertView == null) {
                convertView = inflater.inflate(R.layout.menu_popwindow_item, null);
                holder = new Holder();
                holder.ivItem = (ImageView) convertView.findViewById(R.id.iv_menu_item);
                holder.tvItem = (TextView) convertView.findViewById(R.id.tv_menu_item);
                convertView.setTag(holder);
            } else {
                holder = (Holder) convertView.getTag();
            }
            holder.ivItem.setImageResource(list.get(position).getIcon());
            holder.tvItem.setText(list.get(position).getText());
            return convertView;
        }
        class Holder {
            ImageView ivItem;
            TextView tvItem;
        }
    }
    /**
     * 显示popupWindow
     *
     * @param parent
     */
    public void showPopupWindow(View parent) {
        if (!this.isShowing()) {
            // 以下拉方式显示popupwindow
            this.showAsDropDown(parent);
        } else {
            this.dismiss();
        }
    }
}

绑定

CalcActivity.java

public void  more(View view){
        int[] icons = {R.drawable.ic_setting, R.drawable.ic_delete};
        String[] texts = {"编辑", "删除"};
        List<MenuPopwindowBean> list = new ArrayList<MenuPopwindowBean>();
        MenuPopwindowBean bean = null;
        for (int i = 0; i < icons.length; i++) {
            bean = new MenuPopwindowBean();
            bean.setIcon(icons[i]);
            bean.setText(texts[i]);
            list.add(bean);
        }
        MenuPopwindow pw = new MenuPopwindow(this, list);
        pw.setOnItemClick(new AdapterView.OnItemClickListener(){
            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                                    int position, long id) {
                Adapter adapter=parent.getAdapter();
                MenuPopwindowBean thisBean = (MenuPopwindowBean)adapter.getItem(position);
                Log.i("more", "onItemClick: "+thisBean.getText());
            }

        });
        pw.showPopupWindow(findViewById(R.id.button_forward));
    }

点击图片后日志:
log

源码地址:
git_xvshu_android_test

总结

非常感慨,安卓的资源真丰富,也非常感触自己生活的时代,为我们自学提供了当下最好的土壤,最近从朋友圈听到了一句,自己感觉说到了当下这个时代人面临的问题“这个时代正在惩罚不学习的人”,仔细想想,个人感觉这句话,放到哪个时代都是合适的,不断学习,不断更新自己我们才能飞翔的更自由。

作者:xvshu 发表于2017/8/21 15:07:29 原文链接
阅读:0 评论:0 查看评论

Unity 动画系统-Animation

$
0
0

动画系统  -  Animation的基本属性  代码示例

public class AniceshiDemo : MonoBehaviour {

    Animation ani;    //声明Animatio 类型
	// Use this for initialization
	void Start () {
        ani = GetComponent<Animation>();

        Debug.Log("当前播放的动画剪辑是:" + ani.clip);

        Debug.Log("是否有任意动画在播放:" + ani.isPlaying);

        Debug.Log("返回指定名称的动画状态:" + ani["walk"]);

        Debug.Log("获取该动画的剪辑数量:" + ani.GetClipCount());

        Debug.Log("是否有名为()的在播放:" + ani.IsPlaying("walk"));

        AnimationState anistate = ani["walk"];  //通过this[name]的形式,获取一个  动画状态
        Debug.Log(anistate.name);               //该剪辑的名字和长度
        Debug.Log(anistate.length);    

	}
	
	// Update is called once per frame
	void Update () {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            ani.Play("walk");  //播放walk动作
        }
        if (Input.GetKeyDown(KeyCode.R))
        {
            Debug.Log("成功移除剪辑");
            ani.RemoveClip("walk");    //移除某个剪辑  (参数可以是String类型,或者AnimationClip类型)
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            ani.Rewind("walk");      //倒回  (参数可以是String类型,或者不传参数)
        }

    }
}






作者:Czhenya 发表于2017/8/21 16:22:29 原文链接
阅读:38 评论:0 查看评论

Unity 动画系统-AnimationEvent

$
0
0

两种添加事件的方式

第一种是属性面板方式:需注意的是,以前的版本点击添加事件后都弹出注册事件的窗口,现在添加事件后注册事件的窗口被放在Inspector下,,,

第二种则是代码实现了:相对比而言,属性面板能更方便、准确的控制在什么时间出发注册的事件,都可传递0个或者1个参数,只是实现的形式不同,没有本质的区别

代码实现注册动画时间的参考代码如下:

public class AnimationEventDemo : MonoBehaviour {

    Animation ani;    //声明Animation动画
    AnimationClip aniclip;     //声明aniclip动画剪辑

	// Use this for initialization
	void Start () {
        ani = GetComponent<Animation>();   //获取组件

        aniclip = ani.GetClip("walk");     //获取walk的动画剪辑,并赋值给aniclip

        //创建一个AnimationEvent 对象
        AnimationEvent anievent = new AnimationEvent();

        anievent.functionName = "Show";   //注册方法到事件

        anievent.time = aniclip.length * 0.9f;  //设置在动作中触发事件的时间

        anievent.intParameter = 5;        //传方法中相应的参数

        aniclip.AddEvent(anievent);       //将设置好的事件添加到动画

	}
	
	// Update is called once per frame
	void Update () {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            ani.Play("walk");  //播放walk动作
        }
	}

       public void Show(int i)
       {
           Debug.Log("调用注册方法show参数是:"+i);
       }
}


作者:Czhenya 发表于2017/8/21 16:35:15 原文链接
阅读:41 评论:0 查看评论

微信 Android 视频编码爬过的那些坑

$
0
0

【编者按】Android 视频相关的开发,大概一直是整个 Android 生态、以及 Android API 中,最为分裂以及兼容性问题最为突出的一部分,本文从视频编码器的选择和如何对摄像头输出的 YUV 帧进行快速预处理两方面,从实践角度解析笔者曾趟过 Android 视频编码的那些坑,希望对广大读者有所助益。

Google 针对摄像头以及视频编码相关的 API,控制力一直非常差,导致不同厂商对这两个 API 的实现有不少差异,而且从 API 的设计来看,一直以来优化也相当有限,甚至有人认为这是“Android 上最难用的 API 之一”。

以微信为例,在 Android 设备录制一个 540P 的 MP4 文件,大体上遵循以下流程:

图1 Android 视频流编码流程图

从摄像头输出的 YUV 帧经过预处理之后,送入编码器,获得编码好的 H264 视频流。

上面只是针对视频流的编码,另外还需要对音频流单独录制,最后再将视频流和音频流合成最终视频。

这篇文章主要会对视频流的编码中两个常见问题进行分析:

  • 视频编码器的选择:硬编 or 软编?
  • 如何对摄像头输出的 YUV 帧进行快速预处理:镜像、缩放、旋转?

视频编码器的选择

对于录制视频的需求,不少 App 都需要对每一帧数据进行单独处理,因此很少会直接用到 MediaRecorder 来录取视频,一般来说,会有两个选择:

  • MediaCodec
  • FFMpeg+x264/openh264

下面我们逐个进行解析。

MediaCodec

MediaCodec 是 API 16 之后 Google 推出的用于音视频编解码的一套偏底层的 API,可以直接利用硬件加速进行视频的编解码。调用的时候需要先初始化 MediaCodec 作为视频的编码器,然后只需要不停传入原始的 YUV 数据进入编码器就可以直接输出编码好的 H.264 流,整个 API 设计模型同时包含了输入端和输出端的两条队列。

因此,作为编码器,输入端队列存放的是原始 YUV 数据,输出端队列输出的是编码好的 H.264 流,作为解码器则对应相反。在调用的时候,MediaCodec 提供了同步和异步两种调用方式,但是异步使用 Callback 的方式是在 API 21 之后才加入的,以同步调用为例,一般来说调用方式大概是这样(摘自官方例子):

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

简单解释一下,通过 getInputBuffers 获取输入队列,然后调用 dequeueInputBuffer 获取输入队列空闲数组下标,注意 dequeueOutputBuffer 会有几个特殊的返回值表示当前编解码状态的变化,然后再通过 queueInputBuffer 把原始 YUV 数据送入编码器,而在输出队列端同样通过 getOutputBuffersdequeueOutputBuffer 获取输出的 H.264 流,处理完输出数据之后,需要通过 releaseOutputBuffer 把输出 buffer 还给系统,重新放到输出队列中。

关于 MediaCodec 更复杂的使用例子,可以参照 CTS 测试里面的使用方式:EncodeDecodeTest.java

从上面例子来看 MediaCodec 的确是非常原始的 API,由于 MediaCodec 底层直接调用了手机平台硬件的编解码能力,所以速度非常快,但是因为 Google 对整个 Android 硬件生态的掌控力非常弱,所以这个 API 有很多问题:

  • 颜色格式问题

MediaCodec 在初始化的时候,configure 过程中需要传入一个 MediaFormat 对象,当作为编码器使用的时候,我们一般需要在 MediaFormat 中指定视频的宽高、帧率、码率、I 帧间隔等基本信息。除此之外,还有一个重要的信息就是,指定编码器接受的 YUV 帧的颜色格式,这是由于 YUV 根据其采样比例,UV 分量的排列顺序有很多种不同的颜色格式,而对于 Android 的摄像头在 onPreviewFrame 输出的 YUV 帧格式,没有配置任何参数的情况下,基本上都是 NV21 格式,但 Google 对 MediaCodec 的 API 在设计和规范的时候,显得很不厚道,过于贴近 Android 的 HAL 层了,导致了 NV21 格式并不是所有机器的 MediaCodec 都支持这种格式作为编码器的输入格式。 因此,在初始化 MediaCodec 的时候,我们需要通过 codecInfo.getCapabilitiesForType 来查询机器上的 MediaCodec 实现具体支持哪些 YUV 格式作为输入格式。一般来说,起码在 4.4+ 的系统上,这两种格式在大部分机器上都有支持:

MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar

两种格式分别是 YUV420P 和 NV21,如果机器上只支持 YUV420P 格式,则需要先将摄像头输出的 NV21 格式先转换成 YUV420P,才能送入编码器进行编码,否则最终出来的视频就会花屏,或者颜色出现错乱。

这个算是一个不大不小的坑,基本上用 MediaCodec 进行视频编码都会遇上这个问题。

  • 编码器支持特性相当有限

如果使用 MediaCodec 来编码 H.264 视频流,对于 H.264 格式来说,会有一些针对压缩率以及码率相关的视频质量设置,典型的诸如 Profile(baseline, main, hight)、Profile Level、Bitrate mode(CBR、CQ、VBR),合理配置这些参数可以让我们在同等的码率下,获得更高的压缩率,从而提升视频的质量,Android 也提供了对应的 API 进行设置,可以设置到 MediaFormat 中:

MediaFormat.KEY_BITRATE_MODE
MediaFormat.KEY_PROFILE
MediaFormat.KEY_LEVEL

但问题是,对于 Profile、Level、Bitrate mode 这些设置,在大部分手机上都是不支持的,即使是设置了最终也不会生效,例如设置了 Profile 为 high,最后出来的视频依然还会是 Baseline、Shit 等等。

这个问题,在 7.0 以下的机器几乎是必现的,其中一个可能的原因是,Android 在源码层级 hardcode 了 Profile 的的设置:

// XXX
if (h264type.eProfile != OMX_VIDEO_AVCProfileBaseline) {
 ALOGW("Use baseline profile instead of %d for AVC recording",
     h264type.eProfile);
 h264type.eProfile = OMX_VIDEO_AVCProfileBaseline;
    }

Android 直到 7.0 之后才取消了这段地方的 Hardcode。

if (h264type.eProfile == OMX_VIDEO_AVCProfileBaseline) {
        ....
    } else if (h264type.eProfile == OMX_VIDEO_AVCProfileMain ||
            h264type.eProfile == OMX_VIDEO_AVCProfileHigh) {
        .....
    }

这个问题可以说间接导致了 MediaCodec 编码出来的视频质量偏低,同等码率下,难以获得跟软编码甚至 iOS 那样的视频质量。

  • 16 位对齐要求

前面说到,MediaCodec 这个 API 在设计的时候,过于贴近 HAL 层,这在很多 SoC 的实现上,是直接把传入 MediaCodec 的 buffer,在不经过任何前置处理的情况下就直接送入了 Soc 中。而在编码 H264 视频流的时候,由于 H264 的编码块大小一般是 16x16,于是在一开始设置视频宽高的时候,如果设置了一个没有对齐 16 的大小,例如 960x540,在某些 CPU 上,最终编码出来的视频就会直接花屏。

很明显这还是因为厂商在实现这个 API 的时候,对传入的数据缺少校验以及前置处理导致的。目前来看,华为、三星的 SoC 出现这个问题会比较频繁,其他厂商的一些早期 Soc 也有这种问题,一般来说解决方法还是在设置视频宽高的时候,统一设置成对齐 16 位就好了。

FFMpeg+x264/openh264

除了使用 MediaCodec 进行编码之外,另外一种比较流行的方案就是使用 FFmpeg + x264/OpenH264 进行软编码,FFmpeg 适用于一些视频帧的预处理。这里主要是使用 x264/OpenH264 作为视频的编码器。

x264 基本上被认为是当今市面上最快的商用视频编码器,而且基本上所有 H264 的特性都支持,通过合理配置各种参数还是能够得到较好的压缩率和编码速度的,限于篇幅,这里不再阐述 H.264 的参数配置。

OpenH264 则是由思科开源的另外一个 H264 编码器,项目在 2013 年开源,对比起 x264 来说略显年轻,不过由于思科支付买了 H.264 的年度专利费,所以对于外部用户来说,相当于可以直接免费使用了。另外,firefox 直接内置了 OpenH264,作为其在 WebRTC 中的视频编解码器使用。

但对比起 x264,OpenH264 在 H264 高级特性的支持比较差:

  • Profile 只支持到 baseline,level 5.2;
  • 多线程编码只支持 slice based,不支持 frame based 的多线程编码。

从编码效率上来看,OpenH264 的速度也并不会比 x264 快,不过其最大的好处,还是能够直接免费使用。

软硬编对比

从上面的分析来看,硬编的好处主要在于速度快,而且系统自带,不需要引入外部的库,但是特性支持有限,而且硬编的压缩率一般偏低。对于软编码来说,虽然速度较慢,但是压缩率比较高,而且支持的 H264 特性也会比硬编码多很多,相对来说比较可控。就可用性而言,在 4.4+的系统上,MediaCodec 的可用性是能够基本保证的,但是不同等级机器的编码器能力会有不少差别,建议可以根据机器的配置,选择不同的编码器配置。

YUV 帧的预处理

根据最开始给出的流程,在送入编码器之前,我们需要先对摄像头输出的 YUV 帧进行一些前置处理。

缩放

如果设置了 Camera 的预览大小为 1080P,在 onPreviewFrame 中输出的 YUV 帧直接就是 1920x1080 的大小,如果需要编码跟这个大小不一样的视频,我们就需要在录制的过程中,实时的对 YUV 帧进行缩放。

以微信为例,摄像头预览 1080P 的数据,需要编码 960x540 大小的视频。

最为常见的做法是使用 FFmpeg 的 swsscale 函数进行直接缩放,效果/性能比较好的一般是选择 SWSFAST_BILINEAR 算法:

mScaleYuvCtxPtr = sws_getContext(
                   srcWidth,
                   srcHeight,
                   AV_PIX_FMT_NV21,
                   dstWidth,
                   dstHeight,
                   AV_PIX_FMT_NV21,
                   SWS_FAST_BILINEAR, NULL, NULL, NULL);
sws_scale(mScaleYuvCtxPtr,
                    (const uint8_t* const *) srcAvPicture->data,
                    srcAvPicture->linesize, 0, srcHeight,
                    dstAvPicture->data, dstAvPicture->linesize);

在 Nexus 6P 上,直接使用 FFmpeg 来进行缩放的时间基本上都需要 40ms+,对于我们需要录制 30fps 的来说,每帧处理时间最多就 30ms,如果光是缩放就消耗了如此多的时间,基本上录制出来的视频只能在 15fps 上下了。

很明显,直接使用 FFmpeg 进行缩放实在太慢了,不得不说 swsscale 在 FFmpeg 里面不适用。经对比了几种业界常用的算法之后,我们最后考虑使用快速缩放的算法,如图 3 所示。

我们选择一种叫做局部均值的算法,前后两行四个临近点算出最终图片的四个像素点,对于源图片的每行像素,我们可以使用 Neon 直接实现,以缩放 Y 分量为例:

const uint8* src_next = src_ptr + src_stride;
    asm volatile (
      "1:                                          \n"    
        "vld4.8     {d0, d1, d2, d3}, [%0]!        \n"
        "vld4.8     {d4, d5, d6, d7}, [%1]!        \n"
        "subs       %3, %3, #16                    \n"  // 16 processed per loop
        "vrhadd.u8   d0, d0, d1                    \n"
        "vrhadd.u8   d4, d4, d5                    \n"
        "vrhadd.u8   d0, d0, d4                    \n"
        "vrhadd.u8   d2, d2, d3                    \n"
        "vrhadd.u8   d6, d6, d7                    \n"
        "vrhadd.u8   d2, d2, d6                    \n"
        "vst2.8     {d0, d2}, [%2]!                    \n"  // store odd pixels
        "bgt        1b                             \n"
      : "+r"(src_ptr),          // %0
        "+r"(src_next),         // %1
        "+r"(dst),              // %2
        "+r"(dst_width)         // %3
      :
      : "q0", "q1", "q2", "q3"              // Clobber List
);

上面使用的 Neon 指令每次只能读取和存储 8 或者 16 位的数据,对于多出来的数据,只需要用同样的算法改成用 C 语言实现即可。

在使用上述的算法优化之后,进行每帧缩放,在 Nexus 6P 上,只需要不到 5ms 就能完成了,而对于缩放质量来说,FFmpeg 的 SWSFASTBILINEAR 算法和上述算法缩放出来的图片进行对比,峰值信噪比(psnr)在大部分场景下大概在 38-40 左右,质量也足够好。

旋转

在 Android 机器上,由于摄像头安装角度不同,onPreviewFrame 出来的 YUV 帧一般都是旋转了 90 度或者 270 度,如果最终视频是要竖拍的,那一般来说需要把 YUV 帧进行旋转。

对于旋转的算法,如果是纯 C 实现的代码,一般来说是个 O(n2 )复杂度的算法,如果是旋转 960x540 的 YUV 帧数据,在 Nexus 6P 上,每帧旋转也需要 30ms+,这显然也是不能接受的。

在这里我们换个思路,能不能不对 YUV 帧进行旋转?显当然是可以的。

事实上在 MP4 文件格式的头部,我们可以指定一个旋转矩阵,具体来说是在 moov.trak.tkhd box 里面指定,视频播放器在播放视频的时候,会读取这里的矩阵信息,从而决定视频本身的旋转角度、位移、缩放等,具体可以参考苹果的文档

通过 FFmpeg,我们可以很轻松的给合成之后的 mp4 文件打上这个旋转角度:

char rotateStr[1024];
sprintf(rotateStr, "%d", rotate);
av_dict_set(&out_stream->metadata, "rotate", rotateStr, 0);

于是可以在录制的时候省下一大笔旋转的开销。

镜像

在使用前置摄像头拍摄的时候,如果不对 YUV 帧进行处理,那么直接拍出来的视频是会镜像翻转的,这里原理就跟照镜子一样,从前置摄像头方向拿出来的 YUV 帧刚好是反的,但有些时候拍出来的镜像视频可能不合我们的需求,因此这个时候我们就需要对 YUV 帧进行镜像翻转。

但由于摄像头安装角度一般是 90 度或者 270 度,所以实际上原生的 YUV 帧是水平翻转过来的,因此做镜像翻转的时候,只需要刚好以中间为中轴,分别上下交换每行数据即可,注意 Y 跟 UV 要分开处理,这种算法用 Neon 实现相当简单:

asm volatile (
      "1:                                          \n"
        "vld4.8     {d0, d1, d2, d3}, [%2]!        \n"  // load 32 from src
        "vld4.8     {d4, d5, d6, d7}, [%3]!        \n"  // load 32 from dst
        "subs       %4, %4, #32                    \n"  // 32 processed per loop
        "vst4.8     {d0, d1, d2, d3}, [%1]!        \n"  // store 32 to dst
        "vst4.8     {d4, d5, d6, d7}, [%0]!        \n"  // store 32 to src
        "bgt        1b                             \n"
      : "+r"(src),   // %0
        "+r"(dst),   // %1
        "+r"(srcdata), // %2
        "+r"(dstdata), // %3
        "+r"(count)  // %4  // Output registers
      :                     // Input registers
      : "cc", "memory", "q0", "q1", "q2", "q3"  // Clobber List
    );

同样,剩余的数据用纯 C 代码实现就好了, 在 Nexus 6P 上,这种镜像翻转一帧 1080x1920 YUV 数据大概只要不到 5ms。

在编码好 H.264 视频流之后,最终处理就是把音频流跟视频流合流然后包装到 mp4 文件,这部分我们可以通过系统的 MediaMuxer、mp4v2,或者 FFmpeg 来实现,这部分比较简单,在这里就不再阐述了。

参考文献

  1. 雷霄骅(leixiaohua1020)的专栏 ,大名鼎鼎雷神的博客,里面有非常多关于音视频编码/FFmpeg 相关的学习资料,入门必备。也祈愿他能够在天堂安息吧。
  2. Android MediaCodec stuff,包含了一些 MediaCodec 使用的示例代码,初次使用可以参考下这里。
  3. Coding for NEON,一个系列教程,讲述了一些常用 Neon 指令使用方法。上面在介绍缩放的时候使用到了 Neon,事实上大部分音视频处理过程都会使用到,以 yuv 帧处理为例,缩放,旋转,镜像翻转都可以使用 Neon 来做优化。
  4. libyuv,Google 开源的一个 YUV 处理库,上面只针对 1080p->540p 视频帧缩放的算法,而对于通用的压缩处理,可以直接使用这里的实现,对比起 FFmpeg 的速度快上不少。
  • 作者:周俊杰,微信 Android 客户端开发工程师,常年维护音视频相关模块的开发以及各类兼容性问题的处理,负责微信支付相关需求的开发。
  • 责编:唐门教主(tangxy@csdn.net)
  • 声明:本文为 CSDN《程序员》原创文章,未经许可,请勿转载,如需转载,请留言。

作者:Byeweiyang 发表于2017/8/21 18:21:58 原文链接
阅读:320 评论:3 查看评论

Unity Shader: 优化GPU代码--用step()代替if else等条件语句。

$
0
0

普通的卡通着色Shader:

先看一个Shader,卡通着色。由于卡通着色需要对不同渲染区域进行判定,比较适合做案例。

Shader "Unlit/NewToonShading"
{
    Properties
    {
        _Shininess("Shininess",float)=1
        _Edge("Edge Scale",range(0,1))=0.2
        _FinalColor("Final Color",Color)=(0.5,0.5,0.5,1)
        _EdgeColor("Edge Color",Color)=(0,0,0,1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque"}
        LOD 100

        Pass
        {
            Tags {"LightMode"="Vertex" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal:NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 N:TEXCOORD0;
                float3 L:TEXCOORD1;
                float3 H:TEXCOORD2;
                float3 V:TEXCOORD3;
            };

            float _Shininess;
            float _Edge;
            float4 _FinalColor;
            float4 _EdgeColor;
            float4 _LightPosition_World;

            v2f vert (appdata v)
            {
                v2f o=(v2f)0;

                float4 worldPos=mul(unity_ObjectToWorld,v.vertex);

                float4 lightPos_World=mul(UNITY_MATRIX_I_V,unity_LightPosition[1]);

                o.N=normalize(mul(unity_ObjectToWorld,v.normal));
                o.L=normalize(lightPos_World-worldPos.xyz);

                o.V=normalize(_WorldSpaceCameraPos-worldPos.xyz);
                o.H=normalize(o.L+o.V);

                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.N=normalize(i.N);
                i.L=normalize(i.L);
                i.H=normalize(i.H);
                i.V=normalize(i.V);

                float4 Kd=_FinalColor;
                float4 Ks=0;
                fixed4 col;
                //边缘判定
                float edge=max(dot(i.N,i.V),0);

                if(edge<_Edge){
                    return _EdgeColor;
                }

                //暗光判定
                float diffuseLight=max(dot(i.N,i.L),0);

                if(diffuseLight<=0.1f){     //暗光区域
                    Kd*=0.5f;               //亮光区域亮度减半
                    Ks=0;                   //无高光  //如果diffuseLight<=0,说明N,H夹角大于了90',眼睛或光源在材质表面后方
                    col=Kd+Ks;
                    return col;
                }

                //高光判定
                float specularLight=pow(max(dot(i.N,i.H),0),_Shininess);

                if(specularLight>=0.95f){
                    Ks=float4(1.0f,1.0f,1.0f,0.0f);     //高光
                }

                col=Kd+Ks;
                return col;
            }
            ENDCG
        }
    }
}

这里写图片描述
(上图:渲染结果)

优化的原理:

在片段着色器中,我以正常cpu编程的逻辑进行了优化,例如,if(edge<_Edge){return _EdgeColor;},如果此像素被判定为边缘,则直接返回边缘颜色,那么则不用再进行之后的运算了。以此类推后面又用if else 分别进行了高光,亮光,暗光区的判断。但是这种优化对于gpu编程来讲是无效的。因为对于GPU来讲,各个顶点各个像素都在进行大量的并行运算,每个片段着色器都在同步运行,边缘地带像素的片段着色器虽然率先return,但是它依然要等待最后一个return的像素。只有所有像素全部完成计算,才会进行下一次运算,因此在GPU编程中,if else, switch case等条件语句和太复杂的逻辑是不推荐的。相应的,可以用step()等函数进行替换,用阶梯函数的思维来构建条件语句。这样,所有的线程都执行完全一样的代码,在很多方面对GPU都是有益的。

优化后的Shader:

上面Shader的Step()函数版本:

Shader "Unlit/NewToonShading_StepVersion"
{
    Properties
    {
        _Shininess("Shininess",float)=1
        _Edge("Edge Scale",range(0,1))=0.2
        _FinalColor("Final Color",Color)=(0.5,0.5,0.5,1)
        _EdgeColor("Edge Color",Color)=(0,0,0,1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque"}
        LOD 100

        Pass
        {
            Tags {"LightMode"="Vertex" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal:NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 N:TEXCOORD0;
                float3 L:TEXCOORD1;
                float3 H:TEXCOORD2;
                float3 V:TEXCOORD3;
            };

            float _Shininess;
            float _Edge;
            float4 _FinalColor;
            float4 _EdgeColor;
            float4 _LightPosition_World;

            v2f vert (appdata v)
            {
                v2f o=(v2f)0;

                float4 worldPos=mul(unity_ObjectToWorld,v.vertex);

                float4 lightPos_World=mul(UNITY_MATRIX_I_V,unity_LightPosition[1]);

                o.N=normalize(mul(unity_ObjectToWorld,v.normal));
                o.L=normalize(lightPos_World-worldPos.xyz);

                o.V=normalize(_WorldSpaceCameraPos-worldPos.xyz);
                o.H=normalize(o.L+o.V);

                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                i.N=normalize(i.N);
                i.L=normalize(i.L);
                i.H=normalize(i.H);
                i.V=normalize(i.V);

                float4 Kd=_FinalColor;
                float4 Ks=0;
                fixed4 col;
                //边缘判定
                float edge=max(dot(i.N,i.V),0);

                edge=step(edge,_Edge); //if(edge<=_Edge) edge=1 , else edge=0

                _EdgeColor*=edge;

                //高光判定
                float specularLight=pow(max(dot(i.N,i.H),0),_Shininess);

                specularLight=step(0.95f,specularLight);        //if specularLight>=0.95f specularLight=1 else =0

                //暗光判定

                float diffuseLight=max(dot(i.N,i.L),0);

                diffuseLight=step(0.1f,diffuseLight); //if(diffuseLight>=0.1f) diffuseLight=1   else diffuseLight=0

                Ks=specularLight*diffuseLight;      //if diffuseLight=0, Ks=0; else Ks=specularLight(1 or 0)

                diffuseLight=diffuseLight*0.5f+0.5f;      //change 1 or 0 to 1 or 0.5

            //0.5Kd or Kd  1or0     1or0    0or1    0orEdgeColor    
                col=(Kd*diffuseLight+Ks)*(1.0f-edge)+_EdgeColor;        
                return col;
            }
            ENDCG
        }
    }
}

举例解释:

在HLSL中, step(a,b)既是当b>=a时返回1,否则返回0,换句话说既是当a<=b时返回1,否则返回0。因此可以把被比较数灵活的插入a或b的位置,完成小于或大于的比较。由于返回值是0或1,它无法直接替代if else逻辑判断,但是可以通过改造算法完成,例如:

                //边缘判定
                float edge=max(dot(i.N,i.V),0);

                if(edge<_Edge){
                    return _EdgeColor;
                }

上文中,直接返回的_EdgeColor,将在下文中变为一个000或保持自身值的rgb变量,edge会变为0或1,并在最后的计算步骤中参与最终颜色的计算:

                //边缘判定
                float edge=max(dot(i.N,i.V),0);

                edge=step(edge,_Edge); //if(edge<=_Edge) edge=1 , else edge=0

                _EdgeColor*=edge;
                //...中间过程略...
                            //0.5Kd or Kd  1or0     1or0    0or1    0orEdgeColor    
                col=(Kd*diffuseLight+Ks)*(1.0f-edge)+_EdgeColor;

如果此像素为边缘,edge为1,那么在最终颜色计算中,不论其他变量如何,它都会变为一个0+_EdgeColor的值,既是边缘颜色。如果此像素为非边缘地带,edge为0,_EdgeColor为0,那么最终颜色为 “其他颜色”*1+0,边缘颜色被剔除。

以此类推,原版中高光,亮光与暗光区域判断的返回值也都变成了变量放入最终颜色计算中。具体推理分析请借助step()版本各行后面注释。

测试

这里写图片描述

这里写图片描述

两个版本的FPS小幅波动基本相同,有可能是计算量太小或此Shader内容对此问题不太敏感,但起码证明if else版本按照CPU的思维提前返回相对于step()版本进行所有的计算是无起到任何优势的。

汇编版本:

汇编后的片段着色器代码(部分截取):
if else版本:

   0: dp3 r0.x, v1.xyzx, v1.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v1.xyzx
   3: dp3 r0.w, v4.xyzx, v4.xyzx
   4: rsq r0.w, r0.w
   5: mul r1.xyz, r0.wwww, v4.xyzx
   6: dp3 r0.w, r0.xyzx, r1.xyzx
   7: max r0.w, r0.w, l(0.000000)
   8: lt r0.w, r0.w, cb0[2].y
   9: if_nz r0.w
  10:   mov o0.xyzw, cb0[4].xyzw
  11:   ret 
  12: endif 
  13: dp3 r0.w, v2.xyzx, v2.xyzx
  14: rsq r0.w, r0.w
  15: mul r1.xyz, r0.wwww, v2.xyzx
  16: dp3 r0.w, r0.xyzx, r1.xyzx
  17: max r0.w, r0.w, l(0.000000)
  18: ge r0.w, l(0.100000), r0.w
  19: if_nz r0.w
  20:   mul o0.xyzw, cb0[3].xyzw, l(0.500000, 0.500000, 0.500000, 0.500000)
  21:   ret 
  22: endif 
  23: dp3 r0.w, v3.xyzx, v3.xyzx
  24: rsq r0.w, r0.w
  25: mul r1.xyz, r0.wwww, v3.xyzx
  26: dp3 r0.x, r0.xyzx, r1.xyzx
  27: max r0.x, r0.x, l(0.000000)
  28: log r0.x, r0.x
  29: mul r0.x, r0.x, cb0[2].x
  30: exp r0.x, r0.x
  31: ge r0.x, r0.x, l(0.950000)
  32: and r0.xyzw, r0.xxxx, l(0x3f800000, 0x3f800000, 0x3f800000, 0)
  33: add o0.xyzw, r0.xyzw, cb0[3].xyzw
  34: ret 

step()版本:

   0: dp3 r0.x, v3.xyzx, v3.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v3.xyzx
   3: dp3 r0.w, v1.xyzx, v1.xyzx
   4: rsq r0.w, r0.w
   5: mul r1.xyz, r0.wwww, v1.xyzx
   6: dp3 r0.x, r1.xyzx, r0.xyzx
   7: max r0.x, r0.x, l(0.000000)
   8: log r0.x, r0.x
   9: mul r0.x, r0.x, cb0[2].x
  10: exp r0.x, r0.x
  11: ge r0.x, r0.x, l(0.950000)
  12: dp3 r0.y, v2.xyzx, v2.xyzx
  13: rsq r0.y, r0.y
  14: mul r0.yzw, r0.yyyy, v2.xxyz
  15: dp3 r0.y, r1.xyzx, r0.yzwy
  16: max r0.y, r0.y, l(0.000000)
  17: ge r0.y, r0.y, l(0.100000)
  18: and r0.xz, r0.xxyx, l(0x3f800000, 0, 0x3f800000, 0)
  19: movc r0.y, r0.y, l(1.000000), l(0.500000)
  20: mul r0.x, r0.z, r0.x
  21: mad r0.xyzw, cb0[3].xyzw, r0.yyyy, r0.xxxx
  22: dp3 r1.w, v4.xyzx, v4.xyzx
  23: rsq r1.w, r1.w
  24: mul r2.xyz, r1.wwww, v4.xyzx
  25: dp3 r1.x, r1.xyzx, r2.xyzx
  26: max r1.x, r1.x, l(0.000000)
  27: ge r1.x, cb0[2].y, r1.x
  28: movc r0.xyzw, r1.xxxx, l(0,0,0,0), r0.xyzw
  29: and r1.x, r1.x, l(0x3f800000)
  30: mad o0.xyzw, cb0[4].xyzw, r1.xxxx, r0.xyzw
  31: ret 
作者:liu_if_else 发表于2017/8/21 20:00:37 原文链接
阅读:40 评论:0 查看评论

Chromium插件(Plugin)执行3D渲染的过程分析

$
0
0

       Chromium为网页的<embed>标签创建了Plugin之后,Plugin就负责渲染<embed>标签的内容。Chromium为Plugin提供了OpenGL接口,使得Plugin可在网页上渲染3D内容。当然,我们也可通过WebGL接口在网页上渲染3D内容。不过,前者渲染效率会更高些,因为它是Native接口,后者是JavaScript接口。本文接下来就详细分析Plugin执行3D渲染的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       我们分析Plugin执行3D渲染的过程,更多的是为了理解Chromium与Plugin的交互过程,包括Chromium操作Plugin的过程,以及Plugin调用Chromium提供的接口的过程。接下来我们就以Chromium插件(Plugin)机制简要介绍和学习计划一文提到的GLES2 Example为例,分析Plugin执行3D渲染的过程。

       从前面Chromium网页加载过程简要介绍和学习计划这个系列的文章可以知道,WebKit会将网页抽象为一个DOM Tree。网页中的每一个<embed>标签在这个DOM Tree中都会有一个对应的节点。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章又可以知道,网页的DOM Tree又会被Chromium转化为一个CC Layer Tree。其中,DOM Tree中的<embed>节点将对应于CC Layer Tree中的一个Texture Layer,如图1所示:


图1 DOM Tree中的<embed>标签与CC Layer Tree中的Texture Layer

       Plugin在调用Chromium提供的OpenGL接口的时候,实际上是将<embed>标签的内容渲染在它在CC Layer Tree中对应的Texture Layer上。当Chromium对网页的CC Layer Tree进行绘制的时候,它内部的Texture Layer的内容就会显示在屏幕上。Texture Layer描述的实际上是一个纹理。在前面Chromium硬件加速渲染的UI合成过程分析一文中,我们提到,网页的canvas标签在CC Layer Tree同样是对应有一个Texture Layer。因此,<embed>标签对应的Texture Layer与canvas标签对应的Texture Layer显示在屏幕上的过程是一样的。这一点可以参考前面Chromium硬件加速渲染的UI合成过程分析一文。

       在Android平台上,Chromium提供给Plugin调用的OpenGL接口称为PPB_OPENGLES2_INTERFACE接口。PPB_OPENGLES2_INTERFACE接口提供了一系列的函数,每一个函数都对应于一个glXXX接口,如图2所示:


图2 Plugin调用OpenGL接口的过程

       在调用PPB_OPENGLES2_INTERFACE接口提供的函数时,GLES2Implementation类的相应成员函数会被调用。例如,当PPB_OPENGLES2_INTERFACE接口提供的函数ActiveTexture被调用时,GLES2Implementation类的成员函数ActiveTexture。GLES2Implementation类的成员函数会将要执行的GPU命令写入到一个缓冲区中去,然后通过一个PpapiCommandBufferProxy对象通知Render进程执行缓冲区中的GPU命令。Render进程又会通过一个CommandBufferProxyImpl对象将要执行的GPU命令转发给GPU进程中的一个GpuCommandBufferStub处理。这意味Plugin最终是通过GPU进程执行GPU命令的。关于GLES2Implementation、CommandBufferProxyImpl和GpuCommandBufferStub这三类执行GPU命令的过程,可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       Plugin在通过PPB_OPENGLES2_INTERFACE接口执行OpenGL函数(GPU命令)之前,首先要初始化一个OpenGL环境。这个初始化操作发生在Plugin第一次知道自己的视图大小时,也就是知道它对应的<embed>标签的视图大小时。初始化过程如图3所示:


图3 Plugin的OpenGL环境初始化过程

       Plugin的视图大小是由WebKit计算出来的。计算出来之后,WebKit会通过运行在Render进程中的Plugin Instance Proxy,也就是一个PepperPluginInstanceImpl对象,向运行Plugin进程中的Plugin Instance发出通知。Plugin Instance获得了这个通知之后,就可以初始化一个OpenGL环境了。

       Plugin Instance Proxy是通过调用PPP_INSTANCE_INTERFACE_1_1接口提供的一个函数DidChangeView向Plugin Instance发出通知的,后者又是通过向目标Plugin进程发送一个类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息发出该通知的。

       类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息携带了一个参数PpapiMsg_PPPInstance_DidChangeView。这个参数描述的是一个Routing ID,表示Plugin进程要将类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息交给一个PPP_Instance_Proxy对象处理。这个PPP_Instance_Proxy对象获得该消息后,又会在当前Plugin进程中获得一个PPP_INSTANCE_INTERFACE_1_1接口,并且调用该接口提供的函数DidChangeView。该函数会找到目标Plugin Instance,并且调用它的成员函数DidChangeView。这样,Plugin Instance就可以初始化OpenGL环境了。以我们在前面Chromium插件(Plugin)机制简要介绍和学习计划一文提到的GLES2 Example为例,它的成员函数DidChangeView就通过调用另外一个成员函数InitGL初始化OpenGL环境的。

       Plugin在初始化OpenGL环境的过程中,有两件重要的事情要做。第一件事情是创建一个OpenGL上下文,过程如图4所示:

  

图4 Plugin的OpenGL上文创建过程

       在Plugin进程中,OpenGL上下文通过Graphics3D类描述。因此,创建OpenGL上下文意味着是创建一个Graphics3D对象。这个Graphics3D对象在创建的过程中,会调用PPB_GRAPHICS_3D_INTERFACE_1_0接口提供的一个函数Create。该函数又会通过一个APP_ID_RESOURCE_CREATION接口向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息。在Plugin进程中,APP_ID_RESOURCE_CREATION接口是通过一个ResourceCreationProxy对象实现的,因此,Plugin进程实际上是通过ResourceCreationProxy类向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息的。

       Plugin进程在向Render进程发送类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息时,会指定一个参数APP_ID_PPB_GRAPHICS_3D,表示Render进程在接收到该消息后,要将其分发给一个PPB_Graphics3D_Proxy对象处理。这个PPB_Graphics3D_Proxy对象又会通过调用PPB_Graphics3D_Impl类的静态成员函数CreateRaw创建一个PPB_Graphics3D_Impl对象。这个PPB_Graphics3D_Impl对象在Render进程描述的就是一个OpenGL上下文。

       Plugin在初始化OpenGL环境的过程中做的第二件事情就是将刚刚创建出来的OpenGL上下文指定为当前要使用的OpenGL上下文。这个过程称为OpenGL上下文绑定,如图5所示:


图5 Plugin绑定OpenGL上下文的过程

       Plugin是通过调用PPB_INSTANCE_INTERFACE_1_0接口提供的函数BindGraphics进行OpenGL上下文绑定的。该函数又会通过一个APP_ID_PPB_INSTANCE接口向Render进程发送一个类型为PpapiHostMsg_PPBIntance_BindGraphics的IPC消息。在Plugin进程中,APP_ID_PPB_INSTANCE接口是通过一个PPB_Instance_Proxy对象实现的,因此,Plugin进程实际上是通过PPB_Instance_Proxy类向Render进程发送一个类型为PpapiHostMsg_PPBIntance_BindGraphics的IPC消息的。

       Plugin进程在向Render进程发送类型为PpapiHostMsg_PPBIntance_BindGraphics的IPC消息时,会指定一个参数APP_ID_PPB_INSTANCE,表示Render进程在接收到该消息后,要将其分发给一个PPB_Instance_Proxy对象处理。这个PPB_Instance_Proxy对象又会找到目标Plugin Instance在Render进程中对应的Proxy,也就是一个PepperPluginInstanceImpl对象,并且调用该PepperPluginInstanceImpl对象的成员函数BindGraphics。

       PepperPluginInstanceImpl类的成员函数BindGraphics在执行的过程中,会将指定的OpenGL上下文,也就是前面创建一个PPB_Graphics3D_Impl对象标记为被绑定,这样它接下来就会作为Plugin当前使用的OpenGL上下文了。

       OpenGL环境初始化完成之后,Plugin就可以使用图2所示的PPB_OPENGLES2_INTERFACE接口来执行3D渲染了。一帧渲染完毕,Plugin需要交换当前使用的OpenGL上下文的前后两个缓冲区,也就是执行一个SwapBuffers操作。这个操作的执行过程如图6所示:


图6 Plugin执行SwapBuffers操作的过程

      前面提到,在Plugin进程中,OpenGL上下文是通过一个Graphics3D对象描述的。这个Graphics3D对象可以通过PPB_GRAPHICS_3D_INTERFACE_1_0接口提供的成员函数SwapBuffers完成SwapBuffers操作。

      PPB_GRAPHICS_3D_INTERFACE_1_0接口提供的成员函数SwapBuffers在执行的过程中,会向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_SwapBuffers的IPC消息。这个消息携带了一个参数API_ID_PPB_GRAPHICS_3D,表示Render进程需要将该消息分发给一个PPB_Graphics3D_Proxy对象处理。这个PPB_Graphics3D_Proxy对象会找到Plugin当前绑定的OpenGL上下文,也就是一个PPB_Graphics3D_Impl对象,并且调用该PPB_Graphics3D_Impl对象的成员函数DoSwapBuffers。这时候就可以完成一个SwapBuffers操作,从而也完成一个帧的渲染流程。

      接下来,我们就从WebKit通知Plugin视图大小开始,分析Plugin执行3D渲染的完整流程。从前面Chromium插件(Plugin)模块(Module)加载过程分析一文可以知道,WebKit为<embed>标签创建了一个Plugin Instance之后,会将其放在一个由WebPluginContainerImpl类描述的Plugin View中。当<embed>标签的视图大小发生变化时,WebPluginContainerImpl类的成员函数reportGeometry就会被调用,它的实现如下所示:

void WebPluginContainerImpl::reportGeometry()
{
    ......

    IntRect windowRect, clipRect;
    Vector<IntRect> cutOutRects;
    calculateGeometry(frameRect(), windowRect, clipRect, cutOutRects);

    m_webPlugin->updateGeometry(windowRect, clipRect, cutOutRects, isVisible());

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebPluginContainerImpl.cpp中。

       WebPluginContainerImpl类的成员函数reportGeometry首先是调用成员函数calulateGeometry计算<embed>标签的视图大小,然后再将计算得到的信息告知Content层,这是通过成员变量m_webPlugin指向的一个PepperWebPluginImpl对象的成员函数updateGeometry实现的。这个PepperWebPluginImpl对象的创建过程可以参考前面Chromium的Plugin进程启动过程分析一文。

       接下来我们继续分析PepperWebPluginImpl类的成员函数updateGeometry的实现,以便了解Render进程通知Plugin它描述的<embed>标签的大小的过程,如下所示:

void PepperWebPluginImpl::updateGeometry(
    const WebRect& window_rect,
    const WebRect& clip_rect,
    const WebVector<WebRect>& cut_outs_rects,
    bool is_visible) {
  plugin_rect_ = window_rect;
  if (!instance_->FlashIsFullscreenOrPending()) {
    std::vector<gfx::Rect> cut_outs;
    for (size_t i = 0; i < cut_outs_rects.size(); ++i)
      cut_outs.push_back(cut_outs_rects[i]);
    instance_->ViewChanged(plugin_rect_, clip_rect, cut_outs);
  }
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/pepper_webplugin_impl.cc中。

       PepperWebPluginImpl类的成员变量instance_指向的是一个PepperPluginInstanceImpl对象。从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,这个PepperPluginInstanceImpl对象是用来在Render进程中描述一个Plugin Instance Proxy的。

       PepperWebPluginImpl类的成员函数updateGeometry首先调用上述PepperPluginInstanceImpl对象的成员函数FlashIsFullscreenOrPending判断当前正在处理的Plugin是否是一个Flash Plugin。如果是一个Flash Plugin,并且它当前处于全屏状态或者即将进入全屏状态,那么PepperWebPluginImpl类的成员函数updateGeometry就不会通知Plugin它描述的<embed>标签的视图大小发生了变化。

       我们假设当前正在处理的Plugin不是一个Flash Plugin。这时候PepperWebPluginImpl类的成员函数updateGeometry就会调用上述PepperPluginInstanceImpl对象的成员函数ViewChanged通知Plugin它描述的<embed>标签的视图大小发生了变化。

       PepperPluginInstanceImpl类的成员函数ViewChanged的实现如下所示:

void PepperPluginInstanceImpl::ViewChanged(
    const gfx::Rect& position,
    const gfx::Rect& clip,
    const std::vector<gfx::Rect>& cut_outs_rects) {
  ......

  view_data_.rect = PP_FromGfxRect(position);
  view_data_.clip_rect = PP_FromGfxRect(clip);
  ......

  SendDidChangeView();
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/pepper_plugin_instance_impl.cc中。

       PepperPluginInstanceImpl类的成员函数ViewChanged首先将当前正在处理的Plugin描述的<embed>标签的视图大小信息保存在成员变量view_data_描述的一个ppapi::ViewData对象中,然后再调用另外一个成员函数SendDidChangeView向运行在Plugin进程中的Plugin Instance发送一个视图大小变化通知。

       PepperPluginInstanceImpl类的成员函数SendDidChangeView的实现如下所示:

void PepperPluginInstanceImpl::SendDidChangeView() {
  ......

  ScopedPPResource resource(
      ScopedPPResource::PassRef(),
      (new PPB_View_Shared(ppapi::OBJECT_IS_IMPL, pp_instance(), view_data_))
          ->GetReference());
  ......

  if (instance_interface_) {
    instance_interface_->DidChangeView(
        pp_instance(), resource, &view_data_.rect, &view_data_.clip_rect);
  }
}

       这个函数定义在文件external/chromium_org/content/renderer/pepper/pepper_plugin_instance_impl.cc中。

       PepperPluginInstanceImpl类的成员函数SendDidChangeView首先是将成员变量view_data_描述的ppapi::ViewData对象封装在一个PPB_View_Shared对象。从前面的分析可以知道,被封装的ppapi::ViewData对象描述的当前正在处理的Plugin描述的<embed>标签的视图大小信息。封装得到的PPB_View_Shared对象接下来会传递给PPP_INSTANCE_INTERFACE_1_1接口提供的函数DidChangeView使用。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,PepperPluginInstanceImpl类的成员变量instance_interface_指向的是一个PPP_Instance_Combined对象。这个PPP_Instance_Combined对象描述的是一个PPP_INSTANCE_INTERFACE_1_1接口。运行在Render进程中的Plugin Instance Proxy可以通过这个接口与运行在Plugin进程中的Plugin Instance通信。

       PepperPluginInstanceImpl类的成员函数SendDidChangeView主要就是调用上述PPP_INSTANCE_INTERFACE_1_1接口提供的函数DidChangeView通知运行在Plugin进程中的Plugin Instance,它描述的<embed>标签的视图大小发生了变化,也就是通过调用成员变量instance_interface_指向的PPP_Instance_Combined对象的成员函数DidChangeView进行通知。

       PPP_Instance_Combined类的成员函数DidChangeView的实现如下所示:

void PPP_Instance_Combined::DidChangeView(PP_Instance instance,
                                          PP_Resource view_changed_resource,
                                          const struct PP_Rect* position,
                                          const struct PP_Rect* clip) {
  if (instance_1_1_.DidChangeView) {
    CallWhileUnlocked(
        instance_1_1_.DidChangeView, instance, view_changed_resource);
  } 

  ......
}
       这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppp_instance_combined.cc中。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,PPP_Instance_Combined类的成员变量instance_1_1_描述的是一个PPP_Instance_1_1对象。这个PPP_Instance_1_1对象描述的就是上述的PPP_INSTANCE_INTERFACE_1_1接口。PPP_Instance_Combined类的成员函数DidChangeView通过一个帮助函数CallWhilleUnlocked调用这个PPP_INSTANCE_INTERFACE_1_1接口提供的函数DidChangeView,以便向运行在Plugin进程中的Plugin Instance发出一个视图大小变化通知。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文还可以知道,上述PPP_INSTANCE_INTERFACE_1_1接口提供的函数DidChangeView实现在ppp_instance_proxy.cc文件中,如下所示:

void DidChangeView(PP_Instance instance, PP_Resource view_resource) {
  HostDispatcher* dispatcher = HostDispatcher::GetForInstance(instance);

  EnterResourceNoLock<PPB_View_API> enter_view(view_resource, false);
  ......

  dispatcher->Send(new PpapiMsg_PPPInstance_DidChangeView(
      API_ID_PPP_INSTANCE, instance, enter_view.object()->GetData(),
      flash_fullscreen));
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppp_instance_proxy.cc中。

       参数instance的类型为PP_Instance。PP_Instance描述的实际上是一个32位的有符号整数。这个32位的有符号整数是用来描述一个Plugin的ID。通过这个ID,运行在Render进程中的Plugin Instance Proxy与运行在Plugin进程中的Plugin Instance就能一一对应起来的。

       函数DidChangeView首先通过调用HostDispatcher类的静态成员函数GetForInstance获得一个HostDispatcher对象。从前面Chromium插件(Plugin)模块(Module)加载过程分析一文可以知道,每一个Plugin进程在Render进程中都有一个对应的HostDispatcher对象。这个HostDispatcher对象就是用来与它对应的Plugin进程通信的。给出一个PP_Instance,HostDispatcher类的静态成员函数GetForInstance就可以获得这个PP_Instance描述的Plugin Instance所运行在的Plugin进程对应的HostDispatcher对象。

       获得了目标Plugin进程对应的HostDispatcher对象之后,就可以向它发送一个类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息。这个IPC消息携带了四个参数:

       1. API_ID_PPP_INSTANCE,要求Plugin进程将该IPC消息分发给API_ID_PPP_INSTANCE接口处理。

       2. instance,表示目标Plugin Instance。

       3. ppapi::ViewData,表示目标Plugin Instance的当前视图大小。

       4. flash_fullscreen,表示目标Plugin Instance是否处于全屏状态。

       其中,第3个参数描述的ppapi::ViewData对象来自于前面在PepperPluginInstanceImpl类的成员函数SendDidChangeView中封装的PPB_View_Shared对象。这个PPB_View_Shared对象对象是通过函数DidChangeView的第2个参数view_resource传递进来的。

       函数DidChangeView首先将上述PPB_View_Shared对象封装在一个EnterResourceNoLock<PPB_View_API>对象中。接下来通过调用这个EnterResourceNoLock<PPB_View_API>对象的成员函数object就可以获得它封装的PPB_View_Shared对象。再调用这个PPB_View_Shared对象的成员函数GetData即可以获得它内部封装的一个ppapi::ViewData对象。这个ppapi::ViewData对象内部保存了目标Plugin Instance的当前视图大小信息,因此可以作为上述IPC消息的第3个参数。

       从前面Chromium的Plugin进程启动过程分析一文可以知道,每一个Plugin进程都存在一个PluginDispatcher对象。Plugin进程将会通过这个PluginDispatcher对象的成员函数OnMessageReceived接收Render进程发送过来的IPC消息。这意味着前面从Render进程发送过来的类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息是通过PluginDispatcher类的成员函数OnMessageReceived接收的。

       PluginDispatcher类的成员函数OnMessageReceived是从父类Dispatcher继承下来的,它的实现如下所示:

bool Dispatcher::OnMessageReceived(const IPC::Message& msg) {  
  ......  
  
  InterfaceProxy* proxy = GetInterfaceProxy(  
      static_cast<ApiID>(msg.routing_id()));  
  ......  
  
  return proxy->OnMessageReceived(msg);  
}  
       这个函数定义在文件external/chromium_org/ppapi/proxy/dispatcher.cc中。

       从前面的分析可以知道,此时参数msg指向的Message对象描述的是一个类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息,该消息的Routing ID为API_ID_PPP_INSTANCE,表示要将该消息分发给一个API_ID_PPP_INSTANCE接口处理。这个API_ID_PPP_INSTANCE接口可以通过调用调用另外一个成员函数GetInterfaceProxy获得。

       在前面Chromium插件(Plugin)实例(Instance)创建过程分析一文中,我们已经分析过Dispatcher类的成员函数GetInterfaceProxy的实现,因此这里不再复述。再结合前面Chromium插件(Plugin)模块(Module)加载过程分析一文,我们可以知道,在Plugin进程中,API_ID_PPP_INSTANCE接口是由一个PPP_Instance_Proxy对象实现的,Dispatcher类的成员函数GetInterfaceProxy接下来会将参数msg描述的类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息分发给它处理,也就是调用它的成员函数OnMessageReceived。

       PPP_Instance_Proxy类的成员函数OnMessageReceived的实现如下所示:

bool PPP_Instance_Proxy::OnMessageReceived(const IPC::Message& msg) {
  ......

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(PPP_Instance_Proxy, msg)
    ......
    IPC_MESSAGE_HANDLER(PpapiMsg_PPPInstance_DidChangeView,
                        OnPluginMsgDidChangeView)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppp_instance_proxy.cc中。

       从这里可以看到,PPP_Instance_Proxy类的成员函数OnMessageReceived将类型为PpapiMsg_PPPInstance_DidChangeView的IPC消息分发给另外一个成员函数OnPluginMsgDidChangeView处理,如下所示:

void PPP_Instance_Proxy::OnPluginMsgDidChangeView(
    PP_Instance instance,
    const ViewData& new_data,
    PP_Bool flash_fullscreen) {
  ......

  combined_interface_->DidChangeView(instance, resource,
                                     &new_data.rect,
                                     &new_data.clip_rect);
}

       这个函数定义在文件external/chromium_org/ppapi/proxy/ppp_instance_proxy.cc中。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,PPP_Instance_Proxy类的成员变量combined_interface_指向的是一个PPP_Instance_Combined对象。PPP_Instance_Proxy类的成员函数OnPluginMsgDidChangeView主要是调用这个PPP_Instance_Combined对象的成员函数DidChangeView通知参数instance描述的Plugin Instance,它的视图大小发生了变化。

       PPP_Instance_Combined类的成员函数DidChangeView的实现如下所示:

void PPP_Instance_Combined::DidChangeView(PP_Instance instance,
                                          PP_Resource view_changed_resource,
                                          const struct PP_Rect* position,
                                          const struct PP_Rect* clip) {
  if (instance_1_1_.DidChangeView) {
    CallWhileUnlocked(
        instance_1_1_.DidChangeView, instance, view_changed_resource);
  } 
  ......
}
       这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppp_instance_combined.cc中。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,PPP_Instance_Combined类的成员变量instance_1_1_指向的是一个PPP_Instance对象。这个PPP_Instance对象的成员变量DidChangeView是一个函数指针,它指向的函数为Instance_DidChangeView。PPP_Instance_Combined类的成员函数DidCreate主要是调用这个函数通知参数instance描述的Plugin Instance,它的视图大小发生了变化。

       函数Instance_DidChangeView的实现,如下所示:

void Instance_DidChangeView(PP_Instance pp_instance,
                            PP_Resource view_resource) {
  Module* module_singleton = Module::Get();
  ......
  Instance* instance = module_singleton->InstanceForPPInstance(pp_instance);
  ......
  instance->DidChangeView(View(view_resource));
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/module.cc中。

       函数Instance_DidChangeView首先调用Module类的静态成员函数Get获得当前Plugin进程中的一个pp::Module单例对象。从前面Chromium插件(Plugin)模块(Module)加载过程分析一文可以知道,这个pp::Module单例对象描述的就是在当前Plugin进程中加载的Plugin Module。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,一个Plugin Module创建的所有Plugin Instance都会以它们的ID为键值,保存在一个std::map中。因此,函数Instance_DidChangeView可以在这个std::map中找到与参数pp_instance对应的Plugin Instance,即一个pp::Instance对象。这个通过调用前面获得的pp::Module单例对象的成员函数InstanceForPPInstance实现的。获得了目标pp::Instance对象之后,就可以调用它的成员函数DidChangeView,以便通知它视图大小发生了变化。

       我们在开发一个Plugin的时候,会自定义一个pp::Instance类。例如,在前面Chromium插件(Plugin)机制简要介绍和学习计划一文提到的GLES2 Example,它自定义的pp::Instance类为GLES2DemoInstance。自定义的GLES2DemoInstance类是从pp::Instance类继承下来的,并且会重写成员函数DidChangeView。这意味着接下来GLES2DemoInstance类的成员函数DidChangeView会被调用。

       GLES2DemoInstance类的成员函数DidChangeView的实现如下所示:

void GLES2DemoInstance::DidChangeView(
    const pp::Rect& position, const pp::Rect& clip_ignored) {
  ......
  plugin_size_ = position.size();

  // Initialize graphics.
  InitGL(0);
}
       这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

       参数position描述的一个pp::Rect对象记录了当前正在处理的Plugin Instance的当前视图大小。GLES2DemoInstance类的成员函数DidChangeView首先将这个视图大小记录在成员变量plugin_size_中,接下来又调用另外一个成员函数InitGL初始化一个OpenGL环境。

       GLES2DemoInstance类的成员函数InitGL的实现如下所示:

void GLES2DemoInstance::InitGL(int32_t result) {
  ......

  if (context_) {
    context_->ResizeBuffers(plugin_size_.width(), plugin_size_.height());
    return;
  }
  int32_t context_attributes[] = {
    PP_GRAPHICS3DATTRIB_ALPHA_SIZE, 8,
    PP_GRAPHICS3DATTRIB_BLUE_SIZE, 8,
    PP_GRAPHICS3DATTRIB_GREEN_SIZE, 8,
    PP_GRAPHICS3DATTRIB_RED_SIZE, 8,
    PP_GRAPHICS3DATTRIB_DEPTH_SIZE, 0,
    PP_GRAPHICS3DATTRIB_STENCIL_SIZE, 0,
    PP_GRAPHICS3DATTRIB_SAMPLES, 0,
    PP_GRAPHICS3DATTRIB_SAMPLE_BUFFERS, 0,
    PP_GRAPHICS3DATTRIB_WIDTH, plugin_size_.width(),
    PP_GRAPHICS3DATTRIB_HEIGHT, plugin_size_.height(),
    PP_GRAPHICS3DATTRIB_NONE,
  };
  context_ = new pp::Graphics3D(this, context_attributes);
  ......
  assert(BindGraphics(*context_));

  ......

  FlickerAndPaint(0, true);
}
       这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

       初始化Plugin的OpenGL环境需要执行两个操作:

       1. 创建一个OpenGL上下文。这个OpenGL上下文用一个pp::Graphics3D对象描述。

       2. 将创建出来的OpenGL上下文与Plugin进行绑定,也就是将它指定为Plugin当前使用的OpenGL上下文。这可以通过调用父类pp::Instance继承下来的成员函数BindGraphics来完成。

       GLES2DemoInstance类的成员变量context_就是用来描述Plugin当前使用的OpenGL上下文。如果这个OpenGL上下文还没有创建,那么GLES2DemoInstance类的成员函数InitGL就会根据Plugin当前的视图大小进行创建。否则的话,就只会改变这个OpenGL上下文描述的绘图缓冲区的大小。

       完成以上两个操作之后,GLES2DemoInstance类的成员函数InitGL就会调用另外一个成员函数FlickerAndPaint渲染Plugin的视图。渲染出来的内容最后就会合成在网页的UI中显示出来。

       接下来我们先分析OpenGL上下文的创建过程,也就是一个pp::Graphics3D对象的创建过程,我们pp::Graphics3D类的构造函数开始分析,它的实现如下所示:

Graphics3D::Graphics3D(const InstanceHandle& instance,
                       const int32_t attrib_list[]) {
  if (has_interface<PPB_Graphics3D_1_0>()) {
    PassRefFromConstructor(get_interface<PPB_Graphics3D_1_0>()->Create(
        instance.pp_instance(), 0, attrib_list));
  }
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/graphics_3d.cc中。

       pp::Graphics3D类的构造函数首行调用模板函数has_interface<PPB_Graphics3D_1_0>检查在当前Plugin进程中加载的Plugin Module是否支持PPB_GRAPHICS_3D_INTERFACE_1_0。如果支持的话,那么就会再调用另外一个模板函数get_interface<PPB_Graphics3D_1_0>获得这个PPB_GRAPHICS_3D_INTERFACE_1_0接口。获得了PPB_GRAPHICS_3D_INTERFACE_1_0接口之后,就可以调用它提供的函数Create请求Render进程创建一个OpenGL上下文了。

       从后面的分析我们可以知道,PPB_GRAPHICS_3D_INTERFACE_1_0接口提供的函数Create创建出来的OpenGL上下文用一个proxy::proxy::Graphics3D对象描述,不过它返回给调用者的是分配给proxy::proxy::Graphics3D对象的一个资源ID。pp::Graphics3D类的构造函数会调用从父类pp::Resource继承下来的成员函数PassRefFromConstructor将这个资源ID保存在成员变量pp_resource_中,如下所示:

void Resource::PassRefFromConstructor(PP_Resource resource) {
  PP_DCHECK(!pp_resource_);
  pp_resource_ = resource;
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/resource.cc中。

       以后通过调用pp::Resource类的成员函数pp_resource即可以获得这个资源ID,如下所示:

class Resource {
 public:
  ......

  PP_Resource pp_resource() const { return pp_resource_; }

  ......
};
       这个函数定义在文件external/chromium_org/ppapi/cpp/resource.h中。

       回到 pp::Graphics3D类的构造函数中,接下来我们首先分析PPB_GRAPHICS_3D_INTERFACE_1_0接口的获取过程,也就是模板函数get_interface<PPB_Graphics3D_1_0>的实现,如下所示:

template <typename T> inline T const* get_interface() {
  static T const* funcs = reinterpret_cast<T const*>(
      pp::Module::Get()->GetBrowserInterface(interface_name<T>()));
  return funcs;
}
       这个模板函数定义在文件external/chromium_org/ppapi/cpp/module_impl.h中。

       这里的模板参数T为PPB_Graphics3D_1_0,展开后得到模板函数get_interface<PPB_Graphics3D_1_0>的具体实现为:

inline PPB_Graphics3D_1_0 const* get_interface() {
  static PPB_Graphics3D_1_0 const* funcs = reinterpret_cast<PPB_Graphics3D_1_0 const*>(
      pp::Module::Get()->GetBrowserInterface(interface_name<PPB_Graphics3D_1_0>()));
  return funcs;
}
       它首先会调用另外一个模板函数interface_name<PPB_Graphics3D_1_0>获得要获取的接口名称,接着再调用前面提到的pp::Module类的静态成员函数Get获得当前Plugin进程中的一个pp::Module单例对象。有了这个pp::Module单例对象之后,就可以调用它的成员函数GetBrowserInterface根据名称获得接口。

       我们首先分析模板函数interface_name<PPB_Graphics3D_1_0>的实现,以便知道要获取的接口名称是什么,如下所示:

template <> const char* interface_name<PPB_Graphics3D_1_0>() {
  return PPB_GRAPHICS_3D_INTERFACE_1_0;
}
       这个模板函数定义在文件ppapi/cpp/graphics_3d.cc中。

       从这里可以看到,要获取的接口名称为PPB_GRAPHICS_3D_INTERFACE_1_0,也就我们要获取的是PPB_GRAPHICS_3D_INTERFACE_1_0接口。这个接口可以通过调用前面获得的pp::Module单例对象的GetBrowserInterface获得。

       在前面Chromium插件(Plugin)模块(Module)加载过程分析一文中,我们已经分析过了pp::Module类的GetBrowserInterface的实现,并且我们也知道,在Plugin进程中,PPB_GRAPHICS_3D_INTERFACE_1_0接口是由一个PPB_Graphics3D_1_0对象实现的。这个PPB_Graphics3D_1_0对象的定义如下所示:

const PPB_Graphics3D_1_0 g_ppb_graphics3d_thunk_1_0 = {  
  &GetAttribMaxValue,  
  &Create,  
  &IsGraphics3D,  
  &GetAttribs,  
  &SetAttribs,  
  &GetError,  
  &ResizeBuffers,  
  &SwapBuffers  
};  
      这个对象定义在文件external/chromium_org/ppapi/thunk/ppb_graphics_3d_thunk.cc中。

      这个PPB_Graphics3D_1_0对象的成员变量Create是一个函数指针。这个函数指针指向了函数Create。这个函数也是定义在文件ppb_graphics_3d_thunk.cc中。

      回到Graphics3D类的构造函数中,现在我们就可以知道,它实际上是通过调用上述函数Create请求Render进程为当前正在处理的Plugin创建一个OpenGL上下文的,如下所示:

PP_Resource Create(PP_Instance instance,
                   PP_Resource share_context,
                   const int32_t attrib_list[]) {
  ......
  EnterResourceCreation enter(instance);
  ......
  return enter.functions()->CreateGraphics3D(instance,
                                             share_context,
                                             attrib_list);
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/ppb_graphics_3d_thunk.cc中。

       OpenGL上下文属于一种资源。在Plugin中,创建资源要使用到接口API_ID_RESOURCE_CREATION。函数Create通过构造一个EnterResourceCreation对象来封装对接口API_ID_RESOURCE_CREATION的调用。封装过程如下所示:

EnterResourceCreation::EnterResourceCreation(PP_Instance instance)
    : EnterBase(),
      functions_(PpapiGlobals::Get()->GetResourceCreationAPI(instance)) {
  ......
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.cc中。

       在Plugin进程中,存在一个PluginGlobals单例对象。这个PluginGlobals单例对象可以通过调用PpapiGlobals类的静态成员函数获得。注意,PluginGlobals类是从PpapiGlobals继承下来的。

       获得上述PluginGlobals单例对象之后,EnterResourceCreation类的构造函数就调用它的成员函数GetResourceCreationAPI获得一个API_ID_RESOURCE_CREATION接口,如下所示:

thunk::ResourceCreationAPI* PluginGlobals::GetResourceCreationAPI(
    PP_Instance instance) {
  PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance);
  if (dispatcher)
    return dispatcher->GetResourceCreationAPI();
  return NULL;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/plugin_globals.cc中。

       从前面Chromium的Plugin进程启动过程分析一文可以知道,在Plugin进程中加载的Plugin Module对应有一个PluginDispatcher对象。这个PluginDispatcher对象与运行在Render进程中的HostDispatcher对象对应。所有属于该Plugin Module的Instance都使用相同的PluginDispatcher对象与Render进程通信。

       PluginGlobals类的成员函数GetResourceCreationAPI首先调用PluginDispatcher类的静态成员函数GetForInstance获得与参数instance描述的Plugin Instance对应的PluginDispatcher对象。有了这个PluginDispatcher对象之后,就可以调用它的成员函数GetResourceCreationAPI获得一个API_ID_RESOURCE_CREATION接口,如下所示:

thunk::ResourceCreationAPI* PluginDispatcher::GetResourceCreationAPI() {
  return static_cast<ResourceCreationProxy*>(
      GetInterfaceProxy(API_ID_RESOURCE_CREATION));
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/plugin_dispatcher.cc中。

       PluginDispatcher类的成员函数GetResourceCreationAPI调用另外一个成员函数GetInterfaceProxy获得一个API_ID_RESOURCE_CREATION接口。PluginDispatcher类的成员函数GetInterfaceProxy是从父类Dispatcher继承下来的,在前面Chromium插件(Plugin)实例(Instance)创建过程分析一文中,我们已经分析过它的实现了。

       结合前面Chromium插件(Plugin)模块(Module)加载过程分析一文,我们可以知道,在Plugin进程中,API_ID_RESOURCE_CREATION接口被指定为ResourceCreationProxy类的静态成员函数Create,Dispatcher类的成员函数GetInterfaceProxy将会调用这个函数创建一个ResourceCreationProxy对象,如下所示:

InterfaceProxy* ResourceCreationProxy::Create(Dispatcher* dispatcher) {
  return new ResourceCreationProxy(dispatcher);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/resource_creation_proxy.cc中。

       参数dispatcher指向的是一个PluginDispatcher对象。这个PluginDispatcher对象就是在前面分析的PluginGlobals类的成员函数GetResourceCreationAPI中获取的PluginDispatcher对象。ResourceCreationProxy类的静态成员函数Create使用该PluginDispatcher对象创建了一个ResourceCreationProxy对象,并且返回给最初的调用者,也就是EnterResourceCreation类的构造函数。EnterResourceCreation类的构造函数又会将这个ResourceCreationProxy对象保存在成员变量functions_中。

       这一步执行完成后,回到前面分析的函数Create中。这时候就它构造了一个EnterResourceCreation对象,并且这个EnterResourceCreation对象的成员变量functions_指向了一个ResourceCreationProxy对象。

       函数Create接下来会调用上述EnterResourceCreation对象的成员函数functions它的成员变量functions_指向的ResourceCreationProxy对象。有了这个ResourceCreationProxy对象之后,就可以调用它的成员函数CreateGraphics3D请求Render进程为当前正在处理的Plugin Instance创建一个OpenGL上下文了。

       ResourceCreationProxy类的成员函数CreateGraphics3D的实现如下所示:

PP_Resource ResourceCreationProxy::CreateGraphics3D(
    PP_Instance instance,
    PP_Resource share_context,
    const int32_t* attrib_list) {
  return PPB_Graphics3D_Proxy::CreateProxyResource(
      instance, share_context, attrib_list);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/resource_creation_proxy.cc中。

       ResourceCreationProxy类的成员函数CreateGraphics3D调用PPB_Graphics3D_Proxy类的静态成员函数CreateProxyResource请求Render进程为当前正在处理的Plugin Instance创建一个OpenGL上下文,如下所示:

PP_Resource PPB_Graphics3D_Proxy::CreateProxyResource(
    PP_Instance instance,
    PP_Resource share_context,
    const int32_t* attrib_list) {
  PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance);
  ......

  HostResource share_host;
  gpu::gles2::GLES2Implementation* share_gles2 = NULL;
  if (share_context != 0) {
    EnterResourceNoLock<PPB_Graphics3D_API> enter(share_context, true);
    if (enter.failed())
      return PP_ERROR_BADARGUMENT;

    PPB_Graphics3D_Shared* share_graphics =
        static_cast<PPB_Graphics3D_Shared*>(enter.object());
    share_host = share_graphics->host_resource();
    share_gles2 = share_graphics->gles2_impl();
  }

  std::vector<int32_t> attribs;
  if (attrib_list) {
    for (const int32_t* attr = attrib_list;
         attr[0] != PP_GRAPHICS3DATTRIB_NONE;
         attr += 2) {
      attribs.push_back(attr[0]);
      attribs.push_back(attr[1]);
    }
  }
  attribs.push_back(PP_GRAPHICS3DATTRIB_NONE);

  HostResource result;
  dispatcher->Send(new PpapiHostMsg_PPBGraphics3D_Create(
      API_ID_PPB_GRAPHICS_3D, instance, share_host, attribs, &result));
  ......

  scoped_refptr<Graphics3D> graphics_3d(new Graphics3D(result));
  if (!graphics_3d->Init(share_gles2))
    return 0;
  return graphics_3d->GetReference();
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

       PPB_Graphics3D_Proxy类的静态成员函数CreateProxyResource首先调用PluginDispatcher类的静态成员函数GetForInstance获得与参数instance描述的Plugin Instance对应的一个PluginDispatcher对象。后面将会通过这个PluginDispatcher对象向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息,用来请求Render进程创建一个OpenGL上下文了。

       类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息传递了4个参数给Render进程,分别是:

       1. API_ID_PPB_GRAPHICS_3D,表示Render进程要将该IPC消息分发给API_ID_PPB_GRAPHICS_3D接口处理。

       2. instance,描述要创建OpenGL上下文的Plugin Instance。

       3. share_host,描述另外一个OpenGL上下文,新创建的OpenGL上下文要与它在同一个共享组中。

       4. attribs,描述新创建的OpenGL上下文应具有的属性。

       Render进程根据要求为目标Plugin Instance创建了一个OpenGL上下文之后,会为该OpenGL上下文分配一个ID,并且会将该ID返回给Plugin进程。Plugin进程再将这个ID封装在一个HostResource对象,然后将这个HostResource对象返回给PPB_Graphics3D_Proxy类的静态成员函数CreateProxyResource。

       PPB_Graphics3D_Proxy类的静态成员函数CreateProxyResource再根据获得的HostResource对象创建一个ppapi::proxy::Graphics3D对象,并且调用它的成员函数Init对它进行初始化,相当于是对新创建的OpenGL上下文进行初始化。初始化完成后,就可以进行使用了。

       新创建的Graphics3D对象初始化完成之后,PPB_Graphics3D_Proxy类的静态成员函数CreateProxyResource再调用它的成员函数GetReference获得一个类型为PP_Resource的引用。以后通过这个引用就可以使用该Graphics3D对象,也就是新创建的OpenGL上下文。

       接下来,我们首先分析Render进程为目标Plugin Instance创建OpenGL上下文的过程,即处理类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息的过程,然后再分析新创建的OpenGL上下文在Plugin进程的初始化过程。

       前面提到,Render进程会将类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息分发给API_ID_PPB_GRAPHICS_3D接口处理。从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文可以知道,在Render进程中,API_ID_PPB_GRAPHICS_3D接口是由一个PPB_Graphics3D_Proxy对象实现的,也就是Render进程会将类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息分发给该PPB_Graphics3D_Proxy对象处理,这是通过调用它的成员函数OnMessageReceived实现的。

       类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息在Render进程中的分发过程类似我们在前面Chromium插件(Plugin)实例(Instance)创建过程分析一文提到的类型为PpapiMsg_PPPInstance_DidCreate的IPC消息在Plugin进程中的分发过程,因此这里不再详细描述。

       接下来我们继续分析PPB_Graphics3D_Proxy类的成员函数OnMessageReceived处理类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息的过程,如下所示:

bool PPB_Graphics3D_Proxy::OnMessageReceived(const IPC::Message& msg) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(PPB_Graphics3D_Proxy, msg)
#if !defined(OS_NACL)
    IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBGraphics3D_Create,
                        OnMsgCreate)
    ......
#endif  // !defined(OS_NACL)

    ......
    IPC_MESSAGE_UNHANDLED(handled = false)

  IPC_END_MESSAGE_MAP()
  // FIXME(brettw) handle bad messages!
  return handled;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

       从这里可以看到,PPB_Graphics3D_Proxy类的成员函数OnMessageReceived将类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息分发给另外一个成员函数OnMsgCreate处理,如下所示:

void PPB_Graphics3D_Proxy::OnMsgCreate(PP_Instance instance,
                                       HostResource share_context,
                                       const std::vector<int32_t>& attribs,
                                       HostResource* result) {
  ......

  thunk::EnterResourceCreation enter(instance);

  if (enter.succeeded()) {
    result->SetHostResource(
      instance,
      enter.functions()->CreateGraphics3DRaw(instance,
                                             share_context.host_resource(),
                                             &attribs.front()));
  }
}
      这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

      参数instance要描述的是要创建OpenGL上下文的Plugin Instance在Render进程中对应的Proxy,也就是一个PepperPluginInstanceImpl对象。PPB_Graphics3D_Proxy类的成员函数OnMsgCreate首先是使用它构造一个EnterResourceCreation对象。前面提到,EnterResourceCreation类是用来封装资源创建接口API_ID_RESOURCE_CREATION的。因此,有了前面构造的EnterResourceCreation对象之后,PPB_Graphics3D_Proxy类的成员函数OnMsgCreate就可以使用API_ID_RESOURCE_CREATION接口来为参数instance描述的Plugin Instance创建一个OpenGL上下文了。

       EnterResourceCreation对象在Render进程的构造过程与在Plugin进程的构造过程是不一样的。前面我们已经分析过EnterResourceCreation对象在Plugin进程的构造过程,接下来我们继续分析EnterResourceCreation对象在Render进程的构造过程,以便了解Render进程是如何实现API_ID_RESOURCE_CREATION接口的。

       我们从EnterResourceCreation类的构造函数开始分析EnterResourceCreation对象在Render进程的构造过程,它的实现如下所示:

EnterResourceCreation::EnterResourceCreation(PP_Instance instance)
    : EnterBase(),
      functions_(PpapiGlobals::Get()->GetResourceCreationAPI(instance)) {
  ......
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.cc中。

       在Render进程中,存在一个HostGlobals单例对象。这个HostGlobals单例对象可以通过调用PpapiGlobals类的静态成员函数Get获得。HostGlobals类与前面提到的PluginGlobals类一样,都是从PpapiGlobals类继承下来的。

       获得上述HostGlobals单例对象之后,EnterResourceCreation类的构造函数就调用它的成员函数GetResourceCreationAPI获得一个API_ID_RESOURCE_CREATION接口,如下所示:

ppapi::thunk::ResourceCreationAPI* HostGlobals::GetResourceCreationAPI(
    PP_Instance pp_instance) {
  PepperPluginInstanceImpl* instance = GetInstance(pp_instance);
  if (!instance)
    return NULL;
  return &instance->resource_creation();
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/host_globals.cc中。

       HostGlobals类的成员函数GetResourceCreationAPI首先调用成员函数GetInstance获得参数pp_instance描述的一个Plugin Instance Proxy,也就是一个PepperPluginInstanceImpl对象。有了这个PepperPluginInstanceImpl对象之后,就可以调用它的成员函数resource_creation获得一个API_ID_RESOURCE_CREATION接口,如下所示:

class CONTENT_EXPORT PepperPluginInstanceImpl
    : public base::RefCounted<PepperPluginInstanceImpl>,
      public NON_EXPORTED_BASE(PepperPluginInstance),
      public ppapi::PPB_Instance_Shared,
      public NON_EXPORTED_BASE(cc::TextureLayerClient),
      public RenderFrameObserver {
 public:
  ......

  ppapi::thunk::ResourceCreationAPI& resource_creation() {
    return *resource_creation_.get();
  }

  ......

 private:
  ......

  scoped_ptr<ppapi::thunk::ResourceCreationAPI> resource_creation_;

  ......
};
       这个函数定义在文件external/chromium_org/content/renderer/pepper/pepper_plugin_instance_impl.h中。

       PepperPluginInstanceImpl类的成员变量resource_creation_指向的是一个PepperInProcessResourceCreation对象。PepperPluginInstanceImpl类的成员函数resource_creation将这个PepperInProcessResourceCreation对象返回给调用者。这意味在Render进程中,API_ID_RESOURCE_CREATION接口是通过PepperInProcessResourceCreation类实现的。

       这一步执行完成后,回到前面分析的PPB_Graphics3D_Proxy类的成员函数OnMsgCreate中。这时候就它构造了一个EnterResourceCreation对象,并且这个EnterResourceCreation对象的成员变量functions_指向了一个PepperInProcessResourceCreation对象。

       PPB_Graphics3D_Proxy类的成员函数OnMsgCreate接下来会调用上述EnterResourceCreation对象的成员函数functions它的成员变量functions_指向的PepperInProcessResourceCreation对象。有了这个PepperInProcessResourceCreation对象之后,就可以调用它的成员函数CreateGraphics3DRaw为参数pp_instance描述的Plugin Instance创建一个OpenGL上下文。

       PepperInProcessResourceCreation类的成员函数CreateGraphics3DRaw是从父类ResourceCreationImpl继承下来的,它的实现如下所示:

PP_Resource ResourceCreationImpl::CreateGraphics3DRaw(
    PP_Instance instance,
    PP_Resource share_context,
    const int32_t* attrib_list) {
  return PPB_Graphics3D_Impl::CreateRaw(instance, share_context, attrib_list);
}
      这个函数定义在文件external/chromium_org/content/renderer/pepper/resource_creation_impl.cc中。

      ResourceCreationImpl类的成员函数CreateGraphics3DRaw调用PPB_Graphics3D_Impl类的静态成员函数CreateRaw为参数pp_instance描述的Plugin Instance创建一个OpenGL上下文,如下所示:

PP_Resource PPB_Graphics3D_Impl::CreateRaw(PP_Instance instance,
                                           PP_Resource share_context,
                                           const int32_t* attrib_list) {
  PPB_Graphics3D_API* share_api = NULL;
  if (share_context) {
    EnterResourceNoLock<PPB_Graphics3D_API> enter(share_context, true);
    if (enter.failed())
      return 0;
    share_api = enter.object();
  }
  scoped_refptr<PPB_Graphics3D_Impl> graphics_3d(
      new PPB_Graphics3D_Impl(instance));
  if (!graphics_3d->InitRaw(share_api, attrib_list))
    return 0;
  return graphics_3d->GetReference();
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/ppb_graphics_3d_impl.cc中。

       在Render进程中,Plugin使用的OpenGL上下文通过一个PPB_Graphics3D_Impl对象描述。因此,PPB_Graphics3D_Impl类的静态成员函数CreateRaw会创建一个PPB_Graphics3D_Impl对象,并且调用它的成员函数InitRaw对它进行初始化,也就是对它描述的OpenGL上下文进行初始化。初始化完成后,就会获得它的一个PP_Resource引用。这个引用将会返回给Plugin进程。Plugin进程以后就可以通过这个引用来使用前面创建出来的OpenGL上下文了。

       接下来我们继续分析Render进程为Plugin创建的OpenGL上下文的初始化过程,也就是PPB_Graphics3D_Impl类的成员函数InitRaw的实现,如下所示:

bool PPB_Graphics3D_Impl::InitRaw(PPB_Graphics3D_API* share_context,
                                  const int32_t* attrib_list) {
  ......

  RenderThreadImpl* render_thread = RenderThreadImpl::current();
  ......

  channel_ = render_thread->EstablishGpuChannelSync(
      CAUSE_FOR_GPU_LAUNCH_PEPPERPLATFORMCONTEXT3DIMPL_INITIALIZE);
  ......

  command_buffer_ = channel_->CreateOffscreenCommandBuffer(
      surface_size, share_buffer, attribs, GURL::EmptyGURL(), gpu_preference);
  ......

  return true;
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/ppb_graphics_3d_impl.cc中。

       PPB_Graphics3D_Impl类的成员函数InitRaw首先调用RenderThreadImpl类的静态成员函数current获得一个RenderThreadImpl对象。这个RenderThreadImpl对象描述的是当前Render进程的Render线程。有了这个RenderThreadImpl对象之后,就可以调用它的成员函数EstablishGpuChannelSync创建一个GPU通道,也就是一个GpuChannelHost对象。这个GPU通道的详细创建过程,可以参考前面Chromium的GPU进程启动过程分析一文。

       PPB_Graphics3D_Impl类的成员函数InitRaw接下来又会调用前面创建出来的GpuChannelHost对象的成员函数CreateOffscreenCommandBuffer请求GPU进程为其创建一个离屏渲染类型的OpenGL上下文。这个离屏渲染类型的OpenGL上下文是通过一个CommandBufferProxyImpl对象描述的。Render进程请求GPU进程创建OpenGL上下文的过程,可以参考前面Chromium硬件加速渲染的OpenGL上下文创建过程分析一文。以后Plugin执行GPU命令时,将会作用在上述离屏渲染的OpenGL上下文中。

       这一步执行完成后,Render进程就为Plugin创建了一个OpenGL上下文。这个OpenGL上下文通过一个PPB_Graphics3D_Impl对象描述。这个PPB_Graphics3D_Impl对象会被分配一个ID,并且这个ID会返回给Plugin进程。如前所述,Plugin进程获得了这个ID之后,就会将其封装在一个Graphics3D对象中。这个Graphics3D对象是用来Plugin进程描述一个OpenGL上下文的。

       回到前面分析的PPB_Graphics3D_Proxy类的成员函数CreateProxyResource中,这时候它就获得了一个OpenGL上下文之后。这个OpenGL上下文在Plugin进程中同样需要进行初始化。如前所述,这是通过调用ppapi::proxy::Graphics3D类的成员函数Init实现的。接下我们就继续分析Plugin进程初始化一个OpenGL上下文的过程,也就是分析ppapi::proxy::Graphics3D类的成员函数Init的实现,如下所示:

bool Graphics3D::Init(gpu::gles2::GLES2Implementation* share_gles2) {
  ......

  command_buffer_.reset(
      new PpapiCommandBufferProxy(host_resource(), dispatcher));

  return CreateGLES2Impl(kCommandBufferSize, kTransferBufferSize,
                         share_gles2);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

       ppapi::proxy::Graphics3D类的成员函数Init首先会创建一个PpapiCommandBufferProxy对象。这个PpapiCommandBufferProxy对象是用来与前面在Render进程中创建的CommandBufferProxyImpl对象通信的,也就是前者会将提交给它的GPU命令转发给后者处理,后者又会将接收到的GPU命令发送给GPU进程执行。

       ppapi::proxy::Graphics3D类的成员函数Init接下来又会调用成员函数CreateGLES2Impl根据上述PpapiCommandBufferProxy对象创建一个Command Buffer GL接口。有了这个Command Buffer GL接口之后,Plugin就可以调用它提供的OpenGL函数执行3D渲染了。

       ppapi::proxy::Graphics3D类的成员函数CreateGLES2Impl是从父类PPB_Graphics3D_Shared继承下来的,它的实现如下所示:

bool PPB_Graphics3D_Shared::CreateGLES2Impl(
    int32 command_buffer_size,
    int32 transfer_buffer_size,
    gpu::gles2::GLES2Implementation* share_gles2) {
  gpu::CommandBuffer* command_buffer = GetCommandBuffer();
  DCHECK(command_buffer);

  // Create the GLES2 helper, which writes the command buffer protocol.
  gles2_helper_.reset(new gpu::gles2::GLES2CmdHelper(command_buffer));
  if (!gles2_helper_->Initialize(command_buffer_size))
    return false;

  // Create a transfer buffer used to copy resources between the renderer
  // process and the GPU process.
  const int32 kMinTransferBufferSize = 256 * 1024;
  const int32 kMaxTransferBufferSize = 16 * 1024 * 1024;
  transfer_buffer_.reset(new gpu::TransferBuffer(gles2_helper_.get()));
  ......

  // Create the object exposing the OpenGL API.
  gles2_impl_.reset(new gpu::gles2::GLES2Implementation(
      gles2_helper_.get(),
      share_gles2 ? share_gles2->share_group() : NULL,
      transfer_buffer_.get(),
      bind_creates_resources,
      lose_context_when_out_of_memory,
      GetGpuControl()));

  if (!gles2_impl_->Initialize(
           transfer_buffer_size,
           kMinTransferBufferSize,
           std::max(kMaxTransferBufferSize, transfer_buffer_size),
           gpu::gles2::GLES2Implementation::kNoLimit)) {
    return false;
  }

  ......

  return true;
}

       这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppb_graphics_3d_shared.cc。

       在Chromium中,Command Buffer GL接口是通过GLES2Implementation类描述的。因此,PPB_Graphics3D_Shared类的成员函数CreateGLES2Impl将会创建一个GLES2Implementation对象,并且保存在成员变量gles2_impl_中。

       创建这个GLES2Implementation对象需要一个Command Buffer和一个Transfer Buffer。前者通过一个GLES2CmdHelper对象描述,后者通过一个TransferBuffer对象描述的。在创建GLES2CmdHelper对象的过程中,又需要用到前面创建的一个PpapiCommandBufferProxy对象,以便可以将Plugin要执行的GPU命令转发给Render进程处理。这个PpapiCommandBufferProxy对象可以通过调用PPB_Graphics3D_Shared类的成员函数GetCommandBuffer获得。关于Command Buffer GL接口的创建和使用,可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       这一步执行完成后,回到前面分析的GLES2DemoInstance类的成员函数InitGL中,这时候它就创建了一个OpenGL上下文,并且这个OpenGL上下文已经初始化完成。接下来,GLES2DemoInstance类的成员函数InitGL会将前面创建出来的OpenGL上下文绑定到当前正在处理的Plugin Instance中去。执行了这个操作之后,Plugin Instance才可以在该OpenGL上下文中执行GPU命令。

       给一个Plugin Instance绑定OpenGL上下文是通过调用pp::Instance类的成员函数BindGraphics实现的,如下所示:

bool Instance::BindGraphics(const Graphics3D& graphics) {
  if (!has_interface<PPB_Instance_1_0>())
    return false;
  return PP_ToBool(get_interface<PPB_Instance_1_0>()->BindGraphics(
      pp_instance(), graphics.pp_resource()));
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/instance.cc中。

       pp::Instance类的成员函数BindGraphics需要通过PPB_INSTANCE_INTERFACE_1_0接口为当前正在处理的Plugin Instance绑定一个OpenGL上下文。这个OpenGL上下文由参数graphics指向的一个Graphics3D对象描述。

       因此,pp::Instance类的成员函数BindGraphics首先会检查当前的Plugin进程是否支持PPB_INSTANCE_INTERFACE_1_0接口。如果支持的话,那么就会通过一个模板函数get_interface<PPB_Instance_1_0>获得该PPB_INSTANCE_INTERFACE_1_0接口,如下所示:

template <typename T> inline T const* get_interface() {
  static T const* funcs = reinterpret_cast<T const*>(
      pp::Module::Get()->GetBrowserInterface(interface_name<T>()));
  return funcs;
}
       这个模板函数定义在文件external/chromium_org/ppapi/cpp/module_impl.h中。

       这里的模板参数T为PPB_Instance_1_0,展开后得到模板函数get_interface<PPB_Instance_1_0>的具体实现为:

inline PPB_Instance_1_0 const* get_interface() {
  static PPB_Instance_1_0 const* funcs = reinterpret_cast<PPB_Instance_1_0 const*>(
      pp::Module::Get()->GetBrowserInterface(interface_name<PPB_Instance_1_0>()));
  return funcs;
}
       它首先会调用另外一个模板函数interface_name<PPB_Instance_1_0>获得要获取的接口名称,接着再调用前面提到的pp::Module类的静态成员函数Get获得当前Plugin进程中的一个pp::Module单例对象。有了这个pp::Module单例对象之后,就可以调用它的成员函数GetBrowserInterface根据名称获得接口。

      我们首先分析模板函数interface_name<PPB_Instance_1_0>的实现,以便知道要获取的接口名称是什么,如下所示:

template <> const char* interface_name<PPB_Instance_1_0>() {
  return PPB_INSTANCE_INTERFACE_1_0;
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/instance.cc中。

       从这里可以看到,要获取的接口名称为PPB_INSTANCE_INTERFACE_1_0,也就我们要获取的是PPB_INSTANCE_INTERFACE_1_0接口。这个接口可以通过调用前面获得的pp::Module单例对象的GetBrowserInterface获得。

       在前面Chromium插件(Plugin)模块(Module)加载过程分析一文中,我们已经分析过了pp::Module类的GetBrowserInterface的实现,并且我们也知道,在Plugin进程中,PPB_INSTANCE_INTERFACE_1_0接口是由一个PPB_Instance_1_0对象实现的。这个PPB_Graphics3D_1_0对象的定义如下所示:

const PPB_Instance_1_0 g_ppb_instance_thunk_1_0 = {
  &BindGraphics,
  &IsFullFrame
};
       这个对象定义在文件external/chromium_org/ppapi/thunk/ppb_instance_thunk.cc中。

       这个PPB_Instance_1_0对象的成员变量BindGraphics是一个函数指针。这个函数指针指向了函数BindGraphics。这个函数也是定义在文件ppb_instance_thunk.cc中。

       回到pp::Instance类的成员函数BindGraphics中,现在我们就可以知道,它实际上是通过调用上述函数BindGraphics请求Render进程为当前正在处理的Plugin绑定一个OpenGL上下文的,如下所示:

PP_Bool BindGraphics(PP_Instance instance, PP_Resource device) {
  ......
  EnterInstance enter(instance);
  ......
  return enter.functions()->BindGraphics(instance, device);
}

       这个函数定义在文件external/chromium_org/ppapi/thunk/ppb_instance_thunk.cc中。

       函数BindGraphics需要通过另外一个接口API_ID_PPB_INSTANCE向Render进程发送一个IPC消息,以便请求后者为参数instance描述的Plugin Instance绑定另外一个参数device描述的OpenGL上下文。

       函数BindGraphics是通过EnterInstance类来使用接口API_ID_RESOURCE_CREATION,因此它首先会构造一个EnterInstance对象,如下所示:

EnterInstance::EnterInstance(PP_Instance instance)
    : EnterBase(),
      functions_(PpapiGlobals::Get()->GetInstanceAPI(instance)) {
  ......
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.cc中。

       前面提到,在Plugin进程中,存在一个PluginGlobals单例对象。这个PluginGlobals单例对象可以通过调用PpapiGlobals类的静态成员函数获得。获得了这个PluginGlobals单例对象之后,EnterInstance类的构造函数就调用它的成员函数GetInstanceAPI获得一个API_ID_PPB_INSTANCE接口,如下所示:

thunk::PPB_Instance_API* PluginGlobals::GetInstanceAPI(PP_Instance instance) {
  PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance);
  if (dispatcher)
    return dispatcher->GetInstanceAPI();
  return NULL;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/plugin_globals.cc中。

       PluginGlobals类的成员函数GetInstanceAPI首先调用PluginDispatcher类的静态成员函数GetForInstance获得一个与参数instance描述的Plugin Instance对应的PluginDispatcher对象。有了这个PluginDispatcher对象之后,再调用它的成员函数GetInstanceAPI获得一个API_ID_PPB_INSTANCE接口,如下所示:

thunk::PPB_Instance_API* PluginDispatcher::GetInstanceAPI() {
  return static_cast<PPB_Instance_Proxy*>(
      GetInterfaceProxy(API_ID_PPB_INSTANCE));
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/plugin_dispatcher.cc中。

       PluginDispatcher类的成员函数GetInstanceAPI调用另外一个成员函数GetInterfaceProxy获得一个API_ID_PPB_INSTANCE接口。PluginDispatcher类的成员函数GetInterfaceProxy是从父类Dispatcher继承下来的。Dispatcher类的成员函数GetInterfaceProxy的实现,可以参考前面Chromium插件(Plugin)实例(Instance)创建过程分析一文中。

       再结合前面Chromium插件(Plugin)模块(Module)加载过程分析一文,我们可以知道,在Plugin进程中,API_ID_PPB_INSTANCE接口被指定为模板函数ProxyFactory<PPB_Instance_Proxy>。Dispatcher类的成员函数GetInterfaceProxy将会调用这个函数创建一个PPB_Instance_Proxy对象。这意味着在Plugin进程中,API_ID_PPB_INSTANCE接口是通过一个PPB_Instance_Proxy对象实现的。这个PPB_Instance_Proxy对象会返回给EnterInstance类的构造函数。

       回到前面分析的函数BindGraphics,这时候就它构造了一个EnterInstance对象,并且这个EnterInstance对象的成员变量functions_指向了一个PPB_Instance_Proxy对象。这个PPB_Instance_Proxy对象可以通过调用上述构造的EnterInstance对象的成员函数functions获得。有了这个PPB_Instance_Proxy对象之后,函数BindGraphics就可以调用它的成员函数BindGraphics向Render进程发送一个IPC消息,以便请求它为当前正在处理的Plugin绑定一个OpenGL上下文,如下所示:

PP_Bool PPB_Instance_Proxy::BindGraphics(PP_Instance instance,
                                         PP_Resource device) {
  // If device is 0, pass a null HostResource. This signals the host to unbind
  // all devices.
  HostResource host_resource;
  PP_Resource pp_resource = 0;
  if (device) {
    Resource* resource =
        PpapiGlobals::Get()->GetResourceTracker()->GetResource(device);
    if (!resource || resource->pp_instance() != instance)
      return PP_FALSE;
    host_resource = resource->host_resource();
    pp_resource = resource->pp_resource();
  } else {
    // Passing 0 means unbinding all devices.
    dispatcher()->Send(new PpapiHostMsg_PPBInstance_BindGraphics(
        API_ID_PPB_INSTANCE, instance, 0));
    return PP_TRUE;
  }

  // We need to pass different resource to Graphics 2D and 3D right now.  Once
  // 3D is migrated to the new design, we should be able to unify this.
  EnterResourceNoLock<PPB_Compositor_API> enter_compositor(device, false);
  EnterResourceNoLock<PPB_Graphics2D_API> enter_2d(device, false);
  EnterResourceNoLock<PPB_Graphics3D_API> enter_3d(device, false);
  if (enter_compositor.succeeded() || enter_2d.succeeded()) {
    dispatcher()->Send(new PpapiHostMsg_PPBInstance_BindGraphics(
        API_ID_PPB_INSTANCE, instance, pp_resource));
    return PP_TRUE;
  } else if (enter_3d.succeeded()) {
    dispatcher()->Send(new PpapiHostMsg_PPBInstance_BindGraphics(
        API_ID_PPB_INSTANCE, instance, host_resource.host_resource()));
    return PP_TRUE;
  }
  return PP_FALSE;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_instance_proxy.cc中。

       PPB_Instance_Proxy类的成员函数BindGraphics首先判断参数device的值是否不等于NULL。如果不等于NULL,那么它描述的就是一个OpenGL上下文,并且这个OpenGL上下文要与参数instance描述的Plugin Instance进行绑定。否则的话,就说明要将参数instance描述的Plugin Instance当前使用的OpenGL上下文设置为NULL。

       在我们这个情景中,参数device的值不等于NULL。在这种情况下,PPB_Instance_Proxy类的成员函数BindGraphics首先会根据这个参数获得一个Resource对象。这个Resource对象描述的就是一个OpenGL上下文。这个OpenGL上下文可能是用来执行2D渲染的,也可能是用来执行3D渲染的。PPB_Instance_Proxy类的成员函数BindGraphics会对这两种情况进行区别,不过最终都会向Render进程发送一个类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息。这个IPC消息携带了3个参数:

       1. API_ID_PPB_INSTANCE,要求Render进程将该IPC消息分发给API_ID_PPB_INSTANCE接口处理。

       2. instance,表示目标Plugin Instance。

       3. 通过参数device获得一个PP_Resource对象,描述的是一个要与目标Plugin Instance进行绑定的OpenGL上下文。

       Render进程接收类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息的过程与前面分析的类型为PpapiHostMsg_PPBGraphics3D_Create的IPC消息是类似的,只不过前者最后会分发给API_ID_PPB_INSTANCE接口处理,而后者会分发给API_ID_PPB_GRAPHICS_3D接口处理。它们在Render进程的分发过程都可以参考在前面Chromium插件(Plugin)实例(Instance)创建过程分析一文提到的类型为PpapiMsg_PPPInstance_DidCreate的IPC消息在Plugin进程的分发过程。

       从前面Chromium插件(Plugin)实例(Instance)创建过程分析一文还可以知道,在Render进程中,API_ID_PPB_INSTANCE接口是由一个PPB_Instance_Proxy对象实现的,也就是Render进程会将类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息分发给该PPB_Instance_Proxy对象处理,这是通过调用它的成员函数OnMessageReceived实现的,如下所示:

bool PPB_Instance_Proxy::OnMessageReceived(const IPC::Message& msg) {
  ......

  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(PPB_Instance_Proxy, msg)
#if !defined(OS_NACL)
    ......
    IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBInstance_BindGraphics,
                        OnHostMsgBindGraphics)
    ......
#endif  // !defined(OS_NACL)

    ......

    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
      这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_instance_proxy.cc中。

      从这里可以看到,PPB_Instance_Proxy类的成员函数OnMessageReceived会将类型为PpapiHostMsg_PPBInstance_BindGraphics的IPC消息分发给另外一个成员函数OnHostMsgBindGraphics处理,如下所示:

void PPB_Instance_Proxy::OnHostMsgBindGraphics(PP_Instance instance,
                                               PP_Resource device) {
  // Note that we ignroe the return value here. Otherwise, this would need to
  // be a slow sync call, and the plugin side of the proxy will have already
  // validated the resources, so we shouldn't see errors here that weren't
  // already caught.
  EnterInstanceNoLock enter(instance);
  if (enter.succeeded())
    enter.functions()->BindGraphics(instance, device);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_instance_proxy.cc中。

       参数instance要描述的是要绑定OpenGL上下文的Plugin Instance在Render进程中对应的Proxy,也就是一个PepperPluginInstanceImpl对象。PPB_Instance_Proxy类的成员函数OnHostMsgBindGraphics首先是使用它构造一个EnterInstanceNoLock对象。这个EnterInstanceNoLock对象是用来封装一个Instance API的。这个Instance API可以用来给一个Plugin Instance Proxy绑定一个OpenGL上下文。

       接下来我们从EnterInstanceNoLock类的构造函数开始分析一个EnterInstanceNoLock对象的构造过程,如下所示:

EnterInstanceNoLock::EnterInstanceNoLock(PP_Instance instance)
    : EnterBase(),
      functions_(PpapiGlobals::Get()->GetInstanceAPI(instance)) {
  ......
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.cc中。

       前面提到,在Render进程中,存在一个HostGlobals单例对象。这个HostGlobals单例对象可以通过调用PpapiGlobals类的静态成员函数Get获得。获得了这个HostGlobals单例对象之后,EnterInstanceNoLock类的构造函数就调用它的成员函数GetInstanceAPI获得一个Instance API,如下所示:

ppapi::thunk::PPB_Instance_API* HostGlobals::GetInstanceAPI(
    PP_Instance instance) {
  // The InstanceAPI is just implemented by the PluginInstance object.
  return GetInstance(instance);
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/host_globals.cc中。

       HostGlobals类的成员函数GetInstanceAPI调用另外一个成员函数GetInstance获得参数instance描述的一个Plugin Instance Proxy,也就是一个PepperPluginInstanceImpl对象,如下所示:

PepperPluginInstanceImpl* HostGlobals::GetInstance(PP_Instance instance) {
  ......
  InstanceMap::iterator found = instance_map_.find(instance);
  if (found == instance_map_.end())
    return NULL;
  return found->second;
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/host_globals.cc。

       在Render进程中,每一个Plugin Instance Proxy都会以分配给它的ID保存在HostGlobals类的成员变量instance_map_描述的一个std::map中。因此,给出一个ID,就可以在这个std::map中找到它对应的Plugin Instance Proxy,也就是一个PepperPluginInstanceImpl对象。

       在Render进程中,Instance API是通过PPB_Instance_API类描述的。PepperPluginInstanceImpl类又是从PPB_Instance_API类继承下来的。因此,一个PepperPluginInstanceImpl对象可以当作一个Instance API使用。这个PepperPluginInstanceImpl对象将会返回给EnterInstanceNoLock类的构造函数,并且保存在EnterInstanceNoLock类的成员变量functions_中。

       这一步执行完成后,回到前面分析的PPB_Instance_Proxy类的成员函数OnMessageReceived中。这时候就它构造了一个EnterInstanceNoLock对象,并且这个EnterInstanceNoLock对象的成员变量functions_指向了一个PepperInProcessResourceCreation对象。注意,这个PepperPluginInstanceImpl对象在用作Instance API的同时,也是即将要绑定OpenGL上下文的Plugin Instance Proxy。这个绑定操作是通过调用它的成员函数BindGraphics实现的,如下所示:

PP_Bool PepperPluginInstanceImpl::BindGraphics(PP_Instance instance,
                                               PP_Resource device) {
  ......

  scoped_refptr<ppapi::Resource> old_graphics = bound_graphics_3d_.get();
  if (bound_graphics_3d_.get()) {
    bound_graphics_3d_->BindToInstance(false);
    bound_graphics_3d_ = NULL;
  }
  if (bound_graphics_2d_platform_) {
    bound_graphics_2d_platform_->BindToInstance(NULL);
    bound_graphics_2d_platform_ = NULL;
  }
  if (bound_compositor_) {
    bound_compositor_->BindToInstance(NULL);
    bound_compositor_ = NULL;
  }

  ......

  const ppapi::host::PpapiHost* ppapi_host =
      RendererPpapiHost::GetForPPInstance(instance)->GetPpapiHost();
  ppapi::host::ResourceHost* host = ppapi_host->GetResourceHost(device);
  PepperGraphics2DHost* graphics_2d = NULL;
  PepperCompositorHost* compositor = NULL;
  if (host) {
    if (host->IsGraphics2DHost()) {
      graphics_2d = static_cast<PepperGraphics2DHost*>(host);
    } else if (host->IsCompositorHost()) {
      compositor = static_cast<PepperCompositorHost*>(host);
    } 
    ......
  }

  EnterResourceNoLock enter_3d(device, false);
  PPB_Graphics3D_Impl* graphics_3d =
      enter_3d.succeeded()
          ? static_cast(enter_3d.object())
          : NULL;
  
  if (compositor) {
    if (compositor->BindToInstance(this)) {
      bound_compositor_ = compositor;
      ......
      return PP_TRUE;
    }
  } else if (graphics_2d) {
    if (graphics_2d->BindToInstance(this)) {
      bound_graphics_2d_platform_ = graphics_2d;
      ......
      return PP_TRUE;
    }
  } else if (graphics_3d) {
    // Make sure graphics can only be bound to the instance it is
    // associated with.
    if (graphics_3d->pp_instance() == pp_instance() &&
        graphics_3d->BindToInstance(true)) {
      bound_graphics_3d_ = graphics_3d;
      ......
      return PP_TRUE;
    }
  }

  // The instance cannot be bound or the device is not a valid resource type.
  return PP_FALSE;
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/pepper_plugin_instance_impl.cpp。

       Plugin Instance不仅可以将UI渲染在一个2D上下文或者一个3D上下文中,还可以渲染在一个Compositor上下文中。一个2D上下文和一个3D上下文描述的都只是一个Layer,而一个Compositor上下文可以描述多个Layer。也就是说,Plugin Instance可以通过Compositor上下文将自己的UI划分为多个Layer进行渲染。Chromium提供了一个PPB_COMPOSITOR_INTERFACE_0_1接口,Plugin Instance可以通过这个接口为自己创建一个Compositor上下文。

       其中,2D上下文通过一个PepperGraphics2DHost类描述,3D上下文通过PPB_Graphics3D_Impl类描述,而Compositor上下文通过PepperCompositorHost类描述。PepperPluginInstanceImpl类分别通过bound_graphics_2d_platform_、bound_graphics_3d_和bound_compositor_三个成员变量描述上述三种不同的绘图上下文。

       PepperPluginInstanceImpl类的成员函数BindGraphics主要做的工作就是将参数device描述的绘图上下文绑定为当前正在处理的Plugin Instance Proxy的绘图上下文。如果当前正在处理的Plugin Instance Proxy以前绑定过其它的绘图上下文,那么PepperPluginInstanceImpl类的成员函数BindGraphics就会先解除对它们的绑定。

       PepperPluginInstanceImpl类的成员函数BindGraphics接下来再判断参数device描述的绘图上下文是2D上下文、3D上下文,还是Compositor上下文,并且获取它所对应的PepperGraphics2DHost对象、PPB_Graphics3D_Impl对象和PepperCompositorHost对象,然后保存在对应的成员变量中。这样,当前正在处理的Plugin Instance Proxy就知道了自己最新绑定的绘图上下文是什么。最后,这些获得的PepperGraphics2DHost对象、PPB_Graphics3D_Impl对象和PepperCompositorHost对象的成员函数BindToInstance也会被调用,表示它们当前处于被绑定状态。

       在我们这个情景中,参数device描述的绘图上下文是一个3D上下文,也就是一个3D的OpenGL上下文。因此, PepperPluginInstanceImpl类的成员函数BindGraphics最终会获得一个PPB_Graphics3D_Impl对象,并且保存在成员变量bound_graphics_3d_中。最后,这个PPB_Graphics3D_Impl对象的成员函数BindToInstance会被调用,表示它当前处于被绑定的状态,如下所示:

bool PPB_Graphics3D_Impl::BindToInstance(bool bind) {
  bound_to_instance_ = bind;
  return true;
}
      这个函数定义在文件external/chromium_org/content/renderer/pepper/ppb_graphics_3d_impl.cc中。

      从前面的调用过程可以知道,参数bind的值等于true。这时候PPB_Graphics3D_Impl类的成员变量bound_to_instance_的值也会被设置为true,表示当前正在处理的PPB_Graphics3D_Impl对象描述的3D上下文处于被绑定状态。

      这一步执行完成后,GLES2DemoInstance类就通过PPB_GRAPHICS_3D_INTERFACE_1_0和PPB_INSTANCE_INTERFACE_1_0这两个接口创建和绑定了一个OpenGL上下文,也就是初始化好一个OpenGL环境。接下来,GLES2DemoInstance类就可以通过PPB_OPENGLES2_INTERFACE接口进行3D渲染了。这个PPB_OPENGLES2_INTERFACE接口是在GLES2DemoInstance类的构造函数中获取的,如下所示:

GLES2DemoInstance::GLES2DemoInstance(PP_Instance instance, pp::Module* module)
    : pp::Instance(instance), pp::Graphics3DClient(this),
      callback_factory_(this),
      module_(module),
      context_(NULL),
      fullscreen_(false) {
  assert((gles2_if_ = static_cast<const PPB_OpenGLES2*>(
      module->GetBrowserInterface(PPB_OPENGLES2_INTERFACE))));
  ......
}

       这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

       GLES2DemoInstance类的构造函数是通过调用参数module指向的一个pp::Module对象的成员函数GetBrowserInterface获取PPB_OPENGLES2_INTERFACE接口的。在前面Chromium插件(Plugin)模块(Module)加载过程分析一文中,我们已经分析过了pp::Module类的GetBrowserInterface的实现,并且我们也知道,在Plugin进程中,PPB_OPENGLES2_INTERFACE接口是由一个PPB_OpenGLES2对象实现的。这个PPB_OpenGLES2对象的定义如下所示:

  static const struct PPB_OpenGLES2 ppb_opengles2 = {  
      &ActiveTexture,                       &AttachShader,  
      &BindAttribLocation,                  &BindBuffer,  
      &BindFramebuffer,                     &BindRenderbuffer,  
      &BindTexture,                         &BlendColor,  
      &BlendEquation,                       &BlendEquationSeparate,  
      &BlendFunc,                           &BlendFuncSeparate,  
      &BufferData,                          &BufferSubData,  
      &CheckFramebufferStatus,              &Clear,  
      ......  
      &UseProgram,                          &ValidateProgram,  
      &VertexAttrib1f,                      &VertexAttrib1fv,  
      &VertexAttrib2f,                      &VertexAttrib2fv,  
      &VertexAttrib3f,                      &VertexAttrib3fv,  
      &VertexAttrib4f,                      &VertexAttrib4fv,  
      &VertexAttribPointer,                 &Viewport}; 
       这个对象定义在文件这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppb_opengles2_shared.cc中。

       从这里我们就可以看到PPB_OPENGLES2_INTERFACE接口提供的函数,例如函数Clear,就相当于是OpenGL函数glClear。

       有了这个PPB_OPENGLES2_INTERFACE接口之后,GLES2DemoInstance类就会在成员函数FlickerAndPaint中调用它提供的函数进行3D渲染,如下所示:

void GLES2DemoInstance::FlickerAndPaint(int32_t result, bool paint_blue) {
  ......

  float r = paint_blue ? 0 : 1;
  float g = 0;
  float b = paint_blue ? 1 : 0;
  float a = 0.75;
  gles2_if_->ClearColor(context_->pp_resource(), r, g, b, a);
  gles2_if_->Clear(context_->pp_resource(), GL_COLOR_BUFFER_BIT);
  ......

  context_->SwapBuffers(cb);
  ......
}
       这个函数定义在文件external/chromium_org/ppapi/examples/gles2/gles2.cc中。

       GLES2DemoInstance类的成员函数FlickerAndPaint执行的3D渲染很简单,仅仅是调用PPB_OPENGLES2_INTERFACE接口提供的函数ClearColor和Clear绘制一个背景色,相当于是调用了OpenGL函数glClearColor和glClear绘制一个背景色。在实际开发中,我们可以调用PPB_OPENGLES2_INTERFACE接口提供的其它函数完成更复杂的3D渲染效果。

       从前面的分析可以知道,GLES2DemoInstance类的成员变量context_指向的是一个pp::Graphics3D对象。这个Graphics3D描述的是一个OpenGL上下文。每完成一帧渲染,GLES2DemoInstance类的成员函数FlickerAndPaint都需要调用这个pp::Graphics3D对象的成员函数SwapBuffers,用来交换当前使用的OpenGL上下文的前后两个缓冲区,相当于是调用了EGL函数eglSwapBuffers。

       接下来,我们就以PPB_OPENGLES2_INTERFACE接口提供的函数Clear的执行过程为例,分析Plugin是如何通过PPB_OPENGLES2_INTERFACE接口执行3D渲染操作的,它的实现如下所示:

void Clear(PP_Resource context_id, GLbitfield mask) {
  Enter3D enter(context_id, true);
  if (enter.succeeded()) {
    ToGles2Impl(&enter)->Clear(mask);
  }
}
      这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppb_opengles2_shared.cc中。

      Enter3D定义为一个thunk::EnterResource<thunk::PPB_Graphics3D_API>类,如下所示:

typedef thunk::EnterResource<thunk::PPB_Graphics3D_API> Enter3D;
      这个类定义在文件external/chromium_org/ppapi/shared_impl/ppb_opengles2_shared.cc中。

      回到函数Clear中, 它的参数context_id描述的是一个OpenGL上下文资源ID。函数Clear首先将这个资源ID封装在一个EnterResource<thunk::PPB_Graphics3D_API>对象中,如下所示:

template<typename ResourceT, bool lock_on_entry = true>
class EnterResource
    : public subtle::LockOnEntry<lock_on_entry>,  // Must be first; see above.
      public subtle::EnterBase {
 public:
  EnterResource(PP_Resource resource, bool report_error)
      : EnterBase(resource) {
    Init(resource, report_error);
  }
  ......
};
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.h中。

       EnterResource<thunk::PPB_Graphics3D_API>类的构造函数首先会调用父类EnterBase的构造函数。在调用的时候,会将参数resource描述的资源ID传递进去。EnterBase的构造函数通过这个资源ID获得对应的资源对象,保存在成员变量resource_中,如下所示:

EnterBase::EnterBase(PP_Resource resource)
    : resource_(GetResource(resource)),
      retval_(PP_OK) {
  PpapiGlobals::Get()->MarkPluginIsActive();
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.cc中。

       从前面的分析可以知道,这个资源对象实际上就是一个ppapi::proxy::Graphics3D对象。与此同时,EnterBase的构造函数会通过调用Plugin进程的一个PluginGlobals单例对象的成员函数MarkPluginIsActive将当前正在执行3D渲染操作的Plugin标记为Active状态。

       回到EnterResource<thunk::PPB_Graphics3D_API>类的构造函数中,它接下来又会调用另外一个成员函数Init执行其它的初始化工作,如下所示:

template<typename ResourceT, bool lock_on_entry = true>
class EnterResource
    : public subtle::LockOnEntry<lock_on_entry>,  // Must be first; see above.
      public subtle::EnterBase {
 public:
  .......

  ResourceT* object() { return object_; }
  ......

 private:
  void Init(PP_Resource resource, bool report_error) {
    if (resource_)
      object_ = resource_->GetAs<ResourceT>();
    ......
  }

  ResourceT* object_;

  ......
};
       这个函数定义在文件external/chromium_org/ppapi/thunk/enter.h中。

       EnterResource<thunk::PPB_Graphics3D_API>类的成员函数Init主要是将从父类EnterBase继承下来的成员变量resource_指向的ppapi::proxy::Graphics3D对象转换为一个thunk::PPB_Graphics3D_API对象,并且保存在成员变量object_中。这个thunk::PPB_Graphics3D_API对象以后可以通过调用EnterResource<thunk::PPB_Graphics3D_API>类的成员函数object获得。

       注意,ppapi::proxy::Graphics3D类是从ppapi::PPB_Graphics3D_Shared类继承下来的,ppapi::PPB_Graphics3D_Shared类又是从thunk::PPB_Graphics3D_API类继承下来的,因此,我们可以将ppapi::proxy::Graphics3D对象转换为一个thunk::PPB_Graphics3D_API对象。

       这一步执行完成后,回到前面分析的函数函数Clear中,这时候它就构造了一个EnterResource<thunk::PPB_Graphics3D_API>对象,并且这个EnterResource<thunk::PPB_Graphics3D_API>对象的成员变量object_指向了一个thunk::PPB_Graphics3D_API对象。函数Clear接下来又会调用另外一个函数ToGles2Impl从前面构造的EnterResource<thunk::PPB_Graphics3D_API>对象中获得一个GLES2Implementation对象,如下所示:

gpu::gles2::GLES2Implementation* ToGles2Impl(Enter3D* enter) {
  ......
  return static_cast<PPB_Graphics3D_Shared*>(enter->object())->gles2_impl();
}
       这个函数定义在文件ternal/chromium_org/ppapi$ vi shared_impl/ppb_opengles2_shared.cc中。

       函数ToGles2Impl首先调用参数enter指向的EnterResource<thunk::PPB_Graphics3D_API>对象的成员函数object获得它的成员变量object_所指向的一个thunk::PPB_Graphics3D_API对象。

       前面提到,由于EnterResource<thunk::PPB_Graphics3D_API>类的成员变量object_指向的实际上是一个ppapi::proxy::Graphics3D对象,并且ppapi::proxy::Graphics3D类是从ppapi::PPB_Graphics3D_Shared类继承下来的。因此函数ToGles2Impl又可以将它获得的thunk::PPB_Graphics3D_API对象转换为一个ppapi::PPB_Graphics3D_Shared对象。

       有了这个ppapi::PPB_Graphics3D_Shared对象之后,函数ToGles2Impl就可以调用它的成员函数gles2_impl获得它内部维护的一个GLES2Implementation对象。这个GLES2Implementation对象就是在前面分析的ppapi::proxy::Graphics3D类的成员函数CreateGLES2Impl中创建的。

       函数ToGles2Impl最后会将获得的GLES2Implementation对象返回给函数Clear。函数Clear获得了这个GLES2Implementation对象之后,就会调用它的成员函数Clear给参数context_id描述的OpenGL上下文设置一个背景色。

       从前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文可以知道,当我们调用GLES2Implementation类的成员函数执行GPU命令时,这些GPU命令实际上只是写入到了一个Command Buffer中。这个Command Buffer在合适的时候将会调用它的成员函数WaitForGetOffsetInRange通知GPU进程执行它里面的GPU命令。

       从前面的分析可以知道,为Plugin创建的GLES2Implementation对象所用的Command Buffer是通过一个PpapiCommandBufferProxy对象描述的。当这个WaitForGetOffsetInRange对象的成员函数WaitForGetOffsetInRange被调用的时候,它实际上只是向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_WaitForGetOffsetInRange的IPC消息,如下所示:

void PpapiCommandBufferProxy::WaitForTokenInRange(int32 start, int32 end) {
  ......

  bool success;
  gpu::CommandBuffer::State state;
  if (Send(new PpapiHostMsg_PPBGraphics3D_WaitForTokenInRange(
          ppapi::API_ID_PPB_GRAPHICS_3D,
          resource_,
          start,
          end,
          &state,
          &success)))
    UpdateState(state, success);
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppapi_command_buffer_proxy.cc中。

       这个类型为PpapiHostMsg_PPBGraphics3D_WaitForGetOffsetInRange的IPC消息携带了一个参数ppapi::API_ID_PPB_GRAPHICS_3D,表示Render进程接收到这个IPC消息之后,要分发一个API_ID_PPB_GRAPHICS_3D接口处理。

       前面提到,在Render进程中,API_ID_PPB_GRAPHICS_3D接口是由一个PPB_Graphics3D_Proxy对象实现的,也就是Render进程会将类型为PpapiHostMsg_PPBGraphics3D_WaitForGetOffsetInRange的IPC消息分发给该PPB_Graphics3D_Proxy对象处理,这是通过调用它的成员函数OnMessageReceived实现的,如下所示:

bool PPB_Graphics3D_Proxy::OnMessageReceived(const IPC::Message& msg) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(PPB_Graphics3D_Proxy, msg)
#if !defined(OS_NACL)
    ......
    IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBGraphics3D_WaitForGetOffsetInRange,
                        OnMsgWaitForGetOffsetInRange)
    ......
#endif  // !defined(OS_NACL)

    ......
    IPC_MESSAGE_UNHANDLED(handled = false)

  IPC_END_MESSAGE_MAP()
  // FIXME(brettw) handle bad messages!
  return handled;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc。

       从这里可以看到,PPB_Graphics3D_Proxy类的成员函数OnMessageReceived会将类型为PpapiHostMsg_PPBGraphics3D_WaitForGetOffsetInRange的IPC消息分发给另外一个成员函数OnMsgWaitForGetOffsetInRange处理,如下所示:

void PPB_Graphics3D_Proxy::OnMsgWaitForGetOffsetInRange(
    const HostResource& context,
    int32 start,
    int32 end,
    gpu::CommandBuffer::State* state,
    bool* success) {
  EnterHostFromHostResource<PPB_Graphics3D_API> enter(context);
  ......
  *state = enter.object()->WaitForGetOffsetInRange(start, end);
  *success = true;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc。

       从前面的分析可以知道,参数context描述的OpenGL上下文在Render进程中用一个PPB_Graphics3D_Impl对象描述。PPB_Graphics3D_Proxy类的成员函数OnMsgWaitForGetOffsetInRange会通过构造一个EnterHostFromHostResource<PPB_Graphics3D_API>对象来获得这个PPB_Graphics3D_Impl对象,并且会调用这个PPB_Graphics3D_Impl对象的成员函数WaitForGetOffsetInRange,用来通知它将Plugin写入在Command Buffer中的GPU命令发送给GPU进程处理。

        PPB_Graphics3D_Impl类的成员函数WaitForGetOffsetInRange的实现如下所示:

gpu::CommandBuffer::State PPB_Graphics3D_Impl::WaitForGetOffsetInRange(
    int32_t start,
    int32_t end) {
  GetCommandBuffer()->WaitForGetOffsetInRange(start, end);
  return GetCommandBuffer()->GetLastState();
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/ppb_graphics_3d_impl.cc中。

       PPB_Graphics3D_Impl类的成员函数WaitForGetOffsetInRange首先调用成员函数GetCommandBuffer获得一个CommandBuffer对象,如下所示:

gpu::CommandBuffer* PPB_Graphics3D_Impl::GetCommandBuffer() {
  return command_buffer_;
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/ppb_graphics_3d_impl.cc中。

       PPB_Graphics3D_Impl类的成员函数GetCommandBuffer返回的是成员变量command_buffer_指向的一个CommandBuffer对象。从前面的分析可以知道,这个CommandBuffer对象实现上是一个CommandBufferProxyImpl对象,它是在前面分析的PPB_Graphics3D_Impl类的成员函数InitRaw中创建的。

       回到PPB_Graphics3D_Impl类的成员函数WaitForGetOffsetInRange中,它获得了一个CommandBufferProxyImpl对象之后,就会调用它的成员函数WaitForGetOffsetInRange通知GPU进程执行Plugin写入在Command Buffer中的GPU命令。这个通知过程可以参考前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       这一步执行完成后,回到前面分析的GLES2DemoInstance类的成员函数FlickerAndPaint中,它通过PPB_OPENGLES2_INTERFACE接口提供的函数渲染好一帧后,就会调用其成员变量context_指向的一个pp::Graphics3D对象的成员函数SwapBuffers交换Plugin当前使用的OpenGL上下文的前后两个缓冲区,也就是相当于是执行EGL函数eglSwapBuffers。

       接下来,我们就从pp::Graphics3D类的成员函数SwapBuffers开始,分析Plugin交换当前使用的OpenGL上下文的前后两个缓冲区的过程,如下所示:

int32_t Graphics3D::SwapBuffers(const CompletionCallback& cc) {
  ......

  return get_interface<PPB_Graphics3D_1_0>()->SwapBuffers(
      pp_resource(),
      cc.pp_completion_callback());
}
       这个函数定义在文件external/chromium_org/ppapi/cpp/graphics_3d.cc中。

       pp::Graphics3D类的成员函数SwapBuffers首先调用我们前面分析过的模板函数get_interface<PPB_Graphics3D_1_0>获得一个PPB_GRAPHICS_3D_INTERFACE_1_0接口。获得了PPB_GRAPHICS_3D_INTERFACE_1_0接口之后,再调用它提供的函数SwapBuffers请求Render进程交换正在处理的pp::Graphics3D对象描述的OpenGL上下文的前后缓冲区。

       PPB_GRAPHICS_3D_INTERFACE_1_0接口提供的函数SwapBuffers的实现如下所示:

int32_t SwapBuffers(PP_Resource context,
                    struct PP_CompletionCallback callback) {
  ......
  EnterResource<PPB_Graphics3D_API> enter(context, callback, true);
  if (enter.failed())
    return enter.retval();
  return enter.SetResult(enter.object()->SwapBuffers(enter.callback()));
}
       这个函数定义在文件external/chromium_org/ppapi/thunk/ppb_graphics_3d_thunk.cc中。

       与前面分析的PPB_OPENGLES2_INTERFACE接口提供的函数Clear类似,PPB_GRAPHICS_3D_INTERFACE_1_0接口提供的函数SwapBuffers也是通过构造一个EnterResource<thunk::PPB_Graphics3D_API>对象来获得参数context描述的一个OpenGL上下文资源对象,也就是一个ppapi::proxy::Graphics3D对象。有了这个ppapi::proxy::Graphics3D对象之后,就可以调用它的成员函数SwapBuffers请求Render进程交换它描述的OpenGL上下文的前后缓冲区了。

       ppapi::proxy::Graphics3D类的成员函数SwapBuffers是从父类ppapi::PPB_Graphics3D_Shared继承下来的,它的实现如下所示:

int32_t PPB_Graphics3D_Shared::SwapBuffers(
    scoped_refptr<TrackedCallback> callback) {
  ......
  return DoSwapBuffers();
}
       这个函数定义在文件external/chromium_org/ppapi/shared_impl/ppb_graphics_3d_shared.cc中。

       ppapi::proxy::Graphics3D类的成员函数SwapBuffers又会调用由子类实现的成员函数DoSwapBuffers请求Render进程交换当前正在处理的ppapi::proxy::Graphics3D对象描述的OpenGL上下文的前后缓冲区。

       在我们这个情景中,这个子类即为ppapi::proxy::Graphics3D,因此接下来ppapi::proxy::Graphics3D类的成员函数DoSwapBuffers会被调用,它的实现如下所示:

int32 Graphics3D::DoSwapBuffers() {
  gles2_impl()->SwapBuffers();
  IPC::Message* msg = new PpapiHostMsg_PPBGraphics3D_SwapBuffers(
      API_ID_PPB_GRAPHICS_3D, host_resource());
  msg->set_unblock(true);
  PluginDispatcher::GetForResource(this)->Send(msg);

  return PP_OK_COMPLETIONPENDING;
}
      这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

      ppapi::proxy::Graphics3D类的成员函数DoSwapBuffers首先是通过调用成员函数gles2_impl获得前面为Plugin创建的一个Command Buffer GL接口。有了这个Command Buffer GL接口之后,就可以调用它的成员函数SwapBuffers向Command Buffer写入一个gles2::cmds::SwapBuffers命令。GPU进程在执行这个命令的时候,就会交换当前正在处理的ppapi::proxy::Graphics3D对象描述的OpenGL上下文的前后缓冲区。

      ppapi::proxy::Graphics3D类的成员函数DoSwapBuffers接下来又会调用PluginDispatcher类的静态成员函数GetForResource获得与当前正在处理的Plugin对应的一个PluginDispatcher对象,并且通过这个PluginDispatcher对象向Render进程发送一个类型为PpapiHostMsg_PPBGraphics3D_SwapBuffers的IPC消息,用来通知Render进程更新当前正在处理的Plugin在网页上的视图。这个IPC消息包含两个参数:

      1. API_ID_PPB_GRAPHICS_3D,表示Render进程要将该IPC消息分发给API_ID_PPB_GRAPHICS_3D接口处理。

      2. 通过调用成员函数host_resource获得的一个HostResource对象,这个HostResource对象描述的就是要交换前面缓冲区的OpenGL上下文。

      前面提到,在Render进程中,API_ID_PPB_GRAPHICS_3D接口是由一个PPB_Graphics3D_Proxy对象实现的,也就是Render进程会将类型为PpapiHostMsg_PPBGraphics3D_SwapBuffers的IPC消息分发给该PPB_Graphics3D_Proxy对象处理,这是通过调用它的成员函数OnMessageReceived实现的,如下所示:

bool PPB_Graphics3D_Proxy::OnMessageReceived(const IPC::Message& msg) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(PPB_Graphics3D_Proxy, msg)
#if !defined(OS_NACL)
    ......
    IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBGraphics3D_SwapBuffers,
                        OnMsgSwapBuffers)
    ......
#endif  // !defined(OS_NACL)

    ......
    IPC_MESSAGE_UNHANDLED(handled = false)

  IPC_END_MESSAGE_MAP()
  // FIXME(brettw) handle bad messages!
  return handled;
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

       从这里可以看到,PPB_Graphics3D_Proxy类的成员函数OnMessageReceived会将类型为PpapiHostMsg_PPBGraphics3D_SwapBuffers的IPC消息分发给另外一个成员函数OnMsgSwapBuffers处理,如下所示:

void PPB_Graphics3D_Proxy::OnMsgSwapBuffers(const HostResource& context) {
  EnterHostFromHostResourceForceCallback<PPB_Graphics3D_API> enter(
      context, callback_factory_,
      &PPB_Graphics3D_Proxy::SendSwapBuffersACKToPlugin, context);
  if (enter.succeeded())
    enter.SetResult(enter.object()->SwapBuffers(enter.callback()));
}
       这个函数定义在文件external/chromium_org/ppapi/proxy/ppb_graphics_3d_proxy.cc中。

       参数context描述的是一个要交换前后缓冲区的OpenGL上下文。从前面的分析可以知道,在Render进程中,这个OpenGL上下文是通过一个PPB_Graphics3D_Impl对象描述的。PPB_Graphics3D_Proxy类的成员函数OnMsgSwapBuffers通过构造一个EnterHostFromHostResourceForceCallback<PPB_Graphics3D_API>来获得这个PPB_Graphics3D_Impl对象,并且调用这个PPB_Graphics3D_Impl对象的成员函数SwapBuffers更新当前正在处理的Plugin在网页上的视图。

       PPB_Graphics3D_Impl类的成员函数SwapBuffers是从父类ppapi::PPB_Graphics3D_Shared继承下来的。前面我们已经分析过ppapi::PPB_Graphics3D_Shared类的成员函数SwapBuffers的实现了,它最终会调用由子类实现的成员函数DoSwapBuffers,即PPB_Graphics3D_Impl类的成员函数DoSwapBuffers。

       PPB_Graphics3D_Impl类的成员函数DoSwapBuffers的实现如下所示:

int32 PPB_Graphics3D_Impl::DoSwapBuffers() {
  ......

  if (bound_to_instance_) {
    // If we are bound to the instance, we need to ask the compositor
    // to commit our backing texture so that the graphics appears on the page.
    // When the backing texture will be committed we get notified via
    // ViewFlushedPaint().
    //
    // Don't need to check for NULL from GetPluginInstance since when we're
    // bound, we know our instance is valid.
    HostGlobals::Get()->GetInstance(pp_instance())->CommitBackingTexture();
    commit_pending_ = true;
  } 

  ......

  return PP_OK_COMPLETIONPENDING;
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/ppb_graphics_3d_impl.cc中。

       从前面的分析可以知道,当前正在处理的PPB_Graphics3D_Impl对象描述的OpenGL上下文此时处于绑定状态,也就是它的成员变量bound_to_instance_的值等于true。在这种情况下,PPB_Graphics3D_Impl类的成员函数DoSwapBuffers首先会通过HostGlobals类的静态成员函数Get获得当前Render进程中的一个HostGlobals单例对象。有了这个HostGlobals单例对象之后,就可以调用它的成员函数GetInstance获得与当前正在处理的PPB_Graphics3D_Impl对象进行绑定的一个Plugin Instance Proxy,也就是一个PepperPluginInstanceImpl对象。

      有了上述PepperPluginInstanceImpl对象之后,PPB_Graphics3D_Impl类的成员函数DoSwapBuffers就可以调用它的成员函数CommitBackingTexture更新它在网页上的视图,如下所示:

void PepperPluginInstanceImpl::CommitBackingTexture() {
  ......
  gpu::Mailbox mailbox;
  uint32 sync_point = 0;
  bound_graphics_3d_->GetBackingMailbox(&mailbox, &sync_point);
  ......
  texture_layer_->SetTextureMailboxWithoutReleaseCallback(
      cc::TextureMailbox(mailbox, GL_TEXTURE_2D, sync_point));
  texture_layer_->SetNeedsDisplay();
}
       这个函数定义在文件external/chromium_org/content/renderer/pepper/pepper_plugin_instance_impl.cc。 

       从前面的分析可以知道,PepperPluginInstanceImpl类的成员变量bound_graphics_3d_指向的是一个PPB_Graphics3D_Impl对象。这个PPB_Graphics3D_Impl对象描述的是一个OpenGL上下文。这个OpenGL上下文就是当前正在处理的PepperPluginInstanceImpl对象绑定的OpenGL上下文。

       PepperPluginInstanceImpl类的另外一个成员变量texture_layer_指向的是一个cc::TextureLayer对象。前面提到,网页中的每一个<embed>标签在网页的CC Layer Tree中都对应有一个Texture Layer。这个Texture Layer就是通过上述cc::TextureLayer对象描述的。

       有了上述PPB_Graphics3D_Impl对象之后,PepperPluginInstanceImpl类的成员函数CommitBackingTexture就可以调用它的成员函数GetBackingMailbox获得已经被交换到后端的缓冲区。这个缓冲区实际上是一个纹理缓冲区。这个纹理缓冲区将会作为<embed>标签的内容,因此PepperPluginInstanceImpl类的成员函数CommitBackingTexture就会将它设置上述cc::TextureLayer对象所要绘制的内容。这是通过调用该cc::TextureLayer对象的成员函数SetTextureMailboxWithoutReleaseCallback实现的。

       最后,PepperPluginInstanceImpl类的成员函数CommitBackingTexture调用上述cc::TextureLayer对象的成员函数SetNeedsDisplay将自己标记为需要更新。这样在下一个VSync信号到来的时候,前面设置为它的内容的纹理缓冲区就会被渲染和合成在网页的UI上。这个渲染和合成过程可以参考前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章。

       这样,我们就分析完成了Plugin执行3D渲染的过程,也就是Plugin为<embed>标签绘制3D视图的过程,这是通过调用Chromium提供的PPB_OPENGLES2_INTERFACE接口完成的。从这个过程我们就可以看到Plugin和Chromium的交互过程,也就是它们可以互相调用对方提供的接口,从而使得Plugin可以像我们在前面Chromium扩展(Extension)机制简要介绍和学习计划这个系列的文章提到的Extension一样,增强网页的功能。

       至此,我们也分析完成了Chromium的Plugin机制。重新学习可以参考前面Chromium插件(Plugin)机制简要介绍和学习计划一文。更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/11/14 1:00:15 原文链接
阅读:31008 评论:4 查看评论

Android WebView简要介绍和学习计划

$
0
0

       我们通常会在App的UI中嵌入WebView,用来实现某些功能的动态更新。在4.4版本之前,Android WebView基于WebKit实现。不过,在4.4版本之后,Android WebView就换成基于Chromium的实现了。基于Chromium实现,使得WebView可以更快更流畅地显示网页。本文接下来就介绍Android WebView基于Chromium的实现原理,以及制定学习计划。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       通过前面几个系列文章的学习,我们知道,Chromium的实现是相当复杂的。这种复杂可以体现在编译出来的动态库的大小上,带符号版本1.3G左右,去掉符号后还有27M左右。编译过AOSP源码的都知道,在整个编译过程中,Chromium库占用了大部分的时间,尤其是在生成上述1.3G文件时,电脑几乎卡住了。因此,在使用Chromium实现WebView时,第一个要解决的问题就是它的动态库加载问题。

       Android系统的动态库是ELF文件结构。ELF文件由ELF Header、Program Header Table、Section以及Section Header Table组成。ELF Header位于文件的开头,它同时描述了Program Header Table和Section Header Table在文件中的偏移位置。Section Header Table描述了动态库由哪些Section组成,典型的Section有.text、.data、.bss和.rodata等。这些Section描述的要么是程序代码,要么是程序数据。Program Header Table描述了动态库由哪些Segment组成。一个Segment又是由一个或者若干个Section组成的。Section信息是在程序链接期间用到的,而Segment信息是在程序加载期间用到的。关于ELF文件格式的更详细描述,可以参考Executable and Linkable Format

       对于动态库来说,它的程序代码是只读的。这意味着一个动态库不管被多少个进程加载,它的程序代码都是只有一份。但是,动态库的程序数据,链接器在加载的时候,会为每一个进程都独立加载一份。对于Chromium动态库来说,它的程序代码占据了95%左右的大小,也就是25.65M左右。剩下的5%,也就是1.35M,是程序数据。假设有N个App使用了WebView,并且这个N个App的进程都存在系统中,那么系统就需要为Chromium动态库分配(25.65 + 1.35 × N)M内存。这意味着,N越大,Chromium动态库就占用越多的系统内存。

       在Chromium动态库1.35M的程序数据中,大概有1.28M在加载后经过一次重定位操作之后就不会发生变化。这些数据包含C++虚函数表,以及所有指针类型的常量。它们会被链接器放在一个称为GNU_RELRO Section中,如图1所示:


图1 App进程间不共享GNU_RELRO Section

       如果我们将Chromium动态库的GNU_RELRO Section看成是普通的程序数据,那么Android系统就需要为每一个使用了WebView的App进程都分配1.28M的内存。这将会造成内存浪费。如果我们能App进程之间共享Chromium动态库的GNU_RELRO Section,那么不管有多少个App进程使用了WebView,它占用的内存大小都是1.28M,如图2所示:


图2 App进程间共享GNU_RELRO Section

       这是有可能做到的,毕竟它与程序代码类似,在运行期间都是只读的。不同的地方在于,程序代码在加载后自始至终都不用修改,而GNU_RELRO Section的内容在重定位期间,是需要进行一次修改的,之后才是只读的。但是修改的时候,Linux的COW(Copy On Write)机制就会使得它不能再在各个App进程之间实现共享。

       为了使得Chromium动态库能在不同的App进程之间共享,Android系统执行了以下操作:

       1. Zygote进程在启动的过程中,为Chromium动态库保留了一段足够加载它的虚拟地址内间。我们假设这个虚拟地址空间的起始地址为gReservedAddress,大小为gReservedSize。

       2. System进程在启动的过程中,会请求Zygote进程fork一个子进程,并且在上述保留的虚拟地址空间[gReservedAddress, gReservedAddress + gReservedSize)中加载Chromium动态库。Chromium动态库的Program Header指定了它的GNU_RELRO Section的加载位置。这个位置是相对基地址gReservedAddress的一个偏移量。我们假设这个偏移量为gRelroOffset。上述子进程完成Chromium动态库的GNU_RELRO Section的重定位操作之后,会将它的内容写入到一个RELRO文件中去。

       3. App进程在创建WebView的时候,Android系统也会在上述保留的虚拟地址空间[gReservedAddress, gReservedAddress + gReservedSize)中加载Chromium动态库,并且会直接将第2步得到的RELRO文件内存映射到虚拟地址空间[gReservedAddress + gRelroOffset,  gReservedAddress + gRelroOffset + 1.28)去。

       关于Zygote进程、System进程和App进程的启动过程,可以参考Android系统进程Zygote启动过程的源代码分析Android应用程序进程启动过程的源代码分析这两篇文章。

       Android 5.0的Linker提供了一个新的动态库加载函数android_dlopen_ext。这个函数不仅可以将一个动态库加载在指定的虚拟地址空间中,还可以在该动态库重定位操作完成后,将其GNU_RELRO Section的内容写入到指定的RELRO文件中去,同时还可以在加载一个动态库时,使用指定的RELRO文件内存映射为它的GNU_RELRO Section。因此,上述的第2步和第3步可以实现。函数android_dlopen_ext还有另外一个强大的功能,它可以从一个指定的文件描述符中读入要加载的动态库内容。通常我们是通过文件路径指定要加载的动态库,有了函数android_dlopen_ext之后,我们就不再受限于从本地文件加载动态库了。

       接下来,我们进一步分析为什么上述3个操作可以使得Chromium动态库能在不同的App进程之间共享。

       首先,第2步的子进程、第3步的App进程都是由第1步的Zygote进程fork出来的,因此它们都具有一段保留的虚拟地址空间[gReservedAddress, gReservedAddress + gReservedSize)。

       其次,第2步的子进程和第3步的App进程,都是将Chromium动态库加载在相同的虚拟地址空间中,因此,可以使用前者生成的RELRO文件内存映射为后者的GNU_RELRO Section。

       第三,所有使用了WebView的App进程,都将相同的RELRO文件内存映射为自己加载的Chromium动态库的GNU_RELRO Section,因此就实现了共享。

       App进程加载了Chromium动态库之后,就可以启动Chromium渲染引擎了。Chromium渲染引擎实际上是由Browser进程、Render进程和GPU进程组成的。其中,Browser进程负责将网页的UI合成在屏幕上,Render进程负责加载和渲染网页的UI,GPU进程负责执行Browser进程和Render进程发出的GPU命令。由于Chromium也支持单进程架构(在这种情况下,它的Browser进程、Render进程和GPU进程都是通过线程模拟的),因此接下来我们将它的Browser进程、Render进程和GPU进程统称为Browser端、Render端和GPU端。相应地,启动Chromium渲染引擎,就是要启动它的Browser端、Render端和GPU端。

       对于Android WebView来说,它启动Chromium渲染引擎的过程如图3所示:


图3 Android WebView启动Chromium渲染引擎的过程

       当我们在App的UI中嵌入一个WebView时,WebView内部会创建一个WebViewChromium对象。从名字就可以看出,WebViewChromium是基于Chromium实现的WebView。Chromium里面有一个android_webview模块。这个模块提供了两个类AwBrowserProcess和AwContents,分别用来封装Chromium的Content层提供的两个类BrowserStartupController和ContentViewCore。

       从前面Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文可以知道,通过Chromium的Content层提供的BrowserStartupController类,可以启动Chromium的Browser端。不过,对于Android WebView来说,它并没有单独的Browser进程,它的Browser端是通过App进程的主线程(即UI线程)实现的。      

       Chromium的Content层会通过BrowserStartupController类在App进程的UI线程中启动一个Browser Main Loop。以后需要请求Chromium的Browser端执行某一个操作时,就向这个Browser Main Loop发送一个Task即可。这个Browser Main Loop最终会在App进程的UI线程中执行请求的Task。

       从前面Chromium网页Frame Tree创建过程分析一文可以知道,通过Chromium的Content层提供的ContentViewCore类,可以创建一个Render进程,并且在这个Render进程中加载指定的网页。不过,对于Android WebView来说,它是一个单进程架构,也就是它没有单独的Render进程用来加载网页。这时候ContentViewCore类将会创建一个线程来模拟Render进程。也就是说,Android WebView的Render端是通过在App进程中创建的一个线程实现的。这个线程称为In-Process Renderer Thread。

       现在,Android WebView具有Browser端和Render端了,它还需要有一个GPU端。从前面Chromium的GPU进程启动过程分析一文可以知道,在Android平台上,Chromium的GPU端是通过在Browser进程中创建一个GPU线程实现的。不过,对于Android WebView来说,它并没有一个单独的GPU线程。那么,Chromium的GPU命令由谁来执行呢?

       我们知道,从Android 3.0开始,App的UI就支持硬件加速渲染了,也就是支持通过GPU来渲染。到了Android 4.0,App的UI默认就是硬件加速渲染了。这时候App的UI线程就是一个OpenGL线程,也就是它可以用来执行GPU命令。再到Android 5.0,App的UI线程只负责收集UI渲染操作,也就是构建一个Display List。保存在这个Display List中的操作,最终会交给一个Render Thread执行。Render Thread又是通过GPU来执行这些渲染操作的。这时候App的UI线程不再是一个OpenGL线程,Render Thread才是。Android WebView的GPU命令就是由这个Render Thread执行的。当然,在Android 4.4时,Android WebView的GPU命令还是由App的UI线程执行的。这里我们只讨论Android 5.0的情况,具体可以参考Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章。

       Chromium在android_webview模块中提供了一个DeferredGpuCommandService服务。当Android WebView的Render端需要通过GPU命令绘制网页UI时,它就会通过DeferredGpuCommandService服务提供的RequestProcessGL接口向App的Display List增加一个类型为DrawFunctorOp的操作。当Display List从UI线程同步给Render Thread的时候,它里面包含的DrawFunctorOp操作就会被执行。这时候Android WebView的Render端请求的GPU命令就会在App的Render Thread中得到执行了。

       Android WebView的Browser端在合成网页UI时,是运行在App的Render Thread中的。这时候它仍然是通过DeferredGpuCommandService服务提供的RequestProcessGL接口请求执行GPU命令。不过,DeferredGpuCommandService会检测到它当前运行在App的Render Thread中,因此,就会直接执行它请求的GPU命令。

       Chromium为Android WebView独特的GPU命令执行方式(既不是在单独的GPU进程中执行,也不是在单独的GPU线程中执行)提供了一个称为In-Process Command Buffer GL的接口。顾名思义,In-Process Command Buffer GL与Command Buffer GL接口一样,要执行的GPU命令都是先写入到一个Command Buffer中。然而,这个Command Buffer会提交给App的Render Thread进程执行,过程如图4所示:


图4 Android WebView通过In-Process Command Buffer GL接口执行GPU命令的过程

       In-Process Command Buffer GL接口与前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文分析的Command Buffer GL接口一样,都是通过GLES2Implementation类描述的,区别在于前者通过一个InProcessCommandBuffer对象执行GPU命令,而后者通过一个CommandBufferProxyImpl对象执行GPU命令。更具体来说,就是CommandBufferProxyImpl对象会将要执行的GPU命令提交给Chromium的GPU进程/线程处理,而InProcessCommandBuffer对象会将要执行的GPU命令提交给App的Render Thread处理。

       GLES2Implementation类是通过InProcessCommandBuffer类的成员函数Flush请求它处理已经写入在Command Buffer中的GPU命令的。InProcessCommandBuffer类的成员函数Flush又会请求前面提到的DeferredGpuCommandService服务调度一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread,意思就是说要在App的Render Thread线程中调用InProcessCommandBuffer类的成员函数FlushOnGpuThread。InProcessCommandBuffer类的成员函数FlushOnGpuThread在调用的过程中,就会执行已经写入在Command Buffer中的GPU命令。

       DeferredGpuCommandService服务接收到调度Task的请求之后,会判断当前线程是否允许执行GL操作,实际上就是判断当前线程是否就是App的Render Thread。如果是的话,那么就会直接调用请求调度的Task绑定的InProcessCommandBuffer类的成员函数FlushOnGpuThread。这种情况发生在Browser端合成网页UI的过程中。Browser端合成网页UI的操作是由App的Render Thread主动发起的,因此这个合成操作就可以在当前线程中直接执行。

       另一方面,DeferredGpuCommandService服务接收到调度Task的请求之后,如果发现当前不允许执行GL操作,那么它就会将该Task保存在内部一个Task队列中,并且通过调用Java层的DrawGLFunctor类的成员函数requestDrawGL请求App的UI线程调度执行一个GL操作。这种情况发生在两个场景中。

       第一个场景发生在前面描述的Render端绘制网页UI的过程中。绘制网页UI相当于就是绘制Android WebView的UI。Android WebView属于App的UI里面一部分,因此它的绘制操作是由App的UI线程发起的。Android WebView在App的UI线程中接收到绘制的请求之后,它就会要求Render端的Compositor线程绘制网页的UI。这个Compositor线程不是一个OpenGL线程,它不能直接执行GL的操作,因此就要请求App的UI线程调度执行GL操作。

       从前面Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章可以知道,App的UI线程主要是负责收集当前UI的渲染操作,并且通过一个DisplayListRenderer将这些渲染操作写入到一个Display List中去。这个Display List最终会同步到App的Render Thread中去。App的Render Thread获得了这个Display List之后,就会通过调用相应的OpenGL函数执行这些渲染操作,也就是通过GPU来执行这些渲染操作。这个过程称为Replay(重放) Display List。重放完成之后,就可以生成App的UI了。

       DrawGLFunctor类的成员函数requestDrawGL请求App的UI线程调度执行的GL操作是一个特殊的渲染操作,它对应的是一个GL函数,而一般的渲染操作是指绘制一个Circle、Rect或者Bitmap等基本操作。GL函数在执行期间,可以执行任意的GPU命令。这个GL操作封装在一个DrawGLFunctor对象。App的UI线程又会进一步将这个rawGLFunctor对象封装成一个DrawFunctionOp操作。这个DrawFunctionOp操作会通过DisplayListRenderer类的成员函数callDrawGLFunction写入到App UI的Display List中。当这个Display List被App的Render Thread重放时,它里面包含的DrawFunctionOp操作就会被OpenGLRenderer类的成员函数callDrawGLFunction执行。

       OpenGLRenderer类的成员函数callDrawGLFunction在执行DrawFunctionOp操作时,就会通知与它关联的DrawGLFunctor对象。这个DrawGLFunctor对象又会调用Chromium的android_webview模块提供的一个GL函数。这个GL函数又会找到一个HardwareRenderer对象。这个HardwareRenderer对象是Android WebView的Browser端用来合成网页的UI的。有了这个HardwareRenderer对象之后,上述GL函数就会调用它的成员函数DrawGL,以便请求前面提到的DeferredGpuCommandService服务执行保存在它内部的Task,这是通过调用它的成员函数PerformIdleTask实现的。

       DeferredGpuCommandService类的成员函数PerformIdleTask会依次执行保存在内部Task队列的每一个Task。从前面的分析可以知道,这时候Task队列中存在一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread。因此,这时候InProcessCommandBuffer类的成员函数FlushOnGpuThread就会在当前线程中被调用,也就是在App的Render Thread中被调用。

       前面提到,InProcessCommandBuffer类的成员函数FlushOnGpuThread在调用的过程中,会执行之前通过In-Process Command Buffer GL接口写入在Command Buffer中的GPU命令。InProcessCommandBuffer类的成员函数FlushOnGpuThread又是通过内部的一个GpuScheduler对象和一个GLES2Decoder对象执行这些GPU命令的。其中,GpuScheduler对象负责将GPU命令调度在它们对应的OpenGL上下文中执行,而GLES2Decoder对象负责将GPU命令翻译成OpenGL函数执行。关于GpuScheduler类和GLES2Decoder类执行GPU命令的过程,可以参考前面前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。

       第二个场景发生Render端光栅化网页UI的过程中。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,Render端通过GPU光栅化网页UI的操作也是发生在Compositor线程中的。这时候它请求App的Render Thread执行GPU命令的大概流程相同,也是将要执行的GPU操作封装在一个DrawGLFunctor对象中。不过,这个DrawGLFunctor对象不会进一步被封装成DrawFunctionOp操作写入到App UI的Display List中,而是直接被App的UI线程放入到App的Render Thread中,App的Render Thread再通知它执行所请求的GPU操作。DrawGLFunctor对象获得执行GPU操作的通知后,后流的流程就与前面描述的第一个场景一致了。

       通过以上描述的方式,Android WebView的Browser端和Render端就可以执行任意的GPU命令了。其中,Render端执行GPU命令是为了绘制网页UI,而Browser端执行GPU命令是为了将Render端渲染出来的网页UI合成在App的UI中,以便最后可以显示在屏幕中。这个过程就是Android WebView硬件加速渲染网页的过程,如图5所示:


图5 Android WebView硬件加速渲染网页过程

       从前面Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章可以知道,App从接收到VSync信号开始渲染一帧UI。两个VSync信号时间间隔由屏幕刷新频率决定。一般的屏幕刷新频率是60fps,这意味着每一个VSync信号到来时,App有16ms的时间绘制自己的UI。

       这16ms时间又划分为三个阶段。第一阶段用来构造App UI的Display List。这个构造工作由App的UI线程执行,表现为各个需要更新的View的成员函数onDraw会被调用。第二阶段App的UI线程会将前面构造的Display List同步给Render Thread。这个同步操作由App的Render Thread执行,同时App的UI线程会被阻塞,直到同步操作完成为止。第三阶段是绘制App UI的Display List。这个绘制操作由App的Render Thread执行。

       由于Android WebView是嵌入在App的UI里面的,因此它每一帧的渲染也是按照上述三个阶段进行的。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道,网页的UI最终是通过一个CC Layer Tree描述的,也就是Android WebView的Render端会创建一个CC Layer Tree来描述它正在加载的网页的UI。与此同时,Android WebView还会为Render端创建一个Synchronous Compositor,用来将网页的UI渲染在一个Synchronous Compositor Output Surface上。渲染得到的最终结果通过一个Compositor Frame描述。这意味网页的每一帧都是通过一个Compositor Frame描述的。

       Android WebView的Browser端负责合成Render端渲染出来的UI。为了完成这个合成操作,它同样会创建一个CC Layer Tree。这个CC Layer Tree只有两个节点,一个是根节点,另外一个是根节点的子节点,称为Delegated Renderer Layer。这个Delegated Renderer Layer的内容来自于Render端的渲染结果,也就是一个Compositor Frame。与此同时,Android WebView会为Browser端创建一个Hardware Renderer,用来将它的CC Layer Tree渲染在一个Parent Output Surface上,实际上就是将网页的UI合成在App的窗口上。

       在第一阶段,Android WebView的成员函数onDraw会在App的UI线程中被调用。在调用期间,它又会调用为Render端创建的Synchronous Compositor的成员函数DemandDrawHw,用来渲染Render端的CC Layer Tree。渲染完成后,为Render端创建的Synchronous Compositor Output Surface的成员函数SwapBuffers就会被调用,并且交给它一个Compositor Frame。这个Compositor Frame描述的就是网页当前的UI,它最终会被保存在一个SharedRendererState对象中。这个SharedRendererState对象是用来描述网页的状态的。

       在第二阶段,保存在上述SharedRendererState对象中的Compositor Frame会被提取出来,并且同步给Browser端的CC Layer Tree,也就是设置为该CC Layer Tree的Delegated Renderer Layer的内容。

       在第三阶段,Android WebView为Browser端创建的Hardware Renderer的成员函数DrawGL会在App的Render Thread中被调用,用来渲染Browser端的CC Layer Tree。渲染完成后,为Browser端创建的Parent Output Surface的成员函数SwapBuffers就会被调用,这时候它就会将得到的网页UI绘制在App的窗口上。

       以上就是Android WebView以硬件加速方式将网页UI渲染在App窗口的过程。当然,Android WebView也可用软件方式将网页UI渲染在App窗口。不过,在这个系列的文章中,我们只要关注硬件加速渲染方式,因为这种渲染方式效率会更高,而且Android系统从4.0版本之后,默认已经是使用硬件加速方式渲染App的UI了。

       接下来,我们就结合源码,按照以下四个情景,详细分析Android WebView硬件加速渲染网页UI的过程:

       1. Android WebView加载Chromium动态库的过程

       2. Android WebView启动Chromium渲染引擎的过程

       3. Android WebView执行OpenGL命令的过程

       4. Android WebView硬件加速渲染网页UI的过程

       分析了这四个情景之后,我们就可以对Android WebView的实现有深刻的认识了。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/11/21 0:59:03 原文链接
阅读:38668 评论:16 查看评论

Android WebView加载Chromium动态库的过程分析

$
0
0

       Chromium动态库的体积比较大,有27M左右,其中程序段和数据段分别占据25.65M和1.35M。如果按照通常方式加载Chromium动态库,那么当有N个正在运行的App使用WebView时,系统需要为Chromium动态库分配的内存为(25.65 + N x 1.35)M。这是非常可观的。为此,Android使用了特殊的方式加载Chromium动态库。本文接下来就详细分析这种特殊的加载方式。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       为什么当有N个正在运行的App使用WebView时,系统需要为Chromium动态库分配的内存为(25.65 + N x 1.35)M呢?这是由于动态库的程序段是只读的,可以在多个进程之间进行共享,但是数据段一般是可读可写的,不能共享。在1.35M的数据段中,有1.28M在Chromium动态库加载完成后就是只读的。这1.28M数据包含有C++虚函数表,以及指针类型的常量等,它们在编译的时候会放在一个称为GNU_RELRO的Section中,如图1所示:


图1 App进程间不共享GNU_RELRO Section

       如果我们将该GNU_RELRO Section看作是一般的数据段,那么系统就需要为每一个使用了WebView的App进程都分配一段1.28M大小的内存空间。前面说到,这1.28M数据在Chromium动态库加载完成后就是只读的,那么有没有办法让它像程序段一样,在多个App进程之间共享呢?

       只要能满足一个条件,那么答案就是肯定的。这个条件就是所有的App进程都在相同的虚拟地址空间加载Chromium动态库。在这种情况下,就可以在系统启动的过程中,创建一个临时进程,并且在这个进程中加载Chromium动态库。假设Chromium动态库的加载地址为Base Address。加载完成后,将Chromium动态库的GNU_RELRO Section的内容Dump出来,并且写入到一个文件中去。

       以后App进程加载Chromium动态库时,都将Chromium动态库加载地址Base Address上,并且使用内存映射的方式将前面Dump出来的GNU_RELRO文件代替Chromium动态库的GNU_RELRO Section。这样就可以实现在多个App进程之间共享Chromium动态库的GNU_RELRO Section了,如图2所示:


图2 App进程间共享GNU_RELRO Section

       从前面Android应用程序进程启动过程的源代码分析一文可以知道,所有的App进程都是由Zygote进程fork出来的,因此,要让所有的App进程都在相同的虚拟地址空间加载Chromium动态库的最佳方法就是在Zygote进程的地址空间中预留一块地址。

       还有两个问题需要解决。第一个问题是要将加载后的Chromium动态库的GNU_RELRO Section的内容Dump出来,并且写入到一个文件中去。第二个问题是要让所有的App进程将Chromium动态加载在指定位置,并且可以使用指定的文件来代替它的GNU_RELRO Section。这两个问题都可以通过Android在5.0版本提供的一个动态库加载函数android_dlopen_ext解决。

       接下来,我们就结合源码,分析Zygote进程是如何为App进程预留地址加载Chromium动态库的,以及App进程如何使用新的动态库加载函数android_dlopen_ext加载Chromium动态库的。

       从前面Android系统进程Zygote启动过程的源代码分析一文可以知道,Zygote进程在Java层的入口函数为ZygoteInit类的静态成员函数main,它的实现如下所示:

public class ZygoteInit {
    ......

    public static void main(String argv[]) {
        try {
            .......

            preload();
            .......

            if (startSystemServer) {
                startSystemServer(abiList, socketName);
            }

            ......
            runSelectLoop(abiList);

            ......
        } catch (MethodAndArgsCaller caller) {
            ......
        } catch (RuntimeException ex) {
            ......
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中。

       ZygoteInit类的静态成员函数main在启动System进程以及使得Zygote进程进入运行状态之前,首先会调用另外一个静态成员函数preload预加载资源。这些预加载的资源以后就可以在App进程之间进行共享。

       ZygoteInit类的静态成员函数preload在预加载资源的过程中,就会为Chromium动态库预保留加载地址,如下所示:

public class ZygoteInit {
    ......

    static void preload() {
        ......
        // Ask the WebViewFactory to do any initialization that must run in the zygote process,
        // for memory sharing purposes.
        WebViewFactory.prepareWebViewInZygote();
        ......
    }

    ......
}

       这个函数定义在文件frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中。

       ZygoteInit类的静态成员函数preload是通过调用WebViewFactory类的静态成员函数prepareWebViewInZygote为Chromium动态库预保留加载地址的,后者的实现如下所示:

public final class WebViewFactory {
    ......

    public static void prepareWebViewInZygote() {
        try {
            System.loadLibrary("webviewchromium_loader");
            long addressSpaceToReserve =
                    SystemProperties.getLong(CHROMIUM_WEBVIEW_VMSIZE_SIZE_PROPERTY,
                    CHROMIUM_WEBVIEW_DEFAULT_VMSIZE_BYTES);
            sAddressSpaceReserved = nativeReserveAddressSpace(addressSpaceToReserve);

            ......
        } catch (Throwable t) {
            ......
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数prepareWebViewInZygote首先会加载一个名称为“webviewchromium_loader”的动态库,接下来又会获得需要为Chromium动态库预留的地址空间大小addressSpaceToReserve。知道了要预留的地址空间的大小之后,WebViewFactory类的静态成员函数prepareWebViewInZygote就会调用另外一个静态成员函数nativeReserveAddressSpace为Chromium动态库预留地址空间。

       WebViewFactory类的静态成员函数nativeReserveAddressSpace是一个JNI方法,它在C++层对应的函数为ReserveAddressSpace。这个函数实现在上述名称为“webviewchromium_loader”的动态库中,如下所示:

jboolean ReserveAddressSpace(JNIEnv*, jclass, jlong size) {
  return DoReserveAddressSpace(size);
}
       这个函数定义在文件frameworks/webview/chromium/loader/loader.cpp中。

       函数ReserveAddressSpace调用另外一个函数DoReserveAddressSpace预留大小为size的地址空间,如下所示:

void* gReservedAddress = NULL;
size_t gReservedSize = 0;

jboolean DoReserveAddressSpace(jlong size) {
  size_t vsize = static_cast<size_t>(size);

  void* addr = mmap(NULL, vsize, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  ......

  gReservedAddress = addr;
  gReservedSize = vsize;
  ......

  return JNI_TRUE;
}
       这个函数定义在文件frameworks/webview/chromium/loader/loader.cpp中。

       函数DoReserveAddressSpace是通过系统接口mmap预留指定大小的地址空间的。同时,预留出来的地址空间的起始地址和大小分别记录在全局变量gReservedAddress和gReservedSize中。以后Chromium动态库就可以加载在该地址空间中。

       为Chromium动态库预留好加载地址之后,Android系统接下来要做的一件事情就是请求Zygote进程启动一个临时进程。这个临时进程将会在上述预留的地址空间中加载Chromium动态库。这个Chromium动态库在加载的过程中,它的GNU_RELRO Section会被重定位。重定位完成之后,就可以将它的内容Dump到一个文件中去。这个文件以后就会以内存映射的方式代替在App进程中加载的Chromium动态库的GNU_RELRO Section。

       请求Zygote进程启动临时进程的操作是由System进程完成的。从前面Android系统进程Zygote启动过程的源代码分析一文可以知道,System进程是由Zygote进程启动的,它在Java层的入口函数为SystemServer的静态成员函数main,它的实现如下所示:

public final class SystemServer {
    ......

    public static void main(String[] args) {
        new SystemServer().run();
    }

    ......
}
      这个函数定义在文件frameworks/base/services/java/com/android/server/SystemServer.java中。

      SystemServer的静态成员函数main首先是创建一个SystemServer对象,然后调用这个SystemServer对象的成员函数run在System进程中启动各种系统服务,如下所示:

public final class SystemServer {
    ......

    private void run() {
        ......

        // Start services.
        try {
            startBootstrapServices();
            startCoreServices();
            startOtherServices();
        } catch (Throwable ex) {
            ......
        }

        ......

        // Loop forever.
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    ......
}

       这个函数定义在文件frameworks/base/services/java/com/android/server/SystemServer.java中。

       SystemServer类的成员函数run首先会调用成员函数startBootstrapServices启动Bootstrap类型的系统服务。这些系统服务包括Activity Manager Service、Power Manager Service、Package Manager Service和Display Manager Service等。

       SystemServer类的成员函数run接下来又会调用成员函数startCoreServices启动Core类型的系统服务。这些系统服务包括Lights Service、Battery Service和Usage Stats Service等。

       SystemServer类的成员函数run再接下来还会调用成员函数startOtherServices启动其它的系统服务。这些其它的系统服务包括Account Manager Service、Network Management Service和Window Manager Service等。

       SystemServer类的成员函数startOtherServices启动完成其它的系统服务之后,就会创建一个Runnable。这个Runnable会在Activity Manger Service启动完成后执行,如下所示:

public final class SystemServer {
    ......

    private void startOtherServices() {
        final Context context = mSystemContext;
        AccountManagerService accountManager = null;
        ContentService contentService = null;
        VibratorService vibrator = null;
        IAlarmManager alarm = null;
        MountService mountService = null;
        NetworkManagementService networkManagement = null;
        NetworkStatsService networkStats = null;
        NetworkPolicyManagerService networkPolicy = null;
        ConnectivityService connectivity = null;
        NetworkScoreService networkScore = null;
        NsdService serviceDiscovery= null;
        WindowManagerService wm = null;
        BluetoothManagerService bluetooth = null;
        UsbService usb = null;
        SerialService serial = null;
        NetworkTimeUpdateService networkTimeUpdater = null;
        CommonTimeManagementService commonTimeMgmtService = null;
        InputManagerService inputManager = null;
        TelephonyRegistry telephonyRegistry = null;
        ConsumerIrService consumerIr = null;
        AudioService audioService = null;
        MmsServiceBroker mmsService = null;
        ......

       mActivityManagerService.systemReady(new Runnable() {
            @Override
            public void run() {
                ......

                WebViewFactory.prepareWebViewInSystemServer();

                ......
            }
        });
    }

    ......
}
       这个函数定义在文件frameworks/base/services/java/com/android/server/SystemServer.java中。

       这个Runnable在执行的时候,会调用WebViewFactory类的静态成员函数prepareWebViewInSystemServer请求Zygote进程启动一个临时的进程,用来加载Chromium动态库,并且将完成重定位操作后的GNU_RELRO Section的内容Dump到一个文件中去。

       WebViewFactory类的静态成员函数prepareWebViewInSystemServer的实现如下所示:

public final class WebViewFactory {
    ......

    public static void prepareWebViewInSystemServer() {
        String[] nativePaths = null;
        try {
            nativePaths = getWebViewNativeLibraryPaths();
        } catch (Throwable t) {
            // Log and discard errors at this stage as we must not crash the system server.
            Log.e(LOGTAG, "error preparing webview native library", t);
        }
        prepareWebViewInSystemServer(nativePaths);
    }

    ......
}

       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数prepareWebViewInSystemServer首先调用另外一个静态成员函数getWebViewNativeLibraryPaths获得Chromium动态库的文件路径,如下所示:

public final class WebViewFactory {
    ......

    private static String[] getWebViewNativeLibraryPaths()
            throws PackageManager.NameNotFoundException {
        final String NATIVE_LIB_FILE_NAME = "libwebviewchromium.so";

        PackageManager pm = AppGlobals.getInitialApplication().getPackageManager();
        ApplicationInfo ai = pm.getApplicationInfo(getWebViewPackageName(), 0);

        String path32;
        String path64;
        boolean primaryArchIs64bit = VMRuntime.is64BitAbi(ai.primaryCpuAbi);
        if (!TextUtils.isEmpty(ai.secondaryCpuAbi)) {
            // Multi-arch case.
            if (primaryArchIs64bit) {
                // Primary arch: 64-bit, secondary: 32-bit.
                path64 = ai.nativeLibraryDir;
                path32 = ai.secondaryNativeLibraryDir;
            } else {
                // Primary arch: 32-bit, secondary: 64-bit.
                path64 = ai.secondaryNativeLibraryDir;
                path32 = ai.nativeLibraryDir;
            }
        } else if (primaryArchIs64bit) {
            // Single-arch 64-bit.
            path64 = ai.nativeLibraryDir;
            path32 = "";
        } else {
            // Single-arch 32-bit.
            path32 = ai.nativeLibraryDir;
            path64 = "";
        }
        if (!TextUtils.isEmpty(path32)) path32 += "/" + NATIVE_LIB_FILE_NAME;
        if (!TextUtils.isEmpty(path64)) path64 += "/" + NATIVE_LIB_FILE_NAME;
        return new String[] { path32, path64 };
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       Android系统将Chromium动态库打包在一个WebView Package中。这个WebView Package也是一个APK,它的Package Name可以通过调用WebViewFactory类的静态成员函数getWebViewPackageName获得,如下所示:

public final class WebViewFactory {
    ......

    public static String getWebViewPackageName() {
        return AppGlobals.getInitialApplication().getString(
                com.android.internal.R.string.config_webViewPackageName);
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数getWebViewPackageName又是通过系统字符串资源com.android.internal.R.string.config_webViewPackageName获得WebView Package的Package Name。

       系统字符串资源com.android.internal.R.string.config_webViewPackageName的定义如下所示:

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
  ......

  <!-- Package name providing WebView implementation. -->
    <string name="config_webViewPackageName" translatable="false">com.android.webview</string>

  ......
</resources>
       这个字符串资源定义在文件frameworks/base/core/res/res/values/config.xml中。

       从这里就可以看到,WebView Package这个APK的Package Name定义为"com.android.webview"。通过查找源码,可以发现,这个APK实现在目录frameworks/webview/chromium中。

       回到WebViewFactory类的静态成员函数getWebViewNativeLibraryPaths中,它知道了WebView Package这个APK的Package Name之后,就可以通过Package Manager Service获得它的安装信息。Package Manager Service负责安装系统上所有的APK,因此通过它可以获得任意一个APK的安装信息。关于Package Manager Service,可以参考Android应用程序安装过程源代码分析这篇文章。

       获得了WebView Package这个APK的安装信息之后,就可以知道它用来保存动态库的目录。Chromium动态库就是保存在这个目录下的。因此,WebViewFactory类的静态成员函数getWebViewNativeLibraryPaths最终可以获得Chromium动态库的文件路径。注意,获得Chromium动态库的文件路径可能有两个。一个是32位版本的,另外一个是64位版本的。

       为什么要将Chromium动态库打包在一个WebView Package中呢?而不是像其它的系统动态库一样,直接放在/system/lib目录下。原因为了方便以后升级WebView。例如,Chromium有了新的版本之后,就可以将它打包在一个更高版本的WebView Package中。当用户升级WebView Package的时候,就获得了基于新版本Chromium实现的WebView了。

       这一步执行完成后,回到WebViewFactory类的静态成员函数prepareWebViewInSystemServer中,这时候它就获得了Chromium动态库的文件路径。接下来,它继续调用另外一个静态成员函数prepareWebViewInSystemServer请求Zygote进程启动一个临时的进程,用来加载Chromium动态库,以便将完成重定位操作后的GNU_RELRO Section的内容Dump到一个文件中去。

       WebViewFactory类的静态成员函数prepareWebViewInSystemServer的实现如下所示:

public final class WebViewFactory {

    ......

    private static void prepareWebViewInSystemServer(String[] nativeLibraryPaths) {
        if (DEBUG) Log.v(LOGTAG, "creating relro files");

        // We must always trigger createRelRo regardless of the value of nativeLibraryPaths. Any
        // unexpected values will be handled there to ensure that we trigger notifying any process
        // waiting on relreo creation.
        if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
            if (DEBUG) Log.v(LOGTAG, "Create 32 bit relro");
            createRelroFile(false /* is64Bit */, nativeLibraryPaths);
        }

        if (Build.SUPPORTED_64_BIT_ABIS.length > 0) {
            if (DEBUG) Log.v(LOGTAG, "Create 64 bit relro");
            createRelroFile(true /* is64Bit */, nativeLibraryPaths);
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数prepareWebViewInSystemServer根据系统对32位和64位的支持情况,调用另外一个静态成员函数createRelroFile相应地创建32位和64位的Chromium GNU_RELRO Section文件。

       WebViewFactory类的静态成员函数createRelroFile的实现如下所示:

public final class WebViewFactory {
    ......

    private static void createRelroFile(final boolean is64Bit, String[] nativeLibraryPaths) {
        ......

        try {
            ......

            int pid = LocalServices.getService(ActivityManagerInternal.class).startIsolatedProcess(
                    RelroFileCreator.class.getName(), nativeLibraryPaths, "WebViewLoader-" + abi, abi,
                    Process.SHARED_RELRO_UID, crashHandler);
            ......
        } catch (Throwable t) {
            ......
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数createRelroFile请求系统的Activity Manager Service启动一个Isolated Process。这个Isolated Process的启动过程与App进程是一样的,只不过它是一个被隔离的进程,没有自己的权限。

       从前面Android应用程序进程启动过程的源代码分析一文可以知道,App进程最终是由Zygote进程fork出来的,并且它在Java层的入口函数为ActivityThread类的静态成员函数main。这个入口函数是可以指定的。WebViewFactory类的静态成员函数createRelroFile将请求启动的Isolated Process的入口函数指定为RelroFileCreator类的静态成员函数main。

       这意味着,当上述Isolated Process启动起来之后,RelroFileCreator类的静态成员函数main就会被调用,如下所示:

public final class WebViewFactory {
    ......

    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_32 =
            "/data/misc/shared_relro/libwebviewchromium32.relro";
    private static final String CHROMIUM_WEBVIEW_NATIVE_RELRO_64 =
            "/data/misc/shared_relro/libwebviewchromium64.relro";
    ......

    private static class RelroFileCreator {
        // Called in an unprivileged child process to create the relro file.
        public static void main(String[] args) {
            ......
            try{
                ......
                result = nativeCreateRelroFile(args[0] /* path32 */,
                                               args[1] /* path64 */,
                                               CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
                                               CHROMIUM_WEBVIEW_NATIVE_RELRO_64);
                ......
            } finally {
                ......
            }
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       前面获得的Chromium动态库文件路径会通过参数args传递进来。有了Chromium动态库文件路径之后,RelroFileCreator类的静态成员函数main就会调用另外一个静态成员函数nativeCreateRelroFile对它进行加载。加载完成后,RelroFileCreator类的静态成员函数nativeCreateRelroFile会将Chromium动态库已经完成重定位操作的Chromium GNU_RELRO Section写入到指定的文件中去。对于32位的Chromium动态库来说,它的GNU_RELRO Section会被写入到文件/data/misc/shared_relro/libwebviewchromium32.relro中,而对于64位的Chromium动态库来说,它的GNU_RELRO Section会被写入到文件/data/misc/shared_relro/libwebviewchromium64.relro中。

       RelroFileCreator类的静态成员函数nativeCreateRelroFile是一个JNI方法,它由C++层的函数CreateRelroFile实现,如下所示:

jboolean CreateRelroFile(JNIEnv* env, jclass, jstring lib32, jstring lib64,
                         jstring relro32, jstring relro64) {
#ifdef __LP64__
  jstring lib = lib64;
  jstring relro = relro64;
  (void)lib32; (void)relro32;
#else
  jstring lib = lib32;
  jstring relro = relro32;
  (void)lib64; (void)relro64;
#endif
  jboolean ret = JNI_FALSE;
  const char* lib_utf8 = env->GetStringUTFChars(lib, NULL);
  if (lib_utf8 != NULL) {
    const char* relro_utf8 = env->GetStringUTFChars(relro, NULL);
    if (relro_utf8 != NULL) {
      ret = DoCreateRelroFile(lib_utf8, relro_utf8);
      env->ReleaseStringUTFChars(relro, relro_utf8);
    }
    env->ReleaseStringUTFChars(lib, lib_utf8);
  }
  return ret;
}
       这个函数定义在文件frameworks/webview/chromium/loader/loader.cpp中。

       函数CreateRelroFile判断自己是32位还是64位的实现,然后从参数lib32和lib64中选择对应的Chromium动态库进行加载。这个加载过程是通过调用另外一个函数DoCreateRelroFile实现的,如下所示:

void* gReservedAddress = NULL;
size_t gReservedSize = 0;

......

jboolean DoCreateRelroFile(const char* lib, const char* relro) {
  ......

  static const char tmpsuffix[] = ".XXXXXX";
  char relro_tmp[strlen(relro) + sizeof(tmpsuffix)];
  strlcpy(relro_tmp, relro, sizeof(relro_tmp));
  strlcat(relro_tmp, tmpsuffix, sizeof(relro_tmp));
  int tmp_fd = TEMP_FAILURE_RETRY(mkstemp(relro_tmp));
  ......

  android_dlextinfo extinfo;
  extinfo.flags = ANDROID_DLEXT_RESERVED_ADDRESS | ANDROID_DLEXT_WRITE_RELRO;
  extinfo.reserved_addr = gReservedAddress;
  extinfo.reserved_size = gReservedSize;
  extinfo.relro_fd = tmp_fd;
  void* handle = android_dlopen_ext(lib, RTLD_NOW, &extinfo);
  int close_result = close(tmp_fd);
  ......

  if (close_result != 0 ||
      chmod(relro_tmp, S_IRUSR | S_IRGRP | S_IROTH) != 0 ||
      rename(relro_tmp, relro) != 0) {
    ......
    return JNI_FALSE;
  }

  return JNI_TRUE;
}

       这个函数定义在文件frameworks/webview/chromium/loader/loader.cpp中。

       参数lib描述的是要加载的Chromium动态库的文件路径,另外一个参数relro描述的是要生成的Chromium GNU_RELRO Section文件路径。

       我们假设要加载的Chromium动态库是32位的。函数DoCreateRelroFile的执行过程如下所示:

       1.  创建一个临时的mium GNU_RELRO Section文件,文件路径为”/data/misc/shared_relro/libwebviewchromium32.relro.XXXXXX“,并且会找开这个临时文件,获得一个文件描述符tmp_fd。

       2. 创建一个android_dlextinfo结构体。这个android_dlextinfo结构体会指定一个ANDROID_DLEXT_RESERVED_ADDRESS标志,以及一个Reserved地址gReservedAddress,表示要将Chromium动态库加载在全局变量gReservedAddress描述的地址中。从前面的分析可以知道,Zygote进程为Chromium动态库预留的地址空间的起始地址就保存在全局变量gReservedAddress中。由于当前进程是由Zygote进程fork出来的,因此它同样会获得Zygote进程预留的地址空间。

       3. 前面创建的android_dlextinfo结构体,同时还会指定一个ANDROID_DLEXT_WRITE_RELRO标专,以及一个Relro文件描述符tmp_fd,表示在加载完成Chromium动态库后,将完成重定位操作后的GNU_RELRO Section写入到指定的文件中去,也就是写入到上述的临时文件/data/misc/shared_relro/libwebviewchromium32.relro.XXXXXX中去。

       4. 调用Linker导出的函数android_dlopen_ext按照上述两点规则加载Chromium动态库。加载完成后,临时文件/data/misc/shared_relro/libwebviewchromium32.relro.XXXXXX将被重新命名为”/data/misc/shared_relro/libwebviewchromium32.relro“。

       这样,我们得就到一个Chromium GNU_RELRO Section文件。这个Chromium GNU_RELRO Section文件在App进程加载Chromium动态库时就会使用到。接下来我们就继续分析App进程加载Chromium动态库的过程。

       当App使用了WebView的时候,系统就为会其加载Chromium动态库。这个加载过程是从创建WebView对象的时候发起的。因此,接下来我们就从WebView类的构造函数开始分析App进程加载Chromium动态库的过程,如下所示:

public class WebView extends AbsoluteLayout
        implements ViewTreeObserver.OnGlobalFocusChangeListener,
        ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
    ......

    protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
            Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
        ......

        ensureProviderCreated();
        mProvider.init(javaScriptInterfaces, privateBrowsing);

        ......
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebView.java中。

       WebView类的构造函数会调用另外一个成员函数ensureProviderCreated确保Chromium动态库已经加载。在Chromium动态库已经加载的情况下,WebView类的成员函数ensureProviderCreated还会创建一个WebView Provider,并且保存保存在成员变量mProvider中。这个WebView Provider才是真正用来实现WebView的功能的。

      有了这个WebView Provider之后,WebView类的构造函数就会调用它的成员函数init启动网页渲染引擎。对于基于Chromium实现的WebView来说,它使用的WebView Provider是一个WebViewChromium对象。当这个WebViewChromium对象的成员函数init被调用的时候,它就会启动Chromium的网页渲染引擎。这个过程我们在接下来的一篇文章再详细分析。

       接下来,我们继续分析WebView类的成员函数ensureProviderCreated的实现,如下所示:

public class WebView extends AbsoluteLayout
        implements ViewTreeObserver.OnGlobalFocusChangeListener,
        ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
    ......

    private void ensureProviderCreated() {
        checkThread();
        if (mProvider == null) {
            // As this can get called during the base class constructor chain, pass the minimum
            // number of dependencies here; the rest are deferred to init().
            mProvider = getFactory().createWebView(this, new PrivateAccess());
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebView.java中。

       WebView类的成员函数ensureProviderCreated首先调用成员函数checkThread确保它是在WebView的创建线程中调用的,接下来又会判断成员变量mProvider的值是否为null。如果为null,就表示它还没有当前创建的WebView创建过Provider。在这种情况下,它首先会调用成员函数getFactory获得一个WebView Factory。有了这个WebView Factory之后,就可以调用它的成员函数createWebView创建一个WebView Provider。

       WebView类的成员函数getFactory的实现如下所示:

public class WebView extends AbsoluteLayout
        implements ViewTreeObserver.OnGlobalFocusChangeListener,
        ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
    ......

    private static synchronized WebViewFactoryProvider getFactory() {
        return WebViewFactory.getProvider();
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebView.java中。

       WebView类的成员函数getFactory返回的WebView Factory是通过调用WebViewFactory类的静态成员函数getProvider获得的,如下所示:

public final class WebViewFactory {
    ......

    private static WebViewFactoryProvider sProviderInstance;
    ......

    static WebViewFactoryProvider getProvider() {
        synchronized (sProviderLock) {
            // For now the main purpose of this function (and the factory abstraction) is to keep
            // us honest and minimize usage of WebView internals when binding the proxy.
            if (sProviderInstance != null) return sProviderInstance;

            ......
            try {
                ......
                loadNativeLibrary();
                ......

                Class<WebViewFactoryProvider> providerClass;
                ......
                try {
                    providerClass = getFactoryClass();
                } catch (ClassNotFoundException e) {
                    ......
                } finally {
                    ......
                }

                ......
                try {
                    sProviderInstance = providerClass.newInstance();
                    ......
                    return sProviderInstance;
                } catch (Exception e) {
                    .......
                } finally {
                    ......
                }
            } finally {
                ......
            }
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数getProvider首先是判断静态成员变量sProviderInstance的值是否等于null。如果等于null,那么就说明当前的App进程还没有加载过Chromium动态库。在这种情况下,就需要加载Chromium动态库,并且创建一个WebView Factory,保存在静态成员变量sProviderInstance。接下来我们就先分析Chromium动态库的加载过程,然后再分析WebView Factory的创建过程。

       加载Chromium动态库是通过调用WebViewFactory类的静态成员函数loadNativeLibrary实现的,如下所示: 

public final class WebViewFactory {
    ......

    private static void loadNativeLibrary() {
        ......

        try {
            String[] args = getWebViewNativeLibraryPaths();
            boolean result = nativeLoadWithRelroFile(args[0] /* path32 */,
                                                     args[1] /* path64 */,
                                                     CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
                                                     CHROMIUM_WEBVIEW_NATIVE_RELRO_64);
            ......
        } catch (PackageManager.NameNotFoundException e) {
            ......
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       WebViewFactory类的静态成员函数loadNativeLibrary首先会调用我们前面分析过的成员函数getWebViewNativeLibraryPaths获得要加载的Chromium动态库的文件路径,然后再调用另外一个静态成员函数nativeLoadWithRelroFile对它进行加载。在加载的时候,会指定一个Chromium GNU_RELRO Section文件。这个Chromium GNU_RELRO Section文件就是前面通过启动一个临时进程生成的。

       WebViewFactory类的静态成员函数nativeLoadWithRelroFile是一个JNI方法,它由C++层的函数LoadWithRelroFile实现,如下所示:

jboolean LoadWithRelroFile(JNIEnv* env, jclass, jstring lib32, jstring lib64,
                           jstring relro32, jstring relro64) {
#ifdef __LP64__
  jstring lib = lib64;
  jstring relro = relro64;
  (void)lib32; (void)relro32;
#else
  jstring lib = lib32;
  jstring relro = relro32;
  (void)lib64; (void)relro64;
#endif
  jboolean ret = JNI_FALSE;
  const char* lib_utf8 = env->GetStringUTFChars(lib, NULL);
  if (lib_utf8 != NULL) {
    const char* relro_utf8 = env->GetStringUTFChars(relro, NULL);
    if (relro_utf8 != NULL) {
      ret = DoLoadWithRelroFile(lib_utf8, relro_utf8);
      env->ReleaseStringUTFChars(relro, relro_utf8);
    }
    env->ReleaseStringUTFChars(lib, lib_utf8);
  }
  return ret;
}
       这个函数定义在文件frameworks/webview/chromium/loader/loader.cpp中。

       函数LoadWithRelroFile判断自己是32位还是64位的实现,然后从参数lib32和lib64中选择对应的Chromium动态库进行加载。这个加载过程是通过调用另外一个函数DoLoadWithRelroFile实现的,如下所示:

jboolean DoLoadWithRelroFile(const char* lib, const char* relro) {
  int relro_fd = TEMP_FAILURE_RETRY(open(relro, O_RDONLY));
  ......

  android_dlextinfo extinfo;
  extinfo.flags = ANDROID_DLEXT_RESERVED_ADDRESS | ANDROID_DLEXT_USE_RELRO;
  extinfo.reserved_addr = gReservedAddress;
  extinfo.reserved_size = gReservedSize;
  extinfo.relro_fd = relro_fd;
  void* handle = android_dlopen_ext(lib, RTLD_NOW, &extinfo);
  close(relro_fd);
  ......

  return JNI_TRUE;
}
       这个函数定义在文件frameworks/webview/chromium/loader/loader.cpp中。

       函数DoLoadWithRelroFile的实现与前面分析的函数DoCreateRelroFile类似,都是通过Linker导出的函数android_dlopen_ext在Zyogote进程保留的地址空间中加载Chromium动态库的。注意,App进程是Zygote进程fork出来的,因此它同样会获得Zygote进程预留的地址空间。

       不过,函数DoLoadWithRelroFile会将告诉函数android_dlopen_ext在加载Chromium动态库的时候,将参数relro描述的Chromium GNU_RELRO Section文件内存映射到内存来,并且代替掉已经加载的Chromium动态库的GNU_RELRO Section。这是通过将指定一个ANDROID_DLEXT_USE_RELRO标志实现的。

       之所以可以这样做,是因为参数relro描述的Chromium GNU_RELRO Section文件对应的Chromium动态库的加载地址与当前App进程加载的Chromium动态库的地址一致。只要两个相同的动态库在两个不同的进程中的加载地址一致,它们的链接和重定位信息就是完全一致的,因此就可以通过文件内存映射的方式进行共享。共享之后,就可以达到节省内存的目的了。

        这一步执行完成之后,App进程就加载完成Chromium动态库了。回到前面分析的WebViewFactory类的静态成员函数getProvider,它接下来继续创建一个WebView Factory。这个WebView Factory以后就可以用来创建WebView Provider。

        WebViewFactory类的静态成员函数getProvider首先要确定要创建的WebView Factory的类型。这个类型是通过调用另外一个静态成员函数getFactoryClass获得的,如下所示:

public final class WebViewFactory {

    private static final String CHROMIUM_WEBVIEW_FACTORY =
            "com.android.webview.chromium.WebViewChromiumFactoryProvider";

    ......

    private static Class<WebViewFactoryProvider> getFactoryClass() throws ClassNotFoundException {
        Application initialApplication = AppGlobals.getInitialApplication();
        try {
            // First fetch the package info so we can log the webview package version.
            String packageName = getWebViewPackageName();
            sPackageInfo = initialApplication.getPackageManager().getPackageInfo(packageName, 0);
            ......

            // Construct a package context to load the Java code into the current app.
            Context webViewContext = initialApplication.createPackageContext(packageName,
                    Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
            ......

            ClassLoader clazzLoader = webViewContext.getClassLoader();
            ......
            try {
                return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true,
                                                                     clazzLoader);
            } finally {
                ......
            }
        } catch (PackageManager.NameNotFoundException e) {
            ......
        }
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebViewFactory.java中。

       从这里可以看到,WebViewFactory类的静态成员函数getFactoryClass返回的WebView Factory的类型为com.android.webview.chromium.WebViewChromiumFactoryProvider。这个com.android.webview.chromium.WebViewChromiumFactoryProvider类是由前面提到的WebView Package提供的。

       这意味着WebViewFactory类的静态成员函数getProvider创建的WebView Factory是一个WebViewChromiumFactoryProvider对象。这个WebViewChromiumFactoryProvider的创建过程如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    public WebViewChromiumFactoryProvider() {
        ......

        AwBrowserProcess.loadLibrary();

        .......
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       WebViewChromiumFactoryProvider类的构造函数会调用AwBrowserProcess类的静态成员函数loadLibrary对前面加载的Chromium动态库进行初始化,如下所示:

public abstract class AwBrowserProcess {
    ......

    public static void loadLibrary() {
        ......
        try {
            LibraryLoader.loadNow();
        } catch (ProcessInitException e) {
            throw new RuntimeException("Cannot load WebView", e);
        }
    }

    ......
}
        这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwBrowserProcess.java中。

        AwBrowserProcess类的静态成员函数loadLibrary又调用LibraryLoader类的静态成员函数loadNow对前面加载的Chromium动态库进行初始化,如下所示:

public class LibraryLoader {
    ......

    public static void loadNow() throws ProcessInitException {
        loadNow(null, false);
    }

    ......
}
        这个函数定义在文件external/chromium_org/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java中。

        LibraryLoader类的静态成员函数loadNow又调用另外一个重载版本的静态成员函数loadNow对前面加载的Chromium动态库进行初始化,如下所示:

public class LibraryLoader {
    ......

    public static void loadNow(Context context, boolean shouldDeleteOldWorkaroundLibraries)
            throws ProcessInitException {
        synchronized (sLock) {
            loadAlreadyLocked(context, shouldDeleteOldWorkaroundLibraries);
        }
    }

    ......
}
       这个函数定义在文件external/chromium_org/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java中。

       LibraryLoader类重载版本的静态成员函数loadNow又调用另外一个静态成员函数loadAlreadyLocked对前面加载的Chromium动态库进行初始化,如下所示:

public class LibraryLoader {
    ......

    // One-way switch becomes true when the libraries are loaded.
    private static boolean sLoaded = false;
    ......

    private static void loadAlreadyLocked(
            Context context, boolean shouldDeleteOldWorkaroundLibraries)
            throws ProcessInitException {
        try {
            if (!sLoaded) {
                ......

                boolean useChromiumLinker = Linker.isUsed();

                if (useChromiumLinker) Linker.prepareLibraryLoad();

                for (String library : NativeLibraries.LIBRARIES) {
                    Log.i(TAG, "Loading: " + library);
                    if (useChromiumLinker) {
                        Linker.loadLibrary(library);
                    } else {
                        try {
                            System.loadLibrary(library);
                        } catch (UnsatisfiedLinkError e) {
                            ......
                        }
                    }
                }
                if (useChromiumLinker) Linker.finishLibraryLoad();

                ......
                sLoaded = true;
            }
        } catch (UnsatisfiedLinkError e) {
            ......
        }
        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java中。

       由于并不是所有的系统都支持在加载动态库时,以文件内存映射的方式代替它的GNU_RELRO Section,因此Chromium自己提供了一个Linker。通过这个Linker加载动态库时,能够以文件内存映射的方式代替要加载的动态库的GNU_RELRO Section,也就是实现前面提到的函数android_dlopen_ext的功能。     

       在Android 5.0中,由于系统已经提供了函数android_dlopen_ext,因此,Chromium就不会使用自己的Linker加载动态库,而是使用Android系统提供的Linker来加载动态库。通过调用System类的静态成员函数loadLibrary即可以使用系统提供的Linker来加载动态库。

       LibraryLoader类的静态成员函数loadAlreadyLocked要加载的动态库由NativeLibraries类的静态成员变量LIBRARIES指定,如下所示:

public class NativeLibraries {
    ......

    static final String[] LIBRARIES = { "webviewchromium" };
    
    ......
}
      这个静态成员变量定义在文件external/chromium_org/android_webview/java/generated_src/org/chromium/base/library_loader/NativeLibraries.java中。

      从这里可以知道,LibraryLoader类的静态成员函数loadAlreadyLocked要加载的动态库就是Chromium动态库。这个Chromium动态库前面已经加载过了,因此这里通过调用System类的静态成员函数loadLibrary再加载时,仅仅是只会触发它导出的函数JNI_OnLoad被调用,而不会重新被加载。

      Chromium动态库导出的JNI_OnLoad被调用的时候,Chromium动态库就会执行初始化工作,如下所示:

JNI_EXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  ......

  content::SetContentMainDelegate(new android_webview::AwMainDelegate());
  ......

  return JNI_VERSION_1_4;
}
       这个函数定义在文件external/chromium_org/android_webview/lib/main/webview_entry_point.cc中。

       其中的一个初始化操作是给Chromium的Content层设置一个类型为AwMainDelegate的Main Delegate。这个AwMainDelegate实现在Chromium的android_webview模块中。Android WebView是通过Chromium的android_webview模块加载和渲染网页的。Chromium加载和渲染网页的功能又是实现在Content层的,因此,Chromium的android_webview模块又要通过Content层实现加载和渲染网页功能。这样,Chromium的android_webview模块就可以设置一个Main Delegate给Content层,以便它们可以互相通信。

       给Chromium的Content层设置一个Main Delegate是通过调用函数SetContentMainDelegate实现的,它的实现如下所示:

LazyInstance<scoped_ptr<ContentMainDelegate> > g_content_main_delegate =
    LAZY_INSTANCE_INITIALIZER;

......

void SetContentMainDelegate(ContentMainDelegate* delegate) {
  DCHECK(!g_content_main_delegate.Get().get());
  g_content_main_delegate.Get().reset(delegate);
}
       这个函数定义在文件external/chromium_org/content/app/android/content_main.cc中。

       从前面的分析可以知道,参数delegate指向的是一个AwMainDelegate对象,这个AwMainDelegate对象会被函数SetContentMainDelegate保存在全局变量g_content_main_delegate中。在接下来一篇文章中分析Chromium渲染引擎的启动过程时,我们就会看到这个AwMainDelegate对象的作用。

       这一步执行完成后,Chromium动态库就在App进程中加载完毕,并且也已经完成了初始化工作。与此同时,系统也为App进程创建了一个类型为WebViewChromiumFactoryProvider的WebView Factory。回到前面分析的WebView类的成员函数ensureProviderCreated中,这时候就它会通过调用上述类型为WebViewChromiumFactoryProvider的WebView Factory的成员函数createWebView为当前创建的WebView创建一个WebView Provider,如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    @Override
    public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
        WebViewChromium wvc = new WebViewChromium(this, webView, privateAccess);

        ......

        return wvc;
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       WebViewChromiumFactoryProvider类的成员函数createWebView创建的是一个类型为WebViewChromium的WebView Provider。这个WebView Provider将会返回给WebView类的成员函数ensureProviderCreated。WebView类的成员函数ensureProviderCreated再将该WebView Provider保存在成员变量mProvider中。

       这样,正在创建的WebView就获得了一个类型为WebViewChromium的WebView Provider。以后通过这个WebView Provider,就可以通过Chromium来加载和渲染网页了。

       至此,我们也分析完成了Android WebView加载Chromium动态库的过程。在接下来的一篇文章中,我们继续分析Android WebView启动Chromium渲染引擎的过程。Chromium渲染引擎启动起来之后,Android WebView就可以通过它来加载和渲染网页了。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/11/28 1:00:19 原文链接
阅读:34198 评论:4 查看评论

Android WebView启动Chromium渲染引擎的过程分析

$
0
0

       Android WebView加载了Chromium动态库之后,就可以启动Chromium渲染引擎了。Chromium渲染引擎由Browser、Render和GPU三端组成。其中,Browser端负责将网页UI合成在屏幕上,Render端负责加载网页的URL和渲染网页的UI,GPU端负责执行Browser端和Render端请求的GPU命令。本文接下来详细分析Chromium渲染引擎三端的启动过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

       Android WebView使用了单进程架构的Chromium来加载和渲染网页,因此它的Browser端、Render端和GPU端都不是以进程的形式存在的,而是以线程的形式存在。其中,Browser端实现在App的UI线程中,Render端实现在一个独立的线程中,而GPU端实现在App的Render Thread中。注意,这是针对Android 5.0及以上版本的。Android在4.4版本引入基于Chromium实现的WebView,那时候GPU端与Browser一样,都是实现在App的UI线程中。接下来我们只讨论Android WebView在Android 5.0及以上版本的实现。

       Android WebView启动Chromium渲染引擎三端的过程如图1所示:


图1 Android WebView启动Chromium渲染引擎的过程

       从前面Android WebView加载Chromium动态库的过程分析一文可以知道,当我们在App的UI中嵌入一个WebView时,WebView会在内部创建一个类型为WebViewChromium的Provider。Android WebView就是通过这个Provider来启动和使用Chromium渲染引擎的。

       Chromium里面有一个android_webview模块。这个模块提供了两个类AwBrowserProcess和AwContents,分别用来封装Chromium的Content层提供的两个接口类BrowserStartupController和ContentViewCore,它们分别用来启动Chromium的Browser端和Render端。

       Android WebView启动Chromium的Browser端,实际上就是在App的UI线程创建一个Browser Main Loop。Chromium以后需要请求Browser端执行某一个操作时,就可以向这个Browser Main Loop发送一个Task。这个Task最终会在App进程的UI线程中调度执行。

       Android WebView启动Chromium的Render端,实际上就是在当前的App进程中创建一个线程。以后网页就由这个线程负责加载和渲染。这个线程称为In-Process Renderer Thread。

       由于Chromium的GPU端实现在App的Render Thread中,这个Render Thread是由App负责启动的,因此Chromium无需启动它。不过,Chromium里的android_webview模块会启动一个DeferredGpuCommandService服务。当Chromium的Browser端和Render端需要执行GPU操作时,就会向DeferredGpuCommandService服务发出请求。这时候DeferredGpuCommandService服务又会通过App的UI线程将请求的GPU操作提交给App的Render Thread执行。这一点可以参考前面Android WebView简要介绍和学习计划一文的描述。我们在接下来的一篇文章也会对Chromium的Browser端和Render端执行GPU操作的过程进行详细的分析。

       接下来我们就结合源码,分析Android WebView启动Chromium的Browser端和Render端的过程。对于GPU端,我们仅仅分析与它相关的DeferredGpuCommandService服务的启动过程。在接下来一篇文章分析Android WebView执行GPU命令的过程时,我们再对GPU端进行更详细的分析。

       我们首先分析Android WebView启动Chromium的Browser端的过程。前面提到,WebView会在内部创建一个类型为WebViewChromium的Provider。有了这个Provider之后,WebView就可以调用它的成员函数init启动Chromium的Browser端,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    public void init(final Map<String, Object> javaScriptInterfaces,
            final boolean privateBrowsing) {
        ......

        // We will defer real initialization until we know which thread to do it on, unless:
        // - we are on the main thread already (common case),
        // - the app is targeting >= JB MR2, in which case checkThread enforces that all usage
        //   comes from a single thread. (Note in JB MR2 this exception was in WebView.java).
        if (mAppTargetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mFactory.startYourEngines(false);
            checkThread();
        } else if (!mFactory.hasStarted()) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                mFactory.startYourEngines(true);
            }
        }

        ......

        mRunQueue.addTask(new Runnable() {
                @Override
                public void run() {
                    initForReal();
                    ......
                }
        });
    }

    ......
}
      这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

      WebViewChromium类的成员变量mFactory指向的是一个WebViewChromiumFactoryProvider对象。WebViewChromium类的成员函数init通过调用这个WebViewChromiumFactoryProvider对象的成员函数startYourEngines启动Chromium渲染引擎的Browser端。

       在Android 4.3之前,WebView只能在App的UI线程中创建。相应地,WebView也只能在App的UI线程中启动Chromium渲染引擎的Browser端。这时候WebViewChromium类的成员函数init会传递一个参数true给WebViewChromiumFactoryProvider类的成员函数startYourEngines,表示如果当前线程如果不是UI线程,那么就需要向UI线程发出一个通知,让UI线程执行启动Chromium渲染引擎的Browser端的操作。

       在Android 4.3及以后,WebView也允许在App的非UI线程中创建。这时候WebView允行在App的非UI线程中启动Chromium渲染引擎的Browser端。因此,WebViewChromium类的成员函数init就会传递一个参数false给WebViewChromiumFactoryProvider类的成员函数startYourEngines。

       一般情况下,WebView都是在App的UI线程中创建的。为了简单起见,我们只考虑这种情况。WebViewChromium类的成员函数init调用WebViewChromiumFactoryProvider类的成员函数startYourEngines启动了Chromium渲染引擎的Browser端之后,接下来还会向App的UI线程的消息队列发送一个Runnable。当该Runnable被执行的时候,它就会调用WebViewChromium类的成员函数initForReal创建图1所示的AwContents对象。有了这个AwContents对象之后,后面就可以通过它来加载指定的URL了。

       接下来,我们首先分析WebViewChromiumFactoryProvider类的成员函数startYourEngines启动Chromium渲染引擎的Browser端的过程,然后再分析WebViewChromium类的成员函数initForReal为WebView创建AwContents对象的过程。

       WebViewChromiumFactoryProvider类的成员函数startYourEngines的实现如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    void startYourEngines(boolean onMainThread) {
        synchronized (mLock) {
            ensureChromiumStartedLocked(onMainThread);

        }
    }

    ......
}

       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       WebViewChromiumFactoryProvider类的成员函数startYourEngines调用另外一个成员函数ensureChromiumStartedLocked检查Chromium渲染引擎的Browser端是否已经启动。如果还没有启动,那么就会进行启动,如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    private void ensureChromiumStartedLocked(boolean onMainThread) {
        ......

        if (mStarted) {  // Early-out for the common case.
            return;
        }

        Looper looper = !onMainThread ? Looper.myLooper() : Looper.getMainLooper();
        ......
        ThreadUtils.setUiThread(looper);

        if (ThreadUtils.runningOnUiThread()) {
            startChromiumLocked();
            return;
        }

        // We must post to the UI thread to cover the case that the user has invoked Chromium
        // startup by using the (thread-safe) CookieManager rather than creating a WebView.
        ThreadUtils.postOnUiThread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    startChromiumLocked();
                }
            }
        });
        while (!mStarted) {
            try {
                // Important: wait() releases |mLock| the UI thread can take it :-)
                mLock.wait();
            } catch (InterruptedException e) {
                // Keep trying... eventually the UI thread will process the task we sent it.
            }
        }
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       如果Chromium渲染引擎的Browser端已经启动,那么WebViewChromiumFactoryProvider类的成员变量mStarted的值就会等于true。在这种情况下,WebViewChromiumFactoryProvider类的成员函数ensureChromiumStartedLocked什么也不用做就可以返回。

       另一方面,如果Chromium渲染引擎的Browser端还没有启动,那么WebViewChromiumFactoryProvider类的成员函数ensureChromiumStartedLocked首先会根据参数onMainThread确定Chromium渲染引擎的Browser端要在哪个线程中运行。

       当参数onMainThread的值等于true的时候,就表示Chromium渲染引擎的Browser端要在App的UI线程中运行。这时候如果当前线程不是App的UI线程,那么WebViewChromiumFactoryProvider类的成员函数ensureChromiumStartedLocked就会向App的UI线程的消息队列发送一个Runnable。当该Runnable被执行的时候,才会启动Chromium渲染引擎的Browser端。在这种情况下,当前线程也会等待App的UI线程启动完成Chromium渲染引擎的Browser端。

       当参数onMainThread的值等于true的时候,如果当前线程刚好也是App的UI线程,那么WebViewChromiumFactoryProvider类的成员函数ensureChromiumStartedLocked就可以马上启动Chromium渲染引擎的Browser端。

       当参数onMainThread的值等于false的时候,不管当前线程是否App的UI线程,都表示Chromium渲染引擎的Browser端要在它里面运行。因此,这时候WebViewChromiumFactoryProvider类的成员函数ensureChromiumStartedLocked都会马上启动Chromium渲染引擎的Browser端。

       无论是上述的哪一种情况,用来运行Chromium渲染引擎的Browser端的线程都会通过调用ThreadUtils类的静态成员函数setUiThread记录起来。以后WebView都需要在该线程中访问Chromium渲染引擎。

       WebViewChromiumFactoryProvider类的成员函数ensureChromiumStartedLocked是通过调用另外一个成员函数startChromiumLocked启动Chromium渲染引擎的Browser端的,如下所示:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {
    ......

    private void startChromiumLocked() {
        ......

        AwBrowserProcess.start(ActivityThread.currentApplication());

        ......
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromiumFactoryProvider.java中。

       WebViewChromiumFactoryProvider类的成员函数startChromiumLocked通过调用AwBrowserProcess类的静态成员函数start启动Chromium渲染引擎的Browser端的,如下所示:

public abstract class AwBrowserProcess {
    ......

    public static void start(final Context context) {
        // We must post to the UI thread to cover the case that the user
        // has invoked Chromium startup by using the (thread-safe)
        // CookieManager rather than creating a WebView.
        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
            @Override
            public void run() {
                try {
                    BrowserStartupController.get(context).startBrowserProcessesSync(
                                BrowserStartupController.MAX_RENDERERS_SINGLE_PROCESS);
                    ......
                } catch (ProcessInitException e) {
                    ......
                }
            }
        });
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwBrowserProcess.java中。

       前面提到,用来运行Chromium渲染引擎的Browser端的线程会通过ThreadUtils类的静态成员函数setUiThread记录起来。AwBrowserProcess类的静态成员函数start为了确保Chromium渲染引擎的Browser端在该线程中启动,会通过调用ThreadUtils类的静态成员函数runOnUiThreadBlocking检查当前线程是否就是该线程。如果是的话,那么就会直接启动。否则的话,会向该线程的消息队列发送一个Runnable。当该Runnable被执行的时候,再启动Chromium渲染引擎的Browser端。

       AwBrowserProcess类的静态成员函数start是通过调用当前App进程中的一个BrowserStartupController单例对象的成员函数startBrowserProcessesSync来启动Chromium渲染引擎的Browser端的。这个BrowserStartupController单例对象可以通过调用BrowserStartupController类的静态成员函数get获得。

       AwBrowserProcess类的静态成员函数start在启动Chromium渲染引擎的Browser端的时候,会指定一个BrowserStartupController.MAX_RENDERERS_SINGLE_PROCESS参数。这个参数的值等于0,表示要启动一个单进程架构的Chromium渲染引擎。

       接下来,我们就继续分析Chromium渲染引擎的Browser端的启动过程,也就是BrowserStartupController类的成员函数startBrowserProcessesSync的实现,如下所示:

public class BrowserStartupController {
    ......

    public void startBrowserProcessesSync(int maxRenderers) throws ProcessInitException {
        // If already started skip to checking the result
        if (!mStartupDone) {
            if (!mHasStartedInitializingBrowserProcess) {
                prepareToStartBrowserProcess(maxRenderers);
            }

            ......
            if (contentStart() > 0) {
                // Failed. The callbacks may not have run, so run them.
                enqueueCallbackExecution(STARTUP_FAILURE, NOT_ALREADY_STARTED);
            }
        }

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/BrowserStartupController.java中。

       当BrowserStartupController类的成员变量mStartupDone的值等于true的时候,就表示Chromium渲染引擎的Browser端已经启动了。这时候BrowserStartupController类的成员函数startBrowserProcessesSync就什么也不做就直接返回。

       另一方面,如果Chromium渲染引擎的Browser端还没有启动。这时候BrowserStartupController类的成员函数startBrowserProcessesSync就会调用另外一个成员函数contentStart进行启动。

       在启动Chromium渲染引擎的Browser端之前,BrowserStartupController类的成员函数startBrowserProcessesSync也会检查成员变量mHasStartedInitializingBrowserProcess的值。当这个值等于false的时候,就会先调用成员函数prepareToStartBrowserProcess设置Chromium渲染引擎的启动参数。其中,最重要的就是将Chromium渲染引擎设置为单进程架构。

       接下来,我们先分析将Chromium渲染引擎设置为单进程架构的过程,也就是BrowserStartupController类的成员函数prepareToStartBrowserProcess的实现,然后再分析启动Chromium渲染引擎的Browser端的过程,也就是BrowserStartupController类的成员函数contentStart的实现。

       BrowserStartupController类的成员函数prepareToStartBrowserProcess的实现如下所示:

public class BrowserStartupController {
    ......

    void prepareToStartBrowserProcess(int maxRendererProcesses) throws ProcessInitException {
        ......

        nativeSetCommandLineFlags(maxRendererProcesses,
                nativeIsPluginEnabled() ? getPlugins() : null);
        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/BrowserStartupController.java中。

       BrowserStartupController类的成员函数prepareToStartBrowserProcess调用另外一个成员函数nativeSetCommandLineFlags将Chromium渲染引擎设置为单进程架构。

       BrowserStartupController类的成员函数nativeSetCommandLineFlags是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_BrowserStartupController_nativeSetCommandLineFlags实现,如下所示:

__attribute__((visibility("default")))
void
    Java_com_android_org_chromium_content_browser_BrowserStartupController_nativeSetCommandLineFlags(JNIEnv*
    env, jclass jcaller,
    jint maxRenderProcesses,
    jstring pluginDescriptor) {
  return SetCommandLineFlags(env, jcaller, maxRenderProcesses,
      pluginDescriptor);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/BrowserStartupController_jni.h中。

       函数Java_com_android_org_chromium_content_browser_BrowserStartupController_nativeSetCommandLineFlags调用另外一个函数SetCommandLineFlags将Chromium渲染引擎设置为单进程架构,如下所示:

static void SetCommandLineFlags(JNIEnv* env,
                                jclass clazz,
                                jint max_render_process_count,
                                jstring plugin_descriptor) {
  std::string plugin_str =
      (plugin_descriptor == NULL
           ? std::string()
           : base::android::ConvertJavaStringToUTF8(env, plugin_descriptor));
  SetContentCommandLineFlags(max_render_process_count, plugin_str);
}
       这个函数定义在文件external/chromium_org/content/browser/android/browser_startup_controller.cc中。

       函数SetCommandLineFlags又会调用另外一个函数SetContentCommandLineFlags将Chromium渲染引擎设置为单进程架构,如下所示:

void SetContentCommandLineFlags(int max_render_process_count,
                                const std::string& plugin_descriptor) {
  ......

  CommandLine* parsed_command_line = CommandLine::ForCurrentProcess();
  
  int command_line_renderer_limit = -1;
  if (parsed_command_line->HasSwitch(switches::kRendererProcessLimit)) {
    std::string limit = parsed_command_line->GetSwitchValueASCII(
        switches::kRendererProcessLimit);
    int value;
    if (base::StringToInt(limit, &value)) {
      command_line_renderer_limit = value;
      if (value <= 0)
        max_render_process_count = 0;
    }
  }

  if (command_line_renderer_limit > 0) {
    int limit = std::min(command_line_renderer_limit,
                         static_cast<int>(kMaxRendererProcessCount));
    RenderProcessHost::SetMaxRendererProcessCount(limit);
  } else if (max_render_process_count <= 0) {
    // Need to ensure the command line flag is consistent as a lot of chrome
    // internal code checks this directly, but it wouldn't normally get set when
    // we are implementing an embedded WebView.
    parsed_command_line->AppendSwitch(switches::kSingleProcess);
  } else {
    int default_maximum = RenderProcessHost::GetMaxRendererProcessCount();
    DCHECK(default_maximum <= static_cast<int>(kMaxRendererProcessCount));
    if (max_render_process_count < default_maximum)
      RenderProcessHost::SetMaxRendererProcessCount(max_render_process_count);
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_startup_flags.cc中。

       函数SetContentCommandLineFlags首先检查Android WebView是否设置了switches::kRendererProcessLimit命令行参数。如果设置了,那么这个参数的值就会被解析出来,保存在本地变量command_line_renderer_limit中,用来限定Chromium渲染引擎最多可创建的Render进程的个数的。

       Chromium渲染引擎最多可创建的Render进程的个数还受到参数max_render_process_count的限制:

       1. 当本地变量command_line_renderer_limit的值大于0的时候,那么取max_render_process_count和command_line_renderer_limit之间的较小者作为最多可创建的Render进程的个数。

       2. 当本地变量command_line_renderer_limit的值小于等于0,并且参数max_render_process_count的值也小于等于0的时候,那么Chromium渲染引擎不允许创建Render进程,也就是它使用的是单进程架构。

       3. 当本地变量command_line_renderer_limit的值小于等于0,并且参数max_render_process_count的值大于0的时候,会调用RenderProcessHost类的静态成员函数GetMaxRendererProcessCount根据设备内存的大小计算出可以创建的Render进程的最大数default_maximum。如果参数max_render_process_count的值小于这个最大值,那么就将它设置为可以创建的Render进程的个数。

       在我们这个情景中,Android WebView没有设置switches::kRendererProcessLimit命令行参数,并且参数max_render_process_count的值等于0,因此函数SetContentCommandLineFlags会将Chromium渲染引擎设置为单进程架构。这是通过在Android WebView的命令行参数中设置一个switches::kSingleProcess选项实现的。

       这一步执行完成后,回到前面分析的BrowserStartupController类的成员函数startBrowserProcessesSync中,接下来它会调用另外一个成员函数contentStart启动Chromium渲染引擎的Browser端,如下所示:

public class BrowserStartupController {
    ......

    int contentStart() {
        return ContentMain.start();
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/BrowserStartupController.java中。

       BrowserStartupController类的成员函数contentStart调用ContentMain类的静态成员函数Start启动Chromium渲染引擎的Browser端,如下所示:

public class ContentMain {
    ......

    public static int start() {
        return nativeStart();
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/app/ContentMain.java中。

       ContentMain类的静态成员函数Start调用另外一个静态成员函数nativeStart启动Chromium渲染引擎的Browser端。

       ContentMain类的静态成员函数nativeStart是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_app_ContentMain_nativeStart实现,如下所示:

__attribute__((visibility("default")))
jint Java_com_android_org_chromium_content_app_ContentMain_nativeStart(JNIEnv*
    env, jclass jcaller) {
  return Start(env, jcaller);
}
        这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentMain_jni.h中。

        函数Java_com_android_org_chromium_content_app_ContentMain_nativeStart调用另外一个函数Start启动Chromium渲染引擎的Browser端,如下所示:

LazyInstance<scoped_ptr<ContentMainRunner> > g_content_runner =
    LAZY_INSTANCE_INITIALIZER;

LazyInstance<scoped_ptr<ContentMainDelegate> > g_content_main_delegate =
    LAZY_INSTANCE_INITIALIZER;

......

static jint Start(JNIEnv* env, jclass clazz) {
  ......

  if (!g_content_runner.Get().get()) {
    ContentMainParams params(g_content_main_delegate.Get().get());
    g_content_runner.Get().reset(ContentMainRunner::Create());
    g_content_runner.Get()->Initialize(params);
  }
  return g_content_runner.Get()->Run();
}
       这个函数定义在文件external/chromium_org/content/app/android/content_main.cc中。

       函数Start判断全局变量g_content_runner是否已经指向了一个ContentMainRunner对象。如果还没有指向,那么就说明Chromium渲染引擎的Browser端还没有启动。在这种情况下,函数Start就会调用ContentMainRunner类的静态成员函数Create创建一个ContentMainRunner对象,并且保存在全局变量g_content_runner中。

       ContentMainRunner类的静态成员函数Create的实现如下所示:

ContentMainRunner* ContentMainRunner::Create() {
  return new ContentMainRunnerImpl();
}
       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       从这里可以看到,ContentMainRunner类的静态成员函数Create实际创建的是一个ContentMainRunnerImpl对象。这个ContentMainRunnerImpl返回给函数Start之后,它的成员函数Initialize就会被调用,用来对它进行初始化。

       从前面Android WebView加载Chromium动态库的过程分析一文可以知道,全局变量g_content_main_delegate指向的是一个AwMainDelegate对象。这个AwMainDelegate对象将会封装在一个ContentMainParams对象中,并且传递给前面创建的ContentMainRunnerImpl对象的成员函数Initialize,以便后者用来执行初始化工作。

        ContentMainRunnerImpl类的成员函数Initialize的实现如下所示:

class ContentMainRunnerImpl : public ContentMainRunner {
 public:
  ......

  virtual int Initialize(const ContentMainParams& params) OVERRIDE {
    ......

    delegate_ = params.delegate;
    ......

    int exit_code;
    if (delegate_ && delegate_->BasicStartupComplete(&exit_code))
      return exit_code;
    ......
    
    const CommandLine& command_line = *CommandLine::ForCurrentProcess();
    std::string process_type =
        command_line.GetSwitchValueASCII(switches::kProcessType);

    ......

    ContentClientInitializer::Set(process_type, delegate_);

    ......
}
       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       参数params描述的ContentMainParams对象的成员变量delegate指向的是就是前面描述的全局变量g_content_main_delegate指向的AwMainDelegate对象。ContentMainRunnerImpl类的成员函数Initialize会将这个AwMainDelegate对象保存在成员变量delegate_中,并且会调用这个AwMainDelegate对象的成员函数BasicStartupComplete执行一些基本的初始化工作,如下所示:

bool AwMainDelegate::BasicStartupComplete(int* exit_code) {
  content::SetContentClient(&content_client_);
  ......
}
       这个函数定义在文件external/chromium_org/android_webview/lib/main/aw_main_delegate.cc中。

       AwMainDelegate类的成员变量content_client_描述的是一个AwContentClient对象。这个AwContentClient对象将会设置给Chromium的Content层。这个通过调用函数SetContentClient实现的,如下所示:

static ContentClient* g_client;

......

void SetContentClient(ContentClient* client) {
  g_client = client;
  ......
}

ContentClient* GetContentClient() {
  return g_client;
}
       这个函数定义在文件external/chromium_org/content/public/common/content_client.cc中。

       函数SetContentClient将参数client指向的一个AwContentClient对象保存在全局变量g_client中。这个AwContentClient对象以后可以通过调用函数GetContentClient获得。

       这一步执行完成后,回到前面分析的ContentMainRunnerImpl类的成员函数Initialize的中,它接下来检查Android WebView是否设置了switches::kProcessType命令行参数。如果设置了,那么该参数值process_type描述的就是当前启动的进程的类型(Browser端、Render端或者GPU端)。如果没有设置,那么参数值process_type就会等于一个空字符串,表示当前要启动的是一个Browser端。

       在我们这个情景中,Android WebView没有设置switches::kProcessType,因此得到的参数值process_type就等于一个空字符串。这个空字符串,连同ContentMainRunnerImpl类的成员变量delegate_指向的AwMainDelegate对象,会进一步传递给ContentClientInitializer类的静态成员函数Set执行初始化操作,如下所示:

class ContentClientInitializer {
 public:
  static void Set(const std::string& process_type,
                  ContentMainDelegate* delegate) {
    ContentClient* content_client = GetContentClient();
    if (process_type.empty()) {
      if (delegate)
        content_client->browser_ = delegate->CreateContentBrowserClient();
      ......
    }

    ......
  }
 
  ......
};

      这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

      ContentClientInitializer类的静态成员函数Set首先是调用前面提到的函数GetContentClient获得一个AwContentClient对象,接下来判断参数process_type的值是否等于一个空字符串。如果等于的话,那么就会调用参数delegate指向的一个AwMainDelegate对象的成员函数CreateContentBrowserClient创建一个ContentBrowserClient对象,并且保存在前面获得的AwContentClient对象的成员变量browser_中。

      从前面的分析可以知道,参数process_type的值等于一个空字符串,因此接下来ContentClientInitializer类的静态成员函数Set就会调用参数delegate指向的AwMainDelegate对象的成员函数CreateContentBrowserClient创建一个ContentBrowserClient对象,如下所示:

content::ContentBrowserClient*
    AwMainDelegate::CreateContentBrowserClient() {
  content_browser_client_.reset(new AwContentBrowserClient(this));
  return content_browser_client_.get();
}
       这个函数定义在文件external/chromium_org/android_webview/lib/main/aw_main_delegate.cc中。

       AwMainDelegate类的成员函数CreateContentBrowserClient实际创建的是一个AwContentBrowserClient对象。这个AwContentBrowserClient对象是从ContentBrowserClient类继承下来的。

       这意味着前面设置到Conent层的一个AwContentClient对象的成员变量browser_指向的是一个AwContentBrowserClient对象。这个AwContentBrowserClient对象在接下来启动Chromium渲染引擎的Browser端过程中会使用到。

       这一步执行完成后,回到前面分析的函数Start中。这时候它就创建了一个ContentMainRunner对象,并且对这个ContentMainRunner对象进行初始化。接下来,函数Start继续调用这个ContentMainRunner对象的成员函数Run,以便启动Chromium渲染引擎的Browser端,如下所示:

class ContentMainRunnerImpl : public ContentMainRunner {
 public:
  ......

  virtual int Run() OVERRIDE {
    ......
    const CommandLine& command_line = *CommandLine::ForCurrentProcess();
    std::string process_type =
          command_line.GetSwitchValueASCII(switches::kProcessType);

    MainFunctionParams main_params(command_line);
    ......

#if !defined(OS_IOS)
    return RunNamedProcessTypeMain(process_type, main_params, delegate_);
#else
    return 1;
#endif
  }

  ......
};
       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       ContentMainRunner类的成员函数Run首先获得Android WebView设置的命令行参数switches::kProcessType的值。前面提到,Android WebView没有设置命令行参数switches::kProcessType,因此这里获得的值为一个空字符串,也就是本地变量process_type的值等于一个空字符串。

       接下来,ContentMainRunner类的成员函数Run还会将Android WebView设置的命令行参数封装在一个MainFunctionParams对象中。这个MainFunctionParams对象,连同前面设置的本地变量process_type,以及ContentMainRunner类的成员变量delegate_指向的一个AwMainDelegate对象,会传递给另外一个函数RunNamedProcessTypeMain。这个函数将会负责启动Chromium渲染引擎的Browser端,如下所示:

   const MainFunctionParams& main_function_params,
    ContentMainDelegate* delegate) {
  static const MainFunction kMainFunctions[] = {
#if !defined(CHROME_MULTIPLE_DLL_CHILD)
    { "",                            BrowserMain },
#endif
    ......
    { switches::kRendererProcess,    RendererMain },
    { switches::kGpuProcess,         GpuMain }, 
    ......
  };

  RegisterMainThreadFactories();

  for (size_t i = 0; i < arraysize(kMainFunctions); ++i) {
    if (process_type == kMainFunctions[i].name) {
      if (delegate) {
        int exit_code = delegate->RunProcess(process_type,
            main_function_params);
#if defined(OS_ANDROID)
        // In Android's browser process, the negative exit code doesn't mean the
        // default behavior should be used as the UI message loop is managed by
        // the Java and the browser process's default behavior is always
        // overridden.
        if (process_type.empty())
          return exit_code;
#endif
        if (exit_code >= 0)
          return exit_code;
      }
      return kMainFunctions[i].function(main_function_params);
    }
  }

  ......
}

       这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

       函数RunNamedProcessTypeMain定义了一个MainFunction数组。这个MainFunction数组用来指定不同类型的进程的入口函数。其中,Browser进程、Render进程和GPU进程对应的入口函数分别为BrowserMain、RendererMain和GpuMain。当然,只有在参数delegate的值等于NULL的情况下,这个MainFunction数组才会生效。否则的话,所有进程的入口函数都为该参数指向的ContentMainDelegate对象的成员函数RunProcess。对于非Browser进程,如果参数delegate指向的ContentMainDelegate对象的成员函数RunProcess的返回值小于0,那么上述MainFunction数组也会同样生效。

       从前面的调用过程可以知道,参数process_type的值是一个空字符串,表示函数RunNamedProcessTypeMain需要启动的是一个Chromium渲染引擎的Browser进程(端)。这时候由于另外一个参数delegate指向了一个AwMainDelegate对象,因此,函数RunNamedProcessTypeMain将调用这个AwMainDelegate对象的成员函数RunProcess启动Chromium渲染引擎的Browser端。

       函数RunNamedProcessTypeMain在调用参数delegate指向的AwMainDelegate对象的成员函数RunProcess启动Chromium渲染引擎的Browser端之前,还会调用函数RegisterMainThreadFactories注册一些线程创建工厂函数,如下所示:

static void RegisterMainThreadFactories() {
#if !defined(CHROME_MULTIPLE_DLL_BROWSER)
  ......
  RenderProcessHostImpl::RegisterRendererMainThreadFactory(
      CreateInProcessRendererThread);
  ......
#else
  ......
#endif
}
      这个函数定义在文件external/chromium_org/content/app/content_main_runner.cc中。

      其中的一个线程创建工厂函数是Render线程创建工厂函数,它被指定为函数CreateInProcessRendererThread,并且会通过调用RenderProcessHostImpl类的静态成员函数RegisterRendererMainThreadFactory记录起来,如下所示:

RendererMainThreadFactoryFunction g_renderer_main_thread_factory = NULL;

......

void RenderProcessHostImpl::RegisterRendererMainThreadFactory(
    RendererMainThreadFactoryFunction create) {
  g_renderer_main_thread_factory = create;
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。

       参数create描述的函数CreateInProcessRendererThread将会保存在全局变量g_renderer_main_thread_factory中。以后Chromium渲染引擎的Browser端将会通过这个函数创建In-Process Renderer Thread,以便用来加载和渲染指定的URL。

       这一步执行完成后,回到前面分析的函数RunNamedProcessTypeMain,接下来它就会调用参数delegate指向的AwMainDelegate对象的成员函数RunProcess启动Chromium渲染引擎的Browser端,如下所示:

int AwMainDelegate::RunProcess(
    const std::string& process_type,
    const content::MainFunctionParams& main_function_params) {
  if (process_type.empty()) {
    ......

    browser_runner_.reset(content::BrowserMainRunner::Create());
    int exit_code = browser_runner_->Initialize(main_function_params);
    ......

    return 0;
  }

  return -1;
}
      这个函数定义在文件external/chromium_org/android_webview/lib/main/aw_main_delegate.cc中。

      从前面的调用过程可以知道,参数process_type的值等于一个空字符串。在这种情况下,AwMainDelegate类的成员函数RunProcess会调用BrowserMainRunner类的静态成员函数Create创建一个BrowserMainRunner对象,并且会保存在成员变量browser_runner_中,如下所示:

BrowserMainRunner* BrowserMainRunner::Create() {
  return new BrowserMainRunnerImpl();
}
      这个函数定义在文件external/chromium_org/content/browser/browser_main_runner.cc中。

      从这里可以看到,BrowserMainRunner类的静态成员函数Create创建的实际上是一个BrowserMainRunnerImpl对象。这意味着AwMainDelegate类的成员变量browser_runner_指向的是一个BrowserMainRunnerImpl对象。这个BrowserMainRunnerImpl对象的成员函数Initialize接下来会被调用。在调用的过程中,就会将Chromium渲染引擎的Browser端启动起来,如下所示:

class BrowserMainRunnerImpl : public BrowserMainRunner {
 public:
  ......

  virtual int Initialize(const MainFunctionParams& parameters) OVERRIDE {
    ......

    if (!initialization_started_) {
      initialization_started_ = true;
      ......

      main_loop_.reset(new BrowserMainLoop(parameters));

      main_loop_->Init();

      main_loop_->EarlyInitialization();

      ......

      main_loop_->MainMessageLoopStart();

      ......
    }

    main_loop_->CreateStartupTasks();
    int result_code = main_loop_->GetResultCode();
    if (result_code > 0)
      return result_code;

    // Return -1 to indicate no early termination.
    return -1;
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_runner.cc中。

       BrowserMainRunnerImpl类的成员函数Initialize首先检查成员变量initialization_started_的值是否等于true。如果等于true,那么就说明Chromium渲染引擎的Browser端已经启动过。在这种情况下,BrowserMainRunnerImpl类的成员函数Initialize只会创建一些Startup Task。

       如果Chromium渲染引擎的Browser端还没有启动过,那么BrowserMainRunnerImpl类的成员函数Initialize首先就会创建一个BrowserMainLoop对象,并且保存在成员变量main_loop_中。接下来,BrowserMainRunnerImpl类的成员函数Initialize会调用上述BrowserMainLoop对象的成员函数Init和EarlyInitialization对其进行初始化。初始化完成后,它的成员函数MainMessageLoopStart又会被调用。调用完成后,Chromium渲染引擎的Browser端就启动完成了。启动完成后,上述BrowserMainLoop对象的成员函数CreateStartupTasks也会被调用,用来创建一些Startup Task。

       接下来,我们就分别分析BrowserMainLoop类的成员函数Init、EarlyInitialization、MainMessageLoopStart和CreateStartupTasks的实现,以便了解Chromium渲染引擎的Browser端的启动过程。

       BrowserMainLoop类的成员函数Init用来创建一个BrowserMainParts对象,它的实现如下所示:

void BrowserMainLoop::Init() {
  ......
  parts_.reset(
      GetContentClient()->browser()->CreateBrowserMainParts(parameters_));
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       BrowserMainLoop类的成员函数Init首先调用前面提到的函数GetContentClient获得一个AwContentClient对象,接下来又会调用这个AwContentClient对象的成员函数browser获得它的成员变量browser_指向的一个AwContentBrowserClient对象。获得了这个AwContentBrowserClient对象之后,就可以调用它的成员函数CreateBrowserMainParts创建一个BrowserMainParts对象,并且保存在BrowserMainLoop类的成员变量parts_中,如下所示:

content::BrowserMainParts* AwContentBrowserClient::CreateBrowserMainParts(
    const content::MainFunctionParams& parameters) {
  return new AwBrowserMainParts(browser_context_.get());
}
       这个函数定义在文件external/chromium_org/android_webview/browser/aw_content_browser_client.cc中。

       从这里可以看出,AwContentBrowserClient类的成员函数CreateBrowserMainParts创建的实际上是一个AwBrowserMainParts对象。这个AwBrowserMainParts对象接下来会用来创建一个Native层的UI Message Loop。这个UI Message Loop接下来又会用来创建一个Browser Thread,用来表示Chromium渲染引擎的Browser端。

       这一步执行完成后,回到前面分析的BrowserMainRunnerImpl类的成员函数Initialize中,接下来BrowserMainLoop类的成员函数EarlyInitialization会被调用,用来创建一个Native层的UI Message Loop,如下所示:

void BrowserMainLoop::EarlyInitialization() {
  ......

  if (parts_)
    parts_->PreEarlyInitialization();
  
  ......
}
      这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

      从前面的分析可以知道,BrowserMainLoop类的成员变量parts_指向的是一个AwBrowserMainParts对象。BrowserMainLoop类的成员函数EarlyInitialization会调用这个AwBrowserMainParts对象的成员函数PreEarlyInitialization创建一个UI Message Loop,如下所示:

void AwBrowserMainParts::PreEarlyInitialization() {
  ......
  main_message_loop_.reset(new base::MessageLoopForUI);
  base::MessageLoopForUI::current()->Start();
}
       这个函数定义在文件external/chromium_org/android_webview/browser/aw_browser_main_parts.cc中。

       AwBrowserMainParts类的成员函数PreEarlyInitialization创建了一个MessageLoopForUI对象。这个MessageLoopForUI对象描述的就是一个Native层的UI Message Loop。从前面Chromium多线程模型设计和实现分析一文可以知道,Native层的UI Message Loop并没有自己的线程,而是寄生在App的UI线程中运行(当前线程就是App的UI线程)。App的UI线程在Java层也有一个Message Loop,并且是由这个Java层的Message Loop驱动运行的。

       当我们往Native层的UI Message Loop发送一个消息的时候,Native层的UI Message Loop会向App的UI线程在Java层的Message Loop发送一个消息。当该消息被Java层的Message Loop调度执行的时候,之前发送在Native层的UI Message Loop中的消息就会得到执行。Chromium渲染引擎的Browser端,就是以这种方式运行在App的UI线程中的。

       AwBrowserMainParts类的成员函数PreEarlyInitialization在当前线程中创建了一个MessageLoopForUI对象之后,以后在当前线程中调用MessageLoopForUI类的静态成员函数current时,就会获得该MessageLoopForUI对象。有了这个MessageLoopForUI对象之后,AwBrowserMainParts类的成员函数PreEarlyInitialization就会调用它的成员函数Start,用来启动它描述的Native UI Message Loop。

       这一步执行完成后,回到前面分析的BrowserMainRunnerImpl类的成员函数Initialize中,接下来BrowserMainLoop类的成员函数MainMessageLoopStart会被调用,用来创建一个Browser Thread,如下所示:

void BrowserMainLoop::MainMessageLoopStart() {
  ......

  InitializeMainThread();

  .....
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       BrowserMainLoop类的成员函数MainMessageLoopStart调用另外一个成员函数InitializeMainThread创建一个Browser Thread,如下所示:

void BrowserMainLoop::InitializeMainThread() {
  ......
  main_thread_.reset(
      new BrowserThreadImpl(BrowserThread::UI, base::MessageLoop::current()));
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       BrowserMainLoop类的成员函数InitializeMainThread使用前面创建的Native UI Message Loop创建了一个Browser Thread。这个Browser Thread描述的就是Chromium渲染引擎的Browser端。由于这个Browser Thread是使用前面创建的Native UI Message Loop创建的,因此,它实际上描述的是App的UI线程。以后Chromium请求这个Browser Thread执行操作时,这个操作就会在App的UI线程中执行。

       这一步执行完成之后,Chromium渲染引擎的Browser端就启动完成了。再回到前面分析的BrowserMainRunnerImpl类的成员函数Initialize中,接下来BrowserMainLoop类的成员函数CreateStartupTasks会被调用,用来在Chromium渲染引擎的Browser端执行一些Startup Task,如下所示:

void BrowserMainLoop::CreateStartupTasks() {
  ......

  if (!startup_task_runner_.get()) {
    ......

    StartupTask pre_create_threads =
        base::Bind(&BrowserMainLoop::PreCreateThreads, base::Unretained(this));
    startup_task_runner_->AddTask(pre_create_threads);

    ......
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       其中的一个Startup Task,是在Chromium渲染引擎的Browser端创建其它线程(IO线程、数据库线程、文件线程等)之前执行的,对应的函数为BrowserMainLoop类的成员函数PreCreateThreads。

       BrowserMainLoop类的成员函数PreCreateThread会检查Android WebView的命令行参数是否设置了一个switches::kSingleProcess选项。如果设置了,那么就会将Chromium渲染引擎设置为单进程架构,如下所示:

int BrowserMainLoop::PreCreateThreads() {
  ......

#if !defined(OS_IOS) && (!defined(GOOGLE_CHROME_BUILD) || defined(OS_ANDROID))
  // Single-process is an unsupported and not fully tested mode, so
  // don't enable it for official Chrome builds (except on Android).
  if (parsed_command_line_.HasSwitch(switches::kSingleProcess))
    RenderProcessHost::SetRunRendererInProcess(true);
#endif
  return result_code_;
}
       这个函数定义在文件external/chromium_org/content/browser/browser_main_loop.cc中。

       从前面的分析可以知道,函数SetContentCommandLineFlags会给Android WebView的命令行参数设置一个switches::kSingleProcess选项。在这种情况下,BrowserMainLoop类的成员函数PreCreateThread会调用RenderProcessHost类的静态成员函数SetRunRendererInProcess将Chromium渲染引擎设置为单进程架构,如下所示:

bool g_run_renderer_in_process_ = false;

......

void RenderProcessHost::SetRunRendererInProcess(bool value) {
  g_run_renderer_in_process_ = value;

  ......
}
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。

       从前面的调用过程可以知道,参数value的值等于true。这时候RenderProcessHost类的静态成员函数SetRunRendererInProcess就会将全局变量g_run_renderer_in_process_的值设置为true,表示Chromium渲染引擎使用单进程加载,也就是在需要创建Render进程来加载和渲染网页时,通过一个In-Process Renderer Thread模拟。

       这一步执行完成后,Chromium渲染引擎的Browser端就启动完毕。回到前面分析的WebViewChromium类的成员函数init中,接下来它会继续调用另外一个成员函数initForReal为WebView创建一个AwContents对象。这个AwContents对象以后可以用来加载指定的URL。

       接下来,我们就继续分析WebViewChromium类的成员函数initForReal创建AwContents对象的过程,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    private void initForReal() {
        ......
        mAwContents = new AwContents(mFactory.getBrowserContext(), mWebView, ctx,
                new InternalAccessAdapter(), new WebViewNativeGLDelegate(),
                mContentsClientAdapter, mWebSettings.getAwSettings());

        ......
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       WebViewChromium类的成员函数initForReal主要是创建了一个AwContents对象,并且保存在成员变量mAwContents中。这个AwContents对象的创建过程,也就是AwContents类的构造函数的实现,如下所示:

public class AwContents {
    ......

    public AwContents(AwBrowserContext browserContext, ViewGroup containerView, Context context,
            InternalAccessDelegate internalAccessAdapter, NativeGLDelegate nativeGLDelegate,
            AwContentsClient contentsClient, AwSettings awSettings) {
        this(browserContext, containerView, context, internalAccessAdapter, nativeGLDelegate,
                contentsClient, awSettings, new DependencyFactory());
    }

    ......
}
      这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

      AwContents类的构造函数调用另外一个重载版本的构造函数创建一个AwContents对象,如下所示:

public class AwContents {
    ......

    public AwContents(AwBrowserContext browserContext, ViewGroup containerView, Context context,
            InternalAccessDelegate internalAccessAdapter, NativeGLDelegate nativeGLDelegate,
            AwContentsClient contentsClient, AwSettings settings,
            DependencyFactory dependencyFactory) {
        ......
        mNativeGLDelegate = nativeGLDelegate;
        ......

        setNewAwContents(nativeInit(mBrowserContext));

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       参数nativeGLDelegate指向的是一个WebViewNativeGLDelegate对象。这个WebViewNativeGLDelegate对象会被保存在AwContents类的成员变量mNativeGLDelegate中。

       AwContents类的构造函数会调用另外一个成员函数nativeInit在Native层创建一个WebContents对象。WebContents类是Chromium的Content层向外提供的一个类,通过它可以描述一个网页。

       在Native层创建了一个WebContents对象之后,AwContents类的构造函数会将该WebContents对象传递给另外一个成员函数setNewAwContents,用来在Native层创建一个ContentViewCore对象。ContentViewCore类同样是Chromium的Content层向外提供的一个类,通过它可以加载一个指定的URL,也就是通过可以启动Chromium渲染引擎的Render端。

       接下来,我们就继续分析AwContents成员函数nativeInit和setNewAwContents的实现,以便了解Android WebView在Native层ContentViewCore对象的过程,为接下来分析Chromium渲染引擎的Render端的启动过程做准备。

       AwContents成员函数nativeInit是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_android_1webview_AwContents_nativeInit实现,如下所示:

__attribute__((visibility("default")))
jlong
    Java_com_android_org_chromium_android_1webview_AwContents_nativeInit(JNIEnv*
    env, jclass jcaller,
    jobject browserContext) {
  return Init(env, jcaller, browserContext);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/android_webview/jni/AwContents_jni.h中。

       函数Java_com_android_org_chromium_android_1webview_AwContents_nativeInit调用另外一个函数Init创建一个WebContents对象,并且使用这个WebContents对象创建一个Native层的AwContents对象,如下所示:

static jlong Init(JNIEnv* env, jclass, jobject browser_context) {
  ......
  scoped_ptr<WebContents> web_contents(content::WebContents::Create(
      content::WebContents::CreateParams(AwBrowserContext::GetDefault())));
  ......
  return reinterpret_cast<intptr_t>(new AwContents(web_contents.Pass()));
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       函数Init是通过调用WebContents类的静态成员函数Create创建一个WebContents对象的。WebContents类的静态成员函数Create的实现,可以参考前面Chromium网页Frame Tree创建过程分析一文。

       创建了一个WebContents对象之后,函数Init就使用它来创建一个Native层的AwContents对象,如下所示:

AwContents::AwContents(scoped_ptr<WebContents> web_contents)
    : web_contents_(web_contents.Pass()),
      shared_renderer_state_(
          BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI),
          this),
      browser_view_renderer_(
          this,
          &shared_renderer_state_,
          web_contents_.get(),
          BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI)),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       AwContents类的构造函数首先将参数web_contents指向的WebContents对象保存在成员变量web_contents_中,接下来又会分别构造一个SharedRendererState对象和一个BrowserViewRenderer对象,并且保存在成员变量shared_renderer_state_和browser_view_renderer_中。

       通过AwContents类的成员变量shared_renderer_state_描述的SharedRendererState对象,Chromium渲染引擎的Browser端和Render端可以请求App的Render Thread执行GPU命令。同时,这个SharedRendererState对象也会用来保存Chromium渲染引擎的Render端渲染的每一帧数据。这些帧数据将会交给Chromium渲染引擎的Browser端合成显示在屏幕上。

       通过AwContents类的成员变量browser_view_renderer_描述的BrowserViewRenderer对象,则可以为Chromium渲染引擎的Render端创建一个Synchronous Compositor。这个Synchronous Compositor可以用来将网页的CC Layer Tree渲染在一个Synchronous Compositor Output Surface上。

       接下来我们就继续分析上述SharedRendererState对象和BrowserViewRenderer对象的构造过程。

       SharedRendererState对象的构造过程,也就是SharedRendererState类的构造函数的实现,如下所示:

SharedRendererState::SharedRendererState(
    scoped_refptr<base::MessageLoopProxy> ui_loop,
    BrowserViewRendererClient* client)
    : ui_loop_(ui_loop),
      client_on_ui_(client),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/android_webview$ vi browser/shared_renderer_state.cc中。

       参数ui_loop描述的是一个Native UI Message Loop。这个Native UI Message Loop是通过前面调用BrowserThread类的静态成员函数GetMessageLoopProxyForThread获得的。这个Native UI Message Loop会保存在SharedRendererState类的成员变量ui_loop_。以后通过这个成员变量,就可以向App的Render Thread请求执行GPU操作了。

       另外一个参数client指向的就是前面创建的AwContents对象。这个AwContents对象会保存在SharedRendererState类的成员变量client_on_ui_中。

       BrowserViewRenderer对象的构造过程,也就是BrowserViewRenderer类的构造函数的实现,如下所示:

BrowserViewRenderer::BrowserViewRenderer(
    BrowserViewRendererClient* client,
    SharedRendererState* shared_renderer_state,
    content::WebContents* web_contents,
    const scoped_refptr<base::SingleThreadTaskRunner>& ui_task_runner)
    : client_(client),
      ...... {
  ......
  content::SynchronousCompositor::SetClientForWebContents(web_contents_, this);
  ......
}
       这个函数定义在文件external/chromium_org/android_webview/browser/browser_view_renderer.cc中。

       从前面的调用过程可以知道,参数client指向的是前面创建的AwContents对象。这个AwContents对象会保存在BrowserViewRenderer类的成员变量client_中。

       BrowserViewRenderer类的构造函数接下来会调用SynchronousCompositor类的静态成员函数SetClientForWebContents创建一个Synchronous Compositor,如下所示:

void SynchronousCompositor::SetClientForWebContents(
    WebContents* contents,
    SynchronousCompositorClient* client) {
  ......
  if (client) {
    ......
    SynchronousCompositorImpl::CreateForWebContents(contents);
  }
  if (SynchronousCompositorImpl* instance =
      SynchronousCompositorImpl::FromWebContents(contents)) {
    instance->SetClient(client);
  }
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

       从前面的调用过程可以知道,参数client的值不等于NULL,它指向的是一个BrowserViewRenderer对象。在这种情况下,SynchronousCompositor类的静态成员函数SetClientForWebContents会调用SynchronousCompositorImpl类的静态成员函数CreateForWebContents为前面创建的WebContents对象创建一个Synchronous Compositor。

       SynchronousCompositorImpl类的静态成员函数CreateForWebContents是从父类WebContentsUserData<SynchronousCompositorImpl>继承下来的,它的实现如下所示:

template <typename T>
class WebContentsUserData : public base::SupportsUserData::Data {
 public:
  // Creates an object of type T, and attaches it to the specified WebContents.
  // If an instance is already attached, does nothing.
  static void CreateForWebContents(WebContents* contents) {
    ......
    if (!FromWebContents(contents))
      contents->SetUserData(UserDataKey(), new T(contents));
  }

  ......
};
       这个函数定义在文件external/chromium_org/content/public/browser/web_contents_user_data.h中。

       WebContentsUserData<SynchronousCompositorImpl>类的静态成员函数CreateForWebContents首先调用另外一个FromWebContents检查之前是否已经为参数contents描述的WebContents对象创建过一个SynchronousCompositorImpl对象。如果没有创建过,那么就会创建一个,并且保存在该WebContents对象的内部,这是通过调用它的成员函数SetUserData实现的。这里创建出来的SynchronousCompositorImpl对象描述的就是一个ynchronous Compositor。

       这一步执行完成后,回到前面分析的SynchronousCompositor类的静态成员函数SetClientForWebContents中,接下来它又会通过SynchronousCompositorImpl类的静态成员函数FromWebContents获得前面创建的SynchronousCompositorImpl对象,并且调用它的成员函数SetClient,用来将参数client指向的BrowserViewRenderer对象保存在内部,如下所示:

void SynchronousCompositorImpl::SetClient(
    SynchronousCompositorClient* compositor_client) {
  DCHECK(CalledOnValidThread());
  compositor_client_ = compositor_client;
}
      这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

      SynchronousCompositorImpl类的成员函数SetClient将参数compositor_client指向的BrowserViewRenderer对象保存在成员变量compositor_client_中。

      这一步执行完成后,回到前面分析的Java层的AwContents类的构造函数中,这时候就在Native层创建一个WebContents对象、一个AwContents对象、一个SharedRendererState对象、一个BrowserViewRenderer对象,以及一个SynchronousCompositorImpl对象。这些对象后面在启动Chromium渲染引擎的Render端,以及Chromium渲染引擎的Render端渲染网页UI时,都会使用到。

       Java层的AwContents类的构造函数接下来会调用另外一个成员函数setNewAwContents在Native层创建一个ContentViewCore对象,如下所示:

public class AwContents {
    ......

    private void setNewAwContents(long newAwContentsPtr) {
        ......

        mNativeAwContents = newAwContentsPtr;
        ......

        long nativeWebContents = nativeGetWebContents(mNativeAwContents);
        mContentViewCore = createAndInitializeContentViewCore(
                mContainerView, mContext, mInternalAccessAdapter, nativeWebContents,
                new AwGestureStateListener(), mContentViewClient, mZoomControls);
        .......
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       从前面的调用过程可以知道,参数newAwContentsPtr描述的是前面在Native层创建的AwContents对象。这个AwContents对象将会保存在AwContents类的成员变量mNativeAwContents中。

        AwContents类的成员函数setNewAwContents接下来又会调用另外一个成员函数nativeGetWebContents获得用来创建上述Native层AwContents对象所使用的一个WebContents对象。有了这个WebContents对象之后,就使用它来在Native层创建一个ContentViewCore对象。这是通过调用AwContents类的成员函数createAndInitializeContentViewCore实现的,如下所示:

public class AwContents {
    ......

    private static ContentViewCore createAndInitializeContentViewCore(ViewGroup containerView,
            Context context, InternalAccessDelegate internalDispatcher, long nativeWebContents,
            GestureStateListener gestureStateListener,
            ContentViewClient contentViewClient,
            ContentViewCore.ZoomControlsDelegate zoomControlsDelegate) {
        ContentViewCore contentViewCore = new ContentViewCore(context);
        contentViewCore.initialize(containerView, internalDispatcher, nativeWebContents,
                context instanceof Activity ?
                        new ActivityWindowAndroid((Activity) context) :
                        new WindowAndroid(context.getApplicationContext()));
        ......
        return contentViewCore;
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       AwContents类的成员函数createAndInitializeContentViewCore首先会创建一个Java层的ContentViewCore对象,然后再调用这个Java层的ContentViewCore对象的成员函数initialize对它进行初始化。在初始化的过程中,就会在Native层创建一个对应的ContentViewCore对象,如下所示:

public class ContentViewCore
        implements NavigationClient, AccessibilityStateChangeListener, ScreenOrientationObserver {
    ......

    public void initialize(ViewGroup containerView, InternalAccessDelegate internalDispatcher,
            long nativeWebContents, WindowAndroid windowAndroid) {
        ......

        mNativeContentViewCore = nativeInit(
                nativeWebContents, viewAndroidNativePointer, windowNativePointer,
                mRetainedJavaScriptObjects);
      
        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewCore.java中。

       ContentViewCore类的成员函数initialize会调用另外一个成员函数nativeInit在Native层创建一个ContentViewCore对象,并且保存在成员变量mNativeContentViewCore中。在创建这个Native层的ContentViewCore对象的时候,需要使用到参数nativeWebContents描述的一个Native层的WebContents对象。

       ContentViewCore类的成员函数nativeInit是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_content_browser_ContentViewCore_nativeInit实现,如下所示:

__attribute__((visibility("default")))
jlong
    Java_com_android_org_chromium_content_browser_ContentViewCore_nativeInit(JNIEnv*
    env, jobject jcaller,
    jlong webContentsPtr,
    jlong viewAndroidPtr,
    jlong windowAndroidPtr,
    jobject retainedObjectSet) {
  return Init(env, jcaller, webContentsPtr, viewAndroidPtr, windowAndroidPtr,
      retainedObjectSet);
}
      这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/content/jni/ContentViewCore_jni.h中。

      函数Java_com_android_org_chromium_content_browser_ContentViewCore_nativeInit调用另外一个函数Init在Native层创建一个ContentViewCore对象,如下所示:

jlong Init(JNIEnv* env,
           jobject obj,
           jlong native_web_contents,
           jlong view_android,
           jlong window_android,
           jobject retained_objects_set) {
  ContentViewCoreImpl* view = new ContentViewCoreImpl(
      env, obj,
      reinterpret_cast<WebContents*>(native_web_contents),
      reinterpret_cast<ui::ViewAndroid*>(view_android),
      reinterpret_cast<ui::WindowAndroid*>(window_android),
      retained_objects_set);
  return reinterpret_cast<intptr_t>(view);
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_view_core_impl.cc中。

       从这里可以看到,函数Init会使用参数native_web_contents描述的一个WebContents对象以及其它参数在Native层创建一个ContentViewCoreImpl对象。 ContentViewCoreImpl类是从ContentViewCore继承下来的。  

       ContentViewCoreImpl对象的创建过程,也就是ContentViewCoreImpl类的构造函数的实现,如下所示:

ContentViewCoreImpl::ContentViewCoreImpl(
    JNIEnv* env,
    jobject obj,
    WebContents* web_contents,
    ui::ViewAndroid* view_android,
    ui::WindowAndroid* window_android,
    jobject java_bridge_retained_object_set)
    : ......,
      web_contents_(static_cast<WebContentsImpl*>(web_contents)),
      ...... {
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/android/content_view_core_impl.cc中。

       ContentViewCoreImpl类的构造函数会将参数web_contents指向的一个WebContents对象保存在成员变量web_contents_中。以后通过ContentViewCoreImpl类的成员函数LoadUrl加载指定的URL时,就需要使用到这个WebContents对象。

       在Java层创建了一个AwContents对象和在Native层创建了一个WebContents对象和一个ContentViewCore对象之后,接下来我们就可以在Android WebView中加载指定的URL了。Android WebView又会请求Chromium渲染引擎启动一个Render端,并且在这个Render端中加载指定的URL。接下来,我们就从Android WebView中加载URL开始,分析Chromium渲染引擎启动Render端的过程。

       Android WebView提供了一个成员函数loadUrl,用来加载指定的URL,如下所示:

public class WebView extends AbsoluteLayout
        implements ViewTreeObserver.OnGlobalFocusChangeListener,
        ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
    ......

    public void loadUrl(String url) {
        checkThread();
        if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl=" + url);
        mProvider.loadUrl(url);
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebView.java中。

       WebView类的成员函数loadUrl首先调用成员函数checkThread检查当前线程是否是创建WebView的线程。如果不是,那么就会抛出一个异常出来。

       从前面Android WebView加载Chromium动态库的过程分析一文可以知道,WebView类的成员变量mProvider指向的是一个WebViewChromium对象。如果通过前面检查,那么接下来这个WebViewChromium对象的成员函数loadUrl会被调用,用来加载参数url指定的URL,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    @Override
    public void loadUrl(String url) {
        // Early out to match old WebView implementation
        if (url == null) {
            return;
        }
        loadUrl(url, null);
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       WebViewChromium类的成员函数loadUrl调用另外一个重载版本的成员函数loadUrl加载参数url指定的URL,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    @Override
    public void loadUrl(final String url, Map<String, String> additionalHttpHeaders) {
        ......

        LoadUrlParams params = new LoadUrlParams(url);
        if (additionalHttpHeaders != null) params.setExtraHeaders(additionalHttpHeaders);
        loadUrlOnUiThread(params);
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       WebViewChromium类重载版本的成员函数loadUrl将参数url和additinalHttpHeaders封装一个LoadUrlParams对象中,然后再传这个LoadUrlParams对象传递给另外一个成员函数loadUrlonUiThread,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    private void loadUrlOnUiThread(final LoadUrlParams loadUrlParams) {
        ......
        if (checkNeedsPost()) {
            // Disallowed in WebView API for apps targetting a new SDK
            assert mAppTargetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2;
            mRunQueue.addTask(new Runnable() {
                @Override
                public void run() {
                    mAwContents.loadUrl(loadUrlParams);
                }
            });
            return;
        }
        mAwContents.loadUrl(loadUrlParams);
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       WebViewChromium类的成员函数LoadUrlParams会调用成员函数checkNeedsPost检查当前线程是否就是WebView的创建线程。如果不是,并且当前的Android版本小于4.3,那么就会向WebView的创建线程的消息队列发送一个Runnable。当该Runnable被执行的时候,才会调用WebViewChromium类的成员变量mAwContents指向的一个AwContents对象的成员函数loadUrl加载参数loadUrlParam描述的URL。

       注意,如果当前线程不是WebView的创建线程,并且当前的Android版本大于等于4.3,那么WebViewChromium类的成员函数LoadUrlParams是不允许调用的。在我们这个情景中,前面已经保证了当前线程就是WebView的创建线程。在这种情况下,WebViewChromium类的成员函数LoadUrlParams就会直接调用成员变量mAwContents指向的一个AwContents对象的成员函数loadUrl加载参数loadUrlParam描述的URL,如下所示:

public class AwContents {
    ......

    public void loadUrl(LoadUrlParams params) {
        ......

        mContentViewCore.loadUrl(params);

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       从前面的分析可以知道,AwContents类的成员变量mContentViewCore指向的是一个ContentViewCore对象。AwContents类的成员函数loadUrl调用这个ContentViewCore对象的成员函数loadUrl加载参数params描述的URL。

       ContentViewCore类的成员函数loadUrl加载指定URL的过程可以参考前面Chromium网页Frame Tree创建过程分析一文。在加载的过程中,会创建一个RenderViewHostImpl对象。从前面Chromium的Render进程启动过程分析一文又可以知道,在创建这个RenderViewHostImpl对象的过程中,又会创建一个RenderProcessHostImpl对象描述一个Render端。接下来这个RenderProcessHostImpl对象的成员函数Init又会被调用。在调用的过程中,它就会判断是要创建一个Render进程还是一个Render线程描述一个Render端,如下所示:

bool RenderProcessHostImpl::Init() {    
  ......  
  
  if (run_renderer_in_process()) {  
    ......  
    in_process_renderer_.reset(g_renderer_main_thread_factory(channel_id));  
  
    base::Thread::Options options;  
    ......  
    options.message_loop_type = base::MessageLoop::TYPE_DEFAULT;  
      
    in_process_renderer_->StartWithOptions(options);  
    ......  
  } else {  
    ......  
  
    child_process_launcher_.reset(new ChildProcessLauncher(  
        new RendererSandboxedProcessLauncherDelegate(channel_.get()),  
        cmd_line,  
        GetID(),  
        this));  
  
    ......  
  }  
  
  return true;  
}  
       这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。

       RenderProcessHostImpl类的成员函数Init调用另外一个成员函数run_renderer_in_process判断要创建一个Render进程还是一个Render线程描述一个Render端。

       RenderProcessHostImpl类的成员函数run_renderer_in_process是从父类RenderProcessHost继承下来的,它的实现如下所示:

bool g_run_renderer_in_process_ = false;

......

bool RenderProcessHost::run_renderer_in_process() {
  return g_run_renderer_in_process_;
}
      这个函数定义在文件external/chromium_org/content/browser/renderer_host/render_process_host_impl.cc中。

      RenderProcessHostImpl类的成员函数run_renderer_in_process返回的是全局变量g_run_renderer_in_process_的值。从前面的分析可以知道,这个全局变量g_run_renderer_in_process_已经被Android WebView设置为true。因此,RenderProcessHostImpl类的成员函数Init会创建一个Render线程来描述一个Render端。

      这个Render线程是通过调用另外一个全局变量g_renderer_main_thread_factory描述的一个线程创建工厂函数创建的。从前面的分析可以知道,这个全局变量g_renderer_main_thread_factory描述的线程创建工厂函数为CreateInProcessRendererThread,它的实现如下所示:

base::Thread* CreateInProcessRendererThread(const std::string& channel_id) {
  return new InProcessRendererThread(channel_id);
}
       这个函数定义在文件external/chromium_org/content/renderer/in_process_renderer_thread.cc中。

       从这里可以看到,函数为CreateInProcessRendererThread创建的是一个InProcessRendererThread对象。这个InProcessRendererThread对象描述的是一个类型为In-Process的Render线程。这个Render线程在RenderProcessHostImpl类的成员函数Init中将会被启动起来。这时候Android WebView就将Chromium渲染引擎的Render端启动起来了。

       最后,我们分析Chromium渲染引擎的GPU端。由于Android WebView要求Chromium渲染引擎使用App的Render Thread来执行GPU命令,因此Chromium渲染引擎的GPU端是通过App的Render Thread描述的,它的启动过程可以参考前面Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章。

       为了让Chromium渲染引擎可以使用App的Render Thread执行GPU命令,Chromium的android_webview模块会创建一个DeferredGpuCommandService服务。这个DeferredGpuCommandService服务将会负责请求App的Render Thread执行Chromium渲染引擎的Render端和Browser端发出来的GPU命令。接下来我们就分析这个DeferredGpuCommandService服务的创建过程。

       DeferredGpuCommandService服务是在Android WebView的成员函数onDraw被App的UI线程调用时创建的,因此接下来我们就从Android WebView的成员函数onDraw开始分析DeferredGpuCommandService服务的创建过程,如下所示:

public class WebView extends AbsoluteLayout
        implements ViewTreeObserver.OnGlobalFocusChangeListener,
        ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
    ......

    @Override
    protected void onDraw(Canvas canvas) {
        mProvider.getViewDelegate().onDraw(canvas);
    }

    ......
}
       这个函数定义在文件frameworks/base/core/java/android/webkit/WebView.java中。

       前面提到,WebView类的成员变量mProvider指向的是一个WebViewChromium对象。WebView的成员函数onDraw调用这个WebViewChromium对象的成员函数getViewDelegate获得一个View Delegate,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    @Override
    // This needs to be kept thread safe!
    public WebViewProvider.ViewDelegate getViewDelegate() {
        return this;
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       从这里可以看到,WebViewChromium类的成员函数getViewDelegate返回的View Delegate就是当前正在处理的WebViewChromium对象。这个WebViewChromium对象返回给WebView类的成员函数onDraw之后,它的成员函数onDraw就会被调用,如下所示:

class WebViewChromium implements WebViewProvider,
          WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
    ......

    @Override
    public void onDraw(final Canvas canvas) {
        ......
        if (checkNeedsPost()) {
            runVoidTaskOnUiThreadBlocking(new Runnable() {
                @Override
                public void run() {
                    onDraw(canvas);
                }
            });
            return;
        }
        mAwContents.onDraw(canvas);
    }

    ......
}
       这个函数定义在文件frameworks/webview/chromium/java/com/android/webview/chromium/WebViewChromium.java中。

       WebViewChromium类的成员函数onDraw首先会调用成员函数checkNeedsPost检查当前线程是否就是WebView的创建线程。如果不是,那么就会向WebView的创建线程的消息队列发送一个Runnable。当该Runnable被执行的时候,才会重新进入WebViewChromium类的成员函数onDraw中,并且调用它的成员变量mAwContents指向的一个AwContents对象的成员函数onDraw,用来绘制网页的UI。

       如果当前线程就是WebView的创建线程,那么WebViewChromium类的成员函数onDraw就会直接调用成员变量mAwContents指向的一个AwContents对象的成员函数onDraw绘制网页的UI。

       AwContents类的成员函数onDraw的实现如下所示:

public class AwContents {
    ......

    public void onDraw(Canvas canvas) {
        mAwViewMethods.onDraw(canvas);
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       AwContents类的成员变量mAwViewMethods指向的是一个AwViewMethodsImpl对象。AwContents类的成员函数onDraw调用这个AwViewMethodsImpl对象的成员函数onDraw绘制网页的UI,如下所示:

public class AwContents {
    ......

    private long mNativeAwContents;
    ......
 
    private class AwViewMethodsImpl implements AwViewMethods {
        ......

        @Override
        public void onDraw(Canvas canvas) {
            ......

            if (!nativeOnDraw(mNativeAwContents, canvas, canvas.isHardwareAccelerated(),
                    mContainerView.getScrollX(), mContainerView.getScrollY(),
                    globalVisibleRect.left, globalVisibleRect.top,
                    globalVisibleRect.right, globalVisibleRect.bottom)) {
                ......
            }

            ......
        }

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/android_webview/java/src/org/chromium/android_webview/AwContents.java中。

       AwViewMethodsImpl类的成员函数onDraw会调用外部类AwContents的成员函数nativeOnDraw绘制网页的UI,并且会将外部类的成员变量mNativeAwContents描述的一个Native层的AwContents对象传递给它。

       AwContents类的成员函数nativeOnDraw是一个JNI方法,它由C++层的函数Java_com_android_org_chromium_android_1webview_AwContents_nativeOnDraw实现,如下所示:

__attribute__((visibility("default")))
jboolean
    Java_com_android_org_chromium_android_1webview_AwContents_nativeOnDraw(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeAwContents,
    jobject canvas,
    jboolean isHardwareAccelerated,
    jint scrollX,
    jint scrollY,
    jint visibleLeft,
    jint visibleTop,
    jint visibleRight,
    jint visibleBottom) {
  AwContents* native = reinterpret_cast<AwContents*>(nativeAwContents);
  CHECK_NATIVE_PTR(env, jcaller, native, "OnDraw", false);
  return native->OnDraw(env, jcaller, canvas, isHardwareAccelerated, scrollX,
      scrollY, visibleLeft, visibleTop, visibleRight, visibleBottom);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/android_webview/jni/AwContents_jni.h中。

       函数Java_com_android_org_chromium_android_1webview_AwContents_nativeOnDraw调用参数nativeAwContents描述的一个Native层AwContents对象的成员函数OnDraw绘制网页的UI,如下所示:

bool AwContents::OnDraw(JNIEnv* env,
                        jobject obj,
                        jobject canvas,
                        jboolean is_hardware_accelerated,
                        jint scroll_x,
                        jint scroll_y,
                        jint visible_left,
                        jint visible_top,
                        jint visible_right,
                        jint visible_bottom) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  if (is_hardware_accelerated)
    InitializeHardwareDrawIfNeeded();
  return browser_view_renderer_.OnDraw(
      canvas,
      is_hardware_accelerated,
      gfx::Vector2d(scroll_x, scroll_y),
      gfx::Rect(visible_left,
                visible_top,
                visible_right - visible_left,
                visible_bottom - visible_top));
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       参数is_hardware_accelerated表示App的UI是否采用硬件加速方式绘制。如果是的话,那么Chromium也会使用硬件加速方式绘制网页的UI。在这种情况下,AwContents类的成员函数OnDraw首先会调用另外一个成员函数InitializeHardwareDrawIfNeeded检查是否需要为Chromium初始化一个硬件加速渲染环境。这个硬件加速渲染环境初始化完成后,AwContents类的成员函数OnDraw才会调用成员变量browser_view_renderer_描述的一个BrowserViewRenderer对象的成员函数OnDraw绘制网页的UI。

       AwContents类的成员函数InitializeHardwareDrawIfNeeded在为Chromium初始化硬件加速渲染环境的过程中,就会创建一个DeferredGpuCommandService服务,如下所示:

void AwContents::InitializeHardwareDrawIfNeeded() {
  GLViewRendererManager* manager = GLViewRendererManager::GetInstance();

  base::AutoLock lock(render_thread_lock_);
  if (renderer_manager_key_ == manager->NullKey()) {
    renderer_manager_key_ = manager->PushBack(&shared_renderer_state_);
    DeferredGpuCommandService::SetInstance();
  }
}
       这个函数定义在文件external/chromium_org/android_webview/native/aw_contents.cc中。

       在Chromium渲染引擎中,存在一个GLViewRendererManager单例对象。这个GLViewRendererManager单例对象可以通过调用GLViewRendererManager类的静态成员函数GetInstance获得,它用来记录当前有哪些WebView是采用硬件加速方式绘制的。

       AwContents类的成员函数InitializeHardwareDrawIfNeeded会检查成员变量renderer_manager_key_的值是否等于一个Null Key。如果等于的话,那么就说明当前正在绘制的WebView还没有初始化过硬件加速渲染环境。这时候AwContents类的成员函数InitializeHardwareDrawIfNeeded就会将成员变量shared_renderer_state_描述的一个SharedRendererState对象添加到上述GLViewRendererManager单例对象中。添加之后,会获得一个Key。这个Key就保存在AwContents类的成员变量renderer_manager_key_。同时,DeferredGpuCommandService类的静态成员函数SetInstance会被调用,用来创建一个DeferredGpuCommandService服务。这时候就表示当前正在绘制的WebView的硬件加速渲染环境初始化好了。

        接下来我们继续分析DeferredGpuCommandService类的静态成员函数SetInstance会创建ferredGpuCommandService服务的过程,如下所示:

base::LazyInstance<scoped_refptr<DeferredGpuCommandService> >
    g_service = LAZY_INSTANCE_INITIALIZER;

......

void DeferredGpuCommandService::SetInstance() {
  if (!g_service.Get()) {
    g_service.Get() = new DeferredGpuCommandService;
    content::SynchronousCompositor::SetGpuService(g_service.Get());
  }
}

......

DeferredGpuCommandService* DeferredGpuCommandService::GetInstance() {
  DCHECK(g_service.Get().get());
  return g_service.Get().get();
}
       这个函数定义在文件external/chromium_org/android_webview/browser/deferred_gpu_command_service.cc中。

       DeferredGpuCommandService类的静态成员函数SetInstance检查全局变量g_service是否已经指向了一个DeferredGpuCommandService对象。如果还没有指向,那么就会创建一个DeferredGpuCommandService对象让它指向。创建出来的DeferredGpuCommandService对象描述的就是一个DeferredGpuCommandService服务。这个DeferredGpuCommandService服务可以通过调用DeferredGpuCommandService类的静态成员函数GetInstance获得。

       DeferredGpuCommandService类的静态成员函数SetInstance创建出来的DeferredGpuCommandService服务会被设置为Android WebView绘制网页所使用的Synchronous Compositor的GPU服务,这是通过调用SynchronousCompositor类的静态成员函数SetGpuService实现的,如下所示:

base::LazyInstance<synchronouscompositorfactoryimpl>::Leaky g_factory =
    LAZY_INSTANCE_INITIALIZER;

......

void SynchronousCompositor::SetGpuService(
    scoped_refptr<gpu::InProcessCommandBuffer::Service> service) {
  g_factory.Get().SetDeferredGpuService(service);
}

       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_impl.cc中。

       全局变量g_factory描述的是一个SynchronousCompositorFactoryImpl对象,SynchronousCompositor类的静态成员函数SetGpuService会将参数service描述的一个DeferredGpuCommandService服务保存在它内部。这个通过调用它的成员函数SetDeferredGpuService实现的,如下所示

void SynchronousCompositorFactoryImpl::SetDeferredGpuService(
    scoped_refptr<gpu::InProcessCommandBuffer::Service> service) {
  DCHECK(!service_);
  service_ = service;
}
       这个函数定义在文件external/chromium_org/content/browser/android/in_process/synchronous_compositor_factory_impl.cc中。

       SynchronousCompositorFactoryImpl类的成员函数SetDeferredGpuService将参数service描述的一个DeferredGpuCommandService服务保存在成员变量service_。

       这一步执行完成后,Chromium渲染引擎的Render端以后就会通过保存在SynchronousCompositorFactoryImpl类的成员变量service_中的DeferredGpuCommandService服务来执行GPU命令。这一点我们在接下来一篇文章中再详细分析。

       至此,我们就分析完成了Android WebView启动Chromium渲染引擎的过程,主要就是启动的Chromium渲染引擎的Browser端和Render端。其中,Browser端对应的就是App的UI线程,而Render端对应的是一个类型为In-Process的Render线程。

       由于Android WebView要求Chromium渲染引擎使用App的Render Thread执行GPU命令,因此Chromium渲染引擎就不会创建自己的GPU端。不过,它会创建一个DeferredGpuCommandService服务,用来将Chromium渲染引擎发出的GPU命令交给App的Render Thread执行。在接下来一篇文章中,我们就详细分析Chromium渲染引擎执行GPU命令的过程。敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/12/5 1:02:47 原文链接
阅读:34273 评论:6 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>