Android中的WebView详解

姓名:徐鑫

学号:1501210748

WebView简介

Webview是一款基于Android webkit内核的浏览器行为框架。在Android SDK中叫做Webview,属于常用组建的一种。Webview的作用是加载一些html页面的信息。 出于两点考虑我们要使用webview,一个是兼容已有的项目。比如说,淘宝网已经开发搭建好了网页平台,现在想要把它在移动端展示出来,如果重新开发就十分消耗人力物力,可以将已经建好的web页面信息直接在本地浏览器打开就可以了。第二点是webview可以随时的更新文本信息,也就是服务端开发的好处。一旦服务端发现有bug,可以随时地上线,这样客户端就可以马上展现出来,不会看出任何bug。但是平常开发android应用是非常苦恼的,一旦发现bug就不能修复了,只能在下一个版本进行修改。 实现一个最简单的webview的小例子如下所示: 在Manifest中添加访问网络的权限:

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

在MainActivity中初始化webview控件,调用loadUrl方法指定将要访问的网络url:

package pku.ss.xuxin.web1;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.webkit.WebView;

public class MainActivity extends AppCompatActivity {


    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView= (WebView) findViewById(R.id.webView);
        webView.loadUrl("http://www.pku.edu.cn/");
    }
    }

运行这个小程序,发现在加载activity_main 的时候会自动跳转到系统默认的浏览器打开访问网站,这是因为loadUrl是使用默认浏览器的方式打开,如果想要在app界面中显示链接到的地址,就要重写webView方法


webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public void onReceivedTitle(WebView view, String title) {
        super.onReceivedTitle(view, title);
    }
});


webView.setWebViewClient(new WebViewClient(){
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {

        view.loadUrl(url);
        return super.shouldOverrideUrlLoading(view, url);
    }
});

通过view.loadUrl(url);让其在项目中加载url而不使用浏览器。

自定义WebView的title

我们都知道,一个网页是有一个title的描述的,为了把一个网页的title显示给客户端,需要在客户端做处理信息

为了在在布局文件中添加一个顶部的title,在activity_main中添加布局结构:

<RelativeLayout
    android:id="@+id/web_title_layout"
    android:layout_width="match_parent"
    android:layout_height="50dp">
<Button
    android:id="@+id/back"
    android:layout_alignParentLeft="true"
    android:layout_width="wrap_content"
    android:layout_height="40dp"
    android:text="返回"/>

    <Button
        android:id="@+id/refresh"
        android:layout_alignParentRight="true"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:text="刷新"/>

    <TextView
        android:id="@+id/title"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

初始化组件:

private Button refresh;
private Button back;
private TextView title;

添加id:

back= (Button) findViewById(R.id.back);
refresh= (Button) findViewById(R.id.refresh);
title= (TextView) findViewById(R.id.textView);

给两个按钮添加监听事件:

class MyListener implements View.OnClickListener{

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.refresh:
                webView.reload();
            case R.id.back:
                finish();
                break;
            default:
                break;
        }
        }
    }

refresh.setOnClickListener(new MyListener());
back.setOnClickListener(new MyListener());

重写onReceivedTitle方法

@Override
    public void onReceivedTitle(WebView view, String title) {
        titleview.setText(title);
        super.onReceivedTitle(view, title);
    }
});

我们可以看到,在加载页面的时候,百度页面的title信息已经显示在页面最上端:北京大学,点击刷新按钮可以重载webview页面,点击返回会结束该activity

使用WebView下载文件

我们平常使用浏览器搜索文件,在访问过程中可以下载自己所需要的文件,同样在使用webview时也可以下载文件,首先实例化一个接口 该接口的目的主要是通过该接口我们可以拿到当前浏览器的Url信息,通过这些信息去下载文件、MP3、图片等都可以。

通过

public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength)

方法可以拿到url信息,通过它就能把文件下载到本地

我们需要从网络下载文件必须要创建一个网络线程,此时需要创建一个HttpThread 重写run方法,在run方法中执行下载的操作

通过一个构造方法把url信息传入,否则无法拿到就不能进行下载

private String mUrl;
public HttpThread(String url){
    this.mUrl=url;
}

通过网络创建连接:

