【Android面试题(整理更新)】

【Android篇】

一、Android的声明周期

一)生命周期

  1. onCreate():生命周期的第一个方法,用来资源初始化、调用setContextView( )加载 布局。
  2. onStart():Activity正在启动中,即将用。此时,Activity可见但是还是被隐藏在 stack中。
  3. onResume():Activity已经可见,并且已经出现在前台stack而且已经开始活动了,与onStart()的区别在于是否在前台。
  4. onRestart():Activity正在重新启用,一边是Activity从隐藏到可见时被调用。如:从Home键返回或者从上一个Activity返回。
  5. onPause(): Activity正在停止,正常情况下onStop()会接着被调用,用于存储数据。不能执行过长时间,否则会阻塞UI线程。
  6. onStop():可以做一些稍微耗时的工作,但也不能时间过长,否则会阻塞UI线程。
  7. onDestory():Activity的最后的一个回调方法,可以做一些资源回收的工作。

二)生命周期的具体调用情况:

  1. 第一次打开Activity时:onCreate()–onStart()–onResume()
  2. 打开新的Activity时:前一个Activity:onPause()-- onStop()
  3. 再次回到Activity时:onRestart() --onStart()–onResume()
  4. 用户按返回Home键时:onPause()–onStop()–onDestory()
  5. 从ActivityA启动到ActivityB时:onPause(ActivityA)–onStart()–onResume()–onStop(ActivityA)

二、Android布局

一)六大布局

LinearLayout (线性布局),RelativeLayout(相对布局),FrameLayout(帧布局),
AbsoluteLayout(绝对布局),TableLayout(表格布局),GridLayou(网格布局)。

  1. 线性布局LinearLayout,可以指定子控件的排列方式,比如垂直方向或水平方向。
  2. 绝对布局Relavitelayout,可以指定子控件的相对位置,比如上下左右、居中等,也可以指定一个控件相对另一个控件的相对位置。
  3. 帧布局FrameLayout,显示的View都是一层一层地往上加,显示的是最上面的一层。应用在动态显示碎片Fragment也是比较多的。
  4. 绝对布局AbsoluteLayout,View的显示要定义具体的单位长度px。这个局限性比较多,不能匹配多种屏幕,基本已经不使用了。
  5. 表格布局TableLayout,是线性布局的子类,显示据一般是一行一行的,一行可以有多列,列与列对齐是很方便的。
  6. 表格布局GridLayout,作为android 4.0后新增的一个布局,与前面介绍过的TableLayout(表格布局)其实有点大同小异;如果是显示类似网格效果的多个控件是非常方便的。

二)LinearLayout 和RelativeLayout的优缺点

  1. LinearLayout(线性布局):最常用的布局
    它包含的子控件将以横向或竖向的方式排列,按照相对位置来排列所有的widgets或者其他的containers,超过边界时,某些控件将缺失或消失。因此一个垂直列表的每一行只会有一个widget或者是container,而不管他们有多宽,而一个水平列表将会只有一个行高(高度为最高子控件的高度加上边框高度)。

  2. RelativeLayout(相对布局): 第二常用的布局
    允许子元素指定它们相对于其父元素或兄弟元素的位置,这是实际布局中最常用的布局方式之一。它灵活性大很多,当然属性也多,操作难度也大,属性之间产生冲突的的可能性也大,使用相对布局时要多做些测试。

三)RelativeLayout和LinearLayout性能比较?

  1. RelativeLayout会让子View调用2次onMeasure,LinearLayout在有weight时,也会调用子View2次onMeasure
  2. RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
  3. 在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。

三、Activity的四种启动模式

  1. Standard:
    标准的启动模式,如果需要启动一个activity就会创建该activity的实例。也是activity的默认启动模式。
  2. SingeTop:
    如果启动的activity已经位于栈顶,那么就不会重新创建一个新的activity实例。而是复用位于栈顶的activity实例对象。如果不位于栈顶仍旧会重新创建activity的实例对象。
  3. singleInstance:
    如果使用singleInstance启动模式的activity在启动的时候会复用已经存在的activity实例。不管这个activity的实例是位于哪一个应用当中,都会共享已经启动的activity的实例对象。使用了singlestance的启动模式的activity会单独的开启一个共享栈,这个栈中只存在当前的activity实例对象。适合需要与程序分离开的页面。例如闹铃提醒,将闹铃提醒与闹铃设置分离。
  4. singleTask:
    当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例,singleTask适合作为程序入口点。

四、Android的四大组件

一)四大组件

活动:Activity,用来表现功能
服务:service,后台运行服务,不提供界面展示
广播接收者:BroadCast Receive,用来接收广播
内容提供者:Content Provider,支持多个应用中存储和读取数据,相当于数据库

二)service的使用方式&&生命周期

  1. startService():
    启动;当应用程序组件(如:Activity)调用startSevice时启动服务时,处于started状态,只用service调用stopSelf或者其他组件调用stopService才会停止服务。
    生命周期为:onCreate—onStartCommand—onDestory

  2. bindService():
    绑定;当应用程序组件调用bindServive绑定启动服务时,处于bound状态,其他组件可以通过回调获取service的代理对象和service进行交互,而这两方也进行了绑定,所以启动方销毁时,service也会自动执行unbind操作,当发现所有的绑定都销毁了才会销毁service。*(与Activity进行绑定的)
    生命周期为:onCreate—onBind—onUnbind—onDestory

三)广播

BroadCastReceiver 的生命周期很短暂,当接收到广播的时候创建,当onReceive()方法结束时销毁,运行再主线程中的,不能在其中直接执行耗时任务。

1)注册方式
  1. 静态注册:AndroidManifest中进行注册后,不管改应用程序是否处于活动状态,都会进行监听,比如某个程序时监听内存的使用情况的,当在手机上安装好后,不管改应用程序是处于什么状态,都会执行改监听方法中的内容
  2. 动态注册:代码中进行注册后,当应用程序关闭后,就不再进行监听。
2)分类

通过binder机制实现通信。

  1. 无序广播:
    完全异步,逻辑上可以被任何广播接收者接收到。优点是效率较高。缺点是一个接收者不能将处理结果传递给下一个接收者,并无法终止广播intent的传播。
  2. 有序广播:
    按照被接收者的优先级顺序,在被接收者中依次传播。比如有三个广播接收者 A,B,C,优先级是 A > B >C。那这个消息先传给A,再传给 B,最后传给 C。每个接收者有权终止广播,比如 B 终止广播,C 就无法接收到。此外A接收到广播后可以对结果对象进行操作,当广播传给 B 时, B 可以从结果对象中取得 A 存入的数据。
3)类型

普通广播(自定义的Intent广播),系统广播(系统内置的广播,如:开机、网络状态变化等)、有序广播(发送出去的广播被广播接收者按照先后顺序接收,先接收道广播接受者可对广播进行修改或者截断,使用sendOrderedBroadcast(intent))、APP内部广播(可以列为局部广播,在同一个APP内发送和接收,将exported属性设为false)

五、View和自定义View

一)Android View刷新机制

在Android的布局体系中,父View负责刷新、布局显示子View;而当子View需要刷新时,则是通知父View来完成。

二)Android View绘制流程

  1. 第一步:调用ViewGroup中的onMeasure方法。
    在方法中调用了measureChild方法,执行了所有子控件的onMesure方法测绘出所有的子控件的大小。调用setMeasureDimension方法设置测绘后的大小。
  2. 第二步:调用ViewGroup中的onLayout方法。 在方法调用getChildCount方法
    获取到子条目数量。 用for循环遍历出每一个子条目的对象。 通过对象.layout方法 给子控件设置摆放位置。
  3. 第三步:首先调用ViewGroup的disPatchDraw方法绘制ViewGroup。然后调用View中的onDraw方法进行绘制.
    1. drawBackground(canvas):
      作用就是绘制 View 的背景。
    2. onDraw(canvas) :
      绘制 View 的内容。View的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。
    3. dispatchDraw(canvas):
      遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View
      的绘制过程,基本满足需求。
    4. onDrawForeground(canvas):
      对前景色跟滚动条进行绘制。
    5. drawDefaultFocusHighlight(canvas):
      绘制默认焦点高亮

三)自定义View绘制流程

当我们不满足于Android提供的原生控件和布局时,就应该考虑到自定义view。

  1. 自定义View分为两大块:
    自定义控件 和 自定义容器
  2. 自定义View必须重写两个构造方法 :
    第一个是一个参数的上下文,用于在java代码中new对象使用 第二个是两个参数的一个上下文,一个AttributSet。主要用于在xml中定义使用。

四)View的事件分发机制

事件分为三种ACTION_DOWN、ACTION_MOVE、ACTION_UP。

1)事件分发的三大方法
  1. dispatchTouchEvent:用来进行事件的分发。事件传递到当前的View时,会执行此方法,此方法包含了事件分发的逻辑,返回的结果收到当前View的onTouchEvent和下一个View的dispatchTouchEvent影响。
  2. onInterceptTouchEvent:这个方法在dispatchTouchEvent中被调用,用来判断是否拦截某个,如果在同一个事件序列中拦截了某个事件,此方法不会再被调用,返回结果表示是否拦截此事件。只在ViewGroup中使用,View中没有此方法。
  3. onTouchEvent:在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
2)ViewGroup的分发逻辑

requestDisallowInterceptTouchEvent方法用于影响父元素的事件拦截策略,requestDisallowInterceptTouchEvent(true),表示不允许父元素拦截事件,这样事件就会传递给子View。一般这个方法子View用的多,可以用来处理滑动冲突问题。

  1. View中没有onInterceptTouchEvent方法,所以一旦事件传递到View,那么View的dispatchTouchEvent方法就会被调用。
  2. dispatchTouchEvent方法中处理事件的逻辑顺序是onTouchListener–>onTouchEvent–>onClickListener。
  3. 也就是说如果View设置了onTouchListener,那onTouchListener的onTouch方法会被调用,如果onTouch方法返回true,那事件就被消耗了,事件分发结束,onTouchEvent不会被调用。
  4. 如果onTouch方法返回false,那么onTouchEvent就会被调用。如果View设置了onClickListener,当ACTION_UP事件到来时,onTouchEvent中的onClickListener的onClick方法也会被调用。
  5. View一般都会消耗事件,如果View没有消耗ACTION_DOWN事件,那后面ACTION_MOVE和ACTION_UP就都不会传递给View。
3)自定义View如何提供获取View属性的接口
  1. 在你的自定义View里创建一个接口。
  2. 类成员变量里声明一个这个接口的引用。
  3. 写一个方法获取并持有Activity实现的接口的实例
  4. 在Activity里实现这个接口
  5. Activity里绑定XML里的自定义View属性,并向XML创建的自定义View对象传递Activity实现的接口对象。

五)ListView 和 RecyclerView 简要对比分析

1)使用上
a) ListView:
  1. 继承重写BaseAdapter类;
  2. 自定义ViewHolder与convertView的优化;
  3. 默认不支持单个View 刷新
b) RecyclerView:
  1. 继承重写RecyclerView.Adapter与RecyclerView.ViewHolder
  2. 设置LayoutManager,以及layout的布局效果
  3. 支持局部刷新和动画
2)缓存
a) ListView

二级缓存 :屏幕内View 屏幕外View

b) RecyclerView
  1. 四级缓存 :屏幕内View 屏幕外View
  2. ViewCacheExtension(用户自定义)
  3. 继承ViewCacheExtension实现
  4. 列表有固定的数量条目和宽高,这样子,列表初始化的时候就能直接从这级缓存拿到ViewHolder,不需要再创建ViewHolder,大大节省时间,提高效率,需要自己去管理释放缓存。
c) RecyclerPool (用户自定义)
  1. 多个RecycleView可共享,可用于多个RecyclerView的优化
  2. ViewPager 多个页面 有同样的RecyclerView item 可以设置 一个 RecyclerPool。
3)选取策略
  1. 性能上: RecyclerView 并不比ListView 有明显的性能优势。
  2. 功能上: RecyclerView 适用于灵活布局时,新组件开发优先选取RecyclerView 。
4)RecyclerView 使用技巧

两个位置函数的区别

  1. adapterPosition 在Adapter中绑定点击事件,注意处理 NO_POSITION 的case。
  2. layoutPosition 在layoutManager获取 用户看到的位置
5)RecyclerView 优化技巧
  1. 数据处理与视图绑定分离
    RecyclerView的bindViewHolder方法是在UI线程进行的,如果在该方法进行耗时操作,将会影响滑动的流畅性。
  2. 数据优化
    1. 分页加载远端数据,对拉取的远端数据进行缓存,提高二次加载速度;
    2. 对于新增或删除数据通过DiffUtil,来进行局部数据刷新,而不是一味的全局刷新数据。
    3. DiffUtil是support包下新增的一个工具类,用来判断新数据和旧数据的差别,从而进行局部刷新。DiffUtil的使用,在原来调用mAdapter.notifyDataSetChanged()的地方。
  3. 布局优化
    1. 减少过度绘制
    2. 减少布局层级,可以考虑使用自定义View来减少层级,或者更合理的设置布局来减少层级。
  4. 减少xml文件inflate时间
    xml文件包括:layout、drawable的xml,xml文件inflate出ItemView是通过耗时的IO操作。可以使用代码去生成布局,即new View()的方式。这种方式是比较麻烦,但是在布局太过复杂,或对性能要求比较高的时候可以使用。
  5. 减少View对象的创建
    一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套。
  6. 设置高度固定
    如果item高度是固定的话,可以使用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源。
  7. 共用RecycledViewPool
    在嵌套RecyclerView中,如果子RecyclerView具有相同的adapter,那么可以设置RecyclerView.setRecycledViewPool(pool)来共用一个RecycledViewPool。如果LayoutManager是LinearLayoutManager或其子类,需要手动开启这个特性:layout.setRecycleChildrenOnDetach(true)。
  8. 加大RecyclerView的缓存
    用空间换时间,来提高滚动的流畅性。
recyclerView.setItemViewCacheSize(20);
recyclerView.setDrawingCacheEnabled(true);
recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
  1. 增加RecyclerView预留的额外空间
    额外空间:显示范围之外,应该额外缓存的空间
newLinearLayoutManager(this{
@Override
protectedintgetExtraLayoutSpace(RecyclerView.Statestate){
returnsize;
    }
};
  1. 减少ItemView监听器的创建
    对ItemView设置监听器,不要对每个item都创建一个监听器,而应该共用一个XxListener,然后根据ID来进行不同的操作,优化了对象的频繁创建带来的资源消耗。
  2. 优化滑动操作
    设置RecyclerView.addOnScrollListener();来在滑动过程中停止加载的操作。
  3. 、刷新闪烁
    调用notifyDataSetChange时,适配器不知道整个数据集中的那些内容以及存在,再重新匹配ViewHolder时会发生闪烁。
    设置adapter.setHasStableIds(true),并重写getItemId()来给每个Item一个唯一的ID
  4. 回收资源
    通过重写RecyclerView.onViewRecycled(holder)来回收资源。

六)Framework 工作方式及原理,Activity 是如何生成一个 view 的,机制是什么

