Android面經| 問題歸納

面經專題系列:
Android面經| 問題歸納
Android面經| 回顧展望
Android面經| 算法題解

文章目錄

Android相關

AMS相關

ActivityManagerService是Android中最核心的服務 , 主要負責系統中四大組件的啓動、切換、調度及應用進程的管理和調度等工作,其職責與操作系統中的進程管理和調度模塊類似。
看源碼談談AMS啓動過程:ActivityManagerService分析——AMS啓動流程

Activity相關

SingleTask優化

//如果標籤爲singleTask的activity在棧頂
onNewIntent()
//如果標籤爲singleTask的activity不在棧頂
onNewIntent() -> onStart() -> onResume() -> ...

回退棧中的activity不會調用onCreate(),但是這裏雖然onNewIntent被調用了,但是Intent中所保存的數據卻仍然還是舊數據,因此需要進一步重寫。

/***
* 將activity 的創建模式設置爲singletask, 
* 使用這個方法可以再其他activity想這個activity發送Intent時,這個Intent能夠及時更新
*/
@Override
protected void onNewIntent(Intent intent)
{
     super.onNewIntent(intent);
     setIntent(intent); //這一句必須的,否則Intent無法獲得最新的數據
}

SingleTask優化搭配:

<activity
      android:launchMode="singleTask"
      android:taskAffinity="com.renly.MainActivity"
      android:name="com.renly.MainActivity"
      android:screenOrientation="portrait" />

taskAffinity,可以翻譯爲任務相關性。這個參數標識了一個 Activity 所需要的任務棧的名字,默認情況下,所有 Activity 所需的任務棧的名字爲應用的包名,當 Activity 設置了 taskAffinity 屬性,那麼這個 Activity 在被創建時就會運行在和 taskAffinity 名字相同的任務棧中,如果沒有,則新建 taskAffinity 指定的任務棧,並將 Activity 放入該棧中。另外,taskAffinity 屬性主要和 singleTask 或者 allowTaskReparenting 屬性配對使用,在其他情況下沒有意義。

allowTaskReparenting = "true"

在這種情況下,Activity 可以從其啓動的任務移動到與其具有關聯的任務(如果該任務出現在前臺)。

啓動模式應用場景

launchMode 使用場景
singleTop 適合啓動同類型的 Activity,例如接收通知啓動的內容顯示頁面
singleTask 適合作爲程序入口
singleInstance 適合需要與程序分離開的頁面,例如鬧鈴的響鈴界面

Intent

顯式啓動:
一般用於啓動應用內的Activity

隱式啓動:
a.例子:Intent intent = new Intent(String action,Uri uri);

b.一般用於啓動應用外的Activity,操作系統會自動把匹配隱式Intent的Acttivity顯
示出來供用戶選擇,匹 配的規則跟Activity聲明的 Intent-filter 有關

c.主要組成部分:

(1)action 要執行的操作。也可以通過 setAction() 設置

(2)uri待訪問數據的位置。也可以通過 setData() 和 setDataAndType() 設
置。可以是網頁的URL,某個文件的,或指向 ContentProvider 的某個內容 URI

(3)操作涉及的的數據類型。setType() 和 setDataAndType()設置。如intent.setType(“text/html”)

(4)可選類別。描述何時,何地或者如何啓動某個 Activity。
intent.addCategory(Intent.CATEGORY_LAUNCHER)

參考博客

Service相關

兩種啓動方式

1.startService()啓動方式:主要用於執行後臺計算
2.bindService()啓動方式:主要用於和其它組件的交互
說明:這兩種狀態是可以共存的,即一個Service既可以處於啓動狀態,也可以同時處於綁定狀態。

生命週期

startService() -> onCreate() -> onStartCommand() -> Service運行 -> onDestroy()
bindService() -> onCreate() -> onBind() -> Service運行 -> onUnBind() -> onDestroy()

service 運行於主線程

int onStartCommand(Intent intent, int flags, int startId)會在每次調用startService時回調

