[Android] Expand a RecyclerView in Four Steps

转自:https://www.bignerdranch.com/blog/expand-a-recyclerview-in-four-steps/?utm_source=Android+Weekly&utm_campaign=8f0cc3ff1f-Android_Weekly_165&utm_medium=email&utm_term=0_4eb677ad19-8f0cc3ff1f-337834121

The Expandable RecyclerView library is a lightweight library that simplifies the inclusion of expandable dropdown rows in your RecyclerView. In it, you have two types of views. The parent view is the one visible by default, and it contains the children. The child view is the one that is expanded or collapsed, and will appear when a parent item is clicked.

In this post, we will implement the Expandable RecyclerView in the CriminalIntent application from our Android programming guide. We’ll be showing a more detailed view of each crime from the main list fragment.

You can view the source code for the library and two samples on GitHub, or read the Javadocs.

Not familiar with RecyclerView? Bill Phillips has written two excellent blog posts on the subject.

Let’s Get it Working

Our completed demo will look like this:

Expandable CriminalIntent

Start by adding these two dependencies to your app’s build.gradle file:

dependencies {
    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.bignerdranch.android:expandablerecyclerview:1.0.3'
}

All expanding and collapsing functionality is handled in the adapter, meaning that your RecyclerView is just a stock RecyclerView. All you’ll need to do to set up the Expandable RecyclerView in a layout is add a stock RecyclerView to your activity or fragment’s XML layout.

1. The ViewHolders

First, let’s create the XML layouts for our parent and child views. Our parent layout is going to include the title of the crime and a dropdown arrow to display an animation when the item is expanded or collapsed. We’ll call this layout list_item_crime_parent.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#dbdbda">

    <TextView
        android:id="@+id/parent_list_item_crime_title_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textStyle="bold"
        android:text="Crime Title" />

    <ImageButton
        android:id="@+id/parent_list_item_expand_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_margin="8dp"
        android:src="@android:drawable/arrow_down_float" />

</RelativeLayout>

Now, onto the child layout. The child is going to contain a TextView that shows the date of the crime and a checkbox that we can click when the crime is solved. We’ll call this layout list_item_crime_child.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#cbcbcb">

    <TextView
        android:id="@+id/child_list_item_crime_date_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="Crime Date" />

    <CheckBox
        android:id="@+id/child_list_item_crime_solved_check_box"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="8dp"
        android:text="Solved" />

</RelativeLayout>

Now that we have both of our layouts ready, let’s set up each ViewHolder. We will create a class called CrimeParentViewHolder that extends ParentViewHolder and another class called CrimeChildViewHolder that extends ChildViewHolder.

Let’s start with CrimeParentViewHolder. Go ahead and create a public TextView and ImageButton variable for our two views, like so:

public class CrimeParentViewHolder extends ParentViewHolder {

    public TextView mCrimeTitleTextView;
    public ImageButton mParentDropDownArrow;

    public CrimeParentViewHolder(View itemView) {
        super(itemView);

        mCrimeTitleTextView = (TextView) itemView.findViewById(R.id.parent_list_item_crime_title_text_view);
        mParentDropDownArrow = (ImageButton) itemView.findViewById(R.id.parent_list_item_expand_arrow);
    }
}

Now, onto our CrimeChildViewHolder. We’re again going to create two public variables for our views, but this time one will be a TextView and one will be a CheckBox:

public class CrimeChildViewHolder extends ChildViewHolder {

    public TextView mCrimeDateText;
    public CheckBox mCrimeSolvedCheckBox;

    public CrimeChildViewHolder(View itemView) {
        super(itemView);

        mCrimeDateText = (TextView) itemView.findViewById(R.id.child_list_item_crime_date_text_view);
        mCrimeSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.child_list_item_crime_solved_check_box);
    }
}

Now let’s set up our parent and child objects.

2. Parents and Their Children

For our CriminalIntent example, the data that we want to display in our list items are fields of our Crime object. Since we have both a parent and a child layout, the best practice is to create a child object, separate from the parent, to hold the data that will be displayed in the child.

In this case, our parent object will be the Crime object itself. Let’s make our Crime object implement ParentObject. Implement the getter and setter methods with a list variable:

public class Crime implements ParentObject {

    /* Create an instance variable for your list of children */
    private List<Object> mChildrenList;

    /**
     * Your constructor and any other accessor
     *  methods should go here.
     */

    @Override
    public List<Object> getChildObjectList() {
        return mChildrenList;
    }

    @Override
    public void setChildObjectList(List<Object> list) {
        mChildrenList = list;
    }
}

The expandable RecyclerView allows for multiple children or none, so the children need to be added as a list. You can either add the children directly in the getChildObjectList method we implemented by returning your List of children, or you can add them when we create the list of items by calling setChildObjectList with the list of respective children. I will be doing the latter in this demo.

Our child object will hold two values, a String for the date and a boolean for the solved flag:

public class CrimeChild {

    private Date mDate;
    private boolean mSolved;

    public CrimeChild(Date date, boolean solved) {
        mDate = date;
        mSolved = solved;
    }

    /**
     * Getter and setter methods
     */
}

We’ll populate each parent object with children in the final step. Now, let’s get the adapter working!

3. The Adapter

We’re going need to implement a custom adapter that extends ExpandableRecyclerAdapter. Inside our adapter, we will implement a few methods so we can populate the data in our ViewHolders.

First, let’s create a class and call it CrimeExpandableAdapter. Make it extend ExpandableRecyclerAdapter and give it our two ViewHolder type parameters in the order of <parent view holder, child view holder>. Our header will look like this:

public class CrimeExpandableAdapter extends ExpandableRecyclerAdapter<CrimeParentViewHolder, CrimeChildViewHolder> {

You’ll need to implement the four inherited methods, onCreateParentViewHolder, onBindParentViewHolder, onCreateChildViewHolder and onBindChildViewHolder. Go ahead and implement the default constructor that will take in a Context and a List of ParentObjects.

In our constructor, let’s get access to the LayoutInflater and call it mInflater. This will be used in our onCreateParentViewHolder and onCreateChildViewHolder to inflate the layouts we created earlier. In our constructor, add mInflater = LayoutInflater.from(context);. This will create a layout inflater for us.

Now, let’s create our views and add them to the custom ViewHolders we made earlier. In onCreateParentViewHolder, let’s replace the return null line with these two lines:

View view = mInflater.inflate(R.layout.list_item_crime_parent, viewGroup, false);
return new CrimeParentViewHolder(view);

Let’s now do the same as we did in onCreateParentViewHolder, but adjust for our CrimeChildViewHolder. Again, replace return null with these two lines, but this time in onCreateChildViewHolder:

View view = mInflater.inflate(R.layout.list_item_crime_child, viewGroup, false);
return new CrimeChildViewHolder(view);

To finish off our adapter, let’s work on our onBindViewHolder methods, starting with onBindParentViewHolder. In onBindParentViewHolder, three variables are passed in: the viewholder we created in onCreateParentViewHolder, the position of the item and the parent object associated with that position. We’re going to need to cast the passed parent object to our parent object type (which, as you recall, is our Crime object). Let’s make our onBindParentViewHolder look like this:

public void onBindParentViewHolder(CrimeParentViewHolder crimeParentViewHolder, int i, Object parentObject) {
    Crime crime = (Crime) parentObject;
    crimeParentViewHolder.mCrimeTitleTextView.setText(crime.getTitle());
}

Finally, we will set up our CrimeChildViewHolder. Recall that our CrimeChildViewHolder contains a TextView for a date and a CheckBox to indicate whether the crime is solved. Our onBindChildViewHolder will look like this:

public void onBindChildViewHolder(CrimeChildViewHolder crimeChildViewHolder, int i, Object childObject) {
    CrimeChild crimeChild = (CrimeChild) childObject;
    crimeChildViewHolder.mCrimeDateText.setText(crimeChild.getDate().toString());
    crimeChildViewHolder.mCrimeSolvedCheckBox.setChecked(crimeChild.isSolved());
}

Now our adapter is ready to roll. Let’s finish this all up!

4. Tying it all together

Let’s head back to our main fragment, where we are hosting the RecyclerView. Make sure you find the RecyclerView in your layout and set its layout manager to a new LinearLayoutManager.

I went ahead and created a simple method to generate each of our Crime objects and attach their children. You can add this method in your Fragment:

private ArrayList<ParentObject> generateCrimes() {
    CrimeLab crimeLab = CrimeLab.get(getActivity());
    List<Crime> crimes = crimeLab.getCrimes();
    ArrayList<ParentObject> parentObjects = new ArrayList<>();
    for (Crime crime : crimes) {
        ArrayList<Object> childList = new ArrayList<>();
        childList.add(new CrimeChild(crime.getDate(), crime.isSolved()));
        crime.setChildObjectList(childList);
        parentObjects.add(crime);
    }
    return parentObjects;
}

Another option is to create a new list of children directly in your Crime object and set that to be the childrenList. A third option would be to create a list of children in getChildObjectList and return that list.

Now we can add the following lines to our onCreateView method:

CrimeExpandableAdapter mCrimeExpandableAdapter = new CrimeExpandableAdapter(getActivity(), generateCrimes());
mCrimeExpandableAdapter.setCustomParentAnimationViewId(R.id.parent_list_item_expand_arrow);
mCrimeExpandableAdapter.setParentClickableViewAnimationDefaultDuration();
mCrimeExpandableAdapter.setParentAndIconExpandOnClick(true);

Note that the list you pass into your adapter must be of the type ParentObject.

The setCustomParentAnimationView allows for the arrow in the parent layout to rotate on expand/collapse and setParentClickableAnimationDefaultDuration gives it a default rotation time of 200 milliseconds. setParentAndIconExpandOnClick ensures that we can click both the parent and the arrow to expand the item. You can always remove these if you don’t want an animation or custom triggering view.

Finally, set the RecyclerView’s adapter to finish it up:

mCrimeRecyclerView.setAdapter(mCrimeExpandableAdapter);

That’s it! You now should have a RecyclerView that expands and collapses and has a nice rotation animation to go along with it.

Want to improve it or see more?

The library is open source, so visit the project’s GitHub page to see all the source code, then send a pull request if there are new features you’d like to add.

*Ryan Brooks was an Android intern this summer. Apply now to join our team as an intern in Fall 2015.

[Android] 一个全能型Android开发者需要掌握的知识

从09年开始做Android已经7年了,这期间Android的发展和变化是很大的,特别是第三方库很多,而且已经形成了体系,个人要开发一个小的app比以前容易多了,直接调用别人写的好的库,再自己学着搭搭框架,很快一个app就完成了,但如果要想深入理解其中的原理,还是要认真的分析源码,不管是Android的还是第三方库的,最好理解其中的原理。

下面总结了下要开发一个“像样”的App,大概需要掌握的知识,后面还会更新:

%e5%bc%80%e5%8f%91%e6%b5%81%e7%a8%8b

 

 

[Android]Android视图架构详解

转自:http://blog.csdn.net/u012422440/article/details/51173387

最近一直在研究View的绘制相关的机制,发现需要补充一下Android View Architecture的相关知识,所以就特地研究了一下这方面的代码,写成本篇文章
为了节约你的时间,本篇文章内容大致如下:

  • ActivityDecorViewPhoneWindowViewRoot的作用和相关关系

Android View Architecture

先来几张图,大致展现一下Android 视图架构的大概。

感谢网友提醒,泛化和实现这两种关系的箭头画反啦。以后要仔细学一遍UML了,平时经常画,如果有错误可真是闹笑话啊。

Android View Architecture

View各类关系图

View树状图

Activity和Window

总所周知,Activity并不负责视图控制,它只是控制生命周期和处理事件,真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口,也就是说Activity可以没有Window,那就相当于是Service了。在ActivityThread中也有控制Service的相关函数或许正好印证了这一点。
ActivityWindow的第一次邂逅是在ActivityThread调用Activityattach()函数时。

//[window]:通过PolicyManager创建window,实现callback函数,所以,当window接收到
//外界状态改变时,会调用activity的方法,
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, String referrer, IVoiceInteractor voiceInteractor) {
    ....
    mWindow = PolicyManager.makeNewWindow(this);
    //当window接收系统发送给它的IO输入事件时,例如键盘和触摸屏事件,就可以转发给相应的Activity
    mWindow.setCallback(this);
    .....
    //设置本地窗口管理器
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    .....
}

attach()中,新建一个Window实例作为自己的成员变量,它的类型为PhoneWindow,这是抽象类Window的一个子类。然后设置mWindowWindowManager

Window,Activity和DecorView

DecorViewFrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android版本及主体有关),上面的是标题栏,下面的是内容栏。在Activity中通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,在代码中可以通过ViewGroup content = (ViewGroup)findViewById(R.android.id.content)来得到content对应的layout。
Window中有几个视图相关的比较重要的成员变量如下所示:

  • mDecor:DecorView的实例,标示Window内部的顶级视图
  • mContentParent:setContetView所设置的布局文件就加到这个视图中
  • mContentRoot:是DecorView的唯一子视图,内部包含mContentParent,标题栏和状态栏。

Activity中不仅持有一个Window实例,还有一个类型为ViewmDecor实例。这个实例和Window中的mDecor实例有什么关系呢?它又是什么时候被创建的呢?
二者其实指向同一个对象,这个对象是在Activity调用setContentView时创建的。我们都知道ActivitysetContentView实际上是调用了WindowsetContentView方法。

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) { //[window]如何没有DecorView,那么就新建一个
        installDecor(); //[window]
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ....
    //[window]第二步,将layout添加到mContentParent
    mLayoutInflater.inflate(layoutResID, mContentParent);
    .....
}

代码很清楚的显示了布局文件的视图是添加到mContentParent中,而且Window通过installDecor来新建DecorView

//[window]创建一个decorView
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); //直接new出一个DecorView返回
        ....
    }
    if (mContentParent == null) {
        //[window] 这一步也是很重要的.
        mContentParent = generateLayout(mDecor); //mContentParent是setContentVIew的关键啊
        .....
    }
    ....
}
protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.
    .......
    //[window] 根据不同的style生成不同的decorview啊
    View in = mLayoutInflater.inflate(layoutResource, null);
    // 加入到deco中,所以应该是其第一个child
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in; //给DecorView的第一个child是mContentView
    // 这是获得所谓的content 
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    }
    .....
    return contentParent;
}

