iOS攔截H5的<input>標(biāo)簽讀取文件

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選擇我們所需要的圖片和視頻。

image.png

再看一下相冊也是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的私有類,我們無法直接使用,可以使用字符串加載的方式。

UIImagePickerControllerDelegate的代理對象

熟悉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數(shù)據(jù)

這是我們就能拿到原始圖像了,想怎么加工就怎么加工。
這個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


參考文章和文檔:

  1. http://www.lxweimin.com/p/626f663e955b
  2. http://www.w3school.com.cn/media/media_mimeref.asp
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。