十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LayaNative源码编译全解析:从环境准备到Android/iOS集成

LayaNative源码编译全解析:从环境准备到Android/iOS集成 最开始想自己编译LayaNative源码纯粹是被官方预编译包逼的——项目里有个只在老版本引擎上开发的分支官方预编译包已经停更Android真机上出现诡异的黑屏和脚本回调丢失找官方支持也让先确认版本匹配但匹配后的包仍然有兼容问题。折腾了一周后我决定把LayaNative整个源码拉下来自己编译彻底搞明白这套运行时是怎么工作的。先说结论LayaNative源码编译这件事本质上不是敲几个命令就完事而是需要你对C运行时、NDK版本的匹配、iOS静态库链接顺序、以及LayaAir引擎的JS入口机制都有一定了解。如果你只是需要用LayaAir发布App官方预编译包大概率够用但如果你要做以下三件事之一——给运行时加原生扩展、调试引擎层渲染异常、或者维护一个长期不跟随官方版本更新的老项目——自己编译源码就是绕不开的必经之路。这篇文章会把我从环境准备、源码目录分析、Android/iOS双端编译到最终把产物回填进原生工程的全过程完整记录下来包括那些踩进去又爬出来的坑。适合有LayaAir开发基础、准备承接原生层定制工作的朋友参考。1. 为什么还要自己编译LayaNative预编译包和源码编译的差别1.1 LayaNative在LayaAir引擎里到底是什么角色要理解源码编译的必要性得先知道LayaNative在整套引擎生态里的位置。LayaAir引擎的核心是JavaScript/Typescript运行时的游戏逻辑而LayaNative是专门用于把这种H5游戏逻辑运行到原生App容器里的C层运行时。预编译包是官方已经编译好的静态库它做的事情相当于是把JavaScriptCoreiOS端或V8Android端封装起来再配合LayaAir引擎的WebGL渲染管线在原生视图上直接绘制。这套机制下你不需要关心底层如何创建WebView、如何桥接原生能力只需要引入库、设置入口、加载资源地址就能跑。但预编译包的问题恰恰在于黑盒。当你遇到这类问题——某个系统版本上触摸事件没有回调、网络请求的RSA加密算法不被支持、或者需要注入自己写的原生插件和游戏逻辑通信——官方包的内部实现你看不到也没法改只能通过LayaAir引擎层打补丁绕过。绕过一次两次还可以积攒多了整个项目的稳定性全看运气。1.2 预编译包解决不了的几种场景我整理了一下真正需要源码编译的通常是下面几种场景一个典型场景是引擎版本长期不更新的项目。公司业务线因为历史原因停留在LayaAir 2.1x版本而最新官方包只适配了更高的引擎版本直接替换包会引发脚本层API不兼容。此时自己拉取对应版本的LayaNative源码编译变成唯一能维持版本稳定同时修复运行时Bug的路径。第二个场景是深度原生定制需求。比如游戏内想直接调用原生层的人脸识别SDK、需要把热更新逻辑下沉到C层或者想修改渲染管线的初始化参数。不改LayaNative源码这些需求都要大幅牺牲性能去做JS和原生层的频繁通信。第三个场景比较隐蔽64位架构的兼容问题。旧版本LayaNative的预编译so在某些Android 10设备上加载时会崩溃必须通过重新编译并调整编译选项来规避。1.3 编译前先确认你的引擎版本对应关系这一步极其重要建议先于环境准备进行。LayaNative和LayaAir引擎的版本必须严格对应。对应不上最常见的现象是JS层引用了一个运行时没有实现的方法运行时报Can not find function或者干脆白屏。判断的方式很直接——打开LayaNative源码根目录里面会有固定的版本号文件或分支名。例如2.x源码的tag往往就叫2.13.0、2.12.2这类和引擎版本一致的名字3.x主要走支线管理也要找和引擎大版本相同的分支。如果你用的是LayaAir 2.13.0就只拉取LayaNative仓库里2.13.0的tag或对应分支不要顺手拉个master回来否则后续编译大概率白费功夫。确认版本后建议花十分钟把源码里的README和CHANGELOG通读一遍好的源码维护者会把当前分支的编译环境要求写在最前面LayaNative的仓库维护也保留了类似习惯。里面的信息比任何第三方教程都可靠。2. 源码拉取与工程结构先搞清LayaNative仓库里每块东西干什么2.1 仓库分支怎样选才不出问题LayaNative源码托管在GitHub上仓库名直接搜索LayaNative就能找到。拉取命令很简单但在执行之前必须确定要哪个分支。以我当时维护的2.x老项目为例直接拉取对应引擎的tag比拉分支更可靠。原因是分支会持续合入一些提交而这些提交不一定和当前引擎版本做过严格的回归测试tag则意味着官方已经为特定版本锁定了一个快照虽然可能只修Bug不加固新功能但至少不会引入莫名其妙的行为变化。拉取的参考命令如下git clone https://github.com/layabox/laya-native.git cd laya-native git fetch --tags git checkout 2.13.0提示拉取之后先看目录下有没有submodule或third_party目录如果有需要接着执行git submodule update --init --recursive否则第三方依赖缺失会在编译报错时绕一大圈才能定位到问题本源。2.2 目录结构里最该关注的是哪几个目录初次打开LayaNative源码根目录你会看到一堆文件和目录不用全理解先把下面这些认清楚就够了。build目录是整个编译的调度中心。里面通常放着各平台的构建脚本比如build_android.py、iOS工程入口文件以及编译产物的默认输出路径。这个目录的重要性在于它决定了你用什么方式把C代码变成动态库或静态库。src/core或类似命名的C源码目录是LayaNative运行时的核心实现包括JavaScript引擎的封装、渲染上下文管理、内存管理、网络模块等。你定制原生能力和排查崩溃时主要就看这块。platforms/android和platforms/ios分别存放了各平台的原生工程模板。Android端可能会有.so文件生成相关的构建配置iOS端则往往会有一个可直接被Xcode识别的工程文件或者Podspec描述。把LayaNative源码看成一套半成品Engine会更容易理解——它不是编译完自己运行的App而是等着被你的原生工程引用的依赖库。2.3 编译依赖工具清单编译之前先把工具链准备好。不同平台不一样这里列一个我在实际中验证过的最低要求Android平台需要JDK 8以上建议沿用项目原有JDK版本Android SDKplatform-tools和build-tools齐全NDK版本必须和源码要求的匹配常见历史版本为r17c或r21eCMake 3.10以上取决于NDK内自带版本Python 2或3取决于源码脚本的语法老源码很多用Python 2iOS平台需要macOS版本在Catalina以上太老的系统可能打不开高版本Xcode的工程格式Xcode版本不低于10实际经验是12以上比较稳如果能有一个Apple开发者证书最好模拟器调试时本地签名真机运行需要真机证书。依赖里最容易忽略的是第三方库。老版本LayaNative源码会依赖boost、cURL、openssl等库而官方编译脚本倾向于使用本地缓存或拉取固定commit。如果你发现自己编译的产物在运行某个网络请求功能时闪退十有八九是这些第三方库的编译选项没有对齐脚本预期。3. Android端编译NDK版本、第三方依赖与so产物的完整链路3.1 环境准备NDK版本的前置匹配是最大的坑Android端的编译我最大的心得体会是NDK版本选对项目成功一半。LayaNative这种C运行时代码横跨了很多年如果你直接用Android Studio最新默认的NDK r23以上版本去编老版本源码会立刻遇到一堆废弃API的报错——比如unlink函数找不到、clang的-stdc11选项和代码本身不兼容。要避免这个坑我的做法是先在源码里搜ndkVersion、ndk.dir或构建脚本里写死的NDK路径字符串一般会在build.gradle或local.properties相关配置中看到官方预期版本。如果搜不到直接参考源码目录自带的README或CI脚本。LayaNative的Android构建如果走的是build_android.py脚本脚本里连NDK的下载地址都写好了照着装不要自以为是地升级。NDK版本匹配完成后还需要留意API Level。老项目建议以targetSdkVersion不超过设备实际系统版本为前提编译时API级别不要比目标设备低太多否则会有兼容性问题。3.2 第三方库依赖boost、cURL、openssl的准备方式用脚本直接编译时依赖一般不需要手动处理。但如果你是要把LayaNative集成到已有Android工程里、并保留自己的Native层代码那么依赖的处理方式就变成关键了。LayaNative早期的Android构建走的是NDKr10d时代的工程组织方式第三方库用Android.mk来引用。以boost为例源码依赖boost/function.hpp、boost/shared_ptr.hpp等头文件库。如果这些文件缺失编译会在预处理阶段直接报找不到头文件。优先确认源码根目录是否存在third_party或external目录并且目录里是否已经有完整文件。如果目录存在但空着大概率是submodule没拉全先执行submodule命令。我见过一些朋友在github上下载zip包而不是用git clone结果submodule目录全空编译报错时完全不知道原因。如果目录里压根没有第三方库也不要急着去boost官网下载最新版往里面塞。这里有个经验LayaNative源码针对特定版本做过适配依赖库的版本也需要固定。通常源码里会有一个lock文件、一个*.md5sum或者构建脚本注释里面写明了用的版本号和下载地址按那个来。3.3 编译命令与产物输出位置当环境准备到位Android端编译就没那么玄乎了。LayaNative源码自带的构建入口分两种一种是Python脚本执行行为一般长这样cd $LAYA_NATIVE_HOME/build python build_android.py --target armeabi-v7a --ndk $ANDROID_NDK_HOME --sdk $ANDROID_SDK_HOME脚本内部会完成环境变量设置、NDK调用、第三方库编译、插件构建、最终打包链接等步骤。顺利的情况下编译成功后会看到类似下面的输出路径中出现.so文件out/armeabi-v7a/liblayaRuntime.so另一种是靠Android.mk直接接入你自己的工程。在一些项目里LayaNative源码会被作为module直接include进主工程的jni/Android.mk里。这种情况下编译命令就是你平时NDK开发用的ndk-build。cd $PROJECT_ROOT/jni ndk-build NDK_PROJECT_PATH. APP_BUILD_SCRIPTAndroid.mk NDK_APPLICATION_MKApplication.mk两种方式选哪一种取决于你的项目组织方式。如果只是想要一个纯净的运行时库用脚本方式最省心如果你既要运行时又要自己的Native扩展代码同一套ndk-build工程一起编会更高效。3.4 为什么abiFilters要先只留armeabi-v7a跑通Android设备常见的ABI有armeabi-v7a、arm64-v8a、x86、x86_64。初次编译时强烈建议只编armeabi-v7a先把链路跑通。为什么因为LayaNative这种大型C工程在switch ABI的时候不只是换一个.so那么简单。V8引擎在64位和32位下的垃圾回收参数、内存布局会有差异源码里可能需要通过宏定义或预编译分支做区隔第一次编64位可能会遇到一些只在64位架构下才暴露的编译错误。先用armeabi-v7a打通编译全链路验证产物的功能和稳定性再逐步增加arm64-v8a。官网预编译包经常是同时包含多种ABI的自编译时我们完全可以按需添加。对大部分中小游戏项目而言armeabi-v7a在绝大多数真机上的兼容性都足够x86模拟器需求完全可以在开发阶段用iOS模拟器代替。armeabi-v7a编译通过后在app模块的build.gradle里的abiFilters做如下限制避免把多余的so打进包内android { defaultConfig { ndk { abiFilters armeabi-v7a } } }千万别图省事不写abiFilters直接把自编译的so丢进jniLibs否则极容易出现编译了一个armeabi-v7a的so但apk却包含arm64-v8a的目录系统按高优先级选择64位加载时直接崩溃。4. iOS端编译Xcode工程和静态库生成的实际操作4.1 Xcode工程的入口不是随便openiOS端的LayaNative源码编译和Android端相比体验要舒服得多因为它天生就是为Xcode设计的。但入口文件不是一个普通的.xcodeproj就能直接拖进Xcode开编的。你会在platforms/ios或build/ios目录下找到类似layaNative.xcodeproj或LayaNative.xcworkspace的文件。如果看到的是.xcworkspace说明它带有CocoaPods管理的依赖需要先在目录下执行pod install然后打开生成的.xcworkspace而不是.xcodeproj。直接用.xcodeproj打开会缺失Pods依赖导致链接阶段始终报找不到JavaScriptCore或其它第三方符号的错误。4.2 编译配置架构、bitcode、签名打开Xcode工程后第一步是确认Scheme设置。在Xcode顶部Scheme选择器里选择Editor Scheme在Run和Archive配置里都要把Build Configuration选择为Release。接着关注Build Settings里的架构。如果你的测试机是较新的iPhone处理器已经是arm64e架构但很多老设备上运行需要的是arm64。在ARCHS这一项里建议写成arm64不要包含armv7或者armv7s。原因很简单LayaNative的JS引擎运行库如果包含armv7 slice静态库里所有符号都会被多编译一遍armv7指令集版本二进制体积增大不说有些第三方依赖库本身可能就没有armv7 slice链接时会直接失败。然后是bitcode。LayaNative源码在2.x时代对bitcode的支持状况很不稳定建议直接在Build Settings里把ENABLE_BITCODE设为NO。不然你会遇到的典型报错是链接时提示bitcode bundle could not be generated because ...这类问题很难排查因为报错指向的是第三方静态库内部。最终编译时选中xcodeproj/workspace后按CommandB。构建完成后在工程的Products目录里右键点击生成的.a文件选择Show in Finder就能看到静态库产物一般名字类似liblayaRuntime.a。4.3 编译完成的.a文件怎么验证拿到静态库后不要直接拖入自己的工程就开始happy coding先做两个必要验证。第一个验证是检查架构信息lipo -info liblayaRuntime.a正常输出应该是arm64架构。如果显示的是x86_64你去真机上跑会直接报unexpected Mach-O header。这种情况通常是选择了模拟器目的地编译改成Generic iOS Device再编即可。第二个验证是确认关键符号存在于库中nm liblayaRuntime.a | grep LayaAppDelegate如果关键类和方法名找不到说明你编译的静态库可能只是一个空壳或者模块划分和预期不符。宁可在这里多花五分钟排查也好过在几万人下载的App里崩溃。4.4 静态库的链接顺序和系统依赖自己在Xcode里集成LayaNative静态库时最容易踩的坑是链接顺序。C静态库之间的符号引用需要保证依赖方在前、被依赖方在后。具体到LayaNative就是在Build Phases的Link Binary With Libraries里把liblayaRuntime.a放在JavaScriptCore.framework、OpenGLES.framework、StoreKit.framework这些系统库之前。如果不注意顺序你可能会遇到这样一个诡异报错Undefined symbols for architecture arm64: _OBJC_CLASS_$_JSContext, referenced from: objc-class-ref in liblayaRuntime.a(XXX.o)明明JavaScriptCore.framework已经引入却还是找不到符号。这种就是链接顺序导致的调整顺序后重新编译问题通常瞬间消失。5. 编译产物回填到原生工程目录约定与初始化脚本的衔接5.1 静态库/动态库在原生工程里的放置路径与link配置编译产物拿到之后要把它自然地融进你的原生工程而不是简单丢进去就完事。放置路径和工程引用方式有约定乱了后面会很难维护。对iOS工程推荐建立一个独立的Vendor/LayaNative目录把liblayaRuntime.a、所有LayaNative相关的头文件例如LayaAppDelegate.h、ConchDelegate.h都放进去然后以文件夹引用的方式添加到Xcode工程。这样后续升级LayaNative源码版本时只需要替换这整个目录无需在Build Settings里改动若干次绝对路径。对Android工程自编译的.so文件放入app/src/main/jniLibs/armeabi-v7a/目录。这里还有一个小提醒如果使用gradle插件自动打包so不要在sourceSets里额外指定jniLibs.srcDirs指向一个包含多个ABI的目录否则abiFilters会失效。5.2 AppDelegate的入口改写与启动参数传递LayaNative有一套自己的生命周期入口约定。以iOS为例主工程中的AppDelegate需要继承或组合LayaNative提供的LayaAppDelegate而不是直接继承UIResponder UIApplicationDelegate。一套常见的最小化接入代码大概长这样#import LayaAppDelegate.h interface AppDelegate : LayaAppDelegate end在didFinishLaunchingWithOptions里LayaNative会要求你设置启动时的资源路径、初始化参数等。例如- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [self initializeLaya]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }如果你同时对LayaNative源码做了二次开发注意看头文件里是否多出新的初始化接口比如设置开发调试端口、切换URL、物品变更为本地文件结构等。这些API在预编译包中可能没暴露但源码编译后你有完全的自由度去扩展。Android端的接入入口主要是LayaPlayer或者类似命名的Java类。老版本源码下可能会要求你在MainActivity的onCreate里调用LayaPlayer.launch(MainActivity.this, (TextView) findViewById(R.id.textView), http://your_game_path/index.html);5.3 用自定义JS调试本地资源自编译的价值体现源码编译带来的一个直接福利是可以在原生层直接拦截并修改要加载的JS路径这对本地资源调试非常有价值。官方预编译包往往把资源路径参数写死在打包配置里每次调试都要重新打包App。而自编译后你完全可以在JNI层或Objective-C层加一个判断让启动时读一个本地配置文件NSString *jsPath [[NSBundle mainBundle] pathForResource:debug_config ofType:plist]; if (jsPath [[NSFileManager defaultManager] fileExistsAtPath:jsPath]) { [self setLayaUrl:http://localhost:8080/index.html]; } else { [self setLayaUrl:https://yourproduction.com/index.html]; }这样开发机上改JS代码App内立即生效不需要每次构建原生App。这是我建议所有有后端热更需求的团队都做的一个原生层小改造成本极低收益巨大。6. 从编译报错到定位问题几个真实的高频坑6.1 C标准被强制改变导致一堆老代码编译失败LayaNative老版本源码全是用C11风格写的里面大量使用了auto_ptr、tr1::shared_ptr这类旧标准特性。当你新装的NDK工具链默认编译标准是C17或更高时编译过程会报一堆莫名其妙的错误。第一次看到报错时我一度以为是源码本身损坏。后来排查发现是NDK编译器把默认标准提到了C17而源码又写死了部分依赖旧标准的特性。解决方式有三种在Application.mk或CMakeLists中强制指定-stdc11在NDK的配置文件中设置APP_CPPFLAGS -stdc11使用源码指定的老版本NDK避开新工具链默认标准的问题6.2 系统库缺失导致链接失败Android端编译时经常出现编译过程通过了生成.so数千个目标文件都ok但最后链接时崩溃报找不到__android_log_print或dlopen等符号。这种通常是你没有链接对应的系统库。在Application.mk里加上LIBS : -llog -ldl或者在Android.mk里LOCAL_LDLIBS : -llog -ldl就能解决。iOS端同类问题往往是JavaScriptCore的框架没有加到target。记得在Building Settings的Other Linker Flags里确认存在-ObjC这个标志位经常被忽略但它对Objective-C分类加载至关重要。6.3 高版本Xcode对旧C代码的编译攻击性有时候同样是编译Xcode 14比Xcode 12报错更多不是因为你代码写得有问题而是新编译器做了更严格的类型检查。比如dispatch_once_t必须要用dispatch_once初始化而不能简单地赋0再比如某些宏在旧代码里会触发-Wdeprecated-declarations警告升级为错误。一个相对省心的处理方式是在Build Settings里把CLANG_WARN_*这类警告考虑是否需要全部关闭或者把Treat Warnings as Errors设为NO。当然这只适合编译老库时使用你的原生业务代码不建议这样做。6.4 高频问题排查汇总问题现象直接原因有效解决路径Android编译时报找不到boost头文件submodule没有拉取执行git submodule update --init --recursiveAndroid链接时崩溃缺log/dle系统库未链接Android.mk中添加LOCAL_LDLIBS : -llog -ldliOS编译产物无法真机加载Xcode选择了模拟器目标编译改为Generic iOS Device重新编译iOS链接找不到JavaScriptCore符号系统framework未导入或链接顺序错误确保JavaScriptCore.framework已添加且位于liblayaRuntime.a之后真机上运行闪退但模拟器正常ABI不匹配或bitcode冲突检查abiFilters和ENABLE_BITCODENO加载JS脚本报Can not find functionLayaNative和LayaAir版本不对齐拉取与LayaAir完全一致源码tag重新编译NDK编译报错涉及auto_ptr工具链默认C标准太高强制指定-stdc11这张表是我在实际项目中反复踩过的坑的汇总每条都对应一次完整的排查经历。编译问题看起来千变万化但90%都可以在这张表里找到影子。如果你遇到的是新问题建议优先按下列顺序排查版本对齐、NDK/Xcode版本、第三方依赖完整性、系统库链接、编译选项。7. 源码编译之后还能做什么编译完成、集成成功后LayaNative源码的真正价值才刚刚开始体现。前面五章说的都是把现有代码跑起来但这套代码在你手里意味着极高的扩展自由度。我目前在自己项目里就保留一个LayaNative源码的fork分支每年只做少量同步。基于这个fork我给Android端加了一个原生的磁盘缓存接口把热更资源的读取速度提升了30%左右给iOS端加了内存告警时的主动资源释放逻辑让老旧机型的掉帧明显减少。这些功能放在预编译包时代是想都不敢想的。如果你暂时不想改C源码也可以利用它做性能分析和崩溃定位。在自编译过程中给LayaNative加一层日志输出把JavaScript引擎的GC日志、网络请求耗时、渲染帧数打点打到原生日志系统在排查线上问题时能救你于水火。最后给一个实际经验永远保留一份带符号的编译版本和对应的源码commit号。我自己经历过一次线上崩溃如果没有保留当时提交的代码快照仅靠预编译包崩溃栈里只有一堆地址什么都排查不出来。有了符号表和commit号几分钟就能定位到有问题的C文件。LayaNative源码编译的门槛确实比直接用引擎高不少但一旦跨过去你对整个LayaAir技术栈的理解会上一个层次。希望这篇文章能帮你少走一些弯路。
返回列表