android基于linux,所以当设备上电后,初期的启动流程跟linux系统并无二致,uboot引导,接着载入kernel代码,加载各种驱动,结束后,启动第一个用户级进程init,init接着解析init.rc后,按照配置启动各种linux后台进程。
整个linux系统底层已经Ready后,接着要干嘛,当然是启动android引导进程,装载android运行环境所需的代码和资源。
Android的引导进程是Zygote,我们可以在init.rc中找到她,Zygote运行后,会创建davlik虚拟机,加载framework.jar等代码和资源;Zygote是所有android进程的父进程,这里的android进程是指需要共享dalvik虚拟机的进程。Zygote初始化结束后,会fork出她的第一个子进程system_server,即android的核心服务进程,很多轻量级的service都跑在这个进程中,这些轻量级的服务当中,包含了本章所要详细描述的PackageManager service(后续简称PMS)。
PMS,顾名思义,一个负责安装包管理的服务,它在systemserver起来后被创建并完成初始化,它是整个android的基础,如果PMS出问题了,那整个android运行会乱掉。
本文将会简单介绍PMS的整个实现原理,由于本人水平有限,文中肯定会存在描述不准确或者错误的地方,欢迎指正!
1:APK文件结构概述
Android的安装包叫apk(android package),在介绍怎么管理它之前,肯定要先了解它的内部结构,要不在后续看PMS的相关代码时,会一头雾水。
APK文件就是一个zip压缩文件,用解压缩软件可以看到其内部结构如下:
1) META-INF文件夹主要包含APK内所有文件的摘要清单,清单签名数据和公钥证书
2) resources.arsc和Res文件夹,resources.arsc包含APK全部资源索引数据,Res文件 夹则包含所有的资源文件
3) AndroidManifest.xml文件,包含APK的配置清单数据
4) Classes.dex文件,APK执行代码
1.1 资源文件相关
我们如果将APK文件解压,接着查看内部文件内容,会发现除了assets目录下的,其他所有XML文件都会被转换成二进制文件,android在打包的时候做了什么?这么做的目的是什么?
简单描述如下:
1) 分配资源ID:
Android资源ID是一个4字节的无符号整数,最高字节表示PackageID,次高字节表示TypeID,最低两字节表示EntryID;所以,当我们拿到资源ID时,我们就知道资源所属包,以及对应的资源类型和入口ID。所有的资源ID最终都生成到R.java。
2) 资源索引组织管理:
有了资源ID当然不够,资源ID对应的数据是什么?所以还需要资源索引描述表,这样才可以通过资源ID,快速的找到最匹配的资源数据;
所有分配的资源ID都会被按照指定的数据结构重新组织并连同资源数据保存到resources.arsc中。
resources.arsc是android资源数据的核心,有了它,程序在运行时,就可以根据当前的各种环境,比如language,screensize等等来快速定位最匹配的资源;同时由于资源数据的ID化,又可以保证资源数据最大可能的复用,从而减少APK的大小。
3) XML文件重新格式化:
试想下,如果对AndroidManifest和各种布局文件,如果都是通过纯文本解析,那效率肯定是无法接受的,还有一点,文件中的各种关联数据的值如何获取?比如drawable,string等等。在上面已经描述过,resources.arsc包含了所有数据的ID和对应值,所以我们只需要根据xml内部数据,重新定义数据结构,并将xml各种值元素的key和value替换成resources.arsc中对应的id值,接着将结构化数据存储到同名文件中即可,这就是我们再在包后看到的二进制数据。
接着介绍程序启动后如何读取资源数据,比如我们要根据layout ID读取对应的layout文件
1) 启动后读取resources.arsc文件,得到app资源描述表数据
2) 通过资源ID的次高位字节,得到资源类型为layout,从而在app资源描述表中快速定位到layout类型的数据结构
3) 根据app当前的硬件或系统信息(language,screensize等),在layout类型的数据结构中,快速的匹配到最佳资源,从而拿到资源路径
4) 根据资源路径,获取布局文件数据
5) 根据布局文件数据,生成AttributeSet,接着根据AttributeSet的解析节点信息和对应属性ID,从app资源描述表,获取AttributeSet中当前节点下对应属性的值。
android默认提供了AssetManager来实现上述功能,不过我们实际开发中,更多的是使用Resource来获取资源数据,Resource基于AssetManager重新封装,提供了更加易用的接口并对指定操作做特殊处理,比如drawable,Resource会添加缓存处理等。
下面简单介绍几个跟PMS相关的:
1) AssetManager:
基于APK路径来初始化,接着获取AndroidManifest.xml数据
String archiveSourcePath = “***.apk”; //apk文件路径 AssetManager assmgr = new AssetManager(); int cookie = assmgr.addAssetPath(archiveSourcePath ); //将apk加入AssetManager if (cookie != 0) { //设置系统配置信息,包括语言,屏幕大小等信息 assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); //获取AndroidManifest数据 XmlResourceParser parser = assmgr.openXmlResourceParser(cookie, “AndroidManifest.xml”); } else { Slog.w(TAG, "Failed adding asset path:"+archiveSourcePath); } |
2) Resource:
根据apk路径初始化AssetManager,接着生成Resources
String archiveSourcePath = “***.apk”; //apk文件路径 AssetManager assmgr = new AssetManager(); int cookie = assmgr.addAssetPath(archiveSourcePath ); //将apk加入AssetManager if (cookie != 0) { //基于AssetManager初始化Resource Resources res = new Resources(assmgr, metrics, null); } else { Slog.w(TAG, "Failed adding asset path:"+archiveSourcePath); } |
3) AttributeSet:
属性集合,以下代码假设AttributeSet处于AndroidManifest的根节点,
通过Resources的obtainAttributes函数来读取Manifest中的VersionCode和VersionName值,传入属性集和要读取属性的ID数组
1) 通过指定对应的R.attr数组读取
XmlResourceParser parser = assmgr.openXmlResourceParser(cookie, “AndroidManifest.xml”);
AttributeSet attrs = parser; TypedArray sa = res.obtainAttributes(attributes, new int[]{android.R.attr.versionCode, android.R.attr.versionName}); Integer versionCode = sa.getInteger(0, 0); String versionName = sa.getString(1, 0); |
通过R.attr数组读取,相当于把要读取的属性ID直接写入代码中,扩展性和维护性都比较差,所以建议使用第二种方式。
2) 通过指定styleable读取
styleable其实就是一个属性的集合,比如AndroidManifest styleable的定义如下:
<declare-styleable name="AndroidManifest"> <attr name="versionCode" /> <attr name="versionName" /> <attr name="sharedUserId" /> <attr name="sharedUserLabel" /> <attr name="installLocation" /> </declare-styleable> |
通过使用styleable,就使目标属性集合可配置,从而提高程序的扩展性
TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifest); pkg.mVersionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_versionCode, 0); pkg.mVersionName = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifest_versionName, 0); |
1.2 APK签名相关
Android应用在打包之后,在正式发布前,需要对其签名,签名的目的主要有两个:
1)保护APK数据的完整性
2)作为APK的身份验证
下面简单介绍下签名的原理以及APK解析时的相关验证代码。
1.2.1 消息摘要,加解密,数字签名,数字证书
下面的名词解释相关内容大部分都摘自网络
1)消息摘要
消息摘要算法是使用一个Hash函数对任意长度的输入数据进行处理,输出固定长度的数 据。输出数据称为消息摘要。无法从消息摘要倒推出消息内容。常用的消息摘要算法是 MD5 和 SHA-1。
2)加解密
公钥加密算法又称为非对称密钥加密算法,因为它包含一个公钥-私钥对,称为key pair。即 key pair = private key +public key。从功能上说,两个key作用相同,用一个key加密的消息,只能用另一个key解密,反之亦然。
3)数字签名
数字签名就是将原始数据的信息摘要用private key加密后,和原始数据一起发送给接收方;
接收方收到签名数据后,先使用public key进行解密,拿到信息摘要;接着用收到的原始数据生成信息摘要,并与解密后的信息摘要做对比,如果一致,数据完整性验证通过, 否则验证失败,说明数据被修改了。
由于无法通过publickey推导出private key,通过数字签名就可以确保发送数据额完整,再则private key只有发送者知道,这样又可以确保数据是由private key的拥有者发送的。
4)数字证书
数字签名存在一个问题,那就是数据接收方如何获取publickey?预先把public key给接 收方或者直接和数字签名一起发送给接收方,这么做在通常情况下,肯定是没问题的,但是 在如下两种情况下,会存在安全性问题:
1:预先把publickey 给接收方,如果有人在接收方不知情的情况下,更换了public key,然后再截取发送过来的数据并替换成自己的,这样,接收方和发送方在不知情的情况下,数据已经被人调包了。
2:publickey连同数字签名一起发送给接收方,如果有人截取发送的数据,将数据,数字签名和public key都替换掉,这样也会产生数据调包的问题。
所以,要解决上面安全性问题,核心就是保证发送者的publickey不被调换。
数字证书因此而生,数字证书是指由权威机构CA认证发行的,主要包含拥有者的相关信 息和public key;所以上述发送者,只需要去CA,提供自己的相关信息和publickey做认证加密,通过后,CA会给其一本数字证书;后续发送者发送数据时,将数据用private key加密后,随同数字证书一起发出,就ok了。
1.2.2 APK签名介绍
APK在使用SignApk进行签名后,会在META-INF目录下生成如下文件:
1) MANIFEST.MF保存APK包内除了META-INF目录下文件以外其他文件的SHA-1摘
要数据。
2) CERT.SF保存MANIFEST.MF文件的SHA-1摘要数据和MANIFEST.MF里头每一项
的SHA-1摘要数据。
3) CERT.RSA保存有CERT.SF的数字签名和公钥证书
APK需要使用SignApk来进行签名,在签名前,要使用keytool生成.keystore密码仓库,接着通过aliasname和password从密码仓库中提取公私钥对供SignApk签名时使用。
SignApk工具代码对应系统源码路径build/tools/signapk/SignApk.java,由于main函数比较长,这里就不全部贴出,重点介绍下几个代码段
1) 生成MANIFEST.MF
//遍历APK内部文件,获取SHA-1摘要数据并base64编码 Manifest manifest = addDigestsToManifest(inputJar, hashes); … // MANIFEST.MF JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); je.setTime(timestamp); outputJar.putNextEntry(je); manifest.write(outputJar); |
2) 生成CERT.SF
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : (String.format(CERT_SF_MULTI_NAME, k))); je.setTime(timestamp); outputJar.putNextEntry(je); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k])); byte[] signedData = baos.toByteArray(); outputJar.write(signedData); |
writeSignatureFile负责将manifest数据项重新生成SHA-1摘要并写到baos输出流,最终baos数据写入CERT.SF
3) 生成CERT.RSA
4) /** Sign data and write the digital signature to 'out'. */ 5) private static void writeSignatureBlock( 6) CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, 7) OutputStream out) 8) throws IOException, 9) CertificateEncodingException, 10) OperatorCreationException, 11) CMSException { 12) ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1); 13) certList.add(publicKey); 14) JcaCertStore certs = new JcaCertStore(certList); 15) 16) CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 17) ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey)) 18) .setProvider(sBouncyCastleProvider) 19) .build(privateKey); 20) gen.addSignerInfoGenerator( 21) new JcaSignerInfoGeneratorBuilder( 22) new JcaDigestCalculatorProviderBuilder() 23) .setProvider(sBouncyCastleProvider) 24) .build()) 25) .setDirectSignature(true) 26) .build(signer, publicKey)); 27) gen.addCertificates(certs); 28) CMSSignedData sigData = gen.generate(data, false); 29) 30) ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded()); 31) DEROutputStream dos = new DEROutputStream(out); 32) dos.writeObject(asn1.readObject()); 33) } |
重点看标红的两个函数,addSignerInfoGenerator将CERT.SF数据用私钥加密后存入CERT.RSA中;addCertificates则将公钥证书保存到CERT.RSA中。
到这里,很多人可以会有疑问,为什么要产生CERT.SF这个摘要文件,好像没啥必要啊,直接用MANIFEST.MF不就可以了;细细的思考下,好像的确是这么回事,但是我们还是要相信Google这么做是有原因的,有可能是为了APK校验时效率更高?异或是为了留个口子,便于后续扩展,这里就不做深究,咱们继续往下走