FLAG使用START_FLAG_REDELIVERY,意味着當Service因內存不足而被系統kill後,則會重建服務,並通過傳遞給服務的最後一個 Intent 調用 onStartCommand()

onStartCommand()返回值:
START_STICKY,START_NOT_STICKY,START_REDELIVER_INTENT

service詳細介紹

Service 如何和 Activity 進行通信?

① 通過綁定服務的方式。在綁定的服務中聲明一個Binder類,並創建一個Binder對象,在onBind()函數中返回這個對象,並讓Activity實現ServiceConnection接口,在OnServiceConnected方法中獲取到Service提供的這個Binder對象,通過這個對象的各種自定義的方法就能完成Service與Activity的通信。
② 通過Intent的方式,在StartService()中需要傳入一個Intent對象作爲參數,通過這個Intent實例對象進行實現通信。
③ 通過Callback和Handler的方式,在綁定的服務中聲明一個Binder類,並創建一個Binder對象,在onBind()函數中返回這個對象,讓Activity實現ServiceConnection接口,並且在OnserviceConnected方法中實例化Service中的CallBack接口,並且實現OnDataChange()方法,其中的實質是一段Handler代碼,可以在其中完成耗時操作,以這種方式完成通信。

BroadcastReciever相關

寫一個類繼承BroadcastReceiver重寫onReceive方法,注意onReceive是主線程不要做耗時操作否則阻塞10s會ANR,onReceive()中耗時操作不能開線程做,可以使用goAsync()或使用service來做(由於onReceive在結束後會釋放資源,依賴線程也很有可能會被釋放)

通過LocalBroadcastManager動態註冊的Receiver只會在App應用內廣播。

兩種註冊方式

1.靜態註冊,在註冊清單文件進行聲明
2.動態註冊,在代碼中動態進行註冊

//靜態註冊
<receiver android:name=".MyReceiver">
            <intent-filter>
                <action android:name="kt.com.MyReceiver"/>
            </intent-filter>
</receiver>
//動態註冊
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("kt.com.MyReceiver");
registerReceiver(new MyReceiver(),intentFilter);

兩種註冊方式的區別

1.動態註冊的廣播是非常駐型廣播,此時廣播是跟隨宿主的生命週期的,宿主不在了廣播也就不在了。
2.靜態註冊的廣播是常駐型廣播,即應用程序關閉後,依然能夠收到廣播。

參考博客

ContentProvider相關

ContentProvider管理android以結構化方式存放的數據。他以相對安全的方式封裝數據並且提供簡易的處理機制。Content provider提供不同進程間數據交互的標準化接口。
參考博客

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)

自定義View的步驟

自定義View繪製流程圖

參考博客

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獲得事件處理權。

最後附上流程圖總結:
touch事件傳遞流程

Handler的原理

Handler,Message,looper 和 MessageQueue 構成了安卓的消息機制,handler創建後可以通過 sendMessage 將消息加入消息隊列,然後 looper不斷的將消息從 MessageQueue 中取出來,回調到 Hander 的 handleMessage方法,從而實現線程的通信。

從兩種情況來說,第一在UI線程創建Handler,此時我們不需要手動開啓looper,因爲在應用啓動時,在ActivityThread的main方法中就創建了一個當前主線程的looper,並開啓了消息隊列,消息隊列是一個無限循環,爲什麼無限循環不會ANR?因爲可以說,應用的整個生命週期就是運行在這個消息循環中的,安卓是由事件驅動的,Looper.loop不斷的接收處理事件,每一個點擊觸摸或者Activity每一個生命週期都是在Looper.loop的控制之下的,looper.loop一旦結束,應用程序的生命週期也就結束了。我們可以想想什麼情況下會發生ANR,第一,事件沒有得到處理,第二,事件正在處理,但是沒有及時完成,而對事件進行處理的就是looper,所以只能說事件的處理如果阻塞會導致ANR,而不能說looper的無限循環會ANR

另一種情況就是在子線程創建Handler,此時由於這個線程中沒有默認開啓的消息隊列,所以我們需要手動調用looper.prepare(),並通過looper.loop開啓消息

