Android 值得深入思考的幾個面試問答分享

文末有Android進階面試資料免費分享

1. 事件分發機制大家應該都熟記於心,默認事件分發是逆序的,有哪些方法可以修改分發順序?

記得曾經有位朋友做貼紙應用時,有RT 的需求。默認事件分發為逆序,遍歷子 View 為 (childCount ~ 0 ],有哪些方式可以修改這一策略,比如修改遍歷方式為[0,childCount)?

修改事件分發順序的話,在日常開發中基本遇不到,因為現在的逆序遍歷,是跟View的層級顯示相匹配的,隨便更改反而不太合理。

如果非要修改這個順序,很多同學首先會想到:

重寫dispatchTouchEvent方法,然後在裡面一個for循環,從0開始一個個調用子View的dispatchTouchEvent。

這個方法,不是說絕對不行,只是你要做的事情很多,就比如觸摸座標的轉換:

我們都知道,ViewGroup在分派事件的時候,會檢查子View是否應用過屬性動畫的(位移、縮放、旋轉等),如果有的話還要把座標給映射回去

接著,還會把相對於這個ViewGroup本身的觸摸座標 轉換成 相對於對應子View的觸摸座標。

這樣說可能有點繞,舉個例子:

比如:當手指在屏幕中按下,ViewGroup中收到的event座標(getX,getY)假設是【500,500】,剛好在這個位置上有個子View,那接下來肯定會把事件傳給這個子View的dispatchTouchEvent,這時候如果座標不轉換直接傳的話,那子View收到的event座標(getX,getY)也是【500,500】,這明顯是不對的,正確的座標應該要分別減去它的left和top。

這看起來好像沒什麼大的影響,但如果你的子View沒有重寫onTouchEvent方法的話(比如子View是常用的ImageView,TextView之類的),你的OnClickListener就會無效了,因為默認的onTouchEvent在處理ACTION_MOVE的時候,會檢查event的座標是否已經脫離了View的邊界範圍,如果在邊界範圍之外的話,pressed將會失效(認為沒有被按下),當ACTION_UP時,如果pressed為false,就不會執行PerformClick。

那難道沒有方法可以完美地做到了嗎?

在ViewGroup的dispatchTouchEvent方法中,雖然它是逆序的for,但是呢,它把子View拿出來的時候,卻不是直接操作的mChildren數組,而是通過一個getAndVerifyPreorderedView方法來獲得,這個方法會把當前索引傳進去,還有一個preorderedList。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
final ArrayList<view> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
}
/<view>

如果傳進去的preorderedList不為空,那麼就會直接從它裡面去取。

preorderedList怎麼來?

通過調用buildOrderedChildList方法獲取的。

buildOrderedChildList方法是怎麼樣的?

ArrayList<view> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
if (childrenCount <= 1 || !hasChildWithZ()) return null;
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
// callers should clear, so clear shouldn't be necessary, but for safety...
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z

int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
/<view>

它裡面是通過一個getAndVerifyPreorderedIndex方法來獲取對應的子VIew索引,這個方法要傳進去一個叫customOrder的boolean。

這個customOrder,看名字可以知道,是自定義順序的意思,如果它為true的話,接著會通過getChildDrawingOrder(int childCount, int i)方法來獲取對應的索引,而且,這個方法是protected的,所以我們可以通過重寫這個方法並根據參數"i"來決定返回哪一個View所對應的索引,從而改變分發的順序。

protected int getChildDrawingOrder(int childCount, int i) {
return i;
}

那這個customOrder,什麼時候為true呢?

在buildOrderedChildList方法裡可以看到這麼一句:

final boolean customOrder = isChildrenDrawingOrderEnabled();

emmmm,也就是說,如果要自定義這個順序的話,還需要調用setChildrenDrawingOrderEnabled(true)來開啟。

重新捋一捋流程:

1. setChildrenDrawingOrderEnabled(true)來開啟自定義順序;

2. 重寫getChildDrawingOrder方法來決定什麼時候要返回哪個子View;

Android 值得深入思考的幾個面試問答分享

2. AppCompatTextView 與 TextView 有什麼區別?

1. compat庫是如何將TextView替換為AppCompatTextVew的?

2. 為什麼要進行替換?

3. 根據替換相關原理,我們可以做哪些事情?

先從第二問開始吧:

AppCompatTextView繼承自TextView,是對TextView的一種擴展,因為在5.0中首次推出了MaterialDesign這種設計風格。

但是眾所周知的,5.0推出不可能所有的設備全都一下子更新到最新版本,為了在早期版本上實現新的功能(這些新功能比如從源碼註釋中解讀到比如backgroundTint屬性,根據文本內容自適應大小等).

即為了新特性同樣可以兼容老版本,framework在創建TextView實例的時候,自動幫我們進行了替換。

其它的AppCompatXXX與XXX的關係也是如此。

第一問:

然後第一問,如何完成替換的,我們這裡只拿最直觀的流程舉例,且儘可能的簡化源碼過程,在討論這個問題之前,先了解幾個預備知識:

View是怎麼被解析創建出來的:

1.LayoutInflater:將佈局XML文件實例化為其對應的View對象,我們在Activity中通過setContentView傳入一個Layout的資源文件id,最終該方法最終會調用到PhoneWindow的setContentView方法,這個方法裡面有調用到

mLayoutInflater.inflate(layoutResID, mContentParent);

2.inflate方法,該方法的作用是將指定的XML文件填充到View的層次結構中去,最終無論通過什麼途徑調用到inflate方法,都會走到三個參數的重載方法這裡:

return inflate(parser, root, attachToRoot);

parser你可以認為持有將Layout.XML解析後的數據。後兩個參數的意義如下:

  • 1. root為null,attchToRoot無意義,inflate返回的是當前XML對應的根佈局。
  • 2. root不為null且attachToRoot為true,則整個XML對應的佈局就設置了根佈局是root。
  • 3. root不為null且attachToRoot為false,則會將root的layoutParames設置給當前XML的佈局。

知道了LayoutInflate.inflate做了什麼,再往下,inflate中會調用createViewFromTag,從方法名就能知道,繼續往下走,我們離答案越來越近了。

createViewFromTag做的事情非常有意思:

先看到787行這個if-else,條件是name中有沒有"."字符,如果有我們會執行onCreateView,如果沒有會執行createView。

Android 值得深入思考的幾個面試問答分享

name啥時候有點?

自定義控件的時候。

當是系統控件的時候,createView會有一個填充了第二個參數的調用:

createView(name, "android.view.", attrs);補上了View控件的全路徑名,而自定義控件則不需要,因為傳入的name就是一個全路徑名。

為什麼要全路徑名?

因為View控件對象的創建是通過反射來實現的:

clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
// ...
args[1] = attrs;
final View view = constructor.newInstance(args);

下面對這幾步做一個總結:

XML中保存了ViewTree的結構和View的相關標籤信息(包括View的類型和一些屬性值),然後這些信息會在後面通過反射的方式(如果沒有Factory2和Factory的話)創建實例對象,如果創建的是ViewGroup,則會對它的子View遍歷重複創建步驟,創建完View對象後,會add到對應的ViewGroup中。

其中相關方法的調用流程是:

inflate->rInflate->createViewFromTag->createView。

好像還是沒有看到替換?

還是上一張圖,我們只解釋了後半部分,沒有解釋前半部分,那麼什麼是Factory?

繼續往下看:

createViewFromTag中會先判斷有沒有Factory或者Factory2的對象,如果有,則調用Factory的onCreateView方法。

這兩個類都是接口,其中Factory2是Factory的子接口,都只有唯一一個onCreateView方法。

不同之處在於Factory2的onCreateView方法傳入了parentView。

該方法的作用就是你可以藉助它來改造XML中已經存在了的Tag的值。所以Factory2可以達到改造parentView的目的。

但是我們在日常中根本就沒有任何地方接觸到了Factory(2)呀,那麼它是不是就直接是null呢?

到這裡又是一番源碼調來調去,為了便於理解,只需要知道,這個東西(Factory2),在最開始AppCompatActivity(為了兼容低版本,我們現在Activity默認都是繼承自它)中的onCreate方法中就已經通過層層調用被設置好了。

既然現在Factory2不為空,那麼就應該去走它的onCreateView方法了,這裡又是層層調用,最終來到了AppCompatViewInflater**** 的 createView 方法:

答案就在這裡:

如果創建的是非兼容控件(系統控件那麼多,實現兼容的只是常用的一些控件),那麼就會是143行,在146中通過反射創建View對象。

囉裡囉唆扯了一大堆,還是沒回答第一個問題:

compat庫是如何將TextView替換為AppCompatTextVew的?

個人對這個的理解:在將XML文件解析成包含ViewTree信息之後,開始利用這些信息去創建每一個View節點,在創建View對象的時候,如果發現這個節點是屬於支持兼容的控件比如TextView,那麼就會去調用到new AppCompatTextView()來創建一個兼容的View對象,也就是在創建的時候,及已經實現了替換。

第三問:

根據替換相關原理,我們可以做哪些事情?

整個替換從圖一所示的源碼中可以看到,能夠被替換的關鍵是Factory(2)存在,那麼我覺得,其實問題問的是Factory(2)可以用來做什麼吧?

那麼這個時候,就適合去問站長大人了:

§ 探究 LayoutInflater setFactory

3. getWidth, getMeasuredWidth 有什麼區別?

getWidth和getMeasuredWidth的區別:

getMeasuredWidth方法返回的是測量後的寬度,這個寬度是當setMeasuredDimension方法(measure方法最終會調用setMeasuredDimension)被調用後刷新的

而getWidth返回的是最終layout出來的寬度,在View代碼中返回的是【mRight - mLeft】,這個mRight和mLeft,是在setFrame方法被調用後賦值的(layout方法最終會調用setFrame)。

也就是說,getMeasuredWidth返回值的大小,取決於setMeasuredDimension,而getWidth,則取決於layout。

傳說中一個是 View 寬度,一個是 View 中的內容寬度,這個解答對嗎?

在常規的View中,比如TextView,ImageView這些,如果沒有明確指定寬度的話,那麼他們的getMeasuredWidth返回的寬度,確實就是實際內容的寬度。

但如果在xml佈局裡或自定義View中故意把寬度設置的很大,或者很小,比如設置寬度為9999999,這種情況就不算了。

所以我的回答是:如果這個View和它所在的ViewGroup(在ViewGroup中的onMeasure也可做手腳),都遵守規矩的話,那麼這句話就是對的。

4.butterknife 中的黑科技

很多時候大家在剖析butterknife源碼的時候,更多的是講解其中的apt等,在library中使用buttterknife的時候,會使用R2.id.xxx

class ExampleActivity extends Activity {
@BindView(R2.id.user) EditText username;
@BindView(R2.id.pass) EditText password;
...
}

而非R.id.xxx.

最後

需要Android面試資料題集的可以

關注轉發,私聊我【面試】免費領取。

Android 值得深入思考的幾個面試問答分享

Android 值得深入思考的幾個面試問答分享


分享到:


相關文章: