天天看點

Native 與 H5 互動的那些事

hybrid開發模式目前幾乎每家公司都有涉及和使用,這種開發模式兼具良好的native使用者互動體驗的優勢與webapp跨平台的優勢,而這種 模式,在android中必然需要webview作為載體來展示h5内容和進行互動,而webview的各種安全性、相容性的問題,我想大多數人與它友誼 的小床已經翻了,特别是4.2版本之前的addjavascriptinterface接口引起的漏洞,可能導緻惡意網頁通過js方法周遊剛剛通過 addjavascriptinterface注入進來的類的所有方法從中擷取到getclass方法,然後通過反射擷取到runtime對象,進而調用 runtime對象的exec方法執行一些操作,惡意的js代碼如下:

為了避免這個漏洞,即需要限制js代碼能夠調用到的native方法,官方于是在從4.2開始的版本可以通過為可以被js調用的方法添加@javascriptinterface注解來解決,而之前的版本雖然不能通過這種方法解決,但是可以使用js的prompt方法進行解決,隻不過需要和前端協商好一套公共的協定,除此之外,為了避免webview加載任意url,也需要對url進行白名單檢測,由于android碎片化太嚴重,webview也存在相容性問題,webview的核心也在4.4版本進行了改變,由webkit改為chromium,此外webview還有一個非常明顯的問題,就是記憶體洩露,根本原因就是activity與webview關聯後,webview内部的一些操作的執行在新線程中,這些時間無法确定,而可能導緻webview一直持有activity的引用,不能回收。下面就談談怎樣正确安全的讓native與h5互動

native與h5怎樣安全的進行互動?

要使得h5内的js與native之間安全的互相進行調用,我們除了可以通過添加@javascriptinterface注解來解決(>=4.2),還有通過prompt的方式,不過如果使用官方的方式,這就需要對4.2以下做相容了,這樣使得我們一個app中有兩套js與native互動的方式,這樣極其不好維護,我們應該隻需要一套js與native互動的方式,是以,我們借助js中的prompt方法來實作一套安全的js與native互動的jsbridge架構

1.1 js與native代碼互相調用

我們知道如果native需要調用js中的方法,隻需要使用webview:loadurl();方法即可直接調用指定js代碼,如:

這樣就直接調用了js中的setusername方法并把zhengxiaoyong這個名字傳到這個方法中去了,接下來就是js自己處理了

而如果js要調用native中的java方法呢?這就需要我們自己實作了,因為我們不采取javascriptinterface的方式,而采取prompt方式

對webview熟悉的同學們應該都知道js中對應的window.alert()、window.confirm()、window.prompt()這三個方法的調用在webchromeclient中都有對應的回調方法,分别為:

onjsalert()、onjsconfirm()、onjsprompt(),對于它們傳入的message,都可以在相應的回調方法中接收到,是以,對于js調native方法,我們可以借助這個信道,和前端協定好一段特定規則的message,這個規則中應至少包含這些資訊:

所調用native方法所在類的類名

所調用native的方法名

js調用native方法所傳入的參數

是以基于這些資訊,很容易想到使用http協定的格式來協定規則,如下格式:

對應的我們協定prompt傳入message的格式為:

這樣以來,前端和app端協商好後,以後前端需要通過js調用native方法來擷取一些資訊或功能,就隻需要按照協定的格式把需要調用的類名、方法名、參數放入對應得位置即可,而我們會在onjsprompt方法中接受到,是以我們根據與前端協定好的協定來進行解析,我們可以用一個uri來包裝這段協定,然後通過uri:gethost、getpath、getquery方法擷取對應的類名,方法名,參數資料,最後通過反射來調用指定類中指定的方法

而此時會有人問?port是用來幹嘛的?params格式是kv還是什麼格式?

當然,既然和前端協定好了協定的格式了,那麼params肯定也是需要協定好的,可以用kv格式,也可以用一串json字元串表示,為了解析友善,還是建議使用json格式

而port是用來幹嘛的呢?

port我們并不會直接操作它,它是由js代碼自動生成的,port的作用是為了辨別js中的回調function,當js調用native方法時,我們會得到本次調用的port号,我們需要在native方法執行完畢後再把該port、執行的後結果、是否調用成功、調用失敗的msg等資訊通過調用js的oncomplete方法傳入,這時候js憑什麼知道你本次傳回的資訊是哪次調用的結果呢?就是通過port号,因為在js調用native方法時我們會把自動生成的port号和此次回調的function綁定在一起,這樣以來native方法傳回結果時把port也帶過來,就知道是哪次回調該用哪個function方法來處理

自動生成port和綁定function回調的js代碼如下:

js代碼上已經注釋的很清楚了,就不多解釋了。

經過上面介紹,那麼在native方法執行完成後,當然就需要把結果傳回給js了,那麼結果的格式又是什麼呢?傳回給js方法又是什麼呢?

沒錯,還是需要和前端進行協定,建議資料的傳回格式為json字元串,基本格式為:

其中定義了一個status,這樣的好處是無論在native方法調用成功與否、native方法是否有傳回值,js中都可以收到傳回的資訊,而這個json字元串至少都會包含一個statusjson對象來描述native方法調用的狀況

而傳回給js的方法自然是上面的oncomplete方法:

ps:rainbowbridge是我的jsbridge架構的名字

至此js調用native的流程就分析完成了,一切都看起來那麼美妙,因為,我們自己實作一套js invoke native的主要目的是讓js調用native更加安全,同時也隻維護一套jsbridge架構更加友善,那麼這個安全性表現在哪裡了?

我們知道之前原生的方式漏洞就是惡意js代碼可能會調用native中的其它方法,那麼答案出來了,如果需要讓js invoke native保證安全性,隻需要限制我們通過反射可調用的方法,是以,在jsbridge架構中,我們需要對js能調用的native方法給予一定的規則,隻有符合這些規則js才能調用,而我的規則是:

1、native方法包含public static void 這些修飾符(當然還可能有其它的,如:synchronized)

2、native方法的參數數量和類型隻能有這三個:webview、jsonobject、jscallback。為什麼要傳入這三個參數呢?

2.1、第一個參數是為了提供一個webview對象,以便擷取對應context和執行webview的一些方法

2.2、第二個參數就是js中傳入過來的參數,這個肯定要的

2.3、第三個參數就是當native方法執行完畢後,把執行後的結果回調給js對應的方法中

是以符合js調用的native方法格式為:

判斷js調用的方法是否符合該格式的代碼為,符合則存入一個map中供js調用:

對于有傳回值的方法,并不需要設定它的傳回值,因為方法的結果最後我們是通過jscallback.invokejscallback來進行對js層的回調,比如我貼一個符合該格式的native方法:

js調native代碼執行耗時操作情況處理

一般情況下,比如我們通過js調用native方法來擷取appname、ossdk版本、imsi号、使用者資訊等都不會有問題,但是,假如該native方法需要執行一些耗時操作,如:io、sp、bitmap decode、sqlite等,這時為了保護ui的流暢性,我們需要讓這些操作執行在異步線程中,待執行完畢再把結果回調給js,而我們可以提供一個線程池來專門處理這些耗時操作,如:

【注】:對于webview,它的方法的調用隻能在主線程中調用,當設計到webview的方法調用時,切記不可以放在異步線程中調用,否則就gg了.

js調native流程圖

Native 與 H5 互動的那些事

jsbridge效果圖

Native 與 H5 互動的那些事

1.2 白名單check

上面我們介紹了jsbridge的基本原理,實作了js與native互相調用,而且還避免了惡意js代碼調用native方法的安全問題,通過這樣我們保證了js調用native方法的安全性,即js不能随意調用任意native方法,不過,對于webview容器來說,它并不關心所加載的url是js代碼還是網頁位址,它所做的工作就是執行我們傳入的url,而webview加載url的方式有兩種:get和post,方式如下:

對于這兩種方式,也有不同的應用點,一般get方式用于查,也就是傳入的資料不那麼重要,比如:商品清單頁、商品詳情頁等,這些傳入的資料隻是一些商品類的資訊。而post方式一般用于改,post傳入的資料往往是比較私密的,比如:訂單界面、購物車界面等,這些界面隻有在把使用者的資訊post給伺服器後,伺服器才能正确的傳回相應的資訊顯示在界面上。是以,對于post方式涉及到使用者的私密資訊,我們總不能給一個url就把私密資料往這個url裡面發吧,當然不可能的,這涉及到安全問題,那麼就需要一個白名單機制來檢查url是否是我們自己的,是我們自己的那麼即可以post資料,不是我們自己的那就不post資料,而白名單的定義通常可以以我們自己的域名來判斷,搞一個正規表達式,是以我們可以重寫webview的posturl方法:

