Android之 常见问答总结

1,面向对象开发的四大特性:

  • 封装(Encapsulation):封装是将数据和方法组合在一起,对外部隐藏实现细节,只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。

  • 继承(Inheritance):继承是从已有类中派生出新类,新类具有已有类的属性和方法,并且可以扩展或修改这些属性和方法。这样可以提高代码的复用性和可扩展性。

  • 多态(Polymorphism:多态是指同一种操作作用于不同的对象,可以有不同的解释和实现。它可以通过接口或继承实现,可以提高代码的灵活性和可读性。

  • 抽象(Abstraction):抽象是从具体的实例中提取共同的特征,形成抽象类或接口,以便于代码的复用和扩展。抽象类和接口可以让程序员专注于高层次的设计和业务逻辑,而不必关注底层的实现细节。

2,四大组件

  • 四大组件是什么?Activity可视化界面,Service无界面后台服务,ContentProvider数据共享内容提供者,BroadcastReceiver消息传递广播
  • Activity切换横竖屏时会重新走生命周期,从onstop到onCreate,如果在清单文件配置android:configChanges="orientation|keyboardHidden|screenSize"就可以避免该情况
  • onNewIntent调用时机,Intent是用来进行组件间通信的,那onNewIntent就是用来处理新的数据通信,在每次复用得时候调用,即走onRestart()生命周期得时候调用。
  • Intent传输数据的大小是有限制的,因为Intent 中的 Bundle 是使用 Binder 机制进行数据传送的, 数据会写到内核空间,即Binder 缓冲区域,缓冲区大小一般是1-2M。
  • Activity启动模式,standard默认模式(每次startActivity会创建新得Activity);SingleTop栈顶复用模式;SingleTask栈种单例模式;SingleInstance全局系统单例模式
  • Service和Activity进行通信,通过bindService()可以实现Activity调用Service中的方法;通过广播实现Service向Activity发送消息
  • Activity 的启动过程,调用 startActivity() 后经过重重方法会转移到 ActivityManagerService 的 startActivity(),并通过一个 IPC 回到 ActivityThread 的内部类 ApplicationThread 中,并调用其scheduleLaunchActivity()将启动Activity的消息发送并交由Handler H处理。Handler H对消息的处理会调用handleLaunchActivity()->performLaunchActivity()得以完成Activity对象的创建和启动
  • onSaveInstanceState(),onRestoreInstanceState的调用时机,当系统内存不足等原因回收Activity,Activity销毁前会调用onSaveInstanceState()用来保存数据;当再次启动被回收的Activity会调用onRestoreInstanceState();如切换横竖屏生命周期如下onPause() –> onSaveInstanceState() –> onStop() –> onDestroy() –> onCreate() –> onStart() –> onRestoreInstanceState() –> onResume()

3,RecyclerView缓存

RecyclerView与Listview对比优点:

  • 缓存机制,recycleView四级缓存,listView两级,而且屏幕内缓存都是放在一个list中
  • ViewHolder的编写规范化了,不再需要像 ListView 那样自己调用 setTag
  • 支持多种布局,LayoutManage内置了线性布局、网格布局、瀑布流布局
  • 更加方便添加item动画、控制滑动、修改Item分割线
  • 不支持ItemClick,而是通过RecyclerView.OnItemTouchListener接口来探测触摸事件。虽然稍微 

RecyclerView四级缓存

  • 一级缓存:mAttachedScrap 和 mChangedScrap,用来缓存还在屏幕内的 ViewHolder。
  • 二级缓存:mCachedViews ,用来缓存移除屏幕之外的 ViewHolder,默认大小为2。
  • 三级缓存:ViewCacheExtension,开发给用户的自定义扩展缓存,需要用户自己管理 View 的创建和缓存。(不常用)
  • 四级缓存:RecycledViewPool,ViewHolder的缓存池,默认大小为5。

RecyclerView的滑动回收复用机制 

  • 创建item,比如屏幕内可以显示10个ViewHolder,一开始会创建10个ViewHolder用于显示,创建5个ViewHolder放入缓存池中
  • 上滑时会将滑入屏幕上方的ViewHolder首先放入二级缓存中,由于默认大小为2,所以多余的3个ViewHolder会被回收到四级回收中,此时一级缓存中有10个ViewHolder,二级缓存中有2个ViewHolder,四级缓存中有3个ViewHolder
  • 继续上滑的过程中,一级缓存需要显示15个ViewHolder,此时需要从四级缓存中取出5个ViewHolder,由于只有3个ViewHolder,所以需要创建两个缓存
  • 上滑完毕后,5个一级缓存的ViewHolder移出屏幕,其中2个一级缓存的ViewHolder会被存入二级缓存,之前的2个二级缓存的ViewHolder 和 3个一级缓存的ViewHolder会被清空数据后存至四级缓存中

4,Activity、Window、DecorView与ViewRoot之间的关系 

  • Acitiviy不是真正的展示窗口,是作为一个载体与入口的存在。
  • PhoneWidow是真正的展示窗口,一个Activity上面有一个PhoneWindow,PhoneWinow上有一个DecoView
  • DecoView上面的ContentView(R.id.content)才是我们真是写的布局文件所展示的位置,PhoneWindow通过WindowManager管理DecoView
  • ViewRoot 是 DecorView 的管理者,它负责 View 树的测量、布局、绘制,以及事件分发入口。
  • WMS是PhoneWindow的管理者,是一个系统服务,管理window的显示隐藏以及要显示的位置

5, View绘制流程

View 的坐标系统是相对于父控件而言的,已父控件左上角为原点:

  • getTop()         //获取子 View  最上边  到父 View 顶部的距离
  • getLeft()         //获取子 View  最左边  到父 View 左边的距离
  • getBottom()   //获取子 View  最下方  到父 View 顶部的距离
  • getRight()      //获取子 View  最右边  到父 View 左边的距离

所以如下可以算出控件的宽高:

  • View 的高 = getBottom() - getTop()
  • View 的宽 = getRight() - getLeft()

MotionEvent 中的 getX 和 getRawX的区别:

触摸点相对于其所在父view中的坐标

  • event.getX()  
  • event.getY()

触摸点相对于屏幕坐标系的坐标:

  • event.getRawX()    
  • event.getRawY()      

MeasureSpec 位数含义:

  • 简单来说就是一个 int 值,高 2 位表示测量模式,低 30 位用来表示大小

测量模式:

  • EXACTLY: match_parent 和具体数值的情况
  • AT_MOST:wrap_content 自适应模式
  • UNSPECIFIED:表示不限定测量模式,父容器不对 View 做任何限制,这种适用于系统内部,我们一般不会使用 

OnMeasure测量:

  • 自定义View时候:测量的是自身大小
  • 自定义ViewGroup时候:还需要测试子View大小

测量子view大小代码

  • measureChild(childview, widthMeasureSpec, heightMeasureSpec);

测量自身大小代码:

  • setMeasuredDimension(parentWidth,(heightMode == MeasureSpec.EXACTLY) ? parentHeight: parentautoHeight);

padding

  • 自定义ViewGroup一般处理在OnMeasure中
  • 自定义View一般处理在onDraw中

margin

  • 自定义ViewGroup,在onlayout中自定义实现
  • 自定义View不需要处理,是生效的

OnLayout 布局:

  • 遍历处理子view的位置(自己不需要),child.layout(l,t,r,b)
  • 也就是说,只有自定义ViewGroup时候才需要,如果没有子View可以不用复写。

OnDraw 绘制:

  • paint是画笔,可以设置颜色粗细这些
  • canvas是画布,可以绘制点、线、圆形、方形等,还可以缩放、位移、旋转等
  • matrix是矩阵,canvas实践是通过matrix实现缩放、平移操作的底层实现。

RequesLayout与invalidate区别: 

  • requestLayout是从新走measure-layout-draw流程
  • invalidate是从新走draw流程。    

6,线程

进程和线程 

  • 进程是资源 (CPU、内存等)分配的基本单位,它是程序执行时的一个实例。
  • 线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位

进程的种类

  • 前台进程,可见进程,服务进程,后台进程,空进程

Android 进程间数据共享

  • BroadcastReceiver 广播
  1. 优点:简单易用实时通信
  2. 缺点:只支持数据单向传递,效率低且安全性不高
  3. 场景: 一对多的低频率单向通信
  • ContentProvider 内容提供者
  1. 优点:支持一对多的实时并发通信,在数据源共享方面功能强大,可通过Call方法扩展其它操作
  2. 缺点:可以理解为受约束的AIDL,主要提供对数据源的CRUD操作
  3. 场景: 一对多的进程间数据共享
  • AIDL
  1. 优点:功能强大,支持一对多实时并发通信
  2. 缺点:使用稍复杂,需要处理好线程间的关系
  3. 场景: 一对多通信且有RPC需求
  • 文件共享
  1. 优点:简单易用
  2. 缺点:不适用高并发场景,并且无法做到进程间即时通信
  3. 场景:适用于无并发的情况下,交换简单的数据,对实时性要求不高的场景。
  • Bundle
  1. 优点:简单易用
  2. 缺点:只能传输Bundle支持的数据类型
  3. 场景: 四大组件间进程通信
  • Socket
  1. 优点:功能强大,可通过网络传输字节流,支持一对多实时并发通信
  2. 缺点:传输效率低,开销大,实现细节步骤稍繁琐,不支持直接的RPC
  3. 场景: 网络间的数据交换
  • Messenger
  1. 优点:功能一般,支持一对多串行通信,支持实时通信
  2. 缺点:不能很好地处理高并发的情形,不支持RPC,由于数据通过Message传输,因此只能传输Bundle支持的数据类型
  3. 场景: 低并发的一对多实时通信,无RPC需求,或者无需要返回结果的RPC需求

线程的实现 

  • 基础使用
  1. 继承Thread类
  2. 实现Runnable接口
  3. Handler
  • 复合使用
  1. AsyncTask
  2. HandlerThread
  3. IntentService
  • 高级使用
  1. 线程池(ThreadPool)

 线程的状态和生命周期

  • 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  • 就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。 例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  • 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: - 等待阻塞 > 通过调用线程的wait()方法,让线程等待某工作的完成。 - 同步阻塞 > 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 - 其他阻塞 > 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
  • 等待状态(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示地唤醒

线程的终止

  • 使用退出标志终止线程,代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值
  • 使用stop方法终止线程,thread.stop();程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果
  • 使用interrupt方法终止线程,使用while(!isInterrupted()){……}来判断线程是否被中断。其原理根标记位差不都,只不过interrupt是内部实现好的,而标志位是全局的,手动设置。

线程同步

  • 使用锁对象:通过使用ReentrantLock、synchronized关键字锁定对象,实现线程同步。这种方法是Java最基本的线程同步技术。
  • 使用volatile关键字:声明一个volatile变量可以保证其对所有线程的可见性,每次写入操作都会强制刷新缓存。这种方法适用于只有一个线程进行写操作,其他线程进行读操作的情况。
  • 使用wait/notify机制:wait/notify机制是一种高级、灵活的线程同步方法。wait和notify方法需要在synchronized块内部调用,通过wait释放锁,让其他线程占用该锁,notify则是唤醒等待该锁的线程。
  • 使用CountDownLatch、CyclicBarrier等辅助类:这些类提供了更高级的线程同步功能,可以实现多个线程之间的同步。其中CountDownLatch用于等待多个线程完成后再执行代码,而CyclicBarrier则用于等待所有线程准备好后再执行代码。
  • 使用Atomic类:Atomic类提供了一些原子操作,可以避免多个线程同时操作同一个变量时产生的数据竞争问题。它常用于对基本数据类型的操作,如AtomicInteger、AtomicBoolean等。
     

7,Handel原理

Handler 管理消息:

  • Handler 通过sendMessage 或 post 两种方式去发送消息
  • post 内部也是 sendMessage 形式,只不过把传入的 Runnable 参数包装成 Message 的 callback。

MessageQueue 消息队列:

  • MessageQueue其实就是维护一个 Message 链表,它会根据 Message 的 when 时间排序,延迟消息不过是延时时间再加上当前时间
  • looper是死循环,看过messageQuqe源码知道messageQuqe存消息和取消息都是一个死循环。
  • 内部实现Parcelable(安卓序列化方式,存内存)接口,实例化message建议使用obtain来实例化一个对象,因为obtain方法默认从message池中获取。

Looper 循环传输消息:

  • Looper一直调用 MessageQueue 的 next 方法取消息,然后再把消息分发出去
  • 然后通过 Message 的 target 去dispatchMessage 分发消息,最后回收消息

MessageQueue 的 next 也是一个死循环,为什么不会卡死:

  • 这里就涉及到linux系统的管道机制,往管道里面写数据来唤醒cpu,采用nativeWake方法
  • messageQuqe没有数据的时候cpu处于休眠状态(调用nativePollOnce),只有消息到来的时候才会唤醒,所以不会占用大量cpu资源。

Message如何插入和回收消息

  • 插入我们可以new,也可以用obtain方法从消息池中获取(推荐),
  • 回收时候当message从messageQuqe中取走之后,就好调用回收方法,将message置空后放入消息池中。

Message如何实现的延时加载

  • 所有的sendMessage方法都会调用sendMessageDelayed方法(只不过delayMillis=0),
  • 将消息发送到messageQuqe中的时候,会带着执行时间参数(当前时间+延迟时间),
  • 从消息队列取消息的时候先比较now和delayMillis的大小,now<delay,对延迟时间做一个差值, 然后用差值延迟唤醒。

8,Android系统启动流程

按下电源时:系统启动后各层的顺序执行
BootLoader层:加载引导程序
Linux Kernel层:启动 Linux 内核,加载Linux驱动
C++Framework层:启动 第一个用户进程:init 进程,启动adb进程,log进程,meadia服务
然后孵化出第一个java进程:Zygote进程(承上启下,连接java与native存在)
AndroidFramework层:通过SystemService.java启动AMS、WMS等所有的系统服务系统服务有一百多个,通过SystemServiceManager管理
App层:找到Launcher然后启动Launcher 

Gradle中subprojects 和 allprojects 的区别:

/build.gradle
buildscript {
    repositories {
        jcenter()
        google()
        maven {
            url 'https://maven.google.com/'
            name 'Google'
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
    }
}

allprojects {
    repositories {
        jcenter()
        google()
        maven {
            url "http://maven.xxxxxxxx/xxxxx"
        }
    }
}
  • allprojects是对所有project的配置,包括Root Project;而subprojects是对所有Child Project的配置
  • buildscript里是gradle脚本执行所需依赖,如上所示对应的是maven库和插件 
  • allprojects里是项目本身需要的依赖,比如代码中某个类是打包到maven私有库中的,那么在allprojects—>repositories中需要配置maven私有库,而不是buildscript中,不然找不到

9,事件分发

事件分发的核心概念

  1. 事件源(Event Source):事件源是指产生事件的对象,通常是用户的输入设备,如触摸屏幕、物理按键等。事件源负责将用户的操作转化为事件对象,并将其发送给应用程序进行处理。
  2. 事件分发(Event Dispatch):事件分发是指将事件对象传递给正确的View进行处理的过程。在Android中,事件分发由ViewGroup负责,它是所有View的基类。当一个事件发生时,系统会将该事件从顶层的ViewGroup开始逐级向下传递,直到找到合适的View进行处理。
  3. 事件拦截(Event Intercept):事件拦截是指在事件分发过程中,某个ViewGroup可以拦截事件的传递,阻止它传递给子View。如果一个ViewGroup拦截了事件,那么该事件将停止向下传递,不会再被其他View处理。
  4. 事件处理(Event Handling):事件处理是指在事件分发到达目标View后,该View执行相应的操作来响应事件。每个View都可以实现自己的事件处理逻辑,例如处理触摸事件、点击事件等。

事件分发的处理事件

  1. dispatchTouchEvent(MotionEvent event):用于分发触摸事件。在ViewGroup中,它会根据事件的类型和位置判断是将事件传递给子View还是进行事件拦截。如果事件被拦截,会调用onInterceptTouchEvent()方法;否则,会将事件传递给子View的dispatchTouchEvent()方法。
  2. onInterceptTouchEvent(MotionEvent event):用于拦截事件的传递。在ViewGroup中,它可以决定是否拦截事件并阻止其继续传递给子View。如果返回true,表示拦截事件;如果返回false,表示不拦截事件。
  3. onTouchEvent(MotionEvent event):用于处理触摸事件。在View中,它负责实际处理触摸事件的逻辑,例如处理触摸位置、手势判断等。如果事件被消费,会返回true;否则,返回false,以便让事件继续传递到上层的View。

事件分发机制流程

  1. 当用户进行触摸操作时,事件源(如触摸屏幕)会将用户的操作转化为一个MotionEvent对象,并将其传递给顶层的ViewGroup的dispatchTouchEvent()方法。
  2. 在dispatchTouchEvent()方法中,ViewGroup会根据事件的类型和位置判断是否要拦截事件。如果该ViewGroup返回true,表示拦截事件,不再向下传递;如果返回false,表示不拦截事件,继续传递给子View。
  3. 如果事件被拦截,会调用onInterceptTouchEvent()方法进行处理。在onInterceptTouchEvent()方法中,ViewGroup可以根据需要判断是否要拦截事件,并返回相应的结果。
  4. 如果事件没有被拦截,会继续传递给子View的dispatchTouchEvent()方法。子View可以根据自身的需求对事件进行处理,例如判断点击事件、滑动事件等。
  5. 最终,事件会传递到目标View的onTouchEvent()方法,该方法负责具体的事件处理逻辑。在onTouchEvent()方法中,可以根据事件的类型进行相应的操作,例如处理触摸位置、手势判断等

事件分发关键方法讲解

  1. dispatchTouchEvent(MotionEvent event): 该方法用于事件的分发,通常位于ViewGroup中。在此方法中,可以根据需要对事件进行拦截或传递给子View
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // 在此处进行事件的分发和拦截判断
        boolean handled = false;
        // 拦截事件的逻辑
        if (需要拦截事件) {
            handled = true;
        } else {
            // 继续传递事件给子View
            handled = super.dispatchTouchEvent(event);
        }
        return handled;
    }
    
  2. onInterceptTouchEvent(MotionEvent event): 该方法用于拦截事件,通常位于ViewGroup中。在此方法中,可以根据需要判断是否要拦截事件,并返回相应的结果。
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // 在此处进行事件的拦截判断
        boolean intercepted = false;
        // 拦截事件的逻辑
        if (需要拦截事件) {
            intercepted = true;
        } else {
            intercepted = super.onInterceptTouchEvent(event);
        }
        return intercepted;
    }
    
  3. onTouchEvent(MotionEvent event): 该方法用于处理具体的事件,通常位于View中。在此方法中,可以根据事件的类型进行相应的操作。
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 在此处处理具体的事件逻辑
        boolean handled = false;
        // 处理事件的逻辑
        if (需要处理事件) {
            handled = true;
            // 执行事件处理的代码
        } else {
            handled = super.onTouchEvent(event);
        }
        return handled;
    }
    

使用举例

  1. 多个滑动控件嵌套引起的滑动冲突,比如水平方向和垂直方向的两个Recycleview嵌套,需要拦截判断是左滑还是优化,从而判断交给哪个Recycleview消费事件
  2. Scrollview和Viewpager嵌套,根上面一样处理滑动冲突,可以选择父类拦截或者子类消费
  3. 滑动吸顶效果,父控件向上滑动一段距离后,交给子类滑动。

10,动画种类

  1. 逐帧动画【Frame Animation】,即顺序播放事先准备的图片
  2. 补间动画【Tween Animation】,View的动画效果可以实现简单的平移、缩放、旋转。
  3. 属性动画【Property Animation】,补间动画增强版,支持对对象执行动画。
  4. 过渡动画【Transition Animation】,实现Activity或View过渡动画效果。包括5.0之后的MD过渡动画等

11,持久化方案

  • 文件存储:文件存储是Android中最基本的一种数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据都是原封不动地保存到文件中的,因此比较适合用于存储一些简单的文本数据和二进制数据
  • SharedPreferences存储:SharedPreferences是使用键值对的方式来存储数据的,适合存储轻量级数据,比如登录状态,登录Token,一些配置等信息
  • SQLite数据库存储:适合复杂类型,大量数据的存储,比如依赖网络服务器数据少的应用。
  • ContentProvider:系统内容提供者,根SQLite差不多,可以自定义数据类型存储
  • 网络存储:即数据保存在远程服务器,主要服务器不销毁,数据可以永久保留。

12,序列化方案

 序列化的作用:

  • 序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象
  • 一句话就是序列化将运行时的对象状态转换成二进制,然后保存到流、内存或者通过网络传输给其他端
  • Android中序列化通常使用Serializable或者Parcelable 

Serializable或者Parcelable实现的差异:

  • Serializable的实现,只需要实现Serializable接口即可。这只是给对象打了一个标记(UID),系统会自动将其序列化
  • Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法

Serializable或者Parcelable效率对比

  • Serializable 的使用比较简单,创建一个版本号即可;而 Parcelable 则相对复杂一些,会有四个方法需要实现
  • 一般在保存数据到 SD 卡或者网络传输时建议使用 Serializable 即可,虽然效率差一些,好在使用方便
  • 运行时数据传递时建议使用 Parcelable,比如 Intent,Bundle 等,Android 底层做了优化处理,效率很高

13,MVC,MVP,MVVM区别

MVC模式:

  • M(Model): 模型(用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法)
  • V(View): 视图(渲染页面)
  • C(Controller): 控制器(M和V之间的连接器,用于控制应用程序的流程,及页面的业务逻辑) 

MVP模式:

  • M(Model)模型:专门用来封装和处理数据的
  • V(View)视图:展示数据的
  • P(Presenter)中间层:负责MV之间的通信

MVVM模式:

  • M(Model)模型:专门用来准备数据的
  • V(View)视图:展示页面
  • V(ViewModel)视图:视图和模型(视图和数据的转换)

14,Window,Activity,View

Activity

  • Activity 并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是 Window。一个 Activity 包含了一个 Window,Window才是真正代表一个窗口。
  • Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与 Window、以及 View 进行交互。

Window

  • Window 是一个抽象类,实际在 Activity 中持有的是其子类 PhoneWindow。PhoneWindow 中有个内部类DecorView,通过创建 DecorView 来加载 Activity 中设置的布局 R.layout.activity_main
  • Window 是视图的承载器,内部持有一个 DecorView,而这个 DecorView才是 view 的根布局。
  • Window 通过 WindowManager 将 DecorView 加载其中,并将 DecorView 交给 ViewRoot,进行视图绘制以及其他交互

View

  • DecorView 是 FrameLayout 的子类,它可以被认为是 Android 视图树的根节点视图。
  • DecorView 作为顶级 View,一般情况下它内部包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下三个部分,上面是个 ViewStub,延迟加载的视图(应该是设置ActionBar,根据 Theme 设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏
  • 在 Activity 中通过 setContentView 所设置的布局文件其实就是被加到内容栏之中的,成为其唯一子 View,就是上面的 id 为 content 的 FrameLayout 中,在代码中可以通过 content 来得到对应加载的布局。

总结:

  • 一个 Activity 对应一个 Window 也就是 PhoneWindow,一个 PhoneWindow 持有一个 DecorView 的实例,DecorView 本身是一个 FrameLayout

14,JetPack

JetPack是什么:

  • Jetpack 是一个丰富的组件库,它的组件库按类别分为 4 类,分别是架构(Architecture)、界面(UI)、行为(behavior)和基础(foundation)
  • 每个组件都可以单独使用,也可以配合在一起使用

JetPack优势:

  • UI和业务逻辑解耦。
  • 有效避免生命周期组件内存泄漏。
  • 提高模块可测试性。
  • 提高应用稳定性,有效降低以下异常发生概率

JetPack家族:

  • 基础(foundation)AppCompat ,Android KTX ,Multidex ,Test
  • 架构(Architecture)Data Binding , Lifecycles , LiveData , Navigation , Paging , Room , ViewModel , WorkManager
  • 界面(UI)Animation & Transitions , Auto, TV & Wear , Emoji , Fragment , Layout , Palette
  • 行为(behavior)Download Manager ,Media & Playback ,Permissions ,Notifications,Sharing

15 ,ViewBindding,DataBindding

ViewBinding(视图绑定):

通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。在大多数情况下,视图绑定会替代 findViewById。

android {

   viewBidding {

       enabled=true

   }

}
DataBinding(数据绑定):

可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。是实现 view 和 data 绑定的工具,把数据映射到 view 的 xml中,可以在 xml 布局文件中实现 view 的赋值,方法调用。使用 DataBinding 后,我们不用再写 findViewById,不用再获取控件对象,不用再设置监听,可以节省我们 activity 中的很多获取控件,赋值,添加监听所需要的代码

android {

   dataBidding {

       enabled=true

   }

}

16,网络相关

网络分层

  • 物理层:网络跳线,根据目标ip地址,网卡开始路由分发
  • 链路层:ARP,封装当前设备ip对应的MAC地址,封装当前网关ip的MAC地址
  • 网络层:IP,封装当前设备的ip地址,服务器ip地址
  • 传输层:TCP(面向连接) UDP(无连接协议),封装当前应用程序在本机上的端口,服务器程序的监听端口
  • 应用层:DHCP(动态主机配置)、DNS(域名系统)、http(普通传输)、https(加密传输),DNS(把服务器的域名解析为对应的ip地址->https/http

TCP与UDP的区别

  • TCP 面向连接;UDP 是无连接的,即发送数据之前不需要建立连接
  • TCP 提供可靠的服务。也就是说通过 TCP 连接传送的数据,无差错、不丢失、不重复;UDP 尽最大努力交付,即不保证可靠交付
  • TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的。UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话、实时视频会议等)
  • 每一条 TCP 连接只能是点到点的;UDP 支持一对一、一对多、多对一和多对多的交互通信
  • TCP 首部开销 20 字节;UDP 的首部开销小,只有 8 个字节
  • TCP 的逻辑通信信道是全双工的可靠信道,UDP 则是全双工的不可靠信道

HTTP协议

  • HTTP协议是Hyper Text Transfer Protocol (超文本传输协议)的缩写,是用于从万维网服务器传输超文本到地浏览器的传送协议。
  • 它基于TCP/IP通信协议来传递数据(HTML 文件,图片文件,查询结果等)
  • 属于协议分层中的应用层协议。

HTTP特点

  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径,由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快
  • 灵活:HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记
  • 无连接:限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。
  • 无状态:协议对于事务处理没有记忆能力,也就是说,HTTP请求只能由客户端发起,而服务器不能主动向客户端发送数据

HTTP完整请求报文

HTTP协议的请求报文由请求行、请求头部、空行、请求数据四个部分组成

POST http://www.baidu.com HTTP/1.1 HTTP/1.1
cache-control: no-cache
Postman-Token: 800ec750-6ee8-4b2b-a879-f5d854115862
Content-Type: application/json
User-Agent: PostmanRuntime/3.0.11-hotfix.2
Accept: */*
Host: sitapp.2ncai.com
accept-encoding: gzip, deflate
content-length: 38
Connection: close
 
{"seeType":1,"source":1,"userId":"30"}
  • 请求头
POST http://www.baidu.com HTTP/1.1

GET:请求获得Request-URL所标识的资源
POST:在Request-URL所标识的资源后附加新的数据,即可以向服务端发送请求数据
HEAD:请求获取Request-URL所标识的资源的响应消息报头
PUT:请求服务器存储一个资源,并用Request-URL作为其标识
DELETE:请求服务器删除Request-URL所标识的资源
TRACE:请求服务器回送收到的请求信息,主要用于测试或者诊断
CONNETC:HTTP1.1中预留的能够将连接改为管道方式的代理服务器
OPTIONS:请求查询服务器性能,或者查询与资源相关的选项或需求
  • 请求报头
cache-control: no-cache
Postman-Token: 800ec750-6ee8-4b2b-a879-f5d854115862
Content-Type: application/json
User-Agent: PostmanRuntime/3.0.11-hotfix.2
Accept: */*
Host: sitapp.2ncai.com
accept-encoding: gzip, deflate
content-length: 38
Connection: close
  • 空行
在HTTP协议中,规定空行是必须需要的,即使后面的请求数据为空,也必须有空行
  • 请求数据
{"seeType":1,"source":1,"userId":"30"}

HTTP完整响应报文

HTTP/1.1 200 
Date: Sat, 11 Aug 2022 04:24:25 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Application-Context: application:test:5673
Server: my_server
HTTP/1.1 200 
Date: Sat, 11 Aug 2022 04:24:25 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Application-Context: application:test:5465
Server: my_server

{"success":1,"errorCode":"10001","message":"正确","data":{"userName":"CCCC","userId":30,"headPic":"https://www.baidu.com/upload_images/456346235342.png"}}
  • 状态行
100-199:指示信息,收到请求后,需要请求者继续执行操作
200-299:请求成功,请求已被成功接收并处理
300-399:重定向,要完成请求需要进行更进一步操作
400-499:客户端错误,请求有语法错误或者请求无法实现
500-599:服务端错误,服务器执行错误,无法正确处理请求
  • 常见的状态码
200 OK:客户端请求成功
400 Bad Request:客户端请求有语法错误,服务端无法理解
403 Forbidden:服务端收到请求,但是拒绝提供服务
404 NOT FOUND:服务器无法正常提供信息,或是服务器无法回应
500 Internal Server Error:服务器内部错误,无法完成请求
503 Server Unavailable:服务器当前无法处理客户端请求,一段时间后可能恢复正常
  • 响应报头
Date: Sat, 11 Aug 2022 04:24:25 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Application-Context: application:test:8160
Server: my_server
  • 空行
在HTTP协议中,规定空行是必须需要的,即使后面的响应正文为空,也必须有空行。
  • 响应正文
{"success":1,"errorCode":"10001","message":"正确","data":{"userName":"CCCC","userId":30,"headPic":"https://sitres.2ncai.com/_upload_images/user/head/1711181009252.png"}}

HTTPS和HTTP的区别

  • https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
  • http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  • http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全

17,注解

注解的本质

  • 注解就是一个继承了 Annotation(注解) 接口的接口,public interface Override extends Annotation{}
  • 注解只是元数据,不包含任何业务逻辑

注解的特点

  • 注解Annotation是JDK5.0引入的新技术
  • 注解的格式:@注释名,还可以添加参数
  • 注解不是程序本身,但可以对程序作出解释,这一点,注释和注解的作用类似
  • 注解可以被其他程序读取,比如编译器等等
  • 注解可以给Java包、类型(类、接口、枚举)、构造器、方法、域、参数和局部变量进行注解,相当于给它们添加了一些额外的辅助信息
  • 注解的特点:检查、约束、灵活、方便简洁、让静态语言拥有动态机制
  • 注解和XML的异同:XML是松耦合,包含了项目中的所有配置,项目越大XML越复杂。注解是标记式高耦合的配置方式,可以提供更大的便捷性,易于维护修改

编译时注解和运行时注解

  • 保留阶段不同:运行时注解可以在运行时访问,编译时注解保留到编译时,运行时无法访问
  • 原理不同:运行时注解是Java反射机制,Retrofit就是运行时注解,需要用的时候才用到;编译时注解是通过APT、AbstractProcessor来实现
  • 性能不同:运行时注解使用Java反射,因此对性能上有影响;编译时注解对性能没有影响,这也是为什么ButterKnife从运行时注解切换到编译时注解的原因
  • 产物不同:运行时注解只需要自定义注解处理器即可,不会产生其他文件;编译时通常会产生新的Java源文件

常见注解

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

18,反射

反射定义

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

反射优缺点

  • 优点:可以动态执行,在运行期间根据业务功能动态执行方法、访问属性,最大限度发挥了java的灵活性。
  • 缺点:对性能有影响,这类操作总是慢于直接执行java代码

反射的使用

  • 使用 Class 对象的 newInstance()方法来创建该 Class 对象对应类的实例
//获取 Person 类的 Class 对象 
Class clazz=Class.forName("com.dong.Person"); 
//使用.newInstane 方法创建对象 
Person p=(Person) clazz.newInstance(); 
  • 使用 Class 对象获取指定的 Constructor 对象,再调用 Constructor 对象的 newInstance()
//获取构造方法并创建对象 
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class); 
//创建对象并设置属性 
Person p1=(Person) c.newInstance("张三","男",24);
  • 通过class对象获得一个属性对象
Field c=cls.getFields():获得某个类的所有的公共(public)的字段,包括父类中的字段
Field c=cls.getDeclaredFields():获得某个类的所有声明的字段,即包括public、private和proteced
//获取 Person 类的所有成员属性信息
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
   System.out.println(f.toString());
}
  • 通过class对象获得一个方法对象
Cls.getMethod(),只能获取公共的
Cls.getDeclareMethod(),获取任意修饰的方法,不能执行私有
M.setAccessible(true),让私有的方法可以执行
Method[] method=clazz.getDeclaredMethods();
 for(Method m:method){
    System.out.println(m.toString());
 }
  • 执行方法Method.invoke()
Class ownerClass = Class.forName(className);
Class[] argsClass = new Class[args.length];
for (int i = 0, j = args.length; i < j; i++) {
  argsClass[i] = args[i].getClass();
}
Method method = ownerClass.getMethod(methodName,argsClass);
method.invoke(null, args);

19,Binder原理

Linux进程间通信方式

  • 管道,信号/信号量,消息队列,共享内存,套接字

Android进程间通信方式

  • Intent,ContentProvider,文件共享,AIDL,Messenger,Socket

Binder的定义

  • 进程间通信的机制:可以理解为android的血管,Activity,Service需要和AMS通信的时候,就需要Binder
  • 虚拟物理设备驱动:通过mmap映射时将虚拟内存映射到/dev/binder文件,在linux中这个代表一个驱动
  • 能发起进程间通信的Java类:比如AIDL中的Stub,Proxy,AMS等也实现了IBinder,另外在ServiceManager中查找服务时,所用到的Map集合的value也是IBinder类型的

Binder优势

  • 性能:相比较Socket等其它方式需要在内存中拷贝两次,而Binder只需要经过一次拷贝,所以性能上要优于其它方式
  • 安全性:Linux的IPC机制在本身的实现中,并没有安全措施,而Binder机制的UID/PID是由Binder机制本身在内核空间添加身份标识,安全性高;并且Binder可以建立私有通道,这是linux的通信机制所无法实现的
  • 稳定性:Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好
  • 使用简单:client端函数的名字、参数和返回值和server的方法一模一样,取消了client端和server端的隔阂

内存隔离

  • Linux进程内存隔离:进程是隔离的,进程与进程间内存是不共享的,用户空间和内核空间是隔离的,用户态与内核态是隔离的,想要通信必须要使用IPC
  • 内存映射:Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间

Binder跨进程流程

  • 通信过程:首先 Binder 驱动在内核空间创建一个数据接收缓存区;
  • 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
  • 发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

