Android開發技術面總結

程式設計師小魚發表於2018-12-28

本來著 作者丨AffyFei ,原文地址:https://www.jianshu.com/p/b110f9c1384c

今天早上參加了深圳 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) 必須有迴圈等待。
    一個任務正在等待另一個任務所持有的資源,後者又在等待別的任務所持有的資源,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。

要解決死鎖問題,必須打破上面四個條件的其中之一。在程式中,最容易打破的往往是第四個條件。

關於如何手寫死鎖和定位方法,可參考這篇部落格。

https://blog.csdn.net/Andy_96/article/details/82812538

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()會被觸發,可以在該方法中編寫資料庫升級邏輯。具體的資料庫升級邏輯示例可參考這裡https://blog.csdn.net/s003603u/article/details/53942411

###### 常用的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 DROP COLUMN column_name

Android

1、Broadcast的分類?有序,無序?粘性,非粘性?本地廣播?

廣播可以分為有序廣播、無序廣播、本地廣播、粘性廣播。其中無序廣播通過sendBroadcast(intent)傳送,有序廣播通過sendOrderedBroadcast(intent)傳送。

有序廣播

  • (1) 有序廣播可以用priority來調整優先順序 取值範圍-1000~+1000,預設為0,數值越大優先順序越高,優先順序越高越優先獲得廣播響應。
  • (2) abortBroadcast()可來終止該廣播的傳播,對更低優先順序的遮蔽,注意只對有序廣播生效。

    • (3) 有序廣播在傳播資料中會發生比如setResultData(),getResultData(),在傳播過程中,可以從新設定資料

關於本地廣播,可以檢視這篇文章
http://www.trinea.cn/android/localbroadcastmanager-impl/

總的來說,本地廣播是通過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獲得事件處理權。

最後附上流程圖總結:

image

touch事件傳遞流程

參考:
https://juejin.im/entry/58df5b33570c35005798493c
https://juejin.im/post/5b8f15e26fb9a01a031b12d9#heading-3

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檔案。具體分析流程,可參照以下兩篇文章:

https://www.jianshu.com/p/fa962a5fd939
https://blog.csdn.net/droyon/article/details/51099826

5、記憶體洩露的場景有哪些?記憶體洩漏分析工具使用方法?
  • 常見的記憶體洩露有:
  • 單例模式引起的記憶體洩露。
  • 靜態變數導致的記憶體洩露。
  • 非靜態內部類引起的記憶體洩露。
  • 使用資源時,未及時關閉引起記憶體洩露。
  • 使用屬性動畫引起的記憶體洩露。
  • Webview導致的記憶體洩露。

而對於記憶體洩露的檢測,常用的工具有LeakCanary、MAT(Memory Analyer Tools)、Android Studio自帶的Profiler。關於用法,網上教程很多,可自行查閱,下面兩個經供參考:

三種用法https://blog.csdn.net/qq_20280683/article/details/77964208、MAThttps://blog.csdn.net/yibanbubadao123/article/details/51871733

同時附上官方Android Profiler教程https://developer.android.com/studio/profile/android-profiler?utm_source=android-studio

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,面試官考察的多數是基礎知識是否牢固,溝通表達能力,總結能力。雖然此次面試黃了,但不失為一次很好的經歷。

閱讀更多

面試官:請你介紹一下你的專案經驗

談一下Application和Context

面試官:說說你對網路請求加密的理解?

談一談跨平臺開發

Android螢幕配適、版本配適與多語言支援

相信自己,沒有做不到的,只有想不到的

在這裡獲得的不僅僅是技術!


相關文章