整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

关于android的webView的内容

关于android的webView的内容

目前的网速环境而言,应用程序内加载web的速度是非常的快的,所以当一个APP上的页面需要定期的变动的时候就可以尝试使用webView来进行实现。

其实平时使用的APP中很多都是用到了webView的界面,例如很多的电商网页都使用了webView的功能。

关于webView的定义,在谷歌的网站上给出的是 :一个能够展示web界面的View,

最初始的定义中,webView是不能够使用JavaScript并且其中的很多web页面的错误会自动忽视,如果只是展示一些最基本的HTML页面做为UI的一部分那么这样是OK的。但是这样是不允许界面进行一些交互的,因为这只是最进本的HTML网页并没有JavaScript用来与之交互。

其中WebView最常使用的方法有

加载网页的

//方式1. 加载一个网页:

webView.loadUrl("http://www.google.com/");

//方式2:加载apk包中的html页面

webView.loadUrl("file:///android_asset/test.html");

//方式3:加载手机本地的html页面

webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");

// 方式4: 加载 HTML 页面的一小段内容

WebView.loadData(String data, String mimeType, String encoding)

其中要注意的是加载apk包中的HTML页面的时候file之后是三个斜杠

然后WebView也可以进行网页的前进后退的操作

//是否可以后退

Webview.canGoBack()

//后退网页

Webview.goBack()

//是否可以前进

Webview.canGoForward()

//前进网页

Webview.goForward()

//以当前的index为起始点前进或者后退到历史记录中指定的steps

//如果steps为负数则为后退,正数则为前进

Webview.goBackOrForward(intsteps)

当知晓了前进后退的操作的时候那么这里就可以设置一下使用back按键的时候将当前的Activity的退出改为网页的后退

当需要进行一些自己的一些设置。其中可以创建一个webChromeClient 的子类。这个类是在一些东西影响浏览器UI界面的发生时候被调用。例如界面刷新或者一个alert的一个响应网站的标题和图标什么都会发送到这里。

// Let's display the progress in the activity title bar, like the

// browser app does.

getWindow().requestFeature(Window.FEATURE_PROGRESS);

webview.getSettings().setJavaScriptEnabled(true);

final Activity activity=this;

webview.setWebChromeClient(new WebChromeClient() {

public void onProgressChanged(WebView view, int progress) {

// Activities and WebViews measure progress with different scales.

// The progress meter will automatically disappear when we reach 100%

activity.setProgress(progress * 1000);

}

});

webview.setWebViewClient(new WebViewClient() {

public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {

Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show();

}

});

webview.loadUrl("https://developer.android.com/");

上面Google官网上给出的范例代码。其中可以看到首先是要getSettings().setJavaScriptEnabled(true)设置使用JavaScript。使用getSetting可以进行很多配置

//声明WebSettings子类

WebSettings webSettings=webView.getSettings();

//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript

webSettings.setJavaScriptEnabled(true);

// 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)

// 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可

//支持插件

webSettings.setPluginsEnabled(true);

//设置自适应屏幕,两者合用

webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小

webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小

//缩放操作

webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。

webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放

webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件

//其他细节操作

webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //关闭webview中缓存

webSettings.setAllowFileAccess(true); //设置可以访问文件

webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口

webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片

webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式

然后分别使用了WebViewClient 和WebChromeClient的两个子类的部分方法。

那么这里我们就可以打开androidStudio中的WebViewClient的类查看到底有什么是可以用的上的。

其中在WebView中比较常用的方法

shouldOverrideUrlLoading()

作用是在于打开网页的时候不调用系统的浏览器,而是在本地的WebView中显示

使用的方法都是在webViewClient的子类中复写一下代码就可以

webView.setWebViewClient(new WebViewClient(){

@Override

public boolean shouldOverrideUrlLoading(WebView view, String url) {

view.loadUrl(url);

return true;

}

还有比较常用的就是页面加载开始和页面加载结束的调用。

分别是 onPageStarted和onPageFinished 其中的方法都可以在原来的类中找到,只要重写方法,然后加上自己所需要的操作就可以了。

个蛋(codeegg)第 723 次推文

作者: 骑着蜗牛闯红灯

原文: https://juejin.im/post/5cff8c27f265da1bae38f1c1

先简单介绍一下,Android在4.4之后采用了Chrome内核,所以我们在开发web页面的时候,es6的语法,css3的样式等大可放心使用。

我将分下面几个模块去介绍Android上面WebView。

WebView自身的一些方法


//方式1. 加载一个网页:

webView.loadUrl("http://www.google.com/");

//方式2:加载apk包中的html页面
webView.loadUrl("file:///android_asset/test.html");

//方式3:加载手机本地的html页面
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");



正常情况下,在WebView界面,用户点击返回键是直接退出该页面的,着当然不是我们想要的,我们想要的是网页自己的前进和后退,所以下面介绍网页前进和后退的一些API

//判断是否可以后退
Webview.canGoBack
//后退网页
Webview.goBack

//判断是否可以前进
Webview.canGoForward
//前进网页
Webview.goForward

// 参数传负的话表示后退,传正值的话表示的是前进
Webview.goBackOrForward(int steps)
对返回键的监听,来实现网页的后退
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode==KEYCODE_BACK) && mWebView.canGoBack) {
mWebView.goBack;
return true;
}
return super.onKeyDown(keyCode, event);
}

如何防止WebView内存泄漏

防止内存泄漏的一个原则就是:生命周期长的不要跟生命周期短的玩。

为了防止WebView不造成内存泄漏,

  • 不要在xml里面定义WebView,而是在Activity选中使用代码去构建,并且Context使用ApplicationContext

  • 在Activity销毁的时候,先让WebView加载空内容,然后重rootView中移除WebView,再销毁WebView,最后置空

override fun onDestroy {
if (webView !=) {
webView!!.loadDataWithBaseURL(, "", "text/html", "utf-8", )
webView!!.clearHistory
(webView!!.parent as ViewGroup).removeView(webView)
webView!!.destroy
webView=
}
super.onDestroy

}

WebSetting和WebViewClient,WebChromeClien

  • WebSetting


作用:对WebView进行配置和管理

WebSettings webSettings=webView.getSettings;
// 设置可以与js交互,为了防止资源浪费,我们可以在Activity
// 的onResume中设置为true,在onStop中设置为false
webSettings.setJavaScriptEnabled(true);

//设置自适应屏幕,两者合用
//将图片调整到适合webview的大小
webSettings.setUseWideViewPort(true);
// 缩放至屏幕的大小
webSettings.setLoadWithOverviewMode(true);

//设置编码格式
webSettings.setDefaultTextEncodingName("utf-8");

// 设置允许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

//设置缓存的模式
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

关于缓存的设置

当加载 html 页面时,WebView会在/data/data/包名目录下生成 database 与 cache 两个文件夹,请求的 URL记录保存在 WebViewCache.db,而 URL的内容是保存在 WebViewCache 文件夹下

缓存模式如下:

//LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
//LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取据。
//LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
//LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或no-cache,都使用缓存中的数据。

离线加载

if (NetStatusUtil.isConnected(getApplicationContext)) {
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根据cache-control决定是否从网络上取数据。
} else {
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//没网,则从本地获取,即离线加载
}

webSettings.setDomStorageEnabled(true); // 开启 DOM storage API 功能
webSettings.setDatabaseEnabled(true); //开启 database storage API 功能
webSettings.setAppCacheEnabled(true);//开启 Application Caches 功能

String cacheDirPath=getFilesDir.getAbsolutePath + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //设置 Application Caches 缓存目录
  • WebViewClient 作用

  • 处理各种通知,请求事件,主要有,网页开始加载,记载结束,加载错误(如404),处理https请求,具体使用请看下面代码,注释清晰。


webView!!.webViewClient=object : WebViewClient {
// 启用WebView,而不是系统自带的浏览器
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
view.loadUrl(url)
return true
}

// 页面开始加载,我们可以在这里设置loading
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
tv_start.text="开始加载了..."
}
// 页面加载结束,关闭loading
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
tv_end.text="加载结束了..."
}

// 只要加载html,js,css的资源,每次都会回调到这里
override fun onLoadResource(view: WebView?, url: String?) {
loge("onLoadResource invoked")
}

// 在这里我们可以加载我们自己的404页面
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
loge("加载错误:${error.toString}")
}

// webview默认设计是不开启https的,下面的设置是允许使用https
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
handler?.proceed
}

// js调用Android的方法,在这里可以,该方法不存在通过注解的方式的内存泄漏,但是想拿到Android的返回值的话很难,
// 可以通过Android调用js的代码的形式来传递返回值,例如下面的方式
// Android:MainActivity.java
// mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
// function returnResult(result){
// alert("result is" + result);
// }

override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val uri=Uri.parse(request?.url.toString)
// 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
//假定传入进来的 url="js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
if (uri.scheme=="js") {
if (uri.authority=="webview") {
toast_custom("js调用了Android的方法")
val queryParameterNames=uri.queryParameterNames
queryParameterNames.forEach {
loge(it + ":" + uri.getQueryParameter(it))
}
}
return true
}
return super.shouldOverrideUrlLoading(view, request)
}

// 拦截资源 通常用于h5的首页页面,将常用的一些资源,放到本地
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if(request?.url.toString.contains("logo.gif")){
var inputStream: InputStream?=
inputStream=applicationContext.assets.open("images/test.png")
return WebResourceResponse("image/png","utf-8", inputStream)
}
return super.shouldInterceptRequest(view, request)
}
}

注意:

5.1 以上默认禁止了https和http的混用下面的设置是开启:

if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
  • WebChromeClient 作用

    辅助webview的一下回调方法,可以得到网页加载的进度,网页的标题,网页的icon,js的一些弹框,直接看代码,注释清晰。


webView!!.webChromeClient=object : WebChromeClient {

// 网页加载的进度
override fun onProgressChanged(view: WebView?, newProgress: Int) {
tv_progress.text="$newProgress%"
}

// 获得网页的标题
override fun onReceivedTitle(view: WebView?, title: String?) {
tv_title.text=title
}

//js Alert
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {

AlertDialog.Builder(this@WebActivity)
.setTitle("JsAlert")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> result?.confirm }
.setCancelable(false)
.show
return true
}

// js Confirm
override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
return super.onJsConfirm(view, url, message, result)
}

//js Prompt
override fun onJsPrompt(
view: WebView?,
url: String?,
message: String?,
defaultValue: String?,
result: JsPromptResult?
): Boolean {
return super.onJsPrompt(view, url, message, defaultValue, result)
}

}

Android和js的交互

Android调用js

1. 通过webview的loadUrl

注意:该方式必须在webview加载完毕之后才能调用,也就是webviewClient的onPageFinished方法回调之后,而且该方法的执行 会刷新界面,效率较低

js代码:
function callJs{
alert("Android 调用了 js代码)
}
kotlin代码:
webView?.loadUrl("javascript:callJs")

2. 通过webview的evaluateJavaScript

比起第一种方法,效率更高,但是要在4.4之后才能使用


js代码:

function callJs{
// alert("Android 调用了 js代码)
return {name:'wfq',age:25}
}

kotlin代码:
webView?.evaluateJavascript("javascript:callJs") {
// 这里直接拿到的是js代码的返回值
toast(it) // {name:'wfq',age:25}
}

js调用Android

1. 通过webview的addJavaScriptInterface进行对象映射

我们可以单独定义一个类,所有需要交互的方法可以全部写在这个类里面,当然也可以直接写在Activity里面,下面以直接定义在Activity里面为例,优点:使用方便,缺点:存在漏洞(4.2之前),请看下面的“WebView的一些漏洞以及如何防止”

kotlin中定义被js调用的方法
@JavascriptInterface
fun hello(name: String) {
toast("你好,我是来自js的消息:$msg")
}
js代码
function callAndroid{
android.hello("我是js的,我来调用你了")
}
kotlin中们在webview里面设置Android与js的代码的映射
webView?.addJavascriptInterface(this, "android")

2. 通过webviewClient的shouldOverrideUrlLoading的回调来拦截url

具体使用:解析该url的协议,如果监测到是预先约定好的协议,那么就调用相应的方法。比较安全,但是使用麻烦,js获取Android的返回值的话很麻烦,只能通过上面介绍的通过loadurl去执行js代码把返回值通过参数传递回去

首先在js中约定号协议
function callAndroid{
// 约定的url协议为:js://webview?name=wfq&age=24
document.location="js://webview?name=wfq&age=24"
}

在kotlin里面,当loadurl的时候就会回调到shouldOverrideUrlLoading里面

override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
val uri=Uri.parse(request?.url.toString)
// 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
//假定传入进来的 js://webview?name=wfq&age=24
if (uri.scheme=="js") {
if (uri.authority=="webview") {
toast_custom("js调用了Android的方法")
val queryParameterNames=uri.queryParameterNames
queryParameterNames.forEach {
loge(it + ":" + uri.getQueryParameter(it))
}
}
return true
}
return super.shouldOverrideUrlLoading(view, request)
}

3.通过webChromeClient的onJsAlert,onJsConfirm,onJsPrompt回调来拦截对话框

通过拦截js对话框,得到他们的消息,然后解析即可,为了安全,建议内容采用上面介绍的url协议, 常用的拦截的话就是拦截prompt,因为它可以返回任意值,alert没有返回值,confirm只能返回两种类型,确定和取消

js代码
function clickprompt{
var result=prompt("wfq://demo?arg1=111&arg2=222");
alert("demo " + result);
}

kotlin代码
override fun onJsPrompt(
view: WebView?,
url: String?,
message: String?,
defaultValue: String?,
result: JsPromptResult?
): Boolean {
val uri=Uri.parse(message)
if (uri.scheme=="wfq") {
if (uri.authority=="demo") {
toast_custom("js调用了Android的方法")
val queryParameterNames=uri.queryParameterNames
queryParameterNames.forEach {
loge(it + ":" + uri.getQueryParameter(it))
}
// 将需要返回的值通过该方式返回
result?.confirm("js调用了Android的方法成功啦啦啦啦啦")
}
return true
}
return super.onJsPrompt(view, url, message, defaultValue, result)
}

由于拦截了弹框,所以js代码的alert需要处理 这里的message便是上面代码的返回值通过alert显示出来的信息
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
AlertDialog.Builder(this@WebActivity)
.setTitle("JsAlert")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> result?.confirm }
.setCancelable(false)
.show
return true
}

上面三种方式的区别:

addJavascriptInterface 方便简洁,4.0以下存在漏洞,4.0以上通过@JavascriptInterface注解修复漏洞。

WebViewClient.shouldOverrideUrlLoading回调,不存在漏洞,使用复杂,需要定义协议的约束,但是返回值的话有些麻烦,在不需要返回值的情况下可以使用这个方式。

通过WebChromeClient的onJsAlerta,onJsConfirm,onJsPrompt,不存在漏洞问题,使用复杂,需要进行协议的约束,可以返回值,能满足大多数情况下的互调通信。

WebView的一些漏洞以及如何防止

密码明文存储漏洞

webview默认开启了密码保存功能,在用户输入密码后会弹出提示框询问用户是否保存密码,保存后密码会被明文保存在 /data/data/com.package.name/databases/webview.db 下面,手机root后可以查看,那么如何解决?

WebSettings.setSavePassword(false) // 关闭密码保存提醒功能

WebView 任意代码执行漏洞

addJavascriptInterface漏洞,首先先明白一点,js调用Android代码的时候,我们经常使用的是addJavascriptInterface, JS调用Android的其中一个方式是通过addJavascriptInterface接口进行对象映射,那么Android4.2之前,既然拿到了这个对象,那么这个对象中的所有方法都是可以调用的,4.2之后,需要被js调用的函数加上@JavascriptInterface注解后来避免该漏洞

所以怎么解决

对于Android 4.2以前,需要采用拦截prompt 方式进行漏洞修复

对于Android 4.2以后,则只需要对被调用的函数以 @JavascriptInterface进行注解

域控制不严格漏洞

  • 原因分析 当我们在Applilcation里面,android:exported="true"的时候,A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁,

下面来看下WebView中getSettings类的方法对 WebView 安全性的影响 setAllowFileAccess

// 设置是否允许 WebView 使用 File 协议

// 默认设置为true,即允许在 File 域下执行任意 JavaScript 代码

webView.getSettings.setAllowFileAccess(true);
如果设置为false的话,便不会存在威胁,但是,webview也无法使用本地的html文件

setAllowFileAccessFromFileURLs

// 设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
// 在Android 4.1前默认允许
// 在Android 4.1后默认禁止
webView.getSettings.setAllowFileAccessFromFileURLs(true);

我们应该明确的设置为false,禁止读取其他文件

setAllowUniversalAccessFromFileURLs

// 设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)
// 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs不起作用)
// 在Android 4.1后默认禁止
webView.getSettings.setAllowUniversalAccessFromFileURLs(true);

WebView预加载以及资源预加载

为什么需要预加载

h5页面加载慢,慢的原因:页面渲染慢,资源加载慢

如何优化?

h5的缓存,资源预加载,资源拦截

h5的缓存 Android WebView自带的缓存

1. 浏览器缓存

根据 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或Etag)

等字段来控制文件缓存的机制浏览器自己实现,我需我们处理


2. App Cache


方便构建Web App的缓存,存储静态文件(如JS、CSS、字体文件)


WebSettings settings=getSettings;
String cacheDirPath=context.getFilesDir.getAbsolutePath+"cache/";
settings.setAppCachePath(cacheDirPath);
settings.setAppCacheMaxSize(20*1024*1024);
settings.setAppCacheEnabled(true);

3. Dom Storage

WebSettings settings=getSettings;
settings.setDomStorageEnabled(true);

4. Indexed Database

// 只需设置支持JS就自动打开IndexedDB存储机制
// Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。
WebSettings settings=getSettings;
settings.setJavaScriptEnabled(true);
  • 资源预加载 预加载webview对象,首次初始化WebView会比第二次慢很多的原因:初始化后,即使webview已经释放,但是WebView的一些共享的对象依然是存在的,我们可以在Application里面提前初始化一个Webview的对象,然后可以直接loadurl加载资源

  • 资源拦截 可以将跟新频率低的一些资源静态文件放在本地,拦截h5的资源网络请求并进行检测,如果检测到,就直接拿本地的资源进行替换即可


// 拦截资源 通常用于h5的首页页面,将常用的一些资源,放到本地
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {

if(request?.url.toString.contains("logo.jpg")){
var inputStream: InputStream?=
inputStream=applicationContext.assets.open("images/test.jpg")
return WebResourceResponse("image/png","utf-8", inputStream)
}

return super.shouldInterceptRequest(view, request)
}

常见的使用注意事项

1. Android9.0 已经禁止了webview使用http,怎么解决?

在manifest Application标签下面使用:

android:usesCleartextTraffic="true"

2. 开启混淆之后,Android无法与h5交互?

#保留annotation, 例如 @JavascriptInterface 等 annotation
-keepattributes *Annotation*

#保留跟 javascript相关的属性
-keepattributes JavascriptInterface

#保留JavascriptInterface中的方法
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
#这个类是用来与js交互,所以这个类中的 字段 ,方法, 不能被混淆、全路径名称.类名
-keepclassmembers public class com.youpackgename.xxx.H5CallBackAndroid{
<fields>;
<methods>;
public *;
private *;
}

