普通视图

发现新文章,点击刷新页面。
昨天以前不会开机的男孩

2014年度总结

作者 studentdeng
2015年1月2日 11:01

2014年已经过去了,人生已经到了第27个年头,越来越发现自己不能像20出头的时候那样不加思考的做事情,每件事情每个决定都变得小心翼翼。我的世界已经不仅仅是代码,发现有更多更多的事情需要我去做,需要我去学。

工作

年初我换了一份工作,在加入百度之前,我曾经和部门的高经谈过这个职业迷茫的问题。他讲了他的故事,也告诉我迷茫哪里都有,干我们这一行,必须拥抱变化。这一年来看似什么都没发生,但其实暗流涌动,从一开始的兴奋,到后面因为项目变动而team剧烈变动,并一度考虑离职,再到新的team,再到非常开心,再到归于平静。中间也发生了一些有趣的事情,一次被动面试被拒,一次主动拒绝了一份待遇丰厚的offer。感受到了不变化,也是一种选择。

我非常的幸运,我的工作是我最大的兴趣,而这个也给我带来了别人无法体会的迷茫。工作对我来说不仅仅是钱,我不会因为更多的钱而换工作。我更在乎我是否在做着我喜欢做的事情,我是否有机会施展我所谓的“才华”,是否有足够的时间,让我学习我感兴趣的东西。

听上去似乎有些复杂,但这个问题不仅仅只有我有。很多同事老大都有,而通常的解决办法就是耐心等,做好平时积累,沉下心来学习。另外赶快去找个女朋友吧。

生活

2014年在生活角度来看是个好兆头,我开始学会独立生活,交水、电费。一开始非常开心,但是后来却发现,独处是一件非常难的事情。发现在一个只有自己的空间中,我的确变得懒了。看书少了,颓废多了。早起少了,晚睡多了。仅仅是一个细微的变化————一个人住,就改变了这么多,这让我惊讶,也让我兴奋。我感到了这是一种挑战。别人称为”君子慎独”。感到了更多的压力与责任。是的,自己是否能够约束自己,为自己的行为负责,照顾好自己

年底的时候,自己感冒了。可能对很多人都不是个事儿,但是对于一个高中之后就再也没有感冒吃药的人来说,还是一件蛮重要的事情。我突然发现自己的身体,出了问题。变得弱了很多。走在路上我想到了很多,我感觉害怕,是的,因为我发现我的身体没有往日的灵活与力量,那些马路上快速来往的汽车让我感到恐惧。我想到了老妈老爸,还有家里已经8岁的小白。年纪大的家伙们,需要人的照顾。

学习

学习在我毕业之后就成了我生命中的主线,今年我参加了非常非常多的网课。我非常开心前几年对英语的不断积累,让我可以几乎无字幕的情况下,完成大部分课程。并且有不少课程深深的改变了我对这个世界的认识,打开了另一扇窗户。2014我对历史产生了极大兴趣,对古书中蕴含的道理也非常赞同,他们对人性的把握让我脑洞大开。他们对失败的看法,对成功的定义,面对问题,解决问题的思路让我感到深深的不如,也感到由衷的开心。求知的道路还有很多需要努力。唯一的问题在于看书太少了,附上DNA的统计

女朋友

2014年我有很多的时间和精力放在了如何找到女朋友这个问题上面。在问了不少“有经验的前辈”,并且“打入敌人内部”之后,我得出了一个结论——我的处境非常非常糟糕。很多人在10几岁的时候就已经可以开始追妹子,并且已经开始看各种书,积累各种方法论了。对于一个26岁。还不能看下一本书的我来说,已经落后了整整10年。扪心而我,我发现我做了很多感动自己,恶心别人的事情。妹子们早已被表白到无所谓,拿礼物拿到手软,当然不会在意多一个我,少一个我。世界无外乎有用和无用,在深深的感受到这个世界的恶意之后,我不得不开诚布公的和老妈进行了长谈,表示我注孤的情况,并得到了她的同情。然后开始了相亲之旅。

和绝大多数人一样,非常不顺利,相亲就像是打怪升级,不断的练习,积累经验。虽然没有结果,但是最用心的那个人必然收获最大。再见了10多个之后,真的很不容易不容易遇到一个合适的妹子。可以让我跳入下一个学习区——如何维持一段长期的亲密关系。

追妹子的过程中让我慢慢意识到一个问题,如果你始终都没有办法追到一个妹子,这就说明自己在建立人与人信任关系上面,有问题,至少也是你不愿意向另一个人share你的时间,另一个人也不愿意share她的时间给你。建立信任的过程很难,妹子又是个体差异极大的生物,你无法预知眼前的人是不是最后一个,所能做得只能是尽自己最大努力,珍惜彼此之间的缘分。

最后

这就是我的2014,4个维度。看似平稳,实则充满变化的1年。上海的事情告诉我们,生命有时候很短暂。我希望我的家人,朋友,自己能够平平安安就好。

iOS APP 架构漫谈二

作者 studentdeng
2014年11月5日 16:47

上一篇《iOS APP 架构漫谈》简单介绍了information flow的概念。这篇文章简单介绍另一个在编程中非常重要的思想或工具——状态机(State machine)。对大多数计算机专业的家伙们来说,这应该是一门比较难学的课程,里面包含一大堆揪心的名字比如DFA,NFA,还有一大堆各种各样的数学符号,又是编译原理的基础。不过很遗憾,似乎在做完编译原理课程作业之后,很多人再也没有实现过或是用过状态机了。本文通过一个游戏demo来简单描述一下状态机在实践中的应用。demo code

背景

首先看下我们的使用场景,假如我们需要设计一套联网对战的小游戏。第一个难题可能是如何建立一个通道,让2个手机相互发送消息。这里我并不打算引入server端开发,希望只是通过客户端来实现这个逻辑,这里使用LeanCloud API来简化这个过程。这样我们可以暂时不考虑技术细节,直接站在业务角度去思考如何建立这个游戏。

业务场景–邀请

正式开始游戏之前,总会有一个邀请的环节。假如我们有2个用户,分别是Host,Guest。Host创建游戏,Guest加入游戏。游戏的整个流程和我们平时玩的对战游戏流程并没有多大不同。

1-1

  1. Host创建游戏,他就相当于进入一个等待队列里面。
  2. Guest加入游戏,他从等待队列中找到一个匹配,比如Host。然后对Host发送join message
  3. Host会收到很多join message。由于我们只是选择1vs1。这里假定Host同意Guest加入游戏。Host向Guest发送join confirm message
  4. Guest收到join confirm message, 向Host发送Go消息,表示Guest已经进入游戏
  5. Host收到Go消息。也进入游戏。

具体实现业务逻辑

现在的构想的逻辑只有5步,但其实还会包含很多逻辑,比如超时机制,重发机制。由于中间状态很多,还可能有我们没有想到过的问题。在面对这种复杂逻辑时,会通过状态机来帮助我们理顺逻辑。这时,我们脑中思考的业务其实是一个状态到一个状态的图。 如下

1-1

上半部分是游戏的创建者,下半部分是游戏的加入者。

一开始,尽量简化模型,这里红色剪头表示我们的正确主流路线,黑色表现出错路线。也就是说,一旦错误,就回到原始Idle状态。

开始写代码

在想清楚所有逻辑,并考虑清楚正常路线和错误路线之后,就可以开始写代码了。为了方便,这里直接使用第三方的状态机框架TransitionKit

定义State(HOST)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 TKState *idleState = [TKState stateWithName:@"idle"];
  TKState *waitingJoinState = [TKState stateWithName:@"waitingJoin"];
  TKState *waitingConfirmState = [TKState stateWithName:@"waitingConfirm"];
  TKState *goState = [TKState stateWithName:@"go"];

  [waitingConfirmState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
      [selfWeak sendJoinConfirm];
  }];

  [goState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
      NSLog(@"happy ending");

      [SVProgressHUD showSuccessWithStatus:@"ok"];
  }];

定义Event(HOST)

Event 是建立State到State的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 TKEvent *waitingJoinEvent = [TKEvent eventWithName:CUHostGameManagerWaitingJoinEvent
                           transitioningFromStates:@[idleState]
                                           toState:waitingJoinState];

  TKEvent *receiveInviteEvent = [TKEvent eventWithName:CUHostGameManagerReceiveInviteEvent
                               transitioningFromStates:@[waitingJoinState]
                                               toState:waitingConfirmState];

  TKEvent *receiveConfirmEvent = [TKEvent eventWithName:CUHostGameManagerReceiveConfirmEvent
                                transitioningFromStates:@[waitingConfirmState]
                                                toState:goState];

  TKEvent *disconnectedEvent = [TKEvent eventWithName:CUHostGameManagerDisconnectedEvent
                               transitioningFromStates:nil
                                              toState:idleState];

定义过程(HOST)

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
 - (void)startGame {

      NSAssert(self.session.peerId != nil, @"");

      //这里,如果不是idle,我们切换状态机到idle
      if (![self.stateMachine.currentState.name isEqual:@"idle"]) {
          [self fireEvent:CUHostGameManagerDisconnectedEvent userInfo:nil];
      }

      //这里调用LeanCloud 入队
      AVObject *waitingId = [AVObject objectWithClassName:@"waiting_join_Ids"];
      [waitingId setObject:self.session.peerId forKey:@"peerId"];
      [waitingId saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
          //enqueue 之后,进入waitingJoin状态
          [self fireEvent:CUHostGameManagerWaitingJoinEvent userInfo:nil];
      }];
  }

  - (void)sendJoinConfirm {
      //发送加入确认消息给Guest
      AVMessage *message = [AVMessage messageForPeerWithSession:self.session
                                                   toPeerId:self.peerId
                                                    payload:@"join_confirm"];
      [self.session sendMessage:message transient:YES];
  }

  - (void)session:(AVSession *)session didReceiveMessage:(AVMessage *)message
  {
      if ([message.payload isEqualToString:@"join"]) {
          //收到Join(邀请)之后,发送确认消息
          self.peerId = message.fromPeerId;

          //因为LeanCloud的API比较挫,watch 之后才能发送消息,但是我们不知道什么时候才watch成功。。。。
          //好在只是demo,我们只好用这种方式work around,延迟2s发送消息
          [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendInviteConfirmRequest:) object:nil];
          [self performSelector:@selector(sendInviteConfirmRequest:)
                      withObject:@[message.fromPeerId]
                      afterDelay:2.0f];
      }
      else if ([message.payload isEqualToString:@"go"]) {
          //收到go消息,流程结束
          [self fireEvent:CUHostGameManagerReceiveConfirmEvent userInfo:nil];
      }
  }

  - (void)sendInviteConfirmRequest:(NSArray *)watchPeerIds {
      [self.session watchPeerIds:watchPeerIds];
      [self fireEvent:CUHostGameManagerReceiveInviteEvent userInfo:nil];
  }

定义State(Guest)

1
2
3
4
5
6
7
8
9
10
11
12
13
 TKState *idleState = [TKState stateWithName:@"idle"];
  TKState *waitingReplyState = [TKState stateWithName:@"waitingReply"];
  TKState *goState = [TKState stateWithName:@"go"];

  [waitingReplyState setWillEnterStateBlock:^(TKState *state, TKTransition *transition) {
      [selfWeak searchingGames];
  }];

  [goState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
      [selfWeak sendGo];
      NSLog(@"happy ending");
      [SVProgressHUD showSuccessWithStatus:@"ok"];
  }];

定义Event(Guest)

1
2
3
4
5
6
7
8
9
10
11
 TKEvent *searchingEvent = [TKEvent eventWithName:CUGestGameManagerSearchingEvent
                           transitioningFromStates:@[idleState]
                                           toState:waitingReplyState];

  TKEvent *receiveConfirmEvent = [TKEvent eventWithName:CUGestGameManagerReceiveConfirmEvent
                                transitioningFromStates:@[waitingReplyState]
                                                toState:goState];

  TKEvent *disconnectedEvent = [TKEvent eventWithName:CUGestGameManagerDisconnectedEvent
                              transitioningFromStates:nil
                                              toState:idleState];

定义过程(Guest)

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
- (void)joinGame {

  if (![self.stateMachine.currentState.name isEqual:@"idle"]) {
    [self fireEvent:CUGestGameManagerDisconnectedEvent userInfo:nil];
  }

  [self fireEvent:CUGestGameManagerSearchingEvent userInfo:nil];
}

- (void)searchingGames {
  AVQuery *query = [AVQuery queryWithClassName:@"waiting_join_Ids"];
  [query orderByDescending:@"updatedAt"];
  [query setLimit:1];

  [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    NSMutableArray *installationIds = [[NSMutableArray alloc] init];
    for (AVObject *object in objects) {
      if ([object objectForKey:@"peerId"]) {
        [installationIds addObject:[object objectForKey:@"peerId"]];
      }
    }

    [self.session watchPeerIds:installationIds];

    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendJoinRequest) object:nil];
    [self performSelector:@selector(sendJoinRequest)
               withObject:nil
               afterDelay:2.0f];
  }];
}

- (void)sendJoinRequest {

  for (NSString *item in self.session.watchedPeerIds) {
    AVMessage *message = [AVMessage messageForPeerWithSession:self.session
                                                     toPeerId:item
                                                      payload:@"join"];
    [self.session sendMessage:message transient:YES];
  }
}

- (void)sendGo{
  AVMessage *message = [AVMessage messageForPeerWithSession:self.session
                                                   toPeerId:self.otherPeerId
                                                    payload:@"go"];
  [self.session sendMessage:message transient:YES];
}

最后

state machine 是一个蛮厉害的锤子,只要是一个工具,就肯定会被滥用。。。state machine最大的好处是在于,方便我们思考清楚所有细节,主线,和错误流程。避免因为考虑不周全而产生的bug。结合之前的information flow的思路,会让我们的软件设计更加清楚。

demo code

分布式系统原理--日志技术No Redo Log

作者 studentdeng
2014年10月17日 14:02

上一篇介绍了Redo Log,这篇介绍No Redo Log。

在分布式系统中,某些情况下我们依然需要实现原子操作,有很多方式,其中No Redo(Undo) Log 便是在工程中运用最广泛的思想之一。他的过程非常简单。

下面是一个简单系统的状态。

image

为了实现原子的修改 A,B,C的值。我们把A,B,C 看成一个集合,或是一个“目录”。

1.做一次copy

image

2.对于每个更新的操作,创建一个新的item,然后在新的目录中保存修改后的新值。

image

3.原子性的修改生效目录指针。

image

通过原子性的修改一个值,切换一个状态,完成一系列分布式操作原子性的修改。

分布式系统原理--日志技术Redo Log

作者 studentdeng
2014年10月13日 19:21

问题概述

在分布式系统中,宕机是需要考虑的重要组成部分。日志技术是宕机恢复的重要技术之一。日志技术应用广泛,早些更是广泛应用在数据库设计实现中。本文先介绍基本原理概念,最后通过redis介绍生产环境中的实现方法。

Redo Log

数据库设计中,需要满足ACID,尤其是在支持事务的系统中。当系统遇到未知错误时,可以恢复到一个稳定可靠的状态。有一个很简单的思路,就是记录所有对数据库的写操作日志。那么一旦发生故障,即使丢失掉内存中所有数据,当下一次启动时,通过复现已经记录的数据库写操作日志,依然可以回到故障之前的状态(如果在写操作作日志的时候发生故障,那么这次数据库操作失败)。

操作流程简单如下(假设每次数据变化,都提交):

  1. 更新的操作方式依次记录到磁盘日志文件。
  2. 更新内存中的数据。
  3. 返回更新成功结果。

恢复流程:

  1. 读取日志文件,依次修改内存中的数据。

优点:

  1. 日志文件有序,可以通过append的方式写入磁盘,性能很高。
  2. 简单可靠,应用广泛。可以把内存中的数据,做备份在磁盘中。

缺点:

  1. 使用时间一长,恢复宕机的时间很慢。

解决办法

先具体化下,如果我们内存中保留一个a的值,记录了写操作比如 a = 4; a++; a--; 当这些操作上千万、亿之后,恢复非常慢。甚至可能最后一条就是a=0,按照之前的算法,我们却跑了很长时间。

那么根据这个场景,很容易想到一个解决方案。

操作流程:

  1. 日志文件记录begin check point
  2. 在某个时刻,把内存中的数值,直接snapshot或dump到磁盘上。(比如直接记录a=4)
  3. 日志文件记录end check point

恢复流程:

  1. 扫描日志文件,找到最后的end check point中配对的begin check point
  2. 读入dump文件。
  3. 依次回放记录的日志操作。

优点:

  1. 应用广泛,包括 mysql,oracle。

一些棘手的问题:

  1. 在做snapshot的时候,往往不能停止数据库的服务,那么很可能记录了begin check point之后的日志。那么在重新load begin check point之后的日志时,最后恢复的数据很有可能不对。比如我们记录的是a++这样的日志, 那么重复一条日志,就会让a的值加1。反之如果我们记录是幂等的,比如一直是 a=5 这种操作,那么就对最后结果没有影响。很显然,设计幂等操作系统很麻烦。

  2. 设计一个支持snapshot的内存数据结构,也比较麻烦。

典型的是通过copy-on-write机制。和操作系统中的概念一样。当这个数据结构被修改,就创建一份真正的copy。老数据增加一份dirty flag。如果没有修改就继续使用之前的内存。这样在做snapshot的时候,保证我们的dump数据是begin check point这个时刻的数据。显然这个也比较麻烦。

还有一种支持snapshot的思路是begin check point后,不动老的数据。内存中的数据在新的地方,日志也写在新的地方。最后在end check point做一次merge。这个实现起来简单,但是内存消耗不小。

Redis是如何解决日志问题的

Redis 是一个基于内存的database,不同于memcached,他支持持久化。另外由于redis处理client request 和 response 都是在一个thread里面,也没有抢占式的调度系统,核心业务都是按照event loop顺序执行,而磁盘写日志又开销很大,所以redis实现日志功能做了很多优化。并且提供2种持久化方案。我们需要在不同的场景下,采用不同的方式配置。

snapshotting

某个时刻,redis会把内存中的所有数据snapshot到磁盘文件。更通俗的说法是fork一个child process,把内存中的数据序列化到临时文件,然后在main event loop 中原子的更换文件名。redis,利用了操作系统VM的copy-on-write机制,在不阻塞主线程的情况下,利用子进程和父进程共享的data segment实现snapshot。具体是代码实现在rdb.c, function at rdbSaveBackground

优点:

  1. 简单可靠,如果database 不大,执行的效果非常好。

缺点:

  1. 如果database size 很大,每一次snapshot时间非常长。不得不配置大的间隔,提高了宕机时数据丢失的风险。

为了解决上面的问题,redis增加了AOF。

Append Only File(AOF)

在database术语中,也被叫做WAL。如果开启的AOF的配置,redis会记录所有写操作到日志文件中。那么redis同样会遇到之前我们提到过的问题。

  1. 即便是追加写,磁盘的操作依然比内存慢好几个数量级,频繁的操作容易产生瓶颈。
  2. 如果数据量操作频繁,会产生大量的重复日志数据,导致恢复时间太长。比如记录一条微博的浏览量,会记录大量重复的+1日志。

那么redis是如何解决的呢?

  1. 文件写操作消耗的时间很长,redis会先把记录日志写在内存buffer中,在每一次event loop 结束之后,根据配置判断是否做写操作。每个buffer的大小有限制,这样每次写操作时间不会太长。
  2. 即便是调用write操作,OS并没有立即写入磁盘,redis 同样提供了一些方案决定刷新OS IO buffer的时机(1秒、从不、每次)。
  3. redis 提供一种AOF重写的方式rewriteAppendOnlyFile来处理AOF文件过大情况。

前面我们知道了,这种check point的机制还是比较麻烦的。那么redis是这么设计的。

image

  1. 为了避免加锁,redis 依然创建了一个child process,利用VM的copy-on-write,共享数据。同时保证主线程依然可以处理client请求。
  2. 根据KV的类型,先从内存读取数据,然后再写数据到磁盘,和之前的AOF文件无关。
  3. 那么当子进程rewrite AOF的过程中,main thread依然可以处理新的client request。新增的数据会被放在rewrite buffer中,而且写到原有的AOF文件中。
  4. child process完成后会通知主线程。主线程有一个定时任务,也就是会不断轮询child process是否已经完成(通过信号量)。
  5. 主线程会merge 变化的数据到temp file。
  6. 主线程原子的rename到一个新的AOF文件,之前的AOF就不起作用了。

优点:

  1. 除了merge 和 rename需要阻塞主线程,rewrite不会阻塞主线程。(前提是使用bgrewrite command)。

最后

这些都是性能和稳定性之间做的权衡,根据不同场景需要调整。

参考

缚心猿,锁六耗

作者 studentdeng
2014年10月7日 10:31

转载自Coursera 公开课 professionalism forums thread

【作者徐冰,1994年从中国成都到新加坡。目前修读中医学士学位,兼职电台主持】

无论是谁,有怎样的社会地位,在光鲜亮丽或毫不起眼的外表下,谁没有自己的故事?谁不是一路走来跌跌撞撞留了或深或浅的伤?在独自一人的时候,有多少人可以真正自在地和自己相处?

一次在“医学心理学”课上,老师暂停上课,将课室灯光调暗,让我们做个冥想的练习。

她让我们沉淀思绪,在尽可能放松的状态下,回溯过往。

我们一页页翻过我们各自的故事,重回生命中的重要时刻,走过一级级或快乐,或悲伤,或怨恨,或遗憾的台阶,回到原点。

想象,我们经由时光隧道回访旧时的家。走过熟悉的街道,看到家的门牌,走进去。家居陈设是否和过去一样?家里都有谁?他们的样子如何?他们之间的互动如何?然后,我们看到童年的自己。

这时,容许自己好好看看童年的自己。他是什么样子?他是什么表情?看进他的眼睛,他是否快乐?感受他的各种情绪,盼望和恐惧,愤怒和无助,自责和愧疚。容许自己,陪伴童年的自己片刻,听听他有什么话想说。

把他抱上膝头,轻轻地,拥抱他。

如果他哭,就让他哭。

容许自己,给童年的自己深深的理解和安慰,告诉他,他现在十分安全,没有任何人任何事可以再令他受伤害;告诉他,他只是一个孩子,没有做错什么,在能力许可内,他已经做得够好。跟童年的自己道歉,抱歉忽略和冷落他太久,然后,承诺你再也不会苛责他,嫌弃他,在任何时候,你只会爱他……

我不是第一次接触这样的练习。之前在台湾参加“萨提尔模式”课程时,老师也带着我们做过治疗性的冥想,我已经走过整个过程而且得到了很好的疗愈,因此对我来说,不再有强烈的心理冲击,但我仍感受到明显的情绪涌动。

而许多同学,包括四五十岁的大男人,在那一刻,都不能自已,泣不成声。

多数人,可能从未想到自己会有这样激烈的反应,从未意识到在内心深处,有这样深和痛的创伤。

我们每个人的心里,都有一个内在的小孩。他是长久以来被我们努力压抑的各种情绪,被刻意忽略的伤痛的累积。无论是谁,有怎样的社会地位,在光鲜亮丽或毫不起眼的外表下,谁没有自己的故事?谁不是一路走来跌跌撞撞留了或深或浅的伤?在独自一人的时候,有多少人可以真正自在地和自己相处?

在亲密关系中,又有多少人却感受到难以言说的寂寞?有多少人一直被困在过去走不出来?又有多少人把痛苦转嫁他人造成新的伤害?

那内在的小孩,一直在那里,以各种方式,也许是身体的不适,病痛,或精神上的失调,或人际关系的危机,来呼求我们的关注。我们却从未去理会和照顾他,更不知道那就是我们所有痛苦和不幸的根源。

让我们回到源头,与自己和解。

不要觉得这很荒谬,下次当你一个人的时候,让自己独处在一个不被打扰的环境里,让心静下来,走过岁月的小径,去探望幼年的你。跟他一起玩耍,陪他,注视他,抚摸他,温柔地待他。好像你是他的父母一般,无论他说什么做什么,你都不加评判地接纳。他跟任何一个你爱的人一样,值得你去爱。尤其是,如果那小小的人儿从未得到过足够的爱的话,你,只有你,能带给他补偿,改变他的命运。改变了他的命运,你就改变了自己的命运。

iOS APP 架构漫谈

作者 studentdeng
2014年8月29日 15:48

最近看了一些有关server的东西,一些很简单的东西,不外乎是一些文档规范,另外结合最近看的wwdc的一些video,觉得对软件架构(software architecture)认识又清楚了一些,这里记录下来。

software architecture 听上去是一个很大的概念,实际上也包括很多东西,里面的争议也很多。在我看来软件架构最好放在小的场景中理解。

问题1

我们有2个页面。

  • 页面A:主页面
  • 页面B:详情页面

demo code 1.0.0

2个页面分别显示一个数字,这个数字应该相同。详情会修改这个数字,这里我们发现,详情页面和主页面数字不一样。

数据不一致

问题1 解决方法A

这里首先的感觉就是,详情页面返回,主页面数据没有刷新,导致数据不一致。 那么Fix这个Bug的方法,就是在主页面出现的时候刷新界面

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.displayLabel.text = [[CUDataDAO selectData].data stringValue];
}

现在来看,还不错。但是,我们调用selectData的次数则变得非常非常多。数据不是经常变化的。

demo code 1.0.1

问题1 解决方法B

我们发现既然数据的改变是在页面B进行的,那么页面B修改这个数据的时候,应该把数据变化”通知”给页面A,那么我们写了一个Delegate

@protocol CUDetailViewControllerDelegate <NSObject>

- (void)detailVC:(CUDetailViewController *)vc dataChanged:(NSNumber *)data;

@end

在页面B修改数据之后,通过delegate 通知给页面A。

- (IBAction)changeButtonClicked:(id)sender {
    int value = arc4random() % 100;
    [CUDataDAO setData:value];

    self.displayLabel.text = [@(value) stringValue];

    if ([self.delegate respondsToSelector:@selector(detailVC:dataChanged:)]) {
        [self.delegate detailVC:self dataChanged:@(value)];
    }
}

到此场景1得到了不错的解决。

demo code 1.0.2

问题2

这时我们增加了另一个页面C。这个场景会稍微抽象一点,我们定义了3个数据

  • 页面A的数据dataA
  • 页面B的数据dataB
  • 页面C的数据dataC

问题1中 dataA = dataB。在问题2中dataA = dataB + dataC;

问题2 解决方法C

也就是说页面C的修改,也会影响页面A的数据,那么我们是不是也要写一个XXXXDelegate呢?

这时我们的大脑嗅出了一些不好的味道,如果再来个什么dataD,dataE,我们要写这么多的Delegate么?对于多对一”通知”这种味道,很自然的想到了不用Delegate,而是用NSNotification来做。让我们未雨绸缪一下,定义一个Notificaiton

NSString *const kCUDataChangedNotification = @"CUDataChangedNotification";

[[NSNotificationCenter defaultCenter] postNotificationName:kCUDataChangedNotification
                                                  object:nil
                                                userInfo:nil];

那这个变化broadcast到listener,看上去是一个很赞的idea。

demo code 1.0.3

问题3

过了一段时间,我们发现问题2的方法有一个Bug,当界面停在页面B的时候,切换到页面C,修改数据,B中再返回时,数据和页面A的数据不一致。

数据不一致

那也可以类比解决方法B,得到了下面的方法

解决方法D

既然A和B的数据不一致,而A的数据比B的新,那么保留一个B的指针,然后A变化的时候,更新B就好了。

- (void)handleDataChangedNotification {
    [self updateLabel];
    [self.vc updateLabel];
}

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"push"]) {
        CUDetailViewController *vc = [segue destinationViewController];
        if ([vc isKindOfClass:[CUDetailViewController class]]) {
            self.vc = vc;
        }
    }
}

demo code 1.0.4

问题4

页面C实在是太简单了,这次我们希望在页面C中显示页面A的数据。因为上次我们就产生了一个数据不一致的问题,这次我们注意到了,那么怎么修改呢?

解决方法E

在看了看整个APP各种通知之后,觉得挺麻烦,准备用一个取巧的方法。可以类比解决方法A。在页面C出现的时候,刷新数据,至于什么性能问题,不管了,先fix bug。

- (void)viewWillAppear:(BOOL)animated {
    [self updateLabel];
}

- (void)updateLabel {
    int dataB = [[CUDataDAO selectData].data intValue];
    int dataC = [[CUDataDAO selectOtherData].data intValue];

    self.dataLabel.text = [@(dataB + dataC) stringValue];
}

demo code 1.0.5

问题5

这时的数据需要不断的变化,我们在CUDataDAO加了一个timer 模拟数据变化,数据变化的原因可能是server push 一些数据。client 本地数据库更新了数据,需要在页面A、B、C中显示。

页面C的数据又不一致了。。。。

问题到底在哪里呢

走到这里,我们需要重新思考为什么这个问题会不断的重复出现呢?software architecture就是来解决这个问题的。但是在提出一个合理的方案之前,先思考一个概念。

我们把数据库中的数据,显示到屏幕上,或是传递给View时,这个过程其实是对data 做了一次copy。而且只要不是通过引用或是指针这些方式,通过值传递的方式都是对data做了一次copy。而这个copy的过程,非常类似Cache

通常建立一个Cache会遇到2种问题。

  • Cache情况A: 与original Data 数据不一致,没有及时更新
  • Cache情况B: 重复建立Cache

让我们用这个思路来看我们的解决方案

解决方法A

这是一个非常典型的Cache情况B。数据库的数据并没有变化,但我们却多次重复计算cache

解决方法B

页面之间的关系可以用下面来描述

这里我们隐隐能够感觉到问题,A的数据变化依赖于2个地方。不急,再往后看

解决方法C

解决方法D

事情变得更糟了

解决方法E

和解决方法A类似,同样的重复计算Cache问题。

实际上问题还会更糟

现在还是一个简单的Model,如果project变得很大,那么就会变成这个样子

每一个X都可能是一个Bug。

我们似乎已经找到问题了

《Advanced iOS Application Architecture and Patterns》 中,把这个图叫做information flow。我们的直觉会告诉我们,这个信息的传递,应该是自上而下的树或是森林,而且最好是一个层次平衡结构,要清晰,每一个位置都有相对于的职责。那我们就需要制定一个规则。

在想这个规则之前,如果把上面的图背后的数据忘记,我们感觉这很类似内存模型。当然内存模型会比较复杂。但是我们可以借鉴很多”内存管理中的规则”,比如谁创建,谁销毁。同样,在我们的information flow中,我们希望谁创建Cache,谁更新Cache变化

DAO的数据库似乎很难做这件事情,我们引入了一个新的元素dataSource(当然他本身又是DAO的一个Cache)。其中A、B、C3个都会显示数据,那么他们应该在一个层级,其中B、C会修改数据,他们会把这个数据返回给dataSource,而通过dataSource来把这个变化通知到A、B、C。

这样带来的好处很明显,我们再添加一个D,也不会对其他地方的数据产生任何影响,我们的Unit Test、Mock也更加好写。

我们之前的思路错在哪里呢?

从局部来看,我们之前的思路都没有任何问题,但是整体来看却把问题隐藏化。关键的问题是在于没有找到Truth,找到问题真正的地方。而找到真正的地方,需要我们在大脑中有一个清晰的information flow或是data flow。了解之间元素的相互关系,才能建立一个个的层。才能坐到真正的解耦,解耦并不是仅仅一个个的Manager,更重要的是建立一套清晰的flow机制,或是消息机制,如果没有一套flow,中间引入的各种各样的方法,即便使用了各种设计模式,整个software 依然是深度耦合

疑问

这个APP看上去交互非常复杂

上面的model,有些同学还可能觉得这是交互上面的问题,这个交互看上去非常的复杂,不是一个好设计。

我这里列举一个实际的例子:

A页面要创建动画,动画背后包括很多数据,这些数据会在B,C甚至更多的页面,或是后台被修改。动画本身实际上体现在View,而这些view可能不仅仅在A中有,B,C可能也会有部分的View。

单例怎么样

当然我们可以用单例的法子。单例是个魔鬼,被很多滥用,这个场景用单例,其实仅仅是把全局变量合理的封装在了单例下,因为这份数据,并没有任何理由要一定是一份copy。

recap

在了解这个概念后,再看一些server的架构,规则时,也会更容易理解这些层之间的关系。包括

  • 为什么要规定那些层之间,不能相互调用,不能有静态方法。
  • 一个层之间的model,不能有重叠功能,不能连表查询。
  • 在哪个层才能调用另一个服务,而调用这个服务还必须要通过统一的接口

software architecture 涵盖的东西非常多。这篇只是一个引子,介绍了设计之前的准备工作。但是在实际过程中,我们的模型可能要比我这里写的还要复杂很多。下一篇会介绍一种策略用来处理更加复杂模型的情况。

最后附上一个完整功能的 demo code

参考

《Advanced iOS Application Architecture and Patterns》

机器学习(二) 线性回归、梯度下降实现

作者 studentdeng
2014年8月24日 20:52

了解一个算法最好的方法就是实现它,不过在开始实现算法之前,有一些额外的概念需要理解。

Vectorization

这是上一篇提到的hypothesis的计算公式。

当计算这个表达式值的时候,往往第一个感觉是写一个for loop 然后累加求和

prediction = 0;
for (int i = 0; i < n; ++i) {
    prediction += theta[j] * x[j];
}

但是在machine learning中更倾向于使用矩阵的方式。 比如同样的公式,会看成矩阵相乘。

其中theta和X分别是

这里通过矩阵或是向量来代替之前的loop。

这是上一篇提到的算法

image

计算function J如果用octave来实现则是这个样子

function J = computeCost(X, y, theta)
%COMPUTECOST Compute cost for linear regression
%   J = COMPUTECOST(X, y, theta) computes the cost of using theta as the
%   parameter for linear regression to fit the data points in X and y

% Initialize some useful values
m = length(y); % number of training examples

% You need to return the following variables correctly 
J = 0;

% ====================== YOUR CODE HERE ======================
% Instructions: Compute the cost of a particular choice of theta
%               You should set J to the cost.


t = (X * theta) - y;
J = (sum(t .* t)) / (2 * m);

% =========================================================================

end

image

而求偏导数迭代更新theta的代码则是这个样子

function [theta, J_history] = gradientDescent(X, y, theta, alpha, num_iters)
%GRADIENTDESCENT Performs gradient descent to learn theta
%   theta = GRADIENTDESENT(X, y, theta, alpha, num_iters) updates theta by 
%   taking num_iters gradient steps with learning rate alpha

% Initialize some useful values
m = length(y); % number of training examples
J_history = zeros(num_iters, 1);

for iter = 1:num_iters

    % ====================== YOUR CODE HERE ======================
    % Instructions: Perform a single gradient step on the parameter vector
    %               theta. 
    %
    % Hint: While debugging, it can be useful to print out the values
    %       of the cost function (computeCost) and gradient here.
    %


    s = sum(bsxfun(@times, X * theta - y, X));
    theta = theta - (alpha / m) * s';

    % ============================================================

    % Save the cost J in every iteration    
    J_history(iter) = computeCost(X, y, theta);

end

上面的2部分代码如果做一些合并分别可以简化成1行代码。说到这里自己还是相当羞愧的。今天早上花了3个小时才搞定这2行代码…主要时间花在了 2个地方。

  1. 算好theta去predict的上面,和normal equations的方式计算的答案总是对不上,不得不怀疑人生了。。。后面才发现是因为函数没有完全收敛,在调整learning rate之后误差明显变小了。
  2. 让大脑适应矩阵还是有点难,很多东西看上去很简单,反应很长时间,不过后面会好一些。

为什么用矩阵

在费了老半天力气搞定Vectorization的转变之后,不得不想想为什么要用这个方式做。obviously有2个好处,Andrew课上也提到了好多次。

  1. 增加一个feature很简单,只要把输入增加一列就好,而算法不需要改动。
  2. 矩阵的运算更容易优化,性能比循环更快。实际我们往往处理上百万个Example和N多的features

第一个很好理解,而且把循环的一大堆代码写成一行,显得逼格很高。 第二个会比较麻烦,涉及到了并行计算优化。

其他

在之前的算法中,我们看到了每一次调整theta都需要iterate整个所有的example,但实际中往往需要处理上百万个examples,而这样的iteration显然是不能接受的。实际上会随机选取一部分examples然后去迭代theta,最后得到一个较为可靠的theta向量。

最后附上Andrew作业的图片,虽然Andrew 不希望把答案放在网上或是论坛什么的,不过我觉得都过去2年多了,应该没关系了。

最后的预测效果图

cost function & theta

cost function & theta 等高线

learning rate

《中国古代历史与人物——秦始皇》笔记

作者 studentdeng
2014年8月16日 10:17

最近在coursera学完了台湾国立大学的公开课——《中国古代历史与人物——秦始皇》。虽然自己看过不少课程,看过不少历史书,但是这门课程对我的影响超过了任何一本文史书。我自己完整的看过3遍,并做了作业(可惜没有认真做好)。这门课从某种角度来看,彻底改变了我对文史课程的认知,对自己之前的无知和误解感到深深的愧疚,并再次讥讽一下国内的文史课程。下面是自己整理的东东,方便自己日后review。

  • 读历史需要思辨
  • 每一个决定的结果是否成功是通过时间衡量的,做决定的关键是在于是否得到了自己想要的结果。得到就要付出,除非有其他人替自己付出
  • 社会就是需要和有用,说服别人是因为知道别人需要什么
  • 人和人的差别不在于遇到的问题,而是面对问题时的态度
  • 看书要学会“沙金”,从沙子里面掏出金子,而不是一味的埋怨,批判。任何事情都有值得自己学习的东西。放下自己的成见,很多事情没有自己想象的那么简单。

2-5

秦国百战百胜,不在于它真能变法,而在于列国不能真正变法 历史说明了一个道理,不能适应时代的,只有被淘汰消失

2-6

历史上的所谓的成功和失败,就看你站在什么位置,用什么尺度来衡量。而判断成功和失败的标准,最后还是问自己

3-4

想要成功,就要懂得把理智放在感情之上。

谋事在人这是真得有用,有人用很多的时间去提高效率,但是节约的时间,并没有被真正利用起来。那么这个办法也很一般。 自己需要自己不断的努力,才能让之前的办法真正变成好办法。。。

方法和人关系太大了

这个社会不外乎需要和有用。

你需要知道别人需要什么,你才能变得有用

5-3

富家子弟处逆境难 穷家子弟处顺境难

6-1

人生总会有赌博的时候,对错往往是命运差别巨大。 百折不挠的民族

人生:运气 ,自我要求。好好准备,让自己成为那样的条件。等待机会。

理想是,你知道你下一步应该做什么 妄想是,你只有目标却不知道从何做起

自强的第一步,不自欺

6-8

在中国文化中,最高的道德和最高的智慧必然是合一的

7-3

学会认错,改过。

8-1

改变环境,需要改变自己,让自己适应那个自己想要的环境的要求。 如何改变,通过学习

9-1

做事之前,为虑胜,先虑败,方能考虑周全

9-9

历史启发智慧,所有的学问都能带来智慧

审时度势:

时:你所处的环境。 势:环境变动的方向

变的是时间和环境。 不变的是人性和良知

不仅仅是自己改变,需要改变周围的人。

历史用真,去伪。。。 改动人心

机器学习(一) 简单的背景介绍、线性回归、梯度下降

作者 studentdeng
2014年7月28日 16:48

Introduction

机器学习很久之前就已经热得不行了,直到最近这几个星期,自己才打算了解一些这方面的东西。原因大概有这么3点。

  1. 自从Andrew Ng 加入我厂之后(虽然和我毛关系也没有),总觉得还是需要围观一下这个令他兴奋的领域。
  2. 在听了IDL的有关手环算法分享后(其实毛也没有听懂), 在知道了一大堆的名词如最小二乘、梯度下降、SVM。以及里面很多的线性代数,微积分的概念,让我觉得这是一个很好的回收自己大学时期的沉默成本(微积分、现代是我在学校里面不多的用心学过的课程)的好机会。总之就是对这些很感兴趣。
  3. 前一段时间受组里高工分享睡眠算法影响,对这种阅读paper,然后优化算法的过程感到很开心。

有了这3条,足够我忙活好几个月了 : )

Background

在机器学习中,有2个很大的思路监督学习(supervised learning)非监督学习(unsupervised learning)

监督学习,用通俗的话来说就是你知道问题的答案,需要计算机给出一个更标准的答案

非监督学习,用通俗的话来说就是物以类聚,人以群分。我们拿到了很多数据,但是不知道问题的答案,希望计算机给我们提供思路。

在生产环境中,往往采用混合模式。比如图片搜索,如何能够查找网页中判断那个图片是老虎,那个是狗。就有2个思路。

  1. 根据图片周围的文字。
  2. 图片的图像数据分析。

2个角度相互校验,稳定之后,就可以产生足够的标注信息了。

线性回归(Linear regression)

线性回归主要用于手环的里程部分的计算,涉及到更细节的是 最小二乘,梯度下降。这里从先从最简单的一元线性回归开始。

一元线性回归(Linear regression with one variable)

Regression Problem : Predict real-valued output

1-1 算法运行的过程

最关键的在于如何描述hypothesis。

1-2 一元线性回归中的hypothesis函数

那么应该如何选取参数呢?直觉告诉我们这个直线需要尽可能的拟合我们的数据集。

1-3 线性回归的目标函数

通过下面的cost function 来评估参数的好坏。算法的目标也很清晰,让函数越小越好。

1-4 cost function

那个这个cost function 到底是个什么样子呢?

1-5 图形化的cost function

当然这个图还是看起来比较麻烦,Andrew 用了更为简单绘制的图来表示(有点类似等高线)。 相同的圆圈上,有着相同的cost function value。这里可以看到和上面的图一样,有一个极值。

1-6 一个比较差的选择

1-7 一个很接近极值的选择

梯度下降 (Gradient descent)

梯度下降,不仅仅是用于线性回归,也可以用在其他机器学习的场景下。

1-8 梯度下降的思路(2个参数的情况)

1-9 梯度下降函数图形(2个参数的情况)

我们的目标是寻找这个图形中的最小值,也就是靠近蓝色的地方。直觉告诉我们,我们先随机一个点,然后沿着最大的坡度向下走最后就可以走到一个极值里。

1-10 一条算法路径,全局最优

这个算法也有问题,随着第一个点的位置不同,我们可能找到一个局部最优的解,而不是全局最优。

1-11 另一条算法路径,局部最优

好在在很多实际问题中,我们遇到的情况要好很多,往往只有一个极值

那么梯度下降的算法就可以简单的描述出来,分别计算2个维度的偏导数,直到函数收敛

1-12

通过分别计算偏导数,a 为learning rate,决定每一步的步长,太小函数收敛很慢,太大则可能无法找到极值,甚至函数无法收敛。

这里Andrew 着重指出了一个叫做同步更新的概念

1-13

如果不同步更新,最后也可以得到极致,但是Andrew 更推荐计算完成所有的参数之后,再一起同步更新。

梯度下降和一元线性回归

将图1-4分别偏导后

1-14 算法公式

其他

  1. 根据上面的算法,如果我们的cost function 在一些地方不可导,那算法不就没法继续了?
  2. 有其他的方法,可以不去循环计算而是直接根据工具计算

梯度下降和一般化的线性回归

很多时候我们不仅仅满足2个参数,决定事情的因素很多,我们需要更一般化的公式。

1-15

算法

1-16

分别求偏导后

1-17

梯度下降生产环境中的一些技巧

Feature Scaling

思路: 希望所有的feature在相同或是类似的范围之内,这样梯度下降会更快收敛。

下图是feature的范围不在一起的运算过程,可以看出来不是圆形,2个维度调整的步长不一样,导致很多反复

1-18 红色箭头表示算法的一次迭代

下图则是调整过的feature,好了很多

1-19 红色箭头表示算法的一次迭代

更一般的,Andrew 推荐每一个feature放在[-1, 1]区间范围内

Learning Rate

说到Learning Rate 就不能不提收敛(convergence)。一般应该定义多大的阀值来判断是否收敛呢?

1-20 Andrew 并不推荐使用一个阀值来判断是否收敛

Andrew 更推荐用图表的形式,因为这个不仅仅可以看到是否马上收敛,而且还能看到算法是否运行正常,是不是一些参数的问题,导致算法无法收敛。

1-21

下图是2个出了问题的J函数,通常来说是Learning Rate 过大。

1-22 一些过大的Learning Rate 导致的图形

最后Andrew 还提供了一些practice的Learning Rate 选取方法,比如一些0.001, 0.003, 0.01, 0.03, 0.1, …

参考

Coursera 《Machine Learning》 Stanford Andrew Ng

Core Animation基本概念和Additive Animation

作者 studentdeng
2014年6月24日 19:21

上一篇《AutoLayout 相关概念介绍和动画demo》提到了一些Core Aniamtion的基础知识,这篇依然介绍一些基本概念,最后提到一点iOS8的动画改动。

一些基本概念

说到Core Animation 不能不说Layer, 一个个Layer通过tree的结构组织起来,在Display的过程中实际上有3种Layer tree。

  • model layer tree
  • presentation tree
  • render tree

model Layer tree 中的Layer是我们通常意义说的Layer。当我们修改layer中的属性时,就会立刻修改model layer tree。

layer.position = CGPointMake(0,0); //这里的修改会直接影响model layer tree

presentation tree 是Layer在屏幕中的真实位置。比如我们创建一个动画

1
2
3
4
5
6
7
8
9
   [UIView animateWithDuration:5.0f
                   animations:^{
                     self.animationLabel.center = CGPointMake(200, 400);
                   }];

  //这里用一个Timer print presentLayer的位置。
  CALayer *layer = self.animationLabel.layer.presentationLayer;

  NSLog(@"model:%@, presentLayer%@", NSStringFromCGPoint(self.animationLabel.layer.position), NSStringFromCGPoint(layer.position));

下面是屏幕输出结果

model:{73.5, 155.5}, presentLayer{73.5, 155.5}
model:{200, 400}, presentLayer{73.559769, 155.61552}//开始动画
model:{200, 400}, presentLayer{73.814095, 156.10709}
model:{200, 400}, presentLayer{74.267357, 156.98315}
...
...
...
model:{200, 400}, presentLayer{199.99576, 399.99182}
model:{200, 400}, presentLayer{200, 400}

Note: render tree 在apple的render server进程中,是真正处理动画的地方。而且线程的优先级也比我们主线程优先级高。所以有时候即使我们的App主线程busy,依然不会影响到手机屏幕的绘制工作。

CADisplayLink

了解cocos2dx对CADisplayLink一点也不陌生,对APP开发者可能就有一点远,但是facebook的Pop一下子拉近了我们和CADisplayLink的距离。通过设置callback函数,当屏幕刷新的时候,就可以执行我们的代码。当然,我们也可以利用NSTimer 或是GCD来实现类似的功能。但是CADisplayLink是最优的,因为不管是哪种类型的Timer,即使我们的刷新间隔和屏幕刷新保持一致。我们都无法知道系统什么时候刷新屏幕。

1-1 NSTimer中每一帧其实只有8ms的时间,如果大于8ms,那么就会丢帧

facebook的Pop非常类似UIDynamic,但是我们需要注意一点,相对于传统的model动画来说,CADisplayLink导致部分绘制工作放在了我们APP的地址空间中,也就是说,增大了APP内存,CPU的开销。也更容易遇到性能瓶颈。

Note: model layer的这部分绘制是完全在render server,而render server运行在比APP更高优先级的进程中,而这个也意味着会有进程间通讯的开销。传递的数据包括整个render tree还有动画,所以,Apple 并不推荐我们手动commit transaction, Core Animation 默认会在run loop 中提交transaction。

UIView animation

Apple 最近在推荐一些Modern APP的设计,其中有一条是希望responsive。比如下面的场景,启动一个动画之后,在动画还没有完成之前取消这个动画。

下图的相关代码

这里我们看到了3种情况。

  • 红色的2个动画之间有一个很大的跳动。
  • 绿色的比红色的好一点,没有跳动,但是就像撞到了墙一样,完全丧失了一开始动画的速度。
  • 蓝色的的运动更加平滑,有更真实的物理效果。

UIKit创建的动画,系统是如何理解的

UIKit的动画最后都会通过Core Animation 来实现, 那么当我们修改layer(model layer)的数值时,系统是如何理解并创建动画呢? 比如这里有一个线性的动画,将animationView的坐标从(0,0)移动到(0,500)

1
2
3
4
5
6
7
8
9
   animationView.center = CGPointMake(0, 0);
    [UIView animateWithDuration:1.0f
                        delay:0
                      options:UIViewAnimationOptionCurveLinear
                   animations:^{
                     animationView.center = CGPointMake(0, 500);
                   } completion:^(BOOL finished) {

                   }];

下面是当我们创建一个UIKit的动画时发生的事情

  • Model:在animationView.center = CGPointMake(0, 500);之后会立刻修改animationView的model Layer中的position的值为(0, 500)。
  • Animation:系统的理解就是从原来的model layer的值(0,0)到新的model layer的值(0, 500)创建一个动画。
  • Presentation: Presentation就像上面提到的,是表示animationView当前在屏幕的真实位置(渲染位置),因为还没有”动”起来,所以还是(0,0)

Note: Animation的部分如果没有明白,可以结合后面的回头再看

当我们看到屏幕上面的View移动的时候,发生了下面的事情

这是在0.4s时刻之前的状态。Model Layer的数值没有变化,而Presentation则在变化,和真正的屏幕动画保持一致。

在一个animation并没有完成的情况下,再创建一个动画系统是如何理解的呢?

如果我们在0.5时刻创建一个reverse动画,animationView.center = CGPointMake(0, 0);

1
2
3
4
5
6
7
8
   [UIView animateWithDuration:1.0f
                        delay:0
                      options:UIViewAnimationOptionCurveLinear
                   animations:^{
                     animationView.center = CGPointMake(0, 0);
                   } completion:^(BOOL finished) {

                   }];

  • Model:的数值会被立刻修改成目标数值(0, 0)
  • Animation: 系统的理解是从原来的(0, 500),创建一个去(0,0)的动画
  • Presentation: 基于系统的理解,Presentation layer的数值变成了(0, 500)。1秒中的时间内递减到(0, 0)

到目前为止,我们可以清楚的理解为什么红色的view会有一个大的跳跃,在我们这里的理解就是presentation layer的一个不连续的修改。

绿色的动画效果原因

在上面的基础之前,绿色的就可以简单说一些

  • Model 这里还是和之前一样,表示目标值
  • Animation:系统的理解是从当前的动画位置开始,也就是 (0, 150)开始创建一个1秒的动画到(0,0)
  • Presentation 和我们的预期一样。

linear animation 图中的颜色和本文的颜色无关,只是表示2个动画的stage EseInOut animation 图中的颜色和本文的颜色无关,只是表示2个动画的stage

可以看出来2个动画相接的曲线不平滑,而造成这个不平滑的原因在于把之前的动画覆盖了, 丢掉了之前动画的速度,如果要实现一个更一般化的解决方案,我们很自然的想到了动画合成。

蓝色的动画原因

蓝色的动画比较复杂,使用了Core Animation中的additive属性,动画被设置成相对的,那么就和动画具体的位置无关。最后还合成了2个动画。

首先,解释一下什么是相对的动画。

这里很容易看到,view的真实位置是Animation 的值 + Model的值。系统的理解就是相对目标值(0, 500)来说,创建一个从-500 到 0 的动画。

其次,相比之前的动画,在0.6时刻(为了方便计算,把之前的0.5时刻移动到了0.6时刻)并没有删除掉之前的动画,而是添加了一个新的动画Animation2。也就是一个相对目标值(0,0)来说,创建一个从500到0的动画。整个运动变成了2个动画的合成。

Note: Animation2的duration修改了,在demo code里面并没有修改 :)

这里,我们就得到了一个一般化的解决方案。

图中的颜色和本文的颜色无关,只是表示2个动画的stage

iOS8的改动

Core Animation 有一个additive的属性实际上已经存在很久了,但是却很少被大家知道(我自己也是)。在iOS8 之前,UIKit创建的动画默认是不使用additive的,而在iOS8之后,默认是Additive的。有兴趣的同学可以试一试download demo code用Xcode6(这会还是beta)并打开macro#define USING_UIKIT 1看一下新的UIKit animation效果。

在了解背后的机制之后,其中的变化也很容易理解。

  1. completion block 的调用变了。之前在创建一个UIKit的动画时候,会覆盖掉上一个动画,也就是删除再添加一个新动画,而现在前一个动画会在真正执行完毕才会执行completion block。
  2. 不是所有的动画都支持additive

……

参考

AutoLayout 相关概念介绍和动画demo

作者 studentdeng
2014年6月13日 18:28

前言

最近Apple的动作还是蛮多的,其中有3条很有意思。

  • iOS8中设备旋转,布局的变化
  • 可能的iPhone6屏幕的变化,iPhone和iPad Mac开发越来越趋于统一
  • Xcode6中Interface Builder的变化(IB中显示自定义View)

cocoa touch 开发中适配各种屏幕尺寸已经是能够预测的了,那么跟进AutoLayout 也就是必备技能了。

传统的布局是如何做的

一开始接触iOS的时候,我还是蛮喜欢他的布局系统。简单来说,一个图像,我们通过中心点坐标,旋转角度和轮廓大小来定义他在窗口中的位置

这里的坐标和笛卡尔坐标系不同的是Y的方向

1-1 The default layer geometries for iOS

这里表示了anchorPoint含义,用于表示position相对bounds的位置,比如(0.5, 0.5)表示中心,(0,0)表示左上角

1-2 The default unit coordinate systems for iOS

下面表示了frame bounds position anchorPoint之间的关系,你可能觉得这个anchorPoint似乎没有什么用

1-3, 1-4 How the anchor point affects the layer’s position property

但是当我们旋转一个View的时候,好处就来了

1-5 , 1-6 How the anchor point affects the layer’s position property

传统布局的问题

传统布局是非常高效的,组合各种变化,可以轻易得实现任意的2D动画,当然也可以轻易的解决静态的布局问题。但是在面对多个屏幕,屏幕旋转时,或是需要在2个View 中间动态增加一个View的时候显得非常繁琐。需要不断的写一些计算距离,位置的代码(甚至还有一些magic number)。网上有很多例子,比如beginning-auto-layout-part-1-of-2,或是大家在平时工作中遇到的3.5inch和4inch屏幕之间的适配。

AutoLayout

AutoLayout使用非常简单,Xcode的支持也非常直观。但是因为和之前的方式有很大的不同,新手一开始很容易遇到一大堆的异常,crash在main函数里面,让人非常沮丧。但是在了解AutoLayout之后,就会发现这是一个非常非常elegant的布局解决方案,也很容易理解为什么crash,以及应该如何debug。

constraints 约束

AutoLayout 是一个描述各种约束的行为,比如,一个View 距离父View上边距多少,相邻之间的间隔,各个View之间的宽高关系等等。这一系列的条件就是为了最终确定之前提到的传统布局中需要的东西,这个View的大小,位置。所以,当我们设置的条件不足,或是条件冲突时,就会产生异常。

Intrinsic Content Size 固有大小

在使用AutoLayout的时候,UILabel 我们只需要设定他的position,不需要设置宽高,而一个自定义的UIView,我们不仅仅需要位置,还需要设定宽高,这是为什么呢?

每一个View 都有一个特别的属性叫做Intrinsic Content Size,这个可以理解成是一个View的最合适而且最小的宽度和高度。对于UILabe来说,就是至少得把我设定的文字都显示完整吧,所以系统只需要知道UILabel的位置。而UIView的Intrinsic Content是(0,0)所以需要设置UIView的宽高(或是设定周围的边距等等其他关系可以让系统知道这个View应该多宽,多高)。而Intrinsic Content Size,也是未来自定义View显示到Xcode中必须设置的属性之一。

Phases of Display

使用AutoLayout之后,把view显示到屏幕上面大体分成3步。

  • Update constraints
  • Layout views
  • Display

一般来说layoutSubviews负责布局,比如调整View之间的距离,大小,drawRect负责绘制,比如使用什么颜色。而AutoLayout则是在layout之前增加了一个设定约束的过程,也就是上面提到了update constraints

1-7

在view的layoutSubView中,如果我们调用了[super layoutSubView] 系统就把设定的这些约束计算成每个view的bounds,center属性。当然我们也可以基于AutoLayout的结果,再做布局的调整。

1-8

Display 不是这篇文章的重点,这里略过

Alignment Rect

仔细阅读文档的同学会发现在Apple AutoLayout document中可以看到Alignment Rect 这个家伙。 AutoLayout中的Left,Right等约束,并不是针对View的frame。而是根据Alignment Rect。在绝大多数情况下Alignment = Frame。但是如果对某些需要交互的元素,而图片素材很小的时候,就可以利用Alignment把交互区域变大。可以参考UIImage 中的 imageWithAlignmentRectInsets

1-9

Animation

AutoLayout也可以配合传统的animation方法,整体代码结构如下。

1
2
3
4
5
6
7
8
  [self.view layoutIfNeeded];
  [UIView animateWithDuration:0.3f
                   animations:^{

                     //... update constraints  

                     [self.view layoutIfNeeded];
                   }];

使用AutoLayout也可以轻易的实现之前的设置frame很难实现的动画效果。比如下面的例子(很奇怪,优酷吃掉了后面几秒的动画…)

使用之前传统的动画,实现这个过程,需要计算所有subView之间的距离,位置。而且在修改一个view的frame时,很难做到和其他View的移动速度同步。除非是custom layoutsubview。做起来相当麻烦。但是用AutoLayout则非常简洁直观,只需要设置第一个View的position,然后其他view约定好高度和间隔依次排列就好了。

demo code

当然AutoLayout做动画的时候有的地方也很麻烦,比如希望旋转view A 的时候,或是使用transform时,很容易产生奇怪的结果。一般来说会设置一个host View通过AutoLayout设定位置,然后在旋转view A。一句话就是混合起来,各取优点。

其他

  • Compression Resistance
  • Content Hugging
  • 优先级

简单的来说Compression Resistance 设置view有多大意愿(优先级),愿意压缩里面的内容。Content Hugging设置view 有多大愿意(优先级),愿意显示里面内容之外的部分。

stackoverflow上面有一个很清晰的通过UIButton解释的[例子],可以很容易理解这2个属性。

参考

你和别人是相同的

作者 studentdeng
2014年6月1日 09:22

最近又洗刷了我的一个人生信条。当然其实很早很早,大概2年前就已经动摇了,只是最近彻底想明白了,很开心,记录下来。

很多人认为自己的独一无二的,包括我自己。这个独一无二的理解主要部分是认识到了人与人之间的不同。这种心理在面对选择的时候可以更好的follow your heart,虽然自己的认识很浅薄,但是却受到了上天的眷恋,好运的几个选择都看上去不错。但是上天是公平的,总会有做出错误选择的时候。这时候不得不思考是不是自己从一开始就错误了。

现在看来当然是错的,因为每个人本来天生就是不一样的,这不是明摆着么,不同的长相,不同的家庭,往基础了说,DNA 每个人都完全完全不一样。每个人都会有自己的特殊情况。但是往长远了看,人们的社会地位是一样的。

所以,从某种角度来看,你和别人是相同的

在遇到困难的时候,人们往往觉得自己是最痛苦,最迷茫的那个人(相同的倒霉事情人们往往觉得自己最痛苦,而发生在别人身上的时候却不这么认为,所以得出这个结论并不难),这个时候其实最治愈的话不是一些什么解决方案,而是“你不是一个人”。当真正的意识到这些事情别人也会遇到,别人也会有相同的感受时,才能真正的面对眼前的问题,并最后找到解决办法。相反,当好运的时候,相信这个世界上还有和你一样交好运的人,其实这并不是一件什么值得炫耀的 :)

你和别人是相同的最重要的好处在于,你可以更容易的听取别人的意见,更容易去做改变。

李笑来老师有一段我很喜欢的对井底之蛙的解释。

井底之蛙是不可能知道这世界还有很多其他的井,与它所在的那个井没什么不同。可能是大井,可能是小井;井口可能是方的,可能是圆的,但有一点是相同的,并且至关重要的,这些井的深度是青蛙跳不出去的深度。那只井底之蛙于是更不可能知道其他的井里可能有着与自己一模一样的青蛙,也可能是大一点的,也可能是小一点的;它(们)也不可能知道哪只(哪些)青蛙与自己有着一模一样的想法——尽管它可能从细微角度出发,都是独一无二的,并非100%相同的青蛙。

一开始知道自己是一只青蛙的时候,还是挺沮丧的。因为在面对现在信息膨胀的社会,我就不拿那些太成功的例子来说好了,就拿简单的考试来说。很多人都说考试是一件痛苦的事情,要大量的记背,枯燥又乏味。我们因为做不了这么苦的事情,所以考试没有考好。这是一个非常常见的说辞。类似还有很多,比如人家2个人感情好是因为经历了很多人没有经历的事情,我反正平平淡淡的,就没机会拥有好的感情等等。这些说法本质上就是承认了你和别人是不同的,并以此为理由而拒绝改变

大家不能接受平平淡淡,似乎所有的成就必须经历一段常人不能忍受的苦难。所以很多人越来越愿意追求刺激,迫切的寻找翻身的机会。但是不能不面对的就是,绝大多数情况下生活都是平平淡淡的,而这些平平淡淡生活时候的态度则决定了一个人是否能够挺过不好的事情,是否有机会抓到好运。

最近自己最大的收获,就是感恩,发现自己的人生路虽然走过了大量的弯路,自己的人生信条有超级多的不足,居然还是“成功”的活到现在,还真是受到了上天的保佑,真的应该好好珍惜这份来之不易的运气。当然受到上天眷顾的人也不仅仅是我一个人。其实每个人都受到过上天的眷顾,只是看发现了没有。

承认你和别人是相同的 需要极大的勇气~

facebook pop & tweaks demo

作者 studentdeng
2014年5月9日 19:39

示例代码

最近facebook开源了2个很有价值的project popTweaks。 facebook提供了一个非常赞的topic-Building Paper

这篇文章来简单介绍一下pop的使用,最后使用Tweaks来微小调整动画参数来达到我们最希望的效果。

这是我们最后的效果:

pop is powerful

这个动画效果很简单,有很多方式都可以做到,但是pop来实现它,只需要下面几行代码。

1
2
3
4
5
6
7
  POPBasicAnimation *animation = [POPBasicAnimation animation];
  animation.property = [self animationProperty];
  animation.fromValue = @(0);
  animation.toValue = @(8000);
  animation.duration = 2.0f;

  [self.numberLabel pop_addAnimation:animation forKey:@"numberLabelAnimation"];
1
2
3
4
5
6
7
8
9
10
11
12
  - (POPMutableAnimatableProperty *)animationProperty {
  return [POPMutableAnimatableProperty
      propertyWithName:@"com.curer.test"
           initializer:^(POPMutableAnimatableProperty *prop) {
               prop.writeBlock = ^(id obj, const CGFloat values[]) {
                 UILabel *label = (UILabel *)obj;
                 NSNumber *number = @(values[0]);
                 int num = [number intValue];
                 label.text = [@(num) stringValue];
               };
           }];
}

哈哈,搞定了。pop太强大了。但是细心的同学会发现动画似乎不是我们想要的,我们希望做到那种一开始很快速很激动,最后却有一点慢慢的“欲求不能”的感觉。

很直观的,我们使用了万能的EaseOut动画

1
2
3
4
5
6
7
  POPBasicAnimation *animation = [POPBasicAnimation animation];
  animation.property = [self animationProperty];
  animation.fromValue = @(0);
  animation.toValue = @(8000);
  animation.duration = 2.0f;
  //增加animation 时间函数控制
  animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

增加了一行代码,但是发现这个动画变化的时间还是不能让我们满意,一开始变化的还是慢,后面变化的又有点快。

how can we better build animation

动画的实现其实很简单,抛开性能,就是一个个不断变化的图片,对于我们这个简单的动画,就是一个从0到8000的变化,如果x轴为时间,y轴为大小。我们第一个动画其实是这个样子

easeout好一点是这样子

我们其实希望是这个样子

CAMediaTimingFunction 实际上还提供另一个方法,不是很常用,但是却非常适合我们现在的场景。

1
+ (id)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;

这里我们描述的“时间函数”其实就是贝塞尔曲线

这里推荐一个网站可以很直观的生成贝塞尔曲线。 这里我们得到了参数(.12,1,.11,.94)。

1
2
3
4
5
6
7
  POPBasicAnimation *animation = [POPBasicAnimation animation];
  animation.property = [self animationProperty];
  animation.fromValue = @(0);
  animation.toValue = @(8000);
  animation.duration = 2.0f;
  //修改animation 时间函数
  animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.12 :1: 0.11:0.94];

这里我们已经得到我们想要的动画效果了。而且看上去相当不错。

how can we better build animation

如何才能有更好的效果呢?动画的速度,时间,等等参数都会影响到动画的效果是不是会完美。如何判断动画效果是否足够好。的确是个很难的问题。而解决这个问题的关键,不在于工程师自己折腾,应该找专业的人来做。而这时Tweaks就闪亮登场了。

初始化的时候创建2个tweak用来动态调整时间和目标数值。并修改一下默认的UIWindow为FBTweakShakeWindow

1
2
3
4
5
6
7
8
9
10
11
12
  //reset window
  self.window = [[FBTweakShakeWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

  FBTweak *animationDurationTweak =
      FBTweakInline(@"Content", @"Animation", @"Duration", 2.0, 1.0, 3.0);
  animationDurationTweak.stepValue = [NSNumber numberWithFloat:0.1f];
  animationDurationTweak.precisionValue = [NSNumber numberWithFloat:3.0f];

  FBTweak *animationToValueTweak =
      FBTweakInline(@"Content", @"Animation", @"ToValue", 8000, 1000, 10000);
  animationToValueTweak.stepValue = @(1000);
  animationDurationTweak.precisionValue = [NSNumber numberWithFloat:1.0f];

再把原来创建动画的代码稍微修正一下

1
2
3
4
5
6
7
8
9
10
11
12
  POPBasicAnimation *animation = [POPBasicAnimation animation];
  animation.property = [self animationProperty];
  animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.12 :1: 0.11:0.94];
  animation.fromValue = @(0);

  double animationDuration =
      FBTweakValue(@"Content", @"Animation", @"Duration", 2.0);
  animation.toValue =
      @(FBTweakValue(@"Content", @"Animation", @"ToValue", 8000));
  animation.duration = animationDuration;

  [self.numberLabel pop_addAnimation:animation forKey:@"numberLabelAnimation"];

这样当摇晃手机的时候就可以动态调整动画参数了,最后数据会保存在plist :)。

越简单的越强大~

Design Patterns in iOS — Class Clusters

作者 studentdeng
2014年4月8日 14:49

我对设计模式一直都是一个若有若无的感觉,特别是在手机端开发,觉得用处不是很大,认为设计模式是为了大规模团队合作,分工才能体现出效果。设计模式可以通过分不同的“层”让大家协同开发,相互之间不产生影响。但是最近看法有点改变,觉得还是需要多少了解一些。

天天使用的framework确实是一个庞大的项目,从framework的设计中可以找到很多设计模式的影子,而且还是一个很好的生产化的例子。这里先介绍 Class Clusters

Class Clusters 几乎涉及到iOS日常的所有开发过程中,也可能正是这样,导致我们很容易把它彻底遗忘。这里就拿最常用的 NSString 来讲。

1
2
3
4
5
6
7
8
9
10
  NSString *string1 = @"helloworld";
  NSString *string2 = [[NSString alloc] initWithFormat:@":%@", @"helloworld"];
  NSString *string3 = [NSHomeDirectory() stringByAppendingPathComponent:string1];
  NSTextStorage *storage = [[NSTextStorage alloc] initWithString:string1];
  NSString *string4 = [storage string];

  NSLog(@"%@", [[string1 class] description]);
  NSLog(@"%@", [[string2 class] description]);
  NSLog(@"%@", [[string3 class] description]);
  NSLog(@"%@", [[string4 class] description]);

不知道有多少人试过哈,string3的返回还是让我吃了一惊。下面的结果是在Xcode5.1 SDK7.1 下的结果。

__NSCFConstantString
__NSCFString
NSPathStore2
NSBigMutableString

通过上面的方法创建的 NSString 最后都产生了不同的子类。有人可能会奇怪为什么需要不同的 NSString。因为对于大部分的以阅读内容为主的App来讲,很大部分资源消耗在了字符串处理上面(存储,解析,比较等等),所以对于字符串的存储需要有不同的方式来满足不同的情况,这样才能有性能上的提高。

Note: 设想一下,在这些场景上面,如果Apple直接把这些类扔给开发者,会有什么问题呢?

那么开发者需要自己在不同的场景决定使用不同的子类,不仅学习成本提高,而且也容易生成性能不太好的代码。
现在简单的 NSString 就可以直接覆盖上面的所有场景。而且随着iOS的软硬件的后续开发,开发者还可以在不修改代码的情况下获得性能提升。

既然看到了它的强大之处,那么就开始了解吧。 既然这是第一篇DesignPattern那么就从最简单开始 :)

Abstract Classes

这里引用一下Mike的内容

An abstract class is a class which is not fully functional on its own. It must be subclassed, and the subclass must fill out the missing functionality.

An abstract class is not necessarily an empty shell. It can still contain a lot of functionality all on its own, but it’s not complete without a subclass to fill in the holes.

Abstract Class 的概念很简单,类中所有的方法不需要全部有具体的实现,相当于定义了很多的接口。比如一开始的 NSString

Class Clusters

A class cluster is a hierarchy of classes capped off by a public abstract class. The public class provides an interface and a lot of auxiliary functionality, and then core functionality is implemented by private subclasses. The public class then provides creation methods which return instances of the private subclasses, so that the public class can be used without knowledge of those subclasses.

Clusters的角色不仅要实现 Abstract Class 的方法,还需要自己实现自己的特殊化需求。Abstract Class 负责提供一个“外壳”,真正“干活”的就是Cluster class。这样外部就只需要了解Abstract Class就可以了。

NSString Benefits

比如 __NSCFConstantString 负责 const string,类似 @”helloworld”这样的字符串。这样的字符串有一个特点,不会被修改,当真正处理的时候,可以分配大小合适的内存,甚至可以分配在只读 data segment上面,而不需要分配在堆上面,如果有相同的字符串引用就可以完全赋值相同的地址。那么在retainCount上面的处理也就和其他字符串处理有很大不同。

NSPathStore2 看上去是处理有Path相关的字符串,因为没有源代码,这里我们可以大胆猜测一下,path相关的主要是做字符串的拼接操作,而这些字符串通常很长,占用空间大,但是重复的概率缺很高,那么就可以缓存一些字符串,这样可以减少一些内存的分配释放开销。

How to use

The class cluster architecture involves a trade-off between simplicity and extensibility: Having a few public classes stand in for a multitude of private ones makes it easier to learn and use the classes in a framework but somewhat harder to create subclasses within any of the clusters.

就像Apple文档中提到的,Class Cluster 是在简单和扩展性上面做了一个妥协。Class Clusters 的子类化比较麻烦,而且也看上去也非常trick,Apple 更推荐的方法是用组合的方法来扩展。

大家都知道设计模式有一个非常重的坑就是被过渡设计。Class Cluster 可以帮我们

  • 减少了if else 这样缺乏扩展性的代码
  • 增加新功能支持不影响其他代码

那么这个非常适合应用在适配上面,比如不同屏幕的适配,不同厂家可能的不同的需求。

1
2
3
4
5
6
7
8
9
10
11
+ (id)alloc {
    if ([self class] == [SFSSearchTVC class]) {
        if ([UIDevice currentDevice] systemMajorVersion] < 7) {
            return [SFSSearchTVC6 alloc];
        } else if ([UIDevice currentDevice] systemMajorVersion] == 7) {
            return [SFSSearchTVC7 alloc];
        }
    }

    return [super alloc];
}

上面是代码来自BJ Miller’s blog A Cluster to Remove Clutter 是用于适配iOS6,iOS7的简单例子。

Conclusion

很多设计模式都很像,也很容易糊涂,比如工厂模式和Class Clusters在某些地方就很类似,我自己也并不能很好的分清楚。 设计模式的本质是为了解耦。不管使用哪个设计模式,我们最后追求的都是简单、容易维护和扩展的代码。

octopress notebox plugin

作者 studentdeng
2014年4月4日 16:48

用了这么长时间的octopress总该扩展一点点事情了,在Blogging的时候,总有一些信息是需要被特殊标记的,但是我并不喜欢简单的加粗或是斜体。类似的东西在Apple Document中有很多

Apple Document Note sample

这里我就把这个功能照搬到octopress中了。

1.在plugins目录创建一个notebox.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module Jekyll
  class Notebox < Liquid::Block

    def initialize(name, id, tokens)
      super
      @id = id
    end

    def render(context)
      stressText = paragraphize(super)

      source = "<div class='notebox'><p><strong>Note: </strong>#{stressText}</p></div>"
      source

    end

    def paragraphize(input)
      "#{input.lstrip.rstrip.gsub(/\n\n/, '</p><p>').gsub(/\n/, '<br/>')}"
    end

  end
