first commit

This commit is contained in:
张乾
2024-10-15 23:18:02 +08:00
parent 201a5889b1
commit 1093d24039
47 changed files with 9042 additions and 0 deletions

View File

@ -0,0 +1,243 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 UI 优化UI 渲染的几个关键概念
在开始今天的学习前,我祝各位同学新春快乐、工作顺利、身体健康、阖家幸福,绍文给您拜年啦!
每个做UI的Android开发上辈子都是折翼的天使。
多年来有那么一群苦逼的Android开发他们饱受碎片化之苦面对着各式各样的手机屏幕尺寸和分辨率还要与“凶残”的产品和UI设计师过招日复一日、年复一年的做着UI适配和优化工作蹉跎着青春的岁月。更加不幸的是最近两年这个趋势似乎还愈演愈烈刘海屏、全面屏还有即将推出的柔性折叠屏UI适配将变得越来越复杂。
UI优化究竟指的是什么呢我认为所谓的UI优化应该包含两个方面一个是效率的提升我们可以非常高效地把UI的设计图转化成应用界面并且保证UI界面在不同尺寸和分辨率的手机上都是一致的另一个是性能的提升在正确实现复杂、炫酷的UI设计的同时需要保证用户有流畅的体验。
那如何将我们从无穷无尽的UI适配中拯救出来呢
UI渲染的背景知识
究竟什么是UI渲染呢Android的图形渲染框架十分复杂不同版本的差异也比较大。但是无论怎么样它们都是为了将我们代码中的View或者元素显示到屏幕中。
而屏幕作为直接面对用户的手机硬件,类似厚度、色彩、功耗等都是厂家非常关注的。从功能机小小的黑白屏,到现在超大的全面屏,我们先来看手机屏幕的发展历程。
1. 屏幕与适配
作为消费者来说通常会比较关注屏幕的尺寸、分辨率以及厚度这些指标。Android的碎片化问题令人痛心疾首屏幕的差异正是碎片化问题的“中心”。屏幕的尺寸从3英寸到10英寸分辨率从320到1920应有尽有对我们UI适配造成很大困难。
除此之外材质也是屏幕至关重要的一个评判因素。目前智能手机主流的屏幕可分为两大类一种是LCDLiquid Crystal Display即液晶显示器另一种是OLEDOrganic Light-Emitting Diode的即有机发光二极管。
最新的旗舰机例如iPhone XS Max和华为Mate 20 Pro使用的都是OLED屏幕。相比LCD屏幕OLED屏幕在色彩、可弯曲程度、厚度以及耗电都有优势。正因为这些优势全面屏、曲面屏以及未来的柔性折叠屏使用的都是OLED材质。关于OLED与LCD的具体差别你可以参考《OLED和LCD区别》和《手机屏幕的前世今生可能比你想的还精彩》。今年柔性折叠屏肯定是最大的热点不过OLED的单价成本要比LCD高很多。
对于屏幕碎片化的问题Android推荐使用dp作为尺寸单位来适配UI因此每个Android开发都应该很清楚px、dp、dpi、ppi、density这些概念。
通过dp加上自适应布局可以基本解决屏幕碎片化的问题也是Android推荐使用的屏幕兼容性适配方案。但是它会存在两个比较大的问题
不一致性。因为dpi与实际ppi的差异性导致在相同分辨率的手机上控件的实际大小会有所不同。
效率。设计师的设计稿都是以px为单位的开发人员为了UI适配需要手动通过百分比估算出dp值。
除了直接dp适配之外目前业界比较常用的UI适配方法主要有下面几种
限制符适配方案。主要有宽高限定符与smallestWidth限定符适配方案具体可以参考《Android 目前稳定高效的UI适配方案》《smallestWidth 限定符适配方案》。
今日头条适配方案。通过反射修正系统的density值具体可以参考《一种极低成本的Android屏幕适配方式》《今日头条适配方案》。
2. CPU与GPU
除了屏幕UI渲染还依赖两个核心的硬件CPU与GPU。UI组件在绘制到屏幕之前都需要经过Rasterization栅格化操作而栅格化操作又是一个非常耗时的操作。GPUGraphic Processing Unit )也就是图形处理器,它主要用于处理图形运算,可以帮助我们加快栅格化操作。
你可以从图上看到软件绘制使用的是Skia库它是一款能在低端设备如手机上呈现高质量的2D跨平台图形框架类似Chrome、Flutter内部使用的都是Skia库。
3. OpenGL与Vulkan
对于硬件绘制我们通过调用OpenGL ES接口利用GPU完成绘制。OpenGL是一个跨平台的图形API它为2D/3D图形处理硬件指定了标准软件接口。而OpenGL ES是OpenGL的子集专为嵌入式设备设计。
在官方硬件加速的文档中可以看到很多API都有相应的Android API level限制。
这是为什么呢其实这主要是受OpenGL ES版本与系统支持的限制直到最新的Android P有3个API是仍然没有支持。对于不支持的API我们需要使用软件绘制模式渲染的性能将会大大降低。
Android 7.0把OpenGL ES升级到最新的3.2版本同时还添加了对Vulkan的支持。Vulkan是用于高性能3D图形的低开销、跨平台 API。相比OpenGL ESVulkan在改善功耗、多核优化提升绘图调用上有着非常明显的优势。
在国内“王者荣耀”是比较早适配Vulkan的游戏虽然目前兼容性还有一些问题但是Vulkan版本的王者荣耀在流畅性和帧数稳定性都有大幅度提升即使是战况最激烈的团战阶段也能够稳定保持在5560帧。
Android渲染的演进
跟耗电一样Android的UI渲染性能也是Google长期以来非常重视的基本每次Google I/O都会花很多篇幅讲这一块。每个开发者都希望自己的应用或者游戏可以做到60 fps如丝般顺滑不过相比iOS系统Android的渲染性能一直被人诟病。
Android系统为了弥补跟iOS的差距在每个版本都做了大量的优化。在了解Android的渲染之前需要先了解一下Android图形系统的整体架构以及它包含的主要组件。
我曾经在一篇文章看过一个生动的比喻如果把应用程序图形渲染过程当作一次绘画过程那么绘画过程中Android的各个图形组件的作用是
画笔Skia或者OpenGL。我们可以用Skia画笔绘制2D图形也可以用OpenGL来绘制2D/3D图形。正如前面所说前者使用CPU绘制后者使用GPU绘制。
画纸Surface。所有的元素都在Surface这张画纸上进行绘制和渲染。在Android中Window是View的容器每个窗口都会关联一个Surface。而WindowManager则负责管理这些窗口并且把它们的数据传递给SurfaceFlinger。
画板Graphic Buffer。Graphic Buffer缓冲用于应用程序图形的绘制在Android 4.1之前使用的是双缓冲机制在Android 4.1之后,使用的是三缓冲机制。
显示SurfaceFlinger。它将WindowManager提供的所有Surface通过硬件合成器Hardware Composer合成并输出到显示屏。
接下来我将通过Android渲染演进分析的方法帮你进一步加深对Android渲染的理解。
1. Android 4.0:开启硬件加速
在Android 3.0之前或者没有启用硬件加速时系统都会使用软件方式来渲染UI。
整个流程如上图所示:
Surface。每个View都由某一个窗口管理而每一个窗口都关联有一个Surface。
Canvas。通过Surface的lock函数获得一个CanvasCanvas可以简单理解为Skia底层接口的封装。
Graphic Buffer。SurfaceFlinger会帮我们托管一个BufferQueue我们从BufferQueue中拿到Graphic Buffer然后通过Canvas以及Skia将绘制内容栅格化到上面。
SurfaceFlinger。通过Swap Buffer把Front Graphic Buffer的内容交给SurfaceFinger最后硬件合成器Hardware Composer合成并输出到显示屏。
整个渲染流程是不是非常简单但是正如我前面所说CPU对于图形处理并不是那么高效这个过程完全没有利用到GPU的高性能。
硬件加速绘制
所以从Androd 3.0开始Android开始支持硬件加速到Android 4.0时,默认开启硬件加速。
硬件加速绘制与软件绘制整个流程差异非常大最核心就是我们通过GPU完成Graphic Buffer的内容绘制。此外硬件绘制还引入了一个DisplayList的概念每个View内部都有一个DisplayList当某个View需要重绘时将它标记为Dirty。
当需要重绘时仅仅只需要重绘一个View的DisplayList而不是像软件绘制那样需要向上递归。这样可以大大减少绘图的操作数量因而提高了渲染效率。
2. Android 4.1Project Butter
优化是无止境的Google在2012年的I/O大会上宣布了Project Butter黄油计划并且在Android 4.1中正式开启了这个机制。
Project Butter主要包含两个组成部分一个是VSYNC一个是Triple Buffering。
VSYNC信号
在讲文件I/O跟网络I/O的时候我讲到过中断的概念。对于Android 4.0CPU可能会因为在忙别的事情导致没来得及处理UI绘制。
为解决这个问题Project Buffer引入了VSYNC它类似于时钟中断。每收到VSYNC中断CPU会立即准备Buffer数据由于大部分显示设备刷新频率都是60Hz一秒刷新60次也就是说一帧数据的准备工作都要在16ms内完成。
这样应用总是在VSYNC边界上开始绘制而SurfaceFlinger总是VSYNC边界上进行合成。这样可以消除卡顿并提升图形的视觉表现。
三缓冲机制Triple Buffering
在Android 4.1之前Android使用双缓冲机制。怎么理解呢一般来说不同的View或者Activity它们都会共用一个Window也就是共用同一个Surface。
而每个Surface都会有一个BufferQueue缓存队列但是这个队列会由SurfaceFlinger管理通过匿名共享内存机制与App应用层交互。
整个流程如下:
每个Surface对应的BufferQueue内部都有两个Graphic Buffer 一个用于绘制一个用于显示。我们会把内容先绘制到离屏缓冲区OffScreen Buffer在需要显示时才把离屏缓冲区的内容通过Swap Buffer复制到Front Graphic Buffer中。
这样SurfaceFlinge就拿到了某个Surface最终要显示的内容但是同一时间我们可能会有多个Surface。这里面可能是不同应用的Surface也可能是同一个应用里面类似SurefaceView和TextureView它们都会有自己单独的Surface。
这个时候SurfaceFlinger把所有Surface要显示的内容统一交给Hareware Composer它会根据位置、Z-Order顺序等信息合成为最终屏幕需要显示的内容而这个内容会交给系统的帧缓冲区Frame Buffer来显示Frame Buffer是非常底层的可以理解为屏幕显示的抽象
如果你理解了双缓冲机制的原理那就非常容易理解什么是三缓冲区了。如果只有两个Graphic Buffer缓存区A和B如果CPU/GPU绘制过程较长超过了一个VSYNC信号周期因为缓冲区B中的数据还没有准备完成所以只能继续展示A缓冲区的内容这样缓冲区A和B都分别被显示设备和GPU占用CPU无法准备下一帧的数据。
如果再提供一个缓冲区CPU、GPU和显示设备都能使用各自的缓冲区工作互不影响。简单来说三缓冲机制就是在双缓冲机制基础上增加了一个Graphic Buffer缓冲区这样可以最大限度的利用空闲时间带来的坏处是多使用的了一个Graphic Buffer所占用的内存。
对于VSYNC信号和Triple Buffering更详细的介绍可以参考《Android Project Butter分析》。
数据测量
“工欲善其事必先利其器”Project Butter在优化UI渲染性能的同时也希望可以帮助我们更好地排查UI相关的问题。
在Android 4.1新增了Systrace性能数据采样和分析工具。在卡顿和启动优化中我们已经使用过Systrace很多次了也可以用它来检测每一帧的渲染情况。
Tracer for OpenGL ES也是Android 4.1新增加的工具它可逐帧、逐函数的记录App用OpenGL ES的绘制过程。它提供了每个OpenGL函数调用的消耗时间所以很多时候用来做性能分析。但因为其强大的记录功能在分析渲染问题时当Traceview、Systrace都显得棘手时还找不到渲染问题所在时此时这个工具就会派上用场了。
在Android 4.2系统增加了检测绘制过度工具具体的使用方法可以参考《检查GPU渲染速度和绘制过度》。
3. Android 5.0RenderThread
经过Project Butter黄油计划之后Android的渲染性能有了很大的改善。但是不知道你有没有注意到一个问题虽然我们利用了GPU的图形高性能运算但是从计算DisplayList到通过GPU绘制到Frame Buffer整个计算和绘制都在UI主线程中完成。
UI主线程“既当爹又当妈”任务过于繁重。如果整个渲染过程比较耗时可能造成无法响应用户的操作进而出现卡顿。GPU对图形的绘制渲染能力更胜一筹如果使用GPU并在不同线程绘制渲染图形那么整个流程会更加顺畅。
正因如此在Android 5.0引入了两个比较大的改变。一个是引入了RenderNode的概念它对DisplayList及一些View显示属性做了进一步封装。另一个是引入了RenderThread所有的GL命令执行都放到这个线程上渲染线程在RenderNode中存有渲染帧的所有信息可以做一些属性动画这样即便主线程有耗时操作的时候也可以保证动画流畅。
在官方文档 《检查 GPU 渲染速度和绘制过度》中我们还可以开启Profile GPU Rendering检查。在Android 6.0之后,会输出下面的计算和绘制每个阶段的耗时:
如果我们把上面的步骤转化线程模型可以得到下面的流水线模型。CPU将数据同步sync给GPU之后一般不会阻塞等待GPU渲染完毕而是通知结束后就返回。而RenderThread承担了比较多的绘制工作分担了主线程很多压力提高了UI线程的响应速度。
4. 未来
在Android 6.0的时候Android在gxinfo添加了更详细的信息在Android 7.0又对HWUI进行了一些重构而且支持了Vulkan在Android P支持了Vulkun 1.1。我相信在未来不久的Android Q更好地支持Vulkan将是一个必然的方向。
总的来说UI渲染的优化必然会朝着两个方向。一个是进一步压榨硬件的性能让UI可以更加流畅。一个是改进或者增加更多的分析工具帮助我们更容易地发现以及定位问题。
总结
今天我们通过Android渲染的演进历程一步一步加深对Android渲染机制的理解这对我们UI渲染优化工作会有很大的帮助。
但是凡事都要两面看硬件加速绘制虽然极大地提高了Android系统显示和刷新的速度但它也存在那么一些问题。一方面是内存消耗OpenGL API调用以及Graphic Buffer缓冲区会占用至少几MB的内存而实际上会占用更多一些。不过最严重的还是兼容性问题部分绘制函数不支持是其中一部分原因更可怕的是硬件加速绘制流程本身的Bug。由于Android每个版本对渲染模块都做了一些重构在某些场景经常会出现一些莫名其妙的问题。
例如每个应用总有那么一些libhwui.so相关的崩溃曾经这个崩溃占我们总崩溃的20%以上。我们内部花了整整一个多月通过发了几十个灰度使用了Inline Hook、GOT Hook等各种手段。最后才定位到问题的原因是系统内部RenderThread与主线程数据同步的Bug并通过规避的方法得以解决。
课后作业
人们都说iOS系统更加流畅对于Android的UI渲染你了解多少呢在日常工作中你是使用哪种方式做UI适配的觉得目前在渲染方面最大的痛点是什么欢迎留言跟我和其他同学一起讨论。
在UI渲染这方面其实我也并不是非常资深针对文中所讲的如果你有更好的思路和想法一定给我留言欢迎留下你的想法。
Android渲染架构非常庞大而且演进得也非常快。如果你还有哪些不理解的地方可以进一步阅读下面的参考资料
2018 Google I/ODrawn out: how Android renders
官方文档Android 图形架构
浏览器渲染:一颗像素的诞生
Android 屏幕绘制机制及硬件加速
Android性能优化之渲染篇
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,279 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 UI 优化(下):如何优化 UI 渲染?
孔子曰“温故而知新”在学习如何优化UI渲染之前我们先来回顾一下在“卡顿优化”中学到的知识。关于卡顿优化我们学习了4种本地排查卡顿的工具以及多种线上监控卡顿、帧率的方法。为什么要回顾卡顿优化呢那是因为UI渲染也会造成卡顿并且肯定会有同学疑惑卡顿优化和UI优化的区别是什么。
在Android系统的VSYNC信号到达时如果UI线程被某个耗时任务堵塞长时间无法对UI进行渲染这时就会出现卡顿。但是这种情形并不是我们今天讨论的重点UI优化要解决的核心是由于渲染性能本身造成用户感知的卡顿它可以认为是卡顿优化的一个子集。
从设计师和产品的角度他们希望应用可以用丰富的图形元素、更炫酷的动画来实现流畅的用户体验。但是Android系统很有可能无法及时完成这些复杂的界面渲染操作这个时候就会出现掉帧。也正因如此我才希望做UI优化因为我们有更高的要求希望它能达到流畅画面所需要的60 fps。这里需要说的是即使40 fps用户可能不会感到明显的卡顿但我们也仍需要去做进一步的优化。
那么接下来我们就来看看如何让我们的UI渲染达到60 fps有哪些方法可以帮助我们优化UI渲染性能
UI渲染测量
通过上一期的学习你应该已经掌握了一些UI测试和问题定位的工具。
测试工具Profile GPU Rendering和Show GPU Overdraw具体的使用方法你可以参考《检查GPU渲染速度和绘制过度》。
问题定位工具Systrace和Tracer for OpenGL ES具体使用方法可以参考《Slow rendering》。
在Android Studio 3.1之后Android推荐使用Graphics API DebuggerGAPID来替代Tracer for OpenGL ES工具。GAPID可以说是升级版它不仅可以跨平台而且功能更加强大支持Vulkan与回放。
通过上面的几个工具我们可以初步判断应用UI渲染的性能是否达标例如是否经常出现掉帧、掉帧主要发生在渲染的哪一个阶段、是否存在Overdraw等。
虽然这些图形化界面工具非常好用但是它们难以用在自动化测试场景中那有哪些测量方法可以用于自动化测量UI渲染性能呢
1. gfxinfo
gfxinfo可以输出包含各阶段发生的动画以及帧相关的性能信息具体命令如下
adb shell dumpsys gfxinfo 包名
除了渲染的性能之外gfxinfo还可以拿到渲染相关的内存和View hierarchy信息。在Android 6.0之后gxfinfo命令新增了framestats参数可以拿到最近120帧每个绘制阶段的耗时信息。
adb shell dumpsys gfxinfo 包名 framestats
通过这个命令我们可以实现自动化统计应用的帧率更进一步还可以实现自定义的“Profile GPU Rendering”工具在出现掉帧的时候自动统计分析是哪个阶段的耗时增长最快同时给出相应的建议。
2. SurfaceFlinger
除了耗时我们还比较关心渲染使用的内存。上一期我讲过在Android 4.1以后每个Surface都会有三个Graphic Buffer那如何查看Graphic Buffer占用的内存系统是怎么样管理这部分的内存的呢
你可以通过下面的命令拿到系统SurfaceFlinger相关的信息
adb shell dumpsys SurfaceFlinger
下面以今日头条为例应用使用了三个Graphic Buffer缓冲区当前用在显示的第二个Graphic Buffer大小是1080 x 1920。现在我们也可以更好地理解三缓冲机制你可以看到这三个Graphic Buffer的确是在交替使用。
+ Layer 0x793c9d0c00 (com.ss.***。news/com.**.MainActivity)
//序号 //状态 //对象 //大小
>[02:0x794080f600] state=ACQUIRED, 0x794081bba0 [1080x1920:1088, 1]
[00:0x793e76ca00] state=FREE , 0x793c8a2640 [1080x1920:1088, 1]
[01:0x793e76c800] state=FREE , 0x793c9ebf60 [1080x1920:1088, 1]
继续往下看你可以看到这三个Buffer分别占用的内存
Allocated buffers:
0x793c8a2640: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 0x20000900
0x793c9ebf60: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 0x20000900
0x794081bba0: 8160.00 KiB | 1080 (1088) x 1920 | 1 | 0x20000900
这部分的内存其实真的不小特别是现在手机的分辨率越来越大而且还很多情况应用会有其他的Surface存在例如使用了SurfaceView或者TextureView等。
那系统是怎么样管理这部分内存的呢?当应用退到后台的时候,系统会将这些内存回收,也就不会再把它们计算到应用的内存占用中。
+ Layer 0x793c9d0c00 (com.ss.***。news/com.**.MainActivity)
[00:0x0] state=FREE
[01:0x0] state=FREE
[02:0x0] state=FREE
那么如何快速地判别UI实现是否符合设计稿如何更高效地实现UI自动化测试这些问题你可以先思考一下我们将在后面“高效测试”中再详细展开。
UI优化的常用手段
让我们再重温一下UI渲染的阶段流程图我们的目标是实现60 fps这意味着渲染的所有操作都必须在16 ms= 1000 ms60 fps内完成。
所谓的UI优化就是拆解渲染的各个阶段的耗时找到瓶颈的地方再加以优化。接下来我们一起来看看UI优化的一些常用的手段。
1. 尽量使用硬件加速
通过上一期学习相信你也发自内心地认同硬件加速绘制的性能是远远高于软件绘制的。所以说UI优化的第一个手段就是保证渲染尽量使用硬件加速。
有哪些情况我们不能使用硬件加速呢之所以不能使用硬件加速是因为硬件加速不能支持所有的Canvas API具体API兼容列表可以见drawing-support文档。如果使用了不支持的API系统就需要通过CPU软件模拟绘制这也是渐变、磨砂、圆角等效果渲染性能比较低的原因。
SVG也是一个非常典型的例子SVG有很多指令硬件加速都不支持。但我们可以用一个取巧的方法提前将这些SVG转换成Bitmap缓存起来这样系统就可以更好地使用硬件加速绘制。同理对于其他圆角、渐变等场景我们也可以改为Bitmap实现。
这种取巧方法实现的关键在于如何提前生成Bitmap以及Bitmap的内存需要如何管理。你可以参考一下市面常用的图片库实现。
2. Create View优化
观察渲染的流水线时有没有同学发现缺少一个非常重要的环节那就是View创建的耗时。请不要忘记View的创建也是在UI线程里对于一些非常复杂的界面这部分的耗时不容忽视。
在优化之前我们先来分解一下View创建的耗时可能会包括各种XML的随机读的I/O时间、解析XML的时间、生成对象的时间Framework会大量使用到反射
相应的,我们来看看这个阶段有哪些优化方式。
使用代码创建
使用XML进行UI编写可以说是十分方便可以在Android Studio中实时预览到界面。如果我们要对一个界面进行极致优化就可以使用代码进行编写界面。
但是这种方式对开发效率来说简直是灾难因此我们可以使用一些开源的XML转换为Java代码的工具例如X2C。但坦白说还是有不少情况是不支持直接转换的。
所以我们需要兼容性能与开发效率,我建议只在对性能要求非常高,但修改又不非常频繁的场景才使用这个方式。
异步创建
那我们能不能在线程提前创建View实现UI的预加载吗尝试过的同学都会发现系统会抛出下面这个异常
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:121)
事实上我们可以通过又一个非常取巧的方式来实现。在使用线程创建UI的时候先把线程的Looper的MessageQueue替换成UI线程Looper的Queue。
不过需要注意的是在创建完View后我们需要把线程的Looper恢复成原来的。
View重用
正常来说View会随着Activity的销毁而同时销毁。ListView、RecycleView通过View的缓存与重用大大地提升渲染性能。因此我们可以参考它们的思想实现一套可以在不同Activity或者Fragment使用的View缓存机制。
但是这里需要保证所有进入缓存池的View都已经“净身出户”不会保留之前的状态。微信曾经就因为这个缓存导致出现不同的用户聊天记录错乱。
3. measure/layout优化
渲染流程中measure和layout也是需要CPU在主线程执行的对于这块内容网上有很多优化的文章一般的常规方法有
减少UI布局层次。例如尽量扁平化使用<ViewStub> <Merge>等优化。
优化layout的开销。尽量不使用RelativeLayout或者基于weighted LinearLayout它们layout的开销非常巨大。这里我推荐使用ConstraintLayout替代RelativeLayout或者weighted LinearLayout。
背景优化。尽量不要重复去设置背景这里需要注意的是主题背景theme) theme默认会是一个纯色背景如果我们自定义了界面的背景那么主题的背景我们来说是无用的。但是由于主题背景是设置在DecorView中所以这里会带来重复绘制也会带来绘制性能损耗。
对于measure和layout我们能不能像Create View一样实现线程的预布局呢这样可以大大地提升首次显示的性能。
Textview是系统控件中非常强大也非常重要的一个控件强大的背后就代表着需要做很多计算。在2018年的Google I/O大会发布了PrecomputedText并已经集成在Jetpack中它给我们提供了接口可以异步进行measure和layout不必在主线程中执行。
UI优化的进阶手段
那对于其他的控件我们是不是也可以采用相同的方式接下来我们一起来看看近两年新框架的做法我来介绍一下Facebook的一个开源库Litho以及Google开源的Flutter。
1. Litho异步布局
Litho是Facebook开源的声明式Android UI渲染框架它是基于另外一个Facebook开源的布局引擎Yoga开发的。
Litho本身非常强大内部做了很多非常不错的优化。下面我来简单介绍一下它是如何优化UI的。
异步布局-
一般来说的Android所有的控件绘制都要遵守measure -> layout -> draw的流水线并且这些都发生在主线程中。
Litho如我前面提到的PrecomputedText一样把measure和layout都放到了后台线程只留下了必须要在主线程完成的draw这大大降低了UI线程的负载。它的渲染流水线如下
界面扁平化
前面也提到过降低UI的层级是一个非常通用的优化方法。你肯定会想有没有一种方法可以直接降低UI的层级而不通过代码的改变呢Litho就给了我们一种方案由于Litho使用了自有的布局引擎Yoga)在布局阶段就可以检测不必要的层级、减少ViewGroups来实现UI扁平化。比如下面这样图上半部分是我们一般编写这个界面的方法下半部分是Litho编写的界面可以看到只有一层层级。
优化RecyclerView-
Litho还优化了RecyclerView中UI组件的缓存和回收方法。原生的RecyclerView或者ListView是按照viewType来进行缓存和回收但如果一个RecyclerView/ListView中出现viewType过多会使缓存形同虚设。但Litho是按照text、image和video独立回收的这可以提高缓存命中率、降低内存使用率、提高滚动帧率。
Litho虽然强大但也有自己的缺点。它为了实现measure/layout异步化使用了类似react单向数据流设计这一定程度上加大了UI开发的复杂性。并且Litho的UI代码是使用Java/Kotlin来进行编写无法做到在AS中预览。
如果你没有计划完全迁移到Litho我建议可以优先使用Litho中的RecyclerCollectionComponent和Sections来优化自己的RecyelerView的性能。
2. Flutter自己的布局 + 渲染引擎
如下图所示Litho虽然通过使用自己的布局引擎Yoga一定程度上突破了系统的一些限制但是在draw之后依然走的系统的渲染机制。
那我们能不能再往底层深入把系统的渲染也同时接管过来Flutter正是这样的框架它也是最近十分火爆的一个新框架这里我也简单介绍一下。
Flutter是Google推出并开源的移动应用开发框架开发者可以通过Dart语言开发App一套代码同时运行在iOS和Android平台。
我们先整体看一下Flutter的架构在Android上Flutter完全没有基于系统的渲染引擎而是把Skia引擎直接集成进了App中这使得Flutter App就像一个游戏App。并且直接使用了Dart虚拟机可以说是一套跳脱出Android的方案所以Flutter也可以很容易实现跨平台。
开发Flutter应用总的来说简化了线程模型框架给我们抽象出各司其职的Runner包括UI、GPU、I/O、Platform Runner。Android平台上面每一个引擎实例启动的时候会为UI Runner、GPU Runner、I/O Runner各自创建一个新的线程所有Engine实例共享同一个Platform Runner和线程。
由于本期我们主要讨论UI渲染相关的内容我来着重分析一下Flutter的渲染步骤相关的具体知识你可以阅读《Flutter原理与实践》。
首先UI Runner会执行root isolate可以简单理解为main函数。需要简单解释一下isolate的概念isolate是Dart虚拟机中一种执行并发代码实现Dart虚拟机实现了Actor的并发模型与大名鼎鼎的Erlang使用了类似的并发模型。如果不太了解Actor的同学可以简单认为isolate就是Dart虚拟机的“线程”Root isolate会通知引擎有帧要渲染
Flutter引擎得到通知后会告知系统我们要同步VSYNC。
得到GPU的VSYNC信号后对UI Widgets进行Layout并生成一个Layer Tree。
然后Layer Tree会交给GPU Runner进行合成和栅格化。
GPU Runner使用Skia库绘制相关图形。
Flutter也采用了类似Litho、React属性不可变单向数据流的方案。这已经成为现代UI渲染引擎的标配。这样做的好处是可以将视图与数据分离。
总体来说Flutter吸取和各个优秀前端框架的精华还“加持”了强大的Dart虚拟机和Skia渲染引擎可以说是一个非常优秀的框架闲鱼、今日头条等很多应用部分功能已经使用Flutter开发。结合Google最新的Fuchsia操作系统它会不会是一个颠覆Android的开发框架我们在专栏后面会单独详细讨论Flutter。
3. RenderThread与RenderScript
在Android 5.0系统增加了RenderThread对于ViewPropertyAnimator和CircularReveal动画我们可以使用RenderThead实现动画的异步渲染。当主线程阻塞的时候普通动画会出现明显的丢帧卡顿而使用RenderThread渲染的动画即使阻塞了主线程仍不受影响。
现在越来越多的应用会使用一些高级图片或者视频编辑功能,例如图片的高斯模糊、放大、锐化等。拿日常我们使用最多的“扫一扫”这个场景来看,这里涉及大量的图片变换操作,例如缩放、裁剪、二值化以及降噪等。
图片的变换涉及大量的计算任务而根据我们上一期的学习这个时候使用GPU是更好的选择。那如何进一步压榨系统GPU的性能呢
我们可以通过RenderScript它是Android操作系统上的一套API。它基于异构计算思想专门用于密集型计算。RenderScript提供了三个基本工具一个硬件无关的通用计算API一个类似于CUDA、OpenCL和GLSL的计算API一个类C99的脚本语言。允许开发者以较少的代码实现功能复杂且性能优越的应用程序。
如何将它们应用到我们的项目中?你可以参考下面的一些实践方案:
RenderScript渲染利器
RenderScript :简单而快速的图像处理
Android RenderScript 简单高效实现图片的高斯模糊效果
总结
回顾一下UI优化的所有手段我们会发现它存在这样一个脉络
1. 在系统的框架下优化。布局优化、使用代码创建、View缓存等都是这个思路我们希望减少甚至省下渲染流水线里某个阶段的耗时。
2. 利用系统新的特性。使用硬件加速、RenderThread、RenderScript都是这个思路通过系统一些新的特性最大限度压榨出性能。
3. 突破系统的限制。由于Android系统碎片化非常严重很多好的特性可能低版本系统并不支持。而且系统需要支持所有的场景在一些特定场景下它无法实现最优解。这个时候我们希望可以突破系统的条条框框例如Litho突破了布局Flutter则更进一步把渲染也接管过来了。
回顾一下过去所有的UI优化第一阶段的优化我们在系统的束缚下也可以达到非常不错的效果。不过越到后面越容易出现瓶颈这个时候我们就需要进一步往底层走可以对整个架构有更大的掌控力需要造自己的“轮子”。
对于UI优化的另一个思考是效率目前Android Studio对设计并不友好例如不支持Sketch插件和AE插件。Lottie是一个非常好的案例它很大提升了开发人员写动画的效率。
“设计师和产品你们长大了要学会自己写UI了”。在未来我们希望UI界面与适配可以实现自动化或者干脆把它交还给设计师和产品。
课后作业
在你平时的工作中做过哪些UI优化的工作有没有什么“大招”跟其他同学分享对于Litho, Flutter你又有什么看法欢迎留言跟我和其他同学一起讨论。
今天还有两个课后小作业尝试使用Litho和Flutter这两个框架。
1.使用Litho实现一个信息流界面。
2.使用Flutter写一个Hello World分析安装包的体积。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,313 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 包体积优化(上):如何减少安装包大小?
曾经在15年的时候我在WeMobileDev公众号就写过一篇文章《Android安装包相关知识汇总》也开源了一个不少同学都使用过的资源混淆工具AndResGuard。
现在再看看这篇4年前的文章就像看到了4年前的自己感触颇多啊。几年过去了网上随意一搜都有大量安装包优化的文章那还有哪些“高深”的珍藏秘笈值得分享呢
时至今日微信包体积也从当年的30MB增长到现在的100MB了。我们经常会想现在WiFi这么普遍了而且5G都要来了包体积优化究竟还有没有意义它对用户和应用的价值在哪里
安装包的背景知识
还记得在2G时代我们每个月只有30MB流量那个时候安装包体积确实至关重要。当时我在做“搜狗输入法”的时候我们就严格要求包体积在5MB以内。
几年过去了,我们对包体积的看法有什么改变吗?
1. 为什么要优化包体积
在2018年的Google I/OGoogle透露了Google Play上安装包体积与下载转化率的关系图。
从这张图上看,大体来说,安装包越小,转化率越高这个结论依然成立。而包体积对应用的影响,主要有下面几点:
下载转化率。一个100MB的应用用户即使点了下载也可能因为网络速度慢、突然反悔下载失败。对于一个10MB的应用用户点了下载之后在犹豫要不要下的时候已经下载完了。但是正如上图的数据安装包大小与转化率的关系是非常微妙的。10MB跟15MB可能差距不大但是10MB跟40MB的差距还是非常明显的。
推广成本。一般来说,包体积对渠道推广和厂商预装的单价会有非常大的影响。特别是厂商预装,这主要是因为厂商留给预装应用的总空间是有限的。如果你的包体积非常大,那就会影响厂商预装其他应用。
应用市场。苹果的App Store强制超过150MB的应用只能使用WiFi网络下载Google Play要求超过100MB的应用只能使用APK扩展文件方式上传由此可见应用包体积对应用市场的服务器带宽成本还是会有一点压力的。
目前成熟的超级App越来越多很多产品也希望自己成为下一个超级App希望功能可以包罗万象满足用户的一切需求。但这同样也导致安装包不断变大其实很多用户只使用到很少一部分功能。
下面我们就来看看微信、QQ、支付宝以及淘宝这几款超级App这几年安装包增长的情况。
我还记得在15年的时候为了让微信6.2版本小于30MB我使用了各种各样的手段把体积从34MB降到29.85MB资源混淆工具AndResGuard也就是在那个优化专项中写的。几年过去了微信包体积已经涨到100MB了淘宝似乎也不容乐观。相比之下QQ和支付宝相对还比较节制。
2. 包体积与应用性能
React Native 5MB、Flutter 4MB、浏览器内核20MB、Chromium网络库2MB…现在第三方开发框架和扩展库越来越多很多的应用包体积都已经几十是MB起步了。
那包体积除了转化率的影响,它对我们应用性能还有哪些影响呢?
安装时间。文件拷贝、Library解压、编译ODEX、签名校验特别对于Android 5.0和6.0系统来说Android 7.0之后有了混合编译微信13个Dex光是编译ODEX的时间可能就要5分钟。
运行内存。在内存优化的时候我们就说过Resource资源、Library以及Dex类加载这些都会占用不少的内存。
ROM空间。100MB的安装包启动解压之后很有可能就超过200MB了。对低端机用户来说也会有很大的压力。在“I/O优化”中我们讨论过如果闪存空间不足非常容易出现写入放大的情况。
对于大部分一两年前的“千元机”,淘宝和微信都已经玩不转了。“技术短期内被高估,长期会被低估”,特别在业务高速发展的时候,性能往往就被排到后面。
包体积对技术人员来说应该是非常重要的技术指标,我们不能放任它的增长,它对我们还有不少意义。
业务梳理。删除无用或者低价值的业务,永远都是最有效的性能优化方式。我们需要经常回顾过去的业务,不能只顾着往前冲,适时地还一些“技术债务”。
开发模式升级。如果所有的功能都不能移除那可能需要倒逼开发模式的转变更多地采用小程序、H5这样开发模式。
包体积优化
国内地开发者都非常羡慕海外的应用因为海外有统一的Google Play市场。它可以根据用户的ABI、density和language发布还有在2018年最新推出的App Bundle。
事实上安装包中无非就是Dex、Resource、Assets、Library以及签名信息这五部分接下来我们就来看看对于国内应用来说还有什么高级“秘籍”。
1. 代码
对于大部分应用来说Dex都是包体积中的大头。看一下上面表格中微信、QQ、支付宝和淘宝的数据它们的Dex数量从1个增长到10多个我们的代码量真的增长了那么多倍吗
而且Dex的数量对用户安装时间也是一个非常大的挑战在不砍功能的前提下我们看看有哪些方法可以减少这部分空间。
ProGuard-
“十个ProGuard配置九个坑”特别是各种第三方SDK。我们需要仔细检查最终合并的ProGuard配置文件是不是存在过度keep的现象。
你可以通过下面的方法输出ProGuard的最终配置尤其需要注意各种的keep *很多情况下我们只需要keep其中的某个包、某个方法或者是类名就可以了。
-printconfiguration configuration.txt
那还有没有哪些方法可以进一步加大混淆力度呢这时我们可能要向四大组件和View下手了。一般来说应用都会keep住四大组件以及View的部分方法这样是为了在代码以及XML布局中可以引用到它们。
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.view.View
事实上我们完全可以把非exported的四大组件以及View混淆但是需要完成下面几个工作
XML替换。在代码混淆之后需要同时修改AndroidManifest以及资源XML中引用的名称。
代码替换。需要遍历其他已经混淆好的代码,将变量或者方法体中定义的字符串也同时修改。需要注意的是,代码中不能出现经过运算得到的类名,这种情况会导致替换失败。
// 情况一:变量
public String activityName = "com.sample.TestActivity";
// 情况二:方法体
startActivity(new Intent(this, "com.sample.TestActivity"));
// 情况三:通过运算得到,不支持
startActivity(new Intent(this, "com.sample" + ".TestActivity"));
代码替换的方法我推荐使用ASM。不熟悉ASM的同学也不用着急后面我会专门讲它的原理和用法。饿了么曾经开源过一个可以实现四大组件和View混淆的组件Mess不过似乎已经没在维护了可供你参考。
Android Studio 3.0推出了新Dex编译器D8与新混淆工具R8目前D8已经正式Release大约可以减少3%的Dex体积。但是计划用于取代ProGuard的R8依然处于实验室阶段期待它在未来能有更好的表现。
去掉Debug信息或者去掉行号-
某个应用通过相同的ProGuard规则生成一个Debug包和Release包其中Debug包的大小是4MBRelease包只有3.5MB。
既然它们ProGuard的混淆与优化的规则是一样的那它们之间的差异在哪里呢那就是DebugItem。
DebugItem里面主要包含两种信息
调试的信息。函数的参数变量和所有的局部变量。
排查问题的信息。所有的指令集行号和源文件行号的对应关系。
事实上在ProGuard配置中一般我们也会通过下面的方式保留行号信息。
-keepattributes SourceFile, LineNumberTable
对于去除debuginfo以及行号信息更详细的分析推荐你认真看一下支付宝的一篇文章《Android包大小极致压缩》。通过这个方法我们可以实现既保留行号但是又可以减少大约5%的Dex体积。
事实上支付宝参考的是Facebook的一个开源编译工具ReDex。ReDex除了没有文档之外绝对是客户端领域非常硬核的一个开源库非常值得你去认真研究。
ReDex这个库里面的好东西实在是太多了后面我们还会反复讲到其中去除Debug信息是通过StripDebugInfoPass完成。
{
"redex" : {
"passes" : [
"StripDebugInfoPass"
]
},
"StripDebugInfoPass" : {
"drop_all_dbg_info" : "0", // 去除所有的debug信息0表示不去除
"drop_local_variables" : "1", // 去除所有局部变量1表示去除
"drop_line_numbers" : "0", // 去除行号0表示不去除
"drop_src_files" : "0",
"use_whitelist" : "0",
"drop_prologue_end" : "1",
"drop_epilogue_begin" : "1",
"drop_all_dbg_info_if_empty" : "1"
}
}
Dex分包-
当我们在Android Studio查看一个APK的时候不知道你是否知道下图中“defines 19272 methods”和“references 40229 methods”的区别。
关于Dex的格式以及各个字段的定义你可以参考《Dex文件格式详解》。为了加深对Dex格式的理解推荐你使用010Editor。
“define classes and methods”是指真正在这个Dex中定义的类以及它们的方法。而“reference methods”指的是define methods以及define methods引用到的方法。
简单来说如下图所示如果将Class A与Class B分别编译到不同的Dex中由于method a调用了method b所以在classes2.dex中也需要加上method b的id。
因为跨Dex调用造成的这些冗余信息它对我们Dex的大小会造成哪些影响呢
method id爆表。我们都知道每个Dex的method id需要小于65536因为method id的大量冗余导致每个Dex真正可以放的Class变少这是造成最终编译的Dex数量增多。
信息冗余。因为我们需要记录跨Dex调用的方法的详细信息所以在classes2.dex我们还需要记录Class B以及method b的定义造成string_ids、type_ids、proto_ids这几部分信息的冗余。
事实上我自己定义了一个Dex信息有效率的指标希望保证Dex有效率应该在80%以上。同时为了进一步减少Dex的数量我们希望每个Dex的方法数都是满的即分配了65536个方法。
Dex信息有效率 = define methods数量/reference methods数量
那如何实现Dex信息有效率提升呢关键在于我们需要将有调用关系的类和方法分配到同一个Dex中即减少跨Dex的调用的情况。但是由于类的调用关系非常复杂我们不太可能可以计算出最优解只能得到局部的最优解。
为了提高Dex信息有效率我在微信时曾参与写过一个依赖分析的工具Builder。但在微信最新的7.0版本你可以看到上面表中Dex的数量和大小都增大了很多这是因为他们不小心把这个工具搞失效了。Dex数量的增多对于Tinker热修复时间、用户安装时间都有很大影响。如果把这个问题修复微信7.0版本的Dex数量应该可以从13个降到6个左右包体积可以减少10MB左右。
但是我在研究ReDex的时候发现它也提供了这个优化而且实现得比微信的更好。ReDex在分析类调用关系后使用的是贪心算法计算局部最优值具体算法可查看CrossDexDefMinimizer。
为什么我们不能计算到最优解因为我们需要在编译速度和效果之间找一个平衡点在ReDex中使用这个优化的配置如下
{
"redex" : {
"passes" : [
"InterDexPass"
]
},
"InterDexPass" : {
"minimize_cross_dex_refs": true,
"minimize_cross_dex_refs_method_ref_weight": 100,
"minimize_cross_dex_refs_field_ref_weight": 90,
"minimize_cross_dex_refs_type_ref_weight": 100,
"minimize_cross_dex_refs_string_ref_weight": 90
}
}
那么通过Dex分包可以对包体积优化多少呢因为Android默认的分包方式做得实在不好如果你的应用有4个以上的Dex我相信这个优化至少有10%的效果。
Dex压缩-
我曾经在逆向Facebook的App时惊奇地发现它怎么可能只有一个700多KB的Dex。Google Play是不允许动态下发代码的那它的代码都放到哪里了呢
事实上Facebook App的classes.dex只是一个壳真正的代码都放到assets下面。它们把所有的Dex都合并成同一个secondary.dex.jar.xzs文件并通过XZ压缩。
XZ压缩算法和7-Zip一样内部使用的都是LZMA算法。对于Dex格式来说XZ的压缩率可以比Zip高30%左右。但是不知道你有没有注意到,这套方案似乎存在一些问题:
首次启动解压。应用首次启动的时候需要将secondary.dex.jar.xzs解压缩根据上图的配置信息应该一共有11个Dex。Facebook使用多线程解压的方式这个耗时在高端机是几百毫秒左右在低端机可能需要35秒。这里为什么不采用Zstandard或者Brotli呢主要是压缩率与解压速度的权衡。
ODEX文件生成。前面我就讲过当Dex非常多的时候会增加应用的安装时间。对于Facebook的这个做法首次生成ODEX的时间可能就会达到分钟级别。Facebook为了解决这个问题使用了ReDex另外一个超级硬核的方法那就是oatmeal。
oatmeal的原理非常简单就是根据ODEX文件的格式自己生成一个ODEX文件。它生成的结果跟解释执行的ODEX一样内部是没有机器码的。
如上图所示对于正常的流程我们需要fork进程来生成dex2oat这个耗时一般都比较大。通过oatmeal我们直接在本进程生成ODEX文件。一个10MB的Dex如果在Android 5.0生成一个ODEX的耗时大约在10秒以上在Android 8.0使用speed模式大约在1秒左右而通过oatmeal这个耗时大约在100毫秒左右。
我一直都很想把oatmeal引入进Tinker但是比较担心兼容性的问题。因为每个版本ODEX格式都有一些差异oatmeal是需要分版本适配的。
2. Native Library
现在音视频、美颜、AI、VR这些功能在应用越来越普遍但这些库一般都是使用C或者C++写的也就是说我们的APK中Native Library的体积越来越大了。
对于Native Library传统的优化方法可能就是去除Debug信息、使用c++_shared这些。那我们还有没有更好的优化方法呢
Library压缩-
跟Dex压缩一样Library优化最有效果的方法也是使用XZ或者7-Zip压缩。
在默认的lib目录我们只需要加载少数启动过程相关的Library其他的Library我们都在首次启动时解压。对于Library格式来说压缩率同样可以比Zip高30%左右,效果十分惊人。
Facebook有一个So加载的开源库SoLoader它可以跟这套方案配合使用。和Dex压缩一样压缩方案的主要缺点在于首次启动的时间毕竟对于低端机来说多线程的意义并不大因此我们要在包体积和用户体验之间做好平衡。
Library合并与裁剪-
对于Native LibraryFacebook中的编译构建工具Buck也有两个比较硬核的高科技。当然在官方文档中是完全找不到的它们都隐藏在源码中。
Library合并。在Android 4.3之前进程加载的Library数量是有限制的。在编译过程我们可以自动将部分Library合并成一个。具体思路你可以参考文章《Android native library merging》以及Demo。
Library裁剪。Buck里面有一个relinker的功能原理就是分析代码中JNI方法以及不同Library的方法调用找到没有无用的导出symbol将它们删掉。这样linker在编译的时候也会把对应的无用代码同时删掉这个方法相当于实现了Library的ProGuard Shrinking功能。
包体积监控
关于包体积如果一直放任不管几个版本之后就会给你很大的“惊喜”。我了解到一些应用对包体积卡得很紧任何超过100KB的功能都需要审批。
对于包体积的监控,通常有下面几种:
大小监控。这个非常好理解,每个版本跟上一个版本包体积的对比情况。如果某个版本体积增长过大,需要分析具体原因,是否有优化空间。
依赖监控。每一版本我们都需要监控依赖这里包括新增JAR以及AAR依赖。这是因为很多开发者非常不细心经常会不小心把一些超大的开源库引进来。
规则监控。如果发现某个版本包体积增长很大我们需要分析原因。规则监控也就是将包体积的监控抽象为规则例如无用资源、大文件、重复文件、R文件等。比如我在微信的时候使用ApkChecker实现包体积的规则监控。
包体积的监控最好可以实现自动化与平台化,作为发布流程的其中一个环节。不然通过人工的方式,很难持续坚持下去。
总结
今天我们一起分析了实现难度比较大的包体积优化方法,可能有人会想这些方法实现难度那么大,真的有价值吗?根据我的理解,现在我们已经到了移动优化的“深水区”,网上那些千篇一律的文章已经无法满足需求。也就是说,简单的方法我们都掌握了,而且也都已经在做了,需要考虑接下来应该如何进一步优化。
这时候就需要静下心来学会思考与钻研再往底层走走。我们要去研究APK的文件格式进一步还要研究内部Dex、Library以及Resource的文件格式。同时思考整个编译流程才能找到那些可以突破的地方。
在实现AndResGuard的时候我就对resources.arsc格式以及Android加载资源的流程有非常深入的研究。几年过去了对于资源的优化又有哪些新的秘籍呢我们下一期就会讨论“资源优化”这个主题。
从Buck和ReDex看出来Facebook比国内的研究真的要高深很多希望他们可以补充一些文档让我们学习起来更轻松一些。
课后作业
你的应用会关注包体积吗?你做过哪些包体积优化的工作,有哪些好的方法可以跟同学们分享呢?欢迎留言跟我和其他同学一起讨论。
今天的练习Sample尝试使用ReDex这个项目来优化我们应用的包体积主要有下面几个小任务
strip debuginfo
分包优化
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,229 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 包体积优化(下):资源优化的进阶实践
上一期我们聊了Dex与Native Library的优化是不是还有点意犹未尽的感觉呢那安装包还有哪些可以优化的地方呢
请看上面这张图Assets、Resource以及签名metadata都是安装包中的“资源”部分今天我们就一起来看看如何进一步优化资源的体积。
AndResGuard工具
在美团的一篇文章《Android App包瘦身优化实践》中也讲到了很多资源优化相关的方法例如WebP和SVG、R文件、无用资源、资源混淆以及语言压缩等。
在我们的安装包中,资源相关的文件具体有下面这几个,它们都是我们需要优化的目标文件。
想使用好AndResGuard工具需要对安装包格式以及Android资源编译的原理有很深地理解它主要有两个功能一个是资源混淆一个是资源的极限压缩。
接下来我们先来复习一下这个工具的核心实现,然后再进一步思考还有哪些地方需要继续优化。
1. 资源混淆
ProGuard的核心优化主要有三个Shrink、Optimize和Obfuscate也就是裁剪、优化和混淆。当初我在写AndResGuard的时候希望实现的就是ProGuard中的混淆功能。
资源混淆的思路其实非常简单,就是把资源和文件的名字混淆成短路径:
Proguard -> Resource Proguard
R.string.name -> R.string.a
res/drawable/icon -> res/s/a
那么这样的实现究竟对哪些资源文件有优化作用呢?
resources.arsc。因为资源索引文件resources.arsc需要记录资源文件的名称与路径使用混淆后的短路径res/s/a可以减少整个文件的大小。
metadata签名文件。签名文件MF与SF都需要记录所有文件的路径以及它们的哈希值使用短路径可以减少这两个文件的大小。
ZIP文件索引。ZIP文件格式里面也需要记录每个文件Entry的路径、压缩算法、CRC、文件大小等信息。使用短路径本身就可以减少记录文件路径的字符串大小。
资源文件有一个非常大的特点那就是文件数量特别多。以微信7.0为例安装包中就有7000多个资源文件。所以说资源混淆工具仅仅通过短路径的优化就可以达到减少resources.arsc、签名文件以及ZIP文件大小的目的。
既然移动优化已经到了“深水区”正如Dex和Library优化一样我们需要对它们的格式以及特性有非常深入的研究才能找到优化的思路。而我们要做的资源优化也是如此要对resources.arsc、签名文件以及ZIP格式需要有非常深入的研究与思考。
2. 极限压缩
AndResGuard的另外一个优化就是极限压缩它的极限压缩功能体现在两个方面
更高的压缩率。虽然我们使用的还是Zip算法但是利用了7-Zip的大字典优化APK的整体压缩率可以提升3%左右。
压缩更多的文件。Android编译过程中下面这些格式的文件会指定不压缩在AndResGuard中我们支持针对resources.arsc、PNG、JPG以及GIF等文件的强制压缩。
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
这里可能会有一个疑问为什么Android系统会专门选择不去压缩这些文件呢
压缩效果并不明显。这些格式的文件大部分本身已经压缩过重新做Zip压缩效果并不明显。例如PNG和JPG格式重新压缩只有3%5%的收益,并不是十分明显。
读取时间与内存的考虑。如果文件是没有压缩的系统可以利用mmap的方式直接读取而不需要一次性解压并放在内存中。
Android 6.0之后AndroidManifest支持不压缩Library文件这样安装APK的时候也不需要把Library文件解压出来系统可以直接mmap安装包中的Library文件。
android:extractNativeLibs=“true”
简单来说我们在启动性能、内存和安装包体积之间又做了一个抉择。在上一期中我就讲过对于Dex和Library来说最有效果的方法是使用XZ或者7-Zip压缩对于资源来说也是如此一些比较大的资源文件我们也可以考虑使用XZ压缩但是在首次启动时需要解压出来。
进阶的优化方法
学习完AndResGuard工具的混淆和压缩功能的实现原理后可以帮助我们加深对安装包格式以及Android资源编译的原理的认识。
但AndResGuard毕竟是几年前的产物那现在又有哪些新的进阶优化方法呢
1. 资源合并
在资源混淆方案中我们发现资源文件的路径对于resources.arsc、签名信息以及ZIP文件信息都会有影响。而且因为资源文件数量非常非常多导致这部分的体积非常可观。
那我们能不能把所有的资源文件都合并成同一个大文件,这样做肯定会比资源混淆方案效果更好。
事实上,大部分的换肤方案也是采用这个思路,这个大资源文件就相当于一套皮肤。因此我们完全可以把这套方案推广开来,但是实现起来还是需要解决不少问题的。
资源的解析。我们需要模拟系统实现资源文件的解析例如把PNG、JPG以及XML文件转换为Bitmap或者Drawable这样获取资源的方法需要改成我们自定义的方法。
// 系统默认的方式
Drawable drawable = getResouces().getDrawable(R.drawable.loading);
// 新的获取方式
Drawable drawable = CustomResManager.getDrawable(R.drawable.loading);
那为什么我们不像SVG那样直接把这些解析完的所有Drawable全部丢到系统的缓存中呢这样代码就无需做太多修改之所以没这么做主要是考虑对内存的影响如果我们把全部的资源文件一次性全部解析并且丢到系统的缓存中这部分会占用非常大的内存。
资源的管理。考虑到内存和启动时间所有的资源也是用时加载我们只需要使用mmap来加载“Big resource File”。同时我们还要实现自己的资源缓存池ResourceCache释放不再使用的资源文件这部分内容你可以参考类似Glide图片库的实现。
我在逆向Facebook的App的时候也发现它们的资源和多语言基本走的完全是自己的流程。在“UI优化”时我就说过我们先在系统的框架下尝试做了很多的优化但是渐渐发现这样的方式依然要受系统的各种制约这时就要考虑去突破系统的限制把所有的流程都接管过来。
当然我们也需要在性能和效率之间寻找平衡点,要看自己的应用当前更重视性能提升还是开发效率。
2. 无用资源
AndResGuard中的资源混淆实现的是ProGuard的Obfuscate那我们是否可以同样实现资源的Shrink也就是裁剪功能呢应用通过长时间的迭代总会有一些无用的资源尽管它们在程序运行过程不会被使用但是依然占据着安装包的体积。
事实上Android官方早就考虑到这种情况了下面我们一起来看看无用资源优化方案的演进过程。
第一阶段Lint
从Eclipse时代开始我们就开始使用Lint这个静态代码扫描工具它里面就支持Unused Resources扫描。
然后我们直接选择“Remove All Unused Resources”就可以轻松删除所有的无用资源了。既然它是第一阶段的方案那Lint方案扫描具体的缺点是什么呢
Lint作为一个静态扫描工具它最大的问题在于没有考虑到ProGuard的代码裁剪。在ProGuard过程我们会shrink掉大量的无用代码但是Lint工具并不能检查出这些无用代码所引用的无用资源。
第二阶段shrinkResources
所以Android在第二阶段增加了“shrinkResources”资源压缩功能它需要配合ProGurad的“minifyEnabled”功能同时使用。
如果ProGuard把部分无用代码移除这些代码所引用的资源也会被标记为无用资源然后通过资源压缩功能将它们移除。
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
是不是看起来很完美但是目前的shrinkResources实现起来还有几个缺陷。
没有处理resources.arsc文件。这样导致大量无用的String、ID、Attr、Dimen等资源并没有被删除。
没有真正删除资源文件。对于Drawable、Layout这些无用资源shrinkResources也没有真正把它们删掉而是仅仅替换为一个空文件。为什么不能删除呢主要还是因为resources.arsc里面还有这些文件的路径具体你可以查看这个issues。
所以尽管我们的应用有大量的无用资源但是系统目前的做法并没有真正减少文件数量。这样resources.arsc、签名信息以及ZIP文件信息这几个“大头”依然没有任何改善。
那为什么Studio不把这些资源真正删掉呢事实上Android也知道有这个问题在它的核心实现ResourceUsageAnalyzer中的注释也写得非常清楚并尝试解决这个问题提供了两种思路。
如果想解答系统为什么不能直接把这些资源删除我们需要先回过头来重温一下Android的编译流程。
由于Java代码需要用到资源的R.java文件所以我们就需要把R.java提前准备好。
在编译Java代码过程已经根据R.java文件直接将代码中资源的引用替换为常量例如将R.String.sample替换为0x7f0c0003。
.ap_资源文件的同步编译例如resources.arsc、XML文件的处理等。
如果我们在这个过程强行把无用资源文件删除resources.arsc和R.java文件的资源ID都会改变因为默认都是连续的这个时候代码中已经替换过的0x7f0c0003就会出现资源错乱或者找不到的情况。
因此系统为了避免发生这种情况采用了折中的方法并没有二次处理resources.arsc文件只是仅仅把无用的Drawable和Layout文件替换为空文件。
第三阶段realShrinkResources
那怎么样才能真正实现无用资源的删除功能呢ResourceUsageAnalyzer的注释中就提供了一个思路我们可以利用resources.arsc中Public ID的机制实现非连续的资源ID。
简单来说就是keep住保留资源的ID保证已经编译完的代码可以正常找到对应的资源。
但是重写resources.arsc的方法会比资源混淆更加复杂我们既要从这个文件中抹去所有的无用资源相关信息还要keep住所有保留资源的ID相当于把整个文件都重写了。
正因为异常复杂所以目前Android还没有提供这套方案的完整实现。我最近也正在按照这个思路来实现这套方案希望完成后可以尽快开源出来。
总结
今天我们回顾了AndResGuard工具的实现原理也学习了两种资源优化的进阶方式。特别是无用资源的优化你可以看到尽管是无所不能的Google也并没有把方案做到最好依然存在一些妥协的地方。
其实这种不完美的地方还有很多很多,也正是有了这些不完美的地方,才会出现各种各样优秀的开源方案。也因此我们才会不断思考如何突破系统的限制,去实现更多、更底层的优化。
课后作业
对于Android的编译流程你还有不理解的地方吗对于安装包中的资源你还有哪些好的优化方案欢迎留言跟我和其他同学一起讨论。
不知道你有没有想过其实“第三阶段”的无用资源删除方案也并不是终极解决方案因为它并没有考虑到无用的Assets资源。
但是对于Assets资源代码中会有各种各样的引用方式如果想准确地识别出无用的Assets并不是那么容易。当初在Matrix中我们尝试提供了一套简单的实现你可以参考UnusedAssetsTask。
希望你在课后也可以进一步思考我们可以如何识别出无用的Assets资源在这个过程中会遇到哪些问题
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,119 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 想成为Android高手你需要先搞定这三个问题
专栏上线已经两个多月,模块一“高质量开发”也已经更新完毕,你掌握地如何呢?我知道有不少同学一直随着专栏更新积极学习、认真完成课后的练习作业,并且及时给我反馈,作为专栏作者我很欣慰。
但也有不少同学表示很难跟上专栏的进度似乎对“如何成为Android开发高手”感到更加迷茫也更困惑了。“这个专栏实在是太难了”“我日常工作根本用不上这些知识”“我应该怎样做才能更好地学习这个专栏”。太难了、工作用不到、想学但是又不知道从何入手这是我听到同学们反馈最多的三个问题。
今天既然是专栏“模块一”的答疑时间那我就来解答这三个问题力求帮助迷茫的你拨开云雾找到通向Android开发高手之路。
前几年在业务红利期Atlas、Tinker、React Native、Weex、小程序大行其道我们的应用程序堆积了各种各样的框架以及无数的业务代码。当现在需要出海东南亚需要下沉到三四线城市时在面对各种低端机和恶劣网络条件的时候再猛然回头一看会发现原来我们已经欠下了如此恐怖的“技术债”。
如果想成为一名开发高手,只做好需求是远远不够的,还需要有系统性解决应用性能和架构问题的能力。而这些问题本来就是很复杂的,可能对于一些同学来说解决复杂问题会感到很难,但你要成为高手,就一定要具备解决复杂问题的能力。下面我就先来谈谈这个专栏真的这么“难”吗?
问题一:这个专栏太难了?
性能优化就像一个坑,你永远不知道自己跳进去的坑有多深 。
在过去几个月,我一直在填“启动优化”这个坑,也对这句话有了更深的感触。
业务优化应用层。开始的时候通过业务代码的优化我很轻松地就把启动速度优化了50%。但正如《大停滞》所说的这只是摘完了“所有低垂的果实”。之后很快就陷入了停滞就像冲进了一条漆黑的隧道不知道还可以做哪些事情很多现象从表面也不知道如何解释I/O有时候为什么那么慢线程的优化应该怎么样去衡量
Android Framework系统框架层。为了冲出这条漆黑的隧道我去研究了Android的内存管理、文件系统、渲染框架等各个模块学习如何去优化和监控I/O、线程、卡顿以及帧率建立了各种各样的性能监控框架。为什么我对监控如此重视这是因为对于大厂来说“挖坑容易填坑难”几十上百人协同开发一个项目我们不希望只是解决具体的某一个问题而是要彻底解决某一类问题。但想要实现一个监控框架前提是需要对Framework有非常充分地理解和研究。
Linux Kernel内核层。再深入下去我还需要利用Linux的一些机制例如ftrace、Perf、JVMTI等。在做I/O的类重排、文件重排评估的时候还需要自己去修改内核的参数去刷ROM。
Hardware硬件层。高端机和低端机的硬件差异究竟在哪里eMMC闪存和UFS闪存的区别是什么除了更加了解硬件的性能和特性到了这个阶段我还希望可以向手机厂商和硬件厂商要性能。例如高通的CPU Boost、微信的Hardcoder、OPPO的开放平台等。
启动优化的过程,就像是一个知识爬坡的过程。我们不停地尝试往底层深入,希望去摘更高的果实。那再回到你疑惑的问题:这个专栏难不难?难,因为它试图为你从上往下拆解整个知识架构。
坦白说现在很多移动开发工程师更像是API工程师背后的数据结构、算法和架构相关的知识是不达标的。这个时候如果想往底层走就会感觉步步艰辛。但是上层的API很容易被Deprecated即使你对Android的所有API倒背如流也无法成为真正的开发高手。这样的你即便以后把Android替换成Fuchsia你也还只是一个Dart API工程师。
相反越底层的东西越不容易过时假如我们以后面对的不是Linux内核的系统比如Fuchsia OS也可以根据已经掌握的系统知识套用到现有的操作系统上因为像内存管理、文件系统、信号机制、进程调度、系统调用、中断机制、驱动等内容都是共通的在迁移到新的系统上时可以有一个全局的视角帮助你快速上手。
因此我希望5年后再回头看这个专栏它依然不会过时。所以从知识的深度来看这个专栏的确难。那从知识的广度来看呢
崩溃、内存、卡顿、I/O、渲染、网络…这个专栏涉及的知识的确非常多而且这个专栏也只能提纲挈领还需要你花时间去补充文章里给你链接的更多知识。
所以说无论从知识的深度还是广度来看专栏的确算是比较难。那这个专栏究竟有多难呢可以说超越了大多数腾讯T3或者阿里P7的水平。如果你还没到达这个级别看不懂是正常的因为大部分内容BAT的工程师第一遍可能也看不懂。
把这个专栏写“难”并不是因为我想炫技而是成为一名真正的Android开发高手本来就没有想象得那么容易。只有看到差距才有前进的动力2019年你需要真正迈出走向高手的第一步。
问题二:专栏所讲的工作上用不到?
我一直只是做业务,没有机会接触性能。对于这个专栏,我更期待可以对日常工作有帮助的内容。
正如我上面所说的之所以选择写这些内容是因为它们是移动开发高手所必须掌握的。如果我告诉你MAT怎么用、Profiler怎么用或者告诉你如何去写界面或许这些内容对你日常工作能有帮助但仅仅这些你依然不可能成为一名Android的高手。
这个专栏目希望可以提高你的个人能力,帮助你成长,或许不一定与你当前的工作完全契合,那这个问题怎么解决呢?我认为完全不需要等着别人给你安排,我们在完成工作之余,可以尝试去解决一些应用性能和架构的问题,又或者是团队效率的问题。
这样你工作上的表现也会超出上级的预期,并且可能以后这些高级问题大家都会来咨询你。同事对你建立了信任,这些事情以后可能就都由你负责了,你也成为了大家心目中的“开发高手”。
当然如果你认为目前的平台对未来的发展制约太多,那这个专栏同样也是你去面试大厂的一块非常好的敲门砖。
“打铁还需自身硬”专栏里很多内容大厂面试官可能也不熟悉我专栏里讲的很多问题其实大厂目前做得也都不很完善。在专栏中我会力求去分析腾讯、阿里、头条、Facebook、Google等国内外大厂目前遇到的问题、尝试解决的方案以及未来优化的方向。希望可以扩宽你的视野帮你知道大厂在玩什么、他们都在意什么因为这些对于面试来说也非常重要。
虽然这个专栏涉及那么多的内容但毕竟我们不可能每一项都精通。之前我在一篇文章曾经讲过微信的T型人才理论说的是微信在面试时不会问你Android和iOS的API怎么使用而是希望候选人在某一个领域研究得特别牛、特别深入并且是可以打动面试官的。这意味着如果你在某一个领域证明过自己那微信也会愿意在其他领域给你机会。这里我推荐你看看《谈谈腾讯的技术价值观与技术人才修炼》这篇文章。
不夸张地说LeetCode适量刷题加上这个专栏知识的广度如果再找其中一两个知识点更加深入地研究这样的话进入大公司是不会有太大问题的。
问题三:这个专栏应该怎么学习
我工作用不上,平时还那么忙,应该怎么去学习这个专栏呢?
如果专栏的学习可以跟我们的工作紧密结合在一起,的确是一个非常理想的情况。但是即使是理想情况,关键也还是要靠个人的自驱力。
在极客时间的年终总结里看到一句话特别有感触“2018年买了32个专栏完成了开篇词的学习”。这个专栏应该怎么学你首先应该抛弃焦虑无所畏惧地往前冲。
既然腾讯T3或者阿里P7都会觉得难如果看不懂真的不要气馁也不要焦虑可以结合参考资料慢慢看。因为专栏一直都在可以按照自己的节奏来学习甚至可以用2019年一整年的时间来“死磕”它但千万不要放弃。
还记得当初你在专栏“导读”里立下的flag吗你可以利用这个专栏好好地将知识架构补充完整。我们的基础能力提升了未来无论是大前端还是Flutter都会有用武之地也就更加无需担心Android系统是否会被颠覆。
这个专栏应该怎么学?我给你的第二个建议是多看、多想、多实践。看再多的文章,不去思考文章所讲的内容和意图也是没用的;思考再多,不去动手真正实践也是没用的。
正因为实践这么重要所以我在写专栏时才会把大量的时间花在Sample上面。想想现在有那么多的开源项目可能我们只是调用API或者提一两个issue并不算是真正使用。想要真正用好开源项目需要你去研究内部的机制思考作者的意图。只有在认真研究之后我们才能发现优化的空间。
在学习专栏时,我建议可以先挑一两个知识点开始深入学习。如果你觉得崩溃相关的内容比较困难,可以先略过,等学习完其他知识后再回头来看,肯定会有不一样的体会。
我们的学习过程也是一个树立信心的过程,可能在某几个阶段会感到煎熬,但是只要你攀登上去了,一切都会柳暗花明。
另外专栏的很多文章我喜欢用演进的思路去讲比如耗电的演进、渲染的演进、Android Runtime的演进等。同样的你成为高手的道路应该也是不停向前演进的可能刚开始时不一定是最好的但是只要方向是正确的终究可以到达“高手”这个终点。
最后,也是我反复强调过的,专栏的学习还需要结合大量的背景知识和外部资料,推荐的书籍你可以参考《专栏学得苦?可能你还需要一份配套学习书单》。
总结
今天我们一起来定一个“小目标”也不迟按照专栏给出的方向尝试一下、努力一下走向通往Android开发的高手之路。
未来移动开发无论是变成大前端还是Flutter的世界性能、效率和架构都是永恒不变的主线。今天我们在Android开发打下的坚实基础未来也会帮助我们更好地理解和深入新的开发模式或者新的系统。崩溃、内存、存储、渲染、I/O、网络…这些知识以及它们背后的底层原理依然还是非常重要的。对于其他领域也是如此一个前端开发工程师不能只知道HTML、CSS和JS语法还需要知道它们编译的原理、浏览器实现的原理以及底层的渲染机制等。
最后我想说一个人学习可能会比较孤独,如果可以找到更多志同道合的朋友一起学习,效果可能会更好。正如很多开发所提倡的结对编程,推荐你看看《两位拯救谷歌的超级工程师的故事》。除了结对编程的范例之外,文中有一段话对我的触动也非常大。
Jeff与Sanjay对于计算机的工作原理非常熟悉能够立足bit层级进行思考。Jeff曾经整理出一份《每位程序员都应该了解的那些延迟数字》清单。虽然名为“每位程序员都应该了解”但大多数从业者对这些数字其实非常陌生——例如一级缓存引用通常需要半纳秒或者从内存中顺序读取1MB 大概需要250微秒等等。但这些数字已经直接烙进了Jeff与Sanjay的大脑当中。凭借着他们对谷歌核心软件的多次重写该系统的容量已经提升至新的数量级。
2019年已经过去将近1/6了今年你定的目标完成得怎么样了还有哪些学习计划有什么感受想跟其他同学分享吗欢迎留言跟我和其他同学一起见证你的成长。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,113 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 如何提升组织与个人的研发效能?
通过“高质量开发”模块的学习,相信你已经对打造一款高质量应用信心满满了。不过人们常说“提质增效”,总喜欢把质量和效率联系在一起,我们都希望在保证质量的前提下,为自己的团队提速。
特别是移动互联网在红海厮杀的今天,快速试错变得越来越重要,敏捷开发也被越来越多的团队所推崇。有些时候为了效率我们甚至愿意牺牲部分性能,而选择在合适的时间去偿还这些“债务”。
在“高质量开发”模块中,我侧重如何给应用交付的每个步骤做好“质检”。今天我们就一起来开启新的征程,从组织和个人研发效能的角度,重新帮你审视整个应用交付的过程。
组织的研发效能
1. 何为研发效能
在讨论如何优化组织研发效能之前,请你先思考一下什么是研发效能。
我们平常开发的过程,是从产品的一个需求想法,转变为功能并且发布上线。这个过程会涉及产品、设计、开发、测试,更多的时候可能还会拉上前端、后台或者算法。
产品的交付涉及很多的流程和人员,虽然设计人员出图很快、我们开发效率很高,但也并不能代表研发效能同样很高,研发效能是对整个产品最终交付的速度和质量负责。在《如何衡量研发效能》一文中,将研发效能定义为一个组织持续快速交付价值的能力。
在文中,作者从流动效率、资源效率和质量进一步拆解了研发效能,并提出了研发效能的五个衡量标准。
对于客户端研发来说我们是不是只要保证按时按质实现需求就可以了呢有很多公司尽管实行“996”产品、开发和测试看起来的确都很忙了但是交付速度和质量却仍然不令人满意产品埋怨开发效率低、开发埋怨产品需求不明确、测试埋怨开发质量差、开发埋怨测试发现不了问题等。
这是因为什么呢?对于研发效能这个话题,虽然我并不是这个领域的专家,但根据我多年的工作经验,我可以谈谈个人的两点思考:
提效是每个人的职责。尽管在BAT这些大厂会有专门的研发效能部门但是效能的提升并不是单单只依靠效能部门或者认为是领导的事情而是组织里面每一个人都应该去思考的事情。例如天猫设立的效能目标是“211”也就是2周交付周期、1周开发周期以及1小时发布时长。那团队中的每一个人都应该为这一共同目标而努力回顾在每个发布迭代中遇到的问题以及改进的建议。
提效不仅限于写Android代码。尽管我们是Android开发工程师但是我们的工作不应该局限在写Android代码上关键还是解决需求场景。无论是APM系统、大网络平台、大数据平台这种大型的前后端一体化解决方案还是需求流程上或者增强产研测沟通信任的优化。只要是对提升效能有帮助的我们都可以尝试去实践。在建立了这种整体统筹的思维之后未来我们想转后端、前端甚至是产品都会有很大的帮助。
微信在很早的时候就引入了Google的OKR绩效考核制度记得在2017年Android团队定了“质量”“效能”和“影响力”这三个目标。
接着Android团队的30多个研发也都在围绕这三个目标来制定自己的工作计划例如针对“效能”来说有的人抽离一个UI库或者动画库有的人写一个监控工具有的人提升编译速度有的人写一个Web的值班页面有的人优化需求评审的流程…
这样大家集思广益,一起思考、一起讨论,为达成组织的共同目标而努力,这也是为什么微信开发人员虽然不多,但是战斗力在业界数一数二的原因。
2. 应用交付的流程
前面我从整个组织的角度定义了研发效能的含义以及衡量它的五个标准。同时也结合我在微信的经历谈了我关于提升研发效能的两点思考。可能大部分同学还是感觉整个产品的交付流程类似产品、UI设计这些环节是研发人员无法把控的那接下来我只从研发的流程来看如何提高效能。
正如我在专栏导读《如何打造高质量的应用》所说的一个应用至少会经过开发、编译CI、测试、灰度和发布这几个阶段。下面我从效能的角度分别看看每个阶段需要关注什么问题。
开发阶段。开发阶段解决的是如何用尽可能短的时间完成尽可能多的需求并且保证开发的质量不至于后期过多的返工。项目的架构应该如何选择例如应该采用原生开发还是Web、React Native、Flutter这样的跨平台方案。如何提升团队人员的能力以及工具和框架的成熟度有哪些提升团队工作效率的技巧
编译、CI阶段。编译CI阶段解决的是如何发现和优化开发阶段的一些编码问题以及快速构建出最终产物。Google的Gradle、Facebook的Buck为编译速度做了哪些努力Flutter的Hot Reload为什么可以这么快AspectJ、ASM、ReDex这三种插桩方法的原理和差别是什么腾讯的RDM、阿里的摩天轮这些编译构建平台有什么特别之处
测试阶段。测试阶段是为了发现交付过程的质量问题。测试的确不容易自动化测试成本高也不容易把控发布质量。那如何可以让测试覆盖更多的场景Monkey、性能测试、UI测试应该怎么实践腾讯的RDM、蚂蚁的伙伴是如何做到人人都是测试网易的Airtest测试框架有哪些过人之处
灰度、发布阶段。灰度发布是为了验证产品的效果。发布并不是把包丢出去就可以了我们需要对自己的产品负责。那如何准确、快速地评估产品数据头条、快手是如何做到精准运营和A/B测试如果遇到疑难的线上问题应该怎么解决复杂多变的网络问题又应该怎样去定位和分析
当然为了提升在这个过程的效率我们会用到一些很有用的第三方工具例如用于CodeReview的Gerrit、持续集成的Jenkins、代码审计的Coverity等。
工具不仅可以将大量人工操作变成自动化也可以方便团队更好的协作。例如我们把需求的流转、进度安排变得可视化可以大大地减少产研测团队之间消息的隔阂在这方面阿里的AONE或者腾讯的TPAD都做得非常不错。
项目管理、需求管理、代码托管、构建/部署、测试平台…都是我们常用的工具类似阿里的云效会提供这样的一站式平台从需求发起到分支管理、代码review再到测试发布。在过去这些工具都是各大公司研发效能部门多年的结晶一般都不愿意对外提供。但是得益于云时代、TOB时代的到来现在都愿意打包成商品供我们使用。
当然每个项目都会有自己特殊的情况,这些工具也不一定可以完全符合我们的需要,我们可以根据自己的情况选择合适的服务,或者直接开发自己的工具。
个人的研发效率
个人作为整个组织的一部分,我们效率的提升也会对组织有正向的作用。特别是对某些小团队或者独立开发者来说,个人可能就代表了整个团队。
关于个人效率的提升和时间的管理,有很多书籍专门在讲这个内容。下面从我看到的一些不好的现象,谈谈我个人两个比较深的体会。
1. 思考
年轻人千万不要碰的东西之一,便是能获得短期快感的软件。它们会在不知不觉中偷走你的时间,消磨你的意志力,摧毁你向上的勇气。
随着我们接触到的信息越来越多,越来越多的人很难保持对事情的专注力。工作期间经常想着去刷一下抖音、头条、微信、王者荣耀,强行把时间打破成碎片。
跟产品开了一天的会,他的需求有了,你的代码呢?
可能也有一部分同学他们不刷抖音和头条,但是在上班时间也会被各种邮件、钉钉、会议折磨得痛不欲生。针对这个问题,我的做法是每天上午和下午都会至少保留一个小时“目空一切”的时间,不看邮件、不看钉钉、不接会议。当然有的时候也是无法避免被老板当面拉回到“现实”。
我经常看到团队里面的一些人也存在这种现象最终表现可能是这个人经常“996”看起来很忙但是产出并不高而且个人成长也不明显。
每天我们应该需要有一段时间真正的静下心来工作,而且每过一段时间也要重新审视一下自己的工作,有哪些地方做的不够好?有没有什么事情是自己或者团队的人正在反复而低效在做的,是否可以优化。
2. 方法
关于方法,这里我只说两点,也是同学们经常会出现的问题。
做事的方法。曾经看到一些开发人员非常喜欢用二分法来排查问题。当测试给他报Bug时他会非常熟练的操作Git命令花上一两个小时打出几十个验证包不辞劳苦地尝试。当然二分法我也使用过在毫无头绪的时候的确可以“死马当活马医”。但是我们在使用这个“大杀器”之前起码应该经过自己的思考尝试正面去迎击Bug本身。
提问的方法。在微信和QQ群里面经常会看到有些同学在群里问一个问题可能Google一下就可以得到答案。然后他们在群里灌了一个小时水最后还是没有任何答案。在做Tinker开源的时候我有时也被一些使用者问得心情不再愉悦。事实上提问题是非常体现技术和职业素养的我们在提问题之前需要经过自己的思考和努力在这里推荐你看看《提问的艺术》。
总结
“吾日三省吾身”,无论是组织的研发效能,还是个人的工作效率,我们都需要学会经常去回顾和思考,快速演进、快速迭代,争取未来做得更好。
你在工作和学习的效率上,遇到过哪些问题?对于如何提升工作和学习的效率,你还有什么好的方法和建议吗?欢迎留言分享给我和其他同学。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,249 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 关于编译,你需要了解什么?
作为Android工程师我们每天都会经历无数次编译而对于大型项目来说每次编译就意味着要花去一杯咖啡的时间。可能我讲具体的数字你会更有体会当时我在微信时全量编译Debug包需要5分钟而编译Release包更是要超过15分钟。
如果每次编译可以减少1分钟对微信整个Android团队来说就可以节约1200分钟团队40人 × 每天编译30次 × 1分钟。所以说优化编译速度对于提升整个团队的开发效率是非常重要的。
那应该怎么样优化编译速度呢微信、Google、Facebook等国内外大厂都做了哪些努力呢除了编译速度之外关于编译你还需要了解哪些知识呢
关于编译
虽然我们每天都在编译,那到底什么是编译呢?
你可以把编译简单理解为将高级语言转化为机器或者虚拟机所能识别的低级语言的过程。对于Android来说这个过程就是把Java或者Kotlin转变为Android虚拟机运行的Dalvik字节码的过程。
编译的整个过程会涉及词法分析、语法分析 、语义检查和代码优化等步骤。对于底层编译原理感兴趣的同学,你可以挑战一下编译原理的三大经典巨作:龙书、虎书、鲸鱼书。
但今天我们的重点不是底层的编译原理而是希望一起讨论Android编译需要解决的问题是什么目前又遇到了哪些挑战以及国内外大厂又给出了什么样的解决方案。
1. Android编译的基础知识
无论是微信的编译优化还是Tinker项目都涉及比较多的编译相关知识因此我在Android编译方面研究颇多经验也比较丰富。Android的编译构建流程主要包括代码、资源以及Native Library三部分整个流程可以参考官方文档的构建流程图。
Gradle是Android官方的编译工具它也是GitHub上的一个开源项目。从Gradle的更新日志可以看到当前这个项目还更新得非常频繁基本上每一两个月都会有新的版本。对于Gradle我感觉最痛苦的还是Gradle Plugin的编写主要是因为Gradle在这方面没有完善的文档因此一般都只能靠看源码或者断点调试的方法。
但是编译实在太重要了每个公司的情况又各不相同必须强行造一套自己的“轮子”。已经开源的项目有Facebook的Buck以及Google的Bazel。
为什么要自己“造轮子”呢?主要有下面几个原因:
统一编译工具。Facebook、Google都有专门的团队负责编译工作他们希望内部的所有项目都使用同一套构建工具这里包括Android、Java、iOS、Go、C++等。编译工具的统一优化,所有项目都会受益。
代码组织管理架构。Facebook和Google的代码管理有一个非常特别的地方就是整个公司的所有项目都放到同一个仓库里面。因此整个仓库非常庞大所以他们也不会使用Git。目前Google使用的是PiperFacebook是基于HG修改的也是一种基于分布式的文件系统。
极致的性能追求。Buck和Bazel的性能的确比Gradle更好内部包含它们的各种编译优化。但是它们或多或少都有一些定制的味道例如对Maven、JCenter这样的外部依赖支持的也不是太好。
“程序员最痛恨写文档还有别人不写文档”所以它们的文档也是比较少的如果想做二次定制开发会感到很痛苦。如果你想把编译工具切换到Buck和Bazel需要下很大的决心而且还需要考虑和其他上下游项目的协作。当然即使我们不去直接使用它们内部的优化思路也非常值得我们学习和参考。
Gradle、Buck、Bazel都是以更快的编译速度、更强大的代码优化为目标我们下面一起来看看它们做了哪些努力。
2. 编译速度
回想一下我们的Android开发生涯在编译这件事情上面究竟浪费了多少时间和生命。正如前面我所说编译速度对团队效率非常重要。
关于编译速度我们最关心的可能还是编译Debug包的速度尤其是增量编译incremental build的速度希望可以做到更加快速的调试。正如下图所示我们每次代码验证都要经过编译和安装两个步骤。
编译时间。把Java或者Kotlin代码编译为“.class“文件然后通过dx编译为Dex文件。对于增量编译我们希望编译尽可能少的代码和资源最理想情况是只编译变化的部分。但是由于代码之间的依赖大部分情况这并不可行。这个时候我们只能退而求其次希望编译更少的模块。Android Plugin 3.0使用Implementation代替Compile正是为了优化依赖关系。
安装时间。我们要先经过签名校验校验成功后会有一大堆的文件拷贝工作例如APK文件、Library文件、Dex文件等。之后我们还需要编译Odex文件这个过程特别是在Android 5.0和6.0会非常耗时。对于增量编译最好的优化是直接应用新的代码无需重新安装新的APK。
对于增量编译我先来讲讲Gradle的官方方案Instant Run。在Android Plugin 2.3之前它使用的Multidex实现。在Android Plugin 2.3之后它使用Android 5.0新增的Split APK机制。
如下图所示资源和Manifest都放在Base APK中 在Base APK中代码只有Instant Run框架应用的本身的代码都在Split APK中。
Instant Run有三种模式如果是热交换和温交换我们都无需重新安装新的Split APK它们的区别在于是否重启Activity。对于冷交换我们需要通过adb install-multiple -r -t重新安装改变的Split APK应用也需要重启。
虽然无论哪一种模式我们都不需要重新安装Base APK。这让Instant Run看起来是不是很不错但是在大型项目里面它的性能依然非常糟糕主要原因是
多进程问题。“The app was restarted since it uses multiple processes”如果应用存在多进程热交换和温交换都不能生效。因为大部分应用都会存在多进程的情况Instant Run的速度也就大打折扣。
Split APK安装问题。虽然Split APK的安装不会生成Odex文件但是这里依然会有签名校验和文件拷贝APK安装的乒乓机制。这个时间需要几秒到几十秒是不能接受的。
javac问题。在Gradle 4.6之前如果项目中运用了Annotation Processor。那不好意思本次修改以及它依赖的模块都需要全量javac而这个过程是非常慢的可能会需要几十秒。这个问题直到Gradle 4.7才解决关于这个问题原因的讨论你可以参考这个Issue。
你还可以看看这一个Issue“full rebuild if a class contains a constant”假设修改的类中包含一个“public static final”的变量那同样也不好意思本次修改以及它依赖的模块都需要全量javac。这是为什么呢因为常量池是会直接把值编译到其他类中Gradle并不知道有哪些类可能使用了这个常量。
询问Gradle的工作人员他们出给的解决方案是下面这个
// 原来的常量定义:
public static final int MAGIC = 23
// 将常量定义替换成方法:
public static int magic() {
return 23;
}
对于大型项目来说这肯定是不可行的。正如我在Issue中所写的一样无论我们是不是真正改到这个常量Gradle都会无脑的全量javac这样肯定是不对的。事实上我们可以通过比对这次代码修改看看是否有真正改变某一个常量的值。
但是可能用过阿里的Freeline或者蘑菇街的极速编译的同学会有疑问它们的方案为什么不会遇到Annotation和常量的问题
事实上它们的方案在大部分情况比Instant Run更快那是因为牺牲了正确性。也就是说它们为了追求更快的速度直接忽略了Annotation和常量改变可能带来错误的编译产物。Instant Run作为官方方案它优先保证的是100%的正确性。
当然Google的人也发现了Instant Run的种种问题在Android Studio 3.5之后对于Android 8.0以后的设备将会使用新的方案“Apply Changes”代替Instant Run。目前我还没找到关于这套方案更多的资料不过我认为应该是抛弃了Split APK机制。
一直以来我心目中都有一套理想的编译方案这套方案安装的Base APK依然只是一个壳APK真正的业务代码放到Assets的ClassesN.dex中。
无需安装。依然使用类似Tinker热修复的方法每次只把修改以及依赖的类插入到pathclassloader的最前方就可以不熟悉的同学可以参考《微信Android热补丁实践演进之路》中的Qzone方案。
Oatmeal。为了解决首次运行时Assets中ClassesN.dex的Odex耗时问题我们可以使用“安装包优化“中讲过的ReDex中的黑科技Oatmeal。它可以在100毫秒以内生成一个完全解释执行的Odex文件。
关闭JIT。我们通过在AndroidManifest指定android:vmSafeMode=“true”关闭虚拟机的JIT优化这样也就不会出现Tinker在Android N混合编译遇到的问题。
这套方案应该可以完全解决Instant Run当前的各种问题我也希望对编译优化感兴趣的同学可以自行实现这一套方案并能开源出来。
对于编译速度的优化,我还有几个建议:
更换编译机器。对于实力雄厚的公司直接更换Mac或者其他更给力的设备作为编译机这种方式是最简单的。
Build Cache。可以将大部分不常改变的项目拆离出去并使用远端Cache模式保留编译后的缓存。
升级Gradle和SDK Build Tools。我们应该及时去升级最新的编译工具链享受Google的最新优化成果。
使用Buck。无论是Buck的exopackage还是代码的增量编译Buck都更加高效。但我前面也说过一个大型项目如果要切换到Buck其实顾虑还是比较多的。在2014年初微信就接入了Buck但是因为跟其他项目协作的问题导致在2015年切换回Gradle方案。相比之下可能目前最热的Flutter中Hot Reload秒级编译功能会更有吸引力。
当然最近几个Android Studio版本Google也做了大量的其他优化例如使用AAPT2替代了AAPT来编译Android资源。AAPT2实现了资源的增量编译它将资源的编译拆分成Compile和Link两个步骤。前者资源文件以二进制形式编译Flat格式后者合并所有的文件再打包。
除了AAPT2Google还引入了d8和R8下面分别是Google提供的一些测试数据。
-
那什么是d8和R8呢除了编译速度的优化它们还有哪些其他的作用
3. 代码优化
对于Debug包编译我们更关心速度。但是对于Release包来说代码的优化更加重要因为我们会更加在意应用的性能。
下面我就分别讲讲ProGuard、d8、R8和ReDex这四种我们可能会用到的代码优化工具。
ProGuard
在微信Release包12分钟的编译过程里单独ProGuard就需要花费8分钟。尽管ProGuard真的很慢但是基本每个项目都会使用到它。加入了ProGuard之后应用的构建过程流程如下
ProGuard主要有混淆、裁剪、优化这三大功能它的整个处理流程是
其中优化包括内联、修饰符、合并类和方法等30多种具体介绍与使用方法你可以参考官方文档。
d8
Android Studio 3.0推出了d8并在3.1正式成为默认工具。它的作用是将“.class”文件编译为Dex文件取代之前的dx工具。
d8除了更快的编译速度之外还有一个优化是减少生成的Dex大小。根据Google的测试结果大约会有3%5%的优化。
R8
R8在Android Studio 3.1中引入它的志向更加高远它的目标是取代ProGuard和d8。我们可以直接使用R8把“.class”文件变成Dex。
同时R8还支持ProGuard中混淆、裁剪、优化这三大功能。由于目前R8依然处于实验阶段网上的介绍资料并不多你可以参考下面这些资料
ProGuard和R8对比ProGuard and R8: a comparison of optimizers。
Jake Wharton大神的博客最近有很多R8相关的文章https://jakewharton.com/blog/。
R8的最终目的跟d8一样一个是加快编译速度一个是更强大的代码优化。
ReDex
如果说R8是未来想取代的ProGuard的工具那Facebook的内部使用的ReDex其实已经做到了。
Facebook内部的很多项目都已经全部切换到ReDex不再使用ProGuard了。跟ProGuard不同的是它直接输入的对象是Dex而不是“.class”文件也就是它是直接针对最终产物的优化所见即所得。
在前面的文章中我已经不止一次提到ReDex这个项目因为它里面的功能实在是太强大了具体可以参考专栏前面的文章《包体积优化如何减少安装包大小》。
Interdex类重排和文件重排、Dex分包优化。
Oatmeal直接生成的Odex文件。
StripDebugInfo去除Dex中的Debug信息。
此外ReDex中例如Type Erasure和去除代码中的Aceess方法也是非常不错的功能它们无论对包体积还是应用的运行速度都有帮助因此我也鼓励你去研究和实践一下它们的用法和效果。但是ReDex的文档也是万年不更新的而且里面掺杂了一些Facebook内部定制的逻辑所以它用起来的确非常不方便。目前我主要还是直接研究它的源码参考它的原理然后再直接单独实现。
事实上Buck里面其实也还有很多好用的东西但是文档里面依然什么都没有提到所以还是需要“read the source code”。
Library Merge和Relinker
多语言拆分
分包支持
ReDex支持
持续交付
Gradle、Buck、Bazel它们代表的都是狭义上的编译我认为广义的编译应该包括打包构建、Code Review、代码工程管理、代码扫描等流程也就是业界最近经常提起的持续集成。
目前最常用的持续集成工具有Jenkins、GitLab CI、Travis CI等GitHub也有提供自己的持续集成服务。每个大公司都有自己的持续集成方案例如腾讯的RDM、阿里的摩天轮、大众点评的MCI等。
下面我来简单讲一下我对持续集成的一些经验和看法:
自定义代码检查。每个公司都会有自己的编码规范代码检查的目的在于防止不符合规范的代码提交到远程仓库中。比如微信就定义了一套代码规范并且写了专门的插件来检测。例如日志规范、不能直接使用new Thread、new Handler等而且违反者将会得到一定的惩罚。自定义代码检测可以通过完全自己实现或者扩展Findbugs插件例如美团它们就利用Findbugs实现了Android漏洞扫描工具Code Arbiter。
第三方代码检查。业界比较常用的代码扫描工具有收费的Coverity以及Facebook开源的Infer例如空指针、多线程问题、资源泄漏等很多问题都可以扫描出来。除了增加检测流程我最大的体会是需要同时增加人员的培训。我遇到很多开发者为了解决扫描出来的问题空指针就直接判空、多线程就直接加锁最后可能会造成更加严重的问题。
Code Review。关于Code Review集成GitLab、Phabricator或者Gerrit都是不错的选择。我们一定要重视Code Review这也是给其他人展示我们“伟大”代码的机会。而且我们自己应该是第一个Code Reviewer在给别人Review之前自己先以第三者的角度审视一次代码。这样先通过自己这一关的考验既尊重了别人的时间也可以为自己树立良好的技术品牌。
持续集成涉及的流程有很多,你需要结合自己团队的现状。如果只是一味地去增加流程,有时候可能适得其反。
总结
在Android 8.0Google引入了Dexlayout库实现类和方法的重排Facebook的Buck也第一时间引入了AAPT2。ReDex、d8、R8其实都是相辅相成可以看到Google也在摄取社区的知识但同时我们也会从Google的新技术发展里寻求思路。
我在写今天的内容时还有另外一个体会Google为了解决Android编译速度的问题花了大量的力气结果却不尽如人意。我想说如果我们敢于跳出系统的制约可能才会彻底解决这个问题正如在Flutter上面就可以完美实现秒级编译。其实做人、做事也是如此我们经常会陷入局部最优解的困局或者走进“思维怪圈”这时如果能跳出路径依赖从更高的维度重新思考、审视全局得到的体会可能会完全不一样。
课后作业
在你的工作中,遇到过哪些编译问题?有没有做过具体优化编译速度的工作?对于编译,你还有哪些疑问?欢迎留言跟我和其他同学一起讨论。
对于Android Build System可以说每年都会有不少的变化也有很多新的东西出来。所以我们应该保持敏感度你会发现很多工具都非常有用例如Desugar、Dexlayout、JVM TI、App Bundle等。
今天的课后作业是请你观看2018年Google I/O编译工具相关的视频在留言中写下自己的心得体会。
Whats new with the Android build system (Google I/O 18)
Whats new in Android development tools
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,305 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 编译插桩的三种方法AspectJ、ASM、ReDex
只要简单回顾一下前面课程的内容你就会发现,在启动耗时分析、网络监控、耗电监控中已经不止一次用到编译插桩的技术了。那什么是编译插桩呢?顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。
如上图所示请你回忆一下Java代码的编译流程思考一下插桩究竟是在编译流程中的哪一步工作除了我们之前使用的一些场景它还有哪些常见的应用场景在实际工作中我们应该怎样更好地使用它现在都有哪些常用的编译插桩方法今天我们一起来解决这些问题。
编译插桩的基础知识
不知道你有没有注意到在编译期间修改和生成代码其实是很常见的行为无论是Dagger、ButterKnife这些APTAnnotation Processing Tool注解生成框架还是新兴的Kotlin语言编译器它们都用到了编译插桩的技术。
下面我们一起来看看还有哪些场景会用到编译插桩技术。
1. 编译插桩的应用场景
编译插桩技术非常有趣,同样也很有价值,掌握它之后,可以完成一些其他技术很难实现或无法完成的任务。学会这项技术以后,我们就可以随心所欲地操控代码,满足不同场景的需求。
代码生成。除了Dagger、ButterKnife这些常用的注解生成框架Protocol Buffers、数据库ORM框架也都会在编译过程生成代码。代码生成隔离了复杂的内部实现让开发更加简单高效而且也减少了手工重复的劳动量降低了出错的可能性。
代码监控。除了网络监控和耗电监控我们可以利用编译插桩技术实现各种各样的性能监控。为什么不直接在源码中实现监控功能呢首先我们不一定有第三方SDK的源码其次某些调用点可能会非常分散例如想监控代码中所有new Thread()调用,通过源码的方式并不那么容易实现。
代码修改。我们在这个场景拥有无限的发挥空间例如某些第三方SDK库没有源码我们可以给它内部的一个崩溃函数增加try catch或者说替换它的图片库等。我们也可以通过代码修改实现无痕埋点就像网易的HubbleData、51信用卡的埋点实践。
代码分析。上一期我讲到持续集成里面的自定义代码检查就可以使用编译插桩技术实现。例如检查代码中的new Thread()调用、检查代码中的一些敏感权限使用等。事实上Findbugs这些第三方的代码检查工具也同样使用的是编译插桩技术实现。
“一千个人眼中有一千个哈姆雷特”,通过编译插桩技术,你可以大胆发挥自己的想象力,做一些对提升团队质量和效能有帮助的事情。
那从技术实现上看,编译插桩是从代码编译的哪个流程介入的呢?我们可以把它分为两类:
Java文件。类似APT、AndroidAnnotation这些代码生成的场景它们生成的都是Java文件是在编译的最开始介入。
字节码Bytecode。对于代码监控、代码修改以及代码分析这三个场景一般采用操作字节码的方式。可以操作“.class”的Java字节码也可以操作“.dex”的Dalvik字节码这取决于我们使用的插桩方法。
相对于Java文件方式字节码操作方式功能更加强大应用场景也更广但是它的使用复杂度更高所以今天我主要来讲如何通过操作字节码实现编译插桩的功能。
2. 字节码
对于Java平台Java虚拟机运行的是Class文件内部对应的是Java字节码。而针对Android这种嵌入式平台为了优化性能Android虚拟机运行的是Dex文件Google专门为其设计了一种Dalvik字节码虽然增加了指令长度但却缩减了指令的数量执行也更为快速。
那这两种字节码格式有什么不同呢下面我们先来看一个非常简单的Java类。
public class Sample {
public void test() {
System.out.print("I am a test sample!");
}
}
通过下面几个命令我们可以生成和查看这个Sample.java类的Java字节码和Dalvik字节码。
javac Sample.java // 生成Sample.class也就是Java字节码
javap -v Sample // 查看Sample类的Java字节码
//通过Java字节码生成Dalvik字节码
dx --dex --output=Sample.dex Sample.class
dexdump -d Sample.dex // 查看Sample.dex的Dalvik的字节码
你可以直观地看到Java字节码和Dalvik字节码的差别。
它们的格式和指令都有很明显的差异。关于Java字节码的介绍你可以参考JVM文档。对于Dalvik字节码来说你可以参考Android的官方文档。它们的主要区别有
体系结构。Java虚拟机是基于栈实现而Android虚拟机是基于寄存器实现。在ARM平台寄存器实现性能会高于栈实现。
格式结构。对于Class文件每个文件都会有自己单独的常量池以及其他一些公共字段。对于Dex文件整个Dex中的所有Class共用同一个常量池和公共字段所以整体结构更加紧凑因此也大大减少了体积。
指令优化。Dalvik字节码对大量的指令专门做了精简和优化如下图所示相同的代码Java字节码需要100多条而Dalvik字节码只需要几条。
关于Java字节码和Dalvik字节码的更多介绍你可以参考下面的资料
Dalvik and ART
Understanding the Davlik Virtual Machine
编译插桩的三种方法
AspectJ和ASM框架的输入和输出都是Class文件它们是我们最常用的Java字节码处理框架。
1. AspectJ
AspectJ是Java中流行的AOPaspect-oriented programming编程扩展框架网上很多文章说它处理的是Java文件其实并不正确它内部也是通过字节码处理技术实现的代码注入。
从底层实现上来看AspectJ内部使用的是BCEL框架来完成的不过这个库在最近几年没有更多的开发进展官方也建议切换到ObjectWeb的ASM框架。关于BCEL的使用你可以参考《用BCEL设计字节码》这篇文章。
从使用上来看作为字节码处理元老AspectJ的框架的确有自己的一些优势。
成熟稳定。从字节码的格式和各种指令规则来看字节码处理不是那么简单如果处理出错就会导致程序编译或者运行过程出问题。而AspectJ作为从2001年发展至今的框架它已经很成熟一般不用考虑插入的字节码正确性的问题。
使用简单。AspectJ功能强大而且使用非常简单使用者完全不需要理解任何Java字节码相关的知识就可以使用自如。它可以在方法包括构造方法被调用的位置、在方法体包括构造方法的内部、在读写变量的位置、在静态代码块内部、在异常处理的位置等前后插入自定义的代码或者直接将原位置的代码替换为自定义的代码。
在专栏前面文章里我提过360的性能监控框架ArgusAPM它就是使用AspectJ实现性能的监控其中TraceActivity是为了监控Application和Activity的生命周期。
// 在Application onCreate执行的时候调用applicationOnCreate方法
@Pointcut("execution(* android.app.Application.onCreate(android.content.Context)) && args(context)")
public void applicationOnCreate(Context context) {
}
// 在调用applicationOnCreate方法之后调用applicationOnCreateAdvice方法
@After("applicationOnCreate(context)")
public void applicationOnCreateAdvice(Context context) {
AH.applicationOnCreate(context);
}
你可以看到我们完全不需要关心底层Java字节码的处理流程就可以轻松实现编译插桩功能。关于AspectJ的文章网上有很多不过最全面的还是官方文档你可以参考《AspectJ程序设计指南》和The AspectJ 5 Development Kit Developers Notebook这里我就不详细描述了。
但是从AspectJ的使用说明里也可以看出它的一些劣势它的功能无法满足我们某些场景的需要。
切入点固定。AspectJ只能在一些固定的切入点来进行操作如果想要进行更细致的操作则无法完成它不能针对一些特定规则的字节码序列做操作。
正则表达式。AspectJ的匹配规则是类似正则表达式的规则比如匹配Activity生命周期的onXXX方法如果有自定义的其他以on开头的方法也会匹配到。
性能较低。AspectJ在实现时会包装自己的一些类逻辑比较复杂不仅生成的字节码比较大而且对原函数的性能也会有所影响。
我举专栏第7期启动耗时Sample的例子我们希望在所有的方法调用前后都增加Trace的函数。如果选择使用AspectJ那么实现真的非常简单。
@Before("execution(* **(..))")
public void before(JoinPoint joinPoint) {
Trace.beginSection(joinPoint.getSignature().toString());
}
@After("execution(* **(..))")
public void after() {
Trace.endSection();
}
但你可以看到经过AspectJ的字节码处理它并不会直接把Trace函数直接插入到代码中而是经过一系列自己的封装。如果想针对所有的函数都做插桩AspectJ会带来不少的性能影响。
不过大部分情况我们可能只会插桩某一小部分函数这样AspectJ带来的性能影响就可以忽略不计了。如果想在Android中直接使用AspectJ还是比较麻烦的。这里我推荐你直接使用沪江的AspectJX框架它不仅使用更加简便一些而且还扩展了排除某些类和JAR包的能力。如果你想通过Annotation注解方式接入我推荐使用Jake Wharton大神写的Hugo项目。
虽然AspectJ使用方便但是在使用的时候不注意的话还是会产生一些意想不到的异常。比如使用Around Advice需要注意方法返回值的问题在Hugo里的处理方法是将joinPoint.proceed()的返回值直接返回同时也需要注意Advice Precedence的情况。
2. ASM
如果说AspectJ只能满足50%的字节码处理场景那ASM就是一个可以实现100%场景的Java字节码操作框架它的功能也非常强大。使用ASM操作字节码主要的特点有
操作灵活。操作起来很灵活,可以根据需求自定义修改、插入、删除。
上手难。上手比较难需要对Java字节码有比较深入的了解。
为了使用简单相比于BCEL框架ASM的优势是提供了一个Visitor模式的访问接口Core API使用者可以不用关心字节码的格式只需要在每个Visitor的位置关心自己所修改的结构即可。但是这种模式的缺点是一般只能在一些简单场景里实现字节码的处理。
事实上专栏第7期启动耗时的Sample内部就是使用ASM的Core API具体你可以参考MethodTracer类的实现。从最终效果上来看ASM字节码处理后的效果如下。
相比AspectJASM更加直接高效。但是对于一些复杂情况我们可能需要使用另外一种Tree API来完成对Class文件更直接的修改因此这时候你要掌握一些必不可少的Java字节码知识。
此外我们还需要对Java虚拟机的运行机制有所了解前面我就讲到Java虚拟机是基于栈实现。那什么是Java虚拟机的栈呢引用《Java虚拟机规范》里对Java虚拟机栈的描述
每一条Java虚拟机线程都有自己私有的Java虚拟机栈这个栈与线程同时创建用于存储栈帧Stack Frame
正如这句话所描述的,每个线程都有自己的栈,所以在多线程应用程序中多个线程就会有多个栈,每个栈都有自己的栈帧。
如下图所示我们可以简单认为栈帧包含3个重要的内容本地变量表Local Variable Array、操作数栈Operand Stack和常量池引用Constant Pool Reference
本地变量表。在使用过程中可以认为本地变量表是存放临时数据的并且本地变量表有个很重要的功能就是用来传递方法调用时的参数当调用方法的时候参数会依次传递到本地变量表中从0开始的位置上并且如果调用的方法是实例方法那么我们可以通过第0个本地变量中获取当前实例的引用也就是this所指向的对象。
操作数栈。可以认为操作数栈是一个用于存放指令执行所需要的数据的位置,指令从操作数栈中取走数据并将操作结果重新入栈。
由于本地变量表的最大数和操作数栈的最大深度是在编译时就确定的所以在使用ASM进行字节码操作后需要调用ASM提供的visitMaxs方法来设置maxLocal和maxStack数。不过ASM为了方便用户使用已经提供了自动计算的方法在实例化ClassWriter操作类的时候传入COMPUTE_MAXS后ASM就会自动计算本地变量表和操作数栈。
ClassWriter(ClassWriter.COMPUTE_MAXS)
下面以一个简单的“1+2“为例它的操作数以LIFO后进先出的方式进行操作。
ICONST_1将int类型1推送栈顶ICONST_2将int类型2推送栈顶IADD指令将栈顶两个int类型的值相加后将结果推送至栈顶。操作数栈的最大深度也是由编译期决定的很多时候ASM修改后的代码会增加操作数栈最大深度。不过ASM已经提供了动态计算的方法但同时也会带来一些性能上的损耗。
在具体的字节码处理过程中特别需要注意的是本地变量表和操作数栈的数据交换和try catch blcok的处理。
数据交换。如下图所示在经过IADD指令操作后又通过ISTORE 0指令将栈顶int值存入第1个本地变量中用于临时保存在最后的加法过程中将0和1位置的本地变量取出压入操作数栈中供IADD指令使用。关于本地变量和操作数栈数据交互的指令你可以参考虚拟机规范里面提供了一系列根据不同数据类型的指令。
异常处理。在字节码操作过程中需要特别注意异常处理对操作数栈的影响如果在try和catch之间抛出了一个可捕获的异常那么当前的操作数栈会被清空并将异常对象压入这个空栈中执行过程在catch处继续。幸运的是如果生成了错误的字节码编译器可以辨别出该情况并导致编译异常ASM中也提供了字节码Verify的接口可以在修改完成后验证一下字节码是否正常。
如果想在一个方法执行完成后增加代码ASM相对也要简单很多可以在字节码中出现的每一条RETURN系或者ATHROW的指令前增加处理的逻辑即可。
3. ReDex
ReDex不仅只是作为一款Dex优化工具它也提供了很多的小工具和文档里没有提到的一些新奇功能。比如在ReDex里提供了一个简单的Method Tracing和Block Tracing工具这个工具可以在所有方法或者指定方法前面插入一段跟踪代码。
官方提供了一个例子用来展示这个工具的使用具体请查看InstrumentTest。这个例子会将InstrumentAnalysis的onMethodBegin方法插入到除黑名单以外的所有方法的开头位置。具体配置如下
"InstrumentPass" : {
"analysis_class_name": "Lcom/facebook/redextest/InstrumentAnalysis;", //存在桩代码的类
"analysis_method_name": "onMethodBegin", //存在桩代码的方法
"instrumentation_strategy": "simple_method_tracing"
, //插入策略有两种方案一种是在方法前面插入simple_method_tracing一种是在CFG 的Block前后插入basic_block_tracing
}
ReDex的这个功能并不是完整的AOP工具但它提供了一系列指令生成API和Opcode插入API我们可以参照这个功能实现自己的字节码注入工具这个功能的代码在Instrument.cpp中。
这个类已经将各种字节码特殊情况处理得相对比较完善我们可以直接构造一段Opcode调用其提供的Insert接口即可完成代码的插入而不用过多考虑可能会出现的异常情况。不过这个类提供的功能依然耦合了ReDex的业务所以我们需要提取有用的代码加以使用。
由于Dalvik字节码发展时间尚短而且因为Dex格式更加紧凑修改起来往往牵一发而动全身。并且Dalvik字节码的处理相比Java字节码会更加复杂一些所以直接操作Dalvik字节码的工具并不是很多。
市面上大部分需要直接修改Dex的情况是做逆向很多同学都采用手动书写Smali代码然后编译回去。这里我总结了一些修改Dalvik字节码的库。
ASMDEX开发者是ASM库的开发者但很久未更新了。
DexterGoogle官方开发的Dex操作库更新很频繁但使用起来很复杂。
Dexmaker用来生成Dalvik字节码的代码。
Soot修改Dex的方法很另类是先将Dalvik字节码转成一种Jimple three-address code然后插入Jimple Opcode后再转回Dalvik字节码具体可以参考例子。
总结
今天我介绍了几种比较有代表性的框架来讲解编译插桩相关的内容。代码生成、代码监控、代码魔改以及代码分析,编译插桩技术无所不能,因此需要我们充分发挥想象力。
对于一些常见的应用场景前辈们付出了大量的努力将它们工具化、API化让我们不需要懂得底层字节码原理就可以轻松使用。但是如果真要想达到随心所欲的境界即使有类似ASM工具的帮助也还是需要我们对底层字节码有比较深的理解和认识。
当然你也可以成为“前辈”,将这些场景沉淀下来,提供给后人使用。但有的时候“能力限制想象力”,如果能力不够,即使想象力到位也无可奈何。
课后作业
你使用过哪些编译插桩相关的工具?使用编译插桩实现过什么功能?欢迎留言跟我和其他同学一起讨论。
今天的课后作业是重温专栏第7期练习Sample的实现原理看看它内部是如何使用ASM完成TAG的插桩。在今天的Sample里我也提供了一个使用AspectJ实现的版本。想要彻底学会编译插桩的确不容易单单写一个高效的Gradle Plugin就不那么简单。
除了上面的两个Sample我也推荐你认真看看下面的一些参考资料和项目。
一起玩转Android项目中的字节码
字节码操纵技术探秘
ASM 6 Developer Guide
Java字节码(Bytecode)与ASM简单说明
Dalvik and ART
Understanding the Davlik Virtual Machine
基于ASM的字节码处理工具Hunter和Hibeaver
基于Javassist的字节码处理工具DroidAssist
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,183 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 大数据与AI如何高效地测试
测试作为持续交付中重要的一个环节,它的使命是发现交付过程的质量问题。随着互联网迭代速度的加快,很多产品都是两周甚至每周一个版本,留给测试的时间越来越少。
那在这么短的时间,如何保障产品的质量,怎样高效地测试呢?我们研发模式在不断地变化,测试的定位又有哪些改变,而未来的测试又会发展成什么样的形态呢?
测试的演进历程
“专业的事情留给专业的人员”,社会各领域的分工越来越细,设计、研发、产品、测试大家各司其职,共同完成一个产品。但随着技术的发展,这样的分工并不是一成不变,从近两年大公司技术部门的组织调整来看,测试和开发的角色已经在不断融合。
互联网发展到今天,测试的职责发生了哪些改变?移动端测试又经历了怎么样的演进历程呢?
1. 测试的田园时代
在移动互联网起步之初,基本上还处在传统的软件研发阶段。产品将需求交给研发,研发实现后交给测试,测试把最终产品交付给用户。我们可以把这个阶段叫作测试的“田园时代“。
测试作为研发流程的一个环节只是作为产品交付给用户前的一道屏障承担着质量保证工作。在这个阶段测试有两个最重要的考核指标每个版本发现的Bug数量和遗漏到线上的Bug数量。
在测试的“田园时代”很多Bug可能会出现多次返工产品的交付流程也很难快起来。这个阶段的主要问题有
人员对立。测试的KPI是尽可能地发现开发人员的Bug他们之间非常容易发生对立。特别是出现线上事故的时候开发埋怨测试没用测试埋怨开发无能。
效率低下。虽然引入了Monkey这些基本的自动化工具但是大多数的测试方法还是依赖人工执行。尽管我们不停地加大测试与研发人员的比例但整个过程还是问题多多很难缩短整个产品的交付周期。
2. 测试的效能时代
随着互联网竞争的加剧,交付速度成为了产品的核心能力。国内外的大公司开始适应趋势的变化,把测试团队的职责转变成为团队的效能服务。
测试不只是负责交付质量还需要同时思考产品的质量和效率。正如我前面说到的“211”效能目标也就是2周交付周期、1周开发周期以及1小时发布时长测试人员需要直接为这个目标负责思考如何快速并且高质量完成产品的交付。
长兄于病视神,未有形而除之,故名不出于家。中兄治病,其在毫毛,故名不出于闾。若扁鹊者,镵血脉,投毒药,副肌肤,闲而名出闻于诸侯。
这是扁鹊讲三兄弟治病的故事,说的是长兄治病,是治于病情未发作之前,由于一般人不知道他事先能铲除病因,所以他的名气无法传出去。中兄治病,是治于病情初起之时,一般人以为他只能治轻微的小病,所以他的名气只在乡里。而扁鹊是治于病情严重之时,在经脉上穿针管来放血,在皮肤上敷药,所以都以为我的医术最高明,名气因此响遍天下。
从这个故事来看,扁鹊认为长兄的医术最高,可以做到“治病于未发”。回到我们今天所谈的测试,很多公司已经开始提出“测试左移”,也是希望测试在更早期的阶段介入到交付过程中,不仅是发现问题,要考虑更多的是如何避免问题的出现。
测试需要深入到产品从设计到发布的各个流程,要做到“比产品更懂技术,比研发更懂业务”。为了顺应这个变化,各大公司在组织架构上开始把大型的测试团队打散,揉碎进各个业务开发团队中。“产研测一体化”目的在于统一团队的目标和方向,让所有人为了提升产品效能这个共同目标而努力。这样团队中所有成员成为相亲相爱的一家人,也消除了“产研测”之间的对立现象。
但是在《如何衡量研发效能》一文中所提到的:“在产品迭代前期,团队集中设计、编码,引入缺陷,但并未即时地集成和验证。缺陷一直掩藏在系统中,直到项目后期,团队才开始集成和测试,缺陷集中爆发”。
虽然通过持续交付模式,可以一定程度上削减提交波峰,但是依然无法避免经常出现的“踩点”提交。在测试的效能时代,如何提升测试的效率依然是急需解决的问题。
在这个阶段我给出的答案是持续集成的工具化和平台化。从需求发起到分支管理、Code Review、代码检查以及测试发布等测试团队负责把控各式各样的工具或平台。
由于整个持续交付过程涉及各个阶段的平台工具,这里我只挑跟测试相关的两个平台重点来讲。
测试平台。竞品测试、弱网络测试、启动测试、UI测试等整个测试流程引入了大量的自动化工具。各大公司也改良或创造了不少好用的工具例如腾讯的New Monkey工具可以大大提升Monkey的智能度和覆盖率。除了我们比较熟悉的Espresso、UIAutomator、Appium以及Robotium自动化框架像阿里的Macaca、Facebook的Screenshot Tests for Android都是值得学习的开源测试框架。当然也有一些公司把测试平台打包成服务对外公开例如腾讯的WeTest、华为的开发者服务等。
下面是这些常用工具的简单对比,供你参考。
体验平台。在测试的效能时代自动化并不能完全取代人工测试。在产品交付的不同阶段我们需要提高团队内外人员的参与度。无论每日的Daily Build、封版时的集中体验还是测试期间的员工内部体验、灰度期间的外部众测平台的目的是让尽量多的成员都参与到产品的体验中提升团队成员对产品的认同感。当然各大公司也都有自己的体验平台例如腾讯的RDM、蚂蚁的伙伴APP等。
测试的效能时代也是目前大多数公司所处的阶段,据我了解很多公司的工程效能团队,也是从测试团队演进而来的。随着测试团队职能的转变以及技术深度的提升,会涌现出一大批资深的测试开发人员,也会有更多优秀的测试人员走向开发或者产品的岗位。
不过我发现关于测试国内外也有一些差异例如国外十分推崇开发编写的test case但在国内却非常不容易推行。这主要是因为国内业务的迭代更加快速开发需求都做不完根本没有时间去写test case更不用说有的test case写起来可能比开发需求更费时间。
测试的智能时代
“人人都可以是测试”虽然在稳定性、兼容性又或者是性能测试的一些场景上我们做得非常不错但是对于某些自动化测试场景特别是UI测试目前还达不到人工测试的水平。
就拿UI测试为例由于版本迭代周期越来越短而且UI变动又非常频繁无论是开发还是业务测试人员对写测试脚本和用例的积极性都不是很高。由于测试脚本的编写成本和维护成本比较高可复用程度又比较低所以UI测试往往费时费力很多时候效率还不如人工测试。
那测试从效能时代走向下一阶段,在智能时代我们应该怎样去解决这些问题呢?
1. AI在测试的应用
AI技术在我们熟悉的围棋和星际争霸的人机大战中已经大放异彩了那它在测试领域可以擦出哪些不一样的火花呢
先看看我们在UI自动化测试中遇到的几个困境
覆盖率。自动化测试可以覆盖多少场景,如何解决登录、网络异常等各种各样情况的干扰。
效率。我们是否可以提高写测试用例的效率,或者是降低它的编写成本,做到人人都可以写用例。
准确性。如何提高整个自动化测试过程的稳定性,对于测试流程和最终结果,是否还需要大量的人工干预分析。
网易开源的Airtest、爱奇艺的AIon都尝试利用AI技术解决测试用例的编写效率和门槛问题。
这里主要用到图像识别以及OCR技术以爱奇艺的Alon为例它的整个处理流程是
图片处理。首先是场景的识别例如当前界面是否有弹出对话是否是登陆页面等然后通过对截屏进行图像分割。这里的难点在于文字的OCR识别以及布局的分类例如怎么样把不同的切割图像进行分类并且能够知道这块图像对应的内容。
结果判定。如何判定本次UI测试的结果是否是符合预期的相似度达到多高的程度等。因为UI测试可能遇到的情况有很多我们需要尽可能提升测试的准确性减少人工干预的情况。
在Alon的参考文章中还提到UI2Code这样一个应用场景也就是把一个应用截图或者把一个UI设计图通过图像识别生成对应的代码。其实就是Pixel to App希望实现的效果相信也是很多开发人员的梦想吧让我们可以彻底从UI开发中解放出来通过设计稿就可以直接生成最终的UI代码。
Airtest和Alon解决的核心问题是UI自动化测试的效率但是它们依然需要人工去编写测试用例。对于稳定性测试虽然我上面提到过腾讯的New Monkey不过它依然存在很多缺陷
覆盖场景。虽然是改进版本的Monkey但是它依然覆盖不了所有的用户场景而且很多场景它的执行流程不同于真实的用户。
非智能。这里你可以理解为非人类Monkey的操作和人类的操作方式完全不同。
那有没有更智能的解决方案呢Facebook的Sapienz尝试希望像真实用户一样去使用我们开发的应用它通过收集真实用户的操作路径来训练测试行为。而且在测试出崩溃后Sapienz会自动关联和定位代码提升解决问题的效率。
虽然AI测试目前还无法完全取代人工测试但随着技术的进一步成熟相信它对测试领域的变革将会是革命性的。当然想要实现这个目标我们还有很多工作要做这也意味着这其中还有很多技术创新的机会。
2. 大数据在测试的应用
现在越来越多的业务正在使用数据驱动的方式运作,测试的对象要从简单的代码转变为数据和算法。现在的业务越来越复杂,数据量越来越大,我们应该怎样及时发现产品的质量和业务问题呢?
国内的一些公司提出了基于大数据的“实时质量”体系,希望通过实时获取线上海量数据,完成业务数据校验和质量风险的感知。
这里的数据主要包括质量和业务两个方面。
质量。崩溃、启动速度、卡顿以及联网错误等质量问题,我们希望可以做到分钟级别的实时性,同时支持更加细粒度的维度分析。例如可以通过国家、城市、版本等维度来查看网络问题。
业务。对于产品的核心业务我们需要实现数据收集、分析与校验,打造数据监控和跟踪的能力。
对于基于大数据的“实时质量”测试体系,关键在于如何保证数据的实时性与准确性,这两点我会在“数据评估”的内容中与你详细讨论。
除了质量和业务数据,大公司做得比较多的还有用户反馈和舆情的跟踪和分析。各大公司基本都有自己的一套系统,通过爬取用户反馈、应用市场、微博、新闻资讯等各方面的消息来源,监控产品的舆情情况,你可以参考支付宝如何为移动端产品构建舆情分析体系。
总结
在APM系统搭建和持续交付优化过程我接触了很多测试工程师也关注了各大公司测试的现状。对于测试的职业道路发展我个人的建议是不管测试如何发展测试效率如何提升测试人员都要学会不断地变革变革自己、变革整个研发流程。我们不能只守着自己的一亩三分田需要尝试去做很多以前开发需要做的事情比如性能、稳定性、安全等方面的工作。说得严重一点如果你不能及时地更新自己的技术栈不去往更深入的底层走在测试的智能时代首先淘汰的就是传统的功能测试人员。
在这个变革的时代,我身边也有很多独当一面的测试工程师通过平台化的机遇晋级为专家,也有一些优秀的测试转向了产品或研发,所以说还是要提高自身的能力,把机会握在自己手上。
“学如逆水行舟,不进则退”,对于开发人员同样如此。现在平台工具和框架越来越成熟,很多初学者拿着一大堆开源工具,也能写出炫酷的界面。如果我们不去进步,特别是在大环境不好的时候,也会很容易被淘汰。
今天我分享了一些我对高效测试的心得和体会,如果同学们里有测试领域的专家,非常欢迎你来谈谈对这个行业和发展的看法,分享一下你对高效测试的看法。
课后作业
你所在的公司,目前测试正在处于哪个阶段?对于测试,你还有哪些疑问?欢迎留言跟我和其他同学一起讨论。
今天的课后作业是请你观看网易、爱奇艺以及Facebook关于AI在测试领域应用的分享在留言中写下自己的心得体会。
网易基于AI的网易UI自动化测试方案与实践
爱奇艺基于AI的移动端自动化测试框架的设计与实践
FacebookAutomated fault-finding with Sapienz
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,210 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
29 从每月到每天,如何给版本发布提速?
还记得我们在持续交付设定的目标吗我前面提到过天猫的效能目标是“211”也就是2周交付周期、1周开发周期以及1小时发布时长。对于一些更加敏捷的产品我们可能还会加快到每周一个版本。在如此快的节奏下我们该如何保证产品的质量还有哪些手段可以进一步为发布“提速保质”
更宽泛地说广义的发布并不仅限于把应用提交到市场。灰度、A/B测试 、运营活动、资源配置…我们的发布类型越来越多,也越来越复杂。该如何建立稳健的发布质量保障体系,防止出现线上事故呢?
APK的灰度发布
我们在讨论版本发布速度是需要兼顾效率和质量。如果不考虑交付质量每天一个版本也很轻松。在严格保证交付质量的前提下两周发布一个版本其实并不容易。特别是出现紧急安全或者稳定性问题的时候我们还需要有1小时的发布能力。
正如我在专栏“如何高效地测试”中说的,实现“高质高效”的发布需要强大的测试平台和数据验证平台的支撑。
下面我们一起来看看影响版本发布效率的那些重要因素,以及我对于提升版本发布速度的实践经验。
1. APK灰度
测试平台负责对发布包做各种维度的诊断测试通常会包括Monkey测试、性能测试启动、内存、CPU、卡顿等、竞品测试、UI测试、弱网络测试等。但是即使通过云测平台能够同时测试几十上百台机器本地测试依然无法覆盖所有的机型和用户路径。
为了安全稳定地发布新版本,我们需要先圈定少量用户安装试用,这就是灰度发布。而数据验证平台则负责收集灰度和线上版本的应用数据,这里可能包括性能数据、业务数据、用户反馈以及外部舆情等。
所以说,灰度效率首先被下面两个因素所影响:
测试效率。虽然灰度发布只影响少部分用户但是我们需要尽可能保障应用的质量以免造成用户流失。测试平台的发布测试时间是影响发布效率的第一个因素我们希望可以在1小时内明确待定的发布包是否达到上线标准。
数据验证效率。数据的全面性、实时性以及准确性都会影响灰度版本的评估决策时间是停止灰度发布还是进一步扩大灰度的用户量级或者可以直接发布到全量用户。对于核心数据需要建立小时甚至分钟级别的实时监控。比如微信对于性能数据可以在发布后1小时内评估完毕而业务数据可以在24小时内评估完毕。
另外一方面,如果我们的灰度发布想覆盖一万名用户,那多长时间才有足够的用户下载和安装呢?渠道的能力对灰度发布效率的影响也十分巨大,在国内主要有下面几个灰度渠道。
在国内由于没有统一的应用商店灰度渠道效率的确是一个非常严峻的问题。即使是微信如果不动用“红点提示”这个大杀器每天灰度发布到的用户量可能还不到十万。而国际市场有统一的Google Play可以通过Google Beta进行灰度。但是版本发布需要考虑GP审核的时间目前GP审核速度相比之前有所加快一般只需要一到两天时间。
通过灰度发布我们可以提前收集少部分用户新版本的性能和业务数据,但是它并不适用于精确评估业务数据的好坏。这主要是因为灰度的用户是有选择的,一般相对活跃的用户会被优先升级。
2. 动态部署
对于灰度发布,整个过程最大的痛点依然是灰度包的覆盖速度问题。而且传统的灰度方式还存在一个非常严重的问题,那就是无法回退。“发出去的包,就像泼出去的水”,如果出现严重问题,还可能造成灰度用户的流失。
Tinker动态部署框架正是为了解决这个问题而诞生的我们希望Tinker可以成为一种新的发布方式用来取代传统的灰度甚至是正式版本的发布。相比传统的发布方式热修复有很多得天独厚的优势。
快速。如果使用传统的发布方式微信需要10天时间覆盖50%的用户。而通过热修复在一天内可以覆盖80%以上的用户在3天内可以覆盖95%以上的用户。
可回退。当补丁出现重大问题的时候,可以及时回退补丁,让用户回到基础版本,尽可能降低损失。
为了提升补丁发布的效率微信还专门开发了TinkerBoots管理平台。TinkerBoots平台不仅支持人数、条件等参数设置例如可以选择只针对小米的某款机型下发10000人的补丁而且平台也会打通数据验证平台实现自动化的控量发布自动监控核心指标的变化情况保证发布质量。
Tinker发布已经两年多了虽然热修复技术可以解决很多问题但作为Tinker的作者我必须承认它对国内的Android开发造成了一些不好的影响。
用户是最好的测试。很多团队不再信奉前置的测试平台,他们认为反正有可以回退的动态部署发布,出现质量问题并不可怕,多发几个补丁就可以了。
性能低下。正如专栏“高质量开发”模块所说的,热修复、组件化这些黑科技会对应用的性能产生不小的影响,特别是启动的耗时。
从现在看来,热修复并不能取代发布,它更适合使用在少量用户的灰度发布。如果不是出现重大问题,一般情况也不应该发布针对所有用户的补丁。
组件化回归模块化热修复回归灰度这是国内很多大型App为了性能不得不做出的选择。如果想真正实现“随心所欲”的发布可能需要倒逼开发模式的变革例如从组件化转变为Web、React Native/Weex或者小程序来实现。
A/B测试
正如我前面所说APK灰度是对已有功能的线上验证它并不适合用于准确评估某个功能对业务的影响。
If you are not running experiments, you are probably not growing-
——by Sean Ellis
Sean Ellis是增长黑客模型AARRR之父增长黑客模型中提到的一个重要思想就是“A/B测试”。 Google、 Facebook、国内的头条和快手等公司都非常热衷于A/B测试希望通过测试进行科学的、数据驱动式的业务和产品决策。
那究竟什么是A/B测试如何正确地使用A/B测试呢
1. 什么是A/B测试
可能有同学会认为A/B测试并不复杂只是简单地把灰度用户分为A和B两部分然后通过对比收集到的数据分析得到测试的结论。
事实上A/B测试的难点就是A和B用户群体的圈定我们需要保证测试方案是针对同质人群、同一时间进行确保了除方案变量之外其他变量都是一致的这样才能将指标数据差异归到产品方案从而选出优胜版本在线上发布实现数据增长。
同质。指人群的各种特征分布的一致性。例如我们想验证产品方案对女性用户的购买意愿的影响那么A版本和B版本选择的用户必须都是女性。当然特征分布不光是性别还有国家、城市、使用频率、年龄、职业、新老用户等。
同时。在不同的时间点用户的行为可能不太一样。例如在一些重大的节日用户活跃度会升高那如果A方案的作用时间在节日B方案的作用时间在非节日很显然这种比较对于B方案是不公平的。
所以实现“同质同时”,并不是那么简单,首先需要丰富和精准的用户画像能力,例如版本、国家、城市、性别、年龄、喜好等用户属性。除此之外,还需要一整套强大的后台,完成测试控制、日志处理、指标计算、统计显著性指标等工作。
实现了“同质同时”之后,我们接着要找到产品方案的显著性指标,也就是方案想要证明的目标。例如我们优化了弹窗的提示语,目的是吸引更多的用户点击按钮,那按钮的点击率就是这个测试的显著性指标。
有了这些以后,那我们是不是就可以愉快地开始测试了?不对,你还需要先思考这两个问题:
流量选择。这个测试应该配置多少流量?配少了怕得不出准确的测试结论,配置多了可能要承担更大的风险。这个时候就需要用到最小样本量计算器。
A/B测试作为一种抽样统计它背后涉及大量的统计学原理而这个计算器需要我们提供以下数值
基准线指标值请输入测试要优化的指标的基准值比方“某按钮的点击率”如果该值每日在9%11%之间波动则可输入10%。-
指标最小相对变化请输入你认为有意义的最小相对变动值。假设你将要优化的指标为10%时你在最小相对变化中输入5% 就意味着你认为绝对值变化在9.5%, 10.5%)之间的变动没有意义,即便此时测试版本更优,你也不会采用。-
统计功效1β如果设置统计功效为90%可通俗理解为在A/B测试中当版本A和版本B的某项统计指标本质上存在显著差异时可以正确地识别出版本A和版本B是有显著差异的概率是90%。-
显著性水平α:如果统计指标的差异超过具体的差异,我们才说测试的结果是显著的。
天数选择。A/B测试需要持续几天我们才可以认为测试是可靠的。一般来说可以通过下面的方法计算。
测试所需持续的天数 >= 计算获得用户数 / (场景日均流量 * 测试版本设置流量百分比)
2. 如何进行A/B测试
虽然各个大厂都有自己完善的A/B测试平台但是A/B测试的科学设计并不容易需要不断地学习再加上大量的实践经验。
首先来说所有的A/B测试都应该是有“预谋”的也就是我们需要有对应的预期需要设计好测试的每一个环节。一般来说在测试开始之前需要问清楚自己下面这些问题
为了测试埋点、分流、统计的正确性以及增加A/B测试的结论可信度我们还会在A/B测试的同时增加A/A测试。A/A测试是A/B测试的“孪生兄弟”有的互联网公司也叫空转测试。它主要用于评估我们测试本身是否科学一般我推荐使用A/A/B的测试方式。
那在Android客户端有哪些实现A/B测试的方案呢
用一句话来描述A/B测试的话就是“拿到A/B测试的数据容易拿到可信的A/B测试的数据却很难”因此在指标设计、人群选择、时间设计、方案的设计都需要考虑清楚。同时也需要多思考、多实践推荐你拓展阅读《移动App A/B测试中的5种常见错误》。
统一发布平台
我在专栏里多次提到虽然我们做了大量的优化依然受限于原生开发模式的各种天花板的限制。这时可以考虑跳出这些限制例如Web、React Native/Weex、小程序和Flutter都可以是解决问题的思路。
但是即使我们转变为新的开发模式,灰度和发布的步骤依然是不可或缺的。此外,我们还要面对各种各样的运营活动、推送弹窗、配置下发。
可能很多大厂的同学都深受其苦,面对各式各样的发布平台,一不小心就可能造成运营事故。那应该如何规范发布行为,避免出现下发事故呢?
1. 发布平台架构
每个应用都或多或少涉及下面这些发布类型,但是往往这些发布类型都分别放在大大小小的各种平台中,而且没有统一的管理规范流程,非常容易因为操作错误造成事故。
统一发布平台需要集中管理应用所有的数据下发业务,并建立严格规范的灰度发布流程。
管理。所有的发布都必须通过权限校验,需要经过审批。“谁发布,谁负责”,需要建立严格的事故定级制度。对于因为疏忽导致事故的人员,需要定级处理。
灰度。所有的发布一定要经过灰度测试,慢慢扩大影响的用户范围。但是需要承认,某些下发并不容易测试,例如之前沸沸扬扬的“圣诞节改变展示样式”的事件。对于这种在特定时间生效的运营活动,很难在线上灰度验证。
监控。统一发布平台需要对接应用的“实时数据平台”,在出现问题的时候,需要及时采取补救措施。
业务已经那么艰难了,如果下发了一个导致所有用户启动崩溃的闪屏活动,对应用造成的损失就难以衡量。所以规范的流程和章程,可以一定程度上避免问题的发生。当然监控也同样重要,它可以帮助我们及时发现问题并立刻止损。
2. 运营事故的应对
每天都有大量各种类型的发布,感觉就像在刀尖上行走一样。“人在江湖飘,哪能不挨刀”,当我们发现线上运营问题的时候,还有哪些挽救的措施呢?
启动安全保护。最低限度要保证用户不会由于运营配置导致无法启动应用。
动态部署。如果应用可以正常启动那我们可以通过热修复的方式解决。但是热修复也存在一定的局限性例如有3%5%的用户会无法热修复成功,而且也无法做到立即生效。
远程控制。在应用中,我们还需要保留一条“救命”的指令通道。通过这条通道,可以执行删除某些文件、拉取补丁、修改某些配置等操作。但是为了避免自有通道失效,这个控制指令需要同时支持厂商通道,例如某个下发资源导致应用启动后界面白屏,这个时候我们可以通过下发指令去删除有问题的资源文件。
“万丈高楼平地起”,类似测试平台、发布平台或者数据平台这些内部系统很难短时间内做得非常完善,它们都是通过优化内部无数的小细节演进而来。所以我们需要始终保持耐心,朝着提升组织效能的方向前进。
总结
在过去那个做A/B测试非常艰难而且耗时耗力的年代测试的每个环节都会经过千锤百炼。而现在通过功能强大的A/B测试系统极大地降低测试的成本提高了测试的效率反而很多产品和开发人员变得有点滥用A/B测试或者说有了“不去思考”的借口。
无论是研发主导的性能相关的A/B测试还是产品主导的业务相关的A/B测试其实很多时候都没有经过严谨的推敲往往需要通过反反复复多次测试才能得到一个“结论”而且还无法保证是可信的。所以无论是A/B测试还是日常的灰度都需要有明确的预期真正去推敲里面的每一个环节。不要每次测试发布后才发现实验设置不合理或者发现这里那里漏了好几个数据打点再反反复复进行修改对参与测试的所有人来说都非常痛苦。
另外一方面我们对某个事情的看法并不会一成不变。即使我是Tinker的作者我也认为它只是某一阶段为了解决特定需求的产物。但是无论开发模式怎么改变我们对质量和效率的追求是不会改变的。
课后作业
你的应用是否是A/B测试的狂热分子对于A/B测试你有哪些好的或者坏的的经历欢迎留言跟我和其他同学一起讨论。
今天的课后作业是,思考自己的产品或者公司在灰度发布过程中存在哪些痛点?还有哪些优化的空间?请在留言中写下自己的心得体会。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,184 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
30 数据评估(上):如何实现高可用的上报组件?
无论是“高效测试”中的实时监控,还是“版本发布”中的数据校验平台,我都多次提到了数据的重要性。
对于数据评估我们的期望是“又快又准”。“快”表示数据的时效性。我们希望在1小时内甚至1分钟内就可以对数据进行评估而不需要等上1天或者几天。“准”表示数据的准确性保证数据可以反映业务的真实情况不会因为数据不准确导致做出错误的产品决策。
但是“巧妇难为无米之炊”,数据平台的准确性和时效性依赖客户端数据采集和上报的能力。那应该如何保证客户端上报组件的实时性和准确性?如何实现一个“高可用”的上报组件呢?
统一高可用的上报组件
可能有同学会疑惑,究竟什么是“高可用”的上报组件?我认为至少需要达到三个目标:
数据不会丢失。数据不会由于应用崩溃、被系统杀死这些异常情况而导致丢失。
实时性高。无论是前台进程还是后台进程,所有的数据都可以在短时间内及时上报。
高性能。这里主要有卡顿和流量两个维度应用不能因为上报组件的CPU和I/O过度占用导致卡顿也不能因为设计不合理导致用户的流量消耗过多。
但是数据的完整性、实时性和性能就像天平的两端,我们无法同时把这三者都做到最好。因此我们只能在兼顾性能的同时,尽可能地保证数据不会丢失,让上报延迟更小。
在“网络优化”中我不止一次的提到网络库的统一。网络库作为一个重要的基础组件无论是应用内不同的业务还是Android和iOS多端都应该用同一个网络库。
同理,上报组件也是应用重要的基础组件,我们希望打造的是统一并且高可用的上报组件。
一个数据埋点的过程,主要包括采样、存储、上报以及容灾这四个模块,下面我来依次拆解各个模块,一起看看其中的难点。
1. 采样模块
某些客户端数据量可能会非常大,我们并不需要将它们全部都上报到后台。比如说卡顿和内存这些性能数据,我们只需要抽取小部分用户统计就可以了。
采样模块是很多同学在设计时容易忽视的,但它却是所有模块中最为复杂的一项,需要考虑下面一些策略的选择。
大多数的组件采用的都是PV次数采样这样的确是最简单的。但是我们更多是在性能数据埋点上采样为了降低用户的影响面我更加倾向于使用UV采样的方式。而且为了可以让更多的用户上报我也希望每天都可以更换一批新的用户。
最终我选择的方案是“UV采样 + 用户标识随机 + 每日更换用户”的方式,但是采样还需要满足三个标准。
准确性。如果配置了1%的采样比例需要保证某一时刻只有1%的用户会上报这个数据。
均匀性。如果配置了1%的采样比例每天都会更换不同的1%用户来上报这个数据。
切换的平滑性。用户的切换需要平滑不能在用一个时间例如12点所有用户同时切换这样会导致后台数据不连贯。
实现上面这三个标准并不容易,在微信中我们采用了下面这个算法:
// id用户标识例如微信号、QQ号
id_index = Hash(id) % 采样率倒数
time_index = (unix_timestamp / (24*60*60)) % 采样率倒数
上报用户 =id_index == time_index
每个采样持续24小时使整个切换可以很平滑不会出现所有用户同时在0点更换采样策略。有些用户在早上10点切换有些用户在11点切换会分摊到24小时中。并且从一个小时或者一天的维度来看也都可以保证采样是准确的。
不同的埋点可以设置不同的采样率,它们之间是独立的、互不影响的。当然除了采样率,在采样策略里我们还可以增加其他的控制参数,例如:
上报间隔可以配置每个埋点的上报间隔例如1秒、1分钟、10分钟、60分钟等。
上报网络控制某些点只允许WiFi上传。
2. 存储模块
对于存储模块,我们的目标是在兼顾性能的同时,保证数据完全不会丢失。那应该如何实现呢?我们首先要思考进程和存储模式的选择。
业内最常见的上报组件是“单进程写 + 文件存储 + 内存缓存”虽然这种方式实现最为简单但是无论是跨进程的IPC调用堆积IPC调用总是很慢的还是内存缓存都可能会导致数据的丢失。
回顾一下在“I/O优化”中我列出的mmap、内存与写文件的性能对比数据。
你可以看到mmap的性能非常不错所以我们最终选择的是 “多进程写 + mmap”的方案并且完全抛弃了内存缓存。不过mmap的性能也并不完美它在某些时刻依然会出现异步落盘所以每个进程mmap操作需要放到单独的线程中处理。
“多进程写 + mmap”的方案可以实现完全无锁、无IPC并且数据基本不会丢失看上去简直完美但是真正实现时是不是像图中那么简单呢肯定不会那么简单因为我们需要考虑埋点数据的聚合以及上报数据优先级。
埋点数据的聚合。为了减少上报的数据量尤其是部分性能埋点我们需要支持聚合上报。大部分组件都是使用上报时聚合的方式但是这样无法解决存储时的数据量问题。由于我们使用的是mmap可以像操作内存一样操作文件中的数据可以实现性能更优的埋点数据的聚合功能。
上报数据优先级。很多上报组件埋点时都会使用一个是否重要的参数对于重要的数据采用直接落地的方式。对于我们的方案来说已经默认所有的数据都是重要的。关于上报数据的优先级我建议使用上报间隔来区分例如1分钟、10分钟或者1小时。
对于一些敏感数据可能还需要支持加密。对于加密的数据我建议使用单独的另一个mmap文件存储。
为什么上面我说的是数据基本不会丢失而不是完全不会丢失呢因为当数据还没有mmap落盘也就是处于采样、存储内部逻辑时这个时候如果应用崩溃依然会造成数据丢失。为了减少这种情况发生我们做了两个优化。
精简处理逻辑。尽量减少每个埋点的处理耗时每个埋点的数据处理时间需要压缩到0.1毫秒以内。
KillProcess等待。在应用主动KillProcess之前需要调用单独的函数先等待所有队列中的埋点处理完毕。
3. 上报模块
对于上报模块,我们不仅需要满足上报实时性,还需要合理地优化流量的使用,主要需要考虑的策略有:
为了解决后台进程的上报实时性问题,我们采用了单进程上报的策略,我推荐使用保活能力比较强的进程作为唯一的上报进程。为了更加精细地控制上报间隔,我们采用更加复杂的班车制度模式。
后来经过仔细思考,最终的上报模块采用“多进程写 + 单进程上报”。这里有一个难点那就是如何及时的收集所有已经停止的班车会不会出现多进程同步的问题呢我们是通过Linux的文件rename的原子性以及FileObserver机制实现了一套完全无锁、高性能的文件同步模型。
每个进程在对应优先级的文件“停车”的时候负责把文件rename到上报数据存放的目录中。因为rename是原子操作所以不用担心两个进程会同时操作到同一个文件。而对应的上报进程只需要监听上报数据目录的变化就可以实现文件状态的同步。这样就避免了多进程同步操作同一个文件的问题整个过程也无需使用到跨进程的锁。
当然上报模块里的坑还有很多很多例如合并上报文件时应该优先选择高优先级的文件对于上报的包大小在移动网络需要设置的比WiFi小一些而不同优先级的文件需要合并组包尽量吃满带宽而且在弱网络的时候我们需要把数据包设置得更小一些先上报最高优先级的数据。
4. 容灾模块
虽然我们设计得上报模块已经很强大但是如果使用者调用不合理也可能会导致严重的性能问题。我曾经遇到过某个同学在一个for循环连续埋了一百万个点还有一次是某个用户因为长期没有网络导致本地堆积了大量的数据。
一个强大的组件,它还需要具备容灾的能力,本地一般可以有下面这些策略。
容灾模块主要是保证即使出现开发者使用错误、组件内部异常等情况,也不会给用户的存储空间以及流量造成严重问题。
数据自监控
通过“多进程写 + mmap + 后台进程上报 + 班车模式”我们实现了一套完全无锁、数据基本不会丢失、无跨进程IPC调用的高性能上报组件并且通过容灾机制它还可以实现异常情况的自动恢复。
那线上效果是不是真的这么完美?我们怎样确保上报组件的数据可靠性和时效性呢?答案依然是监控,我们需要建立一套完善的自监控体系,为后续进一步优化提供可靠的数据支撑。
1. 质量监控
上报组件的核心数据指标主要包括以下几个:
当然,如果我们追求更高的实时性,可以选择计算小时到达率,甚至是分钟到达率。
2. 容灾监控
当客户端出现容灾处理时,我们也会将数据单独上报到后台监控起来。
除了异常情况的监控我们还希望将用户每日使用的移动流量和WiFi流量做更加细粒度的分区间监控例如01MB的占比、15MB的占比等。
总结
网络和数据都是非常重要的基础组件,今天我们一起打造了一款跨平台、高可用的上报组件。这也是目前比较先进的方案,在各方面的质量指标都比传统的方案有非常大的提升。
当然真正落实到编码这里面还有非常多的细节需要考虑也还有大大小小很多暗坑。而且虽然我们使用C++实现但是也还需要处理不同平台的些许差异比如iOS根本不需要考虑多进程问题等。
在实践中我的体会是,当我们亲自动手去实现一个网络库或者上报组件的时候,才会深深体会到把一个新东西做出来并不困难,但是如果想要做到极致,那必然需要经过精雕细琢,更需要经过长时间的迭代和优化。
课后作业
你所在的公司,目前正在使用哪个数据上报组件?它存在哪些问题呢?欢迎留言跟我和其他同学一起讨论。
今天的课后作业是,在实现方案中我故意隐去了两个细节点,这里把它们当作课后作业留给你,请你在留言中写下自己的答案。
1. 采样策略的更新。当我们服务器采样策略更新的时候,如果不使用推送,怎样保证新的采样策略可以以最快速度在客户端生效?-
2. 埋点进程突然崩溃。你有没有想到如果Process A突然崩溃那哪个进程、在什么时机、以哪种方式应该负责把Process A对应的埋点数据及时rename到上报数据目录
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,196 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
31 数据评估(下):什么是大数据平台?
数据是连接产品和用户的桥梁它反映了用户对产品的使用情况是我们作出业务决策的重要依据。虽然通过“高可用的上报组件”可以从源头上保障数据采集的准确性和实时性但是随着App业务迭代的复杂化经常会出现遗漏埋点、错误埋点、多端埋点不统一等情况影响了业务数据的稳定性。
我见过很多团队的埋点文档管理得非常不规范有的还在使用Excel来管理埋点文档经常找不到某些埋点的定义。而随着埋点技术和流程的成熟我们需要有一整套完整的方案来保证数据的稳定性。
那埋点应该遵循什么规范?如何实现对埋点整个流程的引导和监控?埋点管理、埋点开发、埋点测试验证、埋点数据监控…怎样打造一站式的埋点平台?在埋点平台之上,大数据平台又是什么样的呢?
埋点的基础知识
我们知道,一个业务埋点的上线需要经历需求、开发、测试等多个阶段,会涉及产品、开发和测试多方协作,而对于大型团队来说,可能还要加上专门的数据团队。
对于传统埋点来说错埋、漏埋这样的问题总会反反复复出现。为了排查和解决数据的准确性问题参与的各方团队都要耗费大量的精力。特别是如果埋点一旦出现问题我们还需要依赖App发布新版本可见埋点的修复周期长而且成本也非常巨大。
那应该如何解决这个问题呢?请先来思考一下,我们应该如何实现一个正确的埋点。
如果想实现一个正确的埋点,必须要满足上面的这四个条件,需要有非常严格的埋点流程管理。因此,你需要做到:
统一的埋点规范。应用甚至是整个公司内部,从日志的格式、参数的含义都要有统一的规则。
统一的埋点流程。在整个埋点过程,产品、开发、测试和数据团队都要肩负起各自的职责,一起通力协作,通过统一、规范的流程来实现正确的埋点。
通过统一的埋点规范和流程,希望可以减少埋点开发的成本,保障数据的准确性。下面我们一起来看看具体应该如何实践。
1. 统一埋点规范
在打开淘宝的主页时不知道你有没有注意到URL后面会带有一个SPM的参数。
https://www.taobao.com/?spm=a21bo.2017.201857.3.5af911d9ycCIDq
这个SPM代表什么含义呢SPM全称是Super Position Model也就是超级位置模型。简单来说它是阿里内部统一的埋点规范协议无论H5还是Native的Android和iOS都要遵循这套规范。
正如上面的链接一样SPM由A.B.C.D四段构成各分段分别代表的含义如下。
A:站点/业务, B:页面, C:页面区块, D:区块内点位
a21bo.2017.201857.3.5af911d9ycCIDq一共有5位这是网站特有的最后一位分配的是一个随机特征码只是用来保证每次点击SPM值的唯一性。
SPM主要有页面访问、控件点击以及曝光三种类型的事件它可以用来记录了用户点击或者查看当前页面的具体信息还可以推算出用户来自上一个页面的哪个位置。基于SPM规范淘宝可以得到用户每个页面PV、点击率、停留时长、转化率、用户路径等各种维度的指标。
对于埋点规范来说从公共参数到内部的各个业务参数我们都需要定义完整的日志格式。目前SPM这套规范已经推广到阿里整个集团以及外部的合作伙伴中这样通过各个部门、各个客户端的规范统一不仅降低了内部学习和沟通协作的成本而且对后续的数据存储、校验、分析都会带来极大的便利。
“一千个读者心中有一千个哈姆雷特”,每个公司的情况可能不一定相同,所以也不能保证阿里的埋点规范适合所有的企业。但是无论我们最终决定使用哪种规范,对于公司内部,至少是应用内部来说,埋点规范应该是统一的。
关于SPM规范如果你想了解更多可以参考《SPM参数有什么作用》和《阿里巴巴的日志采集分享》。
2. 统一埋点流程
埋点的整个过程涉及产品、开发、测试、数据团队等多个团队,如果内部没有完善的流程规范,非常容易出现“四国大混战”。在出现数据问题的时候,也常常会出现互相推卸责任的情况。
“无规矩不成方圆”,我们需要制定统一的埋点流程,严格规范整个埋点过程的各个步骤以及每个参与者在相应步骤的分工和责任。
需求阶段。在需求评审阶段产品需要列出具体的埋点需求。如果有数据团队的话产品的埋点需求需要数据团队review测试同时也需要根据产品的埋点需求制定出对应的埋点测试方案。这个阶段主要由产品负责需要保证需求方案和测试方案都是OK的。
开发阶段。在开发阶段,开发人员根据产品的埋点需求文档,根据具体的埋点规则在客户端中埋点。开发完成后,需要在本地自测通过。这个阶段由开发负责。
测试阶段。在测试阶段,测试人员根据产品的埋点需求和规则,通过之前指定的测试方案进行埋点的本地验收。这个阶段由测试负责。
灰度发布阶段。在灰度发布阶段,测试人员负责对埋点建立线上的监控,查看线上的数据是否符合埋点需求和规则,产品人员需要关心埋点数据是否是符合预期。这个阶段主要是测试负责,但是产品也同样需要参与。
通过统一埋点流程,我们明确规定了埋点各个阶段的任务与职责,这样可以减少埋点的成本、降低出错的概率。
3. 埋点方式
代码埋点、可视化埋点、声明式埋点、无痕埋点对于埋点的方式业界似乎有非常多的流派。在《美团点评前端无痕埋点实践》和《网易HubbleData之Android无埋点实践》都将埋点方式归为以下三类
代码埋点。在需要埋点的节点调用接口直接上传埋点数据,友盟、百度统计等第三方数据统计服务商大都采用这种方案。
可视化埋点。通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据,从而实现所谓的“无痕埋点”, 代表方案有已经开源的Mixpanel。
无痕埋点。它并不是真正不需要埋点而是自动采集全部事件并上报埋点数据在后端数据计算时过滤出有用的数据代表方案有国内的GrowingIO。
我们平常使用最多的就是“代码埋点”方式,而对于“可视化埋点”和“无痕埋点”,它们都需要实现埋点的自动上报,需要实现事件的自动拦截。
怎么理解呢你来回想一下对于SPM方案中的页面切换事件我们可以通过监听Activity或者Fragment的切换实现。那怎样自动监听控件的点击和曝光事件呢
以监听点击事件为例,一般有下面几种方法:
插桩替换。对于控件的点击我们可以通过ASM全局将View.onClickListener中的onClick方法覆写成我们自己的Proxy实现在内部添加埋点代码。
Hook替换。通过Java反射从RootView开始递归遍历所有的控件View对象并Hook它对应的OnClickListener对象同样将它替换成我们的Proxy实现。
AccessibilityDelegate机制。通过AccessibilityDelegate我们可以检测到控件点击、选中、滑动、文本变化等状态。借助AccessibilityDelegate当控件触发点击行为时通过具体的AccessibilityEvent回调添加埋点代码。
dispatchTouchEvent机制。dispatchTouchEvent方法是系统点击事件的分发函数通过重写这些函数就可以实现对所有点击事件的监听。
大数据平台
虽然我们有了统一的埋点规范和流程但是整个流程依然是依赖人工手动的。就以埋点需求管理为例很多团队还在使用Excel来管理随着不断的修改文档会越来越复杂这样也不利于对历史进行跟踪。
那怎样将埋点管理、埋点开发、埋点测试验证、埋点数据监控打造成一站式的埋点平台呢?
1. 埋点一站式平台
埋点一站式平台可以实现管理埋点定义的可视化,辅助开发和测试定位埋点相关的问题。并且自动化验证本地和线上的埋点数据,以及自动分析和告警,也可以减少埋点开发和验证的成本,提升数据质量。
如上图所示,它主要由四个子平台组成。
埋点管理平台。对应用的整个埋点方案进行统一管理包括埋点的各个字段的定义和规则例如对于QQ号这个字段来说要求是纯数字而且非空的。对于SPM规范埋点管理平台也会记录每个页面对应的名称例如淘宝首页会用a123来表示。
埋点开发辅助平台。开发辅助平台是为了提升开发埋点的效率,例如我前面说到的可视化埋点。或者通过埋点管理平台的字段和规则,自动生成代码,开发者可以一键导入埋点定义的类,只需要在代码中添加调用即可。
埋点验证平台。验证平台非常非常重要对于开发人员的埋点测试和测试人员的本地验收我们可以通过扫码或者其他方式切换成数据的实时上传模式。埋点验证平台会拉取配置平台的埋点定义和规则将客户端上报的数据进行实时展示和规则校验。比如说某个埋点漏了一个字段、多了一个字段又或者是违反了预设的规则例如QQ号有字母、数值为空等。
因为手工测试不一定可以覆盖所有的场景,所以我们还需要依赖自动化和灰度验证。它们整体思路还是一致的,只是借助的是线上的非实时通道,每小时或者每日定期输出数据验证的报告。
埋点监控平台。监控的目标是保证整个数据链路的健壮性,这里包括对客户端“高可用上报组件”的监控,例如上一期讲到的质量监控和容灾监控。还有对后端数据解析、存储、分析的监控,例如总日志量、日志异常量、日志的丢失量等。
不知道你是否注意到了,埋点管理平台还会对采样策略进行管理。回到专栏上一期我留给你课后作业的问题,当我们服务器采样策略更新的时候,如果不使用推送,怎样保证新的采样策略可以以最快速度在客户端生效呢?
其实非常简单当用户更改了某个埋点的采样配置时埋点配置平台会将采样策略版本号自增然后将最新的策略以及版本号推送到数据采集模块。埋点SDK每次上报都会带上自己本地的策略版本号如果本地的策略版本号小于服务器的版本号那么数据采集模块会直接把最新的策略在回包中返回。
这种方式保证只要客户端有任意一个埋点上报成功,都可以拿到最新的采样策略。事实上,很多其他的配置都是采用类似的方式更新。
2. 数据产品
埋点的一站式平台,也只是数据平台的一小部分,它负责保证上报数据的准确性。按照我的理解,整个大数据平台的简化架构是下面这个样子的。
采集工具层。应用的上报组件负责数据埋点、日志的组装上报,它需要保证数据的准确性和实时性。
数据采集层。数据采集层对日志进行清洗、处理,可能还需要和我们的埋点一站式平台进行交互。然后需要根据数据的订阅情况将数据分发到不同的计算模块。
数据计算层。计算层主要分为离线计算和实时计算两部分离线计算从数据接收到结果的产出一般至少需要一个小时以上。而实时计算可以实现秒级、分钟级的计算一般只会用于核心业务的监控。而且因为计算量的问题实时计算一般只会计算PV不会计算UV结果。
数据服务层。无论是离线计算还是实时计算我们都会把结果存放到数据的服务层一般都会使用DB。数据服务层主要是为了屏蔽底层的复杂实现我们只需要从这里查询最终的计算结果就可以了。
数据产品层。数据产品一般分为两类,一类是业务型,一类是监控型。业务型一般用来查看和分析业务数据,比如页面的访问、页面的漏斗模型、页面的流向、用户行为路径分析等。监控型主要用来监控业务数据,例如实时的流量监控、或者是非实时的业务数据监控等。
数据服务层是一个非常好的设计,它让整个公司的人都可以非常简便地实现不同类型的数据产品。我们不需要关心下层复杂的数据采集和计算的实现,只要把数据拿出来,做一个满足自己的报表展示系统就可以了。
对于实时监控微信的IDKey、阿里的Sunfire都是非常强大的系统它们可以实现客户端数据分钟级甚至秒级的实时PV监控。
对于中小型公司可能没有能力搭建自己一整套的大数据平台这个时候可能需要使用第三方的服务例如阿里云提供了一套OneData服务。
当然我们也可以搭建一套自己的数据平台但是对于海量大数据来说一个稳定、高性能的数据计算层是非常复杂的我们可以使用外部打包好的数据计算和服务层例如阿里云的MaxCompute大数据计算服务。接着在数据计算层之上再来实现符合我们自己需求的数据产品。
下面是一个简单的数据平台整体架构图你也可以参考大众点评的实现《UAS大众点评用户行为系统》。
总结
在过去的几年里,大数据也是一个经常被提起的概念。对于大数据,或者说与之配套的大数据平台,我自己的体会主要有两点。
1. 技术变革是为了解决需求。如果淘宝没有面对每天亿级的用户访问数量,没有一次又一次的被卷入数据的黑洞中,也不会有他们对大数据方面所做的各种艰苦努力,技术是为了解决业务场景中的痛点。另一方面看,大数据的确存在门槛,如果在中小型企业可能不一定有这样锻炼的机会。
2. 基础设施建设没有捷径可走。高可用的上报组件、埋点一站式平台以及各种各样的数据产品,这些基础设施的建设需要有足够的耐心,投入足够的人力、物力。为什么要采用这样的规范和流程?为什么架构会这样设计?虽然这些方案可能不是最优的,但也是通过血与泪、通过大量的实践,慢慢演进得来的。
课后作业
你所在的公司,有没有统一的埋点规范和埋点流程?对于数据相关的配套设施建设得怎么样?在数据保障方面遇到了哪些问题?欢迎留言跟我和其他同学一起讨论。
在数据平台的建设上面国际的Facebook、国内的阿里都是做得非常不错的公司我推荐你看看阿里数据专家们写的一本书《大数据之路 阿里巴巴大数据实践》。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,193 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
32 线上疑难问题该如何排查和跟踪?
“95%以上的崩溃都能解决或者规避,大部分的系统崩溃也是如此”,这是我在专栏“崩溃优化”中曾经夸下的海口。
虽然收集了尽可能丰富的崩溃现场但总会有一些情况是事先没有预料到的我们无法直接从崩溃日志里找到原因。事实上我们面临的难题远远不止崩溃比如说用户投诉文件下载到99%之后无法继续那如何确定是用户手机网络不好是后台服务器出错还是客户端代码的Bug
我们的业务逻辑越来越复杂,应用运行的环境也变得越来越复杂,因此在实际工作中总会遇到大大小小的线上疑难问题。对于这些问题,如何将它们“抽丝剥茧”,有哪些武器可以帮助我们更好地排查和跟踪呢?
用户日志
对于疑难问题,我们可以把它们分为崩溃和非崩溃两类。一般有哪些传统的排查手段呢?
本地尝试复现。无论是崩溃还是功能性的问题,只要有稳定的复现路径,我们都可以在本地采用各种各样的手段或工具进行反复分析。但是真正的疑难问题往往都很难复现,它们可能跟用户机型、本地存储数据等环境有关。
发临时包或者灰度包。如果发临时包给用户,整个过程用户配合繁琐,而且解决问题的时间也会很长。很多时候我们根本无法联系到用户,这个时候只能通过发线上灰度包的方式。但是为了一步步缩小问题的范围,我们可能又需要一次次地灰度。
我们多么希望能有一些“武器”,帮助工程师用非常低的成本,在非常短的时间内,尽可能地收集足够丰富的信息,更快速地排查和解决问题。
1. Xlog
在日常开发过程中我们经常会使用Logcat日志来排查定位代码中的问题。
对于线上问题,我们也希望可以有用户的完整日志,这样即使问题不能复现,通过日志也可能定位到具体的原因。所谓“养兵千日,用兵一时”,客户端日志只有当出现问题且不容易复现时才会体现出它的重要作用。但是为了保证关键时刻有日志可用,就需要保证程序整个生命周期内都要打日志,所以日志方案的选择至关重要。
在过去因为性能和可靠性问题通常我们只针对某少部分人动态打开日志开关。那如何实现一套高性能、日志不会丢失并且安全的日志方案呢微信在2014年就实现了自己的高性能日志模块Xlog并且在2016年作为Mars的一部分开源到GitHub。关于Xlog的更多实现细节你可以参考源码或者会议分享。
Xlog方案的出现可以让全量用户全天候打开日志也不需要担心对应用性能造成太大的影响。但是Xlog只是一个高性能的日志工具最终是否能解决我们的线上问题还需要看我们如何去使用它。
所以微信制定了严格的日志规范,定期对拉取的日志作规则检查,一旦发现有违反规则的情况,会作出一定的处罚。下面是其中的一些日志规范,我选取一些分享给你。
日志打点怕打太多也怕太少,担心出现问题没有足够丰富的信息去定位分析问题。应该打多少日志,如何去打日志并没有一个非常严格的准则,这需要整个团队在长期实践中慢慢去摸索。在最开始的时候,可能大家都不重视也不愿意去增加关键代码的日志,但是当我们通过日志平台解决了一些疑难问题以后,团队内部的成功案例越来越多的时候,这种习惯也就慢慢建立起来了。
2. Logan
对于移动应用来说我们可能会有各种各样的日志类型例如代码日志、崩溃日志、埋点日志、用户行为日志等。由于不同类型的日志都有自己的特点这样会导致日志比较分散比如我们要查一个问题需要在各个不同的日志平台查不同的日志。美团为了解决这个问题提出了统一日志平台的思路也在Github开源了自己的移动端基础日志库Logan。
Logan整合了各式各样的日志平台打造成一个统一的日志平台进一步提升了开发人员查找问题的效率。不过无论是Logan还是Xlog日志一般会通过下面两种方式上报。
Push拉取。通过推送命令只拉取特定用户的日志。
主动上报。在用户反馈问题、出现崩溃等一些预设场景,主动上报日志。
对于用户日志我们是否已经做到尽善尽美了手动埋点的覆盖范围有限如果关键位置没有预先埋点那可能就需要重新发包。所以美团在Logan基础上还推出了Android动态日志系统Holmes。
Holmes的实现跟美团的Robust热修复思路差不多需要对每个方法进行了插桩来记录方法执行路径也就是在方法的开头插入一段桩代码当方法运行的时候就会记录方法签名、进程、线程、时间等形成一条完整的执行信息。但是这套方案性能的技术难点比较多一般只会动态针对出现问题的用户开启。
虽然这个思路有一定的启发性,但我认为其实并不太实用。一来,对每个方法插桩,会对安装包体积和性能造成很大的影响,导致这套方案过于笨重;二来,很多疑难问题都具有偶发性,当用户出现问题后,再去打开用户日志,可能也不能保证用户的问题可以复现。
动态调试
“只要你能在本地复现我就能解决”这可能是开发对测试说过最多的话了。在本地我们可以通过增加日志或者使用Debugger、GDB等这样的调试工具反复进行验证。
针对远程用户,如果我们可以做到具备跟本地一样的动态调试能力,想想都觉得激动人心。那有没有方案能实现远程动态调试呢?
1. 远程调试
动态调试又或者是动态跟踪它们属于高级的调试技术。事实上它并不是什么新鲜的话题例如Linux中大名鼎鼎的DTrace和SystemTap、Java的BTrace都是非常成熟的方案。我推荐你仔细阅读《动态调试漫谈》和《Java动态追踪技术探究》特别是前者让我有非常大的收获。
在Android端我们能不能实现对用户做动态调试呢在回答这个问题之前请先来思考一下平时我们通过Android Studio进行调试的底层原理是什么。
其实我们的学习委员鹏飞之前已经讲过这块内容了回到“Android JVM TI机制详解”中说到的Debugger Architecture。Java的调试框架是通过JPDAJava Platform Debugger ArchitectureJava平台调试体系结构它定义了一套独立且完整的调试体系主要由以下三部分组成
JVM TIJava虚拟机工具接口被调试者
JDWPJava调试协议通道
JDIJava调试接口调试者
如果你想了解更多关于Java调试框架的信息可以重新回顾一下“Android JVM TI机制详解”里给出的参考链接。
对于Android来说它的调试框架也是在Java调试框架基础上进行的扩展主要包括Android StudioJDI、ddmlib、adb server、adb daemon、Android应用这五个组成部分。
如果想要实现对用户的远程调试,我们需要修改其中的两个部分。
JDWP传输通道。我们不能使用系统adb的方式而是希望将用户的调试信息经过网络通道发送给我们。
JDI前端展示。对于客户端调试数据的展示我们不太容易直接复用Android Studio需要重新实现自己的数据展示页面。
关于具体实现的细节你可以参考美团的文章《Android远程调试的探索与实现》最终整体的流程如下。
当然不同于本地Debug包的调试对于用户调试我们还需要考虑如何突破ProGuard和Debugable的影响。总的来说这套方案有非常大的技术价值可以加深我们对Java调试框架的理解。但是它并不实用因为大部分的场景我们很难在用户不配合的前提下做好调试。而且调试过程也可能会出现各种各样的情况并不容易控制。
不过退而求其次通过这个思路我们可以在本地实现“无线调试”无需adb又或者是实现对混淆包的调试。
2. 动态部署
如果说远程调试并不很实用,那有没有其他方法让用户感知不到我们在进行调试呢?
用户无感知、代码更新,这不正是动态部署所具备的能力,而且动态部署天生就非常适合使用在疑难问题的排查上。
精细化。通过发布平台,我们可以只选择某些问题用户做动态更新。也可以圈定某一批用户,例如某个问题只在华为的某款机型出现,那我可以只针对华为的这款机型下发。
场景。对于疑难问题的排查,我们一般只会增加日志或者简单修改逻辑,动态部署在这个场景是完全可以满足的。
可重复、可回退。对于疑难问题我们可能需要反复尝试不同的解决思路动态部署完全可以解决这个需求。而且在问题解决后我们可以及时将无用的Patch回退。
我还记得曾经为了解决libhw.so的崩溃问题我们历经一个月一共发布了30多个动态部署反复地增加日志、增加Hook点最终才得以解决。
3. 远程控制
动态部署存在生效时间比较慢几分钟到十几分钟、无法覆盖100%用户修改AndroidManifest或者用户手机没有剩余空间等问题。对于一些特定问题我们可以通过下发预设规则的方式来处理。
网络远程诊断是一个非常经典的例子假如有个用户反馈某个网页无法打开我们可以通过本地或者远程下发指令方式对用户的整个网络请求过程做完整的检测看看是不是用户的网络问题是不是DNS的问题在请求的哪个阶段出错了错误码是什么等。
Mars里面也有一个专门的SDT网络诊断模块我们可以顺便回顾一下Mars整个知识结构图。
除了网络的远程诊断之外,网络疑难问题的排查和跟踪本身就是一个非常大的话题。它涉及业务请求从域名解析/流量调度到业务统一接入,再到业务调用的整个访问链路,属于大网络平台的一环。
我们可以通过客户端生成的traceId将统一收集和整合客户端日志、服务端调用日志、自建CDN等日志建立以用户为维度的监控平台提供问题定位功能 。例如Google的Dapper、阿里的EagleEye、微信的点击流平台、QQ的全链路监控平台等都是通过这个思路实现的。
类似网络远程诊断又或者是删除某些文件、上报某些信息这些预设规则是建立在我们已经踩过某个坑或者更多情况是已经无数次踩到同一个坑并且忍无可忍才会搭建一套相应的诊断规则。那我们能不能不通过动态部署也可以简单的调用某些Java代码呢
这个时候就不得不提到非常强大的Lua脚本语言iOS之前大名鼎鼎的Wax热修复、腾讯Unity3D的热更新方案都是使用Lua来实现。Lua的VM非常小只有200KB不到充分保证了时间和内存开销的可控。我们可以对目标用户下发指令动态地执行一段代码并将结果上报或者在方法运行的时候去获取某些对象、参数的快照信息。
下面是Lua和Android的调用事例。
// lua脚本函数
function setText(textView)
tv:setText("set by Lua."..s); //这里的s变量由java注入
tv:setTextSize(30);
end
// android调用
lua.pushString( "from java" ); //压入欲注入变量的值
lua.setGlobal( "s" ); //压入变量名
lua.getGlobal( "setText" ); //获取Lua函数
lua.pushJavaObject( textView ); //压入参数
lua.pcall( 1, 0, 0 ); //执行函数
对于Lua的使用你可以参考官方文档。为了方便我们在Android更加容易的使用Lua也有不少开源库对Lua做了更好的封装例如AndroLua阿里也有一套基于Lua实现的动态化界面框架LuaViewSDK。
美团的Holmes也利用Lua增加了DB查询、上报普通文本、ShardPreferences查询、获取Context对象、查询权限、追加埋点到本地、上传文件等综合能力。正因为Lua脚本如此强大很多大厂App也都在Android中集成Lua。
总结
对于美团、支付宝、淘宝这些超级应用来说,不同的平台、不同的业务可能有上千人同时在一个应用上面开发协作。业务量大、多地区协作开发、业务类型多,每当出现问题都会感到耗时耗力,心力交瘁。
正因为反复“痛过”才会有了微信的用户日志和点击流平台才会有美团的Logan和Homles统一日志系统。所谓团队的“提质增效”就是寻找团队中这些痛点思考如何去改进。无论是流程的自动化还是开发新的工具、新的平台都是朝着这个目标前进。
课后作业
在你的工作中,遇到或者解决过哪些经典的疑难问题?还有哪些强大的疑难问题的排查武器?欢迎留言分享给我和其他同学。
无论是推送拉取用户日志,还是远程调试命令的下发,我们都需要具备区分用户的能力。对于微信这样强登录的应用,我们可以使用微信号作为用户标识。但是用户登陆之前的日志该如何收集呢?
对于用户唯一标识Google有自己的最佳实践方案。对于大部分非强登录的应用搭建自己的用户标识体系非常重要。用户唯一标识需要考虑漂移率、碰撞率以及是否跨应用等因素业界常用的方案有阿里的UTDID、腾讯MTA ID。
今天的课后作业是,应用该如何实现自己的用户标识体系,请你在留言中写下自己的答案。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,99 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
33 做一名有高度的移动开发工程师
专栏更新至今不知不觉第二模块“高效开发”也已经更新完了。稳定性、内存、卡顿、I/O、网络“高质量开发”模块打通了从应用层、Android系统层、Linux内核层再到硬件层的优化路径帮助我们打通“任督二脉”成为一名Android开发高手。
所谓“高效开发”,可以给我们带来了什么呢?移动互联网发展到今天,所有人都说“提质增效”,但是团队效能不是靠我们封装一个工具类或者组件,给其他人低成本复用就够了。持续交付平台、测试平台、发布平台、数据平台、网络平台…我希望你可以跳出客户端的限制,去思考整个产品的研发流程有哪些痛点,不同团队的协作有哪些优化空间,尝试去提升产品的质量和团队的效率。
我们需要的是多想一步,哪怕只是多思考一小步,对自身的成长可能就价值巨大。想要成为一名全面的“开发高手”,不仅要具备系统性解决应用性能和架构问题的攻坚能力,也要有从全局俯视体系和流程的思维能力。这就是我在“高效开发”里希望带给你的思考,希望你可以成为一名“站在高处”的移动开发工程师。
成为有高度的移动开发工程师
在微信的时候我非常推崇T型技术人才理论所谓的“T”无非就是横向和纵向两个维度。纵向解决的是深度问题横向解决的是广度问题。
一个有高度的移动开发工程师,需要能纵向深入,也要能横向全面地思考每一个问题。比如说团队希望治理数据的准确性和实时性问题,如果站在客户端的角度上看,就是思考如何去实现一套数据不会丢失、实时性高以及高性能的埋点上报组件。我们知道,这里面的进程模式、存储模型、同步机制等都很复杂,要做一个高可用的上报组件确实需要具备一定的技术深度。
但是如果站在更高的角度上看,你会发现上报组件的优化并不能从根本上解决团队的数据问题。埋点的规范是什么?埋点的流程是什么?产品、研发、数据、测试几个团队对于数据有哪些痛点?我们需要梳理一个埋点从产品定义、客户端埋点开发、测试验证、后端数据处理、数据展示和监控的整个过程。针对团队的数据治理,我们需要体系化的思考每一个点的问题,从更高的角度去全局思考。
那应该如何来提升自己的高度,站在高处去思考问题呢?下面是我的一些思考。
1. 从终端到跨端
在App开发最原始的时代为了实现代码的内部复用我们封装了各种各样的Util工具类。随着移动互联网的发展应用越来越多也越来越复杂代码还需要在不同应用中复用。这个时候客户端组件呈现出爆发式增长例如图片库中的Glide、Fresco、Picasso组件化中Atlas、DroidPlugin、RePlugin等。
回想一下因为当时Android系统的不成熟和不完善反而造就了一个百花齐放的移动开发时代。在这个时代里我们总可以找到很多优化的点并且持续打磨。随着应用业务复杂性和要求的提升单纯在客户端的单点优化已经满足不了业务的诉求了比如在直播、小程序这样的复杂场景。
这个时候我们第一步就要跳出自身客户端的角色限制,从更为全局的角度看问题、思考问题。你需要明白,客户端的实现只是其中一小块内容而已。假如你接到一个提升页面打开速度的任务,极致优化的基础是我们能深入研究浏览器的渲染原理和缓存机制,但是前端和后端能够做些什么,又应该做些什么呢?除此之外,页面哪里产生、如何发布、发布到哪里、如何下载、如何解析、如何渲染、如何衡量和监控页面的性能,这些全部都是我们需要思考的问题。
一方面在你的项目还没有证明它的价值之前可能很多公司并不愿意投入很多的人力。这个时候我们只能去包办前后端就像当年微信的日志平台、APM平台记得还是我们用Tornado先简单搭建起来。等到这个项目证明了它的价值才会拉更多的人参与进来。所以这就需要我们具备跨端的能力目标是解决产品的问题要知道客户端技术并不是唯一的选择。
另外一方面,针对持续交付平台、测试平台、网络平台、数据平台,很多平台客户端开发者本来就是使用方,我们应该更清楚里面的痛点是什么,有哪些可以改进的地方,所以客户端开发者应该更能主导这些平台的演进。
2. 从平台到中台
正如我上面说到的,组件化只是客户端技术最基本的抽象的体现。怎么理解呢?以性能组件为例,虽然我们收集了应用各个维度的性能数据,但是这些数据在后台如何聚合、如何存储、如何分析、如何报警,我们并没提供解决方案。
每个接入的应用还是要花很大的力气去搭建一整套系统为了解决这个问题集成式服务化的建设开始出现比如以Google的Firebase为代表的各个开发者平台。为了解决应用不同的场景我们不断地孵化出不同的服务平台。
移动开发早就已经过了单兵作战的年代了客户端单点的深耕细作已经不是唯一考量的因素。有没有配套的服务、服务是不是简单易用这些因素对于开发者来说越来越重要。特别是对于大厂来说一个公司有几十上百个应用对于公共业务需要避免重复劳动。国内蚂蚁的mPaaS、阿里云的EMAS移动开发者平台都是遵循这样的服务化思路。
但是平台化是不是就是服务的最终形态呢?你有没有体验过这种的痛苦:一个新应用需要接入公司内部十几个不同的平台,它们的账号信息、注册信息都相互独立,很多功能我们还需要单独去跟每个平台联调测试。为了解决各个平台的割裂,在平台化的基础上,又提出了中台化的思路。
什么是中台呢?简单的理解就是把这些分散的平台又统一为一个超大的平台。有人会想我们是不是在开历史的倒车?还记得当年我们将一个庞大的系统分拆成各个子平台是多么的艰难。事实上,这里中台的“统一”,更多是面向开发者层面的,例如都使用同一个账号、不需要重复注册、平台之间互相闭环等。
关于中台的更多资料,你可以参考《从平台到中台【上】》和《从平台到中台【下】》。在国内,阿里的中台是做得最好的。当然腾讯、头条这些公司也都意识到了它的重要性,最近都在积极调整组织架构,成立了专门的中台部门。但是无论是中台还是平台,都是靠无数大大小小的优化点堆积起来得,它们都需要慢慢地积累,很难在非常短的时间内建设得非常完善。
热点问题答疑
针对高效开发,我全面介绍了目前各大公司的做法和思路。对于持续交付、测试、发布、数据、网络、日志,它们本身涉及的知识是十分庞大的,所以就正如高质量开发一样,即使专栏的内容不能完全理解,也大可不必焦虑,可以反复多读几次专栏的文章和扩展的参考资料,相信你每次学习都会有不同的收获。
你可以按照自己的节奏持续地学习下去,这样我相信无论是对你的视野还是对移动开发的理解,都会有很大的收获。但是在向提升高度的方向迈进时,我们或多或少都会有一些疑问,下面我就挑选几个比较重要的问题,和你进一步聊聊我的看法。
1. 如何提升个人的专注力和效率?
人的大脑就像CPU一样如果频繁切换进程和线程这个代价是非常大的。一会看一下微信一会刷一下抖音一会看一下头条当我们重新切换回工作线程的时候起码需要几分钟才能重新进入状态。
那靠个人的自制力能不能解决这个问题?能,但是非常遗憾的是大部分人都没有这个能力,或者说自制力不够强大。不要给自己被诱惑的机会,因为大部分人都无法承受诱惑。我的建议是,直接卸载掉这些可能影响我们工作的软件(微信可能卸载不了,这也是它强大的地方,笑)。
2.个人发展与公司平台的关系
可能DebugCat同学的疑问你也会有共鸣我每天的工作就是写写界面、调调动画面对的都是无休无止的业务你讲的这些东西虽然高大上但是并没有机会接触到。
对于小型团队来说,更多的是拿来主义,去使用一些第三方的平台。对于大型团队来说,可能你才有机会真正参与到这些平台的开发。但是也有人说在小型团队可以独当一面,但在大厂只能做一颗小小的螺丝钉。
我相信业务和团队这些限制因素的确客观存在,而且对我们的影响的确巨大。但是这并不是决定性的因素,你要问问自己有没有真的去努力。
如果你在大厂,就应该从客户端到后端,尽可能全面深入研究你参与的模块,多想想如何把你所做的模块优化到极致,并且在巨大的用户量面前依然能够稳定运行。如果你在初创团队,在业余时间也要坚持学习,持续探索自己的技术深度。这样在将来,无论是初创团队内部的晋升,还是跳到大厂,这样努力的经验都可以成为未来无数次面试、加薪的一大亮点。
总结
移动开发工程师想要真正站在高处,既需要有技术深度,又要有广度。很明显你下一个问题是:应该先钻研深度,还是扩展广度呢?
我建议你应该至少先在一个技术领域付出大量的精力,深入钻研透彻,然后再去思考广度的问题。这是因为经验丰富的程序员学新的东西都非常快,因为现在已经不那么容易出现太多全新的技术,所谓的新技术其实都是旧技术的重新组合和微创新。
成长是没有捷径的,我发现技术圈也有部分人喜欢在论坛写写文章或者出去授课,在业界可能还小有名气,受到不少人追捧。但是只要真正去大厂面试,可能就会被打回原形。我推荐你看看这篇文章,这里把里面的一句话推荐给你:
老老实实看书,踏踏实实做事儿,早日兑现自己曾经吹过的牛逼。
“金三银四”,最近也是找工作的高峰期。从很多同学的面试经历来看,现在只会单纯写业务代码的人找工作特别难,比如很多大厂的面试官都会针对性能优化的细节,考察你是否真正搞懂底层的机制和原理。环境的要求越来越高,所以我们也要积极转变,踏踏实实的学习。最后我也推荐你看看《程序员成长路线》,希望今天讲的这些“大道理”对你有所启发。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@ -0,0 +1,177 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 聊聊重构:优秀的架构都是演进而来的
每个程序员心中都有一个成为架构师的梦想那成为架构师这个目标是否“遥不可及”呢从我的工作经历来看我一共负责过搜狗输入法、微信等4款亿级产品的架构工作可能有同学会好奇这些大型的App是如何做架构设计的。从我接手的这些应用的现实情况来看看似光鲜的外表下都有一颗千疮百孔的心各种日志随便输出、单例满天飞、生命周期混乱、线程乱创建、线程不安全这些问题随处可见。
所以你可以看到每个大型应用都背负着沉重的历史技术债务,架构师很重要的一项工作就是重构“老态龙钟”的陈旧架构。在接下来的“架构演进”模块中,我们一起学习架构该如何的重构和演进,帮助我们及时偿还这些“历史债务”。
虽然我们天天都在谈“架构”,那你有没有想过究竟什么是架构呢?
什么是架构
什么是架构,每个人都有自己的看法。在我看来,所谓的架构就是面对业务需求场景给出合适的解决方案,使业务能够快速迭代,从而达到“提质增效”的目标。
我先举个例子告诉你什么是架构以及架构的作用。我曾经为了解决UI渲染卡顿这个需求场景我们设计了异步创建View、异步布局与主线程渲染这个架构。不过架构只是设计的抽象对于具体的实现我们可以称之为框架。好的框架可以隐藏大家不需要关心的部分提升我们的效率。例如Facebook的Litho、微信的Vending它们通过框架约束和异步来解决Android应用UI线程卡顿问题。
如果说监控是为了发现问题核心在于“防”那好的架构可以直接避免出现问题所以架构设计的目标在于“治”。为了帮助你更好地理解架构我们先从Android的架构设计说起。
1. Android的架构
在官方文档《平台架构》中对Android的描述如下
Android是一种基于Linux的开放源代码软件栈为广泛的设备和机型而创建。
在深入Android架构之前我们先来思考一下Android需要满足的需求场景也就是各方对它的诉求。
用户。在低内存的设备上也可以流畅地运行各种应用,并且有持久的续航能力。
开发者。应用开发简便,并且可以在平台上获得收益。
硬件厂商。无论是芯片厂商还是手机制造商,都可以低成本地适配和升级自己的系统。
回想Android诞生之初它为了团结一切可以团结的力量广泛取得硬件厂商、开发者以及用户的支持。所以在做架构设计的时候就充分考虑到了这些因素。
Java API接口层。长久以来Java一直霸占第一大编程语言宝座具有广泛的开发者基础。从当时来看Android选择Java作为接口语言是十分明智的。从现在看只是没有在Oracle之前抢先收购Sun是比较失策的导致出现当前的专利诉讼困局。总的来说开发者通过自身熟悉的Java语言友好的接口层就可以使用系统和硬件的各种能力快速搭建出自己的应用。
硬件抽象层。设备制造商需要为内核开发硬件驱动程序为了降低设备制造商的开发成本Android选择成熟、开源的Linux内核并且将Android打造成一个免费、开源的操作系统吸引了更多的手机厂商入局。但是芯片厂商和手机厂商还是需要一起做大量的工作才可以更新到最新的系统在Android 8.0之后Android新增了HAL硬件抽象层。它向上屏蔽了硬件的具体实现使小米这些手机厂商可以跳过芯片厂商单独更新Android的Framework框架。
应用层。在Android设计之初它就考虑到移动设备的各种限制。为了用户在低内存设备有更好的体验同样做了大量的工作。例如设计了基于寄存器架构、可执行文件更小的Dalvik虚拟机以及内核的Low Memory Killer等。为了满足用户的基本需求Android系统在推出之初就内置许多的基础应用。并且通过吸引更多开发者进入也在不断地满足用户的各种需求场景。当然Android也在持续优化Framework和Runtime的性能例如我前面提到过的黄油计划、耗电优化等。
架构是为了需求场景服务而Android的架构正是为了更好地满足硬件设备厂商、开发者以及用户而设计的。我非常推荐你看看《关于Android设计及其意义》和《Android技术架构演进与未来》可以让你从Android系统设计到技术支撑系统发展有更加深刻的理解。
2. 如何做架构选型
对于Android开发者来说很多架构和框架已经非常成熟通常我们更多面临的问题是如何为自己的应用选择合适的框架。回顾一下专栏前面学习过的内容其实我们已经做过一次次的选择例如OkHttp、Cronet、Mars应该选择哪个作为我们的高质量网络库JSON、Protocol Buffers数据序列化方案该如何选择等。
网络库、图片库、UI框架、消息通信框架、存储框架无论是GitHub还是Google官方都有非常多的方案在选择过程我们主要要考虑下面三个因素
框架的成熟度。框架是否已经被大量应用所实践,特别是亿级以上的应用。还有就是框架目前是否还在维护、框架的性能如何等。
工具链的成熟度。配套的工具链是否成熟、完善。例如Flutter作为一门新的技术从开发、编译、测试到发布是否有完善的工具链支持。
人员的学习成本、文档是否完备。结合团队的现状,需要考虑框架的学习成本是否可以接受、学习路径是否平滑、有没有足够的文档和社区支持。
对于架构选型,康威定律是比较重要的准则,这里推荐你看看《从康威定律和技术债看研发之痛》这篇文章,我们的组织架构、代码架构以及流程都应该跟我们团队的规模相匹配。这句话怎么理解呢?就是架构设计或者架构选型不能好高骛远,我们有多大的规模,就做多少的支撑。警惕长期的事情短期做,或者短期的事情长期做。
微信在2013年就开始了模块化改造与此同时淘宝则进行了组件化改造。为什么会有这样的差别呢因为当时微信只有一个团队在开发Android端也就30人不到。为了代码的隔离微信将基础组件下沉放到单独的仓库由专门的人员负责。对于业务来说依然只需要保留同一个仓库只是拆成不同的业务Module。感兴趣的同学可以参考《微信Android模块化架构重构实践》。
但对于淘宝来说,当时就有几百人同时在一个应用上面开发,而且这些人分别属于不同的团队,分散在全国各地。所以无论是基于代码的权限保护,还是从开发效率的考量,都要求将所有业务模块隔离开来,也就是每个业务模块都应该是单独的仓库。
什么是架构演进
“没有过不去的坎只有过不完的坎”。在业务发展的过程总会遇到一些新的问题而且可能在发展到某一时刻时一些旧的问题就不复存在了。例如为了兼容Android 4.X当年我们在架构上做了大量的兼容设计但是当不再需要兼容4.X设备的时候这些包袱我们就可以适时抛弃掉。
1. 为什么要做架构演进
架构是为了业务需求场景服务,那它也要顺应业务的变化而适时调整。也就是说,架构需要跟随业务的发展而演进。
“君有疾在腠理,不治将恐深”,微信每年都会经历一次大的重构,因为我们坚持代码架构最终都会腐烂,该推倒了就该重构,不要一直修修补补。架构演进可以给团队带来下面几个变化:
打破不满。需要打破保守的做法,要积极面对不合理的地方。团队定期需要着手开启重构,将大家平日对代码的不满释放出来。将架构的腐化(效率降低、抱怨上升)转化为架构优化的动力。
重构信任。重构开发者之间的心态,不定期的推动模块重构。一些问题的解决,往往可以推动更多人去尝试。
团队培养。重构也是团队进步的机会,让更多的成员掌握架构能力,培养全员架构意识,实现“人人都是架构师”。
但是对于架构演进的过程,我们需要有辨别能力,也就是常说的“技术视野”。这里包括对各种技术栈的选择和比较、架构设计的考虑,要结合业务和团队当前的情况,做出合理的判断,要清楚的知道做什么事情收益最大等。
这里的反面例子可能就是辛辛苦苦造了一套轮子结果发现别人早就有了甚至比我们做得更好。这个问题的原因就在于你的技术视野。在“高质量开发”和“高效开发”模块我反复地跟你分享目前国内外大厂的最新实践方案正是希望提升我们的技术视野。特别是“高效开发”模块可能有同学会认为这些话题太大了跟自己好像关系不大。其实应用开发流程的每一个步骤都关系到你我同时又涉及大量的内容每一块铺开来可能都可以是一个新的专栏。而作为“Android开发高手课”我更想从顶层给你呈现完整的架构设计而不是去详细分析某个细节优化点。这些都是希望可以帮你站在高处看问题全面提升你的技术视野。
对于技术视野的培养,可能没有太多的捷径,需要我们经过长时间的实践,经历反反复复的挫折,才能从“巨婴”成长为“大师”。
在“架构演进”模块,为了进一步帮助我们提升技术视野和架构的能力,我准备了下面这些内容:
GOT Hook、PLT Hook、Inline Hook我将对比三种Native Hook框架的原理和差异告诉你应该如何去选择。以及对于Native开发我还有哪些经验。
大前端纷纷扰扰,如何演进和选择,如何选择适合我们应用的跨平台和动态化方案。
技术日新月异对于新技术我们应该如何考虑是否跟进Flutter是不是真的可以一统天下。
在应用开发之外我们还邀请了三位专家分别介绍他们在移动游戏、音视频和AI领域开发的经验。对于这些领域它们的架构是什么样的我们又该如何学习和转型。
2. 如何做架构演进
架构演进是必要的,但是我们需要充分认识到困难,真正去做远比想要难多了,特别是其中各种各样的历史包袱问题。
架构的演进,通常来说具体实践方式就是重构。如果我们下定决心要重构,我有两个小建议送给你:
演进式的,符合团队现状的。我们很难找到一个性能最好、成本最省、时间最快的方案,需要权衡性能、成本、时间三者的关系。如果我们时间充裕,那可以朝着更好的性能目标去努力。但如果时间紧急,我们可以分阶段去重构。
可度量的,每个阶段都要有成果。架构演进不能一味“憋大招”,最好能分阶段实施,并且每个阶段都要有成果。这样可以让团队成员更快地感受到优化成果,也可以激励更多的人参与到重构的事业中。
在《Android技术架构演进与未来》一文中回顾了Android版本的发布时间线。Android系统每年都会发布一个新的版本每个版本也会有大大小小的重构。重构的目的依然是希望更好地满足用户、开发者以及硬件厂商的诉求。例如为了提升手机的续航能力我们可以回顾一下Android在耗电优化的演进历程。
为了应用程序执行速度更快Android Runtime也是每个版本都会优化的模块下面是Android Runtime各个版本的演进历程。
对于虚拟机的运行机制与各个版本的差异,也是很多公司在面试时喜欢问的。下面是一些关于虚拟机架构演进比较不错的资料,我把它们分享给你。
Whats new in Android Runtime (Google I/O 18)
Android 8.0中的ART功能改进
oat格式演进
而Android 8.0的Treble计划引入了HAL硬件抽象层解决了硬件厂商升级难的问题。但是即使厂商升级到最新的系统也并不能直接交付给用户这里还存在应用兼容性的问题。为什么Android P要极力推出Hidden API的限制这里最初考虑的并不是安全性的问题而是为了减少每次Android版本升级的兼容性适配时间让Android版本的发布节奏快起来。
Hidden API的设计也有出于架构演进的考量Android不希望出现修改Framework内部任意一个私有方法的时候都可能会引起外部应用兼容适配这会对重构带来非常大的包袱。
Android系统如此应用的架构演进也是如此。由于组件化带来的各种性能问题支付宝和淘宝在架构上也顺应了这种变化。在工程结构上它们依然保留组件在仓库上的代码隔离。但是在最终产物上组件化已经回归模块化非核心业务会逐渐迁移到H5或者小程序。
无论微信、支付宝、淘宝大家都想当超级App努力成为满足用户尽可能多需求的微型操作系统。应用的架构也需要顺应业务形态的转变在《敏捷开发与动态更新在支付宝 App 内的实践》一文中,也描述了支付宝这几年在架构升级驱动研发方式转变,推荐你仔细读读。
总结
从初步接触架构设计,到基本掌握架构的精髓,可以说同样也没有捷径可言。架构设计能力的成长是建立在一个又一个坑、一次又一次的重构之上。不过成为架构师这个目标并不“遥不可及”,在日常工作中我们可以反复进行锻炼。
架构设计不一定是整个应用或者系统的设计,也可以是一个模块或者一个需求的设计。每接手一个需求,我们可以对自己提更高的要求,更加细致地考虑问题。例如如何对现有代码的影响最小,如何快捷清晰的实现功能,在开发过程中如何对组件、控件做更好的封装,如何去优化性能,有没有哪些新的技术可以帮助开发这个需求等。
课后作业
一个技术人的一生应该有个代表作,给自己的技术生涯一个交代。在你的工作中,有没有令你感到满意的架构设计(某个应用、某个模块或者某个框架都可以)?你对架构演进有什么看法,又遇到过哪些问题?欢迎留言分享给我和其他同学。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,296 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
35 Native Hook 技术,天使还是魔鬼?
相信一直坚持学习专栏的同学对Hook一定不会陌生在前面很多期里我无数次提到Hook。可能有不少同学对于Hook还是“懵懵懂懂”那今天我们从来头了解一下什么是Hook。
Hook直译过来就是“钩子”的意思是指截获进程对某个API函数的调用使得API的执行流程转向我们实现的代码片段从而实现我们所需要得功能这里的功能可以是监控、修复系统漏洞也可以是劫持或者其他恶意行为。
相信许多新手第一次接触Hook时会觉得这项技术十分神秘只能被少数高手、黑客所掌握那Hook是不是真的难以掌握希望今天的文章可以打消你的顾虑。
Native Hook的不同流派
对于Native Hook技术我们比较熟悉的有GOT/PLT Hook、Trap Hook以及Inline Hook下面我来逐个讲解这些Hook技术的实现原理和优劣比较。
1. GOT/PLT Hook
在Chapter06-plus中我们使用了PLT Hook技术来获取线程创建的堆栈。先来回顾一下它的整个流程我们将libart.so中的外部函数pthread_create替换成自己的方法pthread_create_hook。
你可以发现GOT/PLT Hook主要是用于替换某个SO的外部调用通过将外部函数调用跳转成我们的目标函数。GOT/PLT Hook可以说是一个非常经典的Hook方法它非常稳定可以达到部署到生产环境的标准。
那GOT/PLT Hook的实现原理究竟是什么呢你需要先对SO库文件的ELF文件格式和动态链接过程有所了解。
ELF格式
ELFExecutableand Linking Format是可执行和链接格式它是一个开放标准各种UNIX系统的可执行文件大多采用ELF格式。虽然ELF文件本身就支持三种不同的类型重定位、执行、共享不同的视图下格式稍微不同不过它有一个统一的结构这个结构如下图所示。
网上介绍ELF格式的文章非常多你可以参考《ELF文件格式解析》。顾名思义对于GOT/PLT Hook来说我们主要关心“.plt”和“.got”这两个节区
.plt。该节保存过程链接表Procedure Linkage Table
.got。该节保存着全局的偏移量表。
我们也可以使用readelf -S来查看ELF文件的具体信息。
链接过程
接下来我们再来看看动态链接的过程当需要使用一个Native库.so文件的时候我们需要调用dlopen("libname.so")来加载这个库。
在我们调用了dlopen("libname.so")之后系统首先会检查缓存中已加载的ELF文件列表。如果未加载则执行加载过程如果已加载则计数加一忽略该调用。然后系统会用从libname.so的dynamic节区中读取其所依赖的库按照相同的加载逻辑把未在缓存中的库加入加载列表。
你可以使用下面这个命令来查看一个库的依赖:
readelf -d <library> | grep NEEDED
下面我们大概了解一下系统是如何加载的ELF文件的。
读ELF的程序头部表把所有PT_LOAD的节区mmap到内存中。
从“.dynamic”中读取各信息项计算并保存所有节区的虚拟地址然后执行重定位操作。
最后ELF加载成功引用计数加一。
但是这里有一个关键点在ELF文件格式中我们只有函数的绝对地址。如果想在系统中运行这里需要经过重定位。这其实是一个比较复杂的问题因为不同机器的CPU架构、加载顺序不同导致我们只能在运行时计算出这个值。不过还好动态加载器/system/bin/linker会帮助我们解决这个问题。
如果你理解了动态链接的过程,我们再回头来思考一下“.got”和“.plt”它们的具体含义。
The Global Offset Table (GOT)。简单来说就是在数据段的地址表假定我们有一些代码段的指令引用一些地址变量编译器会引用GOT表来替代直接引用绝对地址因为绝对地址在编译期是无法知道的只有重定位后才会得到 GOT自己本身将会包含函数引用的绝对地址。
The Procedure Linkage Table (PLT)。PLT不同于GOT它位于代码段动态库的每一个外部函数都会在PLT中有一条记录每一条PLT记录都是一小段可执行代码。 一般来说外部代码都是在调用PLT表里的记录然后PLT的相应记录会负责调用实际的函数。我们一般把这种设定叫作“蹦床”Trampoline
PLT和GOT记录是一一对应的并且GOT表第一次解析后会包含调用函数的实际地址。既然这样那PLT的意义究竟是什么呢PLT从某种意义上赋予我们一种懒加载的能力。当动态库首次被加载时所有的函数地址并没有被解析。下面让我们结合图来具体分析一下首次函数调用请注意图中黑色箭头为跳转紫色为指针。
我们在代码中调用func编译器会把这个转化为func@plt并在PLT表插入一条记录。
PLT表中第一条或者说第0条PLT[0]是一条特殊记录它是用来帮助我们解析地址的。通常在类Linux系统这个的实现会位于动态加载器就是专栏前面文章提到的/system/bin/linker。
其余的PLT记录都均包含以下信息
跳转GOT表的指令jmp *GOT[n])。
为上面提到的第0条解析地址函数准备参数。
调用PLT[0]这里resovler的实际地址是存储在GOT[2] 。
在解析前GOT[n]会直接指向jmp *GOT[n]的下一条指令。在解析完成后我们就得到了func的实际地址动态加载器会将这个地址填入GOT[n]然后调用func。
如果对上面的这个调用流程还有疑问你可以参考《GOT表和PLT表》这篇文章它里面有一张图非常清晰。
当第一次调用发生后之后再调用函数func就高效简单很多。首先调用PLT[n]然后执行jmp *GOT[n]。GOT[n]直接指向func这样就高效的完成了函数调用。
总结一下因为很多函数可能在程序执行完时都不会被用到比如错误处理函数或一些用户很少用到的功能模块等那么一开始把所有函数都链接好实际就是一种浪费。为了提升动态链接的性能我们可以使用PLT来实现延迟绑定的功能。
对于函数运行的实际地址我们依然需要通过GOT表得到整个简化过程如下
看到这里相信你已经有了如何Hack这一过程的初步想法。这里业界通常会根据修改PLT记录或者GOT记录区分为GOT Hook和PLT Hook但其本质原理十分接近。
GOT/PLT Hook实践
GOT/PLT Hook看似简单但是实现起来也是有一些坑的需要考虑兼容性的情况。一般来说推荐使用业界的成熟方案。
微信Matrix开源库的ELF Hook它使用的是GOT Hook主要使用它来做性能监控。
爱奇艺开源的的xHook它使用的也是GOT Hook。
Facebook的PLT Hook。
如果不想深入它内部的原理我们只需要直接使用这些开源的优秀方案就可以了。因为这种Hook方式非常成熟稳定除了Hook线程的创建我们还有很多其他的使用范例。
“I/O优化”中使用matrix-io-canary Hook文件的操作。
“网络优化”中使用Hook了Socket的相关操作具体你可以参考Chapter17。
这种Hook方法也不是万能的因为它只能替换导入函数的方式。有时候我们不一定可以找到这样的外部调用函数。如果想Hook函数的内部调用这个时候就需要用到我们的Trap Hook或者Inline Hook了。
2. Trap Hook
对于函数内部的Hook你可以先从头想一下会发现调试器就具备一切Hook框架具有的能力可以在目标函数前断住程序修改内存、程序段继续执行。相信很多同学都会使用调试器但是对调试器如何工作却知之甚少。下面让我们先了解一下软件调试器是如何工作的。
ptrace
一般软件调试器都是通过ptrace系统调用和SIGTRAP配合来进行断点调试首先我们来了解一下什么是ptrace它又是如何断住程序运行然后修改相关执行步骤的。
所谓合格的底层程序员对于未知知识的了解第一步就是使用man命令来查看系统文档。
The ptrace() system call provides a means by which one process (the “tracer”) may observe and control the execution of another process (the “tracee”), and examine and change the tracees memory and registers. It is primarily used to implement breakpoint debugging and system call tracing.
这段话直译过来就是ptrace提供了一种让一个程序tracer观察或者控制另一个程序tracee执行流程以及修改被控制程序内存和寄存器的方法主要用于实现调试断点和系统调用跟踪。
我们再来简单了解一下调试器GDB/LLDB是如何使用ptrace的。首先调试器会基于要调试进程是否已启动来决定是使用fork或者attach到目标进程。当调试器与目标程序绑定后目标程序的任何signal除SIGKILL都会被调试器做先拦截调试器会有机会对相关信号进行处理然后再把执行权限交由目标程序继续执行。可以你已经想到了这其实已经达到了Hook的目的。
如何Hook
但更进一步思考如果我们不需要修改内存或者做类似调试器一样复杂的交互我们完全可以不依赖ptrace只需要接收相关signal即可。这时我们就想到了句柄signal handler。对我们完全可以主动raise signal然后使用signal handler来实现类似的Hook效果。
业界也有不少人将Trap Hook叫作断点Hook它的原理就是在需要Hook的地方想办法触发断点并捕获异常。一般我们会利用SIGTRAP或者SIGKILL非法指令异常这两种信号。下面以SIGTRAP信号为例具体的实现步骤如下。
注册信号接收句柄signal handler不同的体系结构可能会选取不同的信号我们这里用SIGTRAP。
在我们需要Hook得部分插入Trap指令。
系统调用Trap指令进入内核模式调用我们已经在开始注册好的信号接收句柄signal handler
执行我们信号接收句柄signal handler这里需要注意所有在信号接收句柄signal handler执行的代码需要保证async-signal-safe。这里我们可以简单的只把信号接收句柄当作蹦床使用logjmp跳出这个需要async-signal-safe正如我在“崩溃分析”所说的部分函数在signal回调中使用并不安全的环境然后再执行我们Hook的代码。
在执行完Hook的函数后我们需要恢复现场。这里如果我们想继续调用原来的函数A那直接回写函数A的原始指令并恢复寄存器状态。
Trap Hook实践
Trap Hook兼容性非常好它也可以在生产环境中大规模使用。但是它最大的问题是效率比较低不适合Hook非常频繁调用的函数。
对于Trap Hook的实践方案在“卡顿优化”中我提到过Facebook的Profilo它就是通过定期发送SIGPROF信号来实现卡顿监控的。
3. Inline Hook
跟Trap Hook一样Inline Hook也是函数内部调用的Hook。它直接将函数开始Prologue处的指令更替为跳转指令使得原函数直接跳转到Hook的目标函数函数并保留原函数的调用接口以完成后续再调用回来的目的。
与GOT/PLT Hook相比Inline Hook可以不受GOT/PLT表的限制几乎可以Hook任何函数。不过其实现十分复杂我至今没有见过可以用在生产环境的实现。并且在ARM体系结构下无法对叶子函数和很短的函数进行Hook。
在深入“邪恶的”细节前我们需要先对Inline Hook的大体流程有一个简单的了解。
如图所示Inline Hook的基本思路就是在已有的代码段中插入跳转指令把代码的执行流程转向我们实现的Hook函数中然后再进行指令修复并跳转回原函数继续执行。这段描述看起来是不是十分简单而且清晰
对于Trap Hook我们只需要在目标地址前插入特殊指令并且在执行结束后把原始指令写回去就可以了。但是对Inline Hook来说它是直接进行指令级的复写与修复。怎么理解呢就相当于我们在运行过程中要去做ASM的字节码修改。
当然Inline Hook远远比ASM操作更加复杂因为它还涉及不同CPU架构带来的指令集适配问题我们需要根据不同指令集来分别进行指令复写与跳转。
下面我先来简单说明一下Android常见的CPU架构和指令集
x86和MIPS架构。这两个架构已经基本没有多少用户了我们可以直接忽视。一般来说我们只关心主流的ARM体系架构就可以了。
ARMv5和ARMv7架构。它的指令集分为4字节对齐的定长的ARM指令集和2字节对齐的变长Thumb/Thumb-2指令集。Thumb-2指令集虽为2字节对齐但指令集本身有16位也有32位。其中ARMv5使用的是16位的Thumb16在ARMv7使用的是32位的Thumb32。不过目前ARMv5也基本没有多少用户了我们也可以放弃Thumb16指令集的适配。
ARMv8架构。64位的ARMv8架构可以兼容运行32位所以它在ARM32和Thumb32指令集的基础上增加了ARM64指令集。关于它们具体差异你可以查看ARM的官方文档。
ARM64目前我还没有适配不过Google Play要求所有应用在2019年8月1日之前需要支持64位所以今年上半年也要折腾一下。但它们的原理基本类似下面我以最主流的ARMv7架构为例为你庖丁解牛Inline Hook。
ARM32指令集
ARMv7中有一种广为流传的$PC=$PC+8的说法。这是指ARMv7中的三级流水线取指、解码、执行换句话说$PC寄存器总是指向正在取指的指令而不是指向正在执行的指令。取指总会比执行快2个指令在ARM32指令集下2个指令的长度为8个字节所以$PC寄存器的值总是比当前指令地址要大8。
是不是感觉有些复杂其实这是为了引出ARM指令集的常用跳转方法
LDR PC, [PC, #-4] ;0xE51FF004
$TRAMPOLIN_ADDR
在了解了三级流水线以后就不会对这个PC-4有什么疑惑了。
按照我们前面描述的Inline Hook的基本步骤首先插入跳转指令跳入我们的蹦床Trampoline执行我们实现的Hook后函数。这里还有一个“邪恶的”细节由于指令执行是依赖当前运行环境的即所有寄存器的值而我们插入新的指令是有可能更改寄存器的状态的所以我们要保存当前全部的寄存器状态到栈中使用BLX指令跳转执行Hook后函数执行完成后再从栈中恢复所有的寄存器最后才能像未Hook一样继续执行原先函数。
在执行完Hook后的函数后我们需要跳转回原先的函数继续执行。这里不要忘记我们在一开始覆盖的LDR指令我们需要先执行被我们复写的指令然后再使用如下指令继续执行原先函数。
LDR PC, [PC, #-4]
HOOKED_ADDR+8
是不是有一种大功告成的感觉其实这里还有一个巨大的坑在等着我们那就是指令修复。前面我提到保存并恢复了寄存器原有的状态已达到可以继续像原有程序一样的继续执行。但仅仅是恢复寄存器就足够么显然答案是否定的虽然寄存器被我们完美恢复了但是2条备份的指令被移动到了新的地址。当执行它们的时候$PC寄存器的值是与原先不同的。这条指令的操作如果涉及$PC的值那么它们将会执行出完全不同的结果。
到这里我就不对指令修复再深入解析了,感兴趣的同学可以在留言区进行讨论。
Inline Hook实践
对于Inline Hook虽然它功能非常强大而且执行效率也很高但是业界目前还没有一套完全稳定可靠的开源方案。Inline Hook一般会使用在自动化测试或者线上疑难问题的定位例如“UI优化”中说到libhwui.so崩溃问题的定位我们就是利用Inline Hook去收集系统信息。
业界也有一些不错的参考方案:
Cydia Substrate。在Chapter3中我们就使用它来Hook系统的内存分配函数。
adbi。支付宝在GC抑制中使用的Hook框架不过已经好几年没有更新了。
各个流派的优缺点比较
最后我们再来总结一下不同的Hook方式的优缺点
1.GOT/PLT Hook是一个比较中庸的方案有较好的性能中等的实现难度但其只能Hook动态库之间的调用的函数并且无法Hook未导出的私有函数而且只存在安装与卸载2种状态一旦安装就会Hook所有函数调用。
2.Trap Hook最为稳定但由于需要切换运行模式R0/R3且依赖内核的信号机制导致性能很差。
3.Inline Hook是一个非常激进的方案有很好的性能并且也没有PLT作用域的限制可以说是一个非常灵活、完美的方案。但其实现难度极高我至今也没有看到可以部署在生产环境的Inline Hook方案因为涉及指令修复需要编译器的各种优化。
但是需要注意无论是哪一种Hook都只能Hook到应用自身的进程我们无法替换系统或其他应用进程的函数执行。
总结
总的来说Native Hook是一门非常底层的技术它会涉及库文件编译、加载、链接等方方面面的知识而且很多底层知识是与Android甚至移动平台无关的。
在这一领域做安全的同学可能会更有发言权我来讲可能班门弄斧了。不过希望通过这篇文章让你对看似黑科技的Hook有一个大体的了解希望可以在自己的平时的工作中使用Hook来完成一些看似不可能的任务比如修复系统Bug、线上监控Native内存分配等。
课后作业
今天的信息量是不是有点大关于Native Hook你对它有什么看法还有哪些疑问欢迎留言跟我和其他同学一起讨论。
Native Hook技术的确非常复杂即使我们不懂得它的内部原理我们也应该学会使用成熟的开源框架去实现一些功能。当然对于想进一步深入研究的同学推荐你学习下面这些资料。
链接程序和库指南
程序员的自我修养:链接、装载与库
链接器和加载器 Linkers and Loaders
Linux二进制分析 Learning Linux Binary Analysis
如果你对调试器的研究也非常有兴趣强烈推荐Eli Bendersky写的博客里面有一系列非常优秀的底层知识文章。其中一些关于debugger的感兴趣的同学可以去阅读并亲手实现一个简单的调试器。
how-debuggers-work-part-1
how-debuggers-work-part-2-breakpoints
how-debuggers-work-part-3-debugging-information
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,229 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
36 跨平台开发的现状与应用
在2016年我参加了两场移动技术大会在当时的GMTC大会上原生开发看起来如日中天。
转眼到了2017年HTML5性能越来越好Facebook的React Native、阿里的Weex等跨平台方案在越来越多的公司中实践微信小程序更是给了原生开发最沉重的一击许多小公司可能不再需要开发自己的应用。
“Write once, run anywhere”人们对跨平台开发的尝试从来没有停止过。特别在这个终端碎片化的时代一份代码可以在同一个平台不同的系统版本甚至在不同的平台上运行对开发者的吸引力越来越大。
回想一下HTML5与Native开发的斗争已经持续快十年了那它们的现状是怎样的呢React Native和Weex方案有哪些优势又存在什么问题小程序是不是真的可以一统江湖焦虑的Native开发又应该如何在这个潮流之下谋发展呢
跨平台开发的现状
从2017年开始GMTC“移动技术大会”就更名为“大前端技术大会”。从现在看来前端开发和Native开发并没有谁取代谁而是正在融合融合之后的产物就是所谓的“大前端”。为了顺应这种趋势很多大公司的组织架构也做了相应的调整把前端团队和iOS、Android一起合并为大前端团队。
移动Web技术、React Native和Weex、小程序它们是目前最常用到的跨平台开发方案下面我们一起来看看它们的应用现状。当然对于今年最为火热的Flutter技术专栏后面我会花专门的篇幅去介绍。
1. Web
从桌面时代开始以浏览器为载体的Web技术就具备跨平台、动态更新、扩展性强等优点。随着移动设备性能的增强Web页面的性能也逐渐变得可以接受。客户端中出现越来越多的内嵌Web页面很多应用也会把一些功能模块改为Web实现。
浏览器内核
一个Web页面是由HTML + CSS + JavaScript组成通过浏览器内核执行并渲染成开发者预期的界面。
浏览器内核主要包括两大块功能,它们分别是:
浏览器引擎。浏览器引擎负责处理HTML和CSS遵照的是W3C标准。
JavaScript引擎。JS引擎负责处理JS遵照的是ECMAScript标准。
它们两者互相独立但又有非常紧密的结合而且在不同浏览器内核中的实现也是不太一样的。但随着微软的Edge宣布将内核切换成Chromium目前这个战场主要就剩下苹果和Google两个玩家它们的浏览器引擎分别是Webkit和Blink其实Blink也是fork自WebkitJS引擎分别是JavaScriptCore和V8。
对于浏览器的渲染流程可能很多Android开发并没有前端同学熟悉。一般来说HTML、CSS、JS以及页面用到的一些其他资源图片、视频、字体等都需要从网络下载。而HTML会被解析成DOMCSS会被解析成CSSOMJS会由JS引擎执行最后整合DOM和CSSOM之后合成为一棵Render Tree。
当然整个浏览器的渲染流程不是三言两语就可以说清楚的,下面是我找的一些不错的参考资料,感兴趣的同学可以深入学习:
浏览器渲染一颗像素的诞生强烈推荐看PPT Life of a Pixel
浏览器引擎What is a browser engine?
浏览器系列教程How Browsers Work: Behind the scenes of modern web browsers
Google Web开发者官网Rendering on the Web
虽然Chromium是开源的但是因为它的复杂性国内对它有深入研究的人非常少而拥有定制修改能力的人更是少之又少。因此这块需要投入大量的人力物力国内比较有名的是UC浏览器的U4内核以及腾讯浏览器的X5内核。
性能现状
基于WebView的H5跨平台方案优点确实非常明显。但是性能是它目前最大的问题主要表现在以下两个方面
启动白屏时间。WebView是一个非常重量级的控件无论是WebView的初始化还是整个渲染流程都非常耗时。这导致界面启动的时候会出现一段白屏时间体验非常糟糕。
响应流畅度。由于单线程、历史包袱等原因页面的渲染和JavaScript的执行效率都不如原生。在一些重交互或者动画复杂的场景H5的性能还无法满足诉求。
所以在移动端H5主要应用在一些交互不太复杂的场景一般来说即使帧率不如原生但也基本符合要求。从我个人的感受来看H5当前最大的问题在于启动的白屏时间。
对于Android界面启动的过程我们在窗口动画还没结束的时候大部分时候就已经完成了页面的渲染。启动一个Activity界面我们一般要求在300毫秒以内。
回顾一下浏览器内核渲染的流程,我们其实可以把整个过程拆成三个部分:
Native时间。主要是Activity、WebView创建以及WebView初始化的时间。虽然首次创建WebView的时间会长一些但总体Native时间是可控的。
网络时间。这里包括DNS、TCP、SSL的建连时间和下载主文档的时间。当解析主文档的时候也需要同步去下载主文档依赖的CSS和JS资源以及必要的数据。
渲染时间。浏览器内核构建Render Tree、Layout并渲染到屏幕的时间。
如上图所示我们会更加关心用户看到完整的页面的时间T2这里可以用T2秒开率作为启动速度的衡量标准。
优化方法
Native界面的T2秒开率做到90%以上并不困难相比之下大部分没有经过优化的Web页面的T2秒开率可能都在40%以下,差距还是非常明显的。
那又应该如何去优化呢?从前端的角度来看,常用的优化方法有:
加快请求速度。整个启动过程中网络时间是最不可控的。这里的优化方法有很多例如预解析DNS、减少域名数、减少HTTP请求数、CDN分发、请求复用、懒加载、Gzip压缩、图片格式压缩。
代码优化。主文档的大小越小越好要求小于15KB这里要求我们对HTML、CSS以及JS进行代码优化。以JS为例前端的库和框架真的太多了可能一不小心就引入了各种的依赖框架。对于核心页面我们要求只能使用原生JS或者非常轻量级的JS框架例如使用只有几KB的Preact代替庞大的React框架。
SSR。对于浏览器的渲染流程我上面描述的是CSR渲染模式在这种模式下服务器只返回页面的基本框架。事实上还有一种非常流行的SSRServer Side Rendering渲染模式服务器可以一次性生成直接进行渲染的HTML。这样在T2之前我们可以做到只有一个网络请求但是带来的代价就是服务器计算资源的增加。一般来说我们会在服务器前置CDN来解决访问量的问题。
通过上面的这些优化特别是SSR这个“终极大招”页面的T2秒开率达到70%并不是非常困难的事情。
前端同学能做的都已经做了,接下来我们还可以做些什么呢?这个时候就需要客户端开发登场了。
WebView预创建。提前创建和初始化WebView以及实现WebView的复用这块大约可以节省100200毫秒。
缓存。H5是有多级的缓存机制例如Memory Cache存放在内存中一般资源响应回来就会放进去页面关闭就会释放。Client Cache也就是客户端缓存例如我们最常用的离线包方案提前将需要网络请求的数据下发到客户端通过拦截浏览器的资源请求实现加载。Http Cache是我们比较熟悉的缓存机制而Net Cache就是指DNS解析结果的缓存或预连接的缓存等。
从性能上看Memory Cache > Client Cache >= Http Cache > Net Cache。所谓的缓存就是在用户真正点击打开页面之前提前把数据、资源下载到本地内存或者磁盘中并放到内核相应的缓存中。例如即使我们使用了SSR也可以在用户点击之前提前把服务器渲染好的HTML下载好这样用户真正打开页面的时候可以做到完全没有网络请求。
通过预请求的优化即使比较复杂的页面T2秒开率也可以达到80%以上。但是既然是预请求就会有命中率的问题,服务器也增加了没有真正命中的请求数。所以在客户端性能和服务器压力之间,我们需要找到一个平衡点。
那还有没有进一步优化的空间这个时候需要我们进一步往底层走需要我们有定制修改甚至优化内核的能力。例如很多接口官方的浏览器内核可能并没有暴露而腾讯和UC的内核里面都会有很多的特殊接口。
托管所有网络请求。我们不仅可以托管浏览器的Get请求其他的所有Post请求也能接管这样我们可以做非常多的定制化优化。
私有接口。我们可以暴露很多浏览器的一些非公开接口。以预渲染为例,我可以指定在内存直接渲染某个页面,当用户真正打开的时候,只需要直接做刷新就可以了,实现真正的“秒开”。
兼容性和安全。Android的碎片化导致浏览器内核的兼容性实在令人头疼而且旧版本内核还存在不少的安全漏洞。在应用自带浏览器内核可以解决这些问题而且高版本的内核特性也会更加完善例如支持TLS 1.3、QUIC等。但是带来的代价是安装包增大20MB左右当然我们也可以采用动态下载的方式。
定制的自有页面 + 定制的浏览器内核 + 极致的优化即使是比较复杂的页面T2秒开率也可以达到90%以上平均T2时间可以做到400毫秒以下。
2. React Native和Weex
基于WebView的H5跨平台方案经过近乎疯狂的性能优化看起来性能真的不错了。但是对于一些交互和动画复杂的场景例如左右滑屏、手势性能还是无法满足要求。
Facebook在2015年开源了React Native它抛弃了WebView利用JavaScriptCore来做桥接将JavaScript调用转为Native调用。也就是说React Native最终会生成对应的自定义原生控件走的是系统原生的渲染流程。
而阿里在2016年也开源了Weex它的思路跟React Native很像但是上层DSL使用的是Vue。对于Weex和React Native的架构介绍网上的文章非常多例如《大前端的下一站何去何从》和《Weex技术演进》。
但是世上哪有十全十美的方案React Native/Weex方案为了能达到接近原生开发的性能和交互体验必然要在跨平台和动态性上面做出了牺牲。
React Native和Weex向上对接了前端生态向下对接了原生渲染看起来是非常完美的方案。但是前端和客户端客户端中的Android和iOS它们的差异并不那么容易抹平强行融合就会遇到各种各样的坑。
“React Native从入门到放弃”是很多开发者的心声去年Airbnb、Udacity都相继宣布放弃使用React Native。React Native/Weex并没有彻底解决跨平台的问题而且考虑到对外分享和降级容灾的需要我们依然需要开发一个H5版本的页面。
为了解决这个问题React Native的使用者需要引入一层非常重的中间层期望在这个中间层中帮助我们去抹平这些差异。例如京东的JDReact、携程的Ctrip React Native。
既然React Native和Weex在跨平台上面做了牺牲那它的性能和交互是不是能直接对齐Native开发呢非常遗憾 目前它们的性能我觉得主要还有两个瓶颈。
JS的执行时间。React Native和Weex使用的JavaScriptCore引擎虽然它每年都在进步但是JS是解释性的动态语言它的执行效率相比AOT编译后的Java性能依然会在几倍以上的差距。
跨语言的通信成本。既然要对接前端和原生两个生态就无法避免JS -> C++ -> Java/Objective-C 之间频繁的通信和转换,所以这里面会涉及各种序列化,对性能的影响比较大。
虽然相比H5方案在性能方面有了很大的提升但是React Native和Weex也要面对启动时间慢、帧率不如原生的性能问题。它属于一种比较中庸的方案当然也会有自己的应用场景。例如一些二级页面例如淘宝的分会场它们的业务也比较重要但是交互不会特别复杂同时希望保持一定的动态化能力。
当然Facebook已经意识到React Native的种种性能问题目前正在疯狂重构中希望让React Native更加轻量化、更适应混合开发接近甚至达到原生的体验。Facebook现在透漏的信息并不多感兴趣的同学可以参考《庖丁解牛深入剖析React Native下一代架构重构》。
3. 小程序
2017年初张小龙宣布微信小程序诞生。如今小程序已经走过了两年在这两年间小程序的生态也在健康的发展。
每一个应用都有成为超级App的梦想各个大厂纷纷推出自己的小程序框架微信、厂商、支付宝、今日头条、百度、淘宝、Google Play小程序这个战场已然是“七国大乱战”。
但是小程序并不属于一种跨平台开发方案大家更看重的是它的渠道优势考虑如何通过微信、支付宝这些全民App获得更多的流量和用户。从技术上看小程序的框架技术也是开放的我们可以采用H5方案也可以采用React Native和Weex甚至是Flutter。
从实践上看,我们一起来看看已经正式上线的微信小程序、快应用、支付宝小程序以及百度小程序的差异(技术方面大家公开得并不多,可以参考《支付宝小程序框架》)。
我们可以看到除了独树一帜的快应用其他小程序的技术方案基本都跟随了微信。但是考虑到H5在一些场景的性能问题利用浏览器内核提供的同层渲染能力在WebView之上支持一些原生的控件。如果哪一天微信小程序支持了所有的原生控件那也就成为了另外一套React Native/Weex方案。
“神仙打架百姓遭殃”如果我们想从所有的小程序厂商上面获得流量那就要开发七个不同的小程序。不过幸运的是支付宝小程序和快应用也希望已有的微信小程序能快速迁移到自己平台上所以它们的DSL设计都参考了微信的语法可以说微信推出的DSL已然成为了事实标准。
如上图所示我们希望有一套可以整合所有小程序框架的解决方案一次开发就可以生成不同的小程序。滴滴的Chameleon和京东的Taro都致力于解决这个问题目前它们都已经在GitHub上开源。
跨平台开发的应用
从移动开发诞生之初跨平台就已经是大家前赴后继不断追求的目标。我们可以看看nwind在2015年写的一篇文章《聊聊移动端跨平台开发的各种技术》。如今四年过去了大部分观点依然成立并且从最后Dart的介绍中我们甚至可以看到现在Flutter的雏形。
1. 跨平台开发的场景
Android、iOS、PC不同的平台人们的操作习惯、喜好都不尽相同。对于大公司来说完全的跨平台开发可能是一个伪命题不同的平台应用的UI和交互都不太一样。
那我们对跨平台苦苦追寻了那么多年,希望得什么呢?以我的经验来看,跨平台主要的应用场景有:
部分业务。某个业务或者页面的跨平台共享,有的时候我们还希望可以做到跨应用。例如“全民答题”的时候,可以看到这个功能可以运行在头条系的各个应用中。一个公司共用同一套跨平台方案有非常重大的意义,业务可以在不同的应用中尝试。
核心功能。C++才是生命力最顽强的跨平台方案,大公司也将越来越多的核心模块往底层迁移,例如网络库、数据上报、加解密、音视频等。
2. 跨平台开发对比
H5的跨平台方案只要投入不太高的开发成本就能开发出性能、功能还不错的应用。但是如果想做到极致优化很容易发现开发者可控的东西实在比较少性能和功能都依赖浏览器的支持。
这个时候如果想走得更远,我们不仅需要了解浏览器的内部机制,可能还需要具备定制、修改浏览器内核的能力,这也是阿里、腾讯、头条和百度都要组建内核团队的原因。
原生开发则相反刚开始要投入很高的开发成本但是一旦开始有产出之后开发者能够有更的发挥空间而React Native和Weex方案更是希望打造兼顾跨平台、开发成本以及性能的全方位解决方案。
从目前来看每一种方案都有着自己的使用场景无论是React Natve还是H5都无法完全取代Native开发。当然这里也有一个例外那就是如果我们不再开发应用全面投向小程序。小程序跟原生开发的竞争更多的是在渠道层面的竞争。
总结
现在好像有个观点说“Android开发没人要”大家都想转去做大前端开发是不是真的是这样呢事实上无论我们使用哪一种跨平台方案它们最终都要运行在Android平台上。崩溃、内存、卡顿、耗电这些问题依然存在而且可能会更加复杂。而且从H5极致体验优化的例子来看很多优化是需要深入研究平台特性和系统底层机制我们在“高质量开发”中学到的底层和系统相关的知识依然很重要。
对开发者来说唯一不变的就是学习能力。掌握了学习能力和钻研的精神就能够应对这些趋势变化。无论移动开发未来如何变化哪怕有一天AI真的能够自动写代码具备应变能力的人也丝毫不会惧怕的。
课后作业
跨平台开发也是一个很大很大的话题,今天我只能算是抛砖引玉。对于跨平台开发,你有什么看法?在你的应用中,使用了哪种跨平台开发方式?欢迎留言跟我和其他同学一起讨论。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,175 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
37 移动开发新大陆:工作三年半,移动开发转型手游开发
你好我是张绍文。15年认识庆文的时候他还在微信读书负责Android端的性能优化工作。某一天他跟我说想转岗去尝试一下游戏开发当时我脑海里浮现了两个想法一个是游戏部门传说中60个月的年终奖看起来游戏是一个非常有“钱途”的方向另外一个还是担忧抛弃掉Android开发多年的积累转去完全不熟悉的游戏领域他是否可以胜任。
两年过去了,庆文以他的个人经历亲身证明了两件事情:-
第一游戏开发并没有想象中那么困难。当年他是Android版微信读书的技术核心如今在新的岗位上依然也是技术核心。“一通则百通”技术是相通的最珍贵的是我们的学习能力和钻研精神。-
第二客户端平台知识依然非常重要。手游虽然有非常独立的开发体系但是它还是运行在Android或者iOS系统之上。App开发需要用到的很多技术游戏开发也需要用到。作为Android开发我们一层一层往底层走的优化能力反而是其他大部分游戏开发所不具备的也是我们的优势所在。
在我看来无论移动的未来如何发展不管是大前端的天下还是转向手游、IoT、AI、音视频等其他方向今天我们所熟悉的Android平台知识以及学习这些知识所沉淀下来的能力和方法都是最宝贵的财富。下面我们一起来看看庆文同学的移动开发转型手游开发的那些事。
你好我是李庆文来自腾讯旗下一间手游工作室应绍文的邀请在《Android开发高手课》里和你聊聊手游开发那些事。
我在2017年4月从App客户端开发转岗成为一名“用命创造快乐”的手游客户端开发。在转为手游开发之前虽然玩游戏会被游戏精美的画面吸引但是并不清楚游戏是如何将这些精细的画面呈现出来觉得游戏开发好像一个神秘的组织一样不知道游戏开发团队的同学每天在做什么。如果你也对游戏开发有些好奇不知道是不是也有我之前同样的困惑现在我以一个亲历者的身份跟你讲讲转向手游开发的那事儿。
揭开手游开发的面纱
一般来说,一个手游项目组包括制作人、策划组、美术组、运营组、程序组、音频组。其中游戏核心部分完全由项目组自己完成,部分美术、音频可以交给外包来完成。
策划组分为玩法策划、数值策划,是整个游戏的灵魂,一个游戏的核心玩法是否好玩、周边系统能否更好地支撑核心玩法,是整个游戏成败的关键。有些团队也会把运营组放到策划组当中,运营同学主要的工作职责是外部合作、策划游戏内的活动、根据活动数据对游戏玩法或者活动进行相应调整,希望可以尽量延长游戏生命周期并提高游戏收入。
美术组包括原画师、视觉设计、2D/3D视觉特效设计其中负责视觉特效的同学与负责程序的同学沟通较多主要是因为程序需要与动画配合比如某个动画播放到某一个关键帧之后程序要处理某个特定逻辑。
App开发的程序组同学关注的更多是手机系统的架构希望尽量多了解系统能为App提供的接口或者能力。而手游的程序组同学更多是关注游戏引擎的架构和如何在引擎的基础上优化游戏性能。
下面我以Cocos引擎为例带你看看手游的开发流程以及手游是如何运行的。
1. 游戏架构
以我的游戏项目为例,游戏整体架构如下图所示。
其中:
资源更新模块让游戏能够不断发布Bugfix和Features。
配置系统提供给策划和运营同学主要是编辑XLS文件用于配置游戏内的关卡、活动等。策划同学的配置会在游戏编译过程中自动转表根据配置文件生成Lua文件。
Services管理着游戏内的全部数据。
网络模块负责与后台建立Socket并通信。
游戏内使用MVC模型控制各个游戏内模块的跳转。
在单个功能模块中,逻辑与动画分离,逻辑层产生动画事件,动画层消费动画事件并让游戏内的精灵根据事件执行特定动作。
2. 游戏场景设计
Cocos采用节点树形结构来管理游戏对象一个游戏可以划分为不同的场景Scene一个场景又可以分为不同的层Layer一个层又可以拥有任意个精灵Sprite。游戏里关卡、周边系统的切换也就是一个一个场景的切换就像在电影中变换舞台和场地一样由导演Director负责不同游戏场景之间的切换。一个Cocos游戏的基本结构如下
在同一个场景中的多个精灵可以通过执行动作Action来实现缩放、移动、旋转从而达到让游戏“动起来”的目的。
Cocos引擎中坐标系X轴向右、Y轴向上Z轴Z-Order垂直于手机屏幕指向屏幕外由于是2D游戏所以Z轴只用作控制游戏元素的前后顺序。同一个场景中两个重叠或者部分重叠的 SpriteZ-Order大的精灵会遮挡住Z-Order小的精灵相同Z-Order的精灵后添加到场景中的会遮挡住先添加的。
有了树形结构和Z-Order就能利用Cocos引擎构建出任意2D游戏场景了。
3. 手游的运行流程
游戏的主Activity在onCreate时候创建一个GLSurfaceView它继承自View是引擎用于绘制游戏内容的。同时GLSurfaceView也接受玩家的点击事件用于引擎与玩家的交互。
GLSurfaceView创建之后引擎开始执行main.lua脚本导演调用runWithScene接口游戏就进入到第一个“场景”中了。GLSurfaceView中维护了一个while循环循环中发生的事件比如SurfaceView创建、宽高发生变化等通过Renderer接口传递给外部。Renderer其中一个接口就是onDrawFrameCocos会在onDrawFrame接口实现中根据游戏设置的帧率每间隔一段事件通知一次导演执行一帧游戏循环。
借用《我所理解的Cocos2d-x》书中的单帧处理流程图片你可以看到在每帧中
先响应用户输入,因为用户输入可能影响到当前帧接下来的游戏逻辑。
根据每个精灵设置的动画,计算精灵的位置、缩放等属性。
执行游戏逻辑,比如更新玩家得分、修改当前游戏状态等。在这里开发者依然可以更改精灵的各种属性。
遍历UI树根据之前对精灵属性的设置生成绘制命令交给OpenGL绘制最后把渲染的内容显示到屏幕上。
游戏与App开发的异同
1.关于热更新
热更新是App开发和游戏开发都避不开的话题但是App的热更新与游戏的热更新有着本质的区别。App开发的热更新涉及整个系统层面的实现细节比如我在老东家工作期间实现的热更新框架需要了解的内容非常多包括Dex文件的加载过程、SO文件查找过程和APK编译过程的详细细节每个不小心都可能导致热更新不生效甚至导致严重的外网Bug。
然而使用脚本的游戏引擎是天生支持热更新的。手游热更新,也叫作资源更新,更新内容包括代码、纹理图片、界面等。
以Cocos引擎为例Cocos引擎的核心是C++实现的对外提供了JS、Lua接口。业务开发过程中绝大部分代码是Lua代码只有涉及系统相关接口比如支付、音频播放、WebView等才需要写一部分系统相关的代码。Lua代码经过编译之后生成的二进制代码是“.luac”文件并通过luaL_loadbuffer接口来加载一段“.luac”的文件内容。Cocos引擎在初始化过程中会设置“代码查找路径”加载Lua代码的时候会挨个遍历每一个被设置进来的路径直到找到或者找不到对应的“.luac”文件。而热更新只需要下载“.luac”文件放到优先的查找路径中游戏重启之后引擎就会优先加载新的代码啦。
当然还有另外一种更为激进的方式,不需要重启游戏就能达到热更新的目的。
Cocos以LuaEngine为入口执行Lua代码驱动C++部分的图形库、音频库、物理引擎等。LuaEngine缓存了通过luaL_loadbuffer接口加载起来的代码只要重启整个LuaEngine清理掉缓存的代码就能在后续需要执行代码的时候去重新加载代码文件。这样就能做到不重启游戏也能达到真正“热”更新的目的。
其实Unity引擎也是类似可以使用xLua等类似的技术让Unity引擎也能开心地写Lua脚本。
对于热更新图片,只需要清理掉引擎缓存的图片缓存即可。这样引擎在下次使用该图片渲染界面是,发现在缓存中查找不到该图片,自然就会去加载新的图片了。
2. 游戏也需要优化安装包大小
在App开发过程中优化安装包的大小是一个不可避免的话题。压缩图片、代码插件动态下载等众多实现方案也都为大家所熟知。手游安装包大小可能并不像App安装包要求那么严格热门游戏比如《绝地求生》《王者荣耀》安装包大小几乎都接近2GB。虽然玩家对游戏安装包大小不是特别在意但有意识的减小安装包大小也是很有必要的毕竟安装包大小会影响游戏转化率进而影响游戏收入。
手游安装包中图片资源占了安装包体积的绝大部分以PNG图片为主。通常直接压缩PNG图片是有损压缩经过游戏引擎渲染游戏界面会出现很多噪点。这里有一种分离Alpha通道的压缩方案可以供你参考。32位透明PNG图片包含了四个通道RGBA其中每个通道占8 Bit。把RGBA四个通道中的Alpha通道数据拆分出来存储为一张PNG8图片剩下的RGB数据存储为一张JPG格式图片。JPG格式的图片在保证高压缩率的同时也能保证图片质量以此达到压缩图片大小的目的。这种方案的压缩率大概在70%左右。
在游戏运行过程中使用纹理之前先把JPG和PNG8文件都读取到内存将他们包含的RGB和Alpha数据重新合并后交给OpenGL用于渲染。这个压缩方案简单易操作并且运行时不需要额外的第三方库支持。
3. 游戏的性能优化
性能是开发者永远离不开的话题。手游和App开发一样也需要关注游戏的各方面性能指标比如内存、帧率等。
内存
纹理压缩,减少纹理占用的内存。我前面提到的安装优化包大小中压缩图片,也有助于减少内存占用。在美术同学能接受的前提下,降低图片深度也是快速优化内存的一个方法。
使用精灵池,尽量复用已经创建的精灵,减少屏幕中精灵创建的个数,及时回收不用的内存。
切换场景时尽快释放上一个场景的无用纹理。
帧率
减少Draw Call。游戏引擎每次准备数据并发送给GPU去渲染的过程称为一次Draw Call。Cocos把使用相同贴图、blendFunc、Shader的精灵合并到同一个渲染命令中提交给GPU。我们常说的合图就是把很多小图片合并到一张大图片中这样当屏幕中有多个连续渲染的精灵用到同一个大图CPU就会有可能把它们合并之后统一提交给GPU。另外Cocos合并渲染命令是根据精灵在场景中的顺序来做的最终的合并效果不一定是最优的。我们可以实现自己更优化的合并逻辑。
减少屏幕内需要渲染的精灵个数。如果屏幕中有“成组”的元素需要绘制比如排行榜中每个玩家的Item在Cocos中可以使用CCRenderTexture把每个“成组”的元素绘制到CCRenderTexture绑定的纹理然后用CCRenderTexture替代原有的Item。这与Android开发中自己实现View的onDraw函数类似并且这也是减少Draw Call的一种方式。
分帧。真的遇到CPU密集型的任务时例如同屏幕确实需要大量精灵可以把它们拆分在不同帧里尽量保证游戏内每一帧在规定时间内完成否则会出现卡帧现象。
App开发转手游开发的思考
刚刚过去的2018年是游戏比较惨淡的一年游戏版号从暂停审批到现在终于慢慢开始放出简直是千军万马过独木桥。但从目前来看2018年确实也像某些大咖说的那样可能是游戏行业未来几年最好的一年。国内拿不到版号的游戏只能考虑出海来寻求出路但是出海需要在海外当地运营或者找海外代理。自己运营可能会遇到水土不服画风或者游戏内容也可能不满足当地玩家的需求找海外代理却可能面临连游戏代码都被代理商卖掉的风险。
手游早已经结束了野蛮生长的年代6年前可能随便一款游戏都可能成为爆款类似捕鱼达人、保卫萝卜它们诞生在Android、iOS手机爆发的那几年。而现在的游戏玩家需要的是更新颖的玩法和更加精细的游戏体验。例如MOBA类的《王者荣耀》、SLG类的《乱世王者》甚至细分领域的《恋与制作人》也都在各自的玩法上不断打磨让玩家心甘情愿“氪金”。如果想要转向这个赢者通吃的行业确实需要谨慎提前考察好目标团队是否符合你的职业发展、是否有盈利能力尽量避免踩坑。
1. App开发转手游开发可行吗
2018年和2019年市场对App开发的岗位需求量已经不像几年前那么大而且也有一些同学也咨询过我App开发转手游开发是否合适。关于这个问题我的回答是可以转到手游开发岗位而且难度并没有想象中那么大。
手游开发跟App开发相比只不过是换了工作内容关注的领域不同而已基本原理是相通的。App开发需要用到的很多技术游戏开发依然也需要用到比如卡顿分析、网络、I/O优化。而且游戏的大部分核心逻辑库是与平台无关的所以不必担心适应不了新的工作内容。下面我来总结一下转到手游开发的优势和劣势。
优势
对手机系统有深入了解能迅速切入一部分游戏团队内的工作内容。比如我刚刚转到手游团队时解决的第一个问题游戏在Android系统上JNI找不到函数的Crash稳稳占据了每个版本的前三名版本Crash率由于这个问题而一直保持在1%左右。当时由于团队对系统没有深入了解所以并没有很好的解决办法。这个问题其实读一遍Android系统查找Native函数的代码基本就能找到解决方案了。
劣势
毕竟是两个不同的开发方向转方向的同学也需要迎接压力。与毕业后直接进入游戏行业的同学相比“半路出家”的同学相对缺乏原始技术积累需要从基础的概念开始学起比如上面提到的Draw Call、合图、OpenGL、Shader需要消耗大量精力去学习。所以需要转方向的同学足够自律主动补齐这些技术短板。
2. 转方向后的技术路线
有些同学说App开发者的技术路线宽担心手游开发者将来技术路线会越来越窄将来不好找其他工作。有这个担心是正常的手游开发岗位的市场需求量比移动开发需求量小太多但这不应该成为害怕转手游开发的理由。个人觉得需要从一个更宏观的角度看待这两个不同的开发岗位不要钻在技术的牛角尖里无法自拔。技术通道无风险而更应该担心的是游戏的政策风险。
小游戏在2017年和2018年真是火了一把鉴于小游戏入门简单、上手快想要转方向的同学可以先用小游戏来练手。“麻雀虽小五脏俱全”看明白一个小游戏的执行流程之后相信你对游戏开发也就会有一个感性的认识了。
最后祝各位开发者游戏愉快!
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,198 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 移动开发新大陆Android音视频开发
你好我是张绍文。俊杰目前负责微信的音视频开发工作无论是我们平常经常使用到的小视频还是最新推出的即刻视频都是出自他手。俊杰在音视频方面有非常丰富的经验今天有幸请到他来给我们分享他个人对Android音视频开发的心得和体会。
在日常生活中视频类应用占据了我们越来越多的时间各大公司也纷纷杀入这个战场不管是抖音、快手等短视频类型虎牙、斗鱼等直播类型腾讯视频、爱奇艺、优酷等长视频类型还是Vue、美拍等视频编辑美颜类型总有一款适合你。
未来随着5G普及以及网络资费的下降音视频的前景是非常广阔的。但是另一方面无论是音视频的编解码和播放器、视频编辑和美颜的各种算法还是视频与人工智能的结合AI剪片、视频修复、超清化等它们都涉及了方方面面的底层知识学习曲线比较陡峭门槛相对比较高所以也造成了目前各大公司音视频相关人才的紧缺。如果你对音视频开发感兴趣我也非常建议你去往这个方向尝试我个人是非常看好音视频开发这个领域的。
当然音视频开发的经验是靠着一次又一次的“填坑”成长起来的,下面我们一起来看看俊杰同学关于音视频的认识和思考。
大家好我是周俊杰现在在微信从事Android多媒体开发。应绍文的邀请分享一些我关于Android音视频开发的心得和体会。
不管作为开发者还是用户现在我们每天都会接触到各种各样的短视频、直播类的App与之相关的音视频方向的开发也变得越来越重要。但是对于大多数Android开发者来说从事Android音视频相关的开发可能目前还算是个小众领域虽然可能目前深入这个领域的开发者还不是太多但这个方向涉及的知识点可一点都不少。
音视频的基础知识
1. 音视频相关的概念
说到音视频,先从我们熟悉也陌生的视频格式说起。
对于我们来说最常见的视频格式就是MP4格式这是一个通用的容器格式。所谓容器格式就意味内部要有对应的数据流用来承载内容。而且既然是一个视频那必然有音轨和视轨而音轨、视轨本身也有对应的格式。常见的音轨、视轨格式包括
视轨H.265(HEVC)、H.264。其中目前大部分Android手机都支持H.264格式的直接硬件编码和解码对于H.265来说Android 5.0以上的机器就支持直接硬件解码了但是对于硬件编码目前只有一部分高端芯片可以支持例如高通的8xx系列、华为的98x系列。对于视轨编码来说分辨率越大性能消耗也就越大编码所需的时间就越长。
音轨AAC。这是一种历史悠久音频编码格式Android手机基本可以直接硬件编解码几乎很少遇到兼容性问题。可以说作为视频的音轨格式AAC已经非常成熟了。
对于编码本身上面提到的这些格式都是有损编码因此压缩编码本身还需要一个衡量压缩之后数据量多少的指标这个标准就是码率。同一个压缩格式下码率越高质量也就越好。更多Android本身支持的编解码格式你可以参考官方文档。
小结一下要拍摄一个MP4视频我们需要将视轨 + 音轨分别编码然后作为MP4的数据流再合成出一个MP4文件。
2. 音视频编码的流程
接下来我们再来看看一个视频是怎么拍摄出来的。首先既然是拍摄少不了跟摄像头、麦克风打交道。从流程来说以H.264/AAC编码为例录制视频的总体流程是
我们分别从摄像头/录音设备采集数据,将数据送入编码器,分别编码出视轨/音轨之后再送入合成器MediaRemuxer或者类似mp4v2、FFmpeg之类的处理库最终输出MP4文件。接下来我主要以视轨为例来介绍下编码的流程。
首先直接使用系统的MediaRecorder录制整个视频这是最简单的方法直接就能输出MP4文件。但是这个接口可定制化很差比如我们想录制一个正方形的视频除非摄像头本身支持宽高一致的分辨率否则只能后期处理或者各种Hack。另外在实际App中除非对视频要求不是特别高一般也不会直接使用MediaRecorder。
视轨的处理是录制视频中相对比较复杂的部分输入源头是Camera的数据最终输出是编码的H.264/H.265数据。下面我来介绍两种处理模型。
第一种方法是利用Camera获取摄像头输出的原始数据接口例如onPreviewFrame经过预处理例如缩放、裁剪之后送入编码器输出H.264/H.265。
摄像头输出的原始数据格式为NV21这是YUV颜色格式的一种。区别于RGB颜色YUV数据格式占用空间更少在视频编码领域使用十分广泛。
一般来说因为摄像头直接输出的NV21格式大小跟最终视频不一定匹配而且编码器往往也要求输入另外一种YUV格式一般来说是YUV420P因此在获取到NV21颜色格式之后还需要进行各种缩放、裁剪之类的操作一般会使用FFmpeg、libyuv这样的库处理YUV数据。
最后会将数据送入到编码器。在视频编码器的选择上我们可以直接选择系统的MediaCodec利用手机本身的硬件编码能力。但如果对最终输出的视频大小要求比较严格的话使用的码率会偏低这种情况下大部分手机的硬件编码器输出的画质可能会比较差。另外一种常见的选择是利用x264来进行编码画质表现相对较好但是比起硬件编码器速度会慢很多因此在实际使用时最好根据场景进行选择。
除了直接处理摄像头原始数据以外还有一种常见的处理模型利用Surface作为编码器的输入源。
对于Android摄像头的预览需要设置一张Surface/SurfaceTexture来作为摄像头预览数据的输出而MediaCodec在API 18+之后可以通过createInputSurface来创建一张Surface作为编码器的输入。这里所说的另外一种方式就是将摄像头预览Surface的内容输出到MediaCodec的InputSurface上。
而在编码器的选择上虽然InputSurface是通过MediaCodec来创建的乍看之下似乎只能通过MediaCodec来进行编码无法使用x264来编码但利用PreviewSurface我们可以创建一个OpenGL的上下文这样所有绘制的内容都可以通过glReadPixel来获取然后再讲读取数据转换成YUV再输入到x264即可另外如果是在GLES 3.0的环境我们还可以利用PBO来加速glReadPixles的速度
由于这里我们创建了一个OpenGL的上下文对于目前的视频类App来说还有各种各样的滤镜和美颜效果实际上都可以基于OpenGL来实现。
而至于这种方式录制视频具体实现代码你可以参考下grafika中示例。
视频处理
1. 视频编辑
在当下视频类App中你可以见到各种视频裁剪、视频编辑的功能例如
裁剪视频的一部分。
多个视频进行拼接。
对于视频裁剪、拼接来说Android直接提供了MediaExtractor的接口结合seek以及对应读取帧数据readSampleData的接口我们可以直接获取对应时间戳的帧的内容这样读取出来的是已经编码好的数据因此无需重新编码直接可以输入合成器再次合成为MP4。
我们只需要seek到需要裁剪原视频的时间戳然后一直读取sampleData送入MediaMuxer即可这是视频裁剪最简单的实现方式。
但在实践时会发现seekTo并不会对所有时间戳都生效。比如说一个4min左右的视频我们想要seek到大概2min左右的位置然后从这个位置读取数据但实际调用seekTo到2min这个位置之后再从MediaExtractor读取数据你会发现实际获取的数据上可能是从2min这里前面一点或者后面一点位置的内容。这是因为MediaExtractor这个接口只能seek到视频关键帧的位置而我们想要的位置并不一定有关键帧。这个问题还是要回到视频编码在视频编码时两个关键帧之间是有一定间隔距离的。
如上图所示关键帧被成为I帧可以被认为是一帧没有被压缩的画面解码的时候无需要依赖其他视频帧。但是在两个关键帧之间还存在这B帧、P帧这样的压缩帧需要依赖其他帧才能完整解码出一个画面。至于两个关键帧之间的间隔被称为一个GOP 在GOP内的帧MediaExtractor是无法直接seek到的因为这个类不负责解码只能seek到前后的关键帧。但如果GOP过大就会导致视频编辑非常不精准了实际上部分手机的ROM有改动实现的MediaExtractor也能精确seek
既然如此,那要实现精确裁剪也就只能去依赖解码器了。解码器本身能够解出所有帧的内容,在引入解帧之后,整个裁剪的流程就变成了下面的样子。
我们需要先seek到需要位置的前一I帧上然后送入解码器解码器解除一帧之后判断当前帧的PTS是否在需要的时间戳范围内如果是的话再将数据送入编码器重新编码再次得到H.264视轨数据然后合成MP4文件。
上面是基础的视频裁剪流程对于视频拼接也是类似得到多段H.264数据之后,才一同送入合成器。
另外在实际视频编辑中我们还会添加不少视频特效和滤镜。前面在视频拍摄的场景下我们利用Surface作为MediaCodec的输入源并且利用Surface创建了OpenGL的上下文。而MediaCodec作为解码器的时候也可以在configure的时候指定一张Surface作为其解码的输出。大部分视频特效都是可以通过OpenGL来实现的因此要实现视频特效一般的流程是下面这样的。
我们将解码之后的渲染交给OpenGL然后输出到编码器的InputSurface上来实现整套编码流程。
2. 视频播放
任何视频类App都会涉及视频播放从录制、剪辑再到播放构成完整的视频体验。对于要播放一个MP4文件最简单的方式莫过于直接使用系统的MediaPlayer只需要简单几行代码就能直接播放视频。对于本地视频播放来说这是最简单的实现方式但实际上我们可能会有更复杂的需求
需要播放的视频可能本身并不在本地,很多可能都是网络视频,有边下边播的需求。
播放的视频可能是作为视频编辑的一部分,在剪辑时需要实时预览视频特效。
对于第二种场景我们可以简单配置播放视频的View为一个GLSurfaceView有了OpenGL的环境我们就可以在这上实现各种特效、滤镜的效果了。而对于视频编辑常见的快进、倒放之类的播放配置MediaPlayer也有直接的接口可以设置。
更为常见的是第一种场景例如一个视频流界面大部分视频都是在线视频虽然MediaPlayer也能实现在线视频播放但实际使用下来会有两个问题
通过设置MediaPlayer视频URL方式下载下来的视频被放到了一个私有的位置App不容易直接访问这样会导致我们没法做视频预加载而且之前已经播放完、缓冲完的视频也不能重复利用原有缓冲内容。
同视频剪辑直接使用MediaExtractor返回的数据问题一样MediaPlayer同样无法精确seek只能seek到有关键帧的地方。
对于第一个问题我们可以通过视频URL代理下载的方式来解决通过本地使用Local HTTP Server的方式代理下载到一个指定的地方。现在开源社区已经有很成熟的项目实现例如AndroidVideoCache。
而对于第二个问题来说没法精确seek的问题在有些App上是致命的产品可能无法接受这样的体验。那同视频编辑一样我们只能直接基于MediaCodec来自行实现播放器这部分内容就比较复杂了。当然你也可以直接使用Google开源的ExoPlayer简单又快捷而且也能支持设置在线视频URL。
看似所有问题都有了解决方案,是不是就万事大吉了呢?
常见的网络边下边播视频的格式都是MP4但有些视频直接上传到服务器上的时候我们会发现无论是使用MediaPlayer还是ExoPlayer似乎都只能等待到整个视频都下载完才能开始播放没有达到边下边播的体验。这个问题的原因实际上是因为MP4的格式导致的具体来看是跟MP4格式中的moov有关。
MP4格式中有一个叫作moov的地方存储这当前MP4文件的元信息包括当前MP4文件的音轨视轨格式、视频长度、播放速率、视轨关键帧位置偏移量等重要信息MP4文件在线播放的时候需要moov中的信息才能解码音轨视轨。
而上述问题发生的原因在于当moov在MP4文件尾部的时候播放器没有足够的信息来进行解码因此视频变得需要直接下载完之后才能解码播放。因此要实现MP4文件的边下边播则需要将moov放到文件头部。目前来说业界已经有非常成熟的工具FFmpeg跟mp4v2都可以将一个MP4文件的moov提前放到文件头部。例如使用FFmpeg则是如下命令
ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4
使用-movflags faststart我们就可以把视频文件中的moov提前了。
另外如果想要检测一个MP4的moov是否在前面可以使用类似AtomicParsley的工具来检测。
在视频播放的实践中除了MP4格式来作为边下边播的格式以外还有更多的场景需要使用其他格式例如m3u8、FLV之类业界在客户端中常见的实现包括ijkplayer、ExoPlayer有兴趣的同学可以参考下它们的实现。
音视频开发的学习之路
音视频相关开发涉及面很广今天我也只是简单介绍一下其中基本的架构如果想继续深入这个领域发展从我个人学习的经历来看想要成为一名合格的开发者除了基础的Android开发知识以外还要深入学习我认为还需要掌握下面的技术栈。
语言
C/C++音视频开发经常需要跟底层代码打交道掌握C/C++是必须的技能。这方面资料很多,相信我们都能找到。
ARM NEON汇编这是一项进阶技能在视频编解码、各种帧处理低下时很多都是利用NEON汇编加速例如FFmpeg/libyuv底层都大量利用了NEON汇编来加速处理过程。虽说它不是必备技能但有兴趣也可以多多了解具体资料可以参考ARM社区的教程。
框架
FFmpeg可以说是业界最出名的音视频处理框架了几乎囊括音视频开发的所有流程可以说是必备技能。
libyuvGoogle开源的YUV帧处理库因为摄像头输出、编解码输入输出也是基于YUV格式所以也经常需要这个库来操作数据FFmpeg也有提供了这个库里面所有的功能在libswscale都可以找到类似的实现。不过这个库性能更好也是基于NEON汇编加速
libx264/libx265目前业界最为广泛使用的H.264/H.265软编解码库。移动平台上虽然可以使用硬编码但很多时候出于兼容性或画质的考虑因为不少低端的Android机器在低码率的场景下还是软编码的画质会更好最终可能还是得考虑使用软编解码。
OpenGL ES当今大部分视频特效、美颜算法的处理最终渲染都是基于GLES来实现的因此想要深入音视频的开发GLES是必备的知识。另外除了GLES以外Vulkan也是近几年开始发展起来的一个更高性能的图形API但目前来看使用还不是特别广泛。
ExoPlayer/ijkplayer一个完整的视频类App肯定会涉及视频播放的体验这两个库可以说是当下业界最为常用的视频播放器了支持众多格式、协议如果你想要深入学习视频播放处理它们几乎也算是必备技能。
从实际需求出发,基于上述技术栈,我们可以从下面两条路径来深入学习。
1. 视频相关特效开发
直播、小视频相关App目前越来越多几乎每个App相关的特效往往都是利用OpenGL本身来实现。对于一些简单的特效可以使用类似Color Look Up Table的技术通过修改素材配合Shader来查找颜色替换就能实现。如果要继续学习更加复杂的滤镜推荐你可以去shadertoy学习参考上面有非常多Shader的例子。
而美颜、美型相关的效果特别是美型需要利用人脸识别获取到关键点对人脸纹理进行三角划分然后再通过Shader中放大、偏移对应关键点纹理坐标来实现。如果想要深入视频特效类的开发我推荐可以多学习OpenGL相关的知识这里会涉及很多优化点。
2. 视频编码压缩算法
H.264/H.265都是非常成熟的视频编码标准,如何利用这些视频编码标准,在保证视频质量的前提下,将视频大小最小化,从而节省带宽,这就需要对视频编码标准本身要有非常深刻的理解。这可能是一个门槛相对较高的方向,我也尚处学习阶段,有兴趣的同学可以阅读相关编码标准的文档。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,132 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
39 移动开发新大陆: 边缘智能计算的趋势
你好我是张绍文。今天文章的作者黄振是算法领域的专家过去曾经和他合作过移动端的AI项目无论是他的算法水平还是工程上的能力都让我深感佩服。今天我非常幸运地请到他来给我们分享关于移动边缘智能计算的认识。
如果说过去几年移动开发是那头在风口上的猪现在这个风口明显已经转移到AI最明显的是国内应届生都纷纷涌向了AI方向。那作为移动开发我们如何在AI浪潮之下占据自己的一席之地
以我个人看法来看即使目前AI在移动端的落地场景有限但是AI这个“坑”我们是一定要跳的。我们可以尝试使用TensorFlow这些框架去做一些简单的Demo逐步加深对深度学习的理解。
Google的TensorFlow Lite、Facebook的Cafe2、腾讯的NCNN和FatherCNN、小米的MACE、百度的Paddle-Mobile各个公司都开源了自己的移动深度学习框架。移动端未来必然是深度学习一个非常重要的战场移动开发们需要发挥自己的平台特长。我们对于这些移动深度学习框架的优化应该更有发言权要做到既有算法思维也有更加强大的工程能力。就像之前黄振合作的项目中整个框架的性能优化也是由我们来主导使用了大量ARM NEON指令和汇编进行优化。
大家好我是黄振目前在一家大型互联网公司从事算法工作。我一直在关注边缘智能计算正好过去也开展过移动端机器学习的项目期间也接触了不少Android开发的同学。在一起合作期间我也在思考AI和Android开发可以相结合的点也看到团队中不少Android开发同学展现出的机器学习技术的开发能力。正好应绍文邀请在Android开发专栏和你分享一下我对移动端机器学习的认识希望对你有所帮助。
目前技术的发展有两个趋势。一个趋势是随着5G网络的发展物联网使“万物互联”成为可能。物联网时代也是云计算和边缘计算并存的世界且边缘计算将扮演重要的角色。边缘计算是与云计算相反的计算范式指的是在网络边缘节点终端设备进行数据处理和分析。边缘计算之所以重要原因在于首先在物联网时代将有数十亿的终端设备持续采集数据带来的计算量将是云计算所不能承受的其次终端设备的计算能力不断提高并不是所有的计算都需要在云端完成再有将数据传回云端计算再将结果返回终端的模式不可避免存在时延不仅影响用户体验有些场景也是不可接受的最后随着用户对数据安全和隐私的重视也会要求在终端进行数据加工和计算。
另一个趋势是,人工智能技术的迅速发展。最近几年,以深度学习为代表的人工智能技术取得了突破性的进展,不仅在社会上引起了人们的兴趣和关注,同时也正在重塑商业的格局。人工智能技术的重大价值在于,借助智能算法和技术,将之前必须人工才能完成的工作进行机器化,用机器的规模化解放人工,突破人力生产要素的瓶颈,从而大幅提高生产效率。在商业社会,“规模化”技术将释放巨大的能量、创造巨大的价值,甚至重塑商业的竞争,拓展整个商业的外部边界。
边缘计算既然有计算能力,在很多应用场景中就会产生智能计算的需求,两者结合在一起,就形成了边缘智能计算。
移动端机器学习的开发技术
目前,移动端机器学习的应用技术主要集中在图像处理、自然语言处理和语音处理。在具体的应用上,包括但不限于视频图像的物体检测、语言翻译、语音助手、美颜等,在自动驾驶、教育、医疗、智能家居和物联网等方面有巨大的应用需求。
1. 计算框架
因为深度学习算法具有很多特定的算法算子为了提高移动端开发和模型部署的效率各大厂都开发了移动端深度学习的计算框架例如谷歌的TensorFlow Lite、Facebook的Caffe2等Android开发的同学还是挺有必要去了解这些计算框架的。为了培养兴趣这方面的兴趣并获得感性的认识你可以挑选一个比较成熟的大厂框架例如TensorFlow Lite在手机开发一个物体检测的Demo练练手。
在实际项目中TensorFlow Lite和Caffe2等通常运行比较慢。在网上可以很容易找到各个计算框架的对比图。如果要真正入门的话在这里我推荐大家使用NCNN框架。NCNN是腾讯开源的计算框架优点比较明显代码结构清晰、文件不大而且运行效率高。强烈推荐有兴趣的Android开发同学把源码阅读几遍不仅有助于理解深度学习常用的算法也有助于理解移动端机器学习的计算框架。
阅读NCNN源码抓住三个最基础的数据结构Mat、Layer和Net。其中Mat用于存储矩阵的值神经网络中的每个输入、输出以及权重都是用Mat来存储的。Layer实际上表示操作所以每个Layer都必须有前向操作函数forward函数所有算子例如卷积操作convolution、LSTM操作等都是从Layer派生出来的。Net用来表示整个网络将所有数据节点和操作结合起来。
在阅读NCNN源码的同时建议大家也看些关于卷积神经网络算法的入门资料会有助于理解。
2. 计算性能优化
在实际项目中如果某个路径成为时间开销的瓶颈通常可以将该节点用NDK去实现。但在通常情况下移动端机器学习的计算框架已经是NDK实现的这时改进的方向是采用ARM NEON指令+汇编进行优化。
NEON是适用于ARM Cortex-A系列处理器的一种128Bit SIMDSingle Instruction, Multiple Data单指令、多数据扩展结构。ARM NEON指令之所有能达到性能优化的目的关键就在于单指令多数据。如下图所示
两个操作数各有128Bit各包含4个32Bit的同类型的寄存器通过如下所示的一条指令就可以实现4个32Bit数据的并行计算从而达到性能优化的效果。
VADDQ.S32 Q0,Q1,Q2
我们曾经用NDK的方式实现了深度学习算法中的PixelShuffle操作后来采用ARM NEON+汇编优化的方式计算的效率提升40倍效果还是很显著的。
如果还想进一步提高计算的性能可以采用Int 8量化的方法。在深度学习里面大多数运算都是基于Float 32类型进行的Float 32是32Bit128Bit的寄存器一次可以存储4个Float 32类型数据相比之下Int 8类型的数据可以存储16个。结合前面提到的单指令多数据如果采用Int 8类型的数据单条指令可以同时执行16个Int 8数据的运算从而大大提高了并行性。
但是将Float 32类型的数值量化到Int 8表达不可避免会影响到数据的精度而且量化的过程也是需要时间开销的这些都是需要注意的地方。关于量化的方法感兴趣的同学可以阅读这篇论文。
如果设备具有GPU还可以应用OpenCL进行GPU加速例如小米开源的移动端机器学习框架MACE。
移动端机器学习的算法技术
对于刚开始学习算法的同学,我一直主张不要把算法想象得太复杂,也不要想得太数学化,否则容易让人望而生畏。数学是思维逻辑的表达,我们希望用数学帮助我们理解算法。我们要能够达到对算法的直观理解,从直观上知道和理解为什么这个算法会有这样的效果,只有这样才算是真正掌握了算法。相反,对于一个算法,如果你只记住了数学推导,没形成直观的理解,是不能够灵活应用的。
1. 算法设计
深度学习的图像处理具有很广的应用,而且比较直观和有趣,建议你可以从深度学习的图像处理入手。深度学习的图像处理,最基本的知识是卷积神经网络,所以你可以先学习卷积神经网络。网上有很多关于卷积神经网络的介绍,这里就不赘述了。
理解卷积神经网络关键是理解卷积神经网络的学习机制,理解为什么能够学习。想要理解这一点,首先需要明确两个关键点“前向传播”和“反向传播”。整个神经网络在结构和激活函数确定之后,所谓“训练”或者“学习”的过程,其实就是在不断地调整神经网络的每个权重,让整个网络的计算结果趋近于期望值(目标值)。前向传播是从输入端开始计算,目标是得到网络的输出结果,再将输出结果和目标值相比较,得到结果误差。之后将结果误差沿着网络结构反向传播拆解到每个节点,得到每个节点的误差,然后根据每个节点的误差调整该节点的权重。“前向传播”的目的是得到输出结果,“反向传播”的目的是通过反向传播误差来调整权重。通过两者互相交替迭代,希望达到输出结果和目标值一致。
理解卷积神经网络之后我们可以动手实现手写字体识别的示例。掌握之后可以接着学习深度学习里的物体检测算法例如YOLO、Faster R-CNN等最后可以动手用TensorFlow都写一遍跑跑训练数据、调调参数边动手边理解。在学习过程中要特别留意算法模型的设计思想和解决思路。
2. 效果优化
效果优化指提升算法模型的准确率等指标,通常的方式有以下几种:
优化训练数据
优化算法设计
优化模型训练方式
优化训练数据
因为算法模型是从训练数据中学习的,模型无法学习到训练数据之外的模式。所以,在选择训练数据时要特别小心,必须使得训练数据包含实际场景中会出现的模式。精心挑选或标注训练数据,会有效提升模型的效果。训练数据的标注对效果非常重要,以至于有创业公司专门从事数据标注,还获得了不少融资。
优化算法设计
根据问题采用更好的算法模型采用深度学习模型而不是传统的机器学习模型采用具有更高特征表达能力的模型等例如使用残差网络或DenseNet提高网络的特征表达能力。
优化模型训练方式
优化模型训练方式包括采用哪种损失函数、是否使用正则项、是否使用Dropout结构、使用哪种梯度下降算法等。
3. 计算量优化
虽然我们在框架侧做了大量的工作来提高计算性能,但是如果在算法侧能够减少计算量,那么整体的计算实时性也会提高。从模型角度,减少计算量的思路有两种,一种是设计轻量型网络模型,一种是对模型进行压缩。
轻量型网络设计
学术界和工业界都设计了轻量型卷积神经网络在保证模型精度的前提下大幅减少模型的计算量从而减少模型的计算开销。这种思路的典型代表是谷歌提出的MobileNet从名字上也可以看出设计的目标是移动端使用的网络结构。MobileNet是将标准的卷积神经网络运算拆分成Depthwise卷积运算和Pointwise卷积运算先用Depthwise卷积对输入各个通道分别进行卷积运算然后用Pointwise卷积实现各个通道间信息的融合。
模型压缩
模型压缩包括结构稀疏化和蒸馏两种方式。
在逻辑回归算法中我们通过引入正则化使得某些特征的系数近似为0。在卷积神经网络中我们也希望通过引入正则化使得卷积核的系数近似为0。与普通的正则化不同的是在结构稀疏化中我们希望正则化实现结构性的稀疏比如某几个通道的卷积核的系数全部近似为0从而可以将这几个通道的卷积核剪枝掉减少不必要的计算开销。
蒸馏方法有迁移学习的意思,就是设计一个简单的网络,通过训练的方式,使得该简单的网络具有目标网络近似的表示能力,从而达到“蒸馏”的效果。
Android开发同学的机会
移动端机器学习的计算框架和算法,前者负责模型计算的性能,减少时间开销;后者主要负责模型的精度,还可以通过一些算法设计减少算法的计算量,从而达到减少时间开销的目的。
需要注意的是,在移动端机器学习中,算法模型的训练通常是在服务器端进行的。目前,终端设备通常不负责模型的训练。在使用时,由终端设备加载训练结果模型,执行前向计算得到模型的计算结果。
但是前面讲了那么多行业趋势和机器学习的基本技术那对于移动开发的同学来说如何进入这个“热门”的领域呢移动端机器学习是边缘智能计算范畴的一个领域而且移动端开发是Android开发同学特别熟悉的领域所以这也是Android开发同学的一个发展机会转型进入边缘智能计算领域。Android开发同学可以发挥自己的技术专业优势先在边缘计算的终端设备程序开发中站稳脚跟在未来的技术分工体系中有个坚固的立足点同时逐步学习深度学习算法以备将来往前迈一步进入边缘智能计算领域创造更高的技术价值。
可能在大部分情况下Android开发同学在深度学习算法领域跟专业的算法同学相比不具有竞争优势所以我们千万不要放弃自己所专长的终端设备的开发经验。对大多数Android开发同学而言“专精Android开发 + 懂深度学习算法”才是在未来技术分工中,创造最大价值的姿势。
对于学习路径我建议Android开发同学可以先学习卷积神经网络的基础知识结构、训练和前向计算然后阅读学习NCNN开源框架掌握计算性能的优化方法把开发技术掌握好。同时可以逐步学习算法技术主要学习各种常见的深度学习算法模型并重点学习近几年出现轻量型神经网络算法。总之Android开发同学要重点掌握提高计算实时性的开发技术和算法技术兼顾学习深度学习算法模型。
基于前面的描述我梳理了移动端机器学习的技术大图供你参考。图中红圈的部分是我建议Android开发同学重点掌握的内容。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,173 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
40 动态化实践,如何选择适合自己的方案?
在专栏第36期《跨平台开发的现状与应用》中我分享了H5、React Native/Weex、小程序这几种常见的跨平台开发方式。站在开发的角度虽然跨平台的开发效率要比Native开发更高但是这并不是大前端在国内盛行的最主要原因。
相比跨平台能力,国内对大前端的动态化能力更加偏执。这是为什么呢?移动互联网已经发展十年了,随着业务成熟和功能的相对稳定,整体重心开始偏向运营,强烈的运营需求对客户端架构和发布模式都提出了更高的要求。如果每个修改都需要经历开发、上线、版本覆盖等漫长的过程,根本无法达到快速响应的要求。
所以H5、React Native/Weex、小程序在国内的流行可以说是动态化能力远比跨平台能力重要。那我们应该选择哪一种动态化方式呢正如我在跨平台开发所说的目前这几种方案或多或少都还存在一些性能问题如果一定要使用Native开发方式又有哪些动态化方案今天我们一起来学习应该如何选择适合自己的动态化方案。
动态化实践的背景
前几天在朋友圈看到说淘宝iOS客户端上一个版本的更新已经是两个多月前的事情了。淘宝作为一个业务异常庞大且复杂的电商平台这样的发布节奏在过去是很难想象的。
而现在即使不通过发布新版本,我们也能实现各式各样的运营活动和个性化推荐。依赖客户端的动态化能力,我们不需要等待应用商店审核,也无须依赖用户的主动更新,产品在快速迭代的同时,也有着非常强大的试错能力。
1. 常见的动态化方案
移动端动态化方案在最近几年一直是大家关注的重点,虽然它已经发展了很多年,但是每年都会有新的变化,这里我们先来看看各大公司有哪些已知的动态化方案。
在2018年北京QCon大会上美团工程师分享了他们在动态化的实践《美团客户端动态化实践》。美团作为一个强运营的应用对动态化有非常强烈的诉求也有着非常丰富的实践经验他们将动态化方案分为下面四种类型。
Web容器增强。基于H5实现但是还有离线包等各种优化手段加持代表方案有PWA、腾讯的VasSonic、淘宝的zCache以及大部分的小程序方案。
虚拟运行环境。使用独立的虚拟机运行但最终使用原生控件渲染代表方案有React Native、Weex、快应用等。
业务插件化。基于Native的组件化开发这种方式在淘宝、支付宝、美团、滴滴、360等航母应用上十分常见。代表方案有阿里的Atlas、360的RePlugin、滴滴的VirtualAPK等。除此之外我认为各个热修复框架应该也属于业务插件化的一种类型例如微信的Tinker、美团的Robust、阿里的AndFix。
布局动态化。插件化或者热修复虽然可以做到页面布局和数据的动态修改但是代价巨大而且也不容易实现个性化运营。为了实现“千人千面”淘宝和美团的首页结构都可以通过动态配置更新。代表的方案有阿里的Tangram、Facebook的Yoga。
2. 动态化方案的选择
四大动态化方案哪家强,我们又应该如何选择?在回答这个问题之前,我们先来看看它们的差别。
目前我们还无法找到一种“十全十美”的动态化方案每种方案都有自己的优缺点和对应的使用场景。比如Web容器增强方案在动态化能力、开发效率上有着非常大的优势但稳定性和流畅度差强人意。恰恰相反布局动态化方案在性能上面有非常不错的表现但是在动态化能力和开发效率上面却受到不少限制。
所以说动态方案的选择,我们需要考虑下面这些因素。
业务类型。主要考虑业务的重要性、交互是否复杂、对性能的要求、是否长期迭代等因素。
团队技术栈和代码的历史包袱。在选择方案的时候也需要结合团队的技术栈现状以及代码的历史包袱综合考虑。以微信为例作为一个强交互的IM应用团队基本以Native开发为主而且微信基本没有太多运营上的需求所以当时在动态化方案上只使用了Tinker。当然团队的技术栈并不是永恒不变有了微信小程序之后内部的一些业务也尝试使用小程序来改造。
最终无论我们选择哪种动态化类型我都建议公司内部同一种动态化类型都使用同一个方案这样在统一技术栈的同时也可以实现代码在不同业务之间的迁移。比如阿里内部的虚拟运行环境统一使用Weex一个业务在手淘的效果不错也可以快速迁移到飞猪、天猫等其他应用中实现应用的流量矩阵。
同样对于运营活动也是如此阿里内部有一个叫PopLayer的组件它可以在任意Native页面这个页面甚至可以是Browser弹出H5的部署容器可以在无需发版的情况下对已有的Native界面上浮出透明浮层并且可以不影响Native页面本身的交互。这样做活动我们不需要在客户端提前一两个月开发代码而且同一个活动也可以快速在公司内部的各个应用中上线。
Native动态化方案
Web容器增强和虚拟运行环境方案通过独立的Runtime和JS-SDK来桥接Native模块而业务插件化则通过插件化框架和接口能力直接调用。相比之下前者更加抽象而不易造成代码混乱这也是目前各大公司逐渐开始“去插件化”的原因。
最近两年大前端开发越演越烈传统的Native动态化方案是否还存在价值它又该何去何从热修复、插件化这些方案的未来又将如何演进呢
1. 热修复和插件化
2016年在开源Tinker的时候有两件事情是超出我预料的一个是热修复在国内竟然有那么大的反响另外一个就是它竟然如此的“坑坑不息”。
从《Tinker技术的初心与坚持》一文中你可以看到过去我们踩过的一小部分坑但非常不幸的是填坑之路至今依然没有结束。每次Android新版本发布我们就像迎来期末考试一样步步惊心。
曾经微信希望使用Tinker来代替版本发布在热修复的基础上实现四大组件的代理。但是Android P私有API限制的出现基本打消了这个念头。热修复不能代替版本发布但是我们可以通过它来实现一些应用商店不支持的功能例如精准的灰度人数控制、渠道和用户属性选择、整包的A/B测试等。
另一方面热修复给国内的Android生态也带来一些不太好的影响比如增加用户ROM体积占用、App启动变慢15%、OTA首次卡顿等。特别是Android Q之后动态加载的Dex都只使用解释模式执行会加剧对启动性能的影响。因为性能的问题目前大公司基本暂停了全量用户的热修复只使用热修复用于灰度和测试。
热修复如此,插件化也是如此。笨重的插件化框架不仅影响应用的启动速度,而且多团队协作的时候并没有想象得那么和谐,接口混乱、仓库不好管理、编译速度慢这些问题都会存在。插件化回归模块化和组件化,这也是目前各大公司都在逐步推进的事情。
前一阵子,徐川在《移动开发的罗曼蒂克消亡史 》一文中回顾了热修复和插件化的前世今生。时间一转三年过去了,对于曾经参与这个浪潮的一份子来说,我可以做的只是顺应潮流的变化。
热修复的未来
Tinker设计之初参考了Instant Run的编译方案但是正如专栏第26期《关于编译你需要了解什么》中所说的Google在Android Studio 3.5之后对于Android 8.0以上的设备将会使用Apply Changes替代之前的Instant Run方案。
Apply Changes不再使用插入PathClassloader的方式而是使用我们已经多次讨论过的JVM TI。在Android 8.0之后JVM TI开始逐渐支持ClassTransform和ClassRedefine这两个接口它们可以允许虚拟机在运行时动态修改类实现运行时的动态字节码编织。事实上这个技术在JVM就已经非常成熟Java服务端利用这两个接口实现了类似热部署、远程调试、动态追踪等能力具体你可以参考《Java动态追踪技术探究》。
那热修复的未来将要走向何方本来我对热修复的未来是非常悲观的但是Android Q给了我一个很大的惊喜。我们知道Android P在中国有非常多的应用出现了兼容性问题其中大部分是热修复、插件化以及加固等原因造成的Google提供的数据是43%的兼容性问题由这三个问题造成)。
为了解决这个问题并且减少我们对私有API的调用Google在Android P新增了AppComponentFactory API并且在Android Q增加了替换Classloader的接口instantiateClassloader。在Android Q以后我们可以实现在运行时替换已经存在ClassLoader和四大组件。中国热修复的先驱们用自己的“牺牲”总算换来了Google官方的支持。我们使用Google官方API就可以实现热修复这样以后Android版本再升级也不用担惊受怕了。移动开发的罗曼蒂克并没有消亡Native的热修复再次迎来了春天。
插件化的未来
对于插件化的未来我们需要思考如何“回归官道”。Google在2018年推出了Android App Bundles它可以实现模块的动态下载但是与插件化不同的是它并不支持四大组件代理的能力。
但是Android App Bundles方案依赖Play Service在国内我们根本无法使用。爱奇艺的Qigsaw可能对我们有所启发它基于Android App Bundles实现支持动态更新但是不支持四大组件代理同时完全仿照AAB提供的Play Core Library接口加载插件如果有国际化需求的公司可以在国内版和国际版上无缝切换。这种方案不仅可以使用Google提供的编译工具链也支持国际国内双轨相当于Google为我们维护整个组件化框架在国内只需要实现自己的“Play Service”即可。
当然和热修复一样如果使用AppComponentFactory API我们也可以实现插件化的四大组件代理。但是具体实现上依然需要在AndroidManifest中预先注册四大组件然后具体的替换规则可以在我们自定义的AppComponentFactory实现类中埋好。
以Activity替换为例我们可以将某些类名的Activity替换成其他的Activity新的Activity可以在补丁中也可以在其他插件中。
热修复和插件化作为Native动态化方案它们有一定的局限性。随着移动技术的发展部分功能可能会被替换成小程序等其他动态化方案。但是从目前来看它们依然有非常大的存在价值和使用场景。
2. 布局动态化
正如上文所说像淘宝、美团首页这些场景我们对性能要求非常高这里只能使用Native实现。但是首页也是流量的聚集地“提增长、提留存、提转化”都要求我们有强大的运营能力。最近两年淘宝、天猫一直推行“千人千面”每个用户看到的主页布局、内容可能都不太一样。
布局动态化正是在这个背景之下应运而生,在我看来,布局动态化需要具备下面三个能力。
UI容器化。能够动态地新增、调整UI界面而无需发版。
能力接口化。点击、跳转等通用能力可以通过路由协议对外提供满足UI容器化后的调用需求。
数据通道化。数据上报也可以通过字段配置,实现客户端根据配置自动上报。
在具体的实践上天猫开源的Tangram是一个不错的选择。但是Tangram的整体方案会相对复杂一些我们也可以基于底层的VirtualView做二次开发。
总的来说布局动态化相比虚拟运行环境来说它不仅实现了UI的动态新增和修改也有着良好的体验和性能同时接入和学习成本也比较低。
总结
“路漫漫其修远兮吾将上下而求索”我们对动态化实践的探索一直没有停止。今年Flutter也强势地杀入了这个“战场”那Flutter在跨平台和动态化方面表现如何我们将在专栏下一期中揭晓。
动态化如今在国内是炙手可热的研究方向,虽然每个公司都强行造了自己的轮子,但是动态化方案目前还有很多没有解决的问题。所以在我们解决这些问题的过程中,也还会不断演变出其他的各种新方案。
现在各种类型的动态化方案目前都能找到自己的应用场景。移动技术在快速地发展我们无法准确预料到未来比如说在Android P我们正准备放弃热修复的时候Android Q又使它重新焕发了青春。但是我们坚信无论未来采用何种方案都是为了给用户更好的体验同时让业务可以更快地迭代并在不断地尝试中给用户提供更好的产品。
课后作业
对于动态化实践,你有什么看法?在你的应用中,使用了哪种动态化方式?欢迎留言跟我和其他同学一起讨论。
跨平台和动态化可以说是大前端时代最大的两个特点,也是每年技术大会的重点。今天的课后作业是仔细阅读下面大会的分享内容,学习各大公司在大前端的实践经验,并留言写写你的心得体会。
2018年北京QCon美团《美团客户端动态化实践》。
2018年GMTC闲鱼《基于Google+Flutter的移动端跨平台应用实践》。
2018年GMTC京东《当插件化遇上Android+P》。
2018年GMTC小米《快应用开发与实现指南》。
2018年GMTC绿色守护《Android+研发的昨天、今天与明天》。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,153 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
41 聊聊Flutter面对层出不穷的新技术该如何跟进
“天下苦秦久矣”不管是H5、React Native还是过去两年火热的小程序这些跨平台方案在性能和稳定性上总让我们诟病不已。最明显的例子是React Native已经发布几年了却一直还处在Beta阶段。
Flutter作为今年最火热的移动开发新技术从我们首次看到Beta测试版到2018年12月的1.0正式版总共才经过了9个多月。Flutter在保持原生性能的前提下实现了跨平台开发而且更是成为Google下一代操作系统Fuchsia的UI框架为移动技术的未来发展提供了非常大的想象空间。
高性能、跨平台而且更是作为Google下一个操作系统的重要部分Flutter已经有这么多光环加身那我们是否应该立刻投身这个浪潮之中呢新的技术、新的框架每一年都在不断涌现我们又应该如何跟进呢
Flutter的前世今生
大部分所谓的“新技术”最终都会被遗忘在历史的长河中面对新技术我们首先需要持怀疑态度在决定是否跟进之前你需要了解它的方方面面。下面我们就一起来看看Flutter的前世今生。
Flutter的早期开发者Eric Seidel曾经参加过一个访谈What is Flutter在这个访谈中他谈到了当初为什么开发Flutter以及Flutter的一些设计原则和方向。-
-
Eric Seidel和Flutter早期的几位开发人员都是来自Chrome团队他们在排版和渲染方面具有非常丰富的经验。那为什么要去开发Flutter一直以来他们都为浏览器的性能而感到沮丧有一天他们决定跳出Web的范畴在Chromium基础上通过删除大量的代码抛弃Web的兼容性竟然发现性能是之前的20倍。对此我也深有感触最近半年也一直在做主App的Lite版本安装包也从42MB降到8MB通过删除大量的历史业务性能比主包要好太多太多。
正如访谈中所说Flutter是一个Google内部孵化多年的产品从它开发之初到现在一直秉承着两个最重要的设计原则
性能至上。内置布局和渲染引擎使用Skia通过GPU做光栅化。选择Dart语言作为开发语言在发布正式版本时使用AOT编译不再需要通过解析器解释执行或者JIT。并且支持Tree Shaking无用代码删除减少发布包的体积。
效率至上。在开发阶段支持代码的Hot Reload实现秒级编译更新。重视开发工具链从开发、调试、测试、性能分析都有完善的工具。内置Runtime实现真正的跨平台一套代码可以同时生成Android/iOS应用降低开发成本。
那为什么要选择Dart语言“Dart团队办公室离Flutter团队最近”肯定是其中的一个原因但是Dart也为Flutter追求性能和效率的道路提供了大量的帮助比如AOT编译、Tree Shaking、Hot Reload、多生代无锁垃圾回收等。正因为这些特性Flutter在筛选了多种语言后最终选择了Dart。这里也推荐你阅读Why Flutter Uses Dart这篇文章。
总的来说Flutter内置了布局和渲染引擎使用Dart作为开发语言采用React方式编写UI支持一套代码在多端运行的框架。但是正如专栏前面所说的大前端时代的核心诉求是跨平台和动态化下面我们就一起来看看Flutter在这两方面的表现。
1. Flutter的跨平台开发
虽然React Native/Weex使用了系统原生UI组件通过原生渲染的方式来提升渲染速度和UI流畅度但是因为JS执行效率、JSBridge的通信代价等因素性能依然存在瓶颈而且我们也无法抹平不同系统的平台差异因此这样的跨平台方案注定艰难。
正如Eric Seidel在访谈所说Flutter是从浏览器引擎简化而来无论是它的布局引擎例如也是使用CSS Flexbox布局还是渲染流水线的设计都跟浏览器都有很多相似之处。但是它抛弃了浏览器沉重的历史包袱和Web的兼容性实现了在保持性能的前提下的跨平台开发。
回想一下在专栏第37期《工作三年半移动开发转型手游开发》中我们还描述过另外一套内置Runtime的跨平台方案Cocos引擎。
在我看来Cocos和Unity这些游戏引擎才是最早并且成熟的跨平台框架它们对性能的要求也更加苛刻。即使是“王者荣耀”和“吃鸡”这么复杂的游戏我们也可以做得非常流畅。
Flutter和游戏引擎一样也提供了一套自绘界面的UI Toolkit。游戏引擎虽然能实现跨平台开发但它致力于服务更有“钱途”的游戏开发。游戏引擎对于App开发特别是混合开发支持并不完善。
如下图所示我们可以看到三种跨平台方案的对比Flutter可以说是三者中最为轻量的。
2. Flutter的动态化实践
在专栏第40期《动态化实践如何选择适合自己的方案》中我提到了一个观点“相比跨平台能力国内对大前端的动态化能力更加偏执”。
在性能、跨平台、动态性这个“铁三角”中我们不能同时将三个都做到最优。如果Flutter在性能、跨平台和动态性都比浏览器更好那就不会出现Flutter这个框架了而是成为Chromium的一个跨时代版本。
浏览器选择的是跨平台和动态性而Flutter选择的就是性能和跨平台。Flutter正是牺牲了Web的动态性使用Dart语言的AOT编译使用只有5MB的轻量级引擎才能实现浏览器做不到的高性能。Flutter的第一波受众是Android上使用Java、KotliniOS上使用Objective-C、Swift的Native开发。未来Flutter是否能以此为突破口进一步蚕食Web领域的份额现在还不得而知。
那Flutter是否支持动态更新呢由于编译成AOT代码在iOS是绝对不可以动态更新的。对于AndroidFlutter的动态更新能力其实Tinker就已经实现了。从官方的提供方案来看其实也只是替换下面的变化文件可以说是Tinker的简化版。
官方的动态修复方案可以说是非常鸡肋的并且也是要求应用重启才能生效。如果同时修改了Native代码这也是不支持的。
更进一步说Flutter在Google Play上是否允许动态更新也是抱有疑问的。从Google Play上面的开发者政策中心规定上看在Google Play也是不允许动态更新可执行文件。
从《从Flutter的编译模式》一文中我们可以通过flutter build apk --build-shared-library将Dart代码编译成app.so。无论是libflutter.so还是app.so其实vm_*、isolate_*也是可执行文件的动态更新都违反了Google Play的政策。
当然不排除Google为了推广Flutter为它的动态更新开绿灯。最近我也在咨询Google Play的政策组目前还没有收到答复如果后续有进一步的结果我也可以同步给各位同学。
总的来说Flutter的动态化能力理论上只能通过JIT编译模式解决但是这会带来性能和代码体积的巨大影响。当然闲鱼也在探索一套Flutter的布局动态化方案你可以参考文章《Flutter动态化的方案对比及最佳实现》。
面对新技术,该如何选择
通过上面的学习我们总算对Flutter的方方面面都有所了解。可以说Flutter是一个性能和效率至上但是动态化能力非常有限的框架。
目前闲鱼App、美团外卖、今日头条App、爱奇艺开播助手、网易新闻客户端、京东JDFlutter、马蜂窝旅游App都分享过他们在使用Flutter的一些心得体会。如果有兴趣接入Flutter非常推荐你认真看看前人的经验和教训。
无论是Flutter还是其他新的技术在抉择是否跟进的时候我们需要考虑以下一些因素
收益。接入新的技术或者框架,给我们带来什么收益,例如稳定性、性能、效率、安全性等方面的提升。
迁移成本。如果想得到新技术带来的收益,需要我们付出什么代价,例如新技术的学习成本、原来架构的改造成本等。
成熟度。简单来说,就是这个新技术是否靠谱。跟选择开源项目一样,团队规模、能力是否达标、对项目是否重视都是我们需要考虑的因素。
社区氛围。主要是看跟进这个技术的人够不够多、文档资料是否丰富、遇到问题能否得到帮助等。
1. 对于Flutter我是怎么看的
Flutter是一个非常有前景的技术这一点是毋庸置疑的。我曾经专门做过一次全面的评估分析但是得出的结论是暂时不会在我负责的应用中接入主要原因如下。
目前我还没有跟进Flutter的核心原因在于收益不够巨大如果有足够大的收益其他几个因素都不是问题。而我负责的应用目前使用H5、小程序作为跨平台和动态化方案通过极致优化后性能基本可以符合要求。
从另外一方面来说,新技术的学习和引入,无论是对历史代码、架构,还是我们个人的知识体系,都是一次非常好的重构机会。我非常希望每过一段时间,可以引入一些新的东西,打破大家对现有架构的不满。
新的技术或多或少有很多不完善的地方这是挑战也是机会。通过克服一个又一个的困难和挑战并且在过程中不断地总结和沉淀我们最终可能还收获了团队成员技术和其他能力的成长。以闲鱼为例他们在Flutter落地的同时不仅将他们的经验总结成几十篇非常高质量的文章而且也参加了QCon、GMTC等一些技术大会同时开源了fish-redux、FlutterBoost等几个开源库Flutter也一下成为了闲鱼的技术品牌。
可以相信在过去一年闲鱼团队在共同攻坚Flutter一个又一个难题的过程中无论是团队的士气还是团队技术和非技术上的能力都会有非常大的进步。
2. 对于Flutter大家又是怎么看的
由于我还并没有在实际项目中使用Flutter所以在写今天的文章之前我也请教了很多有实际应用经验的朋友下面我们一起来看看他们对Flutter又是怎么看的。
总结
曾几何时我们一直对Chromium庞大的代码无从入手。而Flutter是一个完整而且比WebKit简单很多的引擎它内部涉及从CPU到GPU从上层到硬件的一系列知识源码中有非常多值得我们挖掘去学习和研究的地方。
未来Flutter应用在小程序中也是一个非常有趣的课题我们可以直接使用或者参考Flutter实现一个小程序渲染引擎。这样还可以带来另外一个好处一个功能例如微信的“附近的餐厅”在不成熟的时候可以先以小程序的方式尝试等到这个功能稳定之后我们又可以无成本地转化为应用内的代码。
Dart语言从2011年启动以来一直想以高性能为卖点试图取代JavaScript但是长期以来在Google外部使用得并不多。那在Flutter这个契机下它未来是否可以实现弯道超车呢这件事给我最大的感触是机会可能随时会出现但是需要我们时刻准备好。
“打铁还需自身硬”我们还是要坚持修炼内功。对于是否要学习Flutter我的答案是“多说无益实践至上”。
课后作业
对于Flutter你有什么看法你是否准备在你的应用中跟进欢迎留言跟我和其他同学一起讨论。
Flutter作为今年最为火热的技术里面有非常多的机遇可以帮我们打造自己的技术品牌例如撰写文章、参加技术会议、开源你的项目等。对于Flutter的学习你可以参考下面的一些资料。
万物之中,源码最美
Flutter官方文档
闲鱼的Flutter相关文章
各大应用的使用总结闲鱼App、美团外卖、今日头条App、爱奇艺开播助手、网易新闻客户端、京东JDFlutter、马蜂窝旅游App
阿里Flutter开发者帮助Appflutter-go
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,79 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
42 Android开发高手课学习心得
你好,我是张绍文。专栏更新至今,转眼间最后一个模块“架构演进”也已经 更新完了。从“Android开发高手课”筹备到现在将近的八个月里非常感谢我们的学习委员孙鹏飞不管是文章答疑还是练习Sample鹏飞为这个专栏付出了太多太多这里再次衷心地跟他说一声谢谢。
无论是我还是鹏飞在写这个专栏的同时也是重塑自身知识架构的过程。在专栏的最后一篇文章我特意邀请鹏飞跟大家分享他的学习心得也算是跟大家做一个小小的告别。鹏飞对于Android系统框架、虚拟机、Linux等底层知识非常熟悉那他是如何做到的呢不得不说的是鹏飞是一个非常自律+宅的人拥有坚持每天早上7点钟起床学习一个半小时的可怕技能。
最近鹏飞也准备奔向新的工作岗位,在此也祝愿他在新岗位上能够发挥所长。接下来我们一起来听听鹏飞学习专栏的心得和思考。
大家好我是孙鹏飞。“Android开发高手课”接近尾声今天我来从一个学习者的角度对专栏做一下总结。专栏涵盖了Android开发的方方面面的知识有技术干货、心得体会、问题答疑和部分科普性质的文章从内容上来说和平时大家看到的教程不太一样没有过多介绍Android基础更多的是提供新的思路和分享经验。很多同学感觉学习比较吃力其实我也是如此在开始准备这门课程的时候很多东西也是从零开始学起。对于一个新的概念从模糊到清晰的过程不会那么容易它可能涉及Android和Linux很多细枝末节的知识点需要阅读大量资料给予支撑才能理解。
下面我就总结一下我学习专栏的过程和一些思考,希望对你有所启发。
关于“高手课”的思考
最近我了解到一些同学对C++和Linux感觉不知道如何入手这里说一下我的学习过程以及相关的学习资料。你可以跟着《C++ Primier Plus》学习一下基本语法和标准库里的函数使用比如字符串操作、内存分配、I/O操作等这里有一个很好的网站学习C/C++语言,包括最新的特性都有介绍。
Linux应用开发的部分可以一步步的参考《UNIX环境高级编程》这本书熟悉一遍标准I/O库、系统数据文件和信息、进程环境、进程控制、进程关系、信号、线程、线程控制、守护进程以及各种I/O、进程间通信、网络IPC的使用。同时也可以参考《Linux/UNIX系统编程手册》这本书相对介绍得更详细一些。在了解了Linux应用开发相关内容后可以深入了解一下Linux内核相关的知识我推荐《Linux技术内幕》这本书是国人写的语言相对于一些翻译书要通顺一些而且还有大量的图解缺点是由于篇幅限制有一部分内容没有介绍。
从专栏中你可以看到有些内容涉及ARM的汇编ARM的汇编资料相对少一些而且实践起来不是很方便这里推荐一个网站Writing ARM Assembly可以学习到基本的汇编操作。关于实践可以使用VisUAL工具来进行实验这个工具可以逐步执行并可以实时观察寄存器和内存里的值。关于汇编相关的内容我认为更多的时候只需要看懂即可在实际中使用的地方可能并不是很多。
Android的更新迭代是很快的在快速的更新中我们需要保持对Android新特性的了解还要掌握兼容问题的处理方法。现在官方文档更新的速度还是很快的中文文档更新速度也很快大部分功能和API新特性的描述都可以在Android官网上找到很详细的文档比如Android Q功能和API 并且每个版本官方都会提供很详细的API差异报告这个报告将修改、新增的API都统计在一起我们可以在这个表里发现一些新功能。
关于虚拟机方面的更新我更多是参考官方的提交记录来查看。具体可以看Android Gerrit这里有每一条提交记录并且可以查看很详细的Code Diff提交记录的描述一般都很清晰所以可以很好的帮助我们理解代码也可以发现一些新的工具和功能。
专栏学习心得
我经常听到很多同学说一个技术很难其实我觉得大多时候不是技术难而是应该说复杂。技术大多都是演进而来其实这种演进和我们业务需求迭代的过程很类似。比如内存管理从固定分区、动态分区机制到分段机制然后发展成现在的分页机制硬件也随之演进都算是一个迭代过程。理解分页机制其实不难但这个知识点所涉及的内容就很繁琐和复杂了比如了解系统如何管理内存需要了解内存布局就会涉及内存划分内存划分又涉及CPU的运行模式然后是物理内存和虚拟内存如何映射会涉及页表的翻译、物理页面的分配和释放、伙伴系统算法为了解决伙伴系统的内存碎片化问题又衍生出迁移类型等。
这一系列涉及的内容就非常复杂,但每一项单拆出来去看,一层层去学习和补充,就会感觉容易很多。这一点其实在业务开发上也有体现,我们刚接手一个复杂的业务,代码庞大,注释和文档都很少,但在一段时间后你还是会对整个业务有或多或少的认识,在接到新的业务的时候也没有觉得难到无从下手,顶多是觉得复杂。底层的系统、框架也是如此,这是一个由点到面的过程。
忙碌状态下的学习
鸿洋写过一篇文章《嗷嗷加班,如何保持学习能力》,讲在繁忙的工作状态下如何保持学习,我看过之后也很有感触。在平时零散的时间里我们看到一篇技术文章,并不是阅读收藏后就结束了,这样你可能会在很短的时间里就忘掉了文章的内容。他将阅读一篇文章分成以下几个步骤:提取这篇文章要解决的问题;然后概括一下涉及的技术点;提取重点内容,比如问题发生的缘由、有哪几种解决方法。总体来说,这个方法是为了在短时间内提取出重点内容,然后记录下来后面再进行复习。所以我们都需要多记录、多复习,可以培养使用一些工具来帮助自己养成习惯。
逃离舒适区
并不是说换一个更忙碌的工作就是逃离舒适区,而是在平时工作和学习过程中保持一种焦虑感,但这种焦虑感不是迷茫和恐慌,而是清晰地认识到自己的不足,然后在工作和业余的时间里填补自己,当你集中注意力攻克一个难点的时候,你会发现这是一件有趣的事。我身边有很多同学都在持续地学习,每个人都有不同的目标,比如学到什么程度、应用到什么地方等,需要一定的压力才能产生比较好的效果,否则很容易迷失丢掉重点。我以前所在的团队有一个学习计划表,每个同学调研一个方向,每周周会的时候都会抽出一定时间去做技术分享,我觉得这是一个很好的方法,人在有一定紧张情绪的情况下注意力会相当集中,这个和考试前、面试前学习效率会很高的道理是一样的。不过我们也不能太过焦虑,我也经常会有焦虑感,我的解决方法是定制计划,半年或者一年,在一段时间专注完成一件事,你会看到自己成长了很多。
刻意练习
也许我们在公司里平时做的业务需求都是缝缝补补,并没有涉及很复杂的内容,大量重复的工作会让人觉得无法提升自己的技术。而平时自己学到的知识在一定时间之后可能就生疏了,而且有些技术可能从原理上看相对简单,但在实现的过程中会遇到各种问题,比如插件化和热修复,这样的技术如果不上手去实践就只能停留在理论层面。对于这种情况,可以采用一种刻意练习的方法,在知晓原理后自己尝试去实现一个类似的框架,在这个过程中你会得到比阅读源码和文章更多的实践经验,可以大大加深对一个知识的理解,也可以锻炼自己的框架设计能力。
阅读源码
别人写的源码分析文章我一般看得比较少除非是自己遇到了很难理解的部分大部分的内容都可以直接在代码里获取到。而且很多源码的分析文章就很少比如Inline Hook ,但是框架实现比较多,那么就只能从代码里获取相关内容。针对一个功能的框架可以去找一些相同功能的多个框架进行对比,看一下实现方式是否有不同之处,同时比较一下每个实现方式的优缺点,并记录一下每个框架所使用的技术。在理解一个框架的实现后,还是建议你去自己写一个类似的框架,因为在自己的实现过程中会遇到各种问题,可以从其他框架里寻找一下方案,然后自己总结一下,这样可以了解这个框架的优势之处,如果在面试的时候被问到一个框架的优缺点,这样也可以有自己的理解,而不是网上帖子的统一描述。
全栈发展
在所谓的互联网寒冬下需要持续关注其他方向的技术提升自身竞争力。在前端的趋势热度和各个公司的发展方向中“大前端”已经成为必要的技能这也可以从各大公司的招聘方向中看出来。因此我们在平时学习过程中可以更多关注一下大前端、跨平台、Flutter相关的内容。现在很多公司和部门讲究中台化和前后端闭环管理对于后台和前端的技术都要大概有所了解才能掌握整个业务的动态只关注一块技术在中国的互联网环境下不太利于自身的发展。换句话讲我们关注得越多机会也就更多一些。
个人能力的提升
这里的能力更多是指软实力的提升,一个是技术视野,也就是一个业务系统的全局把控,将一个自底向上的思维方式发展为从上到下的抽象能力;再有就是以技术价值为导向。以前我总是深入一些技术细节,只是觉得比较有趣,但很少考虑这个技术点能带来什么“价值”。其实在工作晋升和面试的过程中,通常关注更多的是“价值”,我们一般总说业务迭代、模块开发,但很少谈及所做功能的价值,只是觉得技术实现比较简单并没有什么可以讲的,其实我们可以从以下几个方面进行总结。
首先,我们开发了一个新功能、做了一些改进、引入了一些技术等,可能我们大多在做这些工作,也就是实现了一个业务需求,保证了业务功能的使用,这是功能产生的价值。在做这个需求功能的时候,我们有没有考虑过扩展性、重用性、维护性、性能、稳定性、高可用性等呢?性能的提升给用户带来体验上的价值;可扩展性、重用性带来开发效率的提升;稳定性减少了维护的成本等,这些都是技术产出的价值。我们可以更进一步从业务的角度上看,比如完成这个业务给用户体验提升了多少?促进了多少用户增长、提高了多少用户活跃度、降低了多少成本?由于我们在每个业务开发的时候,都会有一些数据统计的埋点,因此在平时的时候我们要多关注这种业务相关的数据。一般产品同学都会有各种数据报表,我们可以将他们总结起来作为自己完成一个业务所产出的价值。
写在最后
最后感谢各位同学能一直跟随“Android开发高手课”学习到最后相信你一定也从专栏里学到了对自己有价值的新知识。我同样也是从专栏上线随着专栏更新一点点学习到现在从专栏里学到了很多思路和方法也巩固了很多基础知识。但更多的基础知识专栏无法详细呈现所以还需要我们以此为起点自己在课下扩展开来多去思考、多做总结。
最最后我想说,每个人在突破自己技术瓶颈时都会经历一段痛苦的时光,只有我们具有坚定的信念,并努力坚持下去,相信我等你回过头来再看曾经认为难以理解的技术和知识时,你会有一种阔然开朗、融会贯通的感觉,这就是成长和进步所带来最大的成就感。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@ -0,0 +1,369 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
Android JVM TI机制详解内含福利彩蛋
你好,我是孙鹏飞。
在专栏卡顿优化的分析中绍文提到可以利用JVM TI机制获得更加非常丰富的顿现场信息包括内存申请、线程创建、类加载、GC信息等。
JVM TI机制究竟是什么它为什么如此的强大怎么样将它应用到我们的工作中今天我们一起来解开它神秘的面纱。
JVM TI介绍
JVM TI全名是Java Virtual Machine Tool Interface是开发虚拟机监控工具使用的编程接口它可以监控JVM内部事件的执行也可以控制JVM的某些行为可以实现调试、监控、线程分析、覆盖率分析工具等。
JVM TI属于Java Platform Debugger Architecture中的一员在Debugger Architecture上JVM TI可以算作一个back-end通过JDWP和front-end JDI去做交互。需要注意的是Android内的JDWP并不是基于JVM TI开发的。
从Java SE 5开始Java平台调试体系就使用JVM TI替代了之前的JVMPI和JVMDI。如果你对这部分背景还不熟悉强烈推荐先阅读下面这几篇文章
深入 Java 调试体系:第 1 部分JPDA 体系概览
深入 Java 调试体系:第 2 部分JVMTI 和 Agent 实现
深入 Java 调试体系:第 3 部分JDWP 协议及实现
深入 Java 调试体系:第 4 部分Java 调试接口JDI
虽然Java已经使用了JVM TI很多年但从源码上看在Android 8.0才集成了JVM TI v1.2主要是需要在Runtime中支持修改内存中的Dex和监控全局的事件。有了JVM TI的支持我们可以实现很多调试工具没有实现的功能或者定制我们自己的Debug工具来获取我们关心的数据。
现阶段已经有工具使用JVM TI技术比如Android Studio的Profilo工具和Linkedin的dexmaker-mockito-inline工具。Android Studio使用JVM TI机制实现了实时的内存监控对象分配切片、GC事件、Memory Alloc Diff功能非常强大dexmaker使用该机制实现Mock final methods和static methods。
1. JVM TI支持的功能
在介绍JVM TI的实现原理之前我们先来看一下JVM TI提供了什么功能我们可以利用这些功能做些什么
线程相关事件 -> 监控线程创建堆栈、锁信息
ThreadStart :线程在执行方法前产生线程启动事件。
ThreadEnd线程结束事件。
MonitorWaitwait方法调用后。
MonitorWaitedwait方法完成等待。
MonitorContendedEnter当线程试图获取一个已经被其他线程持有的对象锁时。
MonitorContendedEntered当线程获取到对象锁继续执行时。
类加载准备事件 -> 监控类加载
ClassFileLoadHook在类加载之前触发。
ClassLoad某个类首次被加载。
ClassPrepare某个类的准备阶段完成。
异常事件 -> 监控异常信息
Exception有异常抛出的时候。
ExceptionCatch当捕获到一个异常时候。
调试相关
SingleStep步进事件可以实现相当细粒度的字节码执行序列这个功能可以探查多线程下的字节码执行序列。
Breakpoint当线程执行到一个带断点的位置断点可以通过JVMTI SetBreakpoint方法来设置。
方法执行
FramePop当方法执行到retrun指令或者出现异常时候产生手动调用NofityFramePop JVM TI函数也可产生该事件。
MethodEntry当开始执行一个Java方法的时候。
MethodExit当方法执行完成后产生异常退出时。
FieldAccess当访问了设置了观察点的属性时产生事件观察点使用SetFieldAccessWatch函数设置。
FieldModification当设置了观察点的属性值被修改后观察点使用SetFieldModificationWatch设置。
GC -> 监控GC事件与时间
GarbageCollectionStartGC启动时。
GarbageCollectionFinishGC结束后。
对象事件 -> 监控内存分配
ObjectFreeGC释放一个对象时。
VMObjectAlloc虚拟机分配一个对象的时候。
其他
NativeMethodBind在首次调用本地方法时或者调用JNI RegisterNatives的时候产生该事件通过该回调可以将一个JNI调用切换到指定的方法上。
通过上面的事件描述可以大概了解到JVM TI支持什么功能详细的回调函数参数可以从JVM TI规范文档里获取到我们可以通过这些功能实们定制的性能监控、数据采集、行为修改等工具。
2. JVM TI实现原理
JVM TI Agent的启动需要虚拟机的支持我们的Agent和虚拟机运行在同一个进程中虚拟机通过dlopen打开我们的Agent动态链接库然后通过Agent_OnAttach方法来调用我们定义的初始化逻辑。
JVM TI的原理其实很简单以VmObjectAlloc事件为例当我们通过SetEventNotificationMode函数设置JVMTI_EVENT_VM_OBJECT_ALLOC回调的时候最终会调用到art::Runtime::Current() -> GetHeap() -> SetAllocationListener(listener);
在这个方法中listener是JVM TI实现的一个虚拟机提供的art::gc::AllocationListener回调当虚拟机分配对象内存的时候会调用该回调源码可见heap-inl.h#194同时在该回调函数里也会调用我们之前设置的callback方法这样事件和相关的数据就会透传到我们的Agent里来实现完成事件的监听。
类似atrace和StrictModeJVM TI的每个事件都需要在源码中埋点支持。感兴趣的同学可以挑选一些事件在源码中进一步跟踪。
JVM TI Agent开发
JVM TI Agent程序使用C/C++语言开发也可以使用其他支持C语言调用语言开发比如Rust。
JVM TI所涉及的常量、函数、事件、数据类型都定义在jvmti.h文件中我们需要下载该文件到项目中引用使用你可以从Android项目里下载它的头文件。
JVM TI Agent的产出是一个so文件在Android里通过系统提供的Debug.attachJvmtiAgent方法来启动一个JVM TI Agent程序。
static fun attachJvmtiAgent(library: String, options: String?, classLoader: ClassLoader?): Unit
library是so文件的绝对地址。需要注意的是API Level为28而且需要应用开启了android:debuggable才可以使用不过我们可以通过强制开启debug来在release版里启动JVM TI功能。
Android下的JVM TI Agent在被虚拟机加载后会及时调用Agent_OnAttach方法这个方法可以当作是Agent程序的main函数所以我们需要在程序里实现下面的函数。
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options,void *reserved)
你可以在这个方法里进行初始化操作。
通过JavaVM::GetEnv函数拿到jvmtiEnv*环境指针Environment Pointer通过该指针可以访问JVM TI提供的函数。
jvmtiEnv *jvmti_env;jint result = vm->GetEnv((void **) &jvmti_env, JVMTI_VERSION_1_2);
通过AddCapabilities函数来开启需要的功能也可以通过下面的方法开启所有的功能不过开启所有的功能对虚拟机的性能有所影响。
void SetAllCapabilities(jvmtiEnv *jvmti) {
jvmtiCapabilities caps;
jvmtiError error;
error = jvmti->GetPotentialCapabilities(&caps);
error = jvmti->AddCapabilities(&caps);
}
GetPotentialCapabilities函数可以获取当前环境支持的功能集合通过jvmtiCapabilities结构体返回该结构体里标明了支持的所有功能可以通过jvmti.h来查看大概内容如下。
typedef struct {
unsigned int can_tag_objects : 1;
unsigned int can_generate_field_modification_events : 1;
unsigned int can_generate_field_access_events : 1;
unsigned int can_get_bytecodes : 1;
unsigned int can_get_synthetic_attribute : 1;
unsigned int can_get_owned_monitor_info : 1;
......
} jvmtiCapabilities;
然后通过AddCapabilities方法来启动需要的功能如果需要单独添加功能则可以通过如下方法。
jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_tag_objects = 1;
到此JVM TI的初始化操作就已经完成了。
所有的函数和数据结构类型说明可以在这里找到。下面我来介绍一些常用的功能和函数。
1. JVM TI事件监控
JVM TI的一大功能就是可以收到虚拟机执行时候的各种事件通知。
首先通过SetEventCallbacks方法来设置目标事件的回调函数如果callbacks传入nullptr则清除掉所有的回调函数。
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.GarbageCollectionStart = &GCStartCallback;
callbacks.GarbageCollectionFinish = &GCFinishCallback;
int error = jvmti_env->SetEventCallbacks(&callbacks, sizeof(callbacks));
设置了回调函数后如果要收到目标事件的话需要通过SetEventNotificationMode这个函数有个需要注意的地方是event_thread如果参数event_thread参数为nullptr则会全局启用改目标事件回调否则只在指定的线程内生效比如很多时候对于一些事件我们只关心主线程。
jvmtiError SetEventNotificationMode(jvmtiEventMode mode,
jvmtiEvent event_type,
jthread event_thread,
...);
typedef enum {
JVMTI_ENABLE = 1,//开启
JVMTI_DISABLE = 0 .//关闭
} jvmtiEventMode;
以上面的GC事件为例上面设置了GC事件的回调函数如果想要在回调方法里接收到事件则需要使用SetEventNotificationMode开启事件需要说明的是SetEventNotificationMode和SetEventCallbacks方法调用没有先后顺序。
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_START, nullptr);
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_FINISH, nullptr);
通过上面的步骤就可以在虚拟机产生GC事件后在回调函数里获取到对应的函数了这个Sample需要注意的是在gc callback里禁止使用JNI和JVM TI函数因为虚拟机处于停止状态。
void GCStartCallback(jvmtiEnv *jvmti) {
LOGI("==========触发 GCStart=======");
}
void GCFinishCallback(jvmtiEnv *jvmti) {
LOGI("==========触发 GCFinish=======");
}
Sample效果如下。
com.dodola.jvmti I/jvmti: ==========触发 GCStart=======
com.dodola.jvmti I/jvmti: ==========触发 GCFinish=======
2. JVM TI字节码增强
JVM TI可以在虚拟机运行的状态下对字节码进行修改可以通过下面三种方式修改字节码。
Static在虚拟机加载Class文件之前对字节码修改。该方式一般不采用。
Load-Time在虚拟机加载某个Class时可以通过JVM TI回调拿到该类的字节码会触发ClassFileLoadHook回调函数该方法由于ClassLoader机制只会触发一次由于我们Attach Agent的时候经常是在虚拟机执行一段时间之后所以并不能修改已经加载的Class比如Object所以需要根据Class的加载时机选择该方法。
Dynamic对于已经载入的Class文件也可以通过JVM TI机制修改当系统调用函数RetransformClasses时会触发ClassFileLoadHook此时可以对字节码进行修改该方法最为实用。
传统的JVM操作的是Java BytecodeAndroid里的字节码操作的是Dalvik BytecodeDalvik Bytecode是寄存器实现的操作起来相对JavaBytecode来说要相对容易一些可以不用处理本地变量和操作数栈的交互。
使用这个功能需要开启JVM TI字节码增强功能。
jvmtiCapabilities.can_generate_all_class_hook_events=1 //开启 class hook 功能标记
jvmtiCapabilities.can_retransform_any_class=1 //开启对任意类进行 retransform 操作
然后注册ClassFileLoadHook事件回调。
jvmtiEventCallbacks callbacks;s
callbacks.ClassFileLoadHook = &ClassTransform;
这里说明一下ClassFileLoadHook的函数原型后面会讲解如何重新修改现有字节码。
static void ClassTransform(
jvmtiEnv *jvmti_env,//jvmtiEnv 环境指针
JNIEnv *env,//jniEnv 环境指针
jclass classBeingRedefined,//被重新定义的class 信息
jobject loader,//加载该 class 的 classloader如果该项为 nullptr 则说明是 BootClassLoader 加载的
const char *name,//目标类的限定名
jobject protectionDomain,//载入类的保护域
jint classDataLen,//class 字节码的长度
const unsigned char *classData,//class 字节码的数据
jint *newClassDataLen,//新的类数据的长度
unsigned char **newClassData) //新类的字节码数据
然后开启事件完整的初始化逻辑可参考Sample中的代码。
SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL)
下面以Sample代码作为示例来讲解如何在Activity类的onCreate方法中插入一行日志调用代码。
通过上面的步骤后就可以在虚拟机第一次加载类的时候和在调用RetransformClasses或者RedefineClasses时在ClassFileLoadHook回调方法里会接收到事件回调。我们目标类是Activity它在启动应用的时候就已经触发了类加载的过程由于这个Sample开启事件的时机很靠后所以此时并不会收到加载Activity类的事件回调所以需要调用RetransformClasses来触发事件回调这个方法用于对已经载入的类进行修改传入一个要修改类的Class数组和数组长度。
jvmtiError RetransformClasses(jint class_count, const jclass* classes)
调用该方法后会在ClassFileLoadHook设置的回调也就是上面的ClassTran sform方法中接收到回调在这个回调方法中我们通过字节码处理工具来修改原始类的字节码。
类的修改会触发虚拟机使用新的方法旧的方法将不再被调用如果有一个方法正在栈帧上则这个方法会继续运行旧的方法的字节码。RetransformClasses 的修改不会导致类的初始化,也就是不会重新调用 方法,类的静态变量的值和实例变量的值不会产生变化,但目标类的断点会失效。
处理类有一些限制我们可以改变方法的实现和属性但不能添加删除重命名方法不能改变方法签名、参数、修饰符不能改变类的继承关系如果产生上面的行为会导致修改失败。修改之后会触发类的校验而且如果虚拟机里有多个相同的Class 我们需要注意一下取到的Class需要是当前生效的Class按照ClassLoader加载机制也就是说优先使用提前加载的类。
Sample中实现的效果是在Activity.onCreate方法中增加一行日志输出。
修改前:
protected void onCreate(@Nullable Bundle savedInstanceState) {
.......
}
修改后:
protected void onCreate(@Nullable Bundle savedInstanceState) {
com.dodola.jvmtilib.JVMTIHelper.printEnter(this,"....");
....
}
我使用的Dalvik字节码修改库是Android系统源码里提供的一套修改框架dexter虽然使用起来十分灵活但比较繁琐也可以使用dexmaker框架来实现。本例还是使用dexter框架使用C++开发可以直接读取classdata然后进行操作可以类比到ASM框架。下面的代码是核心的操作代码完整的代码参考本期Sample。
ir::Type* stringT = b.GetType("Ljava/lang/String;");
ir::Type* jvmtiHelperT=b.GetType("Lcom/dodola/jvmtilib/JVMTIHelper;");
lir::Instruction *fi = *(c.instructions.begin());
VReg* v0 = c.Alloc<VReg>(0);
addInstr(c, fi, OP_CONST_STRING,
{v0, c.Alloc<String>(methodDesc, methodDesc->orig_index)});
addCall(b, c, fi, OP_INVOKE_STATIC, jvmtiHelperT, "printEnter", voidT, {stringT}, {0});
c.Assemble();
必须通过JVM TI函数Allocate为要修改的类数据分配内存将new_class_data指向修改后的类bytecode数组将new_class_data_len置为修改后的类bytecode数组的长度。若是不修改类文件则不设置new_class_data即可。若是加载了多个JVM TI Agent都启用了该事件则设置的new_class_data会成为下一个JVM TI Agent的class_data。
此时我们生成的onCreate方法里已经加上了我们添加的日志方法调用。开启新的Activity会使用新的类字节码执行同时会使用ClassLoader加载我们注入的com.dodola.jvmtilib.JVMTIHelper类。我在前面说过Activity是使用BootClassLoader进行加载的然而我们的类明显不在BootClassLoader里此时就会产生Crash。
java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available
所以需要想办法将JVMTIHelper类添加到BootClassLoader里这里可以使用JVM TI提供的AddToBootstrapClassLoaderSearch方法来添加Dex或者APK到Class搜索目录里。Sample里是将 getPackageCodePath添加进去就可以了。
总结
今天我主要讲解了JVM TI的概念和原理以及它可以实现的功能。通过JVM TI可以完成很多平时可能需要很多“黑科技”才可以获取到的数据比如Thread Park Start/Finish事件、获取一个锁的waiters等。
可能在Android圈里了解JVM TI的人不多对它的研究还没有非常深入。目前JVM TI的功能已经十分强大后续的Android版本也会进一步增加更多的功能支持这样它可以做的事情将会越来越多。我相信在未来它将会是本地自动化测试甚至是线上远程诊断的一大“杀器”。
在本期的Sample里我们提供了一些简单的用法你可以在这个基础之上完成扩展实现自己想要的功能。
相关资料
1.深入 Java 调试体系:第 1 部分JPDA 体系概览
2.深入 Java 调试体系:第 2 部分JVMTI 和 Agent 实现
3.深入 Java 调试体系:第 3 部分JDWP 协议及实现
4.深入 Java 调试体系:第 4 部分Java 调试接口JDI
5.JVM TI官方文档https://docs.oracle.com/javase/7/docs/platform/jvmti/jvmti.html
6.源码是最好的资料http://androidxref.com/9.0.0_r3/xref/art/openjdkjvmti/
福利彩蛋
根据专栏导读里我们约定的,我和绍文会选出一些认真提交作业完成练习的同学,送出一份“学习加油礼包”。专栏更新到现在,很多同学留下了自己的思考和总结,我们选出了@Owen@志伟@许圣明@小洁@SunnyBird,送出“极客时间周历”一份,希望更多同学可以加入到学习和讨论中来,与我们一起进步。
-
@Owen学习总结https://github.com/devzhan/Breakpad
@许圣明@小洁@SunnyBird 通过Pull Requests提交了练习作业https://github.com/AndroidAdvanceWithGeektime/Chapter04/pulls。
极客时间小助手会在24小时内与获奖用户取得联系注意查看短信哦

View File

@ -0,0 +1,53 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
Android工程师的“面试指南”
你好我是孙鹏飞。又到了传统的“金三银四”换工作的高峰期在互联网寒冬下抓住机会就显得尤为重要了。那作为Android工程师我们应该从哪些方面去准备呢例如不太熟悉的技能要不要写在简历上、要复习哪些Android组件的知识、刷算法题目有没有用可能在面试前你都会仔细考虑这些问题。下面我就结合自身的经验和理解帮你梳理一下关于简历、面试和算法方面需要准备的内容分享一些我的心得体会。
简历
简历在面试过程会起到至关重要的作用,我们需要非常注意简历的撰写。
在面试的过程中面试官通常会非常关注你简历中的工作经历、项目介绍、技能特长这三部分的内容如果你面试的公司没有固定题目的话那很多问题都会围绕你简历里这三部分内容去问。这里需要注意的一点是相关技能的书写首先你要让面试官明确你面试的定级是什么。很多时候一个职位对应了很多个职级在投简历的时候你的简历需要让面试官给你一个比较明确的定级否则面试过程会比较被动也会影响面试官对你的判断。因此这部分的内容需要突出自己的特长也要写一些现在公司相对关心的问题比如你对插件化、热修复、组件化、性能优化等很熟悉就可以明确的写上但如果不是很熟悉那么尽量不要去写。如果你对Android某部分内容很熟悉就可以写得相对详细一些比如你对Handler、Binder机制很熟悉就可以写“熟悉Android常见机制比如Handler、Binder机制等”。而看到你很熟悉这部分内容面试官可能在问问题时一层层深入因此你肯定需要提前准备一下这部分内容如何讲解基本可以从机制的优点、重点、难点三方面去说明。
关于项目介绍主要体现你在这个项目或者这个团队中的作用,突出自己的贡献和项目的难点。很多同学可能在公司一直做需求的开发,会觉得自己的项目经验没有亮点,难度也没有那么大,会觉得在这部分内容上比较吃亏。其实每个需求下来的时候你肯定会对这个需求有一个自己的设计,在这个过程中你会考虑如何对现有代码的影响最小,如何快捷清晰的实现功能,以及在开发过程中对组件、控件的封装,考虑如何优化性能,有没有新的技术可以帮助开发这个需求的…我想我们每个需求都是通过一系列的考量和设计后才实施的,你可以回去翻一下自己的代码,然后考虑一下如何把你的这些设计和思考体现在简历上,同样也是个不错的说明。
面试
对于Android工程师来说面试开始的时候都会问一些算法和Android、Java的基础知识。针对Java的基础知识我建议你看一下《码出高效Java 开发手册》《深入理解Java虚拟机》《Java并发编程的艺术》这三本书。对于Android的面试题大多都是跟系统原理有关的内容但也有很多没有准确答案的问题比如四大组件的原理这样的题目需要你从一个宏观的角度去解释一下四大组件或者你也可以拆分开一个个去讲解。
在面试前你需要提前准备一下避免回答问题的时候没有条理导致面试官对你的逻辑思维能力和语言表达能力产生不好的判断。一些Android经常使用到的组件一定要理解清楚比如Handler.postDelay的机制、触摸事件机制、自定义View、如何计算View大小、容器控件如何对子控件进行布局、数据库基本操作、Binder机制、LMK机制等。还有面试官也可能会问一些开源框架的原理我建议你也要多了解一些优秀的网络框架、图片加载框架、日志记录框架、EventBus、AAC框架的原理。对于相对复杂的插件化和热修复来说热修复可以去看一下《深入探索Android热修复》这本书插件化可以去看《Android插件化原理解析》这个系列的文章。还有性能优化最近几年公司对性能优化关注很多有的同学可能做过专门的性能优化或者自己开发过一些工具总结过一些方法论这样比较好答一些。但是大部分同学可能平时都在关注业务需求开发性能优化的实战可能并不是很多。我建议你可以从业务开发过程中找一些点来说比如在做一些公共的业务组件时需要在启动时初始化那么就需要注意初始化过程中的性能又或者在做一个列表页面的时候在复杂的列表View下如何保证滑动性能。相信你在平时开发过程中都会有自己的思考可以结合具体的情景讲出来。
面试的后面大多都会从项目入手,你需要在面试之前针对你的项目做详细的准备。比如面试官会让你介绍一下你的项目,你需要体现出这个项目的难点、你在项目中的贡献、项目的具体实现等,有可能还会问到一些具体的细节,所以建议是实事求是去讲,但一定要对自己的模块非常清晰。除了技术面试以外,有时还有可能会考察一些软技能,比如面试官会考察你跨部门协作能力、沟通能力、时间管理、任务分配和职业规划等。
面试更多的还是要靠平时的积累和临场的发挥做总结是很重要的因为很多内容不经常使用的话过一段时间之后就会忘掉这样就会出现原本自己做过的东西因为忘记了细节结果在面试过程中没法很好地展现出来。就比如插件化、热修复这样的技术其实原理相对来说比较简单但是在开发的过程中会遇到很多很多的坑如果没有一些关键点的文字记录过一段时间之后可能就忘记了某段代码是用于什么目的。所以在做完一次需求之后尽量多总结项目中的难点使用到的框架以及这个框架的原理以及其中花费时间最长的地方。另外Bug最多的地方也要做总结一下原因这样在面试前就不用把代码再翻一遍了解自己的项目细节就可以做到游刃有余了。
对于复习首先要对自己做一次自我了解我是通过画脑图来进行这个过程的我会整体默想一遍大概的知识体系画成类似下图。回想每个知识点可能考到的内容记录下自己模糊的地方然后去看网上同学们总结的面试题再对每个题目都做一下回答。这是一个迭代过程。在你预想的问题都可以回答上来的时候就需要深入挖掘一下技术细节和深度了比如我工作中开发了一个PLT Hook工具这个工具可能是我参考开源项目并封装修改过来的但对其中的细节并没有很了解这个时候你就要对这个开源项目所涉及的内容做一次系统学习了。
另一方面是需要在面试过程中提高面试官对自己的级别评价比如大部分人回答GC的问题都是按照《深入理解Java虚拟机》里的内容复述一遍这种回答基本也是可以的。不过毕竟Java虚拟机和Android虚拟机的GC还是有些差别的如果自己阅读过Android虚拟机GC相关资料或者自己分析过源码的话可以从Android虚拟机的角度解释GC比如Android虚拟机里MarkSweep算法的增量回收、并行回收等后台GC和前台GC、VisitRoot的执行过程、GC的触发方式、TLAB的处理、ConcurrentGC的原理、堆的Trim过程、内存碎片的解决、Reference的处理、finalize函数调用等展开讲。如果你对一个机制很熟悉的话可以把话题引到这上面去然后一层层对这个知识点深入讲解这样可以提升面试官对你的等级评价。
算法
算法是一定要复习的在很多面试的过程中都会穿插算法题。面试的算法题一般不会很难可以分为基础的数据结构比如数组、链表、栈、队列、二叉树、堆的使用这几种常见的数据结构的基础操作一定要很熟悉比如链表逆置、删除、获取第K个元素、判断是否有环等二叉树翻转、深度遍历、层级遍历、求树深度、公共父节点等。另一种是常见的搜索、排序算法这两类算法出现频率很高一定要知道它们常见的几种实现方式比如排序方式有冒泡、快排、插入、归并、堆排序等。注意这里一定不要简单地去记忆算法实现因为面试的时候可能不会直接让你写出对应的算法会出一些使用搜索或者排序算法来实现的题目这类题目你可以去LeetCode上通过标签过滤出来。
另一部分的算法题可能集中在贪心、动态规划、分治算法、深搜广搜等这一类的算法相对需要一些技巧性。但面试算法题通常不需要太多行代码就能完成一般都是在几十行内就能完成的所以你可以优先去找一些经典题目来做比如爬楼梯、最大子序和等。但也会有一些相对复杂的题目是几种算法结合在一起的比如二叉树的最大路径和就是深度搜索和动态规划一起使用的题目。除此之外也可能会遇到通过其他问题引申出的一些算法题目比如HashMap可能会引申出红黑树的实现等。这些都有可能需要准备虽然它可能不会成为你整个面试的一个绊脚石但有可能成为你获取一个高评价的筹码。
“临时抱佛脚”对于算法的学习和积累作用不是很大因此需要我们在平时繁忙的工作中抽出一些时间来复习你也可以去LeetCode、LintCode上刷刷题。另外虽然大部分面试的算法题目都是LeetCode上的简单题目但你同样也需要关注一些中等和困难难度的经典题目。
总结
今天我并没有涉及太多具体的面试题,更多侧重的是如何准备面试,而面试的准备其实是在我们平时工作过程中一点一滴积累的,复习只是作为一种在面试前巩固知识的手段。复习的过程主要是我们对知识点的整理和总结,你可以想一下在面试的时候可能会遇到的问题,以及该如何去表达。但是我想说,虽然“临时抱佛脚”的准备可能有时有用,但是在短时间内靠“突击”是很难理解到某个知识点更加深度层次的内容,而且知识面的广度也是需要时间和经验去积累的。所以不管你是否需要面试,在平时工作过程中都需要多思考、多训练、多总结,在有需要的时候可以厚积薄发。
最后,如果你也在准备面试,可以在留言区分享一下你的准备情况和面试心得。当然你也可以留下你遇到的面试问题,把它分享给其他同学。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。

View File

@ -0,0 +1,147 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
Native下如何获取调用栈
你好我是simsun曾在微信从事Android开发也是开源爱好者、Rust语言“铁粉”。应绍文邀请很高兴可以在“高手课”里和你分享一些编译方面的底层知识。
当我们在调试Native崩溃或者在做profiling的时候是十分依赖backtrace的高质量的backtrace可以大大减少我们修复崩溃的时间。但你是否了解系统是如何生成backtrace的呢今天我们就来探索一下backtrace背后的故事。
下面是一个常见的Native崩溃。通常崩溃本身并没有任何backtrace信息可以直接获得的就是当前寄存器的值但显然backtrace才是能够帮助我们修复Bug的关键。
pid: 4637, tid: 4637, name: crasher >>> crasher <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'some_file.c:123: some_function: assertion "false" failed'
r0 00000000 r1 0000121d r2 00000006 r3 00000008
r4 0000121d r5 0000121d r6 ffb44a1c r7 0000010c
r8 00000000 r9 00000000 r10 00000000 r11 00000000
ip ffb44c20 sp ffb44a08 lr eace2b0b pc eace2b16
backtrace:
#00 pc 0001cb16 /system/lib/libc.so (abort+57)
#01 pc 0001cd8f /system/lib/libc.so (__assert2+22)
#02 pc 00001531 /system/bin/crasher (do_action+764)
#03 pc 00002301 /system/bin/crasher (main+68)
#04 pc 0008a809 /system/lib/libc.so (__libc_init+48)
#05 pc 00001097 /system/bin/crasher (_start_main+38)
在阅读后面的内容之前你可以先给自己2分钟时间思考一下系统是如何生成backtrace的呢我们通常把生成backtrace的过程叫作unwindunwind看似和我们平时开发并没有什么关系但其实很多功能都是依赖unwind的举个例子比如你要绘制火焰图或者是在崩溃发生时得到backtrace都需要依赖unwind
书本中的unwind
1. 函数调用过程
如果你在大学时期修过汇编原理这门课程相信你会对下面的内容还有印象下图就是一个非常标准的函数调用的过程
首先假设我们处于函数main()并准备调用函数foo()调用方会按倒序压入参数此时第一个参数会在调用栈栈顶
调用invoke foo()伪指令压入当前寄存器EIP的值到栈然后载入函数foo()的地址到EIP
此时由于我们已经更改了EIP的值为foo()的地址相当于我们已经进入了函数foo()。在执行一个函数之前编译器都会给每个函数写一段序言prologue这里会压入旧的EBP值并赋予当前EBP和ESP新的值从而形成新的一个函数栈
下一步就行执行函数foo()本身的代码了
结束执行函数foo() 并准备返回这里编译器也会给每个函数插入一段尾声epilogue用于恢复调用方的ESP和EBP来重建之前函数的栈和恢复寄存器
执行返回指令ret被调用函数的尾声epilogue已经恢复了EBP和ESP然后我们可以从被恢复的栈中依次pop出EIP所有的参数以及被暂存的寄存器的值
读到这里相信如果没有学过汇编原理的同学肯定会有一些懵我来解释一下上面提到的寄存器缩写的具体含义上述命名均使用了x86的命名方式讲这些是希望你对函数调用有一个初步的理解其中有很多细节在不同体系结构不同编译器上的行为都有所区别所以请你放松心情跟我一起继续向后看
EBP基址指针寄存器指向栈帧的底部-
在ARM体系结构中R11ARM code或者R7Thumb code起到了类似的作用在ARM64中此寄存器为X29-
ESP栈指针寄存器指向栈帧的栈顶 在ARM下寄存器为R13-
EIP指令寄存器存储的是CPU下次要执行的指令的地址ARM下为PC寄存器为R15
2. 恢复调用帧
如果我们把上述过程缩小站在更高一层视角去看所有的函数调用栈都会形成调用帧stack frame每一个帧中都保存了足够的信息可以恢复调用函数的栈帧
我们这里忽略掉其他不相关的细节重点关注一下EBPESP和EIP你可以看到EBP和ESP分别指向执行函数栈的栈底和栈顶每次函数调用都会保存EBP和EIP用于在返回时恢复函数栈帧这里所有被保存的EBP就像一个链表指针不断地指向调用函数的EBP 这样我们就可以此为线索十分优雅地恢复整个调用栈
这里我们可以用下面的伪代码来恢复调用栈
void debugger::print_backtrace() {
auto curr_func = get_func_from_pc(get_pc());
output_frame(curr_func);
auto frame_pointer = get_register_value(m_pid, reg::rbp);
auto return_address = read_mem(frame_pointer+8);
while (dwarf::at_name(curr_func) != "main") {
curr_func = get_func_from_pc(ret_addr);
output_frame(curr_func);
frame_pointer = read_mem(frame_pointer);
return_address = read_mem(frame_pointer+8);
}
但是在ARM体系结构中出于性能的考虑天才的开发者为了节约R7/R11寄存器使其可以作为通用寄存器来使用因此无法保证保存足够的信息来形成上述调用栈的即使你向编译器传入了-fno-omit-frame-pointer”)。比如下面两种情况ARM就会不遵循一般意义上的序言prologue感兴趣的同学可以具体查看APCS Doc
函数为叶函数即在函数体内再没有任何函数调用
函数体非常小
Android中的unwind
我们知道大部分Android手机使用的都是ARM体系结构那在Android中需要如何进行unwind呢我们需要分两种情况分别讨论
1. Debug版本unwind
如果是Debug版本我们可以通过“.debug_frame”(有兴趣的同学可以了解一下DWARF来帮助我们进行unwind这种方法十分高效也十分准确但缺点是调试信息本身很大甚至会比程序段.TEXT段更大所以我们是无法在Release版本中包含这个信息的
DWARF 是一种标准调试信息格式DWARF最早与ELF文件格式一起设计, 但DWARF本身是独立的一种对象文件格式本身DAWRF和ELF的名字并没有任何意义侏儒精灵是不是像魔兽世界的角色 :)后续为了方便宣传才命名为Debugging With Attributed Record Formats引自wiki
2. Release版本unwind
对于Release版本系统使用了一种类似“.debug_frame的段落是更加紧凑的方法我们可以称之为unwind table具体来说在x86和ARM64平台上是“.eh_frame“.eh_frame_hdr”,在ARM32平台上为“.ARM.extab“.ARM.exidx”。
由于ARM32的标准早于DWARF的方法所有ARM使用了自己的实现不过它们的原理十分接近后续我们只讨论“.eh_frame”,如果你对ARM32的实现特别感兴趣可以参考ARM-Unwinding-Tutorial
“.eh_frame section也是遵循DWARF的格式的但DWARF本身十分琐碎也十分复杂这里我们就不深入进去了只涉及一些比较浅显的内容你只需要了解DAWRF使用了一个被称为DIDebugging Information Entry的数据结构去表示每个变量变量类型和函数等在debug程序时需要用到的内容
“.eh_frame使用了一种很聪明的方法构成了一个非常大的表表中包含了每个程序段的地址对应的相应寄存器的值以及返回地址等相关信息下面就是这张表的示例你可以使用readelf --debug-dump=frames-interp去查看相应的信息Release版中会精简一些信息但所有帮助我们unwind的寄存器信息都会被保留
“.eh_frame section至少包含了一个CFICall Frame Information)。每个CFI都包含了两个条目独立的CIECommon Information Entry和至少一个FDEFrame Description Entry)。通常来讲CFI都对应一个对象文件FDE则描述一个函数
“.eh_frame_hdr section包含了一系列属性除了一些基础的meta信息还包含了一列有序信息初始地址指向“.eh_frame中FDE的指针这些信息按照function排序从而可以使用二分查找加速搜索
总结
总的来说unwind第一个栈帧是最难的由于ARM无法保证会压基址指针寄存器EBP进栈所以我们需要借助一些额外的信息.eh_frame来帮助我们得到相应的基址指针寄存器的值即使如此生产环境还是会有各种栈破坏所以还是有许多工作需要做比如不同的调试器GDBLLDB或者breakpad都实现了一些搜索算法去寻找潜在的栈帧这里我们就不展开讨论了感兴趣的同学可以查阅相关代码
扩展阅读
下面给你一些外部链接你可以阅读GCC中实现unwind的关键函数有兴趣的同学可以在调试器中实现自己的unwinder
_Unwind_Backtrace
uw_frame_state_for
uw_update_context
uw_update_context_1

View File

@ -0,0 +1,91 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
专栏学得苦?可能你还需要一份配套学习书单
你好我是张绍文。专栏已经发布了一段时间很多同学在学习专栏时问我想补充一些基础知识可以参考哪些图书。今天我就结合专栏的编排给你推荐几本我看过并且对我帮助很大的图书。推荐的书单不在于数量而在于希望尽可能覆盖Android开发工程师进阶学习的路径只有掌握牢固的基础知识才能在进阶的道路上走得平稳。专栏把进阶的各个主题由点到线串联起来但这背后必然少不了一些基础的、底层的知识进行支撑而这些经典的图书涵盖的知识点比较全面即使遇到问题时放在手边也是很好的参考书。
作为一名Android开发工程师你需要学习一些Linux的基础知识在做优化时可以有更好的思路。
关于Linux学习我推荐
性能之巅
最强Android书架构大剖析
戳此购买
极客时间专栏Linux性能优化实战
如果想更好地学习虚拟机以及Hook相关的知识你需要对C++以及编译原理有一定的了解。
关于虚拟机,我推荐:
程序员的自我修养——链接、装载与库
垃圾回收算法手册
戳此购买
关于编程语言,我推荐:
More Effective C++
戳此购买
Effective Java中文版第3版
戳此购买
其他的知识,例如网络、数据库的一些细分领域,我推荐:
Web性能权威指南
戳此购买
UNIX网络编程 卷1套接字联网API第3版
戳此购买
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@ -0,0 +1,95 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
专栏学得苦?可能是方法没找对
各位同学我是《Android开发高手课》的编辑Shawn很高兴可以和你分享专栏里同学们自己的故事。小说家马塞尔·普鲁斯特说过“真正的发现之旅不在于寻找新的风景而在于有新的视角”。专栏便是给了你一个“高手”的视角让你重新审视自己工作中处理问题的方法、掌握新的技能进而让自己也成为真正的高手。这个过程一定是艰辛的但只要坚持下去一定能大有收获。
我是Kenny来自广州加入到《Android开发高手课》是希望自己可以深入学习一些Android的高级知识。
学习的过程总是枯燥的我的方法是保持兴趣和新鲜感。你可以把专栏的课程当作是一本武功秘籍每一期就相当于一种招式这样就会很有期待。我基本会在专栏更新第一时间就学习一遍然后再结合Google查找跟专栏相关的知识点把它们融会贯通。另外专栏里提供的练习我也会先自己思考然后再尝试去写demo验证最后跟老师给的Sample进行对照对比分析我和老师给的方案的优劣。
如果你正处学习的迷茫期,我建议学习前先带着问题,比如专栏更新后,先想想自己在工作中是否遇到过和这期主题相关的技术问题。然后就是兴趣了,对技术始终保持兴趣,学习的动力也会更充沛,在学习过程中也会产生更多有价值的思考。
我目前学习最大的收获是思考问题、处理问题的思维方式的变化,在对待“黑科技”也会更慎重了。最主要的是思维得到了发散,解决问题时也会多考虑如何举一反三。
我的2019年“小目标”目前在从事产品性能优化的工作期待2019能把产品做成竞品间各项性能指标第一
我是ZYW目前在从事移动端开发从2008年开始到今天已经10个年头了。
我主要是在更新当天晚上看专栏的内容,看完后再做练习,遇到不会的内容会去搜索相关的知识点。
在当下不论是工作还是学习焦虑肯定是有的。坚持学习从来就不是一蹴而就的事情。我毕业10年了IT编码也10年了现在还在坚持。IT技术更新很快一定要在合适的时间做合适的事情不要怕困难更不要放弃。
我的2019年“小目标”好好的工作好好照顾家人能在Android的道路一直走下去坚持。
我是Owen工作3年做过OA和手机的项目目前在深圳一家上市公司负责海外工具类开发。订阅这个专栏主要是听过江湖上绍文老师的“传说”再加上我对讲的内容也比较感兴趣。
一般专栏更新之后我会选择第一时间听一遍音频或是在上下班的地铁上或是在睡觉前。我会利用零碎时间反复听音频把整片的时间用来反复阅读课程文章然后根据文章的Sample自己亲自实践。因为在实践的过程中会有很多坑要爬需要自己补充老师提到的知识点专栏的篇幅也有限有些知识点就需要自己去查找资料来学习。所以一篇文章我会反复阅读反复查看相关知识点反复实践。遇到不懂的就问绍文老师、同学和同事然后形成自己的理解和总结争取自己也能写一点总结分享出来。
坚持学习的过程,我感觉自己拓宽了知识面,对知识点的体会也加深了。移动开发领域大佬、高手如云,必须孜孜不倦地学习和交流,才能做到真正的理解并且达到一定的高度。
这个专栏整体来说适合爬坡进阶,如果坚持学完会有很多收获。有些知识点我平时没太注意,或者有些知识点平时干脆都没听过,学完给我的感觉我们并不是简单的应用开发,还有太多可以精深的东西。
大家好我是Seven来自四川成都。现在在某家医疗公司担任Android开发主要做的是摄像头开发、实时分析相机预览数据这块之前还做过一年的视频点播和直播的底层开发。在这两年的工作中一直想把自己的半吊子基础给打实。
说来也巧曾经的项目中用到了Tinker也就顺理成章进入了Tinker交流群也在群内交流了一段时间。某一天绍文老师在群里推荐了他的极客时间专栏与此同时众多著名Android大佬和公众号也争相推广我已经站不住了技术的浪潮已经向我扑来我非接不可同时也希望自己能够得到一次深度成长的机会所以我选择了订阅这也是对自己负责吧。
刚开始时我是很用心的每天都听然后课后Sample也在认真做但当我发现Sample搞不定的时候我就改变了学习态度专栏更新我也及时阅读Sample做不出来就放在那里反正保持同步阅读专栏有时也在专栏里留些无关痛痒的话以为这样就是学习。直到某一天专栏出了一篇文章《让Sample跑起来 | 热点问题答疑第1期》我才恍然大悟自己当前的学习状态不对不应该这样学习应该一丝不苟脚踏实地才对。那时的我只是看上去在学习而已实际根本没有任何收获。于是我改正了自己的态度重新打开第1期重新学习认真阅读并理解文中的每一个字遇到不懂的地方就看文中给的链接或者自己查资料慢慢摸索。后来发现这样学习虽然慢但是我心里很踏实而且学到的东西其实不止是专栏中的内容因为在查资料的时候总会查到别的东西可以顺带学习并且做了笔记。我现在的学习状态大概就是这样我还是很满意自己做出的改变。
专栏从开始看到现在这么久了,最主要提升的是我的自觉性,看到不懂的知识点我并不会害怕,而是想着要怎么去弄懂它,也不会消极的对待所谓“高深”的知识了。现在遇到一个问题,我脑袋里面想的是要去解决它,先试着自己去解决,解决不了的话说明知识不到位,需要深入学习。
如果你觉得专栏里的知识很难也不要消极对待很多大的知识点都是由许多小知识点组成的只要分而治之就可以很快建立自信。并且这些小的知识点排列组合会产生无数种知识所以这里也建议同学们认真对待基础知识比如专栏里提到的Linux有关的知识这些都是保质期很长的知识希望我们可以一起坚持下来。
我的2019年“小目标”2018年已经过去希望2019年的自己能够对移动开发的基础知识有更深刻的理解希望自己能够在Android源码分析研究这一块大有长进一起加油吧
我是小洁是一名工作接近4年的Android开发坐标广东东莞公司是做儿童电话手表的我在团队中负责性能相关优化以及组件化优化方面工作。由于目前工作的任务专栏有所涉及加上身边同事的推荐所以想从专栏中借鉴学习老师的各种经验。
由于工作原因,我一般的学习时间主要是在晚上。我会根据自己对当前内容是否感兴趣,以及是否感觉重要而去学习并完成课后的练习。通过专栏我收获了一些老师的经验之谈和对某些方案的思考,能力也有所提升吧。
我认为学习还是靠个人毅力,每当学习感到枯燥或者遇到障碍时,可以沉下来问问自己开始学习的目的,重新调整自己再一步步看下去,终会有豁然开朗的时候。
我的2019年“小目标”希望自己能实现一个性能监控分析。
我是希夷,我订阅专栏的目的是想提高自己的眼界和能力。
我主要是利用空闲的时间学习专栏,并且会做些笔记,但专栏的作业和练习我坚持得不够(捂脸)。我觉得学习专栏时感觉难是正常的,如果不觉得难就说明这个专栏对你提升有限。我认为还是贵在坚持,一遍不懂就多看几遍、多练几遍,勤能补拙,我们学习都会有这个过程,也需要我们给自己打打气。
同样我觉得Android开发基础真的很重要我感觉自己有很多基础的东西要补。通过这个专栏我看到了大公司在移动开发这块使用的比较新的技术确实拓展了自己的视野。
我的2019年“小目标”希望把Kotlin和Flutter用到项目中把从专栏中学到的东西落地到项目实践上也希望自己有更高的收入。
我是志伟从12年开始在芯片企业从事Android ROM开发15年开始在移动互联网企业做Android开发。我希望可以从专栏里更加全面地了解Android开发的技术领略绍文面对亿级App各种技术问题的思考方式和处理方法。
专栏更新当天晚上我都会认真学习通过文章里附带的外部资料、自己查询网上其他资料和专栏提供的Sample进行练习从中提炼自己需要的知识。我还会结合自己的实际工作、以前的经历推敲一下自己在面对专栏里的问题时会如何思考我会怎么去做。
专栏涉及技术面广,又有深度,花时间深入钻研,肯定有很大的收获。比如我自己,通过学习扩展了自己的知识面,对一些技术的理解也更加深入,很多问题也有了解决方案。专栏定位 “高手”,肯定有觉得难或者不懂的知识点,这就更需要坚持,因为这正是成长的机会。另外,还可以直接在专栏留言区跟作者进行交流,从中我也获益良多。
绍文经历过多个App日活过亿的成长期间面临的各种技术难题也是其他App共同存在的但我们不是每个人都有这样的机会去经历。通过专栏可以以高度“浓缩”的方式经历一遍这也是十分宝贵的收获。
我的2019年“小目标”朝架构师的方向“一路走到黑”。
听完身边同学的故事你有没有想和其他同学分享的故事呢欢迎你在留言区也写写自己学习专栏的方法和体会我们会在下一期答疑文章发布时选出留言点赞数最高的3位送出“学习加油礼包”期待你的分享。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
程序员修炼之路 设计能力的提升途径
你好,我是张绍文,今天我要和你分享我的朋友长元的一篇文章,主题是设计能力的提升途径。专栏已经进入架构演进模块,由于每个人对架构的理解都不同,在工作中也会遇到各种各样的架构设计问题,很多时候我们的架构设计能力都是靠不断的理论学习和在设计实践中不断摸索提高的,因此在成为设计高手的道路上,我们肯定或多或少有些自己的经验和体会,当然也少不了踩坑。今天长元分享的设计能力提升路径,希望可以把他的经验分享给你,你可以参考他的提升路径来强化自己的设计能力,在高手的修炼之路上少走弯路。
每当我做完一次内部设计培训以后,经常有同学来问我:如何才能快速提升自己的设计能力?我觉得这个问题非常有代表性,代表了一大波程序员在艰辛修炼路上的心声。今天我就来分享一下我所理解的程序员设计能力的提升路径,也欢迎你留言写写你的思考与体会。
1. 编码历练
代码行经验是个非常重要的东西当你还没有1万行代码经验的时候如果你来问我如何提升设计能力这个问题我只能告诉你不要太纠结看看理论就好老老实实先写代码吧。
一个程序员平均每天码代码的速度是200300行。你可能会说我一天怎么也要写上1000行吧别忘了当你码完代码后你还需要测试、调试、优化、Bug Fix这些时间你没法一直码代码的。
编码规范就不多说了,如果你的代码还是杂乱无章的状态,就先别谈什么设计与架构了,先把基础的工作做好再谈其他的吧。
另外,作为“代码洁癖患者”,推荐你不要在写完代码后,再做批量格式化处理,或者手工再去整理代码,而是应该每敲一个字符,都是符合规范的。习惯真的很重要,有时在招聘面试的时候,我真想添加一个环节,现场编写程序完成一个简单但容易出错的任务,考察一下你的代码基本功。
2. 理论学习
简单说就是看书、看博客,学习你能得到的所有资源,但前提是内容质量要高。例如图书,我推荐:《重构 改善既有代码的设计》《敏捷软件开发原则、模式与实践》《UML和模式应用》《设计模式》等其他你还需要学习面向对象设计原则五大原则
《设计模式》是本很古老的书了只有短短200页但是可能这是最难看懂的一本书了可能一个月都看不完看小说的话200页3个小时也许就看完了吧。而且就算看完了也不会全看懂很可能看懂的内容不超过30%。我想说的是,看不懂没关系,认真看了就行,不用太纠结,因为这不能说明什么问题。
另外我想说一下多线程技术是程序员必须掌握的而且需要理解透彻。现在的高级技术例如GCD会掩盖你对多线程理解不足的问题因为使用起来实在太简单了。另外别说你没写过多线程依然完成了复杂的项目更别说你随手写出的多线程代码好像也没出什么问题啊你可以试试把你的代码给技术好的同事看看分分钟写个Demo让它出错乃至崩溃。
3. 实践
现在,你已经具备了一定的编码经验,而且已经学习了足够的理论知识,接下来就是真正练手的时候了。好好反复思考你学习的这些理论知识,要如何运用到项目中去,通过身体力行的实践,一定要把那些理论搞清楚,用于指导你的实践。在实践的过程中,你要收起从前的自信,首先否定自己以前的做法,保证每次做出的东西相比以前是有进步、有改进的。
4. 重温理论
你已经能看到自己的进步了,发现比以前做得更好了,但是总感觉还不够,好像有瓶颈似的,恭喜你,已经可以看到你未来的潜力了。
重新拿起书本,重温一遍之前看的那些似懂非懂的东西,你会发现之前没弄懂的内容,现在豁然开朗了,不再有那种难于理解的晦涩感了。而且就算是以前你觉得自己已经理解的内容,再看一遍的话,通常也会有新的收获。
5. 再实践
这个阶段,你已经掌握了较多的知识,不但实践经验丰富,各种理论也能手到擒来了。但是,你发现你的设计依然不够专业,而且回过头去看以前写的代码,你会惊讶:天啊,这是谁写的代码,怎么能这样干!然后,就不多说了…此时,你已经进入了自省的阶段,掌握了适合自己的学习方法,之后再学习什么新东西,都不会再难住你了。
6. 总结
先别太得意(不信?那你去给团队分享一次讲座试试),你还需要总结,总结自己的学习方法、总结项目经验、总结设计理论的知识。
如果你能有自己独到的理解,而不是停留在只会使用成熟的设计模式什么的,能根据自己的经验教训总结出一些设计原则,那自然是极好的。
7. 分享
分享是最好的学习催化剂,当你要准备一次培训分享的时候,你会发现先前以为已经理解的东西其实并没有完全理解透彻,因为你无法把它讲清楚,实际上还是研究得不够透彻。这时会迫使你再重新深入学习,做到融汇贯通,然后你才敢走上讲台。否则,当别人提问的时候,你根本回答不上来。
以上,便是我认为的程序员修炼道路的必经阶段。接下来,我再分享几点其他对设计能力提升非常重要的方法。
养成先设计,再编码的习惯。
几乎所有的程序员,一开始都不太愿意写文档,也不太愿意去精心设计,拿到需求总是忍不住那双躁动的手,总觉得敲在键盘上,把一行一行的代码飙出来,才有成就感,才是正确的工作姿势。
我的建议是,没讨论清楚不要编码,不然你一定会返工。
设计重于编码,接口重于实现。
制定接口的过程,本身就是设计过程,接口一定要反复推敲,尽量做减法而不是加法,在能满足需求的情况下越简单越好。
另外不要一个人冥思苦想。可以先简单做一个雏形出来然后去找使用方沟通直到对方满意为止。不要完全根据使用需求去设计接口参考MVVMViewModel就是根据View的需要而对Model进行的再封装不能将这些接口直接设计到Model中。
不盲从设计模式。
设计模式只是一种解决问题的套路方法你也可以有自己的方法当然设计模式如果用好了会让你的设计显得专业、优雅毕竟前辈们的心血结晶是非常有价值的。但是如果滥用的话也会导致更严重的问题甚至可能成为灾难。我觉得面向对象设计原则更加重要有些原则是必须遵守的如单向依赖、SRP等而设计模式本身都是遵守这些原则的有些模式就是为了遵循某原则而设计出来的。
抽象不是万能的,在适当的地方使用,需要仔细推敲。当有更好的方案不用抽象就能解决问题时,尽量避免抽象。我见过太多抽象过火、过度设计的案例了,增加了太多维护成本,还不如按照最自然的方式去写。
空杯心态,向身边的同学学习,站在巨人的肩上,站在别人的肩上。
有人提意见,先收下它(无论接受与否)。
很多程序员都有个“毛病”,就是觉得自己技术牛的不行,不愿意接受别人的意见,尤其是否定意见(文人相轻)。 但是无论是理论的学习,还是编码实践,向身边的同学学习是对自己影响最大的(三人行,必有我师)。
我自己就经常在跟团队同学讨论中获益,当百思不得其解的时候,把问题抛出来讨论一下,通常都能得到一个最佳方案。
另外,跟团队其他人讨论还有一个好处,就是当你的设计有妥协或有些不专业的时候,别人看到代码也不会产生质疑,因为他也参与了讨论,你不用花那么多时间去做解释。
设计期间一定要找其他人一起讨论,我一直比较反对一个人把设计做完、把文档写完,然后才找大家开个评审会那种模式,虽然也有效果,但是效果达不到极致。因为大家没有参与到设计中,通过一次会议的时间理解不一定有那么深,最关键的是,如果在会上发现设计有些问题,但不是致命问题的时候,通常并不会打回重新设计。
相反,如果前期讨论足够,大家都知道你的思路与方案,而且最后也有设计文档,当其他人阅读你的代码的时候,根本无需你再指引,这样今后在工作交接时都会很顺利,何乐而不为呢?
最后,我想呼吁一下,当你去修改维护别人的代码时,最好找模块负责人深入讨论沟通一下,让他明白你的需求以及你的方案,请他帮忙评估方案是否可行,是否会踩坑、埋坑等。如果你恰好是模块的负责人,请行使你的权力,拒绝有问题的不符合要求的代码提交入库。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@ -0,0 +1,184 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 ASM插桩强化练习
你好,我是孙鹏飞。
专栏上一期绍文讲了编译插桩的三种方法AspectJ、ASM、ReDex以及它们的应用场景。学完以后你是不是有些动心想赶快把它们应用到实际工作中去。但我也还了解到不少同学其实接触插桩并不多在工作中更是很少使用。由于这项技术太重要了可以实现很多功能所以我还是希望你通过理论 + 实践的方式尽可能掌握它。因此今天我给你安排了一期“强化训练”,希望你可以趁热打铁,保持学习的连贯性,把上一期的理论知识,应用到今天插桩的练习上。
为了尽量降低上手的难度,我尽量给出详细的操作步骤,相信你只要照着做,并结合专栏上期内容的学习,你一定可以掌握插桩的精髓。
ASM插桩强化练习
在上一期里Eateeer同学留言说得非常好提到了一个工具我也在使用这个工具帮助自己理解ASM。安装“ASM Bytecode Outline”也非常简单只需要在Android Studio中的Plugin搜索即可。
ASM Bytecode Outline插件可以快速展示当前编辑类的字节码表示也可以展示出生成这个类的ASM代码你可以在Android Studio源码编译框内右键选择“Show Bytecode Outline“来查看反编译后的字节码在右侧展示。
我以今天强化练习中的SampleApplication类为例具体字节码如下图所示。
除了字节码模式ASM Bytecode Outline还有一种“ASMified”模式你可以看到SampleApplication类应该如何用ASM代码构建。
下面我们通过两个例子的练习加深对ASM使用的理解。
1. 通过ASM插桩统计方法耗时
今天我们的第一个练习是通过ASM实现统计每个方法的耗时。怎么做呢请你先不要着急同样以SampleApplication类为例如下图所示你可以先手动写一下希望实现插桩前后的对比代码。
那这样“差异”代码怎么样转化了ASM代码呢ASM Bytecode Outline还有一个非常强大的功能它可以展示相邻两次修改的代码差异这样我们可以很清晰地看出修改的代码在字节码上的呈现。
“onCreate”方法在“ASMified”模式的前后差异代码也就是我们需要添加的ASM代码。在真正动手去实现插桩之前我们还是需要理解一下ASM源码中关于Core API里面ClassReader、ClassWriter、ClassVisitor等几个类的用法。
我们使用ASM需要先通过ClassReader读入Class文件的原始字节码然后使用ClassWriter类基于不同的Visitor类进行修改其中COMPUTE_MAXS和EXPAND_FRAMES都是需要特别注意的参数。
ClassReader classReader = new ClassReader(is);
//COMPUTE_MAXS 说明使用ASM自动计算本地变量表最大值和操作数栈的最大值
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
//EXPAND_FRAMES 说明在读取 class 的时候同时展开栈映射帧(StackMap Frame),在使用 AdviceAdapter里这项是必须打开的
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
如果要统计每个方法的耗时我们可以使用AdviceAdapter来实现。它提供了onMethodEnter()和onMethodExit()函数非常适合实现方法的前后插桩。具体的实现你可以参考今天强化练习中的TraceClassAdapter的实现
private int timeLocalIndex = 0;
@Override
protected void onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
timeLocalIndex = newLocal(Type.LONG_TYPE); //这个是LocalVariablesSorter 提供的功能,可以尽量复用以前的局部变量
mv.visitVarInsn(LSTORE, timeLocalIndex);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, timeLocalIndex);
mv.visitInsn(LSUB);//此处的值在栈顶
mv.visitVarInsn(LSTORE, timeLocalIndex);//因为后面要用到这个值所以先将其保存到本地变量表中
int stringBuilderIndex = newLocal(Type.getType("java/lang/StringBuilder"));
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);//需要将栈顶的 stringbuilder 保存起来否则后面找不到了
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitLdcInsn(className + "." + methodName + " time:");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);//将 append 方法的返回值从栈里 pop 出去
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);//将 append 方法的返回值从栈里 pop 出去
mv.visitLdcInsn("Geek");
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);//注意: Log.d 方法是有返回值的,需要 pop 出去
mv.visitInsn(Opcodes.POP);//插入字节码后要保证栈的清洁,不影响原来的逻辑,否则就会产生异常,也会对其他框架处理字节码造成影响
}
具体实现和我们在ASM Bytecode Outline看到的大同小异但是这里需要注意局部变量的使用。在练习的例子中用到了AdviceAdapter的一个很重要的父类LocalVariablesSorter这个类提供了一个很好用的方法newLocal它可以分配一个本地变量的index而不用用户考虑本地变量的分配和覆盖问题。
另一个需要注意的情况是我们在最后的时候需要判断一下插入的代码是否会在栈顶上遗留不使用的数据如果有的话需要消耗掉或者POP出去否则就会导致后续代码的异常。
这样我们就可以快速地将这一大段字节码完成了。
2. 替换项目中的所有的new Thread
今天另一个练习是替换项目中所有的new Thread换为自己项目的CustomThread类。在实践中你可以通过这个方法在CustomThread增加统计代码从而实现统计每个线程运行的耗时。
不过这也是一个相对来说坑比较多的情况你可以提前考虑一下可能会遇到什么状况。同样我们通过修改MainActivity的startThread方法里面的Thread对象改变成CustomThread通过ASM Bytecode Outline看看在字节码上面的差异
InvokeVirtual是根据new出来的对象来调用所以我们只需要替换new对象的过程就可以了。这里需要处理两个指令一个new、一个InvokeSpecial。在大多数情况下这两条指令是成对出现的但是在一些特殊情况下会遇到直接从其他位置传递过来一个已经存在的对象并强制调用构造方法的情况。
而我们需要处理这种特殊情况所以在例子里我们需要判断new和InvokeSpecial是否是成对出现的。
private boolean findNew = false;//标识是否遇到了new指令
@Override
public void visitTypeInsn(int opcode, String s) {
if (opcode == Opcodes.NEW && "java/lang/Thread".equals(s)) {
findNew = true;//遇到new指令
mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");//替换new指令的类名
return;
}
super.visitTypeInsn(opcode, s);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
//需要排查CustomThread自己
if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && findNew) {
findNew= false;
mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", name, desc, itf);//替换INVOKESPECIAL 的类名,其他参数和原来保持一致
return;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
new指令的形态相对特殊比如我们可能会遇到下面的情况
new A(new B(2));
字节码如下你会发现两个new指令连在一起。
NEW A
DUP
NEW B
DUP
ICONST_2
INVOKESPECIAL B.<init> (I)V
INVOKESPECIAL A.<init> (LB;)V
虽然ASM Bytecode Outline工具可以帮助我们完成很多场景下的ASM需求但是在处理字节码的时候还是需要考虑很多种可能出现的情况这点需要你注意一下每个指令的特征。所以说在稍微复杂一些的情况下我们依然需要对ASM字节码以及ASM源码中的一些工具类有所了解并且需要很多次的实践毕竟实践是最重要的。
最后再留给你一个思考题如何给某个方法增加一个try catch呢你可以尝试一下在今天强化练习的代码里根据我提供的插件示例实现一下。
强化练习的代码https://github.com/AndroidAdvanceWithGeektime/Chapter-ASM
福利彩蛋
学到这里相信你肯定会认同成为一个Android开发高手的确不容易能够坚持学习和练习并整理输出分享更是不易。但是也确实有同学坚持下来了。
还记得在专栏导读里我们的承诺吗我们会选出坚持参与学习并分享心得的同学送出2019年GMTC大会的门票。今天我们就来兑现承诺送出价值4800元的GMTC门票一张。获得这个“大礼包”的同学是@唯鹿他不仅提交了作业更是在博客里分享了每个练习Sample实现的过程和心得并且一直在坚持。我在文稿里贴了他的练习心得文章链接如果你对于之前的练习Sample还有不明白的地方可以参考唯鹿同学的实现过程。
Android 开发高手课 课后练习1 ~ 5
Android 开发高手课 课后练习6 ~ 8121719
专栏第4期完成作业
专栏第19期完成作业
GMTC门票还有剩余给自己一个进阶的机会从现在开始一切都还来得及。
小程序、Flutter、移动AI、工程化、性能优化…大前端的下一站在哪里GMTC 2019全球大前端技术大会将于6月北京盛大开幕来自Google、BAT、美团、京东、滴滴等一线前端大牛将与你面对面共话前端那些事聊聊大前端的最新技术趋势和最佳实践案例。-
目前大会最低价7折购票火热进行中讲师和议题也持续招募中点击下方图片了解更多大会详情

View File

@ -0,0 +1,397 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 唯鹿同学的练习手记 第1辑
你好我是张绍文今天我要跟你分享唯鹿同学完成专栏课后练习作业的“手记”。专栏承诺会为坚持完成练习作业的同学送出GMTC大会门票唯鹿同学通过自己的努力和坚持为自己赢得了GMTC大会的门票。
如果你还没开始练习我强烈建议你花一些时间在练习上因为每个练习的Sample都是我和学习委员花费很多精力精心准备的为的是让你在学习完后可以有机会上手实践帮你尽快消化专栏里的知识并为自己所用。
大家好我是唯鹿来自西安从事Android开发也有近5年的时间了目前在做智慧社区方面的业务。我自己坚持写博客已经有三年多的时间了希望分享自己在工作、学习中的收获。
先说说我学习专栏的方法专栏更新当天我就会去学习但是难度真的不小。我对自己的要求并不是看一遍就要搞明白而是遇见不懂的地方立马查阅资料要做到大体了解整篇内容。之后在周末的时候我会集中去做Sample练习一边复习本周发布的内容一边用写博客的方式记录练习的结果。
后面我计划专栏结束后再多看、多练习几遍不断查漏补缺。说真的我很喜欢《Android开发高手课》的难度让我在完成练习作业时有种翻越高山的快感。最后希望同学们一起坚持享受翻越高山带来的成就感。
最近在学习张绍文老师的《Android开发高手课》。课后作业可不是一般的难最近几天抽空练习了一下结合老师给的步骤和其他同学的经验完成了前5课的内容。
我整理总结了一下,分享出来,希望可以帮到一起学习的同学(当然希望大家尽量靠自己解决问题)。
Chapter01
例子里集成了Breakpad来获取发生Native Crash时候的系统信息和线程堆栈信息。通过一个简单的Native崩溃捕获过程完成minidump文件的生成和解析在实践中加深对Breakpad工作机制的认识。
直接运行项目按照README.md的步骤操作就行。
中间有个问题老师提供的minidump_stackwalker工具在macOS 10.14以上无法成功执行因为没有libstdc++.6.dylib库所以我就下载Breakpad源码重新编译了一遍。
使用minidump_stackwalker工具来根据minidump文件生成堆栈跟踪log得到的crashLog.txt文件如下
Operating system: Android
0.0.0 Linux 4.9.112-perf-gb92eddd #1 SMP PREEMPT Tue Jan 1 21:35:06 CST 2019 aarch64
CPU: arm64 // 注意点1
8 CPUs
GPU: UNKNOWN
Crash reason: SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available
Thread 0 (crashed)
0 libcrash-lib.so + 0x600 // 注意点2
x0 = 0x00000078e0ce8460 x1 = 0x0000007fd4000314
x2 = 0x0000007fd40003b0 x3 = 0x00000078e0237134
x4 = 0x0000007fd40005d0 x5 = 0x00000078dca14200
x6 = 0x0000007fd4000160 x7 = 0x00000078c8987e18
x8 = 0x0000000000000000 x9 = 0x0000000000000001
x10 = 0x0000000000430000 x11 = 0x00000078e05ef688
x12 = 0x00000079664ab050 x13 = 0x0ad046ab5a65bfdf
x14 = 0x000000796650c000 x15 = 0xffffffffffffffff
x16 = 0x00000078c83defe8 x17 = 0x00000078c83ce5ec
x18 = 0x0000000000000001 x19 = 0x00000078e0c14c00
x20 = 0x0000000000000000 x21 = 0x00000078e0c14c00
x22 = 0x0000007fd40005e0 x23 = 0x00000078c89fa661
x24 = 0x0000000000000004 x25 = 0x00000079666cc5e0
x26 = 0x00000078e0c14ca0 x27 = 0x0000000000000001
x28 = 0x0000007fd4000310 fp = 0x0000007fd40002e0
lr = 0x00000078c83ce624 sp = 0x0000007fd40002c0
pc = 0x00000078c83ce600
Found by: given as instruction pointer in context
1 libcrash-lib.so + 0x620
fp = 0x0000007fd4000310 lr = 0x00000078e051c7e4
sp = 0x0000007fd40002f0 pc = 0x00000078c83ce624
Found by: previous frame's frame pointer
2 libart.so + 0x55f7e0
fp = 0x130c0cf800000001 lr = 0x00000079666cc5e0
sp = 0x0000007fd4000320 pc = 0x00000078e051c7e4
Found by: previous frame's frame pointer
......
下来是符号解析可以使用NDK中提供的addr2line来根据地址进行一个符号反解的过程该工具在$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line。
注意此处要注意一下平台如果是ARM 64位的so解析是需要使用aarch64-linux-android-4.9下的工具链。
因为我的是ARM 64位的so。所以使用aarch64-linux-android-4.9libcrash-lib.so在app/build/intermediates/cmake/debug/obj/arm64-v8a下0x600为错误位置符号。
aarch64-linux-android-addr2line -f -C -e libcrash-lib.so 0x600
输出结果如下:
Crash()
/Users/weilu/Downloads/Chapter01-master/sample/.externalNativeBuild/cmake/debug/arm64-v8a/../../../../src/main/cpp/crash.cpp:10
可以看到输出结果与下图错误位置一致第10行
Chapter02
该例子主要演示了如何通过关闭FinalizerWatchdogDaemon来减少TimeoutException的触发。
在我的上一篇博客:安卓开发中遇到的奇奇怪怪的问题(三)中有说明,就不重复赘述了。
Chapter03
项目使用了Inline Hook来拦截内存对象分配时候的RecordAllocation函数通过拦截该接口可以快速获取到当时分配对象的类名和分配的内存大小。
在初始化的时候我们设置了一个分配对象数量的最大值如果从start开始对象分配数量超过最大值就会触发内存dump然后清空alloc对象列表重新计算。该功能和Android Studio里的Allocation Tracker类似只不过可以在代码级别更细粒度的进行控制。可以精确到方法级别。
项目直接跑起来后点击开始记录然后点击5次生成1000对象按钮。生成对象代码如下
for (int i = 0; i < 1000; i++) {
Message msg = new Message();
msg.what = i;
}
因为代码从点击开始记录开始触发到5000的数据就dump到文件中点击5次后就会在sdcard/crashDump下生成一个时间戳命名的文件项目根目录下调用命令
java -jar tools/DumpPrinter-1.0.jar dump文件路径 > dump_log.txt
然后就可以在dump_log.txt中看到解析出来的数据
Found 5000 records:
....
tid=4509 android.graphics.drawable.RippleForeground (112 bytes)
android.graphics.drawable.RippleDrawable.tryRippleEnter (RippleDrawable.java:569)
android.graphics.drawable.RippleDrawable.setRippleActive (RippleDrawable.java:276)
android.graphics.drawable.RippleDrawable.onStateChange (RippleDrawable.java:266)
android.graphics.drawable.Drawable.setState (Drawable.java:778)
android.view.View.drawableStateChanged (View.java:21137)
android.widget.TextView.drawableStateChanged (TextView.java:5289)
android.support.v7.widget.AppCompatButton.drawableStateChanged (AppCompatButton.java:155)
android.view.View.refreshDrawableState (View.java:21214)
android.view.View.setPressed (View.java:10583)
android.view.View.setPressed (View.java:10561)
android.view.View.onTouchEvent (View.java:13865)
android.widget.TextView.onTouchEvent (TextView.java:10070)
android.view.View.dispatchTouchEvent (View.java:12533)
android.view.ViewGroup.dispatchTransformedTouchEvent (ViewGroup.java:3032)
android.view.ViewGroup.dispatchTouchEvent (ViewGroup.java:2662)
android.view.ViewGroup.dispatchTransformedTouchEvent (ViewGroup.java:3032)
tid=4515 int[] (104 bytes)
tid=4509 android.os.BaseLooper$MessageMonitorInfo (88 bytes)
android.os.Message.<init> (Message.java:123)
com.dodola.alloctrack.MainActivity$4.onClick (MainActivity.java:70)
android.view.View.performClick (View.java:6614)
android.view.View.performClickInternal (View.java:6591)
android.view.View.access$3100 (View.java:786)
android.view.View$PerformClick.run (View.java:25948)
android.os.Handler.handleCallback (Handler.java:873)
android.os.Handler.dispatchMessage (Handler.java:99)
android.os.Looper.loop (Looper.java:201)
android.app.ActivityThread.main (ActivityThread.java:6806)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:547)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:873)
......
我们用Android Profiler查找一个Message对象对比一下一模一样。
简单看一下Hook代码
void hookFunc() {
LOGI("start hookFunc");
void *handle = ndk_dlopen("libart.so", RTLD_LAZY | RTLD_GLOBAL);
if (!handle) {
LOGE("libart.so open fail");
return;
}
void *hookRecordAllocation26 = ndk_dlsym(handle,
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj");
void *hookRecordAllocation24 = ndk_dlsym(handle,
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj");
void *hookRecordAllocation23 = ndk_dlsym(handle,
"_ZN3art3Dbg16RecordAllocationEPNS_6ThreadEPNS_6mirror5ClassEj");
void *hookRecordAllocation22 = ndk_dlsym(handle,
"_ZN3art3Dbg16RecordAllocationEPNS_6mirror5ClassEj");
if (hookRecordAllocation26 != nullptr) {
LOGI("Finish get symbol26");
MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26,
(void **) &oldArtRecordAllocation26);
} else if (hookRecordAllocation24 != nullptr) {
LOGI("Finish get symbol24");
MSHookFunction(hookRecordAllocation26, (void *) &newArtRecordAllocation26,
(void **) &oldArtRecordAllocation26);
} else if (hookRecordAllocation23 != NULL) {
LOGI("Finish get symbol23");
MSHookFunction(hookRecordAllocation23, (void *) &newArtRecordAllocation23,
(void **) &oldArtRecordAllocation23);
} else {
LOGI("Finish get symbol22");
if (hookRecordAllocation22 == NULL) {
LOGI("error find hookRecordAllocation22");
return;
} else {
MSHookFunction(hookRecordAllocation22, (void *) &newArtRecordAllocation22,
(void **) &oldArtRecordAllocation22);
}
}
dlclose(handle);
}
使用了Inline Hook方案Substrate来拦截内存对象分配时候libart.so的RecordAllocation函数。首先如果我们要hook一个函数需要知道这个函数的地址。我们也看到了代码中这个地址判断了四种不同系统。这里有一个网页版的解析工具可以快速获取。下面以8.0为例。
我在8.0的源码中找到了对应的方法:
7.0方法就明显不同:
我也同时参看了9.0的代码发现没有变化所以我的测试机是9.0的也没有问题。
Hook新内存对象分配处理代码
static bool newArtRecordAllocationDoing24(Class *type, size_t byte_count) {
allocObjectCount++;
//根据 class 获取类名
char *typeName = GetDescriptor(type, &a);
//达到 max
if (allocObjectCount > setAllocRecordMax) {
CMyLock lock(g_Lock);//此处需要 loc 因为对象分配的时候不知道在哪个线程,不 lock 会导致重复 dump
allocObjectCount = 0;
// dump alloc 里的对象转换成 byte 数据
jbyteArray allocData = getARTAllocationData();
// 将alloc数据写入文件
SaveAllocationData saveData{allocData};
saveARTAllocationData(saveData);
resetARTAllocRecord();
LOGI("===========CLEAR ALLOC MAPS=============");
lock.Unlock();
}
return true;
}
Chapter04
通过分析内存文件hprof快速判断内存中是否存在重复的图片并且将这些重复图片的PNG、堆栈等信息输出。
首先是获取我们需要分析的hprof文件我们加载两张相同的图片
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
imageView1.setImageBitmap(bitmap1);
imageView2.setImageBitmap(bitmap2);
生成hprof文件
// 手动触发GC
Runtime.getRuntime().gc();
System.runFinalization();
Debug.dumpHprofData(file.getAbsolutePath());
接下来就是利用HAHA库进行文件分析的核心代码
// 打开hprof文件
final HeapSnapshot heapSnapshot = new HeapSnapshot(hprofFile);
// 获得snapshot
final Snapshot snapshot = heapSnapshot.getSnapshot();
// 获得Bitmap Class
final ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap");
// 获得heap, 只需要分析app和default heap即可
Collection<Heap> heaps = snapshot.getHeaps();
for (Heap heap : heaps) {
// 只需要分析app和default heap即可
if (!heap.getName().equals("app") && !heap.getName().equals("default")) {
continue;
}
for (ClassObj clazz : bitmapClasses) {
//从heap中获得所有的Bitmap实例
List<Instance> bitmapInstances = clazz.getHeapInstances(heap.getId());
//从Bitmap实例中获得buffer数组,宽高信息等。
ArrayInstance buffer = HahaHelper.fieldValue(((ClassInstance) bitmapInstance).getValues(), "mBuffer");
int bitmapHeight = fieldValue(bitmapInstance, "mHeight");
int bitmapWidth = fieldValue(bitmapInstance, "mWidth");
// 引用链信息
while (bitmapInstance.getNextInstanceToGcRoot() != null) {
print(instance.getNextInstanceToGcRoot());
instance = instance.getNextInstanceToGcRoot();
}
// 根据hashcode来进行重复判断
}
}
最终的输出结果:
我们用Studio打开hprof文件对比一下
可以看到信息是一摸一样的。对于更优处理引用链的信息可以参看LeakCanary源码的实现。
我已经将上面的代码打成JAR包可以直接调用
//调用方法:
java -jar tools/DuplicatedBitmapAnalyzer-1.0.jar hprof文件路径
详细的代码我提交到了Github供大家参考。
Chapter05
尝试模仿ProcessCpuTracker.java拿到一段时间内各个线程的耗时占比。
usage: CPU usage 5000ms(from 23:23:33.000 to 23:23:38.000):
System TOTAL: 2.1% user + 16% kernel + 9.2% iowait + 0.2% irq + 0.1% softirq + 72% idle
CPU Core: 8
Load Average: 8.74 / 7.74 / 7.36
Process:com.sample.app
50% 23468/com.sample.app(S): 11% user + 38% kernel faults:4965
Threads:
43% 23493/singleThread(R): 6.5% user + 36% kernel faults3094
3.2% 23485/RenderThread(S): 2.1% user + 1% kernel faults329
0.3% 23468/.sample.app(S): 0.3% user + 0% kernel faults6
0.3% 23479/HeapTaskDaemon(S): 0.3% user + 0% kernel faults982
...
因为了解Linux不多所以看这个有点懵逼。好在课代表孙鹏飞同学解答了相关问题看懂了上面信息同时学习到了一些Linux知识。
private void testIO() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
File f = new File(getFilesDir(), "aee.txt");
FileOutputStream fos = new FileOutputStream(f);
byte[] data = new byte[1024 * 4 * 3000];// 此处分配一个 12mb 大小的 byte 数组
for (int i = 0; i < 30; i++) {// 由于 IO cache 机制的原因所以此处写入多次 cache触发 dirty writeback 到磁盘中
Arrays.fill(data, (byte) i);// 当执行到此处的时候产生 minor fault并且产生 User cpu useage
fos.write(data);
}
fos.flush();
fos.close();
}
});
thread.setName("SingleThread");
thread.start();
}
上述代码就是导致的问题罪魁祸首这种密集I/O操作集中在SingleThread线程中处理导致发生了3094次faults36% kernel完全没有很好利用到8核CPU
最后通过检测CPU的使用率可以更好地避免卡顿现象防止ANR的发生
前前后后用了两三天的时间远远没有当初想的顺利感觉身体被掏空中间也爬了不少坑虽然没有太深入实现代码但是中间的体验过程也是收获不小所以总不能因为难就放弃了先做到力所能及的部分让自己动起来
参考
练习Sample跑起来 | 热点问题答疑第1期
练习Sample跑起来 | 热点问题答疑第2期

View File

@ -0,0 +1,311 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 唯鹿同学的练习手记 第2辑
你好,我是唯鹿。
接着上篇练习手记今天练习68、12、17、19这六期内容主要针对有课后Sample练习的相比15期轻松了很多。
Chapter06
该项目展示了使用PLT Hook技术来获取Atrace的日志可以学习到systrace的一些底层机制。
没有什么问题项目直接可以运行起来。运行项目后点击开启Atrace日志然后就可以在Logcat日志中查看到捕获的日志如下
11:40:07.031 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= install systrace hoook =========
11:40:07.034 8537-8537/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|Record View#draw()
11:40:07.034 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|DrawFrame
11:40:07.035 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|syncFrameState
========= B|8537|prepareTree
========= E
========= E
========= B|8537|eglBeginFrame
========= E
========= B|8537|computeOrdering
========= E
========= B|8537|flush drawing commands
========= E
11:40:07.036 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|eglSwapBuffersWithDamageKHR
========= B|8537|setSurfaceDamage
========= E
11:40:07.042 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|queueBuffer
========= E
11:40:07.043 8537-8552/com.dodola.atrace I/HOOOOOOOOK: ========= B|8537|dequeueBuffer
========= E
========= E
========= E
通过B|事件和E|事件是成对出现的这样就可以计算出应用执行每个事件使用的时间。那么上面的Log中View的draw()方法显示使用了9ms。
这里实现方法是使用了Profilo的PLT Hook来hook libc.so的write与__write_chk方法。libc是C的基础库函数为什么要hook这些方法需要我们补充C、Linux相关知识。
同理Chapter06-plus展示了如何使用 PLT Hook技术来获取线程创建的堆栈README有详细的实现步骤介绍我就不赘述了。
Chapter07
这个Sample是学习如何给代码加入Trace Tag大家可以将这个代码运用到自己的项目中然后利用systrace查看结果。这就是所谓的systrace + 函数插桩。
操作步骤:
使用Android Studio打开工程Chapter07。
运行Gradle Task :systrace-gradle-plugin:buildAndPublishToLocalMaven编译plugin插件。
使用Android Studio单独打开工程systrace-sample-android。
编译运行App插桩后的class文件在目录Chapter07/systrace-sample-android/app/build/systrace_output/classes中查看
对比一下插桩效果,插桩前:
插桩后:
可以看到在方法执行前后插入了TraceTag这样的话beginSection方法和endSection方法之间的代码就会被追踪。
public class TraceTag {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public static void i(String name) {
Trace.beginSection(name);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public static void o() {
Trace.endSection();
}
其实Support-Compat库中也有类似的一个TraceCompat项目中可以直接使用。
然后运行项目打开systrace
python $ANDROID_HOME/platform-tools/systrace/systrace.py gfx view wm am pm ss dalvik app sched -b 90960 -a com.sample.systrace -o test.log.html
最后打开生成的test.log.html文件就可以查看systrace记录
当然这一步我们也可以使用SDK中的Monitor效果是一样的。
使用systrace + 函数插桩的方式我们就可以很方便地观察每个方法的耗时从而针对耗时的方法进行优化尤其是Application的启动优化。
Chapter08
该项目展示了关闭掉虚拟机的class verify后对性能的影响。
在加载类的过程有一个verify class的步骤它需要校验方法的每一个指令是一个比较耗时的操作。这个例子就是通过Hook去掉verify这个步骤。该例子尽量在Dalvik下执行在ART下的效果并不明显。
去除校验代码可以参看阿里的Atlas
AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(this.getApplicationContext(), true);
runtime.setVerificationEnabled(false);
具体运行效果这里我就不展示了,直接运行体验就可以了。
Chapter12
通过复写Application的getSharedPreferences替换系统SharedPreferences的实现核心的优化在于修改了Apply的实现将多个Apply方法在内存中合并而不是多次提交。
修改SharedPreferencesImpl的Apply部分如下
public void apply() {
// 先调用commitToMemory()
final MemoryCommitResult mcr = commitToMemory();
boolean hasDiskWritesInFlight = false;
synchronized (SharedPreferencesImpl.this) {
// mDiskWritesInFlight大于0说明之前已经有调用过commitToMemory()了
hasDiskWritesInFlight = mDiskWritesInFlight > 0;
}
// 源码没有这层判断,直接提交。
if (!hasDiskWritesInFlight) {
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
Chapter14
这个是全面解析SQLite的资料有兴趣的可以下载看看。
Chapter17
该项目展示了如何使用PLT Hook技术来获取网络请求相关信息。
通过PLT Hook代理Socket相关的几个重要函数
/**
* 直接 hook 内存中的所有so但是需要排除掉socket相关方法本身定义的libc(不然会出现循坏)
* plt hook
*/
void hookLoadedLibs() {
ALOG("hook_plt_method");
hook_plt_method_all_lib("libc.so", "send", (hook_func) &socket_send_hook);
hook_plt_method_all_lib("libc.so", "recv", (hook_func) &socket_recv_hook);
hook_plt_method_all_lib("libc.so", "sendto", (hook_func) &socket_sendto_hook);
hook_plt_method_all_lib("libc.so", "recvfrom", (hook_func) &socket_recvfrom_hook);
hook_plt_method_all_lib("libc.so", "connect", (hook_func) &socket_connect_hook);
}
int hook_plt_method_all_lib(const char* exclueLibname, const char* name, hook_func hook) {
if (refresh_shared_libs()) {
// Could not properly refresh the cache of shared library data
return -1;
}
int failures = 0;
for (auto const& lib : allSharedLibs()) {
if (strcmp(lib.first.c_str(), exclueLibname) != 0) {
failures += hook_plt_method(lib.first.c_str(), name, hook);
}
}
return failures;
}
运行项目访问百度的域名https://www.baidu.com输出如下
17:08:28.347 12145-12163/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 10
17:08:28.349 12145-12163/com.dodola.socket E/HOOOOOOOOK: stack:com.dodola.socket.SocketHook.getStack(SocketHook.java:13)
java.net.PlainSocketImpl.socketConnect(Native Method)
java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:334)
java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:196)
java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:178)
java.net.SocksSocketImpl.connect(SocksSocketImpl.java:356)
java.net.Socket.connect(Socket.java:586)
com.android.okhttp.internal.Platform.connectSocket(Platform.java:113)
com.android.okhttp.Connection.connectSocket(Connection.java:196)
com.android.okhttp.Connection.connect(Connection.java:172)
com.android.okhttp.Connection.connectAndSetOwner(Connection.java:367)
com.android.okhttp.OkHttpClient$1.connectAndSetOwner(OkHttpClient.java:130)
com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:329)
com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:246)
com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnection
AF_INET6 ipv6 IP===>183.232.231.173:443
socket_connect_hook sa_family: 1
Ignore local socket connect
02-07 17:08:28.637 12145-12163/com.dodola.socket E/HOOOOOOOOK: respond:<!DOCTYPE html>
<html><!--STATUS OK--><head><meta charset="utf-8"><title>百度一下,你就知道</title>
可以看到我们获取到了网络请求的相关信息。
最后我们可以通过Connect函数的hook实现很多需求例如
禁用应用网络访问
过滤广告IP
禁用定位功能
Chapter19
使用Java Hook实现Alarm、WakeLock与GPS的耗电监控。
实现原理
根据老师提供的提示信息动态代理对应的PowerManager、AlarmManager、LocationManager的mService实现要拦截的方法在PowerManagerService、AlarmManagerService、LocationManagerService中。
实现核心代码:
Object oldObj = mHostContext.getSystemService(Context.XXX_SERVICE);
Class<?> clazz = oldObj.getClass();
Field field = clazz.getDeclaredField("mService");
field.setAccessible(true);
final Object mService = field.get(oldObj);
setProxyObj(mService);
Object newObj = Proxy.newProxyInstance(this.getClass().getClassLoader(), mService.getClass().getInterfaces(), this);
field.set(oldObj, newObj)
写了几个调用方法去触发,通过判断对应的方法名来做堆栈信息的输出。
输出的堆栈信息如下:
当然强大的Studio在3.2后也有了强大的耗电量分析器同样可以监测到这些信息如下图所示我使用的Studio版本为3.3)。
实现不足之处:
可能兼容性上不是特别完善(期待老师的标准答案)。
没有按照耗电监控的规则去做一些业务处理。
心得体会:
本身并不复杂只是为了找到Hook点看了对应的Service源码耗费了一些时间对于它们的工作流程有了更深的认识。
平时也很少使用动态代理,这回查漏补缺,一次用了个爽。
这个作业前前后后用了一天时间之前作业还有一些同学提供PR所以相对轻松些但这次没有参考走了点弯路不过收获也是巨大的。我就不细说了感兴趣的话可以参考我的实现。完整代码参见GitHub仅供参考。
参考
练习Sample跑起来 | 热点问题答疑第3期
练习Sample跑起来 | 热点问题答疑第4期

View File

@ -0,0 +1,324 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 唯鹿同学的练习手记 第3辑
没想到之前的写的练习心得得到了老师的认可看来我要更加认真努力练习了。今天来练习第22、27、ASM这三课的Sample。
Chapter22
尝试使用Facebook ReDex库来优化我们的安装包。
准备工作
首先是下载ReDex
git clone https://github.com/facebook/redex.git
cd redex
接着是安装:
autoreconf -ivf && ./configure && make -j4
sudo make install
在安装时执行到这里,报出下图错误:
其实就是没有安装Boost所以执行下面的命令安装它。
brew install boost jsoncpp
安装Boost完成后再等待十几分钟时间安装ReDex。
下来就是编译我们的Sample得到的安装包信息如下。
可以看到有三个Dex文件APK大小为13.7MB。
通过ReDex命令优化
为了让我们可以更加清楚流程你可以输出ReDex的日志。
export TRACE=2
去除Debuginfo的方法需要在项目根目录执行
redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android -c redex-test/stripdebuginfo.config -P ReDexSample/proguard-rules.pro -o redex-test/strip_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk
上面这段很长的命令,其实可以拆解为几部分:
--sign 签名信息
-skeystore签名文件路径
-akeyalias签名的别名
-pkeypass签名的密码
-c 指定ReDex的配置文件路径
-P ProGuard规则文件路径
-o 输出的文件路径
最后是要处理APK文件的路径
但在使用时,我遇到了下图的问题:
这里是找不到Zipalign所以需要我们配置Android SDK的根目录路径添加在原命令前面
ANDROID_SDK=/path/to/android/sdk redex [... arguments ...]
结果如下:
实际的优化效果是原Debug包为14.21MB去除Debuginfo的方法后为12.91MB,效果还是不错的。去除的内容就是一些调试信息及堆栈行号。
不过老师在Sample的proguard-rules.pro中添加了-keepattributes SourceFile,LineNumberTable保留了行号信息。
所以处理后的包安装后进入首页,还是可以看到堆栈信息的行号。
Dex重分包的方法
redex --sign -s ReDexSample/keystore/debug.keystore -a androiddebugkey -p android -c redex-test/interdex.config -P ReDexSample/proguard-rules.pro -o redex-test/interdex_output.apk ReDexSample/build/outputs/apk/debug/ReDexSample-debug.apk
和之前的命令一样,只是-c使用的配置文件为interdex.config。
输出信息:
优化效果为原Debug包为14.21MB、3个Dex优化后为13.34MB、2个Dex。
根据老师的介绍如果你的应用有4个以上的Dex这个体积优化至少有10%。 看来效果还是很棒棒的。至于其他问题比如在Windows环境使用ReDex可以参看ReDex的使用文档。
Chapter27
利用AspectJ实现插桩的例子。
效果和Chapter07是一样的只是Chapter07使用的是ASM方式实现的这次是AspectJ实现。ASM与AspectJ都是Java字节码处理框架相比较来说AspectJ使用更加简单同样的功能实现只需下面这点代码但是ASM比AspectJ更加高效和灵活。
AspectJ实现代码
@Aspect
public class TraceTagAspectj {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@Before("execution(* **(..))")
public void before(JoinPoint joinPoint) {
Trace.beginSection(joinPoint.getSignature().toString());
}
/**
* hook method when it's called out.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@After("execution(* **(..))")
public void after() {
Trace.endSection();
}
简单介绍下上面代码的意思:
@Aspect在编译时AspectJ会查找被@Aspect注解的类然后执行我们的AOP实现。
@Before:可以简单理解为方法执行前。
@After:可以简单理解为方法执行后。
execution方法执行。
* **(..)第一个星号代表任意返回类型第二个星号代表任意类第三个代表任意方法括号内为方法参数无限制。星号和括号内都是可以替换为具体值比如String TestClass.test(String)。
知道了相关注解的含义,那么实现的代码含义就是,所有方法在执行前后插入相应指定操作。
效果对比如下:
-
下来实现给MainActivity的onResume方法增加try catch。
@Aspect
public class TryCatchAspect {
@Pointcut("execution(* com.sample.systrace.MainActivity.onResume())") // <- 指定类与方法
public void methodTryCatch() {
}
@Around("methodTryCatch()")
public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
// try catch
try {
joinPoint.proceed(); // <- 调用原方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面用到了两个新注解
@Around用于替换以前的代码使用joinPoint.proceed()可以调用原方法
@Pointcut指定一个切入点
实现就是指定一个切入点利用替换原方法的思路包裹一层try catch
效果对比如下
-
当然AspectJ还有很多用法Sample中包含有AspectJ程序设计指南》,便于我们具体了解和学习AspectJ
Chapter-ASM
Sample利用ASM实现了统计方法耗时和替换项目中所有的new Thread
运行项目首先要注掉ASMSample build.gradle的apply plugin: 'com.geektime.asm-plugin'和根目录build.gradle的classpath ("com.geektime.asm:asm-gradle-plugin:1.0") { changing = true }。
运行gradle task ":asm-gradle-plugin:buildAndPublishToLocalMaven"编译plugin插件编译的插件在本地.m2\repository目录下
打开第一步注掉的内容就可以运行了
实现的大致过程是先利用Transform遍历所有文件再通过ASM的visitMethod遍历所有方法最后通过AdviceAdapter实现最终的修改字节码具体实现可以看代码和练习Sample跑起来 | ASM插桩强化练习》。
效果对比
-
下面是两个练习
1.给某个方法增加try catch
这里我就给MainActivity的mm方法进行try catch实现很简单直接修改ASMCode的TraceMethodAdapter
public static class TraceMethodAdapter extends AdviceAdapter {
private final String methodName;
private final String className;
private final Label tryStart = new Label();
private final Label tryEnd = new Label();
private final Label catchStart = new Label();
private final Label catchEnd = new Label();
protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}
@Override
protected void onMethodEnter() {
if (className.equals("com/sample/asm/MainActivity") && methodName.equals("mm")) {
mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/lang/Exception");
mv.visitLabel(tryStart);
}
}
@Override
protected void onMethodExit(int opcode) {
if (className.equals("com/sample/asm/MainActivity") && methodName.equals("mm")) {
mv.visitLabel(tryEnd);
mv.visitJumpInsn(GOTO, catchEnd);
mv.visitLabel(catchStart);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/RuntimeException", "printStackTrace", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(catchEnd);
}
}
visitTryCatchBlock方法前三个参数均是Label实例其中一二表示try块的范围三则是catch块的开始位置第四个参数是异常类型其他的方法及参数就不细说了具体你可以参考ASM文档
实现类似AspectJ在方法执行开始及结束时插入我们的代码
效果我就不截图了代码如下
public void mm() {
try {
A a = new A(new B(2));
} catch (Exception e) {
e.printStackTrace();
}
}
2.查看代码中谁获取了IMEI
这个就更简单了直接寻找谁使用了TelephonyManager的getDeviceId方法并且在Sample中有答案
public class IMEIMethodAdapter extends AdviceAdapter {
private final String methodName;
private final String className;
protected IMEIMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
if (owner.equals("android/telephony/TelephonyManager") && name.equals("getDeviceId") && desc.equals("()Ljava/lang/String;")) {
Log.e("asmcode", "get imei className:%s, method:%s, name:%s", className, methodName, name);
}
}
}
Build后输出如下
总体来说ASM的上手难度还是高于AspectJ需要我们了解编译后的字节码这里所使用的功能也只是冰山一角课代表鹏飞同学推荐的ASM Bytecode Outline插件是个好帮手最后我将我练习的代码也上传到了GitHub里面还包括一份中文版的ASM文档有兴趣的同学可以下载看看
参考
练习Sample跑起来 | ASM插桩强化练
ASM文档

View File

@ -0,0 +1,182 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 热点问题答疑第1期
你好,我是专栏的“学习委员”孙鹏飞。
专栏上线以来很多同学反馈说在运行练习Sample的时候遇到问题。由于这些Sample多是采用C/C++来完成的所以在编译运行上会比传统的纯Java项目稍微复杂一些。今天我就针对第1期第4期中同学们集中遇到的问题做一期答疑。设置练习的目的也是希望你在学习完专栏的内容后可以快速上手试验一下专栏所讲的工具或方法帮你加快掌握技术的精髓。所以希望各位同学可以多参与进来有任何问题也可以在留言区给我们反馈后面我还会不定期针对练习再做答疑。
编译环境配置
首先是同学们问得比较多的运行环境问题。
前几期的练习Sample大多是使用C/C++开发的所以要运行起来需要先配置好SDK和NDKSDK我们一般都是配置好的NDK环境的配置有一些特殊的地方一般我们的Sample都会使用最新的NDK版本代码可能会使用C++11/14的语法进行编写并且使用CMake进行编译我这里给出NDK环境的配置项。
首先需要去NDK官网下载最新版本下载后可以解压到合适的地方一般macOS可以存放在 ANDROID_SDK_HOME/ndk_bundle目录下Android Studio可以默认找到该目录。如果放到别的目录可能需要自己指定一下。
指定NDK目录的方法一般有下面两种。
1.在练习Sample根目录下都会有一个local.properties文件修改其中的ndk.dir路径即可。
ndk.dir=/Users/sample/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/sample/Library/Android/sdk
2.可以在Android Studio里进行配置打开File -> Project Structure -> SDK Location进行修改。
上面两种修改方法效果是一致的。
有些Sample需要降级NDK编译使用可能需要下载旧版本的NDK可以从官网下载。
之后需要安装CMake和LLDB。
CMake一款外部构建工具可与Gradle搭配使用来构建原生库。
LLDB一种调试程序Android Studio使用它来调试原生代码。
这两项都可以在Tools > Android > SDK Manager里进行安装。
这样我们编译所需要的环境就配置好了。
热点问题答疑
01 | 崩溃优化(上):关于“崩溃”那些事儿
关于第1期的Sample同学们遇到的最多的问题是使用模拟器运行无法获取Crash日志的问题。
引起这个问题的缘由比较深层最直观的原因是使用Clang来编译x86平台下的Breakpad会导致运行出现异常从而导致无法抓取日志。想要解决这个问题我们需要先来了解一下NDK集成的编译器。
NDK集成了两套编译器GCC和Clang。从NDK r11开始官方就建议使用Clang详情可以看ChangeLog并且标记GCC为Deprecated并且从GCC 4.8升级到4.9以后就不再进行更新了。NDK r13开始默认使用Clang。NDK r16b以后的版本貌似强制开启GCC会引起错误并将libc++作为默认的STL而NDK r18干脆就完全删除了GCC。
由于Clang的编译会引起x86的Breakpad执行异常所以我们需要切换到GCC下进行编译步骤如下。
1.首先将NDK切换到r16b你可以从这里下载在里面找到对应你操作系统平台的NDK版本。
2.在Android Studio里设置NDK路径为ndk-16b的路径。
3.在练习例子源码的sample和breakpad-build的build.gradle配置里进行如下配置。
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
arguments "-DANDROID_TOOLCHAIN=gcc"
}
}
第二个问题是日志解析工具如何获取。
解析Minidump日志主要是使用minidump_stackwalk工具配合使用的工具是dump_syms这个工具可以获取一个so文件的符号表。
这两项工具需要通过编译Breakpad来获取有部分同学查到的文章是采用Chrome团队的depot_tools来进行工具的源码下载、编译操作。depot_tools是个很好用的工具但是在国内其服务器是无法访问的所以我们采用直接下载源码编译的方式相对来说比较方便。
编译Breakpad有一些需要注意的地方由于Android平台的内核是LinuxAndroid里的动态链接库的符号表导出工具dump_syms需要运行在Linux下暂时没有找到交叉编译在别的平台上的办法所以下面的步骤都是在Linux环境Ubuntu 18.04)下进行的,步骤如下。
1.先下载源码。
2.由于源码里没有附带上一些第三方的库所以现在编译会出现异常我们需要下载lss库到Breakpad源码目录src/third_party下面。
git clone https://chromium.googlesource.com/linux-syscall-support
3.然后在源码目录下执行。
./configure && make
make install
这样我们就可以直接调用minidump_stackwalk、dump_syms工具了。
第三个问题是如何解析抓取下来的Minidump日志。
生成的Crash信息如果授予Sdcard权限会优先存放在/sdcard/crashDump下便于我们做进一步的分析。反之会放到目录/data/data/com.dodola.breakpad/files/crashDump下。
你可以通过adb pull命令拉取日志文件。
adb pull /sdcard/crashDump/
1.首先我们需要从产生Crash的动态库中提取出符号表以第1期的Sample为例产生Crash的动态库obj路径在Chapter01/sample/build/intermediates/cmake/debug/obj下。
此处需要注意一下手机平台按照运行Sample时的平台取出libcrash-lib.so库进行符号表的dump然后调用dump_syms工具获取符号表。
dump_syms libcrash-lib.so > libcrash-lib.so.sym
2.建立符号表目录结构。首先打开刚才生成的libcrash-lib.so.syms找到如下编码。
MODULE Linux arm64 322FCC26DA8ED4D7676BD9A174C299630 libcrash-lib.so
然后建立如下结构的目录Symbol/libcrash-lib.so/322FCC26DA8ED4D7676BD9A174C299630/将libcrash-lib.so.sym文件复制到该文件夹中。注意目录结构不能有错否则会导致符号表对应失败。
3.完成上面的步骤后就可以来解析Crash日志了执行minidump_stackwalk命令。
minidump_stackwalk crash.dmp ./Symbol > dump.txt
4.这样我们获取的crash日志就会有符号表了对应一下之前没有符号表时候的日志记录。
5.如果我们没有原始的obj那么需要通过libcrash-lib.so的导出符号来进行解析这里用到的工具是addr2line工具这个工具存放在$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line下。你要注意一下平台如果是解析64位的动态库需要使用aarch64-linux-android-4.9下的addr2line此处是64位的
aarch64-linux-android-addr2line -f -C -e libcrash-lib.so 0x5f8
Java_com_dodola_breakpad_MainActivity_crash
6.可以使用GDB来根据Minidump调试出问题的动态库这里就不展开了你可以参考这里。
03 | 内存优化4GB内存时代再谈内存优化
针对这一期的Sample很多同学询问Sample中经常使用的Hook框架的原理。
Sample中使用的Hook框架有两种一种是Inline Hook方案Substrate和HookZz一种是PLT Hook方案Facebook Hook这两种方案各有优缺点根据要实现功能的不同采取不同的框架。
PLT Hook相对Inline Hook的方案要稳定很多但是它操作的范围只是针对出现在PLT表中的动态链接函数而Inline Hook可以hook整个so里的所有代码。Inline Hook由于要针对各个平台进行指令修复操作所以稳定性和兼容性要比PLT Hook差很多。
关于PLT Hook的内容你可以看一下《程序员的自我修养链接、装载与库》这本书而Inline Hook则需要对ARM、x86汇编以及各个平台下的过程调用标准Procedure Call Standard有很深入的了解。
第3期里还有部分同学询问Sample中的函数符号是如何来的。
首先如果我们要hook一个函数需要知道这个函数的地址。在Linux下我们获取函数的地址可以通过dlsym函数来根据名字获取动态库里的函数名称一般都会通过Name Mangling技术来生成一个符号名称具体细节可以看这篇文章所以第3期的Sample里出现了很多经过转换的函数名。
void *hookRecordAllocation26 = ndk_dlsym(handle,
"_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPNS_6ObjPtrINS_6mirror6ObjectEEEj");
void *hookRecordAllocation24 = ndk_dlsym(handle, "_ZN3art2gc20AllocRecordObjectMap16RecordAllocationEPNS_6ThreadEPPNS_6mirror6ObjectEj");
这样的函数可以通过c++filt工具来进行反解我在这里给你提供一个网页版的解析工具。
我们需要阅读系统源码来寻找Hook点比如第3期里Hook的方法都是虚拟机内存分配相关的函数。需要注意的一点是要先确认是否存在该函数的符号很多时候由于强制Inline的函数或者过于短小的函数可能没有对应的符号这时候就需要使用objdump、readelf、nm或者各种disassembly工具进行查看根据类名、函数名查找一下有没有对应的符号。
总结
第1期的Breakpad的Sample主要是展示Native Crash的日志如何获取和解读。根据业务的不同我们平时接触的很多都是Java的异常在业务不断稳定、代码异常处理逐渐完善的情况下Java异常的量会逐渐减少而Native Crash的问题会逐步的显现出来。一般比较大型的应用都会或多或少包含一些Native库比如加密、地图、日志、Push等模块由于多方面的原因这些代码会产生一些异常我们需要了解Crash日志来排查解决又或者说绕过这些异常进而提高应用的稳定性。
通过Breakpad的源码以帮你了解到信号捕获、ptrace的使用、进程fork/clone机制、主进程子进程通信、unwind stack、system info的获取、memory maps info的获取、symbol的dump以及symbol反解等通过源码我们可以学习到很多东西。
第2期的Sample提供了解决系统异常的一种思路使用反射或者代理机制来解决系统代码中的异常。需要说明的是FinalizerWatchdog机制并不是系统异常而是系统的一种防护机制。很多时候我们会遇到一些系统Framework的bug产生的Crash比如很常见的Toast异常等这些异常虽然不属于本应用产生的但也会影响用户的使用解决这种异常可以考虑一下这个Sample中的思路。
第3期的Sample描述了一个简单的Memory Allocation Trace监控模块这个模块主要是配合自动性能分析体系来自动发现问题比如大对象的分配数量监控、分配对象的调用栈分析等。它可以做的事很多同学们可以根据这个思路根据自己的业务来开发适合自己的工具。
从第3期的Sample的代码你可以学习到Inline Hook Substrate框架的使用使用ndk_dlopen来绕过Android Classloader-Namespace Restriction机制以及C++里的线程同步等。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@ -0,0 +1,118 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 热点问题答疑第2期
你好我是孙鹏飞。今天我们基于专栏第5期的练习Sample以及热点问题我来给你做答疑。有关上一期答疑你可以点击这里查看。
为了让同学们可以进行更多的实践专栏第5期Sample采用了让你自己实现部分功能的形式希望可以让你把专栏里讲的原理可以真正用起来。
前面几期已经有同学通过Pull request提交了练习作业这里要给每位参与练习、提交作业的同学点个赞。
第5期的作业是根据系统源码来完成一个CPU数据的采集工具并且在结尾我们提供了一个案例让你进行分析。我已经将例子的实现提交到了GitHub上你可以参考一下。
在文中提到“当发生ANR的时候Android系统会打印CPU相关的信息到日志中使用的是ProcessCpuTracker.java”。ProcessCpuTracker的实现主要依赖于Linux里的/proc伪文件系统in-memory pseudo-file system主要使用到了/proc/stat、/proc/loadavg、/proc/[pid]/stat、/proc/[pid]/task相关的文件来读取数据。在Linux中有很多程序都依赖/proc下的数据比如top、netstat、ifconfig等Android里常用的procrank、librank、procmem等也都以此作为数据来源。关于/proc目录的结构在Linux Man Pages里有很详细的说明在《Linux/Unix系统编程手册》这本书里也有相关的中文说明。
关于proc有一些需要说明的地方在不同的Linux内核中该目录下的内容可能会有所不同所以如果要使用该目录下的数据可能需要做一些版本上的兼容处理。并且由于Linux内核更新速度较快文档的更新可能还没有跟上这就会导致一些数据和文档中说明的不一致尤其是大量的以空格隔开的数字数据。这些文件其实并不是真正的文件你用ls查看会发现它们的大小都是0这些文件都是系统虚拟出来的读取这些文件并不会涉及文件系统的一系列操作只有很小的性能开销而现阶段并没有类似文件系统监听文件修改的回调所以需要采用轮询的方式来进行数据采集。
下面我们来看一下专栏文章结尾的案例分析。下面是这个示例的日志数据,我会通过分析数据来猜测一下是什么原因引起,并用代码还原这个情景。
usage: CPU usage 5000ms(from 23:23:33.000 to 23:23:38.000):
System TOTAL: 2.1% user + 16% kernel + 9.2% iowait + 0.2% irq + 0.1% softirq + 72% idle
CPU Core: 8
Load Average: 8.74 / 7.74 / 7.36
Process:com.sample.app
50% 23468/com.sample.app(S): 11% user + 38% kernel faults:4965
Threads:
43% 23493/singleThread(R): 6.5% user + 36% kernel faults3094
3.2% 23485/RenderThread(S): 2.1% user + 1% kernel faults329
0.3% 23468/.sample.app(S): 0.3% user + 0% kernel faults6
0.3% 23479/HeapTaskDaemon(S): 0.3% user + 0% kernel faults982
\.\.\.
上面的示例展示了一段在5秒时间内CPU的usage的情况。初看这个日志你可以收集到几个重要信息。
1.在System Total部分user占用不多CPU idle很高消耗多在kernel和iowait。
2.CPU是8核的Load Average大约也是8表示CPU并不处于高负载情况。
3.在Process里展示了这段时间内sample app的CPU使用情况user低kernel高并且有4965次page faults。
4.在Threads里展示了每个线程的usage情况当前只有singleThread处于R状态并且当前线程产生了3096次page faults其他的线程包括主线程Sample日志里可见的都是处于S状态。
根据内核中的线程状态的宏的名字和缩写的对应R值代表线程处于Running或者Runnable状态。Running状态说明线程当前被某个Core执行Runnable状态说明线程当前正在处于等待队列中等待某个Core空闲下来去执行。从内核里看两个状态没有区别线程都会持续执行。日志中的其他线程都处于S状态S状态代表TASK_INTERRUPTIBLE发生这种状态是线程主动让出了CPU如果线程调用了sleep或者其他情况导致了自愿式的上下文切换Voluntary Context Switches就会处于S状态。常见的发生S状态的原因可能是要等待一个相对较长时间的I/O操作或者一个IPC操作如果一个I/O要获取的数据不在Buffer Cache或者Page Cache里就需要从更慢的存储设备上读取此时系统会把线程挂起并放入一个等待I/O完成的队列里面在I/O操作完成后产生中断线程重新回到调度序列中。但只根据文中这个日志并不能判定是何原因所引起的。
还有就是SingleThread的各项指标都相对处于一个很高的情况而且产生了一些faults。page faluts分为三种minor page fault、major page fault和invalid page fault下面我们来具体分析。
minor page fault是内核在分配内存的时候采用一种Lazy的方式申请内存的时候并不进行物理内存的分配直到内存页被使用或者写入数据的时候内核会收到一个MMU抛出的page fault此时内核才进行物理内存分配操作MMU会将虚拟地址和物理地址进行映射这种情况产生的page fault就是minor page fault。
major page fault产生的原因是访问的内存不在虚拟地址空间也不在物理内存中需要从慢速设备载入或者从Swap分区读取到物理内存中。需要注意的是如果系统不支持zRAM来充当Swap分区可以默认Android是没有Swap分区的因为在Android里不会因为读取Swap而发生major page fault的情况。另一种情况是mmap一个文件后虚拟内存区域、文件磁盘地址和物理内存做一个映射在通过地址访问文件数据的时候发现内存中并没有文件数据进而产生了major page fault的错误。
根据page fault发生的场景虚拟页面可能有四种状态
第一种,未分配;
第二种,已经分配但是未映射到物理内存;
第三种,已经分配并且已经映射到物理内存;
第四种已经分配并映射到Swap分区在Android中此种情况基本不存在
通过上面的讲解并结合page fault数据你可以看到SingleThread你一共发生了3094次fault根据每个页大小为4KB可以知道在这个过程中SingleThread总共分配了大概12MB的空间。
下面我们来分析iowait数据。既然有iowait的占比就说明在5秒内肯定进行了I/O操作并且iowait占比还是比较大的说明当时可能进行了大量的I/O操作或者当时由于其他原因导致I/O操作缓慢。
从上面的分析可以猜测一下具体实现,并且在读和写的时候都有可能发生。由于我的手机写的性能要低一些,比较容易复现,所以下面的代码基于写操作实现。
File f = new File(getFilesDir(), "aee.txt");
FileOutputStream fos = new FileOutputStream(f);
byte[] data = new byte[1024 * 4 * 3000];//此处分配一个12mb 大小的 byte 数组
for (int i = 0; i < 30; i++) {//由于 IO cache 机制的原因所以此处写入多次cache触发 dirty writeback 到磁盘中
Arrays.fill(data, (byte) i);//当执行到此处的时候产生 minor fault并且产生 User cpu useage
fos.write(data);
}
fos.flush();
fos.close();
上面的代码抓取到的CPU数据如下
E/ProcessCpuTracker: CPU usage from 5187ms to 121ms ago (2018-12-28 08:28:27.186 to 2018-12-28 08:28:32.252):
40% 24155/com.sample.processtracker(R): 14% user + 26% kernel / faults: 5286 minor
thread stats:
35% 24184/SingleThread(S): 11% user + 24% kernel / faults: 3055 minor
2.1% 24174/RenderThread(S): 1.3% user + 0.7% kernel / faults: 384 minor
1.5% 24155/.processtracker(R): 1.1% user + 0.3% kernel / faults: 95 minor
0.1% 24166/HeapTaskDaemon(S): 0.1% user + 0% kernel / faults: 1070 minor
100% TOTAL(): 3.8% user + 7.8% kernel + 11% iowait + 0.1% irq + 0% softirq + 76% idle
Load: 6.31 / 6.52 / 6.66
可以对比Sample中给出的数据基本一致
通过上面的说明你可以如法炮制去分析ANR日志中相关的数据来查找性能瓶颈比如如果产生大量的major page fault其实是不太正常的或者iowait过高就需要关注是否有很密集的I/O操作
相关资料
低内存配置
iowait的形成原因和内核分析
page fault带来的性能问题
Linux工具快速教程
Android: memory management insights, part I
Linux 2.6调度系统分析
性能之巅
欢迎你点击请朋友读”,把今天的内容分享给好友邀请他一起学习

View File

@ -0,0 +1,98 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 热点问题答疑第3期
你好我是孙鹏飞。又到了答疑的时间今天我将围绕卡顿优化这个主题和你探讨一下专栏第6期和补充篇的两个Sample的实现。
专栏第6期的Sample完全来自于Facebook的性能分析框架Profilo主要功能是收集线上用户的atrace日志。关于atrace相信我们都比较熟悉了平时经常使用的systrace工具就是封装了atrace命令来开启ftrace事件并读取ftrace缓冲区生成可视化的HTML日志。这里多说一句ftrace是Linux下常用的内核跟踪调试工具如果你不熟悉的话可以返回第6期文稿最后查看ftrace的介绍。Android下的atrace扩展了一些自己使用的categories和tag这个Sample获取的就是通过atrace的同步事件。
Sample的实现思路其实也很简单有两种方案。
第一种方案hook掉atrace写日志时的一系列方法。以Android 9.0的代码为例写入ftrace日志的代码在trace-dev.cpp里由于每个版本的代码有些区别所以需要根据系统版本做一些区分。
第二种方案也是Sample里所使用的方案由于所有的atrace event写入都是通过/sys/kernel/debug/tracing/trace_markeratrace在初始化的时候会将该路径fd的值写入atrace_marker_fd全局变量中我们可以通过dlsym轻易获取到这个fd的值。关于trace_maker这个文件我需要说明一下这个文件涉及ftrace的一些内容ftrace原来是内核的事件trace工具并且ftrace文档的开头已经写道
Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on inside the kernel.
从文档中可以看出来ftrace工具主要是用来探查outside of user-space的性能问题。不过在很多场景下我们需要知道user space的事件调用和kernel事件的一个先后关系所以ftrace也提供了一个解决方法也就是提供了一个文件trace_marker往该文件中写入内容可以产生一条ftrace记录这样我们的事件就可以和kernel的日志拼在一起。但是这样的设计有一个不好的地方在往文件写入内容的时候会发生system call调用有系统调用就会产生用户态到内核态的切换。这种方式虽然没有内核直接写入那么高效但在很多时候ftrace工具还是很有用处的。
由此可知用户态的事件数据都是通过trace_marker写入的更进一步说是通过write接口写入的那么我们只需要hook住write接口并过滤出写入这个fd下的内容就可以了。这个方案通用性比较高而且使用PLT Hook即可完成。
下一步会遇到的问题是想要获取atrace的日志就需要设置好atrace的category tag才能获取到。我们从源码中可以得知判断tag是否开启是通过atrace_enabled_tags & tag来计算的如果大于0则认为开启等于0则认为关闭。下面我贴出了部分atrace_tag的值你可以看到判定一个tag是否是开启的只需要tag值的左偏移数的位值和atrace_enabled_tags在相同偏移数的位值是否同为1。其实也就是说我将atrace_enabled_tags的所有位都设置为1那么在计算时候就能匹配到任何的atrace tag。
#define ATRACE_TAG_NEVER 0
#define ATRACE_TAG_ALWAYS (1<<0)
#define ATRACE_TAG_GRAPHICS (1<<1)
#define ATRACE_TAG_INPUT (1<<2)
#define ATRACE_TAG_VIEW (1<<3)
#define ATRACE_TAG_WEBVIEW (1<<4)
#define ATRACE_TAG_WINDOW_MANAGER (1<<5)
#define ATRACE_TAG_ACTIVITY_MANAGER (1<<6)
#define ATRACE_TAG_SYNC_MANAGER (1<<7)
#define ATRACE_TAG_AUDIO (1<<8)
#define ATRACE_TAG_VIDEO (1<<9)
#define ATRACE_TAG_CAMERA (1<<10)
#define ATRACE_TAG_HAL (1<<11)
#define ATRACE_TAG_APP (1<<12)
下面是我用atrace抓下来的部分日志。
看到这里有同学会问Begin和End是如何对应上的呢要回答这个问题首先要先了解一下这种记录产生的场景。这个日志在Java端是由Trace.traceBegin和Trace.traceEnd产生的在使用上有一些硬性要求这两个方法必须成对出现否则就会造成日志的异常。请看下面的系统代码示例。
void assignWindowLayers(boolean setLayoutNeeded) {
2401 Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "assignWindowLayers");//关注此处事件开始代码
2402 assignChildLayers(getPendingTransaction());
2403 if (setLayoutNeeded) {
2404 setLayoutNeeded();
2405 }
2406
2411 scheduleAnimation();
2412 Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);//事件结束
2413 }
2414
所以我们可以认为B下面紧跟的E就是事件的结束标志但很多情况下我们会遇到上面日志中所看到的两个B连在一起紧跟的两个E我们不知道分别对应哪个B。此时我们需要看一下产生事件的CPU是哪个并且看一下产生事件的task_pid是哪个也就是最前面的InputDispatcher-1944这样我们就可以对应出来了。
接下来我们一起来看看补充篇的Sample它的目的是希望让你练习一下如何监控线程创建并且打印出创建线程的Java方法。Sample的实现比较简单主要还是依赖PLT Hook来hook线程创建时使用的主要函数pthread_create。想要完成这个Sample你需要知道Java线程是如何创建出来的并且还要理解Java线程的执行方式。需要特别说明的是其实这个Sample也存在一个缺陷。从虚拟机的角度看线程其实又分为两种一种是Attached线程我习惯按照.Net的叫法称其为托管线程一种是Unattached线程为非托管线程。但底层都是依赖POSIX Thread来实现的从pthread_create里无法区分该线程是否是托管线程也有可能是Native直接开启的线程所以有可能并不能对应到创建线程时候的Java Stack。
关于线程我们在日常监控中可能并不太关心线程创建时候的状况而区分线程可以通过提前设置Thread Name来实现。举个例子比如在出现OOM时发现是发生在pthread_create执行的时候说明当前线程数可能过多一般我们会在OOM的时候采集当前线程数和线程堆栈信息可以看一下是哪个线程创建过多如果指定了线程名称则很快就能查找出问题所在。
对于移动端的线程来说我们大多时候更关心的是主线程的执行状态。因为主线程的任何耗时操作都会影响操作界面的流畅度所以我们经常把看起来比较耗时的操作统统都往子线程里面丢虽然这种操作虽然有时候可能很有效但还可能会产生一些我们平时很少遇到的异常情况。比如我曾经遇到过由于用户手机的I/O性能很低大量的线程都在wait io或者线程开启的太多导致线程Context switch过高又或者是一个方法执行过慢导致持有锁的时间过长其他线程无法获取到锁等一系列异常的情况
虽然线程的监控很不容易但并不是不能实现只是实现起来比较复杂并且要考虑兼容性。比如我们可能比较关心一个Lock当前有多少线程在等待锁释放就需要先获取到这个Object的MirrorObject然后构造一个MonitorInfo之后获取到waiters的列表而这个列表里就存储了等待锁释放的线程。你看其实过程也并不复杂只是在计算地址偏移量的时候需要做一些处理。
当然还有更细致的优化比如我们都知道Java里是有轻量级锁和重量级锁的一个转换过程在ART虚拟机里被称为ThinLocked和FatLocked而转换过程是通过Monitor::Inflate和Monitor::Deflate函数来实现的。此时我们可以监控Monitor::Inflate调用时monitor指向的Object来判断是哪段代码产生了“瘦锁”到“胖锁”转换的过程从而去做一些优化。接下来要做优化需要先知晓ART虚拟机锁转换的机制如果当前锁是瘦锁持有该锁的线程再一次获取这个锁只递增了lock count并未改变锁的状态。但是lock count超过4096则会产生瘦锁到胖锁的转换如果当前持有该锁的线程和进入MontorEnter的线程不是同一个的情况下就会产生锁争用的情况。ART虚拟机为了减少胖锁的产生做了一些优化虚拟机先通过sched_yield让出当前线程的执行权操作系统在后面的某个时间再次调度该线程执行从调用sched_yield到再次执行的时候计算时间差在这个时间差里占用该锁的线程可能会释放对锁的占用那么调用线程会再次尝试获取锁如果获取锁成功的话则会从 Unlocked状态直接转换为ThinLocked状态不会产生FatLocked状态。这个过程持续50次如果在50次循环内无法获取到锁则会将瘦锁转为胖锁。如果我们对某部分的多线程代码性能敏感则希望锁尽量持续在瘦锁的状态我们可以减少同步块代码的粒度尽量减少很多线程同时争抢锁可以监控Inflate函数调用情况来判断优化效果。
最后还有同学对在Crash状态下获取Java线程堆栈的方法比较感兴趣我在这里简单讲一下后面会有专门的文章介绍这部分内容。
一种方案是使用ThreadList::ForEach接口间接实现具体的逻辑可以看这里。另一种方案是 Profilo里的Unwinder机制这种实现方式就是模拟StackVisitor的逻辑来实现。
这两期反馈的问题不多答疑的内容也可以算作对正文的补充如果有同学想多了解虚拟机的机制或者其他性能相关的问题欢迎你给我留言我也会在后面的文章和你聊聊这些话题比如有同学问到的ART下GC的详细逻辑之类的问题。
相关资料
ftrace kernel doc
ftrace的使用
A look at ftrace
福利彩蛋
今天为认真提交作业完成练习的同学,送出第二波“学习加油礼包”。@Seven同学提交了第5期的作业送出“极客周历”一本其他同学如果完成了练习千万别忘了通过Pull request提交哦。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。

View File

@ -0,0 +1,45 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
练习Sample跑起来 热点问题答疑第4期
你好我是孙鹏飞。今天我们回到专栏第7期和第8期来看看课后练习Sample的运行需要注意哪些问题。另外我结合同学们留言的疑问也来谈谈文件顺序对I/O的影响以及关于Linux学习我的一些方法和建议。
专栏第7期的Sample借助于systrace工具通过字节码处理框架对函数插桩来获取方法执行的trace。这个Sample实现相当完整你在日常工作也可以使用它。
这个Sample使用起来虽然非常简单但其内部的实现相对来说是比较复杂的。它的实现涉及Gradle Transform、Task实现、增量处理、ASM字节码处理、mapping文件使用以及systrace工具的使用等。
对于Gradle来说我们应该比较熟悉它是Android平台下的构建工具。对于平时使用来说我们大多时候只需要关注Android Gradle Plugin的一些参数配置就可以实现很多功能了官方文档已经提供了很详细的参数设置说明。对于一些需要侵入打包流程的操作就需要我们实现自己的Task或者Transform代码来完成比如处理Class和JAR包、对资源做一些处理等。
Gradle学习的困难更多来自于Android Gradle Plugin对Gradle做的一些封装扩展而这部分Google并没有提供很完善的文档并且每个版本都有一些接口上的变动。对于这部分内容的学习我主要是去阅读别人实现的Gradle工具代码和Android Gradle Plugin代码。
关于这期的Sample实现有几个可能产生疑问的地方我们来探讨一下。
这个Sample的Gradle插件是发布到本地Maven库的所以如果没有执行发布直接编译需要先发布插件库到本地Maven中才能执行编译成功。
另一个可能遇到问题的是如果你想把Sample使用到其他项目需要自己将SampleApp中其p的e.systrace.TraceTag”类移植到自己的项目中否则会产生编译错误。
对于字节码处理在Sample中主要使用了ASM框架来处理。市面上关于字节码处理的框架有很多常见的有ASM和Javassist框架其他的框架你可以使用“Java bytecode manipulation”关键字在Google上搜索。使用字节码处理框架需要对字节码有比较深入的了解要提醒你的是这里的字节码不是Dalvik bytecode而是Java bytecode。对于字节码的学习你可以参考官方文档和《Java虚拟机规范》里面对字节码的执行规则和指令说明都有很详细的描述。并且还可以配合javap命令查看反编译的字节码对应的源码这样学习下来会有很好的效果。字节码处理是一个很细微的操作稍有失误就会产生编译错误、执行错误或者Crash的情况里面需要注意的地方也非常多比如Try Catch Block对操作数栈的影响、插入的代码对本地变量表和操作数栈的影响等。
实现AOP的另一种方是可以接操作Dex文件进行Dalvik bytecode字节码注入关于这种实现方式可以使用dexer库来完成在Facebook的Redex中也提供了针对dex的AOP功能。
下面我们来看专栏第8期。我从文章留言里看到有同学关于数据重排序对I/O性能的影响有些疑问不太清楚优化的原理。其实这个优化原理理解起来是很容易的在进行文件读取的操作过程中系统会读取比预期更多的文件内容并缓存在Page Cache中这样下一次读请求到来时部分页面直接从Page Cache读取而不用再从磁盘中获取数据这样就加速了读取的操作。在《支付宝App构建优化解析》里“原理”一节中已经有比较详细的描述我就不多赘述了。如果你对“预读”感兴趣的话我给你提供一些资料可以深入了解一下。
预读readhead机制的系统源码在readhead.c文件中。需要说明的是预读机制可能在不同系统版本中有所变化所以下面我提供的资料大多是基于 Linux 2.6.x的内核在这以后的系统版本可能对 readhead 机制有修改,你需要留意一下。
关于预读机制详细的算法说明可以看《Linux readahead: less tricks for more》和《Sequential File Prefetching In Linux》、《Linux内核的文件预读readahead》 这三篇文档。
从专栏前几篇的正文看很多优化的内容是从Linux的机制入手的如果你对Linux的机制和优化不了解的话是不太容易想到这些方案的。举个例子专栏文章提到的小文件系统是运行在用户态的代码底层依然依赖现存文件系统提供的功能因此需要深入了解Linux VFS、ext4的实现以及它们的优缺点和原理这样我们才能发现为什么大量的小文件依赖现存的文件系统管理是存在性能缺陷的以及下一步如何填补这些性能缺陷。
作为Android开发工程师我们该何学习Linux呢我其实不建议上来就直接阅读系统源码分析相关的书我建议是从理解操作系统概念开始推荐两本操作系统相关的书《深入理解计算机系统》和《计算机系统 系统架构与操作系统的高度集成》。Linux的系统实现其实和传统的操作系统概念在细节上会有不小的差别再推荐一本解析Linux操作系统的书《操作系统之编程观察》这本书结合源码对Linux的各方面机制都进行和很详细的分析。
对于从事Android开发的同学来说确实很有必要深入了解Linux系统相关的知识因为Android里很多特性都是依赖底层基础系统的就比如我刚刚提到的“预读”机制不光可以用在Android的资源加载上也可以拓展到Flutter的资源加载上。假如我们以后面对一个不是Linux内核的系统比如Fuchsia OS也可以根据已经掌握的系统知识套用到现有的操作系统上因为像内存管理、文件系统、信号机制、进程调度、系统调用、中断机制、驱动等内容都是共通的在迁移到新的系统上的时候可以有一个全局的学习视角帮助我们快速上手。对于操作系统内容我的学习路线是先熟悉系统机制然后熟悉系统提供的各个方向的接口比如I/O操作、进程创建、信号中断处理、线程使用、epoll、通信机制等按照《UNIX环境高级编程》这本书的内容一步步的走就可以完成这一步骤熟悉之后可以按照自己的节奏再去学习自己比较感兴趣的模块。此时可以找一本源码分析的书再去阅读比如想了解fork机制的实现、I/O操作的read和write在内核态的调度执行像这些问题就需要有目的性的进行挖掘。
上面这个学习路线是我在学习过程中不断踩坑总结出来的一些经验,对于操作系统我也只是个初学者,也欢迎你留言说说自己学习的经验和问题,一起切磋进步。
最后送出3本“极客周历”给用户故事“专栏学得苦可能是方法没找对”留言点赞数前三的同学分别是@坚持远方@蜗牛@JIA,感谢同学们的参与。

View File

@ -0,0 +1,61 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 移动开发的今天和明天
不知不觉“Android开发高手课”上线更新到现在已经陪伴了大家140多天转眼间到了不得不说“再见”的时候。回想半年前开始筹备这个专栏时当时焦虑和浮躁是移动开发者比较普遍的心态而作为移动开发者的一份子希望通过这个专栏可以分享一些我的经验帮你克服焦虑拥抱未来无限可能。
我的专栏希望通过质量、效率以及架构三个角度来说明一个移动开发高手应该具备的特质。“高质量开发”模块希望让你知道Android开发并不是那么简单一个Android高手需要具备从上层到底层的“通关”能力。Flutter就是一个非常好的例子这里建议你回顾一下《想成为Android高手你需要搞定这三个问题》。
稳定性、内存、网络、渲染、I/O…你可以选择其中一两个点深入往底层走走打造自身的技术壁垒当团队内部如果遇到相关问题比如虚拟机、渲染、数据库的时候第一时间就会想到你来解决。
而“高效开发”模块希望告诉你一个开发高手不仅仅有“深度”,还需要有“广度”。移动开发早已经过了“单兵作战”的年代了,服务化、前后端一体化越来越重要,这里也推荐你回顾一下《做一名有高度的移动开发工程师》。
这里要求我们学会跳出客户端的范畴,具备全链路优化的思维。一个真正的开发高手应该不仅能把性能优化多少倍,而且还能带动整个团队提升开发效率。
最后一个模块是“架构演进”我希望告诉你架构是为性能和效率服务的。无论是H5、React Native/Weex、小程序、Flutter还是类似游戏、音视频、AI这些移动开发“新大陆”它们都是殊途同归。
对于移动开发的今日,在学完专栏以后,我希望你能体会到下面这两点:
移动开发并不简单。想成为一名移动开发高手并不简单,它需要非常深入地学习。当然学完专栏几十篇文章,你也不可能立刻就成为了一名高手,俗话说“师傅领进门,修行靠个人”,在专栏以外你还需要进一步深入学习更多的知识,积累更丰富的经验。
移动开发生命力依然顽强。移动开发很有前途而且这个领域不仅局限于Android开发。我们的出路其实有很多很多只要我们肯拿出折腾Android的热情用从上到下“通关”的钻研精神无论是转向iOS还是游戏、前端等其他的领域我相信绝对都是“降维打击”。
但是不可否认,如今的移动开发已经不再是“风口上的猪”,对于初级移动开发者的岗位越来越少,大厂的要求也随之变得越来越严格。类似微信、头条、阿里,他们都对候选人在算法和底层基础知识考究得非常深入。
但是我相信只要你认真学习完这个专栏,对面试大厂会有非常大的帮助。专栏里提到的很多知识点,目前其实很多大厂也还做得不够完善。正如前面所说,你可以找其中一两个点作为进入大厂的突破口。
而对于Android的未来个人认为会有下面两个变化
合规和用户隐私越发重要。无论被称为史上最严厉的欧盟GDPR法令还是Android Q用户唯一标识、权限等最新限制都可以清楚看到未来对用户隐私和权限更严格限制的趋势。
中国的声音会越来越大。以往我们总说Copy to China现在更多的是Copy from China。无论是Google还是苹果都无法忽视我们13亿人的巨大市场。比如说Android Q就为了中国的热修复框架专门提供新的接口。
对于移动开发的未来可能未来的新设备将颠覆手机也许是嵌入式设备、智能眼镜、车载设备等但这些设备仍然还是移动设备。从当下的技术发展来看我很期待今年的Facebook F8和Google I/O大会我今年也会去现场观摩欢迎面基Facebook今年计划开源很多好东西例如React Native新版本、启动优化测量等。
“三星凉了宝洁药丸诺基亚衰落苹果下凡国外品牌都挺惨…”但现实情况却是三星依然在全球称霸。在我看来“Android开发要完移动开发要完”也是这个道理。我们不应该焦虑也不需要浮躁没有不过时的技术不过时的只有学习能力和态度。
“唯有学习不可辜负”。Android开发的路很长也很宽我们既可以往底层走也能往大中台走还可以去看看IoT、游戏、AI。未来的机会还有很多很多只要我们找准方向积极拥抱变化、迎接挑战每个人都可以走出一条自己的“光明大道”。
不过在工作学习之余,我也奉劝各位同学一定要锻炼好身体,无论什么时候身体都是第一位。我们的职业生涯非常漫长,脑力、体力、心力三者缺一不可。
非常感谢订阅专栏和我一路走来的同学们,因为你们的鼓励让我充满激情,驱使我呈现出最好的内容。最后祝愿你们未来无论是从事哪方面的工作,都能成为一名真正的高手。
到了说再见的时候了各位潜水的同学都来冒个泡吧我希望听听你这几个月学习的心得和收获自己在工作和生活有什么变化2019年的Flag完成得如何了我们可以在专栏里畅所欲言、把酒言欢。

View File

@ -0,0 +1,164 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
聊聊Framework的学习方法
大家好我是陆晓明现在在一家互联网手机公司担任Android系统开发工程师。很高兴可以在极客时间Android开发高手课专栏里分享一些我在手机行业9年的经验以及学习Android的方法。
今天我要跟你分享的是Framework的学习和调试的方法。
首先Android是一种基于Linux的开放源代码软件栈为广泛的设备和机型而创建。下图是Android平台的主要组件。
从图中你可以看到主要有以下几部分组成:
Linux内核
Android Runtime
原生C/C++库
Java API框架后面我称之为Framework框架层
系统应用
我们在各个应用市场看到的大多是第三方应用也就是安装在data区域的应用它们可以卸载并且权限也受到一些限制比如不能直接设置时间日期需要调用到系统应用设置里面再进行操作。
而我们在应用开发过程中使用的四大组件便是在Framework框架层进行实现应用通过约定俗成的规则在AndroidMainfest.xml中进行配置然后继承对应的基类进行复写。系统在启动过程中解析AndroidMainfest.xml将应用的信息存储下来随后根据用户的操作或者系统的广播触发启动对应的应用。
那么我们先来看看Framework框架层都有哪些东西。
Framework框架层是应用开发过程中调用的系统方法的内部实现比如我们使用的TextView、Button控件都是在这里实现的。再举几个例子我们调用ActivityManager的getRunningAppProcesses方法查看当前运行的进程列表还有我们使用NotificationManager的notify发送一个系统通知。
让我们来看看Framework相关的代码路径。
如何快速地学习、梳理Framework知识体系呢常见的学习方法有下面几种
阅读书籍(方便梳理知识体系,但对于解决问题只能提供方向)。
直接阅读源码(效率低,挑战难度大)。
打Log和打堆栈 效率有所提升但需要反复编译添加Log和堆栈代码
直接联调,实时便捷(需要调试版本)。
首先可以通过购买相关的书籍进行学习其中主要的知识体系有Linux操作系统比如进程、线程、进程间通信、虚拟内存建立起自己的软件架构。在此基础上学习Android的启动过程、服务进程SystemServer的创建、各个服务线程AMS/PMS等的创建过程以及Launcher的启动过程。熟悉了这些之后你还要了解ART虚拟机的主要工作原理以及init和Zygote的主要工作原理。之后随着在工作和实践过程中你会发现Framework主要是围绕应用启动、显示、广播消息、按键传递、添加服务等开展这些代码的实现主要使用的是Java和C++这两种语言。
通过书籍或者网络资料学习一段时间后你会发现很多问题都没有现成的解决方案而此时就需要我们深入源码中进行挖掘和学习。但是除了阅读官方文档外别忘了调试Framework也是一把利刃可以让你游刃有余快速定位和分析源码。
下面我们来看看调试Framework的Java部分关于C++的部分需要使用GDB进行调试你可以在课下实践一下调试的过程可以参考《深入Android源码系列》。
我们这里使用Android Studio进行调试在调试前我们要先掌握一些知识。Java代码的调试主要依据两个因素一个是你要调试的进程一个是调试的类对应的包名路径同时还要保证你所运行的手机环境和你要调试的代码是匹配的。只要这两个信息匹配编译不通过也是可以进行调试的。
我们调试的系统服务是在SystemServer进程中可以使用下面的命令验证我这里使用Genymotion上安装Android对应版本镜像的环境演示
ps -A |grep system_server 查看系统服务进程pid
cat /proc/pid/maps |grep services 通过cat查看此进程的内存映射看看是否services映射到内存里面。
这里我们看到信息:/system/framework/oat/x86/services.odex 。
odex是Android系统对于dex的进一步优化目的是为了提升执行效率。从这个信息便可以确定我们的services.jar确实是跑到这里了也就是我们的系统服务相关联的代码可以通过调试SystemServer进程进行跟踪。
下来我们来建立调试环境。
打开Genymotion选择下载好Android 9.0的镜像文件,启动模拟器。
找到模拟器对应的ActivityManagerService.java代码。 我是从http://androidxref.com/下载Android 9.0对应的代码。
打开Android StudioFile -> New -> New Project然后直接Next直到完成就行。
新建一个包名从ActivityManagerService.java文件中找到它这里为com.android.server.am然后把ActivityManagerService.java放到里面即可。
在ActivityManagerService.java的startActivity方法上面设置断点然后找到菜单的Run -> Attach debugger to Android process勾选Show all process选中SystemServer进程确定。
这时候我们点击Genymotion模拟器中桌面的一个图标启动新的界面。
会发现这时候我们设定的断点已经生效。
你可以看到断下来的堆栈信息,以及一些变量值,然后我们可以一步步调试下去,跟踪启动的流程。
对于学习系统服务线程来讲,通过调试可以快速掌握流程,再结合阅读源码,便可以快速学习,掌握系统框架的整个逻辑,从而节省学习的时间成本。
以上我们验证了系统服务AMS服务代码的调试其他服务调试方法也是一样具体的线程信息可以使用下面的命令查看。
ps -T 353
这里353是使用ps -A |grep SystemServer查出 SystemServer的进程号
在上面图中PID = TID的只有第一行这一行如果PID = TID的话也就是这个线程是主线程。下面是我们平时使用Logcat查看输出的信息。
03-10 09:33:01.804 240 240 I hostapd : type=1400 audit(0.0:1123): avc: de
03-10 09:33:37.320 353 1213 D WificondControl: Scan result ready event
03-10 09:34:00.045 404 491 D hwcomposer: hw_composer sent 6 syncs in 60s
这里我还框了一个ActivityManager的线程这个是线程的名称通过查看这行的TID368就知道下面的Log就是这个线程输出的。
03-10 08:47:33.574 353 368 I ActivityManager: Force stopping com.android.providers
学习完上面的知识,相信你应该学会了系统服务的调试。通过调试分析,我们便可以将系统服务框架进行庖丁解牛般的学习,面对大量庞杂的代码掌握起来也可以轻松一些。
我们回过头来再次在终端中输入ps -A看看下面这一段信息。
你可以看到这里的第一列代表的是当前的用户这里有system root和u0_axx不同的用户有不同的权限。我们当前关注的是第二列和第三列第二列代表的是PID也就是进程ID第三列代表的是PPID也就是父进程ID。
你发现我这里框住的都是同一个父进程那么我们来找下这个323进程看看它到底是谁。
root 323 1 1089040 127540 poll_schedule_timeout f16fcbc9 S zygote
这个名字在学习Android系统的时候总被反复提及因为它是我们Android世界的孵化器每一个上层应用的创建都是通过Zygote调用fork创建的子进程而子进程可以快速继承父进程已经加载的资源库这里主要指的是应用所需的JAR包比如/system/framework/framework.jar因为我们应用所需的基础控件都在这里像View、TextView、ImageView。
接下来我来讲解下一个调试也就是对TextView的调试其他Button调试方式一样。如前面所说这个代码被编译到/system/framework/framework.jar那么我们通过ps命令和cat /proc/pid/maps命令在Zygote中找到它同时它能够被每一个由Zygote创建的子进程找到比如我们当前要调试Gallery的主界面TextView。
我们验证下使用ps -A |grep gallery3d查到Gallery对应的进程PID使用cat /proc/pid/maps |grep framework.jar看到如下信息
efcd5000-efcd6000 r--s 00000000 08:06 684 /system/framework/framework.jar
这说明我们要调试的应用进程在内存映射中确实存在那么我们就需要在gallery3d进程中下断点了。
下来我们建立调试环境:
打开Genymotion选择下载好Android 9.0的镜像文件启动模拟器然后在桌面上启动Gallery图库应用。
找到模拟器对应的TextView.java代码。
打开Android StudioFile -> New -> New Project然后直接Next直到完成就行。
新建一个包名从TextView.java文件中找到它的包名这里为android.widget然后把TextView.java放到里面即可。
在TextView.java的onDraw方法上面设置断点然后找到菜单的Run -> Attach debugger to Android process勾选Show all process选中com.android.gallery3d进程我们已知这个主界面有TextView控件确定。
然后我们点击下这个界面左上角的菜单,随便选择一个点击,发现断点已生效,具体如下图所示。
然后我们可以使用界面上的调试按钮(或者快捷键)进行调试代码。
今天我讲解了如何调试Framework中的系统服务进程的AMS服务线程其他PMS、WMS的调试方法跟AMS一样。并且我也讲解了如何调试一个应用里面的TextView控件其他的比如Button、ImageView调试方法跟TextView也是一样的。
通过今天的学习,我希望能够给你一个学习系统框架最便捷的路径。在解决系统问题的时候,你可以方便的使用调试分析,从而快速定位、修复问题。

View File

@ -0,0 +1,163 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 第一堂“云原生”课
本节课程要点
云原生技术发展历程(为什么要学习这门课)
课程简介与预备知识(这门课到底教什么)
云原生的定义与技术要点(本节正式内容)
为什么要开设云原生技术公开课?
云原生技术发展简史
首先从第一个问题进行分享那就是“为什么要开设云原生技术公开课”云原生、CNCF 都是目前非常热门的关键词,但是这些技术并不是非常新鲜的内容。
2004 年— 2007 年Google 已在内部大规模地使用像 Cgroups 这样的容器技术;
2008 年Google 将 Cgroups 合并进入了 Linux 内核主干;
2013 年Docker 项目正式发布。
2014 年Kubernetes 项目也正式发布。这样的原因也非常容易理解,因为有了容器和 Docker 之后,就需要有一种方式去帮助大家方便、快速、优雅地管理这些容器,这就是 Kubernetes 项目的初衷。在 Google 和 Redhat 发布了 Kubernetes 之后,这个项目的发展速度非常之快。
2015 年由Google、Redhat 以及微软等大型云计算厂商以及一些开源公司共同牵头成立了 CNCF 云原生基金会。CNCF 成立之初,就有 22 个创始会员,而且 Kubernetes 也成为了 CNCF 托管的第一个开源项目。在这之后CNCF 的发展速度非常迅猛;
2017 年CNCF 达到 170 个成员和 14 个基金项目;
2018 年CNCF 成立三周年有了 195 个成员19 个基金会项目和 11 个孵化项目,如此之快的发展速度在整个云计算领域都是非常罕见的。
云原生技术生态现状
因此如今我们所讨论的云原生技术生态是一个庞大的技术集合。CNCF 有一张云原生全景图https://github.com/cncf/landscape在这个全景图里已经有 200 多个项目和产品了,这些项目和产品也都是和 CNCF 的观点所契合的。所以如果以这张全景图作为背景,加以思考就会发现,我们今天所讨论的云原生其实主要谈论了以下几点:
云原生基金会 —— CNCF
云原生技术社区,比如像 CNCF 目前正式托管的 20 多个项目共同构成了现代云计算生态的基石,其中像 Kubernetes 这样的项目已经成为了世界第四活跃的开源项目;
除了前面两点之外,现在全球各大公有云厂商都已经支持了 Kubernetes。此外还有 100 多家技术创业公司也在持续地进行投入。现在阿里巴巴也在谈全面上云,而且上云就要上云原生,这也是各大技术公司拥抱云原生的一个例子。
我们正处于时代的关键节点
2019 年正是云原生时代的关键节点,为什么这么说?我们这里就为大家简单梳理一下。 从 2013 年 Docker 项目发布开始说起Docker 项目的发布使得全操作系统语义的沙盒技术唾手可得,使得用户能够更好地、更完整地打包自己的应用,使得开发者可以轻而易举的获得了一个应用的最小可运行单位,而不需要依赖任何 PaaS 能力。这对经典 PaaS 产业其实是一个“降维打击”。 2014 年的时候Kubernetes 项目发布,其意义在于 Google 将内部的 Borg/Omega 系统思想借助开源社区实现了“重生”,并且提出了“容器设计模式”的思想。而 Google 之所以选择间接开源 Kubernetes 而不是直接开源 Borg 项目其实背后的原因也比较容易理解Borg/Omega 这样的系统太复杂了,是没办法提供给 Google 之外的人使用,但是 Borg/Omega 这样的设计思想却可以借助 Kubernetes 让大家接触到,这也是开源 Kubernetes 的重要背景。 这样到了 2015 年到 2016 年,就到了容器编排“三国争霸”的时代,当时 Docker、Swarm、Mesos、Kubernetes 都在容器编排领域展开角逐,他们竞争的原因其实也比较容易理解, 那就是 Docker 或者容器本身的价值虽然大,但是如果想要让其产生商业价值或者说对云的价值,那么就一定需要在编排上面占据一个有利的位置。 Swarm 和 Mesos 的特点那就是各自只在生态和技术方面比较强其中Swarm 更偏向于生态,而 Mesos 技术更强一些。相比之下, Kubernetes 则兼具了两者优势,最终在 2017 年“三国争霸”的局面中得以胜出,成为了当时直到现在的容器编排标准。这一过程的代表性事件就是 Docker 公司宣布在核心产品中内置了 Kubernetes 服务,并且 Swarm 项目逐渐停止维护。 到了 2018 年的时候,云原生技术理念开始逐渐萌芽,这是因为此时 Kubernetes 以及容器都成为了云厂商的既定标准,以“云”为核心的软件研发思想逐步形成。 而到了 2019 年,情况似乎又将发生一些变化。
2019 年——云原生技术普及元年
为什么说 2019 年很可能是一个关键节点呢?我们认为 2019 年是云原生技术的普及元年。 首先大家可以看到,在 2019 年,阿里巴巴宣布要全面上云,而且“上云就要上云原生”。我们还可以看到,以“云”为核心的软件研发思想,正逐步成为所有开发者的默认选项。像 Kubernetes 等云原生技术正在成为技术人员的必修课,大量的工作岗位正在涌现出来。
这种背景下,“会 Kubernetes”已经远远不够了“懂 Kubernetes”、“会云原生架构”的重要性正日益凸显出来。 从 2019 年开始,云原生技术将会大规模普及,这也是为什么大家都要在这个时间点上学习和投资云原生技术的重要原因。
“云原生技术公开课”是一门怎样的课程?
基于上面所提到的技术趋势,所以阿里巴巴和 CNCF 联合开设了云原生技术公开课。 那么这样的公开课到底在讲什么内容呢?
公开课教学大纲
第一期云原生公开课的教学大纲,主要以应用容器和 Kubernetes 为核心,在后面几期将会陆续上线 Service Mesh、Serverless 等相关课程。 在第一期公开课中,我们首先将课程分为两部分——基础知识部分和进阶知识部分:
首先,我们希望通过第一部分的课程讲解帮助大家夯实基础。然后,对于更高阶的内容展开更深入的代码级别的剖析。希望通过这样循序渐进的方式帮助大家学习云原生技术;
其次,在每个课程后面我们的讲师都会设置对应的课后自测考试题,这些考试题实际上是对本节课程最有效的归纳,我们希望能够通过课后评测的方式来帮助大家总结知识点,打造出属于自己的云原生知识体系;
最后,我们的讲师在每个知识点的背后都设计了云端实践,所谓“实践出真知”,学习计算机相关的知识还是需要上手来实际地进行操作才可以。 因此在云端实践部分,讲师会提供详细的实践步骤供大家课后自我联系。并且在这个环节,阿里云还会赠送了定量的阿里云代金券帮助大家更好地在云上进行实践。
以上三个部分就构成了阿里云和 CNCF 联合推出的云原生技术公开课的教学内容。
公开课授课计划
在授课计划方面,初步这样安排:第一堂课在 2019 年 9 月上线此后将会每周更新2节课总共 29 个课时。每个知识点后面都提供了课后自测。 对于讲师阵容而言,也是本次公开课最引以为傲的部分。我们的公开课将会主要由 CNCF 社区资深成员与项目维护者为大家讲解,很多课程讲师都是阿里云容器平台团队的专家级工程师。同时,我们也会邀请云原生社区的资深专家和外部讲师为大家讲解部分内容。因此在课程进行过程中,我们会不定期地安排大咖直播、课程答疑和落地实践案例。 我们希望将这些内容都集成在一起,为大家呈现一个中国最完整、最权威、最具有影响力的云原生技术公开课。
课程预备知识
大家可能存在这样的疑惑,就是想要学习云原生基础知识之前需要哪些预备知识呢?其实大致需要三部分预备知识:
Linux 操作系统知识:主要是一些通识性的基础,最好具有一定的在 Linux 下开发的经验;
计算机和程序设计的基础:这一点到入门工程师或者高年级本科生水平就足够了;
容器的使用基础:希望大家具有容器的简单使用经验,比如 docker run 以及 docker build 等,最好有一定 Docker 化应用开发的经验。当然,我们在课程中也会讲解相关的基础知识。
什么是“云原生”?云原生该怎么落地?
在介绍完课程之后,我们再来详细的聊一聊“云原生”:什么是“云原生”?云原生该怎么落地?这两个问题也是整个课程的核心内容。
云原生的定义
很多人都会问“到底什么是云原生?” 实际上,云原生是一条最佳路径或者最佳实践。更详细的说,云原生为用户指定了一条低心智负担的、敏捷的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径。 因此,云原生其实是一套指导进行软件架构设计的思想。按照这样的思想而设计出来的软件:首先,天然就“生在云上,长在云上”;其次,能够最大化地发挥云的能力,使得我们开发的软件和“云”能够天然地集成在一起,发挥出“云”的最大价值。 所以,云原生的最大价值和愿景,就是认为未来的软件,会从诞生起就生长在云上,并且遵循一种新的软件开发、发布和运维模式,从而使得软件能够最大化地发挥云的能力。说到了这里,大家可以思考一下为什么容器技术具有革命性?
其实,容器技术和集装箱技术的革命性非常类似,即:容器技术使得应用具有了一种“自包含”的定义方式。所以,这样的应用才能以敏捷的、以可扩展可复制的方式发布在云上,发挥出云的能力。这也就是容器技术对云发挥出的革命性影响所在,所以说,容器技术正是云原生技术的核心底盘。
云原生的技术范畴
云原生的技术范畴包括了以下几个方面:
第一部分是云应用定义与开发流程。这包括应用定义与镜像制作、配置 CI/CD、消息和 Streaming 以及数据库等。
第二部分是云应用的编排与管理流程。这也是 Kubernetes 比较关注的一部分包括了应用编排与调度、服务发现治理、远程调用、API 网关以及 Service Mesh。
第三部分是监控与可观测性。这部分所强调的是云上应用如何进行监控、日志收集、Tracing 以及在云上如何实现破坏性测试,也就是混沌工程的概念。
第四部分就是云原生的底层技术,比如容器运行时、云原生存储技术、云原生网络技术等。
第五部分是云原生工具集,在前面的这些核心技术点之上,还有很多配套的生态或者周边的工具需要使用,比如流程自动化与配置管理、容器镜像仓库、云原生安全技术以及云端密码管理等。
最后则是 Serverless。Serverless 是一种 PaaS 的特殊形态,它定义了一种更为“极端抽象”的应用编写方式,包含了 FaaS 和 BaaS 这样的概念。而无论是 FaaS 还是 BaaS其最为典型的特点就是按实际使用计费Pay as you go因此 Serverless 计费也是重要的知识和概念。
云原生思想的两个理论
在了解完云原生的技术范畴之后你就会发现,其所包含的技术内容还是很多的,但是这些内容的技术本质却是类似的。云原生技术的本质是两个理论基础。
第一个理论基础是:不可变基础设施。这一点目前是通过容器镜像来实现的,其含义就是应用的基础设施应该是不可变的,是一个自包含、自描述可以完全在不同环境中迁移的东西;
第二个理论基础就是:云应用编排理论。当前的实现方式就是 Google 所提出来的“容器设计模式”,这也是本系列课程中的 Kubernetes 部分所需主要讲解的内容。
基础设施向云演进的过程
首先为大家介绍一下“不可变基础设施”的概念。其实,应用所依赖的基础设施也在经历一个向云演进的过程,举例而言,对于传统的应用基础设施而言,其实往往是可变的。
大家可能经常会干这样一件事情,比如需要发布或者更新一个软件,那么流程大致是这样的,先通过 SSH 连到服务器,然后手动升级或者降级软件包,逐个调整服务器上的配置文件,并且将新代码直接都部署到现有服务器上。因此,这套基础设施会不断地被调整和修改。 但是在云上,对“云”友好的应用基础设施是不可变的。
这种场景下的上述更新过程会这么做:一旦应用部署完成之后,那么这套应用基础设施就不会再修改了。如果需要更新,那么需要现更改公共镜像来构建新服务直接替换旧服务。而我们之所以能够实现直接替换,就是因为容器提供了自包含的环境(包含应用运行所需的所有依赖)。所以对于应用而言,完全不需要关心容器发生了什么变化,只需要把容器镜像本身修改掉就可以了。因此,对于云友好的基础设施是随时可以替换和更换的,这就是因为容器具有敏捷和一致性的能力,也就是云时代的应用基础设施。 所以,总结而言,云时代的基础设施就像是可以替代的“牲口”,可以随时替换;而传统的基础设施则是独一无二的“宠物”,需要细心呵护,这就体现出了云时代不可变基础设施的优点。
基础设施向云演进的意义
所以,像这样的基础设施向“不可变”演进的过程,为我们提供了两个非常重要的优点。
1、基础设施的一致性和可靠性。同样一个镜像无论是在美国打开在中国打开还是在印度打开都是一样的。并且其中的 OS 环境对于应用而言都是一致的。而对于应用而言,它就不需要关心容器跑在哪里,这就是基础设施一致性非常重要的一个特征。
2、这样的镜像本身就是自包含的其包含了应用运行所需要的所有依赖因此也可以漂移到云上的任何一个位置。
此外,云原生的基础设施还提供了简单、可预测的部署和运维能力。由于现在有了镜像,应用还是自描述的,通过镜像运行起来的整个容器其实可以像 Kubernetes 的 Operator 技术一样将其做成自运维的,所以整个应用本身都是自包含的行为,使得其能够迁移到云上任何一个位置。这也使得整个流程的自动化变得非常容易。
应用本身也可以更好地扩容,从 1 个实例变成 100 个实例,进而变成 1 万个实例,这个过程对于容器化后的应用没有任何特殊的。最后,我们这时也能够通过不可变的基础设施来地快速周围的管控系统和支撑组件。因为,这些组件本身也是容器化的,是符合不可变基础设施这样一套理论的组件。 以上就是不可变基础设施为用户带来的最大的优点。
云原生关键技术点
当我们回过头来看云原生关键技术点或者说它所依赖的技术理论的时候,可以看到主要有这样的四个方向:
如何构建自包含、可定制的应用镜像;
能不能实现应用快速部署与隔离能力;
应用基础设施创建和销毁的自动化管理;
可复制的管控系统和支撑组件。
这四个云原生关键技术点是落地实现云原生技术的四个主要途径,而这四个技术点也是本门课程的 17 个技术点所主要讲述的核心知识。
本节总结
“云原生”具备着重要的意义,它是云时代技术人自我提升的必备路径;
“云原生”定义了一条云时代应用从开发到交付的最佳路径;
“云原生”应用生在云上,长在云上,希望能够将云的能力发挥到极致。
讲师点评
“未来的软件一定是生长于云上的”这是云原生理念的最核心假设。而所谓“云原生”,实际上就是在定义一条能够让应用最大程度利用云的能力、发挥云的价值的最佳路径。在这条路径上,脱离了“应用”这个载体,“云原生”就无从谈起;容器技术,则是将这个理念落地、将软件交付的革命持续进行下去的重要手段之一。
而本期云原生公开课重点讲解的 Kubernetes 项目,则是整个“云原生”理念落地的核心与关键所在。它正在迅速成为连通“云”与“应用”的高速公路,以标准、高效的方式将“应用”快速交付到世界上任何一个位置。如今”云原生应用交付“,已经成为了 2019 年云计算市场上最热门的技术关键词之一。希望学习课程的同学们能够学以致用,持续关注以 K8s 为基础进行“云原生应用管理与交付”的技术趋势。

View File

@ -0,0 +1,143 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 容器基本概念
容器与镜像
什么是容器?
在介绍容器的具体概念之前,先简单回顾一下操作系统是如何管理进程的。
首先,当我们登录到操作系统之后,可以通过 ps 等操作看到各式各样的进程,这些进程包括系统自带的服务和用户的应用进程。那么,这些进程都有什么样的特点?
第一,这些进程可以相互看到、相互通信;
第二,它们使用的是同一个文件系统,可以对同一个文件进行读写操作;
第三,这些进程会使用相同的系统资源。
这样的三个特点会带来什么问题呢?
因为这些进程能够相互看到并且进行通信,高级权限的进程可以攻击其他进程;
因为它们使用的是同一个文件系统,因此会带来两个问题:这些进程可以对于已有的数据进行增删改查,具有高级权限的进程可能会将其他进程的数据删除掉,破坏掉其他进程的正常运行;此外,进程与进程之间的依赖可能会存在冲突,如此一来就会给运维带来很大的压力;
因为这些进程使用的是同一个宿主机的资源,应用之间可能会存在资源抢占的问题,当一个应用需要消耗大量 CPU 和内存资源的时候,就可能会破坏其他应用的运行,导致其他应用无法正常地提供服务。
针对上述的三个问题,如何为进程提供一个独立的运行环境呢?
针对不同进程使用同一个文件系统所造成的问题而言Linux 和 Unix 操作系统可以通过 chroot 系统调用将子目录变成根目录,达到视图级别的隔离;进程在 chroot 的帮助下可以具有独立的文件系统,对于这样的文件系统进行增删改查不会影响到其他进程;
因为进程之间相互可见并且可以相互通信,使用 Namespace 技术来实现进程在资源的视图上进行隔离。在 chroot 和 Namespace 的帮助下,进程就能够运行在一个独立的环境下了;
但在独立的环境下,进程所使用的还是同一个操作系统的资源,一些进程可能会侵蚀掉整个系统的资源。为了减少进程彼此之间的影响,可以通过 Cgroup 来限制其资源使用率,设置其能够使用的 CPU 以及内存量。
那么,应该如何定义这样的进程集合呢?
其实,容器就是一个视图隔离、资源可限制、独立文件系统的进程集合。所谓“视图隔离”就是能够看到部分进程以及具有独立的主机名等;控制资源使用率则是可以对于内存大小以及 CPU 使用个数等进行限制。容器就是一个进程集合,它将系统的其他资源隔离开来,具有自己独立的资源视图。
容器具有一个独立的文件系统,因为使用的是系统的资源,所以在独立的文件系统内不需要具备内核相关的代码或者工具,我们只需要提供容器所需的二进制文件、配置文件以及依赖即可。只要容器运行时所需的文件集合都能够具备,那么这个容器就能够运行起来。
什么是镜像?
综上所述,我们将这些容器运行时所需要的所有的文件集合称之为容器镜像。
那么,一般都是通过什么样的方式来构建镜像的呢?通常情况下,我们会采用 Dockerfile 来构建镜像,这是因为 Dockerfile 提供了非常便利的语法糖,能够帮助我们很好地描述构建的每个步骤。当然,每个构建步骤都会对已有的文件系统进行操作,这样就会带来文件系统内容的变化,我们将这些变化称之为 changeset。当我们把构建步骤所产生的变化依次作用到一个空文件夹上就能够得到一个完整的镜像。 changeset 的分层以及复用特点能够带来几点优势:
第一,能够提高分发效率,简单试想一下,对于大的镜像而言,如果将其拆分成各个小块就能够提高镜像的分发效率,这是因为镜像拆分之后就可以并行下载这些数据;
第二,因为这些数据是相互共享的,也就意味着当本地存储上包含了一些数据的时候,只需要下载本地没有的数据即可,举个简单的例子就是 golang 镜像是基于 alpine 镜像进行构建的,当本地已经具有了 alpine 镜像之后,在下载 golang 镜像的时候只需要下载本地 alpine 镜像中没有的部分即可;
第三,因为镜像数据是共享的,因此可以节约大量的磁盘空间,简单设想一下,当本地存储具有了 alpine 镜像和 golang 镜像在没有复用的能力之前alpine 镜像具有 5M 大小golang 镜像有 300M 大小,因此就会占用 305M 空间;而当具有了复用能力之后,只需要 300M 空间即可。
如何构建镜像?
如下图所示的 Dockerfile 适用于描述如何构建 golang 应用的。
如图所示:
FROM 行表示以下的构建步骤基于什么镜像进行构建,正如前面所提到的,镜像是可以复用的;
WORKDIR 行表示会把接下来的构建步骤都在哪一个相应的具体目录下进行,其起到的作用类似于 Shell 里面的 cd
COPY 行表示的是可以将宿主机上的文件拷贝到容器镜像内;
RUN 行表示在具体的文件系统内执行相应的动作。当我们运行完毕之后就可以得到一个应用了;
CMD 行表示使用镜像时的默认程序名字。
当有了 Dockerfile 之后,就可以通过 docker build 命令构建出所需要的应用。构建出的结果存储在本地,一般情况下,镜像构建会在打包机或者其他的隔离环境下完成。
那么,这些镜像如何运行在生产环境或者测试环境上呢?这时候就需要一个中转站或者中心存储,我们称之为 docker registry也就是镜像仓库其负责存储所有产生的镜像数据。我们只需要通过 docker push 就能够将本地镜像推动到镜像仓库中,这样一来,就能够在生产环境上或者测试环境上将相应的数据下载下来并运行了。
如何运行容器?
运行一个容器一般情况下分为三步:
第一步:从镜像仓库中将相应的镜像下载下来;
第二步:当镜像下载完成之后就可以通过 docker images 来查看本地镜像,这里会给出一个完整的列表,我们可以在列表中选中想要的镜像;
第三步:当选中镜像之后,就可以通过 docker run 来运行这个镜像得到想要的容器,当然可以通过多次运行得到多个容器。一个镜像就相当于是一个模板,一个容器就像是一个具体的运行实例,因此镜像就具有了一次构建、到处运行的特点。
小结
简单回顾一下,容器就是和系统其它部分隔离开来的进程集合,这里的其他部分包括进程、网络资源以及文件系统等。而镜像就是容器所需要的所有文件集合,其具备一次构建、到处运行的特点。
容器的生命周期
容器运行时的生命周期
容器是一组具有隔离特性的进程集合,在使用 docker run 的时候会选择一个镜像来提供独立的文件系统并指定相应的运行程序。这里指定的运行程序称之为 initial 进程,这个 initial 进程启动的时候,容器也会随之启动,当 initial 进程退出的时候,容器也会随之退出。
因此,可以认为容器的生命周期和 initial 进程的生命周期是一致的。当然,因为容器内不只有这样的一个 initial 进程initial 进程本身也可以产生其他的子进程或者通过 docker exec 产生出来的运维操作,也属于 initial 进程管理的范围内。当 initial 进程退出的时候,所有的子进程也会随之退出,这样也是为了防止资源的泄漏。 但是这样的做法也会存在一些问题,首先应用里面的程序往往是有状态的,其可能会产生一些重要的数据,当一个容器退出被删除之后,数据也就会丢失了,这对于应用方而言是不能接受的,所以需要将容器所产生出来的重要数据持久化下来。容器能够直接将数据持久化到指定的目录上,这个目录就称之为数据卷。
数据卷有一些特点,其中非常明显的就是数据卷的生命周期是独立于容器的生命周期的,也就是说容器的创建、运行、停止、删除等操作都和数据卷没有任何关系,因为它是一个特殊的目录,是用于帮助容器进行持久化的。简单而言,我们会将数据卷挂载到容器内,这样一来容器就能够将数据写入到相应的目录里面了,而且容器的退出并不会导致数据的丢失。
通常情况下,数据卷管理主要有两种方式:
第一种是通过 bind 的方式,直接将宿主机的目录直接挂载到容器内;这种方式比较简单,但是会带来运维成本,因为其依赖于宿主机的目录,需要对于所有的宿主机进行统一管理。
第二种是将目录管理交给运行引擎。
容器项目架构
moby 容器引擎架构
moby 是目前最流行的容器管理引擎moby daemon 会对上提供有关于容器、镜像、网络以及 Volume的管理。moby daemon 所依赖的最重要的组件就是 containerdcontainerd 是一个容器运行时管理引擎,其独立于 moby daemon ,可以对上提供容器、镜像的相关管理。
containerd 底层有 containerd shim 模块,其类似于一个守护进程,这样设计的原因有几点:
首先containerd 需要管理容器生命周期,而容器可能是由不同的容器运行时所创建出来的,因此需要提供一个灵活的插件化管理。而 shim 就是针对于不同的容器运行时所开发的,这样就能够从 containerd 中脱离出来,通过插件的形式进行管理。
其次,因为 shim 插件化的实现,使其能够被 containerd 动态接管。如果不具备这样的能力,当 moby daemon 或者 containerd daemon 意外退出的时候,容器就没人管理了,那么它也会随之消失、退出,这样就会影响到应用的运行。
最后,因为随时可能会对 moby 或者 containerd 进行升级,如果不提供 shim 机制,那么就无法做到原地升级,也无法做到不影响业务的升级,因此 containerd shim 非常重要,它实现了动态接管的能力。
本节课程只是针对于 moby 进行一个大致的介绍,在后续的课程也会详细介绍。
容器 VS VM
容器和 VM 之间的差异
VM 利用 Hypervisor 虚拟化技术来模拟 CPU、内存等硬件资源这样就可以在宿主机上建立一个 Guest OS这是常说的安装一个虚拟机。
每一个 Guest OS 都有一个独立的内核,比如 Ubuntu、CentOS 甚至是 Windows 等,在这样的 Guest OS 之下每个应用都是相互独立的VM 可以提供一个更好的隔离效果。但这样的隔离效果需要付出一定的代价,因为需要把一部分的计算资源交给虚拟化,这样就很难充分利用现有的计算资源,并且每个 Guest OS 都需要占用大量的磁盘空间,比如 Windows 操作系统的安装需要 10~30G 的磁盘空间Ubuntu 也需要 5~6G同时这样的方式启动很慢。正是因为虚拟机技术的缺点催生出了容器技术。 容器是针对于进程而言的,因此无需 Guest OS只需要一个独立的文件系统提供其所需要文件集合即可。所有的文件隔离都是进程级别的因此启动时间快于 VM并且所需的磁盘空间也小于 VM。当然了进程级别的隔离并没有想象中的那么好隔离效果相比 VM 要差很多。
总体而言,容器和 VM 相比,各有优劣,因此容器技术也在向着强隔离方向发展。
本节总结
容器是一个进程集合,具有自己独特的视图视角;
镜像是容器所需要的所有文件集合,其具备一次构建、到处运行的特点;
容器的生命周期和 initial 进程的生命周期是一样的;
容器和 VM 相比,各有优劣,容器技术在向着强隔离方向发展。

View File

@ -0,0 +1,308 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 Kubernetes 核心概念
什么是 Kubernetes
Kubernetes从官方网站上可以看到它是一个工业级的容器编排平台。Kubernetes 这个单词是希腊语它的中文翻译是“舵手”或者“飞行员”。在一些常见的资料中也会看到“ks”这个词也就是“k8s”它是通过将8个字母“ubernete ”替换为“8”而导致的一个缩写。
Kubernetes 为什么要用“舵手”来命名呢?大家可以看一下这张图:
这是一艘载着一堆集装箱的轮船,轮船在大海上运着集装箱奔波,把集装箱送到它们该去的地方。我们之前其实介绍过一个概念叫做 containercontainer 这个英文单词也有另外的一个意思就是“集装箱”。Kubernetes 也就借着这个寓意,希望成为运送集装箱的一个轮船,来帮助我们管理这些集装箱,也就是管理这些容器。
这个就是为什么会选用 Kubernetes 这个词来代表这个项目的原因。更具体一点地来说Kubernetes 是一个自动化的容器编排平台,它负责应用的部署、应用的弹性以及应用的管理,这些都是基于容器的。
Kubernetes 有如下几个核心的功能
服务的发现与负载的均衡;
容器的自动装箱,我们也会把它叫做 scheduling就是“调度”把一个容器放到一个集群的某一个机器上Kubernetes 会帮助我们去做存储的编排,让存储的声明周期与容器的生命周期能有一个连接;
Kubernetes 会帮助我们去做自动化的容器的恢复。在一个集群中,经常会出现宿主机的问题或者说是 OS 的问题导致容器本身的不可用Kubernetes 会自动地对这些不可用的容器进行恢复;
Kubernetes 会帮助我们去做应用的自动发布与应用的回滚,以及与应用相关的配置密文的管理;
对于 job 类型任务Kubernetes 可以去做批量的执行;
为了让这个集群、这个应用更富有弹性Kubernetes 也支持水平的伸缩。
下面,我们希望以三个例子跟大家更切实地介绍一下 Kubernetes 的能力。
调度
Kubernetes 可以把用户提交的容器放到 Kubernetes 管理的集群的某一台节点上去。Kubernetes 的调度器是执行这项能力的组件,它会观察正在被调度的这个容器的大小、规格。
比如说它所需要的 CPU以及它所需要的 memory然后在集群中找一台相对比较空闲的机器来进行一次 placement也就是一次放置的操作。在这个例子中它可能会把红颜色的这个容器放置到第二个空闲的机器上来完成一次调度的工作。
自动修复
Kubernetes 有一个节点健康检查的功能,它会监测这个集群中所有的宿主机,当宿主机本身出现故障,或者软件出现故障的时候,这个节点健康检查会自动对它进行发现。
下面 Kubernetes 会把运行在这些失败节点上的容器进行自动迁移,迁移到一个正在健康运行的宿主机上,来完成集群内容器的一个自动恢复。
水平伸缩
Kubernetes 有业务负载检查的能力,它会监测业务上所承担的负载,如果这个业务本身的 CPU 利用率过高,或者响应时间过长,它可以对这个业务进行一次扩容。
比如说在下面的例子中黄颜色的过度忙碌Kubernetes 就可以把黄颜色负载从一份变为三份。接下来,它就可以通过负载均衡把原来打到第一个黄颜色上的负载平均分到三个黄颜色的负载上去,以此来提高响应的时间。
以上就是 Kubernetes 三个核心能力的简单介绍。
Kubernetes 的架构
Kubernetes 架构是一个比较典型的二层架构和 server-client 架构。Master 作为中央的管控节点,会去与 Node 进行一个连接。
所有 UI 的、clients、这些 user 侧的组件,只会和 Master 进行连接,把希望的状态或者想执行的命令下发给 MasterMaster 会把这些命令或者状态下发给相应的节点,进行最终的执行。
Kubernetes 的 Master 包含四个主要的组件API Server、Controller、Scheduler 以及 etcd。如下图所示
API Server 顾名思义是用来处理 API 操作的Kubernetes 中所有的组件都会和 API Server 进行连接,组件与组件之间一般不进行独立的连接,都依赖于 API Server 进行消息的传送;
Controller 是控制器,它用来完成对集群状态的一些管理。比如刚刚我们提到的两个例子之中,第一个自动对容器进行修复、第二个自动进行水平扩张,都是由 Kubernetes 中的 Controller 来进行完成的;
Scheduler 是调度器,“调度器”顾名思义就是完成调度的操作,就是我们刚才介绍的第一个例子中,把一个用户提交的 Container依据它对 CPU、对 memory 请求大小,找一台合适的节点,进行放置;
etcd 是一个分布式的一个存储系统API Server 中所需要的这些原信息都被放置在 etcd 中etcd 本身是一个高可用系统,通过 etcd 保证整个 Kubernetes 的 Master 组件的高可用性。
我们刚刚提到的 API Server它本身在部署结构上是一个可以水平扩展的一个部署组件Controller 是一个可以进行热备的一个部署组件,它只有一个 active它的调度器也是相应的虽然只有一个 active但是可以进行热备。
Kubernetes 的架构Node
Kubernetes 的 Node 是真正运行业务负载的,每个业务负载会以 Pod 的形式运行。等一下我会介绍一下 Pod 的概念。一个 Pod 中运行的一个或者多个容器,真正去运行这些 Pod 的组件的是叫做 kubelet也就是 Node 上最为关键的组件,它通过 API Server 接收到所需要 Pod 运行的状态,然后提交到我们下面画的这个 Container Runtime 组件中。
在 OS 上去创建容器所需要运行的环境,最终把容器或者 Pod 运行起来也需要对存储跟网络进行管理。Kubernetes 并不会直接进行网络存储的操作,他们会靠 Storage Plugin 或者是网络的 Plugin 来进行操作。用户自己或者云厂商都会去写相应的 Storage Plugin 或者 Network Plugin去完成存储操作或网络操作。
在 Kubernetes 自己的环境中,也会有 Kubernetes 的 Network它是为了提供 Service network 来进行搭网组网的。等一下我们也会去介绍“service”这个概念。真正完成 service 组网的组件的是 Kube-proxy它是利用了 iptable 的能力来进行组建 Kubernetes 的 Network就是 cluster network以上就是 Node 上面的四个组件。
Kubernetes 的 Node 并不会直接和 user 进行 interaction它的 interaction 只会通过 Master。而 User 是通过 Master 向节点下发这些信息的。Kubernetes 每个 Node 上,都会运行我们刚才提到的这几个组件。
下面我们以一个例子再去看一下 Kubernetes 架构中的这些组件,是如何互相进行 interaction 的。
用户可以通过 UI 或者 CLI 提交一个 Pod 给 Kubernetes 进行部署,这个 Pod 请求首先会通过 CLI 或者 UI 提交给 Kubernetes API Server下一步 API Server 会把这个信息写入到它的存储系统 etcd之后 Scheduler 会通过 API Server 的 watch 或者叫做 notification 机制得到这个信息:有一个 Pod 需要被调度。
这个时候 Scheduler 会根据它的内存状态进行一次调度决策,在完成这次调度之后,它会向 API Server report 说“OK这个 Pod 需要被调度到某一个节点上。”
这个时候 API Server 接收到这次操作之后,会把这次的结果再次写到 etcd 中,然后 API Server 会通知相应的节点进行这次 Pod 真正的执行启动。相应节点的 kubelet 会得到这个通知kubelet 就会去调 Container runtime 来真正去启动配置这个容器和这个容器的运行环境,去调度 Storage Plugin 来去配置存储network Plugin 去配置网络。
这个例子我们可以看到这些组件之间是如何相互沟通相互通信协调来完成一次Pod的调度执行操作的。
Kubernetes 的核心概念与它的 API
核心概念
第一个概念Pod
Pod 是 Kubernetes 的一个最小调度以及资源单元。用户可以通过 Kubernetes 的 Pod API 生产一个 Pod让 Kubernetes 对这个 Pod 进行调度,也就是把它放在某一个 Kubernetes 管理的节点上运行起来。一个 Pod 简单来说是对一组容器的抽象,它里面会包含一个或多个容器。
比如像下面的这幅图里面,它包含了两个容器,每个容器可以指定它所需要资源大小。比如说,一个核一个 G或者说 0.5 个核0.5 个 G。
当然在这个 Pod 中也可以包含一些其他所需要的资源:比如说我们所看到的 Volume 卷这个存储资源;比如说我们需要 100 个 GB 的存储或者 20GB 的另外一个存储。
在 Pod 里面,我们也可以去定义容器所需要运行的方式。比如说运行容器的 Command以及运行容器的环境变量等等。Pod 这个抽象也给这些容器提供了一个共享的运行环境,它们会共享同一个网络环境,这些容器可以用 localhost 来进行直接的连接。而 Pod 与 Pod 之间,是互相有 isolation 隔离的。
第二个概念Volume
Volume 就是卷的概念,它是用来管理 Kubernetes 存储的,是用来声明在 Pod 中的容器可以访问文件目录的,一个卷可以被挂载在 Pod 中一个或者多个容器的指定路径下面。
而 Volume 本身是一个抽象的概念,一个 Volume 可以去支持多种的后端的存储。比如说 Kubernetes 的 Volume 就支持了很多存储插件,它可以支持本地的存储,可以支持分布式的存储,比如说像 cephGlusterFS 它也可以支持云存储比如说阿里云上的云盘、AWS 上的云盘、Google 上的云盘等等。
第三个概念Deployment
Deployment 是在 Pod 这个抽象上更为上层的一个抽象,它可以定义一组 Pod 的副本数目、以及这个 Pod 的版本。一般大家用 Deployment 这个抽象来做应用的真正的管理,而 Pod 是组成 Deployment 最小的单元。
Kubernetes 是通过 Controller也就是我们刚才提到的控制器去维护 Deployment 中 Pod 的数目,它也会去帮助 Deployment 自动恢复失败的 Pod。
比如说我可以定义一个 Deployment这个 Deployment 里面需要两个 Pod当一个 Pod 失败的时候,控制器就会监测到,它重新把 Deployment 中的 Pod 数目从一个恢复到两个,通过再去新生成一个 Pod。通过控制器我们也会帮助完成发布的策略。比如说进行滚动升级进行重新生成的升级或者进行版本的回滚。
第四个概念Service
Service 提供了一个或者多个 Pod 实例的稳定访问地址。
比如在上面的例子中,我们看到:一个 Deployment 可能有两个甚至更多个完全相同的 Pod。对于一个外部的用户来讲访问哪个 Pod 其实都是一样的,所以它希望做一次负载均衡,在做负载均衡的同时,我只想访问某一个固定的 VIP也就是 Virtual IP 地址,而不希望得知每一个具体的 Pod 的 IP 地址。
我们刚才提到,这个 pod 本身可能 terminal go终止如果一个 Pod 失败了,可能会换成另外一个新的。
对一个外部用户来讲,提供了多个具体的 Pod 地址,这个用户要不停地去更新 Pod 地址,当这个 Pod 再失败重启之后,我们希望有一个抽象,把所有 Pod 的访问能力抽象成一个第三方的一个 IP 地址,实现这个的 Kubernetes 的抽象就叫 Service。
实现 Service 有多种方式Kubernetes 支持 Cluster IP上面我们讲过的 kuber-proxy 的组网,它也支持 nodePort、 LoadBalancer 等其他的一些访问的能力。
第五个概念Namespace
Namespace 是用来做一个集群内部的逻辑隔离的它包括鉴权、资源管理等。Kubernetes 的每个资源,比如刚才讲的 Pod、Deployment、Service 都属于一个 Namespace同一个 Namespace 中的资源需要命名的唯一性,不同的 Namespace 中的资源可以重名。
Namespace 一个用例,比如像在阿里巴巴,我们内部会有很多个 business units在每一个 business units 之间,希望有一个视图上的隔离,并且在鉴权上也不一样,在 cuda 上面也不一样,我们就会用 Namespace 来去给每一个 BU 提供一个他所看到的这么一个看到的隔离的机制。
Kubernetes 的 API
下面我们介绍一下 Kubernetes 的 API 的基础知识。从 high-level 上看Kubernetes API 是由 HTTP+JSON 组成的:用户访问的方式是 HTTP访问的 API 中 content 的内容是 JSON 格式的。
Kubernetes 的 kubectl 也就是 command toolKubernetes UI或者有时候用 curl直接与 Kubernetes 进行沟通,都是使用 HTTP + JSON 这种形式。
下面有个例子:比如说,对于这个 Pod 类型的资源,它的 HTTP 访问的路径,就是 API然后是 apiVesion: V1, 之后是相应的 Namespaces以及 Pods 资源,最终是 Podname也就是 Pod 的名字。
如果我们去提交一个 Pod或者 get 一个 Pod 的时候,它的 content 内容都是用 JSON 或者是 YAML 表达的。上图中有个 yaml 的例子,在这个 yaml file 中,对 Pod 资源的描述也分为几个部分。
第一个部分,一般来讲会是 API 的 version。比如在这个例子中是 V1它也会描述我在操作哪个资源比如说我的 kind 如果是 pod在 Metadata 中,就写上这个 Pod 的名字;比如说 nginx我们也会给它打一些 label我们等下会讲到 label 的概念。在 Metadata 中,有时候也会去写 annotation也就是对资源的额外的一些用户层次的描述。
比较重要的一个部分叫做 SpecSpec 也就是我们希望 Pod 达到的一个预期的状态。比如说它内部需要有哪些 container 被运行;比如说这里面有一个 nginx 的 container它的 image 是什么?它暴露的 port 是什么?
当我们从 Kubernetes API 中去获取这个资源的时候,一般来讲在 Spec 下面会有一个项目叫 status它表达了这个资源当前的状态比如说一个 Pod 的状态可能是正在被调度、或者是已经 running、或者是已经被 terminates就是被执行完毕了。
刚刚在 API 之中,我们讲了一个比较有意思的 metadata 叫做“label”这个 label 可以是一组 KeyValuePair。
比如下图的第一个 pod 中label 就可能是一个 color 等于 red即它的颜色是红颜色。当然你也可以加其他 label比如说 size: big 就是大小,定义为大的,它可以是一组 label。
这些 label 是可以被 selector也就是选择器所查询的。这个能力实际上跟我们的 sql 类型的 select 语句是非常相似的,比如下图中的三个 Pod 资源中,我们就可以进行 select。name color 等于 red就是它的颜色是红色的我们也可以看到只有两个被选中了因为只有他们的 label 是红色的,另外一个 label 中写的 color 等于 yellow也就是它的颜色是黄色是不会被选中的。
通过 labelkubernetes 的 API 层就可以对这些资源进行一个筛选,那这些筛选也是 kubernetes 对资源的集合所表达默认的一种方式。
例如说,我们刚刚介绍的 Deployment它可能是代表一组的 Pod它是一组 Pod 的抽象,一组 Pod 就是通过 label selector 来表达的。当然我们刚才讲到说 service 对应的一组 Pod就是一个 service 要对应一个或者多个的 Pod来对它们进行统一的访问这个描述也是通过 label selector 来进行 select 选取的一组 Pod。
所以可以看到 label 是一个非常核心的 kubernetes API 的概念,我们在接下来的课程中也会着重地去讲解和介绍 label 这个概念,以及如何更好地去使用它。
以一个 demo 结尾
最后一部分,我想以一个例子来结束,让大家跟我一起来尝试一个 kubernetes在尝试 Kubernetes 之前,我希望大家能在本机上安装一下 Kubernetes安装一个 Kubernetes 沙箱环境。
安装这个沙箱环境,主要有三个步骤:
首先需要安装一个虚拟机,来在虚拟机中启动 Kubernetes。我们会推荐大家利用 virtualbox 来作为虚拟机的运行环境;
安装 VirtualBox https://www.virtualbox.org/wiki/Downloads
其次我们需要在虚拟机中启动 KubernetesKubernetes 有一个非常有意思的项目,叫 minikube也就是启动一个最小的 local 的 Kubernetes 的一个环境。
minikube 我们推荐使用下面写到的阿里云的版本,它和官方 minikube 的主要区别就是把 minikube 中所需要的 Google 上的依赖换成国内访问比较快的一些镜像,这样就方便了大家的安装工作;
安装 MiniKube中国版: https://yq.aliyun.com/articles/221687
最后在安装完 virtualbox 和 minikube 之后,大家可以对 minikube 进行启动,也就是下面这个命令。
启动命令minikube start —vm-driver virtualbox
如果大家不是 Mac 系统,其他操作系统请访问下面这个链接,查看其它操作系统如何安装 minikube 沙箱环境。
https://kubernetes.io/docs/tasks/tools/install-minikube/
当大家安装好之后,我会跟大家一起做一个例子,来做三件事情:
提交一个 nginx deployment
kubectl apply -f https://k8s.io/examples/application/deployment.yaml
升级 nginx deployment
kubectl apply -f https://k8s.io/examples/application/deployment-update.yaml
扩容 nginx deployment。
kubectl apply -f https://k8s.io/examples/application/deployment-update.yaml
第一步,我们提交一个 nginx 的 Deployment然后对这个 Deployment 进行一次版本升级,也就是改变它中间 Pod 的版本。最后我们也会尝试对 nginx 进行一次扩容,进行一次水平的伸缩,下面就让大家一起跟我来尝试这三个操作吧。
首先,我们先看一下 minikube 的 status可以看到 kubelet master 和 kubectl 都是配置好的。
下一步我们利用 kubectl 来看一下这个集群中节选的状态可以看到这个master 的节点已经是 running 状态:
我们就以这个为节点,下面我们尝试去看一下现在集群中 Deployment 这个资源:
可以看到集群中没有任何的 Deployment我们可以利用 watch 这个语义去看集群中 Deployment 这个资源的变化情况。
下面我们去做刚才想要的三个操作:第一个操作是去创建一个 Deployment。可以看到下面第一个图这是一个 API 的 content它的 kind 是 Deploymentname 是 nginx-deployment, 有图中它的 replicas 数目是2它的镜像版本是 1.7.9。
我们下面还是回到 kubectl 这个 commnd 来执行这次 Deployment 的真正的操作。我们可以看到一个简单的操作,就会去让 Deployment 不停地生成副本。
Deployment 副本数目是 2 个,下面也可以 describe 一下现在的 Deployment 的状态。我们知道之前是没有这个 Deployment 的,现在我们去 describe 这个 nginx-deployment。
下图中可以看到:有一个 nginx-deployment 已经被生成了,它的 replicas 数目也是我们想要的、selector 也是我们想要的、它的 image 的版本也是 1.7.9。还可以看到,里面的 deployment-controller 这种版本控制器也是在管理它的生成。
下面我们去升级这个 Deployment 版本,首先下载另外一个 yaml 文件 deployment-update.yaml可以看到这里面的 image 本身的版本号从 1.7.9 升级到 1.8。
接下来我们重新 apply 新的 deployment-update 这个 yaml 文件。
可以看到,在另一边的屏幕上显示出了这个 Deployment 升级的一些操作,最终它的 up-to-date 值从 0 变成了 2也就是说所有的容器都是最新版本的所有的 Pod 都是最新版本的。我们也可以 discribe 具体去看一下是不是所有 Pod 的版本都被更新了,可以看到这个 image 的版本由 1.7.9 真正更新到了 1.8。
最后,我们也可以看到 controller 又执行了几次新的操作,这个控制器维护了整个 Deployment 和 Pod 状态。
最后我们演示一下给 Deployment 做水平扩张,下载另一个 yaml 文件 deployment-scale.yaml这里面的 replicas 数目已经从 2 改成了 4。
回到最开始的窗口,用 kubectl 去 apply 这个新的 deployment-scale.yaml 文件,在另外一个窗口上可以看到,当我们执行了 deployment-scale 操作之后,它的容器 Pod 数目从 2 变成了 4。我们可以再一次 describ 一下当前集群中的 deployment 的情况,可以看到它的 replicas 的数目从 2 变到了 4同时也可以看到 controller 又做了几次新的操作,这个 scale up 成功了。
最后,让我们利用 delete 操作把我们刚才生成的 Deployment 给删除掉。kubectl delete deployment也是刚才我们本身的 deployment name当我们把它删除掉之后我们今天所有的操作就完成了。
我们再去重新 get 这个 Deployment也会显示这个资源不再存在这个集群又回到了最开始干净的状态。
以上这就是这堂课中所有的内容了,我们关注了 kubernetes 的核心概念以及 kubernetes 的架构设计,希望大家能在这节课中有所收获,也希望大家能关注云原生技术课堂中的其他内容,谢谢大家的观看!
本节总结
Kubernetes 是一个自动化的容器编排平台,它负责应用的部署、应用的弹性以及应用的管理,这些都是基于容器的;
Kubernetes 架构是一个比较典型的二层架构和 server-client 架构。

View File

@ -0,0 +1,269 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 理解 Pod 和容器设计模式
为什么需要 Pod
容器的基本概念
现在来看第一个问题:为什么需要 Pod我们知道 Pod 是 Kubernetes 项目里面一个非常重要的概念,也是非常重要的一个原子调度单位,但是为什么我们会需要这样一个概念呢?我们在使用容器 Docker 的时候,也没有这个说法。其实如果要理解 Pod我们首先要理解容器所以首先来回顾一下容器的概念
容器的本质实际上是一个进程,是一个视图被隔离,资源受限的进程。
容器里面 PID=1 的进程就是应用本身,这意味着管理虚拟机等于管理基础设施,因为我们是在管理机器,但管理容器却等于直接管理应用本身。这也是之前说过的不可变基础设施的一个最佳体现,这个时候,你的应用就等于你的基础设施,它一定是不可变的。
在以上面的例子为前提的情况下Kubernetes 又是什么呢?我们知道,很多人都说 Kubernetes 是云时代的操作系统,这个非常有意思,因为如果以此类推,容器镜像就是这个操作系统的软件安装包,它们之间是这样的一个类比关系。
真实操作系统里的例子
如果说 Kubernetes 就是操作系统的话,那么我们不妨看一下真实的操作系统的例子。
例子里面有一个程序叫做 Helloworld这个 Helloworld 程序实际上是由一组进程组成的,需要注意一下,这里说的进程实际上等同于 Linux 中的线程。
因为 Linux 中的线程是轻量级进程,所以如果从 Linux 系统中去查看 Helloworld 中的 pstree将会看到这个 Helloworld 实际上是由四个线程组成的,分别是 {api、main、log、compute}。也就是说,四个这样的线程共同协作,共享 Helloworld 程序的资源,组成了 Helloworld 程序的真实工作情况。
这是操作系统里面进程组或者线程组中一个非常真实的例子,以上就是进程组的一个概念。
那么大家不妨思考一下在真实的操作系统里面一个程序往往是根据进程组来进行管理的。Kubernetes 把它类比为一个操作系统,比如说 Linux。针对于容器我们前面提到可以类比为进程就是前面的 Linux 线程。那么 Pod 又是什么呢?实际上 Pod 就是我们刚刚提到的进程组,也就是 Linux 里的线程组。
进程组概念
说到进程组,首先建议大家至少有个概念上的理解,然后我们再详细的解释一下。
还是前面那个例子Helloworld 程序由四个进程组成,这些进程之间会共享一些资源和文件。那么现在有一个问题:假如说现在把 Helloworld 程序用容器跑起来,你会怎么去做?
当然,最自然的一个解法就是,我现在就启动一个 Docker 容器,里面运行四个进程。可是这样会有一个问题,这种情况下容器里面 PID=1 的进程该是谁? 比如说,它应该是我的 main 进程,那么问题来了,“谁”又负责去管理剩余的 3 个进程呢?
这个核心问题在于,容器的设计本身是一种“单进程”模型,不是说容器里只能起一个进程,由于容器的应用等于进程,所以只能去管理 PID=1 的这个进程,其他再起来的进程其实是一个托管状态。 所以说服务应用进程本身就具有“进程管理”的能力。
比如说 Helloworld 的程序有 system 的能力,或者直接把容器里 PID=1 的进程直接改成 systemd否则这个应用或者是容器是没有办法去管理很多个进程的。因为 PID=1 进程是应用本身,如果现在把这个 PID=1 的进程给 kill 了,或者它自己运行过程中死掉了,那么剩下三个进程的资源就没有人回收了,这个是非常非常严重的一个问题。
而反过来真的把这个应用本身改成了 systemd或者在容器里面运行了一个 systemd将会导致另外一个问题使得管理容器不再是管理应用本身了而等于是管理 systemd这里的问题就非常明显了。比如说我这个容器里面 run 的程序或者进程是 systemd那么接下来这个应用是不是退出了是不是 fail 了?是不是出现异常失败了?实际上是没办法直接知道的,因为容器管理的是 systemd。这就是为什么在容器里面运行一个复杂程序往往比较困难的一个原因。
这里再帮大家梳理一下:由于容器实际上是一个“单进程”模型,所以如果你在容器里启动多个进程,只有一个可以作为 PID=1 的进程,而这时候,如果这个 PID=1 的进程挂了,或者说失败退出了,那么其他三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况。
注意Linux 容器的“单进程”模型,指的是容器的生命周期等同于 PID=1 的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。当然,一般情况下,容器应用进程并不具备进程管理能力,所以你通过 exec 或者 ssh 在容器里创建的其他进程,一旦异常退出(比如 ssh 终止)是很容易变成孤儿进程的。
反过来,其实可以在容器里面 run 一个 systemd用它来管理其他所有的进程。这样会产生第二个问题实际上没办法直接管理我的应用了因为我的应用被 systemd 给接管了,那么这个时候应用状态的生命周期就不等于容器生命周期。这个管理模型实际上是非常非常复杂的。
Pod = “进程组”
在 kubernetes 里面Pod 实际上正是 kubernetes 项目为你抽象出来的一个可以类比为进程组的概念。
前面提到的,由四个进程共同组成的一个应用 Helloworld在 Kubernetes 里面,实际上会被定义为一个拥有四个容器的 Pod这个概念大家一定要非常仔细的理解。
就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在 Kubernetes 里面并不会把它们放到一个容器里,因为这里会遇到两个问题。那么在 Kubernetes 里会怎么去做呢?它会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个 Pod 里面。
所以当 Kubernetes 把 Helloworld 给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于 Pod所以我们说 Pod 在 Kubernetes 里面只有一个逻辑单位,没有一个真实的东西对应说这个就是 Pod不会有的。真正起来在物理上存在的东西就是四个容器。这四个容器或者说是多个容器的组合就叫做 Pod。并且还有一个概念一定要非常明确Pod 是 Kubernetes 分配资源的一个单位,因为里面的容器要共享某些资源,所以 Pod 也是 Kubernetes 的原子调度单位。
上面提到的 Pod 设计,也不是 Kubernetes 项目自己想出来的, 而是早在 Google 研发 Borg 的时候,就已经发现了这样一个问题。这个在 Borg paper 里面有非常非常明确的描述。简单来说 Google 工程师发现在 Borg 下面部署应用时,很多场景下都存在着类似于“进程与进程组”的关系。更具体的是,这些应用之前往往有着密切的协作关系,使得它们必须部署在同一台机器上并且共享某些信息。
以上就是进程组的概念,也是 Pod 的用法。
为什么 Pod 必须是原子调度单位?
可能到这里大家会有一些问题:虽然了解这个东西是一个进程组,但是为什么要把 Pod 本身作为一个概念抽象出来呢?或者说能不能通过调度把 Pod 这个事情给解决掉呢?为什么 Pod 必须是 Kubernetes 里面的原子调度单位?
下面我们通过一个例子来解释。
假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个 Pod 里面。具体来说,第一个容器叫做 App就是业务容器它会写日志文件第二个容器叫做 LogCollector它会把刚刚 App 容器写的日志文件转发到后端的 ElasticSearch 中。
两个容器的资源需求是这样的App 容器需要 1G 内存LogCollector 需要 0.5G 内存而当前集群环境的可用内存是这样一个情况Node*A1.25G 内存Node*B2G 内存。
假如说现在没有 Pod 概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把 App 调度到了 Node*A 上面接下来会怎么样呢这时你会发现LogCollector 实际上是没办法调度到 Node*A 上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度。
以上就是一个非常典型的成组调度失败的例子。英文叫做Task co-scheduling 问题,这个问题不是说不能解,在很多项目里面,这样的问题都有解法。
比如说在 Mesos 里面它会做一个事情叫做资源囤积resource hoarding即当所有设置了 Affinity 约束的任务都达到时,才开始统一调度,这是一个非常典型的成组调度的解法。
所以上面提到的“App”和“LogCollector”这两个容器在 Mesos 里面,他们不会说立刻调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,因为需要等待。由于需要等还会有外一个情况会出现,就是产生死锁,就是互相等待的一个情况。这些机制在 Mesos 里都是需要解决的,也带来了额外的复杂度。
另一种解法是 Google 的解法。它在 Omega 系统(就是 Borg 下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个有很多人也能理解,就是悲观锁的设置一定比乐观锁要简单。
而像这样的一个 Task co-scheduling 问题,在 Kubernetes 里,就直接通过 Pod 这样一个概念去解决了。因为在 Kubernetes 里,这样的一个 App 容器和 LogCollector 容器一定是属于一个 Pod 的,它们在调度时必然是以一个 Pod 为单位进行调度,所以这个问题是根本不存在的。
再次理解 Pod
在讲了前面这些知识点之后,我们来再次理解一下 Pod首先 Pod 里面的容器是“超亲密关系”。
这里有个“超”字需要大家理解,正常来说,有一种关系叫做亲密关系,这个亲密关系是一定可以通过调度来解决的。
比如说现在有两个 Pod它们需要运行在同一台宿主机上那这样就属于亲密关系调度器一定是可以帮助去做的。但是对于超亲密关系来说有一个问题即它必须通过 Pod 来解决。因为如果超亲密关系赋予不了,那么整个 Pod 或者说是整个应用都无法启动。
什么叫做超亲密关系呢?大概分为以下几类:
比如说两个进程之间会发生文件交换,前面提到的例子就是这样,一个写日志,一个读日志;
两个进程之间需要通过 localhost 或者说是本地的 Socket 去进行通信,这种本地通信也是超亲密关系;
这两个容器或者是微服务之间,需要发生非常频繁的 RPC 调用,出于性能的考虑,也希望它们是超亲密关系;
两个容器或者是应用,它们需要共享某些 Linux Namespace。最简单常见的一个例子就是我有一个容器需要加入另一个容器的 Network Namespace。这样我就能看到另一个容器的网络设备和它的网络信息。
像以上几种关系都属于超亲密关系,它们都是在 Kubernetes 中会通过 Pod 的概念去解决的。
现在我们理解了 Pod 这样的概念设计,理解了为什么需要 Pod。它解决了两个问题
我们怎么去描述超亲密关系;
我们怎么去对超亲密关系的容器或者说是业务去做统一调度,这是 Pod 最主要的一个诉求。
Pod 的实现机制
Pod 要解决的问题
像 Pod 这样一个东西,本身是一个逻辑概念。那在机器上,它究竟是怎么实现的呢?这就是我们要解释的第二个问题。
既然说 Pod 要解决这个问题,核心就在于如何让一个 Pod 里的多个容器之间最高效的共享某些资源和数据。
因为容器之间原本是被 Linux Namespace 和 cgroups 隔开的,所以现在实际要解决的是怎么去打破这个隔离,然后共享某些事情和某些信息。这就是 Pod 的设计要解决的核心问题所在。
所以说具体的解法分为两个部分:网络和存储。
共享网络
第一个问题是 Pod 里的多个容器怎么去共享网络?下面是个例子:
比如说现在有一个 Pod其中包含了一个容器 A 和一个容器 B它们两个就要共享 Network Namespace。在 Kubernetes 里的解法是这样的:它会在每个 Pod 里,额外起一个 Infra container 小容器来共享整个 Pod 的 Network Namespace。
Infra container 是一个非常小的镜像,大概 100~200KB 左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
所以说一个 Pod 里面的所有容器它们看到的网络视图是完全一样的。即它们看到的网络设备、IP地址、Mac地址等等跟网络相关的信息其实全是一份这一份都来自于 Pod 第一次创建的这个 Infra container。这就是 Pod 解决网络共享的一个解法。
在 Pod 里面,一定有一个 IP 地址,是这个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。所以大家看到的都是一份,而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。
由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为什么在 Kubernetes 里面,它是允许去单独更新 Pod 里的某一个镜像的,即:做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设计。
共享存储
第二问题Pod 怎么去共享存储Pod 共享存储就相对比较简单。
比如说现在有两个容器,一个是 Nginx另外一个是非常普通的容器在 Nginx 里放一些文件,让我能通过 Nginx 访问到。所以它需要去 share 这个目录。我 share 文件或者是 share 目录在 Pod 里面是非常简单的,实际上就是把 volume 变成了 Pod level。然后所有容器就是所有同属于一个 Pod 的容器,他们共享所有的 volume。
比如说上图的例子,这个 volume 叫做 shared-data它是属于 Pod level 的,所以在每一个容器里可以直接声明:要挂载 shared-data 这个 volume只要你声明了你挂载这个 volume你在容器里去看这个目录实际上大家看到的就是同一份。这个就是 Kubernetes 通过 Pod 来给容器共享存储的一个做法。
所以在之前的例子中,应用容器 App 写了日志,只要这个日志是写在一个 volume 中,只要声明挂载了同样的 volume这个 volume 就可以立刻被另外一个 LogCollector 容器给看到。以上就是 Pod 实现存储的方式。
详解容器设计模式
现在我们知道了为什么需要 Pod也了解了 Pod 这个东西到底是怎么实现的。最后,以此为基础,详细介绍一下 Kubernetes 非常提倡的一个概念,叫做容器设计模式。
举例
接下来将会用一个例子来给大家进行讲解。
比如我现在有一个非常常见的一个诉求:我现在要发布一个应用,这个应用是 JAVA 写的,有一个 WAR 包需要把它放到 Tomcat 的 web APP 目录下面,这样就可以把它启动起来了。可是像这样一个 WAR 包或 Tomcat 这样一个容器的话,怎么去做,怎么去发布?这里面有几种做法。
第一种方式:可以把 WAR 包和 Tomcat 打包放进一个镜像里面。但是这样带来一个问题,就是现在这个镜像实际上揉进了两个东西。那么接下来,无论是我要更新 WAR 包还是说我要更新 Tomcat都要重新做一个新的镜像这是比较麻烦的
第二种方式:就是镜像里面只打包 Tomcat。它就是一个 Tomcat但是需要使用数据卷的方式比如说 hostPath从宿主机上把 WAR 包挂载进我们 Tomcat 容器中,挂到我的 web APP 目录下面,这样把这个容器启用起来之后,里面就能用了。
但是这时会发现一个问题:这种做法一定需要维护一套分布式存储系统。因为这个容器可能第一次启动是在宿主机 A 上面,第二次重新启动就可能跑到 B 上去了,容器它是一个可迁移的东西,它的状态是不保持的。所以必须维护一套分布式存储系统,使容器不管是在 A 还是在 B 上,都可以找到这个 WAR 包,找到这个数据。
注意,即使有了分布式存储系统做 Volume你还需要负责维护 Volume 里的 WAR 包。比如:你需要单独写一套 Kubernetes Volume 插件,用来在每次 Pod 启动之前,把应用启动所需的 WAR 包下载到这个 Volume 里,然后才能被应用挂载使用到。
这样操作带来的复杂程度还是比较高的,且这个容器本身必须依赖于一套持久化的存储插件(用来管理 Volume 里的 WAR 包内容)。
InitContainer
所以大家有没有考虑过,像这样的组合方式,有没有更加通用的方法?哪怕在本地 Kubernetes 上,没有分布式存储的情况下也能用、能玩、能发布。
实际上方法是有的,在 Kubernetes 里面,像这样的组合方式,叫做 Init Container。
还是同样一个例子:在上图的 yaml 里,首先定义一个 Init Container它只做一件事情就是把 WAR 包从镜像里拷贝到一个 Volume 里面,它做完这个操作就退出了,所以 Init Container 会比用户容器先启动,并且严格按照定义顺序来依次执行。
然后这个关键在于刚刚拷贝到的这样一个目的目录APP 目录,实际上是一个 Volume。而我们前面提到一个 Pod 里面的多个容器,它们是可以共享 Volume 的,所以现在这个 Tomcat 容器,只是打包了一个 Tomcat 镜像。但在启动的时候,要声明使用 APP 目录作为我的 Volume并且要把它们挂载在 Web APP 目录下面。
而这个时候,由于前面已经运行过了一个 Init Container已经执行完拷贝操作了所以这个 Volume 里面已经存在了应用的 WAR 包:就是 sample.war绝对已经存在这个 Volume 里面了。等到第二步执行启动这个 Tomcat 容器的时候,去挂这个 Volume一定能在里面找到前面拷贝来的 sample.war。
所以可以这样去描述:这个 Pod 就是一个自包含的,可以把这一个 Pod 在全世界任何一个 Kubernetes 上面都顺利启用起来。不用担心没有分布式存储、Volume 不是持久化的,它一定是可以公布的。
所以这是一个通过组合两个不同角色的容器,并且按照这样一些像 Init Container 这样一种编排方式,统一的去打包这样一个应用,把它用 Pod 来去做的非常典型的一个例子。像这样的一个概念,在 Kubernetes 里面就是一个非常经典的容器设计模式叫做“Sidecar”。
容器设计模式Sidecar
什么是 Sidecar就是说其实在 Pod 里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作,比如我们前面举的例子,其实就干了一个事儿,这个 Init Container它就是一个 Sidecar它只负责把镜像里的 WAR 包拷贝到共享目录里面,以便被 Tomcat 能够用起来。
其它有哪些操作呢?比如说:
原本需要在容器里面执行 SSH 需要干的一些事情,可以写脚本、一些前置的条件,其实都可以通过像 Init Container 或者另外像 Sidecar 的方式去解决;
当然还有一个典型例子就是我的日志收集,日志收集本身是一个进程,是一个小容器,那么就可以把它打包进 Pod 里面去做这个收集工作;
还有一个非常重要的东西就是 Debug 应用,实际上现在 Debug 整个应用都可以在应用 Pod 里面再次定义一个额外的小的 Container它可以去 exec 应用 pod 的 namespace
查看其他容器的工作状态,这也是它可以做的事情。不再需要去 SSH 登陆到容器里去看,只要把监控组件装到额外的小容器里面就可以了,然后把它作为一个 Sidecar 启动起来,跟主业务容器进行协作,所以同样业务监控也都可以通过 Sidecar 方式来去做。
这种做法一个非常明显的优势就是在于其实将辅助功能从我的业务容器解耦了,所以我就能够独立发布 Sidecar 容器,并且更重要的是这个能力是可以重用的,即同样的一个监控 Sidecar 或者日志 Sidecar可以被全公司的人共用的。这就是设计模式的一个威力。
Sidecar应用与日志收集
接下来,我们再详细细化一下 Sidecar 这样一个模式,它还有一些其他的场景。
比如说前面提到的应用日志收集,业务容器将日志写在一个 Volume 里面,而由于 Volume 在 Pod 里面是被共享的,所以日志容器 —— 即 Sidecar 容器一定可以通过共享该 Volume直接把日志文件读出来然后存到远程存储里面或者转发到另外一个例子。现在业界常用的 Fluentd 日志进程或日志组件,基本上都是这样的工作方式。
Sidecar代理容器
Sidecar 的第二个用法,可以称作为代理容器 Proxy。什么叫做代理容器呢
假如现在有个 Pod 需要访问一个外部系统,或者一些外部服务,但是这些外部系统是一个集群,那么这个时候如何通过一个统一的、简单的方式,用一个 IP 地址,就把这些集群都访问到?有一种方法就是:修改代码。因为代码里记录了这些集群的地址;另外还有一种解耦的方法,即通过 Sidecar 代理容器。
简单说,单独写一个这么小的 Proxy用来处理对接外部的服务集群它对外暴露出来只有一个 IP 地址就可以了。所以接下来,业务容器主要访问 Proxy然后由 Proxy 去连接这些服务集群,这里的关键在于 Pod 里面多个容器是通过 localhost 直接通信的,因为它们同属于一个 network Namespace网络视图都一样所以它们俩通信 localhost并没有性能损耗。
所以说代理容器除了做了解耦之外,并不会降低性能,更重要的是,像这样一个代理容器的代码就又可以被全公司重用了。
Sidecar适配器容器
Sidecar 的第三个设计模式 —— 适配器容器 Adapter什么叫 Adapter 呢?
现在业务暴露出来的 API比如说有个 API 的一个格式是 A但是现在有一个外部系统要去访问我的业务容器它只知道的一种格式是 API B ,所以要做一个工作,就是把业务容器怎么想办法改掉,要去改业务代码。但实际上,你可以通过一个 Adapter 帮你来做这层转换。
现在有个例子:现在业务容器暴露出来的监控接口是 /metrics访问这个这个容器的 metrics 的这个 URL 就可以拿到了。可是现在,这个监控系统升级了,它访问的 URL 是 /health我只认得暴露出 health 健康检查的 URL才能去做监控metrics 不认识。那这个怎么办?那就需要改代码了,但可以不去改代码,而是额外写一个 Adapter用来把所有对 health 的这个请求转发给 metrics 就可以了,所以这个 Adapter 对外暴露的是 health 这样一个监控的 URL这就可以了你的业务就又可以工作了。
这样的关键还在于 Pod 之中的容器是通过 localhost 直接通信的,所以没有性能损耗,并且这样一个 Adapter 容器可以被全公司重用起来,这些都是设计模式给我们带来的好处。
本节总结
Pod 是 Kubernetes 项目里实现“容器设计模式”的核心机制;
“容器设计模式”是 Google Borg 的大规模容器集群管理最佳实践之一,也是 Kubernetes 进行复杂应用编排的基础依赖之一;
所有“设计模式”的本质都是:解耦和重用。

View File

@ -0,0 +1,302 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 应用编排与管理:核心原理
资源元信息
1. Kubernetes 资源对象
首先,我们来回顾一下 Kubernetes 的资源对象组成:主要包括了 Spec、Status 两部分。其中 Spec 部分用来描述期望的状态Status 部分用来描述观测到的状态。
今天我们将为大家介绍 K8s 的另外一个部分即元数据部分。该部分主要包括了用来识别资源的标签Labels 用来描述资源的注解Annotations 用来描述多个资源之间相互关系的 OwnerReference。这些元数据在 K8s 运行中有非常重要的作用。后续课程中将会反复讲到。
2. labels
第一个元数据,也是最重要的一个元数据是:资源标签。资源标签是一种具有标识型的 KeyValue 元数据,这里展示了几个常见的标签。
前三个标签都打在了 Pod 对象上,分别标识了对应的应用环境、发布的成熟度和应用的版本。从应用标签的例子可以看到,标签的名字包括了一个域名的前缀,用来描述打标签的系统和工具, 最后一个标签打在 Node 对象上,还在域名前增加了版本的标识 beta 字符串。
标签主要用来筛选资源和组合资源,可以使用类似于 SQL 查询 select来根据 Label 查询相关的资源。
3. Selector
最常见的 Selector 就是相等型 Selector。现在举一个简单的例子
假设系统中有四个 Pod每个 Pod 都有标识系统层级和环境的标签,我们通过 Tiefront 这个标签,可以匹配左边栏的 Pod相等型 Selector 还可以包括多个相等条件,多个相等条件之间是逻辑”与“的关系。
在刚才的例子中,通过 Tie=front,Env=dev 的Selector我们可以筛选出所有 Tie=front而且 Env=dev 的 Pod也就是下图中左上角的 Pod。另外一种 Selector 是集合型 Selector在例子中Selector 筛选所有环境是 test 或者 gray 的 Pod。
除了 in 的集合操作外,还有 notin 集合操作,比如 tie notinfront,back将会筛选所有 tie 不是 front 且不是 back 的 Pod。另外也可以根据是否存在某 lable 的筛选Selector release筛选所有带 release 标签的 Pod。集合型和相等型的 Selector也可以用“”来连接同样的标识逻辑”与“的关系。
4. Annotations
另外一种重要的元数据是annotations。一般是系统或者工具用来存储资源的非标示性信息可以用来扩展资源的 spec/status 的描述,这里给了几个 annotations 的例子:
第一个例子,存储了阿里云负载器的证书 ID我们可以看到 annotations 一样可以拥有域名的前缀,标注中也可以包含版本信息。第二个 annotation存储了 nginx 接入层的配置信息,我们可以看到 annotations 中包括“,”这样无法出现在 label 中的特殊字符。第三个 annotations 一般可以在 kubectl apply 命令行操作后的资源中看到, annotation 值是一个结构化的数据,实际上是一个 json 串,标记了上一次 kubectl 操作的资源的 json 的描述。
5. Ownereference
我们当时讲到最后一个元数据叫做 Ownereference所谓所有者一般就是指集合类的资源比如说 Pod 集合,就有 replicaset、statefulset这个将在后序的课程中讲到。
集合类资源的控制器会创建对应的归属资源。比如replicaset 控制器在操作中会创建 Pod被创建 Pod 的 Ownereference 就指向了创建 Pod 的 replicasetOwnereference 使得用户可以方便地查找一个创建资源的对象,另外,还可以用来实现级联删除的效果。** **
操作演示
这里通过 kubectl 命令去连接我们 ACK 中已经创建好的一个 K8s 集群,然后来展示一下怎么查看和修改 K8s 对象中的元数据,主要就是 Pod 的一个标签、注解,还有对应的 Ownerference。
首先我们看一下集群里现在的配置情况:
查看 Pod现在没有任何的一个 Pod
kubectl get pods
然后用事先准备好的一个 Pod 的 yaml创建一个 Pod 出来;
kubectl apply -f pod1.yaml
kubectl apply -f pod2.yaml
现在查看一下 Pod 打的标签,我们用 show-labels 这个选项,可以看到这两个 Pod 都打上了一个部署环境和层级的标签;
kubectl get pods —show-labels
我们也可以通过另外一种方式来查看具体的资源信息。首先查看 nginx1 第一个 Pod 的一个信息,用 -o yaml 的方式输出,可以看到这个 Pod 元数据里面包括了一个 lables 的字段,里面有两个 lable
kubectl get pods nginx1 -o yaml | less
现在再想一下,怎么样对 Pod 已有的 lable 进行修改?我们先把它的部署环境,从开发环境改成测试环境,然后指定 Pod 名字,在环境再加上它的一个值 test ,看一下能不能成功。 这里报了一个错误,可以看到,它其实是说现在这个 label 已经有值了;
kubectl label pods nginx1 env=test
如果想覆盖掉它的话,得额外再加上一个覆盖的选项。加上之后呢,我们应该可以看到这个打标已经成功了;
kubectl label pods nginx1 env=test —overwrite
我们再看一下现在集群的 lable 设置情况,首先可以看到 nginx1 的确已经加上了一个部署环境 test 标签;
kubectl get pods —show-labels
如果想要对 Pod 去掉一个标签,也是跟打标签一样的操作,但是 env 后就不是等号了。只加上 label 名字,后面不加等号,改成用减号表示去除 label 的 k:v
kubectl label pods nginx tie-
可以看到这个 label去标已经完全成功
kubectl get pods —show-labels
下面来看一下配置的 label 值,的确能看到 nginx1 的这个 Pod 少了一个 tie=front 的标签。有了这个 Pod 标签之后,可以看一下怎样用 label Selector 进行匹配?首先 label Selector 是通过 -l 这个选项来进行指定的 ,指定的时候,先试一下用相等型的一个 label 来筛选,所以我们指定的是部署环境等于测试的一个 Pod我们可以看到能够筛选出一台
kubectl get pods —show-labels -l env=test
假如说有多个相等的条件需要指定的,实际上这是一个与的关系,假如说 env 再等于 dev我们实际上是一个 Pod 都拿不到的;
kubectl get pods —show-labels -l env=test,env=dev
然后假如说 env=dev但是 tie=front我们能够匹配到第二个 Pod也就是 nginx2
kubectl get pods —show-labels -l env=dev,tie=front
我们还可以再试一下怎么样用集合型的 label Selector 来进行筛选。这一次我们还是想要匹配出所有部署环境是 test 或者是 dev 的一个 Pod所以在这里加上一个引号然后在括号里面指定所有部署环境的一个集合。这次能把两个创建的 Pod 都筛选出来;
kubectl get pods —show-labels -l env in (dev,test)
我们再试一下怎样对 Pod 增加一个注解,注解的话,跟打标是一样的操作,但是把 label 命令改成 annotate 命令;然后,一样指定类型和对应的名字。后面就不是加上 label 的 k:v 了,而是加上 annotation 的 k:v。这里我们可以指定一个任意的字符串比如说加上空格、加上逗号都可以
kubectl annotate pods nginx1 my-annotate=my annotate,ok
然后,我们再看一下这个 Pod 的一些元数据,我们这边能够看到这个 Pod 的元数据里面 annotations这是有一个 my-annotate 这个 Annotations
kubectl get pods nging1 -o yaml | less
然后我们这里其实也能够看到有一个 kubectl apply 的时候kubectl 工具增加了一个 annotation这也是一个 json 串。
然后我们再演示一下看 Pod 的 Ownereference 是怎么出来的。原来的 Pod 都是直接通过创建 Pod 这个资源方式来创建的,这次换一种方式来创建:通过创建一个 ReplicaSet 对象来创建 Pod 。首先创建一个 ReplicaSet 对象,这个 ReplicaSet 对象可以具体查看一下;
kubectl apply -f rs.yaml
kubectl get replicasets nginx-replicasets -o yaml |less
我们可以关注一下这个 ReplicaSet 里面 spec 里面,提到会创建两个 Pod然后 selector 通过匹配部署环境是 product 生产环境的这个标签来进行匹配。所以我们可以看一下,现在集群中的 Pod 情况;
**kubectl get pods **
将会发现多了两个 Pod仔细查看这两个 Pod可以看到 ReplicaSet 创建出来的 Pod 有一个特点,即它会带有 Ownereference然后 Ownereference 里面指向了是一个 replicasets 类型,名字就叫做 nginx-replicasets
kubectl get pods nginx-replicasets-rhd68 -o yaml | less
控制器模式
1、控制循环
控制型模式最核心的就是控制循环的概念。在控制循环中包括了控制器,被控制的系统,以及能够观测系统的传感器,三个逻辑组件。
当然这些组件都是逻辑的,外界通过修改资源 spec 来控制资源,控制器比较资源 spec 和 status从而计算一个 diffdiff 最后会用来决定执行对系统进行什么样的控制操作,控制操作会使得系统产生新的输出,并被传感器以资源 status 形式上报,控制器的各个组件将都会是独立自主地运行,不断使系统向 spec 表示终态趋近。
2、Sensor
控制循环中逻辑的传感器主要由 Reflector、Informer、Indexer 三个组件构成。
Reflector 通过 List 和 Watch K8s server 来获取资源的数据。List 用来在 Controller 重启以及 Watch 中断的情况下,进行系统资源的全量更新;而 Watch 则在多次 List 之间进行增量的资源更新Reflector 在获取新的资源数据后,会在 Delta 队列中塞入一个包括资源对象信息本身以及资源对象事件类型的 Delta 记录Delta 队列中可以保证同一个对象在队列中仅有一条记录,从而避免 Reflector 重新 List 和 Watch 的时候产生重复的记录。
Informer 组件不断地从 Delta 队列中弹出 delta 记录,然后把资源对象交给 indexer让 indexer 把资源记录在一个缓存中,缓存在默认设置下是用资源的命名空间来做索引的,并且可以被 Controller Manager 或多个 Controller 所共享。之后,再把这个事件交给事件的回调函数
控制循环中的控制器组件主要由事件处理函数以及 worker 组成,事件处理函数之间会相互关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理。对需要处理的事件,会把事件关联资源的命名空间以及名字塞入一个工作队列中,并且由后续的 worker 池中的一个 Worker 来处理,工作队列会对存储的对象进行去重,从而避免多个 Woker 处理同一个资源的情况。
Worker 在处理资源对象时一般需要用资源的名字来重新获得最新的资源数据用来创建或者更新资源对象或者调用其他的外部服务Worker 如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列中,从而方便之后进行重试。
3、控制循环例子-扩容
这里举一个简单的例子来说明一下控制循环的工作原理。
ReplicaSet 是一个用来描述无状态应用的扩缩容行为的资源, ReplicaSet controler 通过监听 ReplicaSet 资源来维持应用希望的状态数量ReplicaSet 中通过 selector 来匹配所关联的 Pod在这里考虑 ReplicaSet rsA 的replicas 从 2 被改到 3 的场景。
首先Reflector 会 watch 到 ReplicaSet 和 Pod 两种资源的变化,为什么我们还会 watch pod 资源的变化稍后会讲到。发现 ReplicaSet 发生变化后,在 delta 队列中塞入了对象是 rsA而且类型是更新的记录。
Informer 一方面把新的 ReplicaSet 更新到缓存中,并与 Namespace nsA 作为索引。另外一方面,调用 Update 的回调函数ReplicaSet 控制器发现 ReplicaSet 发生变化后会把字符串的 nsA/rsA 字符串塞入到工作队列中,工作队列后的一个 Worker 从工作队列中取到了 nsA/rsA 这个字符串的 key并且从缓存中取到了最新的 ReplicaSet 数据。
Worker 通过比较 ReplicaSet 中 spec 和 status 里的数值,发现需要对这个 ReplicaSet 进行扩容,因此 ReplicaSet 的 Worker 创建了一个 Pod这个 pod 中的 Ownereference 取向了 ReplicaSet rsA。
然后 Reflector Watch 到的 Pod 新增事件,在 delta 队列中额外加入了 Add 类型的 deta 记录,一方面把新的 Pod 记录通过 Indexer 存储到了缓存中,另一方面调用了 ReplicaSet 控制器的 Add 回调函数Add 回调函数通过检查 pod ownerReferences 找到了对应的 ReplicaSet并把包括 ReplicaSet 命名空间和字符串塞入到了工作队列中。
ReplicaSet 的 Woker 在得到新的工作项之后,从缓存中取到了新的 ReplicaSet 记录,并得到了其所有创建的 Pod因为 ReplicaSet 的状态不是最新的,也就是所有创建 Pod 的数量不是最新的。因此在此时 ReplicaSet 更新 status 使得 spec 和 status 达成一致。
控制器模式总结
1、两种 API 设计方法
Kubernetes 控制器模式依赖声明式的 API。另外一种常见的 API 类型是命令式 API。为什么 Kubernetes 采用声明式 API而不是命令式 API 来设计整个控制器呢?
首先,比较两种 API 在交互行为上的差别。在生活中,常见的命令式的交互方式是家长和孩子交流方式,因为孩子欠缺目标意识,无法理解家长期望,家长往往通过一些命令,教孩子一些明确的动作,比如说:吃饭、睡觉类似的命令。我们在容器编排体系中,命令式 API 就是通过向系统发出明确的操作来执行的。
而常见的声明式交互方式,就是老板对自己员工的交流方式。老板一般不会给自己的员工下很明确的决定,实际上可能老板对于要操作的事情本身,还不如员工清楚。因此,老板通过给员工设置可量化的业务目标的方式,来发挥员工自身的主观能动性。比如说,老板会要求某个产品的市场占有率达到 80%,而不会指出要达到这个市场占有率,要做的具体操作细节。
类似的,在容器编排体系中,我们可以执行一个应用实例副本数保持在 3 个,而不用明确的去扩容 Pod 或是删除已有的 Pod来保证副本数在三个。
2、命令式 API 的问题
在理解两个交互 API 的差别后,可以分析一下命令式 API 的问题。
命令 API 最大的一个问题在于错误处理;
在大规模的分布式系统中,错误是无处不在的。一旦发出的命令没有响应,调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题。
假设原来的命令,后台实际上已经执行完成了,重试后又多执行了一个重试的命令操作。为了避免重试的问题,系统往往还需要在执行命令前,先记录一下需要执行的命令,并且在重启等场景下,重做待执行的命令,而且在执行的过程中,还需要考虑多个命令的先后顺序、覆盖关系等等一些复杂的逻辑情况。
实际上许多命令式的交互系统后台往往还会做一个巡检的系统,用来修正命令处理超时、重试等一些场景造成数据不一致的问题;
然而,因为巡检逻辑和日常操作逻辑是不一样的,往往在测试上覆盖不够,在错误处理上不够严谨,具有很大的操作风险,因此往往很多巡检系统都是人工来触发的。
最后,命令式 API 在处理多并发访问时,也很容易出现问题;
假如有多方并发的对一个资源请求进行操作,并且一旦其中有操作出现了错误,就需要重试。那么最后哪一个操作生效了,就很难确认,也无法保证。很多命令式系统往往在操作前会对系统进行加锁,从而保证整个系统最后生效行为的可预见性,但是加锁行为会降低整个系统的操作执行效率。
相对的,声明式 API 系统里天然地记录了系统现在和最终的状态。
不需要额外的操作数据。另外因为状态的幂等性,可以在任意时刻反复操作。在声明式系统运行的方式里,正常的操作实际上就是对资源状态的巡检,不需要额外开发巡检系统,系统的运行逻辑也能够在日常的运行中得到测试和锤炼,因此整个操作的稳定性能够得到保证。
最后,因为资源的最终状态是明确的,我们可以合并多次对状态的修改。可以不需要加锁,就支持多方的并发访问。
3、控制器模式总结
最后我们总结一下:
Kubernetes 所采用的控制器模式,是由声明式 API 驱动的。确切来说,是基于对 Kubernetes 资源对象的修改来驱动的;
Kubernetes 资源之后,是关注该资源的控制器。这些控制器将异步的控制系统向设置的终态驱近;
这些控制器是自主运行的,使得系统的自动化和无人值守成为可能;
因为 Kubernetes 的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态应用,我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是后续会介绍的 operator 的场景。
本节总结
本节课的主要内容就到此为止了,这里为大家简单总结一下:
Kubernetes 资源对象中的元数据部分主要包括了用来识别资源的标签Labels 用来描述资源的注解Annotations 用来描述多个资源之间相互关系的 OwnerReference。这些元数据在 K8s 运行中有非常重要的作用;
控制型模式中最核心的就是控制循环的概念;
两种 API 设计方法:声明式 API 和命令式 API Kubernetes 所采用的控制器模式,是由声明式 API 驱动的。

View File

@ -0,0 +1,302 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 应用编排与管理
需求来源
背景问题
首先,我们来看一下背景问题。如下图所示:如果我们直接管理集群中所有的 Pod应用 A、B、C 的 Pod其实是散乱地分布在集群中。
现在有以下的问题:
首先,如何保证集群内可用 Pod 的数量?也就是说我们应用 A 四个 Pod 如果出现了一些宿主机故障,或者一些网络问题,如何能保证它可用的数量?
如何为所有 Pod 更新镜像版本?我们是否要某一个 Pod 去重建新版本的 Pod
然后在更新过程中,如何保证服务的可用性?
以及更新过程中,如果发现了问题,如何快速回滚到上一个版本?
Deployment管理部署发布的控制器
这里就引入了我们今天课程的主题Deployment 管理部署发布的控制器。
可以看到我们通过 Deployment 将应用 A、B、C 分别规划到不同的 Deployment 中,每个 Deployment 其实是管理的一组相同的应用 Pod这组 Pod 我们认为它是相同的一个副本,那么 Deployment 能帮我们做什么事情呢?
首先Deployment 定义了一种 Pod 期望数量,比如说应用 A我们期望 Pod 数量是四个那么这样的话controller 就会持续维持 Pod 数量为期望的数量。当我们与 Pod 出现了网络问题或者宿主机问题的话controller 能帮我们恢复,也就是新扩出来对应的 Pod来保证可用的 Pod 数量与期望数量一致;
配置 Pod 发布方式,也就是说 controller 会按照用户给定的策略来更新 Pod而且更新过程中也可以设定不可用 Pod 数量在多少范围内;
如果更新过程中发生问题的话,即所谓“一键”回滚,也就是说你通过一条命令或者一行修改能够将 Deployment 下面所有 Pod 更新为某一个旧版本 。
用例解读
Deployment 语法
下面我们用一个简单的用例来解读一下如何操作 Deployment。
上图可以看到一个最简单的 Deployment 的 yaml 文件。
“apiVersionapps/v1”也就是说 Deployment 当前所属的组是 apps版本是 v1。“metadata”是我们看到的 Deployment 元信息,也就是往期回顾中的 Labels、Selector、Pod.image这些都是在往期中提到的知识点。
Deployment 作为一个 K8s 资源,它有自己的 metadata 元信息,这里我们定义的 Deployment.name 是 nginx.Deployment。Deployment.spec 中首先要有一个核心的字段,即 replicas这里定义期望的 Pod 数量为三个selector 其实是 Pod 选择器,那么所有扩容出来的 Pod它的 Labels 必须匹配 selector 层上的 image.labels也就是 app.nginx。
就如上面的 Pod 模板 template 中所述,这个 template 它其实包含了两部分内容:
一部分是我们期望 Pod 的 metadata其中包含了 labels即跟 selector.matchLabels 相匹配的一个 Labels
第二部分是 template 包含的一个 Pod.spec。这里 Pod.spec 其实是 Deployment 最终创建出来 Pod 的时候,它所用的 Pod.spec这里定义了一个 container.nginx它的镜像版本是 nginx:1.7.9。
下面是遇到的新知识点:
第一个是 replicas就是 Deployment 中期望的或者终态数量;
第二个是 template也就是 Pod 相关的一个模板。
查看 Deployment 状态
当我们创建出一个 Deployment 的时候,可以通过 kubectl get deployment看到 Deployment 总体的一个状态。如下图所示:
上图中可以看到:
DESIRED期望的 Pod 数量是 3 个;
CURRENT当前实际 Pod 数量是 3 个;
UP-TO-DATE其实是到达最新的期望版本的 Pod 数量;
AVAILABLE这个其实是运行过程中可用的 Pod 数量。后面会提到,这里 AVAILABLE 并不简单是可用的,也就是 Ready 状态的,它其实包含了一些可用超过一定时间长度的 Pod
AGEdeployment 创建的时长,如上图 Deployment 就是已经创建了 80 分钟。
查看 Pod
最后我们可以查看一下 Pod。如下图所示
上图中有三个 PodPod 名字格式我们不难看到。
最前面一段nginx-deployment其实是 Pod 所属 Deployment.name中间一段template-hash这里三个 Pod 是一样的,因为这三个 Pod 其实都是同一个 template 中创建出来的。
最后一段,是一个 random 的字符串,我们通过 get.pod 可以看到Pod 的 ownerReferences 即 Pod 所属的 controller 资源,并不是 Deployment而是一个 ReplicaSet。这个 ReplicaSet 的 name其实是 nginx-deployment 加上 pod.template-hash后面会提到。所有的 Pod 都是 ReplicaSet 创建出来的,而 ReplicaSet 它对应的某一个具体的 Deployment.template 版本。
更新镜像
接下来我们可以看一下,如何对一个给定的 Deployment 更新它所有Pod的镜像版本呢这里我们可以执行一个 kubectl 命令:
kubectl set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1
首先 kubectl 后面有一个 set image 固定写法,这里指的是设定镜像;其次是一个 deployment.v1.apps这里也是一个固定写法写的是我们要操作的资源类型deployment 是资源名、v1 是资源版本、apps 是资源组,这里也可以简写为 deployment 或者 deployment.apps比如说写为 deployment 的时候,默认将使用 apps 组 v1 版本。
第三部分是要更新的 deployment 的 name也就是我们的 nginx-deployment再往后的 nginx 其实指的是 template也就是 Pod 中的 container.name这里我们可以注意到一个 Pod 中,其实可能存在多个 container而我们指定想要更新的镜像的 container.name就是 nginx。
最后,指定我们这个容器期望更新的镜像版本,这里指的是 nginx: 1.9.1。如下图所示:当执行完这条命令之后,可以看到 deployment 中的 template.spec 已经更新为 nginx: 1.9.1。
快速回滚
如果我们在发布过程中遇到了问题,也支持快速回滚。通过 kubectl 执行的话其实是“kubectl rollout undo”这个命令可以回滚到 Deployment 上一版本通过“rollout undo”加上“to-revision”来指定可以回滚到某一个具体的版本。
DeploymeStatus
最后我们来看一下 DeploymeStatus。前面的课程我们学习到每一个资源都有它的 spec.Status。这里可以看一下deploymentStatus 中描述的三个其实是它的 conversion 状态,也就是 Processing、Complete 以及 Failed。
以 Processing 为例Processing 指的是 Deployment 正在处于扩容和发布中。比如说 Processing 状态的 deployment它所有的 replicas 及 Pod 副本全部达到最新版本,而且是 available这样的话就可以进入 complete 状态。而 complete 状态如果发生了一些扩缩容的话,也会进入 processing 这个处理工作状态。
如果在处理过程中遇到一些问题:比如说拉镜像失败了,或者说 readiness probe 检查失败了,就会进入 failed 状态;如果在运行过程中即 complete 状态,中间运行时发生了一些 pod readiness probe 检查失败,这个时候 deployment 也会进入 failed 状态。进入 failed 状态之后,除非所有点 replicas 均变成 available而且是 updated 最新版本deployment 才会重新进入 complete 状态。
操作演示
Deployment 创建及状态
下面我们来进行操作演示:这里连接一个阿里云服务集群。我们可以看到当前集群已经有几个可用的 node。
首先创建对应的 deployment。可以看到 deployment 中的 desired、current、up-to-date 以及 available 已经都达到了可用的期望状态。
Deployment 的结构
这里看到 spec 中的 replicas 是三个selector 以及 template labels中定义的标签都是 appnginxspec 中的 image 是我们期望的 nginx: 1.7.9status 中的 available.replicasreadReplicas 以及 updatedReplicas 都是 3 个。
Pod 状态
我们可以再选择一个 Pod 看一下状态:
可以看到Pod 中 ownerReferences 的功能是 ReplicaSetpod.spec.container 里的镜像是 1.7.9。这个 Pod 已经是 Running 状态,而且它的 conditions.status 是“true”表示它的服务已经可用了。
更新升级
当前只有最新版本的 replicaset那么现在尝试对 deployment 做一次升级。
“kubectl set image”这个操作命令后面接 “deployment”加 deployment.name最后指定容器名以及我们期望升级的镜像版本。
接下来我们看下 deployment 中的 template 中的 image 已经更新为 1.9.1。
这个时候我们再 get pod 看一下状态。
三个 pod 已经升级为新版本pod 名字中的 pod-template-hash 也已更新。
可以看到:旧版本 replicaset 的 spec 数量以及 pod 数量是都是 0新版本的 pod 数量是 3 个。
假设又做了一次更新,这个时候 get.pod 其实可以看到:当前的 pod 其实是有两个旧版本的处于 running另一个旧版本是在删除中而两个新版本的 pod一个已经进入 running一个还在 creating 中。
这时我们可用的 pod 数量即非删除状态的 pod 数量,其实是 4 个,已经超过了 replica 原先在 deployment 设置的数量 3 个。这个原因是我们在 deployment 中有 maxavailable 和 maxsugar 两个操作,这两个配置可以限制我们在发布过程中的一些策略。在后面架构设计中会讲到这个问题。
** **
历史版本保留 revisionHistoryLimit
上图看到,我们当前最新版本的 replicaset 是 3 个 pod另外还有两个历史版本的 replicaset那么会不会存在一种情况就是随着 deployment 持续的更新,这个旧版本的 replicaset 会越积越多呢?其实 deployment 提供了一个机制来避免这个问题:在 deployment spec 中,有一个 revisionHistoryLimit它的默认值为 10它其实保证了保留历史版本的 replicaset 的数量,我们尝试把它改为 1。
由上面第二张图,可以看到两个 replicaset也就是说除了当前版本的 replicaset 之外,旧版本的 replicaset 其实只保留了一个。
回滚
最后再尝试做一下回滚。首先再来看一下 replicaset这时发现旧版本的 replicaset 数量从 0 个增到 2 个,而新版本的 replicaset 数量从 3 个削减为 1 个,表示它已经开始在做回滚的操作。然后再观察一下, 旧版本的数量已经是 3 个,即已经回滚成功,而新版本的 pod 数量变为 0 个。
我们最后再 get pod 看一下:
这时3 个 pod.template-hash 已经更新为旧版本的 hash但其实这 3 个 pod 都是重新创建出来的,而并非我们在前一版本中创建的 3 个 pod。换句话说也就是我们回滚的时候其实是创建了 3 个旧版本的 pod而并非把先前的 3 个 pod 找回来。
架构设计
管理模式
我们来看一下架构设计。首先简单看一下管理模式Deployment 只负责管理不同版本的 ReplicaSet由 ReplicaSet 来管理具体的 Pod 副本数,每个 ReplicaSet 对应 Deployment template 的一个版本。在上文的例子中可以看到,每一次修改 template都会生成一个新的 ReplicaSet这个 ReplicaSet 底下的 Pod 其实都是相同的版本。
如上图所示Deployment 创建 ReplicaSet而 ReplicaSet 创建 Pod。他们的 OwnerRef 其实都对应了其控制器的资源。
Deployment 控制器
我们先简单看一下控制器实现原理。
首先,我们所有的控制器都是通过 Informer 中的 Event 做一些 Handler 和 Watch。这个地方 Deployment 控制器,其实是关注 Deployment 和 ReplicaSet 中的 event收到事件后会加入到队列中。而 Deployment controller 从队列中取出来之后,它的逻辑会判断 Check Paused这个 Paused 其实是 Deployment 是否需要新的发布,如果 Paused 设置为 true 的话,就表示这个 Deployment 只会做一个数量上的维持,不会做新的发布。
如上图,可以看到如果 Check paused 为 Yes 也就是 true 的话,那么只会做 Sync replicas。也就是说把 replicas sync 同步到对应的 ReplicaSet 中,最后再 Update Deployment status那么 controller 这一次的 ReplicaSet 就结束了。
那么如果 paused 为 false 的话,它就会做 Rollout也就是通过 Create 或者是 Rolling 的方式来做更新,更新的方式其实也是通过 Create/Update/Delete 这种 ReplicaSet 来做实现的。
ReplicaSet 控制器
当 Deployment 分配 ReplicaSet 之后ReplicaSet 控制器本身也是从 Informer 中 watch 一些事件,这些事件包含了 ReplicaSet 和 Pod 的事件。从队列中取出之后ReplicaSet controller 的逻辑很简单,就只管理副本数。也就是说如果 controller 发现 replicas 比 Pod 数量大的话,就会扩容,而如果发现实际数量超过期望数量的话,就会删除 Pod。
上面 Deployment 控制器的图中可以看到Deployment 控制器其实做了更复杂的事情,包含了版本管理,而它把每一个版本下的数量维持工作交给 ReplicaSet 来做。
扩/缩容模拟
下面来看一些操作模拟,比如说扩容模拟。这里有一个 Deployment它的副本数是 2对应的 ReplicaSet 有 Pod1 和 Pod2。这时如果我们修改 Deployment replicas controller 就会把 replicas 同步到当前版本的 ReplicaSet 中,这个 ReplicaSet 发现当前有 2 个 Pod不满足当前期望 3 个,就会创建一个新的 Pod3。
发布模拟
我们再模拟一下发布,发布的情况会稍微复杂一点。这里可以看到 Deployment 当前初始的 template比如说 template1 这个版本。template1 这个 ReplicaSet 对应的版本下有三个 PodPod1Pod2Pod3。
这时修改 template 中一个容器的 image Deployment controller 就会新建一个对应 template2 的 ReplicaSet。创建出来之后 ReplicaSet 会逐渐修改两个 ReplicaSet 的数量,比如它会逐渐增加 ReplicaSet2 中 replicas 的期望数量,而逐渐减少 ReplicaSet1 中的 Pod 数量。
那么最终达到的效果是:新版本的 Pod 为 Pod4、Pod5和Pod6旧版本的 Pod 已经被删除了,这里就完成了一次发布。
回滚模拟
来看一下回滚模拟,根据上面的发布模拟可以知道 Pod4、Pod5、Pod6 已经发布完成。这时发现当前的业务版本是有问题的,如果做回滚的话,不管是通过 rollout 命令还是通过回滚修改 template它其实都是把 template 回滚为旧版本的 template1。
这个时候 Deployment 会重新修改 ReplicaSet1 中 Pod 的期望数量,把期望数量修改为 3 个,且会逐渐减少新版本也就是 ReplicaSet2 中的 replica 数量,最终的效果就是把 Pod 从旧版本重新创建出来。
发布模拟的图中可以看到,其实初始版本中 Pod1、Pod2、Pod3 是旧版本,而回滚之后其实是 Pod7、Pod8、Pod9。就是说它的回滚并不是把之前的 Pod 重新找出来,而是说重新创建出符合旧版本 template 的 Pod。
spec 字段解析
最后再来简单看一些 Deployment 中的字段解析。首先看一下 Deployment 中其他的 spec 字段:
MinReadySecondsDeployment 会根据 Pod ready 来看 Pod 是否可用,但是如果我们设置了 MinReadySeconds 之后,比如设置为 30 秒,那 Deployment 就一定会等到 Pod ready 超过 30 秒之后才认为 Pod 是 available 的。Pod available 的前提条件是 Pod ready但是 ready 的 Pod 不一定是 available 的,它一定要超过 MinReadySeconds 之后,才会判断为 available
revisionHistoryLimit保留历史 revision即保留历史 ReplicaSet 的数量,默认值为 10 个。这里可以设置为一个或两个,如果回滚可能性比较大的话,可以设置数量超过 10
pausedpaused 是标识Deployment 只做数量维持,不做新的发布,这里在 Debug 场景可能会用到;
progressDeadlineSeconds前面提到当 Deployment 处于扩容或者发布状态时,它的 condition 会处于一个 processing 的状态processing 可以设置一个超时时间。如果超过超时时间还处于 processing那么 controller 将认为这个 Pod 会进入 failed 的状态。
升级策略字段解析
最后来看一下升级策略字段解析。
Deployment 在 RollingUpdate 中主要提供了两个策略,一个是 MaxUnavailable另一个是 MaxSurge。这两个字段解析的意思可以看下图中详细的 comment或者简单解释一下
MaxUnavailable滚动过程中最多有多少个 Pod 不可用;
MaxSurge滚动过程中最多存在多少个 Pod 超过预期 replicas 数量。
上文提到ReplicaSet 为 3 的 Deployment 在发布的时候可能存在一种情况:新版本的 ReplicaSet 和旧版本的 ReplicaSet 都可能有两个 replicas加在一起就是 4 个,超过了我们期望的数量三个。这是因为我们默认的 MaxUnavailable 和 MaxSurge 都是 25%,默认 Deployment 在发布的过程中,可能有 25% 的 replica 是不可用的,也可能超过 replica 数量 25% 是可用的,最高可以达到 125% 的 replica 数量。
这里其实可以根据用户实际场景来做设置。比如当用户的资源足够,且更注重发布过程中的可用性,可设置 MaxUnavailable 较小、MaxSurge 较大。但如果用户的资源比较紧张,可以设置 MaxSurge 较小,甚至设置为 0这里要注意的是 MaxSurge 和 MaxUnavailable 不能同时为 0。
理由不难理解,当 MaxSurge 为 0 的时候,必须要删除 Pod才能扩容 Pod如果不删除 Pod 是不能新扩 Pod 的,因为新扩出来的话,总共的 Pod 数量就会超过期望数量。而两者同时为 0 的话MaxSurge 保证不能新扩 Pod而 MaxUnavailable 不能保证 ReplicaSet 中有 Pod 是 available 的,这样就会产生问题。所以说这两个值不能同时为 0。用户可以根据自己的实际场景来设置对应的、合适的值。
本节总结
本节课的主要内容就到此为止了,这里为大家简单总结一下。
Deployment 是 Kubernetes 中常见的一种 Workload支持部署管理多版本的 Pod
Deployment 管理多版本的方式,是针对每个版本的 template 创建一个 ReplicaSet由 ReplicaSet 维护一定数量的 Pod 副本,而 Deployment 只需要关心不同版本的 ReplicaSet 里要指定多少数量的 Pod
因此Deployment 发布部署的根本原理,就是 Deployment 调整不同版本 ReplicaSet 里的终态副本数,以此来达到多版本 Pod 的升级和回滚。

View File

@ -0,0 +1,286 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 应用配置管理
本节课程要点
ConfigMaps 和 Secret 资源的创建和使用;
Pod 身份认证的实现和原理;
容器资源、安全、前置校验等配置和使用。
细分为以下八个方面:
需求来源
背景问题
首先一起来看一下需求来源。大家应该都有过这样的经验,就是用一个容器镜像来启动一个 container。要启动这个容器其实有很多需要配套的问题待解决
第一,比如说一些可变的配置。因为我们不可能把一些可变的配置写到镜像里面,当这个配置需要变化的时候,可能需要我们重新编译一次镜像,这个肯定是不能接受的;
第二就是一些敏感信息的存储和使用。比如说应用需要使用一些密码,或者用一些 token
第三就是我们容器要访问集群自身。比如我要访问 kube-apiserver那么本身就有一个身份认证的问题
第四就是容器在节点上运行之后,它的资源需求;
第五个就是容器在节点上,它们是共享内核的,那么它的一个安全管控怎么办?
最后一点我们说一下容器启动之前的一个前置条件检验。比如说,一个容器启动之前,我可能要确认一下 DNS 服务是不是好用?又或者确认一下网络是不是联通的?那么这些其实就是一些前置的校验。
Pod 的配置管理
在 Kubernetes 里面,它是怎么做这些配置管理的呢?如下图所示:
可变配置就用 ConfigMap
敏感信息是用 Secret
身份认证是用 ServiceAccount 这几个独立的资源来实现的;
资源配置是用 Resources
安全管控是用 SecurityContext
前置校验是用 InitContainers 这几个在 spec 里面加的字段,来实现的这些配置管理。
ConfigMap
ConfigMap 介绍
下面我们来介绍第一个部分,就是 ConfigMap。我们先来介绍 ConfigMap 它是用来做什么的、以及它带来的一个好处。它其实主要是管理一些可变配置信息,比如说我们应用的一些配置文件,或者说它里面的一些环境变量,或者一些命令行参数。
它的好处在于它可以让一些可变配置和容器镜像进行解耦,这样也保证了容器的可移植性。看一下下图中右边的编排文件截图。
这是 ConfigMap 本身的一个定义,它包括两个部分:一个是 ConfigMap 元信息,我们关注 name 和 namespace 这两个信息。接下来这个 data 里面可以看到它管理了两个配置文件。它的结构其实是这样的从名字看ConfigMap中包含Map单词Map 其实就是 key:valuekey 是一个文件名value 是这个文件的内容。
ConfigMap 创建
看过介绍之后,再具体看一下它是怎么创建的。我们推荐用 kubectl 这个命令来创建,它带的参数主要有两个:一个是指定 name第二个是 DATA。其中 DATA 可以通过指定文件或者指定目录,以及直接指定键值对,下面可以看一下这个例子。
指定文件的话,文件名就是 Map 中的 key文件内容就是 Map 中的 value。然后指定键值对就是指定数据键值对key:value 形式,直接映射到 Map 的key:value。
ConfigMap 使用
创建完了之后,应该怎么使用呢?
如上图所示,主要是在 pod 里来使用 ConfigMap
第一种是环境变量。环境变量的话通过 valueFrom然后 ConfigMapKeyRef 这个字段,下面的 name 是指定 ConfigMap 名key 是 ConfigMap.data 里面的 key。这样的话在 busybox 容器启动后容器中执行 env 将看到一个 SPECIAL*LEVEL*KEY 环境变量;
第二个是命令行参数。命令行参数其实是第一行的环境变量直接拿到 cmd 这个字段里面来用;
最后一个是通过 volume 挂载的方式直接挂到容器的某一个目录下面去。上面的例子是把 special-config 这个 ConfigMap 里面的内容挂到容器里面的 /etc/config 目录下,这个也是使用的一种方式。
ConfigMap 注意要点
现在对 ConfigMap 的使用做一个总结,以及它的一些注意点,注意点一共列了以下五条:
ConfigMap 文件的大小。虽然说 ConfigMap 文件没有大小限制,但是在 ETCD 里面,数据的写入是有大小限制的,现在是限制在 1MB 以内;
第二个注意点是 pod 引入 ConfigMap 的时候,必须是相同的 Namespace 中的 ConfigMap前面其实可以看到ConfigMap.metadata 里面是有 namespace 字段的;
第三个是 pod 引用的 ConfigMap。假如这个 ConfigMap 不存在,那么这个 pod 是无法创建成功的,其实这也表示在创建 pod 前,必须先把要引用的 ConfigMap 创建好;
第四点就是使用 envFrom 的方式。把 ConfigMap 里面所有的信息导入成环境变量时,如果 ConfigMap 里有些 key 是无效的,比如 key 的名字里面带有数字,那么这个环境变量其实是不会注入容器的,它会被忽略。但是这个 pod 本身是可以创建的。这个和第三点是不一样的方式,是 ConfigMap 文件存在基础上,整体导入成环境变量的一种形式;
最后一点是:什么样的 pod 才能使用 ConfigMap这里只有通过 K8s api 创建的 pod 才能使用 ConfigMap比如说通过用命令行 kubectl 来创建的 pod肯定是可以使用 ConfigMap 的,但其他方式创建的 pod比如说 kubelet 通过 manifest 创建的 static pod它是不能使用 ConfigMap 的。
Secret
Secret 介绍
现在我们讲一下 SecretSecret 是一个主要用来存储密码 token 等一些敏感信息的资源对象。其中,敏感信息是采用 base-64 编码保存起来的,我们来看下图中 Secret 数据的定义。
元数据的话,里面主要是 name、namespace 两个字段;接下来是 type它是非常重要的一个字段是指 Secret 的一个类型。Secret 类型种类比较多,下面列了常用的四种类型:
第一种是 Opaque它是普通的 Secret 文件;
第二种是 service-account-token是用于 service-account 身份认证用的 Secret
第三种是 dockerconfigjson这是拉取私有仓库镜像的用的一种 Secret
第四种是 bootstrap.token是用于节点接入集群校验用的 Secret。
再接下来是 data是存储的 Secret 的数据,它也是 key-value 的形式存储的。
Secret 创建
接下来我们看一下 Secret 的创建。
如上图所示,有两种创建方式:
系统创建:比如 K8s 为每一个 namespace 的默认用户default ServiceAccount创建 Secret
用户手动创建:手动创建命令,推荐 kubectl 这个命令行工具,它相对 ConfigMap 会多一个 type 参数。其中 data 也是一样它也是可以指定文件和键值对的。type 的话,要是你不指定的话,默认是 Opaque 类型。
上图中两个例子。第一个是通过指定文件,创建了一个拉取私有仓库镜像的 Secret指定的文件是 /root/.docker/config.json。type 的话指定的是 dockerconfigjson另外一个我们指定键值对我们 type 没有指定,默认是 Opaque。键值对是 key:value 的形式,其中对 value 内容进行 base64 加密。创建 Secret 就是这么一个情况。
Secret 使用
创建完 Secret 之后,再来看一下如何使用它。它主要是被 pod 来使用,一般是通过 volume 形式挂载到容器里指定的目录,然后容器里的业务进程再到目录下读取 Secret 来进行使用。另外在需要访问私有镜像仓库时,也是通过引用 Secret 来实现。
我们先来看一下挂载到用户指定目录的方式:
第一种方式:如上图左侧所示,用户直接指定,把 mysecret 挂载到容器 /etc/foo 目录下面;
第二种方式:如上图右侧所示,系统自动生成,把 serviceaccount-secret 自动挂载到容器 /var/run/secrets/kubernetes.io/serviceaccount 目录下,它会生成两个文件,一个是 ca.crt一个是 token。这是两个保存了认证信息的证书文件。
使用私有镜像库
下面看一下用 Secret 来使用私有镜像仓库。首先,私有镜像仓库的信息是存储在 Secret 里面的(具体参照上述的Secret创建章节),然后拉取私有仓库镜像,那么通过下图中两种方法的配置就可以:
第一种方式:如下图左侧所示,直接在 pod 里面,通过 imagePullSecrets 字段来配置;
第二种方式是自动注入。用户提前在 pod 会使用的 serviceaccount 里配置 imagePullSecretsPod创建时系统自动注入这个 imagePullSecrets。
Secret 使用注意要点
最后来看一下 Secret 使用的一些注意点,下面列了三点:
第一个是 Secret 的文件大小限制。这个跟 ConfigMap 一样,也是 1MB
第二个是 Secret 采用了 base-64 编码,但是它跟明文也没有太大区别。所以说,如果有一些机密信息要用 Secret 来存储的话,还是要很慎重考虑。也就是说谁会来访问你这个集群,谁会来用你这个 Secret还是要慎重考虑因为它如果能够访问这个集群就能拿到这个 Secret。
如果是对 Secret 敏感信息要求很高,对加密这块有很强的需求,推荐可以使用 Kubernetes 和开源的 vault做一个解决方案来解决敏感信息的加密和权限管理。
第三个就是 Secret 读取的最佳实践,建议不要用 list/watch如果用 list/watch 操作的话,会把 namespace 下的所有 Secret 全部拉取下来,这样其实暴露了更多的信息。推荐使用 GET 的方法,这样只获取你自己需要的那个 Secret。
ServiceAccount
ServiceAccount 介绍
接下来,我们讲一下 ServiceAccount。ServiceAccount 首先是用于解决 pod 在集群里面的身份认证问题,身份认证信息是存在于 Secret 里面。
先看一下上面的左侧截图,可以看到最下面的红框里,有一个 Secret 字段,它指定 ServiceAccount 用哪一个 Secret这个是 K8s 自动为 ServiceAccount 加上的。然后再来看一下上图中的右侧截图,它对应的 Secret 的 data 里有两块数据,一个是 ca.crt一个是 token。ca.crt 用于对服务端的校验token 用于 Pod 的身份认证,它们都是用 base64 编码过的。然后可以看到 metadata 即元信息里,其实是有关联 ServiceAccount 信息的(这个 secret 被哪个 ServiceAccount 使用)。最后我们注意一下 type这个就是 service-account-token 这种类型。
举例Pod 里的应用访问它所属的 K8s 集群
介绍完 ServiceAccount 以及它对应的 secret 后我们来看一下pod 是怎么利用 ServiceAccount 或者说它是怎么利用 secret 来访问所属 K8s 集群的。
其实 pod 创建的时候,首先它会把这个 secret 挂载到容器固定的目录下,这是 K8s 功能上实现的。它要把这个 ca.crt 和 token 这两个文件挂载到固定目录下面。
pod 要访问集群的时候,它是怎么来利用这个文件的呢?我们看一下下面的代码截图:
我们在 Go 里面实现 Pod 访问 K8s 集群时,一般直接会调一个 InClusterConfig 方法,来生成这个访问服务 Client 的一些信息。然后可以看一下,最后这个 Config 里面有两部分信息:
一个是 tlsClientConfig这个主要是用于 ca.crt 校验服务端;
第二个是 Bearer Token这个就是 pod 的身份认证。在服务端,会利用 token 对 pod 进行一个身份认证。
再次回到上图左侧。认证完之后 pod 的身份信息会有两部分:一个是 Group一个是 User。身份认证是就是认证这两部分信息。接着可以使用 RBAC 功能,对 pod 进行一个授权管理。
假如 RBAC 没有配置的话,默认的 pod 具有资源 GET 权限,就是可以从所属的 K8s 集群里 get 数据。如果是需要更多的权限,那么就需要 自行配置 RBAC 。RBAC 的相关知识,我们在后面的课程里面会详细介绍,大家可以关注一下。
Resource
容器资源配合管理
下面介绍一下 Resource容器的一个资源配置管理。
目前内部支持类型有三种CPU、内存以及临时存储。当用户觉得这三种不够有自己的一些资源比如说 GPU或者其他资源也可以自己来定义但配置时指定的数量必须为整数。目前资源配置主要分成 request 和 limit 两种类型一个是需要的数量一个是资源的界限。CPU、内存以及临时存储都是在 container 下的 Resource 字段里进行一个声明。
举个例子wordpress 容器的资源需求,一个是 request ,一个是 limits它分别对需要的资源和资源临界进行一个声明。
Pod 服务质量 (QoS) 配置
根据 CPU 对容器内存资源的需求,我们对 pod 的服务质量进行一个分类,分别是 Guaranteed、Burstable 和 BestEffort。
Guaranteed pod 里面每个容器都必须有内存和 CPU 的 request 以及 limit 的一个声明,且 request 和 limit 必须是一样的,这就是 Guaranteed
BurstableBurstable 至少有一个容器存在内存和 CPU 的一个 request
BestEffort只要不是 Guaranteed 和 Burstable那就是 BestEffort。
那么这个服务质量是什么样的呢?资源配置好后,当这个节点上 pod 容器运行,比如说节点上 memory 配额资源不足kubelet会把一些低优先级的或者说服务质量要求不高的BestEffort、Burstablepod 驱逐掉。它们是按照先去除 BestEffort再去除 Burstable 的一个顺序来驱逐 pod 的。
SecurityContext
SecurityContext 介绍
SecurityContext 主要是用于限制容器的一个行为,它能保证系统和其他容器的安全。这一块的能力不是 Kubernetes 或者容器 runtime 本身的能力,而是 Kubernetes 和 runtime 通过用户的配置,最后下传到内核里,再通过内核的机制让 SecurityContext 来生效。所以这里讲的内容,会比较简单或者说比较抽象一点。
SecurityContext 主要分为三个级别:
第一个是容器级别,仅对容器生效;
第二个是 pod 级别,对 pod 里所有容器生效;
第三个是集群级别,就是 PSP对集群内所有 pod 生效。
权限和访问控制设置项,现在一共列有七项(这个数量后续可能会变化):
第一个就是通过用户 ID 和组 ID 来控制文件访问权限;
第二个是 SELinux它是通过策略配置来控制用户或者进程对文件的访问控制
第三个是特权容器;
第四个是 Capabilities它也是给特定进程来配置一个 privileged 能力;
第五个是 AppArmor它也是通过一些配置文件来控制可执行文件的一个访问控制权限比如说一些端口的读写
第六个是一个对系统调用的控制;
第七个是对子进程能否获取比父亲更多的权限的一个限制。
最后其实都是落到内核来控制它的一些权限。
上图是对 pod 级别和容器级别配置 SecurityContext 的一个例子,如果大家对这些内容有更多的需求,可以根据这些信息去搜索更深入的资料来学习。
InitContainer
InitContainer 介绍
接下来看一下 InitContainer首先介绍 InitContainer 和普通 container 的区别,有以下三点内容:
InitContainer 首先会比普通 container 先启动,并且直到所有的 InitContainer 执行成功后,普通 container 才会被启动;
InitContainer 之间是按定义的次序去启动执行的,执行成功一个之后再执行第二个,而普通的 container 是并发启动的;
InitContainer 执行成功后就结束退出,而普通容器可能会一直在执行。它可能是一个 longtime 的,或者说失败了会重启,这个也是 InitContainer 和普通 container 不同的地方。
根据上面三点内容,我们看一下 InitContainer 的一个用途。它其实主要为普通 container 服务,比如说它可以为普通 container 启动之前做一个初始化,或者为它准备一些配置文件, 配置文件可能是一些变化的东西。再比如做一些前置条件的校验,如网络是否联通。
上面的截图是 flannel 组件的 InitContainer 的一个配置,它的 InitContainer 主要是为 kube-flannel 这个普通容器启动之前准备一些网络配置文件。
结束语
ConfigMap 和 Secret: 首先介绍了 ConfigMap 和 Secret 的创建方法和使用场景,然后对 ConfigMap 和 Secret 的常见使用注意点进行了分类和整理。最后介绍了私有仓库镜像的使用和配置;
Pod 身份认证: 首先介绍了 ServiceAccount 和 Secret 的关联关系,然后从源码角度对 Pod 身份认证流程和实现细节进行剖析,同时引出了 Pod 的权限管理(即 RBAC 的配置管理)
容器资源和安全: 首先介绍了容器常见资源类型 (CPU/Memory) 的配置,然后对 Pod 服务质量分类进行详细的介绍。同时对 SecurityContext 有效层级和权限配置项进行简要说明;
InitContainer: 首先介绍了 InitContainer 和普通 container 的区别以及 InitContainer 的用途。然后基于实际用例对 InitContainer 的用途进行了说明。
好的,我们今天的内容讲到这里,谢谢大家。