关于这个 setRecyclerListener函数在解决 listView滑出屏幕(包括向上滑出和向下滑出)处理相关UI操作或者释放相关资源,真的很好用,
比listView的setOnScrollListener事件的onScroll好用很多,
官网API解释这个方法是这样的:
public static interface AbsListView.RecyclerListener
A RecyclerListener is used to receive a notification whenever a View is placed inside the RecycleBin's scrap heap. This listener is used to free resources associated to Views placed in the RecycleBin.
在一个view被放进回收站的垃圾堆时,RecyclerListener被用来收到一个通知。这个监听器是用来释放 与该view(放进垃圾堆里面的view,在listView里面就是item视图) 相关的资源
AbsListView.RecycleBin
, AbsListView.setRecyclerListener(android.widget.AbsListView.RecyclerListener)
方法摘要 | |
---|---|
void |
onMovedToScrapHeap(View view) Indicates that the specified View was moved into the recycler's scrap heap. 表明一个具体的view被放进 回收站的垃圾堆里面了 |
方法详细信息 |
---|
void onMovedToScrapHeap(View view)
但是呢,仅仅从这里看去,会有一种误读的感觉,【这个本身是不做回收的,只有你自己手动回收才 会 回收资源的,】
AbsListView中有一个类,叫 RecyclerBin就是负责垃圾回收的,但是这个内,源码里面这么解释
RecycleBin mRecycler;
The data set used to store unused views that should be reused during the next layout to avoid creating new ones.
用于存储不用的view,以便在下个layout中使用来避免创建新的。
RecyclerBin是分两层来回收的,一层是activeView(在屏幕上的view),一层是ScrapView(老的view),ScrapView可能会再次被adapter使用的,
意思应该可以这么理解,虽然上面的函数 onMovedToScrapHeap里面说是给放到 回收站的垃圾堆里面,但是下次还是可以用的,有些资源还是需要你自己手动释放的,它只是捕捉到这件事,捕捉到这个item,捕捉到这个view了,
-----------------------------------怎么用 ?也是一个很重要的问题
1.什么时候开启很关键?
有两种用法:
第一种:如果使用的listView是系统自带的,那么就只好用 listView.setRecyclerListener开启监听,但是这种当listView变得复用的时候就比较蛋疼了,意思就是说如果这个需求在多个功能里面被使用了,这种做法就是挺麻烦的,因为你需要在很多地方加上这行代码。而且那么多功能不可能是一个人写的,有可能别人在listVIew的时候加上了,你再去添加将会出错,这对管理整个代码非常的不好
第二种:自己 定义一个ListView ,让他去继承系统的ListV iew ,这样在构造函数里面就写上这个注册函数,那就很方便了,以后大家用的时候,基本不用再关心开启监听时间这件事,只需要在 自定义中的去修改onMovedToScrapHeap 里面的做法,其实就是增加if语句,把你自己的可能情况加上,多好,
2.在void onMovedToScrapHeap(View view)主要干什么?
这个很好理解的,参数view其实就是item对应的那个view,相信使用过listView的都知道,在adapter里面有个getView,每一个view都可能是这个函数的参数,那么你可以做的处理就太多了,
可以直接得到这个view的副本,调用view的相关函数,如果这个view是自定义的,那就更好了,只要是public方法,这个引用对象都是可以调用的,非常的舒服,
其次,你也可以通过这个view 利用findViEWbyid方法,找到你只想处理的那个UI控件,
还可以从view拿到tag,通过tag拿到绑定的对象,进而去处理数据,
反正你能做的事简直太多了,
接下来就是需要分析ListView的回收机制了:
首先理清listview的层级关系,
使用Google Online Draw 画出继承关系图如下:
图中单独画出Scrollview是为了说明该ViewGroup并没有自带回收机制,如果要是Scrollview显示大量view,需要手动做处理。
重要的类有三个:Listview、AbsListView、AdapterView。各个类的大小如下:
从Listview开始, ListView的初始化ListVIew.onLayout过程与普通视图的layout过程不同,流程图如下。从左向右,从上向下。
视图的创建过程的都会执行的三个步骤: onMeasure, onLayout, onDraw
图中可以看出重要的类有三个:Listview、AbsListView、AdapterView。主要的回收类RecycleBin位于AbsListView中。
位于AbsListView中,6466-6900行。
AbsListView的源码中可以看到有个RecycleBin 对象mRecycler。(317行, The data set used to store unused views that should be reused during the next layout to avoid creating new ones. 用于存储不用的view,以便在下个layout中使用来避免创建新的。)注释说明:
The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the start of a layout. By construction, they are displaying current information. At the end of layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that could potentially be used by the adapter to avoid allocating views unnecessarily.
大意是使用两级view来进行回收:
:激活view,当前显示在屏幕上的激活view。
:废弃view,被删除的ActiveView会被自动加入ScrapView。
然后看看RecycleBin内部的重要的变量和方法:
: 当发生View回收时,mRecyclerListener若有注册,则会通知给注册者.RecyclerListener接口只有一个函数onMovedToScrapHeap,指明某个view被回收到了scrap heap. 该view不再被显示,任何相关的昂贵资源应该被丢弃。该函数是处理回收时view中的资源释放。
:The position of the first view stored in mActiveViews. 存储在mActiveViews中的第一个view的位置,即getFirstVisiblePosition。
: Views that were on screen at the start of layout. This array is populated at the start of layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. Views in mActiveViews represent a contiguous range of Views, with position of the first view store in mFirstActivePosition.布局开始时屏幕显示的view,这个数组会在布局开始时填充,布局结束后所有view被移至mScrapViews。
:ArrayList<View>[] Unsorted views that can be used by the adapter as a convert view.可以被适配器用作convert view的无序view数组。 这个ArrayList就是adapter中getView方法中的参数convertView的来源。注意:这里是一个数组,因为如果adapter中数据有多种类型,那么就会有多个ScrapViews。
:view类型总数,列表中可能有多种数据类型,比如内容数据和分割符。
:跟mScrapViews的却别是,mScrapViews是个队列数组,ArrayList<View>[]类型,数组长度为mViewTypeCount,而默认ViewTypeCount = 1的情况下mCurrentScrap=mScrapViews[0]。
下面三个参数分别对应addScrapView中scrapHasTransientState的三个情况
():为每个子类调用forceLayout()。将mScrapView中回收回来的View设置一样标志,在下次被复用到ListView中时,告诉viewroot重新layout该view。forceLayout()方法只是设置标志,并不会通知其parent来重新layout。
():判断给定的view的viewType指明是否可以回收回。viewType < 0是不可以回收。指定忽略的( ITEM_VIEW_TYPE_IGNORE = -1),或者是 HeaderView / (FootViewITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2)是不被回收的。如有特殊需要可以将自己定义的viewType设置为-1,否则,将会浪费内存,导致OOM。
() :Clears the scrap heap.清空废弃view堆,并将这些View从窗口中Detach。
(int childCount, int firstActivePosition):Fill ActiveViews with all of the children of the AbsListView. childCount:The minimum number of views mActiveViews should hold. firstActivePosition:The position of the first view that will be stored in mActiveViews.用AbsListView.的所有子view填充ActiveViews,其中childCount是mActiveViews应该保存的最少的view数,firstActivePosition是mActiveViews中存储的首个view的位置。从代码看该方法的处理逻辑为将当前AbsListView的0-childCount个子类中的非header、footer类添加到mActiveViews数组中。当Adapter中的数据个数未发生变化时,此时用户可能只是滚动,或点击等操作,ListView中item的个数会发生变化,因此,需要将可视的item加入到mActiveView中来管理。
(int position):Get the view corresponding to the specified position. The view will be removed from mActiveViews if it is found. 获取mActiveViews中指定位置的view,如果找到会将该view从mActiveViews中移除。position是adapter中的绝对下标值,mFirstActivePosition前面说过了,是当前可视区域的下标值,对应在adapter中的绝对值,如果找到,则返回找到的View,并将mActiveView对应的位置设置为null。
:Dump any currently saved views with transient state.清掉当前处于transient(转换)状态的所有保存的view。内部为mTransientStateViews和mTransientStateViewsById的clear()调用。
(View scrap, int position):将view放入scrapview list中。If the list data hasn't changed or the adapter has stable IDs, views with transient state will be preserved for later retrieval. scrap:要添加的view。Position:view在父类中的位置。放入时位置赋给scrappedFromPosition 。有transient状态的view不会被scrap(废弃),会被加入mSkippedScrap。
就是将移出可视区域的view,设置它的scrappedFromPosition,然后从窗口中detach该view,并根据viewType加入到mScrapView中。
该方法会调用mRecyclerListener 接口的函数onMovedToScrapHeap(6734)
if (mRecyclerListener != null) { mRecyclerListener.onMovedToScrapHeap(scrap); } |
mRecyclerListener的设置可通过AbsListView的setRecyclerListener方法。
当view被回收准备再利用的时候设置要通知的监听器, 可以用来释放跟view有关的资源。这点似乎很有用。
public void setRecyclerListener(RecyclerListener listener) { mRecycler.mRecyclerListener = listener; } |
(int position) :A view from the ScrapViews collection. These are unordered.该方法中调用了retrieveFromScrap(ArrayList<View> scrapViews, int position)。
(ArrayList<View> scrapViews, int position):无注释。(6902)该方法属于AbsListView。
int size = scrapViews.size(); if (size > 0) { // See if we still have a view for this position. for (int i=0; i<size; i++) { View view = scrapViews.get(i); if (((AbsListView.LayoutParams)view.getLayoutParams()) .scrappedFromPosition == position) { scrapViews.remove(i); return view; } } return scrapViews.remove(size - 1); } else { return null; } |
其中scrappedFromPosition :The position the view was removed from when pulled out of the scrap heap.(6412)根据position,从mScrapView中找:
1. 如果有view.scrappedFromPosition = position的,直接返回该view;
2. 否则返回mScrapView中最后一个;
3. 如果缓存中没有view,则返回null;
a. 第三种情况,这个最简单:
一开始,listview稳定后,显示N个,此时mScrapView中是没有缓存view的,当我们向上滚动一小段距离(第一个此时仍显示部分),新的view将会显示,此时listview会调用Adapter.getView,但是缓存中没有,因此convertView是null,所以,我们得分配一块内存来创建新的convertView;
b. 第二种情况:
在a中,我们继续向上滚动,直接第一个view完全移出屏幕(假设没有新的item),此时,第一个view就会被detach,并被加入到mScrapView中;然后,我们还继续向上滚动,直接后面又将要显示新的item view时,此时,系统会从mScrapView中找position对应的View,显然,是找不到的,则将从mScrapView中,取最后一个缓存的view传递给convertView;
c. 第一种情况:
紧接着在b中,第一个被完全移出,加入到mScrapView中,且没有新增的item到listview中,此时,缓存中就只有第一个view;然后,我此时向下滑动,则之前的第一个item,将被显示出来,此时,从缓存中查找position对应的view有没有,当然,肯定是找到了,就直接返回了。
:Finish the removal of any views that skipped the scrap heap.清空mSkippedScrap。
():Move all views remaining in mActiveViews to mScrapViews. 将mActiveViews 中剩余的view放入mScrapViews。实际上就是将mActiveView中未使用的view回收(因为,此时已经移出可视区域了)。会调用mRecyclerListener.onMovedToScrapHeap(scrap);
():确保mScrapViews 的数目不会超过mActiveViews的数目 (This can happen if an adapter does not recycle its views)。mScrapView中每个ScrapView数组大小不应该超过mActiveView的大小,如果超过,系统认为程序并没有复用convertView,而是每次都是创建一个新的view,为了避免产生大量的闲置内存且增加OOM的风险,系统会在每次回收后,去检查一下,将超过的部分释放掉,节约内存降低OOM风险。
(List<View> views):Puts all views in the scrap heap into the supplied list.将mScrapView中所有的缓存view全部添加到指定的view list中,只看到有AbsListView.reclaimViews有调用到,但没有其它方法使用这个函数,可能在特殊情况下会使用到,但目前从framework中,看不出来。
(int color):Updates the cache color hint of all known views.更新view的缓存颜色提示setDrawingCacheBackgroundColor。为所有的view绘置它们的背景色。
1479-1729
1583-当数据发生改变的时候,把当前的view放到scrapviews里面,否则标记为activeViews。
// Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } }else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap();//移除所有old views ...... // Flush any cached views that did not get reused above recycleBin.scrapActiveViews();//刷新缓存,将当前的ActiveVies 移动到 ScrapViews。 |
dataChanged,从单词的意思我们就可以,这里的优化规则就是基于数据是否有变化,mDataChanged在makeAndAddView(见下文)中有使用。
step1:如果数据发生变化,就将所有view加入到mScrapView中,否则,将所有view放到mActiveView中;
step2:添加view到listview中;
step3:回收mActiveView中的未使用的view到mScrapView中;
注:在step1中,如果是addScrapView,则所有的view将会detach,如果是fillActiveViews,则不会detach,只有在step3中,未用到的view才会detach。
(int position, int y, boolean flow, int childrenLeft, boolean selected)(1772)
Obtain the view and add it to our list of children. The view can be made fresh, converted from an unused view, or used as is if it was in the recycle bin.
View child; if (!mDataChanged) {// 数据没有更新时,使用以前的view // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned // 对复用的View针对当前需要进行配置。定位并且添加这个view到ViewGrop中的children列表,从回收站获取的视图不需要measure,所以最后一个参数为true setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if possible // 创建或者重用视图 child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; |
(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled)(1812)
Add a view as a child and make sure it is measured (if necessary) and positioned properly.
(ListAdapter adapter) (457)
Sets the data behind this ListView. The adapter passed to this method may be wrapped by a WrapperListAdapter, depending on the ListView features currently in use. For instance, adding headers and/or footers will cause the adapter to be wrapped.
if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver);//移除了与当前listview的adapter绑定数据集观察者DataSetObserver } resetList();//重置listview,主要是清除所有的view,改变header、footer的状态 mRecycler.clear();//清除掉RecycleBin对象mRecycler中所有缓存的view,RecycleBin后面着重介绍,主要是关系到Listview中item的重用机制,它是AbsListview的一个内部类 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { //判断是否有headerview和footview mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; } mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; // AbsListView#setAdapter will update choice mode states. super.setAdapter(adapter);
if (mAdapter != null) { mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); mDataSetObserver = new AdapterDataSetObserver();//注册headerview的观察者 mAdapter.registerDataSetObserver(mDataSetObserver);//在RecycleBin对象mRecycler记录下item类型的数量 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); int position; if (mStackFromBottom) { position = lookForSelectablePosition(mItemCount - 1, false); } else { position = lookForSelectablePosition(0, true); } setSelectedPositionInt(position);//AdapterView中的方法,记录当前的position setNextSelectedPositionInt(position);//AdapterView中的方法,记录下一个position if (mItemCount == 0) { // Nothing selected checkSelectionChanged(); } } else { mAreAllItemsSelectable = true; checkFocus(); // Nothing selected checkSelectionChanged(); } requestLayout(); |
(int amount)(3012-3082)
对子view滑动一定距离,添加view到底部或者移除顶部的不可见view。从注释看,不可见的item 的自动移除是在scrollListItemsBy中进行的。
private void scrollListItemsBy(int amount) { offsetChildrenTopAndBottom(amount); final int listBottom = getHeight() - mListPadding.bottom;//获取listview最底部位置 final int listTop = mListPadding.top; //获取listview最顶部位置 final AbsListView.RecycleBin recycleBin = mRecycler; if (amount < 0) { // shifted items up // may need to pan views into the bottom space int numChildren = getChildCount(); View last = getChildAt(numChildren - 1); while (last.getBottom() < listBottom) {//最后的view高于底部时添加下一个view final int lastVisiblePosition = mFirstPosition + numChildren - 1; if (lastVisiblePosition < mItemCount - 1) { last = addViewBelow(last, lastVisiblePosition); numChildren++; } else { break; } } // may have brought in the last child of the list that is skinnier // than the fading edge, thereby leaving space at the end. need // to shift back if (last.getBottom() < listBottom) {//到达最后一个view offsetChildrenTopAndBottom(listBottom - last.getBottom()); } // top views may be panned off screen View first = getChildAt(0); while (first.getBottom() < listTop) {//顶部view移除屏幕时 AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(first, mFirstPosition); //回收view } detachViewFromParent(first); //从父类中移除 first = getChildAt(0); //这行好像没用啊。。。。 mFirstPosition++; } } else { // shifted items down View first = getChildAt(0); // may need to pan views into top while ((first.getTop() > listTop) && (mFirstPosition > 0)) {//顶部view上部有空间时添加view。 first = addViewAbove(first, mFirstPosition); mFirstPosition--; } // may have brought the very first child of the list in too far and // need to shift it back if (first.getTop() > listTop) {//到达第一个view offsetChildrenTopAndBottom(listTop - first.getTop()); } int lastIndex = getChildCount() - 1; View last = getChildAt(lastIndex); // bottom view may be panned off screen while (last.getTop() > listBottom) {//底部view移除屏幕的情况 AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { recycleBin.addScrapView(last, mFirstPosition+lastIndex); } detachViewFromParent(last); last = getChildAt(--lastIndex); } } } |
从以上代码可以看出,Android中view回收的计算是其父view中不再显示的,如果scrollview中包含了一个wrap_content属性的listview,里面的内容并不会有任何回收,引起listview 的getheight函数获取的是一个足以显示所有内容的高度。
(int position, boolean[] isScrap)(2227)
Get a view and have it show the data associated with the specified position. 当这个方法被调用时,说明Recycle bin中的view已经不可用了,那么,现在唯一的方法就是,convert一个老的view,或者构造一个新的view。
position: 要显示的位置
isScrap: 是个boolean数组, 如果view从scrap heap获取,isScrap [0]为true,否则为false。
isScrap[0] = false; View scrapView; scrapView = mRecycler.getTransientStateView(position); if (scrapView == null) { // 查看回收站中是否有废弃无用的View,如果有,则使用它,无需New View。 scrapView = mRecycler.getScrapView(position); } View child; if (scrapView != null) { //此时说明可以从回收站中重新使用scrapView。 child = mAdapter.getView(position, scrapView, this); if(child.getImportantForAccessibility()== IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } if (child != scrapView) { //如果重用的scrapView和adapter获得的view是不一样的,将scrapView进行回收 mRecycler.addScrapView(scrapView, position);// scrapView 仍然放入回收站 if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } else { //如果重用的view和adapter获得的view是一样的,将isScrap[0]值为true,否则默认为false isScrap[0] = true; // Clear any system-managed transient state so that we can // recycle this view and bind it to different data. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } child.dispatchFinishTemporaryDetach(); } }else {//回收站中没有拿到数据,就只能够自己去inflate一个xml布局文件,或者new一个view child = mAdapter.getView(position, null, this); //当getview中传入的 converView=null的时候会在getView的方法中进行新建这个view if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } if (mCacheColorHint != 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } |
(int deltaY, int incrementalDeltaY)(4991)
监视滑动动作
deltaY: Amount to offset mMotionView. This is the accumulated delta since the motion began. 正数表示向下滑动。
incrementalDeltaY :Change in deltaY from the previous event.
....... // 滚动时,不在可见范围内的item放入回收站。。。。。。。 if (down) { int top = -incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { top += listPadding.top; } for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } mRecycler.addScrapView(child, position);//放入回收站 } } } } else { int bottom = getHeight() - incrementalDeltaY; if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { bottom -= listPadding.bottom; } for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { // The view will be rebound to new data, clear any // system-managed transient state. if (child.isAccessibilityFocused()) { child.clearAccessibilityFocus(); } mRecycler.addScrapView(child, position);//放入回收站 } } } } |
在listview中当有多种viewtype的时候,在adapter中继承设置getItemViewType方法可以更有效率 。示例如下:
....... private static final int TYPE_ITEM = 0; private static final int TYPE_SEPARATOR = 1; private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;
@Override public int getItemViewType(int position) { return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM; } @Override public int getViewTypeCount() { return TYPE_MAX_COUNT; }
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; int type = getItemViewType(position); if (convertView == null) { holder = new ViewHolder(); switch (type) { case TYPE_ITEM: convertView = mInflater.inflate(R.layout.item1, null); holder.textView = (TextView)convertView.findViewById......; break; case TYPE_SEPARATOR: convertView = mInflater.inflate(R.layout.item2, null); holder.textView = (TextView)convertView.findViewById......; break; } convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } ........ } |
如果实现了RecyclerListener接口,当一个View由于ListView的滑动被系统回收到RecycleBin的mScrapViews数组时,会调用RecyclerListener中的onMovedToScrapHeap(View view)方法。RecycleBin相当于一个临时存储不需要显示的那部分Views的对象,随着列表滑动,这些Views需要显示出来的时候,他们就被从RecycleBin中拿了出来,RecycleBin本身并不对mScrapViews中的对象做回收操作。
于是在工程里,为ListView添加RecyclerListener接口,并在onMovedToScrapHeap方法中释放ListItem包含的Bitmap资源,这样可以极大的减少内存占用。
用来标记这个view的瞬时状态,用来告诉app无需关心其保存和恢复。从注释看,这种具有瞬时状态的view,用于在view动画播放等情况中。