精選 Android 初中級面試題:深探Handler,多線程,Bitmap


精選 Android 初中級面試題:深探Handler,多線程,Bitmap

Handler

1、談談消息機制Handler作用 ?有哪些要素 ?流程是怎樣的 ?

參考回答:負責跨線程通信,這是因為在主線程不能做耗時操作,而子線程不能更新UI,所以當子線程中進行耗時操作後需要更新UI時,通過Handler將有關UI的操作切換到主線程中執行。

具體分為四大要素:

  • Message(消息):需要被傳遞的消息,消息分為硬件產生的消息(如按鈕、觸摸)和軟件生成的消息。
  • MessageQueue(消息隊列):負責消息的存儲與管理,負責管理由 Handler發送過來的Message。讀取會自動刪除消息,單鏈表維護,插入和刪除上有優勢。在其next()方法中會無限循環,不斷判斷是否有消息,有就返回這條消息並移除。
  • Handler(消息處理器):負責Message的發送及處理。主要向消息池發送各種消息事件(Handler.sendMessage())和處理相應消息事件(Handler.handleMessage()),按照先進先出執行,內部使用的是單鏈表的結構。
  • Looper(消息池):負責關聯線程以及消息的分發,在該線程下從 MessageQueue獲取 Message,分發給Handler,Looper創建的時候會創建一個 MessageQueue,調用loop()方法的時候消息循環開始,其中會不斷調用messageQueue的next()方法,當有消息就處理,否則阻塞在messageQueue的next()方法中。當Looper的quit()被調用的時候會調用messageQueue的quit(),此時next()會返回null,然後loop()方法也就跟著退出。

具體流程如下:

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

在主線程創建的時候會創建一個Looper,同時也會在在Looper內部創建一個消息隊列。而在創鍵Handler的時候取出當前線程的Looper,並通過該Looper對象獲得消息隊列,然後Handler在子線程中通過MessageQueue.enqueueMessage在消息隊列中添加一條Message。

通過Looper.loop() 開啟消息循環不斷輪詢調用 MessageQueue.next(),取得對應的Message並且通過Handler.dispatchMessage傳遞給Handler,最終調用Handler.handlerMessage處理消息。

2、一個線程能否創建多個Handler,Handler跟Looper之間的對應關係 ?

參考回答:一個Thread只能有一個Looper,一個MessageQueen,可以有多個Handler。

以一個線程為基準,他們的數量級關係是:Thread(1) : Looper(1) : MessageQueue(1) : Handler(N)。

3、軟引用跟弱引用的區別

參考回答:

  • 軟引用(SoftReference):如果一個對象只具有軟引用,則內存空間充足時,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以一直被程序使用。
  • 弱引用(WeakReference):如果一個對象只具有弱引用,那麼在垃圾回收器線程掃描的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。
  • 兩者之間根本區別在於:只具有弱引用的對象擁有更短暫的生命週期,可能隨時被回收。而只具有軟引用的對象只有當內存不夠的時候才被回收,在內存足夠的時候,通常不被回收。

推薦文章:Java中的四種引用類型:強引用、軟引用、弱引用和虛引用(https://segmentfault.com/a/1190000015282652)

4、Handler 引起的內存洩露原因以及最佳解決方案

洩露原因:Handler 允許我們發送延時消息,如果在延時期間用戶關閉了 Activity,那麼該 Activity 會洩露。這個洩露是因為 Message 會持有 Handler,而又因為 Java 的特性,內部類會持有外部類,使得 Activity 會被 Handler 持有,這樣最終就導致 Activity 洩露。

解決方案:將 Handler 定義成靜態的內部類,在內部持有Activity的弱引用,並在Acitivity的onDestroy()中調用handler.removeCallbacksAndMessages(null)及時移除所有消息。

5、為什麼系統不建議在子線程訪問UI?

參考回答:Android的UI控件不是線程安全的,如果在多線程中併發訪問可能會導致UI控件處於不可預期的狀態。

這時你可能會問為何系統不對UI控件的訪問加上鎖機制呢?因為:

  • 加鎖機制會讓UI訪問邏輯變的複雜
  • 加鎖機制會降低UI的訪問效率,因為加鎖會阻塞某些線程的執行

6、Looper死循環為什麼不會導致應用卡死?

參考回答:

  • 主線程的主要方法就是消息循環,一旦退出消息循環,那麼你的應用也就退出了,Looer.loop()方法可能會引起主線程的阻塞,但只要它的消息循環沒有被阻塞,能一直處理事件就不會產生ANR異常。
  • 造成ANR的不是主線程阻塞,而是主線程的Looper消息處理過程發生了任務阻塞,無法響應手勢操作,不能及時刷新UI。
  • 阻塞與程序無響應沒有必然關係,雖然主線程在沒有消息可處理的時候是阻塞的,但是隻要保證有消息的時候能夠立刻處理,程序是不會無響應的。

7、使用Handler的postDealy後消息隊列會有什麼變化?

參考回答:如果隊列中只有這個消息,那麼消息不會被髮送,而是計算到時喚醒的時間,先將Looper阻塞,到時間就喚醒它。但如果此時要加入新消息,該消息隊列的對頭跟delay時間相比更長,則插入到頭部,按照觸發時間進行排序,隊頭的時間最小、隊尾的時間最大。

8、可以在子線程直接new一個Handler嗎?怎麼做?

參考回答:不可以,因為在主線程中,Activity內部包含一個Looper對象,它會自動管理Looper,處理子線程中發送過來的消息。而對於子線程而言,沒有任何對象幫助我們維護Looper對象,所以需要我們自己手動維護。所以要在子線程開啟Handler要先創建Looper,並開啟Looper循環

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

推薦文章:Android異步消息處理機制完全解析,帶你從源碼的角度徹底理解(https://blog.csdn.net/guolin_blog/article/details/9991569)

9、Message可以如何創建?哪種效果更好,為什麼?

參考回答:可以通過三種方法創建:

  • 直接生成實例Message m = new Message
  • 通過Message m = Message.obtain
  • 通過Message m = mHandler.obtainMessage()

後兩者效果更好,因為Android默認的消息池中消息數量是10,而後兩者是直接在消息池中取出一個Message實例,這樣做就可以避免多生成Message實例。

線程

1、線程池的好處?四種線程池的使用場景,線程池的幾個參數的理解?

使用線程池的好處是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或則“過度切換”的問題,歸納總結就是:

  • 重用存在的線程,減少對象創建、消亡的開銷,性能佳。
  • 可有效控制最大併發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
  • 提供定時執行、定期執行、單線程、併發數控制等功能。

Android中的線程池都是直接或間接通過配置ThreadPoolExecutor來實現不同特性的線程池.Android中最常見的類具有不同特性的線程池分別為:

  • newCachedThreadPool:只有非核心線程,最大線程數非常大,所有線程都活動時會為新任務創建新線程,否則會利用空閒線程 ( 60s空閒時間,過了就會被回收,所以線程池中有0個線程的可能 )來處理任務.

優點:任何任務都會被立即執行(任務隊列SynchronousQuue相當於一個空集合);比較適合執行大量的耗時較少的任務.

  • newFixedThreadPool:只有核心線程,並且數量固定的,所有線程都活動時,因為隊列沒有限制大小,新任務會等待執行,當線程池空閒時不會釋放工作線程,還會佔用一定的系統資源。

優點:更快的響應外界請求

  • newScheduledThreadPool:核心線程數固定,非核心線程(閒著沒活幹會被立即回收數)沒有限制.

優點:執行定時任務以及有固定週期的重複任務

  • newSingleThreadExecutor:只有一個核心線程,確保所有的任務都在同一線程中按序完成

優點:不需要處理線程同步的問題

通過源碼可以瞭解到上面的四種線程池實際上還是利用 ThreadPoolExecutor 類實現的

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

推薦文章:java線程池解析和四種線程池的使用(https://blog.csdn.net/ztchun/article/details/57413255)

2、Android中還了解哪些方便線程切換的類?

參考回答:

  • AsyncTask:底層封裝了線程池和Handler,便於執行後臺任務以及在子線程中進行UI操作。
  • HandlerThread:一種具有消息循環的線程,其內部可使用Handler。
  • IntentService:是一種異步、會自動停止的服務,內部採用HandlerThread。

3、講講AsyncTask的原理:

AsyncTask中有兩個線程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一個Handler(InternalHandler),其中線程池SerialExecutor用於任務的排隊,而線程池THREAD_POOL_EXECUTOR用於真正地執行任務,InternalHandler用於將執行環境從線程池切換到主線程。

sHandler是一個靜態的Handler對象,為了能夠將執行環境切換到主線程,這就要求sHandler這個對象必須在主線程創建。由於靜態成員會在加載類的時候進行初始化,因此這就變相要求AsyncTask的類必須在主線程中加載,否則同一個進程中的AsyncTask都將無法正常工作。

4、IntentService有什麼用 ?

IntentService可用於執行後臺耗時的任務,當任務執行完成後會自動停止,同時由於IntentService是服務的原因,不同於普通Service,IntentService可自動創建子線程來執行任務,這導致它的優先級比單純的線程要高,不容易被系統殺死,所以IntentService比較適合執行一些高優先級的後臺任務。

5、直接在Activity中創建一個thread跟在service中創建一個thread之間的區別?

在Activity中被創建:該Thread的就是為這個Activity服務的,完成這個特定的Activity交代的任務,主動通知該Activity一些消息和事件,Activity銷燬後,該Thread也沒有存活的意義了。

在Service中被創建:這是保證最長生命週期的Thread的唯一方式,只要整個Service不退出,Thread就可以一直在後臺執行,一般在Service的onCreate()中創建,在onDestroy()中銷燬。所以,在Service中創建的Thread,適合長期執行一些獨立於APP的後臺任務,比較常見的就是:在Service中保持與服務器端的長連接。

6、ThreadPoolExecutor的工作策略 ?

ThreadPoolExecutor執行任務時會遵循如下規則:

  • 如果線程池中的線程數量未達到核心線程的數量,那麼會直接啟動一個核心線程來執行任務。
  • 如果線程池中的線程數量已經達到或則超過核心線程的數量,那麼任務會被插入任務隊列中排隊等待執行。
  • 如果在第2點無法將任務插入到任務隊列中,這往往是由於任務隊列已滿,這個時候如果在線程數量未達到線程池規定的最大值,那麼會立刻啟動一個非核心線程來執行任務。
  • 如果第3點中線程數量已經達到線程池規定的最大值,那麼就拒絕執行此任務,ThreadPoolExecutor會調用RejectedExecutionHandler的rejectedExecution方法來通知調用者。

7、Handler、Thread和HandlerThread的差別?

參考回答:

Handler:在android中負責發送和處理消息,通過它可以實現其他支線線程與主線程之間的消息通訊。

Thread:Java進程中執行運算的最小單位,亦即執行處理機調度的基本單位。某一進程中一路單獨運行的程序。

HandlerThread:一個繼承自Thread的類HandlerThread,Android中沒有對Java中的Thread進行任何封裝,而是提供了一個繼承自Thread的類HandlerThread類,這個類對Java的Thread做了很多便利的封裝。HandlerThread繼承於Thread,所以它本質就是個Thread。與普通Thread的差別就在於,它在內部直接實現了Looper的實現,這是Handler消息機制必不可少的。有了自己的looper,可以讓我們在自己的線程中分發和處理消息。如果不用HandlerThread的話,需要手動去調用Looper.prepare()和Looper.loop()這些方法。

8、ThreadLocal的原理:

ThreadLocal是一個關於創建線程局部變量的類。使用場景如下所示:

  • 實現單個線程單例以及單個線程上下文信息存儲,比如交易id等。
  • 實現線程安全,非線程安全的對象使用ThreadLocal之後就會變得線程安全,因為每個線程都會有一個對應的實例。承載一些線程相關的數據,避免在方法中來回傳遞參數。

當需要使用多線程時,有個變量恰巧不需要共享,此時就不必使用synchronized這麼麻煩的關鍵字來鎖住,每個線程都相當於在堆內存中開闢一個空間,線程中帶有對共享變量的緩衝區,通過緩衝區將堆內存中的共享變量進行讀取和操作,ThreadLocal相當於線程內的內存,一個局部變量。每次可以對線程自身的數據讀取和操作,並不需要通過緩衝區與 主內存中的變量進行交互。並不會像synchronized那樣修改主內存的數據,再將主內存的數據複製到線程內的工作內存。ThreadLocal可以讓線程獨佔資源,存儲於線程內部,避免線程堵塞造成CPU吞吐下降。

在每個Thread中包含一個ThreadLocalMap,ThreadLocalMap的key是ThreadLocal的對象,value是獨享數據。

9、多線程是否一定會高效(優缺點)

多線程的優點:

  • 方便高效的內存共享 - 多進程下內存共享比較不便,且會抵消掉多進程編程的好處
  • 較輕的上下文切換開銷 - 不用切換地址空間,不用更改CR3寄存器,不用清空TLB
  • 線程上的任務執行完後自動銷燬

多線程的缺點:

  • 開啟線程需要佔用一定的內存空間(默認情況下,每一個線程都佔512KB)
  • 如果開啟大量的線程,會佔用大量的內存空間,降低程序的性能
  • 線程越多,cpu在調用線程上的開銷就越大
  • 程序設計更加複雜,比如線程間的通信、多線程的數據共享

綜上得出,多線程不一定能提高效率,在內存空間緊張的情況下反而是一種負擔,因此在日常開發中,應儘量:

  • 不要頻繁創建,銷燬線程,使用線程池
  • 減少線程間同步和通信(最為關鍵)
  • 避免需要頻繁共享寫的數據
  • 合理安排共享數據結構,避免偽共享(false sharing)
  • 使用非阻塞數據結構/算法
  • 避免可能產生可伸縮性問題的系統調用(比如mmap)
  • 避免產生大量缺頁異常,儘量使用Huge Page
  • 可以的話使用用戶態輕量級線程代替內核線程

10、多線程中,讓你做一個單例,你會怎麼做?

多線程中建立單例模式考慮的因素有很多,比如線程安全 -延遲加載-代碼安全:如防止序列化攻擊,防止反射攻擊(防止反射進行私有方法調用) -性能因素

實現方法有多種,餓漢,懶漢(線程安全,線程非安全),雙重檢查(DCL),內部類,以及枚舉

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

推薦文章:單例模式的總結(https://xxxblank.github.io/2017/09/14/singleTon/)

11、除了notify還有什麼方式可以喚醒線程?

參考回答:當一個擁有Object鎖的線程調用 wait()方法時,就會使當前線程加入object.wait 等待隊列中,並且釋放當前佔用的Object鎖,這樣其他線程就有機會獲取這個Object鎖,獲得Object鎖的線程調用notify()方法,就能在Object.wait 等待隊列中隨機喚醒一個線程(該喚醒是隨機的與加入的順序無關,優先級高的被喚醒概率會高)

如果調用notifyAll()方法就喚醒全部的線程。注意:調用notify()方法後並不會立即釋放object鎖,會等待該線程執行完畢後釋放Object鎖。

12、什麼是ANR ? 什麼情況會出現ANR ?如何避免 ?在不看代碼的情況下如何快速定位出現ANR問題所在 ?

  • ANR(Application Not Responding,應用無響應):當操作在一段時間內系統無法處理時,會在系統層面會彈出ANR對話框
  • 產生ANR可能是因為5s內無響應用戶輸入事件、10s內未結束BroadcastReceiver、20s內未結束Service
  • 想要避免ANR就不要在主線程做耗時操作,而是通過開子線程,方法比如繼承Thread或實現Runnable接口、使用AsyncTask、IntentService、HandlerThread等

推薦文章:如何快速分析定位ANR(https://www.jianshu.com/p/cfa9ed42e379)

Bitmap

1、Bitmap使用需要注意哪些問題 ?

選擇合適的圖片規格(bitmap類型):通常我們優化Bitmap時,當需要做性能優化或者防止OOM,我們通常會使用RGB_565,因為ALPHA_8只有透明度,顯示一般圖片沒有意義,Bitmap.Config.ARGB_4444顯示圖片不清楚,Bitmap.Config.ARGB_8888佔用內存最多。:

  • ALPHA_8 每個像素佔用1byte內存
  • ARGB_4444 每個像素佔用2byte內存
  • ARGB_8888 每個像素佔用4byte內存(默認)
  • RGB_565 每個像素佔用2byte內存

降低採樣率:BitmapFactory.Options 參數inSampleSize的使用,先把options.inJustDecodeBounds設為true,只是去讀取圖片的大小,在拿到圖片的大小之後和要顯示的大小做比較通過calculateInSampleSize()函數計算inSampleSize的具體值,得到值之後。options.inJustDecodeBounds設為false讀圖片資源。

複用內存:即通過軟引用(內存不夠的時候才會回收掉),複用內存塊,不需要再重新給這個bitmap申請一塊新的內存,避免了一次內存的分配和回收,從而改善了運行效率。

使用recycle()方法及時回收內存。

壓縮圖片

2、Bitmap.recycle()會立即回收麼?什麼時候會回收?如果沒有地方使用這個Bitmap,為什麼垃圾回收不會直接回收?

  • 通過源碼可以瞭解到,加載Bitmap到內存裡以後,是包含兩部分內存區域的。簡單的說,一部分是Java部分的,一部分是C部分的。這個Bitmap對象是由Java部分分配的,不用的時候系統就會自動回收了
  • 但是那個對應的C可用的內存區域,虛擬機是不能直接回收的,這個只能調用底層的功能釋放。所以需要調用recycle()方法來釋放C部分的內存
  • bitmap.recycle()方法用於回收該Bitmap所佔用的內存,接著將bitmap置空,最後使用System.gc()調用一下系統的垃圾回收器進行回收,調用System.gc()並不能保證立即開始進行回收過程,而只是為了加快回收的到來。

3、一張Bitmap所佔內存以及內存佔用的計算?

參考回答:Bitamp 所佔內存大小 = 寬度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一個像素所佔的內存字節大小

注:這裡inDensity表示目標圖片的dpi(放在哪個資源文件夾下),inTargetDensity表示目標屏幕的dpi,所以你可以發現inDensity和inTargetDensity會對Bitmap的寬高進行拉伸,進而改變Bitmap佔用內存的大小。

在Bitmap裡有兩個獲取內存佔用大小的方法。

  • getByteCount():API12 加入,代表存儲 Bitmap 的像素需要的最少內存。
  • getAllocationByteCount():API19 加入,代表在內存中為 Bitmap 分配的內存大小,代替了 getByteCount() 方法。
  • 在不復用 Bitmap 時,getByteCount() 和 getAllocationByteCount 返回的結果是一樣的。在通過複用 Bitmap 來解碼圖片時,那麼 getByteCount() 表示新解碼圖片佔用內存的大 小,getAllocationByteCount() 表示被複用 Bitmap 真實佔用的內存大小

4、Android中緩存更新策略 ?

Android的緩存更新策略沒有統一的標準,一般來說,緩存策略主要包含緩存的添加、獲取和刪除這三類操作,但不管是內存緩存還是存儲設備緩存,它們的緩存容量是有限制的,因此刪除一些舊緩存並添加新緩存,如何定義緩存的新舊這就是一種策略,不同的策略就對應著不同的緩存算法

比如可以簡單地根據文件的最後修改時間來定義緩存的新舊,當緩存滿時就將最後修改時間較早的緩存移除,這就是一種緩存算法,但不算很完美

5、LRU的原理 ?

參考回答:為減少流量消耗,可採用緩存策略。常用的緩存算法是LRU(Least Recently Used):當緩存滿時, 會優先淘汰那些近期最少使用的緩存對象。主要是兩種方式:

  • LruCache(內存緩存):LruCache類是一個線程安全的泛型類:內部採用一個LinkedHashMap以強引用的方式存儲外界的緩存對象,並提供get和put方法來完成緩存的獲取和添加操作,當緩存滿時會移除較早使用的緩存對象,再添加新的緩存對象。
  • DiskLruCache(磁盤緩存):通過將緩存對象寫入文件系統從而實現緩存效果

最後

今天關於面試的分享就到這裡,還是那句話,有些東西你不僅要懂,而且要能夠很好地表達出來,能夠讓面試官認可你的理解,例如Handler機制,這個是面試必問之題。有些晦澀的點,或許它只活在面試當中,實際工作當中你壓根不會用到它,但是你要知道它是什麼東西。

最後在這裡小編分享一份自己收錄整理上述技術體系圖相關的幾十套騰訊、頭條、阿里、美團等公司19年的面試題,把技術點整理成了視頻和PDF(實際上比預期多花了不少精力),包含知識脈絡 + 諸多細節,由於篇幅有限,這裡以圖片的形式給大家展示一部分。

還有 高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料 幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習。

【Android核心高級技術PDF文檔,BAT大廠面試真題解析】

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

【算法合集】

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

【延伸Android必備知識點】

精選 Android 初中級面試題:深探Handler,多線程,Bitmap

【Android部分高級架構視頻學習資源】

Android精講視頻領取學習後更加是如虎添翼!進軍BATJ大廠等(備戰)!現在都說互聯網寒冬,其實無非就是你上錯了車,且穿的少(技能),要是你上對車,自身技術能力夠強,公司換掉的代價大,怎麼可能會被裁掉,都是淘汰末端的業務Curd而已!現如今市場上初級程序員氾濫,這套教程針對Android開發工程師1-6年的人員、正處於瓶頸期,想要年後突破自己漲薪的,進階Android中高級、架構師對你更是如魚得水,趕快領取吧!

【Android進階學習視頻】、【全套Android面試秘籍PDF】、【Android開發核心知識點筆記】可以 私信我【安卓】免費獲取!


分享到:


相關文章: