精华内容
下载资源
问答
  • 《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式没有。张无忌说记住了一半。...目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较

    《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式没有。张无忌说记住了一半。张三丰又慢吞吞使了一遍,问他记住多少,张无忌说只记得几招了。张三丰最后又示范了一遍,张无忌想了想说,这次全忘光了。张三丰很满意,于是放心让张无忌与八臂神剑去比试。

    首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较真。写的时候花了好几天,几次想放弃,想放弃的原因不是我自己没有弄清楚,而是觉得自己叙事脉络已经紊乱了,感觉无法让读者整明白,怕把读者带到沟里面去,怕自己让人觉得罗嗦废话。但最后,我决定还是坚持下去,因为在反复纠结 –> 不甘 –> 探索 –> 论证 –> 质疑的过程循环中,我完成了对自己的升华,弄明白长久以来的一些困惑。所以,此文最大的目的是给自己作为一些学习记录,如果有幸帮助你解决一些困惑,那么我心宽慰。如果有错的地方,也欢迎指出批评。

    如果你有这样的困扰:
    1. 一个 View 的 parent 一定是 ViewGroup 吗?

    1. Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。

    2. Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。

    3. 在 xml 中设置一个 View 的属性 layout_width 为 wrap_content 或者 match_parent 而不是具体数值 50dp 时,为什么 view 也有正常的尺寸。

    4. 你或多或者知道 Android 测量时的 3 种布局模式:MeasureSpec.EXACTLY、Measure.AT_MOST、Measure.UNSPECIFIED。但你不大能够把握它们。

    5. 你不但对自定义 View 没有问题,对于自定义 ViewGroup 也不在话下,你明白 Android 给出的 3 种测量模式的含义,但是你还是没有来得及去思考,3 种测量模式本身是什么。

    6. 你也许没有想过 Activity 最外层的 View 是什么。

    7. 你也许知道 Activity 最外层的 View 叫做 DecorView。明白它与 PhoneWindow 及 Activity.setContentView() 的联系。但你不知道谁对 DecorView 进行了尺寸测量。

    好了,文章正式开始。请深吸一口气,take it easy!

    无法忽视的自定义 View 话题

    Android 应用层开发绕不开自定义 View 这个话题,在 Android 中官方称之为 Widget,所以本文中的 View 其实与 Widget也就一个意思。虽然现在 Github 上有形形色色的开源库供大家使用,但是作为一名有想法的开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,如果现有的 Widget 无法完成任务,那么我们就应该想到要自定义一个 View 了。
    我们或多或少知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
    - 测量 onMeasure()
    - 布局 onLayout()
    - 绘制 onDraw()

    没有办法说这三个阶段,那个阶段最重要,只是相对而言,测量阶段对于大多开发者而言难度相对其它两个要大,处理的细节也要多得多,自定义一个 View,正确的测量是第一步,正因为如此今天本文的主题就是讨论 View 中的测量机制和细节。

    测量 View 就是测量一个矩形

    得益于人们的想象力,Android 系统平台上出现了各种各样的 View。有 Button、TextView、ListView 等系统自带的组件,也有更多开发者自定义的 View。
    这里写图片描述

    上面是 Android 系统自带的 Widget 表现,它们用来完成不同功能的交互与效果展示,但对于开发者而言,上面的界面还有这样的一面。
    这里写图片描述

    透过另一个视角来观察,所有的 Widget

    世界万物都有某些运行的规则,或者是突破不了的樊篱。对于一个 View 而言,它本质上就是一个矩形,一块四方的区域,铺开一张画布,然后利用所有的资源,在现有的规则之下天马行空。
    这里写图片描述

    因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!

    既然是矩形,那么它肯定有明确的宽高和位置坐标。宽高是在测量阶段得出,然后在布局阶段,根据实际需要确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,它是画家,这个我们很放心。

    打个比方,政府做城市规划时,房地产商们告诉政府他们希望的用地面积,政府综合政策和用地面积的实际情况,给地产商划分土地面积,地图上就是一个个圈圈。地产商们拿到明确的地域范围信息后,在规定好的区域建造自己的高楼或者大厦。而自定义 View 就是拿到这个类似政府规划的区域范围参数,只不过现实世界中,政府规划给地产商的土地不一定是四四方方的矩形,但是在 Android 中 View 拿到的区域一定是矩形。
    这里写图片描述

    好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。

    View 设置尺寸的基本方法

    接下来的过程,我将会用一系列比较细致的实验来说明问题,觉得罗嗦无聊的同学可以直接跳过这一小节。
    我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。

    <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="test"/>

    这样屏幕上就出现了一个按钮。
    这里写图片描述
    我们再把宽高固定。

    <Button
            android:layout_width="200dp"
            android:layout_height="50dp"
            android:text="test"/>

    这里写图片描述
    再换一种情况,将按钮的宽度由父容器决定

    <Button
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="test"/>
    

    这里写图片描述

    上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。

    1. match_parent 代表这个维度上的值与父窗口一样
    2. wrap_content 表示这个维度上的值由 View 本身的内容所决定
    3. 具体数值如 5dp 表示这个维度上 View 给出了精确的值。

    实验1

    我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。

    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>
    

    这里写图片描述
    可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。

    实验2

    <RelativeLayout
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>

    这里写图片描述

    实验3

    <RelativeLayout
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>

    这里写图片描述

    实验4

    <RelativeLayout
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:background="#ff0000">
        <Button
            android:layout_width="1000dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    </RelativeLayout>

    这里写图片描述

    似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园问光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。

    Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。

    山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。

    那么,它们的协议是什么?

    View 和 ViewGroup 之间的测量协议 MeasureSpec

    我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。

    public class TestView extends View {
        public TestView(Context context) {
            super(context);
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
    

    onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。

    它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。

    MeasureSpec

    MeasureSpec 是 View.java 中一个静态类

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
    
        public static final int EXACTLY     = 1 << MODE_SHIFT;
    
    
        public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
    
      ......
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
    
    
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
    
        ......
    }
    

    MeasureSpec 的代码并不是很多,它有最重要的三个静态常量和三个最重要的静态方法。

    MeasureSpec.UNSPECIFIED
    MeasureSpec.EXACTLY
    MeasureSpec.AT_MOST
    
    MeasureSpec.makeMeasureSpec()
    MeasureSpec.getMode()
    MeasureSpec.getSize()

    MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。

    这里写图片描述

    通过 makeMeasureSpec() 方法将 Mode 和 Size 组合成一个 measureSpec 数值。
    而通过 getMode() 和 getSize() 却可以逆向地将一个 measureSpec 数值解析出它的 Mode 和 Size。

    下面讲解 MeasureSpec 的 3 种测量模式。

    MeasureSpec.UNSPECIFIED

    此种模式表示无限制,子元素告诉父容器它希望它的宽高想要多大就要多大,你不要限制我。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。

    MeasureSpec.EXACTLY

    此模式说明可以给子元素一个精确的数值。

    
    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="test"/>
    

    当 layout_width 或者 layout_height 的取值为 match_parent 或者 明确的数值如 100dp 时,表明这个维度上的测量模式就是 MeasureSpec.EXACTLY。为什么 match_parent 也有精确的值呢?我们可以合理推断一下,子 View 希望和 父 ViewGroup 一样的宽或者高,对于一个 ViewGroup 而言它显然是可以决定自己的宽高的,所以当它的子 View 提出 match_parent 的要求时,它就可以将自己的宽高值设置下去。

    MeasureSpec.AT_MOST

    此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是你不能超过我能提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。
    当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。

    了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。

    自定义 View

    我的目标是定义一个文本框,中间显示黑色文字,背景色为红色。
    这里写图片描述

    我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
    attrs.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="TestView">
            <attr name="android:text" />
            <attr name="android:textSize" />
        </declare-styleable>
    </resources>

    TestView.java

    public class TestView extends View {
    
        private  int mTextSize;
        TextPaint mPaint;
        private String mText;
    
        public TestView(Context context) {
            this(context,null);
        }
    
        public TestView(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
            mText = ta.getString(R.styleable.TestView_android_text);
            mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
    
            ta.recycle();
    
            mPaint = new TextPaint();
            mPaint.setColor(Color.BLACK);
            mPaint.setTextSize(mTextSize);
            mPaint.setTextAlign(Paint.Align.CENTER);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
            int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
    
            canvas.drawColor(Color.RED);
            if (TextUtils.isEmpty(mText)) {
                return;
            }
            canvas.drawText(mText,cx,cy,mPaint);
    
        }
    }
    

    布局文件

    <?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"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.frank.measuredemo.MainActivity">
    
        <com.frank.measuredemo.TestView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:text="test"/>
    
    </RelativeLayout>
    

    效果
    这里写图片描述

    我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。

    <com.frank.measuredemo.TestView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="test"/>
    

    将 layout_width 的值改为 match_parent,所以它的宽是由父类决定,但同样它也正常。
    这里写图片描述

    我们已经知道,上面的两种情况其实就是对应 MeasureSpec.EXACTLY 这种测量模式,在这种模式下 TestView 本身不需要进行处理。

    那么有人会问,如果 layout_width 或者 layout_height 的值为 wrap_content 的话,那么会怎么样呢?

    我们继续测试观察。

    <com.frank.measuredemo.TestView
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:text="test"/>
    

    这里写图片描述
    效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。

    <com.frank.measuredemo.TestView
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="test"/>
    

    这里写图片描述

    宽度正常,高度却和 ViewGroup 一样了。
    再看一种情况

    <com.frank.measuredemo.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>
    

    这里写图片描述

    这次可以看到,宽高都和 ViewGroup 一致了。

    但是,这不是我想要的啊!

    wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。

    TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。
    我们的思路可以如下:
    1. 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
    2. 对于 MeasureSpec.AT_MOST 模式,我们要根据自己的内容计算宽高,但是数值不得超过 ViewGroup 给出的建议值。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
            int resultW = widthSize;
            int resultH = heightSize;
    
            int contentW = 0;
            int contentH = 0;
    
            /**重点处理 AT_MOST 模式,TestView 自主决定数值大小,但不能超过 ViewGroup 给出的
             * 建议数值
             * */
            if ( widthMode == MeasureSpec.AT_MOST ) {
    
                if (!TextUtils.isEmpty(mText)){
                    contentW = (int) mPaint.measureText(mText);
                    contentW += getPaddingLeft() + getPaddingRight();
                    resultW = contentW < widthSize ? contentW : widthSize;
                }
    
            }
    
            if ( heightMode == MeasureSpec.AT_MOST ) {
                if (!TextUtils.isEmpty(mText)){
                    contentH = mTextSize;
                    contentH += getPaddingTop() + getPaddingBottom();
                    resultH = contentH < widthSize ? contentH : heightSize;
                }
            }
    
            //一定要设置这个函数,不然会报错
            setMeasuredDimension(resultW,resultH);
    
    }
    
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    
        int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
    
        metrics = mPaint.getFontMetrics();
        cy += metrics.descent;
    
        canvas.drawColor(Color.RED);
        if (TextUtils.isEmpty(mText)) {
            return;
        }
        canvas.drawText(mText,cx,cy,mPaint);
    
    }
    

    代码并不难,我们可以做验证。

    <com.frank.measuredemo.TestView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:text="test"/>
    

    这里写图片描述
    可以看到这才是我们想要的效果。它现在完成了对 MeasureSpec.AT_MOST 模式的适配。我们再验证一下另外一种情况

    <com.frank.measuredemo.TestView
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="48sp"
        android:text="test"/>
    
    

    这里写图片描述
    在 MeasureSpec.EXACTLY 模式下同样没有问题。

    现在,我们已经掌握了自定义 View 的测量方法,其实也很简单的嘛。

    但是,还没有完。我们验证的刚刚是自定义 View,对于 ViewGroup 的情况是有些许不同的。

    View 和 ViewGroup,鸡生蛋,蛋生鸡的关系

    ViewGroup 是 View 的子类,但是 ViewGroup 的使命却是装载和组织 View。这好比是母鸡是鸡,母鸡下蛋是为了孵化小鸡,小鸡长大后如果是母鸡又下蛋,那么到底是蛋生鸡还是鸡生蛋?

    这里写图片描述

    自定义 View 的测量,我们已经掌握了,那现在我们编码来测试自定义 ViewGroup 时的测量变现。
    假设我们要制定一个 ViewGroup,我们就给它起一个名字叫 TestViewGroup 好了,它里面的子元素按照对角线铺设,如下图:
    这里写图片描述

    前面说过 ViewGroup 本质上也是一个 View,只不过它多了布局子元素的义务。既然是 View 的话,那么自定义一个 ViewGroup 也需要从测量开始,问题的关键是如何准确地得到这个 ViewGroup 尺寸信息?

    我们还是需要仔细讨论。

    1. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.EXACTLY 时,这时候的尺寸就可以按照父容器传递过来的建议尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一个 View。
    2. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,这就需要 TestViewGroup 自己计算这个维度上的尺寸数值。就上面给出的信息而言,TestViewGroup 的尺寸非常简单,那就是在一个维度上用自身 padding + 各个子元素的尺寸(包含子元素的宽高+子元素设置的 marging )得到一个可能的尺寸数值。然后用这个尺寸数值与 TestViewGroup 的父容器给出的建议 Size 进行比较,最终结果取最较小值。
    3. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,因为要计算子元素的尺寸,所以如何准确得到子元素的尺寸也是至关重要的事情。好在 Android 提供了现成的 API。
    4. 当 TestViewGroup 测量成功后,就需要布局了。自定义 View 基本上不要处理这一块,但是自定义 ViewGroup,这一部分却不可缺少。但本篇文章不是讨论布局技巧的,只是告诉大家布局其实相对而言更简单一点,无非是确定好子元素的坐标然后进行布局。

    接下来,我们就可以具体编码了。

    public class TestViewGroup extends ViewGroup {
    
    
        public TestViewGroup(Context context) {
            this(context,null);
        }
    
        public TestViewGroup(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        }
    
        public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
        }
    
    
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            //只关心子元素的 margin 信息,所以这里用 MarginLayoutParams
            return new MarginLayoutParams(getContext(),attrs);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
            int resultW = widthSize;
            int resultH = heightSize;
    
            /**计算尺寸的时候要将自身的 padding 考虑进去*/
            int contentW = getPaddingLeft() + getPaddingRight();
            int contentH = getPaddingTop() + getPaddingBottom();
    
            /**对子元素进行尺寸的测量,这一步必不可少*/
            measureChildren(widthMeasureSpec,heightMeasureSpec);
    
            MarginLayoutParams layoutParams = null;
    
            for ( int i = 0;i < getChildCount();i++ ) {
                View child = getChildAt(i);
                layoutParams = (MarginLayoutParams) child.getLayoutParams();
    
                //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
                if ( child.getVisibility() == View.GONE ) {
                    continue;
                }
    
                contentW += child.getMeasuredWidth()
                        + layoutParams.leftMargin + layoutParams.rightMargin;
    
                contentH += child.getMeasuredHeight()
                        + layoutParams.topMargin + layoutParams.bottomMargin;
            }
    
            /**重点处理 AT_MOST 模式,TestViewGroup 通过子元素的尺寸自主决定数值大小,但不能超过
             *  ViewGroup 给出的建议数值
             * */
            if ( widthMode == MeasureSpec.AT_MOST ) {
                resultW = contentW < widthSize ? contentW : widthSize;
            }
    
            if ( heightMode == MeasureSpec.AT_MOST ) {
                resultH = contentH < heightSize ? contentH : heightSize;
            }
    
            //一定要设置这个函数,不然会报错
            setMeasuredDimension(resultW,resultH);
    
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
    
            int topStart = getPaddingTop();
            int leftStart = getPaddingLeft();
            int childW = 0;
            int childH = 0;
            MarginLayoutParams layoutParams = null;
            for ( int i = 0;i < getChildCount();i++ ) {
                View child = getChildAt(i);
                layoutParams = (MarginLayoutParams) child.getLayoutParams();
    
                //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
                if ( child.getVisibility() == View.GONE ) {
                    continue;
                }
    
                childW = child.getMeasuredWidth();
                childH = child.getMeasuredHeight();
    
                leftStart += layoutParams.leftMargin;
                topStart += layoutParams.topMargin;
    
    
                child.layout(leftStart,topStart, leftStart + childW, topStart + childH);
    
                leftStart += childW + layoutParams.rightMargin;
                topStart += childH + layoutParams.bottomMargin;
            }
    
        }
    
    }

    然后我们将之添加进 xml 布局文件中进行测试。

    <com.frank.measuredemo.TestViewGroup
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        <com.frank.measuredemo.TestView
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:text="test"/>
        <TextView
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:background="#00ff40"
            android:text="test"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            android:text="test"/>
    </com.frank.measuredemo.TestViewGroup>

    那么实际效果如何呢?
    这里写图片描述

    再试验一下给 TestViewGroup 加上固定宽高。

    <com.frank.measuredemo.TestViewGroup
        android:layout_width="350dp"
        android:layout_height="600dp"
        android:background="#c3c3c3">
        <com.frank.measuredemo.TestView
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:text="test"/>
        <TextView
            android:layout_width="120dp"
            android:layout_height="50dp"
            android:paddingLeft="2dp"
            android:paddingRight="2dp"
            android:paddingTop="2dp"
            android:textSize="24sp"
            android:background="#00ff40"
            android:text="test"/>
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            android:text="test"/>
    </com.frank.measuredemo.TestViewGroup>
    

    结果如下:
    这里写图片描述

    自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。

    但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。

    TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义套路。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 ViewGroup。

    我个人觉得,尝试自己动手去实现一个流式标签控件,对于提高自定义 ViewGroup 的能力是有很大的提高,因为只有在自己实践的时候,你都会思考,在思考和实验的过程你才会深刻的理解测量机制的用途。

    不过自定义一个流式标签控件是另外一个话题了,也许我会另外开一篇来讲解,不过我希望大家亲自动手去实现它。下面是我自定义 ViewGroup 的一个实例截图。
    这里写图片描述


    洋洋洒洒写了这么多的内容,其实基本上已经完结了,已经不耐烦的同学可以直接跳转到后面的总结。但是,对于有钻研精神的同学来讲,其实还不够。还没有完。

    问题1:到底是谁在测量 View ?

    问题2:到底是什么时候需要测量 View ?

    针对问题 1:
    我们在自定义 TestViewGroup 的时候,在 onMeasure() 方法中,通过了一个 API 对子元素进行了测量,这个 API 就是 measureChildren()。这个方法进行了什么样的处理呢?我们可以去看看。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
    
    protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
    
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    代码简短易懂,分别调用 child 的 measure() 方法。值得注意的是,传递给 child 的测量规格已经发生了变化,比如 widthMeasureSpec 变成了 childWidthMeasureSpec。原因是这两行代码:

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    

    硬着头皮再去看 getChildMeasureSpec() 方法的实现。

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            // 父类本身就是 EXACTLY 模式下
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    // 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    // child 希望尺寸和 parent 一样大,那么满足它
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    // child 希望由自己决定自己的尺寸,但是有个前提是它不能大于 parent 本身给的建议值。
                    // 并且需要设置它的测量模式为 AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            // 父类本身就是 AT_MOST 模式下,对于 parent 本身而言,它也有个最大的尺寸约束
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    // 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    // child 希望尺寸和 parent 一样大,那么满足它,但是 parent 本身有限制,所以也
                    // 需要给 child 加一个限制,这个限制就是将 child 模式设置为 AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    // child 希望由自己决定自己的尺寸,但是有个前提是它不能大于 parent 本身给的建议值。
                    // 并且需要设置它的测量模式为 AT_MOST
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            // 父类本身就是 UNSPECIFIED 模式下,对于 parent 本身而言,它的期望值是想要多大就多大,让 parent 的 parent不要限制它。
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    //如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    // child 希望尺寸和 parent 一样大,那么满足它,但是 parent 本身也需要计算,所以只能设置为0,并且
                    // 将child的测量模式设置为 UNSPECIFIED
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    // child 希望尺寸由自己决定,一般这个时候,parent会给它一个 Size 作为最大值限制,
                    // 但是 parent 本身也需要计算,所以只能设置为0,并且没有给child最大值的设定
                    // 将child的测量模式设置为 UNSPECIFIED
                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            //noinspection ResourceType
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    
    

    代码很清楚,很多事情真相大白了。解释了很多东西,包括:

    1. 为什么 layout_width 或者 layout_height 设置了精确的数值时,它的测量模式为 MeasureSpec.EXACTLY。

    2. 为什么 layout_width 或者 layout_height 设置为 match_parent 时,不一定保证它的测量模式为 MeasureSpec.EXACTLY。它也有可能是 MeasureSpec.AT_MOST,也有可能是 MeasureSpec.UNSPECIFIED。

    3. 为什么 layout_width 或者 layout_height 设置为 wrap_content 时,它最可能的测量模式为 MeasureSpec.AT_MOST,但也有可能是 MeasureSpec.UNSPECIFIED。

    我们继续向前,ViewGroup 的 measureChild() 方法最终会调用 View.measure() 方法。我们进一步跟踪。

    
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
            final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    
            // Optimize layout by avoiding an extra EXACTLY pass when the view is
            // already measured as the correct size. In API 23 and below, this
            // extra pass is required to make LinearLayout re-distribute weight.
            final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                    || heightMeasureSpec != mOldHeightMeasureSpec;
            final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                    && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
            final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                    && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
            final boolean needsLayout = specChanged
                    && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    
            if (forceLayout || needsLayout) {
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
    
    
                int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
    
    
                    // measure ourselves, this should set the measured dimension flag back
                    // onMeasure 在此调用
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } 
    
                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("View with id " + getId() + ": "
                            + getClass().getName() + "#onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }
    
                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
    
        }
    

    代码比较长,删掉了一些与主题无关的代码,我们可以看到,当一个 view 的 measure() 方法调用时,如果它此次的 measureSpec 与 旧的不相同或者是它需要重新布局也就是 requestLayout() 方法被调用时,它会触发 onMeasure() 方法。值得注意的是最后它会检查变量 mPrivateFlags 的一个位 PFLAG_MEASURED_DIMENSION_SET,如果这一位没有置为 1 的话,会抛出异常。而这一位在 onMeasure() 方法中一定要调用。

    好了,鸡生蛋,蛋生鸡结论似乎有个结果了。是 ViewGroup 中的 onMeasure() 调用了 View.measure() 而 View.measure() 调用了 View.onMeasure()。

    这里写图片描述

    于是,我们终于明白了。

    但是呢,还能更深入一点吗?

    Activity 中的道,最顶层的那个 View?

    道生一,一生二,二生三,三生万物,万物负阴而抱阳,冲气以为和。– 《道德经》

    我们已经知道,不管是对于 View 还是 ViewGroup 而言,测量的起始是 measure() 方法,沿着控件树一路遍历下去。那么,对于 Android 一个 Activity 而言,它的顶级 View 或者顶级 ViewGroup 是哪一个呢?

    从 setContentView 说起

    我们知道给 Activity 布局的时候,在 onCreate() 中设置 setContentView() 的资源文件就是我们普通开发者所能想到的比较顶层的 View 了。比如在 activity_main.xml 中设置一个 RelativeLayout,那么这个 RelativeLayout 就是 Activity 最顶层的 View 吗?谁调用它的 measure() 方法触发整个控件树的测量?

    
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initActionBar();
    }
    
    public Window getWindow() {
        return mWindow;
    }
    
    /**
     * Abstract base class for a top-level window look and behavior policy.  An
     * instance of this class should be used as the top-level view added to the
     * window manager. It provides standard UI policies such as a background, title
     * area, default key processing, etc.
     *
     * <p>The only existing implementation of this abstract class is
     * android.policy.PhoneWindow, which you should instantiate when needing a
     * Window.  Eventually that class will be refactored and a factory method
     * added for creating Window instances without knowing about a particular
     * implementation.
     */
    public abstract class Window {
    
    }
    
    public class PhoneWindow extends Window implements MenuBuilder.Callback {
    
    }
    

    可以看到,调用 Activity.setContentView() 其实就是调用 PhoneWindow.setContentView()。

    PhoneWindow.java

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    

    注意,在上面代码中显示,通过 setContentView 传递进来的 view 被添加到了一个 mContentParent 变量上了,所以可以回答上面的问题,通过 setContentView() 中传递的 View 并不是 Activity 最顶层的 View。我们再来看看 mContentParent。

    它只是一个 ViewGroup。我们再把焦点聚集到 installDecor() 这个函数上面。

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
    
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
    
            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();
    
            mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
            if (mTitleView != null) {
                mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
                if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                    View titleContainer = findViewById(com.android.internal.R.id.title_container);
                    if (titleContainer != null) {
                        titleContainer.setVisibility(View.GONE);
                    } else {
                        mTitleView.setVisibility(View.GONE);
                    }
                    if (mContentParent instanceof FrameLayout) {
                        ((FrameLayout)mContentParent).setForeground(null);
                    }
                } else {
                    mTitleView.setText(mTitle);
                }
            } else {
                mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
    
    
            }
        }
    }
    

    代码很长,我删除了一些与主题无关的代码。这个方法体内引出了一个 mDecor 变量,它通过 generateDecor() 方法创建。DecorView 是 PhoneWindow 定义的一个内部类,实际上是一个 FrameLayout。

    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    
    }
    

    我们回到 generate() 方法

    protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }
    

    DecorView 怎么创建的我们已经知晓,现在看看 mContentParent 创建方法 generateLayout()。它传递进了一个 DecorView,所以它与 mDecorView 肯定有某种关系。

    protected ViewGroup generateLayout(DecorView decor) {
            // Apply data from current theme.
    
    
    
    
        WindowManager.LayoutParams params = getAttributes();
    
    
    
        // Inflate the window decor.
    
    
    
        // Embedded, so no decoration is needed.
        layoutResource = com.android.internal.R.layout.screen_simple;
        // System.out.println("Simple!");
    
    
        mDecor.startChanging();
    
        View in = mLayoutInflater.inflate(layoutResource, null);
    
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
    
    
        mDecor.finishChanging();
    
        return contentParent;
    }
    
    

    原代码很长,我删除了一些繁琐的代码,整个流程变得很清晰,这个方法内 inflate 了一个 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是这个被添加进去的 view 中。
    这个 xml 文件是 com.android.internal.R.layout.screen_simple,我们可以从 SDK 包中找出它来。
    这里写图片描述

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    就是一个 LinearLayout ,方向垂直。2 个元素,一个是 actionbar,一个是 content。并且 ViewStub 导致 actionbar 需要的时候才会进行加载。

    总之由以上信息,我们可以得到 Activity 有一个 PhoneWindow 对象,PhoneWindow 中有一个 DecorView,DecorView 内部有一个 LinearLayout,LinearLayout 中存在 id 为 android:id/content 的布局 mContentParent。mContentParent 用来加载 Activity 通过 setContentView 传递进来的 View。所以整个结构呼之欲出。

    注意:因为代码有删简,实际上 LinearLayout 由两部分组成,下面的是 Content 无疑,上面的部分不一定是 ActionBar,也可能是 title,不过这不影响我们,我们只需要记住 content 就好了。

    这里写图片描述

    DecorView 才是 Activity 中整个控件树的根。

    谁测绘了顶级 View ?

    既然 DecorView 是整个测绘的发起点,那么谁对它进行了测绘?谁调用了它的 measure() 方法,从而导致整个控件树自上至下的尺寸测量?

    我们平常开发知道调用一个 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?

    我们再回到 PhoneWindow 的 setContentView() 中来。

    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mContentParent.addView(view, params);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
    

    我们看看 mContentParent.addView(view, params) 的时候发生了什么。

    public void addView(View child, int index, LayoutParams params) {
        if (DBG) {
            System.out.println(this + " addView");
        }
    
        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }
    

    它调用了 requestLayout(),正好要探索它,所以说 setContentView 之后,控件树会进行一次测绘。不过这里是结论,其过程需要我们来验证。

    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
    
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // Only trigger request-during-layout logic if this is the view requesting it,
            // not the views in its parent hierarchy
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
    
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
    
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }
    

    View 在 requestLayout() 中给 mPrivateFlags 添加了 2 个标记 PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED。还引出了另外一个对象 ViewRootImpl。然后调用 parent 的 requestLayout()。

    我们在想最顶层的 View 是 DecorView,但是它有 parent 吗?如果有的话,那是什么?我在此困扰了好久,觉得 DecorView 没有父容器,所以我把自己追踪的线索弄丢了,好在后来找到了。现在可以提示大家一下。
    View 中的 mParent 其实是一个接口:ViewParent。

    /**
     * Defines the responsibilities for a class that will be a parent of a View.
     * This is the API that a view sees when it wants to interact with its parent.
     * 
     */
    public interface ViewParent {
        /**
         * Called when something has changed which has invalidated the layout of a
         * child of this view parent. This will schedule a layout pass of the view
         * tree.
         */
        public void requestLayout();
    
       ......
    
    }
    

    我之前犯糊涂了,以为一个 View 的 parent 一定是 ViewGroup。其实 ViewGroup 只是 ViewParent 接口的实现类之一。

    public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    }
    

    我们再看看 ViewRootImpl 的源码。

    
    /**
     * The top of a view hierarchy, implementing the needed protocol between View
     * and the WindowManager.  This is for the most part an internal implementation
     * detail of {@link WindowManagerGlobal}.
     *
     * {@hide}
     */
    @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
    public final class ViewRootImpl implements ViewParent,
            View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    
    }

    可以看到 ViewRootImpl 也是 ViewParent 的实现类,所以说理论上它也可以成为 DecorView 的 parent。我们再注意到注释解释说 ViewRootImpl 是控件树的最顶端。更多的细节要我去查找 WindowManagerGlobal。好吧,那就找吧。

    
    /**
     * Provides low-level communication with the system window manager for
     * operations that are not associated with any particular context.
     *
     * This class is only used internally to implement global functions where
     * the caller already knows the display and relevant compatibility information
     * for the operation.  For most purposes, you should use {@link WindowManager} instead
     * since it is bound to a context.
     *
     * @see WindowManagerImpl
     * @hide
     */
    public final class WindowManagerGlobal {
    
    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            if (view == null) {
                throw new IllegalArgumentException("view must not be null");
            }
            if (display == null) {
                throw new IllegalArgumentException("display must not be null");
            }
            if (!(params instanceof WindowManager.LayoutParams)) {
                throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
            }
    
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
            if (parentWindow != null) {
                parentWindow.adjustLayoutParamsForSubWindow(wparams);
            }
    
            ViewRootImpl root;
            View panelParentView = null;
    
            synchronized (mLock) {
    
    
                root = new ViewRootImpl(view.getContext(), display);
    
                view.setLayoutParams(wparams);
    
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
    
            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                synchronized (mLock) {
                    final int index = findViewLocked(view, false);
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                }
                throw e;
            }
        }
    
    
    }

    代码不是很多,其中 addView() 方法引起了我的兴趣。
    在此,创建了 ViewRootImpl 对象。并且这个类的注释叫我去查看 WindowManagerImpl。

    感觉越陷越深了,没有办法,深吸一口气,继续向前。

    public final class WindowManagerImpl implements WindowManager {
        private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
        private final Display mDisplay;
        private final Window mParentWindow;
    
        public WindowManagerImpl(Display display) {
            this(display, null);
        }
    
    
        @Override
        public void addView(View view, ViewGroup.LayoutParams params) {
            mGlobal.addView(view, params, mDisplay, mParentWindow);
        }
    
    
    }
    

    原来 WindowManagerImpl 的内部有一个 WindowManagerGlobal 对象,WindowManagerGlobal 代理 WindowManagerImpl ,并且 WindowManagerImpl 是 WindowManager 的实现类。
    也就是我们平常编写悬浮窗经常用到的。

    WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    

    已经走到这一步了,貌似线索已经断了,不过,需要明白一点是 Activity 也是一个 Window,所以创建 Activity 的时候一定会通过 WindowManager addView。

    把焦点回到 Activity,里面还有个 PhoneWindow 值得我们挖掘。在 Activity 源码中搜索 window 被赋值的地方。结果找到了。

    final void attach(Context context, ActivityThread aThread,
                Instrumentation instr, IBinder token, int ident,
                Application application, Intent intent, ActivityInfo info,
                CharSequence title, Activity parent, String id,
                NonConfigurationInstances lastNonConfigurationInstances,
                Configuration config) {
            attachBaseContext(context);
    
            mFragments.attachActivity(this, mContainer, null);
    
            mWindow = PolicyManager.makeNewWindow(this);
            mWindow.setCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);
    
            mUiThread = Thread.currentThread();
    
            mMainThread = aThread;
            mInstrumentation = instr;
            mToken = token;
            mIdent = ident;
            mApplication = application;
            mIntent = intent;
            mComponent = intent.getComponent();
            mActivityInfo = info;
            mTitle = title;
            mParent = parent;
            mEmbeddedID = id;
            mLastNonConfigurationInstances = lastNonConfigurationInstances;
    
            mWindow.setWindowManager(
                    (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                    mToken, mComponent.flattenToString(),
                    (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            if (mParent != null) {
                mWindow.setContainer(mParent.getWindow());
            }
            mWindowManager = mWindow.getWindowManager();
            mCurrentConfig = config;
     }
    

    mWindow 通过 PolicyManager 创建。并且与 WindowManager建立了联系。

    public final class PolicyManager {
        private static final String POLICY_IMPL_CLASS_NAME =
            "com.android.internal.policy.impl.Policy";
    
        private static final IPolicy sPolicy;
    
        static {
            // Pull in the actual implementation of the policy at run-time
            try {
                Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
                sPolicy = (IPolicy)policyClass.newInstance();
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
            } catch (InstantiationException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(
                        POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
            }
        }
    
        // Cannot instantiate this class
        private PolicyManager() {}
    
        // The static methods to spawn new policy-specific objects
        public static Window makeNewWindow(Context context) {
            return sPolicy.makeNewWindow(context);
        }
    
    }
    

    通过反射创建了一个 Policy 对象,然后调用它的 makeNewWindow() 方法得到了一个 window() 对象。

    public class Policy implements IPolicy {
        private static final String TAG = "PhonePolicy";
    
        private static final String[] preload_classes = {
            "com.android.internal.policy.impl.PhoneLayoutInflater",
            "com.android.internal.policy.impl.PhoneWindow",
            "com.android.internal.policy.impl.PhoneWindow$1",
            "com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback",
            "com.android.internal.policy.impl.PhoneWindow$DecorView",
            "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
            "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
        };
    
        static {
            // For performance reasons, preload some policy specific classes when
            // the policy gets loaded.
            for (String s : preload_classes) {
                try {
                    Class.forName(s);
                } catch (ClassNotFoundException ex) {
                    Log.e(TAG, "Could not preload class for phone policy: " + s);
                }
            }
        }
    
        public Window makeNewWindow(Context context) {
            return new PhoneWindow(context);
        }
    
    
    }
    

    到此,我们终于知道了 Activity 中的 mWindow 就是一个 PhoneWindow。

    兜兜转转,我们似乎回到了原点,不过值得庆幸的是我们知道了 Activity 中 PhoneWindow 的由来。

    顶层的 View,由谁测绘?

    为了避免大家遗忘,我把问题又抛出来。这是我们的终极目标,明确了这个答案之后,本篇文章就结束了。

    不过,对于现在而言,目前掌握的信息,我们似乎无从得知,依照上面的思路步骤,我们无法找到这个问题的答案。此路不通,我们就需要想想其它办法了。我们要寻找的是,最开始的时候是谁调用了 DecorView.measure() 方法最终导致整个控件树的重绘。现在线索继了,我们只得再从 Activity 身上挖掘。

    说说 Activity 相关

    我之前阅读过 ActivityManager、WindowManager、PackageManager 的源码。我们大多最关心的是 Activity 的生命周期流程。

    是的,我们在 Activity 中的 onCreate() 方法中加载布局文件,但是却要在 onResume() 之后,图像才能显示出来。所以,我们可以从这里入手。

    Activity 的流程我只简单过一篇,因为是另外的主题了,并且这个主题很庞大繁琐,所以很多的细节我会选择忽略。不熟悉的同学可以自己翻阅资料。

    Activity 的创建

    Java 学习的时候,大家都知道要运行起来就要写一个类,然后包含一个 main() 方法,但是 Activity 没有让我们实现 main(),程序也照样跑起来了,这个估计是很多 Android 初学者比较困扰的地方。其实,Activity 是运行在 Android Framework 之上的,我们编写的程序其实是运行在 Google 工程师们弄好的程序之上,可以说是别人程序中的片断,这就是框架。Android 的应用运行的时候会创建一个包含 main() 方法的类,这个类叫 ActivityThread。一看名字就知道与 Activity 有关。
    我们再来引出另一个类 ActivityManagerService,它是系统最重要的服务之一,用来调度和处理应用层客户端传递过来的相关的请求。

    Activity 的 onResume() 由来

    Activity 第一次启动时,肯定是先执行 onCreate() 然后才执行 onResume() 方法的。

     private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    
    
           ·
        Activity a = performLaunchActivity(r, customIntent);
    
        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            Bundle oldState = r.state;
    
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);
    
        } else {
            // If there was an error, for any reason, tell the activity
            // manager to stop us.
            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(r.token, Activity.RESULT_CANCELED, null);
            } catch (RemoteException ex) {
                // Ignore
            }
        }
    }

    可以看到是先执行 performLaunchActivity() 再执行 handleResumeActivity() 的。performLauncherActivity() 中是创建 Activity 并调用 onCreate() 方法。感兴趣的同学自己去查阅代码。

    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
                boolean reallyResume) {
    
    
        ActivityClientRecord r = performResumeActivity(token, clearHide);
    
        if (r != null) {
            final Activity a = r.activity;
    
    
            if (r.window == null && !a.mFinished && willBeVisible) {
                // 重点在这一段代码
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
    
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
    
                    wm.addView(decor, l);
    
                }
            }
        }   
    }
    

    可能看到 ActivityThread 在 handleResumeActivity() 方法中调用了 WindowManager.addView() 方法,这个方法最终调用的就是本文前面部分已经讲到过的 WindowManagerGlobal.addView()。

    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
    
    
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    
        ViewRootImpl root;
        View panelParentView = null;
    
        synchronized (mLock) {
    
            root = new ViewRootImpl(view.getContext(), display);
    
            view.setLayoutParams(wparams);
    
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
    
        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }
    

    之前我们看到这一段代码的时候,不明白它的用途。现在可以算是明白了。Activity 创建后,onResume() 执行的时候,要将 DecorView 保存进 ViewRootImpl。

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
                if (mView == null) {
                    mView = view;
    
                    mAdded = true;
    
                    // Schedule the first layout -before- adding to the window
                    // manager, to make sure we do the relayout before receiving
                    // any other events from the system.
                    requestLayout();
    
    
                    view.assignParent(this);
    
                }
            }
    
    }
    

    我精简了代码,注意 ViewRootImpl 将自己设置成为了 DecorView 的 parent,这说明了一个问题:

    控件树中任何一个 View 调用 requestLayout() 方法,最终会调用 Activity 相关联的 ViewRootImpl 的 requestLayout() 方法,可以说 ViewRootImpl 操纵了整棵控件树,它的名字名符其实

    通过 setView() 方法 ViewRootImpl 将 DecorView 保存到 mView 变量中,并执行了 ViewRootImpl.requestLayout() 方法。

    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            scheduleConsumeBatchedInput();
        }
    }

    mChoreographer 是一个 Choreographer 对象,你可以简单把它当作一个 Handler 就好,用来编排 Android 界面的绘制。它最终会调用 doTraversal() 方法。

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
    
            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
    
        }
    }
    

    最终调用的是ViewRootImpl 的 performTraversals() 方法。这个代码非常非常非常的长。我只节选我们需要的部分,完整信息请参照源码。

    private void performTraversals() {
            // cache mView since it is used so much below...
            final View host = mView;
    
    
            if (host == null || !mAdded)
                return;
    
            mIsInTraversal = true;
    
    
            WindowManager.LayoutParams lp = mWindowAttributes;
    
            int desiredWindowWidth;
            int desiredWindowHeight;
    
    
    
            WindowManager.LayoutParams params = null;
            if (mWindowAttributesChanged) {
                mWindowAttributesChanged = false;
                surfaceChanged = true;
                params = lp;
            }
    
    
    
            if (mFirst) {
    
    
                if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                        || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
    
                } else {
                    DisplayMetrics packageMetrics =
                        mView.getContext().getResources().getDisplayMetrics();
                    desiredWindowWidth = packageMetrics.widthPixels;
                    desiredWindowHeight = packageMetrics.heightPixels;
                }
    
    
            } else {
    
            }
    
    
    
            boolean layoutRequested = mLayoutRequested && !mStopped;
            if (layoutRequested) {
    
                final Resources res = mView.getContext().getResources();
    
    
                // Ask host how big it wants to be
                windowSizeMayChange |= measureHierarchy(host, lp, res,
                        desiredWindowWidth, desiredWindowHeight);
            }   
    
        }
    

    上面是删除了很多代码的结果,主要为了说明 performTraversals() 在第一次测量的时候,将建议尺寸填写成了屏幕的宽高,然后调用了 measureHierarchy() 方法。

    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
                final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
            int childWidthMeasureSpec;
            int childHeightMeasureSpec;
            boolean windowSizeMayChange = false;
    
            if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
                    "Measuring " + host + " in display " + desiredWindowWidth
                    + "x" + desiredWindowHeight + "...");
    
            boolean goodMeasure = false;
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // On large screens, we don't want to allow dialogs to just
                // stretch to fill the entire width of the screen to display
                // one line of text.  First try doing the layout at a smaller
                // size to see if it will fit.
                final DisplayMetrics packageMetrics = res.getDisplayMetrics();
                res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
                int baseSize = 0;
                if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                    baseSize = (int)mTmpValue.getDimension(packageMetrics);
                }
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
                if (baseSize != 0 && desiredWindowWidth > baseSize) {
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                            + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        goodMeasure = true;
                    } else {
                        // Didn't fit in that size... try expanding a bit.
                        baseSize = (baseSize+desiredWindowWidth)/2;
                        if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
                                + baseSize);
                        childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                        if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                                + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                        if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                            if (DEBUG_DIALOG) Log.v(TAG, "Good!");
                            goodMeasure = true;
                        }
                    }
                }
            }
    
            if (!goodMeasure) {
                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                    windowSizeMayChange = true;
                }
            }
    
    
    
            return windowSizeMayChange;
        }

    根据 LayoutParam 取值不同,设置不同的测量模式。主要调用了 getRootMeasureSpec() 方法,然后调用 performMeasure() 方法。

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    可以看到当给 DecorView 设置的 LayoutParam 属性为 wrap_content 时,它的测量模式为 MeasureSpec.AT_MOST,其它的则为 MeasureSpec.EXACTLY,建议尺寸就是该方向上的屏幕宽高对应值。我们再看看 performMeasure() 方法。

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    
        try {
    
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    
    }
    

    一切水落日出,mView 就是 DecorView。ViewRootImpl 调用了 DecorView 的 measure() 方法,启动了整个控件树的测量。

    不过还可以细挖,那就是传递给 DecorView 的 LayoutParams 属性是什么,Activity 是全屏的,所以可以推断它是全屏的。但实际情况是不是这样呢?

    DecorView 的 LayoutParams 实际上是 ViewRootImpl 中的 mWindowAttributes
    ViewRootImpl.java

    final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
    

    WindowManager.java

     public static class LayoutParams extends ViewGroup.LayoutParams
                implements Parcelable {
    
        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }
    }
    

    mWindowAttributes 宽高测量模式都是 match_parent。所以 DecorView 最终测量的建议尺寸就是屏幕的宽高。

    好了,回顾一下,从我们知道 DecorView 是 Activity 最外层的 View 开始,一直在寻找是谁在测绘它,调用它的 measure() 方法。千回百转,一层层追踪下去,都把 Activity、ActivityThread 牵扯出来了。最终找出了源头 ViewRootImpl。

    总结

    如果你一直顺序阅读到这个地方,我要对你说声感谢,谢谢你的捧场。

    此文最终的目的不是为了让你困惑,如果因为篇幅过长,影响了你们的思维,我最后再做一点总结。

    1. View 的测量是从 measure() 方法开始到 onMeasure()。

    2. View 的 onMeasure() 中会接收到 parent 给出的测量规格建议。测量规格包含测量模式和建议尺寸。

    3. 测量模式包含 MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、和 MeasureSpec.UNSPECIFIED。

    4. View 复写 onMeasure() 方法时,一般不处理 MeasureSpec.UNSPECIFIED 测量模式,对于 MeasureSpec.EXACTLY 模式,直接设定 parent 给出的测试尺寸。需要特别处理的是 MeasureSpec.AT_MOST,它需要的是自主决定尺寸数值,但是不得大于 parent 给出的建议尺寸。

    5. View 复写 onMeasure() 方法最后要通过 setMeasuredDimension() 设置尺寸,不然会报错异常。

    6. ViewGroup 自定义比 View 稍难,因为 onMeasure() 方法中要根据子元素来决定自己的宽高,所以要测量子元素的尺寸,通过 measureChildren() 或者根据 measureChild() 方法,然后通过 getMeasureWidth() / getMeasureHeight() 获取子元素期望的尺寸。

    7. Activity 有一棵控件树,DecorView 是它最顶层的 View,但 Activity 相关联的 ViewRootImpl 对象操纵着它们。

    以上。

    展开全文
  • 一次网页请求背后的连接

    千次阅读 2016-12-07 10:55:39
    一次网页请求背后的连接 想成为一个优秀的前端,对互联网协议是必须要了解的。本文使用WireShark抓包工具,对一次网页请求背后的TCP连接、HTTP请求进行了详细的展示。对网页请求中浏览器使用的并行连接、持久连接...

    一次网页请求背后的连接

    想成为一个优秀的前端,对互联网协议是必须要了解的。本文使用WireShark抓包工具,对一次网页请求背后的TCP连接、HTTP请求进行了详细的展示。对网页请求中浏览器使用的并行连接、持久连接以及TCP连接建立关闭的过程均有所分析讨论,对HTTP如何封装在TCP中进行数据请求以及数据的响应返回也有所展示。供学习交流之用。

    TCP简介

    本文从传输层的报文开始,下层网络IP层、链路层寻址不在这里讨论范围。可以查看相关资料补充计算机网络知识。

    TCP和UDP是传输层的2个协议。

    UDP是一个简单的、尽力而为的数据报传输协议,它不能确保可靠传输。提供一种无连接的服务,不可靠但效率高,适合通信量较小或者延迟敏感的传输。

    相对而言TCP(Transmission Control Protocol)能够提供可靠传输,这也决定了它高度的复杂性以及较大的系统开销。TCP的复杂性足以用一本专门的教材来讲解,这里我们只对其经典的三次握手、“四次挥手”以及基本报文格式进行简介,达到看懂wireshark抓获的封包的目的。

    TCP报文格式


    图1

    TCP的报文格式如上图所示。孜孜不倦地工作在我们每日的互联网通信中,并将长期保持不变。所以你有大把的时光慢慢记住它。

    这里特别需要用到的是红色的标志位部分,分别是:URG,ACK,PSH,RST,SYN,FIN。

    其中在三次握手中十分重要的标志位:

    ACK:这是非常常用的一个标志位,不仅仅在三次握手以及四次挥手中,该标志位的功能就是用于确认。该位置1时表示正确接收到了对方的报文。

    SYN:同步序列号标志。在建立TCP连接时,一定会有一方主动端,一方被动端。网页请求中客户端浏览器就是主动端。当SYN为1而ACK为0时,这就是一个主动请求连接的报文。若对方同意连接,返回一个ACK和SYN均为1的接受报文。可以说它就是建立连接的一个标志。

    FIN:在数据发送完毕,希望断开连接时,向对方发送一个FIN位置1的报文,表示我已经发送完毕。

    还有一些需要说明的是图中红色的标志位上面的2排–32位序列号以及32位确认号。

    先补充说明一点,TCP连接是一个全双工的连接,也就是连接的双方均可以在此连接上进行数据的收发操作。对于大块的数据,TCP的一个报文装不下,需要分成多个报文来发送,然而在实际的网络中,报文很可能经过不同的路由到达对方。报文到达的顺序是没办法保证的因此,32位序列号就用来指明报文的顺序,其具体含义是本报文携带的数据部分,第一个字节的序号。

    举例来说,假设现在有3394字节的数据需要传输(每个字节占用一个序号),每个TCP报文最多携带1424字节的数据,同时假设数据从序号1开始发送(这个序号不一定是1,你可以选定从其他序号开始发送,这个序号是在三次握手的过程中协商好的)。那么第一个TCP报文的32位序列号就为1,发送了1424个字节。那么第二个TCP报文的32位序列号就从1425开始,第三个就为1424+1424的下一个2849开始。

    刚才说了,TCP是全双工的连接,发送方在发送数据的同时也会接收数据。32位确认号就是告诉对方我已经成功接收的数据序号是多少。我们现在转换角色,假设我们是上面发送3394数据的接收方,我已经正确接收了上面的第一个报文,也就是我正确接收到了1到1424字节序号,那么我在发回的报文中将会把32位确认号写为1425(这里正确接收到了1424序号,但TCP自动将其加1,只是为了方便表示我希望接收的下一个字节序号是1425)。注意,只有在ACK标志位置1时该字段才有意义,ACK置1表明确认接收。还有,收发双方选择的起始发送序号不一定相同,比如我选择从1序号开始发,你可以选择从1024序号开始发,序号只是一个编号而已,彼此根据这个编号进行确认就可以了。

    下面我们还会具体分析一次网页请求的报文。

    TCP三次握手

    TCP是面向连接的协议,建立连接需要进行经典的3次握手,为什么一定要进行三次握手,你可以自行查阅资料。

    三次握手的过程如下:


    图2

    在网页请求中,首先客户浏览器端向服务器发送一个SYN为1的报文,其中包含一个自己选定的初始的32位序列号x,这是第一次握手。

    对方收到该请求后,如果同意建立连接,返回一个ACK和SYN标志位均为1的确认报文,将该报文的32位确认号写为x+1,与此同时,自己选定一个初始32位序列号y,这是第二次握手。

    第三次握手,发起请求方收到了确认回复之后,返回一个ACK为1的确认报文,其中的32位确认号为y+1。至此连接建立。客户端将从自己设定的x+1序号开始发送数据。

    这里我们使用wireshark对进行抓包,该网站是我部署在腾讯云的小主页。打开浏览器,请求该网页。抓到下面的封包:


    图3

    可以把图放大看,这里红线圈出来的3条就是3次握手的过程。可能看不太清,我下面会逐一对单个报文截图。

    我们看到在三次握手中间还夹杂了其他的TCP包,实际上在一次网页请求中,不止建立了一个TCP端到端的连接!我们下面会说,这里我们只需要关注用红色圈起来的3个报文。

    第一条:


    图4

    这里我们暂时不关注其他层。但是从下面第一条蓝色上面的网络IP层可看到

    Internet Protocol Version 4, Src:115.156.245.180, Dst: 115.159.81.187
    

    这是网络层的内容,它负责IP寻址,其中封装了手法双方的IP信息。加上Transmission Control Protocol中的内容:

    Source Port: 60545
    Destination Port: 80
    

    可以看出这是从本机115.156.245.180:60545端口到目标主机115.159.81.187:80端口的连接,http服务默认端口就是80。我们看蓝色的Flags:0x002(SYN),可知这是一个TCP请求连接的报文,也就是第一次握手。注意看Sequence number:0,发送方将初始发送序号x选择成了0。

    tip: ip+端口号 就是一个套接字

    第二条:


    图5

    同上,我们可以看到这是一个从目标主机115.159.81.187:80端口发到本机115.156.245.180:60545端口的报文。其中Flags: 0x12(SYN, ACK),SYN、ACK置1,表明对方同意建立连接。这是第二次握手。回复的Acknowledgment number: 1也就是32位确认序列号,为第一次握手的x+1(第一次放松选择的x是0)。同时自己选择的初始发送序号Sequence number:0也为0。

    第三条:


    图6

    这是第三次握手,由本机60545端口发向目标主机80端口一个ACK确认报文。

    红色部分,回复的Acknowledgment number为第二次握手服务器方选定的Sequence number+1。至此连接建立。

    发送第一个HTTP请求,获取HTML文档

    紧接着三次握手建立连接之后,就是一次http请求:


    图7

    我们可以看到在该请求上面一共抓了6个TCP包,实际上这里建立了2个TCP连接,一共6次握手。暂时不管。我们看建立在上面3次握手的TCP连接的HTTP请求的具体内容:


    图8

    通过上图我们可以看出,这是一个从本机115.156.245.180:60545端口到目标主机115.159.81.187:80端口的HTTP请求。

    实际上在网络传输中,从物理层、链路层、网络层、传输层每一层都进行了封装,每一层都添加了自己这一层的包头。大概像下面这个样子:


    图9

    简单的说,以太网的标头封装了发送接收者的MAC地址等信息,IP标头中封装了发送接收者的IP信息,用于找到主机所在的网络。

    到了绿色的TCP标头就是图1中20字节的报文头。而图1中报文头下的数据部分就是本次HTTP请求的内容。也就是说HTTP请求是封装在TCP的数据部分的。

    我们看图8右下角红色框中的内容,我们可以看到16进制数据解码出的字符串,就是本次HTTP请求的Request Header,对比我们在浏览器开发者工具中Network中抓取的http请求的请求头:


    图10

    接收HTML文档

    在上一步中,发送了一个HTTP请求,用来请求HTML文档,接下来的3个TCP报文是服务器端发回的响应,响应的内容就是整个HTML文档:


    图11

    图中之所以说“这3个TCP报文”是因为,HTTP本身就是封装在TCP报文的数据部分的,图中红框里第三个抓到的报文也是TCP报文。打开报文内容会发现整个HTML文档内容的长度为3394个字节,分装在3个TCP报文中传输,长度分别为1424,1424和546个字节。图中三个报文长度分别为1478,1478以及600是因为图中显示的是包含头部信息的完整报文长度。作为TCP数据部分传输的HTML文档分别是1424,1424以及546字节。

    我们可以在wireshark中看到解码的内容信息:

    第一条:

    第二条:

    第三条:

    如果你把内容部分组合起来就是下面的html文档以及HTTP返回头信息:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>首页 - 王浴昊的个人网站</title>
    
        <link rel="stylesheet" media="screen" href="http://wyuhao.com/style.css">
        <link rel="stylesheet" type="text/css" href="http://wyuhao.com/normalize.css">
    
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
        <meta name="author" content="王浴昊">
        <meta name="description" content="王浴昊的个人网站 首页">
        <meta name="robots" content="all">
    
    
        <!--[if lt IE 9]>
        <script src="script/html5shiv.js"></script>
        <![endif]-->
    </head>
    
    <body id="css-zen-garden">
    <div class="page-wrapper">
    
        <section class="intro" id="zen-intro">
            <header role="banner">
                <h1>WYH Personal Website</h1>
                <h2>Easy <abbr title="Cascading Style Sheets">AND</abbr> Professional</h2>
            </header>
    
            <div class="summary" id="zen-summary" role="article">
                <p>A website of sharing my notes of reading and some small demos for <abbr title="Cascading Style Sheets">NEW HANDs</abbr> in front-end learning. Feel free to leave any advice since knowledge sparks during discussion.
                <p>Try to contact me at <a href="/examples/index" title="This page's source HTML code, not to be modified.">673212779@qq.com</a></p>
            </div>
    
            <div class="preamble" id="zen-preamble" role="article">
                <h3>About</h3>
                <p>在这里分享了一些个人前端学习过程中摘录的一些笔记以及部分小的demo供初学者交流学习使用。</p>
                <p>点击下面的导航进入博客、演示页面。建议及评论可以留言我。</p>
            </div>
    
            <div class="acknowlegements" role="article">
                <h3>Acknowlegements</h3>
                <p>Special thanks to <a href="http://www.csszengarden.com/">CSS Zen Garden</a> since this website is based on the style of "Mid Century Modern"(from CSS Zen Garden) designed by <a href="http://www.andrewlohman.com/">ANDREW LOHMAN</a>.</p>
            </div>
    
        </section>
    
        <aside class="sidebar" role="complementary">
            <div class="wrapper">
                <footer>
                    <a href="http://wyuhao.com/notes" title="阅读笔记" class="zen-validate-html">博客</a>
                    <a href="http://wyuhao.com/demo" title="演示" class="zen-validate-css">演示</a>
                    <a href="http://wyuhao.com/msgboard" title="留言板" class="zen-license">留言</a>
                    <a href="http://blog.csdn.net/a153375250/" title="我的CSDN" class="zen-accessibility">CSDN</a>
                    <a href="https://github.com/easysir" title="Github" class="zen-github">Github</a>
                </footer>
            </div>
        </aside>
    
    
    </div>
    
    <!--
    
        These superfluous divs/spans were originally provided as catch-alls to add extra imagery.
        These days we have full ::before and ::after support, favour using those instead.
        These only remain for historical design compatibility. They might go away one day.
    
    -->
    <div class="extra1" role="presentation"></div><div class="extra2" role="presentation"></div><div class="extra3" role="presentation"></div>
    <div class="extra4" role="presentation"></div><div class="extra5" role="presentation"></div><div class="extra6" role="presentation"></div>
    
    </body>
    </html>

    返回头信息参考图10请求头,在这里省略了。

    建立了多个TCP连接

    以上以HTML文档为例,展示了一次HTTP请求的连接及请求响应过程。实际上,我观察本次网页请求抓取的所有包,我发现其中一共建立了4个TCP连接,在图7中显示了其中的2个连接的握手信息。

    这4个连接分别用在了获取页面中引入的样式表、样式表中的字体图片等信息。截取部分依赖:

    background: #c879b2 url(icon/icons-3-pack.svg);
    
    @font-face {
        font-family: 'fira_sansregular';
        src: url('font/firasans-regular-webfont.woff2') format('woff2'),
             url('font/firasans-regular-webfont.woff') format('woff');
        font-weight: normal;
        font-style: normal;
    }
    
    <link rel="stylesheet" type="text/css" href="http://wyuhao.com/normalize.css">

    实际上,一个页面所引用的所有组件,包括样式表、图片、脚本等都是一次HTTP请求,从Network中可以看出本次页面请求一共进行了包含HTML文档在内的7次HTTP请求:

    这些请求分在4个TCP连接中进行。具体在抓包中有所体现,举一个例子来说吧:

    这里对页面中引用的normalize.css就是通过另一个TCP连接获取的,通过端口号可以区分,这里是建立在60546端口的TCP连接,不同于上面的60545。

    实际上浏览器在这里使用了并行连接用来客服单条TCP连接的空载时间以及带宽限制。就像下面一样:

    浏览器会使用并行连接来提升效率,但是需要知道的是,TCP连接本身就会消耗较多的资源。对于一个页面中包含几十上百个HTTP请求的情况,开启几十个TCP连接是不可能的。通常,浏览器会将连接数限制4。本例中,我统计了抓包信息,恰好开启了4个TCP连接。

    连接的关闭

    HTTP基于TCP,TCP又是面向连接的协议。连接有建立就有关闭,我在抓包的时候发现,当所有请求数据都发送完毕的时候,TCP连接并没有马上关闭。

    我们回过头看第一个HTTP请求头信息:

    其中有一项:Connection: keep-alive

    返回头:

    返回头中也有Connection: keep-alive

    在HTTP请求中,keep-alive用来告诉服务器我希望建立一个持久连接,也就是在数据发送完毕后不立即关闭该TCP连接。持久连接能够避免重复地打开关闭连接的消耗同时也能回避TCP慢启动特性带来的损耗。如果服务器同意建立该持久连接,就在响应头中同样包含Connection: keep-alive,否则客户端就会认为服务器不支持keep-alive。本连接是一个持久连接。

    通常持久连接的响应首部还会包含timeout以及max参数,用来估计服务器将连接保持活跃的时长以及保持持久连接数的上限。注意,该值只是估计,不是承诺。不过这里的响应头中并没有包含此类信息。

    本网站我是使用Express框架做的后台,我没有手动设置timeout以及max,也没查到其默认timeout时长,不过我用系统时钟进行了测试,在数据发送完毕后120s左右,TCP连接中断:

    上图中包含了所有4个TCP连接断开的4次挥手信息。

    我发现,在手动关闭网页时也不会立即断开TCP连接。在连接期间刷新网页会继续使用已经建立的TCP连接。但是,如果关闭浏览器,会立即断开TCP连接。

    关于4次挥手

    关于TCP连接的4次挥手。就不具体分析报文了,大家可以自己手动去抓包看一看。但是把其原理说清楚。

    终止连接的操作,连接的双方都可以主动发起。TCP的全双工连接实际上可以分成2个“半连接”,每个“半连接”都是一个单向的连接。关闭一个“半连接”,另一个还能正常工作。2个“半连接”的关闭一共需要4步。

    将设主机A的应用进程先向其TCP发出释放请求,并且不再发送数据。则TCP将发向对方B的报文FIN标志位置1,其32位序列号为前面已经传输过最后一个字节的序号加1。

    主机B收到释放连接的通知后随机发回ACK确认,32位确认号为收到的序列号加1,同时通知高层应用进程。这样A到B的连接释放。此后主机B不再接收主机A发来的数据。但若主机B还有一些数据需要发送到主机A,主机A仍然接收并正常返回确认。

    主机B到A的连接释放同上。

    欢迎访问 我的小站

    展开全文
  • 公众号600篇文章分类和索引

    千次阅读 2020-02-27 07:30:00
    我的这个杂货铺,已经写到了600篇,这是个数字,同时是种鞭策,每次数字的更迭,我都能从中吸取经验和教训,得到一些启发,当然,少不了读者们的支持和鼓励,这些都是动力,督促着我。这个时刻,...

    我的这个杂货铺,已经写到了600篇,这是一个数字,同时是种鞭策,每次数字的更迭,我都能从中吸取经验和教训,得到一些启发,当然,少不了读者们的支持和鼓励,这些都是动力,督促着我。

    这个时刻,我们整个国家还在和新冠做着斗争,在这一场没有硝烟的战争中,无论防控一线,还是我们普通人,都在做着自己能做的、应该做的。在这个艰难时刻,我们会更加团结,共克时艰。

    人生面临很多的选择,有对有错,但其实对错是相对来说的,当下的错,长远看未必是错,坚持自己的信念,知道自己想要的是什么,可能才是最重要的事情。

    再回到杂货铺,我的这些文字,技术类的文章,可以分为,

    非技术的文章,可以分为,

    历史数据统计的文章,从这能了解各个阶段杂货铺的各种统计数据:

    公众号600篇文章数据统计

    公众号500篇文章数据统计

    公众号400篇文章数据统计

    公众号300篇文章数据统计

    公众号250篇文章数据统计

    公众号150篇文章数据统计

    公众号100篇文章数据统计

    历史文章的链接,直达文章链接:

    公众号500篇文章分类和索引

    公众号400篇文章分类和索引

    公众号300篇文章分类和索引

    公众号250篇文章分类和索引

    公众号150篇文章分类和索引

    公众号100篇文章分类和索引

    以下是这600篇文章链接,若是能对你有些许帮助,就达到作用了,

    Oracle常识

    这些Oracle基础面试题,你能快速地回答出来么?

    Oracle的一项“AI“技能介绍

    Oracle软件的收费标准4

    Oracle ADG究竟是否收费?

    Oracle安装部署

    Oracle 19c之RPM安装

    数据库安装的两个小错误

    12c CC,不得已的重装

    Oracle 12c CC安装碰见的认证问题

    Oracle 12c CC安装部署攻略 (下)

    Oracle 12c CC安装部署攻略 (上)

    Oracle Patch补丁体系和如何打补丁

    客户端ADR配置

    安装Oracle Linux碰见的几个问题

    11g Grid Control安装过程的一些“坑”

    Oracle案例

    如何找到抛出ORA-00933错误的SQL

    ORA-12519的错误和解决

    Oracle表中允许支持的最大列数是多少?

    Oracle版本升级过程中,SQL性能下降的案例一则

    索引重建失败的解决

    SELECT和DELETE执行计划的不同选择

    一个DATE数据类型的检索

    DELETE选错执行计划的困境

    《一次Oracle bug的故障排查过程思考》的问题重现解决

    应用执行慢的问题排查路径

    数据库连接池配置参考

    一次Oracle bug的故障排查过程思考

    一个索引创建引出的思考

    并行的常见问题和注意事项

    这条SQL的索引,你会如何创建?

    几个新员工培训的小问题

    字符转换的SQL需求增强

    导入导出的两个小错误

    字符转换的SQL需求

    HINT无效的几个场景

    函数索引构成虚拟隐藏列

    enq: TM - contention锁争用的解决

    执行truncate抛出的ORA-02266

    一个索引热块的性能问题

    间隔分区报错ORA-14758

    变通执行truncate

    9i下优化器模式的选择

    领会ORA-01405错误的含义和解决

    并行创建主键的问题延伸

    使用并行创建主键约束的“奇葩”过程

    delete操作对UNDO表空间容量的冲击

    从rownum的SQL需求还能归纳出的知识

    rownum的SQL需求

    从一条"错误"的SQL,了解rownum的作用

    已存记录的非空约束限制

    探寻大表删除字段慢的原因

    传说中的“谓词越界“场景

    大表删除字段为何慢?

    一个更新数据的需求

    学习一下dbms_parallel_execute

    千万级表数据更新的借鉴帖

    增量数据读取的需求

    千万级表数据更新的总结

    CTAS建表容易忽略的一些细节

    千万级表数据更新的反馈

    误删除数据恢复的一种场景

    千万级表数据更新的需求

    用SQL实现的一个数据统计需求

    隐式转换的案例场景

    在线创建索引的问题案例

    一张频繁DML的大表,如何正确地创建索引?

    数据库层探究应用为何“卡住”

    一个非常小的数据复制需求

    用户的密码过期,如何解决?

    字段拆分多行的需求

    excel数据导入Oracle的需求

    ORA-09817错误场景

    返璞归真 - Oracle 9i的RBO执行计划案例

    初学SQL注入

    PL/SQL中SQL语句10053创建方法

    有关10053事件,你知道这两个知识点么?

    SQL语句Concatenation字符串拼接错误

    探究外键为何要建索引?

    ORA-02266错误解决方案

    一个COMMIT提交次数的问题

    路由冲突对数据库的影响

    统计信息的偏差,导致SQL性能影响的一则案例

    操作分区表提示ORA-01502

    under any table权限

    花有重开日,人无再少年

    一生可能只有一次耐高,但优化可以不止一次

    困扰许久的一个ORA-00060错误解决

    Oracle日期类型占用的空间

    表访问授权问题

    如何查询某个用户下能执行哪些存储过程?

    时间间隔分区,及其默认表空间的几个使用场景

    如何使用TTS进行数据复制

    如何生成其他会话的10046?

    普通堆表在线转换为分区表

    如何统计表的活跃度?

    统计信息锁定的一种场景

    imp/exp导入导出的一些错误

    统计信息的解锁

    普通堆表导入为分区表需求

    无备份情况下,绕开ORA-01578错误的脚本

    Oracle CBO选错执行计划的一种场景

    imp错误IMP-00098: INTERNAL ERROR: impgst2Segmentation fault

    Oracle导入导出的常见错误

    Oracle违反约束数据的workaround

    自适应log file sync影响案例

    试用ODU软件恢复corrupt block

    "0.1"在PL/SQL Developer和sqlplus中如何不显示为".1"?

    一次夜维SQL的性能优化

    从ORA-01950报错我们能了解的知识

    ORA-28002的一个细节(文末有彩蛋)

    上周上线碰见的ORA-00054错误回放

    表中已存重复数据的情况,如何增加唯一性约束?

    新员工培训环境准备中,碰见的两个ORA-600错误

    数据迁移中碰见的一些问题

    有索引却不用的两个场景

    解决导入过程中出现的ORA-02289错误

    如何验证dump文件的有效性

    生产数据导入测试环境碰见的一些问题

    一个诡异的SQL事务现象

    解决Logical Reads高的方法和实验

    缓解latch: cache buffers chains的案例

    如何解决/home/oracle: is a directory报警

    Numeric Overflow,SQL问题?Java Code问题?

    误删除序列sequence,是否可以找回?

    kill等待session的方法 - 引申自恩墨面试题的一些思考

    从ORA-01752的错误,透过现象看本质

    我的第一次坏块故障恢复经历

    一个执行计划异常变更的案例 - 正传

    一个执行计划异常变更的案例 - 外传之SQL Profile(下)

    一个执行计划异常变更的案例 - 外传之SQL Profile(上)

    一个执行计划异常变更的案例 - 外传之直方图

    一个执行计划异常变更的案例 - 外传之SQL AWR

    一个执行计划异常变更的案例 - 外传之ASH

    一个执行计划异常变更的案例 - 外传之AWR

    一个执行计划异常变更的案例 - 外传之查询执行计划的几种方法

    一个执行计划异常变更的案例 - 外传之聚簇因子(Clustering Factor)

    rolling invalidation对子游标产生的影响

    一个执行计划异常变更的案例 - 外传之查看绑定变量值的几种方法

    一个执行计划异常变更的案例 - 外传之绑定变量窥探

    一个执行计划异常变更的案例 - 前传

    使用exp导出报错EXP-00091

    一次有意思的错选执行计划问题定位

    一个低级的ORA-01017错误

    一个开发需求的解决方案 & Oracle临时表介绍

    空格字符的错误造成监听无法启动

    PRVF-0002错误

    TO_DATE函数索引报错ORA-01743

    sqlplus登录报ORA-06502错误的问题排查和解决

    寻找锁定数据库用户的真凶

    传输表空间TTS操作

    Oracle原理

    揭开PLUSTRACE角色面纱

    删除分区如何不让全局索引失效?

    如何不影响生产库性能的情况下评估整库的容量

    一个导数需求的演进过程

    删除分区提示ORA-00942

    新增字段的一点一滴技巧

    DDL操作提示了一个DML操作才会抛的ORA错误?

    一道SQL考题的更多思考

    一道SQL考题的思考

    Oracle的体系结构图万花筒

    Oracle 20c十大新特性

    Oracle 19c十大新特性

    Oracle 18c十大新特性

    Oracle中BETWEEN ... AND的作用

    Oralce密码复杂度设计验证

    对recursive calls的深刻理解

    DCL-数据控制语言

    搞清“连接”和“会话”

    混淆的行迁移和行链接

    Oracle和PG的count

    Oracle各版本的分区表演进

    对imp中的fromuser参数的偏差理解

    小时制式问题

    Oracle中YY和RR表示年份的区别

    Oracle读取数据的顺序问题

    浅谈显式转换和隐式转换

    Oracle不同版本group by的变化

    由点及面了解Oracle的Sequence序列

    什么是rowid?

    来自MOS的一篇文章,《为何在查询中索引未被使用》

    *_tab_privs相关的两张视图介绍

    CBO如何选择相同cost的索引

    探索索引的奥秘 - 10053事件

    探索索引的奥秘 - 有索引就一定会用么?

    探索索引的奥秘 - 索引的属性

    truncate表,会将统计信息清除么?

    一个关于Definer和Invoker的权限问题

    DML错误日志表

    一张几亿的分区表,能改名么?

    修改表字段长度的操作,对业务是否有影响?

    alter table新增字段操作究竟有何影响?(下篇)

    DBMS_STATS.AUTO_SAMPLE_SIZE的值是什么?

    dbms_space.create_table_cost的unwrap解密和原理解析

    非分区表是否可以创建分区索引?

    ORA-01653/01654错误和dba_free_space视图的理解

    创建索引的两种方式比对

    PLUSTRACE角色

    两个和安全相关的Oracle参数演进

    PMON主动调用的频率控制

    一个用户创建引发的权限控制问题

    select count(*)、count(1)、count(主键列)和count(包含空值的列)有何区别?

    interval间隔分区STORE IN参数的作用范围

    NLS_LENGTH_SEMANTICS参数引申的问题

    新增非空约束字段在不同版本中的演进

    alter table新增字段操作究竟有何影响?(上篇)

    外键为何要建索引?

    Oracle Concept翻译

    《Oracle Concept》第三章 - 12

    《Oracle Concept》第三章 - 11

    《Oracle Concept》第三章 - 10

    《Oracle Concept》第三章 - 9

    《Oracle Concept》第三章 - 8
    《Oracle Concept》第三章 - 7

    《Oracle Concept》第三章 - 6

    《Oracle Concept》第三章 - 5

    《Oracle Concept》第三章 - 4

    《Oracle Concept》第三章 - 3

    《Oracle Concept》第三章 - 2

    《Oracle Concept》第三章 - 1

    《Oracle Concept》第二章 - 22 (12c内容补充)

    《Oracle Concept》第二章 - 21 (12c内容补充)

    《Oracle Concept》第二章 - 20 (12c内容补充)

    《Oracle Concept》第二章 - 19

    《Oracle Concept》第二章 - 18

    《Oracle Concept》第二章 - 17

    《Oracle Concept》第二章 - 16

    《Oracle Concept》第二章 - 15

    《Oracle Concept》第二章 - 14

    《Oracle Concept》第二章 - 13

    《Oracle Concept》第二章 - 12

    《Oracle Concept》第二章 - 10

    《Oracle Concept》第二章 - 9

    《Oracle Concept》第二章 - 8

    《Oracle Concept》第二章 - 7

    《Oracle Concept》第二章 - 6

    《Oracle Concept》第二章 - 5

    《Oracle Concept》第二章 - 4

    《Oracle Concept》第二章 - 3

    《Oracle Concept》第二章 - 2

    《Oracle Concept》前言-12c内容补充

    《Oracle Concept》第一章-1

    《Oracle Concept》前言介绍-6

    《Oracle Concept》前言介绍-5

    《Oracle Concept》前言介绍-4

    《Oracle Concept》前言介绍-3

    《Oracle Concept》前言介绍-2

    《Oracle Conecpt》 - 目录结构

    《Oracle Conecpt》 - 初步认识1

    Oracle开发

    jdbc jar版本对读取sequence的一些区别

    JDBC读取数据优化-fetch size

    一个触发器需求的案例

    学习show_space存储过程

    预估表和索引空间容量的方法

    时间戳相减的几种方法

    含LOB的表实际容量计算方法

    经典的print_table学习

    三种批量删除PLSQL写法效率的比对

    SYS_CONTEXT函数返回IP地址的一些误解

    一种批量删除数据的方法

    一个分页排序SQL查询结果集不确定的案例

    11g中利用listagg函数实现自动拼接INSERT语句

    sosi脚本改造

    java.sql.SQLException: 索引中丢失 IN或OUT 参数::x

    MySQL

    MySQL时间函数的选择

    学习的好地方 - 阿里数据库内核组月报站点

    MySQL表名大小写问题

    Lost connection to MySQL server错误

    Windows环境安装MySQL ZIP Archive

    MySQL远程访问权限的设置

    MySQL系统运行状态实时监控(python版本)

    MySQL系统运行状态实时监控(shell版本)

    MySQL 5.6搭建主从复制

    MySQL 5.6 rpm安装方法和碰见的问题

    MySQL的rpm和源码两种安装操作

    EDB

    PG生效参数的演进过程

    EDB官方网站和文档的介绍

    EDB无法删除分区子表的错误

    Oracle/EDB/达梦,对同字段多索引的支持

    EDB索引坏块的问题

    EDB(/PG)对于执行计划的解读

    EDB是什么数据库?

    EDB日期类型的一个问题

    Python

    安装psycopg2碰见的各种错误

    python读取配置文件

    GO:

    Hello Go World!

    应用运维

    mv argument list too long错误

    FTP的两种传输模式的坑

    一个shell中诡异问题的解决

    初识redis

    shell常见问题系列

    什么是“中台”?

    一则open files的故障

    幂等性

    两个shell开发的小错

    几种关系型数据库的介绍

    什么是字符集?

    RAID的基础知识

    wrong ELF class ELFCLASS64错误

    Linux中Too many open files错误的解决

    应用执行慢的定位案例

    一个shell中变量作用域的问题 - 另解

    一个shell中变量作用域的问题

    一次惊心动魄的问题排查

    碰见.swp文件咋办?

    一个shell解析文件的需求

    Linux下如何避免误操作执行rm

    我经常用的一些vi快捷键

    iptables-远程访问数据库端口策略

    filebeat占用Linux空间未释放的问题解决

    经典的运维脚本三步曲

    json格式值班表解析

    定制化删除ES索引数据

    Linux实际内存占用率算法,以及使用Python实现内存监控

    Tuxedo

    Tuxedo错误号汇总

    Tuxedo Client调用无法连接WSL

    两个Tuxedo Client问题的困惑

    Tuxedo两个小问题解决

    Tuxedo域连接错误引出的netstat不同平台参数的区别

    Oracle服务端和客户端版本补丁不一致造成Tuxedo应用程序出core案例

    Tuxedo服务无法启动的问题解决(涉及MP下tlisten和TLOG的报错)

    Java开发

    实现CSDN博客之星的自动排名

    使用druid碰见的一个诡异问题

    Java日期中“y”和“Y”的区别

    为什么说 Java 程序员到了必须掌握 Spring Boot 的时候?

    Bruce Eckel给新手程序员的建议

    有意思的一些红包算法

    JDBC读取新插入Oracle数据库Sequence值的方法

    从MySQL和Oracle的JDBC驱动源码,看批量操作有何不同?

    Java保留两位小数的方法

    开发问题小结

    REST初体验

    MyBatis中的JdbcType映射介绍

    生活随笔

    新年目标

    Oracle ACE,一段不可思议的旅程

    高考

    逃出996,追逐新目标!

    许久不见,甚是想念,天堂之上,愿你安好!

    我和《程序员》杂志的点滴交集

    遇见更好的自己

    这就是生活,不管你喜不喜欢,他就在这里!

    有一场青春,就叫高考

    忽而今夏,一定要抓住你的何洛/章远

    只要你起跑,永远都不晚

    来自《小猪佩奇》的思考

    愿你在天堂一切安好!

    今天是第730天了

    12年老程序员的5次转型(文末有彩蛋)

    一篇FaceBook海外面试经历的文章

    后沙峪之第一天

    DBA 3.0时代来了!

    “扎心”的文案

    我写推文的一些初衷

    《战狼2》观影感想

    不忘初心,方得始终(2016.7-2017.2文章索引)

    bye bye 2016,hello 2017

    育儿:

    棕色和褐色的区别及联系

    为什么空调能制冷和滴水?

    旅途见闻

    跟着Google Map,让我们来一次欧洲球场之行

    藤子不二雄博物馆之行

    英超梦幻之行

    台儿庄游记

    伦敦地铁介绍

    我经历的日本东京交通

    巡游KAMO店

    亚冠之旅-浦和红钻球场篇

    梦想还是要有的,万一实现了呢?

    西甲之旅-皇马伯纳乌

    西甲之旅-巴塞罗那诺坎普

    英超之旅-入境小结篇

    英超之旅-出行前准备篇

    英超之旅-伦敦奥林匹克体育场篇

    英超之旅-酋长球场篇

    英超之旅-洛夫图斯路篇

    英超之旅-古德逊公园篇

    英超之旅-安菲尔德篇

    英超之旅-老特拉福德篇

    英超之旅-依蒂哈德篇

    英超之旅-wifi篇

    英超之旅-伦敦地铁篇

    英超之旅-斯坦福桥球场篇

    英超之旅-观赛球票篇

    英超之旅-拉克文农球场篇

    我的英超之旅-剧透篇

    英超之旅-签证准备篇

    疯狂的事情,即将开始

    牛津辩论社

    魔都之行杂谈

    重庆初印象

    上海见闻

    技术会议

    【CSDN2019年度博客之星总评选】求拉票

    ACOUG年会感想

    ACOUG年会邀请

    数据技术嘉年华来了

    7·21,你能想起什么?

    OCM联盟分享活动

    价值3400的福利,扑面而来!

    DTCC大会归来感想

    小福利 - DTCC大会门票免费送

    GOPS的魔都之行

    下周演讲预告:《见微知著,数据库应用设计优化浅谈》

    有趣的sequence知识 - 一些开发中sequence的使用经验

    有趣的sequence知识 - 18c的sequence新特性

    有趣的sequence知识 - 性能问题

    有趣的sequence知识 - 基本介绍

    OCMU技术分享活动

    学习TED演讲经验

    TED演讲介绍

    参加WOT技术峰会的见闻

    OCMU技术分享PPT链接

    OCMU联盟技术分享

    Oracle技术分享主题投票结果

    Oracle技术分享主题征集投票

    ACOUG年会感想和启发

    EXIN DevOps Master培训有感

    第七届数据技术嘉年华

    2017 Oracle Open World会议介绍汇总

    2017 Oracle Open World (1)

    Oracle培训的建议收集

    百度AI大会的一些想法,了解DuerOS、Amazon Alexa和Echo

    参加ACOUG嘉年华的体验和总结

    OCM成长之旅

    东成西就(国安和首钢)

    一场局气的比赛

    奇迹?谁知道?

    无奈又如何?

    No Miracle

    愿赌服输,站直了,别趴下

    不是因为有希望才坚持,而是因为坚持才有希望!

    争冠,在路上?还是渐行渐远?

    数说国安的上半赛季

    五连胜?这才刚刚开始!

    聊聊两场球

    再现高效传控

    可贵的胜利

    新赛季从超级杯开始!

    从0:3到2:2,国安是冠军!

    决战,还有半场!

    京沪大战,五味杂陈

    悬念一一揭晓了,几家欢喜几家愁

    憋屈、郁闷的一场比赛

    东不成,西没就,又如何?

    不畏强敌,不忘初心,砥砺前行!

    输了,又如何?

    迎难而上,一个字儿:战!

    失败,未尝不是件好事

    决战就在今晚,为了荣誉,加油!

    迎接天王山之战

    一样的京津德比,不一样的心境

    戒骄戒躁,继续前行!

    赢就一起狂,输就一起扛

    不得不吹一波比埃拉

    革命尚未成功,同志还需努力!

    知足者常乐,继续前进!

    自助者天助!

    数据说中超Tiki-Taka,梦想还是要有的

    用实力说话,不服再战!

    闲话国安

    春风十里不如你

    从0:2到2:2的“惊”和“喜”

    国安对阵恒大会有惊喜么?

    京津德比,1:1不亏

    从数据说国安

    96黄金一代的谢幕

    油腻与否,与年龄无关,与心境有关-Stephon Marbury

    曾经的“东成西就”

    我看新赛季的北控和首钢

    中超悬念

    今儿的闲篇儿(10011、亚冠、欧冠、NBA)

    国安0:1上港

    聊聊新赛季的北京首钢

    富力2:1国安

    国安4:4延边

    华夏2:0国安

    国安4:0辽宁

    国安2:2鲁能

    建业0:2国安

    申花1:2国安

    老马,加油!北控,加油!

    国安2:0贵州

    京津德比的一些浅薄见解

    今夜北京无眠,今夜北京无泪!

    世界杯

    写在决赛之前

    传控Tiki-Taka战术解惑

    花有重开日,人无再少年 - 八强之战展望

    真正的战斗,才刚刚开始

    世界杯E-H组第一轮尬聊

    世界杯A-D组第一轮尬聊

    世界杯来了,你关注什么?

    体育

    “真正的”欧洲杯,鸡冻不鸡冻?

    马布里和穆里尼奥的回归

    可谓足篮打水一场空

    足坛转会闲聊

    从足球说到鲁伯特之泪

    球鞋的世界

    少年的梦想

    Comeback Complete

    数字背后的欧洲足球联赛 - Transfers

    数字背后的欧洲足球联赛 - Sponsorship

    数字背后的欧洲足球联赛 - Ownership

    你我都曾是少年

    什么是"Tifo"

    数字背后的欧洲足球联赛 - Domestic competitions and supporters

    无论结果,这是我们自己的球队

    致敬穆帅!

    致敬风城玫瑰,再次绽放!

    “火车头”和“迪纳摩”的故事

    后会无7

    半夜看球,是怎样一种感觉?

    看了这则故事,你会坚持梦想么?

    Black Mamba

    巴萨主场比赛空场的原因 - 有关加泰罗尼亚的科普

    当体育遇上政治

    不是所有失败都毫无意义

    国足希望犹存

    国足出线,有戏么?-记于战乌兹前夜

    聊聊篮球,聊聊中国男篮

    我看内马尔转会

    读书笔记

    《收获,不止SQL优化》 - 脚本积累集合 - 1

    《收获,不止SQL优化》 - 获取执行计划的方法对比

    《收获,不止SQL优化》 - 调优信息一键生成脚本学习

    送书活动 - 小麦苗的《数据库面试笔试宝典》

    《深入理解Oracle 12c数据库管理》 - 第五章学习笔记

    《深入理解Oracle 12c数据库管理》 - 第二章和三章学习笔记

    你是否从文章中,看见了一些自己的影子?

    《深入理解Oracle 12c数据库管理》 - 第一章学习笔记

    我读过的一些Oracle相关的图书

    初次接触《深入理解Oracle 12c数据库管理》

    新冠:

    了解下在疫情期间的饮食注意事项

    特殊时期,科技带给我们的便利!

    气溶胶是什么?

    “居家隔离”不要怕

    新型冠状病毒的持续了解

    非常时期的“非常”绘本

    新型冠状病毒的了解

    其他

    解决打开IE浏览器CPU升高

    为什么航班停在远机位?

    强烈推荐一波高质量的公众号,全是圈内大佬!

    什么是冬令时?

    为什么我会建议你学点编程?

    耳机的世界

    如何在阿里云Windows Server创建能用的FTP Server

    了解香港警队

    PPT中如何制作两圆交叉阴影图

    思维导图工具,如何改默认字体?

    Switch-如何解决网速慢?

    PLSQL Developer中文乱码问题

    科普“云”词汇:IaaS、PaaS、SaaS

    PLSQL Developer免密登录

    GitChat的1024活动,文末有福利!

    英语流利说使用心得

    若干关于excel的知识

    Windows 7清理隐藏文件

    谋一次“私”,集赞喽!

    小而实用的软件介绍-2

    初识Tello无人机(文末有招聘彩蛋)

    分享一波干货,总有一款适合你

    Windows下如何根据端口找出进程?

    成功之路,没有捷径

    什么是GDPR?

    公众号文章主题你来定,欢迎各位投票给出建议

    一些技术公众号推荐

    PLSQL Developer日期格式如何定制化

    PowerDesigner逆向工程导出pdm

    英文科技类文章的翻译

    UltraEdit中文字符被自动删除的原因

    iphone 6s plus听筒无声故障的解决方案

    WPS制作组合图表

    “给赞”和“微信”的PK

    介绍两款小众,但是实用的软件

    MAC下安装SVN客户端

    开源软件许可

    一些微信公众号推荐

    Mac Book报错Unapproved Caller的解决

    无线路由器的些许知识

    VMware Fushion解决"vmmon模块的版本不匹配"报错

    一些搬迁GitLab环境中碰见的问题和解决

    强制找回GitLab管理员账户密码的方法

    MBA中Safari访问Domino邮箱服务器的配置

    GitLab初体验

    展开全文
  • 只需几击键(包括管道(|)或重定向),您就可以创建个即兴数据处理机,其功能要比其各个部分的总和更强大。 尽管您日常使用的某些资源可能是本地资源 (即驻留在您自己的工作站上),但是可能会存储大量且不断...

    UNIX®命令行是WYTIWYG界面-也就是说, 您输入的就是您得到的。 UNIX提供了数百个(甚至数千个)命令,您可以使用这些命令来操纵内核和用户空间中的各种可用资源。 需要监控CPU使用率吗? 尝试topps 是否需要删除所有以.bak结尾的文件? 尝试rm *.bak 在新命令方面需要帮助吗? man

    但是,当您需要的资源驻留在广域网(WAN)和全球Internet上的远程系统上时,该怎么办? 引用《银河旅行者指南 》,“不要惊慌!”。 UNIX命令行可以轻松下载和上传文件,连接到远程计算机,并查询远程服务器和网络的状态。 拿起毛巾:现在是时候去逛太阳系。

    本地工作,全球转移

    在本系列的第1部分第2部分中,您了解了使用UNIX命令行可以完成的工作。 只需几次击键(包括管道(|)或重定向),您就可以创建一个即兴数据处理机,其功能要比其各个部分的总和更强大。

    尽管您日常使用的某些资源可能是本地资源 (即驻留在您自己的工作站上),但是可能会存储大量且不断增长的资产(文件,电子邮件和工具)一定距离(例如,在连接到WAN或Internet的计算机上)。 Web浏览器提供了对此类资源的几乎通用的访问权限,但有一个警告:指向和单击很快就会变得乏味,甚至繁琐,尤其是在必须检索多个项目的情况下。 而且,如果您要编写脚本 (本质上是捕获和重放)重复性或易出错的任务,那么窗口浏览器将是一个困难的盟友。

    就像lscpmailuptimedu manage以及query local resources ,UNIX也提供了一套命令行工具来访问远程资源。 本文向您介绍了其中一些工具,包括一种有用的技术,它既可以帮助访问远程系统,又可以保护您的身份验证凭据。 具体来说,您将学习wgetcurl和Secure shell( ssh )。 wgetcurl工具传输文件; 使用ssh ,您可以安全地登录到远程系统并快速轻松地传输文件。

    Telnet(和其他)的麻烦

    如果您的系统运行rsh (或其变体- rcprexecrlogin ,或Irdist )或telnet ,禁止并立即删除,并伴随守护进程。 此外,如果您不允许匿名文件传输协议(FTP),请也禁用FTP软件。

    尽管rshtelnet是UNIX的长期支持者,但攻击者可以利用这两个实用程序来(轻松)破坏您的系统。 您或您的系统管理员应停止并在运行该软件的地方删除该软件,并用ssh替换这些软件包的功能。

    对于特权FTP访问,请使用sftp rdist替换为更高级的rsync 或者,如果必须提供匿名FTP(或通过HTTP下载),请确保使用防火墙硬件和软件将所有可公开访问的计算机与敏感的内部服务器隔离开。

    但是首先,让我们讨论密码存在的令人讨厌的问题。

    “您不需要臭气熏天的密码!”

    通常,大多数计算机和服务的访问都受到保护。 在某些情况下,验证您的身份(并因此获得访问系统的特权)可能需要复杂的质询-响应交换,安全套接字层(SSL)证书,甚至是生物特征扫描。 但是,通常,密码足以获得访问权限。 就像您的个人识别码(PIN)一样,密码是您的秘密; 如果您选择了正确的密码,其他人可能很难随机猜出它。 您的姓名和强密码共同构成了充分的佐证。

    当然,强密码可能很难记住,并且仅当您收集并记住另一个八个字符的密钥(例如,数字,标点和大小写混合)时,这种压力才会加剧。 一遍又一遍地输入密码可能会很烦人-更糟的是,它为自动控制自动化带来了重大障碍。

    认识到这种麻烦,许多命令行实用程序允许您提供用户名和密码作为命令行参数。 例如,您可以使用以下命令来登录FTP站点,而无需干预:

    ftp ftp://joe:passwd@www.example.com

    但是,使用这种工具可以向共享您计算机的其他用户显示您的凭据。 (例如,尝试使用ps -Aeww来查看系统上每个进程的完整命令行和环境。)

    为了提供与命令行选项相同的便利而没有固有的风险,许多程序可以从称为.netrc (发音为net-rc )的特殊文件中读取您的凭据,该文件通常位于〜/ .netrc中。 .netrc文件必须仅是所有者读写模式(模式0600-rw------- ),并且文件中的每个条目都必须遵循以下简单语法:

    machine ftp.example.com login zaphod password I()Trillian!
    
    machine www.magazine.com login abner password MmG8y*tr
    
    default login anonymous password zaphod@heartofgold.com

    前两行提供了machine关键字和计算机的域名, login关键字和您在计算机上的登录名,以及password关键字,后跟与您的登录名关联的密码。 最后一行的凭据为任何未特别命名的系统提供了默认值。 default行必须是.netrc文件中的最后一行。 (有关.netrc文件配置选项的完整信息,请键入man 5 netrc以查看.netrc手册页。)

    显然,如果任何文件中包含任何身份数据,请使用用户只读模式(模式600 )或用户只读模式(模式400 )保护它,以防止意外覆盖或删除它。 您可能还希望使用模式700保护主目录。

    现在,每当启动启用.netrc的应用程序(包括我接下来讨论的那些应用程序)时,相应的登录名和密码都会自动传递给所需的服务,而只需按一下键盘即可。 您通常可以使用-n选项禁用此自动登录功能。

    移转的过程

    与网页的HTTP和SSL上的HTTP(HTTPS)一起,FTP是最常用的Internet应用程序协议之一。 通过FTP,客户端可以连接到服务器,获取目录和文件的列表,然后下载文件(即,从服务器请求文件)或上传文件(即,将文件发送到服务器以保留) )。 格式为ftp://ftp.example.com/path/to/anotherfile.zip和ftp:// user:password@ftp.example.com/path/to/file.zip的 URL表示,使用FTP协议,连接到ftp.example.com并下载文件/path/to/anotherfile.zip。 后一个URL只是添加用于登录的凭据。

    在大多数台式计算机上,此类URL会启动浏览器或默认的FTP应用程序以下载指定的文件。 但是,您可以将相同的URL与wget命令行实用程序一起使用-一种强大的实用程序,可通过HTTP,HTTPS和FTP下载文件。 它支持.netrc文件,并且完全是非交互式的,因此非常适合自动化。 如果您的系统没有wget ,则可以从GNU Software Foundation下载其源代码。 它只需几个简单的命令就可以在所有UNIX变体上轻松构建,并且您可以将该实用程序放置在您的个人bin目录或中央目录中。

    假设您有一个.netrc文件,让我们看一下wget可以做什么的一些示例。 (在下面的示例中,提供了行号以供参考;您无需键入数字。) 清单1显示了如何使用wget下载文件而又不感到命令行的麻烦。

    清单1.使用wget在命令行下载文件
    1 $ wget http://ftp.gnu.org/pub/gnu/wget/wget-1.10.2.tar.gz
    --16:02:29--  http://ftp.gnu.org/pub/gnu/wget/wget-1.10.2.tar.gz
               => `wget-1.10.2.tar.gz'
    Resolving ftp.gnu.org... 199.232.41.7
    Connecting to ftp.gnu.org[199.232.41.7]:80... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 1,213,056 [application/x-tar]
    
    100%[=====================>] 1,213,056    531.22K/s             
    
    16:02:37 (529.57 KB/s) - `wget-1.10.2.tar.gz' saved [1213056/1213056]
    
    2 $ wget -q ftp://mirror.linux.duke.edu/pub/centos/4.3/os\
      /i386/RELEASE-NOTES-en.html
    
    3 $ cat url_list.txt
    http://www.wikipedia.com
    http://valdez.barebones.com/pub/freeware/TextWrangler_2.1.3.dmg
    
    4 $ wget -i -nv url_list.txt
    16:06:00 URL:http://www.wikipedia.org/ [33606] -> "index.html" [1]
    16:06:41 URL:http://valdez.barebones.com/pub/freeware/
      TextWrangler_2.1.3.dmg [9488296/9488296] -> 
      "TextWrangler_2.1.3.dmg" [1]
    FINISHED --16:06:41--
    Downloaded: 9,521,902 bytes in 2 files
    
    5 $ ls
    RELEASE-NOTES-en.html   index.html              wget-1.10.2.tar.gz
    TextWrangler_2.1.3.dmg  url_list.txt

    命令1通过HTTP从其项目主页下载最新的wget源代码。 默认情况下, wget告知您进度。 您可以使用-q (用于安静模式)选项禁用所有消息。 命令2通过FTP检索了CentOS发行说明的版本,尽管非常安静。

    如果要下载的URL列表很长,则无需将每个URL都放在命令行中。 相反,您可以创建(或生成)要下载的URL列表。 命令3显示url_list.txt,这是一个包含两个URL的简单文本目录; 命令4下载两个URL。 提供列表时,请使用-i选项。 -nv选项- 不详细的首字母缩写-提供更简洁的消息。

    除非您提供下载文件的文件名(使用-o选项),否则wget会创建一个新的本地文件,该文件的名称与远程文件的名称相同,而忽略整个开头的URL。 命令5显示了从命令1到命令3下载的四个文件。

    wget实用程序具有许多选项和功能。 它可以抓取 FTP或网站并下载完整的文件层次结构。 您还可以为自动下载设置配额,提供cookie并继续先前被中断的下载。 阅读wget手册页以了解该工具的许多技巧。

    往上走

    wget实用程序对于手动下载非常有用,但无法上传文件。 它也不能与安全的FTP, telnet和许多其他(旧的和较少使用的)Internet协议互操作。 对于这种传输,您必须转向名副其实的瑞士军刀网: curl

    curl命令行实用程序可以获取和放置数据,因此非常适合将本地文件传输到远程服务器。 更好的是, curl的基础-libcurl库-具有丰富的应用程序编程接口(API),使您可以将curl所有功能直接询问到自己的应用程序中。 CC++ ,PHP和Perl编程语言只是可以利用libcurl的众多语言中的四种。 如果您的系统缺少curl和libcurl,则可以从libcurl主页下载源代码。

    因为curl可以将本地文件复制到远程服务器,所以它是小型备份的理想选择。 例如, 清单2显示了一个Shell脚本,该脚本将充满数据库转储的目录复制到远程FTP服务器以进行安全维护。

    清单2.使用curl远程存储数据库转储
    foreach db (mydns mysql cms tv radio)
      /usr/bin/mysqldump --ppassword --add-drop-table -Q --complete-insert $db > $db.sql 
    end
    
    find dbs -mtime -1 -type f -name '*.sql' -print | foreach file (`xargs`)
      curl -n -T $file ftp://ftp1.archive.example.com
    end

    curl -n命令强制curl读取您的.netrc文件。 -T选项告诉curl将指定的文件上传到给定的URL。 如果您省略了目标文件名, curl简单地重用上载文件的名称。

    您可能会猜到, curlwget拥有更多的选择。 值得阅读curl手册页并牢记这一点。 curl项目还维护一个使用列表 ,包括有关如何使用HTTP POSTPUT命令,如何提供登录凭据,如何使用SSL证书以及如何调试curl请求的说明。 快速提示:尝试curl -v --trace-ascii ...生成跟踪信息。

    分离的六个地址

    现代计算在很大程度上取决于各种形状,大小和服务的机器之间无数,尖锐的互连。 确实,即使在小型计算环境中,一台计算机可能专用于电子邮件,另一台计算机专用于服务网页,而其他计算机则用于执行更专门的任务。 在这种环境中-通常通过局域网(LAN),WAN或虚拟专用网络(VPN)连接-每天登录多台计算机非常普遍和必要。 系统管理员每小时都会从一台计算机反弹到另一台计算机,但是对于开发人员和其他用户来说,登录后需要对关键应用程序进行远程访问是很常见的。

    X窗口系统和当前的桌面软件使远程访问变得相当透明:窗口是一个窗口,基础应用程序可以在任何计算机上运行。 但是同样,即使在以鼠标为中心的世界中,命令行也占有特殊的位置。 例如,如何轻松地在多台计算机上运行同一命令? 或者,更简单地说,如何在远程系统上启动xterm窗口?

    远程系统访问是ssh及其衍生物scpsftp的职责。 sshrsh的安全版本,而scpsftp分别是rcp和FTP的安全替代品。 为什么安全? ssh及其变种提供了更强大的身份验证机制,并使用您选择的几种密码对所有流量进行加密。 即使有人嗅探您的网络, ssh流量看起来也是如此。

    ssh最简单的用法是ssh hostname 此命令连接到主机名,并为您提供登录名和密码提示。 提供正确的凭据,您将处于:

    (www.joe.com) $ ssh web.example.com
    Login: arthur
    Password: ******
    ( web.example.com) $

    如果只想在远程系统上运行一个命令,则无需登录。只需将命令作为ssh的参数提供即可。 例如, 清单3中显示的命令在远程计算机上运行hostname -a -v

    清单3.使用ssh在远程系统上运行命令
    (www.joe.com) $ ssh db.linux-mag.com hostname -a -v
    Login: vogon
    Password: ******
    db
    gethostname()=`db.linux-mag.com'
    Resolving `db.linux-mag.com' ...
    Result: h_name=`db.linux-mag.com'
    Result: h_aliases=`db'
    Result: h_addr_list=`64.34.170.230'

    ssh打开了与db.linux-mag.com的连接,并将hostname -a -v参数传递给远程计算机,该计算机运行命令并将输出返回给本地计算机。

    ssh还提供了一种将文件和整个目录从一台计算机复制到另一台计算机的便捷方法。 scp几乎和cp一样容易使用。 这是一个例子:

    (www.joe.com) $ scp -p -r ~/myproject web.example.com:

    此命令将〜/ myproject目录复制到web.example.com。 如果省略目标路径名,则文件将复制到您的主目录。 -p选项保留所有文件的日期和时间戳,而-r启用递归模式,其中scp下降并复制所有子目录。

    顺便说一句,前面的scp命令等效于运行:

    (www.joe.com) $ tar czf - ~/myproject | ssh www.example.com tar xvzf - 
    Login: deepthought
    Password: ******

    是的,您可以将本地命令的输出通过管道传输到远程命令(反之亦然)。

    您可能已经厌倦了所有这些密码提示。 同样,重复的提示只会减慢工作速度并阻止自动化。 您可能还会厌倦一遍又一遍地键入长主机名。 幸运的是, ssh支持公用或专用密钥身份验证以及系统别名。

    让我们使用DSA加密方案设置公共或私有密钥对。 为此,您必须生成密钥对,将公共密钥复制到远程系统,将其添加到已知密钥列表中,并验证一切正常,如清单4所示。

    清单4.创建和安装公钥或私钥
    1 $ cd ~
    2 $ mkdir .ssh
    3 $ chmod 700 .ssh
    4 $ cd .ssh
    5 $ ssh-keygen -t dsa 
    Generating public/private dsa key pair.
    Enter file in which to save the key (/home/mstreicher/.ssh/id_dsa): ./id_dsa
    Enter passphrase (empty for no passphrase): 
    Enter same passphrase again: 
    Your identification has been saved in ./id_dsa.
    Your public key has been saved in ./id_dsa.pub.
    The key fingerprint is:
    40:6c:26:e7:53:df:d1:7b:c4:79:c5:a8:cd:6b:fe:8e mstreicher@db.linux-mag.com
    6 $ ls
    id_dsa  id_dsa.pub
    7 $ chmod 600 *
    
    8 $ scp id_dsa.pub www.example.com:
    Login: marvin
    Password: ******
    id_dsa  100%  668     0.7KB/s   00:00  
    
    9 $ ssh www.example.com
    Login: marvin
    Password: ******
    A $ mkdir .ssh
    B $ chmod 700 .ssh
    C $ cd .ssh
    D $ cat ../id_dsa.pub >> authorized_keys
    E $ rm ../id_dsa.pub
    F $ chmod 600 *
    G $ logout
    
    10 $ ssh www.example.com
    
    a $ hostname
    www.example.com
    b $ logout

    命令1到3在您的主目录中创建一个名为.ssh的私有本地目录。 该目录必须为模式700 ,否则ssh将不使用公共或私有密钥身份验证。 (您可以在步骤A到C中看到在远程计算机上运行的相同命令序列。)命令5使用DSA创建密钥对。 现在,将两个密码短语留空。 (它们提供了更高级别的安全性,但增加了另一个认证步骤。) ssh-keygen生成两个文件:id_dsa(私钥)和id_dsa.pub(公钥)。 步骤6显示文件,而步骤7保护两个密钥。 您的密钥必须为模式0600或模式0400

    步骤8将公钥复制到远程计算机。 目前,您必须输入密码,但这是最后一次。 命令A到C创建私有.ssh目录,步骤D将公共密钥添加到授权密钥列表中。 文件名--authorized_keys-是有意的。 请勿使用其他名称。 步骤E删除密钥的副本; 与步骤7一样,步骤F保护文件。

    注销并重新登录时,不再需要密码。 ssh (以及scpsftp )针对远程公钥测试您的私钥。 如果找到匹配项,则您的凭据是正确的,并且无需进一步标识即可登录。

    有些系统将始终要求输入密码。 其他系统可能更喜欢RSA,而不是DSA。 请与系统管理员联系,以了解如何登录到特定计算机。 系统管理员也可以设置一些全局设置,以使系统更易于访问。

    随时随地在线

    如今,互联网以人类历史上从未有过的方式将人们广泛地联系在一起。 无论是在博客中共享一天的详细信息,还是为下一个项目下载源代码,电线都已取代轮胎,成为了解决之道。

    网上冲浪仍然是一种流行的运动,但是为了腾出时间进行真正的网上冲浪,开发人员创建了自动进行各种文件传输的方法。 使用脚本和一些UNIX实用程序,您可以使外部Web保持最新并下载站点。 您只需敲击几下即可下载和上传文件,从而使该过程变得快速,简单。 而且,如果您创建.netrc文件,则可以进一步加快工作量。 没有更多密码。

    现在您已经清楚了,将其放下并在信息高速公路上进行公路旅行。 在光纤尽头的餐厅见。 那里的最后一个拿起了标签!


    翻译自: https://www.ibm.com/developerworks/aix/library/au-speakingunix3.html

    展开全文
  • XCache 是个开源的 opcode 缓存器/优化器, 这意味着他能够提高您服务器上的 PHP 性能. 他通过把编译 PHP 后的数据缓冲到共享内存从而避免重复的编译过程, 能够直接使用缓冲区已编译的代码从而提高速度. 通常能够...
  • 1.1 报错ORA-600 [kcratr_nab_less_than_odr],不能启动 1,现象描述:服务器存储断电,导致数据库down机,再次尝试启动数据库,数据库不能正常启动,数据库报错如下 SQL> alter database open; alter database ...
  • 一次不成功的尝试,使用WEBBROWSER控件显示SVG项目中需要在窗口应用程序中显示和操作SVG,我想到的有三种方法:1,自行开发和实现对SVG的显示和操作这种方法需要工作比较多,涉及对SVG标准的理解,根据应用的需要选择...
  • 手机首次充电的正确方法

    千次阅读 2007-10-13 10:36:00
    也许是由于手机电池刚刚完成了一次镍电池到锂电池的革命,所以人们对锂电池的认识并不统一,在许多情况下不正确的说法和做法颇为流行。因此,懂得一点锂电池的知识,掌握锂电池的正确使用方法是非常有必要的。 一、...
  • 怎样选择和创建比特币钱包

    千次阅读 2018-02-26 14:44:06
    许多刚关注币圈的小伙伴开始蠢蠢欲动啦,今天就继续比特币基础知识介绍:钱包的选择和创建。 比特币钱包的选择 现在比特币可选择的钱包实在太多了,多到老韭菜都完全蒙圈。实际上,对刚入门的新手来说,其实不...
  • 章Introduction由西安交通大学常象宇博士主讲,深入浅出的介绍了机器学习的基本概念、学习理论、模型选择、维灾等。
  • D:可行性正确答案: C 知识点: 算法的五大特性:有穷 确切 输入 输出 可行(有效) 1、有穷性(Finiteness) 算法的有穷性是指算法必须能在执行有限个步骤之后终止 2、确切性(Definiteness) 算法
  • 也许是由于手机电池刚刚完成了一次镍电池到锂电池的革命,所以人们对锂电池的认识并不统一,在许多情况下不正确的说法和做法颇为流行。因此,懂得 一点锂电池的知识,掌握锂电池的正确使用方法是非常有
  • Matlab导出高清图片的正确方法

    万次阅读 2016-07-24 20:08:33
    将来需要细致修改时,可以方便打开并二导出。   (4) print 是日常绘图的首选。因为它对格式、分辨率等各项指标进行非常细致的定义,适合直接出图。 (END) 1 你一定要知道的十...
  • 正确配置安装和卸载Cygwin

    万次阅读 2011-05-10 16:13:00
    如果第一次启动,Cygwin会在你的安装目录下(c:/cygwin)目录下生成一个home目录,并在home目录下建立一个和你的windows账户名一样的一个文件夹,比如文件夹名为yourid。并且会在这个文件夹下(c:/cygwin/home/yourid...
  • 引用: “在许多医院的眼科中心,为...” “手术后……朋友如愿地摘掉了那副850度的眼镜,然而就在个月前,我那位朋友发觉自己的视力竟大幅回退,且有大量不规则散光。……最后确诊为圆锥角膜。圆锥角膜的最终结果
  • 许多音频行业内的系统工程师,对于系统中专业的后级功放如何正确设置音量旋钮的位置都存在一些问题,各种理论应有尽有,一套系统中无论配置什么音箱、功放、调音台、及周边器材,最终功放的音量旋钮全部开到最大或者...
  • prometheus二开发之HTTP api(

    千次阅读 2020-02-12 19:42:17
    Prometheus在/api/v1的路径下开放了...若API请求成功 返回个2xx的状态码。 若请求失败,分情况返回以下状态码: 400 Bad Request 参数丢失或不正确; 422 Unprocessable Entity无法执行表达式时; 503 Service...
  • 加盐hash保存密码的正确方式(上)

    千次阅读 2015-10-17 09:34:20
    0x00 背景 ...账号系统最重要的个方面就是如何保护用户的密码。一些大公司的用户数据库泄露事件也时有发生,所以我们必须采取一些措施来保护用户的密码,即使网站被...对密码进行hash操作是件很简单的事情,但是很
  • 项目在选择服务器的时候,最终领导确定的是自购服务器,为了确保工作顺利,刚好手上有台新电脑,我开始从零配置服务器之路。一路遭遇的坎坷 ,难以形容。可能用多了阿里云的web界面之后,人的水平也下降很多。 Access ...
  • 操作系统选择题题库

    万次阅读 2013-10-13 10:26:36
    1.以下有关操作系统的叙述中,哪个是不正确的? (D) A.操作系统管理系统中的各种资源 B.操作系统为用户提供的良好的界面 C.操作系统就是资源的管理者和仲裁者 D.操作系统是计算机系统中的个应 用软件 ...
  • 审稿的时候,不止一次,我遇到作者 错误使用 数据集合跑模型准确率,并和他人成果比较的情况。 他们的研究创意有的很新颖,应用价值较高,工作可能也做了着实不少。 但因对比方法错误,得出来的结果,不具备说服力。...
  • 1.同个消费者连续订阅某个 Topic下的2个不同TagA和TagB,以下描述正确的是 A.订阅TagB的时候会返回错误 B.最终只会收到TagB的消息 C.最终只会收到TagA的消息 D.会收到TagA和TagB的消息 2.EDAS目前无法进行的监控有...
  • 牛客网选择题100题

    千次阅读 2015-12-06 21:08:01
    2 声明个指向含有10个元素的数组的指针,其中每个元素是个函数指针,该函数的返回值是int,参数是int*,正确的是() int ((int *)[10])*p3 任何个非空广义表其表头可能是原子,也可能是列表,而其表尾必定是...
  • 、单项选择题(共20题,每题1.5分,共计30分,每题有且仅有正确选项。)   1、 在二进制下,1011001+()=1100110。 A、1011 B、1101 C、1010 D、1111 B 送分的进制题=w= 2、字符“A”的ASCII码为十六...
  • 保险丝的作用原理及选择

    千次阅读 2018-01-18 10:46:27
    转载路径  保险丝介绍  保险丝(fuse)也被称为电流保险丝,IEC127标准将它定义为...百多年前由爱迪生发明的保险丝用于保护当时昂贵的白炽灯,随着时代的发展,保险丝保护电力设备不受过电流过热的伤害,避免电子设
  • 个文科生的工程师之路

    万次阅读 多人点赞 2017-06-15 00:31:16
    此图出,立即有人调侃:这位同学,看来你很适合做“程序员”啊,毕业记得来华为报到。 实际上,程序员真的是这样吗?对此,博主要说NO,毕业多年,接触的程序员上百人,这样的人有没有,有,但极少。为什么...
  • 第十一届蓝桥杯大赛软件类省赛 C/C++ 大学 B 组 ...对同一题目,选手可多次提交答案,以最后一次提交的答案为准。 选手必须通过浏览器方式提交自己的答案。选手在其它位置的作答或其它 方式提交的答案无效。
  • 我们密切注意当笔尖第一次接触纸张时墨水池的状态。随着压力的增加和减少,刷子的行程如何变宽。笔画结束如何终止。 以矢量形式仔细地重建这些特征。我们使用真实的商业艺术样本,使用画笔和笔尖笔创建的笔划,...
  • 600个教学学习资料链接(zz)

    千次阅读 2011-12-10 01:16:38
    “每日读图”首先让读者通过不同的难易程度选择新闻,然后阅读段有关该新闻的中文简介,对新闻的内容有一定了解后再开始阅读英文新闻,同时有新闻中较难词汇的中文解释。而“教您读新闻”栏目则是逐一分析在新闻...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 41,199
精华内容 16,479
关键字:

一次正确的选择600