文章目录

前几天处理一个很奇葩的问题,解决过程可谓一波三折啊,处理起来却很有意思,这里记录一下。

事情的起因是同事写了一个在android系统调用系统相机的拍照的功能,将路径通过Intent传过去,照片拍好后保存到路径,通过路径取这张照片。

突然有一天点这拍照功能无效了,直接crash。一看异常日志,发现抛出的是ActivityNotFoundException。第一反应是以为是Activity没有注册或者打包出现问题了,于是把Apk反编译,在AndroidManifest.xml中检查了下注册,在dex文件中也找了下相关的类,结果发现都没有问题。

再细看日志,发现和项目中的插件框有关,报错是跟插件框架中的一个启动Activity的方法有关。因为Hook了系统的Instrumention,所以执行启动Activity时,是由框架中这个Hook来接管Activity的启动。于是找到源码,看了下代码实现,部分代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Method method = null;
try {
Class<Instrumentation> cl = (Class<Instrumentation>) this.getClass()
.getClassLoader().loadClass("android.app.Instrumentation");
method = cl.getDeclaredMethod("execStartActivity", new Class[] {
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, Integer.TYPE, android.os.Bundle.class });
method.setAccessible(true);
ActivityResult r = (ActivityResult) method.invoke(
originalInstrumentation, new Object[] { who, contextThread, token,
target, intent, requestCode, aBundle });
return r;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
throw new ActivityNotFoundException();
} catch (Exception e) {
e.printStackTrace();
}

异常是由throw new ActivityNotFoundException();这行代码抛出来的,抛出的原因是执行method.invoke()方法失败了。抱着怀疑的态度验证了下,在抛出异常之前加了LOG输出,果然有LOG输出,确认是这里出现了问题。没有弄明白写这个框架的人为什么要使用这样的障眼法不让InvocationTargetException的异常信息直接输出,而是抛出一个ActivityNotFoundException

最后网上查了下这个异常类,发现这个异常类是一个包装的异常类,通过查看其源码也可以看得到,其内部有一个真实的异常。这也不难理解,因为执行反射的方法,可能会抛出各种异常。按照异常尽可能具体的原则,不能直接抛出一个Exception了事,所以就有了这个包装一个异常的InvocationTargetException类。要输出这个真实的异常,可以直接调用该类的getTargetException().printStackTrace();即可输出真实的异常信息。

输出的异常信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
android.os.FileUriExposedException: file:///sdcard/tmp exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:845)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8957)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8942)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1519)
at java.lang.reflect.Method.invoke(Native Method)
at android.view.View.performClick(View.java:5639)
at android.view.View$PerformClick.run(View.java:22446)
at android.os.Handler.handleCallback(Handler.java:754)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:160)
at android.app.ActivityThread.main(ActivityThread.java:6252)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:895)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:785)
java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at android.view.View.performClick(View.java:5639)
at android.view.View$PerformClick.run(View.java:22446)
at android.os.Handler.handleCallback(Handler.java:754)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:160)
at android.app.ActivityThread.main(ActivityThread.java:6252)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:895)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:785)
Caused by: android.os.FileUriExposedException: file:///sdcard/tmp exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:845)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8957)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8942)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1519)
... 20 more

FileUriExposedException这个异常还是头一次遇到,于是又查了下,在stackoverflow上看到别个的回答:https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed。
遇到这个问题的人还真不少,出现这样的问题是由于android 7.0的系统机制问题,为了安全起见,一个App要访问另外一个App的外部存储目录,需要被访问的App来授权读写权限,所以需要进行版本适配。

在定位问题的时候中间发生一个插曲,写拍照功能的同事用他写的库在demo里面了跑了一下,发现正常,没有任何异常出现。而在项目的App中却出现了问题,所以就说不是他的问题,怀疑是插件框架的问题。因为是我接手了插件框架,所以就开始排查这个问题。

stackoverflow上面有人回答也说了,如果把编译的targetVersion改成24或以上,遇到这种场景就会报错,也给了解决方法,官方的说明和解决方案参照这里:https://developer.android.google.cn/reference/android/support/v4/content/FileProvider.html

找到原因后,叫写这个拍照功能的同事把targetVersion修改一下,果然,demo中问题就出现了。

中间还发生一个插曲,传的路径是使用Uri传过去的,如果使用Uri.parse(String)这个方法将路径字符串直接传过去就不会报错;如果使用Uri.parseFile(File)就会出现crash,android 7.0系统的安全机制是识别file://这样的schema,而对普通的路径直接放行。检查的系统源码如下所示:

  • android.net.Uri.java
1
2
3
4
5
6
7
8
9
10
11
 /**
* If this is a {@code file://} Uri, it will be reported to
* {@link StrictMode}.
*
* @hide
*/
public void checkFileUriExposed(String location) {
if ("file".equals(getScheme()) && !getPath().startsWith("/system/")) {
StrictMode.onFileUriExposed(this, location);
}
}

以上。

文章目录