2019年Android中高級工程師部分面試題

一、中級面試題

1、Activity生命週期

2、onStart()與onResume()有什麼區別?

onStart()是activity界面被顯示出來的時候執行的,但不能與它交互;
onResume()是當該activity與用戶能進行交互時被執行,用戶可以獲得activity的焦點,能夠與用戶交互。

3、Activity啓動流程

startActivity最終都會調用startActivityForResult,通過ActivityManagerProxy調用system_server進程中ActivityManagerService的startActvity方法,如果需要啓動的Activity所在進程未啓動,則調用Zygote孵化應用進程,進程創建後會調用應用的ActivityThread的main方法,main方法調用attach方法將應用進程綁定到ActivityManagerService(保存應用的ApplicationThread的代理對象)並開啓loop循環接收消息。ActivityManagerService通過ApplicationThread的代理髮送Message通知啓動Activity,ActivityThread內部Handler處理handleLaunchActivity,依次調用performLaunchActivity,handleResumeActivity(即activity的onCreate,onStart,onResume)。
 

4、Android類加載器

Android平臺上虛擬機運行的是Dex字節碼,一種對class文件優化的產物,傳統Class文件是一個Java源碼文件會生成一個.class文件,而Android是把所有Class文件進行合併,優化,然後生成一個最終的class.dex,目的是把不同class文件重複的東西只需保留一份,如果我們的Android應用不進行分dex處理,最後一個應用的apk只會有一個dex文件。
Android中常用的有兩種類加載器,DexClassLoader和PathClassLoader,它們都繼承於BaseDexClassLoader。區別在於調用父類構造器時,DexClassLoader多傳了一個optimizedDirectory參數,這個目錄必須是內部存儲路徑,用來緩存系統創建的Dex文件。而PathClassLoader該參數爲null,只能加載內部存儲目錄的Dex文件。所以我們可以用DexClassLoader去加載外部的apk。

5、Android消息機制

  • 應用啓動是從ActivityThread的main開始的,先是執行了Looper.prepare(),該方法先是new了一個Looper對象,在私有的構造方法中又創建了MessageQueue作爲此Looper對象的成員變量,Looper對象通過ThreadLocal綁定MainThread中;

  • 當我們創建Handler子類對象時,在構造方法中通過ThreadLocal獲取綁定的Looper對象,並獲取此Looper對象的成員變量MessageQueue作爲該Handler對象的成員變量;

  • 在子線程中調用上一步創建的Handler子類對象的sendMesage(msg)方法時,在該方法中將msg的target屬性設置爲自己本身,同時調用成員變量MessageQueue對象的enqueueMessag()方法將msg放入MessageQueue中;

  • 主線程創建好之後,會執行Looper.loop()方法,該方法中獲取與線程綁定的Looper對象,繼而獲取該Looper對象的成員變量MessageQueue對象,並開啓一個會阻塞(不佔用資源)的死循環,只要MessageQueue中有msg,就會獲取該msg,並執行msg.target.dispatchMessage(msg)方法(msg.target即上一步引用的handler對象),此方法中調用了我們第二步創建handler子類對象時覆寫的handleMessage()方法,之後將該msg對象存入回收池;

6、Looper.loop()爲什麼不會阻塞主線程

Android是基於事件驅動的,即所有Activity的生命週期都是通過Handler事件驅動的。loop方法中會調用MessageQueue的next方法獲取下一個message,當沒有消息時,基於Linux pipe/epoll機制會阻塞在loop的queue.next()中的nativePollOnce()方法裏,並不會消耗CPU。

7、IdleHandler (閒時機制)

IdleHandler是一個回調接口,可以通過MessageQueue的addIdleHandler添加實現類。當MessageQueue中的任務暫時處理完了(沒有新任務或者下一個任務延時在之後),這個時候會回調這個接口,返回false,那麼就會移除它,返回true就會在下次message處理完了的時候繼續回調。

8、同步屏障機制(sync barrier)

同步屏障可以通過MessageQueue.postSyncBarrier函數來設置。該方法發送了一個沒有target的Message到Queue中,在next方法中獲取消息時,如果發現沒有target的Message,則在一定的時間內跳過同步消息,優先執行異步消息。再換句話說,同步屏障爲Handler消息機制增加了一種簡單的優先級機制,異步消息的優先級要高於同步消息。在創建Handler時有一個async參數,傳true表示此handler發送的時異步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保證UI繪製優先執行。

9、View的繪製原理

View的繪製從ActivityThread類中Handler的處理RESUME_ACTIVITY事件開始,在執行performResumeActivity之後,創建Window以及DecorView並調用WindowManager的addView方法添加到屏幕上,addView又調用ViewRootImpl的setView方法,最終執行performTraversals方法,依次執行performMeasure,performLayout,performDraw。也就是view繪製的三大過程。
measure過程測量view的視圖大小,最終需要調用setMeasuredDimension方法設置測量的結果,如果是ViewGroup需要調用measureChildren或者measureChild方法進而計算自己的大小。
layout過程是擺放view的過程,View不需要實現,通常由ViewGroup實現,在實現onLayout時可以通過getMeasuredWidth等方法獲取measure過程測量的結果進行擺放。
draw過程先是繪製背景,其次調用onDraw()方法繪製view的內容,再然後調用dispatchDraw()調用子view的draw方法,最後繪製滾動條。ViewGroup默認不會執行onDraw方法,如果複寫了onDraw(Canvas)方法,需要調用 setWillNotDraw(false);清楚不需要繪製的標記。
 

10、什麼是MeasureSpec

MeasureSpec代表一個32位int值,高兩位代表SpecMode(測量模式),低30位代表SpecSize(具體大小)。
SpecMode有三類:

  • UNSPECIFIED 表示父容器不對View有任何限制,一般用於系統內部,表示一種測量狀態;

  • EXACTLY 父容器已經檢測出view所需的精確大小,這時候view的最終大小SpecSize所指定的值,相當於match_parent或指定具體數值。

  • AT_MOST 父容器指定一個可用大小即SpecSize,view的大小不能大於這個值,具體多大要看view的具體實現,相當於wrap_content。

11、getWidth()方法和getMeasureWidth()區別呢?

首先getMeasureWidth()方法在measure()過程結束後就可以獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值則是通過視圖右邊的座標減去左邊的座標計算出來的。

12、事件分發機制

requestLayout,invalidate,postInvalidate區別與聯繫

相同點:三個方法都有刷新界面的效果。
不同點:invalidate和postInvalidate只會調用onDraw()方法;requestLayout則會重新調用onMeasure、onLayout、onDraw。

調用了invalidate方法後,會爲該View添加一個標記位,同時不斷向父容器請求刷新,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪製需要重繪的視圖)。
調用requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調用三大流程,從measure開始,對於每一個含有標記位的view及其子View都會進行測量onMeasure、佈局onLayout、繪製onDraw。

13、Binder機制,共享內存實現原理

爲什麼使用Binder?

概念
進程隔離
進程空間劃分:用戶空間(User Space)/內核空間(Kernel Space)
系統調用:用戶態與內核態

原理
跨進程通信是需要內核空間做支持的。傳統的 IPC 機制如管道、Socket 都是內核的一部分,因此通過內核支持來實現進程間通信自然是沒問題的。但是 Binder 並不是 Linux 系統內核的一部分,那怎麼辦呢?這就得益於 Linux 的動態內核可加載模塊(Loadable Kernel Module,LKM)的機制;模塊是具有獨立功能的程序,它可以被單獨編譯,但是不能獨立運行。它在運行時被鏈接到內核作爲內核的一部分運行。這樣,Android 系統就可以通過動態添加一個內核模塊運行在內核空間,用戶進程之間通過這個內核模塊作爲橋樑來實現通信。

在 Android 系統中,這個運行在內核空間,負責各個用戶進程通過 Binder 實現通信的內核模塊就叫 Binder 驅動(Binder Dirver)。

那麼在 Android 系統中用戶進程之間是如何通過這個內核模塊(Binder 驅動)來實現通信的呢?難道是和前面說的傳統 IPC 機制一樣,先將數據從發送方進程拷貝到內核緩存區,然後再將數據從內核緩存區拷貝到接收方進程,通過兩次拷貝來實現嗎?顯然不是,否則也不會有開篇所說的 Binder 在性能方面的優勢了。

這就不得不通道 Linux 下的另一個概念:內存映射

Binder IPC 機制中涉及到的內存映射通過 mmap() 來實現,mmap() 是操作系統中一種內存映射的方法。內存映射簡單的講就是將用戶空間的一塊內存區域映射到內核空間。映射關係建立後,用戶對這塊內存區域的修改可以直接反應到內核空間;反之內核空間對這段區域的修改也能直接反應到用戶空間。
一次完整的 Binder IPC 通信過程通常是這樣:

  • 首先 Binder 驅動在內核空間創建一個數據接收緩存區;

  • 接着在內核空間開闢一塊內核緩存區,建立內核緩存區和內核中數據接收緩存區之間的映射關係,以及內核中數據接收緩存區和接收進程用戶空間地址的映射關係;

  • 發送方進程通過系統調用 copyfromuser() 將數據 copy 到內核中的內核緩存區,由於內核緩存區和接收進程的用戶空間存在內存映射,因此也就相當於把數據發送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。

Binder通訊模型
Binder是基於C/S架構的,其中定義了4個角色:Client、Server、Binder驅動和ServiceManager。

  • Binder驅動:類似網絡通信中的路由器,負責將Client的請求轉發到具體的Server中執行,並將Server返回的數據傳回給Client。

  • ServiceManager:類似網絡通信中的DNS服務器,負責將Client請求的Binder描述符轉化爲具體的Server地址,以便Binder驅動能夠轉發給具體的Server。Server如需提供Binder服務,需要向ServiceManager註冊。
    具體的通訊過程

  • Server向ServiceManager註冊。Server通過Binder驅動向ServiceManager註冊,聲明可以對外提供服務。ServiceManager中會保留一份映射表。

  • Client向ServiceManager請求Server的Binder引用。Client想要請求Server的數據時,需要先通過Binder驅動向ServiceManager請求Server的Binder引用(代理對象)。

  • 向具體的Server發送請求。Client拿到這個Binder代理對象後,就可以通過Binder驅動和Server進行通信了。

  • Server返回結果。Server響應請求後,需要再次通過Binder驅動將結果返回給Client。

ServiceManager是一個單獨的進程,那麼Server與ServiceManager通訊是靠什麼呢?
當Android系統啓動後,會創建一個名稱爲servicemanager的進程,這個進程通過一個約定的命令BINDERSETCONTEXT_MGR向Binder驅動註冊,申請成爲爲ServiceManager,Binder驅動會自動爲ServiceManager創建一個Binder實體。並且這個Binder實體的引用在所有的Client中都爲0,也就說各個Client通過這個0號引用就可以和ServiceManager進行通信。Server通過0號引用向ServiceManager進行註冊,Client通過0號引用就可以獲取到要通信的Server的Binder引用。
 

14、序列化的方式

Serializable是Java提供的一個序列化接口,是一個空接口,用於標示對象是否可以支持序列化,通過ObjectOutputStrean及ObjectInputStream實現序列化和反序列化的過程。注意可以爲需要序列化的對象設置一個serialVersionUID,在反序列化的時候系統會檢測文件中的serialVersionUID是否與當前類的值一致,如果不一致則說明類發生了修改,反序列化失敗。因此對於可能會修改的類最好指定serialVersionUID的值。
Parcelable是Android特有的一個實現序列化的接口,在Parcel內部包裝了可序列化的數據,可以在Binder中自由傳輸。序列化的功能由writeToParcel方法來完成,最終通過Parcel的一系列write方法完成。反序列化功能由CREAOR來完成,其內部標明瞭如何創建序列化對象和數組,並通過Parcel的一系列read方法來完成反序列化的過程。

15、Fragment的懶加載實現

Fragment可見狀態改變時會被調用setUserVisibleHint()方法,可以通過複寫該方法實現Fragment的懶加載,但需要注意該方法可能在onVIewCreated之前調用,需要確保界面已經初始化完成的情況下再去加載數據,避免空指針。
 

16、RecyclerView與ListView(緩存原理,區別聯繫,優缺點)

緩存區別:

  • 層級不同:
    ListView有兩級緩存,在屏幕與非屏幕內。

RecyclerView比ListView多兩級緩存,支持多個離屏ItemView緩存(匹配pos獲取目標位置的緩存,如果匹配則無需再次bindView),支持開發者自定義緩存處理邏輯,支持所有RecyclerView共用同一個RecyclerViewPool(緩存池)。

  • 緩存不同:
    ListView緩存View。

RecyclerView緩存RecyclerView.ViewHolder,抽象可理解爲:
View + ViewHolder(避免每次createView時調用findViewById) + flag(標識狀態);

