《Android开发艺术探索》笔记二

第十章:Android的消息机制
  1. Handler是Android消息机制的上层接口,开发人员只需要与它交互即可,底层需要Looper与MessageQueue的支持,MessageQueue是单链表数据结构存储Message,Looper存储在ThreadLocal中,与线程关联,三者配合完成指定逻辑在指定线程的顺序执行。
  2. Handler是在某一个线程中创建的(并提前完成了消息的实现逻辑),然后在其他线程中去发送消息,原线程的Looper执行到该消息会调用Handler的消息处理逻辑(在原线程),因此Handler可以轻松的实现将业务逻辑在指定线程中去调用,最常见的就是更新UI的业务逻辑在子线程触发,在主线程处理。
  3. 由于控件的更新不是线程安全的,因此要使用主线程Handler去更新UI,使得更新逻辑在单线程顺序执行,避免了并发带来的问题。PS:处理并发也可以在控件的更新方法上synchronized加锁,但是效率低,因此采用单线程处理,类似Java并发中的阻塞队列。
  4. 线程默认是没有Looper的,创建Handler时也会进行检查,没有Looper的话会抛出异常,因此在子线程使用Handler要先自己创建Looper并开启循环;主线程默认已有Looper且已启动了Looper.loop(),因此主线程可以直接创建Handler,通过Looper.getMainLooper()可直接获取到主线程Looper。
  5. Handler、Looper、MessageQueue大致工作流程:
    一个线程只能创建一个Looper,业务过多可以创建多个Handler;Looper内部创建并管理一个messageQueue,线程中创建Handler时也会检查是否已经含有Looper(主要为了与其中的messageQueue建立关联),然后looper.loop()方法会不断循环messageQueue去取消息,messageQueue的next()方法取消息是阻塞的,因此looper无限循环取调用,是阻塞的;其他线程想要执行指定逻辑到创建Handler的线程时,就可以调用Handler的send()方法或post()方法,最终消息会被Handler发到其内部的messageQueue中,然后Looper会单线程顺序拿取里面的消息同时再调msg.target(其实就是handler)的dispatchMessage()方法,dispatchMessage()方法是Handler里的,最终会被下发到handleMessage()方法中,也就是最初创建Handler时实现的重写方法,这样一来消息就是提前在创建者中实现,然后在调用者中去触发,完成将指定好的逻辑放到指定线程中执行的功能。
  6. ThreadLocal的工作原理:
    一般而言线程是内存数据共享的,ThreadLocal是个类似全局数据线程作用域管理类,可以轻松实现不同线程存取不同的数据副本,Looper就是一个需要每个线程独享的数据,当然实现类似方式也可用对象一级一级传递下去(始终保持同一个引用),或者全局维护静态单例对象等;ThreadLocal有个内部类ThreadLocalMap(不是Map,里面有个table数组),里面有个弱引用Entry包装类(弱引用ThreadLocal自己),包装了value(具体数据),table数组存着这些Entry,每个线程有个threadLocals数据域(ThreadLocalMap类型);当使用ThreadLocal存数据时,set()方法会根据线程去取其中的threadLocals,然后将ThreadLocal作为key去寻找放入位置,创建Entry对象包装value并放入table数组;当取数据时,get()方法会根据线程去取其中的threadLocals,然后将ThreadLocal作为key去table数组中找到其中的Entry,然后拿到Entry的value返回数据;这里ThreadLocal作为key主要是通过hash等去找出要存储的位置下标,table是个数组,将生成的Entry包装类放入数组指定的位置。
  7. MessageQueue的工作原理:
    MessageQueue叫消息队列,但本身的实现并不是队列,是一个单链表的数据结构,在插入和删除上比较有优势(不过顺序插入删除没啥体现吧);它只有插入enqueueMessage()和读取next()两个操作,读取伴随着删除,next()方法是阻塞的。
  8. Looper的工作原理:
    Looper.prepare()方法会创建一个looper,内部创建并管理一个messageQueue,Looper.loop()方法会执行for循环无限循环消息队列去取消息,由于消息队列的next()方法是阻塞的,因此它的loop()方法也是阻塞的;由于主线程比较特殊,Looper提供了getMainLooper()方法可以在任何地方获取到主线程的Looper对象;Looper提供了quit()quitSafely()方法用于退出Looper,类似线程池的shutdown()方法,前者立即退出,后者等消息队列处理完后退出,退出后Handler的send()方法会返回false,在子线程中我们自己创建的Looper在不再使用时最好手动quit()一下终止循环;当Looper标记成退出状态后,messageQueue的next()方法就会返回null,for循环就会return退出。
  9. Handler的工作原理:
    Handler就是发送和处理消息,发送使消息顺序执行,处理使任务到指定线程中执行,发送消息时直接调用queue.enqueueMessag()将消息插入队列,查询方法由Looper来调,是阻塞的;消息分发判断顺序为post发出的Runnable > 创建Handler的callback > Handler的handleMessage();Handler其实就是提前包装好业务逻辑,由其他线程发起消息,然后原线程Looper循环到消息后调用Handler来处理消息;Handler的构造方法中可以直接传入一个Looper,就可以指定消息发送到该线程处理了,HandlerThread就是一个自带Looper的线程,提供出Looper来给外部创建Handler使用,实现单线程消息循环机制。
  10. Android的主线程就是ActivityThread,主线程入口方法为main()方法,内部会Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程消息循环,ActivityThread中的H用来给AMS的Binder回调回来提交到主线程做一些回调使用。
    主线程的消息循环需要的Handler就是ActivityThread.H。
  11. 两个问题:
    • 主线程Looper的死循环为何不会导致卡死:循环里queue.next()会阻塞,所以死循环并不会一直执行,相反的,大部分时间是没有消息的,所以主线程大多数时候都是处于休眠状态,也就不会消耗太多的CPU资源导致卡死;阻塞的原理是使用Linux的管道机制实现的,主线程没有消息处理时阻塞在管道的读端,Binder线程会往主线程消息队列里添加消息,然后往管道写端写一个字节,这样就能唤醒主线程从管道读端返回,也就是说looper循环里queue.next()会调用返回。
    • 主线程的死循环阻塞了如何处理其它事务:在Looper.prepareMainLooper()Looper.loop()之间创建了ActivityThread并thread.attach(false),已经将主线程的Handler放出去了,自己的代码逻辑都是通过H来驱动执行的,只需要通过binder线程向H发送消息即可,所以说Android应用程序也是依靠消息驱动来工作的。
第十一章:Android的线程和线程池
  1. 线程是操作系统调度的最小单元,同时也是一种有限的系统资源,不可无限制的创建,线程不可能做到绝对的并行,除非线程数小于等于CPU的核心数,一般来说是不会的。
  2. 主线程指进程锁拥有的线程,与Java一样,进程一开始就有一个线程(主线程或UI线程),主线程要保持较高的响应速度(与用户前台交互),因此不能有太多的耗时操作,主线程以外都是子线程,子线程也叫工作线程,用于异步执行耗时操作。
  3. Andorid中的几种线程形态:
    ①AsyncTask:
    • 轻量级异步任务类,底层使用线程池+Handler实现,提供了几种方法回调(不同线程中),方便开发者在子线程中做耗时任务后更新UI。
    • 几点限制:必须主线程加载(4.1及以上版本已经被系统自动完成);必须在主线程创建(内部的Handler是static的,类加载时就会创建,因此必须在主线程);excute方法必须在主线程调用;一个AsyncTask只能执行一次,后续会抛异常。
    • 1.6之前任务是串行执行,1.6~3.0使用线程池并发执行,3.0以又是串行执行,但可以通过executeOnExecutor()来强制并行执行;因此3.0以前最大128个线程并发,10个缓冲队列,多了就默认拒绝策略抛异常了;3.0以后用一个Deque包装Runnable,循环取消息并执行,因此进程中的所有AsyncTask都在这串行线程池中排队执行。
    • 不建议执行特别耗时的任务,特别耗时的任务建议使用线程池来执行。
    ②HandlerThread:
    • 内部创建了Looper并开启了循环,可以在外部获取此Looper来创建Handler,将任务封装到外部来调用,在该Thread中顺序执行,类似主线程消息机制。
    • 由于内部Looper是无限循环的,使用完毕记得调用quit()quitSafely()一下来终止线程。
    ③IntentService:
    • 本身是个服务,内部有一个worker(HandlerThread)线程可以执行耗时任务,当onHnadleIntent()方法执行结束后,它会自动stopSelf(id)来结束自己,stopSelf()会立即停止服务,stopSelf(id)会等所有消息处理完毕后停止服务;每次启动一次服务,都会向Handler发出一条message,多个任务顺序执行。
    • 服务的优先级更高一些,如果是一个后台线程,没有四大组件的依赖在内存不足时容易被杀死,而服务的优先级较高,这种在服务中再开启线程执行任务,任务线程则不易被杀死。
  4. 线程池相关:
    • 作用:重用线程、有效控制最大并发数、简单的线程管理。
    • 构造方法7个参数:corePooSize(核心线程)、maximumPoolSize(最大线程)、keepAliveTime(空闲线程存活时间)、unit(时间单位)、workQueue(阻塞队列)、threadFactory(线程创建工厂)、rejectedExecutionHandler(拒绝策略)。
    • 拒绝策略:AbortPolicy(抛异常 默认)、CallerRunsPolicy(在调用者线程直接用)、DiscardPolicy(拒绝不抛异常)、DiscardOldestPolicy(将最早任务删掉加入任务队列)。
    • 内部调用顺序:核心线程 > 任务队列 > 最大线程 > 拒绝策略。
  5. 四种常见线程池:
    • FixedThreadPool:核心数=最大数,0存活时间;核心线程不会被回收,适用于快速处理任务。
    • SingleThreadExecutor:核心数=最大数=1,0存活时间;特殊的Fix线程池,单线程顺序执行,不需要处理同步问题。
    • CachedThreadPool:核心数=0,最大数=MAX,存活时间60s,无任务队列;适用于大量短任务,有空闲的直接用,没有就创建。
    • ScheduledThreadPool:核心=自定义固定值,最大=MAX,0存活时间;适用于固定周期的重复任务执行。