這樣就對不是我們自己的url進行了攔截,不把資料發送到不是我們自己的伺服器中

至此,白名單的check還沒有完成,因為這隻是對webview加載url時候做的檢查,而在webview内各中連結的跳轉、其中有些url還可能被營運商劫持注入了廣告,這就有可能在webview容器内的跳轉到某些界面後,該界面的url并不是我們自己的,但是它裡面有js代碼調用native方法來擷取一些資料,雖然說js并不能随便調我們的native方法,但是有些我們指定可以被調用的native方法可能有一些擷取裝置資訊、讀取檔案、擷取使用者資訊等方法,是以,我們也應該在js調用native方法時做一層白名單check,這樣才能保證我們的資訊安全

是以,白名單檢測需要在兩個地方進行檢測:

1、webview:posturl()前檢測url的合法性

2、js調用native方法前檢測目前界面url的合法性

具體代碼如下:

1.3 移除預設内置接口

webview内置預設也注入了一些接口,如下:

這些接口雖然不會影響用prompt方式實作的js與native互動,但是在使用addjavascriptinterface方式時,有可能有安全問題,最好移除

webview相關

2.1 webview的配置

下面給出webview的通用配置:

其中有一項配置,是在4.4以上版本時設定網頁内圖檔可以自動加載,而4.4以下版本則不可自動加載,原因是4.4webview核心的改變,使得webview的性能更優,是以在4.4以下版本不讓圖檔自動加載,而是先讓webview加載網頁的其它靜态資源:js、css、文本等等,待網頁把這些靜态資源加載完成後,在onpagefinished方法中再把圖檔自動加載打開讓網頁加載圖檔:

2.2 webview的獨立程序

通常來說,webview的使用會帶來諸多問題,記憶體洩露就是最常見的問題,為了避免webview記憶體洩露,目前最流行的有兩種做法:

1、獨立程序,簡單暴力,不過可能涉及到程序間通信

2、動态添加webview,對傳入webview中使用的context使用弱引用,動态添加webview意思在布局建立個viewgroup用來放置webview,activity建立時add進來,在activity停止時remove掉

個人推薦獨立程序,好處主要有兩點,一是在webviewactivity使用完畢後直接幹掉該程序,防止了記憶體洩露,二是為我們的app主程序減少了額外的記憶體占用量

使用獨立程序還需注意一點,這個程序中在有多個webviewactivity,不能在activity銷毀時就幹掉程序,不然其它activity也會蹦了,此時應該在該程序建立一個activity的維護集合,集合為空時即可幹掉程序

關于webview的銷毀,如下:

2.3 webview的相容性

2.3.1 不同版本硬體加速的問題

2.3.2 不同裝置點選webview輸入框鍵盤的不彈起

2.3.3 三星手機硬體加速關閉後導緻h5彈出的對話框出現不消失情況

2.3.4 不同版本shouldoverrideurlloading的回調時機

對于shouldoverrideurlloading的加載時機,有些同學經常與onprogresschanged這個方法的加載時機混淆,這兩個方法有兩點不同:

1、shouldoverrideurlloading隻會走get方式的請求,post方式的請求将不會回調這個方法,而onprogresschanged對get和post都會走

2、shouldoverrideurlloading都知道在webview内部點選連結(get)會觸發,它在get請求打開界面時也會觸發,shouldoverrideurlloading還有一點特殊,就是在按傳回鍵傳回到上一個頁面時時不會觸發的,而onprogresschanged在隻要界面更新了都會觸發

對于shouldoverrideurlloading的傳回值,傳回true為剝奪webview對該此請求的控制權,交給應用自己處理,是以webview也不會加載該url了,傳回false為webview自己處理

對于shouldoverrideurlloading的調用時機,也會有不同,在3.0以上是會正常調用的,而在3.0以下,并不是每次都會調用,可以在onpagestarted方法中做處理,也沒必要了,現在應該都适配4.0以上了

2.3.5 頁面重定向導緻webview:goback()無效的處理

像一些界面有重定向,比如:淘寶等,需要按多次(>1)才能正常傳回,一般都是二次,是以可以把那些具有重定向的界面存入一個集合中,在攔截傳回事件中這樣處理:

這裡處理是在按傳回鍵時,如果上一個界面是重定向界面,則直接調用goback,或者也可以finish目前activity

2.3.6 webview無法加載不信任網頁ssl錯誤的處理

有時我們的webview會加載一些不信任的網頁,這時候預設的處理是webview停止加載了,而那些不信任的網頁都不是由ca機構信任的,這時候你可以選擇繼續加載或者讓手機内的浏覽器來加載:

2.3.7 自定義webview加載出錯界面

出錯的界面的顯示,可以在這個方法中控制:

你可以重新加載一段html專門用來顯示錯誤界面,或者用布局顯示一個出錯的view,這時候需要把出錯的webview内容清除,可以使用:

2.3.8 擷取位置權限的處理

如果在webview中有擷取地理位置的請求,那麼可以直接在代碼中預設處理了,沒必要彈出一個框框讓使用者每次都确認:

2.4 打造一個通用的webviewactivity界面

一個通用的webviewactivity當然是樣式和webview内部處理的政策都統一樣,這裡隻對樣式進行說明,因為webview内部的處理各個公司都不一樣,但應該都需要包含這麼幾點吧:

1、白名單檢測

2、url的跳轉

3、出錯的處理

4、…

一個webviewactivity界面,最主要的就是toolbar标題欄的設計了,因為不同的app的webviewactivity界面toolbar上有不同的icon和操作,比如:分享按鈕、重新整理按鈕、更多按鈕,都不一樣,既然需要通用,即可讓調用者傳入某個參數來動态改變這些東西吧,比如傳一個toolbarstyle來辨別此webviewactivity的風格是什麼樣的,背景色、字型顔色、圖示等,包括點選時的動畫效果,作為通用的界面,必須是讓調用者簡單操作,不可能調用時傳入一個圖示id還是一個drawable,是以,主要需要用到tint,來對字型、圖示的顔色動态改變,代碼如下:

h5與native界面互相喚起

對于h5界面,有些操作往往是需要喚起native界面的,比如:h5中的登入按鈕,點選後往往喚起native的登入界面來進行登入,而不是直接在h5登入,這樣一個app就隻需要一套登入了,而我們所做的便是攔截登入按鈕的url:

這個規則我們可以在native的activity的intent-filter中的data來定義,如下:

解析url過程是判斷scheme、host、path的是否有完全與之比對的,有則喚起

而native喚h5,其實也是一個url的解析過程,隻不過需要配置webviewactivity的intent-filter的data,webviewactivity的scheme配置為http和https

上面說到了h5與native互相調起,其實這個可以在app内做成一套界面跳轉的方式,摒棄startactivity,為什麼原生的跳轉方式不佳?

1、因為原生的跳轉需要确定該activity是已經存在的,否則編譯将報錯,這樣帶來的問題是不利于協同開發,如:a、b同學分别正在開發項目的兩個不同的子產品,此時b剛好需要跳a同學的某一個界面,如商品清單頁跳商品詳情頁,這時候b就必須寫個todo,待b完成該子產品後再寫了。而通過url跳轉,隻需要傳入一串url即可

2、原生的跳轉activity與目标activity是耦合的,跳轉activity完全依賴于目标activity

3、原生的跳轉方式不利于管理所傳遞來的參數,擷取參數時需要在跳轉activity的地方确定傳遞了幾個參數、什麼類型的參數,這樣以來跳轉的方式多了,就比較混亂了。當然一個原生跳轉良好的設計是在目的activity實作一個靜态的start方法,其它界面要跳直接調用即可

4、最後一個就是在有參數傳遞的情況下,每次跳轉都要寫好多代碼啊

而urlrouter架構的實作原理,一種實作是可以維護一套activity與url的映射表,這種方式還是沒有擺脫不利于協同開發這個毛病,另外一種是通過一串指定規則的url與manifest中配置的data比對,具體跳轉則是通過intent.setdata()來設定跳轉的url,這種方式比較好,不過需要處理下比對到多個activity時優先選擇的問題

====================================分割線================================