優點
RecylerView提供了局部刷新的接口,通過局部刷新,就能避免調用許多無用的bindView。
RecyclerView的擴展性更強大(LayoutManager、ItemDecoration等)。

17、Android兩種虛擬機區別與聯繫

Android中的Dalvik虛擬機相較於Java虛擬機針對手機的特點做了很多優化。
Dalvik基於寄存器,而JVM基於棧。在基於寄存器的虛擬機裏,可以更爲有效的減少冗餘指令的分發和減少內存的讀寫訪問。
Dalvik經過優化,允許在有限的內存中同時運行多個虛擬機的實例,並且每一個 Dalvik應用作爲一個獨立的Linux進程執行。
java虛擬機運行的是java字節碼。(java類會被編譯成一個或多個字節碼.class文件,打包到.jar文件中,java虛擬機從相應的.class文件和.jar文件中獲取相應的字節碼)
Dalvik運行的是自定義的.dex字節碼格式。(java類被編譯成.class文件後,會通過一個dx工具將所有的.class文件轉換成一個.dex文件,然後dalvik虛擬機會從其中讀取指令和數據)
 

18、adb常用命令行

查看當前連接的設備:adb devices
安裝應用:adb install -r <apk_path> -r表示覆蓋安裝
卸載apk:adb uninstall <packagename>

ADB 用法大全

19、apk打包流程

  • aapt工具打包資源文件,生成R.java文件

  • aidl工具處理AIDL文件,生成對應的.java文件

  • javac工具編譯Java文件,生成對應的.class文件

  • 把.class文件轉化成Davik VM支持的.dex文件

  • apkbuilder工具打包生成未簽名的.apk文件

  • jarsigner對未簽名.apk文件進行簽名

  • zipalign工具對簽名後的.apk文件進行對齊處理

Android應用程序(APK)的編譯打包過程

20、apk安裝流程

  • 複製APK到/data/app目錄下,解壓並掃描安裝包。

  • 資源管理器解析APK裏的資源文件。

  • 解析AndroidManifest文件,並在/data/data/目錄下創建對應的應用數據目錄。

  • 然後對dex文件進行優化,並保存在dalvik-cache目錄下。

  • 將AndroidManifest文件解析出的四大組件信息註冊到PackageManagerService中。

  • 安裝完成後,發送廣播。

21、apk瘦身

APK主要由以下幾部分組成:

  • META-INF/ :包含了簽名文件CERT.SF、CERT.RSA,以及 manifest 文件MANIFEST.MF。

  • assets/ : 存放資源文件,這些資源不會被編譯成二進制。

  • lib/ :包含了一些引用的第三方庫。

  • resources.arsc :包含res/values/中所有資源,例如strings,styles,以及其他未被包含在resources.arsc中的資源路徑信息,例如layout 文件、圖片等。

  • res/ :包含res中沒有被存放到resources.arsc的資源。

  • classes.dex :經過dx編譯能被android虛擬機理解的Java源碼文件。

  • AndroidManifest.xml :清單文件

其中佔據較大內存的是res資源、lib、class.dex,因此我們可以從下面的幾個方面下手:

  • 代碼方面可以通過代碼混淆,這個一般都會去做。平時也可以刪除一些沒有使用類。

  • 去除無用資源。使用lint工具來檢測沒有使用到的資源,或者在gradle中配置shrinkResources來刪除包括庫中所有的無用的資源,需要配合proguard壓縮代碼使用。這裏需要注意項目中是否存在使用getIdentifier方式獲取資源,這種方式類似反射lint及shrinkResources無法檢測情況。如果存在這種方式,則需要配置一個keep.xml來記錄使用反射獲取的資源。(https://developer.android.com/studio/build/shrink-code)[壓縮代碼和資源]

  • 去除無用國際化支持。對於一些第三庫來說(如support),因爲國際化的問題,它們可能會支持了幾十種語言,但我們的應用可能只需要支持幾種語言,可以通過配置resConfigs提出不要的語言支持。

  • 不同尺寸的圖片支持。通常情況下只需要一套xxhpi的圖片就可以支持大部分分辨率的要求了,因此,我們只需要保留一套圖片。

  • 圖片壓縮。 png壓縮或者使用webP圖片,完美支持需要Android版本4.2.1+

  • 使用矢量圖形。簡單的圖標可以使用矢量圖片。

HTTP緩存機制

緩存的響應頭:

Cache-control:標明緩存的最大存活時常;
Date:服務器告訴客戶端,該資源的發送時間;
Expires:表示過期時間(該字段是1.0的東西,當cache-control和該字段同時存在的條件下,cache-control的優先級更高);
Last-Modified:服務器告訴客戶端,資源的最後修改時間;
還有一個字段,這個圖沒給出,就是E-Tag:當前資源在服務器的唯一標識,可用於判斷資源的內容是否被修改了。
除以上響應頭字段以外,還需瞭解兩個相關的Request請求頭:If-Modified-since、If-none-Match。這兩個字段是和Last-Modified、E-Tag配合使用的。大致流程如下:
服務器收到請求時,會在200 OK中回送該資源的Last-Modified和ETag頭(服務器支持緩存的情況下才會有這兩個頭哦),客戶端將該資源保存在cache中,並記錄這兩個屬性。當客戶端需要發送相同的請求時,根據Date + Cache-control來判斷是否緩存過期,如果過期了,會在請求中攜帶If-Modified-Since和If-None-Match兩個頭。兩個頭的值分別是響應中Last-Modified和ETag頭的值。服務器通過這兩個頭判斷本地資源未發生變化,客戶端不需要重新下載,返回304響應。

23、組件化

  • 在gradle.properties聲明一個變量用於控制是否是調試模式,並在dependencies中根據是否是調試模式依賴必要組件。

  • 通過resourcePrefix規範module中資源的命名前綴。

  • 組件間通過ARouter完成界面跳轉和功能調用。

24、MVP

三方庫

okhttp原理

OkHttpClient通過newCall可以將一個Request構建成一個Call,Call表示準備被執行的請求。Call調用executed或enqueue會調用Dispatcher對應的方法在當前線程或者一步開始執行請求,經過RealInterceptorChain獲得最終結果,RealInterceptorChain是一個攔截器鏈,其中依次包含以下攔截器:

  • 自定義的攔截器

  • retryAndFollowUpInterceptor 請求失敗重試

  • BridgeInterceptor 爲請求添加請求頭,爲響應添加響應頭

  • CacheInterceptor 緩存get請求

  • ConnectInterceptor 連接相關的攔截器,分配一個Connection和HttpCodec爲最終的請求做準備

  • CallServerInterceptor 該攔截器就是利用HttpCodec完成最終請求的發送

okhttp源碼解析

25、Retrofit的實現與原理

Retrofit採用動態代理,創建聲明service接口的實現對象。當我們調用service的方法時候會執行InvocationHandler的invoke方法。在這方法中:首先,通過method把它轉換成ServiceMethod,該類是對聲明方法的解析,可以進一步將設定參數變成Request ;然後,通過serviceMethod, args獲取到okHttpCall 對象,實際調用okhttp的網絡請求方法就在該類中,並且會使用serviceMethod中的responseConverter對ResponseBody轉化;最後,再把okHttpCall進一步封裝成聲明的返回對象(默認是ExecutorCallbackCall,將原本call的回調轉發至UI線程)。

Retrofit2使用詳解及從源碼中解析原理 
Retrofit2 完全解析 探索與okhttp之間的關係

26、ARouter原理

可能是最詳細的ARouter源碼分析

27、RxLifecycle原理

在Activity中,定義一個Observable(Subject),在不同的生命週期發射不同的事件;
通過compose操作符(內部實際上還是依賴takeUntil操作符),定義了上游數據,當其接收到Subject的特定事件時,取消訂閱;
Subject的特定事件並非是ActivityEvent,而是簡單的boolean,它已經內部通過combineLast操作符進行了對應的轉化。

28、RxJava

Java

類的加載機制

程序在啓動的時候,並不會一次性加載程序所要用的所有class文件,而是根據程序的需要,通過Java的類加載機制(ClassLoader)來動態加載某個class文件到內存當中的,從而只有class文件被載入到了內存之後,才能被其它class所引用。所以ClassLoader就是用來動態加載class文件到內存當中用的。
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲連接(Linking)。

  • 加載:查找和導入Class文件;

  • 鏈接:把類的二進制數據合併到JRE中;
    (a) 驗證:檢查載入Class文件數據的正確性;

(b) 準備:給類的靜態變量分配存儲空間;
(c) 解析:將符號引用轉成直接引用;

  • 初始化:對類的靜態變量,靜態代碼塊執行初始化操作

29、什麼時候發生類初始化

  • 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

  • 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例左後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄鎖對應的類沒有進行過初始化時。

30、雙親委派模型

Java中存在3種類加載器:
(1) Bootstrap ClassLoader : 將存放於<JAVA_HOME>lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被Java程序直接引用 。
(2) Extension ClassLoader : 將<JAVA_HOME>libext目錄下的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫加載。開發者可以直接使用擴展類加載器。
(3) Application ClassLoader : 負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可直接使用。
每個ClassLoader實例都有一個父類加載器的引用(不是繼承關係,是一個包含的關係),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但是可以用做其他ClassLoader實例的父類加載器。
當一個ClassLoader 實例需要加載某個類時,它會試圖在親自搜索這個類之前先把這個任務委託給它的父類加載器,這個過程是由上而下依次檢查的,首先由頂層的類加載器Bootstrap ClassLoader進行加載,如果沒有加載到,則把任務轉交給Extension ClassLoader加載,如果也沒有找到,則轉交給AppClassLoader進行加載,還是沒有的話,則交給委託的發起者,由它到指定的文件系統或者網絡等URL中進行加載類。還沒有找到的話,則會拋出CLassNotFoundException異常。否則將這個類生成一個類的定義,並將它加載到內存中,最後返回這個類在內存中的Class實例對象。

31、爲什麼使用雙親委託模型

JVM在判斷兩個class是否相同時,不僅要判斷兩個類名是否相同,還要判斷是否是同一個類加載器加載的。

  • 避免重複加載,父類已經加載了,則子CLassLoader沒有必要再次加載。

  • 考慮安全因素,假設自定義一個String類,除非改變JDK中CLassLoader的搜索類的默認算法,否則用戶自定義的CLassLoader如法加載一個自己寫的String類,因爲String類在啓動時就被引導類加載器Bootstrap CLassLoader加載了。

32、HashMap原理,Hash衝突

在JDK1.6,JDK1.7中,HashMap採用數組+鏈表實現,即使用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。但是當位於一個鏈表中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。而JDK1.8中,HashMap採用位數組+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。
當鏈表數組的容量超過初始容量*加載因子(默認0.75)時,再散列將鏈表數組擴大2倍,把原鏈表數組的搬移到新的數組中。爲什麼需要使用加載因子?爲什麼需要擴容呢?因爲如果填充比很大,說明利用的空間很多,如果一直不進行擴容的話,鏈表就會越來越長,這樣查找的效率很低,擴容之後,將原來鏈表數組的每一個鏈表分成奇偶兩個子鏈表分別掛在新鏈表數組的散列位置,這樣就減少了每個鏈表的長度,增加查找效率。
HashMap是非線程安全的,HashTable、ConcurrentHashMap是線程安全的。
HashMap的鍵和值都允許有null存在,而HashTable、ConcurrentHashMap則都不行。
因爲線程安全、哈希效率的問題,HashMap效率比HashTable、ConcurrentHashMap的都要高。
HashTable裏使用的是synchronized關鍵字,這其實是對對象加鎖,鎖住的都是對象整體,當Hashtable的大小增加到一定的時候,性能會急劇下降,因爲迭代時需要被鎖定很長的時間。
ConcurrentHashMap引入了分割(Segment),可以理解爲把一個大的Map拆分成N個小的HashTable,在put方法中,會根據hash(paramK.hashCode())來決定具體存放進哪個Segment,如果查看Segment的put操作,我們會發現內部使用的同步機制是基於lock操作的,這樣就可以對Map的一部分(Segment)進行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操作,保證同步的時候,鎖住的不是整個Map(HashTable就是這麼做的),相對於HashTable提高了多線程環境下的性能,因此HashTable已經被淘汰了。

Java中HashMap底層實現原理(JDK1.8)源碼分析

33、什麼是Fail-Fast機制

Fail-Fast是Java集合的一種錯誤檢測機制。當遍歷集合的同時修改集合或者多個線程對集合進行結構上的改變的操作時,有可能會產生fail-fast機制,記住是有可能,而不是一定。其實就是拋出ConcurrentModificationException 異常。
集合的迭代器在調用next()、remove()方法時都會調用checkForComodification()方法,該方法主要就是檢測modCount == expectedModCount ? 若不等則拋出ConcurrentModificationException 異常,從而產生fail-fast機制。modCount是在每次改變集合數量時會改變的值。

Java提高篇(三四)-----fail-fast機制

34、Java泛型

Java泛型詳解

35、Java多線程中調用wait() 和 sleep()方法有什麼不同?

Java程序中wait 和 sleep都會造成某種形式的暫停,它們可以滿足不同的需要。wait()方法用於線程間通信,如果等待條件爲真且其它線程被喚醒時它會釋放鎖,而 sleep()方法僅僅釋放CPU資源或者讓當前線程停止執行一段時間,但不會釋放鎖。

36、volatile的作用和原理

Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼,最終需要轉化爲彙編指令在CPU上執行。
volatile是輕量級的synchronized(volatile不會引起線程上下文的切換和調度),它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。
由於內存訪問速度遠不及CPU處理速度,爲了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存後在進行操作,但操作完不知道何時會寫到內存。普通共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。如果對聲明瞭volatile的變量進行寫操作,JVM就會想處理器發送一條Lock前綴的指令,表示將當前處理器緩存行的數據寫回到系統內存。

37、一個int變量,用volatile修飾,多線程去操作++,線程安全嗎?

不安全。volatile只能保證可見性,並不能保證原子性。i++實際上會被分成多步完成:1)獲取i的值;2)執行i+1;3)將結果賦值給i。volatile只能保證這3步不被重排序,多線程情況下,可能兩個線程同時獲取i,執行i+1,然後都賦值結果2,實際上應該進行兩次+1操作。

38、那如何才能保證i++線程安全?

可以使用java.util.concurrent.atomic包下的原子類,如AtomicInteger。
其實現原理是採用CAS自旋操作更新值。CAS即compare and swap的縮寫,中文翻譯成比較並交換。CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。自旋就是不斷嘗試CAS操作直到成功爲止。

39、CAS實現原子操作會出現什麼問題?

  • ABA問題。因爲CAS需要在操作之的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成,有變成A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但實際上發生了變化。ABA問題可以通過添加版本號來解決。Java 1.5開始,JDK的Atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。

  • 循環時間長開銷大。pause指令優化。

  • 只能保證一個共享變量的原子操作。可以合併成一個對象進行CAS操作。

40、synchronized

Java中每個對象都可以作爲鎖:

  • 對於普通同步方法,鎖是當前實例對象;

  • 對於靜態同步方法,鎖是當前類的Class對象;

  • 對於同步方法塊,鎖是括號中配置的對象;

當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。synchronized用的鎖是存在Java對象頭裏的MarkWord,通常是32bit或者64bit,其中最後2bit表示鎖標誌位

java對象結構
Java SE1.6爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,在1.6中鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾種狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級。

41、偏向鎖

偏向鎖獲取過程:

  • 訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01,確認爲可偏向狀態。

  • 如果爲可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟5,否則進入步驟3。

  • 如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置爲當前線程ID,然後執行5;如果競爭失敗,執行4。

  • 如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。(撤銷偏向鎖的時候會導致stop the word)

  • 執行同步代碼。

42、輕量級鎖

  • 在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲“01”狀態,是否爲偏向鎖爲“0”),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。

  • 拷貝對象頭中的Mark Word複製到鎖記錄中;

  • 拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。如果更新成功,則執行步驟4,否則執行步驟5。

  • 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態。

  • 如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了不讓線程阻塞,而採用循環去獲取鎖的過程。
    自旋

如果持有鎖的線程能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖後即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
但是線程自旋是需要消耗cup的,說白了就是讓cup在做無用功,如果一直獲取不到鎖,那線程也不能一直佔用cup自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。

43、線程池

好處:1)降低資源消耗;2)提高相應速度;3)提高線程的可管理性。
線程池的實現原理:

  • 當提交一個新任務到線程池時,判斷核心線程池裏的線程是否都在執行。如果不是,則創建一個新的線程執行任務。如果核心線程池的線程都在執行任務,則進入下個流程。

  • 判斷工作隊列是否已滿。如果未滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。

  • 判斷線程池是否都處於工作狀態。如果沒有,則創建一個新的工作線程來執行任務。如果滿了,則交給飽和策略來處理這個任務。

44、假如有n個網絡線程,你需要當n個網絡線程完成之後,再去做數據處理,你會怎麼解決?

這題考的其實是多線程同步的問題。這種情況可以可以使用thread.join();join方法會阻塞直到thread線程終止才返回。更復雜一點的情況也可以使用CountDownLatch,CountDownLatch的構造接收一個int參數作爲計數器,每次調用countDown方法計數器減一。做數據處理的線程調用await方法阻塞直到計數器爲0時。

45、Java中interrupted 和 isInterruptedd方法的區別?

interrupted[靜態方法]()Thread.interrupted()來 檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋 出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。

46、懶漢式單例的同步問題

同步的懶加載雖然是線程安全的,但是導致性能開銷。因此產生了雙重檢查鎖定。但雙重檢查鎖定存在隱藏的問題。instance = new Instance()實際上會分爲三步操作:1)分配對象的內存空間;2)初始化對象;3)設置instance指向剛分配的內存地址;由於指令重排序,2和3的順序並不確定。在多線程的情況下,第一個線程執行了1,3,此時第二個線程判斷instance不爲null,但實際上操作2還沒有執行,第二個線程就會獲得一個還未初始化的對象,直接使用就會造成空指針。
解決方案是用volatile修飾instance,在JDK 1.5加強了volatile的語意之後,用volatile修飾instance就阻止了2和3的重排序,進而避免上述情況的發生。
另一種方式則是使用靜態內部類:

public class Singleton {
    private static class InstanceHolder {
        public static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}

其原理是利用類初始化時會加上初始化鎖確保類對象的唯一性。

47、什麼是ThreadLocal

ThreadLocal即線程變量,它爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。從線程的角度看,目標變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。ThreadLocal的實現是以ThreadLocal對象爲鍵。任意對象爲值得存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。

48、什麼是數據競爭

數據競爭的定義:在一個線程寫一個變量,在另一個線程讀同一個變量,而且寫和讀沒有通過同步來排序。

49、Java內存模型(Java Memory Model JMM)

JM屏蔽各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是一個抽象概念,它涵蓋了緩存、寫緩存區、寄存器以及其他的硬件和編譯器優化。
在執行程序時,爲了提高性能,編譯器和處理器常常會對指令做重排序。在多線程中重排序會對程序的執行結果有影響。
JSR-133內存模型採用happens-before的概念來闡述操作之間的內存可見性。happens-before會限制重排序以滿足規則。
主要的happens-before規則有如下:

