qmsggg / qmsggg_BlogCollect

仅仅只是用于学习和记录使用,里面包括了自己学习android的点点滴滴,希望自己在以后的时间能把之前由于时间原因没有完成的完成了,以此自勉。
57 stars 18 forks source link

Android应用开发实践之其它 #140

Open qmsggg opened 6 years ago

qmsggg commented 6 years ago

Android混合编程:WebView实践

文章目录

一 基本用法

WebView也是Android View的一种, 我们通常用它来在应用内部展示网页, 和以往一样, 我们先来简单看一下它的基本用法。

添加网络权限

<uses-permission android:name="android.permission.INTERNET" />

在布局中添加WebView

<?xml version="1.0" encoding="utf-8"?>
<WebView  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/webview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
/>

使用WebView加载网页

WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("http://www.example.com");

以上就是WebView的简单用法, 相比大家已经十分熟悉, 下面我们就来逐一看看WebView的其他特性。

WebView基本组件

了解了基本用法, 我们对WebView就有了大致的印象, 下面我们来看看构建Web应用的三个重要组件。

WebSettings

WebSettings用来对WebView做各种设置, 你可以这样获取WebSettings:

WebSettings webSettings = mWebView .getSettings();

WebSettings的常见设置如下所示:

JS处理

缩放处理

内容布局

文件缓存

其他设置

WebViewClient

WebViewClient用来帮助WebView处理各种通知, 请求事件。我们通过继承WebViewClient并重载它的方法可以实现不同功能的定制。具体如下所示:

WebChromeClient

WebChromeClient用来帮助WebView处理JS的对话框、网址图标、网址标题和加载进度等。同样地, 通过继承WebChromeClient并重载它的方法也可以实现不同功能的定制, 如下所示:

WebView生命周期

onResume()

WebView为活跃状态时回调,可以正常执行网页的响应。

onPause()

WebView被切换到后台时回调, 页面被失去焦点, 变成不可见状态,onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行。

pauseTimers()

当应用程序被切换到后台时回调,该方法针对全应用程序的WebView,它会暂停所有webview的layout,parsing,javascripttimer。降低CPU功耗。

resumeTimers()

恢复pauseTimers时的动作。

destroy()

关闭了Activity时回调, WebView调用destory时, WebView仍绑定在Activity上.这是由于自定义WebView构建时传入了该Activity的context对象, 因此需要先从父 容器中移除WebView, 然后再销毁webview。

mRootLayout.removeView(webView);  
mWebView.destroy();

WebView页面导航

页面跳转

当我们在WebView点击链接时, 默认的WebView会直接跳转到别的浏览器中, 如果想要实现在WebView内跳转就需要设置WebViewClient, 下面我们先来 说说WebView、WebViewClient、WebChromeClient三者的区别。

如果我们想控制不同链接的跳转方式, 我们需要继承WebViewClient重写shouldOverrideUrlLoading()方法

    static class CustomWebViewClient extends WebViewClient {

        private Context mContext;

        public CustomWebViewClient(Context context) {
            mContext = context;
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (Uri.parse(url).getHost().equals("github.com/qmsggg")) {
                //如果是自己站点的链接, 则用本地WebView跳转
                return false;
            }
            //如果不是自己的站点则launch别的Activity来处理
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            mContext.startActivity(intent);
            return true;
        }
    }

关于shouldOverrideUrlLoading()方法的两点说明:

1 方法返回值

返回true: Android 系统会处理URL, 一般是唤起系统浏览器。 返回false: 当前 WebView 处理URL。

由于默认放回false, 如果我们只想在WebView内处理链接跳转只需要设置mWebView.setWebViewClient(new WebViewClient())即可

/** 
     * Give the host application a chance to take over the control when a new 
     * url is about to be loaded in the current WebView. If WebViewClient is not 
     * provided, by default WebView will ask Activity Manager to choose the 
     * proper handler for the url. If WebViewClient is provided, return true 
     * means the host application handles the url, while return false means the 
     * current WebView handles the url. 
     * This method is not called for requests using the POST "method". 
     * 
     * @param view The WebView that is initiating the callback. 
     * @param url The url to be loaded. 
     * @return True if the host application wants to leave the current WebView 
     *         and handle the url itself, otherwise return false. 
     */  
    public boolean shouldOverrideUrlLoading(WebView view, String url) {  
        return false;  
    }  

2 方法deprecated问题

shouldOverrideUrlLoading()方法在API >= 24时被标记deprecated, 它的替代方法是

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            view.loadUrl(request.toString());
            return true;
        }

但是public boolean shouldOverrideUrlLoading(WebView view, String url)支持更广泛的API我们在使用的时候还是它, 关于这两个方法的讨论可以参见:

http://stackoverflow.com/questions/36484074/is-shouldoverrideurlloading-really-deprecated-what-can-i-use-instead
http://stackoverflow.com/questions/26651586/difference-between-shouldoverrideurlloading-and-shouldinterceptrequest

页面回退

Android的返回键, 如果想要实现WebView内网页的回退, 可以重写onKeyEvent()方法。

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    // Check if the key event was the Back button and if there's history
    if ((keyCode == KeyEvent.KEYCODE_BACK) && myWebView.canGoBack()) {
        myWebView.goBack();
        return true;
    }
    // If it wasn't the Back key or there's no web page history, bubble up to the default
    // system behavior (probably exit the activity)
    return super.onKeyDown(keyCode, event);
}

页面滑动

关于页面滑动, 我们在做下拉刷新等功能时, 经常会去判断WebView是否滚动到顶部或者滚动到底部。

我们先来看一看三个判断高度的方法

getScrollY();

该方法返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离.

getHeight();
getBottom();

该方法都返回当前WebView这个容器的高度

getContentHeight(); 

返回的是整个html的高度, 但并不等同于当前整个页面的高度, 因为WebView有缩放功能, 所以当前整个页面的高度实际上应该是原始html的高度 再乘上缩放比例. 因此, 判断方法是:

if (webView.getContentHeight() * webView.getScale() == (webView.getHeight() + webView.getScrollY())) {
    //已经处于底端
}

if(webView.getScrollY() == 0){
    //处于顶端
}

以上这个方法也是我们常用的方法, 不过从API 17开始, mWebView.getScale()被标记为deprecated

This method was deprecated in API level 17. This method is prone to inaccuracy due to race conditions between the web rendering and UI threads; prefer onScaleChanged(WebView,

因为scale的获取可以用一下方式:

public class CustomWebView extends WebView {

public CustomWebView(Context context) {
    super(context);
    setWebViewClient(new WebViewClient() {
        @Override
        public void onScaleChanged(WebView view, float oldScale, float newScale) {
            super.onScaleChanged(view, oldScale, newScale);
            mCurrentScale = newScale
        }
    });
}

关于mWebView.getScale()的讨论可以参见:

https://developer.android.com/reference/android/webkit/WebView.html

http://stackoverflow.com/questions/16079863/how-get-webview-scale-in-android-4

WebView缓存实现

在项目中如果使用到WebView控件, 当加载html页面时, 会在/data/data/包名目录下生成database与cache两个文件夹。 请求的url记录是保存在WebViewCache.db, 而url的内容是保存在WebViewCache文件夹下。

控制缓存行为

WebSettings webSettings = mWebView.getSettings();
//优先使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 
//只在缓存中读取
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);
/不使用缓存
WwebSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);

清除缓存

clearCache(true); //清除网页访问留下的缓存,由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
clearHistory (); //清除当前webview访问的历史记录,只会webview访问历史记录里的所有记录除了当前访问记录.
clearFormData () //这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据。

WebView Cookies

添加Cookies

public void synCookies() {
    if (!CacheUtils.isLogin(this)) return;
    CookieSyncManager.createInstance(this);
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    cookieManager.removeSessionCookie();//移除
    String cookies = PreferenceHelper.readString(this, AppConfig.COOKIE_KEY, AppConfig.COOKIE_KEY);
    KJLoger.debug(cookies);
    cookieManager.setCookie(url, cookies);
    CookieSyncManager.getInstance().sync();
}

清除Cookies

CookieManager.getInstance().removeSessionCookie();

WebView本地资源访问

当我们在WebView中加载出从web服务器上拿取的内容时,是无法访问本地资源的,如assets目录下的图片资源,因为这样的行为属于跨域行为(Cross-Domain),而WebView是禁止 的。解决这个问题的方案是把html内容先下载到本地,然后使用loadDataWithBaseURL加载html。这样就可以在html中使用 file:///android_asset/xxx.png 的链接来引用包里 面assets下的资源了。

private void loadWithAccessLocal(final String htmlUrl) {
    new Thread(new Runnable() {
        public void run() {
            try {
                final String htmlStr = NetService.fetchHtml(htmlUrl);
                if (htmlStr != null) {
                    TaskExecutor.runTaskOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            loadDataWithBaseURL(htmlUrl, htmlStr, "text/html", "UTF-8", "");
                        }
                    });
                    return;
                }
            } catch (Exception e) {
                Log.e("Exception:" + e.getMessage());
            }

            TaskExecutor.runTaskOnUiThread(new Runnable() {
                @Override
                public void run() {
                    onPageLoadedError(-1, "fetch html failed");
                }
            });
        }
    }).start();
}

注意

二 代码交互

Android原生方案

关于WebView中Java代码和JS代码的交互实现, Android给了一套原生的方案, 我们先来看看原生的用法。后面我们还会讲到其他的开源方法。

JavaScript代码和Android代码是通过addJavascriptInterface()来建立连接的, 我们来看下具体的用法。

1 设置WebView支持JavaScript

webView.getSettings().setJavaScriptEnabled(true);

2 在Android工程里定义一个接口

public class WebAppInterface {
    Context mContext;

    /** Instantiate the interface and set the context */
    WebAppInterface(Context c) {
        mContext = c;
    }

    /** Show a toast from the web page */
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

注意: API >= 17时, 必须在被JavaScript调用的Android方法前添加@JavascriptInterface注解, 否则将无法识别。

3 在Android代码中将该接口添加到WebView

WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new WebAppInterface(this), "Android");

这个"Android"就是我们为这个接口取的别名, 在JavaScript就可以通过Android.showToast(toast)这种方式来调用此方法。

4 在JavaScript中调用Android方法

<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />

<script type="text/javascript">
    function showAndroidToast(toast) {
        Android.showToast(toast);
    }
</script>

在JavaScript中我们不用再去实例化WebAppInterface接口, WebView会自动帮我们完成这一工作, 使它能够为WebPage所用。

注意:

由于addJavascriptInterface()给予了JS代码控制应用的能力, 这是一项非常有用的特性, 但同时也带来了安全上的隐患,

Using addJavascriptInterface() allows JavaScript to control your Android application. This can be a very useful feature or a dangerous security issue. When the HTML in the WebView is untrustworthy (for example, part or all of the HTML is provided by an unknown person or process), then an attacker can include HTML that executes your client-side code and possibly any code of the attacker's choosing. As such, you should not use addJavascriptInterface() unless you wrote all of the HTML and JavaScript that appears in your WebView. You should also not allow the user to navigate to other web pages that are not your own, within your WebView (instead, allow the user's default browser application to open foreign links—by default, the user's web browser opens all URL links, so be careful only if you handle page navigation as described in the following section).

下面正式引入我们在项目中常用的两套开源的替代方案

jockeyjs开源方案

jockeyjs是一套IOS/Android双平台的Native和JS交互方法, 比较适合用在项目中。

Library to facilitate communication between iOS apps and JS apps running inside a UIWebView

jockeyjs对Native和JS的交互做了优美的封装, 事件的发送与接收都可以通过send()和on()来完成。我们先简单的看一下Event的发送与接收。

Sending events from app to JavaScript

// Send an event to JavaScript, passing a payload
jockey.send("event-name", webView, payload);

//With a callback to execute after all listeners have finished
jockey.send("event-name", webView, payload, new JockeyCallback() {
    @Override
    public void call() {
        //Your execution code
    }
});

Receiving events from app in JavaScript

// Listen for an event from iOS, but don't notify iOS we've completed processing
// until an asynchronous function has finished (in this case a timeout).
Jockey.on("event-name", function(payload, complete) {
  // Example of event'ed handler.
  setTimeout(function() {
    alert("Timeout over!");
    complete();
  }, 1000);
});

Sending events from JavaScript to app

// Send an event to iOS.
Jockey.send("event-name");

// Send an event to iOS, passing an optional payload.
Jockey.send("event-name", {
  key: "value"
});

// Send an event to iOS, pass an optional payload, and catch the callback when all the
// iOS listeners have finished processing.
Jockey.send("event-name", {
  key: "value"
}, function() {
  alert("iOS has finished processing!");
});

Receiving events from JavaScript in app

//Listen for an event from JavaScript and log a message when we have receied it.
jockey.on("event-name", new JockeyHandler() {
    @Override
    protected void doPerform(Map<Object, Object> payload) {
        Log.d("jockey", "Things are happening");
    }
});

//Listen for an event from JavaScript, but don't notify the JavaScript that the listener has completed
//until an asynchronous function has finished
//Note: Because this method is executed in the background, if you want the method to interact with the UI thread
//it will need to use something like a android.os.Handler to post to the UI thread.
jockey.on("event-name", new JockeyAsyncHandler() {
    @Override
    protected void doPerform(Map<Object, Object> payload) {
        //Do something asynchronously
        //No need to called completed(), Jockey will take care of that for you!
    }
});

//We can even chain together several handlers so that they get processed in sequence.
//Here we also see an example of the NativeOS interface which allows us to chain some common
//system handlers to simulate native UI interactions.
jockey.on("event-name", nativeOS(this)
            .toast("Event occurred!")
            .vibrate(100), //Don't forget to grant permission
            new JockeyHandler() {
                @Override
                protected void doPerform(Map<Object, Object> payload) {
                }
            }
);

//...More Handlers

//If you would like to stop listening for a specific event
jockey.off("event-name");

//If you would like to stop listening to ALL events
jockey.clear();

通过上面的代码, 我们对jockeyjs的使用有了大致的理解, 下面我们具体来看一下在项目中的使用。

1 依赖配置

下载代码: https://github.com/tcoulter/jockeyjs, 将JockeyJS.Android导入到工程中。

2 jockeyjs配置

jockeyjs有两种使用方式

方式一:

只在一个Activity中使用jockey或者多Activity共享一个jockey实例

//Declare an instance of Jockey
Jockey jockey;

//The WebView that we will be using, assumed to be instantiated either through findViewById or some method of injection.
WebView webView;

WebViewClient myWebViewClient;

@Override
protected void onStart() {
    super.onStart();

    //Get the default JockeyImpl
    jockey = JockeyImpl.getDefault();

    //Configure your webView to be used with Jockey
    jockey.configure(webView);

    //Pass Jockey your custom WebViewClient
    //Notice we can do this even after our webView has been configured.
    jockey.setWebViewClient(myWebViewClient)

    //Set some event handlers
    setJockeyEvents();

    //Load your webPage
    webView.loadUrl("file:///your.url.com");
}

方式二:

另一种就是把jockey当成一种全局的Service来用, 这种方式下我们可以在多个Activity之间甚至整个应用内共享handler. 当然我们同样需要 把jockey的生命周期和应用的生命周期绑定在一起。

//First we declare the members involved in using Jockey

//A WebView to interact with
private WebView webView;

//Our instance of the Jockey interface
private Jockey jockey;

//A helper for binding services
private boolean _bound;

//A service connection for making use of the JockeyService
private ServiceConnection _connection = new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
        _bound = false;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        JockeyBinder binder = (JockeyBinder) service;

        //Retrieves the instance of the JockeyService from the binder
        jockey = binder.getService();

        //This will setup the WebView to enable JavaScript execution and provide a custom JockeyWebViewClient
        jockey.configure(webView);

        //Make Jockey start listening for events
        setJockeyEvents();

        _bound = true;

        //Redirect the WebView to your webpage.
        webView.loadUrl("file:///android_assets/index.html");
    }

}

///....Other member variables....////

//Then we bind the JockeyService to our activity through a helper function in our onStart method
@Override
protected void onStart() {
    super.onStart();
    JockeyService.bind(this, _connection);
}

//In order to bind this with the Android lifecycle we need to make sure that the service also shuts down at the appropriate time.
@Override
protected void onStop() {
    super.onStop();
    if (_bound) {
        JockeyService.unbind(this, _connection);
    }
}

以上便是jockeyjs的大致用法.

三 性能优化

优化网页加载速度

默认情况html代码下载到WebView后,webkit开始解析网页各个节点,发现有外部样式文件或者外部脚本文件时,会异步发起网络请求下载文件,但如果 在这之前也有解析到image节点,那势必也会发起网络请求下载相应的图片。在网络情况较差的情况下,过多的网络请求就会造成带宽紧张,影响到css或 js文件加载完成的时间,造成页面空白loading过久。解决的方法就是告诉WebView先不要自动加载图片,等页面finish后再发起图片加载。

设置WebView, 先禁止加载图片

WebSettings webSettings = mWebView.getSettings();

//图片加载
if(Build.VERSION.SDK_INT >= 19){
    webSettings.setLoadsImagesAutomatically(true);
}else {
    webSettings.setLoadsImagesAutomatically(false);
}

覆写WebViewClient的onPageFinished()方法, 页面加载结束后再加载图片

@Override
public void onPageFinished(WebView view, String url) {
    super.onPageFinished(view, url);
    if (!view.getSettings().getLoadsImagesAutomatically()) {
        view.getSettings().setLoadsImagesAutomatically(true);
    }
}

注意: 4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。

硬件加速页面闪烁问题

4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是,当WebView视图被整体遮住一块,然后突然恢复时(比如使用SlideMenu将WebView从侧边 滑出来时),这个过渡期会出现白块同时界面闪烁。解决这个问题的方法是在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启,如下所示:

过度前关闭硬件加速

if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB){
    mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

过度前开启硬件加速

if(Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB){
    mWebView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}

以上就是本篇文章的全部内容, 大致就说这么多, 在实际的项目中我们通常会自己去封装一个H5Activity用来统一显示H5页面, 下面就提供了完整的H5Activity, 封装了WebView各种特性与jockeyjs代码交互。

该H5Activity提供WebView常用设置、H5页面解析、标题解析、进度条显示、错误页面展示、重新加载等功能。可以拿去稍作改造, 用于自己的项目中。

package com.qmsggg.webview;

import android.content.Context;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;

import com.jockeyjs.Jockey;
import com.jockeyjs.JockeyImpl;

public class H5Activity extends AppCompatActivity {

    public static final String H5_URL = "H5_URL";
    private static final String JOCKEY_EVENT_NAME = "JOCKEY_EVENT_NAME";
    private static final String TAG = H5Activity.class.getSimpleName();

    private Toolbar mToolbar;
    private ProgressBar mProgressBar;

    private Jockey mJockey;
    private WebView mWebView;
    private WebViewClient mWebViewClient;
    private WebChromeClient mWebChromeClient;

    private String mUrl;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_h5);
        setupView();
        setupSettings();
    }

    @Override
    protected void onStart() {
        super.onStart();
        setupJockey();
        setupData();
    }

    private void setupView() {
        mToolbar = (Toolbar) findViewById(R.id.h5_toolbar);
        mProgressBar = (ProgressBar) findViewById(R.id.h5_progressbar);
        mWebView = (WebView) findViewById(R.id.h5_webview);
    }

    private void setupSettings() {

        mWebView.setScrollBarStyle(WebView.SCROLLBARS_INSIDE_OVERLAY);
        mWebView.setHorizontalScrollBarEnabled(false);
        mWebView.setOverScrollMode(WebView.OVER_SCROLL_NEVER);

        WebSettings mWebSettings = mWebView.getSettings();
        mWebSettings.setSupportZoom(true);
        mWebSettings.setLoadWithOverviewMode(true);
        mWebSettings.setUseWideViewPort(true);
        mWebSettings.setDefaultTextEncodingName("utf-8");
        mWebSettings.setLoadsImagesAutomatically(true);

        //JS
        mWebSettings.setJavaScriptEnabled(true);
        mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);

        mWebSettings.setAllowFileAccess(true);
        mWebSettings.setUseWideViewPort(true);
        mWebSettings.setDatabaseEnabled(true);
        mWebSettings.setLoadWithOverviewMode(true);
        mWebSettings.setDomStorageEnabled(true);

        //缓存
        ConnectivityManager connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo info = connectivityManager.getActiveNetworkInfo();
        if (info != null && info.isConnected()) {
            String wvcc = info.getTypeName();
            Log.d(TAG, "current network: " + wvcc);
            mWebSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
        } else {
            Log.d(TAG, "No network is connected, use cache");
            mWebSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        }

        if (Build.VERSION.SDK_INT >= 16) {
            mWebSettings.setAllowFileAccessFromFileURLs(true);
            mWebSettings.setAllowUniversalAccessFromFileURLs(true);
        }

        if (Build.VERSION.SDK_INT >= 12) {
            mWebSettings.setAllowContentAccess(true);
        }

        setupWebViewClient();
        setupWebChromeClient();
    }

    private void setupJockey() {
        mJockey = JockeyImpl.getDefault();
        mJockey.configure(mWebView);
        mJockey.setWebViewClient(mWebViewClient);
        mJockey.setOnValidateListener(new Jockey.OnValidateListener() {
            @Override
            public boolean validate(String host) {
                return "yourdomain.com".equals(host);
            }
        });

        //TODO set your event handler
        mJockey.on(JOCKEY_EVENT_NAME, new EventHandler());
    }

    private void setupData() {
        mUrl = getIntent().getStringExtra(H5_URL);
        if (TextUtils.isEmpty(mUrl)) {
            //TODO show error page
        } else {
            mWebView.loadUrl(mUrl);
        }
    }

    private void setupWebViewClient() {
        mWebViewClient = new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                //TODO 处理URL, 例如对指定的URL做不同的处理等
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
            }

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
            }

            @Override
            public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
                super.onReceivedError(view, request, error);
            }
        };
        mWebView.setWebViewClient(mWebViewClient);
    }

    private void setupWebChromeClient() {
        mWebChromeClient = new WebChromeClient() {
            @Override
            public void onReceivedTitle(WebView view, String title) {
                super.onReceivedTitle(view, title);
                mToolbar.setTitle(title);

            }

            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                super.onProgressChanged(view, newProgress);
                mProgressBar.setProgress(newProgress);
                if (newProgress == 100) {
                    mProgressBar.setVisibility(View.GONE);
                } else {
                    mProgressBar.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }
        };
        mWebView.setWebChromeClient(mWebChromeClient);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
            mWebView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}
