Android中使用x5内核加载网页的实现

前言

由于是使用的腾讯浏览服务,所以这里大部分介绍的是官网的一些东西,不过自己会做一些复杂使用部分的实现,不至于像官网上介绍的笼统。


联系方式

这里用的是TBS腾讯浏览服务。

官网地址是 https://x5.tencent.com

微信公众号:腾讯浏览服务

论坛 http://bbs.mb.qq.com/forum-112-1.html ( 开发者反馈论坛 )

反馈 http://bbs.mb.qq.com/newthread?fid=112


背景

这里介绍一下x5内核相对Android系统内核的一些优势

  • TBS(腾讯浏览服务)的优势

1) 速度快:相比系统webview的网页打开速度有30+%的提升;
2) 省流量:使用云端优化技术使流量节省20+%;
3) 更安全:安全问题可以在24小时内修复;
4) 更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;
5) 兼容好:无系统内核的碎片化问题,更少的兼容性问题;
6) 体验优:支持夜间模式、适屏排版、字体设置等浏览增强功能;
7) 功能全:在Html5、ES6上有更完整支持;
8) 更强大:集成强大的视频播放器,支持视频格式远多于系统webview;
9) 视频和文件格式的支持x5内核多于系统内核;
10) 防劫持是x5内核的一大亮点

  • 运行环境

1)手机ROM版本高于或等于2.2版本;
2)手机RAM大于500M,该RAM值通过手机 /proc/meminfo 文件的MemTotal动态获取
注:如果不满足上述条件,SDK会自动切换到系统WebView,SDK使用者不用关心该切换过程。

  • SDK尺寸指标

1)SDK提供的JAR包约250K


SDK下载

我们肯定需要先下载sdk的jar包,然后才能使用该sdk的功能,也就是这里的x5内核。

腾讯浏览服务的sdk下载链接是:腾讯SDK下载页。这里我选择的是第三个(完整版+文件能力的Android SDK),当然选择第一个也可以,只不过不带有文件能力。第二个适用于快速接入TBS且常规使用WebView的开发者。因为我这里可能还涉及到与html网页的交互,为了方便,就没有选择第二个。因为第一个和第三个都只是单纯的将x5的内核替换系统的内核,其他没有太多的变化。像一些基本的设置也都和系统内核的是一样的,只不过x5的内核优势很明显。


SDK集成

sdk下载完成后,解压如下图所示:
这里写图片描述

里面有文件常见问题解答文档和文件方案接口介绍文档。因为是带有文件能力的sdk,所以有这些文档,文档也比较简单,写的很基础,这里不多介绍了。

这里我们用到的是jar包,我标注在图中了。将jar包添加到我们的项目中,就可以使用对应的功能了。

将jar包放到libs目录下:
这里写图片描述

然后记得添加jar包依赖,如下:
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

经过上面几个步骤之后,你的项目就集成了该sdk,可以使用x5的内核了。


使用

用到的所有原生WebView导入的类和接口都改导入 com.tencent.smtt.sdk 里面的类,类接口名对应。这样就相当于用x5的内核替换了系统的内核。

需要注意的是:

1)请不要在代码里使用下述写法

import android.*;
import android.webkit.*;
import android.webkit.WebStorage.*;
import android.net.*;
import android.net.http.*;

2)除了源码里需要把相关的包名和类名进行替换,布局xml里面的声明也需要替换,例如:

<com.tencent.smtt.sdk.WebView
android:id="@+id/forum_context"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingLeft="5dp"
android:paddingRight="5dp" />

当然,你要是不在布局里面声明,而是自己在代码中new出来,添加的x5内核,那么就不用管xml布局。这里推荐大家用这种方法来实现,会比在xml布局中声明更加的灵活方便。

替换不完全时,可能发生的问题是关于cookie的身份错误、类转换时的crash等。cookie问题产生的原理是:一段代码把cookie塞给了系统内核,另外一段代码尝试从x5的内核里读取cookie就失败了。类转换的错误产生的原理是:比如xml里指定的是系统的webview,java的代码里把它当作x5的webview使用。

然后大家自己原来已经用的系统的内核,再替换为x5的内核需要你仔细一点,别忘记替换包名,不然就会发生上面的错误。如果刚开始就使用的就是集成的x5内核,那么就不用太担心,不过选择导包的时候还是需要注意别用系统的内核,要使用腾讯提供的x5内核。

x5暂时不提供64位so文件,为了保证64位手机能正常加载x5内核,请参照如下链接修改相关配置https://x5.tencent.com/tbs/technical.html#/detail/sdk/1/34cf1488-7dc2-41ca-a77f-0014112bcab7

AndroidManifest.xml里加入权限声明:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

优化异常上报:

为了提高合作方的webview场景稳定性,及时发现并解决x5相关问题,当客户端发生crash等异常情况并上报给服务器时请务必带上x5内核相关信息。x5内核异常信息获取接口为:com.tencent.smtt.sdk.WebView.getCrashExtraMessage(context)。以bugly日志上报为例:

UserStrategy strategy = new UserStrategy(appContext);
  strategy.setCrashHandleCallback(new CrashReport.CrashHandleCallback() {
    public Map<String, String> onCrashHandleStart(int crashType, String errorType, String errorMessage, String errorStack) {
      LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
      String x5CrashInfo = com.tencent.smtt.sdk.WebView.getCrashExtraMessage(appContext);
      map.put("x5crashInfo", x5CrashInfo);
      return map;
    }
    @Override
    public byte[] onCrashHandleStart2GetExtraDatas(int crashType, String errorType, String errorMessage, String errorStack) {
      try {
        return "Extra data.".getBytes("UTF-8");
      } catch (Exception e) {
        return null;
      }
    }
  });
  CrashReport.initCrashReport(appContext, APPID, true, strategy);

适配修改:

1) App 首次就可以加载 x5 内核

App 在启动后(例如在 Application 的 onCreate 中)立刻调用 QbSdk 的预加载接口 initX5Environment ,可参考接入示例,第一个参数传入 context,第二个参数传入 callback,不需要 callback 的可以传入 null,initX5Environment 内部会创建一个线程向后台查询当前可用内核版本号,这个函数内是异步执行所以不会阻塞 App 主线程,这个函数内是轻量级执行所以对 App 启动性能没有影响,当 App 后续创建 webview 时就可以首次加载 x5 内核了

package com.example.administrator.firststeppro.application;

import android.app.Application;

import com.tencent.smtt.sdk.QbSdk;

/**
 * 自定义MyApplication类继承Application
 * 并重写onCreate方法完成一些初始化加载操作
 */
public class MyApplication extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
        preinitX5WebCore();
    }

    /**
     * 预加载x5内核
     */
    private void preinitX5WebCore() {
        if (!QbSdk.isTbsCoreInited()){
            // 这个函数内是异步执行所以不会阻塞 App 主线程,这个函数内是轻量级执行所以对 App 启动性能没有影响
            QbSdk.initX5Environment(this, null);
        }
    }
}

别忘记在清单配置文件中声明该Application:

<application
        android:name=".application.MyApplication"
        android:largeHeap="true"
        android:allowBackup="true"
        android:icon="@mipmap/app_icon"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".activity.SplashActivity"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".activity.MainActivity"
            android:windowSoftInputMode="stateHidden|adjustResize"
            android:configChanges="screenSize|orientation|keyboardHidden"
            android:screenOrientation="portrait"/>

    </application>

设置name属性即可。

2) 目前,由于SDK WebView所提供的WebView类,是对系统WebView的聚合包装,所以:获取系统内核的WebView或者 x5内核的WebView的宽高

android.webkit.WebView webView = new android.webkit.WebView(this);
int width = webView.getWidth();

需要采用下面的方式进行

com.tencent.smtt.sdk.WebView webView = new com.tencent.smtt.sdk.WebView(this);
int width = webView.getView().getWidth();

调整cookie的使用:

com.tencent.smtt.sdk.CookieManager和com.tencent.smtt.sdk.CookieSyncManager的相关接口的调用,在接入SDK后,需要放到创建X5的WebView之后(也就是X5内核加载完成)进行;否则,cookie的相关操作只能影响系统内核。

兼容视频播放:

1)享受页面视频的完整播放体验需要做如下声明:

页面的Activity需要声明android:configChanges="orientation|screenSize|keyboardHidden"

2)视频为了避免闪屏和透明问题,需要如下设置

a)网页中的视频,上屏幕的时候,可能出现闪烁的情况,需要如下设置:Activity在onCreate时需要设置:

getWindow().setFormat(PixelFormat.TRANSLUCENT);(这个对宿主没什么影响,建议声明)

b)在非硬绘手机和声明需要controller的网页上,视频切换全屏和全屏切换回页面内会出现视频窗口透明问题,需要如下设置

声明当前<item name="android:windowIsTranslucent">false为不透明。
特别说明:这个视各app情况所需,不强制需求,如果声明了,对体验更有利

c)以下接口禁止(直接或反射)调用,避免视频画面无法显示:

webview.setLayerType()
webview.setDrawingCacheEnabled(true);

输入法设置

避免输入法界面弹出后遮挡输入光标的问题

方法一:在AndroidManifest.xml中设置

android:windowSoftInputMode="stateHidden|adjustResize"

方法二:在代码中动态设置:

getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);

app 自定义 UA 的说明

如果 app 需要自定义 UA,建议采取在 SDK 默认UA 后追加 app UA 的方式示例:

webSetting.setUserAgentString(webSetting.getUserAgentString() + APP_NAME_UA);
其中 APP_NAME_UA 是 app 自定义 UA

由于我们提供的 TBS jar 已经混淆过,所以 App 混淆时可以不再混淆我们的 TBS jar


代码实现

下面我给出我自己的项目中使用x5内核加载的网页,顺便也测试一下是否成功的加载了x5内核。

package com.example.administrator.firststeppro.fragment;


import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.app.Fragment;
import android.support.annotation.NonNull;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.tencent.smtt.export.external.extension.interfaces.IX5WebViewExtension;
import com.tencent.smtt.export.external.interfaces.SslError;
import com.tencent.smtt.export.external.interfaces.SslErrorHandler;
import com.tencent.smtt.sdk.WebChromeClient;

import com.tencent.smtt.export.external.interfaces.WebResourceResponse;
import com.tencent.smtt.sdk.WebSettings;
import com.tencent.smtt.sdk.WebView;
import com.tencent.smtt.sdk.WebViewClient;
import android.widget.LinearLayout;
import android.widget.Toast;

import com.example.administrator.firststeppro.R;
import com.example.administrator.firststeppro.base.BaseFragment;
import com.example.administrator.firststeppro.orm.AndroidToJs;
import com.example.administrator.firststeppro.utils.LogUtil;
import com.example.administrator.firststeppro.utils.ToastUtil;


import butterknife.BindView;
import butterknife.ButterKnife;
import me.zhanghai.android.materialprogressbar.MaterialProgressBar;

/**
 * A simple {@link Fragment} subclass.
 * 展示个人的csdn博客主页
 */
public class PageOneFragment extends BaseFragment {

    @BindView(R.id.linear_page1_webview)LinearLayout linear_page1_webiew;// webView的父布局
    @BindView(R.id.material_progress_bar)MaterialProgressBar material_progress_bar;

    private boolean isPrepared = false;// 是否已经准备好要加载数据
    private static WebView webView = null;
    private WebSettings webSettings;// WebSettings对象

    public PageOneFragment() {
        // Required empty public constructor
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_page_one, container, false);
        getActivity().getWindow().setFormat(PixelFormat.TRANSLUCENT);// 加载网页视频避免闪屏和透明
        ButterKnife.bind(this, view);

        isPrepared = true;

        setLazyLoad();

        requestPermission();// 申请权限

        return view;
    }

    @SuppressLint("NewApi")
    private void requestPermission() {
        if(getContext().checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
            // 进行授权
            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode){
            case 1:
                if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){

                }else {
                    ToastUtil.showToast(getContext(), "读写文件权限未开启");
                }
                break;
        }
    }


    /*
    @OnClick({R.id.btn_callJs})
    public void doClick(View view){
        switch (view.getId()){
            case R.id.btn_callJs:// Android调用js方法
                webView.post(new Runnable() {
                    @Override
                    public void run() {
                        // 调用index.html文件的callJS()方法
//                        webView.loadUrl("javascript:callJS()");

                        // 不同版本兼容4.4以上
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            webView.evaluateJavascript("index:callJS()", new ValueCallback<String>() {
                                @Override
                                public void onReceiveValue(String value) {
                                    // 此处为js返回的结果
                                    LogUtil.e("TAG","" + value);
                                }
                            });
                        }else {
                            webView.loadUrl("index:callJS()");
                        }

                    }
                });
                break;
        }
    }
    */

    @Override
    protected void setLazyLoad() {
        super.setLazyLoad();
        if (isVisible && isPrepared){
            LogUtil.e("TAG","loadDataPage1");
            if (webView == null){
                openWebViewPage();
            }
        }
    }

    /**
     * 打开webView页面
     */
    private void openWebViewPage() {
        // 创建WebView对象添加到布局中
        webView = new WebView(getContext().getApplicationContext());

        IX5WebViewExtension ix5WebViewExtension = webView.getX5WebViewExtension();
        ix5WebViewExtension.setScrollBarFadingEnabled(false);

        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        webView.setLayoutParams(params);
        linear_page1_webiew.addView(webView);

        // 清除网页访问留下的缓存
        // 由于内核缓存是全局的因此这个方法不仅仅针对webView而是针对整个应用程序
        webView.clearCache(true);

        // 清除当前webView的访问历史记录
        webView.clearHistory();

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

        // https://www.panda.tv/
        // https://blog.csdn.net/csdnzouqi
        // http://soft.imtt.qq.com/browser/tes/feedback.html
//        String url = "file:////android_asset/index.html";// 加载主页面的url
        String url = "http://soft.imtt.qq.com/browser/tes/feedback.html";// 邹奇的博客主页


        // 获取WebSettings对象
        webSettings = webView.getSettings();
        // 特别注意:5.1以上默认禁止了https和http混用。下面代码是开启
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {// 21

        }
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);// 不使用缓存,直接用网络加载
        webSettings.setJavaScriptEnabled(true);// webView支持javascript
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);// 告诉js可以自动打开window

        // 两者一起使用,可以让html页面加载显示适应手机的屏幕大小
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);

        webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放
//        webSettings.setAllowFileAccess(true); //设置可以访问文件
        webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
        webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式

        // 即允许在 File 域下执行任意 JavaScript 代码
//        webSettings.setAllowFileAccess(true);// 设置是否允许 WebView 使用 File 协议
        // 禁止 file 协议加载 JavaScript
//        if (url.startsWith("file://")){
//            webSettings.setJavaScriptEnabled(false);
//        }else {
//            webSettings.setJavaScriptEnabled(true);
//        }

        webSettings.setSavePassword(false);// 关闭密码保存提醒;该方法在以后的版本中该方法将不被支持

        webSettings.setDomStorageEnabled(true);// 设置支持DOM storage API

        // 通过addJavascriptInterface()将Java对象映射到JS对象
        webView.addJavascriptInterface(new AndroidToJs(getContext()), "androidObj");

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

        // 加载html页面的一小段内容 ('#','%', '\', '?' 分别用 %23, %25, %27, %3f 替换)
//        webView.loadData("显示内容", "text/html", "utf-8");

        //设置WebViewClient类
        webView.setWebViewClient(new WebViewClient(){

            // 设置不用系统浏览器打开,直接显示在当前 webview
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // 如果不是http或者https开头的url,那么使用手机自带的浏览器打开
                if (!url.startsWith("http://") && !url.startsWith("https://")){
                    try {
                        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                        startActivity(intent);
                        return true;
                    }catch (Exception e){
                        e.printStackTrace();
                        return true;
                    }
                }
                view.loadUrl(url);
                return false;
//                return super.shouldOverrideUrlLoading(view, url);
            }

            @Override
            public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
                super.onReceivedSslError(webView, sslErrorHandler, sslError);
            }
        });


        //设置WebChromeClient类

        webView.setWebChromeClient(new WebChromeClient(){
            @Override
            public void onProgressChanged(WebView view, int newProgress) {

                // 加载完成,隐藏进度条
                if (newProgress == 100){
                    material_progress_bar.setVisibility(View.GONE);
                }else {
                    material_progress_bar.setVisibility(View.VISIBLE);
                }

            }

            //            // 拦截js的警告框
//
//            @Override
//            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
//
////                ToastUtil.showToast(getContext(), "拦截js alert");
//                new AlertDialog.Builder(getContext())
//                        .setTitle("弹框")
//                        .setMessage(message)
//                        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
//                            @Override
//                            public void onClick(DialogInterface dialog, int which) {
//                                result.confirm();
//                            }
//                        })
//                        .setCancelable(false)
//                        .create().show();
//
//                return true;
////                return super.onJsAlert(view, url, message, result);
//            }
//
//            // 拦截js的确认框
//            @Override
//            public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
//                return super.onJsConfirm(view, url, message, result);
//            }
//
//            // 拦截输入框
//            @Override
//            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//
////                ToastUtil.showToast(getContext(), ""+ message);
//                // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
//                // 假定传入进来的 url = "js://android?arg1=1&arg2=2"(同时也是约定好的需要拦截的)
//                Uri uri = Uri.parse(message);
//
//                if (uri.getScheme().equals("js")){
//
//                    if (uri.getAuthority().equals("android")){
//                        ToastUtil.showToast(getContext(),"Android端拦截成功!");
//
//                        // 可以在协议上带有参数并传递到Android上
////                        HashMap<String, String> params = new HashMap<>();
////                        Set<String> collection = uri.getQueryParameterNames();
//
//                        // 这里可以执行js所需要调用的逻辑
//
//                        // 返回值
//                        result.confirm("Android端返回给js的内容");
//                    }
//
//                }
//
//                return true;
////                return super.onJsPrompt(view, url, message, defaultValue, result);
//            }
        });

        // 加载apk包中的html页面
        webView.loadUrl(url);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        isPrepared = false;// 视图销毁的时候恢复数据加载状态
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (!hidden){
            LogUtil.e("TAG", "onHiddenChanged1");
        }
    }


    // 先让 WebView 加载null内容,然后移除 WebView,再销毁 WebView,最后置空
    private void clearWebView() {
        if (webView != null){
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            webView.clearHistory();
            webView.clearCache(true);// 清除缓存

            ((ViewGroup)webView.getParent()).removeView(webView);
            webView.destroy();
            webView = null;
        }
    }

    /**
     * fragment的点击事件,参数由当前fragment所依附的activity传过来
     * @param keyCode
     * @param event
     * @param context
     */
    public static void onKeyDown(int keyCode, KeyEvent event, Context context) {
        if (webView != null){
            if (webView.canGoBack()){
                webView.goBack();
            }else {
                view_toast_exit = LayoutInflater.from(context).inflate(R.layout.view_toast_exit, null);
                exitApp(2000, context);
            }
        }
    }

    private static Toast toast = null;// 创建Toast对象
    private static long firstTime;// 记录点击返回时第一次的时间毫秒值
    private static View view_toast_exit;// 吐丝,退出应用的view

    /**
     * 退出应用
     * @param timeInterval 设置第二次点击退出的时间间隔
     * @param context
     */
    private static void exitApp(long timeInterval, Context context) {
        if(System.currentTimeMillis() - firstTime >= timeInterval){
            if (view_toast_exit != null){
                toast = new Toast(context);
                toast.setView(view_toast_exit);
                toast.setGravity(Gravity.CENTER, 0, 0);
                toast.setDuration(Toast.LENGTH_SHORT);
                toast.show();
            }else {
                ToastUtil.showToast(context, "再按一次退出程序");
            }

//            ToastUtil.showToast(this, "再按一次退出程序");
            firstTime = System.currentTimeMillis();
        }else {
            if (toast != null){
                toast.cancel();
            }
//            finish();// 销毁当前activity
            System.exit(0);// 完全退出应用
        }
    }
}

那么怎么判断是否加载了x5内核呢?

  • webview的getX5WebViewExtension()返回非null表示已加载了x5内核webview

  • 您的app打开网页http://soft.imtt.qq.com/browser/tes/feedback.html,显示000000表示加载的是系统内核,显示大于零的数字表示加载了x5内核(该数字是x5内核版本号)

上面我加载的url就是http://soft.imtt.qq.com/browser/tes/feedback.html,让我们运行一下看看效果:

这里写图片描述

可以看到确实是成功的加载了x5的内核。


A little bit of progress every day!Come on!

猜你喜欢

转载自blog.csdn.net/csdnzouqi/article/details/80902110
今日推荐