Framework是android 系统对 linux kernel,lib库等封装,提供WMS,AMS,bind机制,handler-message机制等方式,供app使用。
简单来说framework就是提供app生存的环境。

  1. Activity在attch方法的时候,会创建一个phonewindow(window的子类)
  2. onCreate中的setContentView方法,会创建DecorView
  3. DecorView 的addview方法,会把layout中的布局加载进来。

五、线程之间的通信(Handler)

handler整个流程中,主要有四个对象,handler,Message,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建handler对象, 我们通过要传送的消息保存到Message中,handler。post,handler通过调用sendMessage方法将Message发送到MessageQueue中,Looper对象就会不断的调用loop()方法 不断的从MessageQueue中取出Message交给handler进行处理。

一)流程

  1. 通过Message.obtain()从消息池中获取一个消息对象,给相关属性赋值(what等)
  2. Handler对象将消息发送出去(延迟消息是立即发送,延迟处理)
  3. 发送出去的消息会在MessageQueue里进行排序(按照when属性也就是处理时间排序)
  4. Looper会从MessageQueue里取出消息,可能会出现阻塞(消息队列没有消息或者是最近一个消息还没到处理时间),出现阻塞时,Looper陷入沉睡(不浪费内存),到时间了,他会自己唤醒自己
  5. Looper取出消息后就会调用Handler对象的dispatchMessage()方法分发消息(处理消息的方式有三种:Message的callback(为Runnable对象),Handler的callback,和Handler的handleMessage,前两种方式优先级比较高,但是很少用,一般为null)
  6. Handler对象调用handleMessage方法操作 UI(在主线程)
  7. 消息处理完后会从消息队列中移除,Looper会负责将消息清理干净(所有属性回归原值)再放入消息池中以备复用
  8. 发送消息和处理消息是同一个对象

二)异步消息处理机制-handlerThread

  1. HandlerThread本质上是一个线程类,它继承了Thread
  2. HandlerThread有自己的内部Looper对象,可以进行looper循环
  3. 通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务
  4. 优点是不会有堵塞,减少了对性能的消耗,缺点是不能同时进行多任务的处理,需要等待进行处理。处理效率较低。
  5. 与线程池注重并发不同,HandlerThread是一个串行队列,HandlerThread背后只有一个线程。

三)Handler内存泄漏原因及其解决方案

  1. 原因:
    在Activity中使用非静态内部类初始化了一个Handler,此Handler就会持有当前Activity的引用,要一个对象被回收,那么前提它不被任何其它对象持有引用,所以 当我们Activity页面关闭之后,如果 此时Handler 并没有释放Activity的引用,那么Activity不会被回收,当内存不足时,就会导致内存泄露。
    处。
  2. 解决方案:
    a). 尽可能避免使用Handler做延迟操作
    b). 使用静态内部类继承Hanlder(静态内部类不会持有外部对象的引用),如果我们需要在Handler中 使用外部的Activity时,可以定义一个Activity弱引用(WeakReference)对象,弱引用在第二次GC回收时,可以被回收
    c). 在onDestory 时,清除Handler消息队列中的消息removeCallbacksAndMessages(null)

四)Handler的post/send()

  1. 原理
    通过一系列的sendMessageXXX()方法将msg通过消息队列的enqueueMessage()加入到队列中,用户层面发送的都是同步消息,不能发送异步消息,异步消息只能由系统发送。
  2. Handler的post()和postDelayed()方法的异同
    底层都是调用的sendMessageDelayed() post()传入的时间参数为0 postDelayed()传入的时间参数是需要的时间间隔。

五)Handler中Message 消息队列对于延迟消息是如何处理的

在分发消息(sendMessageDelayed)时,会根据延迟消息整理链表,最终构建出一个时间从小到大的序列,然后接受消息(dispatchMessage)。延迟消息的发送是通过循环遍历,不停的获取当前时间戳来与 msg.when 比较,直到小于当前时间戳为止。

六)View中的post和handler的post有什么区别

当项目很小,MainActivity的逻辑也很简单时是看不出啥区别的,但当act的onCreate到onResume之间耗时比较久时(比如2s以上),就能明显感受到这2者的区别了,而且本身它们的实际含义也是很不同的,前者的Runnable真正执行时,可能act的整个view层次都还没完整的measure、layout完成,但后者的Runnable执行时,则一定能保证act的view层次结构已经measure、layout并且至少绘制完成了一次。

六、跨进程通信(AIDL ----> binder)

一)如何跨进程

  1. Binder驱动在内核空间创建一块接收缓存区,
    实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区
  2. 发送进程通过系统调用(copy_from_user)将数据发送到内核缓存区。由于内核缓存区和接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信。

二)binder通信模式:

电话基站:binder驱动
通信录:serviceManager

三)通信流程

  1. 在Remote Service端,定义一个TimeBinder实例。TimeBinder继承自ITime.Stub类,而ITime.Stub类继承自Binder同时实现了ITime接口(关于这些类之间的继承关系后面会说)。这个TimeBinder类就是服务端要返回给客户端的东西。
  2. TimeBinder被包装成Parcel对象在底层传输(Binder通信在底层依赖Binder驱动);当客户端收到传过来的Parcel对象后将其解包,恢复成IBinder对象。为什么恢复成IBinder对象而不是TimeBinder对象呢?如果熟悉Java泛型的话,就会知道,泛型编程会丢失对象原来的类型信息。在Parcel中处理的都是IBinder类型的对象,所以从Parcel中读出来的也是IBinder类型的对象。
  3. 调用ITime.Stub的静态方法asInterface(android.os.IBinder obj),将IBinder对象转换成ITime接口对象。asInterface方法原型为
    public static ITime asInterface(android.os.IBinder obj)
  4. 通过转换得到的ITime接口对象,就可以调用ITime接口中定义的方法了。这样就完成了一次远程调用。

四)MMAP的内存映射原理

内存映射过程是通过系统调用mmap()来实现的 Binder借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。
MMAP内存映射的实现过程,总的来说可以分为三个阶段:

  1. 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域。
  2. 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系。
  3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

七、稳定性(ANR && OOM && 内存泄漏)

一)ANR

ANR->Application Not Responding也就是在规定的时间内,没有响应。

1)三种类型
  1. KeyDispatchTimeout(5 seconds) —主要类型按键或触摸事件在特定时间内无响应
  2. BroadcastTimeout(10 seconds) —BroadcastReceiver在特定时间内无法处理完成
  3. ServiceTimeout(20 seconds) —小概率类型 Service在特定的时间内无法处理完成
2)为什么会超时

事件没有机会处理 & 事件处理超时

3)怎么避免ANR

ANR的关键是处理超时,所以应该避免在UI线程,BroadcastReceiver 还有service主线程中,处理复杂的逻辑和计算而交给work thread操作。

  1. 避免在activity里面做耗时操作,oncreate & onresume
  2. 避免在onReceiver里面做过多操作
  3. 避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。
  4. 尽量使用handler来处理UI thread & workthread的交互

二)OOM

移动端内存有限,手机给每个应用分配大小有限(Google 源生OS分配的内存为16M或者24M,但是不同厂家的ROM会修改)。当你使用的内存空间接近阀值,实例化新对象,需要分配新的内存空间是。就会报Out of Memory。

1)OOM解决方法
a)减少内存对象的占用
  1. ArrayMap/SparseArray代替hashmap
  2. 避免在android里面使用Enum
  3. 减少bitmap的内存占用
    1)inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
    2)decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。
  4. 减少资源图片的大小,过大的图片可以考虑分段加载
b)内存对象的重复利用

大多数对象的复用,都是利用对象池的技术。

  1. listview/gridview/recycleview contentview的复用
  2. inBitmap 属性对于内存对象的复用ARGB_8888/RBG_565/ARGB_4444/ALPHA_8
    这个方法在某些条件下非常有用,比如要加载上千张图片的时候。
  3. 避免在ondraw方法里面 new对象
  4. StringBuilder 代替+

三)OOM

1)内存泄露的根本原因

长生命周期的对象持有短生命周期的对象。短周期对象就无法及时释放。

2)导致内存泄露的情况
a) 静态集合类引起内存泄露
  1. 主要是hashmap,Vector等,如果是静态集合 这些集合没有及时setnull的话,就会一直持有这些对象。
  2. remove 方法无法删除set集 Objects.hash(firstName, lastName);
    经过测试,hashcode修改后,就没有办法remove了。
  3. observer 我们在使用监听器的时候,往往是addxxxlistener,但是当我们不需要的时候,忘记removexxxlistener,就容易内存leak。
b) 广播没有unregisterrecevier
  1. 各种数据链接没有关闭,数据库contentprovider,io,sokect等。cursor
  2. 内部类:
    java中的内部类(匿名内部类),会持有宿主类的强引用this。
    所以如果是new Thread这种,后台线程的操作,当线程没有执行结束时,activity不会被回收。
    Context的引用,当TextView 等等都会持有上下文的引用。如果有static drawable,就会导致该内存无法释放。
  3. 单例
    单例 是一个全局的静态对象,当持有某个复制的类A是,A无法被释放,内存leak。

八、设计模式(MVP && MVVM)

一)MVP

1)整体样式
  1. 各部分之间的通信,都是双向的。
  2. View 与 Model 不发生联系,都通过 Presenter 传递。
  3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
2)优点
  1. 模型与视图完全分离
  2. P层非常容易(适合)做单元测试,Presenter被抽象成接口,可以有多种具体的实现,所以方便进行单元测试
  3. (Presener的复用)一个Presener可以用于多个视图(View),而不需要改变Presenter的逻辑。视图(View)的变化比模型(Model)的变化更频繁的多 ,所以这样超级方便。
  4. (View的复用)View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务逻辑完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做高度可复用的View组件。Activity只处理生命周期的任务,代码变得更加简洁
  5. 可以更高效地使用Model,因为所有的交互都发生在一个地方——Presenter内部
  6. 视图逻辑和业务逻辑分别抽象到了View和Presenter的接口中去,提高代码的可阅读性
3)缺点
  1. Presenter层要处理的业务逻辑过多,复杂的业务逻辑会使P层非常庞大和臃肿。
  2. Presenter中除了业务逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑(没有做到像MVVM数据同步那样一劳永逸),造成Presenter比较笨重,维护起来会比较困难。
  3. 接口及接口中声明的方法粒度不好把控。MVP的架构不是固定的,可能会随着实际需求的不同而有不同的改动。P层就是一个变化比较多的地方,P层的意义是使V与M层解耦。如果粒度太小,那么一旦业务多起来,我们的P层会非常臃肿。而如果粒度太大,那么我们一个P层确实可以达到复用,可却导致了我们不同需求的V层复用同一个P层接口时,要实现好多我们不需要的方法,这就是非常典型的违背了接口隔离,接口的实现类不应该实现没有的方法。而其中有些方法是否会用到以及是否会增加或删减还需要后续进一步确认。
  4. Activity中需要声明大量跟UI相关的方法,而相应的事件通过Presenter调用相关方法来实现。两者互相引用和调用,存在耦合。一旦View层的UI视图发生改变,接口中的方法就需要改变,View层和P层同时都需要修改。

二)MVVM

1)整体样式

M:对应于MVC的M
V:对应于MVC的V
VM:viewModel,是把MVC里的controller的数据的加载、加工功能分离出来
跟MVC一样,主要目的是分离视图(View)和模型(Model)。View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。在MVVM中,数据是核心,由于VIewModel与View之间的双向绑定,操作了ViewModel中的数据,就会同步到DOM,我们透过DOM事件监控用户对DOM的改动,也会同步到ViewModel。

2)优点
  1. 双向绑定技术,当Model变化时,View-Model会自动更新,View也会自动变化。很好做到数据的一致性,不用担心,在模块的这一块数据是这个值,在另一块就是另一个值了。所以 MVVM模式有些时候又被称作:model-view-binder模式。
  2. View的功能进一步的强化,具有控制的部分功能,若想无限增强它的功能,甚至控制器的全部功几乎都可以迁移到各个View上(不过这样不可取,那样View干了不属于它职责范围的事情)。View可以像控制器一样具有自己的View-Model.
  3. 由于控制器的功能大都移动到View上处理,大大的对控制器进行了瘦身。不用再为看到庞大的控制器逻辑而发愁了。
  4. 可以对View或ViewController的数据处理部分抽象出来一个函数处理model。这样它们专职页面布局和页面跳转,它们必然更一步的简化。
3)缺点
  1. 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
  2. 一个大的模块中model也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存就造成了花费更多的内存。
  3. 数据双向绑定不利于代码重用。客户端开发最常用的重用是View,但是数据双向绑定技术,让你在一个View都绑定了一个model,不同模块的model都不同。那就不能简单重用View了。

九、ARouter路由框架

一)初始化做了什么

1)_ARouter init( )
a) 总结

初始化这一操作,表面上是_ARouter ,实则是LogisticsCenter 在帮我们管理逻辑
LogisticsCenter 内部通过先存储SP,然后遍历、匹配(满足条件则添加到具体的集合中,按照文件的前缀不同,将他们添加到路由映射表Warehouse的groupsIndex、interceptorsIndex、providersIndex 中)
具体的路由清单是Warehouse ,不仅保存实例,还给我们提供了缓存。也就是说 同一个目标(RouteMeta、IProvider、IInterceptor)仅会在第一次使用的时候创建一次,然后缓存起来。后面都是直接使用的缓存。

a) 详解

在init方法中会执行LogisticsCenter.init(mContext, executor)这个方法,这个方法中包含了两部分核心代码,
a). 一个是ifelse判断,如果是debug模式或者第一次启动,则走if模块,否则走else模块,在if模块中,获取arouter-compiler生成的文件,然后将该文件,存储在sp中,下次启动应用的时候,直接从sp缓存中读取。
ClassUtils.getFileNameByPackageName(mContext,ROUTE_ROOT_PAKCAGE),这个方法很清楚的扫描指定包路径(ROUTE_ROOT_PAKCAGE)下的所有类,然后放入到Map中。得到每个module里面的/build/generated/source/apt/debug/com/alibaba/android/arouter/routes 下的所有class文件。
b). 另一个是一个for循环,遍历了所有class 找到了 Root 、Interceptors 、Providers 开头的类,通过反射创建实例对象,然后调用 loadInto 方法,将他们按照类型分别存储到Warehouse的对应字段中。

2)_ARouter.afterInit()

实际上是做了一个拦截器的作用

a) 总结

1、如果navigation()不传入Activity作为context,则使用Application作为context
2、内部使用了Intent来进行传递
3、如果在跳转时,设置了flags,且没有设置Activity作为context,则下面的startActivity()方法会发生错误,因为缺少Activity的Task栈;
4、Fragment的判断根据版本不同进行了相应的判断

b) 总结

它实例化了一个InterceptorService。通过调用build方法,最终返回的是一个Postcard对象。这时候往下看源码就不得不提到build和navigation这两个方法了。navigation(clazz)这种方式是属于根据类型查找,而build(path)是根据名称进行查找。
a). build
如果应用中没有实现PathReplaceService这个接口,则pService=null。PathReplaceService可以对所有的路径进行预处理,然后返回一个新的值(返回一个新的String和Uri),即return build(path,extractGroup(path))。这里extractGroup(path)核心逻辑是path.substring(),这个方法主要是获取分组名称。切割path字符串,默认为path中第一部分为组名。这就证明了如果我们不自定义分组,默认就是第一个分号的内容。
b). navigation(Postcard.java)
实际上调用的是_ARouter.navigation()方法,如果(两个)路径没写对,ARouter会Toast提示客户端,路径不对。所以说,interceptorService实例化对象的时机,是在_ARouter这类中的afterInit( )进行实例化的。如果(两个)路径匹配的话,会执行LogisticsCenter.completion( Postcard ))。

二)简单说一下使用ARouter跳转到一个Activity的流程

1.写一个BaseConstant类,用于存放公共字段。

public class BaseConstant {
    public static final String AROUTER_PATH_MODULE1_TEST1 = "/module1/Module1Test1Activity";
    public static final String AROUTER_PATH_MODULE1_TEST2 = "/module1/Module1Test2Activity";
    public static final String AROUTER_PATH_MODULE1_WEBVIEW = "/module1/TestWebViewActivity";
    public static final String AROUTER_PATH_MODULE1_TEST_INTERCEPTOR = "/module1/TestInterceptorActivity";
}

test1的Module1Test1Activity.java

@Route(path = BaseConstant.AROUTER_PATH_MODULE1_TEST1)
public class Module1Test1Activity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_module1_test1);
    }
}

app的MainActivity.java(只有一个跳转,其他略过)

        //不携带参数跳转
        findViewById(R.id.btn1).setOnClickListener(v -> {
            // 1. 应用内简单的跳转
            ARouter.getInstance()
                    .build(BaseConstant.AROUTER_PATH_MODULE1_TEST1)
                    .navigation();
        });

2.运行项目,会生成一些ARouterGroupxxxx、ARouterProvidersxxxx、ARouterRootxxxx三个类。这里主要看主要看ARouterGroupxxxx类,这里把注册的类的路径放到了一个map中。
3.紧接着就是开始做初始化了。此时会先去执行_ARouter.init(),再然后会执行_ARouter.afterInit()。
4.然后就是跳转。

【JAVA篇】

一、常见区别

一)==和equals和hashCode的区别

  1. 基本数据类型的==比较的值相等.
  2. 类的==比较的内存的地址,即是否是同一个对象,在不覆盖equals的情况下,同比较内存地址,原实现也为 == ,如String等重写了equals方法.
  3. hashCode也是Object类的一个方法。返回一个离散的int型整数。在集合类操作中使用,为了提高查询速度。(HashMap,HashSet等比较是否为同一个)
  4. 如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
  5. 如果两个对象不equals,他们的hashcode有可能相等。
  6. 如果两个对象hashcode相等,他们不一定equals。
  7. 如果两个对象hashcode不相等,他们一定不equals。

二)int与integer的区别

  1. int 基本类型
  2. integer 对象 int的封装类

三)String、StringBuffer、StringBuilder区别

  1. String:字符串常量 不适用于经常要改变值得情况,每次改变相当于生成一个新的对象
  2. StringBuffer:字符串变量 (线程安全)
  3. StringBuilder:字符串变量(线程不安全) 确保单线程下可用,效率略高于StringBuffer

四)进程和线程的区别

  1. 进程是cpu资源分配的最小单位,线程是cpu调度的最小单位。
  2. 进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。
  3. 一个进程内可拥有多个线程,进程可开启进程,也可开启线程。
  4. 一个线程只能属于一个进程,线程可直接使用同进程的资源,线程依赖于进程而存在。

五)final、finally和finalize的区别

  1. final:修饰类、成员变量和成员方法,类不可被继承,成员变量不可变,成员方法不可重写
  2. finally:与try…catch…共同使用,确保无论是否出现异常都能被调用到
  3. finalize:类的方法,垃圾回收之前会调用此方法,子类可以重写finalize()方法实现对资源的回收

六)Serializable 和Parcelable 的区别

  1. Serializable Java 序列化接口 在硬盘上读写 读写过程中有大量临时变量的生成,内部执行大量的i/o操作,效率很低。
  2. Parcelable Android 序列化接口 效率高 使用麻烦 在内存中读写(AS有相关插件 一键生成所需方法) ,对象不能保存到磁盘中
  3. Parcelable 消耗内存小
  4. 网络传输用Serializable 程序内使用Parcelable
  5. Serializable 将数据持久化方便
  6. Serializable 使用了反射 容易触发垃圾回收 比较慢

七)list、set和map的区别

  1. Set是最简单的一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。 Set接口主要实现了两个实现类:HashSet: HashSet类按照哈希算法来存取集合中的对象,存取速度比较快
  2. TreeSet :TreeSet类实现了SortedSet接口,能够对集合中的对象进行排序。
  3. List的特征是其元素以线性方式存储,集合中可以存放重复对象。
  4. ArrayList() : 代表长度可以改变得数组。可以对元素进行随机的访问,向ArrayList()中插入与删除元素的速度慢。
  5. LinkedList(): 在实现中采用链表数据结构。插入和删除速度快,访问速度慢。
  6. Map 是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口 从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
  7. HashMap:Map基于散列表的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。
  8. LinkedHashMap: 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。
  9. TreeMap : 基于红黑树数据结构的实现。查看“键”或“键值对”时,它们会被排序(次序由Comparabel或Comparator决定)。TreeMap的特点在 于,你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
  10. WeakHashMao :弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。

八) ArrayMap和HashMap的区别

  1. 存储方式不同
    HashMap内部有一个HashMapEntry<K, V>[]对象,每一个键值对都存储在这个对象里,当使用put方法添加键值对时,就会new一个HashMapEntry对象,
  2. 添加数据时扩容时的处理不一样,进行了new操作,重新创建对象,开销很大。ArrayMap用的是copy数据,所以效率相对要高。
  3. ArrayMap提供了数组收缩的功能,在clear或remove后,会重新收缩数组,是否空间
  4. ArrayMap采用二分法查找;

九) ArrayMap和HashMap的区别wait和seelp方法的区别

Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。

【Kotlin篇】

一、协程

launch方法启动协程有三个参数, 分别是协程上下文、协程启动模式、协程体

一)协程上下文

协程上下文用CoroutineContext表示,kotlin中 比较常用的Job、协程调度器(CoroutineDispatcher)、协程拦截器(ContinuationInterceptor)等都是CoroutineContext的子类,即它们都是协程上下文

二)协程启动模式

  1. DEFAULT:
    立即执行协程体,不需要手动调用join或start等方法,而是在调用launch方法的时候就会自动执行协程体的代码
  2. LAZY:
    只有在需要的情况下才执行协程体,一定要手动调用join或start等方法,否者协程体不会执行
  3. ATOMIC:
    立即执行协程体,但在开始运行之前无法取消, 即开启协程会无视cancelling状态。此启动模式的协程体 即使调了cancel方法 也一定会执行,因为开启协程会无视cancelling状态;上面的example只打印了一句话,是因为执行delay(1000)的时候 发现协程处于关闭状态, 所以出现了JobCancellationException异常,导致下面的代码没有执行,如果 delay(1000) 这句代码用 try catch 捕获一下异常,就会继续执行下面的代码。
  4. UNDISPATCHED:
    立即在当前线程执行协程体,直到第一个 suspend 调用 挂起之后的执行线程取决于上下文当中的调度器了。0 和 1 的执行线程是一样的,当执行完delay(1000), 后面的代码执行线程取决于Dispatchers.Default调度器指定的线程,所以 2 在另一个线程中执行。

二)协程调度器

协程调度器其实也是协程上下文
协程调度器是用来指定协程代码块在哪个线程中执行,kotlin提供了几个默认的协程调度器,分别是Default、Main、Unconfined, 并针对jvm, kotlin提供了一个特有的IO调度器

  1. Dispatchers.Default:指定代码块在线程池中执行
  2. Dispatchers.Main:指定代码块在main线程中执行(针对Android就是ui线程)
  3. Dispatchers.Unconfined:没有指定协程代码快在哪个特定线程中执行,即当前在哪个线程,代码块中接下来的代码就在哪个线程中执行(即一段协程代码块 由于启动了子协程 导致切换了线程, 那么接下来的代码块也是在这个线程中执行)
  4. Dispatchers.IO:它是基于 Default 调度器背后的线程池,并实现了独立的队列和限制,因此协程调度器从 Default 切换到 IO 并不会触发线程切换

三)协程作用域

  1. GlobeScope:
    启动的协程会单独启动一个作用域,无法继承外面协程的作用域,其内部的子协程遵从默认的作用域规则
  2. coroutineScope:
    启动的协程会继承父协程的作用域,其内部的取消操作是双向传播的,子协程未捕获的异常也会向上传递给父协程
  3. supervisorScope:
    启动的协程会继承父协程的作用域,他跟coroutineScope不一样的点是 它是单向传递的,即内部的取消操作和异常传递 只能由父协程向子协程传播,不能从子协程传向父协程

MainScope 就是使用的supervisorScope作用域,所以只需要子协程 出错 或 cancel 并不会影响父协程,从而也不会影响兄弟协程

四)协程异常传递模式

协程的异常传递跟协程作用域有关,要么跟coroutineScope一样双向传递,要么跟supervisorScope一样由父协程向子协程单向传递

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值