【Android专栏】

最近在做Iot开发免不了接触到一些Android的技术,所以出个专栏记录一下…

源起

从在手机上点击一个应用的图标说起

1、首先点击事件会传递给launcher;Launcher其实也是一个app,它是我们开机之后看到的第一个app,它主要用于显示和管理手机上的其他应用,你也可以自己制作一个launcher,比如:

WechatIMG3

2、点击事件会由launcher构造成一个Intent意图,然后通过startActivity方法传递给ActivityManagerService;AMS是在SystemServer进程中启动的,它比launcher初始化要更早一些;

3、然后AMS通过Process.start方法通知到Zygote进程;Zygote进程比SystemServer进程还要早一些存在;关于进程之间的存在先后顺序参考下图,整个Android系统的启动分为linux kernel的启动和android的启动,linux kernel运行起来之后,Android的第一个程序就是init process,主要是一些配置和解析动作,然后就陷入了等待;

很明显能够看出Zygote进程是先于System Server进程之前启动的,且后者是前者fork出来的进程;

IMG_4C4BD5625EC8-1

4、Zygote收到AMS的通知后会fork一个新的进程,而新的进程会从新启应用的ActivityThread开始运行,加载app,创建一个activity对象,这些都是在ActivityThread的mainLoop方法中执行的,然后ActivityThread就陷入了循环;

5、最后ActivityManagerService回调上一步创建activity对象的生命周期方法,onCreate、onStart、onResume然后你就能看到新启应用的页面啦,实际上还有很多细节,不过大致流程就是这样。

IMG_4611D38DE043-1

基础概念

1、apk是由什么组成的?

image-20210205072610547

找了一个Google浏览器的安装包,解压缩可以看到有AndroidManifest.xml、assets文件夹、dex文件、lib文件夹、META-INF文件夹、res文件夹、一个arsc文件和一个sha256文件;

  • AndroidManifest.xml:每个应用都必定会有一个AndroidManifest.xml,它描述了应用的名字、版本、权限、引用的库文件等;

  • assets文件夹:里面包含了一些适配不同语言相关的文件,还有一些版本描述的文件,有些apk可能没有assets文件夹,这个目录不是apk所必须的;image-20210205073023194

  • dex文件:apk都会有,这些dex就是java或者kotlin编译之后生成的二进制文件;

  • lib文件夹:存放了一些Jni代码编译生成后的so文件,动态库文件;类似于window的dll文件;

    1
    2
    3
    .o 就相当于windows里的obj文件 ,一个.c或.cpp文件对应一个.o文件
    .a 是好多个.o合在一起,用于静态连接 ,即STATIC mode,多个.a可以链接生成一个exe的可执行文件
    .so 是shared object,用于动态连接的,和windows的dll差不多,使用时才载入

    image-20210205073348914

  • META-INF文件夹:存放的是签名信息,用来保证apk包的完整性和系统的安全;

  • res文件夹:存放apk用到的资源文件,如:图标,颜色定义等;

    image-20210205073855388

  • arsc文件:Android编译后生成的产物,主要是用来建立资源映射关系;

  • sha256文件:校验相关的,一般apk包里也没有;

四大组件

  • Activity 包含用户界面的组件,主要用于和用户进行交互;
  • ContentProvider 用于在不同的应用程序之间实现数据共享功能;
  • Service 实现程序后台运行的解决方案,非常适合那些不需要和用户交互而且还要求长期运行的任务;
  • BroadcastReceiver Android中的广播机制;

四大组件都需要在AndroidManifest注册才能起作用;

Activity生命周期

IMG_C6B5DC689770-1

1、比如一个activity正在处于界面能与用户交互,突然弹出来一个提醒对话框,抢走了之前activity的焦点,但是并没有完全将其遮蔽,那么前一个activity的状态转移是怎样的?

onResume(能与用户交互)—>onPause(由于弹出提醒对话框被夺走了焦点)

  • 如果此时用户X掉了对话框,切换回了之前的界面,那么应该走到onResume;
  • 如果此时用户放大的对话框,导致activity完全不可见,那么应该走到onStop;

ContentProvider

ContentProvider提供了应用程序之间共享数据的方法;应用程序通过ContentProvider访问数据而不需要关心数据具体存储及访问过程,这样既提高了数据的访问效率,同时也保护了数据;

IMG_1FD13F738572-1

1、补充一个知识点:URL与URI的区别

  • URL是标示资源的物理位置,相当于文件的路径,如http://www.baidu.com
  • URI则是标示资源的逻辑位置,并不提供资源的具体位置,如content://contract/people

Service

Service是Android中实现程序后台运行的解决方案。但是服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程,当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。

Android多线程编程

首先和许多其他的GUI库一样,Android的UI也是线程不安全的。如果想要更新应用程序中的UI元素,则必须在主线程中进行,否则就会出现异常。

image-20210214072108173

比如,直接在子线程中进行更新UI的操作,程序就会异常停止报“CalledFromWrongThreadException”;有些时候我们需要根据子线程的执行结果来更新相应的UI控件,那么应该怎么操作呢?Android的异步消息处理机制完美解决了这个问题,子线程中构造一个message对象,并通过handler的sendMessage方法将该对象发送出去,那么Handler就会收到这条消息,并在handleMessage方法中进行处理;而handleMessage方法中的代码就是在主线程中运行的,所以不会有上面的CalledFromWrongThreadException;

1
2
3
4
5
6
7
8
9
10
11
12
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_TEXT:
// 在这里可以进行UI操作
text.setText("Nice to meet you");
break;
default:
break;
}
}
};

image-20210214072805210

异步消息处理机制详解

Android中的异步消息处理主要由四部分组成:

  • Message: 用于线程之间传递消息,what子段,arg1、arg2、obj字段;
  • Handler: 消息处理者,发送消息一般使用Handler的sendMessage方法,而发出的消息经过一系列辗转,最终会传递到Handler的handleMessage方法中;
  • MessageQueue: 用于存放所有通过Handler发送的消息,每个线程中只会有一个MessageQueue对象;
  • Looper: 每个线程中的MessageQueue的管家,调用Looper的loop方法后,就会进入到一个无限的循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage方法中。每个线程也只会有一个Looper对象;

IMG_D6D1AC18EFAE-1

1、首先需要在主线程中创建一个Handler对象,并重写handleMessage方法;

2、当子线程中的UI需要更新时,创建一个Message对象,并且通过Handler对象调用sendMessage方法,将这条消息发送出去;

3、这条消息就被添加到MessageQueue队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理的消息,最后发回给Handler的handlerMessage方法中。

4、由于Handler是在主线程中创建的,所以此时handleMessage方法中的代码也会在主线程中运行;

BroadcastReceiver

广播:标准广播、有序广播

注册方式:动态注册、静态注册

如何创建一个广播接收器

只需要新建一个类,让它继承BroadcastReceiver,并重写父类的onReceive() 方法。这样当有广播到来时,onReceive方法就会得到执行,具体处理逻辑可以写在这个方法里;比如一个监听网络变化的程序:

1、创建一个NetworkChangeReceiver类继承BroadcastReceiver

2、重写onReceive方法处理逻辑就是弹出一个toast提示;

3、创建一个IntentFilter对象实例,添加对网络状态变化的广播进行监听(当网络发生变化时,系统会发出一条值为android.net.conn.CONNECTIVITY_CHANGE的广播)

4、注册广播registerReceiver传入两个参数,一个是BroadcastReceiver实例,一个是InterFilter实例;

5、这样就写完了,当网络状态发生变化时,系统会发出一条广播,而这条广播被NetworkChangeReceiver监听到从而执行onReceive方法,弹出toast提示;

image-20210222072241380

静态注册实现开机启动

以上是动态注册的方法,写在了onCreate方法中,很明显只有应用程序启动之后注册语句才能够执行,但是如果我想要程序还没有启动就能接收到广播呢?比如我想要一个应用程序监听到开机的广播后能自动启动,这种情况就需要用到静态注册的方法了;

1
2
3
4
5
6
7
8
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

比如定义一个BootCompleteReceiver来实现监听开机广播,直接在AndroidMinifest.xml中这样注册,然后就可以在BootCompleteReceiver的onReceive方法中实现监听到开机广播后你想要的逻辑了;

发送标准广播

前面的广播都是系统发的,我们只是实现了广播监听;现在我们来自己发送一个广播,然后自己监听发送的广播;

1
2
3
4
5
6
7
8
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>

静态方法注册监听,然后在代码中发送自定义的广播;

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}
});
}

发送有序广播

相比于发送标准广播,只需要修改发送方式为sendOrderedBroadcast即可;比如: sendOrderedBroadcast(intent, null), 第一个参数还是和之前一样,第二个参数表示与权限相关的字符串;

有序广播可以通过设置接收器的优先级来决定由哪个接收器先接到广播

1
2
3
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>

也可以在onReceive方法中调用abortBroadcast进行广播截断,这样后面的接收器就收不到这条广播了;

本地广播

  • 只在程序内部使用,不用担心机密数据泄露
  • 其他的程序无法将广播发送到我们程序内部,不需要担心会有安全漏洞
  • 相比全局广播更高效

本地广播其实相比于之前所示改动点有两个:

  1. 发送广播的方式,使用LocalBroadcastManager.sendBroadcast发送;如:LocalBroadcastManager.sendBroadcast(intent);

  2. 注册本地监听器的方式,使用LocalBroadcastManager.registerReceiver注册,如:LocalBroadcastManager.registerReceiver(localReceiver, intentFilter);

最后要注意一点:本地广播是无法通过静态注册的方式来接收的,这也可以理解。因为静态注册本身是为了让程序在未启动的时候也能接收广播,而发送本地广播时,我们的程序肯定是已经启动了的,所以也不需要使用静态注册的能力;

数据存储解决方案

  • 文件存储

  • SharedPreferences存储

  • 数据库存储

    文件存储用到的核心技术就是Context类提供的openFileInput和openFileOutput方法,之后就是通过各种流来进行读写操作;所有的文件默认存储到/data/data//files/目录下;openFileInput方法传入文件名参数,系统会自动从/data/data//files/目录下读取文件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private fun save(inputText: String) {
try {
val output = openFileOutput("data", Context.MODE_PRIVATE);
val writer = BufferedWriter(OutputStreamWriter(output));
writer.use {
it.write(inputText)
}
} catch (e: IOException) {
e.printStackTrace()
}
}

private fun load(): String {
val content = StringBuilder()
try {
val input = openFileInput("data")
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
content.append(it)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return content.toString()
}

SharedPreferences是使用键值对方式来存储数据的,通常使用Context类中的getSharedPreferences方法或者Activity类中的getPreferences方法;SharedPreferences文件都是存放在/data/data//shared_prefs/目录下的;

// todo…

遗留问题

1、项目下的build.gradle与app/build.gradle分别是什么文件?有什么作用?

首先项目下的build.gradle是项目的全局构建脚本,通常这个文件是不需要修改的;repositories闭包一般都会声明google() 和jcenter(),分别对应google的扩展依赖库和jcenter三方开源库;声明了这两行的配置之后,就可以在项目中轻松引用任何google和jenter仓库中的依赖库了;

然后dependencies闭包中使用classpath声明了两个插件:一个Gradle插件和一个Kotlin插件;

gradle并不是专门为构建android项目开发的,Java、c++等多种项目也可以使用Gradle来构建,因此如果想使用它来构建Android项目,则需要声明这个插件;后面的2.3.3是插件的版本号,通常要和当前AndroidStudio的版本是对应的;另一个则表示kontlin插件,如果是Java版本的Android项目则不需要声明;

image-20210729073451961

app模块下的build.gradle是模块的构建脚本,这个文件中会指定很多项目构建相关的配置;

com.android.application表示这是个应用模块而不是库模块,kotlin-android表示插件,如果是kotlin开发那么这个是必须要有的;applicationId是每个应用的唯一标识符,绝对不能重复,默认会使用在创建项目时指定的包名。testInstrumentationRunner 在当前项目中启用JUnit测试,可以为当前项目编写测试用例保证功能的正确性和稳定性;

buildTypes闭包用于指定生成安装文件的相关配置,通常只会有两个子闭包:debug和release。debug用于生成指定测试版安装文件的配置,release用于生成正式版安装文件的配置;另外debug闭包是可以忽略不写的;minifyEnabled用于指定是否对项目的代码进行混淆,proguardFiles用于指定混淆时使用的规则文件;

最后的dependencies闭包指定当前项目所有的依赖关系;通常Android Studio项目一共有3种依赖方式:

  • 本地依赖(比如第一行的fileTree 表示将libs目录下的所有.jar后缀的文件都添加到项目的构建路径中)
  • 库依赖(基本格式是implementation project后面加上要依赖的库的名称)
  • 远程依赖(androidx.appcompat:appcompat:1.1.0,分别表示域名,工程名,版本号)

Screen Shot 2021-07-27 at 22.45.13

2、java的文件流详细解析;

参考文档:

  1. Android Developers
  2. Android系统启动分析
  3. 《第一行代码》