作者:Poan,腾讯移动客户端开发 工程师
商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。
WeTest 导读
本文将会从大家熟悉的Unity为出发点来介绍如何将自己写的或者第三方的Android插件集成到自己的游戏中。
1. Unity是怎么打包APK文件的?
2. 安装及配置Android Studio
3. Android开发基础以及导入到Unity
一、Unity是怎么打包APK文件的?
大家看过一些第三方组件的接入文档都知道,在Unity里面有几个特殊的文件夹是跟打包APK有关的。首先我们就来了解一下,这些文件夹里面的内容是经历了哪些操作才被放到APK里面的呢?
在Unity的Assets目录下,Plugins/Android无疑是其中的重中之重,首先我们先来看一个常见的Plugins/Android目录是什么样子的。
后面的四个是Android工程的文件。前面两个文件夹是我们引用的第三方库,他们也会被打包到APK中。我们这个时候如果点进去前两个文件夹,我们会发现他们的目录结构跟Android这个目录也很像,大概是一下这个样子的。
比较上下两层的目录接口我们可以发现有很多相似的部分,如:libs、res、assets文件夹以及AndroidManifest.xml文件。这些其实都是一个标准的Android项目的所需要的文件。Unity自带的Android打包工具的作用就是把上述这几个文件夹里面的内容以固定的方式组织起来压缩到APK文件里面。
接下来我们分别来看看Android打包工具都会做什么样的操作。
● libs文件夹里面有很多*.jar文件,以及被放在固定名字的文件夹里面的*.so文件。*.jar文件是Java编译器把.java代码编译后的文件,Android在打包的时候会把项目里面的所有jar文件进行一次合并、压缩、重新编译变成classes.dex文件被放在APK根目录下。当应用被执行的时候Android系统内的Java虚拟机(Dalvik或者Art),会去解读classes.dex里面的字节码并且执行。把众多jar包编译成classes.dex文件是打包Android应用不可或缺的一步。
看到这里有人可能会想不对啊,这一步只将jar包打成dex文件,那之前的java文件生成jar文件难道不是在这一步做吗?没错,这里用的jar包一般是由其他Android的IDE生成完成后再拷贝过来的。本文后面的部分会涉及到怎么使用Android的IDE并且生成必要的文件。
● libs文件夹的*.so文件则是可以动态的被Android系统加载的库文件,一般是由C/C++撰写而成然后编译成的二进制文件。要注意的是,由于实际执行这些二进制库的CPU的架构不一样,所以同样的C\C++代码一般会针对不同的CPU架构生成几分不同的文件。这就是为什么libs文件夹里面通常都有armeabi-v7a、armeabi、x86等几个固定的文件夹,而且里面的.so文件也都是有相同的命名方式。Java虚拟机在加载这些动态库的时候会根据当前CPU的架构来选择对应的so文件。有时候这些so文件是可以在不同的CPU架构上执行的,只是在不对应的架构上执行速度会慢一些,所以当追求速度的时候可以给针对每个架构输出对应的so文件,当追求包体大小的时候输出一个armeabi的so文件就可以了。
● assets文件夹,这个里面的东西最简单了,在打包APK的时候,这些文件里面的内容会被原封不动的被拷贝到APK根目录下的assets文件夹。这个文件夹有几个特性。
√ 里面的文件基本不会被Android的打包工具修改,应用里面要用的时候可以读出来。
√ 打出包以后,这个文件夹是只读的,不能修改。
√ 读取这个文件夹里面的内容的时候要通过特定的Android API来读取,参考getAssets()。
√ 基于上述两点,在Unity中,要读取这部分内容要通过WWW来进行加载。
除了Plugins/Android内的所有assets文件夹里面的文件会连同StreamingAssets目录下的文件一起被放到APK根目录下的assets文件夹。
● res文件夹里面一般放的是xml文件以及一些图片素材文件。xml文件一般来说有以下几种:
√ 布局文件,被放在res中以layout开头的文件夹中,文件里描述的一般都是原生界面的布局信息。由于Unity游戏的显示是直接通过GL指令来完成的,所以我们一般不会涉及到这些文件。
√ 字符串定义文件,一般被放到values文件夹下,这个里面可以定义一些字符串在里面,方便程序做国际
化还有本地化用。当然有时候被放到里面的还有其他xml会引用到的字符串,一般常见的是app的名称。
√ 动画文件,一般定义的是Android原生界面元素的动画,对于Unity游戏,我们一般也不会涉及他。
√ 图片资源,一般放在以drawable为开头的文件夹内。这些文件夹的后缀一般会根据手机的像素密度来来进行区分,这样我们可以往这些文件夹内放入对应像素密度的图片资源。
例如后缀为ldpi的drawable文件夹里面的图片的尺寸一般来说会是整个系列里面最小的,因为这个文件夹的内容会被放到像素密度最低的那些手机上运行。而一般1080p或者2k甚至4k的手机在读取图片的时候会从后缀为xxxxhdpi的文件夹里面去读,这样才可以保证应用内的图像清晰。图片资源在打包过程中会被放到APK的res文件夹内的对应目录。
√ Android还有其他一些常见的xml文件,这里就不一一列举了。
res文件夹下的xml文件在被打包的时候会被转换成一种读取效率更高的一种特殊格式(也是二进制的格式),命名的时候还是以xml为结尾被放到APK包里面的res文件夹下,其目录结构会跟打包之前的目录结构相对应。
除了转换xml之外,Android的打包工具还会把res文件夹下的资源文件跟代码静态引用到的资源文件的映射给建立起来,放到APK根目录的resources.arsc文件。这一步可以确保安卓应用启动的时候可以加载出正确的界面,是打包Android应用不可或缺的一步。
● AndroidManifest.xml,这份文件太重要了,这是一份给Android系统读取的指引,在Android系统安装、启动应用的时候,他会首先来读取这个文件的内容,分析出这个应用分别使用了那些基本的元素,以及应该从classes.dex文件内读取哪一段代码来使用又或者是应该往桌面上放哪个图标,这个应用能不能被拿来debug等等。在后面的部分会有详细解释。打包工具在处理Unity项目里面的AndroidManifest文件时会将所有AndroidManifest文件的内容合并到一起,也就是说主项目引用到的库项目里面如果也有AndroidManifest文
件,都会被合并到一起。这样就不需要手动复制粘贴。需要说明的是,这份文件在打包Android程序的时候是必不可少的,但是在Unity打包的时候,他会先检查Plugins目录下有没有这份文件,如果没有就会用一个自带的AndroidManifest来代替。此外,Unity还会自动检查项目中AndroidManifest里面的某些信息是不是默认值,如果是的话,会拿Unity项目中的值来进行替换。例如,游戏的App名称以及图标等。
● project.properties,这份文件一般只有在库项目里面能看得到,里面的内容极少,就只有一句话android.library=true。但是少了这份文件Android的打包工具就不会认为这个文件夹里面是个Android的库项目,从而在打包的时候整个文件夹会被忽略。这有时候不会影响到打包的流程,打包过程中也不会报错,但是打出的APK包缺少资源或者代码,一跑就崩溃。关于这份文件,其实在Unity的官方文档上并没有详细的描述(因为他实际上是Android项目的基础知识),导致很多刚刚接触Unity-Android开发的开发者在这里栽坑。曾
经有个很早就开始用Unity做Android游戏的老前辈告诉我要搞定Unity中的Android库依赖的做法是用Eclipse打开Plugins/Android文件夹,把里面的所有的项目依赖处理好就行了。殊不知这样将Unity项目跟Eclipse项目耦合在一起的做法是不太合理的,会造成Unity项目开启的时候缓慢。
● 其他文件夹例如aidl以及jni在Unity生成APK这一步一般不会涉及到,这里不展开。
看到了上述介绍的Unity打包APK的基础知识我们知道了往Plugins/Android目录下放什么样的文件会对APK包产生什么样的影响。但是实际上上述的内容只是着重的讲了Unity是怎么打包APK,所以接下来会简述一下打包这个步骤到底是怎么完成的。
Android提供了一个叫做aapt的工具,这个工具的全称是Android Asset Packaging Tool,这个工具完成了上述大部分的对资源文件处理的工作,而Unity则是通过对Android提供的工具链(Android Build Tools)的一系列调用从而完成打包APK的操作。这里感觉有点像我们写了个bat/bash脚本,这个脚本按照顺序调用Android提供的工具一样。在一些常见的Android IDE里面,这样的“bat/bash脚本”往往是一个完整的构建系统。最早的Android IDE是Eclipse,他的构建系统是Ant,是基于XML配置的构建系统。后来Android团队推出了Android专用的IDE——Android Studio(这个在文章后面会有详述),他的构建系统则是换成了gradle,从基于xml的配置一下子升级到了语言(DSL, Domain Specific Language)的层级,给使用Android Studio的人带来更多的弹性。
写到这里我想很多人都清楚了要怎么把Android的SDK/插件放到Unity里面并且打包到Unity里面。这时候应该有人会说,光会放这些文件不够啊,我还需要知道自己怎么写Android的代码并且输出相应的SDK/插件给Unity使用啊。
本文接下来的内容将会一步一步描述怎么写Android代码并且输出库文件给Unity。
二、Android开发基础以及导入到Unity
(一)开始你的第一个Android程序
安装完Android Studio并且配置好代理以后我们就可以打开它,在弹出的框中选择“Start a new Android Studioproject”。
在接下来弹出的界面里面输入应用名称,公司域名(这个其实不怎么重要)以包名(Package Name),其中我认为最重要的是包名,毕竟看一个应用的包名可以看得出一个开发者的逼格如何。。。
接下来选择要开发什么类型的App,这里勾上Phone and Tablet就可以了。SDK的选择一般来说根据项目的需要,最低一般不低于API 9: Android 2.3(Gingerbread),这也是Unity能接受的最低SDK。如果有些插件不能运行在这么低的Android SDK环境下的话可以酌情考虑提升到API 15: Android 4.3(IceCreamSandwich),这个等级的API一般也是可以兼容绝大多数近3-4年的机器。
因为我们要输出的内容是给Unity用的,这里可以先选择不带有Activity(就是承载游戏画面的基础部件),后续用到再说。
点击OK以后Android Studio就会开始初始化当前的这个Android项目。初始化会需要一段时间,因为AndroidStudio有可能会去下载一些必要的框架或者更新Android工具的版本。初始化完成以后到左边按照图里面的步骤点开就可以看到整个项目目录树的情况。
通过上图我们可以知道,一个Android Studio的项目(Project)可以由许多小的模块(Module)组成,这些模块可以是带有Activity的应用类模块,也可以是不带有Activity的库模块等等。这些小的模块之间可以有引用关系。我们可以把一些完成基础功能或者容易被复用的模块单独拆出来。
如果要新建一个模块我们可以在上图的列表中点右键选择New Module,在弹出的界面中我们可以选择要新建什么样的模块,或者从Eclipse导入旧的项目也可以。一般来说给Unity游戏开发插件最常用的就是库模块(AndroidLibrary)。同样的,在接下来弹出的窗口中填写好模块名称、包名以及最低运行的SDK。
简单的看一下Android项目的目录结构。如下图所示:
● libs目录跟本文第一部分介绍的libs目录的功用是一样的,把依赖到的库放在这里面就可以了。
● src/main/res目录也是跟本文第一部分介绍的res目录的功能和结构是一样的,把对应资源放进去就可以了。
● 接下来是java代码所在的目录src/main/java,这个目录有点特殊,他的子路径跟java文件里面定义的包名(package name)要对应的上。
● AndroidManifest.xml也是跟第一部分介绍的AndroidManifest的功能是一样的。
● build文件夹是Android Studio动态生成的,打出的APK包(应用模块)或者AAR包(库模块)会被放到这里面的output文件夹。需要注意的是这个文件夹不应该被放提交到svn里面,要不然会造成项目成员之间的冲突,切记。
● src/test以及src/androidTest是做单元测试用的,本文不涉及。
至此,我们就可以开始动手写代码了,这里我们写一个可以弹出Android的Toast提示的Activity来替换掉Unity默认的Activity。
简述一下Unity跟Activity的关系:在Android系统中,打开一个应用,就是开启该应用指定的启动Activity。
Unity里面有个默认的Activity,他的作用就是在系统启动应用的时候加载Unity的Player,这个Player就是就相当于是Unity应用的“播放器”,他会执行我们在Unity项目中创作的内容,并且通过GL指令渲染到指定的SurfaceView中,而SurfaceView则是被置于Activity里面的一个特殊的View。
首先,我们在Android Studio中找到src/main/java(如上图所示),然后点击右键,选择新建Empty Activity。
在弹出的窗口中给你的Activity取个符合Java代码规范的名字,然后再想个合理的包名(当然,也可以直接用默认项目的包名也可以)。可以参考下图的配置:
其中的Generate Layout File,我们在制作给Unity游戏用的Activity是不需要勾上的。Launcher Activity勾上以后Android Studio会帮你在当前模块的AndroidManifest.xml中声明本Activity是应用的入口之一。作为一个库项目我们这边其实也不需要这个选项。点击Finish之后Android Studio就会帮我们在指定目录下创建一个很简单的Activity。里面的内容如下:
需要注意的是这只是一个最基础的Android Activity,他还不会去加载我们的Unity出来,所以我们要让他继承自Unity的Activity而不是默认的。为此,我们要先将Unity相应的jar包引入到我们的模块当中。首先找到Unity的安装目录,然后找到以下子目录Editor\Data\PlaybackEngines\AndroidPlayer\Variations\il2cpp\Release\Classes\里
面的classes.jar,这个就是被打包成jar包的Unity默认的Activity。我们把这个jar包复制到当前模块的libs目录下(可以把这个jar包改成你想要的名字,便于管理)。(这个jar包的源码在Editor\Data\PlaybackEngines\AndroidPlayer\Source\com\unity3d\player这个目录下。感兴趣的同学可以翻阅一下源码,就可以理解Unity播放器的加载机制。)
接下来,我们可以在Android Studio左边的Project View中找到当前的模块以后点击右键,选择“Open ModuleSetting”或者直接按F4。在弹出的窗口中我们选到最右边的页签“Dependencies”,然后选择右边绿色的加号-JarDependency。
从项目的libs文件夹中找到刚刚导入的jar包,点击OK即可。接下来有一个比较关键的步骤就是,我们改变这个jar包的scope属性,因为默认的scope属性(Compile)是会将该jar包里面的内容跟本模块里面Java代码合并到一起。这在之后Unity打包这个模块的jar包的时候会报错,因为Unity里面内置了刚刚这个jar包。所以我们可以参考下图把这个jar包的scope设置成provided。
然后删除上述列表的第一行,因为他会把所有libs文件夹下的jar包都打包到一起。跟我们刚刚做完的provided设置会有冲突。
搞定了这步骤以后我们就可以回到刚刚新创建出来的Activity把他的父类改成UnityPlayerActivity,同时别忘记引用一下相应的package,改完之后的代码是这样的:
到这一步,如果我们的Activity如果能被运行的话,他应该能够借助他的父类UnityPlayerActivity里面的代码来运行Unity。接下来,我们来给这个Activity添加一方法,当这个方法被调用的时候会展示一个系统默认的Toast提示。
看得出来,里面最核心的一个方法其实就只是调用Android里面的Toast组件而已,没啥好解释的。相反,是外面的runOnUiThread是值得大家注意的,在Android编程中,所有涉及到对UI的操作必须要放在UI线程里面来做,否则会造成其他线程修改UI线程里面的数据然后崩溃。由于我们写的这个ShowMessage方法最后会被Unity那边调用,而来自Unity的调用可能不是UI线程,所以我们要给他做适当的保护。
在Android中有很多种调度方法可以把某段代码放到UI线程里面来跑。上面这段代码的runOnUiThread的写法是最简便的一种写法。如果遇到比较复杂的逻辑可以考虑使用Messenger或者Handle来调度线程,感兴趣的同学可以上网查一下。
(二)导入到Unity并且编译
完成Activity的代码编写之后就可以输出这个模块到Unity项目中去。在Android Studio中选择Build - Make Project或者是在左边的项目视图中选中要导出的模块然后选择Build - Make Module。选择完了之后就可以看到下面有个Gradle的进度条,待进度条完成了以后我们就可以到该模块的build/outputs/aar目录下去找输出的文件。打开这个文件夹,可以看到有个*.aar的文件。这个就是该模块所编译出来的结果,如果你用解压缩软件去解压缩它,你会发现他几乎就是一个完整的Android工程。根据本文第一部分所说的内容,我们只要在Unity工程中的Plugins/Android目录下新建一个文件夹,然后把这个文件解压缩以后整个丢进去,再手写一个名字叫project.properties,内容是android.library=true的文件放到新建的文件夹里面就可以了。
胜利在望,我们接下来只要把Unity工程里面的AndroidManifest.xml文件的入口Activity从Unity默认的的改成我们刚刚写的这个就可以了。需要注意的是,如果是旧的Unity工程,可能已经有人写过相关的AndroidManifest文件放在了Plugins/Android目录下,但是如果是全新的Unity项目的话,就没有这份文件了。在打包的时候,如果Unity发现Plugins/Android目录下没有这份文件,他会复制一份默认的文件并且修改其中跟项目有关的内容。这里我们可以从Unity的安装目录的Editor\Data\PlaybackEngines\AndroidPlayer\Apk文件夹内找到AndroidManifest.xml这份文件,把它复制一份到Unity工程的Plugins/Android目录下。接下来就是修改里面的内容。
这里解释一下这份文件里面的一些关键内容。
● package="com.unity3d.player"这里的内容如果放着不动,打包的时候Unity会将其修改为Player Setting的Bundle Identifier。
● android:versionCode以及android:versionName这两部分的内容则在打包时会根据Player Setting里面的Version以及Bundle Version Code的内容来进行修改。
● android:icon以及android:label这两个对应的是应用的图标以及应用名称。如果不改的话,Unity也会自动根据Player Setting里面的内容来进行修改。
● android:debuggable="true"这个在打包的时候Unity也会自动根据Build Setting里面的Development Build选项自动进行修改。
● activity里面的android:name,这个name只的是该activity需要运行的哪个Java的Activity的类。如果不修改,加载的就是Unity默认Activity的类。这篇文章需要把默认的Activity改成刚刚我们的实现,所以,我们把刚刚写好的那个Activity的完整名称写上去(包括包名还有类名)。
● activity里面的android:label,这个是在桌面上图标下面写的那一行文字,也是应用的名称。不修改的话Unity会帮你维护。
● meta-data的这一行的name值是key,value值就是这个key对应的内容。meta-data可以根据需要自定义多个,但是key值不能重复,上面代码里面的unityplayer.UnityActivity应该是写给Unity看的,让Unity知道他自己是运行在这个Activity上。
这里我们基本上只要修改activity里面的android:name这一项。修改完成后,我们就可以通过Unity自带Build功能来出Android包了。出包之前请检查一下Player Setting里面的Bundle Identifier,不能留默认的包名在这里,会造成编译失败。编译过程中,可能会出现一些错误,下面罗列几个常见的错误,可以尝试解决:
1. 合并Manifest文件出错,一般来说是在合并所有的AndroidManifest文件的时候出的错,常见的有重复定义了activity、里面的最低sdk写错了。模块的最低sdk不可低于项目的最低sdk。
2. jar文件dex错误,当你的项目中不小心存在了一个以上的相同的jar文件,就会出这个错误,把重复的删掉,只留一个就好了。
3. 找不到Android SDK里面的工具,这个一般来讲是Unity自己的bug,Unity一般不能兼容最新的Android SDK的工具,所以要手动降级才行。
除了上述这些之外,在打包Android项目的过程中还会出现这些那些的错误,大家看到以后不要慌张,会报错总是好的,而且一般的错误你把错误信息贴在万能的Google上,都能找到解决方案。
(三)Unity对Android代码的调用
文章到这里为止,说清楚了怎么把Android这边写成的插件打包到Unity的项目中去。但其实并没有涉及到Unity中怎么调用刚刚写好在Android的Activity中的代码。这一部对于一个Unity开发来说其实非常简单,只要以Unity提供的AndroidJavaClass还有AndroidJavaObject来做为中介就可以在Unity和Java中互传数据。这两个类的调用给人一种通过反射来调用Java代码的感觉。只要你能通过包名和类名拿到某个Java对象,就可以直接通过成员变量名称或者方法名称直接调用到Java那边的代码。举个例子,假如要在Unity中调用刚刚我们写的那个类的ShowMessage类的话我们需要在Unity中准备以下代码。
简单介绍一下这段代码的几个关键点:
1. 通过UnityPlayer可以很方便的拿到当前Activity的Java对象实例。
2. 对Java对象实例的方法的调用实际上很简单,只要调用Call就可以了。
3. 注意用宏来区隔Native代码。UNITY_ANDROID && !UNITY_EDITOR这个推荐的写法,如果不过滤掉UNITY_EDITOR会在运行的时候报错。
4. 推荐在new出AndroidJavaClass还有AndroidJavaObject的地方用using来进行保护,确保执行结束后Unity会自动回收相应的代码。
其他的部分在这篇文章里面我们不展开。
本文到这里差不多把Unity Android的开发过程描述了一遍,如果有不清楚,欢迎留言。
针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验。
目前功能还在免费开放中。,欢迎点击链接:http://wetest.qq.com/product/cube 使用。
如果对使用当中有任何疑问,欢迎联系腾讯WeTest企业qq:800024531