Unity原生
原生知识
Plugins分类
Unity Plugins主要分为两类:
- Managed Plugins
- Native Plugins
Managed Plugins
Managed plugins are managed .NET assemblies created with tools like Visual Studio or MonoDevelop. They contain only .NET code which means that they can’t access any features that are not supported by the .NET libraries
可以看出Managed Plugins是基于.NET的,只含有.NET code(因为Unity只支持.net 2.0,所以并非所有的.NET库和特性都支持)。
Natiev Plugins
Native plugins are platform-specific native code libraries. They can access features like OS calls and third-party code libraries that would otherwise not be available to Unity.
Native Plugins可以理解成.NET以外的平台相关的本地代码库(比如c++(unmanaged code),java等等编写的库),通过编译使用这些平台相关的bendi 代码我们可以去得到一些Unity不支持的一些功能特性,同时Native Plugins允许我们去重用那些平台相关的库。
Note:
if you forget to add a managed plugin file to the project, you will get standard compiler error messages. If you do the same with a native plugin, you will only see an error report when you try to run the project.(Managed Plugin是基于.NET的,Unity
直接识别,所以在编译时就会报错。而Native Plugin只会在运行时报错。)
Plugins文件扩展名和支持平台
Unity支持的Plugins文件扩展名如下:
.dll(这里要分Managed还是Native,支持多个平台)
.so(Android上使用的代码库文件类型)
.a(IOS上使用的代码库文件类型)
.jar(java文件打包(不包含Android资源文件)
.swift(IOS上新语言swift)
.aar(打包Android包含代码文件和所有Android资源文件)
.framework
.bundle
.plugin
……(这里只列举了比较典型的一些插件文件扩展名)
Unity支持的平台:
Unity是支持多平台的:
- Editor(Unity Editor)
- Windows
- IOS
- Android
……(上述只列举了一部分)
那么为什么需要了解这么多不同扩展名和不同的支持平台了?
因为不同的文件扩展名是针对不同的平台而运用的。
比较典型的就是:
.dll(多平台)
.so .jar .aar(Android)
.a .m .mm .swift .framework(IOS)
那么是不是只要随便编译一个.so文件就能在所有的Android机器上运行了了?
答案是否定的。因为不同的CPU处理器所支持的指令集是不一样的
处理器与Plugins
不同的CPU处理器所支持的指令集是不一样的,也就是说要想我们编译的Plugins要在对应机器上运行起来,我们必须确保我们编译的Plugins支持该设备处理器所支持的指令集。
接下来以Android平台.so为例来学习:
Android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。
应用程序二进制接口(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。在Android系统上,每一个CPU架构对应一个ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。
从上面的引用可以看出,要想在编译的.so文件在不同的Android处理器上运行起来,我们必须保证编译的.so支持该设备的指令集。
但有一点要注意,并不是说每一种CPU都只支持一种架构:
armeabi. By contrast, a typical, ARMv7-based device would define the primary ABI as armeabi-v7a and the secondary one as armeabi, since it can run application native binaries generated for each of them.(ARMv7的机器的第一选择是armabi-v7a,但ARMv7也同时支持armeabi(第二选择))
Many x86-based devices can also run armeabi-v7a and armeabi NDK binaries. For such devices, the primary ABI would be x86, and the second one, armeabi-v7a.(大部分x86机器都支持armeabi-v7a和armeabi,但x86的第一选择是x86)
那么接下来我们通过学习编译和使用.so来加深理解。
.so文件
首先让我们认识一下什么是.so文件?
Linux中的.so文件类似于Windows中的DLL,是动态链接库,也有人译作共享库
为什么我们需要编译成.so文件?
当多个程序使用同一个动态链接库时,既能节约可执行文件的大小,也能减少运行时的内存占用。
Squeeze extra performance out of a device to achieve low latency or run computationally intensive applications, such as games or physics simulations. (压榨机器性能)
Reuse your own or other developers’ C or C++ libraries. (重用C和C++库)
.so与ELF
在了解.so文件结构之前,我们需要了解一下什么是ELF文件?
ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用
ELF文件主要分为三类:
- Relocatable file(可重定位对象文件 e.g. 汇编生成的.o文件)
- Executable file(可执行文件)
- Shared object file(动态库文件 e.g. .so文件)
接下来让我们看看ELF Object file的构成:
为什么会有左右两个很类似的图来说明ELF的组成格式?这是因为ELF格式需要使用在两种场合:
a) 组成不同的可重定位文件,以参与可执行文件或者可被共享的对象文件的链接构建;
b) 组成可执行文件或者可被共享的对象文件,以在运行时内存中进程映像的构建。
如果想知道ELF文件属于哪一类ELF,并且采用了大端还是小段模式,以及支持的架构等信息的话可以通过fiel命令(貌似属于Linux上的命令,Windows上可以通过Cygwin来模拟Linux环境)
可以看到libBugly.so是属于shared objectt,target架构是ARM,是小端(LSB)32位结构
接下来我们关注一下ELF文件具体的组成部分:
主要由三部分组成:
- the ELF header
- the Program Header Table
- the Section Header Table
ELF header除了包含前面file命令打印出的一些信息外,还包括更多描述ELF各文件部分的相关信息。
我们可以通过readelf命令来查看(Windows上除了通过Cygwin,这里我们还可以使用NDK里的arm-linux-androideabi-readelf.exe来查看ELF Header信息)
具体每一个字段的含义可以查看ELF
这里我比较关注的是:
Machine – 表示当前的ELF文件支持的架构。从上面可以看出libBugly.so支持ARM架构
Entry point address – 描述程序入口的虚拟地址,如果没有入口则为0x0(这里因为libBugly.so是提供再链接,所以不存在入口点)
Start of *** – 描述了特定headers内容的内存偏移
Size of *** – 描述了特定header所占字节大小
……
从上面的readelf输出结果可以看出libBugly.so包含了9个program headers,25个section headers。
这里暂时不深入去了解ELF文件了,如需了解,可以查看下面给出的链接。
可执行文件(ELF)格式的理解:
这里因为我在Android 7.0(API 24)遇到了一个问题:
Missing Section Headers
后来了解到在Android 7.0开始,Google做了很多改动和要求,包括下面这一项:
Missing Section Headers (Enforced since API 24)
而因为Section Headers在.so中被移除了所以报出了这个问题。(好像是为了增加破解难度)
这里让我们来了解一个命令工具strip – 用于移除ELF文件中一些不必要的信息。(比如 debug symbol, section headers……)
首先让我们通过readelf –section-headers查看section headers的信息:
可以看到libSubly.so有24个section headers
然后通过strip –remove-sections=.* libBugly.so尝试移除所有sections(这里注意Cygwin strip无法识别.so,但NDK strip却可以,不知道是不是需要)
可以看到我们成功的移除了所有section header(除了.shstrtab)。
我想这应该就是前面遇到Missing Section Headers的原因,Android 7.0上强制要求不能Missing Section Header。(待确认)
这里主要知道如何去查看.so文件的架构等信息以及ELF文件的大概组成以及如何通过strip命令去移除一些ELF文件里不必要的信息即可。
编译和使用.so
结合以前学习的一些知识:
JNI和NDK交叉编译进阶学习
NDK&JNI&Java&Android初步学习总结归纳
要想在Windows上编译出在Android处理器使用的.so文件,我需要通过NDK(一系列工具的集合,帮助开发者快速开发C(或C++)的动态库。集成了交叉编译器)或者Android Studio配合CMake进行编译。
待续…..
.so架构的抉择
还记得前面提到的关于.so架构兼容的问题吗。
按照前面的理论是不是说我们只需要支持armeabi或者armeabi-v7架构,就能在大部分机器上跑了了?
答案是肯定的,这也是为什么有些人通过减少.so的种类来达到减少APK大小。但值得注意的一点是兼容并不是说100%不会出问题,同时兼容的.so并不一定能使使用性能达到最佳。
所以最好的方法还是通过想办法使用正确架构的.so才是上上之策。
至于即想减少APK大小又想完美使用正确的.so架构的方式,可以参考下面这位博主的一些提议:
ANDROID动态加载 使用SO库时要注意的一些问题
Unity Android & IOS Plugin
接下来让我们结合Unity对Android和IOS平台架构的支持来学习理解CPU架构与Unity之间的关系。
Android:
IOS(mono):
IOS(IL2CPP):
可以看到Android上只支持ARMv7和x86
IOS上支持的情况要根据编译后端设置而定(这里提一下Mac上查看.a文件架构的命令是lipo):
Mono只支持IOS ARMv7
IL2CPP支持IOS ARMv7和ARM64
Android因为前面提到的armeabi-v7和x86几乎兼容了所有Android机器,所以只支持这两个是说的过去的。
armv7支持了IOS 4 - 5的大部分机器
但IOS 5之后基本都要求arm64,也就是说要支持IOS新机器我们必须使用IL2CPP作为后端编译工具(Unity 4.6之后开始支持的)
对应平台的Plugin需要放到对应文件目录下。
Android:
Assets/Plugins/Android
IOS:
Assets/Plugins/IOS
Windows:
Assets/Plugins
Android实战
实战Android前,让我们先了解下Android重要的IDE以及代码相关知识。
Android Studio
以前设计到Android开发的时候,都是使用的Eclipse配合ADT插件来弄得。但Eclipse已经被Google放弃了,Google推出了Android Studio作为新一代Android的IDE,所以学习Android Studio用于Android开发是有必要的。
先让我们来了解下什么是Android Studio:
Android Studio 是基于 IntelliJ IDEA 的官方 Android 应用开发集成开发环境 (IDE)。
这里本人使用Android Studio的需求是为了做移动平台游戏开发时用于Android原生开发。
接下来以使用Android Studio配合Unity开发Android为例来学习。
这里放一张Android官网的Android构建流程图加深印象:
JNI(Java交互)
Unity开发搞Android原生开发,主要是通过JNI(Java Native Interface)去访问JVM和Java代码交互。(更多内容见后面的实战学习)
详细的JNI,JVM,Java,Android,NDK等概念学习,参考以前的学习:
JNI和NDK交叉编译进阶学习
NDK&JNI&Java&Android初步学习总结归纳
Android项目相关概念
接下来让我们看看Android Studio创建Android工程之后的基本结构:
接下来我们需要理解一个Android Studio里很重要的同一个概念:
模块
模块是源文件和构建设置的集合,允许您将项目分成不同的功能单元。您的项目可以包含一个或多个模块,并且一个模块可以将其他模块用作依赖项。每个模块都可以独立构建、测试和调试。
Android Studio提供了一下集中不同类型的模块:
- Android应用模块(就是上面我们创建Android Project时看到的app就是我们的Android应用模块)
- 库模块(unityandroidlib就是我单独创建的Android库模块–目的是为了单独导出jar或者aar)
- Google Cloud模块
Android库模块实战
在通过Android库模块导出AAR或者Jar前,我们需要弄学习了解一个自动化构建工具:Gradle
Gradle
Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化建构工具。
这里直接引用别人的对Gradle的总结,来简单了解下Gradle:
Gradle是一种构建工具,它可以帮你管理项目中的差异,依赖,编译,打包,部署……,你可以定义满足自己需要的构建逻辑,写入到build.gradle中供日后复用.
这里暂时需要了解知道的是,Gradle有两大概念:
- Project
Every Gradle build is made up of one or more projects. What a project represents depends on what it is that you are doing with Gradle. For example, a project might represent a library JAR or a web application. It might represent a distribution ZIP assembled from the JARs produced by other projects. - Tasks
Each project is made up of one or more tasks. A task represents some atomic piece of work which a build performs. This might be compiling some classes, creating a JAR, generating Javadoc, or publishing some archives to a repository.
详细的Gradle学习,参考官网内容:
Build Script Basics
Android库模块
这里简单理解下aar和jar的区别就是,AAR是带了所有资源(包括res,AndroidManifest.xml等APK需要的资源+代码Jar的集合)。
Jar库模块
Studio如何导出库模块作为学习对象,因为我们的目标就是通过Android Studio导出AAR或者Jar给Unity使用:
来看看Android库模块是如何新建的:
Project右键->New->Module->Android Library
通过查看Android应用模块和Android库模块的build.gradle,我们可以通过Gradle是如何定义的:
Android应用模块:
1 | apply plugin: 'com.android.application' |
Android库模块:
1 | apply plugin: 'com.android.library' |
接下来我们目标是在如何把我们想要的java代码通过Android模块导出成Jar:
让我们看下Android模块的结构:
把相关jar和java代码导入到我们的:
我们现在需要做的就是通过编写我们unityandroidlib这个Android库模块下的build.gradle去支持导出我们想要的jar:
在写出我们需要的Android模块build.gradle构建文件之前,我们先来了解下Android模块构建的build.gradle里每一个标签的意义:
详细的Android模块构建gradle标签学习
详细的Android DSL标签参考
Android Gradle的详细学习参考Building Android Apps
实战Android模块的build.gradle:
1 | /* |
完成build.gradle编写后,我们打开Gradle Project就可以选中我们新写的exportJar任务,双击执行:
上面的注释很详细了,就不过多描述了,直接来看下我们在release下生成的新得AndroidPlugin.jar吧:
我们成功得到了过滤掉BuildConfig.class,R.class等文件的jar代码包。
得到jar包后,我们还需要把我们的Android相关的资源以及jar包放到我们的Unity对应项目目录才行(Plugins/Android/):
接下来我们需要的是通过Unity封装的JNI去访问Java代码:
AndroidNativeManager.cs
1 | /* |
NativeMessageHandler.cs
1 | /* |
MainActivity.java
1 | package com.tonytang.unityandroidproject; |
至此我们成功完成了使用Android Studio通过编写Gradule导出我们需要的jar代码包,然后通过Unity封装的JNI访问java方法并通过Unity的方法UnityPlayer.UnitySendMessage()方法成功返回调用了CS代码。
关于Unity使用AAR的学习:
待续……
详细的Android DSL标签学习:
Android Plugin DSL Reference
详细的Gradle task学习:
Gradle Task
待续……
AAR库模块
这里再次说一下AAR和Jar的区别:
AAR是带了所有资源(包括res,AndroidManifest.xml等APK需要的资源+代码Jar的集合)。
Unity是可以使用AAR作为资源的使用的,导出AAR和Jar没有太多区别,这里注重讲下Unity使用Android Studio导出的Jar需要注意的地方和坑点。
导出Jar的同时其实已经导出了AAR,AAR存在下面这个目录:
但如果我们直接把res和aar以及AndroidManifest拷贝到Plugins插件目下,打包会发现报错:
错误的意思大致是classes.jar代码已存在有重复。
classes.jar代码重复是因为我们前面导出jar的build.gradle配置了:
1 | // dependencies {}指定编译依赖信息(比如我们的项目需要依赖unity的classes.jar库) |
这里的两句implementation,前者是表示libs下的jar作为引用并一起打包输出到aar里,后者表示要引用并一起打包classes.jar库。
所以这里就是导致我们的AAR里有一份classes.jar库代码存在的原因。
改写build.gradle避免classes.jar被一起打包到AAR里:
1 | // dependencies {}指定编译依赖信息(比如我们的项目需要依赖unity的classes.jar库) |
implementation – Gradle 会将依赖项添加到编译类路径,并将依赖项打包到构建输出
compileOnly – Gradle 只会将依赖项添加到编译类路径(即不会将其添加到构建输出)
通过修改build.gradle和调整目录:
我们避免了classes.jar被打进AAR里,这样一来就不会在Unity打包时有classes.jar代码重复的问题了。
Note:
AAR可以通过修改后缀成.zip通过解压软件插件内部详情
dependencies模块详细说明参考
IOS实战
IOS实战学习前,我们需要了解IOS的开发语言,以前是Objective-C,后来苹果推出了switf。
这里针对Unity而言,我们主要用到的还是以Objective-C为主。
Objective-C语法
*.h
1 | ///所需其他头文件 |
*.mm
1 | #import "*.h" |
Unity与IOS的交互
了解Unity与IOS交互前,让我们先了解部分IOS相关概念:
UIView是一个视图,UIViewController是一个控制器,每一个viewController管理着一个view。
IOS和UIViewController的交互就好比Android里和Activity的交互。
结合Unity官方讲解:
Customizing an iOS Splash Screen Other Versions Leave feedback Structure of a Unity XCode Project
我们可以知道IOS里,UnityAppController.mm是整个程序的入口。如果我们想要自定义入口,我们需要继承至UnityAppController。
AppController.h
1 | // 自定义Unity AppCOntroller |
AppController.mm
1 | //自身的头文件 |
通过上面的代码,我们完成了下面两件事:
- 自定义Unity IOS程序入口
- 添加了自定义的View(绑定到自定义UIViewController上)到rootView上
成功绑定到自定义UIViewController上后,我们就能通过自定义的UIViewController去和IOS打交道了。
接下来看看自定义的UIViewController是如何定义的,结合权限申请实战学习:
UnityViewController.h
1 | //自定义的UIViewController |
UnityViewController.mm
1 | //包含自身头文件 |
可以看到通过自定义的UIViewController,我们就可以直接编写对应原生功能(e.g. 权限申请)代码了。
引用
Customizing an iOS Splash Screen Other Versions Leave feedback Structure of a Unity XCode Project
Unity-IOS交互整理
iOS 系统权限
待续……
Reference
Unity Official Website Part
Knowledge Part
关于Android的.so文件你所需要知道的
动态库(.so)
Getting Started with the NDK
readelf elf文件格式分析
ABI Management
Android changes for NDK developers
可执行文件(ELF)格式的理解
ARM ELF File Format