程式設計師:我想換工作,讓我琢磨琢磨這幾個值得深入思考的面試問答

yilian發表於2019-12-11

馬上就要年末啦,大家包袱款款回家過個熱鬧年,拿完年終獎,又到了跳槽的好機會

俗話說,機會總是給有準備的人,現在就可以看看面試題了。

這裡給大家不定期更新大廠面試真題,今天來分享一下Android面試中幾個值得我們深入思考的面試題還有解析,希望可以幫助到即將面試的小夥伴們,祝面試順利~

文末還有大廠面試專題資料包免費分享~

接下來是正文:

image
image

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。

@Overridepublic 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);
 ...
}

如果傳進去的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;
 }

它裡面是透過一個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;

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做的事情非常有意思:

image
image

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

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 方法:

答案就在這裡:

image
image

如果建立的是非相容控制元件(系統控制元件那麼多,實現相容的只是常用的一些控制元件),那麼就會是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.

最後

上面幾個題都值得深入思考,大家可以關注我,轉發收藏文章

面試合集可以看這裡:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2668157/,如需轉載,請註明出處,否則將追究法律責任。

相關文章