作者:AffyFei
導讀
正常來說去大廠面試應該問一些底層原理,但這次面試卻違反常規問了一些基礎,這也提醒我們不要把基礎丟了。文末有大量的面試題文章,大家記得看,本文中的經歷是作者AffyFei,不是我,下面看正文:
今天早上參加了深圳OPPO開發工程師的技術面試,總的來說面試過程不是很順利。面試官並沒有問一些很深奧的底層原理,基本都是一些Java基礎以及Android四大組件內的基礎,但是我自身在開發過程中並沒有很重視這些理論基礎,導致很多知識點都忘記了。整個面試過程耗時一小時,感謝兩位面試官不厭其煩地給我提示,一方面讓我能夠回想起來那些遺忘的知識點,另一方面也緩解了尷尬的氣氛。。。
順便一說,OPPO的保密工作還是做得比較嚴格的,進去後海卓越中心大樓前需要申請臨時通行證才能進去。而在面試前還需要登記,並且把手機的前後攝像頭都給用膠帶封起來才能進行面試。廢話少說,下面分成兩部分匯總一下這次技術面試的知識點。
Java方面
1.如何理解Java的多態?其中,重載和重寫有什麼區別?
多態是同一個行為具有多個不同表現形式或形態的能力,多態是同一個接口,使用不同的實例而執行不同操作,多態就是程序運行期間才確定,一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法。
多態存在的三個必要條件是:繼承,重寫,父類引用指向子類引用。
多態的三個實現方式是:重寫,接口,抽象類和抽象方法。
重寫(Override)和重載(Overload)的區別
2.談一下JVM內存區域劃分?哪部分是線程公有的,哪部分是私有的?
JVM 的內存區域可以分為兩類:線程私有和區域和線程共有的區域。 線程私有的區域:程序計數器、JVM 虛擬機棧、本地方法棧;線程共有的區域:堆、方法區、運行時常量池。
-
程序計數器,也有稱作PC寄存器。每個線程都有一個私有的程序計數器,任何時間一個線程都只會有一個方法正在執行,也就是所謂的當前方法。程序計數器存放的就是這個當前方法的JVM指令地址。當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然後根據得到的地址獲取到指令,在得到指令之後,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。
JVM虛擬機棧。創建線程的時候會創建線程內的虛擬機棧,棧中存放著一個個的棧幀,對應著一個個方法的調用。JVM 虛擬機棧有兩種操作,分別是壓棧和出站。棧幀中存放著局部變量表(Local Variables)、操作數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。 - 本地方法棧。本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的。在JVM規範中,並沒有對本地方發展的具體實現方法以及數據結構作強制規定,虛擬機可以自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二為一。
堆。堆是內存管理的核心區域,用來存放對象實例。幾乎所有創建的對象實例都會直接分配到堆上。所以堆也是垃圾回收的主要區域,垃圾收集器會對堆有著更細的劃分,最常見的就是把堆劃分為新生代和老年代。java堆允許處於不連續的物理內存空間中,只要邏輯連續即可。堆中如果沒有空間完成實例分配無法擴展時將會拋出OutOfMemoryError異常。 - 方法區。方法區與堆一樣所有線程所共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯後的代碼等數據。在Class文件中除了類的欄位、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
其實除了程序計數器,其他的部分都會發生 OOM。
- 堆。 通常發生的 OOM 都會發生在堆中,最常見的可能導致 OOM 的原因就是內存泄漏。
- JVM虛擬機棧和本地方法棧。 當我們寫一個遞歸方法,這個遞歸方法沒有循環終止條件,最終會導致 StackOverflow 的錯誤。當然,如果棧空間擴展失敗,也是會發生 OOM 的。
- 方法區。方法區現在基本上不太會發生 OOM,但在早期內存中加載的類信息過多的情況下也是會發生 OOM 的。
3.final關鍵字的用法?
final 可以修飾類、變量和方法。修飾類代表這個類不可被繼承。修飾變量代表此變量不可被改變。修飾方法表示此方法不可被重寫 (override)。
4.死鎖是怎麼導致的?如何定位死鎖
某個任務在等待另一個任務,而後者又等待別的任務,這樣一直下去,直到這個鏈條上的任務又在等待第一個任務釋放鎖。這得到了一個任務之間互相等待的連續循環,沒有哪個線程能繼續。這被稱之為死鎖。當以下四個條件同時滿足時,就會產生死鎖:
(1) 互斥條件。任務所使用的資源中至少有一個是不能共享的。
(2) 任務必須持有一個資源,同時等待獲取另一個被別的任務占有的資源。
(3) 資源不能被強占。
(4) 必須有循環等待。一個任務正在等待另一個任務所持有的資源,後者又在等待別的任務所持有的資源,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。
要解決死鎖問題,必須打破上面四個條件的其中之一。在程序中,最容易打破的往往是第四個條件。
5.資料庫如何進行升級?SQLite增刪改查的基礎sql語句?
/**
* Create a helper object to create, open, and/or manage a database.
* This method always returns very quickly. The database is not actually
* created or opened until one of {@link #getWritableDatabase} or
* {@link #getReadableDatabase} is called.
*
* @param context to use to open or create the database
* @param name of the database file, or null for an in-memory database
* @param factory to use for creating cursor objects, or null for the default
* @param version number of the database (starting at 1); if the database is older,
* {@link #onUpgrade} will be used to upgrade the database; if the database is
* newer, {@link #onDowngrade} will be used to downgrade the database
*/
public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}
private SQLiteDatabase getDatabaseLocked(boolean writable) {
.......
db.beginTransaction();
try {
if (version == 0) {
onCreate(db);
} else {
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
在SQLiteOpenHelper的構造函數中,包含了一個version的參數。這個參數即是資料庫的版本。 所以,我們可以通過修改version來實現資料庫的升級。 當version大於原資料庫版本時,onUpgrade()會被觸發,可以在該方法中編寫資料庫升級邏輯。具體的資料庫升級邏輯示例可參考這裡。
常用的SQL增刪改查:
增:INSERT INTO table_name (列1, 列2,…) VALUES (值1, 值2,….)
刪: DELETE FROM 表名稱 WHERE 列名稱 = 值
改:UPDATE 表名稱 SET 列名稱 = 新值 WHERE 列名稱 = 某值
查:SELECT 列名稱(通配是*符號) FROM 表名稱
ps:操作數據表是:ALTER TABLE。該語句用於在已有的表中添加、修改或刪除列。
ALTER TABLE table_name ADD column_name datatype
ALTER TABLE table_name_old RENAME TO table_name_new
Android方面
1.Broadcast的分類?有序,無序?粘性,非粘性?本地廣播?
- 廣播可以分為有序廣播、無序廣播、本地廣播、粘性廣播。其中無序廣播通過sendBroadcast(intent)發送,有序廣播通過sendOrderedBroadcast(intent)發送。
- 有序廣播。
(1) 有序廣播可以用priority來調整優先級 取值範圍-1000~+1000,默認為0,數值越大優先級越高,優先級越高越優先獲得廣播響應。
(2) abortBroadcast()可來終止該廣播的傳播,對更低優先級的屏蔽,注意只對有序廣播生效。
(3) 有序廣播在傳播數據中會發生比如setResultData(),getResultData(),在傳播過程中,可以從新設置數據 - 關於本地廣播,可以查看這篇文章。總的來說,本地廣播是通過LocalBroadcastManager內置的Handler來實現的,只是利用了IntentFilter的match功能,至於BroadcastReceiver 換成其他接口也無所謂,順便利用了現成的類和概念而已。在register()的時候保存BroadcastReceiver以及對應的IntentFilter,在sendBroadcast()的時候找到和Intent對應的BroadcastReceiver,然後通過Handler發送消息,觸發executePendingBroadcasts()函數,再在後者中調用對應BroadcastReceiver的onReceive()方法。
- 粘性消息:粘性消息在發送後就一直存在於系統的消息容器裡面,等待對應的處理器去處理,如果暫時沒有處理器處理這個消息則一直在消息容器裡面處於等待狀態,粘性廣播的Receiver如果被銷毀,那麼下次重建時會自動接收到消息數據。(在 android 5.0/api 21中deprecated,不再推薦使用,相應的還有粘性有序廣播,同樣已經deprecated)
2.Android中的事件傳遞機制?
當我們的手指觸碰到螢幕,事件是按照Activity->ViewGroup->View這樣的流程到達最終響應觸摸事件的View的。而在事件分發過程中,涉及到三個最重要的方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。我們的手指觸摸到螢幕的時候,會觸發一個Action_Down類型的事件,當前頁面的Activity會首先做出相應,也就是說會走到Activity的dispatchTouchEvent()方法內。在這個方法內部有下面兩個邏輯:
- 調用getWindow.superDispatchTouchEvent()。
- 如果上一步返回true,則直接返回true;否則return自己的onTouchEvent()。顯然,當getWindow.superDispatchTouchEvent()返回true,表示當前事件已經被消費掉,無需調用onTouchEvent;否則代表事件並沒有被處理,因此需要調用Activity的onTouchEvent進行處理。
我們都知道,getWindow()返回的是PhoneWindow,因此這句代碼本質上調用了PhoneWindow中的superDispatchTouchEvent()。而後者實際上調用了mDecor.superDispatchTouchEvent(event)。這個mDecor也就是DecorView,它是FrameLayout的一個子類。在DecorView中的superDispatchTouchEvent(event)中調用的是super.dispatchTouchEvent()。因此,本質上調用的是ViewGroup的dispatchTouchEvent()。
到這裡,事件已經從Activity傳遞到ViewGroup了。接下來我們分析ViewGroup。
在ViewGroup的dispatchTouchEvent()中邏輯大致如下:
- 通過onInterceptTouchEvent()判斷當前ViewGroup是否攔截,默認的ViewGroup都是不攔截的;
- 如果攔截,則return自己的onTouchEvent();
- 如果不攔截,則根據child.dispatchTouchEvent()的返回值判斷。如果返回true,則return true;否則return自身的onTouchEvent(),在這裡實現了未處理事件的向上傳遞。
通常情況下,ViewGroup的onInterceptTouchEvent()都返回false,表示不攔截。這裡需要注意的是事件序列,比如Down事件、Move事件…Up事件,從Down到Up是一個完整的事件序列,對應著手指從按下到抬起這一系列事件,如果ViewGroup攔截了Down事件,那麼後續事件都會交給這個ViewGroup的onTouchEvent。如果ViewGroup攔截的不是Down事件,那麼會給之前處理這個Down事件的View發送一個Action_Cancel類型的事件,通知子View這個後續的事件序列已經被ViewGroup接管了,子View恢復之前的狀態即可。
這裡舉一個常見的例子:在一個 Recyclerview 中有很多的 Button,我們首先按下了一個 button,然後滑動一段距離再鬆開,這時候 Recyclerview 會跟著滑動,並不會觸發這個 button 的點擊事件。這個例子中,當我們按下 button 時,這個 button 接收到了 Action_Down 事件,正常情況下後續的事件序列應該由這個 button處理。但我們滑動了一段距離,這時 Recyclerview 察覺到這是一個滑動操作,攔截了這個事件序列,走了自身的 onTouchEvent()方法,反映在螢幕上就是列表的滑動。而這時 button 仍然處於按下的狀態,所以在攔截的時候需要發送一個 Action_Cancel 來通知 button 恢復之前狀態。
事件分發最終會走到View的dispatchTouchEvent()中。在View的dispatchTouchEvent()中沒有onInterceptTouchEvent(),這裡很容易理解,View沒有child,也就不存在攔截。View的dispatchTouchEvent()直接return了自己的onTouchEvent()。如果onTouchEvent()返回true代表事件被消費,否則未消費的事件會向上傳遞,直到有View處理了事件或一直沒有消費,最終回到Activity的onTouchEvent()終止。
有時候會有人混淆onTouchEvent和onTouch。首先,這兩個方法都在View的dispatchTouchEvent()中:
- 如果touchListener不為null,並且這個View是enable的,而且onTouch返回true,都滿足時直接return true,走不到onTouchEvent()方法。
- 否則,就會觸發onTouchEvent()。因此onTouch優先於onTouchEvent獲得事件處理權。
最後附上流程圖總結:
3.Handler的原理?
與Handler密切相關的還有Message、MessageQueue、Looper。
- Message。Message有兩個關鍵的成員變量:target、callback:
(1) target。就是發送消息的Handler
(2) callback。調用Handler.post(Runnable)時傳入的Runnable類型的任務。post事件的本質也是創建了一個Message,將我們傳入的這個runnable賦值給創建的Message的callback這個成員變量。 - MessageQueue。消息隊列用於存放消息,其中重點關注next()方法,它會返回下一個待處理的消息。
- Looper。Looper消息輪詢器其實是連接Handler和消息隊列的核心。想要在一個線程中創建一個Handler,首先要通過Looper.prepare()創建Looper,之後還得調用Looper.loop()開啟輪詢。
(1) prepare()。這個方法做了兩件事:首先通過ThreadLocal.get()獲取當前線程中的Looper,如果不為空則拋出RuntimeException。否則創建Looper,並通過ThreadLocal.set(looper)將當前線程與剛剛創建的Looper綁定。值得注意的是,上面的消息隊列的創建其實就是發生在Looper的構造函數中。
(2)loop()。這個方法開啟了整個事件機制的輪詢。其本質是開啟一個死循環,不斷地通過MessageQueue的next()方法獲取消息msg。拿到消息後會調用msg.target.dispatchMessage()來做處理。綜上也就是調用handler.dispatchMessage()。 - Handler。Handler重點在於發送消息和處理消息。
(1)發送消息。其實發送消息除了 sendMessage 之外還有 sendMessageDelayed 和 post 以及 postDelayed 等等不同的方式。但它們的本質都是調用了 sendMessageAtTime。在 sendMessageAtTime 這個方法中調用了 enqueueMessage。在 enqueueMessage 這個方法中做了兩件事:通過 msg.target = this 實現了消息與當前 handler 的綁定。然後通過 queue.enqueueMessage 實現了消息入隊。
(2)處理消息。 消息處理的核心其實就是dispatchMessage()這個方法。這個方法裡面的邏輯很簡單,先判斷 msg.callback 是否為 null,如果不為空則執行這個 runnable。如果為空則會執行我們的handleMessage方法。
4.ANR出現的情況有幾種? 怎麼分析解決ANR問題?
ANR(Application Not responding)。Android中,主線程(UI線程)如果在規定時內沒有處理完相應工作,就會出現ANR。具體來說,ANR會在以下幾種情況中出現:
(1) 輸入事件(按鍵和觸摸事件)5s內沒被處理
(2) BroadcastReceiver的事件(onRecieve方法)在規定時間內沒處理完(前台廣播為10s,後台廣播為60s)
(3) service 前台20s後台200s未完成啟動
(4) ContentProvider的publish在10s內沒進行完
分析ANR問題,需要結合Log以及trace文件。
5.內存泄露的場景有哪些?內存泄漏分析工具使用方法?
常見的內存泄露有:
- 單例模式引起的內存泄露。
- 靜態變量導致的內存泄露。
- 非靜態內部類引起的內存泄露。
- 使用資源時,未及時關閉引起內存泄露。
- 使用屬性動畫引起的內存泄露。
- Webview導致的內存泄露。
而對於內存泄露的檢測,常用的工具有LeakCanary、MAT(Memory Analyer Tools)、Android Studio自帶的Profiler。關於用法,網上教程很多,可自行查閱,下面兩個經供參考:
三種用法、MAT
同時附上官方Android Profiler教程
6.如何實現啟動優化,有什麼工具可以使用?
重點提到了systrace這個工具,詳細用法可以參考下面幾篇文章:
https://blog.csdn.net/Kitty_Landon/article/details/79192377
https://www.cnblogs.com/baiqiantao/p/7700511.html
https://blog.csdn.net/xiyangyang8/article/details/50545707
https://blog.csdn.net/cxq234843654/article/details/74388328
7.常用的設計模式有哪些?是否了解責任鏈模式?
單例模式,觀察者模式,工廠模式,建造者模式,構造者模式,中間者模式,橋接模式,適配器模式等等。
總結
現在回顧一下,問的問題並不難,只是環環相扣問出了很多細節相關的知識點。由此看來,在日常開發中還需要注重基礎。尤其對於開發經驗是1-5年內的Android Developer,面試官考察的多數是基礎知識是否牢固,溝通表達能力,總結能力。雖然此次面試黃了,但不失為一次很好的經歷。
在這我也分享一份大佬自己收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記,還有高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習
如果你有需要的話,可以點讚+評論+轉發,關注我,然後私信我【進階】我發給你