The Blog Of Mei Xia


  • 首页

  • 归档

MDL之编程

发表于 2018-06-26

在 初试 MDL 这篇文章中,认识了 MDL 文件结构、程序运行以及编译原理,下面就来讲讲 MDL 设计思路以及如何编写各部分。

设计思路

MDL 如何开始,并能使初设很容易转换成高层次的设计:

首先:

  1. 定义使用者在熟悉的术语中操作的对象类型
  2. 列出对象的属性
  3. 定义对象的放置和操作
  4. 其他需要的功能

进阶:

  1. 确定用于表示应用程序对象的 MicroStation 元素
  2. 确定必须从用户那里收集哪些设置
  3. 设计元素放置和操作工具
  4. 用于其他功能的设计工具和设置框

以上就是在编程应用程序的设计细节之前,需要首先拥有的功能规范,描述应用程序要执行的操作。

以下就是实际编程中 MDL 初始化逻辑:

  1. 为对话框、命令表和消息列表打开资源文件:

    1
    2
    RscFileHandle rfHandle;
    mdlResource_openFile (&rfHandle, NULL, FALSE);
  2. 注册消息列表 id 以用于操作的提示或错误:

    1
    2
    //MSGLIST_commands、MSGLIST_prompts 分别为命令ID、命令提示ID
    mdlState_registerStringIds (MSGLIST_commands, MSGLIST_prompts);
  3. 加载命令表:

    1
    mdlParse_loadCommandTable (NULL);
  4. 为系统事件(比如:【SYSTEM_UNLOAD_PROGRAM:程序尚未加载】、【SYSTEM_RELOAD_PROGRAM:程序重新加载】)设置用户的 hook 函数:

    1
    2
    // 在 SYSTEM_UNLOAD_PROGRAM 卸载时在 unloadFunction 函数中做一些事情,比如存储这是输入的值以便下次使用,不用再次输入,而 unloadFunction 就是 hook 函数,用于在指定的事件发生时触发自定义的操作
    mdlSystem_setFunction (SYSTEM_UNLOAD_PROGRAM, unloadFunction);
  5. 使用 MicroStation 对话框管理器注册对话框 hook 函数:

    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
    //注册对话框 hook 函数组,并关联hook函数ID和函数地址
    mdlDialog_hookPublish (sizeof(uHooks)/sizeof(DialogHookInfo), uHooks);
    // 将hook函数 myLevel_comboHook 和 对话框ID为 HOOKITEMID_MyLevelCombo的关联起来,以监测这个对话框条目生命周期的事件
    DialogHookInfo uHooks[] =
    {
    {HOOKITEMID_MyLevelCombo, (PFDialogHook)myLevel_comboHook },
    }
    //针对对话框条目比如:初始化、创建生成、清除等等进行监测,并触发相应的操作
    void myLevel_comboHook (DialogItemMessage *dimP)
    {
    RawItemHdr *riP = dimP->dialogItemP->rawItemP;
    dimP->msgUnderstood = TRUE;
    switch (dimP->messageType)
    {
    case DITEM_MESSAGE_CREATE:
    {
    //对话框条目生成时触发的操作
    break;
    }
    case DITEM_MESSAGE_DESTROY:
    {
    //对话框条目清除时触发的操作
    break;
    }
    .......
    default:
    //对话框条目其他情况默认触发的操作
    break;
    }
    }
  6. 打开应用程序的初始对话框,初始化完成后,主函数返回,应用程序等待它的对话框 hook 函数、用户 hook 函数或命令函数被调用:

    1
    2
    //其中 DIALOGID_Pal 为对话框资源文件中定义的对话框ID
    mdlDialog_open (NULL, DIALOGID_Pal);
  7. 构建单元库,用于需要一组预定义图符的应用程序表示系统内置的单元库中的对象,而这个单元库由开发人员生成并打包成应用程序

  8. 创建命令表,注册命令表中的 command,并将命令ID与命令触发的方法对应:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Private MdlCommandNumber commandNumbers [] =
    {
    {createALine, CMD_HELLOWORLD_CREATE_LINE},
    {createAComplexShape, CMD_HELLOWORLD_CREATE_COMPLEXSHAPE},
    {createAProjectedSolid, CMD_HELLOWORLD_CREATE_PROJECTEDSOLID},
    {createABsplineSurface, CMD_HELLOWORLD_CREATE_BSPLINESURFACE},
    {connectHitPoints, CMD_HELLOWORLD_CONNECT_HITPOINTS},
    {turnOnViewTransient, CMD_HELLOWORLD_TRANSIENT_ON},
    {turnOffViewTransient, CMD_HELLOWORLD_TRANSIENT_OFF},
    0
    };
    mdlSystem_registerCommandNumbers (commandNumbers);
  9. 编写原始命令逻辑,编写应用程序的所有原始命令例程来处理元素放置和操纵。每一个原始的命令都需要设置状态函数来处理事件,比如数据点、重置和显示动态——当它被拖过屏幕时显示一个元素,有时被称为“弹性连接”。

  10. 为直接命令和实用命令编写逻辑,其中直接命令用于修改内部设置的值,比如:精确绘图快速旋转 RQ,但对话框是一种更优雅的方式来设置内部变量的值,因此编写直接命令的需求减少了。而实用命令不需要来自用户的交互输入,并且不只是设置应用程序变量,还有很多状态控制函数,比如会经常使用的:

    1
    2
    //根据事件规定使用的函数,下面的意思是动态绘制状态时调用函数 line_drawLine 进行动态绘制直线
    mdlState_setFunction (STATE_COMPLEX_DYNAMICS, line_drawLine);
  11. 构建工具栏,工具栏是开始命令的图形图标。当用户单击一个工具时,就会启动适当的命令。如果向用户提供命令,MicroStation 类型的应用程序至少有一个工具箱。在开发的初始阶段,创建工具箱并不是绝对必要的,因为可以在命令行键入命令。然而,当您可以单击一个图标以启动一个命令而不是在命令行键入它时,测试就容易多了。

  12. 构建对话框,对话框显示并收集数据

  13. 创建在线帮助的文档

编程实践

要实现【 MDL 初始化逻辑】里的步骤,主要需要编写以下部分:

  • 消息表(xxxmsg.r)
  • 命令表(xxxcmd.r)
  • 对话框(xxx.r)
  • 源码(xxx.cpp)
  • 编译规则(xxx.mke)

接下来如何编写以上文件,根据 初试 MDL 这篇文章中的示例程序 adrwdemo 的绘制直线的功能来讲解:

消息表

消息列表包含应用程序使用的消息。列表中的每条消息都有相应的消息编号。这些消息号必须是唯一的,并以升序的顺序出现在列表中。MessageList的资源标识符(称为resources ID)与消息编号相结合,惟一地标识列表中的给定消息。

消息表结构的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
Messagelist <messageid> =
{
{
{<messagenumber>, "<message>"},
......
}
}
结构字段解析:
messageid:识别这个表的唯一编号
messagenumber:唯一的编号且必须以升序排列
message:包含信息的字符串
ps:messageid和messagenumber结合起来,可以唯一地识别列表中的信息

根据语法在 adrwdemomsg.r 编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//资源相关类是在系统头文件rscdefs.h中定义,比如:MessageList
#include "rscdefs.h"
#include "adrwdemo.h"
MessageList MSGLIST_commands =
{
{
{MSG_cmdLine, "Line"},
}
};
MessageList MSGLIST_prompts =
{
{
{MSG_promptEnterPoint, "Enter point"},
}
};

在 MS 操作时界面左下角的提示框就会显示【Line ->Enter point】,提示需要在视图中输入一个数据点。

命令表

命令表定义了应用程序命令的语法。命令表是分层的,其中一个主表向下分支到子命令树中。一个应用程序的命令表是在一个资源源文件中定义的,它被编译成产生两个输出文件,一个是用来解析和验证键的,另一个是唯一标识每个命令的无符号整数的列表。

命令表结构的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Table <tabteid>=
{
<number>, <subtableid>, <commandclass>, <options>, <commandword>
}
结构字段解析:
tableid:是一个32位无符号整型数。主或根命令表必须赋给一个值为1的
tableid。
number: 定义命令号。
subtableid:指定命令中下一个词的子表。如果没有子表, 则使用 #define CT_NONE。
commandclass:指定命令类别,命令类别是预先定义的,如 INHERIT、PLACEMENT......(INHERIT采用最后一个命令类别,PLACEMENT对于基本命令是最合适的命令类别,DELETE ELEMENT命令属于类操纵,但是DELETE CELL命令位于CELLLIB类中)
options:可用的选项是NONE,DEF,REQ,TRY和CMDSTR(n)。这些选项可以用OR(│ )相连。例如DEF │ TRY(n)。
DEF (缺省), 给特定表指定缺省值。
REQ (要求),告诉命令语法分析程序必须从子表中选择。
TRY (试图分析),这个选项将试图分析子表中的一个值。如果这个值不匹配,则把不匹配部分传递给应用程序。例如:ACTIVESTYLE 3, 这里的3被传递给应用程序。
CMDSTR(n) 每次执行命令时将显示信息表(Messagelist)中的字符串n。
commandword:命令输入字符串

根据语法在 adrwdemocmd.r 编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//避免生成的命令与MicroStation内置命令重复
#pragma suppressREQCmds
#include <rscdefs.h>
//命令表结项选项相关在系统头文件cmdclass.h中定义
#include <cmdclass.h>
#include "adrwdemo.h"
//宏定义命令表 ID
#define CT_NONE 0
#define CT_MAIN 1
#define CT_DEMO 2
Table CT_MAIN =
{
{ 1, CT_DEMO, PLACEMENT, REQ, "DEMO"}
};
Table CT_DEMO =
{
{ 1, CT_NONE, INHERIT, NONE|CMDSTR(MSG_cmdLine), "LINE"}
......
};

在 MS 中顶部菜单栏【实用工具 】-【命令行】键入画线命令【DEMO LINE】,就能启动画线的功能

对话框

工具栏和对话框的GUI相关信息都是存储在资源文件中,通过编写资源文件来控制列表条目、条目资源显示的位置和方式、将被发送到条目hook函数的消息列表以及相关对话框管理器功能的列表。标准对话框条目有:ColorPicker, Generic, GroupBox, IconCmdFrame, Label, LevelMap, ListBox, MenuBar, MLText, OptionButton, PushButton, ScrollBar, Text, ToggleButton, Sash, Scale, RadioButton, MenuBarX, LevelPicker, ToolBox, IconCmdX, ButtonGroup、IconCmdFrameX,这里只列举示例程序中用到的条目类型:

对话框

dl2_dialogboxrs

上图为对话框,其数据结构:

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
typedef struct dialogboxrsc
{
ULong attributes; /* dialog attributes */
int width; /* dialog coords */
int height; /* dialog coords */
ULong helpInfo; /* help for entire dialog */
char *helpSource; /* help task ID */
long dialogHookId; /* dialog hook ID */
long parentDialogId; /* to inform when destroyed */
#if defined (resource)
char label[]; /* dialog label (title) */
DialogItemRsc itemList[]; /* array of dialog items */
#else
long labelLength; /* length of label (title) */
char label[1]; /* dialog label (title) */
#endif
} DialogBoxRsc;
结构字段解析:
attributes:对话框属性,枚举如下:
DIALOGATTR_DEFAULT:默认的对话框类型,未设置属性
DIALOGATTR_GROWABLE:对话框可重定尺寸,不受width、height影响
DIALOGATTR_NORIGHTICONS:对话框右上角没有最小化、最大化按钮只剩关闭按钮DIALOGATTR_MODAL: 模态框,对话框右上角一个操作按钮都没有
DIALOGATTR_UNCLOSEABLE:不能关闭对话框
DIAIOGATTR_SINKABlE:对话框可送到视图窗口后面
DIALOGATTR_NOAUTOSWITCH:键盘焦聚不能自动打开另一个窗口的开关
DIALOGATTR_CLOSEONNEW:当打开一个新设计文件时关闭对话框
DIALOGATTR_ALWAYSSETSTATE:当用户与条目交互时设置条目的状态
......
width、height:对话框的宽、高由定义在<dlogbox.h>中的三个常量指定:
#define DCOORD_RESOLUTION 12
#define XC (DCOORD_RESOLUTION/ 2)
#define YC DCOORD_RESOLUTON
常量DCOORD_RESOLUTION被规定为12个象素。XC是一个字符的宽度, 是字符高的一半。YC是一个
字符的高。
helpinfo、helpSource:为这个应用定义帮助功能。如果不存在帮助,就分别设置
为NOHELP和MHELPo
dialogHookId:hook函数ID,监测这个对话框条目生命周期的事件
parentDialogId:当一个对话框需要启动另一个对话框时,指定父对话框的ID。然后,这个字段用于确定当子对话框被销毁时要通知哪个对话框。如果对话框没有从另一个对话框中启动,或者父对话框不需要被告知子对话框的销毁情况,请使用NOPARENTID。
label:对话框标题
itemList:对话框列通用条目DialogItemRsc的数组

对话框通用条目

mdl2_comitem

红色方框内为对话框通用条目,其数据结构:

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
typedef struct dialogitemrsc
{
Sextent extent; /* item area, origin (in dialog coords), if
width/height is zero, use dimensions
specified in item */
long type; /* item type */
long id; /* item ID */
byte attributes; /* item attributes */
long itemArg; /* item argument */
#if defined (resource)
char label[]; /* item label */
char auxInfo[]; /* item auxiliary information */
#else
int labelLength; /* length of item label */
char label[1]; /* item label */
#endif
} DialogItemRsc;
结构字段解析:
extent:对话框条目的坐标和宽高,Sextent定义如下:
typedef struct spoint2d
{
short x;
short y;
}Spoint2d, SPoint2d;
typedef struct extent
{
SPoint2d origin; / * upper left */
short width;
short height;
}Sextent;
type:指定要创建的条目的类型,枚举类型有:ColorPicker, Generic, GroupBox, IconCmdFrame, Label, LevelMap, ListBox, MenuBar, MLText, OptionButton, PushButton, ScrollBar, Text, ToggleButton, Sash, Scale, RadioButton, MenuBarX, LevelPicker, ToolBox, IconCmdX, ButtonGroup、IconCmdFrameX.
id:指定要加载的条目实例的资源ID
attributes:指定项目的初始状态,启动【ON】允许对物品进行操作或禁用。一个禁用的项目被调用,不能接受输入焦点,并且忽略用户操作。大多数项目都应该启用【ON】。
itemArg:为不同的项目类型包含不同的值。比如:label 条目用这个字段指定使用的理由,大多数条目当前没有使用这个字段
label:表示一个字符串,它允许盖特定对话框中条目的标签。默认标签在每个条目资源中定义。在对话框中指定的标签覆盖条目资源中指定的任何标签。该字段允许使用条目的已定义行为,但以不同的方式标记条目,标签的使用取决于项目类型。
auxInfo:包含额外的item-specific信息。实际该字段仅用于覆盖条目资源中定义的接入【变量】字符串,使条目能够按照项目资源中定义的方式进行操作,但是会影响到不同的应用程序变量。

图标命令框

mdl2_IconCmdFrameRsc

红色方框内为图标命令框,其数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct ditem_iconcmdframersc
{
int nColumns;
int nRows;
ULong helpInfo;
ULong helpSource;
#if defined (resource)
char label[];
DialogItemSpec iconPieces[];
#else
long labelLength;
char label[1];
#endif
} DItem_IconCmdFrameRsc;
结构字段解析:
nColumns、nRows:图标命令框中图标的列数和行数
label:指定图标命令框架的标题。对话框标题将被设置为这个字符串的值
iconPieces:指定图标命令框架的内容。iconPieces是一组对话框的成员。图标成员的类型字段可以是IconCmd或IconCmdPalette。iconPieces成员的id字段表明ditemiconcmdrsc或ditemiconcmdpalettersc的实例加载

图标命令框条目

mdl2_item

红色方框内为图标命令框条目,其数据结构:

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
typedef struct ditem_iconcmdrsc
{
ULong helpInfo;
#if defined (resource)
char helpTaskId[];
ULong attributes;
ULong commandNumber;
char commandTaskId[];
char unparsed[]
char enabledAccessStr[];
DialogItemRsc iconItems[];
#else
long helpTaskLen;
char helpTaskId[1];
#endif
} DItem_IconCmdRsc;
结构字段解析:
attributes:指定图标命令的属性。这个字段通常为0,是通过将属性表中的常量与逻辑运算符|组合在一起构造的,有两个枚举值:
ICONCMDATTR_DONTSINGLESHOT:表示这个图标命令不能双击进入单发操作模式。默认情况下,双击图标会导致微站点为选定的图标输入单发模式。
ICONCMDATTR_DONTAUTOSELECT:表示当与图标相关联的命令被排队到 MicroStation 时,这个图标资源不应该被高亮显示(选中)。
commandTaskId:指定将要执行与命令编号相关联的命令的任务。
将comandTaskId设置为OTASKID,它被定义为空字符串,表明拥有(最初创建)对话框的任务应该执行该命令。
将comandTaskId设置为MTASKID,它被定义为“+”,表明应该使用微站来执行该命令。(命令号必须是在cmdlist.h中定义的一个微站命令号。)
如果您需要指定一个与所有者或 MicroStation 不同的任务,请将任务名称放在这里。
enabledAccessStr:如果未使用,设置为""。
iconItems:一个DialogItemRsc数组和与图标命令关联的弹出对话框项列表。创建此列表的方式与创建普通对话框项列表的方式相同,请参照“DialogBoxRsc Structure”和“DialogBoxRsc Structure”。只有包含在图标命令面板中的图标命令才能有项目列表。图标命令帧不会显示弹出式项目列表。当在图标命令项列表规范中使用时,DialogItemRsc的区段成员指定相对于父图标命令面板左下角的位置,而不是包含图标命令的对话框左上角。

开关状态按钮

mdl2_tbtn

红色方框内开关状态按钮,其数据结构:

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
typedef struct ditem_togglebuttonrsc
{
ULong commandNumber;
ULong commandSource;
long synonymsId;
ULong helpInfo;
ULong helpSource;
long itemHookId;
long itemHookArg;
ULong mask;
char invertFlag;
#if defined (resource)
char label[];
char accessStr[];
#else
long labelLength;
char label[1];
#endif
} DItem_ToggleButtonRsc;
结构字段解析:
SynonymsId:定义在对话条目被修改时与所做的修改相对应的对话条目。最好的例子是
ColorPicker对话条目, 它有SynonysmsId, 如Text Item。在ColorPicker改变时, Text Item修改为ColorPicker选择的相应的颜色编号。
itemHookId:定义hook函数的ID。如果不用hook函数, 则在这个字段中指定为NOH00K。hook函数
允许我们修改对话框和条目的缺省动作。
itemHookArg:为hook函数规定变量。如不需要变量, 则在这个字段中使用NOARG。
accessStr:对话框条目控制的变量。例如, 我们想使对话框中的任何变化都反映在变量
adrwdemoInfo.showAxes中, 所以, accessStr将是"adrwdemoInfo.showAxes"。对话框能识别这个变量,因为它已被说明。

命令列表条目

CmdItemListRsc 基于命令,(命令编号被用作ID)CmdItemListRsc 让 MicroStation 无论如何调用该命令,都可以找到当前命令的工具设置,以此解决由于工具设置项列表与图标相关联,(而不是图标执行的命令),只有当图标在内存中(显示)时才能找到工具设置。在检查 IconCmdRsc 资源中的条目列表之前,MicroStation 现在寻就会找与当前命令相关联的 CmdItemListRsc。

1
2
3
4
CmdItemListRsc <commandID> =
{{
DialogItemRsc itemList[];
}};

对话框管理访问字符串

假设开发按钮的字段 label 为变量 adrwdemoInfo.showAxe,如下:

1
2
3
4
5
6
7
8
/*
开关状态按钮
*/
DItem_ToggleButtonRsc TOGGLEID_ShowAxes =
{
NOCMD, MCMD, NOSYNONYM, NOHELP, MCMD, NOHOOK, NOARG,
NOMASK, NOINVERT, TOGGLE_ShowAxes, "adrwdemoInfo.showAxes"
};

因为访问字符串为检查和修改对话框条目定义变量、结构和结构的指针,所以必须通过发布变量、结构或指针, 使对话框条目能够管理我们的数据。实现分为两步:

  • xxxtyp.mt中定义结构和联合的说明,这个文件含有包含文件和publishStructures语句,publishStructures 语句识别在资源源文件中定义的结构。这个结构不能含有动态说明(分配空间的那些说明)或可执行语句:

    1
    2
    #include "adrwdemo.h"
    publishStructures (adrwdemoinfo);

    以此来发布定义在 adrwdemo.h 中的用于设置是否启用结构平面或者显示坐标轴的全局数据结构体:

    1
    2
    3
    4
    5
    typedef struct adrwdemoinfo
    {
    int showAxes;
    int useCPlane;
    } AdrwdemoInfo;
  • 定义符号集,符号集是一个指向内存区域的指针。在这个内存区域内对话条目可以找到应用的变量、结构和指针,为了建立并公布要存取的结构 AdrwdemoInfo,在 .cpp 程序入口函数中编写如下:

    1
    2
    3
    4
    5
    //使setP符号集初始化,可见标志被分配给每一个符号。在搜寻过程中检查可见标志来决定符号集是否包括在搜寻中。当这些符号用于对话框时, 我们规定VISIBILITY_DIALOG_BOX。这些符号如果用于计算器或预处理器,我们则指定VISIBILITY_CALCVLATOR。如果符号同时用于二者,则规定(VISIBILITY_DIALOG_BOX\ VISIBILITY_CALULATOR)。若符号与调试程序一起使用, 则我们规定VISIBILITY_DBUGGER。
    SymbolSet *setP = mdlCExpression_initializeSet (VISIBILITY_DIALOG_BOX, 0, 0);
    //发布要使用的符号
    mdlDialog_publishComplexVariable (setP, "adrwdemoinfo",
    "adrwdemoInfo", &adrwdemoInfo);

根据各个对话框条目的语法,在 adrwdemo.r 编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <rscdefs.h>
#include <cmdlist.h>
//对话框相关类在系统头文件dlogbox.h中定义
#include <dlogbox.h>
#include "adrwdemo.h"
#include "adrwdcmd.h"
#include "adrwdtxt.h"
/*
MDL应用程序(MA)和DLL之间的链接,仍然有一个MA文件被创建和加载,其中包含对话框资源以及DllMdlApp类型的资源,它告诉MA加载DLL。因此需要在一个.r文件中创建这个文件,该文件将被放入应用程序文件中。通常,这是在命令表资源文件的底部完成的,但是它可以在应用程序中的任何资源文件中。
*/
#define DLLAPP_PRIMARY 1
DllMdlApp DLLAPP_PRIMARY =
{
"ADRWDEMO", "adrwdemo" // taskid, dllName
}
/*
构建对话框
TITLE_Palette 是定义在 adrwdtxt.h 中的字符串"AccuDraw Demo"
*/
DialogBoxRsc DIALOGID_Pal =
{
DIALOGATTR_DEFAULT | DIALOGATTR_NORIGHTICONS,
XC, YC, NOHELP, MHELP, NOHOOK, NOPARENTID, TITLE_Palette,
{
{
{0,0,0,0}, IconCmdFrame, ICONCMDFRAMEID_Frame, ON, 0, "", ""},
}
};
/*
图标命令框
*/
DItem_IconCmdFrameRsc ICONCMDFRAMEID_Frame =
{
5,1,NOHELP,MHELP,TITLE_Palette,
{
{IconCmd, ICONCMDID_Line},
{IconCmd, ICONCMDID_Circle},
{IconCmd, ICONCMDID_Rectangle},
{IconCmd, ICONCMDID_Rectangle2},
{IconCmd, ICONCMDID_ChangeCircle},
}
};
/*
图标命令框条目
其中 MSVERSION 随着每个版本的变化而变化,因此MDL程序员可以有条件地为特定的版本编译代码.
extendedAttributes:扩展属性类型文本,用于鼠标移到图标命令条目上时显示命令描述提示
*/
DItem_IconCmdRsc ICONCMDID_Line =
{
NOHELP, MHELP, ICONCMDATTR_DONTSINGLESHOT,
CMD_DEMO_LINE, OTASKID, "", "",{}
#if defined (MSVERSION) && (MSVERSION >= 0x550)
}
extendedAttributes
{
{
{EXTATTR_FLYTEXT, TXT_Flyover_DemoLine},
{EXTATTR_BALLOON, TXT_Balloon_DemoLine},
}
#endif
};
#if defined (MSVERSION) && (MSVERSION >= 0x551)
CmdItemListRsc CMD_DEMO_LINE =
{{
{
{0,GENY(1),0,0}, ToggleButton, TOGGLEID_ShowAxes, ON, 0, "", ""
},
}};
#endif
/*
开关状态按钮
*/
DItem_ToggleButtonRsc TOGGLEID_ShowAxes =
{
NOCMD, MCMD, NOSYNONYM, NOHELP, MCMD, NOHOOK, NOARG,
NOMASK, NOINVERT, TOGGLE_ShowAxes, "adrwdemoInfo.showAxes"
};

源码

