android app面试整理 2020-5

1、什么是线程池

一个对线程进行统一管理,统一调度的的工具。他可以重用存在线程,减少线程的创建和销毁,从而减少资源的消耗。还可以控制最大并发线程数,提高系统资源的使用率,避免堵塞。可以根据需求灵活的自定义各种线程池,以满足需要。
(1).系统自带的4种线程池

  • FixThreadPool:一个固定线程数的线程池,采用了一个基于链表的无边界队列
  • SingleThreadPool: 一个单线程的线程池,采用无界队列的线程池
  • ScheduleThreadPool:一个核心线程数固定,非核心线程池数量为Integer.MAX_VALUE个大小的,可以执行延迟或周期性任务的线程池
  • CachedThreadPool:一个核心线程池数量为Integer.MAX_VALUE,任务对列为空的线程池

2、sleep和wait的区别

  • sleep来自于Thread wait来自于object
  • 两者都会对线程造成阻塞,但是sleep不会释放持有的锁,wait会释放持有的锁,这样其他的线程依然可以调用同步方法
  • sleep可以在任何地方调用,但是wait只能在同步代码块或者同步方法中调用
  • sleep需要加try catch捕获异常,wait不需要

3、runnable和callable、Future、FutureTask的区别

runnable和callable都是接口,他们实现可以作为线程的执行任务。但是callable有返回值,可以返回执行的结果,runnable不行。Future也是一个接口,像是对callable和runnale的封装,可以获取任务的状态,及返回值。 FuntureTask是一个实现了runnableFuture接口的类,runnableFuture又继承了runnable和Future接口。

4、多线程同步异步和IO同步异步的区别

多线程同步异步:多个线程同时访问一个数据,每个线程都能同时获取到数据就是异步,如果加上了同步锁,同一时间只有一个线程能够访问到数据,就是同步
IO同步异步(IO阻塞):同步是指去请求数据的时候,如果没有返回数据就一直等待下去。异步是指:去请求数据的时候,如果没有返回数据,就继续往下走,数据返回后再通过回调等方法通知去处理

5、handler的机制、一个线程能否创建几个handler,同一个线程多个handler发送消息的时候是怎么区分mesage的?

可以从源码的角度去分析handler的机制,在主函数的入口,ThreadActivity中有一个main函数,里面先执行了一个Loop.prepare方法,这个方法里面创建了一个looper,Looper的构造方法里面又创造了一个messageQueue,所以一个线程对应一个Looper和一个messageQueue。接着main函数里面又执行了Loop.loop方法,这个loop方法里面用while创造了一个无限循环,这个循环里面会不断的从messageQueue里面拿消息,拿到消息会通过dispatchMessage将消息分发下去。
可以创建多个handler,一个线程只有一个loop一个messageQueue,所有的message都是发送在这一个messageQueue的,但是在sendmessage的时候,message里面也包含了对应handler的引用,messageQueue在遍历取出消息的时候会通过message中的handler引用调用dispatchMessage分发给相应的handler

6、AsyncTask的缺点

(1)和handler一样会造成内存泄漏
(2)内置的线程池线程数固定,CORE_POOL 2-4个,MAXIMUM_POOL_SIZE cpu*2+1个
(3)结果丢失。屏幕旋转或者app被kill,导致activity被finish。AsyncTask中持有的activity依然是之前的,导致ui不进行更新

7、broadcast的种类

普通广播、有序广播(根据优先级决定先后顺序)、本地广播、粘性广播(安全性不高,已经不推荐使用)

8、Service的启动方式有哪些

startService 通过这种方式启动的service,只用通过调用stopService停止,生命周期是onCreate,onStartCommend,onDestroy
bindService通过这种方式启动的service是和activity生命周期绑定的,activity退出时需要在onDestroy中调用unBindService,生命周期是onCreate onBind unBind onDestroy

9、activity、fragment的生命周期

activity的生命周期是onCreate - onStart - onResume - onPause - onStop。如果activity 是singleTask或者singleTask就会走onNewIntent - onResume
fragment生命周期是onAttach - onCreate- onCreateView - onActivityCreate - onStart - onResume - onPause - onStop - onDestroyView - onDestroy - onDetach
onAttach():执行该方法时,Fragment与Activity已经完成绑定,该方法有一个Activity类型的参数,代表绑定的Activity,这时候你可以执行诸如mActivity = activity的操作。
onCreate():初始化Fragment。可通过参数savedInstanceState获取之前保存的值。
onCreateView():初始化Fragment的布局。加载布局和findViewById的操作通常在此函数内完成,但是不建议执行耗时的操作,比如读取数据库数据列表。
onActivityCreated():执行该方法时,与Fragment绑定的Activity的onCreate方法已经执行完成并返回,在该方法内可以进行与Activity交互的UI操作,所以在该方法之前Activity的onCreate方法并未执行完成,如果提前进行交互操作,会引发空指针异常。
onStart():执行该方法时,Fragment由不可见变为可见状态。
onResume():执行该方法时,Fragment处于活动状态,用户可与之交互。
onPause():执行该方法时,Fragment处于暂停状态,但依然可见,用户不能与之交互。
onSaveInstanceState():保存当前Fragment的状态。该方法会自动保存Fragment的状态,比如EditText键入的文本,即使Fragment被回收又重新创建,一样能恢复EditText之前键入的文本。
onStop():执行该方法时,Fragment完全不可见。
onDestroyView():销毁与Fragment有关的视图,但未与Activity解除绑定,依然可以通过onCreateView方法重新创建视图。通常在ViewPager+Fragment的方式下会调用此方法。
onDestroy():销毁Fragment。通常按Back键退出或者Fragment被回收时调用此方法。
onDetach():解除与Activity的绑定。在onDestroy方法之后调用。

10、activity的4种启动模式

activity一共有4种启动模式 1、标准启动模式也就是默认启动模式,standard,启动该模式的activity,每启动一次都会创建一个activity的实例放入任务栈中。 2、singleTop 栈顶复用模式,如果栈顶已经是我们要启动的activity实例的话,就直接复用,不在重新创建。3、singleTask 栈内复用模式,如果要启动的activity在任务栈里面已经有一个实例,就直接复用,并将该任务栈之上的activity实例弹出 4、singleInstance 创建一个新的任务栈,并将该实例放到该任务栈中,一个任务栈只会存放一个实例

11、线程同步的几种方式

(1)使用synchronized 修饰方法或者代码块 保证共享数据的可见性、原子性、有序性
(2)使用volatile修饰共享数据 保证数据的可见性
(3)使用reentrantLock 一个比synchronized更灵活的同步方法,它可以提供公平和非公平锁,轮询锁,定时锁。需要手动上锁和释放锁
(4)ThreadLocal 为每个线程创建一个数据的副本,这样每个线程就是对独立的一个数据做修改,不会对其他线程造成影响

12、rxjava原理及优点

一个基于观察者模式的异步操作库,他可以用及其简单的逻辑去处理繁琐的异步任务。rx主要有4个概念observable(被观察者),observer(观察者),subscribe(订阅),event(事件),rxjava还提供了非常丰富的操作符,map、flatmap、concat、zip、filter等,可以满足我们开发过程中的各种需求。整个rxjava的订阅流程可以用3条流来描述,一是构建流:使用Observable.create方法创建一个observable对象,接下来无论是调用操作符还是切换线程每一步都会返回一个新的observable对象,这样可以避免使用回调的写法,保证我们的代码是一个链式的结构,这种写法和builder模式很像,但是builder是返回同样的对象,rxjava是返回一个全新的对象。接下来是订阅流,就是我们调用subscribe进行订阅的时候,是从下往上依次调用构建流里面创建的observable对象,最后调用了observable中的subscribe方法,这样观察者和被观察者就联系了起来。最后是观察者回调流,当事件发生以后,会通过这个流依次通知给各个观察者。
优点:简化了逻辑,模块进行了解耦,不会出现各种嵌套和回调。代码也变得更为简洁。提供了强大的操作符,线程切换也很方便。统一的异常处理。

RxJava内存泄漏
RxJava内存泄漏,是因为事件在传递过程中还没有被处理完,就关闭了当前acitvity。内部类持有外部类的引用,就容易造成内存泄漏,一般有3中解决方案。
1、在ondestroy中手动dispose(),切断订阅,如果rxjava比较多,就都需要手动去处理。比较麻烦
2、使用RxLifecycle框架。但是需要继承RXActivity或者RXFragment,不方便
3、AutoDispose框架,uber开发的,不需要继承,直接通过as

13、dagger原理

dagger是一个依赖注入的框架,他的作用是降低代码之间的耦合性
比如创建一个对象我们之前使用new 的方式。但是如果对象的构造方法入参做了修改,所有通过new这个形式创建对象的地方也都需要做修改。
如果使用这个对象的地方和这个对象的类之间能够有一层专门用来处理对象的创建那就很好了,如果对象的构造方法做了修改,之间处理这一层就可以了,没必要找到所有使用的地方去修改。
dagger的使用是先给需要创建对象的类的构造方法加个Inject的注解。然后使用的地方比如activity之间声明这个对象的引用并且加上inject的注解。然后创建一个module的类,有个provide的方法,可以在这里进行new对象的操作,然后创建一个component的接口,里面有一个inject的方法,入参是要注入到那个地方的类
。build后component会生成一个dagger component的类,module也会生成一个factory的类,如果module里面的provider返回的不是创建的对象的话,对象也会创建一个factory类。module的factoy里面有一个get方法,里面最终会调用到module里面的provider标注的方法,并且返回,这里返回的就是需要生成的对象。然后再daggerComponent里面,调用builder里面的build方法,里面会调用daggerComponent的构造方法,构造方法里面会会去调用module中的get方法,拿到对象的实例,在通过inject方法,将对象指向inject的参数所指的对象里面去。

14、eventBus原理

一个订阅\发布事件总线的框架,基于观察者模式
通过EventBus.getDefault(),获取EventBus单例。register方法里面通过反射获取到订阅者的信息主要是注解的Subscribe方法,EventBus里面有维护一个map,每register一次就传入一次订阅者的信息,post发送信息后遍历map,调用注解的Subscribe方法。自己也可以通过handler制作一个简单的EventBus。

15、view的绘制流程

view的绘制流程是从viewRoot开始的,viewRoot是一个view树的管理者,在ActivityThread中的handlerResumeActivity方法中,初始化,调用了viewRoot中的RequestLayout()方法,最终调用的是performTraversals()方法,这个方法中主要调用了performMeasure,performLayout、performDraw这3个方法,performMeasure中调用到了decorview的Measure方法,decorview没有重写这个方法 他继承自framelayout,framelayout也没有重写这个方法,所以他最终调用了父类view的measure方法,measure是一个final修饰的方法,不能被重写,里面调用到了onMeasure这个方法,decorview重写了这个方法,然后又调用了Framelayout中的onMeasure方法,这个方法里面遍历了子view,调用他们的measure方法,measure会去测量所有的view需要的最大高度和宽度,从而确定view树的高度和宽度,。layout的流程和measure差不多,但是他是先确定父view的位置再来确定子view在父view中的位置。draw就是用来绘制view的

16、RecyclerView和ListView区别

recycleView比listView更灵活,它可以实现横向、竖向、瀑布流的布局进行展示,recycleView会强制要求使用viewholder,listView并不强制。recycleView使用的是4级缓存,listview使用的是2级缓存。但是recycleview需要自己实现onclick点击事件。
RecyclerView的缓存机制是怎样的?

  1. 第一步先从getChangedScrapViewForPosition(position)方法中找需要的视图,但是有个条件mState.isPreLayout()要为true,这个一般在我们调用adapter的notifyItemChanged等方法时为true,其实也很好理解,数据发生了变化,viewholder被detach掉后缓存在mChangedScrap之中,在这里拿到的viewHolder后续需要重新绑定。

  2. 第二步,如果没有找到视图则从getScrapOrHiddenOrCachedHolderForPosition这个方法中继续找。这个方法的代码就不贴了,简单说下这里的查找顺序:

首先从mAttachedScrap中查找
再次从前面略过的ChildHelper类中的mHiddenViews中查找
最后是从mCachedViews中查找的

  1. 第三步, mViewCacheExtension中查找,我们说过这个对象默认是null的,是由我们开发者自定义缓存策略的一层,所以如果你没有定义过,这里是找不到View的。

  2. 第四步,从RecyclerPool中查找,前面我们介绍过RecyclerPool,先通过itemType从SparseArray类型的mscrap中拿到ScrapData,不为空继续拿到scrapHeap这个ArrayList,然后取到视图,这里拿到的视图需要重新绑定。

  3. 第五步,如果前面几步都没有拿到视图,那么调用了mAdapter.createViewHolder(RecyclerView.this, type)方法,这个方法内部调用了一个抽象方法onCreateViewHolder,是不是很熟悉,没错,就是我们自己写一个Adapter要实现的方法之一。

17、界面是怎么显示到屏幕上的

在activity的setContentView中设置布局文件id,最终调用,通过xml解析器将布局转换为
LayoutInflater.from(mContext).inflate(resId, contentParent);

18、FragmentPageAdapter与 FragmentStatePageAdapter区别

FragmentStatePagerAdapter会销毁不需要的Fragment,在销毁的时候,会通过onSaveInstanceState方法来保存Fragment中的Bundle信息。
FragmentPageAdapter不会销毁,会将每个fragment保存FragmentManager
因此FragmentStatePageAdapter适用于Fragment较多的情况,而FragmentPageAdapter则适用于固定的,少量的Fragment情况

19、64k问题出现的原因及解决方案

64k问题是指一个项目中方法数超过了64k个,超过了dalvik规定的可执行文件的数量,从而导致编译器报错。可以引用google的官方方案,配置multidex。multidex是将一个dex文件拆分成了一个主dex文件和多个从dex文件,这样可以避免一个dex文件中方法数超过64k个,但是也引入了新的问题,在android4.0及以前的版本上会出现启动失败的情况,4.0之前对堆内存有限制,dex过大,类加载的时候可能会超过堆内存限制,从而导致无法启动,其次multidex还会导致应用启动速度变慢应用首次启动时dalvik虚拟机会对所有.dex文件zhixingwdexopt操作,生成odex文件,如果从dex文件过大可能导致anr。

最好的方案是合理选择第三库,不要为了一小部分功能而选一个很大的库。不要重复依赖。如果项目很大也可以采用插件式开发框架

20、ContentProvider如何限制对方的访问

maniffest中修改exported限制contentProvider对外的暴露,也可以在contentProvider中设置权限或者更具体的读写权限,只有配置了这些权限的app才能访问contentProvider。或者配置url的访问权限,可以配置只能访问某一个路径下数据。

21、ContentProvider接口分别运行在哪个线程中,是否线程安全

onCreate是UI线程的 ,增删查改方法时候子线程的。非线程安全,需要自己去同步线程,但是如果是对sqlite做操作,sqlite自己实现了线程安全,ContentProvider不需要做其他处理

22、invalidate()方法 requestLayout()方法的区别

invalidate()方法:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”
视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。
requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。

说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制
任何视图包括该调用者本身。

23、反射能不能获取泛型的实际类型?什么情况下能获取

在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。
这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制
反射优点,在运行期可以动态创建对象。增加代码的灵活性和复用性。
缺点:安全性降低,性能降低。破坏了代码的封装。
使用:多个类实现了同一个接口,在factory模式创建对象的时候,为了避免多个if else,可以通过Class.forName(包名.类名)的形式创建。提高代码的复用

可以获取泛型类型的实际类型,无论是获取泛型类或者泛型方法的泛型类型,都是先拿到泛型类或者泛型方法的入参在去判断是不是参数化类型,是的或就可以拿到实际的类型。

24、嵌套滑动机制

