1.Android基础
1.1 如何适配?
Android适配主要指的是针对不同的设备、屏幕尺寸、分辨率、系统版本等因素,使得应用程序在各种环境下都能够正常运行和显示。以下是一些常见的Android适配技术:
1.使用布局文件:使用相对布局、线性布局等布局方式可以让应用程序自适应不同屏幕尺寸和分辨率。这里就是用ConsraintLayout或者RelativeLayout适配。
2.使用dimens资源:使用dimens资源定义尺寸,可以让应用程序在不同的屏幕密度下保持一致的显示效果。
3.使用不同的资源文件:针对不同的屏幕密度、语言、系统版本等,可以使用不同的资源文件。
或使用限制符:使用限制符可以针对不同的设备属性,例如屏幕尺寸、分辨率、密度、语言等,加载不同的资源文件。
4.使用最新的API:使用最新的API,可以让应用程序在新的设备上充分发挥新功能和性能,但需要注意向下兼容。比如我们获取导航栏高度,以前经常用反射的方式拿系统的status_bar_height的值,现在可以用WindowsInsets方法获取系统导航栏高度。
1.2 dp是什么?dpi是什么?density是什么?
在 Android 中,dp、dpi、density 都是和屏幕密度相关的概念。
1.dp(density-independent pixel)是独立于设备屏幕密度的像素单位,也称为设备无关像素或密度无关像素。在开发中,使用 dp 来设置控件的大小,能够保证在不同分辨率、不同屏幕尺寸的设备上都能够保持一致的显示效果。
2.dpi(dots per inch)是屏幕密度的指标,也称为像素密度。它表示在屏幕上每英寸的像素数。屏幕密度越高,每英寸的像素数就越多,显示效果也会更清晰。Android 中,通常会将屏幕密度分为 ldpi、mdpi、hdpi、xhdpi、xxhdpi、xxxhdpi 六个等级,分别对应不同的 dpi 值。ldpi为120dpi,mdpi为160dpi,xhdpi为320dpi,xxhdpi为480dpi,xxxhdpi为640dpi。
density(像素密度)是指在屏幕上每个单位长度内包含的像素数。在 Android 中,density 通常指的是相对密度,也称为密度因子(density factor)。它是指相对于标准屏幕(160dpi)的密度比例。例如,hdpi 的密度因子为 1.5,即相对于标准屏幕密度的 1.5 倍。
1.3 今日头条适配方案原理是什么?
github地址:AndroidAutoSize。
今日头条的 AndroidAutoSize 是一种屏幕适配方案,其原理主要基于修改设备的屏幕密度来实现适配。具体来说,AndroidAutoSize 会在应用程序启动时动态地计算出当前设备的屏幕宽度,然后根据设定的设计图尺寸和屏幕密度,自动计算出当前设备应有的缩放比例,并将其设置为设备的缩放比例。
在应用程序启动时,AndroidAutoSize 会根据当前设备的屏幕宽度和设定的设计图尺寸,计算出设备应有的缩放比例。然后,AndroidAutoSize 会使用 Android 提供的 Configuration 类中的 densityDpi 字段,将缩放比例乘以标准的 160dpi 值,得到一个新的 dpi 值。最后,AndroidAutoSize 会通过反射方式,修改系统的显示密度值,使得系统显示的大小和密度都按照新的 dpi 值进行缩放,从而实现屏幕适配。
相比传统的适配方案,AndroidAutoSize 的优势在于其可以在不需要修改布局的情况下,动态地调整设备的显示密度,从而达到适配的效果。同时,AndroidAutoSize 支持多种适配方式,可以根据实际情况选择最合适的适配方式。
1.4 Activity A 跳转Activity B,Activity B再按back键回退,两个过程各自的生命周期
第一个过程,Activity A跳转到Activity B
Activity A的生命周期:onPause->onStop (这里有可能会走onDestory,取决于系统是否需要回收Activity A)
Activity B的生命周期:onCreate->onStart->onResume
第二个过程:Activity B按返回键
Activity A的生命周期:(是否被回收,回收走onCreate) onRestart -> onStart -> onResume
Activity B的生命周期:onPause -> onStop -> onDestroy
1.5 子线程,是否可以启动Activity
其实是可以的,但不建议。
在 Android 中,UI 线程(即主线程)负责处理 UI 相关的事件和更新,包括 Activity 的启动、更新和销毁等操作。如果在子线程中直接启动新的 Activity,可能会引发以下问题:
线程安全问题:在 Android 中,UI 控件是非线程安全的,只能在 UI 线程中访问和更新。如果在子线程中启动 Activity,可能会访问和更新 UI 控件,从而引发线程安全问题。
可读性和可维护性问题:在 Android 中,Activity 启动通常与 UI 相关,如果将 Activity 启动代码放在子线程中,可能会降低代码的可读性和可维护性。
1.6 Handler机制整体流程?
在 Android 中,Handler 是一种消息传递机制,用于在不同的线程之间传递消息和进行通信。Handler 的基本流程如下:
1.在主线程(UI 线程)中创建一个 Handler 对象,通常可以通过在 Activity 或 Service 中创建一个匿名内部类的方式来实现,或者使用 HandlerThread 创建一个新的线程来处理消息。
2.在子线程中创建一个 Message 对象,该对象包含了需要传递的数据或消息。
3.调用 Handler 的 sendMessage() 方法将 Message 对象发送给主线程的消息队列。这里应该是调用了主线程里面的handler的引用。Handler在主线程声明定义,在子线程调用hanlder的sendMessage方法。
4.主线程中的 Looper 会从消息队列中取出消息,然后将该消息发送给 Handler 对象。
5.Handler 对象会根据消息的类型(what)和数据(obj),调用对应的方法进行处理。通常可以在 Handler 的 handleMessage() 方法中进行消息处理。
需要注意的是,Handler 的消息传递机制是基于 Looper 和消息队列实现的。Looper 负责管理消息队列,并且不断从消息队列中取出消息并分发给对应的 Handler 处理。因此,在创建 Handler 对象之前,需要先创建一个 Looper 对象,并且在主线程中已经有一个默认的 Looper,因此可以直接在主线程中创建 Handler 对象。
Handler 机制的主要作用是在不同的线程之间进行消息传递和通信,可以实现异步操作、线程同步等功能。在 Android 中,Handler 常用于实现后台任务、定时任务、UI 更新等操作。
1.7 Looper.loop()为什么不会阻塞主线程?
当我们在屏幕上划动或者点击时,Android 系统会产生一个输入事件。这个事件会被发送到应用程序的消息队列中,由主线程的 Looper 进程轮询这个消息队列。如果轮询到了这个消息,就会将其交给对应的 Handler 进行处理。在主线程中调用 Looper.loop() 方法时,实际上是在一个无限循环中等待消息的到来。这个循环并不会阻塞主线程,因为当没有新的消息时,Looper 会进入睡眠状态,而不会占用 CPU 资源。只有当有新的消息到来时,Looper 才会被唤醒,将消息分发给对应的 Handler 进行处理。
就像屏幕锁屏唤醒一样,当我们点击电源键关闭屏幕时,屏幕变暗了,但实际上并没有关闭。此时,系统并没有停止运行,而是进入了休眠状态。当我们再次点击电源键唤醒屏幕时,屏幕会重新变亮,并呈现出之前的界面。这个过程中,系统并没有重新启动应用程序,而是将之前的状态保存下来,并在唤醒时恢复状态。这就好像 Looper 进程在等待消息的过程中,系统并没有阻塞主线程,而是将主线程的状态保存下来,并在消息到来时恢复状态。
线程的挂起和恢复,是通过系统底层的操作来实现的。在 Android 中,这个操作是通过 POSIX 线程库(PThread)中的 pthread_suspend 和 pthread_resume 函数来实现的。
当 Looper 进入睡眠状态时,实际上是在循环之前调用了 pthread_suspend 函数,将当前线程挂起。这个函数会将线程的状态保存到内核中,包括寄存器和堆栈等信息。这样,线程就不会再占用 CPU 资源,处于等待状态。当新的消息到来时,Looper 会被唤醒,进入循环,并调用 pthread_resume 函数来恢复之前保存的线程状态,从而继续处理消息。
1.8 IdleHandler(闲时机制)?
IdleHandler(闲时机制)是 Android 系统中的一种机制,它允许应用程序在系统空闲时执行一些任务。IdleHandler 通常被用来执行一些不太紧急,但是需要在系统空闲时尽快完成的任务,例如清理缓存或者后台任务等。
IdleHandler 的实现依赖于 Looper 消息循环机制,具体来说,当 Looper 空闲时,就会检查是否有注册了 IdleHandler 的任务需要执行,如果有,就会立即执行这些任务,直到没有任务可执行为止。然后,Looper 会再次开始接收消息并处理。
通过使用 IdleHandler 机制,可以避免在主线程中执行一些比较耗时的任务,从而避免阻塞 UI 线程,提高应用程序的响应性能。
上述代码中,我们首先创建了一个 Handler 对象,并定义了需要在闲时完成的任务。然后,我们创建了一个 IdleHandler 对象,将其添加到当前线程的 MessageQueue 中。当系统处于闲时状态时,MessageQueue 会自动执行该 IdleHandler 中的任务。最后,我们在 onResume() 和 onPause() 方法中分别将 IdleHandler 添加和移除,以避免在 Activity 不可见时浪费资源。
1.9 postDelay()的实现原理?
Handler 的 postDelay() 方法用于在指定延迟时间后向消息队列中发送一条消息,其具体实现原理如下:
1.计算消息的执行时间:Handler 在发送延迟消息时会先根据当前系统时间和指定的延迟时间计算出消息的执行时间。
2.创建 Message 对象:然后,创建一个 Message 对象,将需要执行的操作封装到该 Message 中,然后将其发送到目标 Handler 的消息队列中。
3.启动定时器:在将 Message 对象添加到消息队列中之前,Handler 首先会通过调用内部的 sendMessageAtTime() 方法,将该消息加入到一个优先级队列中。该队列是按照消息执行时间的先后顺序排序的,其中执行时间最早的消息排在队列的最前面。
4.循环检查:然后,Handler 会不断循环检查该队列,以判断是否有消息的执行时间已经到了。如果有消息的执行时间已经到了,Handler 就会将该消息从队列中取出,并将其加入到消息队列中等待处理。
5.处理消息:一旦消息被取出,Handler 就会根据消息的类型以及携带的数据来进行相应的处理。如果消息是一个 Runnable 对象,Handler 就会将其投递到主线程中运行;如果是其他类型的消息,Handler 就会调用 handleMessage() 方法来处理该消息。
需要注意的是,postDelay() 方法并不是精确的定时器,而是基于当前系统时间和延迟时间的近似计算。因此,在使用 postDelay() 方法时需要特别注意这一点。同时,如果需要实现精确的定时器功能,可以考虑使用 SystemClock.elapsedRealtime() 方法,该方法返回从系统启动到当前时刻的毫秒数,从而避免受到系统时间的影响。
1.10 Handler的post方法和sendMessage方法区别?
Handler 中的 post() 和 sendMessage() 方法都用于向消息队列中添加一条消息,不同之处在于它们的实现方式不同,具体如下:
post() 方法
post() 方法是 Handler 中的一个简化版的发送消息方法,它将一个 Runnable 对象封装成 Message 对象,并将其添加到消息队列中。post() 方法会在消息队列中取出该 Message 对象,并调用该 Message 对象中的 Runnable 对象的 run() 方法。它通常用于在当前线程中执行一个耗时较短的操作,或者在主线程中更新 UI 界面。
sendMessage() 方法
sendMessage() 方法是 Handler 中一个较为底层的方法,它接收一个 Message 对象作为参数,并将该 Message 对象添加到消息队列中。与 post() 方法不同的是,sendMessage() 方法可以设置消息的优先级,消息的优先级越高,就会越早被处理。sendMessage() 方法还可以在发送消息的同时携带一个 int 类型的参数,该参数可以用来标识消息的类型。当 Handler 接收到该消息时,可以根据携带的参数来判断消息的类型,从而执行不同的操作。
总之,post() 方法比 sendMessage() 方法更加简单和直接,适合用于发送一些简单的操作。而 sendMessage() 方法则可以设置消息的优先级,提供更多的功能和扩展性。
1.11 使用Handler需要注意什么问题,怎么解决的?
在使用 Handler 时,有一些常见的问题需要注意:
1.内存泄漏
当一个 Handler 对象被创建并关联到一个线程时,它将持有该线程的 Looper 和 MessageQueue 对象的引用,如果这个 Handler 对象没有及时地被销毁,就会导致内存泄漏。为了避免这种问题,应该尽量避免使用匿名内部类来创建 Handler 对象,并且在不需要使用 Handler 对象时及时将其销毁。
2.ANR
如果在主线程中执行一些耗时的操作,就会导致 ANR(Application Not Responding)错误,这是因为主线程被阻塞,无法及时响应用户的操作。为了避免这种问题,应该将耗时的操作放在子线程中执行,或者使用 post() 方法将操作放到消息队列中,让主线程逐个执行,从而保证主线程的响应性。
3.线程安全
Handler 在多线程环境中使用时需要特别注意线程安全问题,如果多个线程同时向 Handler 中发送消息,就可能会出现线程安全问题。为了解决这个问题,可以使用 synchronized 关键字对 Handler 进行同步,或者使用线程安全的类(比如 AtomicInteger)来处理消息的唯一标识。
4.内存溢出
如果同时向 Handler 中发送大量的消息,就可能会导致内存溢出。为了避免这种问题,可以使用 MessagePool 对象来重用 Message 对象,或者限制消息的数量,保证消息队列中的消息不会过多。
综上所述,为了避免在使用 Handler 时出现问题,需要注意内存泄漏、ANR、线程安全和内存溢出等问题,并采取相应的措施进行解决。
1.12 什么情况会导致内存泄漏,如何修复?
在Android开发中,常见的内存泄漏问题包括以下几种情况:
1.非静态内部类持有外部类的引用:当非静态内部类持有外部类的引用时,如果没有正确地释放这些引用,就会导致内存泄漏。可以使用静态内部类或弱引用来避免这种情况。
2.匿名内部类持有外部类的引用:当匿名内部类持有外部类的引用时,如果没有正确地释放这些引用,就会导致内存泄漏。可以使用静态内部类或弱引用来避免这种情况。
3.Handler导致的内存泄漏:当一个Handler被创建并且与某个线程相关联时,如果没有正确地移除MessageQueue中的Message,就会导致内存泄漏。可以使用静态内部类或弱引用来避免这种情况。
4.资源未关闭导致的内存泄漏:当使用一些需要手动关闭的资源,比如Cursor、FileInputStream等时,如果没有及时地关闭这些资源,就会导致内存泄漏。需要在finally块中正确地关闭资源。
修复Android内存泄漏问题的方法包括以下几个方面:
1.使用静态内部类:使用静态内部类可以避免非静态内部类和匿名内部类持有外部类的引用导致的内存泄漏问题。
2.使用弱引用:可以使用弱引用来避免一些持有对象引用的问题,比如Handler导致的内存泄漏。
3.及时释放资源:当使用一些需要手动关闭的资源时,需要及时地关闭这些资源,避免资源未关闭导致的内存泄漏。
4.使用LeakCanary等内存检测工具:可以使用内存检测工具来检测内存泄漏问题,及时发现和解决问题。
总之,修复Android内存泄漏问题需要认真分析程序代码,了解对象的生命周期和Android系统的特性,找到内存泄漏的根本原因,并采取相应的措施进行修复。
1.13 下载一张很大的图,如何保证不 oom?
在Android中下载一张很大的图,可能会导致OOM(Out Of Memory)问题,可以采取以下几种方式来避免OOM:
1.压缩图片:下载图片后,可以通过压缩图片的方式来减小图片的内存占用,可以使用Android提供的BitmapFactory.Options参数来实现。可以根据需要设置图片的压缩比例、采样率、解码格式等,从而减小图片的内存占用。
2.使用适当的图片加载库:在下载大图时,可以使用适当的图片加载库,如Glide、Picasso等,这些库会自动处理图片的压缩和缓存,避免OOM问题的发生。
3.将图片分段加载:对于非常大的图片,可以将图片分段加载,每次只加载部分图片,然后进行拼接。这种方式可以避免一次性加载整张图片导致的OOM问题。或者使用BitmapRegionDecoder这个来区域加载。
4.内存缓存和磁盘缓存:可以使用内存缓存和磁盘缓存来缓存下载的图片,减少图片的下载和解码次数,提高图片加载效率和用户体验。
总之,下载一张很大的图片时,需要采取适当的措施来避免OOM问题的发生,包括压缩图片、使用适当的图片加载库、将图片分段加载、使用内存缓存和磁盘缓存等。同时也要注意控制图片的大小和数量,避免过多的图片加载导致内存占用过高。
1.14 Handler机制原理?
在Android中,Handler机制是一种重要的机制,它可以用来在不同线程之间传递消息和执行延迟任务。下面是Handler机制的原理:
1.Handler对象:在Android中,Handler对象是用来处理消息队列的。每个Handler对象都会关联一个Looper对象,Looper对象负责维护消息队列。
2.Message对象:Message对象封装了要处理的消息内容,可以包含任何类型的数据。
3.MessageQueue对象:MessageQueue对象维护了所有待处理的消息,它按照时间顺序存储在队列中。
4.Looper对象:Looper对象用于不断地从消息队列中取出消息,并将其发送到对应的Handler对象中进行处理。
5.Thread对象:Handler和Looper通常是在同一个线程中创建的。在Android中,UI线程就是一个使用Handler机制的典型例子。
当我们创建一个Handler对象时,它会自动关联当前线程的Looper对象。然后我们可以使用Handler对象发送消息到消息队列中。当Looper对象从消息队列中取出一条消息时,它会将该消息发送到对应的Handler对象中进行处理。
使用Handler机制的一般步骤如下:
1.创建一个Handler对象。
2.在需要的时候,使用Handler对象发送消息到消息队列中。
3.创建一个Looper对象并开启一个消息循环,Looper对象将不断从消息队列中取出消息。
4.在Handler对象的回调函数中处理消息。
总之,Handler机制是Android中实现线程间通信的一种机制。通过使用Handler、Message、MessageQueue和Looper等组件,我们可以在不同的线程之间发送消息和执行延迟任务。
2.Android高级
2.1 怎么计算一个View在屏幕可见部分的百分比?
要计算一个View在屏幕可见部分的百分比,可以使用以下步骤:
1.获取屏幕的高度和宽度,可以通过Context的Resources对象获取。
2.获取View在屏幕上的坐标位置和尺寸,可以使用View类的getGlobalVisibleRect()方法。
3.计算View的面积,即View的宽度乘以高度。
4.计算View在屏幕可见部分的面积,即View的可见宽度乘以可见高度。可以使用View类的getLocalVisibleRect()方法获取View在屏幕上可见的区域。
5.计算View在屏幕可见部分的百分比,即可见面积除以总面积。
2.2 Android常用的优化UI有哪些?
Android UI 优化可以从多个方面入手,以下是一些常见的 Android UI 优化方法:
1.减少视图层次结构:视图层次结构越深,布局计算和绘制就会越耗费时间。优化的方法是尽量减少视图嵌套,可以使用 ConstraintLayout 来优化布局。
2.使用 RecyclerView:RecyclerView 可以优化列表和网格视图,它可以复用视图并延迟视图的实例化,从而提高性能。
3.使用 ViewPager2:ViewPager2 可以优化滑动视图,它支持嵌套滑动、预加载和滑动监听等功能。
4.使用图像压缩:在应用中使用合适的图像压缩算法可以减小应用的包大小,并减少加载和解码时间。
5.避免在 UI 线程上执行耗时操作:所有的耗时操作都应该在子线程上执行,以避免阻塞 UI 线程,导致应用卡顿。
6.使用卡片布局:在列表视图中使用卡片布局可以提高用户体验,并使列表更加易于阅读和导航。
7.使用动画:动画可以提高用户体验,并使应用看起来更加生动和吸引人。
8.使用异步加载:异步加载可以在后台加载数据,以减少 UI 线程上的工作量,从而提高应用性能。
9.使用内存缓存:在应用中使用内存缓存可以减少重复加载数据的次数,从而提高应用性能。
以上是一些常见的 Android UI 优化方法,但实际的优化方法取决于具体应用的需求和设计。
2.3 WebView 与 JS 交互方式,shouldOverrideUrlLoading、onJsPrompt使用有啥区别?
Android WebView 可以通过多种方式与 JavaScript 交互,其中比较常用的包括使用 shouldOverrideUrlLoading 和 onJsPrompt 方法。
shouldOverrideUrlLoading 方法是在 WebView 中加载一个 URL 时被调用的。在此方法中,开发者可以拦截 WebView 加载的 URL,例如在 WebView 中加载一个链接时,可以使用该方法拦截该链接并在应用内打开。
onJsPrompt 方法是在 WebView 中执行 JavaScript 代码时被调用的。在此方法中,开发者可以通过调用 Android 端的代码,将参数传递给 JavaScript,或者从 JavaScript 中获取返回值。
这两种方法的区别在于它们被调用的时间和目的不同。shouldOverrideUrlLoading 方法主要用于拦截 WebView 加载的 URL,而 onJsPrompt 方法主要用于在 JavaScript 和 Android 端之间传递数据。
需要注意的是,JavaScript 和 Android 之间的交互需要在主线程中进行。如果在子线程中执行这些方法,会抛出异常。
2.4 Android JS 错误治理方案?
在 Android 中,与 JavaScript 交互是常见的需求,但是由于 JavaScript 代码复杂、操作异步等原因,很容易发生错误。为了提高应用程序的健壮性和用户体验,我们需要对 JS 错误进行有效治理。下面是一些 Android 中 JS 错误治理的方案:
1.监听 JavaScript 错误:通过在 WebView 中设置 WebChromeClient,我们可以重写 onConsoleMessage() 方法,以捕获 JavaScript 错误信息。在 onConsoleMessage() 中可以检查错误级别、错误消息以及发生错误的行数和文件名等信息。
2.显示错误信息:如果在 WebView 中发现了 JavaScript 错误,应该向用户展示一个友好的错误信息,而不是直接崩溃或者什么反应都没有。可以通过创建一个自定义的 WebChromeClient,在其中重写 onConsoleMessage() 方法,将 JavaScript 错误信息以友好的方式展示给用户。
3.使用 try-catch 块:在与 JavaScript 交互的过程中,可以使用 try-catch 块来捕获 JavaScript 异常。这可以帮助我们在出现错误时及时处理它们,避免应用程序崩溃。
4.使用回调函数:当与 JavaScript 交互时,我们可以使用回调函数来处理异步操作的结果。在处理回调函数时,需要注意异常处理。可以使用 try-catch 块来捕获异常,并在 UI 上显示错误信息。
5.限制 JavaScript 执行:通过使用 JavaScript 引擎的限制方法,可以限制 JavaScript 执行的最大时间和内存占用等。例如,我们可以使用 setJavaScriptEnabled() 方法来禁用 JavaScript,或者使用 setJavaScriptTimeout() 方法来限制 JavaScript 执行的最大时间。
总之,在开发 Android 应用程序时,需要注意处理 JS 错误,以提高应用程序的稳定性和用户体验。以上方法可以根据具体情况选择使用。
2.5 用户行为监控方案,怎么设计?
Android 用户行为监控是指对用户在应用中的操作进行跟踪和记录,以便分析和优化应用程序的性能和用户体验。以下是一些常见的 Android 用户行为监控方案设计:
1.统计用户行为:通过监视用户的操作,例如点击、滑动、页面切换等,我们可以收集用户行为数据,并对其进行统计和分析。可以使用第三方工具,例如 Google Analytics、Flurry 等,也可以自己开发代码来实现数据的收集和统计。
2.记录应用错误:当应用程序出现错误或崩溃时,我们需要及时记录错误信息,并上传到服务器或本地存储。可以使用第三方库,例如 Crashlytics、Bugsnag 等,或自己开发代码来捕获并记录错误信息。
3.操作步骤记录:对于一些关键操作,例如提交表单、购买商品等,我们可以记录用户的操作步骤和结果,以便在出现问题时进行排查和修复。可以使用自定义日志和数据库等方式来记录这些数据。
4.检测性能问题:通过监测应用程序的响应时间、卡顿情况、内存使用等指标,我们可以检测应用程序中的性能问题。可以使用性能检测工具,例如 Android Profiler、TraceView 等,或自己开发代码来进行性能监测。
5.用户反馈:用户反馈是一种非常有价值的用户行为数据,可以帮助我们发现应用程序中的问题并进行改进。可以通过添加反馈表单、使用第三方反馈工具等方式来收集用户反馈。
总之,在进行 Android 用户行为监控时,我们需要保证数据的准确性和隐私性,并且不会对应用程序性能产生负面影响。通过以上方案,我们可以更好地了解用户需求和应用程序性能,并对应用程序进行优化和改进。
2.6 Picasso基本原理?
Picasso 是一个基于 Android 平台的图片加载库,可以帮助我们快速地加载网络图片和本地图片,并且提供了灵活的缓存策略、图片转换等功能。其基本原理如下:
1.加载图片:当我们使用 Picasso 加载图片时,首先会检查内存缓存中是否有对应的图片,如果有则直接返回。否则,会判断磁盘缓存中是否有对应的图片,如果有则从磁盘缓存中读取,并将其保存到内存缓存中。如果都没有,则从网络上加载图片。
2.显示图片:当加载图片完成后,Picasso 会在主线程上执行回调函数,将加载的图片传递给我们的应用程序。如果需要显示图片,则会将其设置到 ImageView 上。
3.缓存策略:Picasso 支持多种缓存策略,包括内存缓存和磁盘缓存。内存缓存使用 LRU 算法来缓存图片,而磁盘缓存则使用 DiskLruCache 实现。我们可以通过设置缓存大小、缓存目录等参数来控制缓存的大小和位置。
4.图片转换:Picasso 还支持多种图片转换功能,例如裁剪、缩放、旋转、灰度化等。我们可以通过实现自定义的 Transformation 接口来实现自己的图片转换逻辑。
5.异步加载:Picasso 会在后台线程上执行图片加载和缓存操作,避免了在主线程上执行耗时操作的问题。同时,Picasso 也支持取消正在执行的图片加载操作,以避免对应用程序性能的影响。
总之,Picasso 是一个简单易用、功能丰富、性能优秀的图片加载库,在 Android 平台上得到了广泛的应用。通过使用 Picasso,我们可以快速地加载和显示图片,并且提高应用程序的性能和用户体验。
2.7 Picasso是单引擎,怎么做数据隔离?
Picasso 在 Android 中的确是一个单引擎的图片加载库,其内部只维护一个线程池,用于异步加载图片,并且使用 LRU 算法来管理内存缓存。在多 Bundle 的情况下,如果多个 Bundle 中使用 Picasso 加载图片,那么就需要确保数据隔离,避免图片数据的交叉污染。
为了保证数据隔离,可以通过以下几种方式来实现:
1.使用不同的缓存目录:Picasso 支持设置不同的缓存目录,因此可以为每个 Bundle 设置不同的缓存目录,以避免图片数据的交叉污染。
2.使用不同的内存缓存:Picasso 内部使用 LRU 算法来管理内存缓存,因此可以为每个 Bundle 设置不同的内存缓存,以避免图片数据的交叉污染。
3.使用不同的请求标识:Picasso 支持为每个图片加载请求设置标识,因此可以为每个 Bundle 中的图片加载请求设置不同的标识,以避免图片数据的交叉污染。
4.使用不同的实例:Picasso 可以在应用程序中创建多个实例,每个实例维护一个单独的线程池和缓存。因此,可以为每个 Bundle 创建一个独立的 Picasso 实例,以确保数据隔离。
综上所述,为了保证在多 Bundle 的情况下数据隔离,可以采取上述措施中的任意一种或多种进行实现。
2.8 介绍下 Binder 机制,与内存共享机制有什么区别?
Binder 是 Android 操作系统中一种进程间通信 (IPC) 机制,它可以让不同进程中的组件之间进行通信,包括 Activity、Service、BroadcastReceiver 等,是 Android 系统中非常重要的组件之一。
Binder 的工作原理是通过客户端进程和服务端进程之间的跨进程调用 (RPC) 实现的。在 Binder 机制中,客户端进程通过 Binder 代理对象来访问服务端进程中的 Binder 对象,这个过程涉及到进程间的通信和对象的序列化等复杂操作。
与 Binder 机制不同的是,内存共享机制是指通过共享内存来实现进程间通信的一种方式。它可以让不同进程之间共享同一块内存区域,以实现数据的共享和传输。在内存共享机制中,不同进程之间可以通过共享内存来实现数据的共享和传输,这种方式不涉及对象的序列化和跨进程调用等复杂操作。
虽然 Binder 和内存共享机制都可以实现进程间通信,但它们的实现方式和应用场景不同。Binder 适用于需要在不同进程中进行复杂交互和通信的场景,如 Activity 与 Service 之间的通信,而内存共享机制适用于需要高效地共享和传输数据的场景,如多个进程之间共享同一份数据等。
2.9 Binder机制原理?
Android Binder 机制是一种进程间通信 (IPC) 机制,用于实现不同进程之间的通信。它是 Android 系统中非常重要的组件之一,被广泛应用于 Activity、Service、BroadcastReceiver 等组件之间的通信。
在 Android Binder 机制中,客户端进程通过 Binder 代理对象来访问服务端进程中的 Binder 对象。它的实现原理涉及到以下几个重要组件:
1.Binder 驱动
Binder 驱动是 Android 系统中的一个内核模块,它为不同进程之间的通信提供了底层支持。它通过映射物理内存来实现不同进程之间的内存共享,并为每个进程维护了一个虚拟地址空间,用于管理进程间的内存映射关系。
2.Binder 服务
Binder 服务是指在服务端进程中提供服务的组件,它通过继承 Binder 类并实现相应的接口来向客户端进程提供服务。在服务端进程中,Binder 服务会创建一个 Binder 对象并注册到 Binder 驱动中,以便客户端进程可以通过 Binder 代理对象来访问它。
3.Binder 代理
Binder 代理是指客户端进程中的一个代理对象,它负责将客户端进程中的方法调用转发到服务端进程中的 Binder 对象上,并将结果返回给客户端进程。在客户端进程中,Binder 代理对象会通过系统服务获取服务端进程中的 Binder 对象,并将其包装成一个代理对象返回给客户端进程。
4.在客户端进程中,当需要调用服务端进程中的方法时,客户端进程会将调用请求发送到 Binder 驱动中,然后由 Binder 驱动将请求转发到服务端进程中的 Binder 对象上。服务端进程接收到请求后会调用相应的方法并返回结果,然后由 Binder 驱动将结果返回到客户端进程中。
Binder 机制的实现过程涉及到进程间的通信、对象的序列化和跨进程调用等复杂操作。因此,在使用 Binder 机制时需要注意线程安全、内存泄漏和性能等方面的问题。
2.10 APK的打包过程是什么?
APK(Android Package)是Android应用的安装包,它包含了应用的所有代码、资源、配置文件以及其他必要的文件。APK的打包过程主要包括以下几个步骤:
1.编译Java代码:将Java源代码编译为Dalvik虚拟机可以执行的.dex文件。编译过程使用的工具是Android SDK中的dx命令。
2.打包资源:将应用的资源文件(如图片、布局、字符串等)打包成一个二进制资源文件resources.arsc。打包过程使用的工具是Android SDK中的aapt命令。
3.处理AndroidManifest.xml文件:将应用的清单文件AndroidManifest.xml与.dex文件和resources.arsc文件打包成一个.apk文件。打包过程使用的工具是Android SDK中的apkbuilder命令。
4.对.apk文件进行签名:使用开发者的私钥对.apk文件进行签名,以确保.apk文件未被篡改。签名过程使用的工具是Java SDK中的jarsigner命令。
5.对.apk文件进行对齐:对已签名的.apk文件进行对齐,以优化应用的启动速度和性能。对齐过程使用的工具是Android SDK中的zipalign命令。
2.11 APK的签名机制?
APK(Android Package)是 Android 应用程序的标准发布格式,它包含了应用程序的所有资源,包括代码、图像、音频和文本等。
APK 签名机制是一种保证应用程序完整性和安全性的机制。在应用程序发布到 Google Play 商店之前,应用程序需要进行签名。APK 签名是将应用程序与数字证书进行关联的过程。这个数字证书由应用程序开发者私下生成,而签名则是在应用程序打包完成之后完成的。
APK 签名机制主要有以下两个目的:
防止篡改:通过将应用程序与数字证书进行关联,签名机制可以防止应用程序在传输过程中被篡改。如果应用程序在传输过程中被篡改,数字签名就会失效,从而提醒用户不要安装这个应用程序。
确认身份:签名机制可以用于确认应用程序的开发者身份。如果应用程序的数字签名与应用程序开发者的数字证书匹配,那么就可以确认这个应用程序确实是由这个开发者开发的。
在 Android 应用程序中,APK 签名是通过使用 Java Keystore 文件和 keytool 工具来完成的。在签名应用程序之前,开发者需要生成一个私钥和一个数字证书,然后使用 keytool 工具将私钥和数字证书保存到 Java Keystore 文件中。最后,开发者需要使用 jarsigner 工具将应用程序与数字证书进行关联并签名。
3.Java基础
3.1 两个值相等的Integer对象,==比较,是否相等?
在 Java 中,两个值相等的 Integer 对象,使用 == 操作符进行比较时,可能会返回 false。这是因为 Integer 类型是一个类,而不是基本数据类型。当使用 == 操作符比较两个 Integer 对象时,比较的是对象的引用是否相等,而不是它们所代表的值是否相等。
3.2 解释下ClassLoader的双亲委派机制?
Java中的ClassLoader是用来动态加载类的机制,其使用了一种叫做“双亲委派机制”的加载机制。该机制的主要思想是在类的加载时,优先委托父类的ClassLoader去加载,如果父ClassLoader无法加载该类,再由自身的ClassLoader来加载。这种委派机制保证了Java程序的稳定性和安全性,防止不同ClassLoader加载同一个类,导致类的冲突和安全问题。
双亲委派机制的基本实现流程如下:
1.当一个类加载器接收到类加载请求时,首先会检查它的父类加载器是否已经加载过这个类。
2.如果父类加载器已经加载了该类,那么直接返回父类加载器所加载的类。
3.如果父类加载器没有加载该类,则把类加载请求向上委托给父类加载器。
4.如果父类加载器还没有加载该类,则重复步骤3,直到达到了顶层的启动类加载器。
5.如果父类加载器都无法加载该类,那么该类加载器尝试自己去加载该类。
6.如果该类加载器自己也无法加载该类,那么会抛出ClassNotFoundException异常。
使用双亲委派机制,可以保证Java虚拟机中所有的类都会被统一的父类加载器加载,并且不会出现同一个类被多个类加载器加载的情况。同时,也能防止用户自定义的类覆盖Java API中的核心类,从而提高了Java程序的稳定性和安全性。
3.3 synchronized 修饰 static 方法、普通方法、类、方法块区别
synchronized 关键字是 Java 中用于实现线程同步的一种机制,可以修饰方法、代码块和类。在修饰不同的目标上,synchronized 的作用和用法也有所不同:
1.修饰静态方法(static synchronized 方法):在类级别上进行同步,即不同实例对象共享同一个锁。因此,在多线程环境中,只有一个线程能够访问该静态方法,其他线程需要等待该线程执行完毕后才能访问。此外,静态方法可以直接通过类名调用,因此可以不依赖实例对象而进行同步。
2.修饰实例方法(synchronized 方法):在对象级别上进行同步,即每个实例对象都拥有自己的锁。因此,在多线程环境中,每个实例对象可以被多个线程同时访问,但同一时刻只能有一个线程访问该实例方法。这种方式通常用于保证多个线程访问同一个对象时的安全性。
3.修饰代码块(synchronized 块):在对象级别上进行同步,需要指定一个锁对象。只有获得了锁对象的线程才能访问该代码块,其他线程需要等待锁的释放。和实例方法一样,每个实例对象都拥有自己的锁,因此不同实例对象之间的代码块是相互独立的。
4.修饰类(synchronized(Class.class)):在类级别上进行同步,即使用类的 Class 对象作为锁。和静态方法一样,不同实例对象共享同一个锁。因此,当一个线程获得了该锁之后,其他线程需要等待该线程释放锁之后才能访问该类的任意 synchronized 块或静态 synchronized 方法。
需要注意的是,使用 synchronized 修饰方法或代码块时,需要选择合适的锁对象,以避免出现死锁和竞争条件等问题。在使用 synchronized 进行线程同步时,还应该注意控制锁的粒度,尽可能缩小同步块的范围,以提高并发性能。
3.4 synchronized 底层实现原理?
在 Java 中,synchronized 关键字是用来实现同步的,主要是通过对对象的监视器(monitor)进行加锁和解锁来实现的。在 Java 中,每个对象都有一个监视器,它可以被用来实现对象的同步。
具体来说,当一个线程试图进入一个被 synchronized 修饰的代码块时,它必须先获得该代码块所对应的对象的监视器锁。如果锁没有被其他线程持有,则该线程将获得锁,并可以执行代码块中的代码;否则,该线程将被阻塞,直到锁被释放为止。
当线程执行完 synchronized 代码块时,它会释放所持有的监视器锁。如果此时有其他线程在等待该锁,则它们中的一个将获得该锁,并可以执行代码块中的代码。
Java 中 synchronized 的底层实现是通过 Java 对象头的 Mark Word 来实现的。每个 Java 对象头都有一个 Mark Word,其中包含了对象的状态信息以及锁信息。当一个线程尝试获取一个被 synchronized 修饰的代码块的锁时,它会通过修改对象头中的锁信息来获得锁。当线程释放锁时,它会再次修改对象头中的锁信息来释放锁。
需要注意的是,synchronized 关键字仅能保证代码块的原子性和可见性,但不能保证执行顺序。因此,在多线程编程中,应该通过正确的锁设计和线程间通信来保证正确的执行顺序。
3.5 volatile的作用和原理?
volatile 是 Java 中的一个关键字,它用来修饰变量。volatile 变量具有两个主要的作用:保证变量的可见性和禁止指令重排序。
具体来说,当一个变量被声明为 volatile 后,它的值在每次被读取时都会被强制从主内存中读取,而不是从线程的本地缓存中读取。同时,在写入 volatile 变量时,会立即将新值刷新回主内存,而不是等到该线程退出同步块时再刷新。这就保证了所有线程对该变量的访问都是同步的,可以避免数据不一致的问题。
此外,volatile 还可以禁止指令重排序,这是因为编译器和 CPU 可以对指令进行重排序,以优化程序的执行效率。但在多线程环境下,这可能会导致一些问题,比如可能会让一个线程看到一个不正确的对象引用。通过使用 volatile,可以告诉编译器和 CPU 不要对该变量进行重排序,从而保证程序的正确性。
volatile 的原理是通过使用内存屏障(memory barrier)来实现的。内存屏障是一种硬件或软件机制,用于控制指令的执行顺序和内存访问顺序。在 Java 中,volatile 会使用一种特殊的内存屏障来实现内存同步,它会在写入 volatile 变量时插入一个 Store Store 屏障,确保所有之前的存储都已完成,以及在读取 volatile 变量时插入一个 Load Load 屏障,确保所有之前的加载都已完成。这就保证了 volatile 变量的可见性和禁止指令重排序的效果。
在 Java 中,当一个线程写入 volatile 变量时,会插入一个 Store 屏障,以确保该变量的新值被刷新到主内存,并且在该屏障之前的所有存储操作都已经完成。然后,再插入一个 Store-Store 屏障,以确保所有之前的存储操作对其他处理器可见。这两个屏障的作用是协同工作的,以保证 volatile 变量的写入操作的顺序和可见性。
需要注意的是,虽然 volatile 可以保证变量的可见性和禁止指令重排序,但它并不能保证原子性,也就是说,如果一个操作涉及多个 volatile 变量,那么仍然需要通过锁或者其他的同步机制来保证原子性。
3.6 一个 int 变量用 volatile 修饰,多线程去操作 i++,是否线程安全?
用 volatile 修饰一个 int 变量,可以保证对该变量的写入操作对其他线程的可见性,但是不能保证对该变量的读取和写入操作的原子性。因此,多线程去操作 i++,并不是线程安全的。
i++ 操作实际上包含了读取 i 的值、将其增加 1,然后将结果写回 i 的过程。这个过程并不是原子的,多个线程可能会同时读取 i 的值,然后对其进行修改,从而导致数据不一致的问题。即使使用了 volatile 修饰 i 变量,每个线程也只能保证看到其他线程修改后的最新值,但是仍然无法保证 i++ 操作的原子性。
要保证 i++ 操作的线程安全性,可以使用 Java 中的原子类,比如 AtomicInteger,它可以保证对整数变量的操作具有原子性和可见性。或者使用 synchronized 或者 Lock 等同步机制来保证对变量的操作具有原子性和可见性。
3.7 AtomicInteger 的底层实现原理?
AtomicInteger 是 Java 提供的一个原子类,它提供了一些原子操作,例如对整数变量的原子加减、原子赋值、原子比较等操作。AtomicInteger 通过使用 CAS(Compare And Swap)操作实现了对整数变量的原子操作。
CAS 是一种无锁的原子操作,它可以在并发场景下安全地更新共享变量的值。CAS 操作包括三个操作数:内存地址 V,旧的预期值 A 和新值 B。CAS 操作的语义是,只有当 V 的值等于 A 时,才将 V 的值设置为 B,否则不进行任何操作。
AtomicInteger 通过调用 Unsafe 类的 compareAndSwapInt 方法来实现 CAS 操作。Unsafe 类是 Java 提供的一个不安全类,它可以直接操作内存,使用该类需要具有相应的权限。
AtomicInteger 内部使用一个 volatile 变量来存储整数变量的值,保证了该变量的可见性。同时,AtomicInteger 还包含了一个名为 value 的 int 类型的变量,用于存储整数值。AtomicInteger 的操作方法,比如 getAndAdd,incrementAndGet 等都是通过 CAS 操作来实现的。例如,对于 getAndAdd 方法,AtomicInteger 会先通过 CAS 操作获取当前的 value 值,并将其保存在一个局部变量中,然后通过 CAS 操作将 value 的值更新为 value + delta,如果更新成功,则返回原来的 value 值加上 delta,否则重新进行 CAS 操作。
AtomicInteger 的底层实现原理就是使用 CAS 操作实现对整数变量的原子操作,并保证了该变量的可见性。它通过使用 volatile 变量来保证了变量的可见性,并使用 Unsafe 类来实现 CAS 操作。
3.8 什么是CAS操作?
CAS(Compare And Swap)操作是一种无锁的原子操作,它可以在并发场景下安全地更新共享变量的值。CAS 操作包括三个操作数:内存地址 V,旧的预期值 A 和新值 B。CAS 操作的语义是,只有当 V 的值等于 A 时,才将 V 的值设置为 B,否则不进行任何操作。CAS 操作通常用于实现对共享变量的原子操作,比如对整数变量的原子加减、原子赋值、原子比较等操作。
3.9 说下对线程池的理解,以及创建线程池的几个关键参数?
线程池是一种线程管理的机制,它可以在应用程序启动时创建一组线程并维护它们的状态。线程池可以有效地管理和复用线程资源,从而避免了线程的创建和销毁造成的开销,提高了应用程序的性能和响应速度。
在 Java 中,线程池是通过 ThreadPoolExecutor 类来实现的,它提供了丰富的参数和方法来控制线程池的行为和性能。创建线程池时,通常需要指定以下几个关键参数:
1.corePoolSize:线程池的核心线程数。当任务数少于核心线程数时,线程池会保持核心线程数的线程来处理任务。如果任务数超过核心线程数,线程池会根据情况创建新的线程来处理任务。
2.maximumPoolSize:线程池的最大线程数。当任务数超过核心线程数时,线程池会创建新的线程来处理任务,直到线程数达到最大线程数。如果任务数继续增加,新的任务会被放到任务队列中等待处理。
3.keepAliveTime:线程的空闲时间。如果线程池中的线程处于空闲状态超过该时间,线程池会将其销毁,直到线程数不超过核心线程数为止。
4.workQueue:任务队列。用于存放等待执行的任务。Java 提供了多种类型的任务队列,包括 SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue 等。
5.threadFactory:线程工厂。用于创建线程。可以使用默认的线程工厂,也可以自定义线程工厂来创建线程。
6.rejectedExecutionHandler:拒绝策略。当线程池中的线程都在执行任务时,如果任务队列已满,新的任务将无法提交。这时可以使用拒绝策略来处理这些任务,Java 提供了多种类型的拒绝策略,包括 AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy 等。
以上参数可以根据应用程序的需要进行调整,以达到最佳的性能和效果。通常,线程池的核心线程数应该根据应用程序的负载情况和硬件配置进行调整,最大线程数应该根据系统负载情况和硬件配置进行调整。任务队列的选择也应该根据任务类型和应用程序负载情况进行调整,以达到最佳的性能和效果。
Java大致有这些线程池:
1.FixedThreadPool:固定大小线程池。线程池中的线程数是固定的,适用于处理负载比较稳定的场景,如服务器端的网络请求处理等。
2.CachedThreadPool:可缓存线程池。线程池中的线程数是根据任务数量动态调整的,适用于处理负载不稳定的场景,如 Web 服务器等。
3.ScheduledThreadPool:定时任务线程池。用于执行定时任务和周期性任务,适用于处理定时任务、计划任务等场景。
4.SingleThreadExecutor:单线程池。只有一个线程的线程池,适用于处理需要顺序执行的任务,如事件循环等场景。
5.WorkStealingPool:工作窃取线程池。线程池中的线程数是根据需要动态调整的,每个线程都有自己的任务队列,当某个线程的任务执行完毕时,它可以从其他线程的任务队列中窃取任务继续执行,适用于处理负载不均衡的场景,如计算密集型任务等。
在实际应用中,可以根据应用程序的需要和特点选择适合的线程池类型,以达到最佳的性能和效果。同时,还需要根据具体的业务场景和硬件配置等因素进行调优和优化,以进一步提高线程池的效率和性能。
3.10 介绍下ArrayList 和 HashMap 的使用场景,底层实现原理?
ArrayList 和 HashMap 是 Java 中常用的两种集合类,它们的使用场景和底层实现原理如下:
ArrayList:
ArrayList 是一个动态数组,它可以根据需要自动扩展大小。它的使用场景包括:
当需要在列表中进行大量随机访问操作时。
当需要快速在列表末尾添加或删除元素时。
ArrayList 的底层实现原理是通过一个数组来存储数据。当数组已满,需要添加新元素时,ArrayList 会自动将数组扩大一倍。这个操作的时间复杂度为 O(n),因此如果需要在列表中频繁插入或删除元素,LinkedList 可能是更好的选择。
HashMap:
HashMap 是一个键值对映射表,它可以根据需要自动扩展大小。它的使用场景包括:
当需要根据一个键查找对应的值时。
当需要高效地添加或删除键值对时。
HashMap 的底层实现原理是通过一个数组和链表(或红黑树)来存储数据。当需要添加新的键值对时,HashMap 会根据键的哈希值和数组大小计算出该键值对在数组中的位置,并将该键值对添加到对应的链表(或红黑树)中。如果链表(或红黑树)过长,会转化为红黑树。在查找键值对时,HashMap 会根据键的哈希值和数组大小计算出该键值对所在链表(或红黑树)的位置,并在该链表(或红黑树)中查找对应的值。这个操作的时间复杂度为 O(1),因此 HashMap 是高效的键值对查找数据结构。
需要注意的是,由于哈希冲突的存在,不同的键值对可能会被哈希到同一个位置,这时候需要在同一个位置的链表(或红黑树)中进行线性查找,导致时间复杂度变为 O(n)。因此,在设计 HashMap 时需要合理选择哈希函数和扩容因子,以避免哈希冲突过多。
3.11 ArrayList 与 LinkedList 的区别?
Java中 ArrayList 和 LinkedList 是两种不同的集合类,它们的实现方式不同,因此也有不同的特点和适用场景。下面是它们的主要区别:
1.实现方式
ArrayList 是通过动态数组实现的,底层是一个数组,可以动态地增加和缩小容量。LinkedList 是通过链表实现的,每个节点包含了当前元素以及指向前后节点的引用。
2.访问速度
ArrayList 支持随机访问,即可以通过索引直接访问任意一个元素,因为它底层使用的是数组,数组在内存中是连续的,访问速度很快。LinkedList 不支持随机访问,因为访问任意一个元素需要先从头节点或尾节点开始顺序遍历,访问速度比较慢。
3.插入和删除速度
ArrayList 的插入和删除操作需要移动后面的元素,因为插入和删除操作可能会使得数组中间出现空洞。而 LinkedList 的插入和删除操作只需要修改相邻节点的引用即可,时间复杂度为O(1)。因此,如果需要频繁地执行插入和删除操作,LinkedList 的性能优于 ArrayList。
4.内存占用
由于 ArrayList 是动态数组,因此它需要预留一定的容量以存储元素,当存储元素的数量超过容量时,需要重新分配内存并将原有元素拷贝到新的内存区域中。因此,如果事先无法确定集合的大小,或者需要频繁进行扩容,ArrayList 会浪费一部分内存空间。而 LinkedList 是通过链表实现的,每个节点只需要存储元素本身以及指向前后节点的引用,因此内存占用相对较小。
综上所述,如果需要随机访问集合中的元素,或者需要频繁执行添加和删除操作,可以选择 ArrayList。如果需要频繁地执行插入和删除操作,或者无法确定集合的大小,可以选择 LinkedList。
3.12 Java 的几种引用类型,弱引用的使用场景?
在Java中,主要有四种引用类型:
强引用(Strong Reference):使用new关键字创建对象时,默认创建的就是强引用类型的对象,即强引用指向的对象只有在没有任何强引用指向它时才会被回收。
软引用(Soft Reference):使用java.lang.ref.SoftReference类创建,软引用对象指向的对象在内存不足时才会被回收,可以用于实现内存敏感的高速缓存。
弱引用(Weak Reference):使用java.lang.ref.WeakReference类创建,弱引用对象指向的对象在下一次垃圾回收时被回收,主要用于实现对象注册表、监听器和缓存等功能。
虚引用(Phantom Reference):使用java.lang.ref.PhantomReference类创建,虚引用对象指向的对象随时都可能被垃圾回收器回收,主要用于检测对象被垃圾回收器回收的情况。
在Android中,弱引用主要用于以下场景:
缓存功能:Android应用中经常需要使用缓存来优化性能,弱引用可以用于实现内存敏感的缓存。当内存不足时,弱引用引用的对象会被垃圾回收器回收,从而防止内存泄漏。
视图缓存:在Android中,创建视图的成本比较高,因此可以使用弱引用来缓存视图。当视图不再显示时,视图可以被垃圾回收器回收,从而释放内存。
对象注册表:在Android应用中,经常需要注册一些对象,例如Activity、Fragment、BroadcastReceiver等,弱引用可以用于实现对象注册表。当注册的对象被回收时,弱引用会自动从注册表中移除。
监听器功能:在Android应用中,经常需要注册一些监听器,例如View.OnClickListener、TextWatcher等,弱引用可以用于实现监听器。当被监听的对象被回收时,弱引用可以自动取消监听,从而避免内存泄漏。
需要注意的是,由于弱引用指向的对象随时可能被回收,因此在使用弱引用时需要进行额外的判空处理,以避免空指针异常。同时,对于一些比较重要的对象,例如Bitmap等,弱引用可能无法满足需求,需要使用其他方式进行引用。
3.13 数组插入,考虑扩容?
数组中插入一个新元素时,如果数组已经满了,就需要扩容。以下是一种常见的扩容方法:
1.创建一个新的数组,长度为原数组的两倍。
2.将原数组中的元素逐个复制到新数组中。
3.在新数组中插入新元素。
4.将新数组赋值给原数组的引用,使其成为新的数组。
需要注意的是,在扩容过程中可能会涉及到多次内存分配和数据复制,这可能会影响程序的性能。为了避免频繁的扩容操作,可以在数组初始化时就指定一个较大的容量,或者在每次插入新元素时,先检查数组是否已满,如果已满,则扩容一定比较安全。
4.网络相关
4.1 简单介绍下 Https 的原理?
HTTPS是HTTP协议的加密版本,它使用SSL/TLS协议来保护Web通信的安全性。HTTPS基于公开密钥加密和数字证书认证技术,它通过在通信过程中使用SSL/TLS协议来加密HTTP通信内容,从而保证通信数据的隐私性和完整性。
HTTPS的工作原理可以简单描述如下:
1.客户端向服务器发起HTTPS请求。
2.服务器将自己的公钥发送给客户端。
3.客户端使用服务器的公钥对随机生成的对称密钥进行加密。
4.客户端将加密后的对称密钥发送给服务器。
5.服务器使用私钥对接收到的密文进行解密,获得对称密钥。
6.客户端和服务器使用对称密钥进行加密通信。
7.通信过程中,客户端和服务器会互相验证数字证书的合法性,以保证通信的安全性。
HTTPS的核心安全性建立在公开密钥加密和数字证书认证技术之上。其中,公开密钥加密使用两个密钥:公钥和私钥,公钥用来加密数据,私钥用来解密数据。数字证书认证则是指在数据传输过程中使用数字证书来认证通信双方的身份,确保通信的安全性和可靠性。同时,HTTPS还使用SSL/TLS协议来建立安全通信信道,通过加密、压缩、认证等多种技术,保护Web通信过程的安全性和隐私性。
5.算法相关
5.1 用Java实现:二叉树输出第 k 层节点元素?
实现二叉树输出第 k 层节点元素的Java代码,可以通过遍历二叉树并记录每个节点所在的层数来实现。以下是一个使用递归实现的例子:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public static void printKthLevel(TreeNode root, int k) {
if (root == null) {
return;
}
if (k == 1) {
System.out.print(root.val + " ");
} else {
printKthLevel(root.left, k - 1);
printKthLevel(root.right, k - 1);
}
}
// 该方法接受一个二叉树的根节点和一个整数 k 作为输入,输出该二叉树中第 k 层的所有节点元素。
// 我们首先检查根节点是否为空,如果为空,直接返回。如果 k 等于 1,输出当前节点的值。否则,递归遍历左子树和右子树,并将 k 的值减一,直到 k 等于 1 时输出当前节点的值。这样就可以输出指定层数的所有节点元素。
// 调用如下:
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);
int k = 3;
System.out.print("The elements in level " + k + " are: ");
printKthLevel(root, k);
}
5.2 Java实现两个有序的链表的合并?
可以使用递归来实现两个有序链表的合并,具体步骤如下:
1.如果两个链表都为空,则返回空。
2.如果其中一个链表为空,则返回另一个链表。
3.如果两个链表都不为空,比较它们的头节点,将较小的头节点作为合并后的头节点。
4.对于剩下的链表部分,递归地调用步骤3,将结果连接到合并后的头节点后面。
5.返回合并后的链表头节点。
public class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
public class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
在这个代码中,我们首先判断两个链表是否为空,如果其中一个为空,则直接返回另一个链表。接着,我们比较两个链表的头节点,将较小的头节点作为合并后的头节点,并递归地将剩下的链表部分合并到后面。最后,我们返回合并后的链表头节点。
ListNode l1 = new ListNode(1);
l1.next = new ListNode(2);
l1.next.next = new ListNode(4);
ListNode l2 = new ListNode(1);
l2.next = new ListNode(3);
l2.next.next = new ListNode(4);
Solution solution = new Solution();
ListNode mergedList = solution.mergeTwoLists(l1, l2);
while (mergedList != null) {
System.out.print(mergedList.val + " ");
mergedList = mergedList.next;
}
// 输出:1 1 2 3 4 4
5.3 判断字符串合法
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Solution {
public boolean isValid(String str, String regex) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(str);
return matcher.matches();
}
}
5.4 输入一下代码运行结果?
public void test() {
Student student = new Student("Bobo", 15);
changeValue1(student); // student值未改变,不为null! 输出结果 student值为 name:Bobo、age:15
// changeValue2(student); // student值被改变,输出结果 student值为 name:Lily、age:20
System.out.println("student值为 name: " + student.name + "、age:" + student.age);
}
public changeValue1(Student student) {
student = null;
}
public static void changeValue2(Student student) {
student.name = "Lily";
student.age = 20;
}
5.5 二叉树层序遍历,奇数层逆序遍历节点,偶数层正序遍历?
import java.util.*;
public class Solution {
public List<Integer> levelOrderTraversal(TreeNode root) {
if (root == null) {
return Collections.emptyList();
}
Queue<Pair<TreeNode, Integer>> queue = new LinkedList<>();
List<List<Integer>> levelNodes = new ArrayList<>();
queue.add(new Pair<>(root, 0));
while (!queue.isEmpty()) {
Pair<TreeNode, Integer> pair = queue.poll();
TreeNode node = pair.getKey();
int depth = pair.getValue();
if (depth >= levelNodes.size()) {
levelNodes.add(new ArrayList<>());
}
levelNodes.get(depth).add(node.val);
if (node.left != null) {
queue.add(new Pair<>(node.left, depth + 1));
}
if (node.right != null) {
queue.add(new Pair<>(node.right, depth + 1));
}
}
List<Integer> result = new ArrayList<>();
for (int i = 0; i < levelNodes.size(); i++) {
List<Integer> nodes = levelNodes.get(i);
if (i % 2 == 0) {
result.addAll(nodes);
} else {
Collections.reverse(nodes);
result.addAll(nodes);
}
}
return result;
}
}
5.6 手写HashMap
public class MyHashMap<K, V> {
private static final int DEFAULT_CAPACITY = 16;
private Node<K, V>[] buckets;
private int size = 0;
public MyHashMap() {
this(DEFAULT_CAPACITY);
}
public MyHashMap(int capacity) {
buckets = new Node[capacity];
}
public void put(K key, V value) {
int bucketIndex = getBucketIndex(key);
Node<K, V> currentNode = buckets[bucketIndex];
while (currentNode != null) {
if (currentNode.key.equals(key)) {
currentNode.value = value;
return;
}
currentNode = currentNode.next;
}
Node<K, V> newNode = new Node<>(key, value);
newNode.next = buckets[bucketIndex];
buckets[bucketIndex] = newNode;
size++;
}
public V get(K key) {
int bucketIndex = getBucketIndex(key);
Node<K, V> currentNode = buckets[bucketIndex];
while (currentNode != null) {
if (currentNode.key.equals(key)) {
return currentNode.value;
}
currentNode = currentNode.next;
}
return null;
}
public void remove(K key) {
int bucketIndex = getBucketIndex(key);
Node<K, V> currentNode = buckets[bucketIndex];
Node<K, V> prevNode = null;
while (currentNode != null) {
if (currentNode.key.equals(key)) {
if (prevNode == null) {
buckets[bucketIndex] = currentNode.next;
} else {
prevNode.next = currentNode.next;
}
size--;
return;
}
prevNode = currentNode;
currentNode = currentNode.next;
}
}
public boolean containsKey(K key) {
int bucketIndex = getBucketIndex(key);
Node<K, V> currentNode = buckets[bucketIndex];
while (currentNode != null) {
if (currentNode.key.equals(key)) {
return true;
}
currentNode = currentNode.next;
}
return false;
}
public int size() {
return size;
}
private int getBucketIndex(K key) {
int hashCode = key.hashCode();
return hashCode % buckets.length;
}
private static class Node<K, V> {
private final K key;
private V value;
private Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}
6.设计模式
6.1 常见的设计模式有哪些?
1.Builder(建造者)模式:将一个复杂对象的构建过程分解成多个简单对象的构建过程,并将它们逐步构建成一个复杂对象。在 Android 中,常用于创建复杂的对象,例如 AlertDialog.Builder。
2.Factory Method(工厂方法)模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。在 Android 中,常用于创建一系列相关对象,例如不同类型的 Dialog。
3.Adapter(适配器)模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而无法在一起工作的类可以一起工作。在 Android 中,常用于将不同类型的数据适配到 ListView、4.RecyclerView 等控件中显示。
4.Observer(观察者)模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都得到通知并自动更新。在 Android 中,常用于实现事件监听器和广播机制。
5.Singleton(单例)模式:保证一个类仅有一个实例,并提供一个全局访问点。在 Android 中,常用于管理全局的状态信息,例如应用程序的配置信息。
6.Decorator(装饰)模式:动态地给一个对象添加一些额外的职责,而不需要修改类的代码。在 Android 中,常用于对 View 或者 Drawable 进行装饰,例如使用 Drawable 和 ColorFilter 实现带圆角或者阴影的图片。
7.Facade(外观)模式:为子系统中的一组接口提供一个统一的接口,以便更方便地使用子系统。在 Android 中,常用于简化复杂的库或者系统的使用,例如在 MediaStore 中封装各种类型的媒体查询接口。
8.Template Method(模板方法)模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。在 Android 中,常用于封装通用的流程或者算法,例如 AsyncTask 中的 doInBackground 方法。
9.Strategy(策略)模式:定义一系列的算法,把它们一个个封装起来,并使它们可以相互替换。在 Android 中,常用于实现不同的业务逻辑或者算法,例如实现不同的排序方式。
10.Iterator(迭代器)模式:提供一种方法来访问聚合对象中的各个元素,而又不需要暴露该对象的内部表示。在 Android 中,常用于对数据结构进行遍历,例如使用 Cursor 对查询结果进行遍历。
7.HR相关
7.1 为什么要换工作?
作为程序员,换工作的原因可能有很多,以下是一些可能的回答:
职业发展:我想在职业生涯中不断提升自己的技能和能力,并挑战更高级别的职位和更有挑战性的项目,寻求更多的成长机会和发展空间。我的目标是成为一名技术专家或技术管理者。
团队氛围:我希望在一个团队氛围良好、相互支持、共同成长的公司工作。团队合作和互助是一个良好的工作环境中非常重要的因素。
新技术和工具:我希望在新的工作中学习和应用新技术和工具,以便保持自己在行业中的竞争力。有时候,某些公司的技术栈可能过于陈旧或者不适合自己的兴趣和发展方向,这也可能会成为换工作的原因之一。
总之,换工作的原因可能各不相同,但无论什么原因,都应该在回答时坦诚、积极和专业。同时,也要注意不要贬低现在的公司或同事,以免给自己留下不好的印象。