Android混合開發全解析

     現在的app都開始流行混合開發了,這是一個app開發的新技術,作爲android程序猿的我們也應該要了解並且掌握他。那麼在使用之前,我們一定要搞清楚,我們的哪些場景使用混合開發好一些呢?這個問題一定要搞清楚,因爲現在的混合開發還不成熟,Web頁面的渲染效率目前還無法和Native的體驗相比,而大家如果只是爲了採用新技術就盲目的使用混合開發,最後遇到一些體驗問題的話,肯定會得不償失。

     那麼什麼情況適合Html 5開發呢?像一些活動頁面,比如秒殺、團購等適合做Html 5,因爲這些頁面可能涉及的非常炫而且複雜,Html 5開發或許會簡單點,關鍵是這些頁面時效性短,更新更快,因爲一個活動說不定就一週時間,下週換活動,如果這樣的話,你還做Native是肯定不行的,這些場景就需要使用混合開發了。以下是網上能找到的一些比較好的入門介紹,大家可以學習一下。

     談談Android App混合開發

     Android 混合開發 的一些心得

     混合開發的實質就是在JS和Native之間相互調用,其中的第一篇博客中也提到了,實現混合開發的方式主要的有兩種:1、js調用Native中的代碼;2、WebView攔截頁面跳轉。第二種方式因爲在Android 4.2(API 17)一下存在高危的漏洞,漏洞的原理就是Android系統通過 WebView.addJavascriptInterface(Object o, String interface) 方法註冊可供js調用的Java對象,但是系統並沒有對註冊的Java對象方法調用做限制。導致攻擊者可以利用反射調用未註冊的其他任何Java對象,攻擊者可以根據客戶端的能力做任何事情,如下的文章詳細講解了漏洞產生的根本原因:

     WebView 遠程代碼執行漏洞淺析

     上面的博主使用的是別人封裝好的一個js框架,git地址如下:

     safe-java-js-webview-bridge

     而現在介紹較多的還有Facebook的混合開發框架React Native,大家也可以去看一下:

     Use React Native

     react-native

     我們本節要分析的就是safe-java-js-webview-bridge框架了,這裏的代碼也非常簡潔,主界面是WebActivity,看了下我這裏的項目源碼,有兩個WebView類,一個是在frameworks/base/tools/layoutlib/bridge/src/android/webkit路徑下的WebView,它是繼承MockView的,還有一個是在vendor/letv/webview/base/core/java/android/webkit路徑下,開始看到vendor目錄,還以爲把原生的東西重寫的,後來問了下瀏覽器模塊的同事,才知道這不是重寫,而是把原生的移動了個目錄而已,我們後面的分析也都是在這個包下面的類。


     我們從斷點可以看到,獲取回來的WebSettings的實現類是一個名稱爲ContentSettingsAdapter的對象,整個源碼搜遍,找不到任何相關的東西,看來還是沒有源碼。這些可能也是谷歌Chrom瀏覽器的一些核心技術了,如果有哪位精通的,請指點我一下。


     我們先來看一下整個代碼的執行邏輯:


     重點的地方我也標紅出來了,整個WebView上的事件響應、界面顯示都是在WebViewChromiumFactoryProvider類中處理的,WebViewChromiumFactoryProvider類是通過反射生成的,後邊分析的過程中,大家會看到它的產生過程,這些沒有源碼,我們也無從得知它的處理邏輯;還有一個重點的地方,就是最後標紅的那塊,就是在構造JsCallJava對象時,通過StringBuilder拼接一個javascript的角本出來,拼接過程當中,就會通過調用genJavaMethodSign方法,把我們要回調的類的所有方法連接成string字符串注入到javascript角本當中,這裏也就是爲什麼WebView瀏覽器能回調我們java代碼,並且我們可以通過返回來的參數知道是要調用哪個方法的原因了。

     好了,下面我們就一起來看一下整個代碼的執行過程。

     首先,通過new構造一個WebView對象,WebView的構造方法不斷的轉調,最終調用了protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing)構造方法,我們來看一下它的實現:

    /**
     * @hide
     */
    @SuppressWarnings("deprecation")  // for super() call into deprecated base class constructor.
    protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
            Map javaScriptInterfaces, boolean privateBrowsing) {
        super(context, attrs, defStyleAttr, defStyleRes);
        if (context == null) {
            throw new IllegalArgumentException("Invalid context argument");
        }
        sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >=
                Build.VERSION_CODES.JELLY_BEAN_MR2;
        checkThread();
        if (TRACE) Log.d(LOGTAG, "WebView");

        ensureProviderCreated();
        mProvider.init(javaScriptInterfaces, privateBrowsing);
        // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed.
        CookieSyncManager.setGetInstanceIsAllowed();
    }
     調用super父類的構造方法來初始化一些成員變量,這個過程我們就不分析了,跟之前動畫全解析中的初始化的道理基本是一樣的。最重要的就是ensureProviderCreated()這句了,它是對成員變量mProvider進行賦值的,這個mProvider也就是WebView最核心的東西了。ensureProviderCreated方法當中先通過調用getFactory()來獲取一個WebViewFactoryProvider對象,然後再用它的createWebView方法爲給成員變量mProvider賦值,getFactory方法中的實現又是調用WebViewFactory.getProvider()來完成的,我們來看一下這個方法的執行過程:

    static WebViewFactoryProvider getProvider() {
        synchronized (sProviderLock) {
            // For now the main purpose of this function (and the factory abstraction) is to keep
            // us honest and minimize usage of WebView internals when binding the proxy.
            if (sProviderInstance != null) return sProviderInstance;

            final int uid = android.os.Process.myUid();
            if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID) {
                throw new UnsupportedOperationException(
                        "For security reasons, WebView is not allowed in privileged processes");
            }

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
            try {
                Class providerClass = getProviderClass();

                StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()");
                try {
                    sProviderInstance = providerClass.getConstructor(WebViewDelegate.class)
                            .newInstance(new WebViewDelegate());
                    if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
                    return sProviderInstance;
                } catch (Exception e) {
                    Log.e(LOGTAG, "error instantiating provider", e);
                    throw new AndroidRuntimeException(e);
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
                    StrictMode.setThreadPolicy(oldPolicy);
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
        }
    }
     這個方法的返回值是WebViewFactoryProvider,它是一個接口,那麼在這個過程中,肯定是生成了一個實體對象的,可以看到try代碼塊中的邏輯,調用getProviderClass()方法獲取到一個providerClass,然後通過反射調用providerClass.getConstructor(WebViewDelegate.class).newInstance(new WebViewDelegate())來生成一個該類的對象sProviderInstance返回給調用者。那我們接下來就看一下getProviderClass方法的實現:

    private static Class getProviderClass() {
        try {
            // First fetch the package info so we can log the webview package version.
            sPackageInfo = fetchPackageInfo();
            Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
                sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
            loadNativeLibrary();
            Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
            try {
                return getChromiumProviderClass();
            } catch (ClassNotFoundException e) {
                Log.e(LOGTAG, "error loading provider", e);
                throw new AndroidRuntimeException(e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
        } catch (MissingWebViewPackageException e) {
            // If the package doesn't exist, then try loading the null WebView instead.
            // If that succeeds, then this is a device without WebView support; if it fails then
            // swallow the failure, complain that the real WebView is missing and rethrow the
            // original exception.
            try {
                return (Class) Class.forName(NULL_WEBVIEW_FACTORY);
            } catch (ClassNotFoundException e2) {
                // Ignore.
            }
            Log.e(LOGTAG, "Chromium WebView package does not exist", e);
            throw new AndroidRuntimeException(e);
        }
    }
     首先調用loadNativeLibrary()加載動態庫,然後調用getChromiumProviderClass方法去生成我們要的class對象,那麼繼續跟蹤看一下這個方法的實現:

    private static final String CHROMIUM_WEBVIEW_FACTORY =
            "com.android.webview.chromium.WebViewChromiumFactoryProvider";

    private static Class getChromiumProviderClass()
            throws ClassNotFoundException {
        Application initialApplication = AppGlobals.getInitialApplication();
        try {
            // Construct a package context to load the Java code into the current app.
            Context webViewContext = initialApplication.createPackageContext(
                    sPackageInfo.packageName,
                    Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
            initialApplication.getAssets().addAssetPath(
                    webViewContext.getApplicationInfo().sourceDir);
            ClassLoader clazzLoader = webViewContext.getClassLoader();
            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
            try {
                return (Class) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true,
                                                                     clazzLoader);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
        } catch (PackageManager.NameNotFoundException e) {
            throw new MissingWebViewPackageException(e);
        }
    }
     在這個方法中,我們就看到要加載的目標類CHROMIUM_WEBVIEW_FACTORY,也就是我們流程圖中紅色標註的重點,生成這個類是用的當前webViewContext的類加載器來生成的,在Java的雙親委派模型中,大家可以知道,使用instanceof判斷兩個類是否相同時,相同的條件有兩個:1、類的路徑完全相同;2、加載這個類的加載器完全相同,只有這兩個條件同時滿足,instanceof判斷纔會爲true,而最終的重量級類WebViewChromiumFactoryProvider沒有源碼,這樣我們後邊的很多分析過程也就無從得知了。

     上面的過程爲我們創建了最重要的處理類,後邊的邏輯基本就很簡單了,繼續調用createWebView生成一個WebViewProvider對象,並賦值給成員變量mProvider,然後調用init方法初始化,這樣WebView對象就創建好了。然後調用wv.getSettings()獲取一個WebSettings對象,並通過setJavaScriptEnabled方法設置它支持javascript,然後調用setWebChromeClient將我們的回調注入進去,最後設置WebView要加載的url地址。

     我們主要來看一下setWebChromeClient將我們的回調類注入進去的過程。CustomChromeClient的構造方法中直接調用父類InjectedChromeClient的構造方法,傳入的兩個參數"HostApp", HostJsScope.class,分別就是注入到H5頁面中的名稱和回調Native的Java類,在這裏就利用這兩個參數構造一個JsCallJava對象,我們再來看一下JsCallJava類的構造方法的實現:

    public JsCallJava (String injectedName, Class injectedCls) {
        try {
            if (TextUtils.isEmpty(injectedName)) {
                throw new Exception("injected name can not be null");
            }
            mInjectedName = injectedName;
            mMethodsMap = new HashMap();
            //獲取自身聲明的所有方法(包括public private protected), getMethods會獲得所有繼承與非繼承的方法
            Method[] methods = injectedCls.getDeclaredMethods();
            StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\"");
            sb.append(mInjectedName);
            sb.append(" initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};");
            for (Method method : methods) {
                String sign;
                if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == null) {
                    continue;
                }
                mMethodsMap.put(sign, method);
                sb.append(String.format("a.%s=", method.getName()));
            }

            sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
            sb.append(mInjectedName);
            sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h
     在這裏我們可以看到,首先根據傳進來的Class,利用反射獲取到它的所有可執行方法,將這些方法保存在成員變量mMethodsMap當中,後邊H5頁面回調回來了,我們就根據傳回來的參數對比一下,就知道需要執行哪個方法了,獲取到的所有的方法也會通過string字符串的形式拼接成一個完整的javascript角本注入到WebView當中,這裏執行完,那麼所有的環境都準備好了,在WebView加載過程中,會回調onProgressChanged方法,也就是在這裏,把我們拼接好的javascript角本注入進去的。我們把拼接完成的javascript角本字符串打印出來,用HBuilder格式化整齊看一下,如下圖:


     看到javascript的角本看是頭大了,語法好亂,可能也是自己不懂吧,以後還得好好學習。那麼當我們在H5頁面上點擊的時候,就會通過javascript角本處理,然後回調WebView的onJsPrompt方法,也就是InjectedChromeClient類的onJsPrompt方法了,相應的參數都會通過這個方法中的message參數以string字符串的形式傳過來,我們可以看一下獲取IMSI方法的參數:


    這樣就能保證Native和H5溝通無阻了,既然已經調用回來了,參數也都給我們了,那麼接下來在native中執行就簡單了,根據我們之前保存好的方法去匹配,找到目標後就直接執行,最後把結果返回給H5,返回數據給H5當然也是系統已經給我們把框架搭建好了的,我們只需要把數據傳進去就OK了,真是太妙了!!!

     好了,理解完整個過程,那我們要自己去實現一個也就就簡單了,最後我們來把要點總結一下:

     1:一定要支持javascript,可以通過ws.setJavaScriptEnabled(true)來設置

     2:要設置一個H5回調Native的ChromeClient對象,可以通過wv.setWebChromeClient來完成

     3:實現好你的Native回調類,也就是我要在這個類中幹些什麼事情,比如我要打開Activity,顯示對話框,或者獲取手機信息返回給H5等等

     4:要將你的回調類的所有方法通過javascript角本注入到ChromeClient當中,注入是在ChromeClient的onProgressChanged方法中完成的,注入的數據是以string拼接出來的

發佈了94 篇原創文章 · 獲贊 110 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章