之前8月份开始复习一些基础知识,并陆陆续续的总结了一些面试相关的东西,太久没写博客了,今天就做一个基础知识的分享吧。
无奈本人太蔡了,面试了这么多家没有收到一个offer。心灰意冷之后,听从朋友建议,现在已经开始学后端相关的知识了,以后可能发的博客也不是音视频相关的了,更多的是后端相关技术。
首先复习基础,这儿推荐github上的一个项目:
LearningNote
然后就是一些进阶需要掌握的知识 ,下面是我自己做的笔记,希望能帮到大家找到一个好的工作。
Android系统架构分为5层,从下到上依次为 Linux内核层,硬件抽象层,系统运行库层(Native),应用框架层,应用层。
Linux内核层:Android的核心基于Linux内核,在此基础上添加了Android的专用驱动(比如Binder)、系统的安全性、内存管理、进程管理等等。
硬件抽象层(HAL):有了核心还不行,你得需要运行到相应的硬件上才能实现自己的价值吧。而硬件抽象层就是硬件和Linux内核之间的接口,目的就是将硬件抽象化,保护硬件厂商的知识产权(Linux是有开源协议的)
系统运行库层:怎么操纵硬件,显示图像到屏幕?这一层就是干这个的,它分为两部分,分别是C++程序库和Android运行时。
C++程序库:被Android系统中的不同组件使用,可以通过应用框架层被开发者使用,下面是主要的程序库:
openGL ES | 3D绘图函数库 |
---|---|
Media Framework | 多媒体库 |
SQLite | 关系型数据库引擎 |
SSL | 安全套接层 |
Android Runtime:ART虚拟机(5.0之后,Dalvik虚拟机被ART取代),ART在应用第一次安装的时候,就会将字节码预编译成机器码存储到本地,这样应用每次运行就无须执行编译了(Dalvik是每次打开都要即时编译),典型的以空间换时间
应用框架层:Framework层,这层代码是用java编写的,为开发人员提供了API。
应用层。
1. Broadcast广播,当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据 2. Content Provider,多个应用程序之间数据共享的方式(跨进程共享数据) ,应用程序可以完成对数据的增删改查。Android系统本身也提供了很多的Content Provider,比如音频,视频,联系人等信息。 3. 通过AIDL文件,其中AIDL也是通过binder实现进程间通信的。 4. socket 复制代码
Android系统是基于Linux内核的,Linux提供了管道、消息队列、共享内存和socket等IPC机制。那为什么Android还要提供Binder来实现IPC呢?主要是基于性能、稳定性和安全性方面的考虑。
性能:socket作为通用接口,传输效率低,开销大,主要用到跨网络进程通信。消息队列、共享内存和管道采用存储-转发模式,数据拷贝至少需要两次,共享内存虽然无需拷贝,但是控制复杂,难以使用。而binder只需要拷贝一次,性能上只次于共享内存。
稳定性:Binder 基于 C/S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。
安全性:Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。传统的 IPC 只能由用户在数据包中填入 UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标识只有由 IPC 机制在内核中添加。其次传统的 IPC 访问接入点是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝
接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种 内存映射 的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
参考资料: 写给 Android 应用工程师的 Binder 原理剖析
参考: 写给Android App开发人员的Android底层知识
在用法上,它是java的关键字,一般我们不太需要关注他的锁的释放,代码执行完毕或者报错会自动释放锁,并且无法判断锁的状态。
是一个接口,我们使用 ReentrantLock 比较多,有多个获取锁的方式,可以trylock直接返回获取成功或者失败,线程不用一直等待。在finally中必须要释放该锁。
注:引用G神的博客: 点击链接直达
Android中的view是树形结构的,view可能会重叠在一起,当我们点击的地方有多个view的时候,这个时间该给谁,这就是为什么要有事件分发。
先来看看view的树形结构:
上面多出来两个东西是 phonewindow
和 decorview
,其中,主题颜色和标题栏内容等主要就是decorview来负责显示的,那 PhoneWindow
是做什么的呢?
PhoneWindow
继承 window
,并且是 window
唯一的实现类, window
是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴,它其实就像是View界的太上皇。
DecorView` 是 `PhoneWindow` 的一个内部类,其职位相当于小太监,就是跟在 `PhoneWindow` 身边专业为 `PhoneWindow` 服务的,除了自己要干活之外,也负责消息的传递,`PhoneWindow` 的指示通过 `DecorView` 传递给下面的 View,而下面 View 的信息也通过 `DecorView` 回传给 `PhoneWindow` 复制代码
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | X | √ | X |
事件消费 | onTouchEvent | √ | √ | √ |
Activity作为原始的事件分发者,不需要拦截事件,如果需要这个事件不分发下去就行了。
同样的,view在事件传递的最末端,也不需要拦截事件,不处理回传回去就行了。
事件在收集之后最先传递给Activity,然后依次向下传递:
Activity -> PhoneWindow -> DectorView -> ViewGroup -> ... -> view 复制代码
如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃 :
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View 复制代码
上面的模式是一个非常经典的责任链模式
注: 参考链接
在activity的attach方法里面,会创建一个PhoneWindow。
在onCreate中调用setContentView, setContentView
是 window
的一个抽象方法,真正实现类是 PhoneWindow
:
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { //1.初始化 //创建DecorView对象和mContentParent对象 ,并将mContentParent关联到DecorView上 installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews();//Activity转场动画相关 } //2.填充Layout if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene);//Activity转场动画相关 } else { //将Activity设置的布局文件,加载到mContentParent中 mLayoutInflater.inflate(layoutResID, mContentParent); } //让DecorView的内容区域延伸到systemUi下方,防止在扩展时被覆盖,达到全屏、沉浸等不同体验效果。 mContentParent.requestApplyInsets(); //3. 通知Activity布局改变 final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { //触发Activity的onContentChanged方法 cb.onContentChanged(); } mContentParentExplicitlySet = true; } 复制代码
核心方法就两个:installDecor() 和 mLayoutInflater.inflate(layoutResID, mContentParent) ;
installDecor会创建一个 DecorView
对象,该对象将作为整个应用窗口的根视图。然后配置不同窗口修饰属性(style theme等)。
mLayoutInflater.inflate就是解析xml,深度优先地递归解析xml,一层层添加到root view上,最终返回root view.解析的部分大致包含两点:1.解析出View对象,2.解析View对应的Params,并设置给View。
有四种情况会造成ANR发生:
onReceive()
函数时10秒没有处理完成,后台为20秒 ContentProvider
的 publish
在10s内没进行完 如何避免:
尽量避免在主线程中做耗时操作。 多线程==>引出如何实现多线程,线程池的使用
如何分析ANR:
data/anr/
目录下生成一个文件 traces.txt
Logcat
中查看 app启动加速:一个app的启动分为三种不同的状态,其中,我们只需要对第一种状态做优化。对于App来说, 我们可以控制的启动时间线无外乎 Application的onCreate
,首屏Activity的渲染。
布局优化 减少不必要的嵌套
响应优化
内存优化 bitmap的使用,options的jusdecodeBounds属性,设置只解析bitmap的宽高等,然后使用insimplesize对bitmap进行压缩。在android2.3的时代,bitmap的回收需要调用recycler方法,并且置空,但是之后只需要进行置空操作。
加载一张大图:使用BitmapRegionDecode进行局部解码,
lru算法的实现=> LinkedHashMap
电池使用优化
网络优化
参考: Android App优化, 要怎么做?
一步手机的刷新率为60hz,当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(如VSync), 60Hz的屏幕就会一秒内发出 60次这样的信号。而这个信号主要是用于同步CPU、GPU和显示器的。
一般地来说,计算机系统中
,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照同步信号从帧缓冲区取帧数据传递给显示器显示。
面试几家之后发现都没有问,所以略过
GC:垃圾回收器,自动释放垃圾占用的空间,让创建的对象不需要像c/c++那样手动delete、free掉。
GC是在什么时候,对什么东西,做了什么事情?
Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
minor gc/full gc的触发条件、OOM的触发条件,降低GC的调优的策略 (深入理解jvm)
minor gc :当新生代的eden区满了的时候,会触发gc
full gc: 当老年代空间不足,方法区空间不足,minor gc进入老年代的时候。大对象
从gc root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象
JVM将堆分成了二个大区新生代(Young)和老年代(Old),新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace和ToSpace组成。
Young中的98%的对象都是死朝生夕死,所以将内存分为一块较大的Eden和两块较小的Survivor1、Survivor2,JVM默认分配是8:1:1,每次调用Eden和其中的Survivor1(FromSpace),当发生回收的时候,将Eden和Survivor1(FromSpace)存活的对象复制到Survivor2(ToSpace)
注:分成三块是为了充分利用内存。原来是只有一块内存,在这上面做标记清除算法,但是gc之后会存在大量不连续的空间,所以有人提出将内存一分为二,将第一块内存存活的对象转移到第二块内存,然后将第一块内存gc。
新生代的GC(Minor GC):新生代通常存活时间较短基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
老年代的GC(Major GC/Full GC):老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。
参考: 全面理解java内存模型 ,深入理解java虚拟机
Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
类从被加载到虚拟机内存中开始,到卸载出内存中为止,它的整个生命周期如下:
类加载模型
双亲委派模型,从java虚拟机的角度讲,只存在两种不同的类加载器,启动类加载器和所有的其他类加载器,启动类加载器使用C++语言实现,是虚拟机的一部分,其他的类加载器由java语言实现,独立于虚拟机外,对于java开发人员来说,类加载器划分得更细:
双亲委派的工作过程:如果一个类加载器收到了类加载的请求,首先它不会自己去尝试加载这个类,而是把这个请求委派给父类去完成,最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求的时候,子加载器才会尝试自己去加载。
Android中常用的类加载器有: DexClassLoader
和 PathClassLoader
,这两个类都继承自 BaseDexClassLoader
,
PathClassLoader
用于加载内部的Dex文件, DexClassLoader
多传了一个optimizedDirectory参数 ,可以用来加载外部的apk文件(插件化技术的核心)
代码修复主要有两大方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案,这两种方案各有优劣。
底层替换方案限制比较多,但是时效性最好,加载快,立即见效
类加载方案时效性差,需要冷启动才能见效,但修复范围广,限制少。
直接在已加载类中替换掉原有方法,即在原来类基础上进行修改,因此无法实现增减原有类方法或字段,这样会破坏原有类的结构。
热修复原理以及类加载流程
在app重新启动后让Classloader加载新的类。因为当app运行到一半时,所需发生变更的类已经被加载过,而在Android上无法对一个类进行卸载操作,若不重启,原来的类还存储于虚拟机中,新类无法被加载。因此只有在下次重启时,在业务逻辑运行之前抢先加载补丁中的新类,这样后续访问此类时,才会Resolve为新类,从而达到热修复的目的。
参考: 深入源码分析Java线程池的实现原理
池化技术
简单的来讲就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率。比较典型的池化技术有线程池、连接池、内存池、对象池等。
为什么使用线程池
常见的线程池:
ThreadPoolExecutor FixedThreadPool CachedThreadPool SingleThreadPool ScheduledThreadPool
上面的2-5的线程池,都是基于第一个基本线程池实现的,不同的地方在于核心线程数和存放任务的队列类型不同。
如何使用
在 Executors
这个类中封装了很多方法。
有两种方法提交任务,submit和execute,其中submit就是将runnable包装成FutureTask 而已,最终调用的还是execuet,所以我们看execute的实现过程
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { //直接新建核心线程 if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { //线程池是否在运行,添加到队列里等待 int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) //线程池如果没有在运行,移除队列并拒绝任务 reject(command); else if (workerCountOf(recheck) == 0) //核心线程满时,创建非核心线程执行任务 addWorker(null, false); } else if (!addWorker(command, false)) //拒绝 reject(command); } 复制代码
主要的工作是:
阮一峰-互联网协议入门
注:参考 图解HTTPS协议加密解密全过程
在传输层和应用层之间添加了一个SSL层,变成HTTP先和SSL通信,再由SSL和TCP通信
加密方式有两种:
对称密钥加密:加密和解密同用一个密钥。加密时就必须将密钥传送给对方。那密钥如何保证安全传输的?这是一个先有鸡还是先有蛋的问题,传输内容怎么加密的密钥也可以这么加密
非对称加密:使用一对非对称的密钥。一把叫做私有密钥,一把叫做公开密钥。私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
过程:
注: 一步步编写操作系统
任何不兼容的问题,都可以通过增加一“层”来解决。在cpu和外设之间的这一层就是IO接口。IO接口形式不限,它可以是个电路板,也可以是块芯片,甚至可以是个插槽,它的作用就是在cpu和外设之间相互做协调转换,如cpu和外设速度不匹配,它就是变速箱,cpu和外设信号不通用,它就是翻译机。
一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到
实现准备多个计算散列值得函数,发生冲突之后,换一种计算散列值的函数。
在发生冲突的地方插入一条单链表,将冲突的元素都放到这个链表里。
java的hashmap就是使用的这种方法解决冲突 >>通过数组+链表+(红黑树,jdk1.8之后对hashmap做了优化)的方式,将hashcode相同的元素串成一串。