常見導(dǎo)致內(nèi)存問題的案例
1. Handler的使用
private val handler = Handler(Looper.getMainLooper(), object : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
//Looper內(nèi)的message -> handler -> 匿名內(nèi)部類 -> activity
Log.d(TAG, "handleMessage")
TAG = "Callback"
return true;
}
})
private val handler1 = object:Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
TAG = "handler1"
super.handleMessage(msg)
}
}
inner class InnerHandler(context: Context?) : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
TAG = "inner class"
}
}
handler.postDelayed(Runnable {
TAG = ""
}, 200000)
handler.sendEmptyMessageDelayed(0, 200000)
在 Handler 代碼里 android.os.Handler#enqueueMessage 很明顯能看到 message 持有 Handler 的引用。因為一個線程只有一個 Looper , 但是可以有多個 Handler ,所以 message 需要持有 Handler 引用來分發(fā)到指定的 Handler 。
然而上面三種寫法都有導(dǎo)致 Handler 持有外部 Activity 的引用。
第一種是 object 為 實現(xiàn)了 Handler.Callback 接口的匿名內(nèi)部類
第二種是 object 為繼承了 Handler 的 匿名內(nèi)部類
第三種是 InnerHandler 為繼承了 Handler 的內(nèi)部類
所以只要消息一直存在于主線程,就會導(dǎo)致 Activity 不能釋放,導(dǎo)致了內(nèi)存泄漏。
解決方法:
//靜態(tài)內(nèi)部類 kotlin 也為了避免內(nèi)部類持有導(dǎo)致的內(nèi)存泄漏頻繁發(fā)生,所以 默認(rèn)一個 class 代表的是靜態(tài)內(nèi)部類,不持有外部引用
//inner class 才是內(nèi)部類
class Handler2(context: Context?) : Handler(Looper.getMainLooper()) {
private val weakReference: WeakReference<Context> = WeakReference(context)
override fun handleMessage(msg: Message) {
//TAG = "weakReference"
weakReference.get()?.let {
Log.e("", "")
}
}
}
//如果 Message 在退出界面后可以忽略不處理時,可以選擇移除消息
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
2. Static/單例模式
Singleton.getInstance(MainActivity@this)
public class Singleton {
private static Singleton mInstance;
private Context context;
private Singleton(Context context) {
this.context = context;
}
public static Singleton getInstance(Context context){
if(mInstance == null){
mInstance = new Singleton(context);
}
return mInstance;
}
}
一般情況下,靜態(tài)變量 和 Class 與 ClassLoader 的生命周期,進(jìn)程的生命周期一致。上訴寫法會導(dǎo)致 第一次調(diào)用 getInstance 傳入的activity 不會釋放,從而導(dǎo)致內(nèi)存泄漏。所以 Static 關(guān)鍵字修飾的成員變量要注意引用關(guān)系。
解決方案:
- 盡量不在單例里構(gòu)造函數(shù)使用 context , 如果一定要用的話,可以傳入 ApplicationContext 因為它與進(jìn)程的生命周期一致。大部分情況下不會涉及界面的修改, ApplicationContext 就足夠了。
- 如果實在非要使用 Activity 的 context,可以使用軟引用。
3. 非靜態(tài)匿名內(nèi)部類
val thread = Thread(mRunnable)
thread.start()
private val mRunnable = object :Runnable {
override fun run() {
try {
TAG = "mRunnable"
Thread.sleep(200000);
}catch (e: Exception){
e.printStackTrace()
}
}
}
mRunnable 為非靜態(tài)匿名內(nèi)部類實例,持有了外部引用,與 handler 例子類似導(dǎo)致了泄漏。
解決方案:
JAVA 中 設(shè)置 mRunnable 為靜態(tài) 。
kotlin 編譯階段有優(yōu)化,只要不掉用外部變量,則不會持有引用。
4.屬性動畫
anim = ValueAnimator.ofFloat(0f, 1f)
anim.duration = 30000
animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f)
animator.duration = 500000
animator.start()
主要原因在于 android.animation.AnimationHandler#sAnimatorHandler 是 static 的,導(dǎo)致了內(nèi)存泄漏。
可以按如下順序跟代碼:
android.animation.ValueAnimator#cancel
android.animation.ValueAnimator#endAnimation
android.animation.ValueAnimator#removeAnimationCallback
android.animation.ValueAnimator#getAnimationHandler
android.animation.AnimationHandler#getInstance
android.animation.AnimationHandler#sAnimatorHandler
解決方案:
override fun onDestroy() {
super.onDestroy()
anim.cancel()
animator.cancel()
}
5.資源未關(guān)閉
BraodcastReceiver,ContentObserver,F(xiàn)ile,游標(biāo) Cursor,Stream 等資源未及時解注冊和釋放
解決方案:
及時解注冊。資源及時釋放。有些語法糖比如Java 1.7 提供的try-with-resource。以及Kotlin FileReadWrite.kt 大多實現(xiàn)了kotlin.io.CloseableKt#use
6.自定義View onDraw 每次new對象
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
}
onDraw執(zhí)行非常頻繁,如果在里面new 對象會導(dǎo)致頻繁的申請內(nèi)存和釋放。另外一方面,頻繁的 GC 也會導(dǎo)致卡頓。
解決方案:
private Paint mPaint;
public TestView(Context context) {
super(context);
mPaint = new Paint();
}
7. 所有圖片都預(yù)先加載
//mJustView 其實只有部分機(jī)型收到廣播后才需要add到window上,然而這里直接在初始化時就已經(jīng)加載到內(nèi)存里了。
private void makeJustView() {
RelativeLayout root = (RelativeLayout) View.inflate(mContext, R.layout.eye_layout, null);
mConfrim = (Button) root.findViewById(R.id.confirm);
mConfrim.setOnClickListener(this);
mJustView = root;
}
private void init() {
makeJustView();
}
解決方案:
//使用懶加載,僅有部分機(jī)型收到廣播后,才會加載這張圖,對于其他機(jī)型這部分內(nèi)存就省下了
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context conmTextView, Intent intent) {
showProtectView(getJustView());
}
}
};
private View getJustView(){
if(mJustView == null){
makeJustView();
}
return mJustView;
}
private void init() {
}
8. 圖片放錯目錄
假設(shè)本應(yīng)該放在xhdpi的圖片 放在了mdpi目錄下,在xhdpi的機(jī)器上運行時,長寬會被放大兩倍,相應(yīng)的內(nèi)存會變成兩倍。
直接放錯的概率不到,比較容易發(fā)生在換圖,UI只提供了一張圖什么的。
9. XML布局
有些view可能在布局里只顯示一次的,比如引導(dǎo)圖,其實是可以使用 ViewStub。代替VISIBLE/GONE的,需要時再加載。
imageView.setVisibility(View.VISIBLE);
imageView.setVisibility(View.GONE);
10.Bitmap使用
- inSampleSize 采樣加載bitmap
BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
-
Matrix 矩陣 小圖放大,內(nèi)存只有采樣出來的大小,但是顯示效果是放大的。
Matrix matrix = new Matrix(); matrix.postScale(2, 2, 0, 0); imageView.setImageMatrix(matrix); imageView.setScaleType(ScaleType.MATRIX); imageView.setImageBitmap(bitmap); -
Bitmap的像素格式
格式 描述 ALPHA_8 只有一個alpha通道 ARGB_4444 這個從API 13開始不建議使用,因為質(zhì)量太差 ARGB_8888 ARGB四個通道,每個通道8bit,默認(rèn)。 RGB_565 每個像素占2Byte,其中紅色占5bit,綠色占6bit,藍(lán)色占5bit。不需要 alpha 時資源為jpg時用這個比較理想 幀動畫 比如 loading動畫 //用自定義view 代替