从上述的代码中,我们可以清楚的看到mDecormContentParentmContentRoot的关系。
那么,Activity中的mDecor是何时被赋值的?我们如何确定它和Widnow中的mDecor指向同一个对象呢?我们可以查看ActivityThreadhandleResumeActivity函数,它负责处理Activityresume阶段。在这个函数中,Android直接将Window中的DecorView实例赋值给Activity

final Activity a = r.activity;
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;

Window,DecorView 和 ViewRoot

ViewRoot对应ViewRootImpl类,它是连接WindowManagerServiceDecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。ViewRoot可以被理解为“View树的管理者”——它有一个mView成员变量,它指向的对象和上文中WindowActivitymDecor指向的对象是同一个对象

我们来先看一下ViewRoot的创建过程。由于ViewRoot作为WindowMangerServiceDecorView的纽带,只有在WindowManager将持有DecorViewWindow添加进窗口管理器才创建。我们可以查看WindowMangerGlobal中的addView函数。对WindowManager不太熟悉的同学可以参考《Window和WindowManager解析》

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
        // 创建ViewRootImpl,然后将下述对象添加到列表中
    ....
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ....
    try {
        // 添加啦!!!!!!!!这是通过ViewRootImpl的setView来完成,这个View就是DecorView实例
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
      ....
    }
    ....
}

那么,Window是什么时候被添加到WindowManager中的呢?我们回到ActivityThreadhandleResumeActivity函数。我们都知道Activity的resume阶段就是要显示到屏幕上的阶段,在Activity也就是DecorView将要显示到屏幕时,系统才会调用addView方法。
我们在handleResumeActivity函数中找到了下面一段代码,它调用了ActivitymakeVisible()函数。

// ActivityThread
r.activity.makeVisible();

//Activity
    //[windows] DecorView正式添加并显示
    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

我们通过源代码发现,正式在makeVisible函数中,系统进行了Window的添加。

引用
http://wiki.jikexueyuan.com/project/deep-android-v1/surface.html
http://blog.csdn.net/guxiao1201/article/details/41744107
http://forlan.iteye.com/blog/2269381

[Android] View的三次measure,两次layout和一次draw

转自:http://blog.csdn.net/u012422440/article/details/52972825

我在《Android视图结构》这篇文章中已经描述了Activity,WindowView在视图架构方面的关系。前天,我突然想到为什么在setContentView中能够调用findViewById函数?View那时不是还没有被加载,测量,布局和绘制啊。然后就搜索了相关的条目,发现findViewById只需要在inflate结束之后就可以。于是,我整理了Activity生命周期和View的生命周期的关系,并再次做一下总结。

为了节约你的时间,本篇文章的主要内容为:
– Activity的生命周期和它包含的View的生命周期的关系
– Activity初始化时View为什么会三次measure,两次layout但只一次draw?
– ViewRoot的初始化过程

Activity的生命周期和View的生命周期

我通过一个简单的demo来验证Activity生命周期方法和View的生命周期方法的调用先后顺序。请看如下截图
截图

onCreate函数中,我们通常都调用setContentView来设置布局文件,此时Android系统就会读取布局文件,但是视图此时并没有加载到Window上,并且也没有进入自己的生命周期。
只有等到Activity进入resume状态时,它所拥有的View才会加载到Window上,并进行测量,布局和绘制。所以我们会发现相关函数的调用顺序是:

  • onResume(Activity)
  • onPostResume(Activity)
  • onAttachedToWindow(View)
  • onMeasure(View)
  • onMeasure(View)
  • onLayout(View)
  • onSizeChanged(View)
  • onMeasure(View)
  • onLayout(View)
  • onDraw(View)大家会发现,为什么onMeasure先调用了两次,然后再调用onLayout函数,最后还有在依次调用onMeasure,onLayoutonDraw函数呢?

ViewGroup的measure

大家应该都知道,有些ViewGroup可能会让自己的子视图测量两次。比如说,父视图先让每个子视图自己测量,使用View.MeasureSpec.UNSPECIFIED,然后在根据每个子视图希望得到的大小不超过父视图的一些限制,就让子视图得到自己希望的大小,否则就用其他尺寸来重新测量子视图。这一类的视图有FrameLayout,RelativeLayout等。
在《Android视图结构》中,我们已经知道Android视图树的根节点是DecorView,而它是FrameLayout的子类,所以就会让其子视图绘制两次,所以onMeasure函数会先被调用两次。

// FrameLayout的onMeasure函数,DecorView的onMeasure会调用这个函数。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    .....
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ......
        }
    }
    ........
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            ........
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

你以为到了这里就能解释通View初始化时的三次measure,两次layout却只一次draw吗?那你就太天真了!我们都知道,视图结构中不仅仅是DecorViewFrameLayout,还有其他的需要两次measure子视图的ViewGroup,如果每次都导致子视图两次measure,那效率就太低了。所以Viewmeasure函数中有相关的机制来防止这种情况。


public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ......
  // 当FLAG_FORCE_LAYOUT位为1时,就是当前视图请求一次布局操作
  //或者当前当前widthSpec和heightSpec不等于上次调用时传入的参数的时候
  //才进行从新绘制。
    if (forceLayout || !matchingSize &&
            (widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec)) {
            ......
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            ......
    }
    ......
}

源码看到这里,我几乎失望的眼泪掉下来!没办法,只能再想其他的方法来分析这个问题。

断点调试大法好

为了分析函数调用的层级关系,我想到了断点调试法。于是,我果断在onMeasureonLayout函数中设置了断点,然后进行调试。

函数调用帧

在《Android视图架构》一文中,我们知道ViewRootDecorView的父视图,虽然它自己并不是一个View,但是它实现了ViewParent的接口,Android正是通过它来实现整个视图系统的初始化工作。而它的performTraversals函数就是视图初始化的关键函数。
对比两次onMeasure被调用时的函数调用帧,我们可以轻易发现ViewRootImplperformTraversals函数中直接和间接的调用了两次performMeasure函数,从而导致了View最开始的两次measure过程。
然后在相同的performTraversals函数中会调用performLayout函数,从而导致View进行一轮layout过程。
但是为什么这次performTraversals并没有触发View的draw过程呢?反而是View又将重新进行一轮measure,layout过程之后才进行draw。

两次performTraversals

通过断点调试,我们发现在View初始化的过程中,系统调用了两次performTraversals函数,第一次performTraversals函数导致了View的前两次的onMeasure函数调用和第一次的onLayout函数调用。后一次的performTraversals函数导致了最后的onMeasure,onLayout,和onDraw函数的调用。但是,第二次performTraversals为什么会被触发呢?我们研究一下其源码就可知道。


private void performTraversals() {
    ......
    boolean newSurface = false;
    //TODO:决定是否让newSurface为true,导致后边是否让performDraw无法被调用,而是重新scheduleTraversals
    if (!hadSurface) {
        if (mSurface.isValid()) {
            // If we are creating a new surface, then we need to
            // completely redraw it.  Also, when we get to the
            // point of drawing it we will hold off and schedule
            // a new traversal instead.  This is so we can tell the
            // window manager about all of the windows being displayed
            // before actually drawing them, so it can display then
            // all at once.
            newSurface = true;
                    .....
        }
    }
            ......
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            ......
            performDraw();
        }
    } else {
        if (viewVisibility == View.VISIBLE) {
            // Try again
            scheduleTraversals();
        } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).endChangingAnimations();
            }
            mPendingTransitions.clear();
        }
    }
    ......
}

