Google Play测试:虚拟Pixel 2上App因ActivityResultLauncher空指针崩溃
问题描述
在Google Play控制台发布应用时,仅虚拟Google Pixel 2设备(1080x1920,Android 12,SDK 31,x86_64 en_US)出现崩溃,其余9台测试设备均正常。崩溃为两个相似的空指针异常,均指向ActivityResultLauncher.launch()调用。
错误信息
java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.activity.result.ActivityResultLauncher.launch(java.lang.Object)' on a null object reference
详细堆栈跟踪
FATAL EXCEPTION: Thread-2 Process: xxxxxxxx, PID: 8724 java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.activity.result.ActivityResultLauncher.launch(java.lang.Object)' on a null object reference at xxxxxxxx.models.StockInputDialog.lambda$init$9$xxxxxxxx-models-StockInputDialog(StockInputDialog.java:417) at xxxxxxxx.models.StockInputDialog$$ExternalSyntheticLambda9.onClick(Unknown Source:2) at android.view.View.performClick(View.java:7441) at android.view.View.performClickInternal(View.java:7418) at android.view.View.access$3700(View.java:835) at android.view.View$PerformClick.run(View.java:28676) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at androidx.test.espresso.base.Interrogator.loopAndInterrogate(Interrogator.java:10) at androidx.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:7) at androidx.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:1) at androidx.test.espresso.base.UiControllerImpl.injectMotionEvent(UiControllerImpl.java:5) at androidx.test.espresso.action.MotionEvents.sendUp(MotionEvents.java:6) at androidx.test.espresso.action.MotionEvents.sendUp(MotionEvents.java:1) at androidx.test.espresso.action.Tap.sendSingleTap(Tap.java:5) at androidx.test.espresso.action.Tap.-$$Nest$smsendSingleTap(Unknown Source:0) at androidx.test.espresso.action.Tap$1.sendTap(Tap.java:1) at androidx.test.espresso.action.GeneralClickAction.perform(GeneralClickAction.java:4) at androidx.test.espresso.ViewInteraction$SingleExecutionViewAction.perform(ViewInteraction.java:2) at androidx.test.espresso.ViewInteraction.doPerform(ViewInteraction.java:21) at androidx.test.espresso.ViewInteraction.-$$Nest$mdoPerform(Unknown Source:0) at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:6) at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:1) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loopOnce(Looper.java:201) at android.os.Looper.loop(Looper.java:288) at android.app.ActivityThread.main(ActivityThread.java:7839) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
代码实现(迁移至registerForActivityResult的简化结构)
public class StockActivity extends AppCompatActivity implements DialogCloseListener { private ActivityResultLauncher<Intent> stockGalleryActivityResultLauncher; private ActivityResultLauncher<Intent> stockCameraActivityResultLauncher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stockCameraActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { // 结果处理逻辑 }); stockGalleryActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { // 结果处理逻辑 }); } @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.action_add) { mStockInputDialog = new StockInputDialog(this, stockCameraActivityResultLauncher, stockGalleryActivityResultLauncher); return true; } else { return super.onOptionsItemSelected(item); } } } public class StockInputDialog { private final Context mContext; private AlertDialog mInputDialog; private ActivityResultLauncher<Intent> stockCameraActivityResultLauncher; private ActivityResultLauncher<Intent> stockGalleryActivityResultLauncher; public StockInputDialog(Context context, ActivityResultLauncher<Intent> pStockCameraActivityResultLaunchera, ActivityResultLauncher<Intent> pStockGalleryActivityResultLauncher) { mContext = context; stockCameraActivityResultLauncher = pStockCameraActivityResultLaunchera; stockGalleryActivityResultLauncher = pStockGalleryActivityResultLauncher; LayoutInflater li = LayoutInflater.from(this.mContext); View mPromptsView = li.inflate(R.layout.text_input_stock, null); final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this.mContext); alertDialogBuilder.setView(mPromptsView); final ImageButton imgButtonCam = mPromptsView.findViewById(R.id.addCam); final ImageButton imgButtonGal = mPromptsView.findViewById(R.id.addGal); imgButtonCam.setOnClickListener(view -> { Uri uri = FileProvider.getUriForFile(mContext, "fr.foo.bar.provider", new Product(Consts.TEMP_NUM).getIMGPathCacheFile(mContext)); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); stockCameraActivityResultLauncher.launch(intent); // 崩溃触发点 }); imgButtonGal.setOnClickListener(view -> { stockGalleryActivityResultLauncher.launch(new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)); // 崩溃触发点 }); } }
问题分析与修复方案
核心原因
ActivityResultLauncher对象为null,调用launch()时触发空指针异常。大概率是配置变更导致Activity重建:Android 12的Pixel 2设备测试过程中可能发生屏幕旋转等配置变更,此时StockActivity重建,但StockInputDialog引用的是旧Activity的Launcher实例,旧实例已失效并被置为null。
修复方案
方案1:通过Activity弱引用获取最新Launcher
不要直接传递Launcher给Dialog,改为持有Activity的弱引用,调用时从Activity获取最新实例,避免内存泄漏:
public class StockInputDialog { private final WeakReference<StockActivity> mActivityRef; private AlertDialog mInputDialog; public StockInputDialog(StockActivity activity) { mActivityRef = new WeakReference<>(activity); // 其他初始化逻辑... imgButtonCam.setOnClickListener(view -> { StockActivity activity = mActivityRef.get(); if (activity != null) { Uri uri = FileProvider.getUriForFile(activity, "fr.foo.bar.provider", new Product(Consts.TEMP_NUM).getIMGPathCacheFile(activity)); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); activity.getStockCameraActivityResultLauncher().launch(intent); } }); } }
同时在StockActivity中添加Launcher的getter方法:
public ActivityResultLauncher<Intent> getStockCameraActivityResultLauncher() { return stockCameraActivityResultLauncher; } public ActivityResultLauncher<Intent> getStockGalleryActivityResultLauncher() { return stockGalleryActivityResultLauncher; }
方案2:用ViewModel托管Launcher
利用ViewModel的生命周期独立性,避免配置变更导致的实例失效:
public class StockViewModel extends ViewModel { private ActivityResultLauncher<Intent> stockCameraLauncher; private ActivityResultLauncher<Intent> stockGalleryLauncher; public void initLaunchers(ActivityResultRegistry registry) { if (stockCameraLauncher == null) { stockCameraLauncher = registry.register("camera_launcher", new ActivityResultContracts.StartActivityForResult(), result -> { // 结果处理逻辑 }); } if (stockGalleryLauncher == null) { stockGalleryLauncher = registry.register("gallery_launcher", new ActivityResultContracts.StartActivityForResult(), result -> { // 结果处理逻辑 }); } } public ActivityResultLauncher<Intent> getStockCameraLauncher() { return stockCameraLauncher; } public ActivityResultLauncher<Intent> getStockGalleryLauncher() { return stockGalleryLauncher; } }
在StockActivity的onCreate中初始化ViewModel:
private StockViewModel mViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mViewModel = new ViewModelProvider(this).get(StockViewModel.class); mViewModel.initLaunchers(getActivityResultRegistry()); }
Dialog中通过ViewModel获取Launcher:
public StockInputDialog(StockActivity activity) { StockViewModel viewModel = new ViewModelProvider(activity).get(StockViewModel.class); // ... imgButtonCam.setOnClickListener(view -> { // ... viewModel.getStockCameraLauncher().launch(intent); }); }
方案3:临时空指针检查(不推荐)
在调用launch()前添加判空,避免崩溃,但无法解决根本问题:
imgButtonCam.setOnClickListener(view -> { // ... if (stockCameraActivityResultLauncher != null) { stockCameraActivityResultLauncher.launch(intent); } });
内容的提问来源于stack exchange,提问作者Chris972

