android webview与H5混合开发,webview自定义缓存

公司开发的一个项目为Android+H5混合开发,虽然开发量对于原生来说不大,但是针对于H5与android结合的各种坑让初次接手这种开发模式的新手简直不可描述可怜,同样的功能在iOS上面运行的好好的,在Android上面各种问题,这种适配简直烦到极点,真的想竖起中指对着天空大喊一声   太阳你大爷。。。发火废话就不说了,下面记录开发中遇到的一些问题,新手第一次写博文,写的不好不对的地方敬请指教,谢谢!

附上webview缓存demo下载地址---------->>>>>>demo地址

一· 混合开发

 


混合开发中,原生所做的工作很简单,在布局方便,逻辑方面几乎不做什么,只有在H5实现麻烦的时候,或者android特定的时候再去做一些逻辑的判断,主要解决的就是一些H5的交互,混合开发中的BUG之类的。混合开发现在越来越多,其好处就是跨平台,维护方便,在IOS与android两种平台中都可以顺利运行,维护方便,大大降低了开发成本,但是如果细节处理不到位,体验就会很差,下面看细文。

 

 

 

 

二. 项目大体流程

 


1.布局方面,以下是我的项目布局,可以看出几乎没什么东西,一个简单的imageview,用来设置APP的启动页,一个webview用来展示H5。当然如果更简单的话,xml里面一个单纯的linearlayout嵌套一个webview就可以了。

 

 

 

 

<cn.microdone.geemall.views.MyTwoScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="fill_parent"
    android:id="@+id/main_scroll"
    android:scrollbars="none"
    android:fillViewport="true"
    android:fitsSystemWindows="true"
    >


<LinearLayout
    android:id="@+id/main_relative"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">4
    <ImageView
        android:id="@+id/wel_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/begin2x"
        android:visibility="visible"
        />
    <cn.microdone.geemall.views.WebViewMod
        android:id="@+id/activity_loading_webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        android:nextFocusDown="@+id/activity_loading_webView1">
    </cn.microdone.geemall.views.WebViewMod>
</LinearLayout>
</cn.microdone.geemall.views.MyTwoScrollView>

 

 

三.activity方面

 


因为是混合开发,所以在activity中代码也不繁琐。

 

 

 

package cn.Arthur.Test.WebTest;


import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.KeyEvent;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;


public class MainActivity extends Activity {


private WebView webView;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = (WebView) findViewById(R.id.webView);
//支持Js
webView.getSettings().setJavaScriptEnabled(true);
//设置是否开启DOM存储API权限,默认false,未开启,设置为true,WebView能够使用DOM storage API
webView.getSettings().setDomStorageEnabled(true);
//设置默认编码
webView.getSettings().setDefaultTextEncodingName("UTF-8");
//开启数据库功能
webView.getSettings().setDatabaseEnabled(true);
//开启web缓存功能
webView.getSettings().setAppCacheEnabled(true);
// 设置缓冲大小,我设的是8M
webView.getSettings().setAppCacheMaxSize(1024 * 1024 * 8);
//设置数据库db文件目录
webView.getSettings().setDatabasePath(extStorageAppBasePath.toString()); // API 19 deprecated
//设置缓存路径
webView.getSettings().setAppCachePath(extStorageAppCachePath.toString());
//设置在WebView内部是否允许访问文件,默认允许访问。
webView.getSettings().setAllowFileAccess(true);
//设置缓存模式
webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
//设置WebView是否使用viewport,当该属性被设置为false时,加载页面的宽度总是适应WebView控件宽度;
//当被设置为true,当前页面包含viewport属性标签,在标签中指定宽度值生效,如果页面不包含viewport标签,
//无法提供一个宽度值,这个时候该方法将被使用。
webView.getSettings().setUseWideViewPort(true);
//必须设置setWebviewclient  自定义的继承于webviewclient的类就是用来拦截url处理一些与H5交互的时候的逻辑
        webView.setWebViewClient(new MyWebViewClient());
}


//监听手机物理返回键并设置
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
//webview调用goback后会返回到上一次的浏览页面,跟PC端浏览器的后腿是一样的道理
webView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}



//自定义的webviewclient,用来拦截url处理一些与H5交互的时候的逻辑等
class MyWebViewClient extends WebViewClient {

//此方法可以使H5在webview中显示,而不是手机浏览器
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}

//访问url开始时候触发
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}

//访问url结束时候触发
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}

//重写该方法可以加载一些手机本地资源等,文章下面会对该方法有详细介绍
@Override
public WebResourceResponse shouldInterceptRequest(WebView view,
String url) {
// TODO Auto-generated method stub
return super.shouldInterceptRequest(view, url);
}

//与上方法一样,以下会有介绍
@Override
   public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
       return super.shouldInterceptRequest(view, request);
   }
}


}





四. webview的各种设置


   在这种混合开发中,必定用的就是webview来加载H5的页面(这里推荐腾讯的X5,可以自己在百度上搜,个人感觉封装的很齐全,集成也简单,关键是jar包小到不可思议,只有区区的200多K),既然是混合开发,那么注定与H5的交互就不可避免了,当然也需要设置各种属性来支持webview,除了针对于webview的设置外,我们还需要获取websetting这个类来帮助我们进行一些辅助设置,很必要哈。。。

 

//支持JS,此项必不可少
webSetting.setJavaScriptEnabled(true);
//1.网上说是设置此选项提高渲染的优先级,
webSetting.setRenderPriority(WebSettings.RenderPriority.HIGH);
//2.首先阻塞图片,让图片不显示
webSetting..setBlockNetworkImage(true);
 //3.页面加载好以后,在放开图片:
webSetting.setBlockNetworkImage(false);
// 设置缓存模式(下面会详细介绍缓存)
webSetting.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// 开启 DOM storage API 功能
webSetting.setDomStorageEnabled(true);
  webSetting.setAllowFileAccess(true);
//开启 database storage API 功能
webSetting.setDatabaseEnabled(true);
//开启 Application Caches 功能
webSetting.setAppCacheEnabled(true);
//设置db文件缓存路径,但是在API4.4之后webview内核改为chrome后发现并没有什么卵用,4.4之前的确是有缓存的db文件,亲测
webSetting.setDatabasePath(MyApplication.extStorageAppDatabase.toString());
//设置加载的缓存路径,4.4之后在制定路径后,的确有缓存文件
webSetting.setAppCachePath(myApplication.getCacheDir().toString());
//此设置是否保存H5表单数据,发现一个蛋疼的问题,在小米手机上当H5 input框设置为search后,当点击input框的时候//竟然会有历史的搜索记录,而且样式十分难看,设置此属性可以取消历史搜索记录
webSetting.setSaveFormData(false);
//webviewClient类设置webview,重写该类可以通过其中的方法设置你想要的信息,以下会有详细记录
web.setWebViewClient(new MyWebViewClient(this));
//同上
web.setWebChromeClient(new MyWebChromeClient());
//此设置为添加JS对象,以下会有详细解释
web.addJavascriptInterface(new setApp(), "setting");

 

 

五.详细介绍web.addJavaScriptInterface()此方法

 


在混合开发中,免不了与H5进行通信,传值使APP运行,此方法可以使H5的js与原生进行通信,进行传值的操作,
首先,我们需要声明一个
    web.addJavascriptInterface(new TrueName(MainActivity.this),"personName");
括号中,第一个参数是一个类,后面是这个类所对应的对象,如上方,personName就是用来与H5进行交互的对象,这个对象是原生与H5进行商量好后敲定的,双方都需要使用,而TureName类如下方,这个类当初是我原生方面需要点击H5然后弹出自己的activity所做的,当然我的是对应的功能实现对应的类,在开发中,也可以只声明一个对象,然后对应的类中实现所有的与H5交互的方法,这样更简单,需要注意的是声明的发放必须带有@JavascriptInterface注解,否则不会生效,这个是有版本控制的,好像是在4.4之前的话是没这个注解的,具体的我也没有深究,这个注解很重要。

 

 

 

 

 

 

 /**
     * 与JS交互的弹出实名认证页面
     */
    public class TrueNameC {
        Context context;

        public TrueNameC(Context context) {
            this.context = context;

        }

        @JavascriptInterface
        public void trueName() {
            startActivity(new Intent(context, IdCardActivity.class));
        }
        /**
         * 跳转身份证页面
         * String flag就是H5给原生传的参数,H5在JS中写入personName.trueName("小明"),原生就可以接收到信息
         */
        @JavascriptInterface
        public void trueName(String flag) {
            Log.i("cpf", "flag====" + flag);
            Intent intent = new Intent();
            intent.setClass(MainActivity.this, IdCardActivity.class);
            intent.putExtra("infoFlag", flag);
            intent.putExtra("userId", ConStants.USERID);
            startActivity(intent);
        }
    }

 

 

 

 

 

 

六.webviewclient详解

 


在开发过程中,虽然为混合开发,但是不可能什么逻辑,都是H5来写的,原生总得处理一些逻辑,如url的截取,判断webview显示当前的url是你想要的url的时候做一些代码逻辑,这个也挺重要,此时就要用到webviewclient了,于此同时还有webchromeclient,就不在此赘述了。

 

 


在重写webviewclient中的方法中,有如下方法是常用的


1.shouldOverrideUrlLoading并不是每次都在onPageStarted之前开始调用的,就是说一个新的URL不是每次都经过shouldOverrideUrlLoading的,只有在调用webview.loadURL的时候才会调用。若此方法的返回值  return super.shouldOverrideUrlLoading(view, url);则会跳到手机浏览器,return false代表webview处理url是在webview内部执行的,return true代表webview处理url是根据程序来执行的。

 

2.OnReceivedError(),这个方法是加载出错的时候响应,因为我们可以用来加载出错页面


3.OnLoadResource(),这个方法是在加载网络资源的时候触发,无论是图片,json,还要是网络资源,都会触发此方法。


4.onPageStarted(),当webview开始加载url的时候,会进入这个方法。


5.OnPageFinished(),当url加载完成后,触发次方法,我的项目中就是用这个方法进行拦截的,举个例子,如我要判断当前url如果是index的话,我要进行一个toast,如下方,就可以触发toast。
 // 在加载页面完成后响应
        @Override
        public void onPageFinished(WebView view, final String url) {
            pdUrl = url.substring(url.lastIndexOf("/") + 1);
            if (pdUrl.equals("index")) {
            Toast.ma,keText(MainActivity.this, pdUrl + "~~", 0).show();
 } }


 6.shouldInterceptRequest(WebView view, WebResourceRequest request) ,这个方法我们可以用来加载一些本地资源,如混合开发中,基本所有的页面都是由H5进行实现的,而H5页面中的一些图片肯定也是对应的一个url,因此在此方法中,我们可以通过request拿到这个图片你的url,当判断是这个url的时候我们就可以加载本地资源,替换要加载的url,这样做不仅加快了响应速度,显示速度,而且省流量,也因此我们可以使用这个来做自定义缓存,文章下方会详细介绍。


 7.shouldInterceptRequest(WebView view, String url),这个方法与上方的方法基本没有什么不同,功能是一样的,String参数被后来的WebResourceRequest参数所替代,这样可以拿到请求是post还是get,header等诸多信息,区别就是这个方法是4.4之前的,而上面的方法是4.4之后的(好像是4.4)这个方法已经被废弃,但是楼主观察过,在执行顺序上是这个方法先执行,而当我们做自定义缓存的时候,4.4之前是没有上面那个方法的,为了适配,所以建议还是使用此方法。

七.webview自定义缓存

 


 重点来了,因为4.4之后,google更改了webview内核,使用chrome。而当我们在指定了APP的缓存目录后就可以发现由chrome缓存下来的文件,但是却无法控制,而且在我的项目中,由于H5框架的原因,我的webview取到缓存后断网情况下都无法正常显示,这就非常蛋疼了,因此结合网上资料自定义了缓存,在自定义缓存的过程中,我们需要判断当前的url指向的是图片,json,html,还是js,以下是代码,方便摘抄,先看我的webviewclient中的缓存方法,在判断url的后缀是那种类型后,设置文件的type,然后再交给缓存类DVDUrlCache。
 

 

@Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            //读取当前webview正准备加载URL资源
            if (url.indexOf("?") != -1) {
                url = url.substring(0, url.lastIndexOf("?"));
            }


            try {
                //根据资源url获取一个你要缓存到本地的文件名,一般是URL的MD5
                resFileName = getResourcesFileName(url);
                if (TextUtils.isEmpty(resFileName)) {
                    return null;
                }
                if (url.lastIndexOf("html") != -1) {
                    isType = "text/html";
                } else if (url.lastIndexOf("png") != -1) {
                    isType = "image/png";
                } else if (url.lastIndexOf("js") != -1) {
                    isType = "text/javascript";
                } else if (url.lastIndexOf("css") != -1) {
                    isType = "text/css";
                } else if (url.lastIndexOf("ico") != -1) {
                    isType = "image/ico";
                } else if (url.lastIndexOf("jpg") != -1) {
                    isType = "image/jpg";
                } else {
                    isType = "application/json";
                }
                if (isNetworkConnected(MainActivity.this)) {
                    if (url.indexOf("getgoodsSkuLists") != -1) {
                        url = url + "?pageNow=1&pageSize=1000";
                        dvdUrlCache.register(url, resFileName,
                                isType, "UTF-8", dvdUrlCache.ONE_DAY);
                        dvdUrlCache.load(url);
                        return null;
                    } else if (url.indexOf("getUserShoppingCar") != -1//购物车
                            ) {
                        return null;
                    } else if (url.indexOf("getGoodsSkuDetail") != -1) {//商品详情图片
                        return null;
                    } else if (url.indexOf("getIsSkuExist") != -1) {
                        return null;
                    }
                }
                //url包含以下这些路径的时候不对其进行缓存
                if (url.indexOf("getRandKey") != -1//密码键盘
                        || url.indexOf("getMappingAarry") != -1//密码键盘
                        || url.indexOf("getByParentIdAndType") != -1//新建地址
                        || url.indexOf("getIsentReadCount") != -1
                        || url.indexOf("svg") != -1//密码键盘资源
                        || url.indexOf("getUserShoppingCar") != -1//购物车
                        || url.indexOf("getuserinfo") != -1//获取用户是否认证
                        || url.indexOf("getGoodSkuProp") != -1//过去商品详
                        || url.indexOf("newsManagement") != -1
                        || url.indexOf("getUserMessages") != -1
                        || url.indexOf("getAllMessageType") != -1
                        ) {
                    return null;
                }


                //这里是处理本地缓存的URL,缓存到本地,或者已经缓存过的就直接返回而不去网络进行加载
                dvdUrlCache.register(url, resFileName,
                        isType, "UTF-8", dvdUrlCache.ONE_DAY);
                Log.i("whj", url + "====" + resFileName + "===" + isType);
                if (isNetworkConnected(MainActivity.this)) {
                    dvdUrlCache.load(url);
                    return null;
                } else {
                    return dvdUrlCache.load(url);
                }


            } catch (Exception e) {
            }
            return null;
        }
同时用到了一个转换MD5名字的方法,如下
public static String getResourcesFileName(String content) {
        byte[] hash;
        try {
            hash = MessageDigest.getInstance("MD5").digest(content.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("NoSuchAlgorithmException", e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UnsupportedEncodingException", e);
        }


        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) {
                hex.append("0");
            }
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString();
    }


接下来缓存所要使用到的类,用来缓存数据,保存下来


ThreadPoolManager类
import android.util.Log;


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;


/**
 * 线程池
 * <p>
 * Created by hongminghuangfu on 16/9/9.
 */
public class ThreadPoolManager {


    private static final String LOG_TAG = "ThreadPoolManager";
    private static final ThreadPoolManager instance = new ThreadPoolManager();


    private ExecutorService threadPool = Executors.newFixedThreadPool(100);


    public static ThreadPoolManager getInstance() {
        return instance;
    }


    /**
     * @param runnable 不返回执行结果的异步任务
     */
    public void addTask(Runnable runnable) {
        try {
            if (runnable != null) {
                threadPool.execute(runnable);
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "", e);
        }
    }


    /**
     * @param callback 异步任务
     * @return 你可以获取相应的执行结果
     */
    public FutureTask addTaskCallback(Callable<Boolean> callback) {
        if (callback == null) {
            return null;
        } else {
            FutureTask futureTask = new FutureTask<>(callback);
            threadPool.submit(futureTask);
            return futureTask;
        }


    }
// 这是一个demo,如果你看不懂,可以打开跑一下
//    public static void main(String args[]) {
//        FutureTask ft = ThreadPoolManager.getInstance().addTaskCallback(new Callable<Object>() {
//            @Override
//            public Object call() throws Exception {
//                int sum = 0;
//                for (int i = 0; i < 1000; i++) {
//                    sum++;
//                }
//                return sum;
//            }
//        });
//        try {
//            System.out.println("执行结果是:" + ft.get());
//        } catch (InterruptedException | ExecutionException e) {
//            e.printStackTrace();
//        }
//
//    }
}


DVDUrlCache类
public class DVDUrlCache {


    private static final String LOG_TAG = "DVDUrlCache";


    private static final long ONE_SECOND = 1000L;
    private static final long ONE_MINUTE = 60L * ONE_SECOND;
    public long ONE_HOUR = 60 * ONE_MINUTE;
    public long ONE_DAY = 24 * ONE_HOUR;
    public long ONE_MONTH = 30 * ONE_DAY;




    private static final LinkedHashMap<String, Callable<Boolean>> queueMap = new LinkedHashMap<>();




    private static class CacheEntry {
        public String url;
        public String fileName;
        String mimeType;
        public String encoding;
        long maxAgeMillis;


        private CacheEntry(String url, String fileName,
                           String mimeType, String encoding, long maxAgeMillis) {
            this.url = url;
            this.fileName = fileName;
            this.mimeType = mimeType;
            this.encoding = encoding;
            this.maxAgeMillis = maxAgeMillis;
        }
    }


    private Map<String, CacheEntry> cacheEntries = new HashMap<>();
    private File rootDir = null;


    public DVDUrlCache() {
//本地缓存路径,请在调试中自行修改


        // this.rootDir = DiskUtil.getDiskCacheDir(DVDApplicationContext.getInstance().getApplicationContext());
        this.rootDir = new File(EXTSTORAGEAPPCACHEPATH
        );
    }


    public void register(String url, String cacheFileName,
                         String mimeType, String encoding,
                         long maxAgeMillis) {
        CacheEntry entry = new CacheEntry(url, cacheFileName, mimeType, encoding, maxAgeMillis);
        this.cacheEntries.put(url, entry);
    }


    public WebResourceResponse load(final String url) {
        try {
            final CacheEntry cacheEntry = this.cacheEntries.get(url);


            if (cacheEntry == null) {
                return null;
            }
            final File cachedFile = new File(this.rootDir.getPath() + File.separator + cacheEntry.fileName);
            if (cachedFile.exists()) {
                //还没有下载完
                if (queueMap.containsKey(url)) {
                    return null;
                }
                long cacheEntryAge = System.currentTimeMillis() - cachedFile.lastModified();
                if (cacheEntryAge > cacheEntry.maxAgeMillis) {
                    cachedFile.delete();
                    return null;
                }
                return new WebResourceResponse(
                        cacheEntry.mimeType, cacheEntry.encoding, new FileInputStream(cachedFile));
            } else {
                if (!queueMap.containsKey(url)) {
                    queueMap.put(url, new Callable<Boolean>() {
                        @Override
                        public Boolean call() throws Exception {
                            return downloadAndStore(url, cacheEntry);
                        }
                    });
                    final FutureTask<Boolean> futureTask = ThreadPoolManager.getInstance().addTaskCallback(queueMap.get(url));
                    ThreadPoolManager.getInstance().addTask(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                if (futureTask.get()) {
                                    queueMap.remove(url);
                                }
                            } catch (InterruptedException | ExecutionException e) {
                                Log.d(LOG_TAG, "", e);
                            }
                        }
                    });
                }
            }
        } catch (Exception e) {
        }
        return null;
    }


    private boolean downloadAndStore(final String url, final CacheEntry cacheEntry)
            throws IOException {




        FileOutputStream fileOutputStream = null;
        InputStream urlInput = null;
        try {
            URL urlObj = new URL(url);
            URLConnection urlConnection = urlObj.openConnection();
            urlInput = urlConnection.getInputStream();
            String tempFilePath = "";


            tempFilePath = DVDUrlCache.this.rootDir.getPath() + File.separator + cacheEntry.fileName + ".temp";
            File tempFile = new File(tempFilePath);
            fileOutputStream = new FileOutputStream(tempFile);
            byte[] buffer = new byte[1024];
            int length;
            while ((length = urlInput.read(buffer)) > 0) {
                fileOutputStream.write(buffer, 0, length);
            }
            fileOutputStream.flush();
            File lastFile = new File(tempFilePath.replace(".temp", ""));
            boolean renameResult = tempFile.renameTo(lastFile);
            if (!renameResult) {
            }
//            Log.d(LOG_TAG, "Cache file: " + cacheEntry.fileName + " stored. ");
            return true;
        } catch (Exception e) {
            Log.e(LOG_TAG, "", e);
        } finally {
            if (urlInput != null) {
                urlInput.close();
            }
            if (fileOutputStream != null) {
                fileOutputStream.close();
            }
        }


        return false;
    }


}

 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页