使用LeakCanary发现的Activity内存泄漏问题的修复

3


Leak canary显示我的Activity存在泄漏,但我不理解这个泄漏的原因。

我尝试从Activity中删除部分内容以确定泄漏的来源,但没有什么作用。我甚至将Activity的所有内容都删除了,但仍然得到相同的结果。经过这些测试和其他许多测试,我认为在将我发送到PostDetailActivity的Activity中存在问题,不知道我的判断正确吗?

我是新手,请原谅我的实现方式。

以下是Activity的代码:

    public class PostDetailActivity extends AppCompatActivity {

    //details of user and post
    String myUid, myName, myProfilPic, postId, pLikes, pDislike, pTitle, pTime, pImage, pUserPic, pUserName;

    private DatabaseReference likesRef;
    private DatabaseReference dislikeRef;
    boolean mProcessLike = false;
    boolean mProcessDislike = false;
    Integer position, dislike, like;


    //post views
    ImageView userPicture, postImage;
    TextView userName, postTime, postTitle;
    Button likeBtn, dislikeBtn, shareBtn;
    LinearLayout profileLayout, emptyRecycler, commentExtraSpace;
    public RecyclerView recyclerView;
    NestedScrollView scrollView;

    public static List<Comment> commentList;
    CommentAdapter commentAdapter;

    //add comments views;
    EditText commentEt;
    ImageButton sendCommentBtn;
    ImageView userCommentImage;

    Comment replyComment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // DARK/LIGHT THEME CODE START
        if (DarkThem) {
            setTheme(R.style.DarkTheme);

        } else {
            setTheme(R.style.LightTheme);

        }
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_post_detail);

        myUid = FirebaseAuth.getInstance().getCurrentUser().getUid();
        likesRef = FirebaseDatabase.getInstance().getReference().child("Likes");
        dislikeRef = FirebaseDatabase.getInstance().getReference().child("Dislikes");

        Toolbar toolbar = findViewById(R.id.toolBar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setTitle("");
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PostFragment.relogPost(position);
                onBackPressed();

            }
        });

        //getting post info with intent
        Intent intent = getIntent();
        postId = intent.getStringExtra("postId");
        pLikes = intent.getStringExtra("pUp");
        pDislike = intent.getStringExtra("pDown");
        pTitle = intent.getStringExtra("pTitle");
        pTime = intent.getStringExtra("pTime");
        pImage = intent.getStringExtra("pImage");
        pUserPic = intent.getStringExtra("uDp");
        pUserName = intent.getStringExtra("uName");
        try {
            String positionList = intent.getStringExtra("position");
            position = Integer.parseInt(positionList);
        } catch (Exception e) {

        }
        //initializing views post
        userPicture = findViewById(R.id.userPic);
        postImage = findViewById(R.id.pImage);
        userName = findViewById(R.id.userNamePost);
        postTime = findViewById(R.id.pTime);
        postTitle = findViewById(R.id.pTitle);
        likeBtn = findViewById(R.id.upBtn);
        dislikeBtn = findViewById(R.id.dwnBtn);
        shareBtn = findViewById(R.id.shareBtn);
        profileLayout = findViewById(R.id.profileLayout);
        recyclerView = findViewById(R.id.recyclerView);
        scrollView = findViewById(R.id.scrollView);
        emptyRecycler = findViewById(R.id.emptyRecycler);
        commentExtraSpace = findViewById(R.id.commentExtraSpace);

        //initializing views comment
        commentEt = findViewById(R.id.commentEt);
        sendCommentBtn = findViewById(R.id.sendComment);
        userCommentImage = findViewById(R.id.userCommentImage);

        //convert time to dd//mm//yyyy hh:mm am/pm
        Calendar calendar = Calendar.getInstance(Locale.getDefault());
        calendar.setTimeInMillis(Long.parseLong(pTime));
        String pTimeshown = DateFormat.format("dd/MM/yyyy hh:mm aa", calendar).toString();


        //set data for PostDetails
        userName.setText(pUserName);
        postTime.setText(pTimeshown);
        postTitle.setText(pTitle);
        likeBtn.setText(pLikes);
        dislikeBtn.setText(pDislike);


        //Set user pic
        if (pUserPic.equals("default")) {
            userPicture.setImageResource(R.mipmap.ic_launcher_round);
        } else {
            try {
                Glide.with(this).load(pUserPic).into(userPicture);
            } catch (Exception e) {

            }
        }

        //Setting postPicture
        if (pImage.equals("default")) {
            postImage.setImageResource(R.drawable.sunset);
        } else {
            try {
                Glide.with(this).load(pImage).into(postImage);
            } catch (Exception e) {

            }
        }


        setLikes(postId);
        setDislike(postId);
        loadComments();


        sendCommentBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sendComment();
                commentList.add(replyComment);
                commentAdapter.notifyDataSetChanged();
                scrollView.post(new Runnable() {
                    @Override
                    public void run() {
                        scrollView.scrollTo(0, recyclerView.getBottom());
                    }
                });
            }
        });


        dislikeBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                pDislike = dislikeBtn.getText().toString();
                pLikes = likeBtn.getText().toString();
                mProcessDislike = true;
                dislikeRef.addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {

                        if (mProcessDislike) {
                            if (dataSnapshot.child(postId).hasChild(myUid)) {
                                //already disliked ,so remove dislike
                                dislikeRef.child(postId).child(myUid).removeValue();
                                mProcessDislike = false;
                                dislikeBtn.setText(MessageFormat.format("{0}", Integer.parseInt(pDislike) - 1));
                                postList.get(position).setpDown("" + (Integer.parseInt(pDislike) - 1));
                                dislike = Integer.parseInt(pDislike) - 1;
                                postList.get(position).setpDown("" + dislike);
                                dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_downvote_black, 0, 0, 0);
                            } else {
                                //not liked
                                likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
                                    @Override
                                    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                                        //verifying is user has liked the post
                                        if (dataSnapshot.child(postId).hasChild(myUid)) {
                                            likesRef.child(postId).child(myUid).removeValue();
                                            dislikeRef.child(postId).child(myUid).setValue("-1");
                                            likeBtn.setText(MessageFormat.format("{0}", Integer.parseInt(pLikes) - 1));
                                            postList.get(position).setpUp("" + (Integer.parseInt(pLikes) - 1));
                                            dislike = Integer.parseInt(pDislike) + 1;
                                            dislikeBtn.setText(MessageFormat.format("{0}", dislike));
                                            postList.get(position).setpDown("" + dislike);
                                        } else {
                                            dislikeRef.child(postId).child(myUid).setValue("-1");
                                            dislike = Integer.parseInt(pDislike) + 1;
                                            dislikeBtn.setText(MessageFormat.format("{0}", dislike));
                                            postList.get(position).setpDown("" + dislike);
                                        }
                                        mProcessDislike = false;
                                        dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_disliked, 0, 0, 0);
                                        likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_upvote_black, 0, 0, 0);
                                    }

                                    @Override
                                    public void onCancelled(@NonNull DatabaseError databaseError) {
                                    }
                                });
                            }
                        }
                    }

                    @Override
                    public void onCancelled(@NonNull DatabaseError databaseError) {
                    }
                });
            }
        });

        likeBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                pDislike = dislikeBtn.getText().toString();
                pLikes = likeBtn.getText().toString();
                mProcessLike = true;
                likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {

                        if (mProcessLike) {
                            if (dataSnapshot.child(postId).hasChild(myUid)) {
                                //already liked ,so remove like
                                likesRef.child(postId).child(myUid).removeValue();
                                like = Integer.parseInt(pLikes) - 1;
                                mProcessLike = false;
                                likeBtn.setText(MessageFormat.format("{0}", like));
                                postList.get(position).setpUp("" + like);
                                likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_upvote_black, 0, 0, 0);
                            } else {
                                //not liked
                                dislikeRef.addListenerForSingleValueEvent(new ValueEventListener() {
                                    @Override
                                    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                                        //verifying is user has dislkied the post
                                        if (dataSnapshot.child(postId).hasChild(myUid)) {
                                            dislikeRef.child(postId).child(myUid).removeValue();
                                            likesRef.child(postId).child(myUid).setValue("1");
                                            dislikeBtn.setText(MessageFormat.format("{0}", Integer.parseInt(pDislike) - 1));
                                            postList.get(position).setpDown("" + (Integer.parseInt(pDislike) - 1));
                                            like = Integer.parseInt(pLikes) + 1;
                                            likeBtn.setText(MessageFormat.format("{0}", like));
                                            postList.get(position).setpUp("" + like);
                                            dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_downvote_black, 0, 0, 0);
                                        } else {
                                            likesRef.child(postId).child(myUid).setValue("1");
                                            like = Integer.parseInt(pLikes) + 1;
                                            likeBtn.setText(MessageFormat.format("{0}", like));
                                            postList.get(position).setpUp("" + like);
                                        }
                                        mProcessLike = false;
                                        likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_liked, 0, 0, 0);
                                    }

                                    @Override
                                    public void onCancelled(@NonNull DatabaseError databaseError) {
                                    }
                                });
                            }

                        }
                    }

                    @Override
                    public void onCancelled(@NonNull DatabaseError databaseError) {
                    }
                });
            }
        });

        scrollView.post(new Runnable() {
            @Override
            public void run() {
                scrollView.scrollTo(0, commentExtraSpace.getBottom());
            }
        });

        DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference().child("Users").child(myUid);
        databaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                myProfilPic = "" + dataSnapshot.child("imageURL").getValue();
                myName = "" + dataSnapshot.child("username").getValue();
            }

            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {
            }
        });
    }

    private void sendComment() {
        String comment = commentEt.getText().toString().trim();
        //checking if empty
        if (TextUtils.isEmpty(comment)) {
            Toast.makeText(this, getString(R.string.EmptyComment), Toast.LENGTH_SHORT).show();
            return;
        }
        String timeStamp = String.valueOf(System.currentTimeMillis());
        DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference("Comments");
        HashMap<String, String> hashMap = new HashMap<>();
        //put info in hashmap
        hashMap.put("commentId", timeStamp);
        hashMap.put("comment", comment);
        hashMap.put("timeStamp", timeStamp);
        hashMap.put("userId", myUid);
        hashMap.put("userPicture", myProfilPic);
        hashMap.put("userName", myName);
        hashMap.put("like", "0");
        hashMap.put("dislike", "0");
        hashMap.put("replies", "no");
        hashMap.put("liked", "no");
        hashMap.put("disliked", "no");
        databaseReference.child(postId).child(timeStamp).setValue(hashMap);
        replyComment = new Comment(timeStamp, comment, timeStamp, "noid", myProfilPic, myName, "0", "0", "no", "no", "no");
        commentEt.setText("");
    }


    private void loadComments() {
        //linear layout for recyclerView
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getApplicationContext());
        //set layout to recyclerView
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(linearLayoutManager);
        //init comments list
        commentList = new ArrayList<>();

        //path of the post,to get comments;
        DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference("Comments").child(postId);
        databaseReference.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                commentList.clear();

                for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
                    Comment comment = snapshot.getValue(Comment.class);
                    if (snapshot.child("comments").getChildrenCount() >= 1) {
                        comment.setReplies("" + snapshot.child("comments").getChildrenCount());

                    } else {
                        comment.setReplies("no");
                    }
                    if (snapshot.child("likes").hasChild(myUid)) {
                        comment.setLiked("yes");
                    } else {
                        comment.setLiked("no");
                    }
                    if (snapshot.child("dislikes").hasChild(myUid)) {
                        comment.setDisliked("yes");
                    } else {
                        comment.setDisliked("no");
                    }
                    comment.setLike(String.valueOf(snapshot.child("likes").getChildrenCount()));
                    comment.setDislike(String.valueOf(snapshot.child("dislikes").getChildrenCount()));
                    commentList.add(comment);
                    //setup adapter
                }
                commentAdapter = new CommentAdapter(PostDetailActivity.this, commentList, postId);
                //set adapter
                if (commentAdapter.getItemCount() > 0) {
                    recyclerView.setAdapter(commentAdapter);
                    emptyRecycler.setVisibility(View.GONE);
                } else {
                    recyclerView.setVisibility(View.GONE);
                    emptyRecycler.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {
            }
        });
    }

    private void setLikes(final String postKey) {
        likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                if (dataSnapshot.child(postKey).hasChild(myUid)) {
                    //user has liked this post
                    //indicate it with new design
                    likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_liked, 0, 0, 0);
                } else {
                    //user has not liked
                    likeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_upvote_black, 0, 0, 0);
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {
            }
        });
    }

    private void setDislike(final String postKey) {
        dislikeRef.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                if (dataSnapshot.child(postKey).hasChild(myUid)) {
                    //user has liked this post
                    //indicate it with new design
                    dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_disliked, 0, 0, 0);
                } else {
                    //user has not liked
                    dislikeBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_downvote_black, 0, 0, 0);
                }
            }

            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {

            }
        });

    }

    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return super.onSupportNavigateUp();
    }

    @Override
    public void onBackPressed() {
        //  PostFragment.recyclerView.getAdapter().notifyItemChanged(position);
        if (isTaskRoot() && getSupportFragmentManager().getBackStackEntryCount() == 0) {
            finishAfterTransition();
        } else {
            super.onBackPressed();
        }
    }

}

┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│    Leaking: NO (a class is never leaking)
│    ↓ static ActivityThread.sCurrentActivityThread
│                            ~~~~~~~~~~~~~~~~~~~~~~
├─ android.app.ActivityThread instance
│    Leaking: UNKNOWN
│    ↓ ActivityThread.mNewActivities
│                     ~~~~~~~~~~~~~~
├─ android.app.ActivityThread$ActivityClientRecord instance
│    Leaking: UNKNOWN
│    ↓ ActivityThread$ActivityClientRecord.nextIdle
│                                          ~~~~~~~~
├─ android.app.ActivityThread$ActivityClientRecord instance
│    Leaking: UNKNOWN
│    ↓ ActivityThread$ActivityClientRecord.activity
│                                          ~~~~~~~~
╰→ com.RLD.newmemechat.PostDetailActivity instance
​     Leaking: YES (ObjectWatcher was watching this because com.RLD.newmemechat.PostDetailActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
​     key = 533b7987-0f38-41f4-9f1a-e468dcf83264
​     watchDurationMillis = 8537
​     retainedDurationMillis = 3530~~~

你能分享PostDetailActivity的代码吗? - Nabzi
@Golnar 我已经添加了这个活动,抱歉它有点乱。 - TheShyBoy
这个泄漏跟踪有点令人惊讶,它显示gc根是一个系统类,但第一个元素是一个实例而不是一个类。你复制粘贴正确吗?如果是的话,你应该在LeakCanary项目上提交一个问题,并提供一个堆转储来帮助调查发生了什么。 - Pierre-Yves Ricau
@Pierre-YvesRicau 我重新附上了一位编辑建议删除的部分。我做了一些修改,目前还没有再次遇到这个问题,不过我认为我可以复现它。 - TheShyBoy
2个回答

1
这显然是由Android框架引起的泄漏。从泄漏跟踪中我可以看出,与你的代码无关。ActivityThread在ActivityThread.mNewActivities中维护了一个ActivityThread $ ActivityClientRecord的链表。
你可以在这里看到:

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=312-314;drc=d9b11b058c6a50fa25b75d6534a2deaf0e62d4b3

    // List of new activities (via ActivityRecord.nextIdle) that should
    // be reported when next we idle.
    ActivityClientRecord mNewActivities = null;

看起来这是由ActivityThread.Idler完成的,它在主线程空闲时运行:

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=2059-2096;drc=master

    private class Idler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            ActivityClientRecord a = mNewActivities;
            boolean stopProfiling = false;
            if (mBoundApplication != null && mProfiler.profileFd != null
                    && mProfiler.autoStopProfiler) {
                stopProfiling = true;
            }
            if (a != null) {
                mNewActivities = null;
                IActivityTaskManager am = ActivityTaskManager.getService();
                ActivityClientRecord prev;
                do {
                    if (localLOGV) Slog.v(
                        TAG, "Reporting idle of " + a +
                        " finished=" +
                        (a.activity != null && a.activity.mFinished));
                    if (a.activity != null && !a.activity.mFinished) {
                        try {
                            am.activityIdle(a.token, a.createdConfig, stopProfiling);
                            a.createdConfig = null;
                        } catch (RemoteException ex) {
                            throw ex.rethrowFromSystemServer();
                        }
                    }
                    prev = a;
                    a = a.nextIdle;
                    prev.nextIdle = null;
                } while (a != null);
            }
            if (stopProfiling) {
                mProfiler.stopProfiling();
            }
            applyPendingProcessState();
            return false;
        }
    }

这用于告诉活动管理器在创建活动后主线程何时变为空闲状态。不幸的是,如果主线程在Activity.onCreate()和Activity.onDestroy()之间没有空闲时间,似乎会引入泄漏问题。


这个由Android框架引起的泄漏是否也可能是我收到LeakActivity泄漏跟踪的原因?泄漏:是的(ObjectWatcher正在监视此对象,因为leakcanary.internal.activity.LeakActivity接收了Activity#onDestroy()回调并且Activity#mDestroyed为true) - TheShyBoy
可能是这样,但不一定,始终要查看泄漏跟踪以确定原因。 - Pierre-Yves Ricau
根据我的理解和泄漏跟踪,似乎是同样的原因,非常感谢您的回答! - TheShyBoy
我的问题是一旦调用了onWindowFocusChanged(),我立即启动了另一个活动。当我不这样做时,泄漏就消失了。 - Cactusroot

0

你不应该将 Firebase 数据库的引用和监听器放在 Activity 中,而应该将它们放在 ViewModel 中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接