这两天在对接网易云信的IM和实时音视频,为了速度所以使用了网易提供的IM界面库UIKit,对接一切都比较顺利,过程中遇到了一个发图片的问题(java.io.IOException: open failed: EACCES (Permission denied))
网易云信uikit–java.io.IOException
我们来看看到底是怎么回事?
查找问题的根源
图片发送失败看到手机界面弹出一个toast提示“获取图片出错”,全局搜索了一下有好几个地方,最后通过打log定位到一个地方。
网易云信uikit–错误定位
注意看这一行代码
1
| imageFile = ImageUtil.getScaledImageFileWithMD5(imageFile, extension);
|
这里的imageFile == null 所以我们具体看看getScaledImageFileWithMD5
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
| public static File getScaledImageFileWithMD5(File imageFile, String mimeType) {
String filePath = imageFile.getPath();
if (!isInvalidPictureFile(mimeType)) {
LogUtil.i("ImageUtil", "is invalid picture file");
return null;
}
String tempFilePath = getTempFilePath(FileUtil.getExtensionName(filePath));
File tempImageFile = AttachmentStore.create(tempFilePath);
if (tempImageFile == null) {
return null;
}
CompressFormat compressFormat = CompressFormat.JPEG;
// 压缩数值由第三方开发者自行决定
int maxWidth = 720;
int quality = 60;
if (ImageUtil.scaleImage(imageFile, tempImageFile, maxWidth, compressFormat, quality)) {
return tempImageFile;
} else {
return null;
}
}
|
下断点结果发现AttachmentStore.create(tempFilePath)返回的File对象是null,最后问题定位到这里
1
2
3
4
5
6
7
8
9
| try {
f.createNewFile();
return f;
} catch (IOException e) {
if (f != null && f.exists()) {
f.delete();
}
return null;
}
|
果然没有猜错,这里应该是抛出了异常,随后我就弱弱的加了一句e.printStackTrace()然后重现了一下,问题的根源找到了。
e.printStackTrace()打印结果
看到是这个原因,当时我就大吃一惊,怎么会没有权限呢?
为什么会没有权限
这个结果我即喜又悲,因为我知道如果是权限问题那就很容易解决了,但是此时比较尴尬的是我知道所有权限有已经声明了啊,这又是为什么呢?
然后我就顺着如下思路分析
- 是不是只有在7.0手机上有问题,只是7.0的权限问题。
这个问题很好验证,我找了一个6.0的手机也存在同样的问题。
这个我当时很怀疑,但是我特意将 targetSdkVersion设置为22的,里面完全没有使用6.0的动态权限。后来我找了一个5.0的手机也有同样的问题。
额~~这个后来验证完全是瞎想的,没有理论基础,我在uikit中的AndroidManifest.xml中添加权限当然无济于事。
我尝试用我自己写的根路径换上去结果大出意料,竟然可以!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| /**
* 获取文件存储根目录
* @return
*/
public static String getFilesDirPath() {
String filePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
filePath = mApplication.getExternalFilesDir(null).getAbsolutePath();
} else {
filePath = mApplication.getFilesDir().getAbsolutePath();
}
return filePath;
}
|
我猜想可能是这个路径的权限问题,然后找到获取路径的代码,看看究竟是为什么
1
2
3
4
5
6
| private static String getTempFilePath(String extension) {
return StorageUtil.getWritePath(
NimUIKit.getContext(),
"temp_image_" + StringUtil.get36UUID() + "." + extension,
StorageType.TYPE_TEMP);
}
|
连续进入几层方法后看到了这样几行代码
1
2
3
4
5
6
7
8
9
| /**
* 返回指定类型的文件夹路径
*
* @param fileType
* @return
*/
public String getDirectoryByDirType(StorageType fileType) {
return sdkStorageRoot + fileType.getStoragePath();
}
|
额~我们来看看这个sdkStorageRoot是如何被初始化的
1
2
3
4
| private void loadStorageState(Context context) {
String externalPath = Environment.getExternalStorageDirectory().getPath();
this.sdkStorageRoot = externalPath + "/" + context.getPackageName() + "/";
}
|
注意知识点来了:
我们又可以将文件存储的路径分为两大类,一类是路径中含有包名的,一类是路径中不含有包名的,含有包名的路径,因为和某个App有关,所以对这些文件夹的访问都是调用Context里边的方法,而不含有包名的路径,和某一个App无关,我们可以通过Environment中的方法来访问。
通过Enviroment获取根目录需要两个权限
1
2
| <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
|
在6.0不仅要在mainfest中加上上面两个权限,还要在Activity中加上权限,调用这个方法即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
public static void verifyStoragePermissions(Activity activity) {
// Check if we have write permission
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
}
}
|
此时我们回到我们上面说使用Context获取根目录已经验证路径是可以被创建的,果然在init方法中对sdkStorageRoot进行了初始化
1
| StorageUtil.init(context, options.appCacheDir);
|
所以,我们要想使用Context获取的根目录只需要将NimUIKit.init(context)替换为下面代码
1
2
3
| UIKitOptions options = new UIKitOptions();
options.appCacheDir = CommonConfig.getCacheDirPath();
NimUIKit.init(context, options);
|
真相浮出水面
上面分析了那么多到底是为什么呢?我们虽然找到了可以让正常上传图片的方式,但没有找到问题的根本原因,不过现在我们可以肯定的说这是权限导致的问题。
接下来我就想是不是某个module的权限覆盖了sd卡读写权限呢?
果然,在一个module中我发现了这么一个权限声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| <manifest package="ezy.boost.update"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" />
<application>
<provider
android:name="ezy.boost.update.UpdateFileProvider"
android:authorities="${applicationId}.updatefileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_cache_path"/>
</provider>
</application>
</manifest>
|
注意uses-permission后面的android:maxSdkVersion=“18”, 我们的targetSdk是22,这就是说这个权限声明此时不生效,去掉后面的android:maxSdkVersion=“18"重新运行项目,果然问题得到了解决。