标签 女儿 下的文章

给女儿搭建一个博客站点

时光荏苒。转眼间女儿已经成为一名小学生了,依稀还记得当年果果呱呱坠地的情景,独自回味,感慨万千。

果果3岁前,都是我来记录她的生活点滴和成长历程,那个时候她是我们生活舞台的主角。3岁后,果果学会了说话,上了幼儿园,开始学习各种知识、技能以及各种才艺。尤其是在幼儿园中班之后,她学会了写字、组词、造句和写日记,果果完全可以自己用文字来表达自己了! 我觉得是时候让她自己来记录她的成长历程了,我和她妈妈只是辅助和指导就好了。这种想法日益迫切,尤其是果果今年上了小学后,果果的成长更快了。我觉得迫切需要给她一个平台去表达她自己和记录她的成长。传统手段不能满足需求,于是我就想到给她搭建了一个博客站点,辅助她用网络文字、图片的形式记录自6岁上学之后的成长历程。于是这个周末就花了些时间,给女儿搭了一个博客站点。

下面以“流水账”的形式,记录一下这个站点的搭建过程,也许能给和我有同样需求的家长们带来一些帮助^_^。

一、选型和准备工作

博客站点,我首选静态页面的。静态页面,又要快速搭建,我首选github page。github page一般情况下在国内访问相对较为稳定,访问速度也不错,ping延迟一般在100多ms,比我独立购买的Digital Ocean的主机的延迟低很多。还有另外一个原因就是市面上几乎所有主流静态页面生成工具都对github pages有着不错的支持。由于采用静态页面,即便将来迁移到VPS,也几乎是无缝的。于是给果果在github上申请了账号

用与搭建博客、个人站点的静态页面生成工具很多,比如:jekylloctopresshexo以及hugo,用哪个呢?作为Gopher,我首选hugo。接下来,我们来看看用hugo是否能搭建出满足我们需求的基于github page的博客站点吧。

hugo的安装参考hugo github主页上的说明即可。由于hugo import了很多第三方package,有些package可能在墙外,因此配置上加速器是更好的、更快的^_^。

二、基于hugo搭建博客站点

去年曾写过一篇《使用Hugo搭建静态站点(http://tonybai.com/2015/09/23/intro-of-gohugo/)》,讲述如何通过hugo这个golang开发的工具搭建一个属于自己的静态站点(static websites)。不过那篇文章并没有谈到hugo如何与github page结合。

hugo官方文档中,对如何使用hugo创建基于github page站点有着较为详尽的描述,这是由一位名为Spencer Lyon的外国开发者贡献的文章,并且Spencer Lyon给出hugo github page的工程template: hugo-gh-blog。我这里就直接使用了该工程模板,并基于hugo_gh_blog做一些定制化修改,比如“汉化”之类的。

下面是详细的步骤:

1、clone hugo_gh_blog

我们首先将Spencer Lyon的hugo_gh_blog代码库clone到本地,这是我们博客搭建的基础:

$mkdir GuoGuoBlog
$cd GuoGuoBlog
$git clone https://github.com/spencerlyon2/hugo_gh_blog.git
Cloning into 'hugo_gh_blog'...
remote: Counting objects: 489, done.
remote: Total 489 (delta 0), reused 0 (delta 0), pack-reused 489
Receiving objects: 100% (489/489), 84.50 KiB | 24.00 KiB/s, done.
Resolving deltas: 100% (232/232), done.
Checking connectivity... done.
$cd hugo_gh_blog/
$ls
LICENSE        README.md    config.yaml    content/    deploy.sh*    static/        themes/

2、编辑config.yaml和本地调试

进入hugo_gh_blog目录,编辑config.yaml,设置站点的一些元数据:

---
contentdir: "content"
layoutdir: "layouts"
publishdir: "public"
indexes:
  category: "categories"
baseurl: "http://baisibei.github.io"
title: "果果的成长历程"
canonifyurls: true
theme: "Lanyon"
...

接下来,我们来生成我们的静态博客页面:

$hugo
0 draft content
0 future content
2 pages created
0 paginator pages created
2 categories created
in 48 ms

hugo将创建public目录,并将生成的页面放入该目录:

$ls public
404.html    categories/    css/        favicon.ico    img/        index.html    index.xml    posts/        sitemap.xml

public/index.html就是站点首页。

我们在本地可以启动hugo server,并查看生成的站点情况:

$hugo server -t Lanyon
0 draft content
0 future content
2 pages created
0 paginator pages created
2 categories created
in 54 ms
Serving pages from /Users/tony/GuoGuoBlog/hugo_gh_blog/public
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)

打开浏览器,输入localhost:1313。不出意外,你就可以看到类似下面的站点:

img{512x368}

3、创建github page repository

默认情况,github账号xxxx对应的github page repository是xxxx.github.io。于是我在女儿的github账号下创建public repository:baisibei.github.io。

接下来,我们需要将上步中生成的静态页面push到baisibei.github.io这个repository中。在本地进入到hugo_gh_blog/public目录下,执行:

$git init
$git add -A
$git commit -m"initial commit" .
$git remote add origin https//github.com/baisibei.github.io.git
$git push -u origin master

如果你是用自己的github账号替孩子提交,那么请在该repository下设置collaborator。

push一旦成功后,你就可以直接访问:https://xxxx.github.io查看站点页面了。我这里要访问的是baisibei.github.io。

4、样式问题

问题出现了。在本地样式良好的首页,一旦push到github page上,再用浏览器(chrome)打开,我发现样式全部丢失了,首页被render为“全文字”版本。我一开始怀疑css文件路径不对或无法访问到某个css文件,通过“显示网页源代码”,单独试着访问所有css文件,发现这些文件都是可访问的。还有一个现象是:通过mac safari浏览器、手机上的ucweb、微信内置浏览器浏览,均没有样式问题,显示一切正常(Firefox、IE也均有问题)。将hugo_gh_blog放在我的VPS上,并用hugo作为web server,任何浏览器访问都是没有问题的。

针对这个问题,谷歌和度娘了半天,也没有解决掉。于是我有了换工具的想法。在搜索其他工具资料的过程中,我发现了基于hexo的maupassant theme!没错,就是那个和我目前博客同源的主题:maupassant。这个主题采用响应式的设计,对不同屏幕的访问均有很好的适配。

之前我的博客为了适应智能终端的浏览,采用了WPtouch插件,效果差强人意。这次我特地停用了该插件,直接用手机访问我的博客,发现maupassant的显示效果是棒棒的。于是下一步,我将hugo更换为hexo,主题由Lanyon更换为maupassant。

hugo_gh_blog和baisibei.github.io依然保留在github.com上,后者的名字被rename为baisibei.github.io.using.hugo。

三、基于hexo搭建博客站点

1、安装hexo相关工具

第一次用hexo,安装hexo过程需要一些耐心:

$npm install hexo-cli -g
{耐心的等待... ...}

$which hexo
/usr/local/bin/hexo

$hexo -v
hexo-cli: 1.0.2
os: Darwin 13.1.0 darwin x64
http_parser: 2.7.0
node: 6.9.1
v8: 5.1.281.84
uv: 1.9.1
zlib: 1.2.8
ares: 1.10.1-DEV
icu: 57.1
modules: 48
openssl: 1.0.2j

2、创建blog

使用hexo init在本地创建blog repository目录:

$hexo init hexo_gh_blog
{耐心等待...}
... ...
INFO  Start blogging with Hexo!

进入hexo_gh_blog目录:

$cd hexo_gh_blog
$ls
_config.yml    node_modules/    package.json    scaffolds/    source/        themes/

没完,我们还需要install一下相关的依赖:

$npm install
{耐心等待....}

通过”hexo g”命令生成blog文件:

$hexo g
INFO  Start processing
INFO  Files loaded in 270 ms
INFO  Generated: index.html
INFO  Generated: archives/index.html
INFO  Generated: fancybox/blank.gif
INFO  Generated: fancybox/jquery.fancybox.css
INFO  Generated: fancybox/fancybox_sprite.png
INFO  Generated: fancybox/fancybox_loading.gif
INFO  Generated: fancybox/fancybox_overlay.png
INFO  Generated: fancybox/fancybox_loading@2x.gif
INFO  Generated: fancybox/jquery.fancybox.pack.js
INFO  Generated: fancybox/jquery.fancybox.js
INFO  Generated: fancybox/fancybox_sprite@2x.png
INFO  Generated: archives/2016/12/index.html
INFO  Generated: css/fonts/fontawesome-webfont.eot
INFO  Generated: css/fonts/fontawesome-webfont.svg
INFO  Generated: css/style.css
INFO  Generated: css/fonts/fontawesome-webfont.ttf
INFO  Generated: fancybox/helpers/fancybox_buttons.png
INFO  Generated: css/fonts/FontAwesome.otf
INFO  Generated: js/script.js
INFO  Generated: fancybox/helpers/jquery.fancybox-buttons.css
INFO  Generated: archives/2016/index.html
INFO  Generated: css/fonts/fontawesome-webfont.woff
INFO  Generated: fancybox/helpers/jquery.fancybox-media.js
INFO  Generated: fancybox/helpers/jquery.fancybox-buttons.js
INFO  Generated: fancybox/helpers/jquery.fancybox-thumbs.css
INFO  Generated: fancybox/helpers/jquery.fancybox-thumbs.js
INFO  Generated: css/images/banner.jpg
INFO  Generated: 2016/12/16/hello-world/index.html
INFO  28 files generated in 867 ms

$ls
_config.yml    db.json        node_modules/    package.json    public/        scaffolds/    source/        themes/

和hugo命令类似,hexo g也创建了public目录,并将站点的静态文件生成在这个目录下面。

通过hexo s可以启动一个web server,在本地查看生成的静态站点:

$hexo s
INFO  Start processing
INFO  Hexo is running at http://localhost:4000/. Press Ctrl+C to st

hexo自带的landscape theme真的是不咋好看。

3、更换主题为maupassant

先清理一下生成文件,再clone maupassant主题:

$hexo clean
INFO  Deleted database.
INFO  Deleted public folder.

$git clone https://github.com/tufu9441/maupassant-hexo themes/maupassant
Cloning into 'themes/maupassant'...
remote: Counting objects: 1310, done.
remote: Total 1310 (delta 0), reused 0 (delta 0), pack-reused 1309
Receiving objects: 100% (1310/1310), 562.88 KiB | 382.00 KiB/s, done.
Resolving deltas: 100% (747/747), done.
Checking connectivity... done.

$ls themes/
landscape/    maupassant/

编辑hexo_gh_blog/_config.yml文件,修改theme为:maupassant

//_config.xml
theme: landscape

=>

theme: maupassant

重新生成站点静态文件之前,我们还需要安装下面两个工具,否则hexo生成出来的静态页面也是不可用的:

$ npm install hexo-renderer-jade --save
$ npm install hexo-renderer-sass --save

hexo g和hexo s后,你就可以在本地:localhost:4000地址上看到生成的静态页面了:

img{512x368}

仿效上面章节中的步骤,将public目录push到baisibei.github.io repository中,看看我们上传后的站点通过公网访问是否还有“失真”现象,结果:一切正常。

4、定制站点

a) 定制hexo_gh_blog/_config.yml

这个_config.xml中的配置都是站点全局范畴的,这里仅我将我修改过的一些定制属性贴出来:

# Site
title: Amy Bai
subtitle: 果果的成长历程
description: 记录一个小女孩儿在学习、生活、家庭、情感方面的成长经历
author: Amy Bai
language: zh-CN
timezone: Asia/Shanghai
since: 2016
avatar: https://avatars0.githubusercontent.com/u/24524343?v=3&s=400

