Android面试资料整理

文章目录

前言

最近打算找工作了,在网上也看了一些面试资料,但毕竟看别人的总结不如自己写一遍,于是便有了这篇文章的总结。这篇文章会包括Android问题 、Java问题、计算机网络问题、JVM问题以及算法结构问题,这些问题的总结来自于厘米姑娘的面试问题总结,看完她的总结作为一个程序员来说还是很钦佩的,而我写这篇文章能保证的是所有的问题都是我自己理解后的答案,而不是直接搬过来的解析。当然,我自己的理解也可能是有问题的,也希望大家能指出。

面试问题整理

Android 问题

一、Activity

1、Activity 的生命周期

Created with Raphaël 2.2.0 onCreat() onStart() onResume() onPause() onStop() onDestroy() onReStart() yes no
  • onCreate : 该方法是在Activity被创建时回调,它是生命周期第一个调用的方法,我们在创建Activity时一般都需要重写该方法,然后在该方法中做一些初始化的操作,如通过setContentView设置界面布局的资源,初始化所需要的组件信息等。
  • onStart : 此方法被回调时表示Activity正在启动,此时Activity已处于可见状态,只是还没有在前台显示,因此无法与用户进行交互。可以简单理解为Activity已显示而我们无法看见。
  • onResume : 当此方法回调时,则说明Activity已在前台可见,可与用户交互了,onResume方法与onStart的相同点是两者都表示Activity可见,只不过onStart回调时Activity还是后台无法与用户交互,而onResume则已显示在前台,可与用户交互。当然从流程图,我们也可以看出当Activity停止后(onPause方法和onStop方法被调用),重新回到前台时也会调用onResume方法,因此我们也可以在onResume方法中初始化一些资源,比如重新初始化在onPause或者onStop方法中释放的资源。
  • onPause : 此方法被回调时则表示Activity正在停止(Paused形态),一般情况下onStop方法会紧接着被回调。当然,在onPause方法中我们可以做一些数据存储或者动画停止等资源回收的操作,但是不能太耗时,因为这可能会影响到新的Activity的显示——onPause方法执行完成后,新Activity的onResume方法才会被执行。
  • onStop : 一般在onPause方法执行完成直接执行,表示Activity即将停止或者完全被覆盖(Stopped形态),此时Activity不可见,仅在后台运行。同样地,在onStop方法可以做一些资源释放的操作(不能太耗时:比如取消网络连接,注销广播接收器)。
  • onRestart :表示Activity正在重新启动,当Activity由不可见变为可见状态时,该方法被回调。这种情况一般是用户打开了一个新的Activity时,当前的Activity就会被暂停(onPause和onStop被执行了),接着又回到当前Activity页面时,onRestart方法就会被回调。
  • onDestroy :此时Activity正在被销毁,也是生命周期最后一个执行的方法,一般我们可以在此方法中做一些回收工作和最终的资源释放。

2、onStart()和onResume()、onPause()和onStop()的区别?

onStart 和onStop 是从Activty是否可见的角度来回调的,而onResume和onPause 是从当前是否位于前台的角度来回调的,都能够被多次调用。

onStart 表示Activity正在启动,此时Activity已处于可见状态,只是还没有在前台显示,因此无法与用户进行交互。可以简单理解为Activity已显示而我们无法看见。

onStop 表示Activity即将停止或者完全被覆盖(Stopped形态),此时Activity不可见,仅在后台运行。同样地,在onStop方法可以做一些资源释放的操作(不能太耗时)。

onResume 表示此时Activity从后台切换到前台,可以与用户进行交互,于onStart相比,onStart和onResume都表示Activity可见,但onstart的时候Activity还在后台,而onResume时Activity从后台切换到前台。

onPause 表示Activty 此时正在停止,此时Activity切换到后台,不能与用户进行交互。与onStop 都表示不可见,但Activty 都在后台运行。

3、Activity A启动另一个Activity B会回调哪些方法?如果Activity B是完全透明呢?如果启动的是一个Dialog Activity呢?

Activity A启动另一个Activity B会调用Activity A的onPause、B 的onCreate、onStart、onResume 以及A 的onStop 方法。

如果Activity B 是完全透明的,则只会调用Activity A 的onPause 以及B 的onCreate、onStart、onResume 方法。

如果启动的是Dialog,则什么生命周期都不会调用。

如果启动的是对话框Activity ,则跟启动透明的Activity B一样。

4、谈谈onSaveInstanceState()方法?何时会调用?

当非人力终止Activity时,比如说屏幕切换时导致Activity被杀死并重新创建,又或者资源内存不足导致低优先级的Activity被杀死,会调用onSaveInstanceState() 来保存状态。该方法在onStop之前执行,但是和onPause 并没有先后关系。

5、onSaveInstanceState()与onPause()的区别?

onSaveinstanceState() 适用于临时性状态的保存,而onPause() 适用于对数据的持久化保存。

6、如何避免配置改变时Activity重建?

为了避免由于配置改变时Activity重建,可以在AndroidManifest.xml 中对应的Activity中设置android:configChanges=“orientation|screenSize”。此时再次旋转屏幕时,该Activity 不会被系统杀死和重建,只会调用onConfigurationChanged。因此,当配置程序需要响应配置改变,指定configChanges属性,重写onConfigurationChanged 方法即可。

7、优先级低的Activity在内存不足被回收后怎样做可以恢复到销毁前状态?

优先级低的Activity在内存不足被回收后重新打开会引发Activity的重新创建。Activity 被重新创建后会调用onRestoreInstanceState(),这个方法在onStart 之后。并将onSaveInstanceState 保存的Bundle 对象作为参数传到onRestoreInstanceState和onCreat方法。因此可以从这两个方法中的Bundle 参数来判断Activity 是否重建,并取出数据进行恢复。但是需要注意的是,在onCreate取出数据的时候一定要注意判断savedInstanceState 是否为空。另外,谷歌更推荐使用onRestoreInstanceState进行数据恢复。

8、说下Activity的四种启动模式?(有时会出个实际问题来分析返回栈中Activity的情况)

  • standard 标准模式:每次启动一个Activity 都会创建一个新实例。
  • singleTop 栈顶复用模式:如果新的Activity 已经位于任务栈的栈顶,就不需要重新创建,并回调onNewIntent(intent) 方法,如果不存在该栈栈顶,则创建一个Activty 放在栈顶。Intent 的标识为:FLAG_ACTIVITY_SINGLE_TOP 。
  • singleTask 栈内复用模式: 如果该Activity 在一个任务栈中存在,则会将栈内的所有位于这个Activity 之上的所有Activty 都清理出栈,将对应的Activity 置于栈顶,并回调onNewIntent(intent)方法。如果不存在该栈,则创建一个Activty 放在栈顶。Intent 的标识为:FLAG_ACTIVITY_NEW_TASK。
  • singleInstance 单例模式:具有此模式的Activty 只能单独位于一个任务栈中,且此任务栈中只有唯一一个实例。

9、谈谈singleTop和singleTask的区别以及应用场景

  • singleTop: 在一个栈中可以有多个相同的Activity实例,也就是说可以重复创建,为了防止快速点击时多次startActivity,可以将目标的Activity 设置为singleTop
  • singleTask:在一个栈中只能有一个Activity实例,即不可以重复创建,主要用于主页或者登录页的返回。

10、onNewIntent()调用时机?

启动模式为singleTop、singleTask、singleInstance的Activity 在以下情况下都会回调onNewIntent()

singleTop:如果新Activity 已经位于任务栈中的栈顶,就不会重复创建,并且回调onNewIntent 方法。

singleTask: 只要该Activity 在任务栈中存在,就不会重复创建,并且回调onNewIntent 方法。

singleInstance: 只要该Activity 在任务栈中存在,就不会重复创建,并且回调onNewIntent 方法。

11、了解哪些Activity启动模式的标记位?

平时大多使用两个标志位 FLAG_ACTIVITY_SINGLE_TOP 以及 FLAG_ACTIVITY_NEW_TASK

FLAG_ACTIVITY_SINGLE_TOP:对应singleTop 启动模式

FLAG_ACTIVITY_NEW_TASK:对应singleTask 启动模式

12、如何启动其他应用的Activity?

在保证有权限访问的情况下,通过隐式的Intnet 进行目标Activity的intent-filter匹配:

  • 一个Intnet 只有同时匹配某个Activity的intent-filter里面的action,category与data才算是完成匹配,这样才能进行启动其他应用的activity
  • 一个activity可以有多个intent-filter,只要Intent 可以完全匹配其中任何一个intent-filter,都可以启动这个activity
补充:intent-filter里面的action,category,data的匹配原则

action的匹配:intent中的action必须和Activity 过滤规则里面的action 完全内容一致才能匹配成功。一个intent-filter里面可以有多个action,只要完全匹配其中的任何一个action都算作匹配成功。

category的匹配:如果intent中存在category,那么所有的category都必须完全匹配Activity过滤规则里面的category才能匹配成功。当然category 的数量可以少于intent-filter里面的category的数量。但是不能只写category,这样是无法匹配成功的,因为category 只是附加信息。

data的匹配:data 有两个部分组成 分别是 mineType和 URI 。

Data也是遵循的部分匹配原则:只要filter中声明的部分匹配成功,就认为整个URI匹配成功。

13、Activity 的启动过程(分析的是Android 8.0 26以后的流程)

使用startActivity进行新Activty的启动时,经过方法的各种跳转会获取到一个IActivityManager的Binder对象,然后会执行ActivityManagerServer.startActivity方法,继续往下执行,在后面会发现app.thread.scheduleLaunchActivity()方法,app.thread类型为 IApplicationThread,它的最终实现者就是 ApplicationThread (是ActivityThread的内部类),然后会发送一个启动消息给Handler,最终的处理者时ActivityThread类handleLaunchActivity 方法里面的performLaunchActivity方法,在这个方法里面会完成Activity对象的创建启动过程。

启动Activty的请求会由Instrumentation 来处理,然后通过Binder向AMS发送请求,AMS 内部维护着一个ActivityStack,负责栈内Activity的状态同步,AMS通过ActivityThread去同步Activity的状态进而完成Activty 生命周期的调用。

14、Android 系统启动流程

  • 系统加电,执行bootloader。Bootloader负责初始化软件运行所需要的最小硬件环境,最后加载内核到内存。
  • 内核加载进内存后,将首先进入内核引导阶段,在内核引导阶段的最后,调用start_kernel进入内核启动阶段。start_kernel最终启动用户空间的init程序。
  • init程序负责解析init.rc配置文件,开启系统守护进程。两个最重要的守护进程是zygote进程和ServiceManager,zygote是Android启动的第一个Dalvik虚拟机,ServiceManager是Binder通讯的基础。
  • zygote虚拟机启动子进程system_server,在system_server中开启了核心系统服务,并将系统服务添加到ServiceManager中,然后系统进入SystemReady状态。
  • 在SystemReady状态,ActivityManagerService与zygote中的socket通信,通过zygote启动home应用,进入系统桌面。

15、Android应用的启动流程

  • 用户按下桌面上的App图标后,Launcher进程会将请求启动主活动(MainActivity)的请求以Binder的方式发送给AMS服务。
  • AMS服务收到请求后,交付给ActivityStarter处理intent和flag信息,然后交给ActivityStackSuperVisior/ActivityStack处理Activity进程相关流程,同时通过Socket客户端向Zygote进程请求孵化新进程。
  • Zygote进程收到请求后,创建一个新进程,这个新进程就是APP所在进程
  • 在新进程里创建ActivityThread线程,包含main方法,是Android程序的入口,ActivityThread所在线程即是主线程(UI线程)。同时创建ApplicationThread和W线程,他们都继承自Binder类。ApplicationThread线程在主活动创建之前创建,负责监听AMS发送来的创建Activity的请求。Activity创建后,W线程监听WMS发来的消息(比如点击和触摸事件),将消息发送给DectorView,如果DecoterView没有处理,则传递给PhoneWindow,如果PhoneWindow也没有处理,则传递给Activity通过Handler来处理消息。
  • ApplicationThread类监听到了创建Activity的请求,ActivityThread通过ClassLoader类加载器加载Activity并创建Activity实例,然后回调onCreate()方法。

二、Fragment

1、谈一谈Fragment的生命周期?

Created with Raphaël 2.2.0 onAttach() onCreate() onCreateView() onActivityCreated() onStart() onResume() onPause() onStop() onDestoryView() onDestory() onDetach()
  • onAttach : 该方法在Activity与Fragment关联之后调用,只有这里可以修改初始化fragment参数。
  • onCreate : 当Fragment初次创建时调用,但此时相关联的Activity还没有创建完成,这里是获取不到Activity相关联的资源。
  • onCreateView : 当Fragment创建视图时调用,这里返回视图控件。
  • onActivityCreate : 当Activity的onCreate方法结束后调用,这里可以获取Activity 相关联的资源。
  • onStart : 此方法被回调时表示Fragment正在启动,此时Fragment已处于可见状态,只是还没有在前台显示,因此无法与用户进行交互。可以简单理解为Fragment已显示而我们无法看见。
  • onResume : 当此方法回调时,则说明Fragment已在前台可见,可与用户交互了,onResume方法与onStart的相同点是两者都表示Fragment可见,只不过onStart回调时Fragment还是后台无法与用户交互,而onResume则已显示在前台,可与用户交互。
  • onPause : 此方法被回调时则表示Fragment正在停止(Paused形态),一般情况下onStop方法会紧接着被回调。当然,在onPause方法中我们可以做一些数据存储或者动画停止等资源回收的操作,但是不能太耗时。
  • onStop : 此方法被回调时则表示Activity即将停止或者完全被覆盖(Stopped形态),此时Fragment不可见,仅在后台运行。同样地,在onStop方法可以做一些资源释放的操作(不能太耗时:比如取消网络连接,注销广播接收器)
  • onDestoryView : 此方法回调时,则说明在onCreateView创建的视图将要和Fragment 分离。
  • onDestory : 当这个Fragment不在使用时调用,但是他还没有被销毁,仍能在Activty 中找到。
  • onDetach : Fragment生命周期中最后一个回调是onDetach()。调用它以后,Fragment就不再与Activity相绑定,它也不再拥有视图层次结构,它的所有资源都将被释放。

需要注意的一点是:当Fragment B 要替换Fragment A 时,在创建B之后会先销毁A,然后B再创建视图,流程是:(B) onAttach -> (B) onCreate -> (A) onPause -> (A) onStop -> (A) onDestoryView -> (A) onDestory -> (A) onDetach -> (B)onCreateView -> (B) onActivityCreate -> (B) onStart -> (B) onResume

2、Activity和Fragment的异同?

  • 相同点:他们都包含布局,有自己的生命生命周期。
  • 不同点:由于Fragment 是依附于Activty上的,多了些和Activity 关联的生命周期。如onAttach()、onActivityCreate()、onDestoryView()、onDetach();又因为Fragment的生命周期是Activity 调用而不是系统调用的, 所以Activity的生命周期的方法都是protected,而Fragment是public。

3、Activity和Fragment的关系?

  • 正如Fragment 的名字"碎片",它的出现是为了解决Android的碎片化,他可以作为Activity界面的组成部分,在Activity动态的加入、移除和替换。
  • 一个Activity中可以多个Fragment,而一个fragment 也可以在多个Activity 中出现。
  • Android 的FragmentManager 负责调用队列中Fragment的生命周期方法。activity处于运行状态时,FragmentManager立即调用Fragment 的其他生命周期方法与Activity的状态保持一致。

4、何时会考虑使用Fragment?

  • 在一个Activity界面下,有多种不同的场景进行响应,可以使用ViewPager和Fragment搭配使用。
  • 当一款APP包含界面A和界面B,界面B是界面A的详情页,需要同时适配手机和平板,就可以将界面A与界面B 封装成Fragment,这样一套代码就可以适配两种设备。

三、Service

1、谈一谈Service的生命周期?

Created with Raphaël 2.2.0 onCreate() onStartCommand() onBind() onUnBind() onDestory()
  • onCreate() : 服务创建时调用
  • onStartCommand() : 服务启动时调用
  • onBind() : 服务被绑定时调用
  • onUnBind() : 服务被解绑时调用
  • onDestory() :服务停止时被调用

2、Service的两种启动方式?区别在哪?

这两种启动方式是 startService()和bindService()

  • startServcie() 方法第一次启动一个服务,会依次调用onCreate()和onStartCommand()两个方法,以后再调用startService方法启动服务的话不会再创建新的Service对象,直接复用一开始创建的Service,然后调用onStartCommand()方法。直到stopService() 或者stopSelf() 方法被调用,服务才会停止并调用onDestory()方法。而且不管startService()调用了多少次,只要调用一次stopService()或者stopSelf()方法,这个服务就会被停止掉。
  • bindService() 方法第一次绑定一个服务,会依次调用onCreate()和onBind()方法,以后再调用bindService()方法绑定服务都不会再创建并且绑定新的Service对象,直接复用绑定好的Service对象。直到解绑服务unBindService()方法被调用,服务才会被解绑并停止,这时候会依次执行onUnBind()和onDestory()方法。同样的不管bindService()调用了多少次,只要调用一次unbindService()方法,这个服务就会被解绑并停止掉。

3、一个Activty先start一个Service后,再bind时会回调什么方法?此时如何做才能回调Service的destory()方法?

会回调onBind()方法,这时候所触发生命周期方法流程分别是 onCreate()->onStartCommand()->onBind(),要想回调Service的destroy()方法,只能调用unbindService()以及stopService()方法才能使这个服务销毁。

4、Service如何和Activity进行通信?

  • 通过Binder 对象具体实现Service和Activity的通信,利用的是bindService()方法。比如说在自定义的MyService类内部创建一个继承Binder类的MyBinder类,在MyBinder类中创建一个方法返回这个Myservice类,最后在MyService类的onBind()方法中返回这个自定义的MyBinder 类。这样在Activity类绑定这个服务的时候会有onServiceConnection的回调,在回调里面的参数就有这个MyBinder类对象,这样Activity就可以通过MyBinder类对象里面的方法来操作Service的内容,进而实现Service和Activity的通信。
  • 通过广播实现两个实现Service和Activity的通信。比如说在Activity 中注册广播接收器,然后在Service中发送广播。

5、用过哪些系统Service?

  • Wifi服务:WIFI_SERVICE 判断wifi是否开启

  • 音频服务:AUDIO_SERVICE 获取当前的音量 更改音量的大小

  • 连接服务:CONNECTIVITY_SERVICE 获取网络是否连接

  • 窗口服务:WINDOW_SERVICE 获取屏幕的大小

6、是否能在Service进行耗时操作?如果非要可以怎么做?

Service 是运行在主线程中的,并不能进行耗时操作。如果非要进行耗时操作,可以手动打开一个子线程,在子线程中进行耗时操作。如果服务不需要同时处理多个耗时操作请求,可以使用IntentService处理耗时操作。IntentService 是Service 的子类,它使用工作线程逐一处理所有启动请求,只需要实现onHandIntent方法即可。

7、AlarmManager能实现定时的原理?

AlarmManager是Android中常用的一种系统级别的提示服务,在特定的时刻为我们广播一个指定的Intent。简单的说就是我们设定一个时间,然后在该时间到来时,AlarmManager为我们广播一个我们设定的Intent,通常我们使用 PendingIntent,PendingIntent可以理解为Intent的封装包,简单的说就是在Intent上在加个指定的动作。在使用Intent的时候,我们还需要在执行startActivity、startService或sendBroadcast才能使Intent有用。而PendingIntent的话就是将这个动作包含在内了

8、前台服务是什么?和普通服务的不同?如何去开启一个前台服务?

前台服务是指用户可以看到并且当系统内存不足的情况下不允许被杀死的服务,在状态栏必须有一个通知图标,并且下拉状态栏可以看到更详细信息,比如说我们听歌时在状态栏出现的通知。和普通服务的区别在于它可见并且优先级比普通服务要高,内存不足的时候会优先杀死普通服务。在完成Notification通知消息的构建后,在Service的onStartCommand()中可以使用startForeground()方法来让Android服务运行在前台。

9、是否了解ActivityManagerService,谈谈它发挥什么作用?

ActivityManagerService(AMS)是 Android中最核心的服务,主要负责Android四大组件的启动、切换、调度以及应用程序的管理和调度等工作。

AMS提供的主要功能包括以下几项:

  • 统一调度每个应用程序的Activity,应用程序要运行Activity,会首先报告AMS,然后由AMS来决定Activity是否可以启动。
  • 内存管理:Android 官方声称,Activity在退出后,所在的进程不会被立即杀死,从而在下次启动该Activity的时候能够快速启动。这些Activty 只有当内存不足的时候才会被自动杀死,应用程序并不关心这些问题,这些都是在AMS中完成的。
  • 进程管理:AMS向外提供了查询系统正在运行的进程信息的API

10、如何保证Service不被杀死?

1、提高进程优先级,降低进程被杀死的概率

  • 监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。
  • 设置前台service

2、在进程被杀死后,进行拉活

  • 注册高频率广播接收器,唤起进程。如网络变化,解锁屏幕,开机等
  • onDestroy方法里重启service:service +broadcast 方式,就是当service走ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service;
  • 双进程相互唤起。

四、Boardcast Receiver

1、广播有几种形式?什么特点?

  • 普通广播:一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们接收的先后是随机的。
  • 有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递,所以此时的广播接收器是有先后顺序的,且优先级(priority)高的广播接收器会先收到广播消息。有序广播可以被接收器截断使得后面的接收器无法收到它。
  • 本地广播:发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收本应用程序发出的广播。
  • 粘性广播:这种广播只保留最后一条广播,并且一直保留下去,这样即使已经有广播接收器处理了该广播,一旦又有匹配的广播接收器被注册,该粘性广播仍会被接收。

2、广播的两种注册形式?区别在哪?

广播的注册有两种方法:一种在活动里通过代码动态注册,另一种在配置文件里静态注册。两种方式的相同点是都完成了对接收器以及它能接收的广播值这两个值的定义;不同点是动态注册的接收器必须要在程序启动之后才能接收到广播,而静态注册的接收器即便程序未启动也能接收到广播,比如想接收到手机开机完成后系统发出的广播就只能用静态注册了。

五、ContentProvider

1、ContentProvider了解多少?

作为四大组件之一的ContentProvider内容提供者,ContentProvider主要用于数据存储和共享。和文件存储、StaredPreferenes存储、SQLite数据库这三种存储方式不同的是他可以让不同的应用程序进行数据访问,而另外的三种存储方式则只能在当前应用程序内进行数据访问。当然它可以选择性的分享数据,从而让程序本身隐私的数据不会泄露。

六、数据储存

1、Android中提供哪些数据持久存储的方法?

  • File文件储存:通过I/O流的方式写入和读取文件
  • SharedPreference存储:一种轻型的存储方式,常用来存储一些简单的配置信息,本质是基于XML 文件存储key-value键值对数据
  • SQL数据库存储:一款轻量级的关系型数据库,运算速度快,占用资源少,支持SQL语句,用于存储数据量比较大的数据
  • ContentProvider:四大组件之一,用于数据的存储和共享,不仅可以让不同应用程序之间进行数据共享,还可以选择只对哪一部分数据进行共享,可保证程序中的隐私数据不会有泄漏风险。

2、Java中的I/O流读写怎么做?

		try {
            //定义了一个输入流对象 并加入缓存读取流加快读取速度
            FileInputStream fis = new FileInputStream("C:\\A.txt");
            BufferedInputStream bis = new BufferedInputStream(fis);

            //定义了一个输出流对象 并加入缓存输出流加快读取速度
            FileOutputStream fos = new FileOutputStream("C:\\B.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos);

            byte[] bytes = new byte[1024];
            int len;
            // 这里的-1不是指数值,而是指文件为空
            while ((len = bis.read(bytes)) != -1) {
                // 如果有数据的话,就把数据添加到输出流
                bos.write(bytes, 0, len);
            }
            bos.flush();
            bos.close();
        } catch(FileNotFoundException e) {
            e.printStackTrace();
        } catch(IOException e) {
            e.printStackTrace();
        }

3、SharedPreferences适用情形?使用中需要注意什么?

SharedPreferences 是一种轻型的数据存储方式,适用于储存一些简单的配置信息,如int,string,boolean,float、long、set和map。由于系统对SharedPreferences有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读写会变得不可靠,甚至丢失数据。

4、了解SQLite中的事务处理吗?是如何做的?

SQLite在做CRDU操作时都默认开启了事务,然后把SQL语句翻译成对应的SQLiteStatement并调用其相应的CRUD方法,此时整个操作还是在rollback journal这个临时文件上进行,只有操作顺利完成才会更新数据库,否则会被回滚。

5、使用SQLite做批量操作有什么好的方法吗?

使用SQLiteDatabase的beginTransaction 开启一个事务,程序执行到 endTransaction() 方法时会检查事务的标志是否为成功,如果程序执行到 endTransaction()之前调用了setTransactionSuccessful() 方法设置事务的标志为成功则提交事务,如果没有调用setTransactionSuccessful() 方法则回滚事务,这样能保证数据的同步。

6、如果现在要删除SQLite中表的一个字段如何做?

由于SQLite数据库只允许添加表字段而不允许删除和修改表字段,只能采用复制表删除的方法进行操作,也就是说先创建一个新表保留想要保存的字段,然后删除掉原表。

7、使用SQLite时会有哪些优化操作?

  • 在批量操作数据库的时候使用事务的方式更新数据库,这样能保证数据的同步
  • 及时关闭Cursor,避免内存泄漏
  • 耗时操作异步化:数据库的操作属于本地IO,通常比较耗时,可以将这些耗时操作放入异步线程中处理
  • 使用索引加快检索速度:对于查询操作量级较大、业务对要求查询要求较高的推荐使用索引

七、IPC

1、Android中进程和线程的关系?

应用第一次启动时,会启动一个新进程,该进程用应用的包名作为进程名。该进程会启动主线程ActivityThread,也叫做UI线程,UI的绘制都在该线程里完成,然后我们平时创建的线程基本上在进程中执行的。一般来说,一个进程可以有多个线程,当然,线程的数量也必须是有限的。而且一个APP也不能说就只能对应一个进程,我们也可以在AndroidMenifest中给四大组件指定属性android:process开启多进程模式。

2、为何需要进行IPC?多进程通信可能会出现什么问题?

在Android系统中一个应用程序默认只有一个进程,每个进程都有自己独立的资源和内存空间,其他进程不能访问当前进程的内存和资源,无论是多个应用互相访问数据还是一个应用开启多个进程进行数据交流,不同进程中的数据就会有交互的需求,这就无法避免的就要使用IPC(进程间通信)了。

多进程通信容易出现的问题:

  • 静态变量和单例模式失效:由独立的虚拟机造成
  • 线程同步机制失效:由独立的虚拟机造成
  • SharedPreference的不可靠下降:不支持两个进程同时进行读写操作,即不支持并发读写,有一定几率导致数据丢失
  • Application多次创建:Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次。

3、什么是序列化?Serializable接口和Parcelable接口的区别?为何推荐使用后者?

序列化就是表示把一个对象转换成可存储或可传输的状态,序列化以后的对象既可以在本地存储也可以在网络上传播

  • Serializable是Java自带的,而Parcelable是Android特有的。
  • Serializable 在代码实现上很简单,而Parcelable 就需要多写几个方法
  • Serializable 的本质是使用了反射,序列化的过程会比较慢,这种机制在序列化的时候会产生很多临时的对象,会产生频繁的GC垃圾回收。而Parcelable的本质是将对象进行分解,而分解的每一部分都是Intent支持的类型。

如果是在内存中使用,比如说Activity、service之间的对象传递,可以使用Parcelable,因为他的性能比较高,而如果是持久化操作,比如说存储对象,推荐使用Serializable。虽然他的性能比较低,但是Parcelable因为是把对象分解了,不能很好的保持对象的稳定性。

4、Android中为何新增Binder来作为主要的IPC方式?

  • 从性能的角度:传输效率的主要影响因素是内存拷贝数据次数,拷贝的次数越少,传输效率越高。Binder的数据拷贝只需要一次,数据从发送方的缓存区拷贝到内核的缓存区。而管道、消息队列、Socket都需要拷贝两次,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区。虽然共享内存的方式一次拷贝都不需要,但控制复杂,难以使用,所以Binder的传输效率是最好的。
  • 从稳定性的角度:Binder 使用的是C/S架构,C/S架构是指由客户端(Client)和服务端(Service)组成的架构,Client 有什么需求,交给Service 去处理,架构清晰明理,Service 和Client 相对独立,稳定性好。
  • 从安全的角度:传统的Linux IPC的接受方式无法获得对方进程的可靠UID/PID,从而无法鉴别身份。而Binder机制为每个进程都分配了UID/PID,并且在Binder通信时会对UID/PID做有效性检测。

5、使用Binder进行数据传输的具体过程?

  • 注册服务的过程:Server进程向Binder驱动发起了注册服务的请求,Binder驱动将这个注册请求发给了ServiceManager进程,ServiceManager进程添加了该Server进程,这样就完成了注册,Binder驱动持有了Server进程创建的Binder实体
  • 获取服务的过程:Client向Binder 驱动发起了获取服务的请求,Binder驱动将这个请求转发给了ServiceManager进程,ServiceManager将查询到的Server对应的Binder实体的binder引用信息返回给Binder驱动,而Binder驱动将上述Binder的代理对象返回给Client。
  • 开始通信的过程:Client 获取到binder 的代理对象后,可以根据这个代理对象发送数据请求,Client 发送请求后会挂起当前线程,并将参数写入data然后调用transact(),请求会通过系统底层封装后由服务端的onTransact()处理,并将结果写入reply,最后返回调用结果并唤醒客户端线程。

6、Binder框架中ServiceManager的作用?

在Binder框架中定义了四个角色:Server,Client,ServiceManager和Binder驱动。其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间。
在这里插入图片描述

  • ServiceManager服务的管理者:

    ​ Service Manager是系统中一个独立的进程,它是整个Binder机制的守护进程,用来管理开发者创建的各种Server,并且向Client提供查询Server远程接口的功能。

  • Binder驱动

    • 与硬件设备没有关系,其工作方式与设备驱动程序是一样的,工作于内核态。
    • 提供open()mmap()poll()ioctl() 等标准文件操作。
    • 以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。
    • 负责进程之间binder通信的建立,传递,计数管理以及数据的传递交互等底层支持。
    • 驱动和应用程序之间定义了一套接口协议,主要功能由ioctl() 接口实现,由于ioctl()灵活、方便且能够一次调用实现先写后读以满足同步交互,因此不必分别调用write()和read()接口。
    • 其代码位于linux目录的drivers/misc/binder.c中。

7、Android中有哪些基于Binder的IPC方式?简单对比下?

名称优点缺点设用场景
Bundle简单易用只能传输Bundle支持的数据类型四大组件间的进程通信
文件共享简单易用不适合高并发的情况, 并且无法做到进程间的即时通讯无并发访问情况下, 交换简单的数据实时性不高的情况
AIDL功能强大,支持一对多并发通信 ,支持实时通讯需要处理好线程同步一对多通信且有Rpc需求
Messager支持一对多串行通信,() ,支持实时通讯不能很好处理高并发情况,不支持Rpc, 数据通过Message进行传输,因此只能传输Bundle支持的数据类型低并发的一对多即时通信,无 Rpc需求,或者无需返回结果的Rpc需求
ContentProvider在数据源访问方面功能强大,支持一对多并发数据共享,可通过call方法扩展其他操作主要提供数据源的Crud一对多的进程间数据共享
Socket功能强大,可以通过网络传输字节流, 支持一对多并发实时通讯实现细节有点繁琐,不支持直接的Rpc网络数据交换

注:Rpc(调用远程服务中的方法)

8、是否了解AIDL?原理是什么?如何优化多模块都使用AIDL的情况?

AIDL 意思是 Android Interface Defintion Language(Android 接口定义语言)。是用于定义服务端和客户端通信接口的一种描述语言,通过AIDL可以用来生成服务端的一个代理类。通过这个代理类,可以在一个进程中获取另外一个进程的数据和调用其暴露出来的方法,进而满足进程间的通信需求。

AIDL 的本质就是系统提供了一套可快速实现Binder 的工具,定义好AIDL文件只是方便IDE帮我生成所需的Binder类,AIDL并不是必须的文件,AIDL的具体实现就在这个代理类上。

  • AIDL接口:继承IInterface
  • Stub类:Binder的实现类,服务端通过这个类来提供服务。
  • Proxy类:服务器的本地代理,客户端通过这个类调用服务器的方法。
  • asInterface():客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。返回对象:
    • 若客户端和服务端位于同一进程,则直接返回Stub对象本身;
    • 否则,返回的是系统封装后的Stub.proxy对象。
  • asBinder():根据当前调用情况返回代理Proxy的Binder对象。
  • onTransact():运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
  • transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。然后会调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

当有多个业务模块都需要AIDL来进行IPC的时候,此时需要为每个模块都要创建一个AIDL文件,那么对应的Service就会很多,而创建Service是要消耗内存的,就会导致应用资源消耗严重的问题。优化的办法就是建立连接池,将每一个业务模块的Binder请求统一发到一个远程Service中去执行,这样就避免了多次创建Service。

连接池的工作原理:每个业务模块创建自己的AIDL文件并实现接口,然后向服务端提供自己的唯一标识和其对应的Binder对象,服务端只需要一个Service,服务器提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对像,不同的业务模块拿到所需的Binder对象后就可进行远程方法的调用了。

八、View

1、MotionEvent是什么?包含几种事件?什么条件下会产生?

MotionEvent是手指触摸屏幕锁产生的一系列事件

典型的事件有:

  • ACTION_DOWN:手指刚接触屏幕
  • ACTION_MOVE:手指在屏幕上滑动
  • ACTION_UP:手指在屏幕上松开的一瞬间
  • ACTION_CANCEL:手指保持按下操作,并从当前控件转移到外层控件时会触发

2、scrollTo()和scrollBy()的区别?

scrollTo() 是指滑动到指定的位置,而scrollBy() 是指在当前位置上滑动一定的距离,可以不断累加。比如说都是从(0,0)这个位置出发,scrollTo(-10,-10) 会滑动到坐标轴(10,10)这个位置然后不动了,而scrollBy(-10,-10)则会一直滑动下去,第一次到(10,10),第二次到(20,20) 。而且还要注意的一点是他们滑动的是当前View的内容,而不是当前View 的本身。

3、Scroller中最重要的两个方法是什么?主要目的是?

startScroll()方法和computeScroll() 方法

startScroll() 方法并没有滑动的操作,而是初始化滚动数据,这时候就要重绘界面,系统会在绘制View的时候在draw()方法中调用computeScroll()方法,但computeScroll()方法是一个空方法,具体的滑动操作要在这个方法里面去实现,然后根据当前获取到的滚动值根据srollTo方法去移动,然后再重绘界面再移动,最后完成Scroller 的过渡滑动效果。

4、谈一谈View的事件分发机制?

  • 事件分发本质:就是对MotionEvent事件分发的过程。即当一个MotionEvent产生了以后,系统需要将这个点击事件传递到一个具体的View上。
  • 点击事件的传递顺序:Activity(Window) -> ViewGroup -> View
  • 三个主要方法:
    • dispatchTouchEvent:进行事件的分发(传递)。返回值是 boolean 类型,受当前onTouchEvent和下级view的dispatchTouchEvent影响
    • onInterceptTouchEvent:用于事件的拦截,在dispatchTouchEvent()中调用,该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。一旦拦截,则执行ViewGroup的onTouchEvent,在ViewGroup中处理事件,而不接着分发给View。且只调用一次,所以后面的事件都会交给ViewGroup处理。
    • onTouchEvent:用于处理点击事件,在dispatchTouchEvent()中调用。

5、如何解决View的滑动冲突?

处理思路:

  • 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
  • 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View拦截事件何时由内部View拦截事件。
  • 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。

如何实现:

  • 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
  • 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:即在子容器的onTouchEvent方法里调用父容器的请求不允许拦截触摸事件的方法。

6、谈一谈View的工作原理?

View工作流程简单来说就是,先measure测量,用于确定View的测量宽高,再 layout布局,用于确定View的最终宽高四个顶点的位置,最后 draw绘制,用于将View 绘制到屏幕上。

View的绘制流程是从ViewRoot的PerformTraversals方法开始的,performTraversals会依次调用performMeasure, performLayout, performDraw三个方法,这三个方法分别完成顶层View的measure,layout,draw方法,**onMeasure又会调用所有子元素的measure过程,直到完成整个View树的遍历。**同理,performLayout, performDraw的传递流程与performMeasure相似。唯一不同在于,performDraw的传递过程在draw方法中通过dispatchDraw实现,但没有本质区别。

7、MeasureSpec是什么?有什么作用?

MeasureSpec 是一个32位的int 值,高2位代表SpecMode(测量模式),低30位代表SpecSize( 某种测量模式下的规格大小)。

测量模式分为三种

  • UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。
  • EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent具体数值
  • AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content

测量出来的值由子View的布局参数LayoutParams父容器的MeasureSpec值共同决定子View的布局参数
在这里插入图片描述

MeasureSpec的作用:通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小

8、自定义View/ViewGroup需要注意什么?

  • 让View支持wrap_content:因为直接继承View和ViewGroup的控件,如果不在onMeasure方法中对wrap_content做特殊处理,那么就无法达到预期效果
  • 让View支持padding:直接继承View的控件需要在onDraw方法中处理padding,否则用户设置padding属性就不会起作用。
  • 尽量不要在View中使用Handler:View中已经提供了post系列方法,完全可以替代Handler的作用。
  • View中如果有线程或者动画,需要及时停止:如果不处理的话很可能会导致内存泄漏
  • View带有滑动嵌套时,需要处理好滑动冲突问题

9、onTouch()、onTouchEvent()和onClick()关系?

这三个的执行顺序是 onTouch()->onTouchEvent()->onClick()

onTouchListener的onTouch()方法会先触发;如果onTouch()返回false才会接着触发onTouchEvent(),如果返回true,后面的事件也就不会执行。同样的onTouchEvent()也是如此。

10、SurfaceView和View的区别?

SurfaceView是View的子类,是一个适用于频繁刷新界面的View。

他和View 的区别有:

  • View 适用于主动更新的情况,而SurfaceView适用于被动更新的情况,比如说频繁刷新界面。
  • View 在UI主线程中对界面刷新,而SurfaceView 则开启一个子线程来对界面进行刷新。
  • View 在绘图时没有使用双缓冲机制,而SurfaceView在底层机制中实现了双缓冲机制。

双缓冲技术:当一个动画正在显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。

11、invalidate()和postInvalidate()的区别?

Invalidate() 和postInvalidate() 方法都是用来View的刷新,区别在于调用的方式不同,invalidate 只能在主线程中调用,所以在子线程中必须要配合hanlder使用,而postInvalidate() 在子线程中可以直接调用。

九、Drawable 等资源

1、了解哪些Drawable?适用场景?

  • ColorDrawable:表示单色图形
  • BitmapDrawable:表示位图图形
  • NinePatchDrawable:可自动地根据所需的宽/高对图片进行相应的缩放并保证不失真
  • ShapeDrawable:表示纯色、有渐变效果的基础几何图形
  • StateListDrawable:表示一个Drawable的集合且每个Drawable对应着View的一种状态
  • LayerDrawable:可通过将不同的Drawable放置在不同的层上面从而达到一种叠加后的效果

2、mipmap系列中xxxhdpi、xxhdpi、xhdpi、hdpi、mdpi和ldpi存在怎样的关系?

表示不同密度的图片资源,用于适配手机的像素图片显示,像素从高到低依次排序为xxxhdpi>xxhdpi>xhdpi>hdpi>mdpi>ldpi,根据手机的dpi不同加载不同密度的图片。

3、dp、dpi、px的区别?

  • px:像素,如分辨率1920x1080表示高为1920个像素、宽为1080个像素
  • dpi:每英寸的像素点,如分辨率为1920x1080的手机尺寸为4.95英寸,则该手机DPI为(1920x1920+ 1080x1080)½/4.95≈445dpi
  • dp:密度无关像素,是个相对值,在不同分辨率的手机上看起来位置差不多一值,但是像素却不一样

4、res目录和assets目录的区别?

  • res/raw中的文件会被映射到R.java文件中,访问时使用getResource(),可直接使用资源ID,不可以创建子文件夹
  • assets文件夹下的文件不会被映射到R.java中,访问时需要AssetManager类,可以创建子文件夹

十、动画 Animation

1、Android中有哪几种类型的动画?

安卓提供的动画主要分为两种:属性动画(Property Animation)和传统动画 (View Animation)。而View Animation又分为补间动画(TweenAnimation)和帧动画(FrameAnimation)。

  • 补间动画(TweenAnimation):对View进行平移、缩放、旋转和透明度变化的动画,使用简单却不具备交互性,不能真正的改变view的位置,动画发生后其响应事件的位置仍然在动画进行前的地方。
  • 帧动画(FrameAnimation):是按照顺序播放一组预先定义好的图片的动画
  • 属性动画(Property Animation):属性动画的底层只是一个数值发生器,和控件并没有关系,真正改变了对象的属性

2、帧动画在使用时需要注意什么?

使用祯动画要注意不能使用尺寸过大的图片,否则容易造成OOM

3、View动画和属性动画的区别?

View动画是通过不断的图形变换实现的,但他并不具备交互性,不能真正的改变view的位置,动画发生后其响应事件的位置仍然在动画进行前的地方。而属性动画是动态改变属性来实现的,真正的改变了View的位置。

4、View动画为何不能真正改变View的位置?而属性动画为何可以?

View动画改变的只是View的显示,而没有改变View的响应区域;而属性动画会通过反射技术来获取和执行属性的get、set方法,从而改变了对象位置的属性值。

4、属性动画插值器和估值器的作用?

  • 插值器(Interpolator):根据时间流逝的百分比计算出当前属性值改变的百分比。确定动画效果变化的模式,如匀速变化、加速变化等等。View动画和属性动画均可使用。常用的系统内置插值器:

    • 线性插值器(LinearInterpolator):匀速动画

    • 加速减速插值器(AccelerateDecelerateInterpolator):动画两头慢中间快

    • 减速插值器(DecelerateInterpolator):动画越来越慢

  • 类型估值器(TypeEvaluator):根据当前属性改变的百分比计算出改变后的属性值。只针对于属性动画,View动画不需要类型估值器。常用的系统内置的估值器:

    • 整形估值器(IntEvaluator)
    • 浮点型估值器(FloatEvaluator)
    • Color属性估值器(ArgbEvaluator)

十一、Window

1、Activity、View、Window三者之间的关系?

  • Activity是安卓四大组件之一,负责界面展示、用户交互与业务逻辑处理
  • Window就是负责界面展示以及交互的职能部门,就相当于Activity的下属,Activity的生命周期方法负责业务的处理
  • View就是放在Window容器的元素,Window是View的载体,View是Window的具体展示。

关系:Activity通过Window来实现视图元素的展示,window可以理解为一个容器,盛放着一个个的view,用来执行具体的展示工作

2、Window有哪几种类型?

  • 应用Window:对应一个Activity。
  • 子Window:不能单独存在,需附属特定的父Window。如Dialog。
  • 系统Window: 需申明权限才能创建。如Toast。

3、Activity创建和Dialog创建过程的异同?

  • 创建Dialog 的Window,跟Activity 的创建方式一样,都是根据new PhoneWindow()的方式创建的。
  • 初始化DecorView并将Dialog的视图添加到DecorView中去和Activity一样,都是根据Window.setContentView() 添加的。
  • 将DecorView添加到Window中显示,Dialog代码中要执行dialog.show() 方法,但添加的过程和Activity的Window添加过程一样,最终还是通过WindowManager去添加方法到WindowManagerService中, 我们的Window才真正的展示了出来。
  • 当Dialog被关闭时,Dialog代码中要执行dialog.dismiss() 方法,它会通过WindowManager来移除DecorView, 同样最终会通过WindowManagerService来进行移除工作, 毕竟WindowManagerService是所有Window的管理者。

十二、Hander详情

1、谈谈消息机制Hander?作用?有哪些要素?流程是怎样的?

Handler就是将消息放入队列的机制,用来实现跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。

  • Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
  • MessageQueue(消息队列):用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
  • Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
  • Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。

Hander.sendMessage()发送消息时,会通过MessageQueue.enqueneMessage()的方法向MessageQueue中插入一条消息,当Looper.loop()开启循环后,会不断的轮询调用MessageQueue.next()方法,取到消息队列中对头的消息后调用Handler.dispatchMessage()的方法去传递消息,当Handler 收到消息后调用handler.handlerMessage()方法来处理消息。

2、为什么系统不建议在子线程访问UI?

Android的UI控件并不是线程安全的,如果多线程中并发访问可能导致UI控件处于不可预期的状态.

那为什么系统不对UI控件的访问加上锁机制呢?

缺点有两个:首先,加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行.

3、一个Thread可以有几个Looper?几个Handler?

一个Thread只能有一个Looper,但可以有多个Handler。

4、如何将一个Thread线程变成Looper线程?Looper线程有哪些特点?

通过Looper.prepare()可将一个Thread线程转换成Looper线程。Looper线程和普通Thread不同,它通过MessageQueue来存放消息和事件、Looper.loop()进行消息轮询。

5、可以在子线程直接new一个Handler吗?那该怎么做?

可以创建,需要在子线程的new Handler之前添加Looper.prepare()方法,为子线程创建Looper,然后在Handler 之后调用Looper.loop() 方法,用来开启消息轮询。

6、Message可以如何创建?哪种效果更好,为什么?

Message msg = new Message();

Message msg = Message.obtain();

Message msg = handler1.obtainMessage();
后两种方法都是从整个Messge池中返回一个新的Message实例,能有效避免重复Message创建对象,因此更鼓励这种方式创建Message

7、ThreadLocal有什么作用?

线程局部变量,是一种多线程间并发访问变量的解决方案。与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程对象提供变量的独立副本,以保障线程安全。

8、主线程中Looper的轮询死循环为何没有阻塞主线程?

Looper这里的轮询死循环并非简单地死循环,无消息时也会休眠。真正会阻塞卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

9、使用Hanlder的postDealy()后消息队列会发生什么变化?

post delay的Message并不是先等待一定时间再放入到MessageQueue中,而是直接进入并阻塞当前线程,然后将其delay的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。此时,如果队头的Message正是被delay的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该Message,否则唤醒后直接执行。

十三、线程

1、Android中还了解哪些方便线程切换的类?

AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在子线程中进行UI操作。

HandlerThread:一种具有消息循环的线程,其内部可使用Handler。

IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。

2、AsyncTask相比Handler有什么优点?不足呢?

  • Handler机制存在的问题:多任务同时执行时不易精确控制线程。

  • 引入AsyncTask的好处:创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler实例就能完成相同的任务。

3、使用AsyncTask需要注意什么?

  • Task的实例必须在UI thread中创建
  • execute方法必须在UI thread中调用
  • 不要手动的调用onPreExecute(), onPostExecute(Result),doInBackground(Params…), onProgressUpdate(Progress…)这几个方法
  • 该task只能被执行一次,否则多次调用时将会出现异常

4、AsyncTask中使用的线程池大小?

  • SERIAL_EXECUTOR(同步线程池):用于任务的排队,默认是串行的线程池,核心线程数为5、线程池大小为128,同一时间只能处理一个任务
  • THREAD_POOL_EXECUTOR(异步线程池):其核心线程和线程池允许创建的数量由CPU的核数来计算出来,不过任务队列的容量为128。

5、HandlerThread有什么特点?

HandlerThread是一个线程类,它继承自Thread。与普通Thread不同,HandlerThread具有消息循环的效果,这是因为它内部HandlerThread.run()方法中有Looper,能通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环。

6、快速实现子线程使用Handler

  • 实例化一个HandlerThread对象,参数是该线程的名称;
  • 通过 HandlerThread.start()开启线程;
  • 实例化一个Handler并传入HandlerThread中的looper对象,使得与HandlerThread绑定;
  • 利用Handler即可执行异步任务;
  • 当不需要HandlerThread时,通过HandlerThread.quit()/quitSafely()方法来终止线程的执行。

7、IntentService的特点?

不同于线程,IntentService是服务,优先级比线程高,更不容易被系统杀死,因此较适合执行一些高优先级的后台任务;不同于普通Service,IntentService可自动创建子线程来执行任务,且任务执行完毕后自动退出

8、为何不用bindService方式创建IntentService?

IntentService的工作原理是,在IntentService的onCreate()里会创建一个HandlerThread,并利用其内部的Looper实例化一个ServiceHandler对象;而这个ServiceHandler用于处理消息的handleMessage()方法会去调用IntentService的onHandleIntent(),这也是为什么可在该方法中处理后台任务的逻辑;当有Intent任务请求时会把Intent封装到Message,然后ServiceHandler会把消息发送出,而发送消息是在onStartCommand()完成的,只能通过startService()才可走该生命周期方法,因此不能通过bindService创建IntentService。

9、线程池的好处、原理、类型?

好处:首先通过线程池中线程的重用,减少创建和销毁线程的性能开销。其次,能控制线程池中的并发数,否则会因为大量的线程争夺CPU资源造成阻塞。最后,线程池能够对线程进行管理,比如使用ScheduledThreadPool来设置延迟N秒后执行任务,并且每隔M秒循环执行一次。

**原理:**创建线程池需要ThreadPoolExecutor类,根据构造参数的不同可以配置各种各样的线程池。下面说下线程池的实现逻辑:

  • 如果当前线程池的数量小于CORE_POOL_SIZE(核心线程数),那么每来一个任务,都会创建一个线程去执行这个任务。
  • 如果当前线程池的数量大于CORE_POOL_SIZE,那么新来的任务就会放在workQueue(任务队列)中。如果workQueue里面的任务未满,新任务会等待刚才创建的线程空闲下来后将其取出来执行。如果workQueue里面的任务已满,并且MAXIMUM_POOL_SIZE(最大线程数)这个值大于CORE_POOL_SIZE(核心线程数),线程池就会创建新线程执行任务。
  • 如果workQueue的任务已满,并且线程池中的线程数也达到了MAXIMUM_POOL_SIZE,那么任务就会丢给rejectedExecutionHandler (拒绝策略)来处理。
  • 当线程池中的线程超过了CORE_POOL_SIZE时,如果某线程的空闲时间到了KEEP_ALIVE_TIME,那么就会自动销毁,直至线程池中的线程数目不大于corePoolSize,当允许为核心池中的线程设置存活时间时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 。

类型:

  • FixThreadPool:线程数量固定的线程池,所有线程都是核心线程,当线程空闲时不会被回收;能快速响应外界请求。
  • SingleThreadExecutor:只有一个核心线程,可确保所有的任务都在同一个线程中按顺序执行;好处是无需处理线程同步问题。
  • CachedThreadPool:线程数量不定的线程池(最大线程数为Integer.MAX_VALUE),只有非核心线程,空闲线程有超时机制,超时回收;适合于执行大量的耗时较少的任务
  • ScheduledThreadPool:核心线程数量固定,非核心线程数量不定;可进行定时任务和固定周期的任务。

尽量不要使用Executors去创建线程池,因为在阿里开发手册中说明了Executors各个方法的弊端

  • newFixedThreadPool 和 newSingleThreadExecutor:
    主要问题是就只有一到多个的核心线程,堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:
    主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

10、什么是ANR?什么情况会出现ANR?如何避免?在不看代码的情况下如何快速定位出现ANR问题所在?

  • ANR(Application Not Responding,应用无响应):当操作在一段时间内系统无法处理时,会在系统层面会弹出ANR对话框
  • 产生ANR可能是因为5s内无响应用户输入事件、10s内未结束BroadcastReceiver、20s内未结束Service
  • 想要避免ANR就不要在主线程做耗时操作,而是通过开子线程,方法比如继承Thread或实现Runnable接口、使用AsyncTask、IntentService、HandlerThread等

十四、Bitmap

1、加载图片的时候需要注意什么?

  • 当图片资源过大时,可以采用 **BitmapFactory.Options()**按一定的采样率来加载所需尺寸的图片。
  • 为避免多次下载显示图片,可对图片采用内存缓存策略,又为了避免图片占用过多内存导致内存溢出,最好以软引用方式持有图片
  • 当从网上下载图片时,开启子线程进行下载的耗时操作。

2、LRU算法?

缓存算法 LRU(Least Recently Used):当缓存满时, 会优先淘汰那些近期最少使用的缓存对象。

主要是两种方式:

  • LruCache(内存缓存):LruCache类是一个线程安全的泛型类:内部采用一个LinkedHashMap强引用的方式存储外界的缓存对象,并提供getput方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。
  • DiskLruCache(磁盘缓存): 通过将缓存对象写入文件系统从而实现缓存效果

十五、内存优化

1、项目中如何做性能优化的?

  • 布局优化:尽量减少布局文件的层级 比如说在布局多层嵌套的时候尽量使用Relativelayout,在层级相同的情况下使用LinearLayout布局,使用<include>标签重用布局、<merge>标签减少层级、<ViewStub>标签懒加载。
  • 绘制优化:避免在View.onDraw()方法中执行大量的操作,比如说避免创建局部对象以及耗时操作,因为在onDraw()方法在自定义控件时可能会被多次调用。
  • 内存泄漏优化:程序申请内存后,无法释法已经申请的内存,是导致内存溢出(OOM)的主要原因之一。
    • 静态变量导致的内存泄漏:静态变量引用了或者内部持有Activity导致Activity无法销毁会导致内存泄露
    • 单例模式导致的内存泄漏:单例传入参数this来自Activity,使得持有对Activity的引用。
    • 属性动画导致的内存泄漏:没有在onDestroy()中停止无限循环的属性动画,使得View持有了Activity。
    • Handler导致的内存泄漏:在Java中,非静态内部类 & 匿名内部类都默认持有外部类的引用。Message持有对Handler的引用,而非静态内部类的Handler又隐式持有对外部类Activity的引用,使得引用关系会保持至消息得到处理,从而阻止了Activity的回收。可以使用静态内部类+WeakReference弱引用或者当外部类结束生命周期时清空消息队列。
    • 线程导致的内存泄漏:AsyncTask/Runnable以匿名内部类的方式存在,会隐式持有对所在Activity的引用。可以将AsyncTask和Runnable设为静态内部类或独立出来;在线程内部采用弱引用保存Context引用
    • 资源未关闭导致的内存泄漏:在Activity销毁的时候要及时关闭或者注销。
  • 响应速度优化:耗时操作开启一个子线程进行。
  • ListView优化:复用ViewHolder,不要在getView()中执行耗时操作
  • Bitmap优化:对图片进行采样率加载以及内存缓存处理
  • 线程优化:当创建多个线程时,采用线程池的方法。

2、了解哪些性能优化的工具?

3、内存泄漏和内存溢出的区别

  • 内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。是造成应用程序OOM的主要原因之一。
  • 内存溢出(out of memory)是指程序在申请内存时,没有足够的内存空间供其使用。内存泄漏是导致内存溢出的主要原因;直接加载大图片也易造成内存溢出

JAVA 问题

一、java 基础

1、java的基本数据类型有哪些?长度是多少?能存放那些数据?

六种数字类型(四个整数型(默认是int 型),两个浮点型(默认是double 型)),一种字符类型,还有一种布尔型。

  • byte: 数据类型是8位、有符号的以二进制补码表示的整数。占1字节,范围是( -128到127)

  • **short :**数据类型是16位、有符号的以二进制补码表示的整数。占2字节 范围是(-2^15 到 2^15-1)

  • **int :**数据类型是32位、有符号的以二进制补码表示的整数 。占4个字节,范围是(-2^31 到 2^31-1)

  • **long :**数据类型是64位、有符号的以二进制补码表示的整数。占8个字节,范围是(-263到263-1)

  • **float :**数据类型是单精度、32位、符合IEEE 754标准的浮点数。占4字节 float的精度为6~7位有效数字

  • **double : **数据类型是双精度、64位、符合IEEE 754标准的浮点数。占8字节 double的精度为15~16位有

    效数字

  • **char:**类型是一个单一的 16 位 Unicode 字符;占2字节 ,可以储存任何字符;

2、面向对象的四大特性及其含义?

抽象 封装 继承 多态

  • **抽象 abstract :**找出某些对象共有的一些特点,然后归到一个类里面,这个类的创建只考虑这些对象的相似之处。
  • **继承 extends :**继承是子类共享父类方法和数据,并且子类可以重写、新增方法的一种思想, 提高了代码的可重用性和拓展性;
  • **封装 :**将某事物的属性和行为包装到对象中,构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,并且尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系
  • **多态 : **多态就是同一个接口,使用不同的实例而执行不同操作

3、String、StringBuffer和StringBuilder的区别?

  • String是字符串常量,而StringBuffer、StringBuilder都是字符串变量,即String对象一创建后不可更改,而后两者的对象是可更改的
  • StringBuffer是线程安全的,而StringBuilder是非线程安全的,这是由于StringBuffer对方法加了同步锁或者对调用的方法加了同步锁
  • String更适用于少量的字符串操作的情况,StringBuilder适用于单线程下在字符缓冲区进行大量操作的情况,StringBuffer适用于多线程下在字符缓冲区进行大量操作的情况

4、String a="“和String a=new String(”")的的关系和异同?

  • String a="" 是指如果内存中有这个字符串,就指向这个字符串,如果没有,则创建它。
  • String a=new String("") 是根据这个字符串再次构造一个String对象,将新构造出来的String对象的引用赋给str,每次都会创建一个新的。

5、Object的equal()和==的区别?

  • 是Object的公有方法,具体含义取决于如何重写,比如String的equals()比较的是两个字符串的内容是否相同,如果不重写的话比较的是两个对象的内存地址
  • == 比较的是这两个对象的内存地址是否一样

6、什么是装箱、拆箱?

装箱是值类型转成object引用类型,拆箱是已被装箱的引用类型转成原来的值类型。

7、int和Integer的区别?

  • Integer是int的包装类,int则是java的一种基本数据类型
  • Integer变量必须实例化后才能使用,而int变量不需要
  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值
  • Integer的默认值是null,int的默认值是0

8、遇见过哪些异常?异常处理机制知道哪些?

Android中常见的两个异常:

  • 运行时异常:由程序自身的问题导致产生的异常;如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常);属于不可查异常。

  • 非运行时异常:是RuntimeException以外的异常,如ClassNotFoundException(类没找到异常)

    属于可查异常,即强制程序员必须进行处理,如果不进行处理则会出现语法错误。

常见的异常处理机制有:

  • 捕捉异常:由系统自动抛出异常,即try捕获异常->catch处理异常->finally 最终处理
  • 抛出异常:在方法中将异常对象显性地抛出,之后异常会沿着调用层次向上抛出,交由调用它的方法来处理。配合throws声明抛出的异常和throw抛出异常
  • 自定义异常:继承Execption类或其子类

9、什么是反射,有什么作用和应用?

**反射:**在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任何一个对象都能够调用它的任何一个方法和属性,这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

作用: 创建对象、操作属性、调用方法

10、什么是内部类?有什么作用?静态内部类和非静态内部类的区别?

**内部类:**内部类就是定义在另外一个类里面的类。

**作用:**它隐藏在外部类中,封装性更强,不允许除外部类外的其他类访问它;但它可直接访问外部类的成员。

静态内部类和非静态内部类的区别:

  • 静态内部类是指被声明为static的内部类,可不依赖外部类实例化;而非静态内部类需要通过生成外部类来间接生成。
  • 静态内部类只能访问外部类的静态成员变量和静态方法,而非静态内部类由于持有对外部类的引用,可以访问外部类的所有成员

11、final、finally、finalize()分别表示什么含义

**final:**表示不可更改,具体体现在:

  • final修饰的变量必须要初始化,且赋初值后不能再重新赋值
  • final修饰的方法不能被子类重写
  • final修饰的类不能被继承

**finally:**finally是在Java异常处理时用到的,在try ,catch之后执行,不管有没有捕获到异常最后的finally方法会得到执行

**finalize():**在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。

12、重写和重载的区别?

重写表示子类重写父类的方法;重载表示函数的方法名可以一样,但区别在于可以有不同的参数个数或类型

13、抽象类和接口的异同?

  • 使用上的区别:一个类只能继承一个抽象类却可以实现多个接口
  • 设计上的区别:接口是对行为的抽象,无需有子类的前提,是自上而下的设计理念;抽象类是对类的抽象,建立于相似子类之上,是自下而上的设计理念

####14、 为什么匿名内部类中使用局部变量要用final修饰?

为了保证在内部类中能找到外部局部变量,通过final关键字可得到一个外部变量的引用,而且通过final关键字也不会在内部类去做修改该变量的值,保护了数据的一致性。

15、Object有哪些公有方法?

  • equals(): 和==作用相似
  • hashCode():用于哈希查找,重写了equals()一般都要重写该方法
  • getClass(): 获取Class对象
  • toString():转换成字符串
  • wait():让当前线程进入等待状态,并释放它所持有的锁
  • notify()&notifyAll(): 唤醒一个(所有)正处于等待状态的线程

二、集合

1、Java集合框架中有哪些类?都有什么特点

可将Java集合框架大致可分为Set、List、Queue 和Map四种体系

  • Set:代表无序、不可重复的集合,常见的类如HashSet、TreeSet
  • List:代表有序、可重复的集合,常见的类如动态数组ArrayList、双向链表LinkedList、可变数组Vector
  • Map:代表具有映射关系的集合,常见的类如HashMap、LinkedHashMap、TreeMap
  • Queue:代表一种队列集合

2、集合、数组、泛型的关系,并比较

  • 数组元素可以是基本类型,也可以是对象;数组长度限定;数组只能存储一种类型的数据元素
  • 集合元素只能是对象;集合长度可变;集合可存储不同种的数
  • 泛型相比与集合的好处在于它安全简单。具体体现在提供编译时的强类型检查,而不用等到运行;可避免类类型强制转换

3、ArrayList和LinkList的区别?

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。
  2. 对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

4、ArrayList和Vector的区别?

1、Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
2、 当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。

5、HashSet和TreeSet的区别?

HashSet不能保证元素的排列顺序;使用Hash算法来存储集合中的元素,有良好的存取和查找性能;通过equal()判断两个元素是否相等,并两个元素的hashCode()返回值也相等

TreeSet是SortedSet接口的实现类,根据元素实际值的大小进行排序;采用红黑树的数据结构来存储集合元素;支持两种排序方法:自然排序(默认情况)和定制排序。前者通过实现Comparable接口中的compareTo()比较两个元素之间大小关系,然后按升序排列;后者通过实现Comparator接口中的compare()比较两个元素之间大小关系,实现定制排列

6、HashMap和Hashtable的区别?

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口

  • HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
  • HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。
  • 由于HashMap非线程安全,在只有一个线程访问的情况下,效率要高于HashTable。
  • Hashtable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

7、HashMap在put、get元素的过程?体现了什么数据结构?

Put 过程:向Hashmap中put元素时,首先判断key是否为空,为空则直接调用putForNullKey(),不为空则计算key的hash值得到该元素在数组中的下标值;如果数组在该位置处没有元素,就直接保存;如果有,还要比较是否存在相同的key,存在的话就覆盖原来key的value,否则将该元素保存在链头,先保存的在链尾。

Get 过程:从Hashmap中get元素时,计算key的hash值找到在数组中的对应的下标值,返回该key对应的value即可,如果有冲突就遍历该位置链表寻找key相同的元素并返回对应的value

HashMap采用链表散列的数据结构,即数组和链表的结合,在Java8后又结合了红黑树,当链表元素超过8个将链表转换为红黑树

HashMap是用哈希表(直接一点可以说数组加单链表)+红黑树实现的map类。

补充:链表散列,由数组+链表组成的。数组里每个元素存储的是一个链表的头结点。而组成链表的结点其实就是hashmap内部定义的一个类:Entity。Entity包含三个元素:key,value和指向下一个Entity的next

8、如何解决Hash冲突?

Hash法(散列法):首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得p=f(k),f称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元;以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=f(k),从而达到按关键字直接存取元素的目的。

**Hash 冲突 :**当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,即 k1≠k2 ,但 H(k1)=H(k2),这种现象称为冲突,此时称k1和k2为同义词。

解决的办法:

  • 开放定址法:这种方法也称**再散列法,**其基本思想是:当关键字key的哈希地址p=Hkey)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
  • 再哈希法: 这种方法是同时构造多个不同的哈希函数,当哈希地址Hi=RH1key)发生冲突时,再计算Hi=RH2key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
  • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,和基本表发生冲突的元素一律填入溢出表
  • 链地址法:将有冲突数组位置生出链表

10、如何保证HashMap线程安全?什么原理?

使用ConcurrentHashMap,它使用分段锁来保证线程安全

ConcurrentHashMap是线程安全的HashMap,它采取锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

11、HashMap是有序的吗?如何实现有序?

HashMap是无序的,而LinkedHashMap是有序的HashMap,默认为插入顺序,还可以是访问顺序,基本原理是其内部通过Entry维护了一个双向链表,负责维护Map的迭代顺序

12、HashMap是如何扩容的?如何避免扩容?

HashMap几个默认值,初始容量为16、填充因子默认为0.75、扩容时容量翻倍。也就是说当HashMap中元素个数超过16*0.75=12时会把数组的大小扩展为2*16=32,然后重新计算每个元素在数组中的位置

由于每次扩容还需要重新计算元素Hash值,损耗性能,所以建议在使用HashMap时,最好先估算Map的大小,设置初始值,避免频繁扩容

13、hashcode()的作用,与equal()有什么区别?

hashCode()用于计算对象的Hash值,确认对象在散列存储结构中的存储地址。

equals()比较两个对象的地址值是否相等 ;hashCode()得到的是对象的存储位置,可能不同对象会得到相同值。要记住的一点是如果若equals()相等,则hashcode()一定相等,而hashcode()不等,则equals()一定不相等;

三、并发

1、同步和非同步、阻塞和非阻塞的概念

  • 同步和异步体现的是消息的通知机制:所谓同步,方法A调用方法B后必须等到方法B返回结果才能继续后面的操作;所谓异步,方法A调用方法B后只需要让方法B在调用结束后通过回调等方式通知方法A,这时候A可以继续执行的后面的任务。

  • 阻塞和非阻塞侧重于等待消息时的状态:所谓阻塞,就是在结果返回之前让当前线程挂起;所谓非阻塞,就是在等待时可做其他事情,通过轮询去询问是否已返回结果

2、Thread的join()有什么作用?

让父线程等待子线程结束之后才能继续运行。比如说有两个线程t1和t2,先启动t1,然后执行t1.join()方法,再启动t2,执行的结果是 t2线程在t1 线程执行完之后才开始执行。

3、线程有哪些状态?

  • 新建状态:当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码。
  • **就绪状态:**当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
  • **运行状态 (running):**当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
  • **阻塞状态(blocked):**是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
  • **死亡状态:**线程已经结束执行

4、什么是线程安全?保障线程安全有哪些手段?

线程安全就是当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

保证线程安全可从多线程三特性出发:

  • 原子性(Atomicity):单个或多个操作是要么全部执行,要么都不执行
    • Lock:保证同时只有一个线程能拿到锁,并执行申请锁和释放锁的代码
    • synchronized:对线程加独占锁,被它修饰的类/方法/变量只允许一个线程访问
  • 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
    • volatile:保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;
    • synchronized:在释放锁之前会将工作内存新值更新到主存中
  • 有序性(Ordering):程序代码按照指令顺序执行
    • volatile: 本身就包含了禁止指令重排序的语义
    • synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入

5、ReentrantLock和synchronized的区别?

  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

  • 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。而synchronized是非公平的,即在锁被释放时,任何一个等待锁的线程都有机会获得锁。ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数改用公平锁。

  • 锁绑定多个条件:一个ReentrantLock对象可以通过多次调用newCondition()同时绑定多个Condition对象。而在synchronized中,锁对象wait()和notify()或notifyAl()只能实现一个隐含的条件,若要和多于一个的条件关联不得不额外地添加一个锁。

6、synchronized 和volatile的区别?

synchronized :可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码

volatile : 它能够使变量在值发生改变时能尽快地让其他线程知道。

  • synchronized能保证操作的原子性,而volatile不可以,假设线程A和线程B同时读取到变量a值,A修改a后将值更新到主内存,同时B也修改a值会覆盖A的修改操作

  • synchronized可修饰变量、方法和类,而volatile只能修饰变量

  • synchronized可能会造成线程阻塞,而volatile不会造成线程的阻塞

7、synchronized同步代码块还有同步方法本质上锁住的是谁?为什么?

本质上锁住的是对象。在java虚拟机中,每个对象和类在逻辑上都和一个监视器相关联,synchronized本质上是对一个对象监视器的获取。当执行同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器,才能进入同步代码块或同步方法;而没有获取到的线程将会进入阻塞队列,直到成功获取对象监视器的线程执行结束并释放锁后,才会唤醒阻塞队列的线程,使其重新尝试对对象监视器的获取。

8、sleep()和wait()的区别?

sleep()来自Thread类;wait()来自Object类

sleep()用于线程控制自身流程;而wait()用于线程间通信,配合notify()/notifyAll()在同步代码块或同步方法里使用

sleep()的线程不会释放对象锁;wait()会释放对象锁进入等待状态,使得其他线程能使用同步代码块或同步方法

计算机网络

一、基础

####1、五层协议的体系结构分别是什么?每一层都有哪些协议?

物理层

数据链路层

网络层 IP 协议

传输层 TCP/UDP 协议

应用层 HTTP 协议

2、IP 协议

IP 协议又称为互联网协议,是用来分组交换数据的一种协议,因为IP协议定义了寻址方法和数据报的封装结构,所以可以根据源主机和目的主机的地址来进行数据传输。

3、TCP 协议

TCP 协议即为传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议是双向通信的,连接前有三次握手,断开连接也有四次挥手,所以它能保证数据按序发送,按序接受。

引申三次握手以及四次挥手

4、UDP 协议

UDP协议是用户数据报协议,也称之为用户数据报文协议,是一种简单的面向数据报的传输层协议。

UDP 协议没有 TCP 协议稳定,因为它不建立连接,也不按顺序发送,可能会出现丢包现象,使传输的数据出错。但是有得就有失,UDP 的效率更高,因为 UDP 头包含很少的字节,比 TCP 负载消耗少,同时也可以实现双向通信,不管消息送达的准确率,只负责无脑发送。 UDP 一般多用于 IP 电话、网络视频等容错率强的场景。

5、Socket

Socket 被称为“套接字”,作为应用层和传输层之间的桥梁,它把复杂的 TCP/IP 协议簇隐藏在背后,为用户提供简单的客户端到服务端接口,让我们感觉这边输入数据,那边就直接收到了数据,像一个“管道”一样。

Socket 的使用

要实现客户端与服务端的通信,双方都需要实例化一个 Socket。

在客户端实现:

  1. 连接远程机器 根据服务端的IP地址和端口号连接服务端。 调用 Socket() 创建一个流套接字,连接到服务端
  2. 发送数据 调用 Socket 类的 getOutputStream() 和 getInputStream() 获取输出和输入流,进行网络数据的收发
  3. 接收数据
  4. 关闭连接 关闭套接字

在服务端实现:

  1. 绑定端口 调用 ServerSocket(int port) 创建一个 ServerSocket,绑定到指定端口,调用 accept() 监听连接请求,如果客户端请求连接则接受,返回通信套接字
  2. 监听到达数据
  3. 在绑定的端口上接受来自远程机器的连接 调用 Socket 类的 getOutputStream() 和 getInputStream() 获取输出和输入流,进行网络数据的收发
  4. 关闭套接字

6、HTTP协议

HTTP 协议是指 超文本传输协议,HTTP协议是基于TCP/IP 的应用层网络协议,他并不涉及到数据包的传输,而是规定了客户端和服务端的通信格式。

HTTPS 协议是指 超文本传输安全协议,是一种通过计算机网络进行安全通信的传输协议。实际上HTTPS 是由HTTP进行的通信,但由但利用SSL/TLS来加密数据包的方式。

HTTP是明文传输,默认端口号是80,https是ssl加密的传输,身份认证的网络协议,默认端口号是443

7、状态码:

1xx:表示服务器已接收了客户端请求,客户端可继续发送请求

2xx:表示服务器已成功接收到请求并进行处理

  • 200 OK:表示客户端请求成功

3xx:表示服务器要求客户端重定向

4xx:表示客户端的请求有非法内容

  • 400 Bad Request:表示客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthonzed:表示请求未经授权,该状态代码必须与 WWW-Authenticate 报头域一起使用
  • 403 Forbidden:表示服务器收到请求,但是拒绝提供服务,通常会在响应正文中给出不提供服务的原因
  • 404 Not Found:请求的资源不存在,例如,输入了错误的URL

5xx:表示服务器未能正常处理客户端的请求而出现意外错误

  • 500 Internal Server Error:表示服务器发生不可预期的错误,导致无法完成客户端的请求
  • 503 Service Unavailable:表示服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常

8、get和post的区别?

GET:当客户端要从服务器中读取某个资源时使用GET;一般用于获取/查询资源信息;GET参数通过URL传递,传递的参数是有长度限制,不能用来传递敏感信息

POST:当客户端给服务器提供信息较多时可以使用POST;POST会附带用户数据,一般用于更新资源信息;POST将请求参数封装在HTTP 请求数据中,可以传输大量数据,传参方式比GET更安全

9、在地址栏打入URL会发生什么?

浏览器向DNS服务器请求解析该URL中的域名所对应的IP地址

解析出IP地址后,根据该IP地址和默认端口80,和服务器建立TCP连接

浏览器发出读取文件的HTTP请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器

服务器对浏览器请求作出响应,并把对应的html文本发送给浏览器

释放TCP连接,若connection模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求

客户端将服务器响应的html文本解析并显示

10、网络请求框架:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttMdq7xz-1587544361693)(/Users/smile/Desktop/sxl/%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82%E6%A1%86%E6%9E%B6.png)]

11、HTTP1.0、HTTP1.1、HTTP2.0的区别?

HTTP1.0和HTTP1.1的区别:

  • HTTP1.0默认使用短连接,HTTP1.1开始默认使用长连接
  • HTTP1.1增加更多的请求头和响应头来改进和扩充HTTP1.0的功能,比如身份认证、状态管理和Cache缓存等

HTTP2.0和HTTP1.X相比的新特性:

  • 新的二进制格式:HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮,不同于HTTP1.x的解析是基于文本
  • 多路复用:连接共享,即每一个request都是是用作连接共享机制的
  • 服务端推送:服务器主动向客户端推送消息

JVM

JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

1、JVM内存是如何划分的?

JVM会用一段空间来存储执行程序期间需要用到的数据和相关信息,这段空间就是运行时数据区(Runtime Data Area),也就是常说的JVM内存。JVM会将它所管理的内存划分为线程私有数据区线程共享数据区两大类:

线程私有数据区包含:

  • 程序计数器:是当前线程所执行的字节码的行号指示器
  • 虚拟机栈:是为虚拟机执行 Java方法 服务
  • 本地方法栈:是虚拟机使用到的Native方法服务

线程共享数据区包含:

  • Java堆:用于存放几乎所有的对象实例和数组;是垃圾收集器管理的主要区域,也被称做“GC堆”;是Java虚拟机所管理的内存中最大的一块
  • 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

2、谈谈垃圾回收机制?为什么引用计数器判定对象是否回收不可行?知道哪些垃圾回收算法?

垃圾回收机制(GC): Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。

判定对象可回收有两种方法:

  • 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。然而在主流的Java虚拟机里未选用引用计数算法来管理内存,主要原因是它难以解决对象之间相互循环引用的问题,所以出现了另一种对象存活判定算法。
  • 可达性分析法:通过一系列被称为『GC Roots』的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。其中可作为GC Roots的对象:虚拟机栈中引用的对象,主要是指栈帧中的本地变量、本地方法栈中Native方法引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象

四种垃圾收集算法:

分代收集算法:是当前商业虚拟机都采用的一种算法,根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。

  • 新生代:大批对象死去,只有少量存活。使用『复制算法』,只需复制少量存活对象即可。
    • 复制算法:把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象『复制』到另外一块上面,再将这一块内存空间一次清理掉。
  • 老年代:对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。
    • 标记-清除算法:首先『标记』出所有需要回收的对象,然后统一『清除』所有被标记的对象。
    • 标记-整理算法:首先『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。

3、Java中引用有几种类型?在Android中常用于什么情景?

  • 强引用(StrongReference):具有强引用的对象不会被GC;即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象。

  • 软引用(SoftReference):只具有软引用的对象,会在内存空间不足的时候被GC;软引用常用来实现内存敏感的高速缓存

  • 弱引用(WeakReference):无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。

  • 虚引用(PhantomReference):仅持有虚引用的对象,在任何时候都可能被GC;常用于跟踪对象被GC回收的活动;

4、类加载的全过程是怎样的?什么是双亲委派模型?

加载、验证、准备、解析、初始化

  • 加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口

    (将java源代码编译后的.class字节码文件以二进制流的方式加载进内存)

  • 验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证

  • 准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值

  • 解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程

  • 初始化(Initialization):是类加载过程的最后一步,会开始真正执行去执行类初始化的代码逻辑。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制

双亲委派模型:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

双亲委派模型的优点:可以避免类的重复加载,并且可以防止核心API库被随意篡改。

5、工作内存和主内存的关系?在Java内存模型有哪些可以保证并发过程的原子性、可见性和有序性的措施?

java 的内存模型分为主内存工作内存所有线程共享主内存,每个线程都有自己的工作内存,不是共享的。一个线程不能访问另一个线程的工作内存。线程之间通过主内存来实现线程兼间的通信。

  • 原子性(Atomicity):一个操作要么都执行要么都不执行。

    • 可直接保证的原子性变量操作有:readloadassignusestorewrite,因此可认为基本数据类型的访问读写是具备原子性的。
    • 若需要保证更大范围的原子性,可通过更高层次的字节码指令monitorentermonitorexit来隐式地使用lockunlock这两个操作,反映到Java代码中就是同步代码块synchronized关键字。
  • 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

    • 通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现。
    • 提供三个关键字保证可见性:volatile能保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;synchronized对一个变量执行unlock操作之前可以先把此变量同步回主内存中;被final修饰的字段在构造器中一旦初始化完成且构造器没有把this的引用传递出去,就可以在其他线程中就能看见final字段的值。
  • 有序性(Ordering):程序代码按照指令顺序执行。

    • 如果在本线程内观察,所有的操作都是有序的,指“线程内表现为串行的语义”;如果在一个线程中观察另一个线程,所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
    • 提供两个关键字保证有序性:volatile 本身就包含了禁止指令重排序的语义;synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。

6、JVM、Dalvik、ART的区别?

  • JVM(Java Virtual Machine): 编译的是.class文件,基于栈的结构

  • dalvik虚拟机:是Google厂商合作开发的针对Android的虚拟机,编译的是dex文件,基于寄存器结构,相对于JVM更精简,运行效率更高。

  • ART(Android Runtime):Android 5.0 L版本 2014年开始应用,采用AOT(Ahead of Time)预编译,在第一次安装时会将字节码转换成机器码。需要更大的存储空间,首次安装慢一下,以后的每次运行会更快,性能提升,电池续航更好 。

7、Java中堆和栈的区别?

在java中,堆和栈都是内存中存放数据的地方,区别在于:

  • 栈内存:主要用来存放基本数据类型局部变量;当在代码块定义一个变量时会在栈中为这个变量分配内存空间,当超过变量的作用域后这块空间就会被自动释放掉。
  • 堆内存:用来存放运行时创建的对象,比如通过new关键字创建出来的对象和数组;需要由Java虚拟机的自动垃圾回收器来管理。

操作系统

一、Android系统的架构体系

从高到低分别是Android应用层,Android应用框架层,Android系统运行层和Linux内核层。

应用层

所有安装在手机上的应用程序都属于这一层。Android系统将会包含系列的核心应用程序,这些程序包括电子邮件客户端、SMS程序、日历、地图、浏览器、联系人等。这些应用程序都是使用Java编写的。

应用框架层

Android应用程序框架层提供了大量的API供开发者使用,在开发Android应用程序时,就是面向底层的应用程序框架进行的

系统运行库层

这一层通过一些C/C++库来为Android系统提供了主要的特性支持。此层中还有Android运行时库,它主要提供一些核心库,来允许开发者使用Java语言来编写Android应用。因此可以将此层看作由提供Android系统特性的函数库Android运行时库两部分组成,

Linux内核层

这一层为Android设备的各种硬件提供了底层的驱动(Linux内核提供了安全性、内存管理、进程管理、网络协议和驱动模型等核心系统服务)。

Android 运行机制

Android运行时库中包含了Dalvik虚拟机(5.0后改为ART运行环境),它使得每一个Android应用都能运行在独立的进程当中,并且拥有一个自己的Dalvik虚拟机实例。

二、其他问题

1、操作系统中进程和线程的区别?

  • 进程是操作系统分配和管理资源的单位,线程是CPU调度和管理的单位,是CPU调度的最小单元
  • 进程拥有独立的地址空间,而线程间共享地址空间
  • 进程创建的开销比较大,线程创建的开销小

2、进程死锁的产生和避免?

死锁是指多个进程因循环等待资源而造成无法执行的现象,它会造成进程无法执行,同时会造成系统资源的极大浪费。

  • 死锁产生的条件:
    • 互斥使用:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    • 不可抢占:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    • 请求和保持:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    • 循环等待:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
  • 解决死锁的策略:
    • 银行家算法:判断此次请求是否造成死锁若会造成死锁,否则拒绝该请求
    • 鸵鸟算法:忽略该问题,常用于在极少发生死锁的的情况
  • 死锁的避免:通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,即“如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配”

设计模式

基本问题

1、MVC、MVP和MVVM 都分别是什么?

MVC:模型(Model)、视图(View)和控制器(Controller)

  • 模型(Model):专门用来处理业务逻辑,如数据库相关的操作、文件的访问和数据结构等。
  • 视图(View): 专注页面布局和数据显示,如界面的显示
  • 控制器(Controller):连接模型和视图,如把视图的请求发送给模型

MVC实现了视图和模型的分离,避免了视图和模型糅合在一起,当视图改变的时候只要业务逻辑没变不需要改变模型;但是它有一个缺点缺点是因为MVC中的控制器并不能直接更新视图,所以MVC并不能实现视图和模型的完全分离,视图依然依赖模型的数据(数据结构)来显示,也就是说视图依赖模型。

MVP: 模型(Model)、视图(View)和展示器(Presenter)

  • 模型(Model):专门用来处理业务逻辑,如数据库相关的操作、文件的访问和数据结构等。

  • 视图(View):专注页面布局和数据显示,如界面的显示

  • 展示器(Presenter):连接模型和视图,处理视图的请求并根据模型更新视图,将模型和视图进行分离了,所有的交互都发生在Persenter 内部。

MVP用展示器代替了控制器,而展示器是可以直接更新视图,所以MVP中展示器可以处理视图的请求并递送到模型又可以根据模型的变化更新视图,实现了视图和模型的完全分离。

MVVM:模型(Model)、视图(View)和视图模型(ViewModel)

  • 模型(Model):专门用来处理业务逻辑,如数据库相关的操作、文件的访问和数据结构等。

  • 视图(View):专注页面布局和数据显示,如界面的显示

  • 视图模型(ViewModel):连接模型和视图,也是将模型和视图进行了分离,但视图模型和视图是双向绑定的,当视图发生变化的时候视图模型也会发生改变,当视图模型变化的时候视图也随之变化。

MVVM用视图模型代替了MVP中的展示器,视图模型和视图实现了双向绑定,当视图发生变化的时候视图模型也会发生改变,当视图模型变化的时候视图也随之变化。

2、MVC、MVP和MVVM好在哪里,不好在哪里?

MVC的实现了视图和模型的分离,避免了视图和模型糅合在一起,当视图改变的时候只要业务逻辑没变不需要改变模型;但是它有一个缺点缺点是因为MVC中的控制器并不能直接更新视图,所以MVC并不能实现视图和模型的完全分离,视图依然依赖模型的数据(数据结构)来显示,也就是说视图依赖模型。

MVP相比于MVC的优势:

  • 分离了视图逻辑和业务逻辑,降低了耦合。
  • Activity只处理生命周期的任务,代码变得更加简洁
  • 视图逻辑和业务逻辑分别抽象到了View和Presenter的接口中去,提高代码的可阅读性。
  • Presenter被抽象成接口,可以有多种具体的实现,所以方便进行单元测试
  • 把业务逻辑抽到Presenter中去,避免后台线程引用着Activity导致Activity的资源无法被系统回收从而引起内存泄露和OOM。

MVVM相比于MVP的优势:在常规的开发模式中,数据变化需要更新HUI的时候,需要先获取UI控件的引用,然后再更新UI,获取用户的输入和操作也需要通过UI控件的引用,但在MVVM中,这些都是通过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层,数据成为主导因素。这样MVVM层在业务逻辑处理中只要关心数据,不需要直接和UI打交道,在业务处理过程中简单方便很多。

3、如何理解生产者消费者模型?

某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。在生产者与消费者之间在加个缓冲区,我们形象的称之为仓库,生产者负责往仓库了进商品,而消费者负责从仓库里拿商品,这就构成了生产者消费者模型。

具体规则:生产者只在缓存区未满时进行生产,缓存区满时生产者进程被阻塞;消费者只在缓存区非空时进行消费,缓存区为空时消费者进程被阻塞;当消费者发现缓存区为空时会通知生产者生产;当生产者发现缓存区满时会通知消费者消费。

编程如何实现?

1、首先创建一个自定义仓库类,用来实现缓存区

public class Storage {

    // 仓库最大存储的数量为100
    private final int MAX_SIZE = 100;

    // 仓库存储的载体
    private LinkedList<Object> list = new LinkedList<Object>();
    
    public void add(int num) {
        synchronized (list) {
            while (list.size() + num > MAX_SIZE) {
                System.out.println("当前仓库已存放" + list.size() + ",要存放的数量为" + num + ",超出了仓库的存储量,不能够进行存储");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int i = 0; i < num; i++) {
                list.add(new Object());
            }

            System.out.println("要存放的数量为" + num + ",当前仓库已存放" + list.size());
            list.notifyAll();
        }
    }

    public void delete(int num) {
        synchronized (list) {
            while (list.size() < num) {
                System.out.println("当前仓库已存放" + list.size() + ",要取走的数量为" + num + ",仓库的存量不够,不能够进行取走");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int i = 0; i < num; i++) {
                list.remove();
            }
            System.out.println("要取走的数量为" + num + ",当前仓库已存放" + list.size());

            list.notifyAll(); 
        }
    }
}

2、创建一个生产者的线程类,用来插入数据

public class Producer extends Thread {

    private Storage storage;

    private int num;
    
    public Producer(Storage storage) {
        this.storage = storage;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        storage.add(num);
    }
}

3、创建一个消费者的线程类,用来取出数据

public class Consumer extends Thread {

    private Storage storage;

    private int num;

    public Consumer(Storage storage) {
        this.storage = storage;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        storage.consume(num);
    }
}

4、创建一个测试类,启动生产者和消费者的线程,用来实现这个生产者-消费者模型

public class Test {
    
    public static void main(String[] args){

        // 仓库对象
        Storage storage = new Storage();

        // 生产者
        Producer p1 = new Producer(storage);
        Producer p2 = new Producer(storage);
        Producer p3 = new Producer(storage);
        Producer p4 = new Producer(storage);
        Producer p5 = new Producer(storage);
        Producer p6 = new Producer(storage);

        // 设置生产者产品生产数量
        p1.setNum(10);
        p2.setNum(20);
        p3.setNum(30);
        p4.setNum(20);
        p5.setNum(30);
        p6.setNum(10);

        // 消费者
        Consumer c1 = new Consumer(storage);
        Consumer c2 = new Consumer(storage);
        Consumer c3 = new Consumer(storage);

        // 设置消费者产品消费数量
        c1.setNum(20);
        c2.setNum(30);
        c3.setNum(40);

        // 开始执行线程
        p1.start();
        p2.start();
        c1.start();
        c2.start();
        c3.start();
        p3.start();
        p4.start();
        p5.start();
        p6.start();
    }
}

4、是否能从Android中举几个例子说说用到了什么设计模式?

  • View事件分发:

    **责任链模式:**有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。

  • BitmapFactory加载图片:是通过不同的方法名和不同的参数来返回Bitmap

    **工厂模式:**定义一个用于创建对象的接口,让子类决定将哪一个类实例化。

  • ListView的Adapter:

    **适配器模式:**作为两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。

  • AlertDialog.Builder :

    **建造者模式:**将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

  • Adapter.notifyDataSetChanged():

    **观察者模式:**当一个对象被修改时,则会自动通知它的依赖对象

  • Binder机制:

    **代理模式:**为其他对象提供一个代理以控制对这个对象的访问。

5、装饰模式和代理模式有哪些区别?

  • 使用目的不同:代理模式是给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用;装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能
  • 构造不同:代理模式内部保持对目标对象的引用;装饰模式是通过构造函数传参的方式得到目标对象

6、实现单例模式有几种方法?懒汉式中双层锁的目的是什么?两次判空的目的又是什么?

懒汉式:延迟加载,多线程环境下会产生多个single对象 拿时间换空间,只有我需要他的时候才去加载它,懒加载机制

饿汉式:在类加载初始化时就创建好一个静态的对象供外部使用 拿空间换时间 先在内存中开辟一块空间,占用一块地方,等用到了直接就拿来用.

双重检查模式:通过加锁的方式,避免多线程同时执行getInstance()产生多个single对象

7、设计模式原则

  • 单一职责原则:一个类只负责一个功能领域中的相应职责

  • 开放封闭原则:对扩展开放,对修改关闭

  • 里氏代换原则:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

  • 依赖倒置原则:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程

  • 迪米特法则:应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用

  • 合成/聚合复用原则:要尽量使用合成/聚合,尽量不要使用继承

数据结构算法

数据结构

1、什么是数据结构?

数据结构是相互之间存在一种或多种特定关系的数据元素的集合。

数据结构包括数据对象集以及它们在计算机中的组织方式,即它们的逻辑结构和物理存储结构,同时还包括与数据对象集相关的操作集,以及实现这些操作的最高效的算法。

数据结构的分类:

  • 逻辑结构:是指数据对象中数据元素之间的相互关系。包括集合结构、线性结构、树形结构、图形结构。
    • 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其它关系。
    • 线性结构:线性结构中的数据之间是一对一的关系。
    • 树形结构:树形结构中的数据之间存在一种一对多的层次关系。
    • 图形结构:图形结构的数据元素是多对多的关系。
  • 物理结构:是指数据的逻辑结构在计算机中的存储形式。分为顺序存储和链式存储。
    • 顺序存储:是把数据元素存放在地址连续的存储单元里。
    • 链式存储:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。

2、迭代和递归的特点,并比 较优缺点

  • 递归就是通过重复调用函数自身实现循环;满足终止条件时会逐层返回来结束循环
  • 迭代通过函数内某段代码实现循环;使用计数器结束循环

递归代码精简,可读性好,但浪费了空间,因为系统要为每次函数调用分配存储空间,而且递归太深容易造成堆栈的溢出。

迭代效率高;无额外开销,节省空间,但代码不够简洁

算法

1、什么是算法?

算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

算法是解决问题步骤的有限集合,通常用某一种计算机语言进行伪码描述。

通常用时间复杂度和空间复杂度来衡量算法的优劣。

算法的五大特征:输入、输出、有穷性、确定性、可行性。

算法设计要求:正确性、可读性、健壮性、时间效率高和存储低。

2、什么是斐波那契数列?

斐波那契数列指的是这样的数列1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …即这个数列从第3项开始,每一项都等于前两项之和,数学表示F(1)=1,F(2)=1, F(3)=2,F(n)=F(n-1)+F(n-2)(n>=4,n∈N*)

3、了解哪些查找算法,时间复杂度都是多少?

  • **顺序查找:**顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。顺序查找的时间复杂度为O(n)。

  • 二分查找:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。时间复杂度为O(log2n)

  • **插值查找:**基于二分查找算法(差值查找也属于有序查找),将查找点的选择改进为自适应选择,可以提高查找效率。其核心就在于插值的计算公式 (key-a[low])/(a[high]-a[low])*(high-low)。时间复杂度为O(log2n)

  • **斐波那契查找:**也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。时间复杂度为O(log2n)

  • **树表查找:**以树结构存储数据

    • **二叉树查找:**二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在和每个节点的父节点比较大小,查找最适合的范围。

      **PS:**二叉树:左子树结点一定比其根结点小,右子树结点一定比其根结点大

    • 2-3查找树查找:

    • 红黑树查找:

      1. 每个节点要么是红色,要么是黑色。
      2. 根节点必须是黑色
      3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
      4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。
  • **哈希查找:**通过一个哈希函数计算出数据元素的存储地址

4、了解哪些排序算法,并比较一下,以及适用场景

  • 冒泡排序

    最简单的一种排序算法。先从数组中找到最大值(或最小值)并放到数组最右端(或最左端),然后在剩下的数字中找到次大值(或次小值),以此类推,直到数组有序排列。算法的时间复杂度为O(n^2),空间复杂度为常量O(1)。

    	public static void BubbleSort(int arr[]) {
            for (int i = 0; i < arr.length; i++) {
                for (int j = arr.length - 1; j > 0; j--) {
                    if (arr[j] < arr[j - 1]) {
                        int temp;
                        temp = arr[j - 1];
                        arr[j - 1] = arr[j];
                        arr[j] = temp;
                    }
                }
            }
        }
    

    优化:数据的顺序已经排好,没有必要继续进行下去

      public static void BubbleSort1(int [] arr){
          int temp;//临时变量
          boolean flag;//是否交换的标志
          for(int i=0; i<arr.length-1; i++){  
              flag = false;
              for(int j=arr.length-1; j>i; j--){
                  if(arr[j] < arr[j-1]){
                      temp = arr[j];
                      arr[j] = arr[j-1];
                      arr[j-1] = temp;
                      flag = true;
                  }
              }
              if(!flag) break;
          }
      }
    
  • 简单选择排序

    找到最小的值与第一个数的值交换,以此类推,,直到数组有序排列。算法的时间复杂度为O(n^2),空间复杂度为常量O(1)。

      // 选择排序
      public static void SelectionSort(int arr[]) {
          for(int i=0;i<arr.length;i++){
        	  int index=i;
        	  for(int j=i+1;j<arr.length;j++){
        		  if(arr[j]<arr[index]){
        			  index=j;
        		  }
        	  }
        	  if(arr[index]!=arr[i]){
        		  int temp=arr[i];
        		  arr[i]=arr[index];
        		  arr[index]=temp;
        	  }
          }
      }
    
  • 插入排序

    将无序序列插入到有序序列中。算法的时间复杂度为O(n^2),空间复杂度为常量O(1)。

    	public static void InsertSort(int[] arr) {
    		int j, temp;
    		for (int i = 1; i < arr.length; i++) {
    			temp = arr[i];
    			for (j = i - 1; j >= 0; j--) {
    				if (temp < arr[j]) {
    					break;   // 直接退出循环,更新插入时重复的值
    				}
    				arr[j + 1] = arr[j];
    			}
    			arr[j + 1] = temp;
    		}
    	}  
    
  • 希尔排序

    将数的个数设为n,取奇数k=n/2,将下标差值为k的数分为一组,构成有序序列。

    再取k=k/2 ,将下标差值为k的书分为一组,构成有序序列。

    重复第二步,直到k=1执行简单插入排序。

    算法的时间复杂度为O(nlogn)~ O(n2),空间复杂度为常量O(1)。

    	public static void XierSort(int[] arr) {
    		int len = arr.length;
    		int h = 0;
    		while (h <= len) { // 计算首次步长
    			h = 3 * h + 1;
    		}
    		while (h >= 1) {
    			for (int i = h; i < len; i++) {
    				int j = i - h; // 左边的一个元素
    				int get = arr[i]; // 当前元素
    				while (j >= 0 && arr[j] > get) { // 左边的比当前大,则左边的往右边挪动
    					arr[j + h] = arr[j];
    					j = j - h;
    				}
    				arr[j + h] = get; // 挪动完了之后把当前元素放进去
    			}
    			h = (h - 1) / 3;
    		}
    	}
    
  • 归并排序

    先使每个子序列有序,再使子序列段间有序。算法的时间复杂度为O(nlogn),空间复杂度为O(n)。

  • 堆排序

    堆的含义:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。

    将待排序的序列构造成一个堆,选出堆中最大(最小)的移走,再把剩余的元素调整成堆,找出最大(最小)的再移走,重复直至有序。

    算法的时间复杂度为O(nlogn),空间复杂度为O(1)。

  • 快速排序

    选择第一个数为p,小于p的数放在左边,大于p的数放在右边。

    递归的将p左边和右边的数都按照第一步进行,直到不能递归。

    算法的时间复杂度为O(nlogn),空间复杂度为O(1)。最坏时间复杂度O(n2)

    	public static void quitSort(int arr[], int low, int high) {
    		int start = low;
    		int end = high;
    		int key = arr[low];
    
    		while (start < end) {
    			while (end > start && key <= arr[end]) {
    				end--;
    			}
    			if (key > arr[end]) {
    				int temp = arr[end];
    				arr[end] = arr[start];
    				arr[start] = temp;
    			}
    
    			while (start < end && arr[start] <= key) {
    				start++;
    			}
    			if (arr[start] > key) {
    				int temp = arr[start];
    				arr[start] = arr[end];
    				arr[end] = temp;
    			}
    		}
    		
    		if (start > low) {
    			quitSort(arr, low, start - 1);
    		}
    		if (end < high) {
    			quitSort(arr, end + 1, high);
    		}
    	}
    

5、二叉排序树插入或删除一个节点的过程是怎样的?

插入:先查找该元素是否存在于二叉排列树中并记录其根节点,若没有则比较其和根节点大小后插入相应位置

删除:如果数据是子节点,直接删除即可,如果是叶结点,判断子节点有几个,如果只有一个,删除叶节点子节点上移,如果有左右子节点,用删除节点的直接前驱或直接后继来替换当前节点

直接前驱:左子树中最右孩子 直接后继:右子树中最左孩子

6、什么是红黑树?

红黑树是一种平衡二叉查找树,根结点为黑色,节点是红色或者黑色,叶子节点是黑色,每个红节点的两个子节点为黑色,并且任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

7、7瓶水1瓶有毒3只老鼠,怎么找有毒的水,第二天出结果

使用二进制编号

第一瓶水 001 第二瓶水 010 第三瓶水 011 第四瓶水 100
第五瓶水 101 第六瓶水 110 第七瓶水 111

第一只老鼠喝 一三五七瓶水

第二只老鼠喝 二三六七瓶水

第三只老鼠喝 四五六七瓶水

8、二分查找

private int search(int[] arr,int key){
    int low,hight,mid;
    low=0;
    hight=arr.length-1;
    while(low<hight){
        mid=(low+hight)/2;
        if(key<arr[mid]){
            hight=mid-1;
        }else if(key>arr[mid]){
            low=mid+1;
        }else{
            return mid;
        }
    }
    return -1;
}

9、反转链表

public class ListNode{
    int var;
    ListNode next;
}

private ListNode revereListNode(ListNode node){
    if(node==null||node.next==null){
        return node;
    }
    ListNode newNode=revereListNode(node.next);
    node.next.next=node;
    node.next=null;
    return newNode;
}

10、用两个栈实现队列

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>(); 
    //入队 
    public void add(int node) {
        stack1.push(node);
    }
    //出队 
    public int poll() {
        if(stack1.empty()&&stack2.empty()){
            throw new RuntimeException("Queue is empty!");
        }
        if(stack2.empty()){
            while(!stack1.empty()){
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }
}

11、用三个线程,顺序打印字母A-Z,输出结果是1A、2B、3C、1D 2E…

private static char c = 'A';
private static int i = 0;
public static void main(String[] args) {        
    Runnable runnable = new Runnable() {
           public void run() {
              synchronized (this) {//加锁
                try {
                    int threadId = Integer.parseInt(Thread.currentThread().getName());
                    while (i < 26) {
                         if (i % 3 == threadId - 1) {
                             System.out.println(threadId +""+ (char) c++);
                             i++;
                             notifyAll();// 唤醒处于等待状态的线程
                         } else {
                             wait();// 释放当前锁并进入等待状态
                         }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
              }//执行结束释放当前锁
           }
        };
        Thread t1 = new Thread(runnable, "1");
        Thread t2 = new Thread(runnable, "2");
        Thread t3 = new Thread(runnable, "3");
        t1.start();
        t2.start();
        t3.start();
}
  • 3
    点赞
  • 0
    评论
  • 5
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页

打赏

谦行

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者