App Extension

陳超邦發表於2019-02-27

概念

這裡有一個來自官方的概念。

App Extension
App Extension

An app extension lets you extend custom functionality and content beyond your app and make it available to users while they’re interacting with other apps or the system.

App Extension 是用來擴充套件系統或者其他應用的功能的類似外掛的東西。一般建立一個 App Extension 是為了針對性的完成一個任務,例如圖片處理,或者分享。

另外,App Extension 的執行是執行緒獨立的,這意味著無需開啟主應用即可使用這部分分化出來的功能。

Extension Points

  • 標記系統的可擴充套件部分
  • 打包在系統框架中
  • 提供不同的 API 和政策

App Extension 的優點

這個問題更像是在問,為什麼不直接使用原生應用。以蘋果的角度來看,App Extension 相對臃腫的應用有什麼區別?

1.更小的記憶體資源消耗,更少的啟動時間。

開發商會希望的自己的應用常駐在記憶體中,以便為使用者提供更快捷的服務,提高使用者粘性,但是對於系統而言,減少這樣的行為可以提高流暢度。

2.一個更重要的原因是增加程式數,創造隔絕環境。

如果多個應用使用到了相同 App Extension 的功能,就可以通過同時開啟多個程式來為不同的應用提供服務(資源消耗少)。同時將隔絕 App Extension 之間的影響,避免因為一個崩潰而影響其他。

3.最重要的原因放在後面,更細緻的許可權劃分

蘋果希望開發者的應用更深層次的豐富系統內容,那不可避免的需要賦予應用更大的許可權。以和業務息息相關的 Notification Service 來說,下發給應用的推送,經過這種型別的 Extension 可以做一些預先的處理,例如圖片下載或者欄位解碼等,但這也意味著這個 Extension 需要在接收到推送後被系統喚醒。

如果喚醒的物件不是擴充套件而是應用,那除了資源方面的考慮,安全方面也比較難以防備。

App Extension 與 Host App 的通訊

App Extension

Host App 請求 App Extension 這部分的內容,是通過系統來進行的(UIActivityViewController)。

App Extension 響應 Host App,會呼叫 beginRequestWithExtensionContext: 的代理方法。在這個代理裡面可以獲取到一個 NSExtensionContext 的例項。這個例項物件包括了 inputItems,並可以進行 openURL: 操作。

啟用規則匹配

Info.plist

<Key>NSExtension<Key>
    <dict>
        <key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
            <dict>
                <key>NSExtensionActivationSupportsImageWithMaxCount</key>
                <integer>10</integer>
                <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
                <integer>1</integer>
            </dict>
        </dict>
    </dict>
複製程式碼

自定義規則

- NSExtensionContext
    - NSArray *inputItems;
        - NSExtensionItem *inputItem;
            - NSArray *attachments;
                - NSItemProvider *attachment;
                    - NSArray<NSString *> *registeredTypeIdentifiers;
複製程式碼

典型的過濾規則可以滿足大部分的情況,而自定義規則可以滿足更復雜更具體的過濾條件。

SUBQUERY (
    extensionItems,
    $extensionItem,
    SUBQUERY (
        $extensionItem.attachments,
        $attachment,
        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
    ).@count == 10
    OR
    SUBQUERY (
        $extensionItem.attachments,
        $attachment,
        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url"
    ).@count == 1
    OR
    SUBQUERY (
        $extensionItem.attachments,
        $attachment,
        ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" 
    ).@count == 1
).@count == 1
複製程式碼

App Extension 與 Containing App 的通訊

App Extension

常規解決方案

App Extension 和其 Containing App 的沙盒是分開管理的,所以資料並不共享。按照蘋果文件 Sharing Data with Your Containing App 中提供的方法,App Extension 和其 Containing App 要想共享資料,必須處於同一個 App Group 中,使用 NSUserDefaults 來儲存和讀取內容。

// Create and share access to an NSUserDefaults object
NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName: @"com.example.domain.MyShareExtension"];

// Use the shared user defaults object to update the user`s account
[mySharedDefaults setObject:theAccountName forKey:@"lastAccountName"];
複製程式碼

無法滿足實時性要求高的情況。

Open Url

作用範圍很受侷限。因為各種 App Extension 之中只有 Today Extension 有許可權進行 Open Url 開啟 Containing App。

WatchConnectivity (iOS 9.0+)

僅用於 WatchOS 與 iOS 之間的雙向通訊。

App Extension

允許在前臺進行資料的實時傳輸。支援所有可序列化的資料(NSCoding Protocol)。

探究其他實時資料共享的方案

可以明白,在資料交流上,除了蘋果所提供的 App Group 的資料共享方案,其他都是基於通用的程式通訊方式來實現的。

只討論 iOS 的前提下,下面的幾種都是可用的方案。

MacOS 的話還包括 Mach Port(iOS7 以後不可用),NSConnection 和 XPC 等。

File Coordination (iOS5+)

NSFileCoordinator 類協調在同一程式中的多個程式和物件之間讀取和寫入檔案和目錄。 只要另一個程式在磁碟上更改了檔案,就可以通知物件。

- (instancetype)initWithFilePresenter:(id<NSFilePresenter>)filePresenterOrNil;
複製程式碼
// 寫
// NSFileCoordinatorWritingOptions 對應檔案操作時的 presenter 代理呼叫。
- (void)coordinateWritingItemAtURL:(NSURL *)url
                           options:(NSFileCoordinatorWritingOptions)options 
                             error:(NSError * _Nullable *)outError 
                        byAccessor:(void (^)(NSURL *newURL))writer;
                        
// 讀
- (void)coordinateReadingItemAtURL:(NSURL *)url 
                           options:(NSFileCoordinatorReadingOptions)options 
                             error:(NSError * _Nullable *)outError 
                        byAccessor:(void (^)(NSURL *newURL))reader;
複製程式碼

它有助於確保程式在寫入檔案時獲得對檔案的獨佔訪問許可權。

這是蘋果官方建議的方案。不過 TN2408 文件同時也說到的是:

Important: When you create a shared container for use by an app extension and its containing app in iOS 8.0 or later, you are obliged to write to that container in a coordinated manner to avoid data corruption. However, you must not use file coordination APIs directly for this in iOS 8.1.x and earlier. If you use file coordination APIs directly to access a shared container from an extension in iOS 8.1.x and earlier, there are certain circumstances under which the file coordination machinery deadlocks.

谷歌翻譯:
重要提示:當您在iOS 8.0或更高版本中建立供應用程式擴充套件及其包含應用程式使用的共享容器時,您必須以協調的方式寫入該容器以避免資料損壞。
但是,在iOS 8.1.x及更早版本中,不得直接使用檔案協調API。
如果直接使用檔案協調API從iOS 8.1.x及更早版本的擴充套件訪問共享容器,則在某些情況下檔案協調機制會死鎖。

在 iOS8.1.X 及其早前的 iOS 版本,使用這種方案可能會導致死鎖,這是需要避免的。這個問題在 iOS8.2 版本得到修復。

notify.h

notify 是 iOS 一直都支援的程式通訊機制,允許程式之間進行事件傳遞。就像 API 註釋裡面說的。

These routines allow processes to exchange stateless notification events. Processes post notifications to a single system-wide notification server, which then distributes notifications to client processes that have registered to receive those notifications, including processes run by other users.

谷歌翻譯:
這些例程允許程式交換無狀態通知事件。 程式將通知釋出到單個系統範圍的通知伺服器,然後通知伺服器將通知分發給已註冊接收這些通知的客戶端程式,包括其他使用者執行的程式。

uint32_t notify_register_dispatch(const char *name, int *out_token, dispatch_queue_t queue, notify_handler_t handler)
複製程式碼

問題在於 notify 無法直接傳遞資料。所公佈出來的唯一 Post Notification 的 API,只帶了一個 name 的引數。

uint32_t notify_post(const char *name);
複製程式碼

所以需要配合前面的 NSUserDefaults 使用。程式的某一端修改資料後,隨之傳送對應的事件,接受端於是去取對應的共享資料並解析。偽造出實時資料共享。

附: MMWormhole 實現參考。

BSD Socket

使用 Client-Server 這種架構方式,可以通過讓 Containing App 充當伺服器,在磁碟上建立套接字並等待連線,其他主機(App Extension)通過連線到伺服器充當客戶端。

蘋果的 DTS10003794-CH1-SECTION31 文件也指出:

A UNIX domain socket appears as an item in the file system. The client and server usually hard code the path to this socket. You should use a path to an appropriate directory (like /var/tmp) and then give the socket a unique name within that directory. For example, Sample Code `CFLocalServer` uses a path of /var/tmp/com.apple.dts.CFLocalServer/Socket.

谷歌翻譯:
UNIX域套接字顯示為檔案系統中的項。
客戶端和伺服器通常把路徑硬編碼到此套接字中。
您應該使用適當目錄的路徑(如/ var / tmp),然後在該目錄中為套接字指定唯一的名稱。
例如,示例程式碼`CFLocalServer`使用/var/tmp/com.apple.dts.CFLocalServer/Socket的路徑。

這種 Socket 的建立,要求客戶端和服務端擁有相同路徑的讀寫許可權。

而 App Extension 與 Containing App 正好同時擁有同一 App Group 下檔案的訪問和寫入許可權。

Server 監聽
// 檔案路徑
const char *socket_path = ...

//AF_INET 表示IPv4網路協議
//AF_INET6 表示IPv6
//AF_UNIX 表示本地套接字(使用一個檔案)
int fd = socket(AF_UNIX, 0, 0);

struct sockaddr addr;
addr.sun_family = AF_UNIX;
addr.sun_path = socket_path;

// 地址分配
bind(fd, (struct sockaddr *)&addr, sizeof(addr));

// kLLBSDServerConnectionsBacklog => 允許的連線上限
listen(fd, kLLBSDServerConnectionsBacklog);
複製程式碼
Server 接受連線
// DISPATCH_SOURCE_TYPE_READ => A dispatch source that monitors a file descriptor for pending
dispatch_source_t listeningSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, (uintptr_t)fd, 0, self.queue);
dispatch_source_set_event_handler(listeningSource, ^ {
    // 返回客戶端的檔案描述符
    int client_fd = accept(fd, &client_addr, &client_addrlen);
});
dispatch_resume(listeningSource);
複製程式碼
Client 請求連線
// 檔案路徑,同上
const char *socket_path = ...

int fd = socket(AF_UNIX, SOCK_STREAM, 0);

struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
addr.sun_path = socket_path;

// 連線
connect(fd, (struct sockaddr *)&addr, sizeof(addr));
複製程式碼
Common 收發訊息
// 與檔案描述符關聯的 I/O 通道
dispatch_io_t channel = dispatch_io_create(0, fd, self.queue, ^ (__unused int error) {}); 

// 收取訊息
dispatch_io_read(channel, 0, SIZE_MAX, self.queue, ^ (bool done, dispatch_data_t data, int error) {
    // ...
});

// 傳送訊息
dispatch_io_write(channel, 0, message_data, self.queue, ^ (bool done, __unused dispatch_data_t data, int write_error) {
    // ...
});
複製程式碼

socket_path 的讀寫許可權是連線得以建立的前提。

BSD Socket 這種連線的建立,簡單猜測是通過監聽同一檔案的讀寫來實現的。這與前面的 notify 這種方案有些類似,不同的地方在於 notify 方案需要主動傳送通知。

資料共享方案的總結

不同的場景使用不同的方案。

  • 沒有實時性要求的,可以直接使用 NSUserDefault 或者 NSFileManager 方案。

  • 如果 >= iOS8.2 的,建議使用 NSFileCoordinator 方案。

BSD Socket 相對 notify 方案有更多的處理,但是維持連線的建立在 App Extension 與 Containing App 之間變的不那麼容易。

如果考慮到應用本身是常駐記憶體的,或者是允許後臺執行的媒體播放應用,那麼 BSD Socket 方案會更合適。

而如果是普通應用,因為切換後臺或者被系統幹掉發生的行為比較頻繁,那麼使用 notify 來接受資訊配合一部分的啟動重新整理操作會更合適。

參考文章

  1. keenlab.tencent.com/en/2018/07/…
  2. medium.com/@theninjapr…
  3. ddeville.me/2015/02/int…
  4. nshipster.com/inter-proce…
  5. zh.wikipedia.org/wiki/Berkel…
  6. developer.apple.com/library/arc…
  7. developer.apple.com/library/arc…
  8. developer.apple.com/library/arc…

相關文章