主線程Looper從消息隊列讀取消息,當讀完所有消息時,主線程阻塞。子線程往消息隊列發送消息,並且往管道文件寫數據,主線程即被喚醒,從管道文件讀取數據,主線程被喚醒只是爲了讀取消息,當消息讀取完畢,再次睡眠。因此loop的循環並不會對CPU性能有過多的消耗。

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內沒進行完

內存泄露的場景有哪些?內存泄漏分析工具使用方法?

常見的內存泄露有:
1.非靜態內部類的靜態實例
非靜態內部類會持有外部類的引用,如果非靜態內部類的實例是靜態的,就會長期的維持着外部類的引用,組織被系統回收,解決辦法是使用靜態內部類

2.多線程相關的匿名內部類和非靜態內部類
匿名內部類同樣會持有外部類的引用,如果在線程中執行耗時操作就有可能發生內存泄漏,導致外部類無法被回收,直到耗時任務結束,解決辦法是在頁面退出時結束線程中的任務

3.Handler內存泄漏
Handler導致的內存泄漏也可以被歸納爲非靜態內部類導致的,Handler內部message是被存儲在MessageQueue中的,有些message不能馬上被處理,存在的時間會很長,導致handler無法被回收,如果handler是非靜態的,就會導致它的外部類無法被回收,解決辦法是1.使用靜態handler,外部類引用使用弱引用處理2.在退出頁面時移除消息隊列中的消息

4.Context導致內存泄漏
根據場景確定使用Activity的Context還是Application的Context,因爲二者生命週期不同,對於不必須使用Activity的Context的場景(Dialog),一律採用Application的Context,單例模式是最常見的發生此泄漏的場景,比如傳入一個Activity的Context被靜態類引用,導致無法回收

5.靜態View導致泄漏
使用靜態View可以避免每次啓動Activity都去讀取並渲染View,但是靜態View會持有Activity的引用,導致無法回收,解決辦法是在Activity銷燬的時候將靜態View設置爲null(View一旦被加載到界面中將會持有一個Context對象的引用,在這個例子中,這個context對象是我們的Activity,聲明一個靜態變量引用這個View,也就引用了activity)

6.WebView導致的內存泄漏
WebView只要使用一次,內存就不會被釋放,所以WebView都存在內存泄漏的問題,通常的解決辦法是爲WebView單開一個進程,使用AIDL進行通信,根據業務需求在合適的時機釋放掉

7.資源對象未關閉導致
如Cursor,File等,內部往往都使用了緩衝,會造成內存泄漏,一定要確保關閉它並將引用置爲null

8.集合中的對象未清理
集合用於保存對象,如果集合越來越大,不進行合理的清理,尤其是入股集合是靜態的

9.Bitmap導致內存泄漏
bitmap是比較佔內存的,所以一定要在不使用的時候及時進行清理,避免靜態變量持有大的bitmap對象

10.監聽器未關閉
很多需要register和unregister的系統服務要在合適的時候進行unregister,手動添加的listener也需要及時移除

而對於內存泄露的檢測,常用的工具有LeakCanary、MAT(Memory Analyer Tools)、Android Studio自帶的Profiler。

如何避免OOM?

1.使用更加輕量的數據結構:如使用ArrayMap/SparseArray替代HashMap,HashMap更耗內存,因爲它需要額外的實例對象來記錄Mapping操作,SparseArray更加高效,因爲它避免了Key Value的自動裝箱,和裝箱後的解箱操作

2.避免枚舉的使用,可以用靜態常量或者註解@IntDef替代

3.Bitmap優化:
a.尺寸壓縮:通過InSampleSize設置合適的縮放
b.顏色質量:設置合適的format,ARGB_6666/RBG_545/ARGB_4444/ALPHA_6,存在很大差異
c.inBitmap:使用inBitmap屬性可以告知Bitmap解碼器去嘗試使用已經存在的內存區域,新解碼的Bitmap會嘗試去使用之前那張Bitmap在Heap中所佔據的pixel data內存區域,而不是去問內存重新申請一塊區域來存放Bitmap。利用這種特性,即使是上千張的圖片,也只會僅僅只需要佔用屏幕所能夠顯示的圖片數量的內存大小,但複用存在一些限制,具體體現在:在Android 4.4之前只能重用相同大小的Bitmap的內存,而Android 4.4及以後版本則只要後來的Bitmap比之前的小即可。使用inBitmap參數前,每創建一個Bitmap對象都會分配一塊內存供其使用,而使用了inBitmap參數後,多個Bitmap可以複用一塊內存,這樣可以提高性能

4.StringBuilder替代String: 在有些時候,代碼中會需要使用到大量的字符串拼接的操作,這種時候有必要考慮使用StringBuilder來替代頻繁的“+”

5.避免在類似onDraw這樣的方法中創建對象,因爲它會迅速佔用大量內存,引起頻繁的GC甚至內存抖動

6.減少內存泄漏也是一種避免OOM的方法

如何實現進程保活

a: Service 設置成 START_STICKY kill 後會被重啓(等待5秒左右),重傳Intent,保持與重啓前一樣
b: 通過 startForeground將進程設置爲前臺進程, 做前臺服務,優先級和前臺應用一個級別,除非在系統內存非常缺,否則此進程不會被 kill
c: 雙進程Service: 讓2個進程互相保護對方,其中一個Service被清理後,另外沒被清理的進程可以立即重啓進程
d: 用C編寫守護進程(即子進程) : Android系統中當前進程(Process)fork出來的子進程,被系統認爲是兩個不同的進程。當父進程被殺死的時候,子進程仍然可以存活,並不受影響(Android5.0以上的版本不可行)聯繫廠商,加入白名單
e.鎖屏狀態下,開啓一個一像素Activity

數據庫如何進行升級?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 DROP COLUMN column_name

Android屏幕適配

一種極低成本的Android屏幕適配方式 - 字節跳動技術團隊

AsyncTask、HandlerThread、IntentService區別和使用

AsyncTask,HandlerThread,IntentService

  • AsyncTask原理:內部是Handler和兩個線程池實現的,Handler用於將線程切換到主線程,兩個線程池一個用於任務的排隊,一個用於執行任務,當AsyncTask執行execute方法時會封裝出一個FutureTask對象,將這個對象加入隊列中,如果此時沒有正在執行的任務,就執行它,執行完成之後繼續執行隊列中下一個任務,執行完成通過Handler將事件發送到主線程。AsyncTask必須在主線程初始化,因爲內部的Handler是一個靜態對象,在AsyncTask類加載的時候他就已經被初始化了。在Android3.0開始,execute方法串行執行任務的,一個一個來,3.0之前是並行執行的。如果要在3.0上執行並行任務,可以調用executeOnExecutor方法。

  • HandlerThread原理:繼承自 Thread,start開啓線程後,會在其run方法中會通過Looper 創建消息隊列並開啓消息循環,這個消息隊列運行在子線程中,所以可以將HandlerThread 中的 Looper 實例傳遞給一個 Handler,從而保證這個 Handler 的 handleMessage 方法運行在子線程中,Android 中使用 HandlerThread的一個場景就是 IntentService

  • IntentService原理:繼承自Service,它的內部封裝了 HandlerThread 和Handler,可以執行耗時任務,同時因爲它是一個服務,優先級比普通線程高很多,所以更適合執行一些高優先級的後臺任務,HandlerThread底層通過Looper消息隊列實現的,所以它是順序的執行每一個任務。可以通過Intent的方式開啓IntentService,IntentService通過handler將每一個intent加入HandlerThread子線程中的消息隊列,通過looper按順序一個個的取出並執行,執行完成後自動結束自己,不需要開發者手動關閉
    IntentService分析

Bitmap優化

主動釋放Bitmap資源

bitmap.recycle(); 
bitmap = null; 

在不加載圖片的前提下獲得圖片的寬高

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;

圖片大小壓縮,採樣率一般是2的指數,即1、2、4、8、16……

bitmapFactoryOptions.inSampleSize = 2;

圖片像素壓縮
Android中圖片有四種屬性,分別是:
ALPHA_8:每個像素佔用1byte內存
ARGB_4444:每個像素佔用2byte內存
ARGB_8888:每個像素佔用4byte內存 (默認)
RGB_565:每個像素佔用2byte內存
Android默認的顏色模式爲ARGB_8888,這個顏色模式色彩最細膩,顯示質量最高。但同樣的,佔用的內存也最大。 所以在對圖片效果不是特別高的情況下使用RGB_565(565沒有透明度屬性),如下:

        public static Bitmap readBitMap(Context context, intresId) {
            BitmapFactory.Options opt = newBitmapFactory.Options();
            opt.inPreferredConfig = Bitmap.Config.RGB_565;
            opt.inPurgeable = true;
            opt.inInputShareable = true;
            //獲取資源圖片 
            InputStreamis = context.getResources().openRawResource(resId);
            returnBitmapFactory.decodeStream(is, null, opt);
        }

Android框架相關

LeakCanary相關

相關播客

Retrofit相關

RxJava相關

okHttp相關

rxjava相關

mvp相關

以上移步筆者博客 學習筆記| OkHttp+Retrofit+Dagger2+RxJava+MVP架構

Java相關

該部分內容筆者按照該博客集合順序+閱讀源碼進行復習。
Java相關博客專題

collection裏面有什麼子類?

(其實面試的時候聽到這個問題的時候,你要知道,面試官是想考察List,Set)
list和set是實現了collection接口的。

  • List:
    1.可以允許重複的對象。
    2.可以插入多個null元素。
    3.是一個有序容器,保持了每個元素的插入順序,輸出的順序就是插入的順序。
    4.常用的實現類有 ArrayList、LinkedList 和 Vector。ArrayList 最爲流行,它提供了使用索引的隨意訪問,而 LinkedList 則對於經常需要從 List 中添加或刪除元素的場合更爲合適。

  • Set:
    1.不允許重複對象
    2.無序容器,你無法保證每個元素的存儲順序,TreeSet通過 Comparator 或者 Comparable 維護了一個排序順序。
    3.只允許一個 null 元素
    4.Set 接口最流行的幾個實現類是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基於HashMap 實現的 HashSet;TreeSet 還實現了 SortedSet 接口,因此 TreeSet 是一個根據其compare() 和 compareTo() 的定義進行排序的有序容器。

什麼場景下使用list,set,map?

  • 如果你經常會使用索引來對容器中的元素進行訪問,那麼 List 是你的正確的選擇。如果你已經知道索引了的話,那麼 List 的實現類比如 ArrayList 可以提供更快速的訪問,如果經常添加刪除元素的,那麼肯定要選擇LinkedList。
  • 如果你想容器中的元素能夠按照它們插入的次序進行有序存儲,那麼還是 List,因爲 List 是一個有序容器,它按照插入順序進行存儲。
  • 如果你想保證插入元素的唯一性,也就是你不想有重複值的出現,那麼可以選擇一個 Set 的實現類,比如 HashSet、LinkedHashSet 或者 TreeSet。所有 Set 的實現類都遵循了統一約束比如唯一性,而且還提供了額外的特性比如 TreeSet 還是一個 SortedSet,所有存儲於 TreeSet 中的元素可以使用 Java 裏的 Comparator 或者 Comparable 進行排序。LinkedHashSet 也按照元素的插入順序對它們進行存儲。
  • 如果你以鍵和值的形式進行數據存儲那麼 Map 是你正確的選擇。你可以根據你的後續需要從 Hashtable、HashMap、TreeMap 中進行選擇。

fail-fast機制

“快速失敗”也就是fail-fast,它是Java集合的一種錯誤檢測機制。當多個線程對集合進行結構上的改變的操作時,有可能會產生fail-fast機制。記住是有可能,而不是一定。例如:假設存在兩個線程(線程1、線程2),線程1通過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會拋出ConcurrentModificationException 異常,從而產生fail-fast機制。
相關博客

ArrayList和LinkedList的區別

  • Arraylist
    底層是基於動態數組,根據下表隨機訪問數組元素的效率高,向數組尾部添加元素的效率高;但是,刪除數組中的數據以及向數組中間添加數據效率低,因爲需要移動數組。例如最壞的情況是刪除第一個數組元素,則需要將第2至第n個數組元素各向前移動一位。而之所以稱爲動態數組,是因爲Arraylist在數組元素超過其容量大,Arraylist可以進行擴容(針對JDK1.8? 數組擴容後的容量是擴容前的1.5倍)

  • Linkedlist
    基於鏈表的動態數組,數據添加刪除效率高,只需要改變指針指向即可,但是訪問數據的平均效率低,需要對鏈表進行遍歷。

HashMap、Hashtable、ConcurrentHashMap的原理與區別

HashTable

  • 底層數組+鏈表實現,無論key還是value都不能爲null,線程安全,實現線程安全的方式是在修改數據時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化
  • 初始size爲11,擴容:newsize = olesize*2+1
  • 計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

  • 底層數組+鏈表實現,可以存儲null鍵和null值,線程不安全
  • 初始size爲16,擴容:newsize = oldsize*2,size一定爲2的n次冪
  • 擴容針對整個Map,每次擴容時,原來數組中的元素依次重新計算存放位置,並重新插入
  • 插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容)
  • 當Map中元素總數超過Entry數組的75%,觸發擴容操作,爲了減少鏈表長度,元素分配更均勻
  • 計算index方法:index = hash & (tab.length – 1)
    HashMap面試題

ConcurrentHashMap

  • 底層採用分段的數組+鏈表實現,線程安全
  • 通過把整個Map分爲N個Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。(讀操作不加鎖,由於HashEntry的value變量是 volatile的,也能保證讀取到最新的值。)
  • Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術
  • 有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖
  • 擴容:段內擴容(段內元素超過該段對應Entry數組長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容

詳細原理區別內容

Hash衝突如何解決?

開放定址法

這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=H(key)出現衝突時,以p爲基礎,產生另一個哈希地址p1,如果p1仍然衝突,再以p爲基礎,產生另一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi ,將相應元素存入其中。這種方法有一個通用的再散列函數形式:
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)爲哈希函數,m 爲表長,di稱爲增量序列。增量序列的取值方式不同,相應的再散列方式也不同。主要有以下三種:

  • 線性探測再散列
    dii=1,2,3,…,m-1
    這種方法的特點是:衝突發生時,順序查看錶中下一單元,直到找出一個空單元或查遍全表。
  • 二次探測再散列
    di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
    這種方法的特點是:衝突發生時,在表的左右進行跳躍式探測,比較靈活。
  • 僞隨機探測再散列

再哈希法

這種方法是同時構造多個不同的哈希函數:

Hi=RH1(key) i=1,2,…,k

當哈希地址Hi=RH1(key)發生衝突時,再計算Hi=RH2(key)……,直到衝突不再產生。這種方法不易產生聚集,但增加了計算時間。

鏈地址法

這種方法的基本思想是將所有哈希地址爲i的元素構成一個稱爲同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於經常進行插入和刪除的情況。

如何理解Java的多態?其中,重載和重寫有什麼區別?

多態是同一個行爲具有多個不同表現形式或形態的能力,多態是同一個接口,使用不同的實例而執行不同操作,多態就是程序運行期間才確定,一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法。
多態存在的三個必要條件是:繼承,重寫,父類引用指向子類引用。
多態的三個實現方式是:重寫,接口,抽象類和抽象方法。

final關鍵字的用法?

final 可以修飾類、變量和方法。修飾類代表這個類不可被繼承。修飾變量代表此變量不可被改變。修飾方法表示此方法不可被重寫 (override)。

死鎖是怎麼導致的?如何定位死鎖?

某個任務在等待另一個任務,而後者又等待別的任務,這樣一直下去,直到這個鏈條上的任務又在等待第一個任務釋放鎖。這得到了一個任務之間互相等待的連續循環,沒有哪個線程能繼續。這被稱之爲死鎖。當以下四個條件同時滿足時,就會產生死鎖:
(1) 互斥條件。任務所使用的資源中至少有一個是不能共享的。
(2) 任務必須持有一個資源,同時等待獲取另一個被別的任務佔有的資源。
(3) 資源不能被強佔。
(4) 必須有循環等待。一個任務正在等待另一個任務所持有的資源,後者又在等待別的任務所持有的資源,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。
首先模擬問題定位,使用jstack,使用 jps或者系統的ps命令、任務管理器等工具,確定進程ID。
然後獲取線程棧:

${JAVA_HOME}\bin\jstack your_pid

避免死鎖的方法思路:

儘量避免使用多個鎖,並且只有需要的時候才持有鎖。
如果使用多個鎖,儘量設計好鎖的獲取順序。例如銀行家算法。
使用帶超時的方法,爲程序帶來更多的可控性。

反射【待補全】

說下java中的線程創建方式,線程池的工作原理。

java中有三種創建線程的方式,或者說四種

  1. 繼承Thread類實現多線程
  2. 實現Runnable接口
  3. 實現Callable接口
  4. 通過線程池

線程池的工作原理:線程池可以減少創建和銷燬線程的次數,從而減少系統資源的消耗,當一個任務提交到線程池時
a. 首先判斷核心線程池中的線程是否已經滿了,如果沒滿,則創建一個核心線程執行任務,否則進入下一步
b. 判斷工作隊列是否已滿,沒有滿則加入工作隊列,否則執行下一步
c. 判斷線程數是否達到了最大值,如果不是,則創建非核心線程執行任務,否則執行飽和策略,默認拋出異常

進程與線程的區別

進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例。
線程是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位,一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。

設計模式相關

說下你所知道的設計模式與使用場景

建造者模式

將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
使用場景比如最常見的AlertDialog,拿我們開發過程中舉例,比如Camera開發過程中,可能需要設置一個初始化的相機配置,設置攝像頭方向,閃光燈開閉,成像質量等等,這種場景下就可以使用建造者模式

裝飾者模式

動態的給一個對象添加一些額外的職責,就增加功能來說,裝飾模式比生成子類更爲靈活。裝飾者模式可以在不改變原有類結構的情況下曾強類的功能,比如Java中的BufferedInputStream 包裝FileInputStream,舉個開發中的例子,比如在我們現有網絡框架上需要增加新的功能,那麼再包裝一層即可,裝飾者模式解決了繼承存在的一些問題,比如多層繼承代碼的臃腫,使代碼邏輯更清晰

生產者消費者模式

生產者消費者模式並不是GOF提出的23種設計模式之一,23種設計模式都是建立在面向對象的基礎之上的,但其實面向過程的編程中也有很多高效的編程模式,生產者消費者模式便是其中之一,它是我們編程過程中最常用的一種設計模式。

在實際的軟件開發過程中,經常會碰到如下場景:某個模塊負責產生數據,這些數據由另一個模塊來負責處理(此處的模塊是廣義的,可以是類、函數、線程、進程等)。產生數據的模塊,就形象地稱爲生產者;而處理數據的模塊,就稱爲消費者。

單單抽象出生產者和消費者,還夠不上是生產者/消費者模式。該模式還需要有一個緩衝區處於生產者和消費者之間,作爲一箇中介。生產者把數據放入緩衝區,而消費者從緩衝區取出數據。

JVM相關

談一下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 的。

GC的兩種判定方法(對象存活判定):

引用計數和可達性分析。

引用計數方式

最基本的形態就是讓每個被管理的對象與一個引用計數器關聯在一起,該計數器記錄着該對象當前被引用的次數,每當創建一個新的引用指向該對象時其計數器就加1,每當指向該對象的引用失效時計數器就減1。當該計數器的值降到0就認爲對象死亡。

可達性分析算法

