Swift之利用API可用性解决App Extension无法编译

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之利用API可用性解决App Extension无法编译相关的知识,希望对你有一定的参考价值。

一、问题分析

  • 从 Xcode12.5 开始,苹果要求所有的 Extension Target 必须设置 APPLICATION_EXTENSION_API_ONLY 为 true,否则将会导致编译错误“Application extensions and any libraries they link to must be built with the APPLICATION_EXTENSION_API_ONLY build setting set to YES”;但是通常会在主工程和 Extension 之间使用 Framework 或其他方式共享代码,这些代码中使用了非 extension-only API,所以导致问题出现,那么该如何解决呢?
  • 以一个具体的工程结构为例,如下图所示:

在这里插入图片描述

  • 主工程 Host App 中,创建了一个 Share Extension 的扩展 Target 做分享相关的操作;另外为了模块化,有一个 Library 工程包含所有的基础组件和 Fundation 扩展方法,NetworkService 工程包含网络请求相关的功能封装和处理,它们都被编译为 Framework 供主工程和 Share Extension 共同使用。
  • 首先需要把 Share Extension、Library、NetworkService 这三个工程的 Build Setting 中 APPLICATION_EXTENSION_API_ONLY 设置为true;由于在 Library 和NetworkService 中都使用了 UIApplication.shared.open,UIApplication.shared.keyWindow 这类非 extension-only 的 API,所以编译这两个子工程是无法通过的。
  • 首先想到的解决办法是代码拆分,可以把 Library 按是否使用 extension-only API 进行拆分,拆分成两个工程 Libray 和 LibraryExtension,LibrayExtension 中包含符合 extension-only 的 API,提供给 Share Extesnion 使用;Library 中包含其他不设限 API,提供给主工程或其它非 Extesnion 工程使用;
  • 然后 NetworkService 也采用相同方法进行改造,这种方式是可以解决问题的,但是除了拆分代码创建新工程的代价,也会带来很多额外的工作量;比如主工程中 Host App,原来都是引用 import Library,现在就需要逐个修改确认,是使用 Libray 还是 LibraryExtension,或者添加同时引用两个,这对于一个已有的大工程来说,是一个不小的工作量。
  • 另外,在搜索这个问题的解决方式时,发现 swift-cast 网站作者提供了一个另外的解决方案:使用 ACTION_EXTENSION;如果 App 恰好使用的是 Action Extension 可以采用这个方式去尝试:
	#if !ACTION_EXTENSION
	    //codes that don't obey extension-only API requests
	#else
	    //normal codes 
	#endif 
  • 显然,上面的两种方式都有各自的局限性,所以需要寻找更加广泛使用的解决方案,改动更小的解决方案;Swift 语言提供了 API 可用性的标识,这个功能能够解决现在所遇到的问题。

二、Swift 的 API 可用性(API availability)

  • 在 Swift 中使用 @available 可以标记 API 的可用性信息,比如是否 API 在某个版本被废弃,这个 API 需要的 Swift 版本大于 5.4 才能使用。
① 平台可用性
	@available(ios 13.0, OSX 10.15, *)
	@available(tvOS, unavailable)
	@available(watchOS, unavailable)
	public struct SearchField: View{
	    ...
	}
  • 在 SwiftUI 中定义一个 SearchField 组件,就需要对其适用的平台和系统版本进行限制,上面这段代码表明,SearchField 适用于大于等于 iOS13 或 OSX 10.15 版本,同时针对 tvOS 和 watchOS 都是不可用的。
	@available(platform version , platform version ..., *)
  • 具体注释定义如下:
    • platform:指定具体适用的平台,比如iOS,macCatalyst,macOS/OSX,tvOS 或者 watchOS,还可以指定适用的扩展 Extension Target,比如 ApplicationExtension 或 macOSApplicationExtension,这是解决文章开头提出的问题的重要工具;
    • version:具体的数字,可以由一位、两位或三位正整数通过点号(.)分割组成,分别代表主版本号,次版本号,补丁版本号;
    • 可以有多个 platform+version 组成,之间用逗号分隔(,),比如@available(iOS 13.0, OSX 10.15, *);
    • 星号(*),表示该API可用于所有其他平台。为了处理潜在的未来平台,平台可用性注释总是需要一个星号。
