iOS拦截H5的标签读取文件
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
选择我们所需要的图片和视频。
再看一下相册也是
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
的私有类,我们无法直接使用,可以使用字符串加载的方式。
熟悉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
参考文章和文档: