分析和避免应用内存泄露

修复内存泄露的经验总结

 

By C.L.Wang

版本和分支

使用v7.7.0预发版本

Commit: ba18201192f98cd9a63da30a1748eb35c89db36a

工具

使用LeakCanary

版本:1.3.1

GitHub: https://github.com/square/leakcanary

误区:

(1)引入LeakCanary,项目的运行速度未受到较大影响

(2)使用LeakCanary检测到的错误都是存在的,不存在误报

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
 }


public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

引发内存泄露的原因

  • 单例持有Activity
  • 服务持有Activity
  • Timer定时器
  • 第三方库的Context
  • View持有Activity
  • 单例持有内部类

单例持有Acitvity

代码

// 位置: WelcomeActivity, @华仔
public class WelcomeActivity extends CYDoctorNetworkActivity40 {
    StartPageInfoManager.getInstance().getRemoteData(WelcomeActivity.this, null);
}


// 位置: StartPageInfoManager
public class StartPageInfoManager extends DataManager<StartPageAdInfo> {
    @Override
    public void getRemoteData(final Context context, OnGetRemoteDataListener listener) {
        getScheduler(context)
    } 
}

// 位置: DataManager @东哥
public abstract class DataManager<T> {
    protected WebOperationScheduler getScheduler(Context context) {
        if (sScheduler == null) {
            // 修改context.getApplicationContext()
            sScheduler = new WebOperationScheduler(context);
        }
        return sScheduler;
    }
}

// 位置: WebOperationScheduler
public class WebOperationScheduler {
    public WebOperationScheduler(Context context) {
        this(context, 0);
    }

    public WebOperationScheduler(Context context, int timeout) {
        super();
        mContext = new WeakReference<Context>(context.getApplicationContext());

        // !!!内存泄露
        if (context instanceof FragmentActivity)
            mFragmentActivity = (FragmentActivity) context;

        mAsyncTaskIds = new ArrayList<Integer>();
    }
}

Leak

Service持有Acitvity

代码

// 位置: UploadImageGridViewFragment @蛋蛋
public class UploadImageGridViewFragment {
	protected ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            mService.setActivity(getActivity());
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
}

// 位置: UploadImageForStartAskService @蛋蛋
public class UploadImageForStartAskService extends Service {
    // !!! 内存泄露
    public void setActivity(Activity activity) {
        this.mActivity = activity;
    }

    // 修改: 防止内存泄露, 添加位置, 根据逻辑判断, 或者重构代码
    // public void removeReferences() {
    //    mActivity = null;
    //    mHandler = null;
    // }
}

Leak

Timer定时器

代码

// 位置: MainActivity600 @国信
public class MainActivity600 extends MainActivity2 {
    // !!! 内存泄露
    // 计时器TimerTask和Timer配合使用时, 造成内存泄露
    private Timer mTimer = new Timer();

    // 修改
    protected void onDestroy() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
    }
}

Leak

第三方库Context

代码

// 位置: Initializer @东哥
public class Initializer {
    public static void init(final Activity activity) {
        initPicasso(activity);
    }

    private static void initPicasso(final Context context) {
        // !!!内存泄露
    	// 修改context.getApplicationContext()
        Picasso picasso = new Picasso.Builder(context)...
    }
}

Leak

View持有Context & 单例持有内部类

代码,这个泄露关系非常复杂,有多处不合理

// MainActivity 包含 NewsTabFragment 包含 NewsNormalFragment
// 位置: NewsNormalFragment @东哥
public class NewsNormalFragment extends RemoteDataList2Fragment {
    @Override
    protected G7BaseAdapter getListAdapter() {
    	// 注意, 此处使用的Activity, 表面NewsNormalViewHolder的引用是Activity.
        // 应该修改G7BaseAdapter的Context引用
        G7BaseAdapter adapter = new G7BaseAdapter(getActivity());
        adapter.setHolderForObject(NewsNormalItem.class, NewsNormalViewHolder.class);
        return adapter;
    }
}

// 位置: NewsNormalViewHolder @东哥
public class NewsNormalViewHolder extends G7ViewHolder<NewsNormalItem> {
    protected CYAutoScrollViewPager mVPHots; // 泄露位置
    @Override
    public void setData(Context context, NewsNormalItem data) {
        if (data.mHots != null) {
            // 注意此处使用的Activity
            HotNewsAdapter adapter = new HotNewsAdapter(context);
            ...
            for (int i = 0; i < data.mHots.size(); ++i) {
            	// !!!内存泄露, View持有Activity
                View dot = new View(context);
                ...
                mLLDots.addView(dot);
            }
        }
    }

    // @吴林
    public void setCommandReceiver(String actionPrefix){
	// !!!内存泄露, 持有View -> View持有Activity 
        LocalBroadcastManager.getInstance(getContext()).registerReceiver(new CommandReceiver(),filter);
    }
    class CommandReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
        	...
        }
    }
}

Leak

建议

  • 统一修改应用中的内存泄露
  • 在Debug中添加LeakCanary

益处

  • 减少OOM问题,应用更加稳定
  • 提升编程质量,程序员自我修养

坏处

  • 看到以前的代码,影响心情
  • 增加工作量,延迟开发项目

Over

大家想听什么样的技术讲座

Analyze App's Memory Leak

By C.L.Wang

Analyze App's Memory Leak

Talk about how to find and solve memory leak in the app.

  • 2,106