② API 可用性
  • 在软件开发过程中会不断改进,引入新的 API,废弃旧的 API,与此对应的 @available 可以进行这部分工作 API 的标记。
	// With introduced, deprecated, and/or obsoleted
	@available(platform | *
	          , introduced: version , deprecated: version , obsoleted: version
	          , renamed: "..."
	          , message: "...")
	
	// With unavailable
	@available(platform | *, unavailable , renamed: "..." , message: "...")
  • 说明:
    • platform:与前面介绍一致;
    • introduced, deprecated, obsoleted:API 在指定版本开始可以使用,标记 introduced;API 即将在指定版本被废弃,标记 deprecated,使用此 API 时编译产生警告;API 在指定版本开始被淘汰,标记 obsoleted,使用此 API 时编译产生错误。
    • unavailable:通过与 platform 配合使用,表示指定平台版本 API 不能使用,如果在此情况下使用此 API 将会产生编译错误。
    • renamed:当使用此 API 时提供另一个 API 用来替换当前 API,当有此标识时 Xcode 提供了一个自动修复选项。
    • message:在编译警告或错误发生时,提供一个字符串说明给使用者。
  • 与平台可用性不同,这种形式只允许指定一个平台。因此,如果你想注释多个平台的可用性,你需要使用多个 @available 属性。例如,下面是简单示例如何表示多个平台:
	@available(macOS, introduced: 10.15)
	@available(iOS, introduced: 13)
	@available(watchOS, introduced: 6)
	@available(tvOS, introduced: 13)
③ #available
  • 在 Swift 中,你可以使用可用性条件 #available 来断言 if、guard 和 while 语句,以确定运行时 API 的可用性。与 @available 属性不同,#available 条件不能用于 Swift 语言版本检查。
  • #available 表达式的语法类似于 @available 属性:
	if | guard | while #available(platform version , platform version ..., *)
  • 不能使用&&和||等逻辑操作符组合多个 #available 表达式,但可以使用逗号,它们等价于&&。在实践中,这只对调整 Swift 语言版本和单个平台的可用性有用(因为对多个平台的检查要么是多余的,要么是不可能的)。
	// 要求Swift 5 和 iOS 13
	guard #available(swift 5.0), #available(iOS 13.0) else { return }
  • 注意:#available 没有对应的 unavailable 标识,只能判断符合条件的。

三、解决问题

  • “App Extension 无法编译通过”的具体解决方案:使用 @available 对 API 做函数级别的平台 API 标记。
  • 以下面 gotoAppSystemSetting 函数为例:
 	@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
    @available(watchOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
    @available(tvOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
    @available(iOSMacApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
    @available(OSXApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
    static func gotoAppSystemSetting() {
        if let url = URL(string: UIApplication.openSettingsURLString) {
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }   
        }
    }
  • 由于函数中使用了 UIApplication.shared.open 这类 Extension 不能使用的 API,所以需要增加 Extension 不可用 @available 标记。这样修改之后,由于明确的标记了 API 的可用性范围,开启 APPLICATION_EXTENSION_API_ONLY 为 true 之后,编译也可以正常通过。
① 函数调用链
  • 单个 API 函数问题解决后,你还需要考虑函数调用的链,比如在函数 A 中使用了 UIApplication.shared.keyWindow,那么 A 函数就需要标记为不能 Extension 使用的 API;
	@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
	func A() {
	    ...
	    // 调用UIApplication.shared.keyWindow
	    ...
	}
  • 接下来,如果有函数 B 调用了函数 A,函数 C 都调用了函数 B,那么函数 B 和 C 也都需要上述相同的 @available 标记;
	@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
	func B() {
	    ...
	    A() // 函数B中调用A函数
	    ...
	}
	
	@available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
	func C() {
	    ...
	    B() // 函数C中调用B函数
	    ...
	}
② 协议函数
  • 继续看协议函数相关的一个问题,有一个协议 DialogueViewProtocol,声明了一个 show 方法如下面代码所示:
	public protocol DialogueViewProtocol{
	    func show(cancelHander acancelHander:(() -> Void)? ,comfirmHander acomfirmHander:(() -> Void)?)
	}
  • 有两种样式 DialogueView 组件需要实现,分别是 DialogueView_1 和 DialogueView_2,为了抽取共同代码,它们都继承自 DialogueView;
	// DialogueView_1继承DialogueView
	public class DialogueView_1:DialogueView{
	   
	}
	//DialogueView_2继承DialogueView
	public class DialogueView_2:DialogueView{
	
	}
	//DialogueView_1遵守DialogueViewProtocol实现show函数
	extension DialogueView_1:DialogueViewProtocol{
	    public func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
	     ...
	     //  不同的内部实现
	    }
	    
	//DialogueView_2遵守DialogueViewProtocol实现show函数    
	extension DialogueView_2:DialogueViewProtocol{
	    public func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
	     ...
	     // 不同的内部实现
	    }
  • 在具体实现两个不同样式的 DialogueView 子类时,它们都遵守 DialogueViewProtocol,实现了不同的 show 方法;
  • 在 show 函数中两个方法都用到了 UIApplication.shared.keyWindow,显然都无法被 Extension 使用,所以需要对 show 方法进行前面相同的 @available(iOSApplicationExtension, unavailable, message: “This method is NS_EXTENSION_UNAVAILABLE.”)标注,这样修改之后,API 不被 Extension 使用的问题解决了,但是又引出了新的问题,导致 show 方法不可见,两个子 View 没有实现遵守 DialogueViewProtocol 协议;
  • 那么怎么来解决呢?需要继续改造代码,在 DialogueView 基类中实现协议,来解决这个问题:
	// 基类DialogueView遵守DialogueViewProtocol
	extension DialogueView:DialogueViewProtocol{
	    public func show(cancelHander acancelHander:(() -> Void)? ,comfirmHander acomfirmHander:(() -> Void)?){
	        // 空实现
	    }
	}
	
	extension DialogueView_1{
	// 标记,复写show函数
	    @available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
	    public override func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
	        ... 
	    }
	}
	
	extension DialogueView_2{
		// 标记,复写show函数
	    @available(iOSApplicationExtension, unavailable, message: "This method is NS_EXTENSION_UNAVAILABLE.")
	    public override func show(cancelHander acancelHander: (() -> Void)?,comfirmHander acomfirmHander:(() -> Void)?){
	        ...
	    }
	}
  • DialogueView 遵循 DialogueView,实现了 show 函数的空实现,然后在 DialogueView_1 和 DialogueView_2 中重写(override)show 函数,需要注意 DialogueView 中 show 函数因为不使用 Extension 限制 API,所以无需 @available 标记,而 DialogueView_1 和 DialogueView_2 是需要的。
③ Objective-C 函数 API
  • 使用 Objective-C 编写的函数也可能是用了 Extension 不可用的 API,Objective-C 语言也提供了 NS_EXTENSION_UNAVAILABLE_IOS 对其进行标记,系统函数中有很多这样的例子:
	./EventKitUI.framework/Headers/EKEventViewController.h:NS_EXTENSION_UNAVAILABLE_IOS("EventKitUI is not supported in extensions")
	./Foundation.framework/Headers/NSObjCRuntime.h:#define NS_EXTENSION_UNAVAILABLE_IOS(_msg)  __IOS_EXTENSION_UNAVAILABLE(_msg)
	./UIKit.framework/Headers/UIAlertView.h:- (instancetype)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id /*<UIAlertViewDelegate>*/)delegate cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... NS_REQUIRES_NIL_TERMINATION NS_EXTENSION_UNAVAILABLE_IOS("Use UIAlertController instead.");
	./UIKit.framework/Headers/UIApplication.h:- (void)beginIgnoringInteractionEvents NS_EXTENSION_UNAVAILABLE_IOS("");               // nested. set should be set during animations & transitions to ignore touch and other events
  • 具体针对 iOS 来说:
	NS_EXTENSION_UNAVAILABLE_IOS("..."
  • 标记 Extension 不能使用这些 API,后面有一个参数,可以作为提示,用什么 API 替换。
④ 普通API
  • 在头文件中对函数添加标记:
	+ (UIImage *)launchImage NS_EXTENSION_UNAVAILABLE_IOS("");
⑤ 重写系统方法的 API
  • 如果是重写了系统方法,比如继承 UIViewController,重写了 statusBarStyle,那么对其进行标记就需要在实现函数中:
	- (UIStatusBarStyle)statusBarStyle NS_EXTENSION_UNAVAILABLE_IOS("") {
	    return [UIApplication sharedApplication].statusBarStyle;
	}

四、总结

  • 由于 Xcode12.5 新版本带来的变化,导致需要利用 Swift 语言 API 可用性的标记,对不合符 extension-only 的 API 进行 @available(iOSApplicationExtension, unavailable) 标记,Objective-C 语言也有对应的 NS_EXTENSION_UNAVAILABLE_IOS("…")标记可以使用;也需要对当前标记函数调用链上的上游函数增加标记,另外,也可能需要针对协议函数,实现默认实现,以解决标记 @available 不可见问题。

以上是关于Swift之利用API可用性解决App Extension无法编译的主要内容,如果未能解决你的问题,请参考以下文章

iOS逆向之利用Xcode重签名

mybatis文件映射之利用延迟加载解决collection分布查询

安卓开发之利用XmlSerializer生成XML文件

Android性能优化之利用Rxlifecycle解决RxJava内存泄漏

Android性能优化之利用LeakCanary检测内存泄漏及解决办法(转)

Android性能优化之利用LeakCanary检测内存泄漏及解决办法(转)