記憶體洩漏與優化分析指南
前言
在android開發中,我們都或多或少的會遇到一些記憶體洩漏的問題,雖然大都知道哪些情況會導致記憶體洩露,但是還是不可避免的會遇到類似的問題,因此,知道如何去查詢記憶體洩露就顯得非常重要了。本篇和大家分享下如何進行記憶體洩漏的定位分析,以及對記憶體佔用的優化分析。相信大家看了之後會有所收穫。
為了有一個良好的分析體驗,我特意新建了一個用於分析記憶體方面的專案,該專案是一個簡易的新聞客戶端,結構上大致是這樣的,mvp開發模式,網路資料方面採用Retrofit + rxjava,列表使用LRecyclerView,新聞頁面由ViewPager將十幾個不同型別的新聞列表Fragment頁面組合在一起。種情況由於頁面的切換,以及資料列表的重新整理載入等,在開發中還是比較典型的,在記憶體控制上也是有較高要求的,因此是比較適合用來做記憶體分析的。
記憶體快照分析方法
這裡我們直接使用Android Studio的記憶體分析工具進行分析。開啟Android Monitor,可看到Logcat,切換到Monitors,可看到記憶體,CPU相關資訊。
1. 找到當前分析的應用,這裡為com.test.memory。
2. 點選幾次Initiate GC,用於通知垃圾收集進行垃圾回收,避免無效的記憶體分析。
3. 點選Dump Java Heap,過一會就會開啟記憶體快照。
接下來分析記憶體快照
1. 點選選擇PackageTreeView,這樣就可以按包名層級進行類的查詢。
2. 左上部分就是應用相關的類的記憶體資訊了。通常我們只需按包名com.test.memory找到自己應用下的類進行分析,這裡找到NewsListFragment進行分析,它代表一種型別的新聞列表頁面。
3. 可以看到TotalCount這一列是12,也就是說當前有12個NewsListFragment物件,也就是12個NewsListFragment新聞列表頁面了,因為之前有將所有型別的頁面都開啟過了。
4. Shallow Size這一欄,可以看到是2880,代表的意思就是NewsListFragment的所有物件佔用了多少記憶體,這裡是12個的總大小,因此一個NewsListFragment的大小是240。注意,這裡僅僅是指NewsListFragment本身佔用的記憶體,而作為它的引用屬性物件所佔的記憶體是不算其中的,比如它持有的檢視View的大小是不算其中的,而只算一個int型別引用的大小,4位元組。所以Shallow Size通常並不大,因為它只是當前物件本身的大小,不算它引用物件的大小在其中。
5. Retained Size這一欄,是1757826,也就是1.75M大小了,也就是說12個NewsListFragment所持有的總大小是1.75M,這裡的持有大小,它不但包括NewsListFragment本身的大小,還包括它持有物件的大小,並且是它持有物件可被回收的大小。因此Retained Size是指,如果NewsListFragment這個物件被回收時,它最終能被回收的記憶體,也就是它本身的記憶體,和一部分只有被它引用的物件的記憶體,而還被其他物件持有的記憶體是不算在其中的,例如context物件,它不僅被NewsListFragment引用,所以它的記憶體大小是不算入在Retained Size中的。
6. 右上部分代表的是所選擇類的所有物件和其屬性所佔記憶體情況。例如這裡是NewsListFragment類的12個物件的具體記憶體情況,和它其中各個屬性引用物件的記憶體情況。這裡可以分析其中的哪些屬性或引用物件佔用的記憶體較高。
7. 下面的部分指的是當前的NewsListFragment物件被哪些物件引用了,可以檢視它的引用樹,可用於查詢最終導致記憶體洩露無法被釋放的最終根源。
記憶體洩露分析
明白瞭如何檢視記憶體快照資訊,知道它們代表的含義之後,接下來舉一個例子來分析下記憶體洩露問題。場景是這樣的,在每個新聞列表頁面個NewsListFragment的onCreateView方法時,我會新增一個LeakAnimView在其上,並開始執行縮放動畫,當onDestoryView時移除LeakAnimView,並停止它的動畫。程式碼如下:
public class NewsListFragment extends BaseListFragment<NewsPresenter>
implements NewsContract.View {
private LeakAnimView animView;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if(ControInfos.isTestLeak){
//如果測試記憶體洩露問題,則執行
animView = new LeakAnimView(view.getContext());
RelativeLayout parent = (RelativeLayout) view;
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
params.addRule(RelativeLayout.CENTER_IN_PARENT);
parent.addView(animView, params);
animView.start();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if(ControInfos.isTestLeak){
//如果測試記憶體洩露問題,則執行
if(animView != null && animView.getParent() != null){
RelativeLayout parent = (RelativeLayout) animView.getParent();
animView.cancel();
parent.removeView(animView);
animView = null;
}
}
}
...
}
下面是LeakAnimView的實現:
/**
* 存在記憶體洩露的動畫View,由於動畫Cancel之後,還是會回撥onAnimationEnd,所以需要額外判斷是否取消狀態,否則動畫會一直執行下去,導致記憶體洩露問題
*/
public class LeakAnimView extends View{
private static final String TAG = "AnimView";
private AnimatorSet animatorSet, animatorSet2;
private ObjectAnimator scaleX, scaleY;
private ObjectAnimator scaleX2, scaleY2;
private boolean isAnimating;
public LeakAnimView(Context context) {
super(context);
init();
}
public LeakAnimView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
setBackgroundColor(Color.RED);
animatorSet = new AnimatorSet();
scaleX = ObjectAnimator.ofFloat(this, "scaleX", 0.5f, 1f);
scaleY = ObjectAnimator.ofFloat(this, "scaleY", 0.5f, 1f);
animatorSet.play(scaleX).with(scaleY);
animatorSet.setDuration(500);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.d(TAG, "animatorSet onAnimationStart");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d(TAG, "animatorSet onAnimationEnd");
//取消動畫時,該方法依然會被回撥,所以下個動畫會執行,存在記憶體洩露問題,所以要做狀態的判斷
if(ControInfos.exitstLeak){
//這裡存在記憶體洩露問題
animatorSet2.start();
}else{
//這裡解決了記憶體洩露問題
if(isAnimating){
animatorSet2.start();
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
Log.d(TAG, "animatorSet onAnimationCancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animatorSet2 = new AnimatorSet();
scaleX2 = ObjectAnimator.ofFloat(this, "scaleX", 1f, 0.5f);
scaleY2 = ObjectAnimator.ofFloat(this, "scaleY", 1f, 0.5f);
animatorSet2.play(scaleX2).with(scaleY2);
animatorSet2.setDuration(500);
animatorSet2.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.d(TAG, "animatorSet2 onAnimationStart");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d(TAG, "animatorSet2 onAnimationEnd");
//取消動畫時,該方法依然會被回撥,所以下個動畫會執行,存在記憶體洩露問題,所以要做狀態的判斷
if(ControInfos.exitstLeak){
//這裡存在記憶體洩露問題
animatorSet.start();
}else{
//這裡解決了記憶體洩露問題
if(isAnimating){
animatorSet.start();
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
Log.d(TAG, "animatorSet2 onAnimationCancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
public void start(){
if(isAnimating){
return;
}
isAnimating = true;
animatorSet.start();
}
public void cancel(){
if(isAnimating){
isAnimating = false;
if(animatorSet.isRunning() || animatorSet.isStarted()){
animatorSet.cancel();
}
if(animatorSet2.isRunning() || animatorSet2.isStarted()){
animatorSet2.cancel();
}
}
}
}
上面只給出測試導致記憶體洩露的部分,其他程式碼實現可以看專案原始碼。其實導致記憶體洩露的原因也比較簡單,但是如果對動畫不是很熟悉的話,容易踩這個坑,做一個迴圈動畫,動畫1執行完後執行動畫2,動畫2執行完後執行動畫1,如此迴圈。重點是取消的時候,除了會回撥onAnimationCancel之外,仍然會回撥onAnimationEnd,而如果不在其中做標記判斷的話,那麼又會去執行下一個動畫,那麼取消方法並不能停止動畫,動畫會一直持有LeakAnimView,然後導致NewsListFragment即便是所屬的Activity頁面關閉了也不能被釋放,這時就存在記憶體洩露問題了。
如圖所示,NewsListFragment物件是12個,而LeakAnimView卻有62個之多,如果左右滑動更多的話,會一直增加,而從底部引用樹中也可以看出是動畫導致的記憶體洩露。那麼關閉頁面之後,看看這些NewsListFragment和LeakAnimView能不能被回收
發現這些物件並沒有隨著所屬Activity頁面的關閉而被回收。那麼在修改了記憶體洩露問題之後,看看效果是怎麼樣的
可以看到,LeakAnimView變成了12個,無論怎麼樣左右滑動頁面,它都只儲存在12以內,這說明記憶體洩露不存在了,同時當將頁面關閉時,可以看到LeakAnimView和NewsListFragment物件數量都為0,都被回收了。
記憶體佔用分析
上面我們通過分析將記憶體洩露的問題解決了,但是我們深知當前的狀態並不是完美的。雖然不存在記憶體洩露,但是記憶體佔用的問題還是可以進行優化的。特別是在每個列表頁面資料量大,頁面的佈局複雜,帶有重量級的控制元件在其中時,如果這些不能隨著PageAdapter的滑動進行一定的釋放的話,記憶體佔用也是會非常高,導致記憶體溢位的問題。這裡我們還是以LeakAnimView來做個例子吧,我們知道,當我們瀏覽過所有的NewsListFragment頁面後,NewsListFragment的物件數量維持在12,相應的LeakAnimView也是在12個。
但是不覺得有點奇怪嗎?我在NewsListFragment的onDestroyView中是做了移除操作的,並且將animView設為null了,照理說應該沒有被其他物件引用了,應該是可以被回收的,這樣的話,除了有兩三個LeakAnimView物件還存在之外,其他應該都是被回收的啦,但是為啥沒有呢?我們看下其中一個LeakAnimView物件的引用樹,發現了問題。
當前的LeakAnimView物件被[email protected] (0x12f534d8)給引用了,當然它還有被其他給引用,不過經分析,有效的引用是屬於RelativeLayout.DependencyGraph.Node的,那這個是幹嘛用的,跟進程式碼發現,原來RelativeLayout中有個Node來管理它的子View,每個子View作為一個節點Node,DependencyGraph則是用來管理節點Node,Node還持有的當前的LeakAnimView物件的話,說明Node沒有被釋放,執行release方法,也就是DependencyGraph沒有執行clear方法。
public class RelativeLayout{
...
private static class DependencyGraph {
...
void clear() {
final ArrayList<Node> nodes = mNodes;
final int count = nodes.size();
for (int i = 0; i < count; i++) {
nodes.get(i).release();
}
nodes.clear();
mKeyNodes.clear();
mRoots.clear();
}
static class Node {
...
void release() {
view = null;
dependents.clear();
dependencies.clear();
sPool.release(this);
}
}
}
}
再找哪裡呼叫了DependencyGraph的clear方法,發現是在RelativeLayout的sortChildren方法中,而sortChildren是在onMeasure方法中被呼叫的
public class RelativeLayout{
...
private void sortChildren() {
final int count = getChildCount();
if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
mSortedVerticalChildren = new View[count];
}
if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
mSortedHorizontalChildren = new View[count];
}
final DependencyGraph graph = mGraph;
graph.clear();
for (int i = 0; i < count; i++) {
graph.add(getChildAt(i));
}
graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
sortChildren();
}
...
}
}
也就是說onMeasure沒有在NewsListFragment執行onDestroyView時執行。那這個怎麼解決,我現在也沒有比較好的解決方案,想了一個做驗證性的方法,通過反射主動呼叫RelativeLayout的sortChildren方法
public class NewsListFragment extends BaseListFragment<NewsPresenter>
implements NewsContract.View {
...
@Override
public void onDestroyView() {
super.onDestroyView();
if(ControInfos.isTestLeak){
//如果測試記憶體洩露問題,則執行
if(animView != null && animView.getParent() != null){
Log.e("NewsListFragment", "remove pre, animView parent : " + animView.getParent());
RelativeLayout parent = (RelativeLayout) animView.getParent();
animView.cancel();
parent.removeView(animView);
//這裡通過反射主動呼叫RelativeLayout的sortChildren方法,達到清除animView被RelativeLayout.DependencyGraph.Node持有引用的問題
ReflectUtil.invokeMethod(parent.getClass().getName(), "sortChildren", parent, null, new Object[]{});
Log.e("NewsListFragment", "remove post, animView parent : " + animView.getParent());
Log.e("NewsListFragment", "remove post, parent size : " + parent.getChildCount());
animView = null;
}
}
}
}
現在測試一下看看效果。
很欣喜的看到,這個只有2個LeakAnimView物件了(當前的NewsListFragment和旁邊的NewsListFragment所持有的LeakAnimView物件)。說明確實是由於被RelativeLayout.DependencyGraph.Node持有的引用導致LeakAnimView物件不能被回收了。當然通過反射去實現不一定是合適的辦法,大家可以想想其他更合適的方法去實現。
顯然,這樣省去了10個LeakAnimView物件所佔用的記憶體,那麼再延伸到NewsListFragment持有的View的話,是不是可以想辦法去實現回收其他10個NewsListFragment中的View的記憶體呢,那麼想想,記憶體佔用是不是會減少很多?具體怎麼去做需要大家自己去做嘗試和驗證。
總結
好啦,到總結的時候了。無論是記憶體洩露的檢測分析,還是記憶體佔用的優化分析,都可以通過檢視Android Studio匯出的記憶體快照進行分析。記憶體洩露問題著重看類物件的數量Total Size,看是否符合預期,而記憶體佔用則更注重去找記憶體佔用較大的物件Shallow Size,分析它的數量,以及哪裡佔用了較大記憶體,分析是否合理,然後進行鍼對性的優化,更深的體會就得自己親自嘗試了,
我的GitHub
微信公眾號 hesong ,微信掃一掃下方二維碼即可關注: