
百度外卖用户端测试团队负责人,负责团队技术发展规划以及项目质量保障。负责设计并开发自动化测试框架与工具,帮助团队提升测试的质量和效率。深入理解持续集成体系建设,对质量体系有深刻的理解和积累。
查看内存使用情况使用adb dumpsys 命令 adb shell dumpsys meminfo 其中,package_name 也可以换成程序的pid,pid可以通过 adb shell top | grep app_name 来查找,下图是滴滴主端的内存使用情况 应用级内存 didi@bogon ~ adb shell dumpsys meminfo com.sdu.didi.psnger Applications Memory Usage (in Kilobytes): Uptime: 180975 Realtime: 180975 ** MEMINFO in pid 7914 [com.sdu.didi.psnger] ** Pss Private Private SwapPss Heap Heap Heap Total Dirty Clean Dirty Size Alloc Free ------ ------ ------ ------ ------ ------ ------ Native Heap 32081 31972 92 294 59904 41985 17918 Dalvik Heap 26859 26764 0 318 27257 16354 10903 Dalvik Other 6354 6348 0 0 Stack 1424 1424 0 0 Ashmem 1558 1544 0 0 Gfx dev 4942 1636 8 0 Other dev 27 0 24 0 .so mmap 13066 384 8904 199 .apk mmap 19677 80 16676 0 .ttf mmap 11 0 0 0 .dex mmap 48628 24 37604 0 .oat mmap 12470 0 3128 0 .art mmap 2754 1872 44 15 Other mmap 2227 8 1256 0 EGL mtrack 41100 41100 0 0 GL mtrack 6984 6984 0 0 Unknown 5939 5924 12 99 TOTAL 227026 126064 67748 925 87161 58339 28821 App Summary Pss(KB) ------ Java Heap: 28680 Native Heap: 31972 Code: 66800 Stack: 1424 Graphics: 49728 Private Other: 15208 System: 33214 TOTAL: 227026 TOTAL SWAP PSS: 925 Objects Views: 121 ViewRootImpl: 1 AppContexts: 3 Activities: 1 Assets: 4 AssetManagers: 3 Local Binders: 75 Proxy Binders: 35 Parcel memory: 37 Parcel count: 131 Death Recipients: 1 OpenSSL Sockets: 7 SQL MEMORY_USED: 945 PAGECACHE_OVERFLOW: 263 MALLOC_SIZE: 62 DATABASES pgsz dbsz Lookaside(b) cache Dbname 4 88 135 16/40/7 /storage/emulated/0/Android/data/com.sdu.didi.psnger/files/im/im_database_282680000258050.db 4 28 45 171/107/4 /data/user/0/com.sdu.didi.psnger/databases/dns_record.db 4 32 84 10/24/7 /data/user/0/com.sdu.didi.psnger/databases/ad 4 20 37 46/22/5 /data/user/0/com.sdu.didi.psnger/databases/location_info.db 4 24 41 5/19/2 /data/user/0/com.sdu.didi.psnger/databases/download_file.db 4 100 149 58/44/25 /data/user/0/com.sdu.didi.psnger/databases/DIDI_DATABASE 4 20 19 0/23/2 /data/user/0/com.sdu.didi.psnger/databases/audio_record_2 4 12 0/0/0 (attached) temp 4 20 56 3/15/3 /data/user/0/com.sdu.didi.psnger/databases/audio_record_2 (1) 重点关注如下几个字段: (1) 私有(Clean and Dirty)内存 进程独占的内存,也就是应用进程销毁时系统可以直接回收的内存容量。 通常来说,“private dirty”内存是其最重要的部分,因为只被自己的进程使用。它只在内存中存储,因此不能做分页存储到外存(Android不支持swap)。 所有分配的Dalvik堆和本地堆都是“private dirty”内存;Dalvik堆和本地堆中和Zygote进程共享的部分是共享dirty内存。 (2) Total 的 PSS 信息 实际使用内存,这是另一种应用内存使用的计算方式,这个值就是我们应用真正占据的内存大小。 PSS会把跨进程的共享页也计算在内。任何独占的内存页直接计算它的PSS值,而和其它进程共享的页则按照共享的比例计算PSS值。例如,在两个进程间共享的页,计算进每个进程PPS的值是它的一半大小。 PSS计算方式的一个好处是:把所有进程的PSS值加起来就可以确定所有进程总共占用的内存。这意味着用PSS来计算进程的实际内存使用、进程间对比内存使用和总共剩余内存大小是很好的方式。 通常来说,只需关心Pss Total列和Private Dirty列就可以了。在一些情况下,Private Clean列和Heap Alloc列也会提供很有用的信息。 代码示例 /** * 获取进程内存Private Dirty数据 * * @param context * @param pid * 进程ID * @return nativePrivateDirty、dalvikPrivateDirty、 TotalPrivateDirty */ public static long[] getPrivDirty(Context context, int pid) { ActivityManager mAm = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); int[] pids = new int[1]; pids[0] = pid; MemoryInfo[] memoryInfoArray = mAm.getProcessMemoryInfo(pids); MemoryInfo pidMemoryInfo = memoryInfoArray[0]; long[] value = new long[3]; // Natvie Dalvik Total value[0] = pidMemoryInfo.nativePrivateDirty; value[1] = pidMemoryInfo.dalvikPrivateDirty; value[2] = pidMemoryInfo.getTotalPrivateDirty(); return value; } /** * 获取进程内存PSS数据 * * @param context * @param pid * @return nativePss、dalvikPss、TotalPss */ public static long[] getPSS(Context context, int pid) { long[] value = new long[3]; // Natvie Dalvik Total if (pid >= 0) { int[] pids = new int[1]; pids[0] = pid; ActivityManager mAm = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); MemoryInfo[] memoryInfoArray = mAm.getProcessMemoryInfo(pids); MemoryInfo pidMemoryInfo = memoryInfoArray[0]; value[0] = pidMemoryInfo.nativePss; value[1] = pidMemoryInfo.dalvikPss; value[2] = pidMemoryInfo.getTotalPss(); } else { value[0] = 0; value[1] = 0; value[2] = 0; } return value; } 获取手机总内存和可用内存信息 “/proc/meminfo”文件记录了android手机的一些内存信息,通过读取文件”/proc/meminfo”的信息能够获取手机Memory的总量。 # cat /proc/meminfo cat /proc/meminfo MemTotal: 94096 kB 所有可用RAM大小。 MemFree: 1684 kB LowFree与HighFree的总和,被系统留着未使用的内存。 Buffers: 16 kB 用来给文件做缓冲大小 Cached: 27160 kB 被高速缓冲存储器(cache memory)用的内存的大小(等于diskcache minus SwapCache)。 SwapCached: 0 kB 被高速缓冲存储器(cache memory)用的交换空间的大小。已经被交换出来的内存,仍然被存放在swapfile中,用来在需要的时候很快的被替换而不需要再次打开I/O端口。 Active: 35392 kB 在活跃使用中的缓冲或高速缓冲存储器页面文件的大小,除非非常必要,否则不会被移作他用。 Inactive: 44180 kB 在不经常使用中的缓冲或高速缓冲存储器页面文件的大小,可能被用于其他途径。 Active(anon): 26540 kB Inactive(anon): 28244 kB Active(file): 8852 kB Inactive(file): 15936 kB Unevictable: 280 kB Mlocked: 0 kB SwapTotal: 0 kB 交换空间的总大小。 SwapFree: 0 kB 未被使用交换空间的大小。 Dirty: 0 kB 等待被写回到磁盘的内存大小。 Writeback: 0 kB 正在被写回到磁盘的内存大小。 AnonPages: 52688 kB 未映射页的内存大小。 Mapped: 17960 kB 设备和文件等映射的大小。 Slab: 3816 kB 内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗。 SReclaimable: 936 kB 可收回Slab的大小。 SUnreclaim: 2880 kB 不可收回Slab的大小(SUnreclaim+SReclaimable=Slab)。 PageTables: 5260 kB 管理内存分页页面的索引表的大小。 NFS_Unstable: 0 kB 不稳定页表的大小。 Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 47048 kB Committed_AS: 1483784 kB VmallocTotal: 876544 kB VmallocUsed: 15456 kB VmallocChunk: 829444 kB 要获取android手机总内存大小,只需读取”/proc/meminfo”文件的第1行,并进行简单的字符串处理即可。 代码示例 /** * 获取空闲内存和总内存拼接字符串 * * @return 总内存 */ public static String getFreeAndTotalMem() { long[] memInfo = getMemInfo(); return Long.toString(memInfo[1] + memInfo[2] + memInfo[3]) + "M/" + Long.toString(memInfo[0]) + "M"; } /** * 获取内存信息:total、free、buffers、cached,单位MB * * @return 内存信息 */ public static long[] getMemInfo() { long memInfo[] = new long[4]; try { Class<?> procClazz = Class.forName("android.os.Process"); Class<?> paramTypes[] = new Class[] { String.class, String[].class, long[].class }; Method readProclines = procClazz.getMethod("readProcLines", paramTypes); Object args[] = new Object[3]; final String[] memInfoFields = new String[] { "MemTotal:", "MemFree:", "Buffers:", "Cached:" }; long[] memInfoSizes = new long[memInfoFields.length]; memInfoSizes[0] = 30; memInfoSizes[1] = -30; args[0] = new String("/proc/meminfo"); args[1] = memInfoFields; args[2] = memInfoSizes; if (null != readProclines) { readProclines.invoke(null, args); for (int i = 0; i < memInfoSizes.length; i++) { memInfo[i] = memInfoSizes[i] / 1024; } } } catch (Exception e) { e.printStackTrace(); } return memInfo; } 总结 1)Pss/SharedDirty/Private Dirty三列是读取了/proc/process-id/smaps文件获取的,可以通过adb shell cat /proc/process-id/smaps来查看(需要root)。这是个普通的linux文件,描述了进程的虚拟内存区域的具体信息。 2)Native HeapSize/Alloc/Free三列是使用C函数mallinfo得到的。 3)Dalvik HeapSize/Alloc/Free并非该cpp文件产生,而是android的Debug类生成。 meminfo结果详细分析 Pss对应的TOTAL值:内存所实际占用的值。 Dalvik Heap Size:从RuntimetotalMemory()获得,DalvikHeap总共的内存大小。 Dalvik HeapAlloc:RuntimetotalMemory()-freeMemory() ,Dalvik Heap分配的内存大小。 Dalvik Heap Free:从RuntimefreeMemory()获得,DalvikHeap剩余的内存大小。 Dalvik Heap Size 约等于Dalvik HeapAlloc+ Dalvik HeapFree。 Cursor:/dev/ashmem/Cursor Cursor消耗的内存(KB)。 Ashmem:/dev/ashmem,匿名共享内存用来提供共享内存通过分配一个多个进程可以共享的带名称的内存块。 Other dev:/dev/,内部driver占用的在 “Otherdev”。 .so mmap:C 库代码占用的内存。 .jar mmap:Java 文件代码占用的内存。 .apk mmap:apk代码占用的内存。 .ttf mmap:ttf 文件代码占用的内存。 .dex mmap:Dex 文件代码占用的内存。 Other mmap:其他文件占用的内存。 私有(Clean and Dirty)内存: 进程独占的内存。也就是应用进程销毁时系统可以直接回收的内存容量。通常来说,“private dirty”内存是其最重要的部分,因为只被自己的进程使用。它只在内存中存储,因此不能做分页存储到外存(Android不支持swap)。所有分配的Dalvik堆和本地堆都是“private dirty”内存;Dalvik堆和本地堆中和Zygote进程共享的部分是共享dirty内存。 实际使用内存 (PSS): 这是另一种应用内存使用的计算方式,把跨进程的共享页也计算在内。任何独占的内存页直接计算它的PSS值,而和其它进程共享的页则按照共享的比例计算PSS值。例如,在两个进程间共享的页,计算进每个进程PPS的值是它的一半大小。PSS计算方式的一个好处是:把所有进程的PSS值加起来就可以确定所有进程总共占用的内存。这意味着用PSS来计算进程的实际内存使用、进程间对比内存使用和总共剩余内存大小是很好的方式。 通常来说,只需关心Pss Total列和Private Dirty列就可以了。在一些情况下,Private Clean列和Heap Alloc列也会提供很有用的信息。下面是一些应该查看的内存分配类型(行中列出的类型): Dalvik Heap: 应用中Dalvik分配使用的内存。Pss Total包含所有的Zygote分配(如上面PSS定义所描述的,共享跨进程的加权)。Private Dirty是应用堆独占的内存大小,包含了独自分配的部分和应用进程从Zygote复制分裂时被修改的Zygote分配的内存页。注意:新平台版本有Dalvik Other这一项。Dalvik Heap中的Pss Total和Private Dirty不包括Dalvik的开销,例如即时编译(JIT)和垃圾回收(GC),然而老版本都包含在Dalvik的开销里面。 Heap Alloc: 是应用中Dalvik堆和本地堆已经分配使用的大小。它的值比Pss Total和Private Dirty大,因为进程是从Zygote中复制分裂出来的,包含了进程共享的分配部分。 .so mmap和.dex mmap: mmap映射的.so(本地) 和.dex(Dalvik)代码使用的内存。Pss Total 包含了跨应用共享的平台代码;Private Clean是应用独享的代码。通常来说,实际映射的内存大小要大一点——这里显示的内存大小是执行了当前操作后应用使用的内存大小。然而,.so mmap 的private dirty比较大,这是由于在加载到最终地址时已经为本地代码分配好了内存空间。 Unknown: 无法归类到其它项的内存页。目前,这主要包含大部分的本地分配,就是那些在工具收集数据时由于地址空间布局随机化(Address Space Layout Randomization ,ASLR)不能被计算在内的部分。和Dalvik堆一样, Unknown中的Pss Total把和Zygote共享的部分计算在内,Unknown中的Private Dirty只计算应用独自使用的内存。 TOTAL: 进程总使用的实际使用内存(PSS),是上面所有PSS项的总和。它表明了进程总的内存使用量,可以直接用来和其它进程或总的可以内存进行比较。Private Dirty和Private Clean是进程独自占用的总内存,不会和其它进程共享。当进程销毁时,它们(特别是Private Dirty)占用的内存会重新释放回系统。Dirty内存是已经被修改的内存页,因此必须常驻内存(因为没有swap);Clean内存是已经映射持久文件使用的内存页(例如正在被执行的代码),因此一段时间不使用的话就可以置换出去。 ViewRootImpl: 进程中活动的根视图的数量。每个根视图与一个窗口关联,因此可以帮助确定涉及对话框和窗口的内存泄露。 AppContexts和Activities: 当前驻留在进程中的Context和Activity对象的数量。可以很快的确认常见的由于静态引用而不能被垃圾回收的泄露的 Activity对象。这些对象通常有很多其它相关联的分配,因此这是追查大的内存泄露的很好办法。 注意:View 和 Drawable 对象也持有所在Activity的引用,因此,持有View 或 Drawable 对象也可能会导致应用Activity泄露。
总体CPU 获取CPU信息思路 Android系统是基于Linux内核的,所以系统文件的结构和Linux下一样,系统总体CPU使用信息放在/proc/stat文件下,/proc/cpuinfo文件存放CPU的其它信息,包括CPU名称,直接读取即可。 通过proc获取CPU信息: Linux CPU 九元组参数解析(单位:jiffies): (jiffies是内核中的一个全局变量,用来记录自系统启动一来产生的节拍数,在linux中,一个节拍大致可理解为操作系统进程调度的最小时间片,不同linux内核可能值有不同,通常在1ms到10ms之间) user 从系统启动开始累计到当前时刻,处于用户态的运行时间,不包含 nice值为负进程。 nice 从系统启动开始累计到当前时刻,nice值为负的进程所占用的CPU时间 system 从系统启动开始累计到当前时刻,处于核心态的运行时间 idle 从系统启动开始累计到当前时刻,除IO等待时间以外的其它等待时间 iowait 从系统启动开始累计到当前时刻,IO等待时间(since 2.5.41) irq 从系统启动开始累计到当前时刻,硬中断时间(since 2.6.0-test4) softirq 从系统启动开始累计到当前时刻,软中断时间(since 2.6.0-test4) 可以每1s获取一次CPU信息,分析整机CPU占用率。总的cpu时间totalCpuTime = user + nice + system + idle + iowait + irq + softirq + stealstolen +guest 计算方法 1、 采样两个足够短的时间间隔的Cpu快照,分别记作t1,t2,其中t1、t2的结构均为: (user、nice、system、idle、iowait、irq、softirq、stealstolen、guest)的9元组; 2、 计算总的Cpu时间片totalCpuTime a) 把第一次的所有cpu使用情况求和,得到s1; b) 把第二次的所有cpu使用情况求和,得到s2; c) s2 - s1得到这个时间间隔内的所有时间片,即totalCpuTime = s2 - s1 ; 3、计算空闲时间idle idle对应第四列的数据,用第二次的idle - 第一次的idle即可 idle = idle2 - idle1 4、计算cpu使用率 CPU总使用率(%) = 100*((totalCputime2- totalCputime1)-(idle2-idle1))/(totalCputime2-totalCputime1) 示例代码 public static long getTotalCpuTime() { // 获取系统总CPU使用时间 String[] cpuInfos = null; BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader( new FileInputStream("/proc/stat")), 1000); String load = reader.readLine(); cpuInfos = load.split(" "); } catch (IOException ex) { ex.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } long totalCpu = Long.parseLong(cpuInfos[2]) + Long.parseLong(cpuInfos[3]) + Long.parseLong(cpuInfos[4]) + Long.parseLong(cpuInfos[6]) + Long.parseLong(cpuInfos[5]) + Long.parseLong(cpuInfos[7]) + Long.parseLong(cpuInfos[8]); return totalCpu; } 应用级CPU 单个应用CPU监控 Emmagee是将选中应用的PID传入,读取/proc/PID/stat文件信息及可获取该PID对应程序的CPU信息。 计算方法 1、首先获取应用的进程id: adb shell ps | grep com.package | awk '{print $2}' > tmp 2、根据进程id,通过proc获取CPU信息 while read line; do adb shell cat /proc/$line/stat | awk '{print $14,$15,$16,$17}' >> appcpu0; done < tmp 说明:以下只解释对我们计算Cpu使用率有用相关参数(14-17列) 参数解释 pid 进程号 utime 该任务在用户态运行的时间,单位为jiffies stime 该任务在核心态运行的时间,单位为jiffies cutime 所有已死线程在用户态运行的时间,单位为jiffies cstime 所有已死在核心态运行的时间,单位为jiffies 结论:进程的总Cpu时间processCpuTime = utime + stime + cutime + cstime,该值包括其所有线程的cpu时间。 之后可以每1s获取一次CPU信息,分析获得app的CPU占用率等信息 单个程序的CPU使用率(%) = 100*(processCpuTime2-processCpuTime1)/(totalCpuTime2-totalCpuTime1) 示例代码 public static long getAppCpuTime(int pid) { // 获取应用占用的CPU时间 String[] cpuInfos = null; BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader( new FileInputStream("/proc/" + pid + "/stat")), 1000); String load = reader.readLine(); cpuInfos = load.split(" "); } catch (IOException ex) { ex.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } long appCpuTime = Long.parseLong(cpuInfos[13]) + Long.parseLong(cpuInfos[14]) + Long.parseLong(cpuInfos[15]) + Long.parseLong(cpuInfos[16]); return appCpuTime; } }
一、前言 首先是启动appium,由于多台真机设备的测试,当然是要用到多个appium,其实对于多设备用appium做并发自动化测试,为了解决冲突,无非是解决两个问题 a、设备udid向appium发送以识别是哪台设备要做自动化测试b、appium启动所占用的端口 其实a的话有尝试过做指定设备的自动化测试就知道,b的话无非是appium用到的服务端口(默认4723),对应还有android端的bootstrap的端口以及iOS端的webdriveragent的转发端口,关于端口问题,在appium 1.6.5之后都是没问题的。 二、准备 iOS需要准备可正常build & test 的wda 若要在iOS真机执行,需要提前安装真机执行所需依赖 三、执行 【iOS】 appium -p 4723 --webdriveragent-port 8102 --device-name f899b567337e8eb4505ccad03752e00f56809ac8 appium -p 4725 --webdriveragent-port 8100 --device-name bd07a036d51bad5c0b7269f3f1f6adc83149f177 --webdriveragent-port 就是webdriveragent的端口转发的指定端口,比如在iOS端上的webdriveragent启动服务后默认是手机ip:8100,那你本地就可以通过一个如8101的端口去映射手机的8100端,这样就能做到访问手机上的webdriveragent 顺带提一下一般appium是用自己目录下面的webdirveragent来build的,所以在此之前需要去里面添加证书和重命名包名,不然build不成功就不可行了 【Android】 appium -p 4723 -bp 8201 -U 68de2f65 appium -p 4724 -bp 8202 -U 81CEBMJ2379J
导语 在开发过程中,功能不仅要满足业务需求,也要关注功能对App性能带来的一些问题。开发人员在开发阶段检测性能比较容易,iOS端可以直接通过instruments工具进行检测。但是在测试阶段,测试人员要检测性能需要下载开发工具成本比较高。如果客户端能够将性能数据上传到服务端并且通过一些界面进行展示,对测试人员来说是一种可以检测性能的比较好的方法。 本文章主要介绍iOS端如何通过代码采集性能数据,其中包括电池数据,CPU数据,内存数据,卡顿数据,流量数据以及冷启动时间等。 电池数据 首先来看一下电池数据,iOS电池数据采集方案主要有以下三种方案:UIDevice,IOKit,越狱。 1、UIDevice:提供了获取设备电池的相关信息,包括当前电池的状态以及电量。获取电池信息之前需要先将 batteryMonitoringEnabled 属性设置为 YES,然后就可以通过 batteryState 和 batteryLevel 获取电池信息。 优点:api简单,易于使用。 缺点:粗粒度,能够采集到的数据较少,不符合需求。 2、IOKit: 是一个iOS 系统的私有框架,它可以被用来获取硬件和设备的详细信息,也是与硬件和内核服务通信的底层框架。通过它可以获取设备电量信息,精确度达到1%。 优点:可以获取较多的电池相关的数据。 缺点:因为要访问私有api,不能通过苹果审核,只能在线下取值。获取到的值是设备的电池数据,无法达到应用级别的数据获取。 3、越狱方案:通过iOSDiagnosticsSupport 私有库,Runtime 拿到 MBSDevice 实例,获取电量日志信息表,日志信息表中包含了 iOS 系统采集的小时级别的耗电量。 优点:可以获取到应用的耗电量。 缺点:获取到的耗电量是以小时为单位的,时间间隔太长,不符合需求。 最后,为了能够采集更多的电池数据,我们选择的方案是通过访问IOKit的私有api获取数据,并且在提交到app store时将这部门代码从包里移除掉,以免影响app的审核结果。 核心代码如下: 通过以上方式可以获取到的数据包括但不限于: 当前充电状态 电量 是否连接USB(支持iOS10以下系统) 是否有电池 最大值 电压 温度(支持iOS10以下系统) CPU数据 iOS的线程技术是基于Mach 线程技术实现的,在 Mach 层中thread_basic_info 结构体提供了线程的基本信息,并且每个线程中包含线程的cpu_usage。获取当前App的占用率就是所有线程的cpu_usage之和。 通常一个 task 包含多个线程,在内核提供了 task_threads API 调用获取指定 task 的线程列表以及线程个数,也就是target_task 任务中的所有线程保存在 act_list 数组中,数组中包含 act_listCnt 个条目。然后可以通过 thread_info API 调用来查询指定线程的信息。 因此,获取当前APP的CPU占用率需要遍历所有线程,将cpu_usage求和。 接下来就是获取当前设备的CPU总占用率。 iOS中CPU状态一般包括CPU_STATE_USER, CPU_STATE_SYSTEM, CPU_STATE_IDLE 和 CPU_STATE_NICE等四种。 1、CPU_STATE_USER:运行在用户态空间或者说是用户进程。 2、CPU_STATE_SYSTEM:在内核空间运行的分配内存、IO操作、创建子进程……等。 3、CPU_STATE_IDLE:空闲状态。 4、CPU_STATE_NICE:用户空间进程的CPU的调度优先级。 因此,除了空闲状态都属于CPU占用状态,因此当前CPU的总使用率为(用户+系统+调度)/(用户+系统+调度+空闲)。通过host_statistics获取host_cpu_load_info结构体数据,该结构体中 cpu_ticks 包含了 CPU 运行时四种不同该状态的时钟脉冲的数量,并且根据这四个不同状态的时间脉冲,计算出CPU的总占用率。 内存数据 获取内存数据同样也可以通过mach_task_basic_info结构获取resident_size值作为当前App已占用的内存大小。 但是在测试中发现,通过该结构体获取的值与Xcode中的内存数据对不上,往往差好几兆甚至好几十兆。因此通过查找资料,有一篇文章介绍通过逆向Xcode来获取Xcode计算内存方法以及结构体。该方法获取到的已占用内存大小与Xcode的值几乎一致,可以作为一个判断标准。具体参考代码如下: 接下来,如果要获取当前设备可以使用的空闲内存,首先要了解iOS系统的内存分配。 Free Memory:未使用的 RAM 容量,随时可以被应用分配使用。 Wired Memory:用来存放内核代码和数据结构,它主要为内核服务,如负责网络、文件系统之类的;对于应用、framework、一些用户级别的软件是没办法分配此内存的。但是应用程序也会对 Wired Memory 的分配有所影响。 Active Memory:活跃的内存,正在被使用或很短时间内被使用过。 Inactive Memory:最近被使用过,但是目前处于不活跃状态。 Purgeable Memory:可以理解为可释放的内存,主要是大对象或大内存块才可以使用的内存,此内存会在内存紧张的时候自动释放掉。 因此,空闲内存看成总内存大小减去 Wired Memory大小,Active Memory大小以及Inactive Memory大小。在32位系统通过这种方式获取空闲内存与Xcode数据作比较误差范围较小,而在64位系统上的数据与Xcode数据一比较误差较大,同样找到一个逆向Xcode获取Xcode的计算内存方法。64位系统获取空闲内存的具体代码如下: 卡顿数据 检测卡顿数据的方式通常有两种:一种是FPS卡顿检测,另一种是主线程卡顿检测。 FPS卡顿检测:检测当前页面的帧率,帧率越高意味着界面越流畅,通过计算丢帧率来检测当前页面的卡顿情况。 主线程卡顿检测:通过开辟一个子线程来监控主线程的RunLoop,当两个状态区域之间的耗时大于阈值时,就记为发生一次卡顿。 一、FPS卡顿检测 目前我们要采集的方主要是基于CADisplayLink以屏幕刷新频率同步绘图的特性,观察屏幕当前帧数的指示器,若帧率少于指定的帧率看成一个FPS卡顿。具体代码如下: 二、主线程卡顿检测 在主线程在Runloop的某个阶段进行长时间的耗时操作,因此主要思路就是开辟一个子线程去计算kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting两个状态区域之间的耗时是否超过某个阀值来断定主线程的卡顿情况。 那么,为什么要用kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting状态进行判定呢?首先要理清楚Runloop的运行机制,以下为RunLoop 顺序: 看完RunLoop顺序,就可以看到处理事件主要有两个时间段 — kCFRunLoopBeforeSources 发送之后与 kCFRunLoopAfterWaiting 发送之后。dispatch_semaphore_t 是一个信号量机制,信号量到达或者超时会继续向下进行。若超时则返回的结果必定不为0,若信号量到达返回的结果为0。利用这个特性我们判断卡顿出现的条件为在信号量发送 kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting后进行了大量的操作,在一段时间内没有再发送信号量,看成超时。也就是主线程长时间的停留在这两个状态上。转换为代码就是判断有没有超时,若超时了,再判断当前停留的状态是不是这两个状态,如果是,就判定为卡顿。具体参考代码如下: 流量数据 流量数据主要统计在当前App内发生的所有网络请求相应的数据大小。首先,先通过facebook提供的sonar框架捕捉app内的所有request和response。在实际的网络请求中 Request 和 Response 不一定是成对的,如果网络断开、或者突然关闭进程,都会导致不成对现象,如果将 Request 和 Response 记录在同一条数据,将会对统计造成偏差。因此request和response分开统计流量。若有对SonarKit框架感兴趣的同学可以直接访问官网进一步了解,官网:https://fbsonar.com/docs/getting-started.html 一、统计Request流量 首先需要了解请求报文的组成,如图: 那么,Request所花费的流量就是将把Line的大小,Header的大小,空格以及Body大小累加的合。 1、Line大小的统计 Line没有可以直接转换成CFNetwork相关数据的私有接口,但是我们很清楚 HTTP 请求报文 Line 部分的组成,因此可以手动计算Line的大小。 2、Header大小统计 通过request.allHTTPHeaderFields 拿到的头部数据是有很多缺失的,并不是完整的数据。同时由于无法直接转换到 CFNetwork 层,所以一直拿不到完整的 Header 数据。缺少的数包括但不限于以下几个字段:Accept,Connection,Host,当前Request的Cookie。由于基本上缺失的都是固定的几个字段,忽略这几个字段对统计的结果影响不大。因此主要针对cookie的数据并且手动大小进行补全。因此总Header的大小可以看成request.allHTTPHeaderFields数据大小加上cookie大小。 3、Body大小统计 最后是body部分,通过resquest.HTTPBody来计算Body大小。这里要注意的地方就是通过 NSURLConnection 发出的网络请求 resquest.HTTPBody 拿到的是 nil。需要通过 HTTPBodyStream 读取 stream 来获取 request 的 Body 大小。 最后,将Line大小,Header大小,Body大小相加就是当前request所话费的流量。 二、统计Response流量 请求报文的组成如下: 那么Response所花费的流量就是将把Status Line的大小,Header的大小,空格以及Body大小累加的合。 1、StatusLine大小 NSURLResponse没有接口能直接获取报文中的 Status Line。因此,最后通过转换到 CFNetwork 相关类拿到了Status Line 的数据后计算它的大小,这其中可能涉及到了读取私有 API,因此需要注意审核问题。 2、Header大小 通过 httpResponse.allHeaderFields拿到 Header 字典,转换成 NSData 计算大小。 3、Body大小 对于 Body 的计算,采用 expectedContentLength 或者去 NSURLResponse 对象的 allHeaderFields 中获取 Content-Length 值,其实都不够准确。Content-Length 只是表示 Body 部分的大小,因此采取直接获取body大小的方式。还有一个需要注意对 gzip 情况进行区别分析。我们知道 HTTP 请求中,客户端在发送请求的时候会带上 Accept-Encoding,这个字段的值将会告知服务器客户端能够理解的内容压缩算法。而服务器进行相应时,会在 Response 中添加 Content-Encoding 告知客户端选中的压缩算法。若Content-Encoding使用了 gzip,则模拟一次 gzip 压缩,再计算字节大小。 冷启动时间 App的冷启动就是,当应用启动时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用, 这个启动方式就叫做冷启动(后台不存在该应用进程)。下面先看看苹果官方文档给的应用的启动时序图,图中可以看到冷启动是一个User taps app icon到Final initialization(applicationDidFinishLaunching: withOptions:)的过程,所以冷启动时间就是从用户唤醒App开始一直到App已启动所消耗的时间。 因此,冷启动时间 =DidLauching时间 - main()函数执行之前的时间。类的+ load方法在main函数执行之前调用,所以我们采取在+ load方法记录开始时间的方案。具体参考代码如下: 当applicationDidFinishLaunching:withOptions:方法执行完毕后,添加一个回调获取AppDidFinishLaunching后的时间。并且将开始时间与load开始时间相减作为应用冷启动时间。 总结 以上介绍了iOS中通过代码采集性能数据的方案,目前还在继续优化采集方案,希望本文章能够帮助大家对iOS性能数据采集的了解。 参考文章: https://fbsonar.com/docs/getting-started.html http://www.cocoachina.com/ios/20170629/19680.html http://www.cocoachina.com/ios/20180606/23691.html https://cloud.tencent.com/developer/article/1006222 https://www.jianshu.com/p/6c10ca55d343 http://ddrccw.github.io/2017/12/30/2017-12-30-reverse-xcode-with-lldb-and-hopper-disassembler/ https://www.jianshu.com/p/8e764d05275b?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
一、移动端性能测试指标性能测试需要收集的指标项包含:页面时长、电量、CPU、内存、流量、包大小。目前阶段主要关注的指标项:页面时长、电量 二、指标收集&分析方法 页面时长:RD跟进所需场景进行埋点并上报Omega进行场景化、链路化统计分析 电量:Android使用batterystatus进行电量收集、使用Battery Historian进行电量数据分析;iOS diagnostics 方案可行性待定 三、电量测试方法 step1:打开App执行性能场景 step2:性能场景执行完毕后,通过adb batterystats 命令获取电量日志 step3:导出电量报告bugreport,借助Battery Historian 进行可视化分析 Battery Historian工具介绍: https://github.com/google/battery-historian 四、电量度量标准 五、性能测试场景 六、测试前提 在测试之前,确保以下测试环境要素都已满足(适用Android/iOS): 七、测试机型 八、测试版本 九、QA人员 十、性能测试报告
前言 起因:在路测阶段有不少同学反映,满电的手机出去路测,回来时已基本没电;也有反馈路测期间手机发烫; 了解原因之前我们先要知道CPU为什么会耗电? CPU在运行复杂度不同的任务是采用调频处理的,当手机处理复杂任务时,频率也会提高,自然对于电量的需求会增加。另外,当APP进程的CPU使用率超过1%的时候,都是耗电比较大的。 可能导致App耗电过快的原因 1、频繁的交互 正常情况下,关闭后台软件 VS 行程中导航的时候手机耗电一定是差别很大的,行程中涉及司乘同现、路线规划、最佳视野、定位上报...等等 再比如,在玩王者荣耀游戏 VS 听音乐的时候,因为玩游戏的时候会和屏幕产生很多的交互,但是听音乐就不会这样,频繁的交互式非常耗电的。 2、动画效果 顺风车场景包含了大量的动效场景,例如,首页发单控件下拉动效、司乘等待页动效、行程中动效...等等 当我们设计交互动画的效果时,调用的都是view或者其子类,比如按钮在点击前是效果1,点击后变成效果2,设置更复杂的动画,此时view的重绘让CPU或GPU不断计算,耗电量同样会增加。 3、布局文件嵌套太多 app的布局文件影响着app展示给用户的效果,当布局过于复杂,布局文件嵌套太多时,布局xml文件越来越繁多,查找、加载这些文件显示时会造成CPU计算加重,也会影响手机耗电。 参考way社区发表的一篇文章:顺风车Android性能优化之View布局优化 (http://way.xiaojukeji.com/article/10784) 4、频繁的网络请求 5、定时任务唤醒CPU 安卓CPU休眠时一种安卓极致省电的一种模式,如果你息屏一段时间,CPU会自动进入休眠。但在某些场景下,比如当订单状态变更时,我们的应用会给你推送通知,当你亮屏打开手机后会看到这条通知,那么它就是唤醒了手机的CPU,而我们知道CPU工作时需要消耗电量,尤其是在频繁唤醒的情况下,或者发送心跳包。 6、频繁切换网络 切换网络往往需要硬件的支持,硬件需要跑起来也是需要电量的,并且数据网络比wifi更加耗电,2G,3G,4G网络耗电都不同。 7、高运算量代码 比如解析json这类耗时时间较长的数据格式,或二进制编码解码等。比如,首页response返回的json体多则几百行,json解析效率主要是解析耗时 8、代码中执行的timer定时器 Android 的 Timer 类可以用来计划需要循环执行的任务,Timer 的问题是它需要用 WakeLock 让 CPU 保持唤醒状态,再加上不恰当的使用WakeLock最终没有合理释放掉,使得系统长时间无法进入休眠,势必导致高耗电 timer实现的源码是用while(true)循环来检测是否到时间点,没到就wait,并且continue 9、传感器 安全需求中的全程开端需要App置于前台(屏幕保持常亮)、端内大部分场景需要GPS请求 10、SD卡读写 11、蓝牙 优化建议 1、减少应用与屏幕的交互 在设计app的时候适当简化用户的操作流程,简化掉可以帮助用户做的,不仅仅是为了省电,也可以提高用户的效率。 2、减少不必要的动画效果 有些复杂的动画效果完全可以省略,采用静态的app启动页,或者是点击事件的交互、页面跳转时就用尽量减少不必要的动画效果。 3、简化布局文件,避免过多的嵌套 4、HTTP请求优化 http请求可以采用gzip压缩减少传输过程中的数据量,app发起http请求和服务端返回的http请求数据都采用gzip压缩 5、json解析 比较流行的有fast jackson,jackson,gson,jackson解析效率相当高,基本是gson的十倍 6、读写优化 尽量减少SD卡读写操作,包括SharedPreferences,能保存在内存中的尽量保存在内存中,不用害怕内存爆掉,内存绝对不会因为多存了几十个变量而溢出的,如果需要保存到SD卡的数据很多,那只能说我们的App在设计实现方面还有问题,建议重新理下App的编码设计或者是功能设计 7、AlarmManager 通过AlarmManager可唤醒设备,但项目中不限制的滥用,也会导致系统被频繁唤醒; 8、尽量使用WiFi testerhome大神说,因为手机数据流量会调用手机上面的一些硬件设备,从而唤醒cpu增加耗电,这个跟开启摄像头,开启gps是一样的,一涉及调用硬件设备的绝对唤醒cpu,一唤醒cpu耗电绝对增加 9、非必要,则不要监听网络广播 10、非必要,则不要使用后台常驻service 虽然我们很清楚耗电的原因,但涉及的因素方方面面,我们无法改变重构底层代码、无法优化整个系统、技术瓶颈、不得不定时发送心跳包…想做好性能也并非易事。但是我们能够做到是尽量减少交互、改善产品交互逻辑、优化动画效果、简化布局…其实我们能够做的还很多。 耗电举例分析 power_profile.xml 功耗配置文件部分配置如下: <device name="Android"> <item name="battery.capacity">3450</item> ... <item name="screen.on">178.708</item> <item name="screen.full">240.790</item> ... </device> 这款手机的电池容量为3450mAh,亮屏时电流为178.708mA,亮度调节到最大时电流为240.790mA。这意味着,如果排除其他耗电影响,仅仅亮屏,该手机可以维持3450mAh178.708mA=19.305h(小时);如果将屏幕亮度调到最大,该手机可以维持3450mAh240.790mA=14.327h(小时)。因此,电池容量越大,手机的续航时间一般更长;在手机重度使用的情况下,耗电会加快。 除了屏幕,手机上需要供电的模块还有很多:CPU、相机、闪光灯、音频、视频、蓝牙、modem、wifi、gps。按单位电流值排一个序的话: 相机(平均1152mAh) > modem(最高604mAh) > wifi(发数据370mAh) > CPU(最高频率290mAh) > 屏幕(最亮240mAh) > 音频(75mAh) > 视频(50mAh) > GPS(49mAh) > 蓝牙(8mAh) 将电池容量分成100隔的话,每隔电就是34.5mAh,既如果一个小时内用了34.5mA的电量,那就会掉一个隔电。参考这些电流值,可以很容易知道:手机拍照是很耗电的,使用数据流量看视频也是很耗电的,重度使用情况下,手机也就能使用2~3个小时。但如果手机一直处于休眠状态,CPU的单位电流值不到2mAh,这样待机个把月也是可以的。所以,一个正常的手机,电池是否耐用,是跟个人的使用习惯相关的。 我们平时分析的功耗问题,是在同等条件下的对比试验,找到异常耗电的原因。譬如:手机的初始条件相似(电池容量、相同的应用等),在同样的环境下放置一个晚上,如果对比机出现明显的掉电异常,就可以通过电量日志找到异常耗电的原因。 参考文献 官方建议优化的一些方法https://developer.android.google.cn/training/monitoring-device-state/index.html 对低电耗模式和应用待机模式进行针对性优化https://developer.android.google.cn/training/monitoring-device-state/doze-standby.html Android 7.0新特性对电池管理进一步加强,一些新的变化可能多对我们现有的业务会造成影响需关注https://developer.android.google.cn/about/versions/nougat/android-7.0-changes.html#perf
终端团队性能指标虽然当前顺风车并无各项性能指标的度量标准, 但我们可以参考有做过移动端性能测试经验的团队。 1、首先关注在性能测试场景中App的总体耗电是否符合基线标准,若符合则pass,否则需进一步定位具体耗电原因 2、秉承先有后优的原则,具体耗电项指标先参照终端团队,在性能的迭代中不断改进指标 Android Vitals 规则符合系统的规则,让系统认为你耗电是正常的。Android P 是通过 Android Vitals 监控后台耗电,所以我们需要符合 Android Vitals 的规则,目前它的具体规则如下: 虽然上面的标准可能随时会改变,但是可以看到,Android 系统目前比较关心后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间 WakeLock 阻止系统后台休眠。 安卓绿色联盟在安卓绿色联盟的会议中,华为公开过他们后台资源使用的“红线”,也可以参考里面的一些规则: 业内规则提炼不同应用监控的事项或者参数都不太一样。 由于每个应用的具体情况都不太一样,我们需要将监控的内容抽象成规则,下面是一些可以用来参考的简单规则:
1、iOS11系统的测试设备 2、连接公司内网 3、通过数据线连接后选择window/Devices and Simulators 4、勾选connect via network 注:设备需设置密码 5、加载完成后移除有线连接 展示以上页面代表无线连接成功,移除有线连接后配置IP与设备一致。 点击设备右键,选择Connect via IP Address,输入与被测试设备一致的IP地址 6、选择xcode/instruments 选择电量 7、数据分析 仪器指示从0到20的等级,如果能源使用等级很高不一定是应用出现问题,可能是应用程序需要更多的能量来执行任务 Foreground App Activity(前台应用的活动):前台应用程序的活动的百分比。 前台运用,图像处理,媒体,其他 cellular in out 蜂窝移动网络下行,上行 Wi-Fi 下行,上行 Graphics(图形):图形活动的百分比。 Sleep/Wake:后台/前台 Brightness:亮度
前言 电池电量耗费的源头实在太多,基本Android 设备上任何一个活动都会引起电池电量的消耗。 目前部分手机有 耗电排行的功能, 能显示出App耗电详情排行。虽然谷歌开放sdk 中并没有公开电量统计的API 或者文档,但因为安全中心->省电优化→耗电排行 中就是通过app 能显示出耗电详情排行,所以虽然未公开API但实则有相关的耗电API。耗电名单在主要记录在BatterySipper里面(在frameworks/base/core 下) 概要 我们平常说的手机耗电量,一般涵盖两个方面:硬件层面的功耗和软件层面的电量。 手机有很多硬件模块:CPU,蓝牙,GPS,显示屏,Wifi,射频(Cellular Radio)等,在手机使用过程中,这些硬件模块可能处于不同的状态,譬如Wifi打开或关闭,屏幕是亮还是暗,CPU运行或休眠。 硬件模块在不同的状态下的耗电量是不同的。Android在进行电量统计时,并不是采用直接记录电流消耗量的方式,而是跟踪硬件模块在不同状态下的使用时间,收集一些可用信息,用来近似的计算出电池消耗量。 应用程序的耗电量由很多部分组成,可能使用了GPS,蓝牙等模块,可能应用程序要求长时间亮屏(譬如游戏、视频类应用)。 一个应用程序的电量统计,可以采用累计应用程序使用所有硬件模块时间这种方式近似计算出来。 举一个例子,假定某个APK的使用了GPS,使用时间用 t 表示。GPS模块单位时间的耗电量用 w 表示,那么,这个APK使用GPS的耗电量就可以按照如下方式计算: 耗电量 = 单位时间耗电量(w) × 使用时间(t) 电量统计服务的代码逻辑涉及到以下android源码: frameworks/base/services/java/com/android/server/SystemServer.java frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java frameworks/base/services/core/java/com/android/server/am/BatteryStatsService.java frameworks/base/core/java/android/os/BatteryStats.java frameworks/base/core/java/com/android/internal/os/BatteryStatsImpl.java frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java frameworks/base/core/res/res/xml/power_profile.xml 本文介绍的电量统计的原理,并不涉及到硬件层面的功耗设计,仅从软件层面围绕以下几个问题进行分析: Android如何启动电量统计服务?电量统计涉及到哪一些硬件模块?如何计算一个应用程序的耗电量?电量统计需要完成哪些具体工作? 耗电分类 系统中将耗电总共分成了五大类:App,Wifi,Bluetooth ,User,Mobile。 电量计算大体可以分为两块:软件App功耗、硬件功耗 核心处理只有两个函数: processAppUsage:应用程序耗电量计算,是指每一个应用程序使用硬件模块所产生的耗电量 processMiscUsage :其他杂项耗电量计算,所谓杂项,其实就是用户比较关心的一大类,包括:待机的耗电量、亮屏的耗电量、通话的耗电量、Wifi的耗电量等 void refreshStats(int statsType, SparseArray asUsers, long rawRealtimeUs,long rawUptimeUs) { // Initialize mStats if necessary. getStats(); ...... //初始化一些PowerCalculato 以及各类时间参数 processAppUsage(asUsers); .... // 记录移动数据流量到mMobilemsppList 中 processMiscUsage(); Collections.sort(mUsageList); .... // 对统计数据做一些去杂和优化 } 电量统计服务的启动过程 电量统计服务是一个系统服务,名字为batterystats,在Android系统启动的时候,这个服务就会被启动,其启动时序如下图所示: 电量统计服务是间接由ActivityManagerService(后文简称AMS)来启动,AMS是Android系统最为基础的服务,进入Android系统后,最优先启动的,就是这类服务。 耗电统计触发时机 Android框架层通过一个名为batterystats的系统服务,实现了电量统计的功能。batterystats获取电量的使用信息有两种方式: 被动(push):有些硬件模块(wifi, 蓝牙)在发生状态改变时,通知batterystats记录状态变更的时间点 主动(pull):有些硬件模块(cpu)需要batterystats主动记录时间点,譬如记录Activity的启动和终止时间,就能计算出Activity使用CPU的时间 收集的信息基本都包含硬件模块的状态和被使用的时间两个维度。为什么仅仅是收集不同硬件模块的使用时间呢? 前面我们说过,手机电压通常是恒定的,耗电量是通过 “单位时间电流量(I) × 使用时间(t)” 来计算,而单位时间电流量是由厂商给定的,定义在power_profile.xml中, 所以,只需要收集不同硬件模块的使用时间,就可以近似的计算出耗电量了 收集信息被组织起来,在内存中的数据结构是由BatteryStats类描述的。 为了能够从不同维度统计耗电量,这个数据结构设计得比较复杂,我们不在这里展开讨论。 记录应用程序中所有Activity从显示状态(Resumed)到消失状态(Paused)的时间,就能够统计应用程序的前台运行时间。Activity状态的切换是由AMS掌控的,因此AMS需要将Activity的状态信息通知给batterystats服务。 除了应用程序前台运行时间,还有很多信息是batterystats服务关注的,包括WakeLock、Sendor、Wifi、Audio、Video等,这些信息的采集方式都会经过以下步骤: 由相应的模块发起状态变更的通知 BatteryStats使用定时器记录起止时间 应用程序可能会使用多个硬件模块,所以,耗电信息收集的策略也被设计得比较复杂,譬如,要使用到很多计时器,就设计出了“计时器池”来提高资源利用率。 电量信息存储 收集到的电量信息,在内存中是由BatteryStats这个类来描述的,Android支持历史电量信息的显示的,如果重新启动Android,那内存中的数据就丢失了, 所以需要把这些信息存储到磁盘上,磁盘上的 /data/system/batterystats.bin 文件中就是电量信息的序列化数据。 batterystats服务启动时,会从 batterystats.bin 这个文件中读取数据,来初始化BatteryStats这个数据结构。 所以,在手机使用的过程中,收集到的电量信息,就会被当作历史信息,不定时的写入到磁盘保存下来,下次batterystats启动时,又会被用到。 软件功耗计算 前面我们提到耗电量是通过计算: 耗电量 = 单位时间的耗电量(w) × 使用时间(t) = 电压(U) × 单位时间电流量(I) × 使用时间(t) 在手机上电压一般是恒定的,所以,计算耗电量只需要知道单位时间电流量即可。有了power_profile.xml这个文件描述的单位时间电流量,再收集硬件模块在不同状态下的使用时间,就能够近似的计算出耗电量了。 软件功耗相关方法 //计算app 消耗的Cpu电量到cpuPowerMah 中 mCpuPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); //计算app 使用的Wakelock电量到wakeLockPowerMah 中 mWakelockPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); // 计算app 使用radio 网络消耗的电量到mobileRadioPowerMah 中 mMobileRadioPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); // 计算app 使用的Wifi电量到wifiPowerMah 中 mWifiPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); // 计算app 使用蓝牙的电量到bluetoothPowerMah 中 mBluetoothPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); // 计算app 使用的Sensor电量到sensorPowerMah 中 mSensorPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); // 计算app 使用camera的电量到cameraPowerMah 中 mCameraPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); // 计算app 使用闪光灯Flashlight 的电量到flashlightPowerMah mFlashlightPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah + sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah + flashlightPowerMah + bluetoothPowerMah; 至此,我们分析了以下两个问题: Android如何启动电量统计服务? Android系统启动 -> AMS启动和注册 -> batterystats启动和注册 Android如何计算耗电量? 并不是直接跟踪电流消耗量,而是采用“单位时间电流量(I)×使用时间(t)”来做近似计算。不同硬件模块的单位时间电流量是需要厂商给定的。 硬件功耗计算 硬件功耗计算函数在:processMiscUsage() private void processMiscUsage() { addUserUsage(); // 多用户中每个用户的耗电量 // 公式:user_power = user_1_powerMah + user_2_powerMah + … + user_n_powerMah; (n为所有的user的总数) addPhoneUsage(); // modem通话耗电量 // 公式:phonePower = (phoneOnPower * phoneOnTimeMs ) / (60 * 60 * 1000); addScreenUsage(); // 屏幕耗电量 // 公式:screenOnPower = screenOnTimeMs * POWER_SCREEN_ON addWiFiUsage(); // wifi耗电量 // wifiPowerMah = ((totalRunningTimeMs - mTotalAppWifiRunningTimeMs) * mWifiPowerOn) / (1000* 60* 60); addBluetoothUsage(); // 蓝牙耗电量 // 公式:bluetoohPower = ((idleTimeMs * mIdleMa) + (rxTimeMs * mRxMa) + (txTimeMs * mTxMa)) / (10006060); addMemoryUsage(); // DDR内存耗电量 // 公式:memoryPower = (mAatRail_1 * timeMs_1 + mAatRail_2 * timeMs_2 + ... + mAatRail_n * timeMs_n) / (1000 * 60 * 60) (mAatRail_n :是该读写速率级别下的功率,timeMs_n:是在mAatRail_n 级别下的时间) addIdleUsage(); // CPU suspend/idle状态下的耗电量(不包括蜂窝数据空闲功耗) // 公式:idlePower = (suspendPowerMaMs + idlePowerMaMs) / (60 * 60 * 1000); if (!mWifiOnly) {//(当只有wifi上网功能的设备时不计算蜂窝数据功耗,如平板,电视等) addRadioUsage(); //移动数据网络的耗电量 } } 电量信息统计服务的统计方式可以简单总结为:耗电量 = 模块耗电功率 * 模块耗电时间,其耗电功率中硬件耗电功率由硬件厂商提供过来的Power_profile.xml 中配置好了,模块耗电时间为系统中各种Timer 计时器来统计的。 电量计算流程及公式图 参考文献1、https://duanqz.github.io/2015-07-21-batterystats-part1#33-%E7%94%B5%E9%87%8F%E8%AE%A1%E7%AE%97
通过执⾏battery命令(不需要root) adb命令获取电量量消耗信息 获取整个设备的电量量消耗信息 获取某个apk的电量量消耗信息 batterystats使用步骤 通过执⾏battery命令(不需要root) 通过 adb shell dumpsys battery,返回结果后有电池的各种信息,其中就包括 level(百分⽐比)adb命令查看电池电量量信息: adb shell dumpsys battery eroqltechn:/ $ dumpsys battery Current Battery Service state: mBootCompleted: true AC powered: false #有线充电器状态 USB powered: true #USB连接状态 Wireless powered: false #无线充电状态 Max charging current: 0 #最大充电电流,单位微安(uA) Max charging voltage: 0 #最大充电电压,单位微伏(uV) Charge counter: 0 status: 2 #充电状态,UNKNOWN=1,CHARGING=2,DISCHARGING=3, NOT_CHARGING=4,FULL=5 health: 2 #电池健康状态:只有数字2表示goodpresent: true #电池是否安装在机身 level: 93 #电量: 百分比 scale: 100 #满电100% voltage: 4265 #电池电压 temperature: 289 #电池温度,单位是0.1摄氏度technology: Li-ion #电池种类batterySWSelfDischarging: falsebatteryMiscEvent: 0 mSecPlugTypeSummary: 2 LED Charging: true LED Low Battery: true current now: 217 #电流值,负数表示正在充电charge counter: 0 Adaptive Fast Charging Settings: true USE_FAKE_BATTERY: false SEC_FEATURE_BATTERY_SIMULATION: false FEATURE_WIRELESS_FAST_CHARGER_CONTROL: true mWasUsedWirelessFastChargerPreviously: false mWirelessFastChargingSettingsEnable: true BatteryInfoBackUp mSavedBatteryAsoc: 88 mSavedBatteryMaxTemp: 429 mSavedBatteryMaxCurrent: 1746 mSavedBatteryUsage: 6447 FEATURE_SAVE_BATTERY_CYCLE: true adb命令获取电量量消耗信息 获取整个设备的电量量消耗信息 adb shell dumpsys batterystats | more 获取某个apk的电量量消耗信息 adb shell dumpsys batterystats com.sdu.didi.psnger | more 由于输出信息太多,可使⽤用命令more 或者 less 分篇查看 输出信息如下(由于篇幅, 只粘贴部分) heroqltechn:/ $ dumpsys batterystats com.sdu.didi.psnger Discharge step durations: #0: +1h43m34s304ms to 92 (screen-off, power-save-off, device- idle-on) #1: +1h54m26s635ms to 93 (screen-off, power-save-off, device- idle-on) #2: +1h59m33s225ms to 94 (screen-off, power-save-off, device- idle-on) #3: +1h58m56s325ms to 95 (screen-off, power-save-off, device- idle-on) #4: +2h2m44s341ms to 96 (screen-off, power-save-off, device-idle- on) #5: +2h2m20s111ms to 97 (screen-off, power-save-off, device-idle- on) #6: +1h46m1s361ms to 98 (screen-off, power-save-off, device-idle- on) Estimated screen off time: 8d 0h 17m 12s 800ms Estimated screen off device idle time: 8d 0h 17m 12s 800ms Daily stats: Current start time: 2019-01-22-04-46-42 Next min deadline: 2019-01-23-01-00-00 Next max deadline: 2019-01-23-03-00-00 Current daily steps: Discharge total time: 7d 19h 32m 18s 800ms (from 3 steps) Discharge screen off time: 7d 19h 32m 18s 800ms (from 3 steps) Discharge screen off device idle time: 7d 19h 32m 18s 800ms (from 3 steps) Daily from 2019-01-21-11-43-46 to 2019-01-22-04-46-42: Discharge total time: 8d 3h 3m 13s 700ms (from 3 steps) Discharge screen off time: 8d 3h 3m 13s 700ms (from 3 steps) Discharge screen off device idle time: 8d 3h 3m 13s 700ms (from 3 steps) Charge total time: 6h 35m 21s 800ms (from 18 steps) 也可以将上述命令标准输出到⼀一个⽂文件,来进⾏行行分析。 windows : > xxx.txt Mac/Linux: > xxx.txt 将获得的数据转换为可视化的html⽂文件 命令:python historian.py xxx.txt > xxx.html 关于电量,还可以通过battery-historian⼯工具来获取。https://github.com/google/battery-historian batterystats使用步骤 第一步:清除手机电量消耗历史情况(连接手机) adb shell dumpsys batterystats -enable full-wake-history =====打开全量日志记录 adb shell dumpsys batterystats --reset =====清空电量数据 第二步:设计场景测试(注:不链接手机)\ 第三步:导出测试数据 adb shell dumpsys batterystats > d:/batterystat.txt =======生成TXT文件(导出到本地) adb shell dumpsys batterystats > /sdcard/batterystat.txt =======生成TXT文件(导出到SD卡) exit-----退出shell命令
两种版本分析模式: Historian V2 模式分析 1、纵坐标: 重要参数:wake_lock、plugged、battery_level、screen 具体说明: 1.1.battery_level:电量,可以看出电量的变化。 比如图中的数据显示出当前的电量为17%。 1.2.plugged:充电状态,这一栏显示是否进行充电,以及充电的时间范围。 大图中反应了在某一时间插入了数据线,然后一直持续到数据采集结束。 1.3.screen:屏幕是否点亮。 这一点,可以用于考虑睡眠状态和点亮状态下电量的使用信息。 1.4.top app:该栏显示当前时刻哪个app处于最上层,就是当前手机运行的app。 用来判断某个app对手机电量的影响,这样也能判断出该app的耗电量信息。该栏记录了应用在某一个时刻启动,以及运行的时间,这对我们比对不同应用对性能的影响有很大的帮助。 1.5.Userspace wakelock:在Android的运行机制里,当手机空闲时会进入到休眠状态。而wakeloack的作用就是禁止系统进入休眠,硬件保持高能耗运行从而可以实现关屏唤醒等毒瘤操作。 wake_lock:两种锁,一种计数锁(锁一次,释放一次);非计数锁(锁了很多次,只需要release一次就可以解除了 ps:系统为了节省电量,CPU在没有任务忙的时候就会自动进入休眠。有任务需要唤醒CPU高效执行的时候,就会给CPU加wake_lock锁。 1.7.plug:充电方式,usb或者插座,以及显示连接的时间。 其余参数(有些参数还没有看到): CPU runing CPU运行的状态、是否被唤醒 Kernel only uptime 只有内核运行时间 Activity Manager Proc 活跃的用户进程 Mobile network type 网络类型 Mobile radio active 移动蜂窝信号 BP侧耗电 Crashes(logcat) 某个时间点出现crash的应用 Doze 是否进入doze模式 Device active 和Doze相反 JobScheduler 异步作业调度 SyncManager 同步操作 Temp White List 电量优化白名单 Phone call 是否打电话 GPS 是否使用GPS Network connectivity 网络连接状态(wifi、mobile是否连接) Mobile signal strength 移动信号强度(greatgoodmoderatepoor) Wifi scan 是否在扫描WiFi信号 Wifi supplicant 是否有WiFi请求 Wifi radio 是否正在通过wifi传输数据 Wifi signal strength wifi信号强度(greatgoodmoderatepoor) Wifi running WiFi组件是否在工作(未传输数据) Wifi on 同上 Audio 音频是否开启 Camera 相机是否在工作 Video 是否在播放视频 Foreground process 前台进程 Package install 是否在进行包安装 Package active 包管理在工作 Battery level 电池当前电量 Temperature 电池温度 Charging on 在充电 Logcat misc 是否在导出日志 2、横坐标: 横坐标是一个时间范围,以一分钟为周期,到第60秒的时候变为0。时间范围以重置为起点,获取bugreport内容时刻为终点。坐标的间隔,会随着时间的长度发生改标。
1、Install Docker Desktop for Mac (ps: Requires Apple Mac OS Sierra 10.12 or above.)手动下载安装 https://docs.docker.com/docker-for-mac/install/ 使用 Homebrew 安装 brew cask install docker(推荐) 2、Run the Battery Historian image. 使用命令 docker run -d -p 9999:9999 bhaavan/battery-historian 加载启动镜像 (有问题的镜像,不要用) docker run -p 9998:9998 gcr.io/android-battery-historian/stable:3.0 --port 9998 (亲测可用镜像) didi@localhost ~ docker run -p 9998:9998 gcr.io/android-battery-historian/stable:3.0 --port 9998 Unable to find image 'gcr.io/android-battery-historian/stable:3.0' locally 3.0: Pulling from android-battery-historian/stable c62795f78da9: Pull complete d4fceeeb758e: Pull complete 5c9125a401ae: Pull complete 0062f774e994: Pull complete 6b33fd031fac: Pull complete a6bd6e1d0bdb: Pull complete 76cf9d0635af: Pull complete 856d20d533e0: Pull complete e63a73f6a528: Pull complete 1a75578c9353: Pull complete 24f3649604d9: Pull complete 10f637765748: Pull complete e06a9fa76cf2: Pull complete Digest: sha256:265a37707f8cf25f2f85afe3dff31c760d44bb922f64bbc455a4589889d3fe91 Status: Downloaded newer image for gcr.io/android-battery-historian/stable:3.0 2019/04/15 12:46:23 Listening on port: 9998 2019/04/15 12:48:19 Trace starting analysisServer processing for: GET 2019/04/15 12:48:19 Trace finished analysisServer processing for: GET 2019/04/15 12:48:20 Trace starting analysisServer processing for: GET 2019/04/15 12:48:20 Trace finished analysisServer processing for: GET 2019/04/15 12:48:27 Trace starting analysisServer processing for: POST 2019/04/15 12:48:27 Trace starting reading uploaded file. 2330165 bytes 2019/04/15 12:48:28 failed to extract battery info: could not find battery time info in bugreport 2019/04/15 12:48:28 failed to extract time information from bugreport dumpstate: open /usr/lib/go-1.6/lib/time/zoneinfo.zip: no such file or directory 2019/04/15 12:48:28 Trace started analyzing "bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-14-31-10.zip~bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-14-31-10/bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-14-31-10.txt" file. 2019/04/15 12:48:28 Trace finished processing checkin. 2019/04/15 12:48:28 Trace finished processing summary data. 2019/04/15 12:48:28 Trace finished generating Historian plot. 2019/04/15 12:48:28 Trace finished analyzing "bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-14-31-10.zip~bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-14-31-10/bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-14-31-10.txt" file. 2019/04/15 12:48:29 Trace ended analyzing file. 2019/04/15 12:48:29 Trace finished analysisServer processing for: POST didi@localhost /usr/local/Cellar/go/1.12.4/src/github.com/google/battery-historian master docker ps -all CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c19bb05d9198 bhaavan/battery-historian "/bin/sh -c 'go run …" 19 minutes ago Up 19 minutes 0.0.0.0:9999->9999/tcp sad_shaw didi@localhost /usr/local/Cellar/go/1.12.4/src/github.com/google/battery-historian master didi@localhost /usr/local/Cellar/go/1.12.4/src/github.com/google/battery-historian master docker images REPOSITORY TAG IMAGE ID CREATED SIZE bhaavan/battery-historian latest 9a3a9fd0ca2f 2 years ago 922MB 3、Open BHRun historian and visit http://localhost:9999 4、Upload Report Both .txt and .zip bug reports are accepted. To take a bug report from your Android device, you will need to enable USB debugging under Settings > System > Developer Options To obtain a bug report from your development device running Android 7.0 and higher: $ adb bugreport bugreport.zip didi@localhost ~ adb bugreport bugreport.zip /data/user_de/0/com.android.shell/files/bugreports/bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-11-08-01.zip: 1 file pulled. 23.2 MB/s (2295220 bytes in 0.094s) // 手机本地 didi@localhost ~ adb pull /data/user_de/0/com.android.shell/files/bugreports/bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-12-08-32.zip /Users/didi/Documents/ # 导出到电脑 /data/user_de/0/com.android.shell/files/bugreports/bugreport-ALP-AL00-HUAWEIALP-AL00-2019-04-15-12-08-32.zip: 1 file pulled. 24.1 MB/s (2415380 bytes in 0.096s) For devices 6.0 and lower: $ adb bugreport > bugreport.txt 5、Start analyzing! Timeline: System stats: App stats:
Wakelock analysis Kernel trace analysis Other command line tools Wakelock analysis 默认情况下,Android不会记录指定应用的用户空间下wakelock transitions的时间戳。如果想要让 Historian 在timeline展示每个 individual wakelock 的详细信息,就需要在开始操作试验之前通过下面的命令开启 full wakelock reporting。 adb shell dumpsys batterystats --enable full-wake-history 需要注意的是,一旦开启 full wakelock reporting,电池历史日志记录将在几小时后溢出。使用此选项可进行短期测试(3-4小时)。 Kernel trace analysis 要生成记录内核唤醒源和内核唤醒锁活动的跟踪文件, 首先开启 kernel trace logging: $ adb root $ adb shell Set the events to trace. $ echo "power:wakeup_source_activate" >> /d/tracing/set_event $ echo "power:wakeup_source_deactivate" >> /d/tracing/set_event The default trace size for most devices is 1MB, which is relatively low and might cause the logs to overflow.8MB to 10MB should be a decent size for 5-6 hours of logging. $ echo 8192 > /d/tracing/buffer_size_kb $ echo 1 > /d/tracing/tracing_on 使用设备跑测试场景完成后导出日志 $ echo 0 > /d/tracing/tracing_on $ adb pull /d/tracing/trace <some path> ##### Take a bug report at this time. $ adb bugreport > bugreport.txt Other command line tools System stats $ go run cmd/checkin-parse/local_checkin_parse.go --input=bugreport.txt Timeline analysis $ go run cmd/history-parse/local_history_parse.go --summary=totalTime --input=bugreport.txt Diff two bug reports $ go run cmd/checkin-delta/local_checkin_delta.go --input=bugreport_1.txt,bugreport_2.txt
一、espresso简介espresso是google官方推出的ui自动化框架,可以用来做单元测试和自动化测试。 官方说明文档:https://developer.android.com/training/testing/espresso/ 官方中文文档:https://lovexiaov.gitbooks.io/official-espresso-doc/content/ 优点: 1、代码风格简洁,非常易学 2、api非常小 3、Espresso 的测试跑起来很快 4、官方的框架,android studio支持 5、适用于白盒&黑盒自动化 缺点: 1、不能跨app运行 2、定位TabLayout中的tab时,由于它们拥有相同的类型和id,难以定位view 3、不能进行改变屏幕方向的操作 二、espresso组成Espresso 由 3 个主要的组件构成 **ViewMatchers:查找view,通过onView()在当前层级中定位出viewViewActions:与view交互,通过perform()对view执行某种操作 ViewAssertions:为view设置断言,通过check()检查view的状态** / withId(R.id.my_view) is a ViewMatcher // click() is a ViewAction // matches(isDisplayed()) is a ViewAssertion onView(withId(R.id.my_view)) .perform(click()) .check(matches(isDisplayed())); onView()查找视图的几种用法如下: onView(withClassName()) 根据视图的类名称查找 onView(withContentDescription()) 根据视图的内容描述查找 onView(withId()) 通过视图的ID查找 onView(withText()) 视图中显示的文本查找 onView官方文档:https://developer.android.google.cn/reference/android/support/test/espresso/matcher/ViewMatchers perform()用法: click():返回一个点击动作,Espresso利用这个方法执行一次点击操作,就和我们自己手动点击按钮一样。 clearText():返回一个清除指定view中的文本action,在测试EditText时用的比较多。 swipeLeft():返回一个从右往左滑动的action,这个在测试ViewPager时特别有用。 swipeRight():返回一个从左往右滑动的action,这个在测试ViewPager时特别有用。 swipeDown():返回一个从上往下滑动的action。 swipeUp():返回一个从下往上滑动的action。 closeSoftKeyboard():返回一个关闭输入键盘的action。 doubleClick():返回一个双击action pressBack():返回一个点击手机上返回键的action。 longClick():返回一个长按action perform()官方文档:https://developer.android.google.cn/reference/android/support/test/espresso/action/ViewActions ViewAssertions文档:https://developer.android.google.cn/reference/android/support/test/espresso/assertion/ViewAssertions 三、demo演示1、创建工程打开androidstudio,选择File->New->New Project,进入一下界面 Application name中的是工程名,company domain是package name,可以根据需要修改,也可以使用默认值;修改完之后点击next进入下一项;一路next,直到进入如下界面,Activity Name里面的内容默认是MainActivity,可以根据需要修改,也可以不修改,最后点击finish,这样就创建好了一个最简单的android工程。 2、配置espresso环境g在app/build_gradle文件的dependencies中添加 androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'; 在defaultConfig中添加 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 如下图所示: build_gradle文件被修改之后,需要点击右上角的sync Now,否则修改无法生效 3、写测试用例在app/src/下有三个文件夹,main,androidTest和test,main中用来编写工程源码,androidTest和test用来存放测试代码,espresso的代码放在androidTest目录下,命名规则:在被测试文件类名后+Test,EspressoActivity的测试用例文件就是EspressoActivityTest public ActivityTestRule activityRule = new ActivityTestRule(EspressoActivity.class);指定要测试的activity @Test后的是测试用例,上面的一共包含三个测试用例 onView(withId(R.id.test)).check(matches(isDisplayed()));表示查找R.id.test控件,然后查看改控件是否正在展示 onView(withId(R.id.test)).perform(click());查找R.id.test控件,然后执行点击操作 onView(withId(R.id.edit_text)).check(matches(withText("123")));查找R.id.test控件,然后查看text是否为“123”,如果控件中文案等于“123”,则这条用例测试通过,否则测试失败。 如何执行测试用例?选择测试文件,然后点击run,如下图所示 测试用例执行结束后,会把测试结果展示出来,如下图所示: 在测试结果中可以看到测试用例总个数,失败用例和成功用例的情况。 点开失败用例的函数,可以查看失败原因,下图的失败原因是因为所选view的text实际为”sucess“,但是断言中的位”123“
一、需求评审 1.需求评审的目的 明确功能优先级,评审业务流程设计的合理性,评估技术可行性。 2.需求评审中注意事项 a)提前了解产品需求,明确核心流程、功能结构 b)评审过程中不避免乏味,时间越长越容易分心,所以先了解重点模块,循序渐进 c)评审中遇到争议点,避免发散讨论,引导大家快速决策,明确沟通,明确产品拍板 d)评审中遇到无法决策的点,记录下来,会后处理,不过多纠缠,后续让产品决策后更新需求文档。 3.需求评审中常见问题 a)需求不明确。(包含需求写的不详细,涉及到的功能描述不清楚,该需求涉及到的功能没有考虑全面) 一般情况下产品的需求设计都会存在考虑不全,QA可能最全面最了解整提需求的人,遇到不明确的问题可以当场指出,请产品决策,不能当场决策的可会后记录拍板后更新文档。 b)需求有异议。(包含认为需求本身的不合理性、测试方案的不可行性) 需求评审中可以对需求本身提处异议,可以发表自己的见解,是否采纳产品决定。例如: 需求评审中需求设计到的测试,经评估觉得按照测试方案来算不可行,可以提出异议。多方商议折中处理。 c)需求文档缺失。(包含但不限于需求涉及到的地方比较多,文档中没有描述清晰) 描述不清晰的地方或者一笔带过的地方也要让产品补全,为了后续的case编写和测试减轻沟通成本。 d)数据埋点需求存在问题。 需求评审也包含数据埋点的评审,埋点评审主要关注埋点的数量,参数的合理性,方便后续测试 ,有疑问的埋点可以当场指出,并得出结论。待确定的问题要记录并提醒产品更新。 二、case编写 1.什么是测试用例 测试用例(Test Case)是为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求,通俗的讲:就是把我们测试系统的操作步骤用按照一定的格式用文字描述出来。 2.为什么要写测试用例(1)、 理清思路,避免遗漏理清思路是我们认为最重要的一点,有的系统本来就是一个大而复杂的项目,我们需要把项目功能细分,根据每一个功能通过编写用例的方式来整理我们测试系统的思路,避免遗漏掉要测试的功能点。 (2)、 跟踪测试进度进展通过编写测试用例,执行测试用例,我们可以很清楚的知道我们的测试进度,方便跟踪我们的测试进度。 (3)、 回归测试首先我们的系统不是测一遍就完了的,我们需要在集成环境测试,线上还要进行回归,其次还要全部业务线合并集成测试,而且也有可能会有不同的人在不同的阶段、不同的集成环境进行测试,那么我们就需要测试用例来规范和指导我们的测试行为。 (4)、 历史参考在我们所做项目的各个版本中,也许会有很多功能是相同或相近的,我们对这类功能设计了测试用例,便于以后我们遇到类似功能的时候可以做参考依据。 另外如果产品发布后出现了发布缺陷(就是我们现在的线上问题),测试用例也是分析发布后缺陷的依据之一。 3.如何编写测试用例(1)、测试需求分析,得到测试点 在测试需求分析阶段,我们只有需求文档,所以编写测试用例的唯一依据就是需求文档,因此在进行用例编写之前一定要进行需求分析,需求分析的主要工作就是:了解需求的整个实现背景;分析需求的合理性;明确需求的范围,挖掘需求文档中隐藏的需求;在通过需求交底的过程,确定开发的初步实现思路和方法,随着测试需求分析的深入,列出需求的框架,包括测试范围即各个功能点,测试的场景等;确定一些测试可以提前介入的工作等;需要说明的是对于需求中的问题一定要记录下来,找需求确认,需求漏掉的或者存在问题的地方,开发和测试更容易漏掉,而且遗漏的需求很有可能会使得项目整体业务逻辑发生变化,一定要及时提前确认。 (2)、分析得到用例优先级 得到了需求的各个测试点后,应该先将这些测试点简单的分配一下优等级,一般分为高中低三个优先级,我认为得到优先级后可以让需求用例的设计更有侧重和着重点。 (3)、细化测试点变成可执行case 根据测试需求分析得到的需求框架,梳理细化测试点,这里的测试点虽然粗,但是不应该有遗漏,这是进行测试点细化的前提。根据测试点,细化出具体的测试用例,要注意各个点的组合测试的情况,还要注意各个测试点的反向测试的情况。 在细化测试点的时候,我们可以要参考以前写好的公共测试用例,甚至可以直接引用,这样既可以避免一些不必要的时间浪费,但是参考不等于照搬,在引用的同时,也一定要思考本次需求自己特有的测试点。 另外需要考虑的就是测试点细化到什么程度的问题,也就是一个度的问题,我们要把握好测试点细化的一个度的问题,太粗的测试点没有指导意义,太细的测试点容易让我们纠的太细,忽略整体的测试,反而也起不到一个指导的效果,所以一定要把握好测试点细化的度。 (4)、及时更新测试用例 需求分析和用例编写阶段,是主要的细化用例时间,这段时间的目标是梳理出可指导执行测试的用例,但是需求会有变动,需求会有维护,用例也一样,所以用例是需要持续维护的, 所以在需求变动的同时,我们也要及时维护测试用例,否则的话,测试用例很可能成为一个错误的指导。 另外测试用例完成后就会进入一个用例评审的阶段,在用例评审阶段,会有用例评审人,针对你的用例作出的评审,主要检查你的用例是否有测试点遗漏,场景遗漏,测试case描述模糊,测试结果输出模糊等问题,针对用例评审人提出的问题,我们也要及时的更改我们的用例。 (5)、及时维护核心测试用例(也可以叫做通用的测试用例) 什么是核心测试用例呢?我理解的核心测试用例就是:项目中或者跨项目中很多的公用业务,固化模块,这些功能基本上是趋于稳定不变的,因此可以梳理出通用的比较全面的测试点,作为指导和规范业务和模块的规范,这些生成的规范即通用的测试用例。当我们针对某一模块或者业务持续维护时,就发现我们需要持续维护这的用例,就会发现有些用例业务类似、执行步骤一致、验证项属性一致等等,这个时候通过梳理业务的通用属性,通用用例梳理梳理成章。所以说,核心的测试用例是一个对用例不断维护的产出,因此我们在测试软件维护的过程中一定要及时的更新核心测试用例,对后面的测试和用例维护有一个很大的指导作用。 4.如何提升用例的编写能力(1)、 熟悉业务,了解系统 任何系统都有大的业务背景,只要熟悉了业务知识才能更有效的使用系统。 任何系统在使用过程中,都有一个熟悉的过程,对系统越熟悉,越容易发现系统问题和业务问题。 (2)、 用客观的思考方式站在用户的角度分析 作为测试人员如果想提升测试用例的编写能力,首先应该做到的就是站在客户的角度分析客户需要什么和客户想要什么,客户不想要什么,也就是所谓的客户的使用场景,这样有利于我们更好的挖掘和思考隐含的需求。至于这个需求该不该做,那是需求人员的职责,这个需求做起来复不复杂那是开发人员的事情,作为测试人员需要考虑的事就是你所设计的正向和反向测试用例是不是用户常用到的场景,以及一些客户基本不会用到的场景有哪些。 (3)、 多思考,不要拘束于惯性思维 我们知道一个人做一个工作时间越久,也就是我们说的经验越丰富,可能这个思维方式就会越被限定住。比如,测试的统计表多了,当拿到一个新增的统计表的时候,首先想到的是公用用例上所列的测试点基本上就是最全的了,我都不用思考,直接用就行了。 其实这是一个误区,公用用例的目的是帮助我们减少一些不必要的内耗,但是我们的思维不要被它所限定,如果公用用例中某个点是错的,那我们岂不要一错再错了。所以作为一个测试人员如果想要提升自己的测试用例设计能力,一定要多思考,不要被这种惯性思维束缚,不要被所谓的经验束缚。 (4)、 不要闭门造车,利用好网络资源 提升测试用例设计能力,多思考是非常重要的,但是不是让你傻思考,当你的进步遇到瓶颈的时候,不要闭门造车,做井底之蛙,要充分利用网络上的学习资源,学习一些前辈的经验,并把这些运用到实际的测试用例设计中去。山外青山楼外楼,多浏览和关注一些关于测试用例设计的网站或者微信公众号,广开言路,相信会对你的测试用例设计能力的提升会有很大的帮助的。 (5)、 善于总结分享 基于以上四点我们还要做到善于总结,乐于分享,把经常见到的用例设计的误区和一些好的用例设计,和用例设计习惯分享给周围的小伙伴,这样可以集众人之所长,不断提升我们的用例设计能力。 5.编写用例中常遇到的问题 (1)、case编写时无从下手总结了一下本人编写case的方法,仅供参考按照模块划分,梳理思维导图按照页面划分,梳理思维导图按照测试重点划分,梳理思维导图 (2)、相同页面或相同场景一样的需求,但是涉及到的页面过多,重复case太多 一般情况下可以把相同的case整理成一条,后面详细标注一下每一个入口或者每一个页面或每一个操作。简单举个例子~ (3)、异常场景考虑不全面 总计一下我们现在测试中异常场景可以分为以下几类: 网络异常:弱网、断网、网络切换等 服务端返回异常:必要的字段未返回或返回为空(可用Charles模拟),格式返回不正确等 电话、短信、等第三方打断操作 页面多次切换、同一接口多次请求、前后台切换等 备注:case是给所有人看的,所以case编写的时候一定要清晰明了,重点描述清晰。三、问题跟进1、如何跟进问题 (1)、明确问题优先级,优先解决P0级别的问题。 测试中常遇到的影响主流程的问题、崩溃、该功能开发未完成等情况,先找开发沟通解决问题,后续补提jira。不要只提jira不找开发跟进,会影响测试进度。不影响主流程和新功能测试的bug可以先提jira,后沟通或先不沟通,等一轮测试完成后在持续跟进bug。 (2)、持续更新问题事宜明细与跟踪 p0级别的bug要持续催促开发优先解决问题,非P0级别的bug 一轮测试完成后也要持续跟进未解决的问题,二轮测试阶段要每天bug清0,每天下班前验证待测试的bug。 (3)、遇到需求变更怎么办 a:要求产品、开发、测试三方一起评估是否可做。不可做的要给出明确的不可做理由,例如:测试人员紧缺、测试时间不够,需求不合理等。 b:开发同意后评估测试成本和时间, c:明确提测时间和测试影响范围 d:明确指出新增需求如果测试中出现问题(例如提测delay,影响范围过大,测试成本太高等问题)如何解决 e:明确是否可以代码回滚 (4)、有冲突的问题及时寻求外部支持 如何获取外部支持 *合理选用书面沟通和口头沟通,准备好清晰的沟通材料 *明确并熟知各部门职责的三级负责人(每个模块的产品、客户端开发、API开发、后端开发) *不明事宜或困难及时找三级负责人沟通 *沟通技巧:换位思考、同理心沟通 *三级负责人之间如果存在沟通后不能解决的问题,可以找二级负责人或一级负责人。 (5)、测试时间太紧张怎么办 a:明确需求优先级 b:合理安排测试时间 c:分批提测,提前介入测试 d:把控提测质量,要求开发必须执行准入case切通过后才可以提测。 e:及时跟进开发进度,避免提测delay 四、总结 测试闭环也就是是一个项目的测试周期,从前期的需求评审到后期的发版整个流程缺一不可。
前言我们的目标 了解爬虫什么是爬虫爬虫的基本流程能爬取那些数据如何解析数据python爬虫架构Python 爬虫架构介绍Scrapy介绍及框架图具体爬虫操作一、页面获取二、目标提取三、指定链接抓取四、数据下载&存储五、添加交互附录Py2.x vs Py3.x爬虫脚本前言马蜂窝评论抄袭事件经过数据分析,马蜂窝上有7454个抄袭账号,合计从携程、艺龙、美团、Agoda、Yelp上抄袭搬运了572万条餐饮点评与1221万条酒店点评。有1800万条是机器全网抓取的,各种评论截图拼凑在一起 ,简直触目惊心! 我们的目标 了解爬虫概念、流程、原理首先肯定要实现图片抓取这个基本功能然后实现对用户所给的链接进行抓取最后可以有一定的简单交互 了解爬虫什么是爬虫举例来说:我们可以把互联网比作一张大的蜘蛛网,数据便是存放于蜘蛛网的各个节点,而爬虫就是一只小蜘蛛,沿着网络抓取自己的猎物(数据)。 从技术层面来说就是:通过程序模拟浏览器请求站点的行为,把站点返回的HTML代码/JSON数据/二进制数据(图片、视频) 爬到本地,进而提取自己需要的数据,存放起来使用。 爬虫的基本流程模拟浏览器发送请求(获取网页代码)->提取有用的数据->存放于数据库或文件中 1、发起请求 使用http库向目标站点发起请求,即发送一个Request Request包含:请求头、请求体等 Request模块缺陷:不能执行JS 和CSS 代码 2、获取响应内容 如果服务器能正常响应,则会得到一个Response Response包含:html,json,图片,视频等 3、解析内容 解析html数据:正则表达式(RE模块),第三方解析库如Beautifulsoup,pyquery等 解析json数据:json模块 解析二进制数据:以wb的方式写入文件 4、保存数据 文本:纯文本,Json,Xml等 关系型数据库:如mysql,oracle,sql server等结构化数据库 非关系型数据库:MongoDB,Redis等key-value形式存储 能爬取那些数据网页文本:如HTML文档,Json格式化文本等图片:获取到的是二进制文件,保存为图片格式(妹子的图片...)视频:同样是二进制文件(想看的视频...)其他:只要你能通过浏览器访问的数据都可以通过爬虫获取(酒店信息....) 如何解析数据直接处理Json解析正则表达式处理BeautifulSoup解析处理PyQuery解析处理XPath解析处理python爬虫架构Python 爬虫架构介绍爬虫架构主要由五个部分组成,分别是调度器、URL管理器、网页下载器、网页解析器、应用程序(爬取的有价值数据)。 调度器:相当于一台电脑的CPU,主要负责调度URL管理器、下载器、解析器之间的协调工作。URL管理器:包括待爬取的URL地址和已爬取的URL地址,防止重复抓取URL和循环抓取URL,实现URL管理器主要用三种方式,通过内存、数据库、缓存数据库来实现。网页下载器:通过传入一个URL地址来下载网页,将网页转换成一个字符串,网页下载器有urllib2(Python官方基础模块)包括需要登录、代理、和cookie,requests(第三方包)网页解析器:将一个网页字符串进行解析,可以按照我们的要求来提取出我们有用的信息,也可以根据DOM树的解析方式来解析。 常用的网页解析器有: 正则表达式(直观,将网页转成字符串通过模糊匹配的方式来提取有价值的信息,当文档比较复杂的时候,该方法提取数据的时候就会非常的困难)html.parser(Python自带的)beautifulsoup(第三方插件,html.parser 和 beautifulsoup 以及 lxml 都是以 DOM 树的方式进行解析的)lxml(第三方插件,可以解析 xml 和 HTML)应用程序:就是从网页中提取的有用数据组成的一个应用。下面用一个图来解释一下调度器是如何协调工作的: 我们来看一下主流爬虫框架在GitHub上的活跃度: Scrapy介绍及框架图Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。Scrapy 使用 Twisted这个异步网络库来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。 具体爬虫操作我们以百度贴吧为例,一步步教你如何进行图片爬取。 一、页面获取寻找规律,分析规律 图中的黑色方框左边填写xpth,右边会返回对应的结果,可以看到当前页面的帖子全部抓取到了。xpth具体怎么写要根据右边的检查元素来具体分析,每个网站的方式不一样,但是细心寻找可以找到相同的规律。 二、目标提取找到规律并能匹配上开始写代码了:go 要让python可以进行对网页的访问,那肯定要用到urllib.request包 urllib中有 urlopen(str) 方法用于打开网页并返回一个对象,调用这个对象的read()方法后能直接获得网页的源代码,内容与浏览器右键查看源码的内容一样。 三、指定链接抓取分析当前页面所有的贴吧名,即:/p/xxxxxxx 之后拼接 “host + 贴吧名”,行程最终的贴吧链接 进入每个帖子后,再对帖子内的图片进行一次过滤,仅匹配与楼主上传的相关图片 四、数据下载&存储爬取贴吧前多少页的数据 五、添加交互 附录Py2.x vs Py3.xPy2.x:Urllib库、Urllin2库 Py3.x:Urllib库 变化: 2.x vs 3.x 在Pytho2.x中使用import urllib2——-对应的,在Python3.x中会使用import urllib.request,urllib.error。在Pytho2.x中使用import urllib——-对应的,在Python3.x中会使用import urllib.request,urllib.error,urllib.parse。在Pytho2.x中使用import urlparse——-对应的,在Python3.x中会使用import urllib.parse。在Pytho2.x中使用import urlopen——-对应的,在Python3.x中会使用import urllib.request.urlopen。在Pytho2.x中使用import urlencode——-对应的,在Python3.x中会使用import urllib.parse.urlencode。在Pytho2.x中使用import urllib.quote——-对应的,在Python3.x中会使用import urllib.request.quote。在Pytho2.x中使用cookielib.CookieJar——-对应的,在Python3.x中会使用http.CookieJar。在Pytho2.x中使用urllib2.Request——-对应的,在Python3.x中会使用urllib.request.Request。 爬虫脚本爬取百度贴吧贴主的图片 -- coding:utf-8 -- import urllib.requestfrom lxml import etree def loadPage(url): """ 作用:根据url发送请求,获取服务器响应文件 url: 需要爬取的url地址 """ # headers = {"User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"} request = urllib.request.Request(url) html = urllib.request.urlopen(request).read() # 解析HTML文档为HTML DOM模型 content = etree.HTML(html) # print content # 返回所有匹配成功的列表集合 link_list = content.xpath('//div[@class="t_con cleafix"]/div/div/div/a/@href') # link_list = content.xpath('//a[@class="j_th_tit"]/@href') for link in link_list: fulllink = "http://tieba.baidu.com" + link # 组合为每个帖子的链接 # print link loadImage(fulllink) 取出每个帖子里的每个图片连接 def loadImage(link): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"} request = urllib.request.Request(link, headers=headers) html = urllib.request.urlopen(request).read() # 解析 content = etree.HTML(html) # 取出帖子里每层层主发送的图片连接集合 # link_list = content.xpath('//img[@class="BDE_Image"]/@src') # link_list = content.xpath('//div[@class="post_bubble_middle"]') link_list = content.xpath('//img[@class="BDE_Image"]/@src') # 取出每个图片的连接 for link in link_list: print("link:" + link) writeImage(link) def writeImage(link): """ 作用:将html内容写入到本地 link:图片连接 """ # print "正在保存 " + filename headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"} # 文件写入 request = urllib.request.Request(link, headers=headers) # 图片原始数据 image = urllib.request.urlopen(request).read() # 取出连接后10位做为文件名 filename = link[-10:] # 写入到本地磁盘文件内 with open("/Users/didi/Downloads/crawlertest/" + filename, "wb") as f: f.write(image) # print("已经成功下载 " + filename) def tiebaSpider(url, beginPage, endPage): """ 作用:贴吧爬虫调度器,负责组合处理每个页面的url url : 贴吧url的前部分 beginPage : 起始页 endPage : 结束页 """ for page in range(beginPage, endPage + 1): pn = (page - 1) * 50 filename = "第" + str(page) + "页.html" print(filename) fullurl = url + "&pn=" + str(pn) print(fullurl) loadPage(fullurl) # print html print("下载完成") if name == "__main__": kw = input("请输入需要爬取的贴吧名:") beginPage = int(input("请输入起始页:")) endPage = int(input("请输入结束页:")) url = "http://tieba.baidu.com/f?" key = urllib.parse.urlencode({"kw": kw}) fullurl = url + key tiebaSpider(fullurl, beginPage, endPage)
整个平台的搭建使用的是python的unittest测试框架,这里简单介绍下unittest模块的简单应用。 unittest是python的标准测试库,相比于其他测试框架是python目前使用最广的测试框架。 Unittest framework 正常调用unittest的流程是: 1、TestLoader 自动将测试用例TestCase中加载到TestSuite里 2、在执行TestCase过程中,先进行SetUp()环境准备,执行测试代码。 3、TextTestRunner调用TestSuite的run方法,顺序执行里面的TestCase中以test开头的方法,其中TestLoader在加载过程中,进行添加的TestCase是没有顺序的。一个TestCase里如果存在多个验证方法的话,会按照方法中test后方首字母的排序进行执行。可以通过手动调用TestSuite的addTest、addTests方法来动态添加TestCase,这样既可以确定添加用例的执行顺序,也可避免TestCase中的验证方法一定要用test开头。 4、最后tearDown()进行测试的还原。 5、得到测试结果TestResult。 KeywordsTestCase: The individual unit of testing.TestSuite: A collection of test cases, test suites, or both. It is used to aggregate tests that should be executed together.TestLoader: Load the test cases to the test suite.TestRunner: A component which orchestrates the execution of tests and provides the outcome to the user.TestFixture: Represents the preparation needed to perform one or more tests, and any associate cleanup actions. setUp(), tearDown(), setUpClass(), tearDownClass()Decorator: skip, skipIf, skipUnless, expectedFailure unittest有四个比较重要的概念是test fixture, test case, test suite, test runner, 。 test fixture:The test fixture is everything we need to have in place to exercise the sut。简单来说就是做一些测试过程中需要准备的东西,比如创建临时的数据库,文件和目录等,其中 setUp() 和 setDown() 是最常用的方法 test case:用户自定义的测试case的基类,调用run()方法,会依次调用setUP方法、执行用例的方法、tearDown()方法。 test suite:测试用例集合,可以通过addTest()方法手动增加Test Case,也可通过TestLoader自动添加Test Case,TestLoader在添加用例时,会没有顺序。 test runner:运行测试用例的驱动类,可以执行TestCase,也可执行TestSuite。执行后TestCase和Testsuite会自动管理TestResult。 Command linepython -m unittest xxxpython -m unittest -hpython -m unittest discover # discovery the test cases(test*.py) and execute automatically
一、用例编写规则 1.unittest提供了test cases、test suites、test fixtures、test runner相关的类,让测试更加明确、方便、可控。使用unittest编写用例,必须遵守以下规则: (1)测试文件必须先import unittest (2)测试类必须继承unittest.TestCase (3)测试方法必须以“test_”开头 (4)测试类必须要有unittest.main()方法 2.pytest是python的第三方测试框架,是基于unittest的扩展框架,比unittest更简洁,更高效。使用pytest编写用例,必须遵守以下规则: (1)测试文件名必须以“test_”开头或者"_test"结尾(如:test_ab.py) (2)测试方法必须以“test_”开头。 (3)测试类命名以"Test"开头。 总结: pytest可以执行unittest风格的测试用例,无须修改unittest用例的任何代码,有较好的兼容性。 pytest插件丰富,比如flask插件,可用于用例出错重跑;还有xdist插件,可用于设备并行执行。 二、用例前置和后置 1.unittest提供了setUp/tearDown,只能针对所有用例。 2.pytest提供了模块级、函数级、类级、方法级的setup/teardown,比unittest的setUp/tearDown更灵活。模块级(setup_module/teardown_module)开始于模块始末,全局的 函数级(setup_function/teardown_function)只对函数用例生效(不在类中) 类级(setup_class/teardown_class)只在类中前后运行一次(在类中) 方法级(setup_method/teardown_method)开始于方法始末(在类中) 类里面的(setup/teardown)运行在调用方法的前后 pytest还可以在函数前加@pytest.fixture()装饰器,在测试用例中装在fixture函数。fixture的使用范围可以是function,module,class,session。 firture相对于setup和teardown来说有以下几点优势:命名方式灵活,不局限于setup和teardown这几个命名conftest.py 配置里可以实现数据共享,不需要import就能自动找到一些配置,可供多个py文件调用。scope="module" 可以实现多个.py跨文件共享前置scope="session" 以实现多个.py跨文件使用一个session来完成多个用例用yield来唤醒teardown的执行 三、断言 1.unittest提供了assertEqual、assertIn、assertTrue、assertFalse。 2.pytest直接使用assert 表达式。 四、报告 1.unittest使用HTMLTestRunnerNew库。 2.pytest有pytest-HTML、allure插件。 五、失败重跑 1、unittest无此功能。 2、pytest支持用例执行失败重跑,pytest-rerunfailures插件。 六、参数化 1、unittest需依赖ddt库, 2、pytest直接使用@pytest.mark.parametrize装饰器。 七、用例分类执行 1、unittest默认执行全部用例,也可以通过加载testsuit,执行部分用例。 2、pytest可以通过@pytest.mark来标记类和方法,pytest.main加入参数("-m")可以只运行标记的类和方法。
概述pytest是一个非常成熟的全功能的Python测试框架,主要特点有以下几点: 1、简单灵活,容易上手,文档丰富;2、支持参数化,可以细粒度地控制要测试的测试用例;3、能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests);4、pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等;5、测试用例的skip和xfail处理;6、可以很好的和CI工具结合,例如jenkins环境配置安装Python依赖库:pip3 install pytestpip3 install pytest-allure-adaptor 安装 Command Tool:brew tap qatools/formulasbrew install allure-commandline 如何获取帮助信息查看 pytest 版本 pytest --version显示可用的内置函数参数 pytest --fixtures通过命令行查看帮助信息及配置文件选项 pytest --help编写规则编写pytest测试样例非常简单,只需要按照下面的规则: 测试文件以test_开头(以_test结尾也可以)测试类以Test开头,并且不能带有 init 方法测试函数以test_开头断言使用基本的assert即可 fixture的scope参数scope参数有四种,分别是'function','module','class','session',默认为function。 function:每个test都运行,默认是function的scopeclass:每个class的所有test只运行一次module:每个module的所有test只运行一次session:每个session只运行一次setup和teardown操作setup,在测试函数或类之前执行,完成准备工作,例如数据库链接、测试数据、打开文件等teardown,在测试函数或类之后执行,完成收尾工作,例如断开数据库链接、回收内存资源等备注:也可以通过在fixture函数中通过yield实现setup和teardown功能 如何执行pytest # run all tests below current dirpytest test_mod.py # run tests in module file test_mod.pypytest somepath # run all tests below somepath like ./tests/pytest -k stringexpr # only run tests with names that match the the "string expression", e.g. "MyClass and not method" will select TestMyClass.test_something but not TestMyClass.test_method_simple pytest test_mod.py::test_func # only run tests that match the "node ID", e.g "test_mod.py::test_func" will be selected only run test_func in test_mod.py Console参数介绍-v 用于显示每个测试函数的执行结果-q 只显示整体测试结果-s 用于显示测试函数中print()函数输出-x, --exitfirst, exit instantly on first error or failed test-h 帮助 在第N个用例失败后,结束测试执行pytest -x # 第01次失败,就停止测试pytest --maxfail=2 # 出现2个失败就终止测试执行特定用例指定测试模块pytest test_mod.py指定测试目录pytest testing/通过关键字表达式过滤执行pytest -k "MyClass and not method"这条命令会匹配文件名、类名、方法名匹配表达式的用例,这里这条命令会运行 TestMyClass.test_something, 不会执行 TestMyClass.test_method_simple 通过 node id 指定测试用例nodeid由模块文件名、分隔符、类名、方法名、参数构成,举例如下:运行模块中的指定用例 pytest test_mod.py::test_func运行模块中的指定方法 ytest test_mod.py::TestClass::test_method通过标记表达式执行pytest -m slow这条命令会执行被装饰器 @pytest.mark.slow 装饰的所有测试用例 通过包执行测试pytest --pyargs pkg.testing这条命令会自动导入包 pkg.testing,并使用该包所在的目录,执行下面的用例 获取用例执行性能数据获取最慢的10个用例的执行耗时 pytest --durations=10生成 JUnitXML 格式的结果文件这种格式的结果文件可以被Jenkins或其他CI工具解析 pytest --junitxml=path禁用插件例如,关闭 doctest 插件 pytest -p no:doctest Pytest 的 Exit Code 含义清单Exit code 0 所有用例执行完毕,全部通过Exit code 1 所有用例执行完毕,存在Failed的测试用例Exit code 2 用户中断了测试的执行Exit code 3 测试执行过程发生了内部错误Exit code 4 pytest 命令行使用错误Exit code 5 未采集到可用测试用例文件 扩展插件3.1. 测试报告 安装与样例pip install pytest-cov # 计算pytest覆盖率,支持输出多种格式的测试报告pytest --cov-report=html --cov=./ test_code_target_dir Console参数介绍--cov=[path], measure coverage for filesystem path (multi-allowed), 指定被测试对象,用于计算测试覆盖率--cov-report=type, type of report to generate: term, term-missing, annotate, html, xml (multi-allowed), 测试报告的类型--cov-config=path, config file for coverage, default: .coveragerc, coverage配置文件--no-cov-on-fail, do not report coverage if test run fails, default: False,如果测试失败,不生成测试报告--cov-fail-under=MIN, Fail if the total coverage is less than MIN. 如果测试覆盖率低于MIN,则认为失败Console Result---------------------------------------------------------------- coverage: platform linux2, python 2.7.14-final-0 ---------------------------------------------------------------- Name Stmts Miss Cover pytest1.py 18 0 100%Html Result image.png3.2. 测试顺序随机pip install pytest-randomly 3.3. 分布式测试pip install pytest-xdist 3.4. 出错立即返回pip install pytest-instafail 安装Allure Pytest AdaptorAllure Pytest Adaptor是Pytest的一个插件,通过它我们可以生成Allure所需要的用于生成测试报告的数据。安装pytest-allure-adaptor插件方法: example:com.atlassian.confluence.content.render.xhtml.XhtmlException: Missing required attribute: {http://atlassian.com/resource/identifier}value@allure.severity("critical") # 优先级,包含blocker, critical, normal, minor, trivial 几个不同的等级@allure.feature("测试模块_demo1") # 功能块,feature功能分块时比story大,即同时存在feature和story时,feature为父节点@allure.story("测试模块_demo2") # 功能块,具有相同feature或story的用例将规整到相同模块下,执行时可用于筛选@allure.issue("BUG号:123") # 问题表识,关联标识已有的问题,可为一个url链接地址@allure.testcase("用例名:测试字符串相等") # 用例标识,关联标识用例,可为一个url链接地址 Allure中对严重级别的定义: Blocker级别——中断缺陷 客户端程序无响应,无法执行下一步操作。 Critical级别――临界缺陷,包括: 功能点缺失,客户端爆页。 Major级别——较严重缺陷,包括: 功能点没有满足需求。 Normal级别――普通缺陷,包括: 数值计算错误 JavaScript错误。 Minor级别———次要缺陷,包括: 界面错误与UI需求不符。 打印内容、格式错误 程序不健壮,操作未给出明确提示。 Trivial级别——轻微缺陷,包括: 辅助说明描述不清楚 显示格式不规范,数字,日期等格式。 长时间操作未给用户进度提示 提示窗口文字未采用行业术语 可输入区域和只读区域没有明显的区分标志 必输项无提示,或者提示不规范。 Enhancement级别——测试建议、其他(非缺陷) 以客户角度的易用性测试建议。 通过测试挖掘出来的潜在需求。 生成html报告命令1、pytest命令基础上加--alluredir,生成xml报告。 pytest -s -q --alluredir [xml_report_path]//[xml_report_path]根据自己需要定义文件夹,作者定义为:/report/xml 用例执行完成之后会在[xml_report_path]目录下生成了一堆xml的report文件,当然这不是我们最终想要的美观报告。 2、需要使用 Command Tool 来生成我们需要的美观报告。 allure generate [xml_report_path] -o [html_report_path]//[html_report_path]根据自己需要定义文件夹,作者定义为:/report/html 打开 index.html,之前写的 case 报告就会呈现在你面前 注️:直接用chrome浏览器打开报告,报告可能会是空白页面。解决办法:1、在pycharm中右击index.html选择打开方式Open in Browser就可以了。2、使用Firefox直接打开index.html。 三、定制报告Feature: 标注主要功能模块Story: 标注Features功能模块下的分支功能Severity: 标注测试用例的重要级别Step: 标注测试用例的重要步骤Issue和TestCase: 标注Issue、Case,可加入URLattach: 标注增加附件Environment: 标注环境Environment字段 1、Features定制详解 -- coding: utf-8 -- @Time : 2018/8/17 上午10:10 @Author : WangJuan @File : test_case.py import allureimport pytest @allure.feature('test_module_01')def test_case_01(): """ 用例描述:Test case 01 """ assert 0 @allure.feature('test_module_02')def test_case_02(): """ 用例描述:Test case 02 """ assert 0 == 0 if name == '__main__': pytest.main(['-s', '-q', '--alluredir', './report/xml']) 添加feature,Report展示见下图。 2、Story定制详解 -- coding: utf-8 -- @Time : 2018/8/17 上午10:10 @Author : WangJuan @File : test_case.py import allureimport pytest @allure.feature('test_module_01')@allure.story('test_story_01')def test_case_01(): """ 用例描述:Test case 01 """ assert 0 @allure.feature('test_module_01')@allure.story('test_story_02')def test_case_02(): """ 用例描述:Test case 02 """ assert 0 == 0 if name == '__main__': pytest.main(['-s', '-q', '--alluredir', './report/xml']) 添加story,Report展示见下图。 3、用例标题和用例描述定制详解 -- coding: utf-8 -- @Time : 2018/8/17 上午10:10 @Author : WangJuan @File : test_case.py import allureimport pytest @allure.feature('test_module_01')@allure.story('test_story_01') test_case_01为用例title def test_case_01(): """ 用例描述:这是用例描述,Test case 01,描述本人 """ #注释为用例描述 assert 0 if name == '__main__': pytest.main(['-s', '-q', '--alluredir', './report/xml']) 添加用例标题和用例描述,Report展示见下图。 4 、Severity定制详解Allure中对严重级别的定义:1、 Blocker级别:中断缺陷(客户端程序无响应,无法执行下一步操作)2、 Critical级别:临界缺陷( 功能点缺失)3、 Normal级别:普通缺陷(数值计算错误)4、 Minor级别:次要缺陷(界面错误与UI需求不符)5、 Trivial级别:轻微缺陷(必输项无提示,或者提示不规范) -- coding: utf-8 -- @Time : 2018/8/17 上午10:10 @Author : WangJuan @File : test_case.py import allureimport pytest @allure.feature('test_module_01')@allure.story('test_story_01')@allure.severity('blocker')def test_case_01(): """ 用例描述:Test case 01 """ assert 0 @allure.feature('test_module_01')@allure.story('test_story_01')@allure.severity('critical')def test_case_02(): """ 用例描述:Test case 02 """ assert 0 == 0 @allure.feature('test_module_01')@allure.story('test_story_02')@allure.severity('normal')def test_case_03(): """ 用例描述:Test case 03 """ assert 0 @allure.feature('test_module_01')@allure.story('test_story_02')@allure.severity('minor')def test_case_04(): """ 用例描述:Test case 04 """ assert 0 == 0 if name == '__main__': pytest.main(['-s', '-q', '--alluredir', './report/xml']) 添加Severity,Report展示见下图。 5、Step定制详解 -- coding: utf-8 -- @Time : 2018/8/17 上午10:10 @Author : WangJuan @File : test_case.py import allureimport pytest @allure.step("字符串相加:{0},{1}") 测试步骤,可通过format机制自动获取函数参数 def str_add(str1, str2): if not isinstance(str1, str): return "%s is not a string" % str1 if not isinstance(str2, str): return "%s is not a string" % str2 return str1 + str2 @allure.feature('test_module_01')@allure.story('test_story_01')@allure.severity('blocker')def test_case(): str1 = 'hello' str2 = 'world' assert str_add(str1, str2) == 'helloworld' if name == '__main__': pytest.main(['-s', '-q', '--alluredir', './report/xml']) 添加Step,Report展示见下图。 6、Issue和TestCase定制详解 -- coding: utf-8 -- @Time : 2018/8/17 上午10:10 @Author : WangJuan @File : test_case.py import allureimport pytest @allure.step("字符串相加:{0},{1}") # 测试步骤,可通过format机制自动获取函数参数def str_add(str1, str2): print('hello') if not isinstance(str1, str): return "%s is not a string" % str1 if not isinstance(str2, str): return "%s is not a string" % str2 return str1 + str2 @allure.feature('test_module_01')@allure.story('test_story_01')@allure.severity('blocker')@allure.issue("http://www.baidu.com")@allure.testcase("http://www.testlink.com")def test_case(): str1 = 'hello' str2 = 'world' assert str_add(str1, str2) == 'helloworld' if name == '__main__': pytest.main(['-s', '-q', '--alluredir', './report/xml']) 添加Issue和TestCase,Report展示见下图。 7、Environment定制详解 具体Environment参数可自行设置 allure.environment(app_package='com.mobile.fm')allure.environment(app_activity='com.mobile.fm.activity')allure.environment(device_name='aad464')allure.environment(platform_name='Android') 添加Environment参数,Report展示见下图。 8、attach定制详解 file = open('../test.png', 'rb').read() allure.attach('test_img', file, allure.attach_type.PNG)在报告中增加附件:allure.attach(’arg1’,’arg2’,’arg3’):arg1:是在报告中显示的附件名称arg2:表示添加附件的内容arg3:表示添加的类型(支持:HTML,JPG,PNG,JSON,OTHER,TEXTXML) 添加attach参数,Report展示见下图。 此外,Allure还支持Jenkins Plugin~
背景: 由于公司要测试APP 产品的耗电问题,我们采取的办法很low,对各个模块功能进行大量的手动测试,再通过Emmagee或GT得出来的结果来评估产品耗电,流量,CPU,内存的消耗等。由于手工效率太低,而且不准确,我们就决定用自动化来实现,但用自动化又面临了一个USB接电脑供电的问题,从而导致计算出来的功耗与手动跑的有很大的误差。 1、将 Android 设备和 adb 主计算机连接到这两者都可以访问的常用 WLAN 网络。注意,并非所有访问点均适用;可能需要使用已正确配置防火墙的访问点以支持 adb 的访问点。注:如果您尝试连接到 Android Wear 设备,则通过关闭与其连接的手机的蓝牙强制将它连接到 WLAN。 2、使用 USB 电缆将设备连接到主计算机。 3、设置目标设备以侦听端口 5555 上的 TCP/IP 连接。 didi@localhost ~ adb devices List of devices attached 68de2f65 device didi@localhost ~ adb tcpip 5555 restarting in TCP mode port: 5555 4、从目标设备断开 USB 电缆连接。 5、查找 Android 设备的 IP 地址。例如,在 Nexus 设备上,您可以通过访问 Settings > About tablet(或 About phone) > Status > IP address 查找 IP 地址。或者,在 Android Wear 设备上,您可以通过访问 Settings > Wi-Fi Settings > Advanced > IP address 查找 IP 地址。 6、连接至设备,通过 IP 地址识别此设备。 didi@localhost ~ adb connect 172.22.139.35 connected to 172.22.139.35:5555 7、请确认您的主计算机已连接至目标设备: didi@localhost ~ adb devices List of devices attached 172.22.139.35:5555 device 8、更改datest - capabilities中手机的udid: capability="sm_g9300-68de2f65" capabilities[capability] = {} capabilities[capability]['platformName'] = 'Android' capabilities[capability]['platformVersion'] = '7.0' capabilities[capability]['deviceName'] = '68de2f65' # capabilities[capability]['udid'] = '68de2f65' capabilities[capability]['udid'] = '172.22.139.35:5555' capabilities[capability]['appPackage'] = 'com.sdu.didi.psnger' capabilities[capability]['appActivity'] = 'com.didi.sdk.app.launch.DidiLoadDexActivity' capabilities[capability]['noReset'] = 'true' capabilities[capability]['newCommandTimeout'] = 300 capabilities[capability]['command_executor'] = "http://127.0.0.1:4723/wd/hub" capabilities[capability]['recreateChromeDriverSessions'] = 'true' capabilities[capability]['unicodeKeyboard'] = 'true' capabilities[capability]['automationName'] = "uiautomator2" # capabilities[capability]['systemPort'] = 8201 现在,可以开始操作了! PS:如果 adb 连接丢失:请确保您的主机仍与您的 Android 设备连接到同一个 WLAN 网络。通过再次执行 adb connect 步骤重新连接。如果无法连接,则重置 adb 主机: didi@localhost master ● adb kill-server 然后,从头开始操作。 经测试,脚本运行的速度和有线并无太大的差异,无线启动appium感觉比有线稍微慢,大概在10秒左右,在可以接受范围。 至此,我们已经可以解决这几个问题了:1、我们不必非要连接数据线做安卓的自动化测试2、我们可以更精确的来衡量功耗的使用3、手机电池寿命更长
1、什么是WDA WebDriverAgent是Facebook 在17年的 SeleniumConf 大会上推出了一款新的iOS移动测试框架。 下面摘录一段官方对于WebDriverAgent的介绍字段:(官方文档:https://github.com/facebook/WebDriverAgent) WebDriverAgent 在 iOS 端实现了一个 WebDriver server ,借助这个 server 我们可以远程控制 iOS 设备。你可以启动、杀死应用,点击、滚动视图,或者确定页面展示是否正确。This makes it a perfect tool for application end-to-end testing or general purpose device automation.(它说它是iOS上一个完美的e2e的自动化解决方案) It works by linking XCTest.framework and calling Apple’s API to execute commands directly on a device.(链接XCTest.framework调用苹果的API直接在设备上执行命令) WebDriverAgent is developed and used at Facebook for end-to-end testing and is successfully adopted by Appium. (Appium封装工作正在进行中,如果一旦封装好,那么以后就可以直接用Appium提供的binding了。)It is currently maintained by Marek Cirkos and Mehdi Mulani。 2、简单原理图 WebDriver之所以能够实现与浏览器进行交互,是因为浏览器实现了Mobile JSON Wire Protocol Specification协议,这个协议是使用JOSN通过HTTP进行传输。 它的实现使用了经典的Server-Client架构(C/S),客户端发送一个requset,服务器端返回一个response。 在开始下面的内部实现细节的讲解前,我们下明确几个概念: 1、WDAClient WDAClient是基于WebDriverAgent实现的WDA的客户端。facebook-wda 就是 WDA 的 Python 客户端库,通过直接构造HTTP请求直接跟WebDriverAgent通信。 2、WDAServer 运行WDA App的机器,实现了WebDriver的通讯协议 3、Session 服务器端需要维护客户端的Session,客户端首次发送请求的字符串是'/session/sessionId/url′。服务器端根据url打开对应的url地址,同时将sessionId/url′。服务器端根据url打开对应的url地址,同时将sessionId解析成真实的值。然后返回给客户端。以后客户端再向浏览器发送请求时,将会携带session值一起发送。 1 2 3 4 5 6 7 8 [debug] [BaseDriver] Creating session with W3C capabilities: {"alwaysMatch":{"platformNa... [BaseDriver] Session created with session id: 7a1d8eca-8c48-4a94-8256-ab283e2af4c3 [Appium] New AndroidUiautomator2Driver session created successfully, session 7a1d8eca-8c48-4a94-8256-ab283e2af4c3 added to master session list [debug] [BaseDriver] Event 'newSessionStarted' logged at 1540198593998 (16:56:33 GMT+0800 (中国标准时间)) [debug] [W3C] Cached the protocol value 'W3C' for the new session 7a1d8eca-8c48-4a94-8256-ab283e2af4c3 [debug] [W3C] Responding to client with driver.createSession() result: {"capabilities":{"platform":"LINUX","webStorageEnabled":false,"takesScreenshot":true,"javascriptEnabled":true,"databaseEnabled":false,"networkConnectionEnabled":true,"locationContextEnabled":false,"warnings":{},"desired":{"platformName":"Android","unicodeKeyboard":true,"command_executor":"http://127.0.0.1:4723/wd/hub","noReset":true,"appActivity":"com.didi.sdk.app.launch.DidiLoadDexActivity","automationName":"uiautomator2","newCommandTimeout":300,"deviceName":"68de2f65","recreateChromeDriverSessions":"true","platformVersion":"7.0","appPackage":"com.sdu.didi.psnger"},"platformName":"Android","unicodeKeyboard":true,"command_executor":"http://127.0.0.1:4723/wd/hub","noReset":true,"appActivity":"com.didi.sdk.app.launch.DidiLoadDexActivity","automationName":"uiautomator2","newCommandTimeout":300,"deviceName":"68de2f65","recreateChromeDriverSessions":"true","platformVersion":"7.0","appPackage":"com.sdu.didi.psnger","deviceUDID":"68de2f65","deviceScreenSize":"1440x2560","deviceScreenDensity":640,"deviceModel":"SM... [HTTP] <-- POST /wd/hub/session 200 27897 ms - 1238 1 2 3 [debug] [JSONWP Proxy] Matched '/session' to command name 'createSession' [debug] [JSONWP Proxy] Proxying [POST /session] to [POST http://localhost:8200/wd/hub/session] with body: {"desiredCapabilities":{"platform":"LINUX","webStorageEnabled":false,"takesScreenshot":true,"javascriptEnabled":true,"databaseEnabled":false,"networkConnectionEnabled":true,"locationContextEnabled":false,"warnings":{},"desired":{"platformName":"Android","unicodeKeyboard":true,"command_executor":"http://127.0.0.1:4723/wd/hub","noReset":true,"appActivity":"com.didi.sdk.app.launch.DidiLoadDexActivity","automationName":"uiautomator2","newCommandTimeout":300,"deviceName":"68de2f65","recreateChromeDriverSessions":"true","platformVersion":"7.0","appPackage":"com.sdu.didi.psnger"},"platformName":"Android","unicodeKeyboard":true,"command_executor":"http://127.0.0.1:4723/wd/hub","noReset":true,"appActivity":"com.didi.sdk.app.launch.DidiLoadDexActivity","automationName":"uiautomator2","newCommandTimeout":300,"deviceName":"68de2f65","recreateChromeDriverSessions":"true","platformVersion":"7.0","appPackage":"com.sdu.didi.psnger","deviceUDID":"68de2f65","deviceScreenSize":"1440x2560","deviceScreenDensity":640,"deviceMod... [debug] [JSONWP Proxy] Got response with status 200: {"sessionId":"eef20bb5-f3ed-4cbc-9977-b32f8be4eea9","status":0,"value":"Created Session"} 4、WebElement WebDriverAPI中的对象,代表页面上的一个DOM元素。 5、JsonWireProtocol JsonWireProtocol(以下简称JWP)是通过使用webdriver与remote server进行通信的web service 协议。通过http请求,完成和remote server的交互。 6、Mobile JSON Wire Protocol Specification 移动端自动化协议 7、iOS Accessibility 3、执行流程 1. 启动webdriveragent 2. 启动App 向WebdriverAgent发送post请求 ,请求参考WDA项目中 FBSessionCommands.m 请求地址:url=http://#{ip}:8100/session,WevDriverAgent会响应启动app,并返回session数据; 3. 启动app后,定位元素以及操作元素 定位元素 post请求:url+/session/element, 请求参数是定位元素标签以及值 参考 FBFindElementCommands.m;响应会返回elementId 操作*元素post请求:url+/session/element/id/* 参考项目中文件:/Commands/FBElementCommands.m 里面介绍了很多元素操作的方法 进行相应的转换即可。 4、测试代码与Webdriver的交互 接下来我会以获取界面元素这个基本的操作为例来分析两者之间的关系。在测试代码中,我们第一步要做的是新建一个webdriver类的对象: 这里新建的driver对象是一个webdriver.Remote()类的对象,而webdriver.Remote()类的本质是 也就是一个来自Remote的WebDriver类。这个.remote.webdriver是继承了selenium.webdriver.remote.command 以python为例,在selenium库中,通过ID获取界面元素的方法是这样的: DATest对其进行二次封装后是这样的: find_elements_by_id是selenium.webdriver.remote.webdriver.WebDriver类的实例方法。在代码中,我们直接使用的其实不是selenium.webdriver.remote.webdriver.WebDriver这个类,而是针对各个浏览器的webdriver类,例如webdriver.Chrome()、webdriver.Remote()。 所以说在测试代码中执行各种浏览器操作的方法其实都是selenium.webdriver.remote.webdriver.WebDriver类的实例方法。 接下来我们再深入selenium.webdriver.remote.webdriver.WebDriver类来看看具体是如何实现例如find_element_by_id()的实例方法的。 通过Source code可以看到: 这个方法最后call了一个execute方法,方法的定义如下: 如注释中提到的,其中的关键在于一个名为command_executor的对象执行了execute方法。 response = self.command_executor.execute(driver_command, params) 名为command_executor的对象是RemoteConnection类的对象,并且这个对象是在新建selenium.webdriver.remote.webdriver.WebDriver类对象的时候就完成赋值的 self.command_executor = RemoteConnection(command_executor, keep_alive=keep_alive)。 结合selenium.webdriver.remote.webdriver.WebDriver类的类注释来看: WebDriver类的功能是通过给一个remote server发送指令来控制浏览器。而这个remote server是一个运行WebDriver wire protocol的server。而RemoteConnection类就是负责与Remote WebDriver server的连接的类。 可以注意到有这么一个新建WebDriver类的对象时候的参数command_executor,默认值='http://127.0.0.1:4444/wd/hub'。这个值表示的是访问remote server的URL。因此这个值作为了RemoteConnection类的构造方法的参数,因为要连接remote server,URL是必须的。 现在再来看RemoteConnection类的实例方法execute。 这个方法有两个参数: command params command表示期望执行的指令的名字。通过观察self._commands这个dict可以看到,self._commands存储了selenium.webdriver.remote.command.Command类里的常量指令和WebDriver wire protocol中定义的指令的对应关系。 以FIND_ELEMENT为例可以看到,指令的URL部分包含了几个组成部分: HTTP请求方法。WebDriver wire protocol中定义的指令是符合RESTful规范的,通过不同请求方法对应不同的指令操作。 sessionId。Session的概念是这么定义的: The server should maintain one browser per session. Commands sent to a session will be directed to the corresponding browser. 也就是说sessionId表示了remote server和浏览器的一个会话,指令通过这个会话变成对于浏览器的一个操作。 element。这一部分用来表示具体的指令。 而selenium.webdriver.remote.command.Command类里的常量指令又在各个具体的类似find_elements的实例方法中作为execute方法的参数来使用,这样就实现了selenium.webdriver.remote.webdriver.WebDriver类中实现各种操作的实例方法与WebDriver wire protocol中定义的指令的一一对应。而selenium.webdriver.rmote.webelement.WebElement中各种在WebElement上的操作也是用类似的原理实现的。 实例方法execute的另一个参数params则是用来保存指令的参数的,这个参数将转化为JSON格式,作为HTTP请求的body发送到remote server。remote server在执行完对浏览器的操作后得到的数据将作为HTTP Response的body返回给测试代码,测试代码经过解析处理后得到想要的数据。 通过对python selenium库的分析,希望能够帮助大家对selenium和webdriver的实现原理有更进一步的了解,在日常的自动化脚本开发中更加快捷的定位问题和解决问题。 附录: appium-log
基础性能评测 1.制定性能基线 基于滴滴其他业务线团队已有的性能经验作为参考依据,如附录1 2.确立性能场景 根据用户行为分析高频重要场景,整体性能时间在20-30分钟,每隔场景的路径不宜过长 3.性能测试方法 使用滴滴内部自研的性能工具ET、哆啦A梦(或GT)获取性能数据 4.生成测试报告 根据操作具体场景获取到的性能数据,DMTC对测试数据进行统计和分析,与制定的基线指标结合给出测试结论或者评分,最后通过报告形式展示在Web端 App性能测试是通过测试工具获取App运行过程中的各项指标数据,然后取平均值,最大值,最小值等统计值作为结果分析。 但基础性能评测只是从统计学的角度来评测App的性能,力度略粗,具体影响性能的问题点并没有暴露出来。 深度性能评测 1.内存泄漏分析 引入LeakCanary,结合自动化测试进行页面的跳转,同时上报泄漏信息。 2.卡顿分析 在App代码中引入BlockCanary工具,使用自动化进行页面测试,收集卡顿信息,并上报到DMTC,生成报告。 附录1 性能测试初步检查项和标准如下: 指标 测试原则 检查项 优先级 Android测试方法 iOS测试方法 备注 流量 1. 资源无重复拉取 2. 资源合理缓存和压缩 3. 业务和流量寻求平衡 是否存在资源的重复拉取 P0 1. 使用ET关注实时流量 2.发现流量异常点,使用Charles抓包查看细节 3.H5页面使用Chrome开发者工具定位 1. 使用GT关注业务场景流量,和历史进行场景对比 2.使用Charles抓包查看细节,发现流量异常点 3.H5页面使用Chrome开发者工具定位 图片大小是否合适(单张Banner大图片小于100K,头像等小于10K) P0 合理缓存(包含H5页面),非首次时流量大幅降低 P0 H5页面中js/css/html代码需要进行压缩和缓存 P1 CPU CPU无长时间占用,正常回落 手机灭屏1min后,CPU占用低于2% P0 1.使用ET关注实时CPU占用 2.发现CPU异常点,使用DDMS工具Threads,TraceView定位 3.后台CPU可以使用命令top -d 1 |grep psnger进行监测 1.使用GT关注实时CPU占用,和历史进行场景对比 2.当发现CPU异常时,可以使用Instruments中的Time Profiler进行耗时方法定位 滴滴有部分场景确实有CPU持续占用,如订单进行中时实时上报位置,当有异议时需和开发确认是否必须如此 应用置于后台1min后,CPU占用低于2% P0 手机前台运行,CPU占用是否超20%占用持续10s P1 手机前台运行,CPU占用是否瞬时超过50% P1 内存 1.无内存泄漏 2.无内存陡增 主功能页面反复进出,内存无泄漏 P0 1.使用ET关注实时PSS内存使用 2.发现疑似性能点后,使用DDMS 抓取hprof文件,使用Mat工具分析 3.可以借助dumpsys meminfo packagename查看内存具体分布 1.使用GT关注实时内存使用,和历史版本数据进行场景对比; 2.使用appconsole,Leaks,Allocation等工具检查和定位内存泄漏; 滴滴Android debug包上有LeakCanary组件可以直接监测内存泄漏,需要遍历主要页面。 进入新页面,内存增量小于20M P1 电量 1.减少无端电量消耗 灭屏静置一会,后台无线程持续运行 P1 1.使用CPU监测方法监测线程活动 2.使用Android5.0 工具 Battery-Historian分析灭屏电量 1.使用GT+系统耗电量排行的方式测试耗电量,和历史版本进行对比; 数据需要和历史对比,和竞品对比 版本间电量无比较大增长(需建立基线数据),如大于20% P0 流畅度 1.页面滑动流畅 2.操作无明显卡顿 1.在主要列表页滑动,监测FPS或者SM大于45 P0 1.使用ET进行流畅度监测,发现疑似点后使用TraceView或SysTrace进行定位 2.4.2及以上系统开发者模式-调试GPU过度绘制打开,查看过渡绘制情况 1.使用GT进行流畅度监测; 2.使用appconsole检查卡顿; 3.使用Core Animation进行FPS测试 4.当发现卡顿时使用Time Profiler定位 2.查看页面过渡绘制,无大片红色区域 P0 加载时间 1.页面打开流畅,无白屏或长时间转圈等待 应用冷启动时长小于3s P0 1.使用ET工具或使用命令 logcat -b event | grep am_activity_launch_time实时监测加载时间 2.发现时间较长的页面,使用TraceView工具定位 录屏或者掐表方式进行 应用热启动时长小于1s P0 首次进入Native页面时长小于1000ms(Wifi) P0 首次进入H5页面时长小于2000ms(Wifi) P1
使用GT的差异化场景 平台 描述 release版本 development版本 Android 在Android平台上,如果希望使用GT的高级功能,如“插桩”等,就必须将GT的SDK嵌入到被调测的应用的工程里,再配合安装好的GT使用。 支持 Android iOS 在iOS平台上,GT是以Framework的形式发布的,使用者只要把GT的Framework合入到被调测工程中,就能使用GT的全部功能。 支持 N/A
第一步 先将环境配置好,包括libimobiledevice、ios-deploy 第二步 在终端,打开appium/appium-xcuitest-driver/WebDriverAgent的安装路径,默认全局位置是 /usr/local/lib/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent 输入命令安装依赖库 mkdir -p Resources/WebDriverAgent.bundle ./Scripts/bootstrap.sh -d 第三步 在 finder,打开appium/appium-xcuitest-driver/WebDriverAgent的安装路径,默认路径同上。 在 WebDriverAgentLib 和 WebDriverAgentRunner 模块的 "General" tab,设置 "Automatically manage signing",并且设置你的开发者团队,应该会自动设置 Signing Ceritificate,看起来如下图所示: 第四步 在下图标记处,选择WebDriverAgentRunner模块和需要运行的真机,然后在主菜单的Product-Test运行项目,如运行成功即可。 收工 完成以上4个步骤,一般都可以完成 Appium的 iOS 真机测试配置。
#-*- coding:utf-8 -*-from case.beatles.test_beatles import TestBeatlesfrom framework.logger import Loggerfrom page.ios.beatles.jenkins_page import Jenkins_Toolclass DownloadApp(TestBeatles): def testDownloadfromJenkins(self): try: jp = Jenkins_Tool() jp.download_app_from_jenkins()except Exception, e: Logger.error(e)self.save_screenshot()self.fail(e.message) #-*- coding:utf-8 -*-from datetime import datetimeimport urllibimport jenkinsimport os job_names = ['job-ios','job_android'] username = 'Jenkins用户名'password = 'Jenkins密码'base_ios_debug_download_url = 'https://xxx/ios/Debug/'base_android_debug_download_url = 'https://xxx/android/Debug/'ios_app_suffix = '.ipa'class Jenkins_Tool(): def __init__(self): self.jenkins_url = 'http://jenkins host url' self.sever = jenkins.Jenkins(self.jenkins_url, username=username, password=password)# 获取最新构建号 def get_build_number(self, job_name): lastest_completed_build_number = self.sever.get_job_info(job_name)['lastCompletedBuild']['number']# print(lastest_completed_build_number) return lastest_completed_build_number# 获取Jenkins下的所有job def get_all_jobs(self): all_josbs = self.sever.get_all_jobs()print(self.sever.jobs_count())for job in all_josbs: print(job['name'])# 获取构建日志 def get_build_console_output(self, job_name): number = self.get_build_number(job_name) resps = (self.sever.get_build_console_output(job_name, number))print(resps)# 生成ios下载的绝对URL def getIosAbsoluteURL(self, job_name): ios_download_url = base_ios_debug_download_url + str(self.get_build_number(job_name)) + '/' + self.getPkgName(job_name)return ios_download_url# 生成android下载的绝对URL def getAndroidAbsoluteURL(self, job_name): android_download_url = base_android_debug_download_url + str(self.get_build_number(job_name)) + '/' + self.getPkgName(job_name)return android_download_url# 拼接包名 def getPkgName(self, job_name): number = self.get_build_number(job_name)if job_name == 'carpool_iosbuild': pkg_name = 'iOS包名前缀' + str(number) + '_.ipa' # print(pkg_name) return pkg_nameelse: pkg_name = 'Android包名.apk' return pkg_name# 从Jenkins下载最新ios App def download_ios_app(self, job_name): local = os.path.join('/Users/didi/Downloads/', self.getPkgName(job_name)) ios_download_url = self.getIosAbsoluteURL(job_name)print('开始下载iOS App...') urllib.urlretrieve(ios_download_url, local)print('iOS App 下载完成')# 从Jenkins下载最新android App def download_android_app(self, job_name): local = os.path.join('/Users/didi/Downloads/', self.getPkgName(job_name)) android_download_url = self.getAndroidAbsoluteURL(job_name)print('开始下载Android App...') urllib.urlretrieve(android_download_url, local)print('Android App 下载完成')# 下载进度 def callbackfunc(blocknum, blocksize, totalsize): '''回调函数 @blocknum: 已经下载的数据块 @blocksize: 数据块的大小 @totalsize: 远程文件的大小 ''' percent = 100.0 * blocknum * blocksize / totalsize if percent > 100: percent = 100 print "%.2f%%" % percent# 重命名ios app def rename(self, file_dir, file_name): os.chdir(file_dir) #切换目录 # print "当前目录为: %s" % os.listdir(os.getcwd()) for root, dirs, files in os.walk(file_dir): os.listdir(os.getcwd()) # 获得当前目录中的内容 for file in files: if os.path.splitext(file)[1] == '.ipa': os.rename(file_name, 'xxx'+ios_app_suffix)print('ios app已重命名为 xxx.ipa')return #从Jenkins下载最新的构建包 def download_app_from_jenkins(self): jt = Jenkins_Tool() start_download_time = datetime.now() # 开始下载时间 jt.download_ios_app('ios-job') jt.rename('/Users/didi/Downloads/',jt.getPkgName('ios-job')) jt.download_android_app('android-job') end_download_time = datetime.now() # 下载完成时间 print('Android & iOS下载耗时:' + str((end_download_time - start_download_time).seconds))return True
方法一:不推荐 driver.execute_script("mobile: scroll", {"direction": "down"}) 实际使用中发现会执行两次滑动操作 方法二:不推荐 driver.execute_script("mobile: swipe", {"direction": "down"}) 滑动距离较小 方法三:推荐使用 driver.execute_script("mobile:dragFromToForDuration",{"duration":0.5,"element":None,"fromX":0,"fromY":650,"toX":0,"toY":100}
# -*- coding:utf-8 -*- import urllib.request from lxml import etree def loadPage(url): """ 作用:根据url发送请求,获取服务器响应文件 url: 需要爬取的url地址 """ # headers = {"User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"} request = urllib.request.Request(url) html = urllib.request.urlopen(request).read() # 解析HTML文档为HTML DOM模型 content = etree.HTML(html) # print content # 返回所有匹配成功的列表集合 link_list = content.xpath('//div[@class="t_con cleafix"]/div/div/div/a/@href') # link_list = content.xpath('//a[@class="j_th_tit"]/@href') for link in link_list: fulllink = "http://tieba.baidu.com" + link # 组合为每个帖子的链接 # print link loadImage(fulllink) # 取出每个帖子里的每个图片连接 def loadImage(link): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"} request = urllib.request.Request(link, headers=headers) html = urllib.request.urlopen(request).read() # 解析 content = etree.HTML(html) # 取出帖子里每层层主发送的图片连接集合 # link_list = content.xpath('//img[@class="BDE_Image"]/@src') # link_list = content.xpath('//div[@class="post_bubble_middle"]') link_list = content.xpath('//img[@class="BDE_Image"]/@src') # 取出每个图片的连接 for link in link_list: print("link:" + link) writeImage(link) def writeImage(link): """ 作用:将html内容写入到本地 link:图片连接 """ # print "正在保存 " + filename headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"} # 文件写入 request = urllib.request.Request(link, headers=headers) # 图片原始数据 image = urllib.request.urlopen(request).read() # 取出连接后10位做为文件名 filename = link[-10:] # 写入到本地磁盘文件内 with open("/Users/didi/Downloads/crawlertest/" + filename, "wb") as f: f.write(image) # print("已经成功下载 " + filename) def tiebaSpider(url, beginPage, endPage): """ 作用:贴吧爬虫调度器,负责组合处理每个页面的url url : 贴吧url的前部分 beginPage : 起始页 endPage : 结束页 """ for page in range(beginPage, endPage + 1): pn = (page - 1) * 50 filename = "第" + str(page) + "页.html" print(filename) fullurl = url + "&pn=" + str(pn) print(fullurl) loadPage(fullurl) # print html print("下载完成") if __name__ == "__main__": kw = input("请输入需要爬取的贴吧名:") beginPage = int(input("请输入起始页:")) endPage = int(input("请输入结束页:")) url = "http://tieba.baidu.com/f?" key = urllib.parse.urlencode({"kw": kw}) fullurl = url + key tiebaSpider(fullurl, beginPage, endPage)
一、问题背景 在开发过程中,往往会听到 “性能优化” 这个概念,这个概念很大,比如网络性能优化、耗电量优化等等,对 RD 而言,最容易做的或者是影响最大的,应该是 View 的性能优化。当业务愈加庞大、界面愈加复杂的时候,没有一个良好的开发习惯和 View 布局优化常识,做出来的界面很容易出现 “卡顿” 现象,从而严重影响用户体验。 结合具体业务特点进行梳理,对于性能问题的产生大致概括为以下3个方面: 1、首先,需求开发或重构过程中,由于 RD 同学的关注点主要放在单个业务开发上,所以很容易忽略性能上的问题,即使在开发过程中发现了卡顿问题,但由于业务紧张,也不太会放下当前的工作去处理性能方面的问题。 2、其次,测试过程中时而会有 QA 同学反馈说某些页面相比之前的版本出现了严重的卡顿问题,但有时通过发版平台、logcat 等并未找到该机型的卡顿记录,最终一些可能存在的性能问题也就不了了之。 3、再次,大部分PM同学关注的都是 RD 是否有100%实现需求,UI/UE 同学关注的是应用的交互体验是否良好,并没有太多同学会去关注应用的性能问题,对于性能较好的手机可能体验不到差距,对于中低档手机,流畅度却起着关键的作用。 二、问题归类 引起卡顿的原因从细节上可分为以下几类原因: 外部因素最为致命!日常开发中更多的应该关心布局的嵌套层级和冗余资源。 比如,当需要将一个 TextView 和一张图片放在一起展示时,我们可以考虑使用 TextView 的 drawableLeft(drawableRight、drawableTop、drawableBottom) 属性来设置图片,而不是使用一个 LinearLayout 来将 TextView 和 ImageView 封装在一起,这样就能减少 View 的绘制层级。 又比如,子元素和父元素都是相同的背景时,就不必在每个子元素中都添加背景属性,等等。 接下来,我们针对过度绘制和布局层级进行测试分析。 三、问题复现 3.1、查看页面是否存在过度绘制 方法一:可通过打开手机“开发者选项”->"调试GPU过度绘制开关",就可以在手机屏幕上查看绘制情况。 方法二:通过 adb 命令开启 GPU 过度绘制调试 :adb shell setprop debug.hwui.overdraw show 如下图所示: 图1:开发者选项 图2:开启GPU过度绘制 开启GPU过度绘制后,点击应用,可以看到各种颜色的区域。依据过度绘制的层度可以分成: 原色:无过度绘制 (一个像素只被绘制了一次) 蓝色:1 次过度绘制 (一个像素被绘制了两次) 绿色:2 次过度绘制 (一个像素被绘制了三次) 粉色:3 次过度绘制 (一个像素被绘制了四次) 红色:4 次及以上过度绘制(一个像素被绘制了五次以上) 图3:GPU绘制图解 接下来我们从设计的角度来看下App是否GPU绘制过度,看一下以下几个界面: 图4:QQ浏览器 图5:顺风车-车服务(优化前) 图6:Google浏览器 从上图我们可以看出,QQ浏览器页面GPU绘制比较正常基本都是在1x-2x范围内,滴滴顺风车(优化前)和Google浏览器过度绘制较为严重,基本都是3x-4x。 颜色越深代表绘制的次数越大,当一个屏幕大部分都被粉丝或红色占据时,我们就必须考虑优化了。 3.2、View的绘制流程 为了更好地理解 View 性能优化的原理,以及造成 “卡顿” 的可能原因,我们简单讲解下View的绘制流程,为后续的 hierarchyviewer 分析做铺垫。 我们都知道,View的绘制分为三个阶段:测量、布局和绘制,这三个阶段各自的作用如下: measure: 为整个 View 树计算实际的大小,即设置实际的高(对应属性:mMeasureHeight)和宽(对应属性:mMeasureWidth),每个 View 的控件的实际宽高都是由父视图和本身视图所决定的。 layout:为将整个根据子视图的大小以及布局参数将 View 树放到合适的位置上。 draw:利用前两部得到的参数,将视图显示在屏幕上。 当一个 Activity 对象被创建完成之后,会将一个 DecorView 对象添加到 Window 中,同时会创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 对象建立联系,然后绘制流程就会从 ViewGroup 的 performTraversals() 方法开始执行,如下图所示: 图7:view的绘制流程 整个绘制流程从 ViewRootImpl 的 performTraversals() 方法开始,在该方法内会调用 performMeasure() 方法进行测量子 View(也就是根 View,顶级的 ViewGroup),调用完 performMeasure()后,会接着调用 performLayout() 和 performDraw() 进行 View 的布局和绘制。 四、问题分析 4.1、较多的背景重叠 过度绘制是指屏幕中某个范围的像素在单个帧中被多次渲染(超过一次),比如父控件设置了背景色,子控件设置了图片显示或者文本显示,这样在子控件的对应区域,就会渲染两次。每次渲染都会带来性能消耗,同一区域渲染的次数越多,那么带来的消耗就越大。 举例来说,粉刷一个房间或一间房子时,给墙壁涂上颜色需要做大量的工作。假如还要重新粉刷一次的话,第二次粉刷的颜色会覆盖住第一次的颜色,第一次的颜色就永远不可见了,等于第一次粉刷做的大量工作就完全被浪费掉。 以"乘客端-顺风车-车服务"页面为例,绘制颜色呈现为红色的原因在于其页面渲染共经历5层绘制: 第一层为地图页,是平台规定的,而顺风车页面对其进行了遮盖处理,所以即使不可见也不可去掉。可参考下图的“快车”地图页 图8:地图页面 第二层为车服务页面的背景渲染,顺风车统一背景色,所以不可修改为无色背景 图9:车服务页面 第三层为页面布局中的卡片布局背景 第四层为页面具体控件元素的渲染 第五层为RD在layout中多添加的一层布局背景 Layout如下图所示 图10:车服务Loyout布局 分析布局可知:多层布局重复设置了背景色导致Overdraw。 4.2、复杂的Layout层级 Android的布局文件的加载是LayoutInflater利用pull解析方式来解析,然后根据节点名通过反射的方式创建出View对象实例;同时嵌套子View的位置受父View的影响,类如RelativeLayout、LinearLayout等经常需要measure两次才能完成,而嵌套、相互嵌套、深层嵌套等的发生会使measure次数呈指数级增长,所费时间呈线性增长。 图11:顺风车车服务首页初始状态(优化前) 图12:初始状态View个数及耗时(优化前) 使用Hierarchy Viewer来看查看一下设置界面,可以从下图中得到首页界面的一些数据及存在的问题: 嵌套共计7层(仅setContentView设置的布局),布局嵌套过深; 共绘制332个View,以及若干个无用布局。 页面渲染总耗时为:50.574ms 由此得到结论:Android渲染需要消耗时间,布局越复杂,性能就越差。那么随着控件数量越多、布局嵌套层次越深,展开布局花费的时间几乎是线性增长,性能也就越差。 五、解决问题 优化的目的,主要就是减少绘制时间。 5.1、过度渲染解决 去掉冗余background后的Overdraw如下图所示: 图13:顺风车乘客端(优化后) 另外一个容易忽略的点是我们的Activity使用的Theme可能会默认的加上背景色,不需要的情况下也可以去掉。 5.2、Layout层级优化 布局的优化其实说白了就是减少层级,越简单越好,减少overdraw,就能更好的突出性能。 5.2.1、尽量使用相对布局 一般情况下用LinearLayout的时候总会比RelativeLayout多一个View的层级。而每次往应用里面增加一个View,或者增加一个布局管理器的时候,都会增加运行时对系统的消耗,因此这样就会导致界面初始化、布局、绘制的过程变慢。 图14:布局比较 选择布局容器的基本准则: 在RelativeLayout和LinearLayout同时能够满足需求时,尽可能的使用 RelativeLayout 以减少 View 层级,因为可以通过扁平的RelativeLayout降低LinearLayout嵌套所产生布局树的层级,使 View 树趋于扁平化。 在不影响层级深度的情况下,使用 LinearLayout 和 FrameLayout 而不是 RelativeLayout。 图15:RelativeLayout布局 5.2.2、使用标签重用Layout 布局重用之include 如果一些布局在许多布局文件中都需要被使用,我们就可以把它单独写在一个布局中,然后使用这个标签在需要使用它的地方把这个布局加进去,这样就达到了重用的目的,最典型的一个用法就是,如果我们自定义了一个TitleBar,这个TitleBar可能需要在每个Activity的布局文件中都使用到,这样我们就可以使用这个标签来实现。 图16:include标签 直接使用include标签的layout来指定就可以把这个bts_home_tip_full_layout的布局文件加入进去,这样在每个Activity中我们就可以使用include标签来重用这个布局了,不需要在每个里面都重复写一个bts_home_tip_full_layout的布局了,下面我们来看看这个bts_home_tip_full_layout的布局文件。 总结一点:这个标签主要是做到布局的重用,使用这个标签可以把公共布局嵌入到所需要嵌入的地方。 布局重用之merge 在使用了include后可能导致布局嵌套过多,多余不必要的layout节点,从而导致解析变慢。例如Layout A中的RelativeLayout布局中使用了include标签,而在引入的布局文件中也包含了RelativeLayout,那么在Layout A的布局中实际会有两个RelativeLayout被加载进行渲染。 在layout中可以使用merge标签来作为include标签的一种辅助扩展来使用,其主要作用是为了防止在引用布局文件时产生多余的布局嵌套,减少布局的深度。 不必要的节点和嵌套可通过hierarchy viewer或设置->开发者选项->显示布局边界查看。 图17:merge标签 5.2.3、按需载入视图 viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,既不会占用显示,也不会占用位置,从而在解析layout时节省cpu和内存。 使用ViewStub并不会影响UI初始化时的性能。 viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。 ViewStub使用延迟加载的方式,当需要时才会加载,避免资源的浪费,减少渲染时间,在需要的时候才加载View。 图18:viewstub标签 最开始使用setContentView(R.layout.bts_home_entrance_layout)的时候,ViewStub只是起到一个占位符的作用,它并不会占用空间,所以对其他的布局没有影响。 当我们点击Button的时候,我们就可以把ViewStub的layout属性指定的布局加载进来,用它来替换ViewStub,这样就把我们需要加载的内容加载进来了。 这里的ViewStub控件的layout指定为bts_home_not_open_layout。当点击button隐藏的时候不会显示bts_home_not_open_layout,而点击button显示的时候就会用bts_home_not_open_layout替代ViewStub。 现在我们再使用Hierarchy Viewer来检测一下: 图19:优化后的车服务首页布局层次 图20:优化之后的View个数及耗时 优化后: 1. 控件数量从332个减少到227个,减少31.6%; 2.优化后的页面总耗时为39.463ms,页面优化提高21.9% 六、建议 1.保持整体背景统一 建议前期在设计时尽量保持整体背景统一,另外可以检查在布局和代码中设置的背景,有些背景是被隐藏在底下的,它永远不可能显示出来,这种没必要的背景尽量要移除,因为它很可能会严重影响到app的性能。 2.检查和优化布局 首先推荐用Android提供的布局工具Hierarchy Viewer来检查和优化布局。 建议1:如果嵌套的线性布局加深了布局层次,可以使用相对布局来取代。 建议2:用标签来合并布局,这可以减少布局层次。 建议3:用标签来重用布局,抽取通用的布局可以让布局的逻辑更清晰明了。
下载Eclipse Memory Analyzer在mac上打开的时候出现以下异常: !SESSION 2017-05-13 15:25:56.717 ----------------------------------------------- eclipse.buildId=unknown java.version=1.8.0_111 java.vendor=Oracle Corporation BootLoader constants: OS=macosx, ARCH=x86_64, WS=cocoa, NL=zh_CN Framework arguments: -keyring /Users/sailfish/.eclipse_keyring -showlocation Command-line arguments: -os macosx -ws cocoa -arch x86_64 -keyring /Users/sailfish/.eclipse_keyring -showlocation !ENTRY org.eclipse.osgi 4 0 2017-05-13 15:25:58.676 !MESSAGE Application error !STACK 1 java.lang.IllegalStateException: The platform metadata area could not be written: /private/var/folders/k4/knjt7v5x59l25z_tqmvg094r0000gn/T/AppTranslocation/3CBB3175-DD9A-4A3A-B93F-898BA4445384/d/mat.app/Contents/MacOS/workspace/.metadata. By default the platform writes its content under the current working directory when the platform is launched. Use the -data parameter to specify a different content area for the platform. at org.eclipse.core.internal.runtime.DataArea.assertLocationInitialized(DataArea.java:61) at org.eclipse.core.internal.runtime.DataArea.getStateLocation(DataArea.java:129) at org.eclipse.core.internal.preferences.InstancePreferences.getBaseLocation(InstancePreferences.java:44) at org.eclipse.core.internal.preferences.InstancePreferences.initializeChildren(InstancePreferences.java:199) at org.eclipse.core.internal.preferences.InstancePreferences.<init>(InstancePreferences.java:59) at org.eclipse.core.internal.preferences.InstancePreferences.internalCreate(InstancePreferences.java:209) at org.eclipse.core.internal.preferences.EclipsePreferences.create(EclipsePreferences.java:391) at org.eclipse.core.internal.preferences.EclipsePreferences.create(EclipsePreferences.java:379) at org.eclipse.core.internal.preferences.PreferencesService.createNode(PreferencesService.java:389) at org.eclipse.core.internal.preferences.RootPreferences.getChild(RootPreferences.java:63) at org.eclipse.core.internal.preferences.RootPreferences.getNode(RootPreferences.java:101) at org.eclipse.core.internal.preferences.RootPreferences.node(RootPreferences.java:90) at org.eclipse.core.internal.preferences.AbstractScope.getNode(AbstractScope.java:38) at org.eclipse.core.runtime.preferences.InstanceScope.getNode(InstanceScope.java:80) at org.eclipse.ui.preferences.ScopedPreferenceStore.getStorePreferences(ScopedPreferenceStore.java:229) at org.eclipse.ui.preferences.ScopedPreferenceStore.<init>(ScopedPreferenceStore.java:133) at org.eclipse.ui.plugin.AbstractUIPlugin.getPreferenceStore(AbstractUIPlugin.java:288) at org.eclipse.ui.internal.Workbench$5.run(Workbench.java:620) at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:337) at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:606) at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:150) at org.eclipse.mat.ui.rcp.Application.start(Application.java:26) at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:196) at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:134) at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:104) at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:380) at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:235) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:669) at org.eclipse.equinox.launcher.Main.basicRun(Main.java:608) at org.eclipse.equinox.launcher.Main.run(Main.java:1515) 原因/private/var/folders/k4/knjt7v5x59l25z_tqmvg094r0000gn/T/AppTranslocation/3CBB3175-DD9A-4A3A-B93F-898BA4445384/d/mat.app/Contents/MacOS/workspace/.metadata是只读文件 解决方案: 1.邮件点击mat.app,选择显示包内容 2.进入eclipse文件夹,打开 MemoryAnalyzer.ini 3.添加data相关信息 -data /Users/didi/mat-log 4.再次打开mat即可正常使用 注意事项 data参数和路径必须在两个不同的行 data参数必须放在Laucher之前,否则启动还是不成功
浅析APP控件:模态弹窗与非模态弹窗 在手机app应用中各种格式的弹窗效果相信大家都看过,此次分享就来谈谈关于app弹窗设计以及弹窗的适用情景。 一、弹窗的定义 1、弹窗作用 弹窗是为了让用户回应,需要用户与之交互的窗口。 ==非模态弹窗一般被设计成用来告诉用户信息内容,而模态弹窗除了告诉用户信息内容外还需要用户进行功能操作。== 2、模态弹窗 会打断用户的操作行为,强制用户必须进行操作,否则不可以进行其他操作。(Alerts/dialog,Actionbar,Popover) 3、非模态弹窗 不会影响用户操作,用户可以不与回应,通常有时间限制,出现一段时间就会自动消失。(Toast/HUD,Snackbar) 二、弹窗分类 以下将以各类弹窗的含义、作用、适用来进行浅析 Alerts/Dialog:警告框与对话框 含义:意为警告、对话,跟弹窗属性非常吻合,就是紧急状况,打扰用户的行为。 剖析:iOS中警告框称之为Alerts,作用是用来传达重要信息,并伴随着需要用户进行操作。 iOS规范中,警告框包含的元素如下:标题(必选)、描述信息(可选)、输入框(可选)、按钮(必选),必须包含标题、包含一个或多个按钮。 Android规范中,弹窗交互按钮需结合实际情况,不用“是/否”原则进行设计。 作用:告知用户当前发生的状况,让用户主动选择回应。 适用:重要性较高的操作时,如退出、删除、清空等 Actionbar(Sheets、Acitivity View):操作栏、操作列表、活动视图 含义:译为工具栏、操作栏 剖析:当用户触发某一个操作的时候,出现此窗口。 一般会给用户提供更多的功能选择,一般可采用官方控件。 一般都设计有一个默认的“取消”按钮,点击取消可以关闭弹窗。 Aciton Sheets(动作菜单/动作面板/行动列表)和Activity Views是iOS上特有的交互形式,其特性如下: 是由用户操作后触发的一种特定的模态弹出框 ,呈现一组与当前情境相关的两个或多个选项。用户可以使用Action Sheet启动某个任务,或者确认是否开始执行某个可能具有破坏性的操作。Action Sheet属于iOS规范,近年来Android平台也出现了类似功能的控件。 作用:操作列表提供一系列在当前情景下可以完成当前任务的操作,而这样的形式不会永久占用页面UI的空间。 适用:如分享功能 Popover/Popup:浮出框/浮层弹窗 含义:意为弹出窗口,像气泡一样浮动于顶层窗口 剖析:当用户点击某个控件或者某个区域时浮出的半透明或者不透明的弹窗窗口,不会对用户所在位置进行跳转。 作用:可以在当前页面进行更多的操作行为,显示/隐藏页面中的折叠信息。 适用:首页位置呈现一些常用操作的快捷入口。 Toast/HUD:提示框(iOS没有Toast,只有HUD) 含义:Toast也被称为吐司提示,Toast是安卓系统的一个控件名词,现也应用于iOS系统中。 剖析:提示框属于一种轻量级的弹窗反馈形式,常以小弹框的形式出现,持续1-2秒自动消失,可以出现在屏幕任意位置。 提示信息能给予用户及时反馈,确保用户知晓自己所处的状态,并可以做出相应的措施。 iOS用户更习惯于在顶部感知反馈信息,不干扰用户浏览主体内容。Toast出现在屏幕顶部不会遮挡主体内容。(如花瓣、有道云笔记) Android正统的规范中Toast: 出现在屏幕底部。 只能放文字不能带图标,文字要精简不宜太长。 不是模态的,可以透过Toast对其他控件进行操作。 短时间后会自动消失。 不能对Toast进行交互。 优先适用于系统提示,不能手动操作让Toast主动消失。 (以上为今日头条、微博、即刻) HUD与Toast的区别: HUD只出现在屏幕的中央,Toast则在底部; HUD可以包含icon,Toast只能纯文字; HUD一般是毛玻璃透明,Toast一般是灰黑或者黑色半透明; HUD中内容可以变化(如调节音量时),Toast中内容不可变化 作用: 优点: 占用屏幕空间小。 不会打断用户操作。 使用简单适用范围广 缺点: 出现时间短,在碎片化时代注意力不集中容易错过Toast提示。 遮盖其他控件,但不能对Toast进行交互。 适用:提示不需要的反馈信息,如刷新后的成功状态。 Snackbar:底部弹窗 Android特有的交互形式,在Google的MD规范中,将Toast和Snackbars归为一类。有些时候也有应用在iOS系统中,也可以理解为加强版的Toast。 含义:译为快餐、小吃。 剖析:Snackbars与toast一样是从屏幕底部向上出现,但是Snackbar不同的是可以经过用户进行其他操作而消失。 适用:较多适用于撤销操作。 三、总结 通过此次分享,希望能够帮助QA同学更好的了解我们产品内的控件。通过分析和了解弹窗的类别、适用范围,才能更好地在日常测试工作中了解RD描述App控件的专业术语、能够在提交bug时描述的更为专业和清楚。
安装教程可以参考我之前写的,这里不再多说,直接说问题。 安装完成后输入stf doctor查看工具依赖是否正确 问题1:Unexpected error checking ZeroMQ: Error: Module version mismatch. Expected 48, got 47. 问题分析:根据错误提示,初步定位node moudle的版本不一致,期望的是48,而我的node moudle 是47.于是进入node官网(https://nodejs.org/en/download/releases/)下载moudle 等于48的nodejs版本。 node moudle version 可以根据下图中红框一列来确定自己所需要的版本。 解决方法:将v5.x版本升级到v6.x后,再次运行stf doctor,该问题解决。又出现问题2。 ps:问题灵感源:https://github.com/nodejs/node/wiki/Breaking-changes-between-v6-and-v7 Native Modules (Addons) The Native Module version mismatch error has been updated to be far more clear. Refs: [1fda657cac], #8391 Previously: Module version mismatch. Expected 51, got 48. Now: The module '<module>' was compiled against a different Node.js version using NODE_MODULE_VERSION 48. This version of Node.js requires NODE_MODULE_VERSION 51. Please try re-compiling or Re-installing the module (for instance, using `npm rebuild` or `npm install`). The NODE_MODULE_VERSION is now 51. Refs: [96933df2ff], #8808 问题2:Error: The module '/usr/local/lib/node_modules/stf/node_modules/_zmq@2.15.3@zmq/build/Release/zmq.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 47. This version of Node.js requires NODE_MODULE_VERSION 57. Please try re-compiling or re-installing the module 问题分析:在GitHub上看到外国友人这么说的: 大概是说在安装某个依赖应用中,我们升级或者安装了其他版本的node版本导致安装应用时所使用node版本与我们变更后当前的node版本不一致。 解决方法:根据错误提示建议,及国外友人建议,执行npm install 或 npm rebuild去重新构建node,注意:执行npm install / npm rebuild命令必须进入stf根目录执行 问题3: scripts.prepublish: "bower install --allow-root && not-in-install && gulp build || in-install" (node:1305) fs: re-evaluating native module sources is not supported. If you are using the graceful-fs module, please update it to a more recent version. module.js:442 throw err; ^ Error: Cannot find module 'strip-json-comments' at Function.Module._resolveFilename (module.js:440:15) at Function.Module._load (module.js:388:25) at Module.require (module.js:468:17) at require (internal/module.js:20:19) at Object.<anonymous> (/root/stf-master/node_modules/.npminstall/eslint/2.13.0/eslint/lib/config/config-file.js:23:21) at Module._compile (module.js:541:32) at Object.Module._extensions..js (module.js:550:10) at Module.load (module.js:458:32) at tryModuleLoad (module.js:417:12) at Function.Module._load (module.js:409:3) Error: Run "sh -c bower install --allow-root && not-in-install && gulp build || in-install" error, exit code 1 Error: Run "sh -c bower install --allow-root && not-in-install && gulp build || in-install" error, exit code 1 at ChildProcess.proc.on.code (/usr/lib/node_modules/cnpm/node_modules/runscript/index.js:67:21) at emitTwo (events.js:106:13) at ChildProcess.emit (events.js:191:7) at maybeClose (internal/child_process.js:852:16) at Process.ChildProcess._handle.onexit (internal/child_process.js:215:5) 解决方法:参考 https://toutiao.io/posts/9kf4j3/preview 方可解决 问题4:插入手机后,STF显示手机信息但一直处于disconnect状态,刷新页面后,手机设备信息全部消失。查看stf控制台输出,如下: FTL/util:lifecycle 7678 [5e56e8f2] Shutting down due to fatal error INF/provider 6994 [*] Cleaning up device worker "5e56e8f2" ERR/provider 6994 [*] Device worker "5e56e8f2" died with code 1 npm ERR! jpeg-turbo@0.4.0 install: node-pre-gyp install --fallback-to-build npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the jpeg-turbo@0.4.0 install script 'node-pre-gyp install --fallback-to-build'. 解决方法: 1、install yams, run brew install yasm 2、At last run cnpm install -g stf
1、xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> xctestwd start with port: 8001 >> xctest-client.js:224:14 [master] pid:23473 xcode version: 8.3.3 >> XCTestWD version: 1.1.3 >> xctest-client.js:172:14 [master] pid:23473 2017-09-06 13:25:53.829 xcodebuild[23972:1511642] IDETestOperationsObserverDebug: Writing diagnostic log for test session to: /Users/iwm/Library/Developer/Xcode/DerivedData/XCTestWD-efsibylwwbocoagvfhextzqaiznx/Logs/Test/83B29A51-3E20-40D5-9FC5-8AF3FF9E65B7/Session-XCTestWDUITests-2017-09-06_132553-0OzQOm.log >> xctest-client.js:173:14 [master] pid:23473 please check project: /usr/local/lib/node_modules/macaca-ios/node_modules/_xctestwd@1.1.3@xctestwd/XCTestWD/XCTestWD.xcodeproj >> xctest-client.js:172:14 [master] pid:23473 2017-09-06 13:25:53.829 xcodebuild[23972:1511554] [MT] IDETestOperationsObserverDebug: (D95D65A7-8E60-4C4A-99F9-8625E6865131) Beginning test session XCTestWDUITests-D95D65A7-8E60-4C4A-99F9-8625E6865131 at 2017-09-06 13:25:53.829 with Xcode 8E3004b on target <DVTiPhoneSimulator: 0x7f926ffca720> { SimDevice: SimDevice : iPhone 6 (916D2F3F-4FB9-4645-976B-0AE22822073B) : state={ Booted } deviceType={ SimDeviceType : com.apple.CoreSimulator.SimDeviceType.iPhone-6 } runtime={ SimRuntime : 10.3.1 (14E8301) - com.apple.CoreSimulator.SimRuntime.iOS-10-3 } } (10.3.1 (14E8301)) >> xctest-client.js:173:14 [master] pid:23473 please check project: /usr/local/lib/node_modules/macaca-ios/node_modules/_xctestwd@1.1.3@xctestwd/XCTestWD/XCTestWD.xcodeproj >> proxy.js:55:14 [master] pid:23473 Proxy: /wd/hub/session:POST to http://localhost:8001/wd/hub/session:POST with body: {"desiredCapabilities":{"bu... >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: socket hang up >> xctest-client.js:172:14 [master] pid:23473 2017-09-06 13:26:15.954 xcodebuild[23972:1511565] IDETestOperationsObserverDebug: Writing diagnostic log for test session to: /Users/iwm/Library/Developer/Xcode/DerivedData/XCTestWD-efsibylwwbocoagvfhextzqaiznx/Logs/Test/83B29A51-3E20-40D5-9FC5-8AF3FF9E65B7/Session-XCTestWDUITests-2017-09-06_132615-86y4Ws.log >> xctest-client.js:173:14 [master] pid:23473 please check project: /usr/local/lib/node_modules/macaca-ios/node_modules/_xctestwd@1.1.3@xctestwd/XCTestWD/XCTestWD.xcodeproj >> xctest-client.js:172:14 [master] pid:23473 2017-09-06 13:26:15.954 xcodebuild[23972:1511554] [MT] IDETestOperationsObserverDebug: (FC34FA0B-C27D-4C22-B1C4-603C0CD416F7) Beginning test session XCTestWDUITests-FC34FA0B-C27D-4C22-B1C4-603C0CD416F7 at 2017-09-06 13:26:15.954 with Xcode 8E3004b on target <DVTiPhoneSimulator: 0x7f926ffca720> { SimDevice: SimDevice : iPhone 6 (916D2F3F-4FB9-4645-976B-0AE22822073B) : state={ Booted } deviceType={ SimDeviceType : com.apple.CoreSimulator.SimDeviceType.iPhone-6 } runtime={ SimRuntime : 10.3.1 (14E8301) - com.apple.CoreSimulator.SimRuntime.iOS-10-3 } } (10.3.1 (14E8301)) >> xctest-client.js:173:14 [master] pid:23473 please check project: /usr/local/lib/node_modules/macaca-ios/node_modules/_xctestwd@1.1.3@xctestwd/XCTestWD/XCTestWD.xcodeproj >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> xctest-client.js:172:14 [master] pid:23473 Failing tests: >> xctest-client.js:173:14 [master] pid:23473 please check project: /usr/local/lib/node_modules/macaca-ios/node_modules/_xctestwd@1.1.3@xctestwd/XCTestWD/XCTestWD.xcodeproj >> xctest-client.js:172:14 [master] pid:23473 -[XCTextWDRunner testRunner()] ** TEST FAILED ** >> xctest-client.js:173:14 [master] pid:23473 please check project: /usr/local/lib/node_modules/macaca-ios/node_modules/_xctestwd@1.1.3@xctestwd/XCTestWD/XCTestWD.xcodeproj >> xctest-client.js:255:14 [master] pid:23473 killing deviceLogProc pid: 23973 >> xctest-client.js:260:14 [master] pid:23473 killing runnerProc pid: 23972 >> xctest-client.js:183:14 [master] pid:23473 xctest client exit with code: 65, signal: null >> xctest-client.js:108:18 [master] pid:23473 simulator log process exit with code: null, signal: SIGKILL >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> proxy.js:61:22 [master] pid:23473 xctest client proxy error with: Error: connect ECONNREFUSED 127.0.0.1:8001 >> xctest-client.js:247:14 [master] pid:23473 Fail to start xctest: Error: connect ECONNREFUSED 127.0.0.1:8001 解决方法: 对于iOS平台,在模拟器上跑用例时 app的安装包需要基于.app包压缩后的zip包(这个.app包可以找对应的iOS开发同学提供,在XCode工程目录下Products目录下),而不能用.ipa包进行压缩; 对于真机,可以直接使用.ipa文件,不过需要涉及证书签名等问题,关于这个,可参考https://testerhome.com/topics/6503。另外,对于配置参数,iOS与安卓有各自特有的参数,具体配置信息可参考:desired-caps 2、官方常见问题汇总(https://macacajs.com/zh/faq) iOS中的Webview链接不上 Android的Webview版本问题 Promise编写异步测试用例 导出测试报告失败 权限问题导致的无法运行 Linux环境下运行注意事项 Windows环境下的问题 如何查找和定位Native和网页的元素 iOS 包编译问题导致无法运行 iOS 真机证书问题 移动端手势相关 截图结果相关问题 多任务测试 多设备测试
一、Macaca框架 PS:上图所有模块均可以在官方github上找到对应的源码 https://github.com/macacajs 二、各模块浅析 2.1、Macaca 2.1. macaca-cli Macaca提供的命令行工具 $macaca server 启动server $macaca server --verbose 启动server并打印详细日志 $macaca doctor 检验当前macaca环境配置 2. app-inspector macaca提供的元素查找工具,可以将app视图的结构以布局结构树的格式在浏览器上展示出来,用过点击某个元素,就可以方便的查询到该控件的基本信息,以方便查找。具体使用可参考官网: https://macacajs.com/inspector 3. UI Recorder macaca提供的脚本录制工具,可以通过录制获得脚本,对于入门同学很有帮助。https://macacajs.com/recorder 2.2、WebDriver-Server Macaca是按照经典的Server-Client设计模式进行设计的,也就是我们常说的C/S架构。WebDriver-server部分便充当了server这部分的角色,他的职责就是等待client发送请求并做出响应。 2.3、WebDriver-Client client端简单来讲就是我们的测试代码,我们测试代码中的一些行为,比如控件查找、点击等,这些行为以http请求的方式发送给server,server接收请求,并执行相应操作,并在response中返回执行状态、返回值等信息。 也正是基于这种经典的C/S架构,所以client端具有跨语言的特点,macaca-wd,wd.java,wd.py分别是Macaca团队针对Js Java 以及Python的封装,只要能保证client端遵循webdriver的协议,任意语言都可以。 2.4、DriverList 自动化要在不同的平台上跑,需要有对应平台的驱动,这部分驱动接收到来自server的操作命令,驱动各自平台的底层完成对应的操作。 1. Android Macaca针对安卓平台的驱动集合 macaca-android 安卓驱动 macaca-adb 封装了安卓的adb命令,来实现一些adb的操作,比如安装、卸载、启动app、获取设备列表这些操作 android-unicode 经过封装后的输入法,解决中文输入的问题 uiautomator-client 将来自server的操作指令转换为UIAutomator可以识别的指令,驱动uiautomator完成对应的操作 android-performance 用于自动化测试安卓性能相关的支持 2. iOS Macaca针对iOS平台的驱动集合 macaca-ios iOS驱动 xctest-client 同安卓的uiautomator-client异曲同工,对XCUITest的封装,将来自server的操作指令转换为XCUITest可以识别的指令,驱动XCUITest完成对应的操作 ios-simulator 用于对ios模拟器的支持,可以通过模拟器运行用例 remote-debug 用于远程调试 3. Hybrid Macaca针对Hybrid的驱动集合。 macaca-chrome web测试驱动 macaca-chromedriver 驱动chrome浏览器 ios-webkit-debug-proxy 适用于iOS平台对webview的调试 4. Electron Macaca针对pc端网页应用的支持 macaca-electron 三、Macaca执行流程图 了解了Macaca的组成模块以及他们各自的作用,下面我们看一下各个模块是如何组装起来实现自动化测试流程的 四、结合实例讲解Macaca基本原理 文章以百度外卖APP(iOS版)为例,下面我们开始我们的用例执行。 1. 启动macaca server 从这一步打印的信息我们可以看到,这一步实际上执行的是流程图中第一步的操作,启动server,建立连接,然后server返回所连接的ip以及端口号,因为我们是本地跑,所以ip实际上是本机的ip地址 2. 执行用例 以命令行为例,先执行“” build工程,稍作等待,就会看到项目构建结果,如下图所示 然后执行“”运行测试用例,稍作等待,就会看到系统自动启动了ios的模拟器并跑起来了用例。执行过程中的某个截图如下: 我们来看一下对应用例启动的client端核心代码: 在这段代码中,我们做的工作是根据不同的平台设置用例的一些基础启动信息,包含平台类型、安装包地址、设备id、是否覆盖安装等参数,设置完成后,通过driver.initDriver(desiredCapabilities)这个操作启动driver,这个过程便会按照流程图中的第二个步骤发送http请求,server会接收到这个请求并创建一个session,在这次的用例执行中,所有的操作都会基于这个session进行,来看一下针对这个操作控制台所打印的信息(为方便突出主要过程省略了部分无关日志): 经过如上步骤后,连接便已经成功建立了,下一步我们以添加商品为例再分析一下具体操作,对应的client端的代码如下: (因为框架层封装了一些操作,所以代码看上去比较少,具体的控件查找部分看不到,有需要详细了解的可以研究源码) 执行对应按钮的操作,我们会在控制台上看到如下的日志: 在上面的日志中我们可以看到,当我们查找登录按钮的时候,client发送了一个http请求给server,请求的操作是element(这个表示控件查找),参数是{"using":"name","value":"搜索"},这是告诉server我们要找的这个控件的name属性是“搜索”,server收到这个请求,通过router路由转发给iOS的驱动(在启动driver的时候已经设置的平台类型,因此这里能知道找ios),iOS驱动收到请求驱动XCUITest框架对模拟器上的目标app执行对应的控件查找操作,得到response后原路返回给client,这样就完成了一次请求的完整的生命周期。
一、业内相对流行的几款UI自动化框架 二、他们的区别在哪里? 三、有哪些可用的UI自动化框架?我们是如何选择的? 条件1:支持移动端app自动化 从以上对比其实已经可以比较明确的帮助大家做出自己的选择,从我所在的团队来讲,我们主要做的是移动端的UI自动化,相信现在大多数同学所做的也都是这一类的自动化,因此,仅限于PC端webApplication的几个框架就不可避免的要排除掉了,这其中包含Selenium,PhantomJS,以及KARMAR。 条件2:支持多平台自动化 此外,对于移动端的UI自动化,我们希望可以同时覆盖安卓以及iOS平台,最好是一套脚本能同时在两个端上跑,鉴于此,只提供单一平台的Selendroid,Robotium可以暂时不用考虑了。 条件3:学习成本低 经过上面两次筛选,我们的选择剩下了Macaca && Appium && Calabash,这其中,Macaca以及Appium都是支持多语言的,Appium支持的最多,包含了Ruby Python Java Js OC PHP C#(.Net)这些几乎所有主流的语言,Macaca目前支持Js Java以及Python,也能基本满足需要,相比之下,Calabash只支持Ruby,这个对团队是有一定的挑战的,因为我们的团队大家基本上以Java技术栈为主,如果采用Ruby,意味着所有的同学都要先学习一下这门语言,这个成本对于我们这样的团队而言成本是很高的,因此,Calabash也从我们的待选list中删除。 四、最后的抉择 经过三轮筛选,目前摆在我们面前的有两个选择,Appium && Macaca,经过一段时间的对比调研,我们最终选择了Macaca,主要考虑因素如下: 周边工具支持 相对Appium,Macaca提供了更加全面的周边工具支持,这其中包含可持续集成平台Reliable,元素查找工具app-inspector,脚本录制工具UI-Recorder等。 Reliable持续集成平台可以帮助我们进行用例的管理以及任务的调度,对于UI自动化,只有当他成为一种规范化的程式定期的触发与执行,这样才能发挥他的作用,因此,一个持续集成系统对于自动化的长期发展是必不可少的,而Appium并没有提供这套系统。目前外卖团队一直使用基于Jenkins的CI。而对于无CI的团队来讲,这意味着需要从无到有的搭建自己的一套持续集成环境,这个投入无疑也是巨大的。 app-inspector 元素查找工具,极大的方便了控件的查找以及定位。 UI-Recorder脚本录制工具可以快速的通过录制得到脚本,方便新手入门。 具体的使用可以参考官方网站,都有详细的介绍。 轻量 从功能上来讲,Appium较Macaca是有优势的,Appium已经发展了多年,积累了很多经验,但是也造成了一些尾大不掉的毛病。以对安卓API版本的支持为例,Appium支持所有的安卓API版本,而Macaca只支持API>17(相当于Android4.2系统)的版本,这个跟两者的底层原理有关,Macaca对于安卓的支持是基于安卓sdk的UIAutomator框架,而这个框架是从API 17开始支持的,Appium从最早的安卓开始,对于API 17以上的版本,Appium与Macaca一样是基于UIAutomator的,对于API 17以下的版本,Appium则基于老的instrument,对于这部分的支持,还引入了Selendroid。但是从应用的角度讲,Android7已经发行,4.2系统以下的设备占比不大,我们不希望为了支持这部分少量的机型而增加自己在自动化上的工作量。简单来讲,大而全的并不一定是最好的,关键是找到适合自己的。 社区活跃,中文文档丰富 Macaca是由阿里集团开源的框架,官方网站提供了中文版以及英文版双语文档,虽然对于开发者而言,阅读英文文档的能力是必须的,但是中文文档的提供无疑能帮助很多基础相对薄弱的同学快速入门。此外,Macaca团队还提供了用于技术交流的微信群以及钉钉群,当遇到问题的时候可以方便的联系到主创团队的相关同学,这无疑也能给大家带来很大的方便。 平台更丰富 虽然Macaca和Appium同时都支持PC与移动端,但是Macaca新增了对于Electron应用的支持,这个是其他框架都不具备的。 API的统一性 研究过Appium的官方API,以java-client为例,针对iOS和安卓,控件以及Driver等类别都根据平台不同而不一样,对于控件,有AndroidElement,IOSElement,对于Driver,有AndroidDriver,IOSDriver,如果我们的用例要支持多平台,就需要处理多种平台不一致性,但Macaca从底层上就没有区分iOS与安卓,基本上除了各自系统特有的几个API以外都可以通过一个统一的API支持,这就方便了很多。 特别说明 虽然我们从开始就排除了Selenium,但是必须要特别说明的一点不管是Macaca还是Appium,他们在一定程度上都借鉴了Selenium的很多内容,包括对webdriver协议的支持,selenium grid的方式支持多机并行等,因此,对UI自动化感兴趣的同学,有必要先去了解一下Selenium的基础原理,这对于理解Macaca与Appium的底层原理都有很重要的帮助。 参考资料: Macaca官网 Appium testerHome中文文档 Selenium官网 Sendroid官网 Robotium官网 PhantomJs官网 Calabash官网 KARMA官网
Macaca 提供多端录制回放工具 UIRecorder,录制回放工具无疑是最受用户欢迎的工具。录制是生产用例最低成本的方式,测试用例在这个过程中已经变成了“中间产物”。 跨多端设计 UIRecorder 支持 PC 和移动端,能够支持所有平台是因为 UIRecorder 在操作层和 Webdriver 服务端做了良好的解耦设计。UIRecorder 在 PC 端重度依赖 Chrome Extension 的事件拦截,移动端直接则与 Macaca Server 通信。 运行原理 启动 Macaca Server,提供 Webdriver 服务 UIRecorder 在启动时会启动 ChromeDriver 服务 并加载内置的 Chrome 扩展程序 事件处理 用户端事件相对复杂的是 drag 手势,当操作左侧截图图片时,会执行如下的事件处理。 screenshot.addEventListener('click', function(event) { var upX = event.offsetX, upY = event.offsetY; var clickDuration = new Date().getTime() - downTime; if (Math.abs(downX - upX) < 20 && Math.abs(downY - upY) < 20) { var cmdData = getNodeInfo(Math.floor(upX * scaleX), Math.floor(upY * scaleY)); if (isSelectorMode) { if (cmdData.path) { showExpectDailog(cmdData.path); } } else { var pressTime = new Date().getTime() - downTime; cmdData.duration = (pressTime / 1000).toFixed(2); saveCommand(clickDuration> 500 ? 'press' : 'click', cmdData); } downTime = 0; } }); screenshot.addEventListener('mousedown', function(event) { downX = event.offsetX; downY = event.offsetY; downTime = new Date().getTime(); event.stopPropagation(); event.preventDefault(); }); screenshot.addEventListener('mouseup', function(event) { var upX = event.offsetX, upY = event.offsetY; if (downX >=0 && downY >= 0 && upX >= 0 && upY >= 0 && (Math.abs(downX - upX) >= 20 || Math.abs(downY - upY) >= 20)) { var dragTime = new Date().getTime() - downTime; saveCommand('drag', { fromX: Math.floor(downX * scaleX), fromY: Math.floor(downY * scaleY), toX: Math.floor(upX * scaleX), toY: Math.floor(upY * scaleY), duration: (dragTime / 1000).toFixed(2) }); downTime = 0; } event.stopPropagation(); event.preventDefault(); }); document.addEventListener('mouseup', function(event) { if (downTime !== 0) { var upX = event.clientX < screenshot.offsetLeft ? 0 : screenshot.width -1; var upY = event.clientY; var dragTime = new Date().getTime() - downTime; saveCommand('drag', { fromX: Math.floor(downX * scaleX), fromY: Math.floor(downY * scaleY), toX: Math.floor(upX * scaleX), toY: Math.floor(upY * scaleY), duration: (dragTime / 1000).toFixed(2) }); downTime = 0; } }); screenshot.addEventListener('mousemove', function(event) { var bestNodeInfo = { node: null, boundSize: 0 }; var x = Math.floor(event.offsetX * scaleX); var y = Math.floor(event.offsetY * scaleY); getBestNode(appTree, x, y, bestNodeInfo); var node = bestNodeInfo.node; if (node) { var offsetLeft = screenshot.offsetLeft; var offsetTop = screenshot.offsetTop; var left = node.startX / scaleX; var top = node.startY / scaleY; var width = node.endX / scaleX - left; var height = node.endY / scaleY - top; showLine(left + offsetLeft, top + offsetTop, width, height); } else { hideLine(); } }); document.addEventListener('keypress', function(event) { if(!isLoading && !isShowDialog){ showTextDailog(event.key); event.stopPropagation(); event.preventDefault(); } }); 录制是双刃剑 录制工具可以量产用例,将测试用例的生产成本降到最低,但从实质上看产生的测试用例可维护性也降低了,同时无法模块化,二次维护成本却提高了,只能重新录制。对于功能固化,不需要频繁维护的场景可以适当使用录制。 https://github.com/alibaba/uirecorder
为方便更多用户查找界面元素,提供了同时支持 Android 和 iOS 平台的 Inspector 工具,此功能也是 Macaca 录制器的重要部分。另外,Web 元素查找请直接只用 chrome-inspector。 Android iOS 安装 要安装app-inspector,你需要首先安装Node.js。国内用户可以安装cnpm加快NPM模块安装速度。(切换阿里云镜像)推荐安装macaca-cli$ npm install macaca-cli -g你需要准备好你需要进行查看的移动平台的环境。Android 请安装Android SDK,iOS 安装Xcode。 然后使用macaca命令行工具检测环境是否准备好。$ macaca doctor如果你看到一堆绿色的文字输出了,说明你的这个环境是OK 的。然后你就可以安装使用app-inspector。app-inspector安装$ npm install app-inspector -g从命令行启动 用法 查看手机devices$ adb devices$ app-inspector -u YOUR-DEVICE-ID关于如何获取设备ID,请查看获取设备ID 部分。你的命令行将输出如下的文字inspector start at: http://192.168..:5678**然后在浏览器里面打开输出的链接http://192.168..:5678。推荐用Chrome浏览器。 更多用法和信息请参考 Macaca 文档。
启动sonar服务:/usr/local/sonarqube-5.6.6/bin/macosx-universal-64/sonar.sh console sonar sonar-runner jnekins 全局配置 sonar sonar-runner job配置 job执行结果
工具 目的 检查项 FindBugs 检查.class 基于Bug Patterns概念,查找javabytecode(.class文件)中的潜在bug。 它使用静态分析方法标识出Java程序中上百种潜在的不同类型的错误。 主要检查bytecode中的bug patterns,如NullPoint空指针检查、没有合理关闭资源、字符串相同判断错(==,而不是equals)等 PMD 检查源文件 一个基于静态规则集的Java源码分析器, 检查Java源文件中的潜在问题 主要包括: – 可能的bug——空的try/catch/finally/switch块。– 无用代码(Dead code):无用的本地变量,方法参数和私有方法。– 空的if/while语句。– 过度复杂的表达式——不必要的if语句,本来可以用while循环但是却用了for循环。– 可优化的代码:浪费性能的String/StringBuffer的使用。 CheckStyle 检查源文件 主要关注格式 检查Java源文件是否与代码规范相符 它定义了一系列可用的模块,每一个模块提供了严格程度(强制的,可选的…)可配置的检查规则。 规则可以触发通知(notification),警告(warning)和错误(error) 主要包括: Javadoc注释 命名规范 多余没用的Imports Size度量,如过长的方法 缺少必要的空格Whitespace 重复代码
1、问题现象 1.1、** EXPORT FAILED ** 1.2、** ARCHIVE FAILED ** 2、问题分析&定位 2.1、从Console log来看,应该是iOS证书问题导致build failed 2.2、进入官网,查看证书状态,发现证书状态为Invaild(也可使用工具查看:iPhone 配置实用工具.zip) 2.3、编辑invaild证书,重新生成新的enterprise证书 3、问题解决步骤 3.1、xcode–accounts,账号拥有以下两个权限 3.2、登录账号,下载profiles 3.3、安装证书(p12) 3.4、安装完证书后,在钥匙串中查看已安装证书 从Apple Member Center网站下载证书到Mac上双击即可安装(当然也可在Xcode中添加开发账号自动同步证书和[生成]配置文件)。证书安装成功后,在KeychainAccess|Keys中展开创建CSR时生成的Key Pair中的私钥前面的箭头,可以查看到包含其对应公钥的证书(Your requested certificate will be the public half of the key pair.);在Keychain Access|Certificates中展开安装的证书(ios_development.cer)前面的箭头,可以看到其对应的私钥。 3.5、进入xcode,点击manage certificates,查看已安装的证书 Certificate被配置到【Xcode Target|Build Settings|Code Signing|Code Signing Identity】下,下拉选择Identities from Profile "..."(一般先配置Provisioning Profile)。 Xcode中配置的Code Signing Identity(entitlements、certificate)必须与Provisioning Profile匹配,并且配置的Certificate必须在本机Keychain Access中存在对应Public/Private Key Pair,否则编译会报错。 4、证书配置常见错误 1.no such provisioning profile was found Xcode Target|Genera|Identity Team下提示"Your build settings specify a provisioning profile with the UUID "xxx",howerver, no such provisioning profile was found." Xcode Target|BuildSettings|Code Signing|当前配置的指定UDID的provisioning profile在本地不存在,此时需要更改Provisioning Profile。必要时手动去网站下载或重新生成Provisioning Profile或直接在Xcode中Fix issue予以解决(可能自动生成iOS Team ProvisioningProfile)! 2.No identities from profile Build Settings|CodeSigning的Provisioning Profile中选择了本地安装的provisioning profile之后,Code Signing Identity中下拉提示No identities from profile “…”or No identities from keychain. Xcode配置指定UDID的provisioning profile中的DeveloperCertificates在本地KeyChain中不存在(No identities are available)或不一致(KeyPair中的Private Key丢失),此时需去网站检查ProvisioningProfile中的App ID-Certificate-Device配置是否正确。如果是别人提供的共享账号(*.developerprofile)或共享证书(*.p12),请确保导出了对应Key Pair中的Private Key。必要时也直接在Xcode中Fix issue予以解决(可能自动生成iOS Team ProvisioningProfile)。 3.Code Signing Entitlements file do not match profile "Invalid application-identifier Entitlement" or "Code Signing Entitlements file do not match those specified in your provisioning profile.(0xE8008016)." (1)检查对应版本(Debug)指定的*.entitlements文件中的“Keychain Access Groups”键值是否与ProvisioningProfile中的Entitlements项相吻合(后者一般为前者的Prefix/Seed)。 (2)也可以将Build Settings|Code Signing的Provisioning Profile中对应版本(Debug)的Entitlements置空。 4.Xcode配置反应有时候不那么及时,可刷新、重置相关配置项开关(若有)或重启Xcode试试。 5、参考 《iPhone真机调试应用程序》《iOS Developer:真机测试》 《iOS Development--Certificates, Provisioning Profiles》 《关于Certificate、Provisioning Profile、App ID的介绍及其关系》 《数字签名和数字证书》《iOS keyChain 研究》 《苹果开发者账号那些事儿》《iOS關於Provisioning Profiles這些事》 《iOS Code Signing 学习笔记》《代码签名探析/Inside Code Signing》 《iOS Code Signing: 解惑/iOS Code Signing: Under The Hood》 《iOS行货自动打包》《解决Xcode无法生成Archive的问题》《iOS发布遇到的一些问题》 《Xcode打包ipa包》《iOS程序生成ipa进行真机测试》
1、问题现象 1.1、Slave went offline during the build 1.2、useNewCruncher has been deprecated. It will be removed in a future version of the gradle plugin. New cruncher is now always enabled. Incremental java compilation is an incubating feature. 2、问题分析&定位 从上诉两个log日志信息来看,有两种可能: 1、gradle 在构建过程中,Jenkins slave agent 的daemon异常,导致slave offline,从而build failed 2、引入了其他的Java编译方式,及一些被deprecated的api 那么我就依据初步分析的这两个可能问题去Google 3、问题解决 3.1、依据Google中各路大神对第二个问题的解析,并根据解决方案进行尝试,均无功而返。 3.2、参考Stack Overflow中对:“Why did the Gradle Daemon die?”的文章中,大神是这样解答的: Gradle build daemon disappeared unexpectedly most frequently occurs when something else kills the long-running Gradle Daemon process and the client process (the Daemon uses local TCP connections to communicate) tries to send a message and gets no response. For example, running gradle --stop or killall java while a build is occurring will reproduce this problem. 附文章链接:https://stackoverflow.com/questions/29660238/why-did-the-gradle-daemon-die
一幕幕都是心酸; 问题1: 直接在终端输入fastlane init:会报如下错误: 解决:选则项目的.xcworkspace所在的目录 问题2: 解决:登录https://developer.apple.com/register/,进行注册登录 问题3: 从问题描述中可以定位到是由于你的开发者帐号没有在团队中,但当你加入到developer team时,你会惊奇的发现,走到这一步时还是会报错,并且如果跳过这一步,往下执行时,最后生产的Fastlane文件夹里只有fastfile与Appfile这两个文件,网上查了很多,说再执行deliver init时就可以,结果出现如下: 问题4: 报错为Bundle Identifier of App 这时的你鼓足勇气,想着能不能也跳过这一步,只要环境没问题就行,此时就进行Beta测试,在fastfile里增加如下: (注:该图片是依据上篇Fastlane-iOS(调研篇)截取的,也是依据该篇实施的) 出现如下, 问题5: 此时心里只有” 我是谁,我在哪里,我干了什么。。。。。。。。。” 静静一思考,只要问题3没解决,接着就会出现问题4,问题5,所以干掉问题3; 解决问题3: not be in team近一步的意思是你的developer帐号没有上传到ITunes的App Store与Testflight的权限(找rd@陈卓帮忙) 操作:在本地安装有该权限的证书 在xcode中,确认证书,如下: 先点击manage certificates 再点击Doenload Manual Profiles 下载 再执行证书认证时,发现总报错: 你又仔仔细细地检查了证书安装的正确性,以及重启xcode,发现还是不行; 这时你会怀疑是否是该项目就是本身有问题,换个项目,重新操作,发现操作陈工,如下: 完美!!! 问题6:在本地构建 .ipa 和 .app.dSYM 终端输入: fastlane test_beta (#test_beta为lane名) 解决问题6: 终端修改为: fastlane ios test_beta, 遇到问题7: 解决问题7: 先进入到xcworkspace的路径下,再进行fastlane ios test_beta 参考文档:http://www.jianshu.com/p/9858909a6d81 打包成功,并在该目录下生产WaiMai.ipa 文件和对应 WaiMai.app.dSYM 文件,如下图: 问题8: 上传应用至TestFlight的过程: 解决问题8:公司网络慢导致的,回家执行就可以 问题9: 解决问题9: 由于上传到testflight的版本必须增大,所以在上传到testflight时,需要在Appstore上新开一个版本,再上传就可以了; 问题10:在构建版本号时:需要在xcode的三个地方进行修改版本号: a.WaiMai b.WaiMai Notification c.WaiMai Today Extension
前提:ios真机环境已安装ok 运行ios自动化的步骤如下: 步骤1、手机端安装WebDriverAgent 1、下载webdriverAgent,从github上下载代码 git clone https://github.com/facebook/WebDriverAgent 运行初始化脚本 ./Scripts/bootstrap.sh 该脚本会使用Carthage下载所有的依赖,使用npm打包响应的js文件 执行完成后,直接双击打开WebDriverAgent.xcodeproj这个文件。 2、配置WebDriverAgent环境 在以下三个文件中都需要进行配置: (1). 编译WebDriverAgentLib 注:Bundle identifier:com.xxx.yyyyy (xxx每个人都要不一样才行) (2).编译WebDriverAgentRunner (3)配置 IntegrationApp (4) (5).在终终端输入:xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'id=udid' test,待执行完成后手机端会安装WebDriverAgent。 一切正常的话,手机上会出现一个无图标的WebDriverAgent应用,启动之后,马上又返回到桌面。这是很正常的不要奇怪 此时控制台界面可以看到设备的IP。如果看不到的话,使用这种方法打开 通过上面给出的IP和端口,加上/status合成一个url地址。例如http://10.0.0.1:8100/status,然后浏览器打开。如果出现一串JSON输出,说明WDA安装成功了。 若运行后出现:TEST Fail,如下报错: 解决:卸载手机上的 WebDriverAgentRunner程序,重新执行xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'id=udid' test, 在进行端口转发: 有些国产的iPhone机器通过手机的IP和端口还不能访问,此时需要将手机的端口转发到Mac上。 $ brew install imobiledevice 这里若出现以下报错,没关系,直接往下走: $ iproxy 8100 8100 接着使用iproxy --help 可以查到更具体的用法。 这时通过访问http://localhost:8100/status确认WDA是否运行成功。 步骤二、打开appium-desktop,启动appium server+启动inspector,获取页面元素 1、StartServer 2、创建Inspector session 3、创建好session后,session的参数信息必须和当前链接的设备信息一致,否则会报错误信息,最后start session,就可以看到下图的dom树结构,如果出现图3,则说明需要重新编译运行WebdriverAgent了 步骤三、在原来框架的基础上,写Appdriver驱动 步骤四、写自动化case ios的元素获取可以通过xpath,name,label,assessbilityid获取,上述通过name获得 步骤五、运行testng 可以看到case运行正常 参考文档: https://www.jianshu.com/p/2b4236086165 https://blog.csdn.net/weixin_39142498/article/details/78950804 https://testerhome.com/topics/7775 https://testerhome.com/topics/4904 https://testerhome.com/topics/7220 https://www.cnblogs.com/yuhanle/articles/8213675.html
通常在读写文件之前,需要判断文件或目录是否存在,不然某些处理方法可能会使程序出错。所以最好在做任何操作之前,先判断文件是否存在。 这里将介绍三种判断文件或文件夹是否存在的方法,分别使用os模块、Try语句、pathlib模块。 1.使用os模块 os模块中的os.path.exists()方法用于检验文件是否存在。 判断文件是否存在 import os os.path.exists(test_file.txt) #True os.path.exists(no_exist_file.txt) #False 判断文件夹是否存在 import os os.path.exists(test_dir) #True os.path.exists(no_exist_dir) #False 可以看出用os.path.exists()方法,判断文件和文件夹是一样。 其实这种方法还是有个问题,假设你想检查文件“test_data”是否存在,但是当前路径下有个叫“test_data”的文件夹,这样就可能出现误判。为了避免这样的情况,可以这样: 只检查文件 import os os.path.isfile("test-data") 通过这个方法,如果文件”test-data”不存在将返回False,反之返回True。 即是文件存在,你可能还需要判断文件是否可进行读写操作。 判断文件是否可做读写操作 使用os.access()方法判断文件是否可进行读写操作。 语法: os.access(path, mode) path为文件路径,mode为操作模式,有这么几种: os.F_OK: 检查文件是否存在; os.R_OK: 检查文件是否可读; os.W_OK: 检查文件是否可以写入; os.X_OK: 检查文件是否可以执行 该方法通过判断文件路径是否存在和各种访问模式的权限返回True或者False。 import os if os.access("/file/path/foo.txt", os.F_OK): print "Given file path is exist." if os.access("/file/path/foo.txt", os.R_OK): print "File is accessible to read" if os.access("/file/path/foo.txt", os.W_OK): print "File is accessible to write" if os.access("/file/path/foo.txt", os.X_OK): print "File is accessible to execute" 2.使用Try语句 可以在程序中直接使用open()方法来检查文件是否存在和可读写。 语法: open(<file/path>) 如果你open的文件不存在,程序会抛出错误,使用try语句来捕获这个错误。 程序无法访问文件,可能有很多原因: 如果你open的文件不存在,将抛出一个FileNotFoundError的异常; 文件存在,但是没有权限访问,会抛出一个PersmissionError的异常。 所以可以使用下面的代码来判断文件是否存在: try: f =open() f.close() except FileNotFoundError: print "File is not found." except PersmissionError: print "You don't have permission to access this file." 其实没有必要去这么细致的处理每个异常,上面的这两个异常都是IOError的子类。所以可以将程序简化一下: try: f =open() f.close() except IOError: print "File is not accessible." 使用try语句进行判断,处理所有异常非常简单和优雅的。而且相比其他不需要引入其他外部模块。 3. 使用pathlib模块 pathlib模块在Python3版本中是内建模块,但是在Python2中是需要单独安装三方模块。 使用pathlib需要先使用文件路径来创建path对象。此路径可以是文件名或目录路径。 检查路径是否存在 path = pathlib.Path("path/file") path.exist() 检查路径是否是文件 path = pathlib.Path("path/file") path.is_file()
在Java中,所有对象都存储在堆中。他们通过new关键字来进行分配,JVM会检查是否所有线程都无法在访问他们了,并且会将他们进行回收。在大多数时候程序员都不会有一丝一毫的察觉,这些工作都被静悄悄的执行。但是,有时候在发布前的最后一天,程序挂了。 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space OutOfMemoryError是一个让人很郁闷的异常。它通常说明你干了写错误的事情:没必要的长时间保存一些没必要的数据,或者同一时间处理了过多的数据。有些时候,这些问题并不一定受你的控制,比如说一些第三方的库对一些字符串做了缓存,或者一些应用服务器在部署的时候并没有进行清理。并且,对于堆中已经存在的对象,我们往往拿他们没办法。 这篇文章分析了导致OutOfMemoryError的不同原因,以及你该怎样应对这种原因的方法。以下分析仅限于Sun Hotspot虚拟机,但是大多数结论都适用于其他任何的JVM实现。它们大多数基于网上的文章以及我自己的经验。我没有直接做JVM开发的工作,因此结论并不代表JVM的作者。但是我确实曾经遇到过并解决了很多内存相关的问题。 垃圾回收介绍 请参考这篇文章。简单的说,标记-清除算法(mark-sweep collect)以garbage collection roots作为扫描的起点,并对整个对象图进行扫描,对所有可达的对象进行标记。那些没有被标记的对象会被清除并回收。 Java的垃圾回收算法过程意味着如果出现了OOM,那么说明你在不停的往对象图中添加对象并且没有移除它们。这通常是因为你在往一个集合类中添加了很多对象,比如Map,并且这个集合对象是static的。或者,这个集合类被保存在了ThreadLocal对象中,而这个对应的Thread却又长时间的运行,一直不退出。 这与C和C++的内存泄露完全不一样。在这些语言中,如果一些方法调用了malloc()或者new,并且在方法退出的时候没有调用相应的free()或者delete,那么内存就会产生泄露。这些是真正意义上得泄露,你在这个进程范围内不可能再恢复这些内存,除非使用一些特定的工具来保证每一个内存分配方法都有其对应的内存释放操作相对应。 在java中,“泄露”这个词往往被误用了。因为从JVM的角度来说,所有的内存都是被良好管理的。问题仅仅是作为程序员的你不知道这些内存是被哪些对象占用了。但是幸运的是,你还是有办法去找到和定位它们。 在深入探讨之前,你还有最后一件关于垃圾收集的知识需要了解:JVM会尽最大的能力去释放内存,直到发生OOM。这就意味着OOM不能通过简单的调用System.gc()来解决,你需要找到这些“泄露”点,并自己处理它们。 设置堆大小 学院派的人非常喜欢说Java语言规范并没有对垃圾收集器进行任何约定,你甚至可以实现一个从来不释放内存的JVM(实际是毫无意义的)。Java虚拟机规范中提到堆是由垃圾回收器进行管理,但是却没有说明任何相关细节。仅仅说了我刚才提到的那句话:垃圾回收会发生在OOM之前。 实际上,Sun Hotspot虚拟机使用了一个固定大小的堆空间,并且允许在最小空间和最大空间之间进行自动增长。如果你没有指定最小值和最大值,那么对于’client’模式将会默认使用2Mb最为最小值,64Mb最为最大值;对于’server’模式,JVM会根据当前可用内存来决定默认值。2000年后,默认的最大堆大小改为了64M,并且在当时已经认为足够大了(2000年前的时候默认值是16M),但是对于现在的应用程序来说很容易就用完了。 这意味着你需要显示的通过JVM参数来指定堆的最小值和最大值: java -Xms256m -Xmx512m MyClass 这里有很多经验上得法则来设定最大值和最小值。显然,堆的最大值应该设定为足以容下整个应用程序所需要的全部对象。但是,将它设定为“刚刚好足够大”也不是一个很好的注意,因为这样会增加垃圾回收器的负载。因此,对于一个长时间运行的应用程序,你一般需要保持有20%-25%的空闲堆空间。(你得应用程序可能需要不同的参数设置,GC调优是一门艺术,并且不在该文章讨论范围内) 让你奇怪的时,设置合适的堆的最小值往往比设置合适的最大值更加重要。垃圾回收器会尽可能的保证当前的的堆大小,而不是不停的增长堆空间。这会导致应用程序不停的创建和回收大量的对象,而不是获取新的堆空间,相对于初始(最小)堆空间。Java堆会尽量保持这样的堆大小,并且会不停的运行GC以保持这样的容量。因此,我认为在生产环境中,我们最好是将堆的最小值和最大值设置成一样的。 你可能会困惑于为什么Java堆会有一个最大值上限:操作系统并不会分配真正的物理内存,除非他们真的被使用了。并且,实际使用的虚拟内存空间实际上会比Java堆空间要大。如果你运行在一个32位系统上,一个过大的堆空间可能会限制classpath中能够使用的jar的数量,或者你可以创建的线程数。 另外一个原因是,一个受限的最大堆空间可以让你及时发现潜在的内存泄露问题。在开发环境中,对应用程序的压力往往是不够的,如果你在开发环境中就拥有一个非常大得堆空间,那么你很有可能永远不会发现可能的内存泄露问题,直到进入产品环境。 在运行时跟踪垃圾回收 所有的JVM实现都提供了-verbos:gc选项,它可以让垃圾回收器在工作的时候打印出日志信息: java -verbose:gc com.kdgregory.example.memory.SimpleAllocator [GC 1201K->1127K(1984K), 0.0020460 secs] [Full GC 1127K->103K(1984K), 0.0196060 secs] [GC 1127K->1127K(1984K), 0.0006680 secs] [Full GC 1127K->103K(1984K), 0.0180800 secs] [GC 1127K->1127K(1984K), 0.0001970 secs] ... Sun的JVM提供了额外的两个参数来以内存带分类输出,并且会显示垃圾收集的开始时间: java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps com.kdgregory.example.memory.SimpleAllocator 0.095: [GC 0.095: [DefNew: 177K->64K(576K), 0.0020030 secs]0.097: [Tenured: 1063K->103K(1408K), 0.0178500 secs] 1201K->103K(1984K), 0.0201140 secs] 0.117: [GC 0.118: [DefNew: 0K->0K(576K), 0.0007670 secs]0.119: [Tenured: 1127K->103K(1408K), 0.0392040 secs] 1127K->103K(1984K), 0.0405130 secs] 0.164: [GC 0.164: [DefNew: 0K->0K(576K), 0.0001990 secs]0.164: [Tenured: 1127K->103K(1408K), 0.0173230 secs] 1127K->103K(1984K), 0.0177670 secs] 0.183: [GC 0.184: [DefNew: 0K->0K(576K), 0.0003400 secs]0.184: [Tenured: 1127K->103K(1408K), 0.0332370 secs] 1127K->103K(1984K), 0.0342840 secs] ... 从上面的输出我们可以看出什么?首先,前面的几次垃圾回收发生的非常频繁。每行的第一个字段显示了JVM启动后的时间,我们可以看到在一秒钟内有上百次的GC。并且,还加入了每次GC执行时间的开始时间(在每行的最后一个字段),可以看出垃圾搜集器是在不停的运行的。 但是在实时系统中,这会造成很大的问题,因为垃圾搜集器的执行会夺走很多的CPU周期。就像我之前提到的,这很可能是由于初始堆大小设置的太小了,并且GC日志显示了:每次堆的大小达到了1.1Mb,它就开始执行GC。如果你得系统也有类似的现象,请在改变自己的应用程序之前使用-Xms来增大初始堆大小。 对于GC日志还有一些很有趣的地方:除了第一次垃圾回收,没有任何对象是存放在了新生代(“DefNew”)。这说明了这个应用程序分配了包含大量数据的数组,在显示世界里这是很少出现的。如果在一个实时系统中出现这样的状况,我想到的第一个问题是“这些数组拿来干什么用?”。 堆转储(Heap Dumps) 一个堆转储可以显示你在应用程序说使用的所有对象。从基础上讲,它仅仅反映了对象实例的数量和类文件所占用的字节数。当然你也可以将分配这些内存的代码一起dump出来,并且对比历史存货对象。但是,如果你要dump的数据信息越多,JVM的负载就会越大,因此这些技术仅仅应该使用在开发环境中。 怎样获得一个内存转储 命令行参数-XX:+HeapDumpOnOutOfMemoryError是最简单的方式生成内存转储。就像它的名字所说的,它会在内存被用完的时候(发生OOM)进行转储,这在产品环境非常好用。但是由于这个是一种事后转储(已经发生了OOM),它只能提供一种历史性的数据。它会产生一个二进制文件,你可以使用jhat来操作该文件(这个工具在JDK1.6中已经提供,但是可以读取JDK1.5产生的文件)。 你可以使用jmap(JDK1.5之后就自带了)来为一个运行中得java程序产生堆转储,可以产生一个在jhat中使用的dump文件,或者是一个存文本的统计文件。统计图可以在进行分析时优先使用,特别是你要在一段时间内多次转储堆并进行分析和对比历史数据。 从转储内容和JVM的负荷的扩展性上考虑的话,可以使用profilers。Profiles使用JVM的调试接口(debuging interface)来搜集对象的内存分配信息,包括具体的代码行和方法调用栈。这个是非常有用的:不仅仅可以知道你分配了一个数GB的数组,你还可以知道你在一个特定的地方分配了950MB的对象,并且直接忽略其他的对象。当然,这些结果肯定会对JVM有开销,包括CPU的开销和内存的开销(保存一些原始数据)。你不应该在产品环境中使用profiles。 堆转储分析:live objects Java中的内存泄露是这样定义的:你在内存中分配了一些对象,但是并没有清除掉所有对它们的引用,也就是说垃圾搜集器不能回收它们。使用堆转储直方图可以很容易的查找这些泄露对象:它不仅仅可以告诉你在内存中分配了哪些对象,并且显示了这些对象在内存中所占用的大小。但是这种直方图最大的问题是:对于同一个类的所有对象都被聚合(group)在一起了,所以你还需要进一步做一些检测来确定这些内存在哪里被分配了。 使用jmap并且加上-histo参数可以为你产生一个直方图,它显示了从程序运行到现在所有对象的数量和内存消耗,并且包含了已经被回收的对象和内存。如果使用-histo:live参数会显示当前还在堆中得对象数量及其内存消耗,不论这些对象是否要被垃圾搜集器进行回收。 也就是说,如果你要得到一个当前时间下得准确信息,你需要在使用jmap之前强制执行一次垃圾回收。如果你的应用程序是运行在本地,最简单的方式是直接使用jconsole:在’Memory’标签下,有一个’Perform GC’的按钮。如果应用程序是运行在服务端环境,并且JMX beans被暴露了,MemoryMXBean有一个gc()操作。如果上述的两种方案都没办法满足你得要求,你就只有等待JVM自己触发一次垃圾搜集过程了。如果你有一个很严重的内存泄露问题,那么第一次major collection很可能预示着不久后就会OOM。 有两种方法使用jmap产生的直方图。其中最有效的方法,适用于长时间运行的程序,可以使用带live的命令行参数,并且在一段时间内多次使用该命令,检查哪些对象的数量在不断增长。但是,根据当前程序的负载,该过程可能会花费1个小时或者更多的时间。 另外一个更加快速的方式是直接比较当前存活的对象数量和总的对象数量。如果有些对象占据了总对象数量的大部分,那么这些对象很有可能发生内存泄露。这里有一个例子,这个应用程序已经连续几周为100多个用户提供了服务,结果列举了前12个数量最多的对象。据我所知,这个程序没有内存泄露的问题,但是像其他应用程序一样做了常规性的内存转储分析操作。 ~, 510> jmap -histo 7626 | more num #instances #bytes class name ---------------------------------------------- 1: 339186 63440816 [C 2: 84847 18748496 [I 3: 69678 15370640 [Ljava.util.HashMap$Entry; 4: 381901 15276040 java.lang.String 5: 30508 13137904 [B 6: 182713 10231928 java.lang.ThreadLocal$ThreadLocalMap$Entry 7: 63450 8789976 <constMethodKlass> 8: 181133 8694384 java.lang.ref.WeakReference 9: 43675 7651848 [Ljava.lang.Object; 10: 63450 7621520 <methodKlass> 11: 6729 7040104 <constantPoolKlass> 12: 134146 6439008 java.util.HashMap$Entry ~, 511> jmap -histo:live 7626 | more num #instances #bytes class name ---------------------------------------------- 1: 200381 35692400 [C 2: 22804 12168040 [I 3: 15673 10506504 [Ljava.util.HashMap$Entry; 4: 17959 9848496 [B 5: 63208 8766744 <constMethodKlass> 6: 199878 7995120 java.lang.String 7: 63208 7592480 <methodKlass> 8: 6608 6920072 <constantPoolKlass> 9: 93830 5254480 java.lang.ThreadLocal$ThreadLocalMap$Entry 10: 107128 5142144 java.lang.ref.WeakReference 11: 93462 5135952 <symbolKlass> 12: 6608 4880592 <instanceKlassKlass> 当我们要尝试寻找内存泄露问题,可以从消耗内存最多的对象着手。这听上去很明显,但是往往它们并不是内存泄露的根源。但是,它们任然是应该最先下手的地方,在这个例子中,最占用内存的是一些char[]的数组对象(总大小是60MB,基本上没有任何问题)。但是很奇怪的是当前存货(live)的对象竟然占了历史分配的总对象大小的三分之二。 一般来说,一个应用程序会分配对象,并且在不久之后就会释放它们。如果保存一些对象的应用过长的时间,就很有可能会导致内存泄露。但是虽然是这么说的,实际上还是要具体情况具体分析,主要还是要看这个程序到底在做什么事情。字符数组对象(char[])往往和字符串对象(String)同时存在,大部分的应用程序都会在整个运行过程中一直保持着一些字符串对象的引用。例如,基于JSP的web应用程序在JSP页面中定义了很多HTML字符串表达式。这种特殊的应用程序提供HTML服务,但是它们需要保持字符串引用的需求却不一定那么清晰:它们提供的是目录服务,并不是静态文本。如果我遇到了OOM,我就会尝试找到这些字符串在哪里被分配,为什么没有被释放。 另一个需要关注的是字节数组([B)。在JDK中有很多类都会使用它们(比如BufferedInputStream),但是却很少在应用程序代码中直接看到它们。通常它们会被用作缓存(buffer),但是缓存的生命周期不会很长。在这个例子中我们看到,有一半的字节数组任然保持存活。这个是令人担忧的,并且它凸显了直方图的一个问题:所有的对象都按照它的类型被分组聚合了。对于应用程序对象(非JDK类型或者原始类型,在应用程序代码中定义的类),这不是一个问题,因为它们会在程序的一个部分被集中分配。但是字节数组有可能会在任何地方被定义,并且在大多数应用程序中都被隐藏在一些库中。我们是否应当搜索调用了new byte[]或者new ByteArrayOutputStream()的代码? 堆转储分析:相关的原因和影响分析 为了找到导致内存泄露的最终原因,仅仅考虑按照类别(class)的分组的内存占用字节数是不够的。你还需要将应用程序分配的对象和内存泄露的对象关联起来考虑。一个方法是更加深入查看对象的数量,以便将具有关联性的对象找出来。下面是一个具有严重内存问题的程序的转储信息: num #instances #bytes class name ---------------------------------------------- 1: 1362278 140032936 [Ljava.lang.Object; 2: 12624 135469922 [B ... 5: 352166 45077248 com.example.ItemDetails ... 9: 1360742 21771872 java.util.ArrayList ... 41: 6254 200128 java.net.DatagramPacket 如果你仅仅去看信息的前几行,你可能会去定位Object[]或者byte[],这些都是徒劳的。真正的问题出在ItemDetails和DatagramPacket上:前者分配了大量的ArrayList,进而又分配了大量的Object[];后者使用了大量的byte[]来保存从网络上接收到的数据。 第一个问题,分配了大量的数组,实际上不是内存泄露。ArrayList的默认构造函数会分配容量是10的数组,但是程序本身一般只使用1个或者2个槽位,这对于64位JVM来说会浪费62个字节的内存空间。一个更好的涉及方案是仅仅在有需要的时候才使用List,这样对每个实例来说可以节约额外的48个字节。但是,对于这种问题也可以很轻易的通过加内存来解决,因为现在的内存非常便宜。 但是对于datagram的泄露就比较麻烦(如同定位这个问题一样困难):这表明接收到的数据没有被尽快的处理掉。 为了跟踪问题的原因和影响,你需要知道你的程序是怎样在使用这些对象。不多的程序才会直接使用Object[]:如果确实要使用数组,程序员一般都会使用带类型的数组。但是,ArrayList会在内部使用。但是仅仅知道ArrayList的内存分配是不够的,你还需要顺着调用链往上走,看看谁分配了这些ArrayList。 其中一个方法是对比相关的对象数量。在上面的例子中,byte[]和DatagramPackage的关系是很明显的:其中一个基本上是另外一个的两倍。但是ArrayList和ItemDetails的关系就不那么明显了。(实际上一个ItemDetails中会包含多个ArrayList) 这往往是个陷阱,让你去关注那么数量最多的一些对象。我们有数百万的ArrayList对象,并且它们分布在不同的class中,也有可能集中在一小部分class中。尽管如此,数百万的对象引用是很容易被定位的。就算有10来个class可能会包含ArrayList,那么每个class的实体对象也会有十万个,这个是很容易被定位的。 从直方图中跟踪这种引用关系链是需要花费大量精力的,幸运的是,jmap不仅仅可以提供直方图,它还可以提供可以浏览的堆转储信息。 堆转储分析:跟踪引用链 浏览堆转储引用链具有两个步骤:首先需要使用-dump参数来使用jmap,然后需要用jhat来使用转储文件。如果你确定要使用这种方法,请一定要保证有足够多的内存:一个转储文件通常都有数百M,jhat需要好几个G的内存来处理这些转储文件。 tmp, 517> jmap -dump:live,file=heapdump.06180803 7626 Dumping heap to /home/kgregory/tmp/heapdump.06180803 ... Heap dump file created tmp, 518> jhat -J-Xmx8192m heapdump.06180803 Reading from heapdump.06180803... Dump file created Sat Jun 18 08:04:22 EDT 2011 Snapshot read, resolving... Resolving 335643 objects... Chasing references, expect 67 dots................................................................... Eliminating duplicate references................................................................... Snapshot resolved. Started HTTP server on port 7000 Server is ready. 提供给你的默认URL显示了所有加载进系统的class,但是我觉得并不是很有用。相反,我直接使用http://localhost:7000/histo/,这个地址是一个直方图的视角来进行显示,并且是按照对象数量和占用的内存空间进行排序了的。 这个直方图里的每个class的名称都是一个链接,点击这个链接可以查看关于这个类型的详细信息。你可以在其中看到这个类的继承关系,它的成员变量,以及很多指向这个类的实体变量信息的链接。我不认为这个详细信息页面非常有用,而且实体变量的链接列表很占用很多的浏览器内存。 为了能够跟踪你的内存问题,最有用的页面是’Reference by Type’。这个页面含有两个表格:入引用和出引用,他们都被引用的数量进行排序了。点击一个类的名字可以看到这个引用的信息。 你可以在类的详细信息(class details)页面中找到这个页面的链接。 堆转储分析:内存分配情况 在大多数情况下,知道了是哪些对象消耗了大量的内存往往就可以知道它们为什么会发生内存泄露。你可以使用jhat来找到所有引用了他们的对象,并且你还可以看到使用了这些对象的引用的代码。但是在有些时候,这样还是不够的。 比如说你有关于字符串对象的内存泄露问题,那么就很有可能会花费你好几天的时间去检查所有和字符串相关的代码。要解决这种问题,你就需要能够显示内存在哪里被分配的堆转储。但是需要注意的是,这种类型的堆转储会对你的应用程序产生更多的负载,因为负责转储的代理需要记录每一个new操作符。 有许多交互式的程序可以做到这种级别的数据记录,但是我找到了一个更简单的方法,那就是使用内置的hprof代理来启动JVM。 java -Xrunhprof:heap=sites,depth=2 com.kdgregory.example.memory.Gobbler hprof有许多选项:不仅仅可以用多种方式输出内存使用情况,它还可以跟踪CPU的使用情况。当它运行的时候,我指定了一个事后的内存转储,它记录了哪些对象被分配,以及分配的位置。它的输出被记录在了java.hprof.txt文件中,其中关于堆转储的部分如下: SITES BEGIN (ordered by live bytes) Tue Sep 29 10:43:34 2009 percent live alloc'ed stack class rank self accum bytes objs bytes objs trace name 1 99.77% 99.77% 66497808 2059 66497808 2059 300157 byte[] 2 0.01% 99.78% 9192 1 27512 13 300158 java.lang.Object[] 3 0.01% 99.80% 8520 1 8520 1 300085 byte[] SITES END 这个应用程序没有分配多种不同类型的对象,也没有将它们分配到很多不同的地方。一般的转储有成百上千行的信息,显示了每一种类型的对象被分配到了哪里。幸运的是,大多数问题都会出现在开头的几行。在这个例子中,最突出的是64M的存活着的字节数组,并且每一个平均32K。 大多数程序中都不会一直持有这么大得数据,这就表明这个程序没有很好的抽取和处理这些数据。你会发现这常常发生在读取一些大的字符串,并且保存了substring之后的字符串:很少有人知道String.substring()后会共享原始字符串对象的字节数组。如果你按照一行一行地读取了一个文件,但是却使用了每行的前五个字符,实际上你任然保存的是整个文件在内存中。 转储文件也显示出这些数组被分配的数量和现在存活的数量完全相等。这是一种典型的泄露,并且我们可以通过搜索’trace’号来找到真正的代码: TRACE 300157: com.kdgregory.example.memory.Gobbler.main(Gobbler.java:22) 好了,这下就足够简单了:当我在代码中找到指定的代码行时,我发现这些数组被存放在了ArrayList中,并且它也一直没有出作用域。但是有时候,堆栈的跟踪并没有直接关联到你写的代码上: TRACE 300085: java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java:71) java.util.zip.ZipFile$2.<init>(ZipFile.java:348) 在这个例子中,你需要增加堆栈跟踪的深度,并且重新运行你的程序。但是这里有一个需要平衡的地方:当你获取到了更多的堆栈信息,你也同时增加了profile的负载。默认地,如果你没有指定depth参数,那么默认值就会是4。我发现当堆栈深度为2的时候就可以发现和定位我程序中得大部分问题了,当然我也使用过深度为12的参数来运行程序。 另外一个增大堆栈深度的好处是,最后的报告结果会更加细粒度:你可能会发现你泄露的对象来自两到三个地方,并且它们都使用了相同的方法。 堆转储分析:位置、地点 当很多对象在分配的不久后就被丢弃时,分代垃圾搜集器就会开始运行。你可以使用同样的原则来找发现内存泄露:使用调试器,在对象被分配的地方打上断点,并且运行这段代码。在大多数时候,当它们被分配不久后就会加入到长时间存活(long-live)的集合中。 永久代 除了JVM中的新生代和老年代外,JVM还管理着一片叫‘永久代’的区域,它存储了class信息和字符串表达式等对象。通常,你不会观察到永久代中的垃圾回收;大多数的垃圾回收发生在应用程序堆中。但是不像它的名字,在永久代中的对象不会是永久不变的。举个例子,被应用程序classloader加载的class,当不再被classloader引用时就会被清理掉。当应用程序服务被频繁的热部署时就可能会发生: Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 这一这个信息:这个不管应用程序堆的事。当应用程序堆中还有很多空间时,也有可能用完永久代的空间。通常,这发生在重新部署EAR和WAR文件时,并且永久代还不够大到可以同时容纳新的class信息和老的class信息(老的class会一直被保存着直到所有的请求在使用完它们)。当在运行处于开发状态的应用时更容易发生。 解决永久代错误的第一个方法就是增大永久大的空间,你可以使用-XX:MaxPermSize命令行参数。默认是64M,但是web应用程序或者IDE一般都需要256M。 java -XX:MaxPermSize=256m 但是在通常情况下并不是这么简单的。永久代的内存泄露一般都和在应用堆中的内存泄露原因一样:在一些地方的对象引用了并不该再引用的对象。以我的经验,很有可能有些对象直接引用了一些Class对象,或者在java.lang.reflect包下面的对象,而不是某些类的实例对象。正式因为web引用的classloader的组织方式,通常罪魁祸首都出现在服务的配置当中。 例如,你使用了Tomcat,并且有一个目录里面有很多共享的jars:shared/lib。如果你在一个容器里同时运行好几个web应用,将一些公用的jar放在这个目录是很有道理的,因为这样的话这些class仅仅被加载一次,可以减少内存的使用量。但是,如果其中的一些库具有对象缓存的话,会发生什么事情呢? 答案是这些被缓存了的对象的类永远不会被卸载,直到缓存释放了这些对象。解决方案就是将这些库移动到WAR或者EAR中。但是在某些时候情况也不会像这么简单:JDKs bean introspector会缓存住由root classloader加载的BeanInfo对象。并且任何使用了反射的库也会缓存这些对象,这样就导致你不能直到真正的问题所在。 解决永久代的问题通常都是比较痛苦的。一般可以先考虑加上-XX:+TraceClassLoading和-XX:+TraceClassUnloading命令行选项以便找出那些被加载了但是没有被卸载的类。如果你加上了-XX:+TraceClassResolution命令行选项,你还可以看到哪些类访问了其他类,但是没有被正常卸载。 这里有针对这三个选项的一个实例。第一行显示了MyClassLoader类从classpath中被加载了。因为它又从URLClassLoader继承,因此我们看到了接下来的’RESOLVE’消息,紧跟着又是一条’RESOLVE’消息,说明Class类也被解析了。 [Loaded com.kdgregory.example.memory.PermgenExhaustion$MyClassLoader from file:/home/kgregory/Workspace/Website/programming/examples/bin/] RESOLVE com.kdgregory.example.memory.PermgenExhaustion$MyClassLoader java.net.URLClassLoader RESOLVE java.net.URLClassLoader java.lang.Class URLClassLoader.java:188 所有的信息都在这里的,但是通常情况下将一些共享库移动到WAR/EAR中往往可以很快速的解决问题。 当堆内存还有空间时发生的OutOfMemoryError 就像你刚才看到的关于永久代的消息,也许应用程序堆中还有空闲空间,但是也任然可能会发生OOM。这里有几个例子: 连续的内存分配 当我描述分代的堆空间时,我一般会说对象会首先被分配在新生代,然后最终会被移动到老年代。但这不是绝对正确的:如果你的对象足够大,那么它就会直接被分配在老年代。一般用户自己定义的对象是不会(也不应该)达到这个临界值,但是数组却却有可能:在JDK1.5中,当数组的对象超过0.5M的时候就会被直接分配到老年代。 在32位机器上,0.5M换算成Object[]数组的话就可以包含131,072个元素。这已经是很大的了,但是在企业级的应用中这是很有可能的。特别是当使用了HashMap时,它经常需要重新resize自己(里面的数组数据结构)。一些应用程序可能还需要更大的数组。 当没有连续的堆空间来存放这些数组对象时(就算在垃圾回收并且对内存进行了紧凑之后),问题就产生了。这很少见,但是如果当前的程序已经很接近堆空间的上限时,这就变得很有可能了。增大堆空间上限是最好的解决方案,但是你也许可以试试事先分配好你的容器的大小。(后面的小对象可以不需要连续的内存空间) 线程 JavaDoc中对OOM的描述是,当垃圾搜集器不能在释放更多的内存空间时,JVM会抛出OOM。这里只对了一半:当JVM的内部代码收到来自操作系统的ENOMEM错误时,JVM也会抛出OOM。Unix程序员一般都知道,这里有很多地方可以收到ENOMEN错误,创建线程的过程是其中之一: Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread 在我的32位Linux系统中,使用JDK1.5,我可以最多开启5,550个线程直到抛出异常。但是实际上在堆中任然有很多空闲空间,这是怎么回事呢? 在这个场景的背后,线程实际上是被操作系统所管理,而不是JVM,创建线程失败的可能原因有很多很多。在我的例子中,每一个线程都需要占用大概0.5M的虚拟内存作为它的栈空间,在5000个线程被创建之后,大约就有2G的内存空间被占用。有些操作系统就强制制定了一个进程所能创建的线程数的上限。 最后,针对这个问题没有一个解决方案,除非更换你的应用程序。大多数程序是不需要创建这么多得线程的,它们会将大部分的时间都浪费在等待操作系统调度上。但是有些服务程序需要创建数千个线程去处理请求,但是它们中得大多数都是在等待数据。针对这种场景,NIO和selector就是一个不错的解决方案。 Direct ByteBuffers 从JDK1.4之后Java允许程序程序使用bytebuffers来访问堆外的内存空间(受限)。虽然ByteBuffer对象本身很小,但是堆外的内存可不一定很小: Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory 这里有多个原因会导致bytebuffer分配失败。通常情况下,你可能超过了最多的虚拟内存上限(仅限于32位系统),或者超过了所有物理内存和交换区内存的上限。除非你是在以很简单的方式处理超过你的机器内存上限的数据,否则你在使用direct buffer产生OOM的原因和你使用堆的原因基本上是一样的:你保持着一些你不该引用的数据。前面介绍的堆分析技术可以帮助你找到泄露点。 申请的内存超过物理内存 就像我前面提到的,你在启动一个JVM时,你需要指定堆的最小值和最大值。这就意味着,JVM会在运行期动态改变它对虚拟内存的需求。在一个内存受限的机器上,你可以同时运行多个JVM,甚至它们所有指定的最大值之和大于了物理内存和交换区的大小。当然,这就有可能会导致OOM,就算你的程序中存活的对象大小小于你指定的堆空间也是一样的。 这种情况和跑多个C++程序使用完所有的物理内存的原因是一样的。使用JVM可能会让你产生一种假象,以为不会出现这种问题。唯一的解决方案是购买更多的内存,或者不要同时跑那么多程序。没有办法让JVM可以’快速失败’;但是在Linux上你可以申请比总内存更多的内存。 堆外内存的使用 最后一个需要注意的问题是:Java中得堆仅仅是所占用内存的一部分。JVM还会为它所创建的线程、内部代码、工作空间、共享库、direct buffer、内存映射文件分配内存。在32位的JVM中,这所有的内存都需要被映射到2G的虚拟内存空间中,这是非常有限的(特别是对于服务端或者后端应用程序)。在64位的JVM中,虚拟内存基本没存在什么限制,但是实际的物理内存(含交换区)可能会很稀缺。 一般来说,虚拟内存不会造成什么大问题;操作系统和JVM可以很好的管理它们。通常情况下,你需要查看虚拟内存的映射情况主要是为了direct buffer所使用的大块的内存或者是内存映射文件。但是你还是很有必要知道什么是虚拟内存的映射。 要查看在Linux上的虚拟内存映射情况可以使用pmap;在Windows中可以使用VMMap。下面是使用pmap来dump的一个Tomcat应用。实际的dump文件有好几百行,所展示的部分仅仅是比较有意思的部分: 08048000 60K r-x-- /usr/local/java/jdk-1.5/bin/java 08057000 8K rwx-- /usr/local/java/jdk-1.5/bin/java 081e5000 6268K rwx-- [ anon ] 889b0000 896K rwx-- [ anon ] 88a90000 4096K rwx-- [ anon ] 88e90000 10056K rwx-- [ anon ] 89862000 50488K rwx-- [ anon ] 8c9b0000 9216K rwx-- [ anon ] 8d2b0000 56320K rwx-- [ anon ] ... afd70000 504K rwx-- [ anon ] afdee000 12K ----- [ anon ] afdf1000 504K rwx-- [ anon ] afe6f000 12K ----- [ anon ] afe72000 504K rwx-- [ anon ] ... b0cba000 24K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/catalina-ant-jmx.jar b0cc0000 64K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/catalina-storeconfig.jar b0cd0000 632K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/catalina.jar b0d6e000 164K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/tomcat-ajp.jar b0d97000 88K r-xs- /usr/local/java/netbeans-5.5/enterprise3/apache-tomcat-5.5.17/server/lib/tomcat-http.jar ... b6ee3000 3520K r-x-- /usr/local/java/jdk-1.5/jre/lib/i386/client/libjvm.so b7253000 120K rwx-- /usr/local/java/jdk-1.5/jre/lib/i386/client/libjvm.so b7271000 4192K rwx-- [ anon ] b7689000 1356K r-x-- /lib/tls/i686/cmov/libc-2.11.1.so ... dump文件展示给你了关于虚拟内存映射的4个部分:虚拟内存地址,大小,权限,源(从文件加载的部分)。最有意思的部分是它的权限部分,它表示了该内存段是否是只读的(r-)还是读写的(rw)。 我会从读写段开始分析。所有的段都具有名字”[ anon ]“,它在Linux中说明了该段不是由文件加载而来。这里还有很多被命名的读写段,它们和共享库关联。我相信这些库都具有每个进程的地址表。 因为所有的读写段都具有相同的名字,一次要找出出问题的部分需要花费一点时间。对于Java堆,有4个相关的大块内存被分配(新生代有2个,老年代1个,永久代1个),他们的大小由GC和堆配置来决定。 其他问题 这部分的内容并不是对所有地方都适用。大部分都是我解决问题的过程中总结的实际经验。 不要被虚拟内存的统计信息所误导 有很多抱怨说Java是’memory hog’,经常被top命令的’VIRT’部分和Windows任务管理器的’Mem Usage’列所证实。需要澄清的是,有太多的东西都不会算进这个统计信息中,有些还是与其他程序共享的(比如说C的库)。实际上也有很多‘空’的区域在虚拟内存映射空间中:如果你适用-Xms1000m来启动JVM,就算你还没有开始分配对象,虚拟内存的大小也会超过1000m。 一个更好的测量方法是使用驻留集的大小:你的应用程序真正使用的物理内存的页数,不包含共享页。这就是top命令中得’RES’列。但是,驻留集并不是对你的程序所需使用的总内存最好的测量方法。操作系统只有在你的程序真正需要使用它们的时候才会将它们放进进程的内存空间中,一般来说是在你的系统处于高负载的情况下才会出现,这会花费一段较长的时间。 最后:始终使用工具来提供所需的详细信息来分析Java中的内存问题。并且只有当出现OOM的时候才考虑下结论。 OOM的罪魁祸首经常离它的抛出点很近 内存泄露一般在内存被分配之后不久发生。一个相似的结论是,OOM的根源一般都离它的抛出点很近,可以使用堆跟踪技术来首先进行分析。其基本原理是,内存泄露一般和产生大量的内存相关联。这说明了,导致泄露的代码具有更高的失败风险率,不管是因为其内存分配代码被调用的过于频繁,还是因为每次调用都分配的过大的内存。因此,可以优先考虑使用栈跟踪来定位问题。 和缓存相关的部分最值得怀疑 我在这篇文章中提到缓存了很多次:在我数十年的Java工作经历中发现,和内存泄露相关的类进场都是和缓存相关的。实际上缓存是很难编写的。 使用缓存有很多很多很好的理由,并且使用自己写的缓存也有很多好的理由。如果你确定要使用缓存,请先回答下面的问题: 哪些对象会被放进缓存?如果你所要缓存的对象都是同一种类型(或者具有继承关系),那么相比一个可以容纳各种类型的缓存来说更好跟踪问题。 有多少对象会被同时放进缓存?如果你像让ProductCache缓存1000个对象,但是在内存分析结果中发现了10000个对象,那么这之间的关系就比较好定位。如果你指定了这个缓存最多的容量上限,那么你就可以很容易的计算出这个缓存最多需要多少内存。 过期和清除策略是什么?每一个缓存为了控制存在于其中的对象的存货周期,都需要一个明确的驱逐策略。如果你没有指定一个明确的驱逐策略,那么有些对象就很有可能比它真正需要的存活周期要长,占用更多的内存,加重垃圾搜集器的负载(记住:在标记阶段需要的时间和存活对象的数量成正比)。 是否会在缓存之外同时持有这些存活对象的引用?缓存最好的应用场景是,调用频繁,并且调用时间很短,并且所缓存的对象的获取代价很大。如果你需要创建一个对象,并且在整个应用程序的生命周期中都需要引用这个对象,那么就没有必要将这个对象放入缓存(也许使用池技术可以显示总得对象数量)。 注意对象的生命周期 一般来说对象可以被划分为两类:一类是伴随着整个程序的生命周期而存活;另外一来是仅仅存活并服务于一个单一的请求。搞清楚这个非常重要,你仅仅需要关心你认为是长时间存活的对象。 一种方法是在程序启动的时候全部初始化好所有长时间(long-lived)存活的对象,不管他们是否要立刻被用到。另外一个方法是使用依赖注入框架,比如Spring。这不仅仅可以很方便的bean配置文件中找到所有long-lived的对象(不需要扫描整个classpath),还可以很清楚的知道这些对象在哪里被使用。 查找在方法参数中被错误使用的对象 在大部分场景中,在一个方法中被分配的对象都会在方法退出的时候被清理掉(除开被返回的对象)。当你都是用局部变量来保存这些对象的时候,这个规则很容易被遵守。但是,有时候任然会使用实体变量来保存这些对象,特别是在方法中会调用大量其他方法的时候,主要是为了避免过多和麻烦的方法参数传递。 这样做不是一定会产生泄漏。后续的方法调用会重新对这些变量进行赋值,这样就可以让之前被创建的对象被回收。但是这样导致不必要的内存开销,并且让调试更加困难。但是从设计的角度出发,当我看到这样的代码时,我就会考虑将这个方法单独提出来形成一个独立的类。 J2EE:不要滥用session session对象是用来在多个请求之间保存和共享用户相关的数据,主要是因为HTTP协议是无状态的。有时候它便成了一个用于缓存的临时性解决方案。 这也不是说一定就会产生泄漏,因为web容器会在一段时间后让用户的session失效。但是它却显著提高了整个程序的内存占用量,这是很糟糕的。并且它非常难调试:就像我之前提到的,很难看出对象被哪些其他的对象所持有。 小心过量的垃圾搜集 虽然OOM很糟糕,但是如果不停的执行垃圾搜集将会更加糟糕:它会抢走本该属于你的程序的CPU时间。 有些时候你仅仅是需要更多的内存 就像我在开头的地方所说的,JVM是唯一的一个让你指定你的数据最大值(内存上限)的现代编程环境。因此,会有很多时候让你以为发生了内存泄露,但是实际上你仅仅需要增加你的堆大小。解决内存问题的第一步最好还是先增加你的内存上限。如果你真的遇到了内存泄露问题,那么无论你增加了多少内存,你最后都还
首先是进入DDMS,然后运行应用,这时候就能在左边的区域看到应用的包名了。选中要测试的应用,然后点击上方的update heap图标。 点击后控制台就会被触发了,但现在控制台可能没有下面的信息,因为只有在GC后控制台才会真正触发。所以你可以点击Cause GC按钮,然后就可以看到下面的信息了。 说明:当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化 这些数据包括当前的数据对象,类对象个数,我们主要关注的是最上面的那个汇总栏(有ID的那个表格),还有下面的data object(数据对象),也就是我们的程序中大量存在的类类型的对象。 在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下这个值的大小决定了是否会有内存泄漏。可以这样判断: a) 不断的操作当前应用,同时注意观察data object的Total Size值; b) 正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平; c) 反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大, 直到到达一个上限后导致进程被kill掉,这就是我们不希望的! 下面是我跑了我的一个例子,通过不断滑动照片墙来加载新的图片,从下面的动态图可以看见,当旧的图片被移出屏幕的时候引用了GC,占用的内存有明显的回落,接着开始上升(因为又加载了新的图片),但上升到一定程度便不会继续升高,这就说明这个程序不会不断的产生大量的对象,不太会出现OOM。
1.环境要求 Windows、JDK1.7.0以上、WinRAR 2.打包步骤 (1)从Jenkins打包平台取得最终作为发版外卖apk (2)apk重命名为src.zip(没错,就是改成一个压缩包) (3)打包工具解压,将src.zip解压到打包工具目录,如图 (4)先看一下打包工具目录,build.bat为我们最终执行打包任务的批处理文件,foodfinder.keystore就是我们打包使用的签名,sources.txt中保存着我们要打包使用的渠道号(以及产物apk文件名),最终产物会保存到release文件夹中,至于sources.txt中渠道号与产物文件名填写方式可直接通过后面提供的渠道号Excel表格复制后粘贴进TXT中即可 (5)build.bat文件中需注意的点: path后面winRAR的路径要填写好,其次是保证jdk、Java等环境变量配置OK,否则会报错,双击.bat文件,弹出命令框,等待至提示“按任意键结束”即可 (6)产物构建完之后,记得要及时将release文件夹中产物取出,否则下次构建会直接被当前构建产物覆盖 3.打包渠道号以及注意事项 (1)渠道号列表,左侧为全部应用商店渠道号,右侧为地推用渠道号(目前暂不需要,只需应用商店渠道即可,若PM要求另论) (2)不同应用商店使用的副标题 电子表格 副标题apk需要由RD提供,QA依据不同副标题母包进行渠道包打包操作
一、什么是内存泄漏? 大家都知道,java是有垃圾回收机制的,这使得java程序员比C++程序员轻松了许多,存储申请了,不用心心念念要加一句释放,java虚拟机会派出一些回收线程兢兢业业不定时地回收那些不再被需要的内存空间(注意回收的不是对象本身,而是对象占据的内存空间)。 Q1:什么叫不再被需要的内存空间? **答:**Java没有指针,全凭引用来和对象进行关联,通过引用来操作对象。如果一个对象没有与任何引用关联,那么这个对象也就不太可能被使用到了,回收器便是把这些“无任何引用的对象”作为目标,回收了它们占据的内存空间。 Q2:如何分辨为对象无引用? **答:**2种方法 引用计数法 直接计数,简单高效,Python便是采用该方法。但是如果出现 两个对象相互引用,即使它们都无法被外界访问到,计数器不为0它们也始终不会被回收。为了解决该问题,java采用的是b方法。 可达性分析法 这个方法设置了一系列的“GC Roots”对象作为索引起点,如果一个对象 与起点对象之间均无可达路径,那么这个不可达的对象就会成为回收对象。这种方法处理 两个对象相互引用的问题,如果两个对象均没有外部引用,会被判断为不可达对象进而被回收(如下图)。 Q3:有了回收机制,放心大胆用不会有内存泄漏? **答:**答案当然是No! 虽然垃圾回收器会帮我们干掉大部分无用的内存空间,但是对于还保持着引用,但逻辑上已经不会再用到的对象,垃圾回收器不会回收它们。这些对象积累在内存中,直到程序结束,就是我们所说的“内存泄漏”。 当然了,用户对单次的内存泄漏并没有什么感知,但当泄漏积累到内存都被消耗完,就会导致卡顿,崩溃。 二、发现内存泄漏 内存泄漏不可小视,在Android开发中,比如说一个Activity页面会占用许多资源开销,如果页面发生泄漏,关闭以后页面没有能被系统回收,对应用程序的伤害是很大的。 Q1:在Android开发测试中一般如何发现内存泄漏的发生呢? 答: 方法1:反复操作观察内存变化 内存泄漏常见变现为程序使用时间越长,内存占用越多。那我们通过反复操作应用,比如反复点开/关闭页面,观察内存变化状况是否一点点上涨,可以粗略地判断是否有内存泄漏 1.通过 DDMS 中的 heap 工具,可以查看应用内存的使用情况 2.Android studio也可以方便查看 方法2:通过代码检测Activity泄漏 基本思路: 1)debug版本可以起一个长期工作的线程LeakThread在后台专门做泄漏检测 2)向Application注册一个 页面生命周期 的监听:application.registerActivityLifecycleCallbacks 3)在监听类中对 onActivityDestoryed(Activity activity) 的事件回调做处理: 如果一个Activity走到onDestroy,那么这个Activity对象就是需要被回收的目标。 我们声明一个检测对象的弱引用ref = new WeakReference<Object>(activity)。 **PS:**与强引用和软引用相比,弱引用不会被回收器当做一个“有效”的引用,不会影响其引用对象的释放。实际上,垃圾回收器会毫不犹豫地回收只有弱引用的对象~ 4)在 LeakThread中我们每隔一段时间检测一下ref.get() 是否为空,为空说明activity已被释放。不为空可以手动触一次发gc;如果超过一段时间,比如50s,页面对象还未被清理,我们可以推断内存泄漏的发生. 5)当内存泄漏发生时,提示给开发者,并自动dump出.prof文件。 因为代码检测不是这里的重点,代码就不贴了,只记思路。 三、分析内存泄漏(DDMS dump + MAT分析) 发现可能出现内存泄漏时,我们需要对.prof文件进行分析,方能快速定位到是哪个倒霉家伙导致了内存泄漏 3.1、如何dump出.prof文件?(可参照前文图片) 打开DDMS ,Eclipse 可以切到DDMS视图,Android studio可以从Tools-Android-Android device monitor进入DDMS 找到app的进程,在进程上方点击“update heap”按钮,可以先主动出发一次GC,待内存占用数据稍微稳定下来后 点击“Dump HProf File”,便可以导出.prof文件 3.2:导出.prof文件后如何分析? Android studio可以直接打开prof文件。点开Analyzer Tasks的面板,点击右上角的开始按钮。 分析完成后,发生内存泄漏的页面对象会出现在Analysis Results面板-Leak Activityes的目录下。 如图,原来泄漏发生是LoadingRoomActivity的锅! 3.3 进一步分析泄漏的原因,你会需要一个好用的内存分析工具:MAT 在官网可以下载到它: http://www.eclipse.org/mat/downloads.php 虽然MAT不会准确告诉你你的代码哪泄漏了,但是它会给你发现哪泄露的数据和线索。 3.3.1 打开.hprof前可能遇到的问题: 在MAT中打开.prof页面,你可能会遇到一点小挫折: 如上图,可能会弹出 ‘Parsing heap dump from xxx has encountered a proplem’ 的错误弹窗 这是因为文件版本和编辑器能支持的版本有冲突的原因。 解决方案如下: Sdk安装目录下platform-tools里有一个hprof-conv工具可以解决该问题。在cmd控制台执行: hprof-conv input.hprof output.hprof 重新再MAT打开output.hprof 就可以打开了~ 值得一提的是,如果你dump出的文件太大的话,也有可能发现打不开的现象,这时候,打开安装MAT目录下的MemoryAnalyzer.ini 把-XmX改大些重启即可。但是也不要改得比你机器的可用内存还大,不能太贪心哈哈~ 3.3.2 打开.phrof文件后的分析 通过MAT打开.phrof文件后,会弹出Overview 和 Leak Suspects 2个标签页。 Leak Suspects标签页可见如下图: Leak Suspects视图展示了app内存占用的比例,浅色是空闲的内存,其他是内存占用的空间。每块内存对应的问题也都列在下面。点开每个Problem Suspect下的details,可以看到有哪些类的实例占用了内存和占用大小等信息~ 此时我们已经有了怀疑的目标,为了更清晰地查看,我们可以回到Overview页面,打开Histogram页面: 在打开的Histogram标签页中,我们填入检测对象,在列出的匹配项中过滤掉对象的非强引用。 到这里我们就可以看到,是哪个坏蛋hold住了你的对象了。MAT能够给到的支持也就到这里,接下来,还是需要你根据这些线索到代码中寻找判别和修正了~``
1.Java Crash java代码导致jvm退出,弹出“程序已经崩溃”的对话框,最终用户点击关闭后进程退出。Logcat会在“AndroidRuntime”tag下输出Java的调用栈。 2.Native Crash No.&Name Reason Resolution Comment 1.空指针 试图对空指针进行操作时(如读取空指针指向的内存),处理器就会产生一个异常 在使用指针前加以判断,如果为空,则是不可访问的。 空指针目前是糯米app最多的一种引起crash的原因,但是它也很容易被发现和修复。 2.野指针 指向的是一个无效的地址,该地址如果是不可读不可写的,那么会马上Crash;如果访问的地址为可写,而且通过野指针修改了该处的内存,那么很有可能会等一段时间(其它的代码使用了该处的内存后)才发生Crash。 在指针变量定义时,一定要初始化,特别是在结构体或类中的成员指针变量。 在释放了指针指向的内存后,如该指针不再用应置为NULL 看代码很难查找,通过代码分析工具也很难找出,只有通过专业的内存检测工具,才能发现这类bug。 。 数组越界 访问无效的地址。如果该地址不可读写,则会马上Crash;如果修改了该处的内存,造成内存破坏,那么有可能会等一段时间才在别处发生Crash。 所有数组遍历的循环,都要加上越界判断。 用下标访问数组时,要判断是否越界。 通过代码分析工具可以发现绝大部分的数组越界问题。 破坏内存的bug,很难查找。 整数除以零 整数除以零默认的处理方式是终止进程 在做整数除法时,要判断被除数是否为0的情况。 改情况在开发环境下很难出现,但庞大的用户量和复杂的用户输入,就很容易导致被除数为0的情况出现。 格式化输出参数错误 与野指针类似,但是只会读取无效地址的内存,而不会造成内存破坏。其结果是要么打印出错乱的数据,要么访问了无读写权限的内存而立即宕机。 在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致 缓冲区溢出 通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏函数调用的堆栈,修改函数调用的返回地址。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,造成程序崩溃。 检查所有容易产生漏洞的库调用,比如sprintf,strcpy等,它们都没有检查输入参数的长度。 使用带有长度检查的库调用,如用snprintf来代替sprintf,或者自己在sprintf上封装一个带长度检查的函数。 内存管理错误 可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。 内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。 imageview,图片占据太多内存,糯米app狂刷列表。