优秀程序员的天性就是好奇,软件是怎么运作的、屏幕是如何显示的、桌面窗体为何能如此人性化的被鼠标拖动?如果你经常会有这样一些问题迸发在脑海中,恭喜你,你是一名很有潜力的程序员。
我在大学读的是自动化专业,属于电子类,再者对计算机相当感兴趣,第一次看到这玩意时,就觉得这东西太神奇了(其实当时只要看到有屏幕的东西,都觉得很神奇)。硬件+软件的深入让我直接打通了了解这一神秘机器的任督二脉。软件我不是最牛逼的,硬件其实我很歇菜。但是透过硬件看软件估计我还有那么一点点发言权。下面相关内容是个人小小的感性认识,希望能对大家有一定启发。因为个人从毕业至今做了约2年安卓,所以就拿安卓来看吧。
安卓属于操作系统(以下简称os),运行于硬件之上,os上可搭载app,app基于os暴漏的api编写。编写安卓程序时,你会看到程序引用了android.jar(这玩意在"ad目录\sdk\platforms\android-17"下,约18.2M),程序之中用到的大部分api都在这里(如:activity),但是一个奇怪的现象时,编译的apk中并没有这玩意,一个简单的demo编译的apk只有几M。但是如果引用第三方类库(如libs中导入jar包、引用外部包)时,编译的apk中就会包含该jar包,并且体积增大。这是因为android.jar中的代码已经存在于操作系统内了(不一定叫android.jar,且分散在多个jar中),而写代码时用的android.jar主要作用是用来做编译时检查、支持代码提示、跳转至源码,android.jar就是一个编程范本。ok,编写完最小demo后,就可以运行在安卓手机上了,一小段代码即可显示很丰富的界面、按钮、点击处理。但是这一小段代码脱离os是无法运作的,简单的效果背后是庞当的体系在作支撑。
比如显示,xml编写的布局文件,运行时会被翻译成java,类、属性、结构层次一一对应(所以xml写的东西一般能完全用java替代)。显示的视图大部分基于View,View.draw负责该View的全部绘制,你所看到的按钮的默认背景以及点击效果是系统自带的png图片。但是View.draw到底是谁调用呢,如果打断点调试,会发现层次非常的,从外向内调,很容易看晕(断点找机制这种事要长期磨练,非一日之寒)。断点能看到最终发起者是Choreographer(这玩意负责"绘制/事件"等的"发起"),那Choreographer又是被谁驱动去"发起绘制"的呢,ActivityThread,ActivityThread是apk每帧运作的根源。了解GUI编程的同学知道,程序看是没有运作,但是可以响应点击,是因为app在一直监视着你,这种监视叫"消息循环"(说白了,就是个死循环),这个死循环一直在判断有没有消息(你可以简单的把它理解为一个标志位)到来(比如点击),如果有有就分支回调相应处理函数。
ok,Choreographer是如何被驱动的就又清晰了一步,有人向ActivityThread的消息循环的投了一个"绘制消息"。但是这个消息是哪来的呢。程序第一次运行时,会主动抛一个(你可以假想在程序初始化中抛),运行时,各种点击、动画都会抛(类似view.invalidate的东西),触发重新绘制。
ok,绘制触发的根源大概也追踪到了一定程度。但是视图是如何显示在屏幕上的呢,虽然view.draw中可以很清晰的看到用canvas画了写什么东西(如最常用的canvas.drawBitmap),但是canvas的这些draw到底是怎么传送到屏幕上的呢。这一段根源我暂时还没探究过源码,但是毕竟搞过arm,手动驱动过显示屏,这里只做大致猜测,基本原理应该差不到哪去。
基本思路是,app绘制→os→驱动→硬件,哈哈,貌似所有和硬件相关的最终机制都是这样。驱动是直接和硬件打交道的,一般是汇编或c编写,直接控制硬件接口,一般被称为hal(硬件抽象层)。os作用中间桥梁。canvas含一块bitmap(对应一块内存),canvas的各种draw会形成像素矩阵(屏幕像素点的描述)存在bitmap内,经过os传递给驱动,驱动直接驱动屏幕。此间的细节代码、机制相当繁琐。但理论上,图像描述在内存中,基本上就可以显示出来了,那bitmap这块内存是如何会显示出来的呢?
有一块内存叫"显存"(是不是很熟悉,台式机的显存一般可放在显卡中,其实放哪都是块内存),顾名思义,显存就是用来显示的内存。基本原理是,驱动用两个for(行列)扫描显存,读取每个像素,将单个像素信号设置到对应屏幕的一个点上。两个for就是一个面了。但是这单个像素信号是怎么设置到屏幕单点上的呢?这里我们可以在将单点简化为"电灯泡",只有两种颜色(白+黑),灯泡接在主板的一个io接口上,io口上电,灯就亮。断掉,灯就黑。驱动的代码根据显存中单点数据,控制io接口的上断电即可。ok,复杂一点,现在的屏幕颜色是RGB(0xffffff表白色,ARGB中alpha只是用来给多个层运算混合,最终显示的还是RGB)。一个屏幕点其实由三个点(红/绿/蓝)组成(贴近你家的电视会看得比较清楚)。每个点的强弱从不亮到最亮划分等级,用一个字节表示(0x00~0xff,即0~255,共256级),一个点接8个io接口就可以控制了,驱动控制8x3个io口就可以显示一个点的所有颜色了。
是不是清晰许多了。但是一个屏幕有这么多点(1024x768=69632),芯片虽小,但也不可能有这么多接口吧。玩过嵌入式的同学知道,一般接一个显示屏,应该不会超过50根线(应该比这少的多,记得不是很清楚)。刚才说过驱动的绘制一般是用两个for一个点一个点的扫的,所以理论上暴漏一个点的接口(8x3个)就可以了(实际稍有差别),但是屏幕怎么知道这一个点是绘制在哪一个实际点上呢?主板、屏幕通过约定好的时序同步进行就可以了(加几条同步信号接口,表示哪行哪列)。
这次大概就漫游到这里吧,穷追不舍不是为了死砖牛角尖,而是一步步验证自己的思路,锻炼自己的左脑。安卓中结构最好的源码莫过于andorid-os,那是凝聚力多少思维的结晶。