本文出自 Eddy Wiki ,轉載請注明出處:http://eddy.wiki/interview-android.html
本文收集整理了 Android 面試中會遇到與 Android 知識相關的簡述題。
內存相關
什么情況會導致內存泄漏
內存泄漏,簡單點說就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻不再被使用導致 GC 不能回收。
內存泄漏是指無用對象(不再使用的對象)持續占有內存或無用對象的內存得不到及時釋放,從而造成內存空間的浪費稱為內存泄漏。
- 資源對象沒關閉造成的內存泄漏。對于使用了BraodcastReceiver,ContentObserver,File,游標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷毀時及時關閉或者注銷,否則這些資源將不會被回收,造成內存泄漏。
- 構造Adapter時,沒有使用緩存的convertView。
- Bitmap對象不在使用時沒有調用recycle()釋放內存。
- 長生命周期持有短生命周期對象的引用造成的內存泄漏。試著使用關于application的context來替代和activity相關的context。保持對對象生命周期的敏感,特別注意單例、靜態對象、全局性集合等的生命周期。
- 注冊沒取消造成的內存泄漏。
- 集合中對象沒清理造成的內存泄漏。集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變量 (比如類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那么沒有相應的刪除機制,很可能導致集合所占用的內存只增不減。
- 單例造成的內存泄漏。由于單例的靜態特性使得其生命周期跟應用的生命周期一樣長,所以如果使用不恰當的話,很容易造成內存泄漏。
- 匿名內部類和非靜態內部類持有外部類的引用造成的內存泄漏。
- Handler 造成的內存泄漏。
參考:
什么情況會導致 OOM
- 加載對象過大,例如:圖片、文件等。
- 相應資源過多,沒有來不及釋放。
- 內存泄漏。
如何避免 OOM:
- 減少對象的內存占用
- 使用更加輕量的數據結構。例如,我們可以考慮使用 ArrayMap 或 SparseArray 而不是 HashMap 等傳統數據結構。
- 避免在 Android 里面使用 Enum。
- 減小 Bitmap 對象的內存占用。縮放比例,解碼格式。
- 使用更小的圖片。
- 內存對象的重復利用
- 復用系統自帶的資源。但需要留意不同系統版本的差異性。
- 在 ListView 和 GridView 等大量出現重復子組件的視圖里對 ConvertView 復用。
- Bitmap 對象的復用。在 ListView 和 GridView 等顯示大量圖片的控件里,需要使用 LRU 的機制來緩存處理好的 Bitmap。
- 避免在 onDraw 方法里面執行對象的創建。
- 使用大量字符串拼接操作是,考慮使用 StringBuffer 代替 “+”。
- 避免對象的內存泄漏
- 注意 Activity 的泄漏。1)內部類引用導致 Activity 的泄漏。典型的是 Handler 導致的泄漏。為了解決這個問題,可以在UI退出之前,執行remove Handler消息隊列中的消息與runnable對象。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關系的目的。2)Activity Context被傳遞到其他實例中,這可能導致自身被引用而發生泄漏。
- 考慮使用 Application Context 而不是 Activity Context。對于大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),我們都可以考慮使用Application Context而不是Activity的Context,這樣可以避免不經意的Activity泄露。
- 注意臨時Bitmap對象的及時回收。
- 注意監聽器的注銷。
- 注意緩存容器中的對象泄漏。
- 注意 WebView 的泄漏。
- 注意 Cursor 對象是否及時關閉。
參考:
Android 為每個應用程序分配的內存大小是多少
Android 程序內存一般限制在16M,也有的是24M
查看每個應用程序最高可用內存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
//可用堆內存,單個應用可以使用的最大內存,如果應用內存使用超過這個值,就報OOM
int heapgrowthlimit = manager.getMemoryClass();
//進程內存空間分配的最大值,表示的是單個虛擬機可用的最大內存
int heapsize = manager.getLargeMemoryClass();
L.d("heapgrowthlimit = "+heapgrowthlimit+"m"+", heapsize = "+heapsize+"");
//應用程序最大可用內存
int maxMemory = ((int) Runtime.getRuntime().maxMemory())/1024/1024;
//應用程序已獲得內存
long totalMemory = ((int) Runtime.getRuntime().totalMemory())/1024/1024;
//應用程序已獲得內存中未使用內存
long freeMemory = ((int) Runtime.getRuntime().freeMemory())/1024/1024;
System.out.println("---> maxMemory="+maxMemory+"M,totalMemory="+totalMemory+"M,freeMemory="+freeMemory+"M");
Android中弱引用與軟引用的應用場景。
預防內存泄漏!擅用WeakReference!
所有從類外部傳來的對象(特別對于Context,View,Fragmet,Activity對象),如果要將其放進類內部的容器對象或者靜態類中引用,請一直用WeakReference包裝!比如在TabLayout的源碼中,容器對象或者靜態類中引用,用WeakReference包裝。在TabLayoutOnPageChangeListener中,就為TabLayout做了WeakReference wrap。
ANR
ANR 定位和修正
如果開發機器上出現問題,我們可以通過查看/data/anr/traces.txt即可,最新的ANR信息在最開始部分。
出現 ANR 原因:
應用在5秒內未響應用戶的輸入事件(如按鍵或者觸摸)。
BroadcastReceiver在10秒內未完成相關的處理。
Service在 20秒內無法處理完成。
主線程被IO操作(從4.0之后網絡IO不允許在主線程中)阻塞。
主線程中存在耗時的計算。
主線程中錯誤的操作,比如Thread.wait或者Thread.sleep等。
修正方法:
使用AsyncTask處理耗時IO操作。
使用Thread或者HandlerThread時,調用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)設置優先級,否則仍然會降低程序響應,因為默認Thread的優先級和主線程相同。
使用Handler處理工作線程結果,而不是使用Thread.wait()或者Thread.sleep()來阻塞主線程。
Activity的onCreate和onResume回調中盡量避免耗時的代碼。
BroadcastReceiver中onReceive代碼也要盡量減少耗時,建議使用IntentService處理
如何避免:
- UI線程盡量只做跟UI相關的工作
- 耗時的操作(比如數據庫操作,I/O,連接網絡或者別的有可能阻塞UI線程的操作)把它放在單獨的線程處理
- 盡量用Handler來處理UIThread和別的Thread之間的交互
- 使用 AsyncTask 時:在doInBackground()方法中執行耗時操作;在onPostExecuted()更新UI 。
- 使用Handler實現異步任務時:在子線程中處理耗時操作,處理完成之后,通過handler.sendMessage()傳遞處理結果;在handler的handleMessage()方法中更新 UI 或者使用handler.post()方法將消息放到Looper中。
什么是ANR,如何避免ANR。
什么是FC?如何避免FC的發生,另外FC發生時如何捕獲相應的uncaught exception?
性能優化
怎么對 Android APP 進行性能優化
合理管理內存
- 節制的使用Service
- 當界面不可見時釋放內存
- 當內存緊張時釋放內存
- 避免在Bitmap上浪費內存
- 使用優化過的數據集合
- 知曉內存的開支情況
- 謹慎使用抽象編程
- 盡量避免使用依賴注入框架
- 使用多個進程
- 避免內存泄漏
高性能編碼優化
- 避免創建不必要的對象
- 靜態優于抽象
- 對常量使用static final修飾符
- 使用增強型for循環語法
- 多使用系統封裝好的API
- 避免在內部調用Getters/Setters方法
布局優化技巧
- 重用布局文件
- 僅在需要時才加載布局
參考:
Android APP 內存分析工具有哪些
Square 開源庫 LeakCanary
參考:
利用 LeakCanary 來檢查 Android 內存泄漏
屏幕適配經驗
參考:
數據存儲
數據存儲,數據持久化的方式有哪些
- 網絡
- SharedPreference: 除SQLite數據庫外,另一種常用的數據存儲方式,其本質就是一個xml文件,常用于存儲較簡單的參數設置。
- SQLite:SQLite是一個輕量級的數據庫,支持基本的SQL語法,是常被采用的一種數據存儲方式。Android為此數據庫提供了一個名為SQLiteDatabase的類,封裝了一些操作數據庫的api
- File: 即常說的文件(I/O)存儲方法,常用語存儲大數量的數據,但是缺點是更新數據將是一件困難的事情。
- ContentProvider: Android系統中能實現所有應用程序共享的一種數據存儲方式,由于數據通常在各應用間的是互相私密的,所以此存儲方式較少使用,但是其又是必不可少的一種存儲方式。例如音頻,視頻,圖片和通訊錄,一般都可以采用此種方式進行存儲。每個Content Provider都會對外提供一個公共的URI(包裝成Uri對象),如果應用程序有數據需要共享時,就需要使用Content Provider為這些數據定義一個URI,然后其他的應用程序就通過Content Provider傳入這個URI來對數據進行操作。
文件和數據庫哪個效率高
- 數據量非常少,可以使用文件。
- 如果涉及到查詢等操作,并且數據量大,則應該使用數據庫。
SharedPreference 實現
如何導入外部數據庫
把數據庫存放到 res/raw 目錄下,然后在第一次安裝啟動應用的時間把該數據庫拷貝到應用的內部存儲空間即 android 系統下的 /data/data/packagename/ 目錄下。
談談 SQLite
SQLite 數據庫升級更新如何保留原來數據
在使用數據庫之前,基本上會自定義一個類繼承自SQLiteOpenHelper。該類的其中一個構造函數形式是這樣的(另一個多出來一個DatabaseErrorHandler):
public SQLiteOpenHelper(Context context, String name,
CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
這個構造函數里面的version參數即是我們設定的版本號。第一次使用數據庫時傳遞的這個版本將被系統記錄,并調用SQLiteOpenHelper#onCreate()方法進行建表操作。后續傳入的版本如果比這個高,則會調用SQLiteOpenHelper#onUpgrade()方法進行升級。
跨越版本的升級
處理好了單個版本的升級,還有一個更加棘手的問題:如果應用程序發布了多個版本,以致出現了三個以上數據庫版本, 如何確保所有的用戶升級應用后數據庫都能用呢?有兩種方式:
- 確定 相鄰版本 的差別,從版本1開始依次迭代更新,先執行v1到v2,再v2到v3……
- 為 每個版本 確定與現在數據庫的差別,為每個case撰寫專門的升級代碼。
方式一的優點是每次更新數據庫的時候只需要在onUpgrade方法的末尾加一段從上個版本升級到新版本的代碼,易于理解和維護,缺點是當版本變多之后,多次迭代升級可能需要花費不少時間,增加用戶等待;
方式二的優點則是可以保證每個版本的用戶都可以在消耗最少的時間升級到最新的數據庫而無需做無用的數據多次轉存,缺點是強迫開發者記憶所有版本數據庫的完整結構,且每次升級時onUpgrade方法都必須全部重寫。
以上簡單分析了兩種方案的優缺點,它們可以說在花費時間上是剛好相反的,至于如何取舍,可能還需要結合具體情況分析。
參考:
SQLite 性能優化
參考:
網絡通訊
描述一次網絡請求的流程
- 建立TCP連接。在HTTP工作開始之前,Web瀏覽器首先要通過網絡與Web服務器建立連接,該連接是通過TCP來完成的,該協議與IP協議共同構建Internet,即著名的TCP/IP協議族,因此Internet又被稱作是TCP/IP網絡。HTTP是比TCP更高層次的應用層協議,根據規則,只有低層協議建立之后才能進行更高層協議的連接,因此,首先要建立TCP連接,一般TCP連接的端口號是80。
- Web瀏覽器向服務器發送請求命令。一旦建立了TCP連接,Web瀏覽器就會向Web服務器發送請求命令。例如:GET/sample/hello.jsp HTTP/1.1。
- Web瀏覽器發送請求頭信息。瀏覽器發送其請求命令之后,還要以頭信息的形式向Web服務器發送一些別的信息,之后瀏覽器發送了一空白行來通知服務器,它已經結束了該頭信息的發送。
- Web服務器應答。 客戶機向服務器發出請求后,服務器會客戶機回送應答, HTTP/1.1 200 OK ,應答的第一部分是協議的版本號和應答狀態碼。
- Web服務器發送應答頭信息。 正如客戶端會隨同請求發送關于自身的信息一樣,服務器也會隨同應答向用戶發送關于它自己的數據及被請求的文檔。
- Web服務器向瀏覽器發送數據。Web服務器向瀏覽器發送頭信息后,它會發送一個空白行來表示頭信息的發送到此為結束,接著,它就以Content-Type應答頭信息所描述的格式發送用戶所請求的實際數據。
- Web服務器關閉TCP連接。一般情況下,一旦Web服務器向瀏覽器發送了請求數據,它就要關閉TCP連接,然后如果瀏覽器或者服務器在其頭信息加入了這行代碼:Connection:keep-alive
TCP連接在發送后將仍然保持打開狀態,于是,瀏覽器可以繼續通過相同的連接發送請求。保持連接節省了為每個請求建立新連接所需的時間,還節約了網絡帶寬。
推送心跳包是TCP包還是UDP包或者HTTP包
TCP
參考:
Android 代碼中實現 WAP 方式聯網
- 通過 APN 列表,獲取代碼服務器和端口號,如果未設置,則設置成對應運營商的配置。
- 實現 HtppClient 代理。
CMWAP, CMNET有何區別,網絡通訊時是否要特殊處理?如何切換接入點?
斷點續傳
首先說多線程,我們要多線程下載一個大文件,就有開啟多個線程,多個connection,既然是一個文件分開幾個線程來下載,那肯定就是一個線程下載一個部分,不能重復。那么我們這么確定一個線程下載一部分呢,就需要我們在請求的header里面設置。
conn.setRequestProperty("Range", "bytes="+startPos+"-"+endPos);
這里startPos是指從數據端的哪里開始,endPos是指數據端的結束.根據這樣我們就知道,只要多個線程,按順序指定好開始跟結束,就可以不沖突的下載了。那么我們寫文件的時候又該怎么寫呢。
byte[] buffer = new byte[1024];
int offset = 0;
print("Thread "+this.threadId+" starts to download from position "+startPos);
RandomAccessFile threadFile = new RandomAccessFile(this.saveFile,"rwd");
threadFile.seek(startPos);
// ...
threadFile.write(buffer,0,offset);
這樣就可以保證數據的完整性,也不會重復寫入了。
那么我們接著說斷點續傳,斷點續傳其實也很簡單,原理就是使用數據庫保存上次每個線程下載的位置和長度。例如我開了兩個線程T1,T2來下載一個文件,設文件總大小為1024M,那么就是每個線程下載512M。可是我的下載中斷了,那么我下次啟動線程的時候(繼續下載),是不是應該要知道,我原來下載了多少呢。所以是這樣的,我沒下載一點,就更新數據庫的數據,例如T1,下載了100M,就要實時更新數據庫,記錄下100M,并且記錄下這個線程開始下載位置(startPos),還有線程負責的長度(512M)。那么我繼續下載的時候,就可以像服務器請求startPos+1000M開始的數據了,然后在文件里面也是seek(startPos+1000M)的位置繼續下載,就可以實現斷點續傳了。
參考:
網絡的優化
參考:
HttpClient
動態加載
插件化,動態加載
參考:
Android中ClassLoader和java中有什么關系和區別?
插件化的原理實際是 Java ClassLoader 的原理,看其他資料前請先看:Java ClassLoader基礎
Android 也有自己的 ClassLoader,分為 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader,區別在于 PathClassLoader 不能直接從 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已經安裝過的 apk(因為安裝過的 apk 在 cache 中存在緩存的 dex 文件)。而 DexClassLoader 可以加載外部的 apk、jar 或 dex文件,并且會在指定的 outpath 路徑存放其 dex 文件。
參考:
注解
什么是注解
參考:
Java Annotation 及幾個常用開源項目注解原理簡析
使用注解是否會影響性能
有些注解是用反射實現的所以影響性能。這類注解庫對程序的性能影響并沒有想象中的那么夸張,而且類似dagger2這類編譯時注解的框架是沒有性能影響的。
JNI
JNI開發流程
WebView
WebView和JS
React Native
jar
65k限制 做內部庫設計時,最重要的考慮是jar的成本,方法數、體積。
Android 傻瓜式分包插件
GitHub:https://github.com/TangXiaoLv/Android-Easy-MultiDex
這是一個可自定義哪些類放在 MainDex 中的插件。ReadMe 中詳細介紹了在使用 MultiDex 時,為了解決 MainDex 方法數超標的問題,碰到的一個個坑及如何解決,并列出了詳細的參考資料,一篇很不錯的文章。
參考:
U8SDK——支持自動拆分成多個dex文件(MultiDex支持)
其他
APP啟動過程
如何判斷應用被強殺
在Applicatio中定義一個static常量,賦值為-1,在歡迎界面改為0,如果被強殺,application重新初始化,在父類Activity判斷該常量的值。
參考:
http://blog.csdn.net/Small_Lee/article/details/51886746
應用被強殺如何解決
如果在每一個Activity的onCreate里判斷是否被強殺,冗余了,封裝到Activity的父類中,如果被強殺,跳轉回主界面,如果沒有被強殺,執行Activity的初始化操作,給主界面傳遞intent參數,主界面會調用onNewIntent方法,在onNewIntent跳轉到歡迎頁面,重新來一遍流程。
簡述靜默安裝的原理,如何在無需root權限的情況下實現靜默安裝?
參考:
Serializable和Parcelable的區別
- 都能實現序列化且可用于Intent間的數據傳遞
- Serializable是Java中的序列化接口,使用簡單但開銷大,序列化和反序列化過程需要大量I/O操作。
- Parcelable更適合Android平臺,使用麻煩但效率高,主要用在內存序列化上。
Debug和Release狀態的不同
Toolbar的使用
低版本 SDK 如何實現高版本 API
自己實現或@TargetApi annotation
在低版本的 SDK 使用高版本的 API 會報錯。解決方法是:在高版本 SDK 中使用高版本 API,低版本 SDK 中自己實現。
- 在使用了高版本 API 的方法前面加一個 @TargetApi(API版本號)。
- 在代碼中判斷版本號來控制不同的版本使用不同的代碼。
@TargetApi(11)
public void text() {
if(Build.VERSION.SDK_INT >= 11){
// 使用 API 11 的方法
} else {
// 使用自己實現的方法
}
實現一個單例
public class Singleton{
private volatile static Singleton mSingleton;
private Singleton(){
}
public static Singleton getInstance(){
if(mSingleton == null){\\A
synchronized(Singleton.class){\\C
if(mSingleton == null)
mSingleton = new Singleton();\\B
}
}
return mSingleton;
}
}
如 View 的事件分發,屏幕適配經驗,性能優化的經驗、Java 線程幾種用法等
如 AIDL、插件化, 如網絡的優化, 如緩存的處理, 如插件化, 如 Service 保活
函數調用Trace 怎么玩
AlarmManager以及Wakelock的使用
算法理解
什么是二分算法
設計模式
Android 中主要用到的幾種設計模式
Android設計模式
參考:
http://blog.csdn.net/bboyfeiyu/article/details/44563871
架構設計
mvc mvp mvvm
參考:
http://www.tianmaying.com/tutorial/AndroidMVC