Fragment中使用Glide+Firebase时getActivity()空指针问题求助
我正在开发一款允许用户点击ImageView更换头像的应用,头像来自用户相册并上传至FirebaseStorage。用户的个人信息单独存储在FirebaseDatabase中,图片URL会后续添加。
选择图片并上传后,出现如下错误:
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.app.Activity.isDestroyed()' on a null object reference
但图片仍成功上传至FirebaseStorage,用户信息中的ImageUrl也已更新,重启应用能正常显示刚上传的图片。错误指向Glide.with(getActivity())代码段,该代码位于FirebaseDatabase.addValueEventListener中。
以下是相关代码片段:
onCreateView方法
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment FrameLayout rootView = (FrameLayout)inflater.inflate(R.layout.fragment_tch_profile, container, false); mDataRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { tv_email.setText(dataSnapshot.child("email").getValue(String.class)); tv_name.setText(dataSnapshot.child("name").getValue(String.class)); tv_gs.setText(dataSnapshot.child("cassSection").getValue(String.class)); if(dataSnapshot.hasChild("tImage")){ Glide.with(getActivity()) .load(dataSnapshot.child("tImage").getValue(String.class)) .crossFade() .placeholder(R.mipmap.ic_loader) .thumbnail(0.1f) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(iv_image); } } @Override public void onCancelled(DatabaseError databaseError) { Toast.makeText(getActivity(),"The read failed: " + databaseError.getCode(),Toast.LENGTH_SHORT).show(); } }); return rootView; }
onRequestPermissionsResult方法
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case READ_EXTERNAL_STORAGE: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) callGallery(); return; } }
onActivityResult方法
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == GALLERY_INTENT && resultCode == RESULT_OK) { mImageURI = data.getData(); iv_image.setImageURI(mImageURI); StorageReference filePath = mStorageRef.child("userImage").child(mImageURI.getLastPathSegment()); filePath.putFile(mImageURI).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() { @Override public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) { Uri downloadUri = taskSnapshot.getDownloadUrl(); //Ignore This error mDataRef.child("tImage").setValue(downloadUri.toString()); } }); } }
辅助方法callGallery
private void callGallery() { Intent i = new Intent(Intent.ACTION_PICK); i.setType("image/*"); startActivityForResult(i, GALLERY_INTENT); }
Logcat错误信息:
FATAL EXCEPTION: main
Process: kayaba.akihiro.educ_games, PID: 4502
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.app.Activity.isDestroyed()' on a null object reference
at com.bumptech.glide.manager.RequestManagerRetriever.assertNotDestroyed(RequestManagerRetriever.java:133)
at com.bumptech.glide.manager.RequestManagerRetriever.get(RequestManagerRetriever.java:102)
at com.bumptech.glide.Glide.with(Glide.java:653)
at kayaba.akihiro.educ_games.TchProfile$3.onSuccess(TchProfile.java:169)
at kayaba.akihiro.educ_games.TchProfile$3.onSuccess(TchProfile.java:161)
at com.google.firebase.storage.zzi.zzi(Unknown Source)
at com.google.firebase.storage.zzz.run(Unknown Source)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
哈哈,这个问题我太熟了!本质就是Fragment生命周期和Firebase异步回调的冲突——当Firebase的ValueEventListener触发回调时,你的Fragment可能已经和Activity脱钩了(比如用户在上传头像的过程中切到了其他页面),这时候getActivity()返回null,Glide拿着空的Activity去初始化就炸了。
给你几个实用的解决办法,按推荐程度排序:
1. 直接绑定Glide到Fragment本身(最省心)
Glide本身支持和Fragment的生命周期绑定,你不用再纠结getActivity()是否有效,直接把Fragment实例传给Glide就行:
if(dataSnapshot.hasChild("tImage")){ Glide.with(TchProfile.this) // 这里传Fragment自己,不是getActivity() .load(dataSnapshot.child("tImage").getValue(String.class)) .crossFade() .placeholder(R.mipmap.ic_loader) .thumbnail(0.1f) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(iv_image); }
Glide内部会自动监听Fragment的生命周期,一旦Fragment销毁,它会自动取消图片加载请求,既避免了空指针,还能防止内存泄漏,一举两得!
2. 手动移除Firebase监听器(从根源切断问题)
你现在用的是addValueEventListener,这个监听器会一直监听数据变化,哪怕Fragment已经销毁了,它可能还会触发回调。所以最好在Fragment销毁时主动移除它:
- 先把监听器存成Fragment的成员变量:
private ValueEventListener mProfileDataListener;
- 在
onCreateView里初始化并添加:
mProfileDataListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // 原来的更新UI逻辑... } @Override public void onCancelled(DatabaseError databaseError) { Toast.makeText(getActivity(),"The read failed: " + databaseError.getCode(),Toast.LENGTH_SHORT).show(); } }; mDataRef.addValueEventListener(mProfileDataListener);
- 在Fragment的
onDestroyView方法里移除监听器:
@Override public void onDestroyView() { super.onDestroyView(); if (mDataRef != null && mProfileDataListener != null) { mDataRef.removeEventListener(mProfileDataListener); } }
这样Fragment销毁后,监听器就不会再触发回调,从根源上避免了空指针的问题,还能优化内存使用。
3. 提前检查Context有效性(应急方案)
如果上面两种办法暂时不想改,也可以在调用Glide前先检查Context是否有效:
if(dataSnapshot.hasChild("tImage")){ // 先判断Fragment是否还和Activity绑定 if (isAdded()) { Glide.with(requireContext()) .load(dataSnapshot.child("tImage").getValue(String.class)) .crossFade() .placeholder(R.mipmap.ic_loader) .thumbnail(0.1f) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(iv_image); } }
isAdded()方法可以判断Fragment是否已经附加到Activity上,requireContext()是AndroidX Fragment提供的安全获取Context的方法(如果Context为空会抛出明确异常,方便调试)。
内容的提问来源于stack exchange,提问作者a.kyaba

