目前的網速環境而言,應用程序內加載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的樣式等大可放心使用。
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) // 關閉密碼保存提醒功能
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)
}
常見的使用注意事項
在manifest Application標簽下面使用:
android:usesCleartextTraffic="true"
#保留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.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為例,UICollectionView的delegate,有個方法聲明,collectionView:didSelectItemAtIndexPath:,實現這個方法我們就可以給列表元素添加點擊事件。
我們的目標是 Hook 這個 delegate 的方法,在點擊回調的時候進行額外的埋點操作。
通常情況下,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。直接的原因總結有以下兩點:
如圖示,setDelegate傳入的是一個代理對象 proxy,proxy 引用了實際的實現UICollectionViewDelegate協議的delegate,proxy 實際上并沒有實現UICollectionViewDelegate的任何一個方法,它把所有方法都轉發給實際的delegate。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。