Unity3D:修改iOS应用程序委托

我认为许多为iOS开发游戏的人都不得不面对这样一个事实,那就是必须使用一种或另一种本机功能。 关于Unity3D,此问题可能会引起很多问题:为了实现某种功能,您必须考虑使用Objective-C编写的本机插件。 此刻有人立即绝望并放弃了这个主意。 有人正在AssetStore或论坛中寻找现成的解决方案,希望现成的解决方案已经存在。 如果没有现成的解决方案,那么我们中最顽固的人只会看到iOS编程的深渊以及与Objective-C代码进行Unity3D交互的深渊。

那些选择最后一条道路的人(尽管我认为他们自己知道),在这条艰难而棘手的道路上将面临许多问题:

  • iOS是一个完全陌生且孤立的生态系统,以自己的方式进行开发。 至少,您将需要花费大量时间来了解如何访问应用程序,以及在自动生成的Xcode项目的深处是Unity3D引擎与应用程序的本机组件进行交互的代码。
  • Objective-C是一种相当独立且几乎没有什么编程语言。 当涉及到与Unity3D应用程序的C ++代码进行交互时,这种称为“ Objective-C ++”的语言的“方言”进入了现场。 关于他的信息很少,大部分都是古老的档案。
  • Unity3D和iOS应用程序之间的交互协议很少描述。 您应该完全依靠网络爱好者的教程,他们会编写如何开发最简单的本机插件。 同时,很少有人接触到更深层次的问题,以及因需要做复杂的事情而引起的问题。

那些想了解Unity3D与iOS应用程序交互机制的人,请关注。

为了阐明Unity3D与本机代码交互的阴郁瓶颈,本文介绍了iOS应用程序委托与Unity3D代码的交互方面,其中实现了C ++和Objective-C工具,以及如何自行修改应用程序委托。 该信息对于更好地了解Unity3D + iOS链接机制以及实际使用都是有用的。

iOS与应用程式之间的互动


作为介绍,让我们看一下如何在iOS中实现应用程序与系统的交互,反之亦然。 从原理上讲,iOS应用程序的启动如下所示:

图片

为了从代码的角度研究这种机制,适合使用Xcode使用“ Single View App”模板创建的新应用程序。



通过选择此模板,输出将为您提供可以在设备或仿真器上运行并显示白屏的最简单的iOS应用程序。 Xcode将帮助创建一个项目,其中只有5个带有源代码的文件(其中2个是标头.h文件)和几个我们不感兴趣的辅助文件(排版,配置,图标)。



让我们看看源代码文件负责什么:

  • ViewController.m / ViewController.h-对我们来说不是很有趣的源代码。 由于您的应用程序有一个View(不是用代码表示,而是使用Storyboard),因此您将需要Controller类来控制此View。 通常,这种方式Xco​​de本身鼓励我们使用MVC模式。 生成Unity3D的项目将没有这些源文件。
  • AppDelegate.m / AppDelegate.h是您的应用程序的委托。 定制应用程序代码开始工作的应用程序中的兴趣点。
  • main.m-应用程序的起点。 以任何C / C ++应用程序的方式,它包含程序启动所用的主要功能。

现在,让我们看一下从main.m文件开始的代码:

int main(int argc, char * argv[]) { //1 @autoreleasepool { //2 return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); // 3 } } 

在第1行中,所有内容都很清楚,没有任何解释,让我们继续进行第2行。这表明应用程序生命周期将在自动释放池中发生。 使用自动释放池告诉我们,我们会将应用程序的内存管理委托给此特定池,也就是说,它将在需要释放特定变量的内存时处理问题。 关于iOS上的内存管理的故事超出了本故事的范围,因此没有必要深入研究此主题。 对于对此主题感兴趣的人,您可以找到例如本文

让我们继续进行第3行。它调用UIApplicationMain函数。 程序启动参数(argc,argv)传递给它。 然后,在该函数中,指示要创建哪个类作为应用程序的主类,并创建其实例。 最后,指示要用作应用程序委托的类,创建其实例,配置应用程序类实例与其委托之间的连接。

在我们的示例中,将nil作为表示应用程序实例的类传递-大致而言,本地类似物为null。 除了nil之外,还可以在那里传递从UIApplication继承的特定类。 如果指定nil,则将使用UIApplication。 此类是在iOS上管理和协调应用程序工作的集中点,并且是单例。 有了它,您几乎可以了解有关应用程序当前状态,通知,窗口,系统本身中发生的,会影响该应用程序的事件等所有信息。 该类几乎从不继承。 我们将更详细地介绍Application Delegate类的创建。

创建应用程序委托


指示哪个类用作应用程序委托发生在函数调用中

 NSStringFromClass([AppDelegate class]) 

让我们分部分分析此调用。

 [AppDelegate class] 

此构造返回AppDelegate类的对象(在AppDelegate.h / .m中声明),并且NSStringFromClass函数将类名称作为字符串返回。 我们只需将要创建的类的字符串名称传递给UIApplicationMain函数即可。 为了更好地理解,可以将main.m文件中的第3行替换为以下内容:

 return UIApplicationMain(argc, argv, nil, @"AppDelegate"); 

并且其实现的结果将与原始版本相同。 显然,开发人员决定采用这种方法,以便不使用字符串常量。 使用标准方法,如果重命名委托类,则解析器将立即引发错误。 在使用常规行的情况下,代码将被成功编译,并且仅通过启动应用程序将收到错误。

仅使用类的字符串名称来创建类的类似机制可能会使您想起C#的反射。 在C#中,Objective-C及其运行时功能比反射功能强大得多。 在本文中,这是很重要的一点,但是描述所有功能将花费很多时间。 但是,我们仍将在下面的Objective-C中遇到“反射”问题。 仍然需要了解应用程序委托及其功能的概念。

应用程序委托


应用程序与iOS的所有交互都发生在UIApplication类中。 此类承担许多责任-通知事件的起源,应用程序的状态等。 在大多数情况下,他的角色是通知。 但是,当系统中发生某些事情时,我们应该能够以某种方式响应此更改,以执行某种自定义功能。 如果UIApplication类的实例执行此操作,则此做法将开始类似于称为Divine Object的方法。 因此,值得考虑使此类脱离部分职责。

出于这些目的,iOS生态系统使用诸如应用程序委托之类的东西。 从名称本身,我们可以得出结论,我们正在处理诸如委派的设计模式。 简而言之,我们只是将处理对应用程序某些事件的响应的责任转移给应用程序委托。 为此,在我们的示例中,创建了AppDelegate类,我们可以在其中编写自定义功能,而让UIApplication类在黑盒模式下工作。 就某人的架构设计之美而言,这种方法可能引起争议,但iOS作者自己将我们推向了这种方法,并且绝大多数开发人员(如果不是全部)都在使用它。

为了直观地验证应用程序委托在应用程序工作期间多长时间接收一次特定消息,请看一下该图:

图片

黄色矩形表示响应应用程序生命周期(应用程序生命周期)的某些事件而调用一个或另一个委托方法。 该图仅说明了与应用程序状态更改有关的事件,而没有显示委托人职责的许多其他方面,例如接受通知或与框架进行交互。

以下是一些示例,我们可能需要从Unity3D访问应用程序委托:

  1. 处理推送和本地通知
  2. 将应用程序启动事件记录到分析中
  3. 确定如何启动应用程序-“清理”或退出后台
  4. 如何启动应用程序-通过tach进行通知,使用主屏幕快速操作或仅通过incon上的tach
  5. 与WatchKit或HealthKit进行交互
  6. 打开和处理来自另一个应用程序的URL。 如果此URL适用于您的应用程序,则可以在您的应用程序中对其进行处理,而无需让系统在浏览器中打开该URL。

这不是方案的全部列表。 此外,值得注意的是,委托人在其本机插件中修改了许多分析和广告系统。

Unity3D如何实现应用程序委托


现在,让我们看一下Unity3D生成的Xcode项目,并了解如何在Unity3D中实现应用程序委托。 在为iOS平台构建时,Unity3D自动为您生成一个Xcode项目,该项目使用大量样板代码。 该模板代码还包括应用程序委托代码。 在任何生成的项目中,您都可以找到文件UnityAppController.hUnityAppController.mm 。 这些文件包含我们感兴趣的UnityAppController类的代码。

实际上,Unity3D使用了“单视图应用程序”模板的修改版本。 仅在此模板中,Unity3D不仅使用应用程序委托来处理iOS事件,而且还用于初始化引擎本身,准备图形组件等等。 如果您看一下方法,这很容易理解。

 - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions 

在UnityAppController类的代码中。 当您可以将控制权转移到自定义代码时,将在应用程序初始化时调用此方法。 例如,在此方法内,您可以找到以下几行:

 UnityInitApplicationNoGraphics([[[NSBundle mainBundle] bundlePath] UTF8String]); [self selectRenderingAPI]; [UnityRenderingView InitializeForAPI: self.renderingAPI]; _window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds]; _unityView = [self createUnityView]; [DisplayManager Initialize]; _mainDisplay = [DisplayManager Instance].mainDisplay; [_mainDisplay createWithWindow: _window andView: _unityView]; [self createUI]; [self preStartUnity]; 

甚至没有深入探讨这些挑战的细节,您就可以猜测它们与为工作准备Unity3D有关。 事实证明以下情况:

  1. 主要函数从main.mm调用
  2. 创建了应用程序及其委托的实例类。
  3. 应用程序代表准备并启动Unity3D引擎
  4. 您的自定义代码开始工作。 如果使用il2cpp,则您的代码将从C#转换为IL,然后转换为C ++代码,后者直接进入Xcode项目。

这个脚本听起来很简单且合乎逻辑,但是它带来了一个潜在的问题:如果在Unity3D中工作时无法访问源代码,我们如何修改应用程序委托?

受影响的Unity3D修改应用程序委托


我们可以看一下AppDelegateListener.mm/.h文件。 它们包含允许您将任何类注册为应用程序委托的事件侦听器的宏。 这是一种好方法,我们不需要修改现有代码,而只需添加一个新代码。 但这有一个很大的缺点:并非所有应用程序事件都受支持,并且无法获取有关应用程序启动的信息。

但是,最显而易见的方法是在Unity3D构建Xcode项目后手动更改委托源代码。 这种方法的问题很明显-如果您用手制作程序集,并且不必为每次组装后手动修改代码而感到困惑,那么该选项就很合适。 在使用构建器(Unity Cloud Build或任何其他构建计算机)的情况下,此选项绝对不可接受。 为此,Unity3D开发人员给我们留下了漏洞。

UnityAppController.h文件除了声明变量和方法外,还包含一个宏定义:

 #define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ... 

这个宏使重写应用程序委托成为可能。 为此,您需要采取一些简单的步骤:

  1. 在Objective-C中编写您自己的应用程序委托
  2. 在源代码中的某处添加以下行
     IMPL_APP_CONTROLLER_SUBCLASS(___) 
  3. 将此源放在Unity3D项目的Plugins / iOS文件夹中

现在,您将收到一个项目,其中标准的Unity3D应用程序委托将替换为您的自定义委托。

委托替换宏如何工作


让我们看一下宏的完整源代码:

 #define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ... @interface ClassName(OverrideAppDelegate) \ { \ } \ +(void)load; \ @end \ @implementation ClassName(OverrideAppDelegate) \ +(void)load \ { \ extern const char* AppControllerClassName; \ AppControllerClassName = #ClassName; \ } \ @end 

在源代码中使用此宏将在编译阶段将宏中描述的代码添加到源代码主体中。 该宏执行以下操作。 首先,它将load方法添加到类的接口。 在Objective-C上下文中的接口可以看作是公共字段和方法的集合。 在C#中,静态加载方法将出现在您的类中,该方法不返回任何内容。 接下来,此load方法的实现将添加到您的类的代码中。 在此方法中,将声明变量AppControllerClassName,该变量是char类型的数组,然后将为该变量分配一个值。 此值是您的类的字符串名称。 显然,此信息不足以了解此宏的操作机制;因此,我们应该了解此“加载”方法是什么以及为什么声明变量。

官方文档说,load是一种特殊的方法,即使在调用主函数之前,也要在应用程序启动的早期为每个类(特别是该类,而不是其实例)调用一次。 应用程序启动时的Objective-c(运行时)运行时环境将注册将在应用程序操作期间使用的所有类,并将在其上调用load方法(如果已实现)。 事实证明,甚至在应用程序中任何代码开始之前,变量AppControllerClassName都将添加到您的类中。

然后您可能会想:“如果在方法内部声明了该变量,并且在退出该方法时将其从内存中删除,那么拥有此变量有什么意义呢?” 这个问题的答案超出了Objective-C的范围。

C ++在哪里?


让我们再看一下该变量的声明

 extern const char* AppControllerClassName; 

在此声明中唯一可能无法理解的是extern修饰符。 如果您尝试在纯Objective-C中使用此修饰符,则Xcode将引发错误。 事实是,此修饰符不是Objective-C的一部分;它是用C ++实现的。 通过说它是“带类的C语言”,可以很简洁地描述Objective-C。 它是C语言的扩展,并允许无限制地使用散布在Objective-C代码中的C代码。

但是,要使用extern和其他C ++功能,您需要做一些技巧-使用Objective-C ++。 实际上,由于只有Objective-C代码可以插入C ++代码,因此实际上没有关于该语言的信息。 为了使编译器考虑应将某些源文件编译为Objective-C ++,而不是Objective-C,只需将此文件的扩展名从.m更改为.mm即可

extern修饰符本身用于声明全局变量。 更准确地说,告诉编译器“相信我,存在这样一个变量,但是它的内存不是在这里分配的,而是在另一个源中分配的。 我保证,她也很有价值。” 因此,我们的代码行仅创建了一个全局变量,并将自定义类的名称存储在其中。 剩下的只是要了解可以在哪里使用此变量。

回到主要


我们回想起之前所说的-通过指定类名称来创建应用程序委托。 如果委托是使用常规Xcode项目模板中的常数[myClass class]创建的,则显然,来自Unity的人决定应将此值包装在变量中。 使用科学的戳方法,我们采用Unity3D生成的Xcode项目,并转到main.mm文件。

在其中,我们看到了比以前更复杂的代码,由于不必要而缺少了部分代码:

 // WARNING: this MUST be c decl (NSString ctor will be called after +load, so we cant really change its value) const char* AppControllerClassName = "UnityAppController"; int main(int argc, char* argv[]) { ... UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]); } return 0; } 

在这里,我们看到了这个非常变量的声明,并在其帮助下创建了应用程序委托。
如果我们创建了一个自定义委托,那么必需的变量就已经存在并且已经很重要了-我们类的名称。 在main函数之前声明和初始化变量可确保其具有默认值-UnityAppController。

现在,有了这个决定,一切都应该很清楚了。

宏观问题


当然,对于绝大多数情况,使用此宏是一个很好的解决方案。 但是,值得注意的是其中存在很大的陷阱:您不能有多个自定义委托。 发生这种情况的原因是,如果有2个或更多类使用IMPL_APP_CONTROLLER_SUBCLASS宏(ClassName),则将为它们中的第一个分配我们所需变量的值,而其他分配将被忽略。 并且此变量是一个字符串,也就是说,它不能被分配多个值。

您可能会认为此问题是退化的,在实践中不太可能。 但是,如果确实没有发生这样的问题,即使在非常奇怪的情况下,也不会发生本文。 情况可能如下。 您有一个使用许多分析和广告服务的项目。 其中许多服务都具有Objective-C组件。 它们已经存在于您的项目中很长时间了,您不知道它们带来的麻烦。 在这里,您需要编写一个自定义委托。 您使用了一个神奇的宏,该宏旨在使您免于出现问题,构建项目并获得有关装配成功的报告。 在设备上运行项目,您的功能不起作用,并且您没有收到任何错误。

可能是广告或分析插件之一使用了相同的宏。 例如,在来自AppsFlyer的插件中, 使用了此宏。

如果有多个声明,则extern变量的值是什么?


有趣的是,如果在多个文件中声明了相同的extern变量,并以宏的方式(在load方法中)对其进行了初始化,那么我们如何才能理解该变量将采用的值呢? 为了理解该模式,创建了一个简单的测试应用程序,其代码可以在此处找到。

该应用程序的本质很简单。 有A和B两个类,在这两个类中都声明了外部变量AexternVar,并为其分配了一个特定值。 类中变量的值设置不同。 在主函数中,记录了此变量的值。 通过实验发现,变量的值取决于将源添加到项目中的顺序。 在应用程序执行期间,Objective-C运行时注册类的顺序取决于此。 如果要重复实验,请打开项目,然后在项目设置中选择“构建阶段”选项卡。 由于该项目是经过测试的小型项目,因此只有8个源代码。 所有这些都显示在“编译源”列表中的“构建阶段”选项卡上。



如果在此列表中,类A的来源高于类B的来源,则该变量将采用类B的值。否则,该变量将采用类A的值。

试想一下,从理论上讲,这会引起多少问题,这是很小的差别。 尤其是如果项目规模巨大,会自动生成并且您不知道在哪个类中声明了此类变量。

解决问题


在本文的前面,曾说过Objective-C将为C#Reflection抢先一步。 具体来说,要解决我们的问题,您可以使用一种称为Method Swizzling的机制。 这种机制的本质是,我们有机会在应用程序中将任何类的方法的实现替换为另一种方法的实现。 因此,我们可以使用自定义方法替换UnityAppController中感兴趣的方法。 我们采用现有的实现并补充所需的代码。 我们正在编写将所需方法替换为现有方法实现的代码。 在应用程序的工作期间,使用宏的委托将像以前一样工作,调用UnityAppController的基本实现,然后我们的自定义方法将起作用,我们将达到预期的结果。 本文对这种方法进行了很好的编写和说明。 使用这种技术,我们可以使一个辅助类成为自定义委托的类似物。在该类中,我们将编写所有自定义代码,从而使自定义类成为一种包装器,以调用其他类的功能。这种方法可以使用,但是由于很难跟踪替换该方法的位置以及它将导致的后果,因此它是非常隐式的。

问题的另一种解决方案


发生的问题的主要方面是有很多自定义委托,或者您只能有一个,或者用第二个代替它。同时,无法确保自定义委托的代码不会蔓延到不同的源文件中。事实证明,当应用程序中只有一个委托时,可以将这种情况视为一种参考,您需要能够创建任意数量的自定义类,而这些类中没有一个使用宏来避免问题。

事情很小,剩下的事情就是确定如何使用Unity3D做到这一点,同时保留使用构建机器构建项目的能力。解决方案算法如下:

  1. 我们以所需的数量编写自定义委托,将插件的逻辑划分为不同的类,遵守SOLID的原理,而不求助于复杂性。
  2. UnityAppController XCode . UnityAppController .
  3. UnityAppController Unity .
  4. XCode UnityAppController ,

此列表中最困难的项目无疑是最后一个。但是,可以使用后处理构建脚本在Unity3D中实现此功能。这样的脚本是在一个美丽的夜晚编写的,您可以在GitHub上观看

此后处理非常易于使用,请在Unity项目中选择它。在“检查器”窗口中查看,并在其中看到一个名为NewDelegateFile的字段。将修改后的UnityAppController拖放到此字段中并保存。



在构建iOS项目时,标准代表将被修改后的代表代替,并且不需要人工干预。现在,当向项目添加新的自定义委托时,您只需要修改Unity项目中的UnityAppController选项即可。

聚苯乙烯


感谢最终的所有人,这篇文章真的很长。我希望所画的信息对您有所帮助。

Source: https://habr.com/ru/post/zh-CN430118/


All Articles