ZFFramework 中用到的一些技巧收集

By | 2016-06-01

ZFFramework 有好长一段时间了, 期间积累了不少经验,
姑且留个文做个记录

设计相关

跨平台的实现

采用 core + protocol [+ implementation] 的设计, 不同于大多数跨平台框架, ZF 的 implementation 层都是动态注册的

优点:

  • implementation 只在对应模块被使用到时是必须的, 可以方便的做模块化和功能裁减
  • 有利于将平台实现层拆分出去, 要知道 C艹 世界里面折腾环境和编译选项可是很麻烦的事情,
    所以尽量避免这种麻烦事污染到核心代码所属的项目

缺点:

  • 虽然大多数场景下可以忽略, 不过多少还是会有些性能问题, 遵循二八原则, 总会有那么些东西需要为了性能而放弃良好的设计

动态注册

ZF 里面很多子功能都采用动态注册的设计, 例如:

  • 指定类型的序列化和反序列化的注册, 动态注册每个类型
  • file descriptor, 从纯字符串根据指定规则解析出数据, 动态注册每种解析规则
  • object creator, 从指定规则创建对象, 动态注册每种创建规则
  • scroll view 的滚动逻辑和滚动条, 可以替换为不同的实现

动态注册讲白了也是为了实现模块化和可扩展性, 这部分其实没什么技术含量所以不啰嗦了

观察者模式

这个很 low 的设计其实是大多数场景下最好用的, 本来不想单独列出来,
但是发现最近 Android SDK 把不少 setListener 接口更新为 addListener 就让不少人兴奋不已,
觉得还是有必要列一下

最典型的反例其实就是很多人用的最多的, 比如 Android 里面 setOnClickListener 和 iOS 里面的 delegate,
这种只能注册一个的设计非常不利于模块化和功能扩展

作为最简单易行却最实用的设计模式, 要做好这个其实很简单, 首先好好设计 "事件", 首先好好设计 "事件", 首先好好设计 "事件",
然后注意监听要用 add/remove 而不是 set 就好了

object tag

又是一个很 low 但很好用的设计, 在单根继承体系下非常实用,
简单讲就是这样:

class Object
{
    map<string, Object> tags;
};

这种设计能够很方便的对已有代码进行扩展, 新增功能的额外状态可以保存在 tag 中, 而不需要影响原有的任何代码

其实更进一步的是 Object-C 里面语法级别提供的功能 "category", 这个真是一个非常非常好的设计, 可惜 C艹 无法优雅的实现

分层 view

这个也是为了实现扩展性而引入的一个设计, 简单的说就是每个 view 可以分层插入子元素, 各个层之间互不影响, 伪代码差不多是这个样子:

class View
{
    vector<View> bgViews;
    vector<View> views;
    vector<View> fgViews;
};

列举几个典型的使用场景, 看看各位在自己熟悉的框架下要如何优雅的实现:

  • 给已有的按钮类添加一个右上角 "new" 的小图标
  • 某个 view 被点击时, 自动闪烁一下

静态反射 vs 动态反射

C艹 其实已有不少反射库, 不过由于 C艹 的模板的特殊性, 大约可以分为两种

  • 静态反射

    典型为 boost, 利用 template traits 和 bind 等技术, 直接访问原始数据

    优点是性能非常好, 缺点是不够灵活, 大多需要显式的通过特化的模板来访问,
    用字符串反射等方式用起来就比较无力

    典型适用场景为网络通讯或计算密集型业务, 一是需要性能, 二是业务结构相对确定而且结构清晰

  • 动态反射

    典型为 Qt, 利用各种方式注册 meta data, 然后通过字符串查找来找到地址或回调函数

    优点是非常灵活, 可以动态注册, 可以不需要知道实际类型 (多少有点类似多态),
    缺点是性能较差, 而且 "注册" 本身也挺麻烦的, 像 Qt 需要 moc, MFC 需要各种难看的宏

    典型适用场景为 UI 模块或上层业务, 因为这些玩意儿大多非常繁杂, 涉及到的类型又多,
    如果用静态方式来实现, 所有东西都需要显式申明, 很容易产生严重的耦合问题

    需要更灵活一点的, 则都走脚本语言绑定了, 利用纯动态语言的动态特性来玩

扯了那么多, 也就容易明白为什么涉及到 UI 的框架基本上用的都是动态反射了

技巧相关

枚举的数值化

枚举大家都很熟了, 然而更经常用到的并不是严格意义上的枚举, 典型的就是一些事件定义, 这种场景有以下特点:

  • 不强求值必须在给定范围内, 比如随时可以新增一个值
  • 不关心每项的具体值, 只要能与其它项区分开即可

于是大多数习惯的做法是使用字符串:

class MyEvent {
    string Event0 = "Event0";
    string Event1 = "Event1";
    string Event2 = "Event2";
}

然而对于枚举来说, 比较的时候那些字符串比较操作就显得很多余了

所以 ZF 里采用 map<int, string> 的方式来处理事件定义, 枚举值动态注册, int 值递增,
这样既保证使用字符串来定义事件的便捷, 又保证枚举值使用时整型值比较的高效

RAII

没错, 这个学过 C艹 的人都懂的基本设计, 用的好的话可以减少很多代码, 下面要提到的全局初始化就是 RAII 的一个典型使用场景

写过 Object-C 代码的童鞋相信对 MRC 下的 property 都有种又爱又恨的感觉, 因为 dealloc 的时候都需要对所有 retain property 逐个清空,
这个实在太麻烦了

用 C艹 的 RAII 来实现就方便多了, 用一个空对象去包装一下 property 然后在析构函数做清理操作即可

类似的使用也广泛用于智能指针等设计中

全局初始化

传统模块化的设计不管模块化做的多好, 最终还是避免不了需要一个初始化入口, 比如需要在父模块调用一下子模块的初始化函数,
里面干一些资源初始化或者事件监听的注册等等

ZF 里面利用 C艹 特有的 RAII 来解决这个问题, 详情可以参见 ZF_GLOBAL_INITIALIZER_INIT,
简单的说就是利用一个静态全局变量的构造函数去做一些初始化的操作

但是和传统 C艹 的处理方式不同, 在静态全局变量的构造函数这边只做注册操作, 实际上的初始化是在 app 启动完成后,
再遍历这些已注册的入口进行初始化操作, 这样做的好处, 可以明确的控制初始化顺序和依赖关系,
别以为这是小事, 模块多了, 初始化顺序的不确定可能会造成很多不可预期的问题

具体实现的伪代码

// global
class InitializerData
{
    Level level;
    Constructor constructor;
    Object *instance;
};
static vector<InitializerData> initializerList;
void mainInit(void)
{
    // 此处可以根据 level 人为控制初始化顺序
    for_each(e in initializerList)
    {
        e.instance = e.constructor();
    }
}

// actual initializer
class MyInitializer
{
    MyInitializer(void) { /* actual init step */ }
    ~MyInitializer(void) { /* actual cleanup step */ }
    static Object *constructor(void) {return new MyInitializer();}
};
class MyInitializerRegister
{
    MyInitializerRegister(void)
    {
        initializerList.add(InitializerData(MyInitializer::constructor, MyLevel));
    }
};
static MyInitializerRegister dummy;

项目相关

unity builds

参考 ZFFramework/tools/common/unity_build.sh

关于 unity builds 的各种优缺点可以参考 这里,
ZF 中用到的场景可以参考 这里,
这里就不啰嗦了

自动编译脚本

参考 ZFFramework/tools/common/run_recursive.sh,
ZFFramework/zfcleanup.sh

模块化开发是个很好的设计方式, 不过带来的问题是各种麻烦的依赖关系, 主要包括:

  • 各种 include ../../xxx 的路径问题, 一不小心就多写或少写个路径
  • 新增或删除模块, 还得同步修改配置

于是搞了这么个东西, 原理很简单, 递归查找指定文件名前缀的脚本文件,
然后把项目根目录或者重要路径作为参数传递给脚本,
这样不管怎么挪怎么新增或删除模块, 都可以不用改动代码或配置了

