UIWebView的理解
UIWebView
|__ _UIWebViewScrollView
|__ UIWebBrowserView
这里有两个私有的类,我们先来看看_UIWebViewScrollView
,这玩意看名字都可以知道是什么的子类了,就是UIScrollView
。实际上对应的就是UIWebView
的scrollView属性,这也显然是为什么UIWebView
可以滚动的原因。UIWebBrowserView
才是真正用于渲染显示内容的视图。
UIWebView的渲染机制
为什么加载一个页面很大、内容很多的网页,UIWebView也能正常工作?
显然UIWebView也不是一次性把整个页面全部渲染的,而是把当前可见的部分再大一点的范围渲染了,在滚动的时候再一边滚动一边渲染。和UITableView的复用机制还是相当类似的,不过是在layer层级上进行的。如何证明这个结论,看下图:
这是在UIWebBrowserView
的截图,可以看到渲染的部分和没渲染的部分,UIWebBrowserView
的frame大小是真实的内容大小。
倘若把UIWebView
的大小设置得和UIWebBrowserView
一样大会怎么样?答案就是,整个UIWebBrowserView
会全部渲染,然后如果内容过多的话,直接超出内存然后崩溃。
UIScrollView内嵌UIWebView
iBook和网易云阅读都是使用这种方案的,当然具体的思路还是不同的。这样子必须要解决的一个问题就是手势的问题。
如果不做额外的处理,在UIScrollView上面放一个包含UIScrollView子类的View。滚动方向还一样的话,系统会无法识别究竟滚动哪一个View。具体表现为,两个UIWebView的连接处,当滚到上一个UIWebView的末位的时候,不得不松开手指,然后再次拖动才能滚动。因此这类手势的处理还是相当麻烦的。
UIScrollView手势科普
这里还可以科普一下UIScrollView
的手势处理,UIScrollView
是包含以下的手势:
UIScrollViewDelayedTouchesBeganGestureRecognizer
,
UIScrollViewPanGestureRecognizer
,
UIScrollViewPagingSwipeGestureRecognizer
,
UIScrollViewPinchGestureRecognizer
(不一定包含)
这些类都是私有的,除了第一个,其余的看名字也是可以大概猜出来是什么用途了吧?好的,那么第一个手势到底是为什么而存在呢?我们看看UIScrollViewDelayedTouchesBeganGestureRecognizer
的定义:
@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
struct CGPoint {
float x;
float y;
} _startSceneReferenceLocation;
UIDelayedAction *_touchDelay;
}
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end
可以看到一个UIDelayedAction
的玩意,再看UIDelayedAction
的定义:
@interface UIDelayedAction : NSObject {
SEL m_action;
BOOL m_canceled;
double m_delay;
NSString *m_runLoopMode;
id m_target;
NSTimer *m_timer;
id m_userInfo;
}
- (void)cancel;
- (void)dealloc;
- (double)delay;
- (id)initWithTarget:(id)arg1 action:(SEL)arg2 userInfo:(id)arg3 delay:(double)arg4;
- (id)initWithTarget:(id)arg1 action:(SEL)arg2 userInfo:(id)arg3 delay:(double)arg4 mode:(id)arg5;
- (BOOL)scheduled;
- (void)setTarget:(id)arg1;
- (id)target;
- (void)timerFired:(id)arg1;
- (void)touch;
- (void)touchWithDelay:(double)arg1;
- (void)unschedule;
- (id)userInfo;
@end
看到NSTimer
和initWithTarget:action:userInfo:delay:mode:
,应该都可以猜个七八成,但是有些东西最好还是不要靠猜,能确定的可以确定一下。看看-[UIScrollViewDelayedTouchesBeganGestureRecognizer touchesBegan:withEvent:]
的伪码:
void -[UIScrollViewDelayedTouchesBeganGestureRecognizer touchesBegan:withEvent:](void * self, void * _cmd, void * var_10, void * var_14) {
edi = self;
[edi clearTimer];
esi = @selector(view);
eax = [edi view];
eax = [eax delaysContentTouches];
if (eax != 0x0) {
var_10 = [UIDelayedAction alloc];
var_14 = @selector(sendDelayedTouches);
eax = [edi view];
objc_msgSend_fpret(eax, @selector(_touchDelayForScrollDetection));
eax = *NSRunLoopCommonModes;
eax = *eax;
asm{ fstp qword [ss:esp+0x14] };
edi->_touchDelay = [var_10 initWithTarget:edi action:var_14 userInfo:0x0 delay:stack[27] mode:stack[26]];
esi = *_OBJC_IVAR_$_UIScrollViewDelayedTouchesBeganGestureRecognizer._startSceneReferenceLocation;
eax = [edi _activeTouchesForEvent:arg_C];
eax = [edi _centroidOfTouches:eax excludingEnded:0x0];
*(edi + esi + 0x4) = edx;
*(edi + esi) = eax;
}
else {
[edi sendTouchesShouldBeginForTouches:arg_8 withEvent:arg_C];
}
return;
}
显然UIScrollViewDelayedTouchesBeganGestureRecognizer
和UIDelayedAction
的关系还是比较清晰了。然而光说这个也没什么说服力,直接用hook的方式把[UIDelayedAction initWithTarget:action:userInfo:delay:mode:]
hook了看看。参数如下图:
也就是UIScrollView
在0.15秒内不能识别出手势就会向touch事件发出消息。
如果根据不同的手势控制返回会怎么样,如下面代码:
- (id)init__WithTarget:(id)arg1 action:(SEL)arg2 userInfo:(id)arg3 delay:(double)arg4 mode:(id)arg5
{
if ([arg1 isKindOfClass:NSClassFromString(@"UIScrollViewDelayedTouchesBeganGestureRecognizer")]) {
return nil;
} else
return [self init__WithTarget:arg1 action:arg2 userInfo:arg3 delay:arg4 mode:arg5];
}
如果UIDelayedAction
失效的话,那么UIScrollView
的一系列的touch事件都无法捕捉了,就是这些方法都不会调用了:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
然而知道了这个东西还是无法解决那个问题,因为分发这些touchEvent是在系统更高层的[UIWindow _sendGesturesForEvent:]
内进行,那里还涉及了其它几个类,这条路不是太好走。
UIWebView的渲染时机
这个时候,如果想到把UIWebView
的frame设置为和内容一样大,的确可以解决手势问题,但又会出现渲染和内存问题。
好吧,那回来看看_UIWebViewScrollView
,发现其实现的方法也不多,因为真正实现东西比较多的还是在UIWebView
内部。UIWebView
是实现了UIScrollViewDelegate
的协议的,所以更多的逻辑在UIWebView
上而不是在_UIWebViewScrollView
。
好的,之前说到UIWebView
是根据当前滚动的情况来实现可视部分范围的渲染,那么这个东西具体是在那里进行呢?一般来说,大家首先想到的是UIScrollViewDelegate
的scrollViewDidScroll:
,不过_UIWebViewScrollView
没实现,而是在UIWebView
里面实现了。伪码太多了不列出来了,虽然UIWebView
实现了scrollViewDidScroll:
,但是不涉及UIWebBrowserView
的渲染操作,只是一些其他view的操作。
那么现在就要把关注点放在UIWebBrowserView
,UIWebBrowserView
的继承关系是这样的,UIWebBrowserView
->UIWebDocumentView
->UIWebTiledView
。实际上UIWebBrowserView
和UIWebTiledView
的实现还不算是太多API(相对于UIWebDocumentView
来说),不过作为渲染这类基础的东西,在基类UIWebTiledView
能找到答案的机率会更大一些。
最后锁定在_didScroll
这么一个方法,实现很简单:
void -[UIWebTiledView _didScroll](void * self, void * _cmd) {
var_8 = self;
var_4 = *0xd65ed4;
[[var_8 super] setNeedsLayout];
return;
}
好的,那我先来详解一下为什么_didScroll
会被调用,调用链如下:
-[UIScrollView setContentOffset]
->-[UIScrollView _notifyDidScroll]
->-[UIView _didScroll]
然后,在_notifyDidScroll
里面实际上把_scrollNotificationViews
数组内的每个view都调用_didScroll
。_didScroll
是UIView
的一个虚方法,默认不做任何事情。
回到UIWebTiledView
的渲染问题上,setNeedsLayout
以后,对应的-[UIWebTiledView layoutSubviews]
就会调用。里面才是各种渲染的方法,不过也不是真正的渲染代码,真正彻底的渲染操作实际是在WebCore.framework
私有framework里面进行的。我到这里就暂时止步了。
慎重起见,我还是hook了_didScroll
进行了验证,如果在_didScroll
不实现任何东西的话,滚动了以后UIWebView
就会出现后面部分都没渲染的情况。这和上面分析的情况还是吻合的。
从上面的分析,已经可以得知,UIWebBrowserView
的渲染是不依赖UIWebView
或者是特定的scrollView的。换句话说,把UIWebBrowserView
单独拿出来,放到其他的scrollView之上也是不存在渲染问题的。
UIWebView重载
然而直接把UIWebBrowserView
加到UIScrollView
上,每次加载新页面的时候还会出现UIScrollView
的contentOffset
被重置的情况。因此,还得把_restoreScrollPointForce:
屏蔽。
Class NEWebBrowserView = Nil;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 动态创建新类NEWebBrowserView
NSString *superClass = [@"UIWeb" stringByAppendingString:@"BrowserView"];
NEWebBrowserView = objc_allocateClassPair(NSClassFromString(superClass), "NEWebBrowserView", 0);
objc_registerClassPair(NEWebBrowserView);
// 添加新方法
Method tmpMethod = class_getInstanceMethod(NEBookWebView.class, @selector(NE_restoreScrollPointForce:));
IMP func = method_getImplementation(tmpMethod);
class_addMethod(NEWebBrowserView, @selector(NE_restoreScrollPointForce:), func, method_getTypeEncoding(tmpMethod));
// 交换方法
Method newMethod = class_getInstanceMethod(NEWebBrowserView, @selector(NE_restoreScrollPointForce:));
Method oldMethod = class_getInstanceMethod(NEWebBrowserView, NSSelectorFromString([@"_restore" stringByAppendingString:@"ScrollPointForce:"]));
method_exchangeImplementations(newMethod, oldMethod);
});
}
最后修改一下:
// 动态改变browser的类为NEWebBrowserView
object_setClass(self.realContentView, NEWebBrowserView);
这样就可以“安全”地使用定制好的NEWebBrowserView
了。
至于,UIWebBrowserView
是怎么找到父视图的UIScrollView
呢?其实是用-[UIView _scroller]
找出来的,因为内部也涉及了一个私有方法,所以就不展开来说了。
事后PS
上面也说了UIWebView
实现了UIScrollViewDelegate
的方法,那么问题来了,我们平时对_UIWebViewScrollView
也就是UIWebView.scrollView
设置delegate
真的没问题吗?
答案当然是没问题,但是为什么呢?
@interface _UIWebViewScrollView : UIWebScrollView {
BOOL _bouncesSetExplicitly;
UIWebBrowserView *_browserView;
_UIWebViewScrollViewDelegateForwarder *_forwarder;
}
- (void)_setWebView:(id)arg1;
- (void)_weaklySetBouncesHorizontally:(BOOL)arg1;
- (void)dealloc;
- (id)delegate;
- (id)initWithFrame:(struct CGRect { struct CGPoint { float x_1_1_1; float x_1_1_2; } x1; struct CGSize { float x_2_1_1; float x_2_1_2; } x2; })arg1;
- (void)setBounces:(BOOL)arg1;
- (void)setBouncesHorizontally:(BOOL)arg1;
- (void)setBouncesVertically:(BOOL)arg1;
- (void)setContentInset:(struct UIEdgeInsets { float x1; float x2; float x3; float x4; })arg1;
- (void)setDelegate:(id)arg1;
@end
看到_UIWebViewScrollViewDelegateForwarder
的成员变量,再看其定义:
@interface _UIWebViewScrollViewDelegateForwarder : NSObject <UIScrollViewDelegate> {
<UIScrollViewDelegate> *_delegate;
UIWebView *_webView;
}
@property(copy,readonly) NSString * debugDescription;
@property <UIScrollViewDelegate> * delegate;
@property(copy,readonly) NSString * description;
@property(readonly) unsigned int hash;
@property(readonly) Class superclass;
@property UIWebView * webView;
- (id)delegate;
- (void)forwardInvocation:(id)arg1;
- (id)methodSignatureForSelector:(SEL)arg1;
- (BOOL)respondsToSelector:(SEL)arg1;
- (void)setDelegate:(id)arg1;
- (void)setWebView:(id)arg1;
- (id)webView;
@end
很熟悉是吧,也就是典型的消息转发,实现细节就不研究,也是八九不离十。