临近年末,不管这一年过得如何,都有这么一个习惯,就是文字来记录下这一年发生的点点滴滴和令人印象深刻的事件,同样也是对年初设立的目标是否达成进行自我评分,无论有没有做到,都是自我一段宝贵经历。
2020年注定是一个不寻常的年份,甚至往后10年来看,你或许会发现很多事件改变或习惯养成都是从这一年开始,因为今年发生了什么,想必大家都知道,疫情的影响有些可能是不可逆的。
对自己来说,今年是习惯被重塑的一年,为何这么说,主要有两个方面冲击,一个是外部冲击,一个是内部冲击。所谓外部冲击,就是在疫情下的习惯养成,以前出门从来不需要带口罩,现在出门的话,如果没有个口罩,内心会产生焦虑和不安,因为出入一些公众场合,没有口罩就很难正常进出。
还有外部环境的变化,之前经常参与一些的社交活动也暂停了,日常跟人沟通交流也相对减少,对于他人发生的变化也不敏感,更多是开始关注自我,如何进行自我提升,更多是从自我已有的内容去挖掘。
同样对于内部冲击来说,更多地关注的是自我改变。我们都渴望内心不断地成长,心智变得更加成熟,思考问题更加全面点,这些点点滴滴地变化其实都是从每个人的日常习惯中形成。
比如在2020年初我设立的一个目标,是每天晚11点半入睡,早6点半起床,刻意打造良好习惯,后来在中途变成了,每天晚11点入睡,早7点起床。基于这个目标愿景是希望自己能养成一个良好的生活作息习惯,其实不然,现在回顾来看,发现每天11点入睡对我来说太难,经常因为其他忙碌事情导致晚睡,一般都是12点以后才睡,同样想对于早起来说,从要求自己是从6点起来,变成7点起来,然后是现在基本每天8点左右才起来。
所以说这个目标是彻底没完成,你说很难吗,不见得,那为何做不到呢,可能是过去几年生活习惯惯性导致,或者说没碰到一件真正有刺激的事情导致去改变。因此对于明年来说,自己对于生活作息习惯这方面,更多是寻找适合自己生活习惯类型,不再强制自己一定要在某个时间段睡觉或起来,寻找到适合自己的精力旺盛有效时刻为主。
对于用「学习」二字来说,个人觉得今年其实无时无刻在学习,因为每天都会看大量的文字,跟同事或朋友沟通交流,自我实践总结所得,这些动作基本每天都在重复在做,但是是否真正有消化,有吸收,有改进,那就不见得了。
我想说的「学习」是自我吸收后的输入,个人喜欢用文字输出,来表达自我学习后的感悟和总结。比如,阅读后写读书笔记是我常常会执行的动作,今年在前面1-2个月做的比较好,基本会在看完一些书之后及时进行总结输出,但是到了后面则没有再执行下去了。
为何又没做到呢,现在回顾起来,发现上半年时间周末基本都是弄房子装修的问题。基本每个周末都会去跟进下装修进度,这种其实是很耗时间和精力,比如周末弄点东西,都需要人在场,这就导致你感觉没周末都在弄装修的事情,没完没了。导致平时周六基本都会写写文章总结本周学习输入是什么情况下,在耗完精力之后,根本没心情去做输出。
可能也是因为在2019年连续完成每天写作日更之后,一种懒惰的表现,想想自己曾经也完成过连续一年每天都在写作,那我是否可以连续一段时间休息下不写呢,也有这种心理在作怪。
虽然没有把每周写作内容输出在微信公众号里,但是一些阅读笔记还是记录下来,只不过没有进行编辑整理,公开发布出来而已。要说完全没有收获,也不见得,日常生活中思考问题的角度和分析事情背景等等,会应用一些之前学到的方法论来判断。
这个目标就有意思了,从我的年度目标回顾来看,进行了三次变化。比如持续经营知识星球专栏,加强反思和检视的流程,因为2019年已经经营了一年的知识星球专栏,虽然经营的惨淡,没几个人关注,但好歹也是持续输出分享的积累结果,因此想持续经营,主要目的就是对于日常感悟和反思总结可以让更多人看到,自身也可以回顾等等,但是现实情况是惨淡经营。
后来一段时间内发现微信视频号崛起,是个机会点,可能是那段时间很多人在分享视频号,并且相比抖音,视频号刚起来,对于素人来说也是不错的一个增长点,本身自己也有一些内容可以分享,因此也做了相关尝试,结果就是尝试不理想。
或许是自己如何运营视频号的一些方法论和内容有问题,并且没有坚持和过多尝试等等,只是偿了个新鲜感而已。
后来在11月份时候,又把探索目标改变成制作时间管理课程,还好这个目标确实完成了,目前已经完成了相关PPT内容,后续就是把课程给录制出来就好。为何又想去探索录制课程呢,也是因为想进一步挖掘自己优势有什么,能给他人带来什么样的价值所在。
这些尝试,虽然惨淡,或者经营失败,又或许尝试其他内容等等,个人觉得这样的经历还是宝贵的,因为起码自己努力去做探索,主要是探索自己的个人价值所在,探索自身终生事业方向所在,也是进一步让自身活的更有意义感。
今年也是给自己写年终总结的第八年了,翻看过去几年所写的年度总结,会明显感觉自己过去几年经历了什么,收获有什么,哪些地方发生明显变化,哪些地方有遗憾的,哪些地方需要改进的,但是到现在却一直没有改进,这些记录能形成一个你自己独特的成长轨迹。
所以,还是希望大家在年末这几天,抽点时间静下心来,回顾和复盘一下这一年在自己身上发生的人和事,挑几个重点记录下来,多年以后再回头来看,发现曾经的自己是那么可爱。
在2020年初的时候,我把2019年「写作日更」总结出一个成长小册,面对即将过去的2020年,本身输出的内容则不多,但是也会整理一个成长小册出来,把今年自身感受比较深刻的内容整理出来,并与大家分享,敬请期待!
年终复盘很有必要,就是看看自己哪里表现的好「得瑟下」,表现的不好,督促自己去完善,因为一切都是为了遇见更美好的将来而努力着。
2021年,聚焦「组建家庭的突破」主题
]]>1,GIF格式。5秒的动画,一张图大小可能就会达到5-10M,然后UI那边制作背景需要透明的效果做不了,打包下载压缩包所需要更多的流量。
2,帧动画。简单说就是把GIF图片给拆开为一张张图,比如一秒20帧的GIF图被拆开为20张静态图,然后用程序代码组成一帧一帧渲染效果动画,但是缺点也是很明显,做不到动态更新,只能提前集成在本地资源中,这个方案也被否决掉。
3,第三方动画渲染库。比如基于Airbnb开源的lottie库和YY出品的SVGA解析库,lottie解析格式是以后缀为.json文件,相比GIF文件,大小是小10倍以上,但是在CPU占用上却奇高无比。因为我们的项目针对没有GPU能力的车机系统,车机上的内置芯片性能比目前主流手机性能差很多。同样SVGA库也是因为CPU占用率高的问题被否决掉。
基于目前已有的硬件条件,可能最希望是升级硬件设备,那样的话无论是对于UI和开发来说,都是皆大欢喜,UI可基于lottie做炫酷的动效,而开发也不会因为性能问题而进行各种评估。但现实往往是残酷的,只能基于目前车机条件进行开发,那么作为开发人员,当然是得想各种方法去满足产品需求了,那就把目光转移,后来转移到一种叫做「WebP」格式的图片。
基于WebP格式做出来的图片,UI那边可以做透明的背景动效,我们开发这边测了下性能,发现CPU和内存占用也满足产品测的要求,正好折中是我们想要选择的解决方案。既然之前是没怎么听过,那么就有必须去了解下「WebP」是什么东西了。
对于之前没接触过的知识点,首先第一步是打Google,输入webp这四个字母,Google搜索出来的首页就会告诉你这是什么了,也就是What的定义。引用「WebP」官网定义的一句话:
WebP is a modern image format that provides superior lossless and lossy compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster.
进一步说,「WebP」是一种新的图片格式,可提供出色的无损和有损压缩,对于Web开发来说,可以创建更小和更丰富的图像。根据官网测试,WebP无损压缩的图片比PNG格式图片,文件大小上少 26%,WebP有损图片在同样 SSIM 质量指标上比JPEG格式图片少25~34%,SSIM是一种衡量两张数字影像相似的指标。
官网给出有损压缩测试方法:
对比图如下:
同样WebP与JPG格式进行加载时间对比,可以发现WebP优秀很多。
从图中可以看到大小和图片加载速度上比jpg格式优胜很多,对于web页面来说,文件体积减少了,加载时间缩短了,那么页面的渲染速度加快了,特别是图片越来越多的情况下,能对性能进行提升和带宽节省。
对于项目中要用到各种动效图片,大部分人首先想到是GIF格式的图片,那么相比GIF,WebP有什么优势呢?
对于Android系统来说,WebP 在Android 4.0及以上原生支持,对于4.0以下可以使用官方提供提供的编解码库,但现在主流的手机上,Android 4.0以下已经可以忽略不计了,反而对于在IOT设备上,则有可能存在低版本,因此对于此类开发项目,如果选择WebP格式则需要事先评估下了。
从官网的描述来看,WebP是使用VP8关键帧编码以有损方式进行图像数据压缩,也就是说如果要支持解码的话,我们需要对这个VP8算法进行解码。WebP容器,也就是WebP的RIFF容器是支持在WebP的基本用例的功能。
WebP文件格式基于RIFF(资源交换文件格式)文档格式。具体格式定义如下:
1 | 0 1 2 3 |
RIFF文件的基本元素是一个块。它包括了Chunk FourCC 、 Chunk Size、 Chunk Payload三部分 。其中Chunk FourCC是一个32位ASCII编码的块文件的唯一标识。 Chunk Size则代表该块文件的大小, Chunk Payload则是数据有效承载,如果“块大小”为奇数,则添加一个填充字节(应为0)。
我们常用ChunkHeader('ABCD')来描述RIFF文件,这里ABCD则是FourCC单个块,则该元素大小为8个字节。
那么接下去看WebP文件头,具体格式如下:
1 | 0 1 2 3 |
1,'RIFF': 32 bits:32位 ASCII字符“ R”,“ I”,“ F”,“ F”。
2,文件大小,32位,从偏移量8开始的文件大小,以字节为单位。此字段的最大值为2 ^ 32减去10个字节,因此,整个文件的大小最多为4GiB减去2个字节。
3,'WEBP': 32 bits:ASCII字符“ W”,“ E”,“ B”,“ P”。
那么对于包含多帧动画为主的图片,它的头文件如何呢,具体如下:
1 | 0 1 2 3 |
Background Color:画布的默认背景颜色,以[B,G,R,Alpha]字节顺序排列,此颜色可用于填充框架周围画布上未使用的空间,以及第一帧的透明像素。处置方法为1时也使用背景色。
Loop Count:循环播放动画的次数。 0表示无限循环。
除了这几个文件头格式之外,还有其他几个文件头格式,比如VP8X、VP8、VP8L、ANMF、ICCP等,具体格式可以在 Extended File Format 查看。基于Android系统的话,主要是以VP8X、VP8、VP8算法解码,对块文件进行解析,代码如下:
1 | static BaseChunk parseChunk(WebPReader reader) throws IOException { |
在对算法解码之前,需要把WebP格式文件加载到内存中去,此时就需要用到Reader这个读写器,我们从官网的定义可以看到,读取WebP文件的代码称为读取器,而写入WebP文件的代码称为写入器。那么这个涉及到文件I/O的读写,数据流的读取和写入问题。
具体定义读取器的接口代码如下:
1 | public interface Reader { |
具体文件读取可以从文件、字节流等地方获取。读取数据之后,就需要对数据进行解析,我们知道如果是动画效果的图片,本质是以帧集合组成的内容,无论是GIF图支持WebP格式的动画图,本质也是一帧一帧进行渲染。好比我们看到的Android渲染视图是以一秒60帧,所以我们看到如果每帧超过16ms的话,就容易引起卡顿的原因。
因此对于帧渲染接口的定义就显得很关键了,具体接口定义如下:
1 | public abstract class Frame<R extends Reader, W extends Writer> { |
一帧可以理解为一张静态图,如果有20帧组成的动画,可以理解成有20张图片按照连贯顺序一张张过一遍,那就形成了有动画的效果。所以我们要解析动画,本质是还是去解析每张静态图,通过每张图的绘制,把整个动画给绘制出来。这一张图片就包括宽度、高度、在屏幕上的横向、纵向坐标、运行时间等,但最关键还是需要把图会绘制出来,这里面就是draw方法的重写。
关于draw方法重载,还是以绘制图片为主,具体代码如下:
1 | public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, WebPWriter writer) { |
我们知道Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。
那么该高效地加载Bitmap呢,其实核心思也很简单,就是采用BitmapFactory.Options来加载所需尺寸的图片。主要是用到它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小,当inSampleSize大于1时,比如为2,那么采样后的图片其宽/宽均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。从最新官方文档中指出,inSampleSize的取值应该是2的指数,比如1、2、4、8、16等等。
通过采样率即可有效地加载图片,那么到底如何获取采样率呢,获取采样率也很简单,循序如下流程:
你看设计到最后,本质还是把由很多帧组成的动画格式,拆分到具体每一帧的图片,针对图片进行图片帧绘制,进而把动画的效果给渲染出来。
总的来说,不同图片显示选择是根据具体业务场景来做评估,像我们最近在开发的项目中,主要是以图片形象为主,那么就会过多关注有关图片的CPU使用率和内存占用率的比例。如果发现常规的图片格式不满足需求,那么就是需要调研和寻找不同的解决方案。这本来就是没有固定的一套解决方案,只有相对合适的解决方案,因此,无论是从UI角度,还是从开发角度,甚至是产品角度,都得寻得整个产品中平衡度,寻找合适点,是能满足各方需求,进而打造更完善的产品应用。
参考地址:
1,https://developers.google.cn/speed/webp
2,https://developers.google.cn/speed/webp/docs/riff_container
2,https://github.com/penfeizhou/APNG4Android
]]>其中JVM是打造Java跨平台的关键,但相比Java API接口文档和Java本身编程语言,Java虚拟机相关的资料则显得异常匮乏。Java虚拟机隐藏了底层技术的复杂性以及机器与操作系统的差异性,而为千万开发者建立起使用方便的跨平台开发框架,哪怕运行程序的物理机器的情况千差万别,但Java虚拟机则在这千差万别的物理机上建立了统一的运行平台,从而使得开发者只需聚焦他们的业务程序。
正是这个跨平台机制,实现了再任何一台虚拟机上编译的程序都能在任何一台虚拟机上正常运行,这一极大优势使得Java应用的开发比传统的C/C++应用开发来的更加高效,也导致Java技术栈能力圈越来越广。也正好是Java虚拟机良好的封装,作为开发者如果仅仅限于使用方便的API上,而不是去理解Java世界里真正的核心是什么,那么能力其实是难以进一步提高的,因此去了解Java虚拟机来龙去脉是很有必要的。
简单做个思维导图,这篇文章主要讲的正如图中所示几个方面:JVM简介、JVM内存运行机制、虚拟机类。
Java为何能获得如此广泛的应用,除了它是拥有一门结构严谨、面向对象的编程语言之外,还有一点是脱离了硬件平台的束缚,真正实现了「一次编写,到处运行」的局面,并且提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界问题,这些好处就是统一放在Java虚拟机中。
发展历史
从1996年Sun公司发布的JDK1.0版本以来,最早期是Sun Classic VM,到大名鼎鼎的HotSpot VM,然后进入到移动设备的Google Android Dalvik VM,还有其他VM,包括Microsoft JVM等等。其中最有名莫过于HotSpot VM和Google Android Dalvik VM,HotSpot VM是当前使用范围最广的Java虚拟机,它的热点代码探测技术,它在优化程序的响应时间和最佳执行性能获得平衡,都使得它声名大噪。
而Google Android Dalvik VM则是因为过去10年移动互联网大热,搭载Android系统的移动设备几十亿台迅猛发展。本质上说 Dalvik VM并不是真正算的上一个Java虚拟机,因为它没有遵循Java虚拟机规范,不能执行Java的Class文件,但是它的Dex文件可以通过Class 文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API。
做什么
Java虚拟机主要能做的是提供一种跨平台开发框架,让使用者一次编写的程序,就能在各个平台上到处运行,使得开发者与硬件平台脱离,并且提供的自动内存管理机制和运行时编译优化,进而使得应用Java应用随着运行时间增加而获得更高的性能。
运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,主要包括以下几个运行时区域:
1,程序计数器,主要是当前线程所执行的字节码的行号指示器。 2,Java虚拟机栈,Java中的每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。我们常见的StackOverflowError错误,就是常见栈深度大于虚拟机所允许的深度。 3,本地方法栈,执行的是虚拟机使用到Nativie方法服务。 4,Java堆,也叫Java Heap,这个是Java虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时就创建了。几乎所有的对象实例都在这里分配内存。 5,方法区。这个是主要是用以虚拟机加载类信息、常量、静态变量、编译之后的代码等等。
内存管理方式
要知道Java是以什么闻名吗,当然是Java虚拟机的内存管理,也就是垃圾收集(GC),它的内存动态分配和内存回收技术已经相当成熟,看起来这么完善了,为何还要去学习Java虚拟机,主要目的是为了在排查各种内存溢出、内存泄漏问题时,我们可以快速定位出问题,并且优化和监控。
在实例对象时,如何确定对象是否已死,有两种方式介绍下:
1,引用计数算法。给实例对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1。这个引用计数算法虽然判断效率很高,但是有个问题是它很难解决对象之间的相互循环引用问题。
2,可达分析算法。这个算法思路主要是判定对象是否存活,从GC Roots的对象作为起点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。这就引出了Java中的强引用、软引用、弱引用、虚引用这四个区别。
说到垃圾收集算法,至于如何实现,大家不妨有空去看看源码,这里主要介绍算法的思想:
1,标记-清除算法。这里就包含两个阶段,“标记”和“清除”,首先标记出所需要回收的对象,在标记完成后统一回收所有被标记的对象。这个算法的不足之点在于效率问题,另一个是空间问题,标记清除之后会产生大量的不连续内存碎片,因为碎片,会容易引发另一次的垃圾收集动作。
2,复制算法。这个就解决碎片的问题,它将可用内存按容量划分大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把使用过的内存空间一次清理掉,但这个算法的问题是容易造成内存浪费。
3,标记-整理算法。这个算法就结合前面两个算法的特点,避免它们的弊端,先标记,但是后续不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4,分代收集算法。当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种主要根据对象存活周期的不同将内存划分几块。一般把Java堆分为新生代和老年代,这样就根据各个年代的特点去采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
至于内存分配和回收策略,主要是根据分代收集算法,一般对象优先在Eden分配,如果是大实例对象的话,则直接进入到老年代,还有长期存活的对象将进入老年代。
Java内存的自动管理机制虽是举世闻名,但是另外一个机制也是不甘落后,那就是类加载机制。在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的,虽然这种方式会使得类加载稍微增加一点性能开销,但是给Java应用程序提供高度的灵活性,比如依赖运行期动态加载和动态链接这个特点实现的。这个特点在OSGi技术中体现的淋漓尽致。
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。一般在遇到new、getstatic、putstatic和invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。在加载阶段,虚拟机主要完成以下3件事:
而对类加载器,需要重点去了解它的双亲委派模型,从Java虚拟机角度来看,只存在两种不同的类加载器,一种是启动类加载器(Bootstarp ClassLoader),这个类加载器是C++语言实现的,是虚拟机自身的一部分,另一个就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。这个双亲委派模式带来的一个优势会先检查父加载器是否已经被加载过,从这点衍生出Java世界的很多伟大技术出来,比如代码热替换,模块热部署,就是即插即用。
通过了解Java虚拟机,对于Java虚拟机的运行机制有一定的了解,当然关于Java虚拟机内容还有很多要挖掘,比如性能调优参数、调度、垃圾回收算法实现方式等等,对于作为一个Android开放人员来说,学习Java虚拟机更多是为了丰富自己的专业技术维度,扩展边界。
]]>一直以来,保持这么一个习惯,会在每年元旦时,给自己过去一年的生活经历进行简单的总结,这样总结回顾持续已经进行6年,今年则是第7年了。
不管如何,在辞旧迎新时刻里,人们对于过去一年时光总要进行适当的回顾。不管过去一年里遇到顺心的事、糟心的烦、开心时刻、悲伤往事,这一切所经历的最终会变成你人生旅程里某一个记忆点。因为过去时光已经一去不复返了,懊悔也好、遗憾也好,怎么说都已经是回不来了,虽然说回不来了,那也不能抛之脑后,对于过往一切还是需要记录点什么。显然,人类发明的文字书写系统,就是一个很好地记录手段,你可以利用其中的书写工具来帮助你,以记录那一去不复返的时光。
纵观过去几年的回顾总结,会发现这么一个现象,那就是每年的年度目标是在逐渐减少的,前面几年一般会把年度目标待办事项罗列满满的,生怕不够,后来年岁渐长之后,就明白了人的精力是有限,不可能既能做这个事情,又能去做另外一个事情,这就犹如逻辑学里的矛盾律,不可能既是A又不是A。其实一年中能完成立的Flag就不错了,何况是多件,这也是为何Facebook创始人扎克伯格会以每年做一件事而闻名,因为真正优秀顶尖的人早就知道这个道理。
这些都是年纪小的时候需要付出的学费,学费一交,后续就学乖了。因此,因此在2019年,我的年度目标主要聚焦在两个方向上,「突破和挑战」,这也是根据过去一年所发生所经历的生活旅程,总结送给自己的五个字。
接下去就根据这两个方向,进行详细的回顾,突破点到底体现在哪里,挑战又有什么,突破的地方有两点:第二收入和财商教育,挑战的点有两个方面:跑步和写作。
突破点在于搭建了一个付费专栏,踏出了尝试创造第二收入的第一步,因为这些收入来源恰好是根据自身生活学习习惯相结合。我们知道现在绝大多数人的收入来源主要靠工资,可以简称「第一收入」,那么「第二收入」则是除了第一收入之外的,在不损害第一收入利益前提下,而进行探索的。
就我自己所推崇的模式是以你花一次时间打造的产品,可以无限次售卖,自动创造被动收入,特别是需要善于利用互联网工具来帮助自己达到目的,比如会员订阅流程就是该模式下的典型代表,还有可能你的在线课程、语音产品、付费专栏等等知识付费产品,都符合这个模式。
而这一年里,我自己则是在知识星球上创建了一个付费专栏,主要目的是想把日常所学的知识,所经历的感悟,无论是阅读、个人效能、跑步、财商学习、社群沟通所获、优秀的人对话感悟,每天不断在上面进行输出。本意是提倡参加这个专栏的小伙伴进行一起学习精进,把各自生活经历书写在上面,一起记录点滴和进步,因为一群人一起学习是最高效的学习方式。
当然对于知识星球的运营,自己这一年下来,运营不佳,具体什么硬核内容输出,定位是否准确问题,专栏的名称叫做「第二身份试验场」,第二身份是希望每个人都能去追求的,那到底这个专栏产生的意义和价值在哪里,通过这一年的运营下来,到现在我才明确。就是希望能参与进去的人,能利用这个专栏平台把自身第二身份探索点滴记录下来,形成一个生活行动轨迹,比如经过一年时间践行之后,然后可以在岁末时候看看的成果物有哪些输出,这反应到人们心理则叫做「进步」,换句话说,收获满满地的成就感。
对于2020年,知识星球专栏会投入更多精力和时间去好好运营,这次会聚焦内容输出了,具体输出什么内容,等之后时间推出的2020新年计划里阐述。
因为过去一年通过刻意练习写作能力,在写的文章中收获了一定的写作打赏费用,这也是额外收入一种。在来年,还会开始用自己认知的变现,因为基于前期5年学习进阶基础,我觉得我自己的基本认知体系框架已经搭建好,想进一步发展,那就需要往外推出,看看其他人的反馈,到底好还是不好,增加多渠道变现。况且昨天刚发了一条朋友圈,2020年的目标是想买入一套房,利用自己的能力去尽可能赚取更多的收入,提供更多的价值输出,是光明正大的一条途径。
财商教育是2019年里专注学习的目标,主要目的是想搭建自我投资知识体系,通过的途径则是阅读输入、线上跟随一群人进行创富读书会,线下则去玩现金流游戏和线下读书会。
为何财商水平提升显得那么迫切,在年中的时候我才想明白,主要目的是为了养老准备。不知是否有人想过这么一个问题,你这一生想要赚多少钱,你这一生需要花费多少钱,你这一生实际又能赚多少钱呢?我的答案如图: 插入一张图片
因此,想要靠一份死工资收入想达到以上水平,这辈子肯定是无望。如果收入水平是以线性增长也不现实,我们需要学会以指数增长为目标导向,然后接受一种叫做「慢慢变富」的概念,要知道巴菲特老先生,他的绝大多数收入比例是在他50岁以后,特别是他最近十年以来,投资基数越大,指数收入增长就可观。
财商教育的理解在我自己看来,是一种基于长期思维,以终为始导向的终身学习过程。试想一下未来30年里,你靠什么养老,为什么要思考未来30年之后的事情,因为你会变老,你靠什么决定你未来30年的成败,主要是财商认知水平来决定,因为这一过程可以持续30年进行学习和实践的。
如果此刻说想为自己养老做准备,正好昨天在一个朋友聊天沟通中谈到的,那就是此刻培养自己合理的消费观,避免那些冲动不合理的消费行为,延迟自我消费满足,增强储蓄能力,当下开头能做的是银行现金定存,可能是一个不错的选择。
现在打开我的悦跑圈APP,查看年度跑步里程,发现不到100公里。很显然这个目标没达成,年度目标规划的是500公里,完成都不到1/5,现在回想下为何有如此大的差距呢,从深层次来看,还是对自己的健康身材漠不关心。或许因为日常生活没经历过健康痛处,仅仅知道健康是很重要,但没有把优先级排在前列,很多时候会为了写作让步,并且运动时间不规律,生活作息也是不规律。
本来以为自我生物钟调整能力很强,但是通过记录睡眠数据之后,发现有两个特征很明显:晚睡和晚起。晚睡基本是12点以后,基本入睡时间点在12点30分-01点,然后早上起床时间段为8点,睡眠时间是充足了,但是运动时间则没有了。
一周3-4次跑步时刻,基本能做到每周1次跑步,每次能有5公里跑步距离就不错了,包括践行的跑步姿势、跑步数据、参与的跑步活动都比往年都减少了,因此这点是需要2020年里重点改善的一块。
要说在2019年里跑步目标没达到有遗憾之外,那么完成写作日更目标则是自豪感十足。发起这个每天微信公众号文章写作日更对我来说是个十足挑战,因为你会面临着写作内容枯竭、写作手法固定、写作风格转换等问题,并且得保证每天有写作输出,能日复一日持续地去践行,本身就是对稳定性考验。
因此,得想清楚为何要写作日更,写作日更目的是为了什么。现在回顾起来,总结有两点:刻意练习逻辑思维能力和建立个人品牌影响力。
刻意练习逻辑思维能力,通过书写的方式,可以很好地把你平时思绪给汇聚在一起,然后分析总结输出,形成自我思考过程。然后通过微信公众号这个渠道,表达了你对某个事件的看法,传递出你自己思考的观点,让更多知道了你所思所想,那么在此过程中,自然而然会有个人影响力地传递。时间久了之后,一些人的印象中就知道某个作者是对某个领域有比较深的理解,因为它之前输出了那么多的文章来表达,写作出来的文章也是很好地验证凭据。
当然,还会有写作收获,关于这点我会再写一篇文章来总结过去一年通过写作之路讲讲自己的收获,这点对于我自己来说是一个壮举,也是未来可以向身边小伙伴吹牛逼的资本,就冲能吹牛逼,自己无论如何也会坚持完成写作日更,因为人都喜欢被别人夸赞的。
但在来年自己会再接再厉,但不去做写作日更了,那确实太耗精力和心力,写作频率固化为每周一次输出。当然微信公众号会持续经营,可能会想着其他内容形式输出,敬请期待。
如果说过去一年里,自己深刻的一件事,那就是「写作日更」。毕竟持续写了一年,写出自己逐渐形成的知识体系框架,然后也是写作,让我自己独立思考能力获得进一步的解放。涉及了一些知识点和面,可能平时自己都不会去看的书,因为写作而结缘,包括一些摄影书籍、财务会计书籍等,这些工具技能书,只有真正用到之后才会去阅读。最重要的一个收获是,就是写出了自己个人成长小册,到时候通过梳理过去一年写作内容,整理一本杂志手册出来,并与大家分享。
年终复盘很有必要,就是看看自己哪里表现的好「得瑟下」,表现的不好,督促自己去完善,因为一切都是为了遇见更美好的将来而努力着。
2020年,聚焦「刻意打造良好习惯」主题
]]>话不多说,先来张图,分享大纲如下:
之前受一篇文章启发,说的是如何讲解好一个技术点知识,可以分为两部分去介绍:外部应用维度和内部设计维度,基本从这两个角度出发,可以把一个技术点讲的透彻。同样,我把这种方式应用到写作中去。
在 Android 中,TTS全称叫做 Text to Speech,从字面就能理解它解决的问题是什么,把文本转为语音服务,意思就是你输入一段文本信息,然后Android 系统可以把这段文字播报出来。这种应用场景目前比较多是在各种语音助手APP上,很多手机系统集成商内部都有内置文本转语音服务,可以读当前页面上的文本信息。同样,在一些阅读类APP上我们也能看到相关服务,打开微信读书,里面就直接可以把当前页面直接用语音方式播放出来,特别适合哪种不方便拿着手机屏幕阅读的场景。
这里主要用到的是TextToSpeech类来完成,使用TextToSpeech的步骤如下:
创建TextToSpeech对象,创建时传入OnInitListener监听器监听示范创建成功。 设置TextToSpeech所使用语言国家选项,通过返回值判断TTS是否支持该语言、国家选项。 调用speak()或synthesizeToFile方法。 关闭TTS,回收资源。
XML文件
1 |
|
Activity文件
1 | public class TtsMainActivity extends AppCompatActivity implements View.OnClickListener,TextToSpeech.OnInitListener { |
加上权限
1 | <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission> |
由于目前我在公司负责开发的产品是属于语音助手类型,自然这类 TTS 发声的问题和坑日常见的比较多。常见的有如下几种类型:
随着物联网的到来,IoT设备增多,那么对于类似语音助手相关应用也会增多,因为语音是一个很好的入口,现在逐步从显示到去显示的过程,很多智能设备有些是不需要屏幕的,只需要能识别语音和播放声音。因此,随着这类应用的增长,对于TTS 相关的API接口调用频率肯定也是加大,相信谷歌在这方面也会逐步在完善。
从外部使用角度入手,基本是熟悉API接口和具体项目中应用碰到的问题,然后不断总结出来比较优化的实践方式。了解完外部角度切入,那么我们需要里面内部设计是怎么一回事,毕竟作为一个开发者,知道具体实现原理是一个基本功。
Android TTS 目标就是解决文本转化为语音播报的过程。那它到底是怎么实现的呢,我们从TextToSpeech类的构造函数开始分析。
这里我们用Android 6.0版本源码分析为主,主要涉及的相关类和接口文件,在源码中的位置如下:
framework.java framework/core/android.java external.java external.java external.java external_android_tts_compat_SynthProxy.cpp external_svox_picottsengine.cpp
初始化角度:先看TextToSpeech类,在使用时,一般TextToSpeech类要进行初始化,它的构造函数有三个,最后真正调用的构造函数代码如下:
1 | /** |
从构造函数可以看到,调用到initTts操作,我们看下initTts方法里是什么东东,代码如下:
1 | private int initTts() { |
这里比较有意思了,第一步先去连接用户请求的TTS引擎服务(这里可以让我们自定义TTS引擎,可以替换系统默认的引擎),如果没找到连接用户的TTS引擎,那么就去连接默认引擎,最后是连接高性能引擎,从代码可以看出高性能引擎优先级最高,默认引擎其次,connectToEngine方法代码如下:
1 | private boolean connectToEngine(String engine) { |
这里的Engine.INTENT_ACTION_TTS_SERVICE的值为"android.intent.action.TTS_SERVICE";其连接到的服务为action,为"android.intent.action.TTS_SERVICE"的服务,在external.xml文件可以发现:
1 | <service android:name=".PicoService" |
系统自带的默认连接的服务叫做PicoService,其具体代码如下:其继承于CompatTtsService。
1 | public class PicoService extends CompatTtsService { |
我们再来看看CompatTtsService这个类,这个类为抽象类,它的父类为TextToSpeechService,其有一个成员SynthProxy类,该类负责调用TTS的C++层代码。如图:
我们来看看CompatTtsService的onCreate()方法,该方法中主要对SynthProxy进行了初始化:
1 |
|
紧接着看看SynthProxy的构造函数都干了什么,我也不知道干了什么,但是里面有个静态代码块,其加载了ttscompat动态库,所以它肯定只是一个代理,实际功能由C++本地方法实现
1 | /** |
我们可以看到,在构造函数中,调用了native_setup方法来初始化引擎,其实现在C++层(com_android_tts_compat_SynthProxy.cpp)。
我们可以看到ngine->funcs->init(engine, __ttsSynthDoneCB, engConfigString);这句代码比较关键,这个init方法上面在com_svox_picottsengine.cpp中,如下:
1 | /* Google Engine API function implementations */ |
到这里,TTS引擎的初始化就完成了。
再看下TTS调用的角度,一般TTS调用的类是TextToSpeech中的speak()方法,我们来看看其执行流程:
1 | public int speak(final CharSequence text, |
主要是看runAction()方法:
1 | private <R> R runAction(Action<R> action, R errorResult, String method, |
主要看下mServiceConnection类的runAction方法,
1 | public <R> R runAction(Action<R> action, R errorResult, String method, |
可以发现最后会回调action.run(mService)方法。接着执行service.playAudio(),这里的service为PicoService,其继承于抽象类CompatTtsService,而CompatTtsService继承于抽象类TextToSpeechService。
所以会执行TextToSpeechService中的playAudio(),该方法位于TextToSpeechService中mBinder中。该方法如下:
1 |
|
接着执行mSynthHandler.enqueueSpeechItem(queueMode, item),其代码如下:
1 | /** |
主要是看 speechItem.play()方法,代码如下:
1 | /** |
可以看到主要播放实现方法为playImpl(),那么在TextToSpeechService中的playAudio()中代码可以知道这里的speechitem为SynthesisSpeechItemV1。
因此在play中执行的playimpl()方法为SynthesisSpeechItemV1类中的playimpl()方法,其代码如下:
1 |
|
在playImpl方法中会执行onSynthesizeText方法,这是个抽象方法,记住其传递了一个synthesisCallback,后面会讲到。哪该方法具体实现是在哪里呢,没错,就是在TextToSpeechService的子类CompatTtsService中。来看看它怎么实现的:
1 |
|
最终又回到系统提供的pico引擎中,在com_android_tts_compat_SynthProxy.cpp这个文件中,可以看到使用speak方法,代码如下:
1 | static jint |
至此,TTS的调用就结束了。
从实现原理我们可以看到Android系统原生自带了一个TTS引擎。那么在此,我们就也可以去自定义TTS引擎,只有继承ITextToSpeechService接口即可,实现里面的方法。这就为后续自定义TTS引擎埋下伏笔了,因为系统默认的TTS引擎是不支持中文,那么市场上比较好的TTS相关产品,一般是集成讯飞或者Nuance等第三方供应商。
因此,我们也可以看到TTS优劣势。
优势:接口定义完善,有着完整的API接口方法,同时支持扩展,可根据自身开发业务需求重新打造TTS引擎,并且与原生接口做兼容,可适配。
劣势:原生系统TTS引擎支持的多国语言有限,目前不支持多实例和多通道。
从目前来看,随着语音成为更多Iot设备的入口,那么在语音TTS合成播报方面技术会越来越成熟,特别是对于Android 系统原生相关的接口也会越来越强大。因此,对于TTS后续的发展,应该是冉冉上升。
总的来说,对于一个知识点,前期通过使用文档介绍,到具体实践,然后在实践中优化进行总结,选择一个最佳的实践方案。当然不能满足“知其然而不知其所以然”,所以得去看背后的实现原理是什么。这个知识点优劣势是什么,在哪些场景比较适用,哪些场景不适用,接下来会演进趋势怎么样。通过这么一整套流程,那么对于一个知识点来说,可以算是了然于胸了。
]]>2018年度目标回顾
思维导图
正如有这么一句话:“我的2018年目标,就是搞定2017年,那些原定于2016年完成的安排,不为别的,只为兑现我 2015 年时要完成的 2014年年度计划的诺言。”
是不是每年都这样制定年度目标呢?不管过去制定的目标完成多少,但是有年底目标总比没有目标来的强,既然是自己制定了,是不是一定得跪着含着泪也得去完成呢,不见得。
如果要用一个词语来总结过去的一年,我给自己送四个字:「不够落地」。为何会是这四个字呢,主要原因是年初定的目标,现在年底回顾总结时发现执行不到位,有些目标落地的过于形式,虽然时间投入进去了,但是产出却没有,很多时候都是仅仅走过场而已。比方说,每天的早睡早起,定这个目标的意义在于想做一个晨型人,早起有更多的时间可以独处和进行一些输入学习。可是一年下来,结果不理想,虽然定好是每天6点05分的闹钟,但是天气一冷,闹钟响了,仅仅是醒过来,然后顺手把闹钟给关掉,却没有起床,而是继续睡下去,睡的不得不起来时间点,那时才慢悠悠起来,好像永远睡不醒一样。所以这个有点自欺欺人,有了早起的意识,却没有早起的行为。除此之外,不仅仅是这一点,还有其他方面也是如此,因此过去承诺没做到的,在2019年里要逐步实现,其中最重要的一个前提是有足够的时间去践行,故早起就是一个很好挤出时间的好习惯。
如果以九宫格方式进行复盘的话,我把这一年自己所经历过的事情从以下8个维度切入回顾:「健康、家庭、效能、财富、学习、事业、社交、休闲」。
健康维度
健康维度体重、跑步、睡眠三个方面。
体重70公斤:2018年的目标是体重降低到60公斤,想不到年底竟然达到70公斤,明显是没控制好,回顾原因,无外乎久坐不动,日常生活缺少一定量的运动,同时饮食也没得到合适的控制。
跑步里程500公里:对于跑步,2018年规划是跑500公里,实际完成情况才46.46公里,刚好1/10。说起来惭愧,之前计划是每周三次5公里跑步,这一年回顾来看,基本就没有坚持在跑步,追究原因,自己行动力不够坚定,同时对我要求不高,容易犯懒,还有今年报名参加杭州马拉松也是没报上,没有一个明确的动力去督促自己。2019年跑多少公里不再当个目标来执行了,更多是当日常运动习惯来落地。
睡眠质量:对于睡眠质量,现在习惯是每天晚上11点半-12点期间入睡,早上起来呢,则是8点左右,睡眠时间是够8个小时,但是通过一些软件记录发现,深度睡眠时间比较少,大多数时间是浅度睡眠。其实每天配合一定的运动量再入睡,睡眠质量会高很多。
感想:既然在年初时候就喊出要实现六块腹肌目标,这个口号也喊4年了,依然不够奏效,为何?主要原因是自己对于是否真想要有六块腹肌没那么大决心,对于诱惑没有一点免疫力,说了也写下来了,依然是做不到,很多事不成功未尝不是这个原因呢。过去的也已经过去,再后悔也没用,就只有寄希望于未来,来年再战。
家庭维度
家庭维度包括感情生活、个人情绪、沟通和陪伴三个方面。
感情生活:16年开始谈恋爱,在一起有2年了,感情也是从刚开始三个月的蜜月期,到现在越来越亲密,这一年也是互相支持和陪伴。之前我习惯是独自考虑事情,碰到一些重要事情也是独自去承担,因为从小到大都是习惯于一个人了,现在意识上逐渐改变成为有两个人,要知道现在是2个人了,身上背负的责任感更强烈了, 需要迅速成长起来,怎么样成为一个负责任的男人。这一年时间里,我们也越来越了解对方,她美丽、善良、天真、可爱,但也会小任性,做事情马虎,考虑的事情有时候会稍微片面,但这都无所谓,因为我爱她,我希望自己能给她最大的幸福,我最大的愿景是能够娶上她,跟她白头偕老。但同时在日常相处中,也会经常闹闹小脾气,遇到双方委屈时刻,此时更多应该是耐心陪伴和沟通了,希望来年,两人相处模式会更温馨和谐,在稳定的关系中进一步创造活力,日常生活中时不时有惊喜浪漫时刻,一起经历更有趣的事,毕竟有着共同的回忆,感情会越发稳定。
个人情绪:在过去的一年,自己少了一些抱怨,多了一些好奇心,在与朋友或同事相处过程中,无论是对人还是对事,都需要保持一颗敬畏和谦卑的心去对待。同时也要积极进行换位思考,多点乐观,积极主动,情绪上遇到压抑,需要及时找个方式进行发泄,不能累积,压力肯定是有的,但更多需要积极去面对,需要更多的平常心。
沟通和陪伴:沟通在于每周固定于奶奶电话一次,作为一个从小被奶奶抚养大的人,现在就是希望老人家身体保持健康,长寿。陪伴则是有空的话,要定期回去看看,回去不仅仅是自己老家,更多也是去女票父母家。
效能维度
效能维度包括早睡早起、Omnifocus驱动、晨间日记三个方面。
早睡早起:在2018年初的计划里是每天保持早睡早起的习惯,曾经还能为自己保持早上6点多就能起来挺高兴的,这一年回顾下来发现早上基本都是8点左右,偶尔有那么一段时间进行早起,却没有维持下去,没有早起,导致之前保持的晨跑习惯也丢弃了,跑步目标也未完成,曾经的晨读习惯也放弃了,所以在2019年需要把这个习惯培养起来,因为在干任何事之前,需要保证你自己有时间才行,没时间去行动,大都是在说空话。
Omnifocus驱动:Omnifocus是GTD践行神器,可以很好的安排一天待办事项,行程等等。统筹规划如何更有效合理利用这一天的时间安排,现在工作上基本习惯于挑三件重要事情先处理,同样在每天生活中的三件重要事情处理也带来帮助很大,已经逐渐形成以Omnifocus驱动过着每一天的机制。
晨间日记:从2014年12月开始写的晨间日记,写到现在已经有4年多,但是越写到后面越觉得写这个是在于形式,而不是认真去完成。某一天感觉比较重要,就会去反思和检视,感觉心情不好,则不会做一些记录,其实每天花10分钟去检视和反思是很有必要的,殊不知这部分才是意义所在。同时今年晨间日记模板也适时调整了,稍微简化了,偏向于自己更愿意记录形式,会有更多想法记录的,晨间日记不仅仅局限于在晨间写,更多反应的是在日常时刻,有想法和感悟要及时记录,它更多是载体而已。
财商维度
财商维度包括年收入预期、开始负债、 财商技能学习三个方面。
年收入预期:年初定的目标是在2018年希望年收入能达到40万,现在目前阶段是达到30万,离目标还是有一定差距的。现在收入方式也是比较单一,纯工资性收入,没有工资以外其他收入,比较单薄,万一遇到裁员和失业呢,我能靠其他什么收入来维持目前的支出呢,这也是这一整年都在焦虑原因,所以在2019年要积极扩展自己的第二收入,逐步脱离主要靠工资性为主的收入方式。
开始负债:3月份时候买入一套房,首付是借的,房子也是按揭,所以伴随着就是有债务了,目前房子对我来说就是负债,需要每月从口袋中拿出现金流去付月供。债务是可以促进经济的发展,但是需要记住这么一句话:你的债务增长速度不要超过收入增长速度。
财商知识:每周定期参加线上房产读书会,同时阅读了财商培养相关的书籍,对于一些概念有了进一步的认知,线下多玩现金流游戏, 同时也接触到房地产投资一些流派,财商学习是需要一辈子去学习,最终目的是建立自我投资回报体系,逐步提高自我的投资性收入。
学习维度
学习维度包括PMP证书、阅读和写作、线下课程三个方面。
PMP正式:在6月份报名了PMP考试,经过了2个月的线下课程培训,顺利考试通过,获得PMP证书,这也是在2018年最大收获之一。
阅读和写作:年初给自己定的目标是50本书计划,分专业和非专业类型,今年非专业书大概是看了10本,专业书看了20本,总共是30本,3/5,计划没完成,所以来年再努力。至于写作,微信公众号文章基本保持一个月一篇节奏,写的就更少了,除此之外,简书上写技术类文章,也是基本是保持每月一篇的节奏,不够勤快,所以在2019年,这两点要着重加强。原则:输出可以倒逼输入,就算了读了很读书或学了很多,没有某种方式输出,并且分享出来,其实没学到多少的。
线下课程:更多是参与一些自己感兴趣的课程,比如财富学习课、混沌大学线下报名课程等。
事业维度
事业维度包括专业技能、第二收入、投资关注三个方面。
专业技能:工作本身是研发方面,从事的是Android开发相关,今年最大的突破,是从应用架构方面转变Framework层深入,之前对于APP架构和性能优化有深入研究,现在在Framework层有了进一步提升。专业目标是当一名全栈工程师,不仅仅在移动端要继续深入,同时对于服务端开发框架和流程需要持续学习,同时对于基础性知识要更进一步夯实。
第二收入:尝试过团建项目,结合视觉引导,可惜失败了,不过也算是为了尝试自己第二收入进行的努力,还有通过写技术文章获得赞赏,但是收入不多而已。2019年的目标是要将自己过去几年的所学所得进行输出,提供稳定有价值的服务,开始尝试变现,打造好一个产品,选择好一个载体很关键。
投资关注:对于房地产、股票持续关注,看房的话,需要200套起步,虽然2018年股票市场萎靡不振,但是如果不追涨,在目前股票底部区域,选择被低估的股票,静候时间即可。
社交维度
社交维度包括线下活动、链接、社群三个方面。
线下活动:2018年目标是每周参加一次线下活动,这个至今为止,完成的还不错,活动类型则会比较多了,重在去体验和感受。
链接:参加社交的目的在于链接他人,通过链接,找到同频人,了解不同人的差异和想法,先弱连接,然后再强连接,甚至是好朋友都有可能,这需要自己是否愿意给自己机会,打造一个开放的系统。
社群:这个就很多活动了,跑步社群、演讲社群、读书会社群、混沌大学社群、书友会社群、现金流社群、吃喝玩乐样样都有。
休闲维度
休闲维度包括旅行、独处、相聚三个方面。
旅行:2018年的梦想板目标之一是去泰国参加天灯节,这个今年没实现,只能放在来年实现了, 本来2018计划一次长途旅行,一次短途旅行,貌似今年没怎么出去玩,知道了再多,也得有空亲眼去见见才行。
独处:总的来说,今年自我独处时间相对较少,基本没有一些让自己安静下来的场景,因为少了早起时间,所以在来年需要注重独处时刻,隔一段时间定期总结深入思考,很有必要。
相聚:曾经在6月份发起过一个男神私董会,但是仅仅相聚几次,其实是需要那种线下朋友相聚时刻,大家在职场、生活中碰到的困惑和问题可以抛出来共同讨论,通过讨论寻找到合适的解决方式,因为别人碰到的,很有可能自己在不远的将来也会碰到。
一年中印象深刻
如果说自己在过去一年里,非要说印象最深刻的一件事是什么的话,我觉得是经历过3月份杭州楼市最疯狂的阶段,那种全款买房还得四处托关系的,按揭贷款买房的完全没机会买的让人印象最深刻,然后在我自己买完房子的第二天,杭州正好出摇号政策,这个点掐的刚刚好,然后就开始经历4、5、6月份万人摇号场景,然后7、8月份开始,土拍市场降温,9、10月份二手房成交量持续走低,到现在11、12月份,行情来个大转弯,现在很多楼盘开始流拍,各种待交付楼盘进入维权阶段,精装修开始变成惊装修,真的可以说是一个很合格正弦曲线。
年终复盘很有必要,就是看看自己哪里表现的好「得瑟下」,表现的不好,督促自己去完善,因为一切都是为了遇见更美好的将来而努力着。
2019年,聚焦「第二收入」主题。
]]>今天则会从更小细粒度入手,主要讲讲在组件化架构下组件与组件之间通信机制是如何、包括所谓的UI跳转,其实也是组件化通信,只不过它稍微特殊点,单独抽取出来而已。学习知识的过程很常见的一个思路就是从整体概况入手,首先对整体有个粗略的印象,然后再深入细节,抽丝剥茧般去挖掘其中的内在原理,一个点一个不断去突破,这样就能建立起自己整个知识树,所以今天我们就从通信机制这个点入手,看看其中内在玄机有哪些。
同样,在每写一篇文章之前,放个思维导图,这样做的好处对于想写的内容有很好的梳理,逻辑和结构上显得清晰点。
总所周知,Android提供了很多不同的信息的传递方式,比如在四大组件中本地广播、进程间的AIDL、匿名间的内存共享、Intent Bundle传递等等,那么在这么多传递方式,哪种类型是比较适合组件与组件直接的传递呢。
说了这么多,那组件化通信什么机制比较适合呢?既然组件层中的模块是相互独立的,它们之间并不存在任何依赖。没有依赖就无法产生关系,没有关系,就无法传递消息,那要如何才能完成这种交流?
目前主流做法之一就是引入第三者,比如图中的Base Module。
组件层的模块都依赖于基础层,从而产生第三者联系,这种第三者联系最终会编译在APP Module中,那时将不会有这种隔阂,那么其中的Base Module就是跨越组件化层级的关键,也是模块间信息交流的基础。比较有代表性的组件化开源框架有得到DDComponentForAndroid、阿里Arouter、聚美Router 等等。
除了这种以通过引入第三者方式,还有一种解决方式是以事件总线方式,但这种方式目前开源的框架中使用比例不高,如图:
事件总线通过记录对象,使用监听者模式来通知对象各种事件,比如在现实生活中,我们要去找房子,一般都去看小区的公告栏,因为那边会经常发布一些出租信息,我们去查看的过程中就形成了订阅的关系,只不过这种是被动去订阅,因为只有自己需要找房子了才去看,平时一般不会去看。小区中的公告栏可以想象成一个事件总线发布点,监听者则是哪些想要找房子的人,当有房东在公告栏上贴上出租房信息时,如果公告栏有订阅信息功能,比如引入门卫保安,已经把之前来这个公告栏要查看的找房子人一一进行电话登记,那么一旦有新出租消息产生,则门卫会把这条消息一一进行短信群发,那么找房子人则会收到这条消息进行后续的操作,是马上过来看,还是延迟过来,则根据自己的实际情况进行处理。在目前开源库中,有EventBus、RxBus就是采用这种发布/订阅模式,优点是简化了Android组件之间的通信方式,实现解耦,让业务代码更加简洁,可以动态设置事件处理线程和优先级,缺点则是每个事件需要维护一个事件类,造成事件类太多,无形中加大了维护成本。那么在组件化开源框架中有ModuleBus、CC 等等。
这两者模式更详细的对比,可以查看这篇文章多个维度对比一些有代表性的开源android组件化开发方案
事件总线,又可以叫做组件总线,路由+接口,则相对好理解点,今天从阅读它们框架源码,我们来对比这两种实现方案的不同之处。
这边选取的是ModuleBus框架,这个方案特别之处在于其借鉴了EventBus的思想,组件的注册/注销和组件调用的事件发送都跟EventBus类似,能够传递一些基础类型的数据,而并不需要在Base Moudel中添加额外的类。所以不会影响Base模块的架构,但是无法动态移除信息接收端的代码,而自定义的事件信息类型还是需要添加到Base Module中才能让其他功能模块索引。
其中的核心代码是在与 ModuleBus 类,其内部维护了两个ArrayMap键对值列表,如下:
1 | /** |
在使用方法上,在onCreate()和onDestroy()中需要注册和解绑,比如
1 | ModuleBus.getInstance().register(this); |
最终使用类似EventBus 中 post 方法一样,进行两个组件间的通信。这个框架的封装的post 方法如下
1 | public void post(Class<?> clientClass,String methodName,Object...args){ |
可以看到,它是通过遍历之前内部的ArrayMap,把注册在里面的方法找出,根据传入的参数进行匹配,使用反射调用。
#####接口+路由
接口+路由实现方式则相对容易理解点,我之前实践的一个项目就是通过这种方式实现的。具体地址如下:DemoComponent 实现思路是专门抽取一个LibModule作为路由服务,每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。
比如定义两个路由地址,一个登陆组件,一个设置组件,核心代码:
1 | public class RouterPath { |
那么就相应着就有两个接口API,如下:
1 | public interface ILoginProvider extends IProvider { |
这两个接口API对应着是向外暴露这两个组件的能提供的通信能力,然后每个组件对接口进行实现,如下:
1 | "登陆页面") (path = RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE, name = |
这其中使用的到了阿里的ARouter页面跳转方式,内部本质也是接口+实现方式进行组件间通信。
调用则很简单了,如下:
1 | ILoginProvider loginService = (ILoginProvider) ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).navigation(); |
还有一个组件化框架,就是ModularizationArchitecture ,它本质实现方式也是接口+实现,但是封装形式稍微不一样点,它是每个功能模块中需要使用注解建立Action事件,每个Action完成一个事件动作。invoke只是方法名为反射,并未用到反射,而是使用接口方式调用,参数是通过HashMap<String,String>传递的,无法传递对象。具体详解可以看这篇文章 Android架构思考(模块化、多进程)。
页面跳转也算是一种组件间的通信,只不过它相对粒度更细化点,之前我们描述的组件间通信粒度会更抽象点,页面跳转则是定位到某个组件的某个页面,可能是某个Activity,或者某个Fragment,要跳转到另外一个组件的Activity或Fragment,是这两者之间的通信。甚至在一般没有进行组件化架构的工程项目中,往往也会封装页面之间的跳转代码类,往往也会有路由中心的概念。不过一般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity。每个组件可以注册自己所能处理的短链的 Scheme 和 Host,并定义传输数据的格式,然后注册到统一的 UIRouter 中,UIRouter 通过 Scheme 和 Host 的匹配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注解,然后通过 APT 形成具体的逻辑代码。
下面简单介绍目前比较主流的两个框架核心实现思路:
ARouter 核心实现思路是,我们在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activityClass映射关系的类文件,然后app进程启动的时候会拿到这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。
ActivityRouter 核心实现思路是,它是通过路由 + 静态方法来实现,在静态方法上加注解来暴露服务,但不支持返回值,且参数固定位(context, bundle),基于apt技术,通过注解方式来实现URL打开Activity功能,并支持在WebView和外部浏览器使用,支持多级Activity跳转,支持Bundle、Uri参数注入并转换参数类型。它实现相对简单点,也是比较早期比较流行的做法,不过学习它也是很有参考意义的。
总的来说,组件间的通信机制在组件化编程和组件化架构中是很重要的一个环节,可能在每个组件独自开发阶段,不需要与其他组件进行通信,只需要在内部通信即可,当处于组件集成阶段,那就需要大量组件进行互相通信,体现在每个业务互相协作,如果组件间设计的不好,打开一个页面或调用一个方法,想当耗时或响应慢,那么体现的则是这个APP使用比较卡顿,仅仅打开一个页面就是需要好几秒才能打开,则严重影响使用者的体验了,甚至一些大型APP,可能组件分化更小,种类更多,那么组件间的通信则至关重要了。所以,要打造一个良好的组件化框架,如何设计一个更适合自己本身的业务类型的通信机制,就需要多多进行思考了。
参考文章: 1,https://github.com/luckybilly/AndroidComponentizeLibs 2,http://blog.spinytech.com/2016/12/28/android_modularization/
]]>对于HTTP协议,想必大家都不陌生,在工作中经常用到,特别是针对移动端和前端开发人员来说,要获取服务端数据,基本走的网络请求都是基于HTTP协议,特别是RESTFUL + JSON 这种搭配特别主流。那如果让大家具体讲讲HTTP协议背后的历史、原理、交互流程、与HTTPS区别、身份认证、Web攻防技术等等信息,大家能讲的出来吗,反正我讲的也是一知半解,虽然会经常看这方面的文章,但也只是在具体项目进行开发过程中碰到对某个概念不清楚,才会去特意看下,却没有特意去总结归纳为一直知识点,没有完整的表达描述过,其实对这个知识点还是没掌握好的,所以用写作方式来进行阐述是很好一个方式,目前也正在践行着。
在写作之前,这篇文章主要想讲的内容在以下这张图中,通过做思维导图方式来表达一篇文章内容,我觉得逻辑会特别清楚,同时也是对某个知识点会很好进行总结归纳。
###HTTP历史
发展由来
在1989 年 3 月, 互联网还只属于少数人。 在这一互联网的黎明期, HTTP 诞生了。CERN( 欧洲核子研究组织)的蒂姆 • 伯纳斯 - 李( Tim BernersLee)博士提出了一种能让远隔两地的研究者们共享知识的设想。 最初设想 的 基本理念是: 借助多文档之间相互关联形成的超文本( HyperText),连成可相互参阅的 WWW( World Wide Web,万维网)。并且版本从 HTTP 1.0 到 HTTP 1.1 再到现在的 HTTP 2.0,目前主流版本还是基于 HTTP 1.1,HTTP 协议同时也是目前互联网上应用最为广泛的一种网络协议,所有的 WWW 文件都必须遵守这个标准,设计HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。
TCP/IP
我们都知道 HTTP 协议在 根据 TCP/IP 网络分层来看,它是属于应用层,TCP/IP 网络分层总共有5层,它是属于最上层,它的下一层则是 TCP/IP 传输层,如图所示:
从逻辑平行来看,发送方和接受方都是处于同一平行层,发送方每层传递的信息会在下一层进行信息封装加密,然后逐层传递,通过实际物理链路进行传递,然后接收方接收到信息进行解密分析,不断把报文头信息进行还原,最后处理发送方发送过来的信息,处理完之后,再用同样的方式传递回去,两者传输通信方式是全双工模式。在此之前需要一个建立连接过程,所谓的三次握手,通信结束也有断开连接过程,也就是四次握手断开操作。
在讲述 HTTP 协议为何了解 TCP/IP 内容呢,因为我们需要知道 HTTP 协议实际通信过程是怎么样的,它所依赖的环境是怎么样的,从切面角度去看,实际是经历了这5层通信,从平面去看,默认以为是客户端与服务端仅仅进行平层通信而已,那是因为封装的方便。
因为目前主流在用的还是以 HTTP 1.1 版本为主,那就用这个版本来分析。
一个典型HTTP1.1的请求协议报文结构,大体上可以分为三块,即请求行、头部、消息主体。
请求行
请求行包含HTTP请求方法、请求的URL、HTTP协议版本三个内容,它们之间以空格间隔,并以回车+换行结束。HTTP请求方法有下面几种,常用的有GET、POST请求。
请求头部
头部可以分成三个部分,为常用头域、请求头域、实体头域。其中常用头域和实体头域部分内容在响应协议部分也有相同的定义。
常用头域
常用头域名称 | 作用描述 |
---|---|
Cache-Control | 缓存控制 |
Connection | HTTP 1.1默认是支持长连接的(Keep-Alive),如果不希望支持长连接则需要在此域中写入close |
Date | 表明消息产生的日期和时间 |
Pragma | |
Trailer | |
Transfer-Encoding | 告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式 |
Upgrade | 给出了发送端可能想要”升级”使用的新版本或协议 |
Via | 显示了报文经过的中间节点(代理、网关) |
Warning |
请求头域
请求头域名称 | 作用描述 |
---|---|
Accept | 指明请求端可以接受处理的媒体类型 |
Accept-Charset | 指明请求端可以接受的字符集 |
Accept-Encoding | 指明请求端可以接受的编码格式 |
Authorization | 授权 |
Expect | 允许客户端列出某请求所要求的服务器行为 |
From | 提供了客户端用户的E-mail地址 |
Host | 指明请求端的网络主机和端口号 |
If-Match | 服务端在响应头部里面返回ETag信息,客户端请求时在头部添加If-Match(值为响应的ETag),服务端接收后判断ETag是否相同,若相同则处理请求,否则不处理请求。 |
If-Modified-Since | 客户端在请求某一资源文件时,在头部加上If-Modified-Since(值为该资源文件的最后修改时间),服务端接收后将客户端上报的修改时间与服务器存储的文件的最后修改时间做对比,如果相同,说明资源文件没有更新,返回304状态码,告诉客户端使用原来的缓存文件。否则返回资源内容。 |
If-None-Match | 服务端在响应头部里面返回ETag信息,客户端请求时在头部添加If-None-Match(值为响应的ETag),服务端接收后判断ETag是否相同,若相同,说明资源没有更新,返回304状态码,告诉客户端使用原来的缓存文件。否则返回资源内容。 |
If-Range | 该头域与Range头域一起使用,服务端在响应头部里面返回ETag信息,客户端请求时在头部添加If-Range(值为响应的ETag),服务端接收后判断ETag是否相同,若相同,则返回状态码206,返回内容为Range指定的字节范围。若不相同,则返回状态码200,返回内容为整个实体。 |
If-Unmodified-Since | 客户端在请求某一资源文件时,在头部加上If-Modified-Since(值为该资源文件的最后修改时间),端接收后将客户端上报的修改时间与服务器存储的文件的最后修改时间做对比,如果相同,则返回资源内容,如果不相同则返回状态码412。 |
Max-Forwards | 配合TRACE、OPTIONS方法使用,限制在通往服务器的路径上的代理或网关的数量。 |
Proxy-Authorization | 代理授权 |
Range | 表示客户端向服务端请求指定范围的字节数量:Range:bytes=0-500表示请求第1个到第501个的字节数量。Range:bytes=100-表示请求第101到文件倒数第一个字节的字节数量。Range:bytes=-500表示请求最后500个字节的数量。Range可以同时指定多组(Range:bytes=500-600,601-999)。并不是所有的服务端都支持字节范围请求的,如果支持字节范围请求,服务端会返回状态码206,若不支持则会返回200,客户端需要根据状态码来判断服务端是否支持字节范围操作。此域可用于断点下载,即在断点处请求后面的内容,也可用于多线程下载同一个文件,每个线程负责一个文件的一部分下载工作,多个线程协同完成整个文件的下载。 |
Referer | 用于指定客户端请求的来源,是从搜索引擎过来的?还是从其它网站链接过来的?服务器根据此域,有时可以用做防盗链处理,不在指定范围内的来源,统统拒绝。 |
TE | 指明客户端可以接受哪些传输编码。 |
实体头域
实体头域名称 | 作用描述 |
---|---|
Allow | 指明被请求的资源所支持的方法,如GET、HEAD、PUT |
Content-Encoding | 指明实体内容所采用的编码方式 |
Content-Language | 指明实体内容使用的语言 |
Content-Length | 指明请求实体的字节数量 |
Content-Location | 可以用来为实体提供对应资源的位置 |
Content-MD5 | 指定实体内容的MD5,用于内容的完整性校验(base64的128位MD5) |
Content-Range | |
Content-Type | 指定实体的媒体类型 |
Expires | 指明实体的过期时间 |
Last-Modified | 指明实体最后被修改的时间 |
HTTP1.1的响应协议报文结构,大体上可以分为三块,即状态行、头部、消息主体。
状态行
状态行包含HTTP协议版本、状态码、原因短语三个内容,它们之间以空格间隔,并以回车+换行结束。
状态码由三位数字组成,第一位数字定义了响应类型,主要有如下五种类型的状态码
状态码类型 | 作用描述 |
---|---|
1xx | 报告(请求被接收,继续处理) |
2xx | 成功(请求被成功的接收并处理) |
3xx | 重发 |
4xx | 客户端出错(客户端错误的协议格式和不能处理的请求) |
5xx | 服务器出错(服务器无法完成有效的请求处理) |
状态码和对应的原因短语详细描述
状态码 | 原因短语 | 中文描述 |
---|---|---|
100 | Continue | 继续 |
101 | Switching Protocols | 切换协议 |
200 | OK | 成功 |
201 | Created | 已创建 |
202 | Accepted | 接受 |
203 | Non-Authoritative information | 非权威信息 |
204 | No Content | 无内容 |
205 | Reset Content | 重置内容 |
206 | Partial Content | 部分内容 |
300 | Multiple Choices | 多个选择 |
301 | Moved Permanently | 永久移动 |
302 | Found | 发现 |
303 | See Other | 见其它 |
304 | Not Modified | 没有改变 |
305 | Use Proxy | 使用代理 |
307 | Temporary Redirect | 临时重发 |
400 | Bad Request | 坏请求 |
401 | Unauthorized | 未授权的 |
402 | Payment Required | 必需的支付 |
403 | Forbidden | 禁用 |
404 | Not Found | 没有找到 |
405 | Method Not Allowed | 方法不被允许 |
406 | Not Acceptable | 不可接受的 |
407 | Proxy Authentication Required | 需要代理验证 |
408 | Request Timeout | 请求超时 |
409 | Confilict | 冲突 |
410 | Gone | 不存在 |
411 | Length Required | 长度必需 |
412 | Precondition Failed | 先决条件失败 |
413 | Request Entity Too Large | 请求实体太大 |
414 | Request-URI Too Long | 请求URI太长 |
415 | Unsupported Media Type | 不支持的媒体类型 |
416 | Requested Range Not Satisfiable | 请求范围不被满足 |
417 | Expectation Failed | 期望失败 |
500 | Internal Server Error | 内部服务器错误 |
501 | Not Implemented | 服务端没有实现 |
502 | Bad Gateway | 坏网关 |
503 | Service Unavailable | 服务不能获得 |
504 | Gateway Timeout | 网关超时 |
505 | HTTP Version Not Supported | HTTP协议版本不支持 |
响应头域
响应头域名称 | 作用描述 |
---|---|
Accept-Ranges | 服务器向客户端指明服务器对范围请求的接受度 |
Age | 从原始服务器到代理缓存形成的估算时间(以秒计,非负) |
ETag | 实体标签 |
Location | 指定重定向的URI |
Proxy-Autenticate | 它指出认证方案和可应用到代理的该URL上的参数 |
Retry-After | 如果实体暂时不可取,通知客户端在指定时间之后再次尝试 |
Server | 指明服务器用于处理请求的软件信息 |
Vary | 告诉下游代理是使用缓存响应还是从原始服务器请求 |
WWW-Authenticate | 表明客户端请求实体应该使用的授权方案 |
交互流程
整体通信其实就是发送/响应过程,一个请求过去,对方有响应内容来返回,请求发送和响应回答方式,同时 HTTP 1.1 的特点是无状态的、快速响应的,一次连接则马上就断开。HTTP 2.0 则是相反,完善了 HTTP 1.1 出现的问题,两者连接是可复用的,同时可支持并行发送,一次多个文件传递,多个文件响应,支持传递的文件大小以二进制方式,这样确保可支持更大文件,在安全性上比 HTTP 1.1上更强大,具体细节可查阅相关文档。
URL 和 URI
这里有必要提下 URL 和 URI 这个两个名词的区别。URL表示标记了一个WWW互联网资源(用地址标记),并给出了他的访问地址。而URI表示一个网络资源,仅此而已。
通信流程
具体步骤:
步骤 1:客户端通过发送 Client Hello 报文开始 SSL 通信。 报文中包含客户端支持的 SSL 的指定版本、 加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
步骤 2:服务器可进行 SSL 通信时, 会以 Server Hello 报文作为应答。 和客户端一样, 在报文中包含 SSL 版本 以及加密组件。 服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。
步骤 3:之后服务器发送 Certificate 报文。 报文中包含公开密钥证书。
步骤 4:最后服务器发送 Server Hello Done 报文通知客户端, 最初阶段的 SSL 握手协商部分结束。
步骤 5:SSL 第一次握手结束之后, 客户端以 Client Key Exchange 报文作为回应。 报文中包含通信加密中使用 的一种被称为 Pre- master secret 的随机密码串。 该报文已用步骤 3 中的公开密钥进行加密。
步骤 6:接着客户端继续发送 Change Cipher Spec 报文。 该报文会提示服务器, 在此报文之后的通信会采用 Pre- master secret 密钥加密。
步骤 7:客户端发送 Finished 报文。 该报文包含连接至今全部报文的整体校验值。 这次握手协商是否能够成功, 要以服务器是否能够正确解密该报文作为判定标准。
步骤 8:服务器同样发送 Change Cipher Spec 报文。
步骤 9:服务器同样发送 Finished 报文。
步骤 10:服务器和客户端的 Finished 报文交换完毕之后, SSL 连接就算建立完成。 当然, 通信会受到 SSL 的保护。 从此处开始进行应用层协议的通信, 即发送 HTTP 请求。
步骤 11: 应用层协议通信, 即发送 HTTP 响应。
步骤 12: 最后 由 客户 端断开连接。 断开连接时, 发送 close_ notify 报文。 上图做了一些省略, 这步之后再 发送 TCP FIN 报文来关闭与 TCP 的通信。
加密算法
常见的加密算法可以分成三类,对称加密算法,非对称加密算法和Hash算法。
对称加密
指加密和解密使用相同密钥的加密算法。对称加密算法的优点在于加解密的高速度和使用长密钥时的难破解性。假设两个用户需要使用对称加密方法加密然后交换数据,则用户最少需要2个密钥并交换使用,如果企业内用户有n个,则整个企业共需要n×(n-1) 个密钥,密钥的生成和分发将成为企业信息部门的恶梦。对称加密算法的安全性取决于加密密钥的保存情况,但要求企业中每一个持有密钥的人都保守秘密是不可能的,他们通常会有意无意的把密钥泄漏出去——如果一个用户使用的密钥被入侵者所获得,入侵者便可以读取该用户密钥加密的所有文档,如果整个企业共用一个加密密钥,那整个企业文档的保密性便无从谈起。
常见的对称加密算法:DES、3DES、DESX、Blowfish、IDEA、RC4、RC5、RC6和AES
非对称加密
指加密和解密使用不同密钥的加密算法,也称为公私钥加密。假设两个用户要加密交换数据,双方交换公钥,使用时一方用对方的公钥加密,另一方即可用自己的私钥解密。如果企业中有n个用户,企业需要生成n对密钥,并分发n个公钥。由于公钥是可以公开的,用户只要保管好自己的私钥即可,因此加密密钥的分发将变得十分简单。同时,由于每个用户的私钥是唯一的,其他用户除了可以可以通过信息发送者的公钥来验证信息的来源是否真实,还可以确保发送者无法否认曾发送过该信息。非对称加密的缺点是加解密速度要远远慢于对称加密,在某些极端情况下,甚至能比非对称加密慢上1000倍。
常见的非对称加密算法:RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)
Hash算法
Hash算法特别的地方在于它是一种单向算法,用户可以通过Hash算法对目标信息生成一段特定长度的唯一的Hash值,却不能通过这个Hash值重新获得目标信息。因此Hash算法常用在不可还原的密码存储、信息完整性校验等。
常见的Hash算法:MD2、MD4、MD5、HAVAL、SHA、SHA-1、HMAC、HMAC-MD5、HMAC-SHA1
数字证书和数字签名证书
数字证书是由权威的CA机构颁发的无法被伪造的证书,用于校验发送方实体身份的认证。解决如上问题,只需要发送方A找一家权威的CA机构申请颁发数字证书,证书内包含A的相关资料信息以及A的公钥,然后将正文A、数字证书以及A生成的数字签名发送给B,此时中间人M是无法篡改正文内容而转发给B的,因为M不可能拥有这家CA的私钥,无法随机制作数字证书。当然,如果M也申请了同一家CA的数字证书并替换发送修改后的正文、M的数字证书和M的数字签名,此时B接收到数据时,会校验数字证书M中的信息与当前通信方是否一致,发现数字证书中的个人信息为M并非A,说明证书存在替换风险,可以选择中断通信。
为什么CA制作的证书是无法被伪造的?其实CA制作的数字证书内还包含CA对证书的数字签名,接收方可以使用CA公开的公钥解密数字签名,并使用相同的摘要算法验证当前数字证书是否合法。制作证书需要使用对应CA机构的私钥,因此CA颁发的证书是无法被非法伪造的(CA的私钥泄露不在考虑讨论与考虑范围内)。
数字证书签名的基础就是非对称加密算法和数字签名,其无法伪造的特性使得其应用面较广,HTTPS中就使用了数字证书来保证握手阶段服务端传输的公钥的可靠性。
数字签名是非对称加密算法和摘要算法的一种应用,能够保证信息在传输过程中不被篡改,也能保证数据不能被伪造。使用时,发送方使用摘要算法获得发布内容的摘要,然后使用私钥对摘要进行加密(加密后的数据就是数字签名),然后将发布内容、数字签名和公钥一起发送给接收方即可。接收方接收到内容后,首选取出公钥解密数字签名,获得正文的摘要数据,然后使用相同的摘要算法计算摘要数据,将计算的摘要与解密的摘要进行比较,若一致,则说明发布内容没有被篡改。
计算机本身无法判断坐在显示器前的使用者的身份。 进一步说, 也无法确认网络的那头究竟有谁。 可见,为了 弄清究竟是谁在访问服务器, 就得让对方的客户端自报家门。 比如,就算正在访问服务器的对方声称自己是 小明, 身份是否属实这点却也无从谈起。 为确认小明本人是否真的具有访问系统的权限, 就需要核对“ 登录者 本人才知道的信息”、“ 登录者本人才会有的信息”。所以才需要以下几种验证。
常见的web攻击技术有哪些呢,如下:
1,XSS 跨站攻击技术:主要是攻击者往网页里嵌入恶意脚本,或者通过改变 HTML 元素属性来实现攻击,主要原因在于开发者对用户的变量直接使用导致进入 HTML 中会被直接编译成 JS,通常的 GET 请求通过 URL 来传参,可以在 URL 中传入恶意脚本,从而获取信息,解决方法:特殊字符过滤。
2,SQL 注入攻击:主要是就是通过把 SQL 命令插入到 Web 表单 提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令,比如 select * from test where username="wuxu" or 1=1,这样会使用户跳过密码直接登录,具体解决方案:
3,OS 命令注入攻击:系统提供命令执行类函数主要方便处理相关应用场景的功能.而当不合理的使用这类函数,同时调用的变量未考虑安全因素,就会执行恶意的命令调用,被攻击利用。主要原因是服务端在调用系统命令时采用的是字符串连接的方式,比如 a="a.txt;rm -rf *",system("rm -rf {$a}"),这会给服务端带去惨痛的代价,具体解决方案:
4,HTTP 首部注入攻击
5,邮件首部注入攻击:它允许恶意攻击者注入任何邮件头字段,BCC、CC、主题等,它允许 黑客 通过注入手段从受害者的邮件服务器发送垃圾邮件。主要是利用邮件系统传参的bug来进行攻击,解决方法:1、使用正则表达式来过滤用用户提交的数据。例如,我们可以在输入字符串中搜索(r 或 n)。2、永远不要信任用户的输入。3、使用外部组建和库
6,目录遍历攻击:目录遍历是Http所存在的一个安全漏洞,它使得攻击者能够访问受限制的目录,并在Web服务器的根目录以外执行命令。
7,远程目录包含攻击,原理就是注入一段用户能控制的脚本或代码,并让服务端执行。比如 php 中的include($filename),而此 filename 由用户传入,用户即可传入一段恶意脚本,从而对服务其造成伤害,解决方法:当采用文件包含函数的时候,不应动态传入,而应该有具体的文件名,如果动态传入,要保证动态变量不被用户所控制
8,会话劫持:这是一种通过获取用户Session ID后,使用该 Session ID 登录目标账号的攻击方法,此时攻击者实际上是使用了目标账户的有效 Session。会话劫持的第一步是取得一个合法的会话标识来伪装成合法用户,因此需要保证会话标识不被泄漏,通俗一点就是用户在登录时,唯一标示用户身份的 session id被劫持,使得攻击者可以用这个 session id 来进行登录后操作,而攻击者主要是通过 窃取:使用网络嗅探,XSS 攻击等方法获得。而第一种方式网络嗅探,我们可以通过 ssl 加密,也就是 https 来对报文进行加密,从而防止报文被截获,而第二种方式xss 攻击,方式在第一种已经给出,不再赘述。此外通过设置 HttpOnly。通过设置 Cookie 的 HttpOnly 为 true,可以防止客户端脚本访问这个 Cookie,从而有效的防止 XSS 攻击,还有就是设置 token 验证。关闭透明化Session ID。透明化 Session ID 指当浏览器中的 Http 请求没有使用 Cookie 来存放 Session ID 时,Session ID 则使用URL来传递。
9,会话固定:会话固定是会话劫持的一种,区别就是,会话固定是攻击者通过某种手段重置目标用户的SessionID,然后监听用户会话状态;用户携带sessionid进行登录,攻击者获取sessionid来进行会话,解决方案:服务端设置用户登录后的sessionid与登录前不一样即可,另外会话劫持的方法也可以用在会话固定上
10,csrf跨站伪造请求攻击:其实就是攻击者盗用了你的身份,以你的名义发送恶意请求。
总的来说,通过输出这么一篇文章,自己的对 HTTP 协议有了进一步的认知,同时也通过写作整理过程让自己对某一个知识点有很好的联想和串联,积累从点开始,然后形成面,最后就会有一个知识树生长起来。
]]>上图显示的是传统的服务端架构和客户端 App 架构对比。传统的服务端架构中最底下是一个 OS,一般是 Linux,最上面服务端的业务,而中间有非常多的层次可以在架构上,按照我们的意愿搭建中间的各个层次的衔接环节,使得架构具有足够的灵活性和扩展性。但是到了 App 就会面对一个完全不同的现状,App 的OS(Android或iOS)本质上并不是一个很瘦的像 Linux 这样的 OS,而是在 OS 上有一个很重的 App Framework,开发一个普通的客户端应用所要用到的绝大多数接口都在 Framework 里,而上面的业务也是一个非常复杂多样化的业务,最后会发现“架构”是在中间的一个非常尴尬的夹心层,因为会遇到很多在服务端架构中不需要面临的挑战。比如以下两点:
客户端 APP 与服务端在架构上是有着一定的区别,在选择对客户端架构需要谨慎对待,需要有许多权衡的条件,在此前提上,是否有一种归一的方式呢,可以分而治之,并行开发,把业务分隔成一个个单独的组件,整个架构围绕组件开发,构建也是组件,一切皆组件。答案是有的,那就是打造客户端组件框架。
客户端 APP 自身在飞速发展,APP 版本不断迭代,新功能不断增加,业务模块数量不断增加,业务上的处理逻辑越变越复杂,同时每个模块代码也变得越来越多,这就引发一个问题,所维护的代码成本越来越高,稍微一改动可能就牵一发而动全身,改个小的功能点就需要回归整个 APP 测试,这就对开发和维护带来很大的挑战。同时原来APP 架构方式是单一工程模式,业务规模扩大,随之带来的是团队规模扩大,那就涉及到多人协作问题,每个移动端软件开发人员势必要熟悉如此之多代码,如果不按照一定的模块组件机制去划分,将很难进行多人协作开发,随着单一项目变大,而且 Andorid 项目在编译代码方面就会变得非常卡顿,在单一工程代码耦合严重,每修改一处代码后都需要重新编译打包测试,导致非常耗时,最重要的是这样的代码想要做单元测试根本无从下手,所以必须要有一个更灵活的架构去代替过去单一工程模式。
同样这样的问题在我们工作具体项目中处处碰到,就拿我们组内负责的某个移动端 APP 来说,就碰到如下几个问题:
项目工程架构模式改变是大势所趋,那又该如何做呢?那就是:打造组件化开发框架,用以解决目前所面临问题,在讲解如何打造之前,需要谈谈组件化概念,组件化框架是什么。
问:什么是组件,什么是组件化?
答:在软件开发领域,组件(Component)是对数据和方法的简单封装,功能单一,高内聚,并且是业务能划分的最小粒度。举个我们生活中常见的例子就是电脑主板上每个元件电容器件,每个元件负责的功能单一、容易组装、即插即拔,但作用有限,需要一定的依赖条件才可使用。如下图:
那么同样,组件化 就是基于组件可重用的目的上,将一个大的软件系统按照分离关注点的形式,拆分成多个独立的组件,使得整个软件系统也做到电路板一样,是单个或多个组件元件组装起来,哪个组件坏了,整个系统可继续运行,而不出现崩溃或不正常现象,做到更少的耦合和更高的内聚。
问:组件化、模块化容易混淆,两者区别又是什么?
答:模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块我们相对熟悉,比如登录功能可以是一个模块,搜索功能可以是一个模块等等。而组件化就是更关注可复用性,更注重关注点分离,如果从集合角度来看的话,可以说往往一个模块包含了一个或多个组件,或者说模块是一个容器,由组件组装而成。简单来说,组件化相比模块化粒度更小,两者的本质思想都是一致的,都是把大往小的方向拆分,都是为了复用和解耦,只不过模块化更加侧重于业务功能的划分,偏向于复用,组件化更加侧重于单一功能的内聚,偏向于解耦。
问:组件化能带来什么好处?
答:简单来说就是提高工作效率,解放生产力,好处如下:
回到刚开始讲的 APP 单一工程模式,看张常见 APP 单一工程模式架构图:
上图是目前比较普遍使用的 Android APP 技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。如下图:
组件化的指导思想是:分而治之,并行开发,一切皆组件。要实现组件化,无论采用什么样的技术方式,需要考虑以下七个方面问题:
组件单独运行。因为每个组件都是高度内聚的,是一个完整的整体,如何让其单独运行和调试?
代码隔离。组件之间的交互如果还是直接引用的话,那么组件之间根本没有做到解耦,如何从根本上避免组件之间的直接引用,也就是如何从根本上杜绝耦合的产生?
组件化架构目标:告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为 AAR 包集成到“ APP 壳工程”中,组成一个完整功能的 APP。
先给出框架设计图,然后再对这七个问题进行一一解答。
从图中可以看到,业务组件之间是独立的,互相没有关联,这些业务组件在集成模式下是一个个 Library,被 APP 壳工程所依赖,组成一个具有完整业务功能的 APP 应用,但是在组件开发模式下,业务组件又变成了一个个 Application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。
各个业务组件通信是通过路由转发,如图:
这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系。
那么针对以上提出的七个问题,具体解决如下:
1,代码解耦问题
对已存在的项目进行模块拆分,模块分为两种类型,一种是功能组件模块,封装一些公共的方法服务等,作为依赖库对外提供,一种是业务组件模块,专门处理业务逻辑等功能,这些业务组件模块最终负责组装APP。
2,组件单独运行问题
通过 Gradle 脚本配置方式,进行不同环境切换。比如只需要把 Apply plugin: 'com.android.library' 切换成Apply plugin: 'com.android.application' 就可以,同时还需要在 AndroidManifest 清单文件上进行设置,因为一个单独调试需要有一个入口的 Activity。比如设置一个变量 isModule,标记当前是否需要单独调试,根据isModule 的取值,使用不同的 gradle 插件和 AndroidManifest 清单文件,甚至可以添加 Application 等 Java 文件,以便可以做一下初始化的操作。
3,组件间通信问题
通过接口+实现的结构进行组件间的通信。每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。在组件化架构设计图中 Common 组件就包含了路由服务组件,里面包括了每个组件的路由入口和跳转。
4,UI 跳转问题
可以说 UI 跳转也是组件间通信的一种,但是属于比较特殊的数据传递。不过一般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity。每个组件可以注册自己所能处理的短链的 Scheme 和 Host,并定义传输数据的格式,然后注册到统一的 UIRouter 中,UIRouter 通过 Scheme 和 Host 的匹配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注解,然后通过 APT 形成具体的逻辑代码。目前方式是引用阿里的 ARouter 框架,通过注解方式进行页面跳转。
5,组件生命周期问题
在架构图中的核心管理组件会定义一个组件生命周期接口,通过在每个组件设置一个配置文件,这个配置文件是通过使用注解方式在编译时自动生成,配置文件中指明具体实现组件生命周期接口的实现类,来完成组件一些需要初始化操作并且做到自动注册,暂时没有提供手动注册的方式。
6,集成调试问题
每个组件单独调试通过并不意味着集成在一起没有问题,因此在开发后期我们需要把几个组件机集成到一个 APP 里面去验证。由于经过前面几个步骤保证了组件之间的隔离,所以可以任意选择几个组件参与集成,这种按需索取的加载机制可以保证在集成调试中有很大的灵活性,并且可以加大的加快编译速度。需要注意的一点是,每个组件开发完成之后,需要把 isModule 设置为 true并同步,这样主项目就可以通过参数配置统一进行编译。
7,代码隔离问题
如果还是 compile project(xxx:xxx.aar) 来引入组件,我们就完全可以直接使用到其中的实现类,那么主项目和组件之间的耦合就没有消除,那之前针对接口编程就变得毫无意义。我们希望只在 assembleDebug 或者 assembleRelease 的时候把 AAR 引入进来,而在开发阶段,所有组件都是看不到的,这样就从根本上杜绝了引用实现类的问题。
目前做法是主项目只依赖 Common 的依赖库,业务组件通过路由服务依赖库按需进行查找,用反射方式进行组件加载,然后在主工程中调用组件服务,组件与组件之间调用则是通过接口+实现进行通信,后续规划通过自定义Gradle 插件,通过字节码自动插入组件的依赖进行编译打包,实现自动筛选 assembleDebug 或 assembleRelease 这两个编译命任务,只有属于包含这两个任务的命令才引入具体实现类,其他的则不引入。
一,创建工程
1,APP空壳工程
通过AndroidStudio创建一个APP空壳工程,如图:
然后在 APP 工程添加依赖具体业务组件 Module。比如:
2,具体业务组件Module
需要遵循一定组件命名规范,为何需要规范呢,因为需要通过组件命名规范来约束和保证组件的统一性和一致性,避免出现冲突。比如登陆组件,那么名称:b(类型)-ga(部门缩写)-login(组件名称),这就是我们基于共同的约定进行命名的,为后期维护和扩展都带来辨识度。
二,业务组件配置文件
1,build.gradle配置文修改。如下:
1 | if (isModule.toBoolean()) { |
这里需要有几点说明一下:
1,通过 isModule.toBoolean() 方法来进行组件间集成模式和组件模式的切换,包括模块是属于Application 还是 Library,由于集成了 ARouter,所以需要对 ARouter 配置文件进行处理。
2,如果组件模式下, 则需要重新设置 AndroidManifest.xml 文件,里面配置新的Application路径。比如Login组件单独运行 AndroidManifest 清单文件
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
3,实现组件全局应用配置类,这个类的目的是在组件加载时初始化一些组件自身的资源,如下:
1 | public class LoginApplicationDelegate implements IApplicationDelegate { |
三,路由服务
1,定义公共组件路由API和入口,通过路由服务组件查找,如图:
2,组件路由实现
每个组件对外提供什么能力,首先需要在路由服务组件创建一个接口文件,如下是登陆组件接口声明和实现。
Login 接口:
具体实现:
路由使用:比如我们想从设置页面跳转到登陆页面,使用 Login 接口里的方法,使用如下:
1 | ILoginProvider loginService = (ILoginProvider) ARouter.getInstance().build(RouterPath.ROUTER_PATH_TO_LOGIN_SERVICE).navigation(); |
总的来说,通过应用组件化框架,使得我们工作中的具体项目变得更轻、好组装、编译构建更快,不仅提高工作效率,同时自我对移动应用开发认知有进一步的提升。因为组件化框架具有通用性,特别适用于业务模块迭代多,量大的大中型项目,是一个很好的解决方案。至于组件化框架之后演化的道路,则是打造组件仓库,完善组件开发规范,丰富组件功能库,有一些粒度大的业务组件可以进一步的细化,对组件功能进行更单一的内聚,同时基于现有组件化框架,便于过度在未来打造插件化框架,进一步升级 APP 动态能力,比如热加载、热修复等,那又是另一种使用场景和设计架构了,其实组件化和插件化框架最大的区别就是在是否具备动态更新能力。
把项目简化下,github地址:DemoComponent,感兴趣的可以下过去看看。
参考文章:
]]>那么不禁要反问一句,现在从事移动开发,无论是 Android 还是 IOS,未来还有发展空间吗?还有随着移动开发越来越向大前端靠拢,甚至原本一些属于原生开发者的岗位也被前端开发人员胜任,想想在本来拥挤的房间里,还要分割本来属于自己那么点空间给他人用,变得更拥挤了,那滋味肯定是不好受了,那么未来我们的容身之地又在哪里呢?
笔者是从 2013 年底开始从事移动开发,刚毕业那么还是从事服务端开发,写着.NET 技术栈代码,至今也有接近 5 年的经验了。我想说,如果仅仅停留在表面的框架上,仅仅停留在使用别人的轮子上,而对于里面实现机制和原理不求甚解的话,那是很容易被淘汰的。但是如果,在移动开发上有一门深度的技能,比如在 Android 中你对移动架构有一定独特的见解,在性能优化上有一定的造诣,对于Android 系统体系有着清晰的认知,并且在一个行业积累了丰富的行业经验,也是亮点所在,那基本就是属于吃香的类型了。所以简单来说,对于一个概念不能停留在使用层面,要深入研究里面构造如何,为何会有这么一个概念,如果没有这个概念,那又会是呈现什么样的局面呢?开发编程也是一样,要追寻的是编程的道,而不是编程的术,别看现在各种流行框架大行其道,要是深入研究下去,一层一层拨开,你也会惊讶里面的实现机制无外乎就那么几种,套来套去,只不过一些开源库作者或组织封装的好而已。
基于目前市场表现,我们都知道下一个风口是 AI,但是作为一个移动开发者如何在即将来临的 AI 时代吃口红利呢。我的回答是:致力于做一个终身学习者,追本溯源去探寻代码世界哪些不变的道,你又会说了,哪些是道呢,简单举例下,比如编程思想、常用的设计模式、设计原则、算法和数据结构、网络通信机制、操作系统、重构原则、架构思维等等。同时在目前发展情形下,也越来越趋向全栈工程师的路线,借用之前在网上看到一篇文章的图,想进阶全栈工程师之路看需要哪些技能,如下:
从图中可以看到技能被分成基础软技能、技术软硬技能,不仅要熟悉移动端开发套路,还要对大前端技术栈也有一定要掌握,同时对于服务端开发流程也要了解,我们很多从事移动开发人员,基本一开始就是从移动端入手,对于服务端开发很多时候是没有概念,这些跟那种从服务端开发转型做移动开发相比起来就处于一定的劣势了,有些甚至不知道 Restful 是什么,还以为是一种框架呢,其实这仅仅只是服务端约定好的接口编码风格而已。
为何会想起写这么一篇文章呢,一方面这几天工作需要,组内正好想规划 2018 年 Android 技术路线,简单来说就是目前我们组处在什么样的水平程度,目前所做项目用的技术处在什么阶段,在未来一年内,项目技术迭代该如何走,走到什么程度。另一方面是从事 Android 开发这几年,一直也没好好规划自己的技术路线,想认真整理下未来进阶之路。我们都知道 Android 技术体系一直很庞大,刚开始学的时候基本是从一个点一个点开始,没有系统全局观概念,同时也是学不过来,从做上层应用开始,到做 Framework 层,然后再到系统层做驱动开发各个层面的开发者都有,绝大部分开发者都是从应用层开发,往往做到 Framework 层就浅尝辄止了,一直以来,做Android 开发有这么两个说法,如果是做应用开发,往应用架构方向发展比较合适,如果是做系统层开发,往往底层驱动比较合适。
看张图:
挑些图中几个点简单来谈谈自己的一些想法。
移动架构
移动架构是 2017 移动技术年度TOP5话题之一,从中就体会到架构是有多火,记得刚开始学 Android 时候,哪有现在那么框架,那时候谈架构的更多是在服务端开发,比如多层架构,有展示层、业务逻辑层、数据访问层这就是最简单的三层模式,Android系统则是基于事件驱动响应机制设计的单页面架构,其实跟浏览器中的窗口页面是一样的, 系统中一直有个消息轮询监听机制,哪个事件被触发了,相应的响应代码进行处理,这些处理操作是被提前注册到系统中。
最早开始的开发模式基本是基于Android 系统自带 MVC 模式,Activity 基本类似于Control 的作用了,View 和Mode 互相耦合,后来才演化出现在主流的 MVP、MVVM 模式,顺便提一句,MVVM 模式其实是在微软 WPF 技术体系中提出来。
图中显示两个方面:
展示层: MVC、MVP、MVVM、Clean、Flux、Android Architecture Components
架构层:模块化->组件化->插件化->沙盒/双开技术,比如可以双开微信,类似Docker, 每个页面都是插件,类似Vue.js中每个页面都是组件。
性能优化
简单来说,一个APP 是需要从三个方面被关注的,业务功能、符合逻辑的交互、性能响应。如果我们在使用一个 APP 时候,经常滑动时经常卡顿、时不时崩溃、有些功能设计简直非常规,比如在 Web 网站有树层级等面包屑点击,你非要在手机也搞一个类似树级点击加载,那是不是有点强人所难了,我上拉下拉、左滑右滑不行吗,非要通过点击才行吗?那么性能优化核心是什么呢?追求快、稳、省、小,关注卡顿、内存泄漏和崩溃、代码质量和逻辑、安装包大小四个方面。想进一步的了解的话,可以查阅下笔者这篇文章《Android APP 性能优化的一些思考》
APP安全
APP从代码安全、到传输安全,再到存储安全。代码可以通过混淆、加固来保证、传输安全基本基于加密算法和Token来保证传输的唯一性、存储应用不可逆加密算法进行设置、所以掌握一些密码学理论尤为重要,起码要知道哈希散列算法,对称加密和非对称加密等一些常见的加密算法。
基础进阶
我们平常在工作中碰到的View滑动冲突问题、其实通过掌握View工作机制和Android触摸事件体系就能轻易解决掉,常见解决方式有外部拦截法和内部拦截法,基于横坐标滑动距离与纵坐标滑动距离相减得出的值,判断出是左右滑动还是上下滑动。还有对于需要开发绚丽的动画效果,那么对于视图动画和属性动画一些特性必须有一定的了解。JNI 和 NDK 开发也是比较常见,特别对于一些做 SDK 项目为主的,这一块开发流程也是需要掌握,Android 中的四大组件工作机制其实底层应用的是Binder机制,我们不妨从 AIDL 这个接口来了解 Binder。
持续集成
持续集成编译环境是敏捷开发中很重要的一个组成部分,它能够有效地提高整个团队的生产效率,最大化的减少人为的出错的可能。比如,通过代码的持续提交,可以减少代码合并的痛苦,更快地与其他人代码集成,通过集成编译,能够及早地发现代码库存在的错误,并支持产品、测试等人员及时取包进行功能验证,所以对于Git、Gradle工具、Jenkins服务器需要掌握起来。
开发语言
今年可以说是 Kotlin 年,在 Google IO 之后 Kotlin 着实风光了一把,开发者对于效率的追求是 Kotlin 如此受欢迎的最大原因,而它的势头也很不错,跨平台的野心让更多人有了使用它的理由,如今看起来,它甚至比 Swift 更有前途。既然 Kotlin 已然成为 Android 世界的头等公民,与 Java 完全兼容,我们有什么理由不去拥抱它呢。
大前端
其实移动端开发也是属于前端开发,只不过原来我们所指的前端往往都是Web的前端开发人员,开发的是网站,而移动端何尝不是一种展示载体,同样有入口,只不过相比网站端移动端具体天然的可移动性、可便捷性等特性。随着 H5 兴起,原生能实现的功能同样在 H5 端也能实现,并且在体验性方面也逐渐提高,并且H5天生就具有动态性和跨平台,这也是 H5 能够一时潮流的原因之一。
设计原则
这些设计原创才是本质,才是不变的,才是我们需要真正要掌握的,开发语言特性、开发框架发展实在是太快了,我们更不上,不是有这么一句话吗,封装具体变化的,抽象起来就行,去追寻哪些不变的内容。掌握一种设计模式其实也就掌握一种解决方案,这些都是前人总结的知识结晶,基本都是基于特定领域解决特定的问题,我们需要学会在前人肩膀上解决问题。
服务端开发
最近微服务火的一塌糊涂,虽然我们基本是做移动端开发,但是基于全栈发展的趋势,对于服务端开发势必要了解,可能没有实战项目让你真刀真枪的干,但是对于微服务的结构理论、服务与服务之间通信、聚合是什么样的流程也是需要有所耳闻,在自己擅长领域深入专研的同时横向扩展关注也是需要的,不能关起门来闭门造车,两耳不闻窗外事那是不行的。
阅读源码
对于Android源码和第三方库源码阅读,可以根据自己感兴趣的类型,选择相应的源码库或模块,给自己约定一个时间点,看完之后最后有个流程图,哪些是核心类,类与类之间都有什么关系,这些开源代码实现的机制是什么,用到哪些解决思想,这些要点最终可以通过文章输出,我觉得输出倒逼输入是一种很不错的学习方式。
移动AI
AI,也称人工智能,1956年,在达特茅斯学院举行的一次会议上正式确立了人工智能的研究领域。会议的参加者在接下来的数十年间是AI研究的领军人物,他们中有许多人预言,经过一代人的努力,与人类具有同等智能水平的机器将会出现。现在来看AI这个概念很早就存在了,之前没发展很大原因是基础设施不够完善、研究成本高,现在能够大力发展是基于我们的网络带宽变大、计算机性能提升、计算成本降低等等因素备齐了。AI是需要通过算法来落地,那么对于算法理论背景就是数学,所以想进阶AI开发,就需要去学习相关数学知识,特别是线性代数和概率论这两门,是支撑很多算法的理论知识。
总的来说,技术发展能推动社会的进步,解放生产力,进而提高人的社会生产效率,创造价值。技术落地是需要商业应用场景配合,如何配合呢,就是通过每个不同商业模式来实现。最终一门技术是需要与具体使用业务紧密结合起来,如果脱离技术谈业务显得空洞,脱离业务谈技术显得偏理论,最好是两者相结合。这也就要求我们这些从事软件开发人员在追求技术积累的同时要注重业务积累,让业务驱动技术发展,用技术手段来解决实际业务问题,在技术积累中,辨别哪些是不变的道,哪些又是一时流行的而已,这就需要练就一双火眼金睛了。
]]>其实近年来,随着 Android 版本不断迭代,Google 提供的Android 系统已经越来越流畅,目前最新发布的版本是 Android 8.0 Oreo 。但是在国内大部分用户用的 Android 手机系是各大厂商定制过的版本,往往不是最新的原生系统内核,可能绝大多数还停留在 Android 5.0 系统上,甚至 Android 6.0 以上所占比例还偏小,更新存在延迟性。
由于 Android 系统源码是开放的,每个人只要遵从相应的协议,就可以对源码进行修改,那么国内各个厂商就把基于 Android 源码改造成自己对外发布的系统,比如我们熟悉的小米手机 Miui 系统、华为手机 EMUI 系统、Oppo 手机 ColorOS 系统等。由于每个厂商都修改过 Android 原生系统源码,这里面就会引发一个问题,那就是著名的Android 碎片化问题,本质就是不同 Android 系统的应用兼容性不同,达不到一致性。
由于存在着各种 Android 碎片化和兼容性问题,导致 Android 开发者在开发应用时需要对不同系统进行适配,同时每个 Android 开发者的开发水平参差不齐,写出来的应用性能也都存在不同类型的问题,导致用户在使用过程中用户体验感受不同,那么有些问题用户就会转化为 Android 系统问题,进而影响对Android 手机的评价。
今天想说的重点是Android APP 性能优化,也就是在开发应用程序时应该注意的点有哪些,如何更好地提高用户体验。一个好的应用,除了要有吸引人的功能和交互之外,在性能上也应该有高的要求,即使应用非常具有特色,在产品前期可能吸引了部分用户,但是用户体验不好的话,也会给产品带来不好的口碑。那么一个好的应用应该如何定义呢?主要有以下三方面:
业务/功能
符合逻辑的交互
优秀的性能
众所周知,Android 系统作为以移动设备为主的操作系统,硬件配置是有一定的限制的,虽然配置现在越来越高级,但仍然无法与 PC 相比,在 CPU 和内存上使用不合理或者耗费资源多时,就会碰到内存不足导致的稳定性问题、CPU 消耗太多导致的卡顿问题等。
面对问题时,大家想到的都是联系用户,然后查看日志,但殊不知有关性能类问题的反馈,原因也非常难找,日志大多用处不大,为何呢?因为性能问题大部分是非必现的问题,问题定位很难复现,而又没有关键的日志,当然就无法找到原因了。这些问题非常影响用户体验和功能使用,所以了解一些性能优化的一些解决方案就显得很重要了,并在实际的项目中优化我们的应用,进而提高用户体验。
可以把用户体验的性能问题主要总结为4个类别:
流畅
稳定
省电、省流量
安装包小
性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题,整理归类如下:
从图中可以看到,打造一个高质量的应用应该以4个方向为目标:快、稳、省、小。
快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望。
稳:减低 crash 率和 ANR 率,不要在用户使用过程中崩溃和无响应。
省:节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫。
小:安装包小可以降低用户的安装成本。
要想达到这4个目标,具体实现是在右边框里的问题:卡顿、内存使用不合理、代码质量差、代码逻辑乱、安装包过大,这些问题也是在开发过程中碰到最多的问题,在实现业务需求同时,也需要考虑到这点,多花时间去思考,如何避免功能完成后再来做优化,不然的话等功能实现后带来的维护成本会增加。
Android 应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。卡顿的场景有很多,按场景可以分为4类:UI 绘制、应用启动、页面跳转、事件响应,如图:
这4种卡顿场景的根本原因可以分为两大类:
界面绘制。主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。
数据处理。导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。
引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,所以,要解决卡顿,就要先了解 Android 系统的显示原理。
Android 显示过程可以简单概括为:Android 应用程序把经过测量、布局、绘制后的 surface 缓存数据,通过 SurfaceFlinger 把数据渲染到显示屏幕上, 通过 Android 的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕上。
我们都知道在 Android 的每个 View 绘制中有三个核心步骤:Measure、Layout、Draw。具体实现是从 ViewRootImp 类的performTraversals() 方法开始执行,Measure 和 Layout都是通过递归来获取 View 的大小和位置,并且以深度作为优先级,可以看出层级越深、元素越多、耗时也就越长。
真正把需要显示的数据渲染到屏幕上,是通过系统级进程中的 SurfaceFlinger 服务来实现的,那么这个SurfaceFlinger 服务主要做了哪些工作呢?如下:
响应客户端事件,创建 Layer 与客户端的 Surface 建立连接。
接收客户端数据及属性,修改 Layer 属性,如尺寸、颜色、透明度等。
将创建的 Layer 内容刷新到屏幕上。
维持 Layer 的序列,并对 Layer 最终输出做出裁剪计算。
既然是两个不同的进程,那么肯定是需要一个跨进程的通信机制来实现数据传递,在 Android 显示系统中,使用了 Android 的匿名共享内存:SharedClient,每一个应用和 SurfaceFlinger 之间都会创建一个SharedClient ,然后在每个 SharedClient 中,最多可以创建 31 个 SharedBufferStack,每个 Surface 都对应一个 SharedBufferStack,也就是一个 Window。
一个 SharedClient 对应一个Android 应用程序,而一个 Android 应用程序可能包含多个窗口,即 Surface 。也就是说 SharedClient 包含的是 SharedBufferStack的集合,其中在显示刷新机制中用到了双缓冲和三重缓冲技术。最后总结起来显示整体流程分为三个模块:应用层绘制到缓存区,SurfaceFlinger 把缓存区数据渲染到屏幕,由于是不同的进程,所以使用 Android 的匿名共享内存 SharedClient 缓存需要显示的数据来达到目的。
除此之外,我们还需要一个名词:FPS。FPS 表示每秒传递的帧数。在理想情况下,60 FPS 就感觉不到卡,这意味着每个绘制时长应该在16 ms 以内。但是 Android 系统很有可能无法及时完成那些复杂的页面渲染操作。Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的 60FPS。如果某个操作花费的时间是 24ms ,系统在得到 VSYNC 信号时就无法正常进行正常渲染,这样就发生了丢帧现象。那么用户在 32ms 内看到的会是同一帧画面,这种现象在执行动画或滑动列表比较常见,还有可能是你的 Layout 太过复杂,层叠太多的绘制单元,无法在 16ms 完成渲染,最终引起刷新不及时。
根据Android 系统显示原理可以看到,影响绘制的根本原因有以下两个方面:
绘制任务太重,绘制一帧内容耗时太长。
主线程太忙,根据系统传递过来的 VSYNC 信号来时还没准备好数据导致丢帧。
绘制耗时太长,有一些工具可以帮助我们定位问题。主线程太忙则需要注意了,主线程关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据,所以特别需要避免任何主线程的事情,这样应用程序才能保持对用户操作的即时响应。总结起来,主线程主要做以下几个方面工作:
UI 生命周期控制
系统事件处理
消息处理
界面布局
界面绘制
界面刷新
除此之外,应该尽量避免将其他处理放在主线程中,特别复杂的数据计算和网络请求等。
性能问题并不容易复现,也不好定位,但是真的碰到问题还是需要去解决的,那么分析问题和确认问题是否解决,就需要借助相应的的调试工具,比如查看 Layout 层次的 Hierarchy View、Android 系统上带的 GPU Profile 工具和静态代码检查工具 Lint 等,这些工具对性能优化起到非常重要的作用,所以要熟悉,知道在什么场景用什么工具来分析。
1,Profile GPU Rendering
在手机开发者模式下,有一个卡顿检测工具叫做:Profile GPU Rendering,如图:
它的功能特点如下:
一个图形监测工具,能实时反应当前绘制的耗时
横轴表示时间,纵轴表示每一帧的耗时
随着时间推移,从左到右的刷新呈现
提供一个标准的耗时,如果高于标准耗时,就表示当前这一帧丢失
2,TraceView
TraceView 是 Android SDK 自带的工具,用来分析函数调用过程,可以对 Android 的应用程序以及 Framework 层的代码进行性能分析。它是一个图形化的工具,最终会产生一个图表,用于对性能分析进行说明,可以分析到每一个方法的执行时间,其中可以统计出该方法调用次数和递归次数,实际时长等参数维度,使用非常直观,分析性能非常方便。
3,Systrace UI 性能分析
Systrace 是 Android 4.1及以上版本提供的性能数据采样和分析工具,它是通过系统的角度来返回一些信息。它可以帮助开发者收集 Android 关键子系统,如 surfaceflinger、WindowManagerService 等 Framework 部分关键模块、服务、View系统等运行信息,从而帮助开发者更直观地分析系统瓶颈,改进性能。Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载等,在 UI 显示性能分析上提供很好的数据,特别是在动画播放不流畅、渲染卡等问题上。
1,布局优化
布局是否合理主要影响的是页面测量时间的多少,我们知道一个页面的显示测量和绘制过程都是通过递归来完成的,多叉树遍历的时间与树的高度h有关,其时间复杂度 O(h),如果层级太深,每增加一层则会增加更多的页面显示时间,所以布局的合理性就显得很重要。
那布局优化有哪些方法呢,主要通过减少层级、减少测量和绘制时间、提高复用性三个方面入手。总结如下:
减少层级。合理使用 RelativeLayout 和 LinerLayout,合理使用Merge。
提高显示速度。使用 ViewStub,它是一个看不见的、不占布局位置、占用资源非常小的视图对象。
布局复用。可以通过
尽可能少用wrap_content。wrap_content 会增加布局 measure 时计算成本,在已知宽高为固定值时,不用wrap_content 。
删除控件中无用的属性。
2,避免过度绘制
过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费了多余的 CPU 以及 GPU 资源。
如何避免过度绘制呢,如下:
布局上的优化。移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片
自定义View优化。使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。
3,启动优化
通过对启动速度的监控,发现影响启动速度的问题所在,优化启动逻辑,提高应用的启动速度。启动主要完成三件事:UI 布局、绘制和数据准备。因此启动速度优化就是需要优化这三个过程:
UI 布局。应用一般都有闪屏页,优化闪屏页的 UI 布局,可以通过 Profile GPU Rendering 检测丢帧情况。
启动加载逻辑优化。可以采用分布加载、异步加载、延期加载策略来提高应用启动速度。
数据准备。数据初始化分析,加载数据可以考虑用线程初始化等策略。
4,合理的刷新机制
在应用开发过程中,因为数据的变化,需要刷新页面来展示新的数据,但频繁刷新会增加资源开销,并且可能导致卡顿发生,因此,需要一个合理的刷新机制来提高整体的 UI 流畅度。合理的刷新需要注意以下几点:
尽量减少刷新次数。
尽量避免后台有高的 CPU 线程运行。
缩小刷新区域。
5,其他
在实现动画效果时,需要根据不同场景选择合适的动画框架来实现。有些情况下,可以用硬件加速方式来提供流畅度。
在 Android 系统中有个垃圾内存回收机制,在虚拟机层自动分配和释放内存,因此不需要在代码中分配和释放某一块内存,从应用层面上不容易出现内存泄漏和内存溢出等问题,但是需要内存管理。Android 系统在内存管理上有一个 Generational Heap Memory 模型,内存回收的大部分压力不需要应用层关心, Generational Heap Memory 有自己一套管理机制,当内存达到一个阈值时,系统会根据不同的规则自动释放系统认为可以释放的内存,也正是因为 Android 程序把内存控制的权力交给了 Generational Heap Memory,一旦出现内存泄漏和溢出方面的问题,排查错误将会成为一项异常艰难的工作。除此之外,部分 Android 应用开发人员在开发过程中并没有特别关注内存的合理使用,也没有在内存方面做太多的优化,当应用程序同时运行越来越多的任务,加上越来越复杂的业务需求时,完全依赖 Android 的内存管理机制就会导致一系列性能问题逐渐呈现,对应用的稳定性和性能带来不可忽视的影响,因此,解决内存问题和合理优化内存是非常有必要的。
Android 应用都是在 Android 的虚拟机上运行,应用 程序的内存分配与垃圾回收都是由虚拟机完成的。在 Android 系统,虚拟机有两种运行模式:Dalvik 和 ART。
1,Java对象生命周期
一般Java对象在虚拟机上有7个运行阶段:
创建阶段->应用阶段->不可见阶段->不可达阶段->收集阶段->终结阶段->对象空间重新分配阶段
2,内存分配
在 Android 系统中,内存分配实际上是对堆的分配和释放。当一个 Android 程序启动,应用进程都是从一个叫做 Zygote 的进程衍生出来,系统启动 Zygote 进程后,为了启动一个新的应用程序进程,系统会衍生 Zygote 进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。其中,大多数的 RAM pages 被用来分配给Framework 代码,同时促使 RAM 资源能够在应用所有进程之间共享。
但是为了整个系统的内存控制需要,Android 系统会为每一个应用程序都设置一个硬性的 Dalvik Heap Size 最大限制阈值,整个阈值在不同设备上会因为 RAM 大小不同而有所差异。如果应用占用内存空间已经接近整个阈值时,再尝试分配内存的话,就很容易引起内存溢出的错误。
3,内存回收机制
我们需要知道的是,在 Java 中内存被分为三个区域:Young Generation(年轻代)、Old Generation(年老代)、Permanent Generation(持久代)。最近分配的对象会存放在 Young Generation 区域。对象在某个时机触发 GC 回收垃圾,而没有回收的就根据不同规则,有可能被移动到 Old Generation,最后累积一定时间在移动到 Permanent Generation 区域。系统会根据内存中不同的内存数据类型分别执行不同的 GC 操作。GC 通过确定对象是否被活动对象引用来确定是否收集对象,进而动态回收无任何引用的对象占据的内存空间。但需要注意的是频繁的 GC 会增加应用的卡顿情况,影响应用的流畅性,因此需要尽量减少系统 GC 行为,以便提高应用的流畅度,减小卡顿发生的概率。
做内存优化前,需要了解当前应用的内存使用现状,通过现状去分析哪些数据类型有问题,各种类型的分布情况如何,以及在发现问题后如何发现是哪些具体对象导致的,这就需要相关工具来帮助我们。
1,Memory Monitor
Memory Monitor 是一款使用非常简单的图形化工具,可以很好地监控系统或应用的内存使用情况,主要有以下功能:
显示可用和已用内存,并且以时间为维度实时反应内存分配和回收情况。
快速判断应用程序的运行缓慢是否由于过度的内存回收导致。
快速判断应用是否由于内存不足导致程序崩溃。
2,Heap Viewer
Heap Viewer 的主要功能是查看不同数据类型在内存中的使用情况,可以看到当前进程中的 Heap Size 的情况,分别有哪些类型的数据,以及各种类型数据占比情况。通过分析这些数据来找到大的内存对象,再进一步分析这些大对象,进而通过优化减少内存开销,也可以通过数据的变化发现内存泄漏。
3,Allocation Tracker
Memory Monitor 和 Heap Viewer 都可以很直观且实时地监控内存使用情况,还能发现内存问题,但发现内存问题后不能再进一步找到原因,或者发现一块异常内存,但不能区别是否正常,同时在发现问题后,也不能定位到具体的类和方法。这时就需要使用另一个内存分析工具 Allocation Tracker,进行更详细的分析, Allocation Tracker 可以分配跟踪记录应用程序的内存分配,并列出了它们的调用堆栈,可以查看所有对象内存分配的周期。
4,Memory Analyzer Tool(MAT)
MAT 是一个快速,功能丰富的 Java Heap 分析工具,通过分析 Java 进程的内存快照 HPROF 分析,从众多的对象中分析,快速计算出在内存中对象占用的大小,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象。
如果在内存泄漏发生后再去找原因并修复会增加开发的成本,最好在编写代码时就能够很好地考虑内存问题,写出更高质量的代码,这里列出一些常见的内存泄漏场景,在以后的开发过程中需要避免这类问题。
资源性对象未关闭。比如Cursor、File文件等,往往都用了一些缓冲,在不使用时,应该及时关闭它们。
注册对象未注销。比如事件注册后未注销,会导致观察者列表中维持着对象的引用。
类的静态变量持有大数据对象。
非静态内部类的静态实例。
Handler临时性内存泄漏。如果Handler是非静态的,容易导致 Activity 或 Service 不会被回收。
容器中的对象没清理造成的内存泄漏。
WebView。WebView 存在着内存泄漏的问题,在应用中只要使用一次 WebView,内存就不会被释放掉。
除此之外,内存泄漏可监控,常见的就是用LeakCanary 第三方库,这是一个检测内存泄漏的开源库,使用非常简单,可以在发生内存泄漏时告警,并且生成 leak tarce 分析泄漏位置,同时可以提供 Dump 文件进行分析。
没有内存泄漏,并不意味着内存就不需要优化,在移动设备上,由于物理设备的存储空间有限,Android 系统对每个应用进程也都分配了有限的堆内存,因此使用最小内存对象或者资源可以减小内存开销,同时让GC 能更高效地回收不再需要使用的对象,让应用堆内存保持充足的可用内存,使应用更稳定高效地运行。常见做法如下:
对象引用。强引用、软引用、弱引用、虚引用四种引用类型,根据业务需求合理使用不同,选择不同的引用类型。
减少不必要的内存开销。注意自动装箱,增加内存复用,比如有效利用系统自带的资源、视图复用、对象池、Bitmap对象的复用。
使用最优的数据类型。比如针对数据类容器结构,可以使用ArrayMap数据结构,避免使用枚举类型,使用缓存Lrucache等等。
图片内存优化。可以设置位图规格,根据采样因子做压缩,用一些图片缓存方式对图片进行管理等等。
Android 应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用,比较常用的解决方式如下:
提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。
代码静态扫描工具。常见工具有Android Lint、Findbugs、Checkstyle、PMD等等。
Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。
Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息。
在移动设备中,电池的重要性不言而喻,没有电什么都干不成。对于操作系统和设备开发商来说,耗电优化一致没有停止,去追求更长的待机时间,而对于一款应用来说,并不是可以忽略电量使用问题,特别是那些被归为“电池杀手”的应用,最终的结果是被卸载。因此,应用开发者在实现需求的同时,需要尽量减少电量的消耗。
在 Android5.0 以前,在应用中测试电量消耗比较麻烦,也不准确,5.0 之后专门引入了一个获取设备上电量消耗信息的 API:Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,和Systrace 一样,是一款图形化数据分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况,最后提供一些可供参考电量优化的方法。
除此之外,还有一些常用方案可提供:
计算优化,避开浮点运算等。
避免 WaleLock 使用不当。
使用 Job Scheduler。
应用安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高,因此,减小安装包大小可以让更多用户愿意下载和体验产品。
常用应用安装包的构成,如图所示:
从图中我们可以看到:
assets文件夹。存放一些配置文件、资源文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。
res。res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。
META-INF。保存应用的签名信息,签名信息可以验证 APK 文件的完整性。
AndroidManifest.xml。这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。
classes.dex。Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。
resources.arsc。记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。
减少安装包大小的常用方案
代码混淆。使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
资源优化。比如使用 Android Lint 删除冗余资源,资源文件最少化等。
图片优化。比如利用 AAPT 工具对 PNG 格式的图片做压缩处理,降低图片色彩位数等。
避免重复功能的库,使用 WebP图片格式等。
插件化。比如功能模块放在服务器上,按需下载,可以减少安装包大小。
性能优化不是更新一两个版本就可以解决的,是持续性的需求,持续集成迭代反馈。在实际的项目中,在项目刚开始的时候,由于人力和项目完成时间限制,性能优化的优先级比较低,等进入项目投入使用阶段,就需要把优先级提高,但在项目初期,在设计架构方案时,性能优化的点也需要提早考虑进去,这就体现出一个程序员的技术功底了。
什么时候开始有性能优化的需求,往往都是从发现问题开始,然后分析问题原因及背景,进而寻找最优解决方案,最终解决问题,这也是日常工作中常会用到的处理方式。
]]>随着公司业务不断发展,移动开发项目越来越多,项目任务时间紧,我们内部开发流程是以项目为导向,有别于一般公司对产品不断迭代的做法,但移动端开发人员资源有限,需要在不同项目之间做业务场景切换开发,就会经常出现项目完成时间 Delay。面对这样的问题,我们该如何去解决呢?现在了解到的现状是每个业务组都有配备 Web 前端开发人员,那么是否能把涉及到业务模块分发给具体业务组 Web 前端开发人员去开发,剥离业务模块,我们移动端开发人员则专注于框架的开发或者手机端设备能力开发,比如可支持调用摄像头,监听网络状态变化,提供地理位置信息等等,有没有这样一套适合的解决方案呢,答案当然是有的。我们引入了可利用 Web 前端能力和移动端操作系统原生能力相结合开发模式,叫做 Hybrid 混合开发。
为何选择 Hybrid 开发模式
在实践过程中碰到什么问题和解决
小结
1,目前工作中碰到的问题
随着公司业务飞速发展,移动端定制的项目越来越多,同时每个项目的业务逻辑呈现出复杂化和差异化特点,每个项目都需要提供 Android 版本和 IOS 版本,增加开发成本,开发周期往往又会被拖长。同时近年来前端技术蓬勃发展,HTML5 大行其道,很多主流 APP 厂商都利用 HTML5 前端能力来编写业务模块并结合原生设备能力进行混合开发,常见的比如淘宝、京东、微信、携程等等。虽然目前业务项目多,但是用户交互体验要求不高,常见页面也是列表,表单居多,适合充分利用HTML 5能力,因此引入Hybrid 混合开发模式,这样只需要 Web 前端开发人员写一遍前端业务代码,却能同时在Android 系统和 IOS 系统中执行。
2,Web APP、Hybrid APP、Native APP 对比
目前主流应用程序大体分为三类:Web App、Hybrid App、 Native App,如图:
Web APP
Web App 指采用Html5 语言写出的 App,不需要下载安装。类似于现在所说的轻应用。生存在浏览器中的应用,基本上可以说是触屏版的网页应用。
优点
(1)开发成本低,更新快
(2)更新无需通知用户,不需要手动升级
(3)能够跨多个平台和终端
缺点:
(1)临时性的入口
(2)无法获取系统级别的通知,提醒,动效等等
(3)用户留存率低
(4)设计受限制诸多
(5)体验较差
Hybrid App
Hybrid App 从外观上来看是一个Native App ,实则只有一个UIWebView,里面访问的是一个Web App ,如新闻类和视频类的应用普遍采取该策略:Native 的框架加上Web 的内容。不同于Native App 需要针对不同的平台使用不同的开发语言(如使用Objective-C、Swift开发iOS应用,使用Java等开发Android应用),Hybrid App 允许开发者仅使用一套网页语言代码(HTML5+CSS+JavaScript),即可开发能够在不同平台上部署的类原生应用 。由于Hybrid App 结合了Native app良好用户交互体验和Web App 跨平台开发的优势,能够显著节省移动应用开发的时间和成本,Hybrid App 得到越来越多公司的青睐。
按照网页语言和程序语言的混合,Hybrid App 通常可以分为三种类型:
多View混合型:Native View 和 Web View 独立展示,交替出现。 其应用主体通常是Native App,Web技术作为补充。即在需要的时候,将 Web View作为独立的 View 运行,在 Web View内完成相关的展示操作。开发难度与Native App相当.比如:微信里的公众号文章使用的是Web View 。
单View混合型:在同一个View 内,Native View 和Web View 为层叠关系,同时出现。开发成本较高,难度较大,但是体验较好。比如:百度搜索同时实现充分的灵活性和较好的用户体验。
Web主体型:应用主体是Web View ,穿插 Native 功能,主要以网页语言编写。整体开发难度低,基本可以实现跨平台,而用户体验好坏,主要取决于底层中间件的交互与跨平台能力。比如:项目管理工具 Basecamp 使用Web view呈现内容,调用系统原生 API 实现界面导航等功能来提高用户体验。
Hybrid App 也并非是完美的解决方案。由于其使用 HTML5,某些依赖于复杂的原生功能或者繁重的过渡动画的应用会出现卡顿。同时,为了模拟Native App 的UI和感官,需要投入额外的时间和精力;尽管可以跨平台,但是并不能完全支持所有的设备和操作系统。最后,如果应用的体验不够原生化,如一个简单的网站,则还有被Apple App Store拒绝的风险。
Native App
Native APP 指的是原生程序,一般依托于操作系统,有很强的交互,是一个完整的 App,可拓展性强。需要用户下载安装使用。
优点:
(1)打造完美的用户体验,性能稳定
(2)操作速度快,上手流畅
(3)访问本地资源(通讯录,相册)
(4)设计出色的动效,转场,
(5)拥有系统级别的贴心通知或提醒,用户留存率高
缺点:
(1)分发成本高(不同平台有不同的开发语言和界面适配)
(2)维护成本高(例如一款App已更新至V5版本,但仍有用户在使用V2, V3, V4版本,需要更多的开发人员维护之前的版本)
(3)更新缓慢,根据不同平台,提交–审核–上线 等等不同的流程,需要经过的流程较复杂
三者技术特性
如下图表中对比了Native App、 Hybrid App、Web App在不同方面的表现,可以根据实际情况选择最佳的解决方案。
3,主流 APP Hybrid 应用比例
那么在实际应用场景中,有哪些选择了Hybrid app呢?实际上,我们很可能使用过很多Hybrid app,却并没有意识到它们是借了Native台子唱戏的Web app。根据Appcelerator的官网,目前单是运行基于它的平台搭建的Hybrid app的设备就有近2.86亿台。国外常见的有LinkedIn、Yelp、Netflix、Wunderlist ,国内主流的大厂基本也是采用了Hybrid 模式,应该是应用很广泛,同时技术上也是成熟稳定。
4,选择 Hybrid 混合开发的原因
Hybrid 开发模式在开发页面 UI 上有天生的便利,而原生的则如果需要一个比较华丽的界面,就需要花很长的时间去开发。
在业务上,看具体情况,有些简单业务在 Web上就可以处理,而如果涉及到复杂的业务,则可以用原生来写。
在基本能力上,原生的强,可以提供手机端独有的特性,但 Hybrid 则需要依赖 Javascript 中间层进行转化获取设备能力。
对于少界面,重业务的可以用原生,对于多界面,重效果的,可以用 Web 方式进行开发。
项目背景介绍
目前在一个项目实行的开发模式就是 Hybrid 混合开发,Web 技术与 Android 原生能力结合开发,Web 技术负责界面开发和相关业务, Android 原生能力则提供手机端特有设备能力,比如调用摄像头,网络状态监听,数据库操作等等。但这个项目的特殊性相关业务与我们提供的 Android 原生插件能力高度耦合,比如为这个项目提供数据库插件就是专门定制开发的,对于 Excel 插件的能力也是高度依赖一机一档相关字段,这跟我们选型用Hybrid 混合开发模式 的初心是相背离。我们初心是希望 Web 开发人员只需要专注于业务开发和界面绘制,原生部分则是提供相应的Android 设备能力集即可,每个插件跟业务是完全无关,这样就可以做到原生开发和Web开发互相解耦,两者之间通过接口隔离即可。
实践过程中碰到的问题
无论如何,一机一档项目是第一个应用 Hybrid 混合开发进行实战的项目,遇到的问题或者坑都是很正常,积极面对解决,并且不断进行总结和反思。把之前碰到的问题,简单罗列总结下:
开发人员调试困难问题。前端人员在开发时候是编写HTML5页面,所运行的环境跟 PC 端有很大的不同,因为需要运行在具体手机的环境上,因此需要每次编写完,需要通过移动端人员集成打包出一个APP 包进行安装验证,每新增或修改一个页面就需要重新打包验证,每次都需要集成测试,步骤繁琐,效率低下。
项目集成测试问题。Android 系统 Webview 和 PC 端浏览器内核版本差异问题导致加载效果不一致。
前端开发框架兼容问题。前端开发人员技术选型是基于 Vue.js 框架,这是一个渐进式 Javascript 框架,刚开始不支持。
文档不规范问题。在前期开发阶段,文档提供不详细,开发人员使用规则不清楚,导致沟通成本增加。
Webview 性能问题。
如何解决
关于调试困难问题。提供一个调试工具叫做 Chrome DevTool,通过 Inspect 模式加载手机端里的 HTML5 页面,为何选择用 Chrome,因为Chrome 是目前主流前端开发调试利器,不仅能支持 Web 端开发,对于 HTML5 页面调试开发同样是能监听到 Javascript 报错或 CSS 报错,对于资源、网络、日志、内存等等,都是一步到位。同时在 APP 里提供一个在线调试环境,就是 Web 前端开发人员布置一个站点,在手机端通过 IP 地址远程访问站点,这样就可以在手机端实时看到刚刚修改内容是什么。
关于项目集成测试问题。在集成测试阶段,对Android 系统 Webview 和 PC 端浏览器内核版本区别有进一步认识,在Android 5.0 之前选用的是 Webkit 内核来加载 Web 资源文件,而在 Android 5.0 之后,则选用 Chromium 作为内核来加载,那么在为 PC 端浏览器端,如果你选择的是 Chorme 作为你默认浏览器的话,它的内核也是 Chromium 。尽管两者内核类型一样,都是 Chromium ,但两者加载 Javascript 效果上表现也不一样,比如最新浏览器版本可支持 ES 6 特性,但是在最新版的手机上就不一定 ES 6特性,目前通过调查 Android 5.0 之前的系统市场占有率,发现比例为不到20%,暂时适配到 Android 5.0 版本。
关于前端开发框架兼容问题。刚开始选用 Hybrid 开发模式时,对于公司内部 前端开发人员选用何种前端框架不甚了解,我们这边提供的 Demo 则是最原始的 HTML + Javascript + CSS 写法,以为前端人员只需要简单了解下就能上手,但在实践中发现却不是这样的。他们选型的前端技术是基于 Vue.js ,因为 Vue.js 是需要编译打包,生成发布的内容是混淆过的HTML + Javascript ,里面 Javascript 文件加载顺序使得我们开发 Javascript 插件调用引起问题,那样就会导致前端人员在调用具体插件能力时候,发现这个插件里的某个方法还没定义,就导致页面数据出错。后来通过了解 Vue.js 开发方式,调整项目工程中 Javascript 执行顺序, 确保具体插件调用在 Vue.js 执行前触发。
关于文档不规范问题。在前期开发阶段,前端人员没有统一查找目前已有插件能力的地方,仅仅根据我们提供的 Javascript 文件里的方法注释,虽然是针对每个方法的 Demo 用法,但是在实际开发中,前端开发人员也会调用出错。不是这个方法回调方法写错,就是参数类型传入传错,这样就导致的一个结果,前端开发人员不断地过来询问这个方法是如何调用的,我明明已经根据你的 Demo 写法进行编码了,为何还是报错的,前期的沟通成本还是很高。所以需要一个提供统一文档地方,里面写明了具体配置如何,写法如何,怎么是一步一步走,基本上可以避免类似的错误,更好的提高工作效率,减少沟通成本,所以一个规范的文档是很有必要的。
关于 WebView 性能加载问题。这是在解决 WebView 加载 HTML + Javascript + CSS 等资源时发现一个白屏问题,同时用 HTML5 做页面本身就会比原生加载来的慢。为了提高用户体验,在加载等待时,提供一个加载框来提示,等 HTML 资源文件全部渲染完毕后,等待框再消失,这样就可以避免一定的白屏现象。
整体来说,为何会选择 Hybrid 混合开发模式是基于当前业务场景需要,技术是服务于业务发展,业务场景变化导致技术解决方案的选型也需要相应变化。面对以项目导向的开发现状,不能一昧追求最新最酷的技术,也不能对过时的技术方案过分保守,应该需要对当前业务场景进行判断,选择合适的解决方案才最佳的策略,没有一劳永逸的技术手段,只有时刻变化的业务需求和不断更新迭代技术方案。通过在一机一档项目中实战,面对问题,积极解决问题,也正是在解决问题过程中,产生新的想法和尝试,不断地完善框架能力,使得框架功能越来越全,进而更好的服务于业务开发问题,提高业务响应能力,降低开发成本,提升工作效率。
]]>什么是单元测试
小结
单元测试本质上也是代码,与普通代码的区别在于它是验证代码正确性的代码。可简单做个定义:单元测试是开发人员编写的、用于检测在特定条件下目标代码正确性的代码。
软件开发天生就具有复杂性,没人敢打包票说自己写的代码一点问题都没有,或者不经测试就能保证代码正确运行,可能你在这个执行路径下能够执行,殊不知还有其他路径,有一一去验证过吗,因此,要保证程序的正确性就必须要对我们代码进行严格测试。
举个简单例子:比如有个计算类,里面有个 add 方法,操作就是两个数进行相加。
1 | public class Calculator { |
常规做法:假如你写好了这个方法,你想进行验证 add 方法的正确性,需要写个使用 add 方法的 main 函数,首先实例化 Calculator 类,然后调用 add 方法并传入两个参数,比如 1 和 2。然后你运行这个工程,看得出结果是否为 3 ,如果是 3 ,则表明我这个方法写的没有错误,可能就不测试了,就继续开发后续的功能,如果不是 3 ,则返回去看看代码中哪里出错了,重新进行调试,甚至有时候肉眼还看不出代码哪里出错,此时就引入断点去查看,在此期间,很大一部分时间就花在断点、调试、运行上。
单元测试做法:首先会利用 JUnit 测试框架(至于这个框架后面介绍)写一段测试代码,如下:
1 | public class CalculatorTest { |
这里的 CalculatorTest 是 Calculator 对应的测试类,这里的 testAdd 对应着 add 的测试方法,进行测试一般分为三步骤:
看到 Assert 这个关键词了吗,这里可以理解为断言或者期望值,根据入参的值,期望有个什么值输出,而不是靠肉眼去验证是不是自己想要的值,是直接通过判断值是否相等性来验证会具有更客观性。
以上介绍的只是单元测试一点点,那它能给我们带来哪些更多好处呢?
通常我们在做任何工作会先考虑它的回报,编写代码更是如此。如果单元测试的作用不大,没有人会愿意再写一堆无用的代码,那么单元测试到底能够给我们带来什么优点呢?如下:
等等,讲了这么多优点,无非就是良好的接口设计、正确性、可回归、可测试、完善的调用文档、高内聚、低耦合,这些优点已经足以让我们对单元测试重视起来了,但是个人觉得还有更重要的原因。
很多开发人员不写单元测试,最重要的一个原因是他们并不知道单元测试能够带来什么好处,甚至根本不了解单元测试这个词,那自然就像平行线般与之毫无交集。还有一个比较重要的原因是一些开发人员的编程思想还处在一个相对初级的阶段,开发软件只管实现功能,什么高内聚、低耦合、重构、设计、可测试等认为太过专业,对于这些名词以及意义还不了解,这自然不会考虑使用了。还有一些非思想层面的理由,如下:
JUnit 是一个 Java 语言的单元测试框架,它是 xUnit 单元测试架构体系的一个实例,用于编写和运行可重复的测试。它包括以下特性:
TestNG 是一个测试框架,其灵感来自 JUnit 和 NUnit ,但引入了一些新的功能,使其功能更强大,使用更方便。TestNG 消除了大部分的旧框架的限制,使开发人员能够编写更加灵活和强大的测试。 因为它在很大程度上借鉴了Java注解( JDK5.0 引入的)来定义测试,它也可以显示如何使用这个新功能在真实的Java语言生产环境中。
特点如下:
因为 JUnit 测试框架是基于 Java 语言,当然 Android 开发也是基于 Java 语言,所以在 Android 中我们可以用 Junit4 单元测试框架进行回归测试,但同时,Google 也提供了一个 AndroidJUnit4 测试框架,看名字就知道它是基于 JUnit 4 框架适合在 Android 环境中做单元测试。
那么,AndroidJUnit4 和 Junit4 有什么区别呢?很大一个区别在于:
1,AndroidJUnit4 测试可以在真机的环境下进行。比如你要测文件读取SD卡,或者操作 SqlLite 数据库,这些条件只有在真机上才有的,此时你用 AndroidJUnit4 框架测试,可以直接跑起来用真实的环境做相应的单元测试。
2,JUnit4 测试是运行在工程项目中,也就是在编译阶段。此时如果想要模拟 Android 环境,比如我想用 JUnit4 来测试 Activity 类,那么就需要引用第三方库来支持,引用 Mockito 和 Robolectric 框架来模拟 Android 环境进行相应的单元测试。
所以何时用 AndroidJUnit4 和 JUnit4 不同的框架进行单元测试,就看你待测试的方法前置条件是什么,然后做不同的选择。
总的来说,单元测试不是集成测试,单元测试只是测试一个方法单元,不是测试一整个流程。集成测试是一种End To End的系统测试,测试相关模块集成在一起是否能够按照预期工作,一般都是接口或者功能层面的测试,可能会依赖很多系统因素,测试的代码逻辑一般比较复杂,运行时间会比较长,出错之后的修复成本高。单元测试则是开发者在集成测试之前就已经进行自测过,同时呢,进行单元测试之后,对于某个方法的执行路径组合进行了一一验证,它只关注三个目标:
主要分为以下几个部分
百度百科里是这么定义插件的:「 是一种遵循一定规范的应用程序接口编写出来的程序,只能运行在程序规定的系统平台下,而不能脱离指定的平台单独运行。」,也就是说,插件可以提供一种动态扩展能力,使得应用程序在运行时加载原本不属于该应用的功能,并且做到动态更新和替换。
那么在 Android 中,何为「 插件化 」,顾名思义,就是把一些核心复杂依赖度高的业务模块封装成独立的插件,然后根据不同业务需求进行不同组合,动态进行替换,可对插件进行管理、更新,后期对插件也可进行版本管理等操作。在插件化中有两个概念需要讲解下:
宿主
所谓宿主,就是需要能提供运行环境,给资源调用提供上下文环境,一般也就是我们主 APK ,要运行的应用,它作为应用的主工程所在,实现了一套插件的加载和管理的框架,插件都是依托于宿主的APK而存在的。
插件
插件可以想象成每个独立的功能模块封装为一个小的 APK ,可以通过在线配置和更新实现插件 APK 在宿主 APK 中的上线和下线,以及动态更新等功能。
那么为何要使用插件化技术,它有何优势,能给我们带来什么样好处,这里简单列举了以下几点:
首先我们要知道插件化技术是属于比较复杂一个领域,复杂点在于它涉及知识点广泛,不仅仅是上层做应用架构能力,还要求我们对 Android 系统底层知识需要有一定的认知,这里简单罗列了其中会涉及的知识点:
首先,要介绍的是 Binder ,我们都知道 Android 多进程通信核心就是 Binder ,如果没有它真的寸步难行。 Binder 涉及两层技术,你可以认为它是一个中介者模式,在客户端和服务器端之间, Binder 就起到中介的作用。如果要实现四大组件的插件化,就需要在 Binder 上做修改, Binder 服务端的内容没办法修改,只能改客户端的代码,而且四大组件的每个组件的客户端都不一样,这个就需要深入研究了。学习Binder的最好方式是 AIDL ,这方面在网上有很多资料,最简单的方式就是自己写个 aidl 文件自动生成一个 Java 类,然后去查看这个Java类的每个方法和变量,然后再去看四大组件,其实都是跟 AIDL 差不多的实现方式。
其次,是 App 打包的流程。代码写完了,执行一次打包操作,中途经历了资源打包、 Dex 生成、签名等过程。其中最重要的就是资源的打包,即 AAPT 这一步,如果宿主和插件的资源id冲突,一种解决办法就是在这里做修改。
第三, App 在手机上的安装流程也很重要。熟悉安装流程不仅对插件化有帮助,在遇到安装 Bug 的时候也非常重要。手机安装 App 的时候,经常会有下载异常,提示资源包不能解析,这时需要知道安装 App 的这段代码在什么地方,这只是第一步。第二步需要知道, App 下载到本地后,具体要做哪些事情。手机有些目录不能访问, App 下载到本地之后,放到哪个目录下,然后会生成哪些文件。插件化有个增量更新的概念,如何下载一个增量包,从本地具体哪个位置取出一个包,这个包的具体命名规则是什么,等等。这些细节都必须要清楚明白。
第四,是 App 的启动流程。 Activity 启动有几种方式?一种是写一个 startActivity ,第二种是点击手机 App ,通过手机系统里的 Launcher 机制,启动 App 里默认的 Activity 。通常, App 开发人员喜闻乐见的方式是第二种。那么第一种方式的启动原理是什么呢?另外,启动的时候,Main 函数在哪里?这个 Main 函数的位置很重要,我们可以对它所在的类做修改,从而实现插件化。
第五点更重要,做 Android 插件化需要控制两个地方。首先是插件 Dex 的加载,如何把插件 Dex 中的类加载到内存?另外是资源加载的问题。插件可能是 Apk 也可能是 so 格式,不管哪一种,都不会生成 R.id ,从而没办法使用。这个问题有好几种解决方案。一种是是重写 Context 的 getAsset 、 getResource 之类的方法,偷换概念,让插件读取插件里的资源,但缺点就是宿主和插件的资源 id 会冲突,需要重写 AAPT 。另一种是重写 AMS中保存的插件列表,从而让宿主和插件分别去加载各自的资源而不会冲突。第三种方法,就是打包后,执行一个脚本,修改生成包中资源id。
第六点,在实施插件化后,如何解决不同插件的开发人员的工作区问题。比如,插件1和插件2,需要分别下载哪些代码,如何独立运行?就像机票和火车票,如何只运行自己的插件,而不运行别人的插件?这是协同工作的问题。火车票和机票,这两个 Android 团队的各自工作区是不一样的,这时候就要用到 Gradle 脚本了,每个项目分别有各自的仓库,有各自不同的打包脚本,只需要把自己的插件跟宿主项目一起打包运行起来,而不用引入其他插件,还有更厉害的是,也可以把自己的插件当作一个 App 来打包并运行。
上面介绍了插件化的入门知识,一共六点,每一点都需要花大量时间去理解。否则,在面对插件化项目的时候,很多地方你会一头雾水。而只要理解了这六点核心,一切可迎刃而解。
在Android中应用插件化技术,其实也就是动态加载的过程,分为以下几步:
Android 项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:
第一点, Android 中 NDK 中其实就使用了动态加载,动态加载 .so 库并通过 JNI 调用其封装好的方法。后者一般是由 C/C++ 编译而成,运行在 Native 层,效率会比执行在虚拟机层的 Java 代码高很多,所以 Android 中经常通过动态加载 .so 库来完成一些对性能比较有需求的工作(比如 Bitmap 的解码、图片高斯模糊处理等)。此外,由于 .so 库是由 C/C++ 编译而来的,只能被反编译成汇编代码,相比中 dex 文件反编译得到的 Smali 代码更难被破解,因此 .so 库也可以被用于安全领域。
其二,“基于 ClassLoader 的动态加载 dex/jar/apk 文件”,就是我们指在 Android 中 动态加载由 Java 代码编译而来的 dex 包并执行其中的代码逻辑,这是常规 Android 开发比较少用到的一种技术,目前说的动态加载指的就是这种。
Android 项目中,所有 Java 代码都会被编译成 dex 文件,Android 应用运行时,就是通过执行 dex 文件里的业务代码逻辑来工作的。使用动态加载技术可以在 Android 应用运行时加载外部的 dex 文件,而通过网络下载新的 dex 文件并替换原有的 dex 文件就可以达到不安装新 APK 文件就升级应用(改变代码逻辑)的目的。
所以说,在 Android 中的 ClassLoader 机制主要用来加载 dex 文件,系统提供了两个 API 可供选择:
在 Android 中实现插件化框架,需要解决的问题主要如下:
下面分析几个目前主流的开源框架,看看每个框架具体实现思路和优缺点。
DL 动态加载框架 ( 2014 年底)
是基于代理的方式实现插件框架,对 App 的表层做了处理,通过在 Manifest 中注册代理组件,当启动插件组件时,首先启动一个代理组件,然后通过这个代理组件来构建,启动插件组件。 需要按照一定的规则来开发插件 APK,插件中的组件需要实现经过改造后的 Activity、FragmentActivity、Service 等的子类。
优点如下:
缺点如下:
DroidPlugin ( 2015 年 8 月)
DroidPlugin 是 360 手机助手实现的一种插件化框架,它可以直接运行第三方的独立 APK 文件,完全不需要对 APK 进行修改或安装。一种新的插件机制,一种免安装的运行机制,是一个沙箱(但是不完全的沙箱。就是对于使用者来说,并不知道他会把 apk 怎么样), 是模块化的基础。
实现原理:
插件 Host 的程序架构:
优点如下:
缺点如下:
Small ( 2015 年底)
Small 是一种实现轻巧的跨平台插件化框架,基于“轻量、透明、极小化、跨平台”的理念,实现原理有以下三点。
架构图:
优点如下:
缺点如下:
与其他主流框架的区别:
1 | DyLA : Dynamic-load-apk @singwhatiwanna |
DyLA | DiLA | ACDD | DyAPK | DPG | APF | Small | |
---|---|---|---|---|---|---|---|
加载非独立插件 | × | x | √ | √ | × | √ | √ |
加载.so后缀插件 | × | × | ! | × | × | × | √ |
Activity生命周期 | √ | √ | √ | √ | √ | √ | √ |
Service动态注册 | × | × | √ | × | √ | √ | x |
资源分包共享 | × | × | ! | ! | × | ! | √ |
公共插件打包共享 | × | × | × | × | × | × | √ |
支持AppCompat | × | × | × | × | × | × | √ |
支持本地网页组件 | × | × | × | × | × | × | √ |
支持联调插件 | × | x | × | × | × | × | √ |
ACDD | DyAPK | APF | Small | |
---|---|---|---|---|
插件Activity代码无需修改 | √ | √ | √ | √ |
插件引用外部资源无需修改name | × | × | × | √ |
插件模块无需修改build.gradle | × | x | × | √ |
VirtualAPK (2017年 6 月 )
VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件方面。
架构图:
实现思路:
VirtualAPK 对插件没有额外的约束,原生的 apk 即可作为插件。插件工程编译生成 apk后,即可通过宿主 App 加载,每个插件 apk 被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的 App 一样运行。
特性如下:
四大组件均不需要在宿主manifest中预注册,每个组件都有完整的生命周期。
theme
和LaunchMode
,支持透明主题;start
、stop
、bind
和unbind
,并支持跨进程bind插件中的Service;CRUD
和call
方法等,支持跨进程访问插件中的Provider。自定义 View
,支持自定义属性和style
,支持动画;PendingIntent
以及和其相关的Alarm
、Notification
和AppWidget
;Application
以及插件manifest中的meta-data
;so
。优秀的兼容性
AMS
和IContentProvider
,hook 过程做了充分的兼容性适配。入侵性极低
如下是 VirtualAPK 和主流的插件化框架之间的对比。
特性 | DynamicLoadApk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件无需在宿主manifest中预注册 | √ | × | √ | √ | √ |
插件可以依赖宿主 | √ | √ | √ | × | √ |
支持 PendingIntent | × | × | × | √ | √ |
Android 特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
RePlugin (2017 年 7 月)
RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。
框架图:
主要优势有: 极其灵活:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件 非常稳定:Hook 点仅有一处(ClassLoader),无任何 Binder Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的 Android ROM。 特性丰富:支持近乎所有在“单品”开发时的特性。包括静态 Receiver、 Task-Affinity 坑位、自定义 Theme、进程坑位、AppCompat、DataBinding等。 易于集成:无论插件还是主程序,只需“数行”就能完成接入。 管理成熟:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等。 数亿支撑:有 360 手机卫士庞大的数亿用户做支撑,三年多的残酷验证,确保App用到的方案是最稳定、最适合使用的。
主要是测试各个框架之间上手的容易度如何,并做不同对比,这边写了两个 Demo 例子,一个是基于 Small 框架,一个基于 VirtualAPK 框架,从中能看出不同。
Small 实践
要引用官方最新的版本,不然在宿主和插件合并build.gradle
的时候会出现一个 BUG,这是个坑位,注意行走。其次在模块命名上要遵循一定的规则,比如业务模块用 app.* ,公共库模块用 lib.* ,相当于包名 .app.,.lib. 。每次在插件中添加一个 activity 组件,都需要在宿主中配置路由,然后在重新编译插件一遍,不然直接运行的话,在宿主中是找到新添加的 activity 组件,会报该组件没在系统 manifest 中,所以每次新增或修改建议插件都重新编译一遍。官方里说了,对于 Service 支持不太友好,就没去实践了。
VirtualAPK 实践 有个坑需要注意的是构建环境,官方说明是要以下版本环境,Gradle 2.14.1 和 com.android.tools.build 2.1.3, 之前编译的是用最新的Gradle版本,导致一直有问题,至于是否有其他问题,可以看官方文档。
具体代码 Small Demo :https://github.com/cr330326/MySmall
VirtualAPK Demo :https://github.com/cr330326/MyVirtualAPKDemo
正如开头所说,要实现插件化的框架,无非就是解决那典型的三个问题:插件代码如何加载、插件中的组件生命周期如何管理、插件资源和宿主资源冲突怎么办。每个框架针对这三个问题,都有不同的解决方案,同时呢,根据时间顺序,后出来的框架往往都会吸收已经出的框架精髓,进而修复那些比较有里程碑意义框架的不足。但这些框架的核心思想都是用到了代理模式,有的在表面层进行代理,有的则在系统应用层进行代理,通过代理达到替换和瞒天过海,最终让 Android 系统误以为调用插件功能和调用原生开发的功能是一样的,进而达到插件化和原生兼容编程的目的。
2,包建强的无线技术空间,写给Android App 开发人员看的 Android 底层知识 置顶8篇
]]>因为工作原因,最近需要研究Cordova框架,看了其中的源码和实现方式,当场在看的时候马上能理解,但是事后再回去看相关源码时候却发现之前理解的内容又忘记了,又不得不重新开始看,所以总觉得需要记录下来,这样也表明之前也是学习过,俗话说「好记性不如烂笔头 」,想必也是体现了笔记的重要性。
为何要用Cordova
什么是Cordova
Cordova中UML类图
Cordova实现机制
小结
随着移动互联网的发展,现在基本是APP满天飞,不知在大家印象中,如果我去下载一个APP,那么基本都能看到有两种选择,一种是Android版本,一种是IOS版本。不管我的手机是哪种操作系统,安装完一个APP之后,后续如果有新的版本发布的时候,我还必须去更新,才能享用新版本里的功能,比如我装了“京东”这个APP,前几天正好碰到“618”活动,那么之前一个月APP Store就提醒我要去更新最新的APP版本,以免错过“618”活动中新的功能使用。相对来说IOS系统更新APP比起Android系统用户体验会好一点,但是还是稍显麻烦点。
那么有没有一种方式,我只需要开发一个APP版本,就能去适配通用的操作系统呢,不仅可以适配Android、IOS,还可以适配其他系统,比如Windows Phone、 Palm WebOS、Blackberry等等。有,Cordova就能提供这种能力,代码写一次,就能到处运行,跟我们日常开发网站效果一样,基于写Web APP,根据输出平台要求不同,就能提供不同类型的安装包。Cordova其设计初衷是希望用户群体能够通过跨平台开发的方法降低原生开发的成本,为此,开发人员需要安装原生开发环境,配置工程,使用HTML5、CSS3、JS和原生SDK生成应用。
官网定义如下:
Apache Cordova是一个开源的移动开发框架。允许你用标准的web技术-HTML5,CSS3和JavaScript做跨平台开发。 应用在每个平台的具体执行被封装了起来,并依靠符合标准的API绑定去访问每个设备的功能,比如说:传感器、数据、网络状态等。
使用Apache Cordova的人群:
移动应用开发者,想扩展一个应用的使用平台,而不通过每个平台的语言和工具集重新实现。
web开发者,想包装部署自己的web App将其分发到各个应用商店门户。
移动应用开发者,有兴趣混合原生应用组建和一个WebView(一个特别的浏览器窗口) 可以接触设备A级PI,或者你想开发一个原生和WebView组件之间的插件接口。
架构图
从图中,我们可以看到它提供了Web APP、WebView、Cordova Plugins。
Web APP
这是存放应用程序代码的地方,体现是你的具体业务逻辑模块。应用的实现是通过web页面,默认的本地文件名称是是index.html,这个本地文件应用CSS,JavaScript,图片,媒体文件和其他运行需要的资源。应用执行在原生应用包装的WebView中,这个原生应用是你分发到app stores中的。
WebView
Cordova启用的WebView可以给应用提供完整用户访问界面。在一些平台中,他也可以作为一个组件给大的、混合应用,这些应用混合和Webview和原生的应用组件。
Cordova Plugins
插件是Cordova生态系统的重要组成部分。他提供了Cordova和原生组件相互通信的接口并绑定到了标准的设备API上,这使你能够通过JavaScript调用原生代码。
其实Cordova通过命令来添加项目的,但是可以选择哪个平台去编译,比如我们添加Android平台,在Android默认mainActivity类,我们可以看到它其实继承CordovaActivity类,一切初始化条件是从loadUrl方法开始。
1 | package com.example.hello; |
进而得到以下UML类图
简单分析下,CordovaActivity内依赖一个WebView类,一个Preferences类,一个CordovaInterface接口,并同时初始化一些配置信息。WebView具体实现是由CordovaWebViewImpl类,CordovaInterface接口具体实现是由CordovaInterfaceImpl类实现。
CordovaWebViewImpl是核心类,里面会把一些插件能力初始化,用一个PluginManager进行管理,包含一个引擎类—CordovaWebViewEngine,这个引擎是通过反射的方式创建,自身初始化的时候把NativeToJsMessageQueue关联起来,里面包含着以Js字符串为主的双向链表,把每次从前端通过JS代码存储起来,然后通过绑定的桥接方式Pop出到相应的Native代码中去。
最终实现由SystemWebViewEngine类来对Android系统中WebView控件进行二次包装,这个类的初始化是在CordovaWebViewImpl类反射创建,相关插件和消息传递也是通过SystemWebViewEngine进行绑定。
当Cordova框架启动时候,CordovaActivity类中的onCreate方法调用loadUrl方法即可启动,最终在SystemWebViewEngine类的init方法中,会调用webView的addJavascriptInterface方法,看到这个方法是不是很熟悉,我们常规让webView支持开启JavaScript调用接口也是使用此特性。
1 | private static void exposeJsInterface(WebView webView, CordovaBridge bridge) { |
那么SystemExposedJsApi类new出来的对象就等同抛出“_cordovaNative”对象给JS端调用,进去看下SystemExposedJsApi类包含哪些内容,
1 | class SystemExposedJsApi implements ExposedJsApi { |
其中最关键是exec方法,其中bridgeSecret代表选择哪个桥接方式,service一般对应着你本地Java文件类名,action代表java文件中方法名,callbackId代表回调函数的Id,也就是句柄,arguments代表传递的参数。看出其中设计思想了没,service往往是本地能力集的类名,比如web端想调用相机,一般起个Camera类代表这个相机服务类,然后在这个类中定义方法,也就是action参数,这个action名称可扩展,因为方法名称可各种各样,适合自定义功能扩展。
SystemExposedJsApi对象初始化
在创建SystemExposedJsApi时需要CordovaBridge类,CordovaBridge类初始化需要CordovaWebView的PluginManager对象和NativeToJsMessageQueue对象。因为所有的JS端与Android native代码交互都是通过SystemExposedJsApi对象的exec方法。在exec方法中执行PluginManager的exec方法,PluginManager去查找具体的Plugin并实例化然后再执行Plugin的execute方法,并根据同步标识判断是同步返回给JS消息还是异步。由NativeToJsMessageQueue统一管理返回给JS的消息。
何时加载Plugin,如何加载
Cordova中很重要的部分是插件,Cordova在启动每个Activity的时候都会将配置文件中的所有plugin加载到PluginManager,在第一次loadUrl方法时,就会去初始化PluginManager并加载plugin,PluginManager在加载plugin的时候并不是马上实例化plugin对象,而是只是将plugin的Class名字保存到一个hashmap中,用service名字作为key值。当JS端通过JavascriptInterface接口的SystemExposedJsApi对象请求Android时,PluginManager会从hashmap中查找到plugin,如果该plugin还未实例化,利用java反射机制实例化该plugin,并执行plugin的execute方法。
Cordova的数据返回
Cordova中通过exec()函数请求android插件,数据的返回可同步也可以异步于exec()函数的请求。在开发android插件的时候可以重写public boolean isSynch(String action)方法来决定是同步还是异步。Cordova在android端使用了一个队列(NativeToJsMessageQueue)来专门管理返回给JS的数据。
1,同步 Cordova在执行完exec()后,android会马上返回数据,但不一定就是该次请求的数据,可能是前面某次请求的数据;因为当exec()请求的插件是允许同步返回数据的情况下,Cordova也是从NativeToJsMessageQueue队列头pop头数据并返回。然后再根据callbackID反向查找某个JS请求,并将数据返回给该请求的success函数。 2,异步 Cordova在执行完exec()后并不会同步得到一个返回数据。Cordova在执行exec()的同时启动了一个XMLHttpRequest对象方式或者prompt()函数方式的循环函数来不停的去获取NativeToJsMessageQueue队列中的数据,并根据callbackID反向查找到相对应的JS请求,并将该数据交给success函数。
webView.sendJavascript 发送到js队列,onNativeToJsMessageAvailable 负责执行js.
Native 调用 JS 执行方式有三种实现 LoadUrlBridgeMode、 OnlineEventsBridgeMode、PrivateApiBridgeMode
1、webView.sendJavascript 发送js方法到JS队列
2、onJsPrompt 方法拦截,获取调用方式
3、调用setBridgeMode 方法调用onNativeToJsMessageAvailable 执行javascript调用
总的来说,使用Cordova框架开发优缺点很明显。
优点:
缺点:
最后想说一句,无论是选择原生模式开发还是Hybrid混合模式,一定是要基于具体业务场景去选择,而不是盲目和绝对化觉得哪种模式好就不做分析想当然的去选择,还是有选择的结合,要知道应用之美在于药到病除。
]]>上一篇讲了Android触摸事件的传递机制,具体可以看这里 初识Android触摸事件传递机制。既然知道Android中触摸事件的传递分发,那么它能解决什么样的问题,在我们实际开发中如何应用,这点很重要,知道原理是为了解决问题而准备的。这篇文章的核心讲的如何解决View的滑动冲突,这个问题在日常开发中很常见,比如内部嵌套Fragment视图是左右滑动,外部用一个ScrollView来包含,可以上下滑动,如果不进行滑动冲突处理的话,就会造成外部滑动方向和内部滑动方向不一致。
常见的滑动冲突场景可以简单分为以下三种:
如图:
场景1,主要是将ViewPager和Fragment配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果。在这个效果中可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView,所以就造成了滑动冲突,但是在ViewPager内部处理了这种滑动冲突,因此在采用ViewPager时我们就无须关注这个问题,而如果把ViewPager换成ScrollView,那就必须自己手动处理,不然造成的结果就是内外两层只能一层能够滑动。
场景2,就复杂一点,当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层滑动,要么就是内外两层都滑动但很卡顿。
场景3,是场景1和场景2两种情况的嵌套,显得更复杂了。比如外部有一个SlideMenu效果,内部有一个ViewPager,ViewPager的每一个页面中又是一个ListView。虽然场景3滑动冲突看起来很复杂,但都是几个单一的滑动冲突的叠加,因此需要一一拆解开来即可。
一般来说,不管滑动冲突有多么复杂,它都有既定的规则,根据这些规则我们就可以选择合适的方法去处理。
对于场景1,它的处理规则就是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动,需要让内部View拦截点击事件。具体来说就是根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。
如图:
简单来说,就是根据水平方向和竖直方向的距离差来判断,如果是Dx>Dy,那么则是水平滑动,如果是Dy>Dx,那么则是竖直滑动。
场景2,则是比较特殊,它无法根据滑动的角度,距离差以及速度差来做判断。这个时候就需要从业务上找到突破点,比如,当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时需要内部View来响应View的滑动
对于场景3的话,它的滑动规则也更复杂,和场景2一样,同样是从业务上找到突破点。
外部拦截法是指点击事件都是先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件,就不拦截了,这样就可以解决滑动冲突的问题,外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,伪代码如下:
1 |
|
首先ACTION_DOWN这个事件,父容器必须返回false,这样保证后续move和up的事件可以传递给子View,根据move事件来决定是否拦截,如果父容器拦截就返回true,否则返回false。
实现一个自定义类似ViewPager的控件,嵌套ListView的效果,源代码如下:
1 | public class HorizontalScrollViewEx extends ViewGroup { |
这个情况的拦截条件就是父容器在滑动过程中水平距离差比垂直距离差大,那么就进行拦截,否则就不拦截,继续传递事件。
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法复杂。伪代码如下:
1 |
|
当子元素调用requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
前面是用自定义类似的ViewPager,现在重写一个ListView,我们可以自定义一个ListView,叫做ListViewEx,然后对内部拦截法的模板代码进行修改即可。
1 | public class ListViewEx extends ListView { |
同时对于包含ListViewEx外部布局进行修改,在onInterceptTouchEvent事件上不进行拦截
1 | public class HorizontalScrollViewEx2 extends ViewGroup { |
这个拦截规则也是父容器在滑动过程中水平距离差与垂直距离差相比。
总的来说,滑动冲突的场景可以分为三种,内外部方向不一致、内外部方向一致、嵌套前面两种情况。如何解决,不管多么复杂的滑动冲突,可以进行拆分,根据的一定的规则,第一种情况可根据滑动距离差、速度差和角度差来解决,第二种和第三种情况,可根据业务上找到突破点,业务上一种状态需要响应,切换到另外一种状态时则不响应,根据业务需求得出相应的处理规则,有了处理规则可以进行下一步处理。
]]>今天总结的一个知识点是Andorid中View事件传递机制,也是核心知识点,相信很多开发者在面对这个问题时候会觉得困惑,另外,View的另外一个难题滑动冲突,比如在ScrollView中嵌套ListView,都是上下滑动,这该如何解决呢,它解决的依据就是View事件的传递机制,所以开发者需要对View的事件传递机制有较深入的理解。
我们都知道Android中看到的页面很多是Activity组件,然后在Activity中嵌套控件,比如TextView、RelativeLayout布局等,其实这些控件的基类都是View这个抽象类,而ViewGroup也是View的子类,区别在于ViewGroup是可以当做其他子类的容器,一张关系图如下:
简单一句话,这些View控件的载体是Activity,Activity通过从DecorView开始进行绘制。
ACTION_DOWN
:用户手指按下操作,往往也代表着一次触摸事件的开始。ACTION_MOVE
:用户手指在屏幕上移动,一般情况下的轻微移动都会触发一系列的移动事件。ACTION_POINTER_DOWN
:额外的手指按下操作。ACTION_POINTER_UP
:额外的手指的离开操作ACTION_UP
:用户手指离开屏幕的操作,一次抬起操作标志着一次触摸事件的结束。在一次屏幕触摸操作中,ACTION_DOWN
和ACTION_UP
是必需的,ACTION_MOVE
则是看情况而定,如果只是点击,那么检测到只有按下和抬起操作。
分发(Dispatch):事件的分发对应着dispatchTouchEvent方法,在Andorid系统中,所有的触摸事件都是通过这个方法来分发的。
1 | boolean dispatchTouchEvent (MotionEvent ev) |
这个方法中,可以决定直接消费这个事件或者将事件继续分发给子视图处理。
拦截(Intercept):事件拦截对应着onInterceptTouchEvent方法,这个方法只有在ViewGroup及其子类中才存在,在View和Activity中是不存在的。
1 | boolean onInterceptTouchEvent (MotionEvent ev) |
这个方法用来判断是否拦截某个事件,如果拦截了某个事件,那么在同一序列事件当中,那么这个方法不会被再次调用。
消费(Consume):事件消费对应着onTouchEvent方法。
1 | boolean onTouchEvent (MotionEvent event) |
用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再接收到事件
在Android系统中,拥有事件传递处理能力的有三种:
这里说的View指的是除了ViewGroup之外的View控件,比如TextView、Button、CheckBox等,View控件本身就是最小的单位,不能作为其他View的容器,View拥有dispatchTouchEvent、onTouchEvent两个方法,所以这里就定义了一个继承TextView的类MyTextView,通过代码查看日志,看流程如何走。
1 | public class MyTextView extends TextView { |
同时定义一个MainActivity类用来展示MyTextView,在这个Activity中,我们为MyTextView设置了点击onClick和onTouch监听,方便跟踪了解事件传递的流程。
1 | public class MainActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { |
查看结果:
从中可以看到,事件是从down-move-up这样顺序执行,onTouch方法优先于onClick方法调用,如果都是以super方法传递的话,最后的结果是在MyTextView的onTouchEvent方法内被消费的,如果不消费的话,则会把事件返回到它的父级去消费,如果父级也没消费,那么最终会返回到Activity中处理。
ViewGroup作为View控件的容器存在,ViewGroup拥有dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三个方法。同样,我们自定义一个ViewGroup,继承自RelativeLayout,实现一个MyRelativeLayout。
1 | public class MyRelativeLayout extends RelativeLayout { |
查看结果:
从中可以看到触摸事件的传递顺序也是从Activity到ViewGroup,再由ViewGroup递归传递给它的子View。ViewGroup通过onInterceptTouchEvent方法对事件进行拦截,如果该方法返回true,则事件不会继续传递给子View,如果返回false或者super.onInterceptTouchEvent,则事件会继续传递给子View。在子View中对事件进行消费后,ViewGroup将不接收到任何事件。
在Android系统事件中,View和ViewGroup的伪代码如下:
1 | public boolean dispatchTouchEvent(MotionEvent ev){ |
用三张图来表示Android中触摸机制的流程。
1,View内触摸事件不消费
2,View内触摸事件消费
3,ViewGroup拦截触摸事件
一些总结:
参考地址:
1,https://www.youtube.com/watch?v=EZAoJU-nUyI
]]>在之前项目中,有个需求是这样的,要显示书的阅读足迹列表,具体要求是显示最近30天阅读情况,布局是用列表项布局,然后如果有更早的书,则显示更早的阅读情况,布局是用网格布局,如图所示:
要是放在之前的做法,一般都是ListView,再对特殊item样式进行单独处理,后来Android在5.0的时候出了一个RecyclerView组件,简单介绍下RecyclerView,一句话:只管回收与复用View,其他的你可以自己去设置,有着高度的解耦,充分的扩展性。至于用法,大家可以去官网查看文档即可,网上也很多文章介绍如何使用,这里不多说。想讲的重点是关于装饰者模式如何在RecyclerView中应用,如下:
定义:Decorator模式(别名Wrapper),动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案。
也就是说动态地给一个对象添加一些额外的职责,比如你可以增加功能,相比继承来说,有些父类的功能我是不需要的,我可能只用到某部分功能,那么我就可以自由组合,这样就显得灵活点,而不是那么冗余。
有几个要点:
UML图:
我们既然知道了装饰者和被装饰对象有相同的超类型,在做书的阅读足迹这个页面的时候,整个页面外部是一个RecyclerView,比如这样:
1 | <android.support.v4.widget.SwipeRefreshLayout |
同时每个Item项里面又嵌套一个RecyclerView,但外部只有2个Item项,一个Item项代表最近30天要显示的书的内容,一个Item项是显示更早书的内容。其中因为涉及到RecyclerView嵌套的问题,所以需要做滑动冲突的相关处理。所以这里用到自定义扩展的RecyclerView,具体解决滑动冲突代码如下:
1 |
|
按照思路就是内部拦截法,也就是RecyclerView自己处理,默认是不拦截,如果滑动距离超过所规定距离,我们就拦截自己处理,设置是可滚动的状态。
解决完滑动冲突之后,具体看看item项中的布局:
1 | <?xml version="1.0" encoding="utf-8"?> |
可以看到,每个Item项目一个头部,一个RecyclerView。然后是Adapter的适配,这里就是常用的RecyclerView Adapter的方式,要继承RecyclerView.Adapter<RecyclerView.ViewHolder>方法,同时要实现onCreateViewHolder、onBindViewHolder、getItemCount、getItemViewType方法,具体代码如下:
1 | public class ReadFootPrintAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
本来到这里,基本功能是完成了,可后来产品说要加个上下刷新,加载更多的操作。需求是随时可变的, 我们能不变的就是修改的心,那应该怎么做合适呢,是再增加itemType类型,加个加载更多的item项,那样修改的点会更多,此时想到了装饰者模式,是不是可以有个装饰类对这个adapter类进行组合呢,这样不需要修改原来的代码,只要扩展出去,况且我们知道都需要继承RecyclerView.Adapter,那么就可以把ReadFootPrintAdapter当做一个内部成员设置进入。我们来看下装饰者类:
1 | public class ReadFootPrintAdapterWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
这个Wrapper类就是装饰类,里面包含了一个RecyclerView.Adapter类型的成员,一个底部View,到时候在外部调用的时候,只需要传递一个RecyclerView.Adapter类型的参数进去即可,这样就形成了组合的关系。具体使用如下:
1 | mFooterView = LayoutInflater.from(mContext).inflate(R.layout.refresh_loadmore_layout, mRecyclerView, false); |
这样即达到需求要求,又能对原来已有的代码不进行修改,只进行扩展,何乐而不为。
这虽然是工作中一个应用点,但我想在开发过程中还有很多应用点,用上设计模式。日常开发中基本都强调设计模式的重要性,或许你对23种设计模式都很熟悉,都了解到它们各自的定义,可是等真正应用了,却发现没有踪迹可寻,写代码也是按照以前老的思路去做,那样就变成了知道是知道,却不会用的尴尬局面。如何突破呢,我觉得事后复盘和重构很有必要,就是利用项目尾声阶段,空的时候去review下自己写过的代码,反思是否有更简洁的写法,还有可以参考优秀代码,它们是怎么写,这样给自己找找灵感,再去结合自己已有的知识存储,说不定就能走上理论和实践相结合道路上。
]]>Android的动画可以分为三种:View动画、帧动画和属性动画。View动画通过对场景里的对象不断做图像变换(平移、缩放、旋转、透明度)从而产生动画效果,它是一种渐近式动画,并且View动画支持自定义。帧动画通过顺序播放一系列图像从而产生动画效果,可以简单理解为图片切换动画。属性动画通过动态地改变对象的属性从而达到动画效果,属性动画为API 11的新特性,在低版本无法直接使用属性动画,但我们仍然可以通过兼容库来使用它。
View动画的作用对象是View,它支持平移动画、缩放动画、旋转动画和透明度动画。有四个子类:TranslateAnimation,ScaleAnimation,RotateAnimation和AlphaAnimation。可以通过XML来定义。比如:
1 | <?xml version="1.0" encoding="utf-8"?> |
帧动画是顺序播放一组预先定义好的图片,类似于电影播放。不同于View动画,系统提供了另外一个类AnimationDrawable来使用帧动画。虽然比较简单,但是容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。
属性动画中有ValueAnimator、ObjectAnimator和AnimatorSet等概念,通过它们可以实现绚丽的动画。属性动画可以对任意对象的属性进行动画而不仅仅是View,动画默认时间间隔300ms,默认帧率10ms/帧。在一个时间间隔内完成对象从一个属性值到另一个属性值的改变,因此属性动画几乎是无所不能,只要对象有这个属性,它都能实现动画效果。
有个开源动画库:nineoldandroids来兼容之前的版本,因为属性动画是从API 11开始才有的。比较常用的动画类ValueAnimator、ObjectAnimator和AnimatorSet,其中ObjectAnimator继承ValueAnimator,AnimatorSet是动画集合,可以定义一组动画,它们使用起来也是极其简单的。如何使用呢:
1 | private void performAnimate(final View target, final int start, final int end) { |
不同形式的线程虽然都是线程,但是它们具有不同的特性和使用场景。AsyncTask封装了线程池和Handler,它主要是为了方便开发者在子线程中更新UI,HandlerThread是一中消息循环的线程,在它的内部可以使用Handler。IntentService是一个服务,系统对其进行了封装使其可以更方便地执行后台任务,IntentService内部采用HandlerThread来执行任务,当任务执行完毕后IntentService会自动退出。
在操作系统中,线程是操作系统的调度的最小单元,同时线程又是一种受限的系统资源,即线程不可能无限制地产生,并且线程的创建和销毁都会相应的开销。如果一个进程中频繁地创建和销毁线程,这显然不是高效的做法,正确的做法是采用线程池,在这个线程池中会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。
AsyncTask
AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI。从实现上来说,AsyncTask封装了Thread和Handler,通过AsyncTask可以更加方便地执行后台任务以及在主线程中访问UI,但是AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的任务来说,用线程池比较好点。
AsyncTask提供了4个核心方法:
看下源码:
1 | public abstract class AsyncTask<Params, Progress, Result> { |
从中我们知道了,线程池中线程的数量跟CPU内核多少有关,在一个处理队列中最多只有128个,这个并发数超过就会报异常,同时源码里也看到,是通过sHandler发送一个MESSAGE_POST_RESULT的消息进行最终处理的。
sHandler是一个静态的Handler对象,为了能够将执行环境切换到主线程,这就要求sHandler这个对象必须在主线程中创建。由于静态成员会在加载类的时候进行初始化,因此这就变相要求AsyncTask的类必须在主线程中加载,否则同一个进程中的AsyncTask都无法正常工作。
还有一点要注意下,从Android 3.0开始,默认情况下AsyncTask是串行执行的。但在Android 3.0之前是并行执行的。
HandlerThread
HandlerThread继承了Thread,它是一种可以使用Handler的Thread,它的实现很简单,就在run方法中通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环,这样在实际的使用中就允许在HandlerThread中创建Handler。看下源代码:
1 | public class HandlerThread extends Thread { |
IntentService
IntentService是一种特殊的Service,它继承了Service并且它是一种抽象类,因此必须创建它的子类才能使用IntentService。IntentService可用于执行后台耗时的任务,当任务执行后它会自动停止,同时由于IntentService是服务的原因,这导致他的优先级比单纯的线程要高很多,所以IntentService比较适合执行一些高优先级的后台任务,因为它的优先级高不容易被系统杀死。看下源码:
1 | public abstract class IntentService extends Service { |
线程池的优点:
Android中的线程池的概念来源于Java中的Executor,Executor是一个接口,真正的线程池的实现为ThreadPoolExecutor。ThreadPoolExecutor提供一系列参数来配置线程池,通过不同的参数可以创建不同的线程池,从线程池的功能特性来说,线程池主要分为4类。
ThreadPoolExecutor执行任务时大致遵循以下规则:
线程池主要有4类: