欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Hybrid学习记录

程序员文章站 2022-04-21 12:04:47
...

一、初始化

1.加载

//方式1. 加载一个网页:
webView.loadUrl("http://www.google.com/");

//方式2:加载apk包中的html页面
webView.loadUrl("file:///android_asset/test.html");

//方式3:加载手机本地的html页面
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");

//方式4:加载某一段代码
val body = "示例:这里有个img标签,地址是相对路径[外链图片转存失败(img-8vlE8Da6-1562229182334)(https://mp.csdn.net/uploads/allimg/130923/1FP02V7-0.png)]";
//baseUrl表示基础的网页,data表示要加载的内容,mimeType表示加载网页的类型
//encoding表示编码格式,historyUrl表示可用历史记录
webView.loadDataWithBaseURL("http://www.jcodecraeer.com", body, "text/html", "utf-8",null);

防止域控制不严格漏洞

settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowUniversalAccessFromFileURLs(false);

2.暂停

override fun onPause() {
    //通知内核尝试停止所有处理,如动画和地理位置,但是不能停止Js
    webView.onPause()
    //全应用程序的webView都会被暂停,降低CPU功耗
    webView.pauseTimers()
    //若加载的 html 里有JS 在执行动画等操作,需要关闭Javascript支持
    settings.setJavaScriptEnabled(false)
    super.onPause()
}

override fun onResume() {
    //重新**WebView
    webView.onResume()
    //恢复pauseTimers状态
    webView.resumeTimers()
    //若加载的 html 里有JS 在执行动画等操作,重新进入开启Javascript支持
    // 禁止 file 协议加载 JavaScript
    if (url.startsWith("file://")){
        settings.setJavaScriptEnabled(false)
    }else{
        settings.setJavaScriptEnabled(true)
    }
    super.onResume()
}

3.关闭
在关闭Activity时,如果WebView的音乐或视频,还在播放,就必须销毁Webview

override fun onDestroy() {
    super.onDestroy()
    //从当前父容器移除WebView,防止其持有Activity的引用
    constraintLayout.removeView(webView)
    //销毁WebView
    webView.destroy();
}

4.导航
用户点击back键会有两种情况,有前一页的页面,那么就后退,否则退出程序

override fun onBackPressed() {
    //是否可以后退一页,是则后退,否则退出程序
    if (webView.canGoBack()){
        webView.goBack()
    }else{
        finish()
    }
}

前进一页也要先判断是否有前一页
if (webView.canGoForward()){
    webView.goForward()
}

5.设置缓存

val settings = webView.getSettings()
if (OKHttpUtil.INSTANCE.isNetworkConnected()) {
    // 默认根据cache-control决定是否从网络上取数据
    settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
    // 没网,离线加载,优先加载缓存(即使已经过期)
    settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}

根据需要可以清除缓存

//清除整个应用程序的缓存,传入true表示同时内存与磁盘,false表示仅清除内存
webView.clearCache(true);

//调用方法时,注意是当前,也就是说当前的页面记录并不会被删除。
//这样的话我从A打开B同时调用该方法,此时当前页面是A,清空的是A的之前的记录,A的自身记录还在。
//因此要在当前页面是B的时候调用该方法。
//清除当前webview访问的历史记录
webView.clearHistory();

//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据
webView.clearFormData();

6.设置存储

//开启 DOM storage API 功能 较大存储空间,使用简单,默认值 false
//不安全,任何人都能读取到它
settings.setDomStorageEnabled(true);

7.不使用系统游览器

webView.webViewClient = object : WebViewClient(){
    /**
     * 拦截页面加载,返回true表示宿主app拦截并处理了该url
     * 否则返回false由当前WebView处理
     */
    override fun shouldOverrideUrlLoading(
        view: WebView,
        request: WebResourceRequest
    ): Boolean {
        //重定向返回true
        //view.loadUrl(url);
        //return true;
        return false
    }
}

8.loading

webView.webViewClient = object : WebViewClient(){
    //开始载入页面加载loading
    override fun onPageStarted(view: WebView, url: String, favicon: Bitmap) {
        progressBar.visibility = View.VISIBLE
    }
    //结束载入页面关闭loading
    override fun onPageFinished(view: WebView, url: String) {
        progressBar.visibility = View.GONE
    }
}

9.加载异常

webView.webViewClient = object : WebViewClient(){
    //加载资源时出错
    override fun onReceivedError(
        view: WebView,
        request: WebResourceRequest,
        error: WebResourceError
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            when(error.errorCode){
                
            }
        }
    }
    //http出错
    override fun onReceivedHttpError(
        view: WebView,
        request: WebResourceRequest,
        errorResponse: WebResourceResponse
    ) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            when(errorResponse.statusCode){
    
            }
        }
    }
}

10.弹窗提示

// 设置允许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

webView.webChromeClient = object : WebChromeClient() {
    //获取页面标题
    override fun onReceivedTitle(view: WebView, title: String) {
        pageTitle = title
    }

    //弹出javascript的警告框
    override fun onJsAlert(
        view: WebView,
        url: String,
        message: String,
        result: JsResult
    ): Boolean {
        AlertDialog.Builder([email protected])
            .setTitle("安全提示")
            .setMessage(message)
            .setCancelable(false)
            .setPositiveButton("OK", DialogInterface.OnClickListener { dialog, which ->
                result.confirm()
            })
            .show()
        return true
    }

    //弹出javascript的确认框
    override fun onJsConfirm(
        view: WebView,
        url: String,
        message: String,
        result: JsResult
    ): Boolean {
        var isDetermine = true
        AlertDialog.Builder([email protected])
            .setTitle("确认提示")
            .setMessage(message)
            .setCancelable(false)
            .setPositiveButton("确定", DialogInterface.OnClickListener { dialog, which ->
                result.confirm()
            })
            .setNegativeButton("取消", DialogInterface.OnClickListener { dialog, which ->
                result.cancel()
                isDetermine = false
            })
            .show()
        return isDetermine
    }

    //弹出javascript输入框,点击确认返回输入框中的值,点击取消返回 null
    override fun onJsPrompt(
        view: WebView,
        url: String,
        message: String,
        defaultValue: String,
        result: JsPromptResult
    ): Boolean {
        val editText = EditText([email protected])
        editText.setText(defaultValue)
        AlertDialog.Builder([email protected])
            .setTitle("确认提示")
            .setView(editText)
            .setCancelable(false)
            .setPositiveButton("确定", DialogInterface.OnClickListener { dialog, which ->
                result.confirm(editText.text.toString())
            })
            .setNegativeButton("取消", DialogInterface.OnClickListener { dialog, which ->
                result.cancel()
            })
            .show()
        return true
    }
}

11.自适应屏幕
会与缩放操作冲突,一般不开启自适应屏幕

//将图片调整到适合webview的大小,默认值 false
settings.setUseWideViewPort(true); 
//缩放至屏幕的大小,默认值 false
settings.setLoadWithOverviewMode(true); 

12.缩放操作

//支持缩放,默认为true。是下面那个的前提。
settings.setSupportZoom(true); 
//设置内置的缩放控件。若为false,则该WebView不可缩放
settings.setBuiltInZoomControls(true); 
//隐藏原生的缩放控件,默认为true
settings.setDisplayZoomControls(false); 

13.资源加载

// 是否自动加载图片
settings.setLoadsImagesAutomatically(true);
// 禁止加载网络图片
settings.setBlockNetworkImage(false);
// 禁止加载所有网络资源       
settings.setBlockNetworkLoads(false);

二、交互

1.Javascript

// 是否支持Javascript,默认值false,在onResume()和onPause()中开启和关闭
// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://")){
    settings.setJavaScriptEnabled(false)
}else{
    settings.setJavaScriptEnabled(true)
}

1.1Android调用js的方法

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    webView.evaluateJavascript("javascript:changename()", ValueCallback {
    
    })
}else{
    webView.loadUrl("javascript:changename()")
}

1.2.js调用Android方法
先定义一个要映射给js的对象,要让js调用的方法要加上 @JavascriptInterface

public class WebTest {

    private Context context;

    public WebTest(Context context){
        this.context = context;
    }

    @JavascriptInterface
    public void show(String s) {
        Toast.makeText(context,s,Toast.LENGTH_SHORT).show();
    }

}

然后设置给前端,将WebTest对象映射到前端的mAndroid对象

webview.addJavascriptInterface(new WebTest(this),"mAndroid");

移除已注入的Javascript对象

//下次加载或刷新页面时生效
public void removeJavascriptInterface("mAndroid");

2.下载文件

webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
    //调用http下载
}

3.长按

webView.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View view) {
        WebView.HitTestResult result = ((WebView) view).getHitTestResult();
        if(result != null){
            switch (result.getType()){
                case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
                    String imgUrl = result.getExtra();
                    ...
                    return true;
                ...
            }
        }
        return false;
    }
});

HitTestResult 的Type
WebView.HitTestResult.UNKNOWN_TYPE //未知类型

WebView.HitTestResult.PHONE_TYPE //电话类型

WebView.HitTestResult.EMAIL_TYPE //电子邮件类型

WebView.HitTestResult.GEO_TYPE //地图类型

WebView.HitTestResult.SRC_ANCHOR_TYPE //超链接类型

WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE //带有链接的图片类型

WebView.HitTestResult.IMAGE_TYPE //单纯的图片类型

WebView.HitTestResult.EDIT_TEXT_TYPE //选中的文字类型

三、混淆

keepattributes *Annotation*  
-keepattributes *JavascriptInterface*
-keep public class org.mq.study.webview.DemoJavaScriptInterface{
    public <methods>;
}

如果是内部类:

-keepattributes *Annotation*  
-keepattributes *JavascriptInterface*
-keep public class org.mq.study.webview.webview.DemoJavaScriptInterface$InnerClass{
    public <methods>;
}

四、优化

WebView首次启动需要一个初始化的动作,会占用70-700ms,这里可以如下优化:
1.全局WebView
客户端启动时便初始化一个全局WebView待用,当用户访问页面时便使用这个全局Web体View。

@SuppressLint("StaticFieldLeak")
object WebViewUtil {

    private lateinit var webView:WebView
    private lateinit var application: Application
    private lateinit var contextWrapper:MutableContextWrapper

    fun init(application:Application){
        this.application = application
        contextWrapper = MutableContextWrapper(application)
        webView = WebView(contextWrapper)
    }

    /**
     * 进入Activity时重用WebView
     */
    fun reuse(activityContext:Activity):WebView{
        contextWrapper.baseContext = activityContext
        return webView
    }

    /**
     * 退出Activity时重置防止内存泄漏
     */
    fun reset(){
        contextWrapper.baseContext = application
    }

}

在Application中初始化WebViewUtil

WebViewUtil.init(this)

在Activity布局文件中创建一个layout指定WebView位置

<FrameLayout
    android:id="@+id/layout"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/guideline2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

在Activity中调用WebView

webView = WebViewUtil.reuse(this)
layout.addView(webView)

退出Activity时 Context 替换为 Application

override fun onDestroy() {
    super.onDestroy()
    layout.removeView(webView)
    WebViewUtil.reset()
}

如果复用WebView会有历史记录存在,有两种处理方式
方式一,在onPageFinished中清除厉害记录

webView.webViewClient = object : WebViewClient() {
    override fun onPageFinished(view: WebView?, url: String?) {
        webView.clearHistory()
    }
}

方式二,非首次进入不加载url

if (webView.url == null){
    webView.loadUrl(url)
}

2.并行加载

不使用WebView的网络功能,而是在WebView初始化时并行网络操作,然后将数据加载到WebView中
2.1启动子线程请求页面主资源,子线程中不断将网络数据读取到内存中,也就是网络流(NetStream)和内存流(MemStream)之间的转换;

2.2当WebView初始化完成的时候,提供一个中间层BridgeStream来连接WebView和数据流;

2.3当WebView读取数据的时候,中间层BridgeStream会先把内存的数据读取返回后,再继续读取网络的数据。

3.预加载
静态页面:
WebView初始化前,提前获取首页页面,如果是第一次,获取的是完整的html文件,如不是则获取增量包

动态页面:
将H5文件分为静态模板和将要发生变化的数据块,默认先加载静态模板,有变化只更新变化的部分

参考自腾讯的VasSonic框架 https://github.com/Tencent/VasSonic

相关标签: Hybrid WebView