对接网易云信UIKit遇到的那些坑

这两天在对接网易云信的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的手机也存在同样的问题。

  • 是不是6.0权限问题?

这个我当时很怀疑,但是我特意将 targetSdkVersion设置为22的,里面完全没有使用6.0的动态权限。后来我找了一个5.0的手机也有同样的问题。

  • 是不是引入的module要额外再声明一次权限。

额~~这个后来验证完全是瞎想的,没有理论基础,我在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”重新运行项目,果然问题得到了解决。