Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
很多APP都嵌套了webview,騰訊的webview的簡(jiǎn)單使用。
騰訊webview的sdk下載地址
用法:
1、布局文件:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<com.xinggui.wz.chuangjiaoplatform.view.MyCustomViewTitleLayout
android:id="@+id/custom1_webview"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
<ProgressBar
android:id="@+id/progressBar1"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:progressDrawable="@drawable/progressbar"
android:visibility="gone"/>
<com.tencent.smtt.sdk.WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none" />
</LinearLayout>
2、再就是onCreate里面的代碼:
myWebView=(WebView) findViewById(R.id.webview);
progressBar=(ProgressBar) findViewById(R.id.progressBar1);
initWebView();//初始化webview的設(shè)置,注釋寫的很清楚
String url="你的URL"
myWebView.loadUrl(url);
private void initWebView() {
WebSettings webSettings=myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);//支持js
// myWebView.requestFocusFromTouch();//如果webView中需要用戶手動(dòng)輸入用戶名、密碼或其他,則webview必須設(shè)置支持獲取手勢(shì)焦點(diǎn)
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //允許js彈窗
myWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String s) {
webView.loadUrl(s);
return true;
}
});
myWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView webView, String s, String s1, JsResult jsResult) {
new AlertDialog.Builder(WebViewActivity.this).setTitle("提示消息").setMessage(s1).setPositiveButton("OK", null).show();
jsResult.confirm(); //不調(diào)用,alert只彈出一次
return true;
}
@Override
public void onProgressChanged(WebView webView, int newProgress) {
super.onProgressChanged(webView, newProgress);
if (newProgress==100) {
progressBar.setVisibility(View.GONE);
} else {
progressBar.setVisibility(View.VISIBLE);
progressBar.setProgress(newProgress);//設(shè)置加載進(jìn)度
}
}
});
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
//如果不做任何處理,瀏覽網(wǎng)頁(yè),點(diǎn)擊系統(tǒng)“Back”鍵,整個(gè)Browser會(huì)調(diào)用finish()而結(jié)束自身,
// 如果希望瀏覽的網(wǎng) 頁(yè)回退而不是推出瀏覽器,需要在當(dāng)前Activity中處理并消費(fèi)掉該Back事件。
if (keyCode==KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) {
myWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
項(xiàng)目就不貼出來(lái)了,代碼很齊全,直接新建項(xiàng)目拷貝就能用
什么要學(xué)習(xí)Android與H5互調(diào)?
微信,QQ空間等大量軟件都內(nèi)嵌了H5,不得不說(shuō)是一種趨勢(shì)。Android與H5互調(diào)可以讓我們的實(shí)現(xiàn)混合開發(fā),至于混合開發(fā)就是在一個(gè)App中內(nèi)嵌一個(gè)輕量級(jí)的瀏覽器,一部分原生的功能改為Html 5來(lái)開發(fā)。
優(yōu)勢(shì):使用H5實(shí)現(xiàn)的功能能夠在不升級(jí)App的情況下動(dòng)態(tài)更新,而且可以在Android或iOS的App上同時(shí)運(yùn)行,節(jié)約了成本,提高了開發(fā)效率。
原理:其實(shí)就是Java代碼和JavaScript之間的調(diào)用。
開局插入一張文章的目錄結(jié)構(gòu):
WebView簡(jiǎn)介
要實(shí)現(xiàn)Android與H5互調(diào),WebView是一個(gè)很重要的控件,WebView可以很好地幫助我們展示html頁(yè)面,所以有必要先了解一下WebView。
一丶WebView常用方法
//加載assets目錄下的test.html文件 webView.loadUrl("file:///android_asset/test.html"); //加載網(wǎng)絡(luò)資源(注意要加上網(wǎng)絡(luò)權(quán)限) webView.loadUrl("http://blog.csdn.net");
webView.setWebViewClient(new WebViewClient(){ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if("http://www.jikedaohang.com/".equals(url)) { view.loadUrl("https://www.baidu.com/"); } return true; } });
mWebView.setWebViewClient(new WebViewClient(){ @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { WebResourceResponse response=null; if (url.contains("logo")) { try { InputStream logo=getAssets().open("logo.png"); response=new WebResourceResponse("image/png", "UTF-8", logo); } catch (IOException e) { e.printStackTrace(); } } return response; } });
webView.setWebViewClient(new WebViewClient() { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); // 開始加載網(wǎng)頁(yè)時(shí)處理 如:顯示"加載提示" 的加載對(duì)話框 ... } @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); // 網(wǎng)頁(yè)加載完成時(shí)處理 如:讓 加載對(duì)話框 消失 ... } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); // 加載網(wǎng)頁(yè)失敗時(shí)處理 如:提示失敗,或顯示新的界面 ... } });
webView.setWebViewClient(new WebViewClient() { @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.proceed(); // 接受信任所有網(wǎng)站的證書 // handler.cancel(); // 默認(rèn)操作 不處理 // handler.handleMessage(null); // 可做其他處理 } });
webView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView view, int progress) { setTitle("頁(yè)面加載中,請(qǐng)稍候..." + progress + "%"); setProgress(progress * 100); if (progress==100) { //... } } });
//1.首先在WebView初始化時(shí)添加如下代碼 if(Build.VERSION.SDK_INT >=19) { /*對(duì)系統(tǒng)API在19以上的版本作了兼容。因?yàn)?.4以上系統(tǒng)在onPageFinished時(shí)再恢復(fù)圖片加載時(shí),如果存在多張圖片引用的是相同的src時(shí),會(huì)只有一個(gè)image標(biāo)簽得到加載,因而對(duì)于這樣的系統(tǒng)我們就先直接加載。*/ webView.getSettings().setLoadsImagesAutomatically(true); } else { webView.getSettings().setLoadsImagesAutomatically(false); } //2.在WebView的WebViewClient子類中重寫onPageFinished()方法添加如下代碼: @Override public void onPageFinished(WebView view, String url) { if(!webView.getSettings().getLoadsImagesAutomatically()) { webView.getSettings().setLoadsImagesAutomatically(true); } }
class MyDownloadListenter implements DownloadListener{ @Override public void onDownloadStart(String url, String userAgent,String contentDisposition, String mimetype, long contentLength) { //下載任務(wù)...,主要有兩種方式 //(1)自定義下載任務(wù) //(2)調(diào)用系統(tǒng)的download的模塊 Uri uri=Uri.parse(url); Intent intent=new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); } }
webview.setDownloadListener(new MyDownloadListenter());
public boolean onKeyDown(int keyCode, KeyEvent event) { //其中webView.canGoBack()在webView含有一個(gè)可后退的瀏覽記錄時(shí)返回true if ((keyCode==KeyEvent.KEYCODE_BACK) && webView.canGoBack()) { webView.goBack(); return true; } return super.onKeyDown(keyCode, event); } }
二丶WebSettings配置
WebSettings webSettings=webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setDomStorageEnabled(true);
settings.setDatabasePath(cacheDirPath);
settings.setAppCachePath(cacheDirPath);
settings.setDefaultTextEncodingName(“utf-8”);
settings.setUseWideViewPort(false);
settings.setSupportZoom(true);
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
settings.supportMultipleWindows();
settings.setAllowFileAccess(true);
settings.setNeedInitialFocus(true);
settings.setBuiltInZoomControls(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLoadWithOverviewMode(true);
settings.setLoadsImagesAutomatically(true);
三丶WebViewClient 的回調(diào)方法列表
WebViewClient主要用來(lái)輔助WebView處理各種通知、請(qǐng)求等事件,通過(guò)setWebViewClient方法設(shè)置。
(1)更新歷史記錄
doUpdateVisitedHistory(WebView view, String url, boolean isReload)
(2)應(yīng)用程序重新請(qǐng)求網(wǎng)頁(yè)數(shù)據(jù)
onFormResubmission(WebView view, Message dontResend, Message resend)
(3)在加載頁(yè)面資源時(shí)會(huì)調(diào)用,每一個(gè)資源(比如圖片)的加載都會(huì)調(diào)用一次。
onLoadResource(WebView view, String url)
(4)開始載入頁(yè)面調(diào)用,通常我們可以在這設(shè)定一個(gè)loading的頁(yè)面,告訴用戶程序在等待網(wǎng)絡(luò)響應(yīng)。
onPageStarted(WebView view, String url, Bitmap favicon)
(5)在頁(yè)面加載結(jié)束時(shí)調(diào)用。同樣道理,我們知道一個(gè)頁(yè)面載入完成,于是我們可以關(guān)閉loading 條,切換程序動(dòng)作。
onPageFinished(WebView view, String url)
(6)報(bào)告錯(cuò)誤信息
onReceivedError(WebView view, int errorCode, String description, String failingUrl)
(7)獲取返回信息授權(quán)請(qǐng)求
onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host,String realm)
(8)重寫此方法可以讓webview處理https請(qǐng)求。
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
(9)WebView發(fā)生改變時(shí)調(diào)用
onScaleChanged(WebView view, float oldScale, float newScale)
(10)Key事件未被加載時(shí)調(diào)用
onUnhandledKeyEvent(WebView view, KeyEvent event)
(11)重寫此方法才能夠處理在瀏覽器中的按鍵事件。
shouldOverrideKeyEvent(WebView view, KeyEvent event)
(12)在網(wǎng)頁(yè)跳轉(zhuǎn)時(shí)調(diào)用,這個(gè)函數(shù)我們可以做很多操作,比如我們讀取到某些特殊的URL,于是就可以不打開地址,取消這個(gè)操作,進(jìn)行預(yù)先定義的其他操作,這對(duì)一個(gè)程序是非常必要的。
shouldOverrideUrlLoading(WebView view, String url)
(13)在加載某個(gè)網(wǎng)頁(yè)的資源的時(shí)候多次調(diào)用(已過(guò)時(shí))
shouldInterceptRequest(WebView view, String url)
(14)在加載某個(gè)網(wǎng)頁(yè)的資源的時(shí)候多次調(diào)用
shouldInterceptRequest(WebView view, WebResourceRequest request)
注意:
shouldOverrideUrlLoading在網(wǎng)頁(yè)跳轉(zhuǎn)的時(shí)候調(diào)用,且一般每跳轉(zhuǎn)一次只調(diào)用一次。
shouldInterceptRequest只要是網(wǎng)頁(yè)加載的過(guò)程中均會(huì)調(diào)用,資源加載的時(shí)候都會(huì)回調(diào)該方法,會(huì)多次調(diào)用。
四丶WebChoromeClient的回調(diào)方法列表
WebChromeClient主要用來(lái)輔助WebView處理Javascript的對(duì)話框、網(wǎng)站圖標(biāo)、網(wǎng)站標(biāo)題以及網(wǎng)頁(yè)加載進(jìn)度等。通過(guò)WebView的setWebChromeClient()方法設(shè)置。
(1)監(jiān)聽網(wǎng)頁(yè)加載進(jìn)度
onProgressChanged(WebView view, int newProgress)
(2)監(jiān)聽網(wǎng)頁(yè)標(biāo)題 : 比如百度頁(yè)面的標(biāo)題是“百度一下,你就知道”
onReceivedTitle(WebView view, String title)
(3)監(jiān)聽網(wǎng)頁(yè)圖標(biāo)
onReceivedIcon(WebView view, Bitmap icon)
Java和JavaScript互調(diào)
為方便展示,使用addJavascriptInterface方式實(shí)現(xiàn)與本地js交互(存在漏洞)。也可通過(guò)其他方式實(shí)現(xiàn),比如攔截ur進(jìn)行參數(shù)解析l等。
Java調(diào)JS
function javaCallJs(arg){ document.getElementById("content").innerHTML=("歡迎:"+arg ); }
webView.loadUrl("javascript:javaCallJs("+"'"+name+"'"+")");
以上代碼就是調(diào)用了JS中一個(gè)叫javaCallJs(arg)的方法,并傳入了一個(gè)name參數(shù)。(具體效果下面有展示)
JS調(diào)java
webView.addJavascriptInterface(new JSInterface (),"Android");
class JSInterface { @JavascriptInterface public void showToast(String arg){ Toast.makeText(MainActivity.this,arg,Toast.LENGTH_SHORT).show(); } }
<input type="button" value="點(diǎn)擊Android被調(diào)用" onclick="window.Android.showToast('JS中傳來(lái)的參數(shù)')"/>
window.Android.showToast(‘JS中傳來(lái)的參數(shù)’)”中的”Android”即addJavascriptInterface()中指定的,并且JS向java傳遞了參數(shù),類型為String。而showToast(String arg)會(huì)以Toast的形式彈出此參數(shù)。
java與JS互調(diào)代碼示例
先看效果圖:
不好意思,傳錯(cuò)了,是這張:
代碼非常簡(jiǎn)單,并且加了注釋,直接看代碼就可以了。
<html> <head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <script type="text/javascript"> function javaCallJs(arg){ document.getElementById("content").innerHTML=("歡迎:"+arg ); } </script> </head> <body> <div id="content"> 請(qǐng)?jiān)谏戏捷斎肽挠脩裘?lt;/div> <input type="button" value="點(diǎn)擊Android被調(diào)用" onclick="window.Android.showToast('JS中傳來(lái)的參數(shù)')"/> </body> </html>
javaCallJs是java調(diào)用JS的方法,showToast方法是JS調(diào)用java的方法
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/ll_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="20dp" android:background="#000088"> <EditText android:id="@+id/et_user" android:layout_width="0dp" android:layout_height="wrap_content" android:hint="輸入WebView中要顯示的用戶名" android:background="#008800" android:textSize="16sp" android:layout_weight="1"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="40dp" android:layout_marginRight="20dp" android:textSize="16sp" android:text="確定" android:onClick="click"/> </LinearLayout> </LinearLayout>
很簡(jiǎn)單,就是一個(gè)輸入框和一個(gè)確定按鈕,點(diǎn)擊按鈕會(huì)調(diào)用JS中的方法。
package com.wangjian.webviewdemo; import android.annotation.SuppressLint; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.webkit.JavascriptInterface; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private WebView webView; private LinearLayout ll_root; private EditText et_user; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ll_root=(LinearLayout) findViewById(R.id.ll_root); et_user=(EditText) findViewById(R.id.et_user); initWebView(); } //初始化WebView private void initWebView() { //動(dòng)態(tài)創(chuàng)建一個(gè)WebView對(duì)象并添加到LinearLayout中 webView=new WebView(getApplication()); LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); webView.setLayoutParams(params); ll_root.addView(webView); //不跳轉(zhuǎn)到其他瀏覽器 webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } }); WebSettings settings=webView.getSettings(); //支持JS settings.setJavaScriptEnabled(true); //加載本地html文件 webView.loadUrl("file:///android_asset/JavaAndJavaScriptCall.html"); webView.addJavascriptInterface(new JSInterface(),"Android"); } //按鈕的點(diǎn)擊事件 public void click(View view){ //java調(diào)用JS方法 webView.loadUrl("javascript:javaCallJs(" + "'" + et_user.getText().toString()+"'"+")"); } //在頁(yè)面銷毀的時(shí)候?qū)ebView移除 @Override protected void onDestroy() { super.onDestroy(); ll_root.removeView(webView); webView.stopLoading(); webView.removeAllViews(); webView.destroy(); webView=null; } private class JSInterface { //JS需要調(diào)用的方法 @JavascriptInterface public void showToast(String arg){ Toast.makeText(MainActivity.this,arg,Toast.LENGTH_SHORT).show(); } } }
需要注意的地方
參考鏈接:安卓webview的一些坑
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); wv.saveState(outState); Log.e(TAG, "save state..."); }
恢復(fù)狀態(tài)(在activity的onCreate(bundle savedInstanceState)里)
if(null!=savedInstanceState){ wv.restoreState(savedInstanceState); Log.i(TAG, "restore state"); }else{ wv.loadUrl("http://3g.cn"); }
其他一些常見問(wèn)題:
1. WebViewClient.onPageFinished()。
你永遠(yuǎn)無(wú)法確定當(dāng)WebView調(diào)用這個(gè)方法的時(shí)候,網(wǎng)頁(yè)內(nèi)容是否真的加載完畢了。當(dāng)前正在加載的網(wǎng)頁(yè)產(chǎn)生跳轉(zhuǎn)的時(shí)候這個(gè)方法可能會(huì)被多次調(diào)用,StackOverflow上有比較具體的解釋(How to listen for a Webview finishing loading a URL in Android?), 但其中列舉的解決方法并不完美。所以當(dāng)你的WebView需要加載各種各樣的網(wǎng)頁(yè)并且需要在頁(yè)面加載完成時(shí)采取一些操作的話,可能WebChromeClient.onProgressChanged()比WebViewClient.onPageFinished()都要靠譜一些。
2. WebView后臺(tái)耗電問(wèn)題。
當(dāng)你的程序調(diào)用了WebView加載網(wǎng)頁(yè),WebView會(huì)自己開啟一些線程(?),如果你沒(méi)有正確地將WebView銷毀的話,這些殘余的線程(?)會(huì)一直在后臺(tái)運(yùn)行,由此導(dǎo)致你的應(yīng)用程序耗電量居高不下。對(duì)此我采用的處理方式比較偷懶,簡(jiǎn)單又粗暴(不建議),即在Activity.onDestroy()中直接調(diào)用System.exit(0),使得應(yīng)用程序完全被移出虛擬機(jī),這樣就不會(huì)有任何問(wèn)題了。
3. 切換WebView閃屏問(wèn)題。
如果你需要在同一個(gè)ViewGroup中來(lái)回切換不同的WebView(包含了不同的網(wǎng)頁(yè)內(nèi)容)的話,你就會(huì)發(fā)現(xiàn)閃屏是不可避免的。這應(yīng)該是Android硬件加速的Bug,如果關(guān)閉硬件加速這種情況會(huì)好很多,但無(wú)法獲得很好的瀏覽體驗(yàn),你會(huì)感覺(jué)網(wǎng)頁(yè)滑動(dòng)的時(shí)候一卡一卡的,不跟手。
4. 在某些手機(jī)上,Webview有視頻時(shí),activity銷毀后,視頻資源沒(méi)有被銷毀,甚至還能聽到在后臺(tái)播放。即便是像剛才那樣各種銷毀webview也無(wú)濟(jì)于事,解決辦法:在onDestory之前修改url為空地址。
5.WebView硬件加速導(dǎo)致頁(yè)面渲染閃爍問(wèn)題
關(guān)于Android硬件加速 開始于Android 3.0 (API level 11),開啟硬件加速后,WebView渲染頁(yè)面更加快速,拖動(dòng)也更加順滑。但有個(gè)副作用就是容易會(huì)出現(xiàn)頁(yè)面加載白塊同時(shí)界面閃爍現(xiàn)象。解決這個(gè)問(wèn)題的方法是設(shè)置WebView暫時(shí)關(guān)閉硬件加速 代碼如下:
if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.HONEYCOMB) {
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
JavaScript簡(jiǎn)介
JavaScript是一種動(dòng)態(tài)類型的腳本語(yǔ)言;在1995年時(shí),由Netscape公司的Brendan Eich,在網(wǎng)景導(dǎo)航者瀏覽器上首次設(shè)計(jì)實(shí)現(xiàn)而成。因?yàn)镹etscape與Sun合作,Netscape管理層希望它外觀看起來(lái)像Java,因此取名為JavaScript。
JavaScript腳本語(yǔ)言具有以下特點(diǎn):
(1) 腳本語(yǔ)言。JavaScript是一種解釋型的腳本語(yǔ)言,是在程序的運(yùn)行過(guò)程中逐行進(jìn)行解釋執(zhí)行,不需要預(yù)編譯。而Java、C++等語(yǔ)言需要先編譯后執(zhí)行;
(2) 動(dòng)態(tài)性。JavaScript能夠動(dòng)態(tài)修改對(duì)象的屬性,沒(méi)有辦法在編譯的時(shí)候知道變量的類型,只有在運(yùn)行的時(shí)候才能確定。而Java、C++等都是靜態(tài)類型語(yǔ)言,他們?cè)诰幾g的時(shí)候就能夠知道每個(gè)變量的類型;
(3) 跨平臺(tái)性。JavaScript腳本語(yǔ)言不依賴于操作系統(tǒng),僅需要瀏覽器的支持。可以在多種平臺(tái)下運(yùn)行(如Windows、Linux、Mac、Android、IOS等)。
二
JavaScript與Java語(yǔ)言區(qū)別
從上面介紹的JavaScript語(yǔ)言特點(diǎn)會(huì)發(fā)現(xiàn)JavaScript的效率會(huì)比Java、C++低很多;看以下這個(gè)實(shí)例:
當(dāng)JavaScript引擎分析到該段代碼時(shí),根本不知道a和b是什么類型,唯一的辦法就是運(yùn)行的時(shí)候根據(jù)實(shí)際傳過(guò)來(lái)的對(duì)象再來(lái)計(jì)算,這顯然會(huì)導(dǎo)致嚴(yán)重的性能問(wèn)題。
當(dāng)編譯上面Java代碼的時(shí)候,根據(jù)右邊類型Class1的定義,獲取對(duì)象a的屬性x的時(shí)候,其實(shí)就是對(duì)象a的地址,大小是一個(gè)整形。同時(shí)獲取對(duì)象b的屬性y的時(shí)候,其實(shí)就是對(duì)象b的地址加上4個(gè)字節(jié),這些都是在生成本地代碼時(shí)確定的,無(wú)需在運(yùn)行本地代碼的時(shí)候再?zèng)Q定他們的地址和類型是什么,這顯然能夠節(jié)省時(shí)間。
再看一下兩者分別是怎樣存儲(chǔ)對(duì)象a和b的:
對(duì)于傳統(tǒng)的JavaScript解釋器來(lái)說(shuō),因?yàn)椴恢繿和b的具體類型,就用屬性名-屬性值對(duì)來(lái)保存,之后訪問(wèn)對(duì)象的屬性值時(shí)就需要通過(guò)屬性名匹配來(lái)獲取對(duì)應(yīng)的值;對(duì)象b也是同樣的結(jié)果來(lái)保存相同的屬性。隨著對(duì)象的增多,這顯然帶來(lái)了巨大的空間浪費(fèi)。
而上面的Java代碼在編譯時(shí)就確定了類Class1的成員類型,訪問(wèn)x就是對(duì)象a的地址,y就是a的地址加上4個(gè)字節(jié);所以字符“x”和“y”運(yùn)行時(shí)都不再需要,因?yàn)椴辉傩枰~外查找這些屬性地址的工作。
從上面實(shí)例可以看到JavaScript和Java語(yǔ)言的區(qū)別包括以下幾個(gè)部分:
編譯確定位置:Java有編譯和執(zhí)行兩個(gè)階段,位置的偏移信息都是在編譯器編譯的時(shí)候決定的,當(dāng)Java生成本地代碼之后,對(duì)象的屬性和偏移信息都計(jì)算完成。而JavaScript沒(méi)有類型,只有在對(duì)象執(zhí)行創(chuàng)建的時(shí)候才確定這些信息,而且JavaScript語(yǔ)言能夠在執(zhí)行時(shí)修改對(duì)象的屬性。
偏移信息共享:Java有類型定義,所有的對(duì)象都是共享偏移信息的,訪問(wèn)他們只需要按照編譯時(shí)確定的偏移量即可。JavaScript則不同,每個(gè)對(duì)象都有自我描述,屬性和位置偏移信息都包含在自身的結(jié)構(gòu)中。
偏移信息查找:Java查找偏移地址很簡(jiǎn)單,都是在編譯代碼時(shí),對(duì)使用到的類型成員變量直接設(shè)置偏移量。而JavaScript則需要通過(guò)屬性名匹配才能查找到對(duì)應(yīng)的值。
Java語(yǔ)言有明顯的兩個(gè)階段:編譯和運(yùn)行,如下圖所示:
Java代碼經(jīng)過(guò)編譯器編譯之后生成的是字節(jié)碼,字節(jié)碼是跨平臺(tái)的一種中間表示,不同于本地代碼。該字節(jié)碼與平臺(tái)無(wú)關(guān),能夠在不同的操作系統(tǒng)上運(yùn)行。在運(yùn)行字節(jié)碼階段,Java的運(yùn)行環(huán)境是Java虛擬機(jī)加載字節(jié)碼。Java虛擬機(jī)一般都引入JIT技術(shù)來(lái)將字節(jié)碼轉(zhuǎn)變成本地代碼來(lái)提高執(zhí)行效率。第一階段對(duì)時(shí)間要求不嚴(yán)格,第二階段對(duì)每個(gè)步驟所花費(fèi)的時(shí)間非常敏感,時(shí)間越短越好。
JavaScript語(yǔ)言的編譯和執(zhí)行都是在運(yùn)行階段執(zhí)行的,如下圖所示:
因?yàn)槎际窃诖a運(yùn)行過(guò)程中來(lái)處理這些步驟,所以每個(gè)階段的時(shí)間越短越好,而且每引入一個(gè)階段都是額外的時(shí)間開銷,所以一個(gè)JavaScript引擎主要包含以下幾個(gè)部分:
編譯器:主要工作是將源代碼編譯成抽象語(yǔ)法樹;
解釋器:主要是接受字節(jié)碼,解釋執(zhí)行這個(gè)字節(jié)碼;
JIT工具:將字節(jié)碼或抽象語(yǔ)法樹轉(zhuǎn)換成本地代碼;
垃圾回收期和分析工具(Profiler):負(fù)責(zé)垃圾回收和收集引擎中的信息,幫助改善引擎的性能。
三
V8引擎介紹
V8是一個(gè)JavaScript引擎實(shí)現(xiàn)的開源項(xiàng)目,最開始由一些語(yǔ)言學(xué)家設(shè)計(jì)出來(lái),后被Google收購(gòu),成為了JavaScript引擎和眾多相關(guān)技術(shù)的引領(lǐng)者。V8支持眾多的操作系統(tǒng),包括Windows、Linux、Android、Mac OS X等。同時(shí)它也能夠支持眾多的硬件架構(gòu),如IA32、X64、ARM、MIPS等,將主流軟硬件平臺(tái)一網(wǎng)打盡。由于它是一個(gè)開源項(xiàng)目,開發(fā)者可以自由使用它的強(qiáng)大能力,目前炙手可熱的NodeJs項(xiàng)目就是基于V8項(xiàng)目研發(fā)的。
第一條語(yǔ)句:表示建立一個(gè)域,用于包含一組Handle對(duì)象,便于管理和釋放它們;
第二條語(yǔ)句:根據(jù)Isolate對(duì)象來(lái)獲取一個(gè)Context對(duì)象,使用Handle來(lái)管理。Handle對(duì)象本身存放在棧上,而實(shí)際的Context對(duì)象保存在堆中;
第三條語(yǔ)句:根據(jù)兩個(gè)對(duì)象Isolate和Context來(lái)創(chuàng)建一個(gè)函數(shù)間使用的對(duì)象,使用Persistent類來(lái)管理;
第四條語(yǔ)句:表示為Context對(duì)象創(chuàng)建一個(gè)基于棧的域,下面的執(zhí)行步驟都是在該域中對(duì)應(yīng)的上下文中來(lái)進(jìn)行的;
第五條語(yǔ)句:讀入一段JavaScript代碼;
第六條語(yǔ)句:將代碼字符串編譯成V8的內(nèi)部表示,并保存成一個(gè)Script對(duì)象;
第七條語(yǔ)句:執(zhí)行編譯后的內(nèi)部表示,獲得生成的結(jié)果。
首先通過(guò)編譯器將源代碼編譯成抽象語(yǔ)法樹:
不同于JavaScriptCore引擎,V8引擎并不將抽象語(yǔ)法樹轉(zhuǎn)變成字節(jié)碼,而是通過(guò)JIT編譯器的全代碼生成器從抽象語(yǔ)法樹直接生成本地代碼。
其過(guò)程中的主要類圖如下:
Script:表示的是JavaScript代碼,既包含源代碼,又包含編譯之后生成的本地代碼,所以它既是編譯入口,又是運(yùn)行入口;
Compiter:編譯器類,輔助Script類來(lái)編譯生成代碼,它主要起一個(gè)協(xié)調(diào)者的作用,會(huì)調(diào)用解析器(Parse)來(lái)生成抽象語(yǔ)法樹和全代碼生成器,來(lái)為抽象語(yǔ)法樹生成本地代碼;
Parse:將源代碼解析并構(gòu)建成抽象語(yǔ)法樹,使用AstNodeFactory類來(lái)創(chuàng)建他們,并使用Zone類來(lái)分配內(nèi)存;
AstNode:抽象語(yǔ)法樹節(jié)點(diǎn)類,是其他所有節(jié)點(diǎn)的基類;
AstVisitor:抽象語(yǔ)法樹的訪問(wèn)者類,主要用來(lái)遍歷抽象語(yǔ)法樹;
FullCodeGenerator:AstVisitor類的子類,通過(guò)遍歷抽象語(yǔ)法樹來(lái)為JavaScript生成本地可執(zhí)行的代碼。
V8運(yùn)行階段的主要類圖如下:
Script:前面介紹過(guò),包含編譯之后生成的本地代碼,運(yùn)行代碼的入口;
Execution:運(yùn)行代碼的輔助類,包含一些重要的函數(shù)“call”,它輔助進(jìn)入和執(zhí)行Script中的本地代碼;
JSFunction:需要執(zhí)行的JavaScript函數(shù)表示類;
Runtime:運(yùn)行本地代碼的輔助類,主要提供運(yùn)行時(shí)各種輔助函數(shù);
Heap:運(yùn)行本地代碼需要使用的內(nèi)存堆;
MarkCompactCollector:垃圾回收機(jī)制的主要實(shí)現(xiàn)類,用來(lái)標(biāo)記,清除和整理等基本的垃圾回收過(guò)程;
SweeperThread:負(fù)責(zé)垃圾回收的線程。
V8中代碼的執(zhí)行過(guò)程如下圖:
四
V8引擎所做優(yōu)化
1. 優(yōu)化回滾
Crankshaft編譯器主要針對(duì)熱點(diǎn)函數(shù)進(jìn)行優(yōu)化,它是基于JS源碼分析的,而不是本地代碼。為了性能考慮Crankshaft編譯器會(huì)進(jìn)行一些樂(lè)觀的預(yù)測(cè),認(rèn)為這些代碼比較穩(wěn)定,變量類型不會(huì)發(fā)生變化,所以能夠生成高效的本地代碼。然而進(jìn)行優(yōu)化之后,V8發(fā)現(xiàn)并不是最優(yōu)的,會(huì)執(zhí)行優(yōu)化回滾操作。
2. 隱藏類
將對(duì)象劃分成不同的組,相同的組內(nèi)對(duì)象擁有相同的屬性名和屬性值,組內(nèi)的所有對(duì)象貢獻(xiàn)該信息:
實(shí)例中對(duì)象a和b包含相同的屬性名,V8就會(huì)將其歸為同一個(gè)組,也就是隱藏類。這些屬性在隱藏類中有相同的偏移值,這樣,對(duì)象a和b可以共享這個(gè)類型信息,當(dāng)訪問(wèn)這些對(duì)象屬性的時(shí)候,根據(jù)隱藏類的偏移值就可以知道它們的位置并進(jìn)行訪問(wèn)。
3. 內(nèi)存管理
V8使用堆來(lái)管理JavaScript使用的數(shù)據(jù),以及生成的代碼、哈希表等;為了更方便地實(shí)現(xiàn)垃圾回收,同很多虛擬機(jī)一樣,V8將堆分成三個(gè)部分,第一個(gè)是年輕分代,第二個(gè)是年老分代,第三個(gè)是大對(duì)象保留的空間;如下圖:
4. 快照(Snapshot)
V8引擎開始啟動(dòng)的時(shí)候,需要加載很多內(nèi)置的全局對(duì)象,同時(shí)也要建立內(nèi)置的函數(shù),比如Array、String、Math等。為了讓引擎更加整潔,加載對(duì)象與建立函數(shù)等任務(wù)都是使用JS文件來(lái)實(shí)現(xiàn)的,V8引擎負(fù)責(zé)在編譯和執(zhí)行輸入的JavaScript代碼之前,先加載他們。
快照機(jī)制就是將一些內(nèi)置的對(duì)象和函數(shù)加載之后的內(nèi)存保存并序列化。序列化之后的結(jié)果很容易被反序列化,經(jīng)過(guò)快照機(jī)制的啟動(dòng)時(shí)間,可以縮短啟動(dòng)時(shí)間。快照機(jī)制也能夠?qū)㈤_發(fā)者認(rèn)為需要的JS文件序列化,減少以后處理的時(shí)間。
5. 綁定和擴(kuò)展
V8提供兩種機(jī)制來(lái)擴(kuò)展引擎的能力,第一是Extension機(jī)制,就是通過(guò)V8提供的基類Extension來(lái)達(dá)到擴(kuò)展JavaScript能力的目的。第二是綁定,使用IDL文件或者接口文件來(lái)生成綁定文件,然后將這些文件同V8引擎代碼一起編譯。
五
實(shí)踐——寫JavaScript需要注意地方
1. 不要破壞隱藏類
建議:在構(gòu)造函數(shù)中初始化所有對(duì)象成員,不要在以后更改類型;以相同的順序初始化對(duì)象成員。
2. 數(shù)據(jù)表示
在V8中,數(shù)據(jù)的表示分成兩個(gè)部分,第一個(gè)部分是數(shù)據(jù)的實(shí)際內(nèi)容,它們是變長(zhǎng)的;第二部分是數(shù)據(jù)的句柄,句柄的大小是固定的,句柄中包含指向數(shù)據(jù)的指針。為什么要這樣設(shè)計(jì)呢?主要是因?yàn)閂8需要進(jìn)行垃圾回收,并需要移動(dòng)這些數(shù)據(jù)內(nèi)容,如果直接使用指針的話就會(huì)出問(wèn)題或者需要比較大的開銷,使用句柄的話就不存在這些問(wèn)題,只需要將句柄中的指針修改即可。
具體的定義如下:
一個(gè)Handler的大小是4字節(jié)(32位機(jī)器),整數(shù)直接從value_
中獲取值,而無(wú)需從堆中分配,然后分配一個(gè)指針指向它,這可以減少內(nèi)存的使用并增加數(shù)據(jù)的訪問(wèn)速度。
所以,對(duì)于數(shù)值來(lái)說(shuō),只要能夠使用整數(shù)的,盡量不要使用浮點(diǎn)數(shù)。
3. 數(shù)組初始化
建議:初始化使用數(shù)組常量小型固定大小的數(shù)組;
不要儲(chǔ)存在數(shù)字?jǐn)?shù)組非數(shù)字值(對(duì)象);
不要?jiǎng)h除數(shù)組中的元素,尤其是數(shù)字?jǐn)?shù)組;
不要裝入未初始化或刪除元素。
4. 內(nèi)存
對(duì)引用不再使用的對(duì)象的變量設(shè)置為空(a=),引入delete關(guān)鍵字,刪除無(wú)用對(duì)象。
5. 優(yōu)化回滾
不要書寫出觸發(fā)優(yōu)化回滾的代碼,否則會(huì)大幅降低代碼的性能。執(zhí)行多次之后,不要出現(xiàn)修改對(duì)象類型的語(yǔ)句。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。