三星设备在拍照后应用程序崩溃了。

3

我的应用程序允许用户使用设备的默认相机应用程序拍照,就像这样:

    private void takePhoto(Uri outputUri) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
        startActivityForResult(intent, TAKE_PHOTO_REQUEST_CODE);
    }

查看崩溃日志,我发现许多应用程序崩溃似乎是在拍照后恢复应用程序时发生的。
异常和堆栈跟踪:

Fatal Exception: android.view.ViewRootImpl$CalledFromWrongThreadException
Only the original thread that created a view hierarchy can touch its views.
android.view.ViewRootImpl.checkThread (ViewRootImpl.java:11379)
android.view.ViewRootImpl.requestLayout (ViewRootImpl.java:2562)
android.view.ViewRootImpl.updateConfiguration (ViewRootImpl.java:6324)
android.app.ActivityThread.handleActivityConfigurationChanged (ActivityThread.java:6925)
android.app.servertransaction.ActivityConfigurationChangeItem.execute (ActivityConfigurationChangeItem.java:53)
android.app.servertransaction.ActivityTransactionItem.execute (ActivityTransactionItem.java:45)
android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
android.app.ActivityThread$H.handleMessage (ActivityThread.java:2571)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:226)
android.os.Looper.loop (Looper.java:313)
android.app.ActivityThread.main (ActivityThread.java:8741)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)

堆栈跟踪有几个不同的变体,但它们都经历了一些配置更改,这让我相信用户在拍照时旋转了设备,当应用程序以不同的方向返回前台时崩溃了。
我的应用程序活动在其清单中具有android:configChanges="orientation|screenSize",因为它包含一个WebView,但在onConfigurationChanged期间它不做任何事情。
在调查崩溃时,我添加了日志以打印线程名称和ID到主要生命周期方法以及onConfigurationChanged。我发现onConfigurationChanged在应用程序崩溃之前就会在与onCreate调用相同的主线程上调用。在崩溃之前没有调用onStartonResume
为整个事情增加更多神秘感,崩溃仅发生在三星设备上(但在各种手机/平板电脑型号上)。
尽管我认为我的应用程序没有做任何独特或不寻常的事情,但我找不到任何类似的在线报告。

任何想法/建议将不胜感激!

编辑
添加更多堆栈跟踪变体:

Fatal Exception: android.view.ViewRootImpl$CalledFromWrongThreadException
Only the original thread that created a view hierarchy can touch its views.
android.view.ViewRootImpl.checkThread (ViewRootImpl.java:11586)
android.view.ViewRootImpl.requestLayout (ViewRootImpl.java:2648)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.widget.TextView.onConfigurationChanged (TextView.java:4706)
android.view.View.dispatchConfigurationChanged (View.java:16145)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewRootImpl.updateConfiguration (ViewRootImpl.java:6502)
android.app.ActivityThread.handleActivityConfigurationChanged (ActivityThread.java:6941)
android.app.servertransaction.ActivityConfigurationChangeItem.execute (ActivityConfigurationChangeItem.java:53)
android.app.servertransaction.ActivityTransactionItem.execute (ActivityTransactionItem.java:45)
android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
android.app.ActivityThread$H.handleMessage (ActivityThread.java:2574)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:226)
android.os.Looper.loop (Looper.java:313)
android.app.ActivityThread.main (ActivityThread.java:8757)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)

Fatal Exception: android.view.ViewRootImpl$CalledFromWrongThreadException
Only the original thread that created a view hierarchy can touch its views.
android.view.ViewRootImpl.checkThread (ViewRootImpl.java:11586)
android.view.ViewRootImpl.requestLayout (ViewRootImpl.java:2648)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.view.View.requestLayout (View.java:27612)
android.widget.TextView.onConfigurationChanged (TextView.java:4706)
android.view.View.dispatchConfigurationChanged (View.java:16145)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewGroup.dispatchConfigurationChanged (ViewGroup.java:1654)
android.view.ViewRootImpl.updateConfiguration (ViewRootImpl.java:6502)
android.app.ActivityThread.handleActivityConfigurationChanged (ActivityThread.java:6941)
android.app.ActivityThread$ActivityClientRecord$1.onConfigurationChanged (ActivityThread.java:797)
android.view.ViewRootImpl.performConfigurationChange (ViewRootImpl.java:6462)
android.view.ViewRootImpl.handleResized (ViewRootImpl.java:2424)
android.view.ViewRootImpl.-$$Nest$mhandleResized
android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl (ViewRootImpl.java:6728)
android.view.ViewRootImpl$ViewRootHandler.handleMessage (ViewRootImpl.java:6697)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:226)
android.os.Looper.loop (Looper.java:313)
android.app.ActivityThread.main (ActivityThread.java:8757)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:571)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)

感谢您的评论,我添加了一个调用相机的代码片段。 我没有包含onActivityResult部分,因为在这个方法之前应用程序会崩溃。 - Tako
你的代码中是否使用了new Runnable()或者线程执行的run(){}?你遇到的错误几乎总是由于没有正确使用runOnUiThread(new Runnable(){}引起的。 - G. Putnam
是的,我的代码中有Runnables(用于在主线程之外执行的任务),但没有任何与UI相关的内容。 此外,如果应用程序在我的某个Runnable中崩溃,它应该会显示在堆栈跟踪中,不是吗? - Tako
添加代码片段以更好地说明。 - Ali Azaz Alam
“onResume在崩溃之前没有被调用” onResume()方法会在onActivityResult()方法之后被调用,但不一定是立即调用。 - Vivek Gupta
显示剩余6条评论
5个回答

1

是的,这只发生在三星设备上。我曾经遇到过这个问题。

以下是拍照时发生的情况:

  1. 打开相机意图以拍摄照片
  2. 拍照
  3. 返回您的活动或片段
  4. 获取捕获的图像并使用它

这是使用设备相机捕获图像的常规生命周期。

现在,运行在Android 13上的三星设备(不知道低版本如何,因为我没有低版本的设备)存在一个问题,即如果您在纵向模式下使用相机意图拍照,然后返回到横向模式下的活动或片段,则会破坏先前的对象配置更改。

即使您在纵向模式下捕获图片,然后将设备旋转到横向模式,再将其旋转回纵向模式并返回到活动中,也会发生这种情况,因为如上所述的配置更改。

因此,这是我的解决方案,它可以正常工作。我现在将其应用于所有包含此类功能的应用程序中。

1. 使用传统方法在清单文件中修复活动方向,根据需要提供固定的屏幕方向,可以是portraitlandscape

<activity
    android:name=".activity.MainActivity"
    android:screenOrientation="portrait" />

2. 如果您的屏幕具有响应式功能,可以在设备方向更改时既可以是纵向也可以是横向,则将此config添加到清单中的activity tag

<activity
    android:name=".activity.MainActivity"
    android:configChanges="orientation|screenSize"/>

现在,当你拍完照后返回到你的活动页面时,你之前创建的对象将不会被销毁。

编辑:

如果你已经在Manifest中设置了配置,那么你可以像我在一些应用程序中所做的那样向前迈进一步。

当用户启动相机时,通过编程将屏幕方向设置为portrait

setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

因此,当用户返回到活动时,请通过设置已经设置的方向来释放肖像锁定。

因此,在相机屏幕上不会旋转您的设备方向。


1
我很高兴听到有人在三星设备上遇到了类似的崩溃问题,现在我感觉不那么疯狂了,哈哈。 但是,如果问题是由操作系统声明的对象引起的,你会期望看到NullReferenceException或其他什么吗? 我知道我的应用程序能够很好地处理MainActivity在相机打开时被销毁的情况。这种情况经常发生。 此外,我已经为我的活动设置了configChanges="orientation|screenSize"。 - Tako
@Tako 你好,请查看我的编辑答案。如果之前的解决方案不起作用,还需要再进行一步操作。由于OEM处理相机捕获意图时存在一些问题,除了尝试不同的解决方案并找到完美的解决方案外,我们无法做任何事情,哈哈。 - Dev4Life
这是一个有趣的建议。我会尝试一下并回报! - Tako
@Tako 好的,没问题。如果有问题请告诉我。 - Dev4Life

0
通常,这种错误发生在后台任务(如网络处理)中,因此它们使用不同的线程。为了解决这个问题,请使用Activity.runOnUiThread方法从视图原始活动中调用。

谢谢您的回答,但是在哪里呢?在相机关闭之后和崩溃之前,我的代码都没有被调用。活动甚至还没有恢复。 - Tako
你需要修复onActivityResult方法的实现。为了进行快速测试,请移除该方法并进行测试,如果没有问题,请使用Activity.runOnUiThread实现你的代码。 - utrucceh
onActivityResult在应用程序崩溃之前没有被执行。此外,onActivityResult不是在主线程上调用的吗? - Tako

0
请使用以下代码,它应该可以正常工作。不过,startActivityForResult() 已经被弃用了。
ActivityResultLauncher<Intent> cameraActivityResultLauncher;

cameraActivityResultLauncher = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        // There are no request codes
                        Intent data = result.getData();
                        Bitmap img = (Bitmap)data.getExtras().get("data");
                        imageView.setImageBitmap(img);
                    }
                }
            });

Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraActivityResultLauncher.launch(cameraIntent);

有趣的事情。 我想启动ActivityResultLaunchers,但它们只在AppCompatActivity中实现,而我的应用程序使用普通的Activity。我想我应该迁移到AppCompat。不过,我想知道你是否能理性地解释为什么这与线程问题有关?还是只是一种直觉? - Tako

0
您可以尝试调试。由于某种原因,ViewRootImpl实例中的mThread字段与通知配置更改的线程不同。以下是引发异常的方法:
void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

可能是该字段被设置为 null 或更改了,或者线程确实发生了变化。也许三星已经修改了该类。

另外尝试在不离开应用程序的情况下触发配置更改,看看会发生什么。


感谢您的建议。 我无法直接调试,因为我的设备上从未崩溃,只有在用户设备上(叹气)才会崩溃。 我能做的就是添加日志,重新部署到Play商店,并在应用程序崩溃时查看新日志。 这就是我发现onConfigurationChanged是在创建视图的同一线程上调用的方式。不幸的是,我不能使用此方法添加日志以检查线程(checkThread())。 应用程序在前台时进行的配置更改不会造成任何问题。 - Tako
@Tako 不用谢。首先,你应该尽力复制崩溃,这几乎是必须的,这样你才能理解它,然后验证你的修复。你说还有其他类似于这个的堆栈跟踪。你能分享一下吗?也许我们可以从中获得更多信息。你在活动或其他类中保留了任何视图的引用吗?这可能是由内存泄漏引起的。这只是一个猜测。 - Isidro Rodriguez
相信我,我已经尝试了很多个小时来复制它,但都没有成功。只有在我绝望的时候才会在SO上询问问题。我添加了另外两个堆栈跟踪变量。我将视图的引用保留在Activity内部,但不在Activity之外的任何地方,这不应该会引起任何问题,对吧? - Tako

0
如果您需要对图像和相机进行完全控制,请遵循官方的Android文档:https://developer.android.com/training/camerax/take-photo。否则,请按照以下步骤操作:
1- 请求权限
  private val requestSinglePermissionLauncher = 
 registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
    // permission granted , ok
} else {
    // Permission Denied , follow google recommendation for this cases...
  }
}

2 - 代码调用应该是这样的:

 requestSinglePermissionLauncher.launch(Manifest.permission.CAMERA)

3- 使用预先构建的“TakePicture”合约:

private val getCameraImage =  
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
    //get image uri : $uri
  }
}

完整代码:

 private val getCameraImage = 
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
    //get image uri : $uri
  }
}    
private val requestSinglePermissionLauncher = 
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
    // you should provide an uri : where to store your image
     getCameraImage.launch(uri)
} else {
    // Permission Denied , follow google recommendation for this cases...
  }
}
// some where in code
requestSinglePermissionLauncher.launch(Manifest.permission.CAMERA) 

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