adrwdemo.cpp 文件的画线功能由以下三部分组成:

  1. 引入头文件,涉及用到的宏定义、全局变量以及函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <MicroStationAPI.h>
    #include <cmdlist.h>
    #include <dlogman.fdf>
    #include <AccuDraw.h>
    #include <mselmdsc.fdf>
    #include "adrwdemo.h"
    #include "adrwdemocmd.h"
    #include "adrwdtxt.h
  2. 程序入口函数【 extern “C” DLLEXPORT int MdlMain (int argc, char *argv[]) { }】,编写参照【MDL初始化逻辑】

    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
    extern "C" DLLEXPORT int MdlMain (int argc, char *argv[])
    {
    RscFileHandle rfHandle;
    SymbolSet *setP;
    //打开资源文件,载入内存
    mdlResource_openFile (&rfHandle, NULL, FALSE);
    //加载命令表
    mdlParse_loadCommandTable (NULL);
    //创建命令表,注册命令表中的 command,并将命令ID与命令触发的方法对应
    Private MdlCommandNumber commandNumbers [] =
    {
    {line, CMD_DEMO_LINE},
    {circle, CMD_DEMO_CIRCLE},
    {rect, CMD_DEMO_RECTANGLE},
    {changeCircle, CMD_DEMO_CHANGE},
    {rect2, CMD_DEMO_RECTANGLE2},
    0
    };
    mdlSystem_registerCommandNumbers (commandNumbers);
    //注册消息列表 id 以用于操作的提示或错误
    mdlState_registerStringIds (MSGLIST_commands, MSGLIST_prompts);
    /*
    使setP符号集初始化,并发布使用的符号,符号集是一个指向内存区域的指针。在这个内存区域内对话条目可以找到应用的变量、结构和指针,为了建立并公布要存取的结构 adrwdemoinfo
    */
    setP = mdlCExpression_initializeSet (VISIBILITY_DIALOG_BOX, 0, 0);
    mdlDialog_publishComplexVariable (setP, "adrwdemoinfo",
    "adrwdemoInfo", &adrwdemoInfo);
    adrwdemoInfo.showAxes = FALSE;
    打开应用程序的初始对话框
    mdlDialog_open (NULL, DIALOGID_Pal);
    return 0;
    }

    当程序运行完这个函数,视图中就会显示 adrwdemo 工具框。

  3. 画线功能函数,MS内部机制是事件驱动,由事件队列统一管理调度,所以当按下 adrwdemo 工具框中的画线图标命令,这个画线任务就会进入事件队列,由事件队列处理器调用画线功能函数,所以需要使用函数mdlState_startPrimitive编写一个原始命令,由原始命令例程来处理元素放置和操纵,参考【MDL初始逻辑】第9条

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    //画线初始命令
    Private void line (char *unparsed)
    {
    /*
    调用mdlState_startPrimitive基本命令生成元素,并调用line_dataPt1开始画线,并配置宏定义字符串MSG_cmdLine 和 MSG_promptEnterPoint 组成"Line->enter point"的命令提示
    */
    mdlState_startPrimitive (line_dataPt1, line, MSG_cmdLine, MSG_promptEnterPoint);
    }
    //画线输入第一个数据点,作为画线的起始点
    Private void line_dataPt1 (Dpoint3d *ptP, int view)
    {
    //存储确定的第一个数据点
    ptStack[0] = *ptP;
    //设置事件触发函数,当输入数据点时触发函数line_dataPt2,动态绘制时触发函数line_drawLine
    mdlState_setFunction (STATE_DATAPOINT, line_dataPt2);
    mdlState_setFunction (STATE_COMPLEX_DYNAMICS, line_drawLine);
    //设置输出的命令提示
    mdlOutput_rscPrintf (MSG_PROMPT, NULL, MSGLIST_prompts,
    MSG_promptEnterPoint);
    }
    //画线输入第二个数据点,作为画线的终结点
    Private void line_dataPt2 (Dpoint3d *ptP, int view)
    {
    double length; /* length of line placed */
    DVec3d xAxisVec; /* unit vector -- direction of line */
    //传入第二个数据点,根据两点绘制直线
    line_drawLine (ptP, view, NORMALDRAW);
    //求出两点法向量的距离
    length = mdlVec_computeNormal (&xAxisVec, ptP, &ptStack[0]);
    //设置精确绘图 AccuDraw 的位置、距离、方向等,以便更好地操作,提高工具的可用性和能力
    mdlState_setAccudrawContext (
    ACCUDRAW_SetDistance |
    ACCUDRAW_SetXAxis,
    NULL, /* origin: last point OK */
    NULL, /* delta (not used here) */
    &length, /* distance */
    NULL, /* angle (not used here) */
    &xAxisVec /* x axis: last line */
    );
    //将第二个数据点设置为第一个
    ptStack[0] = *ptP;
    mdlState_setFunction (STATE_COMPLEX_DYNAMICS, line_drawLine);
    mdlOutput_rscPrintf (MSG_PROMPT, NULL, MSGLIST_prompts,
    MSG_promptEnterPoint);
    }
    //结合line_dataPt1和line_dataPt2两个数据点画线
    Private void line_drawLine (Dpoint3d *ptP, int view, MstnDrawMode drawMode)
    {
    MSElement elem;
    Dpoint3d points[2];
    points[0] = ptStack[0];
    points[1] = *ptP;
    //根据传入的两个点,创建元素
    mdlLine_create (&elem, NULL, points);
    //显示创建的直线元素
    mdlElement_display (&elem, drawMode);
    //当完成绘制,就将元素头部中的属性位设置为未锁定、新元素,而不修改,也就是在确定第二个点之后在视图生成固定位置的直线,而不动态改变了
    if (drawMode == NORMALDRAW)
    mdlElement_add (&elem);
    }

编译规则

MDL 的编译文件是 makefile 类型的文件,拥有自己的语法和变量,在 Linux 中也用来进行宏编译,它的好处:

  • 简化编译时所需要执行的命令
  • 若在编译完成之后,修改了某个源码文件,则 make 仅会针对被修改了的文件进行编译。其他的目标文件不会被更改
  • 最后可以依照相依性来更新执行文件

adrwdemo.mke 编写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//定义mdl程序名
appName = adrwdemo
/*
定义mdl目标文件路径,其中
$(o):mdl内置变量,objects 文件的路径,如:C:\Program Files (x86)\Bentley\MicroStation V8i (SELECTseries)\MicroStation\mdl\objects
$(oext):objects 文件后缀.obj
*/
appObjects = $(o)$(appName)$(oext)
//定义资源文件路径
appRscs = $(o)$(appName).rsc $(o)$(appName)cmd.rsc $(o)$(appName)typ.rsc $(o)$(appName)msg.rsc
//定义当前makefile文件的路径
baseDir = $(_MakeFilePath)
//引入 mdl 内置的编译规则
%include mdl.mki
dirToSearch = $(MSMDE)/mdl/MicroStationAPI
%include cincapnd.mki
//创建objects、rscObjects、reqdObjs三个文件夹,路径在C:\Program Files (x86)\Bentley\MicroStation V8i (SELECTseries)\MicroStation\mdl,用来放置编译后的文件
always:
~mkdir $(o)
~mkdir $(rscObjects)
~mkdir $(reqdObjs)
/*
设置dlmlink.mkil里面的变量,以便调用dlmlink,其中注意 DLM_LIBRARY_FILES 源码依赖的类库都要添加进来,DLM_OBJECT_FILES 需要编译的.cpp文件都要添加进来
*/
DLM_NO_SIGN = 1
DLM_OBJECT_DEST = $(o)
DLM_NAME = $(appName)
DLM_OBJECT_FILES = $(appObjects)
DLM_NO_DLS = 1
DLM_NO_DEF = 1
DLM_NOENTRY = 1
DLM_DEST = $(mdlapps)
DLM_NO_DELAYLOAD = 1
DLM_NO_NTBSADDR = 1
DLM_LIBRARY_FILES = $(mdlLibs)BentleyDgn.lib \
$(mdlLibs)toolsubs.lib \
$(mdlLibs)ditemlib.lib \
$(mdlLibs)mdllib.lib \
$(mdlLibs)msbspline.lib \
$(mdlLibs)mtg.lib
/*
编译资源文件,编译语法:
{
目标(target):目标文件1 目标文件2
<tab>编译规则
}
第一行中的目标就是要建立的信息,而目标文件就是具有相关性的源文件,第二行开头需要<tab>缩进一下再写编译规则,如果为空行则表示默认使用mdl.mki内置的编译规则
*/
$(baseDir)$(appName)cmd.h : $(baseDir)$(appName)cmd.r
$(o)$(appName)cmd.rsc : $(baseDir)$(appName)cmd.r
$(o)$(appName)typ.r : $(baseDir)$(appName)typ.mt
$(o)$(appName)typ.rsc : $(o)$(appName)typ.r
$(o)$(appName)msg.rsc : $(baseDir)$(appName)msg.r
$(o)$(appName).rsc : $(baseDir)$(appName).r
//根据上一步编译好的.rsc文件生成可执行文件.ma
$(mdlapps)$(appName).ma : $(appRscs)
$(msg)
> $(o)make.opt
-o$@
$(appRscs)
<
$(RLibCmd) @$(o)make.opt
~time
//调动VS提供的编译器cl,将源文件.cpp编译成目标文件.obj,由 dmlink.mki 定义的编译规则链接到DLL
$(o)$(appName)$(oext) : $(baseDir)$(appName).cpp
%include dlmlink.mki

编写完成,运行程序,实现下图红框中的画线功能:

mdl2_pargramrun

初试 MDL

发表于 2018-06-16

MDL 介绍

MDL 是 MicroStation 程序的开发手段,为开发人员提供工具创建应用程序,以充分利用 MicroStation 和基于 MicroStation 的应用程序的强大功能。MDL 的开发手段有三种分别是PureMDL、NativeCode、Addins,早期使用 PureMDL 开发的时候, MDL 是MicroStation Development Language(MicroStation开发语言)的简称,后来使用NativeCode 的开发方法 ,这里的MDL不再是MicroStation Development Language而是MicroStation Development Library(MicroStation开发库)。MDL 应用程序与 MicroStation 紧密集成,一个 MDL 命令或操作不能与一个核心的 MicroStation 命令区分开来。事实上,基础 MicroStation 产品的许多特性都是作为 MDL 扩展实现的。MDL可用于开发简单的实用程序、定制的命令或复杂的商业应用程序。

MDL开发手段:

  1. PureMDL:使用 Bentley 公司基于 C语言的一种扩展语言编写,编译器和链接器均由Bentley公司提供,最终生成的程序扩展名为.MA(MicroStation Application的缩写),开发者只需在源代码编辑器编辑程序

  2. NativeCode:MDL可以基于C++来开发,采用微软的Visual Studio作为开发工具来生成本机代码的DLL,同时,为了保留MDL的一些特性(如命令表、MDL特有的资源等),仍然需要生成一个.MA文件。采用面向对象的编程方式来写代码,还能直接调用任何VC++中可以调用的功能。大多数新的MicroStation开发功能(如XAttribute、点云、i-model等)都采用类的形式提供,这些新的功能也要求我们必须使用NativeCode MDL来开发应用

  3. Addins:Addins是基于.NET框架的,使用 C#、C++/CLI 或 VB.NET 语言来开发 Addins 应用程序。相比较于MVBA(MicroStation Visual Basic for Application),Addins能支持命令表、能编译成DLL;相比较于MDL(MicroStation Development Language/Library),Addins 能用 WinForm 来设计界面,再也不用学习对初学者来说难以掌握的.r资源了

    ps:本系列 blog 中的例子均使用 NativeCode 的开发手段

MDL 开发环境

由于使用 NativeCode 作为MDL开发手段,所以采用微软的Visual Studio作为开发工具,本系列 blog 的示例是在 MicroStation V8i (SELECTseries 3) 运行,所以 VS 的版本为2005,这里需要强调的是,如果安装了 VS2013或或者其他版本,也仅仅是用2013作为编辑器、调试器而不是作为编译器和链接器。后面两个功能仍然是用VS2005。只要你用bmake来编译V8i的代码,它会自动搜索到 VS2005 的 cl.exe 和 link.exe 的。对于大多数情况下用VS2013也能创建V8i下的代码,但对于一些特定情况,生成的代码一执行就会导致Mstn崩溃,比如点云类编程。所以,一定要用VS2005来支撑 V8i 的开发。

开发环境配置如下(以下软件的下载链接: https://pan.baidu.com/s/1StcP62cxdUv3opP1lFkbEA 提取密码: q33e):

  • 安装 MicroStation SDK 对应 MicroStation V8i (SELECTseries 3) 版本
  • 安装 VS 2005 ,详细配置查看 bentley 社区文章 在Visual Studio环境中生成并调试MDL应用

  • 安装 Visual Assist(代码提示补全插件)

    • 破解:解压之后安装,然后全局搜索 VA_X.dll,使用解压之后的 VA_X.dll 全部替换实现破解。

    • 识别 .fdf 文件:在MicroStation V8 中,引入了新的文件类型“ .fdf”,它们是函数定义文件(Function Definition File)。这些文件中含有某一类别的MDL函数原型定义,同时又有对包含文件的引用,所以在V8编程中经常只需在程序开头包含这些.fdf文件就可以了。但 Visual Assist 识别不了 .fdf 文件,就无法提供 .fdf 文件函数的提示,解决方法:

      1. 输入 regedit 命令打开注册表

      2. 在注册表路径 HKEY_CURRENT_USER\SOFTWARE\Whole Tomato\Visual Assist X\ 下找到对应 Visual Assist 版本,把下面信息导入注册表:

        1
        2
        3
        4
        5
        6
        Windows Registry Editor Version 5.00
        [HKEY_CURRENT_USER\SOFTWARE\Whole Tomato\Visual Assist X\VANet15]
        "ExtHeader"=".h;.hh;.hpp;.hxx;.ipp;.tlh;.inl;.p;.rh;.dh;.ih;.ph;.hm;.fdf;"
        "ExtSource"=".c;.cpp;.mc;.cc;.cxx;.tli;.mt;.moc;"
        "ExtResource"=".rc;.rc2;.r;"

MDL 实践

示例程序的源码目录结构

示例程序下载链接: https://pan.baidu.com/s/1Xc43qlfr9STwPLgoxxj8SQ 提取密码: rqx4a,找到 adrwdemo 下载到本地,这个 demo 的功能是提供一个基本画图工具栏,可以画线、矩形、圆并能修改。程序目录结构,如下图:

file_struct

左边红色框内的源码目录结构:

  • .cpp:MDL的代码源文件(PureMDL 与 NativeCode 不同在于代码源文件后缀为 .mc)。
  • .h:头文件或包含文件,其中包含由多个源文件共享的宏定义或数据结构体的定义等等。头文件包含在 .app文件、.mt文件和 .r文件中。
  • .r:资源源文件,资源源文件中的一些数据示例是对话框描述、命令表和消息。rcomp读取一个资源源文件并生成一个资源文件。
  • .mt:类型文件,用来生成MDL C表达式内置函数的类型定义的源文件。rsctype读取类型文件并生成资源源文件。
  • .mke:makefile文件,由bmake读取,按照其中的规则构建应用程序。

下面是编译过程中的产生的文件类型:

  • .mo:对象文件,由mcomp创建。这些文件被用作mlink的输入。
  • .mp:程序文件,一种特殊类型的资源文件,可以使用rlib与其他资源文件合并。由mlink创建的程序文件包含MDL可以解释的伪代码。程序文件是最简单的应用程序文件。
  • .rsc:资源文件,rcomp生成大多数资源文件。MDL程序和应用程序文件也是资源文件。
  • .mm:映射文件,由mlink生成。
  • .ml:库文件,由mlib创建和更新。库文件通常包含MDL对象文件,也可以包含任何类型的文件。
  • .ma:应用程序文件,一个包含与一个应用程序相关的所有资源的资源文件。一个应用程序文件可以用mlink或rlib创建。

运行示例程序

  1. 以管理员身份运行 打开 VS2005 (编译程序时需要管理员权限在MS目录下创建目录),在顶部菜单栏中点击【文件】 - 【打开】 -【 项目/解决方案】,在示例程序目录下找到 adrwdemo.vcproj 打开后,参照【MDL 源码目录结构】中的截图。

  2. 如果在【MDL 开发环境】那一步中配置好了 vs2005 需要的环境变量和外部工具,就可以点击顶部菜单栏中 【工具】- 【Rebuild MDLApp (Debug)】运行,如下图:

    run_mdl

    如果点击运行之后,在编译日志输出框中显示上图左下角红框中的内容,就表示编译成功

  3. 编译成功之后,在 MicroStation V8i (SELECTseries 3) 中,打开【实用工具】-【命令行】或者直接按F9,在输入框中键入 mdl load adrwdemo 进行加载使用,如果加载成功,会在消息框中显示 adrwdemo 已加载,并在当前视图中出现 adrwdemo 工具对话框,如下图:

    keyin_load

  4. 点击 adrwdemo 工具对话框中左边第三个个命令进行画矩形,也可以直接在【命令行】中键入【demo RECTANGLE】进行操作,如下图:

    play_cmd

    点击之后显示上图标记的三点:

    1. 显示画矩形的设置框,可以选择是否用结构平面或者是否显示坐标轴
    2. 显示提示,命令为 【Rectangle】,并提示【enter point】需要输入一个数据点
    3. 输入之后,显示 AccuDraw ,并根据鼠标点动态进行矩形的绘制,直到确定第二点,完成绘制。
  5. 注意事项:

    • 当在 VS 中编译示例程序 adrwdemo 时,需要确定在 MS 已经在【命令行】键入【mdl unload adrwdemo 】进行卸载,不然会因为当前 adrwdemo 正在进行使用,而无法再次编译。
    • 调试模式时,步骤为 【VS编译成功】 -> 【打开 MS】->【调试-附加到进程】->【MS 加载程序】

程序剖析

程序需要编译生成可执行文件才能运行,普及一下计算机程序的编译过程:

  1. 预处理:序列化、宏定义展开、#include展开(引用文件展开)
  2. 语法和语义分析:使用预处理后的单词构建词法树 - 执行语义分析生成语法树 - 输出AST(抽象语法树)
  3. 代码生成和优化:将AST转化成更低级的中间码 - 优化生成代码 - 目标代码生成 - 输出汇编代码
  4. 汇编程序:将汇编代码转化成目标文件
  5. 连接器:将多个目标文件合并成可执行文件(或者一个动态库)

而 MDL的编译,由以下提供的开发实用程序实现:


Utility name Description
bmake 用于为复杂的应用程序自动化编译、链接和资源构建的实用工具
mcomp 编译器用于编译MDL源文件
mlib 用来管理在库文件中文件镜像
mlink 将目标文件链接到一个程序中
rcomp 资源编译器用于编译资源源文件
rlib 将多个资源文件合并到一个资源文件中
rsctype 类型生成器用来为一组内置函数生成类型描述,被用于在运行时识别C的表达式(对话管理程序用C表达式去管理访问字符串)
rdump 转储资源文件的内容。如果在加载资源时遇到了麻烦,并且想要检查一个.r文件的内容,那么它是非常有用的(ps:rdump -v xxx.rsc)

结合图片中实用程序的描述和程序的编译过程步骤,可以看出 bmake 的作用就是简化应用程序的编译过程,本质就是表中实用程序结合。

bmake 编译 PureMDL 应用程序的过程 :

PureMDL_bmake

bmake 编译 NativeCode 应用程序的过程 :

NativeCode_bmake

其中 cl.exe 和 link.exe 则是来自VS的编译器和连接器程序, cl(全称clang是C/C++编译器)与mcomp 作用一样也是编译源文件,而 link 与 mlink 作用一样也是链接目标文件。

在了解源码目录的文件结构、程序运行情况以及 MDL编译原理之后,结合上述,剖析一下源码中的文件控制的功能:

  • 文件夹【国际化】下的两个文件 adrwdemomsg.r 和 adrwdtxt.h,控制命令提示(ps:Rectangle->Enter point)和对话框文本字符定义,并能根据需要切换语言,如:汉语、法语等,来实现国际化需求。

    analyze_1

  • 文件夹【头文件】下的 adrwdemo.h 控制宏定义以及定义全局的数据结构体,在点击工具栏矩形命令后弹出的设置框(ps:Use construction Plane 和 Show Axes)在设置框选择的值就是由 .h 文件定义的结构体变量控制。

    analyze_2

  • 文件夹【源文件】下的 adrwdemo.cpp 就是程序核心功能、逻辑的编写,实现画线、画矩形等功能。

    analyze_7

  • 文件夹【资源文件】下:

    • adrwdemo.r:工具对话框编的资源源文件,用来编写工具对话框,控制对话框条目的位置、类型、显示内容、条目执行的命令。

      analyze_3

    • adrwdemocmd.r:命令表的资源源文件,用来编写 adrwdemo 功能对应的命令,本质在对话框中选择的每一个操作背后其实对应的就是一个命令,所以在对话框选择绘制矩形也能在【命令行】中键入【demo rectangle】实现。

      analyze_4

    • adrwdemotyp.mt:类型文件,用来发布在 .h 定义的数据的结构体,以此来识别在资源源文件中定义的结构,在示例中就是画矩形的设置框中的数据结构,记录否用结构平面或者是否显示坐标轴。

      analyze_5

  • adrwdemo.mke:makefile文件,定义示例程序 adrwdemo 依赖的类库和资源文并包含构建应用程序所需的命令和指令 ,使用按一定规则构建应用程序

    analyze_6

    ​

Pod私有库二进制化

发表于 2017-11-02

在制作 Pod私有库 之后,接下来我们在此基础上实现 Pod私有库的二进制化,什么是二进制化?其实通过CocosPods导入的我们制作的Pod私有库,实际是源码,会随着主工程一起编译,二进制化指的是通过编译把组件的源码转换成静态库或动态库,以提高该组件在App项目中的编译速度。打开 Pod 私有库示例工程,实现以下步骤:

1、添加 Static Library

  • 在 xcode 中创建新 Target -> YXPlayerSDKBinary:
    1
    YXPlayerSDK->New->Target->iOS->Framework & Library->Cocoa Touch Static Library

    如果项目最低支持到iOS8可以创建 Dynamic Framework。

  • 将组件 YXPlayerSDK 的 Classes 文件夹下的文件 link 到 YXPlayerSDKBinary 中,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    YXPlayerSDK
    ├── Assets
    │   ├── YXPlayerSDKBinary.bundle
    │   │   ├── Info.plist
    │   │   └── playerPause@2x.png
    │   └── playerPause@2x.png
    ├── Classes
       ├── TestOne
       │   ├── TestViewController.h
       │   └── TestViewController.m
       └── TestTwo
       ├── DoubboViewController.h
       └── DoubboViewController.m
       ||
       || YXPlayerSDK 中 Classes 文件里的文件拖到 YXPlayerSDKBinary 中(link源码而不是复制)
       ||
    YXPlayerSDKBinary
    ├── TestOne
    │   ├── TestViewController.h
    │   └── TestViewController.m
    └── TestTwo
    ├── DoubboViewController.h
    └── DoubboViewController.m
  • 配置 YXPlayerSDKBinary

    在 YXPlayerSDKBinary Target 中的 Build Phase 界面:

    1、选择Editor\Add Build Phase\Add Copy Headers Build Phase

    2、在 Compile Sources 和 Headers 中添加拖进的文件

  • 设置 YXPlayerSDKBinary Target 中的 Build Settings 界面:

    1、 iOS Deployment Target 选择和YXPlayerSDK.podspec 中的 s.platform 保持一致

    2、Public Headers Folder Path 中配置为 include/YXPlayerSDKBinary

    3、禁掉无效代码和debug用符号:

    • Dead Code Stripping设置为NO
    • Strip Debug Symbol During Copy 全部设置为NO
    • Strip Style设置为Non-Global Symbols

    完成上述配置,选择目标为iOS Device,按下command + B进行编译,一旦成功,工程导航栏中Product目录下 libYXPlayerSDKBinary.a 文件将从红色变为黑色,表明现在该文件已经存在了。右键单击 libYXPlayerSDKBinary.a,选择 Show in Finder。在此目录下,你将看到静态库 libRWUIControls.a 以及定为 public 的头文件在此也可看到。

2、将 Static Library 构建为 Framework

  • 创建 framework 目录结构
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    YXPlayerSDKBinary.framework
    ├── Headers -> Versions/Current/Headers
    ├── Versions
    │   ├── A
    │   │   ├── Headers
    │   │   │   ├── DoubboViewController.h
    │   │   │   └── TestViewController.h
    │   │   └── YXPlayerSDKBinary
    │   └── Current -> A
    └── YXPlayerSDKBinary -> Versions/Current/YXPlayerSDKBinary

    在静态库构建过程中添加脚本来创建这种结构,在项目导航栏中选择 YXPlayerSDK,然后选择 YXPlayerSDKBinary 静态库目标,选择 Build Phases 栏,然后选择 Editor/Add Build Phase/Add Run Script Build Phase 来添加一个新的脚本在最后命名为 Build Framework:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    set -e
    export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"
    # Create the path to the real Headers die
    mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers"
    # Create the required symlinks
    /bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current"
    /bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers"
    /bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \
    "${FRAMEWORK_LOCN}/${PRODUCT_NAME}"
    # Copy the public headers into the framework
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
    "${FRAMEWORK_LOCN}/Versions/A/Headers"
    # Copy the public headers into the private pod/include
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
    "${SRCROOT}/../${PROJECT_NAME}/Products/include"

    这个脚本首先创建了YXPlayerSDKBinary.framework/Versions/A/Headers目录,然后创建了一个framework所需要的三个连接符号(symbolic links)。

    • Versions/Current => A
    • Headers => Versions/Current/Headers
    • YXPlayerSDKBinary => Versions/Current/YXPlayerSDKBinary

    最后,将公共头文件从你之前定义的公共头文件路径拷贝到Versions/A/Headers目录下,-a参数确保修饰次数作为拷贝的一部分不会改变,防止不必要的重新编译。

  • 多架构编译

    每个CPU架构都需要不同的二进制数据,为了让创建的 framework 能在所有可能的架构上运行。就需要创建了二进制FAT(File Allocation Table,文件配置表),它包含了所有架构的片段(slice)。构建过程:

    1、创建新的 Target 构建 framework:

    1
    YXPlayerSDK->New->Target->Cross-platform->Other->Aggregate

    找到 Aggregate,点击Next,将目标命名为 Framework。为什么使用集合(Aggregate)目标来创建一个framework呢?为什么这么不直接?因为OS X对库的支持更好一些,事实上,Xcode直接为每一个OS X工程提供一个Cocoa Framework 编译目标。基于此,你将使用集合编译目标,作为Bash脚本的连接串来创建神奇的framework目录结构。

    2、在库工程中选择Framework目标,在Build Phases中添加一个依赖。展开Target Dependencies面板,点击 + 按钮选择 YXPlayerSDKBinary 静态库。

    3、在 Framework 静态库目标下,选择Build Phases栏,然后选择Editor/Add Build Phase/Add Run Script Build Phase来添加一个新的脚本在最后命名为 MultiPlatform Build:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    set -e
    # If we're already inside this script then die
    if [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then
    exit 0
    fi
    export RW_MULTIPLATFORM_BUILD_IN_PROGRESS=1
    RW_FRAMEWORK_NAME="YXPlayerSDKBinary"
    RW_INPUT_STATIC_LIB="lib${RW_FRAMEWORK_NAME}.a"
    RW_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework"
    function build_static_library {
    # Will rebuild the static library as specified
    # build_static_library sdk
    xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \
    -target "${TARGET_NAME}" \
    -configuration "${CONFIGURATION}" \
    -sdk "${1}" \
    ONLY_ACTIVE_ARCH=NO \
    BUILD_DIR="${BUILD_DIR}" \
    OBJROOT="${OBJROOT}" \
    BUILD_ROOT="${BUILD_ROOT}" \
    SYMROOT="${SYMROOT}" $ACTION
    }
    function make_fat_library {
    # Will smash 2 static libs together
    # make_fat_library in1 in2 out
    xcrun lipo -create "${1}" "${2}" -output "${3}"
    }
    # Extract the platform (iphoneos/iphonesimulator) from the SDK name
    if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then
    RW_SDK_PLATFORM=${BASH_REMATCH[1]}
    else
    echo "Could not find platform name from SDK_NAME: $SDK_NAME"
    exit 1
    fi
    # Extract the version from the SDK
    if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then
    RW_SDK_VERSION=${BASH_REMATCH[1]}
    else
    echo "Could not find sdk version from SDK_NAME: $SDK_NAME"
    exit 1
    fi
    # Determine the other platform
    if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then
    RW_OTHER_PLATFORM=iphonesimulator
    else
    RW_OTHER_PLATFORM=iphoneos
    fi
    # Find the build directory
    if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$RW_SDK_PLATFORM$ ]]; then
    RW_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}"
    else
    echo "Could not find other platform build directory."
    exit 1
    fi
    # Build the other platform.
    build_static_library "${RW_OTHER_PLATFORM}${RW_SDK_VERSION}"
    # If we're currently building for iphonesimulator, then need to rebuild
    # to ensure that we get both i386 and x86_64
    if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
    build_static_library "${SDK_NAME}"
    fi
    echo "input_1 : ${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}"
    echo "input_2 : ${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}"
    echo "output : ${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
    # Join the 2 static libs into 1 and push into the .framework
    make_fat_library "${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
    "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
    "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
    # Ensure that the framework is present in both platorm's build directories
    cp -a "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}" \
    "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}"
    # Copy the lib into the private pod/lib
    ditto "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}" \
    "${SRCROOT}/../${PROJECT_NAME}/Products/lib/${RW_INPUT_STATIC_LIB}"
    # Copy the framework to the pod/framework
    ditto "${RW_FRAMEWORK_LOCATION}" \
    "${SRCROOT}/../${PROJECT_NAME}/Products/Framework/${RW_F
    RAMEWORK_NAME}.framework"
    # Copy the resources bundle to pod/Assets
    ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
    "${SRCROOT}/../${PROJECT_NAME}/Assets/${RW_FRAMEWORK_NAME}.bundle"

    从脚本上最后可以看出,我们把构建出来的二进制文件放在 YXPlayerSDK 组件文件下的Products 文件中,所以在构建之前,我们先在 YXPlayerSDK 组件文件下增添一个 Products 文件存放 .a 和 .frameworky 以及它们的头文件,目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    YXPlayerSDK
    ├── Assets
    ├── Classes
    └── Products
    ├── Framework #存放 Framework 文件
    ├── include #存放相关头文件
    └── lib #存放 .a 文件

    选择Framework集合方案(aggregate scheme),按下cmd+B编译该framework。构建完成之后,就能看到

    Products 文件夹下添加完之后的目录结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Products
    ├── Framework
    │   └── YXPlayerSDKBinary.framework
    │   ├── Headers -> Versions/Current/Headers
    │   ├── Versions
    │   │   ├── A
    │   │   │   ├── Headers
    │   │   │   │   ├── DoubboViewController.h
    │   │   │   │   └── TestViewController.h
    │   │   │   └── YXPlayerSDKBinary
    │   │   └── Current -> A
    │   └── YXPlayerSDKBinary -> Versions/Current/YXPlayerSDKBinary
    ├── include
    │   ├── DoubboViewController.h
    │   └── TestViewController.h
    └── lib
    └── libYXPlayerSDKBinary.a

3、打包 Bundle 资源

打开 YXPlayerSDK工程,点击Add Target按钮添加新的 target,导航到OS X/Framework and Library/Bundle。将新的Bundle命名为YXPlayerSDKResources,这里需要配置几个编译设置:

  • 因为正在创建一个在iOS上使用的bundle,这与默认的OS X不同。选择YXPlayerSDKResources目标,然后点击Build Settings栏,搜索base sdk,选择Base SDK这一行,按下delete键,这一步将OS X切换为iOS。

  • 同时你需要将工程名称改为YXPlayerSDKResources。搜索product name,双击进入编辑模式,将${TARGET_NAME}替换为YXPlayerSDKResources。

  • 默认情况下,有两种resolutions的图片可以产生一些有趣的现象。例如,当导入一个retina @2x版本的图片时,普通版的和Retina版的将会合并成一个多resolution的TIFF(标签图像文件格式,Tagged Image File Format)。这不是一件好事。搜索hidpi将COMBINE_HIDPI_IMAGES设置为NO。

  • 确保编译framework时,bundle也能被编译并将framework作为依赖添加到集体目标中。选中Framework目标,选择Build Phases栏,展开Target Dependencies面板,点击 + 按钮,选择RWUIControlsResources目标将其添加为依赖。

  • 在Framework目标的Build Phases中,打开MultiPlatform Build面板,在脚本的最后添加下述代码,这条指令将拷贝构建好的bundle指定位置:

    1
    2
    3
    # Copy the resources bundle to pod/Assets
    ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
    "${SRCROOT}/../${PROJECT_NAME}/Assets/${RW_FRAMEWORK_NAME}.bundle"

4、解决二进制化的依赖

使用源码的话,依赖第三方库或私有库,在 podspec 文件添加 s.dependency 就行,编译的时候 CocoaPods 会自己做好依赖的 link,但二进制化,是我们自己先编译好 .a 或 .framework,这种依赖怎么做呢?

1、在 YXPlayerSDK 组件工程里的 Podfile 添加如下:
1
2
3
4
target 'YXPlayerSDKBinary' do
pod '三方库'
pod '私有库'
end
2、在 podspec 文件添加如下,二进制化如果使用的是 .a 就是设置 vendored_libraries ,是 .framework 就设置 vendored_frameworks,如下:
1
2
3
4
5
6
7
8
9
//使用 .a
s.ios.vendored_libraries = 'YXPlayerSDK/Products/lib/*{a}'
s.dependency '三方库'
s.dependency '私有库
//使用 .framework
s.ios.vendored_frameworks = 'YXPlayerSDK/Products/Framework/*{framework}'
s.dependency '三方库'
s.dependency '私有库

注意一下,Podfile 里面依赖库的要和 podspec 依赖的库保持一致。

3、为了方便调试,加入环境变量 IS_SOURCE 实现Pod私有库源码和二进制的其换,完整的 YXPlayerSDK.podspec 如下:
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
Pod::Spec.new do |s|
s.name = 'YXPlayerSDK'
s.version = '0.1.6'
s.summary = 'YXPlayerSDK'
s.description = <<-DESC
YXPlayerSDK.
DESC
s.homepage = 'http://git.jc/elephant-ios-component/YXPlayerSDK.git'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'muhuashanjin' => 'tanghy@gold-finance.com.cn' }
s.source = { :git => "http://git.jc/elephant-ios-component/YXPlayerSDK.git", :tag => "0.1.6" }
s.ios.deployment_target = '8.0'
if ENV['IS_SOURCE']
#------------ source code -------------
s.resource_bundles = {'YXPlayerSDK' => ['YXPlayerSDK/Assets/*.png']}
s.public_header_files = 'YXPlayerSDK/Classes/**/*.h'
s.source_files = 'YXPlayerSDK/Classes/**/*.{h,m}'
s.dependency 'PLPlayerKit','~> 2.4.3'
s.dependency 'SVProgressHUD'
s.dependency 'YXBase', '~> 0.1.9'
else
#------------ compile code -------------
s.resource_bundles = {'YXPlayerSDK' => ['YXPlayerSDK/Assets/YXPlayerSDKBinary.bundle/*.png']}
s.source_files = 'YXPlayerSDK/Products/include/**'
s.public_header_files = 'YXPlayerSDK/Products/include/*.h'
s.ios.vendored_libraries = 'YXPlayerSDK/Products/lib/*{a}'
# s.ios.vendored_frameworks = 'YXPlayerSDK/Products/Framework/*{framework}'
s.dependency 'PLPlayerKit','~> 2.4.3'
s.dependency 'SVProgressHUD'
s.dependency 'YXBase', '~> 0.1.9'
end
#------------ subspec -------------
#s.subspec 'TestOne' do |testOne|
# testOne.source_files = 'YXPlayerSDK/Classes/testOne/*'
# testOne.public_header_files = 'YXPlayerSDK/Classes/testOne/**/*.h'
# testOne.dependency 'AFNetworking', '~> 3.1.0'
#end
#s.subspec 'TestTwo' do |testTwo|
# testTwo.source_files = 'YXPlayerSDK/Classes/testTwo/*'
# testTwo.public_header_files = 'YXPlayerSDK/Classes/testTwo/**/*.h'
# testTwo.dependency 'YXPlayerSDK/TestOne'
#end
end

使用源码编译命令 :IS_SOURCE=1 pod install

使用二进制编译命令:pod install

4、配置完依赖库后,执行 pod install 加载依赖库,然后按下cmd+B 重新编译该framework,可能会遇到以下报错:
  • 1
    pod error: The 'xxxx ' target has transitive dependencies that include static binaries ...

    这是因为 Podfile 中加上 use_frameworks!,开启这个选项之后,所有以源码引入的pod都会编译成动态链接库,而如果依赖的Pod里面的库包含静态库,这样就造成 pod install 时会把静态库编译到App里面,源码编译成的动态库没法依赖它,出现上面的错误。

    解决方法:Pod私有库不以源码导入,而是二进制化后导入,再 pod install。

  • 1
    2
    build error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool: can't locate file for: -lPods-YXPlayerSDKBinary
    build error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool: file: -lPods-YXPlayerSDKBinary is not an object file (not allowed in a library)

    这是因为执行 pod install ,整个项目按照先编译被依赖Pod,然后依赖其他Pod的Pod也被构建出来,最终所有的组件被编译为一个lib-Pods-XXX.a 被添加进项目进去。

    解决方法:去掉 Build Phases 的 Link Binary With Libraries 中的 libPods-YXPlayerSDKBinary.a,然后重新编译。

5、调试二进制化的Pod私有库

接下来就可以在组件示例工程中引入调试,先本地引入调试,在 YXPlayerSDK 的 Podfile 中添加如下:

1
2
3
target 'YXPlayerSDK_Example' do
pod 'YXPlayerSDK', :path => '../'
end

运行成功之后,就可以推送到git仓库,打上tag,然后验证Pod私有库,推送到远程私有Spec Repo,再引入调试:

1
2
3
target 'YXPlayerSDK_Example' do
pod 'YXPlayerSDK', '~> 0.1.5'
end

完成之后,脱离组件示例工程再新建一个项目Demo,pod 引入私有库,注意几个问题:

  • 私有库二进制化后,Podfile 中需要开启 use_frameworks!,不然报错如下:
1
2
3
ld: warning: directory not found for option '-F/Users/thy/Desktop/Demo/Pods/YXPlayerSDK/YXPlayerSDK/Products/Framework'
ld: framework not found xxxxxx
clang: error: linker command failed with exit code 1 (use -v to see invocation)
  • Pod私有库,二进制化使用的是 framework, 如果编译项 OTHER_LDFLAGS 中为 -l”YXPlayerSDKBinary” ,需要改为-framework “YXPlayerSDKBinary”,反之亦然,不然报错如下:
1
2
3
4
5
6
7
8
9
#改为 -framework "YXPlayerSDKBinary"
ld: warning: directory not found for option '-F/Users/thy/Desktop/Demo/Pods/YXPlayerSDK/YXPlayerSDK/Products/Framework'
ld: library not found for -lYXPlayerSDKBinary
clang: error: linker command failed with exit code 1 (use -v to see invocation)
#改为 -l"YXPlayerSDKBinary"
ld: warning: directory not found for option '-F/Users/thy/Desktop/Demo/Pods/YXPlayerSDK/YXPlayerSDK/Products/Framework'
ld: framework not found YXPlayerSDKBinary
clang: error: linker command failed with exit code 1 (use -v to see invocation)

示范例代码:Private-Pod-Example

参考

iOS开发—创建你自己的Framework

CocoaPods组件平滑二进制化解决方案

Objective-C和Swift混编的一些经验

Project.pbxproj的剖析

发表于 2017-10-09

project.pbxproj

  • 介绍

    project.pbxproj 文件被包含于 Xcode 工程文件 .xcodeproj 之中,存储着 Xcode 工程的各项配置参数。它本质上是一种旧风格的 Property List 文件,历史可追溯到 NeXT 的 OpenStep,其可读性不如 xml 和 json。Property List 在苹果家族的历史上存在三种格式:OpenStep,XML 和 Binary,*除了 OpenStep 被废弃不支持写入以外,其余格式都提供 API 支持读写。该文件以明确的编码信息开头,通常是UTF-8。这意味着该文件在其开始时不能承载BOM(Byte Ordering Mark:即字节顺序标记,它是插入到以UTF-8、UTF16或UTF-32编码Unicode文件开头的特殊标记,用来识别Unicode文件的编码类型。对于UTF-8来说,BOM并不是必须的,因为BOM用来标记多字节编码文件的编码类型和字节顺序(big-endian或little-endian),否则解析将失败。与 json 最明显的差别是:数组用小括号括起来并用逗号隔开元素;字典用大括号括起来并用分号隔开键值对,键值之间用等号连接;二进制数据用尖括号 <> 括起来:

    1
    2
    3
    4
    5
    6
    7
    数组:
    project.pbxproj => ( "1", "2", "3" )
    json => [ "1", "2", "3" ]
    字典:
    project.pbxproj => { "key" = "value"; ... }
    json => { "key" : "value", ... }

    对 Xcode 工程进行添加、删除文件或者配置编译参数等,实际上都是对 project.pbxproj文件的修改。使用 git diff 查看所做文件的修改,会发现唯一被修改的就是这个 project.pbxproj 文件,其他的文件并没有被修改。

  • 解析文件格式

    根元素总共有 5 个键值对,Key 分为:archiveVersion,classes,objectVersion,objects 和 rootObject。上图中所有的配置对象都在 objects 中,对象的Key 都为 UUID,对应的 Value 可当做是一个字典。

    这里的 key 是 UUID 作为交叉引用的索引,保证每个配置信息对象的唯一性。因为 UUID 根据主机Mac地址或(伪)随机数等加上时间戳生成(ps:UUID生成规则),避免了多人在同一时间段操作修改工程文件带来的问题。也就是说工程中每项配置对象都有个唯一的 UUID,然后其他配置对象想引用某个配置对象直接使用它的 UUID 即可。这就跟我们编程时使用指针指向某个对象的地址一样,其他对象的属性想引用它,只需要给属性传个指针地址就行了。

    文件的引用由 PBXBuildFile 中属性 fileRef 指向 PBXFileReference,它有一个path属性标明文件路径,但需要注意的是这里的 path 显示的路径可能不是项目目录下的全路径,只是显示文件名,要得到文件全路径,需要使用 PBXFileReference 的 UUID 索引到 PBXGroup、PBXReferenceProxy等中一层层递归向上搜索路径来拼接成全路径。

    objects 的键值对根据内容类型被分成了若干个 section,采用注释的方式分节也使得可读性更强。section 的数量跟工程有关,尤其是每个工程的 BuildPhase 和 Target 差别都很大,section 列表就是上图中objects后的分支结构,显示在 Xcode 中能看见所有的公共配置信息都存在于 project.pbxproj 中。主要包含跟文件相关的 BuildFile,Group 和 FileReference,跟编译相关的 BuildPhase 和 Build Configuration(List),以及一些列 Target 和 TargetDependency等。

    着重介绍几个属性:

  • PBXProject

    PBXProject标识着整个工程,由根元素的rootObject引入。

  • PBXBuildFile

    PBXBuildFile是文件类,被PBXBuildPhase等作为文件包含或被引用的资源。

  • PBXFileReference

    PBXFileReference用于跟踪项目引用的每一个外部文件,比如源代码文件、资源文件、库文件、生成目标文件等。

  • PBXGroup

    PBXGroup用于组文件,或者嵌套组, 是 Xcode 中用来组织文件的一种方式,它对文件系统没有任何影响,无论你创建或者删除一个 Group,都不会导致 folder 的增加或者移除。 当然如果在你删除时选择 Move to Trash 就是另外一说了。

    在 Group 中的文件的关系, 不会与 folder 中的有什么冲突, 它只是 Xcode 为你提供的一种分离关注的方式。Group 之间的关系, 也是在 project.pbxproj 中定义的,这个文件中包含了 Xcode 工程中所有 File 和 Group 的关系。

    Group 在我们的工程中就是黄色的文件夹,而 Folder 是蓝色的文件夹(一般在 Xcode 工程中, 我们不会使用 Folder)。

  • PBXNativeTarget

    PBXNativeTarget就是工程中的Target ,它指定了一个用于产品(product), 并且包含了从工程中的一些文件中构建产品的命令。这些命令使用构建设置(build settings)和构建阶段(build phases)的方式来组织, 可以在 Xcode 编辑器中改变这些设置。如果工程中有多个target,都会在这个section中有所体现。

  • PBXSourcesBuildPhase

    PBXSourcesBuildPhase用于构建阶段中编译源文件。

  • PBXResourcesBuildPhase

    PBXResourcesBuildPhase用于构建阶段需要复制的资源文件。

    文件格式具体分析详见 Xcode Project File Format。

  • 操作 Property List 的途径

    plutil命令提供处理 Property list 文件的能力,将Property list 文件转成 XML 格式命令如下:

    1
    $ plutil -convert xml1 -s -r -o project.pbxproj.xml project.pbxproj

    在 Cocoa 的 NSPropertyListSerialization 也提供了类似的功能,更面向对象,其实 plutil 和 NSPropertyListSerialization 底层都是调用 CoreFoundation 的CFPropertyList 相关的 API,所以功能类似。

    但两者在读入 project.pbxproj 文件时,字典中键值对的顺序会跟文件中原始的顺序不一致。这是因为字典为了实现快速查找会将 key 按序存储(比如字典序或用红黑树排序)。

    大多数操作 project.pbxproj 文件工具的原理,也就是利用 plutil 转成 json 或 xml 后进行处理。

  • 操作 project.pbxproj 文件

    Xcodeproj 是一个使用 Ruby 来创建和修改 Xcode 工程文件的工具. 属于Cocoapods的一个组件,Cocoapods 就是利用Xcodeproj组件,脚本化的管理任务和构造友好的Xcode库,它同时支持Xcode workspaces (.xcworkspace)、configuration files (.xcconfig)和Xcode Scheme files (.xcscheme)。

    • 安装

      Xcodeproj 通过 RubyGems 安装,命令如下:

      1
      $ [sudo] gem install xcodeproj
    • 源码分析

      API文档地址,这里解析一下 Xcodeproj module 下 几个重要的类:

      • Project

        Xcodeproj 组件用这个 class 来抽象 Xcode project 文件,可用这个类来操作已存在的Xcode project 文件,甚至可以创建一个Xcode project 文件。调用 Project 的 API 返回 AbstractObject 的实例,这个 AbstractObject 的类就是用来包装 project.pbxproj 中的 objects,以此来描述 Xcode project 文件,进行操作。

      • Object

        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
        Object
        ├── AbstractObject
        │ ├── AbstractBuildPhase
        │ ├── PBXCopyFilesBuildPhase
        │ ├── PBXFrameworksBuildPhase
        │ ├── PBXHeadersBuildPhase
        │ ├── PBXResourcesBuildPhase
        │ ├── PBXRezBuildPhase
        │ ├── PBXShellScriptBuildPhase
        │ └── PBXSourcesBuildPhase
        │ ├── AbstractTarget
        │ ├── PBXAggregateTarget
        │ ├── PBXLegacyTarget
        │ └── PBXNativeTarget
        │ ├── AbstractObjectAttribute
        │ ├── PBXBuildFile
        │ ├── PBXBuildRule
        │ ├── PBXContainerItemProxy
        │ ├── PBXFileReference
        │ ├── PBXGroup
        │ ├── PBXVariantGroup
        │ └── XCVersionGroup
        │ ├── PBXProject
        │ ├── PBXReferenceProxy
        │ ├── PBXTargetDependency
        │ ├── XCBuildConfiguration
        │ └── XCConfigurationList

        上面就是Object module 中类的结构图,用 Object module 来抽象文件中的属性,其中的类名和 project.pbxproj 文件中属性名一样,不同在于添加了一些抽象类来管理相同类型的属性,这样设计使得操作属性非常方便,直接操作这些属性类就行。

      • UUIDGenerator

        在源工程 uuid_generator.rb 的文件中,类UUIDGenerator就是用来生成UUID,但与系统生成UUID不同的是,UUIDGenerator 利用文件路径,然后MD5之后来作为UUID。其中在方法 generate_paths 可以看出拼接文件路径的逻辑,有路径的属性就直接利用 path ,没有路径的就根据属性类型按照一定规则拼接路径,然后在方法uuid_for_path中MD5一下路径。

  • 实践

    • 创建 Xcodeproj 工程文件,并保存(这里需要注意的是不管是这里的创建工程,还是对工程的修改最后一定要使用save保存,才会执行对文件的工程的修改)

      1
      2
      # def new(klass)
      Xcodeproj::Project.new("./JCTest.xcodeproj").save
    • 打开 Xcodeproj 文件

      1
      2
      # def self.open(path)
      project=Xcodeproj::Project.open("./JCTest.xcodeproj")
    • 创建 Group 分组,名称为 JCTestGroup,对应的路径为./JCTestGroup

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      # def new_group(name, path = nil, source_tree = :group)
      @param source_tree : SOURCE_TREES_BY_KEY = {
      :absolute => '<absolute>',
      :group => '<group>',
      :project => 'SOURCE_ROOT',
      :built_products => 'BUILT_PRODUCTS_DIR',
      :developer_dir => 'DEVELOPER_DIR',
      :sdk_root => 'SDKROOT',
      }
      JCTestGroup=proj.main_group.new_group("JCTest","./JCTest")
    • Group 分组添加文件引用

      1
      2
      # def new_reference(path, source_tree = :group)
      ref=JCTestGroup.new_reference("JCTest.m")
    • 创建 Target

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # def new_target(type, name, platform, deployment_target = nil, product_group = nil, language = nil)
      @param [Symbol] type : ":application",":framework",":dynamic_library",
      ":static_library"
      @param [String] name : the name of the target product
      @param [Symbol] platform : ":ios" or ":osx"
      @param [String] deployment_target : the deployment target for the platform
      @param [PBXGroup] product_group : the product group, where to add to a file
      reference of the created target.
      @param [Symbol] language : ":objc" or ":swift"
      @return [PBXNativeTarget] the target.
      target = project.new_target(:application,"JCTest",:ios)

      下面是 new_target 方法的内部实现,可以看到初始化 Target、Product、Build phases、Frameworks,那么如果需要对 Xcodeproj 工程文件进行修改,就可以直接操作这些对象得到想到的配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      def self.new_target(project, type, name, platform, deployment_target,
      product_group, language)
      # ----------------- Target -------------------
      # 新建 target 加入到 project.targets 中
      target = project.new(PBXNativeTarget)
      project.targets << target
      target.name = name
      target.product_name = name
      /* 设置 product_type 类型 ,使用 Constants 中的 PRODUCT_TYPE_UTI 获取:
      PRODUCT_TYPE_UTI = {
      :application => 'com.apple.product-type.application',
      :framework => 'com.apple.product-type.framework',
      :dynamic_library => 'com.apple.product-type.library.dynamic',
      :static_library => 'com.apple.product-type.library.static',
      :bundle => 'com.apple.product-type.bundle',
      :octest_bundle => 'com.apple.product-type.bundle',
      :unit_test_bundle => 'com.apple.product-type.bundle.unit-test',
      :ui_test_bundle => 'com.apple.product-type.bundle.ui-testing',
      :app_extension => 'com.apple.product-type.app-extension',
      :command_line_tool => 'com.apple.product-type.tool',
      :watch_app => 'com.apple.product-type.application.watchapp',
      :watch2_app => 'com.apple.product-type.application.watchapp2',
      :watch_extension => 'com.apple.product-type.watchkit-extension',
      :watch2_extension => 'com.apple.product-type.watchkit2-extension',
      :tv_extension => 'com.apple.product-type.tv-app-extension',
      :messages_application => 'com.apple.product-type.application.messages',
      :messages_extension => 'com.apple.product-type.app-extension.messages',
      :sticker_pack => 'com.apple.product-type.app-extension.messages-
      sticker-pack',
      :xpc_service => 'com.apple.product-type.xpc-service',
      }.freeze
      */
      target.product_type = Constants::PRODUCT_TYPE_UTI[type]
      # target 配置信息
      target.build_configuration_list = configuration_list(project, platform,
      deployment_target, type, language)
      # ----------------- Product -------------------
      product = product_group.new_product_ref_for_target(name, type)
      target.product_reference = product
      # ----------------- Build phases -------------------
      target.build_phases << project.new(PBXSourcesBuildPhase)
      target.build_phases << project.new(PBXFrameworksBuildPhase)
      # ----------------- Frameworks -------------------
      framework_name = (platform == :osx) ? 'Cocoa' : 'Foundation'
      target.add_system_framework(framework_name)
      target
      end
    • target 的配置信息 build_configuration_list,其实在创建 Target 的 new_target 方法中已经使用 configuration_list 方法初始化 build_configuration_list 配置信息,使用 set_setting 可以修改 buildSettings 的配置,这里的 key 与 buildSettings 的 key 是一致的:

      1
      2
      3
      # def set_setting(key, value)
      target.build_configuration_list.set_setting('INFOPLIST_FILE',
      "$(SRCROOT)/JCTest/Info.plist")

      看一下 configuration_list 内部实现,可以看出为 section XCBuildConfiguration 初始化 project.targets 中所有的 target 的 Release 和 Debug 的 buildSettings,使用 common_build_settings 方法设置在当前 platform 和 configuration 下的 buildSettings:

      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
      # def self.configuration_list(project, platform = nil, deployment_target = nil, target_product_type = nil, language = nil)
      cl = project.new(XCConfigurationList)
      cl.default_configuration_is_visible = '0'
      cl.default_configuration_name = 'Release'
      release_conf = project.new(XCBuildConfiguration)
      release_conf.name = 'Release'
      release_conf.build_settings = common_build_settings(:release, platform,
      deployment_target, target_product_type, language)
      debug_conf = project.new(XCBuildConfiguration)
      debug_conf.name = 'Debug'
      debug_conf.build_settings = common_build_settings(:debug, platform,
      deployment_target, target_product_type, language)
      cl.build_configurations << release_conf
      cl.build_configurations << debug_conf
      project.build_configurations.each do |configuration|
      next if cl.build_configurations.map(&:name).include?(configuration.name)
      new_config = project.new(XCBuildConfiguration)
      new_config.name = configuration.name
      new_config.build_settings = common_build_settings(configuration.type,
      platform, deployment_target, target_product_type, language)
      cl.build_configurations << new_config
      end
      cl
      end
    • target添加相关的文件引用,这样编译的时候才能引用到:

      1
      2
      3
      4
      5
      6
      # def add_file_references(file_references, compiler_flags = {})
      @param [Array<PBXFileReference>] file_references : the files references of
      the source files that should be added to the target.
      @param [String] compiler_flags : the compiler flags for the source
      files
      target.add_file_references([ref],'-fno-objc-arc')
    • save 保存,对 project 的修改,只有调用 save 之后才会执行

      1
      project.save
    • 以下是利用 Xcodeproj 组件创建一个 Xcodeproj 工程的完整代码,但需要注意的是 Xcodeproj 创建的只是工程的各种配置以及文件的索引,但实体文件需要自己组织好工程目录,与填写在 Xcodeproj 组件的路径一致

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      require 'xcodeproj'
      #创建 JCTest.xcodeproj工程文件,并保存
      Xcodeproj::Project.new("./JCTest.xcodeproj").save
      #打开创建的JCTest.xcodeproj文件
      proj=Xcodeproj::Project.open("./JCTest.xcodeproj")
      #创建一个分组,名称为JCTest,对应的路径为./JCTest
      JCTestGroup=proj.main_group.new_group("JCTest","./JCTest")
      #给JCTest分组添加文件引用
      JCTestGroup.new_reference("AppDelegate.h")
      ref1=JCTestGroup.new_reference("AppDelegate.m")
      ref2=JCTestGroup.new_reference("Assets.xcassets")
      JCTestGroup.new_reference("Base.lproj/LaunchScreen.storyboard")
      JCTestGroup.new_reference("Base.lproj/Main.storyboard")
      JCTestGroup.new_reference("ViewController.h")
      ref4=JCTestGroup.new_reference("ViewController.m")
      #在JCTest分组下创建一个名字为Supporting Files的子分组,并给该子分组添加main和info.plist文件引用
      supportingGroup=JCTestGroup.new_group("Supporting Files")
      ref3=supportingGroup.new_reference("main.m")
      supportingGroup.new_reference("Info.plist")
      #创建target,主要的参数 type: application :dynamic_library framework :static_library 意思大家都懂的
      #name:target名称
      #platform:平台 :ios或者:osx
      target = proj.new_target(:application,"JCTest",:ios)
      #添加target配置信息
      target.build_configuration_list.set_setting('INFOPLIST_FILE', "$(SRCROOT)/JCTest/Info.plist")
      #target添加相关的文件引用,这样编译的时候才能引用到
      target.add_file_references([ref1,ref2,ref3,ref4])
      testGroup=proj.main_group.new_group("JCTestTests","./JCTestTests")
      ref4=testGroup.new_reference("JCTestTests.m")
      supportingGroup=testGroup.new_group("Supporting Files")
      supportingGroup.new_reference("Info.plist")
      #创建test target
      testTarget = proj.new_target(:unit_test_bundle,"JCTestTests",:ios,nil,proj.products_group)
      testRefrence = testTarget.product_reference
      testRefrence.set_explicit_file_type('wrapper.cfbundle')
      testRefrence.name = "JCTestTests.xctest"
      testTarget.add_file_references([ref4])
      #保存
      proj.save
  • 参考

    通过Xcodeproj深入探究Xcode工程文件 一

    通过Xcodeproj深入探究Xcode工程文件 二

    使用代码为 Xcode 工程添加文件

    使用xcodeproj为工程添加framework

    懒人福利:用脚本来修改Xcode工程

    Let’s Talk About project.pbxproj

    pbxprojHelper–Xcode工程文件助手

    Xcodeproj API

    Xcode Project File Format

Pod私有库

发表于 2017-09-25

1、创建私有Spec Repo

Spec Repo是什么?它是所有的Pods的一个索引,就是一个容器,所有公开的Pods都在这个里面,它实际是一个Git仓库remote端在GitHub上,所以想要创建pod私有库,需要创建类似于master的私有Spec Repo,先在远程仓库创建一个工程JCLiveIOSSpecs,然后执行创建私有Spec Repo命令:

1
2
#pod repo add [Private Repo Name][GitHub HTTPS clone URL]
$ pod repo add JCLiveIOSSpecs http://git.jc/elephant-ios-component/JCLiveIOSSpecs.git

完成之后,进入到~/.cocoapods/repos目录下就可以看到JCLiveIOSSpecs这个目录了。

2、创建Pod项目工程文件

从零开始创建一个组件库,可以使用Cocoapods提供的工具Using Pod Lib Create ,先cd到要创建项目的目录然后执行

1
$ pod lib create YXPlayerSDK

然后显示四个问题,1.是否需要一个例子工程;2.选择一个测试框架;3.是否基于View测试;4.类的前缀;4个问题的具体介绍可以去看官方文档。回答完4个问题他会自动执行pod install命令创建项目并生成依赖,项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
YXPlayerSDK
├── Example #demo APP
│ ├── YXPlayerSDK
│ ├── YXPlayerSDK.xcodeproj
│ ├── YXPlayerSDK.xcworkspace
│ ├── Podfile #demo APP 的依赖描述文件
│ ├── Podfile.lock
│ ├── Pods #demo APP 的依赖文件
│ └── Tests
├── LICENSE #开源协议 默认MIT
├── Pod #组件的目录
│ ├── Assets #资源文件
│ └── Classes #类文件
├── YXPlayerSDK.podspec #第三步要创建的podspec文件
└── README.md #markdown格式的README

3、创建私有库pod组件

所需要做的工作就是在相应的 Pods/Developemnt Pods/ 组件 /Classes 下编码,就是向Development Pods文件夹中添加库文件和资源,将编写的组件相关的class放入YXPlayerSDK/Classes中、资源图片文件放入YXPlayerSDK/Assets中,并配置podspec文件,然后进入Example文件夹执行pod install命令,再打开项目工程可以看到,刚刚添加的组件已经在Pods子工程下Development Pods/YXPlayerSDK中了。

向私有库中添加资源(图片、音视频等)方法共有三种:

  • 第一种

    1
    spec.resources = ["Images/*.png", "Sounds/*"]

    但是这些资源会在打包的时候直接拷贝的app的Bundle中,这样说不定会和其它资源产生命名冲突

  • 第二种

    1
    spec.resource = "Resources/MYLibrary.bundle"

    把资源都放在bundle中,然后打包时候这个bundle会直接拷贝进app的mainBundle中。使用的时候在mainBundle中查找这个bundle然后再搜索具体资源

    1
    2
    3
    NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"JZShare" withExtension:@"bundle"];
    NSBundle *bundle = [NSBundle bundleWithURL:bundleURL];
    UIImage *img = [UIImage imageNamed:icon inBundle:bundle compatibleWithTraitCollection:nil];
  • 第三种

    1
    2
    3
    4
    spec.resource_bundles = {
    'MyLibrary' => ['Resources/*.png'],
    'OtherResources' => ['OtherResources/*.png']
    }

    这种方法利用 framework 的命名空间,有效防止了资源冲突。pod会把添加的资源文件编译成bundle,使用方法是先拿到最外面的 bundle,然后再去找下面指定名字 的 bundle 对象,再搜索具体资源,其中需要注意的方法 [NSBundle bundleForClass:<#ClassFromPodspec#>] 返回某个 class 对应的 bundle 对象。具体的:

    • 如果你的 pod 以 framework 形式被链接,那么返回这个 framework 的 bundle。
    • 如果以静态库(.a)的形式被链接,那么返回 client target 的 bundle,即 mainBundle。

    以下是如何访问:

    1
    2
    3
    4
    NSBundle *bundle = [NSBundle bundleForClass:[MYSomeClass class]];
    NSURL *bundleURL = [bundle URLForResource:@"YXPlayerSDK" withExtension:@"bundle"];
    NSBundle *resourceBundle = [NSBundle bundleWithURL: bundleURL];
    UIImage *img = [UIImage imageNamed:icon inBundle:bundle compatibleWithTraitCollection:nil];

    具体分析见 Pod 添加资源文件。

然后提交整个项目代码push到远程分支,并打个tag推送到远程分支,这个tag必须和podspec文件的s.source = { :git => “http://git.jc/elephant-ios-component/YXPlayerSDK.git“, :tag => “0.0.2” }中tag一致。

4、编辑podspec文件

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
Pod::Spec.new do |s|
#名称
s.name = "YXPlayerSDK"
#版本号
s.version = "0.1.0"
#简短介绍
s.summary = "Just Testing."
#详细介绍
s.description = <<-DESC
YXPlayerSDK description
DESC
#主页,这里要填写可以访问到的地址,不然验证不通过
s.homepage = "http://git.jc/elephant-ios-component/YXPlayerSDK.git"
#截图
# s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2"
#开源协议
s.license = 'MIT'
#作者信息
s.author = { 'muhuashanjin' => 'tanghy@gold-finance.com.cn' }
#项目地址,这里不支持ssh的地址,验证不通过,只支持HTTP和HTTPS,最好使用HTTPS
s.source = { :git => "http://git.jc/elephant-ios-component/YXPlayerSDK.git", :tag => "0.0.2" }
#多媒体介绍地址
# s.social_media_url = 'https://twitter.com/<twitter_username>'
#支持的平台及版本
s.platform = :ios, '8.0'
#是否使用ARC,如果指定具体文件,则具体的问题使用ARC
s.requires_arc = true
#代码源文件地址,**/*表示Classes目录及其子目录下所有文件,如果有多个目录下则用逗号分开,如果需要在项目中分组显示,这里也要做相应的设置
s.source_files = 'YXPlayerSDK/Classes/**/*'
#资源文件地址
s.resource_bundles = {
'YXPlayerSDK' => ['YXPlayerSDK/Assets/*.{storyboard,xib,xcassets,json,imageset,png}']
}
#公开头文件地址
s.public_header_files = 'YXPlayerSDK/Classes/**/*.h'
#该pod依赖的系统framework,多个用逗号隔开
s.frameworks = 'UIKit','CoreGraphics'
#该pod依赖的系统library,多个用逗号隔开
s.libraries = 'iconv','sqlite3','stdc++','z'
#第三方.a文件
s.vendored_libraries = 'YXPlayerSDK/Classes/ThirdParty/*.{a}'
#第三方frameworks文件
s.vendored_frameworks = ['YXPlayerSDK/Classes/BaiduMap_IOSSDK_v3.0.0_Lib/BaiduMapAPI_Base.framework',
'YXPlayerSDK/Classes/BaiduMap_IOSSDK_v3.0.0_Lib/BaiduMapAPI_Location.framework']
#依赖关系,该项目所依赖的其他库,如果有多个需要填写多个s.dependency
s.dependency 'AFNetworking', '~> 2.3'
end

这里强调一下 s.summary 必填,s.description 不是必填,但如果填写了 s.description,它的长度必须比 s.summary 长且字符串不能一样。

如果要添加更多的模块到YXPlayerSDK之中,包括工具类,底层Model及类目扩展等,就需要subspec功能,给YXPlayerSDK创建了多个子分支。

具体做法是先将源文件添加到YXPlayerSDK/Classes中,然后按照不同的模块对文件目录进行整理,假如我有2个模块,所以在YXPlayerSDK/Classes下会创建了2个子目录,注释podspec文件的s.source_files和s.dependency,修改如下:

1
2
3
4
5
6
7
8
9
10
11
s.subspec 'TestOne' do |testOne|
testOne.source_files = 'YXPlayerSDK/Classes/testOne/*'
testOne.public_header_files = 'YXPlayerSDK/Classes/testOne/**/*.h'
testOne.dependency 'YXPlayerSDK/testTwo'
end
s.subspec 'TestTwo' do |testTwo|
testTwo.source_files = 'YXPlayerSDK/Classes/testTwo/*'
testTwo.public_header_files = 'YXPlayerSDK/Classes/testTwo/**/*.h'
testTwo.dependency 'YXPlayerSDK/TestOne'
end

完成后,进入Example文件夹执行pod install命令,会看到目录Development Pods/YXPlayerSDK下多了两个文件夹TestOne和TestTwo,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
YXPlayerSDK
├── YXPlayerSDK
│ ├── Classes
│ ├── TestOne
│ └── TestTwo
├── TestTwo
├── Resources
└── Support Files
||
pod install 之后,目录结构变化
||
YXPlayerSDK
├── TestOne
│ ├── YXPlayerSDK
│ ├── Classes
│ ├── TestOne
├── TestTwo
├── Resources
└── Support Files

由此创建了subspec所以项目整体的依赖dependency,源文件source_files,头文件public_header_files,资源文件resource等都移动到了各自的subspec中,每个subspec之间也可以有相互的依赖关系(只能单向依赖)如:

1
testTwo.dependency 'YXPlayerSDK/TestOne'

这样分为多个subspec,在项目pod就可以使用其中的某个subspec了,如下:

1
2
3
pod 'YXPlayerSDK/TestOne'
或者
pod 'YXPlayerSDK',:subspecs=>['TestOne','TestTwo']

如果已经有了现成的项目,那么就需要给这个项目创建一个podspec文件,创建它需要执行Cocoapods的另外一个命令:

1
$ pod spec create PodTestLibrary git@coding.net:wtlucky/podTestLibrary.git

5、提交pod组件到Spec Repo中

先验证编辑好的podsepc文件,如果因为有无关紧要的警告而未通过检查,则输入以下命令加上--allow-warnings ,如果想要输出编译过程,加上--verbose:

1
$ pod lib lint --allow-warnings --verbose

如果私有库里面引用静态库会导致验证是无法通过的,报错 include of non-modular header inside framework module ··· [-Werror,-Wnon-modular-include-in-framework-module] ,加上--use-libraries。

然后还可以本地测试podspec文件,创建一个新的项目,在这个项目的Podfile文件中直接指定刚才创建编辑好的podspec文件,看是否可用, 在Podfile中可以这样编辑:

1
pod 'YXPlayerSDK', :path => '~/Desktop/git/JC_LivePod/YXPlayerSDK' #指定路径

编辑完成后,执行pod install命令安装依赖,可以看到YXPlayerSDK跟测试项目一样存在于Development Pods/PodTestLibrary中。

没有自身的WARNING或者ERROR之后,就可以再次提交到Spec Repo中,命令如下:

1
$ pod repo push JCLiveIOSSpecs YXPlayerSDK.podspec --allow-warnings

之后到~/.cocoapods/repos/JCLiveIOSSpecs目录下查看,也使用pod search命令查看自己库:

1
2
3
4
5
6
7
8
$ pod search YXPlayerSDK
-> YXPlayerSDK (0.0.4)
YXPlayerSDK
pod 'YXPlayerSDK', '~> 0.0.4'
- Homepage: http://git.jc/elephant-ios-component/YXPlayerSDK.git
- Source: http://git.jc/elephant-ios-component/YXPlayerSDK.git
- Versions: 0.0.4, 0.0.2, 0.0.1 [JCLiveIOSSpecs repo]
(END)

这里特别强调一下,如果 s.dependency 中依赖的是 pod 私有库的话,验证和提交命令都需要指定 source :

1
2
3
4
5
#验证
pod lib lint --sources=http://git.jc/elephant-ios-component/JCLiveIOSSpecs.git,https://github.com/CocoaPods/Specs.git --allow-warnings
#提交
pod repo push JCLiveIOSSpecs YXPlayerSDK.podspec --sources=http://git.jc/elephant-ios-component/JCLiveIOSSpecs.git,https://github.com/CocoaPods/Specs.git --allow-warnings

6、项目中导入pod私有库组件

当你执行 pod install 的时候,CocoaPods 默认只会在master下搜索,而我们的spec是存在我们私有的JCLiveIOSSpecs目录下的,所以需要在Podfile中指定搜索路径,在文件顶部中如下两行代码:

1
2
source "https://github.com/CocoaPods/Specs.git" #官方仓库地址
source "http://git.jc/elephant-ios-component/JCLiveIOSSpecs.git" #私有仓库地址

在指定pod的私有库组件时有一个坑,如pod ‘YXBase’, ‘~> a.b.1’,cocoaPods实际pod组件的版本为a.b.x(x为当前版本库中最大值)。但如何pod指定版本,这就需要修改Podfile.lock文件中PODS:YXBase的版本号和Podfile文件中YXBase版本号一致

7、更新本地cocoapods的spec资源配置信息

pod install 时报错:

1
None of your spec sources contain a spec satisfying the dependencies: AFNetworking (~> 3.1.0), AFNetworking (= 3.1.0)

这句话的意思是说:你spec资源中不包含AFNetworking的3.1.0的配置信息。这里面有个关键词,spec资源和配置信息。所以需要更新本地cocoapods的spec资源配置信息,使用命令:

1
$ pod repo update

但pod repo update实际是更新整个.cocoapods下的所有库,其实我们可以只更新其中某个库来达到快速可用的目的。下面提供两个方法解决:

(1)正规方法:
指定更新单独库pod repo update /Users/<user>/.cocoapods/repos/master/Specs/<lib name>

(2)野路子:
如果方法1仍然无法解决问题,而又着急使用。可以直接到相应目录下手动增加缺少的版本目录和spec文件,/Users/<user>/.cocoapods/repos/master/Specs/<lib name>/3.2.0/<lib name>.spec。spec文件参考git上相应库的版本。

8、Podfile文件中use_frameworks!

  • 在Podfile文件里不使用 use_frameworks! 则是会生成相应的 .a文件(静态链接库),通过 static libraries 这个方式来管理pod的代码。Linked:libPods-xxx.a包含了其它用pod导入的第三方框架的.a文件
  • 在Podfile文件里使用了use_frameworks! 则cocoapods 会生成相应的 .frameworks文件(动态链接库:实际内容为 Header + 动态链接库 + 资源文件),使用 dynamic frameworks 来取代 static libraries 方式。Linked:Pods_xxx.framework包含了其它用pod导入的第三方框架的.framework文件。

  • 用cocoapods 导入swift 框架 到 swift项目和OC项目都必须要 use_frameworks!

  • 使用 dynamic frameworks,必须要在Podfile文件中添加 use_frameworks!

    具体分析地址:Podfile中的 use_frameworks!

9、Podfile和target

Podfile本质上是用来描述Xcode工程中的targets用的。如果不显式指定Podfile对应的target,CocoaPods会创建一个名称为default的隐式target,会和工程中的第一个target相对应。换句话说,如果在Podfile中没有指定target,那么只有工程里的第一个target能够使用Podfile中描述的Pods依赖库。

如果想在一个Podfile中同时描述project中的多个target,根据需求的不同,可以有不同的实现方式:

  • 多个target中使用相同的Pods依赖库

    1
    2
    3
    4
    5
    6
    7
    link_with 'target1','target2'
    platform :ios
    pod 'SDWebimage', '~> 1.1.1'
    platform :ios ,'7.0'
    pod 'AFNetworking', '~> 2.3.1'
  • 不同的target使用完全不同的Pods依赖库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    target :'target1' do
    platform :ios
    pod 'SDWebimage', '~> 1.1.1'
    end
    target :'target2' do
    platform :ios ,'7.0'
    pod 'AFNetworking', '~> 2.3.1'
    end
    其中,do/end作为开始和结束标识符。

参考

Cocoapods使用私有库中遇到的坑

SQLCipher 让数据库不再裸奔

发表于 2017-07-08

SQLCipher 让数据库不再裸奔

一、 数据库加密,不加密,这是个问题

数据存储是基本上每个移动APP都需要使用的,在移动应用平台android/iOS上系统都集成了 免费版 的轻量级数据库SQLite,但是,免费版的SQLite有一个致命缺点:不支持加密。意味着,你app上的数据库在裸奔!特别是在iOS8.3之前,未越狱的系统也可以通过工具拿到应用程序沙盒里面的文件,后面也有蒸米绕过非越狱手机沙盒读取微信数据库,为了安全考虑,就需要对数据库进行加密。
​

二、 加密方式

对数据库加密的思路有两种:

  1. 将内容加密后再写入数据库

    这种方式使用简单,在入库/出库只需要将字段做对应的加解密操作即可,一定程度上解决了将数据赤裸裸暴露的问题。但也有很大弊端:

    • 这种方式并不是彻底的加密,还是可以通过数据库查看到表结构等信息。
    • 对于数据库的数据,数据都是分散的,要对所有数据都进行加解密操作会严重影响性能。
  2. 对数据库文件加密
    ​
    将整个数据库整个文件加密,这种方式基本上能解决数据库的信息安全问题。目前已有的SQLite加密基本都是通过这种方式实现的。iOS平台可用的SQLite加密工具:

    • SQLite Encryption Extension (SEE)
      事实上SQLite有加解密接口,只是免费版本没有实现而已。而SQLite Encryption Extension (SEE)是SQLite的加密版本,收费的
    • SQLiteEncrypt
      使用AES加密,其原理是实现了开源免费版SQLite没有实现的加密相关接口,SQLiteEncrypt是收费的。
    • SQLiteCrypt
      使用256-bit AES加密,其原理和SQLiteEncrypt一样,都是实现了SQLite的加密相关接口,SQLiteCrypt也是收费的。
    • SQLCipher
      需要说明的是,SQLCipher是完全开源的,代码托管在Github上。SQLCipher使用256-bit AES加密,由于其基于免费版的SQLite,主要的加密接口和SQLite是相同的,但也增加了一些自己的接口,详情见官网。
      SQLCipher页也分为收费版本和免费版本,官网介绍说收费版只是集成起来更简单,不用再添加OpenSSL依赖库,而且编译速度更快,从功能上来说没有任何区别。

综上:果断选择免费版本SQLCipher作为数据库加密工具。

三、 SQLCipher

1. 特性

官网介绍特性:

1
2
3
4
5
- 快速只有5 - 15%的性能开销加密
- 100%的数据库中的数据文件是加密的
- 使用良好的安全模式(CBC模式,密钥推导)
- 零配置和应用程序级加密
- OpenSSL加密库提供算法

2. 特性探究

  • 数据库操作速度

图(1)、图(2)是官方给出的Demo测试出来的,图(1)中数据表没有设置索引,图(2)中数据表设置了索引,图(3)在官方Demo中增加的一组多线程下处理事务的测试

  • 加密解密过程

    SQLite数据库设计中考虑了安全问题并预留了加密相关的接口。但是并没有给出实现。SQLite 数据库源码中通过使用SQLITE_HAS_CODEC宏来控制是否使用数据库加密。并且预留了四个结构让用户自己实现以达到对数据库进行加密的效果。这四个接口分别是:
    sqlite3_key(): 指定数据库使用的密钥
    sqlite3_rekey():为数据库重新设定密钥;
    sqlite3CodecGetKey():返回数据库的当前密钥
    sqlite3CodecAttach(): 将密钥及页面编码函数与数据库进行关联。

    而sqlcipher就是实现这四个接口以及自己的一些接口,从源码入手看一下加解密过程:
    

    定位创建数据库时设置数据库密码的函数:

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL)setKeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
if (!keyData) {
return NO;
}
int rc = sqlite3_key(_db, [keyData bytes], (int) [keyData length]);
return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
return NO;
#endif
}

查找sqlite3_key( ),进入sqlite3.c文件,找到sqlite3CodecAttach,这个函数内部做了几个重要的事情:

  1. 初始化sqlcipher,以及在sqlcipher_openssl_setup中设置加密算法:

    sqlcipher_openssl_random:盐值 (randnum)
    sqlcipher_openssl_kdf:hash (sha1)
    sqlcipher_openssl_cipher:aes (aes-256)

  2. sqlcipher_codec_ctx_init,最核心的东西就是这个函数调用,将上层传进来的初始密码与数据库的指针通过一个codec_ctx的结构体绑定起来了,codec_ctx的结构定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct codec_ctx {
    int kdf_salt_sz;
    int page_sz;
    unsigned char *kdf_salt;
    unsigned char *hmac_kdf_salt;
    unsigned char *buffer;
    Btree *pBt;
    cipher_ctx *read_ctx;
    cipher_ctx *write_ctx;
    unsigned int skip_read_hmac;
    unsigned int need_kdf_salt;
    };

    这个codec_ctx实际上就类似于每次打开sqlcipher数据库后,可以对数据库操作的句柄,在数据库未关闭之前,一直在维护在内存中,为后续的增删改查操作使用。其中的ciper_ctx比较关键,就是它持有了我们输入的原始密码。

  3. sqlite3pager_sqlite3PagerSetCodec,将密钥及页面编码函数与数据库进行关联

接下来看看密码是怎样被处理,既然知道算法指针kdf、cipher以及初始密码pass指针,全局搜索”->kdf、->cipher、->pass”查找所有使用到访问这些指针的地方,定位到sqlite3Codec便是处理密钥推导、加密数据库内容的地方。注意到sqlcipher_cipher_ctx_key_derive和sqlcipher_page_cipher函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void* sqlite3Codec(void *iCtx, void *data, Pgno pgno, int mode) {
...
//call to derive keys if not present yet
if((rc = sqlcipher_codec_key_derive(ctx)) != SQLITE_OK) {
sqlcipher_codec_ctx_set_error(ctx, rc);
return NULL;
}
...
...
if(pgno == 1) memcpy(buffer, SQLITE_FILE_HEADER, FILE_HEADER_SZ); //copy file header to the first 16 bytes of the page
rc = sqlcipher_page_cipher(ctx, CIPHER_READ_CTX, pgno, CIPHER_DECRYPT, page_sz - offset, pData + offset, (unsigned char*)buffer + offset);
..
}
从注释看来,sqlcipher_codec_key_derive推导密钥,作用是实现原始密码到真正的加密KEY的转化,即HASH加盐以及AES;sqlcipher_page_cipher则是对数据页加密,看来找到加密的关键入口了。

由此可以总结出数据库加解密的核心逻辑,解密的操作是以page为单位进行的(每一个page就是db文件的每1024个字节),流程如下:

3. 集成

  • 途径:
    1. 如果你使用cocoapod的话就不需要自己配置了,为了方便,我们直接使用FMDB进行操作数据库,FMDB也支持SQLCipher :
      pod ‘FMDB/SQLCipher’
    2. 做依赖工程引入,详见官网,有详细图文说明
  • 使用:
    1. 重写FMDatabase里面的- (BOOL)open和- (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName 方法,增加设置密码的操作[self setKey:kDBencryptKey],其中的kDBencryptKey为数据库密钥,通过写一个类目来实现,减小对现有代码的入侵,减低耦合。
    2. 对app中已经存在的数据库,需要先对对数据库进行加密,实现一个数据库加密解密工具类。

编写的类目和工具类地址:github

Mei Xia

6 日志
GitHub
© 2018 Mei Xia
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.3