文章目錄
  1. 1. 原生知识
    1. 1.1. Plugins分类
      1. 1.1.1. Managed Plugins
      2. 1.1.2. Natiev Plugins
    2. 1.2. Plugins文件扩展名和支持平台
      1. 1.2.1. 处理器与Plugins
      2. 1.2.2. .so文件
      3. 1.2.3. .so与ELF
      4. 1.2.4. 编译和使用.so
      5. 1.2.5. .so架构的抉择
      6. 1.2.6. Unity Android & IOS Plugin
        1. 1.2.6.1. Android实战
          1. 1.2.6.1.1. Android Studio
          2. 1.2.6.1.2. JNI(Java交互)
          3. 1.2.6.1.3. Android项目相关概念
          4. 1.2.6.1.4. Android库模块实战
            1. 1.2.6.1.4.1. Gradle
            2. 1.2.6.1.4.2. Android库模块
            3. 1.2.6.1.4.3. Jar库模块
            4. 1.2.6.1.4.4. AAR库模块
      7. 1.2.7. IOS实战
        1. 1.2.7.1. Objective-C语法
        2. 1.2.7.2. Unity与IOS的交互
        3. 1.2.7.3. 引用
  2. 2. Reference
    1. 2.1. Unity Official Website Part
    2. 2.2. Knowledge Part

原生知识

Plugins分类

Unity Plugins主要分为两类:

  1. Managed Plugins
  2. 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是支持多平台的:

  1. Editor(Unity Editor)
  2. Windows
  3. IOS
  4. 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文件主要分为三类:

  1. Relocatable file(可重定位对象文件 e.g. 汇编生成的.o文件)
  2. Executable file(可执行文件)
  3. Shared object file(动态库文件 e.g. .so文件)

接下来让我们看看ELF Object file的构成:
ELFFileStructure
为什么会有左右两个很类似的图来说明ELF的组成格式?这是因为ELF格式需要使用在两种场合:
a) 组成不同的可重定位文件,以参与可执行文件或者可被共享的对象文件的链接构建;
b) 组成可执行文件或者可被共享的对象文件,以在运行时内存中进程映像的构建。

如果想知道ELF文件属于哪一类ELF,并且采用了大端还是小段模式,以及支持的架构等信息的话可以通过fiel命令(貌似属于Linux上的命令,Windows上可以通过Cygwin来模拟Linux环境)
ELFFileCommand
可以看到libBugly.so是属于shared objectt,target架构是ARM,是小端(LSB)32位结构

接下来我们关注一下ELF文件具体的组成部分:
ELFFileStructureDetail

主要由三部分组成:

  1. the ELF header
  2. the Program Header Table
  3. the Section Header Table

ELF header除了包含前面file命令打印出的一些信息外,还包括更多描述ELF各文件部分的相关信息。
我们可以通过readelf命令来查看(Windows上除了通过Cygwin,这里我们还可以使用NDK里的arm-linux-androideabi-readelf.exe来查看ELF Header信息)
ELFHeaderReadelfCommand
具体每一个字段的含义可以查看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的信息:
SectionHeadersInfo
可以看到libSubly.so有24个section headers

然后通过strip —remove-sections=.* libBugly.so尝试移除所有sections(这里注意Cygwin strip无法识别.so,但NDK strip却可以,不知道是不是需要)
StripSectionsAfter
可以看到我们成功的移除了所有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:
AndroidSupportedArchitectures

IOS(mono):
IOSMonoSupportedArchitectures

IOS(IL2CPP):
IOSIL2CPPSupportedArchitectures

可以看到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构建流程图加深印象:
AndroidBuildFlowChart

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工程之后的基本结构:
AndroidStudioAndroidProjectStucture

接下来我们需要理解一个Android Studio里很重要的同一个概念:
模块
模块是源文件和构建设置的集合,允许您将项目分成不同的功能单元。您的项目可以包含一个或多个模块,并且一个模块可以将其他模块用作依赖项。每个模块都可以独立构建、测试和调试。

Android Studio提供了一下集中不同类型的模块:

  1. Android应用模块(就是上面我们创建Android Project时看到的app就是我们的Android应用模块)
  2. 库模块(unityandroidlib就是我单独创建的Android库模块—目的是为了单独导出jar或者aar)
  3. Google Cloud模块
Android库模块实战

在通过Android库模块导出AAR或者Jar前,我们需要弄学习了解一个自动化构建工具:Gradle

Gradle

Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化建构工具。

这里直接引用别人的对Gradle的总结,来简单了解下Gradle:
Gradle是一种构建工具,它可以帮你管理项目中的差异,依赖,编译,打包,部署……,你可以定义满足自己需要的构建逻辑,写入到build.gradle中供日后复用.

这里暂时需要了解知道的是,Gradle有两大概念:

  1. 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.
  2. 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库模块

Android 库在结构上与 Android 应用模块相同。它可以提供构建应用所需的一切内容,包括源代码、资源文件和 Android 清单。不过,Android 库将编译到您可以用作 Android 应用模块依赖项的 Android 归档 (AAR) 文件,而不是在设备上运行的 APK。

这里简单理解下aar和jar的区别就是,AAR是带了所有资源(包括res,AndroidManifest.xml等APK需要的资源+代码Jar的集合)。

Jar库模块

Studio如何导出库模块作为学习对象,因为我们的目标就是通过Android Studio导出AAR或者Jar给Unity使用:
来看看Android库模块是如何新建的:
Project右键->New->Module->Android Library
AndroidStuidoAndroidModule

通过查看Android应用模块和Android库模块的build.gradle,我们可以通过Gradle是如何定义的:
Android应用模块:

1
apply plugin: 'com.android.application'

Android库模块:

1
apply plugin: 'com.android.library'

接下来我们目标是在如何把我们想要的java代码通过Android模块导出成Jar:
让我们看下Android模块的结构:
AndroidStudioAndroidModule
把相关jar和java代码导入到我们的:
AndroidStudioAndroidMudleImportCode

我们现在需要做的就是通过编写我们unityandroidlib这个Android库模块下的build.gradle去支持导出我们想要的jar:
在写出我们需要的Android模块build.gradle构建文件之前,我们先来了解下Android模块构建的build.gradle里每一个标签的意义:
详细的Android模块构建gradle标签学习
详细的Android DSL标签参考
Android Gradle的详细学习参考Building Android Apps

实战Android模块的build.gradle:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
* 表示使用Android插件进行gradle的Android自动化构建
* com.android.library表示是构建成一个Android模块
*/

apply plugin: 'com.android.library'

// android {}指定如何配置android相关编译构建
android {
// 指定Android API编译版本
compileSdkVersion 27

//defaultConfig {}指定Android部分默认设置,可以通过这里的指定覆盖AndroidManifest.xml里的部分内容
defaultConfig {
minSdkVersion 15 // 最低支持的Android API版本
targetSdkVersion 27 // 目标Android API版本
versionCode 1 // App的version number
versionName "1.0" // App的version name

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

// buildTypes {}指定编译类型的详细信息,比如release和debug的详细编译设定
buildTypes {
// release的编译设定
release {
minifyEnabled false // 是否开启代码精简
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // 代码混淆相关设置
}
}
}

// dependencies {}指定编译依赖信息(比如我们的项目需要依赖unity的classes.jar库)
dependencies {
// fileTree - 指定目录(libs)下符合条件(*.jar)的文件添加到编译路径下
implementation fileTree(include: ['*.jar'], dir: 'libs')
// com.android.support:appcompat-v7:27.1.1 - 添加Android兼容模式库到项目,用于支持更多的theme和兼容老的部分功能
implementation 'com.android.support:appcompat-v7:27.1.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
// files - 猜测是指定需要依赖的jar库(没找到官方解释)
implementation files('libs/classes.jar')
}

// task - 添加自定义的构建任务
// 这里是增加一个删除旧的jar的任务
// type:指定任务的类型delete删除
task deleteOldJar(type: Delete) {
// 指定删除release/AndroidPlugin.jar文件
delete 'build/libs/classes.jar'
delete 'release/AndroidPlugin.jar'
}

// task - 添加自定义的构建任务
// 这里增加一个从新打包jar的任务,目的是为了去除默认build里打包的BuildConfig.class(会导致Unity使用jar时报错)
// type: 指定任务类型是Jar打包
task makeJar(type: Jar) {
// 指定jar来源(文件或者目录)
from file('build/intermediates/classes/release')
// 指定需要重新打包后的jar名字
archiveName = 'classes.jar'
// 输出目录
destinationDir = file('build/libs/')
// 过滤不需要的class文件
exclude('com/tonytang/unityandroidproject/BuildConfig.class')
exclude('com/tonytang/unityandroidproject/BuildConfig\$*.class')
exclude('com/tonytang/unityandroidproject/R.class')
exclude('com/tonytang/unityandroidproject/R\$*.class')
// 需要包含打包到jar中的文件
include('com/tonytang/unityandroidproject/**')
}

// 指定makeJar任务依赖的任务build(依赖的任务先执行)
makeJar.dependsOn(build)

// task - 添加自定义的构建任务
// 这里是增加一个到处jar的任务
// type:指定任务类型为Copy复制
task exportJar(type: Copy) {
// 从哪里复制
from('build/libs/')
// 复制到哪里
into('release/')
/// 重命名文件
rename('classes.jar', 'AndroidPlugin.jar')
}

// 指定exportJar任务依赖的任务deleteOldJar,build,makeJar(依赖的任务先执行)
exportJar.dependsOn(deleteOldJar, makeJar)

完成build.gradle编写后,我们打开Gradle Project就可以选中我们新写的exportJar任务,双击执行:
AndroidStudioGradleProjectView
AndroidStudioGraduleBuildProccess

上面的注释很详细了,就不过多描述了,直接来看下我们在release下生成的新得AndroidPlugin.jar吧:
AndroidModuleJar
我们成功得到了过滤掉BuildConfig.class,R.class等文件的jar代码包。
得到jar包后,我们还需要把我们的Android相关的资源以及jar包放到我们的Unity对应项目目录才行(Plugins/Android/):
UnityAndroidFolderStructure

接下来我们需要的是通过Unity封装的JNI去访问Java代码:
AndroidNativeManager.cs

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
* Description: AndroidNativeManager.cs
* Author: TONYTANG
* Create Date: 2018/08/10
*/


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_ANDROID
/// <summary>
/// AndroidNativeManager.cs
/// Android原生管理类
/// </summary>
public class AndroidNativeManager : NativeManager
{
/// <summary>
/// Android主Activity对象
/// </summary>
private AndroidJavaObject mAndroidActivity;

/// <summary>
/// 初始化
/// </summary>
public override void init()
{

base.init();
Debug.Log("init()");
if (Application.platform == RuntimePlatform.Android)
{
//这里使用using的目的是确保AndroidJavaClass对象尽快被删除
using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
//获得当前Activity(这里是为了获得MainActivity)
mAndroidActivity = jc.GetStatic<AndroidJavaObject>("currentActivity");
if (mAndroidActivity == null)
{
Debug.Log("获取UnityMainActivity::mAndroidActivity成员失败!");
}
else
{
Debug.Log("获取UnityMainActivity::mAndroidActivity成员成功!");
}
}
}
}

/// <summary>
/// 调用原生方法
/// </summary>
public override void callNativeMethod()
{

base.callNativeMethod();
Debug.Log("callNativeMethod()");
if (mAndroidActivity != null)
{
mAndroidActivity.Call("javaMethod", "cs param");
}
}
}
#endif

NativeMessageHandler.cs

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
/*
* Description: NativeMessageHandler.cs
* Author: TONYTANG
* Create Date: 2018/08/10
*/


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// NativeMessageHandler.cs
/// 原生消息相应处理器
/// </summary>
public class NativeMessageHandler : MonoBehaviour {

/// <summary>
/// 接收原生消息
/// </summary>
/// <param name="msg"></param>
public void resUnityMsg(string msg)
{

Debug.Log(string.Format("resUnityMsg : {0}", msg));
}
}

MainActivity.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
33
34
35
36
37
38
39
40
41
42
package com.tonytang.unityandroidproject;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;

import com.unity3d.player.UnityPlayer;
import com.unity3d.player.UnityPlayerActivity;

public class MainActivity extends UnityPlayerActivity {

// Unity那一侧相应Java回调的GameObject名字
public final static String mUnityCallBackHandler = "GameLauncher";

// Android上下文
public Context mContext = null;

// 主Activity
public Activity mCurrentActivity = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Log.d("JNI", "onCreate()");
mContext = this.getApplicationContext();
mCurrentActivity = this;
******
}

******

//Unity call part
public void javaMethod(String csparam)
{

Log.d("JNI", "javaMethod() with parameter:" + csparam);
UnityPlayer.UnitySendMessage(mUnityCallBackHandler,"resUnityMsg", "java param");
}
}

UnityJNICall

至此我们成功完成了使用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存在下面这个目录:
AndroidAAROutput

但如果我们直接把res和aar以及AndroidManifest拷贝到Plugins插件目下,打包会发现报错:
错误的意思大致是classes.jar代码已存在有重复。

classes.jar代码重复是因为我们前面导出jar的build.gradle配置了:

1
2
3
4
5
6
7
8
// dependencies {}指定编译依赖信息(比如我们的项目需要依赖unity的classes.jar库)
dependencies {
// fileTree - 指定目录(libs)下符合条件(*.jar)的文件添加到编译路径下
implementation fileTree(include: ['*.jar'], dir: 'libs')
******
// files - 指定需要依赖的库文件(没找到官方解释)
implementation files('libs/classes.jar')
}

这里的两句implementation,前者是表示libs下的jar作为引用并一起打包输出到aar里,后者表示要引用并一起打包classes.jar库。

所以这里就是导致我们的AAR里有一份classes.jar库代码存在的原因。

改写build.gradle避免classes.jar被一起打包到AAR里:

1
2
3
4
5
6
7
8
9
10
11
// dependencies {}指定编译依赖信息(比如我们的项目需要依赖unity的classes.jar库)
dependencies {
// fileTree - 指定目录(libs)下符合条件(*.jar)的文件添加到编译路径下
// 因为项目依赖了Unity的class.jar但又不希望classes.jar跟着aar一起导出(会导致unity打包报错 -- classes.jar包重复问题)
// 所以这里不能使用fileTree包含lib目录下所有jar,需要修改成不包含classes.jar的形式
implementation fileTree(include: ['*.jar'], dir: 'libs/include')
******
// files - 猜测是显示指定依赖文件(没找到官方解释)
// 这里专门用compileOnly是为了避免classes.jar进aar包
compileOnly files('libs/exclude/classes.jar')
}

implementation — Gradle 会将依赖项添加到编译类路径,并将依赖项打包到构建输出
compileOnly — Gradle 只会将依赖项添加到编译类路径(即不会将其添加到构建输出)
通过修改build.gradle和调整目录:
AndroidGradleLibsFolderStructure
我们避免了classes.jar被打进AAR里,这样一来就不会在Unity打包时有classes.jar代码重复的问题了。

Note:
AAR可以通过修改后缀成.zip通过解压软件插件内部详情
dependencies模块详细说明参考

IOS实战

IOS实战学习前,我们需要了解IOS的开发语言,以前是Objective-C,后来苹果推出了switf。
这里针对Unity而言,我们主要用到的还是以Objective-C为主。

Objective-C语法

*.h

1
///所需其他头文件
#import "*.h"

//<Protocol> OC里的Protocol相当于Interface,需要实现接口方法
//但有一点不同的是Protocol里并不是所有的接口方法都必须实现
//@required -- 必须实现的部分
//@optional -- 可选实现的部分
@interface *:NSObject<*>{

    // 类变量声明
    //......
}

//类属性声明
@property (nonatomic, strong) * *属性名;
//Unity回调GameObject名字
@property (nonatomic, strong) NSString *mGameObjectHandler;

//类方法声明
//+代表属于类方法,相当于静态
//-代表属于对象,相当于类成员
//方法定义格式如下:
//(+ or -)(return 返回类型)方法名:(参数1类型) 参数1名 : (参数2类型) 参数2名;
+(类型*) Instance;

-(void)方法名;

-(void)方法名:(参数类型 *)参数名;
@end

*.mm

1
#import "*.h"

//定义全局的静态变量
static 类型名* 变量名 = nil;

//声明UnitySendMessage为外部方法,用于native code和C#发消息交互
#if defined(__cpluscplus)
//确保函数的编译方式
//确保找到正确的函数名
//这一点好比C++指定不同的调用约定(stdcall(C++默认使用)和cdecl(C默认使用))
//会决定函数名生成,参数入栈顺序,堆栈维护等
//在读取的时候也需要指定同样编译方式
extern "C"{
#endif  
    extern void UnitySendMessage(const char *, const char *, const char *);

#if defined(__cpluscplus)
}
#endif

//指定实现的类型
@implementation 类型名

//获取单例对象
+(返回类型*) Instance
{
    return *;
}

-(void)方法名
{
    //方法实现
    NSLog(@"initSDK");
    self.mGameObjectHandler = @"响应原生消息的GameObejct名字";
}

-(void)方法名:(参数1类型 *)key value:(参数2类型 *)value
{
    //Log打印方式
    NSLog(@"参数设置:key = %@ value = %@", key,value);
    //字符串拼接方式
    NSString *msg = [NSString stringWithFormat:@"%@ %@ %s", key, value, issuccess ? "true" : "false"];
    //原生发送消息给CS一侧
    UnitySendMessage([self.mGameObjectHandler UTF8String], "CS回调响应方法名", [msg UTF8String]);
}

//用于C#和Objective C交互的代码
extern "C"{
    void CS方法名()
    {
        [[类型名 类型静态变量] 成员方法];
    }

    void CS方法名(char* key, char* value)
    {
        //CS char*到NSString转换方式
        NSString* parameterkey = [NSString stringWithUTF8String:key];
        NSString* parametervalue = [NSString stringWithUTF8String:value];
        [[类型名 类型静态变量] 成员方法:参数1值 参数2名字:参数2值];
    }
}
@end

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
//导入Unity的UnityAppController.h,自定义AppController需要继承至UnityAppController
#import "UnityAppController.h"