运行机制

  • Client:使用服务的进程,通过名字向ServiceManager获取对应的Binder
  • Server:提供服务的进程,将创建的Binder与它的字符形式的名字以数据包的形式通过Binder驱动向ServiceManager注册服务,供其他进程远程调用。
  • ServiceManager:一个独立的进程,可将字符形式的Binder名字转化成对应的Binder实例,它会维护Binder名字与Binder实体的表。注册与获取服务,都是使用Binder方式的进程间通信,而ServiceManager提供的Binder与其他的不一样,首先进程使用BINDERSETCONTEXT_MGR命令,将其注册成ServiceManager,同时Binder驱动会为它创建一个Binder实体;在其他进程中获取它时都是通过0号引用进行获取,从而与ServiceManager进行通信。
  • Binder驱动:提供进程之间通信的建立、Binder传递等一些底层操作的支持,

    注册服务:Server通过Binder驱动向ServiceManager注册Binder,驱动为这个Binder创建一个在内核中的节点以及将其引用和名字打包传给ServiceManager,ServiceManager就可以把它们填到表中。

    使用服务:Client通过Binder驱动,用Binder名字去ServiceManager中获取对应的Binder引用,通过该引用,就可以使用Server提供的服务。

20,ClassLoader类加载器

ClassLoader的作用

  • ClassLoader主要用于动态地加载Java类和资源文件,这些Java类包括应用程序、Java核心库程序等等
  • ClassLoader还可以加载Applet、插件和Web应用程序中使用的类
  • ClassLoader中的加载器依次被称为父类加载器和子类加载器,父类加

双亲委托

  • 一个类加载器查找class和resource时,是通过“委托模式”进行的
  • 它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器
  • 然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回
  • 如果没有找到,则一级一级返回,最后到达自身去查找这些对象,这种机制就叫做双亲委托

ClassLoader的种类

  • BootstrapClassLoader:这是 Java 虚拟机(JVM)默认的 ClassLoader,用于加载 Java 核心类库(如 java.lang 包中的类)。Android 系统中,由于使用的是 Dalvik 虚拟机,因此不使用 Bootstrap ClassLoader。

  • PathClassLoader:这是 Android 系统中的一个重要的 ClassLoader,用于加载 apk 文件、odex 文件、dex 文件和 jar 文件中的类和资源文件。在 Android 应用中,每个 Module 都有自己的 PathClassLoader。

  • DexClassLoader:这是 Android 系统中的一个特殊的 ClassLoader,用于加载 dex 文件中的类和资源文件。与 PathClassLoader 不同,DexClassLoader 可以加载任意位置的 dex 文件,包括在应用运行时生成的 dex 文件。

  • BaseDexClassLoader:这是 PathClassLoader 和 DexClassLoader 的共同父类,定义了一些通用的方法和字段,如获取父类 ClassLoader、获取类路径等。

  • WebViewClassLoader:这是 Android 系统中的一个特殊的 ClassLoader,用于加载 WebView 组件中的类和资源文件。

21组件化、模块化、插件化

  • 模块化:模块化是指将一个项目按照业务逻辑划分成若干个模块,比如购物类app可以划分为商品展示模块、购物车模块、订单模块、客服模块等等,将一个项目分成几层,这样可以保证每个模块的职能单一
  • 组件化:模块化虽然进行了分层开发,但是代码复用性不高。所以组件化是在模块化的基础上的进一步演变,它划分的更细了,每个组件都是独立的,可以按照需要选择需要的组件组合起来就成为了整个项目
  • 插件化:本质上也是模块化的一种,它也是按照业务逻辑进行划分,但不是划分成一个个模块,而是划分成插件,这些插件可以独立编译打包成为一个独立子app,因为插件化的加载是动态的,所以可以实现热修复

22热修复

什么是热修复

  • 热修复就是对线上版本的静默更新
  • 当APP发布上线之后,如果出现了严重的bug,通常需要重新发版来修复,但是重新走发布流程可能时间比较长,重新安装APP用户体验也不友好,所以出现了热修复
  • 热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对用户来说是无感的

热修复的实现方案

  • 类加载方案,即dex插桩,这种思路在插件化中也会用到。如微信的Tinker、qq空间的QZone、美团的Robust、饿了么的Amigo
  • 底层替换方案,即修改替换ArtMethod,阿里系的AndFix等

热修复原理

  • 根据类加载机制,可以知道热修复的原理就是将补丁包dex文件放到dexElements数组靠前的位置
  • 这样在加载class时,优先找到补丁包中的dex文件,加载到class之后就不再寻找,从而原来apk里同名的类就不会再使用,达到修复的目的

热修复流程

  • 获取到当前应用的PathClassLoader;
  • 反射获取到DexPathList属性对象pathList;
  • 反射修改pathList的dexElement:
  • 把补丁包patch.dex转化为Element[](patch)
  • 获得pathList的dexElements属性(old)
  • patch+dexElements合并,并反射赋值给patchList的dexElements

23,类加载流程

类加载有五个过程

  • 加载->验证->准备>解析->初始化

加载

  • 通过类的全限定名获取类的字节码文件
  • 将字节码转化为运行时数据结构
  • 在内存中生成一个class对象,作为方法区进入这人类的入口
  • 对于数组不需要通过类加载的过程,有JVM动态构造出来的,但是内部的元素还是要通过类加载的过程。

验证

  • 验证是链接的第一步,主要是验证是否符合JVM规定的class文件格式
  • 文件格式验证:主要判断是否符合class文件格式: 是否一魔数开头,JVM版本是否符合,常量池Q 的常量tag等
  • 元数据验证:判断加载类是否有父类,是否继承了不允许继承的类,是否为抽象类,如不是是否实现了父类或者接口的所有方法
  • 字节码验证:验证程序语义是否合法,是否符合逻辑,对类的方法体进行校验和分析
  • 符号引用验证:验证类中引用的资源(类,变量,方法) 是否存在,访问权限是否合法

准备

  • 准备是链接的第二步,目的是将类的静态变量只分配内存,初始化默认值,主要还是八种数据类型,以及引用类型
  • 注意,boolean在内存当中存储的时候是按照int类型存储的,0表示true,1表示false

解析

  • 解析是链接的第二步,主要就是将类,接口,变量,方法的间接引用转化为直接引用
  • 符号引用: 用一组符号来描述锁引用的目标,这个符号能定位到目标
  • 直接引用:直接指向目标的指针、偏移量以及能够间接定位到目标的句柄

初始化

  • 这是JVM类加载的最后阶段,执行类构造器里面的<cinit>方法,对类的静态变量和静态方法进行初始化操作

24,垃圾回收机制

什么是垃圾回收

  • 我们目前接触的大都是高级语言,即面向对象的程序设计语言,每创建一个对象就会在内存中分配一定大小的空间,这样对象死亡的时候就需要从内存中清除,从而避免内存占满引发内存泄漏而导致系统卡顿,甚至崩溃。

怎样回收

  • 手动回收:如C/C++等,需要手动标记对象的引用数量,引用为0可以回收。
  • 自动回收:如java,运行环境在java虚拟机中,可以自动标记,整理,清除无用的对象

Java 内存区域

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

内存回收机制

  • 年轻代:所有新生成的对象都放在年轻代。年轻代分为一个Eden区和两个Survivor区。GC时,当Eden区满时,还存活的对象会被复制到其中一个Survivor区(A)。当这个Survivor区(A)也满时,就会被复制到另一个Survivor区(B)。当Survivor区(B)也满时,从第一个Survivor(B)复制过来并且还存活的对象,就会被复制到老年代。
  • 老年代:在年轻代经历了N次垃圾回收仍然存活的到对象,就被放到老年代。
  • 持久代主要存放静态文件,比如Java类,方法等。持久代对垃圾回收没有明显影响。如果持久代空间太小,可通过-XX:MaxPermSize =< N配置。

GC收集器原理

  • 可达性算法:从GCRoot对象为起点,向下搜索,可到达的对象是称为GC可达,GC收集器不会回收,不可到达的对象称为不GC不可达,是GC收集器回收的对象。
  • GCRoot对象:(1)虚拟机栈(栈帧找那个的局部变量表)中的对象;(2)方法区中类静态变量引用的对象;(3)方法区中常量引用的对象。

收集垃圾算法

  • 引用计数算法(Reachability Counting):通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收
  • 可达性分析算法(Reachability Analysis):通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。

回收垃圾算法

  • 标记清除算法(Mark-Sweep):是最基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片
  • 复制算法(Copying):是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效
  • 标记整理算法(Mark-Compact):标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

    标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多

  • 分代收集算法分代收集算法(Generational Collection):严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 — 整理算法来进行回收

25,内存泄漏

内存泄露和内存溢出

  • 内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。通常少次数的内存无法及时回收并不会到程序造成什么影响,但是如果在内存本身就比较少获取多次导致内存无法正常回收时,就会导致内存不够用,最终导致内存溢出
  • 内存溢出 (out of memory):是指程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中。也就是说给你个int类型的存储数据大小的空间,但是却存储一个long类型的数据,这样就会导致内存溢出

常见的内存泄漏原因和解决方法

  • 静态变量导致的内存泄漏:一个静态变量又是非静态内部类会一直持有对外部类的引用,导致外部类Activity无法被回收。解决办法:将内部类设为静态内部类或独立出来;使用context.getApplicationContext()。
  • 单例模式导致的内存泄漏:单例传入参数this来自Activity,使得持有对Activity的引用。解决办法:传参context.getApplicationContext()。
  • 属性动画导致的内存泄漏:没有在onDestroy()中停止无限循环的属性动画,使得View持有了Activity。解决办法:在Activity.onDestroy()中调用Animator.cancel()停止动画。
  • Handler导致的内存泄漏:Message持有对Handler的引用,而非静态内部类的Handler又隐式持有对外部类Activity的引用,使得引用关系会保持至消息得到处理,从而阻止了Activity的回收。解决办法:使用静态内部类+WeakReference弱引用;当外部类结束生命周期时清空消息队列。
  • 线程导致的内存泄漏:AsyncTask/Runnable以匿名内部类的方式存在,会隐式持有对所在Activity的引用。解决办法:将AsyncTask和Runnable设为静态内部类或独立出来;在线程内部采用弱引用保存Context引用。
  • 资源未关闭导致的内存泄漏:未及时注销资源导致内存泄漏,如BraodcastReceiver、File、Cursor、Stream、Bitmap等。解决办法:在Activity销毁的时候要及时关闭或者注销。例如:① BraodcastReceiver:调用unregisterReceiver()注销;②Cursor,Stream、File:调用close()关闭;③Bitmap:调用recycle()释放内存(2.3版本后无需手动)。
  • Adapter导致的内存泄漏:不使用缓存而只依靠getView() 每次重新实例化Item,会给gc制造压力。解决办法:在构造Adapter时使用缓存的convertView。
  • WebView导致的内存泄漏。WebView比较特殊,即使是调用了它的destroy方法,依然会导致内存泄漏。解决办法:其实避免WebView导致内存泄漏的最好方法就是让WebView所在的Activity处于另一个进程中,当这个Activity结束时杀死当前WebView所处的进程即可,我记得阿里钉钉的WebView就是另外开启的一个进程,应该也是采用这种方法避免内存泄漏。
  • 集合类泄漏:比如全局map等有静态应用,最后没有做删除。解决办法:在onDestry时回收不需要的集合。

26,性能优化

布局优化

  • 尽量减少布局文件的层级,布局中的层级少了,Android绘制时的工作量就少了,程序的性能自然就高了
  • 选择地使用性能较低的ViewGroup,在布局简单的情况下,优先使用 LinearLayout,其次考虑 RelativeLayout 和 ConstraintLayout 
  • 在布局比较复杂的情况下,优先使用 RelativeLayout 和 ConstraintLayout,不建议使用 LinearLayout 各种嵌套
  • 使用按需加载布局ViewStub和Merge,不占用空间层次。

ViewStub

  • ViewStub是一种没有大小,不占用布局的View,直到当调用 inflate() 方法或者可见性变为VISIBLE时,才会将指定的布局加载到父布局中
  • ViewStub加载完指定布局之后会被移除,不再占用空间,所以 inflate() 方法只能调用一次

Merge的使用

  • merge既不是View也不是ViewGroup,只是一种标记。
  • merge必须在布局的根节点。
  • 当merge所在布局被添加到容器中时,merge节点被合并不占用布局,merge下面的所有视图转移到容器中。

绘制优化

  • onDraw中不要创建新的局部对象,因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。
  • onDraw方法中不要做耗时的任务,不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。

网络优化

  • 尽量减少网络请求,能够合并的就尽量合并
  • 避免DNS解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在DNS劫持的风险。可以根据业务需求采用增加动态更新IP的方式,或者在IP方式访问失败时切换到域名访问方式。
  • 大量数据的加载采用分页的方式
  • 网络数据传输采用GZIP压缩
  • 加入网络数据的缓存,避免频繁请求网络
  • 上传图片时,在必要的时候压缩图片

Android内存优化

  • 减少内存泄漏的相关优化,上面有讲到,对象的合理创建使用,资源的及时关闭,引用的慎用
  • 避免内存溢出优化,大图片压缩处理,Bitmap及时回收,下载文件的位置选择,线程开销的合理使用
  • 网络和图片使用缓存加载,布局加载如adapter列表适配也使用缓存,避免内存的瞬间开销
  •  检测、分析内存泄漏的工具:(1)MemoryMonitor:随时间变化,内存占用的变化情况。(2)MAT:输入HRPOF文件,输出分析结果,查看不同类型对象及其大小,对象占用内存及其引用关系。(3)LeakCanary:实时监测内存泄漏的库(LeakCanary原理)

卡顿优化

  • 不要在主线程进行网络访问/大文件的IO操作
  • 绘制UI时,尽量减少绘制UI层次;减少不必要的view嵌套,可以用Hierarchy Viewer工具来检测,后面会详细讲;
  • 布局是用的FrameLayout的时候,我们可以把它改成merge,可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout)
  • 提高显示速度,使用ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。
  • 在view层级相同的情况下,尽量使用 LinerLayout而不是RelativeLayout;因为RelativeLayout在测量的时候会测量二次,而LinerLayout测量一次,可以看下它们的源码;
  • 删除控件中无用的属性;
  • 布局复用.比如listView 布局复用
  • 尽量避免过度绘制(overdraw),比如:背景经常容易造成过度绘制。由于我们布局设置了背景,同时用到的MaterialDesign的主题会默认给一个背景。这时应该把主题添加的背景去掉;还有移除
  • XML 中非必须的背景
  • 自定义View优化。使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。也是避免过度绘制.
  • 启动优化,启动速度的监控,发现影响启动速度的问题所在,优化启动逻辑,提高应用的启动速度。比如闪屏页面,合理优化布局,加载逻辑优化,数据准备.
  • 合理的刷新机制,尽量减少刷新次数,尽量避免后台有高的 CPU 线程运行,缩小刷新区域。

耗电优化

  • 合理的使用wake_lock锁,wake_lock锁主要是相对系统的休眠(这里就是为了省电,才做休)而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了wake_lock锁。
  • 使用jobScheduler2,集中处理一些网络请求,有些不用很及时的处理可以放在充电的时候处理,比如,图片的处理,APP下载更新等等;
  • 计算优化,避开浮点运算等。
  • 数据在网络上传输时,尽量压缩数据后再传输,建议用FlatBuffer序列化技术,这个比json效率高很多倍,不了解FlatBuffer,建议找资料学习一下。

Bitmap图片优化

  • 主要是对加载图片进行压缩,避免加载图片多大导致OOM出现。
  • 对图片本身进行操作。尽量不要使用setImageBitmap、setImageResource、BitmapFactory.decodeResource来设置一张大图,因为这些方法在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存.
  • 图片进行缩放的比例,SDK中建议其值是2的指数值,值越大会导致图片不清晰。
  • 不用的图片记得调用图片的recycle()方法

启动优化

  • 通过启动加载逻辑优化:可以采用分布加载、异步加载、延期加载策略来提高应用启动速度。
  • 数据准备:数据初始化分析,加载数据可以考虑用线程初始化等策略。
  • 异步加载:Application中加入异步线程、或MainActivity中加入异步线程
  • 延迟加载功能:首屏绘制完成之后加载
  • 动态加载布局:主布局文件优化
  • 主布局文件深度优化
  • 功能代码深度优化

数据结构优化

  • 使用Android自带的spareArray来代替HashMap;
  • 比如HashMap和ArrayMap,优先使用ArrayMap;
  • 优先使用基本类型,而非包装类
  • 减少占内存较大的枚举的使用
  • 采用三级缓存机制:LRUCache
  • 图片压缩:inSampleSize、RGB_565替换RGB_8888

27,apk安装包瘦身和优化

  • 减少应用中不必要的资源文件,比如图片,在不影响APP效果的情况下尽量压缩图片,有一定的效果
  • 在使用了SO库的时候,优先保留v7版本的SO库,删掉其他版本的SO库,如果你使用了很多SO库,比方说一个版本的SO库一共10M,那么只保留v7版本,删掉armeabi和v8版本的SO库,一共可以减少20M的体积。
  • res资源优化,(1)只使用一套图片,使用高分辨率的图片。(2)UI设计在ps安装TinyPNG插件,对图片进行无损压缩。(3)svg图片:一些图片的描述,牺牲CPU的计算能力的,节省空间。使用的原则:简单的图标。(4)图片使用WebP(https://developers.google.com/speed/webp/)的格式(Facebook、腾讯、淘宝在用。)缺点:加载相比于PNG要慢很多。但是配置比较高。工具:http://isparta.github.io/(5)使用tintcolor(android - Changedrawable color programmatically)实现按钮反选效果。
  • 代码优化,(1)实现功能模块的逻辑简化(2)Lint工具检查无用文件将无用的资源列在“UnusedResources: Unused resources”,删除。(3)移除无用的依赖库。
  • lib资源优化,(1)动态下载的资源。(2)一些模块的插件化动态添加。(3)so文件的剪裁和压缩。
  • assets资源优化,(1)音频文件最好使用有损压缩的格式,比如采用opus、mp3等格式,但是最好不要使用无损压缩的音乐格式(2)对ttf字体文件压缩,可以采用FontCreator工具只提取出你需要的文字。比如在做日期显示时,其实只需要数字字体,但是使用原有的字体库可能需要10MB大小,如果只是把你需要的字体提取出来生成的字体文件只有10KB
  • 代码混淆,使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
  • 插件化,可将功能模块放服务器,需要用时再加载。
  • 7z极限压缩,具体请参考微信的安接包压缩,实现实现原理,有时间再分析;

28,数据结构与算法算法

常见数据结构

常见算法

Java中常用数据结构

数组:

  • 数组是一种数据结构,可以在其中存储相同类型的元素。在Java中,我们可以使用数组来存储基本数据类型,如int,float,double等,以及对象类型。数组的一个主要优点是可以快速访问数组中的元素,因为每个元素都有一个索引值。
  • 使用数组的主要缺点是固定长度。一旦数组被创建,其大小就无法更改。 如果我们需要在数组中插入或删除元素,则必须先创建一个新数组,将所有元素复制到其中,然后再插入或删除所需元素。 这个过程的时间复杂度是O(n)

链表:

  • 链表是一种数据结构,可以用来存储具有相同类型的数据元素。与数组不同,链表中的元素不需要紧密排列在一起。每个元素被称为节点,并且包含一个存储元素的数据字段和一个指向下一个节点的指针。
  • 链表有很多种不同的类型,包括单链表,双向链表和循环链表。 链表的一个主要优点是可以动态添加和删除元素,因为它们不需要紧密排列在一起。 这个过程的时间复杂度是O(1)。
  • 链表的主要缺点在于对于某些操作,例如访问或搜索特定索引处的元素,访问时间要长,因为必须花费O(n)时间遍历链表中的元素

栈:

  • 栈是一种数据结构,可以用于存储和操作数据。栈可以在其顶部插入和删除元素。栈遵循“先进先出”的原则,因此该数据结构可表示为一个“后进先出”(LIFO)数据结构。因此,从栈顶删除元素之前必须先删除顶端的元素。
  • Java中的栈可以使用内置类java.util.Stack来实现。 它提供了很多不同的方法,例如push(将元素压入栈顶),pop(删除栈顶元素)和peek(返回栈顶元素)

队列:

  • 队列是一种数据结构,可以用于存储和操作数据。 队列可以在其末尾插入元素,并从其前面删除元素。 队列遵循“先进先出”的原则,因此可表示为“先进先出”(FIFO)数据结构。
  • Java中的队列可以使用内置类java.util.Queue来实现。 它提供了很多不同的方法,例如offer(向队列添加元素),poll(从队列的开始处删除元素)和peek(返回队列的开始处的元素)

哈希表:

  • 哈希表是一种数据结构,可以存储键值对。哈希表使用哈希函数将键值映射到数组中的索引,这使得访问和搜索哈希表的元素变得非常快。 哈希表的时间复杂度为O(1)。
  • Java中的哈希表可以使用内置类java.util.HashMap或java.util.Hashtable来实现。 它们在实现方式上略有不同,而Hashtable是线程安全的版本

树:

  • 树是一种数据结构,可以存储有层次关系的数据。 树是一种节点和边集合,其中每个节点包含一个值和指向其子节点的零个或多个指针。 树的根节点是唯一的,而其他节点则可以分为上级和下级。
  • Java中的树可以使用内置类java.util.TreeMap和java.util.TreeSet来实现。 它们使用平衡二叉树来最小化搜索或插入和删除元素的时间复杂度。 平衡二叉树保证树的高度不会超过O(log n)

开源框架(LivaData,Glide,OKHttp,Retrofit,Rxjava)

猜你喜欢

转载自blog.csdn.net/qq_29848853/article/details/130408717