0%

我从来不相信好人必定有好报。这个社会不是因为你是个好人就给你好的回报,而是因为你比别人更了解这个社会的运转机制并做了正确的选择。送礼有送礼的规矩,做生意有做生意的规矩,做官有做官的规矩,要混得好,你得比别人在更多方面都要懂得生存规则。我还坚信的:如果你懂得并遵守了各行各业的规矩,一般来说也会是个好人。
Read more »

摘要:一个普通应用,大到微信, 小到豆瓣FM,必不可少的都包括四部分:Network、Logic、Data、View(NLDV)。如何把他们组合起来,结构清晰、又协作便利,是前端主程的基本修养。本文用通(有)俗(点)易(啰)懂(嗦)的语言,界定了这四个模块的职能范围,同时提供了一种简单易用的组织方式。知者可互动,不知者可参考。

先唠点嗑

说说自己吧:毕业三年,经历4个项目,前两个主做功能开发,后两个全面负责。因为大学对UI交互深感兴趣,梦想成为优秀的交互设计师,所以毕业就做了前端,想着至少也离得近点。不料一入代码深似海,以前看过的十几本交互书也随风而去,脑子里剩下的只有:几行干巴巴的代码、几个平台的API、和一点不成熟的总结。借此机会,整理一下。

我把他名为NLDV,也就图一叫着方便(首字母的组合),并不是想标新立异。你或许会想到MVC,其实本质上跟MVC没啥区别。只是与时俱进,现在App几乎都是连着网的,也就把Network提出来了。

从第三个项目开始,这个NLDV的结构在我意识里渐渐清晰。第四个项目(游戏),因为是从零开始,我把NLDV的结构应用到了项目中。经历了渲染引擎更换和网络引擎更换,过程中其它模块儿做得改动极少,切身体会到它的妙处,忍不住前来分享。

从账号登陆谈起

我们从一个最简单的登陆请求谈起。按照NLDV框架的思想,步骤如下:

  1. Logic告诉Network:以后你收到来自远方服务器的消息都跟我打声招呼,不然我告诉程序员整死你丫。(Logic向Network注册网络监听函数。)
  2. 用户点击登陆按钮,此时View调用Logic的登录方法。Logic赶紧写封信,告诉Network把这封信送到服务器。(Logic根据传入的参数,构造相应的消息体,Network负责把消息发出去)
  3. 漫长的等待。。。
  4. Network终于等到服务器发来的消息,就急急忙忙递给Logic。Logic打开信封一看:“尼玛,居然能一次成功了!32个赞!”
    1. 可是信上还有性别,年龄,头像,邮箱等等等信息,头都大了,不记下来恐怕是隔夜就忘啊。于是,找来Logic的御用秘书Data(只有Logic能写入),这些信息就交给你了,临时存储还是永久存储我不管,反正我和View来取的时候,你要能给我。
    2. 好消息要和大家分享,于是Logic全应用广播:“我们已经出色的完成了登陆任务,大家再接再厉”
  5. View收到此广播消息,用界面告诉用户“亲爱的,你登陆成功了”。到此,完成一次登陆。
    PS:对于大多数界面实例,Logic刚刚发的这条广播是可以当耳边风的,因为跟自己业务无关。但,对于登陆界面来说,他必须时刻竖起耳朵监听这个消息,要不然就是失职。所以一般情况下登陆View会在发送登录请求之前就向系统注册监听这个消息的函数,以确保万无一失。这是后话,没看懂也没关系

职责分配

首先,看一幅图:
NLDV Diagram

通过上面的例子和图示,可以来总结一下NLDV四大家族各自的职能范围了:

Network

Network的是数据交流的基础。在这里并不单指socket,并且不暴露任何底层网络的实现。而是一个更加完整、稳定,有纠错功能的职能单位。主要功能:

  • 响应Logic的调用,将构造好的消息体转换成服务器能识别的字节串,发送出去。
  • 接收服务器发过来的字节串,转换成前端可识别的结构体,通知Logic。
    • 前后端在定义消息的时候,一般会用一个消息号(/或 主消息号-子消息号对)来唯一标识一种消息。
    • 而Logic也不止一个,账号系统,业务系统等,每个系统有对应的Logic,分别处理相关的业务逻辑。
    • 一个Logic仅仅会对某一些消息感兴趣,所以,它只向Network注册自己感兴趣的消息号。
  • 容错处理。比如有限次的自动重发,需要的时候自动重连,网络彻底不可用时通知Logic等。

我们用最少的接口来定义它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Real msg struct will implement this interface, adding some getter/setters.
interface IMessage
{
uint getMsgId();// unique id for a type of msg
byte[] toBytes();// serialize to bytes.
void parseBytes( byte[] bytes); //deserialize to useful info.
}

//Generally, "Logic" will implement this interface for recieving data
interface INetworkHandler
{
void onMessageRecv( IMessage msg);
}

interface INetwork
{
void send( IMessage msg );
void registHandler( uint msgId, INetworkHandler handler );
void unregistHandler( uint msgId, INetworkHandler handler );
}

Logic

Logic是一个应用中核心实现业务逻辑的部分。主要功能有:

  • 响应来自用户(View)的功能请求。或通过写入Data来改变状态,或构造消息体发送到服务器。
  • 收到网络消息后,做相应的逻辑处理,将数据的改变写入Data,最后广播本地事件。
    • 这里的本地事件通常用一个 字符串或者整数指代,称为本地事件ID。
    • 这个过程通常会用到观察者模式。有一个本地消息中心LocalMessageCenter,监听者(通常是View)用本地事件ID来向LocalMessageCenter注册,而Logic调用LocalMessageCenter.dispatch( localEventId ) 即可广播此本地事件。
    • 是的,Logic直接调用View的方法也能达到同样的目的。但是,为了减小Logic和View之间的耦合性,还是选用LocalMessageCenter作为中间层。随着需求的改变,View类可能面目全非,但LocalMessageCenter的接口却可以长久不变。

Logic的大致结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class SomeLogic implemets INetworkHandler
{
SomeLogic()
{
Network.getInstance().registHandler(1, this);
Network.getInstance().registHandler(2, this);
}


void onMessageRecv( IMessage msg)
{
switch( msg.getMsgId() )
{
case 1:
logicHandler1(IMessage msg);
break;
case 2:
logicHandler2(IMessage msg);
break;
...
}
}

//请求操作
void logicReq1(...);
void logicReq2(...);
...

//消息处理
void logicHandler1(IMessage msg)
{
//TODO: 收到消息,逻辑处理

//TODO: 向Data写入数据

//TODO: 广播本地事件
LocalMessageCenter.getInstance().dispatch( "Logic1Compelete" );
};
void logicHandler2(IMessage msg);
...
}



ILocalMessageHandler
{
void onLocalMessage( String msg );
}

LocalMessageCenter
{
static LocalMessageCenter getInstance(); // Singleton

void dispatch( String msg );
void regist( String msg, ILocalMessageHandler handler );
void unregist( String msg, ILocalMessageHandler handler );
}

Data

这个模块是全应用的数据中心。提供两个功能:

  • 存储。前面已经提到,基本只有Logic对Data有写入权限。由于没有想到好的办法,这个规范暂时只能通过编码习惯来约定,没有做框架级的约定,如果大家有好的办法,欢迎补充。Logic 不关心数据的存储方式:同步OR异步,临时OR永久;这些都由Data自己决定。
  • 读取。Logic和View都会使用到Data中的数据。但是需要注意异步读取的问题。一般App要求View对操作的响应速度要在0.1s级别。所以,若涉及大量的数据存储或读取,便需要借助异步处理。对于存储,我们可能不是那么关心异步存储什么时候结束,只需要知道它成功了既可。但对于读取,经常遇到的情况是:页面上加个菊花,数据完全读取成功之后,移除菊花。这就需要一个通知机制。此时,我们也会用LocalMessageCenter作为通信的桥梁。

View

NLDV框架对View的限制,相比以上3个模块,非常少。因为,对于NLDV框架来说,View跟特定的平台无关,即它是对各种不同平台显示框架的抽象。在IOS里它是UIFramework,在Android里它是xxx,在游戏里它是openGL,甚至,在下面会讲到的机器人模拟器里它是一堆测试代码。

在这个框架里,View只在两个过程中出现:

  1. 调用Logic的方法,发送逻辑请求。
  2. 监听本地事件,读取Data,显示内容。

只补充一点:在View显示过程中,需要用到很多二级数据(二级数据,就是跟原始数据相对,对原始数据进行整合或者筛选后得到的数据),这些二级数据的处理过程最好在View中处理。因为这些代码大多跟特定的界面有关,而跟App的主要逻辑关系不大,为了以后更改方便,最好写在View里。

WHY NLDV?

看到这里,读者大概能隐约的感受到NLDV的一些优点,但是又不那么清晰,要不要看下去呢?下个里程碑就到了看这个人扯淡有用么?再看下去两盘Dota的时间可就没了丫。。。

别急,举几个栗子提提神。

引擎更换

最早,因为兼容PC版本的斗地主,我们采用了原始的字节对齐的方式进行网络传输。又因为设计失误,网络层字节的pack和unpack以及异步读取的处理,都各种出问题。(因为需要跨平台,所以我们用了C++语言,采用了最基础的BSD socket进行socket连接。)

结果是,游戏在网络不稳定的情况下各种闪退。这下产品经理不高兴了。又因为当初开发网络层的同学接手了其他事情,只剩下我各种修修补补,最终也没能彻底解决问题。

于是,我决定重写网络层。(妈蛋,早看这段代码不爽了)。于是我花了两天封装了一个带自动重连和容错功能,支持异步接受和发送的GameSocket,简单测试可以发送和接受字节。5分钟替换游戏中的旧网络层,你猜,怎么着?一次Run就登陆成功了!点了几下,所有功能完好如初!尼玛,世界上有比这还幸福的事情么?

其实,别听我说的挺牛逼的,其实替换过程就改了不到10行代码。因为实在是跟Logic、Data、View没啥耦合的地方。

制作机器人

一般在线游戏,都会有一两个用户没问题,大量用户就有问题的时候。所以,机器人测试总是必要的。

当我把前端代码交给后端,简单介绍了一下结构之后,后端的小伙伴儿们都惊呆了。不是因为他看到这框架有多么优秀,而是:“这样,我只要写一个while循环,500行代码就能完成一个机器人了啊。我还申请了一个星期来做这个事情呢!”(这是他的原话)。

说的更具体点,制作一个机器人就这么几步:

  1. 把所有的View代码文件删掉。
  2. 写一个AndroidLoop类。在这个类里,监听斗地主主流程里必须处理的LocalMessage,在适当的时候发送主流程中的请求。(对于斗地主来说,主流程包括登陆、选房间、抢地主、出牌、退房间。每个应用有所不同,灵活自便。)
  3. NLD(NLDV去掉V)部分都不变,几乎不用改一行代码。

逻辑清晰

框架的作用,理论层面上规范了整个软件的结构;而在实现层面,通俗一点,它就规范了什么代码该写在哪里,不要随地乱放

作为一个针对性很强的框架,在上述过程中,NLDV约束了很多可能不需要约束的规范。其中大部分是项目中的干货经验。我知道他并不总是好的,我考略了很久要不要把他们加进来。最终,我还是写下来了,考虑到刚开始从零开始写应用的读者来说,这些可能避免他们走很多弯路;而对有经验的读者来说,可能他们有判断的能力,可以取舍自如。

我想,如果严格按照NLDV框架来编写程序,显而易见的好处就是:

  1. 层次清晰,出现问题容易定位。
  2. 主程再也不用担心同事们把代码写得到处都是了。。。

框架之外(下回分解)

NLDV的适用场景(下回分解)

因为各种原因,这篇博客断断续续写了两个星期了,再不发布就要胎死腹中了。所以,最后两节放在这篇日志的续集中写。如果您感兴趣,请私信我,我会尽快补上。

每本书的封面之下都有一套自己的骨架,作为一个分析阅读的读者,你的责任就是要找出这个骨架。一本书出现在你面前时,肌肉包着骨头,衣服包裹着肌肉,可说是盛装而来。你用不着揭开它的外衣或是撕去它的肌肉,才能得到在柔软表皮下的那套骨架。但是你一定要用一双X光般的透视眼来看这本书,因为那是你了解一本书、掌握其骨架的基础。
Read more »

屌丝程序员的逆袭一般分两步:让自己变得牛逼, 让别人知道你牛逼. 我通过写博客来让自己更擅长归纳和抽象, 也通过写博客来让别人知道我能胜任某份工作.
Read more »

摘要: cocos2dx 是一款优秀的多平台,专为2D游戏设计的引擎. 在活跃的开源社区的推进下, 越发稳定和强大. 2.x -> 3.x的更新幅度很大, 性能的提升和功能的丰富也非常明显. 但在享受进步的同时,也要承受迁徙之苦. 本文主要是总结自己迁徙的经历, 以防大家走弯路.


基础准备

  1. 我一直把VS当作主开发环境, eclipse和xcode作为特定机型的调试环境. 所以, 偷个懒, 假设你也在用VS开发. 另外两个平台也也都有正则表达式替换功能, 大同小异.
  2. 在这里, 假设你已经搭好了3.x的VS开发环境, 能正常运行HelloWorld演示.
  3. 假设你有一定的正则表达式基础. 如果没有的话, 可参考这篇速成教程:正则表达式30分钟入门教程

进入正题

cocos2dx 3.1的简要feature更新介绍可参考这里, 详细的changelog可参考这里.在升级之前, 建议扫一遍changelog( 否则都不知道要做啥…).

接下来, 进入正题啦:

语法更新

本文将有非常多字符串替换的步骤, 都在vs2012中进行. vs2012中字符串替换窗口如下:
此处是vs窗口截图
本文将上图代表的替换表示为:( 以后将不特殊说明 )

1
2
//regex-replace
\bUIImageView\b ==> ui::ImageView

obj-c式命名 ==> c++命名空间式命名

以下是常见的替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//regex-replace
//Widget:
\bUIWidget\b ==> ui::Widget
\bUIImageView\b ==> ui::ImageView
\bUIButton\b ==> ui::Button
...
\bUILayout\b ==> ui::Layout
\bUILabel\b ==> ui::Text
\bUILayer\b ==> Layer
\bCCObject\b ==> Ref
...

\baddTouchEventListener\s*\(\s*(.+)\s*,\s*toucheventselector\s*\((.*)\)\s*\) ==> addTouchEventListener( CC_CALLBACK_2( $2, $1 ))
\bTouchEventType\b ==> ui::Widget::TouchEventType
\bTOUCH_EVENT_([A-Z]+)\b ==> ui::Widget::TouchEventType::$1
\baddWidget\b\s*\( ==> addChild(

注释: \b代表单词的分割符. ()代表被标记的内容, $1 代表原始字符串中被标记的内容中的第一段. 具体请参考在 Visual Studio 中使用正则表达式.

include文件变更

因为包结构的变化, 所以有些组件的定义会未被include.
主要用到的head文件有:

head 描述
cocos2d.h cocos2dx的基本数据类和Node类都包含在里面
ui/CocosGUI.h cocos2dx 绝大部分的UI类都包含在内. 2.x版本中, UI类都包含在cocos-ext.h中. 所以绝大部分原来引用cocos-ext.h的地方都需要引用此文件
cocostudio/CocoStudio.h cocostudio功能相关的类都包含在里面. 最主要的是各种读取json文件的Reader. 其次, 是Armature动画.
cocos-ext.h 相比2.x的cocos-ext.h, 此文件做了非常大的精简. 现在主要包括CCScrollView及其子类, 另外还有EditBox

std::function作为监听函数

虽然, 3.1 的版本仍然支持绝大多数老版本的回调函数方式, 比如: m_btnSubmit->addTouchEventListener (this,toucheventselector(RewardItemCell::onBtnSubmitClick));仍然能工作. 但是, 不能保证在将来的3.x版本中仍然如此. 所以, 尽量一次搞定吧. 在obj-c式命名 ==> c++命名空间式命名章节中, 有批量替换的正则表达式,可作为参考.

功能更新

中间层UILayer的去除

在2.x版本, Widget需要作为UILayer的孩子节点才能响应触摸事件; 而在3.x版本中取消了这个限制, Widget的响应机制变为跟Menu类似. 目前, 我也没有大量的去掉之前的UILayer层, 大部分功能仍然正常工作.

键盘响应方式变更

在2.x版本中, 键盘响应的监听是用CCDirector::sharedDirector()->getKeypadDispatcher()->addDelegate(this);实现的. 在3.x版本中是这样:

1
2
3
auto keyListener = EventListenerKeyboard::create();
keyListener->onKeyReleased = CC_CALLBACK_2(MainScene::keyBackClicked, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(keyListener, this);

监听的取消, 也要做响应的替换.

Armature的陷阱

在2.x版本中, 一般通过下面的方式来监听Armature的事件. 在监听到COMPLETE的时候, 可以将此Armature移出场景.

1
2
CCArmature* swf = CCArmature::create(swfName);
swf->getAnimation()->setMovementEventCallFunc(this, movementEvent_selector(OutCardEffectHelper::onAnimationEnd));

但是在3.x中, 我们不能在监听事件的回调函数中做任何可能会导致release此对象的操作. 否则, 会导野指针错误.
是不是有点鸡肋? 我们来看看这种情况是怎么发生的:

1
2
3
4
5
6
7
8
Scheduler->Armature:update(t)
Armature->ArmatureAnimation:update(t)
ArmatureAnimation->this:告诉监听者
Note right of this:假设此时触发了COMPLETE事件
Note right of this: this->remove Armature, release Armature
Armature->Bone: update(t)
Note right of Armature: set `_armatureTransformDirty = false`
Note right of Armature: but now, Armature has been RELEASED!!!

我临时用定时器来移除Armature的. 具体过程如下:

  1. ArmatureAnimation增加一个公开函数: virtual MovementData *getMovementData() const { return _movementData; }
  2. 通过下面的方式来移除: (PS: Lmada 表达式真是好用啊啊啊啊啊 )
1
2
3
4
5
6
auto data = swf->getAnimation()->getMovementData();
float speed = data->scale;
float frames = data->duration;
float delay = frames/60/speed;
string id = FORMAT_TEXT( "%p", swf );
Director::getInstance()->getScheduler()->schedule( [swf](float t){swf->removeFromParent();},this,0,0,delay,false, id);

更加优雅的anchorPoint

如果你加载了cocostudio生成的ui json文件, 并改动过内部Widget的位置的话, 你会发现, 代码中x=20跟cocostudio ui编辑器中x=20效果是不一样的.
问题出在哪里呢?
简单来说, 2.x版本中的anchorPoint对自己在父亲节点的位置和孩子节点在自己中的位置都有影响; 3.x版本中的anchorPoint只对自己在父亲节点中的位置有影响.

举个例子. 假设A是父亲节点,B是A的孩子节点. A和B同为10*10的正方形.

1
2
3
4
5
6
A.anchorPoint = Point(0.5,0.5);
A.setSize(Size(10,10))
B.anchorPoint = Point(0.5,0.5);
B.setSize(Size(10,10))
B.setPosition( Point::ZERO );
A.addChild(B);

在2.x版本中: A和B的位置完全重合
在3.x版本中: B的中心点和A的左下角的点重合.

此处是位置示意图

Sprite的默认GLProgram

什么?你没听过GLProgram, 那恭喜你, 可以跳过这小节了. 因为你肯定不会出现下面的恼人情况.

SHADER_NAME_POSITION_TEXTURE_COLOR是2.x版本中的默认GLProgram. 它对应的vect和frag分别是:ccPositionTextureColor_vertccPositionTextureColor_frag. vect用来确定位置,frag用来确定色彩.

然而, 在3.x版本中,默认的GLProgramSHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP, 对应的vect和frag分别是:ccPositionTextureColor_noMVP_vertccPositionTextureColor_noMVP_frag.

如果你恰好用过ccPositionTextureColor_vert的话, 建议改为ccPositionTextureColor_noMVP_vert. 否则会出现莫名的位置偏移问题.

stl::vector的不稳定排序导致的层级问题

2.x版本中, 如果没有对孩子设置过zOrder的话, 孩子节点的覆盖顺序为: 后加的节点在上层, 先加的节点在下层.

3.x版本中, win32环境下, 孩子节点的覆盖层级还能保续. 但是在android平台下, zOrder相同的孩子, 层级顺序是随机的. 关键在下面的代码:

1
2
3
4
5
6
7
void Node::sortAllChildren()
{
if( _reorderChildDirty ) {
std::sort( std::begin(_children), std::end(_children), nodeComparisonLess );
_reorderChildDirty = false;
}
}

win32和android平台对stl::sort()的实现不同, android下的排序算法不是稳定的. 解决方法:

  1. 修改上述代码, 实现稳定排序
  2. 添加孩子节点的时候手动设置zOrder来保证层级顺序.

umeng等第三方库的更新

第三方库, 也有很多跟cocos2dx版本相关. 注意升级, 否则会闪退.

摘要: 此文对cocos2d-x引擎中最具代表性,最能体现框架结构的几个类做了简单的介绍, 包括Director,Application, Renderer, EventDispatcher, Scheduler. 对于这些类, 也只对关系主要流程的方法做了介绍, 略过了容错代码和其它细节. 主要目的是让大家快速的对cocos2d-x引擎有一个全面笼统的认识, 也方便快速定位问题.


EventDispatcher

EventDispatcher,EventListener,Event之间的关系

  • EventDispatcher: 事件分发器, 相当于所有事件的中控中心. 管理着EventListener,当一个Event到来的时候决定CallBack的调用顺序。
  • Event ( EventTouch, EventKeyboard 等), 具体的事件数据,
  • EventListener ( EventListenerTouch, EventListenerKeyboard 等 ): 建立了EventCallBack的映射关系, EventDispatcher 根据这种映射关系调用对应的 CallBack.

Event

Event有以下几种类型:

1
2
3
4
5
6
7
8
9
enum class Type
{
TOUCH,
KEYBOARD,
ACCELERATION,
MOUSE,
FOCUS,
CUSTOM
};

Event最重要的属性就是type, 标识了它是那种类型的事件, 也决定了由哪个EventListner来处理它.

EventListener

EventListner有以下几种类型:

1
2
3
4
5
6
7
8
9
10
11
enum class Type
{
UNKNOWN,
TOUCH_ONE_BY_ONE,
TOUCH_ALL_AT_ONCE,
KEYBOARD,
MOUSE,
ACCELERATION,
FOCUS,
CUSTOM
};

除了UNKNOWN, 跟Event::Type相比,Event::Type::TOUCH同时被两种类型的EventListener处理: TOUCH_ONE_BY_ONETOUCH_ALL_AT_ONCE. 这两种EventListener分别处理单点触摸事件和多点触摸事件. 多说几句: 假如一个TouchEvent事件中有多个触摸点, 那么类型为 EventListener::Type::TOUCH_ONE_BY_ONEEventListener 会把这个事件分解成若干个单点触摸事件来处理. 而类型为 EventListener::Type::TOUCH_ALL_AT_ONCEEventListener 就是来处理多点触摸的, 会一次处理它.
其它几种类型都是一一对应的, 即一种Event::TypeEvent会被对应类型的EventListener接受.

存放 EventListener 的地方

EventDispatcher中, 它把以上7种 EventListener::Type 类型的 EventListner 放到7个队列中. 也就是在这样一个字段中:

1
std::unordered_map<EventListener::ListenerID, EventListenerVector*> _listenerMap;
  • EventListener::ListenerID : 每一种EventListener::Type有唯一的 EventListener::ListenerID. 其实通过这段代码 typedef std::string ListenerID; 可知: EventListener::ListenerID 就是简单 string, 就是一个名称而已.
  • EventListenerVector: 顾名思义, 就是一个 EventListener 的向量容器. 相对于普通的向量容器, 它增加了priority管理功能.

EventListener的fixedPriority

简单来说, 每个 EventListener 有自己的 fixedPriority 属性, 它是一个整数.

EventListener的遍历顺序

EventDispatcher 在抛发事件的时候, 会先处理 Event 的时候, 会优先遍历 fixedPriority 低的 EventListener, 调用它的 CallBack. 在某些条件下, 一个 Event 被一个 EventListener 处理之后, 会停止遍历其它的 EventListener. 反映到实战中就是: 你监听了某种事件, 这种事件也出发了, 但是对应的回调函数并没有被调用, 也就是被优先级更高的 EventListener 截获了.

如果 fixedPriority 一样呢? 按照什么顺序?

  1. fixedPriority 为0, 这个值是专门为 Scene Object 预留的. 即, 默认情况下, 绝大多数继承自 Node 的对象添加的普通事件监听器, 其 fixedPriority 都为0. 此时, NodeglobalZOrder决定了优先级, 值越大, 越先被遍历到, 即在显示层中层级越高, 越先接受事件. 这在ui响应逻辑中也是合理的.

  2. fixedPriority 不为0, 那就按添加顺序.

    Event在什么条件下会被优先级更高的EventListener截获?

  3. 对于 EventListenerTouchOneByOne, 它有一个字段: _needSwallow, 当它为 true 的时候, 如果它接受了某个 Event, 优先级更低的 EventListener 就接受不到了. 可以用 EventListenerTouchOneByOne::setSwallowTouches(bool needSwallow) 来改变它.

  4. 对于其它类型的 EventLIstener, 只有在显示调用了 Event::stopPropagation() 的时候, 才会中断遍历.

核心函数: EventDispatcher::dispatchEvent()

下面我们看看EventDispatcher最核心的函数:
void EventDispatcher::dispatchEvent(Event* event): 当有响应的事件到来的时候, 都会调用这个函数来通知监听了此事件的对象.

其实, 上面的介绍, 已经把这个函数里绝大部分逻辑都描述了,这里做一个最后的总结
事件抛发的简要流程如下:

  1. 检查 _listenerMap 中所有的EventListnerVector, 如果哪个容器的 EventListener 优先级顺序需要更新, 则重新排序
  2. 对于类型为 Event::Type::TOUCH 的事件, 则按照EventListener的遍历顺序遍历所有的 EventListener. 只有接受了 EventTouch::EventCode::BEGAN 事件的 EventListener, 才会收到其他类型的 EventTouch 事件.
  3. 对于其他类型的事件, 也按照EventListener的遍历顺序的顺序遍历对应的EventListener.

总结

Eventdispatcher 中的其它函数, 主要功能都是 添加EventListener, 删除EventListener等, 不做详细介绍.

总的来说, Eventdispatcher 是一个中转器:

  1. 事件的产生模块儿, 只关心自己构造正确的 Event, 调用 EventDispatcher::dispatchEvent(Event* event) 交给 EventDispatcher.
  2. 需要监听事件的模块儿, 只需调用 EventDispatcher::addEventListener(EventListener* listener) (或者它的其它变种)来注册自己作为监听者.
  3. EventDispatcher 的作用是:
    1. 把特定类型的 Event 送给对应类型的 EventListener.
    2. 对于同一种 Event, 规定了事件送达的优先级.

Written with StackEdit.

摘要: 此文对cocos2d-x引擎中最具代表性,最能体现框架结构的几个类做了简单的介绍, 包括Director,Application, Renderer, EventDispatcher, Scheduler. 对于这些类, 也只对关系主要流程的方法做了介绍, 略过了容错代码和其它细节. 主要目的是让大家快速的对cocos2d-x引擎有一个全面笼统的认识, 也方便快速定位问题.


GLView

cocos2d-xopenGL的封装. 不同平台下, openGL有一些差别.

openGL

一段简单的例子

以下内容引用自Introduction to OpenGL. 需要更具体的介绍也可参考这个链接.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# include <whateverYouNeed.h>
main() {
InitializeAWindowPlease();

glClearColor (0.0, 0.0, 0.0, 0.0);
glClear (GL_COLOR_BUFFER_BIT);

glColor3f (1.0, 1.0, 1.0);
glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0);
glBegin(GL_POLYGON);
glVertex3f (0.25, 0.25, 0.0);
glVertex3f (0.75, 0.25, 0.0);
glVertex3f (0.75, 0.75, 0.0);
glVertex3f (0.25, 0.75, 0.0);
glEnd();
glFlush();

UpdateTheWindowAndCheckForEvents();
}

OpenGL Command Syntax

  • OpenGL commands use the prefix gl and initial capital letters for each word making up the command name
  • some seemingly extraneous letters appended to some command names (for example, the 3f in glColor3f() and glVertex3f())

OpenGL as a State Machine

OpenGL is a state machine. You put it into various states (or modes) that then remain in effect until you change them.

Application

主要方法:

1
2
3
4
virtual const char * getCurrentLanguage();
virtual Platform getTargetPlatform();
virtual void setAnimationInterval(double interval);
int run();//启动主循环

run()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int Application::run()
{

...

while(!glview->windowShouldClose())
{
QueryPerformanceCounter(&nNow);
if (nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart)
{
nLast.QuadPart = nNow.QuadPart;

director->mainLoop(); //Director进行这一帧的渲染
glview->pollEvents(); // This function processes only those events that have already been received and then returns immediately.
}
else
{
Sleep(0);
}
}

...

return true;
}

Director

主要函数预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//openGL Matrix Operate
void pushMatrix(MATRIX_STACK_TYPE type);
void popMatrix(MATRIX_STACK_TYPE type);
void loadIdentityMatrix(MATRIX_STACK_TYPE type);
void loadMatrix(MATRIX_STACK_TYPE type, const Mat4& mat);
void multiplyMatrix(MATRIX_STACK_TYPE type, const Mat4& mat);
Mat4 getMatrix(MATRIX_STACK_TYPE type);
void resetMatrixStack();

//View Data
inline double getAnimationInterval();
inline bool isDisplayStats();
inline GLView* getOpenGLView();
inline Projection getProjection();
Size getVisibleSize() const;

Vec2 getVisibleOrigin() const;
Vec2 convertToGL(const Vec2& point);
Vec2 convertToUI(const Vec2& point);
float getZEye() const;

// Scene 场景管理
inline Scene* getRunningScene();
void runWithScene(Scene *scene);
void pushScene(Scene *scene);


// 控制绘制的暂停和恢复
void end();
void pause();
void resume();

//绘制图形(界面展示最重要的函数)
void drawScene();

//Getter and Setter
Scheduler* getScheduler() const { return _scheduler; }
void setScheduler(Scheduler* scheduler);

ActionManager* getActionManager() const { return _actionManager; }
void setActionManager(ActionManager* actionManager);

EventDispatcher* getEventDispatcher() const { return _eventDispatcher; }
void setEventDispatcher(EventDispatcher* dispatcher);

Renderer* getRenderer() const { return _renderer; }

drawScene(): 主要绘制函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Draw the Scene
void Director::drawScene()
{
...

if (! _paused)
{
_scheduler->update(_deltaTime); //Scheduler 定时器 更新
_eventDispatcher->dispatchEvent(_eventAfterUpdate); //Dispatcher 抛发事件.
}

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //glClear

if (_nextScene) //取得下一个将要显示的Scene.
{
setNextScene();
}

pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW); //将上一次绘制的Context放到堆栈

// draw the scene
if (_runningScene)
{
_runningScene->visit(_renderer, Mat4::IDENTITY, false);
_eventDispatcher->dispatchEvent(_eventAfterVisit);
}

_renderer->render(); //渲染
_eventDispatcher->dispatchEvent(_eventAfterDraw);

popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW); //返回到上一次绘制时的状态.


// swap buffers
if (_openGLView)
{
_openGLView->swapBuffers(); //把上面渲染的结果显示到屏幕
}

...
}

Node::visit() 函数

预览

Node::visit() 的主要功能就是

  1. 调用所有孩子的visit函数
  2. 调用self->draw()函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)
    {
    // quick return if not visible. children won't be drawn.
    if (!_visible)
    {
    return;
    }

    uint32_t flags = processParentFlags(parentTransform, parentFlags);

    // IMPORTANT:
    // To ease the migration to v3.0, we still support the Mat4 stack,
    // but it is deprecated and your code should not rely on it
    Director* director = Director::getInstance();
    director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, _modelViewTransform);

    int i = 0;


    if(!_children.empty())
    {
    sortAllChildren();
    // draw children zOrder < 0
    for( ; i < _children.size(); i++ )
    {
    auto node = _children.at(i);

    if ( node && node->_localZOrder < 0 )
    node->visit(renderer, _modelViewTransform, flags);
    else
    break;
    }
    // self draw
    this->draw(renderer, _modelViewTransform, flags);

    for(auto it=_children.cbegin()+i; it != _children.cend(); ++it)
    (*it)->visit(renderer, _modelViewTransform, flags);
    }
    else
    {
    this->draw(renderer, _modelViewTransform, flags);
    }

    director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    }

Node::draw()

因为Node是所有可显示对象的父类, 没有任何显示内容, 所以draw函数为空.
这里我们以Sprite::draw函数为例简单介绍下draw的作用.

1
2
3
4
5
6
7
8
9
10
11
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
// Don't do calculate the culling if the transform was not updated
_insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;

if(_insideBounds)
{
_quadCommand.init(_globalZOrder, _texture->getName(), getGLProgramState(), _blendFunc, &_quad, 1, transform);
renderer->addCommand(&_quadCommand);
}
}

我们看到, Sprite::draw函数主要实现了[添加一个QuadCommandRender中去]的功能.
再看看Label的绘制函数.

Label::draw

1
2
3
4
5
6
7
8
9
10
11
void Label::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
// Don't do calculate the culling if the transform was not updated
_insideBounds = (flags & FLAGS_TRANSFORM_DIRTY) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;

if(_insideBounds) {
_customCommand.init(_globalZOrder);
_customCommand.func = CC_CALLBACK_0(Label::onDraw, this, transform, flags);
renderer->addCommand(&_customCommand);
}
}

其实, 跟Sprite::draw也差不多. 关键在于这个RenderCommand怎么构造和执行的.

Renderer 渲染器

主要函数预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void initGLView();

/** Adds a `RenderComamnd` into the renderer */
void addCommand(RenderCommand* command);

/** Adds a `RenderComamnd` into the renderer specifying a particular render queue ID */
void addCommand(RenderCommand* command, int renderQueue);

/** Pushes a group into the render queue */
void pushGroup(int renderQueueID);

/** Pops a group from the render queue */
void popGroup();

/** Creates a render queue and returns its Id */
int createRenderQueue();

/** Renders into the GLView all the queued `RenderCommand` objects */
void render();

可见它主要由两个功能:

  1. ReanderCommand进行排序和分类管理
  2. 进行渲染:render()

渲染函数Renderer::render()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Renderer::render()
{
...

if (_glViewAssigned)
{
...
//排列渲染队列
for (auto &renderqueue : _renderGroups)
{
renderqueue.sort();
}
//进行渲染
visitRenderQueue(_renderGroups[0]);
...
}
...
}

Renderer::visitRenderQueue

按照顺序执行所有的 RenderCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void Renderer::visitRenderQueue(const RenderQueue& queue)
{
ssize_t size = queue.size();

for (ssize_t index = 0; index < size; ++index)
{
auto command = queue[index];
auto commandType = command->getType();
if(RenderCommand::Type::QUAD_COMMAND == commandType)
{
auto cmd = static_cast<QuadCommand*>(command);
//Batch quads
if(_numQuads + cmd->getQuadCount() > VBO_SIZE)
{
drawBatchedQuads();
}

_batchedQuadCommands.push_back(cmd);

memcpy(_quads + _numQuads, cmd->getQuads(), sizeof(V3F_C4B_T2F_Quad) * cmd->getQuadCount());
convertToWorldCoordinates(_quads + _numQuads, cmd->getQuadCount(), cmd->getModelView());

_numQuads += cmd->getQuadCount();

}
else if(RenderCommand::Type::GROUP_COMMAND == commandType)
{
flush();
int renderQueueID = ((GroupCommand*) command)->getRenderQueueID();
visitRenderQueue(_renderGroups[renderQueueID]);
}
else if(RenderCommand::Type::CUSTOM_COMMAND == commandType)
{
...
}
...
}
}

openGL VAO, VBO 介绍.

GLSL渲染语言入门与VBO、VAO使用:绘制一个三角形
OpenGL 4.0 VAO VBO 理解

Schelduler介绍

Scheldulercocos2d-x中实现延迟调用,定时调用时最重要的功能. 类似于其他语言中的Timer
他最核心的函数就是:

1
void schedule(const ccSchedulerFunc& callback, void *target, float interval, unsigned int repeat, float delay, bool paused, const std::string& key);

用来启动一个定时操作: 在延迟delay时间后, 每隔repeat时间, 调用一次callback. target用来标记这个操作属于谁, 方便管理, 比如在析构的时候调用void unschedule(void *target)即可移除当前对象的所有定时操作.

Schelduler的其它大部分方法, 要么是它的衍生, 为了减少调用参数; 要么是对定时操作的控制, 比如暂停, 恢复, 移除等. 如果只对想对框架的各个模块有大概的了解, 可以不做深入.

EventDispatcher

(后续添加)

摘要: 在android上开发c++应用, crash日志都是汇编码, 很难对应到c++代码中去. 通过此文, 你可以定位到程序崩溃时的C++代码, 精确查找问题.

背景介绍

  1. 本文主要内容: 利用android的crash log来对c++开发的android应用进行错误定位.

  2. 容易稳定复现的BUG, 一般可以通过断点调试来解决. 如果测试人员也无法稳定复现, log就成了程序吊定位问题的救命稻草.

  3. 通用操作系统都有自己的日志系统, android也不例外. 救命稻草已经给你了~ ( 怎样查看android的系统日志 )

  4. 但是, android的系统日志在c++代码崩溃时, 打印的都是内存地址和寄存器. 比如, 这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
06-20 15:54:35.331 23889 23889 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
06-20 15:54:35.331 23889 23889 I DEBUG : Build fingerprint: 'google/razorg/deb:4.4.2/KOT49H/937116:user/release-keys'
06-20 15:54:35.331 23889 23889 I DEBUG : Revision: '0'
06-20 15:54:35.331 23889 23889 I DEBUG : pid: 1981, tid: 2020, name: Thread-3399 >>> com.guangyou.ddgame <<<
06-20 15:54:35.331 23889 23889 I DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000028
06-20 15:54:35.431 187 710 D audio_hw_primary: out_set_parameters: enter: usecase(0: deep-buffer-playback) kvpairs: routing=2
06-20 15:54:35.511 23889 23889 I DEBUG : r0 76d94458 r1 00000000 r2 00000000 r3 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : r4 760c1a48 r5 751e2440 r6 00000001 r7 760c1a48
06-20 15:54:35.511 23889 23889 I DEBUG : r8 00000001 r9 76c96f3c sl 76c861c0 fp 76d94444
06-20 15:54:35.511 23889 23889 I DEBUG : ip 00000001 sp 76d94430 lr 75a81bd8 pc 75a81bdc cpsr 600f0010
06-20 15:54:35.511 23889 23889 I DEBUG : d0 746968775f327865 d1 6a6e6169642f675f
06-20 15:54:35.511 23889 23889 I DEBUG : d2 5f6f616978757169 d3 676e702e6e776f6d
06-20 15:54:35.511 23889 23889 I DEBUG : d4 0000000009000000 d5 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d6 0000000000000000 d7 0000000000000000

这密密麻麻的都是些神马, 是人看的么?

饿. 这个麻… 谁让你当程序猿! 让你当! 活该要看天书!

硬着头皮也要来, 我们就来讲讲怎么消化天书吧~

怎样获取android的系统日志

假设你已经安装了 Android Develop Tools, 可以成功调用adb. 并打开android开发用机的调试模式, 连接到电脑.

打开命令行, 在命令行输入: adb logcat. 就可以看到满屏幕的日志啦.
输入adb logcat --help可以看到 logcat的用法提示.

这里有两个参数特别提醒一下, 比较常用:
1. -v XXXX: 用来选择log输出样式, 一般建议 threadtime, 更加详细.
2. -d: 让log一次性输出后马上完毕. 如果没有此命令, logcat 工具会一直输出, 即使更新在界面上.

如果需要保存log到文件, 方便以后查看. 可输入命令:
adb logcat -v threadtime -d > log.txt

理解NDK的crash log

如果你用c++开发的android应用在运行过程中, c++代码发生错误导致程序崩溃, 系统就会记录 crash log到上述的系统日志中.

下面是我正在开发的游戏一次崩溃后, 截取的日志( 插个广告, 全名斗地主下载地址: http://sj.ddwan.com )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
06-20 15:54:35.331 23889 23889 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
06-20 15:54:35.331 23889 23889 I DEBUG : Build fingerprint: 'google/razorg/deb:4.4.2/KOT49H/937116:user/release-keys'
06-20 15:54:35.331 23889 23889 I DEBUG : Revision: '0'
06-20 15:54:35.331 23889 23889 I DEBUG : pid: 1981, tid: 2020, name: Thread-3399 >>> com.guangyou.ddgame <<<
06-20 15:54:35.331 23889 23889 I DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000028
06-20 15:54:35.431 187 710 D audio_hw_primary: out_set_parameters: enter: usecase(0: deep-buffer-playback) kvpairs: routing=2
06-20 15:54:35.511 23889 23889 I DEBUG : r0 76d94458 r1 00000000 r2 00000000 r3 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : r4 760c1a48 r5 751e2440 r6 00000001 r7 760c1a48
06-20 15:54:35.511 23889 23889 I DEBUG : r8 00000001 r9 76c96f3c sl 76c861c0 fp 76d94444
06-20 15:54:35.511 23889 23889 I DEBUG : ip 00000001 sp 76d94430 lr 75a81bd8 pc 75a81bdc cpsr 600f0010
06-20 15:54:35.511 23889 23889 I DEBUG : d0 746968775f327865 d1 6a6e6169642f675f
06-20 15:54:35.511 23889 23889 I DEBUG : d2 5f6f616978757169 d3 676e702e6e776f6d
06-20 15:54:35.511 23889 23889 I DEBUG : d4 0000000009000000 d5 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d6 0000000000000000 d7 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d8 0000000000000000 d9 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d10 0000000000000000 d11 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d12 0000000000000000 d13 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d14 0000000000000000 d15 0000000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d16 c3c3c3c3c3c3c3c3 d17 c3c3c3c3c3c3c3c3
06-20 15:54:35.511 23889 23889 I DEBUG : d18 41c7ddc227000000 d19 3ff0000000000000
06-20 15:54:35.511 23889 23889 I DEBUG : d20 3f811110896efbb2 d21 3fd7096611460fdb
06-20 15:54:35.511 23889 23889 I DEBUG : d22 c0176a8ee0000000 d23 bfc5230c760b0605
06-20 15:54:35.511 23889 23889 I DEBUG : d24 0000000000000000 d25 3fc7922925a107e2
06-20 15:54:35.511 23889 23889 I DEBUG : d26 3fdaa0f8fab43e33 d27 3fb43ad076b251ab
06-20 15:54:35.511 23889 23889 I DEBUG : d28 3fa15cb6bdc3c156 d29 3ec6cd878c3b46a7
06-20 15:54:35.511 23889 23889 I DEBUG : d30 3f65f3b6b9b97e01 d31 3ef99342e0ee5069
06-20 15:54:35.511 23889 23889 I DEBUG : scr 20000012
06-20 15:54:35.511 23889 23889 I DEBUG :
06-20 15:54:35.511 23889 23889 I DEBUG : backtrace:
06-20 15:54:35.511 23889 23889 I DEBUG : #00 pc 0089cbdc /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocos2d::Texture2D::getContentSize() const+32)
06-20 15:54:35.511 23889 23889 I DEBUG : #01 pc 0088f8dc /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocos2d::Sprite::setTexture(std::string const&)+128)
06-20 15:54:35.511 23889 23889 I DEBUG : #02 pc 007863dc /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocos2d::ui::Button::loadTextureDisabled(std::string const&, cocos2d::ui::Widget::TextureResType)+336)
06-20 15:54:35.511 23889 23889 I DEBUG :
06-20 15:54:35.511 23889 23889 I DEBUG : stack:
06-20 15:54:35.511 23889 23889 I DEBUG : 76d943f0 00000001
06-20 15:54:35.511 23889 23889 I DEBUG : 76d943f4 4006bc0d /system/lib/libc.so (free+12)
06-20 15:54:35.511 23889 23889 I DEBUG : 76d943f8 76a72c54
06-20 15:54:35.511 23889 23889 I DEBUG : 76d943fc 75eca614 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94400 751c23c8 [anon:libc_malloc]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94404 751c23c8 [anon:libc_malloc]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94408 751c23c8 [anon:libc_malloc]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9440c 75a749b4 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocos2d::Sprite::setTexture(cocos2d::Texture2D*)+128)
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94410 0000003d
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94414 00e8efc8
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94418 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9441c 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94420 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94424 76d94458 [stack:2020]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94428 00000020
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9442c 76d94444 [stack:2020]
06-20 15:54:35.511 23889 23889 I DEBUG : #00 76d94430 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94434 76d94458 [stack:2020]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94438 76a66184
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9443c 760c1a48 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94440 76d9447c [stack:2020]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94444 75a748e0 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocos2d::Sprite::setTexture(std::string const&)+132)
06-20 15:54:35.511 23889 23889 I DEBUG : #01 76d94448 76d944ec [stack:2020]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9444c 793ff0e8 [anon:libc_malloc]
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94450 76a72c54
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94454 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94458 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9445c 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94460 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94464 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d94468 00000000
06-20 15:54:35.511 23889 23889 I DEBUG : 76d9446c 00000000
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94470 7b91dcf8 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94474 78ce6c50 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94478 76d944b4 [stack:2020]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d9447c 7596b3e0 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocos2d::ui::Button::loadTextureDisabled(std::string const&, cocos2d::ui::Widget::TextureResType)+340)
06-20 15:54:35.521 23889 23889 I DEBUG : #02 76d94480 00000001
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94484 00000000
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94488 76d944ec [stack:2020]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d9448c 793fe780 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94490 76d944f0 [stack:2020]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94494 793ff0e8 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94498 00000001
06-20 15:54:35.521 23889 23889 I DEBUG : 76d9449c 4006bc0d /system/lib/libc.so (free+12)
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944a0 76a72c54
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944a4 75eca614 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944a8 78ce6c50 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944ac 78ce6c50 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944b0 76d9455c [stack:2020]
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944b4 75924e54 /data/app-lib/com.guangyou.ddgame-1/libcocos2dcpp.so (cocostudio::ButtonReader::setPropsFromJsonDictionary(cocos2d::ui::Widget*, rapidjson::GenericValue<rapidjson::UTF8<char>, rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator> > const&)+752)
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944b8 00000000
06-20 15:54:35.521 23889 23889 I DEBUG : 76d944bc 78ce6c50 [anon:libc_malloc]
06-20 15:54:35.521 23889 23889 I DEBUG :
06-20 15:54:35.521 23889 23889 I DEBUG : memory near r0:
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94438 76a66184 760c1a48 76d9447c 75a748e0
06-20 15:54:35.521 23889 23889 I DEBUG : 76d94448 76d944ec 793ff0e8 76a72c54 00000000
...
06-20 15:54:35.521 23889 23889 I DEBUG :
06-20 15:54:35.521 23889 23889 I DEBUG : memory near r4:
06-20 15:54:35.521 23889 23889 I DEBUG : 760c1a28 760811c8 75ee318c 75ee3194 75ee319c
06-20 15:54:35.521 23889 23889 I DEBUG : 760c1a38 4006d091 75f9a1f4 75f4ee5c 75e8ea0c
...

下面来逐行解读:

  1. ndk crash log以*** *** *** *** ***开始.
  2. 第一行Build fingerprint: 'google/razorg/deb:4.4.2/KOT49H/937116:user/release-keys' 指明了运行的Android版本, 如果您有多份crash dump的话这个信息就比较有用了.
  3. 接着一行显示的是当前的线程id(pid)和进程id(tid). 如果当前崩溃的线程是主线程的话, pid和tid会是一样的~
  4. 第四行, 显示的是unix信号. 这里的signal 11, 即SIGSEGV, 表示段错误, 是最常见的信号.(什么是unix信号, 什么是SIGSEGV)
  5. 接下来的部分是系统寄存器的dump信息.
    符号 解释
    rX(X=[0~9]) 代表整数寄存器
    dX(X=[0~31]) 是浮点指针寄存器
    fp (or r11) 指向当前正在执行的函数的堆栈底.
    ip (or r12) 一个寄存器, 我也没弄明白是干啥的.
    sp (or r13) 当前正在执行的函数的堆栈顶.(跟fp相对应)
    lr (or r14) link register. 简单来说, 当当前指令执行完了,
    就会从这个寄存器获取地址, 来知道需要返回
    到哪里继续执行.
    pc (or r15) program counter. 存放下一条指令的地址
    cpsr Current Program Status Register. 表示当前
    运行环境和状态的一些字节位.
  6. Crash dump还包含PC之前和之后的一些内存字段.
  7. 最后, 是崩溃时的调用堆栈. 如果你执行的是debug版本, 还能还原一些c++代码.

利用ndk-stack定位崩溃代码

上面的一些信息能简单的帮你定位以下问题. 如果信息量还不够大的话, 那就还有最后一招: 还原历史.

Android NDK自从版本R6开始, 提供了一个工具ndk-stack( 在目录{ndk_root}/中 ). 这个工具能自动分析dump下来的crash log, 将崩溃时的调用内存地址和c++代码一行一行对应起来.

我们先看一下用法, 执行命令ndk-stack --help

1
2
3
4
5
6
7
Usage:
ndk-stack -sym <path> [-dump <path>]

-sym Contains full path to the root directory for symbols.
-dump Contains full path to the file containing the crash dump.
This is an optional parameter. If ommited, ndk-stack will
read input data from stdin
  • -dump参数很容易理解, 即dump下来的log文本文件. ndk-stack会分析此文件.
  • -sym参数就是你android项目下,编译成功之后,obj目录下的文件.

下面我们就来示范一下:

1
2
3
4
5
6
7
8
9
10
11
$ adb logcat | ndk-stack -sym ./obj/local/armeabi
********** Crash dump: **********
Build fingerprint: 'htc_wwe/htc_bravo/bravo:2.3.3/
GRI40/96875.1:user/release-keys'
pid: 1723, tid: 1743 >>> com.packtpub.droidblaster <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0000000c
Stack frame #00 pc 00010a2c /data/data/com.packtpub.droidblaster/lib/libdroidblaster.so: Routine update in /home/packt/Project/Chapter11/DroidBlaster_Part11/jni/TimeService.cpp:25
Stack frame #01 pc 00009fcc /data/data/com.packtpub.droidblaster/lib/libdroidblaster.so: Routine onStep in /home/packt/Project/Chapter11/DroidBlaster_Part11/jni/DroidBlaster.cpp:53
Stack frame #02 pc 0000a348 /data/data/com.packtpub.droidblaster/lib/libdroidblaster.so: Routine run in /home/packt/Project/Chapter11/DroidBlaster_Part11/jni/EventLoop.cpp:49
Stack frame #03 pc 0000f994 /data/data/com.packtpub.droidblaster/lib/libdroidblaster.so: Routine android_main in /home/packt/Project/Chapter11/DroidBlaster_Part11/jni/Main.cpp:31
...

熟悉的代码出现啦~~

摘要:此文是书籍《行之有效:IT技术团队管理之道》的读书笔记. 主要是方便自己回顾. 您也可以通过此文简要了解此书的内容.

IT技术团队员工的特点

  1. 高学历, 知识密集型. 技术立身.
  2. 人际关系简单. 摩擦大多数由技术产生, 相处也比较容易.
  3. 需要较高的自由度.( 经理的职能不是强迫人们工作, 而是让人自觉地工作 ). 一是希望完成任务之后能做自己的事情; 二是希望以自己的方式完成工作.
  4. 发展前途. 没有发展前途, 就会离开.
  5. 敏感, 流动性大.

关于招聘的考量

  1. 技术能力满足基本要求
  2. 做事风格与团队匹配
  3. 沟通能力
  4. 工作努力.

员工的本性很重要, 将来的塑造过程中很难改变.

关于员工塑造

通常把员工看作是一员大将, 他们各自为团队镇守一方. 这样, 一个团队的战斗力才全面, 强劲.

另一个方面来讲, 一个人认为应该帮他(她)的人越多, 他的责任感就越小. 我们需要明确每一个人的责任.

员工责任感与参与度

一个人的积极性和责任感是性格的一部分, 虽然可以提高, 但是不要寄予太大的希望. 招聘的时候对这一方面要好好考察.

管理者的一个重要责任就是让员工负责. 如果员工不明白自己的职责所在, 或者总认为自己可以部分责任, 管理者会很辛苦, 而且团队和项目会走向失败.

员工会执行分配下来的任务, 但是会支持自己参与的事情. 简单的执行和内心的支持是不一样的.

提高员工参与度的方法:

  1. 外围的事情, 可以由”民主”决定.
  2. 日常的工作, 比如Code review也可以由员工参与
  3. 制定规则前, 征询大家的意见,最好能在会议中通过公开的讨论“诱导”大家得出此规则.

激发员工的责任感

  • 第一个要素就是: 自己以身作则. 自己处处率先垂范, 不管怎么样的员工都会慢慢跟上.
  • 光环理论: 如果你希望员工做到什么, 就要去表扬他们,好像他们已经做到了. 员工主动维护这个形象的力量是很大的.

权利和影响力

官方权利的基础上培养个人影响力

新管理者要给员工心理上接受和磨合的时间. ( 自己招聘的员工会有先天的”拥护”光环. )

管理者怎么提高个人影响力:

  1. 专业能力要强
  2. 工作努力.( 风格可有差异 )
  3. 对人态度好, 公平, 关心员工.

分清楚管与不管的界限

一个典型的IT技术团队的管理者被授予的权利包括:

  1. 团队日常管理
  2. 项目管理
  3. 绩效考核.
  4. 招聘新员工 ( 其中于技术相关的部分, 一部分的建议权 )
  5. 晋升, 加薪, 解雇等方面的建议权.

不包括:

  1. 财务权
  2. 采购权
  3. 完全的人事权
  4. 参与公司商业活动
  5. 参与公司决策

对于员工的关心, 原则是只管工作, 不管生活.
但是, 一个人情的社会, 必要的关心也不可缺少, 这里有一个比较简易的划分:

每个人的生活中, 有两部分, 一是普通的生活, 称为公生活, 例如身体不适, 孩子上学, 回家探亲, 周末逛街, 等等. 另一个是私密生活, 包括宗教恋情, 婚姻状态, 性取向, 价值观, 人生观, 等等. 公生活可以谈论, 而且管理者应该主动关心公生活. 对于私生活, 即使员工主动提及, 也不要深入探究.

要约和领地

心理要约, 是指公司级管理者与员工达成的约定, 特别是没有明文规定的, 彼此在内心达成的一致.

领地, 可以通俗的理解为”地盘”. 员工都有自己的”领地”, 比如座位空间, 其负责的工作, 等等. 应该尽量尊重他们的”领地”.

史玉柱说:

一定要尊重部下. 因为你这样做了, 他假如换个单位就碰不到这么好的领导. 所以遇到困难的时候他也不会走.
最重要的是你从内心深处一定要把他堪称是和你平等的人,有的老板会觉得你比我低一筹, 我是老板, 你是我的”马仔”. 假如你真的有这种想法, 必然会通过你的言行表现出来. 这样你周围的人必然不会跟你一条心. 人是对等的, 你一旦尊重他们, 他们就会更加尊重你.

关于个性

一般来说, 比较稳妥的做法是, 管理者只在团队前展示相对中性的喜好和个人观点, 例如读书, 锻炼, 旅游, 等等. 容易引起争论的则要避免, 不要有意或者无意的在团队面前强调, 例如对宗教,民族,社会问题比较尖锐的立场等.

对于管理者来说, 包容下属以与自己不同的方式工作, 是一大挑战. 君子和而不同, 除非真的有必要, 管理者不要去干预员工的工作方式.

保持距离

并不是对每一个人好, 就能获得尊重. 尊重原则, 有效执行, 才是管理的真谛.

随时激励

每个人都需要激励. 激励有一个保鲜期, 所以要及时, 多次激励.

考核

鼓励并没有错. 但是, 不同的人有不同的性格. 有的同事, 你要委婉一些. 有的同事, 你需要直接一些. 目标是一样的, 让他们能清楚的了解你的意思, 不用去猜. 在绩效考核的时候更要说得清楚, 否则, 会产生误解.

简介:很久很久以前, 网上流传着一个免费的,识别率暴高的,稳定的Speech To Text API, 那就是Google Speech API. 但是最近再使用的时候,总是返回500 Error. 后来通过查看源码知道需要增加一个参数:key=.... 可能是为了防止滥用吧. 并且, 最近Chrome另外发布了一个长连接实时的识别接口, 这对开发者来说真是巨大的福音啊. 在这里主要对这两个接口的用法进行介绍.

关键字

SpeechToText,API,google,STT,ASR,SR,speech,recognition

申请Chromium API keys

本文使用的Google Speech API是为google自家的浏览器Chrome服务的. 可以通过这个Demo体验一下实际使用的效果: Google Speech To Text Demo.
Chrome来源于开源项目Chromium. 为了方便开发者调试使用, google 开放了这个STT(Speech to Text)接口. 但是, 因为这个借口只供调试使用, 所以在流量和次数上都有限制.并且, 不提供购买.

好了, 背景介绍完毕, 我们来第一步: 申请Chromium开发者权限.
具体步骤请参考how to get chromium API keys).

Acquiring Keys

  1. Make sure you are a member of chromium-dev@chromium.org (you can just subscribe to chromium-dev and choose not to receive mail).
    For convenience, the APIs below are only visible to people subscribed to that group.
  2. Make sure you are logged in with the Google account associated with the email address that you used to subscribe to chromium-dev.
  3. Go to https://cloud.google.com/console(请使用旧版console)
  4. Click the red Create project… button.
  5. (Optional) You may add other members of your organization or team on the Team tab.
  6. In the ‘APIs & auth’ > APIs tab, click the On/Off button to turn each of the following APIs to the On position, and read and agree to the Terms of Service that is shown:

    (This list might be out of date; try searching for APIs starting with “Chrome” or having “for Chrome” in the name.) * Chrome Remote Desktop API
    • Chrome Spelling API
    • Chrome Suggest API
    • Chrome Sync API
    • Chrome Translate Element
    • Google Maps Geolocation API (requires enabling billing but is free to use; you can skip this one, in which case geolocation features of Chrome will not work)
    • Safe Browsing API
    • Speech API
    • Time Zone API
    • Google Cloud Messaging for Chrome
    • Google Now For Chrome API
      If any of these APIs are not shown, recheck step 1.
  7. Go to the Credentials tab under the APIs & auth tab.
  8. Click the red Create New Client ID button in the OAuth section to create an OAuth 2.0 client ID.
    • You want “Installed Application” for the Application type section
    • You want “Other” for the Installed application type section
  9. A new box should now appear titled “Client ID for installed applications”. In the next sections, we will refer to the values of the “Client ID” and “Client secret” fields in this box later (below).
  10. Click the red Create New Key button in the Public API Access section and create a new Browser key.
    You want to leave the box on the “Create a browser key and configure allowed referers” empty.
  11. A new box should appear titled “Key for browser applications”. The next sections will refer to the value of the “API key” field too.

好了, 到这里, 我们已经获得了应用key, 在下文我们用{key}表示这个key.

One Shot Recognition

我们用curl来向服务器发送请求:

1
2
3
4
5
curl -X POST \
--data-binary @speech.flac \
--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7' \
--header 'Content-Type: audio/x-flac; rate=8000;' \
'https://www.google.com/speech-api/v1/recognize?client=chromium&lang=zh-CN&maxresults=5&pfilter=0&key=AIzaSyC6Tkf4*****Q0CdISn-qnHhwLaS3cg2a0'
参数 解释
-X POST 表示发送HTTP请求
–data-binary @speech.flac 发送音频文件speech.flac
–user-agent ‘…’ http的参数,设置浏览器的user-agent信息
–header http的参数. 指定了传送内容的类型(audio/flac)和音频频率(8000Hz). 注意, 只支持特定的几种频率(8000Hz,4000Hz还有几个记不清了),上传的flac文件频率要和参数一致.
https://www.google.com/.../&key=
AIzaSyC6Tkf*****Q0CdISn-qnHhwLaS3cg2a0
http请求地址,其中最后一部分的key,应该替换为您申请的{key}.

等待一分钟左右, 如果你运气好的话, 能看到如下结果:
Result Image

结果格式如下, 应该很清晰了吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"status": 0,
"id": "b3447b5d98c5653e0067f35b32c0a8ca-1",
"hypotheses":
[
{
"utterance": "i like pickles",
"confidence": 0.9012539
},
{
"utterance": "i like pickle"
}
]
}

如果您录音的格式不对的话, 可以用开源软件sox方便的转换格式和码率. 举个栗子:

1
sox ./speech.mp3 -b 8 speech.flac trim 0 15
参数 解释
./speech.mp3 输入文件
-b 8 输出文件频率为 8kHz
speech.flac 输出文件名
trim 0 15 截取输入文件的0~15秒的部分, 输出出来

Stream Recognition

后来, Google 提供了更先进的live的双向的识别接口. 即同时打开两个HTTP连接, 一个负责实时发送(POST)音频流, 一个负责接受(GET).
这里有一个PHP版本的Demo. 可以参考实现您自己的Stream Recognition:
Google Speech API – Full Duplex PHP Version

引用:

  1. Google Speech API – Full Duplex PHP Version
    http://mikepultz.com/2013/07/google-speech-api-full-duplex-php-version/

  2. Accessing Google Speech API / Chrome 11
    http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/

  3. Google Speech To Text API ( 9 months ago )
    https://gist.github.com/alotaiba/1730160

  4. 避开Google Voice Search利用Google Speech API实现Android语音识别
    http://my.eoe.cn/sisuer/archive/5960.html

  5. How to Use Google Speech API( with sox )
    http://www.x2q.net/blog/2013/09/16/how-to-use-google-speech-api/

  6. Google Chomium Open Project
    http://src.chromium.org/viewvc/chrome/trunk/src/content/browser/speech/
    http://src.chromium.org/viewvc/chrome/trunk/src/content/browser/speech/google_one_shot_remote_engine.cc