Android杂项积累

平时工作内容基于安卓系统,确并不直接参与安卓部分开发,而只是为安卓层提供natvie方法的调用界面、实现以及再往下的BSP相关的驱动开发。但是总感觉理解安卓系统的一些重要特性也是有必要的。本文仅记录自己对安卓框架原理上的理解,无实用性。

1、Framework

使用java语言做终端开发的人大致分为两类:应用开发与framework开发。应用开发总的来说是使用安卓系统提供的原材料与框架编写特定需求的app,假如某些功能framework并未提供,例如你的产品需要使用一款奇葩的传感器,原生framework没有提供对应的api来操作它,那么就需要在有人在原有框架中添加通路,向上为应用开发工程师提供传感器的操作函数java或者叫虚拟机内的函数,向下打通到c实现的针对该传感器的linux驱动调用,这些人做的就是framework开发。
在这里插入图片描述
上图中展示了framework提供的所有功能框架,大部分是java代码实现,肯定也都是虚拟机这个容器内的框架了。例如安卓四大组件中的服务与activity都在这里实现
Framework定义了客户端组件和服务端组件功能及接口。包含3个主要部分:服务端,客户端和Linux驱动。

一:服务端

服务端主要包含两个重要类,分别是WindowManagerService(Wms)和ActivityManagerService(Ams)。Wms的作用是为所有的应用程序分配窗口,并管理这些窗口。包括分配窗口大小,调节各窗口的叠放次序,隐藏或显示窗口。Ams的作用是管理所有应用程序中的Activity。

除此之外,在服务端还包括2个消息处理类:
1)KeyQ类:

该类为Wms的内部类,继承于KeyInputQueue类, KeyQ对象一旦创建,就立即启动一个线程,该线程会不断地读取用户的UI操作消息,比如按键、触摸屏、trackball、鼠标等,并把这些消息放到一个消息队列QueueEvent类中。

2)InputDispatcherThread类:

该类的对象一旦创建,也会立即启动一个线程,该线程会不断地从QueueEvent中取出用户消息,并进行一定的过滤(用Wms中相应函数处理),过滤后,再将这些消息发给当前活动的客户端程序中。

二:客户端

客户端主要包括以下重要类:

1)ActivityThread类:
该类为应用程序的主线程类,所有的Apk程序有且只有一个ActivityThread类,程序的入口为该类中的static main()函数。

2)Activity类:
该类为APK程序的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread主类会根据用户操作选择运行哪个Activity对象。

3)PhoneWindow类:
该类继承与Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。

4)Window类:
该类提供了一组通用的窗口(Window)操作API, 这里的窗口仅仅是程序层面上的,Wms所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是Wms所有管理的一个窗口。Window是一个abstract类型。

5)DecorView类:
该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration, 即“修饰”的意思,DecorView就是对普通的FrameLayout进行了一定的修饰,比如添加一个通用的TitleBar, 并响应特定的按键消息等。

6)ViewRoot类:
Wms管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现方式就是使用Handler, ViewRoot就是继承于Handler,其作用主要是接收Wms的通知。

7)W类:
该类继承于Binder, 并且是ViewRoot的一个内部类。

8)WindowManager类:
客户端要申请创建一个窗口,而具体创建窗口的任务是由Wms完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和Wms进行交互,客户端不能直接和Wms进行交互。

2、JNI

上一节提到java在虚拟机的包装下可以跨平台运行(即虚拟机屏蔽了软硬件的差异),但是某些驱动程序必须使用c语言进行编写,那么framework就需要提供一种方法实现这一需求。当然这并不是安卓系统中需要面对的问题,任何采用java开发应用程序都需要提供一种本地库的调用机制。
JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。具体JNI技术使用的例子可以参考:

但是要注意,JNI不是什么IPC技术,无论是JNI的Java调用者还是C++世界的实现,虽然跨越了JVM(JVM内的Java与C++的内存管理截然不同),但是这都是编译的东西,整个过程只会有一个进程,在安卓系统那就是JAVA写的activity、service所依附的进程,而C++部分只是一个so的形式,调用时是被重定位进其它进程中使用的。所以说,JNI只是一种接口规范,为JAVA语言调用c++接口提供一种跨越虚拟机的路径,当然没有framework,java可以通过JNI调用C++,如果有framework,那么java则需要先实用binder联系到framework提供的对应服务,再由服务通过JNI间接调用c++实现的native方法,这两者方法的实现步骤可总结为:

  • java->jni->c++
    1、C/C++完成linux设备节点的打开,读写操作,并向java对应类注册实现的native方法,编译成动态库,供app加载使用
    2、java编写一个类,用于在实例化时,加载已经编译好的C动态库(这里需要将so打包到apk中)
    3、app UI类在放生事件后,通过上面的类,调用本地方法

  • java client->binder -> framework service-> JNI->c++

    1、新建服务如何声明(LedService):
    新建一个java class (编译aidl文件,编译生成接口类),接触该接口类,编写自己的class
    2、新建服务注册:
    修改已有文件SystemServer,启动时实例化新加服务类(实际为向系统注册服务)
    3、编写本地方法对应的jni接口(com_android_server_LedService.cpp)
    封装jni接口
    4、编写本地方法实现(led_hal.c)
    具体的设备节点操作
    5、修改已有文件onload.cpp
    添加第三步实现的jni接口中的注册函数,完成本地方法的注册
    6、修改app
    iLedService = ILedService.Stub.asInterface(ServiceManager.getService(“led”));
    意思是通过binder到sevice_manager中查找服务接口(led为SystemServer中添加服务的名称)

3、SDK/NDK

android sdk (Android Software Development Kit, 即android软件开发工具包)可以说只要你使用java去开发Android这个东西就必须用到。他包含了SDK Manager 和 AVD Manage,对于android系统的一些开发版本的管理以及模拟器管理。它只能运行纯java程序,有了它模拟器才可以使用。

而ndk (Native Development Kit)跟sdk差不多的是它也是一个开发工具包。用它开发c/c++是很方便的。他有一个强大的编译集合。Java调C、C++(jni接口),是一些java如何调C的代码。它会把C代码编译成一个.SO的动态库,通过jni接口用java代码去调用它,有了它我们可以直接在android代码中去添加C代码。

很早以前android是只有sdk的。并没有ndk。这就意味着一旦android的开发者要使用c/c++的三方库或者需要用到c/c++就必须使用非官方的法子,用java的jni去调用c/c++。就像耍小聪明走后门一样。而ndk的出现就意味着jni调用的这种方法转正了变成官方了以后你不需要再走后面大路正面随你走。如果要操作底层直接操作内存,操作地址那不得不去使用c/c++因为java这块想做这些有点困难。所以ndk是必须需要出现的。对于android来说sdk和ndk是同种语言的2种不同时期的必须品。
基于NDK使用jni
在这里插入图片描述

4、Binder

安卓系统中进程间通信的最主要的方式就是IPC,每一个服务均运行在一个独立的进程中,因此是以Java实现,所以本质上来说是运行在一个独立进程的Dalvik虚拟机中。那么问题来了,开发者的App也运行在一个独立的进程空间中,如果调用到系统的服务层中的接口?答案是IPC(Inter-Process Communication),进程间通讯是和RPC(Remote Procedure Call)不一样的,实现原理也不一样。每一个系统服务在应用框架层都有一个Manager与之对应,方便开发者调用其相关功能,具体关系如下:
在这里插入图片描述
Android 从下而上分了内核层、硬件抽象层、系统服务层、Binder IPC 层、应用程序框架层
Android 中"应用程序框架层"以 SDK 的形式开放给开发者使用,“系统服务层” 中的核心服务随系统启动而运行,通过应用层序框架层提供的 Manager 实时为应用程序提供服务调用。系统服务层中每一个服务运行在自己独立的进程空间中,应用程序框架层中的 Manager 通过 Binder IPC 的方式调用系统服务层中的服务。
安卓的驱动核心是linux,因此Binder的实现也离不开linux驱动的协助,按照代码分层的概念来看Binder应该是这样的:
在这里插入图片描述
可以看到,虽然安卓系统极力在弱化进程的概念,只暴露给用户组件的使用方式,但是组件间(并不仅仅值JVM中的应用,linux下的应用程序也是如此)的通信同样属于Linux中的IPC,另外要注意到Binder属于一种偏基础的功能,后面提到的AIDL\HIDL都是基于Binder实现的RPC。仔细观察Binder的实现,如果要通过ioctl来使用Binder相关驱动,势必涉及到JVM内接口到Native的调用,那么就会用到JNI,因此实际的Binder代码结构如下(此图仅展示参与Bindr通信的一个安卓服务进程的函数调用,当然若进程装载的是一个linux应用,则没有Java与JNI部分):
在这里插入图片描述
Binder详解

5、HIDL

官话:

HAL接口定义语言(简称HIDL)适用于指定HAL和其用户之间的接口的一种接口描述语言(IDL)。HIDL允许指定类型和方法调用。从更更烦的意义上来说HIDL适用于在独立编程的代码库之间通信的系统。HIDL旨在用于进程间通信(IPC)。进程之间的通信经过Binder化。对于必须与进程相关联的代码库,还可以使用直通模式。

其实HIDL目的就一个:利用binder进程通信方式,将原来通过JNI调用Vendor提供的So的方式进行解耦,任何System下的应用进程要想使用Vendor下的接口,必须通过IPC打通。这一做法的好处就是安卓Framework下的进程与Vendor提供的服务区分为完全隔离的两个进程。
在这里插入图片描述
早在Android提出HIDL之前,某电视厂商在Vendor的Hal与Framework之间就有添加一层中间件代码,此时的Hal调用并非简单的JNI,具体过程如下:

  1. 中间件生成的进程负责实际调用Hal接口,并且提供Binder通信中的Client与Server库。
  2. Framework下的服务则是先通过JNI到达Native层,Native层再搞一个TVService服务去dlopen中间件编译出的Binder客户端,利用Binder通信到达服务端所在进程,即中间件进程,最终完成Hal的调用

安卓引入Treble后,无论你是中间件代码还是芯片厂家代码,都被分配到了Vendor分区,System分区的进程无法直接调用Hal接口,虽然某电视厂商的设计构架中是通过原生Binder实现的,但是就连Binder的调用也被禁止了,所以只能适配最新的HIDL,将原生的Binder直接调整为HIDL封装过的Binder,各进程关系保持不变。具体HIDL的使用方法可以参考:
Binder死磕到底(四):Treble化架构

6、AIDL

类似于HIDL,AIDL也是一种接口规范,主要用于JAVA世界内进程的通信,在Treble下则是system分区内进程(进程代码位于System分区)的通信规则。实际上AIDL仍然是Binder机制的包装,安卓只是提供了一套开发环境与规则,方便JAVA程序员更加快捷的基于Binder机制完成进程间的通信。至此,AIDL别无他妙,附注一个实现例子Android AIDL使用详解在这里插入图片描述

7、class文件、jar包、APK是什么鬼

  • Java语言的一个非常重要的特点就是与平台的 无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(class文件),就可以在多种平台上不加修改的运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的及其指令执行。这就是Java能够“一次编译,到处运行”的原因。
    在这里插入图片描述
  • jar包,从名字来看与tar命令类似,实际上它的确是一种压缩文件,不过在其中添加了必要的元数据,就可以被java文件无需解压即可引用。当然这里说的是源码级的引用,就像C原因的.h文件。
  • APK其实就是一个ZIP文件,修改APK的文件格式为.zip,使用解压缩工具就能显示里面的内容,所以说APK是安卓定义的一种将class文件、第三方库、配置文件按照特定规则打包的产物。
  • AndroidManifest.xml,App的项目全局配置文件,生成的APK中该文件为二进制格式,可以使用apktool工具反编译查看原文件
  • lib, native 库文件;里面存放SO文件供app层调用;lib文件夹中有针对各种硬件架构设计(CPU)的文件夹,如X86,AMR-V7a,ARM;
    那我们是不是要针对各种价格写不同的文件夹尼?答案是否;在加载SO的过程中,Andriod系统首先会读取和手机硬件相关的文件夹,如果没有找到则会在兼容性文件夹中对应SO;
    如一个ARM-V7A的手机,在读取SO的时候,如果在Lib/AMR-V7a文件夹中没有找到对应的SO,则会在ARM文件夹中寻找;
    这点和android读取drawable文件还是很相似的。
  • assert,一般用于存放音视频频,字体等文件,应用中该目录存放什么,生产APK之后还是什么,不会发生变化
  • META-INF , 该文件主要用于保证APK的完整性,里面的文件结构如下
    这里写图片描述
    ··MANIFEST.MF:这个文件保存了整个 apk 文件中所有文件的文件名 + SHA-1后的 base64 编码值,使用notepadd++打开即可查看
    这里写图片描述
    ··RSA文件:这里之说RSA文件,但是没有指明MGame.RAS,是因为各家产生会对APK进行壳处理,修改这里的RAS名称;这个文件保存了公钥和加密方式的信息
    ··SF文件:这个文件与 MANIFEST.MF 的结构一样,只是其编码会被被私钥加密。使用notepadd++打开即可查看
    综述:如果你的APK被别人修改了,但是没有私钥生成 CERT.SF,就无法完成校验,即安装失败
  • res,资源文件,存在这个文件夹下的所有文件都会映射到Android工程的.R文件中,生成对应的ID,访问的时候直接使用资源ID即R.id.filename; 包含图片,布局,动画等,其中布局文件,动画文件等XML会被编译成二进制文件,可以使用apktool工具反编译查看原文件,图片文件可以查看
  • resource.arsc,App的资源索引表,编译后的二进制资源文件;
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210315085416606.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1ZGFzaGVpMg==,size_16,color_FFFFFF,t_70

8、 奇葩的内存管理

  • java里无论是new一个对象还是创建一个临时变量,都只是获取该变量存放的堆地址(类似于指针)。并不跟C++一样,区分堆与栈的不同使用场景,不过好处是内存是由JVM管理,程序员不需要手动管理
  • java中方法参数传递方式是按值传递:
    如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
    如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。

Android不仅在业务上为应用提供众多实用的组件与服务,在编程技术上也是想破脑袋。对于C++程序员来说,内存管理从来都是既方便又麻烦的事情。而在Android 中可以广泛看到的template class Sp 句柄类实际上是android 为实现垃圾回收机制的智能指针。智能指针是c++ 中的一个概念,因为c++ 本身不具备垃圾回收机制,而且指针也不具备构造函数和析构函数,所以为了实现内存( 动态存储区) 的安全回收,必须对指针进行一层封装,而这个封装就是智能指针,其实说白了,智能指针就是具备指针功能同时提供安全内存回收的一个类。

sp: 实现自己的类MyClass继承RefBase,在堆上分配一个该类,用指针p指向这块地址,再用p实例化一个sp,那么sp就会动态修改
MyClass::RefBase中的引用计数,其他地方在实例化另外一个sp指向相同内存地址,也会维护引用计数,当有sp析构时则会对引用计数减1
,继而在RefBase的析构中判断引用计数是否为0。若为0,则delete指向的堆内存。这样可利用析构函数自动判断是否该delete内存

wp: wp类似sp,但是维护的引用计数成为弱引用,默认配置下,弱引用计数不会控制实际对象的delete,只会delete影子对象。
因此,wp指向的对象,仅仅用作记录,真正要访问该对象内存时,需要先promote为sp,如果实际对象已经delete,那么promote会
失败,这样可确保防止错误的内存访问。

9 、四种进程间通信

广播介绍
由于应用程序之间不能共享内存。在不同应用程序之间交互数据(跨进程通讯),在Android SDK中提供了4种用于跨进程通讯的方式。这4种方式正好对应于android系统中4种应用程序组件:Activity、Content Provider、Broadcast和Service。其中Activity可以跨进程调用其他应用程序的Activity;Content Provider可以跨进程访问其他应用程序中的数据(以Cursor对象形式返回),当然,也可以对其他应用程序的数据进行增、删、改操 作;Broadcast可以向android系统中所有应用程序发送广播,而需要跨进程通讯的应用程序可以监听这些广播;Service和Content Provider类似,也可以访问其他应用程序中的数据,但不同的是,Content Provider返回的是Cursor对象,而Service返回的是Java对象,这种可以跨进程通讯的服务叫AIDL服务。

Activity

Activity的跨进程访问与进程内访问略有不同。虽然它们都需要Intent对象,但跨进程访问并不需要指定Context对象和Activity的 Class对象,而需要指定的是要访问的Activity所对应的Action(一个字符串)。有些Activity还需要指定一个Uri(通过 Intent构造方法的第2个参数指定)。

在android系统中有很多应用程序提供了可以跨进程访问的Activity,例如,下面的代码可以直接调用拨打电话的Activity。

Intent callIntent = new  Intent(Intent.ACTION_CALL, Uri.parse("tel:12345678" );  
startActivity(callIntent);
Content Provider

Android应用程序可以使用文件或SqlLite数据库来存储数据。Content Provider提供了一种在多个应用程序之间数据共享的方式(跨进程共享数据)。应用程序可以利用Content Provider完成下面的工作

  1. 查询数据

  2. 修改数据

  3. 添加数据

  4. 删除数据

    虽然Content Provider也可以在同一个应用程序中被访问,但这么做并没有什么意义。Content Provider存在的目的向其他应用程序共享数据和允许其他应用程序对数据进行增、删、改操作。
    Android系统本身提供了很多Content Provider,例如,音频、视频、联系人信息等等。我们可以通过这些Content Provider获得相关信息的列表。这些列表数据将以Cursor对象返回。因此,从Content Provider返回的数据是二维表的形式。

广播(Broadcast)

广播是一种被动跨进程通讯的方式。当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据。这就象电台进行广播一样,听众只能被动地收听,而不能主动与电台进行沟通。
在应用程序中发送广播比较简单。只需要调用sendBroadcast方法即可。该方法需要一个Intent对象。通过Intent对象可以发送需要广播的数据。

Service

1.利用AIDL Service实现跨进程通信,这是我个人比较推崇的方式,因为它相比Broadcast而言,虽然实现上稍微麻烦了一点,但是它的优势就是不会像广播那样在手机中的广播较多时会有明显的时延,甚至有广播发送不成功的情况出现。

注意普通的Service并不能实现跨进程操作,实际上普通的Service和它所在的应用处于同一个进程中,而且它也不会专门开一条新的线程,因此如果在普通的Service中实现在耗时的任务,需要新开线程。

要实现跨进程通信,需要借助AIDL(Android Interface Definition Language)。Android中的跨进程服务其实是采用C/S的架构,因而AIDL的目的就是实现通信接口。

10 、安卓下的线程

对于耗时处理,不能放在Activity的主线程中做,所以开辟子线程是最容易想到的事情。
Android系统存在多个层面的线程接口:

  • 例如系统自身提供的一些具有线程属性的组件:Handler;Message;MessageQueue;Looper;HandlerThread都是多线程组件,当然他们都是通过实例化Thread类实现的
  • Java语言线程类Thread提供的线程实现,所有的apk都用这种方式开发
  • 在Native的应用则是使用Linux线程模型 Pthread来进行多线程开发

从分层来看,Android的framework或者使用Java写的APk,都运行于Linux之上,自然上层的各种形式的线程实现全部由Linux提供,所以它们都是基于C库的pthread来作为底层实现的(早期Java有所谓的green thread,它由运行环境或虚拟机(VM)调度,而不是由本地底层操作系统调度的线程。不过目前已弃用)。

所以Andorid下的多线程开发时,看你是在虚拟机还是native层开发,当然不管那一层其本质都是pthread提供实现,再往底层说的话,都是linux驱动来实现,只不过越接近上层封装的越便捷方便,同时也更加的不利于细节的展示。

11 、安卓启动

https://www.jianshu.com/p/9f978d57c683

1. init
首先linux的uboot引导程序拉起整个Linux os的kernel层,kernel的start_kerenl()函数会完成一系列Linux系统级的初始化工作,包括 :

  • 创建异常向量表和初始化中断处理函数
  • 初始化系统核心进程调度器和时钟中断处理机制
  • 初始化串口控制台
  • 创建初始化系统cache,为各种内存调用机制提供缓存,包括动态内存分配,虚拟文件系统(VirtuaFile System)及页缓存。
  • 初始化内存管理,检测内存大小及被内核占用的内存情况。
  • 初始化系统的进程间通信机制(IPC);当以上所有的初始化工作结束后,start_kernel()函数会调用rest_init()函数来进行最后的初始化,包括创建系统的第一个进程——init进程来结束内核的启动,从此以后用户进程正式工作
  • 挂载最小根文件系统,用于驱动程序工作以及init进程的启动
    在这里插入图片描述

2. zygote
init进程仍属于linux系统,当它通过init.rc去fork安卓的始祖(既是盘古也是女娲,因为后面zygote会产生其它进程还会响应其它进程的召唤)进程时,才会逐渐拉起安卓的运行环境,例如system_server进程、虚拟机等。拉起zygote的init入口函数就是service_start()。整个zygote初始化的过程如下图(一半native 一半java),关键工作包括:

  • 调用native接口,创建虚拟机
  • 加载资源、共享库等
  • 注册关键JNI函数,供java世界使用
  • 使用jni调用java函数,进入java运行环境
  • 创建AF_UNIX socket服务端,供zygote与其它进程通信
  • fork并启动system_server进程,这个进程是所有系统service的大本营
  • 睡眠等待子进程的socket请求

在这里插入图片描述
3. system_server
system_server主体已经完全运行于java世界,他在运行伊始需要完成如下几步工作:

  • 启动线程用于Binder通信
  • 启动各种系统服务:电源管理、看门狗、WindowManager…
  • 加入Binder进入循环等待
  • 启动Launcher
    在这里插入图片描述
    注意,当应用要启动某个组件,例如起一个Activity,会首先利用Binder调用system_server的ActivityManager服务端实现,由system_server再通过Socket将该请求传递到Zygote,所以说真正安卓中虽然apk不了解一个Activity到底是啥,但实际上它确实是一个进程,而且一定是Zygote的子孙!
    在这里插入图片描述

12 、Android NDK vs AOSP Build System

前言
最近自己一直在做有关 Android 系统源码底层的开发,就经常接触到Android NDK和AOSP(Android Open Source Project) Build System这两个东西,但是由于他们两者都可以将C/C++代码编译成可执行文件或者动态链接库,导致我经常将这两者弄混淆了。所以,痛定思痛,不想再被这种似四而非的感觉折磨了,今天就抽空写下这篇文章来捋清楚两者之间关系。

Android NDK

Android NDK 是什么?

Android NDK 本质上是一套交叉编译工具集,它可以将 C/C++ 源码编译成适用于不同硬件平台的库文件和可执行文件,而这些库文件和可执行文件可以被上层的基于 Java 语言编写的 APP 加载调用,从而实现了 C/C++ 源码在 APP 中的复用。

Android NDK 应用实例

例如,在图像处理中我们常用的OpenCV库就是使用 C++ 编写的,如果我们想在我们使用 Java 开发的 Android APP 中使用 OpenCV 库中的一些处理函数,那么该怎么办呢?
当然,你可以直接去找基于 Java 实现的 OpenCV 的 jar 包,然后去调用对应的函数,但是这种Java 实现版本的 OpenCV 在处理的效率上肯定不及 C++ 实现版本的 OpenCV(尤其是在做图形处理方面)。
所以,另外一种方法就是通过 Android NDK 工具将 OpenCV 的代码编译成指定硬件平台的库文件,然后在 Android APP 进程中通过JNI的方式来使用 OpenCV 中提供的处理函数,实现自己想要的某种功能。

Android NDK 编译系统

Android NDK 编译系统其实本质上就是一系列的交叉编译工具链,而 NDK 中所使用的编译脚本ndk-build就是根据编译配置文件Android.mk和Application.mk来调用这些交叉编译工具链中的工具编译生成指定 ABI 平台下目标链接库文件或者可执行文件。

这里我觉得还是有必要多费点文字对 Android NDK 包中的文件及目录的内容进行一个说明,以便大家对 NDK 有更加深一步的理解。NDK 包中的文件及目录结构如下所示:

a@a:~/Android/NDK/android-ndk-r12b$ tree -L 1
.
|-- build
|-- CHANGELOG.md
|-- ndk-build
|-- ndk-depends
|-- ndk-gdb
|-- ndk-stack
|-- ndk-which
|-- platforms
|-- prebuilt
|-- python-packages
|-- shader-tools
|-- source.properties
|-- sources
`-- toolchains
7 directories, 7 files
  • build:该目录下包含了 ndk-build 编译脚本中所使用到的各种小的脚本文件,例如我们在 Android.mk 中常见的将源码编译成共享链接库文件的语句:include $(BUILD_SHARED_LIBRARY)中的BUILD_SHARED_LIBRARY对应的就是一个编译脚本文件:/build/core/build-shared-library.mk。
  • ndk-build:NDK 的核心,NDK 编译所使用的最主要的编译脚本。
  • ndk-gdb:调试 NDK 编译出来的库的工具。
  • ndk-stack:对 Android 系统运行时共享链接库崩溃时产生的tombstone文件进行解析的工具。它在调试异常崩溃 Bug 时定为到具体出错的源码位置非常有帮助,具体使用方法详见我的这篇博客:Android NDK -Tombstone/Crash 分析
  • platforms:这个文件夹里保存的是各个 Android 系统版本下不同硬件平台对应的 NDK 提供的系统共享链接库文件,例如:liblog.so, libdl.so, libc.so等等。
  • source:这个目录下主要包含的就是 NDK 所中的C++ STL库的源码,有gnu_stl的实现,也有llvm-stl的实现。
  • toolchains:这个目录也是非常关键的一个目录,它里面包含了不同目标硬件平台下面的交叉编译工具,可以将 C/C++ 库编译成可运行在arm,x86,x86-64,mips等不同硬件平台上面。

这里就不对Android.mk编译配置文件的编写方法展开说明了,有关内容可以参见下面这篇文章:Mastering Android NDK Build System - Part 1: Techniques with ndk-build和Android.mk

Android NDK 中系统提供的共享链接库

有时我们的自己编写的源码中除了实现某种特定的功能之外,可能还会在C/C++代码中使用到日志打印输出函数,而这个日志打印函数就位于 Android NDK 中 Android 系统提供的liblog.so库中。除了 liblog.so 库之外,NDK 还提供了下面这些系统共享链接库供我们自己的源码进行加载调用:

a@a:~/Android/NDK/android-ndk-r12b/platforms/android-22/arch-x86/usr/lib$ ls -al
total 10996
drwxr-xr-x 2 woshijpf woshijpf    4096 Jun 15  2016 .
drwxr-xr-x 4 woshijpf woshijpf    4096 Jun 15  2016 ..
-rw-r--r-- 1 woshijpf woshijpf    2204 Jun 15  2016 crtbegin_dynamic.o
-rw-r--r-- 1 woshijpf woshijpf    1992 Jun 15  2016 crtbegin_so.o
-rw-r--r-- 1 woshijpf woshijpf    2204 Jun 15  2016 crtbegin_static.o
-rw-r--r-- 1 woshijpf woshijpf     704 Jun 15  2016 crtend_android.o
-rw-r--r-- 1 woshijpf woshijpf     648 Jun 15  2016 crtend_so.o
-rwxr-xr-x 1 woshijpf woshijpf   10772 Jun 15  2016 libEGL.so
-rwxr-xr-x 1 woshijpf woshijpf   34640 Jun 15  2016 libGLESv1_CM.so
-rwxr-xr-x 1 woshijpf woshijpf   28428 Jun 15  2016 libGLESv2.so
-rwxr-xr-x 1 woshijpf woshijpf   46592 Jun 15  2016 libGLESv3.so
-rwxr-xr-x 1 woshijpf woshijpf    6752 Jun 15  2016 libOpenMAXAL.so
-rwxr-xr-x 1 woshijpf woshijpf    7036 Jun 15  2016 libOpenSLES.so
-rwxr-xr-x 1 woshijpf woshijpf   28876 Jun 15  2016 libandroid.so
-rw-r--r-- 1 woshijpf woshijpf 8814206 Jun 15  2016 libc.a
-rwxr-xr-x 1 woshijpf woshijpf  125464 Jun 15  2016 libc.so
-rwxr-xr-x 1 woshijpf woshijpf    5400 Jun 15  2016 libdl.so
-rwxr-xr-x 1 woshijpf woshijpf    5212 Jun 15  2016 libjnigraphics.so
-rwxr-xr-x 1 woshijpf woshijpf    5436 Jun 15  2016 liblog.so
-rw-r--r-- 1 woshijpf woshijpf 1333352 Jun 15  2016 libm.a
-rwxr-xr-x 1 woshijpf woshijpf   26708 Jun 15  2016 libm.so
-rwxr-xr-x 1 woshijpf woshijpf   18184 Jun 15  2016 libmediandk.so
-rw-r--r-- 1 woshijpf woshijpf  105024 Jun 15  2016 libstdc++.a
-rwxr-xr-x 1 woshijpf woshijpf    5536 Jun 15  2016 libstdc++.so
-rw-r--r-- 1 woshijpf woshijpf  575800 Jun 15  2016 libz.a
-rwxr-xr-x 1 woshijpf woshijpf   11768 Jun 15  2016 libz.so

那么这些系统提供的共享链接库是怎么被加载使用的呢?
例如,我现在写了一个 C++ 源文件hello.cpp,并且在这个源文件中调用了__android_log_print()函数打印日志,那么我就需要在对应的Android.mk文件中加入下面这条语句来显示地链接/system/lib/liblog.so库:

LOCAL_LDLIBS := -llog
那么这些系统共享链接库是如何编译出来的呢?
因为安全性和兼容性问题以及上层应用程序的需求,NDK 提供的只是 Android 系统中一小部分系统共享链接库,并且这些系统共享链接库都是在Android 源码编译时生成的,例如,liblog.so库就是由 Android 源码中 /system/core/liblog/ 目录下的源码编译而来的。而在 $NDK/platforms/android-22/arch-arm/usr/include/android/log.h 头文件中声明的日志打印函数__android_log_write()的实际代码实现就在 Android 源码的/system/core/liblog/logd_write.c文件中。

AOSP Build System

AOSP是Android Open Source Project的简称,接下来用我就用它等价地表示 Android 系统源码。

AOSP Build System 是什么?

AOSP Build System是用来编译 Android 系统,Android SDK 以及相关文档的一套框架。该编译系统主要由 Make 文件(注意:这里的 Make 文件不是 Makefile 文件,而是 Android 编译系统自己构架的一套编译配置文件,通常以*.mk 为文件后缀),Shell 脚本以及 Python 脚本组成,其中最主要的是 Make 文件。

在 Android Build System 中编译所使用到的Make文件主要分为三类:

  1. 第一类是 Build 系统核心文件,此类文件定义了整个 Build 系统的框架,而其他所有 Make 文件都是在这个框架的基础上编写出来的。
  2. 第二类是针对某个产品(一个产品可能是某个型号的手机或者平板电脑)的 Make 文件,这些文件通常位于 device 目录下,该目录下又以公司名以及产品名分为两级目录。
  3. 第三类是针对某个模块的 Make 文件。整个 Android 系统中,包含了大量的模块,每个模块都有一个专门的 Make 文件,这类文件的名称统一为Android.mk,该文件中定义了如何编译当前模块。Build 系统会在整个源码树中扫描名称为“Android.mk”的文件并根据其中的内容执行模块的编译。
    AOSP Build System 所依赖的编译工具
    Android 系统从下到上主要分为下面5层,而每一层所使用的编程语言如下:
  • Kernel:Android 定制化过的Linux Kernel,使用的当然是C语言了。
    HAL:这一层是用户态驱动层,它主要功能是和下层 Kernel 中的硬件驱动程序进行交互,这一层主要使用的语言是C/C++。
  • Frameworks native:这一层是 Android 系统核心组件的实现位置,在这一层中主要通过C++语言来实现。
  • Frameworks java:这一层其实是对 Frameworks native 层套上一层 Java 的外壳,封装成 Android SDK 提供给上层的 APP 开发者进行调用,这一层主要使用了Java语言进行实现。
  • APP:这一层主要面向的是 Android APP 开发人员,并且 Android 提供的 SDK 是基于 Java 语言的,所以 APP 的代码实现也是Java语言。

既然 Android 系统源码中包含了 3 种编程语言,那么在AOSP Build System中肯定也使用了许多编译工具来进行编译(Android 官方推荐使用 Ubuntu 14.04 来对 Android 源码进行编译,所以这里就以 Ubuntu 系统中所使用的编译工具为例):

  • 编译 C/C++ 代码:使用 Ubuntu 14.04 中自带的gcc编译器即可。
  • 编译 Java 代码:在 Ubuntu 14.04 中推荐使用OpenJDK-1.7来进行编译。
  • make 工具:前面提到 AOSP Build System 是基于 make 工具,所以这里也使用 Ubuntu 14.04 中自带的make工具即可。
  • AOSP Build System 编译系统共享链接库的方法
    在AOSP Build System 是什么?小节中,我提到了 Android 系统源码中也使用了Android.mk文件来将某个模块编译成库文件或者可执行文件。

例如,Android 系统源码中的AudioFlinger服务对应使用的是系统中的libaudioflinge.so共享链接库文件 ,该共享链接库的源码实现位于frameworks/av/services/audioflinger,在同一目录下面的Android.mk编译配置文件如下所示:


LOCAL_SRC_FILES:=               \ # 编译该模块所需要使用到的源文件
    AudioFlinger.cpp            \
    Threads.cpp                 \
    Tracks.cpp                  \
    Effects.cpp                 \
    AudioMixer.cpp.arm          \
    PatchPanel.cpp
LOCAL_SRC_FILES += StateQueue.cpp
LOCAL_C_INCLUDES := \
    $(TOPDIR)frameworks/av/services/audiopolicy \
    $(call include-path-for, audio-effects) \
    $(call include-path-for, audio-utils)
LOCAL_SHARED_LIBRARIES := \ # 链接该模块所依赖的共享链接库文件
    libaudioresampler \
    libaudioutils \
    libcommon_time_client \
    libcutils \
    libutils \
    liblog \
    libbinder \
    libmedia \
    libnbaio \
    libhardware \
    libhardware_legacy \
    libeffects \
    libpowermanager \
    libserviceutility
LOCAL_STATIC_LIBRARIES := \ # 链接该模块所依赖的静态链接库文件
    libscheduling_policy \
    libcpustats \
    libmedia_helper
LOCAL_MODULE:= libaudioflinger 
LOCAL_CFLAGS += -fvisibility=hidden #隐藏共享链接库中的符号,使之不被其他共享库所访问
include $(BUILD_SHARED_LIBRARY) # 编译成 libaudioflinger.so 库文件

在 Android 源码树的根目录下运行下面的命令来配置好 Android 源码编译的环境:

$ source build/envsetup.sh
$lunch # 选择自己需要编译的 Android 系统版本
有了 Android 编译环境之后,只需要在将当前的工作目录切换到 frameworks/av/services/audioflinger 目录下来编译AudioFlinger模块

由于 libaudioflinger.so 所需依赖其他的系统共享链接库文件,所以需要先把整个 Android 源码生成这些共享链接库文件
$ mm # 读取当前工作目录下的 Android.mk 文件,编译 libaudioflinger.so 共享链接库文件
注意:虽然咋一看上去 Android 源码中某个模块的编译配置文件Android.mk和 NDK 中所用的编译配置文件Android.mk没有什么不同,但是其实还是有一些细微的区别的,尤其是在使用共享链接库方面。

例如,上面 Android 系统中源码编译出来的libaudioflinger.so库文件中链接liblog.so库文件使用的是LOCAL_SHARED_LIBRARIES编译变量:

LOCAL_SHARED_LIBRARIES := \ # 链接该模块所依赖的共享链接库文件

liblog

而在 NDK 编译自己使用 C/C++ 编写的模块时,如果要链接liblog.so库文件,Android.mk文件中的写法则是:

LOCAL_LDLIBS := -llog
所以,我们可以看出来在AOSP中所有编译出来的系统链接库文件(不管是静态库文件还是共享链接库文件)对AOSP中各个模块都是可见和可以被链接使用的,而对于 NDK 来说它只能通过LOCAL_LDLIBS的变量来链接使用 Android 系统中提供的一小部分系统链接库文件。

相关参考文章
有关 AOSP Build System 更加详细的介绍,可以参考下面的的文章:
理解 Android Build 系统
《Embedded Android》 Chapter 4 – The Build System
Android Build System Ultimate Guide
Establishing a Build Environment

Android NDK 和 AOSP Build System 的差异

前面我们对Android NDK和AOSP Build System做了比较详细的说明,所以在这一小节中就是对两者从下面几个方面进行一个差异对比:

两种编译系统面向的开发人员群体

  • Android NDK它所面对的开发人员群体是APP开发人员,他们想使用C/C++代码来实现某种功能,然后在上层 APP 的Java代码中来通过 JNI 的方式来调用这些函数。例如,一些手机游戏 APP 的开发人员,为了使得游戏运行时画面更加流畅,他们就常常会把这些图像渲染这块耗时和性能要求较高的模块通过C/C++代码调用OpenGLES API函数来实现,然后通过ndk-build编译成共享库文件,然后被上层的 APP 中的 Java 代码加载调用。
  • AOSP Build System面向的开发人员群体则是一些底层操作系统的开发人员,他们需要根据自己的需求和硬件平台的特性对Android源码进行一个定制修改,然后通过AOSP Build System重新编译得到自己想要的 Android 系统的镜像和库文件。

生成的链接库或可执行文件的目的

  • Android NDK生成链接库或可执行文件的目的是为了执行实现上层 APP 层中的需要通过C/C++才能实现的某种功能,还是上面举过的例子,手机游戏 APP 开发人员需要通过C/C++才能实现一些性能要求很高的图像渲染操作。
  • AOSP Build System中生成的链接库文件或可执行文件都是 Android 系统运行起来必须依赖的库文件,非常重要!!!例如,我们前面一直举的liblog.so就是由AOSP Build System编译出来的一个系统共享链接库文件,如果没有这个文件,那么 Android 系统的日志系统就挂了。

系统共享链接库的支持

  • Android NDK中支持用户自己编写的 C/C++ 代码链接调用一部分系统共享链接库文件,例如liblog.so, libOpenSLES.so, libGLESv2.so等库文件,有关 Andorid NDK 支持那些系统库文件,详见 NDK 的官方文档:Android NDK Native APIs
  • AOSP Build System就好比是系统共享链接库的“妈”,既然这些库文件都由它编译出来的,那么AOSP中任意一个模块肯定都可以使用全部的系统共享链接库文件。

13、 Android Native开发方式

Native开发开发,基本是躺平状态,应用提的需求照单全收。但是,躺的姿势也是有区别的,不同的姿势可以让上面的人多出点力或者多享受一些。无论是AOSP或者NDK编译方式都是有两种可选的Native开放套路:

  1. 打包so库及jar包供他人使用

Android的NDK开发相信各位已经精通各种姿势了。不过基本上都是那种native代码和java代码都在同一个工程中,因为应用从头到脚都是我们自己的,也不需要分离。但有时候可能需要我们自己把某些库打包起来供别人使用,或者使用别人提供给我们的库。本篇文章及下篇文章就讲讲so库如何打包。
参考

  1. 通过C/C++调用第三方so库,纯Native开发

打包so库及jar包的博客我讲了如何将自己的代码打包成so库,并且配合jar包供他人调用。但那种方式仅适合对方从java层调用,如果算法是比较核心的,而又为了效率必须从native来调用,那种方式就不合适了。本篇讲如何打包我们自己的核心代码供他人在native调用,如果对方愿意,也可以自己封装然后从java来调用,灵活性更高。并且在调试的时候更加方便。这种方式是更接近纯C/C++工程的集成方式。
参考

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值