由源代码可以看出,当newSurface为真时,performTraversals函数并不会调用performDraw函数,而是调用scheduleTraversals函数,从而再次调用一次performTraversals函数,从而再次进行一次测量,布局和绘制过程。
我们由断点调试可以轻易看到,第一次performTraversals时的newSurface为真,而第二次时是假。
断点调试截图

总结

虽然我已经通过源码得知View初始化时measure三次,layout两次,draw一次的原因,但是Android系统设计时,为什么要将整个初始化过程设计成这样?我却还没有明白,为什么当Surface为新的时候,要推迟绘制,重新进行一轮初始化,这些可能都要涉及到Surface的相关内容,我之后要继续学习相关内容!

Android有用的代码集合

代码地址:https://github.com/Blankj/AndroidUtilCode/tree/master/utilcode/src/main/java/com/blankj/utilcode/utils

为方便查找,已进行大致归类,其目录如下所示:

  • Activity相关→ActivityUtils.java
    isActivityExists : 判断是否存在Activity
    launchActivity   : 打开Activity
  • App相关→AppUtils.java
    isInstallApp          : 判断App是否安装
    installApp            : 安装App(支持6.0)
    installAppSilent      : 静默安装App
    uninstallApp          : 卸载App
    uninstallAppSilent    : 静默卸载App
    launchApp             : 打开App
    getAppPackageName     : 获取App包名
    getAppDetailsSettings : 获取App具体设置
    getAppName            : 获取App名称
    getAppIcon            : 获取App图标
    getAppPath            : 获取App路径
    getAppVersionName     : 获取App版本号
    getAppVersionCode     : 获取App版本码
    getAppSignature       : 获取App签名
    getAppSignatureSHA1   : 获取应用签名的的SHA1值
    isSystemApp           : 判断App是否是系统应用
    isAppForeground       : 判断App是否处于前台
    getAppInfo            : 获取App信息
    getAppsInfo           : 获取所有已安装App信息
    cleanAppData          : 清除App所有数据
  • 栏相关→BarUtils.java
    setTransparentStatusBar : 设置透明状态栏(api大于19方可使用)
    hideStatusBar           : 隐藏状态栏
    getStatusBarHeight      : 获取状态栏高度
    isStatusBarExists       : 判断状态栏是否存在
    getActionBarHeight      : 获取ActionBar高度
    showNotificationBar     : 显示通知栏
    hideNotificationBar     : 隐藏通知栏
  • 清除相关→CleanUtils.java
    cleanInternalCache    : 清除内部缓存
    cleanInternalFiles    : 清除内部文件
    cleanInternalDbs      : 清除内部数据库
    cleanInternalDbByName : 根据名称清除数据库
    cleanInternalSP       : 清除内部SP
    cleanExternalCache    : 清除外部缓存
    cleanCustomCache      : 清除自定义目录下的文件
  • 剪贴板相关→ClipboardUtils.java
    copyText   : 复制文本到剪贴板
    getText    : 获取剪贴板的文本
    copyUri    : 复制uri到剪贴板
    getUri     : 获取剪贴板的uri
    copyIntent : 复制意图到剪贴板
    getIntent  : 获取剪贴板的意图
  • 关闭相关→CloseUtils.java
    closeIO        : 关闭IO
    closeIOQuietly : 安静关闭IO
  • 常量相关→ConstUtils.java
    MemoryConst : 存储相关常量
    TimeConst   : 时间相关常量
    RegexConst  : 正则相关常量
  • 转换相关→ConvertUtils.javaTest
    bytes2HexString, hexString2Bytes         : byteArr与hexString互转
    chars2Bytes, bytes2Chars                 : charArr与byteArr互转
    byte2Size, size2Byte                     : 字节数与unit为单位的size互转
    byte2FitSize                             : 字节数转合适大小
    bytes2Bits, bits2Bytes                   : bytes与bits互转
    input2OutputStream, output2InputStream   : inputStream与outputStream互转
    inputStream2Bytes, bytes2InputStream     : inputStream与byteArr互转
    outputStream2Bytes, bytes2OutputStream   : outputStream与byteArr互转
    inputStream2String, string2InputStream   : inputStream与string按编码互转
    outputStream2String, string2OutputStream : outputStream与string按编码互转
    bitmap2Bytes, bytes2Bitmap               : bitmap与byteArr互转
    drawable2Bitmap, bitmap2Drawable         : drawable与bitmap互转
    drawable2Bytes, bytes2Drawable           : drawable与byteArr互转
    view2Bitmap                              : view转Bitmap
    dp2px, px2dp                             : dp与px互转
    sp2px, px2sp                             : sp与px互转
  • 崩溃相关→CrashUtils.java
    getInstance : 获取单例
    init        : 初始化
  • 设备相关→DeviceUtils.java
    isRoot          : 判断设备是否root
    getSDKVersion   : 获取设备系统版本号
    getAndroidID    : 获取设备AndroidID
    getMacAddress   : 获取设备MAC地址
    getManufacturer : 获取设备厂商,如Xiaomi
    getModel        : 获取设备型号,如MI2SC
  • 判空相关→EmptyUtils.javaTest
    isEmpty    : 判断对象是否为空
    isNotEmpty : 判断对象是否非空
  • 编码解码相关→EncodeUtils.javaTest
    urlEncode                         : URL编码
    urlDecode                         : URL解码
    base64Encode, base64Encode2String : Base64编码
    base64Decode                      : Base64解码
    base64UrlSafeEncode               : Base64URL安全编码
    htmlEncode                        : Html编码
    htmlDecode                        : Html解码
  • 加密解密相关→EncryptUtils.javaTest
    encryptMD2, encryptMD2ToString                         : MD2加密
    encryptMD5, encryptMD5ToString                         : MD5加密
    encryptMD5File, encryptMD5File2String                  : MD5加密文件
    encryptSHA1, encryptSHA1ToString                       : SHA1加密
    encryptSHA224, encryptSHA224ToString                   : SHA224加密
    encryptSHA256, encryptSHA256ToString                   : SHA256加密
    encryptSHA384, encryptSHA384ToString                   : SHA384加密
    encryptSHA512, encryptSHA512ToString                   : SHA512加密
    encryptHmacMD5, encryptHmacMD5ToString                 : HmacMD5加密
    encryptHmacSHA1, encryptHmacSHA1ToString               : HmacSHA1加密
    encryptHmacSHA224, encryptHmacSHA224ToString           : HmacSHA224加密
    encryptHmacSHA256, encryptHmacSHA256ToString           : HmacSHA256加密
    encryptHmacSHA384, encryptHmacSHA384ToString           : HmacSHA384加密
    encryptHmacSHA512, encryptHmacSHA512ToString           : HmacSHA512加密
    encryptDES, encryptDES2HexString, encryptDES2Base64    : DES加密
    decryptDES, decryptHexStringDES, decryptBase64DES      : DES解密
    encrypt3DES, encrypt3DES2HexString, encrypt3DES2Base64 : 3DES加密
    decrypt3DES, decryptHexString3DES, decryptBase64_3DES  : 3DES解密
    encryptAES, encryptAES2HexString, encryptAES2Base64    : AES加密
    decryptAES, decryptHexStringAES, decryptBase64AES      : AES解密
  • 文件相关→FileUtils.javaTest
    getFileByPath             : 根据文件路径获取文件
    isFileExists              : 判断文件是否存在
    isDir                     : 判断是否是目录
    isFile                    : 判断是否是文件
    createOrExistsDir         : 判断目录是否存在,不存在则判断是否创建成功
    createOrExistsFile        : 判断文件是否存在,不存在则判断是否创建成功
    createFileByDeleteOldFile : 判断文件是否存在,存在则在创建之前删除
    copyDir                   : 复制目录
    copyFile                  : 复制文件
    moveDir                   : 移动目录
    moveFile                  : 移动文件
    deleteDir                 : 删除目录
    deleteFile                : 删除文件
    listFilesInDir            : 获取目录下所有文件
    listFilesInDir            : 获取目录下所有文件包括子目录
    listFilesInDirWithFilter  : 获取目录下所有后缀名为suffix的文件
    listFilesInDirWithFilter  : 获取目录下所有后缀名为suffix的文件包括子目录
    listFilesInDirWithFilter  : 获取目录下所有符合filter的文件
    listFilesInDirWithFilter  : 获取目录下所有符合filter的文件包括子目录
    searchFileInDir           : 获取目录下指定文件名的文件包括子目录
    writeFileFromIS           : 将输入流写入文件
    writeFileFromString       : 将字符串写入文件
    getFileCharsetSimple      : 简单获取文件编码格式
    getFileLines              : 获取文件行数
    readFile2List             : 指定编码按行读取文件到List
    readFile2SB               : 指定编码按行读取文件到StringBuilder中
    getFileSize               : 获取文件大小
    getFileMD5                : 获取文件的MD5校验码
    getDirName                : 根据全路径获取最长目录
    getFileName               : 根据全路径获取文件名
    getFileNameNoExtension    : 根据全路径获取文件名不带拓展名
    getFileExtension          : 根据全路径获取文件拓展名
  • 图片相关→ImageUtils.java
    bitmap2Bytes, bytes2Bitmap       : bitmap与byteArr互转
    drawable2Bitmap, bitmap2Drawable : drawable与bitmap互转
    drawable2Bytes, bytes2Drawable   : drawable与byteArr互转
    getBitmap                        : 获取bitmap
    scale                            : 缩放图片
    clip                             : 裁剪图片
    skew                             : 倾斜图片
    rotate                           : 旋转图片
    getRotateDegree                  : 获取图片旋转角度
    toRound                          : 转为圆形图片
    toRoundCorner                    : 转为圆角图片
    fastBlur                         : 快速模糊
    renderScriptBlur                 : renderScript模糊图片
    stackBlur                        : stack模糊图片
    addFrame                         : 添加颜色边框
    addReflection                    : 添加倒影
    addTextWatermark                 : 添加文字水印
    addImageWatermark                : 添加图片水印
    toAlpha                          : 转为alpha位图
    toGray                           : 转为灰度图片
    save                             : 保存图片
    isImage                          : 根据文件名判断文件是否为图片
    getImageType                     : 获取图片类型
    compressByScale                  : 按缩放压缩
    compressByQuality                : 按质量压缩
    compressBySampleSize             : 按采样大小压缩
  • 意图相关→IntentUtils.java
    getInstallAppIntent         : 获取安装App(支持6.0)的意图
    getUninstallAppIntent       : 获取卸载App的意图
    getLaunchAppIntent          : 获取打开App的意图
    getAppDetailsSettingsIntent : 获取App具体设置的意图
    getShareTextIntent          : 获取分享文本的意图
    getShareImageIntent         : 获取分享图片的意图
    getComponentIntent          : 获取其他应用组件的意图
    getShutdownIntent           : 获取关机的意图
    getCaptureIntent            : 获取拍照的意图
  • 键盘相关→KeyboardUtils.java
    hideSoftInput                 : 动态隐藏软键盘
    clickBlankArea2HideSoftInput0 : 点击屏幕空白区域隐藏软键盘(注释萌萌哒)
    showSoftInput                 : 动态显示软键盘
    toggleSoftInput               : 切换键盘显示与否状态
    isShowSoftInput               : 判断键盘是否显示
  • 日志相关→LogUtils.javaTest
    init       : 初始化函数
    getBuilder : 获取LogUtils建造者
    v          : Verbose日志
    d          : Debug日志
    i          : Info日志
    w          : Warn日志
    e          : Error日志
  • 网络相关→NetworkUtils.java
    openWirelessSettings               : 打开网络设置界面
    isAvailable                        : 判断网络是否可用
    isConnected                        : 判断网络是否连接
    is4G                               : 判断网络是否是4G
    isWifiConnected                    : 判断wifi是否连接状态
    getNetworkOperatorName             : 获取移动网络运营商名称
    getPhoneType                       : 获取移动终端类型
    getNetWorkType, getNetWorkTypeName : 获取当前的网络类型(WIFI, 2G, 3G, 4G)
  • 手机相关→PhoneUtils.java
    isPhone           : 判断设备是否是手机
    getIMEI           : 获取IMIE码
    getIMSI           : 获取IMSI码
    getPhoneStatus    : 获取手机状态信息
    dial              : 跳至填充好phoneNumber的拨号界面
    call              : 拨打phoneNumber
    sendSms           : 发送短信
    getAllContactInfo : 获取手机联系人
    getContactNum     : 打开手机联系人界面点击联系人后便获取该号码(注释萌萌哒)
    getAllSMS         : 获取手机短信并保存到xml
  • 正则相关→RegexUtils.javaTest
    isMobileSimple : 验证手机号(简单)
    isMobileExact  : 验证手机号(精确)
    isTel          : 验证电话号码
    isIDCard15     : 验证身份证号码15位
    isIDCard18     : 验证身份证号码18位
    isEmail        : 验证邮箱
    isURL          : 验证URL
    isChz          : 验证汉字
    isUsername     : 验证用户名
    isDate         : 验证yyyy-MM-dd格式的日期校验,已考虑平闰年
    isIP           : 验证IP地址
    isMatch        : string是否匹配regex
  • 屏幕相关→ScreenUtils.java
    getDeviceWidth, getDeviceHeight                 : 获取手机分辨率
    setTransparentStatusBar                         : 设置透明状态栏(api大于19方可使用)
    hideStatusBar                                   : 隐藏状态栏(注释萌萌哒)
    getStatusBarHeight                              : 获取状态栏高度
    isStatusBarExists                               : 判断状态栏是否存在
    getActionBarHeight                              : 获取ActionBar高度
    showNotificationBar                             : 显示通知栏
    hideNotificationBar                             : 隐藏通知栏
    setLandscape                                    : 设置屏幕为横屏(注释萌萌哒)
    snapShotWithStatusBar, snapShotWithoutStatusBar : 获取屏幕截图
    isScreenLock                                    : 判断是否锁屏
  • SD卡相关→SDCardUtils.java
    isSDCardEnable : 判断SD卡是否可用
    getDataPath    : 获取SD卡Data路径
    getSDCardPath  : 获取SD卡路径
    getFreeSpace   : 计算SD卡的剩余空间
    getSDCardInfo  : 获取SD卡信息
  • 服务相关→ServiceUtils.java
    isRunningService : 获取服务是否开启
  • Shell相关→ShellUtils.java
    isRoot  : 判断设备是否root
    execCmd : 是否是在root下执行命令
  • 尺寸相关→SizeUtils.java
    dp2px, px2dp     : dppx转换
    sp2px, px2sp     : sppx转换
    applyDimension   : 各种单位转换
    forceGetViewSize : 在onCreate()即可强行获取View的尺寸
    measureView      : ListView中提前测量View尺寸(注释萌萌哒)
  • SP相关→SPUtils.javaTest
    SPUtils    : SPUtils构造函数
    putString  : SP中写入String类型value
    getString  : SP中读取String
    putInt     : SP中写入int类型value
    getInt     : SP中读取int
    putLong    : SP中写入long类型value
    getLong    : SP中读取long
    putFloat   : SP中写入float类型value
    getFloat   : SP中读取float
    putBoolean : SP中写入boolean类型value
    getBoolean : SP中读取boolean
    getAll     : SP中获取所有键值对
    remove     : SP中移除该key
    contains   : SP中是否存在该key
    clear      : SP中清除所有数据
  • 字符串相关→StringUtils.javaTest
    isEmpty          : 判断字符串是否为null或长度为0
    isSpace          : 判断字符串是否为null或全为空格
    null2Length0     : null转为长度为0的字符串
    length           : 返回字符串长度
    upperFirstLetter : 首字母大写
    lowerFirstLetter : 首字母小写
    reverse          : 反转字符串
    toDBC            : 转化为半角字符
    toSBC            : 转化为全角字符
    getPYFirstLetter : 获得第一个汉字首字母
    cn2PY            : 中文转拼音
  • 线程池相关→ThreadPoolUtils.java
    ThreadPoolUtils                               : ThreadPoolUtils构造函数
    execute                                       : 在未来某个时间执行给定的命令
    execute                                       : 在未来某个时间执行给定的命令链表
    shutDown                                      : 待以前提交的任务执行完毕后关闭线程池
    shutDownNow                                   : 试图停止所有正在执行的活动任务
    isShutDown                                    : 判断线程池是否已关闭
    isTerminated                                  : 关闭线程池后判断所有任务是否都已完成
    awaitTermination                              : 请求关闭、发生超时或者当前线程中断
    submit                                        : 提交一个Callable任务用于执行
    submit                                        : 提交一个Runnable任务用于执行
    invokeAll, invokeAny                          : 执行给定的任务
    schedule                                      : 延迟执行Runnable命令
    schedule                                      : 延迟执行Callable命令
    scheduleWithFixedRate, scheduleWithFixedDelay : 延迟并循环执行命令
  • 时间相关→TimeUtils.javaTest
    milliseconds2String                               : 将时间戳转为时间字符串
    string2Milliseconds                               : 将时间字符串转为时间戳
    string2Date                                       : 将时间字符串转为Date类型
    date2String                                       : 将Date类型转为时间字符串
    date2Milliseconds                                 : 将Date类型转为时间戳
    milliseconds2Date                                 : 将时间戳转为Date类型
    milliseconds2Unit                                 : 毫秒时间戳单位转换(单位:unit)
    getIntervalTime                                   : 获取两个时间差(单位:unit)
    getCurTimeMills, getCurTimeString, getCurTimeDate : 获取当前时间
    getIntervalByNow                                  : 获取与当前时间的差(单位:unit)
    isLeapYear                                        : 判断闰年
    getWeek, getWeekIndex                             : 获取星期
    getWeek, getWeekIndex                             : 获取星期
    getWeekOfMonth                                    : 获取月份中的第几周
    getWeekOfYear                                     : 获取年份中的第几周
  • 吐司相关→ToastUtils.java
    init               : 吐司初始化
    showShortToastSafe : 安全地显示短时吐司
    showLongToastSafe  : 安全地显示长时吐司
    showShortToast     : 显示短时吐司
    showLongToast      : 显示长时吐司
    cancelToast        : 取消吐司显示
  • 压缩相关→ZipUtils.javaTest
    zipFiles           : 批量压缩文件
    zipFile            : 压缩文件
    unzipFiles         : 批量解压文件
    unzipFile          : 解压文件
    unzipFileByKeyword : 解压带有关键字的文件
    getFilesPath       : 获取压缩文件中的文件路径链表
    getComments        : 获取压缩文件中的注释链表
    getEntries         : 获取压缩文件中的文件对象
  • 更新Log→update_log.md

做这份整理是想把它作为Android开发的小字典,当遇到一些琐碎问题时,不用再面向百度或者谷歌查询API的使用,费时费力,这里有的话,大家尽管撸走;同时也希望它能逐日壮大起来,期待大家的Star和完善,当然我也会一直更新发布版本和日志,为了方便大家导入,现已上传jcenter;其中很多代码也是汇四方之精华,谢谢前辈们的提供,当然最终还是要通过单元测试的,如有错误,请及时告之;开设QQ群提供讨论,群号:74721490,至于验证问题对大家来说肯定都是小case;最近在玩微博,玩的话向大家求个关注

Download


Gradle:

compile 'com.blankj:utilcode:1.3.0'

Proguard


-keep class com.blankj.utilcode.** { *; }
-keep classmembers class com.blankj.utilcode.** { *; }
-dontwarn com.blankj.utilcode.**

 

文/Blankj(简书作者)
原文链接:http://www.jianshu.com/p/72494773aace

RESTful Web服务学习

作者:蒜瓣
链接:https://zhuanlan.zhihu.com/p/21644769
来源:知乎
 

随着 REST 成为大多数 Web 和 Mobile 应用的默认选择,势必要对它的基本原理有所了解。

在它提出十多年后的今天,REST 已经成为最重要的 Web 应用技术之一。随着所有技术朝着 API 方向发展,它的重要性有可能持续快速地增长。每门主要编程语言现在已经包含构建 RESTful Web 服务的框架。同样地,Web 开发者和架构师对 REST 和 RESTful 服务有一个清晰的理解是很重要的。这篇教程解释了 REST 架构,然后研究使用它构建通用地基于API的任务的细节。

什么是 REST

REST 代表表述性状态转移(representational state transfer),它是一种网络化超媒体应用的架构风格。它主要是用于构建轻量级的、可维护的、可伸缩的 Web 服务。基于 REST 的服务被称为 RESTful 服务。REST 不依赖于任何协议,但是几乎每个 RESTful 服务使用 HTTP 作为底层协议。

RESTful 使用HTTP post(创建、更新)数据、读取数据、删除数据。使用HTTP实现CRUD(创建、读取、更新、删除)操作。

RESTful 服务特点:

每个系统都使用资源。这些资源可以是图片,视频文件,网页,商业信息,或者在基于计算机的系统中可以被代表的任何事物。服务的目的是提供一个窗口给客户端以便客户端能访问这些资源。服务架构师和开发人员想要这些服务变得易于实现、维护、扩展、伸缩。RESTful 架构允许这些,甚至更多。一般来说,RESTful 服务应该有下面的属性和特征,也就是我要详细描述的内容:

  • 模型表示(Representations)
  • 消息(Messages)
  • URIs
  • 一致接口(Uniform interface)
  • (无状态)Stateless
  • 资源之间的链接(Links between resources)
  • 缓存(Caching)

模型表示(Representations)

RESTful 服务的焦点在资源上和怎么提供对资源的访问。资源很容易被认为和OOP中的对象一样。一个资源能由其他资源组成。当设计一个系统的时候,第一件要做的事情是定义资源和决定资源之间的关系。这有点像设计数据库的第一步。定义实体和关系。

一旦我们定义了资源,接下来我们需要找到一种用于在系统中表示这些资源的方法。你可以使用任何格式来表示资源。REST 对此没有限制。

例如,根据你的需求,你可以决定使用 JSON 或者 XML。如果你在构建 Web 服务,此服务用于 Web 页面中的 AJAX 调用,那 JSON 是很好地选择。 XML 可以用来表示比较复杂的资源。例如一个被称为“Person”的资源可以表示如下:

列表1:资源的JSON 表示。

{
    "ID": "1",
    "Name": "M Vaqqas",
    "Email": "m.vaqqas@gmail.com",
    "Country": "India"
}

列表2:资源的XML 表示。

<Person>
	<ID>1</ID>
	<Name>M Vaqqas</Name>
	<Email>m.vaqqas@gmail.com</Email>
	<Country>India</Country>
</Person>

实际上,你可以使用不止一种的格式并且决定使用其中哪一种用于依赖于客户端类型或一些请求参数的响应。无论使用哪个格式,好的模型表示(representation )应该具有以下明显的特征:

  • 客户端和服务端应该能够理解这种模型表示(representation )的格式。
  • 模型表示(representation )应该能够完整的表示资源。如果需要表示部分资源,然后你需要考虑将资源分解成子资源。分割大资源到更小的资源同样允许你传递更小的表现。较小的模型表示(representation)意味着更少的时间来创建和传输。这也意味着更快的服务。
  • 模型表示(representation)应该能够互相链接资源。可以通过替换 URI 或者是唯一 ID。

消息(Messages)

客户端和服务端经由消息相互沟通。客户端发送请求到服务器,服务器使用响应答复。除了实际的数据,这些信息也包含一些关于消息的元数据。对于设计 RESTful 服务了解 HTTP 1.1的请求格式和响应格式是很重要的。

HTTP 请求

图1中展示了HTTP请求格式。

图1:HTTP 请求格式

<VERB> GET, PUT, POST, DELETE, OPTIONS等等 HTTP 方法的一种。

<URI> 资源的URI。操作将在这个 URI 上执行。

<HTTP Version> HTTP 版本,通常是“HTTP v1.1”。

<Request Header> 包含 header 键值对集合的元数据。这些设置包含消息的信息和发送者像客户端的类型,客户端支持的格式,消息体的格式类型,响应的缓存设置,和许多信息。

<Request Body> 是实际的消息内容。在 RESTful 服务中,这就是消息中资源表示的位置。

在 HTML 消息中没有标签和标识标记区块的开始或结束。

列表三是简单的 POST 请求消息,这个请求想要插入一条新的 Person 资源。

列表3:简单 POST 请求

POST http://MyService/Person/
Host: MyService
Content-Type: text/xml; charset=utf-8
Content-Length: 123
<?xml version="1.0" encoding="utf-8"?>
<Person>
  <ID>1</ID>
  <Name>M Vaqqas</Name>
  <Email>m.vaqqas@gmail.com</Email>
  <Country>India</Country>
</Person>

列表4:GET 请求

GET http://www.w3.org/Protocols/rfc2616/rfc2616.html HTTP/1.1
Host: www.w3.org
Accept: text/html,application/xhtml+xml,application/xml; …
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 …
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,hi;q=0.6

HTTP Response

图2展示了 HTTP 响应的格式:

图2:HTTP 响应格式。

服务器返回 <response code>,<response code>包含请求的状态。<response code>通常是三位数字的HTTP状态码(3-digit HTTP status code)。

<Response Header> 包含关于响应消息的元数据和设置。

<Response Body> 包含如果请求成功的模型表示(representation)。

列表5是我从清单三的请求中得到的真实响应。

列表5:真实的 GET 请求的响应。

HTTP/1.1 200 OK
Date: Sat, 23 Aug 2014 18:31:04 GMT
Server: Apache/2
Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT
Accept-Ranges: bytes
Content-Length: 32859
Cache-Control: max-age=21600, must-revalidate
Expires: Sun, 24 Aug 2014 00:31:04 GMT
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns='http://www.w3.org/1999/xhtml'>
<head><title>Hypertext Transfer Protocol -- HTTP/1.1</title></head>
<body>
...

响应码 200 OK 表示一切正常,并且消息体包含我请求的有效的模型表示(representation)。在这个例子中,模型表示(representation)是 HTML 文档,HTML 文档是通过在响应头中的 Content-Type 声明地。在这个消息中的 header 是不解自明地,但是我将会在接下来的文章中讨论他们中的一些。这有很多其他属性。你可以使用叫Fiddle的免费工具抓取调试这些HTTP请求和响应。

资源定位

REST 要求每个资源至少有一个 URI。 RESTful 服务使用人类可读的URIs层级目录来定位资源。URI 要做的工作是定义一个资源或资源集合。实际的操作由 HTTP 动作决定。URI 应该没有任何关于处理和动作的内容。这使我们能够调用相同的 URI 使用不同的 HTTP 动词来执行不同的操作。

假设我们有一个 person 的数据库并且我们希望通过服务器暴露给外部。Person 资源可以像下面这样被定位到:

MyService/Persons/1

此URL遵循格式:Protocol://ServiceName/ResourceType/ResourceID

对于构建良好的 URIs 这有些重要的推荐:

  • 使用复数名词命名你的资源。
  • 避免使用制造混乱的空格。使用_或者-代替。
  • URI 不区分大小写。为了更清晰我使用驼峰写法。你也可以使用全部小写的URIs。
  • 你也能够有你自己的约定,但是要在整个服务保持一致。确保你的客户端都知道这个约定。你的客户端 URIs 程序构建将更简单如果它们知道你遵循的资源层级和URI约定。
  • 好的 URI 是不会变更的。再决定服务的 URIs 之前要先思考思考。如果你需要改变资源的定位,不要放弃老的 URI。如果请求来自老的 URI,使用状态码300重定向客户端到新的location。
  • 避免使用动词命名你的资源直到你的资源是一个实际地操作或过程。动词更加适合操作的命名。例如,RESTful 服务不应该有 MyService/FetcthPerson/MyService/DeletePerson? 类似的 URI。

URI中的查询参数

前面的URI是用查询参数帮助构建的。

MyService/Persons?

查询参数方法运行良好而且 REST 不会阻止你使用查询参数。然而,这种方式有一些劣势:

  • 增加了复杂性,降低了可读性。如果你使用更多的参数问题会更加明显。
  • 像Google这样的搜索引擎爬网程序和索引器忽略uri查询参数。如果你正在进行Web开发,这是你的Web服务一部分很大的劣势,导致搜索引擎屏蔽。

查询参数的基本目的是提供参数给需要的数据项的操作。例如,如果你想要模型表示(presentation)格式由客户端决定,你可以通过参数实现像下面这样。

MyService/Persons/1?

MyService/Persons/1?

包含format和encoding参数的父子层级URI看上去逻辑上不正确因为它们没有这种关系。

MyService/Persons/1/jso

查询参数也允许可选参数。在URI中显然是不可能的。你仅仅应该在提供参数值给处理过程的时候使用。

统一接口

RESTful应该有统一接口。HTTP 1.1 提供了一系列方法,被称为动作。在这其中比较重要的动作是:

安全的操作是指对原始资源值不会产生影响的操作。列如,数学上的操作除以1就是安全的操作,因为无论多少次用1除一个数,原始数值都不会改变。幂等操作是指无论多少次执行都给出相同结果的操作。例如,数学上的乘以0就是幂等的,因为无论计算多少次结果都是零,结果都是一样的。类似的,一个安全的HTTP方法不会使服务器上的资源发生变化。一个幂等的HTTP方法无论执行多少次都会有相同的响应。把方法分类成安全和幂等的可以使客户端在不稳定的Web环境中再次触发相同的请求的结果变得更可预测。

在 Web 上 GET 可能是最流行的方法。它用来获取资源。

HEAD 仅返回响应头和空的响应体。这个方法可以用在你不需要全部的资源模型表示(representation)的时候。例如,HEAD 可以快速检测服务器上的资源是否存在。

OPTIONS 用于获取资源允许的操作。例如,思考下面的请求:

OPTIONS http://MyService/Persons/1 HTTP/1.1
HOST: MyService

在服务验证之后请求返回下面内容:

200 OK
Allow: HEAD, GET, PUT

第二行包含客户端可以使用的方法。

你应该仅仅出于它们的实际意义使用这些方法。例如:绝不要使用 GET 在服务器上创建或删除资源。如果你没这样做,将会扰乱你的客户端导致他们做出意外的操作。举例说明,然我们考虑下面的请求:

GET http://MyService/DeletePersons/1 HTTP/1.1
HOST: MyService

根据 HTTP 1.1 规范,GET 请求的目的是从服务器获取资源。但是它很容易实现一个删除 Person 的请求。这个请求也许运行的很棒,但是这不是RESTful 设计。换言之,使用 DELETE 方法来删除资源像下面这样:

DELETE http://MyService/Persons/1 HTTP/1.1
HOST: MyService

REST 建议统一接口,HTTP 提供了统一接口。然而,这由服务架构师和开发人员保持它的统一。

PUT 和 POST 的区别

对于这两个方法我提供了几乎相同的简短描述。这两个方法困扰着许多开发人员。所以让我们单独地讨论他们。

PUT 和 POST 的关键不同在于 PUT 是幂等的,而 POST 不是。

另一个不同,使用 PUT 你需要定义资源完整的 URI。这意味着客户端能构造资源的URI哪怕资源不存在于服务器上。客户端选择资源唯一的名字或 ID 是可能的。就像在服务器上创建一个用户需要客户端选择用户 ID。如果客户端不能猜测出资源完整的URI,你别无选择,只能使用 POST。

很明显,PUT 请求不会修改或创建超过一个资源,无论触发多少次(如果URI相同)。当资源存在时 PUT 和 POST 是没有区别的,都是更新已存在资源。第三个请求(POST MyService/Persons/)会在每次触发都创建资源。许多开发人员认为 REST 不允许 POST 被用于更新操作。然而,REST 并没有这样的限制。

无状态

RESTful 服务是无状态的并且不会为任何客户端保持状态。一个请求不应该依赖过去的请求,服务对待每个请求都是独立的。HTTP 是无状态协议的设计,你需要做一些额外的事情实现状态服务。使用当前的技术真的很容易实现状态服务。我们需要清楚的理解无状态和有状态设计以便我们避免误解。

无状态设计像这样:

Request1: GET MyService/Persons/1 HTTP/1.1

Request2: GET MyService/Persons/2 HTTP/1.1

每个请求都能被单独对待。

有状态设计,像这样:

Request1: GET MyService/Persons/1 HTTP/1.1

Request2: GET MyService/NextPerson HTTP/1.1

为了处理第二个请求,服务器需要记住客户端最后获取的 PersonID。换句话说,服务器需要记住当前状态————否则请求2无法处理。设计你的服务的方式是一个请求绝不要涉及前一个请求。无状态服务更容易集群,更容易维护,更容易伸缩。这样的服务提供了更好的响应时间,因为它们能容易的负载均衡。

资源之间的链接

资源模型表示(representation )可以包含其他资源的链接就像 HTML 页面包含到其他页面的链接一样。被服务返回的模型表示(representations )应该能驱动处理流就像网站的情况一样。当访问网站的时候,首先是索引页面,单击其中的一个链接跳转到另外一个页面等等。

让我们考虑下客户端请求一个包含许多其他资源的资源。替代输入所有资源,你可能会列出资源的链接。

例如,如果多个Person是Club的一部分,那么Club能像列表6中一样表示。

列表6:Club

<Club>        
	<Name>Authors Club</Name>
	<Persons>
		<Person>
			<Name>M. Vaqqas</Name>
			<URI>http://MyService/Persons/1</URI>
		</Person>
		<Person>
			<Name>S. Allamaraju</Name>
			<URI>http://MyService/Persons/12</URI>
		</Person>
	</Persons>
</Club> 

缓存

缓存是存贮生成结果的概念,使用存储结果替代在不久的将来重复的请求生成的结果。缓存可以在客户端、服务端或者他们之间的任何组件上完成,比如代理服务器。缓存是提升服务器性能的很棒的方法。但如果不妥善处理,会导致客户端使用失效的结果。

缓存可以由下面的HTTP头控制:

这些头部的值可以组合起来用在Cache-Control指令中来检查缓存结果是否有效。最通用的用于Cache-Control的指令如下:


服务决定这些头和指令的值是根据资源的特性。

文档化 RESTful 服务

RESTful 服务不必包含用于帮助客户端发现它的文档。因为URIs、链接、统一接口,在运行时极其容易发现 RESTful 服务。客户端仅需要简单地知道服务的基础地址,并且从这个地址客户端通过遍历资源正在使用的链接就能发现服务。OPTION 方法能被有效地用在发现服务的处理过程中。

这不意味着 RESTful 服务一点也不需要文档化。没有理由不文档化你的服务。你应该为开发人员和客户端文档化每个资源和URI。你可以用任何格式构建你的文档,但是它应该包含足够关于资源、URIs、可用方法的信息,和其他需要访问你的服务使用的信息。下面的 table 是我的 MyService 的简单文档。这个简单短小的文档包含了 MyService 的各方面并且足够开发一个客户端。

Service Name:MyService

Address: MyService/


你也可以文档化每个资源的模型表示(representations )并且提供一些简单的模型表示(representations )。

结论

开发轻量级的 Web 服务 REST 是极好的方法,容易实现,维护、暴露开放。HTTP 提供了卓越的接口来实现 RESTful 服务,比如统一接口和缓存。然而,那要开发人员正确地实现和利用这些特性。如果我们对基础有了正确地理解,那么使用现有的技术比如Python、.net、java实现REST服务是很容的。我希望这篇文章为你开始开发你自己的RESTful服务提供了足够的信息。

一个可以自动生成RESTful API的库,功能非常强大:github.com/Meituan-Dian

参考

rest.elkstein.org/2008/

http://www.drdobbs.com/web-development/restful-web-services-a-tutorial/240169069

github.com/wanbei/blog/