滑动嵌套有3种方式:
1、事件拦截 比较灵活,但是实现复杂
2、子view实现nestedScrollingChild,父view实现nestedScrollingParent,子view每次滑动时都传递给父view,询问是否需要消耗该次滑动。
3[、oordinatorLayout 及 Behavior

25、app启动黑屏的原因

app冷启动,从启动到展示布局,有一个过程。也就是contentView还没有加载上来。这时候会显示白屏或者黑屏。

26、IPC有哪几种,Binder的原理

Bundle 四大组件之间的传值,但是只能传递基本数据类型,使用简单

  • 共享文件
  • Messenger 一个基于AIDL实现的轻量级的IPC方式
  • AIDL 可以跨进程访问另外一个app
  1. Messenger本质也是AIDL,只是进行了封装,开发的时候不用再写.aidl文件。
    结合我自身的使用,因为不用去写.aidl文件,相比起来,Messenger使用起来十分简单。但前面也说了,Messenger本质上也是AIDL,故在底层进程间通信这一块,两者的效率应该是一样的。

  2. 在service端,Messenger处理client端的请求是单线程的,而AIDL是多线程的。
    使用AIDL的时候,service端每收到一个client端的请求时,就会启动一个线程(非主线程)去执行相应的操作。而Messenger,service收到的请求是放在Handler的MessageQueue里面,Handler大家都用过,它需要绑定一个Thread,然后不断poll message执行相关操作,这个过程是同步执行的。

  3. client的方法,使用AIDL获取返回值是同步的,而Messenger是异步的。
    Messenger只提供了一个方法进行进程间通信,就是send(Message msg)方法,发送的是一个Message,没有返回值,要拿到返回值,需要把client的Messenger作为msg.replyTo参数传递过去,service端处理完之后,在调用客户端的Messenger的send(Message msg)方法把返回值传递回client,这个过程是异步的,而AIDL你可以自己指定方法,指定返回值,它获取返回值是同步的。

  • contentProvider内容提供者


    image.png

26、Binder原理 (socket)

android为了安全及稳定性着想,让每一个app都运行在一个独立的虚拟机中,也就是每一个app都运行在一个进程之中,这些进程都有属于自己的内存空间,并且不能访问其他的进程。binder是一个基于CS架构的IPC机制,他是连接客户端 服务端 和serviceManager的桥梁。service向binder发起注册的请求,binder将请求发送给service manager进程,serviceManager进程添加service进程。client向binder发起获取服务的请求,binder将请求发送给service manager,service manager找到对应的的service后发送给binder并返回给client。这样client就和service建立了连接。当client向serivce发送数据的时候,是将数据发送到client的共享空间中,binder拿到数据后去serviceManager找到对应的serivce并发送给service的共享空间。

28、smartRefreshLayout、swiperRefreshLayout、PullToRefreshLayout原理及区别

29、OkHttp工作原理

30、谈谈对多态的理解

一件事物的多种形态。变量的引用及这个变量调用的方法在编译期是不确定的,只有在运行期才能确定。这也是java不是一门完全的静态语言的理由。多态的前提条件是

  • 继承
  • 子类重写父类的方法
  • 向上转型
    多态的好处就是可以让我们不针对具体实现去编程,而是针对接口或者继承去编程,这样可以提高代码的解耦。

31、java为什么有装箱和拆箱

是java早年设计缺陷。基础类型是数据,不是对象,也不是Object的子类。需要装箱才能和其他Object的子类共用同一个接口。

32、什么时候会使用HashMap?他有什么特点?

是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。

33、 你知道HashMap的工作原理吗?

HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。它是基于哈希表的 Map 接口的非同步实现。
他是基于hashing算法的原理,通过put(key,value)和get(key)方法储存和获取值的。

存:我们将键值对K/V 传递给put()方法,它调用K对象的hashCode()方法来计算hashCode从而得到bucket位置,之后储存Entry对象。(HashMap是在bucket中储存 键对象 和 值对象,作为Map.Entry)
取:获取对象时,我们传递 键给get()方法,然后调用K的hashCode()方法从而得到hashCode进而获取到bucket位置,再调用K的equals()方法从而确定键值对,返回值对象。

碰撞:当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’就会发生。如何解决,就是利用链表结构进行存储,即HashMap使用LinkedList存储对象。但是当链表长度大于8(默认)时,就会把链表转换为红黑树,在红黑树中执行插入获取操作。

扩容:如果HashMap的大小超过了负载因子定义的容量,就会进行扩容。默认负载因子为0.75。就是说,当一个map填满了75%的bucket时候,将会创建原来HashMap大小的两倍的bucket数组(jdk1.6,但不超过最大容量),来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

为什么扩容要以2的倍数扩容?
答案当然是为了性能。在HashMap通过键的哈希值进行定位桶位置的时候,调用了一个indexFor(hash, table.length);方法。

/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length-1);
}

&为位与运算符
可以看到这里是将哈希值h与桶数组的length-1(实际上也是map的容量-1)进行了一个与操作得出了对应的桶的位置,h & (length-1)。

34、app保活

进程的启动分冷启动和热启动,当用户退出某一个进程的时候,并不会真正的将进程退出,而是将这个进程放到后台,以便下次启动的时候可以马上启动起来,这个过程名为热启动,这也是Android的设计理念之一。这个机制会带来一个问题,每个进程都有自己独立的内存地址空间,随着应用打开数量的增多,系统已使用的内存越来越大,就很有可能导致系统内存不足。为了解决这个问题,系统引入LowmemoryKiller(简称lmk)管理所有进程,根据一定策略来kill某个进程并释放占用的内存,保证系统的正常运行
LMK基本原理
所有应用进程都是从zygote孵化出来的,记录在AMS中mLruProcesses列表中,由AMS进行统一管理,AMS中会根据进程的状态更新进程对应的oom_adj值,这个值会通过文件传递到kernel中去,kernel有个低内存回收机制,在内存达到一定阀值时会触发清理oom_adj值高的进程腾出更多的内存空间

  • Activity提权
  • Service提权
  • 广播拉活
  • “全家桶”拉活
  • Service机制(Sticky)拉活
  • 账户同步拉活
  • JobScheduler拉活
  • 推送拉活
  • Native拉活(NDK)
  • 双进程守护(JobScheduler和两个Service结合用)

35、tcp和udp区别

1、tcp是基于连接的,可靠性高;udp是基于无连接的,可靠性较低;

2、由于tcp需要有三次握手、重新确认等连接过程,实时性差;同时过程复杂,也使其易于被攻击;而udp无连接,因而实时性较强,也稍安全;

3、在传输相同大小的数据时,tcp报头20字节;udp首部开销只有8个字节,tcp报头比udp复杂,故实际包含的用户数据较少。tcp无丢包,而udp有丢包,故tcp开销大,udp开销较小;

4、每条tcp连接只能是点到点的;udp支持一对一、一对多、多对一、多对多的交互通信。

36、surfaceview为什么可以在子线程中进行更新?

最主要的原因是surfaceview中有一个属于自己的surface,而其他的view都是共享一个surface。surfaceview的ui是通过自己的surface对应的canvas通过surfaceflinger绘制到自己的layer上的,而且surfaceview1的刷新不会导致整个窗口的刷新,只会刷新自己的这个小窗口,所以刷新效率更高。

39、并发和并行
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

多线程是并发概念

37、gc工作原理

jvm的模型中,线程间数据共享的区域有堆和方法区,堆内存里存储的一个类实例化后对象的数据。方法区主要存储的一个类的信息和一个常量池。线程间数据不共享的区域有虚拟机栈和程序计数器、本地方法栈。一个对象是否存活有两种算法,一个是引用计数法:一个对象被引用一次就+1,清除一次引用就-1,如果计数是0,这个对象就可以被判断为是死亡状态。引用计数的缺点是如果是循环引用就无效了,还有一个可达性分析,就是对象引用的过程当做一个有向图,通常将一个线程的对象当做根节点。从这个对象开始依次往前找复制引用的过程,如果发现这个对象最终无法到达根节点,就判断这个对象死亡了。gc的时机主要有两种,一种是主动调用,一种是内存不足系统主动调用gc进行回收。gc回收的算法有下面这几种,1、标记清除法:第一阶段会先遍历一次所有对象,标记那些对象已经死亡那些存活,第二阶段会再遍历一次,将死亡的对象进行回收。 2、标记整理法,在同一块区域内,将存活的对象放到内存的另一端,然后对其余的内存做清除。3、复制法:将内存平均分成了两部分,一部分内存满了的时候就将所有存活的对象复制到另一个内存中,然后清空之前的内存。 4、分代收集法 是对上面3个方法的一个合集。将堆内存分成了新生代和老年代,对象最开始是创建到新生代的,如果新生代的对象历经几次gc还是存活就会移到老年代。新生代又分为eden和survivor to和survivor from3个区域。对象的创建都是在eden,如果eden满了就会将存活的对象移from区域,并将eden清除,如果eden再慢了就会将eden和from存活的对象移到to,如果eden再慢了就会将eden和to存活的对象移到from,反正每一个词eden满了触发了younggc就会将所有存活的对象复制到另一个空的区域。老年代因为对象周期比较长不会频繁的销毁,所以采用的是标记清除法和标记整理法。然后是方法区的回收,方法区主要回收的是常量池里面的常量和已经加载的类的信息。采用的标记清除法和标记整理法。常量如果没有被引用就会被回收,一个类所有的实例被回收,对象的class对象和classloader被回收,这个类就会从方法区中进行回收。

38、view的绘制原理

说view的绘制流程可以先说一下activity的窗口结构。每个ativity都有一个window,window是一个抽象类,他有一个实现类是phonewindow,phonewindow里面有一个decorView,这个decorView其实是一个framelayout,里面包含了两个Framelayout,上面是一个titlebar,下面是一个contentview,我们在activity中通过setContentView设置的布局就会绘制在这个contentview上面
view绘制流程的入口是viewroot中的performTraversals开始的。里面调用了performMeasure performLayout performDraw方法,这3个方法又分辨调用了measure layout draw这3个方法,measure里面遍历调用了子view的Measure方法,measure方法是final修饰,无法被重写,measure里面又调用了onmeasure方法,这个方法里面又一个参数,MeasureSpec ······。这个measure会确定每个子view的高度了宽度。从而确定整个view树的最大高度了宽度。然后是layout方法,和measure方法一样,会遍历的调用子view的onlayout方法,从而确定子view的位置。最后是ondraw方法,ondraw就是用来绘制view的。默认是空,需要子view重写

39、volatile和synchronized的区别

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

40、synchronize 为什么性能不好

这里涉及到了java 的线程模型 (1:1 用户线程和内核线程)线程上下文切换导致性能损耗因为用户态切换到内核态,会增加操作系统的开销

41、LeakCanary 原理

LeakCanary实现内存泄漏的主要判断逻辑是这样的。当我们观察的Activity或者Fragment销毁时,我们会使用一个弱引用去包装当前销毁的Activity或者Fragment,并且将它与本地的一个ReferenceQueue队列关联。我们知道如果GC触发了,系统会将当前的引用对象存入队列中。
如果没有被回收,队列中则没有当前的引用对象。所以LeakCanary会去判断,ReferenceQueue是否有当前观察的Activity或者Fragment的引用对象,第一次判断如果不存在,就去手动触发一次GC,然后做第二次判断,如果还是不存在,则表明出现了内存泄漏。

42、解决UI卡顿

不要在主线程做耗时操作
view树不要太深 标签 标签 标签
draw里面不要创建对象
减少过渡绘制 比如不要在看不到的view设置背景色 :
用Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用getWindow().setBackgroundDrawable(null);或者在theme中添加android:windowbackground=”null”;

43、app启动优化

从两个方面入手,一、视觉优化 代码优化
视觉优化:
将activity的主题中设置背景透明,但会引发问题,在其他app上调用我们的app会导致其他app不走onstop生命周期
在splash页面添加主题,背景设置为splash背景

代码优化:减轻applaction的负担,不要将太多的初始化放在这里,能够放在子线程的就放在子线程,activity不会立即用到的可以延迟加载。
利用splash的显示时间做一些初始化
布局进行优化,view树不要太深

44、Java 的char是两个字节,如何存UTF-8字符?

UTF-8只能存1-4个字节
char不存UTF-8的字节,而是UTF-16 (字符用两个字节或四个字节表示)
Unicode通用字符集占两个字节
Unicode是字符集,不是编码,ASCII也是字符集
java String的length不是字符数

45、字节和字符的区别

字节是一种计量单位,我们可以用他表示数据大小,一个字节是用8个2进制表示,所以一个字节有2的8次方256种形式。
字符是指计算机中使用的文字或符号,在ASCII中一个英文字母、数字占一个字节。但有些字符比如中文 个数远远超过了256个,所以就有了Unicode编码集,它包含了全世界所有的字符。在Unicode中,一个中文占2个字节,但是有些文字会占用3个字节甚至更多,如果每个字符都占用3-4个字节,对于一个字节的英文字母来说,就极大地浪费了空间,所以出现了UTF-8的编码格式。UTF-8是一种变长的编码格式,他会按照字符在Unicode中的的16进制大小,来决定占用几个字节。

46、String可以有多长

看是以那种方式创建的String。如果是 String str = "aa",该常量是保存在方法区的常量池中的,需要看字节码中的规定,字节码中规定用两个字节的大小来保存常量。两个字节就是65535个
如果是new String("")创建,受虚拟机的限制,理论上是Integer.MAX_VALUE大小,实际上也有可能更小,主要看当前堆内存有多大

47、Java的匿名内部类有哪些限制?

没有名字,但是从虚拟机角度来说,它是有名字的。
(匿名内部类有名字,因为虚拟机需要是识别它,通过反射,Class.forName(包名 + 1就是这个类里面的第一个匿名内部类)
会导致内存泄漏,是因为内部类持有外部类的引用,编译器帮我们定义了内部类的构造方法。构造方法里面将外部类的引用作为了入参。
1、没有人类认知意义的名字
2、只能继承或实现一个接口
3、匿名内部类持有外部类的引用容易引发内存泄漏

48、怎么理解java的方法分派

静态分派 - 方法重载分派 (对于一些动态语言 情况不一样,比如groovy)

  • 编译期确定
  • 依据调用者的声明类型和方法参数类型

动态分派 - 方法覆写分派

  • 运行时确定
  • 依据调用者的实际类型分派

48、Java泛型中的实现机制是怎样的

https://www.jianshu.com/p/36356dba3ee9
java的泛型是一种伪泛型,采用了类型擦除的机制,会在编译期擦除泛型的类型变为object,并且在相应的地方插入了强制类型转换的代码,所以jiava虚拟机中不存在泛型。其他语言比如c++或者c#就刚好相反,他们会在编译器根据泛型类型生成一个全新的类型。但是这样导致字节码膨胀,增加内存的负担。
为什么要用伪泛型,java1.5才推出泛型,为了兼容性更好,保证即使1.5之前的代码不添加上泛型也能正常运行,所以采用了这种方式。
泛型的缺陷:
1、 泛型类型在编译时被擦除为object。所以基本数据类型无法作为泛型参数,所以要采用基本类型的装箱。
2、泛型类型无法作为方法的重载,因为泛型经过编译后去掉了泛型类型。两个方法就是一样的
3、泛型类型无法创造实例。也是因为经过编译后去掉了泛型类型。
4、静态方法无法引用类泛型参数。类的泛型参数只有实例化的时候才会知道。 静态方法根本不需要实例就可以调用。可以给静态方法声明一个泛型类型,经过编译器后会自动进行强转
5、和真泛型比较,伪泛型在字节码中会出现大量的类型转换,这样也是增加了类型转换的开销

50、ActivityForResult这么麻烦为什么不考虑回调?

ActivityForResult确实很麻烦,代码逻辑分离,跳转的代码和处理的代码分得太开,onActivityResult传的参数intent并不能保证是非空的,如果返回的结果种类比较多,onActivityResult需要用switch case做处理。
不考虑回调的原因是因为跳转的当前activity会因为内存原因被kill调,返回的时候需要重新创建,这样回调就收不到,而且容易造成内存泄漏

51、如何停止一个线程

自带有一个Thread.stop()方法,因为线程安全问题已经被废弃。因为通过Thread.stop去停止一个线程,该线程会释放持有的所有的锁。如果此时线程数据写到一半,释放了锁,其他在等待锁的线程就会进入同步代码,读到的数据就有可能被破坏。
线程为什么没有暂停和继续
如果线程暂停,依然持有锁,如果其他线程也在等待锁,就会造成死锁

使用interrupt 中断线程 interrupt在native也是用用的一个标志位,并且加了锁,所以interrupt本质上不是中断了线程,而是用标志位来通知任务结束
使用boolean标志位 并且加上volatile

52、如何写出线程安全的程序

线程不安全主要是因为共享了可变的资源,所以为了保证线程安全主要从以下几个方面来解决
1、不共享资源 保证变量为局部变量 ThreadLocal
2、共享不可变的资源 使用final
3、共享可变资源 但是要保证可见性(volatile)和原子性、禁止重排序(虚拟机为了优化程序性能会对非依赖关系的代码进行重排序,两个没有相互依赖关系的赋值,可能下面的先执行)
保证可见性:使用final 改为共享不可变的资源,使用volatile每次修改都会通知其他线程。加锁使用synchronized,他释放锁的时候会将工作内存的数据刷新给主内存。
保证原子性:加锁,只有执行完,其他线程才能进入
禁止重排序:volatile 和 synchronized
为什么要禁止重排序:一个对象的创建,是先拿到一块内存,然后调用构造方法,最后将内存地址给对象的引用。但是可能出现拿到地址后,先给了对象的引用,再执行完构造方法。

53、如何在Android中写出优雅的异步代码

可以用回调 但是可能进入回调地狱
可以用handler 子线程通过消息发送给主线程 handlermessage可能会很复杂,不优美
使用rxjava 将异步逻辑扁平化,注意异常处理和取消处理
使用kotlin的协程 将异步逻辑扁平化,注意异常处理和取消处理

Activity的启动流程是怎样的

image.png

54、跨APP启动activity有哪些注意事项

sharedUserId是一样 ,如果启动的app exported为false,使用相同的sharedUserId可以保证两个app是处于同一个进程中
activity 设置是否exported
定义action使用隐式启动方式
在被启动的activity中定义权限。 启动的app中加上权限。但是需要定影权限的app先安装
需要注意拒绝服务漏洞,其他app传递实现了seriazable接口的对象,由于没有这个对象会crash,可以添加try catch

如何解决Activity参数类型安全及接口繁琐的问题?

类型安全:Bundle 的 K-V不能在编译器保证类型,只有在运行期才知道发送方的类型,需要人工保证
接口繁琐:启动Activity时参数和结果传递都依赖Intent

55、如何在代码任意位置为当前Activity添加view?

可以参考setContentView的源码,拿到contentview的view,通过contentView.aaView(view,params);
可以监听activity的生命周期,new ActivityLifecycleCallbacks(){
}获取当前activity。

56、如何实现微信右滑返回的效果

fragment相对于activity实现更简单
fragment:
不涉及window的控制,只是view级别的
实现view跟随手势滑动移动的效果
实现手势结束后判断取消或者返回执行归位动作
Activity:
除了实现手势和动画之外要处理好window
需要将当前activity设置成透明,这会影响生命周期(windowBackground设置成透明的,windowIsTranslucent 设置成true--有坑)
关于activity透明,可以使用反射 调用activity中隐藏的conventToTranslucent方法。滑动时让当前activity透明,滑动解锁了让当前activity不透明。

57、android为什么非UI线程不能更新UI?

可以反着想,如果ui线程可以更新ui,为了保证线程安全,需要到处加锁,UI可能需要高频的修改,频繁的加锁释放锁会提高性能的消耗。
刷新ui的时候最终会调用到viewRootImpl这个类里面,更新前会调用一个checkThread的方法,回去比较刷新ui的线程和viewRootImpl所在的线程是否一致,不一致就会抛出异常。viewRootImpl是在ActivityThread的handleResumeActivity中创建的,所以viewRootImpl所在的线程一定是在主线程。但是有一种特殊的情况,在oncreate中,在子线程也可能去更新ui,刚才说了现在的防止ui在主线程刷新ui的机制中,会做一个checkThread检查,但是viewRootImpl实例是在handleResumeActivity这个方法中创建的,看名字也知道是在onresume生命周期才创建,所以在oncreate生命周期viewRootImpl是还没有创建的,也就不会去做checkThread检查。
所以我们一般在IO线程中执行耗时的操作,通过handler发送消息,在UI线程中进行更新
非UI线程也可以更新UI:
使用SurfaceView
surfaceView在draw前会lock,draw后调用unLock。然后通知主线程把他显示出来

58、 Handler发送消息的delay可靠吗?

不可靠

  • messageQueue 中message比较多的时候会导致线程卡顿,压力比较大
  • delay时间较短,低于线程中获取一次message的周期
    优化方向:过滤重复消息。每次发送前都先过滤。
    互斥消息的取消。
    不要重复创建大量的message,使用message.obtain()方法获取message,obtain中有复用的机制
    可以在子线程中创建handler,降低UI线程的压力

59、 looper为什么不会导致CPU占用过高

主线程处于死循环时,如果messageQueue没有消息, 就会阻塞在 Message msg = queue.next()这个方法,这时候主线程释放cpu资源会进入休眠状态。这里还用到了linux中的管道和epoll机制。他会通过管道向描述符进行读写操作,当queue为空的时候,会阻塞在读取文件。如果向描述符进行写操作, queue.next()就会返回读取结果,从而会唤醒主线程。
那么我们在主线程中耗时为什么会造成 ANR 异常呢?
那是因为我们在主线程中进行耗时的操作是属于在这个死循环的执行过程中, 如果我们进行耗时操作, 可能会导致这条消息还未处理完成,后面有接受到了很多条消息的堆积,从而导致了 ANR 异常.

59、 主线程的Looper为什么不会导致应用ANR?

ANR是怎么产生的
前台服务:20s
后台服务: 200s
前台广播:10秒
后台广播:60s
输入时间:5秒

60、 如何避免OOM

OOM是怎么产生的
已使用的内存+新申请的内存 > 可分配内存
如何优化程序减少内存占用?
使用合适的数据结构
比如数据比较多就使用HashMap而不是ArrayMap(二分查找)

避免使用枚举 一个int占4个字节,一个枚举占24个字节
Bitmap的使用: 尽量根据实际需求选择合适的分辨率
不适用帧动画,使用代码实现动效
考虑对Bitmap的重采样和内存的复用(不要重新申请内存,LRUCache来缓存bitmap)。
谨慎使用多进程
谨慎使用largHeap = “true” 会影响gc的效率
是用NDK,native Heap 没有专门的限制 内存大户的核心逻辑主要在Native

内存优化5R法则
Reduce缩减: 降低图片分辨率/重采样/抽稀策略
Reuse复用:池化策略(message 、bitmap复用)/避免频繁创建对象,减小GC压力
Recycle回收:主动销毁、结束,避免内存泄漏/生命周期闭环
Refactor 重构:更合适的数据结构/更合理的程序架构
Revalue重审:谨慎使用Large Heap/多进程/第三方框架

61、 如何对图片进行缓存

  • 网络/磁盘/内存缓存
  • 缓存算法分析
    加载图片的时候先看看memory有没有,没有再看Disk有没有,没有再从网络获取
    缓存算法:
    需要考虑哪些应该保存,哪些应该丢弃,什么时候丢弃。从缓存中获取的成本、缓存的成本(是不是一定要用缓存)
    LRU缓存 最近最少的使用,每次加入缓存的时候抛弃最近一段时间最少使用的数据
    LFU缓存 最少使用频率

LruCache 线程安全,使用的LinkedHashMap,将插入的数据放入最后
不要使用中的util.LruCache,里面是删除最后一个元素,最后一个元素是最近使用的元素,应该删除第一个元素,使用v4中的LruCache

62、 如何规避Android P对访问私有API的限制?

Android P以前访问私有API
1、比如hide api,自行编译系统源码并导入项目
2、使用反射 setAccessible(true)不仅仅可以绕过访问权限的控制,还可以修改final变量
Android P对api加入了白名单机制。这个检测机制在反射里面,看反射的源码,里面有对白名单的检测,和对policy的检测,我们主要是要修改runtime 中的hidden_api_policy_,让policy == hidden_api_policy_,hidden_api_policy_的修改主要是在native中进行的修改。有个freeReflection框架可以避Android P对访问私有API的限制。

63、 fresco 缓存原理

Fresco使用三级缓存,已解码内存缓存;未解码内存缓存;磁盘缓存

第一级缓存就是保存bitmap,直接存的就是bitmap对象,5.0 以下,这些位于ashmem,5.0以上,直接位于java的heap上
第二级缓存保存在内存,但是没有解码,使用时需要解码,
第三级缓存就是保存在本地文件,同样文件也未解码,使用的时候要先解码啦!

在5.0以下,GC将会显著地引发界面卡顿。Fresco将图片放到一个特别的内存区域。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得APP更加流畅,减少因图片内存占用而引发的OOM。

64、glide缓存原理

  1. 内存缓存:LruResourceCache(memory)+弱引用activeResources

Map>> activeResources正在使用的资源,当acquired变量大于0,说明图片正在使用,放到activeResources弱引用缓存中,经过release()后,acquired=0,说明图片不再使用,会把它放进LruResourceCache中

2)磁盘缓存:DiskLruCache,这里分为Source(原始图片)和Result(转换后的图片)

第一次获取图片,肯定网络取,然后存active\disk中,再把图片显示出来,第二次读取相同的图片,并加载到相同大小的imageview中,会先从memory中取,没有再去active中获取。如果activity执行到onStop时,图片被回收,active中的资源会被保存到memory中,active中的资源被回收。当再次加载图片时,会从memory中取,再放入active中,并将memory中对应的资源回收。

之所以需要activeResources,它是一个随时可能被回收的资源,memory的强引用频繁读写可能造成内存激增频繁GC,而造成内存抖动。资源在使用过程中保存在activeResources中,而activeResources是弱引用,随时被系统回收,不会造成内存过多使用和泄漏。

65、 是否了SurfaceView,它是什么?他的继承方式是什么?他与View的区别(从源码角度,如加载,绘制等)。

SurfaceView中采用了双缓冲机制,保证了UI界面的流畅性,同时SurfaceView不在主线程中绘制,而是另开辟一个线程去绘制,所以它不妨碍UI线程;
SurfaceView继承于View,他和View主要有以下三点区别:
(1)View底层没有双缓冲机制,SurfaceView有;
(2)view主要适用于主动更新,而SurfaceView适用与被动的更新,如频繁的刷新
(3)view会在主线程中去更新UI,而SurfaceView则在子线程中刷新;
SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha()
View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。
SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快,Camera预览界面使用SurfaceView。
GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。

66、为什么冷启动会有白屏黑屏问题?

原因在于加载主题样式Theme中的windowBackground等属性设置给MainActivity发生在inflate布局当onCreate/onStart/onResume方法之前,而windowBackground背景被设置成了白色或者黑色,所以我们进入app的第一个界面的时候会造成先白屏或黑屏一下再进入界面。解决思路如下
1.给他设置windowBackground背景跟启动页的背景相同,如果你的启动页是张图片那么可以直接给windowBackground这个属性设置该图片那么就不会有一闪的效果了

`

2.采用世面的处理方法,设置背景是透明的,给人一种延迟启动的感觉。,将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会"立即"进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们心机的把Theme里的windowBackground的颜色设置成透明的,强行把锅甩给了手机应用厂商(手机反应太慢了啦)

`

3.以上两种方法是在视觉上显得更快,但其实只是一种表象,让应用启动的更快,有一种思路,将Application中的不必要的初始化动作实现懒加载,比如,在SpashActivity显示后再发送消息到Application,去初始化,这样可以将初始化的动作放在后边,缩短应用启动到用户看到界面的时间

l

你可能感兴趣的:(android app面试整理 2020-5)