@interface AppController : UnityAppController
@property(nonatomic,strong) UINavigationController *naVC;
-(void) createUI;
@end

AppController.mm

1
//自身的头文件
#import "AppController.h"
//导入我们自己写接口的类的.h文件,用于实现自定义的UIViewController
#import "UnityViewController.h"
//其他文件
#import <Foundation/Foundation.h>

@implementation AppController
-(void) createUI
{
    NSLog(@"AppController:createUI()");
    _rootController = [[UIViewController alloc]init];
    _rootView = [[UIView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    _rootController.view = _rootView;
        
    //自定义viewcontroller添加到现有的viewcontroller上
    UnityViewController *vc = [[UnityViewController alloc]init];
    self.naVC =[[UINavigationController alloc]initWithRootViewController:vc];
    
    //添加自定义view到rootView上
    [_rootView addSubview:self.naVC.view];

    _window.rootViewController = _rootController;
    [_window bringSubviewToFront:_rootView];
    [_window makeKeyAndVisible];
}
@end

//这是一个固定写法,因为这个代码,所以启动界面是从这里启动
IMPL_APP_CONTROLLER_SUBCLASS(AppController);

通过上面的代码,我们完成了下面两件事:

  1. 自定义Unity IOS程序入口
  2. 添加了自定义的View(绑定到自定义UIViewController上)到rootView上

成功绑定到自定义UIViewController上后,我们就能通过自定义的UIViewController去和IOS打交道了。
接下来看看自定义的UIViewController是如何定义的,结合权限申请实战学习:
UnityViewController.h

1
//自定义的UIViewController
#import "UnityAppController.h"

@interface UnityViewController : UIViewController
//权限申请
- (void)audioAuthAction;
@end

UnityViewController.mm

1
//包含自身头文件
#import "UnityViewController.h"

//其他头文件
#import "UnityAppController+ViewHandling.h"
#import <UI/UnityView.h>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@implementation UnityViewController

- (void)viewDidLoad
{
    NSLog(@"UnityViewController:viewDidLoad()");
    [super viewDidLoad];
    [self.view addSubview:GetAppController().unityView];
    GetAppController().unityView.frame = self.view.frame;
    
    // Do any additional setup after loading the view.
    // 权限申请相关
    [self audioAuthAction];
}

//权限申请
- (void)audioAuthAction
{
    //麦克风权限申请
    NSLog(@"UnityViewController:audioAuthAction()");
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
       NSLog(@"%@",granted ? @"麦克风准许":@"麦克风不准许");
    }];
}

-(void)viewWillAppear:(BOOL)animated{
    //隐藏导航条view
    NSLog(@"UnityViewController:viewWillAppear()");
    self.navigationController.navigationBar.hidden = YES;
}

-(void)viewDidDisappear:(BOOL)animated
{
    NSLog(@"UnityViewController:viewDidDisappear()");
    self.navigationController.navigationBar.hidden = NO;
}
@end

可以看到通过自定义的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

Unity Plugin

Knowledge Part

关于Android的.so文件你所需要知道的
动态库(.so))
Getting Started with the NDK
readelf elf文件格式分析
ABI Management
Android changes for NDK developers
可执行文件(ELF)格式的理解
ARM ELF File Format

文章目錄
  1. 1. 原生知识
    1. 1.1. Plugins分类
      1. 1.1.1. Managed Plugins
      2. 1.1.2. Natiev Plugins
    2. 1.2. Plugins文件扩展名和支持平台
      1. 1.2.1. 处理器与Plugins
      2. 1.2.2. .so文件
      3. 1.2.3. .so与ELF
      4. 1.2.4. 编译和使用.so
      5. 1.2.5. .so架构的抉择
      6. 1.2.6. Unity Android & IOS Plugin
        1. 1.2.6.1. Android实战
          1. 1.2.6.1.1. Android Studio
          2. 1.2.6.1.2. JNI(Java交互)
          3. 1.2.6.1.3. Android项目相关概念
          4. 1.2.6.1.4. Android库模块实战
            1. 1.2.6.1.4.1. Gradle
            2. 1.2.6.1.4.2. Android库模块
            3. 1.2.6.1.4.3. Jar库模块
            4. 1.2.6.1.4.4. AAR库模块
      7. 1.2.7. IOS实战
        1. 1.2.7.1. Objective-C语法
        2. 1.2.7.2. Unity与IOS的交互
        3. 1.2.7.3. 引用
  2. 2. Reference
    1. 2.1. Unity Official Website Part
    2. 2.2. Knowledge Part