缓存性能html5缓存的那些事

ImagineCode      2022-02-17     167

关键词:

更多前端文章:http://lvtraveler.github.io/

关于存储

说到存储,你可能会想到这是服务器端的一种设置。

服务器端的存储介质大体上分为4种:

  • cache:缓存,它可以让从数据库、磁盘上输出的东西/数据放置在缓存里,从而减少数据库或是磁盘的读取与写入(IO)操作;
  • 磁盘文件:如,我们常常会将图片、视频等文件存放在磁盘上;
  • 数据库:mySqlmongoDB…关系非关系数据库;
  • 内存:通常放置频繁要使用到的东西,能够提高读取效率;缓存(cache)也是存放在内存里的;

HTML的存储-cookies

在HTML5出生之前,通常在浏览器(客户端)使用cookies来存储客户端的内容;

cookies的特点:

  • 每次的http请求头中,都会带有cookies——缺点;
  • 每个域名只能存储4K大小的cookies;
  • 主域名污染:如果我们使用cookies存储主域名的东西,那么子域名下得Http请求都会带上主域名的东西;

如果关联上网络,那么将带来安全问题。

所以,通常我们会使用cookies用在如购物车、身份验证等问题上。

下面,我们来看一下百度首页的cookies在浏览器端的一个存储形态:

如图:

技术分享

HTTP这一列,如果在setCookie的时候,这里就会打钩,这与HTTPOnly相关。

HTTPOnly:

如果把HTTPOnly设置为true,那么cookies只能被server服务器端来读取或是修改,客户端没有权限进行读取和修改。例如,我们在进行身份验证的时候,就可以使用这个。

Secure:与安全相关,如果设置了,那么请求只能是来自HTTP加密请求。

HTML的存储-UserData

  • 只有IE支持,有微软提供API,但不符合W3C标准;
  • 存储在XML文件中;

HTML5的存储

针对以上问题,HTML5的出现,需要解决以下问题:

  • 解决4K的大小问题;
  • 解决请求头常带存储信息的问题;
  • 解决关系型存储的问题;
  • 跨浏览器平台问题;

HTML5存储形式

  • 本地存储——localstorage sessionstorage
  • 离线缓存——application cache
  • IndexedDB、Web SQL

本地存储

API:

localstorage 、sessionstorage

存储形式:

key–>value

过期时间:

localstorage:永久存储,永不失效,除非手动删除
sessionstorage:重新打开页面,或是关闭浏览器,sessionstorage才会消失;

存储大小:
每个域名能存5M;

支持情况:

IE8+,safari3.2+,chrome,firefox等主流浏览器都支持;

使用方法——localstoragesessionstorage

主要涉及到5个方法:

  • getItem:获取localstoragesessionstorage
  • setItem:设置localstoragesessionstorage
  • removeItem:移除localstoragesessionstorage
  • key:获取某一个位置上的key值,按值从0开始索引;
  • clear:全部清除localstoragesessionstorage

例如:我们打开www.baidu.com

在控制台Console输出面板,输入:

localStorage.setItem("test1","test");
那么在Resources面板的Local Storage下,将出现Key=test1,value=test的记录

localStorage.getItem("test1"); //输出test

localStorage.key(0);//输出BDSUGSTORED

sessionstorage的API与localstorage一样,但是你要注意一点:

sessionStorage需要在浏览器关闭或是重新打开页面,才会消失;

本地存储可以存储什么?

  • 数组(需要将其序列化为字符串才能存储);
  • json数据——将其转化为字符串存储;
  • 图片
  • 脚本、样式文件:通过ajax

只要能被转化为字符串的数据,都能被localstorage存储;

本地存储如何存储图片

先来看一段代码:

    var src="demo.jpg";

    function set(key){
        var img=document.createElement(‘img‘);

        img.addEventListener("load",function(){
            //创建一个canvas
            var imgCanvas=document.createElement("canvas"),
            imgContext=imgCanvas.getContext("2d");
            //确保canvas元素的大小和图片的尺寸一致
            imgCanvas.width=this.width;
            imgCanvas.height=this.height;
            //渲染图片到canvas中,使用canvas的drawImage()方法
            imgContext.drawImage(this,0,0,this.width,this.height);
            //用canvas的dataUrl的形式取出图片,imgAsDataURL是一个base64的字符串
            var imgAsDataURL=imgCanvas.toDataURL("image/png");
            //保存到本地存储中
            //使用try-catch()查看是否支持localstorage
            try{
                localStorage.setItem(key,imgAsDataURL);//将取出的图片存放到localStorage 
            }
            catch(e) {
             console.log("Storage failed:"+e);//存储失败
            }

        },false);
        img.src=src;
    }   
    function get(key) {//从本地缓存获取图片并且渲染
    var srcStr=localStorage.getItem(key);//从localStorage中取出图片
    var imgObj=document.createElement(‘img‘);//创建一个img标签
    imgObj.src=srcStr;
    document.body.appendChild(imgObj);
    }

        注释:
        (1)、这个比较适合用在不常更改的图片,但是如果图片的base64大小比较大的话,将比较耗费localStorage的资源;

        (2)、canvas有一个安全策略的问题:如果图片和你本身请求的域名不在同一个域名下,浏览器会报出一个安全问题,这个时候我们要给我们的服务器加一个“允许跨域”访问的响应头————Access Orign=*,这样来保证你的图片可进行跨域被canvas来画;

HTML5本地存储需要注意的:

  • 使用前判断浏览器是否支持localStorage;(IOS浏览器在无痕模式浏览下,是无法打开localStorage;以及,其他奇葩浏览器,在存储localstorage的时候报错)

做法:根据前面代码,我们在检查是否支持,先进行setItem()一次,然后对setItem进行异常捕获;

  • 写数据的时候,需要异常处理,避免超出容量抛出错误;
    localStorage本身只有5M;

  • 避免把敏感的信息存入localStorage;

  • key的唯一性;重复写,将会覆盖之前的key;

HTML5本地存储使用限制:

  • 存储更新策略,过期控制:localStorage是永不过期的,业务上如果想实现一些过期策略,需要在localStorage上加一层处理过期的机制;
  • 各个子域名之间不能共享存储数据;(借助H5的postMessage()这个API做一些跨域上得处理)
  • 超出存储大小之后如何存储——使用一些如LRU、FIFO的算法去淘汰一些旧的数据;
  • server端如何取到数据——使用post/get参数

处理过期控制

先来看一下代码:

function set(key,y){
        var curTime=new Date().getTime();
        //存储一个当时存储时候的时间
        localStorage.setItem(key,JSON.stringify({data:v,time:curTime}));

    }
    function get(key,exp) {
        var data=localStorage.getItem(key);
        var dataObj=JSON.parse(data);
        if(new Date().getTime()-dataObj.time>exp) {//get出来的时间减去当时存储的时间大于过期时间,那么就认为过期
           console.log("过期");
        }else {
        //否则,返回值
         console.log("data="+dataObj.data);
        }
    }

本地存储使用场景

  • 本地数据存储,减少网络传输
  • 在弱网络的环境下,会发生高延迟,低带宽,应该尽量把数据(如脚本、样式)本地化;

我们来看一张图,显示的是本地存储和网络拉取耗时的对比:

技术分享

IndexedDB

概念

IndexedDB,是一种能做浏览器中持久地存储结构化数据的数据库,并且为web应用提供了丰富的查询能力;

支持情况

chrome11+opera不支持firefox 4+IE 10+,移动端浏览器支持能力弱

存储结构

IndexedDB是按域名分配独立空间,一个独立域名下可以创建多个数据库,每个数据库可以创建对个对象存储空间(表/table),一个对象存储空间可以存储多个对象数据;

如图:

技术分享

使用IndexedDB实现离线数据库

这里我们主要从IndexedDB 的四大功能入手:

  • 增删改
  • 事务处理
  • 游标
  • 索引

下面我们通过一段代码来讲解,请关注里面的注释:

<!DOCTYPE html>
    <html>

    <div class="form-group">
        <label for="name">姓名:</label><input type="text" id="name" value="" />   
        <label for="phone">电话:</label><input type="text" id="phone" value="" /> 
        <label for="address">地址:</label><input type="text" id="address" value="" /> 
        <input type="button" id="seletBtn" value="查询" />    
        <input type="button" id="add" value="添加" /> 
        <input type="button" id="deleteDB" value="删除数据库" /> 
    </div>


    <script type="text/javascript">
        var db;
        var arrayKey=[];
        var openRequest;
        var lastCursor;
        var indexedDB=window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;//indexedDB在不同的浏览器下不同

        var dbName="person";//数据库名称
        var tableName="testTable";//表名称
        function init() {
            openRequest=indexedDB.open(dbName);//页面加载时先打开一个DB,如果该DB存在,则打开;不存在,则新建

            //触发事件——当一个“新的数据库”被创建或者数据库的“版本号”被更改时触发
            openRequest.onupgradeneeded=function(e){
                console.log("onupgradeneeded");
                var thisDb=e.target.result;
                console.log(thisDb.version);

                //检查这个数据库中是否包含我们要查找的表
                if(!thisDb.objectStoreNames.contains(tableName)){
                    //不包含——创建一个表
                    console.log("需要创建一个objectStore");
                    //keyPath:主键,autoIncrement:主键自增
                    var objectStore=thisDb.createObjectStore(tableName,{keyPath:"id",autoIncrement:true});
                    //创建表的时候,指定哪些字段是能被索引的
                    objectStore.createIndex("name","name",{unique:false});//创建索引
                    objectStore.createIndex("phone","phone",{unique:false});
                }

            }
            //触发事件——成功打开一个数据库时触发
            openRequest.onsuccess=function(e){
                db=e.target.result;
                console.log(db.version);
                db.onerror=function(event){
                    alert("数据库错误:"+event.target.errorCode);
                    console.dir(event.target);
                };
                //判断该数据库中有没有这个表
                if(db.objectStoreNames.contains(tableName)){
                    //存在这个表
                    console.log("包含表:"+tableName);
                    //通过事物机制操作一个表的读写,从而保证数据的一致性和可靠性
                    var transaction=db.transaction([tableName],"readwrite");
                    //事物的事件
                    transaction.oncomplete=function(event){
                        console.log("完成");
                    };

                    transaction.onerror=function(event){
                        console.dir(event);
                    };
                    var objectStore=transaction.objectStore(tableName);//通过事物获取表中一个objectStore对象,即表的对象

                    //遍历表的记录——游标-openCursor,这是indexedDb的重点
                    objectStore.openCursor().onsuccess=function(event){
                        var cursor=event.target.result;
                        if(cursor){
                            console.log(cursor.key);
                            console.dir(cursor.value);
                            render({key:cursor.key,name:cursor.value["name"],phone:cursor.value["phone"],address:cursor.value["address"]});
                            lastCursor=cursor.key;//如果不设置lastCursor,那么游标默认是下一条接着下一条来遍历;设置了lastCursor,游标将循环遍历
                            cursor.continue();
                        }else {
                            console.log("请使用游标来搞定");
                        }
                    };
                    objectStore.openCursor().onerror=function(event){
                        console.dir(event);
                    };

                }

            }


            //添加新记录
            document.querySelector("#add").addEventListener("click",function(){
                var name=document.querySelector("#name").value();
                var phone=document.querySelector("#phone").value();
                var address=document.querySelector("address").value();
                var person={"name":name,"phone":phone,"address":address};//设置对象
                //通过事务——操作表
                var transaction=db.transaction([tableName],"readwrite");
                transaction.oncomplete=function(event){
                    console.log("事务处理完成");
                };
                transaction.onerror=function(event){
                    console.dir(event);
                };
                var objectStore=transaction.objectStore(tableName);//创建一个表对象
                objectStore.add(person);//添加对象到表中——add()
                //将新增的记录显示处理
                objectStore.openCursor().onsuccess=function(event){
                    cursor=event.target.result;
                    var key;
                    if(lastCursor==null){
                        key=cursor.key;
                        lastCursor=key;
                    }else {
                        key=++lastCursor;
                    }
                    render({key:key,name:name,phone:phone,address:address});
                    console.log("成功添加新记录:"+key);
                    console.dir(person);
                }

            });

            //删除指定ID
            function deleteRecord(id){
                var transaction=db.transaction([tableName],"readwrite");
                transaction.oncomplete=function(event){
                    console.log("事务处理完成");
                };
                transaction.onerror=function(event){
                    console.dir(event);
                };
                var objectStore=transaction.objectStore(tableName);
                var removeKey=parseInt(id);
                var getRequest=objectStore.get(removeKey);//获取索引值---get()
                getRequest.onsuccess=function(e){
                    var result=getRequest.result;
                    console.dir(result);
                }
                var request=objectStore.delete(removeKey);//删除——delete
                request.onsuccess=function(e){
                    console.log("删除成功");
                };
                request.onerror=function(e){
                    console.log("删除错误"+e);
                };
                //隐藏删除的DOM
                document.getElementById(removeKey).style.display="none";
            }

            //查询记录
            document.querySelector("#seletBtn").addEventListener("click",function(){
                var curName=document.getElementById("selname").value;
                var transaction=db.transaction([tableName],"readwrite");
                transaction.oncomplete=function(event){
                    console.log("事务处理完成");
                };
                transaction.onerror=function(event){
                    console.dir(event);
                };
                var objectStore=transaction.objectStore(tableName);
                var boundKeyRange=IDBKeyRange.only(curName);//生成一个表示范围的Range对象---IDBKeyRange,有4个方法,onlylowerBoundupperBoundound
                objectStore.index("name").openCursor(boundKeyRange).onsuccess=function(event){
                    //从indexedDb中找到name
                    var cursor=event.target.result;
                    if(!cursor){
                        return;
                    }
                    var rowData=cursor.value;
                    console.log(rowData);
                    document.getElementById(‘content‘).innerHTML="";
                    render({key:cursor.value.id,name:cursor.value["name"],phone:cursor.value["phone"],address:cursor.value["address"]});
                    cursor.continue();
                };

            });
            //删除数据库
            document.querySelector("#deleteDB").addEventListener("click",function(){
                //使用deleteDatabase()
                var deleteDB=indexedDB.deleteDatabase(dbName);
                var content=document.querySelector("#content");
                while(content.firstChild){
                    content.removeChild(content.firstChild);
                }
                deleteDB.onsuccess=function(event){
                    console.log("删除成功");
                };
                deleteDB.onerror=function(event){
                    console.dir(event.target);
                };
            });

            //渲染
            function render(opt){
                var child_node = document.createElement("div");
                var child_node_child1 = document.createElement("div");
                var child_node_child2 = document.createElement("div");
                var child_node_child3 = document.createElement("div");
                var child_node_child4 = document.createElement("div");
                child_node_child1.setAttribute("class","table_child");
                child_node_child2.setAttribute("class","table_child");
                child_node_child3.setAttribute("class","table_child");
                child_node_child4.setAttribute("class","table_child");
                child_node_child1.setAttribute("style","float:left");
                child_node_child2.setAttribute("style","float:left");
                child_node_child3.setAttribute("style","float:left");
                child_node_child4.setAttribute("style","float:left");
                child_node_child1.innerHTML = name;
                child_node_child2.innerHTML = opt.phone;
                child_node_child3.innerHTML = opt.address;
                child_node_child4.innerHTML = "<input type=‘button‘ value=‘删除‘>"
                child_node.appendChild(child_node_child1);
                child_node.appendChild(child_node_child2);
                child_node.appendChild(child_node_child3);
                child_node.appendChild(child_node_child4);
                child_node.setAttribute("class","table_tr");
                child_node.setAttribute("id",opt.key);
                var content = document.getElementById(‘content‘);
                content.appendChild(child_node)

            }

        }
    </script>

    </body>
    </html>

离线缓存——application Cache

何为离线缓存

它是能让web应用在离线的情况下继续使用,通过一个叫manifest的文件指明需要缓存的资源;你可以通过navigator.online检测是否在线;

原理

如图:

技术分享

解释:

(1)用户通过浏览器(browser)去访问应用,首先检测浏览器是否有一个叫做“App cache”的东西存在,如果存在,则从中检索出app cache所要缓存的list,然后把资源(缓存在浏览器中)拉取出来,返回给用户;

(2)在访问的同时,会检查server上一个叫做manifest的文件,如果该文件有更新,就把manifest指定的文件从server端重新拉取一次,然后把这些缓存在浏览器中,并更新相应的app cache文件;如果manifest这个文件没有更新,那么就啥也不做。

从上图,我们总结2点:

  • 缓存机制的改变,会更新app cache.但是,用户访问,会返回上一次的结果。这样一来,会有一个麻烦,即如果你的业务发生更改,你就需要去更新一次manifest。

注意:更改完,第一次是不生效的,只有第二次刷新才会生效;

  • 如果有一个文件要更新,你就要去更新manifest,而更新manifest文件,它会把server上的文件全部重新拉取一次,而非只是拉取你需要更改的那个文件,这就会造成损耗;

浏览器支持情况

safari on ios 3.2+android 1,5+window phone 9+

应用

例子:cache.appcache

CACHE MANIFEST

    #version 1.0

    CACHE:

    #需要缓存的文件
    /css/a.css
    /js/a/js
    /images/a.png

    NETWORK:

    #每次重新拉取的文件

    *

    FALLBACK

    #离线状况下代替的文件

    /404.html

在页面上引入manifest文件:

<html manifest="cache.appcache">

在服务器添加mime-type text/cache-manifest

如果在服务器上添加:

找到你的xampp/apache/conf目录,找到mime.types文件,在最后面添加一条记录:

text/cache-manifest appcache (appcache是后缀名,你可以选择其他的)

我们来看一个例子:

<html lang="en" manifest="cache.appcache">
      <head>
        <meat charset="utf-8" />
      </head>
      <body>
         <h1><demo1/h1>

      <script type="text/javascript">
        window.addEventListener(‘load‘,function(e){
        //监听app cache的updateready事件
         window.applicationCache.addEventListener(‘updateready‘,function(e)){

            console.log(window.applicationCache.status);
            if(window.applicatioinCache.status==window.applicationCache.UPDATEREADY){
            //application cache的版本号发生改年
              window.applicationCache.swapCache();
              window.location.reload();
            }else {
              console.log("manifest没有更改");
            }
        },false);
      },false);
      </script>
      </body>

    </html>

注意:

  • app cache会自动地将本页当做一个静态页缓存;

  • 如果你要更新,请更新server端的manifest文件的版本;

  • 如果你不想启用app cache,或者说现在app cache不适合你现在的应用,那么有一个做法:

更改server端上manifest文件的名称,例如cache1.appcache,这个时候再去刷新浏览器,首先,浏览器还是会从app cache缓存中读取缓存,到第二次刷新的时候,浏览器会到server端查找manifest文件,发现这个文件不存在,那么浏览器会走网络从Server上重新拉取文件;

app cache优势:

  • 完全离线
  • 资源缓存,加载更快
  • 降低服务器负载

app cache缺陷:

  • 含有manifest属性的当前请求页无论如何都会被缓存;
  • 更新需要建立在manifest文件的更新,文件更新后是需要页面再次刷新的,并且在第2次刷新才能获取新资源;
  • 更新是全局性的,无法单独更新某个文件;
  • 对于链接的参数变化的敏感的,任何一个参数的修改都会被重新缓存,例如:index.html和index.html?v=1会被认为是不同文件,分别缓存;

app cache适用场景

  • 单地址页面
  • 对实时性要求不要的业务
  • 离线web应用

总结

在实际应用中,我们需要根据业务的需要来采取相应的缓存措施,如上所述,html5的几种缓存都有各自的优缺点和适用场景,有时我们也需要组合使用。

关于HTML5缓存我们就介绍到这里。

参考

HTML5之IndexedDB使用详解





亿级请求下多级缓存那些事

本文转自:中生代技术 什么是多级缓存 所谓多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率,这也是应用最广的方案之一。我们应用的整体架构如图1所示: 图1多级缓存方案 整体流... 查看详情

缓存那些事-zz

https://tech.meituan.com/cache_about.html前言一般而言,现在互联网应用(网站或App)的整体流程,可以概括如图1所示,用户请求从界面(浏览器或App界面)到网络转发、应用服务再到存储(数据库或文件系统),然后返回到界面呈现内... 查看详情

搞懂分布式技术13:缓存的那些事

搞懂分布式技术13:缓存的那些事缓存和它的那些淘汰算法们为什么我们需要缓存?很久很久以前,在还没有缓存的时候……用户经常是去请求一个对象,而这个对象是从数据库去取,然后,这个对象变得越来越大,这个用户每... 查看详情

关于cdn那些事

...资源。2、CDN工作原理:首先:我们需要了解一下未加CDN缓存服务的网站访问过程。用户提交域名--》浏览器查询DNS本地缓存(host文件或者浏览器缓存)有就直接用,没有就通过浏览器请求DNS服务器,对域名进行解析获取服务器IP... 查看详情

http协商缓存vs强缓存

之前一直对浏览器缓存只能描述一个大概,深层次的原理不能描述上来;终于在前端的两次面试过程中被问倒下,为了泄恨,查阅一些资料最终对其有了一个更深入的理解,废话不多说,赶紧来看看浏览器缓存的那些事吧,有不... 查看详情

html5应用程序缓存

...L5,通过创建cachemanifest文件,可以轻松创建web应用的离线缓存。 什么事应用程序缓存?HTML5引入了应用程序缓存,这意味着web应用可进行缓存,并在没有因特网连接时进行访问。 应用程序缓存为应用带来三个优势:1.离... 查看详情

链表(上)

链表(上)@(数据结构与算法)链表的经典应用场景:LRU缓存淘汰算法。缓存是一种提高数据读取性能的计数,如常见的:CPU缓存,数据库缓存,浏览器缓存等。缓存的大小有限,当缓存被用满时,那些数据应该被清理出去,那些... 查看详情

hibernate缓存

Cachehibernate提供查询缓存,用于减轻数据压力,提高数据库性能。经常做查询,修改比较少,而且不需要事时显示的数据,甚至能允许一部分过期数据存在,这样才使用查询缓存.hibernate提供一级缓存,二级缓存,三级缓存缓存种类一级... 查看详情

hibernate缓存

Cachehibernate提供查询缓存,用于减轻数据压力,提高数据库性能。经常做查询,修改比较少,而且不需要事时显示的数据,甚至能允许一部分过期数据存在,这样才使用查询缓存.hibernate提供一级缓存,二级缓存,三级缓存缓存种类一级... 查看详情

hibernate

Hibernate的二级缓存理解缓存定义:缓存(Cache):计算机领域非常通用的概念。它介于应用程序和永久性数据存储源(如硬盘上的文件或者数据库)之间,其作用是降低应用程序直接读写永久性数据存储源的频率,从而提高应用... 查看详情

[html5]app离线缓存(applicationcache)

故名思意,这个接口就是让网页或者文件在本地缓存下来,优点是可以提高网站的加载速度,同理如果缓存全部文件到本地则可以离线浏览网页。我们可以把那些框架文件和哪些陈年不变的图片文件缓存到本地,提高网站二次浏... 查看详情

关于性能测试的那些事

 之前有做过几次做性能测试,略有心得和大家分享一下 从测试需求开始,到完成测试,都需要经过很多阶段 首先是测试需求,要评估测试需求是否合理,并不是所有的性能测试需求都需要直接来安排测试,而是评估... 查看详情

app性能优化的那些事

 来源:树下的老男孩 链接:http://www.jianshu.com/p/2a01e5e2141f 这次我们来说说iOSapp中滑动的那些事。iOS为了提高滑动的流畅感,特意在滑动的时候将runloop模式切换到UITrackingRunLoopMode,在这个过程中专心做跟滑动相关的工... 查看详情

redis简介,安装

简介什么是缓存?缓存就是将数据存放在距离计算机最近的位置以加快处理速度。缓存是改善软件性能的第一手段,在复杂的软件设计中,缓存几乎无处不在。大型网站架构设计在很多方面都使用了缓存设计。Redis就是目前缓存... 查看详情

谁给我解释一下一级缓存和二级缓存啊~

参考技术A无论是一级还是二级缓存都可以看成一个高性能的内存,性能要比内存强很多,可以看成是几个性能数量级的内存。而且一级缓存要比二级更为高效强大。他们都是暂时寄存数据,等待CPU处理。但是一级缓存的造价昂... 查看详情

app性能优化的那些事

...发人员把注意力更多的放在开发功能上面,比较少去考虑性能的问题,可能这其中涉及到objective-c,c++跟lua,优化起来相对复杂一些,导致应用在比如touch等较低端的产品上,光从启动到进入页 查看详情

HTML5 会支持离线缓存音频的访问吗?

】HTML5会支持离线缓存音频的访问吗?【英文标题】:WillHTML5supporttheaccessofofflinecachedaudio?【发布时间】:2010-06-0321:55:02【问题描述】:我们想要制作一个基于音频的基于网络的应用程序,该应用程序将具有许多声音sn-ps。我们希... 查看详情

关于android性能监控matrix那些事?你知道那些?(完)(代码片段)

关于Android性能监控Matrix那些事?你知道那些?(上)关于Android性能监控Matrix那些事?你知道那些(中)?视频也更新了:微信Matrix卡顿监控实战,函数自动埋点监控方案今天抽空把后面的... 查看详情