  • 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。

  • 監視器鎖規則:對一個鎖的解鎖,happens-before與鎖隨後對這個鎖的加鎖。

  • volatile變量規則:對一個volatile域的寫,happens-before與任意後續對這個volatile域的讀。

  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。

50、Java內存區域

  • 程序計數器:當前線程鎖執行的字節碼的行號指示器,用於線程切換恢復,是線程私有的;

  • Java虛擬機棧(棧):虛擬機棧也是線程私有的。每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

  • 本地方法棧:與虛擬機棧類似,服務於Native方法。

  • Java堆:堆是被所有線程共享的一塊內存,用於存放對象實例。是垃圾收集器管理的主要區域,也被稱作GC堆。

  • 方法區:與Java堆一樣,是線程共享的內存區域,用於存儲已被虛擬機加載的類信息、常量、靜態常量、即時編譯器編譯後的代碼等數據。

  • 運行時常量池:是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。

51、判斷對象是否需要回收的方法

  • 引用計數算法。實現簡單,判定效率高,但不能解決循環引用問題,同時計數器的增加和減少帶來額外開銷,JDK1.1以後廢棄了。

  • 可達性分析算法/根搜索算法 。根搜索算法是通過一些“GC Roots”對象作爲起點,從這些節點開始往下搜索,搜索通過的路徑成爲引用鏈(Reference Chain),當一個對象沒有被GC Roots 的引用鏈連接的時候,說明這個對象是不可用的。 Java中可作爲“GC Root”的對象包括:虛擬機棧(本地變量表)中引用的對象;方法區中類靜態屬性和常量引用的對象。本地方法棧中引用的對象。

52、引用類型

  • 強引用:默認的引用方式,不會被垃圾回收,JVM寧願拋出OutOfMemory錯誤也不會回收這種對象。

  • 軟引用(SoftReference):如果一個對象只被軟引用指向,只有內存空間不足夠時,垃圾回收器纔會回收它;

  • 弱引用(WeakReference):如果一個對象只被弱引用指向,當JVM進行垃圾回收時,無論內存是否充足,都會回收該對象。

  • 虛引用(PhantomReference):虛引用和前面的軟引用、弱引用不同,它並不影響對象的生命週期。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。虛引用通常和ReferenceQueue配合使用。
    ReferenceQueue

作爲一個Java對象,Reference對象除了具有保存引用的特殊性之外,也具有Java對象的一般性。所以,當對象被回收之後,雖然這個Reference對象的get()方法返回null,但這個SoftReference對象已經不再具有存在的價值,需要一個適當的清除機制,避免大量Reference對象帶來的內存泄漏。
在java.lang.ref包裏還提供了ReferenceQueue。我們創建Reference對象時使用兩個參數的構造傳入ReferenceQueue,當Reference所引用的對象被垃圾收集器回收的同時,Reference對象被列入ReferenceQueue。也就是說,ReferenceQueue中保存的對象是Reference對象,而且是已經失去了它所軟引用的對象的Reference對象。另外從ReferenceQueue這個名字也可以看出,它是一個隊列,當我們調用它的poll()方法的時候,如果這個隊列中不是空隊列,那麼將返回隊列前面的那個Reference對象。於是我們可以在適當的時候把這些失去所軟引用的對象的SoftReference對象清除掉。

53、垃圾收集算法

  • 標記-清楚算法(Mark-Sweep)
    在標記階段,確定所有要回收的對象,並做標記。清除階段緊隨標記階段,將標記階段確定不可用的對象清除。標記—清除算法是基礎的收集算法,有兩個不足:1)標記和清除階段的效率不高;2)清除後回產生大量的不連續空間,這樣當程序需要分配大內存對象時,可能無法找到足夠的連續空間。

  • 複製算法(Copying)
    複製算法是把內存分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的對象複製到另一塊上,然後把這塊內存整個清理掉。複製算法實現簡單,運行效率高,但是由於每次只能使用其中的一半,造成內存的利用率不高。現在的JVM 用複製方法收集新生代,由於新生代中大部分對象(98%)都是朝生夕死的,所以會分成1塊大內存Eden和兩塊小內存Survivor(大概是8:1:1),每次使用1塊大內存和1塊小內存,當回收時將2塊內存中存活的對象賦值到另一塊小內存中,然後清理剩下的。

  • 標記—整理算法(Mark-Compact)
    標記—整理算法和複製算法一樣,但是標記—整理算法不是把存活對象複製到另一塊內存,而是把存活對象往內存的一端移動,然後直接回收邊界以外的內存。標記—整理算法提高了內存的利用率,並且它適合在收集對象存活時間較長的老年代。

  • 分代收集(Generational Collection)
    分代收集是根據對象的存活時間把內存分爲新生代和老年代,根據各代對象的存活特點,每個代採用不同的垃圾回收算法。新生代採用複製算法,老年代採用標記—整理算法。

54、內存分配策略

  • 對象優先在Eden分配。

  • 大對象直接進入老年代。 大對象是指需要大量連續內存空間的Java對象,最典型的就是那種很長的字符串以及數組。

  • 長期存活的對象進入老年代。存活過一次新生代的GC,Age+1,當達到一定程度(默認15)進入老年代。

  • 動態對象年齡判定。如果在Survivor空間中相同Age所有對象大小的總和大於Survivor空間一半。那麼Age大於等於該Age的對象就可以直接進入老年代。

  • 空間分配擔保。 在發生新生代GC之前,會檢查老年代的剩餘空間是否大於新生代所有對象的總和。如果大於則是安全的,如果不大於有風險。

 

收藏

二、高級面試題目

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

a.建造者模式:

將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。

使用場景比如最常見的AlertDialog,拿我們開發過程中舉例,比如Camera開發過程中,可能需要設置一個初始化的相機配置,設置攝像頭方向,閃光燈開閉,成像質量等等,這種場景下就可以使用建造者模式

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

觀察者模式:
代理模式:
門面模式:
單例模式:
生產者消費者模式:

2、java語言的特點與OOP思想

這個通過對比來描述,比如面向對象和麪向過程的對比,針對這兩種思想的對比,還可以舉個開發中的例子,比如播放器的實現,面向過程的實現方式就是將播放視頻的這個功能分解成多個過程,比如,加載視頻地址,獲取視頻信息,初始化解碼器,選擇合適的解碼器進行解碼,讀取解碼後的幀進行視頻格式轉換和音頻重採樣,然後讀取幀進行播放,這是一個完整的過程,這個過程中不涉及類的概念,而面向對象最大的特點就是類,封裝繼承和多態是核心,同樣的以播放器爲例,一面向對象的方式來實現,將會針對每一個功能封裝出一個對象,吧如說Muxer,獲取視頻信息,Decoder,解碼,格式轉換器,視頻播放器,音頻播放器等,每一個功能對應一個對象,由這個對象來完成對應的功能,並且遵循單一職責原則,一個對象只做它相關的事情

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

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

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

