最近打算找工作了,在网上也看了一些面试资料,但毕竟看别人的总结不如自己写一遍,于是便有了这篇文章的总结。这篇文章会包括Android问题 、Java问题、计算机网络问题、JVM问题以及算法结构问题,这些问题的总结来自于厘米姑娘的面试问题总结,看完她的总结作为一个程序员来说还是很钦佩的,而我写这篇文章能保证的是所有的问题都是我自己理解后的答案,而不是直接搬过来的解析。当然,我自己的理解也可能是有问题的,也希望大家能指出。
- 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正在被销毁,也是生命周期最后一个执行的方法,一般我们可以在此方法中做一些回收工作和最终的资源释放。
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 都在后台运行。
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一样。
当非人力终止Activity时,比如说屏幕切换时导致Activity被杀死并重新创建,又或者资源内存不足导致低优先级的Activity被杀死,会调用onSaveInstanceState() 来保存状态。该方法在onStop之前执行,但是和onPause 并没有先后关系。
onSaveinstanceState() 适用于临时性状态的保存,而onPause() 适用于对数据的持久化保存。
为了避免由于配置改变时Activity重建,可以在AndroidManifest.xml 中对应的Activity中设置android:configChanges=“orientation|screenSize”。此时再次旋转屏幕时,该Activity 不会被系统杀死和重建,只会调用onConfigurationChanged。因此,当配置程序需要响应配置改变,指定configChanges属性,重写onConfigurationChanged 方法即可。
优先级低的Activity在内存不足被回收后重新打开会引发Activity的重新创建。Activity 被重新创建后会调用onRestoreInstanceState(),这个方法在onStart 之后。并将onSaveInstanceState 保存的Bundle 对象作为参数传到onRestoreInstanceState和onCreat方法。因此可以从这两个方法中的Bundle 参数来判断Activity 是否重建,并取出数据进行恢复。但是需要注意的是,在onCreate取出数据的时候一定要注意判断savedInstanceState 是否为空。另外,谷歌更推荐使用onRestoreInstanceState进行数据恢复。
启动模式为singleTop、singleTask、singleInstance的Activity 在以下情况下都会回调onNewIntent()
singleTop:如果新Activity 已经位于任务栈中的栈顶,就不会重复创建,并且回调onNewIntent 方法。
singleTask: 只要该Activity 在任务栈中存在,就不会重复创建,并且回调onNewIntent 方法。
singleInstance: 只要该Activity 在任务栈中存在,就不会重复创建,并且回调onNewIntent 方法。
平时大多使用两个标志位 FLAG_ACTIVITY_SINGLE_TOP 以及 FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_SINGLE_TOP:对应singleTop 启动模式
FLAG_ACTIVITY_NEW_TASK:对应singleTask 启动模式
在保证有权限访问的情况下,通过隐式的Intnet 进行目标Activity的intent-filter匹配:
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匹配成功。
使用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 生命周期的调用。 隐式错门忒神 斯达克
- 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
- onCreate() : 服务创建时调用
- onStartCommand() : 服务启动时调用
- onBind() : 服务被绑定时调用
- onUnBind() : 服务被解绑时调用
- onDestory() :服务停止时被调用
这两种启动方式是 startService()和bindService()
会回调onBind()方法,这时候所触发生命周期方法流程分别是 onCreate()->onStartCommand()->onBind(),要想回调Service的destroy()方法,只能调用unbindService()以及stopService()方法才能使这个服务销毁。
Wifi服务:WIFI_SERVICE 判断wifi是否开启
音频服务:AUDIO_SERVICE 获取当前的音量 更改音量的大小
连接服务:CONNECTIVITY_SERVICE 获取网络是否连接
窗口服务:WINDOW_SERVICE 获取屏幕的大小
Service 是运行在主线程中的,并不能进行耗时操作。如果非要进行耗时操作,可以手动打开一个子线程,在子线程中进行耗时操作。如果服务不需要同时处理多个耗时操作请求,可以使用IntentService处理耗时操作。IntentService 是Service 的子类,它使用工作线程逐一处理所有启动请求,只需要实现onHandIntent方法即可。
AlarmManager是Android中常用的一种系统级别的提示服务,在特定的时刻为我们广播一个指定的Intent。简单的说就是我们设定一个时间,然后在该时间到来时,AlarmManager为我们广播一个我们设定的Intent,通常我们使用 PendingIntent,PendingIntent可以理解为Intent的封装包,简单的说就是在Intent上在加个指定的动作。在使用Intent的时候,我们还需要在执行startActivity、startService或sendBroadcast才能使Intent有用。而PendingIntent的话就是将这个动作包含在内了
前台服务是指用户可以看到并且当系统内存不足的情况下不允许被杀死的服务,在状态栏必须有一个通知图标,并且下拉状态栏可以看到更详细信息,比如说我们听歌时在状态栏出现的通知。和普通服务的区别在于它可见并且优先级比普通服务要高,内存不足的时候会优先杀死普通服务。在完成Notification通知消息的构建后,在Service的onStartCommand()中可以使用startForeground()方法来让Android服务运行在前台。
ActivityManagerService(AMS)是 Android中最核心的服务,主要负责Android四大组件的启动、切换、调度以及应用程序的管理和调度等工作。
AMS提供的主要功能包括以下几项:
1、提高进程优先级,降低进程被杀死的概率
2、在进程被杀死后,进行拉活
广播的注册有两种方法:一种在活动里通过代码动态注册,另一种在配置文件里静态注册。两种方式的相同点是都完成了对接收器以及它能接收的广播值这两个值的定义;不同点是动态注册的接收器必须要在程序启动之后才能接收到广播,而静态注册的接收器即便程序未启动也能接收到广播,比如想接收到手机开机完成后系统发出的广播就只能用静态注册了。
作为四大组件之一的ContentProvider内容提供者,ContentProvider主要用于数据存储和共享。和文件存储、StaredPreferenes存储、SQLite数据库这三种存储方式不同的是他可以让不同的应用程序进行数据访问,而另外的三种存储方式则只能在当前应用程序内进行数据访问。当然它可以选择性的分享数据,从而让程序本身隐私的数据不会泄露。
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();
}
SharedPreferences 是一种轻型的数据存储方式,适用于储存一些简单的配置信息,如int,string,boolean,float、long、set和map。由于系统对SharedPreferences有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读写会变得不可靠,甚至丢失数据。
SQLite在做CRDU操作时都默认开启了事务,然后把SQL语句翻译成对应的SQLiteStatement并调用其相应的CRUD方法,此时整个操作还是在rollback journal这个临时文件上进行,只有操作顺利完成才会更新数据库,否则会被回滚。
使用SQLiteDatabase的beginTransaction 开启一个事务,程序执行到 endTransaction() 方法时会检查事务的标志是否为成功,如果程序执行到 endTransaction()之前调用了setTransactionSuccessful() 方法设置事务的标志为成功则提交事务,如果没有调用setTransactionSuccessful() 方法则回滚事务,这样能保证数据的同步。
由于SQLite数据库只允许添加表字段而不允许删除和修改表字段,只能采用复制表删除的方法进行操作,也就是说先创建一个新表保留想要保存的字段,然后删除掉原表。
应用第一次启动时,会启动一个新进程,该进程用应用的包名作为进程名。该进程会启动主线程ActivityThread,也叫做UI线程,UI的绘制都在该线程里完成,然后我们平时创建的线程基本上在进程中执行的。一般来说,一个进程可以有多个线程,当然,线程的数量也必须是有限的。而且一个APP也不能说就只能对应一个进程,我们也可以在AndroidMenifest中给四大组件指定属性android:process开启多进程模式。
在Android系统中一个应用程序默认只有一个进程,每个进程都有自己独立的资源和内存空间,其他进程不能访问当前进程的内存和资源,无论是多个应用互相访问数据还是一个应用开启多个进程进行数据交流,不同进程中的数据就会有交互的需求,这就无法避免的就要使用IPC(进程间通信)了。
多进程通信容易出现的问题:
序列化就是表示把一个对象转换成可存储或可传输的状态,序列化以后的对象既可以在本地存储也可以在网络上传播
如果是在内存中使用,比如说Activity、service之间的对象传递,可以使用Parcelable,因为他的性能比较高,而如果是持久化操作,比如说存储对象,推荐使用Serializable。虽然他的性能比较低,但是Parcelable因为是把对象分解了,不能很好的保持对象的稳定性。
在Binder框架中定义了四个角色:Server,Client,ServiceManager和Binder驱动。其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间。
ServiceManager服务的管理者:
Service Manager是系统中一个独立的进程,它是整个Binder机制的守护进程,用来管理开发者创建的各种Server,并且向Client提供查询Server远程接口的功能。
Binder驱动:
名称 | 优点 | 缺点 | 设用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程通信 |
文件共享 | 简单易用 | 不适合高并发的情况, 并且无法做到进程间的即时通讯 | 无并发访问情况下, 交换简单的数据实时性不高的情况 |
AIDL | 功能强大,支持一对多并发通信 ,支持实时通讯 | 需要处理好线程同步 | 一对多通信且有Rpc需求 |
Messager | 支持一对多串行通信,() ,支持实时通讯 | 不能很好处理高并发情况,不支持Rpc, 数据通过Message进行传输,因此只能传输Bundle支持的数据类型 | 低并发的一对多即时通信,无 Rpc需求,或者无需返回结果的Rpc需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过call方法扩展其他操作 | 主要提供数据源的Crud | 一对多的进程间数据共享 |
Socket | 功能强大,可以通过网络传输字节流, 支持一对多并发实时通讯 | 实现细节有点繁琐,不支持直接的Rpc | 网络数据交换 |
注:Rpc(调用远程服务中的方法)
AIDL 意思是 Android Interface Defintion Language(Android 接口定义语言)。是用于定义服务端和客户端通信接口的一种描述语言,通过AIDL可以用来生成服务端的一个代理类。通过这个代理类,可以在一个进程中获取另外一个进程的数据和调用其暴露出来的方法,进而满足进程间的通信需求。
AIDL 的本质就是系统提供了一套可快速实现Binder 的工具,定义好AIDL文件只是方便IDE帮我生成所需的Binder类,AIDL并不是必须的文件,AIDL的具体实现就在这个代理类上。
当有多个业务模块都需要AIDL来进行IPC的时候,此时需要为每个模块都要创建一个AIDL文件,那么对应的Service就会很多,而创建Service是要消耗内存的,就会导致应用资源消耗严重的问题。优化的办法就是建立连接池,将每一个业务模块的Binder请求统一发到一个远程Service中去执行,这样就避免了多次创建Service。
连接池的工作原理:每个业务模块创建自己的AIDL文件并实现接口,然后向服务端提供自己的唯一标识和其对应的Binder对象,服务端只需要一个Service,服务器提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对像,不同的业务模块拿到所需的Binder对象后就可进行远程方法的调用了。
MotionEvent是手指触摸屏幕锁产生的一系列事件
典型的事件有:
scrollTo() 是指滑动到指定的位置,而scrollBy() 是指在当前位置上滑动一定的距离,可以不断累加。比如说都是从(0,0)这个位置出发,scrollTo(-10,-10) 会滑动到坐标轴(10,10)这个位置然后不动了,而scrollBy(-10,-10)则会一直滑动下去,第一次到(10,10),第二次到(20,20) 。而且还要注意的一点是他们滑动的是当前View的内容,而不是当前View 的本身。
startScroll()方法和computeScroll() 方法
startScroll() 方法并没有滑动的操作,而是初始化滚动数据,这时候就要重绘界面,系统会在绘制View的时候在draw()方法中调用computeScroll()方法,但computeScroll()方法是一个空方法,具体的滑动操作要在这个方法里面去实现,然后根据当前获取到的滚动值根据srollTo方法去移动,然后再重绘界面再移动,最后完成Scroller 的过渡滑动效果。
处理思路:
如何实现:
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实现,但没有本质区别。
MeasureSpec 是一个32位的int 值,高2位代表SpecMode(测量模式),低30位代表SpecSize( 某种测量模式下的规格大小)。
测量模式分为三种
测量出来的值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定子View的布局参数
MeasureSpec的作用:通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小
这三个的执行顺序是 onTouch()->onTouchEvent()->onClick()
onTouchListener的onTouch()方法会先触发;如果onTouch()返回false才会接着触发onTouchEvent(),如果返回true,后面的事件也就不会执行。同样的onTouchEvent()也是如此。
SurfaceView是View的子类,是一个适用于频繁刷新界面的View。
他和View 的区别有:
双缓冲技术:当一个动画正在显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。
Invalidate() 和postInvalidate() 方法都是用来View的刷新,区别在于调用的方式不同,invalidate 只能在主线程中调用,所以在子线程中必须要配合hanlder使用,而postInvalidate() 在子线程中可以直接调用。
表示不同密度的图片资源,用于适配手机的像素图片显示,像素从高到低依次排序为xxxhdpi>xxhdpi>xhdpi>hdpi>mdpi>ldpi,根据手机的dpi不同加载不同密度的图片。
安卓提供的动画主要分为两种:属性动画(Property Animation)和传统动画 (View Animation)。而View Animation又分为补间动画(TweenAnimation)和帧动画(FrameAnimation)。
使用祯动画要注意不能使用尺寸过大的图片,否则容易造成OOM
View动画是通过不断的图形变换实现的,但他并不具备交互性,不能真正的改变view的位置,动画发生后其响应事件的位置仍然在动画进行前的地方。而属性动画是动态改变属性来实现的,真正的改变了View的位置。
View动画改变的只是View的显示,而没有改变View的响应区域;而属性动画会通过反射技术来获取和执行属性的get、set方法,从而改变了对象位置的属性值。
插值器(Interpolator):根据时间流逝的百分比计算出当前属性值改变的百分比。确定动画效果变化的模式,如匀速变化、加速变化等等。View动画和属性动画均可使用。常用的系统内置插值器:
线性插值器(LinearInterpolator):匀速动画
加速减速插值器(AccelerateDecelerateInterpolator):动画两头慢中间快
减速插值器(DecelerateInterpolator):动画越来越慢
类型估值器(TypeEvaluator):根据当前属性改变的百分比计算出改变后的属性值。只针对于属性动画,View动画不需要类型估值器。常用的系统内置的估值器:
关系:Activity通过Window来实现视图元素的展示,window可以理解为一个容器,盛放着一个个的view,用来执行具体的展示工作
Handler就是将消息放入队列的机制,用来实现跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
Hander.sendMessage()发送消息时,会通过MessageQueue.enqueneMessage()的方法向MessageQueue中插入一条消息,当Looper.loop()开启循环后,会不断的轮询调用MessageQueue.next()方法,取到消息队列中对头的消息后调用Handler.dispatchMessage()的方法去传递消息,当Handler 收到消息后调用handler.handlerMessage()方法来处理消息。
Android的UI控件并不是线程安全的,如果多线程中并发访问可能导致UI控件处于不可预期的状态.
那为什么系统不对UI控件的访问加上锁机制呢?
缺点有两个:首先,加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行.
一个Thread只能有一个Looper,但可以有多个Handler。
通过Looper.prepare()可将一个Thread线程转换成Looper线程。Looper线程和普通Thread不同,它通过MessageQueue来存放消息和事件、Looper.loop()进行消息轮询。
可以创建,需要在子线程的new Handler之前添加Looper.prepare()方法,为子线程创建Looper,然后在Handler 之后调用Looper.loop() 方法,用来开启消息轮询。
Message msg = new Message();
Message msg = Message.obtain();
Message msg = handler1.obtainMessage();
后两种方法都是从整个Messge池中返回一个新的Message实例,能有效避免重复Message创建对象,因此更鼓励这种方式创建Message
线程局部变量,是一种多线程间并发访问变量的解决方案。与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程对象提供变量的独立副本,以保障线程安全。
Looper这里的轮询死循环并非简单地死循环,无消息时也会休眠。真正会阻塞卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。
post delay的Message并不是先等待一定时间再放入到MessageQueue中,而是直接进入并阻塞当前线程,然后将其delay的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。此时,如果队头的Message正是被delay的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该Message,否则唤醒后直接执行。
AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在子线程中进行UI操作。
HandlerThread:一种具有消息循环的线程,其内部可使用Handler。
IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。
Handler机制存在的问题:多任务同时执行时不易精确控制线程。
引入AsyncTask的好处:创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler实例就能完成相同的任务。
HandlerThread是一个线程类,它继承自Thread。与普通Thread不同,HandlerThread具有消息循环的效果,这是因为它内部HandlerThread.run()
方法中有Looper,能通过Looper.prepare()
来创建消息队列,并通过Looper.loop()
来开启消息循环。
不同于线程,IntentService是服务,优先级比线程高,更不容易被系统杀死,因此较适合执行一些高优先级的后台任务;不同于普通Service,IntentService可自动创建子线程来执行任务,且任务执行完毕后自动退出
IntentService的工作原理是,在IntentService的onCreate()里会创建一个HandlerThread,并利用其内部的Looper实例化一个ServiceHandler对象;而这个ServiceHandler用于处理消息的handleMessage()方法会去调用IntentService的onHandleIntent(),这也是为什么可在该方法中处理后台任务的逻辑;当有Intent任务请求时会把Intent封装到Message,然后ServiceHandler会把消息发送出,而发送消息是在onStartCommand()完成的,只能通过startService()才可走该生命周期方法,因此不能通过bindService创建IntentService。
好处:首先通过线程池中线程的重用,减少创建和销毁线程的性能开销。其次,能控制线程池中的并发数,否则会因为大量的线程争夺CPU资源造成阻塞。最后,线程池能够对线程进行管理,比如使用ScheduledThreadPool来设置延迟N秒后执行任务,并且每隔M秒循环执行一次。
**原理:**创建线程池需要ThreadPoolExecutor类,根据构造参数的不同可以配置各种各样的线程池。下面说下线程池的实现逻辑:
类型:
尽量不要使用Executors去创建线程池,因为在阿里开发手册中说明了Executors各个方法的弊端
缓存算法 LRU(Least Recently Used):当缓存满时, 会优先淘汰那些近期最少使用的缓存对象。
主要是两种方式:
LinkedHashMap
以强引用的方式存储外界的缓存对象,并提供get
和put
方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。
标签重用布局、
标签减少层级、
标签懒加载。六种数字类型(四个整数型(默认是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字节 ,可以储存任何字符;
抽象 封装 继承 多态
装箱是值类型转成object引用类型,拆箱是已被装箱的引用类型转成原来的值类型。
Android中常见的两个异常:
运行时异常:由程序自身的问题导致产生的异常;如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常);属于不可查异常。
非运行时异常:是RuntimeException以外的异常,如ClassNotFoundException(类没找到异常)
属于可查异常,即强制程序员必须进行处理,如果不进行处理则会出现语法错误。
常见的异常处理机制有:
**反射:**在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任何一个对象都能够调用它的任何一个方法和属性,这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
作用: 创建对象、操作属性、调用方法
**内部类:**内部类就是定义在另外一个类里面的类。
**作用:**它隐藏在外部类中,封装性更强,不允许除外部类外的其他类访问它;但它可直接访问外部类的成员。
静态内部类和非静态内部类的区别:
**final:**表示不可更改,具体体现在:
**finally:**finally是在Java异常处理时用到的,在try ,catch之后执行,不管有没有捕获到异常最后的finally方法会得到执行
**finalize():**在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。
重写表示子类重写父类的方法;重载表示函数的方法名可以一样,但区别在于可以有不同的参数个数或类型
####14、 为什么匿名内部类中使用局部变量要用final修饰?
为了保证在内部类中能找到外部局部变量,通过final关键字可得到一个外部变量的引用,而且通过final关键字也不会在内部类去做修改该变量的值,保护了数据的一致性。
可将Java集合框架大致可分为Set、List、Queue 和Map四种体系
1、Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
2、 当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。
HashSet不能保证元素的排列顺序;使用Hash算法来存储集合中的元素,有良好的存取和查找性能;通过equal()
判断两个元素是否相等,并两个元素的hashCode()
返回值也相等
TreeSet是SortedSet接口的实现类,根据元素实际值的大小进行排序;采用红黑树的数据结构来存储集合元素;支持两种排序方法:自然排序(默认情况)和定制排序。前者通过实现Comparable接口中的compareTo()
比较两个元素之间大小关系,然后按升序排列;后者通过实现Comparator接口中的compare()
比较两个元素之间大小关系,实现定制排列
HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口
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
Hash法(散列法):首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得p=f(k),f称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元;以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=f(k),从而达到按关键字直接存取元素的目的。
**Hash 冲突 :**当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,即 k1≠k2 ,但 H(k1)=H(k2),这种现象称为冲突,此时称k1和k2为同义词。
解决的办法:
使用ConcurrentHashMap,它使用分段锁来保证线程安全
ConcurrentHashMap是线程安全的HashMap,它采取锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
HashMap是无序的,而LinkedHashMap是有序的HashMap,默认为插入顺序,还可以是访问顺序,基本原理是其内部通过Entry维护了一个双向链表,负责维护Map的迭代顺序
HashMap几个默认值,初始容量为16、填充因子默认为0.75、扩容时容量翻倍。也就是说当HashMap中元素个数超过16*0.75=12
时会把数组的大小扩展为2*16=32
,然后重新计算每个元素在数组中的位置
由于每次扩容还需要重新计算元素Hash值,损耗性能,所以建议在使用HashMap时,最好先估算Map的大小,设置初始值,避免频繁扩容
hashCode()用于计算对象的Hash值,确认对象在散列存储结构中的存储地址。
equals()比较两个对象的地址值是否相等 ;hashCode()得到的是对象的存储位置,可能不同对象会得到相同值。要记住的一点是如果若equals()相等,则hashcode()一定相等,而hashcode()不等,则equals()一定不相等;
同步和异步体现的是消息的通知机制:所谓同步,方法A调用方法B后必须等到方法B返回结果才能继续后面的操作;所谓异步,方法A调用方法B后只需要让方法B在调用结束后通过回调等方式通知方法A,这时候A可以继续执行的后面的任务。
阻塞和非阻塞侧重于等待消息时的状态:所谓阻塞,就是在结果返回之前让当前线程挂起;所谓非阻塞,就是在等待时可做其他事情,通过轮询去询问是否已返回结果
让父线程等待子线程结束之后才能继续运行。比如说有两个线程t1和t2,先启动t1,然后执行t1.join()方法,再启动t2,执行的结果是 t2线程在t1 线程执行完之后才开始执行。
线程安全就是当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
保证线程安全可从多线程三特性出发:
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。而synchronized是非公平的,即在锁被释放时,任何一个等待锁的线程都有机会获得锁。ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数改用公平锁。
锁绑定多个条件:一个ReentrantLock对象可以通过多次调用newCondition()同时绑定多个Condition对象。而在synchronized中,锁对象wait()和notify()或notifyAl()只能实现一个隐含的条件,若要和多于一个的条件关联不得不额外地添加一个锁。
synchronized :可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码
**volatile :**它能够使变量在值发生改变时能尽快地让其他线程知道。
synchronized能保证操作的原子性,而volatile不可以,假设线程A和线程B同时读取到变量a值,A修改a后将值更新到主内存,同时B也修改a值会覆盖A的修改操作
synchronized可修饰变量、方法和类,而volatile只能修饰变量
synchronized可能会造成线程阻塞,而volatile不会造成线程的阻塞
本质上锁住的是对象。在java虚拟机中,每个对象和类在逻辑上都和一个监视器相关联,synchronized本质上是对一个对象监视器的获取。当执行同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器,才能进入同步代码块或同步方法;而没有获取到的线程将会进入阻塞队列,直到成功获取对象监视器的线程执行结束并释放锁后,才会唤醒阻塞队列的线程,使其重新尝试对对象监视器的获取。
sleep()来自Thread类;wait()来自Object类
sleep()用于线程控制自身流程;而wait()用于线程间通信,配合notify()/notifyAll()在同步代码块或同步方法里使用
sleep()的线程不会释放对象锁;wait()会释放对象锁进入等待状态,使得其他线程能使用同步代码块或同步方法
####1、五层协议的体系结构分别是什么?每一层都有哪些协议?
物理层
数据链路层
网络层 IP 协议
传输层 TCP/UDP 协议
应用层 HTTP 协议
IP 协议又称为互联网协议,是用来分组交换数据的一种协议,因为IP协议定义了寻址方法和数据报的封装结构,所以可以根据源主机和目的主机的地址来进行数据传输。
TCP 协议即为传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议是双向通信的,连接前有三次握手,断开连接也有四次挥手,所以它能保证数据按序发送,按序接受。
引申三次握手以及四次挥手
UDP协议是用户数据报协议,也称之为用户数据报文协议,是一种简单的面向数据报的传输层协议。
UDP 协议没有 TCP 协议稳定,因为它不建立连接,也不按顺序发送,可能会出现丢包现象,使传输的数据出错。但是有得就有失,UDP 的效率更高,因为 UDP 头包含很少的字节,比 TCP 负载消耗少,同时也可以实现双向通信,不管消息送达的准确率,只负责无脑发送。 UDP 一般多用于 IP 电话、网络视频等容错率强的场景。
Socket 被称为“套接字”,作为应用层和传输层之间的桥梁,它把复杂的 TCP/IP 协议簇隐藏在背后,为用户提供简单的客户端到服务端接口,让我们感觉这边输入数据,那边就直接收到了数据,像一个“管道”一样。
要实现客户端与服务端的通信,双方都需要实例化一个 Socket。
在客户端实现:
在服务端实现:
HTTP 协议是指 超文本传输协议,HTTP协议是基于TCP/IP 的应用层网络协议,他并不涉及到数据包的传输,而是规定了客户端和服务端的通信格式。
HTTPS 协议是指 超文本传输安全协议,是一种通过计算机网络进行安全通信的传输协议。实际上HTTPS 是由HTTP进行的通信,但由但利用SSL/TLS来加密数据包的方式。
HTTP是明文传输,默认端口号是80,https是ssl加密的传输,身份认证的网络协议,默认端口号是443
1xx:表示服务器已接收了客户端请求,客户端可继续发送请求
2xx:表示服务器已成功接收到请求并进行处理
3xx:表示服务器要求客户端重定向
4xx:表示客户端的请求有非法内容
5xx:表示服务器未能正常处理客户端的请求而出现意外错误
GET:当客户端要从服务器中读取某个资源时使用GET;一般用于获取/查询资源信息;GET参数通过URL传递,传递的参数是有长度限制,不能用来传递敏感信息
POST:当客户端给服务器提供信息较多时可以使用POST;POST会附带用户数据,一般用于更新资源信息;POST将请求参数封装在HTTP 请求数据中,可以传输大量数据,传参方式比GET更安全
浏览器向DNS服务器请求解析该URL中的域名所对应的IP地址
解析出IP地址后,根据该IP地址和默认端口80,和服务器建立TCP连接
浏览器发出读取文件的HTTP请求,该请求报文作为TCP三次握手的第三个报文的数据发送给服务器
服务器对浏览器请求作出响应,并把对应的html文本发送给浏览器
释放TCP连接,若connection模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求
客户端将服务器响应的html文本解析并显示
HTTP1.0和HTTP1.1的区别:
HTTP2.0和HTTP1.X相比的新特性:
JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
JVM会用一段空间来存储执行程序期间需要用到的数据和相关信息,这段空间就是运行时数据区(Runtime Data Area),也就是常说的JVM内存。JVM会将它所管理的内存划分为线程私有数据区和线程共享数据区两大类:
线程私有数据区包含:
线程共享数据区包含:
垃圾回收机制(GC): Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。
判定对象可回收有两种方法:
四种垃圾收集算法:
分代收集算法:是当前商业虚拟机都采用的一种算法,根据对象存活周期的不同,将Java堆划分为新生代和老年代,并根据各个年代的特点采用最适当的收集算法。
强引用(StrongReference):具有强引用的对象不会被GC;即便内存空间不足,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会随意回收具有强引用的对象。
软引用(SoftReference):只具有软引用的对象,会在内存空间不足的时候被GC;软引用常用来实现内存敏感的高速缓存。
弱引用(WeakReference):无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
虚引用(PhantomReference):仅持有虚引用的对象,在任何时候都可能被GC;常用于跟踪对象被GC回收的活动;
加载、验证、准备、解析、初始化
加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口
(将java源代码编译后的.class字节码文件以二进制流的方式加载进内存)
验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证
准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值
解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程
初始化(Initialization):是类加载过程的最后一步,会开始真正执行去执行类初始化的代码逻辑。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制
双亲委派模型:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
双亲委派模型的优点:可以避免类的重复加载,并且可以防止核心API库被随意篡改。
java 的内存模型分为主内存和工作内存,所有线程共享主内存,每个线程都有自己的工作内存,不是共享的。一个线程不能访问另一个线程的工作内存。线程之间通过主内存来实现线程兼间的通信。
原子性(Atomicity):一个操作要么都执行要么都不执行。
read
、load
、assign
、use
、store
和write
,因此可认为基本数据类型的访问读写是具备原子性的。monitorenter
和monitorexit
来隐式地使用lock
和unlock
这两个操作,反映到Java代码中就是同步代码块synchronized
关键字。可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
volatile
能保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;synchronized
对一个变量执行unlock操作之前可以先把此变量同步回主内存中;被final
修饰的字段在构造器中一旦初始化完成且构造器没有把this
的引用传递出去,就可以在其他线程中就能看见final字段的值。有序性(Ordering):程序代码按照指令顺序执行。
volatile
本身就包含了禁止指令重排序的语义;synchronized
保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。JVM(Java Virtual Machine): 编译的是.class文件,基于栈的结构
dalvik虚拟机:是Google厂商合作开发的针对Android的虚拟机,编译的是dex文件,基于寄存器结构,相对于JVM更精简,运行效率更高。
ART(Android Runtime):Android 5.0 L版本 2014年开始应用,采用AOT(Ahead of Time)预编译,在第一次安装时会将字节码转换成机器码。需要更大的存储空间,首次安装慢一下,以后的每次运行会更快,性能提升,电池续航更好 。
在java中,堆和栈都是内存中存放数据的地方,区别在于:
从高到低分别是Android应用层,Android应用框架层,Android系统运行层和Linux内核层。
所有安装在手机上的应用程序都属于这一层。Android系统将会包含系列的核心应用程序,这些程序包括电子邮件客户端、SMS程序、日历、地图、浏览器、联系人等。这些应用程序都是使用Java编写的。
Android应用程序框架层提供了大量的API供开发者使用,在开发Android应用程序时,就是面向底层的应用程序框架进行的。
这一层通过一些C/C++库来为Android系统提供了主要的特性支持。此层中还有Android运行时库,它主要提供一些核心库,来允许开发者使用Java语言来编写Android应用。因此可以将此层看作由提供Android系统特性的函数库和Android运行时库两部分组成,
这一层为Android设备的各种硬件提供了底层的驱动(Linux内核提供了安全性、内存管理、进程管理、网络协议和驱动模型等核心系统服务)。
Android运行时库中包含了Dalvik虚拟机(5.0后改为ART运行环境),它使得每一个Android应用都能运行在独立的进程当中,并且拥有一个自己的Dalvik虚拟机实例。
死锁是指多个进程因循环等待资源而造成无法执行的现象,它会造成进程无法执行,同时会造成系统资源的极大浪费。
MVC:模型(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中的展示器,视图模型和视图实现了双向绑定,当视图发生变化的时候视图模型也会发生改变,当视图模型变化的时候视图也随之变化。
MVC的实现了视图和模型的分离,避免了视图和模型糅合在一起,当视图改变的时候只要业务逻辑没变不需要改变模型;但是它有一个缺点缺点是因为MVC中的控制器并不能直接更新视图,所以MVC并不能实现视图和模型的完全分离,视图依然依赖模型的数据(数据结构)来显示,也就是说视图依赖模型。
MVP相比于MVC的优势:
MVVM相比于MVP的优势:在常规的开发模式中,数据变化需要更新HUI的时候,需要先获取UI控件的引用,然后再更新UI,获取用户的输入和操作也需要通过UI控件的引用,但在MVVM中,这些都是通过数据驱动来自动完成的,数据变化后会自动更新UI,UI的改变也能自动反馈到数据层,数据成为主导因素。这样MVVM层在业务逻辑处理中只要关心数据,不需要直接和UI打交道,在业务处理过程中简单方便很多。
某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。在生产者与消费者之间在加个缓冲区,我们形象的称之为仓库,生产者负责往仓库了进商品,而消费者负责从仓库里拿商品,这就构成了生产者消费者模型。
具体规则:生产者只在缓存区未满时进行生产,缓存区满时生产者进程被阻塞;消费者只在缓存区非空时进行消费,缓存区为空时消费者进程被阻塞;当消费者发现缓存区为空时会通知生产者生产;当生产者发现缓存区满时会通知消费者消费。
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();
}
}
View事件分发:
**责任链模式:**有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
BitmapFactory加载图片:是通过不同的方法名和不同的参数来返回Bitmap
**工厂模式:**定义一个用于创建对象的接口,让子类决定将哪一个类实例化。
ListView的Adapter:
**适配器模式:**作为两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。
AlertDialog.Builder :
**建造者模式:**将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
Adapter.notifyDataSetChanged():
**观察者模式:**当一个对象被修改时,则会自动通知它的依赖对象
Binder机制:
**代理模式:**为其他对象提供一个代理以控制对这个对象的访问。
懒汉式:延迟加载,多线程环境下会产生多个single对象 拿时间换空间,只有我需要他的时候才去加载它,懒加载机制
饿汉式:在类加载初始化时就创建好一个静态的对象供外部使用 拿空间换时间 先在内存中开辟一块空间,占用一块地方,等用到了直接就拿来用.
双重检查模式:通过加锁的方式,避免多线程同时执行getInstance()产生多个single对象
单一职责原则:一个类只负责一个功能领域中的相应职责
开放封闭原则:对扩展开放,对修改关闭
里氏代换原则:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
依赖倒置原则:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程
迪米特法则:应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用
合成/聚合复用原则:要尽量使用合成/聚合,尽量不要使用继承
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
数据结构包括数据对象集以及它们在计算机中的组织方式,即它们的逻辑结构和物理存储结构,同时还包括与数据对象集相关的操作集,以及实现这些操作的最高效的算法。
数据结构的分类:
递归代码精简,可读性好,但浪费了空间,因为系统要为每次函数调用分配存储空间,而且递归太深容易造成堆栈的溢出。
迭代效率高;无额外开销,节省空间,但代码不够简洁
算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
算法是解决问题步骤的有限集合,通常用某一种计算机语言进行伪码描述。
通常用时间复杂度和空间复杂度来衡量算法的优劣。
算法的五大特征:输入、输出、有穷性、确定性、可行性。
算法设计要求:正确性、可读性、健壮性、时间效率高和存储低。
斐波那契数列指的是这样的数列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*)
**顺序查找:**顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。顺序查找的时间复杂度为O(n)。
二分查找:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。时间复杂度为O(log2n)
**插值查找:**基于二分查找算法(差值查找也属于有序查找),将查找点的选择改进为自适应选择,可以提高查找效率。其核心就在于插值的计算公式 (key-a[low])/(a[high]-a[low])*(high-low)。时间复杂度为O(log2n)
**斐波那契查找:**也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。时间复杂度为O(log2n)
**树表查找:**以树结构存储数据
**二叉树查找:**二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在和每个节点的父节点比较大小,查找最适合的范围。
**PS:**二叉树:左子树结点一定比其根结点小,右子树结点一定比其根结点大
2-3查找树查找:
红黑树查找:
null
(树尾端)的任何路径,都含有相同个数的黑色节点。**哈希查找:**通过一个哈希函数计算出数据元素的存储地址
冒泡排序
最简单的一种排序算法。先从数组中找到最大值(或最小值)并放到数组最右端(或最左端),然后在剩下的数字中找到次大值(或次小值),以此类推,直到数组有序排列。算法的时间复杂度为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);
}
}
插入:先查找该元素是否存在于二叉排列树中并记录其根节点,若没有则比较其和根节点大小后插入相应位置
删除:如果数据是子节点,直接删除即可,如果是叶结点,判断子节点有几个,如果只有一个,删除叶节点子节点上移,如果有左右子节点,用删除节点的直接前驱或直接后继来替换当前节点
直接前驱:左子树中最右孩子 直接后继:右子树中最左孩子
红黑树是一种平衡二叉查找树,根结点为黑色,节点是红色或者黑色,叶子节点是黑色,每个红节点的两个子节点为黑色,并且任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
使用二进制编号
第一瓶水 001 第二瓶水 010 第三瓶水 011 第四瓶水 100
第五瓶水 101 第六瓶水 110 第七瓶水 111
第一只老鼠喝 一三五七瓶水
第二只老鼠喝 二三六七瓶水
第三只老鼠喝 四五六七瓶水
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;
}
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;
}
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();
}
}
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();
}