WeTest腾讯质量开发平台 面向 Unity 程序员的 Android 快速上手教程

腾讯WeTest · 2017年07月28日 · 2436 次阅读

作者:Poan,腾讯移动客户端开发 工程师
商业转载请联系腾讯 WeTest 获得授权,非商业转载请注明出处。
原文链接:http://wetest.qq.com/lab/view/324.html


WeTest 导读

随着 Unity、cocos2dx 等优秀跨平台游戏引擎的出现,开发者可以把自己从繁重的 Android、iOS 原生台开发中解放出来,把精力放在游戏的创作。原来做一款跨平台的游戏可能需要开发者懂得 Java、Objective-C、C# 甚至是 C、C++,现在借助 Unity 我们开发者只需要懂得很少的原生应用开发知识就能够打造一款优秀的游戏。特别是在鹅厂,有了 Apollo 这样的组件,原生的接入更加简单,可能每个项目组只需要有 1-2 个人懂 Android,iOS 开发就够了。但是也正因为如此,很多同事有了充足的理由不去学习、接触 Android 和 iOS 的开发,等到真正需要做接入的时候才开始找人找资料,难免会踩坑。基于此,本文的目的就是通过介绍基础的 Android 开发知识以及部分的实际操作,让大家有一定的 Android 基础知识储备。又或者是当作一份 Unity 接入 Android SDK/插件的基础教程,只要照着做,就基本上不会错了。


本文将会从大家熟悉的 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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册