3. 如何调试?

3.1 在WebViewActivity里面,开启调试

// 开启调试
WebView.setWebContentsDebuggingEnabled(true)

3.2 chrome浏览器地址栏输入 chrome://inspect

3.3 手机打开USB调试,打开webview页面,点击chrome页面的最下面的inspect,这样,便可以进入了web开发,看控制台,网络请求等

日问题:

看到WebView是不是很头疼?

者:字节移动技术——段文斌

前言

众所周知,精确的推荐离不开大量埋点,常见的埋点采集方案是在响应用户行为操作的路径上进行埋点。但是由于 App 通常会有比较多界面和操作路径,主动埋点的维护成本就会非常大。所以行业的做法是无埋点,而无埋点实现需要 AOP 编程。

一个常见的场景,比如想在UIViewController出现和消失的时刻分别记录时间戳用于统计页面展现的时长。要达到这个目标有很多种方法,但是 AOP 无疑是最简单有效的方法。Objective-C 的 Hook 其实也有很多种方式,这里以 Method Swizzle 给个示例。

@interface UIViewController (MyHook)

@end

@implementation UIViewController (MyHook)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        /// 常规的 Method Swizzle封装
        swizzleMethods(self, @selector(viewDidAppear:), @selector(my_viewDidAppear:));
        /// 更多Hook
    });
}

- (void)my_viewDidAppear:(BOOL)animated {
  /// 一些Hook需要的逻辑

  /// 这里调用Hook后的方法,其实现其实已经是原方法了。
  [self my_viewDidAppear: animated];
}

@end

接下来我们探讨一个具体场景:

UICollectionView或者UITableView是 iOS 中非常常用的列表 UI 组件,其中列表元素的点击事件回调是通过delegate完成的。这里以UICollectionView为例,UICollectionViewdelegate,有个方法声明,collectionView:didSelectItemAtIndexPath:,实现这个方法我们就可以给列表元素添加点击事件。

我们的目标是 Hook 这个 delegate 的方法,在点击回调的时候进行额外的埋点操作。

方案迭代

方案 1 Method Swizzle

通常情况下,Method Swizzle 可以满足绝大部分的 AOP 编程需求。因此首次迭代,我们直接使用 Method Swizzle 来进行 Hook。

@interface UICollectionView (MyHook)

@end

@implementation UICollectionView (MyHook)

// Hook, setMyDelegate:和setDelegate:交换过
- (void)setMyDelegate:(id)delegate {
    if (delegate !=nil) {
        /// 常规Method Swizzle
        swizzleMethodsXXX(delegate, @selector(collectionView:didSelectItemAtIndexPath:), self, @selector(my_collectionView:didSelectItemAtIndexPath:));

    }

    [self setMyDelegate:nil];
}

- (void)my_collectionView:(UICollectionView *)ccollectionView didSelectItemAtIndexPath:(NSIndexPath *)index {
  /// 一些Hook需要的逻辑

  /// 这里调用Hook后的方法,其实现其实已经是原方法了。
  [self my_collectionView:ccollectionView didSelectItemAtIndexPath:index];
}

@end

我们把这个方案集成到 App 里面进行测试验证,发现没法办法验证通过。

主要原因 App 是一个庞大的项目,其中引入了非常多的三方库,比如 IGListKit 等,这些三方库通常对UICollectionView的使用都进行了封装,而这些封装,恰恰导致我们不能使用常规的 Method Swizzle 来 Hook 这个 delegate。直接的原因总结有以下两点:

  1. setDelegate传入的对象不是实现UICollectionViewDelegate协议的那个对象

如图示,setDelegate传入的是一个代理对象 proxy,proxy 引用了实际的实现UICollectionViewDelegate协议的delegate,proxy 实际上并没有实现UICollectionViewDelegate的任何一个方法,它把所有方法都转发给实际的delegate