分类: app

  • 关于app:移动App测试崩溃常见的测试场景

    挪动App测试解体常见的测试场景

    01.验证在有不同的屏幕分辨率,操作系统和运营商的多个设施上。

    02.用新公布的操作系统版本验证。

    03.验证在如隧道,电梯等网络品质忽然扭转的环境中。

    04.通过手动网络从蜂窝更改到Wi-Fi ,或反过来,验证。

    05.验证在没有网络的环境中的App行为。

    06.验证复电/短信和设施特定的警报(如警报和告诉)时。

    07.通过扭转设施的方向,以不同的视图模式,验证。

    08.验证设施内存不足时。

    09.通过用测试工具施加载荷验证。

    10.用不同的反对语言验证。

    举荐浏览:

    App测试、Web测试和接口测试个别测试流程

    App功能测试纲要总结

    APP兼容性测试的目标和要求是什么?

    APP兼容性测试难点在哪里?

  • 关于app:App测试Web测试和接口测试一般测试流程

    app测试流程:

    1、需要剖析,理解具体需要

    2、测试筹备:原型图、效果图、需要文件、测试用例、用例评审、各种测试数据筹备

    3、测试环节:承受版本,开始执行

    1)冒烟测试:对版本品质的管制以及此版本是否具备测试条件;

    2)装置、卸载测试:通过商店装置,且反对应用第三方工具装置

    3)在线降级测试:验证数字签名、在线跨版本升级、降级后可应用

    4)业务功能测试:业务逻辑测试、性能点测试、关联性测试;对需要和测试用例笼罩

    5)稳定性及异样性测试:交互性测试(客户端多种打搅测试,待机拔插线等操作)、断 网 、 断电异样等状况客户端稳固

    6)性能测试,服务器接口,多线程压测;客户端不同网络下的响应速度

    7)易用性测试:界面与交互性测试,合乎交互标准,用户体验良好,应用方便快捷

    8)适配性测试:分辨率,不同版本零碎,不同尺寸等反对

    9)提交bug,争议bug评审,回归测试

    10)手机流量及电量测试:客户端应用监控电量和流量软件,确定符合规范

    11)内存泄露测试

    12)联机调试测试,关上调试模式,通过logcat记录每个操作,不便谬误定位

    13)外网测试:笼罩wifi、2g、3g、4g,电信、挪动、联通所有组合测试

    14)自动化测试

    a、ui自动化测试 :个别应用python+appium进行ui自动化测试

    4、公布性能,上线验证,公布上线报告

    web测试流程

    1、需要剖析,理解具体需要

    2、测试筹备:原型图、效果图、需要文件、测试用用例、用例评审、各种测试数据筹备

    3、测试环节:承受版本开始执行

    1)冒烟测试:对版本品质的管制以及此版本是否具备测试条件;

    2)UI测试:核查效果图

    3)功能测试:核查需要文件测试用例,对性能进行验证

    4)兼容测试:各个浏览器,手机是否兼容

    5)性能测试,服务器接口,多线程等压测

    6)平安测试

    7)易用性测试:界面与交互性测试,合乎交互标准,用户体验良好,应用方便快捷

    8)提交bug,争议bug评审,回归测试

    9)自动化测试

    a、ui自动化测试 :个别应用python+selenium进行ui自动化测试

    4、公布性能,上线验证,公布上线报告

    接口测试流程

    1、需要剖析:接口之间的逻辑关系,接口文档具体理解

    2、测试筹备:接口文档、接口测试用例、各种测试数据筹备

    3、测试环节,承受版本

    1)功能测试:功是否依照接口文档实现

    2)业务逻辑:是否依赖业务

    3)参数异样:a关键字参数(语言中的关键字)、b参数为空、c多少参数、d谬误参数

    4)数据异样:a关键字数据、b数据为空、c长度不统一(短处数据库字段长度)、d谬误数据

    5)平安:cookie、header(特地是挪动端应用)、惟一识别码(客户端罕用)

    6)回归测试

    7)自动化测试:个别应用python+requests进行接口自动化测试

    4、公布性能,上线验证、发送上线报告

    举荐浏览:

    什么是冒烟测试?

    Bug治理的个别规范流程

    继续集成,继续交付,继续部署三者之间有什么异同?

    疾速找出bug的几点倡议

    App功能测试纲要总结

  • 关于app:移动APP性能测试包括哪些方面

    挪动App性能测试次要测试App在边界、压力等极其条件下运行是否满足客户需要,例如在电量有余、访问量增大等状况下App运行是否失常。上面介绍挪动App的性能测试要点。

    1.边界测试

    在各种边界压力下,如电量有余、存储空间有余、网络不稳固时,测试App是否能正确响应、失常运行。

    2.压力测试

    对挪动App一直施加压力,如一直减少负载、一直增大数据吞吐量等以确定App的服务瓶颈,取得App能提供的高并发服务级别,确定App性能是否满足用户需要。

    3.响应能力测试

    确应能力测试本质上也是种压力测试, 在肯定条件下 App是否能够正确响应,响应工夫是否超过了客户需要。

    4.耗能测试

    App测试运行时对挪动设施的资源占用状况,包含内存、CPU耗费,App长期运行时耗电量、耗流量状况,验证App对资源的耗费是否满足用户需要。

    举荐浏览:

    性能测试须要做些什么?

    性能测试分为哪几种类型?

    性能测试常见的一些指标

    性能测试中常见的的三大误区

  • 关于app:APP兼容性测试难点在哪里泽众云测试

    APP兼容性测试,在于须要笼罩不同的android零碎版本、不同的手机品牌和型号、不同的分辨率。

    当然,这是外表的起因,不是具体影响的内容。

    通过咱们的理论教训,咱们发现,兼容性测试的几个难点是:

    第一,app的装置和受权。

    第二,app对象在不同手机上的变动。

    第三,平安键盘和字体导致的差别。

    咱们先看app的装置。

    当咱们去装置一个app,咱们晓得,不同的手机采纳了不同的装置平安控制策略。比方,简略的会询问平安起源是否容许,简单的须要输出以后手机的pin来容许装置。

    当咱们应用自动化脚本来装置,就须要辨认不同手机对安全控制的解决,反映在界面上,就是要动静辨认装置时候弹出的各种询问解决、明码输出等状况。

    实际上,简单的状况是,当零碎弹出须要输出pin的界面的时候,出于安全性的思考,自动化测试工具却无奈辨认、无奈“发现”这个界面,对于自动化测试工具来说,就是“一片光明”,就是什么都没有。因而,这种状况下,是无奈实现自动化装置的。

    所以,总结一句话,所谓一个脚本可能实现所有手机的自动化测试,都是耍流氓。你须要手工去装置APP给这些非凡的手机。具体是哪些型号,就不点名了。

    再看受权。

    当装置实现,第一次启动app,会怎么样?会弹出很多受权窗口,须要你来容许拜访。这个受权窗口的弹出,跟APP其实是没有关系的(也有一点,就是app须要申请什么样的权限),于是就又回到了跟装置相似的问题,这时候,须要解决的不是利用解决的脚本,而是各个手机受权的脚本,一样的不肯定可能99.99%的自动化解决。

    所以,咱们反复一下,说写一个脚本就可能实现所有的操作,无需人工干预的,就是耍流氓,不,是坑骗客户。

    当然,自动化的解决,可能实现80%以上的状况,还有5%-20%是难以解决的,因为会遇到各种状况,特地是手机开发厂商处于平安的思考,不容许辨认屏幕对象,不容许镜像屏幕的时候。

    举荐浏览:

    手机app兼容性测试,次要是针对哪些方面测试?

    通过在线泽众云测试平台做APP兼容性测试精确吗?

  • 关于app:从第一代到第五代App加固技术详解

    App加固技术性能倒退

    通过这几年的倒退APP加固技术,一直失去疾速迭代倒退,加固的强度也在一直的晋升。加固技术的倒退次要经验:动静加载、内存不落地加载、指令抽取、指令转换、虚拟机爱护。上面就别离对这些技术实现进行解析。

    动静加载

    Android动静加载加固技术用于爱护App利用的逻辑不被逆向与剖析,最早广泛在恶意软件中应用,它次要基于Java虚拟机提供的动静加载技术。因为动静加载技术次要依赖于java的动静加载机制,所以要求要害逻辑局部必须进行解压,并且开释到文件系统。这种动静加载技术不足之处在于:1.这一解压开释机制就给攻击者留下间接获取对应文件的机会;2.能够通过hook虚拟机要害函数,进行dump出原始的dex文件数据。

    不落地加载

    Android不落地加载技术,它是在动静加载技术的根底进行改良。它通过借鉴第一代加固的动静加载技术中,要害逻辑局部必须开释到文件系统的缺点,它次要新增文件级别的加解密。

    文件级别的加解密技术次要有两种实现计划:1.通过拦挡零碎的IO相干函数,在这些零碎的函数中进行通明加解密。2.间接调用虚拟机提供的函数,进行不落地的加载。这种文件级别的加解密不足之处在于:1.因为在App启动时需解决大量加解密操作,它会造成App启动卡顿假死或黑屏景象,用户体验感较差;2.因为它的内存是间断的,通过hook要害函数就能够获取到间断残缺的dex数据。

    指令抽取

    android的指令抽取,次要在于函数根本的抽取爱护。通过应用android虚拟机自带的解释器进行执行代码。将原始App中dex文件的函数内容进行革除,并将独自挪动到一个加密文件中,在App运行的时候,再将函数内容从新复原到对应的函数体。

    这一指令抽取技术的不足之处在于:1.应用大量的虚拟机内部结构,会呈现兼容性问题;2.应用android虚拟机进行函数内容的执行,无奈反抗自定义虚拟机;3.它跟虚拟机的JIT优化呈现抵触,达不到最佳的性能体现。

    指令转换/VMP

    它次要通过实现自定义Android虚拟机的解释器,因为自定义解释器无奈对Android零碎内的其余函数进行间接调用,所有必须应用java的jni接口进行调用。

    这种实现技术次要有两种实现:1.dex文件内的函数被标记为native,内容被抽离并转换为一个合乎jni要求的动静库。2.dex文件内的函数被标记为native,内容被抽离并转换为自定义的指令格局。并通过实现自定义接收器,进行执行代码。它次要通过虚拟机提供的jni接口和虚拟机进行交互。这一指令转换技术实现计划不足之处在于:在攻击者背后,攻击者能够间接将这个加固技术计划当做黑盒,通过实现自定义的jni接口对象进行外部调试剖析,从而失去残缺的原始dex文件。

    虚拟机源码爱护

    通过利用虚拟机技术爱护(www.dingxiang-inc.com)App中的所有代码,包含java、Kotlin、C/C++等多种代码,虚拟机技术次要是通过把外围代码编译成两头的二进制文件,随后生成独特的虚拟机源码,爱护执行环境和只有在该环境下能力执行的运行程序。通过基于llvm工具链实现ELF文件的vmp爱护。通过虚拟机爱护技术,让ELF文件领有独特的可变指令集,大大提高了指令跟踪,逆向剖析的强度和难度。

    App加固后的性能实现

    字符串加密:将App的源代码中敏感字符串做随机加密解决。在运行时进行对字符串动静解密,这样就能够防止攻击者,通过利用工具进行动态逆向剖析发现要害字符串信息,从而疾速定位到利用中的业务代码。

    控制流平坦化:将so文件中C\C++代码中的执行管制逻辑变换为平坦的管制逻辑,从形象语法树层面进行深度混同,使得其在罕用反编译工具中,极大的升高反编译逆向代码的可读性,减少逆向代码的剖析难度。

    指令替换:对代码中的运算表达式进行等效转换,使其在罕用反编译工具中,进步破解者逆向剖析门槛,无效的爱护外围算法的原始逻辑。

    局部变量名称混同:对源代码中的变量名称进行做混同操作,混同后变量名称变成无任何意义的名称。这给剖析者加大了剖析强度。

    符号混同:对App利用中的类名称、函数名称进行混同操作,增大间接用工具剖析难度,让反编译逆向工具,无奈间接通过类名称、函数名称进行疾速定位App的外围代码。

    混同多样化:采纳在混同过程中引入随机性技术,在雷同的混同策略下,每次混同后的代码均不统一,进一步晋升攻击者通过利用工具进行动态剖析的难度。

    不通明谓词:将代码中分支跳转判断条件,由原来的确定值变为表达式,减少程序逻辑的复杂性、升高代码的可读性。

    防动静调试:对App利用进行防调试爱护、检测到配置防动静调试性能的类、办法、函数被IDA逆向工具进行动静调试时候,App利用进行主动退出运行操作,有利于爱护App利用间接被动静调试,从而进步攻防反抗的门槛。

    防动静注入:对App利用进行防动静注入爱护,当利用zygote或ptrace技术进行App利用的注入操作时,App利用进行主动退出运行操作,以此进行进攻攻打方对App利用的非法操作,防止动态分析执行代码,从而达到动静爱护App利用平安。

    HOOK检测:对App进行防HOOK爱护,检测到配置防hook爱护性能的类名、办法名、函数名在被frida、xposed等工具动静hook时候,App进行主动退出操作,以此进行进步进攻App安全性,爱护App不被注入攻打,抵挡歹意侵入。

    代码段测验:对App利用中的代码段进行完整性校验,发现代码段被篡改,App利用进行主动退出运行,避免App利用中的代码逻辑被篡改,以此进行动静爱护App的源代码安全性。

    完整性校验:对App中指定的函数级进行完整性校验,当利用被从新签名和代码的完整性受到毁坏时候,检测点进行触发App程序闪退,以此抵挡支流的调试器调试剖析,从而达到动静爱护程序平安。

    通过实现源到源的虚拟化爱护,增强了源代码的安全性。通过多样化动态防护伎俩,实现控制流混同;字符串加密;符号混同等多样化伎俩全方位爱护代码,大大的进步代码动态逆向能力,让被逆向的代码无奈被了解。通过弱小动静防护伎俩,避免代码逻辑被篡改;爱护源代码反抗支流调试器,防止源代码被动态分析执行,防HOOK技术爱护代码不被注入攻打,进步动静逆向剖析的门槛。

    App加固技术总结

    通过App加固技术不仅能够进步对逆向后的代码浏览难度、而且有利于升高App被破解、插入病毒、木马、后门程序等恶意代码的危险,同时也能加强用户隐衷数据、交易数据的安全性。通过App加固技术,也是为了更好应答国家对App平安合规监管检测的规范,升高App被第三方媒体曝光,从而重大影响企业品牌形象和信用,为企业和开发者的业务失常倒退保驾护航。

    随着加固技术的疾速倒退和攻防反抗技术的疾速迭代,而虚拟机源码爱护技术作为以后支流的加固技术,置信在将来很长一段时间,它会始终占据主导且当先的位置,因为通过对源码的虚拟化爱护,从而可能为App提供足够强度的爱护。

  • 关于app:使用APICloud-AVM框架开发人事档案管理助手app实战

    因为人事档案具备涉密性,所以本利用没有应用后盾服务,全副性能都在APP本地实现。

    开发工具采纳 APICloud Studio3,基于VSCode的(PS:比基于Atom的autio2好用太多);

    数据库采纳sqllite,没有应用UI框架,集体感觉AVM自身反对的flex布局配合自写CSS款式,齐全能够实现市面上所有的UI框架的元素,这个取决于集体功力。

    一、我的项目思维脑图

    二、性能介绍

    1、人员花名册

    2、编制状况

    3、集体核心

    三、技术要点

    手势明码验证,本地数据库操作,语音播报。

    用到的模块

    我的项目文件目录

    援用一下官网的对于目录构造的介绍

    四、性能开发详解

    1、首页导航

    零碎首页应用tabLayout,能够将相干参数配置在JSON文件中,再在config.xml中将content的值设置成该JSON文件的门路。如果底部导航没有非凡需要这里强烈建议大家应用tabLayout为APP进行布局,官网曾经将各类手机屏幕及不同的分辨率进行了适配,免去了很多对于适配方面的问题。

    app.json文件内容,对于json文件的命名是没有限度的,我习惯用app。

    {
        "name": "root",
        "textOffset": 6,
        "color": "#999999",
        "selectedColor": "#006aff",
        "scrollEnabled": false,
        "hideNavigationBar": false,
        "bgColor": "#fff",
        "navigationBar": {
            "background": "#006aff",
            "shadow": "rgba(0,0,0,0)",
            "color": "#fff",
            "fontSize": 18,
            "hideBackButton": true
        },
        "tabBar": {
          "background": "#fff",
          "shadow": "#eee",
          "color": "#5E5E5E",
          "selectedColor": "#006aff",
          "textOffset": 3,
          "fontSize": 11,
          "scrollEnabled": true,
          "index": 1,
          "preload": 0,
          "frames": [
            {
              "title": "编制状况",
              "name": "home",
              "url": "./pages/records/organ"
            },
            {
              "title": "人员花名册",
              "name": "course",
              "url": "./pages/person/organ"
            },
            {
              "title": "集体核心",
              "name": "user",
              "url": "./pages/main/main"
            }
          ],
          "list": [
            {
              "text": "编制",
              "iconPath": "./image/authoried-o.png",
              "selectedIconPath": "./image/authoried.png"
            },
            {
              "text": "人员",
              "iconPath": "./image/person-o.png",
              "selectedIconPath": "./image/person.png"
            },
            {
              "text": "我的",
              "iconPath": "./image/user-o.png",
              "selectedIconPath": "./image/user.png"
            }
          ]
        }
      }

    2、列表显示及分页

    通过上拉刷新和下拉操作,配合JS办法实现分页查问性能。

    <template name='list'>
        <scroll-view scroll-y class="main" enable-back-to-top refresher-enabled refresher-triggered={refresherTriggered} onrefresherrefresh={this.onrefresherrefresh} onscrolltolower={this.onscrolltolower}>
            <view class="item-box">
                <view class="item" data-id={item.id} v-for="(item, index) in personList" tapmode onclick="openTab">
                    <image class="avator" src={item.photo} mode="widthFix"></image>
                    <text class="item-title">{item.name}</text>
                    <text class="item-sub-title">{item.nation}</text>
                </view>
            </view>
            <view class="footer">
                <text class="loadDesc">{loadStateDesc}</text>
            </view>        
        </scroll-view>
    </template>
    <script>
        import $util from "../../utils/utils.js"
        export default {
            name: 'list',    
            data() {
                return{
                    personList:[],
                    skip: 0,
                    refresherTriggered: false,
                    haveMoreData: true,
                    loading: false,
                    organid:0
                }
            },
            computed: {            
                loadStateDesc(){
                    if (this.data.loading || this.data.haveMoreData) {
                        return '加载中...';
                    } else if (this.personList.length > 0) {
                        return '没有更多啦';
                    } else {
                        return '临时没有内容';
                    }
                }
            },
            methods: {
                apiready(){
                    this.data.organid = api.pageParam.id;
                    this.loadData(false);
                    //更换头像
                    api.addEventListener({
                        name: 'setavator'
                    }, (ret, err) => {
                        this.loadData();
                    });
                    //新增人员信息
                    api.addEventListener({
                        name: 'addperson'
                    }, (ret, err) => {
                        this.loadData();
                    });
                    //删除人员信息
                    api.addEventListener({
                        name: 'delperson'
                    }, (ret, err) => {
                        this.loadData();
                    });
                    if(api.getPrefs({sync: true,key: 'role'})=='99'){
                        //增加编辑按钮
                        api.setNavBarAttr({
                            rightButtons: [{
                                text: '新增'
                            }]
                        });
                        //监听右上角按钮点击事件
                        api.addEventListener({
                            name: 'navitembtn'
                        }, (ret, err) => {
                            if (ret.type == 'right') {
                                $util.openWin({
                                    name: 'personadd',
                                    url: 'personadd.stml',
                                    title: '新增人员信息',
                                    pageParam:{
                                        organid:this.data.organid
                                    }
                                });
                            }
                        });
                    }            
                },
                loadData(loadMore) {
                    if (this.data.loading) {
                        return;
                    }
                    api.showProgress();
                    this.data.loading = true;
                    var limit = 15;
                    var skip = loadMore?(this.data.skip+1)*limit:0;
     
                    // console.log('select id,name,grade,sex,nation,photo from authority where organ = '+this.data.organid+' order by id limit '+limit+' offset '+skip);
     
                    var db = api.require('db');
                    db.selectSql({
                        name: 'doc',
                        sql: 'select id,name,grade,sex,nation,photo from authorized where organ = '+this.data.organid+' order by id limit '+limit+' offset '+skip
                    }, (ret, err)=> {
                        // console.log(JSON.stringify(ret));
                        // console.log(JSON.stringify(err));
                        if (ret.status) {
                            let records = ret.data;
                            this.data.haveMoreData = records.length == limit;
                            if (loadMore) {
                                this.data.personList = this.data.personList.concat(records);
                            } else {
                                this.data.personList = records;
                            }
                            this.data.skip = skip;
                        } else {
                            this.data.recordsList = records;
                            api.toast({
                                msg:err.msg
                            })
                        }
                        this.data.loading = false;
                        this.data.refresherTriggered = false;
                        api.hideProgress();
                    });
                },
                /*下拉刷新页面*/
                onrefresherrefresh(){
                    this.data.refresherTriggered = true;
                    this.loadData(false);
                },
                onscrolltolower() {
                    if (this.data.haveMoreData) {
                        this.loadData(true);
                    }
                },
                openTab(e){
                    let id = e.currentTarget.dataset.id;
                    $util.openWin({
                        name: "personinfo",
                        url: 'personinfo.stml',
                        title: '人员信息',
                        pageParam:{
                            id:id
                        }
                    });
                }
            }
        }
    </script>

    3、表单提交

    采纳AVM自带的from控件,通过onsubmit进行数据提交

    4、头像图片上传及base64转码

    因为是本地sqllite数据库,人员头像图片须要转成base64编码存储到数据库中。通过官网模块trans进行图片转码操作。

    <image class="avator" src={this.data.src} mode="widthFix"  onclick="setavator"></image>
     
                setavator(){
                    api.actionSheet({
                        cancelTitle: '勾销',
                        buttons: ['拍照', '关上相册']
                    }, (ret, err) => {
                        if (ret.buttonIndex == 3) {
                            return false;
                        }
                        var sourceType = (ret.buttonIndex == 1) ? 'camera' : 'album';
                        api.getPicture({
                            sourceType: sourceType,
                            allowEdit: true,
                            quality: 20,
                            destinationType:'url'
                        }, (ret, err) => {
                            if (ret && ret.data) {
                                var trans = api.require('trans');
                                trans.decodeImgToBase64({
                                    imgPath: ret.data
                                }, (ret, err) => {
                                    // console.log(JSON.stringify(ret));
                                    // console.log(JSON.stringify(err));
                                    if (ret.status) {
                                        let b64 =  "data:image/jpeg;base64,"+ret.base64Str;
                                        this.data.src = b64;
                                    } else {
                                        api.toast({
                                            msg:'照片上传失败,请从新抉择!'
                                        })
                                    }
                                });
                            }
                        });
                    });
                },

    5、sqllite数据库 db模块

    因为数据库文件须要存储的利用安装文件中,所有须要官网fs模块配合应用来进行数据库文件的操作。

    copyDB(){
                var fs = api.require('fs');
                fs.copyTo({
                    oldPath: 'widget://db/doc.db',
                    newPath: 'fs://db'
                }, function(ret, err) {
                    // console.log(JSON.stringify(ret));
                    // console.log(JSON.stringify(err));
                    if (ret.status) {
                        // console.log(JSON.stringify(ret));
                        api.toast({
                            msg:'拷贝数据库胜利!'
                        })
                    } else {
                        // console.log(JSON.stringify(err));
                        api.toast({
                            msg:JSON.stringify(err)
                        })
                    }
                });
            },
            openDB(){
                var db = api.require('db');
                db.subfile({
                    directory:'fs://db'
                }, (ret, err)=> {
                    // console.log(JSON.stringify(ret));
                    // console.log(JSON.stringify(err));
                    if (ret.status) {
                        // console.log(JSON.stringify(ret));
                        //关上数据库
                        db.openDatabase({
                            name: 'doc',
                            path: ret.files[0]
                        }, (ret, err)=> {
                            // console.log(JSON.stringify(ret));
                            // console.log(JSON.stringify(err));
                            if (ret.status) {
                                // console.log(JSON.stringify(ret));
                                api.toast({
                                    msg:'关上数据库胜利!'
                                })
                            } else {
                                // console.log(JSON.stringify(err));
                                api.toast({
                                    msg:JSON.stringify(err)
                                })
                            }
                        });
                        
                    } else {
                        // console.log(JSON.stringify(err));
                        api.toast({
                            msg:JSON.stringify(err)
                        })
                    }
                });
            },
            closeDB(){
                var db = api.require('db');
                db.closeDatabase({
                    name: 'doc'
                }, function(ret, err) {
                    if (ret.status) {
                        console.log(JSON.stringify(ret));
                        api.toast({
                            msg:'敞开数据库胜利'
                        })
                    } else {
                        // console.log(JSON.stringify(err));
                        api.toast({
                            msg:JSON.stringify(err)
                        })
                    }
                });
            },
            updateDB(){
                var fs = api.require('fs');
                var db = api.require('db');
                db.closeDatabase({
                    name: 'doc'
                }, (ret, err) => {
                    if (ret.status) {
                        //拷贝文件
                        fs.copyTo({
                            oldPath: 'widget://doc.db',
                            newPath: 'fs://db/'
                        }, (ret, err) => {
                            if (ret.status) {
                                 db.subfile({
                                    directory:'fs://db'
                                }, (ret, err)=> {
                                    if(ret.status){
                                        //关上数据库
                                        db.openDatabase({
                                            name: 'doc',
                                            path: ret.files[0]
                                        }, (ret, err)=> {
                                            if (ret.status) {
                                                api.toast({
                                                    msg:'数据库更新胜利!'
                                                })
                                            } else {
                                                api.toast({
                                                    msg:JSON.stringify(err)
                                                })
                                            }
                                        });
                                    }
                                    else{
                                        api.toast({
                                            msg:JSON.stringify(err)
                                        })
                                    }                              
                                })
                            } else {
                                api.toast({
                                    msg:JSON.stringify(err)
                                })
                            }
                        });
                    } else {
                        api.toast({
                            msg:JSON.stringify(err)
                        })
                    }
                }); 
            },

    6、语音播报性能

    采纳官网提供的IFLyVoice模块,须要留神的是,根底资源文件和发音人资源文件(.jet文件)须要到科大讯飞开发者平台进行下载导入的我的项目中。还有对数字的解读不是准确,尤其是年份,最好不要用数字,而是用中文。

    //增加朗诵按钮
                        api.setNavBarAttr({
                            rightButtons: [{
                                text: '朗诵'
                            }]
                        });
                        //监听右上角按钮点击事件
                        api.addEventListener({
                            name: 'navitembtn'
                        }, (ret, err) => {
                            // console.log(JSON.stringify(this.data.info));
                            if (ret.type == 'right') {
                                var IFlyVoice = api.require('IFlyVoice');
                                IFlyVoice.initSpeechSynthesizer((ret)=>{
                                //   console.log(JSON.stringify(ret));
                                });
                                IFlyVoice.startSynthetic({
                                    text:this.data.info,
                                    commonPath_Android:'widget://res/android/common.jet',
                                    pronouncePath_Android:'widget://res/android/xiaoyan.jet',
                                    pronounceName:'xiaoyan',
                                    speed:40
                                },(ret,err)=>{
                                    // console.log(JSON.stringify(ret));
                                    // console.log(JSON.stringify(err));
                                    if (ret.status) {
                                        // console.log('合成胜利');
                                    } else {
                                        // console.log(JSON.stringify(err));
                                    }
                                });
                            }
                        });

    7、手势密码保护

    手势密码保护因为官网模块存在款式问题,及原生模块存在遮罩问题,所以采纳了平台上提供的H5模块。APICloud弱小之处在这里进行了酣畅淋漓的体现,通过AVM及原生模块无奈实现的性能,能够再用H5的形式来实现!牛逼!!!!,通过设置全局变量来记录是否已设置手机明码,每次利用启动通过这个变量来判断是否开启手势密码保护。

    
    this.data.islock =api.getPrefs({sync: true,key: 'islock'});
                    if(this.data.islock=='Y'){
                        api.openFrame({
                            name: 'h5lock',
                            url:'../../html/h5lock.html'
                        })
                    }
                    else{
                        api.toast({
                            msg:'您还没有设置手势明码,为了数据安全,请尽快设置。'
                        })
                    }
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
        <title>H5lock</title>
        <style type="text/css">
            body {
                text-align: center;
                background-color: #000000;
            }
            .title {
                /*color: #87888a;*/
                margin-top: 85px;
                font-size: 20px;
                font-weight:lighter;
            }
        </style>
    </head>
    <body>
    <script type="text/javascript" src="../script/H5lock.js"></script>
    <script type="text/javascript">
        var opt = {
            chooseType: 3, // 3 , 4 , 5,
            width: 300, // lock wrap width
            height: 300, // lock wrap height
            container: 'element', // the id attribute of element
            inputEnd: function (psw){} // when draw end param is password string
        }
        var lock = new H5lock(opt);
        lock.init();
    </script>
    </body>
    </html>
    (function(){
            window.H5lock = function(obj){
                this.height = obj.height;
                this.width = obj.width;
                this.chooseType = Number(window.localStorage.getItem('chooseType')) || obj.chooseType;
                this.devicePixelRatio = window.devicePixelRatio || 1;
            };
     
     
            H5lock.prototype.drawCle = function(x, y) { // 初始化解锁明码面板 小圆圈
                this.ctx.strokeStyle = '#87888a';//明码的点点默认的色彩
                this.ctx.lineWidth = 2;
                this.ctx.beginPath();
                this.ctx.arc(x, y, this.r, 0, Math.PI * 2, true);
                this.ctx.closePath();
                this.ctx.stroke();
            }
            H5lock.prototype.drawPoint = function(style) { // 初始化圆心
                for (var i = 0 ; i < this.lastPoint.length ; i++) {
                    this.ctx.fillStyle = style;
                    this.ctx.beginPath();
                    this.ctx.arc(this.lastPoint[i].x, this.lastPoint[i].y, this.r / 2.5, 0, Math.PI * 2, true);
                    this.ctx.closePath();
                    this.ctx.fill();
                }
            }
            H5lock.prototype.drawStatusPoint = function(type) { // 初始化状态线条
                for (var i = 0 ; i < this.lastPoint.length ; i++) {
                    this.ctx.strokeStyle = type;
                    this.ctx.beginPath();
                    this.ctx.arc(this.lastPoint[i].x, this.lastPoint[i].y, this.r, 0, Math.PI * 2, true);
                    this.ctx.closePath();
                    this.ctx.stroke();
                }
            }
            H5lock.prototype.drawLine = function(style, po, lastPoint) {//style:色彩 解锁轨迹
                this.ctx.beginPath();
                this.ctx.strokeStyle = style;
                this.ctx.lineWidth = 3;
                this.ctx.moveTo(this.lastPoint[0].x, this.lastPoint[0].y);
     
                for (var i = 1 ; i < this.lastPoint.length ; i++) {
                    this.ctx.lineTo(this.lastPoint[i].x, this.lastPoint[i].y);
                }
                this.ctx.lineTo(po.x, po.y);
                this.ctx.stroke();
                this.ctx.closePath();
     
            }
            H5lock.prototype.createCircle = function() {// 创立解锁点的坐标,依据canvas的大小来平均分配半径
     
                var n = this.chooseType;
                var count = 0;
                this.r = this.ctx.canvas.width / (1 + 4 * n);// 公式计算
                this.lastPoint = [];
                this.arr = [];
                this.restPoint = [];
                var r = this.r;
                for (var i = 0 ; i < n ; i++) {
                    for (var j = 0 ; j < n ; j++) {
                        count++;
                        var obj = {
                            x: j * 4 * r + 3 * r,
                            y: i * 4 * r + 3 * r,
                            index: count
                        };
                        this.arr.push(obj);
                        this.restPoint.push(obj);
                    }
                }
                this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
                for (var i = 0 ; i < this.arr.length ; i++) {
                    this.drawCle(this.arr[i].x, this.arr[i].y);
                }
                //return arr;
            }
            H5lock.prototype.getPosition = function(e) {// 获取touch点绝对于canvas的坐标
                var rect = e.currentTarget.getBoundingClientRect();
                var po = {
                    x: (e.touches[0].clientX - rect.left)*this.devicePixelRatio,
                    y: (e.touches[0].clientY - rect.top)*this.devicePixelRatio
                  };
                return po;
            }
            H5lock.prototype.update = function(po) {// 外围变换办法在touchmove时候调用
                this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
     
                for (var i = 0 ; i < this.arr.length ; i++) { // 每帧先把面板画进去
                    this.drawCle(this.arr[i].x, this.arr[i].y);
                }
     
                this.drawPoint('#27AED5');// 每帧花轨迹
                this.drawStatusPoint('#27AED5');// 每帧花轨迹
     
                this.drawLine('#27AED5',po , this.lastPoint);// 每帧画圆心
     
    // if (this.lastPoint.length == 4) {
    //     // debugger
    // }
     
                for (var i = 0 ; i < this.restPoint.length ; i++) {
                    if (Math.abs(po.x - this.restPoint[i].x) < this.r && Math.abs(po.y - this.restPoint[i].y) < this.r) {
                        this.drawPoint(this.restPoint[i].x, this.restPoint[i].y);
                        this.lastPoint.push(this.restPoint[i]);
                        this.restPoint.splice(i, 1);
                        break;
                    }
                }
     
            }
            H5lock.prototype.checkPass = function(psw1, psw2) {// 检测明码
                var p1 = '',
                p2 = '';
                for (var i = 0 ; i < psw1.length ; i++) {
                    p1 += psw1[i].index + psw1[i].index;
                }
                for (var i = 0 ; i < psw2.length ; i++) {
                    p2 += psw2[i].index + psw2[i].index;
                }
                return p1 === p2;
            }
            H5lock.prototype.storePass = function(psw) {// touchend完结之后对明码和状态的解决
     
                if (this.pswObj.step == 1) {
                    if (this.checkPass(this.pswObj.fpassword, psw)) {
                        this.pswObj.step = 2;
                        this.pswObj.spassword = psw;
                        document.getElementById('title').innerHTML = '明码保留胜利';                                   
     
                        this.drawStatusPoint('#2CFF26');
                         this.drawPoint('#2CFF26');
                        window.localStorage.setItem('passwordxx', JSON.stringify(this.pswObj.spassword));
                        window.localStorage.setItem('chooseType', this.chooseType);
     
                    } else {
                        document.getElementById('title').innerHTML = '两次不统一,从新输出';
                        this.drawStatusPoint('red');
                         this.drawPoint('red');
                        delete this.pswObj.step;
                    }
                } else if (this.pswObj.step == 2) {
                    if (this.checkPass(this.pswObj.spassword, psw)) {
                        var title = document.getElementById("title");
                        title.style.color = "#2CFF26";
                        title.innerHTML = '解锁胜利';
     
                        this.drawStatusPoint('#2CFF26');//小点点外圈高亮
                        this.drawPoint('#2CFF26');
                        this.drawLine('#2CFF26',this.lastPoint[this.lastPoint.length-1] , this.lastPoint);// 每帧画圆心
     
                        api.closeFrame();
                        
     
                    } else if (psw.length < 4) {
                        
                        this.drawStatusPoint('red');
                        this.drawPoint('red');
                        this.drawLine('red',this.lastPoint[this.lastPoint.length-1] , this.lastPoint);// 每帧画圆心
     
                        var title = document.getElementById("title");
                        title.style.color = "red";
                        title.innerHTML = '请连贯4个点';
     
                    } else {
                        this.drawStatusPoint('red');
                        this.drawPoint('red');
                        this.drawLine('red',this.lastPoint[this.lastPoint.length-1] , this.lastPoint);// 每帧画圆心
     
     
                        var title = document.getElementById("title");
                        title.style.color = "red";
                        title.innerHTML = '手势明码谬误,请重试';
                    }
                } else {
                    this.pswObj.step = 1;
                    this.pswObj.fpassword = psw;
                    document.getElementById('title').innerHTML = '再次输出';
                }
     
            }
            H5lock.prototype.makeState = function() {
                if (this.pswObj.step == 2) {
                    // document.getElementById('updatePassword').style.display = 'block';
                    //document.getElementById('chooseType').style.display = 'none';
     
                    var title = document.getElementById("title");
                    title.style.color = "#87888a";
                    title.innerHTML = '请解锁';
     
                } else if (this.pswObj.step == 1) {
                    //document.getElementById('chooseType').style.display = 'none';
                    // document.getElementById('updatePassword').style.display = 'none';
                } else {
                    // document.getElementById('updatePassword').style.display = 'none';
                    //document.getElementById('chooseType').style.display = 'block';
                }
            }
            H5lock.prototype.setChooseType = function(type){
                chooseType = type;
                init();
            }
            H5lock.prototype.updatePassword = function(){
                window.localStorage.removeItem('passwordxx');
                window.localStorage.removeItem('chooseType');
                this.pswObj = {};
                document.getElementById('title').innerHTML = '绘制解锁图案';
                this.reset();
            }
            H5lock.prototype.initDom = function(){
                var wrap = document.createElement('div');
                var str = '<h4 id="title" class="title" style="color:#87888a">请绘制您的图形明码</h4>';
     
                wrap.setAttribute('style','position: absolute;top:0;left:0;right:0;bottom:0;');
                var canvas = document.createElement('canvas');
                canvas.setAttribute('id','canvas');
                canvas.style.cssText = 'background-color: #000;display: inline-block;margin-top: 76px;';
                wrap.innerHTML = str;
                wrap.appendChild(canvas);
     
                var width = this.width || 320;
                var height = this.height || 320;
                
                document.body.appendChild(wrap);
     
                // 高清屏锁放
                canvas.style.width = width + "px";
                canvas.style.height = height + "px";
                canvas.height = height * this.devicePixelRatio;
                canvas.width = width * this.devicePixelRatio;
                
     
            }
            H5lock.prototype.init = function() {
                this.initDom();
                this.pswObj = window.localStorage.getItem('passwordxx') ? {
                    step: 2,
                    spassword: JSON.parse(window.localStorage.getItem('passwordxx'))
                } : {};
                this.lastPoint = [];
                this.makeState();
                this.touchFlag = false;
                this.canvas = document.getElementById('canvas');
                this.ctx = this.canvas.getContext('2d');
                this.createCircle();
                this.bindEvent();
            }
            H5lock.prototype.reset = function() {
                this.makeState();
                this.createCircle();
            }
            H5lock.prototype.bindEvent = function() {
                var self = this;
                this.canvas.addEventListener("touchstart", function (e) {
                    e.preventDefault();// 某些android 的 touchmove不宜触发 所以减少此行代码
                     var po = self.getPosition(e);
     
                     for (var i = 0 ; i < self.arr.length ; i++) {
                        if (Math.abs(po.x - self.arr[i].x) < self.r && Math.abs(po.y - self.arr[i].y) < self.r) {
     
                            self.touchFlag = true;
                            self.drawPoint(self.arr[i].x,self.arr[i].y);
                            self.lastPoint.push(self.arr[i]);
                            self.restPoint.splice(i,1);
                            break;
                        }
                     }
                 }, false);
                 this.canvas.addEventListener("touchmove", function (e) {
                    if (self.touchFlag) {
                        self.update(self.getPosition(e));
                    }
                 }, false);
                 this.canvas.addEventListener("touchend", function (e) {
                     if (self.touchFlag) {
                         self.touchFlag = false;
                         self.storePass(self.lastPoint);
                         setTimeout(function(){
     
                            self.reset();
                        }, 1000);
                     }
     
     
                 }, false);
     
                //  document.getElementById('updatePassword').addEventListener('click', function(){
                //      self.updatePassword();
                //   });
            }
    })();

    8、设置手势明码

    登录胜利之后,在集体核心来设置手势明码。可通过参数设置来初始化明码强度。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
        <title>H5lock</title>
        <style type="text/css">
            body {
                text-align: center;
                background-color: #000000;
            }
            .title {
                /*color: #87888a;*/
                margin-top: 85px;
                font-size: 20px;
                font-weight:lighter;
            }
            .reset{
                position: relative;
                top: 200px;
                font-size: 20px;
                text-align: center;
            }
        </style>
    </head>
    <body>
    <script type="text/javascript" src="../script/setH5lock.js"></script>
    <script type="text/javascript">
        var opt = {
            chooseType: 3, // 3 , 4 , 5,
            width: 300, // lock wrap width
            height: 300, // lock wrap height
            container: 'element', // the id attribute of element
            inputEnd: function (psw){} // when draw end param is password string
        }
        var lock = new H5lock(opt);
        lock.init();
    </script>
    </body>
    </html>

    9、批改明码

    零碎默认设定了用户的初始密码,用户登录零碎后会提醒进行明码批改,批改后的明码进行了MD5加密,因为没有后盾零碎,所以明码的MD5加密,采纳了JS来进行加密。通过开发工具调试控制台装置js插件

    装置胜利之后会在文件目录中显示

    而后在用的中央间接引入即可。

    import $md5 from '../../node_modules/js-md5/build/md5.min.js'
     var db = api.require('db');
                        db.executeSql({
                            name: 'doc',
                            sql: "update user set password = '"+ md5(ret.text) +"' where id = 1"
                        }, (ret, err)=> {
                            // console.log(JSON.stringify(ret));
                            // console.log(JSON.stringify(err));
                            if (ret.status) {
                                api.alert({
                                    title: '音讯揭示',
                                    msg: '明码批改胜利,请从新登陆',
                                }, (ret, err) => {
                                    //革除用户信息
                                    api.removePrefs({
                                        key: 'username'
                                    });
                                    api.removePrefs({
                                        key: 'userid'
                                    });
                                    api.removePrefs({
                                        key: 'password'
                                    });    
                                    $util.openWin({
                                        name: 'login',
                                        url: '../main/login.stml',
                                        title: '',
                                        hideNavigationBar:true
                                    });
                                });
                            } else {                        
                                api.toast({
                                    msg:JSON.stringify(err)
                                })
                            }
                        });

    10、封装工具类插件 utils.js

    在须要用到插件中通用办法的中央,间接援用即可。

    import $util from "../../utils/utils.js"
    const $util = {
        openWin(param){
            var param = {
                name: param.name,
                url: param.url,
                title: param.title||'',
                pageParam: param.pageParam||{},
                hideNavigationBar: param.hideNavigationBar || false,
                navigationBar:{
                    background:'#1492ff',
                    shadow: '#fff',
                    color: '#fff'
                }
            };
            if (this.isApp()) {
                api.openTabLayout(param);
            } else {
                api.openWin(param);
            }
        },
        isApp(){
            if (api.platform && api.platform == 'app') {
                return true;
            }
            return false;
        },
        fitRichText(richtext, width){
            var str = `<img style="max-width:${width}px;"`;
            var result = richtext.replace(/\<img/gi, str);
            return result;
        },
        isLogin(){
            if(api.getPrefs({sync: true,key: 'userid'})){
                return true;
            }
            return false;
        },
        openDataBase(){
            var fs = api.require('fs');
            var db = api.require('db');
            db.subfile({
                directory:'fs://db'
            }, (ret, err)=> {
                if(ret.status){
                    //关上数据库
                    db.openDatabase({
                        name: 'doc',
                        path: ret.files[0]
                    }, (ret, err)=> {
                        if (ret.status) {
                            // api.toast({
                            //     msg:'关上数据库胜利!'
                            // })
                        } else {
                            api.toast({
                                msg:JSON.stringify(err)
                            })
                        }
                    });
                }
                else{
                    //拷贝文件
                    fs.copyTo({
                        oldPath: 'widget://doc.db',
                        newPath: 'fs://db/'
                    }, function(ret, err) {
                        if (ret.status) {
                            db.subfile({
                                directory:'fs://db'
                            }, (ret, err)=> {
                                if(ret.status){
                                    //关上数据库
                                    db.openDatabase({
                                        name: 'doc',
                                        path: ret.files[0]
                                    }, (ret, err)=> {
                                        if (ret.status) {
     
                                        } else {
                                            api.toast({
                                                msg:JSON.stringify(err)
                                            })
                                        }
                                    });
                                }
                                else{
                                    api.toast({
                                        msg:JSON.stringify(err)
                                    })
                                }
                            })   
                        } else {
                            api.toast({
                                msg:JSON.stringify(err)
                            })
                        }
                    });
                }
            })
        }
    }
    export default $util;

    11、用户性能权限

    零碎分为2级用户,管理员账号和领导账号。通过角色ID进行辨别,管理员账号有信息的增删改查性能,领导账号只有信息的查问性能。

    用于登录胜利之后将用户信息进行缓存。

    //登陆APP
                submit() {                
                    api.showProgress();
                    // console.log( "select id,username,password from user where username = '"+this.data.user+"' and password = '"+md5(this.data.psw)+"'");
                    var db = api.require('db');
                    db.selectSql({
                        name: 'doc',
                        sql: "select id,username,password,role from user where username = '"+this.data.user+"' and password = '"+md5(this.data.psw)+"'"
                    }, (ret, err)=> {
                        // console.log(JSON.stringify(ret));
                        // console.log(JSON.stringify(err));
                        if (ret.status) {
                            if(ret.data.length==1){
                                api.setPrefs({key:'username',value:ret.data[0].username});
                                api.setPrefs({key:'userid',value:ret.data[0].id});
                                api.setPrefs({key:'password',value:ret.data[0].password});
                                api.setPrefs({key:'role',value:ret.data[0].role});
     
                                api.sendEvent({
                                    name: 'loginsuccess',
                                });
                                api.closeWin();
                            }
                            else{
                                api.toast({
                                    msg:'登陆失败,请输出正确的用户名和明码'
                                })
                            }
                            
                        } else {                        
                            api.toast({
                                msg:JSON.stringify(err)
                            })
                        }
                        api.hideProgress();
                    });
                }

    在须要验证用户权限的中央通过获取角色ID,进行逻辑判断。

    if(api.getPrefs({sync: true,key: 'role'})=='99'){
                        //增加编辑按钮
                        api.setNavBarAttr({
                            rightButtons: [{
                                text: '编辑'
                            }]
                        });
                        //监听右上角按钮点击事件
                        api.addEventListener({
                            name: 'navitembtn'
                        }, (ret, err) => {
                            if (ret.type == 'right') {
                                $util.openWin({
                                    name: 'personedit',
                                    url: 'personedit.stml',
                                    title: '人员信息编辑',
                                    pageParam:{
                                        id:this.data.id
                                    }
                                });
                            }
                        });
                    }
                    else{
                        //增加朗诵按钮
                        api.setNavBarAttr({
                            rightButtons: [{
                                text: '朗诵'
                            }]
                        });
                        //监听右上角按钮点击事件
                        api.addEventListener({
                            name: 'navitembtn'
                        }, (ret, err) => {
                            // console.log(JSON.stringify(this.data.info));
                            if (ret.type == 'right') {
                                var IFlyVoice = api.require('IFlyVoice');
                                IFlyVoice.initSpeechSynthesizer((ret)=>{
                                //   console.log(JSON.stringify(ret));
                                });
                                IFlyVoice.startSynthetic({
                                    text:this.data.info,
                                    commonPath_Android:'widget://res/android/common.jet',
                                    pronouncePath_Android:'widget://res/android/xiaoyan.jet',
                                    pronounceName:'xiaoyan',
                                    speed:40
                                },(ret,err)=>{
                                    // console.log(JSON.stringify(ret));
                                    // console.log(JSON.stringify(err));
                                    if (ret.status) {
                                        // console.log('合成胜利');
                                    } else {
                                        // console.log(JSON.stringify(err));
                                    }
                                });
                            }
                        });
                    }

    12、双击退出应用程序

    利用如果不做任何解决,在利用初始页面登程keyback事件,会弹出提示框提醒是否退出程序,体验感极差。针对此进行了优化,因为利用首页采纳了tablayout,所以只须要在tablayout默认选中项的索引页面中增加双击keyback事件的监听,并通过api.toast进行提醒。

    在登录页面也须要增加此监听,应为用户退出登录之后,会主动跳转至登录页,如果不做解决,用户点击物理返回键就会导致用户在没有登录的状况下跳回上一页。加了此监听事件用户点击返回键不做解决,双击会提醒退出程序。
     

    //监听返回  双击退出程序
                    api.setPrefs({
                        key: 'time_last',
                        value: '0'
                    });
                    api.addEventListener({
                        name : 'keyback'
                        }, (ret, err) => {
                        var time_last = api.getPrefs({sync: true,key: 'time_last'});
                        var time_now = Date.parse(new Date());
                        if (time_now - time_last > 2000) {
                            api.setPrefs({key:'time_last',value:time_now});
                            api.toast({
                                msg : '再按一次退出APP',
                                duration : 2000,
                                location : 'bottom'
                            });
                        } else {
                            api.closeWidget({
                                silent : true
                            });
                        }
                    });

    13、利用动静权限

    安卓10之后,对利用的权限要求进步,不在像老版本一样配置上就会主动获取,必须进行提醒。

    根据官网给出的教程进行了动静权限的设置。

    增加 mianfest.xml文件

    <?xml version="1.0" encoding="UTF-8"?>
    <manifest>
        <application name="targetSdkVersion" value="28"/>
    </manifest>

    在零碎索引页进行动静权限获取揭示,本零碎只波及到了文件存储权限的获取,如须要获取多个权限,在List[]数组中持续增加须要的权限,而后依据增加的权限个数,做相应的几个判断即可。

    let limits=[];
                    //获取权限
                    var resultList = api.hasPermission({
                        list: ['storage']
                    });
                    if (resultList[0].granted) {
                        // 已受权,能够持续下一步操作
                    } else {
                        limits.push(resultList[0].name);
                    }
                    if(limits.length>0){
                        api.requestPermission({
                            list: limits,
                        }, (res) => {
                            
                        });
                    }
  • 关于app:向工程腐化开炮-proguard治理

    作者:刘天宇(谦风)

    工程腐化是app迭代过程中,一个十分辣手的问题,波及到宽泛而细碎的具体细节,对研发效力&体验、工程&产物品质、稳定性、包大小、性能,都有绝对“荫蔽”而间接的影响。个别不会造成不可接受的阻碍,却时常蹦出来导致“阵痛”,有点像蛀牙或智齿,到了肯定水平不拔不行,但不同的是,工程的腐化很难通过一次性“拔除”来根治,任何一次“拔除”之后,须要无效的可继续治理计划,造成常态化的防腐体系。

    工程腐化拆解来看,是组成app的代码工程中,工程构造自身,以及各类“元素”(manifest、代码、资源、so、配置)的腐化。优酷架构团队近年来,继续在进行思考、实际与治理,并积淀了一些技术、工具、计划。现逐个分类汇总,辅以相干畛域常识解说,整顿成为《向工程腐化开炮》系列技术文章,分享给大家。心愿更多同学,一起退出到与工程腐化的这场持久战中。

    本文为系列文章首篇,将聚焦于java代码proguard,这一细分畛域。对工程腐化,间接开炮!

    在Android(java)开发畛域,个别提到“代码proguard”,是指利用Proguard工具对java代码进行裁剪、优化、混同解决,从而实现无用代码删除(tree-shaking)、代码逻辑优化、符号(类、变量、办法)混同。proguard处理过程,对apk构建耗时、产物可控性(运行时稳定性)、包大小、性能,都有重要影响。

    很多时候开发者会用“混同”来代指整个Proguard解决,尽管不精确,但联合语境来了解,只有不产生歧义,也无伤大雅。值得注意的是,google官网曾经在近几年的Android Gradle Plugin中,应用自研的R8工具代替了Proguard工具,来实现上述三个性能。但“代码proguard”的说法,曾经造成惯用语,在本文中除非特地阐明,“代码proguard”就是指处理过程,而非Proguard工具自身。

    基础知识

    本章先简要介绍一些基础知识,不便大家对proguard有一个“框架性”的清晰认知。

    性能介绍

    Proguard的三个外围性能,作用如下:

    • 裁剪(shrink)。通过对所有代码援用关系,进行整体性的动态剖析,检测并移除无用的类、变量、办法、属性。对最终apk的减小,具备重要作用;
    • 优化(optimize)。这是整个Proguard处理过程中,最简单的一部分。通过对代码执行逻辑的深层次剖析,移除无用的代码分支、办法参数、本地变量,对办法/类进行内联,甚至是优化指令汇合,总计蕴含几十项优化项。一方面能够升高代码大小占用,另一方面,也是最为重要的,是可能升高运行时办法执行耗时;
    • 混同(obfuscate)。通过缩短类、变量、办法名称的形式,升高代码大小占用,对最终apk的减小,同样具备重要作用。同时,也是减少apk防破解难度的一个高级技术计划。

    上述三个处理过程,shrink和optimize交替进行,依据配置能够循环屡次(R8不可配置循环次数)。一个典型的Proguard处理过程如下:


    Proguard处理过程

    其中,app classes包含application工程、sub project工程、内部依赖aar/jar、local jar、flat dir aar中的所有java代码。library classes则包含android framework jar、legacy jars等仅在编译期须要的代码,运行时由零碎提供,不会打包到apk中。

    配置项

    Proguard提供了弱小的配置项,对整个处理过程进行定制。在这里,将其划分为全局性配置,以及keep配置两类。留神,R8为了放弃处理过程的统一可控性,以及更好的解决成果,勾销了对大部分全局性配置的反对。

    全局性配置

    全局性配置,是指影响整体处理过程的一些配置项,个别又能够分为以下几类:

    1、裁剪配置

    • -dontshrink。指定后,敞开裁剪性能;
    • -whyareyoukeeping。指定指标类、变量、办法,为什么被“keep住”,而没有在apk中被裁剪掉。留神,R8和Proguard给出的后果含意并不相同。来直观看下比照:
    # 示例:类TestProguardMethodOnly被keep规定间接“keep住”,TestProguardMethodOnly中的一个办法中,调用了TestProguardFieldAndMethod类中的办法。
    
    # Proguard给出的后果,是最短门路,即如果多个keep规定/援用导致,只会给出最短门路的信息
    Explaining why classes and class members are being kept...
    
    com.example.myapplication.proguard.TestProguardMethodOnly
      is kept by a directive in the configuration.
    
    com.example.myapplication.proguard.TestProguardFieldAndMethod
      is invoked by    com.example.myapplication.proguard.TestProguardMethodOnly: void methodAnnotation() (13:15)
      is kept by a directive in the configuration.
    # 后果解读: 
    # 1. “is kept by a directive in the configuration.”,TestProguardMethodOnly是被keep规定间接“keep住”
    # 2. “is invoked by xxxx",TestProguardFieldAndMethod是被TestProguardMethodOnly调用,导致被“keep住”;“is kept by a directive in the configuration.”,TestProguardMethodOnly被keep规定间接“keep住”
    
    
    # R8给出的后果,是类被哪个keep规定间接命中,即如果类被其余保留下来的类调用,然而没有keep规定间接对应此类,那么此处给出的后果,是“Nothing is keeping xxx"
    com.example.myapplication.proguard.TestProguardMethodOnly
    |- is referenced in keep rule:
    |  /Users/flyeek/workspace/code-lab/android/MyApplication/app/proguard-rules.pro:55:1
    Nothing is keeping com.example.myapplication.proguard.TestProguardFieldAndMethod
    # 后果解读: 
    # 1. “is referenced in keep rule: xxx”,TestProguardMethodOnly是被具体的这一条规定间接“keep住”。不过,如果有多条规定均“keep住”了这个类,在此处只会显示一条keep规定。
    # 2. “Nothing is keeping xxxx",TestProguardFieldAndMethod没有被keep规定间接“keep住”

    2、优化配置

    • -dontoptimize。指定后,敞开优化性能;
    • -optimizationpasses。优化次数,实践上优化次数越多,成果越好。一旦某次优化后无任何成果,将进行下一轮优化;
    • -optimizations。配置具体优化项,具体可参考Proguard文档。上面是顺手找的一个proguard处理过程log,大家感触下优化项:


    优化(optimize)项展现

    • 其它。包含-assumenosideeffects、-allowaccessmodification等,具体可参考文档,不再详述;

    3、混同配置

    • -dontobfuscate。指定后,敞开混同性能;
    • 其它。包含-applymapping、-obfuscationdictionary、-useuniqueclassmembernames、dontusemixedcaseclassnames等若干配置项,用于精细化管制混同处理过程,具体可参考文档。

    keep配置

    绝对于全局配置,keep配置大家最相熟和罕用,用来指定须要被保留住的类、变量、办法。被keep规定间接命中,进而保留下来的类,称为seeds(种子)。

    在这里,咱们能够思考一个问题:如果apk构建过程中,没有任何keep规定,那么代码会不会全副被裁剪掉?答案是必定的,最终apk中不会有任何代码。可能有同学会说,我用Android Studio新建一个app工程,开启了Proguard然而没有配置任何keep规定,为什么最终apk中会蕴含一些代码?这个是因为Android Gradle Plugin在构建apk过程中,会主动生成一些混同规定,对于所有keep规定的起源问题,在前面的章节会讲到。

    好了,持续回到keep配置上来。keep配置反对的规定非常复杂,在这里将其分为以下几类:

    1、间接保留类、办法、变量;

    • -keep。被保留类、办法、变量,不容许shrink(裁剪),不容许obfuscate(混同);
    • -keepnames。等效于-keep, allowshrinking。保留类、办法、变量,容许shrink,如果最终被保留住(其它keep规定,或者代码调用),那么不容许obfuscate;

    2、如果类被保留(未裁剪掉),则保留指定的变量、办法;

    • -keepclassmembers。被保留的变量、办法,不容许shrink(裁剪),不容许obfuscate(混同);
    • -keepclassmembernames。等效于-keepclassmembers, allowshrinking。被保留的变量、办法,容许shrink,如果最终被保留住,那么不容许obfuscate;

    3、如果办法/变量,均满足指定条件,则保留对应类、变量、办法;

    • -keepclasseswithmembers。被保留类、办法、变量,不容许shrink(裁剪),不容许obfuscate(混同);
    • keepclasseswithmembernames。等效于-keepclasseswithmembers, allowshrinking。被保留类、办法、变量,容许shrink,如果最终被保留住,那么不容许obfuscate。

    残缺keep规定格局如下,感触下复杂度:

     -keepXXX [,modifier,...] class_specification
     
     # support modifiers:
     includedescriptorclasses
     includecode
     allowshrinking
     allowoptimization
     allowobfuscation
     
     # class_specification format:
     [@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
        [extends|implements [@annotationtype] classname]
    [{
        [@annotationtype]
        [[!]public|private|protected|static|volatile|transient ...]
        <fields> | (fieldtype fieldname [= values]);
    
        [@annotationtype]
        [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...]
        <methods> | <init>(argumenttype,...) | classname(argumenttype,...) | (returntype methodname(argumenttype,...) [return values]);
    }]
    
    # 此外,不同地位均反对不同水平的通配符,不详述.

    在理论工作中,个别不会用到非常复杂的keep规定,所以残缺用法不用刻意学习,遇到时可能通过查文档看懂即可。举一个比拟有意思的例子,来完结本大节。

    ===================== 示例 =====================
    # 示例类:
    package com.example.myapplication.proguard;
    public class TestProguardFieldOnly {
        public static String fieldA;
        public int fieldB;
    }
    
    package com.example.myapplication.proguard;
    public class TestProguardMethodOnly {
        public static void methodA() {
            Log.d("TestProguardClass", "void methodA");
        }
    }
    
    package com.example.myapplication.proguard;
    public class TestProguardFieldAndMethod {
        public int fieldB;
    
        public static void methodA() {
            Log.d("TestProguardClass", "void methodA");
        }
    }
    
    # keep规定:
    -keepclasseswithmembers class com.example.myapplication.proguard.** {
        *;
    }
    
    # 问题:上述这条keep规定,会导致哪几个示例类被“保留”?
    # 答案:TestProguardFieldOnly和TestProguardFieldAndMethod

    辅助文件

    这里要讲的辅助文件,是指progaurd生成的一些文件,用于理解处理结果,对排查裁剪、混同相干问题很有帮忙(必要)。


    辅助文件

    配置项汇合

    配置项汇合,汇总了所有配置信息,并对某些配置进行“开展”。因为配置项能够在多个文件、多个工程中定义(前面会讲到所有起源),因而配置项汇合不便咱们对此集中查看。

    通过配置项-printconfiguration <filepath>关上此项输入,例如-printconfiguration build/outputs/proguard.cfg会生成${application工程根目录}/build/outputs/proguard.cfg文件,示例内容如下:

    keep后果(seeds.txt)

    keep后果,是对keep规定间接“保留”类、变量、办法的汇总。留神,被其它保留办法调用,导致间接“保留”的类、变量、办法,不在此后果文件中。

    通过配置项-printseeds <filepath>关上此项输入,例如-printseeds build/outputs/mapping/seeds.txt会生成${application工程根目录}/build/outputs/mapping/seeds.txt文件,示例内容如下:

    com.example.libraryaar1.proguard.TestProguardConsumerKeep: void methodA()
    com.example.myapplication.MainActivity
    com.example.myapplication.MainActivity: MainActivity()
    com.example.myapplication.MainActivity: void openContextMenu(android.view.View)
    com.example.myapplication.R$array: int planets_array
    com.example.myapplication.R$attr: int attr_enum

    裁剪后果(usage.txt)

    裁剪后果,是对被裁剪掉类、变量、办法的汇总。

    通过配置项-printusage <filepath>关上此项输入,例如-printusage build/outputs/mapping/usage.txt会生成${application工程根目录}/build/outputs/mapping/usage.txt文件,示例内容如下:

    androidx.drawerlayout.R$attr
    androidx.vectordrawable.R
    androidx.appcompat.app.AppCompatDelegateImpl
        public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
        public boolean hasWindowFeature(int)
        public void setHandleNativeActionModesEnabled(boolean)

    留神,如果类被残缺裁剪,只列出类的全限定名;如果类没有被裁剪,而是类中的变量、办法被裁剪,此处会先列出类名称,再列出被裁剪掉的变量、办法。

    混同后果(mapping.txt)

    裁剪后果,是对被混同类、变量、办法的汇总。

    通过配置项-printmapping <filepath>关上此项输入,例如-printmapping build/outputs/mapping/mapping.txt会生成${application工程根目录}/build/outputs/mapping/mapping.txt文件,示例内容如下:

    ===================== Proguard示例:列出被保留的所有类,以及混同后果 =====================
    com.example.myapplication.MyApplication -> com.example.myapplication.MyApplication:
        void <init>() -> <init>
    com.example.myapplication.proguard.TestProguardAndroidKeep -> com.example.myapplication.proguard.TestProguardAndroidKeep:
        int filedA -> filedA
        void <init>() -> <init>
        void methodA() -> methodA
        void methodAnnotation() -> methodAnnotation
    com.example.myapplication.proguard.TestProguardAnnotation -> com.example.myapplication.proguard.TestProguardAnnotation:
    com.example.myapplication.proguard.TestProguardFieldAndMethod -> com.example.myapplication.proguard.a:
        void methodA() -> a
    com.example.myapplication.proguard.TestProguardInterface -> com.example.myapplication.proguard.TestProguardInterface:
        void methodA() -> methodA
    com.example.myapplication.proguard.TestProguardMethodOnly -> com.example.myapplication.proguard.TestProguardMethodOnly:
        void <init>() -> <init>
        void methodAnnotation() -> methodAnnotation
    
    ===================== R8示例:仅列出被保留,且被混同的类、变量、办法 =====================
    # compiler: R8
    # compiler_version: 1.4.94
    # min_api: 21
    com.example.libraryaar1.LibraryAarClassOne -> a.a.a.a:
        void test() -> a
    com.example.libraryaar1.R$layout -> a.a.a.b:
    com.example.libraryaar1.R$styleable -> a.a.a.c:
    com.example.myapplication.proguard.TestProguardFieldAndMethod -> a.a.b.a.a:
        void methodA() -> a

    Proguard和R8的输入内容,以及格局,有一些差别。在理论解读时,须要留神。

    工程利用

    在对proguard基础知识,具备一个整体“框架性”认知后,接下来看看在理论工程中,为了更好的应用proguard,须要理解到的一些事项。本节不会讲述最根底的应用形式,这些能够在官网文档和各类文章中很容易找到。

    工具抉择

    首先,看看有哪些工具能够抉择。对于Android开发畛域,有Proguard和R8两个工具可供选择(很久以前还有一个AGP – Android Gradle Plugin内置的代码裁剪工具,齐全过期,不再列出),其中后者是google官网自研的Proguard工具替代者,在裁剪和优化的解决耗时,以及解决成果上,都比Proguard工具要好。二者的一些比照如下:

    尽管R8不提供全局性的处理过程管制选项,然而提供了两种模式:

    • 失常模式。optimize(优化)策略与Proguard尽可能放弃最大水平的兼容性,个别app能够较平滑的从Proguard切换到R8失常模式;
    • 残缺模式。在优化策略上,采纳了更激进的计划,因而绝对于Proguard,可能须要额定的keep规定来保障代码可用性。开启形式为在gradle.properties文件中,减少配置:android.enableR8.fullMode=true。

    在可用性上,R8曾经达到比拟成熟的状态,倡议还在应用proguard的app,尽快将切换R8打算提上日程。不过,须要留神的是,即便是失常模式,R8的优化策略与progaurd还是存在肯定差别,因而,须要进行全面的回归验证来提供品质保障。

    自定义配置

    后面讲了很多对于配置项的内容,在具体的工程中,如何减少自定义配置规定呢?大部分同学应该都会感觉,这个问题简略的不能再简略,那咱们换一个问题,最终参加到处理过程的配置,都来自于哪里?

    AAPT生成的混同规定,来看几个示例,有助于大家理解哪些keep规定曾经被主动增加进来,毋庸手动解决:

    # Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/build/intermediates/merged_manifests/fullRelease/AndroidManifest.xml:28
    -keep class com.example.myapplication.MainActivity { <init>(); }
    # Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/build/intermediates/merged_manifests/fullRelease/AndroidManifest.xml:21
    -keep class com.example.myapplication.MyApplication { <init>(); }
    # Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/library-aar-1/build/intermediates/packaged_res/release/layout/layout_use_declare_styleable1.xml:7
    -keep class com.example.libraryaar1.CustomImageView { <init>(...); }
    
    # Referenced at /Users/flyeek/workspace/code-lab/android/MyApplication/app/src/main/res/layout/activity_main.xml:9
    -keepclassmembers class * { *** onMainTextViewClicked(android.view.View); }

    能够看到layout中onClick属性值对应的函数名称,无奈被混同,同时会生成一条容易导致适度keep的规定,因而在理论代码中,不倡议这种应用形式。

    对于子工程/内部模块中携带的配置,须要特地留神,如果不审慎解决,会带来意想不到的后果。

    治理实际

    后面两章,对proguard的基础知识,以及工程利用,进行了相干解说,置信大家曾经对proguard造成了初步的整体认知。因为配置项起源宽泛,尤其是consumerProguard机制的存在,导致依赖的内部模块中可能携带“问题”配置项,这让配置项难以整体管控。此外,keep配置与指标代码拆散,代码删除后,keep配置非常容易被保留下来。在工程实际中,随着app一直迭代,会遇到以下两类问题:

    • 全局性配置,被非预期批改。是否混同、是否裁剪、优化次数、优化类型等一旦被批改,会导致代码产生较大变动,影响稳定性、包大小、性能;
    • keep配置,一直减少,逐步腐化。keep规定数量,与构建过程中proguard耗时,成非线性反比(去除无用/冗余 keep规定,能够进步构建速度)。过于宽泛的keep规定,会导致包大小减少,以及代码无奈被优化,进而影响运行时性能。

    “工欲善其事,必先利其器”,在理论动手治理前,别离进行了检测工具的开发。基于工具提供的检测后果,别离发展治理工作。(本文波及工具,均属于优酷自研「onepiece检测剖析套件」的一部分)

    全局配置

    全局配置检测能力(工具),提供proguard全局性配置检测能力,并基于白名单机制,对指标配置项的值,与白名单不统一状况,及时感知。同时,提供选项,当全局性配置产生非预期变动时,终止构建过程,并给出提醒。

    当存在与白名单不统一的全局配置时,生成的检测后果文件中,会列出不统一的配置项,示例内容如下:

    * useUniqueClassMemberNames
    |-- [whitelist] true
    |-- [current] false
    
    * keepAttributes
    |-- [whitelist] [Deprecated, Signature, Exceptions, EnclosingMethod, InnerClasses, SourceFile, *Annotation*, LineNumberTable]
    |-- [current] [Deprecated, Signature, Exceptions, EnclosingMethod, InnerClasses, SourceFile, AnnotationDefault, *Annotation*, LineNumberTable, RuntimeVisible*Annotations]

    通过这个检测能力,实现了对要害全局性配置的爱护,从而无效防止非预期变动产生(当然,坑都是踩过的,不止一次…)。

    keep 配置

    keep配置的治理,则要艰难很多。以对最终apk影响来看,keep配置能够划分为以下四类:

    • 无用规定。对最终处理结果,齐全没有任何影响。换句话讲,如果一条keep规定,不与任何class匹配,那么这条规定就是无用规定;
    • 冗余规定。一条规定的keep成果,齐全能够被已有的其它一条或多条规定所蕴含。这会导致不必要的配置解析,以及处理过程耗时减少(每一条keep规定,都会拿来与所有class进行匹配);
    • 适度规定。超过必要的keep范畴,将不必要类、变量、办法进行了保留。在这里,也包含原本只须要keepnames,然而却间接keep的状况;
    • 精准规定。遵循最小保留准则的必要规定。无需解决,然而须要留神的是,app中的自研业务代码,尽量应用support或androidX中提供的@keep注解,做到keep规定与代码放在一起。

    上述前三类规定,都属于治理指标,现从剖析、解决、验证三个维度,来比拟这三类规定的难度。


    keep规定治理难度比照

    1、剖析

    • 无用。通过将每条keep规定,与每个class进行匹配,即可确定是否对此class有“影响”。这个匹配的难度,次要来自于keep规定的复杂度,以及与proguard的匹配后果保持一致;
    • 冗余。如果是一条规定,成果齐全被其它规定所“蕴含”,这种能够先计算每条keep规定对每个class的影响,最初再找出“保留”范畴雷同,或具备“蕴含”关系,实践上能够实现。然而对于一条规定,被另外多条规定“蕴含”时,检测复杂度会变得很高;
    • 适度。这个根本无奈精准检测,因为哪些类、变量、办法应该被保留,原本就须要通过“运行时被如何应用”进行判断。如果适度规定能够被检测,那么所有keep规定实践上也无需手动增加;

    2、解决

    • 无用。间接删除即可;
    • 冗余。删除其中一条或多条规定,或者合并几条规定;
    • 适度。减少限定词、改写规定等。须要对预期成果有清晰的意识,以及keep规定的熟练掌握;

    3、验证

    • 无用。对最终裁剪、混同后果,无任何影响。验证辅助文件中的「裁剪后果」、「混同后果」即可,为了进一步确认影响,也能够比照验证apk自身;
    • 冗余。和无用规定一样,都是对处理结果无影响,验证形式也统一;
    • 适度。对最终裁剪、优化、混同后果,都有影响。须要通过性能回归的形式进行验证。

    在工具开发上,实现了一个辅助定位性能,以及三个检测能力:

    1、【辅助】模块蕴含keep规定列表。每个模块蕴含的keep规定,不便查看每一条keep规定的起源。

    project:app:1.0
    |-- -keepclasseswithmembers class com.example.myapplication.proguard.** { * ; }
    |-- -keepclassmembers class com.example.myapplication.proguard.** { * ; }
    |-- -keep class com.example.libraryaar1.CustomImageView { <init> ( ... ) ; }
    |-- -keep class com.example.myapplication.proguard.**
    |-- -keepclasseswithmembers class * { @android.support.annotation.Keep <init> ( ... ) ; }
    
    project:library-aar-1:1.0
    |-- -keep interface * { <methods> ; }

    2、【检测】keep规定命中类检测。每个keep规定,命中哪些类,以及这些类所属模块。

    * [1] -keep class com.youku.android.widget.TextSetView { <init> ( ... ) ; }    // 这是keep规定,[x]中的数字,示意keep规定命中模块的数量
    |-- [1] com.youku.android:moduleOne:1.21.407.6     // 这是keep命中模块,[x]中的数字,示意模块中被命中类的数量
    |   |-- com.youku.android.widget.TextSetView    // 这是模块中,被命中的类
    
    * [2] -keep public class com.youku.android.vo.** { * ; }
    |-- [32] com.youku.android:ModuleTwo:1.2.1.55
    |   |-- com.youku.android.vo.MessageSwitchState$xxx
    |   |-- com.youku.android.vo.MessageCenterNewItem$xxxx
    ......
    |-- [14] com.youku.android:ModuleThree:1.0.6.47
    |   |-- com.youku.android.vo.MCEntity
    |   |-- com.youku.android.vo.NUMessage
    |   |-- com.youku.android.vo.RPBean$xxxx
    ......

    3、【检测】类被keep规定命中检测。每个class(以及所属模块),被哪些keep规定命中。绝对于-whyareyoukeeping,本检测聚焦类被哪些keep规定间接“影响”。

    * com.youku.arch:ModuleOne:2.8.15   // 这个是模块maven坐标
    |-- com.youku.arch.SMBridge    // 这个是类名称,以下为命中此类的keep规定列表
    |   |-- -keepclasseswithmembers , includedescriptorclasses class * { native <methods> ; }
    |   |-- -keepclasseswithmembernames class * { native <methods> ; }
    |   |-- -keepclasseswithmembers class * { native <methods> ; }
    |   |-- -keepclassmembers class * { native <methods> ; }
    |-- com.youku.arch.CFixer
    |   |-- -keepclasseswithmembers , includedescriptorclasses class * { native <methods> ; }
    |   |-- -keepclasseswithmembernames class * { native <methods> ; }
    |   |-- -keepclasseswithmembers class * { native <methods> ; }
    |   |-- -keepclassmembers class * { native <methods> ; }

    4、【检测】无用keep规定检测。哪些keep规定未命中任何类。

    * -keep class com.youku.android.NoScrollViewPager { <init> ( ... ) ; }
    * -keep class com.youku.android.view.LFPlayerView { <init> ( ... ) ; }
    * -keep class com.youku.android.view.LFViewContainer { <init> ( ... ) ; }
    * -keep class com.youku.android.view.PLayout { <init> ( ... ) ; }
    * [ignored] -keep class com.youku.android.view.HAListView { <init> ( ... ) ; }
    * -keep class com.youku.android.CMLinearLayout { <init> ( ... ) ; }
    * [ignored] -keepclassmembers class * { *** onViewClick ( android.view.View ) ; }  // 当某条keep规定位于ignoreKeeps配置中时,会加上[ignored]标签

    此外,还提供了「裁剪后果」、「混同后果」的比照剖析工具,便于对无用/冗余keep规定的清理后果,进行验证。

    ===================== 裁剪后果比照 =====================
    *- [add] android.support.annotation.VisibleForTestingNew
    *- [delete] com.youku.arch.nami.tasks.refscan.RefEdge
    *- [delete] com.example.myapplication.R$style
    *- [modify] com.youku.arch.nami.utils.elf.Flags
    |   *- [add] private void testNew()
    |   *- [delete] public static final int EF_SH4AL_DSP
    |   *- [delete] public static final int EF_SH_DSP
    
    ===================== 混同后果比照 =====================
    *- [add] com.cmic.sso.sdk.d.q
    |   *- [add] a(com.cmic.sso.sdk.d.q$a) -> a
    |   *- [add] <clinit>() -> <clinit>
    *- [delete] com.youku.graphbiz.GraphSearchContentViewDelegate
    |   *- [delete] mSearchUrl -> h
    |   *- [delete] <init>() -> <init>
    *- [modify] com.youku.alixplayermanager.RemoveIdRecorderListener ([new]com.youku.a.f : [old]com.youku.b.f)
    *- [modify] com.youku.saosao.activity.CaptureActivity ([new/old]com.youku.saosao.activity.CaptureActivity)
    |   *- [modify] hasActionBar() ([new]f : [old]h)
    |   *- [modify] showPermissionDenied() ([new]h : [old]f)
    *- [modify] com.youku.arch.solid.Solid ([new/old]com.youku.arch.solid.e)
    |   *- [add] downloadSo(java.util.Collection,boolean) -> a
    |   *- [delete] buildZipDownloadItem(boolean,com.youku.arch.solid.ZipDownloadItem) -> a

    优酷主客,治理基线版本,共有3812条keep规定,通过剖析工具,发现其中758条(20%)未命中任何类,属于无用规定。对其中700条进行了清理,并通过比照「裁剪后果」和「混同后果」,确保对最终apk无影响。残余大部分来自于AAPT编译资源时,主动产生的规定,然而资源中援用到的类在apk中不存在,由此导致keep规定无用。想要清理这些规定,须要删除资源中对这些不存在类的援用,临时先加到白名单。

    # layout中援用不存在的class,在apk编译过程中,并不会引发构建失败,但仍然会生成绝对应的keep规定。
    # 这个layout一旦在运行时被“加载“,那么会引发Java类找不到的异样。
    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.example.myapplication.NonExistView
            android:id="@+id/main_textview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"/>
    
    </LinearLayout>
    
    # 生成的keep规定为:-keep class com.example.myapplication.NonExistView { <init> ( ... ) ; }

    对于冗余规定和适度规定,初步进行了小批量试清理,复杂度较高,同时危险难以掌控,先不进行批量清理,后续逐渐清理掉。


    keep规定散布&清理后果

    至此,优酷的残缺release包构建中progurad解决耗时缩小了18%。接下来,一方面在application工程履行中心化管控(优酷禁用了内部模块的consumerProguard),按团队隔离配置文件,并制订keep规定准入机制;另一方面,将无用keep配置作为一个卡口项,在版本迭代过程中部署,进入常态化治理阶段。

    治理全景

    最初,对proguard腐化治理,给出一份全景图:


    Proguard治理全景

    还能做些什么

    工程腐化的其余细分战场,还在进行。对于proguard治理,后续一方面在工具的检测能力上,会针对「冗余keep规定」以及「适度keep规定」,进行一些摸索;另一方面,对存量keep规定的清理,也并非欲速不达,任重而道远,与诸君共勉。

    【参考文档】

    • Proguard官网文档:https://www.guardsquare.com/m…
    • R8官网文档:https://developer.android.com…
    • consumerProguardFiles官网文档:https://developer.android.com…

    关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际&干货给你思考!

  • 关于app:讲一讲应用服务的新鲜事儿

    为了新性能的公布,以及 Linux 和 Windows 的改良,Azure App Service 团队付出了十分多的致力。很开心的是,咱们看到了Windows Containers 的正式版本,并可利用于App Service环境 v3上。此外,面向 Linux 和 Windows 客户的 Azure Monitor Logs 集成也正式公布了!

    正式公布

    ASEv3上的Windows Containers

    Windows Containers为App Service客户提供将Web工作负载迁徙到App Service的扩大选项,这些App Service可能具备更简单的装置和运行时依赖项。例如,开发者能够应用在 Windows 容器中运行的 Web 应用程序出现 HTML 和 PDF 页面。开发人员还能够通过自定义容器的 Dockerfile 来执行工作。在App Service上运行的所有 Windows 容器都应用 Hyper-V 隔离模式,因而客户能够释怀进行硬件级别的隔离。

    App Service与Azure Monitor Logs集成

    App Service与 Azure Monitor Logs的集成现已正式公布,可供跨App Service平台的 Linux 和 Windows 的开发人员应用。
    无关设置 Azure Monitor Logs集成的更多详细信息,请参阅此文章,其中蕴含无关App Service上可用的各种基于文本的日志类型的更多信息。

    其余更新

    新语言版本

    .NET 6 on App Service与 Application Insights 和 App Service’s AppLogs(应用程序日志)齐全集成,为开发人员提供跨 Linux 和 Windows 的弱小可察看性和日志记录性能。
    Node 16 LTS 将不久后在Early Access 中提供,并将齐全反对生产工作负载。 Python 3.9很快就会变成一个齐全内置的语言堆栈,冷启动工夫失去改良。

    Linux App Service的新诊断工具

    App Service为 Linux 上的 .NET 开发人员增加了新工具,以收集 .NET Profiler 跟踪和内存转储。Profiler 跟踪有助于辨认应用程序故障和迟缓的申请,内存转储收集大量信息以进行更深刻的调试和剖析。

    更容易创立Web利用+数据库

    Web 应用程序创立体验更新,开发者只需提供几条信息(Web 利用名称和语言),平台会主动倡议匹配的数据库引擎。而后你只需为数据库服务器命名,平台将会解决其余所有事件。

    主动将 Web 应用程序迁徙到 Azure App Service

    App Service迁徙助手也进行了相应更新,反对将基于 Windows 和 Linux 的 Apache Tomcat 应用程序迁徙到App Service。客户还能够抉择容器化路线,利用 Azure Migrate App Containerization Tool 将 ASP.NET 应用程序迁徙到 Windows 容器,将 Apache Tomcat 应用程序迁徙到 Linux App Service上运行的容器。


    更多服务更新,请扫码浏览原文

  • 关于app:元宇宙带来沉浸式智能登录你学会了吗

    备受资本市场钟爱的元宇宙概念,正掀起一番番波澜。元宇宙作为虚实相融的互联网利用和社会形态,与沉迷式体验严密相干。

    多重门路,打造无感知沉迷式智能登录

    《设计的法令》中曾提及“沉迷”的实质,即让人专一眼前指标,陷入由设计者营造的情景,从而忘却真实世界场景,从而带来虚构的、精力层面的感触和感知。
    就APP登录场景而言,作为用户关上利用的第一步,其绝大部分利用其登录与注册的款式都较为简单,与沉迷式场景心心相印。非浸入式登录页面使登录门路同利用场景,如看电影、社交等,齐全切割,如同正想好好享受一番电影之旅,忽然一个格调齐全不搭干的登录页面硬生生跳进去,打断用户的体验设想,成果可想而知十分差:用户既不会注册登录,场景体验又被打搅。
    在元宇宙大行其道的当下,各类利用的登录场景都应更具沉迷感,沉迷式智能登录成
    为了新命题。友盟+产品经营专家魏怡爽介绍了APP构建沉迷式登录的三大办法:

    其一,场景浸入模式。场景浸入模式作为全屏模式,往往视角较大且没有边框,在耳濡目染间对用户造成该利用没有限度的心理暗示。场景浸入模式往往是滚动、动静的模式。除横滚、纵滚、切屏播放等动静播放设计,还会在利用启动后间接进入浸入式视角,增强用户感知体验。

    其二,应用浸入模式,其往往以横屏或竖屏形式展示,背景采纳动态图片,虽不会像场景式那般活泼,但能够直观地体现该利用外围用处。因而,其通过插入应用场景图片,或者置入局部元素布局,从而高度贴合业务场景,加强用户的交互体验。

    其三,半屏或者弹框登录模式。可能疾速让用户进入沉迷式状态,使得用户在登录时不会跳戏,甚至回顾串联起方才的行为。
    不过,上述门路并非通过吸引用户眼球的形式使用户忘却本人正在登录利用,而是通过门路与一键登录相耦合的形式实现无感知沉迷智能登录,从而优化用户体验。
    所谓一键登录模式,是由运营商开发的一套号码认证流程,用户全程在可知的状况下实现登录。具体来说,当用户被动唤起注册/登录环节进入预取号页面时,预取号页面仅展现掩码号码,并提醒此环节须要受权,当其还是掩码时,开发者并不能获取到相干手机号数据。
    预取号页面必须插入运营商认证服务条款,且条款勾选框默认不勾选,提醒用户仔细阅读服务条款后再决定勾选,当用户被动批准服务条款后,号码认证闭环才得以实现。而后,一键登录按钮高亮,用户被动实现登录环节,开发者能力取得用户登录账号,让C端用户在自主、可知、受权、有选的状况下注册/登录环节,既爱护了用户,也爱护了开发者。
    值得注意的是,在此过程中,用户如不愿应用手机号码认证,仍可跳回其余验证形式,并在此状况下实现注册登录认证,最大水平保障流程的合规性。

    登录模式优劣之辨,谁是最初的赢家


    为什么抉择本机号码认证和一键登录?市面上各种各样的登录形式互有优劣,以账户明码登录模式为例,其作为一种收费的模式,在各大畛域极为罕用。不过,该形式在用户留存率、危险安全性、用户体验以及用户行为追踪方面的利用并不现实。
    不同用户有着本人特有的账户命名习惯,这同开发者治理账号体系的模式并不符合。此外,明码规定的愈发简单,也在肯定水平上折损了用户的体验。开发者也有作为用户因记不起来登录账户明码而放弃登录甚至卸载利用的经验。
    与此同时,账户明码模式也有着较高的平安危险,一旦用户被盗号,找回所消耗工夫、精力对于用户体验是极大的损耗,很可能导致开发者再也无奈触达用户。

    另一方面,社交账户受权登录也是当下较为风行的登录模式。例如用户能够通过微博,微信,支付宝等社交账户第三方登录或受权利用。尽管开发者能够在用户登录利用后获取OpenID或UnionID等标识,用于账号体系搭建,但社交账号登录的唯一性及可追踪性仍比拟差,有时候并不能通过这些信息指向一个惟一的人。
    实际上,用于第三方登录的OpenID或UnionID如果同时也是用户微信账号的话,开发者后续再想追踪用户便会有许多条件限度。比方开发者须要有公众号或订阅号,且用户关注该公众号或订阅号的状况下,能力通过OpenID或UnionID标识用户,所以其实追踪效率与成果并不现实。

    除第三方受权登录外,短信验证作为当下使用量较大的用户登录模式,其实也尤其劣势。短信验证自身基于运营商开发底层能力,通过API即可间接调取,较为便当。但短信验证模式始终须要期待短信收发流程,且短信自身是能够被技术手段窃取的,因而在用户体验与平安危险上仍存在优化空间。

    无论是传统账号密码登录,还是社交帐号受权登录,抑或是短信验证登录模式,均存在各自问题,并不值得举荐。与之相同,本机号码认证和一键登录的模式则较为全面,可能有效应对绝大多数场景问题。
    为何这么说?首先,一般而言单个用户至少领有两个手机号码,所以手机号码唯一性较高,更便于指向惟一的用户。
    其次,本机号码认证和一键登录并不需要用户输出账户明码、期待短信验证,或是跳转多个页面进行受权,而仅需看到页面受权,点击登录即可实现,全程仅一秒,因而其在用户体验与留存率方面体现极佳。
    除此之外,本机号码认证和一键登录两种模式都处于关闭、可控的业务流程,用户只能在其手机上实现该操作,不存在账户窃取行为,因而其平安危险极低,而C端用户实现手机号认证并登录后,开发者便可在非法合规的状况下,通过惟一的手机号追踪用户,因而号码登录毫无疑问是体现最好的一种模式。

    登录场景的“三宜”与“三忌”

    打造一个优良的利用登录流程,除需优良的登录模式外,对页面、流程设计的打磨同样重要。
    对此,从帮忙开发者晋升C端用户体验的角度登程,开发者应做到“三宜”与“三忌”,助力开发者设计本身的登录和注册业务流,从而让用户的注册登录率和留存旋率达到最高。
    “三宜”第一宜,即简化登录流程,保障登录页面跳转数量降至最低,以1-2步为最佳。以某语音社交利用为例,其作为一个零碎利用,启动利用间接拉起注册登录页面,通过此类强登录模式吸引用户注册利用。

    第二宜,即通过场景浸入或应用浸入,置入利用业务场景元素。如教育利用可能会有老师与学生的教学场景,而游览利用置入各地风景名胜素材,从注册/登录页便建设同用户的品牌互动,进而传递利用用处与品牌价值。
    第三宜,则是辨别新老用户。新老用户登录场景实际上存在差别,开发者往往对新用户知之甚少,或新用户登录账号尚未应用过,因而登录流程可能需跳转两个页面,而老用户因为信息齐备,仅需单个页面即可实现登录,从而晋升交互体验,优化用户流失率。

    而第一忌为硬上浸入式动效,导致利用卡顿。开发者在设计浸入式动效时,肯定要思考利用的承受力,因为有的利用在拉取注册/登录页面时可能会有卡顿,相似的不晦涩感会极大侵害用户的应用体验。
    第二忌乃流程繁琐,需用户仔细阅读文字,而不从视觉区别登录层级。第三忌,则是登录需多页面跳转,导致层层散失用户。
    在元宇宙时代行将到来之际,注册/登录页面作为利用最后的端口,对用户登录率、留存率非常重要,而在泥沙俱下的时代,一个好的注册/登录流程设计则能助力利用吸引更多用户,从市场中怀才不遇。
    对此,友盟+作为国内当先的第三方全域数据智能服务商,同开发者深度耦合,推出友盟+智能认证,为开发者提供简略、平安的号码认证登录计划,一站式疾速集成,晋升注册转化率。
    目前友盟+智能认证号码认证成功率居于市场领先水平,服务端成功率更高达99.9%。可用工夫占比以分钟为计,稳定性高达99%,保障服务稳定性的同时助力开发者业务倒退。

    智能认证将参加友盟+年度大促“年末特惠精选流动”,低至78折,欢送开发者关注
    友盟+和智能认证产品官网。理解更多并立刻应用智能认证:https://www.umeng.com/u-verify

    扫一扫退出友盟+ 技术社群
    与超过1000+挪动开发者独特探讨挪动开发最新动静

    欢送点击【友盟+】,理解友盟+ 最新挪动技术
    欢送关注【友盟全域数据】公众号

  • 关于app:App开发者不容错过的大盘点

    上个月刚刚完结的Microsoft Ignite 2021堪称是亮点多多,明天咱们来为所有的App开发者做一个总结梳理。如果你的日常工作是无关利用开发与翻新的,或者对这个方向感兴趣,请跟着咱们一起全面理解一下吧。

    内容亮点

    构建将来的翻新应用程序,咱们须要什么?

    开发者无时不刻地在推动翻新。对于如何把握可能带来影响力的机会,Ashmi首先分享了她的观点。而后,Donovan介绍了他对原生云的定义、微服务架构的益处,并与 Rick 就 DevOps 和 Chaos Engineering 开展了探讨。此次会议还波及如何使.NET和Java应用程序现代化。最初,Amanda Silver演示了在不受语言和框架的限度下,GitHub Actions、Codespaces和Playwright等工具如何助力开发、测试和CI/CD。
    上面的插图阐明了云原生和DevOps的局部,展现了新的Azure Container apps公众预览版,它是一种齐全托管的无服务器容器服务,专为微服务构建,可依据 HTTP 流量、事件或长时间运行的后台作业动静扩大。

    对于容器和无服务器的翻新,在Azure上创立微服务应用程序**,大会中演示了Azure Container apps和Azure Kubernetes Service的Open Service Mesh(OSM)add-on,这是一个基于CNCF Envoy我的项目构建的轻量级、可扩大的云原生开源服务网格。

    最新公布

    除了Azure Container apps和Open Service Mesh add-on for AKS,咱们还公布了Azure Communication Services, API 治理, Logic Apps, Azure Web PubSub, Java on Azure容器平台和DevOps.的新性能。

    Azure Communications services发表了两项行将进行的改良,旨在加强跨多个平台的客户体验:Azure Communication服务与 Microsoft Teams 的互操作性,用于匿名会议退出,12 月初正式公布;以及本月晚些时候SMS 短代码性能预览。

    • 在AKS和Azure Red Hat OpenShift(ARO)上运行Java Enterprise Edition(Java EE)应用程序的新解决方案,现已在Azure Marketplace可用。客户能够将Azure Marketplace offers用于Oracle WebLogic Server on AKS以及IBM WebSphere Liberty/Open Liberty on AKS and ARO,在 Azure 上主动供给资源。

    • 开发人员能够通过 Azure Active Directory 和 GitHub Actions 之间的 OpenID Connect 集成来爱护他们在Azure 的部署。还公布了 DevOps Workflow Generator 工具,以帮忙客户可视化他们的整个 DevOps 工具链并找到改良它的办法。


    更多对于Azure Application公布和停顿的视频和文章,欢送扫码查阅!