如何将第三方库封装成Xamarin.iOS链接库

如果你使用的是原生的开发技术,那么你肯定是不需要阅读本文的。但是如果在你的技术栈中,Xamarin.iOS有其一席之地的话,花点时间看一看或者收藏一下本文以备日后查阅,你一定不会吃亏。因为无论是Xamarin官方所提供的文档,抑或是国内所能查找的资料(事实上,几乎找不到),要么已经过时,要么遗漏了某些重要的细节,以至于即使你依样画葫芦,却发现怎么画也画不像。在本文中,我将以我最近封装的一个第三方库——OpenCC,作为示例,实践出真知。

OpenCC

OpenCC是一个非常出名的中文简体转繁体的开源项目,对于做输入法的我来说自然是非常需要的。但是我在github上转了一圈,并没有发现有人为OpenCC封装了可供Xamarin.iOS调用的绑定库,倒是发现了一个Xamarin.Android的。于是我决定自己封装一个,并开源出来,以飨世人,文末会给出开源地址。但是很快,我就发现,我他妈踩了一大坑啊!

环境准备

  1. 具备开发Xamarin.iOS的必要环境,包括Mac、Xcode、Xamarin Studio 或Visual Studio for Mac,如果是在Windows平台,则是Visual Studio和一台Mac设备充当编译的主机;本文以Mac平台为例;
  2. 安装Xcode Command Line Tools,注意:选择的版本应与本机所安装的Xcode版本一致。(安装方法在链接中)
  3. 安装最新版的Objective Sharpie,这个是帮我们自动生成绑定代码的工具,可以节省编程人员大量的精力,特别是在需要绑定的接口数量特别多的时候,堪称神器。但它不是万能的。(安装方法在链接中)

准备材料

  1. 下载OpenCC-iOS的代码。
    目前github上的OpenCC-iOS的项目有两个,一个Objective-C的,一个Swifty的。Xamarin官方目前只给出了绑定Objective-C写成的库的方法,但是出于好奇,我还是把两个项目都下载下来了,我这一行为居然成为最后破局的关键。
  2. 分析源码
    要将第三方库封装成Xamarin.iOS可以调用的库,并不是将源码下载之后无脑封装就可以了,我们还需要对源码进行分析,将其中我们真正需要的部分提取出来。
    打开OpenCC-iOS两个的项目,仔细观察,会发现核心的代码放在src文件夹下



    事实上,如果你还下载了OpenCC的Android版本,你会发现这一部分的代码是完全一样的,他们是用标准的C++实现的。

  3. 难题
    OpenCC不是纯代码的项目,其中包含了许多用于简转繁的词库文件和配置文件。而官方给出的例子是一个纯代码的项目,而且静态库项目也是不能携带资源的,他们似乎并没有打算告诉我应该如何处理这种情况。
    真是mmp!

封装步骤

1 创建一个Xcode静态库项目;

1.1 File ->New ->Project

选择Cocoa Touch Static Library


然后是给项目命名和选择存储位置,这里我就不给出截图了,在本文中我创建的项目名为OpenCC。

1.2 将OpenCC-master(Objective-C版本)项目中src、darts-clone和rapidjson-0.11几个文件夹和OpenCCService.h与OpenCCService.mm两个文件都复制到该项目空间下。

其实OpenCC-master中还有一个tclap-1.2.1文件夹和gtest-1.7.0文件夹,不过这是用于调试的代码,没有必要放到静态库中去。毕竟就算要调试,我也不会在静态库项目中进行。不仅如此,接下来,我们还要移除代码对这两个项目的引用。

1.3 在代码中添加我们刚才复制的文件。


注意,将我们刚才复制的文件夹中的文件都添加到OpenCC文件夹中,虽然他们实际存储在不同的文件夹里,但是添加的时候要将他们都添加的OpenCC文件夹下,添加后的结果大概是这样子。

这个时候,我们可以尝试编译一下文件,但会发现报错。



这正是我刚才没有选择添加的tclap中的文件。这个时候我其实有两个选择,一个是将tclap项目中的文件也都添加到这个项目中,一个是删除所有与之相关的代码。我发现tclap项目下的文件也是不少,加上前面给出的解释,我选择了后者。对待gtest项目相关的代码也是一样。我做的事情大概有这么两件:

  • 注释掉对这两个项目的所有引用和分散在各处的用于调试的main方法;
  • 移除所有以Test和TestBase为后缀的文件,这些是测试用的文件,对于静态库而言也是无用的。

最后,我再执行编译,Build success!显而易见,在进行下一步之前,我们必须能成功地进行编译。

2 将静态库项目编译成Fat库文件

与Fat库相对应的是Thin库。静态库项目可以编译成不用架构下的.a文件,比如i386(模拟器用的)、Arm64和Armv7。如果库文件只适用于一种架构,则是Thin库,如果是多种架构,则是Fat库。鉴于方便我们调试和使用,我们要把项目编译成Fat库文件。

官方给出的Makefile的内容如下:

XBUILD=/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild
PROJECT_ROOT=./YOUR-PROJECT-NAME
PROJECT=$(PROJECT_ROOT)/YOUR-PROJECT-NAME.xcodeproj
TARGET=YOUR-PROJECT-NAME

all: lib$(TARGET).a

lib$(TARGET)-i386.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphonesimulator -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphonesimulator/lib$(TARGET).a $@

lib$(TARGET)-armv7.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphoneos -arch armv7 -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphoneos/lib$(TARGET).a $@

lib$(TARGET)-arm64.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphoneos -arch arm64 -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphoneos/lib$(TARGET).a $@

lib$(TARGET).a: lib$(TARGET)-i386.a lib$(TARGET)-armv7.a lib$(TARGET)-arm64.a
    xcrun -sdk iphoneos lipo -create -output $@ $^

clean:
    -rm -f *.a *.dll

把YOUR-PROJECT-NAME全部替换成OpenCC就是我要使用的Makefile文件了。这里需要注意的是,要确保make命令的开头是Tab缩进的。如果出现Makefile:9: *** missing separator. Stop.错误,则说明格式不正确。

将Makefile文件放在项目同一级的目录下:


在Xcode项目中的Build Phases设置OpenCCService.h为public:


打开终端,cd 进入该目录,执行make命令:

如果一切顺利的话,我们可以该目录下看到.a文件了。

执行xcrun -sdk iphoneos lipo -info libOpenCC.a看这个Fat库文件是否包含各个架构的.a文件,其结果应该是Architectures in the fat file: libOpenCC.a are: i386 armv7 x86_64 arm64

3 使用Objective Sharpie生成API定义文件

在生成文件之前,我们利用sharpie xcode -sdks查看一下所安装的SDK版本:

接下来,我使用如下命令生成API定义文件sharpie bind -output SuiHanOpenCC -namespace SuiHanOpenCC -sdk iphoneos11.2 /Users/huangboru/myfile/XcodeWorkspace/SuihanOpenCC/OpenCC/OpenCC/OpenCCService.h -scope /Users/huangboru/myfile/XcodeWorkspace/SuihanOpenCC/OpenCC/OpenCC -c -F .
其中

  • SuiHanOpenCC是我自行指定的输出目录和命名空间的名称

  • /Users/huangboru/myfile/XcodeWorkspace/SuihanOpenCC/OpenCC/OpenCC/OpenCCService.h是目标文件的全路径

  • -scope /Users/huangboru/myfile/XcodeWorkspace/SuihanOpenCC/OpenCC/OpenCC指定了生成的接口范围,如果不增加这条命令的话,Objective Sharpie会将OpenCCService.h文件中include的头文件中声明的公共接口也一并生成对应的C# API,这会导致定义文件的体积和接口的数量膨胀,那些接口我们既用不上,同时也增加了我们纠正问题的成本。

Objective Sharpie并不是万能,在自动生成的接口中很可能会带有 [Verify]特性。这说明基于已有信息,Objective Sharpie无法确定其所生成的接口是否合适,这个特性会强制我们检查每一个带有该特性的接口定义,否则后续的Xamarin项目是不能通过编译的。
如果想要了解更多关于接口定义的信息,参考该文档Binding Types Reference Guide,如果你还是不知道正确的接口是什么样的,删除掉所有的[Verify]标记就OK了。

在这个例子中,由于我加入了-scope参数,所以生成出来的文件内容非常简单,而这些也正是我所需要的。

ApiDefinitions.cs的内容如下:

using System;
using Foundation;

namespace SuiHanOpenCC
{
    // @interface OpenCCService : NSObject
    [BaseType (typeof(NSObject))]
    interface OpenCCService
    {
        // -(instancetype)initWithConverterType:(OpenCCServiceConverterType)converterType;
        [Export ("initWithConverterType:")]
        IntPtr Constructor (OpenCCServiceConverterType converterType);

        // -(NSString *)convert:(NSString *)str;
        [Export ("convert:")]
        string Convert (string str);
    }
}

StructsAndEnums.cs的内容如下:

using System;
using ObjCRuntime;

namespace SuiHanOpenCC
{
    [Native]
    public enum OpenCCServiceConverterType : nint
    {
        S2t,
        T2s,
        S2tw,
        Tw2s,
        S2hk,
        Hk2s,
        S2twp,
        Tw2sp,
        T2hk,
        T2tw
    }
}

有了libOpenCC.a、ApiDefinitions.cs和StructsAndEnums.cs这三个文件,接下来我们就可以创建绑定库项目了。

4. 创建Xamarin.iOS Bindings Library

打开Xamarin Studio或者Visual Studio for Mac ,在本例中我使用的是Visual Studio for Mac,别问我两者有什么区别,我只想说改名也是一门艺术活。真是mmp!

  1. 创建一个新的Xamarin.iOS Bindings Library项目;


这里我取名为SuiHanOpenCC。新建好之后,我们会看到项目的结构如下:


  1. 添加.a文件
    右击Native References,找到我们前面生成好的libOpenCC.a文件,将其添加到项目中;


根据官方文档的说法,当我们完成.a文件的添加之后,IDE会自动为我们生成一个.linkwith.cs文件:


但是!但是!但是!我并没有看到这个文件!!!我一度怀疑是我添加文件的姿势有问题,但是无论我试多少遍,这个文件就是不会出现。也许,官方已经调整了具体做法,但是文档并没有改过来,所以我只能猜测IDE到底现在会为我做什么?

我猜这个文件已经生成了,只不过不可见而已,虽然我打开show All Files,还是看不见这个文件。于是我决定尝试编译一下。


哇~~~Wonderful!

几番探索之后,我确认了一点,添加完.a文件后,IDE什么都没有为我做。WTF!说好的自行车呢!

最后,我发现,我需要在AssemblyInfo.cs文件中自行加入这句话。

using ObjCRuntime;
[assembly: LinkWith("libOpenCC.a", SmartLink = true, ForceLoad = true, IsCxx = true)]
  1. 复制API定义
  • 将前面的ApiDefinitions.cs中的内容复制到项目中的ApiDefinition.cs文件中;
  • 将前面的StructsAndEnums.cs中的内容复制到项目中的Structs.cs文件中;

但是此时编译是通不过的,枚举类OpenCCServiceConverterType的定义有问题。


于是,我将nint改成了uint。此时有报/Users/huangboru/myfile/xamarin_workspace/SuiHanOpenCC/SuiHanOpenCC/BTOUCH: Error BI1026: bgen:SuiHanOpenCC.OpenCCServiceConverterType: Enums attributed with [NativeAttribute] must have an underlying type oflongorulongin parameterconverterType' from SuiHanOpenCC.OpenCCService.Constructor (BI1026) (SuiHanOpenCC)`,看这意思是只要ulong和long,最后我将uint改成ulong,编译通过了。但是关于这一点,官方文档中并没有提及,当然也可能是我没有找到;

  1. 解决OpenCC中的资源问题
    关于OpenCC项目中存在的词库文件和配置文件其实很好解决,就是在引用了绑定库的项目的Resource中添加这些文件即可,但是如果每一次使用这个库都要手动添加这些文件,实在太麻烦了。
    于是我又创建了一个Xamarin.iOS Class Library项目。


这个项目我取名为OpenCC,添加了对SuiHanOpenCC的引用和封装,并将需要使用到的资源放在这个项目中,如此一来,其它的项目只需要引用这个项目即可。

但是在解决这个问题的过程中我发现了OpenCC项目中也有坑啊。
在调试的过程中,项目一直报libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: STPhrases.ocd not found or not accessible.,我以为STPhrases.ocd是一个可执行文件,以至于思路七拐八弯,始终不得其门而入。最后发现其根结是Swifty版本的OpenCC中对资源的命令方法与Objective-C版本的OpenCC不同。目前来看,两个版本的OpenCC的src代码内核是一样的,但内核中已经采用了.ocd格式,而Objective-C版本的OpenCC中并没有包含这种格式的文件。最后我从Swifty项目中的.ocd文件替代了原有的.txt文件才算真正地解决了问题;

5.写在最后

我已将我封装好的项目推送至github,如有需要,欢迎使用:Xamarin.iOS.OpenCC
如果看完这篇文章,你仍然无法完成对第三方库的封装的话,我这里还有一言相赠:珍爱生命,远离Xamarin。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容