end

Liquid::Template.register_tag('notebox', Jekyll::Notebox)

2.在sass/custom中的文件_stype.scss的最后添加下面的代码

1
2
3
4
5
6
7
8
9
.notebox {
  border:1px;
  border-style: solid;
  border-color: #5088C5;
  background-color:#fff;
  margin:.75em 0 1.5em;
  padding:.75em .667em .75em .750em;
  text-align:left;
}

3.markdown的语法(因为格式问题写成了%,需要替换成%

{% notebox %}
the text to note
{% endnotebox %}

效果

Note: text

Core Bluetooth Concept

作者 studentdeng
2014年3月22日 20:41

Core Bluetooth 里面的名词还是挺多的,这里简单记录一下,一上来的时候,还是很容易混淆的,这里记录一下。

Basic Concept

Bluetooth low energy (BLE,还有地方叫做BTLE,最恨各种简写了) 简单说是一种低功耗的短距离无线传输技术,主要用于低功耗设备传输,比如心率、记步器、智能家居方向,还有连接其他iOS设备。

Core Bluetooth API 支持BLE4.0,做了协议封装,让开发者不需要完整了解BLE协议就可以快速开发APP。

Central and Peripheral

BLE中有2个非常重要的概念就是Central和Peripheral,有一点类似Client Server。

  • Peripheral是数据的发送方(比如运动手环需要把位置,步数等数据传递给其他设备)。
  • Central是数据的接收方(比如手机接收手环传递来的步数)

1-1 Central 和 Peripheral 心率设备和Apple product

Centrals Discover and Connect to Peripherals That Are Advertising

  • Peripheral把advertising packets广播出去,advertising packet 包括会包含一些重要的信息,比如设备名字,所提供的服务。

  • Central 则是扫描自己感兴趣的advertising packet,比如一个APP需要查找当前家里的室温,会通过参数设定,只是检索温度设备发来的packet。

1-2 一个简单的advertising模型

Data structure

  • Peripheral 是最上层的一种服务抽象,比如iOS 系统内置的ANCS服务,另外我们自己也可以创建自己的服务。
  • characteristic 则是用来描述服务中的具体内容(比如手环有传递行走路程的接口,还有行走位置的接口),一个服务可以包含多个characteristics。

1-3 心率检测仪包含1个服务,1个服务中包含2个characteristics,一个用来传递心率,一个用来传递位置

How to

YmsCoreBluetooth 是个不错的框架,有很详细的介绍,这里就不赘述了。

Apple Notification Center Service

作者 studentdeng
2014年3月22日 15:42

名词解释与约定

名词解释

  • Apple Notification Center Service 简称 ANCS。
  • ANCS 服务(iOS设备,如iPhone,iPad等)的publisher 称为 Notification Provider。
  • 任意的ANCS服务的client(硬件设备,配件)称为 Notification Consumer。
  • Generic Attribute Profile简称GATT。
  • 在iOS NotificationCenter 显示的通知称为 iOS notification。
  • 通过GATT characteristic 发布的通知称为 GATT notification。
  • iPhone、 iPad等运行iOS系统的设备称为iOS设备

约定

文本档覆盖的内容中除特别描述外,和字节序相关的都是用Little-endian (小端模式)。 文本档覆盖的内容中除特别描述外,和字符串传递相关的都是UTF-8

Note: ANCS 并不保证始终存在,服务开启,关闭机制由iOS系统决定,Device 需要一直检测,查找ANCS是否存在。

协议内容

ANCS的uart profile UUID : ” 7905F431-B5CE-4E99-A40F-4B1E122D00D0”

uart profile 中包括3个 characteristic

  • Notification Source UUID: “9FBF120D-6301-42D9-8C58-25E699A21DBD” (notifiable)
  • Control Point UUID: “69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9”(write with response)
  • Data Source UUID:” 22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB “(notifiable)

Device端Notification Source 是必须实现的。Centrol Point 和 Data Source 可选。

Notification Source Characteristic

Notification Source characteristic 包括3个功能

  • 新的iOS设备通知
  • iOS设备通知修改
  • iOS设备通知删除

当Device端 subscribes Notification Source characteristic时,GATT Notification 会立刻分发出去。所以,Notification consumer (Device)在subscribe之前就需要做好立马接受和处理消息的状态。

Notification Source response format

图2-1 Notification Source characteristic 通过GATT Notification 的格式。

  • EventID:描述iOS设备上面的通知是增加、删除、还是修改。
  • EventFlags: 标志位,描述iOS设备通知(重要, 静默)
  • CategoryID:iOS设备通知类型
  • CategoryCount:给定类型的通知数量,比如有2个未读email消息通知,这时有一个新的未读消息通知push到iOS设备,这时CategoryCount = 3 NotificationUID:通知的唯一标示,这个字段用于和Control Point characteristic 检索更多的信息。

image

表格3-1:CategoryID的描述

image

Notification Source Response Example

Device 获取的来此Notification Source的数据是“00 01 00 01 43 00 00 00”

image

图2-2一个iOS通知的生命周期。

比如当iOS设备(如iPhone)收到一个iMessage消息,iOS NotificationCenter会产生一个Notification,ANCS会通知device 有一个新的通知。当iOS设备阅读这个iMessage消息之后,iOS Not ificationCenter 会删除掉这个通知,ANCS把这个删除通知push到device

Control Point 和 Data Source Characteristic

只是Notification Source 不能获得足够的信息, Control Point 和 Data Source characteristic用来解决这个问题。 Device 向Control Point characteristic 写一个命令,如果成功,会从DataSource characteristic 获得response。

获取通知属性命令

该命令根据NotificationUID 查找通知的详细内容(通知属性)。

image

图2-3 获取通知属性命令格式 CommandID: 必须设置为0 NotificationUID:通知的唯一标示(Notification Source 获得) AttributeIDS:需要检索信息list

image

图3-5 可以检索的通知属性列表,其中Title, subtitle, Message 需要增加2个bytes的字段表示长度。

获取通知属性命令Example “00 43 00 00 00 00 01 FF FF 05”

image

获取通知属性命令Response消息格式

image

图2-4 获取通知属性命令返回数据格式

CommandID: 0 NotificationUID:通知的唯一标示 AttributeList:具体的属性返回数据列表. 如果返回的属性空,长度是0

如果返回的数据长度大于 GATT MTU,那么数据会被分几段传输。Device 需要对数据拼接。

通知属性命令Response 消息格式Example

下图查找 NotificationUID为2的 AppIdentifier、Title、SubTitle、Message Date属性返回数据

image

获取APP属性命令

该命令通过APPIdentifier查找iOS设备中安装的APP的属性。

图2-5 获取APP属性命令格式

CommandID: 必须设置成1 AppIdentifier:字符串’\0’ 结尾。 AttributeIDS:查找ID列表

获取APP属性命令 Example

查找AppIdentifier 为 “com.apple.mobilemail” 的APP属性

image

获取APP属性命令Response消息格式

image

图2-6获取APP属性命令返回数据格式

  • CommandID:必须是1
  • AppIdentifier:字符串 ‘\0’结尾
  • AttributeList:具体的属性返回数据列表. 如果返回的属性空,长度是0

如果返回的数据长度大于 GATT MTU,那么数据会被分几段传输。Device 需要对数据拼接。

获取APP属性命令Response消息格式Example

查找AppIdentifier 为 “com.apple.mobilemail” 的APP属性返回数据 汉字 “邮件”

image

sessions会话

ANCS 的 session 从设备订阅characteristic 开始到取消订阅或是disconnect结束。所有的Identifier 比如 NotificationUID,AppIdentifier 只在当前的session有效。

当session结束后,设备需要删除掉所有的在session中获得的Identifier信息,这些信息会在session建立的时候重新通知设备.

错误代码

  • 未知命令:(0xA0)
  • 无效命令:(0xA1)
  • 无效参数(0xA2) : 比如NotificationUID 找不到

注意:如果产生了上面的错误,都不会再收到任何的GATT 通知。

参考内容

Apple Notification Center Service (ANCS) Specification

Xcode5 Plugins 开发简介

作者 studentdeng
2014年2月21日 13:13

这篇文章介绍如何给Xcode5开发插件。如果之前了解iPhone & iPad 开发,那么下面的内容对您非常熟悉。最后我们会开发一个简单的插件,记录Xcode开发中Building的时间。

准备工作

首先编写一个Plugin还是需要不少额外的配置,这里推荐Xcode Plugin Template。用这个templage来帮助我们开发Plugin。

另外,编写插件和之前的iPhone or Mac上的APP不太一样。从某种意义上来说就是用Xcode调试Xcode。所以这里需要额外配置一点东西。

  • 修改Scheme

image

  • Executable 选择Xcode.app

image

当我们Build & Run Project的时候就可以看到启动了一个新的Xcode进程,当然除了Xcode, Mail或是其他程序我们都可以调试。

如何编写插件

因为Apple至今并没有公开Xcode Plugin的文档,所以我们需要通过一些其他方法寻找思路。

1
2
3
4
5
6
7
 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationLog:) name:nil object:nil];
  
  - (void)notificationLog:(NSNotification *)notify
  {
      NSLog(@"%@", notify.name);
  }
  

这里稍微有一点特殊,参数notificationName 设为nil,下面是Apple的文档,不是很清楚。

notificationName If you pass nil, the notification center doesn’t use a notification’s name to decide whether to deliver it to the observer.

但是目前来看,似乎可以看到所有的通知。当然绝大部分是重复的,对我们没有意义。很幸运最后我们找到了2个通知是我们需要的,下面的代码,已经做了过滤。

1
2
3
4
5
6
7
8
9
 - (void)notificationLog:(NSNotification *)notify
  {
      if (![notify.name hasPrefix:@"IDEBuildOperation"]) {
          return;
      }

      NSLog(@"%@", notify.name);
  }
  

这2个通知分别是

  • IDEBuildOperationWillStartNotification
  • IDEBuildOperationDidStopNotification

这个我们不得不赞一下cocoa的命名方式,大家都可以猜出这2个通知的含义。剩下的事情就很简单了。统计build时间。

最后

这是项目源代码。有兴趣的同学可以玩玩,看一下自己的编译时间有多长。另外最终的代码中还增加了2个小的features。

  • 查看当前打开Xcode的人数
  • 查看自己打开Xcode专注的时间有多长,这个时间是当Xcode被focus的时候才统计,另外不足1分钟不计算在内。

Have fun!

我的2008-2018

作者 studentdeng
2014年2月11日 02:04

5年前拿到人生第一个冠军

因为小时候很贪玩,并没有考好大学。07年上了一个普通大学。08年正好大二,本来我是打算金盆洗手好好学习的,而且大一做的还不错。但是一个叫做Dota的游戏又给了我一次玩的机会。当时玩这个的人还很少,我和室友和另外3个其他学院的家伙们组成了一个电竞战队。我们的能力起初并不被看好,但是我们却拿到了当年全省第一届电竞Dota项目的冠军,也获得了人生第一桶金。再后来还参加了另一个比赛,最后去E世界演播大厅打比赛。一路上来每局都是逆风,但我们都一次次靠团队配合和抓对方失误扳了回来,最后成为一个黑马进入华北赛区8强。

游戏对很多人来说都是玩玩的娱乐项目,但是对我和我的小伙伴们来说,里面有很多个日日夜夜看比赛录像,锻炼意识,枯燥的基本功训练在里面。里面有成功的欢乐(冠军之后,还被媒体采访过)也有遗憾的泪水。我还记得打完最后一场在回旅店的路上。我挺自责的,因为团队中我是中单的Gank发起者(Dota游戏里面一个比较重要的位置),而我觉得我发挥的不够好,连累了大家。我的队长对我说,咱们没有北京这些学校这么好的电竞氛围,能走到这里已经很厉害了。Anyway,此生对游戏再也没有当年的执着劲了。因为能够在正确的时间,在一个玩游戏被众人鄙视的氛围里面遇到一帮子一起玩的人,组成一个团队,并且最后拿到冠军,已经是我能做到的极限了而且还有极大运气。

4年前开始思考未来的工作

在游戏人生将近10年之后,我发现,我还是得回到现实,为自己的未来做打算。因为当时在学生会网络部混日子,给学院和学校做个烂网站。然后有一天其他学院的同学找到我说咱们一起联合做一个项目,参加全国的比赛。也托玩游戏的福,我妈妈觉得她的儿子,这么多年来第一次战胜了“别人家的孩子”,给我买了一个多普达的手机,是我的第一部智能手机。极大的震撼了我,我觉得电脑太笨重了,便携的移动设备一定是王道,因为有大量的碎片时间可以利用,我觉得智能手机一定会超过PC,我的未来一定是在移动方面的。也托这个比赛的福,更刺激了我在移动方面的兴趣,从而在网上认识了一些IT界的大牛,都在大公司,还有在国外工作的,并且参与了他们的开源项目。大二的一个暑假我都在编程中度过的,最后那个比赛获得了优秀奖,全国前20。和他们一起工作,对我的帮助极大,不仅是理论还是实践。而且我相信给我更多的时间可以做的更好,因为我参加的那个开源项目是当时windows mobile上面最好的也是唯一的UI Framework。

3年前开始创业

不知不觉大四了,之前一起参加开源项目的一个家伙,准确说是2个家伙要出来创业了,不准备在大公司呆了,(他们一个在微软一个在中国移动),然后这个家伙给我吹了一个大泡泡,现在想想挺有趣的,当是准备做基于手机联系人的IM,那会还没有米聊和微信。我觉得挺有意思的,这个不就是做一个移动版本的QQ么,而且当是的手机发短信还是挺费钱的,一条信息的流量几乎忽略不计,而且没有字数限制。一定可以改变人们的沟通方式,后面还可以发图片发声音,而不是只是文字。当是的产品原型是国外的Kik Messenger。当然,我们并没有做到最后,因为各种各样的原因,最后我们放弃了这个项目。

在2010年底,我们开始把精力转向在线教育。更准确说的是在线英语教育。我接触在线教育是在小学的时候,在线教育能够把高质量的教学资源给更多的人,而且中国学英语的人会越来越多,市场会越来越大。另外结合手机本身就可以发音的特性,这些都是普通书本不能提供的。最后我们还找到了一家著名的英语培训机构作为我们的内容提供商。另外参考了一家广州企业的盈利模式,然后开始尝试。我在这个过程中开始把之前做IM项目积累的经验,快速应用到这个项目中。而且最后作为我的毕业设计,拿到最高分90,做为我大学生活的结束。

2年前开始转行,拿投资

我第一次看到iPhone是大二的时候,当时我觉得这东西就是一个掌机,后面才发现是手机(别笑话我),而且一直认为这是一个纯装逼的行为。但是当真正做到移动领域的时候,开发设计windows mobile上面App的时候,很多的设计的确都是参考iPhone的。也开始接触iPhone上面的开发,到后面发现windows mobile真的不行了。不管是工业设计,还是App的用户体验都差距巨大,但是价格缺没差多少。。。而Android的开放和便宜(相对于windwos mobile 的license),windows mobile 必然是没有前途的,于是乎开始了和AppStore 斗智斗勇的日子:),最后成为了国内比较早的一批iOS开发者。

创业的这个项目也遇到了问题,很多问题,盈利模式很难复制,用户没有在移动产品上面的付费习惯也缺乏渠道,而且用户量不仅不够,还在不断下滑。团队有部分人因为家庭问题,先走一步了(因为之前日子过的太苦逼,女朋友表示日子没法过)。我也考虑过离开,但是觉得不能以这种方式离开,主要是不能接受这种方式的失败,然后就继续做下去了。付费很难做,但是用户量还是可以有的。最后我们做了一个垂直社交————英语教育社区。当时英语教育软件里面为数不多的(可能是唯一的)移动社区App,我们被AppStore 推荐,iOS下载量一天增长了1W倍,加上Android成功内置电信,最后拿到了200W天使投资。这段日子虽然苦逼,却是我工作上面成长最快的时候,人总是被逼的么,拿着倒计时牌算日子过。中间遇到些问题并请教了很多国外的开发者,自己也写了一些内容比较深的blog(相对在那个时候),自认为在iOS设计和开发上面自己在业内可以排到top 5%,因为不仅产品被资本和Apple Store认可,而且那会自己的很多blog也在Google搜索的最前面。

另外,一个同学去CMU了,我觉得不管是我的工作,还是其他什么的,我都需要把英语学好了。我问他你准备了多长时间,他说4年。从英语到字幕组到论文到申请。 我知道我的大学成绩是这辈子不太可能去CMU了,但是我还是蛮想出国的,然后介于我低下的语言天赋,我觉得我至少需要准备3年英语。

1年前退投资,分手

2012年的开始还是蛮不错的,团队终于没有钱的压力了(暂时)。然后我们搬到了美团,美丽说他们曾经在的地方,创业公司么,大家都希望沾点喜气,而且交通也方便,而且便宜。然后我们3个月没有做事情,因为我们暂时走到了前面,而且前一段时间我们一直在转变,需要想清楚我们应该做什么。当然不是完全没做,只是主线产品没大的升级,做过一些其他的小尝试,但是效果都不好。整个团队都很迷茫,我也不例外。我觉得这样下去,我在年底就没有什么东西可以总结的了(最后显然我把事情想简单了),然后就报了EF的班,开始我的3年英语计划。另外开始健身,减肥。一个月减了20斤,每天游泳1.5km。工作方面开始负责服务器,不仅仅是负责iOS方向。实现并设计了新版本的服务器。时间转眼就到年底了。因为各种原因吧,我们和投资人谈崩,团队整体撤出,产品留给投资人。然后就是换办公室,无外乎就是换一个小房子,然后再清退一些人,有意思的是,这个时候也和女朋友分手了。这就是所谓的祸不单行吧。

2012挺特别的,我的生日正好是末日的那一天,可惜万分期待的末日并没有到来。我意识到我并不是真正的足够了解自己,也并不是真正的清楚自己在做什么,几年前我自认为我在做正确的事情,但是我在那个时候觉得我看不清楚。那会我开始接触MBTI,了解积极心理学,给自己灌超大量的鸡汤,感谢我的老大(leo,cube),感谢我周围的朋友(强X和顺X),当然还有默默关注着我而且始终在背后默默帮我的风哥(突然想起来,最近实在太忙,坑了风哥了,对不住了),最后还要感谢一个远方的笔友。

今年又转行了 Freelance

2013年,移动互联网发生了天翻地覆的变化,一大堆的开发者涌入了这个行业,也有大量的App走到大家面前,我的tutor或是close friend,和他同甘共苦的另一半修成正果了,而且他也比较给力,前一段时间他们团队拿到$2000W的B轮,宝宝也有了,祝福。另外强X拿到Morgan的offer开始自己的职业生涯了。我呢?我觉得前几年过的太累了,按照老大的话,就是过的没有尊严。2013年准备挣挣钱。这一年来说我可以算是一个Freelance。平时就是看看书,学学英语,健健身,遛遛狗。有事情了,然后做一点。到头来还算不错,至少比一般大公司挣得多。不用上下班,没有人约束你做什么,想睡几点睡几点。一年也就工作不到4个月,大部分在休假。但却是我最难受的1年。因为这个生活显然是50岁的节奏,不是一个20多岁的人干得事情。当然这也有好处,就是我有大量的时间和负能量去支持我这个龌龊的灵魂又减了将近20斤,练出6块腹肌并且在年底来了一次马拉松。因为我如果不做一点东西的话,2013年总结的时候我该写点什么呢?

扯远了,移动行业2013年是一个起点,很多传统行业开始意识到移动领域的价值。而这一年中,我们的很多项目也和他们相关。年底也出现了很多将互联网概念和传统行业结合创业的例子,不管是黄太吉还是马佳佳的情趣用品,都在告诉我们移动互联网还有一波浪潮可以淌。很多传统企业在2014年会加大在移动方面的探索。这里面还是有很多机会。我们现在的能力在2014年做到2013年甚至可能比2013年还要好也并不是不可能。但是我却不得不看清楚一个现实就是移动互联网入门的门槛是如此之低。很多一线大公司的平台级别产品极大的简化了开发的风险和时间。而且投行是这个一个行业的方向标,我自己也并不看好自己现在处的行业中的位置————一个纯互联网方案提供商。

小米通过他的品牌不仅卖手机,还要卖路由器,电视等等。我依稀感觉到互联网不仅仅是改变人们的沟通方式,会开始慢慢的走近人们的生活,而且事实上有些地方已经走到前面,包括交通(机票,火车票,打车),运动健康(可穿戴设备),还有一些政府的项目也开始增加了不少移动部分。另外就是后端的XX云概念。而这些都是我现在没有办法做的事情。所以,我又准备转行了。

朋友家人

在创业的风风雨雨中,认识了一些朋友。有的走上正轨,拿到B轮,得到市场认可,而且资本也会让他们继续平稳的走下去。线上教育一直是我喜欢做的事情,但是教育本身就是一种回报很难估算的投资(不像一件衣服好看不好看那么直接),而且还有大量的时间成本。再他们赢得一些用户口碑之后,一段时间不容易被其他产品影响,还会越滚越大。另外一些朋友开始在大公司里面摸爬滚打,也蛮不错的。因为毕竟是主流价值观。有一大推可以借鉴的经验和路子。还有一些朋友准备考GMAT。

我家是一个普通家庭,可能唯一特别一点的就是我有一个特别开明的妈妈,我妈妈认为她最大的遗憾就是当初她老师告诉她要学好电脑,而她没咋当回事。所以她一直很支持我玩电脑。有一天我有意无意的在她的书柜里面找到的一本有关Basic语言的编程书,然后自己写下了我的第一段程序,一个简单的计算器,那会我上小学6年级。我一直觉得我有一个很特别的妈妈,我妈妈是一个大网虫,我的QQ号都是她替我申请的,而且特别喜欢电子游戏。但是,我显然让她失望了。我并没有按照她给我计划好的路子走,而是按照自己的性子来的。走了一条很大的弯路,而且是不归路。我没有考上一个好大学,也没有找一个可以让她安心的工作。而且现在我也没有办法告诉她我什么时候可以结婚生孩子。

妈妈的身体差一点,爸爸的身体比较厉害,打了30多年的拳,虽然比我大30岁但是在我减肥之前我确定他身体比我好。他们2个年轻的时候做过几次正确的投资,现在衣食无忧,自己没事弄点小买卖充实一下无聊的日子,按照我妈的话,这辈子剩下的事情就是等我结婚报孙子。

但是很明显,作为一个26岁,要啥没啥,连个稳定收入都没有的闲杂人等,到哪里给她找儿媳妇去?

2014

2014年我有2个比较大的计划需要尝试。抽象来说,就是给自己找一个大的平台。在这几年来,我深刻的认识到了自己的见识、理解还是能力都远远不够。不得不承认我可能错过了我人生中的第一次机会。我需要的是一个大的平台,让我从中学习理解这个行业。一个是出国,一个是去一家大公司,而且在一个不错的部门里面。我还没有找到这个大平台,但是我能确定一点的是,这个平台是符合主流价值观的。因为我这几年来偏离太多,虽然我自己收获不小,但是我希望能够走一些别人走过的路。最近的1个月来,自己一直在忙这2件事情,估计最晚需要忙到明年6月份。那句老话说得好,人无远虑,必有近忧。我现在还没有办法估计5年后的事情,因为我5年前每一年做的事情,都没有重复的,而且都是某种程度上面不一样的。我唯一能确定的就是2014年,也是一个完全不一样的一年。而且是我人生中最重要的一年。如果我找到一个平台,那么意味着我2013年成为一个美丽的间隔年,而如果没有,那么就是又一个非常大的教训。

困惑 2015-2018

上面的是我在一个月前写的,这几天我一直再思考一个问题,今天又失眠了。我未来的5年应该是什么打算呢?一直想不清楚,所以也不知道该如何写下去。其中有一个很重要的事情,就是成家是否在我的计划之中,同时它还附带了户口,房子,小孩上学等一系列问题。太原眼下,甚至是3年都很难有适合我的工作,但是生活还是未来小孩上学太原都要比北京的条件要好不少。花大量的时间和金钱拿一个北京的户口和房子值么?还是在太原的投资回报率高?抛开工作,太原的生活显然比北京舒适。但问问自己,甘心么?这一切都好难说。而且很多事情还涉及到父母,不是我一个人说了算,头疼。希望2014年底,我可以有一个自己的答案。恩,是希望,不是必须完成的事情。

❌
❌