1. 內(nèi)存泄漏
為什么會出現(xiàn)內(nèi)存泄漏?因為在GC垃圾回收時會利用GC Root可達(dá)性
分析算法去遍歷哪些對象正在被引用摧阅。如果一個對象該銷毀時卻被另一個更長生命周期的對象引用,則會發(fā)生該銷毀的對象無法被回收,導(dǎo)致內(nèi)存泄漏郭毕。
在Java中苍匆,有四種對象引用:強引用
可達(dá)性分析算法中此引用不會被回收刘急;軟引用
可達(dá)性分析算法如果此時內(nèi)存溢出時,這種引用的對象會被回收浸踩;弱引用
可達(dá)性分析算法叔汁,對于這種引用對象會將不在引用鏈之內(nèi)則會將其回收;虛引用
則是一種標(biāo)志作用民轴,在回收時會調(diào)用finalize()方法攻柠。
2. 內(nèi)存泄漏常見場景
2.1 單例模式中使用Context
當(dāng)在一個單例對象中持有Activity的Context引用時,該引用會持有整個應(yīng)用程序的生命周期后裸,這可能會導(dǎo)致內(nèi)存泄漏瑰钮。
public class MySingleton {
private Context mContext;
private static MySingleton sInstance;
private MySingleton(Context context) {
mContext = context;
}
public static MySingleton getInstance(Context context) {
if (sInstance == null) {
sInstance = new MySingleton(context);
}
return sInstance;
}
// ... some other methods ...
}
正確代碼:
public class MySingleton {
private Context mContext;
private static MySingleton sInstance;
private MySingleton(Context context) {
mContext = context.getApplicationContext(); //使用ApplicationContext代替Activity或Application的Context
}
public static MySingleton getInstance(Context context) {
if (sInstance == null) {
sInstance = new MySingleton(context);
}
return sInstance;
}
// ... some other methods ...
}
2.2 非靜態(tài)內(nèi)部類持有外部類引用
當(dāng)一個非靜態(tài)內(nèi)部類實例化時,它會持有一個對外部類實例的引用微驶,如果該內(nèi)部類的實例長時間存在浪谴,則可能導(dǎo)致外部類實例的生命周期過長开睡,從而導(dǎo)致內(nèi)存泄漏。
示例代碼:
public class MyActivity extends Activity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ... handle message ...
}
};
// ... some other methods ...
}
正確代碼:
public class MyActivity extends Activity {
private static class MyHandler extends Handler {
private WeakReference<MyActivity> mActivity;
public MyHandler(MyActivity activity) {
mActivity = new WeakReference<MyActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
MyActivity activity = mActivity.get();
if (activity != null) {
// ... handle message ...
}
}
}
private MyHandler mHandler = new MyHandler(this);
// ... some other methods ...
}
2.3 注冊廣播接收器未注銷
當(dāng)應(yīng)用程序注冊廣播接收器時苟耻,如果不及時注銷篇恒,它將一直存在,從而導(dǎo)致內(nèi)存泄漏凶杖。
正確代碼:
public class MyActivity extends Activity {
private BroadcastReceiver mReceiver;
private boolean mIsReceiverRegistered = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mReceiver = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
mIsReceiverRegistered = true;
registerReceiver(mReceiver, filter); //注冊廣播接收器
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mIsReceiverRegistered) { //判斷廣播接收器是否已注冊
unregisterReceiver(mReceiver); //注銷廣播接收器
mIsReceiverRegistered = false;
}
}
// ... some other methods ...
}
2.4 匿名內(nèi)部類持有外部類引用
當(dāng)一個匿名內(nèi)部類實例化時胁艰,它會持有一個對外部類實例的引用,如果該內(nèi)部類的實例長時間存在智蝠,則可能導(dǎo)致外部類實例的生命周期過長腾么,從而導(dǎo)致內(nèi)存泄漏。
示例代碼:
public class MyActivity extends Activity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread().start();
}
// ... some other methods ...
}
2.5 Handler導(dǎo)致的內(nèi)存泄漏
當(dāng)使用Handler時杈湾,如果在處理消息時解虱,持有Activity或Fragment的引用,則可能導(dǎo)致內(nèi)存泄漏漆撞。
示例代碼:
public class MyActivity extends Activity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ... handle message ...
}
};
// ... some other methods ...
}
正確代碼:
public class MyActivity extends Activity {
private static class MyHandler extends Handler {
private WeakReference<MyActivity> mActivity;
public MyHandler(MyActivity activity) {
mActivity = new WeakReference<MyActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
MyActivity activity = mActivity.get();
if (activity != null) {
// ... handle message ...
}
}
}
private MyHandler mHandler = new MyHandler(this);
// ... some other methods ...
}
2.6 資源沒有正確釋放導(dǎo)致的內(nèi)存泄漏
當(dāng)使用資源(如Bitmap殴泰、Cursor等)時,如果沒有正確釋放浮驳,則可能導(dǎo)致內(nèi)存泄漏悍汛。
示例代碼:
public class MyActivity extends Activity {
private Bitmap mBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
}
// ... some other methods ...
}
正確代碼:
public class MyActivity extends Activity {
private Bitmap mBitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBitmap != null) {
mBitmap.recycle();
mBitmap = null;
}
}
// ... some other methods ...
}
3. 內(nèi)存泄漏監(jiān)控
雖然對常見的內(nèi)存泄漏場景有認(rèn)識,但是還是需要對內(nèi)存泄漏進行自動監(jiān)控抹恳,目前有的檢測工具有:leakcanary和matrix的Resource Canary员凝。其使用過程不再講述。
3.1 檢測內(nèi)存泄漏
無論是使用Leakcanary還是Matrix工具奋献,它們檢測代碼是否內(nèi)存泄露都是一樣的思路:
通過 registerActivityLifecycleCallbacks在每個Activity銷毀onDestroy的時候健霹,通過弱引用WeakReference去持有Activity,然后通過間隔預(yù)定的時間手動調(diào)用GC瓶蚂,并通過弱引用的get()方法去查看Activity在內(nèi)存中是否被回收糖埋,如果沒有被回收則判斷為內(nèi)存泄露。當(dāng)然手動調(diào)用GC并一定會做回收操作窃这,像Matrix就通過多次的GC判斷瞳别,才認(rèn)為內(nèi)存泄露。
class ActivityRefWatcher(val application: Application?) {
private var handlerThread: HandlerThread? = null
private var handler: Handler? = null
private var lastTriggeredTime: Long = 0
private val maxRedetectTimes = 2
private val lock = java.lang.Object()
private val destroyedActivityInfos: ConcurrentLinkedQueue<DestroyedActivityInfo> by lazy { ConcurrentLinkedQueue() }
private val retryableTaskExecutor: RetryableTaskExecutor by lazy {
RetryableTaskExecutor(GC_TIME, handlerThread)
}
init {
handlerThread =
HandlerThreadUtil.getNewHandlerThread("ActivityRefWatcher", Thread.NORM_PRIORITY)
handler = HandlerThreadUtil.getDefaultHandler()
}
fun start() {
stopDetect()
application?.registerActivityLifecycleCallbacks(removedActivityMonitor)
scheduleDetectProcedure()
}
fun stop() {
stopDetect()
handler?.removeCallbacksAndMessages(null)
}
private val removedActivityMonitor: ActivityLifecycleCallbacks =
object : EmptyActivityLifecycleCallback() {
override fun onActivityDestroyed(activity: Activity) {
// 弱引用Activity杭攻,并收集相關(guān)信息
pushDestroyedActivityInfo(activity)
// 2s 后開始觸發(fā)gc
handler?.postDelayed({ triggerGc() }, delayTime)
}
}
private val scanDestroyedActivitiesTask: RetryableTaskExecutor.RetryableTask =
object : RetryableTaskExecutor.RetryableTask {
override fun execute(): RetryableTaskExecutor.RetryableTask.Status {
return checkDestroyedActivities()
}
}
private fun scheduleDetectProcedure() {
retryableTaskExecutor.executeInBackground(scanDestroyedActivitiesTask)
}
private fun stopDetect() {
application?.unregisterActivityLifecycleCallbacks(removedActivityMonitor)
unscheduleDetectProcedure()
}
private fun unscheduleDetectProcedure() {
retryableTaskExecutor.clearTasks()
destroyedActivityInfos.clear()
}
private fun pushDestroyedActivityInfo(activity: Activity) {
val activityName = activity.javaClass.name
val uuid = UUID.randomUUID()
val keyBuilder = java.lang.StringBuilder()
keyBuilder.append(ACTIVITY_REFKEY_PREFIX)
.append(activityName)
.append("_")
.append(java.lang.Long.toHexString(uuid.mostSignificantBits))
.append(java.lang.Long.toHexString(uuid.leastSignificantBits))
val key = keyBuilder.toString()
val destroyedActivityInfo = DestroyedActivityInfo(key, activity, activityName)
destroyedActivityInfos.add(destroyedActivityInfo)
synchronized(lock) {
lock.notifyAll()
}
}
/**
* 調(diào)用GC
*/
private fun triggerGc() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTriggeredTime < GC_TIME / 2 - 100) {
Log.d(TAG, "skip triggering gc for frequency")
return
}
lastTriggeredTime = currentTime
Log.d(TAG, "triggering gc...")
Runtime.getRuntime().gc()
try {
Thread.sleep(100)
} catch (e: InterruptedException) {
e.printStackTrace()
}
Runtime.getRuntime().runFinalization()
Log.d(TAG, "gc was triggered.")
}
private fun checkDestroyedActivities(): RetryableTaskExecutor.RetryableTask.Status {
if (destroyedActivityInfos.isEmpty()) {
synchronized(lock) {
try {
while (destroyedActivityInfos.isEmpty())
lock.wait()
} catch (ignored: Throwable) {
// Ignored.
}
}
return RetryableTaskExecutor.RetryableTask.Status.RETRY
}
triggerGc()
val infoIt = destroyedActivityInfos.iterator()
while (infoIt.hasNext()) {
val destroyedActivityInfo = infoIt.next()
triggerGc()
if (destroyedActivityInfo.activityRef.get() == null) {
infoIt.remove()
continue
}
++destroyedActivityInfo.detectedCount
if (destroyedActivityInfo.detectedCount < maxRedetectTimes) {
triggerGc()
continue
}
Log.i(
TAG,
"the leaked activity ${destroyedActivityInfo.activityName} with key ${destroyedActivityInfo.key} has been processed. stop polling",
)
// 內(nèi)存泄漏
infoIt.remove()
}
return RetryableTaskExecutor.RetryableTask.Status.RETRY
}
companion object {
private const val TAG = "ActivityRefWatcher"
private val delayTime: Long = 2_000
private const val ACTIVITY_REFKEY_PREFIX = "ACTIVITY_RESCANARY_REFKEY_"
private val GC_TIME = TimeUnit.MINUTES.toMillis(1)
}
}
3.2 dump與分析hprof
當(dāng)知道有Activity內(nèi)存泄漏之后祟敛,就要去分析內(nèi)存泄漏的引用鏈。這里可以通過內(nèi)存快照來分析泄漏鏈兆解,hprof文件就是虛擬機在某個時刻上所有對象的內(nèi)存快照馆铁,記錄了對象的類名,大小和引用關(guān)系等锅睛。所以埠巨,這里關(guān)于hprof有兩個操作历谍,一是dump hprof文件,二是分析hprof文件辣垒。這兩個操作都是耗時的望侈,一定要在子線程或通過fork一個子進程,進行dump和分析hprof勋桶。
dump hprof
dump內(nèi)存快照可以通過Debug.dumpHprofData
方法脱衙,dump文件大小可能有幾百兆,一些優(yōu)化是邊dump邊裁剪文件哥遮,這需要涉及到native hook的技術(shù)岂丘。
下面分別列出在子線程中dump和在子進程dump的實例代碼:
- 子線程dump
public void dumpHprofData() {
final String hprofPath = "/sdcard/myapp.hprof";
// 在異步線程中執(zhí)行dumpHprofData()操作
new Thread(new Runnable() {
@Override
public void run() {
Debug.dumpHprofData(hprofPath);
// 在主線程中提示用戶操作完成
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(MyActivity.this, "hprof file generated", Toast.LENGTH_SHORT).show();
}
});
}
}).start();
}
val storageDirectory = File(application?.cacheDir, "leakactivity")
if (!storageDirectory.exists()) {
storageDirectory.mkdir()
}
val fileName =
SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date())
val file = File(storageDirectory, fileName)
// dump 出堆轉(zhuǎn)儲文件
Debug.dumpHprofData(file.absolutePath)
Log.i(TAG, "dumpHeap: ${file.absolutePath}")
- 子進程dump
private fun dumpHprof() {
Thread {
// 創(chuàng)建一個子進程
val process = Runtime.getRuntime().exec(arrayOf("sh"))
// 獲取輸出流和輸入流
val outputStream = process.outputStream
val inputStream = process.inputStream
// 向子進程寫入命令
outputStream.write("am dumpheap com.package.name /sdcard/leak.hprof\n".toByteArray())
outputStream.flush()
// 讀取子進程輸出的結(jié)果
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
Log.d("DumpHprof", line!!)
}
// 關(guān)閉輸出流和輸入流
outputStream.close()
inputStream.close()
// 等待子進程結(jié)束
process.waitFor()
// 處理hprof文件
// ...
}.start()
}
這段代碼創(chuàng)建了一個子進程陵究,并向子進程發(fā)送命令來執(zhí)行Debug.dumpHprofData操作眠饮,這樣就可以讓主線程不卡頓了。
分析 hprof
分析hprof也是一種相對耗時的操作铜邮,分析hprof可以在本地也可以放到服務(wù)器上仪召,如果之前的hprof文件沒有裁剪,可以裁剪之后才分析或上傳松蒜。https://blog.yorek.xyz/android/3rd-library/hprof-shrink/ 一文中講了幾種方案扔茅,這里不展開分析。提供幾種開源方案秸苗,一種是shark_leakcanary庫召娜,一種是haha庫,當(dāng)然也可以自己實現(xiàn)分析hprof文件惊楼,其主要結(jié)構(gòu)是header和多個record組成玖瘸。
如下是使用shark_leakcanary庫實現(xiàn)的hprof文件的分析。
private fun dumpHeap() {
handler?.post {
val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener { step ->
Log.i(TAG, "Analysis in progress, working on: ${step.name}")
})
val heapAnalysis = heapAnalyzer.analyze(
heapDumpFile = file,
leakingObjectFinder = FilteringLeakingObjectFinder(
AndroidObjectInspectors.appLeakingObjectFilters
),
referenceMatchers = AndroidReferenceMatchers.appDefaults,
computeRetainedHeapSize = true,
objectInspectors = AndroidObjectInspectors.appDefaults.toMutableList(),
proguardMapping = null,
metadataExtractor = AndroidMetadataExtractor
)
Log.i(TAG, "dumpHeap: \n$heapAnalysis")
}
}
4. Bitmap優(yōu)化
4.1 常規(guī)Bitmap優(yōu)化
圖片占用的內(nèi)存大小 = 圖片寬度 × 圖片高度 × 每個像素占用的字節(jié)數(shù)
例如檀咙,如果有一張 1000 × 1000 像素的 ARGB_8888 格式的圖片雅倒,每個像素占用 4 個字節(jié),則該圖片占用的內(nèi)存大小為:1000 × 1000 × 4 = 4,000,000 字節(jié) = 3.81 MB
由于 Bitmap 對象可能占用大量的內(nèi)存弧可,因此在使用 Bitmap 時需要注意其優(yōu)化蔑匣,以避免內(nèi)存問題和性能問題。以下是一些 Android Bitmap 的優(yōu)化方案:
- 使用 inSampleSize 屬性來減少 Bitmap 對象的內(nèi)存使用棕诵,它指定了加載圖片時縮小的倍數(shù)裁良。例如,如果將 inSampleSize 設(shè)為 2校套,則圖片將被縮小為原始大小的 1/2价脾。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
- 使用 RGB_565 格式來減少 Bitmap 對象的內(nèi)存使用。默認(rèn)情況下搔确,Android 使用 ARGB_8888 格式來表示 Bitmap 對象彼棍。而使用 RGB_565 格式可以將每個像素的內(nèi)存使用減少到一半灭忠。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
- 使用 BitmapRegionDecoder 來只加載圖片的一部分,而不是整個圖片座硕。例如弛作,如果只需要加載圖片的頂部一部分,可以使用以下代碼:
InputStream inputStream = getResources().openRawResource(R.drawable.image);
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, false);
Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, decoder.getWidth(), decoder.getHeight() / 2), null);
- 緩存 Bitmap 對象
使用 LruCache 來緩存 Bitmap 對象华匾,以避免頻繁地創(chuàng)建和銷毀 Bitmap 對象映琳。LruCache 是一個內(nèi)存緩存類,可以在內(nèi)存達(dá)到一定限制時自動刪除最近最少使用的對象蜘拉。例如萨西,以下代碼演示了如何使用 LruCache 來緩存 Bitmap 對象:
private LruCache<String, Bitmap> mBitmapCache;
public void initBitmapCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
};
}
public void addBitmapToCache(String key, Bitmap bitmap) {
if (getBitmapFromCache(key) == null) {
mBitmapCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromCache(String key) {
return mBitmapCache.get(key);
}
- 合理釋放 Bitmap 對象
當(dāng) Bitmap 對象不再使用時,需要手動調(diào)用 recycle() 方法來釋放內(nèi)存旭旭。例如谎脯,以下代碼演示了如何在 ImageView 中加載 Bitmap 并在不再需要時釋放 Bitmap:
ImageView imageView = findViewById(R.id.image_view);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
imageView.setImageBitmap(bitmap);
// 釋放 Bitmap 對象
imageView.setImageBitmap(null);
bitmap.recycle();
4.2 大圖監(jiān)控
可以Plugin、Transform持寄、ASM技術(shù)來在ImageView的方法進行插樁技術(shù)源梭。
- 創(chuàng)建一個Gradle插件,使用Transform技術(shù)修改字節(jié)碼稍味。
- 使用ASM技術(shù)在ImageView的setImageDrawable()方法中插入代碼废麻,用于監(jiān)控圖片的寬高。
具體代碼不表模庐。
5. 內(nèi)存常見優(yōu)化
最后在來總結(jié)下烛愧,內(nèi)存優(yōu)化的有哪些常用方案:
使用 SparseArray 和 ArrayMap 替代 HashMap
HashMap 存儲大量的對象時會消耗很大的內(nèi)存,尤其是在處理大量數(shù)據(jù)時掂碱。在 Android 開發(fā)中怜姿,我們可以使用 SparseArray 和 ArrayMap 來替代 HashMap。SparseArray 是 Android 提供的一個優(yōu)化版的 Map顶吮,專門用來處理鍵為 int 類型的情況社牲;而 ArrayMap 則是優(yōu)化版的 Map,專門用來處理小數(shù)據(jù)集合的情況悴了,它比 HashMap 更加高效搏恤。使用 Bitmap 配置的 ARGB_8888
Bitmap 是 Android 開發(fā)中經(jīng)常使用的對象,它占用大量的內(nèi)存湃交。在使用 Bitmap 時熟空,我們可以通過設(shè)置 Bitmap 的 Config 來減少內(nèi)存消耗。ARGB_8888 是一種高質(zhì)量的 Bitmap 配置搞莺,雖然會占用更多的內(nèi)存息罗,但是可以保證圖片的清晰度。使用 BitmapFactory.Options 來壓縮圖片
在 Android 應(yīng)用中才沧,我們經(jīng)常需要加載大量的圖片迈喉,而這些圖片的分辨率往往很高绍刮,導(dǎo)致內(nèi)存消耗過大。為了減少內(nèi)存消耗挨摸,我們可以使用 BitmapFactory.Options 來對圖片進行壓縮孩革。可以通過設(shè)置 BitmapFactory.Options 中的 inSampleSize 屬性來控制壓縮比例得运。使用 LruCache 來緩存對象
LruCache 是 Android 提供的一種緩存對象的方式膝蜈,它可以幫助我們減少內(nèi)存消耗。LruCache 可以按照最近最少使用的原則來緩存對象熔掺,并且可以根據(jù)緩存對象的大小來自動調(diào)整緩存容量饱搏。及時釋放資源
在 Android 開發(fā)中,我們需要及時釋放無用的資源置逻,以避免內(nèi)存泄漏和內(nèi)存溢出推沸。例如,關(guān)閉 Cursor 對象诽偷、釋放 Bitmap 對象坤学、及時取消異步任務(wù)等。使用工具檢查內(nèi)存泄漏問題
使用內(nèi)存分析工具报慕,可以幫助我們檢查內(nèi)存問題,包括內(nèi)存泄漏和內(nèi)存溢出压怠。我們可以使用這些工具來找出應(yīng)用中的內(nèi)存問題眠冈,并及時進行優(yōu)化。優(yōu)化布局和控件
在布局中菌瘫,我們可以使用 FrameLayout 代替 RelativeLayout蜗顽,因為 RelativeLayout 對內(nèi)存消耗較大。在使用控件時雨让,我們可以避免使用過多的控件和嵌套控件雇盖,盡量使用簡單的布局方式和控件。