iOS如何写一个SDK
在iOS开发中,如果使用第三方工具除了开源项目一般都不会直接给出源码,通常给出静态库或者动态库这两种方式来使用其功能。而我们在开发中也有开发通用库的需求,所以也是需要使用静态库与动态库。
一. 对库的理解
库是共享程序代码的方式,一般分为静态库和动态库。库的一般特征:
1.一般是给应用提供通用服务的,非独立运行的程序集合
2.一般都是编译过的,方便使用
库的连接,主要有两大类方式:静态连接、动态连接。
静态连接,一般是指在创建应用程序的时候,将库集成进去,这样做的好处就是应用程序包自身可以独立运行,而不好的地方就是包会略显臃肿,库不能共享。
动态连接,创建应用的时候只约定好与库之间的调用关系,而不彻底将库包集成进应用。这样在应用运行时,需要运行环境中提供库,并且连接装载。优劣与静态库相反,动态链接库需要库环境,但由于本身不集成库内容,会比较小,同时也为和其他应用共享库的使用提供了可能。
- 静态库常见的是 .a
- 动态库(共享库)常见的是 Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd。
特别注意平时我们经常说的Framework(in Apple) 是Cocoa/Cocoa Touch程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件、说明文档等集中在一起,方便开发者使用。也就是说我们的 Framework其实是资源打包的方式,和静态库动态库的本质是没有什么关系。
二、静态库和动态库的区别
- 静态库:链接时会被完整的复制到可执行文件中,所以如果两个程序都用了某个静态库,那么每个二进制可执行文件里面其实都含有这份静态库的代码。
- 动态库: 链接时不复制,在程序启动后用动态加载,然后再决议符号,所以理论上动态库只用存在一份,好多个程序都可以动态链接到这个动态库上面,达到了节省内存(不是磁盘是内存中只有一份动态库),还有另外一个好处,由于动态库并不绑定到可执行程序上,所以我们想升级这个动态库就很容易,windows和linux上面一般插件和模块机制都是这样实现的。
库类型 | 优点 | 缺点 |
---|---|---|
静态库 | 1. 目标程序没有外部依赖,直接就可以运行。2. 效率较动态库高。 |
| 1. 会使用目标程序的体积增大。 | | 动态库 | 1. 不需要拷贝到目标程序中,不会影响目标程序的体积。
- 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。
- 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。实现动态更新 | 1. 动态载入会带来一部分性能损失(可以忽略不计)
- 动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux lib not found 错误)。 |
静态库可以简单理解为一堆目标文件(.o/.obj)的打包体(并非二进制文件),而动态库可以简单理解为 一个没有main函数的可执行文件。
三、iOS的动态库(被阉割的动态库)
iOS平台上规定不允许存在动态库,并且所有的 IPA 都需要经过Apple的私钥加密后才能用,基本你用了动态库也会因为签名不对无法加载,(越狱和非 APP store 除外)。于是就把开发者自己开发动态库成为了天方夜谭。 iOS8之前因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,并且iOS是单进程的,也就是某一时刻只有一个进程在运行,那么你写个共享库,给谁共享呢。同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,综上所以上动态库也就没有存在的必要了。
但是后来iOS8之后,iOS有了App Extesion特性,而且Swift也诞生了。由于iOS主App需要和Extension共享代码,Swift语言机制也需要动态库,于是苹果后来提出了Embedded Framework,这种动态库允许APP和APP Extension共享代码,但是这份动态库的生命被限定在一个APP进程内。简单点可以理解为被阉割的动态库。 但是这种动态库(Embedded Framework) 和系统的 UIKit.Framework 还是有很大区别,传统的动态库是给多个进程用的,而这里的动态库(Embedded Framework)是给单个进程里面多个可执行文件用的。系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 动态库(Embedded Framework) 哪怕是动态的,最后也还是要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的)。所以苹果没有直接把这种Embedded Framework称作动态库而是叫Embedded Framework。
上面提到跟Swift也有原因,在Swift的项目中如果要在项目中使用外部的代码,可选的方式只有两种,一种是把代码拷贝到工程中,另一种是用动态 Framework。使用静态库是不支持的。这个问题的根本原因主要是 Swift 的运行库没有被包含在 iOS 系统中,而是会打包进 App 中(这也是造成 Swift App 体积大的原因),静态库会导致最终的目标程序中包含重复的运行库。
四、开始制作SDK(以动态库为例)
1. 创建App工程,命名为RealDemo。(创建方法可以参考手把手完成第一个iOS版本的Hello World) 2. 关闭RealDemo工程,然后在RealDemo目录下创建Framework工程,命名为RealSDK 注意目录!!! 3. 设置Framework工程的Build Settings 由于我们以动态库为例,选择Dynamic Library.如果要制作静态库,则选择Static Library。
4. 关闭RealSDK工程,创建WorkSpace,命名为RealDemo 5. 连接Framework工程和App工程 打开 RealDemo.xcworkspace,毫无疑问,空空如也。 直接把需要连接的Framework工程(RealSDK.xcodeproj)和App工程(RealDemo.xcodeproj)拖进来即可
注意:两者是同级关系! 6. 把Framework添加到App工程中 有过SDK开发经验的同学到这里应该已经看明白了,所谓实时联调说白了就是用WorkSpace把两个工程连接起来而已,跟Pod的原理有几分相似。
7. 给Framework加点功能
增加一个RealDog类,定义一个eat方法,实现里面打印一句话“吃骨头”。然后改下RealDog.h的Target Membership为Public,意思为公开头文件。
RealDog的实现如下:
@implementation RealDog
+ (void)eat {
NSLog(@"吃骨头");
}
@end
8. 在App的ViewController调用一下SDK的方法
#import "ViewController.h"
#import <RealSDK/RealDog.h>
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[RealDog eat];
}
@end
9. 最后运行一下,可以发现App工程成功调用了SDK的方法 OK,实时联调到此结束。
五、使用脚本合并真机、模拟器等多种架构的Framework
1.添加一个Aggregate Target。路径:RealSDK Project -> TARGETS -> "+"(左下角) -> Cross-platform - Other -> Aggregate 2.Aggregate Target 命名为“RealSDK-Script” 3.依赖RealSDK 4.添加脚本
这个脚本是通用的,各位同学直接复制粘贴即可~
# Type a script or drag a script file from your workspace to insert its path.
UNIVERSAL_OUTPUTFOLDER=../Framework/
# 创建输出目录,并删除之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
rm -rf "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework"
# 分别编译模拟器和真机的Framework
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
# 定义真机、模拟器Build文件夹路径变量
IPHONE_BUILD=${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework
SIMULATOR_BUILD=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework
# 拷贝framework到univer目录
cp -R "${IPHONE_BUILD}" "${UNIVERSAL_OUTPUTFOLDER}/"
#cp -R "${SIMULATOR_BUILD}" "${UNIVERSAL_OUTPUTFOLDER}/"
# 定义输出路径变量
OUTPUT_PATH=${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework
# 合并framework,输出最终的framework到build目录
lipo -create "${IPHONE_BUILD}/${PROJECT_NAME}" "${SIMULATOR_BUILD}/${PROJECT_NAME}" -output "${OUTPUT_PATH}/${PROJECT_NAME}"
5.运行脚本 6.运行结果
如果你的Mac是最新的M1芯片,那么可能会出现以下报错:
fatal error: lipo: /Users/hujianhui/Library/Developer/Xcode/DerivedData/RealDemo-ckvcidkkuvgpadeiqrvgjdyikcdc/Build/Products/Debug-iphoneos/RealSDK.framework/RealSDK and /Users/hujianhui/Library/Developer/Xcode/DerivedData/RealDemo-ckvcidkkuvgpadeiqrvgjdyikcdc/Build/Products/Debug-iphonesimulator/RealSDK.framework/RealSDK have the same architectures (arm64) and can't be in the same fat output file
去除iOS模拟器的arm64架构即可。
六、后记
1.Framework中使用Category
在Framework工程的Build Setting中添加-ObjC。另外,使用我们SDK的App的Build Setting中也要添加-ObjC。
2.Framework支持bitcode
Android写一个SDK
上一章节我们从零到一完成了一个App开发,今天这个章节我们编写我们的第一个SDK
App 开发更偏向于用户层面,从 UI 展示到业务逻辑处理,全程处理用户的行为。而 SDK 面向的是开发者,开发更偏向于功能方面,注重功能的开发实现。
什么是 SDK?
SDK全称 Software Development Kit,广义上的 SDK 是为特定的软件包、软件框架、硬件平台、操作系统等建立应用程序时所使用的开发工具的集合。 做 Android 开发避免不了要使用很多第三方的 SDK,比如极光 SDK、支付宝 SDK、微博 SDK 等等。所谓 SDK 就是一个开发工具包,全称是 Software Development Kit,翻译过来是软件开发工具包。SDK 通常是为辅助开发某类软件而编写的特定软件包。
SDK 设计的基本原则
- SDK 安全,稳定
- 接口命名规范
- library 小而精
- 不依赖第三方SDK
SDK是嵌入App里面去,SDK 的首要就是保证基本的安全性,不能乱开放接口导致App数据泄露,其次是SDK的稳定性, SDK 的crash 如果没有被try catch 捕获到,会导致App崩溃,这样会导致第三方接入的 App 体验性非常差,会造成接入方的用户流失。 对于 SDK 开发来说,统一命名规范很重要,最好的状态是接入方看到接口命名就能知道是哪家厂商的 SDK。换句话说就是 SDK 的命名规范统一,形成自己公司的品牌效应。同时也方便接入方使用。 对于编码规范,网上都有各个大厂的规范模板,可以选择其中一个或自定义自己团队的规范,尽早统一代码风格。 避免造成接入方的App增加很大,不然会引起接入方的不满,甚至下架。 精是指功能要专注,比如极光推送, 就是专注推送相关的功能。不依赖第三方SDK,这个也和很好理解,SDK 如果依赖的第三方SDK, 这样既会导致SDK增加,也会增加接入方的集成成本,不利于SDK 小而精的原则。
Android SDK 介绍
Android App集成第三方SDK的文件类型,主要有三种,一种是 jar 包文件,和 so 文件 ,另外一种是 aar 文件, jar 包是Java 提供的SDK 文件类型,里面包含的是纯Java编译过后的代码。so 一般是 C和 C++打包的成库的文件。 aar 名字来源于 Android Archive,见名知义,是一个 Android 库项目的二进制归档文件,使用 Android Studio ,非常简单可以生成一个 AAR 文件。aar 库文件里面,包含了 jar 和 so,还有资源res等文件,结构等同一个app。 它可以提供构建应用所需的一切内容,包括源代码、资源文件和 Android 清单。不过,Android 库将编译为您可以用作 Android 应用模块依赖项的 Android ARchive (AAR) 文件,而不是编译为在设备上运行的 APK。与 JAR 文件不同,AAR 文件会为 Android 应用提供以下功能:
- AAR 文件可以包含多项 Android 资源和一个清单文件,让您除了能够在 Java 类和方法中进行捆绑以外,还能够在布局和可绘制对象等共享资源中进行捆绑。
- AAR 文件可以包含 C/C++ 库,供应用模块的 C/C++ 代码使用。
SDK 工程创建
打开上一个章节我们创建的工程,在工程上创建一个library module,命名为GPush,我们模拟实现一个推送简短新闻的接口
添加依赖项
如需在同一项目中的另一个应用或库模块中使用新的 Android 库代码,请添加一个项目级依赖项:
- 依次转到 File > Project Structure > Dependencies。
- 选择要在其中使用库的模块。
- 在 Declared Dependencies 标签页中,点击 +,然后在下拉菜单中选择 Module Dependency。
接口设计
Client#onReceiveMessage GPush#pushMessage 从下面给出UML图可以看出,只需要一个方法就可以监听到新闻推送了,GPushImpl#start(Client client)
GPush 类
package com.myname.library;
interface GPush {
void pushMessage(String msg);
}
GPushImpl 类
package com.myname.library;
import android.os.Handler;
import android.os.HandlerThread;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
public class GPushImpl implements GPush {
private List<Client> mClients;
private HandlerThread mHandlerThread;
private Handler mHandler;
private Random mRandom = new Random();
private List<String> msgs = new ArrayList<String>() {
{
add("1、文旅部:严查以中老年为目标的包价游产品");
add("2、加快推进沿长江户籍改革,服务长江经济带高质量发展。");
add("3、今年首批10家非法社会组织网站被关停,含中国文艺名人协会等。");
add("4、上海:5月1日起,电动自行车骑乘人员必须戴头盔。");
add("5、广州:清明祭扫实行实名预约,倡导网上祭扫、错峰延后祭扫。");
add("6、河北武安铁矿致6死事故涉嫌瞒报,企业相关人员被控制。");
add("7、黄峥辞任拼多多董事长:将放弃超级表决权,投入科学研究。");
add("8、打破国外20年垄断,国产人工心脏研发成功,但商用落地时间暂不确定。");
add("9、调查:六成青年入睡时间晚于23点,梦多睡眠浅成年轻人睡眠主要问题。");
}
};
GPushImpl() {
mClients = new ArrayList<>();
mHandlerThread = new HandlerThread("Push-Thread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
@Override
public void pushMessage(String msg) {
Iterator<Client> iterator = mClients.iterator();
while (iterator.hasNext()) {
iterator.next().onReceiveMessage(msg);
}
}
public static void start(Client client) {
GPushImpl gPush = new GPushImpl();
gPush.mClients.add(client);
gPush.mHandler.post(gPush.mRunnable);
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
mHandler.postDelayed(mRunnable, mRandom.nextInt(10000));
pushMessage(msgs.get(mRandom.nextInt(msgs.size())));
}
};
}
Client 类
public interface Client {
void onReceiveMessage(String msg);
}
开始监听新闻推送
GPushImpl.start {
Toast.makeText(MainActivity@this,it,Toast.LENGTH_LONG).show()
}
SDK 打包
./gradlew :GPush:assembleRelease
打包完成后就行生成一个aar文件, 这个文件就是我们打包的结果了。
最后注意事项
混淆
基于代码保护的目的,Gradle 打包默认会根据build.gradle和proguard-rules.pro配置的混淆规则,来对代码进行一个混淆, 如果library里面使用了如GSON或者反射等技术则需要对部分类keep
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
-keep class com.myname.library.** {*;}
【作者】:胡健辉
【公开范围】:公司内和公司外