🎺🔥 android native与webview 传递流数据(h5、webview、android、byte[]、blog) - 初版

avatar
前端工程师

4db67634841d4acfb7aef89233a0cf01_tplv-k3u1fbpfcp-watermark.jpeg

上期文章:https://juejin.cn/post/7259272190637539386,感谢大家支持

背景

在androidx-Webkit,我们如果需要传递一张图片在webview与native,可以通过两种方式来实现

  • 文件blog转换成base64,再由接收端继续处理
  • 发送端先将图片上传至服务器,再将图片地址传递至接收端,接收端再将图片下载下来

相比于将图片直接发送到文件服务,然后传个链接给客户端,文件blog转换成base64传给客户端具有明显优势

  • 不占用服务资源
  • 速度快
  • 不需要服务端开发成本

但是在真正落地时才发却发生了一个问题,android内存溢出了,最后经过排查是base64的字符串长度太长了,其实本质也是图片太大,导致输出的base64格式的字符串也相应的超过了客户端的字符串最大长度。难道我们要放弃这个方案么?显然不可!

探索直接传流: demo源码 解析

既然是图片太大了导致的,那么就改变思路,尝试将图片分片传递,然后由native去逐片保存,那么问题来了,再经过九九一天的尝试过后,终于完成了这项不太艰巨但是可以爽快摸鱼的任务。

流处理服务-UploadBlobServer

这个类主要做了两件事情,一个是将base64还原回byte[]对象,另外一件事情就是将文件保存到本地。

  // base64还原回byte[]
  import android.util.Base64;
  byte[]  buffer = Base64.decode(bufferBase64,Base64.DEFAULT);
 
   // 保存流到指定文件-android中需要创建一个工作线程
   private class  DownThread {
        private String cacheFilePath = null;
        private InputStream inputStream;
        public DownThread(String cacheFilePath) {
            this.cacheFilePath = cacheFilePath;
        }
        private RandomAccessFile fileOutputStream = null;
        public void write(byte[] buffer,int seekStart) throws IOException {
            File cacheFile = new File(this.cacheFilePath);
            if (!cacheFile.exists()) {
                cacheFile.createNewFile();
            }
            fileOutputStream = new RandomAccessFile(cacheFile,"rwd");
            inputStream = new ByteArrayInputStream(buffer);
            fileOutputStream.seek(seekStart);
            int length = -1;
            while ((length = inputStream.read(buffer)) != -1) {
                fileOutputStream.write(buffer,0,length);
            }
        }
        public File close() throws IOException {
            inputStream.close();
            fileOutputStream.close();
            return  new File(cacheFilePath);
        }
    }
   
文件传递控制器UploadBlobContainer

为了方便我们去管理文件传递的过程而封装的这个类,它的主要作用是控制多文件、分片下载的过程

public class UploadBlobContainer {

    private static HashMap<String, UploadBlobServer> updateBlobHashMap =new HashMap<>();

    private static UploadBlobListener<UploadBlobBean> uploadBlobListener;

    public static void setUploadBlobListener(UploadBlobListener<UploadBlobBean> uploadBlobListener) {
        UploadBlobContainer.uploadBlobListener = uploadBlobListener;
    }

    /**
     * 获取唯一类
     * @param uploadBlobBean
     * @return
     */
    public static UploadBlobServer getUpdateBlob(UploadBlobBean uploadBlobBean){
        String fileName = uploadBlobBean.getFileName();
        if(updateBlobHashMap.containsKey(fileName)){
            return updateBlobHashMap.get(fileName);
        }
        // 初始化 流传输类
        UploadBlobServer updateBlob=new UploadBlobServer();
        // 事件统一输出
        updateBlob.setUploadBlobListener(new UploadBlobListener<String>() {
            @Override
            public void start(String fileName) {
                 if(UploadBlobContainer.uploadBlobListener!=null){
                     UploadBlobContainer.uploadBlobListener.start(uploadBlobBean);
                 }
            }

            @Override
            public void write(String fileName) {
                if(UploadBlobContainer.uploadBlobListener!=null){
                    UploadBlobContainer.uploadBlobListener.write(uploadBlobBean);
                }
            }

            @Override
            public void end(String fileName) {
                if(UploadBlobContainer.uploadBlobListener!=null){
                    UploadBlobContainer.uploadBlobListener.end(uploadBlobBean);
                }
                // 完成后从输出站内退出
                updateBlobHashMap.remove(fileName);
            }
        });
        updateBlobHashMap.put(fileName,updateBlob);
        return updateBlob;
    }

    public static  void upload(UploadBlobBean uploadBlobBean) {
        upload(uploadBlobBean,null);
    }
    public static  void upload(UploadBlobBean uploadBlobBean,UploadBlobListener<UploadBlobBean> uploadBlobListener) {
        setUploadBlobListener(uploadBlobListener);
        UploadBlobServer uploadBlobServer = getUpdateBlob(uploadBlobBean);
        switch (uploadBlobServer.status){
            case PADDING:
                uploadBlobServer.init(uploadBlobBean.getFileName(),uploadBlobBean.getContentLength());
            case WRITE:
                try {
                    uploadBlobServer.uploadFile(uploadBlobBean.getBody(),uploadBlobBean.getFileName());
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;

        }
    }
}
js的实现

最主要的功能是

  • 转化流为byte[]格式
  • byte[]分片
  • byte[]转base64
// 转化流为byte[]格式
function getUint8ArrayBlob (blobStr) {
  const byteString = atob(blobStr.split(',')[1])
  const ab = new ArrayBuffer(byteString.length)
  const blob = new Uint8Array(ab)
  for (let i = 0; i < byteString.length; i++) {
    blob[i] = byteString.charCodeAt(i)
  }
  return blob
}
// byte[]分片
function sliceUint8Array (byteArray) {
  const blocks = []
  let length = byteArray.length
  console.log('getFile-h5-length', length + '::=>' + length)
  while (length) {
    const startIdnex = byteArray.length - length
    const end = startIdnex + (length >= 65535 ? 65535 : length)
    const byteArrayBlock = byteArray.slice(startIdnex, end)
    length -= byteArrayBlock.length
    blocks.push(byteArrayBlock)
    console.log('getFile-h5-length', startIdnex + '::' + end)
  }
  return blocks
}

byte[]转化为base64格式,这里这么处理说明一下,为何不直接转成base64,因为尝试过直接转化后,并不能还原,所以String.fromCharCode()将其再次处理后才能正常还原,有谁知道为什么么?

// byte[]转化为base64格式
function arrayBufferToBase64 (buffer) {
  let binary = ''
  const bytes = new Uint8Array(buffer)
  const len = bytes.byteLength
  for (let i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i])
  }
  // window.btoa():将ascii字符串或二进制数据转换成一个base64编码过的字符串,该方法不能直接作用于Unicode字符串.
  return window.btoa(binary)
}

又能愉快的传图了

look、look一下子

题外话 轻松加愉快的androidx-Webkit

在androidx-Webkit之中,我们就不用为传递文件的功能大为光火,因为androidx-Webkit已经支持了字节流传递的方式,直接秒杀上面的所有方案,以下是我搬运的简绘Android同学的文章代码,文章链接在底端,希望大家同样支持一下。

native:

// App (in Java)
WebMessageListener myListener = new WebMessageListener() {
  @Override
  public void onPostMessage(WebView view, WebMessageCompat message, Uri sourceOrigin,
           boolean isMainFrame, JavaScriptReplyProxy replyProxy) {
    // Communication is setup, send file data to web.
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
      // Suppose readFileData method is to read content from file.
      byte[] fileData = readFileData("myFile.dat");
      replyProxy.postMessage(fileData);
    }
  }
}

js:

// Web page (in JavaScript)
myObject.onmessage = function(event) {
  if (event.data instanceof ArrayBuffer) {
    const data = event.data;  // Received file content from app.
    const dataView = new DataView(data);
    // Consume file content by using JavaScript DataView to access ArrayBuffer.
  }
}
myObject.postMessage("Setup!");

补充native于webview交互:juejin.cn/post/708674…

参考文章

juejin.cn/post/725976…

项目地址:

Android端:github.com/runner-up-j…

h5端:github.com/runner-up-j…