corresponding


  • 首页

  • 标签

  • 分类

  • 归档

浅谈Android底层

发表于 2017-11-22 | 分类于 Android

学习的原因

很多应用开发者对Android底层望而却步,主要有两个原因:
1.底层太难,我看不懂
2.学习底层知识对目前开发没有帮助

对于太难的这点

我觉得是因为一开始直接扑进源码里面,看到那些一长串一长串的函数,很容易看得晕头转向,恶心想吐。如果掌握合适的学习方法,会发现其实并没有这么困难。
我建议,在看源码前先理清Android底层的主干逻辑,再拆解出各个模块,各个击破。

对于无帮助这点

我觉得Android底层知识的即时回报非常小,但是长期回报是巨大的。
如果能熟悉Android底层的原理,当在开发中遇到一些奇奇怪怪的问题时,我们可以通过debug和查看log等方式,结合底层原理去发现蛛丝马迹,真正解决一部分烦人的小概率bug。而且熟悉android底层的设计架构,在未来做软件架构设计时,可以参考借鉴,甚至可以在此基础上设计出更棒的架构。
这话听上去觉得特别夸张,其实不然,有两点原因:
1.Android底层的架构也在不断调整和优化中,这说明目前的不是最优解;
2.Android更新迭代了这么多版本,需要兼容旧版本,有些地方不能完全放开去设计,需要在兼容和完全优化中做选择。


作者在学习过程中,尝试按照自己的思路总结Android底层主干逻辑。肯定会有许多不足之处,希望大家多多指出。
由表到底,分成三层:

1.应用程序背后:Android的各大系统服务
2.如何获取这些系统服务:ServiceManager
3.如何通信:Binder体系

应用程序背后:Android的各大系统服务

这里首先要说明下,ActivityManagerService等各种应用服务,虽然说以Service结尾,但是这与Android四大组件Service并无关系。四大组件中的Service,主要提供需要在后台长期运行的服务(如复杂计算、下载等等);这里的Service代表Client/Server架构中的Server。

Client/Server架构简称为C/S架构,也是客户端/服务器端架构。服务器端主要提供数据管理、数据共享、数据及系统维护和并发控制等,客户端程序主要完成用户的具体的业务。
在Android系统中,Client就是我们写的各种应用程序,Server实现页面跳转,屏幕展示等功能细节。Client向Server发出命令,Server去实现完整的功能。

注:Client/Server架构是Server,我们说的Android系统服务是Service。虽然说都是表示“服务”,建议还是注意下拼写,方便更好区分。

我举一个简单的例子

应用开发者常见的工作是,去实现一个Activity并且显示在手机屏幕上。
对于最简单的页面,开发者只要做三步:
1.AndroidManifest文件加入声明
2.Activity中设置setContentView
3.调用startActivity()去启动(发出指令,后续系统Service去实现)

在背后辛勤工作的就是Android的各大系统服务,例如ActivityManagerService(后续简称AMS)主要管理Activity运行状态,WindowManagerService(后续简称WMS)主要负责控制手机屏幕显示内容。

如何获取这些系统服务:ServiceManager

上一节简单的描述了下应用通过各大系统服务去完成Activity生成和屏幕显示。
这里就会有个问题,我们如何去获取这些服务。在应用开发中,如果我们需要使用第三方控件OkHttp,我们需要导入okhttp包,或者在gradle中写入对其的依赖,之后我们才可以调用OkHttp中的对象和方法。

在Android底层也是类似,其中有一个ServiceManager在统筹管理所有的服务。
还是采用上节说的C/S架构,应用程序是客户端,向ServiceManager服务端发起请求获取指定name的服务,要求服务端给与AMS的访问引用。
应用程序持有AMS的引用后,继续采用C/S架构。应用本身还是客户端,此时AMS充当服务端,处理服务端发起的各种Activitiy请求。

这个时候就继续思考一步,AMS、WMS这些系统服务如何和ServiceManager建立联系的呢?
这是在Android手机开机时,AMS、WMS会向ServiceManager注册,将自己的name和实体传给ServiceManager,ServiceManager中会有专门的数据结构(红黑树)去记录这些数据。
注:这里还是采用C/S架构,不过AMS变成了客户端,ServiceManager变成服务端。