同样, 第三方库也可以用这种方式来管理, 参考
ZFFramework/zfsetup.sh

这样, 子模块可以自己管理自己的依赖关系, 而不需要同时修改到父模块的脚本文件

跨平台脚本

参考 ZFFramework/tools/common/zfsh.sh,
ZFFramework/tools/release/release.zfsh

脚本是个好用的玩意儿, 撸跨平台代码自然避不开各种脚本, 然而:

  • 为每个平台维护一份脚本今后很难维护
  • 虽然可以引入一个跨平台的脚本环境 (例如 Windows 下用 cygwin), 然而大多数情况下编译中用到的脚本只是些很基本的功能,
    比如复制些文件, 调用编译命令,
    有点杀鸡用牛刀的感觉
  • 有人说 cmake, 各平台配一遍环境就够你受的了, 更别说和 IDE 整合的问题了

所以折腾了这么个玩意儿, 原理很简单, 按照规定的格式写, 然后纯文本替换后再执行即可

POSIX         Windows          这什么 JB 玩意儿
# comment     rem comment      <ZF_COMMENT> comment
A = "$1"      set A = "%~1%"   <ZF_SET> A = "<ZF_P1>"
B = $A        set B = %A%      <ZF_SET> B = <ZF_PL>A<ZF_PR>
sh $B.sh      call %B%.bat     <ZF_SH> <ZF_PL>B<ZF_PR>.<ZF_SH_EXT>

虽然实际上写起来又臭又长, 但毕竟只需要写一份脚本即可

当然也可以做成将一个平台的脚本 "翻译" 成另一个平台的语法, 但是这个实现起来比较困难, 而且在很多细节问题上无法做的比较通用, 在语法上也会有更多限制

时间戳

参考 ZFFramework/tools/common/git_check.sh,
ZFFramework/tools/common/timestamp_check.sh,
ZFFramework/tools/common/timestamp_save.sh

ZF 里面, 在 app 构建阶段, 采用脚本自动更新所有第三方库的 git,
然而 git 本身没有什么缓存的参数,
导致每次编译运行的时候都要去 git pull,
非常浪费时间, 于是折腾了这么个玩意儿

最近一次 git clonegit pull 之后, 保存当前时间戳,
之后只要没有超时, 就不再去 git pull

有其他什么费时操作又不需要每次更新的, 也可以用类似的方式来玩耍

工程文件模板

参考 ZFFramework/ZF/ZFAlgorithm/zfsrc/ZFAlgorithm/ZFTextTemplateRun.h,
ZFFramework/ZF/ZFAlgorithm/zfsrc/ZFAlgorithm/ZFTextTemplate.h,
ZFFramework/tools/zfproj_creator/zfproj_creator.sh

配工程文件是个巨蛋疼的事情, 即使是从一个空项目新建,
也还得配置一堆项目名, 依赖关系什么的,
其实里面很多东西都是重复性工作,
于是又折腾了这么个玩意儿

其实已经有现成的 Google CTemplate 了, 原理上都一样,
就是基本的文本替换, 文本块条件开关, 等等

顺便吐槽一下 XCode 的工程文件, 真是见过的最糟糕的工程文件,
元素内容是通过一个随机 hash 值进行标记的,
导致每次添加新元素时工程文件都是不确定的内容,
非常不利于版本控制

XCode 工程文件排序

参考 truebit/xUnique

用于处理 XCode 的工程文件并排序, 可以有效避免那屎一样的冲突, 具体自行参见里面的文档

  • 虽说文档里建议用 git hook 或 XCode Build Scheme, 不过我这试过是可以在 Build Phase 里面添加 Run Shell Script 来使用的,
    个人建议如果这么用没问题的话还是加在 Build Phase 里面更方便一点
  • 建议使用这个 fork, 原 repo 在某些情况下排序和 XCode 的排序不一致

转载请注明来自: http://zsaber.com/blog/p/113

既然都来了, 有啥想法顺便留个言呗? (无奈小广告太多, 需审核, 见谅)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注