# URL
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: http://daughter.tonybai.com

默认情况下,maupassant主题的menu中包含rss菜单项(站点的订阅feed),对应的访问路径是/atom.xml,但要生成atom.xml,需要安装另外两个plugins:hexo-generator-feed和hexo-generator-sitemap:

在_config.xml中添加:

plugins:
- hexo-generator-feed
- hexo-generator-sitemap

安装这两个插件;

$npm install hexo-generator-feed --save
hexo-site@0.0.0 /Users/tony/GuoGuoblog/hexo_gh_blog
└── hexo-generator-feed@1.2.0

$npm install hexo-generator-sitemap --save
hexo-site@0.0.0 /Users/tony/GuoGuoblog/hexo_gh_blog
└── hexo-generator-sitemap@1.1.2

安装后,执行hexo g,会看到atom.xml的生成。不过由于hexo版本似乎与feed插件有兼容性问题,当执行hexo s时,命令报错。我暂时在_config.xml中先注释掉这两个插件,待后期看是否能解决,不过这不影响站点的主要功能。

b) about页面

在maupassant主题的menu中默认还包含了about菜单项,但在生成的站点静态页面中点击about菜单项,将返回失败页面。如何给站点添加about页面呢?

在hexo_gh_blog/source下创建about目录,进入about目录,创建index.md文件,内容诸如:

---
title: 关于我
---
我是Amy Bai,小名果果。

这样hexo g和hexo s后,你就有about页面可供访问了。

5、写post

hexo通过hexo new 命令来创建一篇post,我更喜欢简单粗暴,直接再hexo_gh_blog/source/_post创建一个xxx.md文件,这就是一篇post。post内的markdown格式和很多工具都是类似的:

以initial-post.md为例:
---
title: "第一篇(待写)"
date: "2016-12-15"
description: "第一篇博文,敬请期待^O^"
categories:
    - "日记"
    - "感悟"
---

## 标题一

第一篇文章,敬请期待

### 子标题

## 小结

^O^

hexo按md文件头中的date对post进行排序。title就是显示在文章中的标题。description是文章摘要。默认情况下,maupassant主题在首页只是展示文章摘要而不是全文。

四、域名绑定

还没有申请顶级域名下的二级域名,目前打算绑定daughter.tonybai.com这个子域名。怎么做呢?

在public目录下,创建CNAME文件,文件内容:daughter.tonybai.com。然后将文件Push到github上去。

在你的域名管理站点,创建”daughter.tonybai.com”子域名,并将其CNAME值设置为”baisibei.github.io”。生效后,打开浏览器,访问”daughter.tonybai.com”,你就可以看到你刚刚生成的新站点了。

五、小结

站点搭建好了!用各种终端访问,感觉效果还不错。post发布也很方便,如果你想自动发布,定义一下hexo deploy即可。我个人习惯手动提交,也就没这个步骤了。

接下来,把内容创作的任务就交给果果了^_^。

Hello, Cocos2d-x

女儿从两岁半开始接触iPad,在这个年龄段也只有一些幼教类游戏适合她玩。虽然知道iPad玩久了对视力有伤害,但有时候还真拗不过果果,索性 也就让她玩一会儿。之前对智能终端上的东西不是很在意,也没啥兴趣,这大概与当年在大学时做Win32 GUI开发的糟糕经历多多少少有点关系。不过智能终端是大势所趋,历史的潮流不能违抗。虽然自己并非以Android/iOS编程为主业,但适当学习学习 总归没有坏处,万一作出一个像"Flappy Bird"的游戏,爆发一下,还是蛮Happy的。于是在开始学习实践之前给自己定了一个小目标:今年六一儿童节送给女儿一款自己制作的小游戏。

智能终端上的游戏目前风头正劲,试问哪个智能手机上没有几款企鹅公司出品的游戏呢!之前从未涉猎过游戏开发,但知道游戏开发前要挑选一款合适的游 戏引擎,自己从头开始敲代码的时代已经out了。在寻觅游戏引擎之前,我需要回答三道摆在我面前的选择题:

    1、2D引擎还是3D引擎?
    2、平台专用引擎还是跨平台引擎?
    3、收费引擎还是开源引擎?

作为入门级选手,2D游戏显然更适合上手一些,另外适合果果这个年龄段的幼教类的游戏也多以2D游戏居多。3D游戏本身也太难了,不仅要 Programming能力,还要3D建模能力,这些学习起来周期就太长了;一直是Ubuntu Fans,手头没有Mac Book,这样开发iOS程序变成一件糟心的事,在Ubuntu下搭建iOS App开发环境繁杂的很,即便是虚拟机也懒得尝试。但从游戏体验来看,还是在iPad上玩更好一些,因此最好引擎能跨平台,以便后续迁移到iOS上;开源 和用开源惯了,收费的引擎目前不在考虑范围之内。综上,我要寻找的是一款开源的、跨平台的Mobile 2D Game Engine。

于是我找到了Cocos2d-x!Cocos2d-x是Cocos2d-iphone的C++跨平台分支,由于是国人创立的,在国内有着较大的用 户群,引擎资料也较多,社区十分活跃。国内已经出版了多本有关Cocos2d-x的中文书籍,比如《Cocos2d-x高级开发教程:制作自己的 “捕鱼达人”》 、《Cocos2d-x权威指南》 等都还不错。更重要的是Cocos2d-x自带了丰富的例子,供初学者“临摹学习”,其中cocos2d-x-2.2.2/samples/Cpp /TestCpp这个例子几乎涵盖了该引擎的绝大多数功能。下面就开启Cocos2d-x的入门之旅(For Android)。

一、引擎安装

试验环境:
   Ubuntu 12.04.1 x86_64
   gcc 4.6.3
   javac 1.7.0_21
   java "1.7.0_21" HotSpot 64-bit Server VM
   adt-bundle-linux-x86_64-20131030.zip
   android-ndk-r9d-linux-x86_64.tar.bz2

Cocos2d-x官网目前提供2.2.2稳定版以及3.0beta2版的下载(当然你也可以下载到更老的版本)。由于3.0改变较大,资料不 多,且对编译器等版本的要求较高(需要支持C++ 11标准),因此这里依旧以2.2.2版本作为学习目标。Cocos2d-x-2.2.2下载后解压到某个目录:比如/home1/tonybai/android-dev/cocos2d-x-2.2.2。 如果仅是用Cocos2d-x开发Android版本游戏,则不需要做什么编译工作。Android Game Project会在Project build时自动用NDK的编译器编译C++代码,并与NDK链接。如果你想早点看看Cocos2d-x sample中的例子运行起来到底是什么样子的,你可以在Ubuntu下编译出Linux版本的游戏:在cocos2d-x-2.2.2下执行make-all-linux-project.sh即可。编译需要一段时间,编译成 功后,我们可以进入到“cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.linux/bin/release” 下执行“HelloCpp”这个可执行文件,一个最简单的Cocos2d-x游戏就会展现在你的面前了。

Android sample project的构建稍微复杂些:

首先在Eclipse中添加libcocos2dx Library project from existed code(注意:不Copy到workspace,原地建立)。该Project的代码路径为cocos2d-x-2.2.2/cocos2dx/platform /android/java。在project.properties和AndroidManifest.xml适当修改你所使用的api版本, 以让编译通过。我这里用的是 target=android-19。

然后,设置NDK_ROOT环境变量(比如export NDK_ROOT='/home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c'), 供build_native.sh使用。

最后添加游戏project。在Eclipse中添加HelloCpp project from existed code,位置cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android(注 意:不Copy到Workspace中,原地建立)。在HelloCpp的project.properties中添加“android.library.reference.1=../../../../cocos2dx/platform/android /java”。同样别忘了在project.properties和AndroidManifest.xml适当修改你所使用 的api版本,以让编译通过。

如果一切顺利的话,你会在Console窗口看到“**** Build Finished ****”。Problems窗口显示“0 errors“。 启动Android模拟器,Run Application,同样的HelloCpp画面会呈现在模拟器上。

Cocos2d-x是建构在OpenGL技术之上的。对于Android平台而言,Android SDK已经完全封装了opengl es 1.1/2.0的API(android.opengl.*;javax.microedition.khronos.egl.*;javax.microedition.khronos.opengles.*), 引擎完全可以建立在这个之上,无需C++代码。但Cocos2d-x是一个跨平台的2D游戏引擎,核心选择了用C++代码实现(iOS提供的C绑 定,不提供Java绑定;Android则提供了Java和C绑定),因此 在开发Android平台的2D游戏时,引擎部分是SDK与NDK交相互应,比如GLThread的创建和管理用的是SDK的 GLSurfaceView和GLThread,但真正的Surface绘制部分则是回调Cocos2d-x用C++编写的绘制实现(链接NDK 中的库)。

二、Cocos2d-x Android工程代码组织结构

以samples/Cpp/HelloApp的Android工程为例,Android版的Cocos2d-x工程与普通android应用程序 差别 不大,核心部分只是多了一个jni目录和一个build_native.sh脚本文件。其中jni目录下存放的是Java和C++调用转换的“胶 水”代码;build_native.sh则是用于编译jni下C++代码以及 cocos2dx_static library代码的构建脚本。

HelloCpp的构建过程摘要如下:

**** Build of configuration Default for project HelloCpp ****

bash /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/build_native.sh
NDK_ROOT = /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c
COCOS2DX_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../..
APP_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/..
APP_ANDROID_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android
+ /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c/ndk-build -C /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.androidNDK_MODULE_PATH=/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../..:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../../cocos2dx/platform/third_party/android/prebuilt
Using prebuilt externals
Android NDK: WARNING:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../../cocos2dx/Android.mk:cocos2dx_static: LOCAL_LDLIBS is always ignored for static libraries  
make: Entering directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android'
[armeabi] Compile++ thumb: hellocpp_shared <= main.cpp
[armeabi] Compile++ thumb: hellocpp_shared <= AppDelegate.cpp
[armeabi] Compile++ thumb: hellocpp_shared <= HelloWorldScene.cpp
[armeabi] Compile++ thumb: cocos2dx_static <= CCConfiguration.cpp
[armeabi] Compile++ thumb: cocos2dx_static <= CCScheduler.cpp
 … …
[armeabi] Compile++ thumb: cocos2dx_static <= CCTouch.cpp
[armeabi] StaticLibrary  : libcocos2d.a
[armeabi] Compile thumb  : cpufeatures <= cpu-features.c
[armeabi] StaticLibrary  : libcpufeatures.a
[armeabi] SharedLibrary  : libhellocpp.so
[armeabi] Install        : libhellocpp.so => libs/armeabi/libhellocpp.so
make: Leaving directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android'

**** Build Finished ****

指挥NDK编译的则是jni下的Android.mk文件,其角色类似于Makefile。

三、Cocos2d-x Android工程代码阅读

单独将如何阅读代码拿出来,是为了后面分析引擎的驱动流程做准备工作。学习类似Cocos2d-x这样的游戏引擎,仅仅停留在游戏逻辑层代码是不 能很好的把握引擎本质的,因此适当的挖掘引擎实现实际上对于理解和使用 引擎都是大有裨益的。

以一个Cocos2d-x Android工程为例,它的游戏逻辑代码以及涉及的引擎代码涵盖在一下路径下(还是以HelloCpp的Android工程为例):

    项目层:
        * cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/src  主Activity的实现;
        * cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/jni/hellocpp  Cocos2dxRenderer类的nativeInit实现,用于引出Application的入口;
        * cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes 你的游戏逻辑,以C++代码形式呈现;
   
    引擎层:
        * cocos2d-x-2.2.2/cocos2dx/platform/android/java/src 引擎层对Android Activity、GLSurfaceView以及Render的封装
        * cocos2d-x-2.2.2/cocos2dx/platform/android/jni 对应上面封装的native method实现
        * cocos2d-x-2.2.2/cocos2dx、cocos2d-x-2.2.2/cocos2dx/platform、cocos2d-x- 2.2.2/cocos2dx/platform/android   cocos2dx引擎的核心实现(针对android平台)

后续的代码分析也将从这两个层次、六处位置出发。

四、从Activity开始

之前多少了解了一些Android App开发的知识,Android App都是始于Activity的。游戏也是App的一种,因此在Android平台上,Cocos2d-x游戏也是从Activity开始的。于是 Activity,确切的说是Cocos2dxActivity是我们这次引擎驱动机制分析的出发点。

回顾Android Activity的Lifecycle,Activity启动的顺序是:Activity.onCreate -> Activity.onStart() -> Activity.onResume()。接下来我们将按照 这条主线进行引擎驱动机制的分析。

HelloCpp.java中的HelloCpp这个Activity完全无所作为,仅仅是继承其父类Cocos2dxActivity的实现罢 了。

// HelloCpp.java
public class HelloCpp extends Cocos2dxActivity{
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
    }
    … …
}

我们来看Cocos2dxActivity类。

// Cocos2dxActivity.java

@Override
protected void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    sContext = this;
    this.mHandler = new Cocos2dxHandler(this);
    this.init();
    Cocos2dxHelper.init(this, this);

public void init() {
        // FrameLayout
        ViewGroup.LayoutParams framelayout_params =
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                                       ViewGroup.LayoutParams.FILL_PARENT);
        FrameLayout framelayout = new FrameLayout(this);
        framelayout.setLayoutParams(framelayout_params);

        … …
        // Cocos2dxGLSurfaceView
        this.mGLSurfaceView = this.onCreateView();

        // …add to FrameLayout
        framelayout.addView(this.mGLSurfaceView);
        … …
        this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
        … …

        // Set framelayout as the content view
        setContentView(framelayout);
}

从上面代码可以看出,onCreate调用的init方法才是Cocos2dxActivity初始化的核心。在init方法 中,Cocos2dxActivity创建了一个Framelayout实例,并将该实例作为content View赋给了Cocos2dxActivity的实例。Framelayout实例也并不孤单,一个设置了Cocos2dxRenderer实例的 GLSurfaceView被Added to it。而Cocos2d-x引擎的初始化已经悄悄地在这几行代码间完成了,至于初始化的细节我们后续再做分析。

接下来是onResume方法,它的实现如下:

    @Override
    protected void onResume() {
        super.onResume();

        Cocos2dxHelper.onResume();
        this.mGLSurfaceView.onResume();
    }

onResume调用了View的onResume()。

// Cocos2dxGLSurfaceView:
    @Override
    public void onResume() {
        super.onResume();

        this.queueEvent(new Runnable() {
            @Override
            public void run() {
                Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnResume();
            }
        });
    }

Cocos2dxGLSurfaceView将该事件打包放到队列里,扔给了另外一个线程去执行(后续会详细说明这个线程),对应的方法在 Cocos2dxRenderer class中。

    public void handleOnResume() {
        Cocos2dxRenderer.nativeOnResume();
    }

Render实际上调用的是native方法。

    JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnResume() {
        if (CCDirector::sharedDirector()->getOpenGLView()) {
            CCApplication::sharedApplication()->applicationWillEnterForeground();
        }
    }

applicationWillEnterForeground方法在你的AppDelegate.cpp中;

void AppDelegate::applicationWillEnterForeground() {
    CCDirector::sharedDirector()->startAnimation();//

    // if you use SimpleAudioEngine, it must resume here
    // SimpleAudioEngine::sharedEngine()->resumeBackgroundMusic();
}

这里仅是重新获得了一下时间罢了。

五、Render Thread(渲染线程) - GLThread

游戏引擎要兼顾UI事件和屏幕帧刷新。Android的OpenGL应用采用了UI线程(Main Thread) +  渲染线程(Render Thread)的模式。Activity活在Main Thread(主线程)中,也叫做UI线程。该线程负责捕获与用户交互的信息和事件,并与渲染(Render)线程交互。比如当用户接听电话、切换到其他 程序时,渲染线程必须知道发生了 这些事件,并作出即时的处理,而这些事件及处理方式都是由主线程中的Activity以及其装载的View传递给渲染线程的。我们在Cocos2dx的框 架代码中看不到渲染线程的诞生过程,这是因为这一过程是在Android SDK层实现的。

我们回顾一下Cocos2dxActivity.init方法的关键代码:

    // Cocos2dxGLSurfaceView
    this.mGLSurfaceView = this.onCreateView();

    // …add to FrameLayout
    framelayout.addView(this.mGLSurfaceView);
    this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
       
    // Set framelayout as the content view
    setContentView(framelayout);

Cocos2dxGLSurfaceView是 android.opengl.GLSurfaceView的子类。在android 上做原生opengl es 2.0编程的人应该都清楚GLSurfaceView的重要性。但渲染线程并非是在Cocos2dxGLSurfaceView实例化时被创建的,而是在 setRenderer的时候。

我们来看Cocos2dxGLSurfaceView.setCocos2dxRenderer的实现:

    public void setCocos2dxRenderer(final Cocos2dxRenderer renderer) {
        this.mCocos2dxRenderer = renderer;
        this.setRenderer(this.mCocos2dxRenderer);
    }

setRender是Cocos2dxGLSurfaceView父类GLSurfaceView实现的方法。在Android SDK GLSurfaceView.java文件中,我们看到:

       public void setRenderer(Renderer renderer) {
        checkRenderThreadState();
        if (mEGLConfigChooser == null) {
            mEGLConfigChooser = new SimpleEGLConfigChooser(true);
        }
        if (mEGLContextFactory == null) {
            mEGLContextFactory = new DefaultContextFactory();
        }
        if (mEGLWindowSurfaceFactory == null) {
            mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
        }
        mRenderer = renderer;
        mGLThread = new GLThread(mThisWeakRef);
        mGLThread.start();

    }

GLThread的实例是在这里被创建并开始执行的。至于渲染线程都干了些什么,我们可以通过其run方法看到:

        @Override
        public void run() {
            setName("GLThread " + getId());
            if (LOG_THREADS) {
                Log.i("GLThread", "starting tid=" + getId());
            }

            try {
                guardedRun();
            } catch (InterruptedException e) {
                // fall thru and exit normally
            } finally {
                sGLThreadManager.threadExiting(this);
            }
        }

run方法并没有给我们带来太多有价值的东西,真正有价值的信息藏在guardedRun方法中。guardedRun是这个源文件中规模最为庞 大的方法,但抽取其核心结构后,我们发现它大致就是一个死循环,以下是摘要式的伪代码:

while (true) {
   synchronized (sGLThreadManager) {
       while (true) {
           …. …
           if (! mEventQueue.isEmpty()) {
               event = mEventQueue.remove(0);
               break;
           }
        }  
   }//end of synchronized (sGLThreadManager)

    if (event != null) {
       event.run();
       event = null;
       continue;
   }  

   if needed
       view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);

   if needed
       view.mRenderer.onSurfaceChanged(gl, w, h);

   if needed
       view.mRenderer.onDrawFrame(gl);
}

在这里我们看到了event、Renderer的三个回调方法onSurfaceCreated、onSurfaceChanged以及 onDrawFrame,后续我们会对这三个函数做详细分析的。

六、游戏逻辑的入口

在HelloCpp的Classes下有好多C++代码文件(涉及具体的游戏逻辑),在HelloCpp的android project jni目录下也有Jni胶水代码,那么这些代码是如何和引擎一起互动生效的呢?

上面讲到过,涉及到画面的一些渲染都是在GLThread中进行的,这涉及到onSurfaceCreated、 onSurfaceChanged以及onDrawFrame三个方法。我们看看 Cocos2dxRenderer.onSurfaceCreated方法的实现,该方法会在Surface被首次渲染时调用:

    public void onSurfaceCreated(final GL10 pGL10, final EGLConfig pEGLConfig) {
        Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
        this.mLastTickInNanoSeconds = System.nanoTime();
    }

该方法继续调用HelloCpp工程jni目录下的nativeInit代码:

void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    else
    {
        ccGLInvalidateStateCache();
        CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
        ccDrawInit();
        CCTextureCache::reloadAllTextures();
        CCNotificationCenter::sharedNotificationCenter()->postNotification(EVENT_COME_TO_FOREGROUND, NULL);
        CCDirector::sharedDirector()->setGLDefaultValues();
    }
}

这似乎让我们看到了游戏逻辑的入口了:

    CCEGLView *view = CCEGLView::sharedOpenGLView();
    view->setFrameSize(w, h);

    AppDelegate *pAppDelegate = new AppDelegate();
    CCApplication::sharedApplication()->run();

继续追踪CCApplication::run方法:

int CCApplication::run()
{
    // Initialize instance and cocos2d.
    if (! applicationDidFinishLaunching())
    {
        return 0;
    }

    return -1;
}

applicationDidFinishLaunching,没错这就是游戏逻辑的入口了。我们得回到Samples代码目录中去找到对应方法 的实现。

//cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes/AppDelegate.cpp

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    CCDirector* pDirector = CCDirector::sharedDirector();
    CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();

    pDirector->setOpenGLView(pEGLView);
    CCSize frameSize = pEGLView->getFrameSize();
    … …

    // turn on display FPS
    pDirector->setDisplayStats(true);

    // set FPS. the default value is 1.0/60 if you don't call this
    pDirector->setAnimationInterval(1.0 / 60);

    // create a scene. it's an autorelease object
    CCScene *pScene = HelloWorld::scene();

    // run
    pDirector->runWithScene(pScene);

    return true;
}

的确,在applicationDidFinishLaunching中我们做了很多引擎参 数的设置。接下来大管家CCDirector实例登场,并运行了HelloWorld Scene的实例。但这依旧是初始化的一部分,虽然方法名让人听起来像是某种持续连贯行为:

//cocos2d-x-2.2.2/cocos2dx/CCDirector.cpp

void CCDirector::runWithScene(CCScene *pScene)
{
    … …

    pushScene(pScene);
    startAnimation();
}

void CCDisplayLinkDirector::startAnimation(void)
{
    if (CCTime::gettimeofdayCocos2d(m_pLastUpdate, NULL) != 0)
    {
        CCLOG("cocos2d: DisplayLinkDirector: Error on gettimeofday");
    }

    m_bInvalid = false;
}

两个方法均只是初始化了某些数据成员变量,并未真正将引擎驱动起来。

七、驱动引擎

之所以游戏画面是运动的,那是因为屏幕以较高的帧数刷新的缘故,这样人眼就会看到连续的动作,就和电影的放映原理是一样的。在Cocos2d-x 引擎中这些驱动屏幕刷新的代码在哪里呢?

我们回顾一下之前谈到的GLThread线程,我们说过画面渲染的工作都是由它来完成的。GLThread的核心是guardedRun函数,该 函数以“死循环”的方式调用Cocos2dxRender.onDrawFrame方法对画面进行持续渲染。

我们来看看引擎实现的Cocos2dxRender.onDrawFrame方法:

