书名:深入理解Android内核设计思想(第2版)(上下册)
ISBN:978-7-115-45263-4
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 林学森
责任编辑 张 涛
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
全书从操作系统的基础知识入手,全面剖析进程/线程、内存管理、Binder机制、GUI显示系统、多媒体管理、输入系统、虚拟机等核心技术在Android中的实现原理。书中讲述的知识点大部分来源于工程项目研发,因而具有较强的实用性,希望可以让读者“知其然,更知其所以然”。本书分为编译篇、系统原理篇、应用原理篇、系统工具篇,共4篇25章,基本涵盖了参与Android开发所需具备的知识,并通过大量图片与实例来引导读者学习,以求尽量在源码分析外为读者提供更易于理解的思维方式。
本书既适合Android系统工程师,也适合于应用开发工程师来阅读,从而提升Android开发能力。读者可以在本书潜移默化的学习过程中更深刻地理解Android系统,并将所学知识自然地应用到实际开发难题的解决中。
4次大幅改版,N次修订,前后历时近3年,本书终于要与读者见面了。
在这3年的时间里,Android系统不断更新换代,书本内容也尽可能紧随其步伐——我总是会在第一时间下载到工程源码,然后系统性地比对和研究每次改版后的差异。可以说本书伴随着Android的高速发展,完整地见证了它给大家带来的一次又一次惊喜。
在这么长的写作跨度中,有一个问题始终萦绕在我的脑海中,即“为什么写这本书”?
市面上讲解操作系统的著作很多,主要风格有两种。
高校中采用的操作系统教材多数属于这种类型。它们主要阐述通用的计算机理论与原理,一般不会针对某个具体的操作系统做详细剖析。这类书籍是我们进入计算机科学的“敲门砖”。只有基础打得扎实,研究市面上任何一款操作系统才能做到“有的放矢”。
这类书籍以讲解某个具体的操作系统为主,如市面上就有非常多的关于Windows和Linux系统的。前者因为不开源,谁也不可能深入代码级别进行讲解;而后者则恰恰相反,任何人都能轻松获取到完整的内核源码。在Linux之父经典名言“Read the f***king Source Code”的鼓励下,无数有志之士投入到“代码汪洋”的分析中,从中细细感受大师们的设计艺术。
那么本书属于什么类型呢?个人认为更贴切地说,就是上面两种的结合。
本书的一个主要宗旨是希望读者可以由浅入深地逐步理解Android系统的方方面面。因而在每章节内容的编排上,采用由整体到局部的线索铺展开来——先让读者有一个直观感性的认识,明白“是什么”“有什么用”,然后才剖析“如何做到的”。这样的一个好处是读者在学习过程中不容易产生困惑;否则如果直接切入原理,长篇大论地分析代码,仅一大堆函数调用就可能让人失去学习的方向。这样的结果往往是,读者花了非常多的时间来理清函数关系,但始终不明白代码编写者的意图,甚至连这些函数想实现什么功能都无法完全理解。
本书希望可以从更高的层次,即抽象的、反映设计者思想的角度去理解系统。而在思考的过程中,大部分情况下我们都将从读者容易理解的基础知识开始讲起。就好比画一张素描画一 样—— 先给出一张白纸,勾勒出整体框架,然后针对重点部位细细加工,最后才能还原出完整的画面。另外,本书在对系统原理本身进行讲解的同时,也最大程度地结合工程项目中可能遇到的难点,理论联系实际地进行解析。希望这样的方式既能让读者真正学习到Android系统的设计思想,也能学有所用,增加一些实际的项目开发经验和技巧。
细心的读者会发现本书章节中包含了“Android和OpenGL ES”“信息安全基础概述”等看似与本书无关的内容——有些人可能会产生疑问,是否有此必要?
根据我们多年的Android项目开发和培训经验,答案就是“非常有必要”。举个例子,Android的显示系统是围绕OpenGL ES来展开的,后者是它的“根基”。但另外,并非所有开发人员都 深谙OpenGL ES。这样导致的结果就是他们在学习显示系统的过程中,有一种“四处碰壁”的感觉——实践证明,正是这些因素直接打击到了大家学习Android系统的信心。
因此我们在讲解系统实现原理之前,会最大程度地为读者提炼出所需的背景知识。有了这样的铺垫,相信对大家学习Android内核大有裨益。
本书在内容选择上依据的是“研发人员(包括系统开发和应用程序开发)参与实际Android项目所需具备的知识”,因而具有较强的实用性。全书共分为4篇,涵盖了编译、系统原理、应用原理和系统工具等多个方面。
其中第一篇不仅详细介绍了Android源码的下载及编译过程,为读者呈现了“Hello World”式的入门向导——更为重要的是,结合编译系统的架构和内部原理,为各厂家定制自己的Android产品提供了参考范例。
Android本质上只是市面上众多主流的操作系统之一。所以在系统原理的讲解过程中,我们将首先引导读者从计算机体系结构、经典的操作系统理论(比如进程/线程管理、进程间通信等)的角度来思考问题——包括Android在内的任何操作系统内核在实现过程中都“逃”不出这些经典的理论范畴。本书虽然是剖析Android系统的,但更希望读者可以从中学到“渔”,而不仅仅是“鱼”。
从动态运行的角度来理解,Android内核是由众多系统服务组成的,如ActivityManagerService、GUI系统中的SurfaceFlinger、音频系统中的AudioFlinger、输入系统InputManagerService等。而各服务之间通信的基础就是Binder机制。本书在阐述它们错综复杂的关系中,遵循由“整体到局部”“由点及面”的科学方法,将知识点深入浅出地铺展开来,希望为读者全面理解Android内核提供“思维捷径”。
与其他讲解Android应用程序的书籍不同,本书在分析APK应用程序时的立足点是它的内部实现原理。如Intent匹配规则、应用程序的资源适配过程、字符编码的处理、Widget机制、应用程序的编译打包等都是应用开发人员在工作中经常会遇到的难题。通过系统性地解析隐藏在这些实现背后的原理,有助于他们彻底摆脱困惑,加深对应用开发的理解。
不论是系统工程师还是应用开发人员,Android调试工具都至关重要。但我们在实际工作中发现,不少研发人员对这些工具“只知其一,不知其二”。因而系统工具篇中将针对常用调试工具进行全面解析,希望由此可以让大家学习到如何“举一反三”,真正把它们的作用发挥得“淋漓尽致”。
(1)通过大量情景图片与实例引导读者学习,以求尽量在源码分析外为读者提供更易于理解的思维路径。
(2)作者在展开一个话题时,通常会由浅入深、由总体框架再到细节实现。这样可以保证读者能跟得上分析的节奏,并且“有根有据可循”,尽可能防止部分读者阅读技术书籍时“看了后面忘了前面”的现象。
(3)目前市面上不少Android书籍仍停留在Android 2.3或者更早期的版本。虽然原理类似,但对于开发人员来说,他们需要与项目研发相契合的技术书籍。本书希望尽可能紧随Android的更新步伐,为读者了解最新的Android技术提供帮助。
(4)本书的出发点仍是操作系统的经典原理,并以此为根基扩展分析Android中的具体实现机制——贯穿其中的是经久不衰的理论知识。
(5)本书所阐述的知识点大部分来源于工程项目研发的经验总结,因而具有较强的实用性,希望可以让读者“知其然,更知其所以然”。做到真正贴近读者,贴近开发需求。
感谢王益民董事长、钟宝英女士长期以来的关心、信任和支持——你们在很多方面都是我们学习的楷模。衷心祝愿王总企业蒸蒸日上、再创辉煌;衷心祝愿钟小姐事事顺心如意、永葆青春。
感谢人民邮电出版社的编辑,你们的专业态度和处理问题的人性化,是所有作者的“福音”。
感谢我的家人、长辈和朋友林进跃、林美玉、林惠忠、刘冰、林月明、温艳,感谢你们长期以来对我工作和生活上无微不至的关心和支持。
感谢所有读者的支持,是你们赋予了我写作的动力。另外,因为个人能力和水平有限,书中可能还有不足之处,希望读者不吝指教,一起探讨学习,作者的联系方式是:xuesenlin@alumni. cuhk.net。编辑联系和投稿邮箱是:zhangtao@ptpress.com.cn。
Android系统的诞生地——美国硅谷。
Google大楼前摆放着Android的最新版本雕塑,历史版本则被放置在Android Statues Park中
写第2版前言时,笔者刚好在美国加州硅谷等地公事出差访问。其间我一直在思考的问题是,美国硅谷(Silicon Valley)在近几十年时间里长盛不衰的原因是什么?技术的浪潮总是一波接着一波的,谁又会在不远的将来接替Google的Android系统,在操作系统领域成为下一轮的弄潮儿?我们又应该如何应对这种“长江后浪推前浪”的必然更迭呢?
从历史的长河来看,新技术、新事物的诞生往往和当时的大背景有着不可分割的关系。如果我们追溯硅谷的发展史,会发现其实它相对于美国很多传统地区来说还是非常年轻的。“硅谷”这个词是在1971年的“Silicon Valley in the USA”系列报导文章中才首次出现的。20世纪四五十年代开始,硅谷就像一匹脱了缰的野马一般,“一发不可收拾”。从早期的Hewlett-Packard公司,到仙童、AMD、Intel以及后来的Apple、Yahoo!等众多世界一流企业,硅谷牢牢把握住了科技界的几次大变革,成功汇集了美国90%以上的半导体产业,逐步呈现出“生生不息”的景象。
但为什么是硅谷,而不是美国其他地区成为高科技行业的“发动机”呢?
古语有云,“天时、地利、人和,三者不得,虽胜有殃”。
现在我们回过头来看这段历史,应该说硅谷早期的发展和当时的世界大环境有很大关系——更确切地说,正是美国国防工业的发展诉求,才给了硅谷创业初期的“第一桶金”。只有“先活下来,才有可能走得更远”。而接下来社会对半导体工业需求的爆炸式增长,同样让硅谷占据了“天时”的优势,再接再厉最终走上良性循环。
密密麻麻的硅谷大企业
(引用自cdn.com)
硅谷的“地利”和“人和”,可能主要体现在:
(1)Stanford University
斯坦福大学校园
Stanford University在硅谷的发展过程中起到了非常关键的作用。20世纪50年代的时候,这所大学还并不是很起眼,各方面条件都比较糟糕,她的毕业生也多数会去东海岸寻求就业机会。后来她的一位教授Frederick Terman看到了产业和学术的接合点,从学校里划分出一大块空地来鼓励学生创业,并且指导其中两位学生创立了Hewlett-Packard公司。随后的几年他又成立了Stanford Research Park,这也同时是后来全球各高科技园区的起点,并吸引了越来越多的公司加入。在那段时间里,相信起到核心催化作用的是“产”+“学”的高度结合——将科技产品不断推陈出新产生经济效益,然后再回馈到研究领域。在几十年的跨度里,很多顶尖公司(Google、Yahoo!、HP等)的创始人都出自该校。有统计显示Standford师生及校友创造了硅谷一半以上的总产值,其影响力可见一斑。
(2)便利的地理环境
整个硅谷地区面积并不是很大,属于温带海洋性气候,全年平均温度在13℃~24℃,污染很小。同时,它依林傍海,陆、海、空都可以很好地与外界相连,这样一来自然有利于人才的引入。
(3)鼓励创新,完善的专利保护机制
从法律上讲,硅谷每年有超过4000项的专利申请,工程师和律师的比例达到了10∶1。在创新点得到保护的同时,也使得初创公司能够得到进一步的发展,从而避免它们被扼杀在摇篮中。从观念上来说,硅谷人对知识产权还是非常尊重的,他们大多认为剽窃是没有技术含量的,相当于“涸泽而渔”。
(4)完善的风投体系,并容忍失败
事实上在硅谷创业,其成本和失败率都很高——其中能存活3~5年的公司只有10%~20%。一方面,风险投资方需要高度容忍这样的失败率;另一方面,在允许快速试错的同时,风险投资方又可以从某些成功中获得巨大收益——硅谷就是一个可以达到这种矛盾平衡的神奇所在地。
“三十年河东,三十年河西”,技术的浪潮总是在不断演进的。从Symbian、Black Berry,到Android、iOS,历史经验告诉我们没有一项技术是会永远一成不变的。所以我们在技术领域的探索过程中,既要拿“鱼”,更要学会“渔”——前者是为当前的工作而努力,后者则是为我们的未来做投资。以Android操作系统为例,事实上我们除了“知其然”外,还更应该学习它的内部设计思想——即“知其所以然”。当我们真正地理解了那些“精华”所在以后,那么相信以后再遇到任何其他的操作系统,就都可以做到“触类旁通”了。也只有这样,或许才能在快速变革的科技领域中把握住脉搏,立于不败之地。
林学森
于美国硅谷
在第1版上市的这两年时间里,不断有读者来信分享他们阅读本书时的感想和心得,笔者首先要在这里衷心地向大家说声感谢!正是你们的支持和肯定,才有了《深入理解Android内核设计思想》(第2版)的诞生。
其中有不少读者提到了他们希望在本书后续更新中看到的内容,包括Android虚拟机的内部实现原理、Android的安全机制、Gradle自动化构建工具等——这些要求都在本次版本更新中得到了体现。
需要特别说明的是,第2版中的所有新增和有更新的部分都是基于Android最新的N版本展开的。由于Android版本的更新换代很快,且版本间的差异巨大,导致书中很多内容几乎需要全部重写。另外笔者写书都是在下班后的业余时间进行的,所以即便是每晚奋笔疾书到深夜,再加上周末和节假日时间(如果没有加班工作的话),最后发现更新全书所需时间依然要大于Android系统的发布间隔。为了让读者可以早日阅读到大家感兴趣的内容,本次版本的部分章节保留了第1版的原有内容——本书下一次再版时会争取将它们更新到Android的最新版本。这一点希望得到大家的谅解,谢谢!
感谢我目前任职公司的领导和同事们,是你们的帮助和支持,才让我更快地融入到了这个大家庭中。在一个到处都是“聪明人”和具有“狼性奋斗者”精神的公司里,每天的进步和知识积累都是让人愉悦的。
感谢人民邮电出版社的编辑,你们的专业态度和处理问题的人性化,是所有作者的“福音”。
感谢我的家人林进跃、张建山、林美玉、杨惠萍、林惠忠、林月明,没有你们的鼓励与理解,就没有本书的顺利出版。
感谢我的妻子张白杨的默默付出,是你工作之外还无怨无悔地在照顾着我们可爱的宝宝,才让我有充足的时间和精力来写作。
感谢所有读者的支持,是你们赋予了我写作的动力。另外,因为个人能力和水平有限,书中难免会有不足之处,希望读者不吝指教,一起探讨学习,作者的联系方式是:xuesenlin@alumni. cuhk.net。编辑联系和投稿邮箱是:zhangtao@ptpress.com.cn。本书读者交流QQ群为216840480。
作者
第1章 Android系统简介
第2章 Android源码下载及编译
第3章 Android编译系统
美国当地时间2015年5月28日,“Google I/O 2016”大会在旧金山市的Moscone Center举行。
会议公布的官方数据如下:
2016年,Google则直接把大会地址从传统的Moscone Center改到了Shoreline公园的户外,吸引了成千上万来自全球各地的科技爱好者。
从2008年9月Google发布Android 1.0版本开始,Android已经走过了8个年头。在这短短的几年间,这个以机器人为Logo的操作系统不仅席卷了全球各地的手机市场,而且与iOS、Windows Phone形成三足鼎立之势,更渗透到传统与新兴电子产业的方方面面。越来越多的电子产品已开始采用Android系统,如Android电视、平板电脑、MP4等与人们日常生活息息相关的电子设备。
那么,Android势不可当的魅力从何而来呢?本章将试着以Android系统的发展历史为主线,先为读者提供最直观的背景知识,从而为以后的“透过现象看本质”打下一定的基础。
“Android”一词先天就充满着天才们改变世界的梦想味。虽然一件杰出的作品并不能只靠“名号”,但毋庸置疑的是,一个叫得响又耐人寻味的名称总会使人产生不自觉的亲近感。这或许就是每个Android版本都会有个代号的原因。下面来看看各个版本对应的Android“外号”,如表1-1所示。
表1-1 Android各版本的代号
Code name |
Version |
API level |
---|---|---|
(no code name) |
1.0 |
API level 1 |
(no code name) |
1.1 |
API level 2 |
Cupcake(纸杯蛋糕) |
1.5 |
API level 3, NDK 1 |
Donut(甜甜圈) |
1.6 |
API level 4, NDK 2 |
Éclair(松饼) |
2.0 |
API level 5 |
Éclair |
2.0.1 |
API level 6 |
Éclair |
2.1 |
API level 7, NDK 3 |
Froyo(冻酸奶) |
2.2.x |
API level 8, NDK 4 |
Gingerbread(姜饼) |
2.3 - 2.3.2 |
API level 9, NDK 5 |
Gingerbread |
2.3.3 - 2.3.7 |
API level 10 |
Honeycomb(蜂巢) |
3.0 |
API level 11 |
Honeycomb |
3.1 |
API level 12, NDK 6 |
Honeycomb |
3.2.x |
API level 13 |
Ice-creamSandwich(冰激凌三明治) |
4.0.1 - 4.0.2 |
API level 14, NDK 7 |
Ice-creamSandwich |
4.0.3 - 4.0.4 |
API level 15 |
Jelly Bean(果冻豆) |
4.1.x |
API level 16 |
Jelly Bean |
4.2.x |
API level 17 |
Jelly Bean |
4.3.x |
API level 18 |
KitKat |
4.4.x |
API level 19 |
KitKat with wearable extensions |
4.4W |
API level 20 |
Lollipop |
5.0.1 |
API level 21 |
Lollipop |
5.1.1 |
API level 22 |
Marshmallow |
6.0 |
API level 23 |
Nougat |
7.0 |
API level 24 |
Nougat |
7.1.1 |
API level 25 |
“Android”一词来源于法国作家Auguste Villiers de l'Isle-Adam的科幻小说《L'ève future》(未来夏娃),是机器人的意思。因此,最初每个系统版本的命名也都是以全球著名的机器人为参考的,如“AstroBoy”。后来由于版权问题,才改为以食物的方式取名。不过Android的Logo仍然是机器人的形象,如图1-1所示。
▲图1-1 Android官方Logo
和很多著名的科技企业一样,Android的创始人Andy Rubin也是一个技术狂人。在创立Android公司前,他曾完成多项当时被称为“过于超前”的产品研发,并取得了一定的成绩。而创办Android,最初的目的是提供一款开放式的移动平台系统。从2003年10月Andy Rubin开始启动这一系统的研究,Android便正式走上历史的舞台。以下是关于这个系统的一些重要历史事件。
AOSP的宗旨是:
Android Open Source Project is to create a successful real-world product that improves the mobile experience for end users。
因为是开源开放的组织,所以意味着每个人都可以参与进来,并为整个项目的发展添砖加瓦。如果读者有意愿成为其中的一员,可以参考该组织的相关说明(http://source.android.com/source/ index.html)。
至此,Android版本的发布驶入正常轨道,保持着每年多次升级的速度,并加入越来越多的创新功能。
关于Android每个版本的特性及改版后一些重要变化的详细说明,请参阅官方网站(http://source.android.com/source/overview.html)。相信经过仔细比对各个版本的变化,读者会发现Android系统确实一直在秉承其“为终端用户提供更好的移动设备体验”的宗旨。
在这一节中,我们将从观察者的角度来客观评价Android系统某些突出的特点。这些分析中既包含了其值得肯定的诸多优点,也不吝指出其需要持续改善的地方。只有正确全面地了解一个系统所存在的优缺点,才可能在开发的过程中“知己知彼,百战不殆”,真正让系统为我们所用。
相对于iOS和Windows Phone阵营,Android操作系统最大的特点就是开放性;而且有别于个别开源项目的“藏藏掖掖”和“犹抱琵琶半遮面”,Android几乎所有源码都可以免费下载到。无论是公司组织,还是个人开发者,Android对于下载者基本没有限制,也没有下载权限的认证束缚。关于如何下载系统源码的完整描述,请参考下一章节。
当然,这并不代表开发者可以随意使用Android源码。事实上,Android遵循的是Apache开源软件许可证。因此,所有跨越许可证规定范畴的行为都将是被禁止的。希望了解更多Apache协议条款详情的读者,可以自行查阅其官方说明(http://www.apache.org/)。
不过在大部分情况下,Android操作系统仍然被认为是“高度自由”的。这也是越来越多的厂商选择Android作为下一代产品基础平台最主要的原因之一。可以想象,在其他操作系统对其授权的设备动辄收取每台高达几十甚至数百美元专利费的情况下,采用Android开源系统理论上就意味着降低成本。
另外,由于整个操作系统是开源的,从而给诸多产品制造商、软件开发商提供了创新的土壤环境。各厂商可以根据自己的需求,来完成对原生态系统的修改。大多数情况下,这种修改只是基于上层UI交互的“二次包装”,而保留底层系统的大框架。这就好比Google为大家免费提供了已经盖好的办公大楼,虽然是毛坯房,但相较于“万丈高楼平地起”的艰辛,显然已经为我们节约了大量的项目时间;而且我们可以通过“装修”把主要精力倾注在用户看得到的地方,从而更大限度地摆脱“产品同质化”。事实上,目前全球范围内已经有非常多这样的“装修范例”。大到跨国企业、运营商,小到一些初创的设计公司,都选择在Android系统上进行“界面”改造,再冠以新的操作系统名号。其中也不乏一些成功者,根据不同的地域环境、文化差异、使用习惯而定制出新的系统——这些具有“本地化”风格的“办公楼”往往比原生态系统更贴近当地消费者,因此受到热烈追捧。
而这一切,都要归功于Android系统的开放性。
要学习Android系统,就不得不提它的分层架构。早期版本的Android系统框架包括4层,即Linux Kernel、Library and Runtime、Application Framework及Application。后来因为版权相关原因在Kernel层之上新增了一个Hardware Abstraction Layer。我们会在后续小节对各层功能适当地展开讨论。
由此可见,Android系统是一个“杂合体”,即便说其“包罗万象”也一点不为过。它包括了prebuilt、bionic等在内的不少开源项目。管理这些项目显然不是件容易的事,这也是Android系统提供Repo工具,而不是直接使用Git来进行版本管理的原因之一(详见后续章节的描述)。
在面对这么多独立项目的时候,合理的分层架构就显得异常重要——既要保证系统功能的完整性,也要确保各项目的相对独立性。Android系统成功地做到了这一点,整个软件栈条理清晰,分工明确。一方面,它将底层复杂性与移植难度尽可能隐藏起来;另一方面,则提供尽可能方便的上层API接口,为开发者设计实现各种应用程序打下了坚实的基础。
SDK(Software Development Kit)是操作系统与开发者之间的接口,也可以看成一个系统对外的窗口。对于广大的开发者而言,能否借助这个工具在尽可能短的周期内设计出符合需求而又稳定可靠的应用程序,是评判一个操作系统SDK好坏的重要标准之一。
Android系统的大部分应用程序可以基于Java来开发。如果读者曾参与过大型的C/C++研发项目(特别是面向嵌入式系统的),一定不会忘记加班加点解决内存泄露或者空指针异常的那些无眠夜。Java语言对这些软件开发中最令人头疼的问题进行了强有力的改造,不但提供了垃圾回收机制,而且彻底隐藏了指针的使用。即便程序出现了崩溃,通常情况下也可以根据调用栈及各种Log来定位出问题的根源。这无疑为我们快速解决问题、保证程序稳定性提供了很好的平台基础。
Android系统通过总结应用程序的开发规律,提供了Activity、Service、Broadcast Receiver及Content Provider四大组件;并且和MFC类似,设计了人性化的向导模式来帮助开发者便捷地生成工程原型。可以说,这些都为项目开发节约了不少宝贵时间。
另外,Android SDK覆盖面相当广,且仍在持续扩充中。从线程管理、进程间通信等程序设计基础到各种界面组件的应用,只要是开发者能想到的,几乎都可以在SDK中找到现成的调用接口。而对一些界面特效的封装,使得开发者可以高效地设计出各种绚丽的UI效果,进而让Android系统加分良多。
Android版本的更迭是一件让无数人兴奋的事。除了那些令人眼前一亮的新功能外,不断改进的用户交互界面也是吸引用户的一个重要因素。我们可以明显地从新老版本的对比中寻找到Google追求绝佳用户体验的决心。
下面先来看看Gingerbread(2.3版本)的Launcher与Camera界面,如图1-2所示。
▲图1-2 Launcher和Camera界面
然后来看看后续版本上的变化,如图1-3所示。
▲图1-3 后续Android版本变化
可以发现,新的版本相较于以前,不但在UI界面的色彩搭配、布局上有了很大提高,用户交互也更趋于人性化。这种对于用户最直观的“艺术盛宴”展示,促使越来越多的人投身到Android阵营中。
IT业界长期以来都有一个共识——开发一个操作系统(OS)并不是最难的,而基于这个新的操作系统建立完整的生态系统才是最大的难点。用一句老话来说,颇有点“打江山易,守江山难”的味道。
那么,什么是基于OS的生态系统(ecosystem)呢?虽然我们一再听到媒体在大肆宣扬这个词,但目前还没有人能给出权威、严谨的解释来阐述这个特殊“生态系统”的定义与形成。本书下面所提出的释义也未必能完整解读这个词,读者可以带着自己的理解深入思考。
Ecosystem原本是生态学中的一个概念。简单而言,它体现了一定时间和空间内能量的可循环平衡流动,例如,自然界的生态系统组成如下。
因此,它们之间所体现出的能量循环如图1-4所示。
▲图1-4 自然生态系统的能量循环
生产者依靠无机环境制造食物,并实现自养;而消费者则需要消耗其他生物来生存发展;分解者最终将有机物分解为可被生产者重新利用的物质。这样,就构成了整个生态链的循环。
针对Android生态系统,我们可以得到以下的类比,如图1-5所示。
▲图1-5 Android生态系统假想
在Android生态系统中:
当然,这只是本书对Android生态系统的一个初步设想,实际情况一定更复杂。但客观来说,Google一方面既在努力打造“双赢”的市场机制,以吸引更多的开发者介入;另一方面也在提高为消费者服务的能力,如图1-6所示。
▲图1-6 各操作系统平台占有率及开发者赢利对比
虽然最新的调查报告显示,依靠Android软件赢利的开发者寥寥无几,还远远比不上iOS系统赢利模式成熟(见图1-6)。但同时也应该看到,随着整个Android市场占有率的提升以及Google一系列措施的实行,Android生态系统正在逐步完善,前景一片光明。
开源是一把“双刃剑”,它带来的一个突出问题就是阵营混乱。和一些操作系统需要收取高额的加盟费用不同,Android的免费开源大大降低了开发商的准入门槛。因此,出现了“人人都可以做手机”的局面。无论是资本、研发实力雄厚的大型企业,还是初出茅庐没有太多经验的小公司,都在不断进入这个生态圈。这既是Android系统的优势,同时也是隐患。
因为每个厂商都可以根据自己的需求来改造原生态系统,从而难免造成整个Android阵营的分裂;而且,有的开发商也在努力基于Android建立自己的生态系统。这无疑会对Android的整体发展产生一定的影响。如果这些状况在今后一段时间里无法得到解决,那么很可能阻滞Android的进一步发展壮大。
使用过Android相关产品的用户一定有这样的体验,那就是开机慢。一个针对目前市面上主流Android设备的不完全统计显示,Android产品的平均开机时间超过了1分钟,有的甚至达到5分钟以上。对于某些需要快速实时响应的电子设备而言(比如车载电子导航一体机,往往需要在汽车启动后非常短的时间内完成操作系统开机,以显示倒车影像),这样的“龟速”显然是很难让人接受的。
值得欣慰的是,Google也正致力于运行速度的改进。随着新版本的不断发布,我们已经可以明显地感受到Android在这方面所做的努力与成效。
对于Android平台的应用开发者而言,最头疼的恐怕并不是某项创新功能的研发,而是对市面上多种设备的适配。在这方面,iOS的开发人员有绝对的优势,因为他们面对的往往只是一款机器(如iPhone6、iPhone7),而且屏幕尺寸、分辨率等系统属性也都是固定已知的,如图1-7所示。
Android系统由于开源、生产商众多,致使产品形态五花八门。以手机为例,为消费者所熟识的全球大型Android手机开发商就已经超过了20个。而这些厂商还有各自不同的产品型号——这也就意味着屏幕大小、分辨率等各种硬件参数的差异。按照目前行业的普遍经验,开发一款成熟的Android手机应用软件,需要适配200款以上不同厂商的手机,以保证软件发布后不至于出现大规模的用户投诉。如果是面向海外市场,则需要兼容的终端产品数量可能还会更多。
▲图1-7 OpenSignalMaps对市面上主流Android手机品牌的跟踪结果
虽然Android针对这一问题有一定的解决方法(详见本书应用原理篇的相关章节),但以实际开发经验来看,暂时还没有很好地解决难题的方法。
Android系统框架如图1-8所示。
▲图1-8 Android系统5层框架图
注意
引用自2008年的Google I/O大会《Anatomy & Physiology of an Android》主题演讲,作者Patrick Brady。
前面说过,Android系统是由众多子项目组成的。从编程语言的角度来看,这些项目主要是使用Java和C/C++来实现的;从整体系统框架而言,分成内核层、硬件抽象层、系统运行库层、应用程序框架层以及应用程序层。本书的一个主要宗旨是希望读者可以由浅入深地逐步理解Android系统的方方面面。因而在每章节内容的编排上,我们采用了由整体到局部的线索来铺展开——先让读者有一个直观感性的认识,明白“是什么”“有什么用”,然后才剖析“如何做到的”。这样做的一个好处是读者在学习的过程中不容易产生困惑。否则如果直接切入原理,长篇大论地分析代码,仅一大堆函数调用就可能让人失去学习的方向。这样的结果往往是读者花了非常多的时间来理清函数关系,但始终不明白代码编写者的意图,甚至连这些函数想实现什么功能都无法完全理解。
本书希望可以从更高的层次,即抽象的、反映代码设计思想和设计者初衷的角度去理解系统。而在思考的过程中,大部分情况下我们都将从读者容易理解的基础开始讲起。就好比画一张素描,先给出一张白纸,勾勒出整体的框架,然后针对重点部位细细加工,最后才能还原出完整的画面。另外,本书在对系统原理本身进行讲解的同时,也最大程度地结合工程项目中可能遇到的问题,理论联系实际地进行解析。希望这样的方式既能让读者真正学习到Android系统的设计思想,也能学有所用,增加一些实际的项目开发经验和技巧。
接下来的内容将对Android系统的5层框架做一个简单描述。
Android的底层是基于Linux操作系统的。从严格意义上来讲,它属于Linux操作系统的一个变种。Android选择在Linux内核的基础上来搭建自己的运行平台有几个好处。
首先,避开了与硬件直接打交道。Linux经过多年的发展,这方面工作正是它的强项,其表现可以说相当优秀。更为难能可贵的是,Linux本身也是开源的,所以Android系统没有必要花费额外的时间去做重复工作。
其次,基于Linux系统的驱动开发可扩展性很强。这对于嵌入式系统而言非常重要,因为每款产品在硬件上或多或少都会有差异,如果驱动开发不能做到高度可扩展和易用性,那么Android系统的移植工作将是无止境的噩梦。
值得一提的是,Android的工程项目中并没有包括内核源码——内核源码的具体下载方式可以参见下一章节。
大家可能都有这样的疑问,既然Linux内核是专职与硬件打交道的,为什么又杀出个“程咬金”硬件抽象层(HAL)呢?没错,这个“人物”一开始并没有出现在Android的“剧本”中,其出场是有一定历史原因的。
HAL的第一次亮相要追溯到2008年的Google I/O大会上。当时Google员工Patrick Brady发表了一篇名为《Anatomy & Physiology of an Android》的演讲,并在其中提出了带HAL的Android新架构。根据这份文档的描述,HAL是:
(1)User space C/C++ library layer;
(2)Defines the interface that Android requires hardware “drivers” to implement;
(3)Separates the Android platform logic from the hardware interface。
也就是说,它希望通过定义硬件“驱动”的接口来进一步降低Android系统与硬件的耦合度。另外,由于Linux遵循的是GPL协议(注意,Android开源项目基于Apache协议),意味着其下的所有驱动都应该是开源的——这点对于部分厂商来说是无法接受的。因而,Android提供了一种“打擦边球”的做法来规避这类问题。我们会在后续章节中继续讲解关于HAL的更多知识。
这一层中包含了支撑整个系统正常运行的基础库。由于这些库多数由C/C++实现,因此也被一些开发人员称为“C库层”,以区别于应用程序框架层。Android中很多系统运行库实际上都是成熟的开源项目,如WebKit、OpenGL、SQLite等。我们并不要求读者去理解所有库的内部原理,这样做不现实。重点在于Android系统是如何有机地与这些库建立联系,从而保证整个设备的稳定运作的。
与系统运行库被称为“C库层”相对应,应用程序框架层往往被冠以“Java库”的称号。这是因为框架层所提供的组件一般都用Java语言编写而成,它们一方面为上层应用程序提供了API接口;另一方面也囊括了不少系统级服务进程的实现,是与Android应用程序开发者关系最直接的一层。
目前Android的软件开发分为两个方向,即系统移植与应用程序的开发。
对于一名出色的应用程序员而言,不仅要了解该使用哪些系统API接口去完成一个功能,还要尽可能了解这些接口及其下的系统底层框架是如何实现的。虽然理解系统运行原理对于应用开发者来说并不是必需的,但在很多情况下却可以极大地提高程序员分析问题的能力,也可在产品性能优化方面产生积极的作用。
在分析Android源码前,首先要学会如何下载和编译系统。本章将向读者完整地呈现Android源码的下载流程、常见问题以及处理方法,并从开发者的角度来理解如何正确地编译出Android系统(包括原生态系统和定制设备)。
后面,我们将在此基础上深入到编译脚本的分析中,以“庖丁解牛”的方式来还原一个庞大而严谨的Android编译系统。
Git是一种分布式的版本管理系统,最初被设计用于Linux内核的版本控制。本书工具篇中对Git的使用方法、原理框架有比较详细的剖析,建议读者先到相关章节阅读了解。
Git的功能非常强大,速度也很快,是当前很多开源项目的首选工具。不过Git也存在一定的缺点,如相对于图形界面化的工具没那么容易上手、需要对内部原理有一定的了解才能很好地运用、不支持断点续传等。
为此,Google提供了一个专门用于下载Android系统源码的Python脚本,即Repo。
在Repo环境下,版本修改与提交流程是:
由此可见,Repo与我们在工具篇中讨论的Git流程有些许不同,差异主要体现在与远程服务仓库的交互上;而本地的开发仍然是以原生的Git命令为主。下面我们讲解Repo的一些常用命令,读者也可以拿它和Git进行仔细比较。
同步操作可以让本地代码与远程仓库保持一致。它有两种形式。
如果是同步当前所有的项目:
$ repo sync
或者也可以指定需要同步的某个项目:
$ repo sync [PROJECT1] [PROJECT2]…
创建一个分支所需的命令:
$ repo start <BRANCH_NAME>
也可以查看当前有多少分支:
$ repo branches
或者:
$ git branch
以及切换到指定分支:
$ git checkout <BRANCH_NAME>
查询当前状态:
$ repo status
查询未提交的修改:
$ repo diff
暂存文件:
$git add
提交文件:
$git commit
如果是提交修改到服务器上,首先需要同步一下:
$repo sync
然后执行上传指令:
$repo upload
了解了Repo的一些常规操作后,这一小节接着分析Android源码下载的全过程。这既是剖析Android系统原理的前提,也是让很多新手感到困惑的地方——源码下载可以作为初学者了解Android系统的“Hello World”。
值得一提的是,Android官方建议我们务必确保编译系统环境符合以下几点要求:
在虚拟机上或是其他不支持的系统(例如Windows)上编译Android系统也是可能的,事实上Google鼓励大家去尝试不同的操作系统平台。不过Google内部针对Android系统的编译和测试工作大多是在Ubuntu LTS(14.04)上进行的。因而建议开发人员也都选择同样的操作系统版本来开展工作,经验告诉我们这样可以少走很多弯路。
如果是在虚拟机上运行的Linux系统,那么理论上至少需要16GB的RAM/Swap才有可能完成整个Android系统的编译。
要特别提醒大家的是,以下所有步骤都是在Ubuntu操作系统中完成的(“#”号后面表示注释内容)。
$ cd ~ #进入home目录
$ mkdir bin #创建bin目录用于存放Repo脚本
$ PATH=~/bin:$PATH #将bin目录加入系统路径中
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo #curl
#是一个基于命令行的文件传输工具,它支持非常多的协议。这里我们利用curl来将repo保存到相应目录下
$ chmod a+x ~/bin/repo
注:网上有很多开发者(中国大陆地区)反映上面的地址经常无法成功访问。如果读者也有类似困扰,可以试试下面这个:
$curl http://android.googlesource.com/repo > ~/bin/repo
另外,国内不少组织(特别是教育机构)也对Android做了镜像,如清华大学提供的开源项目(TUNA)的mirror地址如下:
https://aosp.tuna.tsinghua.edu.cn/
下面是TUNA官方对Android代码库的使用帮助节选:
Android镜像使用帮助
参考Google教程https://source.android.com/source/downloading.html,将https://android.google source.com/全部使用git://aosp.tuna.tsinghua.edu.cn/android/代替即可。
本站资源有限,每个IP限制并发数为4,请勿使用repo sync-j8这样的方式同步。
替换已有的AOSP源代码的remote。
如果你之前已经通过某种途径获得了AOSP的源码(或者你只是init这一步完成后),你希望以后通过TUNA同步AOSP部分的代码,只需要将.repo/manifest.xml把其中的AOSP这个remote的fetch从https://android. googlesource.com改为git://aosp.tuna.tsinghua.edu.cn/android/。
<manifest>
<remote name="aosp"
- fetch="https://android.googlesource.com"
+ fetch="git://aosp.tuna.tsinghua.edu.cn/android/"
review="android-review.googlesource.com" />
<remote name="github"
这个方法也可以用来在同步Cyanogenmod代码的时候从TUNA同步部分代码
下载repo后,最好进行一下校验,各版本的校验码如下所示:
对于 版本 1.17, SHA-1 checksum是:ddd79b6d5a7807e911b524cb223bc3544b661c28
对于 版本 1.19, SHA-1 checksum是:92cbad8c880f697b58ed83e348d06619f8098e6c
对于 版本 1.20, SHA-1 checksum 是:e197cb48ff4ddda4d11f23940d316e323b29671c
对于 版本 1.21, SHA-1 checksum 是:b8bd1804f432ecf1bab730949c82b93b0fc5fede
在开始下载源码前,需要对Repo进行必要的配置。
如下所示:
$ mkdir source #用于存放整个项目源码
$ cd source
$ repo init -u https://android.googlesource.com/platform/manifest
############以下为注释部分########
init命令用于初始化repo并得到近期的版本更新信息。如果你想获取某个非master分支的代码,需要在命令最后加上-b选项。如:
$ repo init -u https://android.googlesource.com/platform/manifest -b android-4.0.1_r1
完成配置后,repo会有如下提示:
repo initialized in /home/android
这时在你的机器home目录下会有一个.repo目录,用于记录manifest等信息##########
######
完成初始化动作后,就可以开始下载源码了。根据上一步的配置,下载到的可能是最新版本或者某分支版本的系统源码。
$ repo sync
由于整个Android源码项目非常大,再加上网络等不确定因素,运气好的话可能1~2个小时就能品尝到“Android盛宴”;运气不好的话,估计一个礼拜也未必能完成这一步——如果下载一直失败的话,读者也可以尝试到网上搜索别人已经下载完成的源码包,因为通常在新版本发布后的第一时间就有热心人把它上传到网上了。
可以看到在Repo的帮助下,整个下载过程还是相当简单直观的。
提示:如果你在下载过程中出现暂时性的问题(如下载意外中断),可以多试几次。如果一直存在问题,则很可能是代理、网关等原因造成的。更多常见问题的描述与解决方法,可以参见下面这个网址。
http://source.android.com/source/known-issues.html
典型的repo下载界面如图2-1所示。
▲图2-1 原生Android工程的典型下载界面
Android系统本身是由非常多的子项目组成的,这也是为什么我们需要repo来统一管理AOSP源码的一个重要原因,如图2-2所示(部分)。
▲图2-2 子项目
另外,不同子项目之间的branches和tags的区别如图2-3所示。
▲图2-3 Android各子项目的分支和标签
(左:frameworks/base,中:frameworks/native,右:/platform/libcore)
当我们使用repo init命令初始化AOSP工程时,会在当前目录下生成一个repo文件夹,如图2-4所示。
▲图2-4 repo文件
其中manifests本身也是一个Git项目,它提供的唯一文件名为default.xml,用于管理AOSP中的所有子项目(每个子项目都由一个project标签表示):
另外,default.xml中记录了我们在初始化时通过-b选项指定的分支版本,例如“android-n-preview-2”:
这样当执行repo sync命令时,系统就可以根据我们的要求去获取正确的源码版本了。
友情提示:经常有读者询问阅读Android源码可以使用哪些工具。除了著名的Source Insight外,另外还有一个名为SlickEdit的IDE也是相当不错的(支持Windows、Linux和Mac),建议大家可以对比选择最适合自己的工具。
任何一个项目在编译前,都首先需要搭建一个完整的编译环境。Android系统通常是运行于类似Arm这样的嵌入式平台上,所以很可能涉及交叉编译。
什么是交叉编译呢?
简单来说,如果目标平台没有办法安装编译器,或者由于资源有限等无法完成正常的编译过程,那就需要另一个平台来辅助生成可执行文件。如很多情况下我们是在PC平台上进行Android系统的研发工作,这时就需要通过交叉编译器来生成可运行于Arm平台上的系统包。需要特别提出的是,“平台”这个概念是指硬件平台和操作系统环境的综合。
交叉编译主要包含以下几个对象。
宿主机(Host):指的是我们开发和编译代码所在的平台。目前不少公司的开发平台都是基于X86架构的PC,操作系统环境以Windows和Linux为主。
目标机(Target):相对于宿主机的就是目标机。这是编译生成的系统包的目标平台。
交叉编译器(Cross Compiler):本身运行于宿主机上,用于产生目标机可执行文件的编译器。
针对具体的项目需求,可以自行配置不同的交叉编译器。不过我们建议开发者尽可能直接采用国际权威组织推荐的经典交叉编译器。因为它们在release之前就已经在多个项目上测试过,可以为接下来的产品开发节约宝贵的时间。表2-1所示给出了一些常见的交叉编译器及它们的应用环境。
表2-1 常用交叉编译器及应用环境
交叉编译器 |
宿 主 机 |
目 标 机 |
---|---|---|
armcc |
X86PC(windows),ADS开发环境 |
Arm |
arm-elf-gcc |
X86PC(windows),Cygwin开发环境 |
Arm |
arm-linux-gcc |
X86PC(Linux) |
Arm |
本书所采用的宿主机是X86PC(Linux),通过表2-1可知在编译过程中需要用到arm-linux-gcc交叉编译器(注:Android系统工程中自带了交叉编译工具,只要在编译时做好相应的配置即可)。
接下来我们分步骤来搭建完整的编译环境,并完成必要的配置。所选取的宿主机操作系统是Ubuntu的14.04版本LTS(这也是Android官方推荐的)。为了不至于在编译过程中出现各种意想不到的问题,建议大家也采用同样的操作系统环境来执行编译过程。
Step1. 通用工具的安装
表2-2给出了所有需要安装的通用工具及它们的下载地址。
表2-2 通用编译工具的安装及下载地址
通 用 工 具 | 安装地址、指南 | |
Python 2.X | http://www.python.org/download/ | |
GNU Make 3.81 -- 3.82 | http://ftp.gnu.org/gnu/make/ | |
JDK | Java 87 针对Kitkat以上版本 | 最新的Android工程已经改用OpenJDK,并要求为Java 87及以上版本。这点大家应该特别注意,否则可能在编译过程中遇到各种问题。具体安装方式见下面的描述 |
JDK 6 针对Gingerbread到Kitkat之间的版本 | http://java.sun.com/javase/downloads/ | |
JDK 5 针对Cupcake到Froyo之间版本 | ||
Git 1.7以上版本 | http://git-scm.com/download |
对于开发人员来说,他们习惯于通过以下方法安装JDK(如果处于Ubuntu系统下):
Java 6:
$ sudo add-apt-repository "deb http://archive.canonical.com/ lucid partner"
$ sudo apt-get update
$ sudo apt-get install sun-java6-jdk
Java 5:
$ sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu hardy main multiverse"
$sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu hardy-updates main
multiverse"
$ sudo apt-get update
$ sudo apt-get install sun-java5-jdk
但是随着Java的版本变迁及Sun(已被Oracle收购)公司态度的转变,目前获取Java的方式也发生了很大变化。基于版权方面的考虑(大家应该已经听说了Oracle和Google之间的官司恩怨),Android系统已经将Java环境切换到了OpenJDK,安装步骤如下所示:
$ sudo apt-get update
$ sudo apt-get install openjdk-8-jdk
首先通过上述命令install OpenJDK 8,成功后再进行如下配置:
$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac
如果出现Java版本错误的问题,make系统会有如下提示:
************************************************************
You are attempting to build with the incorrect version
of java.
Your version is: WRONG_VERSION.
The correct version is: RIGHT_VERSION.
Please follow the machine setup instructions at
https://source.android.com/source/download.html
************************************************************
Step2. Ubuntu下特定工具的安装
注意,这一步中描述的安装过程是针对Ubuntu而言的。如果你是在其他操作系统下执行的编译,请参阅官方文档进行正确配置;如果你是在虚拟机上运行的Ubuntu系统,那么请至少保留16GB的RAM/SWAP和100GB以上的磁盘空间,这是完成编译的基本要求。
$ sudo apt-get install bison g++-multilib git gperf libxml2-utils make zlib1g-
dev:i386 zip
所需的命令如下:
$ sudo apt-get install git gnupg flex bison gperf build-essential \
zip curl libc6-dev libncurses5-dev:i386 x11proto-core-dev \
libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-glx:i386 \
libgl1-mesa-dev g++-multilib mingw32 tofrodos \
python-markdown libxml2-utils xsltproc zlib1g-dev:i386
$ sudo ln -s /usr/lib/i386-linux-gnu/mesa/libGL.so.1 /usr/lib/i386-linux-gnu/libGL.so
需要安装的程序比较多,不过我们还是可以通过apt-get来轻松完成。
具体命令如下:
$ sudo apt-get install git-core gnupg flex bison gperf build-essential \
zip curl zlib1g-dev libc6-dev lib32ncurses5-dev ia32-libs \
x11proto-core-dev libx11-dev lib32readline5-dev lib32z-dev \
libgl1-mesa-dev g++-multilib mingw32 tofrodos python-markdown \
libxml2-utils xsltproc
注意,如果以上命令中存在某些包找不到的情况,可以试试以下命令:
$ sudo apt-get install git-core gnupg flex bison gperf libsdl-dev libesd0-dev libwxgtk2.6-dev build-essential zip curl libncurses5-dev zlib1g-dev openjdk-6-jdk ant gcc-multilib g++-multilib
如果你的操作系统刚好是Ubuntu 10.10,那么还需要:
$ sudo ln -s /usr/lib32/mesa/libGL.so.1 /usr/lib32/mesa/libGL.so
如果你的操作系统刚好是Ubuntu 11.10,那么还需要:
$ sudo apt-get install libx11-dev:i386
Step3. 设立ccache(可选)
如果你经常执行“make clean”,或者需要经常编译不同的产品类别,那么ccache还是有用的。它可以作为编译时的缓冲,从而加快重新编译的速度。
首先,需要在.bashrc中加入如下命令。
export USE_CCACHE=1
如果你的home目录是非本地的文件系统(如NFS),那么需要特别指定(默认情况下它存放于~/.ccache):
export CCACHE_DIR=<path-to-your-cache-directory>
在源码下载完成后,必须在源码中找到如下路径并执行命令:
prebuilt/linux-x86/ccache/ccache -M 50G
#推荐的值为50-100GB,你可以根据实际情况进行设置
Step4. 配置USB访问权限
USB的访问权限在我们对实际设备进行操作时是必不可少的(如下载系统程序包到设备上)。在Ubuntu系统中,这一权限通常需要特别的配置才能获得。
可以通过修改/etc/udev/rules.d/51-android.rules来达到目的。
例如,在这个文件中加入以下命令内容:
# adb protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e12", MODE="0600", OWNER
="<username>"
# fastboot protocol on passion (Nexus One)
SUBSYSTEM=="usb", ATTR{idVendor}=="0bb4", ATTR{idProduct}=="0fff", MODE="0600", OWNER
="<username>"
# adb protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e22", MODE="0600", OWNER
="<username>"
# fastboot protocol on crespo/crespo4g (Nexus S)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e20", MODE="0600", OWNER
="<username>"
# adb protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="22b8", ATTR{idProduct}=="70a9", MODE="0600", OWNER
="<username>"
# fastboot protocol on stingray/wingray (Xoom)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="708c", MODE="0600", OWNER
="<username>"
# adb protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="04e8", ATTR{idProduct}=="6860", MODE="0600", OWNER
="<username>"
# fastboot protocol on maguro/toro (Galaxy Nexus)
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e30", MODE="0600", OWNER
="<username>"
# adb protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d101", MODE="0600", OWNER
="<username>"
# fastboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d022", MODE="0600", OWNER
="<username>"
# usbboot protocol on panda (PandaBoard)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d00f", MODE="0600", OWNER
="<username>"
# usbboot protocol on panda (PandaBoard ES)
SUBSYSTEM=="usb", ATTR{idVendor}=="0451", ATTR{idProduct}=="d010", MODE="0600", OWNER
="<username>"
如果严格按照上述4个步骤来执行,并且没有任何错误——那么恭喜你,一个完整的Android编译环境已经搭建完成了。
上一小节我们建立了完整的编译环境,可谓“万事俱备,只欠东风”,现在就可以执行真正的编译操作了。
下面内容仍然采用分步的形式进行讲解。
Step1. 执行envsetup脚本
脚本文件envsetup.sh记录着编译过程中所需的各种函数实现,如lunch、m、mm等。你可以根据需求进行一定的修改,然后执行以下命令:
$ source ./build/envsetup.sh
也可以用点号代替source:
$ . ./build/envsetup.sh
Step2. 选择编译目标
编译目标由两部分组成,即BUILD和BUILDTYPE。表2-3和表2-4给出了详细的解释。
表2-3 BUILD参数详解
BUILD |
设 备 |
备 注 |
---|---|---|
Full |
模拟器 |
全编译,即包括所有的语言、应用程序、输入法等 |
full_maguro |
maguro |
全编译,并且运行于 Galaxy Nexus GSM/HSPA+ ("maguro") |
full_panda |
panda |
全编译,并且运行于 PandaBoard ("panda") |
可见BUILD可用于描述不同的目标设备。
表2-4 BUILDTYPE参数详解
BUILDTYPE |
备 注 |
---|---|
User |
编译出的系统有一定的权限限制,通常用来发布最终的上市版本 |
userdebug |
编译出的系统拥有root权限,通常用于调试目的 |
Eng |
即engineering版本 |
可见BUILDTYPE可用于描述各种不同的编译场景。
选择不同的编译目标,可以使用以下命令:
$ lunch BUILD-BUILDTYPE
如我们执行命令“lunch full-eng”,就相当于编译生成一个用于工程开发目的,且运行于模拟器的系统。
如果不知道有哪些产品类型可选,也可以只敲入“lunch”命令,这时会有一个列表显示出当前工程中已经配置过的所有产品类型(后续小节会讲解如何添加一款新产品);然后可以根据提示进行选择,如图2-5所示。
▲图2-5 使用“lunch”来显示所有产品
Step3. 执行编译命令
最直接的就是输入如下命令:
$ make
对于2.3以下的版本,整个编译过程在一台普通计算机上需要3小时以上的时间。而对于JellyBean以上的项目,很可能会花费5小时以上的时间(这取决于你的宿主机配置)。
如果希望充分利用CPU资源,也可以使用make选项“-jN”。N的值取决于开发机器的CPU数、每颗CPU的核心数以及每个核心的线程数。
例如,你可以使用以下命令来加快编译速度:
$ make –j4
有个小技巧可以为这次编译轻松地打上Build Number标签,而不需要特别更改脚本文件,即在make之前输入如下命令:
$ export BUILD_NUMBER=${USER}-'date +%Y%m%d-%H%M%S'
在定义BUILD_NUMBER变量值时要特别注意容易引起错误的符号,如“$”“&”“:”“/”“\”“<”“>”等。
这样我们就成功编译出Android原生态系统了——当然,上面的“make”指令只是选择默认的产品进行编译。假如你希望针对某个特定的产品来执行,还需要先通过上一小节中的“lunch”进行相应的选择。
接下来看看如何编译出SDK。这是很多开发者,特别是应用程序研发人员所关心的。因为很多时候通过SDK所带的模拟器来调试APK应用,比在真机上操作要来得高效且便捷;而且模拟器可以配置出各种不同的屏幕参数,用以验证应用程序的“适配”能力。
SDK是运行于Host机之上的,因而编译过程根据宿主操作系统的不同会有所区别。详细步骤如下:
Mac OS和Linux
(1)下载源码,和前面已经讲过的源码下载过程没有任何区别。
(2)执行envsetup.sh。
(3)选择SDK对应的产品。
$ lunch sdk-eng
提示:如果通过“lunch”没有出现“sdk”这个种类的产品也没有关系,可以直接输入上面的命令。
(4)最后,使用以下命令进行SDK编译:
$ make sdk
Windows
运行于Windows环境下的SDK编译需要基于上面Linux的编译结果(注意只能是Linux环境下生成的结果,而不支持MacOS)。
(1)执行Linux下SDK编译的所有步骤,生成Linux版的SDK。
(2)安装额外的支持包。
$ sudo apt-get install mingw32 tofrodos
(3)再次执行编译命令,即:
$ . ./build/envsetup.sh
$ lunch sdk-eng
$ make win_sdk
这样我们就完成Windows版本SDK的编译了。
当然上面编译SDK的过程也同样可以利用多核心CPU的优势。例如:
$ make -j4 sdk
面向Host和Target的编译结果都存放在源码工程out目录下,分为两个子目录。
host:SDK生成的文件存放在这里。例如:
MacOS
out/host/darwin-x86/sdk/android-sdk_eng.<build-id>_mac-x86.zip
Windows
out/host/windows/sdk/android-sdk_eng.${USER}_windows/
target:通过make命令生成的文件存放在这里。
另外,启动一个模拟器可以使用以下命令。
$ emulator [OPTIONS]
模拟器提供的启动选项非常丰富,读者可以参见本书工具篇中的详细描述。
上一小节我们学习了原生态Android系统的编译步骤,为大家进一步理解定制设备的编译流程打下了基础。Android系统发展到今天,已经在多个产品领域得到了广泛的应用。相信有一个问题是很多人都想了解的,那就是如何在原生态Android系统中添加自己的定制产品。
仔细观察整个Android源码项目可以发现,它的根目录下有一个device文件夹,其中又包含了诸如samsung、moto、google等厂商名录,如图2-6所示。
▲图2-6 device文件夹下的厂商目录
在Android编译系统中新增一款设备的过程如下。
Step 1. 和图2-6所列的各厂商一样,我们也最好先在device目录下添加一个以公司命名的文件夹。当然,Android系统本身并没有强制这样做(后面会看到vendor目录也是可以的),只不过规范的做法有利于项目的统一管理。
然后在这个公司名目录下为各产品分别建立对应的子文件夹。以samsung为例,其文件夹中包含的产品如图2-7所示。
▲图2-7 一个厂商通常有多种产品
完成产品目录的添加后,和此项目相关的所有特定文件都应该优先放置到这里。一般的组织结构如图2-8所示。
▲图2-8 device目录的组织架构
由图2-8最后一行可以看出,一款新产品的编译需要多个配置文件(sh、mk等)的支持。我们按照这些文件所处的层级进行一个系统的分类,如表2-5所示。
表2-5 定制新设备所需的配置文件分类
层 级 |
作 用 |
---|---|
芯片架构层(Architecture) |
产品所采用的硬件架构,如ARM、X86等 |
核心板层(Board) |
硬件电路的核心板层配置 |
设备层(Device) |
外围设备的配置,如有没有键盘 |
产品层(Product) |
最终生成的系统需要包含的软件模块和配置,如是否有摄像头应用程序、默认的国家或地区语言等 |
也就是说,一款产品由底层往上的构建顺序是:芯片架构→核心板→设备→产品。这样讲可能有点抽象,给大家举个具体的例子。我们知道,当前嵌入式领域市场占有率最高的当属ARM系列芯片。但是首先,ARM公司本身并不生产具体的芯片,而只授权其他合作伙伴来生产和销售半导体芯片。ARM架构就是属于最底层的硬件体系,需要在编译时配置。其次,很多芯片设计商(如三星)在获得授权后,可以在ARM架构的基础上设计出具体的核心板,如S5PV210。接下来,三星会将其产品进一步销售给有需要的下一级厂商,如某手机生产商。此时就要考虑整个设备的硬件配置了,如这款手机是否要带有按键、触摸屏等。最后,在确认了以上3个层次的硬件设计后,我们还可以指定产品的一些具体属性,如默认的国家或地区语言、是否带有某些应用程序等。
后续的步骤中我们将分别讲解与这几个层次相关的一些重要的脚本文件。
Step 2. vendorsetup.sh
虽然我们已经为新产品创建了目录,但Android系统并不知道它的存在——所以需要主动告知Android系统新增了一个“家庭成员”。以三星toro为例,为了让它能被正确添加到编译系统中,首先就要在其目录下新建一个vendorsetup.sh脚本。这个脚本通常只需要一个语句。具体范例如下:
add_lunch_combo full_toro-userdebug
大家应该还记得前一小节编译原生态系统的第一步是执行envsetup.sh,函数add_lunch_combo就是在这个文件中定义的。此函数的作用是将其参数所描述的产品(如full_toro-userdebug)添加到系统相关变量中——后续lunch提供的选单即基于这些变量产生的。
那么,vendorsetup.sh在什么时候会被调用呢?
答案也是envsetup.sh。这个脚本的大部分内容是对各种函数进行定义与实现,末尾则会通过一个for循环来扫描工程中所有可用的vendorsetup.sh,并执行它们。具体源码如下:
# Execute the contents of any vendorsetup.sh files we can find.
for f in 'test -d device && find device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null'
\
'test -d vendor && find vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null'
do
echo "including $f"
. $f
Done
unset f
可见,默认情况下编译系统会扫描如下路径来查找vendorsetup.sh:
/vendor/
/device/
注:vendor这个目录在4.3版本的Android工程中已经不存在了,建议开发者将产品目录统一放在device中。
打一个比方,上述步骤有点类似于超市的工作流程:工作人员(编译系统)首先要扫描仓库(vendor和device目录),统计出有哪些商品(由vendorsetup.sh负责记录),并通过一定的方式(add_lunch_combo@envsetup.sh)将物品上架,然后消费者才能在货架上挑选(lunch)自己想要的商品。
Step 3. 添加AndroidProducts.mk。消费者在货架上选择(lunch)了某样“商品”后,工作人员的后续操作(如结账、售后等)就完全基于这个特定商品来展开。编译系统会先在商品所在目录下寻找AndroidProducts.mk文件,这里记录着针对该款商品的一些具体属性。不过,通常我们只在这个文件中做一个“转向”。如:
/*device/samsung/toro/AndroidProducts.mk*/
PRODUCT_MAKEFILES := \
$(LOCAL_DIR)/aosp_toro.mk \
$(LOCAL_DIR)/full_toro.mk
因为AndroidProducts.mk对于每款产品都是通用的,不利于维护管理,所以可另外新增一个或者多个以该产品命名的makefile(如full_toro.mk和aosp_toro.mk),再让前者通过PRODUCT_MAKEFILES“指向”它们。
Step4. 实现上一步所提到的某产品专用的makefile文件(如full_toro.mk和aosp_toro.mk)。可以充分利用编译系统已有的全局变量或者函数来完成任何需要的功能。例如,指定编译结束后需要复制到设备系统中的各种文件、设置系统属性(系统属性最终会写入设备/system目录下的build.prop文件中)等。以full_toro.mk为例:
/*device/samsung/toro/full_toro.mk*/
#将apns等配置文件复制到设备的指定目录中
PRODUCT_COPY_FILES += \
device/samsung/toro/bcmdhd.cal:system/etc/wifi/bcmdhd.cal \
device/sample/etc/apns-conf_verizon.xml:system/etc/apns-conf.xml \
…
# 继承下面两个mk文件
$(call inherit-product, $(SRC_TARGET_DIR)/product/aosp_base_telephony.mk)
$(call inherit-product, device/samsung/toro/device_vzw.mk)
# 下面重载编译系统中已经定义的变量
PRODUCT_NAME :=full_toro #产品名称
PRODUCT_DEVICE := toro #设备名称
PRODUCT_BRAND := Android #品牌名称
…
这部分的变量基本上以“PRODUCT_”开头,我们在表2-6中对其中常用的一些变量做统一讲解。
表2-6 PRODUCT相关变量
变 量 |
描 述 |
---|---|
PRODUCT_NAME |
产品名称,最终会显示在系统设置中的“关于设备”选项卡中 |
PRODUCT_DEVICE |
设备名称 |
PRODUCT_BRAND |
产品所属品牌 |
PRODUCT_MANUFACTURER |
产品生产商 |
PRODUCT_MODEL |
产品型号 |
PRODUCT_PACKAGES |
系统需要预装的一系列程序,如APKs |
PRODUCT_LOCALES |
所支持的国家语言。格式如下: |
PRODUCT_POLICY |
本产品遵循的“策略”,如: |
PRODUCT_TAGS |
一系列以空格分隔的产品标签描述 |
PRODUCT_PROPERTY_OVERRIDES |
用于重载系统属性。 |
Step 5. 添加BoardConfig.mk文件。这个文件用于填写目标架构、硬件设备属性、编译器的条件标志、分区布局、boot地址、ramdisk大小等一系列参数(参见下一小节对系统映像文件的讲解)。下面是一个范例(因为toro中的BoardConfig主要引用了tuna的BoardConfig实现,所以我们直接讲解后者的实现):
#/*device/samsung/tuna/BoardConfig.mk*/
TARGET_CPU_ABI := armeabi-v7a ## eabi即Embedded application binary interface
TARGET_CPU_ABI2 := armeabi
…
TARGET_NO_BOOTLOADER := true ##不编译bootloader
…
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 685768704#system.img分区大小
BOARD_USERDATAIMAGE_PARTITION_SIZE := 14539537408#userdata.img的分区大小
BOARD_FLASH_BLOCK_SIZE := 4096 #flash块大小
…
BOARD_WLAN_DEVICE := bcmdhd #wifi设备
可以看到,这个makefile文件中涉及的变量大部分以“TARGET_”和“BOARD_”开头,且数量众多。相信对于第一次编写BoardConfig.mk的开发者来说,这是一个不小的挑战。那么,有没有一些小技巧来加速学习呢?
答案是肯定的。
各大厂商在自己产品目录下存放的BoardConfig.mk样本就是我们学习的绝佳材料。通过比较可发现,这些文件大部分都是雷同的。所以我们完全可以先从中复制一份(最好选择架构、主芯片与自己项目相当的),然后根据产品的具体需求进行修改。
Step 6. 添加Android.mk。这是Android系统下编译某个模块的标准makefile。有些读者可能分不清楚这个文件与前面几个步骤中的makefile有何区别。我们举例说明,如果Step1-Step5中的文件用于决定一个产品的属性,那么Android.mk就是生产这个“产品”某个“零件”的“生产工序”。——要特别注意,只是某个“零件”而已。整个产品是需要由很多Android.mk生产出的“零件”组合而成的。
Step7. 完成前面6个步骤后,我们就成功地将一款新设备定制到编译系统中了。接下来的编译流程和原生态系统是完全一致的,这里不再赘述。
值得一提的是,/system/build.prop这个文件的生成过程也是由编译系统控制的。具体处理过程在/build/core/Makefile中,它主要由以下几个部分组成:
这个脚本用于向build.prop中输出各种<key> <value>组合,实现方式也很简单。下面是其中的两行节选:
echo "ro.build.id=$BUILD_ID"
echo "ro.build.display.id=$BUILD_DISPLAY_ID"
清理工作,将黑名单中的项目从最终的build.prop中移除。
开发人员在定制一款新设备时,可以根据实际情况将自己的配置信息添加到上述几个组成部分中,以保证设备的正常运行。
不同产品的硬件配置往往是有差异的。比如某款手机配备了蓝牙芯片,而另一款则没有;即便是都内置了蓝牙模块的两款手机,它们的生产商和型号也很可能不一样——这就不可避免地要涉及内核驱动的移植。前面我们分析的编译流程只针对Android系统本身,而Linux内核和Android的编译是独立的。因此对于设备开发商来说,还需要下载、修改和编译内核版本。
接下来以Android官方提供的例子来讲解如何下载合适的内核版本。
这个范例基于Google的Panda设备,具体步骤如下。
Step1. 首先通过以下命令来获取到git log:
$ git clone https://android.googlesource.com/device/ti/panda
$ cd panda
$ git log --max-count=1 kernel
这样就得到了panda kernel的提交值,在后续步骤中会用到。
Step2. Google针对Android系统提供了以下可用的内核版本:
$ git clone https://android.googlesource.com/kernel/common.git
$ git clone https://android.googlesource.com/kernel/exynos.git
$ git clone https://android.googlesource.com/kernel/goldfish.git
$ git clone https://android.googlesource.com/kernel/msm.git
$ git clone https://android.googlesource.com/kernel/omap.git
$ git clone https://android.googlesource.com/kernel/samsung.git
$ git clone https://android.googlesource.com/kernel/tegra.git
上述命令的每一行都代表了一个可用的内核版本。
那么,它们之间有何区别呢?
由此可见,与Panda设备相匹配的是omap.git这个版本的内核。
Step3. 除了Linux内核,我们还需要下载prebuilt。具体命令如下:
$ git clone https://android.googlesource.com/platform/prebuilt
$ export PATH=$(pwd)/prebuilt/linux-x86/toolchain/arm-eabi-4.4.3/bin:$PATH
Step4. 完成以上步骤后,就可以进行Panda内核的编译了:
$ export ARCH=arm
$ export SUBARCH=arm
$ export CROSS_COMPILE=arm-eabi-
$ cd omap
$ git checkout <第一步获取到的值>
$ make panda_defconfig
$ make
整个内核的编译相对简单,读者可以自行尝试。
将编译生成的可执行文件包通过各种方式写入硬件设备的过程称为烧录(flash)。烧录的方式有很多,各厂商可以根据实际的需求自行选择。常见的有以下几种。
(1)SD卡工厂烧录方式
当前市面上的CPU主芯片通常会提供多种跳线方式,来支持嵌入式设备从不同的存储介质(如Flash、SD Card等)中加载引导程序并启动系统。这样的设计显然会给设备开发商带来更多的便利。研发人员只需要将烧录文件按一定规则先写入SD卡,然后将设备配置为SD卡启动。一旦设备成功启动后,处于烧写模式下的BootLoader就会将各文件按照要求写入产品存储设备(通常是FLASH芯片)的指定地址中。
由此可见Bootloader的主要作用有两个:其一是提供下载模式,将组成系统的各个Image写入到设备的永久存储介质中;其二才是在设备开机过程中完成引导系统正常启动的重任。
一个完整的Android烧录包至少需要由3部分内容(即Boot Loader,Linux Kernel和Android System)组成。我们可以利用某种方式对它们先进行打包处理,然后统一写入设备中。一般情况下,芯片厂商(如Samsung)会针对某款或某系列芯片提供专门的烧录工具给开发人员使用;否则各产品开发商需要根据实际情况自行研发合适的工具。
总的来说,SD卡的烧录手法以其操作简便、不需要PC支持等优点被广泛应用于工厂生产中。
(2)USB方式
这种方式需要在PC的配合下完成。设备首先与PC通过USB进行连接,然后运行于PC上的客户端程序将辅助Android设备来完成文件烧录。
(3)专用的烧写工具
比如使用J-Tag进行系统烧录。
(4)网络连接方式
这种方式比较少见,因为它要求设备本身能接入网络(局域网、互联网),这对于很多嵌入式设备来说过于苛刻。
(5)设备Bootloader+fastboot的模式
这也就是我们俗称的“线刷”。需要特别注意的是,能够使用这种升级模式的一个前提是设备中已经存在可用的Bootloader,因而它不能被运用于工厂烧录中(此时设备中还未有任何有效的系统程序)。
当然,各大厂商通常还会在这种模式上做一些“易用性的封装”(譬如提供带GUI界面的工具),从而在一定程度上降低用户的使用门槛。
迫使Android设备进入Bootloader模式的方法基本上大同小异,下面这两种是最常见的:
通过“fastboot reboot-bootloader”命令来重启设备并进入Bootloader模式;
在关机状态下,同时按住设备的“音量减”和电源键进入Bootloader模式。
(6)Recovery模式
和前一种方式类似,Recovery模式同样不适用于设备首次烧录的场景。“Recovery”的字面意思是“还原”,这也从侧面反映出它的初衷是帮助那些出现异常的系统进行快速修复。由于OTA这种得到大规模应用的升级方式同样需要借助于Recovery模式,使得后者逐步超出了原先的设计范畴,成为普通消费者执行设备升级操作的首选方式。我们将在后续小节中对此做更详细的讲解。
早期的Android系统只支持32位CPU架构的编译,但随着越来越多的64位硬件平台的出现,这种编译系统的局限性就突显出来了。因而Android系统推出了一种新的编译方式,即Multilib build。可想而知,这种编译系统上的改进需要至少满足两个条件:
64位和32位平台在很长一段时间内都需要“和谐共处”,因而编译系统必须保证以下几个场景。
Case1:支持只编译64-bit系统。
Case2:支持只编译32-bit系统。
Case3:支持编译64和32bit系统,64位系统优先。
Case4:支持编译32和64位系统,32位系统优先。
事实上Multilib Build提供了比较简便的方式来满足以上两个条件,我们将在下面内容中学习到它的具体做法。
(1)平台配置
BoardConfig.mk用于指定目标平台相关的很多属性,我们可以在这个脚本中同时指定Primary和Secondary的CPU Arch和ABI:
与Primary Arch相关的变量有TARGET_ARCH、TARGET_ARCH_VARIANT、TARGET_CPU_VARIANT等,具体范例如下:
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := arm64-v8a
与Secondary Arch相关的变量有TARGET_2ND_ARCH、TARGET_2ND_ARCH_VARIANT、TARGET_2ND_CPU_VARIANT等,具体范例如下:
TARGET_2ND_ARCH := arm
TARGET_2ND_ARCH_VARIANT := armv7-a-neon
TARGET_2ND_CPU_VARIANT := cortex-a15
TARGET_2ND_CPU_ABI := armeabi-v7a
TARGET_2ND_CPU_ABI2 := armeabi
如果希望默认编译32-bit的可执行程序,可以设置:
TARGET_PREFER_32_BIT := true
通常lunch列表中会针对不同平台提供相应的选项,如图2-9所示。
▲图2-9 相应的选项
当开发者选择不同平台时,会直接影响到TARGET_2ND_ARCH等变量的赋值,从而有效控制编译流程。比如图2-10中左、右两侧分别对应我们使用lunch 1和lunch 2所产生的结果,大家可以对比下其中的差异。
▲图2-10 控制编译流程
另外,还可以设置TARGET_SUPPORTS_32_BIT_APPS和TARGET_SUPPORTS_64_BIT_APPS来指明需要为应用程序编译什么版本的本地库。此时需要特别注意:
那么在支持不同位数的编译时,所采用的Tool Chain是否有区别?答案是肯定的。
如果你希望使用通用的GCC工具链来同时处理两种Arch架构,那么可以使用TARGET_GCC_VERSION_EXP;反之你可以使用TARGET_TOOLCHAIN_ROOT和2ND_TARGET_TOOLCHAIN_ROOT来为64和32位编译分别指定不同的工具链。
(2)单模块配置
我们当然也可以针对单个模块来配置Multilib。
需要特别注意的是,在make命令中直接指定的目标对象只会产生64位的编译。举一个例子来说,“lunch aosp_arm64-eng”→“make libc”只会编译64-bit的libc。如果你想编译32位的版本,需要执行“make libc_32”。
描述单模块编译的核心脚本是Android.mk,在这个文件里我们可以通过指定LOCAL_MULTILIB来改变默认规则。各种取值和释义如下所示:
只考虑Primary Arch的情况
同时编译32和64位版本
只编译32位版本
只编译64位版本
这是默认值。编译系统会根据其他配置来决定需要怎么做,如LOCAL_MODULE_TARGET_ARCH,LOCAL_32_BIT_ONLY等。
如果你需要针对某些特定的架构来做些调整,那么以下几个变量可能会帮到你:
可以指定一个Arch列表,例如“arm x86 arm64”等。这个列表用于指定你的模块所支持的arch范围,换句话说,如果当前正在编译的arch不在列表中将导致本模块不被编译:
如其名所示,这个变量起到和上述变量相反的作用。
这两个变量的末尾多了个“WARN”,意思就是如果当前模块在编译时被忽略,那么会有warning打印出来。
各种编译标志也可以打上与Arch相应的标签,如以下几个例子:
我们再来看一下安装路径的设置。对于库文件来说,可以使用LOCAL_MODULE_RELATIVE_PATH来指定一个不同于默认路径的值,这样32位和64位的库都会被放置到这里。对于可执行文件来说,可以分别使用以下两类变量来指定文件名和安装路径:
分别指定32位和64位下的可执行文件名称。
分别指定32位和64位下的可执行文件安装路径。
(3)Zygote
支持Multilib Build还需要考虑一个重要的应用场合,即Zygote。可想而知,Multilib编译会产生两个版本的Zygote来支持不同位数的应用程序,即Zygote64和Zygote32。早期的Android系统中,Zygote的启动脚本被直接书写在init.rc中。但从Lollipop开始,这种情况一去不复返了。我们来看一下其中的变化:
/*system/core/rootdir/init.rc*/
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
根据系统属性ro.zygote的不同,init进程会调用不同的zygote描述脚本,从而启动不同版本的“孵化器”。以ro.zygote为“zygote64_32”为例,具体脚本如下:
/*system/core/rootdir/init.zygote64_32.rc*/
service zygote /system/bin/<strong>app_process64</strong> -Xzygote /system/bin --zygote --start-system
-server --socket-name=zygote
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
service zygote_secondary /system/bin/<strong>app_process32</strong> -Xzygote /system/bin --zygote --
socket-name=zygote_secondary
class main
socket zygote_secondary stream 660 root system
onrestart restart zygote
这个脚本描述的是Primary Arch为64,Secondary Arch为32位时的情况。因为zygote的承载进程是app_process,所以我们可以看到系统同时启动了两个Service,即app_process64和app_process32。关于zygote启动过程中的更多细节,读者可以参考本书的系统启动章节,我们这里先不进行深入分析。
因为系统需要有两个不同版本的zygote同时存在,根据前面内容的学习我们可以断定,zygote的Android.mk中一定做了同时编译32位和64位程序的配置:
/*frameworks/base/cmds/app_process/Android.mk*/
LOCAL_SHARED_LIBRARIES := \
libcutils \
libutils \
liblog \
libbinder \
libandroid_runtime
LOCAL_MODULE:= app_process
LOCAL_MULTILIB := <strong>both</strong>
LOCAL_MODULE_STEM_32 := app_process32
LOCAL_MODULE_STEM_64 := app_process64
include $(BUILD_EXECUTABLE)
上面这个脚本可以作为需要支持Multilib build的模块的一个范例。其中LOCAL_MULTILIB告诉系统,需要为zygote生成两种类型的应用程序;而LOCAL_MODULE_STEM_32和LOCAL_MODULE_STEM_64分别用于指定两种情况下的应用程序名称。
通过前面几个小节的学习,我们已经按照产品需求编译出自定制的Android版本了。编译成功后,会在out/target/product/[YOUR_PRODUCT_NAME]/目录下生成最终要烧录到设备中的映像文件,包括system.img,userdata.img,recovery.img,ramdisk.img等。初次看到这些文件的读者一定想知道为什么会生成这么多的映像、它们各自都将完成什么功能。
这是本小节所要回答的问题。
Android中常见image文件包的解释如表2-7所示。
表2-7 Android系统常见image释义
Image |
Description |
---|---|
boot.img |
包含内核启动参数、内核等多个元素(详见后面小节的描述) |
ramdisk.img |
一个小型的文件系统,是Android系统启动的关键 |
system.img |
Android系统的运行程序包(framework就在这里),将被挂载到设备中的/system节点下 |
userdata.img |
各程序的数据存储所在,将被挂载到/data目录下 |
recovery.img |
设备进入“恢复模式”时所需要的映像包 |
misc.img |
即“miscellaneous”,包含各种杂项资源 |
cache.img |
缓冲区,将被挂载到/cache节点中 |
它们的关系可以用图2-11来表示。
接下来对boot、ramdisk、system三个重要的系统image进行深入解析。
▲图2-11 关系图
理解boot.img的最好方法就是学习它的制作工具—— mkbootimg,源码路径在system/core/ mkbootimg中。这个工具的语法规则如下:
mkbootimg --kernel <filename> --ramdisk <filename>
[ --second <2ndbootloader-filename>] [ --cmdline <kernel-commandline> ]
[ --board <boardname> ] [ --base <address> ]
[ --pagesize <pagesize> ] -o|--output <filename>
--kernel:指定内核程序包(如zImage)的存放路径;
--ramdisk:指定ramdisk.img(下一小节有详细分析)的存放路径;
--second:可选,指第二阶段文件;
--cmdline:可选,内核启动参数;
--board:可选,板名称;
--base:可选,内核启动基地址;
--pagesize:可选,页大小;
--output:输出名称。
那么,编译系统是在什么地方调用mkbootimg的呢?
其一就是droidcore的依赖中,INSTALLED_BOOTI MAGE_TARGET,如图2-12所示。
▲图2-12 droidcore的依赖
其二就是生成INSTALLED_BOOTIMAGE_TARGET的地方(build/core/Makefile),如图2-13所示。
▲图2-13 生成INSTALLED_BOOTIMAGE_TARGET的地方
可见mkbootimg程序的各参数是由INTERNAL_BOOTIMAGE_ARGS和BOARD_MKBOOTIMG_ARGS来指定的,而这两者又分别取决于其他makefile中的定义。如BoardConfig.mk中定义的BOARD_KERNEL_CMDLINE在默认情况下会作为--cmdline参数传给mkbootimg;BOARD_KERNEL_BASE则作为--base参数传给mkbootimg。
按照Bootimg.h中的描述,boot.img的文件结构如图2-14所示。
▲图2-14 boot.img的文件结构
各组成部分如下:
存储内核启动“头部”—— 内核启动参数等信息,占据一个page空间,即4KB大小。Header中包含的具体内容可以通过分析Mkbootimg.c中的main函数来获知,它实际上对应boot_img_hdr这个结构体:
/*system/core/mkbootimg/Bootimg.h*/
struct boot_img_hdr
{
unsigned char magic[BOOT_MAGIC_SIZE];
unsigned kernel_size; /* size in bytes */
unsigned kernel_addr; /* physical load addr */
unsigned ramdisk_size; /* size in bytes */
unsigned ramdisk_addr; /* physical load addr */
unsigned second_size; /* size in bytes */
unsigned second_addr; /* physical load addr */
unsigned tags_addr; /* physical addr for kernel tags */
unsigned page_size; /* flash page size we assume */
unsigned unused[2]; /* future expansion: should be 0 */
unsigned char name[BOOT_NAME_SIZE]; /* asciiz product name */
unsigned char cmdline[BOOT_ARGS_SIZE];
unsigned id[8]; /* timestamp / checksum / sha1 / etc */
};
这样讲有点抽象,下面举个实际的boot.img例子,我们可以用UltraEditor或者WinHex把它打开,如图2-15所示。
可以看到,文件最起始的8个字节是“ANDROID!”,也称为BOOT_MAGIC;后续的内容则包括kernel_size,kernel_addr等,与上述的boot_img_hdr结构体完全吻合。
▲图2-15 boot header实例
内核程序是整个Android系统的基础,也被“装入”boot.img中——我们可以通过--kernel选项来指定内核映射文件的存储路径。其所占据的大小为:
n pages=(kernel_size + page_size - 1) / page_size
由此可以看出,boot.img中的各元素必须是页对齐的。
不仅是kernel,boot.img中也包含了ramdisk.img。其所占据大小为:
m pages=(ramdisk_size + page_size - 1) / page_size
可见也是页对齐的。
其他关于ramdisk的详细描述请参照下一小节,这里先不做解释。
这一项是可选的。其占据大小为:
o pages= (second_size + page_size - 1) / page_size
这个元素通常用于扩展功能,默认情况下可以忽略。
无论什么类型的文件,从计算机存储的角度来说都只不过是一堆“0”“1”数字的集合—— 它们只有在特定处理规则的解释下才能表现出意义。如txt文本用Ultra Editor打开就可以显示出里面的文字;jpg图像文件在Photoshop工具的辅助下可以让用户看到其所包含的内容。而文本与jpeg图像文件本质上并没有区别,只不过存储与读取这一文件的“规则”发生了变化—— 正是这些“五花八门”的“规则”才创造出成千上万的文件类型。
另外,文件后缀名也并不是必需的,除非操作系统用它来鉴别文件的类型。而更多情况下,后缀名的存在只是为了让用户有个直观的认识。如我们会认为“*.txt”是文本文档、“*.jpg”是图片等。
Android的系统文件以“.img”为后缀名,这种类型的文件最初用来表示某个disk的完整复制。在从原理的层面讲解这些系统映像之前,可以通过一种方式来让读者对这些文件有个初步的感性认识(下面的操作以ramdisk.img为例,其他映像文件也是类似的)。
首先对ramdisk.img执行file命令,得到如下结果:
$file ramdisk.img
ramdisk.img: gzip compressed data, from Unix
这说明它是一个gZip的压缩文件。我们将其改名为ramdisk.img.gz,再进行解压。具体命令如下:
$gzip –d ramdisk.img.gz
这时会得到另一个名为ramdisk.img的文件,不过文件类型变了:
$file ramdisk.img
ramdisk.img: ASCII cpio archive (SVR4 with no CRC)
由此可知,这时的ramdisk.img是CPIO文件了。
再来执行以下操作:
$cpio -i -F ramdisk.img
3544 blocks
这样就解压出了各种文件和文件夹,范例如图2-16所示。
▲图2-16 范例
可以清楚地看到,常用的system目录、data目录以及init程序(系统启动过程中运行的第一个程序)等文件都包含在ramdisk.img中。
这样我们可以得出一个大致的结论,ramdisk.img中存放的是root根目录的镜像(编译后可以在out/target/product/[YOUR_PRODUCT_NAME]/root目录下找到)。它将在Android系统的启动过程中发挥重要作用。
要将system.img像ramdisk.img一样解压出来会相对麻烦一些。不过方法比较多,除了以下提到的方式,读者还可以尝试使用unyaffs(参考http://code.google.com/p/unyaffs/或者http://code. google.com/p/yaffs2utils/)来实现。
这里我们采取mount的方法,这是目前最省时省力的解决方式。
步骤如下:
编译成功后,这个工具的可执行文件在out/host/linux-x86/bin中。
源码目录 system/extras/ext4_utils。
将此工具复制到与system.img同一目录下。
执行如下命令可以查询simg2img的用法:
$ ./simg2img --h
Usage: simg2img <sparse_image_file><raw_image_file>
对system.img执行:
$ ./simg2img system.img system.img.step1
将上一步得到的文件通过以下操作挂载到system_extracted中:
$ mkdir system_extracted
$ sudo mount -o loop system.img.step1 system_extracted
最终我们得到如图2-17所示的结果。
▲图2-17 结果图
这说明该image文件包含了设备/system节点中的相关内容。
Android领域的开放性催生了很多第三方ROM的繁荣(例如市面上“五花八门”的Recovery、定制的Boot Image、System Image等),同时也给系统本身的安全性带来了挑战。
从4.4版本开始,Android结合Kernel的dm-verity驱动能力实现了一个名为“Verified Boot”的安全特性,以期更好地保护系统本身免受恶意程序的侵害。我们在本小节将向大家讲解这一特性的基本原理,以便读者们在无法成功利用fastboot写入image时可以清楚地知道隐藏在背后的真正原因。
我们先来熟悉表2-8所示的术语。
当设备开机以后,根据Boot State和Device State的状态值不同,有如图2-18所示几种可能性。
表2-8 Verified Boot相关术语
术 语 |
释 义 |
---|---|
dm-verity |
Linux kernel的一个驱动,用于在运行时态验证文件系统分区的完整性(判断依据是Hash Tree和Signed metadata) |
Boot State |
保护等级,分为GREEN、YELLOW、ORANGE和RED四种 |
Device State |
表明设备接受软件刷写的程度,通常有LOCKED和UNLOCKED两种状态 |
Keystore |
公钥合集 |
OEM key |
Bootloader用于验证boot image的key |
▲图2-18 Verified Boot总体流程
(引用自Android官方文档)
最下方的4个圆圈颜色分别为:GREEN、YELLOW、RED和ORANGE。例如当前设备的Device State是LOCKED,那么就首先需要经历OEM KEY Verification——如果通过的话Boot State是GREEN,表示系统是安全的;否则需要进入下一轮的Signature Verification,其结果决定了Boot State是YELLOW或者是RED(比较危险)。当然,如果当前设备本身就是UNLOCKED的,那就不用经过任何检验——不过它和YELLOW、RED一样的地方是,都会在屏幕上显式地告诫用户潜在的各种风险。部分Android设备还会要求用户主动做出选择后才能正常启动,如图2-19所示典型示例。
如果设备的Device State发生切换的话(fastboot就提供了类似的命令,只不过大部分设备都需要解锁码才能完成),那么系统中的data分区将会被擦除,以保证用户数据的安全。
▲图2-19 典型示例
我们知道,Android系统在启动过程中要经过Bootloader->Kernel->Android三个阶段,因而在Verified Boot的设计中,它对分区的看护也是环环相扣的。具体来说,Bootloader承担boot和recovery分区的完整性校验职责;而Boot Partition则需要保证后续的分区,如system的安全性。另外,Recovery的工作和Boot是基本类似的。
不过,由于分区文件大小有差异,具体的检验手段也是不同的。结合前面小节对boot.img的描述,其在增加了verified boot后的文件结构变化如图2-20所示。
▲图2-20 文件结构变化
除了mkbootimg来生成原始的boot.img外,编译系统还会调用另一个新工具,即boot_signer(对应源码目录system/extras/verity)来在boot.img的尾部附加一个signature段。这个签名是针对boot.img的Hash结果展开的,默认使用的key在/build/target/product/security目录下。
而对于某些大块分区(如System Image),则需要通过dm-verity来验证它们的完整性。关于dm-verity还有非常多的技术细节,限于篇幅我们不做过多讨论,但强烈建议读者自行查阅相关资料做进一步深入学习。
ODEX是Android旧系统的一个优化机制。对于很多开发人员来说,ODEX可以说是既熟悉又陌生。熟悉的原因在于目前很多手机系统,或者APK中的文件都从以前的格式变成了如图2-21和图2-22所示的样子。
而陌生的原因在于有关ODEX的资料并不是很多,不少开发人员对于ODEX是什么,能做什么以及它的应用流程并不清楚——这也是我们本小节所要向大家阐述的内容。
▲图2-21 系统目录system/framework下的文件列表
ODEX是Optimized Dalvik Executable的缩写,从字面意思上理解,就是经过优化的Dalvik可执行文件。Dalvik是Android系统(目前已经切换到Art虚拟机)中采用的一种虚拟机,因而经过优化的ODEX文件让我们很自然地想到可以为虚拟机的运行带来好处。
事实上也的确如此——ODEX是Google为了提高Android运行效率做出努力的成果之一。我们知道,Android系统中不少代码是使用Java语言编写的。编译系统首先会将一个Java文件编译成class的形式,进而再通过一个名为dx的工具来转换成dex文件,最后将dex和资源等文件压缩成zip格式的APK文件。换句话说,一个典型的Android APK的组成结构如图2-23所示。
▲图2-22 系统目录/system/app下的文件列表
▲图2-23 APK的组成结构
本书的Android应用程序编译和打包章节将做更为详细介绍。现在大家只要知道APK中有哪些组成元素就可以了。当应用程序启动时,系统需要提取图2-23中的dex(如果之前没有做过ODEX优化的话,或者/data/dalvik-cache中没有对应的ODEX缓存),然后才能执行加载动作。而ODEX则是预先将DEX提取出来,并针对当前具体设备做了优化工作后的产物,这样做除了能提高加载速度外,还有如下几个优势:
ODEX是在dex基础上针对当前具体设备所做的优化,因而它和生成时所处的具体设备有很大关联。换句话说,除非破解者能提供与ODEX生成时相匹配的环境文件(比如core.jar、ext.jar、framework.jar、services.jar等),否则很难完成破解工作。这就在无形中提高了系统的安全性。
按照Android系统以前的做法,不仅APK中需要存放一个dex文件,而且/data/dalvik-cache目录下也会有一个dex文件,这样显然会浪费一定的存储空间。相比之下,ODEX只有一份,而且它比dex所占的体积更小,因而自然可以为系统节省更多的存储空间。
前面我们讨论了系统包烧录的几种传统方法,而Android系统其实还提供了另一种全新 的升级方案,即OTA(Over the Air)。OTA非常灵活,它既可以实现完整的版本升级,也可以做到增量升级。另外,用户既可以选择通过SD卡来做本地升级,也可以直接采用网络在线升级。
不论是哪种升级形式,都可以总结为3个阶段:
下面我们来逐一分析这3个阶段。
升级包也是由系统编译生成的,其编译过程本质上和普通Android系统编译并没有太大区别。如果想生成完整的升级包,具体命令如下:
$make otapackage
注意
生成OTA包的前提是,我们已经成功编译生成了系统映像文件(system.img等)。
最终将生成以下文件:
out/target/product/[YOUR_PRODUCT_NAME]/[YOUR_PRODUCT_NAME]-ota-eng.[UID].zip
而生成差分包的过程相对麻烦一些,不过方法也很多。以下给出一种常用的方式:
将上一次生成的完整升级包复制并更名到某个目录下,如~/OTA_DIFF/old_target_file.zip;
对源文件进行修改后,用make otapackage编译出一个新的OTA版本;
将本次生成的OTA包更名后复制到和上一个升级包相同的目录下,如~/OTA_DIFF/ new_target_file.zip;
调用ota_from_target_files脚本来生成最终的差分包。
这个脚本位于:
build/tools/releasetools/ota_from_target_files
值得一提的是,完整升级包的生成过程其实也使用了这一脚本。区分的关键就在于使用时是否提供了-i参数。
其具体语法格式是:
ota_from_target_files [Flags] input_target_files output_ota_package
所有Flags参数释义如表2-9所示。
表2-9 ota_from_target_files参数
参 数 |
说 明 |
---|---|
-b (--board_config) <file> |
在新版本中已经无效 |
-k (--package_key) <key> |
<key>用于包的签名默认使用input_target-files中的META/misc_info.txt文件如果此文件不存在,则使用build/target/product/security/testkey |
-i (--incremental_from) <file> |
该选项用于生成差分包 |
-w (--wipe_user_data) |
由此生成的OTA包在安装时会自动擦除user data 分区 |
-n (--no_prereq) |
忽略时间戳检查 |
-e (--extra_script) <file> |
将<file>内容插入update脚本的尾部 |
-a (--aslr_mode) <on|off> |
是否开启ASLR技术默认为开 |
在这个例子中,我们可以采用以下命令生成一个OTA差分包:
./build/tools/releasetools/ota_from_target_files-i ~/OTA_DIFF/old_target_file.zip~/OTA_DIFF/new_target_file.zip
这样生成的update.zip就是最终可用的差分升级包。一方面,差分升级包体积较小,传输方便;但另一方面,它对升级的设备有严格要求,即必须是安装了上一升级包版本的那些设备才能正常使用本次的OTA差分包。
如图2-24所示,有两种常见的渠道可以获取到OTA升级包,分别是在线升级和本地升级。
▲图2-24 获取OTA升级包的两种方式
开发者将编译生成的OTA包上传至网络存储服务器上,然后用户可以直接通过终端访问和下载升级文件。通常我们把下载到的OTA包存储在设备的SD卡中。
在线升级的方式涉及两个核心因素。
设备厂商需要架构服务器来存放、管理OTA包,并为客户端提供包括查询在内的多项服务。
客户终端如何与服务器进行交互,是否需要认证,OTA包如何传输等都是需要考虑的。
由此可见,在线升级方式要求厂商提供较好的硬件环境来解决用户大规模升级时可能引发的问题,因而成本较高。不过这种方式对消费者来说比较方便,而且可以实时掌握版本的最新动态,所以对凝聚客户有很大帮助。目前很多主流设备生产商(如HTC)和第三方的ROM开发商(如MIUI)都提供了在线升级模式。
服务器和客户端的一种理论交互方案可以参见图2-25所示的图例。
步骤如下:
在手动升级的情况下,由用户发出升级的指令;而在自动升级的情况下,则由程序根据一定的预设条件来启动升级流程。比如设定了开机自动检查是否有可用的更新,那么每次机器启动后都会去服务器取得最新的版本信息。
无论是手动还是自动升级,都必须通过服务器查询信息。与服务器的连接方式是多种多样的,由开发人员自行决定。在必要的情况下,还应该使用加密连接。
如果一切顺利,我们就得到了服务器上最新升级文件的版本号。接下来需要将这个版本号与本地安装的系统版本号进行比较,决定是否进入下一步操作。
如果服务器上的升级文件要比本地系统新(在制定版本号规则时,应尽量考虑如何可以保证新旧版本的快速比较),那么升级继续;否则中止升级流程——且若是手动升级的情况,一定要提示用户中止的原因,避免造成不好的用户体验。
升级文件一般都比较大(Android系统文件可能达到几百MB)。这么大的数据量,如果是通过移动通信网络(GSM\WCDMA\CDMA\TD-SCDMA等)来下载,往往不现实。因此如果没有事先知会用户而自动下载的话,很可能会引起用户的不满。“提示框”的设计也要尽可能便利,如可以让用户快捷地启用Wi-Fi通道进行下载。
下载后的升级文件需要存储在本地设备中才能进入下一步的升级。通常这一文件会直接被放置在SD卡的根目录下,命名为update.zip。
接下来系统将自动重启,并进入RecoveryMode进行升级。
▲图2-25 在线升级图例
OTA升级包并非一定要通过网络在线的方式才可以下载到——只要条件允许,就可以从其他渠道获取到升级文件update.zip,并复制到SD卡的根目录下,然后手动进入升级模式(见下一小节)。
在线升级和本地升级各有利弊,开发商应根据实际情况来提供最佳的升级方式。
经过前面小节的讲解,现在我们已经准备好系统升级文件了(不论是在线还是本地升级),接下来就进入OTA升级最关键的阶段——Recovery模式,也就是大家俗称的“卡刷”。
Recovery相关的源码主要在工程项目的如下目录中:
\bootable\recovery
因为涉及的模块比较多,这个文件夹显得有点杂乱。我们只挑选与Recovery刷机有关联的部分来进行重点分析。
▲图2-26 进入RecoveryMode的流程
图2-26所示是Android系统进入RecoveryMode的判断流程,可见在如下两种情况下设备会进入还原模式。
很多Android设备的RecoveryKey都是电源和Volume+的组合键,因为这两个按键在大部分设备上都是存在的。
系统在某些情况下会主动要求进入还原模式,如我们前面讨论的“在线升级”方式——当OTA包下载完成后,系统需要重启然后进入RecoveryMode进行文件的刷写。
当进入RecoveryMode后,设备会运行一个名为“Recovery”的程序。这个程序对应的主要源码文件是/bootable/recovery/ recovery.cpp,并且通过如下几个文件与Android主系统进行沟通。
(1)/cache/recovery/command INPUT
Android系统发送给recovery的命令行文件,具体命令格式见后面的表格。
(2)/cache/recovery/log OUTPUT
recovery程序输出的log文件。
(3)/cache/recovery/intent OUTPUT
recovery传递给Android的intent。
当Android系统希望开机进入还原模式时,它会在/cache/recovery/command中描述需要由Recovery程序完成的“任务”。后续Recovery程序通过解析这个文件就可以知道系统的“意图”,如表2-10所示。
表2-10 CommandLine参数释义
Command Line |
Description |
---|---|
--send_intent=anystring |
将text输出到recovery.intent中 |
--update_package=path |
安装OTA包 |
--wipe_data |
擦除user data,然后重启 |
--wipe_cache |
擦除cache(不包括user data),然后重启 |
--set_encrypted_filesystem=on|off |
enable/disable加密文件系统 |
--just_exit |
直接退出,然后重启 |
由表格所示的参数可以知道Recovery不但负责OTA的升级,而且也是“恢复出厂设置”的实际执行者,如图2-27所示。
▲图2-27 系统设置中的“恢复出厂设置”
接下来分别讲解这两个功能在Recovery程序中的处理流程。
恢复出厂设置。
(1)用户在系统设置中选择了“恢复出厂设置”。
(2)Android系统在/cache/recovery/command中写入“--wipe_data”。
(3)设备重启后发现了command命令,于是进入recovery。
(4)recovery将在BCB(bootloader control block)中写入“boot-recovery”和“--wipe_data”,具体是在get_args()函数中——这样即便设备此时重启,也会再进入erase流程。
(5)通过erase_volume来重新格式化/data。
(6)通过erase_volume来重新格式化/cache。
(7)finish_recovery将擦除BCB,这样设备重启后就能进入正常的开机流程了。
(8)main函数调用reboot来重启。
上述过程中的BCB是专门用于recovery和bootloader间互相通信的一个flash块,包含了如下信息:
struct bootloader_message {
char command[32];
char status[32];
char recovery[1024];
};
依据前面对Android系统几大分区的讲解,BCB数据应该存放在哪个image中呢?没错,是misc。
OTA升级具体如下。
(1)OTA包的下载过程参见前一小节的介绍。假设包名是update.zip,存储在SDCard中。
(2)系统在/cache/recovery/command中写入"--update_package=[路径名]"。
(3)系统重启后检测到command命令,因而进入recovery。
(4)get_args将在BCB中写入"boot-recovery" 和 "--update_package=..." —— 这样即便此时设备重启,也会尝试重新安装OTA升级包。
(5)install_package开始安装OTA升级包。
(6)finish_recovery擦除BCB,这样设备重启后就可以进入正常的开机流程了。
(7)如果install失败的话:
(8)main调用maybe_install_firmware_update,OTA包中还可能包含radio/hboot firmware的更新,具体过程略。
(9)main调用reboot重启系统。
总体来说,整个Recovery.cpp源文件的逻辑层次比较清晰,读者可以基于上述流程的描述来对照并阅读代码。
目前我们已经学习了Android原生态系统及定制产品的编译和烧录过程。和编译相对的,却同样重要的是反编译。比如,一个优秀的“用毒”高手往往也会是卓越的“解毒”大师,反之亦然。大自然的一个奇妙之处即万事万物都是“相生相克”的,只有在竞争中才能不断地进步和发展。
首先要纠正不少读者可能会持有的观点——“反编译”就是去“破解”软件。应该说,破解一款软件的确需要用到很多反编译的知识,不过这并不是它的全部用途。比如笔者就曾经在开发过程中利用反编译辅助解决了一个bug,在这里和读者分享一下。
问题是这样的:开发人员A修改了framework中的某个文件,然后通过正常的编译过程生成了image,再将其烧录到了机器上。但奇怪的是,文件的修改并没有体现出来(连新加的log也没有打印出来)。显然,出现问题的可能是下列步骤中的任何一个,如图2-28所示。
▲图2-28 可能出现问题的几个步骤
可疑点为:
因为加log的那个函数是系统会频繁调用到的,而且log就放在函数开头没有加任何判断,所以这个可能性被排除。
打印log所用的方法与此文件中其他地方所用的方法完全一致,而且其他地方的log确实成功输出了,所以也排除这一可能性。
虽然Android的编译系统非常强大,但是难免会有bug,因而这个可能性还是存在的。那么如何确定我们修改的文件真的被编译到了呢?此时反编译就有了用武之地了。
这并不是空穴来风,确实发生过开发人员因为粗心大意烧错版本的“事故”(对于某些细微修改,编译系统不会主动产生新的版本号)。通过反编译机器上的程序,然后和原始文件进行比较,我们可以清楚地确认机器中运行的程序是不是预期的版本。
由上述分析可知,反编译是确定该问题最直接的方式。
Android反编译过程按照目标的不同分为如下两类(都是基于Java语言的情况)。
不论针对哪种目标对象,它们的步骤都可以归纳为如图2-29所示。
APK应用安装包实际上是一个Zip压缩包,使用Zip或WinRAR等软件打开后里面有一个“classes.dex”文件—— 这是Dalvik JVM虚拟机支持的可执行文件(Dalvik Executable)。关于这个文件的生成过程,可以参见本书应用篇中对APK编译过程的介绍。换句话说,classes.dex这个文件包含了所有的可执行代码。
▲图2-29 反编译的一般流程
由前面小节的学习我们知道,odex是classes.dex经过dex优化(optimize)后产生的。一方面,Dalvik虚拟机会根据运行需求对程序进行合理优化,并缓存结果;另一方面,因为可以直接访问到程序的odex,而不是通过解压缩包去获取数据,所以无形中加快了系统开机及程序的运行速度。
针对反编译过程,我们首先是要取得程序的dex或者odex文件。如果是APK应用程序,只需要使用Zip工具解压缩出其中的classes.dex即可(有的APK原始的classes.dex会被删除,只保留对应的odex文件);而如果是包含在系统image中的系统包(如framework就是在system.img中),就需要通过其他方法间接地将其原始文件还原出来。具体步骤可以参见前一小节的介绍。
取得dex/odex文件后,我们将它转化成Jar文件。
目前已经有不少研究项目在分析Android的逆向工程,其中最著名的就是smali/baksmali。可以在这里下载到它的最新版本:
http://code.google.com/p/smali/downloads/list
“smali”和“baksmali”分别对应冰岛语中“assembler”和“disassembler”。为什么要用冰岛语命名呢?答案就是Dalvik这个名字实际上是冰岛的一个小渔村。
如果是odex,需要先用baksmali将其转换成dex。具体语法如下:
$ baksmali -a <api_level> -x <odex_file> -d <framework_dir>
-a指定了API Level,-x表示目标odex文件,-d指明了framework路径。因为这个工具需要用到诸如core.jar,ext.jar,framework.jar等一系列framework包,所以建议读者直接在Android源码工程中out目录下的system/framework中进行操作,或者把所需文件统一复制到同一个目录下。
范例如下(1.4.1版本):
$ java -jar baksmali-1.4.1.jar -a 16 -x example.odex
如果是要反编译系统包中的odex(如services.odex),请参考以下命令:
$java -Xmx512m -jar baksmali-1.4.1.jar -a 16 -c:core.jar:bouncycastle.jar:ext.jar:framework.
jar:android.policy.jar:services.jar:core-junit.jar -d framework/ -x services.odex
更多语法规则可以通过以下命令获取:
$ java -jar baksmali-1.4.1.jar --help
执行结果会被保存在一个out目录中,里面包含了与odex相应的所有源码,只不过由smali语法描述。读者如果有兴趣的话,可以阅读以下文档来了解smali语法:
http://code.google.com/p/smali/wiki/TypesMethodsAndFields
当然对于大部分开发人员来说,还是希望能反编译出最原始的Java语言文件。此时就要再将smali文件转化成dex文件。具体命令如下:
$ java -jar smali-1.4.1.jar out/ -o services.dex
于是接下来的流程就是dex→Java,请参考下面的说明。
前面我们已经成功将odex“去优化”成dex了,离胜利还有一步之遥——将dex转化成jar文件。目前比较流行的工具是dex2jar,可以在这里下载到它的最新版本:
http://code.google.com/p/dex2jar/downloads/list
使用方法也很简单,具体范例如下:
$ ./dex2jar.sh services.dex
上面的命令将生成services_dex2jar.jar,这个Jar包中包含的就是我们想要的原始Java文件。那么,选择什么工具来阅读Jar中的内容呢?在本例中,我们只是希望确定所加的log是否被正确编译进目标文件中,因而可以使用任何常用的文本编辑器查阅代码。而如果希望能更方便地阅读代码,推荐使用jd-gui,它是一款图形化的反编译代码阅读工具。
这样,整个反编译过程就完成了。
顺便提一下,目前,几乎所有的Android程序在编译时都使用了“代码混淆”技术,反编译后的结果和原始代码还是有一定差距,但不影响我们理解程序的主体架构。“代码混淆”可以有效地保护知识产权,防止某些不法分子恶意剽窃,或者篡改源码(如添加广告代码、植入木马等),建议大家在实际的项目开发中尽量采用。
我们知道Android系统下的应用程序主要是由Java语言开发的,但这并不代表它不支持其他语言,比如C++和C。事实上,不同类型的应用程序对编程语言的诉求是有区别的——普通Application的UI界面基本上是静态的,所以,利用Java开发更有优势;而游戏程序,以及其他需要基于OpenGL(或基于各种Game Engine)来绘制动态界面的应用程序则更适合采用C或者C++语言。
伴随着Android系统的不断发展,开发者对于C/C++语言的需求越来越多,也使得Google需要不断完善它所提供的NDK工具链。NDK的全称是Native Development Kit,可以有效支撑Android系统中使用C/C++等Native语言进行开发,从而让开发者可以:
完成同样的功能,Java虚拟机理论上来说比C/C++要耗费更多的系统资源。因而,如果程序本身对运行性能要求很高的话,建议利用NDK进行开发。
好处是显而易见,即最大程度地避免重复性开发。
NDK的官方网址是:
https://developer.android.com/ndk/index.html
它的安装很简单,在Windows下只要下载一个几百MB的自解压包然后双击打开它就可以了。NDK文件夹可以被放置到磁盘中的任何位置,不过为了操作方便,建议开发者可以设置系统环境变量来指向其中的关键程序。NDK既支持Java和C/C++混合编程的模式,也允许我们只开发纯Native实现的程序。前者需要用到JNI技术(即Java Native Interface),它的神奇之处在于可以让两种看似没有瓜葛的语言间进行无缝的调用。例如下面是一个JNI的实例:
public class MyActivity extends Activity {
/**
* Native method implemented in C/C++
*/
public <strong>native</strong> void jniMethodExample();
}
MyActivity是一个Java类,它的内部包含一个声明为Native的成员变量,即jniMethodExample。这个函数的实现是通过C/C++完成的,并被编译成so库来供程序加载使用。更多JNI的细节,我们将在后续章节进行详细介绍。
本小节我们将通过一个具体实例来着重讲解如何利用NDK来为应用程序执行C/C++的编译。
在此之前,请确保你已经下载并解压了NDK包,并为它设置了正确的系统环境变量。这个例子中将包含如下几个文件,我们统一放在一个JNI文件夹中:
Android.mk用于描述一个Android的模块,包括应用程序、动态库、静态库等。它和我们本章节讲解的用法基本一致,因而不再赘述。
Application.mk用于描述你的程序中所用到的各个Native模块(可以是静态或者动态库,或者可执行程序)。这个脚本中常用的变量不多,我们从中挑选几个核心的来讲解:
指向程序的根目录。当然,如果你是按照Android系统默认的结构来组织工程文件的话,这个变量是可选的。
用于指示当前是release或者debug版本。前者是默认的值,将会生成优化程度较高的二进制文件;调试模式则会生成未优化的版本,以便保留更多的信息来帮助开发者追踪问题。在AndroidManifest.xml的<application>标签中声明android:debuggable会将默认值变更为debug,不过APP_OPTIM的优先级更高,可以重载debuggable的设置。
设置对全体module有效的C/C++编译标志。
用于描述一系列链接器标志,不过只对动态链接库和可执行程序有效。如果是静态链接库的情况,系统将忽略这个值。
用于指示编译所针对的目标Application Binary Interface,默认值是armeabi。可选值如表2-11所示。
表2-11 可选值
指 令 集 |
ABI值 |
---|---|
Hardware FPU instructions on ARMv7 based devices |
APP_ABI := armeabi-v7a |
ARMv8 AArch64 |
APP_ABI := arm64-v8a |
IA-32 |
APP_ABI := x86 |
Intel64 |
APP_ABI := x86_64 |
MIPS32 |
APP_ABI := mips |
MIPS64 (r6) |
APP_ABI := mips64 |
All supported instruction sets |
APP_ABI := all |
文件testNative.cpp中的内容就是程序的源码实现,对此NDK官方提供了较为完整的Samples供大家参考,涵盖了OpenGL、Audio、Std等多个方面,有兴趣的读者可以自行下载分析。
那么有了这些文件后,如何利用NDK把它们编译成最终产物呢?
最简单的方式就是采用如下的命令:
cd <project>
$ <ndk>/ndk-build
其中ndk-build是一个脚本,等价于:
$GNUMAKE -f <ndk>/build/core/build-local.mk
<parameters>
<ndk>指的是NDK的安装路径。
可见使用NDK来编译还是相当简单的。另外,除了常规的编译外,ndk-build还支持多种选项,譬如:
“clean”表示清理掉之前编译所产生的各种中间文件;
“-B”会强制发起一次完整的编译流程;
“NDK_LOG=1”用于打开NDK的内部log消息;
……
除了本章所描述的Android原生代码外,开发人员也可以选择一些知名的第三方开源ROM来进行学习,譬如CyanogenMod。
CyanogenMod(简称CM)的官方网址如下:
http://www.cyanogenmod.org/
它目前的最新版本是基于Android 6.0的CM 13,并同时支持Google Nexus、HTC、Huawei、LG等多个品牌的众多设备。CyanogenMod的初衷是将Android系统移植到更多的没有得到Google官方支持的设备中,所以有的时候CM针对某特定设备的版本更新时间可能比设备厂商来得还要早。
那么CyanogenMod是如何做到针对多种设备的移植和适配工作的呢?我们将在接下来的内容中为大家揭开这个问题的答案。图2-30是CM的整体描述图。
▲图2-30 CM的整体描述
下面我们分步骤进行讲解。
Step1. 前期准备
在做Porting之前,有一些准备工作需要我们去完成。
(1)获取设备的Product Name、Code Name、Platform Architecture、Memory Size、Internal Storage Size等信息
这些数据有很多可以从/system/build.prop文件中获得,不过前提条件是手机需要被root。
(2)收集设备对应的内核源码
根据GPL开源协议的规定,Android厂商必须公布受GPL协议保护的内容,包括内核源码。因而实现这一步是可行的,只是可能会费些周折。
(3)获取设备的分区信息
Step2. 建立3个核心文件夹
分别是:
设备特有的配置和代码将保存在这个路径下。
这个文件夹中的内容是从原始设备中拉取出来的,由此可见主要是那些没有源代码可以生成的部分,例如一些二进制文件。
专门用于保存内核版本源码的地方。
CM提供了一个名为mkvendor.sh的脚本来帮助创建上述文件夹的“雏形”,有兴趣的读者可以参见build/tools/device/mkvendor.sh文件。不过很多情况下还需要开发者手工修改其中的部分文件,例如device目录下的BoardConfig.mk、device_[codename].mk、cm.mk、recovery.fstab等核心文件。
Step3. 编译一个用于测试的recovery image
编译过程和普通CM编译的最大区别在于选择make recoveryimage。如果在recovery模式下发现Android设备的硬件按键无法使用,那么可以尝试修改/device/[vendor]/[codename]/recovery/ recovery_ui.cpp中的GPIO配置。
Step4. 为上述的device目录建立github仓库,以便其他人可以访问到。
Step5. 填充vendor目录
可以参考CM官网上成熟的设备范例提供的extract-files.sh和setup-makefiles.sh,并据此完成适合自己的这两个脚本。
Step6. 通过CM提供的编译命令最终编译出ROM升级包,并利用前面生成的recovery来将其刷入到设备中。这个过程很可能不是“一蹴而就”的,需要不断调试和修改,直至成功。
当然,限于篇幅我们在本小节只是讲解了CM升级包的核心制作过程,读者如果有兴趣的话可以查阅http://www.cyanogenmod.org/来获取更多细节详情。