線程池的工作原理:線程池可以減少創建和銷燬線程的次數,從而減少系統資源的消耗,當一個任務提交到線程池時

  1. 首先判斷核心線程池中的線程是否已經滿了,如果沒滿,則創建一個核心線程執行任務,否則進入下一步

  2. 判斷工作隊列是否已滿,沒有滿則加入工作隊列,否則執行下一步

  3. 判斷線程數是否達到了最大值,如果不是,則創建非核心線程執行
    任務,否則執行飽和策略,默認拋出異常

4、說下 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性能有過多的消耗。

5、內存泄漏的場景和解決辦法

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也需要及時移除

6、如何避免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的方法

7、說下 Activity 的啓動模式,生命週期,兩個 Activity 跳轉的生命週期,如果一個 Activity 跳轉另一個 Activity 再按下 Home 鍵在回到 Activity 的生命週期是什麼樣的

啓動模式

Standard 模式:Activity 可以有多個實例,每次啓動 Activity,無論任務棧中是否已經有這個Activity的實例,系統都會創建一個新的Activity實例

SingleTop模式:當一個singleTop模式的Activity已經位於任務棧的棧頂,再去啓動它時,不會再創建新的實例,如果不位於棧頂,就會創建新的實例

SingleTask模式:如果Activity已經位於棧頂,系統不會創建新的Activity實例,和singleTop模式一樣。但Activity已經存在但不位於棧頂時,系統就會把該Activity移到棧頂,並把它上面的activity出棧

SingleInstance模式:singleInstance 模式也是單例的,但和singleTask不同,singleTask 只是任務棧內單例,系統裏是可以有多個singleTask Activity實例的,而 singleInstance Activity 在整個系統裏只有一個實例,啓動一singleInstanceActivity 時,系統會創建一個新的任務棧,並且這個任務棧只有他一個Activity

8、生命週期

onCreate onStart onResume onPause onStop onDestroy
兩個 Activity 跳轉的生命週期

1.啓動A
onCreate - onStart - onResume

2.在A中啓動B
ActivityA onPause
ActivityB onCreate
ActivityB onStart
ActivityB onResume
ActivityA onStop

3.從B中返回A(按物理硬件返回鍵)
ActivityB onPause
ActivityA onRestart
ActivityA onStart
ActivityA onResume
ActivityB onStop
ActivityB onDestroy

4.繼續返回

ActivityA onPause
ActivityA onStop
ActivityA onDestroy

9、onRestart 的調用場景

(1)按下home鍵之後,然後切換回來,會調用onRestart()。
(2)從本Activity跳轉到另一個Activity之後,按back鍵返回原來Activity,會調用onRestart();
(3)從本Activity切換到其他的應用,然後再從其他應用切換回來,會調用onRestart();

說下 Activity 的橫豎屏的切換的生命週期,用那個方法來保存數據,兩者的區別。觸發在什麼時候在那個方法裏可以獲取數據等。

是否瞭解SurfaceView,它是什麼?他的繼承方式是什麼?他與View的區別(從源碼角度,如加載,繪製等)。

SurfaceView中採用了雙緩衝機制,保證了UI界面的流暢性,同時 SurfaceView 不在主線程中繪製,而是另開闢一個線程去繪製,所以它不妨礙UI線程;

SurfaceView 繼承於View,他和View主要有以下三點區別:

(1)View底層沒有雙緩衝機制,SurfaceView有;

(2)view主要適用於主動更新,而SurfaceView適用與被動的更新,如頻繁的刷新

(3)view會在主線程中去更新UI,而SurfaceView則在子線程中刷新;

SurfaceView的內容不在應用窗口上,所以不能使用變換(平移、縮放、旋轉等)。也難以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha()

View:顯示視圖,內置畫布,提供圖形繪製函數、觸屏事件、按鍵事件函數等;必須在UI主線程內更新畫面,速度較慢。

SurfaceView:基於view視圖進行拓展的視圖類,更適合2D遊戲的開發;是view的子類,類似使用雙緩機制,在新的線程中更新畫面所以刷新界面速度比view快,Camera預覽界面使用SurfaceView。

GLSurfaceView:基於SurfaceView視圖再次進行拓展的視圖類,專用於3D遊戲開發的視圖;是SurfaceView的子類,openGL專用。
如何實現進程保活

a: Service 設置成 START_STICKY kill 後會被重啓(等待5秒左右),重傳Intent,保持與重啓前一樣

b: 通過 startForeground將進程設置爲前臺進程, 做前臺服務,優先級和前臺應用一個級別,除非在系統內存非常缺,否則此進程不會被 kill

c: 雙進程Service: 讓2個進程互相保護對方,其中一個Service被清理後,另外沒被清理的進程可以立即重啓進程

d: 用C編寫守護進程(即子進程) : Android系統中當前進程(Process)fork出來的子進程,被系統認爲是兩個不同的進程。當父進程被殺死的時候,子進程仍然可以存活,並不受影響(Android5.0以上的版本不可行)聯繫廠商,加入白名單

e.鎖屏狀態下,開啓一個一像素Activity
說下冷啓動與熱啓動是什麼,區別,如何優化,使用場景等。
app冷啓動: 當應用啓動時,後臺沒有該應用的進程,這時系統會重新創建一個新的進程分配給該應用, 這個啓動方式就叫做冷啓動(後臺不存在該應用進程)。冷啓動因爲系統會重新創建一個新的進程分配給它,所以會先創建和初始化Application類,再創建和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在界面上。

app熱啓動: 當應用已經被打開, 但是被按下返回鍵、Home鍵等按鍵時回到桌面或者是其他程序的時候,再重新打開該app時, 這個方式叫做熱啓動(後臺已經存在該應用進程)。熱啓動因爲會從已有的進程中來啓動,所以熱啓動就不會走Application這步了,而是直接走MainActivity(包括一系列的測量、佈局、繪製),所以熱啓動的過程只需要創建和初始化一個MainActivity就行了,而不必創建和初始化Application

10、冷啓動的流程