注意:
主线程方法体中使用handler的post()方法,由于同在主线程,当前方法体执行完后才会可能开始执行msgQueue队列中的下一个消息;而thread.start()之后子线程就处于准备状态要开始并行执行了,与当前方法体下一行代码的执行顺序是不可控的(不过一般在子线程做的都是耗时的逻辑,不会那么快跑完)。

  1. 多线程相关类:
    • Runnable可以在线程或线程池中执行,Callable、Future、FutureTask只能在线程池中执行。
    • Callable:也是一个业务包装类,包装到call方法,有返回值<T>。
    • Future:本身是一个接口,封装了cancel()、isDone()、get()等方法,get()是等待业务返回值(阻塞方法)。
    • FutureTask:实现了RunnableFuture<V>接口,RunnableFuture<V>继承自Future和Runnable接口,算是一个接口适配器模式(类似Stub类),可以把一个Runnable、Callable或FutureTask来submit()给线程池,完了拿到Future<?>后可以对任务进行管理和获取返回值(阻塞)。
  2. 程序中的优化策略:
    • CopyOnWriteArrayList:读写List,基于读写分离思想,可并发读,写的时候copy了一份副本,因此写占用了双倍内存,用的少。
    • ConcurrentHaspMap:分段锁HashMap,分段加锁技术,可以实现并发HashMap。
    • BlockingQueue:
    add()/move():可用返回true/false,不可用抛异常。
    offer()/peek():可用返回true/false,不可用返回null,不阻塞。
    pull(time,unit)/element():等待取出队首元素;抛异常。
    put()/take():阻塞方法。
    ArrayBlockQueue(数组实现/有界/线程安全/阻塞队列)、LinkedBlockQueue(单向链表实现/先进先出/无界/线程安全/阻塞队列);LinkedBlockDeque(双向列表实现/容量可指定也可不指定/线程安全/阻塞队列)、ConcurrentLinkedQueue(链接节点实现/线程安全/无界)。
  3. 同步锁技术:
    • synchronized:粗略锁,作用于对象、方法或class,利用对象的内部锁;当作用于函数时,锁的是该对象中所有同步实例方法,当作用于static函数时,锁的是该类的所有同步静态方法;方法加锁锁的是整段业务逻辑;在拿到锁后,sleep()方法不会改变锁的情况。
    • ReentrankLock/Condition:每个对象有内部锁,也可以new一个条件Lock,它具有灵活性、可通信性,可以实现轮训和定时锁,利用Condition可以实现线程间条件通信。
    • Semaphore:信号量,本质是一个共享锁,维护一个信号量许可集,自定义许可集大小,其余线程可以调用smp.aquire()/release()方法来获取和释放锁,用于控制线程通过量。
    • CyclicBarrier:循环栅栏,自定义等待集大小和业务Runnable(),其余线程调用cb.await()进入等待,等待线程够数时执行定义好的业务run()方法,完了线程继续执行。
    • CountDownLatch:同步辅助类,计时等待,自定义等待次数,本线程调用cd.await()进入等待,其余线程执行完调用cd.countDown()等待计数减一,等待次数结束本线程继续执行。
第十二章:Bitmap的加载和Cache
  1. 比较常用的缓存策略是内存缓存LruCache和磁盘缓存DiskLruCache,这种缓存策略为:当缓存快满时,会淘汰近期最少是用的缓存目标。
  2. Bitmap的高效加载是先将BitmapFactory.Options的inJustDecodeBounds设为true,这样不会真正的加载图片,只是获取图片的原始宽高,然后计算出缩略比例设置inSampleSize参数,完了再将inJustDecodeBounds设为false,最后再用Factory加载出真正的Bitmap就行了。注意:采样率一般设为2的倍数,计算时一般也是先用长宽/2,如果还大再去压缩,避免过度压缩,具体算法的话因人而异。
  3. LruCache是一个泛型类,内部采用LinkedHashMap存储缓存对象的强引用,当内存不足时,会回收最近最少使用目标,内部已经实现了线程安全;DiskLruCache将缓存对象写入文件系统,目录默认是sdcard/Andorid/data/package_name/cache下,也可根据需求自定义目录。也可使用ACache进行缓存,类似SP文件。
  4. 四种引用类型:强引用(直接的对象引用)、软引用(系统内存不足时会被gc回收)、弱引用(只要被gc扫到就会回收)、虚引用(结合引用队列使用)。
  5. Runtime.getRuntime().maxMemory()可获取系统运行最大内存大小;Runtime.getRuntime().availableProcessors()可获取CPU核心数。
  6. 优化列表卡顿的几种做法:异步加载图片、控制加载频率、滑动停止加载、开启硬件加速。
第十三章:综合技术
  1. 异常处理器:
    • 程序运行时如果有未捕获异常则会崩溃,这时候如果用setDefaultUncaughtExceptionHandler()给线程设置一个异常处理器CrashHandler,当程序崩溃时就会回调uncaughtException()方法。
    • 默认的异常处理器是Thread的静态成员,因此它作用于当前进程的所有线程(也可给线程设置单独的异常处理器),我们一般是先获取到默认的异常处理器,再设置自己的异常处理器,当崩溃时自己先上传log,然后继续让默认异常处理器去终止进程。
  2. APK分包:
    • Android中单个dex文件能包含的最大方法数为65536,包括AndroidFramework、依赖的jar包和本身的所有代码,超数后无法编译通过且抛出异常;有时候方法数并没有到65536,能编译通过但是低版本手机不能安装,这是因为应用在安装时,dexopt程序会为了优化apk,将dex文件的所有方法信息提前放到一个固定大小的方法缓冲区LinearAlloc内,不同版本的缓冲区大小是不同的,超过了就装不上。
    • Android5.0以前使用multidex需要引入android-support-multidex.jar包,5.0以后默认支持了multidex,注意BuildTools要升级到21.1以上。
    • 在gradle文件中的defaultConfig里开启分包multiDexEnabled true,引入jar包,manifest中将application标签设为MultiDexApplication或 Application继承自MultiDexApplication,在attachBaseContex()中设置MultiDex.install(this)即可。
    • gradle里可以写脚本--main-dex-list配置哪些类打到主dex中,注意multidex的jar包中9个类必须打进主dex,Application的成员变量是先于attachBaseContex()方法的,因此不要提前声明其他dex中的类。
  3. dex2jar和jd-jui可以查看apk文件解压后的dex中的java代码;
    apktool可以对apk进行解包、修改和二次打包,二次打包后需要重新签名。
    • 解包:apktool.bat d -f xxx.apk yyy
    • 打包:apktool.bat b yyy xxx2.apk
    • 签名:java -jar signapk.jar testkey.x509.pem testkey.pk8 xxx2.apk xxx2_signed.apk
  4. 签名、证书相关:
    • .keystore/.jks/.pem/.pk8是同级的,都是签名文件,都可以给apk签名。
    • keytool:是个秘钥和证书管理工具,可以用来生成秘钥库,如Eclipse生成的是.keystore,AS生成的是.jks,内部可按别名存多组密钥的信息。
    • jarsigner:工具利用密钥库中的信息来校验java存档文件的数字签名和签名jar/apk文件用的。
    • keytool用法:
    keytool生成证书:keytool -genkey -keystore xxx.keystore -alias xxx -keyals RSA -validity 1000;
    keytool查看证书:test keytool -list -keystore test.keystore;
    jarsigner签名apk:jarsigner -verbose -keystrore xxx.keystore -signerjar signed.apk unsign.apk 'xxx';

• .jks是一个秘钥管理仓库,只存放一类东西(密钥),库中的密钥类型分为私钥、公钥和密钥对,都有别名,公钥进入仓库就能拿走,私钥是需要密码的。
• .keystore是Java下keytool生成的秘钥管理仓库,存放两类东西(密钥实体和证书),将密钥实体和证书信息存在keystore中(证书只有公钥)。

  1. ClassLoader动态加载过程:
    • Dalvik虚拟机和其他Java虚拟机一样,在程序运行时首先要将类加载到内存中,类可以从class文件中读取也可以从其他形式的二进制流中读取。
    • Andorid平台上虚拟机运行的是Dex字节码,一种对class文件优化后的产物,Android将所有class文件合并优化,生成xxx.dex,如果分包的话会有多个dex文件。
    • ClassLoader分类:Classloader -> BootClassLoader、UrlClassLoader、BaseDexClassLoader(DexPathList) -> PathClassLoader(只能加载已安装的apk,art虚拟机还是可以加载未安装的)、DexClassLoader(支持其他apk、dex和jar文件)
    • ClassLoader构造方法中传入父Classloader,无的话默认创建PathClassLoader -> BootClassLoader,找类的时候先判断是否被加载过,然后先从父loader中加载(双亲委派机制)。
    • BootClassLoader是ClassLoader内部类我们没法使用,UrlClassLoader只能加载jar 不能用(dalvik虚拟机不能直接识别jar),BaseDexClassLoader我们一般修改这个类;BaseDexClassLoader(dexPath,optimizedDirectory,libraryPath,parentCl)四个参数为:目标dex文件路径、odex优化后的dex存放路径、c/c++库的路径、父classloader(一般context.getClassLoader())。
    • 类加载过程会从pathList中寻找class,DexPathList中dex包装成Element存在一个数组里(一般只有一个element),dexElement中的dexFile存着最终的dex文件,因此将新的dex文件插入dexElement数组前面可以实现热更新。
    • Android5.0以前是Dalvik虚拟机,5.0以后是Art虚拟机,据说Art在程序第一次安装时就把字节码预先编译成机器码(预编译)并优化了性能等细节(将.dex转为oat文件),接口是一致的,实现了一套完全兼容Java虚拟机的接口。
第十四章:JNI和NDK编程
  1. JNI是Java Native Interface(Java本地接口),为了方便Java调用C、C++层的一层本地封装接口,NDK是Android提供的一套工具集合,方便通过JNI来访问本地代码和编译生成各版本的so库。
  2. JNI编程的好处是提高安全性难以反编译、动态库便于平台移植和提供某些计算类代码执行效率等。
  3. C层JNIEnv*是一个指向JNI环境的指针,类似Java里的this,可以通过它访问JNI提供的接口方法;jobject是Java层的引用(Java对象的this),可以通过它访问Java层代码,类似回调;JNIEXPORT和JNICALL都是JNI定义的宏。
  4. JNI开发流程:定义native方法 -> System.loadLibrary()加载生成的.so库 -> javac/javah生成头文件 -> 实现.cpp/.c文件 -> gcc编译出.so库 -> java -D运行类即可。
  5. NDK提供了一系列工具集合便于生成.so库和调用.so库,Eclipse中需要配置.mk文件,AS中直接在gradle中配置即可,ndk-build命令可以导出.so库,就可以用了,创建so库除了使用ndk-build命令,也可使用gradle自动编译产生so库。
  6. JNI数据类型分为基本类型和引用类型;基本类型以j开头(jint),引用类型有类、对象和数组,以j开头(jobject)。
  7. JNI类型标签标识了一个特定的Java类型,可以是类和方法也可以是数据类型;
    • 类:L+包名+类名+;,如Ljava/lang/String;
    • 基本类型:大写字母,如int为I;
    • 数组:[+大写字母/类标签,如[D/、[Ljava/lang/String;
    • 方法:参数类型+返回值,如boolean fun(int a, double b, int[]c)为(ID[I)Z。
  8. JNI调Java层方法时,会先找类,再找方法的id,然后通过id来调方法(如果是非静态方法要先构造类对象)。
第十五章:Andorid性能优化
  1. 布局优化:多用LinearLayout、FrameLayout;<include/>标签(id覆盖,width/height都指定其余属性才生效)、<merge/>标签(什么属性都没有)、<ViewStub/>标签(加载后就没stub这个对象了,用inflatedId或生成的View)。
  2. 绘制优化:onDraw()方法由于会频繁调用,因此不要创建过多局部变量、不要做耗时操作等,该方法在主线程执行;属性动画、补间动画等在页面onDestroy()或View的onDetachedFromWindow()后记得关闭。
  3. 内存泄露优化:无论何时静态变量不要持有activity的引用、静态内部类+弱引用、流的关闭、监听者的解注册等。
  4. 线程优化:ANR发生时在/data/anr目录下会有一个traces.txt文件记录log。
    注意:尽量不要让主线程也去与子线程竞争锁,比如都去访问synchronized方法,如果锁被其他子线程持有且耗时,主线程就会阻塞住。
  5. ListView和Bitmap优化:getView()不要做耗时操作,开启硬件加速;Options采样压缩等。
  6. 其他优化:使用线程池、不要过多使用枚举、常量使用final修饰、SparseArray等数据结构、适当软引用和弱引用、静态内部类避免持有外部类引用等。
补充知识点
  • SDK区别:
    minSDKVersion <= targetSDKVersion <= compileSDKVersion
    • minSDKVersion:要求的最低API版本(安卓系统);
    • targetSDKVersion:程序适配的API版本(在这个版本的系统上运行效率高一些,权限等功能会体现出来);
    • compileSDKVersion:代码编译时的API版本(提示方法的更新、弃用等);
    注意:每一次安卓系统的发布都会伴随新的SDK和support库一起发布,support库的大版本号需要和compileSDKVersion保持一致(只是官方建议,不一致也能编译通过),有些类只在高版本SDK里有,所以如果compileSDKVersion编译时判断通过则编译打包没事,但是运行时在低版本可能就会崩溃了。
    • 因此才会有SupportV4、V7等支持包,包含了如Fragment、Material Design控件等兼容包,引入V7默认也会引入V4下的所有类,同时引入也不会报错。

  • Jar包冲突的本质:
    • 问题:Java应用程序因某种因素,加载到不正确的类而导致其行为跟预期不一致。
    • 两种情况:同一个Jar包出现了多个不同版本;不同Jar包内含有包名类名相同的类。
    • 解决方法:对于第一类,maven和gradle有一套自己的处理逻辑可以选择出较高的版本进行编译,但是如果不符合预期就还是有可能报错;对于第二类,如果类的包名、类名、方法信息完全一致编译就不会报错,classloader会选出靠前的jar包中的类,但是运行时可能会因为功能不对而达不到预期或者报错。
    • 安卓中:相同的V4包重复引入,只要相同方式但版本不同,不会报错,会自动选择最高版本;如果是maven和本地jar同时映引入但版本相同,也不会报错;只有maven和本地jar同时引入切版本不同才会报错;如果是多module打包,对于库的引入可以使用exclude来避免重复。

  • 安卓抓包工具:
    Fidder,必须在同一网段内可抓包,PC默认监听127.0.0.1:8888端口,手机连接PC代理;捕获手机Https请求时,需要手机访问PC的IP并下载cer证书,有些手机下载需完要到安全设置->从文件夹读取证书来手动安装cer证书。
    功能:捕获Https、Http统计、命令行、断点break point、AutoResponder返回本地文件、配置Host、过滤会话Filters、会话比较、会话查询、会话保存、编码工具等。

  • getMeasuredWidth()和getWith()的区别:
    getMeasuredWidth()方法获取的值是setMeasuredDimension()中设置的值,它是在measure()方法后确定的;getWidth()方法获取的值mRight-mLeft,它是在layout()方法后确定的,因此比getMeasuredWidth()值获取的晚。
    • 这两个值其实没什么不同,一般在onLayout()方法中可以使用getMeasuredWidth()方法获取宽度,而在onLayout()之后使用getWidth()就行了。
    注意:在Activity的onWindowFocusChanged()、或view.post()方法后view的getWidth()才被赋值。

  • LayoutInfalter三个方法的区别:
    Inflate(resId,null):只创建temp,返回temp,外层宽高无效;
    Inflate(resId,parent,false):创建temp,然后执行temp.setLayoutParams(params),返回temp,外层宽高有效(ListView中也有效,用于getView()方法获取单独的View);
    Inflate(resId,parent,true):创建temp,然后执行root.addView(temp, params),最后返回root,外层宽高有效(ListView中会报错,ListView不允许直接addView(),正常布局没事)。

  • WebViewClient方法加载顺序:
    • 如果没有设置WebViewClient则会打开默认浏览器;设置后如果返回true,则表示代码处理url,webView不处理,因此后面一般再跟一句webView.load(url);如果直接返回false,表示由webView来处理该url,不用写额外代码。
    onReceiveTitle()每次都会执行,包括goBack()后也会执行,重定向时title可能会有问题,需要自己维持title栈和url栈的来解决重定向问题。
    第一页:onPageStarted() -> onPageFinished();如果无连接则onPageStarted() -> onReceivedError -> onPageFinished()
    第二页:shouldOverrideUrlLoading() -> onPageStarted() -> onPageFinished();无连接时同上。
    返回上一页:onPageStarted() -> onPageFinished();返回上一页不调overrideUrl,无网络连接不影响返回上一页。
    注意:onReceivedError和shouldOverrideUrlLoading两个被弃用的方法,有时候不会被回调,重写新方法即可(情况处理更多样),但是新方法回调顺序可能有变,如onPageStarted() -> onPageFinished() -> onReceivedError()

  • 热修复原理:
    ①DEX分包方案:
    • JVM执行.class文件,DVM执行.dex文件,dx工具会把.class文件转为.dex包,再与资源文件一起打成.apk包,所以.apk包其实就是.zip,包含dex文件、资源文件、assets包等。
    • 安卓应用默认只打一个dex包,安卓初次装应用的时候会进行dex优化,使用DexOpt工具,加载过程中会生成ODdex文件,执行效率会更高;DexOpt会把一个dex包的所有方法id检索起来存在一个链表结构里,链表长度使用short类型来保存,因此一个dex方法数不能超过65536个;另外,DexOpt使用LinearAlloc来存储应用方法信息,缓冲区大小不同版本限制不同,因此低版本可能方法还没达到65536,可以编译但是无法安装。
    • 注意,再打完多个dex包后,应用只会加载主dex包,其他的dex包需要我们自己手动去加载,加载还是比较耗时的,不建议放到主线程,未加载之前调用其他dex包中的类会报错。
    ②热修复技术:
    • ClassLoader中包含一个pathList对象,pathList里有个dexElements数组(每个dex都包装成了一个Element),同一个dex里类不能重复,但不同dex里类可重复,因此类被加载的时候会去遍历dex数组找类,找到第一个后就返回了,这时候我们可以把类单独打成补丁dex包,插入到Elements最前面即可。
    • 注意,如果两个相互引用的类不在同一个dex包中,就会报错,这是由于引用者被打上CLASS_ISPREVERIFIED标签,就会进行dex校验的结果(5.0以上ART运行时机制,odex优化不会有此问题);因此安卓应用在第一次安装的时候会优化dex->odex,虚拟机启动的时候有个启动参数vertify如果为被打开,就会进行dvmVertifyClass校验,如果两个类的直接引用在同一个dex下,则校验成功,打上CLASS_ISPREVERIFIED标签,那我们要做的事就是阻止类被打上这个标签(不让同一个dex包中的类被直接引用),到时候就不会做校验了。
    • 空间团队的做法是打了一个hack.dex包,里面只有一个AntilazyLoad.class类,在dx工具执行之前,修改所有.class文件,在每个类的构造方法中打印了一行Syetem.out.println(AntilazyLoad.class),这就导致后面校验不成功,就不会打上CLASS_ISPREVERIFIED标签了(javassist的使用,类似反射);注意应用启动的时候必须先加载进来hack.dex,否则后面AntilazyLoad.class类就会找不到报错,另外,类被打上CLASS_ISPREVERIFIED实际上是为了提高性能的,因此我们强制不打,略有性能损失。
    ③热修复实现:
    • 第一步,通过javassist修改每个.class文件,引入hack.jar中的类,阻止类被打上CLASS_ISPREVERIFIED标签(注意hack.jar需要dex优化过);
    • 第二步,gradle中编写task,实现在dx前执行第一步的操作;
    • 第三步,hack.jar可以放入assets包打入apk,然后在Application中onCreate()中先将hack.jar复制到项目private的dex目录下;
    • 第四步,执行patch操作,通过反射拿到pathClassLoader中的主项目dexElements,再拿到插件中dexElements,合并两个数组即可;
    • 第五步,热修复完成,补丁dex包与hack.jar过程一致。

  • Java 内存分配策略:
    Java 程序运行时的内存分配策略有三种,分别是静态分配、栈式分配、堆式分配,对应的三种分配策略使用的内存空间分别是静态存储区(也称方法区)、栈区、堆区。
    • 静态存储区(方法区):主要存放静态数据、全局static数据和常量,这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在;
    • 栈区:当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放,因为栈内存分配运算内置于处理器的指令集中,所以效率很高,但是分配的内存容量有限;
    • 堆区:又称动态内存分配,通常就是指在程序运行时直接new出来的内存,也就是对象的实例所在的区域,这部分内存在不使用时将会由Java垃圾回收器来负责回收。
    -栈与堆的区别:
    • 在方法体内定义的局部变量、一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的,当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。
    • 堆内存用来存放所有由new创建的对象(包括该对象其中的所有成员变量)和数组,在堆中分配的内存,将由Java垃圾回收器来自动管理,在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量,我们可以通过这个引用变量来访问堆中的对象或者数组。

  • 内存泄漏相关:
    • 定义:当一个对象已经不需要再使用了,本该被回收时,而有另外一个生命周期较长且正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
    • 常见场景:
    非静态内部类的静态实例化(会持有外部类引用,静态实例化后不会销毁,外部引用就不会销毁)、单例持有Context(类似上条,单例常驻内存,如果持有Activity,导致Activity不会被回收)、非静态Handler做耗时操作(类似上条,持有外部Activity的引用,耗时操作导致外部Activity不会被回收)、io等资源使用后没有关闭(用到缓冲,存在于虚拟机内和外,需要手动close再置null)、Bitmap没有标记回收(使用完毕需要标记一下可回收,否则Bitmap一般是比较占内存的)、事件总线库的注册与解注册(注册后被观察者会持有观察者的引用,如果是Activity不解注册会内存泄漏)。
    • 总结:确保对象能够在正确的时机被回收掉,在需要的时候使用,不需要的时候及时可以被回收,因此注意生命周期长的对象不要一直持有生命周期短且应该被回收的对象。
    • 补充关于Handler内存泄漏的解释:Handler非静态内部类会持有外部类引用this,所以可以直接使用Activity中的属性、方法等,因此如果有耗时操作会导致外部Activity泄漏。可以使用静态内部类,杜绝直接对外类的引用,但是无法访问外部类实例域和方法了,只能访问静态域,如果必须要使用外部类的实例域,则可以使用弱引用从构造方法中传入外部类即可。

  • Java注解:
    • 元注解:
    @Retention:注解保留的生命周期,有SOURCE/CLASS/RUNTIME三种;
    @Target:注解对象的作用范围,有TYPE/FIELD/METHOD/PARAMETER/CONSTRUCTOR/PACKAGE等;
    @Inherited:所标注的注解在作用的类上是否可被继承,使用时,子类会继承该注解,但只作用于类,对方法、属性无效;
    @Documented:javadoc的工具化文档。
    • 作用:降低项目耦合度、自动完成一些规律性代码、自动生成一些Java代码。
    • 注意:注解使用反射,可以拿到属性的注解对象和注解值来进行一些额外操作,因此运行时注解其实使用反射是比较影响效率的,可以使用编译时注解在编译时通过反射获取注解,自动生成一些代码,如ButterKinfe等库的实现,编译时注解依赖于注解处理器AbstractProcessor,是javac的一款工具,用来编译时扫描和处理注解,并生成Java文件注入到源码中。

  • 编码相关:
    编码:ASCII最早表示英文、Unicode是中文字符集可表示更多字符但是会混淆和浪费空间(比较常见)、UTF-8/UTF-16是可变长编码方式;因此英文使用ASCII不会有问题,中文不能使用ASCII,中英都可以使用Unicode或UTF-8。

  • 加密相关:
    • Base64:一般网络上传输的数据要进行Base64编码一次,它不是加密算法,是一种编码转换,因为ASCII码的128~255之间是不可见字符,网上传输数据时可能会解析错误,因此Base64的实际作用就是将二进制转为可见的字符形式的数据形式。
    • MD5/SHA1:MD5和SHA1都是一套摘要算法,不能算是加密算法,它可以把任何一个文件、程序、图像等类型,不管体积有多大,转化成一个固定长度的MD5或哈希值,不可逆,因此可以用来判断文件、数据是否被更改,SHA1略叼一点,但是MD5略快一点。
    • DES/3DES/AES:对称加密算法,DES->3DES->AES是越来越叼,使用向量可以增强加密强度,一般用于与RSA合作双向加密。
    • RSA:非对称加密算法,公钥加密、私钥解密,或者私钥加密、公钥解密,甲方发起请求,使用乙方的公钥加密,乙方使用自己的私钥解密;签名认证:对发送的数据摘要出MD5值,用私钥加密与内容一起发送,接收方使用公钥解密得到MD5值,摘要出内容对比MD5是否一致即可。
    -项目加密过程:
    • 登录模式下,用户登录时获取随机AES_KEY,然后用AES加密请求数据,用RSA加密AES_KEY,一起发送给服务端,服务端保存用户AES_KEY,返回数据仅用AES加密,本地用AES解密,服务端同时会返回token,下次请求直接带token过去,服务端就找到相应的AES_KEY进行解密。
    • 游客模式下,用户每次都随机生成AES_KEY(实际上也就进入APP生成一个),然后用AES加密请求数据,用RSA加密AES_KEY,一起发送给服务端,返回数据仅用AES加密,本地用AES解密。

  • 绝对路径和相对路径:
    • 绝对路径形如:E:\book\代码\test.java,不同主机磁盘文件路径不同,一般不使用绝对路径;相对路径形如:gg.png,以"/"分隔,一般使用相对路径,通用。
    -相对路径三种格式:
    ../ 当前路径的上一级路径;
    ./ 当前路径(可以省略不写);
    / 虚拟目录的根路径;

  • 正则表达式:
    \d:匹配一个数字; \w:匹配一个字母或数字;\s:匹配至少一个空格。
    .:匹配任意1个字符;?:匹配0个或1个字符;*:匹配0个或多个字符;+:匹配1个或多个字符。
    {n}:表示n个字符,用{n,m}表示n-m个字符;如\d{3}表示匹配3个数字,\d{3,8}表示3-8个数字。
    []:表示一个字符范围,如[0-9a-zA-Z\_][a-zA-Z\_\$][0-9a-zA-Z\_\$]*
    A|B:匹配A或B;^:表示行的开头,如^\d表示必须以数字开头;$:表示行的结束,如\d$表示必须以数字结束。

  • Labmda表达式:
    • 定义:lambda是匿名函数的别名,用于简化匿名内部类的调用。
    • 语法:lambda表达式展示了一个接口抽象方法最有价值的两点,即参数列表和具体实现,(参数列表...)->{语句块}
    • lambda表达式共有三种形式:函数式接口、方法引用和构造器引用。
    • 参数只有一个时不用发加括号,没有或大于一个需要括号;当表达式只有一句话大括号和return都可以不用加,有多行的话需要大括号,也就需要return。
    • 方法引用的三种格式:object::instanceMethod / ClassName::staticMethod / ClassName::instanceMethod,第三种比较少见,参数列表的第一个参数为方法的调用者,其余参数为方法参数;构造器引用:ClassName::New
    • Java8中新增了一个effective final功能,如int effectiveFinalInt = 666,不用加final修饰符可以在接口回调里使用该变量,前提是确认它是不会被修改的。
    • 匿名内部类中,this关键字指匿名内部类本身的对象;而在lambda表达式中,this指向外围的类对象。
    • lambda表达式经过编译器编译后,每个表达式会增加1~2个方法数,Android方法数有65536限制,需注意。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容