Unity3D: Modificar delegado de aplicación de iOS

Creo que muchos en el proceso de desarrollo de un juego para iOS tuvieron que enfrentar el hecho de que se hace necesario usar una u otra funcionalidad nativa. Con respecto a Unity3D, pueden surgir muchos problemas en este tema: para implementar algún tipo de característica, debe buscar complementos nativos escritos en Objective-C. Alguien en este momento inmediatamente se desespera y abandona la idea. Alguien está buscando soluciones preparadas en AssetStore o en foros, esperando que ya exista una solución preparada. Si no hay soluciones preparadas, entonces los más persistentes de nosotros no vemos otra manera que sumergirnos en el abismo de la programación de iOS y la interacción de Unity3D con el código Objective-C.

Quienes elijan el último camino (aunque, creo, ellos mismos lo saben), enfrentarán muchos problemas en este camino difícil y espinoso:

  • iOS es un ecosistema absolutamente desconocido y aislado, que se desarrolla a su manera. Como mínimo, tendrá que pasar mucho tiempo para comprender cómo puede llegar a la aplicación y en qué partes del proyecto Xcode generado automáticamente se encuentra el código para que el motor Unity3D interactúe con el componente nativo de la aplicación.
  • Objective-C es un lenguaje de programación bastante separado y poco parecido. Y cuando se trata de interactuar con el código C ++ de la aplicación Unity3D, el "dialecto" de este lenguaje, llamado Objective-C ++, entra en escena. Hay muy poca información sobre él, la mayor parte es antigua y de archivo.
  • El protocolo de interacción entre la aplicación Unity3D y iOS está mal descrito. Debe confiar únicamente en los tutoriales de los entusiastas de la red que escriben cómo desarrollar el complemento nativo más simple. Al mismo tiempo, pocas personas tocan cuestiones más profundas y problemas derivados de la necesidad de hacer algo complicado.

Aquellos que quieran aprender sobre los mecanismos de interacción de Unity3D con una aplicación iOS, por favor, bajo cat.

Con el fin de aportar más claridad al estrecho cuello de botella de la interacción de Unity3D con el código nativo, este artículo describe los aspectos de interacción de un delegado de aplicación iOS con código Unity3D, con el que se implementan las herramientas C ++ y Objective-C, y cómo modificar la aplicación delega usted mismo. Esta información puede ser útil tanto para una mejor comprensión de los mecanismos de vinculación de Unity3D + iOS como para un uso práctico.

Interacción entre iOS y la aplicación.


Como introducción, veamos cómo se implementa la interacción de la aplicación con el sistema en iOS y viceversa. Esquemáticamente, el lanzamiento de una aplicación iOS se ve así:

imagen

Para estudiar este mecanismo desde el punto de vista del código, es adecuada una nueva aplicación creada en Xcode utilizando la plantilla "Aplicación de vista única".



Al elegir esta plantilla, la salida le dará la aplicación de iOS más simple que puede ejecutarse en un dispositivo o emulador y mostrar una pantalla en blanco. Xcode creará un proyecto útil en el que solo habrá 5 archivos con código fuente (2 de ellos serán archivos de encabezado .h) y varios archivos auxiliares que no nos interesan (composición tipográfica, configuraciones, iconos).



Veamos de qué son responsables los archivos de código fuente:

  • ViewController.m / ViewController.h : códigos fuente no muy interesantes para nosotros. Dado que su aplicación tiene una Vista (que no está representada por código, sino que usa el Storyboard), necesitará la clase Controlador, que controlará esta Vista. En general, de esta manera Xcode nos anima a usar el patrón MVC. El proyecto que genera Unity3D no tendrá estos archivos fuente.
  • AppDelegate.m / AppDelegate.h es el delegado de su aplicación. El punto de interés en la aplicación donde comienza el trabajo del código de aplicación personalizado.
  • main.m : el punto de partida de la aplicación. A la manera de cualquier aplicación C / C ++, contiene la función principal con la que se inicia el programa.

Ahora, veamos el código que comienza con el archivo main.m :

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

Con la línea 1, todo está claro y sin explicación, pasemos a la línea 2. Indica que el ciclo de vida de la aplicación ocurrirá dentro del grupo de Liberación automática. El uso del grupo de liberación automática nos dice que confiaremos la administración de memoria de la aplicación a este grupo en particular, es decir, tratará los problemas cuando sea necesario liberar memoria para una variable en particular. La historia sobre la administración de memoria en iOS está más allá del alcance de esta historia, por lo que no tiene sentido profundizar en este tema. Para aquellos que estén interesados ​​en este tema, pueden encontrar, por ejemplo, este artículo .

Pasemos a la línea 3. Llama a la función UIApplicationMain . Se le pasan los parámetros de inicio del programa (argc, argv). Luego, en esta función, se indica qué clase usar como la clase principal de la aplicación, se crea su instancia. Y, finalmente, se indica qué clase usar como delegado de la aplicación, se crea su instancia, se configuran las conexiones entre la instancia de la clase de aplicación y su delegado.

En nuestro ejemplo, nil se pasa como la clase que representará la instancia de la aplicación; en términos generales, el análogo local es nulo. Además de nil, puede pasar una clase específica heredada de UIApplication allí . Si se especifica nil, se usará la aplicación UIA. Esta clase es un punto centralizado para administrar y coordinar el trabajo de una aplicación en iOS y es un singleton. Con él, puede aprender casi todo sobre el estado actual de la aplicación, notificaciones, ventanas, eventos que ocurrieron en el propio sistema que afectan a esta aplicación y mucho más. Esta clase casi nunca hereda. Nos detendremos en la creación de la clase Delegado de aplicaciones con más detalle.

Crear delegado de aplicación


Una indicación de qué clase usar como delegado de la aplicación ocurre en una llamada de función

 NSStringFromClass([AppDelegate class]) 

Analicemos esta llamada en partes.

 [AppDelegate class] 

Esta construcción devuelve un objeto de la clase AppDelegate (que se declara en AppDelegate.h / .m), y la función NSStringFromClass devuelve el nombre de la clase como una cadena. Simplemente pasamos el nombre de cadena de la clase que se creará y utilizará como delegado a la función UIApplicationMain. Para una mejor comprensión, la línea 3 en el archivo main.m podría reemplazarse por lo siguiente:

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

Y el resultado de su implementación sería idéntico a la versión original. Aparentemente, los desarrolladores decidieron adoptar este enfoque para no usar una constante de cadena. Con un enfoque estándar, si cambia el nombre de una clase delegada, el analizador arrojará inmediatamente un error. En el caso de utilizar la línea habitual, el código se compilará correctamente y recibirá un error solo al iniciar la aplicación.

Un mecanismo similar para crear una clase, usando solo el nombre de la cadena de la clase, puede recordarle Reflection from C #. Objective-C y su tiempo de ejecución son mucho más poderosos que Reflection en C #. Este es un punto bastante importante en el contexto de este artículo, pero tomaría mucho tiempo describir todas las características. Sin embargo, todavía nos encontraremos con "Reflexión" en el Objetivo-C a continuación. Queda por comprender el concepto del delegado de la aplicación y sus funciones.

Delegado de aplicaciones


Toda interacción de la aplicación con iOS ocurre en la clase UIApplication. Esta clase asume muchas responsabilidades: notifica sobre el origen de los eventos, el estado de la aplicación y mucho más. En su mayor parte, su papel es notificar. Pero cuando algo sucede en el sistema, deberíamos poder responder de alguna manera a este cambio, para realizar algún tipo de funcionalidad personalizada. Si una instancia de la clase UIApplication hace esto, esta práctica comenzará a parecerse a un enfoque llamado Objeto Divino . Por lo tanto, vale la pena pensar en liberar a esta clase de parte de sus responsabilidades.

Es para estos fines que el ecosistema de iOS usa algo como un delegado de aplicación. A partir del nombre en sí, podemos concluir que estamos tratando con un patrón de diseño como Delegación . En resumen, simplemente transferimos la responsabilidad del procesamiento de la respuesta a ciertos eventos de la aplicación al delegado de la aplicación. Para este propósito, en nuestro ejemplo, se creó la clase AppDelegate en la que podemos escribir funcionalidades personalizadas, mientras que la clase UIApplication funciona en modo de recuadro negro. Este enfoque puede parecer controvertido para alguien en términos de la belleza del diseño de la arquitectura, pero los propios autores de iOS nos están empujando a este enfoque y la gran mayoría de los desarrolladores (si no todos) lo usan.

Para verificar visualmente con qué frecuencia durante el trabajo de la aplicación, el delegado de la aplicación recibe un mensaje en particular, eche un vistazo al diagrama:

imagen

Los rectángulos amarillos indican las llamadas de uno u otro método delegado en respuesta a ciertos eventos de la vida de la aplicación (ciclo de vida de la aplicación). Este diagrama ilustra solo eventos relacionados con cambios en el estado de la aplicación y no refleja muchos otros aspectos de la responsabilidad del delegado, como aceptar notificaciones o interactuar con marcos.

Estos son algunos ejemplos en los que podríamos necesitar acceso a un delegado de aplicaciones de Unity3D:

  1. manejo de notificaciones push y locales
  2. Registro de eventos de lanzamiento de aplicaciones para análisis
  3. determinación de cómo iniciar la aplicación: "limpiar" o salir del fondo
  4. cómo se lanzó la aplicación: mediante tach para notificación, usando acciones rápidas de la pantalla de inicio o simplemente tach en incon
  5. interacción con WatchKit o HealthKit
  6. abrir y procesar URL desde otra aplicación. Si esta URL se aplica a su aplicación, puede procesarla en su aplicación en lugar de permitir que el sistema abra esa URL en un navegador

Esta no es la lista completa de escenarios. Además, vale la pena señalar que el delegado modifica muchos sistemas de análisis y publicidad en sus complementos nativos.

Cómo Unity3D implementa un delegado de aplicación


Veamos ahora el proyecto Xcode generado por Unity3D y descubramos cómo se implementa el delegado de aplicaciones en Unity3D. Al compilar para la plataforma iOS, Unity3D genera automáticamente un proyecto Xcode para usted, que utiliza una gran cantidad de código repetitivo. Este código de plantilla también incluye el código de delegado de aplicación. Dentro de cualquier proyecto generado, puede encontrar los archivos UnityAppController.h y UnityAppController.mm . Estos archivos contienen el código de la clase UnityAppController que nos interesa.

De hecho, Unity3D utiliza una versión modificada de la plantilla "Aplicación de vista única". Solo en esta plantilla, Unity3D usa el delegado de la aplicación no solo para manejar eventos de iOS, sino también para inicializar el motor, preparar componentes gráficos y mucho más. Esto es muy fácil de entender si nos fijamos en el método.

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

en el código de la clase UnityAppController. Este método se llama en el momento de la inicialización de la aplicación, cuando puede transferir el control a su código personalizado. Dentro de este método, por ejemplo, puede encontrar las siguientes líneas:

 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]; 

Sin siquiera entrar en detalles sobre lo que hacen estos desafíos, puede adivinar que están relacionados con la preparación de Unity3D para el trabajo. Resulta el siguiente escenario:

  1. La función principal se llama desde main.mm
  2. Se crean las clases de instancia de la aplicación y su delegado.
  3. El delegado de la aplicación prepara y lanza el motor Unity3D
  4. Tu código personalizado comienza a funcionar. Si usa il2cpp, su código se traduce de C # a IL y luego al código C ++, que ingresa directamente al proyecto Xcode.

Este script suena bastante simple y lógico, pero trae consigo un problema potencial: ¿cómo podemos modificar el delegado de la aplicación si no tenemos acceso al código fuente cuando trabajamos en Unity3D?

Unity3D afectado para modificar el delegado de la aplicación


Podemos echar un vistazo a los archivos AppDelegateListener.mm/.h . Contienen macros que le permiten registrar cualquier clase como escucha de eventos para el delegado de la aplicación. Este es un buen enfoque, no necesitamos modificar el código existente, sino simplemente agregar uno nuevo. Pero tiene un inconveniente importante: no todos los eventos de aplicaciones son compatibles y no hay forma de obtener información sobre el inicio de la aplicación.

La salida más obvia, sin embargo, inaceptable es cambiar el código fuente del delegado a mano después de que Unity3D construya el proyecto Xcode. El problema con este enfoque es obvio: la opción es adecuada si realiza ensamblajes con las manos y no le confunde la necesidad de modificar el código manualmente después de cada ensamblaje. En el caso de usar constructores (Unity Cloud Build o cualquier otra máquina de compilación), esta opción es absolutamente inaceptable. Para estos propósitos, los desarrolladores de Unity3D nos dejaron un vacío legal.

El archivo UnityAppController.h , además de declarar variables y métodos, también contiene una definición de macro:

 #define IMPL_APP_CONTROLLER_SUBCLASS(ClassName) ... 

Esta macro solo permite anular el delegado de la aplicación. Para hacer esto, debe seguir algunos pasos simples:

  1. Escriba su propio delegado de aplicación en Objective-C
  2. En algún lugar dentro del código fuente, agregue la siguiente línea
     IMPL_APP_CONTROLLER_SUBCLASS(___) 
  3. Coloque esta fuente dentro de la carpeta Plugins / iOS de su proyecto Unity3D

Ahora recibirá un proyecto en el que el delegado de la aplicación Unity3D estándar será reemplazado por uno personalizado.

¿Cómo funciona la macro de reemplazo de delegado?


Veamos el código fuente completo de la macro:

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

El uso de esta macro en su fuente agregará el código descrito en la macro al cuerpo de su fuente en la etapa de compilación. Esta macro hace lo siguiente. Primero, agregará el método de carga a la interfaz de su clase. Una interfaz en el contexto de Objective-C puede considerarse como una colección de campos y métodos públicos. En C #, aparecerá un método de carga estática en su clase que no devuelve nada. A continuación, la implementación de este método de carga se agregará al código de su clase. En este método, se declarará la variable AppControllerClassName, que es una matriz de tipo char y luego se le asignará un valor a esta variable. Este valor es el nombre de la cadena de su clase. Obviamente, esta información no es suficiente para comprender el mecanismo de operación de esta macro, por lo tanto, debemos entender qué es este método de "carga" y por qué se declara una variable.

La documentación oficial dice que cargar es un método especial que se llama una vez para cada clase (específicamente la clase, no sus instancias) en la etapa inicial del lanzamiento de la aplicación, incluso antes de que se llame a la función principal. El entorno de tiempo de ejecución Objective-c (tiempo de ejecución) al inicio de la aplicación registrará todas las clases que se utilizarán durante la operación de la aplicación y llamará al método de carga, si se implementa. Resulta que incluso antes del inicio de cualquier código en nuestra aplicación, la variable AppControllerClassName se agregará a su clase.

Entonces podría pensar: "¿Y cuál es el punto de tener esta variable si se declara dentro del método y se eliminará de la memoria cuando salga de este método?". La respuesta a esta pregunta se encuentra un poco más allá de los límites de Objective-C.

¿Y dónde está C ++?


Echemos otro vistazo a la declaración de esta variable.

 extern const char* AppControllerClassName; 

Lo único que puede ser incomprensible en esta declaración es el modificador externo. Si intenta utilizar este modificador en Objective-C puro, Xcode arrojará un error. El hecho es que este modificador no es parte de Objective-C; se implementa en C ++. Objective-C puede describirse de manera muy sucinta diciendo que es "lenguaje C con clases". Es una extensión del lenguaje C y permite el uso ilimitado del código C intercalado con el código Objective-C.

Sin embargo, para usar funciones externas y otras de C ++, debe hacer algún truco: use Objective-C ++. Prácticamente no hay información sobre este lenguaje, debido al hecho de que es solo el código Objective-C que permite la inserción del código C ++. Para que el compilador considere que algún archivo fuente debe compilarse como Objective-C ++, y no Objective-C, solo necesita cambiar la extensión de este archivo de .m a .mm .

El modificador externo en sí mismo se usa para declarar una variable global. Más precisamente, para decirle al compilador "Créeme, tal variable existe, pero la memoria para ella no fue asignada aquí, sino en otra fuente. Y ella también tiene un valor, te lo garantizo. Por lo tanto, nuestra línea de código simplemente crea una variable global y almacena el nombre de nuestra clase personalizada en ella. Solo queda entender dónde se puede usar esta variable.

Volver a la página principal


Recordamos lo que se dijo anteriormente: el delegado de la aplicación se crea especificando el nombre de la clase. Si en una plantilla Xcode de proyecto normal se creó un delegado utilizando el valor constante [clase myClass], entonces, aparentemente, los chicos de Unity decidieron que este valor debería estar envuelto en una variable. Usando el método de empuje científico, tomamos el proyecto Xcode generado por Unity3D y vamos al archivo main.mm.

En él vemos un código más complejo que antes, falta parte de este código como innecesario:

 // 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; } 

Aquí vemos la declaración de esta muy variable, y la creación del delegado de la aplicación con su ayuda.
Si creamos un delegado personalizado, entonces la variable necesaria existe y ya importa: el nombre de nuestra clase. Declarar e inicializar la variable antes de la función principal garantiza que tenga un valor predeterminado: UnityAppController.

Ahora con esta decisión todo debería quedar muy claro.

Problema macro


Por supuesto, para la gran mayoría de las situaciones, usar esta macro es una gran solución. Pero vale la pena señalar que hay un gran obstáculo: no puede tener más de un delegado personalizado. Esto sucede porque si 2 o más clases usan la macro IMPL_APP_CONTROLLER_SUBCLASS (ClassName), entonces para la primera de ellas se asignará el valor de la variable que necesitamos y se ignorarán otras asignaciones. Y esta variable es una cadena, es decir, no se le puede asignar más de un valor.

Puede pensar que este problema es degenerado y poco probable en la práctica. Pero este artículo no habría sucedido si tal problema no hubiera ocurrido realmente, e incluso en circunstancias muy extrañas. La situación puede ser la siguiente. Tiene un proyecto en el que utiliza muchos servicios de análisis y publicidad. Muchos de estos servicios tienen componentes Objective-C. Han estado en su proyecto durante mucho tiempo y no conoce los problemas con ellos. Aquí debe escribir un delegado personalizado. Utiliza una macro mágica diseñada para salvarte de problemas, crear un proyecto y obtener un informe sobre el éxito del ensamblaje. Ejecute el proyecto en el dispositivo y su funcionalidad no funcionará y no recibirá un solo error.

Y puede ser que uno de los complementos de publicidad o análisis use la misma macro. Por ejemplo, en el complemento de AppsFlyer se usa esta macro.

¿Cuál es el valor de la variable externa en el caso de declaraciones múltiples?


Es interesante entender si la misma variable externa se declara en varios archivos y se inicializan a la manera de nuestra macro (en el método de carga), entonces ¿cómo podemos entender qué valor tomará la variable? Para comprender el patrón, se creó una aplicación de prueba simple, cuyo código se puede encontrar aquí .

La esencia de la aplicación es simple. Hay 2 clases A y B, en ambas clases se declara la variable externa AexternVar, se le asigna un valor específico. Los valores de la variable en las clases se establecen de manera diferente. En la función principal, se registra el valor de esta variable. Se encontró experimentalmente que el valor de la variable depende del orden en que se agregan las fuentes al proyecto. El orden en que el tiempo de ejecución de Objective-C registra las clases durante la ejecución de la aplicación depende de esto. Si desea repetir el experimento, abra el proyecto y seleccione la pestaña Construir fases en la configuración del proyecto. Como el proyecto es de prueba y pequeño, solo tiene 8 códigos fuente. Todos ellos están presentes en la pestaña Fases de compilación en la lista de fuentes de compilación.



Si en esta lista la fuente de la clase A es más alta que la fuente de la clase B, entonces la variable tomará un valor de la clase B. De lo contrario, la variable tomará un valor de la clase A.

Solo imagine cuántos problemas puede causar esto teóricamente es un pequeño matiz. Especialmente si el proyecto es enorme, se genera automáticamente y no sabe en qué clases se declara dicha variable.

Resolución de problemas


Anteriormente en el artículo, se dijo que Objective-C dará un buen comienzo a C # Reflection. Específicamente, para resolver nuestro problema, puede usar el mecanismo llamado Método Swizzling . La esencia de este mecanismo es que tenemos la oportunidad de reemplazar la implementación de un método de cualquier clase con otro durante la aplicación. Por lo tanto, podemos reemplazar el método de interés en UnityAppController con uno personalizado. Tomamos la implementación existente y complementamos el código que necesitamos. Estamos escribiendo código que reemplaza la implementación existente del método con la que necesitamos. Durante el trabajo de la aplicación, el delegado que usa la macro funcionará como antes, invocando la implementación básica de UnityAppController, y allí nuestro método personalizado entrará en juego y lograremos el resultado deseado. Este enfoque está bien escrito e ilustrado en este artículo . Con esta técnica, podemos hacer una clase auxiliar, un análogo de un delegado personalizado.En esta clase, escribiremos todo el código personalizado, haciendo de la clase personalizada una especie de Wrapper para llamar a la funcionalidad de otras clases. Este enfoque funcionará, pero es extremadamente implícito debido al hecho de que es difícil rastrear dónde se reemplaza el método y qué consecuencias tendrá.

Otra solución al problema.


El aspecto principal del problema que sucedió es que hay muchos delegados personalizados, o solo puede tener uno, o reemplazarlo parcialmente por un segundo. Al mismo tiempo, no hay forma de asegurarse de que el código de los delegados personalizados no se arrastre a diferentes archivos de origen. Resulta que la situación se puede considerar como una referencia cuando solo hay un delegado en la aplicación, debe poder crear clases personalizadas tantas como desee, mientras que ninguna de estas clases usa la macro para evitar problemas.

La cosa es pequeña, queda por determinar cómo se puede hacer esto usando Unity3D, mientras se deja la capacidad de construir un proyecto usando una máquina de construcción. El algoritmo de solución es el siguiente:

  1. Escribimos delegados personalizados en la cantidad requerida, dividiendo la lógica de los complementos en diferentes clases, observando los principios de SOLID y no recurriendo a la sofisticación.
  2. UnityAppController XCode . UnityAppController .
  3. UnityAppController Unity .
  4. XCode UnityAppController ,

El elemento más difícil de esta lista es, sin duda, el último. Sin embargo, esta característica se puede implementar en Unity3D utilizando el script de compilación posterior al proceso. Tal guión fue escrito una noche hermosa, puedes verlo en GitHub .

Este postproceso es bastante fácil de usar, elíjalo en un proyecto de Unity. Mire en la ventana del Inspector y vea un campo llamado NewDelegateFile. Arrastre y suelte su UnityAppController modificado en este campo y guárdelo.



Al construir un proyecto de iOS, el delegado estándar será reemplazado por uno modificado, y no se requiere intervención manual. Ahora, al agregar nuevos delegados personalizados al proyecto, solo necesita modificar la opción UnityAppController en su proyecto Unity.

PS


Gracias a todos los que llegaron al final, el artículo realmente resultó ser extremadamente largo. Espero que la información pintada sea útil.

Source: https://habr.com/ru/post/es430118/


All Articles