概念
Service,即服务,主要用于在后台执行长时间运行的操作。它提供了一种标准的方式来执行异步任务。可以让应用完成后台任务的部署。同时,还可以建立进程间通信,使得不同应用可以共享数据,而不需要了解对方的内部实现。
Service的创建与使用
Service的使用分为以下几个步骤:
- 手动创建Service的子类。
- 在AndroidManifest.xml文件中注册这个Service。
- 实现Service的抽象方法。
- 通过start或者bind的方法启动service
代码示例:
<service android:name=".MyService" />
Service的生命周期
Service的生命周期分为以下几个阶段:
- onCreate():Service被创建时调用。
- onStartCommand():Service被启动时调用。
- onBind():Service被绑定时调用。
- onUnbind():Service被解绑时调用。
- onDestroy():Service被销毁时调用。
Service的分类
Service分为以下几种类型:
- 前台Service:前台Service会一直保持运行状态,并且会显示在通知栏中。
- 后台Service:后台Service会在系统内存不足时被回收。
- 绑定Service:绑定Service会在Service和客户端之间建立一个连接,并且可以在客户端和Service之间传递数据。
Service的启动方式
Service的启动方式分为以下几种:
- startService():启动Service,但是Service会在后台运行,并且不会和调用者有直接的联系。
- bindService():绑定Service,Service会和调用者建立一个连接,并且可以在客户端和Service之间传递数据。
- startForeground():启动前台Service,Service会在通知栏中显示一个通知,并且可以在通知栏中进行操作。
代码示例:
// 1. 启动Service
Intent intent = new Intent(this, MyService.class);
startService(intent);
// 2. 绑定Service
Intent intent = new Intent(this, MyService.class);
// 创建一个ServiceConnection对象,并重写两个方法:
// onServiceConnected():Service连接成功时调用。
// onServiceDisconnected():Service连接断开时调用。
ServiceConnection connection = new MyServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 获取Service的实例
MyService myService = ((MyService.MyBinder)service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
// Service连接断开时调用
}
};
bindService(intent, connection, Context.BIND_AUTO_CREATE);
unbindService(connection);
stopService(intent);
直接start时,如果创建成功,Service内部会回调onCreate方法,然后回调onStartCommand方法。启动前台Service时,一样会回调onStartCommand方法,然后回调onStartForeground方法。
bind时,会回调onCreate方法,然后回调onBind方法。
结束Service的方式
结束Service的方式分为以下几种:
- stopService():停止Service,通过start方式拉起的Service在停止之后会被销毁。回调onDestroy()方法。
- unbindService():解除Service和调用者的连接。当所有的调用者都解除连接时,Service会被销毁。
Service被销毁时,会回调onDestroy()方法,我们就可以在这里做一些清理工作。
代码示例:
// 停止Service
Intent intent = new Intent(this, MyService.class);
stopService(intent);
// 解除Service和调用者的连接
unbindService(connection);
另外,service内部也可以自己停止,调用stopSelf()方法。
来自腾讯元宝AI的总结,检查无大问题后创建文章。 在 Android 开发中,Gradle、Gradle 插件(Android Gradle Plugin,简称 AGP)、JDK 和 Android SDK 的版本之间有一定的兼容性要求。以下是它们之间的对应关系(截至 2024 年 6 月,基于官方文档和社区实践):
一、Gradle 插件版本(AGP)与 Android Studio 版本对应关系
| Android Studio 版本 | AGP(Gradle Plugin)版本 |
|---|
| Android Studio Flamingo (2022.2.1) | 8.0.x |
| Android Studio Giraffe (2022.3.1) | 8.1.x |
| Android Studio Hedgehog (2023.1.1) | 8.2.x |
| Android Studio Iguana (2023.2.1) | 8.3.x |
| Android Studio Jellyfish (2023.3.1) | 8.4.x |
| Android Studio Koala (2024.1.1) | 8.5.x |
注意:Android Studio 的每个版本通常会推荐使用特定版本的 AGP,建议尽量使用官方推荐的组合,以避免兼容性问题。
二、AGP 版本与 Gradle 版本对应关系
| AGP 版本 | 推荐的 Gradle 版本 |
|---|
| 8.5.x | 8.7 - 8.9 |
| 8.4.x | 8.5 - 8.7 |
| 8.3.x | 8.3 - 8.5 |
| 8.2.x | 8.0 - 8.3 |
| 8.1.x | 7.6 - 8.0 |
| 8.0.x | 7.5 - 7.6 |
| 7.4.x | 7.5 - 7.6 |
| 7.3.x | 7.4 - 7.5 |
| 7.2.x | 7.3 - 7.4 |
| 7.1.x | 7.2 - 7.3 |
| 7.0.x | 7.0 - 7.2 |
说明:Gradle 版本过高或过低都可能导致构建失败,务必参考https://developer.android.com/studio/releases/gradle-plugin#updating-gradle确认兼容性。
三、AGP 版本与 JDK 版本对应关系
| AGP 版本 | 推荐的 JDK 版本 |
|---|
| 8.5.x | JDK 17 |
| 8.4.x | JDK 17 |
| 8.3.x | JDK 17 |
| 8.2.x | JDK 17 |
| 8.1.x | JDK 11 或 JDK 17 |
| 8.0.x | JDK 11 或 JDK 17 |
| 7.4.x 及以下 | JDK 11 |
注意:从 AGP 8.0 开始,官方推荐使用 JDK 11,但从 AGP 8.1 起,JDK 17 也得到支持。使用 JDK 8 已不再被支持(从 AGP 7.0 起已移除对 JDK 8 的支持)。
四、AGP 版本与 Android SDK 版本(compileSdkVersion)对应关系
AGP 本身并不直接限制 compileSdkVersion,但为了兼容性和稳定性,建议:
- AGP 8.x:建议使用
compileSdkVersion 34(Android 14)或更高。 - AGP 7.x:建议使用
compileSdkVersion 33(Android 13)或 34(视情况)。 - AGP 4.x - 6.x:建议使用
compileSdkVersion 30 - 32。
注意:compileSdkVersion 是你用来编译项目的 SDK 版本,它不需要与你的 targetSdkVersion 相同,但建议尽量保持一致或接近,以确保兼容性。
五、总结表格(AGP 8.5.x 为例)
| 项目 | 推荐版本 |
|---|
| Android Studio | Jellyfish (2023.3.1) 或 Koala (2024.1.1) |
| AGP(Gradle Plugin) | 8.5.x |
| Gradle | 8.7 - 8.9 |
| JDK | JDK 17 |
| compileSdkVersion | 34(Android 14)或更高 |
六、如何查看当前项目的版本信息
- Gradle 版本:
- 打开项目根目录下的
gradle/wrapper/gradle-wrapper.properties 文件,查看 distributionUrl,例如:distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
- AGP 版本:
- 打开项目根目录下的
build.gradle(Project 级别)文件,查看:dependencies {
classpath "com.android.tools.build:gradle:8.5.0"
}
- JDK 版本:
- 在 Android Studio 中:
File > Project Structure > SDK Location > JDK location - 或者在终端运行
./gradlew -v 查看使用的 JDK 版本。
- compileSdkVersion:
- 在模块级别的
build.gradle 文件中查看:android {
compileSdkVersion 34
}
多一条备注:
在 Android Studio 中使用GUI界面进行assemble,build等操作时,JDk版本是在设置里选取的对应的版本。如果是在Terminal或者其他环境下使用 ./gradlew assembleRelease 等命令时,所使用的JDK版本是在系统的环境变量中配置的。
Thread.sleep()
Thread.sleep() 方法在 Java 中用于使当前正在执行的线程暂停指定的时间。它的实现涉及到 Java 虚拟机(JVM)以及操作系统层面的协作。
“该方法一般用来告知 CPU 让出处理时间给 App 的其他线程或者其他 App 的线程。”
方法签名:
public static native void sleep(long millis) throws InterruptedException;
可以看到sleep是一个native方法,底层是通过系统调用(System Call)来实现的。
调用 Thread.sleep() 时,有以下需要注意的点:
Thread.sleep() 作用于当前正在执行的线程,会暂停当前线程。暂停意思就是当前线程会放弃 CPU 时间片,用 sleep 后,当前线程会放弃 CPU 的使用权,进入 TIMED_WAITING 状态。这意味着它不会参与 CPU 的调度,直到睡眠时间结束或者被中断。操作系统接收到休眠请求后,会将当前线程从可运行队列中移除,并将其放入一个等待队列,直到指定的休眠时间过去。操作系统内核的调度器负责管理这些等待的线程,并在时间到达后将其重新放回可运行队列。(放回队列不代表立即执行,操作系统内核的调度器会根据优先级等因素再次决定是否执行该线程)- 需要注意,在slepp期间不会释放锁。这是一个非常重要的特性。
Thread.sleep() 不会释放任何线程持有的监视器锁(monitor lock)。如果一个线程在持有锁的情况下调用 sleep,其他试图获取该锁的线程将仍然被阻塞。这与 Object.wait() 方法不同,wait() 会释放锁。 - 另外,
Thread.sleep() 还是一个可中断的方法。在休眠中的线程,其他线程可以调用它的 interrupt() 方法,sleep() 会立即抛出 InterruptedException。这允许你提前唤醒一个正在睡眠的线程。
经典使用场景
GC优化:在快速循环中插入sleep(0)
while (true) {
processBatch();
Thread.sleep(0); // 让出CPU给GC线程
}
降低CPU占用:批处理任务降频
for (int i = 0; i < 1_000_000; i++) {
processItem();
if (i % 100 == 0) {
Thread.sleep(1); // 每100次休眠1ms
}
}
测试调试场景
Thread.sleep(5000);
viewmodel.getTestData();
sleep时间精度问题
操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。
在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。
基于Linux的Android系统则是结合了以上两种方案来分配cpu资源的。
抢占式调度是核心机制
内核可以在任何时刻强制中断当前正在运行的进程(任务),将CPU分配给更高优先级的任务。这是Linux内核(包括安卓)的核心调度特性。例如,当有来电(高优先级实时任务)时,系统会立即抢占当前正在运行的应用进程,优先处理通话相关任务。中断驱动诸如硬件中断(如触摸事件、传感器数据)可以触发内核抢占当前任务,快速响应事件。
时间片轮转(辅助策略)
每个任务被分配一个固定的时间片(CPU执行时间),时间片用完后,即使任务未完成,也会被强制挂起,调度器选择下一个任务运行。CFS调度器(Completely Fair Scheduler):Linux内核默认的调度器(安卓也使用),通过虚拟运行时间(vruntime)动态调整任务的优先级和时间片分配,尽量保证公平性。安卓会根据任务类型(如交互式应用、后台服务)动态调整时间片长度,优先保障前台应用的响应速度。
Thread.sleep() 的实际休眠时间不一定精确。这是因为:
- 操作系统调度: 操作系统调度是基于时间片和优先级进行的,即使休眠时间到了,线程也需要等待操作系统再次调度它。
- 系统负载: 如果系统负载很高,CPU 资源紧张,线程可能无法在精确的时间点被唤醒。
- 时钟粒度: 操作系统时钟中断的粒度(resolution)也影响了
sleep 的精度。例如,如果操作系统的时钟中断是 10 毫秒,那么即使你 sleep(1),线程也可能至少休眠 10 毫秒。
如果在休眠期间发生中断,底层的系统调用会检测到这个中断信号,并返回一个错误码给 JVM。JVM 会捕获这个错误,并向上层 Java 代码抛出 InterruptedException。
Thread.sleep(1000),1000ms后是否立即执行?
不一定,在未来的1000毫秒内,线程不再参与到CPU竞争。那么1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束;况且,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。
Thread.sleep(0),是否有用?
休眠0ms,这样的休眠有何意义?Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,重新计算优先级。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。这也是我们在大循环里面经常会写一句Thread.sleep(0) ,因为这样就给了其他线程获得CPU控制权的权力,界面就不会假死在那里。
SystemClock.sleep()
在 Android 平台中,SystemClock.sleep(long millis) 是一个用于让当前线程休眠指定毫秒数的方法。它位于 android.os.SystemClock 类中,是一个非常常用的工具方法,在 Android 中被推荐使用,尤其是在系统级开发或需要 更稳定、更可预测 的休眠行为时。
public static void sleep(long ms)
{
long start = uptimeMillis();
long duration = ms;
boolean interrupted = false;
do {
try {
Thread.sleep(duration);
}
catch (InterruptedException e) {
interrupted = true;
}
duration = start + ms - uptimeMillis();
} while (duration > 0);
if (interrupted) {
// Important: we don't want to quietly eat an interrupt() event,
// so we make sure to re-interrupt the thread so that the next
// call to Thread.sleep() or Object.wait() will be interrupted.
Thread.currentThread().interrupt();
}
}
从源码中可以看到,SystemClock.sleep 是一个循环调用 Thread.sleep 的方法,直到时间到了才返回。内部做了try catch,在中途如果线程被中断,循环会临时忽略这次打断。在预设的睡眠时间结束之后,也会重新设置中断标志位,以便上层调用者知道线程被中断了。
在使用时需要注意不要在主线程中进行过长的休眠,以避免影响用户体验和应用性能。根据具体需求,也可以考虑使用其他更灵活的调度机制来实现延迟或定时任务。
Kotlin协程的delay
上述的 Thread.sleep 和Kotlin 协程的 delay 函数之间存在一个根本性的区别:Thread.sleep 会阻塞(block)当前线程,而 delay 函数是非阻塞的(non-blocking)。故delay一般不称为阻塞,而是挂起。
delay函数的上层实现
delay 是一个 挂起函数(suspending function),它只能在协程或另一个挂起函数中调用。它的签名如下:
suspend fun delay(timeMillis: Long)
当你调用 delay 时,在 Kotlin 协程层会发生以下情况:
- 协程暂停,而非线程阻塞:
delay 会暂停当前正在执行的协程,而不是它所运行的底层线程。 - 放弃 CPU,但线程可用于其他工作: 当协程被
delay 挂起时,它会从执行它的线程上“脱离”下来,但该线程并没有被阻塞,它可以立即被用来执行其他等待中的协程或任务。 - 状态保存(Continuation): 在编译时,遇到挂起函数,Kotlin 编译器会生成特殊的代码来保存当前协程的执行上下文,包括局部变量、程序计数器(即下一条要执行的指令)等。这个保存下来的上下文被称为 Continuation(续体)。这个过程就是CPS转换。
- 调度器交互:
delay 函数会将这个 Continuation 对象以及指定的延迟时间传递给当前的 CoroutineDispatcher(协程调度器)。调度器负责安排协程的执行。 - 可取消性: 与
Thread.sleep 类似,delay 也是可取消的。如果协程在 delay 期间被取消(例如,通过调用其 Job.cancel() 方法),delay 会立即抛出 CancellationException。
delay的下层实现:JVM 与 CoroutineDispatcher
delay 的非阻塞特性主要由 CoroutineDispatcher 和底层的计时机制实现。
CoroutineDispatcher 是 Kotlin 协程框架中的核心组件,它决定了协程在哪个线程上执行以及如何调度。
当 delay 被调用时,调度器不会让线程调用 Thread.sleep() 来阻塞自己。相反,它会注册一个定时任务。
调度器通常会维护一个任务队列。当协程被遇到 delay 这个挂起函数挂起时,它的 Continuation 会被放入一个内部的延迟任务队列中,并关联一个唤醒时间。
底层计时器: 调度器会利用底层的非阻塞计时机制来处理这些延迟任务。这可能涉及到:
ScheduledExecutorService: 在 JVM 上,CoroutineDispatcher 内部通常会使用 java.util.concurrent.ScheduledExecutorService 来安排在未来某个时间点执行任务。ScheduledExecutorService 维护一个线程池,可以异步地执行定时任务,而不会阻塞调用者线程。- 系统事件循环: 在某些特定环境下(例如 Android平台 的主线程),调度器可能会与操作系统或框架提供的事件循环(event loop)集成,将协程的恢复任务作为事件注册到事件队列中。
具体的挂起和恢复流程可以看之前写的更详细的分析:
Kotlin协程挂起恢复源码解析
概念
Activity是最常用的四大组件之一,主要用于显示用户界面,管理维护界面内的一系列控件和数据,并响应用户的屏幕操作事件。
创建
新建项目时可以自动选择Activity模板。Android Studio会自动帮我们生成Activity的代码,我们只需要在Activity的子类中实现处理逻辑即可。
此外,也可以在后续的实现过程中手动新建新的Activity类,这种方式需要手动修改AndroidManifest.xml文件,将新的Activity注册到清单文件中。
插入,Manifest文件的作用是:
- 声明应用程序的组件,如Activity、Service、BroadcastReceiver等。
- 配置应用程序的权限,如网络访问权限、存储访问权限等。
- 配置应用程序的启动Activity。
- 配置应用程序的主题。
每一个Activity都会绑定一个xml布局文件,加载自己的内容区域里面。
以下是我项目中的一个Activity代码:
public class MainActivity extends AppCompatActivity {
private Fragment loginfragment, signupfragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loginfragment = new LoginFragment();
signupfragment = new SignupFragment();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, loginfragment).commit();
}
}
启动
Activity的启动方式有两种:
- 显式启动。在代码中使用Intent对象指定要启动的Activity类,然后调用startActivity()方法启动Activity。
- 隐式启动。在代码中使用Intent对象指定要启动的Activity的动作和类别,然后调用startActivity()方法启动Activity。
代码示例:
// 显式启动
Intent intent = new Intent(this, MyActivity.class);
startActivity(intent);
// 隐式启动
Intent intent = new Intent("com.example.myaction");
intent.addCategory("com.example.mycategory");
startActivity(intent);
隐式启动在高版本Android中已经被弃用,建议使用显式启动。
传递数据
Activity之间的数据传递可以使用Intent对象。Intent对象可以携带数据,包括基本数据类型、字符串、对象等。
代码示例:
// 传递基本数据类型
Intent intent = new Intent(this, MyActivity.class);
intent.putExtra("key", value);
startActivity(intent);
// 传递字符串
Intent intent = new Intent(this, MyActivity.class);
intent.putExtra("key", "value");
startActivity(intent);
// 传递对象
Intent intent = new Intent(this, MyActivity.class);
intent.putExtra("key", new MyObject());
startActivity(intent);
接收数据
Activity被启动后,可以在onCreate方法里面,检查Intent对象是否携带数据,然后获取数据。 如果Activity已经在前台,而外部又有启动这个Activity的操作,我们可以在onNewIntent方法里面,检查Intent对象是否携带数据,然后获取数据。
代码示例:
public class MyActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
// 检查intent对象是否携带数据
if (getIntent().hasExtra("key")) {
// 获取数据
String value = getIntent().getStringExtra("key");
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// 检查intent对象是否携带数据
if (intent.hasExtra("key")) {
// 获取数据
String value = intent.getStringExtra("key");
}
}
}
销毁
Activity的销毁可以使用finish()方法。
代码示例:
public class MyActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
// 通过一个按钮来销毁Activity
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}
Activity的状态
根据以上的简要流程,可以总结出Activity有四种状态:
- 运行状态。Activity位于返回栈的栈顶,并且处于运行状态。
- 暂停状态。Activity不再位于返回栈的栈顶,但是仍然可见,比如Activity被覆盖住了。
- 停止状态。Activity不再位于返回栈的栈顶,并且不可见,比如Activity被另一个Activity覆盖住了。
- 销毁状态。Activity不再位于返回栈的栈顶,并且被系统回收了。
启动模式
Activity的启动模式有四种:
- standard。默认启动模式,每次启动Activity都会创建一个新的实例。
- singleTop。如果Activity已经位于返回栈的栈顶,那么不会创建新的实例,而是复用已经存在的实例。
- singleTask。如果Activity已经在返回栈中存在,那么不会创建新的实例,而是复用已经存在的实例,并将该Activity之上的所有Activity出栈。
- singleInstance。如果Activity已经在返回栈中存在,那么不会创建新的实例,而是复用已经存在的实例,并将该Activity之上的所有Activity出栈。
可以在Manifest文件中配置Activity的启动模式,也可也在代码中拉起Activity的时候设置启动模式。
代码示例:
<activity android:name=".MyActivity" android:launchMode="singleTop" />
java代码示例:
Intent intent = new Intent(this, MyActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
Activity的生命周期
Activity 类中定义了7个回调方法,覆盖了Activity 生命周期的每一个环节:
- onCreate()。这个方法你已经看到过很多次了,我们在每个Activity 中都重写了这个方法,它会在Activity 第一次被创建的时候调用。你应该在这个方法中完成Activity 的初始化操作,比如加载布局、绑定事件等。
- onStart()。这个方法在Activity 由不可见变为可见的时候调用。onResume()。这个方法在Activity 准备好和用户进行交互的时候调用。此时的Activity 一定位于返回栈的栈顶,并且处于运行状态。
- onPause()。这个方法在系统准备去启动或者恢复另一个Activity 的时候调用。我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶Activity 的使用。
- onStop()。这个方法在Activity 完全不可见的时候调用。它和onPause()方法的主要区别在于,如果启动的新Activity 是一个对话框式的Activity ,那么onPause()方法会得到执行,而onStop()方法并不会执行。
- onDestroy()。这个方法在Activity 被销毁之前调用,之后Activity 的状态将变为销毁状态。
- onRestart()。这个方法在Activity 由停止状态变为运行状态之前调用,也就是Activity被重新启动了。
代码示例,在生命周期回调方法里面加入Log打印,感知运行流程:
public class MyActivity extends Activity {
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate()");
}
@Override
public void onStart() {
super.onStart();
Log.i(TAG, "onStart()");
}
@Override
public void onResume() {
super.onResume();
Log.i(TAG, "onResume()");
}
@Override
public void onPause() {
super.onPause();
Log.i(TAG, "onPause()");
}
@Override
public void onStop() {
super.onStop();
Log.i(TAG, "onStop()");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy()");
}
}
Activity的分类
根据运行时期的作用分类,一个是MainActivity,一个是普通Activity。
MainActivity是程序的入口,它是Android应用程序的主界面,从桌面点击图标之后创建的第一个Activity,用户也可以通过它来启动其他的Activity。
普通Activity是指除了MainActivity之外的其他Activity,它们可以在应用程序中被启动、暂停、停止和销毁。用来承载其他的一些页面功能。
代码示例:
public class MyActivity extends Activity {
@Override
public void onCreate() {
super.onCreate();
}
}
清单文件注册展示:
<activity
android:name=".activity.CameraActivity"
android:exported="false" />
<activity
android:name=".activity.MainActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activity.MusicPlayActivity" />
xmlns
xmlns 是主节点的namespace命名空间的意思,告诉内部语句可以使用哪些合法的属性参数.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳转"/>
</LinearLayout>
- wrap_content:包裹内容,方框的长宽随内容多少而变化。
- id不是必需的,在Java源码中需要find的则需要设置id
- xml文件就是在主节点里写子空间,叶子节点
ImageView的填充方式
设置ImageView填充方式的前提是使用src作为设置图片的来源,否则的话,会导致图片填充方式设置无效的情况。
- scaleType-“matrix”是保持原图大小、从左上角的点开始,以矩阵形式绘图。
- scaleType-“fitXY”是将原图进行横方向(即XY方向)的拉伸后绘制的。
- scaleType-“fitstart”是将原图沿左上角的点(即matrix方式绘图开始的点),按比例缩放原图绘制而成的。
- scaleType-“fitcenter”是将原图沿上方居中的点(即matrix方式绘图第一行的居中的点),按比例缩放原图绘制而成的。
- scaleType=“fitEnd”是将原图沿下方居中的点(即matrix方式绘图最后一行的居中的点),按比例缩放原图绘制而成的。
- scaleType=“Center”是保持原图大小,以原图的几何中心点和ImagView的几何中心点为基准只绘制ImagView大小的图像。
- scaleType=“centerCrop”不保持原图大小,以原图的几何中心点和lmagView的几何中心点为基准,只绘制ImagView大小的图像(以填满lmagView为目标,对原图进行裁剪)
- scaleType=“centerlnside”不保持原图大小,以原图的几何中心点和lmagView的几何中心点为基准,只绘制ImagView大小的图像(以显示完整图片为目标,对原图进行缩放)
完整的页面创建过程包括三个步骤
- 在layout目录下创建XML文件
- 创建与XML文件对应的Java代码
- 在AndroidManifest.xml 中注册页面配置
<activity
android:name=".NewActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ComposeDemo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
每次手动创建一个新的activity都要在清单文件中注册一下
或者,可以在new里面一键生成activity,清单文件也注册好了
设置文本
- 在 XML 文件中通过属性 android:text 设置文本
- 在 Java 代码中调用文本视图对象的 setText 方法设置文本
设置文本大小
(1)在Java代码中调节字体大小
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv_hello = findViewById(R.id.tv hello);
tv_hello.setTextSize(40);
}
(2)在xml文件里直接设置textsize
字体大小单位
- 使用sp作为字体大小单位,会随着系统的字体大小改变
- 而dp作为单位则不会随之改变
通常情况下,我们还是建议使用sp作为字体的单位,除非一些特殊的情况,不想跟随系统字体变化的,可以使用dp.
字体颜色设置
同样有两种设置方式
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
TextView tv_hello = findViewById(R.id.tv_hello);
tv_hello.setTextColor(Color.BLUE);
//第一种是在Java代码中获取对象再进行设置
}
<TextView
android:id="@+id/tv_hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello"
android:textSize="40sp"
android:textColor="@color/purple_200"/>
3秒自动跳转界面的代码
package com.example.zhanfeng_android;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.TextView;
public class Helloworld extends AppCompatActivity {
@Override
protected void onResume(){
super.onResume();
goNextPage();
}
private void goNextPage(){
TextView tv_hello = findViewById(R.id.tv_hello);
tv_hello.setText("3秒后自动跳转下一界面");
new Handler(Looper.myLooper()).postDelayed(mGoNext, 3000);
}
private Runnable mGoNext = new Runnable(){
@Override
public void run(){
startActivity(new Intent(Helloworld.this, NewPage.class));
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
}
}
所有看得到的控件都可以叫做视图,视图尺寸的设置
视图宽度通过属性android:layout_width表达,视图高度通过属性android:layout_height表达,宽高的取值主要有下列三种:
- match_parent:表示与上级视图保持一致。
- wrap_content:表示与内容自适应,
- 以dp为单位的具体尺寸。
自适应的内容不可超出上级视图,否则会出异常
要在Java代码中设置宽高,需要先将xml中属性设为wrap_content
视图间距
- margin为当前的视图和它的平级视图之间的距离
- padding为当前视图和其下级视图的间距
- padding为控件本身的边框和里面的内容的距离
如果不设置方向就是四周的边框均设置距离
举例:
LinearLayout最好用的就是可以设置内部控件所占的比重。 在子控件里设置 android:layout_weight 可以设置控件占容器的比例
LinearLayout(线性布局)
以水平或垂直方向排列子视图。 通过android:orientation属性设置排列方向。 子视图可以通过android:layout_weight属性设置权重,以实现按比例分配空间。
RelativeLayout(相对布局)
允许子视图相对于其他子视图或父视图进行定位。 通过android:layout_alignParentTop、android:layout_toLeftOf等属性设置相对位置。
FrameLayout(帧布局)
所有子视图都堆叠在左上角,后添加的子视图会覆盖前面的子视图。 通常用于实现叠加效果,如地图上的标记。
TableLayout(表格布局)
以表格形式排列子视图,类似于HTML中的<table>标签。 通过android:stretchColumns属性设置可拉伸的列。
GridLayout(网格布局)
将子视图放置在网格中,可以指定行列数。 通过android:layout_row和android:layout_column属性设置子视图的位置。
ConstraintLayout(约束布局)
以相对位置和约束条件来排列子视图。 每个子视图至少需要两个方向的约束,可以通过app:layout_constraintStart_toEndOf等属性设置约束。
android的text和tools的text区分。tools是调试时的工具,而android才是运行时实际显示的。
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="real"
tools:text="test_text" />
对齐方式
单独一个gravity是设置下属视图
而layout_gravity是设置自己在父布局中的对齐方式
实例演示,对比代码和界面来看。
布局页面的嵌套
<include layout="@layout/fragment_app_detail"/>
xml文件本质
xml布局文件中的一个个view节点,最终也是会被解析成java代码类采用xml方式进行布局,只不过是为了方便我们开发者进行直观的布局。
其实也可以直接用Java代码来写和修改布局。
git插入技巧
查看最近提交的版本号:git reflog 回退到历史版本:git reset –hard ad2080c
应用权限声明
都要在manifest文件中进行标注
Java和xml布局文件是怎样关联的
检查Manifest文件,找到的启动页面MainActivity。
MainActivity调用onCreate方法–>调用setContentView方法–>R.layout.activity_main找到布局文件。
Gradle文件分析
新手在工程方面碰到的错误很多和gradle文件有关 Gradle是什么,有什么作用? 编译,打包安卓工程的一个工具 Project中的Module
项目级里的Gradle插件版本和Gradle版本的匹配关系要对应
插件版本7.2,那么Gradle版本需要7.3.3以上

而模组级的Gradle文件主要关注其依赖项
android {
namespace = "com.stephen.redfindemo"
compileSdk = 34
defaultConfig {
applicationId = "com.stephen.redfindemo"
minSdk = 30
targetSdk = 34
versionCode = 32
versionName = "2.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
其中各个参数的意义:compileSdkVersion,minSdkVersion,targetSdkVersion,build ToolsVersion
- compileSdkVersion 是编译代码所使用的sdk 版本,并且与sdk manager 里面下载的那些sdk platforms是对应的。也就是说,compileSdk Version使用的版本,在sdk manager 里面必须是已经下载了才能用。最新的as做了优化,即使你没有下,当写上某个版本后,as会自动帮你下载。
- minSdkVersion 是对app可运行的手机设备的最小版本限制。与sdk manager里面下载的东西无关,只是一个标识而已。
- targetSdkVersion 是对app要运行的手机设备的目标版本的标识,也与sdk manager 里面下载的东西无关,标识了该app是为某个版本的手机设备而设计的,在这个目标版本的手机上做了充分的测试。
当你的手机版本大于这个目标版本时,该app也能运行。因为高版本的手机是可以运行低版本软件的。这也符合常理,越先进的手机功能应该越强大嘛,不仅能运行新东西,也能兼容老东西。
因此,minSdkVersion 和targetSdkVersion是对我们开发的app所能运行设备的系统版本的范围约束。最低不能小于minSdkVersion,但没有最高限制。原因上面已经说了,高版本手机可以运行低版本软件嘛。从名字上也可以理解,它叫targetSdkVersion 而没有叫maxSdk Version
build ToolsVersion 是独立出来的一个东西,和上面三个都没关系,就是构建代码的工具的版本。 与sdk manager 里面的sdk tools 下载的东西是对应的。要想使用某个版本,必须得已经下载了对应的sdk Build-tools。
重要关系:minSdkVersion <= targetSdkVersion <= compileSdkVersion
RecyclerView使用流程
首先,在页面里添加recyclerview控件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
第二步,写item的xml控件,以通讯录为例,需要添加一个装头像的ImageView,一个姓名TextView,一个电话号码TextView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/head"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="30sp"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="20sp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
第三步定义一个Person类,添加姓名,电话的String,还有头像的ID,自动生成构造器和getter,setter 第四步写MyAdapter类,继承自RecyclerView.Adapter,覆写以下三个方法
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
然后再在里面写MyViewHolder类,继承自RecyclerView.ViewHolde,这个类用于装来自item里的控件。
class MyViewHolder extends RecyclerView.ViewHolder{
TextView nametext;
TextView phoneText;
ImageView head;
public MyViewHolder(View itemView) { //传View对象即可
super(itemView); //调用super方法
this.nametext = itemView.findViewById(R.id.textView);
this.phoneText = itemView.findViewById(R.id.textView2);
this.head = itemView.findViewById(R.id.imageView);
}
}
fragment的使用
fragment是将屏幕中的控件集中统一管理,所以本身可以直接作为页面来使用。
使用示例:
第一,建立新的空白fragment
第二,fragment的页面解析方式和activity有点不同,需要用到inflate,在xml里写完界面之后,传到view类
第三,fragment里对于文本和按钮处理方式和activity相同。
第四,在activity的xml文件里添加fragment控件,id必须添加。
以按钮进行多界面切换,fragment管理器的使用
private void replaceFragment(Fragment fragment){
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
最后一定要commit才能生效.
activity和fragment的通信
使用bundle类来传递,键值对的形式存储信息。 在fragment的java代码里,使用getarguments()来接收信息,获得的是bundle对象实例。从中解析数据。
动态添加fragment的小结
- 创建一个待处理的fragment
- 获取FragmentManager,一般都是通过getSupportFragmentManager()
- 开启一个事务 transaction,一般调用fragmentManager的beginTransaction()
- 使用transaction进行fragment的替换
- 提交事务
Android的数据存储
SharedPreferences
SharedPreferences是Android平台上一个轻量级的存储类,用来保存应用的一些常用配置,比如Activity状态,Activity暂停时,将此activity的状态保存到SharedPereferences中;当Activity重载,系统回调方法onSaveInstanceState时,再从SharedPreferences中将值取出。SharedPreferences提供了java常规的Long、Int、String等类型数据的保存接口。SharedPreferences类似过去Windows系统上的ini配置文件,但是它分为多种权限,可以全局共享访问。提示最终是以xml方式来保存,整体效率来看不是特别的高,对于常规的轻量级而言比SQLite要好不少,如果真的存储量不大可以考虑自己定义文件格式。该xml在应用程序私有目录下,其他程序无法访问。
使用Handler处理多线程之间的信息传递
了解了Message、Handler、MessageQueue以及Looper的基本概念后,我们再来把异步消息处理的整个流程梳理一遍。首先需要在主线程当中创建一个Handler对象,并重写handleMessage( )方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条消息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage ( )方法中。由于Handler是在主线程中创建的,所以此时 handleMessage( )方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。整个异步消息处理机制的流程示意图如下图所示。
JSON数据解析
- JSONObject 表示一个JSON节点
- JSONObject.opt(“key”)根据键获取值,如果没找到匹配的键,则返回空。(推荐)
- JSONObject.get(“key”)根据键获取值,如果没找到匹配的键,则抛出异常。
根据值的类型获取:
- JSONObject.optString(“key”)
- JSONObject.optInt(“key”)
- JSONObject.optBoolean(“key”)
- JSONObject.optJsONObject(“key”)
- JSONObject.optJSONArray(“key”)