iOS拦截H5的<input>标签读取文件

HTML的input标签在 type = "file" 时,即变为文件上传控件,浏览器会去监听这个标签,根据标签的另外一个 accept 字段的内容去调取各个平台的相关系统资源,如图片,视频,声音等,iOS也不例外。通过这个标签,移动端的H5页面就有直接获取系统资源的能力。但是有时候我们并不想让H5拿到原始的文件,或者是希望能够加工一下。比如:文件的压缩,文件格式转换,文件的编辑等。

<form>
    <input type="file" accept="image/gif, image/jpeg"/>
</form>
复制代码

也许大部分情况下我们会直接采用JS交互的方式。这种方式可定义和可控的程度都比较高,弊端也就是需要交互的地方都要跟H5协商好每个页面去写交互代码。

本文通过拦截的方式,笔者不认为是一种可靠的方案,因为随着iOS系统的升级很可能就变了,不利于项目的稳定,给维护带来麻烦。不过作为另外一种解决问题的思路,感兴趣还是可以看看的。


先以图片的获取为例

1. 寻找切入口

通过Debug View Hierarchy工具查看视图树寻找点击H5标签的弹窗 第一层

第一层
显然这个ActionSheet无法决定最终是哪一张图片,这个切入点不合适,我们再往里面看。
拍照页面

在拍照页面,看到了熟悉的身影,UIImagePickerController. UIImagePickerController类是获取选择图片和视频的用户接口,我们可以用UIImagePickerController选择我们所需要的图片和视频。

image.png
再看一下相册也是 UIImagePickerController,这下比较可以确定就是这个了。

2.尝试hook UIImagePickerControllerDelegate

先把UIImagePickerController的delegate属性的setter方法替换成我们自己的,以便后续修改一些代理方法。

+ (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];//调用原来的方法实现,让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的同学应该知道不论是相机还是相册,我们最终拿到图片都是通过这个代理方法:

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey, id> *)info;
复制代码

把这个代理的实现替换掉

+ (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);
    }
}

/**
 替换代理方法的实现
 */
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) {// 如果没有实现 delegate 方法,则手动动态添加
        Method noneMethod = class_getInstanceMethod(replacedClass, noneSel);
        class_addMethod(originalClass, originalSel, method_getImplementation(noneMethod), method_getTypeEncoding(noneMethod));
        return;
    }
    
    // 向实现 delegate 的类中添加新的方法
    class_addMethod(originalClass, replacedSel, method_getImplementation(replacedMethod), method_getTypeEncoding(replacedMethod));
    
    // 重新拿到添加被添加的 method, 因为替换的方法已经添加到原类中了, 应该交换原类中的两个方法
    Method newMethod = class_getInstanceMethod(originalClass, replacedSel);
    if(!isDelegateMethodHooked && originalMethod && newMethod) {
        method_exchangeImplementations(originalMethod, newMethod);// 实现交换
        isDelegateMethodHooked = YES;
    }
}

- (void)swizzled_imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {

}
复制代码

info数据

这是我们就能拿到原始图像了,想怎么加工就怎么加工。 这个info里面的信息都是什么,这里就不做过多解释了。需要的同学可以查看 官方文档

3. 回传信息给H5

上面我们知道,UIImagePickerController的代理对象是WKFileUploadPanel类的实例,那么该类中必定实现了UIImagePickerControllerDelegate的代理方法。所以我们在加工完数据之后,调用一下原始实现,把我们的加工数据给它,从而实现替换。代码参见上面的:

- (void)swizzled_imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
复制代码

其他文件类型的拦截

<input>标签支持上传哪些媒体类型,可以查看MIME类型参考手册

这里给出几个大类,如下表格:

描述
audio/* 接受所有的声音文件。
video/* 接受所有的视频文件。
image/* 接受所有的图像文件。
MIME_type 一个有效的 MIME 类型,不带参数。请参阅 IANA MIME 类型,获得标准 MIME 类型的完整列表。

相应的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一样能选择所有文件。视频和图片这是只能选择相应类型。其它文件类型的限制和实现就留由读者们自己探索吧。

另外,在实际的需求当中可能只是需要替换H5页面的UIImagePickerControllerDelegate,也不希望影响到其他模块。所以在demo中加了替换和恢复的代码,以及相应时机,具体请看 github


参考文章和文档:

  1. www.jianshu.com/p/626f663e9…
  2. www.w3school.com.cn/media/media…

猜你喜欢

转载自juejin.im/post/5c64ce696fb9a049ca37daf7