將GC Roots對象作爲起始節點,向下搜索,搜索走過的路徑爲引用鏈;當一個對象到GC Roots沒有引用鏈時,則該對象是不可用的;

可作爲GC Roots的對象:

  1. 方法區中靜態屬性引用的對象

  2. 方法區中常量引用的對象

  3. 虛擬機棧引用的對象 (棧幀中本地變量表)

  4. 本地方法棧中JNI引用的對象 (Native方法)

GC的三種收集方法:

標記清除、標記整理、複製算法的原理與特點,分別用在什麼地方,如果讓你優化收集方法,有什麼思路?

標記清除算法

最基礎的收集算法,其他收集算法都是基於這種思想。標記清除算法分爲“標記”和“清除”兩個階段:首先標記出需要回收的對象,標記完成之後統一清除對象。
它的主要缺點:
①.標記和清除過程效率不高 。
②.標記清除之後會產生大量不連續的內存碎片。

標記整理

標記操作和“標記-清除”算法一致,後續操作不只是直接清理對象,而是在清理無用對象完成後讓所有存活的對象都向一端移動,並更新引用其對象的指針。主要缺點:在標記-清除的基礎上還需進行對象的移動,成本相對較高,好處則是不會產生內存碎片。

複製算法

它將可用內存容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊用完之後,就將還存活的對象複製到另外一塊上面,然後在把已使用過的內存空間一次理掉。這樣使得每次都是對其中的一塊進行內存回收,不會產生碎片等情況,只要移動堆訂的指針,按順序分配內存即可,實現簡單,運行高效。主要缺點:內存縮小爲原來的一半。

雙親委派模型:

Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

啓動類加載器,負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即時放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被java程序直接引用。
擴展類加載器:負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用該類加載器。
應用程序類加載器:負責加載用戶路徑上所指定的類庫,開發者可以直接使用這個類加載器,也是默認的類加載器。 三種加載器的關係:啓動類加載器->擴展類加載器->應用程序類加載器->自定義類加載器。
這種關係即爲類加載器的雙親委派模型。其要求除啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不以繼承關係實現,而是用組合的方式來複用父類的代碼。

雙親委派模型的工作過程:如果一個類加載器接收到了類加載的請求,它首先把這個請求委託給他的父類加載器去完成,每個層次的類加載器都是如此,因此所有的加載請求都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它在搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。

好處:java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己寫了一個名爲java.lang.Object的類,並放在程序的Classpath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行爲也無法保證,應用程序也會變得一片混亂。

實現:在java.lang.ClassLoader的loadClass()方法中,先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父加載失敗,則拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。

高併發相關

ThreadLocal的設計理念與作用

ThreadPool用法與優勢(這裏在Android SDK原生的AsyncTask底層也有使用)

線程池的底層實現和工作原理(建議寫一個雛形簡版源碼實現)

計算機網絡相關

TCP/IP五層模型


在TCP/IP四層協議中,網絡層又被稱爲網際層(用網際層這個名字是強調這一層是爲了解決不同網絡的互連問題),而數據鏈路層與物理層合併爲網絡接口層。

TCP協議

  • Transmission Control Protocol,傳輸控制協議
  • 面向連接的協議
  • 需要三次握手建立連接
  • 需要四次揮手斷開連接
  • TCP報頭最小長度:20字節

三次握手過程

  1. 客戶端發送:SYN = 1, SEQ = X, 端口號
  2. 服務器回覆:SYN = 1, ACK = X + 1, SEQ = Y
  3. 客戶端發送:ACK = Y + 1, SEQ = X + 1

確認應答信號ACK = 收到的SEQ + 1。 連接建立中,同步信號SYN始終爲1。連接建立後,同步信號SYN=0。

四次揮手過程

  1. A向B提出停止連接請求,FIN = 1
  2. B收到,ACK = 1
  3. B向A提出停止連接請求,FIN = 1
  4. A收到,ACK = 1
SYN:同步位
seq:編號位
ACK:確認位
ack:確認編號位

算法相關

面試中的 10 大排序算法總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章