public void onDrawFrame(final GL10 gl) {
        /*
         * FPS controlling algorithm is not accurate, and it will slow down FPS
         * on some devices. So comment FPS controlling code.
         */

        /*
        final long nowInNanoSeconds = System.nanoTime();
        final long interval = nowInNanoSeconds – this.mLastTickInNanoSeconds;
        */

        // should render a frame when onDrawFrame() is called or there is a
        // "ghost"
        Cocos2dxRenderer.nativeRender();

        /*
        // fps controlling
        if (interval < Cocos2dxRenderer.sAnimationInterval) {
            try {
                // because we render it before, so we should sleep twice time interval
                Thread.sleep((Cocos2dxRenderer.sAnimationInterval – interval) / Cocos2dxRenderer.NANOSECONDSPERMICROSECOND);
            } catch (final Exception e) {
            }
        }

        this.mLastTickInNanoSeconds = nowInNanoSeconds;
        */
    }

这个方法实现得比较奇怪,似乎修改过多次,但最后还是决定只保留了一个方法调用: Cocos2dxRenderer.nativeRender()。从注释掉的代码来看,似乎是想在这个方法中通过Thread.sleep来控制 Render Thread渲染的帧率。但由于控制的不理想,索性就不控制了,让guardedRun真正变成了dead loop。但从HelloCpp Sample运行时的状态显示,画面始终保持在60帧左右,让人十分诧异。据说Cocos2d-x 3.0版本重新设计了渲染这块的机制。(后记:在Android上虽然没有帧数控制,但真正的渲染帧率实际上还受到"垂直同步"信号 – vertical sync的影响。在游戏中,也许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等vsync信号到达,才可以绘制。这样fps实际上要要受到操作系统刷新率值的制约)。

nativeRender从命名来看,这显然是一个C++编写的函数实现。我们只能到jni目录下寻找。

cocos2d-x-2.2.2/cocos2dx/platform/android/jni/ Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp

    JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
        cocos2d::CCDirector::sharedDirector()->mainLoop();
    }

nativeRender也很简洁,直接调用了CCDirector的mainLoop,也就是说每帧渲染过程中真正干活地是 CCDirector::mainLoop。到此我们终于找到了引擎渲染的驱动器:GLThead::guardedRun,以“死循环”的方式刷新着画面,让我们感受到“动”的魅力。

八、mainLoop

进一步我们来看看mainLoop所做的工作。mainLoop是CCDirector类的一个纯虚函数,CCDirector的子类CCDisplayLinkDirector真正实现了 它:

//CCDirector.cpp
void CCDisplayLinkDirector::mainLoop(void)
{
    if (m_bPurgeDirecotorInNextLoop)
    {
        m_bPurgeDirecotorInNextLoop = false;
        purgeDirector();
    }
    else if (! m_bInvalid)
     {
         drawScene();

         // release the objects
         CCPoolManager::sharedPoolManager()->pop();
     }
}

void CCDirector::drawScene(void)
{
    // calculate "global" dt
    calculateDeltaTime();

    //tick before glClear: issue #533
    if (! m_bPaused)
    {
        m_pScheduler->update(m_fDeltaTime);
    }

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    /* to avoid flickr, nextScene MUST be here: after tick and before draw.
     XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
    if (m_pNextScene)
    {
        setNextScene();
    }

    kmGLPushMatrix();

    // draw the scene
    if (m_pRunningScene)
    {
        m_pRunningScene->visit();
    }

    // draw the notifications node
    if (m_pNotificationNode)
    {
        m_pNotificationNode->visit();
    }

    if (m_bDisplayStats)
    {
        showStats();
    }

    kmGLPopMatrix();

    m_uTotalFrames++;

    // swap buffers
    if (m_pobOpenGLView)
    {
        m_pobOpenGLView->swapBuffers();
    }

    if (m_bDisplayStats)
    {
        calculateMPF();
    }
}

帧渲染由mainLoop调用的drawScene()完成,drawScene方法根据Scene下的渲染树,根据node的最新属性逐个渲染 node,并调整各个Node的调度定时器数据,细节这里就不详细说明了。

九、UI线程与GLThread的交互

用户的屏幕触控动作由UI线程捕捉到,该类事件需要传递给引擎,并由GLThread根据各个画面元素的最新状态重新绘制画面。UI线程负责处理用户交互 事件,并将特定的事件通知GLThread处理。UI线程通过Cocos2dxGLSurfaceView的queueEvent方法,将事件以及处理方 法传递给GLThread执行的。

Cocos2dxGLSurfaceView的queueEvent方法继承自其父类GLSurfaceView:

    public void queueEvent(Runnable r) {
        mGLThread.queueEvent(r);
    }

而GLThread的queueEvent方法实现如下:

public void queueEvent(Runnable r) {
    if (r == null) {
        throw new IllegalArgumentException("r must not be null");
    }  
    synchronized(sGLThreadManager) {
        mEventQueue.add(r);
        sGLThreadManager.notifyAll();

    }  
}

该方法将event互斥地放入EventQueue,并通知阻塞在Queue上的线程取货。

运行着的GLThread实例在guardedRun中会从event队列中取出runnable event并run的。
  
while (true) {
    synchronized (sGLThreadManager) {
        while (true) {
            if (mShouldExit) {
                return;
            }  

            if (! mEventQueue.isEmpty()) {
                event = mEventQueue.remove(0);
                break;
            }  
         …….
        }  
     }  

     … …
     if (event != null) {
        event.run();
        event = null;
        continue;
    }  
    …
}

Activity的各种事件Pause、Resume、Stop以及View的各种屏幕触控事件都是通过queueEvent传递给GLThread执行的,比如:View的onKeyDown方法:

    //Cocos2dxGLSurfaceView.java
    @Override
    public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
        switch (pKeyCode) {
            case KeyEvent.KEYCODE_BACK:
            case KeyEvent.KEYCODE_MENU:
                this.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);
                    }
                });
                return true;
            default:
                return super.onKeyDown(pKeyCode, pKeyEvent);
        }
    }

十、小结

有了以上的对Cocos2d-x引擎的理解后,再编写游戏代码就更加游刃有余了,至少出现问题时,我们知道应该在哪里查找了。就像对汽车的发动机了如指掌 后,一旦发生动力故障,我们基本知道排除的方法。但对发动机了解的再透彻,也不能代表就能设计和生产出好车,游戏也是这样,对引擎了解是一码事,设计和实 现出好游戏是另外一码事。学习引擎只是编写游戏的起点而已。

2013小结

2013年的个人年终总结比以往来得晚了一些,至于原因,我也说不清楚,拖延症也罢,其他原因也罢,总之是晚了。

写年终小结已经有小几年了,风格一直如一,无非是老三样:工作得失、生活酸甜以及新年展望,今年也不利外。

* 工作篇

我们部门在所在行业里已经摸爬滚打了10多年了,经 历和见证了这个行业从诞生、增长、成熟到如今的衰退的整个过程。也正是由于处于行业的衰退期,2013年部门的运营十分艰难。十年对于任何一个行业来说, 可能都已经过了其巅峰期,真心不能再期望这个行业还能会有下一个高峰了,对于个人来说也是如此。转型、业务突破变成了领导常挂在嘴边的词汇,但做起来又何 其艰难。

2013年,我们的业务转型依旧是围绕着我们的“金主”,虽然他们的业务营收也受到了微信等OTT业务的极大影响,传统业务投资也在缩减。对于个人而言, 除了负责传统产品线,转型、业务突破也成了我的绩效目标的一部分。于是在2013年,写文档比写代码多了一点,出差比常年多了一点,周六周日的连续加班也 多出了一点。在这些尝试中,以5月份某运营商某信的重构项目最为让人印象深刻。为了这个合同额几个亿的项目,我们近30人连续奋战了一个多月编写技术建议 书和投标方案,过程辛苦但却颇感充实,最终我们拿到了两个第二、两个第三的成绩。也许这个结果对于公司来说算是一种失败,但对于我个人来说,我获得了些许 转型的信心,以至于在后续的几次投标资料编写过程中,面对较新的领域,我也可以镇定自若。

掐指算来,这一年我以咨询顾问的临时角色参与了8个大大小小项目的前期交流以及投标支持工作,其中六个标以失败或不了了之而告终,还有两个标尚未有最终结 果。对于这样的结果,我也只能表示无奈。虽然我心里也十分清楚,对于国内这类解决方案项目的投标,技术往往不是最重要的,况且对于这些新领域,我们的技术 储备还不够系统,积累较为浅薄,落地的也的确较少。但面对这样的局面,我们还能怎么做呢?我也期待新一年能得到一个新的答案。

当然2013的工作中不全是遗憾,年末之前新系统的上线算是为我的2013划上了一个还算不错的句号,毕竟这是我两年来为之付出最多,也是最重要的一个工作目标。另外2013年继续整理和总结自己的一些管理经验工作原则。在过程方面继续深入改善,尤其在代码质量方面。

在技术精深方面,今年没有太多进步。年初的时候曾探讨过如何在现有项目中使用一些成熟的开源技术和产品,比如memcachedzookeeper等, 为了保持手热,还尝试做些算法类的编码,这个在experiments库中有体现。在其他方面,可谓是“三无状态”:无技术书籍翻译、无技术杂志投稿、无 新开源项目发起。另外今年没有尝试去学习什么新语言,理由在此

在年末的绩效评审时,观察到一些现象:那些绩效最末尾的人,往往并非是自身不够努力,而是领导赋予的目标不明确,这会给下属带来更多的不安,多数下属也会因为工作目标的不明确,而表现出更为糟糕的绩效。

* 生活篇

我个人十分注重工作和生活的平衡,不知道这种理念对于一个革命尚未成功的人来说算不算正确。

今年写了56篇博文,只完成了计划值的3/4,算是可接受范围,博文质量有所提升,访问次数和评论反馈也多了许多。文章以技术理解偏多,深入的偏少。技术攻关还是留给年轻人去吧。另外就是经验总结和感悟偏多,这也许与工作年头多有关系吧。

读书方面,据豆瓣不完全统计一共读了61本,这照比去年要多出不少,想必是有了Kindle PaperWhite的缘故吧,使得碎片时间得到充分利用。技术、商业书籍依旧占较大比例,小说尤其是科幻小说也不少。同样是因为电子书,今年纸质书籍购 买减少了(痛定思痛后的决定),双十一、双十二以及圣诞促销均没有出手。不过豆瓣上想读的书单依旧还有上百本^_^,任重道远啊。

今年爱上了跑步,坚持到11月末,因出差和天气转深寒等原因,决定暂停一段时间,等春节后气温回升时再拾起这个好习惯,相信不是大问题。跑步的确让我的身体状况大为好转,至少感冒次数大为下降。                 

今年的一些家庭目标也多已实现,比如和老婆一起去香港、带孩子去海边玩等。数码装备也更新了一圈。

果果这个小家伙那叫一个茁壮成长啊。年中给她换了一个大的幼儿园,她也变得十分喜欢和小朋友在一起玩了,有时候还觉得在家里没有意思。每周果果还要上一节 她最喜欢的舞蹈课,我们的初衷就是让她多与小朋友老师接触,也不指望她能学出什么样子来,不过她学得倒是有模有样,十分认真。现在的果果简直就是一个小大 人,每天从早到晚说个不停,精力那叫一个充沛,有时候不得不强迫她去睡觉^_^。

* 新年展望

感觉这一年的进步有些差强人意,心底真心感觉自己的努力还是太少了,于是立下了“少睡觉,多干活”的目标。

新的一年,无论是个人还是工作,都要更多的思考如何将知识、技能和经验转化为更多价值,如何将业务经验、技术积累转化为合同。

新的一年,要主动适应转型,无论是工作上的还是个人方向上的,争取在这一年里能找到正确的方向,并成功入门。最好给自己做一个三年到五年的布局。

新的一年,尝试继续保持生活与工作的平衡,也许这将变成一种奢侈的期望。

新的一年,还有什么比全家健康快乐更重要的呢。




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多