以淘宝app为例,可以看到在淘宝逛逛页面中,同时存在「关注」、「发现」、「视频」三个子tab,而在「发现」页面,同时存在「全部」、「玩乐」、「文创」、「家居」、「汽车」等子类目。当然在淘宝的实际业务中这些子类目应该是根据营销投放的数据接口动态配置类目名称和相关数据的,未必会有多状态管理的实际场景。这里只是以该页面结构作为演示,现在假设一种业务场景,需要判断是否是在「发现」->「玩乐」,如果是那就进行xxxx操作。
代码应该如何设计?这里推荐借助位运算,在同一个状态值上标记不同状态位的方式来进行状态管理。
1. 通过 << 定义数据的状态
1 | static final int CONST_TAB = 0x00000001; |
1 | // 给当前状态添加指定的状态标记 |
1 | // 判断当前状态model中是否存在状态标记 |
1 | // 从当前状态中移除指定的状态标记 |
试想一下,如果不使用同一个状态值上不同状态位,而是使用多个状态值的组合来表示很多状态,那么在处理状态逻辑时就要使用很多if else嵌套语句来进行组合判断。
使用位运算可以极大减少代码量,让逻辑判断变得更清晰更简单。
1 | private static void testDataInputStream() { |
虽然在API调用上两者表现几乎一致,但是在具体的实现上有较大的差别,BufferedInputStream增加了缓存buffer,通过先批量填充buffer再读取内存的方式提高了读取性能并且可以解决两端流速不匹配的问题。通过对象的组合设计对读取方式实现了增强,同样的,想要在buffer读取的基础上增加按照数据类型读取的能力,可以通过嵌套DataInputStream的方式实现。
Java IO类库在设计上选择了组合而不是继承的方式,在实现功能的同时减少了类的数量。通过IO类库可以简单总结下装饰器模式的特点。【通过向现有的对象添加新的功能,不改变其内部结构,并且在原有功能基础上对其进行增强】。就像同样是文件读取功能,Buffered能力和readChar能力可以组合使用,并没有改变被包装对象的内部实现,并且增强了不同类型的read能力。
装饰器模式的角色有:
对比Java IO库中的设计:
装饰器模式的优点:更灵活的扩展对象功能。相比较使用类继承,对象包装的方式更灵活。可以根据实际需要为一个对象任意组合嵌套多种装饰功能。
从代码结构的角度来说,装饰器模式和代理模式都使用了对象包装的方式,那么他们的的差异在哪里?在代理模式中,代理类附加的功能和原始类不相关,而在装饰器模式中,装饰器类附加的是和原始类相关的增强功能。
装饰器模式可以解决继承关系过于复杂的问题,通过组合关系替代继承关系。装饰器模式的主要作用是给原始类增加增强功能。一个原始类可以嵌套多个装饰器类。装饰器类需要与原始类继承相同的抽象类或者接口。
《王争:设计模式之美》
https://www.cnblogs.com/pluto-charon/p/16030199.html
关键词:Blocked,waiting to lock,locked
看堆栈:死锁堆栈,业务调用堆栈,IPC堆栈,系统调用堆栈
关键字:Load,CPU,Slow Operation,Kswapd,Mmcqd,Kwork,Lowmemkiller 等等
通过日志中这些关键字判断当前系统是否存在 (CPU,Mem,IO) 资源紧张的情况。
关键字:user,sys,IOWait
通过观察系统负载,则可以进一步明确是 CPU 资源紧张,还是 IO 资源紧张;如果系统负载过高,一定是有某个进程或多个进程引起的。反之系统负载过高又会影响到所有进程调度性能。通过观察 User,Sys 的 CPU 占比,可以进一步发分析当前负载过高是发生在应用空间,还是系统空间,如大量调用逻辑 (如文件读写,内存紧张导致系统不断回收内存等等),知道这些之后,排查方向又会进一步缩小范围。
关键字:CPU usage
测试同学反馈在测试环境上,某些操作后app必现ANR,通过bugreport命令抓取以下ANR trace。
1 | DALVIK THREADS (226): |
通过trace日志和viewpage源码并不能第一时间定位原因,只能看出并不是简单的锁同步问题导致的ANR。转而通过logcat日志查找更多有用的信息。
1 | Load: 0.6 / 0.62 / 0.48 |
1 | CPU usage from 0ms to 6338ms later (2022-07-12 12:05:52.611 to 2022-07-12 12:05:58.950) with 99% awake: |
通过以上日志可以看到,
Load: 0.6 / 0.62 / 0.48
ANR 发生前 1 分钟,前 5 分钟,前 15 分钟,系统的整体负载并不高,具体数值代表单位时间等待系统调度的任务数。
CPU usage from 24258ms to 13621ms ago
82% 16394/xxxxxxxxxxxx: 71% user + 11% kernel / faults: 4691 minor 1 major
ANR问题发生之前(CPU usage from XXX to XXX ago),app进程CPU使用率 82%,其中user占比71%,kernel 占比11%。system_server进程CPU使用率16% 。
CPU usage from 0ms to 6338ms later
119% 14827/xxxxxxxxxxxx: 109% user + 10% kernel / faults: 7495 minor
ANR问题发生以后(CPU usage from XXX to XXX later),app进程CPU使用率 119%,其中user占比109%,kernel 占比10%。system_server进程CPU使用率46% 。
通过以上数据可以判断,问题发生时,系统负载并不高,系统调用不频繁,系统IO负载很低。但是app用户进程的CPU资源很紧张,高CPU使用率应该发生在app的用户进程,大概率还是业务代码实现有问题导致的ANR。
通过日志大致判断问题发生在app业务代码中,梳理了以下的问题排查思路。
1 | void populate(int newCurrentItem) { |
大部分的ANR问题主要难点在于发现问题和分析问题发生的原因。一般的分析步骤是通过借助分析日志,从ANR Trace、logcat日志、kernel日志到消息队列日志,逐步将可能出现问题的范围缩小,通过CPU、IO、系统调用情况并结合代码走查定位问题原因。本文的分析过程相对比较简单,但是总体遵循这样的分析路径,最终定位并解决了该问题。
我们都知道当应用发生ANR时,系统会将ANR时的日志信息保存到在/data/anr/traces.txt文件中。可以通过分析traces.txt文件对引发ANR问题的原因进行分析,但是在实际发生问题的时候,因为系统文件访问权限的问题,很难在非root的系统中获取到/data/anr/traces.txt问题。所以这里需要借助另一个工具adb bugreport来获取异常日志。
https://developer.android.com/studio/debug/bug-report?hl=zh-cn
默认情况下,ZIP 文件称为 bugreport-BUILD_ID-DATE.zip,它可能会包含多个文件,但最重要的文件是 bugreport-BUILD_ID-DATE.txt。此文件就是错误报告,它包含系统服务 (dumpsys)、错误日志 (dumpstate) 和系统消息日志 (logcat) 的诊断输出。系统消息包括设备抛出错误时的堆栈轨迹,以及从所有应用中使用 Log 类写入的消息。
ZIP 文件中有一个 version.txt 元数据文件,其中包含 Android 版本号,而且启用 systrace 后,ZIP 文件中还会包含 systrace.txt 文件。Systrace 工具可以获取并显示应用进程和其他 Android 系统进程的执行时间,从而帮助分析应用的性能。
dumpstate 工具会将文件从设备的文件系统复制到 ZIP 文件的 FS 文件夹下,以便您引用它们。例如,设备中的 /dirA/dirB/fileC 文件会在 ZIP 文件中生成 FS/dirA/dirB/fileC 条目。
https://source.android.com/source/read-bug-reports.html?hl=zh-cn
1 | $ adb bugreport |
1 | $ adb shell ls /bugreports/ |
解压bugreport.zip就可以看到系统logcat和dumpsys日志信息,而我们想要重点分析的ant traces日志就只/FS/data/anr目录中。
1 | "main" prio=5 tid=1 Blocked |
1 | "pool-1-thread-14" prio=5 tid=60 Waiting |
1 | "DefaultDispatcher-worker-2" daemon prio=5 tid=88 Waiting |
通过以上的3条traces日志表现,可以大致得到以下线索:
通过查阅网上的资料大致了解一下WebView内核启动的源码,问题就更清晰了。原来在WebView内核的启动流程里,会有一个runnable被提交给主线程,负责在主线程执行startChromiumLocked方法,只有当主线程执行完该方法以后,才会结束等待状态。这也就是为什么上述的DefaultDispatcher-worker-2和pool-1-thread-14两个线程一直处于等待状态的原因。而发生死锁的原因正式因为在此时,主线程想要获取的锁对象正在被tid=60的pool-1-thread-14线程持有,startChromiumLocked方法又得不到真正的执行。这样相互持有依赖的锁对象而又都处于等待状态,这就是标准的死锁问题了。
WebViewFactoryProvider.getStatics()
1 |
|
WebViewChromiumAwInit.getStatics()
1 | public SharedStatics getStatics() { |
WebViewChromiumAwInit.ensureChromiumStartedLocked(Boolean)
1 | // This method is not private only because the downstream subclass needs to access it, |
正如以上分析的那样,可以写出以下的demo代码,就可以100%复现这个问题。解决这个问题的方法也很简单,避免主线程和子线程等待同一个对象锁就可以了。
1 | Object lock = new Object(); |
https://ivonhoe.github.io/2018/10/14/android-anr-1/
https://juejin.cn/post/6982896002680225800
Observatory是Dart VM的性能分析工具,可以借助Observatory对Dart VM进行内存,cpu,线程,渲染流程等方面的分析,在进行页面绘制性能分析时,可借助Observatory的timeline工具进行辅助分析。可以通过以下两种方式打开Observatory URL的Web页面。更详细信息可以查看官方文档:
https://flutterchina.club/debugging/
https://dart.cn/tools/dart-devtools
可以使用flutter run --profile | grep 127
命令可以看到,在终端中包含了观测台的地址
1.通过点击Android studio底部的工具栏图标,打开Devtools
2.在截取浏览器中显示的uri参数
3.借助urldecode工具解码,两次decode后就是Observatory本地服务地址。
Flutter提供了很多debug flag用来帮助开发者跟踪不同处理流程的执行过程,需要注意的是,所有debug开头的flag只有在debug模式下才会生效,如
最简单的方式是在程序顶部入口void main()
中设置它,如下案例代码所示:
这样我们就可以在Observatory的Timeline中查看widget build在每一帧中的调用和耗时情况
要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用systrace)。 使用dart:developer的Timeline工具来包含你想测试的代码块,例如:
1 | Timeline.startSync('interesting function'); |
这段代码等同于Android原生端的beginSection和endSection
1 | Trace.beginSection(""); |
然后打开你应用程序的Observatory timeline页面,在”Recorded Streams”中选择’Dart’复选框,并执行你想测量的功能。刷新页面将在Chrome的跟踪工具中显示应用按时间顺序排列的timeline记录。
使用命令:flutter run --profile --trace-skia
重点关注saveLayer和clipPath函数的调用
通过抓取skp,回放每条skia绘图指令的执行过程,可以在屏幕上直观的看到绘制流程的每个步骤,单步分析每一条绘图指令,从而定位在单帧绘制中影响绘制效率的问题。
点击查看【bilibili】
使用步骤:
flutter screenshot --type=skia --observatory-uri=uri
命令,抓取一帧skia绘图指令的执行过程,截取skp可以单步检测每条绘图指令ln -s ~/Library/Application\ Support/Google/AndroidStudio4.1/plugins ~/Library/Application\ Support/AndroidStudio4.1
https://www.sunmoonblog.com/2020/01/10/flutter-performance-tools/
https://cloud.tencent.com/developer/article/1591997
https://flutterchina.club/debugging/
https://flutter.cn/docs/testing/code-debugging
SPI,全称Service Provicer Interface。在JDK1.6内置的一套服务发现机制,主要用来服务架构扩展和替换组件。在面向对象设计中,最基本的原则就是面向接口编程,通过接口暴露模块的功能定义,将具体的接口实现隐藏在模块内部。而服务发现机制,提供了通过接口获取服务实现的能力,实现模块的可插拔、可拓展、可替换。保证了由调用方在调用时选择自己需要的外部实现。
简单来是在利用JDK提供的ServiceLoader机制可以通过以下几个步骤:
META-INFO/services
目录中,文件名为接口类的类名java.util.ServiceLoader.load
方法加载接口类,获取到在META-INFO中定义的接口实现类在META-INFO中的声明形式大致如下所示:
我们经常使用的autoservice(https://github.com/google/auto)解决了什么问题呢?其实autoservice只是提供了注解和注解处理器的能力,通过自定义一个annotationprocessor将@AutoService
注解标记的实现类信息,收集起来并保存到META-INFO/services
目录中。通过注解的方式将上面的步骤3优化掉了,这样既提高了效率,又避免了方案变更时需要手动更新配置文件的问题。
主要是类配置文件的加载耗时问题。在java项目中,java.util.ServiceLoader通过加载jar文件META-INFO/services目录下的配置文件获取实现类信息,当在Android项目中,java.util.ServiceLoader时通过加载apk包目录下META-INFO/services中的配置文件,这里都涉及到整个zip文件的遍历,加载效率是非常低的,一般情况下会导致几百毫秒的耗时。而如果在一些关键流程中触发了JDK ServiceLoader的加载,几百毫秒就会带来很差的用户体验。
先梳理现状:
ServiceLoader加载耗时过长的主要原因是,需要从zip文件中检索出meta-info目录下的配置文件,想要提升配置加载的效率,解决方法也很简单,可以将配置文件保存到更容易加载的地方,这里可以选择将配置信息保存到安卓原生提供的资源路径下,也可以选择将配置信息保存到类文件中。当然想要在相对复杂的业务系统中解决一个问题,既包括解决技术问题本身,也需要考虑到方案落地的成本和影响面。特别是高业务价值低技术价值的遗产系统中做技术优化,首先保证的是兼容现有业务逻辑。这里优先选择将相关配置信息保存在类文件中,借助android编译插件技术,在编译期间做信息收集,在运行时通过类加载的方式将所需service信息加载出来。这里可以主要参考wmrouter的实现思路,但是又稍有差别。
先对比wmrouter的实现方案,可以发现大致可以分为以下几个步骤:
通过以上流程的说明,可以看到整个流程被分为两步:
前面也提到,在现有的遗产系统中,如果照搬wmrouter的方案会存在什么问呢?如果照搬上述的方案,在annotationprocessor处理阶段,需要将所有使用autoservice @AutoService的组件使用新的annotationprocessor重新发布一边。这个工作量和变更范围是巨大的。有没有更优化的方案呢?
解决思路也很直接,将注解的收集和配置信息保存到类文件合并成一步。
通过在AGP transform流程中收集注解,同时使用ASM框架动态生成相关字节码,将相关的实现类信息保存到类文件中。就可以避免重新发布大量业务组件的问题,将变更范围控制到最小。
ServiceLoader带来的性能问题,一旦被发现,解决思路其实是很直接的,并没有太多拐弯的地方。但是在这个技术方案落地的过程中遇到的问题比技术方案本身多很多。特别是在已有大量业务系统在使用的组件中做技改,影响面的评估和回滚方案是要优先被考虑的,所以在运行时加载配置阶段,需要通过配置开关控制是优先加载类信息还是保持原加载流程不变,来做到技改方案可观测可回滚。同时在技术方案的选择上,应该选择影响面尽可能小,变更尽可能少的方案。尽可能避免业务方的更改。
https://zhuanlan.zhihu.com/p/28909673
https://github.com/meituan/WMRouter
在介绍设计模式之前,先了解几个基础的概念。了解概念的含义并不是为了咬文嚼字,而是希望能从原理上理解设计模式背后想要解决的问题
a.test(b)
,a就是消息的接受者,这个函数的调用方就是消息的发送者。a.test(b)
为例,如在Java中,在被执行的test函数,只跟a对象的运行时类型有关。a.test(b)
为例,哪个test函数被执行,不单单和a对象的类型有关还和b对象的类型有关。可以看到所谓分派就是函数的调用,所谓单分派和双分派就是和语言的多态特性有关,在常见的Java,C++,C#语言中,在语言层面都是只支持单分派的。想要实现双重分派,就要借助设计模式,比如访问者模式。你肯定会问,双重分派的作用是什么?不解决双重分派的问题不行吗?其实这种问题在项目代码中一定俯拾皆是,类似下面的这种代码,我们想要针对不同类型的文件(pdf,ppt,word)执行不同的文件提取和文件压缩操作。试想下,可能是你来实现,你要怎么做?是不是很容易写出下面这样的代码
1 | class Extractor extends Processor { |
可以看到这段代码的逻辑执行,既要根据接收者的运行时类型来决定processXXXFile(file)
的执行,这里的接收者可以理解成是当前方法所对应的Processor
对象。又要根据ResourceFile file
对象的运行时的实际类型来做类型的判断,这里就会有很多instanceof
和else if
,switch case
的多重嵌套。这种设计的代码虽然可以实现功能,但是在面对需求变更和扩展时会非常不灵活,既要加很多else if
,也不利于功能的内聚和复用。那么这些代码如果用访问者模式,应该怎么来实现呢?这里借用王争在<<设计模式之美>>中的实例代码:
1 | public abstract class ResourceFile { |
上面介绍了访问者模式设计初衷和设计方法,这里再看下访问者模式在实际工程中的应用。访问者模式最常见的应用场景就是访问复杂的结构或者对象,在不改变数据结构的情况下,将数据访问和数据操作分离出来,用回调的方式在访问者中处理业务逻辑。在面对不同的访问处理时,只需要新定义一个访问者实现不同的访问处理逻辑就可以了。这样说可能也很抽象,可以在在ASM中,是如何利用访问者的设计模式,实现字节码文件的读取和修改的。
ASM使用ClassReader遍历class文件结构获取文件中的类和对象信息,在其accept方法中接收ClassVisitor,在ClassVisitor的不同回调方法中完成不同的字节码操作。可以通过代码示例看到主要有以下几个类:
1 | ClassReader cr = new ClassReader(inputStream); |
利用访问者模式来解决这样的双重分派问题,如上面的类图所示,通过几个角色来做功能的区分,将文件的访问和处理分离成两个独立的接口。
理解访问者模式的设计,我觉得重点在理解所谓的回调再回调。这里有两次回调就意味着有两种类型接口,第一次回调是被访问者通过accept接受访问者,第二次回调是访问者通过visit方法访问被访问者,通过两次互相调换类型的调用,也就是通过两次单分派实现了双重分派。
王争<<设计模式之美>>
https://www.jianshu.com/p/cd17bae4e949
https://www.liaoxuefeng.com/wiki/1252599548343744/1281319659110433
代理控制了对象的真实访问。代理模式是指,在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。主要解决在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
这里直接访问对象时带来的问题,可以类比上面生活中的例子,车站买票太远(对象单次访问成本高)选择用代买点买票。直接去酒店订房间太麻烦(实现功能的对象太多无法直接选择出最佳的对象)而选择在酒店平台预定酒店。
几乎所有安卓开发应该都应该知道,binder是android中实现进程间通信的方案,也都听说过所谓共享内存、Socket、管道、消息队列等等一系列其他的进程间通信(IPC)的方案,甚至听说过诸如dubbo,spring cloud之类的远程过程调用(RPC)。这里binder作为一个IPC方案同时在设计上又有很多RPC方案的影子,如果说想要理解RPC,这里一定绕不开代理模式。不管是Android的binder还是后端消息中间件dubbo都是使用代理模式来做设计的,在通讯的两端分别用代理隐藏实际的通信细节,让调用方像调用自己进程内对象方法一样实现对跨进程对象的调用。你比较熟悉组件化开发的话,也可以先从组件化的视角来类比binder通信,如果让你来实现一个进程间通信的架构方案,有哪些东西是必不可少的呢?
先类比组件化
再看Binder IPC,
1 | interface IMyAidlInterface { |
queryLocalInterface
和Stub.DESCRIPTOR
查询到远程服务在client的代理对象1 | public static com.android.aidldemo.IMyAidlInterface asInterface(android.os.IBinder obj) { |
Stub.proxy
隐藏了和远程服务对象真实通信的细节,client不需要关心这个代理对象是不是真实的服务实现方,就像调用本地方法一样调用原生服务对象。而Stub.proxy
实现了调用参数的序列化和响应结果的序列化,帮助client拿到了远程调用的结果。可以对照下图理解代理过程:
同样的角色分布我们可以再看看系统的ActivityManagerService。IActivityManager是一个服务接口,代表了服务能力。ActivityManagerNative代表系统本地服务,ActivityManagerService是它的具体实现。而ActivityMangerProxy代表在app中的Binder代理对象,实现client到service调用的代理转发。
相比较Binder的静态代理,retrofit使用了动态代理模式。所谓动态代理模式,是指并没有手动创建一个代理类,而是使用动态字节码的方式创建代理类(用class生成class),然后使用反射的方式创建代理类的对象,再使用反射方式调用被代理的方法。可以先看一个在java中最简单的动态代理写法:
1 | public class TestProxy { |
我们使用Proxy.newProxyInstance
和InvocationHandler
动态构造代理对象,通过获取invoke method注解对请求的描述信息,生成ServiceMethod
对象,并根据该对象执行相应的网络请求。
1 | public <T> T create(final Class<T> service) { |
这里借用知乎大佬的一张图,同时对比下静态代理和动态代理的差异,主要在于代理类如何生成。
如果是编译或者编码过程中生成的代理类就是静态代理,所以静态代理的一个缺点就是会生成很多代理类。
如果在运行时或者编译时动态生成的代理类,一般就是动态代理。可以通过接口的Class对象,创建一个代理Class,通过代理Class创建代理对象。也就是所谓的用Class造Class。
http://weishu.me/2016/01/12/binder-index-for-newer/
https://zhuanlan.zhihu.com/p/35519585
http://gityuan.com/2016/09/04/binder-start-service/
https://www.zhihu.com/question/20794107
从设计模式的角度,拦截器的设计常常称之为责任链模式(Chain of Responsibility)。责任链模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链模式的优点:
责任链模式的缺点:
1 | /// 拦截器起点和终点 |
1 | /// 拦截器迭代 |
1 | /// 单个拦截器递归回调到拦截器链 |
和okhttp的拦截器相比,wmrouter中的拦截器差异在于他并没有response返回值,拦截器之间通过callback的方式返回结果或中断拦截流程。个人理解主要有以下几点考虑。
wmrouter在查找路由的过程中,使用了多层UriHandler和Interceptor嵌套的拦截器,在UriHandler的handle方法中,通过shouldHandle是否被当前的Handler拦截,再通过是否包含拦截器来确定是handler拦截还是Interceptor拦截,如果是Interceptor拦截则进入下一层。
所以相比较okhttp的拦截器,wmrouter的拦截器模式分层更明显,更像洋葱。
1 | public void handle(@NonNull final UriRequest request, @NonNull final UriCallback callback) { |
在ChainedInterceptor中根据递归调用next方法更新拦截器迭代,通过Callback回溯到上一层,这里的上一层可能是Interceptor也可能是UriHandler。
1 | private void next(@NonNull final Iterator<UriInterceptor> iterator, @NonNull final UriRequest request, |
可以参照下图理解
可以参照wmrouter官方文档的流程图理解ChainedHandler和ChainedInterceptor的多层嵌套关系。
你一定看过类似这样的事件分发流程图(出自https://www.gcssloop.com/customview/dispatch-touchevent-theory):
在touch分发的流程中三个关键的方法,dispatchTouchEvent(),interceptTouchEvent(),touchEvent()分别代表事件分发,事件拦截,事件消费。你也一定听过所谓的U型事件传递路径,从Activity开始分发,到ViewGroup,再到View。根据方法返回值是true还是false来决定上述的三个方法是否分发、拦截或消费touch事件。参考如下伪代码理解
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
如果从责任链的角度来看,你是否想过在整个事件分发的流程中,整套机制的最终目标是什么?
当parent和child同时设置了click事件监听,为什么是child优先响应?
因为click事件是在onTouchEvent中响应的,而onTouchEvent的消费顺序是先child后parent,当view设置了touchListener或者是clickListener,事件就会被view拦截
http://c.biancheng.net/view/1383.html
https://www.debug8.com/javascript/t_66952.html
https://segmentfault.com/a/1190000012227736
https://www.gcssloop.com/customview/dispatch-touchevent-theory
https://blog.csdn.net/lfdfhl/article/details/50707724
在项目中升级AndroidX时遇到一个运行时异常,查看日志可了解到是因为找不到方法addOnTabSelectedListener
,异常堆栈如下:
1 | E Caused by: java.lang.reflect.InvocationTargetException |
因为从support27到support28版本,TabLayout的API发生了变更,addOnTabSelectedListener
方法入参发生了变化,可通过查看下图中的函数字节码可以看到,AndroidX中的该函数的入参为BaseOnTabSelectedListener
,而support27中的该函数的入参为onTabSelectedListener
。函数字节码如下:
发生改问题的原因是AndroidX是基于Support Library 28的重构。而在当前项目中依赖了27.0.2的TabLayout support包,并在依赖support27的基础上使用进行了AndroidX包名替换转换。所以在动手之前还是建议先把官方看清楚,在官方文档里说的很清楚了,要先把当前的 Support Library 依赖升级至版本28,然后再使用Android Studio的工具转换AndroidX。
demo说明:
methodA
methodTest
声明的函数参数为接口A1 | class TestMethod { |
反编译以上Java代码:
1 | Compiled from "TestMethod.java" |
由以上可知,所谓的方法签名至于函数定义时声明的参数类型有关,和调用时传递的参数类型无关。
方法签名为了唯一标识一个方法。如果你查询一些资料和文档可能会得到下面的说法:
说法一:
同一类中不能存在两个名字及描述符完全相同的方法。
在Java中方法签名包括:方法名、形参参数列表、泛型方法类型参数列表。Java的方法签名并不包括返回值和访问修饰符。当类中存在签名相同的两个方法时编译会报错,当两个方法的其中一个签名是另一个方法的子签名时也会报错。自签名的定义是一个签名在类型擦除后与另一个签名相同,则称其为第二个签名的的子签名。
说法二:
在同一class文件中,两个方法可以拥有同样的特征签名,前提是返回值不能相同。
这看起来是自相矛盾的啊,到底一个函数签名包不包含方法的返回值类型呢?
如下面所示的代码,使用Java编译器编译一定是编译不通过的。
1 | public class TestMethodSameName { |
但是如果你使用使用javac和javap命令查看方法签名时你会发现
1 | javac TestMethodSameName.java |
可以看到所谓方法描述是包含函数参数类型和返回值类型的(descriptor: ([Ljava/lang/String;)V)
.所以是不是以上说法一和说法二都对呢?
其实说法一针对的是Java编译器,说法二针对的是JVM的。在虚拟机的标准里是允许一个类中方法名和形参一样但是返回值不一样,因为这两个方法对应的方法描述不一样,针对字节码层面这种情况是存在的,JVM是也正常运行的。
那么为什么JVM支持的东西,JAVA偏偏不支持呢?
因为在方法调用的时候并不能保证指定了方法的返回信息,编译器并不知道你实际调用了哪个方法。还是以上面的代码为例,当忽略返回值的调用时就搞不清到底在调用哪个方法了。
1 | foo(1) |
重载:同一个类中方法相同方法参数不同的方法。不能通过返回类型是否相同来判断重载。
重写:方法签名必定相同,返回值必定相同, 访问修饰符 子 > 父, 异常类 子 < 父
JVM调用方法有五条指令,分别是
程序绑定: 指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定。
静态绑定: 在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。例如:C。针对java简单的可以理解为程序编译期的绑定。invokespecial和invokestatic是采用静态绑定。
动态绑定: 在运行时根据具体对象的类型进行绑定。invokevirtual和invokeinterface是采用动态绑定。
可以看到,在调用private、super、
回到问题,父类的private方法子类可以重写吗?
答案很明显,不能,从invoke指令的角度看,父类的private方法的程序绑定在编译期就已经确定了,跟Java的多态特性是无关的,所以不能被重写。
如果只是针对AndroidX的升级其实很简单,只要关注到一点,AndroidX的升级和转换要保证所有依赖的support包都升级到28.0.0以上。
如果从JVM规范和指令角度看,一个很小的点还是有很多值得深挖的地方的。
https://mp.weixin.qq.com/s/fmnoKH-R9PCmg-3ATRMIbQ
http://wxweven.win/2017/09/15/JVM-invokespecial%E5%92%8Cinvokevirtual/
https://hllvm-group.iteye.com/group/topic/27033
https://www.iteye.com/blog/rednaxelafx-479301
https://www.cnblogs.com/onlywujun/p/3523991.html
问题日志:
1 | java.lang.NullPointerException: Attempt to read from field 'int android.graphics.Bitmap$Config.nativeInt' on a null object reference |
问题代码:
1 | val opts = BitmapFactory.Options() |
根据日志可以看出是Bitmap的config为null引起的空指针问题,在使用BitmapFactory.decodeStream时,正常解析到了Bitmap对象,但是该图片的Config获取为null,进而引发一个空指针异常,导致了后续图片加载的失败。该段代码也很简单,重新写了个demo应用单独运行这段代码,并特别使用了出现线上故障的资源图片,测试的结果依然是debug版本的demo Bitmap.getConfig获取正常,为ARGB.8888。release版本 Bitmap.getConfig获取为null。尝试修改inPreferredConfig为ARGB_8888或删除这个参数。demo运行正常,Bitmap和Bitmap.Config都能正常获取。
这是一个和inPreferredConfig参数有关的问题?考虑到该段代码加载的图片资源可能是透明的png,首先需要承认的是使用565模式解析有alpha通道的图片资源是并不科学的,虽然565配置对非透明图片的加载可以省一般的内存空间。但文档不是这么说的呀?inPreferredConfig
参数指定的配置并不是一个非强制的选项,而是建议选项,Android在实际解码时会参考此参数的配置,但如果此配置不满足,Android会重新选取一个合适的配置来对图片进行解码。
所以基于文档的说明,使用RGB_565的配置decode有alpha通道的图片并不会有什么问题,在正常情况下Android系统实际还是会选择ARGB_8888的配置进行加载。现在很明显这不是正常情况。不正常的点在哪里?看起来肯定和这张图片有关,换个图片就能解决这个问题了,不然换个图片?不用inPreferredConfig
参数也行,但是为什么?问题到底出在哪里?
一头扎进BitmapFactory.cpp中,然而并没有什么明显的线索,头大了~~
看了大半天BitmapFactory的源码,对加载流程大致有了了解,但是对解决问题并没有什么思路,和大师交流这个问题时,他的一句话点醒了我。
最最诡异的其实是debug和release差异的问题啊。难道release打包时,资源打包对这个图片动了手脚?所有,手动将release版本的app解压出来,找到有问题的png。果然发现了不一样的地方,使用AS的图片查看器查看png图片,在右上角可以看到,原本在源码中32-bit color的图片在release包中变成了8-bit color。
原本2k的图片在从release包里解压出来后只有700B,aapt对资源做了压缩,难道是压缩引起的问题?在编译文件中配置cruncherEnabled false
关闭对png的压缩操作。
1 | aaptOptins{ |
问题消失了,实锤!和资源打包时对png的压缩有关~
什么是alpha通道?
将图像转换为调色板颜色模式时,会给每一个像素分配一个固定的颜色值,这些颜色值储存在简洁的颜色表中,或包含多达256种色的调色板中。因此,调色板颜色模式的图像包含的数据比24位颜色模式的图像小,对于颜色范围有限的图像,通过这个色彩转换模式效果更佳,用户可以设定转换颜色的调色板,从而得到指定颜色的阶数的位图。
调色板颜色模式也被称为是索引模式,调色板颜色模式只有在图像颜色小于等于256色的时候才有,16位高彩和24位32位真彩是没有调色板色的。它只支持单通道图像(8位/像素),因此,我们通过限制调色板、索引颜色减小文件大小,同时保持视觉上的品质不变——如用于多媒体动画的应用或网页。只有16位以下的才用调色板,真彩色不用调色板。
aaptOptins cruncherEnabled
为true
时进行了png压缩操作。将png图片的32位全彩色模式图片转换成了8bit位索引模式选择使用ARGB8888或者不设置inPreferredConfig
参数进行bitmap decode。其实默认inPreferredConfig
值就是ARGB_8888
。
1 | public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888; |
inPreferredConfig
的时候要注意什么?参数inpreferredconfig表示图片解码时使用的颜色模式,也就是图片中每个像素颜色的表示方式。参数inpreferredconfig的可选值有四个,分别为ALPHA_8,RGB_565,ARGB_4444,ARGB_8888。它们的含义列举如下。
根据Android官方文档的说明,如果inPreferredConfig不为null,解码器会尝试使用此参数指定的颜色模式来对图片进行解码,如果inPreferredConfig为null或者在解码时无法满足此参数指定的颜色模式,解码器会自动根据原始图片的特征以及当前设备的屏幕位深,选取合适的颜色模式来解码,例如,如果图片中包含透明度,那么对该图片解码时使用的配置就需要支持透明度,默认会使用ARGB_8888来解码。
inPreferredConfig指定的配置并非是一个强制选项,而是建议的选项,Android在解码时会参考该配置,如果该配置不满足,Android会重新选取一个合适的配置来对图片进行解码。
详细的验证过程可参考https://blog.csdn.net/ccpat/article/details/46834089
从 getConfig 方法的文档可以看到这个描述: If the bitmap’s internal config is in one of the public formats, return that config, otherwise return null.
如果位图的内部 config 是公开格式的其中之一就返回这个 config,否则返回 null。
这里的内部格式,就是指 SkBitmap::Config 枚举值了,这里的公开格式指的是Java层Bitmap.Config的枚举值。如果这个值并未在 Java 层 Bitmap.Config 中公开,就返回 null,像索引颜色对应的 kIndex8_Config 就会导致 getConfig() 会返回 null。
可查看源码external/skia/include/core/SkBitmap.h
,Skia的颜色配置,其中kIndex8_Config表示每像素8bits,使用 SkColorTable 来描述颜色,这个 SkColorTable 顾名思义就是颜色表了。
1 | enum Config { |
external/skia/src/images/SkImageDecoder_libpng.cpp
的getBitmapConfig方法,如果当前图片是调色板模式,会执行canUpscalePaletteToConfig
方法,该方法返回 false 则 configp 被设置为 kIndex8_Config。
1 | bool SkPNGImageDecoder::getBitmapConfig(png_structp png_ptr, png_infop info_ptr, SkBitmap::Config* configp, bool* hasAlphap, SkPMColor* theTranspColorp) { |
当dstConfig为565_config并且图片包含alpha通道时,或者dstConfig不属于kARGB_8888_Config、kARGB_4444_Config 、kRGB_565_Config之一,则canUpscalePaletteToConfig
如何才能令其返回false
。
1 | static bool canUpscalePaletteToConfig(SkBitmap::Config dstConfig, bool srcHasAlpha) { |
再看getPrefConfig的实现,getPrefConfig返回由java层设置的fDefaultPref,当config为kNo_Config时,返回GetDeviceConfig(),其实也是kNo_Config。那么config什么时候为SkBitmap::kNo_Config呢?当Java层设置的inPreferredConfig为null时,fDefaultPref会被赋值为kNo_Config。详情可查看getNativeBitmapConfig
方法。
1 | SkBitmap::Config SkImageDecoder::getPrefConfig(SrcDepth srcDepth, bool srcHasAlpha) const { |
综上所述,只要PNG文件本身是索引颜色格式,且在调用BitmapFactory.decodeXXX方法族时,将传入的BitmapFactory.Options.inPreferredConfig置为null即可解码得到索引颜色格式的Bitmap对象,如果这张PNG是带有alpha通道的,inPreferredConfig设置为RGB_565也可以。此时该Bitmap的Config为null。
https://www.jianshu.com/p/ecacf2f60cb2
https://www.jianshu.com/p/cc17d18c3447
http://www.coreldrawchina.com/X7jiaocheng/cdr-tiaosebanyanse-moshi.html
https://www.jianshu.com/p/f56292504ad3
https://blog.csdn.net/ccpat/article/details/46834089
https://android.googlesource.com/platform/frameworks/base/+/marshmallow-dev/tools/aapt2/Png.cpp
aspectd的环境搭建需要flutter源码、aspectd源码和dart源码,并需要在系统中设置相应的全局环境变量。
下载flutter源码:
1 | git clone https://github.com/flutter/flutter.git |
下载aspectd源码:
1 | git clone https://github.com/alibaba-flutter/aspectd.git |
配置flutter镜像、本地flutter源码地址、flutter bin目录、dart bin目录:
1 | export PUB_HOSTED_URL=https://pub.flutter-io.cn |
aspectd需要
1.切换到flutter的git目录:cd ${path-for-git-flutter}
2.将aspectd源码中的git patch文件合并到flutter源码工程中,合并git patch:git apply --3way ~/Github/aspectd/0001-aspectd.patch
3.删除原有的的flutter编译工具:rm bin/cache/flutter_tools.stamp
4.重新构建新的flutter编译工具:flutter doctor -v
到aspectd源码目录的example目录下执行:flutter run --debug --verbose
如果你能一次运行成功并aspectd生效,请直接跳转到第二章!
编译不过或运行demo没有打印出想要的日志是aspectd使用时最常见的问题。aspectd的基本原理实际上是使用了dart对虚拟语法树操作的api,通过对flutter dill文件进行虚拟语法树遍历,完成对dill文件的转换,进而实现对dart的切面操作。所以在aspectd的编译上需要依赖dart源码中的kernal
和front_end
,可通过查看aspectd源码根目录中的pubspec.yaml
查看依赖库和对应的ref。
1 | dependency_overrides: |
在1.2步骤中,使用git patch命令修改flutter源码引入了aspectd.dart
文件,该文件做的核心操作就包括下载aspectd的依赖库、编译aspectd.dart.snapshot和根据注解内容使用aspect.dart.snapshot执行具体的dill transform操作。所以,aspectd是否生效的两个关键点是aspectd依赖库是否下载成功和aspectd.snapshot文件是否编译成功。
因为aspect使用依赖github源码指定ref的方式依赖kenerl和front_end库,这个过程需要下载github上dart-lang的所有源码(约900M左右),在国内的网络环境下很难做到一次成功,这里分享一个绕过因网络不稳定问题导致aspectd不生效的方法。
git clone https://github.com/dart-lang/sdk.git
pubspec.yaml
指定的ref上,如上例中,可执行 git checkout 5e39817ec7ab7f56f381c244d105c7e40913a3e0
dart --snapshot=snapshot/aspectd.dart.snapshot tool/starter.dart
flutter run --debug -v
即可生效等待另一个flutter命令释放锁Waiting for another flutter command to release the startup lock...
解决方法,将bin/cache下的lockfile删除后重新执行命令rm ${path-for-git-flutter}/bin/cache/lockfile
如何使用命令行编译工程
debug版本:flutter run --debug --verbose
release版本:flutter run --release --verbose
pub命令是什么?flutter pub get
pub是dart提供的包管理工具,在flutter源码中的flutter/bin/cache/dart-sdk/bin/pub
目录下有pub可执行文件,想要单独执行pub命令可讲该目录加入到系统的环境变量中
相当于android gradle的gradle sync
相当于ios pod中的pod install
相当于js npm中的npm install
在AOT变一下,如果不能被应用主入口(main)最终可能调用到,那么将被视为无用代码而被丢弃掉。AOP代码因为其注入逻辑的无侵入性,所以不会被main调用,因为使用此注解告诉编译器不要丢弃这段逻辑。
Aspect注解可以使得像asepctd源码example中aop_impl.dart
这样的AOP实现类被方便的识别和提取,也可以起到方便开关的作用,如果想禁用掉这段AOP逻辑,移除@Aspect注解即可
在介绍这几个注解之前需要理解关于AOP的几个概念,aspectd官方介绍文档对aspectd的说明引入了很对对aop设计的说明,比如什么是Advice?什么是Before\Around\After?如果对这些概念没有预先的概念,读aspectd的文档是一头雾水的,至少我是这样!
能够插入切面的一个点。这个点可以是类的某个方法调用前、调用后、方法抛出异常后等。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为
指定一个通知将被引发的一系列连接点的集合。切点是连接点规则的描述。切点和连接点不是一对一的关系,一个切点匹配多个连接点
包含连接点的对象
在特定的连接点,AOP框架执行的动作。通知有常见的几种类型:
aspectd只有一种统一的通知类型,就是Around。具体分为两种注解,分别是@Call和@Execute,这两种注解表达的PointCut都是通过包装原有方法实现的。差别是,@Call的PointCut是调用的地方,并不会修改原始方法的内部。@Execute会修改原有方法的内部。举个例子,分别使用@Call和@Execute对test
方法执行切面操作
1 | void test(){ |
@Call表达注解的实际代码会变成这样:
1 | void test(){ |
@Execute表达注解的实际代码会变成这样:
1 | void invokeExecutor(){ |
而@Inject相对于Call/Executor而言,多了一个lineNum的参数,用于指定插入逻辑的具体行号。用于在具体方法中间插入处理逻辑。
adb shell dumpsys meminfo $applicationId [-d]
私有内存 Private (Clean and Dirty)
这是仅属于你的进程的内存,这是你的进程被销毁时系统可以回收的RAM。通常情况下最重要的部分是Private Dirty RAM。
按比例分配占用内存 PSS
这表示您的应用的 RAM 使用情况,考虑了在各进程之间共享 RAM 页的情况。您的进程独有的任何 RAM 页会直接影响其 PSS 值,而与其他进程共享的 RAM 页仅影响与共享量成比例的 PSS 值。例如,两个进程之间共享的 RAM 页会将其一半的大小贡献给每个进程的 PSS。
通常情况下,仅需关注 Pss Total 和 Private Dirty 列。一些情况下,Private Clean 和 Heap Alloc 列提供的数据也需要关注。
Dalvik Heap
PSS Total: 包括所有 Zygote 按比例分配的内存。
Private Dirty: 数值是仅分配到您应用的堆的实际 RAM,是由自己的分配和任何 Zygote 分配页组成,这些分配页自从 Zygote 派生应用进程以来已被修改(COW)。
.so mmap 和 .dex mmap
Pss Total: 包括应用之间共享的平台代码
Private Clean: 是应用自己的代码
.oat mmap
这是代码映像占用的 RAM 量,根据多个应用通常使用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响
EGL mtrack 和 GL mtrack
EGL mtrack: gralloc分配的内存,主要是窗口系统,SurfaceView/TextureView和其他的由gralloc分配的GraphicBuffer总和
GL mtrack: 驱动上报的GL内存使用情况。 主要是GL texture大小,GL command buffer,固定的全局驱动程序RAM开销等的总和
这里有一个小技巧,应用开发者调用startTrimMemory会帮助app或者系统更多的释放内存,减少内存压力,但是调用的位置和时机要慎重,因为清除了缓存,在下一次绘制(vsync的下一个信号到来)的时候绘制效率不会很高。详细原理可参考:https://cloud.tencent.com/developer/article/1070616
1 | /** |
TOATAL
进程占用的按比例分配占用内存 (PSS) 总量。等于上方所有 PSS 字段的总和。表示您的进程占用的内存量占整体内存的比重,可以直接与其他进程和可用总 RAM 比较。
Private Dirty 和 Private Clean 是进程中的总分配,未与其他进程共享。它们(尤其是 Private Dirty)等于你的进程被破坏后将释放回系统中的 RAM 量。Dirty RAM 是因为已被修改而必须保持在 RAM 中的 RAM 页(因为没有交换);Clean RAM 是已从某个持久性文件(例如正在执行的代码)映射的 RAM 页,如果一段时间不用,可以移出分页。
ViewRootImpl
根视图数量,每一个根视图关联一个window,由此可确定涉及对话框或其他window的内存泄露。
AppContexts 和 Activities
Context和Activity对象数量,用于快速确定Activity的泄露情况。
将android app的内存信息转换成hprof格式的磁盘文件,这个过程就叫堆转储。堆转储的目标当然是为了获取当前Java虚拟机的内存信息以供排查内存问题。在android平台上常见的三种堆转储方法。
1 | try { |
Memory Analyzer Tool(简称MAT) 使用方法可参考: Android内存优化之二:MAT使用进阶。
这里补充的是,MAT支持使用OQL(对象查询语言)查询堆文件信息,你可以像类似使用SQL查询数据库一样查询堆文件上的内存对象的信息,举个例子,我想知道当前内存中有多少个Activity?
1 | select * from instanceof android.app.Activity |
查询大小大于100kb大小的Bitmap对象,图略
1 | select * from instanceof android.graphics.Bitmap s where s.@retainedHeapSize >100000 |
关于OQL详情参考:http://help.eclipse.org/kepler/index.jsp,搜索OQL
LeakCanary是内存泄露检测工具,LeakCanary 中文使用说明和LeakCanary的开源地址:https://github.com/square/leakcanary
如何监控内存泄露的发生?
利用弱引用。当JVM进行垃圾回收时,无论内存是否充足,如果该对象只有弱引用存在,那么该对象会被垃圾回收器回收。所以Leakcanary在进行内存泄露的监控时,利用弱引用的上述特性,在对象生命周期结束后主动gc并检查该对象的弱引用是否被回收,如果弱引用没有被正常回收,说明在对象生命周期结束以后,该对象还被其他对象持有他的非弱引用。该对象还有到达GC ROOTS的可达路径。如果在对象生命周期结束后弱引用不存在了,说明该对象已经被JVM的垃圾回收器正常回收了,该对象的内存空间也被正常回收。所以Leakcanary设计成是对有明确生命周期的对象的自动监控,比如Activity对象,也可以是你想要跟踪的有明确生命周期的对象。
如何判断弱引用被回收?
利用ReferenceQueue。当垃圾回收器准备回收一个被引用包装的对象时,该引用会被加入到关联的ReferenceQueue。程序可以通过判断引用队列中是否已经加入引用,来了解被引用的对象是否被GC回收。
1 | Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) { |
如何分析对象的被引用情况?
遍历对象树。
可使用square提供的堆文件分析库(com.squareup.haha:haha:2.0.4
)对hprof文件进行解析得到对象树。
1 | private Snapshot getSnapShot(String hprofPath) { |
可根据解析获得的Snapshot对象完成对特定类名对象的查找操作。进而批量分析出当前内存中执行类型对象的数量和对象引用情况。
1 | List<ClassObj> classObjList = snapshot.findAllDescendantClasses(filterKey()); |
可通过解析到的snapshot对象获取Root节点,自定义对象数爬虫,从Root节点遍历整个对象树,查找出异常对象的分布。如希望查找出当前对象树中,找到最大的组件类型有哪些:
1 | mDistanceVisitor = new RetainedSizeVisitor(); |
1 | /** |
https://developer.android.com/studio/command-line/dumpsys#meminfo
https://cloud.tencent.com/developer/article/1070616
https://cloud.tencent.com/developer/article/1070616
http://gityuan.com/2016/01/02/memory-analysis-command/
https://blog.csdn.net/msf568834002/article/details/78881341
在看JVM内存模型时,不妨先了解一下堆转储文件的协议。通过HPROF的问题格式可以大致了解JVM在内存中的划分情况。
总的来说,HPROF文件分为两个大的部分,分别是Hprof Header和Hprof Body。其中Header部分又包括以下几个部分:
Body部分就是完整的jvm堆信息,将堆上的对象引用状况表述到文件中。其中在表述类对象是静态方法和静态成员变量时稍复杂,详细格式请查看 3.2 ~ 3.4。以下是HPROF文件格式的详细说明:
||||||||||||
| :——– | :——–:| :——: |:——: |:——: |:——: |:——: |
| “JAVA PROFILE 1.0.3/0”(Magic Code) | 4byte mIdSize (储存id的字节长度) | 8byte (Time Stamp) |
| 0x01(string table) | 4 byte | 4 byte(字符串length) | mIdSize byte | strlen(length) byte|
| …… | …… | …… | …… | …… |
| 0x02(class table) | 4 byte | 4 byte (length) | 4byte(class serial number) | mIdSize byte| 4 byte(Stack trace serial number)| 4 byte(class name string id)
| …… | …… | …… | …… | …… |
| 0x04(stack frame) | 4 byte | 4 byte (length) | mIdSize byte | mIdSize byte (methodName string id) | mIdSize byte (methodSignature string id) | mIdSize byte (sourceFile string id ) | mIdSize byte (serial) | mIdSize byte (lineNumber )|
| …… | …… | …… | …… | …… |
| 0x05(stack trace) | 4 byte | 4 byte (length) | 4 byte serialNumber | 4 byte threadSerialNumber | 4 byte numFrames | numFrames * mIdSize byte stack frame id|
| …… | …… | …… | …… | …… |
| 0x0C(HEAP DUMP) | 4 byte | mIdSize byte (length) |
| 0xFF(ROOT_UNKNOWN) | mIdSize byte string id |
| 0x01(ROOT_JNI_GLOBAL) | mIdSize byte string id | mIdSize byte jni global id|
| 0x02(ROOT_JNI_LOCAL) | mIdSize byte string id | 4 byte thread serial number | 4 byte stack frame number |
| 0x03(ROOT_JAVA_FRAME) | mIdSize byte string id | 4 byte thread serial number | 4 byte stack frame number |
| 0x04(ROOT_NATIVE_STACK) | mIdSize byte string id | 4 thread serial number|
| 0x05(ROOT_STICKY_CLASS) | mIdSize byte string id |
| 0x06(ROOT_THREAD_BLOCK) | mIdSize byte string id | 4 thread serial number|
| 0x07(ROOT_MONITOR_USED) | mIdSize byte string id |
| 0x08(ROOT_THREAD_OBJECT) | mIdSize byte string id | 4 byte thread serial number | 4 byte stackSerialNumber |
| 0x20(ROOT_CLASS_DUMP) | mIdSize byte string id |
| 0x21(ROOT_INSTANCE_DUMP) | mIdSize byte string id | mIdSize byte stackId | mIdSize byte class id | 4 byte remaining |
| 0x22(ROOT_OBJECT_ARRAY_DUMP)| mIdSize byte string id | mIdSize byte stack id | num elements | mIdSize byte class id| mIdSize * num elements(skip) |
| 0x23(ROOT_PRIMITIVE_ARRAY_DUMP)| mIdSize byte string id | mIdSize byte stack id | 4 byte num elements | 4 byte primitive type | mIdSize * num elements(skip) |
| 0xC3(ROOT_PRIMITIVE_ARRAY_NODATA)| mIdSize byte string id | mIdSize byte stack id | 4 byte num elements | 4 byte primitive type | 4 * num elements(skip) |
| 0xfe(ROOT_HEAP_DUMP_INFO) | mIdSize byte heap id | mIdSize byte heap name id(string id)
| 0x89(ROOT_INTERNED_STRING)| mIdSize byte string id |
| 0x8a(ROOT_FINALIZING) | mIdSize byte string id |
| 0x8b(ROOT_DEBUGGER) | mIdSize byte string id |
| 0x8c(ROOT_REFERENCE_CLEANUP) | mIdSize byte string id |
| 0x8d(ROOT_VM_INTERNAL) | mIdSize byte string id |
| 0x8e(ROOT_JNI_MONITOR) | mIdSize byte string id | 4 byte thread serial number | 4 byte stack frame number |
| 0x90(ROOT_UNREACHABLE) | mIdSize byte string id|
| 0x1C(HEAP DUMP SEGMENT) | 4 byte | mIdSize byte (length) |
||||
| :——– | :——–:|
| 0x20(ROOT_CLASS_DUMP) | 1 |
| id| 4|
| stack serial number| 4|
| super class id| 4|
| class loader id| 4|
| signeres id| 4|
| protection domain id| 4|
| reserved| 4|
| reserved| 4|
| instance size | 4|
| const pool num entries| 2|
| 2 * num entries |
| static fields num entries | 2|
| static fields | static fields num entries * (static fields),下面会再单独列出来|
| instance fields num entries| 2|
| instance fields| instance fields num entries * (instance fields)
|||||
| :——– | :——–:| :——: |
| 4 | 1 | 4|
| static fields id| static fields type| type size|
||||
| :——– | :——–:|
| 4 | 1|
| instance id| instance type|
如果你仔细研究了上面的内容的话,可能你就会有这样一个问题,HEAP DUMP和HEAP DUMP SEGMENT有什么区别?为什么要有两个标记?
其实堆转储的hprof文件格式中,原本是使用4字节32位存储堆对象的 “HEAP DUMP” (0x0C)的区块长度,但同时也就限制了HEAP DUMP的大小必须在4GB以内。在出现这个问题的情况下,在HPROF文件中新增了”HEAP DUMP SEGMENT” (0x1C)的格式,用来将超过4GB的JVM堆对象信息分别存储到文件的多个区块中。
Be Careful with HPROF Heapdumps Bigger than 4GB @2013-04-16
程序计数器
在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。
Java虚拟机栈
线程私有,每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈
线程私有,本地方法栈的功能和特点类似于虚拟机栈,不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。我们常见的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈。
Java堆
所有线程共享的内存区域,所有对象实例及数组都要在堆上分配内存。
方法区
所有线程共享的内存区域,为了区分堆,又被称为非堆。用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
在谈到JVM内存结构时,从HPROF文件的header和body部分就有体现,从数据上划分结构就可以分为方法区(包括运行时常量池,也就是header中string table的部分)、java堆(body部分)、虚拟机栈、本地方法栈和程序计数器。其中方法区和java堆部分是所有线程共享的数据区,其他则为线程独有的数据区。为什么会有这样的结构划分和设计?
先看设计内存的目的,是因为随着CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存。但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存来缓解这种症状。基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存。回到JVM的内存模型中, Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理。这样也就说的通了,为啥在JVM中有的区域是线程共享的,有的区域是线程独享的。
垃圾回收(Garbage Collection,简称GC),是垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。这种机制也不单单只有Java虚拟机才有,ObjectC和C#都有自己相应的垃圾回收机制,垃圾回收机制是帮助程序员自动管理对象内存空间的机制。
常见的垃圾回收方式,引用计数算法和根搜索算法。
引用计数就像是每个对象有个账本,当一个对象被另一个对象引用时该对象的引用计数器+1,当引用失效时引用计数器-1,任何引用计数为0的对象可以被当作垃圾收集。引用计数有一个先天的缺陷,那就是多个对象相互持有引用形成一个引用环是,那么环中的所有对象引用计数都不为0,这时这些对象都不能被垃圾回收器回收。
根节点搜索指的是从根节点集合出发找到所有引用链可达对象,当一个对象到根节点集合(GC ROOTS)没有任何引用链存在时就证明此对象是不可用的。从对比JVM规范的垃圾回收根节点(来自:Garbage Collection Roots)、HPROF文件协议中的GC ROOTS tag类型和square haha库源码中对GC ROOTS的类型定义,参照下表。
|JVM规范名称| HPROF中的TAG | haha库中的RootType枚举类型 | 描述 |
| :——– | :——–:| :——: |:——: |:——: |:——: |:——: |
| System Class | 0x05 | RootType.SYSTEM_CLASS | 被bootstrap/system class加载器加载的类,例如所有rt.jar中包名为 java.util.*的类 |
| JNI Local | 0x02 | RootType.NATIVE_LOCAL | native代码中的本地变量,例如user defined JNI code or JVM internal code |
| JNI Global | 0x01 | RootType.NATIVE_STATIC | native中的全局变量,例如user defined JNI code or JVM internal code |
| Thread Block | 0x06 | RootType.THREAD_BLOCK | |
| Thread | | | |
| Busy Monitor | 0x07 | RootType.BUSY_MONITOR | 所有调用 wait()、 notify()方法的, 或者同步的。For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object. |
| Java Local | 0x03 | RootType.JAVA_LOCAL | 本地变量,例如线程栈帧中的参数和方法 |
| Native Stack | 0x04 | RootType.NATIVE_STACK | In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection |
| Finalizable | 0x8a | RootType.FINALIZING | 在finalizer等待队列里的对象 |
| Unfinalized | | | |
| Unreachable | 0x90 | RootType.UNREACHABLE | 从其他根节点都无法到达的对象 |
| Java Stack Frame | 0x03 | RootType.JAVA_LOCAL | A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects. |
| Unknown | 0xff | RootType.UNKNOWN | |
| | 0x89 | RootType.INTERNED_STRING | |
| | 0x8b | RootType.DEBUGGER | |
| | 0x8c | RootType.REFERENCE_CLEANUP | |
| | 0x8d | RootType.VM_INTERNAL | |
| | 0x8e | RootType.NATIVE_MONITOR | |
对照上面这个表总结起来,所谓JVM GC Roots是进行垃圾回收时根节点的集合。大致包含以下几个方面:
按照根搜索GC的思想,从根节点出发的找到的对象就被认定为存活的,其他的对象都是“无用的”,但是GC ROOTS的集合不应该是一成不变的,特别是面对分代GC时。为啥这样说呢?分代GC是一种部分收集(partial collection)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC roots的一部分。具体到分两代的分代式GC来说,如果第0代叫做young gen,第1代叫做old gen,那么如果有minor GC / young GC只收集young gen里的垃圾,则young gen属于“收集部分”,而old gen属于“非收集部分”,那么从old gen指向young gen的引用就必须作为minor GC / young GC的GC ROOTS的一部分。所以针对一次GC来说,GC ROOTS的类型类型范围未必只有jvm规范定义中所列举的那几种情况。
Android Q开始google才为ART虚拟机添加分代收集机制。
JVM内存模型与GC算法
Android Hprof 协议
java的gc为什么要分代?RednaxelaFX的回答
Android Q Beta 正式发布 | 精于形,安于内
https://www.cnblogs.com/dingyingsi/p/3760447.html
使用原生提供的ShapeDrawable实现背景。
1 | <shape xmlns:android="http://schemas.android.com/apk/res/android"> |
在介绍“贴图”之前先说明在Android绘图相关的两个必备知识,分布是Paint Style和Path。
在用画笔(Paint)的时候有三种Style,选择不同的画笔样式时就可达到不同的画笔效果,分别是
当我们在想要绘制一些形状时,Canvas提供了一些基础形状的绘制方法,如圆形、矩形、椭圆等。你只需要选择相应的绘制方法并设置你想要的绘制参数就能绘制出你想要的简单图形效果。但对于那些复杂一点的图形则没法去绘制,如一个心形、正多边形、五角星等,使用Path不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。Path封装了由直线和曲线(二次,三次贝塞尔曲线等)构成的几何路径。你能用Canvas中的drawPath来把这条路径画出来(同样支持Paint的不同绘制模式),也可以用于剪裁画布和根据路径绘制文字。
以左上角贴图实现为例,使用Path约束绘图范围
1 | RectF fakeCornerRectF = sRectF; |
使用Paint.Style.FILL画笔绘制,将贴图效果绘制在ImageView容器的(0,0)坐标上。即可达到想要的圆角效果
例如使用android support包里的RoundedBitmapDrawable,创建一个被裁剪圆角的BitmapDrawable。
1 | RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap); |
1 |
|
UI 控件的elevation属性可以设置其高度,呈现在界面中的直观效果就是阴影效果,在 xml 布局文件中,通过 android:elevation 属性设置,在 java 代码中通过 View 类提供的setElevation()方法设置。但是这个属性存在版本兼容问题,是 Android 5.0 引进的 API。所以,当 minSdkVersion 值小于21时,系统会在 xml 的对应使用地方给出一个 lint 提示:
Attribute elevation is only used in API level 21 and higher
当然你也可以选择忽略这个提示,或者使用tools:targetApi属性消除这个提示,这样做的话,在低于5.0版本的系统中将不会出现阴影效果。然而,有一个更好的办法做到兼容,那就是借助ViewCompat这个万能的兼容类,使View 的 elevation 属性兼容至低版本中:
ViewCompat.setElevation(View view, float elevation)
注意:尤其要注意,视图的阴影一定是由有轮廓的视图投射出来的。简单来说,就是需要设置控件的背景,即 android:background 属性。我们可以选择图片作为背景,也可以使用
说到阴影效果最简单最省力的方法莫过于设置一个.9的背景图啦!这里推荐一个站点,可以在线制作.9阴影图。http://inloop.github.io/shadow4android/
先看想要做到的阴影效果,想要在红色的轮播banner下方显示一条红色的阴影效果。用已知的阴影方案比如设置视图的Z轴高度或者设置.9阴影背景都无法实现这种效果。
可以将实现上图的局部阴影效果的绘制步骤分解成两层:
这里有个前提,需要关闭当前View的硬件加速功能。setLayerType(LAYER_TYPE_SOFTWARE, null)
。具体的实现代码如下所示:
1 |
|
https://yifeng.studio/2017/02/26/android-elevation-and-shadow/
]]>
- 应用进程自身引起的,例如:主线程阻塞、挂起、死循环
- 应用进程的其他线程的CPU占用率高,使得主线程无法抢占到CPU时间片
常见的三种ANR类型:
BROADCAST_FG_TIMEOUT
和BROADCAST_BG_TIMEOUT
。SERVICE_TIMEOUT
和SERVICE_BACKGROUND_TIMEOUT
。先看下面的错误实例:
1 |
|
在子线程保存数据到文件或数据库(这里用sleep操作模拟耗时io操作),如果同时可能涉及到在主线程操作同一个锁对象的情况在,这时你是否会习惯的使用synchronized关键词保证list的同步呢?当在主线程和异步线程产生了对相同对象的竞争关系,那这时就很容易出现主线程的阻塞,而阻塞的时间长短就取决于主线程啥时候获取到竞争对象。而此时反馈在系统层面当用户操作不会得到响应,最终应用以ANR的形式退出。运行上面的错误代码你会获取类似下面的ANR日志信息Input dispatching timed out (Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.)
查看虚拟机trace文件输出目录adb shell getprop dalvik.vm.stack-trace-file
,再查看手机目录下/data/anr/traces.txt
的文件内容,会发现main线程在等待释放锁<0x0af98db2>,而这个锁正在被thread 10所持有
再查看tid=10的线程的线程状态,该线程正在sleeping,这也印证了上面实例的代码,主线程在等待一个sleep线程释放锁而导致了ANR。当然在实际项目中的日志和原因未必会这么明显,但形如实例的错误代码确实是很常见的场景。
参考http://gityuan.com/2017/01/01/input-anr/的input-anr异常原因的总结,input anr主要分为以下几类。
所以如2.1中实例代码,当ANR发生在Activity的onCreate流程中时,你讲看到无窗口, 有应用
的日志信息,当ANR发生在对某个View的OnClickListener中时,你将从日志中获取事件等待队列不为空且头事件分发超时500ms
的信息,这样通过不同的日志信息就可大致定位ANR出现的用户场景,进而方便定位出问题代码。
http://yuanfentiank789.github.io/2017/09/05/ANR%E5%88%86%E6%9E%90/
http://gityuan.com/2017/01/01/input-anr/
https://maoao530.github.io/2017/02/21/anr-analyse/
http://rayleeya.iteye.com/blog/1955657
xpath是移动端定义可操作区域的数字标识,是用来标识可操作的控件的。既然想要通过一串字符标识在移动端app中的可操作控件,那么xpath的生成规则需要满足以下几个原则:
在满足上述xpath原则的基础上,可通过以下几个参数作为组成XPath的生成参数:
综上所述就可以得到以下的XPath生成方式。
$ xpath = Md5(url+root path + resource name)$
参考 https://www.jianshu.com/p/69ce01e15042
在用户产生用户点击数据的过程中,埋点系统上报控件xpath给埋点后台,但是在实际的数据分析过程中需要知道每个xpath对应的控件和区块名称具体是什么。在每个电商系统中针对埋点都会有一套自己的位置模型规范,简称SPM(super position model)。所以针对最终的数据分析和统计需要一个xpath到spm的映射关系,将实际产生的xpath埋点数据转换成业务中的SPM。实现方式也很简单,只需要在开发版本中提供一个编辑模式,将移动端本地生成的xpath通过一个编辑工具转换成SPM数据就可以了,简单的效果图如下所示:
在考虑记录页面跳转的方案时,可能最先想到的是在通过统一路由跳转的方式跟踪所有页面跳转,但是实际的项目中首先你得有个统一的路由不是?在面对已有的项目代码从工作量和效果上来说这种方式都不是最佳方案。即便是已有统一的路由方案也很难保证没有错埋漏埋的情况存在。别忘记 ActivityLifecycleCallbacks
1 | public class RuntimeActivityCallbacks implements Application.ActivityLifecycleCallbacks { |
《Android插件化之Small框架实践总结》
《Android插件化之Small框架原理》
《Android插件化之资源加载机制》
安卓的动态化发布
动态化就像是天赋,有些人天生就有的能力却是需要你花非常大精力也未必能获取到的。前端的开发应该从来不需要动态化的方案吧!插件化方案的一个最大作用应该就是绕开应用市场的审核周期,尽可能的像后端Java或者前端JS一样,随时发布随时生效。
真正意义上的模块解耦
插件化的方式让模块与模块之间在开发方式上真正的隔离,达到了解耦的目标。而这在之前android原生的开发方式上是很难达到的。
dex 65535问题
插件化的方案也可以看做一个dex分包和资源分包的方案。
在《Android插件化之Small框架实践总结》 中对比了Small插件化框架的优缺点,选择Small最重要的原因还是轻量化,当然很多问题也是轻量化带来的。
兼容性问题
兼容性问题大致分布在以下几个方面:
插件化框架带来的稳定性问题
在团队技术选型上优先考虑的应该是和现有业务适配吧。如果你所面对的业务不存在快速迭代频繁发布的需求,插件化框架的威力可能就要减小一半了。同时在选择个人开发者维护的开源项目时,依然是要考虑到其架构的稳定性和bug的修复时效。因为一旦在一个商业化的项目中使用开源框架,稳定性和兼容性一定会放在首要位置的。在这一点上,可能从大公司孵化出的开源项目会更有优势。
在回看插件化框架对android应用中模块解耦的贡献,对比android应用的原生的开发方式。能够从插件化框架吸收的模块解耦方法上可以看到大致这几个方面。
模块间的解耦要依赖工具而不是约定开发规范
以2个业务app模块为例,应该有一个工具存在避免这两个模块产生耦合关系,如果一旦存在耦合关系就可能编译报错。而不是靠开发者约定的开发规范。因为规范是可以不遵守的。这是同层级间的耦合管理。
避免依赖传递
这是不同层级的模块间耦合问题。implementation关键词就可以解决这个问题。
路由
相比较插件化框架你可能更需要一个页面路由工具,业界有很多业界路由框架,比如阿里的ARouter,路由用来解决模块解耦带来的页面跳转问题,所以自己动手实现一个简单的路由工具也不是很难。
可以查看google文档上对Android Studio 3.0后新引入的依赖配置的差异说明。
https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration
简单来说google将compile配置拆成了两个关键词,分别是api和implementation。api和implementation的差异不单单是编译效率的差异,我觉得更重要的是,implementation避免了依赖的传递。以下图对比api和implementation的差异可以发现,当使用implementation配置依赖时,app模块将不会直接依赖D
模块。在使用老的依赖配置compile
时,实际上并没有做到模块的分层,最底下的模块依然可以被最上层的模块依赖,实际的依赖规则在开发者的规范里,而不存在项目模块的管理中。
以上就是我对插件化框架的看法,如果你的业务存在开始迭代频繁发布的情况存在,那么你可能就很需要一个插件化框架来带来开发方式上的改变,核心在于提高了研发效率。但是在使用插件化项目之前你需要对插件化框架的边界和扩展边界的成本有一个清晰的认识。实际上上面所说的所有插件化框架带来的问题都是可以解决的。问题的关键在于在你的团队这样的付出和产出是否值得呢?相信每个人都可能会有不同的看法。
当然开源的插件化框架依然是重点学习的方向,理解不同插件化方案的实现原理是深入理解安卓系统很好的切入点。接下来我还会花更多的时间深入的学习360和阿里巴巴的插件化方案。
最近在读《邓小平改变中国》这本书,让我对从文革结束到改革开放前那段对大多数中国人讳莫如深的历史有了大致的了解。“实践是检验真理的唯一标准”,这句很多80后90后耳熟能详的口号背后,原来发生了这么多的波折,冲破了如此多的障碍。理论与实践的统一是马克思主义的一个最基本的原则。实事求是是毛泽东思想的精髓。听起来虚头巴脑,但确实应该是解决问题和对待未知事物的核心方法论。对!这是一篇技术总结。
HttpUrlConnection
,HttpClient
或者okHttp
,开发者都必须在此自身业务场景的基础上进行api的二次封装。一个功能强大且易用的网络框架不仅仅能够提高开发效率,起到事半功倍的效果,还能起到规范业务开发结果的作用。发送Http get请求的示例代码:
1 | // Instantiate the RequestQueue. |
查看以上Volley使用的官方示例,可以将其划分为以下几个使用步骤:
Response.Listener
通过volley的使用方法你就可以大致猜测volley在完成一个网络请求的大致过程。开发者使用请求相关信息和接收返回结果的Callback
封装成一个Request
,并将其放在请求队列中,在请求队列的背后一定有负责真正网络请求任务的线程从队列中消费网络请求的Request
,在获取网络请求结果后通过线程间消息机制将网络请求的结果在主线程返回给接受消息的Response.Listener
。当然真正的过程肯定会更复杂。但是不管怎样,volley的设计思路是一个经典的生产者消费者模式。
从一个Volley api使用者的视角回头再看一下volley,你是否有这些疑问?
Request
的方式不友好。以Get请求为例,需要自己手动将请求参数拼接到请求url中。如果是POST请求情况还会更复杂。Response.Listener
对象接收请求让代码不够美观不说,更建立了请求者和请求框架之间的强耦合关系。以上面的示例代码为例,如果这段代码写在Activity中,构造的Response.Listener
的匿名内部类存在当前Activity的隐式引用,很容易引起不必要的内存泄露。如果这段代码不在Activity中,那还要多一层数据的轮转机制。简直是开发者的噩梦。发送Http post请求的示例代码:
1 | //创建网络处理的对象 |
相比较volley,okhttp在请求api的设计上更合理些。okhttp使用RequestBuilder和BodyBuilder构造网络请求,并且隐藏了网络框架的内部实现,让请求的过程更简单优雅。但在接收请求结果上依然选择Callback
监听的方式。
目前android平台上主流的网络请求都是基于OkHttp框架的,okhttp框架针对http协议进行了封装和优化,支持http/2协议,共享连接池的设计有利于提高请求效率,拦截器的设计支持监视、重写、和重试等特殊业务场景的需求。极大地降低了开发者的使用成本,同时兼备稳定性和可扩展性。所以我们在分析volley和okhttp在实际业务中的不足和优势,选择使用okhttp进行网络请求和连接,在其基础上进行业务封装,力求设计出尽可能的符合自身业务需求和场景的网络框架。具体使用设计需求如下:
String
还是JSON
,不管是Java对象还是文件,都能简单的获取。结合以上几点对网络框架的期望,所以诞生了以下实例的API:
1 | @Test |
API说明:
RequestBuilder.obtain
从对象池中返回复用的Request对象.get()
.postJson()
.postFormEncode()
postStream(Binary binary)
等方法封装了常见的get与post请求并指定了不同的body格式addParam(key, value)
方法添加请求参数,这里并不因为请求方法的不同而存在api上的差异onGetMethodSuccess()
和 onGetMethodFailed()
方便被NetworkCallback
注解标记,并且通过BasicRequest.into(this, "getMethod", 1, 1L, (short) 1, false, 1D, 1f)
绑定了请求与回调方法之间的关系。其中into的方法参数一次为:包含改回调方法的对象,注解的名称,以及Callback方法需要的其他额外参数。buildJsonRequest
将返回结果自动转换成ShopInfo
对象。Http缓存策略是一个相对复杂的问题,大致分为以下三个方面:
决定Http的相应内容是否可缓存在客户端。Http响应头中的Cache-Control
字段,分为Public
、Private
、no-cache
、max-age
、no-store
5种类型。其中前4个都会缓存文件数据(关于 no-cache 应理解为“不建议使用本地缓存”,其仍然会缓存数据到本地),后者 no-store 则不会在客户端缓存任何响应数据。
决定客户端是否可直接从本地缓存数据中加载数据并展示,否则就发请求到服务端获取。Http响应头中的Expires
字段指明了缓存数据有效的绝对时间,告诉客户端到了这个时间点后该本地缓存就该作废了。这里的作废是指客户端不能直接再从本地读取缓存,需要再发一次请求到服务端去确认。确认下这个缓存还有没有用。这个过程就要说到下面的缓存对比策略。
决定客户端本地的缓存数据是否仍然有效。客户端检测到数据过期或浏览器刷新后,往往会重新发起一个 http 请求到服务器,服务器此时并不急于返回数据,而是看请求头有没有带标识( If-Modified-Since、If-None-Match)过来,如果判断标识仍然有效,则返回304告诉客户端取本地缓存数据来用即可(这里要注意的是你必须要在首次响应时输出相应的头信息(Last-Modified、ETags)到客户端)。
根据上述的三种缓存策略,这里贴出客户端对http缓存控制的关键代码:
1 | /** |
如何确定缓存时间的关键流程:
首先需要定义一个CommonResponse,所有的JSON格式的Response都继承它。按照服务端低响应状态的约定,当status为false
时表示请求结果失败,这里说的失败指的是无法返回客户端预期的正确业务结果。
1 | public class CommonResponse implements Serializable { |
当网络出现异常导致的连接失败时,或者当服务端返回的数据无法正常序列化为指定的类的实例时,或者CommonResponse的status变量为false时,网络框架都会抛出一个可被自动捕获的Throwable
,并将返回值和错误类型回调到标记为@NetworkCallback(type = ResponseType.FAILED)
的对应方法中。
因为篇幅的原因,暂时只针对上述的几个方面较的阐述了一个网络框架的设计思路,还包括但不仅限于下面的这些讨论方向,有机会再详聊!如:
线程的切换
对响应数据完整性的校验
Zip文件的请求与自动解压缩,bspatch算法的增量文件请求并根据增量文件自动生成全量文件等
网络模块与缓存模块的解耦设计
网络性能的监控
httpDNS方案的应用
cookie的管理(可参考github开源项目nohttp
项目的设计)
在实际的业务开发过程中,针对上述问题的设计和封装已经能够覆盖大部分复杂的业务场景。相信一定能让一个开发人员写出赏心悦目自嗨的代码了。
https://tech.youzan.com/android_http/
https://mp.weixin.qq.com/s/qOMO0LIdA47j3RjhbCWUEQ
https://www.cnblogs.com/chenqf/p/6386163.html
http://blog.csdn.net/yaofeiNO1/article/details/54428021
http://blog.csdn.net/qmickecs/article/details/73696954
http://blog.csdn.net/qmickecs/article/details/73822619
所谓的”无痕埋点”,其实就是通过技术手段,无差别的记录用户在产品中的行为,当有一天突然想对某一个控件做点击分析时,不再需要开发手动添加数据采集信息。因为从部署埋点方案的时候,就一直在收集所有的用户的数据了。用户的数据并不是分析需求产生的那一刻才有的。无痕埋点在无差别的记录用户所有行为,而实际的埋点结果产出取决于BI工程师对无痕埋点数据的清洗。
从技术角度总结以往埋点项目结果产出困难的原因,这些也是无痕埋点方案想要解决的核心问题。
所以,在项目上使用无痕埋点来解决项目链路过长的问题的方法是将埋点数据的产生过程分成两个并行的部分。将数据的采集过程前置,不再依赖运营和产品经理的需求产出。现有全量用户数据,再由运营和产品分析数据产出结果。
移动端埋点的方案以在Android系统上实现为例,关键在于解决2个问题:
- 如何统一标识控件
- 统一拦截用户操作行为。这里说的用户操作主要还是用户的单击事件。
为了自动生成事件标识,我们需要获取每个控件自身的ID、类名以及位于所属父组件的Index等特征信息,并逐级向上遍历找到根节点。约定控件标识的生成规则为 /root/ClassName:id/ClassName:id
,以某一个业务界面的扫一扫
按钮为例,寻找它到root节点的控件路径,它的控件标识字符串应该是 /root/RelativeLayout:-1/FrameLayout:1997209645/RelativeLayout:-1/RelativeLayout:1997209836/LinearLayout:1997209965/TextView:1997209967
,并通过md5算法将这个标识字符串生成为XPATH
就是我们想要的控件标识。当后台上报数据中包含1e3cdc9499fac8088220756a46c85599
的点击时。我们就认为是扫一扫
控件被点击了。
还是以Android端上的实现为例,如何统一拦截用户的单击事件呢?先看如何实现一个单击操作的响应,在Android上一般的做法是针对View设置一个单击的监听。
1 | /** |
那么有没有办法统一把所有的单击事件都替换掉呢?在运行时!业务开发的工程师还是按照原生Android系统的api实现对单击事件的响应,埋点SDK在运行时统一替换所有的View.OnClickListener
,将其替换成原有View.OnClickListener
的包装。当执行AutoTraceListenerWrapper
的时候实际执行的还是原有Listener
的单击事件,同时又能统一拦截所有的单击事件。
1 | public class AutoTraceListenerWrapper implements View.OnClickListener { |
那么如何统一替换所有的View的单击监听呢?只需要遍历安卓视图结构的View Tree,使用反射机制替换掉所有的原生OnClickListener
就可以了。
埋点问题是不能通过一个方案适配所有业务场景的,不同的场景下需要选择不同的埋点方案。无痕埋点方案针对的是用户的简单用户行为事件,比如如何规范采集用户的点击事件。例如针对页面跳转的事件统计还是需要你单独埋点。埋点数据能不能最终完美的呈现依赖的不单单是技术方案,还有更多的是数据规范的问题。例如在面对运行时才能获知的业务信息时,如何使用埋点技术将需要的业务信息做统一的上报,是另一个重要的关键点。例如在做数据清洗时如何建立规范的数据中间表?无痕埋点只是获取完整埋点数据的第一步。