大概是 8 月中旬的时候, 我们项目发生了一个很严重的线上事故. 在版本更新之后, 部分 Android 玩家反馈点击按钮开始游戏或活动按钮会闪退.
开始收到这个反馈时, 并没有太在意, 心想是不是机型适配有问题 ? 加上当时有别的工作在忙, 就没有去理会. 大概一个小时后, 玩家的邮件像雪花一样纷纷而至, 我才开始意识到, 更新出问题了.
大概是 8 月中旬的时候, 我们项目发生了一个很严重的线上事故. 在版本更新之后, 部分 Android 玩家反馈点击按钮开始游戏或活动按钮会闪退.
开始收到这个反馈时, 并没有太在意, 心想是不是机型适配有问题 ? 加上当时有别的工作在忙, 就没有去理会. 大概一个小时后, 玩家的邮件像雪花一样纷纷而至, 我才开始意识到, 更新出问题了.
我使用 Quick-cocos2d-x 内置的 store 类请求商品信息时, 收到这样的错误:
nvalidProductIdentifiers [CCStore_obj]
[CCStore_obj] productsRequestDidReceiveResponse() invalid pid: com.xxx.xxx
首先, 检查你请求的商品 id 在 iTunes 后台是否创建, 是否拼写错误. 如果没有问题, 那么就不太好办了, 会有很多原因导致这个问题.
ZZB_Amoy 博客的这篇文章总结了下可能的原因, 如下:
- 创建的App ID是否启用了IAP功能。
- 商品信息是否配置到iTurn Connect,并到达“Ready to Submit”状态。
- 在iTurn Connect中创建Test User,并收取邮件激活。之后登录到测试用手机的设置页面中(Store选项)。
- App的Bundle Id是否和后台配置的App Id一致。
- 是否创建相应的provisioning profile,并用此签名App。
- iTurn Connect后台配置完商品信息后,是否等待若干小时生效。
- SKProductsRequest请求的商品Id必须和iTurn Connect中配置的一致。(如:com.test.product.xxx)
- iTunes Connect中配置的银行信息是否正确。
- 是否先删除旧App,再重新编译生成新的。
- 请不要使用越狱手机测试。
下面说说我两次遇到这个问题的解决方案:
注:在测试阶段,可以不用上传APP软件包,但必须创建测试用Apple Id,并在手机设置中(store选项)登录。
运行游戏过程中收到如下错误:
[1515:710439] -[AppController window]: unrecognized selector sent to instance 0x2c85c00
libc++abi.dylib: terminate_handler unexpectedly threw an exception
这个在接入某一个平台 sdk 时遇到的问题, 于是便问了下他们的技术, 很快解决了问题.
虽然问题解决了, 但是我并不明白各种缘由. Google 了下, 大概明白了, 原来如此. 从错误中我们可以看到这句 [AppController window]
, 从语法来看, 这是要调用 AppController 的 window 函数, 但是在我们之前的写法中没有实现这个函数, 便出错了. 而使用 @property
这个东西, 会自动帮你实现一个 window/setWindow
函数, 这样就不会找不到这个函数了.
这个也是在接入第三方 sdk 时遇到的问题, 游戏一启动就会崩溃, 收到错误如下:
dyld: Symbol not found: _OBJC_CLASS_$_SFSafariViewController
Referenced from: /var/mobile/Applications/CF4146B4-3F79-4644-86CA-F19E52E64BAA/superarmoreddivision.app/superarmoreddivision
Expected in: /System/Library/Frameworks/SafariServices.framework/SafariServices
in /var/mobile/Applications/CF4146B4-3F79-4644-86CA-F19E52E64BAA/superarmoreddivision.app/superarmoreddivision
Google 了一下, 没有任何人遇到过这样的问题, 这就十分棘手了, 完全不知从何入手. 经过一番探索, 找到了几个有用的线索:
这就可以肯定是某个 sdk 中使用了 SFSafariViewController
这个类, 但是还是没有办法定位是那个 sdk . 我不知道是否有一个命令查找符号引用, 因此只能采用最笨的排除法了, 我将引入的 sdk 依次删除, 看是否能够运行.
最终定位到了某个广告统计 sdk , 在询问其 ios 技术人员后得到了解决方案. 原来他们 sdk 需要以 optional
的形式引入 SafariServices.framework
.
都怪我没有仔细阅读文档, 白白耽误了一段时间, 下次一定要注意!
集成 Facebook sdk 时, 调用登录接口游戏就会崩溃, 这个问题 Google 一下就能解决, 解决方案也很简单, 在 Info.plist 中加入下面几行代码即可:
1 | <key>LSApplicationQueriesSchemes</key> |
Stackoverflow 上的答案可以移步这里, Facebook 官网上也给了解答.
隐藏状态栏在 ios9 上换了一种方式, 还是需要在 Info.plist 中进行配置:
1 | <key>UIStatusBarHidden</key> |
Stackoverflow 上的答案可以移步这里.
游戏内的一些弹框为了保证在游戏的最上层显示, 偷懒使用了 Quick-cocos2d-x 提供的 device.showAlert
接口. showAlert 内部使用 UIAlertView
实现, 运行一直良好, 有一天突然就不行了, 一调用就崩溃.
各种办法都试过了, 网上都说是线程安全问题, 我试了一下各种处理都不行, 打断点跟踪到最底层也无济于事. 几近绝望之时, @bin 的一句话提醒了我:
会不是屏幕方向的问题 ?
最终一番尝试, 删除了 RootViewController.mm
中几个屏幕方向相关的函数:
1 | */ |
这个 bug 出现之诡异, 解决方案之诡异, 在我遇到的 bug 中也算是很少见了.
爱因斯坦的质能方程 E=MC^2
, 用在编程界同样适用 Error = More Code ^ 2
. 代码越多, 出错的可能性就更大, 这个结论很正确呀. 那么我们如何使用更少的代码实现同样的需求呢 ?
一. 普通技
让我们来看一段代码:
1 | local monthly_is_taken = app.player:getAttribute("monthly_is_taken") |
显然这个 if 语句是没有必要的, 我们可以直接使用 bool 进行函数参数传递:
1 | local monthly_is_taken = app.player:getAttribute("monthly_is_taken") |
我们可以看到减少了 %60 的代码, 逻辑反而变得更清晰了.
我们都明白了中间变量的意义, 主要是为提高代码的可读性. 但是有时候中间变量的太多, 在增加码量的同时, 也会打断我们的我们的思路.
比如我们要算一个等差数列的和, 我们都知道使用公式 (首项+末项)*项数/2
, 我们看一下这个实现:
1 | local array = {1,3,5,7,9} |
就不如下面这个实现:
1 | local array = {1,3,5,7,9} |
这样的话, 我们上一个示例的代码可以进一步精简:
1 | self._monthly_take:setButtonEnabled(app.player:getAttribute("monthly_is_taken")) |
如果是逻辑相悖的判断条件, 我们可以使用 elseif 语句连接, 而不用多个 if 语句.
1 | if self.item_id == "43" then |
我们可以修改为:
1 | if self.item_id == "43" then |
这样修改后, 对逻辑的执行时间也优化哟, 因为一但有一个 if 语句命中, 后面的 elseif 都不会再去判断了.
如果一个逻辑中有大量的 if-elseif 语句, 我么就可以使用 config 的形式替换掉它, 使得逻辑更加简洁.
让我们看一个示例:
1 | if _data.type == GameEnum.MailType.MAIL_TYPE_SYSTEM then |
这是一段关于邮件标题的逻辑, 这里只节选出了 1/4 的代码, 真的是又臭又长. 我们可以这样子去优化它:
1 | local CONFIG = { |
因为只是代码节选, 所以上面修改是一段伪代码, 但是看起来超级清爽的有木有! 对于一开始无法确定的数据如何配置呢? 我们可以配置一个 function
, 用的时候取出来调用就可以啦.
当我们拿到一段数据后, 总是要先预处理数据, 后面才是使用数据. 预处理阶段很重要的一步就是某些数据的默认值.
1 | function sum3(_num1, _num2, _num3) |
很繁琐是不是, 这时候我们可以使用 and 和 or 来优化默认值的设置:
1 | function sum3(_num1, _num2, _num3) |
当 or
的前面部分是 nil
或者 false
的情况下, 返回这个表达式的值后面部分. 下面我列举一下常用类型的默认值用法:
1 | -- number |
这里值得一提的是 boolean
类型, 如果希望默认值是 false 话, 就不需要默认值, 因为 nil 和 false 对于判断来说以意义一致. 而如果希望默认值是 true 的话, 并不是 a = a or true
, 而是 a == nil and true
, 大家可以细想一下其中的含义.
比如我们要统计一个列表中, 每个元素出现的次数:
1 | local list = {1,2,2,3,1,3} |
因为 counter
不可能提前初始化好, 所以总是要判断存不存在这个元素, 我们也可以利用上面提到的技巧做这个事情:
1 | local list = {1,2,2,3,1,3} |
是不是变得很简洁 ?
解决方案:
http://www.jianshu.com/p/3930df903a44
这个问题可能是因为你 XCode 没有下载对应 iOS 的 SDK 导致, 一般情况需要同步更新 XCode.
这个问题有可能是你项目 Team 选择的是一个没有开发者资格的账号导致的, 虽然可以正常开发, 真机调试, 但是是不能发布的, 所以也无法 Export .
解决方案:
1 | xcodebuild -exportArchive -exportFormat ipa -archivePath your-archive-file-name.xcarchive -exportPath ~/Desktop/test.ipa |
很可能是之前手机已经装过一个同 Bundle ID 的应用, 但是现在换了签名.
解决方案:
删掉手机已经安装的那个应用就可以啦.
签名失败了, 这种情况一般发生在使用别人给的证书打包时. 这时候我们项目 Build Setting > Code Signing Identity
就不能选择 iOS Developer
, 而是要选择导入的签名文件.
如果没有发布应用的话, 需要用沙盒测试账号来测试. 我们需要先在 设置>iTunes Store 和 App Store
中 注销账号, 然后打开游戏, 开始购买, 这时候输入你的测试账号. 成功后如果有跳转 App Store 的话或者绑定付款方式的话, 不同理会, 再返回应用购买就可以了.
某一次突然, 一旦调用 Quick-cocos2d-x 提供的 device.showAlert
就会崩溃, 断点调试无果, 崩溃时提示的内容也不尽相同.
解决方案:
删除 RootViewController.mm
中所有和屏幕方向代码, 就解决啦.
1 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation |
感谢 @子龙山人 大神提供的解决方案, 点击这里查看.
将 UIVideoPlayer-ios.mm
文件 ~VideoPlayer()
函数中的 dealloc
修改为 release
即可.
1 | - [((UIVideoViewWrapperIos*)_videoView) dealloc]; |
这两天一个运营的同事跑过来问我:
他: 咱们的游戏适配那啥 VIP6 了么?
我: ….
苹果商店在儿童节之后就不允许未适配 IPV6 的应用上架了, IPV6 是啥 ? 需要做些什么呢 ?
Quick-Cocos2d-x 项目的 iOS 工程使用 Tgarget Dependencise
依赖 cocos2d_lib
和 cocos_lua_bindings
工程.
这样子在 iOS Archive 时会重新编译这两个项目, 十分痛苦, 尤其是一次出七八个渠道的包, 好几个小时就耗在里面了.
为什么不用静态库, 编译出 .a , 使用时直接链接就可以了嘛.
找了一下, 原来早已经有小伙伴想到了这点, 这篇文章 Build cocos2d-x fat static library 就特别棒. 从中我们可以发现一个特别有用的脚本 buildstaticlib.sh, 可以直接使用 xcode 工程编译出静态库.
不过这个脚本只能编译出 Release
版, 我修改下可以传入 configuration
, 这样我们可以分别编译出 Debug 和 Release 版的静态库啦, 我修改后的文件在这里.
因为我们要编译出多个静态库, 所有又写了另一个脚本 build.sh
调用 buildstaticlib.sh
, 内容如下:
1 | ./buildstaticlib.sh $QUICK_V3_ROOT/cocos/scripting/lua-bindings/proj.ios_mac/cocos2d_lua_bindings.xcodeproj "libluacocos2d iOS" "Release" |
运行成功后会在当前目录生成 4 个 .a 文件, 下一步中将会用到.1
2
3
4
5
6
7.
├── build.sh
├── buildstaticlib.sh
├── libcocos2d\ iOS-debug.a
├── libcocos2d\ iOS.a
├── libluacocos2d\ iOS-debug.a
└── libluacocos2d\ iOS.a
使用 XCode 打开 proj.ios_mac
目录下的 xxx.xcodeproj
工程.
首先移除对 cocos2d_lib
和 cocos_lua_bindings
工程的依赖, 右键点击 Delete
然后选择 Remove reference
就可以.
我们静态库的依赖是在这里添加的, 在 Debug 和 Release 选项中分别加入对应的静态库.
这样就完成啦, 尝试一下 Archive 的速度吧 !
我们改成静态库后, 调试 cocos 引擎的代码会多有不便, 而且一旦修改了 cocos 的代码, 就得重新生成静态库, 对于开发阶段太不友好了.
我们的解决方案, 就是再建立一个 debug 工程, 这个工程依旧使用依赖项目的方式编译 cocos , 调试流程和以前一致. 上线打包时则使用我们的静态库版本, 多渠道也做在这个工程中, 享受静态库带来的编译加速.
最终我们的目录结构是这个样子的:
1 | . |
这一步可有可无, 我的代码洁癖又犯了, 所以顺手改了一下.
这时的生产环境除静态库外的内容和调试环境几乎一致, 然而有一些东西是我们用不到的:
删除这些时改动了 AppDelegate 中的东西, 这也上一步为什么从 runtimes-src
目录复制了一份.
这一步我们目前还没有做, 只是一个想法.
修改完使用静态库后, 编译速度得到了很大的提升, 但还没有达到极致, 因为 quick 特有的 c++ 文件还是以文件形式存在于工程中的. 所有 Archive 的时候还是有一百多个源文件需要编译.
如果我们能进一步拆分, 新建一个 lib 工程, 将 quick 的源文件添加和依赖项目添加进去, 我们的游戏只依赖这样的一个静态库, 是否可以达到一个极致的编译速度 ?
在编译出 debug 版的静态库之前, 我还有想法将这几个静态库压缩上传到 git 上, 编译出 debug 版之后, 我就一个想法, ignore them !
所以我最终的策略 将这几个 .a 在 git 上忽略掉, 同时在那个目录保留了一个编译脚本, 谁要用到 iOS 项目的时候, 发现没有 .a , 自己运行脚本编译一份就可以啦 !
现在那个编译脚本会编译出一个 fat
(armv7 armv7s arm64 i386 x86_64) 版的静态库, 内部实现其实是编译了好多次, 导致现在编译时间非常长.
思考:
i386 x86_64
版本 ?看到网上有过这个说法, 我没有在修改前后分别 Archive 对比包体, 不太严谨.
但和我之前某一次的包相比, 只大了几百KB, 还不太确定是不是与使用静态库有关系, 大家在修改时可以注意对比一下.
在我们版本迭代的过程中, 总有一些图片被废弃掉, 如果当时忘记删除的话, 久而久之也就忘记了. 如果在上线前不做一次整理的话, 它们就会残存在你的资源中, 浪费包的体积.
为避免这种情况, 我们可以做的是:
可以用到的系统命令是 ack
, 我们可以通过 brew install ack
安装, 使用的效果:
关于 ack 的更多用法请移步这里.
大家都知道图片在拉伸的过程中会失真, 那么如何避免这个情况呢? 使用9图.
注: 配图来自http://mux.baidu.com/?p=1506
这样我们就可以将一张很大的图缩小到很小, 然后使用9图拉伸, 起到节省资源的目的. 9图在 cocos 中的对象是 Scale9Sprite
, 具体用法可以参考这篇文章.
知乎上有一篇问题讲的就是这个:
拳皇中的人物变色是如何实现的?
知乎日报上的这篇
cocos2d-x 版由 @偶尔e网事 大神实现, 对应的对象是 SpriteWithHue
, 目前已经默认集成到了 cocos2d-x 中.
这样我们就可以将原来只是色调差异的图片用程序来实现啦~
注: 配图来自http://bullteacher.com/7-textures.html
游戏中的有些图片完全可以通过平铺实现, 这样的话我们就可以让美术只出一个平铺单元的图片,在程序中去实现平铺.
首先, 平铺的这个功能是 opengl 层面就支持的, 详情大家可以移步这里, cocos2d-x 中实现平铺很简单:
1 | -- 首先, 使用平铺单元图片创建一个精灵 |
注意: 平铺单元图片的尺寸只能是2的幂
无损压缩还是十分值得推荐的, 它的原理知乎上这个答案讲的很清晰, 我节选其中的关键文字:
1.核心原理很简单,通俗的解释一下,就是由于PNG格式的灵活性,他可以有很多种方式表示同一张图片,不同方式有时就会导致文件大小不一样…
2.还有一点是PNG采用的是deflate算法,也非常的灵活,他的压缩率和encoder的实现有关,不同的encoder使用的时间,压缩出来的大小都不一样…
3.当然除了上面这两点是真正的无损压缩以外,还有减小PNG文件大小的方式就是去除一些对图片本身没有任何影响的metadata…
所以无损压缩纯粹是单方面的受益, 是一定要做的.
我们无损压缩主要用到的工具: ImageOptim
有损压缩会损失一部分的图片质量, 但带来的受益还是十分可观的. 这是一个抉择的过程, 以最小的代价获取最大的受益, 甚至不能批量处理, 可能需要一张一张的人肉对比压缩.
我们有损压缩主要用到的工具: PP鸭
然后在程序中翻转3次,得到其他角度的图片. 一般会用在图片尺寸特别大的场景.
如上图, 我们游戏中一个全屏幕的雷达就是通过这个方案减少图片体积的.
将展示精度不强的图片(比如: 游戏背景上的小装饰, 爆炸的序列帧)缩小, 在程序中放大.
用jpg和黑白色png作为遮罩实现透明
用shader使图片背景透明
cocos2dx中使用JPG图和只带Alpha的PNG图合成渲染
我们之前曾经采取过其中的一个方案, 将一张 png 图片拆分为 jpg+alpha.png 的形式, 整体的包体小了近 25% , 不过也带来的其他的一些副作用.
建议大家使用这类黑科技前一定要做好调研和测试用例, 评估一下实际的收益.
我们项目中的输入框使用的都是 EditBox , 但是 EditBox 还存在一些问题, 这里给大家分享一下我们的解决方案.
用过 EditBox 的同学都知道这样一个情况, EditBox 在创建时是无法传入字体大小的, 字体大小默认和 EditBox 的 size 一致. 如果要修改字体大小的话, 就必须有程序的参与, 十分讨厌.
而我们聪明的设计师 @大勇同学 则想到了一个非常棒的办法, 使用一张透明的9图来创建 EditBox, 后面再放置一个真实效果的 Scale9Sprite , 这样就可以实现字体比边框小很多的输入框了.
多行输入是一个很有必要的事情, 我们在写邮件, 军团公告等界面都有类似的需求, 然而 EditBox 并不能很好的支持多行输入, 不同平台间也存在差异, 一直很头疼这件事情.
然而团队中另外一位成员 @小齐同学 却用另一种十分脑洞的方案解决了这个问题, 着实让人佩服. 他的思路是这样子的:
创建一个和需求大小一致的 EditBox, 同时创建一个 LalbelTTF , 将 dimensions 属性设置为需求大小. 处理 EditBox 使之看不见, 但又能正常输入, 同时监听输入文字变化事件, 在事件中修改 LalbelTTF 的文字.
核心就是让 EditBox 承担只文字输入的功能, 而让另外一个 LalbelTTF 来承担文字显示的功能. 实现的代码如下:
1 | function EditBoxUtil.multiline(_editbox, _label, _params) |
这段代码和简单, 但背后所遇到的坑却不少, 且听我来道一道:
如果只是想设置字体大小, EditBox 明明有提供 setFontSize
接口, 为什么要调用 setFont
? 请看 setFontSize 实现:
1 | void EditBox::setFontSize(int fontSize) |
可以看到 setFontSize 在没有设置字体名称 _fontName
时是没有作用的.
在接入 android 前,我们是没有分平台实现的, 只是 setFont("Helvetica",0)
, 在 iOS 上没有任何问题, 但是在 Android 上会 catch 到 divide by zero
崩溃, 估计是某一个地方用 fontsize 做被除数了吧 , 于是 Android 上改为设置透明度.
崩溃在 Cocos2dxBitmap.java
的 getPixels
函数中:1
2
3
4
5
6
7
8
9
10
11private static byte[] getPixels(final Bitmap bitmap) {
if (bitmap != null) {
final byte[] pixels = new byte[bitmap.getWidth() * bitmap.getHeight() * 4];
final ByteBuffer buf = ByteBuffer.wrap(pixels);
buf.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(buf);
return pixels;
}
return null;
}
在 new byte
这里触发了报错的原因是 Out Of Memory
异常 ! 调试发现 bitmap
的宽高惊人的达到了 12000x600
!
经过 @bin 的提醒, 发现这里可能是因为 EditBox 字体过大的原因, 因为 EditBox 会默认设置字体字体大小和 Scale9Sprite 的 PreferredSize 一直, 就可能设置字体大小为 100+ , 文字一多尺寸当然就上去了! 所以便有了这么一行:
1 | if device.platform == "android" then |
按照要求游戏中玩家可以输入文字的地方都是不能够输入 Emoji 表情的, 原因有两点:
因此, 我们需要屏蔽 Emoji 表情的输入, 我们有两种做法:
我们采用的是第二种方案, 这个无法通过纯 lua 代码实现, 需要分平台去做.
修改 UIEditBoxImpl-ios.mm
文件的 shouldChangeCharactersInRange
函数:
1 | - (BOOL)textField:(UITextField *) textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string |
这段代码是我从 https://github.com/woxtu/NSString-RemoveEmoji 中提取出来的.
Android 上的实现也很简单, 主要是需要创建一个新的 InputFilter
用来过滤 Emoji 表情. 需要修改 Cocos2dxEditBoxDialog.java
文件成员变量添加:
1 | public static InputFilter EMOJI_FILTER = new InputFilter() { |
修改 onCreate
函数 setFilters
处逻辑:
1 | if (this.mMaxLength > 0) { |
关于 Emoji 的相关修改都已经推送到了 github 上, 点击这里查看.
最近这段时间遇到了两次比较严重的真机崩溃问题, 都是之前所没有遇到过的, 特此记录一下, 希望能帮助到遇到类似问题的朋友.
之所以强调真机, 是因为这些问题在 player 上或者 debug 版无法出现, 只有真正运行在手机上才可能遇到, 因为最后我们 Archive 出来的包都是 release 版的.
The identity used to sign the executable is no longer valid.
Please verify that your device’s clock is properly set, and that your signing certificate is not expired. (0xE8008018).
解决方案:
View Details...
Download All
> 等待完成后点击 Done
关闭reset
.表现出来是竖屏游戏上线有黑边, 跟踪发现获得的设备尺寸为 960x640, 而非 1136x640.
解决方案:
添加一张尺寸为 1136x640
名为 `Default-568h@2x.png` 的启动图即可.
参考资料:
http://discuss.cocos2d-x.org/t/getframesize-get-wrong-screen-size/7657
cocos2d-x 在 XCode 中编译 warning 太多, 出错后 error 会被淹没在一大堆的警告中, 得拖动半天才能找得到.
解决方案:
在页面最下方有一个感叹号型按钮, 点击选中即可.
XCode 7 error: “A launch storyboard or xib must be provided unless the app requires full screen”
解决方案:
我们勾上全屏即可:
目前就这些, 后面会持续更新 !