當點擊app的啓動圖標時,安卓系統會從Zygote進程中fork創建出一個新的進程分配給該應用,之後會依次創建和初始化Application類、創建MainActivity類、加載主題樣式Theme中的windowBackground等屬性設置給MainActivity以及配置Activity層級上的一些屬性、再inflate佈局、當onCreate/onStart/onResume方法都走完了後最後才進行contentView的measure/layout/draw顯示在界面上

冷啓動的生命週期簡要流程:

Application構造方法 –> attachBaseContext()–>onCreate –>Activity構造方法 –> onCreate() –> 配置主體中的背景等操作 –>onStart() –> onResume() –> 測量、佈局、繪製顯示

冷啓動的優化主要是視覺上的優化,解決白屏問題,提高用戶體驗,所以通過上面冷啓動的過程。能做的優化如下:

  • 減少 onCreate()方法的工作量

  • 不要讓 Application 參與業務的操作

  • 不要在 Application 進行耗時操作

  • 不要以靜態變量的方式在 Application 保存數據

  • 減少佈局的複雜度和層級

  • 減少主線程耗時

爲什麼冷啓動會有白屏黑屏問題?原因在於加載主題樣式Theme中的windowBackground等屬性設置給MainActivity發生在inflate佈局當onCreate/onStart/onResume方法之前,而windowBackground背景被設置成了白色或者黑色,所以我們進入app的第一個界面的時候會造成先白屏或黑屏一下再進入界面。解決思路如下

1.給他設置 windowBackground 背景跟啓動頁的背景相同,如果你的啓動頁是張圖片那麼可以直接給 windowBackground 這個屬性設置該圖片那麼就不會有一閃的效果了

<style name=``"Splash_Theme"` `parent=``"@android:style/Theme.NoTitleBar"``>`
    <item name=``"android:windowBackground"``>@drawable/splash_bg</item>`
    <item name=``"android:windowNoTitle"``>``true``</item>`
</style>`

2.採用世面的處理方法,設置背景是透明的,給人一種延遲啓動的感覺。,將背景顏色設置爲透明色,這樣當用戶點擊桌面APP圖片的時候,並不會"立即"進入APP,而且在桌面上停留一會,其實這時候APP已經是啓動的了,只是我們心機的把Theme裏的windowBackground 的顏色設置成透明的,強行把鍋甩給了手機應用廠商(手機反應太慢了啦)

<style name=``"Splash_Theme"` `parent=``"@android:style/Theme.NoTitleBar"``>`
    <item name=``"android:windowIsTranslucent"``>``true``</item>`
    <item name=``"android:windowNoTitle"``>``true``</item>`
</style>`

3.以上兩種方法是在視覺上顯得更快,但其實只是一種表象,讓應用啓動的更快,有一種思路,將 Application 中的不必要的初始化動作實現懶加載,比如,在SpashActivity 顯示後再發送消息到 Application,去初始化,這樣可以將初始化的動作放在後邊,縮短應用啓動到用戶看到界面的時間

11、Android 中的線程有那些,原理與各自特點

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按順序一個個的取出並執行,執行完成後自動結束自己,不需要開發者手動關閉

12、ANR的原因

1.耗時的網絡訪問
2.大量的數據讀寫
3.數據庫操作
4.硬件操作(比如camera)
5.調用thread的join()方法、sleep()方法、wait()方法或者等待線程鎖的時候
6.service binder的數量達到上限
7.system server中發生WatchDog ANR
8.service忙導致超時無響應
9.其他線程持有鎖,導致主線程等待超時
10.其它線程終止或崩潰導致主線程一直等待

13、三級緩存原理

當 Android 端需要獲得數據時比如獲取網絡中的圖片,首先從內存中查找(按鍵查找),內存中沒有的再從磁盤文件或sqlite中去查找,若磁盤中也沒有才通過網絡獲取

LruCache 底層實現原理:

LruCache 中 Lru 算法的實現就是通過 LinkedHashMap 來實現的。LinkedHashMap 繼承於 HashMap,它使用了一個雙向鏈表來存儲 Map中的Entry順序關係

對於get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,還做些調整Entry順序鏈表的工作。

LruCache中將LinkedHashMap的順序設置爲LRU順序來實現LRU緩存,每次調用get(也就是從內存緩存中取圖片),則將該對象移到鏈表的尾端。
調用put插入新的對象也是存儲在鏈表尾端,這樣當內存緩存達到設定的最大值時,將鏈表頭部的對象(近期最少用到的)移除。

14、說下你對 Collection 這個類的理解。

Collection是集合框架的頂層接口,是存儲對象的容器,Colloction定義了接口的公用方法如add remove clear等等,它的子接口有兩個,List和Set,List的特點有元素有序,元素可以重複,元素都有索引(角標),典型的有

Vector:內部是數組數據結構,是同步的(線程安全的)。增刪查詢都很慢。

ArrayList:內部是數組數據結構,是不同步的(線程不安全的)。替代了Vector。查詢速度快,增刪比較慢。

LinkedList:內部是鏈表數據結構,是不同步的(線程不安全的)。增刪元素速度快。

而Set的是特點元素無序,元素不可以重複

HashSet:內部數據結構是哈希表,是不同步的。

Set集合中元素都必須是唯一的,HashSet作爲其子類也需保證元素的唯一性。

判斷元素唯一性的方式:

通過存儲對象(元素)的hashCode和equals方法來完成對象唯一性的。

如果對象的hashCode值不同,那麼不用調用equals方法就會將對象直接存儲到集合中;

如果對象的hashCode值相同,那麼需調用equals方法判斷返回值是否爲true,
若爲false, 則視爲不同元素,就會直接存儲;
若爲true, 則視爲相同元素,不會存儲。

如果要使用HashSet集合存儲元素,該元素的類必須覆蓋hashCode方法和equals方法。一般情況下,如果定義的類會產生很多對象,通常都需要覆蓋equals,hashCode方法。建立對象判斷是否相同的依據。
TreeSet:保證元素唯一性的同時可以對內部元素進行排序,是不同步的。

判斷元素唯一性的方式:

根據比較方法的返回結果是否爲0,如果爲0視爲相同元素,不存;如果非0視爲不同元素,則存。

TreeSet對元素的排序有兩種方式:

方式一:使元素(對象)對應的類實現Comparable接口,覆蓋compareTo方法。這樣元素自身具有比較功能。

方式二:使TreeSet集合自身具有比較功能,定義一個比較器Comparator,將該類對象作爲參數傳遞給TreeSet集合的構造函數

                                                                                                                                                                                        -END

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