如果你使用的是原生的开发技术,那么你肯定是不需要阅读本文的。但是如果在你的技术栈中,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 of longorulongin 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。