1.什么是协程?
什么是协程
协程,英文名是 Coroutine, 又称为微线程,是一种用户态的轻量级线程。协程不像线程和进程那样,需要进行系统内核上的上下文切换,协程的上下文切换是由程序员决定的。
协程相对于多线程的优点
多线程编程是比较困难的,因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断。
而协程默认会做好全方位保护, 以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行。总结下大概下面几点:
无需系统内核的上下文切换,减小开销;
无需原子操作锁定及同步的开销,不用担心资源共享的问题;
单线程即可实现高并发,单核 CPU 即便支持上万的协程都不是问题,所以很适合用于高并发处理,尤其是在应用在网络爬虫中。
协程的缺点
同样的总结下大概以下 2 点。
无法使用 CPU 的多核
协程的本质是个单线程,它不能同时用上单个 CPU 的多个核,协程需要和进程配合才能运行在多 CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,就比如网络爬虫来说,限制爬虫的速度还有其他的因素,比如网站并发量、网速等问题都会是爬虫速度限制的因素。除非做一些密集型应用,这个时候才可能会用到多进程和协程。
处处都要使用非阻塞代码
写协程就意味着你要一值写一些非阻塞的代码,使用各种异步版本的库,比如后面的异步爬虫教程中用的 aiohttp 就是一个异步版本的request库等。 不过这些缺点并不能影响到使用协程的优势。
- 内存泄露及解决办法:
内存泄露就是指该被GC垃圾回收的,由于有另外一个对象仍然在引用它,导致无法回收,造成内存泄露,过多的内存泄露会导致OOM。
android中的内存泄露通常是Activity或者Fragment的泄露。下文分析以Activity展开,Fragment同理。
- 非静态内部类、匿名内部类
- 静态的View
- Handler
- 监听器(各种需要注册的Listener,Watcher等)
- 资源对象没关闭造成内存泄漏
- 属性动画
- RxJava
- WebView
- 其他的系统控件以及自定义View
- 不一定非要使用弱引用才行
解决办法: - 非静态内部类、匿名内部类
非静态内部类、匿名内部类 都会持有外部类的一个引用,如果有一个静态变量引用了非静态内部类或者匿名内部类,导致非静态内部类或者匿名内部类的生命周期比外部类(Activity)长,就会导致外部类在该被回收的时候,无法被回收掉,引起内存泄露, 除非外部类被卸载(JVM自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载,除非使用自定义的类加载器,感兴趣的同学可以研究一下)。
解决办法:
将非静态内部类、匿名内部类 改成静态内部类,或者直接抽离成一个外部类。
如果在静态内部类中,需要引用外部类对象,那么可以将这个引用封装在一个WeakReference中。如下面代码所示: - 静态的View
有时,当一个Activity经常启动,但是对应的View读取非常耗时,我们可以通过静态View变量来保持对该Activity的rootView引用。这样就可以不用每次启动Activity都去读取并渲染View了。这确实是一个提高Activity启动速度的好方法!但是要注意,一旦View attach到我们的Window上,就会持有一个Context(即Activity)的引用。而我们的View有事一个静态变量,所以导致Activity不被回收。
解决办法:
在使用静态View时,需要确保在资源回收时,将静态View detach掉。 - Handler
我们知道,主线程的Looper对象不断从消息队列中取出消息,然后再交给Handler处理。如果在Activity中定义Handler对象,那么Handler肯定是持有Activty的引用。而每个Message对象是持有Handler的引用的(Message对象的target属性持有Handler引用),从而导致Message间接引用到了Activity。如果在Activty destroy之后,消息队列中还有Message对象,Activty是不会被回收的。当然了,如果消息正在准备(处于延时入队期间)放入到消息队列中也是一样的。
解决办法:
将Handler放入单独的类或者将Handler放入到静态内部类中(静态内部类不会持有外部类的引用)。如果想要在handler内部去调用所在的外部类Activity,可以在handler内部使用弱引用的方式指向所在Activity,这样不会导致内存泄漏。
或者在onDestory时,调用相应的方法移除回调和删除消息。 - 监听器(各种需要注册的Listener,Watcher等)
当我们需要使用系统服务时,比如执行某些后台任务、为硬件访问提供接口等等系统服务。我们需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。
例如:EditText的一个addTextChangeListener,如果在回调方法里有耗时操作,可能会造成内存泄露。
解决办法:
在onDestory时,取消注册,editText.removeTextChangedListener - 资源对象没关闭造成内存泄漏
当我们打开资源时,一般都会使用缓存。比如读写文件资源、打开数据库资源、使用Bitmap资源等等。当我们不再使用时,应该关闭它们,使得缓存内存区域及时回收。虽然有些对象,如果我们不去关闭,它自己在finalize()函数中会自行关闭。但是这得等到GC回收时才关闭,这样会导致缓存驻留一段时间。如果我们频繁的打开资源,内存泄漏带来的影响就比较明显了。
解决办法:
及时关闭资源 - 属性动画
在使用ValueAnimator或者ObjectAnimator时,如果没有及时做cancel取消动画,就可能造成内存泄露。
因为在cancel方法里,最后调用了endAnimation(); ,在endAnimation里,有个AnimationHandler的单例,会持有属性动画对象的引用,如下代码所示;
解决办法:
在在onDestory时,调用动画的cancel方法 - RxJava
在使用RxJava时,如果在发布了一个订阅后,由于没有及时取消,导致Activity/Fragment无法销毁,导致的内存泄露
解决办法:
参考Uber出品的一个开源库AutoDispose的使用,可以参考下文:
Android架构中添加AutoDispose解决RxJava内存泄漏 - WebView
在android 5.1及以上版本的代码中,WebView可能会存在内存泄露,
原因可以参考这篇文章:Android 5.1 WebView内存泄漏问题及解决
解决办法:
在销毁webview前一定要onDetachedFromWindow,我们先将webview从它的父view中移除再调用destroy方法,代码如下: - 其他的系统控件以及自定义View
在 Android Lollipop 之前使用 AlertDialog 可能会导致内存泄漏
参考:一个内存泄漏引发的血案
Dialog和DialogFragment在Android5.0以下的内存泄漏
参考:解决Android5.0以下Dialog引起的内存泄漏
View的post方法导致的内存泄漏分析
view中有线程或者动画 要及时停止
这是为了防止内存泄漏,可以在onDetachedFromWindow方法中结束,这个方法回调的时机是 当View的Activity退出或者当前View被移除的时候 会调用 这时候是结束动画或者线程的好时机 另外还有一个对应的方法 onAttachedToWindow 这个方法调用的时机是在包含View的Activity启动时 回调 回调在onDraw方法 之前
不一定非要使用弱引用才行
如避免AsyncTask内存泄漏的简单例子:
这里是AsyncTask:
当然这个例子非常基础,但是我认为作为另一种解决方案的演示来说足够了。
这里是另一个使用RxJava实现的简单例子,我们仍然没有使用弱引用。
参考:https://blog.csdn.net/unicorn97/article/details/81009204进程间的五种通信方式介绍
数据结构之数组和链表的区别
数组(Array)
一、数组特点:
所谓数组,就是相同数据类型的元素按一定顺序排列的集合;数组的存储区间是连续的,占用内存比较大,故空间复杂的很大。但数组的二分查找时间复杂度小,都是O(1);数组的特点是:查询简单,增加和删除困难;
1.1 在内存中,数组是一块连续的区域
1.2 数组需要预留空间
在使用前需要提前申请所占内存的大小,如果提前不知道需要的空间大小时,预先申请就可能会浪费内存空间,即数组的空间利用率较低。注:数组的空间在编译阶段就需要进行确定,所以需要提前给出数组空间的大小(在运行阶段是不允许改变的)
1.3 在数组起始位置处,插入数据和删除数据效率低。
插入数据时,待插入位置的元素和他后面的所有元素都需要向后搬移
删除数据时,待删除位置后面的所有元素都需要向前搬移。
1.4 随机访问效率很高,时间复杂度可以达到O(1)
因为数组的内存是连续的,想要访问那个元素,直接从数组的首地址向后偏移就可以访问到了。
1.5 数组开辟的空间,在不够使用的时候需要进行扩容;扩容的话,就涉及到需要把旧数组中的所有元素向新数组中搬移。
1.6 数组的空间是从栈分配的。(栈:先进后出)
二、数组的优点:
随机访问性强,查找速度快,时间复杂度是0(1)
三、数组的缺点:
3.1 从头部删除、从头部插入的效率低,时间复杂度是o(n),因为需要相应的向前搬移和向后搬移。
3.2 空间利用率不高
3.3 内存空间要求高,必须要有足够的连续的内存空间。
3.4 数组的空间大小是固定的,不能进行动态扩展
链表(ListNode)
一、链表的特点:
所谓链表,链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:查询相对于数组困难,增加和删除容易。
1.1 在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续。
1.2 链表中的元素有两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址。
每一个数据都会保存下一个数据的内存地址,通过该地址就可以找到下一个数据
1.3 查找数据时间效率低,时间复杂度是o(n)
因为链表的空间是分散的,所以不具有随机访问性,如果需要访问某个位置的数据,需要从第一个数开始找起,依次往后遍历,知道找到待查询的位置,故可能在查找某个元素时,时间复杂度是o(n)
1.4 空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
1.5 任意位置插入元素和删除元素时间效率较高,时间复杂度是o(1)
1.6 链表的空间是从堆中分配的。(堆:先进先出,后进后出)
二、链表的优点
2.1 任意位置插入元素和删除元素的速度快,时间复杂度是o(1)
2.2 内存利用率高,不会浪费内存
2.3 链表的空间大小不固定,可以动态拓展。
三、链表的缺点
随机访问效率低,时间复杂度是o(1)
4.Bitmap对象的理解
1.Bitmap在Android中指的是一张图片。
2.通过BitmapFactory类提供的四类方法:
1)decodeFile(从文件系统加载出一个Bitmap对象)
2)decodeResource(从资源中加载出一个Bitmap对象)
3)decodeStream(从输入流中加载出一个Bitmap对象)
4)decodeByteArray(从字节数组中加载出一个Bitmap对象)
其中decodeFile , decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。
3.BitmapFactory.Options的参数
①inSampleSize参数
上述四类方法都支持BitmapFactory.Options参数,而Bitmap的按一定采样率进行缩放就是通过BitmapFactory.Options参数实现的,主要用到了inSampleSize参数,即采样率。通过对inSampleSize的设置,对图片的像素的高和宽进行缩放。
当inSampleSize=1,即采样后的图片大小为图片的原始大小。小于1,也按照1来计算。
当inSampleSize>1,即采样后的图片将会缩小,缩放比例为1/(inSampleSize的二次方)。
关于inSampleSize取值的注意事项:
通常是根据图片宽高实际的大小/需要的宽高大小,分别计算出宽和高的缩放比。但应该取其中最小的缩放比,避免缩放图片太小,到达指定控件中不能铺满,需要拉伸从而导致模糊。
②inJustDecodeBounds参数
我们需要获取加载的图片的宽高信息,然后交给inSampleSize参数选择缩放比缩放。
想先不加载图片却能获得图片的宽高信息,可通过inJustDecodeBounds=true,然后加载图片就可以实现只解析图片的宽高信息,并不会真正的加载图片,所以这个操作是轻量级的。
当获取了宽高信息,计算出缩放比后,然后在将inJustDecodeBounds=false,再重新加载图片,就可以加载缩放后的图片。
4.高效加载Bitmap的流程
①将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。这里要设置Options.inJustDecodeBounds=true,这时候decode的bitmap为null,只是把图片的宽高放在Options里,
②从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
③根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
④将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
5.什么是深拷贝与浅拷贝
浅拷贝:
对内存地址的复制,让目标对象指针和源对象指向同一片内存空间。注意:当内存销毁的时候,只想对象的指针,必须重新定义,才能够使用
浅拷贝是一个传址,也就是把a的值赋给b的时候同时也把a的址赋给了b,当b(a)的值改变的时候,a(b)的值同时也会改变
深拷贝:深拷贝是指,拷贝对象的具体内容,二内存地址是自主分配的,拷贝结束之后俩个对象虽然存的值是一样的,但是内存地址不一样,俩个对象页互相不影响,互不干涉
深拷贝的几种方法:
1、JSON内置的方法
- var a={x:1}
- var b=JSON.parse(JSON.stringfiy(a))
- console.log(b)//{x:1}
- b.x=2
- console.log(b)//{x:2}
- console.log(a)//{x:1}
原理:该方法是用JSON.parse将对象转为字符串,然后在用JSON.stringify转回对象json字符串转换为对象的时候,会自己去构建新的内存地址存放数据
注:如果对象属性为function,因为JSON格式字符串不支持function,在构建的时候会自动删除
2、Object的内置方法assign
var a={x:1}
- var b=Object.assign({}, a);
- console.log(b); //{x:1}
- b.x = 2;
- console.log(b); //{x:2}
- console.log(a);
- 原理:该方法是用Object.assign对对象进行拼接, 将后续对象的内容插入到第一个参数指定的对象,不会修改第一个参数之后的对象,而我们将第一个对象指定为一个匿名空对象,实现深拷贝
注:对象嵌套层次过深,超过2层,就会出现浅拷贝的状况,比如echarts组件的option对象
3.递归实现 - 对象锁和类锁是否会互相影响?
·对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
· 类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。
· 类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。 - Android Looper用法及原理
Looper是android为线程间异步消息通信提供的一种机制,利用Looper机制可以方便我们实现多线程编程时线程间的相互沟通。当然,如果不用Looper而采用其它的线程间通信方式(像管道,信号量,共享内存,消息队列等)也是一样的。Looper的实现是利用消息队列的方式,为用户封装了一层API,用户不需要考虑互斥加锁的问题,方便用户的使用。
8.Handler机制相关概念
• Looper:一个线程可以产生一个Looper对象,由它来管理此线程里的MessageQueue(消息队列)。
• Handler:你可以构造Handler对象来与Looper沟通,以便push新消息到MessageQueue里;或者接收Looper从Message Queue取出发送来的消息。
• Message Queue(消息队列):用来存放线程放入的消息。
• 线程:UIthread 通常就是main thread,而Android启动程序时会替它建立一个MessageQueue。 - final、finally、finalize的区别
一、final
如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在new一个对象时初始化(即只能在声明变量或构造器或代码块内初始化),而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能覆盖(重写)。
二、finally
在异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
finally和return的执行顺序:
https://blog.csdn.net/jdfk423/article/details/80406297
情况一:
如果程序是从try代码块或者catch代码块中返回时,finally中的代码总会执行。而且finally语句在return语句执行之后return返回之前执行的。可以使用编译器的Debug功能查看详细过程。
情况二:
我们可以看到当finally有返回值时,会直接返回。不会再去返回try或者catch中的返回值。
情况三:
如果try和catch的return是一个变量时且函数的是从其中一个返回时,后面finally中语句即使有对返回的变量进行赋值的操作时,也不会影响返回的值。
三、finalize
方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。注意:finalize不一定被jvm调用,只有当垃圾回收器要清除垃圾时才被调用。
ActivityThread,AMS,WMS的工作原理
1.ActivityThread:是Android应用的主线程(UI线程)
2.WMS(WindowManagerService)管理的整个系统所有窗口的UI
作用:
为所有窗口分配Surface:客户端向WMS添加一个窗口的过程,其实就是WMS为其分配一块Suiface的过程,一块块Surface在WMS的管理下有序的排布在屏幕上。Window的本质就是Surface。(简单的说Surface对应了一块屏幕缓冲区)
管理Surface的显示顺序、尺寸、位置
管理窗口动画
输入系统相关:WMS是派发系统按键和触摸消息的最佳人选,当接收到一个触摸事件,它需要寻找一个最合适的窗口来处理消息,而WMS是窗口的管理者,系统中所有的窗口状态和信息都在其掌握之中,完成这一工作不在话下。
3.AMS(ActivityManagerService)
ActivityManager是客户端用来管理系统中正在运行的所有Activity包括Task、Memory、Service等信息的工具。但是这些这些信息的维护工作却不是又ActivityManager负责的。在ActivityManager中有大量的get()方法,那么也就说明了他只是提供信息给AMS,由AMS去完成交互和调度工作。
作用:
统一调度所有应用程序的Activity的生命周期
启动或杀死应用程序的进程
启动并调度Service的生命周期
注册BroadcastReceiver,并接收和分发Broadcast
启动并发布ContentProvider
调度task
处理应用程序的Crash
查询系统当前运行状态volatile关键字的作用
1 保证内存可见性(变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值
)
2 禁止指令重排序(单例)
3 不保证原子性
注释:尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全
多线程安全问题,可以采取加锁synchronized的方式,也可以使用concurrent包下的原子类AtomicInteger