qmsggg commented 6 years ago

Android网络编程:基础理论汇总

文章目录

在Android的网络开发过程中,我们通常会使用像Okhttp、Retrofit这种高度封装的网络库,它们完全屏蔽了相关技术细节。但是掌握其中的原理对我们来 说是很重要的,要知其然,也要知其所以然,只要掌握了这些原理,你才能更好的理解Okhttp等网络库的源码实现。

网络编程通常会涉及以下几个角色:

怎么去理解它们的关系呢?🤔

例如我们是双十一从马老板家买了部手机,这个时候我们就是客户端,马老板就是服务端。手机要通过快递公司的汽车运送到我们手中。TCP/IP就相当于汽车,但是光有汽车是不够的,还要对汽车 进行分类,不然都是一样的汽车就乱套了,而完成分类的就是HTTP/HTTPS了,HTTP/HTTPS会告诉这些汽车,你是负责送货的(GET),你是负责退货的(POST)等等。

注:文章中部分图片来源于网络,这次就偷个懒,有些流程图就不画了。😁

一 TCP/IP

TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,

TCP协议是HTTP/HTTPS、WebSocket等协议的基础,我们首先来看看它们的报文格式。

1.1 IP数据报与TCP报文

关于IP数据报与TCP报文你只需要理解它的结构就行,不用去记它,等到使用的时候不记得了,查一下就好了。

IP数据报

  1. 版本——占 4 bit,指IP协议的版本. 目前的 IP 协议版本号为 4 (即 IPv4)
  2. 首部长度——占 4 bit,可表示的最大数值是 15 个单位(一个单位为 4 字节)因此 IP 的首部长度的最大值是60字节。
  3. 总长度——占 16 bit,指首部和数据之和的长度,单位为字节,因此数据报的最大长度为 65535 字节。总长度必须不超过最大传送单元 MTU。
  4. 标识(identification) 占 16 bit,它是一个计数器,用来产生数据报的标识。当数据报需要分片时,此标识表示同一个数据报的分片。
  5. 标志(flag):3 bit,D0:MF,D1:DF,D2保留, DF位用来表示数据报是否允许分片,DF=1不分片;MF位表示是否有后续分片,MF=0表示是最后一片。
  6. 片偏移(13 bit)指出:较长的分组在分片后某分片在原分组中的相对位置。片偏移以 8 个字节为偏移单位。
  7. 生存时间(8 bit)记为 TTL (Time To Live)表示数据报在网络中的寿命,其单位为秒。在目前的实际应用中,常以“跳”为单位。
  8. 协议(8 bit)字段指出此数据报携带的数据使用何种协议(如TCP/UDP等)以便目的主机的 IP 层将数据部分上交给哪个处理过程
  9. 首部检验和(16 bit)字段只检验数据报的首部不包括数据部分。这里不采用 CRC 检验码而采用简单的“反码算术求和”计算方法。
  10. 源地址和目的地址都各占 4 字节,32bit 的IP地址
  11. 可选字段的长度是 可变的,1~40 字节,用于增加IP数据报的控制功能。
  12. 填充字段保证IP首部长度是 4 字节的整倍数

TCP报文

  1. 源端口和目的端口字段——各占 2 字节。端口是传输层与应用层的服务接口。传输层的复用和分用功能都要通过端口才能实现。
  2. 序号字段——占 4 字节。TCP 连接中传送的数据流中的每一个字节都编上一个序号。序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。
  3. 确认号字段——占 4 字节,是期望收到对方的下一个报文段的数据的第一个字节的序号。
  4. 数据偏移——占 4 bit,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。“数据偏移”的单位不是字节而是 32 bit 字(4 字节为计算单位)。
  5. 保留字段——占 6 bit,保留为今后使用,但目前应置为 0。
  6. 紧急比特 URG —— 当 URG=1 时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)。
  7. 确认比特 ACK —— 只有当 ACK=1 时确认号字段才有效。当 ACK=0 时,确认号无效。
  8. 推送比特 PSH(Push)接收方 TCP 收到推送比特置1的报文段,就尽快地交付给接收应用进程,而不再等到整个缓存都填满了后再向上交付.
  9. 复位比特 RST (Reset) —— 当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。
  10. 同步比特 SYN —— 同步比特 SYN 置为 1,就表示这是一个连接请求或连接接受报文。
  11. 终止比特 FIN (FINal) —— 用来释放一个连接。当FIN=1 时,表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。
  12. 窗口字段 —— 占 2 字节。窗口字段用来控制对方发送的数据量,单位为字节。TCP 连接的一端根据设置的缓存空间大小确定自己的接收窗口大小,然后通知对方以确定对方的发送窗口的上限。
  13. 检验和 —— 占 2 字节。检验和字段检验的范围包括首部和数据这两部分。在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部。
  14. 紧急指针字段 —— 占 16 bit。紧急指针指出在本报文段中的紧急数据的最后一个字节的序号。
  15. 选项字段 —— 长度可变。TCP 首部可以有多达40字节的可选信息,用于把附加信息传递给终点,或用来对齐其它选项。

1.2 三次握手与四次分手

TCP用三次握手(three-way handshake)过程创建一个连接,使用四次分手 关闭一个连接。

三次握手与四次分手的流程如下所示:

三次握手

四次分手

三次握手与四次分手也是个老生常谈的概念,举个简单的例子说明一下。

三次握手

例如你小时候出去玩,经常玩忘了回家吃饭。你妈妈也经常过来喊你。如果你没有走远,在门口的小土堆上玩泥巴,你妈妈会喊:"小新,回家吃饭了"。你听到后会回应:"知道了,一会就回去"。妈妈听 到你的回应后又说:"快点回来,饭要凉了"。这样你妈妈和你就完成了三次握手的过程。😁说到这里你也可以理解三次握手的必要性,少了其中一个环节,另一方就会陷入等待之中。

三次握手的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误.

四次分手

例如偶像言情剧干净利落的分手,女主对男主说:我们分手吧🙄,男主说:分就分吧😰。女主说:你果然是不爱我了,你只知道让我多喝热水🙄。男主说:事到如今也没什么好说的了,祝你幸福🙃。四次分手完成。说到这里你可以理解 了四次分手的必要性,第一次是女方(客户端)提出分手,第二次是男主(服务端)同意女主分手,第三次是女主确定男主不再爱她,也同意男主分手。第四次两人彻底拜拜(断开连接)。

因为TCP是全双工模式,所以四次分手的目的就是为了可靠地关闭连接。

二 HTTP/HTTPS

HTTP(HyperText Transfer Protocol)是一种用于分布式、协作式和超媒体信息系统的应用层协议[1]。HTTP是万维网的数据通信的基础。

HTTP是最常见的应用层协议,我们日常开发中基本上接触到的都是这个协议。

2.1 HTTP报文

HTTP应用程序是通过相互发送报文工作的,报文是HTTP应用程序之间发送的数据块,报文通常分为请求报文和响应报文两种,请求报文向服务器请求一个动作,响应报文将请求结果返回给客户端。

HTTP请求报文分为三部分:请求行、请求首部、请求实体.

请求报文

GET /his?wd=&from=pc_web&rf=3&hisdata=&json=1&p=3&sid=20740_20742_1424_18280_20417_17001_15840_11910_20744_20705&csor=0&cb=jQuery110206488567241711853_1469936513370&_=1469936513371 HTTP/1.1
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0
Accept: text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, *//*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Referer: https://www.baidu.com/
Cookie: BAIDUID=DB24D5F4AB36694CF00C4877ADA56562:FG=1; BIDUPSID=DB24D5F4AB36694CF00C4877ADA56562; PSTM=1469936050; BDRCVFR[gltLrB7qNCt]=mk3SLVN4HKm; BD_CK_SAM=1; H_PS_PSSID=20740_20742_1424_18280_20417_17001_15840_11910_20744_20705; BD_UPN=133252; H_PS_645EC=96a0XJobAseSCdbn9%2FviULLD7KreCHN4V4HzQtcGacKF8tGu13Nzd6j9PoB2SPPVj1d5; BD_HOME=0; __bsi=11860814506529643127_00_0_I_R_25_0303_C02F_N_I_I_0
Connection: keep-alive

响应报文

HTTP响应报文分为三部分:状态行、响应首部、响应实体。

HTTP/1.1 200 OK
Server: bfe/1.0.8.14
Date: Sun, 31 Jul 2016 03:41:53 GMT
Content-Type: baiduApp/json; v6.27.2.14; charset=UTF-8
Content-Length: 95
Connection: keep-alive
Cache-Control: private
Expires: Sun, 31 Jul 2016 04:41:53 GMT
Set-Cookie: __bsi=12018325985460509248_00_0_I_R_4_0303_C02F_N_I_I_0; expires=Sun, 31-Jul-16 03:41:58 GMT; domain=www.baidu.com; path=/

报文通常由以下部分组成:

方法

方法(method):客户端希望服务器对资源执行的动作。

我们主要讨论我们最常用的两个GET/POST。

GET与POST在本质上都是TCP连接,只是GET直接把参数写在请求行中,而POST把参数放在请求体中。关于这两个方法,要注意以下两点:

状态码

更多关于状态码的细节可以参见HTTP状态码

首部

首部通常和方法配合工作,共同决定了客户端和服务器能做什么事情。

常见的通用首部

常见的请求首部

常见的响应首部

常见的实体首部

更多关于首部的细节可以参见HTTP首部

2.2 HTTP与HTTPS

HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份 认证,保护交换数据的隐私与完整性。

如下图所示,可以很明显的看出两个的区别:

注:TLS是SSL的升级替代版,具体发展历史可以参考传输层安全性协议

HTTP与HTTPS在写法上的区别也是前缀的不同,客户端处理的方式也不同,具体说来:

所以你可以看到,HTTPS比HTTP多了一层与SSL的连接,这也就是客户端与服务端SSL握手的过程,整个过程主要完成以下工作:

SSL握手是一个相对比较复杂的过程,更多关于SSL握手的过程细节可以参考TLS/SSL握手过程

SSL/TSL的常见开源实现是OpenSSL,OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。 更多源于OpenSSL的技术细节可以参考OpenSSL

三 WebSocket

WebSocket是一种在单个TCP连接上进行全双工通讯的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握 手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

为什么需要WebSocket,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。而WebSocket可以实现双向通信。一般来说WebSocket是用来实现双工通信的长连接的。HTTP想要达到 这种效果,一般会通过轮询或者long poll来实现,这样比较占用资源且非常被动。

一个典型的WebSocket请求与响应

客户端请求

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

服务器响应


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/

这里会使用Upgrade: websocket Connection: Upgrade 提示当前发起的是WebSocket协议,注意升级协议。

注:Okhttp已支持WebSocket。

我们同样也来看看WebSocket的报文结构。

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

已定义的帧类型:

WebSocket协议的控制帧有3种:

前面我们讲完了几种常用的协议,最后我们再看看和编码相关的知识,这在日常的业务开发中也经常用到。

四 实体与编码

前面我们已经提到过与内容编码相关的实体首部:

对于我们来说,比较常见到的也需要重点关注的是Content-Type、Content-Encoding这两个。

Content-Type描述的是当前内容的MIME类型,关于MIME类型:

MIME:表示一种主要的对象类型和一个特定的子类型。

它主要有以下几种类型:

Content-Encoding描述的是编码类型,它的意义在于告诉服务端当前客户端支持的编码方式,这样服务端就会根据该编码方式来编码数据。如果没有该首部,则默认认为 客户端支持所有的编码方式。

Accept-Encoding 能够接受的编码方式列表。    Accept-Encoding: gzip:q=1.0, deflate:q=0.5, *:q=0   

另外Accept-Encoding还可以用q来表示编码优先级,1.0表示最希望使用的编码,0表示不想接受该编码。

常见的编码类型有:

当然我们常用的就是gzip,Okhttp里面可以利用Okio进行gzip压缩,这里我们也贴下代码。

/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}