因项目需求,需要在Unity打包到移动端的APP上做一个拍照/录屏分享的功能。由于第一次尝试开发公共的unity3d native unmanaged plugin,过程中还是踩了不少的坑,在此总结一下。

1.拍照分享

首先想到的是使用第三方SDK来接入社交分享功能,网上查了一下发现有Mob、友盟还有一些国外的开源SDK,调研了一番,选择Mob的SDK来开发拍照分享的功能。

首先在Unity中写一段脚本用于保存当前屏幕像素,并保存为图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IEnumerator CaptureScreenshot()
{
switchBtn.SetActive(false);
takePhotoBtn.SetActive(false);
yield return new WaitForSeconds(0.1f);
int width = Screen.width;
int height = Screen.height;
screenShotTexture = new Texture2D(width, height, TextureFormat.RGB24, false);
screenShotTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
screenShotTexture.Apply();
byte[] bytes = screenShotTexture.EncodeToPNG();
File.WriteAllBytes(screenShotPath, bytes);
yield return new WaitForSeconds(0.1f);
shareBtn.SetActive(true);
cancelShareBtn.SetActive(true);
screenShotImage.SetActive(true);
screenShotImage.GetComponent<RawImage>().texture = screenShotTexture;
}

这里要注意的是整个过程需要在协程中做,首先隐藏所有UI组件,在下一帧中执行保存像素的逻辑,在下一帧显示UI组件。 协程中使用WaitForSeconds以保证拍照所得的图片中没有UI组件。

http://wiki.mob.com/Unity3D%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E6%8C%87%E5%8D%97/
下一步按照Mob的文档,导入ShareSDK组件,将刚刚保存的图片分享出去,这一步不需要多说,值得注意的就是需要在各家社交平台的开放平台上去申请id key。

各家社交平台的开放平台地址汇总,可以选择需要分享到的平台来申请。

2.录制视频分享

录制视频分享比较麻烦,直接用SDK分享的话,分享出去的是一个包含视频的网页链接,视频需要上传到Mob的云平台去重新编码,这个速度会比较慢,而且画质有损失,Mob的平台不稳定的时候,分享出去的视频还会无法观看。

体验了AR相机等带有视频录制分享的应用后,发现这些应用基本都是采用的Native API的发送文件来分享,于是考虑录制的过程用ShareSDK,拿到本地的mp4文件路径之后,再自己写一个Android的Plugin来做分享的功能

2.1Android中调用Native 文件分享API

在Android Studio中新建一个工程,创建一个Java Library模块

核心代码如下:

1
2
3
4
5
6
7
8
9
10
public void ShareFile(String filePath){
Intent shareIntent = new Intent(Intent.ACTION_SEND);
File file = new File(filePath);
Uri uri = Uri.fromFile(file);
startShare(context, shareIntent, uri);
String fileType = context.getContentResolver().getType(uri);
shareIntent.setType(fileType);
shareIntent.putExtra(Intent.EXTRA_STREAM,uri);
UnityPlayer.currentActivity.startActivity(shareIntent);
}

为了在Android中以插件的方式调用,需要让类继承Unity的UnityPlayerActivity。

从Unity的安装目录Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes下拷贝出Unity的Android class jar包,classes.jar(Unity 5.5.1)

在工程的libs中导入classes.jar,类中继承Unity的UnityPlayerActivity。

1
2
3
4
5
6
7
8
public class TarShareFile extends UnityPlayerActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void ShareFile(String filePath){
}
}

将上面的代码打包为jar包

2.2Unity 调用Android Unmanaged Plugin

将jar包拷贝到Untiy工程的 Assets/Plugins/Android目录下

Unity代码中:

1
2
3
AndroidJavaObject tarShareFile = new AndroidJavaObject("com.tx.tar.TarShareFile");
String videoPath = GetVideoPath();
tarShareFile.Call("ShareFile", videoPath);

即可打开Android Native的分享面板,向支持文件分享的应用发送文件了

2.3Android 7.0适配

然而在Android 7.0下事情并不会那么简单。

为了提高私有文件的安全性,面向 Android 7.0 或更高版本的应用私有目录被限制访问,这就意味着直接通过Uri.fromFile(file)来访问文件不可行了。上面的代码在Android7.0下会报 FileUriExposedException,下面是Android7.0下的适配方法

2.3.1 在AndroidManifext.xml下加入Provider配置

1
2
3
4
5
6
7
8
9
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>

2.3.2 在res/xml下新建provider_paths.xml文件(与上面android:resource中配置的名称相同)

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>

2.3.3拷贝support-core-utils-24.2.0.aar

为了访问在Unity中访问FileProvider类,需要在Android SDK中的support-core-utils-24.2.0.aar包拷贝至Unity工程的/Assets/Plugins/Android目录

3.3.4 适配Android的分享API

Java代码中,文件访问的方式修改为

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
public void ShareFile(String filePath, Context context){
Intent shareIntent = new Intent(Intent.ACTION_SEND);
File file = new File(filePath);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String providerName = context.getPackageName() + ".fileProvider";
uri = FileProvider.getUriForFile(context,providerName , file);
}
else{
uri = Uri.fromFile(file);
}
startShare(context, shareIntent, uri);
String fileType = context.getContentResolver().getType(uri);
shareIntent.setType(fileType);
shareIntent.putExtra(Intent.EXTRA_STREAM,uri);
UnityPlayer.currentActivity.startActivity(shareIntent);
}
```
注意provider的名称需要和AndroidManifest中的`android:authorities`属性的配置一致
由于在Unity中是直接new出来的java类对象,在类中没有办法获取应用的context,所以需要在Unity代码中获取应用context,并传递给jar包中的方法。
```csharp
AndroidJavaClass unity = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject currentActivity = unity.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject context = currentActivity.Call<AndroidJavaObject>("getApplicationContext");
AndroidJavaObject tarShareFile = new AndroidJavaObject("com.tx.tar.TarShareFile");
String videoPath = GetVideoPath();
tarShareFile.Call("ShareFile", videoPath, context);

上面的工作完成后,重新打包jar包,拷贝至Unity工程的/Assets/Plugins/Android目录,并将jar包工程下的res目录、AndroidManifest.xml文件都拷贝至/Assets/Plugins/Android目录,重新打包Unity应用,发现在Android7.0下已经可以调起分享面板了。

3.3.5 让目标应用认识content://xxx/xxx 虚拟目录

上面的步骤之后,已经可以在Android7.0下走完分享流程了,但是测试一下分享到QQ,会发现QQ提示“找不到文件”

将文件路径打出来,Uri.From的方式获取的路径是file://storage/emulated/0/包名/xxx/xxx.mp4这样的绝对路径,而FileProvider访问文件,则会像一些Web服务器一样,用一个虚拟目录的路径来访问文件,刚刚的绝对路径,FileProvider读取之后,则是content://provider_paths.xml中配置的路径名称/包名/xxx/xxx.mp4

这个视频文件路径在Android中可以被本应用的Intent对象识别,但是无法被其他应用识别,所以在手Q中测试还是提示文件不存在。

解决方案有两种,第一种是在Application的OnCreate中添加下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.N) {
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
}
```
第二种方式是先将视频文件用MediaScanner扫描一遍,得到一个新的虚拟目录路径,这个路径就可以被其他应用识别了
```java
Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
scanIntent.setData(uri);
context.sendBroadcast(scanIntent);
MediaScannerConnection.scanFile(context,new String[] {filePath},null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
//在这里用新的Uri来发送文件
}
});

由于需要在Unity3D中以Unmanaged Plugin的方式来调用这段代码,不方便在Application的OnCreate中注入逻辑,所以我采用了第二种方案。

2.4更好的插件库文件组织方式

经过前面的步骤这个社交分享的插件已经可以使用了。但是要将整个项目打包成一个unitypackage作为一个公共插件,还会有一点问题: plugins目录下会有一堆本插件依赖的库和代码、配置文件,其他用户导入package之后,很容易和用户之前的资源其冲突。

所以考虑将本插件所有的代码和库、配置文件都放在一个plugis目录下一个以插件名字命名的目录里,然而这样做打包运行之后,发现报异常ClassNotFound,说明我们的jar包是必须放在Assets/Plugins/Andoird根目录下的。

如何解决这个问题呢?

参考一下Unity3D Building and using plug-ins for Android 文档 https://docs.unity3d.com/Manual/PluginsForAndroid.html

发现Unity调用android native代码的方式有4种:

  • jar包调用
  • aar调用
  • Android Libraries工程调用
  • .so动态库调用

而此插件需要的调用方式特性是:

  • 可以用自己的目录组织文件
  • 需要包含能被Unity在Android平台打包时识别的AndroidManifest.xml文件和res目录

阅读文档并对比几种调用方式后,发现只有Android Libraries工程调用是合适需求的。

/Assets/Plugins/Android目录下新建以插件名字命名的目录,在此目录中创建子目录存放jar包模块的工程

其中插件jar包编译好之后,放到libs中去,project.properties文件是一个关键的文件,其中的内容:

target=android-8
android.library=true

有了这个文件在目录下,Unity就会认为这是一个Android Library项目,在打包APK的时候将其中的库和配置文件考虑进来。

至此,分享插件开发完毕。

后续再一套分享的交互组件,放到一个GameObject然后保存为一个预制件, 再将Unity工程导出为一个unitypackage,其他用户直接导入此package,拖入预制件即可一秒钟接入社交分享组件!

☞ 参与评论