美文网首页
Unity 集成到原生工程——iOS篇

Unity 集成到原生工程——iOS篇

作者: 太刀 | 来源:发表于2020-12-26 18:23 被阅读0次

    0. iOS 应用程序基本架构

    在此之前,笔者并未从事过 iOS 原生开发,以下都是在分析 Unity Xcode 工程时做的简单总结,不保证准确性和完整性。

    0.0 UIApplication

    一个 iOS 的应用程序关联一个 UIApplication 实例,用来管理和协调其它的实例,通过 UIApplication 可以获取和操作 AppDelegate 和 Window 对象。

    0.2 UIApplicationDelegate

    顾名思义,这是 UIApplication 的代理,UIApplication 通过 UIApplicationDelegate 来履行职责,比如创建窗口和 View,添加View,管理应用的生命周期等。

    0.3 UIView 和 UIViewController

    UIView 是所有UI 控件的基类,UIView对象负责屏幕上一个区域的显示样式如颜色、大小,以及动作等。UIViewController 负责 UIView 的创建、加载与卸载。

    0.4 UIWindow

    UIWindow 是一个视图控件,用来承载 UIView,将 UIView显示到屏幕上面,可以通过 addSubView 的方法将 view 添加到 window 中,或者设置 window 的 rootViewController,添加该 ViewController 对应的 view。UIApplication 中有多个 window,通过 makeKeyAndVisable 可以将 window 设置为 keyWindow。

    1. Unity导出XCode工程分析

    我们来整理一下 Unity XCode 工程的结构

    1.0 XCode 工程整理结构

    新建一个空 Unity 工程和空场景,在PlayerSettings中切换到 iOS 平台,使用 il2cpp 的模式导出,得到对应的XCode工程目录结构如图:

    Unity XCode工程结构图
    • Data 目录
      包含了序列化后的场景和场景资源,名字诸如level以及level.resS;
      子目录 Resources 包含了Unity工程中 Resources 目录下的资源序列化结果;
      子目录 Managed 包含了 .NET 程序集数据(dll/dat文件), machine.config 文件包含各种 .NET 服务设置。在每次重新构建时,Data目录都会被刷新。

    • Classes 目录
      包含了几乎所有 Unity 生成的源代码。在PlayerSettings不变的情况下重新导出,将只有 Native 子目录会被刷新,其它目录不会发生变化。

    • Frameworks 目录
      包含工程用到的所有 .framework 库文件,重新导出不会刷新这个目录。

    • Libraries 目录
      包含静态库文件 libil2cpp.a 和 libiPhone-lib.a, 以及将 Unity 本机代码与 .NET 绑定的 RegisterMonoModules.h/cpp,每次重新导出都将刷新这个目录

    • Products 目录
      存放工程构建结果.app文件


    1.1 Classes 目录

    列举一下 Classes 目录下比较重要的文件和子目录

    • main.mm
      应用程序的入口点,关键代码如下
    const char* AppControllerClassName = "UnityAppController";
    
    int main(int argc, char* argv[])
    {
    #if UNITY_USES_DYNAMIC_PLAYER_LIB
        SetAllUnityFunctionsForDynamicPlayerLib();
    #endif
    
        UnityInitStartupTime();
        @autoreleasepool
        {
            UnityInitTrampoline();
            UnityInitRuntime(argc, argv);
    
            RegisterMonoModules();
            NSLog(@"-> registered mono modules %p\n", &constsection);
            RegisterFeatures();
    
            // iOS terminates open sockets when an application enters background mode.
            // The next write to any of such socket causes SIGPIPE signal being raised,
            // even if the request has been done from scripting side. This disables the
            // signal and allows Mono to throw a proper C# exception.
            std::signal(SIGPIPE, SIG_IGN);
    
            UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]);
        }
    
        return 0;
    }
    
    • UnityAppController 工程中最重要最核心的一个类,管理 Unity Runtime 初始化、窗口和视图的创建、事件分发、生命周期管理、渲染API等重要功能。UnityAppController 被分成了以下几个子模块
    子模块 作用
    UnityAppController Runtime初始化,应用生命周期管理
    UnityAppController+Rendering 渲染
    UnityAppController+UnityInterface 提供暂停状态设置和查询
    UnityAppController+ViewHandling 创建splash和游戏视图,管理窗口旋转
    • Classes/UI 目录
      基本的界面控制,UnityAppController+ViewHandling在这个目录里面

    • Classes/Native 目录 ,Unity工程中的所有“业务代码”,在 il2cpp 阶段生成的cpp代码都存放在这个目录

    • Classes/Unity 目录 一些 iOS 平台特性相关接口,以及libiPhone-lib.a 中库方法的声明,比较重要的是 UnityInterface.h,包括 Unity 生命周期接口、传感器接口、分辨率/旋转处理接口等。贴一下其中我们比较关注的关于生命周期方法的声明:

    // life cycle management
    
    void    UnityInitStartupTime();
    void    UnityInitRuntime(int argc, char* argv[]);
    void    UnityInitApplicationNoGraphics(const char* appPathName);
    void    UnityInitApplicationGraphics();
    void    UnityCleanup();
    void    UnityLoadApplication();
    void    UnityPlayerLoop();                  // normal player loop
    void    UnityBatchPlayerLoop();             // batch mode like player loop, without rendering (usable for background processing)
    void    UnitySetPlayerFocus(int focused);   // send OnApplicationFocus() message to scripts
    void    UnityLowMemory();
    void    UnityPause(int pause);
    int     UnityIsPaused();                    // 0 if player is running, 1 if paused
    void    UnityWillPause();                   // send the message that app will pause
    void    UnityWillResume();                  // send the message that app will resume
    void    UnityInputProcess();
    void    UnityDeliverUIEvents();             // unity processing impacting UI will be called in there
    

    1.2 Unity 应用启动流程

    • main.mm 中指定启动 AppDelegate 为 UnityAppController:
    const char* AppControllerClassName = "UnityAppController";
    
    UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]);
    
    • UnityAppController.applicationdidFinishLaunchingWithOptions 方法中创建 window 和 view, 关键代码:
        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];
    

    UnityInitApplicationNoGraphics说明此处并未进行 Unity 图形相关的初始化,事实上这里只是创建了 splash 界面。[self createUI] 定义在 UnityAppController+ViewHandling 子模块中,具体实现大概是:

        _rootController = [self createRootViewController];
        
        [self willStartWithViewController: _rootController];
    
        [_window makeKeyAndVisible];
        [UIView setAnimationsEnabled: NO];
    
        ShowSplashScreen(_window);
    

    Unity 项目实际打开的时机在 UnityAppController.startUnity:

    - (void)startUnity:(UIApplication*)application
    {
        NSAssert(_unityAppReady == NO, @"[UnityAppController startUnity:] called after Unity has been initialized");
    
        UnityInitApplicationGraphics();
    
        // we make sure that first level gets correct display list and orientation
        [[DisplayManager Instance] updateDisplayListCacheInUnity];
    
        UnityLoadApplication();
        Profiler_InitProfiler();
    
        [self showGameUI];
        [self createDisplayLink];
    
        UnitySetPlayerFocus(1);
    }
    

    原生层创建了 Unity 的 window和 view 并显示,至于显示Unity场景、UI这些工作,已经是 Unity Runtime 自己去做的事情了,到这里我们已经知道 Unity 启动时在原生端的大致流程。

    Unity-XCode工程架构图

    1.3 生命周期回调

    我们知道 Unity 脚本的生命周期函数包括Awake(),Start(),Update()等,与切换前后台相关的主要是 OnApplicationPause()OnApplicationFocus(),这两个方法应该是在触发前后台切换时由原生传递给 Unity Runtime的,我们先看看 iOS AppDelegate的生命周期:

    AppDelegate生命周期

    看看工程中 UnityAppController 对应的生命周期方法中的关键代码:

    // 切回前台
    - (void)applicationDidBecomeActive:(UIApplication*)application
    {
        ::printf("-> applicationDidBecomeActive()\n");
    
        [self removeSnapshotView];
    
        if (_unityAppReady)
        {
            if (UnityIsPaused() && _wasPausedExternal == false)
            {
                UnityWillResume();
                UnityPause(0);
            }
            if (_wasPausedExternal)
            {
                if (UnityIsFullScreenPlaying())
                    TryResumeFullScreenVideo();
            }
            UnitySetPlayerFocus(1);
        }
        else if (!_startUnityScheduled)
        {
            _startUnityScheduled = true;
            [self performSelector: @selector(startUnity:) withObject: application afterDelay: 0];
        }
    
        _didResignActive = false;
    }
    
    // 切到后台
    - (void)applicationWillResignActive:(UIApplication*)application
    {
        ::printf("-> applicationWillResignActive()\n");
    
        if (_unityAppReady)
        {
            UnitySetPlayerFocus(0);
    
            _wasPausedExternal = UnityIsPaused();
            if (_wasPausedExternal == false)
            {
                // Pause Unity only if we don't need special background processing
                // otherwise batched player loop can be called to run user scripts.
                if (!UnityGetUseCustomAppBackgroundBehavior())
                {
                    // Force player to do one more frame, so scripts get a chance to render custom screen for minimized app in task manager.
                    // NB: UnityWillPause will schedule OnApplicationPause message, which will be sent normally inside repaint (unity player loop)
                    // NB: We will actually pause after the loop (when calling UnityPause).
                    UnityWillPause();
                    [self repaint];
                    UnityPause(1);
    
                    // this is done on the next frame so that
                    // in the case where unity is paused while going
                    // into the background and an input is deactivated
                    // we don't mess with the view hierarchy while taking
                    // a view snapshot (case 760747).
                    dispatch_async(dispatch_get_main_queue(), ^{
                        // if we are active again, we don't need to do this anymore
                        if (!_didResignActive)
                        {
                            return;
                        }
    
                        _snapshotView = [self createSnapshotView];
                        if (_snapshotView)
                            [_rootView addSubview: _snapshotView];
                    });
                }
            }
        }
    
        _didResignActive = true;
    }
    

    可以看出,在 UnityAppController 切前后台的方法中调用了 UnityPause(),UnitySetPlayerFocus(),对应了 Unity 中的 OnApplicationPause()UnityApplicationFocus()

    2. Cocos XCode工程分析

    这里使用的是 Cocos Creator 引擎,为方便叙述,下面就直接使用 Cocos 来替代了。

    2.0 XCode 目录结构分析

    同样的,新建一个空的cocos creator 项目并导出 XCode 工程,目录结构如下:


    cocos creator XCode 工程目录结构
    • cocos2d_libs.xcodeproj
      这是一个子工程,有些项目会把它编译成静态库引入到项目,这个子工程包含cocos 原生的全部核心模块,根据工程的目录名字大概可以知道包含了哪些内容:


      cocos-2d子工程结构

      其中的 platform/CCApplication.h 是应用的核心类。

    • Classes 目录中只有一个 jsb_module_register.cpp,看起来是 cocos 引擎注册 js 模块用的

    • Frameworks 库文件目录

    • Products .app 文件存放地

    • ios iOS平台相关代码,AppController 放在这里

    2.1 cocos 应用启动流程

    • main.m 中指定启动 AppDelegate 为 AppController
    int main(int argc, char *argv[]) {
        
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
        int retVal = UIApplicationMain(argc, argv, nil, @"AppController");
        [pool release];
        return retVal;
    }
    
    • AppController.applicationdidFinishLaunchingWithOptions 方法中创建 window 和 view, 关键代码:
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [[SDKWrapper getInstance] application:application didFinishLaunchingWithOptions:launchOptions];
        // Add the view controller's view to the window and display.
        float scale = [[UIScreen mainScreen] scale];
        CGRect bounds = [[UIScreen mainScreen] bounds];
        window = [[UIWindow alloc] initWithFrame: bounds];
        
        // cocos2d application instance
        app = new AppDelegate(bounds.size.width * scale, bounds.size.height * scale);
        app->setMultitouch(true);
        
        // Use RootViewController to manage CCEAGLView
        _viewController = [[RootViewController alloc]init];
    #ifdef NSFoundationVersionNumber_iOS_7_0
        _viewController.automaticallyAdjustsScrollViewInsets = NO;
        _viewController.extendedLayoutIncludesOpaqueBars = NO;
        _viewController.edgesForExtendedLayout = UIRectEdgeAll;
    #else
        _viewController.wantsFullScreenLayout = YES;
    #endif
        // Set RootViewController to window
        if ( [[UIDevice currentDevice].systemVersion floatValue] < 6.0)
        {
            // warning: addSubView doesn't work on iOS6
            [window addSubview: _viewController.view];
        }
        else
        {
            // use this method on ios6
            [window setRootViewController:_viewController];
        }
        
        [window makeKeyAndVisible];
        
        [[UIApplication sharedApplication] setStatusBarHidden:YES];
        [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(statusBarOrientationChanged:)
            name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
        
        //run the cocos2d-x game scene
        app->start();
        
        return YES;
    }
    

    app->start(); 开启了 cocos 游戏的主循环。 appcocos2d::Application 实例:

    class  AppDelegate : public cocos2d::Application
    

    cocos2d::Application 定义在子工程 cocos2d_lib.xcodeproj 中,比较重要的方法声明:

        // This class is useful for internal usage.
        static Application* getInstance() { return _instance; }
        
        Application(const std::string& name, int width, int height);
        virtual ~Application();
        
        virtual bool applicationDidFinishLaunching();
        virtual void onPause();
        virtual void onResume();
        
        inline void* getView() const { return _view; }
        inline std::shared_ptr<Scheduler> getScheduler() const { return _scheduler; }
        inline RenderTexture* getRenderTexture() const { return _renderTexture; }
        
        void runOnMainThread();
        
        void start();
        void restart();
        void end();
    
    • 以切后台为例,查看原生事件如何一步步透传到 cocos 的 js脚本

    切后台时,原生端 AppController.applicationWillResignActive 被执行

    - (void)applicationWillResignActive:(UIApplication *)application {
        /*
         Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
         Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
         */
        app->onPause();
        [[SDKWrapper getInstance] applicationWillResignActive:application];
    }
    

    继承自cocos2d::ApplicationAppDelegate对象 app 调用 EventDispatcher 派发切后台事件

    void AppDelegate::onPause()
    {
        EventDispatcher::dispatchOnPauseEvent();
    }
    

    分别对原生和 js 层派发事件

    void EventDispatcher::dispatchOnPauseEvent()
    {
        // dispatch to Native
        CustomEvent event;
        event.name = EVENT_ON_PAUSE;
        EventDispatcher::dispatchCustomEvent(event);
    
        // dispatch to JavaScript
        dispatchEnterBackgroundOrForegroundEvent("onPause");
    }
    

    看看针对 js 层派发事件的实现

    static void dispatchEnterBackgroundOrForegroundEvent(const char* funcName)
    {
        if (!se::ScriptEngine::getInstance()->isValid())
            return;
    
        se::AutoHandleScope scope;
        assert(_inited);
    
        se::Value func;
        __jsbObj->getProperty(funcName, &func);
        if (func.isObject() && func.toObject()->isFunction())
        {
            func.toObject()->call(se::EmptyValueArray, nullptr);
        }
    }
    

    3. 工程合并

    4. 接口互调和数据互传

    5. 坑和填坑

    参考文献:

    相关文章

      网友评论

          本文标题:Unity 集成到原生工程——iOS篇

          本文链接:https://www.haomeiwen.com/subject/fwoonktx.html