Android基础篇
1. Android的四大组件有哪些?
Android的四大组件包括Activity、Service、BroadcastReceiver和ContentProvider。
- Activity:用于表现功能。它是所有程序的根本,所有程序的流程都运行在Activity之中,可以算是开发者遇到的最频繁,也是Android当中最基本的模块之一。
- Service:后台运行服务,不提供界面呈现。它是android系统中的一种组件,它跟Activity的级别差不多,但是他不能自己运行,只能后台运行,并且可以和其他组件进行交互。
- BroadcastReceiver:用于接收广播。
- ContentProvider:支持在多个应用中存储和读取数据。
2. 说说Activity的生命周期
- onCreate() :当Activity正在被创建时调用。
- onRestart() :当Activity从不可见重新变为可见状态时调用。
- onStart() :表示Activity正在被启动,已经从不可见到可见状态,但还没出现在前台,但用户看不到,还无法和用户进行交互。
- onResume() :表示Activity已经可见并且出现在前台并开始活动。
- onPause() :表示Activity正在停止,正常情况下,紧接着onStop就会被调用。
- onStop() :表示Activity即将暂停,此时Activity工作在后台,已经不可见了,可以与onPause方法一样做一些轻量级操作,但依然不能太耗时。
- onDestroy() :表示活动即将被销毁。
- onNewIntent():这个方法在Activity收到新的Intent时被调用,可以用于处理多个调用只有一个Activity实例的情况。在Activity中实现onNewIntent()方法,并设置Activity的launchMode为singleTask或者singleTop,就可以实现这个功能
3. 说说Activity的启动模式
Activity的启动模式有四种,分别是standard、singleTop、singleTask和singleInstance。
- standard模式:这是Activity的默认启动模式。每当启动一个Activity时,它都会在任务栈中创建一个新的实例。这种模式虽然可以保存之前启动的Activity,但可能会浪费空间。
- singleTop模式:当查看任务栈顶和将要启动的Activity是否是同一个Activity时,如果是就直接复用,否则就新创一个实例。
- singleTask模式:在任务栈中查看是否有和要启动的Activity相同的实例。如果有,就直接把该Activity之上的所有Activity全部弹出使之置于栈顶。
- singleInstance模式:在整个系统中只创建一个Activity实例。
4. Fragment的生命周期
- onAttach:当Fragment与Activity发生关联时调用。
- onCreate:创建Fragment时被回调。
- onCreateView:每次创建、绘制该Fragment的View组件时回调该方法,Fragment将会显示该方法返回的View组件。
- onActivityCreated:当Fragment所在的Activity被启动完成后回调该方法。
- onStart:启动Fragment时被回调,此时Fragment可见。
- onResume:恢复Fragment时被回调,获取焦点时回调。
- onPause:暂停Fragment时被回调,失去焦点时回调。
- onStop:停止Fragment时被回调,Fragment不可见时回调。
- onDestroyView:销毁与Fragment有关的视图,但未与Activity解除绑定。
- onDestroy:销毁Fragment时被回调。
- onDetach:与onAttach相对应,当Fragment与Activity关联被取消时调用。
5. Android中Fragment和Activity通信的方式有哪些
- 通过Activity的实例进行通信:你可以通过在Activity中持有Fragment的实例来直接与Fragment通信。但是这种方式需要你在Fragment中手动地管理生命周期,否则可能会在Activity被销毁后仍然持有Fragment的实例,导致内存泄漏。
- 通过FragmentManager进行通信:你可以通过FragmentManager的findFragmentById或findFragmentByTag方法找到特定的Fragment,然后与之通信。这种方式是Fragment和Activity之间通信的标准方式。
- 通过EventBus进行通信:如果你正在使用EventBus库,你也可以通过EventBus来在Fragment和Activity之间进行通信。这种方式不需要你手动管理生命周期,但是需要你手动地注册和注销EventBus。
- 通过LocalBroadcastManager进行通信:你可以通过LocalBroadcastManager发送广播,然后在Fragment中注册一个广播接收器来接收这个广播。这种方式也可以在Fragment和Activity之间进行通信,但是通常用于在不同的组件之间进行通信,而不是在同一组件的不同部分之间进行通信。
- 通过ViewModel进行通信:如果你正在使用MVVM架构,你可以通过ViewModel来在Fragment和Activity之间进行通信。这种方式通常用于在视图层和数据层之间进行通信,而不是在不同的视图组件之间进行通信。
- 通过接口回调的方式
6. Android中,startService和bindService有啥区别?
- 生命周期:startService的服务与启动它的组件无关,即使启动它的组件被销毁,服务也会继续运行。而bindService的服务与调用它的组件绑定,当与之绑定的所有组件都被销毁时,服务也会被销毁。
- 通讯:startService通常不提供与启动它的组件进行交互的方式。而bindService可以通过一个IBinder接口与服务进行交互。
- 多次启动/绑定:多次调用startService并不会创建多个服务实例,而是多次调用了服务的onStartCommand方法。多个组件可以绑定到同一个服务,但服务只有一个实例。
- 结束方式:服务需要通过stopSelf或stopService来显式结束。当所有客户端解除绑定后,服务会自动停止。
- 用途:startService更适用于不需要与用户界面进行交互、但需要运行较长时间的任务,例如后台音乐播放、文件下载、数据同步等。bindService则适用于需要与用户界面交互或者与其他应用组件交互的服务,例如即时聊天、传感器数据监控等。
总结来说,startService和bindService的主要区别在于它们的生命周期、通讯方式、多次启动/绑定以及结束方式等方面。开发者应该根据具体需求选择合适的方式启动服务。
7. 既使用startService又使用bindService的情况可以吗
可以,既使用startService又使用bindService的情况是可以的。
如果一个Service又被启动又被绑定,那么该Service会一直在后台运行。首先不管如何调用,onCreate始终只会调用一次。对应startService调用多少次,Service的onStart方法便会调用多少次。Service的终止,需要unbindService和stopService同时调用才行。不管startService与bindService的调用顺序,如果先调用unbindService,此时服务不会自动终止,再调用stopService之后,服务才会终止;如果先调用stopService,此时服务也不会终止,而再调用unbindService或者之前调用bindService的Context不存在了(如Activity被finish的时候)之后,服务才会自动停止。
8. 说说Android数据存储的方式有哪些?
- SharedPreferences:这是一种轻量级的数据存储方式,主要用于存储一些简单的配置信息,如登录的账号密码等。它采用Map数据结构来存储数据,以key-value的方式存储,可以更简单的读写。
- 文件存储:这是一种比较常见的方式,在读取写入文件的时候,与Java中的I/O程序完全一样,提供了openFileInput()和openFileOutput()方法来读取设备商的文件。
- SQLite数据库:这是一种轻型的关系型数据库,可以用于存储和管理大量的结构化数据。Android提供了SQLite数据库的API,开发者可以直接使用SQL语句进行数据的增删改查操作。
- ContentProvider:当实例继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。
- 网络存储:通过网络接口进行数据的存储和上传等操作。这种方式主要用于实时采集到的数据需要马上通过网络传输到数据处理中心进行存储并进行处理的情况。
9. View的绘制流程
View的绘制流程主要包括以下三个步骤:
- measure阶段:这个阶段主要是计算出控件树中的各个控件要显示其内容的话,需要多大尺寸。起点是ViewRootImpl的measureHierarchy()方法,measureHierarchy()方法中有一段源码如下:getRootMeasureSpec()方法用来获取根MeasureSpec,根MeasureSpec代表了对decorView的宽高的约束信息。
- layout阶段:这个阶段主要是判断是否需要重新计算View的位置,需要的话则计算。在layout方法中,View会根据父容器的尺寸以及自身的尺寸来确定自己在父容器中的位置和大小。
- draw阶段:这个阶段主要是判断是否需要重新绘制View,需要的话则重绘制。在draw方法中,View会先绘制自己的背景,然后再绘制自己的内容,最后再绘制自己的前景。在绘制自己的内容时,View会调用自己的onDraw方法来进行绘制。
10. View的事件分发机制
当一个用户在Android设备上进行触摸操作时,会生成一个MotionEvent对象,这个对象包含了触摸事件的所有信息,如触摸点的位置、触摸的方式(按下、抬起、移动等)等。
这个MotionEvent对象首先被传递到Activity的窗口中。在窗口中,事件被传递给顶级View(DecorView)。DecorView是一个特殊的View,它包含了整个界面树,也就是所有的View。
在DecorView中,事件首先会通过dispatchTouchEvent方法进行分发。这个方法会根据事件的类型(按下、抬起、移动等)和位置,将事件传递给相应的View或者ViewGroup进行处理。
如果事件可以被传递给某个具体的View或ViewGroup,那么这个View或ViewGroup就会调用它的onTouchEvent方法来处理这个事件。如果事件不能被传递给任何View或ViewGroup,那么事件就会被上交给它的父View或ViewGroup来处理。
这个过程会一直持续下去,直到找到一个可以处理这个事件的View或ViewGroup为止。这就是Android的View事件分发机制的核心部分。
此外,Android还提供了一些其他的机制来处理事件,如onInterceptTouchEvent方法,当return true时它可以在事件分发过程中拦截事件,改变事件的流向。这些机制可以用于实现一些特殊的效果和交互方式。
11. 自定义View的方式有哪些?
-
继承已有的View类:你可以通过继承已有的View类,例如TextView、Button等,来创建自己的自定义View。这种方式可以让你利用已有View类的功能,同时添加自己的特殊功能。
-
继承已有的ViewGroup类:如果你需要创建的自定义View包含多个子View,那么可以通过继承已有的ViewGroup类,例如LinearLayout、RelativeLayout等,来创建自定义的ViewGroup。
-
继承已有的Drawable类:如果你需要创建的自定义View是用来绘制图形的,那么可以通过继承已有的Drawable类,例如BitmapDrawable、ColorDrawable等,来创建自定义的Drawable。
12. Android中常用的布局有哪啊些?
- LinearLayout:线性布局,分为垂直(vertical)和水平(horizontal)两种。
- RelativeLayout:相对布局,是相对于某个控件位置的布局。
- FrameLayout:帧布局,是一种层叠式的布局。
- TableLayout:表格布局。
- AbsoluteLayout:绝对布局,将每个控件之间的位置写死,所以不会适配不同大小的屏幕,现在已经被开发人员抛弃。
- ConstraintLayout:约束布局,它是一种新型的布局方式,被广泛使用。ConstraintLayout提供了一种更加灵活、强大的方式来创建复杂的布局。通过使用约束(constraints)
13.SharedPreferences中的commit和apply有啥区别?
-
返回值:commit方法返回一个boolean值,表示修改是否提交成功;而apply方法没有返回值。
-
提交方式:commit是同步的提交到硬件磁盘,而apply是先将修改数据原子提交到内存,然后异步真正提交到硬件磁盘。
-
提示信息:commit方法在操作失败时,会提示异常;而apply方法不会提示任何失败的提示。
-
效率:由于commit是同步提交,因此在多个并发的commit操作时,它们会等待正在处理的commit保存到磁盘后才进行操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,从一定程度上提高了效率。
14. SharedPreferences是线程安全的吗?
SharedPreferences默认实现不是线程安全的。在多线程环境下,同时进行读写操作可能会导致数据不一致或其他问题。
如果需要在多线程环境下使用SharedPreferences,建议采取适当的线程同步机制,以确保数据的正确性和一致性。例如,可以使用互斥锁(Mutex Lock)、synchronized关键字或其他线程同步机制来保护共享资源。
15. SharedPreferences和DataStore有啥区别?
- 存储方式:SharedPreferences是以键值对的方式存储数据,适用于存储简单的数据,如字符串、整数等。而DataStore则提供了一种更灵活、强大的数据存储方式,可以存储键值对、类型化对象等更复杂的数据类型。
- 性能:SharedPreferences在加载数据时可能会阻塞UI线程,导致ANR异常。而DataStore则采用异步、一致的事务方式存储数据,不会阻塞UI线程,从而提高了应用的性能。
- 跨进程通信:SharedPreferences不支持跨进程通信,而DataStore可以用于跨进程通信。
- 数据迁移:SharedPreferences不支持数据迁移,而DataStore可以通过迁移工具进行数据迁移。
总的来说,SharedPreferences适用于存储简单的数据,而DataStore则适用于存储更复杂的数据类型,且具有更高的性能和跨进程通信能力。
16. Android中线程异步的方式有哪些?
- 使用Thread:通过创建一个新的Thread对象,并重写其run()方法来执行异步任务。这是最简单的方式,但需要注意的是,Android的主线程(UI线程)不支持直接使用Thread,否则会导致ANR异常。
- 使用Handler:Handler是Android中用于在主线程(UI线程)中执行消息或Runnable对象的机制。通过创建一个Handler对象,可以将子线程中的任务发送到主线程中执行。
- 使用AsyncTask:AsyncTask是Android提供的一个轻量级异步类,用于在后台线程中执行一些耗时的操作,并在完成后更新UI。AsyncTask提供了doInBackground()方法用于执行后台任务,onPostExecute()方法用于在任务完成后更新UI。
- 使用Thread + Looper + Handler:这种方式结合了Thread和Handler,通过Looper来管理消息队列,实现异步任务。这种方式可以实现更复杂的异步操作,但需要更多的代码和更深入的理解。
- 使用Service(如IntentService):Service是Android中用于在后台执行长时间运行的操作的组件。通过创建一个Service对象,可以在后台执行一些耗时的任务,而不会阻塞UI线程。
- 使用ExecutorService:ExecutorService是Android提供的一个高级的异步执行机制,可以创建和管理多个线程池,用于执行异步任务。
17. 说说Handler机制
Android中的Handler机制是一种在主线程(UI线程)和工作线程之间进行通信的机制。它允许子线程将任务发送到主线程中执行,从而避免在主线程中执行耗时操作,提高应用的性能。
Android中的Handler机制涉及以下类:
- Handler:Handler是处理程序消息和执行相关操作的组件。当一个消息被发送到Handler时,它会被添加到消息队列中,并在适当的时候被处理。Handler的作用是接收子线程发送过来的消息或Runnable对象,并在主线程中执行。
- Message:Message是包含消息数据的类。它可以包含不同的消息类型(如Runnable、Runnable对象、数据等),以及一些额外的数据(如Bundle)。MessageQueue 内部实现并不是用的队列, 实际上通过一个单链表的数据结构来维护消息列表。next 方法是一个无限循环的方法,如果 消息队列中没有消息,那么 next 方法会一直阻塞。当有新消息到来时,next 方法会放回这 条消息并将其从单链表中移除。
- MessageQueue:MessageQueue是消息队列,它负责存储消息,并按先进先出(FIFO)的顺序进行处理。当一个消息被发送到Handler时,它会被添加到MessageQueue中等待处理。
- Looper:Looper是消息循环,它负责不断查询MessageQueue中的消息,并将消息传递给对应的Handler进行处理。每个线程只能有一个Looper,而每个Handler都关联一个Looper,如果有新消息就会立刻处理,否则会一直阻塞。
18. 可以在子线程new一个Handler吗?
在Android中,可以在子线程中创建Handler。但是,创建Handler的目的是为了在主线程上执行某些操作,例如更新UI。因此,即使你在子线程中创建了Handler,你也应该确保在主线程上执行与UI相关的操作。
如果你在子线程中执行与UI相关的操作,可能会导致应用程序崩溃或不可预知的行为。因此,通常建议在主线程上执行与UI相关的操作,并使用Handler将需要在主线程上执行的任务传递给主线程。
以下是一个示例代码,演示如何在子线程中创建Handler并在主线程上执行操作:
在这个示例中,我们在子线程中执行任务,然后使用Handler将需要在主线程上执行的任务传递给主线程。在主线程上执行与UI相关的操作时,我们创建了一个新的Runnable对象,并将其传递给Handler。这样,Runnable将在主线程上执行,并且可以在其中更新UI或其他与UI相关的操作。
19. Handler里消息的处理是在主线程还是子线程?
Handler是Android中用来在UI线程上处理其他线程传递的消息或Runnable的机制。当你在子线程中创建一个Handler时,你实际上是在子线程中创建了一个消息队列和一个消息分发器。但是,当这些消息到达时,它们实际上是由主线程来处理的。因此,尽管消息是在子线程中产生的,但处理这些消息的代码仍然在主线程中执行。
20. 在Activity里使用Handler需要注意什么?
- 避免在Handler中持有Activity的引用:如前所述,如果Handler持有Activity的引用,可能会导致内存泄漏。因此,应该避免在Handler中持有Activity的引用,而是持有Runnable或Message对象的引用。
- 使用WeakReference来持有Activity的引用:为了避免内存泄漏,可以使用WeakReference来持有Activity的引用。WeakReference是一种弱引用,它不会阻止对象被垃圾回收。当Activity被销毁时,WeakReference指向的对象会被垃圾回收,从而避免了内存泄漏。
- 及时移除Handler的消息:当Activity被销毁时,应该及时移除Handler的消息,以避免消息队列中残留的Activity引用导致内存泄漏。可以通过在Activity的onDestroy方法中移除Handler的消息来实现。
- 避免在Handler中执行复杂的计算:如果需要在Handler中执行复杂的计算,应该考虑使用其他线程来执行这些计算,而不是在Handler中执行。因为Handler是在主线程上执行的,如果执行复杂的计算,会阻塞主线程,导致UI响应变慢。
21. Handler 中有 Loop 死循环,为什么没有阻塞主线程?
Android 是单线程模型, UI的更新只能在主线程中执行, 在开发过程中, 不能在主线程中执行耗时的操作, 避免造成卡顿, 甚至导致ANR.
这里面, 我故意把执行耗时这四个字突出, 我想大家在面试的时候说个这个问题, 但是造成界面卡顿甚至ANR的原因真的是执行耗时操作本身造成的吗??
现在我们来写个例子, 我们定义一个 button, 在 button 的 onClick 事件中写一个死循环来模拟耗时操作, 代码很简单, 例子如下:
注意, 这里我们运行程序, 然后点击按钮以后, 接下来不做任何操作运行程序以后, 你会发现, 我们的程序会已知打印 log, 并不会出现ANR的情况...按照我们以往的想法, 如果我们在主线程中执行了耗时的操作, 这里还是一个死循环, 那么肯定会造成ANR的情况, 那为什么我们的程序现在还在打印 log, 并没有出现我们所想的ANR呢??接下来让我们继续, 如果这时候你用手指去触摸屏幕, 比如再次点击按钮或者点击我们的返回键, 你会发现5s 以后就出现了ANR....其实前面的这个例子, 已经很好的说明了我们的问题. 之所以运行死循环不会导致ANR, 而在自循环以后触摸屏幕却出发了ANR, 原因就是因为耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发。其实这也是我们标题索要讨论的Looper 中的 loop()方法不会导致主线程卡死的原因之一。看过 Looper 源码的都知道, 在 loop() 方法中也是有死循环的:
前面我们说过, 死循环并不是导致主线程卡多的真正原因, 真正的原因是死循环后面的事件没有得到分发, 那 loop()方法里面也是一个死循环, 为什么这个死循环后面的事件没有出现问题呢??
熟悉Android 消息机制的都知道, Looper 中的 loop()方法, 他的作用就是从消息队列MessageQueue 中不断地取消息, 然后将事件分发出去:
最终调用的是 msg.target.dispatchMessage(msg) 将我们的事件分发出去, 所以不会造成卡顿或者ANR.
22. Android 的子线程能否做到更新 UI?
Android的子线程不能直接更新UI。UI的更新必须在主线程(UI线程)上执行,因为Android的UI组件(如TextView、Button等)都是主线程上的资源。
如果你在子线程中尝试更新UI,例如使用setText()或invalidate()等方法,那么这可能会导致程序崩溃或不可预知的行为。因为Android不允许在非UI线程上直接修改UI组件。
如果你需要在子线程中执行一些耗时的操作,并且希望在完成后更新UI,你可以使用Handler来实现。Handler可以用来将子线程中的任务传递给主线程,然后在主线程上执行。例如:
23. 在 Activity 中获取某个 View 的宽高
- 通过 Activity的onWindowFocusChanged
scss
复制代码
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
// 当 Activity 的窗口得到焦点和失去焦点时均会被调用一次
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
- 通过 view.post(runnable)
scss
复制代码
@Override
protected void onStart() {
super.onStart();
view.post(() -> {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
});
}
- 通过 ViewTreeObserver
java
复制代码
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
24. 动画类型有哪啊些,它们有什么区别?
帧动画、补间动画和属性动画。
(1)帧动画是通过连续播放一系列的静态图像来创建动画效果;
(2)补间动画只能自定义两个关键帧在旋转、位移、缩放、透明度四个方面的变化,
(3)而属性动画可以定义任何属性的变化。另外,补间动画只能对UI组件执行动画,但属性动画几乎可以对任何对象执行动画(不管它是否在屏幕上显示)
25. 说下切换横竖屏时 Activity 的生命周期变化?
(1)竖屏: 启动:onCreat->onStart->onResume.
(2)切换横屏时: onPause-> onSaveInstanceState ->onStop->onDestory
onCreat->onStart->onSaveInstanceState->onResume.
但是,我们在如果配置这个属性:android:configChanges="orientation|keyboardHidden|screenSize" 就不会在调用Activity的生命周期,只会调用onConfigurationChanged方法,以避免在横竖屏切换时重新创建Activity。
26. Android的屏幕适配方案有哪些?
- dp与px的转换:在Android中,dp(设备独立像素)是一种用于适配不同屏幕尺寸和密度的单位。为了在屏幕上显示正确的像素值,需要进行dp到px的转换。转换公式为:px = dp * (dpi / 160)。其中,dpi是设备的像素密度,每个设备的dpi都可能不同。因此,在进行布局和绘制时,需要根据设备的dpi进行适当的转换,以确保在不同设备上显示正确的像素值。
- 布局适配:Android提供了多种布局方式,如LinearLayout、RelativeLayout、FrameLayout等,可以根据需要选择合适的布局方式进行适配。同时,还可以使用布局参数(如match_parent、wrap_content等)和布局属性(如padding、margin等)进行更精确的布局调整。
- 尺寸适配:Android还提供了多种尺寸单位,如px、dp、sp等,可以根据需要选择合适的尺寸单位进行适配。例如,可以使用dp单位进行布局适配,因为dp单位是设备独立像素,可以根据设备的dpi进行自动转换。
- 屏幕分辨率限定符适配:根据当前市面上手机的屏幕分辨率创建不同的文件夹,系统运行的时候,会自动去选择读取对应的文件夹中的xml,即每种屏幕分辨率的设备需要定义一套dimens.xml文件。这种方式适用于对UI设计图在不同设备上显示效果要求较高的场景。
- 权重适配(百分比):在某些情况下,可以通过设置控件的权重来调整其在不同屏幕上的大小和位置。权重可以用于垂直或水平方向上的分配空间。通过设置控件的权重值,可以让系统自动根据屏幕大小和可用空间来调整控件的大小和位置。
- 使用第三方适配方案,如头条适配方案(AndroidAutoSize)