写在前面的话:接触Android的时间也不短了,听了视频、看了书、敲了代码,写了博客,做了demo。。。但是想做出一款优秀的APP(哪怕是封装一个不错的功能)还有很长的路要走。于是前些日子我打算更加深入地往底层、往源代码方向研究Android——我就买了一本《Android群英传》拜读一下,在刚读到前言的时候,我发现作者推荐了阅读官方的Training和Guide,我才意识到,其实之前我接触到的各个渠道的Android知识,都是来自官方文档,与其更加深入地了解Android,不如把官方文档的内容好好拜读一下,它才是最好的教材。从今天起,我将努力把Android官方文档翻译一遍,当然不是全部内容,而是我认为重要的知识点,一些很基础的内容就略过了。
本文将从官方文档中最开始的内容《Introduction》说起。
如需访问官方原文,请您点击这个链接:《Introduction to Android》。
Android应用程序提供了多种访问入口
一个Android应用程序由不同的独立组件构成,并且可以独立调用,比如一个Activity可以提供一屏UI显示,一个Service可以用于在后台执行任务。
在一个组件中启动另一个组件,可以使用intent,甚至可以使用intent启动其他一般用程序,这使得一个应用程序可以有多个访问的入口,并且可以让其他应用程序的某个组件看起来好像自己应用的一部分。
一个应用程序可适配不同设备
Android提供了可适配的框架,一种资源可以适配不同的设备。比如一个layout布局会根据不同设备的屏幕自动匹配。开发者可以在应用运行时查看应用程序所需权限,开发者也可以在自己开发的应用中声明所需权限,Google Play可以自动筛选支持应用所需权限的设备,从而减少不必要的适配问题。
Android应用程序主要由Java编写,Android SDK工具将代码、资源文件和数据内容打包成APK文件。APK文件包含了该应用程序的所有组件,该APK文件可以在支持Android的设备上安装并使用。
一旦安装成功,应用程序将受到沙箱机制的保护:
Android操作系统是一个多用户的Linux操作系统,每个应用程序就代表一个用户。
默认情况下,系统为每个APP分配一个用户ID(该用户ID对于APP来说不可见,仅系统可见),系统使用一个APP来管理所有文件的权限,这个APP也有用户ID,使用这个ID才能访问这些权限。
每个应用程序都有独立的虚拟机,这使得每个应用可以互不干扰地运行。
默认情况下,每个APP都有自己的Linux进程。一个应用的某个组件需要执行,Android就会启动这个应用的进程;相反,当应用不再使用或系统内存紧张时,该应用所在进程将被停止。
通过上述要求,Android是一个满足最小权限原则(principle of least privilege)的系统,也就是说,在默认情况下,每个APP只能启动该它所需的功能组件,系统未给予其权限的内容,APP将无法访问。
然而使用下列方式,应用程序之间可以共享数据,或者程序可以访问系统服务:
下面将介绍APP的核心基本组件、manifest文件、与代码分离的资源文件及如何使用资源文件修饰符适配不同的设备。
也就是所谓的四大组件,每个组件都有自己独立的生命周期,管理者组件的创建和销毁。
Activities
Activity代表了与用户交互的UI(它并不是一个UI,而是一个用于呈现UI 的载体),您所看到的屏幕上的内容,都是以Activity作为载体的。如需访问有关Activity的官方文档,请您点击这个链接:《Activities》,当然我也将在后续博文中翻译该文档。
Services
Service是一个运行在后台的组件,它主要用于长连接和远程操作,它没有与用户交互的UI界面。比如说,Service可以与在后台运行音乐,或者从网上下载数据而不影响用户的UI操作。可以在Activity中启动或者绑定Service以便与其通信。
Content providers
content provider管理着一个APP中的数据信息。您可以将数据存储在任何可以持久化保存的地方,如SQLite数据库,文件系统,网络上等。其他应用可以通过content provider访问本应用的数据信息。例如,系统的联系人应用程序中的Content Provider提供了通讯录信息,使用ContactsContract.Data类(需配合Content Resolver)就可以访问系统的通讯录信息。
Content Provider对应用的数据隐私性做了较好的保护。
Broadcast receivers
Broadcast receivers用于接收系统发出的各式广播信息。系统广播有很多,如:点灭屏幕时,电量低时,获取图片时 等。应用程序也可以自定义广播,如当APP下载了一些信息需通知其他APP“信息已下载完毕可供使用”时,应用可发出一条广播,而其他应用可截获该广播。Broadcast receivers同样无法进UI展示,但显示一条Notification表示有广播发出。更简单地说,Broadcast receivers仅是发起了一个意图,可以指导对该意图感兴趣的应用做一些事情。
Android系统的一个独特设计就是任何一个APP都能其中另一APP的组件,例如,如果您打算使用系统摄像头照一张照片,使用自己的应用就能启动,而不必再从头编写照相功能的代码,您也无需将自己APP的代码与照相的应用程序的代码有什么关联。您只需使用自己的activity启动带有照相功能的activity并返回照相的结果就行了。这个带有照相功能的activity就好像是自己应用的一部分。
当您启动了一个APP的组件,就启动了该APP所在的进程,并初始化该组件的类,比如,您的APP启动了相机应用中的activity,该activity将运行在属于相机应用的进程中,而不是您的APP中。因此,Android应用并不存在唯一的入口(如没有main()方法)。
通过intent这个异步请求,可以启动activities, services, 和 broadcast receivers这三大组件,Intent在运行时将不同的组件做一绑定,不论这些组件是否来自同一应用。通过intent启动组件有两种方式:隐式intent和显式intent,这两种方式将在后续稳重中具体介绍。
对于启动activity或service,intent可以附加一些action,或是一些指向具体数据内容的URI。例如某些intent打算启动一个带有展示图片功能的activity,或是打算打开一个网页,在某些情况下,启动后的组件会通过intent返回一些数据(比如说您希望intent打开的是某个通讯录的联系人,并返回这个联系人的信息,返回的信息就包含在intent中,而且还包含了指向这个联系人的URI信息)。
对于broadcast receivers来说,可以在intent-filter中声明想要接收什么样的广播,比如说您打算在您的应用中收到系统发出的“电量低”的广播,那么只需要在您的broadcast receivers中配置有关“电量低”的消息就行了。
对于content provider来说,intent无法启动。Content Provider需要和ContentResolver配对使用。 content resolver负责处理与之对象的ContentProvider事务,所以ContentProvider不用显式地调用contentResolver中的方法,就能将数据传递出去。这种做法很好地将应用中的数据与访问该数据的方法分离,提高了应用的安全性。
可以使用下列方法启动相应组件:
在使用组件之前,需在Manifest文件中对组件进行注册。除了注册组件以外,Manifest文件还可以作如下事情:
配置activity的方法如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:icon="@drawable/app_icon.png" ... >
<activity android:name="com.example.project.ExampleActivity"
android:label="@string/example_label" ... >
</activity>
...
</application>
</manifest>
其中<application>
标签的icon属性用于配置启动图标。<activity>
标签的name属性用于注册应用中的activity的子类,需使用全限定名注册。
broadcast receivers除了可以在ManiFest文件中静态注册,还可以在代码中使用registerReceiver()方法动态注册。有关ManiFest文件的更多内容,请您点击这个链接:《App Manifest》,我将在后续文章中翻译该文档。
intent的真正强大之处在于它的隐式启动,隐式intent可以描述其想要启动的目标组件所具备的特性而不具体指明目标组件的名字。
通过为组件配置 intent filters,组件具有被隐式intent筛选的能力,只有匹配了intent filters中的条件,该组件才能被隐式intent启动。例如,您打算在应用中配置一个发送邮件的activity,使得通过隐式intent表明“发送邮件”的意图,那么您可以这样为activity配置 intent filters:
<manifest ... >
...
<application ... >
<activity android:name="com.example.project.ComposeEmailActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:type="*/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
更多有关intent filter的官方介绍,请您访问这个链接:《Intents and Intent Filters》
支持Android的设备各不相同,它们支持的底层驱动也各有千秋,为了防止将需要某个底层驱动的应用(如带有照相功能的应用)安装在并不支持该功能的设备上(该设备没有摄像头),需在manifest文件中配置,这个配置信息在系统中仅是说明性质的(即Android系统并不读取这个配置说明),但将应用发布到某个平台上(如Google play)时,该平台会读取这些声明。比如说, 您的应用需要调用系统摄像头并运行在最小版本为Android 2.1(API 7)的设备上,您需要作如下配置:
<manifest ... >
<uses-feature android:name="android.hardware.camera.any"
android:required="true" />
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="19" />
...
</manifest>
低于android 2.1或不支持摄像头的设备将无法在Google play上搜索到该应用。
您也可以声明您的应用使用摄像头但不是必须的,这需要将use-features标签的android:required属性设置为false,并在代码中检测设备是否有摄像头、关闭所有与摄像头有关的功能。
APP资源实现了与代码的分离,这些资源包括图片、影音等可视资源。例如,您可以定义animations,、menus、 styles、 colors或是activity的layout资源,使用资源配置,可以做到代码的解耦,并使其与更多地设备适配。
对于每一个资源,SDK工具都会为之设置一个唯一的整型ID,通过这个ID,您可以在代码中引用这个资源,
为各种不同的设备适配资源是APP资源的最大优势,您可以在资源文件名中追加修饰符(qualifier)以匹配不同的设备。如res/values-fr/文件夹中放置法语的字符串信息,而默认的res/values/目录下放置默认的字符串信息,系统会根据您安装的设备系统语言自动匹配不同文件的内容。
类似这种限定符还有很多,比如您还可以为适配横竖屏的不同布局而追加修饰符等。有关资源修饰符的官方文档,请您点击这个链接:《Providing Resources》。
android设备千千万,这需要您的android应用程序具备很好的兼容性。为了更好的适配各式屏幕,android提供了修饰符。
关于兼容性,分为两种:设备的兼容性(device compatibility)和APP的兼容性(app compatibility)。
对于设备的兼容性,您不必担心,只要是能发布在Google play上的应用,都兼容android 设备。
但是对于APP的兼容性,您就得留意了,因为不同设备对硬件的支持不尽相同,比如有的应用需要指南针的底层支持,那么不支持指南针的设备将无法运行该应用。
以下是检测设备是否支持指南针功能,若不支持,应将其关闭:
PackageManager pm = getPackageManager();
if (!pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_COMPASS)) {
// This device does not have a compass, turn off the compass feature
disableCompassFeature();
}
每一个android版本都为应用做了向后兼容的适配,也就是说,您应该为应用程序适配新的版本以做到向前兼容(以我的经验来看,假设当前市面上占有率最高的版本是android 5.1,那么您应将应用的targetSDKVersion设置为 23 (android 6.0),这个设置表明您的应用程序在android6.0 的设备上可以正常运行,这可以保证占有率最高的android 5.1的设备升级到android6.0时,您的应用程序不会崩溃。所以一般将targetSDKVersion设置为比目前市场占有率最高的版本设备高1-2个版本即可。)
在默认情况下,每个应用都没有任何权限去访问其他应用、系统设置和用户的隐私数据。包括读写用户的隐私信息(通讯录和邮件),读写其他应用的数据,访问Internet、使设备保持唤醒状态等等。
基于android系统的沙箱机制,应用必须显式地分享数据和资源,这需要显式地申请权限:应用显式地在配置中申请权限,还需获得用户的同意。
所有apk文件都必须用证书签名,并由开发持有一个私有的key。这张签了名的证书就是对该应用程序开发者的认证,这张证书无需由权威机构认证,而是由开发工具进行自我签名,使用证书可以保护开发者的权益,并且系统可以允许或拒绝应用使用签名级别的权限(signature-level permissions);证书还可以允许或拒绝应用请求与其他应用持有相同Linux ID(request to be given the same Linux identity)的权利。
安装应用时,android赋予每个一包(android以包名区分不同应用)一个确定的Linux用户ID(Linux User ID),从该应用的安装到卸载,这个ID一直存在且保持不变。在不同的设备中,ID可能会不同;但这个ID在同一台设备上会保持唯一。
出于安全性的考虑,在一般情况下,两个不同的包(应用)不能处于同一进程中;但是,您可以在AndroidManifest.xml文件的manifest标签中的sharedUserId属性中为不同的包(应用)配置相同的用户ID(User ID),这时两个包(应用)会被看作是一个应用,它们具有相同的用户ID和文件权限(user ID and file permissions)。需特别注意的是,为了安全性的考虑,两个应用具有相同的签名,同时还配置了相同的sharedUserId属性,这两个应用才拥有相同的用户ID。
一个应用存储的数据都会被赋予该应用的用户ID,其他应用无法共享这些数据。
在默认情况下,一个Android的应用程序为被赋予任何权限,这对用户的体验或数据的存取会产生不利影响。为了使用被设备保护的特性,您必须使用<uses-permission>
标签添加权限。
比如说,您希望模拟器可以接收发送过来的短消息时,可是使用下列代码添加权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.myapp" >
<uses-permission android:name="android.permission.RECEIVE_SMS" />
...
</manifest>
如果您申请的是基本权限(normal permission;基本权限意味着该权限不会对用户的隐私或设备的运行造成太多负面影响),系统会自动赋予这些权限;如果您申请的是危险权限(dangerous permission;危险权限意味着该权限可能对用户的隐私或设备的运行造成潜在的影响(potentially affect))【有关权限级别的官方介绍,请您点击这个链接:《Normal and Dangerous Permissions》】,系统会显式地告知并请求用户授权。设备的版本不同,告知和请求的方式也不同:
有些时候,应用请求权限失败时,会抛出SecurityException异常,但这个一场不什么时候都会抛出,大多是情况下,请求权限失败时只会打印Log。
一个特别的权限可能会在多处地方被使用:
为了保证您的应用总能适配新的API版本增加的权限,建议将targetSdkVersion设置得尽可能的高(as high as possible)。
系统为权限分成了若干个级别,其中最重要的两个级别即为基本权限和危险权限。
所有的危险权限都被分成了权限组,若设备的版本号是 Android 6.0 (API level 23)或以上,并且targetSdkVersion为23或更高。当您的APP请求危险权限时,下列行为将会发生:
事实上,无论是基本权限还是危险权限都被分成了权限组,但是只有危险权限所在的权限组会发生上述的情形,所以您可以忽略权限组中的基本权限。
若设备的版本是Android 5.1 (API level 22)或以下,而且targetSdkVersion为22或以下,系统会在应用安装时,声明所需的所有权限,这些声明是以权限组展示给用户的而不是单个权限。
以下列出危险权限及其权限组:
若打算自定义权限,您需要在manifest文件中使用<permission>
标签声明。
比如说,若打算控制某个Activity的启动权(谁可以启动该Activity),您可以配置如下代码:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.me.app.myapp" >
<permission android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
android:label="@string/permlab_deadlyActivity"
android:description="@string/permdesc_deadlyActivity"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="dangerous" />
...
</manifest>
其中
<protectionLevel>
属性是不能缺省的,该属性表明了请求该权限的级别,或谁可以被允许持有该权限。<permissionGroup>
属性是可选的,该属性仅是用来以分组的形式将权限显示给用户,您可以将该属性设置到一个标准权限组标准权限组(android.Manifest.permission_group)里,也可以设置一个自定义的权限组。推荐使用前者,因为这样可以减少弹出对话框的次数。<string name="permlab_callPhone">directly call phone numbers</string>
<string name="permdesc_callPhone">Allows the application to call
phone numbers without your intervention. Malicious applications may
cause unexpected calls on your phone bill. Note that this does not
allow the application to call emergency numbers.</string>
若您在manifest文件中声明了高级权限(High-level permissions),那么这种权限不是所有组件都能获取的,若希望针对某个组件获取高级权限,您应当在该组件的标签中用android:permission 属性声明该高级权限。
除了上述提到的为broadcast receiver声明高级权限外,您还可以在调用Context.sendBroadcast()方式传入一个在String类型的权限。
由于Broadcasts和receiver都可以被赋予权限,两种权限都需要经过intent的筛选才能匹配。
处于安全的考虑,对于content provider,可以为其声明保护自己的读写权限。比如说,获取邮件需要权限,但赋予的URI却指向了一个图片浏览器,该浏览器无法打开邮件,因为浏览器没有被赋予打开邮件的权限。
为了解决这个问题,可以在启动activity时为intent设置Intent.FLAG_GRANT_READ_ URI _PERMISSION 或 Intent.FLAG_GRANT_WRITE_URI_PERMISSION字段。