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

这两天在对接网易云信的IM和实时音视频,为了速度所以使用了网易提供的IM界面库UIKit,对接一切都比较顺利,过程中遇到了一个发图片的问题(java.io.IOException: open failed: EACCES (Permission denied))

网易云信uikit–java.io.IOException 网易云信uikit–java.io.IOException

我们来看看到底是怎么回事?

查找问题的根源

图片发送失败看到手机界面弹出一个toast提示“获取图片出错”,全局搜索了一下有好几个地方,最后通过打log定位到一个地方。

网易云信uikit–错误定位 网易云信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()打印结果 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"重新运行项目,果然问题得到了解决。