修复 Leak canary 发现的 Activity 内存泄漏

Fixing Activity memory leaks found by Leak canary


Leak Canary 显示我的 activity 有泄漏,但我不明白泄漏的原因。

我试图从 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~~~

您不应将您的 firebase 数据库引用和侦听器放在 activity 中。您应该将它们放在您的 viewModel 中。

这显然是 Android 框架造成的漏洞。 leaktrace 中与您的代码没有任何关系,据我所知,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 后主线程空闲时通知 activity 管理器。不幸的是,如果主线程在 Activity.onCreate() 和 Activity.onDestroy() 之间没有空闲,这似乎会导致泄漏。