URL httpUrl=new URL(mUrl);

接受输入流,发送输出流:

HttpURLConnection conn= (HttpURLConnection) httpUrl.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);

拿到url后,通过它实现下载的方式是重中之重,将要下载的文件会存放在SD卡上,这就需要在程序中判断SD卡是否存在

if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){}

拿到外置的存储目录:

downloadFile=Environment.getExternalStorageDirectory();

文件写入SD卡的具体目录设置:

File sdFile=new File(downloadFile,"test.apk");

至此,文件已经创建好了。接下来通过http得到这个流: 首先建一个6k的缓存,

byte[] b=new byte[6*1024];

数据流的位置信息需要读出,使用len记录当前读到的内容在当前字节是多大一个长度。-1是标志一个流终止的标志

while ((len=in.read(b))!=-1)

得到下载地址地流

InputStream in=conn.getInputStream();

同样要创建输出流:

FileOutputStream out;
out=new FileOutputStream(sdFile);

判断如果该流数据为空的情况下,就往这个流中去写入相关的数据,调用其write方法,写入缓存中,从0开始写,写到目前读到的位置上

while ((len=in.read(b))!=-1){
    if(out!=null){
        out.write(b,0,len);
    }

完事之后要关闭输入流与输出流

if(out!=null){
    out.close();
}
if (in!=null){
    in.close();
}

这样,下载流的线程就全部写好了。 接下来,在MainActivity中new一个线程对象,将url传入

new HttpThread(url).start();

设置监听方法:

webView.setDownloadListener(new MyDownload());

判断如果该url的结尾是以“.apk”结尾的话,就下载这个文件

System.out.println("url---------->" + url);
if(url.endsWith(".apk")){
    new HttpThread(url).start();
}

可以查看结果,文件已经开始正常下载:

其实,调用系统的方式去下载会更加方便,无需自己再写:

Uri uri=Uri.parse(url);
Intent intent=new Intent(Intent.ACTION_VIEW);
startActivity(intent);

这样的下载方式会将下载进城显示在notification中,第二种方式比自己写的方法要快捷多了。

WebView对错误码的处理

有的时候我们通过网络去访问一个html页面,但是如果突然间没有网络了,webview会导入一个错误页面,这个页面是一个系统默认的界面。一般通过两种策略处理这种错误情况,一种是加载一个本地html的错误页面,一种是纯native写一个错误页面 在setWebViewClient中重写onReceivedError方法,该方法只是回调在webview出现错误的时候,通过webview加载一个本地的url,在街面上显示404消息告知网络连接错误。

@Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        super.onReceivedError(view, errorCode, description, failingUrl);
        view.loadUrl("file:///android_asset/error.html");
    }
});

还有一种方式是通过native布局去展示一个错误页面的布局,首先加入一个textview布局

<TextView
    android:id="@+id/text_error"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignTop="@+id/webView" />

初始化

private TextView mTextView_Error;
mTextView_Error= (TextView) findViewById(R.id.text_error);

同样在setWebViewClient中重写onReceivedError方法,通过setText的方法显示出自己创建的页面文字。然后把webview隐藏掉,使用setVisibility的方法。

@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    super.onReceivedError(view, errorCode, description, failingUrl);
  //  view.loadUrl("file:///android_asset/error.html");
    mTextView_Error.setText("404 error");
    webView.setVisibility(View.GONE);
}

这样一来,本地的错误页就已经展示出来了。

总体来说,WebView对错误码的处理有以下几种方式:

1.加载本地assert目录下文件(error.html) webcontent.loadUrl(" file:///android_asset/error.html ");

2.加载网络url(http:// www.pku.edu.cn) webcontent.loadUrl(" http://www.pku.edu.cn ");

3.加载 String 类型html

String
errorHtml = "<html><body><h1>Page not find!</h1></body></html>";
webcontent.loadData(errorHtml,
 "text/html", "UTF-8");

4.加载SD卡html:

webcontent.loadUrl(" content://com.android.htmlfileprovider/sdcard/kris.html ");

WebView如何同步Cookie

在我们平常开发的过程中,经常会有这样的需求,客户端在用户登陆完之后会保存一个cookie信息,有些界面需要通过WebView去展示,这种情况下只需要将登录信息的cookie传给服务器,让服务器做一个标识,避免我们再次登录的情况。如果不把默认的cookie传给服务器,服务器就会让用户重新登陆,这样让用户在登陆完之后还要重新登陆会降低用户体验。为了解决此类问题,就需要实现客户端和WebView同步。Cookie是服务器端给存储客户端的一些信息,例如用户登录的时间、购物车等都是通过Cookie的方式去存储的。 在本地创建一个index网页,可以模拟登陆操作:

首先搭建一个取Cookie的接口类,继承一个Thread,该类的作用主要是模拟登陆

public class HttpCookie extends Thread{
    @Override
    public void run() {
        HttpClient client=new DefaultHttpClient();
        HttpPost post=new HttpPost("");
        List<NameValuePair>list=new ArrayList<NameValuePair>();
        list.add(new BasicNameValuePair("name", "nates"));
        list.add(new BasicNameValuePair("age", "12"));
        try{
            post.setEntity(new UrlEncodedFormEntity(list));
            HttpResponse response=client.execute(post);
            if(response.getStatusLine().getStatusCode()==200){
                AbstractHttpClient abstractHttpClient=(AbstractHttpClient) client;
                List<Cookie> cookie=abstractHttpClient.getCookieStore().getCookies();
                for(Cookie cookie:cookies){
                    System.out.println("name"+cookie.getName()+"age="+cookie.getValue());
                }
            }
        }catch (UnsupportedEncodingException e){
            e.printStackTrace();
        }catch (ClientProtocolExcention e){
            e.printStackTrace();
        }

    }
}

接下来要在主程序中调用,取出cookie,然后考虑和webview进行通信

new HttpCookie().start();

为了和主程序进行通信,在主程序建立Handler,并构建构造函数,通过for(Cookie cookie:cookies)拿到一个Cookie,然后把这个Cookie返还给主线程,让主线程拿到cookie之后接受消息,直接通过WebView去加载,并把cookie设置上。

for(Cookie cookie:cookies){
    System.out.println("name"+cookie.getName()+"age="+cookie.getValue());

    Message message=new Message();
    System.out.println("cookie---------------"+cookie);
    message.obj=cookie;
    return;
}

主线程的Handler负责处理handleMessage方法

public void handleMessage(android.os.Message msg){
    String cookie=(String)msg.obj;
    CookieSyncManager.createInstance(MainActivity.this);
    CookieManager cookieManager=CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    cookieManager.setCookie("", cookie);
    CookieSyncManager.getInstance().sync();
}

如果想和Web进行同步,就以这种方法进行实现。实现实现的免登录界面状态如下:

WebView与javascript调用混淆

在平常开发时,WebView与javascript是可以相互调用的,但是我们把一个apk正式发布的时候,需要打一个release包,release包的作用就是把代码做混淆,但是如果一旦混淆之后,如果不加保护,webview与javascript就无法互相调用的。比如说,在javascript中调用一些本地的方法,但是一旦打了混淆包后,没有保护措施的话会发现调用的方法不起作用。下面通过具体代码说明这一问题:webview与javascript出了问题。 首先新建一个类映射javascript对象类,javascript调用类如下:

public class WebHost {
    public Context mContext;

    public WebHost(Context context){
        this.mContext=context;
    }

    public  void callJ
s(){
        Toast.makeText(mContext,"call from js",Toast.LENGTH_LONG).show();
    }
}

接下来在MainActivity中进行映射,把本地的java对象映射给javascript调用

webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new WebHost(this),"js");

在本地进行映射之后,还需要到web页面进行一个调用,在本地web页面创建一个button,调用call方法,call方法就是javascript的方法

调用js的callJs方法

fuction call(){
    js.callJs();
}

已经调用成功,说明javascript已经能够调用本地的callJs。 接下来叙述混淆包的问题,需要把演示事例项目webView_01整个项目打成一个混淆包,在cmd中执行命令ant clean relase,把之前的文件进行清空,

然后执行ant clean release就打成了一个release包

找到对应的.apk release文件拖入虚拟机运行,可以看到,再点击calljava按钮的时候就不会打印Toast那句话了,这就是因为在混淆之后JS方法不能调用的问题,如果这样进行发布apk版本的话肯定是非常不合适的。

其实解决方法也是非常简单的,就是在混淆文件当中把JS调用的方法不混淆就可以了。打开res目录下的proguard.cfg文件,加上以下代码:

-keep public class pku.ss.xuxin.webview_01.WebHost{
 public<methods>; 
}

本段代码意思是保证这个包名下的类都不被混淆,通过这种方法再次打混淆包之后,这些方法就不再被混淆,js就可以找到这些方法正常调用。重新打混淆包之后进行测试 发现Toast可以进行正常打印了,这样就可以给javascript进行调用了。如果在平常开发中遇到webview与javascript调用进行混淆的时候,只需要在proguard文件中配置一下给javascript调用的java类不被混淆就可以了。

WebView导致远程注入

WebView导致远程注入问题是一个很严峻的问题,某些情况下在某些版本的浏览器上去加载一些恶意的代码,可以拿到一些我们手机的信息,那么这样通过一个web页面就能拿到我们手机信息,通过执行恶意的命令拿到我们手机中的比如说联系人、通话、照片等信息,这样的情况是可以避免的。下面通过一个例子来说明webview是如何远程注入的 在本地有一个创建好的index.jsp文件,其中代码主要是遍历js的对象,拿到一个getclass,这是一个我们映射给js的java对象,然后通过反射区加载一个runtime类,去调用一些执行linux命令的方法。execute主要是列取当前sd卡的目录信息(想要改写execute也可以,比如说删除手机信息、给手机发送文件等等都是可以做到的):

通过浏览器执行这个页面,单机回车,就会在本地扫出sd的目录,最上方框出的红色字体就是导致远程泄露的对象,拿到这个对象之后就可以执行反射的方法去执行linux命令。

在本地查找文件log-netstat.txt文件,是简单的ls一下sd卡里的文件夹。如下图所示:

这种情况是非常危险的,可以查看sd卡的目录也就说明可以进行很多其他的操作,给用户带来巨大的安全隐患。那么Google也已经意识到这样的问题了,在4.2版本之后,这个bug就已经修复了。为了验证,启动一个4.2以后的版本模拟器,还是运行之前的页面,回车后可以看到一下页面:

说明这个对象已经没有被定义了,说明在4.2版本之后,就已经修复了这个bug,无法再通过这个bug拿取用户的信息了。文件系统中也没有发现log-netstat.txt文件,说明已经解决了该漏洞。 出现远程注入问题可以通过以下方式去解决:

1、 浏览器厂商已经解决

2、 开发人员要避免该问题,就应该减少javascript的调用。

把所有javascript与webview的进行调用都通过自定义协议的方式去处理。也就是在本地根据一个url去定义一些规则,通过这些规则去调用本地客户端,间接的去避免这种直接通过webview调用javascript。

WebView自定义协议拦截

通常来说,一般客户端人员需要跟前端人员进行协议上的约束,去定义一个协议去给js打开一个本地客户端,这个协议的定义方式有很多种,可以任意去定义,客户端只需要在截获url信息,根据参数信息取出相应的标识去打开本地的页面。下面是通过一个实例来解析: 在本地的index.jsp文件中添加一个超链接,告诉本地区打开一个activity,在本地客户端去解析这个url就可以了:

<a href=http://192.168.103.69:8080/webs/error.html?startActivity”> load page</a>

创建一个新的SecondActivity,并加载新的布局文件。

public class SecondActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.id.second);
    }
}

在MainActivity中重写shouldOverrideUrlLoading方法,判断如果以?startActivity结尾的话就跳转SecondActivity:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if(url.endsWith("?startActivity")){
        Intent intent=new Intent(MainActivity.this.SecondActivity.class);
        startActivity(intent);
        return true;
    }
    view.loadUrl(url);
    return super.shouldOverrideUrlLoading(view, url);
}

可以看到点击load page之后会打开secondActivity,证明自定义协议的例子已经成功了。 其实自定义协议主要用于约束于前端人员与客户端人员的通信方式,至于协议具体定义成什么样可以根据实际需求去制定。

results matching ""

    No results matching ""