再再深入一步,我们需要通过ServiceManager获取其他服务,那我们怎么获取ServiceManager呢?
这里ServiceManager充当大管家的角色,是在开机时最先被创造的服务,并且被赋予0的代号。所有的服务都要先请示ServiceManager。
注:匿名服务除外,匿名服务不需要注册在ServiceManager。当前连接的服务直接传递匿名服务给应用。
后续启动的服务都可以根据0去找到ServiceManager,并且把自己注册进去。
注:这里还是采用C/S架构,不过AMS变成了客户端,ServiceManager变成服务端。

至此我们把获取系统服务,从表到里分析了一遍。我们再换一个维度,以时间发展表述下。
注:学习的时候要时刻记住,所有的对象都不是直接持有,需要通过各种请求获取后才持有。
再注:这里的持有不一定是持有实体,可能是种引用。

如何通信:Binder体系

上面我们了解了系统服务的作用和如何获取系统服务,还有一个更加基础的问题,应用如何和这些系统服务通信。

在Android中,各个应用和各个服务处于不同的进程。就不能像进程内编程一样直接调用其他类的函数,需要进程间的通信(IPC:Inter-Process Communication)
这里Android采用Binder方式。

这里思考下,Linux IPC常见的有pipe、socket、共享内存等等,为什么最后Android会选择Binder呢?
Android之间有大量的跨进程通信,对性能、安全性、易用性要求都很高,综合考虑后选择了Binder方式。

继续问,Binder的性能优势?

socket主要用于网络通信,以TCP/IP作为基础,需要分包、重组等工作,所以效率递比较底下。
注:Android有采用Unix Domain Socket(UDS),针对进程间通信优化。在Android中也有使用,这里暂不讨论。

pipe采用消息转发机制,需要两次拷贝。

Binder在数据传输过程中,只需要一次拷贝。

经过上述操作后,服务端地址s和客户端地址c指向同一块内存。
服务端想与客户端通信时,就将本地内容拷贝到地址s中,客户端在同时监听地址c中的内存变化,及时获取新信息。

Binder的安全性优势?

一般的Linux IPC在通信时,请求方会发送user id(uid)和process id(pid),服务方后根据此去检查请求方的权限,判断后续是否给与服务。
看上去特别安全,但是根源上出现了问题,uid和pid是请求方添加的。这意味着,请求方A可以修改uid和pid,设置成B一样。服务端是没有办法识别出来,这样安全性就无法保障了。

Binder优化了uid和pid机制,不再由请求方自己添加,而是由内核自动添加。

Binder的易用性优势?

采用C/S架构,应用和服务分离,逻辑清晰。
应用持有一个服务的引用,向该引用发起各种请求,引用内部在通过Binder的细节传输给正式的服务,应用开发者不需要管通信的细节。反之,像Linux IPC的共享内存,虽然不需要拷贝,性能特别高。但是使用起来特别复杂,应用开发者需要控制管理服务的内存。

至此,简单的讲述下了《Android的各大系统服务》,《如何获取这些系统服务:ServiceManager》,《如何通信:Binder体系》。当然,本篇只是描述下系统服务的最表层,背后还有许许多多的代码细节和设计者巧妙构思,后续出相关的文章和大家展示。

里面出现最多的字样就是C/S架构,大家在学习中牢记这个架构,Android底层所有的细节都围绕这个展开。

防御式编程

发表于 2017-11-20 | 分类于 coding

在我自己的编码工作中,调bug的时间至少占总时间的50%。
有些时候修复一个的bug甚至会占用几天的时间,最后发现问题出在几天没加条件预防的语句,这太让人恼火了。

我总是在苦恼,为何我的程序有这么多闪退。
为什么就不能有一个安全的编程环境给我们,让我们这些菜鸟写的代码不闪退。

代码世界中危机丛生,程序跑着跑着就要歇菜。

随着发际线的慢慢后退,我开始认清一个令人感觉残酷的现实:
这是你的代码,你需要为其付所有责任,包括最基础的安全性检查。

慢慢的我们需要变得保守,不能轻易相信所有的数据,需要在使用前对其检查。


防御式编程

原则:就是对来源数据持怀疑、不信任的态度
对调用者含有敌意,他们可能会放入各种奇怪的未经检查的数据。
其中最常用的就是null的判断。

在面向对象的编程中,任何一个对象都初始为null(final和 static关键字声明的变量除外)
当然,在一般情况下,变量会在使用前被赋值。
当调用null对象的方法时,会抛出NullPointerException。
如果没有及时对Exception进行捕获,程序就会报错。

处理方法

这里有常用的两种处理方法,两者相互合作,相辅相成。

变量的预先检查

1
2
3
4
5
6
7
8
void func(Model arg) {
if (null == arg) {
// todo
} else {
// arg的合理性检查
arg.do();
}
}

如果变量不符合要求,后续代码无法继续执行。
可以改成如下方式:

1
2
3
4
5
6
7
8
9
10
11
void func(Model arg) {
if (null == arg) {
// todo
return;
}
if (arg不符合要求) {
// todo
return;
}
// ...
}

异常后处理

1
2
3
4
5
6
7
8
9
void func(Model arg) {
try {
arg.do();
} catch (NullPointerException e) {
// todo
} catch (Exception e) {
// todo
}
}

合理的使用Exception,能帮助我们更好的代码分层。

Exception它可以报错误上报给调用链的上层。
每个函数都有他的职责范围和处理错误的范围,底层函数不需要处理所有的Exception,他可以将自己无法处理的Exception交给上级。

效率

有人会有疑问:每个函数进入处都进行合理性检查,会不会特别降低效率。

对于一般的应用代码而言,这里的开销微乎其微,不用特别在意。
现有编译器都会帮用户做性能上优化。
如果对于底层或SDK的代码来说,还是需要注意下。

每次都检查,烦

有人表示:我倒是不纠结与性能,但是在一个深度调用链,每个函数都要写一遍参数检查,又繁琐又影响阅读。
我之前也一直在纠结这个问题,觉得很难控制这个度。后来在看Android源码,终于有点想明白了,可以靠函数名传递一些有用信息。(源码真是博大精深,里面有各种精巧的函数设计)
这是在ActivityManagerService启动Activity过程中,调用各种startActivityXXX函数。
下面简单罗列下

1
2
3
4
5
startActivityAsUser()
startActivityMayWait()
startActivityLocked()
startActivityCheckedLocked()
...

这里可以发现,函数中名有Checked和Locked。

checked表示已经检查过了各种权限,Locked表示处于线程安全的情况下。
如果我们需要写几个深度调用函数时,某些可以使用checked字样,就不用重复的做参数检查。

final关键字

对于某些赋值后内容不会改变的变量(读上去有点拗口),可以加上final关键字。
不用每次检查是否为null,直接去检查其构造函数,看看其中没有null检查。

IDE的warning

发展到如今,IDE已经上warning提醒已经做得非常完善。
有时候因为做得过于完善,所以写一上午的代码,IDE啪啪啪列出十几条warning,大家反正懒得去看。就像每个香烟上都写着“吸烟有害健康”,也没见烟民们戒烟不抽。

warning还是有用的,大家要理解IDE作者的良苦用心。

最好是写好小一段代码,就去看看新增的warning,能过滤到一些低级的错误。

断言

有人会说,这些安全性检查和处理,包括一些理论上不应该出现的情况。
本来出现这些情况,可以在开发过程中及时的闪退,在上线前修复。

别担心,语言开发者早就想到了这个问题,祭出了大杀器Assert断言。

而且现在的编译器,在release环境下会去掉断言,真是贴心。

工具检查

有时候难免有疏落,这个时候就需要lint去检查下。
在C/C++程序中,可以使用PC-lint,并且打开Pointer-parameter-may-be-NULL这个开关(+fpn)。选项假设所有传递到函数中的指针都有可能是NULL。

重大错误

如果当程序运行时出现重大错误,核心功能出现问题,程序还是会闪退的。
不要企图靠以上各种方法去续1s命。
这个时候就需要平时好好做好单元测试,以及好好对待测试人员,希望他们发现尽可能多的bug。


总结下,这篇文章核心就是

“对象使用前,要检查下其合理性”。

当然检查的力度,是一个仁者见仁智者见智的问题。
一千个人读者有一千个哈姆雷特,有代码洁癖者,也有收放自如的高手。
这里面没有谁的策略更优。

对自己来说,写的顺手bug少,最关键。

12
corresponding

corresponding

12 日志
7 分类
12 标签
GitHub
© 2018 corresponding
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.3
访问人数 总访问量 次