从零开始搞一个androidApp,实现h5自动更新、jsbridge
准备
window电脑
java jdk (包含了java jre)下载安装
android sdk下载安装
android studio下载安装
gradle下载
一台带sim卡的android手机
nodejs下载安装
npm install -g cordava 安装
1、运行app,拉取版本控制文件 https://xxx.com/project/update.json
/* 格式如下 MD5_8标示8位数的md5值*/ /* version:全量更新的tag,如果本地与version不一样,就做全量更新*/ /* last_version:最新的版本*/ /* pres_version:有增量包的版本,不超过10个*/ { version:20230101 last_version:"MD5_8" }
需要app端实现 下载增量包或全量包,更新h5版本
1、如果version与本地不同,则下载全量包 ${down_path}${last_version}.zip
2、如果version与本地相同,则判断last_version
3、如果last_version与本地不同,首先下载增量包 ${down_path}${pre_version}-${last_version}.zip,
4、如果增量包存在,则完成更新下载
5、如果增量包不存在,则下载全量包${down_path}${last_version}.zip
6、如果全量包存在,则完成更新下载
7、如果全量包不存在,则更新下载失败,客户端继续使用旧版本
碰到的问题
访问本地assets目录下的json文件,解析成json
String CACHEPATH = "update.json";//assets目录下的缓存文件 String UPDATEPATH = "http://caoke90.gitee.io/suiplugin/update.json";//每次更新的文件 String DOWNDIR = "https://example.com/project/dist/";//zip所在的远程目录 String pre_res_version=""; String pre_h5_version=""; try { // 1. 读取Android应用程序资源JSON文件 InputStreamReader isr = new InputStreamReader(getAssets().open(CACHEPATH), "UTF-8"); BufferedReader br = new BufferedReader(isr); String line; StringBuilder builder = new StringBuilder(); while ((line = br.readLine()) != null) { builder.append(line); } br.close(); isr.close(); JSONObject cacheJSON = new JSONObject(builder.toString());//builder读取了JSON中的数据。 //直接传入JSONObject来构造一个实例 pre_res_version = cacheJSON.getString("res_version"); pre_h5_version = cacheJSON.getString("h5_version"); } catch (Exception e) { Log.i("updateH5Assets","assets目录不存在"+CACHEPATH); }
https请求json报错:
1、使用okhttp3,src目录AndroidManifest.xml文件manifest中添加下一行,配置允许请求网络
<uses-permission android:name="android.permission.INTERNET"/>
2、src目录下build.gradle文件中添加依赖,用gradle下载
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
3、请求https报错,设置信任所有的证书,定义了单例Ajax,Ajax.getText("https://www.cnblogs.com/caoke/p/17209909.html")
4、下载zip包,解压到cache目录下www目录异常,读取本地json文件,报错json文件,定义了工具类UtilsApi,本地h5文件的更新功能有了
5、在index.html页面点击a标签打开test.html,发现报错,添加webviewClient解决
到目前为止:实现了h5的版本更新功能。
入口文件:MainActivity.java
package pers.example.demo1; import android.os.StrictMode; import java.io.*; import android.os.Bundle; import android.webkit.WebSettings; import android.webkit.WebViewClient; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { BridgeWebView webView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //在Android 4.0以上,网络连接不能放在主线程上 StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); //1、配置更新 String cacheDir = this.getCacheDir().getAbsolutePath(); String cacheUpdatePath = cacheDir+"/www/update.json";//本地的缓存文件 String remoteUpdatePath = "http://192.168.3.213/update.json";//每次更新的文件 String remotezipDir = "http://192.168.3.213/";//压缩包zip所在的远程目录 String cacheWWWPath = cacheDir+"/www";//本地的www目录 File cacheWWWFile=new File(cacheWWWPath); //2、更新本地h5资源 UtilsApi.updateH5Assets(cacheUpdatePath,remoteUpdatePath,remotezipDir,cacheWWWFile); setContentView(R.layout.activity_main); //3、设置webview webView = findViewById(R.id.webView); //声明WebSettings子类 WebSettings webSettings = webView.getSettings(); webSettings.setAllowFileAccess(true);//不然本地html打不开 webSettings.setUseWideViewPort(true); webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); webSettings.setJavaScriptEnabled(true);//才能让Webivew支持<meta>标签的viewport属性 webSettings.setJavaScriptCanOpenWindowsAutomatically(true); webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式 webView.setWebViewClient(new WebViewClient()); //4、加载手机本地的html页面 webView.loadUrl(cacheWWWPath+"/index.html"); // webView.loadUrl("http://www.baidu.com"); // 5. 展示所有文件 showList(cacheWWWFile); } private static void showList(File file) { if (file.isDirectory()) {//如果是目录 System.out.println("文件夹:" + file.getPath()); File[] listFiles = file.listFiles();//获取当前路径下的所有文件和目录,返回File对象数组 for (File f : listFiles) {//将目录内的内容对象化并遍历 showList(f); } } else if (file.isFile()) {//如果是文件 System.out.println("文件:" + file.getPath()); } } }
h5更新功能:UtilsApi.java
package pers.example.demo1; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class UtilsApi { // 构造方法设置为私有,避免在Singleton类外部创建Singleton对象 private UtilsApi() {} public static void updateH5Assets(String cacheUpdatePath,String remoteUpdatePath,String remotezipDir,File cacheWWWFile) { String TAG="updateH5Assets"; String pre_res_version=""; String pre_h5_version=""; try { fileProber(new File(cacheUpdatePath)); // 1. 读取Android应用程序资源JSON文件 JSONObject cacheJSON = UtilsApi.readFileToJSON(cacheUpdatePath);//builder读取了JSON中的数据。 //直接传入JSONObject来构造一个实例 pre_res_version = cacheJSON.getString("res_version"); pre_h5_version = cacheJSON.getString("h5_version"); } catch (Exception e) { Log.i(TAG,"assets目录不存在"+cacheUpdatePath); } Log.i(TAG,pre_res_version); Log.i(TAG,pre_h5_version); int tag=0; try { Log.i(TAG,remoteUpdatePath); // 2. 下载JSON文件 JSONObject nJSON=Ajax.getJSON(remoteUpdatePath); // 3. 解析JSON String res_version=nJSON.getString("res_version"); String h5_version=nJSON.getString("h5_version"); String zipPath=""; if(!res_version.equals(pre_res_version)){ deleteAllFiles(cacheWWWFile); //下载全量包 tag=1; zipPath=remotezipDir+h5_version+".zip"; }else if(!h5_version.equals(pre_h5_version)){ //下载增量包 tag=2; deleteAllFiles(cacheWWWFile); zipPath=remotezipDir+h5_version+".zip"; // zipPath=remotezipDir+pre_h5_version+"-"+h5_version+".zip"; } Log.i(TAG,zipPath); if(tag>0){ InputStream zipStream =Ajax.getInputStream(zipPath); unzipWithStream(zipStream,cacheWWWFile); zipStream.close(); // 4. 存储文件到Android应用程序资源目录 writeFileByJSON(cacheUpdatePath, nJSON); } }catch (Exception e) { e.printStackTrace(); } } // 删除文件夹以及子文件夹、子文件 public static void deleteAllFiles(File root) { File files[] = root.listFiles(); if (files != null) for (File f : files) { if (f.isDirectory()) { // 判断是否为文件夹 deleteAllFiles(f); try { f.delete(); } catch (Exception e) { } } else { if (f.exists()) { // 判断是否存在 deleteAllFiles(f); try { f.delete(); } catch (Exception e) { } } } } } /** * 解压 * * @param zipStream * @param destFile */ public static void unzipWithStream(InputStream zipStream, File destFile) { try { ZipInputStream zis = new ZipInputStream(zipStream); ZipEntry zipEntry = null; while ((zipEntry = zis.getNextEntry()) != null) { String dir = destFile.getPath() + File.separator + zipEntry.getName(); File dirFile = new File(dir); //创建上级文件夹 fileProber(dirFile); if (zipEntry.isDirectory()) { dirFile.mkdirs(); } else { BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dir)); int len; byte[] buff = new byte[1024]; while ((len = zis.read(buff, 0 ,1024)) != -1) { bos.write(buff, 0, len); } bos.close(); } zis.closeEntry(); } } catch (IOException e) { throw new RuntimeException(e); } catch (Exception e) { throw new RuntimeException(e); } } // 确保上级目录存在 private static void fileProber(File dirFile) { File parentFile = dirFile.getParentFile(); if (!parentFile.exists()) { // 递归寻找上级目录 fileProber(parentFile); parentFile.mkdir(); } } public static JSONObject readFileToJSON(String filePath) throws IOException, JSONException { InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "UTF-8"); BufferedReader br = new BufferedReader(isr); String line; StringBuilder builder = new StringBuilder(); while ((line = br.readLine()) != null) { builder.append(line); } br.close(); isr.close(); return new JSONObject(builder.toString());//builder读取了JSON中的数据。 } public static void writeFileByJSON(String filePath, JSONObject jsonObject) throws IOException { FileOutputStream fos= new FileOutputStream(filePath); OutputStreamWriter os= new OutputStreamWriter(fos); BufferedWriter w= new BufferedWriter(os); w.write(jsonObject.toString()); w.close(); } }
接口请求:Ajax.java
package pers.example.demo2; import android.annotation.SuppressLint; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; /** * Created by ck on 23-3-15 * okhttp3的再封装 */ public class Ajax { private static OkHttpClient client = null; // 构造方法设置为私有,避免在Singleton类外部创建Singleton对象 private Ajax() {} // 证书工厂 @SuppressLint("TrulyRandom") private static SSLSocketFactory createSSLSocketFactory() { SSLSocketFactory sSLSocketFactory = null; try { SSLContext sc = SSLContext.getInstance("TLS"); sc.init(null, new TrustManager[]{new TrustAllManager()}, new SecureRandom()); sSLSocketFactory = sc.getSocketFactory(); } catch (Exception ignored) { } return sSLSocketFactory; } // 证书管理器 private static class TrustAllManager implements X509TrustManager { @SuppressLint("TrustAllX509TrustManager") @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } @SuppressLint("TrustAllX509TrustManager") @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } // 证书端口 private static class TrustAllHostnameVerifier implements HostnameVerifier { @SuppressLint("BadHostnameVerifier") @Override public boolean verify(String hostname, SSLSession session) { return true; } } private static OkHttpClient getClient() throws UnsupportedEncodingException { if(client==null){ synchronized (OkHttpClient.class) { if (client == null) { client= new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .sslSocketFactory(createSSLSocketFactory(),new TrustAllManager()) .hostnameVerifier(new TrustAllHostnameVerifier()) .retryOnConnectionFailure(true).build(); } } } return client; } public static String getText(String url) throws IOException { Request request = new Request.Builder().header("User-Agent","AndroidApp-AndroidApp/1.1.0").url(url).build(); Response response = getClient().newCall(request).execute(); if(response.isSuccessful()){ return response.body().string(); } throw new IOException(); } public static JSONObject getJSON(String url) throws IOException, JSONException { Request request = new Request.Builder() .header("User-Agent","AndroidApp-AndroidApp/1.1.0") .url(url).get().build(); Response response = getClient().newCall(request).execute(); if(response.isSuccessful()){ return new JSONObject(response.body().string()); } throw new IOException(); } public static InputStream getInputStream(String url) throws IOException { Request request = new Request.Builder().header("User-Agent","AndroidApp-AndroidApp/1.1.0").url(url).build(); Response response = getClient().newCall(request).execute(); if(response.isSuccessful()){ return response.body().byteStream(); } throw new IOException(); } }
JS调用Android基本有下面三种方式,主要用到第2个
webView.addJavascriptInterface()
WebViewClient.shouldOverrideUrlLoading()
WebChromeClient.onJsAlert()/onJsConfirm()/onJsPrompt() 方法分别回调拦截JS对话框alert()、confirm()、prompt()消息
Android调用JS,主要用到第1个
webView.loadUrl();
webView.evaluateJavascript()
通过数据格式消息体,实现两端的听、说通信
public class Message {
public String responseId;
public String responseData;
public String callbackId;
public String data;
public String handlerName;
}
callbackId等于responseId
根据jsbrige1.0.4框架版本,实现了通信方案
前端js调用java
1、js端带着请求格式Message{handlerName,callbackId,data},用iframe.src通知java开始接受数据
2、java端loadUrl调用javascript:WebViewJavascriptBridge._fetchQueue();
3、js端拼接数据,
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
3、java端在shouldOverrideUrlLoading中接受数据集合,flushMessageQueue函数中处理请求或者回复
java调用前端js
1、页面不存在时记录要说的,页面在就直接发送数据
2、函数将数据转成请求格式Message{handlerName,callbackId,data}的数据,然后发送出去
2、java端loadUrl调用javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');
3、js端在_dispatchMessageFromNative函数中处理请求或者回复,回复格式Message{responseId,responseData}
package pers.example.demo2; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.StrictMode; import android.util.Log; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import androidx.appcompat.app.AppCompatActivity; import com.github.lzyzsd.jsbridge.BridgeHandler; import com.github.lzyzsd.jsbridge.BridgeWebView; import com.github.lzyzsd.jsbridge.CallBackFunction; import com.github.lzyzsd.jsbridge.DefaultHandler; import com.google.gson.Gson; import java.io.File; public class MainActivity extends AppCompatActivity { private final String TAG = "MainActivity"; BridgeWebView webView; int RESULT_CODE = 0; ValueCallback<Uri> mUploadMessage; static class Location { String address; } static class User { String name; Location location; String testStr; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //在Android 4.0以上,网络连接不能放在主线程上 StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); //1、配置更新 String cacheDir = this.getCacheDir().getAbsolutePath(); String cacheUpdatePath = cacheDir+"/www/update.json";//本地的缓存文件 String remoteUpdatePath = "http://192.168.1.217/update.json";//每次更新的文件 String remotezipDir = "http://192.168.1.217/";//压缩包zip所在的远程目录 String cacheWWWPath = cacheDir+"/www";//本地的www目录 File cacheWWWFile=new File(cacheWWWPath); //2、更新本地h5资源 UtilsApi.updateH5Assets(cacheUpdatePath,remoteUpdatePath,remotezipDir,cacheWWWFile); setContentView(R.layout.activity_main); //3、设置webview webView = (BridgeWebView) findViewById(R.id.webView); //声明WebSettings子类 WebSettings webSettings = webView.getSettings(); webSettings.setAllowFileAccess(true);//不然本地html打不开 webSettings.setUseWideViewPort(true); webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); webSettings.setJavaScriptEnabled(true);//才能让Webivew支持<meta>标签的viewport属性 webSettings.setJavaScriptCanOpenWindowsAutomatically(true); webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式 webView.setDefaultHandler(new DefaultHandler()); //4、加载手机本地的html页面 webView.loadUrl(cacheWWWPath+"/index.html"); Log.i("tag",cacheWWWPath+"/index.html"); webView.registerHandler("submitFromWeb", new BridgeHandler() { @Override public void handler(String data, CallBackFunction function) { Log.i(TAG, "handler = submitFromWeb, data from web = " + data); function.onCallBack("submitFromWeb exe, response data 中文 from Java"); } }); User user = new User(); Location location = new Location(); location.address = "SDU"; user.location = location; user.name = "大头鬼"; webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() { @Override public void onCallBack(String data) { } }); webView.send("hello"); } }