1 Android中屏幕密度的计算方式
在 Android 中,屏幕密度的计算方式是通过比较屏幕上每英寸(Inch)的物理像素数目来确定的。屏幕密度是指屏幕上每英寸的物理像素数目(通常表示为 DPI 或 PPI)。
具体而言,Android 中的屏幕密度计算方式如下:
屏幕密度(dpi)= √(横向像素数² + 纵向像素数²)/ 屏幕对角线英寸数
例如,对于 5.5 英寸、750*1334 像素的屏幕,计算屏幕密度的公式如下:
屏幕密度(dpi)= √(750² + 1334²)/ 5.5 ≈ 326 dpi
屏幕密度通常被分为以下几种类型:
-
- ldpi(低密度):表示屏幕密度为 120 dpi;
-
- mdpi(中密度):表示屏幕密度为 160 dpi;
-
- hdpi(高密度):表示屏幕密度为 240 dpi;
-
- xhdpi(超高密度):表示屏幕密度为 320 dpi;
-
- xxhdpi(超超高密度):表示屏幕密度为 480 dpi;
-
- xxxhdpi(超超超高密度):表示屏幕密度为 640 dpi。
在开发 Android 应用时,需要根据屏幕密度的不同,提供不同密度的资源文件,以确保应用在不同类型的设备上都可以正常显示。
通常,将资源文件分别放置在不同密度的目录下,例如 res/drawable-ldpi、res/drawable-mdpi、res/drawable-hdpi 等。当应用在不同的设备上运行时,系统会自动根据设备的屏幕密度,从相应的目录中加载对应密度的资源文件。
2 Android中dp与px的转换
在 Android 开发中,使用 dp 和 px 是常见的尺寸单位,使用 dp 能够使得应用在不同屏幕大小和密度的设备上有更好的适应性,使用 px 则是直接使用实际的像素数目。
下面介绍一下 dp 与 px 之间的转换:
2.1 dp 转 px
dp 和 px 的转换需要考虑设备的屏幕密度,一般来说,设备的屏幕密度越高,则 dp 转换成的 px 像素数目越多。
一般情况下,我们可以使用 Android SDK 提供的一个常量进行转换,这个常量为 density,是指当前设备的屏幕密度。例如,将 dp 值转换为 px 的方法如下:
public int dp2px(int dpValue) {
float scale = getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
在这个方法中,我们首先获取了当前设备的屏幕密度,然后将 dp 值乘以 density 值,最后加上 0.5f 并强制转换为 int 类型,即可得到转换后的像素值。
2.2 px 转 dp
如果想将 px 像素数目转换为相应的 dp 值,则需要按照以下公式进行计算:
dp = px / density
同样地,我们可以编写一个方法进行转换:
public int px2dp(int pxValue) {
float scale = getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
在这个方法中,我们首先获取了当前设备的屏幕密度,然后将像素数目除以 density 值,最后加上 0.5f 并强制转换为 int 类型,即可得到转换后的 dp 值。
需要注意的是,在进行 dp 和 px 的转换时,需要使用 context.getResources().getDisplayMetrics().density 来获取当前设备的屏幕密度值,而不能使用 DisplayMetrics 类的 density 属性,因为 DisplayMetrics 类的 density 属性是固定的,而不是根据当前设备的屏幕密度而变化的。
3 Android中Activity的启动模式
Android 中的 Activity 启动模式非常重要,它可以影响 Activity 的生命周期和任务栈的运作。不同的启动模式适用于不同的场景,下面给出常见的启动模式以及应用场景:
-
- Standard(标准模式) 标准模式是 Activity 的默认启动模式。每次启动 Activity 都会创建一个新的实例,不管这个 Activity 是否已经存在。适用于独立运行的 Activity,不需要特殊的启动模式。
-
- SingleTop(栈顶复用模式) singleTop 模式表示如果当前 Activity 在栈顶,则直接复用该 Activity,不会创建新的实例。如果不在栈顶,则创建新的实例,并加入任务栈的栈顶。适用于需要及时更新数据的情况,比如聊天界面、通知界面等。
-
- SingleTask(栈内复用模式) singleTask 模式表示如果当前任务栈中不存在该 Activity,则创建新的实例,并放在栈底。如果存在,则将该 Activity 上面的所有 Activity 弹出栈,使其处于栈顶位置。适用于需要保持唯一性的 Activity,比如主界面、登录界面等。
-
- SingleInstance(单实例模式) singleInstance 模式表示 Activity 是全局的,独立于其他任务栈。在该模式下,系统会创建一个新的任务栈来存放该 Activity 的实例,并且该任务栈中只能存在该 Activity 的实例。适用于需要和其他应用程序隔离的 Activity,比如电话界面、相机界面等。 需要注意的是,启动模式只影响 Activity 的启动方式和任务栈的运作,不会影响 Activity 的生命周期。在使用 Activity 启动模式时,需要根据实际需求选择合适的启动模式,才能达到最佳的效果。
4 Android Service 的启动方式
在 Android 开发中,可以通过以下方式开启 Service
4.1 startService()
startService() 方法用于启动一个 Service,不会影响调用者的生命周期,除非调用 stopService() 或 stopSelf() 方法。使用 startService() 启动 Service 后,Service 会一直运行,直到调用 stopService()、stopSelf() 或 Service 自己调用 stopSelfResult()。
4.2 bindService()
bindService() 方法用于启动一个 Service,会影响调用者的生命周期,调用者与 Service 之间建立起连接,可以通过 ServiceConnection 回调接口进行通信。使用 bindService() 启动 Service 后,Service 仅在连接存在时运行,只有调用者与 Service 都停止后才会被销毁。
4.3 startForegroundService()
startForegroundService() 方法用于启动一个前台 Service,与 startService() 类似,但是启动的 Service 必须调用 startForeground() 方法,将 Service 放在前台通知栏中显示。
4.4 startIntentService()
startIntentService() 方法用于启动一个 IntentService,会在后台执行任务,任务执行完毕后自动停止 Service。
使用 IntentService 可以避免手动停止 Service 的操作,从而提高应用的稳定性和可靠性。
需要注意的是,开启 Service 后需要在适当的时候停止 Service,以避免资源浪费和内存泄漏。可以通过 stopService()、unbindService() 来停止 Service,或者在 Service 内部调用 stopSelf()、stopSelfResult() 来停止 Service。
5 Android 广播的注册方式
在 Android 开发中,可以通过以下方式注册广播:
5.1 静态注册
静态注册是指在 AndroidManifest.xml 文件中声明广播接收器,并指定接收的广播类型和处理逻辑。这种方式在应用启动时就会注册广播接收器,只有在应用被卸载或更新时才会被注销。静态注册的优点是方便,缺点是无法动态添加或移除广播接收器,且应用无法接收未运行时的广播。
5.2 动态注册
动态注册是指在代码中使用 registerReceiver() 方法注册广播接收器,并通过 unregisterReceiver() 方法注销。
动态注册的优点是可以在运行时动态添加或移除广播接收器,缺点是需要在恰当的时间注销广播接收器,否则会引起内存泄漏或其他不必要的问题。
一般建议在 Activity 的 onResume() 方法中注册广播接收器,在 onPause() 方法中注销广播接收器。
需要注意的是,在注册广播时需要指定 IntentFilter,即要接收的广播类型。可以通过 IntentFilter.addAction() 方法添加要接收的广播类型,也可以在 IntentFilter 构造函数中指定。广播类型可以是系统定义的,也可以是自定义的,根据实际需求选择合适的广播类型。
6 Android中Activity与fragment通信方式
在 Android 中,Activity 与 Fragment 之间可以通过以下几种方式进行通信:
6.1 使用接口回调
Activity 可以定义一个接口,在 Fragment 中实现该接口,并在 Activity 中获取 Fragment 实例后将该接口传递给 Fragment,这样 Fragment 就可以通过接口回调的方式与 Activity 进行通信。
6.2 使用广播
Activity 可以通过发送广播的方式通知 Fragment 进行相应的操作,Fragment 可以注册广播接收器并在接收到广播时执行相应的操作。需要注意的是,使用广播进行通信可能会影响性能,建议在必要时才使用。
6.3 使用事件总线
事件总线是一种常用的通信方式,常见的事件总线包括 EventBus 和 RxJava。
Activity 可以发送事件,Fragment 可以订阅事件并执行相应的操作。需要注意的是,使用事件总线需要添加相关依赖库,建议在大型项目中使用。
6.4 直接调用方法
如果 Fragment 与 Activity 之间的关系比较紧密,也可以直接调用 Activity 中的方法进行通信。Activity 可以获取 Fragment 实例并调用其方法,或者 Fragment 可以通过 getActivity() 方法获取 Activity 实例并直接调用其方法。需要注意的是,直接调用方法可能会导致耦合性较高,建议在必要时才使用。
综上所述,不同的通信方式适用于不同的场景,需要根据实际需求选择合适的方式。
7 Android中如何实现加载大图
在 Android 中,如果要加载大图,可以考虑以下几种方法:
7.1 使用 BitmapRegionDecoder
BitmapRegionDecoder 是 Android 提供的一个类,可以用来加载大图并显示其中的一个区域。使用这种方法,可以避免一次性加载整张大图,从而降低内存占用。
需要注意的是,使用 BitmapRegionDecoder 加载大图需要在子线程中进行,否则可能会引起 ANR。
7.2 使用 BitmapFactory.Options
可以通过 BitmapFactory.Options 中的 inSampleSize 属性来缩小图片的尺寸,从而减少内存占用。inSampleSize 的值应该是 2 的指数,表示缩小的倍数。需要注意的是,缩小图片的尺寸会降低图片的质量,需要根据实际需求选择合适的缩小倍数。
7.3 使用 Glide 或 Picasso 等第三方库
Glide 和 Picasso 等第三方库可以自动对加载的图片进行压缩和缩放,从而避免一次性加载整张大图。同时,这些库也提供了其他的图片加载功能,如图片缓存、图片旋转、图片裁剪等。
7.4 将图片分割成多个小图
将图片分割成多个小图,并在需要时分别加载并拼接成一张完整的大图。
这种方法可以避免一次性加载整张大图,但需要进行额外的处理和管理,比较复杂。
需要注意的是,加载大图可能会引起 OOM(Out of Memory)错误,因此需要合理地进行内存管理,避免一次性加载过多的图片。同时,也需要考虑用户的体验,如在加载图片时显示加载进度条,避免用户等待时间过长。
8 Android中缓存数据的方式有哪些
Android 中常用的缓存数据的方式主要有以下几种:
8.1 SharedPreferences
SharedPreferences 是 Android 开发中最为常见的轻量级数据存储方式之一,适合存储一些简单的键值对数据,比如用户的设置、应用的配置等。通过 SharedPreferences 存储的数据会存储在应用的私有目录下。
8.2 文件缓存
将数据以文件形式存储在本地,可以使用 Java 中的 File 类或 Android 中的 Context.getCacheDir() 方法保存数据。适合存储一些较大的或者结构比较复杂的数据,如图片、音频、视频等。
8.3 数据库
使用 SQLite 数据库存储数据,适合存储数据量较大或结构比较复杂的数据。通过 SQLiteOpenHelper 或者 ContentProvider 可以方便地对数据库进行增删改查操作。
7.4 内存缓存
将数据保存在内存中,适合存储需要频繁读写的数据,如图片缓存等。Android 中可以使用 LruCache 类实现内存缓存。
需要注意的是,缓存数据虽然可以提高应用性能,但也要注意缓存数据的过期时间和清除策略,避免缓存数据占用过多的存储空间。另外,对于一些敏感的数据,也应该采取加密等措施来保证数据安全。
9 Android LruCache 缓存策略
LRU 缓存的基本原理是,缓存中的数据按照最近访问的时间顺序排序,当缓存满时,删除最近最少使用的数据,从而保证缓存中的数据是最近访问过的,并且可以最大程度地利用缓存空间。
在 Android 中,可以通过 LruCache 类来实现 LRU 缓存。 LruCache 类的使用步骤如下:
- 创建 LruCache 实例 可以通过 new LruCache<K,V> (int maxSize) 方法来创建 LruCache 实例,其中 maxSize 表示缓存的最大容量。
- 存储数据 可以通过 put(K key, V value) 方法向 LruCache 中存储数据。如果存储的数据大小超过了缓存的最大容量,LruCache 会自动删除最近最少使用的数据。
- 获取数据 可以通过 get(K key) 方法从 LruCache 中获取数据。如果数据存在于缓存中,LruCache 会将该数据移动到缓存的末尾,并返回该数据;否则返回 null。
10 Android中自定义View的步骤
在 Android 中,实现自定义 View 的一般步骤如下:
- 创建自定义 View 的类:可以通过继承 View 或者 ViewGroup 类来创建自定义 View 的类,并实现相应的方法来实现自定义 View 的功能。
- 提供自定义 View 的属性:在自定义 View 的类中,通过使用 @Attr、@Styleable 注解来定义自定义 View 的属性,并在 attrs.xml 文件中声明对应的属性。
- 重写 onMeasure 方法:onMeasure 方法用于测量自定义 View 的大小,可以根据自定义 View 的属性和布局的要求来计算自定义 View 的测量值。
- 重写 onDraw 方法:onDraw 方法用于绘制自定义 View,可以在该方法中使用 Canvas 对象进行绘制操作。
- 处理触摸事件:如果需要处理触摸事件,可以重写 onTouchEvent 方法来处理。
- 处理滑动事件:如果需要支持滑动操作,可以使用 Scroller 类来实现平滑滚动效果。
- 提供动画效果:可以使用动画来为自定义 View 添加动态效果,比如使用属性动画实现 View 的平移、旋转、缩放等效果。
- 处理自定义 View 的生命周期:可以通过重写 onAttachedToWindow、onDetachedFromWindow 等方法来处理自定义 View 的生命周期。 通过以上步骤,就可以完成自定义 View 的实现。需要注意的是,在创建自定义 View 时,要尽量避免使用硬编码、耦合度高等问题,增强自定义 View 的可复用性和可维护性。
11 概述一下Android中的事件分发机制
在 Android 中,事件分发机制是指当用户通过触摸屏幕、按下按键等操作时,系统对这些事件的处理流程。事件分发机制主要涉及以下三个对象:View、ViewGroup 和 Window。
事件分发机制主要包括以下几个步骤:
- 系统将触摸事件传递给顶层的 Window,由 Window 将该事件传递给具体的 View。
- View 会将触摸事件传递给自己的 onTouchEvent() 方法进行处理。如果该 View 没有处理该事件,则将该事件传递给其父 ViewGroup 进行处理。
- ViewGroup 会先将该事件传递给自己的 onTouchEvent() 方法进行处理。如果该 ViewGroup 没有处理该事件,则将该事件传递给其子 View 进行处理。
- 如果事件仍未被处理,则将事件传递给其上一级的 ViewGroup 进行处理,直到事件被处理或者到达顶层的 Window。
- 如果事件都没有被任何 View 或 ViewGroup 处理,则该事件被判断为无效事件。
需要注意的是,事件分发机制的具体实现可能会受到 View 的特定实现和事件类型的影响,比如ScrollView会拦截滑动事件进行处理。同时,通过重写 View 或 ViewGroup 的方法,也可以对事件分发机制进行自定义实现。
11 概述一下 Handler的原理
Handler 是 Android 中用于处理消息和线程间通信的机制。Handler 的原理主要包括以下几个步骤:
- 创建一个 Handler 对象 在创建 Handler 对象时,系统会自动将该 Handler 与创建它的线程的 MessageQueue 相关联,同时该 Handler 会与创建它的线程的 Looper 相关联。
- 向 Handler 发送消息 当通过 Handler 的 sendMessage() 方法向 Handler 发送消息时,系统会创建一个 Message 对象,并将该对象添加到 Handler 对应的 MessageQueue 中。
- 将消息分发给目标线程 当 Looper 准备处理该线程的 MessageQueue 时,系统会将 MessageQueue 中的消息一个一个地取出,并根据消息中的 target 属性,将消息分发给对应的 Handler 进行处理。
- 处理消息 当 Handler 接收到消息后,会根据消息中的 what 属性和 Handler 内部的 handleMessage() 方法进行相应的处理。
- 处理完成后,可能发送新的消息 在 handleMessage() 方法中,如果需要向该 Handler 发送新的消息,则可以调用 sendMessage() 方法,将新的消息添加到该 Handler 对应的 MessageQueue 中。
总的来说,Handler 的原理是通过 MessageQueue 和 Looper 来实现的,当通过 Handler 的 sendMessage() 方法向 Handler 发送消息时,系统会将该消息添加到 MessageQueue 中,然后通过 Looper 依次取出消息,并将其分发给对应的 Handler 进行处理。
12 Android中子线程使用Handler应该注意什么
在 Android 中,子线程使用 Handler 进行线程间通信时,需要注意以下几点:
-
确认与哪个 Looper 相关联 与主线程不同,子线程默认是没有关联 Looper 的。因此,在使用 Handler 时,需要先创建一个 Looper 对象,并使用该对象创建一个 Handler 对象。
-
避免内存泄漏 当子线程结束时,如果 Handler 对象没有被及时清理,则可能会导致内存泄漏问题。因此,在使用 Handler 时,需要尽量避免使用匿名内部类,以便在适当的时候释放 Handler 对象。
-
不要在子线程中更新 UI Android 中规定,只能在主线程中更新 UI,否则可能会导致程序崩溃。因此,在使用 Handler 时,需要注意不要在子线程中直接更新 UI,而是需要通过 Handler 将更新 UI 的操作发送给主线程进行处理。
综上所述,使用 Handler 进行线程间通信时,需要注意与哪个 Looper 相关联、避免内存泄漏,并且不要在子线程中更新 UI。
13 Android 中创建子线程的方式有哪几种
13.1 使用 Thread 类进行创建
Thread 是 Java 中的一个类,可以通过继承 Thread 类或者创建 Thread 对象并传入 Runnable 对象来创建子线程
13.2 使用 Runnable 接口进行创建
Runnable 是 Java 中的一个接口,可以通过实现 Runnable 接口并将其传入 Thread 对象来创建子线程。
13.3 使用 AsyncTask 类进行创建
AsyncTask 是 Android 中的一个类,可以通过继承 AsyncTask 类并重写其方法来创建子线程。AsyncTask 可以方便地进行 UI 操作,并且不需要手动处理线程间通信问题。
13.4 使用线程池进行创建
线程池是一种可以重复利用线程的机制,可以减少创建和销毁线程所带来的开销。Android 中常用的线程池包括 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor。
14 Android中子线程与主线程的通信方式
在 Android 中,子线程与主线程之间的通信是一种常见的需求,通常有以下几种方式:
14.1 使用 Handler
Handler 是 Android 中的一个类,可以用来在不同线程间传递消息。主线程可以创建一个 Handler 对象,并将其传递给子线程。子线程可以在需要更新 UI 界面时,通过调用 Handler 的 post 方法来通知主线程进行相应的操作。例如:
Handler handler = new Handler(Looper.getMainLooper());
// 在子线程中执行耗时操作
new Thread(new Runnable() {
@Override
public void run() {
// 子线程要执行的代码
// 发送消息通知主线程更新 UI
handler.post(new Runnable() {
@Override
public void run() {
// 主线程要执行的代码
}
});
}
}).start();
14.2 使用 runOnUiThread 方法
Activity 类提供了一个 runOnUiThread 方法,可以用来在主线程中执行代码。子线程可以通过该方法来更新 UI 界面。例如:
new Thread(new Runnable() {
@Override
public void run() {
// 子线程要执行的代码
// 在主线程中更新 UI
runOnUiThread(new Runnable() {
@Override
public void run() {
// 主线程要执行的代码
}
});
}
}).start();
14.3 使用 AsyncTask
AsyncTask 是 Android 中的一个类,在 doInBackground 方法中执行耗时操作,在 onPostExecute 方法中更新 UI 界面。例如:
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
// 子线程要执行的代码
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// 主线程要执行的代码
}
}.execute();
14.4 使用 broadcastReceiver 可以用 broadcastReceiver 来实现子线程向主线程发送消息。子线程发送 broadcast,主线程接收 broadcast,然后在主线程的 onReceive 方法中进行相应的操作。例如:
// 在子线程中发送 broadcast
Intent intent = new Intent("com.example.UPDATE_UI");
sendBroadcast(intent);
// 在主线程中注册 broadcastReceiver
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 主线程要执行的代码
}
};
registerReceiver(receiver, new IntentFilter("com.example.UPDATE_UI"));
综上所述,Android 中子线程与主线程的通信方式有使用 Handler、使用 runOnUiThread 方法、使用 AsyncTask 和使用 broadcastReceiver。根据实际需求选择合适的方式。
15 概述一下Android中的冷启动与热启动
在 Android 中,应用启动有两种方式:冷启动和热启动。
15.1 冷启动
是指在应用没有在后台运行的情况下,点击应用图标启动应用。
这种方式需要进行完整的应用启动过程,包括创建新的进程、创建应用对象、创建应用的界面和初始化数据等操作。由于需要执行完整的启动过程,所以耗时较长。
15.2 热启动
是指在应用已经在后台运行的情况下,再次点击应用图标启动应用。
这种方式不需要重新创建进程和应用对象,而是直接从后台切换到前台并恢复应用的状态。由于不需要进行完整的启动过程,所以速度较快。
需要注意的是,如果应用在后台运行较长时间且系统资源紧张,也可能会被系统回收掉,此时再次启动应用就会触发冷启动过程。
为了提高用户体验,Android中的应用通常会对启动进行优化,例如使用启动页、预加载等方式,降低用户感知到的启动耗时,提高应用启动速度。同时,在应用开发中也需要注意启动优化,减少启动时间,提高用户体验。
16 Android运行时类的加载机制
在 Android 中,Java 代码会被编译成 .class 文件,然后打包成 .dex 文件,在运行时加载到虚拟机中执行。 具体的过程如下:
16.1
.dex 文件的加载 在应用启动后,Android 系统会为应用分配一个独立的虚拟机 Dalvik 或 ART。Dalvik 和 ART 会分别加载应用的 .dex 文件,将其中的代码解析成字节码,在虚拟机中执行。
16.2 类的查找
虚拟机在加载应用的 .dex 文件时,会扫描这些文件,查找其中的类信息并记录在一个列表中。当需要加载某个类时,虚拟机会先在这个列表中查找,如果找到了就直接加载。如果没有找到,则会通过类加载器向其他 .dex 文件或系统的类库中查找。
16.3. 类的加载
如果找到了需要加载的类,虚拟机会先检查该类是否已经被加载过。如果没有加载过,就会为这个类创建一个 Class 对象,并在虚拟机中为其分配内存空间。然后,虚拟机会根据类的信息,加载这个类的父类和接口,并将这些内容一起存储在内存中。
16.4 类的链接
在类加载的过程中,虚拟机会对这个类进行链接。
链接的过程包括验证、准备和解析三个阶段。其中,验证阶段会检查这个类的信息是否合法;准备阶段会分配并初始化类变量的内存空间;解析阶段会将符号引用转换成直接引用。
16.5 类的初始化
最后,如果这个类还没有被初始化过,就会触发其初始化过程。类的初始化过程包括静态变量的赋值和静态代码块的执行等操作。
总之,Android 运行时类的加载机制和传统的 Java 运行时类加载机制比较相似,但有一些细节上的差别。例如,Android 运行时环境采用的是基于 .dex 文件的虚拟机,而不是基于 .class 文件的虚拟机。此外,Android 系统还可以通过类加载器加载外部的 .jar 文件,从而扩展应用的功能。
17 Android中对象的内存回收机制
在 Android 中,对象的内存回收机制由垃圾回收器来实现。垃圾回收器是一个系统级别的服务,它会定期扫描应用的内存空间,检查哪些对象已经不再被引用,然后将这些对象标记为垃圾,并回收它们所占用的内存空间。 对象的内存回收机制主要涉及以下几个方面:
17.1. 引用计数
引用计数是一种简单直观的内存回收方式,它记录每个对象被引用的次数。当对象的引用计数减为 0 时,就可以将其回收。但是,引用计数方式容易出现循环引用的问题,导致一些对象无法被回收,因此在 Android 中并没有采用该方式进行内存回收。
17.2. 标记清除
标记清除是一种基于可达性分析的内存回收方式。垃圾回收器会从根对象(如 Activity、Service 等)开始遍历,找出所有被引用的对象,并在堆上为这些对象打上标记。然后,垃圾回收器会扫描堆上所有的对象,将没有标记的对象视为垃圾,将其回收。缺点是标记和清除过程效率较低。
17.3. 复制算法
复制算法是一种基于空间复制的内存回收方式。它将堆空间分为两个相等的部分,每次只使用其中一部分,当这一部分的空间用尽时,将其中的存活对象复制到空间未用尽的另一部分上,并清除原来的部分。缺点是浪费了一半的空间。
17.4. 标记整理
标记整理是一种综合了标记清除和复制算法的内存回收方式。
它先进行标记,然后把所有存活的对象往堆的一端移动,把未被使用的空间集中起来,最后清除所有移动后的垃圾对象。缺点是移动对象的过程需要耗费一定的时间。
总之,Android 中的对象内存回收机制主要采用基于可达性分析的标记清除和标记整理算法,以及一些优化手段来提高内存回收效率。应用开发者可以通过减少对象的创建、合理使用缓存等手段来减轻垃圾回收器的负担,从而提高应用的性能和稳定性。
18 Android中如何实现插件动态加载
在 Android 中,实现插件化主要有两种方式:动态加载和静态加载,其中动态加载是指在应用程序运行时加载插件,而静态加载是指在编译时加载插件。
下面介绍一下 Android 中如何实现动态加载插件。 实现动态加载插件的具体步骤如下:
- 创建插件 APK 文件 开发者需要创建一个插件 APK 文件,该文件包含插件的所有资源和代码。插件 APK 文件应该包含一个 Activity,该 Activity 用于启动插件。
- 创建宿主应用程序 宿主应用程序是指用于加载插件的应用程序。开发者需要在宿主应用程序中创建一个用于加载插件的 Activity,该 Activity 可以根据插件的包名和类名启动插件中的 Activity。
- 在宿主应用程序中加载插件 宿主应用程序可以通过以下几种方式加载插件: 3.1 使用 ClassLoader 加载插件中的类和资源; 3.2 使用反射调用插件中的方法; 3.3 使用 Intent 调用插件中的 Activity。
- 更新插件 应用程序热更新可以帮助开发者快速修复应用程序中的问题,并提供更好的用户体验。开发者可以通过下载新版插件文件并替换原版插件文件的方式进行插件更新,在插件更新过程中需要注意安全性和稳定性问题。
需要注意的是,动态加载插件需要谨慎考虑,开发者需要注意插件的安全性和稳定性问题。建议在必要时才使用动态加载插件,避免对应用程序的稳定性和用户体验造成不良影响。
最新的开发方式建议是 Android + 小程序 来替换插件化更新方案。
19 概述一下Android中实现动画的方式
Android 中实现动画的方式有以下几种:
19.1. View Animation
View Animation 又称 tween(补间)动画,是最基本的动画实现方式,可以实现平移、旋转、缩放和透明度等效果,通过对 View 进行缩放、移动、旋转和淡入淡出等处理,让 View 呈现类似于动效的效果。View Animation 通常用于简单的动画效果实现,适用于 API 1 及以上版本。
19.2.
Property Animation Property Animation 是 Android API 3.0 引入的新动画特性,与 View Animation 不同,Property Animation 可以实现更灵活、更自然的动效,适用于 API 11 及以上版本。Property Animation 可以对 View 的属性进行具体的操作,如对 View 的位置、大小、圆角、透明度等属性进行操作。同时,还可以对多个 View 进行联合动画。
19.3. Drawable Animation
Drawable Animation 可以实现类似帧动画的效果,用于显示连续的帧图像。它通过在 Drawable 对象中定义一组不同的图片,在一段时间内依次播放这些图片,从而实现动态效果。Drawable Animation 通常用于一些简单的帧动画效果,适用于 API 1 及以上版本。
19.4. 动画布局
动画布局是一个布局容器,可以将多个动画效果结合起来,实现复杂的动画效果。动画布局可以使 View 具有有机的运动效果,可以很方便地实现多种复杂的动画效果。动画布局通常用于比较复杂的动画效果,适用于 API 3 及以上版本。 综上所述,Android 中实现动画的方式有多种选择,开发者可以根据实际需求选择合适的方式,实现想要的动画效果。
20 Andorid中如何实现数据加密
在 Android 中,可以使用如下方式实现数据加密:
20.1. 对称加密
对称加密算法(如 AES、DES、3DES)使用相同的密钥对数据进行加密和解密。在 Android 中,可以使用 javax.crypto 包中的类来实现对称加密。
20.2. 非对称加密
非对称加密算法(如 RSA、DSA)使用公钥加密,私钥解密或者使用私钥签名,公钥验证。在 Android 中,可以使用 java.security 包中的类来实现非对称加密。
20.3. 消息摘要
消息摘要算法(如 MD5、SHA)将任意长度的数据转换为一个固定长度的摘要信息,通常用于数据完整性校验。在 Android 中,可以使用 java.security 包中的类来实现消息摘要。
20.4. HTTPS HTTPS 是基于 SSL/TLS 协议的加密协议,在网络传输过程中对数据进行加密。在 Android 中,可以使用 HttpsURLConnection 或者 OkHttp 等框架来实现 HTTPS 请求。
需要注意的是,在实现数据加密时,需要注意密钥的生成和管理、加密算法的选择、数据传输的安全性等问题。同时,还需要注意密钥加密和数据加密的分离,以保证密钥的安全性。
21 AES 加密算法原理概述
AES(Advanced Encryption Standard),高级加密标准,是一种对称加密算法,用于保护敏感信息的安全性,安全性高于 DES 等老式加密算法。
AES 加密算法的原理如下:
- 密钥生成 AES 加密算法使用一个密钥进行加密和解密,密钥的长度可以是 128 位、192 位或 256 位。根据密钥长度的不同,加密算法分别称为 AES-128、AES-192 和 AES-256。
- 明文的转换 AES 加密算法将明文分成若干个长度为 16 字节的块(如果明文不足 16 字节,要进行填充)。每个块通过加密算法的运算,转换为一个密文块,然后将这些密文块组合起来,形成加密后的密文。
- 加密过程 AES 加密算法的加密过程使用一系列复杂的算法,包括字节替换、行移位、列混淆和轮密钥加。这些算法将密文块与密钥进行混合运算,从而得到加密后的密文块。AES 加密算法采用多轮加密,轮数的数量取决于密钥的长度和块的大小。
- 解密过程 AES 解密算法与加密算法完全对称,使用相同的密钥和相反的运算,可以将加密后的密文解密为明文。解密过程也是多轮的。
总结:AES 加密算法通过多轮加密和解密运算,使用密钥对明文进行加密,从而得到安全的密文,保证了信息的机密性。原理比较复杂,但其应用广泛,是现代网络通信中常用的加密算法之一。
22 概述一下 Android中的RenderScript
RenderScript 是用于在 Android 上以高性能运行计算密集型任务的框架。RenderScript 主要用于数据并行计算,不过串行工作负载也可以从中受益。
RenderScript 运行时可在设备上提供的多个处理器(如多核 CPU 和 GPU)间并行调度工作。这样您就能够专注于表达算法而不是调度工作。RenderScript 对于专注于图像处理、计算摄影或计算机视觉的应用来说尤其有用。
从 Android 12 开始,RenderScript API 已被废弃。可使用 RenderScript 内建函数替换工具包来替代。
23 概述一下Android中的权限体系
在 Android 中,权限体系用于保护用户数据和系统资源,确保应用只能在其授权范围内操作。
Android 的权限体系主要分为以下几个层次。
- 应用权限 应用权限是指应用在安装时需要获取的权限,如访问相机、文件系统等。应用权限由 Android 系统定义,并按照危险级别分类,包括普通权限和危险权限。在 Android 6.0 及以上版本中,应用必须在运行时动态请求危险权限,用户可以选择授予或拒绝应用权限。
- 用户权限 用户权限是指 Android 操作系统为用户保留的一些权限,如拨打电话、发送短信等,这些权限可以在系统设置中进行管理和授权。用户权限属于系统级别的权限,不同于应用权限。
- 设备策略权限 设备策略权限是指 Android 企业设备管理功能所需的权限,包括锁定设备、清除设备数据等。这些权限由设备管理员管理和授权,在拥有这些权限的前提下,企业设备管理者可以管理和控制设备的使用。
- 未知来源应用安装权限 在 Android 系统中,用户可以安装来自 Google Play 商店以外的应用,但需要先在设置中授权允许来自未知来源的应用安装。这是一种安全机制,可以防止用户意外安装有恶意程序的应用。
- root 权限 root 权限是指用户获取设备 root 权限后所拥有的系统级别权限。拥有 root 权限的用户可以对系统进行自由操作和配置,但同时也带来了安全和稳定性问题,因此不建议普通用户获取 root 权限。
24 概述一下 Android 12 的一些新特性
- 更出色的隐私保护和权限管理:Android 12 加强了隐私保护和权限管理,提供了更加细致的权限控制和更加安全的数据保护。
- Materia You 设计语言:Android 12 推出了全新的设计语言 Materia You,增加了更多的动画特效和个性化设置,为用户带来更加流畅、美观的体验。
- 手势操作增强:Android 12 增强了手势操作的功能和灵敏度,提供了更加自然的交互方式。
- 更加智能和便捷的用户体验:Android 12 引入了一系列智能化特性,如自动切换 Wi-Fi 和手机数据网络、智能截屏等,为用户带来更加便捷的使用体验。
- 更加高效的性能优化和资源管理:Android 12 提供了更加高效的内存管理和资源分配,为应用提供更加稳定、快速的性能表现。
- 数字身份和数字钱包:Android 12 支持数字身份和数字钱包功能,用户可以安全地存储和管理自己的身份证、驾驶证、信用卡等个人信息。
总之,Android 12 的新特性主要聚焦于隐私保护、个性化设计、智能化特性、性能优化和数字身份等方面,为用户带来更加智能、便捷和安全的使用体验。
25 概述一下 Android 13 的一些新特性
25.1 发送通知权限
大多数应用程序利用通知发布有用的警报和提醒,在Android 13中,谷歌增加了通知的运行时权限。
为了不干扰用户和开发者,Android 13中的通知访问会根据正在运行的应用程序的目标API级别进行不同的处理。然而,不管应用程序的目标API级别如何,Android 13都会提示用户授予应用程序发送通知的权限。
Android 13 或更高版本为目标平台,需要在应用程序manifest文件中,显示声明的权限,并且需要动态申请。
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application ...>
...
</application>
</manifest>
25.2 读取媒体文件权限
以Android 13 及以上为目标平台,需要申请如下权限.要保持与旧版本Android的兼容性,请把READ_EXTERNAL_STORAGE的maxSdkVersion设为32。
<manifest ...>
<!-- Required only if your app targets Android 13. -->
<!-- Declare one or more the following permissions only if your app needs
to access data that's protected by them. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Required to maintain app compatibility. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application ...>
...
</application>
</manifest>
25.3 附近的WIFI设备权限
在Android 13中,Google将Wi-Fi扫描与位置分离。Android 13 为管理设备与周围 Wi-Fi 热点连接的应用添加 NEARBY_WIFI_DEVICES运行时权限 (属于 NEARBY_DEVICES 权限组)。
以 Android 13 为目标平台的应用程序,访问附近的 WI-FI 设备。除特例API需要申请ACCESS_FINE_LOCATION外,其他需要申请android.permission.NEARBY_WIFI_DEVICES运行时权限;
25.4 广播接收器
广播接收器是否应被导出以及是否对设备上的其他应用可见,Android 13 或更高版本为目标平台的应用,在应用的每个广播接收器中,明确指明其他应用是否可以向其发送广播,如以下代码段所示:
context.registerReceiver(sharedBroadcastReceiver, intentFilter,
RECEIVER_EXPORTED);
//或者是
context.registerReceiver(privateBroadcastReceiver, intentFilter,
RECEIVER_NOT_EXPORTED);
如果在注册时不指定,系统会抛出 SecurityException。
25.5 新增的功能
- 新的照片选择器和 API
- Android 13 开始刷新 Android 的核心库
- Android 13 添加了对可编程 RuntimeShader 对象的支持
- 新的图块放置API将允许应用程序提示用户直接将自定义图块添加到活动的快速设置图块集合中
- Android 13版本在CameraManager类中包含了新方法,可以让应用程序获得并设置手电筒强度级别。
- 从 Android 13 开始,将内容添加到剪贴板时,系统会显示标准视觉确认界面
26 概述一下Android中WebView与JS的交互方式
在 Android 中,WebView 与 JavaScript 之间可以通过以下几种方式进行交互:
26.1. 使用 addJavascriptInterface() 方法
Android 4.2 及以上版本支持使用 addJavascriptInterface() 方法在 WebView 中注册一个 Java 对象,使其暴露给 JavaScript 调用。Java 对象中的 public 方法可以被 JavaScript 直接调用,从而实现 WebView 和 JavaScript 的相互调用。
26.2. 使用 evaluateJavascript() 方法
Android 4.4 及以上版本支持使用 evaluateJavascript() 方法在 WebView 中执行 JavaScript 代码,并获取执行结果。通过该方法,可以实现 WebView 调用 JavaScript 方法,并获取返回值。需要注意的是,该方法是异步的,返回结果需要通过回调方法获取。
26.3. 使用 JavaScriptInterface 接口 Android 2.3 及以上版本支持使用 JavaScriptInterface 接口实现 WebView 和 JavaScript 的交互。通过在 Java 中定义接口,在 JavaScript 中实现该接口,从而实现 JavaScript 调用 Java 方法的功能。
26.4. 使用 shouldOverrideUrlLoading()
方法 WebView 中的 shouldOverrideUrlLoading() 方法可以拦截 WebView 中的网页中的 URL 请求,从而实现 JavaScript 与 Java 之间的通信。通过在 WebView 中加载特定的 URL 请求,Java 可以捕获该请求,并触发相应的操作。 综上所述,以上这些方法都可以实现 WebView 和 JavaScript 的交互,需要根据实际需求选择合适的方式。需要注意的是,在使用 WebView 和 JavaScript 交互时,要注意数据安全问题,并尽量避免用户输入的数据被攻击者利用。
27 Android Webview中的定位问题
在 Android WebView 中,定位功能通常需要用户授权访问位置信息,否则定位功能将无法正常工作。同时,定位功能还需要满足以下条件:
- WebView 中需要启用 JavaScript 支持。
- WebView 中需要设置 setGeolocationEnabled() 方法为 true,以启用定位功能。
- Android 设备需要启用定位服务,并且需要开启网络定位和 GPS 定位。
- AndroidManifest.xml 文件中需要添加相应的权限声明,如 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION,以获取定位信息的权限。 在 Android 6.0 及以上版本,用户授权访问位置信息需要动态申请权限。
可以通过以下代码示例实现:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 未授权,申请定位权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE);
} else {
// 已授权,启用定位功能
webView.getSettings().setGeolocationEnabled(true);
webView.getSettings().setJavaScriptEnabled(true);
}
在处理定位信息时,还需要实现 WebChromeClient 类中的 onGeolocationPermissionsShowPrompt() 方法,用于向用户显示位置信息请求对话框并获取用户授权,示例代码如下:
// 实现 WebChromeClient 类
private class MyWebChromeClient extends WebChromeClient {
@Override
public void onGeolocationPermissionsShowPrompt(String origin,
GeolocationPermissions.Callback callback) {
// 显示位置信息请求对话框,并获取用户授权
callback.invoke(origin, true, false);
}
}
需要注意的是,定位功能在使用时需要注意隐私问题,尽量避免用户信息泄露。
28 Android Webview 的输入框问题
在 Android WebView 中,输入框问题主要有以下几种情况:
- 输入法弹出问题:当 WebView 中包含输入框时,用户点击该输入框时,输入法可能不会弹出。这个问题可以通过设置 webView.requestFocus() 方法来解决。
- 输入框文字不能被清空问题:在某些情况下,用户在输入框中输入文字后,无法通过点击清空按钮或通过长按输入框来清空输入框中的文字。这个问题通常发生在加入了某些特定的 CSS 样式或 JS 脚本时。可以通过设置 webView.getSettings().setJavaScriptEnabled(true) 方法来启用JavaScript支持以解决这个问题。
- 输入框获得焦点问题:在某些情况下,输入框可能无法获得焦点,例如在使用 Hybrid App 开发时,输入框的焦点可能会丢失。这个问题可以通过手动设置输入框获取焦点来解决,例如在加载 WebView 时调用 input.requestFocus() 方法即可。
- 输入框光标位置问题:在某些情况下,输入框中的光标可能会出现在输入框外面。这个问题通常发生在 WebView 中嵌套了多个层级的 div 标签时。可以通过设置输入框的 position 属性来解决这个问题,例如在 CSS 中设置:
input { position: relative; z-index: 1; }
以上是常见的 Android WebView 输入框问题和解决方法,需要根据实际情况选择与处理。
29 Java中类加载的双亲委派原理
双亲委派是指在 Java 中类加载器在加载一个类时,首先将加载请求委派给父类加载器,如果父类加载器无法加载该类,则再交给当前加载器自己尝试加载。如果当前加载器仍然无法加载,则将加载请求继续向上委派,直到顶层的启动类加载器。只有在所有的父类加载器都无法加载该类时,才会由当前类加载器自己加载。
双亲委派机制的主要目的是保证 Java 类库的安全性和稳定性。由于 Java 类库涉及到Java核心类库和第三方库,不同的类可能会以不同的方式被加载,如果不存在规范的加载顺序,就会出现一些安全和稳定性问题。而通过双亲委派机制,可以保证不同的类库或应用程序不会干扰到核心类库,从而保证Java的安全性和稳定性。
例如,当我们在类路径下自定义了一个名为java.lang.String的类,如果没有双亲委派机制,Java虚拟机可能会优先加载我们自定义的类,而不是核心的java.lang.String类,这样将会导致程序不稳定。而通过双亲委派机制,Java虚拟机会首先委派给父类加载器Bootstrap类加载器去加载java.lang.String类,因为Bootstrap类加载器是最顶层的类加载器,所以如果它无法加载,才会继续向下委派,直到系统最低层的应用程序加载器。
总之,双亲委派机制通过定义清晰的类加载器层次关系,保证了Java核心类库和第三方库的安全和稳定性。
30 HashSet如何保证不重复
在 HashSet 中,不允许存在重复的元素,它是通过两个方式来保证其元素不重复的。
30.1 哈希表
HashSet 内部实际上是一个 HashMap,其中所有元素都存储在 HashMap 的 Key 中,而 Value 中存储了一个不变的 Object 对象。因此 HashSet 实际上是基于 HashMap 的封装,它使用 HashMap 作为存储结构。HashMap 的 Key 可以看成是 HashSet 的元素,而 Value 始终为一个不变的 Object 对象。
当向 HashSet 中插入一个元素时,HashSet 首先会根据该元素的 hashCode() 方法返回值得到一个 hash 值,并由此确定该元素在 HashSet 内部存储的位置。如果该位置上已经有元素存在,则需要比较这两个元素是否相等。如果两个元素不相等,则需要在该位置上继续寻找合适的位置。如此循环直到找到一个空的位置或者找到了与该元素相等的元素为止。
30.2 equals() 方法
由于 HashSet 是通过 hashCode() 方法和 equals() 方法来判断元素是否相等的,因此如果两个元素的 hashCode() 方法返回值相等,并且 equals() 方法返回 true,则认为这两个元素是相等的,HashSet 将不会将后续被添加的元素添加到 HashSet 中。
因此,在使用 HashSet 时应该为需要添加到 HashSet 中的元素正确地实现 hashCode() 方法和 equals() 方法,否则添加到 HashSet 中的元素可能会重复。
综上所述,HashSet 通过哈希表和 equals() 方法来保证其元素不重复。实现 hashCode() 方法和 equals() 方法对于保证 HashSet 内部元素不重复是至关重要的。
31 Java中wait/notify关键字
在Java中,wait/notify是一种线程同步机制,用于实现多线程之间的协作和互斥操作。wait/notify是基于对象的锁机制,只能用于同步方法或同步代码块中,使用时需要先获取对象的锁。
31.1 Wait方法
当线程调用一个对象的wait方法时,该线程会被阻塞,释放当前对象的锁,并进入对象的等待队列中,直到其他线程调用了该对象的notify/notifyAll方法,或者等待时间到达后,线程才会被唤醒。
31.2 Notify方法
当线程调用一个对象的notify方法时,该对象的等待队列中的一个线程将被唤醒,被唤醒的线程需要等待当前线程退出同步块或方法后,才能再次竞争该对象的锁。如果要唤醒等待队列中的所有线程,可以使用notifyAll方法。
使用wait/notify技术可以实现多线程的协作和互斥操作,避免了繁琐的轮询等待机制,提高了程序性能。但是,使用wait/notify提供的线程同步机制需要谨慎使用,否则可能会导致死锁和其他问题。
32 Java中实现线程同步的方式
Java中实现线程同步的方式主要包括以下几种:
- synchronized关键字:使用synchronized关键字可以对共享资源进行同步,使得在同一时间只有一个线程可以访问该资源。synchronized可以修饰方法或代码块,保证了线程的互斥访问和可见性。
- Lock接口:Lock是Java5中引入的一个新的同步机制,通过Lock和Unlock方法来对共享资源进行加锁和释放操作,与synchronized相比,可以更加灵活地控制同步范围。
- ReentrantLock类:ReentrantLock是Lock接口的一个实现类,功能比synchronized更加强大,包括可重入锁、可中断锁、公平性、实现Condition等功能。
- volatile关键字:使用volatile关键字修饰的变量在多个线程之间可见,即一个线程对该变量做出的修改,会被其他线程立即看到,并更新自己的缓存。volatile关键字可以保证线程之间的可见性。
- AtomicInteger类:AtomicInteger是Java中提供的一个原子性变量类,它可以保证多个线程同时对一个整数变量进行加减操作时的原子性。
- synchronized块和Lock接口实现读写锁:在读写分离的场景中,可以使用synchronized块或Lock接口的实现类ReadWriteLock来实现读写锁,从而提高程序的并发性和性能。
总的来说,以上几种方式都可以用来实现线程之间的同步和协作,我们在实际开发中需要根据具体的需求选择合适的方式来实现线程同步。
33 Java的原子性、可见性、有序性概述
Java 中的原子性、可见性和有序性是指在多线程环境下,Java 内存模型对数据的访问和操作具有的特性。它们分别表示如下含义:
- 原子性:指一个操作是不可中断的,在执行过程中,不能被其他线程干扰。Java 中提供了一些原子性操作类,如 AtomicBoolean、AtomicInteger、AtomicLong 等,能够保证该操作的原子性,并且在多线程环境下也可以正常使用。
- 可见性:指一个线程对共享数据的修改,能够被其他线程及时地感知到。Java 中使用 volatile 关键字可以实现可见性。当一个变量被 volatile 关键字修饰时,表示该变量被多个线程共享,任意一个线程对变量的修改都会被其他线程及时地感知到。
- 有序性:指程序执行的顺序按照代码的先后顺序执行。Java 中使用 synchronized 关键字和 Lock 接口可以保证有序性,因为它们都会在执行之前获得锁,从而保证代码的先后顺序。
34 Java 线程中的join
Java中的join()方法是一个非常重要的线程同步方法。它可以让当前线程等待另一个线程执行完毕后再执行。
使用join()方法可以保证线程的执行顺序,避免出现多线程并发时的问题。 join()方法的使用非常简单,只需要在需要等待其他线程执行完毕的线程中调用另一个线程的join()方法即可。例如,我们可以使用如下代码等待线程A执行完毕:
Thread threadA = new Thread(() -> {
// 线程A的业务逻辑
});
// 启动线程A
threadA.start();
// 等待线程A执行完毕
try {
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
在上面的代码中,我们创建了一个线程A,并启动它。然后,我们在当前线程中调用线程A的join()方法,等待线程A执行完毕后再继续执行。
需要注意的是,join()方法会抛出InterruptedException异常,我们需要处理它。
此外,如果join()方法的参数设置为一个较长的时间,那么在等待期间如果被中断,也会触发InterruptedException异常。
总之,使用join()方法可以让多个线程之间协调一致地执行,避免出现多线程并发时的问题。但是,在使用join()方法时,我们需要注意处理InterruptedException异常,并合理设置等待时间,避免出现死锁等问题。
35 Java中任务调度
Java中任务调度主要通过以下两种方式实现:
-
java.util.Timer 和 java.util.TimerTask 这是Java中最基本的任务调度方式,它基于时间驱动,可以指定一个时间点或一个时间间隔,执行相应的任务。Timer类负责定时调度,而TimerTask类则表示要定时执行的任务。在使用时,我们需要创建Timer和TimerTask的实例,然后通过Timer的schedule()方法来指定任务的执行时间和间隔。
-
Java.util.concurrent.ScheduledExecutorService 这是Java 5之后引入的新的任务调度方式,它是在ExecutorService基础上扩展的,可以使用ScheduledExecutorService执行定时任务。ScheduledExecutorService可以通过schedule()、scheduleAtFixedRate()和scheduleWithFixedDelay()方法来实现任务的定时调度,每个方法的调度方式稍有不同,可以根据业务需求进行选择。
任务调度在Java中广泛应用于各种场景,例如定时任务、异步执行、数据备份等。它可以帮助我们自动化地完成各种任务,并提高系统的可靠性和效率。但是,在使用任务调度时,我们需要注意安全性和性能等方面的问题,避免出现竞态条件、死锁等并发问题。
36 Java中wait和sleep方法
Java中的wait()和sleep()方法都可以用于线程的等待和暂停,但是它们的作用和使用方式有所不同。
wait()方法是Object类中定义的方法,可以让当前线程等待,直到其他线程调用notify()或notifyAll()方法唤醒它。在使用wait()方法时,我们需要将其放在synchronized块中,并且使用notify()或notifyAll()方法来唤醒等待的线程。
例如,我们可以使用如下代码实现线程之间的等待和唤醒:
Object lock = new Object();
// 等待线程
Thread waitThread = new Thread(() -> {
synchronized (lock) {
try {
lock.wait();
// 被唤醒后执行的逻辑
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 唤醒线程
Thread notifyThread = new Thread(() -> {
synchronized (lock) {
lock.notify();
}
});
// 启动线程
waitThread.start();
notifyThread.start();
sleep()方法是Thread类中定义的静态方法,可以让当前线程暂停一段时间。在使用sleep()方法时,我们不需要使用synchronized块,也不需要其他线程的唤醒操作。 例如,我们可以使用如下代码实现线程的暂停:
Thread.sleep(1000); // 暂停一秒钟
需要注意的是,wait()方法和sleep()方法的作用和用法不同,不能混淆使用。在使用wait()方法时,需要将其放在synchronized块中,并且通过notify()或notifyAll()方法来唤醒等待的线程。在使用sleep()方法时,不需要使用synchronized块,并且不需要其他线程的唤醒操作。