抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Android 系统架构

Android 8 前后两者区别在于 Android 8 之后的系统架构多了一层硬件抽象层(HAL)。将物理硬件与操作系统分离,使开发人员可以更加方便地编写硬件驱动程序,并且能够在各种不同地硬件平台之间进行移植。

Linux 内核层

为 Android 设备的各种硬件提供了底层的驱动, 同时 Android 对于 Linux 进行了改动,比如它没有 glibc。

android 的本质是一个基于 Linux 上运行的 Java 虚拟机。

硬件抽象层(HAL)

硬件抽象层(Hardware Abstraction Layer,简称 HAL)是操作系统或软件与硬件之间的接口层,用于提供硬件操作的抽象,使得上层软件可以不依赖于具体硬件实现,而能在不同硬件平台上运行。它的主要作用是隐藏硬件的细节,提供一致的、统一的接口,从而简化硬件与软件的交互。

原生 C/C++库(native)

Native C/C++ 层是指使用 C 和 C++ 语言编写的代码,通常被称为本地层。在 Android 应用程序中,本地层主要用于访问底层系统资源,例如文件系统、网络协议栈和硬件接口等。本地层还可以提供与 Java 代码的互操作性,使得 Android 应用程序能够充分利用本地代码的优势,例如高效的内存管理和快速的算法执行速度。

原生库的功能和作用

原生 C/C++库为 Android 系统和应用程序提供以下几个方面的支持:

  • 图形处理和渲染:提供图形和用户界面处理库,用于渲染 2D/3D 图形。
  • 多媒体支持:支持音频、视频的播放、录制及处理,为媒体相关的应用提供基础支持。
  • 数据存储和访问:提供数据库访问、文件系统管理和存储支持,常用于持久化数据存储。
  • 网络通信:支持应用的网络功能,例如 HTTP 通信、Socket 通信等,保障应用能够实现网络交互。
  • 计算和算法:提供数学计算和加密算法的实现,确保应用在加密、解密、数据分析等方面的需求。

系统运行库层(Android Runtime)

通过一些 C/C++库为 Android 系统提供了主要的特性支持。包含两部分,一个是系统库,另一个是 ART 虚拟机。

ART 虚拟机

ART(Android Runtime)是 Android 操作系统的应用运行时环境,是 Android 应用执行的基础。ART 从 Android 5.0 开始正式替代了早期的 Dalvik 虚拟机,成为 Android 默认的应用运行时。

ART 虚拟机在设计上具有以下几个核心特性:

  • 提前编译(Ahead-of-Time, AOT):ART 在应用安装时将应用代码从字节码(Java 字节码)编译为本地机器码,从而避免了运行时的编译开销。当用户打开一个应用程序时,ART 会在后台使用 AOT 编译器将应用程序的 Dex 字节码转换成本地机器代码,并存储在设备的存储空间中。这样,在下次打开应用程序时,就可以直接加载已经编译好的代码,避免了解释执行的过程,从而提高了应用程序的运行速度和响应时间。
  • 即时编译(Just-in-Time, JIT):ART 在后续版本中引入了 JIT 编译,用于动态优化应用性能。在应用运行时,如果检测到一些代码片段频繁被执行,ART 会对这些热点代码进行 JIT 编译,从而进一步优化其执行效率。
  • 高效的垃圾回收(Garbage Collection, GC):ART 采用并发垃圾回收机制,减少应用卡顿现象。
  • 改进的内存管理:ART 具有高效的内存分配机制,能够减少内存碎片化,提升内存利用率。
  • 调试和诊断支持:ART 提供了更多调试工具和性能分析支持,帮助开发者更好地优化应用。

应用框架层(Java API 框架)

提供了构建应用程序时可能用到的各种 API,Android 自带的一些核心应用就是 使用这些 API 完成的,开发者可以使用这些 API 来构建自己的应用程序。

应用层

所有安装在手机上的应用程序都是属于这一层的。

文件系统分析

概括

  1. gradle 和.idea 这两个目录下放置的都是 Android Studio 自动生成的一些文件,我们无须关心,也不要去 手动编辑。

  2. app 项目中的代码、资源等内容都是放置在这个目录下的,我们后面的开发工作也基本是在这 个目录下进行的,待会儿还会对这个目录单独展开讲解。

  3. build 这个目录主要包含了一些在编译时自动生成的文件,你也不需要过多关心。

  4. gradle 这个目录下包含了 gradle wrapper 的配置文件,使用 gradle wrapper 的方式不需要提前 将 gradle 下载好,而是会自动根据本地的缓存情况决定是否需要联网下载 gradle。 Android Studio 默认就是启用 gradle wrapper 方式的,如果需要更改成离线模式,可以 点击 Android Studio 导航栏 →File→Settings→Build, Execution, Deployment→Gradle,进行配置更改。

  5. .gitignore 这个文件是用来将指定的目录或文件排除在版本控制之外的。关于版本控制,我们将在第 6 章中开始正式的学习。

  6. build.gradle 这是项目全局的 gradle 构建脚本,通常这个文件中的内容是不需要修改的。稍后我们将会 详细分析 gradle 构建脚本中的具体内容。

  7. gradle.properties 这个文件是全局的 gradle 配置文件,在这里配置的属性将会影响到项目中所有的 gradle 编 译脚本。

  8. gradlew 和 gradlew.bat 这两个文件是用来在命令行界面中执行 gradle 命令的,其中 gradlew 是在 Linux 或 Mac 系统 中使用的,gradlew.bat 是在 Windows 系统中使用的。

  9. iml 文件是所有 IntelliJ IDEA 项目都会自动生成的一个文件(Android Studio 是基于 IntelliJ IDEA 开发的),用于标识这是一个 IntelliJ IDEA 项目,我们不需要修改这个文件中的任何内容。 (老版本)

  10. local.properties 这个文件用于指定本机中的 Android SDK 路径,通常内容是自动生成的,我们并不需要修 改。除非你本机中的 Android SDK 位置发生了变化,那么就将这个文件中的路径改成新的位置即可。

  11. settings.gradle 这个文件用于指定项目中所有引入的模块。由于 HelloWorld 项目中只有一个 app 模块,因 此该文件中也就只引入了 app 这一个模块。通常情况下,模块的引入是自动完成的,需要我 们手动修改这个文件的场景可能比较少。

app 目录下的结构

  1. build 这个目录和外层的 build 目录类似,也包含了一些在编译时自动生成的文件,不过它里面的 内容会更加复杂,我们不需要过多关心。 (老版本)
  2. libs 如果你的项目中使用到了第三方 jar 包,就需要把这些 jar 包都放在 libs 目录下,放在这个目 录下的 jar 包会被自动添加到项目的构建路径里。 (老版本)
  3. androidTest 此处是用来编写 Android Test 测试用例的,可以对项目进行一些自动化测试。
  4. java 毫无疑问,java 目录是放置我们所有 Java 代码的地方(Kotlin 代码也放在这里),展开该目录,你将看到系统帮我们自动生成了一个 MainActivity 文件。
  5. res 这个目录下的内容就有点多了。简单点说,就是你在项目中使用到的所有图片、布局、字符串等资源都要存放在这个目录下。当然这个目录下还有很多子目录,图片放在 drawable 目录下,布局放在 layout 目录下,字符串放在 values 目录下,所以你不用担心会把整个 res 目录弄得乱糟糟的。
  6. AndroidManifest.xml 这是整个 Android 项目的配置文件,你在程序中定义的所有四大组件都需要在这个文件里注册,另外还可以在这个文件中给应用程序添加权限声明。由于这个文件以后会经常用到, 我们等用到的时候再做详细说明。
  7. test 此处是用来编写 Unit Test 测试用例的,是对项目进行自动化测试的另一种方式。
  8. .gitignore 这个文件用于将 app 模块内指定的目录或文件排除在版本控制之外,作用和外层 的.gitignore 文件类似。
  9. app.iml IntelliJ IDEA 项目自动生成的文件,我们不需要关心或修改这个文件中的内容。(老版本)
  10. build.gradle 这是 app 模块的 gradle 构建脚本,这个文件中会指定很多项目构建相关的配置,我们稍后 将会详细分析 gradle 构建脚本中的具体内容。
  11. proguard-rules.pro 这个文件用于指定项目代码的混淆规则,当代码开发完成后打包成安装包文件,如果不希 望代码被别人破解,通常会将代码进行混淆,从而让破解者难以阅读。

资源目录

  • drawable 目录

用于存放应用的图像资源,包括位图(PNG、JPG)、矢量图(SVG、Vector Drawable)、图形 XML 文件等。

  • layout 目录

用于存放 XML 布局文件,用来定义应用界面的布局结构。

  • mipmap 目录

主要用于存放应用启动图标(ic_launcher)的不同分辨率图标资源。

  • values 目录

用于存放各种类型的资源值文件,通常包含以下几种文件:

**values/strings.xml**:存放应用中使用的字符串资源。

**values/colors.xml**:存放颜色资源。

**values/dimens.xml**:存放尺寸资源,如布局中的间距、字体大小等。

**values/styles.xml**:存放样式资源,定义应用主题、样式属性等。

**values/attrs.xml**:存放自定义属性。

  • xml 目录

用于存放 XML 配置文件,常见的有自定义的静态配置数据、布局约束文件等。

字符串在哪里—以 Helloworld 为例

首先打开 AndroidManifest.xml 文件,从中可以找到如下代码

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Helloworld">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

这段代码表示对 MainActivity 进行注册,没有在 AndroidManifest.xml 里注册的 Activity 是不能使用的。其中 intent-filter 里的两行代码非常重要,和表示 MainActivity 是这个项目的主 Activity,在手机上点击应用图标,首先启动的就是这个 Activity。Activity 是 Android 应用程序的门面,凡是在应用中你看得到的东西,都是放在 Activity 中的。

打开 MainActivity,代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
HelloworldTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}

首先可以看到,MainActivity 是继承自 ComponentActivity 的。ComponentActivity 是 AndroidX 中提供的一种向下兼容的 Activity,可以使 Activity 在不同系统版本中的功能保持一致性。而 Activity 类是 Android 系统提供的一个基类,我们项目中所有自定义的 Activity 都必须继承它或者它的子类才能拥有 Activity 的特性(ComponentActivity 是 Activity 的子类)。然后可以看到 MainActivity 中有一个 onCreate()方法,这个方法是一个 Activity 被创建时必定要 执行的方法,并且没有“HelloWorld”的字样。

“HelloWorld”是在哪里定义的呢?其实 Android 程序的设计讲究逻辑和视图分离,因此是不推荐在 Activity 中直接编写界面的。一种更加通用的做法是,在布局文件中编写界面,然后在 Activity 中引入进来。

res 目录中的内容就变得非常简单了。所有 以“drawable”开头的目录都是用来放图片的,所有以“mipmap”开头的目录都是用来放应用图标的,所有以“values”开头的目录都是用来放字符串、样式、颜色等配置的,所有 以“layout”开头的目录都是用来放布局文件的。

打开 res/values/strings.xml 文件,内容如下所示

1
2
3
<resources>
<string name="app_name">Helloworld</string>
</resources>

可以看到,这里定义了一个应用程序名的字符串,我们有以下两种方式来引用它。

在代码中通过 R.string.app_name 可以获得该字符串的引用。

在 XML 中通过@string/app_name 可以获得该字符串的引用。

基本的语法就是上面这两种方式,其中 string 部分是可以替换的,如果是引用的图片资源就可 以替换成 drawable,如果是引用的应用图标就可以替换成 mipmap,如果是引用的布局文件就 可以替换成 layout,以此类推。

build.gradle 文件

不同于 Eclipse,Android Studio 是采用 Gradle 来构建项目的。Gradle 是一个非常先进的项目 构建工具,它使用了一种基于 Groovy 的领域特定语言(DSL)来进行项目设置,摒弃了传统基 于 XML(如 Ant 和 Maven)的各种烦琐配置。

DSL 的全称是 Domain Specific Language 即 领域特定语言,我们可以通过 DSL 语言 构建出属于我们自己的语法结构,而在 Kotlin 中并不只有一种方式实现 DSL,而主要的实现方式就是高阶函数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
plugins {
id 'com.android.application'//声明了使用的Gradle插件
}
//一般有两种值可选:com.android.application表示这是一个应用程序模块,com.android.library 表示这是一个库模块。应用程序模块和库模块的最大区别在于,一个是可以直接运行的,一个只能作为代码库依附于别的应用程序模块来运行

android {
namespace 'com.example.the_first_demo'//指定应用程序的命名空间,也就是应用程序的包名
compileSdk 33//指定编译使用的API级别,sdk版本

defaultConfig {
applicationId "com.example.the_first_demo" //应用程序ID
minSdk 16 //最低支持的Android版本
targetSdk 33//指定的值表示你在该目标版本上已经做过了充分的测试,会启用该版本的新特性(目标版本)
versionCode 1 //版本代码
versionName "1.0"//版本名称

​ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false //指定是否对项目的代码进行混淆,true 表示混淆,false 表示不混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// proguardFiles 用于指定混淆时使用的规则文件,这里指定了两个文件
// 第一个proguard-android-optimize.txt 是在Android SDK目录下的,里面是所有项目通用的混淆规则;
// 第二个proguard-rules.pro 是在当前项目的根目录下的,里面可以编写当前项目特有的混淆规则
​ }
}
compileOptions {
​ sourceCompatibility JavaVersion.VERSION_1_8 //指定编译使用的Java版本
​ targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
//声明了应用所依赖的库
//指定应用程序运行时需要的库
implementation 'androidx.appcompat:appcompat:1.4.1'//是支持应用程序兼容旧版本Android的库
implementation 'com.google.android.material:material:1.5.0'//是Material Design组件库
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'//是一个用于布局的库
//指定单元测试时需要的库
testImplementation 'junit:junit:4.13.2'//JUnit单元测试库
//指定Android测试时需要的库
androidTestImplementation 'androidx.test.ext:junit:1.1.3'//JUnit扩展库
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'//Android UI测试的Espresso库
}

日志

ndroid 中的日志工具类是 Log(android.util.Log),这个类中提供了如下 5 个方法来供我 们打印日志。

Log.v()。用于打印那些最为琐碎的、意义最小的日志信息。对应级别 verbose,是 Android 日志里面级别最低的一种。

Log.d()。用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。 对应级别 debug,比 verbose 高一级。

Log.i()。用于打印一些比较重要的数据,这些数据应该是你非常想看到的、可以帮你分 析用户行为的数据。对应级别 info,比 debug 高一级。

Log.w()。用于打印一些警告信息,提示程序在这个地方可能会有潜在的风险,最好去修 复一下这些出现警告的地方。对应级别 warn,比 info 高一级。

Log.e()。用于打印程序中的错误信息,比如程序进入了 catch 语句中。当有错误信息打 印出来的时候,一般代表你的程序出现严重问题了,必须尽快修复。对应级别 error,比 warn 高一级。

四大组件

活动(Activity):在应用中能看到的东西都是放在活动中。

服务(Service):无法看到,它会一直在后台运行,即使用户退出了应用,服务可以继续运行。

广播接收器(Broadcast Receiver):允许应用接受来自各处的广播信息,比如电话、短信等。

内容提供器(Content Provider):为应用程序之间共享数据提供了可能,比如读取系统电话簿中的联系人。

虚拟机

关于 Dalvik 虚拟机 - 简书 (jianshu.com)

DX 工具用来转换 Java class 称为 dex 格式,但不是全部。多个类型包含在一个 Dex 文件中,多个类型中重复的字符串和其他常数在 Dex 中只存放一次,以节省空间。Java 字节码转换成 DVM 所使用的替代指令集。一个未压缩 Dex 文件通常是稍稍小于一个已经压缩的 jar 文件。

kotlin

变量:

Kotlin 中定义一个变量,只允许在变量前声明两种关键字:val 和 var。它拥有出色的类型推导机制。

val(value 的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋 值,对应 Java 中的 final 变量。

var(variable 的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新 赋值,对应 Java 中的非 final 变量。

也可以显式地声明了变量 a 为特定类型,此时 Kotlin 就不会再尝试进行类型推导了 如:

1
val a: Int = 10 

函数:

1
2
3
fun methodName(param1: Int, param2: Int): Int { 
return 0
}

fun(function 的简写)是定义函数的关键字

函数名后面紧跟着一对括号,里面可以声明该函数接收什么参数,参数的数量可以是任意多个,例如上述示例就表示该函数接收两个 Int 类型的参数。参数的声明格式是“参数名: 参数类型”,其中参数名也是可以随便定义的,这一点和函数名类似。如果不想接收任何参数,那么写一对空括号就可以了。

参数括号后面的那部分是可选的,用于声明该函数会返回什么类型的数据,上述示例就表示该 函数会返回一个 Int 类型的数据。如果你的函数不需要返回任何数据,这部分可以直接不写。

语句:

Kotlin 中的条件语句主要有两种实现方式:if 和 when。 首先学习 if,Kotlin 中的 if 语句和 Java 中的 if 语句几乎没有任何区别,但是 Kotlin 中的 if 语句相比于 Java 有一个额外的功能,它是可以有返回值的,返回值就是 if 语句每 一个条件中最后一行代码的返回值。

1
2
3
4
5
6
7
8
fun largerNumber(num1: Int, num2: Int): Int { 
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}

when 语句和 if 语句一样, 也是可以有返回值的

1
2
3
4
5
6
7
fun getScore(name: String) = when (name) { 
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}

when 语句允许传入一个任意类型的参数,然后可以在 when 的结构体中定义一系列的条件,格式 是:

1
匹配值 -> { 执行逻辑 } 

除了精确匹配之外,when 语句还允许进行类型匹配

1
2
3
4
5
6
7
fun checkNumber(num: Number) { 
when (num) {
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number not support")
}
}

for-in 循环

1
2
3
4
5
fun main() { 
for (i in 0..10) {
println(i)
}
}

Kotlin 中可以使用 until 关键字来创建一个左闭右开的区间,如 下所示:

1
val range = 0 until 10 

数学表达方式是[0, 10)

而如果你想跳过其中的一些元素,可以使用 step 关键字

1
2
3
4
5
fun main() { 
for (i in 0 until 10 step 2) {
println(i)
}
}

相当于 for-i 循环中 i = i + 2 的效果

如果你想创建一个降序的区间,可以使用 downTo 关键字

1
2
3
4
5
fun main() { 
for (i in 10 downTo 1) {
println(i)
}
}

Android 逆向分析初探:

APK 反编译之一:基础知识_apk 反编译基础-CSDN 博客

APK 文件其实是 zip 格式,后缀名被修改为 apk,可以用解压软件打开,结构如下:

AndroidManifest.xml 是程序全局配置文件,描述应用的名字、版本、权限、引用的库文件等信息。apk 中的 AndroidManifest.xml 是经过压缩的,可以通过 AXMLPrinter2 工具解开,命令为:java -jar AXMLPrinter2.jar AndroidManifest.xml。

classes.dex 是 java 源码编译后生成的 dalvik 字节码文件。其中包含 APK 的可执行代码,反编译 apk 时,反编译的其实就是 dex 文件。

resources.arsc 是编译后的二进制资源文件的索引。

res 目录存放资源文件,包括图片,字符串等。

libs 目录存放程序中用到的 so 库,即动态链接库。

META-INF 目录下存放的是签名信息,用来保证 apk 包的完整性和系统的安全

assets 目录用于存放需要打包到应用程序的静态文件。

Activity:

Activity 表示应用的一个屏幕,用于呈现给用户,并可接受处理与用户交互产生的界面事件。

Android 系统中以栈的形式保存所有 Activity。当一个新的 Activity 启动时,该 Activity 就会被压入 Activity 栈顶。当用 户按下 Back 返回按钮,当前 Activity 就会被关闭,栈中下一个 Activity 就会被移到栈顶,变成活跃状态。Activity 有 4 种状态:

活动状态。Activity 处于栈顶,可见的、具有焦点,可接受用户交互。系统会优先保持其所需资源,使其处于可见状态。

暂停状态。某些状态下,Activity 处于可见状态,但是没有获得焦点,不能与用户交互,此时处于暂停状态,比如长按 Home 键,当前应用就会变为暂停状态。在极端状况下,系统会终止处于暂停状态的 Activity,释放资源。

停止状态。当一个 Activity 完全不可见时,它就会变为停止状态。此时内存中还会保留相关信息。

Android 内存管理会优先释放处于非活动状态 Activity 的资源,接着释放处于停止状态的资源,只有在极端状况下,才会释放处于暂停状态 Activity 的资源。

当启动一个 Activity 时,会依次执行 onCreate、onStart、onResume 方法,当 Activity 重新获得焦点时,会依次执行 onRestart、onStart、onResume 方法,当关闭 Activity 时,会依次执行 onPause、onStop、onDestroy 方 法

了解 smali 结构

使用 Apktool 反编译 apk 后,会在反编译目录下生成一个 smali 文件夹,里面存放着的就是所有反编译出的 smali 文件,这些文件会根据程序包的层次结构生成相应的目录,程序中所有的类都会在相应的目录下生成独立的 smali 文件。baksmali 在反编译文件时,会为每个类单独生成了一个 smali 文件,内部类作为一个独立的类,它也拥有自己独立的 smali 文件,其文件名以“外部类名$内部类名”的形式定义。

签名检查的原理

每一个软件在发布时都需要开发人员对其进行签名,而签名使用的密钥文件是开发人员所独有的。破解者通常无法拿到开发人员的签名密钥,因此,签名是 Android 软件中一种有效的身份标识。如果软件运行过程中发现签名和自身发布时的不一致,很有可能是被篡改过,就可以中止软件的运行。Android SDK 中提供了检测软件签名的方法,可以调用 PackageManager 类的 getPackageInfo()方法,为第二个参数传入 PckageManager.GET_SIGNATURES,返回的 PackageInfo()对象中的 signatures 字段就是软件发布时的签名。但是,这个签名内容较长,不适合在代码中做比较判断,所以可以计算签名的哈希值并以它来做比较。哈希值相同,可以认为签名相同。

杂记

所谓原生,就是离 libart.so 更近:安卓原生就是 libart.so 来解释。Linux 原生就是 CPU 来直接解释。

pengxurui/AndroidFamily: 🔥【Android 面经 + Android 学习指南】一份帮助 Android 开发者知识积累与能力进阶的学习路线

评论