天天看點

WebViewClient 簡介 API 案例

代碼位置:https://github.com/baiqiantao/WebViewTest.git

設計思想了解

在WebView的設計中,不是什麼事都要WebView類幹的,有相當多的雜事是分給其他類做的,這樣WebView專心幹好自己的解析、渲染工作就行了。比如我們最熟知的,所有針對WebView的設定都封裝到了WebSettings裡。我們知道,在使用WebView加載資源過程中,可能會有大量各種類型事件的回調,為了友善開發組處理這些回調,針對不同的事件回調,google将這些回調進行了分類集合,于是就産生了WebViewClient、WebChromeClient這兩個大類。

很多同學一看到WebChromeClient類裡有Chrome,立馬就會想到google的Chrome浏覽器,其實這裡并不是"特指"Chrome浏覽器的意思,而是"泛指"浏覽器的意思。

為什麼叫WebChromeClient呢?這是因為WebChromeClient中集合了影響浏覽器的事件到來時的回調方法,是以這裡需要突出浏覽器的概念,而Chrome則是google自家的浏覽器名稱,也是目前市面上最受歡迎的浏覽器,是以就采用了WebChromeClient來做為名稱吧,純屬臆想……

簡單來說就是

  • WebViewClient:在影響【View】的事件到來時,會通過WebViewClient中的方法回調通知使用者
  • WebChromeClient:當影響【浏覽器】的事件到來時,就會通過WebChromeClient中的方法回調通知用法。

回調事件總結

WebViewClient就是幫助WebView處理各種通知、請求事件的,常用到的如:

  1. onLoadResource、onPageStart、onPageFinish
  2. onReceiveError、onReceivedHttpError、onReceivedSslError
  3. shouldInterceptRequest、shouldOverrideKeyEvent、shouldOverrideUrlLoading
  4. onReceivedClientCertRequest、onReceivedHttpAuthRequest、onReceivedLoginRequest
  5. 其他:doUpdateVisitedHistory、onFormResubmission、onPageCommitVisible、onRenderProcessGone、onScaleChanged、onUnhandledKeyEvent

實際使用的話,如果你的WebView隻是用來處理一些html的頁面内容,隻用WebViewClient就行了,如果需要更豐富的處理效果,比如JS、進度條等,就要用到WebChromeClient。

API

shouldInterceptRequest 方法

  • WebResourceResponse  shouldInterceptRequest(WebView view, WebResourceRequest request)  Notify the host application of a resource request and allow the application to return the data. 通知資源請求的主機應用程式,并允許應用程式傳回資料。
    • If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return傳回的 response and data will be used. NOTE: This method is called on a thread other than而不是 the UI thread so clients should exercise caution謹慎 when accessing private data or the view system. 
    • 該函數會在請求資源前調用,且無論任何資源,比如超連結、JS檔案、圖檔等,在每一次請求資源時都會回調。我們可以通過傳回一個自定義的WebResourceResponse來讓WebView加載指定的資源。比如,如果我們需要改變網頁的背景,替換網頁中圖檔等,都可以在這個回調時處理。但是必須注意的是,此回調是在非UI線程中執行的。
    • 參數 request:Object containing the details of the request. 包含請求的詳細資訊的對象
    • 傳回值:A WebResourceResponse containing the response information or null if the WebView should load the resource itself.

例如,替換所有的圖檔為自定義的圖檔

if (request.getUrl().toString().endsWith(".jpg")) {
    try {
        return new WebResourceResponse("text/html", "UTF-8", view.getContext().getAssets().open("icon.jpg"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}
return null;           
  • WebResourceResponse  shouldInterceptRequest(WebView view, String url)  This method was deprecated in API level 21. Use shouldInterceptRequest(WebView, WebResourceRequest) instead.
  • boolean  shouldOverrideKeyEvent(WebView view, KeyEvent event)  Give the host application a chance to handle the key event synchronously. 給主機應用程式一次同步處理鍵事件的機會。預設行為傳回false。
    • e.g. 例如 menu shortcut菜單快捷鍵 key events need to be filtered過濾 this way. If return true, WebView will not handle the key event. If return false, WebView will always handle the key event, so none of the super in the view chain will see the key event.
    • 重寫此方法才能夠處理在浏覽器中的按鍵事件。如果應用程式想要處理該事件則傳回true,否則傳回false。
    • 傳回值:True if the host application wants to handle the key event itself, otherwise return false. 

shouldOverrideUrlLoading 方法

boolean  shouldOverrideUrlLoading(WebView view, WebResourceRequest request)  Give the host application a chance to take over the control when a new url is about to be loaded in the current WebView. 當一個新的url即将加載到目前的WebView中時,讓主機應用程式有機會接管控制權。

    • 當加載的網頁需要重定向的時候就會回調這個函數,告知我們應用程式是否需要接管控制網頁加載,如果應用程式接管并且return true,意味着主程式接管網頁加載,如果傳回false,則會讓webview自己處理。
    • 由于每次超連結在加載前都會先走shouldOverrideUrlLoading回調,是以如果我們想攔截某個URL(比如将其轉換成其它URL進行自定義跳轉,或彈吐司等其他操作)可以在這裡做。
    • 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. 則傳回true表示主機應用程式處理該url,而傳回false表示目前的WebView處理該URL。
    • 根據以上描述可以知道:我們隻需僅僅給WebView設定一個WebViewClient對象,而不需要重寫shouldOverrideUrlLoading方法(即使用它的預設回調),就可以實作在此WebView中加載網頁中的其他URL了。現在大部分APP采用的重寫shouldOverrideUrlLoading的做法大都是畫蛇添足(當然如果需要自定義跳轉的話,是一定要重寫此方法的)。
    • 參數 request:Object containing the details of the request.
    • 傳回值: 傳回true則目前應用程式要自己處理這個url, 傳回false則不處理。
Notes:
This method is not called for requests using the POST "method".  當請求的方式是"POST"方式時這個回調是不會通知的。
This method is also called for subframes with non-http schemes,  這種方法也被稱為具有非http方案的子幀
    thus it is strongly disadvised勸止,勸阻(某人做某事) to unconditionally(無條件地) call loadUrl(String) 
    from inside the method            
  • boolean  shouldOverrideUrlLoading(WebView view, String url)  This method was deprecated in API level 24. Use shouldOverrideUrlLoading(WebView, WebResourceRequest) instead.

Error回調

  • void  onReceivedError(WebView view, int errorCode, String description, String failingUrl)  This method was deprecated in API level 23. Use onReceivedError(WebView, WebResourceRequest, WebResourceError) instead.
  • void  onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)  Report web resource loading error to the host application. 向主機應用程式報告Web資源加載錯誤
    • 當浏覽器通路指定的網址發生錯誤時會通知我們應用程式,比如網絡錯誤。我們可以在這裡做錯誤處理,比如再請求加載一次,或者提示404的錯誤頁面。
    • These errors usually indicate表示 inability to connect to the server無法連接配接到伺服器. Note that unlike the deprecated廢棄 version of the callback, the new version will be called for any resource (iframe, image, etc), not just for the main page. Thus是以, it is recommended to perform minimum required work 執行最低要求的工作 in this callback.
    • 參數 request:The originating request.
    • 參數 error:Information about the error occured.
onPageFinished tells you that the WebView has stopped loading, onReceivedError tells you there was an error.
They're not "success" and "failure" callbacks which is why you'll get both in case of an error.
也即:onPageFinished僅僅表示網頁加載完成了,不能說明這個網頁是否成功的加載了。           
  • void  onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)  Notify the host application that an HTTP error has been received from the server while loading a resource. 通知主機應用程式在加載資源時從伺服器收到HTTP錯誤。
    • HTTP errors have status codes >= 400. This callback will be called for any resource (iframe, image, etc), not just for the main page. Thus, it is recommended to perform minimum required work 執行最低要求的工作 in this callback. 
    • Note that the content of the server response伺服器響應的内容 may not be provided within the errorResponse parameter 參數中可能不提供.
    • 參數 errorResponse:Information about the error occured.
  • void  onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)  Notify the host application that an SSL error occurred while loading a resource. 當網頁加載資源過程中發現SSL錯誤時回調。
    • The host application must call either handler.cancel() or handler.proceed(). Note that the decision決定 may be retained保留 for use in response用來響應 to future SSL errors. The default behavior is to cancel the load.
    • HTTPS協定是通過SSL來通信的,是以當使用HTTPS通信的網址出現錯誤時,就會通過onReceivedSslError回調通知過來
    • SslErrorHandler隻有兩個函數proceed()和cancel()。proceed()表示忽略錯誤繼續加載,cancel()表示取消加載。在onReceivedSslError的預設實作中是使用的cancel()來取消加載,是以一旦出來SSL錯誤,HTTPS網站就會被取消加載了。如果想忽略錯誤繼續加載就隻有重寫onReceivedSslError,并在其中調用proceed()。
    • 當HTTPS傳輸出現SSL錯誤時,錯誤會隻通過onReceivedSslError回調傳過來,而不會觸發onReceivedError回調。
    • 參數 handler:An SslErrorHandler object that will handle the user's response. 處理使用者請求的對象。
    • error :The SSL error object. 包含了目前SSL錯誤的基本所有資訊

開始加載和加載完成

  • void  onPageFinished(WebView view, String url)  Notify the host application that a page has finished loading. 當核心加載完目前頁面時會通知我們的應用程式
    • When onPageFinished() is called, the rendering picture渲染的圖檔 may not be updated yet. To get the notification for the new Picture, use onNewPicture(WebView, Picture).
    • This method is called only for main frame.
  • void  onPageStarted(WebView view, String url, Bitmap favicon)  Notify the host application that a page has started loading.當核心開始加載通路的url時會通知應用程式
    • This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted one time for the main frame. 對每個main frame,這個函數隻會被調用一次,是以如果一個頁面包含 iframe 或者 framesets 不會另外調用一次
    • This also means that onPageStarted will not be called when the contents of an embedded frame changes, i.e. clicking a link whose target is an iframe, it will also not be called for fragment navigations (navigations to #fragment_id). 當網頁内内嵌的 frame 發生改變時也不會調用onPageStarted。即點選目标是iframe的連結,也不會調用fragment導航(導航到#fragment_id)
    • 參數 Bitmap favicon(網站圖示):如果這個favicon已經存儲在本地資料庫中,則會傳回這個網頁的favicon,否則傳回為null。
(1) iframe 可能不少人不知道什麼含義,這裡我解釋下,iframe 我們加載的一張,下面有很多連結,我們随便點選一個連結是即目前host的一個iframe.
(2) 有個問題可能是開發者困惑的,onPageStarted和shouldOverrideUrlLoading 在網頁加載過程中這兩個函數到底哪個先被調用。
     當我們通過loadUrl的方式重新加載一個網址時候,這時候會先調用onPageStarted再調用shouldOverrideUrlLoading
     當我們在打開的這個網址點選一個link,這時候會先調用shouldOverrideUrlLoading再調用onPageStarted。
     不過shouldOverrideUrlLoading不一定每次都被調用,隻有需要的時候才會被調用。           

其他回調方法

  • void  doUpdateVisitedHistory(WebView view, String url, boolean isReload)  Notify the host application to update its visited links database. 通知主機應用程式更新其通路連結資料庫(更新通路曆史)
    • 通知應用程式可以将目前的url存儲在資料庫中,意味着目前的通路url已經生效并被記錄在核心當中。這個函數在網頁加載過程中隻會被調用一次。注意網頁前進後退并不會回調這個函數。
    • 參數 url:The url being visited. 目前正在通路的url 
    • 參數 isReload:True if this url is being reloaded. 如果是true,那麼這個是正在被reload的url
  • void  onFormResubmission(WebView view, Message dontResend, Message resend)  As the host application if the browser should resend data as the requested page was a result of a POST. 作為主機應用程式,如果浏覽器應該重新發送資料,因為請求的頁面是POST的結果
    • 如果浏覽器需要重新發送POST請求,可以通過這個時機來處理。預設是不重新發送資料。
    • 參數 dontResend:The message to send if the browser should not resend 當浏覽器不需要重新發送資料時使用的參數。
    • 參數 resend:The message to send if the browser should resend data 當浏覽器需要重新發送資料時使用的參數。
  • void  onLoadResource(WebView view, String url)  Notify the host application that the WebView will load the resource specified by the given url. 通知應用程式WebView即将加載 url 指定的資源。
    • 注意,每一個資源(比如圖檔)的加載都會調用一次此方法。
  • void  onPageCommitVisible(WebView view, String url)  Notify the host application that WebView content left over from previous page navigations will no longer be drawn. 通知主機應用程式将不再繪制從上一頁導航遺留的WebView内容。
    • This callback can be used to determine确定 the point at which it is safe to make a recycled WebView visible, ensuring保證 that no stale陳舊的 content is shown. It is called at the earliest point at which it can be guaranteed確定 that onDraw(Canvas) will no longer draw any content from previous navigations. The next draw will display either the background color of the WebView, or some of the contents of the newly loaded page.
    • This method is called when the body of the HTTP response has started loading, is reflected in反映在 the DOM, and will be visible in subsequent随後 draws. This callback occurs early in the document loading process在文檔加載過程的早期發生, and as such you should expect期望、明白 that linked resources (for example, css and images) may not be available.
    • This callback is only called for main frame navigations.
  • void  onReceivedClientCertRequest(WebView view, ClientCertRequest request)  Notify the host application to handle a SSL client certificate request. 通知主機應用程式來處理SSL用戶端證書請求。
    • The host application is responsible負責 for showing the UI if desired需要 and providing the keys. There are three ways to respond: proceed(), cancel() or ignore(). Webview stores the response in memory (for the life of the application) if proceed() or cancel() is called and does not call onReceivedClientCertRequest() again for the same host and port pair 針對相同的主機和端口. Webview does not store the response if ignore() is called. 
    • Note that, multiple layers多層 in chromium network stack might be caching the responses緩存響應, so the behavior for ignore is only a best case effort隻是最好努力的情況. This method is called on the UI thread. During the callback, the connection is suspended暫停. 
    • For most use cases, the application program should implement the KeyChainAliasCallback interface and pass it to choosePrivateKeyAlias(Activity, KeyChainAliasCallback, String[], Principal[], Uri, String) to start an activity for the user to choose the proper alias别名. The keychain activity will provide the alias through the callback method in the implemented interface. Next the application should create an async task to call getPrivateKey(Context, String) to receive the key. 
    • An example implementation of client certificates can be seen at AOSP Browser. 
    • The default behavior is to cancel, returning no client certificate.
  • void  onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)  Notifies the host application that the WebView received an HTTP authentication request. 通知應用程式WebView接收到了一個Http auth的請求
    • The host application can use the supplied提供的 HttpAuthHandler to set the WebView's response to the request. The default behavior is to cancel the request.
    • 參數 handler:用來響應WebView請求的HttpAuthHandler對象
    • 參數 host:請求認證的host
    • 參數 realm:認證請求所在的域
  • void  onReceivedLoginRequest(WebView view, String realm, String account, String args)  Notify the host application that a request to automatically log in the user has been processed. 通知應用程式有個自動登入帳号過程(通知主程式執行了自動登入請求)
    • 參數 realm :The account realm used to look up accounts. 賬戶的域名,用來查找賬戶。
    • 參數 account:An optional account 可選的賬戶. If not null, the account should be checked against accounts on the device 需要和本地的賬戶進行. If it is a valid可用 account, it should be used to log in the user.
    • 參數 args:Authenticator specific arguments used to log in the user. 驗證指定參數的登入使用者
  • boolean  onRenderProcessGone(WebView view, RenderProcessGoneDetail detail)  Notify host application that the given webview's render process has exited. 通知主機應用程式,給定的Webview渲染程序已退出。
    • Multiple多個 WebView instances may be associated with關聯 a single render渲染 process; onRenderProcessGone will be called for each WebView that was affected受影響的. 
    • The application's implementation of this callback should only attempt to clean up the specific特定的 WebView given as a parameter作為參數提供的, and should not assume假定、假設 that other WebView instances are affected. The given WebView can't be used, and should be removed from the view hierarchy視圖層次結構, all references to it should be cleaned up, e.g any references in the Activity or other classes saved using findViewById and similar calls, etc等等. To cause an render process crash for test purpose 為了測試目的,導緻渲染過程崩潰, the application can call  loadUrl("chrome://crash")  on the WebView. 
    • Note that multiple WebView instances may be affected if they share共享 a render process, not just而不僅僅是 the specific WebView which loaded chrome://crash.
    • 參數 detail:the reason why it exited.
    • 傳回值:true if the host application handled the situation情況 that process has exited, otherwise, application will crash if render process crashed, or be killed if render process was killed by the system.
  • void  onScaleChanged(WebView view, float oldScale, float newScale)  Notify the host application that the scale applied to the WebView has changed.
    • WebView顯示縮放比例發生改變時調用
  • void  onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg)  This method was deprecated in API level 8. This method is no longer called. When the WebView encounters a redirect loop, it will cancel the load.
  • void  onUnhandledKeyEvent(WebView view, KeyEvent event)  Notify the host application that a key was not handled by the WebView. 通知主機應用程式,一個鍵未被WebView處理。
    • Except system keys, WebView always consumes消耗 the keys in the normal flow正常流中的鍵 or if shouldOverrideKeyEvent returns true. 
    • This is called asynchronously異步 from where the key is dispatched調用. It gives the host application a chance to handle the unhandled key events.
    • 注意:如果事件為MotionEvent,則事件的生命周期隻存在方法調用過程中,如果WebViewClient想要使用這個Event,則需要複制Event對象。

案例

public class MyWebViewClient extends WebViewClient {
	private ProgressBar mProgressBar;
	private WebViewActivity activity;
	
	public MyWebViewClient(WebViewActivity activity) {
		super();
		this.activity = activity;
		mProgressBar = activity.getProgress_bar();
	}
	
	@Override
	public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
		//通知主機應用程式更新其通路連結資料庫(更新通路曆史)。isReload:是否是正在被reload的url
		Log.i("bqt", "【doUpdateVisitedHistory】" + url + "   " + isReload);
		super.doUpdateVisitedHistory(view, url, isReload);
	}
	
	@Override
	public void onPageStarted(WebView view, String url, Bitmap favicon) {
		//favicon(網站圖示):如果這個favicon已經存儲在本地資料庫中,則會傳回這個網頁的favicon,否則傳回為null
		Log.i("bqt", "【onPageStarted】" + url);
		if (mProgressBar != null) mProgressBar.setVisibility(View.VISIBLE);//在開始加載時顯示進度條
		activity.getIv_icon().setVisibility(View.GONE);
		super.onPageStarted(view, url, favicon);
	}
	
	@Override
	public void onPageFinished(WebView view, String url) {
		Log.i("bqt", "【onPageFinished】" + url);
		if (mProgressBar != null) mProgressBar.setVisibility(View.GONE);//在結束加載時隐藏進度條
		super.onPageFinished(view, url);
	}
	
	@Override
	public void onLoadResource(WebView view, String url) {
		Log.i("bqt", "【onLoadResource】" + url);//每一個資源(比如圖檔)的加載都會調用一次
		super.onLoadResource(view, url);
	}
	
	@TargetApi(Build.VERSION_CODES.M)
	@Override
	public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
		//通路指定的網址發生錯誤時回調,我們可以在這裡做錯誤處理,比如再請求加載一次,或者提示404的錯誤頁面
		//如點選一個迅雷下載下傳的資源時【ftp://***  -10  net::ERR_UNKNOWN_URL_SCHEME】
		Log.i("bqt", "【onReceivedError】" + request.getUrl().toString() + "  " + error.getErrorCode() + "  " + error.getDescription());
		if (error.getErrorCode() == -10) view.loadUrl("file:///android_asset/h5/test.html");
		else super.onReceivedError(view, request, error);
	}
	
	@TargetApi(Build.VERSION_CODES.M)
	@Override
	public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
		//HTTP錯誤具有> = 400的狀态碼。請注意,errorResponse參數中可能不提供伺服器響應的内容。
		//如【502  utf-8  text/html】【http://www.dy2018.com/favicon.ico  404    text/html】
		Log.i("bqt", "【onReceivedHttpError】" + request.getUrl().toString() + "  " + errorResponse.getStatusCode()
				+ "  " + errorResponse.getEncoding() + "  " + errorResponse.getMimeType());
		super.onReceivedHttpError(view, request, errorResponse);
	}
	
	@Override
	public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
		//HTTP錯誤具有> = 400的狀态碼。請注意,errorResponse參數中可能不提供伺服器響應的内容。
		//如,點選12306中的購票時【https://kyfw.12306.cn/otn/  3  Issued to: CN=kyfw.12306.cn,***】
		Log.i("bqt", "【onReceivedSslError】" + error.getUrl() + "  " + error.getPrimaryError() + "  " + error.getCertificate().toString());
		if (new Random().nextBoolean()) super.onReceivedSslError(view, handler, error);//預設行為,取消加載
		else handler.proceed();//忽略錯誤繼續加載
	}
	
	@Override
	public void onScaleChanged(WebView view, float oldScale, float newScale) {
		//應用程式可以處理改事件,比如調整适配螢幕
		Log.i("bqt", "【onScaleChanged】" + "oldScale=" + oldScale + "  newScale=" + newScale);
		super.onScaleChanged(view, oldScale, newScale);
	}
	
	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
	@Override
	public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
		//每一次請求資源時都會回調。如果我們需要改變網頁的背景,可以在這裡處理。
		//如果傳回值為null,則WebView會照常繼續加載資源。 否則,将使用傳回的響應和資料。
		Log.i("bqt", "【shouldInterceptRequest】" + request.getUrl().toString() + "  " + request.getMethod());
		if (new Random().nextBoolean()) return super.shouldInterceptRequest(view, request);
		else if (request.getUrl().toString().endsWith("你妹的.jpg")) {
			try {
				return new WebResourceResponse("text/html", "UTF-8", view.getContext().getAssets().open("icon.jpg"));
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
	
	@Override
	public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
		//給主機應用程式一次同步處理鍵事件的機會。如果應用程式想要處理該事件則傳回true,否則傳回false。
		Log.i("bqt", "【shouldOverrideKeyEvent】" + event.getAction() + "  " + event.getKeyCode());
		return super.shouldOverrideKeyEvent(view, event);
	}
	
	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
	@Override
	public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
		//貌似都還是調用的廢棄的那個方法
		Log.i("bqt", "【shouldOverrideUrlLoading】" + request.getUrl().toString() + "  " + request.getMethod());
		return super.shouldOverrideUrlLoading(view, request);
	}
	
	@SuppressWarnings("deprecation")
	@Override
	public boolean shouldOverrideUrlLoading(WebView view, String url) {
		boolean b = new Random().nextBoolean();
		Log.i("bqt", "【shouldOverrideUrlLoading廢棄方法】" + b + "  " + url);
		//識别電話、短信、郵件等
		if (url.startsWith(WebView.SCHEME_TEL) || url.startsWith("sms:") || url.startsWith(WebView.SCHEME_MAILTO)) {
			Intent intent = new Intent(Intent.ACTION_VIEW);
			intent.setData(Uri.parse(url));
			view.getContext().startActivity(intent);
			return true;
		}
		
		if (b) return super.shouldOverrideUrlLoading(view, url);//沒必要折騰,隻要設定了WebViewClient,使用預設的實作就行!
		else {
			view.loadUrl(url);//不去調用系統浏覽器, 而是在本WebView中跳轉
			return true;
		}
	}
}           

2017-8-15

來自為知筆記(Wiz)

本文來自部落格園,作者:白乾濤,轉載請注明原文連結:https://www.cnblogs.com/baiqiantao/p/7367830.html

繼續閱讀