mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-20 08:03:43 +08:00
del
This commit is contained in:
180
极客时间专栏/geek/Android开发高手课/模块三 架构演进/34 | 聊聊重构:优秀的架构都是演进而来的.md
Normal file
180
极客时间专栏/geek/Android开发高手课/模块三 架构演进/34 | 聊聊重构:优秀的架构都是演进而来的.md
Normal file
@@ -0,0 +1,180 @@
|
||||
<audio id="audio" title="34 | 聊聊重构:优秀的架构都是演进而来的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8a/88/8ac0a596d1de125566a9ca4094959188.mp3"></audio>
|
||||
|
||||
每个程序员心中都有一个成为架构师的梦想,那成为架构师这个目标是否“遥不可及”呢?从我的工作经历来看,我一共负责过搜狗输入法、微信等4款亿级产品的架构工作,可能有同学会好奇这些大型的App是如何做架构设计的。从我接手的这些应用的现实情况来看,看似光鲜的外表下都有一颗千疮百孔的心:各种日志随便输出、单例满天飞、生命周期混乱、线程乱创建、线程不安全这些问题随处可见。
|
||||
|
||||
所以你可以看到每个大型应用都背负着沉重的历史技术债务,架构师很重要的一项工作就是重构“老态龙钟”的陈旧架构。在接下来的“架构演进”模块中,我们一起学习架构该如何的重构和演进,帮助我们及时偿还这些“历史债务”。
|
||||
|
||||
虽然我们天天都在谈“架构”,那你有没有想过究竟什么是架构呢?
|
||||
|
||||
## 什么是架构
|
||||
|
||||
什么是架构,每个人都有自己的看法。在我看来,所谓的架构就是面对业务需求场景给出合适的解决方案,使业务能够快速迭代,从而达到“提质增效”的目标。
|
||||
|
||||
我先举个例子,告诉你什么是架构,以及架构的作用。我曾经为了解决UI渲染卡顿这个需求场景,我们设计了异步创建View、异步布局与主线程渲染这个架构。不过架构只是设计的抽象,对于具体的实现,我们可以称之为框架。好的框架可以隐藏大家不需要关心的部分,提升我们的效率。例如Facebook的Litho、微信的[Vending](https://gryamy.gitbooks.io/vending-doc/content/),它们通过框架约束和异步来解决Android应用UI线程卡顿问题。
|
||||
|
||||
如果说监控是为了发现问题,核心在于“防”,那好的架构可以直接避免出现问题,所以架构设计的目标在于“治”。为了帮助你更好地理解架构,我们先从Android的架构设计说起。
|
||||
|
||||
**1. Android的架构**
|
||||
|
||||
在官方文档[《平台架构》](https://developer.android.com/guide/platform?hl=zh-cn)中,对Android的描述如下:
|
||||
|
||||
>
|
||||
Android是一种基于Linux的开放源代码软件栈,为广泛的设备和机型而创建。
|
||||
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/df/90763fd9662c8a75553dc92a78112ddf.png" alt="">
|
||||
|
||||
在深入Android架构之前,我们先来思考一下Android需要满足的需求场景,也就是各方对它的诉求。
|
||||
|
||||
<li>
|
||||
**用户**。在低内存的设备上也可以流畅地运行各种应用,并且有持久的续航能力。
|
||||
</li>
|
||||
<li>
|
||||
**开发者**。应用开发简便,并且可以在平台上获得收益。
|
||||
</li>
|
||||
<li>
|
||||
**硬件厂商**。无论是芯片厂商还是手机制造商,都可以低成本地适配和升级自己的系统。
|
||||
</li>
|
||||
|
||||
回想Android诞生之初,它为了团结一切可以团结的力量,广泛取得硬件厂商、开发者以及用户的支持。所以在做架构设计的时候,就充分考虑到了这些因素。
|
||||
|
||||
<li>
|
||||
**Java API接口层**。长久以来Java一直霸占第一大编程语言宝座,具有广泛的开发者基础。从当时来看,Android选择Java作为接口语言是十分明智的。从现在看,只是没有在Oracle之前抢先收购Sun是比较失策的,导致出现当前的专利诉讼困局。总的来说,开发者通过自身熟悉的Java语言,友好的接口层就可以使用系统和硬件的各种能力,快速搭建出自己的应用。
|
||||
</li>
|
||||
<li>
|
||||
**硬件抽象层**。设备制造商需要为内核开发硬件驱动程序,为了降低设备制造商的开发成本,Android选择成熟、开源的Linux内核,并且将Android打造成一个免费、开源的操作系统,吸引了更多的手机厂商入局。但是芯片厂商和手机厂商还是需要一起做大量的工作才可以更新到最新的系统,在Android 8.0之后,Android新增了HAL硬件抽象层。它向上屏蔽了硬件的具体实现,使小米这些手机厂商可以跳过芯片厂商,单独更新Android的Framework框架。
|
||||
</li>
|
||||
<li>
|
||||
**应用层**。在Android设计之初,它就考虑到移动设备的各种限制。为了用户在低内存设备有更好的体验,同样做了大量的工作。例如设计了基于寄存器架构、可执行文件更小的Dalvik虚拟机以及内核的Low Memory Killer等。为了满足用户的基本需求,Android系统在推出之初就内置许多的基础应用。并且通过吸引更多开发者进入,也在不断地满足用户的各种需求场景。当然,Android也在持续优化Framework和Runtime的性能,例如我前面提到过的黄油计划、耗电优化等。
|
||||
</li>
|
||||
|
||||
架构是为了需求场景服务,而Android的架构正是为了更好地满足硬件设备厂商、开发者以及用户而设计的。我非常推荐你看看[《关于Android设计及其意义》](https://mp.weixin.qq.com/s/twfpUMf9CfXcgwtFFkJ4Ig)和[《Android技术架构演进与未来》](http://mp.weixin.qq.com/s/W38aauoCEEUbL8KvUkb_Rw),可以让你从Android系统设计到技术支撑系统发展有更加深刻的理解。
|
||||
|
||||
**2. 如何做架构选型**
|
||||
|
||||
对于Android开发者来说,很多架构和框架已经非常成熟,通常我们更多面临的问题是如何为自己的应用选择合适的框架。回顾一下专栏前面学习过的内容,其实我们已经做过一次次的选择,例如OkHttp、Cronet、Mars应该选择哪个作为我们的高质量网络库,JSON、Protocol Buffers数据序列化方案该如何选择等。
|
||||
|
||||
网络库、图片库、UI框架、消息通信框架、存储框架,无论是GitHub还是Google官方都有非常多的方案,在选择过程我们主要要考虑下面三个因素:
|
||||
|
||||
<li>
|
||||
**框架的成熟度**。框架是否已经被大量应用所实践,特别是亿级以上的应用。还有就是框架目前是否还在维护、框架的性能如何等。
|
||||
</li>
|
||||
<li>
|
||||
**工具链的成熟度**。配套的工具链是否成熟、完善。例如Flutter作为一门新的技术,从开发、编译、测试到发布,是否有完善的工具链支持。
|
||||
</li>
|
||||
<li>
|
||||
**人员的学习成本、文档是否完备**。结合团队的现状,需要考虑框架的学习成本是否可以接受、学习路径是否平滑、有没有足够的文档和社区支持。
|
||||
</li>
|
||||
|
||||
对于架构选型,康威定律是比较重要的准则,这里推荐你看看[《从康威定律和技术债看研发之痛》](https://blog.csdn.net/junecauzhang/article/details/61427915)这篇文章,我们的组织架构、代码架构以及流程都应该跟我们团队的规模相匹配。这句话怎么理解呢?就是架构设计或者架构选型不能好高骛远,我们有多大的规模,就做多少的支撑。警惕长期的事情短期做,或者短期的事情长期做。
|
||||
|
||||
微信在2013年就开始了模块化改造,与此同时淘宝则进行了组件化改造。为什么会有这样的差别呢?因为当时微信只有一个团队在开发,Android端也就30人不到。为了代码的隔离,微信将基础组件下沉,放到单独的仓库,由专门的人员负责。对于业务来说,依然只需要保留同一个仓库,只是拆成不同的业务Module。感兴趣的同学可以参考[《微信Android模块化架构重构实践》](https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/9c/4ff7f07b2bdec613bc460b9e9dec719c.png" alt="">
|
||||
|
||||
但对于淘宝来说,当时就有几百人同时在一个应用上面开发,而且这些人分别属于不同的团队,分散在全国各地。所以无论是基于代码的权限保护,还是从开发效率的考量,都要求将所有业务模块隔离开来,也就是每个业务模块都应该是单独的仓库。
|
||||
|
||||
## 什么是架构演进
|
||||
|
||||
“没有过不去的坎,只有过不完的坎”。在业务发展的过程,总会遇到一些新的问题,而且可能在发展到某一时刻时,一些旧的问题就不复存在了。例如为了兼容Android 4.X,当年我们在架构上做了大量的兼容设计,但是当不再需要兼容4.X设备的时候,这些包袱我们就可以适时抛弃掉。
|
||||
|
||||
**1. 为什么要做架构演进**
|
||||
|
||||
架构是为了业务需求场景服务,那它也要顺应业务的变化而适时调整。也就是说,架构需要跟随业务的发展而演进。
|
||||
|
||||
“君有疾在腠理,不治将恐深”,微信每年都会经历一次大的重构,因为我们坚持代码架构最终都会腐烂,该推倒了就该重构,不要一直修修补补。架构演进可以给团队带来下面几个变化:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/d0/d08aff81147f83407fe9deb3d6bcddd0.png" alt="">
|
||||
|
||||
<li>
|
||||
**打破不满**。需要打破保守的做法,要积极面对不合理的地方。团队定期需要着手开启重构,将大家平日对代码的不满释放出来。将架构的腐化(效率降低、抱怨上升)转化为架构优化的动力。
|
||||
</li>
|
||||
<li>
|
||||
**重构信任**。重构开发者之间的心态,不定期的推动模块重构。一些问题的解决,往往可以推动更多人去尝试。
|
||||
</li>
|
||||
<li>
|
||||
**团队培养**。重构也是团队进步的机会,让更多的成员掌握架构能力,培养全员架构意识,实现“人人都是架构师”。
|
||||
</li>
|
||||
|
||||
但是对于架构演进的过程,我们需要有辨别能力,也就是常说的“技术视野”。这里包括对各种技术栈的选择和比较、架构设计的考虑,要结合业务和团队当前的情况,做出合理的判断,要清楚的知道做什么事情收益最大等。
|
||||
|
||||
这里的反面例子可能就是辛辛苦苦造了一套轮子,结果发现别人早就有了,甚至比我们做得更好。这个问题的原因就在于你的技术视野。在“高质量开发”和“高效开发”模块,我反复地跟你分享目前国内外大厂的最新实践方案,正是希望提升我们的技术视野。特别是“高效开发”模块,可能有同学会认为这些话题太大了,跟自己好像关系不大。其实应用开发流程的每一个步骤都关系到你我,同时又涉及大量的内容,每一块铺开来可能都可以是一个新的专栏。而作为“Android开发高手课”,我更想从顶层给你呈现完整的架构设计,而不是去详细分析某个细节优化点。这些都是希望可以帮你站在高处看问题,全面提升你的技术视野。
|
||||
|
||||
对于技术视野的培养,可能没有太多的捷径,需要我们经过长时间的实践,经历反反复复的挫折,才能从“巨婴”成长为“大师”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cb/9d/cb31d2a5f5c10ea23a3e99a3aa35c39d.png" alt="">
|
||||
|
||||
在“架构演进”模块,为了进一步帮助我们提升技术视野和架构的能力,我准备了下面这些内容:
|
||||
|
||||
<li>
|
||||
GOT Hook、PLT Hook、Inline Hook,我将对比三种Native Hook框架的原理和差异,告诉你应该如何去选择。以及对于Native开发,我还有哪些经验。
|
||||
</li>
|
||||
<li>
|
||||
[大前端纷纷扰扰,如何演进和选择](https://mp.weixin.qq.com/s/WWqsd-SnILUWbiKEnSArDQ),如何选择适合我们应用的跨平台和动态化方案。
|
||||
</li>
|
||||
<li>
|
||||
技术日新月异,对于新技术我们应该如何考虑是否跟进,Flutter是不是真的可以一统天下。
|
||||
</li>
|
||||
<li>
|
||||
在应用开发之外,我们还邀请了三位专家,分别介绍他们在移动游戏、音视频和AI领域开发的经验。对于这些领域,它们的架构是什么样的,我们又该如何学习和转型。
|
||||
</li>
|
||||
|
||||
**2. 如何做架构演进**
|
||||
|
||||
架构演进是必要的,但是我们需要充分认识到困难,真正去做远比想要难多了,特别是其中各种各样的历史包袱问题。
|
||||
|
||||
架构的演进,通常来说具体实践方式就是重构。如果我们下定决心要重构,我有两个小建议送给你:
|
||||
|
||||
- **演进式的,符合团队现状的**。我们很难找到一个性能最好、成本最省、时间最快的方案,需要权衡性能、成本、时间三者的关系。如果我们时间充裕,那可以朝着更好的性能目标去努力。但如果时间紧急,我们可以分阶段去重构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/df/d1a3a57aa97e4a510e39a00c88acd8df.png" alt="">
|
||||
|
||||
- **可度量的,每个阶段都要有成果**。架构演进不能一味“憋大招”,最好能分阶段实施,并且每个阶段都要有成果。这样可以让团队成员更快地感受到优化成果,也可以激励更多的人参与到重构的事业中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/20/771f780657230e86ef33d99a11bef520.png" alt="">
|
||||
|
||||
在《Android技术架构演进与未来》一文中,回顾了Android版本的发布时间线。Android系统每年都会发布一个新的版本,每个版本也会有大大小小的重构。重构的目的依然是希望更好地满足用户、开发者以及硬件厂商的诉求。例如为了提升手机的续航能力,我们可以回顾一下Android在耗电优化的演进历程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/8c/0035fdf871970fe18ebc3a89bfc5c68c.png" alt="">
|
||||
|
||||
为了应用程序执行速度更快,Android Runtime也是每个版本都会优化的模块,下面是Android Runtime各个版本的演进历程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/c0/ee4c0ff385955162fc9d77ffdf7d9ac0.png" alt="">
|
||||
|
||||
对于虚拟机的运行机制与各个版本的差异,也是很多公司在面试时喜欢问的。下面是一些关于虚拟机架构演进比较不错的资料,我把它们分享给你。
|
||||
|
||||
<li>
|
||||
[What’s new in Android Runtime (Google I/O '18)](https://www.youtube.com/watch?v=Yi9-BqUxsno&list=PLOU2XLYxmsIInFRc3M44HUTQc3b_YJ4-Y&index=19)
|
||||
</li>
|
||||
<li>
|
||||
[Android 8.0中的ART功能改进](https://source.android.com/devices/tech/dalvik/improvements)
|
||||
</li>
|
||||
<li>
|
||||
[oat格式演进](http://romainthomas.fr/oat/)
|
||||
</li>
|
||||
|
||||
而Android 8.0的[Treble](https://source.android.com/devices/architecture#resources)计划,引入了HAL硬件抽象层,解决了硬件厂商升级难的问题。但是即使厂商升级到最新的系统也并不能直接交付给用户,这里还存在应用兼容性的问题。为什么Android P要极力推出Hidden API的限制?这里最初考虑的并不是安全性的问题,而是为了减少每次Android版本升级的兼容性适配时间,让Android版本的发布节奏快起来。
|
||||
|
||||
Hidden API的设计也有出于架构演进的考量,Android不希望出现修改Framework内部任意一个私有方法的时候,都可能会引起外部应用兼容适配,这会对重构带来非常大的包袱。
|
||||
|
||||
Android系统如此,应用的架构演进也是如此。由于组件化带来的各种性能问题,支付宝和淘宝在架构上也顺应了这种变化。在工程结构上,它们依然保留组件在仓库上的代码隔离。但是在最终产物上,组件化已经回归模块化,非核心业务会逐渐迁移到H5或者小程序。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/ba/bd0a6b6b8f236a770bce2acbc947e9ba.png" alt="">
|
||||
|
||||
无论微信、支付宝、淘宝,大家都想当超级App,努力成为满足用户尽可能多需求的微型操作系统。应用的架构也需要顺应业务形态的转变,在[《敏捷开发与动态更新在支付宝 App 内的实践》](https://mp.weixin.qq.com/s/eXzojM0lCaaCW4JxBaU6BQ)一文中,也描述了支付宝这几年在架构升级驱动研发方式转变,推荐你仔细读读。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/ef/e1c5114eb3ce98a98408a1932339bdef.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
从初步接触架构设计,到基本掌握架构的精髓,可以说同样也没有捷径可言。架构设计能力的成长是建立在一个又一个坑、一次又一次的重构之上。不过成为架构师这个目标并不“遥不可及”,在日常工作中我们可以反复进行锻炼。
|
||||
|
||||
架构设计不一定是整个应用或者系统的设计,也可以是一个模块或者一个需求的设计。每接手一个需求,我们可以对自己提更高的要求,更加细致地考虑问题。例如如何对现有代码的影响最小,如何快捷清晰的实现功能,在开发过程中如何对组件、控件做更好的封装,如何去优化性能,有没有哪些新的技术可以帮助开发这个需求等。
|
||||
|
||||
## 课后作业
|
||||
|
||||
一个技术人的一生应该有个代表作,给自己的技术生涯一个交代。在你的工作中,有没有令你感到满意的架构设计(某个应用、某个模块或者某个框架都可以)?你对架构演进有什么看法,又遇到过哪些问题?欢迎留言分享给我和其他同学。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
318
极客时间专栏/geek/Android开发高手课/模块三 架构演进/35 | Native Hook 技术,天使还是魔鬼?.md
Normal file
318
极客时间专栏/geek/Android开发高手课/模块三 架构演进/35 | Native Hook 技术,天使还是魔鬼?.md
Normal file
@@ -0,0 +1,318 @@
|
||||
<audio id="audio" title="35 | Native Hook 技术,天使还是魔鬼?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/09/6db27ce706e7ae726f4f1e27d6358b09.mp3"></audio>
|
||||
|
||||
相信一直坚持学习专栏的同学对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](https://github.com/AndroidAdvanceWithGeektime/Chapter06-plus)中,我们使用了PLT Hook技术来获取线程创建的堆栈。先来回顾一下它的整个流程,我们将libart.so中的外部函数pthread_create替换成自己的方法pthread_create_hook。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/71/f5/715e03d40d7c5f185959e284c23e9df5.png" alt="">
|
||||
|
||||
你可以发现,GOT/PLT Hook主要是用于替换某个SO的外部调用,通过将外部函数调用跳转成我们的目标函数。GOT/PLT Hook可以说是一个非常经典的Hook方法,它非常稳定,可以达到部署到生产环境的标准。
|
||||
|
||||
那GOT/PLT Hook的实现原理究竟是什么呢?你需要先对SO库文件的ELF文件格式和动态链接过程有所了解。
|
||||
|
||||
**ELF格式**
|
||||
|
||||
ELF(Executableand Linking Format)是可执行和链接格式,它是一个开放标准,各种UNIX系统的可执行文件大多采用ELF格式。虽然ELF文件本身就支持三种不同的类型(重定位、执行、共享),不同的视图下格式稍微不同,不过它有一个统一的结构,这个结构如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/96/181d3fed100b9ca11360a03625db5296.png" alt="">
|
||||
|
||||
网上介绍ELF格式的文章非常多,你可以参考[《ELF文件格式解析》](https://felixzhang00.github.io/2016/12/24/2016-12-24-ELF%E6%96%87%E4%BB%B6%E8%A3%85%E8%BD%BD%E9%93%BE%E6%8E%A5%E8%BF%87%E7%A8%8B%E5%8F%8Ahook%E5%8E%9F%E7%90%86/)。顾名思义,对于GOT/PLT Hook来说,我们主要关心“.plt”和“.got”这两个节区:
|
||||
|
||||
<li>
|
||||
**.plt**。该节保存过程链接表(Procedure Linkage Table)。
|
||||
</li>
|
||||
<li>
|
||||
**.got**。该节保存着全局的偏移量表。
|
||||
</li>
|
||||
|
||||
我们也可以使用`readelf -S`来查看ELF文件的具体信息。
|
||||
|
||||
**链接过程**
|
||||
|
||||
接下来我们再来看看动态链接的过程,当需要使用一个Native库(.so文件)的时候,我们需要调用`dlopen("libname.so")`来加载这个库。
|
||||
|
||||
在我们调用了`dlopen("libname.so")`之后,系统首先会检查缓存中已加载的ELF文件列表。如果未加载则执行加载过程,如果已加载则计数加一,忽略该调用。然后系统会用从libname.so的`dynamic`节区中读取其所依赖的库,按照相同的加载逻辑,把未在缓存中的库加入加载列表。
|
||||
|
||||
你可以使用下面这个命令来查看一个库的依赖:
|
||||
|
||||
```
|
||||
readelf -d <library> | grep NEEDED
|
||||
|
||||
```
|
||||
|
||||
下面我们大概了解一下系统是如何加载的ELF文件的。
|
||||
|
||||
<li>
|
||||
读ELF的程序头部表,把所有PT_LOAD的节区mmap到内存中。
|
||||
</li>
|
||||
<li>
|
||||
从“.dynamic”中读取各信息项,计算并保存所有节区的虚拟地址,然后执行重定位操作。
|
||||
</li>
|
||||
<li>
|
||||
最后ELF加载成功,引用计数加一。
|
||||
</li>
|
||||
|
||||
但是这里有一个关键点,在ELF文件格式中我们只有函数的绝对地址。如果想在系统中运行,这里需要经过**重定位**。这其实是一个比较复杂的问题,因为不同机器的CPU架构、加载顺序不同,导致我们只能在运行时计算出这个值。不过还好动态加载器(/system/bin/linker)会帮助我们解决这个问题。
|
||||
|
||||
如果你理解了动态链接的过程,我们再回头来思考一下“.got”和“.plt”它们的具体含义。
|
||||
|
||||
<li>
|
||||
**The Global Offset Table (GOT)**。简单来说就是在数据段的地址表,假定我们有一些代码段的指令引用一些地址变量,编译器会引用GOT表来替代直接引用绝对地址,因为绝对地址在编译期是无法知道的,只有重定位后才会得到 ,GOT自己本身将会包含函数引用的绝对地址。
|
||||
</li>
|
||||
<li>
|
||||
**The Procedure Linkage Table (PLT)**。PLT不同于GOT,它位于代码段,动态库的每一个外部函数都会在PLT中有一条记录,每一条PLT记录都是一小段可执行代码。 一般来说,外部代码都是在调用PLT表里的记录,然后PLT的相应记录会负责调用实际的函数。我们一般把这种设定叫作“[蹦床](http://en.wikipedia.org/wiki/Trampoline_%28computing%29)”(Trampoline)。
|
||||
</li>
|
||||
|
||||
PLT和GOT记录是一一对应的,并且GOT表第一次解析后会包含调用函数的实际地址。既然这样,那PLT的意义究竟是什么呢?PLT从某种意义上赋予我们一种懒加载的能力。当动态库首次被加载时,所有的函数地址并没有被解析。下面让我们结合图来具体分析一下首次函数调用,请注意图中黑色箭头为跳转,紫色为指针。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/33/b86ff32360acd5151050fd30e1762233.png" alt="">
|
||||
|
||||
<li>
|
||||
我们在代码中调用func,编译器会把这个转化为func@plt,并在PLT表插入一条记录。
|
||||
</li>
|
||||
<li>
|
||||
PLT表中第一条(或者说第0条)PLT[0]是一条特殊记录,它是用来帮助我们解析地址的。通常在类Linux系统,这个的实现会位于动态加载器,就是专栏前面文章提到的/system/bin/linker。
|
||||
</li>
|
||||
<li>
|
||||
其余的PLT记录都均包含以下信息:
|
||||
<ul>
|
||||
<li>
|
||||
跳转GOT表的指令(jmp *GOT[n])。
|
||||
</li>
|
||||
<li>
|
||||
为上面提到的第0条解析地址函数准备参数。
|
||||
</li>
|
||||
<li>
|
||||
调用PLT[0],这里resovler的实际地址是存储在GOT[2] 。
|
||||
</li>
|
||||
|
||||
在解析前GOT[n]会直接指向jmp *GOT[n]的下一条指令。在解析完成后,我们就得到了func的实际地址,动态加载器会将这个地址填入GOT[n],然后调用func。
|
||||
|
||||
如果对上面的这个调用流程还有疑问,你可以参考[《GOT表和PLT表》](https://www.jianshu.com/p/0ac63c3744dd)这篇文章,它里面有一张图非常清晰。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/c6/8bacb98e41eaa8ed048294dbf42896c6.png" alt="">
|
||||
|
||||
当第一次调用发生后,之后再调用函数func就高效简单很多。首先调用PLT[n],然后执行jmp *GOT[n]。GOT[n]直接指向func,这样就高效的完成了函数调用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/1e/08d7d70593533b9908426945d16ad91e.png" alt="">
|
||||
|
||||
总结一下,因为很多函数可能在程序执行完时都不会被用到,比如错误处理函数或一些用户很少用到的功能模块等,那么一开始把所有函数都链接好实际就是一种浪费。为了提升动态链接的性能,我们可以使用PLT来实现延迟绑定的功能。
|
||||
|
||||
对于函数运行的实际地址,我们依然需要通过GOT表得到,整个简化过程如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/27/698af1afdf2327233ef5bf2d09df4c27.png" alt="">
|
||||
|
||||
看到这里,相信你已经有了如何Hack这一过程的初步想法。这里业界通常会根据修改PLT记录或者GOT记录区分为GOT Hook和PLT Hook,但其本质原理十分接近。
|
||||
|
||||
**GOT/PLT Hook实践**
|
||||
|
||||
GOT/PLT Hook看似简单,但是实现起来也是有一些坑的,需要考虑兼容性的情况。一般来说,推荐使用业界的成熟方案。
|
||||
|
||||
<li>
|
||||
微信Matrix开源库的[ELF Hook](https://github.com/Tencent/matrix/tree/master/matrix/matrix-android/matrix-android-commons/src/main/cpp/elf_hook),它使用的是GOT Hook,主要使用它来做性能监控。
|
||||
</li>
|
||||
<li>
|
||||
爱奇艺开源的的[xHook](https://github.com/iqiyi/xHook),它使用的也是GOT Hook。
|
||||
</li>
|
||||
<li>
|
||||
Facebook的[PLT Hook](https://github.com/facebookincubator/profilo/tree/master/deps/plthooks)。
|
||||
</li>
|
||||
|
||||
如果不想深入它内部的原理,我们只需要直接使用这些开源的优秀方案就可以了。因为这种Hook方式非常成熟稳定,除了Hook线程的创建,我们还有很多其他的使用范例。
|
||||
|
||||
<li>
|
||||
“I/O优化”中使用[matrix-io-canary](https://github.com/Tencent/matrix/tree/master/matrix/matrix-android/matrix-io-canary) Hook文件的操作。
|
||||
</li>
|
||||
<li>
|
||||
“网络优化”中使用Hook了Socket的相关操作,具体你可以参考[Chapter17](https://github.com/AndroidAdvanceWithGeektime/Chapter17)。
|
||||
</li>
|
||||
|
||||
这种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 tracee’s 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信号为例,具体的实现步骤如下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/cc/0746c1505e06c80345cbeb30b7c6c6cc.png" alt="">
|
||||
|
||||
<li>
|
||||
注册信号接收句柄(signal handler),不同的体系结构可能会选取不同的信号,我们这里用SIGTRAP。
|
||||
</li>
|
||||
<li>
|
||||
在我们需要Hook得部分插入Trap指令。
|
||||
</li>
|
||||
<li>
|
||||
系统调用Trap指令,进入内核模式,调用我们已经在开始注册好的信号接收句柄(signal handler)。
|
||||
</li>
|
||||
<li>
|
||||
执行我们信号接收句柄(signal handler),这里需要注意,所有在信号接收句柄(signal handler)执行的代码需要保证[async-signal-safe](http://man7.org/linux/man-pages/man7/signal-safety.7.html)。这里我们可以简单的只把信号接收句柄当作蹦床,使用logjmp跳出这个需要async-signal-safe(正如我在“崩溃分析”所说的,部分函数在signal回调中使用并不安全)的环境,然后再执行我们Hook的代码。
|
||||
</li>
|
||||
<li>
|
||||
在执行完Hook的函数后,我们需要恢复现场。这里如果我们想继续调用原来的函数A,那直接回写函数A的原始指令并恢复寄存器状态。
|
||||
</li>
|
||||
|
||||
**Trap Hook实践**
|
||||
|
||||
Trap Hook兼容性非常好,它也可以在生产环境中大规模使用。但是它最大的问题是效率比较低,不适合Hook非常频繁调用的函数。
|
||||
|
||||
对于Trap Hook的实践方案,在“[卡顿优化(下)](https://time.geekbang.org/column/article/72642)”中,我提到过Facebook的[Profilo](https://github.com/facebookincubator/profilo),它就是通过定期发送SIGPROF信号来实现卡顿监控的。
|
||||
|
||||
**3. Inline Hook**
|
||||
|
||||
跟Trap Hook一样,Inline Hook也是函数内部调用的Hook。它直接将函数开始(Prologue)处的指令更替为跳转指令,使得原函数直接跳转到Hook的目标函数函数,并保留原函数的调用接口以完成后续再调用回来的目的。
|
||||
|
||||
与GOT/PLT Hook相比,Inline Hook可以不受GOT/PLT表的限制,几乎可以Hook任何函数。不过其实现十分复杂,我至今没有见过可以用在生产环境的实现。并且在ARM体系结构下,无法对叶子函数和很短的函数进行Hook。
|
||||
|
||||
在深入“邪恶的”细节前,我们需要先对Inline Hook的大体流程有一个简单的了解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b7/56/b78e7fd925a5299b13058ef54d094d56.jpg" alt="">
|
||||
|
||||
如图所示,Inline Hook的基本思路就是在已有的代码段中插入跳转指令,把代码的执行流程转向我们实现的Hook函数中,然后再进行指令修复,并跳转回原函数继续执行。这段描述看起来是不是十分简单而且清晰?
|
||||
|
||||
对于Trap Hook,我们只需要在目标地址前插入特殊指令,并且在执行结束后把原始指令写回去就可以了。但是对Inline Hook来说,它是直接进行指令级的复写与修复。怎么理解呢?就相当于我们在运行过程中要去做ASM的字节码修改。
|
||||
|
||||
当然Inline Hook远远比ASM操作更加复杂,因为它还涉及不同CPU架构带来的指令集适配问题,我们需要根据不同指令集来分别进行指令复写与跳转。
|
||||
|
||||
下面我先来简单说明一下Android常见的CPU架构和指令集:
|
||||
|
||||
<li>
|
||||
**x86和MIPS架构**。这两个架构已经基本没有多少用户了,我们可以直接忽视。一般来说我们只关心主流的ARM体系架构就可以了。
|
||||
</li>
|
||||
<li>
|
||||
**ARMv5和ARMv7架构**。它的指令集分为4字节对齐的定长的ARM指令集和2字节对齐的变长Thumb/Thumb-2指令集。Thumb-2指令集虽为2字节对齐,但指令集本身有16位也有32位。其中ARMv5使用的是16位的Thumb16,在ARMv7使用的是32位的Thumb32。**不过目前ARMv5也基本没有多少用户了,我们也可以放弃Thumb16指令集的适配**。
|
||||
</li>
|
||||
<li>
|
||||
**ARMv8架构**。64位的ARMv8架构可以兼容运行32位,所以它在ARM32和Thumb32指令集的基础上,增加了ARM64指令集。关于它们具体差异,你可以查看[ARM的官方文档](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801b/IBAIEGDJ.html)。
|
||||
</li>
|
||||
|
||||
ARM64目前我还没有适配,不过Google Play要求所有应用在2019年8月1日之前需要支持64位,所以今年上半年也要折腾一下。但它们的原理基本类似,下面我以最主流的ARMv7架构为例,为你庖丁解牛Inline Hook。
|
||||
|
||||
**ARM32指令集**
|
||||
|
||||
ARMv7中有一种广为流传的`$PC=$PC+8`的说法。这是指ARMv7中的三级流水线(取指、解码、执行),换句话说`$PC`寄存器总是指向正在取指的指令,而不是指向正在执行的指令。取指总会比执行快2个指令,在ARM32指令集下2个指令的长度为8个字节,所以`$PC`寄存器的值总是比当前指令地址要大8。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/f9/bb8ea924c24a27638d575e25ca101cf9.png" alt="">
|
||||
|
||||
是不是感觉有些复杂,其实这是为了引出ARM指令集的常用跳转方法:
|
||||
|
||||
```
|
||||
LDR PC, [PC, #-4] ;0xE51FF004
|
||||
$TRAMPOLIN_ADDR
|
||||
|
||||
```
|
||||
|
||||
在了解了三级流水线以后,就不会对这个PC-4有什么疑惑了。
|
||||
|
||||
按照我们前面描述的Inline Hook的基本步骤,首先插入跳转指令,跳入我们的蹦床(Trampoline),执行我们实现的Hook后函数。这里还有一个“邪恶的”细节,由于指令执行是依赖当前运行环境的,即所有寄存器的值,而我们插入新的指令是有可能更改寄存器的状态的,所以我们要保存当前全部的寄存器状态到栈中,使用BLX指令跳转执行Hook后函数,执行完成后,再从栈中恢复所有的寄存器,最后才能像未Hook一样继续执行原先函数。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/2d/0589d761c3079aad02287b13a7c0612d.jpg" alt="">
|
||||
|
||||
在执行完Hook后的函数后,我们需要跳转回原先的函数继续执行。这里不要忘记我们在一开始覆盖的LDR指令,我们需要先执行被我们复写的指令,然后再使用如下指令,继续执行原先函数。
|
||||
|
||||
```
|
||||
LDR PC, [PC, #-4]
|
||||
HOOKED_ADDR+8
|
||||
|
||||
```
|
||||
|
||||
是不是有一种大功告成的感觉?其实这里还有一个巨大的坑在等着我们,那就是**指令修复**。前面我提到保存并恢复了寄存器原有的状态,已达到可以继续像原有程序一样的继续执行。但仅仅是恢复寄存器就足够么?显然答案是否定的,虽然寄存器被我们完美恢复了,但是2条备份的指令被移动到了新的地址。当执行它们的时候,`$PC`寄存器的值是与原先不同的。这条指令的操作如果涉及`$PC`的值,那么它们将会执行出完全不同的结果。
|
||||
|
||||
到这里我就不对指令修复再深入解析了,感兴趣的同学可以在留言区进行讨论。
|
||||
|
||||
**Inline Hook实践**
|
||||
|
||||
对于Inline Hook,虽然它功能非常强大,而且执行效率也很高,但是业界目前还没有一套完全稳定可靠的开源方案。Inline Hook一般会使用在自动化测试或者线上疑难问题的定位,例如“UI优化”中说到libhwui.so崩溃问题的定位,我们就是利用Inline Hook去收集系统信息。
|
||||
|
||||
业界也有一些不错的参考方案:
|
||||
|
||||
<li>
|
||||
[Cydia Substrate](http://www.cydiasubstrate.com/)。在[Chapter3](https://github.com/AndroidAdvanceWithGeektime/Chapter03)中,我们就使用它来Hook系统的内存分配函数。
|
||||
</li>
|
||||
<li>
|
||||
[adbi](https://github.com/crmulliner/adbi)。支付宝在[GC抑制](https://juejin.im/post/5be1077d518825171140dbfa)中使用的Hook框架,不过已经好几年没有更新了。
|
||||
</li>
|
||||
|
||||
## 各个流派的优缺点比较
|
||||
|
||||
最后我们再来总结一下不同的Hook方式的优缺点:
|
||||
|
||||
1.GOT/PLT Hook是一个比较中庸的方案,有较好的性能,中等的实现难度,但其只能Hook动态库之间的调用的函数,并且无法Hook未导出的私有函数,而且只存在安装与卸载2种状态,一旦安装就会Hook所有函数调用。
|
||||
|
||||
2.Trap Hook最为稳定,但由于需要切换运行模式(R0/R3),且依赖内核的信号机制,导致性能很差。
|
||||
|
||||
3.Inline Hook是一个非常激进的方案,有很好的性能,并且也没有PLT作用域的限制,可以说是一个非常灵活、完美的方案。但其实现难度极高,我至今也没有看到可以部署在生产环境的Inline Hook方案,因为涉及指令修复,需要编译器的各种优化。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/b9/dbaf5281ac153571b5f2b93f75ae2db9.jpg" alt="">
|
||||
|
||||
但是需要注意,无论是哪一种Hook都只能Hook到应用自身的进程,我们无法替换系统或其他应用进程的函数执行。
|
||||
|
||||
## 总结
|
||||
|
||||
总的来说Native Hook是一门非常底层的技术,它会涉及库文件编译、加载、链接等方方面面的知识,而且很多底层知识是与Android甚至移动平台无关的。
|
||||
|
||||
在这一领域,做安全的同学可能会更有发言权,我来讲可能班门弄斧了。不过希望通过这篇文章,让你对看似黑科技的Hook有一个大体的了解,希望可以在自己的平时的工作中使用Hook来完成一些看似不可能的任务,比如修复系统Bug、线上监控Native内存分配等。
|
||||
|
||||
## 课后作业
|
||||
|
||||
今天的信息量是不是有点大?关于Native Hook,你对它有什么看法,还有哪些疑问?欢迎留言跟我和其他同学一起讨论。
|
||||
|
||||
Native Hook技术的确非常复杂,即使我们不懂得它的内部原理,我们也应该学会使用成熟的开源框架去实现一些功能。当然对于想进一步深入研究的同学,推荐你学习下面这些资料。
|
||||
|
||||
<li>
|
||||
[链接程序和库指南](https://docs.oracle.com/cd/E37934_01/pdf/E36754.pdf)
|
||||
</li>
|
||||
<li>
|
||||
[程序员的自我修养:链接、装载与库](https://item.jd.com/10067200.html)
|
||||
</li>
|
||||
<li>
|
||||
[链接器和加载器 Linkers and Loaders](https://item.jd.com/42971729145.html)
|
||||
</li>
|
||||
<li>
|
||||
[Linux二进制分析 Learning Linux Binary Analysis](https://item.jd.com/12240585.html)
|
||||
</li>
|
||||
|
||||
如果你对调试器的研究也非常有兴趣,强烈推荐[Eli Bendersky](https://eli.thegreenplace.net/)写的博客,里面有一系列非常优秀的底层知识文章。其中一些关于debugger的,感兴趣的同学可以去阅读,并亲手实现一个简单的调试器。
|
||||
|
||||
<li>
|
||||
[how-debuggers-work-part-1](https://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1)
|
||||
</li>
|
||||
<li>
|
||||
[how-debuggers-work-part-2-breakpoints](https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints)
|
||||
</li>
|
||||
<li>
|
||||
[how-debuggers-work-part-3-debugging-information](https://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information)
|
||||
</li>
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
237
极客时间专栏/geek/Android开发高手课/模块三 架构演进/36 | 跨平台开发的现状与应用.md
Normal file
237
极客时间专栏/geek/Android开发高手课/模块三 架构演进/36 | 跨平台开发的现状与应用.md
Normal file
@@ -0,0 +1,237 @@
|
||||
<audio id="audio" title="36 | 跨平台开发的现状与应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/ec/75bca44db605bdf6c7125ecab56f67ec.mp3"></audio>
|
||||
|
||||
在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组成,通过浏览器内核执行并渲染成开发者预期的界面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/6a/ae3ca2e7151208a5ce7bb45d7e3d976a.png" alt="">
|
||||
|
||||
浏览器内核主要包括两大块功能,它们分别是:
|
||||
|
||||
<li>
|
||||
**浏览器引擎**。浏览器引擎负责处理HTML和CSS,遵照的是W3C标准。
|
||||
</li>
|
||||
<li>
|
||||
**JavaScript引擎**。JS引擎负责处理JS,遵照的是ECMAScript标准。
|
||||
</li>
|
||||
|
||||
它们两者互相独立但又有非常紧密的结合,而且在不同浏览器内核中的实现也是不太一样的。但随着微软的Edge宣布将内核切换成Chromium,目前这个战场主要就剩下苹果和Google两个玩家,它们的浏览器引擎分别是Webkit和Blink(其实Blink也是fork自Webkit),JS引擎分别是[JavaScriptCore](https://trac.webkit.org/wiki/JavaScriptCore)和[V8](https://v8.dev/)。
|
||||
|
||||
对于浏览器的渲染流程,可能很多Android开发并没有前端同学熟悉。一般来说,HTML、CSS、JS以及页面用到的一些其他资源(图片、视频、字体等)都需要从网络下载。而HTML会被解析成DOM,CSS会被解析成CSSOM,JS会由JS引擎执行,最后整合DOM和CSSOM之后合成为一棵Render Tree。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/98/4224c50dce3deebc1f4560f3cf65ae98.png" alt="">
|
||||
|
||||
当然整个浏览器的渲染流程不是三言两语就可以说清楚的,下面是我找的一些不错的参考资料,感兴趣的同学可以深入学习:
|
||||
|
||||
<li>
|
||||
浏览器渲染:[一颗像素的诞生](https://mp.weixin.qq.com/s/QoFrdmxdRJG5ETQp5Ua3-A);强烈推荐看PPT: [Life of a Pixel](https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.g25ae9c179b_0_75)
|
||||
</li>
|
||||
<li>
|
||||
浏览器引擎:[What is a browser engine?](https://hacks.mozilla.org/2017/05/quantum-up-close-what-is-a-browser-engine/)
|
||||
</li>
|
||||
<li>
|
||||
浏览器系列教程:[How Browsers Work: Behind the scenes of modern web browsers](https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/)
|
||||
</li>
|
||||
<li>
|
||||
Google Web开发者官网:[Rendering on the Web](https://developers.google.cn/web/updates/2019/02/rendering-on-the-web)
|
||||
</li>
|
||||
|
||||
**虽然[Chromium](https://www.chromium.org/Home)是开源的,但是因为它的复杂性,国内对它有深入研究的人非常少,而拥有定制修改能力的人更是少之又少。因此这块需要投入大量的人力物力,国内比较有名的是UC浏览器的U4内核以及腾讯浏览器的X5内核。**
|
||||
|
||||
**性能现状**
|
||||
|
||||
基于WebView的H5跨平台方案,优点确实非常明显。但是性能是它目前最大的问题,主要表现在以下两个方面:
|
||||
|
||||
<li>
|
||||
**启动白屏时间**。WebView是一个非常重量级的控件,无论是WebView的初始化,还是整个渲染流程都非常耗时。这导致界面启动的时候会出现一段白屏时间,体验非常糟糕。
|
||||
</li>
|
||||
<li>
|
||||
**响应流畅度**。由于单线程、历史包袱等原因,页面的渲染和JavaScript的执行效率都不如原生。在一些重交互或者动画复杂的场景,H5的性能还无法满足诉求。
|
||||
</li>
|
||||
|
||||
所以在移动端H5主要应用在一些交互不太复杂的场景,一般来说即使帧率不如原生,但也基本符合要求。从我个人的感受来看,H5当前最大的问题在于启动的白屏时间。
|
||||
|
||||
对于Android界面启动的过程,我们在窗口动画还没结束的时候,大部分时候就已经完成了页面的渲染。启动一个Activity界面,我们一般要求在300毫秒以内。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/14/9fd70d4a528d903355beba20c1c65e14.png" alt="">
|
||||
|
||||
回顾一下浏览器内核渲染的流程,我们其实可以把整个过程拆成三个部分:
|
||||
|
||||
<li>
|
||||
**Native时间**。主要是Activity、WebView创建以及WebView初始化的时间。虽然首次创建WebView的时间会长一些,但总体Native时间是可控的。
|
||||
</li>
|
||||
<li>
|
||||
**网络时间**。这里包括DNS、TCP、SSL的建连时间和下载主文档的时间。当解析主文档的时候,也需要同步去下载主文档依赖的CSS和JS资源,以及必要的数据。
|
||||
</li>
|
||||
<li>
|
||||
**渲染时间**。浏览器内核构建Render Tree、Layout并渲染到屏幕的时间。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/a1/b9685887787e43ed6de77dd6bb302ca1.png" alt="">
|
||||
|
||||
如上图所示,我们会更加关心用户看到完整的页面的时间T2,这里可以用**T2秒开率**作为启动速度的衡量标准。
|
||||
|
||||
**优化方法**
|
||||
|
||||
Native界面的T2秒开率做到90%以上并不困难,相比之下大部分没有经过优化的Web页面的T2秒开率可能都在40%以下,差距还是非常明显的。
|
||||
|
||||
那又应该如何去优化呢?从前端的角度来看,常用的优化方法有:
|
||||
|
||||
<li>
|
||||
**加快请求速度**。整个启动过程中,网络时间是最不可控的。这里的优化方法有很多,例如预解析DNS、减少域名数、减少HTTP请求数、CDN分发、请求复用、懒加载、Gzip压缩、图片格式压缩。
|
||||
</li>
|
||||
<li>
|
||||
**代码优化**。主文档的大小越小越好([要求小于15KB](https://aosabook.org/en/posa/high-performance-networking-in-chrome.html)),这里要求我们对HTML、CSS以及JS进行代码优化。以JS为例,前端的库和框架真的太多了,可能一不小心就引入了各种的依赖框架。对于核心页面,我们要求只能使用原生JS或者非常轻量级的JS框架,例如使用只有几KB的Preact代替庞大的React框架。
|
||||
</li>
|
||||
<li>
|
||||
**SSR**。对于浏览器的渲染流程,我上面描述的是CSR渲染模式,在这种模式下,服务器只返回页面的基本框架。事实上还有一种非常流行的[SSR(Server Side Rendering)](https://developers.google.cn/web/updates/2019/02/rendering-on-the-web)渲染模式,服务器可以一次性生成直接进行渲染的HTML。这样在T2之前,我们可以做到只有一个网络请求,但是带来的代价就是服务器计算资源的增加。一般来说,我们会在服务器前置CDN来解决访问量的问题。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/c0/7de6d6cf32dc307e3b9152bec1b6f7c0.png" alt="">
|
||||
|
||||
通过上面的这些优化,特别是SSR这个“终极大招”,页面的T2秒开率达到70%并不是非常困难的事情。
|
||||
|
||||
前端同学能做的都已经做了,接下来我们还可以做些什么呢?这个时候就需要客户端开发登场了。
|
||||
|
||||
<li>
|
||||
**WebView预创建**。提前创建和初始化WebView,以及实现WebView的复用,这块大约可以节省100~200毫秒。
|
||||
</li>
|
||||
<li>
|
||||
**缓存**。H5是有多级的缓存机制,例如Memory Cache存放在内存中,一般资源响应回来就会放进去,页面关闭就会释放。Client Cache也就是客户端缓存,例如我们最常用的离线包方案,提前将需要网络请求的数据下发到客户端,通过拦截浏览器的资源请求实现加载。[Http Cache](https://developers.google.cn/web/fundamentals/performance/optimizing-content-efficiency/http-caching)是我们比较熟悉的缓存机制,而Net Cache就是指DNS解析结果的缓存,或预连接的缓存等。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/82/761d08be2655efb28f602083a35c7e82.png" alt="">
|
||||
|
||||
从性能上看,Memory Cache > Client Cache >= Http Cache > Net Cache。所谓的缓存,就是在用户真正点击打开页面之前,提前把数据、资源下载到本地内存或者磁盘中,并放到内核相应的缓存中。例如即使我们使用了SSR,也可以在用户点击之前,提前把服务器渲染好的HTML下载好,这样用户真正打开页面的时候,可以做到完全没有网络请求。
|
||||
|
||||
通过预请求的优化,即使比较复杂的页面,T2秒开率也可以达到80%以上。但是既然是预请求就会有命中率的问题,服务器也增加了没有真正命中的请求数。所以在客户端性能和服务器压力之间,我们需要找到一个平衡点。
|
||||
|
||||
那还有没有进一步优化的空间?这个时候需要我们进一步往底层走,需要我们有定制修改甚至优化内核的能力。例如很多接口官方的浏览器内核可能并没有暴露,而腾讯和UC的内核里面都会有很多的特殊接口。
|
||||
|
||||
<li>
|
||||
**托管所有网络请求**。我们不仅可以托管浏览器的Get请求,其他的所有Post请求也能接管,这样我们可以做非常多的定制化优化。
|
||||
</li>
|
||||
<li>
|
||||
**私有接口**。我们可以暴露很多浏览器的一些非公开接口。以预渲染为例,我可以指定在内存直接渲染某个页面,当用户真正打开的时候,只需要直接做刷新就可以了,实现真正的“秒开”。
|
||||
</li>
|
||||
<li>
|
||||
**兼容性和安全**。Android的碎片化导致浏览器内核的兼容性实在令人头疼,而且旧版本内核还存在不少的安全漏洞。在应用自带浏览器内核可以解决这些问题,而且高版本的内核特性也会更加完善,例如支持TLS 1.3、QUIC等。但是带来的代价是安装包增大20MB左右,当然我们也可以采用动态下载的方式。
|
||||
</li>
|
||||
|
||||
定制的自有页面 + 定制的浏览器内核 + 极致的优化,即使是比较复杂的页面T2秒开率也可以达到90%以上,平均T2时间可以做到400毫秒以下。
|
||||
|
||||
**2. React Native和Weex**
|
||||
|
||||
基于WebView的H5跨平台方案,经过近乎疯狂的性能优化,看起来性能真的不错了。但是对于一些交互和动画复杂的场景(例如左右滑屏、手势),性能还是无法满足要求。
|
||||
|
||||
Facebook在2015年开源了[React Native](https://github.com/facebook/react-native),它抛弃了WebView,利用JavaScriptCore来做桥接,将JavaScript调用转为Native调用。也就是说,React Native最终会生成对应的自定义原生控件,走的是系统原生的渲染流程。
|
||||
|
||||
而阿里在2016年也开源了[Weex](https://github.com/apache/incubator-weex),它的思路跟React Native很像,但是上层DSL使用的是Vue。对于Weex和React Native的架构介绍,网上的文章非常多,例如[《大前端的下一站何去何从?》](https://www.infoq.cn/article/9*CZfjFghPVqZJlc7ScM?utm_medium=hao.caibaojian.com&utm_source=hao.caibaojian.com)和[《Weex技术演进》](https://yq.aliyun.com/articles/444881?spm=a2c4e.11163080.searchblog.9.2eba2ec1hzUpLo)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/54/498f043e2c7459e6e41f34ddfddf7b54.png" alt="">
|
||||
|
||||
但是世上哪有十全十美的方案?React Native/Weex方案为了能达到接近原生开发的性能和交互体验,必然要在跨平台和动态性上面做出了牺牲。
|
||||
|
||||
React Native和Weex向上对接了前端生态,向下对接了原生渲染,看起来是非常完美的方案。但是前端和客户端,客户端中的Android和iOS,它们的差异并不那么容易抹平,强行融合就会遇到各种各样的坑。
|
||||
|
||||
“React Native从入门到放弃”是很多开发者的心声,去年Airbnb、Udacity都相继[宣布放弃使用React Native](https://infoq.cn/article/2018/07/Udacity-Abandon-React-Native)。React Native/Weex并没有彻底解决跨平台的问题,而且考虑到对外分享和降级容灾的需要,我们依然需要开发一个H5版本的页面。
|
||||
|
||||
为了解决这个问题,React Native的使用者需要引入一层非常重的中间层,期望在这个中间层中帮助我们去抹平这些差异。例如京东的[JDReact](https://infoq.cn/article/jd-618-ReactNative-jingdong-practise)、携程的[Ctrip React Native](https://s.qunarzz.com/ymfe_conf/ppt/2017autumn_ctrip_liaoliqiang.pdf)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b2/11/b2f6f19bb6ae9a71546645dc52984411.png" alt="">
|
||||
|
||||
既然React Native和Weex在跨平台上面做了牺牲,那它的性能和交互是不是能直接对齐Native开发呢?非常遗憾, 目前它们的性能我觉得主要还有两个瓶颈。
|
||||
|
||||
<li>
|
||||
**JS的执行时间**。React Native和Weex使用的[JavaScriptCore](https://trac.webkit.org/wiki/JavaScriptCore)引擎,虽然它每年都在进步,但是JS是解释性的动态语言,它的执行效率相比AOT编译后的Java,性能依然会在几倍以上的差距。
|
||||
</li>
|
||||
<li>
|
||||
**跨语言的通信成本**。既然要对接前端和原生两个生态,就无法避免JS -> C++ -> Java/Objective-C 之间频繁的通信和转换,所以这里面会涉及各种序列化,对性能的影响比较大。
|
||||
</li>
|
||||
|
||||
虽然相比H5方案在性能方面有了很大的提升,但是React Native和Weex也要面对启动时间慢、帧率不如原生的性能问题。它属于一种比较中庸的方案,当然也会有自己的应用场景。例如一些二级页面(例如淘宝的分会场),它们的业务也比较重要,但是交互不会特别复杂,同时希望保持一定的动态化能力。
|
||||
|
||||
当然,Facebook已经意识到React Native的种种性能问题,目前正在疯狂重构中,希望让React Native更加轻量化、更适应混合开发,接近甚至达到原生的体验。Facebook现在透漏的信息并不多,感兴趣的同学可以参考[《庖丁解牛!深入剖析React Native下一代架构重构》](https://mp.weixin.qq.com/s/dXZTqXOSi3fiOesDJ7gsFQ)。
|
||||
|
||||
**3. 小程序**
|
||||
|
||||
2017年初,张小龙宣布微信小程序诞生。如今小程序已经走过了两年,在这两年间,小程序的生态也在健康的发展。
|
||||
|
||||
每一个应用都有成为超级App的梦想,各个大厂纷纷推出自己的小程序框架:微信、厂商、支付宝、今日头条、百度、淘宝、[Google Play](https://www.infoq.cn/article/XTE9WzSL11iHmW*WBozi),小程序这个战场已然是“七国大乱战”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/76/1cd013191bd6937fec9b63a47bfb5676.png" alt="">
|
||||
|
||||
但是小程序并不属于一种跨平台开发方案,大家更看重的是它的渠道优势,考虑如何通过微信、支付宝这些全民App获得更多的流量和用户。从技术上看,小程序的框架技术也是开放的,我们可以采用H5方案,也可以采用React Native和Weex,甚至是Flutter。
|
||||
|
||||
从实践上看,我们一起来看看已经正式上线的微信小程序、快应用、支付宝小程序以及百度小程序的差异(技术方面大家公开得并不多,可以参考[《支付宝小程序框架》](https://mp.weixin.qq.com/s/VD0K47KdD-E5EYlnNz2Cbw))。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/61/df12348b0ebadaf13219a66cccb97261.jpg" alt="">
|
||||
|
||||
我们可以看到除了独树一帜的快应用,其他小程序的技术方案基本都跟随了微信。但是考虑到H5在一些场景的性能问题,利用浏览器内核提供的同层渲染能力,在WebView之上支持一些原生的控件。如果哪一天微信小程序支持了所有的原生控件,那也就成为了另外一套React Native/Weex方案。
|
||||
|
||||
“神仙打架,百姓遭殃”,如果我们想从所有的小程序厂商上面获得流量,那就要开发七个不同的小程序。不过幸运的是,支付宝小程序和快应用也希望已有的微信小程序能快速迁移到自己平台上,所以它们的DSL设计都参考了微信的语法,可以说微信推出的DSL已然成为了事实标准。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e8/21/e8e3f8ab6c7867a85ded1741c1b53a21.png" alt="">
|
||||
|
||||
如上图所示,我们希望有一套可以整合所有小程序框架的解决方案,一次开发就可以生成不同的小程序。滴滴的[Chameleon](https://mp.weixin.qq.com/s/YtBXUATwI1pnuF6W69KJYA)和京东的[Taro](https://mp.weixin.qq.com/s/H4W1SD-E5_KhuR1CNfrD2g)都致力于解决这个问题,目前它们都已经在GitHub上开源。
|
||||
|
||||
## 跨平台开发的应用
|
||||
|
||||
从移动开发诞生之初,跨平台就已经是大家前赴后继不断追求的目标。我们可以看看nwind在2015年写的一篇文章[《聊聊移动端跨平台开发的各种技术》](http://fex.baidu.com/blog/2015/05/cross-mobile/)。如今四年过去了,大部分观点依然成立,并且从最后Dart的介绍中,我们甚至可以看到现在Flutter的雏形。
|
||||
|
||||
**1. 跨平台开发的场景**
|
||||
|
||||
Android、iOS、PC,不同的平台人们的操作习惯、喜好都不尽相同。对于大公司来说,完全的跨平台开发可能是一个伪命题,不同的平台应用的UI和交互都不太一样。
|
||||
|
||||
那我们对跨平台苦苦追寻了那么多年,希望得什么呢?以我的经验来看,跨平台主要的应用场景有:
|
||||
|
||||
<li>
|
||||
**部分业务**。某个业务或者页面的跨平台共享,有的时候我们还希望可以做到跨应用。例如“全民答题”的时候,可以看到这个功能可以运行在头条系的各个应用中。一个公司共用同一套跨平台方案有非常重大的意义,业务可以在不同的应用中尝试。
|
||||
</li>
|
||||
<li>
|
||||
**核心功能**。C++才是生命力最顽强的跨平台方案,大公司也将越来越多的核心模块往底层迁移,例如网络库、数据上报、加解密、音视频等。
|
||||
</li>
|
||||
|
||||
**2. 跨平台开发对比**
|
||||
|
||||
H5的跨平台方案只要投入不太高的开发成本,就能开发出性能、功能还不错的应用。但是如果想做到极致优化,很容易发现开发者可控的东西实在比较少,性能和功能都依赖浏览器的支持。
|
||||
|
||||
这个时候如果想走得更远,我们不仅需要了解浏览器的内部机制,可能还需要具备定制、修改浏览器内核的能力,这也是阿里、腾讯、头条和百度都要组建内核团队的原因。
|
||||
|
||||
原生开发则相反,刚开始要投入很高的开发成本,但是一旦开始有产出之后,开发者能够有更的发挥空间,而React Native和Weex方案更是希望打造兼顾跨平台、开发成本以及性能的全方位解决方案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/f7/2e5f5b95bccd1cd0a029b3062339a6f7.png" alt="">
|
||||
|
||||
从目前来看,每一种方案都有着自己的使用场景,无论是React Natve还是H5,都无法完全取代Native开发。当然这里也有一个例外,那就是如果我们不再开发应用,全面投向小程序。小程序跟原生开发的竞争,更多的是在渠道层面的竞争。
|
||||
|
||||
## 总结
|
||||
|
||||
现在好像有个观点说“Android开发没人要”,大家都想转去做大前端开发,是不是真的是这样呢?事实上,无论我们使用哪一种跨平台方案,它们最终都要运行在Android平台上。崩溃、内存、卡顿、耗电这些问题依然存在,而且可能会更加复杂。而且从H5极致体验优化的例子来看,很多优化是需要深入研究平台特性和系统底层机制,我们在“高质量开发”中学到的底层和系统相关的知识依然很重要。
|
||||
|
||||
对开发者来说,唯一不变的就是学习能力。掌握了学习能力和钻研的精神,就能够应对这些趋势变化。无论移动开发未来如何变化,哪怕有一天AI真的能够自动写代码,具备应变能力的人也丝毫不会惧怕的。
|
||||
|
||||
## 课后作业
|
||||
|
||||
跨平台开发也是一个很大很大的话题,今天我只能算是抛砖引玉。对于跨平台开发,你有什么看法?在你的应用中,使用了哪种跨平台开发方式?欢迎留言跟我和其他同学一起讨论。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<audio id="audio" title="37 | 移动开发新大陆:工作三年半,移动开发转型手游开发" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/e1/b70550f0b3c6035cf1169cd5abc1d5e1.mp3"></audio>
|
||||
|
||||
>
|
||||
你好,我是张绍文。15年认识庆文的时候,他还在微信读书负责Android端的性能优化工作。某一天,他跟我说想转岗去尝试一下游戏开发,当时我脑海里浮现了两个想法,一个是游戏部门传说中60个月的年终奖,看起来游戏是一个非常有“钱途”的方向;另外一个还是担忧,抛弃掉Android开发多年的积累,转去完全不熟悉的游戏领域,他是否可以胜任。
|
||||
|
||||
|
||||
>
|
||||
<p>两年过去了,庆文以他的个人经历亲身证明了两件事情:<br>
|
||||
第一,游戏开发并没有想象中那么困难。当年他是Android版微信读书的技术核心,如今在新的岗位上依然也是技术核心。“一通则百通”,技术是相通的,最珍贵的是我们的学习能力和钻研精神。<br>
|
||||
第二,客户端平台知识依然非常重要。手游虽然有非常独立的开发体系,但是它还是运行在Android或者iOS系统之上。App开发需要用到的很多技术,游戏开发也需要用到。作为Android开发,我们一层一层往底层走的优化能力,反而是其他大部分游戏开发所不具备的,也是我们的优势所在。</p>
|
||||
|
||||
|
||||
>
|
||||
在我看来,无论移动的未来如何发展,不管是大前端的天下,还是转向手游、IoT、AI、音视频等其他方向,今天我们所熟悉的Android平台知识以及学习这些知识所沉淀下来的能力和方法,都是最宝贵的财富。下面我们一起来看看庆文同学的移动开发转型手游开发的那些事。
|
||||
|
||||
|
||||
你好,我是李庆文,来自腾讯旗下一间手游工作室,应绍文的邀请,在《Android开发高手课》里和你聊聊手游开发那些事。
|
||||
|
||||
我在2017年4月,从App客户端开发转岗成为一名“用命创造快乐”的手游客户端开发。在转为手游开发之前,虽然玩游戏会被游戏精美的画面吸引,但是并不清楚游戏是如何将这些精细的画面呈现出来,觉得游戏开发好像一个神秘的组织一样,不知道游戏开发团队的同学每天在做什么。如果你也对游戏开发有些好奇,不知道是不是也有我之前同样的困惑?现在我以一个亲历者的身份,跟你讲讲转向手游开发的那事儿。
|
||||
|
||||
## 揭开手游开发的面纱
|
||||
|
||||
一般来说,一个手游项目组包括制作人、策划组、美术组、运营组、程序组、音频组。其中游戏核心部分完全由项目组自己完成,部分美术、音频可以交给外包来完成。
|
||||
|
||||
策划组分为玩法策划、数值策划,是整个游戏的灵魂,一个游戏的核心玩法是否好玩、周边系统能否更好地支撑核心玩法,是整个游戏成败的关键。有些团队也会把运营组放到策划组当中,运营同学主要的工作职责是外部合作、策划游戏内的活动、根据活动数据对游戏玩法或者活动进行相应调整,希望可以尽量延长游戏生命周期并提高游戏收入。
|
||||
|
||||
美术组包括原画师、视觉设计、2D/3D视觉特效设计,其中负责视觉特效的同学与负责程序的同学沟通较多,主要是因为程序需要与动画配合,比如某个动画播放到某一个关键帧之后,程序要处理某个特定逻辑。
|
||||
|
||||
App开发的程序组同学关注的更多是手机系统的架构,希望尽量多了解系统能为App提供的接口或者能力。而手游的程序组同学更多是关注游戏引擎的架构和如何在引擎的基础上优化游戏性能。
|
||||
|
||||
下面我以Cocos引擎为例,带你看看手游的开发流程以及手游是如何运行的。
|
||||
|
||||
**1. 游戏架构**
|
||||
|
||||
以我的游戏项目为例,游戏整体架构如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/91/01389bcd58dcb27a5b99983d69b88991.png" alt="">
|
||||
|
||||
其中:
|
||||
|
||||
<li>
|
||||
资源更新模块让游戏能够不断发布Bugfix和Features。
|
||||
</li>
|
||||
<li>
|
||||
配置系统提供给策划和运营同学,主要是编辑XLS文件用于配置游戏内的关卡、活动等。策划同学的配置会在游戏编译过程中自动转表,根据配置文件生成Lua文件。
|
||||
</li>
|
||||
<li>
|
||||
Services管理着游戏内的全部数据。
|
||||
</li>
|
||||
<li>
|
||||
网络模块负责与后台建立Socket并通信。
|
||||
</li>
|
||||
<li>
|
||||
游戏内使用MVC模型控制各个游戏内模块的跳转。
|
||||
</li>
|
||||
<li>
|
||||
在单个功能模块中,逻辑与动画分离,逻辑层产生动画事件,动画层消费动画事件并让游戏内的精灵根据事件执行特定动作。
|
||||
</li>
|
||||
|
||||
**2. 游戏场景设计**
|
||||
|
||||
Cocos采用节点树形结构来管理游戏对象,一个游戏可以划分为不同的场景(Scene),一个场景又可以分为不同的层(Layer),一个层又可以拥有任意个精灵(Sprite)。游戏里关卡、周边系统的切换也就是一个一个场景的切换,就像在电影中变换舞台和场地一样,由导演(Director)负责不同游戏场景之间的切换。一个Cocos游戏的基本结构如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/0d/9ca7c16d109401bf378443f1a484620d.jpg" alt="">
|
||||
|
||||
在同一个场景中的多个精灵可以通过执行动作(Action)来实现缩放、移动、旋转,从而达到让游戏“动起来”的目的。
|
||||
|
||||
Cocos引擎中,坐标系X轴向右、Y轴向上,Z轴(Z-Order)垂直于手机屏幕指向屏幕外,由于是2D游戏,所以Z轴只用作控制游戏元素的前后顺序。同一个场景中两个重叠或者部分重叠的 Sprite,Z-Order大的精灵会遮挡住Z-Order小的精灵,相同Z-Order的精灵,后添加到场景中的会遮挡住先添加的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/ba/a901e8f4c070edae3da1097d55bb7bba.png" alt="">
|
||||
|
||||
有了树形结构和Z-Order,就能利用Cocos引擎构建出任意2D游戏场景了。
|
||||
|
||||
**3. 手游的运行流程**
|
||||
|
||||
游戏的主Activity在`onCreate`时候创建一个GLSurfaceView,它继承自View,是引擎用于绘制游戏内容的。同时GLSurfaceView也接受玩家的点击事件,用于引擎与玩家的交互。
|
||||
|
||||
GLSurfaceView创建之后,引擎开始执行`main.lua`脚本,导演调用`runWithScene`接口,游戏就进入到第一个“场景”中了。GLSurfaceView中维护了一个`while`循环,循环中发生的事件(比如SurfaceView创建、宽高发生变化等)通过`Renderer`接口传递给外部。`Renderer`其中一个接口就是`onDrawFrame`,Cocos会在`onDrawFrame`接口实现中根据游戏设置的帧率,每间隔一段事件通知一次导演执行一帧游戏循环。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/db/039b709bbd29787d94ba661f38c0cadb.png" alt="">
|
||||
|
||||
借用《我所理解的Cocos2d-x》书中的单帧处理流程图片,你可以看到在每帧中:
|
||||
|
||||
<li>
|
||||
先响应用户输入,因为用户输入可能影响到当前帧接下来的游戏逻辑。
|
||||
</li>
|
||||
<li>
|
||||
根据每个精灵设置的动画,计算精灵的位置、缩放等属性。
|
||||
</li>
|
||||
<li>
|
||||
执行游戏逻辑,比如更新玩家得分、修改当前游戏状态等。在这里开发者依然可以更改精灵的各种属性。
|
||||
</li>
|
||||
<li>
|
||||
遍历UI树,根据之前对精灵属性的设置,生成绘制命令,交给OpenGL绘制,最后把渲染的内容显示到屏幕上。
|
||||
</li>
|
||||
|
||||
## 游戏与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开发一样,也需要关注游戏的各方面性能指标,比如内存、帧率等。
|
||||
|
||||
**内存**
|
||||
|
||||
<li>
|
||||
纹理压缩,减少纹理占用的内存。我前面提到的安装优化包大小中压缩图片,也有助于减少内存占用。在美术同学能接受的前提下,降低图片深度也是快速优化内存的一个方法。
|
||||
</li>
|
||||
<li>
|
||||
使用精灵池,尽量复用已经创建的精灵,减少屏幕中精灵创建的个数,及时回收不用的内存。
|
||||
</li>
|
||||
<li>
|
||||
切换场景时尽快释放上一个场景的无用纹理。
|
||||
</li>
|
||||
|
||||
**帧率**
|
||||
|
||||
<li>
|
||||
减少Draw Call。游戏引擎每次准备数据并发送给GPU去渲染的过程,称为一次Draw Call。Cocos把使用相同贴图、blendFunc、Shader的精灵合并到同一个渲染命令中提交给GPU。我们常说的合图就是把很多小图片合并到一张大图片中,这样当屏幕中有多个连续渲染的精灵用到同一个大图,CPU就会(有可能)把它们合并之后统一提交给GPU。另外Cocos合并渲染命令是根据精灵在场景中的顺序来做的,最终的合并效果不一定是最优的。我们可以实现自己更优化的合并逻辑。
|
||||
</li>
|
||||
<li>
|
||||
减少屏幕内需要渲染的精灵个数。如果屏幕中有“成组”的元素需要绘制,比如排行榜中每个玩家的Item,在Cocos中可以使用CCRenderTexture,把每个“成组”的元素绘制到CCRenderTexture绑定的纹理,然后用CCRenderTexture替代原有的Item。这与Android开发中自己实现View的onDraw函数类似,并且这也是减少Draw Call的一种方式。
|
||||
</li>
|
||||
<li>
|
||||
分帧。真的遇到CPU密集型的任务时,例如同屏幕确实需要大量精灵,可以把它们拆分在不同帧里,尽量保证游戏内每一帧在规定时间内完成,否则会出现卡帧现象。
|
||||
</li>
|
||||
|
||||
## 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年真是火了一把,鉴于小游戏入门简单、上手快,想要转方向的同学可以先用小游戏来练手。“麻雀虽小,五脏俱全”,看明白一个小游戏的执行流程之后,相信你对游戏开发也就会有一个感性的认识了。
|
||||
|
||||
最后祝各位开发者游戏愉快!
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
209
极客时间专栏/geek/Android开发高手课/模块三 架构演进/38 | 移动开发新大陆:Android音视频开发.md
Normal file
209
极客时间专栏/geek/Android开发高手课/模块三 架构演进/38 | 移动开发新大陆:Android音视频开发.md
Normal file
@@ -0,0 +1,209 @@
|
||||
<audio id="audio" title="38 | 移动开发新大陆:Android音视频开发" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ee/92/eed11e99ea5938b47f2a3c5943a9af92.mp3"></audio>
|
||||
|
||||
>
|
||||
你好,我是张绍文。俊杰目前负责微信的音视频开发工作,无论是我们平常经常使用到的小视频,还是最新推出的即刻视频,都是出自他手。俊杰在音视频方面有非常丰富的经验,今天有幸请到他来给我们分享他个人对Android音视频开发的心得和体会。
|
||||
|
||||
|
||||
>
|
||||
在日常生活中,视频类应用占据了我们越来越多的时间,各大公司也纷纷杀入这个战场,不管是抖音、快手等短视频类型,虎牙、斗鱼等直播类型,腾讯视频、爱奇艺、优酷等长视频类型,还是Vue、美拍等视频编辑美颜类型,总有一款适合你。
|
||||
|
||||
|
||||
>
|
||||
未来随着5G普及以及网络资费的下降,音视频的前景是非常广阔的。但是另一方面,无论是音视频的编解码和播放器、视频编辑和美颜的各种算法,还是视频与人工智能的结合([AI剪片](https://mp.weixin.qq.com/s/ZtsqF7bBODFymEaYFG6KAg)、视频修复、超清化等),它们都涉及了方方面面的底层知识,学习曲线比较陡峭,门槛相对比较高,所以也造成了目前各大公司音视频相关人才的紧缺。如果你对音视频开发感兴趣,我也非常建议你去往这个方向尝试,我个人是非常看好音视频开发这个领域的。
|
||||
|
||||
|
||||
>
|
||||
当然音视频开发的经验是靠着一次又一次的“填坑”成长起来的,下面我们一起来看看俊杰同学关于音视频的认识和思考。
|
||||
|
||||
|
||||
大家好,我是周俊杰,现在在微信从事Android多媒体开发。应绍文的邀请,分享一些我关于Android音视频开发的心得和体会。
|
||||
|
||||
不管作为开发者还是用户,现在我们每天都会接触到各种各样的短视频、直播类的App,与之相关的音视频方向的开发也变得越来越重要。但是对于大多数Android开发者来说,从事Android音视频相关的开发可能目前还算是个小众领域,虽然可能目前深入这个领域的开发者还不是太多,但这个方向涉及的知识点可一点都不少。
|
||||
|
||||
## 音视频的基础知识
|
||||
|
||||
**1. 音视频相关的概念**
|
||||
|
||||
说到音视频,先从我们熟悉也陌生的视频格式说起。
|
||||
|
||||
对于我们来说,最常见的视频格式就是[MP4](https://zh.wikipedia.org/zh-cn/MP4)格式,这是一个通用的容器格式。所谓容器格式,就意味内部要有对应的数据流用来承载内容。而且既然是一个视频,那必然有音轨和视轨,而音轨、视轨本身也有对应的格式。常见的音轨、视轨格式包括:
|
||||
|
||||
<li>
|
||||
视轨:[H.265(HEVC)](https://zh.wikipedia.org/wiki/%E9%AB%98%E6%95%88%E7%8E%87%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81)、[H.264](https://zh.wikipedia.org/wiki/H.264/MPEG-4_AVC)。其中,目前大部分Android手机都支持H.264格式的直接硬件编码和解码;对于H.265来说,Android 5.0以上的机器就支持直接硬件解码了,但是对于硬件编码,目前只有一部分高端芯片可以支持,例如高通的8xx系列、华为的98x系列。对于视轨编码来说,分辨率越大性能消耗也就越大,编码所需的时间就越长。
|
||||
</li>
|
||||
<li>
|
||||
音轨:[AAC](https://zh.wikipedia.org/wiki/%E9%80%B2%E9%9A%8E%E9%9F%B3%E8%A8%8A%E7%B7%A8%E7%A2%BC)。这是一种历史悠久音频编码格式,Android手机基本可以直接硬件编解码,几乎很少遇到兼容性问题。可以说作为视频的音轨格式,AAC已经非常成熟了。
|
||||
</li>
|
||||
|
||||
对于编码本身,上面提到的这些格式都是[有损编码](https://zh.wikipedia.org/zh-hans/%E6%9C%89%E6%8D%9F%E6%95%B0%E6%8D%AE%E5%8E%8B%E7%BC%A9),因此压缩编码本身还需要一个衡量压缩之后,数据量多少的指标,这个标准就是[码率](https://zh.wikipedia.org/wiki/%E6%AF%94%E7%89%B9%E7%8E%87#%E5%A4%9A%E5%AA%92%E4%BD%93%E7%9A%84%E6%AF%94%E7%89%B9%E7%8E%87)。同一个压缩格式下,码率越高质量也就越好。更多Android本身支持的编解码格式,你可以参考[官方文档](https://developer.android.com/guide/topics/media/media-formats)。
|
||||
|
||||
小结一下,要拍摄一个MP4视频,我们需要将视轨 + 音轨分别编码,然后作为MP4的数据流,再合成出一个MP4文件。
|
||||
|
||||
**2. 音视频编码的流程**
|
||||
|
||||
接下来,我们再来看看一个视频是怎么拍摄出来的。首先,既然是拍摄,少不了跟摄像头、麦克风打交道。从流程来说,以H.264/AAC编码为例,录制视频的总体流程是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/73/406204beab2de273ecdb436d4bf66773.png" alt="">
|
||||
|
||||
我们分别从摄像头/录音设备采集数据,将数据送入编码器,分别编码出视轨/音轨之后,再送入合成器(MediaRemuxer或者类似mp4v2、FFmpeg之类的处理库),最终输出MP4文件。接下来,我主要以视轨为例,来介绍下编码的流程。
|
||||
|
||||
首先,直接使用系统的[MediaRecorder](https://developer.android.com/reference/android/media/MediaRecorder)录制整个视频,这是最简单的方法,直接就能输出MP4文件。但是这个接口可定制化很差,比如我们想录制一个正方形的视频,除非摄像头本身支持宽高一致的分辨率,否则只能后期处理或者各种Hack。另外,在实际App中,除非对视频要求不是特别高,一般也不会直接使用MediaRecorder。
|
||||
|
||||
视轨的处理是录制视频中相对比较复杂的部分,输入源头是Camera的数据,最终输出是编码的H.264/H.265数据。下面我来介绍两种处理模型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/82/fb9fe38693f3a86af745b97fc8d22882.png" alt="">
|
||||
|
||||
第一种方法是利用Camera获取摄像头输出的原始数据接口(例如onPreviewFrame),经过预处理,例如缩放、裁剪之后,送入编码器,输出H.264/H.265。
|
||||
|
||||
摄像头输出的原始数据格式为NV21,这是[YUV](https://zh.wikipedia.org/wiki/YUV)颜色格式的一种。区别于RGB颜色,YUV数据格式占用空间更少,在视频编码领域使用十分广泛。
|
||||
|
||||
一般来说,因为摄像头直接输出的NV21格式大小跟最终视频不一定匹配,而且编码器往往也要求输入另外一种YUV格式(一般来说是YUV420P),因此在获取到NV21颜色格式之后,还需要进行各种缩放、裁剪之类的操作,一般会使用[FFmpeg](https://www.ffmpeg.org/)、[libyuv](https://chromium.googlesource.com/libyuv/libyuv/)这样的库处理YUV数据。
|
||||
|
||||
最后会将数据送入到编码器。在视频编码器的选择上,我们可以直接选择系统的MediaCodec,利用手机本身的硬件编码能力。但如果对最终输出的视频大小要求比较严格的话,使用的码率会偏低,这种情况下大部分手机的硬件编码器输出的画质可能会比较差。另外一种常见的选择是利用[x264](https://www.videolan.org/developers/x264.html)来进行编码,画质表现相对较好,但是比起硬件编码器,速度会慢很多,因此在实际使用时最好根据场景进行选择。
|
||||
|
||||
除了直接处理摄像头原始数据以外,还有一种常见的处理模型,利用Surface作为编码器的输入源。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/06/5003363cbda442463e50b7c6a245b306.png" alt="">
|
||||
|
||||
对于Android摄像头的预览,需要设置一张Surface/SurfaceTexture来作为摄像头预览数据的输出,而MediaCodec在API 18+之后,可以通过[createInputSurface](https://developer.android.com/reference/android/media/MediaCodec#createInputSurface)来创建一张Surface作为编码器的输入。这里所说的另外一种方式就是,将摄像头预览Surface的内容,输出到MediaCodec的InputSurface上。
|
||||
|
||||
而在编码器的选择上,虽然InputSurface是通过MediaCodec来创建的,乍看之下似乎只能通过MediaCodec来进行编码,无法使用x264来编码,但利用PreviewSurface,我们可以创建一个OpenGL的上下文,这样所有绘制的内容,都可以通过glReadPixel来获取,然后再讲读取数据转换成YUV再输入到x264即可(另外,如果是在GLES 3.0的环境,我们还可以利用[PBO](http://www.songho.ca/opengl/gl_pbo.html)来加速glReadPixles的速度)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/cb/98ae4dbac4cac80639d7dfe4682414cb.png" alt="">
|
||||
|
||||
由于这里我们创建了一个OpenGL的上下文,对于目前的视频类App来说,还有各种各样的滤镜和美颜效果,实际上都可以基于OpenGL来实现。
|
||||
|
||||
而至于这种方式录制视频具体实现代码,你可以参考下grafika中[示例](https://github.com/google/grafika/blob/master/app/src/main/java/com/android/grafika/CameraCaptureActivity.java)。
|
||||
|
||||
## 视频处理
|
||||
|
||||
**1. 视频编辑**
|
||||
|
||||
在当下视频类App中,你可以见到各种视频裁剪、视频编辑的功能,例如:
|
||||
|
||||
<li>
|
||||
裁剪视频的一部分。
|
||||
</li>
|
||||
<li>
|
||||
多个视频进行拼接。
|
||||
</li>
|
||||
|
||||
对于视频裁剪、拼接来说,Android直接提供了[MediaExtractor](https://developer.android.com/reference/android/media/MediaExtractor)的接口,结合seek以及对应读取帧数据readSampleData的接口,我们可以直接获取对应时间戳的帧的内容,这样读取出来的是已经编码好的数据,因此无需重新编码,直接可以输入合成器再次合成为MP4。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/61/ad/61b18d372ba93905b547d60da313a1ad.png" alt="">
|
||||
|
||||
我们只需要seek到需要裁剪原视频的时间戳,然后一直读取sampleData,送入MediaMuxer即可,这是视频裁剪最简单的实现方式。
|
||||
|
||||
但在实践时会发现,[seekTo](https://developer.android.com/reference/android/media/MediaExtractor#seekTo)并不会对所有时间戳都生效。比如说,一个**4min**左右的视频,我们想要seek到大概**2min**左右的位置,然后从这个位置读取数据,但实际调用seekTo到2min这个位置之后,再从MediaExtractor读取数据,你会发现实际获取的数据上可能是从2min这里前面一点或者后面一点位置的内容。这是因为MediaExtractor这个接口只能seek到视频[关键帧](https://zh.wikipedia.org/wiki/%E9%97%9C%E9%8D%B5%E6%A0%BC#%E8%A6%96%E8%A8%8A%E7%B7%A8%E8%BC%AF%E7%9A%84%E9%97%9C%E9%8D%B5%E6%A0%BC)的位置,而我们想要的位置并不一定有关键帧。这个问题还是要回到视频编码,在视频编码时两个关键帧之间是有一定间隔距离的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/7b/9a59d62bc13f09d9ccf971ce6d8d5b7b.jpg" alt="">
|
||||
|
||||
如上图所示,关键帧被成为**I帧**,可以被认为是一帧没有被压缩的画面,解码的时候无需要依赖其他视频帧。但是在两个关键帧之间,还存在这**B帧**、**P帧**这样的压缩帧,需要依赖其他帧才能完整解码出一个画面。至于两个关键帧之间的间隔,被称为一个[GOP](https://zh.wikipedia.org/wiki/%E5%9C%96%E5%83%8F%E7%BE%A4%E7%B5%84) ,在GOP内的帧,MediaExtractor是无法直接seek到的,因为这个类不负责解码,只能seek到前后的关键帧。但如果GOP过大,就会导致视频编辑非常不精准了(实际上部分手机的ROM有改动,实现的MediaExtractor也能精确seek)。
|
||||
|
||||
既然如此,那要实现精确裁剪也就只能去依赖解码器了。解码器本身能够解出所有帧的内容,在引入解帧之后,整个裁剪的流程就变成了下面的样子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/89/53/89f6cf7d68677a1a6225905117877853.png" alt="">
|
||||
|
||||
我们需要先seek到需要位置的前一I帧上,然后送入解码器,解码器解除一帧之后,判断当前帧的[PTS](https://en.wikipedia.org/wiki/Presentation_timestamp)是否在需要的时间戳范围内,如果是的话,再将数据送入编码器,重新编码再次得到H.264视轨数据,然后合成MP4文件。
|
||||
|
||||
上面是基础的视频裁剪流程,对于视频拼接,也是类似得到多段H.264数据之后,才一同送入合成器。
|
||||
|
||||
另外,在实际视频编辑中,我们还会添加不少视频特效和滤镜。前面在视频拍摄的场景下,我们利用Surface作为MediaCodec的输入源,并且利用Surface创建了OpenGL的上下文。而MediaCodec作为解码器的时候,也可以在[configure](https://developer.android.com/reference/android/media/MediaCodec#configure)的时候,指定一张Surface作为其解码的输出。大部分视频特效都是可以通过OpenGL来实现的,因此要实现视频特效,一般的流程是下面这样的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0f/5d/0f4a26fd743155b102887758056ba85d.png" alt="">
|
||||
|
||||
我们将解码之后的渲染交给OpenGL,然后输出到编码器的InputSurface上,来实现整套编码流程。
|
||||
|
||||
**2. 视频播放**
|
||||
|
||||
任何视频类App都会涉及视频播放,从录制、剪辑再到播放,构成完整的视频体验。对于要播放一个MP4文件,最简单的方式莫过于直接使用系统的[MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer),只需要简单几行代码,就能直接播放视频。对于本地视频播放来说,这是最简单的实现方式,但实际上我们可能会有更复杂的需求:
|
||||
|
||||
<li>
|
||||
需要播放的视频可能本身并不在本地,很多可能都是网络视频,有边下边播的需求。
|
||||
</li>
|
||||
<li>
|
||||
播放的视频可能是作为视频编辑的一部分,在剪辑时需要实时预览视频特效。
|
||||
</li>
|
||||
|
||||
对于第二种场景,我们可以简单配置播放视频的View为一个GLSurfaceView,有了OpenGL的环境,我们就可以在这上实现各种特效、滤镜的效果了。而对于视频编辑常见的快进、倒放之类的播放配置,MediaPlayer也有直接的接口可以设置。
|
||||
|
||||
更为常见的是第一种场景,例如一个视频流界面,大部分视频都是在线视频,虽然MediaPlayer也能实现在线视频播放,但实际使用下来,会有两个问题:
|
||||
|
||||
<li>
|
||||
通过设置MediaPlayer视频URL方式下载下来的视频,被放到了一个私有的位置,App不容易直接访问,这样会导致我们没法做视频预加载,而且之前已经播放完、缓冲完的视频,也不能重复利用原有缓冲内容。
|
||||
</li>
|
||||
<li>
|
||||
同视频剪辑直接使用MediaExtractor返回的数据问题一样,MediaPlayer同样无法精确seek,只能seek到有关键帧的地方。
|
||||
</li>
|
||||
|
||||
对于第一个问题,我们可以通过视频URL代理下载的方式来解决,通过本地使用Local HTTP Server的方式代理下载到一个指定的地方。现在开源社区已经有很成熟的项目实现,例如[AndroidVideoCache](http://AndroidVideoCache)。
|
||||
|
||||
而对于第二个问题来说,没法精确seek的问题在有些App上是致命的,产品可能无法接受这样的体验。那同视频编辑一样,我们只能直接基于MediaCodec来自行实现播放器,这部分内容就比较复杂了。当然你也可以直接使用Google开源的[ExoPlayer](https://github.com/google/ExoPlayer),简单又快捷,而且也能支持设置在线视频URL。
|
||||
|
||||
看似所有问题都有了解决方案,是不是就万事大吉了呢?
|
||||
|
||||
常见的网络边下边播视频的格式都是MP4,但有些视频直接上传到服务器上的时候,我们会发现无论是使用MediaPlayer还是ExoPlayer,似乎都只能等待到整个视频都下载完才能开始播放,没有达到边下边播的体验。这个问题的原因实际上是因为MP4的格式导致的,具体来看,是跟MP4[格式](http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf)中的moov有关。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/42/6439de8111b1ad7173291c0723071142.png" alt="">
|
||||
|
||||
MP4格式中有一个叫作moov的地方存储这当前MP4文件的元信息,包括当前MP4文件的音轨视轨格式、视频长度、播放速率、视轨关键帧位置偏移量等重要信息,MP4文件在线播放的时候,需要moov中的信息才能解码音轨视轨。
|
||||
|
||||
而上述问题发生的原因在于,当moov在MP4文件尾部的时候,播放器没有足够的信息来进行解码,因此视频变得需要直接下载完之后才能解码播放。因此,要实现MP4文件的边下边播,则需要将moov放到文件头部。目前来说,业界已经有非常成熟的工具,[FFmpeg](https://ffmpeg.org/ffmpeg-formats.html#Options-8)跟[mp4v2](https://code.google.com/archive/p/mp4v2/)都可以将一个MP4文件的moov提前放到文件头部。例如使用FFmpeg,则是如下命令:
|
||||
|
||||
```
|
||||
ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4
|
||||
|
||||
```
|
||||
|
||||
使用`-movflags faststart`,我们就可以把视频文件中的moov提前了。
|
||||
|
||||
另外,如果想要检测一个MP4的moov是否在前面,可以使用类似[AtomicParsley](http://atomicparsley.sourceforge.net/)的工具来检测。
|
||||
|
||||
在视频播放的实践中,除了MP4格式来作为边下边播的格式以外,还有更多的场景需要使用其他格式,例如m3u8、FLV之类,业界在客户端中常见的实现包括[ijkplayer](https://github.com/bilibili/ijkplayer)、[ExoPlayer](https://github.com/google/ExoPlayer),有兴趣的同学可以参考下它们的实现。
|
||||
|
||||
## 音视频开发的学习之路
|
||||
|
||||
音视频相关开发涉及面很广,今天我也只是简单介绍一下其中基本的架构,如果想继续深入这个领域发展,从我个人学习的经历来看,想要成为一名合格的开发者,除了基础的Android开发知识以外,还要深入学习,我认为还需要掌握下面的技术栈。
|
||||
|
||||
**语言**
|
||||
|
||||
<li>
|
||||
C/C++:音视频开发经常需要跟底层代码打交道,掌握C/C++是必须的技能。这方面资料很多,相信我们都能找到。
|
||||
</li>
|
||||
<li>
|
||||
ARM NEON汇编:这是一项进阶技能,在视频编解码、各种帧处理低下时很多都是利用NEON汇编加速,例如FFmpeg/libyuv底层都大量利用了NEON汇编来加速处理过程。虽说它不是必备技能,但有兴趣也可以多多了解,具体资料可以参考ARM社区的[教程](https://community.arm.com/processors/b/blog/posts/coding-for-neon---part-1-load-and-stores)。
|
||||
</li>
|
||||
|
||||
**框架**
|
||||
|
||||
<li>
|
||||
[FFmpeg](https://ffmpeg.org/):可以说是业界最出名的音视频处理框架了,几乎囊括音视频开发的所有流程,可以说是必备技能。
|
||||
</li>
|
||||
<li>
|
||||
[libyuv](https://chromium.googlesource.com/libyuv/libyuv/):Google开源的YUV帧处理库,因为摄像头输出、编解码输入输出也是基于YUV格式,所以也经常需要这个库来操作数据(FFmpeg也有提供了这个库里面所有的功能,在[libswscale](https://www.ffmpeg.org/doxygen/2.7/swscale_8h.html)都可以找到类似的实现。不过这个库性能更好,也是基于NEON汇编加速)。
|
||||
</li>
|
||||
<li>
|
||||
[libx264](https://www.videolan.org/developers/x264.html)/[libx265](http://x265.org/):目前业界最为广泛使用的H.264/H.265软编解码库。移动平台上虽然可以使用硬编码,但很多时候出于兼容性或画质的考虑,因为不少低端的Android机器,在低码率的场景下还是软编码的画质会更好,最终可能还是得考虑使用软编解码。
|
||||
</li>
|
||||
<li>
|
||||
[OpenGL ES](https://www.khronos.org/opengles/):当今,大部分视频特效、美颜算法的处理,最终渲染都是基于GLES来实现的,因此想要深入音视频的开发,GLES是必备的知识。另外,除了GLES以外,[Vulkan](https://www.khronos.org/vulkan/)也是近几年开始发展起来的一个更高性能的图形API,但目前来看,使用还不是特别广泛。
|
||||
</li>
|
||||
<li>
|
||||
[ExoPlayer](https://github.com/google/ExoPlayer)/[ijkplayer](https://github.com/bilibili/ijkplayer):一个完整的视频类App肯定会涉及视频播放的体验,这两个库可以说是当下业界最为常用的视频播放器了,支持众多格式、协议,如果你想要深入学习视频播放处理,它们几乎也算是必备技能。
|
||||
</li>
|
||||
|
||||
从实际需求出发,基于上述技术栈,我们可以从下面两条路径来深入学习。
|
||||
|
||||
**1. 视频相关特效开发**
|
||||
|
||||
直播、小视频相关App目前越来越多,几乎每个App相关的特效,往往都是利用OpenGL本身来实现。对于一些简单的特效,可以使用类似[Color Look Up Table](https://en.wikipedia.org/wiki/3D_lookup_table)的技术,通过修改素材配合Shader来查找颜色替换就能实现。如果要继续学习更加复杂的滤镜,推荐你可以去[shadertoy](https://www.shadertoy.com/)学习参考,上面有非常多Shader的例子。
|
||||
|
||||
而美颜、美型相关的效果,特别是美型,需要利用人脸识别获取到关键点,对人脸纹理进行三角划分,然后再通过Shader中放大、偏移对应关键点纹理坐标来实现。如果想要深入视频特效类的开发,我推荐可以多学习OpenGL相关的知识,这里会涉及很多优化点。
|
||||
|
||||
**2. 视频编码压缩算法**
|
||||
|
||||
H.264/H.265都是非常成熟的视频编码标准,如何利用这些视频编码标准,在保证视频质量的前提下,将视频大小最小化,从而节省带宽,这就需要对视频编码标准本身要有非常深刻的理解。这可能是一个门槛相对较高的方向,我也尚处学习阶段,有兴趣的同学可以阅读相关编码标准的文档。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
136
极客时间专栏/geek/Android开发高手课/模块三 架构演进/39 | 移动开发新大陆: 边缘智能计算的趋势.md
Normal file
136
极客时间专栏/geek/Android开发高手课/模块三 架构演进/39 | 移动开发新大陆: 边缘智能计算的趋势.md
Normal file
@@ -0,0 +1,136 @@
|
||||
<audio id="audio" title="39 | 移动开发新大陆: 边缘智能计算的趋势" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/42/3464f2a7af5033f49be15faf6b237842.mp3"></audio>
|
||||
|
||||
>
|
||||
你好,我是张绍文。今天文章的作者黄振是算法领域的专家,过去曾经和他合作过移动端的AI项目,无论是他的算法水平,还是工程上的能力,都让我深感佩服。今天我非常幸运地请到他来给我们分享关于移动边缘智能计算的认识。
|
||||
|
||||
|
||||
>
|
||||
如果说过去几年移动开发是那头在风口上的猪,现在这个风口明显已经转移到AI,最明显的是国内应届生都纷纷涌向了AI方向。那作为移动开发,我们如何在AI浪潮之下占据自己的一席之地?
|
||||
|
||||
|
||||
>
|
||||
以我个人看法来看,即使目前AI在移动端的落地场景有限,但是AI这个“坑”我们是一定要跳的。我们可以尝试使用TensorFlow这些框架去做一些简单的Demo,逐步加深对深度学习的理解。
|
||||
|
||||
|
||||
>
|
||||
Google的[TensorFlow Lite](https://www.tensorflow.org/lite)、Facebook的[Cafe2](https://caffe2.ai/)、腾讯的[NCNN](https://github.com/Tencent/ncnn)和[FatherCNN](https://github.com/Tencent/FeatherCNN)、小米的[MACE](https://github.com/XiaoMi/mace)、百度的[Paddle-Mobile](https://github.com/PaddlePaddle/paddle-mobile),各个公司都开源了自己的移动深度学习框架。移动端未来必然是深度学习一个非常重要的战场,移动开发们需要发挥自己的平台特长。我们对于这些移动深度学习框架的优化应该更有发言权,要做到既有算法思维,也有更加强大的工程能力。就像之前黄振合作的项目中,整个框架的性能优化也是由我们来主导,使用了大量ARM NEON指令和汇编进行优化。
|
||||
|
||||
|
||||
大家好,我是黄振,目前在一家大型互联网公司从事算法工作。我一直在关注边缘智能计算,正好过去也开展过移动端机器学习的项目,期间也接触了不少Android开发的同学。在一起合作期间,我也在思考AI和Android开发可以相结合的点,也看到团队中不少Android开发同学展现出的机器学习技术的开发能力。正好应绍文邀请,在Android开发专栏和你分享一下我对移动端机器学习的认识,希望对你有所帮助。
|
||||
|
||||
目前,技术的发展有两个趋势。一个趋势是,随着5G网络的发展,物联网使“万物互联”成为可能。物联网时代,也是云计算和边缘计算并存的世界,且边缘计算将扮演重要的角色。边缘计算是与云计算相反的计算范式,指的是在网络边缘节点(终端设备)进行数据处理和分析。边缘计算之所以重要,原因在于:首先,在物联网时代,将有数十亿的终端设备持续采集数据,带来的计算量将是云计算所不能承受的;其次,终端设备的计算能力不断提高,并不是所有的计算都需要在云端完成;再有,将数据传回云端计算再将结果返回终端的模式,不可避免存在时延,不仅影响用户体验,有些场景也是不可接受的;最后,随着用户对数据安全和隐私的重视,也会要求在终端进行数据加工和计算。
|
||||
|
||||
另一个趋势是,人工智能技术的迅速发展。最近几年,以深度学习为代表的人工智能技术取得了突破性的进展,不仅在社会上引起了人们的兴趣和关注,同时也正在重塑商业的格局。人工智能技术的重大价值在于,借助智能算法和技术,将之前必须人工才能完成的工作进行机器化,用机器的规模化解放人工,突破人力生产要素的瓶颈,从而大幅提高生产效率。在商业社会,“规模化”技术将释放巨大的能量、创造巨大的价值,甚至重塑商业的竞争,拓展整个商业的外部边界。
|
||||
|
||||
边缘计算既然有计算能力,在很多应用场景中就会产生智能计算的需求,两者结合在一起,就形成了边缘智能计算。
|
||||
|
||||
## 移动端机器学习的开发技术
|
||||
|
||||
目前,移动端机器学习的应用技术主要集中在图像处理、自然语言处理和语音处理。在具体的应用上,包括但不限于视频图像的物体检测、语言翻译、语音助手、美颜等,在自动驾驶、教育、医疗、智能家居和物联网等方面有巨大的应用需求。
|
||||
|
||||
**1. 计算框架**
|
||||
|
||||
因为深度学习算法具有很多特定的算法算子,为了提高移动端开发和模型部署的效率,各大厂都开发了移动端深度学习的计算框架,例如谷歌的[TensorFlow Lite](https://www.tensorflow.org/lite)、Facebook的[Caffe2](https://caffe2.ai/)等,Android开发的同学还是挺有必要去了解这些计算框架的。为了培养兴趣这方面的兴趣并获得感性的认识,你可以挑选一个比较成熟的大厂框架,例如TensorFlow Lite,在手机开发一个物体检测的Demo练练手。
|
||||
|
||||
在实际项目中,TensorFlow Lite和Caffe2等通常运行比较慢。在网上可以很容易找到各个计算框架的对比图。如果要真正入门的话,在这里我推荐大家使用[NCNN](https://github.com/Tencent/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 SIMD(Single Instruction, Multiple Data,单指令、多数据)扩展结构。ARM NEON指令之所有能达到性能优化的目的,关键就在于单指令多数据。如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/ae/503ce6dbbd5ace49e5bd94ab5459b5ae.png" alt="">
|
||||
|
||||
两个操作数各有128Bit,各包含4个32Bit的同类型的寄存器,通过如下所示的一条指令,就可以实现4个32Bit数据的并行计算,从而达到性能优化的效果。
|
||||
|
||||
```
|
||||
VADDQ.S32 Q0,Q1,Q2
|
||||
|
||||
```
|
||||
|
||||
我们曾经用NDK的方式实现了深度学习算法中的PixelShuffle操作,后来采用ARM NEON+汇编优化的方式,计算的效率提升40倍,效果还是很显著的。
|
||||
|
||||
如果还想进一步提高计算的性能,可以采用Int 8量化的方法。在深度学习里面,大多数运算都是基于Float 32类型进行的,Float 32是32Bit,128Bit的寄存器一次可以存储4个Float 32类型数据;相比之下,Int 8类型的数据可以存储16个。结合前面提到的单指令多数据,如果采用Int 8类型的数据,单条指令可以同时执行16个Int 8数据的运算,从而大大提高了并行性。
|
||||
|
||||
但是,将Float 32类型的数值量化到Int 8表达,不可避免会影响到数据的精度,而且量化的过程也是需要时间开销的,这些都是需要注意的地方。关于量化的方法,感兴趣的同学可以阅读这篇[论文](https://arxiv.org/abs/1806.08342)。
|
||||
|
||||
如果设备具有GPU,还可以应用OpenCL进行GPU加速,例如小米开源的移动端机器学习框架[MACE](https://github.com/XiaoMi/mace)。
|
||||
|
||||
## 移动端机器学习的算法技术
|
||||
|
||||
对于刚开始学习算法的同学,我一直主张不要把算法想象得太复杂,也不要想得太数学化,否则容易让人望而生畏。数学是思维逻辑的表达,我们希望用数学帮助我们理解算法。我们要能够达到对算法的直观理解,从直观上知道和理解为什么这个算法会有这样的效果,只有这样才算是真正掌握了算法。相反,对于一个算法,如果你只记住了数学推导,没形成直观的理解,是不能够灵活应用的。
|
||||
|
||||
**1. 算法设计**
|
||||
|
||||
深度学习的图像处理具有很广的应用,而且比较直观和有趣,建议你可以从深度学习的图像处理入手。深度学习的图像处理,最基本的知识是卷积神经网络,所以你可以先学习卷积神经网络。网上有很多关于卷积神经网络的介绍,这里就不赘述了。
|
||||
|
||||
理解卷积神经网络关键是理解卷积神经网络的学习机制,理解为什么能够学习。想要理解这一点,首先需要明确两个关键点“前向传播”和“反向传播”。整个神经网络在结构和激活函数确定之后,所谓“训练”或者“学习”的过程,其实就是在不断地调整神经网络的每个权重,让整个网络的计算结果趋近于期望值(目标值)。前向传播是从输入端开始计算,目标是得到网络的输出结果,再将输出结果和目标值相比较,得到结果误差。之后将结果误差沿着网络结构反向传播拆解到每个节点,得到每个节点的误差,然后根据每个节点的误差调整该节点的权重。“前向传播”的目的是得到输出结果,“反向传播”的目的是通过反向传播误差来调整权重。通过两者互相交替迭代,希望达到输出结果和目标值一致。
|
||||
|
||||
理解卷积神经网络之后,我们可以动手实现手写字体识别的示例。掌握之后,可以接着学习深度学习里的物体检测算法,例如YOLO、Faster R-CNN等,最后可以动手用TensorFlow都写一遍,跑跑训练数据、调调参数,边动手边理解。在学习过程中,要特别留意算法模型的设计思想和解决思路。
|
||||
|
||||
**2. 效果优化**
|
||||
|
||||
效果优化指提升算法模型的准确率等指标,通常的方式有以下几种:
|
||||
|
||||
<li>
|
||||
优化训练数据
|
||||
</li>
|
||||
<li>
|
||||
优化算法设计
|
||||
</li>
|
||||
<li>
|
||||
优化模型训练方式
|
||||
</li>
|
||||
|
||||
**优化训练数据**
|
||||
|
||||
因为算法模型是从训练数据中学习的,模型无法学习到训练数据之外的模式。所以,在选择训练数据时要特别小心,必须使得训练数据包含实际场景中会出现的模式。精心挑选或标注训练数据,会有效提升模型的效果。训练数据的标注对效果非常重要,以至于有创业公司专门从事数据标注,还获得了不少融资。
|
||||
|
||||
**优化算法设计**
|
||||
|
||||
根据问题采用更好的算法模型,采用深度学习模型而不是传统的机器学习模型,采用具有更高特征表达能力的模型等,例如使用残差网络或DenseNet提高网络的特征表达能力。
|
||||
|
||||
**优化模型训练方式**
|
||||
|
||||
优化模型训练方式包括采用哪种损失函数、是否使用正则项、是否使用Dropout结构、使用哪种梯度下降算法等。
|
||||
|
||||
**3. 计算量优化**
|
||||
|
||||
虽然我们在框架侧做了大量的工作来提高计算性能,但是如果在算法侧能够减少计算量,那么整体的计算实时性也会提高。从模型角度,减少计算量的思路有两种,一种是设计轻量型网络模型,一种是对模型进行压缩。
|
||||
|
||||
**轻量型网络设计**
|
||||
|
||||
学术界和工业界都设计了轻量型卷积神经网络,在保证模型精度的前提下,大幅减少模型的计算量,从而减少模型的计算开销。这种思路的典型代表是谷歌提出的[MobileNet](https://blog.csdn.net/u011974639/article/details/79199306),从名字上也可以看出设计的目标是移动端使用的网络结构。MobileNet是将标准的卷积神经网络运算拆分成Depthwise卷积运算和Pointwise卷积运算,先用Depthwise卷积对输入各个通道分别进行卷积运算,然后用Pointwise卷积实现各个通道间信息的融合。
|
||||
|
||||
**模型压缩**
|
||||
|
||||
模型压缩包括结构稀疏化和蒸馏两种方式。
|
||||
|
||||
在逻辑回归算法中,我们通过引入正则化,使得某些特征的系数近似为0。在卷积神经网络中,我们也希望通过引入正则化,使得卷积核的系数近似为0。与普通的正则化不同的是,在结构稀疏化中,我们希望正则化实现结构性的稀疏,比如某几个通道的卷积核的系数全部近似为0,从而可以将这几个通道的卷积核剪枝掉,减少不必要的计算开销。
|
||||
|
||||
蒸馏方法有迁移学习的意思,就是设计一个简单的网络,通过训练的方式,使得该简单的网络具有目标网络近似的表示能力,从而达到“蒸馏”的效果。
|
||||
|
||||
## Android开发同学的机会
|
||||
|
||||
移动端机器学习的计算框架和算法,前者负责模型计算的性能,减少时间开销;后者主要负责模型的精度,还可以通过一些算法设计减少算法的计算量,从而达到减少时间开销的目的。
|
||||
|
||||
需要注意的是,在移动端机器学习中,**算法模型的训练通常是在服务器端进行的**。目前,终端设备通常不负责模型的训练。在使用时,由终端设备加载训练结果模型,执行前向计算得到模型的计算结果。
|
||||
|
||||
但是前面讲了那么多行业趋势和机器学习的基本技术,那对于移动开发的同学来说,如何进入这个“热门”的领域呢?移动端机器学习是边缘智能计算范畴的一个领域,而且移动端开发是Android开发同学特别熟悉的领域,所以这也是Android开发同学的一个发展机会,转型进入边缘智能计算领域。Android开发同学可以发挥自己的技术专业优势,先在边缘计算的终端设备程序开发中站稳脚跟,在未来的技术分工体系中有个坚固的立足点;同时,逐步学习深度学习算法,以备将来往前迈一步,进入边缘智能计算领域,创造更高的技术价值。
|
||||
|
||||
可能在大部分情况下,Android开发同学在深度学习算法领域,跟专业的算法同学相比,不具有竞争优势,所以我们千万不要放弃自己所专长的终端设备的开发经验。对大多数Android开发同学而言,“专精Android开发 + 懂深度学习算法”才是在未来技术分工中,创造最大价值的姿势。
|
||||
|
||||
对于**学习路径**,我建议Android开发同学可以先学习卷积神经网络的基础知识(结构、训练和前向计算),然后阅读学习NCNN开源框架,掌握计算性能的优化方法,把开发技术掌握好。同时,可以逐步学习算法技术,主要学习各种常见的深度学习算法模型,并重点学习近几年出现轻量型神经网络算法。总之,Android开发同学要重点掌握提高计算实时性的开发技术和算法技术,兼顾学习深度学习算法模型。
|
||||
|
||||
基于前面的描述,我梳理了移动端机器学习的技术大图供你参考。图中红圈的部分,是我建议Android开发同学重点掌握的内容。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/96/141be237a4475c362daa1aa3be794996.png" alt="">
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
177
极客时间专栏/geek/Android开发高手课/模块三 架构演进/40 | 动态化实践,如何选择适合自己的方案?.md
Normal file
177
极客时间专栏/geek/Android开发高手课/模块三 架构演进/40 | 动态化实践,如何选择适合自己的方案?.md
Normal file
@@ -0,0 +1,177 @@
|
||||
<audio id="audio" title="40 | 动态化实践,如何选择适合自己的方案?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/25/96/2513a3decd8de8dca0ef60cea74da296.mp3"></audio>
|
||||
|
||||
在专栏第36期[《跨平台开发的现状与应用》](https://time.geekbang.org/column/article/88161)中,我分享了H5、React Native/Weex、小程序这几种常见的跨平台开发方式。站在开发的角度,虽然跨平台的开发效率要比Native开发更高,但是这并不是大前端在国内盛行的最主要原因。
|
||||
|
||||
相比跨平台能力,国内对大前端的动态化能力更加偏执。这是为什么呢?移动互联网已经发展十年了,随着业务成熟和功能的相对稳定,整体重心开始偏向运营,强烈的运营需求对客户端架构和发布模式都提出了更高的要求。如果每个修改都需要经历开发、上线、版本覆盖等漫长的过程,根本无法达到快速响应的要求。
|
||||
|
||||
所以H5、React Native/Weex、小程序在国内的流行,可以说是动态化能力远比跨平台能力重要。那我们应该选择哪一种动态化方式呢?正如我在跨平台开发所说的,目前这几种方案或多或少都还存在一些性能问题,如果一定要使用Native开发方式,又有哪些动态化方案?今天我们一起来学习应该如何选择适合自己的动态化方案。
|
||||
|
||||
## 动态化实践的背景
|
||||
|
||||
前几天在朋友圈看到说淘宝iOS客户端上一个版本的更新,已经是两个多月前的事情了。淘宝作为一个业务异常庞大且复杂的电商平台,这样的发布节奏在过去是很难想象的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/7d/bb60313024c59b2ed4b4430396e25f7d.png" alt="">
|
||||
|
||||
而现在即使不通过发布新版本,我们也能实现各式各样的运营活动和个性化推荐。依赖客户端的动态化能力,我们不需要等待应用商店审核,也无须依赖用户的主动更新,产品在快速迭代的同时,也有着非常强大的试错能力。
|
||||
|
||||
**1. 常见的动态化方案**
|
||||
|
||||
移动端动态化方案在最近几年一直是大家关注的重点,虽然它已经发展了很多年,但是每年都会有新的变化,这里我们先来看看各大公司有哪些已知的动态化方案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/73/27f332588dae2d5f4d1a210e3e77dc73.jpg" alt="">
|
||||
|
||||
在2018年北京QCon大会上,美团工程师分享了他们在动态化的实践[《美团客户端动态化实践》](https://github.com/AndroidAdvanceWithGeektime/Chapter37/blob/master/Qcon%E5%8C%97%E4%BA%AC2018--%E3%80%8A%E7%BE%8E%E5%9B%A2%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%8A%A8%E6%80%81%E5%8C%96%E5%AE%9E%E8%B7%B5%E3%80%8B--%E6%96%B9%E9%94%A6%E6%B6%9B.pdf)。美团作为一个强运营的应用,对动态化有非常强烈的诉求,也有着非常丰富的实践经验,他们将动态化方案分为下面四种类型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/e0/5e851695fbc022bc80a9ce634b5b34e0.png" alt="">
|
||||
|
||||
<li>
|
||||
**Web容器增强**。基于H5实现,但是还有离线包等各种优化手段加持,代表方案有PWA、腾讯的VasSonic、淘宝的zCache以及大部分的小程序方案。
|
||||
</li>
|
||||
<li>
|
||||
**虚拟运行环境**。使用独立的虚拟机运行,但最终使用原生控件渲染,代表方案有React Native、Weex、快应用等。
|
||||
</li>
|
||||
<li>
|
||||
**业务插件化**。基于Native的组件化开发,这种方式在淘宝、支付宝、美团、滴滴、360等航母应用上十分常见。代表方案有阿里的Atlas、360的RePlugin、滴滴的VirtualAPK等。除此之外,我认为各个热修复框架应该也属于业务插件化的一种类型,例如微信的Tinker、美团的Robust、阿里的AndFix。
|
||||
</li>
|
||||
<li>
|
||||
**布局动态化**。插件化或者热修复虽然可以做到页面布局和数据的动态修改,但是代价巨大,而且也不容易实现个性化运营。为了实现“千人千面”,淘宝和美团的首页结构都可以通过动态配置更新。代表的方案有阿里的Tangram、Facebook的Yoga。
|
||||
</li>
|
||||
|
||||
**2. 动态化方案的选择**
|
||||
|
||||
四大动态化方案哪家强,我们又应该如何选择?在回答这个问题之前,我们先来看看它们的差别。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/67/2b7dcceb9c990918e09e2f0ddd05a967.jpg" alt="">
|
||||
|
||||
目前我们还无法找到一种“十全十美”的动态化方案,每种方案都有自己的优缺点和对应的使用场景。比如Web容器增强方案在动态化能力、开发效率上有着非常大的优势,但稳定性和流畅度差强人意。恰恰相反,布局动态化方案在性能上面有非常不错的表现,但是在动态化能力和开发效率上面却受到不少限制。
|
||||
|
||||
所以说动态方案的选择,我们需要考虑下面这些因素。
|
||||
|
||||
<li>
|
||||
**业务类型**。主要考虑业务的重要性、交互是否复杂、对性能的要求、是否长期迭代等因素。
|
||||
</li>
|
||||
<li>
|
||||
**团队技术栈和代码的历史包袱**。在选择方案的时候,也需要结合团队的技术栈现状以及代码的历史包袱综合考虑。以微信为例,作为一个强交互的IM应用,团队基本以Native开发为主,而且微信基本没有太多运营上的需求,所以当时在动态化方案上只使用了Tinker。当然团队的技术栈并不是永恒不变,有了微信小程序之后,内部的一些业务也尝试使用小程序来改造。
|
||||
</li>
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/70/d985b8e7426ae89d9ccabced38422c70.jpg" alt="">
|
||||
|
||||
最终无论我们选择哪种动态化类型,我都建议公司内部同一种动态化类型都使用同一个方案,这样在统一技术栈的同时,也可以实现代码在不同业务之间的迁移。比如阿里内部的虚拟运行环境统一使用Weex,一个业务在手淘的效果不错,也可以快速迁移到飞猪、天猫等其他应用中,实现应用的流量矩阵。
|
||||
|
||||
同样对于运营活动也是如此,阿里内部有一个叫[PopLayer](https://yq.aliyun.com/articles/59050)的组件,它可以在任意Native页面(这个页面甚至可以是Browser)弹出H5的部署容器,可以在无需发版的情况下对已有的Native界面上浮出透明浮层,并且可以不影响Native页面本身的交互。这样做活动我们不需要在客户端提前一两个月开发代码,而且同一个活动也可以快速在公司内部的各个应用中上线。
|
||||
|
||||
## Native动态化方案
|
||||
|
||||
Web容器增强和虚拟运行环境方案通过独立的Runtime和JS-SDK来桥接Native模块,而业务插件化则通过插件化框架和接口能力直接调用。相比之下前者更加抽象而不易造成代码混乱,这也是目前各大公司逐渐开始“去插件化”的原因。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/24/3bfe93916ac78f7190474cdd6c36ad24.png" alt="">
|
||||
|
||||
最近两年,大前端开发越演越烈,传统的Native动态化方案是否还存在价值,它又该何去何从?热修复、插件化这些方案的未来又将如何演进呢?
|
||||
|
||||
**1. 热修复和插件化**
|
||||
|
||||
2016年在开源Tinker的时候有两件事情是超出我预料的,一个是热修复在国内竟然有那么大的反响,另外一个就是它竟然如此的“坑坑不息”。
|
||||
|
||||
从[《Tinker:技术的初心与坚持》](https://mp.weixin.qq.com/s/tlDy6kx8qVHQOZjNpG514w)一文中,你可以看到过去我们踩过的一小部分坑,但非常不幸的是,填坑之路至今依然没有结束。每次Android新版本发布,我们就像迎来期末考试一样步步惊心。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/fa/85a4abdd15b978a0f2ec2dec8d9298fa.png" alt="">
|
||||
|
||||
曾经微信希望使用Tinker来代替版本发布,在热修复的基础上实现四大组件的代理。但是Android P私有API限制的出现,基本打消了这个念头。热修复不能代替版本发布,但是我们可以通过它来实现一些应用商店不支持的功能,例如精准的灰度人数控制、渠道和用户属性选择、整包的A/B测试等。
|
||||
|
||||
另一方面,热修复给国内的Android生态也带来一些不太好的影响,比如增加用户ROM体积占用、App启动变慢15%、OTA首次卡顿等。特别是Android Q之后,动态加载的Dex都只使用解释模式执行,会加剧对启动性能的影响。因为性能的问题,目前大公司基本暂停了全量用户的热修复,只使用热修复用于灰度和测试。
|
||||
|
||||
热修复如此,插件化也是如此。笨重的插件化框架不仅影响应用的启动速度,而且多团队协作的时候并没有想象得那么和谐,接口混乱、仓库不好管理、编译速度慢这些问题都会存在。插件化回归模块化和组件化,这也是目前各大公司都在逐步推进的事情。
|
||||
|
||||
前一阵子,徐川在[《移动开发的罗曼蒂克消亡史 》](https://mp.weixin.qq.com/s/2xBnlmESZjq7UTtcfzqhcA)一文中回顾了热修复和插件化的前世今生。时间一转三年过去了,对于曾经参与这个浪潮的一份子来说,我可以做的只是顺应潮流的变化。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/0b/388999a6bcca0adeb91635d3f0eca80b.png" alt="">
|
||||
|
||||
**热修复的未来**
|
||||
|
||||
Tinker设计之初参考了Instant Run的编译方案,但是正如专栏第26期[《关于编译,你需要了解什么?》](https://time.geekbang.org/column/article/82468)中所说的,Google在Android Studio 3.5之后,对于Android 8.0以上的设备将会使用[Apply Changes](https://medium.com/androiddevelopers/android-studio-project-marble-apply-changes-e3048662e8cd)替代之前的Instant Run方案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/69/e6396e5202b17edc691e7b934f6e6b69.png" alt="">
|
||||
|
||||
Apply Changes不再使用插入PathClassloader的方式,而是使用我们已经多次讨论过的JVM TI。在Android 8.0之后,JVM TI开始逐渐支持ClassTransform和ClassRedefine这两个接口,它们可以允许虚拟机在运行时动态修改类,实现运行时的动态字节码编织。事实上这个技术在JVM就已经非常成熟,Java服务端利用这两个接口实现了类似热部署、远程调试、动态追踪等能力,具体你可以参考[《Java动态追踪技术探究》](https://tech.meituan.com/2019/02/28/java-dynamic-trace.html)。
|
||||
|
||||
那热修复的未来将要走向何方?本来我对热修复的未来是非常悲观的,但是Android Q给了我一个很大的惊喜。我们知道,Android P在中国有非常多的应用出现了兼容性问题,其中大部分是热修复、插件化以及加固等原因造成的(Google提供的数据是43%的兼容性问题由这三个问题造成)。
|
||||
|
||||
为了解决这个问题,并且减少我们对私有API的调用,Google在Android P新增了[AppComponentFactory](https://developer.android.com/reference/android/app/AppComponentFactory) API,并且在Android Q增加了替换Classloader的接口instantiateClassloader。在Android Q以后,我们可以实现在运行时替换已经存在ClassLoader和四大组件。中国热修复的先驱们用自己的“牺牲”,总算换来了Google官方的支持。我们使用Google官方API就可以实现热修复,这样以后Android版本再升级也不用担惊受怕了。移动开发的罗曼蒂克并没有消亡,Native的热修复再次迎来了春天。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/d3/2c57e70df59f3db4ee33e96819c3ced3.png" alt="">
|
||||
|
||||
**插件化的未来**
|
||||
|
||||
对于插件化的未来,我们需要思考如何“回归官道”。Google在2018年推出了[Android App Bundles](https://developer.android.com/guide/app-bundle),它可以实现模块的动态下载,但是与插件化不同的是,它并不支持四大组件代理的能力。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d7/10/d7162ca4f725a4b3671702a87ad65c10.png" alt="">
|
||||
|
||||
但是Android App Bundles方案依赖Play Service,在国内我们根本无法使用。爱奇艺的[Qigsaw](https://zhuanlan.zhihu.com/p/40035587?utm_source=wechat_timeline&utm_medium=social&utm_oi=748276889193299968&from=timeline&isappinstalled=0)可能对我们有所启发,它基于Android App Bundles实现(支持动态更新,但是不支持四大组件代理),同时完全仿照AAB提供的Play Core Library接口加载插件,如果有国际化需求的公司可以在国内版和国际版上无缝切换。这种方案不仅可以使用Google提供的编译工具链,也支持国际国内双轨,相当于Google为我们维护整个组件化框架,在国内只需要实现自己的“Play Service”即可。
|
||||
|
||||
当然和热修复一样,如果使用[AppComponentFactory](https://developer.android.com/reference/android/app/AppComponentFactory) API,我们也可以实现插件化的四大组件代理。但是具体实现上依然需要在AndroidManifest中预先注册四大组件,然后具体的替换规则可以在我们自定义的AppComponentFactory实现类中埋好。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/c2/b095bdb7c73b481904c384a296eb4ec2.png" alt="">
|
||||
|
||||
以Activity替换为例,我们可以将某些类名的Activity替换成其他的Activity,新的Activity可以在补丁中,也可以在其他插件中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9d/43/9d7c201af7f57909e7d8af4da1010c43.png" alt="">
|
||||
|
||||
**热修复和插件化作为Native动态化方案,它们有一定的局限性。随着移动技术的发展,部分功能可能会被替换成小程序等其他动态化方案。但是从目前来看,它们依然有非常大的存在价值和使用场景。**
|
||||
|
||||
**2. 布局动态化**
|
||||
|
||||
正如上文所说,像淘宝、美团首页这些场景,我们对性能要求非常高,这里只能使用Native实现。但是首页也是流量的聚集地,“提增长、提留存、提转化”都要求我们有强大的运营能力。最近两年,淘宝、天猫一直推行“千人千面”,每个用户看到的主页布局、内容可能都不太一样。
|
||||
|
||||
布局动态化正是在这个背景之下应运而生,在我看来,布局动态化需要具备下面三个能力。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/17/a8/1791818514edda690fe9b455f32083a8.jpg" alt="">
|
||||
|
||||
<li>
|
||||
**UI容器化**。能够动态地新增、调整UI界面而无需发版。
|
||||
</li>
|
||||
<li>
|
||||
**能力接口化**。点击、跳转等通用能力可以通过路由协议对外提供,满足UI容器化后的调用需求。
|
||||
</li>
|
||||
<li>
|
||||
**数据通道化**。数据上报也可以通过字段配置,实现客户端根据配置自动上报。
|
||||
</li>
|
||||
|
||||
在具体的实践上,天猫开源的[Tangram](https://github.com/alibaba/Tangram-Android/blob/master/README-ch.md)是一个不错的选择。但是Tangram的整体方案会相对复杂一些,我们也可以基于底层的[VirtualView](https://github.com/alibaba/Virtualview-Android/blob/master/README-ch.md)做二次开发。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/59/d8/59e307f779fdd94f43403d8b9baaabd8.jpg" alt="">
|
||||
|
||||
总的来说,布局动态化相比虚拟运行环境来说,它不仅实现了UI的动态新增和修改,也有着良好的体验和性能,同时接入和学习成本也比较低。
|
||||
|
||||
## 总结
|
||||
|
||||
“路漫漫其修远兮,吾将上下而求索”,我们对动态化实践的探索一直没有停止。今年,Flutter也强势地杀入了这个“战场”,那Flutter在跨平台和动态化方面表现如何,我们将在专栏下一期中揭晓。
|
||||
|
||||
动态化如今在国内是炙手可热的研究方向,虽然每个公司都强行造了自己的轮子,但是动态化方案目前还有很多没有解决的问题。所以在我们解决这些问题的过程中,也还会不断演变出其他的各种新方案。
|
||||
|
||||
现在各种类型的动态化方案,目前都能找到自己的应用场景。移动技术在快速地发展,我们无法准确预料到未来,比如说在Android P我们正准备放弃热修复的时候,Android Q又使它重新焕发了青春。但是我们坚信,无论未来采用何种方案,都是为了给用户更好的体验,同时让业务可以更快地迭代,并在不断地尝试中,给用户提供更好的产品。
|
||||
|
||||
## 课后作业
|
||||
|
||||
对于动态化实践,你有什么看法?在你的应用中,使用了哪种动态化方式?欢迎留言跟我和其他同学一起讨论。
|
||||
|
||||
跨平台和动态化可以说是大前端时代最大的两个特点,也是每年技术大会的重点。今天的课后作业是仔细阅读下面大会的分享内容,学习各大公司在大前端的实践经验,并留言写写你的心得体会。
|
||||
|
||||
<li>
|
||||
2018年北京QCon:美团[《美团客户端动态化实践》](https://github.com/AndroidAdvanceWithGeektime/Chapter37/blob/master/Qcon%E5%8C%97%E4%BA%AC2018--%E3%80%8A%E7%BE%8E%E5%9B%A2%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%8A%A8%E6%80%81%E5%8C%96%E5%AE%9E%E8%B7%B5%E3%80%8B--%E6%96%B9%E9%94%A6%E6%B6%9B.pdf)。
|
||||
</li>
|
||||
<li>
|
||||
2018年GMTC:闲鱼[《基于Google+Flutter的移动端跨平台应用实践》](https://github.com/AndroidAdvanceWithGeektime/Chapter37/blob/master/GMTC2018%EF%BC%8D%E3%80%8A%E5%9F%BA%E4%BA%8EGoogle%2BFlutter%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%AB%AF%E8%B7%A8%E5%B9%B3%E5%8F%B0%E5%BA%94%E7%94%A8%E5%AE%9E%E8%B7%B5%E3%80%8B-%E7%8E%8B%E6%A0%91%E5%BD%AC.pdf)。
|
||||
</li>
|
||||
<li>
|
||||
2018年GMTC:京东[《当插件化遇上Android+P》](https://github.com/AndroidAdvanceWithGeektime/Chapter37/blob/master/GMTC2018-%E3%80%8A%E5%BD%93%E6%8F%92%E4%BB%B6%E5%8C%96%E9%81%87%E4%B8%8AAndroid%2BP%E3%80%8B-%E5%BC%A0%E5%BF%97%E5%BC%BA.pdf)。
|
||||
</li>
|
||||
<li>
|
||||
2018年GMTC:小米[《快应用开发与实现指南》](https://github.com/AndroidAdvanceWithGeektime/Chapter37/blob/master/GMTC2018-%E3%80%8A%E5%BF%AB%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E4%B8%8E%E5%AE%9E%E7%8E%B0%E6%8C%87%E5%8D%97%E3%80%8B-%E8%91%A3%E6%B0%B8%E6%B8%85.pdf)。
|
||||
</li>
|
||||
<li>
|
||||
2018年GMTC:绿色守护[《Android+研发的昨天、今天与明天》](https://github.com/AndroidAdvanceWithGeektime/Chapter37/blob/master/GMTC2018-%E3%80%8AAndroid%2B%E7%A0%94%E5%8F%91%E7%9A%84%E6%98%A8%E5%A4%A9%E3%80%81%E4%BB%8A%E5%A4%A9%E4%B8%8E%E6%98%8E%E5%A4%A9%E3%80%8B-%E5%86%AF%E6%A3%AE%E6%9E%97.pdf)。
|
||||
</li>
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<audio id="audio" title="41 | 聊聊Flutter,面对层出不穷的新技术该如何跟进?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b8/06/b887fa6cd26b7e10205ddb962be06906.mp3"></audio>
|
||||
|
||||
“天下苦秦久矣”,不管是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](https://www.youtube.com/watch?v=h7HOt3Jb1Ts),在这个访谈中他谈到了当初为什么开发Flutter,以及Flutter的一些设计原则和方向。<br>
|
||||
<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/5c/aca58328ba1b4f1463d1b2b806c5ad5c.png" alt="">
|
||||
|
||||
Eric Seidel和Flutter早期的几位开发人员都是来自Chrome团队,他们在排版和渲染方面具有非常丰富的经验。那为什么要去开发Flutter?一直以来他们都为浏览器的性能而感到沮丧,有一天他们决定跳出Web的范畴,在Chromium基础上通过删除大量的代码,抛弃Web的兼容性,竟然发现性能是之前的20倍。对此我也深有感触,最近半年也一直在做主App的Lite版本,安装包也从42MB降到8MB,通过删除大量的历史业务,性能比主包要好太多太多。
|
||||
|
||||
正如访谈中所说,Flutter是一个Google内部孵化多年的产品,从它开发之初到现在,一直秉承着两个最重要的设计原则:
|
||||
|
||||
<li>
|
||||
**性能至上**。内置布局和渲染引擎,使用Skia通过GPU做光栅化。选择Dart语言作为开发语言,在发布正式版本时使用AOT编译,不再需要通过解析器解释执行或者JIT。并且支持Tree Shaking无用代码删除,减少发布包的体积。
|
||||
</li>
|
||||
<li>
|
||||
**效率至上**。在开发阶段支持代码的[Hot Reload](https://juejin.im/post/5bc80ef7f265da0a857aa924),实现秒级编译更新。重视开发工具链,从开发、调试、测试、性能分析都有完善的工具。内置Runtime实现真正的跨平台,一套代码可以同时生成Android/iOS应用,降低开发成本。
|
||||
</li>
|
||||
|
||||
那为什么要选择Dart语言?“Dart团队办公室离Flutter团队最近”肯定是其中的一个原因,但是Dart也为Flutter追求性能和效率的道路提供了大量的帮助,比如AOT编译、Tree Shaking、Hot Reload、多生代无锁垃圾回收等。正因为这些特性,Flutter在筛选了多种语言后,最终选择了Dart。这里也推荐你阅读[Why Flutter Uses Dart](https://hackernoon.com/why-flutter-uses-dart-dd635a054ebf)这篇文章。
|
||||
|
||||
总的来说,Flutter内置了布局和渲染引擎,使用Dart作为开发语言,采用React方式编写UI,支持一套代码在多端运行的框架。但是正如专栏前面所说的,大前端时代的核心诉求是跨平台和动态化,下面我们就一起来看看Flutter在这两方面的表现。
|
||||
|
||||
**1. Flutter的跨平台开发**
|
||||
|
||||
虽然React Native/Weex使用了系统原生UI组件,通过原生渲染的方式来提升渲染速度和UI流畅度,但是因为JS执行效率、JSBridge的通信代价等因素,性能依然存在瓶颈,而且我们也无法抹平不同系统的平台差异,因此这样的跨平台方案注定艰难。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/09/64ee06b802ca55df19b8f1a30e422809.png" alt="">
|
||||
|
||||
正如Eric Seidel在访谈所说,Flutter是从浏览器引擎简化而来,无论是它的布局引擎(例如也是使用CSS Flexbox布局),还是渲染流水线的设计,都跟浏览器都有很多相似之处。但是它抛弃了浏览器沉重的历史包袱和Web的兼容性,实现了在保持性能的前提下的跨平台开发。
|
||||
|
||||
回想一下在专栏第37期[《工作三年半,移动开发转型手游开发》](https://time.geekbang.org/column/article/88442)中,我们还描述过另外一套内置Runtime的跨平台方案:[Cocos引擎](https://github.com/cocos2d/cocos2d-x)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/87/6f/87abff685ed21a61a5b1bf89da143f6f.png" alt="">
|
||||
|
||||
在我看来,Cocos和Unity这些游戏引擎才是最早并且成熟的跨平台框架,它们对性能的要求也更加苛刻。即使是“王者荣耀”和“吃鸡”这么复杂的游戏,我们也可以做得非常流畅。
|
||||
|
||||
Flutter和游戏引擎一样,也提供了一套自绘界面的UI Toolkit。游戏引擎虽然能实现跨平台开发,但它致力于服务更有“钱途”的游戏开发。游戏引擎对于App开发,特别是混合开发支持并不完善。
|
||||
|
||||
如下图所示,我们可以看到三种跨平台方案的对比,Flutter可以说是三者中最为轻量的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a2/02/a297b66d9b4378679d384ef4fe378c02.jpg" alt="">
|
||||
|
||||
**2. Flutter的动态化实践**
|
||||
|
||||
在专栏第40期[《动态化实践,如何选择适合自己的方案》](https://time.geekbang.org/column/article/89555)中,我提到了一个观点:“相比跨平台能力,国内对大前端的动态化能力更加偏执”。
|
||||
|
||||
在性能、跨平台、动态性这个“铁三角”中,我们不能同时将三个都做到最优。如果Flutter在性能、跨平台和动态性都比浏览器更好,那就不会出现Flutter这个框架了,而是成为Chromium的一个跨时代版本。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/7b/b9288c3aedb11afe45957557b90f257b.png" alt="">
|
||||
|
||||
浏览器选择的是跨平台和动态性,而Flutter选择的就是性能和跨平台。Flutter正是牺牲了Web的动态性,使用Dart语言的AOT编译,使用只有5MB的轻量级引擎,才能实现浏览器做不到的高性能。Flutter的第一波受众是Android上使用Java、Kotlin,iOS上使用Objective-C、Swift的Native开发。未来Flutter是否能以此为突破口,进一步蚕食Web领域的份额,现在还不得而知。
|
||||
|
||||
那Flutter是否支持动态更新呢?由于编译成AOT代码,在iOS是绝对不可以动态更新的。对于Android,Flutter的动态更新能力其实Tinker就已经实现了。从官方的提供方案来看,其实也只是替换下面的变化文件,可以说是Tinker的简化版。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/9c/6a410383cadbaae7c8822dee6f455d9c.png" alt="">
|
||||
|
||||
官方的动态修复方案可以说是非常鸡肋的,并且也是要求应用重启才能生效。如果同时修改了Native代码,这也是不支持的。
|
||||
|
||||
更进一步说,Flutter在Google Play上是否允许动态更新也是抱有疑问的。从Google Play上面的开发者政策中心[规定](https://play.google.com/intl/zh-CN_ALL/about/privacy-security-deception/malicious-behavior/)上看,在Google Play也是不允许动态更新可执行文件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/55/0369ac6b78069e2b5ea983bd1fa14d55.png" alt="">
|
||||
|
||||
从[《从Flutter的编译模式》](https://www.stephenw.cc/2018/07/30/flutter-compile-mode/)一文中,我们可以通过`flutter build apk --build-shared-library`将Dart代码编译成app.so。无论是libflutter.so,还是app.so(其实`vm_*`、`isolate_*`也是可执行文件)的动态更新,都违反了Google Play的政策。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/99/8580a8da34098dc918e4cb7c7330c199.png" alt="">
|
||||
|
||||
当然不排除Google为了推广Flutter,为它的动态更新开绿灯。最近我也在咨询Google Play的政策组,目前还没有收到答复,如果后续有进一步的结果,我也可以同步给各位同学。
|
||||
|
||||
总的来说,Flutter的动态化能力理论上只能通过JIT编译模式解决,但是这会带来性能和代码体积的巨大影响。当然,闲鱼也在探索一套Flutter的布局动态化方案,你可以参考文章[《Flutter动态化的方案对比及最佳实现》](https://mp.weixin.qq.com/s/N5ih-DY5TuKyn_a0P2mz0Q)。
|
||||
|
||||
## 面对新技术,该如何选择
|
||||
|
||||
通过上面的学习,我们总算对Flutter的方方面面都有所了解。可以说Flutter是一个性能和效率至上,但是动态化能力非常有限的框架。
|
||||
|
||||
目前[闲鱼App](https://www.yuque.com/xytech/flutter/tc8lha)、[美团外卖](https://tech.meituan.com/2018/08/09/waimai-flutter-practice.html)、[今日头条App](https://mp.weixin.qq.com/s/-vyU1JQzdGLUmLGHRImIvg)、[爱奇艺开播助手](https://mp.weixin.qq.com/s/7GSPvP_hOWCv64esLLc0iw)、[网易新闻客户端](http://mp.weixin.qq.com/s/a0in4DqB8Bay046knkRr1g)、京东[JDFlutter](https://mp.weixin.qq.com/s/UhfgfNEdogm7Busr0apAGQ)、[马蜂窝旅游App](https://mp.weixin.qq.com/s/WBnj_6sOonjR9XUnB-wZPA),都分享过他们在使用Flutter的一些心得体会。如果有兴趣接入Flutter,非常推荐你认真看看前人的经验和教训。
|
||||
|
||||
无论是Flutter,还是其他新的技术,在抉择是否跟进的时候,我们需要考虑以下一些因素:
|
||||
|
||||
<li>
|
||||
**收益**。接入新的技术或者框架,给我们带来什么收益,例如稳定性、性能、效率、安全性等方面的提升。
|
||||
</li>
|
||||
<li>
|
||||
**迁移成本**。如果想得到新技术带来的收益,需要我们付出什么代价,例如新技术的学习成本、原来架构的改造成本等。
|
||||
</li>
|
||||
<li>
|
||||
**成熟度**。简单来说,就是这个新技术是否靠谱。跟选择开源项目一样,团队规模、能力是否达标、对项目是否重视都是我们需要考虑的因素。
|
||||
</li>
|
||||
<li>
|
||||
**社区氛围**。主要是看跟进这个技术的人够不够多、文档资料是否丰富、遇到问题能否得到帮助等。
|
||||
</li>
|
||||
|
||||
**1. 对于Flutter,我是怎么看的**
|
||||
|
||||
Flutter是一个非常有前景的技术,这一点是毋庸置疑的。我曾经专门做过一次全面的评估分析,但是得出的结论是暂时不会在我负责的应用中接入,主要原因如下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/ba/2316c956d3895fb2e4b2514a521e1bba.jpg" alt="">
|
||||
|
||||
目前我还没有跟进Flutter的核心原因在于收益不够巨大,如果有足够大的收益,其他几个因素都不是问题。而我负责的应用目前使用H5、小程序作为跨平台和动态化方案,通过极致优化后性能基本可以符合要求。
|
||||
|
||||
从另外一方面来说,新技术的学习和引入,无论是对历史代码、架构,还是我们个人的知识体系,都是一次非常好的重构机会。我非常希望每过一段时间,可以引入一些新的东西,打破大家对现有架构的不满。
|
||||
|
||||
新的技术或多或少有很多不完善的地方,这是挑战,也是机会。通过克服一个又一个的困难和挑战,并且在过程中不断地总结和沉淀,我们最终可能还收获了团队成员技术和其他能力的成长。以闲鱼为例,他们在Flutter落地的同时,不仅将他们的经验总结成几十篇非常高质量的文章,而且也参加了QCon、GMTC等一些技术大会,同时开源了[fish-redux](https://github.com/alibaba/fish-redux)、[FlutterBoost](https://github.com/alibaba/flutter_boost)等几个开源库,Flutter也一下成为了闲鱼的技术品牌。
|
||||
|
||||
可以相信,在过去一年,闲鱼团队在共同攻坚Flutter一个又一个难题的过程中,无论是团队的士气还是团队技术和非技术上的能力,都会有非常大的进步。
|
||||
|
||||
**2. 对于Flutter,大家又是怎么看的**
|
||||
|
||||
由于我还并没有在实际项目中使用Flutter,所以在写今天的文章之前,我也请教了很多有实际应用经验的朋友,下面我们一起来看看他们对Flutter又是怎么看的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/14/49ca149bb073fcbd0dc3b06494ddad14.jpg" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
曾几何时,我们一直对Chromium庞大的代码无从入手。而Flutter是一个完整而且比WebKit简单很多的引擎,它内部涉及从CPU到GPU,从上层到硬件的一系列知识,源码中有非常多值得我们挖掘去学习和研究的地方。
|
||||
|
||||
未来Flutter应用在小程序中也是一个非常有趣的课题,我们可以直接使用或者参考Flutter实现一个小程序渲染引擎。这样还可以带来另外一个好处,一个功能(例如微信的“附近的餐厅”)在不成熟的时候可以先以小程序的方式尝试,等到这个功能稳定之后,我们又可以无成本地转化为应用内的代码。
|
||||
|
||||
Dart语言从2011年启动以来,一直想以高性能为卖点,试图取代JavaScript,但是长期以来在Google外部使用得并不多。那在Flutter这个契机下,它未来是否可以实现弯道超车呢?这件事给我最大的感触是,机会可能随时会出现,但是需要我们时刻准备好。
|
||||
|
||||
“打铁还需自身硬”,我们还是要坚持修炼内功。对于是否要学习Flutter,我的答案是“多说无益,实践至上”。
|
||||
|
||||
## 课后作业
|
||||
|
||||
对于Flutter,你有什么看法?你是否准备在你的应用中跟进?欢迎留言跟我和其他同学一起讨论。
|
||||
|
||||
Flutter作为今年最为火热的技术,里面有非常多的机遇,可以帮我们打造自己的技术品牌(例如撰写文章、参加技术会议、开源你的项目等)。对于Flutter的学习,你可以参考下面的一些资料。
|
||||
|
||||
<li>
|
||||
万物之中,[源码](https://github.com/flutter/flutter)最美
|
||||
</li>
|
||||
<li>
|
||||
[Flutter官方文档](https://flutter.dev/docs)
|
||||
</li>
|
||||
<li>
|
||||
[闲鱼的Flutter相关文章](https://www.yuque.com/xytech/flutter)
|
||||
</li>
|
||||
<li>
|
||||
各大应用的使用总结:[闲鱼App](https://www.yuque.com/xytech/flutter/tc8lha)、[美团外卖](https://tech.meituan.com/2018/08/09/waimai-flutter-practice.html)、[今日头条App](https://mp.weixin.qq.com/s/-vyU1JQzdGLUmLGHRImIvg)、[爱奇艺开播助手](https://mp.weixin.qq.com/s/7GSPvP_hOWCv64esLLc0iw)、[网易新闻客户端](http://mp.weixin.qq.com/s/a0in4DqB8Bay046knkRr1g)、京东[JDFlutter](https://mp.weixin.qq.com/s/UhfgfNEdogm7Busr0apAGQ)、[马蜂窝旅游App](https://mp.weixin.qq.com/s/WBnj_6sOonjR9XUnB-wZPA)
|
||||
</li>
|
||||
<li>
|
||||
阿里Flutter开发者帮助App:[flutter-go](https://github.com/alibaba/flutter-go)
|
||||
</li>
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
|
||||
|
||||
|
||||
77
极客时间专栏/geek/Android开发高手课/模块三 架构演进/42 | Android开发高手课学习心得.md
Normal file
77
极客时间专栏/geek/Android开发高手课/模块三 架构演进/42 | Android开发高手课学习心得.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<audio id="audio" title="42 | Android开发高手课学习心得" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/64/e93852e3cfbf15d19fcc05831d58eb64.mp3"></audio>
|
||||
|
||||
>
|
||||
你好,我是张绍文。专栏更新至今,转眼间最后一个模块“架构演进”也已经 更新完了。从“Android开发高手课”筹备到现在将近的八个月里,非常感谢我们的学习委员孙鹏飞,不管是文章答疑还是练习Sample,鹏飞为这个专栏付出了太多太多,这里再次衷心地跟他说一声谢谢。
|
||||
|
||||
|
||||
>
|
||||
无论是我还是鹏飞,在写这个专栏的同时,也是重塑自身知识架构的过程。在专栏的最后一篇文章,我特意邀请鹏飞跟大家分享他的学习心得,也算是跟大家做一个小小的告别。鹏飞对于Android系统框架、虚拟机、Linux等底层知识非常熟悉,那他是如何做到的呢?不得不说的是,鹏飞是一个非常自律(+宅)的人,拥有坚持每天早上7点钟起床学习一个半小时的可怕技能。
|
||||
|
||||
|
||||
>
|
||||
最近鹏飞也准备奔向新的工作岗位,在此也祝愿他在新岗位上能够发挥所长。接下来我们一起来听听鹏飞学习专栏的心得和思考。
|
||||
|
||||
|
||||
大家好,我是孙鹏飞。“Android开发高手课”接近尾声,今天我来从一个学习者的角度,对专栏做一下总结。专栏涵盖了Android开发的方方面面的知识,有技术干货、心得体会、问题答疑和部分科普性质的文章,从内容上来说和平时大家看到的教程不太一样,没有过多介绍Android基础,更多的是提供新的思路和分享经验。很多同学感觉学习比较吃力,其实我也是如此,在开始准备这门课程的时候,很多东西也是从零开始学起。对于一个新的概念,从模糊到清晰的过程不会那么容易,它可能涉及Android和Linux很多细枝末节的知识点,需要阅读大量资料给予支撑才能理解。
|
||||
|
||||
下面我就总结一下我学习专栏的过程和一些思考,希望对你有所启发。
|
||||
|
||||
## 关于“高手课”的思考
|
||||
|
||||
最近我了解到一些同学对C++和Linux感觉不知道如何入手,这里说一下我的学习过程以及相关的学习资料。你可以跟着《C++ Primier Plus》学习一下基本语法和标准库里的函数使用,比如字符串操作、内存分配、I/O操作等,这里有一个很好的[网站](https://zh.cppreference.com/)学习C/C++语言,包括最新的特性都有介绍。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/ed/a4969b8f63ab9df1b25f6f783bad0aed.png" alt="">
|
||||
|
||||
Linux应用开发的部分,可以一步步的参考《UNIX环境高级编程》这本书,熟悉一遍标准I/O库、系统数据文件和信息、进程环境、进程控制、进程关系、信号、线程、线程控制、守护进程,以及各种I/O、进程间通信、网络IPC的使用。同时也可以参考《Linux/UNIX系统编程手册》,这本书相对介绍得更详细一些。在了解了Linux应用开发相关内容后,可以深入了解一下Linux内核相关的知识,我推荐《Linux技术内幕》,这本书是国人写的,语言相对于一些翻译书要通顺一些,而且还有大量的图解;缺点是由于篇幅限制,有一部分内容没有介绍。
|
||||
|
||||
从专栏中你可以看到有些内容涉及ARM的汇编,ARM的汇编资料相对少一些,而且实践起来不是很方便,这里推荐一个网站[Writing ARM Assembly](https://azeria-labs.com/writing-arm-assembly-part-1/),可以学习到基本的汇编操作。关于实践可以使用[VisUAL](https://salmanarif.bitbucket.io/visual/index.html)工具来进行实验,这个工具可以逐步执行并可以实时观察寄存器和内存里的值。关于汇编相关的内容,我认为更多的时候只需要看懂即可,在实际中使用的地方可能并不是很多。
|
||||
|
||||
Android的更新迭代是很快的,在快速的更新中我们需要保持对Android新特性的了解,还要掌握兼容问题的处理方法,。现在官方文档更新的速度还是很快的,中文文档更新速度也很快,大部分功能和API新特性的描述都可以在Android官网上找到很详细的文档,比如[Android Q功能和API](https://developer.android.com/preview/features) ,并且每个版本官方都会提供很详细的[API差异报告](https://developer.android.com/sdk/api_diff/q-beta1/changes.html),这个报告将修改、新增的API都统计在一起,我们可以在这个表里发现一些新功能。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/53/dae40528ad994a7e4637980f787bb153.png" alt="">
|
||||
|
||||
关于虚拟机方面的更新,我更多是参考官方的提交记录来查看。具体可以看[Android Gerrit](https://android-review.googlesource.com/q/project:platform/art+status:open),这里有每一条提交记录,并且可以查看很详细的Code Diff,提交记录的描述一般都很清晰,所以可以很好的帮助我们理解代码,也可以发现一些新的工具和功能。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/a0/10c64f71ea7507bffc3411815f486fa0.png" alt="">
|
||||
|
||||
## 专栏学习心得
|
||||
|
||||
我经常听到很多同学说一个技术很难,其实我觉得大多时候不是技术难,而是应该说复杂。技术大多都是演进而来,其实这种演进和我们业务需求迭代的过程很类似。比如内存管理,从固定分区、动态分区机制到分段机制,然后发展成现在的分页机制,硬件也随之演进,都算是一个迭代过程。理解分页机制其实不难,但这个知识点所涉及的内容就很繁琐和复杂了,比如了解系统如何管理内存需要了解内存布局,就会涉及内存划分;内存划分又涉及CPU的运行模式,然后是物理内存和虚拟内存如何映射,会涉及页表的翻译、物理页面的分配和释放、伙伴系统算法;为了解决伙伴系统的内存碎片化问题,又衍生出迁移类型等。
|
||||
|
||||
这一系列涉及的内容就非常复杂,但每一项单拆出来去看,一层层去学习和补充,就会感觉容易很多。这一点其实在业务开发上也有体现,我们刚接手一个复杂的业务,代码庞大,注释和文档都很少,但在一段时间后你还是会对整个业务有或多或少的认识,在接到新的业务的时候也没有觉得难到无从下手,顶多是觉得复杂。底层的系统、框架也是如此,这是一个由点到面的过程。
|
||||
|
||||
**忙碌状态下的学习**
|
||||
|
||||
鸿洋写过一篇文章[《嗷嗷加班,如何保持学习能力》](https://mp.weixin.qq.com/s/gGOHF4-_tTHHGSbsX4Mptg),讲在繁忙的工作状态下如何保持学习,我看过之后也很有感触。在平时零散的时间里我们看到一篇技术文章,并不是阅读收藏后就结束了,这样你可能会在很短的时间里就忘掉了文章的内容。他将阅读一篇文章分成以下几个步骤:提取这篇文章要解决的问题;然后概括一下涉及的技术点;提取重点内容,比如问题发生的缘由、有哪几种解决方法。总体来说,这个方法是为了在短时间内提取出重点内容,然后记录下来后面再进行复习。所以我们都需要多记录、多复习,可以培养使用一些工具来帮助自己养成习惯。
|
||||
|
||||
**逃离舒适区**
|
||||
|
||||
并不是说换一个更忙碌的工作就是逃离舒适区,而是在平时工作和学习过程中保持一种焦虑感,但这种焦虑感不是迷茫和恐慌,而是清晰地认识到自己的不足,然后在工作和业余的时间里填补自己,当你集中注意力攻克一个难点的时候,你会发现这是一件有趣的事。我身边有很多同学都在持续地学习,每个人都有不同的目标,比如学到什么程度、应用到什么地方等,需要一定的压力才能产生比较好的效果,否则很容易迷失丢掉重点。我以前所在的团队有一个学习计划表,每个同学调研一个方向,每周周会的时候都会抽出一定时间去做技术分享,我觉得这是一个很好的方法,人在有一定紧张情绪的情况下注意力会相当集中,这个和考试前、面试前学习效率会很高的道理是一样的。不过我们也不能太过焦虑,我也经常会有焦虑感,我的解决方法是定制计划,半年或者一年,在一段时间专注完成一件事,你会看到自己成长了很多。
|
||||
|
||||
**刻意练习**
|
||||
|
||||
也许我们在公司里平时做的业务需求都是缝缝补补,并没有涉及很复杂的内容,大量重复的工作会让人觉得无法提升自己的技术。而平时自己学到的知识在一定时间之后可能就生疏了,而且有些技术可能从原理上看相对简单,但在实现的过程中会遇到各种问题,比如插件化和热修复,这样的技术如果不上手去实践就只能停留在理论层面。对于这种情况,可以采用一种刻意练习的方法,在知晓原理后自己尝试去实现一个类似的框架,在这个过程中你会得到比阅读源码和文章更多的实践经验,可以大大加深对一个知识的理解,也可以锻炼自己的框架设计能力。
|
||||
|
||||
**阅读源码**
|
||||
|
||||
别人写的源码分析文章我一般看得比较少,除非是自己遇到了很难理解的部分,大部分的内容都可以直接在代码里获取到。而且很多源码的分析文章就很少,比如Inline Hook ,但是框架实现比较多,那么就只能从代码里获取相关内容。针对一个功能的框架可以去找一些相同功能的多个框架进行对比,看一下实现方式是否有不同之处,同时比较一下每个实现方式的优缺点,并记录一下每个框架所使用的技术。在理解一个框架的实现后,还是建议你去自己写一个类似的框架,因为在自己的实现过程中会遇到各种问题,可以从其他框架里寻找一下方案,然后自己总结一下,这样可以了解这个框架的优势之处,如果在面试的时候被问到一个框架的优缺点,这样也可以有自己的理解,而不是网上帖子的统一描述。
|
||||
|
||||
**全栈发展**
|
||||
|
||||
在所谓的互联网寒冬下,需要持续关注其他方向的技术,提升自身竞争力。在前端的趋势热度和各个公司的发展方向中,“大前端”已经成为必要的技能,这也可以从各大公司的招聘方向中看出来。因此我们在平时学习过程中,可以更多关注一下大前端、跨平台、Flutter相关的内容。现在很多公司和部门讲究中台化和前后端闭环管理,对于后台和前端的技术都要大概有所了解才能掌握整个业务的动态,只关注一块技术在中国的互联网环境下不太利于自身的发展。换句话讲,我们关注得越多,机会也就更多一些。
|
||||
|
||||
**个人能力的提升**
|
||||
|
||||
这里的能力更多是指软实力的提升,一个是技术视野,也就是一个业务系统的全局把控,将一个自底向上的思维方式发展为从上到下的抽象能力;再有就是以技术价值为导向。以前我总是深入一些技术细节,只是觉得比较有趣,但很少考虑这个技术点能带来什么“价值”。其实在工作晋升和面试的过程中,通常关注更多的是“价值”,我们一般总说业务迭代、模块开发,但很少谈及所做功能的价值,只是觉得技术实现比较简单并没有什么可以讲的,其实我们可以从以下几个方面进行总结。
|
||||
|
||||
首先,我们开发了一个新功能、做了一些改进、引入了一些技术等,可能我们大多在做这些工作,也就是实现了一个业务需求,保证了业务功能的使用,这是功能产生的价值。在做这个需求功能的时候,我们有没有考虑过扩展性、重用性、维护性、性能、稳定性、高可用性等呢?性能的提升给用户带来体验上的价值;可扩展性、重用性带来开发效率的提升;稳定性减少了维护的成本等,这些都是技术产出的价值。我们可以更进一步从业务的角度上看,比如完成这个业务给用户体验提升了多少?促进了多少用户增长、提高了多少用户活跃度、降低了多少成本?由于我们在每个业务开发的时候,都会有一些数据统计的埋点,因此在平时的时候我们要多关注这种业务相关的数据。一般产品同学都会有各种数据报表,我们可以将他们总结起来作为自己完成一个业务所产出的价值。
|
||||
|
||||
## 写在最后
|
||||
|
||||
最后感谢各位同学能一直跟随“Android开发高手课”学习到最后,相信你一定也从专栏里学到了对自己有价值的新知识。我同样也是从专栏上线,随着专栏更新一点点学习到现在,从专栏里学到了很多思路和方法,也巩固了很多基础知识。但更多的基础知识专栏无法详细呈现,所以还需要我们以此为起点,自己在课下扩展开来,多去思考、多做总结。
|
||||
|
||||
最最后我想说,每个人在突破自己技术瓶颈时都会经历一段痛苦的时光,只有我们具有坚定的信念,并努力坚持下去,相信我等你回过头来再看曾经认为难以理解的技术和知识时,你会有一种阔然开朗、融会贯通的感觉,这就是成长和进步所带来最大的成就感。
|
||||
|
||||
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user