HTML的input標(biāo)簽在 type = "file"
時,即變?yōu)槲募蟼骺丶瑸g覽器會去監(jiān)聽這個標(biāo)簽,根據(jù)標(biāo)簽的另外一個 accept
字段的內(nèi)容去調(diào)取各個平臺的相關(guān)系統(tǒng)資源,如圖片,視頻,聲音等,iOS也不例外。通過這個標(biāo)簽,移動端的H5頁面就有直接獲取系統(tǒng)資源的能力。但是有時候我們并不想讓H5拿到原始的文件,或者是希望能夠加工一下。比如:文件的壓縮,文件格式轉(zhuǎn)換,文件的編輯等。
<form>
<input type="file" accept="image/gif, image/jpeg"/>
</form>
也許大部分情況下我們會直接采用JS交互的方式。這種方式可定義和可控的程度都比較高,弊端也就是需要交互的地方都要跟H5協(xié)商好每個頁面去寫交互代碼。
本文通過攔截的方式,筆者不認(rèn)為是一種可靠的方案,因為隨著iOS系統(tǒng)的升級很可能就變了,不利于項目的穩(wěn)定,給維護(hù)帶來麻煩。不過作為另外一種解決問題的思路,感興趣還是可以看看的。
先以圖片的獲取為例
1. 尋找切入口
通過Debug View Hierarchy
工具查看視圖樹尋找點擊H5標(biāo)簽的彈窗
第一層
顯然這個ActionSheet無法決定最終是哪一張圖片,這個切入點不合適,我們再往里面看。
在拍照頁面,看到了熟悉的身影,UIImagePickerController
.
UIImagePickerController
類是獲取選擇圖片和視頻的用戶接口,我們可以用UIImagePickerController
選擇我們所需要的圖片和視頻。
再看一下相冊也是
UIImagePickerController
,這下比較可以確定就是這個了。
2.嘗試hook UIImagePickerControllerDelegate
先把UIImagePickerController
的delegate屬性的setter方法替換成我們自己的,以便后續(xù)修改一些代理方法。
+ (void)hookDelegate {
if (!isDelegateSetterHooked){
Method originalMethod = class_getInstanceMethod([UIImagePickerController class], @selector(setDelegate:));
Method replaceMethod = class_getInstanceMethod([UIImagePickerController class], @selector(new_setDelegate:));
method_exchangeImplementations(originalMethod, replaceMethod);
isDelegateSetterHooked = YES;
}
}
/**
替換后的delegate setter
@param delegate delegate
*/
- (void)new_setDelegate:(id<UIImagePickerControllerDelegate>)delegate {
[self new_setDelegate:delegate];//調(diào)用原來的方法實現(xiàn),讓UIImagePickerController的代理有值
SEL swizzledSEL = @selector(swizzled_imagePickerController:didFinishPickingMediaWithInfo:);
SEL originSEL = @selector(imagePickerController:didFinishPickingMediaWithInfo:);
if ([self isKindOfClass:[UIImagePickerController class]]) {
if (!delegate) {//代理清空時,去掉代理方法的hook
Class class = NSClassFromString(@"WKFileUploadPanel");
unHook_delegateMethod(class,swizzledSEL,originSEL);
return;
}
hook_delegateMethod([delegate class], originSEL, [self class], swizzledSEL, swizzledSEL);
}
}
通過我們自己的setter方法中的斷點可以看出,此時的代理對象是WKFileUploadPanel
的實例,這個類是WKWebKit
的私有類,我們無法直接使用,可以使用字符串加載的方式。
熟悉UIImagePickerController
的同學(xué)應(yīng)該知道不論是相機(jī)還是相冊,我們最終拿到圖片都是通過這個代理方法:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey, id> *)info;
把這個代理的實現(xiàn)替換掉
+ (void)hookDelegate {
SEL swizzledSEL = @selector(swizzled_imagePickerController:didFinishPickingMediaWithInfo:);
SEL originSEL = @selector(imagePickerController:didFinishPickingMediaWithInfo:);
if (swizzledSEL && originSEL) {
Class class = NSClassFromString(@"WKFileUploadPanel");
hook_delegateMethod(class, originSEL, [UIImagePickerController class], swizzledSEL, swizzledSEL);
}
}
/**
替換代理方法的實現(xiàn)
*/
static void hook_delegateMethod(Class originalClass, SEL originalSel, Class replacedClass, SEL replacedSel, SEL noneSel) {
//原實例方法
Method originalMethod = class_getInstanceMethod(originalClass, originalSel);
//替換的實例方法
Method replacedMethod = class_getInstanceMethod(replacedClass, replacedSel);
if (!originalMethod) {// 如果沒有實現(xiàn) delegate 方法,則手動動態(tài)添加
Method noneMethod = class_getInstanceMethod(replacedClass, noneSel);
class_addMethod(originalClass, originalSel, method_getImplementation(noneMethod), method_getTypeEncoding(noneMethod));
return;
}
// 向?qū)崿F(xiàn) delegate 的類中添加新的方法
class_addMethod(originalClass, replacedSel, method_getImplementation(replacedMethod), method_getTypeEncoding(replacedMethod));
// 重新拿到添加被添加的 method, 因為替換的方法已經(jīng)添加到原類中了, 應(yīng)該交換原類中的兩個方法
Method newMethod = class_getInstanceMethod(originalClass, replacedSel);
if(!isDelegateMethodHooked && originalMethod && newMethod) {
method_exchangeImplementations(originalMethod, newMethod);// 實現(xiàn)交換
isDelegateMethodHooked = YES;
}
}
- (void)swizzled_imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
}
這是我們就能拿到原始圖像了,想怎么加工就怎么加工。
這個info里面的信息都是什么,這里就不做過多解釋了。需要的同學(xué)可以查看 官方文檔。
3. 回傳信息給H5
上面我們知道,UIImagePickerController
的代理對象是WKFileUploadPanel
類的實例,那么該類中必定實現(xiàn)了UIImagePickerControllerDelegate
的代理方法。所以我們在加工完數(shù)據(jù)之后,調(diào)用一下原始實現(xiàn),把我們的加工數(shù)據(jù)給它,從而實現(xiàn)替換。代碼參見上面的:
- (void)swizzled_imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
其他文件類型的攔截
<input>
標(biāo)簽支持上傳哪些媒體類型,可以查看MIME類型參考手冊
這里給出幾個大類,如下表格:
值 | 描述 |
---|---|
audio/* | 接受所有的聲音文件。 |
video/* | 接受所有的視頻文件。 |
image/* | 接受所有的圖像文件。 |
MIME_type | 一個有效的 MIME 類型,不帶參數(shù)。請參閱 IANA MIME 類型,獲得標(biāo)準(zhǔn) MIME 類型的完整列表。 |
相應(yīng)的HTML
<form>
<input type="file" accept="audio/*"/>
</form>
<form>
<input type="file" accept="video/*"/>
</form>
<form>
<input type="file" accept="image/*"/>
</form>
<form>
<input type="file" accept="MIME_type"/>
</form>
筆者嘗試了一下,iOS對audio/*
類型的支持似乎不是很友好,這個識別出來跟最后的MIME_type
一樣能選擇所有文件。視頻和圖片這是只能選擇相應(yīng)類型。其它文件類型的限制和實現(xiàn)就留由讀者們自己探索吧。
另外,在實際的需求當(dāng)中可能只是需要替換H5頁面的UIImagePickerControllerDelegate
,也不希望影響到其他模塊。所以在demo中加了替換和恢復(fù)的代碼,以及相應(yīng)時機(jī),具體請看
github
參考文章和文檔: