├── .DS_Store ├── .gitignore ├── README.md ├── README_CN.md └── post ├── AMS核心分析.md ├── ANR问题优化.md ├── APK的打包流程.md ├── AQS的实现原理.md ├── ARouter实现原理.md ├── ActivityThread.md ├── Activity的启动模式.md ├── Android基础知识汇总.md ├── Android系统启动流程.md ├── App的启动流程.md ├── ArrayMap的实现原理.md ├── Binder与AIDL.md ├── Binder机制原理.md ├── Bitmap.md ├── Butterknife实现原理.md ├── CAS、UnSafe类即Automic并发包.md ├── Choreographer详解.md ├── ConcurrentHashMap.md ├── Dalvik与ART.md ├── Fragment核心原理.md ├── Glide实现原理.md ├── HR面常问问题.md ├── HTTPS实现原理.md ├── HandlerThread实现原理.md ├── Handler的实现原理.md ├── Handler相关面试题.md ├── HashMap实现原理.md ├── Hash表与HashMap.md ├── Home.md ├── Http协议.md ├── IdleHandler.md ├── InputManagerService.md ├── Instrumentation.md ├── IntentService实现原理.md ├── JMM与volatile关键字.md ├── JVM.md ├── Java等待与唤醒机制.md ├── Java线程中断机制.md ├── Java集合框架.md ├── LayoutInflater.md ├── LeakCanary 实现原理.md ├── Lifecycle实现原理.md ├── MVC、MVP与MVVM.md ├── OKHttp实现原理.md ├── PMS安装与签名校验.md ├── RecyclerView实现原理.md ├── ReentrantLock实现原理.md ├── Retrofit实现原理.md ├── RxJava中的线程池.md ├── RxJava实现原理.md ├── SharedPreferences.md ├── Socket.md ├── SparseArray实现原理.md ├── SurfaceFlinger.md ├── TCP与UDP.md ├── TM项目优化方案.md ├── ThreadLoacal的实现原理.md ├── UI界面及卡顿优化.md ├── ViewModel的实现原理.md ├── ViewRootImpl.md ├── View事件分发机制.md ├── View的绘制流程.md ├── WMS核心分析.md ├── WorkManager的实现原理.md ├── XML解析原理.md ├── requestLayout与invalidate.md ├── synchronized的实现原理.md ├── 二叉树相关算法.md ├── 内存优化.md ├── 动态代理.md ├── 包体积优化.md ├── 协程实现原理.md ├── 单例模式.md ├── 启动优化.md ├── 多线程与并发基础.md ├── 字符串相关算法.md ├── 屏幕刷新机制.md ├── 屏幕适配.md ├── 排序算法.md ├── 数组相关算法.md ├── 查找算法.md ├── 电池电量优化.md ├── 算法:动态规划.md ├── 线上性能监控.md ├── 线上性能监控2-Matrix实现原理.md ├── 线程池的实现原理.md ├── 组件化WebView架构搭建.md ├── 组件化架构的搭建.md ├── 观察者模式.md ├── 计算机网络.md ├── 责任链模式.md ├── 递归算法.md ├── 链表相关算法.md ├── 面向对象与Java基础知识.md ├── 项目中遇到的问题.md └── 高阶函数实现原理.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhpanvip/AndroidNote/913040ea47149474d078959e648b0855e7bbafbe/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /AndroidNote.iml 2 | /.idea/ 3 | /.DS_Store -------------------------------------------------------------------------------- /post/ANR问题优化.md: -------------------------------------------------------------------------------- 1 | ANR(Application Not responding),是指应用程序未响应,Android系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成ANR 2 | 3 | ## 出现场景 4 | - Service Timeout 5 | - BroadcastQueue Timeout 6 | - ContentProvider Timeout 7 | - InputDispatching Timeout 8 | 9 | ## Timeout时长 10 | - 对于前台服务,则超时为SERVICE_TIMEOUT = 20s; 11 | - 对于后台服务,则超时为SERVICE_BACKGROUND_TIMEOUT = 200s 12 | - 对于前台广播,则超时为BROADCAST_FG_TIMEOUT = 10s; 13 | - 对于后台广播,则超时为BROADCAST_BG_TIMEOUT = 60s; 14 | - ContentProvider超时为CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10s; 15 | - InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 16 | - 主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。 17 | - 主线程中存在耗时的计算 18 | - 主线程中错误的操作,比如Thread.wait或者Thread.sleep等 19 | Android系统会监控程序的响应状况,一旦出现下面两种情况,则弹出ANR对话框 20 | - 应用在5秒内未响应用户的输入事件(如按键或者触摸) 21 | - BroadcastReceiver未在10秒内完成相关的处理 22 | 23 | ## 2)如何避免 24 | 25 | 避免ANR的基本思路就是将IO操作在工作线程来处理,减少其他耗时操作和错误操作 26 | 27 | - 使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同。 28 | - 使用Handler处理工作线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程。 29 | - Activity的onCreate和onResume回调中尽量避免耗时的代码 30 | - BroadcastReceiver中onReceive代码也要尽量减少耗时,建议使用IntentService处理。 31 | - 避免主线程跟工作线程发生锁的竞争 32 | - 减少系统耗时binder的调用 33 | - 谨慎使用sharePreference 34 | - 注意主线程执行provider query操作 35 | 36 | ## 如何改善 37 | 38 | 通常100到200毫秒就会让人察觉程序反应慢,为了更加提升响应,可以使用下面的几种方法 39 | 40 | 如果程序正在后台处理用户的输入,建议使用让用户得知进度,比如使用ProgressBar控件。 41 | 42 | 程序启动时可以选择加上欢迎界面,避免让用户察觉卡顿。 43 | 44 | 使用Systrace和TraceView找出影响响应的问题。 45 | 46 | 如果开发机器上出现问题,我们可以通过查看/data/anr/traces.txt即可,最新的ANR信息在最开始部分。 47 | 48 | ## trace文件解读 49 | 50 | ### 1. 人为的收集trace.txt的命令 51 | adb shell kill -3 888 //可指定进程pid 52 | 执行完该命令后traces信息的结果保存到文件/data/anr/traces.txt 53 | ### 2. trace文件解读 54 | 55 | 56 | -------------------------------------------------------------------------------- /post/APK的打包流程.md: -------------------------------------------------------------------------------- 1 | ## APK中包含的内容 2 | 3 | 1.resources.arsc:包含了所有资源文件的映射,可以理解为索引,通过该文件能找到对应的资源文件信息 4 | 5 | 2.AndroidManifest.xml:Project中AndroidManifest.xml编译后得到的二进制xml文件 6 | 7 | 3.META-INF:主要保存各个资源文件的SHA1 hash值,用于校验资源文件是否被篡改,防止二次打包时资源文件被替换,该目录下主要包括下面三个文件: 8 | 9 | - CERT.RSA:保存签名和公钥证书 10 | - MANIFEST.MF:保存版本号以及对每个文件(包括资源文件)整体的SHA1 hash 11 | - CERT.SF:保存对每个文件头3行的SHA1 hash 12 | - res:Project中res目录下资源文件编译后得到的二进制xml文件 13 | 14 | 3.classes.dex:Dex是DalvikVM executes的缩写,即Android Dalvik执行程序 15 | 16 | 4.lib:对应Project中的libs目录,包含.so文件。 17 | 18 | ## 打包APK用到的工具 19 | 20 | 在APK编译打包过程中,用到了以下工具,这些工具大部分位于Android SDK的build-tools目录下: 21 | 22 | 1.aapt:全称Android Asset Packaging Tool,即Android资源打包工具 23 | 24 | 2.aidl:将.aidl文件转换为.java文件的工具 25 | 26 | 3.Java Compiler:java编译器,将.java文件转换为.class文件的工具,运行命令javac 27 | 28 | 4.dex:将.class文件转换为Davik VM能识别的.dex文件的工具,运行命令dx 29 | 30 | 5.apkbuilder:生成APK的工具 31 | 32 | 6.Jarsigner:.jar文件的签名工具 33 | 34 | 7.zipalign:字节码对齐工具 35 | 36 | 37 | ## 打包流程 38 | 39 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae7b715dd08f40078903f56eeabd5a54~tplv-k3u1fbpfcp-watermark.webp) 40 | 41 | (方形:表示文件,椭圆:表示工具及操作) 42 | 43 | 上面这张图,显示了更为详细的构建流程。以虚线为界,前半部分描述了 编译流程 ,后半部分则描述了 打包流程。 44 | 45 | 下面具体分析构建流,分为七步(其中编译1-4、打包5-7): 46 | 47 | 1.aapt过程:使用aapt/aapt2打包res目录资源文件,生成R.java、resources.arsc和res目录。 48 | 49 | R.java保存了res目录下所有资源的id,数据类型都是整型,我们在程序中都是通过使用Android API依据R文件中的资源id来获取对应资源 50 | 51 | 2.aidl生成Java文件:AIDL是Android Interface Definition Language的缩写,是Android跨进程通讯的一种方式,该阶段会检索Project中所有的aidl文件,并转换为对应的Java文件。 52 | 53 | 3.javac编译:使用JDK里的javac编译Project src目录下的Java源文件、R.java以及aidl生成的Java文件,并生成.class文件。 54 | 55 | 4.生成DEX文件:通过dx工具将.class文件转换为classes.dex,目前的gradle multi-dex编译方式会生成classes2.dex ... classesN.dex。 56 | 57 | 5.打包生成APK:使用apkBuilder将resources.arsc、res目录、AndroidManifest.xml、assets目录、dex文件打包成初始APK,具体逻辑是在com.android.sdklib.build.ApkBuilder中实现的。 58 | 59 | 6.签名apk文件:使用apksigner为APK添加签名信息 60 | 61 | 7.zipalign优化签名包:使用zipalign工具对签名包进行内存对齐操作,即优化安装包的结构。 62 | 63 | 64 | [Android APK编译打包过程](https://www.jianshu.com/p/a71bd35d6dd9) 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /post/AQS的实现原理.md: -------------------------------------------------------------------------------- 1 | ## 学习AQS的必要性 2 | 3 | 队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。 4 | 5 | ## AQS使用方式和其中的设计模式 6 | 7 | AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在AQS里有一个int型的state来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。 8 | 9 | ``` 10 | /** 11 | * The synchronization state. 12 | */ 13 | private volatile int state; 14 | ``` 15 | 16 | 在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。 17 | 18 | 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系: 19 | 20 | 锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节; 21 | 22 | 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。 23 | 24 | 实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。 25 | 26 | ## 模板方法模式 27 | 28 | 同步器的设计基于模板方法模式。模板方法模式的意图是,定义一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。我们最常见的就是Spring框架里的各种Template。 29 | 30 | 31 | 32 | ## AQS中的方法 33 | ### 1.模板方法 34 | 实现自定义同步组件时,将会调用同步器提供的模板方法, 35 | 36 | | 方法名称 | 描述 | 37 | |--|--| 38 | | void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回。否则,进入进入同步队列等待。该方法将会调用重写tryAcquire(int arg)方法 | 39 | | void acquireInterruptibly(int arg) | 与acquire(int arg)相同,但是该方法相应中断,当前线程未获取到同步状态而进入队列中,如果当前线程被中断,则方法会抛出InterrputedException并返回。 | 40 | | boolean tryAcquireNanos(int arg, long nanosTimeout) |在acqureInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将返回false,如果获取到就返回true | 41 | | void acquireShared(int arg) |共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别在于同一时刻可以有多个线程获取到同步状态。 | 42 | | void acquireSharedInterruptibly(int arg) | 与acquireShared(int arg)相同,该方法相应中断 | 43 | | boolean tryAcquireSharedNanos(int arg,long nanos) | 在acquireSharedInterruptibly(int arg)基础上增加了超时限制 | 44 | | boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 | 45 | | boolean releaseShared(int arg) | 共享式的释放同步状态 | 46 | | Collection getQueuedThreads() | 获取等待在同步队列上的线程集合 | 47 | 48 | 这些模板方法同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。 49 | 50 | ### 2.可重写的方法 51 | 52 | | 方法名称 | 描述 | 53 | |--|--| 54 | | protected boolean trayAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 | 55 | | protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取到同步状态 | 56 | | protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值表示获取成功,反之获取失败 | 57 | | protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 | 58 | | protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 | 59 | 60 | ### 3.访问或修改同步状态的方法 61 | 62 | 重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。 63 | 64 | - getState():获取当前同步状态。 65 | - setState(int newState):设置当前同步状态。 66 | - compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。 67 | 68 | ## CLH队列锁 69 | 70 | CLH队列锁即Craig, Landin, and Hagersten (CLH) locks。 71 | 72 | CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。 73 | 74 | 当一个线程需要获取锁时: 75 | 76 | - 1.创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用 77 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/thread/clh1.png) 78 | - 2.线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred 79 | 80 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/thread/clh2.png) 81 | 82 | 线程B需要获得锁,同样的流程再来一遍 83 | 84 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/thread/clh3.png) 85 | 86 | - 3.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false) 87 | 88 | - 4.当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点 89 | 90 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/thread/clh4.png) 91 | 92 | 如上图所示,前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。 93 | 94 | CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH队列锁常用在SMP体系结构下。 95 | 96 | Java中的AQS是CLH队列锁的一种变体实现。 -------------------------------------------------------------------------------- /post/ARouter实现原理.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## ARouter实现原理 -------------------------------------------------------------------------------- /post/Activity的启动模式.md: -------------------------------------------------------------------------------- 1 | ## Activity的启动模式 2 | 3 | Android 提供了四种Activity启动方式: 4 | 5 | ### 1.标准模式:standard 6 | 7 | 每启动一次Activity,就会创建一个新的Activity实例并置于栈顶。谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。 8 | 9 | 特殊情况下,如果在Service或Application中启动一个Activity,其并没有所谓的任务栈,可以使用标记位Flag来解决。解决办法:为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,创建一个新栈。 10 | 11 | ### 2.栈顶复用模式:singleTop 12 | 13 | 如果需要新建的Activity位于任务栈栈顶,那么此Activity的实例就不会重建,而是复用栈顶的实例。并回调: 14 | 15 | ```java 16 | @Override 17 | protected void onNewIntent(Intent intent) { 18 | super.onNewIntent(intent); 19 | } 20 | ``` 21 | 22 | 由于不会重建一个Activity实例,则不会回调其他生命周期方法。 23 | 24 | 应用场景:在通知栏点击收到的通知,然后需要启动一个Activity,这个Activity就可以用singleTop,否则每次点击都会新建一个Activity。 25 | 26 | ### 3.栈内复用模式:singleTask 27 | 28 | 该模式是一种单例模式,即一个栈内只有一个该Activity实例。singleTask会具有clearTop特性,会把之上的栈内Activity清除。与singleTop一样,系统也会回调它的onNewIntent方法。 29 | 30 | 当一个singleTask模式的Activity请求启动后,系统会首先寻找是否存在这个Activity想要的任务栈,如果不存在,就创建一个新的任务栈,然后创建Activity的实例后把这个Activity放入到这个任务栈中。如果存在所需要的任务栈,这时,要看这个Activity是否在栈中有实例存在,如果有,那么系统就会把这个Activity调用到栈顶,并调用它的onNewIntent方法。如果不存在,就会创建这个Activity的实例,并将其压入栈顶。 31 | 32 | 该模式,可以通过在AndroidManifest文件的Activity中指定该Activity需要加载到哪个栈中,即singleTask的Activity可以指定想要加载的目标栈。singleTask和taskAffinity配合使用,指定开启的Activity加入到哪个栈中。 33 | 34 | ```xml 35 | 39 | 40 | ``` 41 | 42 | **关于taskAffinity**:TaskAffinity可以理解为任务相关性,这个参数标识了Activity所需要的任务栈名字。每个Activity都有taskAffinify属性,这个属性指出了它希望进入的任务栈。如果一个Activity没有显式的指明该Activity的taskAffinity,那么它的这个属性就等于Application指明的taskAffinity,如果Application也没有指明,那么该taskAffinity的值就等于包名。 43 | 44 | 上述配置中,Activity1指定了taskAffinity为“com.test.task",在这种情况下启动Activity1,如果“com.test.task"栈不存在,则创建这个栈,并把创建的Activity压入这个栈内。如果“com.test.task"栈存在,并且其中没有该Activity实例,则会创建Activity并压入栈顶,如果这个任务栈中有该Activity实例,则clearTop把该Activity实例之上的Activity杀死并出栈,重用并让该Activity实例处在栈顶,然后调用onNewIntent()方法。 45 | 46 | **singleTask应用场景:** 47 | 48 | 对于大部分应用,当我们在主界面点击返回按钮都是退出应用,那么当我们第一次进入主界面之后,主界面位于栈底,以后不管我们打开了多少个Activity,只要我们再次回到主界面,都应该使用将主界面Activity上所有的Activity移除的方式来让主界面Activity处于栈顶,而不是往栈顶新加一个主界面Activity的实例,通过这种方式能够保证退出应用时所有的Activity都能被销毁。 49 | 50 | ### 4.单例模式:singleInstance 51 | 52 | 作为栈内复用的加强版,打开该Activity时,直接创建一个新的任务栈,并创建该Activity实例放入栈中。一旦该模式的Activity实例已经存在于某个栈中,任何应用在激活该Activity时都会重用该栈中的实例。 53 | 54 | 应用场景:呼叫来电界面 55 | 56 | ## 场景分析 57 | 58 | 假设当前有两个任务栈,前台任务栈中有AB(自栈底到栈顶,下同)两个Activity,后台任务栈有CD两个Activity。如果CD都被设置了SingleTask,此时,通过A启动D,那么整个后台任务栈都会被切换到前台,可以理解为栈变成了ABCD的顺序,此时按back按键后,D出栈,C显示;再次按back键,C出栈,B显示,以此类推。 59 | 60 | 如果上述场景中A启动的是C而不是D,那么D会被首先clearTop掉,即任务栈变为ABC,按返回时C先出栈,接着B、A出栈。 61 | 62 | 63 | 64 | 65 | 66 | ## 相关面试题 67 | 68 | ### 1.一个被设置为standard模式的Activity在被ApplicationContext启动时会什么会报错? 69 | 70 | 因为standard模式的Activity默认会进入启动它的Activity所属的任务栈,但是由于非Activity类型的Context自身并没有所属的任务栈,因此,此时启动standard模式的Activity就有问题了。可以通过给待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样在启动这个Activity时就会为它创建一个新的任务栈。此时,这个Activity实际上是以singleTask模式启动的。 71 | 72 | ### 2.有A、B、C三个Activity,其中A启动模式为standard,taskAffinity为默认。B和C的启动模式都设置为singleTask,并且都指定taskAffinity为"com.test.task"。此时,如果在A中点击按钮启动B,在B中点击按钮又启动C,在C中点击按钮又启动A,最后在A中点击按钮启动B。现在按两次返回键会回到哪里? 73 | 74 | 会回到桌面。第一次A启动B,由于B指定了单独的任务栈“com.test.task",因此会首先创建”com.test.task"任务栈(以下简称该任务栈为T),然后实例化B,并将B加入T任务栈中。此时B启动C,由于C与B指定的都是T任务中,因此C会实例化后加入到T任务栈,接着在C中启动A,由于此时T任务栈中没有A,因此会实例化A并将A加入到T任务栈。接着A再次启动B,由于B已经存在于T任务栈中,因此会执行clearTop,将C和A都出栈,然后显示出B。此时点击返回B会finish掉,此时后台任务栈中的A会显示出来,再次点击返回,A结束回到桌面。 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /post/Android基础知识汇总.md: -------------------------------------------------------------------------------- 1 | ### 简述在横竖屏切换的过程中,Activity的生命周期。 2 | 3 | 横竖屏切换的生命周期: 4 | > onPause() --> onSaveInstanceState() --> onStop() --> onDestory() --> onCreate() --> onStart() --> onRestoreInstanceState() --> onResume() 5 | 6 | 在Activity由于异常情况下终止时,系统会调用 onSaveInstanceState 来保存当前 Activity 的状态。这个方法的调用是在onStop之前,它和onPause没有既定的时序关系,该方法只有在Activity被异常终止的情况下调用。当异常终止的Activity被重建之后,系统会调用onRestoreInstanceState,并且把Activity销毁时onSaveInstanceState方法所保存的Bundle对象参数同时传递给onRestoreInstanceState和onCreate方法。因为,可以通过onRestoreInstanceState方法来恢复Activity的状态,该方法的调用时机是在onStart之后。其中,onCreate和onRestoreInstanceState方法来恢复Activity状态的区别:onRestoreInstanceState回调则表明其中Bundle对象非空,不用加非空判断,而onCreate需要非空判断,建议使用onRestoreInstanceState。 7 | 8 | 可以通过在AndroidManifest文件的Activity中指定如下属性来避免横竖屏切换: 9 | ``` 10 | android:configChanges = "orientation| screenSize" 11 | ``` 12 | 13 | ### Activity的Flags 14 | 15 | Activity的Flags很多,这里介绍集中常用的,用于设定Activity的启动模式,可以在启动Activity时,通过Intent.addFlags()方法设置。 16 | 17 | - FLAG_ACTIVITY_NEW_TASK 即 singleTask 18 | - FLAG_ACTIVITY_SINGLE_TOP 即 singleTop 19 | - FLAG_ACTIVITY_CLEAR_TOP 当他启动时,在同一个任务栈中所有位于它之上的Activity都要出栈。如果和singleTask模式一起出现,若被启动的Activity已经存在栈中,则清除其之上的Activity,并调用该Activity的onNewIntent方法。如果被启动的Activity采用standard模式,那么该Activity连同之上的所有Activity出栈,然后创建新的Activity实例并压入栈中。 20 | 21 | ### Activity的启动过程 22 | 23 | ![](https://camo.githubusercontent.com/dd7fc1ae08e21b7c3b034367be505bdcda18d4160a2dafcd54ed7a1b553bfc76/68747470733a2f2f692e6c6f6c692e6e65742f323031382f30362f30352f356231363432303233633764372e706e67) 24 | 25 | - Launcher通过Binder进程间通信机制通知AMS,它要启动一个Activity 26 | - AMS通过Binder进程间通信机制通知Launcher进入Paused状态 27 | - Launcher通过Binder进程间通信机制通知AMS,它已经准备就绪进入Paused状态,于是AMS就创建一个新的线程,用来启动一个ActivityThread实例,即将要启动的Activity就是在这个ActivityThread实例中运行 28 | - ActivityThread通过Binder进程间通信机制将一个ApplicationThread类型的Binder对象传递给AMS,以便以后AMS能够通过这个Binder对象和它进行通信 29 | - AMS通过Binder进程间通信机制通知ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了 30 | 31 | ### Context的理解? 32 | Android应用模型是基于组件的应用设计模式,组件的运行要有一个完整的Android工程环境。在这个工程环境下,Activity、Service等系统组件才能够正常工作,而这些组件并不能采用普通的Java对象创建方式,new一下就能创建实例了,而是要有它们各自的上下文环境,也就是Context,Context是维持Android程序中各组件能够正常工作的一个核心功能类。 33 | 34 | 35 | 源码中的Context 36 | ``` 37 | public abstract class Context { 38 | } 39 | ``` 40 | 它是一个纯抽象类,那就看看它的实现类。 41 | 42 | ![](https://camo.githubusercontent.com/f13f4e36dff25e4779267e1673708d89b1e3cf1c6af1766989cce8381170fb58/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f313138373233372d316234633063643331666430313933662e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430) 43 | 44 | 它有两个具体实现类:ContextImpl和ContextWrapper。 45 | 46 | 其中ContextWrapper类,是一个包装类而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象指定真正的Context对象,调用ContextWrapper的方法都会被转向其包含的真正的Context对象。ContextThemeWrapper类,其内部包含了与主题Theme相关的接口,这里所说的主题就是指在AndroidManifest,xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,所以Service直接继承与ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所有函数,应用程序中所调用的各种Context类的方法,其实现均来源于该类。Context得两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。 Activity、Application、Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。 47 | 48 | ### 一个应用程序有几个Context? 49 | 50 | 在应用程序中Context的具体实现子类就是:Activity、Service和Application。那么Context数量=Activity数量+Service数量+1。那么为什么四大组件中只有Activity和Service继承Context呢?BroadcastReceiver和ContextPrivider并不是Context的子类,它们所持有的Context都是其他地方传过去的,所以并不计入Context总数。 51 | 52 | ### Context能干什么? 53 | ``` 54 | TextView tv = new TextView(getContext()); 55 | 56 | ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...); 57 | 58 | AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode); 59 | 60 | getApplicationContext().getContentResolver().query(uri, ...); 61 | 62 | getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8; 63 | 64 | getContext().startActivity(intent); 65 | 66 | getContext().startService(intent); 67 | 68 | getContext().sendBroadcast(intent); 69 | ``` 70 | 71 | ### Context的作用域 72 | 73 | 虽然Context神通广大,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。由于Context的具体实例是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会报错。 74 | 75 | ![](https://camo.githubusercontent.com/cb4b5eeeaf7c3c75d4857f966bfe3c4a0626b34c01fd67a5548191eef7ddaf0e/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f313138373233372d666233326230663939326461343738312e706e673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430) 76 | 77 | 从上图我们可以发现Activity所持有的Context的作用域最广,无所不能,因此Activity继承至ContextThemeWrapper,而Application和Service继承至ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大。着重讲一下不推荐使用的两种情况: 78 | 79 | 1. 如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错: 80 | 81 | android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want? 82 | 83 | 这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式都不推荐,Service同Application。 84 | 85 | 2. 在Application和Service中去LayoutInflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用,这种方式也不推荐使用。 86 | 87 | 一句话总结:凡是跟UI相关的,都应该使用Activity作为Context来处理;其他的一些操作,Service、Activity、Application等实例都可以,当然了注意Context引用的持有,防止内存泄露。 88 | 89 | ### getApplication()和getApplicationContext()的区别? 90 | 91 | 其内存地址是一样的。Application本身就是一个Context,这里获取getApplicationContext得到的结果就是Application本身的实例。getApplication方法的语义性很强,就是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application,但是如果在一些其他的场景,比如BroadcastReceiver中也想获取Application实例,这时就可以借助getApplicationContext方法了。 92 | 93 | ``` 94 | public class MyReceiver extends BroadcastReceiver{ 95 | @Override 96 | public void onReceive(Contextcontext,Intentintent){ 97 | Application myApp= (Application)context.getApplicationContext(); 98 | } 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /post/Android系统启动流程.md: -------------------------------------------------------------------------------- 1 | ## Android系统启动概述 2 | 3 | Android 系统启动的过程,大致有以下几部分: 4 | 5 | 6 | 1. 启动电源以及系统启动 7 | 8 | 当电源按下时引导芯片代码从预定义的地方开始执行。加载引导程序 BootLoader 到 RAM,然后执行。 9 | 10 | 2. 引导程序 BootLoader 11 | 12 | 引导程序 BootLoader 是在 Android 操作系统开始运行前的一个小程序,它的主要作用是把系统 OS 拉起来运行。 13 | 14 | 3. Linux 内核启动 15 | 16 | 当内核启动时,设置缓存、存储器、计划列表、加载驱动。当内核完成系统设置时,它首先在系统文件中寻找 init.rc 文件,并启动 init 进程。 17 | 18 | 4. init 进程启动 19 | 20 | 初始化和启动属性服务,并且启动 Zygote 进程。 21 | 22 | 5. Zygote 进程启动 23 | 24 | 创建 Java 虚拟机并为 Java 虚拟机注册 JNI 方法,创建服务端 Socket,启动 SystemServer 进程。 25 | 26 | 6. SystemServer 进程启动 27 | 28 | 启动 Binder 线程池和 SystemServiceManager,并且启动各种系统服务。 29 | 30 | 7. Launcher 启动 31 | 32 | 被 SystemServer 进程启动的 AMS 会启动 Launcher,Launcher 启动后会将已安装的应用的快捷图标显示在界面上。 33 | 34 | ![](https://camo.githubusercontent.com/e30dad1128b7dad0e64f04ce844780bf08a6d6c78f6542857415d6efaa70bd4c/68747470733a2f2f692e6c6f6c692e6e65742f323031392f30342f32332f356362653561366362623131342e706e67) 35 | 36 | ## init进程启动 37 | init 进程是 Android 系统中用户空间的第一个进程,进程号为 1。当我们按下电源时,系统启动后会加载引导程序,引导程序又启动 Linux 内核,在 Linux 内核加载完成后,第一件事就是要启动 init 进程。 38 | 39 | init 进程主要做了以下三件事: 40 | 41 | 1. 创建和挂载启动所需的文件目录 42 | 2. 初始化和启动属性服务 43 | 3. 解析 init.rc 配置文件并启动 Zygote 进程 44 | 45 | ## Zygote 进程启动过程 46 | 47 | 在 Android 系统中,DVM 和 ART、应用程序进程以及运行系统的关键服务的 SystemServer 进程都是由 Zygote 进程来创建的,我们也将它称为孵化器。 48 | 49 | Zygote 进程都是通过 fork 自身来创建子进程的。再通过 JNI 调用 ZygoteInit 的 main 方法后,Zygote 便进入了 Java 框架层,此前是没有任何代码进入 Java 框架层的,也就是说,Zygote 开创了 Java 框架层。 50 | 51 | ```java 52 | public static void main(String argv[]) { 53 | //... 54 | try { 55 | //... 56 | //1. 创建一个 Server 端的 Socket,socketName 为 zygote 57 | zygoteServer.registerServerSocketFromEnv(socketName); 58 | 59 | if (!enableLazyPreload) { 60 | bootTimingsTraceLog.traceBegin("ZygotePreload"); 61 | EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, 62 | SystemClock.uptimeMillis()); 63 | //2. 预加载资源和类 64 | preload(bootTimingsTraceLog); 65 | EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, 66 | SystemClock.uptimeMillis()); 67 | bootTimingsTraceLog.traceEnd(); // ZygotePreload 68 | } else { 69 | Zygote.resetNicePriority(); 70 | } 71 | //... 72 | if (startSystemServer) { 73 | //3. 启动 SystemServer 进程 74 | Runnable r = forkSystemServer(abiList, socketName, zygoteServer); 75 | //4. 等待 AMS 请求 76 | caller = zygoteServer.runSelectLoop(abiList); 77 | } catch (Throwable ex) { 78 | Log.e(TAG, "System zygote died with exception", ex); 79 | throw ex; 80 | } finally { 81 | zygoteServer.closeServerSocket(); 82 | } 83 | } 84 | ``` 85 | Zygote 进程主要做了以下几件事: 86 | 87 | 1. 创建 AppRuntime 并调用其 start 方法,启动 Zygote 进程 88 | 2. 创建 Java 虚拟机并为 Java 虚拟机注册 JNI 方法 89 | 3. 通过 JNI 调用 ZygoteInit 的 main 方法进入 Zygote 的 Java 框架层 90 | 4. 通过 registerZygoteSocket 方法创建服务端 Socket,并通过 runSelectLoop 方法等待 AMS 的请求来创建新的应用程序进程 91 | 5. 启动 SystemServer 进程 92 | 93 | ## SystemServer 94 | 95 | SystemServer 进程主要用于创建系统服务,比如 IMS、AMS、WMS、PMS 等都是由它来创建的。 96 | 97 | SystemServer 进程被创建后,主要做了以下工作: 98 | 99 | 1. 启动 Binder 线程池,这样就可以与其他进程进行通信 100 | 2. 启动 SystemServiceManager,其用于对系统的服务进程创建、启动和生命周期管理 101 | 3. 启动各种系统服务 102 | 103 | -------------------------------------------------------------------------------- /post/ArrayMap的实现原理.md: -------------------------------------------------------------------------------- 1 | ## ArrayMap的实现原理 -------------------------------------------------------------------------------- /post/Binder机制原理.md: -------------------------------------------------------------------------------- 1 | #### 面试题 2 | 3 | 1. 谈谈你对 Binder 的理解? 4 | 2. 一次完整的 IPC 通信流程是怎样的? 5 | 3. Binder 对象跨进程传递的原理是怎么样的? 6 | 4. 说一说 Binder 的 oneway 机制 7 | 5. Framework 中其他的 IPC 通信方式 8 | 9 | #### 谈谈你对 Binder 的理解? 10 | 11 | Binder 是 Android 中一种高效、方便、安全的进程间通信方式。高效是指 Binder 只需要一次数据拷贝,把一块物理内存同时映射到内核和目标进程的用户空间。方便是指用起来简单直接,Client 端使用 Service 端的提供的服务只需要传 Service 的一个描述符即可,就可以获取到 Service 提供的服务接口。安全是指 Binder 验证调用方可靠的身份信息,这个身份信息不能是调用方自己填写的,显然不可靠的,而可靠的身份信息应该是 IPC 机制本身在内核态中添加。 12 | 13 | Binder 通信模型由四方参与,分别是 Binder 驱动层、Client 端、Service 端和 ServiceManager。 14 | 15 | Client 端表示应用程序进程,Service 端表示系统服务,它可能运行在 SystemService 进程,比如 AMS、PKMS等,也可能是运行在一个单独的进程中,比如 SurfaceFlinger。ServiceManager 是 Binder 进程间通信方式的上下文管理者,它提供 Service 端的服务注册和 Client 端的服务获取功能。它们之间是不能直接通信的,需要借助于 Binder 驱动层进行交互。 16 | 17 | 这就需要它们首先通过 binder_open 打开 binder 驱动,然后根据返回的 fd 进行内存映射,分配缓冲区,最后启动 binder 线程,启动 binder 线程一方面是把这个这些线程注册到 binder 驱动,另一方面是这个线程要进入 binder_loop 循环,不断的去跟 binder 驱动进程交互。 18 | 19 | 接下来就可以开始 binder 通信了,我们从 ServiceManager 说起。 20 | 21 | ServiceManger 的 main 函数首先调用 binder_open 打开 binder 驱动,然后调用 binder_become_context_manager 注册为 binder 的大管家,也就是告诉 Binder 驱动 Service 的注册和获取都是通过我来做的,最后进入 binder_loop 循环。binder_loop 首先通过 BC_ENTER_LOOPER 命令协议把当前线程注册为 binder 线程,也就是 ServiceManager 的主线程,然后在一个 for 死循环中不断去读 binder 驱动发送来的请求去处理,也就调用 ioctl。 22 | 23 | 有了 ServiceManager 之后,Service 系统服务就可以向 ServiceManager 进行注册了。也 ServiceFlinger 为例,在它的入口函数 main 函数中,首先也需要启动 binder 机制,也就是上所说的那三步,然后就是初始化 ServiceFlinger,最后就是注册服务。注册服务首先需要拿到 ServiceManager 的 Binder 代理对象,也就是通过 defaultServiceManager 方法,真正获取 ServiceManager 代理对象的是通过 getStrongProxyForHandle(0),也就是查的是句柄值为 0 的 binder 引用,也就是 ServiceManager。如果没查到就说明可能 ServiceManager 还没来得及注册,这个时候 sleep(1) 等等就行了。然后就是调用 addService 来进行注册了。addService 就会把 name 和 binder 对象都写到 Parcel 中,然后就是调用 transact 发送一个 ADD_SERVICE_TRANSACTION 的请求。实际上是调用 IPCThreadState 的 transact 函数,第一个参数是 mHandle 值,也就是说底层在和 binder 驱动进行交互的时候是不区分 BpBinder 还是 BBinder,它只认一个 handle 值。 24 | 25 | Binder 驱动就会把这个请求交给 Binder 实体对象去处理,也就是是在 ServiceManager 的 onTransact 函数中处理 ADD_SERVICE_TRANSACTION 请求,也就是根据 handle 值封装一个 BinderProxy 对象,至此,Service 的注册就完成了。 26 | 27 | 至于 Client 获取服务,其实和这个差不多,也就是拿到服务的 BinderProxy 对象即可。 28 | 29 | 在回答的时候,最后可以画一下图: 30 | 31 | ![](https://i.loli.net/2020/03/27/REqCWzQSnokHKFw.png) 32 | 33 | ![](https://i.loli.net/2020/03/28/1qUCWh5B7vSVzAe.png) 34 | 35 | 上面已经说清楚了 Binder 通信模型的大致流程,下面可以再说一下一次完整的 IPC 通信流程是怎么样的。 36 | 37 | #### 一次完整的 IPC 通信流程是怎样的? 38 | 39 | 首先是从应用层的 Proxy 的 transact 函数开始,传递到 Java 层的 BinderProxy,最后到 Native 层的 BpBinder 的 transact。在 BpBinder 的 transact 实际上是调用 IPCThreadState 的 transact 函数,在它的第一个参数是 handle 值,Binder 驱动就会根据这个 handle 找到 Binder 引用对象,继而找到 Binder 实体对象。在这个函数中,做了两件事件,一件是调用 writeTransactionData 向 Binder 驱动发出一个 BC_TRANSACTION 的命令协议,把所需参数写到 mOut 中,第二件是 waitForResponse 等待回复,在它里面才会真正的和 Binder 驱动进行交互,也就是调用 talkWithDriver,然后接收到的响应执行相应的处理。这时候 Client 接收到的是 BR_TRANSACTION_COMPLETE,表示 Binder 驱动已经接收到了 Client 的请求了。在这里面还有一个 cmd 为 BR_REPLY 的返回协议,表示 Binder 驱动已经把响应返回给 Client 端了。在 talkWithDriver 中,是通过系统调用 ioctl 来和 Binder 驱动进行交互的,传递一个 BINDER_WRITE_READ 的命令并且携带一个 binder_write_read 数据结构体。在 Binder 驱动层就会根据 write_size/read_size 处理该 BINDER_WRITE_READ 命令。 40 | 41 | 到这里,已经讲完了 Client 端如何和 Binder 驱动进行交互的了,下面就讲 Service 端是如何和 Binder 驱动进行交互的。 42 | 43 | Service 端首先会开启一个 Binder 线程来处理进程间通信请求,也就是通过 new Thread 然后把该线程 joinThreadPool 注册到 Binder 驱动。注册呢也就是通过 BC_ENTER_LOOPER 命令协议来做的,接下来就是在 do while 死循环中调用 getAndExecuteCommand。它里面做的就是不断从驱动读取请求,也就是 talkWithDriver,然后再处理请求 executeCommand。在 executeCommand 中,就会根据 BR_TRANSACTION 来调用 BBinder Binder 实体对象的 onTransact 函数来进行处理,然后在发送一个 BC_REPLY 把响应结构返回给 Binder 驱动。Binder 驱动在接收到 BC_REPLY 之后就会向 Service 发送一个 BR_TRANSACTION_COMPLETE 协议表示 Binder 驱动已经收到了,在此同时呢,也会向 Client 端发送一个 BR_REPLY把响应回写给 Client 端。 44 | 45 | 需要注意的是,上面的 onTransact 函数就是 Service 端 AIDL 生成的 Stub 类的 onTransact 函数,这时一次完整的 IPC 通信流程就完成了。 46 | 47 | 最后可画一张图即可: 48 | 49 | ![](https://i.loli.net/2020/03/28/1ZbMj2fUiX8BGc7.png) 50 | 51 | #### Binder 对象跨进程传递的原理是怎么样的? 52 | 53 | 有以下五点: 54 | 55 | 1. Parcel 的 writeStrongBinder 和 readStrongBinder 56 | 2. Binder 在 Parcel 中存储原理,flat_binder_object 57 | 3. 说清楚 binder_node,binder_ref 58 | 4. 目标进程根据 binder_ref 的 handle 创建 BpBinder 59 | 5. 由 BpBinder 再往上到 BinderProxy 到业务层的 Proxy 60 | 61 | 在 Native 层,Binder 对象是存在 Parcel 中的,通过 readStrongBinder/writeStrongBinder 来进行读或写,在其内部是通过一个 flat_binder_object 数据结构进行存储的,它的 type 字段是 BINDER_TYPE_BINDER,表示 Binder 实体对象,它的 cookie 指向自己。 62 | 63 | Parcel 到了驱动层是如何处理的呢?其实就是根据 flat_binder_object 创建用于在驱动层表示的 binder_node Binder 实体对象和 binder_ref Binder 引用对象。 64 | 65 | 读 Binder 对象就是调用 unflatten_binder 把 flat_binder_object 解析出来,如果是 Binder 实体对象,返回的就是 cookie,如果是 Binder 引用对象,就是返回 getStrongProxyForHandle(handle),其实也就是根据 handle 值 new BpBinder 出来。 66 | 67 | #### Binder OneWay 机制 68 | 69 | OneWay 就是异步 binder 调用,带 ONEWAY 的 waitForResponse 参数为 null,也就是不需要等待返回结果,而不带 ONEWAY 的,就是普通的 AIDL 接口,它是需要等待对方回复的。 70 | 71 | 对于系统服务来说,一般都是 oneway 的,比如在启动 Activity 时,它是异步的,不会阻塞系统服务,但是在 Service 端,它是串行化的,都是放在进程的 todo 队列里面一个一个的进行分发处理。 72 | 73 | ![](https://i.loli.net/2020/03/28/8ENCcGDdYVlUKQm.png) 74 | 75 | #### Framework IPC 方式汇总 76 | 77 | Android 是基于 Linux 内核构建的,Linux 已经提供了很多进程间通信机制,比如有管道、Socket、共享内存、信号等,在 Android Framework 中不仅用到了 Binder,这些 IPC 方式也都有使用到。 78 | 79 | 首先讲一下管道,管道是半双工的,管道的描述符只能读或写,想要既可以读也可以写就需要两个描述符,而且管道一般是用在父子进程之间的。Linux 提供了 pipe 函数创建一个管道,传入一个 fd[2] 数组,fd[0] 表示读端,fd[1] 表示写端。假如父进程创建了一对文件描述符,fork 出得子进程继承了这对文件描述符,这时候父进程想要往子进程写东西,就可以拿 fd[1] 写,然后子进程在 fd[0] 就可以读到了。在 Android 中,Native 层的 Looper 使用到了管道,它里面使用 epoll 监听读事件(epoll_wait),如果其他进程往里面写东西他就能收到通知。管道在哪写的呢,其实是在 wake 函数中,当别的线程向 Looper 线程发消息并且需要唤醒 Looper 线程的时候就会调用 wake 函数,wake 函数里面呢就是向管道写一个 "W" 字符。管道使用起来还是很方便的,主要是能配合 epoll 机制监听读写事件。这是 Android 19 才会使用到管道,更高版本使用的是 EventFd。 80 | 81 | 然后就是 Socket,Socket 是全双工的,也就是说既可以读也可以写,而且进程之间不需要亲缘关系,只需要公开一个地址即可。Framewok 中使用到 Socket 最经典的莫过于 Zygote 等待 AMS 请求 fork 应用程序进程了。在 Zygote 的 main 方法中 register 一个 ZygoteSocket,然后进入 runSelectLoop 循环去监听有没有新的连接,如果有的数据发过来就会去调用 runOnce 函数去根据参数 fork 出新的应用程序进程,其实就是去执行 ActivityThread 的 main 函数,然后也会通过这个 Socket 把新创建的应用进程 pid 返回给 Zygote。 82 | 83 | 接着是共享内存,共享内存它是不需要多次拷贝的,而且特别快。拿到文件描述符分别映射到进程的地址空间即可。在 Android 中提供了 MemoryFile 类,里面封装了 ashmem 机制,也就是 Android 的匿名共享内存。首先通过 ashmem_create_region 创建一块匿名共享内存,返回一个 fd,然后调用 mmap 函数给这个 fd 映射到当前进程地址空间中。 84 | 85 | 最后就是信号,信号是单向的,而且发出去之后不关心处理结果,知道进程的 pid 就能发信号了。在杀应用进程的时候会调用 Process 的 killProcess 函数发送一个 SIGNAL_KILL 信号。还有 Zygote 在 fork 完成一个新的子进程之后还会监听 SIGCHLD 信号,如果子进程退出之后就会回收相应的资源,避免子进程成为一个僵尸进程。 86 | 87 | 88 | [Binder 系列口水话 89 | ](https://github.com/Omooo/Android-Notes/blame/master/blogs/Android/%E5%8F%A3%E6%B0%B4%E8%AF%9D/Binder.md) 90 | 91 | -------------------------------------------------------------------------------- /post/Bitmap.md: -------------------------------------------------------------------------------- 1 | ## 一、创建Bitmap 2 | 3 | Bitmap的构造方法的修饰符是default的,意味着无法直接通过构造方法实例化Bitmap。 4 | 5 | 6 | 7 | ### 1. BitmapFactory 8 | 9 | Android中提供了BitmapFactory来实例化Bitmap。BitmapFactory提供了多个decodeXXX方法供从不同来源加载图片资源并解析成Bitmap。主要方法如下图所示: 10 | 11 | 12 | 13 | ![](https://img-blog.csdnimg.cn/20181213211808924.png) 14 | 15 | - **decodeByteArray(byte[] data, int offset, int length, Options opts)** 从指定字节数组的offset位置开始,将长度为length的字节数据解析成Bitmap对象。 16 | - **decodeFile(String pathName, Options opts)** 加载pathName路径的图片,并解析创建Bitmap对象。 17 | - **decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)** 从FileDescriptor对应的文件中解析创建Bitmap对象。 18 | - **decodeResource(Resources res, int id, Options opts)** 用于给定的资源ID从指定的资源汇总解析创建Bitmap对象。 19 | - **decodeStream(InputStream is, Rect outPadding, Options opts)** 用于从指定输入流中解析创建Bitmap对象。 20 | 21 | 下面以decodeResource与decodeByteArray为例来看加载并解析Bitmap 22 | 23 | #### decodeResource 24 | 25 | ```kotlin 26 | val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) 27 | ``` 28 | 29 | #### decodeByteArray 30 | 31 | ```kotlin 32 | val bytes = assets.open("pic.jpg").readBytes() 33 | val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) 34 | ``` 35 | 36 | 37 | 38 | ### 2.Bitmap.createBitmap 39 | 40 | Bitmap中提供了众多重载的静态方法createBitmap来创建Bitmap。主要如下图所示: 41 | 42 | ![](https://img-blog.csdnimg.cn/20181212164203698.png) 43 | 44 | 这些方法大致可以分为三类: 45 | 46 | #### 1) 根据已有的Bitmap来创建新Bitmap 47 | 48 | ```java 49 | /** 50 | * 通过矩阵的方式,返回原始 Bitmap 中的一个不可变子集。新 Bitmap 可能返回的就是原始的 Bitmap,也可能还是复制出来的。 51 | * 新 Bitmap 与原始 Bitmap 具有相同的密度(density)和颜色空间; 52 | * 53 | * @param source 原始 Bitmap 54 | * @param x 在原始 Bitmap 中 x方向的其起始坐标(你可能只需要原始 Bitmap x方向上的一部分) 55 | * @param y 在原始 Bitmap 中 y方向的其起始坐标(你可能只需要原始 Bitmap y方向上的一部分) 56 | * @param width 需要返回 Bitmap 的宽度(px)(如果超过原始Bitmap宽度会报错) 57 | * @param height 需要返回 Bitmap 的高度(px)(如果超过原始Bitmap高度会报错) 58 | * @param m Matrix类型,表示需要做的变换操作 59 | * @param filter 是否需要过滤,只有 matrix 变换不只有平移操作才有效 60 | */ 61 | public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height, 62 | @Nullable Matrix m, boolean filter) { 63 | 64 | } 65 | ``` 66 | 67 | #### 2) 通过像素点数组创建空的Bitmap 68 | 69 | ```java 70 | /** 71 | * 72 | * 返回具有指定宽度和高度的不可变位图,每个像素值设置为colors数组中的对应值。 73 | * 其初始密度由给定的确定DisplayMetrics。新创建的位图位于sRGB 颜色空间中。 74 | * @param display 显示将显示此位图的显示的度量标准 75 | * @param colors 用于初始化像素的sRGB数组 76 | * @param offset 颜色数组中第一个颜色之前要跳过的值的数量 77 | * @param stride 行之间数组中的颜色数(必须> = width或<= -width) 78 | * @param width 位图的宽度 79 | * @param height 位图的高度 80 | * @param config 要创建的位图配置。如果配置不支持每像素alpha(例如RGB_565), 81 | * 那么colors []中的alpha字节将被忽略(假设为FF) 82 | */ 83 | public static Bitmap createBitmap(@NonNull DisplayMetrics display, 84 | @NonNull @ColorInt int[] colors, int offset, int stride, 85 | int width, int height, @NonNull Config config) { 86 | 87 | } 88 | ``` 89 | 90 | #### 3) 创建缩放的Bitmap 91 | 92 | ```java 93 | /** 94 | * 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap 95 | */ 96 | public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter) { 97 | 98 | } 99 | ``` 100 | 101 | 102 | 103 | ## BitmapFactory.Options 104 | 105 | 106 | 107 | 使用BitmapFactory时经常会用到Options这个静态内部类。它内部有很多比较重要的属性。如下: 108 | 109 | - **inJustDecodeBounds** 如果这个值为true,那么在解析的时候不会返回Bitmap,而是只返回这个Bitmap的尺寸。所以,如果只是想知道Bitmap的尺寸,但又不想将其加载到内存中可以使用这个属性。 110 | - **outWidth和outHeight** 表示Bitmap的宽和高。一般和inJustDecodeBounds一起使用来获取Bitmap的宽高,但不加载到内存。 111 | - **inSampleSize** 压缩图片时采样率的值,会根据inSampleSize按照比例(1/inSampleSize)来缩小Bitmap的宽高。如果inSampleSize为2,那么Bitmap的宽为原来的1/2,高为原来的1/2。那么这个Bitmap所占内存会缩小为原来的1/4。 112 | - **inDensity** 表示的是这个Bitmap的像素密度,对应的是DisplayMetrics中的densityDpi,density. 113 | - **inTargetDensity** 表示要被新 **Bitmap** 的目标像素密度,对应的是 **DisplayMetrics** 中的 **densityDpi**。 114 | - **inScreenDensity** 表示实际设备的像素密度,对应的是 **DisplayMetrics** 中的 **densityDpi**。 115 | - **inPreferredConfig** 这个值是设置色彩模式,默认值是 **ARGB_8888**,这个模式下,一个像素点占用 **4Byte** 。**RGB_565** 占用 **2Byte**,**ARGB_4444** 占用 **4Byte**(以废弃)。 116 | - **inPremultiplied** 这个值和透明度通道有关,默认值是 **true**,如果设置为 **true**,则返回的 **Bitmap** 的颜色通道上会预先附加上透明度通道。 117 | - **inDither** 这个值和抖动解码有关,默认值为 **false**,表示不采用抖动解码。 118 | - **inScaled** 设置这个**Bitmap** 是否可以被缩放,默认值是 **true**,表示可以被缩放。 119 | - **inPreferQualityOverSpeed** 这个值表示是否在解码时图片有更高的品质,仅用于 **JPEG** 格式。如果设置为 **true**,则图片会有更高的品质,但是会解码速度会很慢。 120 | - **inBitmap** :这个参数用来实现 Bitmap 内存的复用,但复用存在一些限制,具体体现在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的内存,而 Android 4.4 及以后版本则只要后来的 Bitmap 比之前的小即可。使用 inBitmap 参数前,每创建一个 Bitmap 对象都会分配一块内存供其使用,而使用了 inBitmap 参数后,多个 Bitmap 可以复用一块内存,这样可以提高性能。 121 | 122 | 123 | 124 | **BitmapFactory.Options的使用** 125 | 126 | ```kotlin 127 | val options = BitmapFactory.Options() 128 | options.inPreferredConfig = Bitmap.Config.RGB_565 // 设置bitmap的颜色格式 129 | options.inSampleSize = 2 // 设置采样率 130 | val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options) 131 | ``` 132 | 133 | 134 | 135 | ## 图片压缩 136 | 137 | ### 1.质量压缩 138 | 139 | 质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。 140 | 141 | 我们可以看到有个参数:quality,可以调节你压缩的比例,但是还要注意一点就是,质量压缩对png格式这种图片没有作用,因为png是无损压缩。 142 | 143 | ```java 144 | private void compressQuality() { 145 | Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test); 146 | mSrcSize = bm.getByteCount() + "byte"; 147 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 148 | bm.compress(Bitmap.CompressFormat.JPEG, 100, bos); 149 | byte[] bytes = bos.toByteArray(); 150 | mSrcBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); 151 | } 152 | ``` 153 | 154 | ### 2. 采样压缩 155 | 156 | 这个方法主要用图片分辨率较大,但是设置的目标View较小时进行的。可以通过采样压缩将图片分辨率压缩到View的宽高相等。由于图片的分辨率减小,所有图片加载到内存时占用的空间也会更小。代码如下: 157 | 158 | ```java 159 | BitmapFactory.Options options = new Options(); 160 | options.inSampleSize = 2; 161 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options); 162 | ``` 163 | 164 | ### 3.矩阵缩放 165 | 166 | 可以通过矩阵来缩放图片的尺寸达到压缩图片的效果,与采样压缩原理一样。 167 | 168 | ```java 169 | private void compressMatrix() { 170 | Matrix matrix = new Matrix(); 171 | matrix.setScale(0.5f, 0.5f); 172 | Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test); 173 | mSrcBitmap = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true); 174 | bm = null; 175 | } 176 | ``` 177 | 178 | ### 4. 使用inPreferredConfig压缩 179 | 180 | 通过inPreferredConfig的配置,修改单个像素点占用的内存来实现压缩。如在包含透明通道的图片中可以将inPreferredConfig设置为RGB_565,相比RGB_8888节省一半的内存开销。 181 | 182 | 内容来源: 183 | 184 | https://blog.csdn.net/wanliguodu/article/details/84973846 185 | 186 | https://www.jianshu.com/p/08ed0e3c4e71 -------------------------------------------------------------------------------- /post/Butterknife实现原理.md: -------------------------------------------------------------------------------- 1 | ## Butterknife实现原理 -------------------------------------------------------------------------------- /post/CAS、UnSafe类即Automic并发包.md: -------------------------------------------------------------------------------- 1 | ## CAS的实现原理 2 | 3 | CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下 4 | 5 | 执行函数:CAS(V,E,N);其包含3个参数: 6 | 7 | - V表示要更新的变量 8 | 9 | - E表示预期值 10 | 11 | - N表示新值 12 | 13 | 如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下 14 | 15 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/thread/cas.png) 16 | 17 | 由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。 18 | 19 | ### 1.什么是原子操作? 20 | 21 | 假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。 22 | 23 | ### 2.如何实现原子操作? 24 | 实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁, 25 | 26 | 实现原子操作还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。 27 | 28 | CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。 29 | 30 | 31 | ### 3.CAS实现原子操作的三大问题 32 | 33 | - ABA问题。 34 | 35 | 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。 36 | 37 | ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。 38 | 如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。 39 | 40 | - 循环时间长开销大。 41 | 42 | 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 43 | 44 | - 只能保证一个共享变量的原子操作。 45 | 46 | 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。 47 | 还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。 48 | 49 | ## Unsafe类 50 | 51 | Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,据说Oracle正在计划从Java 9中去掉Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下: 52 | 53 | ### 1.内存管理,Unsafe类中存在直接操作内存的方法 54 | ``` 55 | //分配内存指定大小的内存 56 | public native long allocateMemory(long bytes); 57 | //根据给定的内存地址address设置重新分配指定大小的内存 58 | public native long reallocateMemory(long address, long bytes); 59 | //用于释放allocateMemory和reallocateMemory申请的内存 60 | public native void freeMemory(long address); 61 | //将指定对象的给定offset偏移量内存块中的所有字节设置为固定值 62 | public native void setMemory(Object o, long offset, long bytes, byte value); 63 | //设置给定内存地址的值 64 | public native void putAddress(long address, long x); 65 | //获取指定内存地址的值 66 | public native long getAddress(long address); 67 | 68 | //设置给定内存地址的long值 69 | public native void putLong(long address, long x); 70 | //获取指定内存地址的long值 71 | public native long getLong(long address); 72 | //设置或获取指定内存的byte值 73 | public native byte getByte(long address); 74 | public native void putByte(long address, byte x); 75 | //其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同 76 | 77 | //操作系统的内存页大小 78 | public native int pageSize(); 79 | ``` 80 | 81 | ### 2.提供实例对象新途径。 82 | ``` 83 | //传入一个对象的class并创建该实例对象,但不会调用构造方法 84 | public native Object allocateInstance(Class cls) throws InstantiationException; 85 | ``` 86 | ### 3.CAS 操作相关 87 | CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现,在稍后讲解Atomic系列内部方法是基于下述方法的实现的。 88 | 89 | ``` 90 | //第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值, 91 | //expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。 92 | public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x); 93 | 94 | public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 95 | 96 | public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x); 97 | ``` 98 | 这里还需介绍Unsafe类中JDK 1.8新增的几个方法,它们的实现是基于上述的CAS方法,如下 99 | ``` 100 | //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta, 101 | //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值 102 | public final int getAndAddInt(Object o, long offset, int delta) { 103 | int v; 104 | do { 105 | //获取内存中最新值 106 | v = getIntVolatile(o, offset); 107 | //通过CAS操作 108 | } while (!compareAndSwapInt(o, offset, v, v + delta)); 109 | return v; 110 | } 111 | 112 | //1.8新增,方法作用同上,只不过这里操作的long类型数据 113 | public final long getAndAddLong(Object o, long offset, long delta) { 114 | long v; 115 | do { 116 | v = getLongVolatile(o, offset); 117 | } while (!compareAndSwapLong(o, offset, v, v + delta)); 118 | return v; 119 | } 120 | 121 | //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue, 122 | //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值 123 | public final int getAndSetInt(Object o, long offset, int newValue) { 124 | int v; 125 | do { 126 | v = getIntVolatile(o, offset); 127 | } while (!compareAndSwapInt(o, offset, v, newValue)); 128 | return v; 129 | } 130 | 131 | // 1.8新增,同上,操作的是long类型 132 | public final long getAndSetLong(Object o, long offset, long newValue) { 133 | long v; 134 | do { 135 | v = getLongVolatile(o, offset); 136 | } while (!compareAndSwapLong(o, offset, v, newValue)); 137 | return v; 138 | } 139 | 140 | //1.8新增,同上,操作的是引用类型数据 141 | public final Object getAndSetObject(Object o, long offset, Object newValue) { 142 | Object v; 143 | do { 144 | v = getObjectVolatile(o, offset); 145 | } while (!compareAndSwapObject(o, offset, v, newValue)); 146 | return v; 147 | } 148 | 149 | ``` 150 | 151 | ### 4.类和实例对象以及变量的操作,主要方法如下 152 | ``` 153 | //获取字段f在实例对象中的偏移量 154 | public native long objectFieldOffset(Field f); 155 | //静态属性的偏移量,用于在对应的Class对象中读写静态属性 156 | public native long staticFieldOffset(Field f); 157 | //返回值就是f.getDeclaringClass() 158 | public native Object staticFieldBase(Field f); 159 | 160 | 161 | //获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址, 162 | //通过偏移量便可得到该对象的变量,进行各种操作 163 | public native int getInt(Object o, long offset); 164 | //设置给定对象上偏移量的int值 165 | public native void putInt(Object o, long offset, int x); 166 | 167 | //获得给定对象偏移量上的引用类型的值 168 | public native Object getObject(Object o, long offset); 169 | //设置给定对象偏移量上的引用类型的值 170 | public native void putObject(Object o, long offset, Object x); 171 | //其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同 172 | 173 | //设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见 174 | public native void putIntVolatile(Object o, long offset, int x); 175 | //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。 176 | public native int getIntVolatile(Object o, long offset); 177 | 178 | //其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile及getIntVolatile相同,引用类型putObjectVolatile也一样。 179 | 180 | //与putIntVolatile一样,但要求被操作字段必须有volatile修饰 181 | public native void putOrderedInt(Object o,long offset,int x); 182 | ``` 183 | 184 | 185 | ## 并发包中的原子操作类 186 | 187 | 子更新基本类型主要包括3个类: 188 | 189 | - AtomicBoolean:原子更新布尔类型 190 | - AtomicInteger:原子更新整型 191 | - AtomicLong:原子更新长整型 192 | 193 | 这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等. 194 | 195 | ``` 196 | public class AtomicInteger extends Number implements java.io.Serializable { 197 | private static final long serialVersionUID = 6214790243416807050L; 198 | 199 | // 获取指针类Unsafe 200 | private static final Unsafe unsafe = Unsafe.getUnsafe(); 201 | 202 | //下述变量value在AtomicInteger实例对象内的内存偏移量 203 | private static final long valueOffset; 204 | 205 | static { 206 | try { 207 | //通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移 208 | //通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作 209 | valueOffset = unsafe.objectFieldOffset 210 | (AtomicInteger.class.getDeclaredField("value")); 211 | } catch (Exception ex) { throw new Error(ex); } 212 | } 213 | //当前AtomicInteger封装的int变量value 214 | private volatile int value; 215 | 216 | public AtomicInteger(int initialValue) { 217 | value = initialValue; 218 | } 219 | public AtomicInteger() { 220 | } 221 | //获取当前最新值, 222 | public final int get() { 223 | return value; 224 | } 225 | //设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。 226 | public final void set(int newValue) { 227 | value = newValue; 228 | } 229 | //最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载 230 | public final void lazySet(int newValue) { 231 | unsafe.putOrderedInt(this, valueOffset, newValue); 232 | } 233 | //设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法 234 | public final int getAndSet(int newValue) { 235 | return unsafe.getAndSetInt(this, valueOffset, newValue); 236 | } 237 | //如果当前值为expect,则设置为update(当前值指的是value变量) 238 | public final boolean compareAndSet(int expect, int update) { 239 | return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 240 | } 241 | //当前值加1返回旧值,底层CAS操作 242 | public final int getAndIncrement() { 243 | return unsafe.getAndAddInt(this, valueOffset, 1); 244 | } 245 | //当前值减1,返回旧值,底层CAS操作 246 | public final int getAndDecrement() { 247 | return unsafe.getAndAddInt(this, valueOffset, -1); 248 | } 249 | //当前值增加delta,返回旧值,底层CAS操作 250 | public final int getAndAdd(int delta) { 251 | return unsafe.getAndAddInt(this, valueOffset, delta); 252 | } 253 | //当前值加1,返回新值,底层CAS操作 254 | public final int incrementAndGet() { 255 | return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 256 | } 257 | //当前值减1,返回新值,底层CAS操作 258 | public final int decrementAndGet() { 259 | return unsafe.getAndAddInt(this, valueOffset, -1) - 1; 260 | } 261 | //当前值增加delta,返回新值,底层CAS操作 262 | public final int addAndGet(int delta) { 263 | return unsafe.getAndAddInt(this, valueOffset, delta) + delta; 264 | } 265 | //省略一些不常用的方法.... 266 | } 267 | ``` 268 | 通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过Unsafe类中的CAS相关操作的方法实现的,这也同时证明AtomicInteger是基于无锁实现的,这里重点分析自增操作实现过程,其他方法自增实现原理一样。 269 | 270 | 271 | 参考链接:https://blog.csdn.net/javazejian/article/details/72772470 272 | -------------------------------------------------------------------------------- /post/Dalvik与ART.md: -------------------------------------------------------------------------------- 1 | Android开发中我们接触的是与Java虚拟机类似的Dalvik虚拟机和ART虚拟机,下面梳理一下三者区别和原理: 2 | 3 | 一,Dalvik虚拟机 4 | Dalvik虚拟机( Dalvik Virtual Machine ),简称Dalvik VM或者DVM。Dalvik 发音有道词典并没有收录。说说来历,它是由Dan Bornstein编写的,名字源于他的祖先居住过的名为Dalvik的小渔村。DVM是Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。需要注意的是DVM并不是一个Java虚拟机(以下简称JVM) 5 | 6 | ##### DVM与JVM的区别 7 | 8 | DVM之所以不是一个JVM ,主要原因是DVM并没有遵循JVM规范来实现。DVM与JVM主要有以下区别。 9 | 10 | **基于的架构不同** 11 | JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度慢,对于性能有限的移动设备,显然不是很适合。 12 | DVM是基于寄存器的,它没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈指令,同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。 13 | 14 | ##### 执行的字节码不同 15 | 16 | 在Java SE程序中,Java类会被编译成一个或多个.class文件,打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为: .java文件 -> .class文件 -> .jar文件 17 | 而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该.dex文件读取指令和数据。执行顺序为: 18 | .java文件 –>.class文件-> .dex文件 19 | 20 | ![img](https://img-blog.csdn.net/20180103114759812?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaWJsYWRl/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 21 | 22 | 如上图所示,.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。 23 | 而在.apk文件中只包含了一个.dex文件,这个.dex文件里面将所有的.class里面所包含的信息全部整合在一起了,这样再加载就提高了速度。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中,减少了I/O操作,提高了类的查找速度。 24 | 25 | ##### DVM允许在有限的内存中同时运行多个进程 26 | 27 | DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。 28 | 29 | ##### DVM由Zygote创建和初始化 30 | 31 | 在Android系统启动流程(二)解析Zygote进程启动过程这篇文章中我介绍过 Zygote,可以称它为孵化器,它是一个DVM进程,同时它也用来创建和初始化DVM实例。每当系统需要创建一个应用程序时,Zygote就会fock自身,快速的创建和初始化一个DVM实例,用于应用程序的运行。 32 | 33 | ##### DVM架构 34 | 35 | DVM的源码位于dalvik/目录下,其中dalvik/vm目录下的内容是DVM的具体实现部分,它会被编译成libdvm.so;dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump是.dex文件的反编译工具;DVM的可执行程序位于dalvik/dalvikvm中,将会被编译成dalvikvm可执行程序。DVM架构如下图所示。 36 | 37 | ![这里写图片描述](https://img-blog.csdn.net/20180103115025320?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaWJsYWRl/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 38 | 39 | 从上图可以看出,首先Java编译器编译的.class文件经过DX工具转换为.dex文件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行,最后交与Linux处理。 40 | 41 | ##### DVM的运行时堆 42 | 43 | DVM的运行时堆主要由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,所有进程都共享该区域,比如系统资源。Allocation Space是在Zygote进程fork第一个子进程之前创建的,它是一种私有进程,Zygote进程和fock的子进程在Allocation Space上进行对象分配和释放。 44 | 45 | ##### 除了这两个Space,还包含以下数据结构: 46 | 47 | Card Table: 用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。 48 | Heap Bitmap: 有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。 49 | Mark Stack: DVM的运行时堆使用标记-清除(Mark-Sweep)算法进行GC,不了解标记-清除算法的同学查看Java虚拟机(四)垃圾收集算法这篇文章。Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。 50 | 51 | #### 二.ART虚拟机 52 | 53 | ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ART。在Android 5.0时,默认采用ART,DVM从此退出历史舞台。 54 | 55 | ##### ART与DVM的区别 56 | 57 | 1. DVM中的应用每次运行时,字节码都需要通过即时编译器(JIT,just in time)转换为机器码,这会使得应用的运行效率降低。而在ART中,系统在安装应用时会进行一次预编译(AOT,ahead of time),将字节码预先编译成机器码并存储在本地,这样应用每次运行时就不需要执行编译了,运行效率也大大提升。 58 | 2. ART占用空间比Dalvik大(字节码变为机器码之后,可能会增加10%-20%),这就是“时间换空间大法”。 59 | 3. 预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。 60 | 61 | ##### ART的运行时堆 62 | 63 | 1. 与DVM的GC不同的是,ART的GC类型有多种,主要分为Mark-Sweep GC和Compacting GC。ART的运行时堆的空间根据不同的GC类型也有着不同的划分,如果采用的是Mark-Sweep GC,运行时堆主要是由四个Space和多个辅助数据结构组成,四个Space分别是Zygote Space、Allocation Space、Image Space和Large Object Space。Zygote Space、Allocation Space和DVM中的作用是一样的。Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12k)。其中Zygote Space和Image Space是进程间共享的。 64 | 采用Mark-Sweep GC的运行时堆空间划分如下图所示。 65 | 2. ![这里写图片描述](https://img-blog.csdn.net/20180103115804781?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaWJsYWRl/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 66 | 67 | ##### 除了这四个Space,ART的Java堆中还包括两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map,以及三个Object Stack -------------------------------------------------------------------------------- /post/HR面常问问题.md: -------------------------------------------------------------------------------- 1 | ### 1.X年内的规划是什么/自己的职业规划是什么/半年后你觉得你能做出怎样的成绩? 2 | 3 | "首先还是立足于本职工作,把本职工作做好。另外自己的优势会在Android开发方向,也是自己感兴趣的内容,因此希望在以后的工作中能不断的提升自己的能力。在以后的工作中承担更多的职能,想负责管理一个团队。" 4 | 你有野心,想成为 leader,看好公司发展同时自己发展和公司能保持平行,且为公司业务出一份力,这些都是你的目标。这样就给HR传递了一个非常积极且有目标性的信号。 5 | 6 | ### 2.你觉得你最大的缺点是什么? 7 | 回答示范1: 我的公开演讲能力比较差, 在公共场合讲话的时候我会感到紧张, 不过谈论我熟悉的领域我会比较放松。所以当我需要做公开发言的时候,我必须要准备得很充分。我确实羡慕那些无论什么话题都能够高谈阔论的人。 8 | 9 | 回答示范2: 我有的时候做事情宏观有余, 细节不足。有时犯一些低级的错误, 比方说把打字的时候把2005年打成2004年, 丢东西什么的。去年我和同学一起策划迎新晚会的时候,我忘记了最后检查一次麦克风, 结果演出半小时之前发现麦克风失灵, 引起了很大的恐慌。所以我特别喜欢和注重细节的人在一起, 能从他们身上学到很多东西。 10 | 11 | 回答示范3: 我有时候急于求成, 或者说做事爱急躁。一旦接手一个任务, 总是想要尽快把它赶完, 总觉得做完了一件事情心里才舒服。但是,欲速则不达, 太追求efficiency, 就会牺牲accuracy。我现在总是提醒自己accuracy第一位, efficiency第二位, 这样会好得多。 12 | 13 | ### 3.我们部门最近项目要赶进度,你来的话会经常加班?对于加班你怎么看? 14 | 15 | 这时候如果真的是你不能接受加班,那就实话实说。不排除这是一个压力测试,所以你可以马上反问:”会经常加班吗?公司经常加班的强度频率如何” 16 | 当你这么一问,你也能知道公司究竟是怎么加班的?是赶项目还是硬性规定?这样面试官在接收你这个问题的时候会开始解释他定义的加班强度是怎么样的,你听清楚之后可以根据自身情况判断是否可以接受,就能巧妙化解这个问题。最后你还可以这么说,面试官听到之后就可以做出相应判断了。 17 | “这个加班方面自己有一些想法,赶项目加班属于正常,自己也非常支持希望看到项目早点上线,但是如果每天的工作可以按时完成,这样还需要加班,这样不太合适。” 18 | 19 | ### 4.你有什么问题想问我吗? 20 | 21 | #### (1) 职责 22 | - 试用期内可能遇到最大的困难是什么? 23 | - 如何评估试用期内的工作表现? 24 | - On-call (电话值班)的计划或者规定是什么? 25 | - -值班或者遇到问题加班时候有加班费吗? 26 | - 我的日常工作是什么? 27 | - 团队里面初级和高级工程师的比例是多少?(有计划改变吗) 28 | - 入职培训会是什么样的? 29 | - 自己单独的开发活动和按部就班工作的比例大概是怎样的? 30 | - 每天预期/核心工作时间是多少小时? 31 | - 在你看来,这个工作做到什么程度算成功? 32 | - 我入职的岗位是新增还是接替之前离职的同事?(是否有技术债需要还)?(zh) 33 | - 入职之后在哪个项目组,项目是新成立还是已有的?(zh) 34 | 35 | #### (2)技术 36 | 37 | - 公司常用的技术栈是什么? 38 | - 你们怎么使用源码控制系统? 39 | - 你们怎么测试代码? 40 | - 你们怎么追踪 bug? 41 | - 你们怎么集成和部署代码改动?是使用持续集成和持续部署吗? 42 | - 你们的基础设施搭建方法在版本管理系统里吗?或者是代码化的吗? 43 | - 从计划到完成一项任务的工作流是什么样的? 44 | - 你们如何准备故障恢复? 45 | - 有标准的开发环境吗?是强制的吗? 46 | - 你们需要花费多长时间来给产品搭建一个本地测试环境?(分钟/小时/天) 47 | - 你们需要花费多长时间来响应代码或者依赖中的安全问题? 48 | - 所有的开发者都可以使用他们电脑的本地管理员权限吗? 49 | - 公司是否有技术分享交流活动?有的话,多久一次呢?(zh) 50 | 51 | #### (3) 团队 52 | 53 | - 工作是怎么组织的? 54 | - 团队内/团队间的交流通常是怎样的? 55 | - 如果遇到不同的意见怎样处理? 56 | - 谁来设定优先级 / 计划? 57 | - 如果被退回了会怎样?(“这个在预计的时间内做不完”) 58 | - 每周都会开什么类型的会议? 59 | - 产品/服务的规划是什么样的?(n周一发布 / 持续部署 / 多个发布流 / ...) 60 | - 生产环境发生事故了怎么办?是否有不批评人而分析问题的文化? 61 | - 有没有一些团队正在经历还尚待解决的挑战? 62 | - 公司技术团队的架构和人员组成?(zh) 63 | 64 | #### (3) 公司 65 | 66 | - 有没有会议/旅行预算?使用的规定是什么? 67 | - 晋升流程是怎样的?要求/预期是怎样沟通的? 68 | - 技术和管理两条职业路径是分开的吗? 69 | - 对于多元化招聘的现状或者观点是什么? 70 | - 有公司级别的学习资源吗?比如电子书订阅或者在线课程? 71 | - 有获取证书的预算吗? 72 | - 公司的成熟度如何?(早期寻找方向 / 有内容的工作 / 维护中 / ...) 73 | - 我可以为开源项目做贡献吗?是否需要审批? 74 | - 有竞业限制或者保密协议需要签吗? 75 | - 你们认为公司文化中的空白是什么? 76 | - 能够跟我说一公司处于不良情况,以及如何处理的故事吗? 77 | 78 | #### (4) 商业 79 | 80 | - 你们现在盈利吗? 81 | - 如果没有的话,还需要多久? 82 | - 公司的资金来源是什么?谁影响或者指定高层计划或方向? 83 | - 你们如何挣钱? 84 | - 什么阻止了你们挣更多的钱? 85 | - 你们认为什么是你们的竞争优势? 86 | 87 | #### (5) 远程工作 88 | 89 | - 远程工作和办公室工作的比例是多少? 90 | - 公司提供硬件吗?更新计划如何? 91 | - 额外的附件和家居可以通过公司购买吗?这方面是否有预算? 92 | - 有共享办公或者上网的预算吗? 93 | - 多久需要去一次办公室? 94 | - 公司的会议室是否一直为视频会议准备着? 95 | 96 | #### (6) 办公室工作 97 | 98 | - 办公室的布局如何?(开放的 / 小隔间 / 独立办公室) 99 | - 有没有支持/市场/或者其他需要大量打电话的团队在我的团队旁边办公? 100 | 101 | #### (7) 待遇 102 | 103 | - 如果有奖金计划的话,奖金如何分配? 104 | - 如果有奖金计划的话,过去的几年里通常会发百分之多少的奖金? 105 | - 有五险一金或者其他退休养老金等福利吗?如果有的话,公司有配套的商业保险吗? 106 | - 贵公司是如何保证人才不流失的? 107 | 108 | #### (8)带薪休假 109 | 110 | - 带薪休假时间有多久? 111 | - 病假和事假是分开的还是一起算? 112 | - 我可以提前使用假期时间吗?也就是说应休假期是负的? 113 | - 假期的更新策略是什么样的?也就是说未休的假期能否滚入下一周期 114 | - 照顾小孩的政策如何? 115 | - 无薪休假政策是什么样的? 116 | 117 | 118 | 119 | https://blog.csdn.net/valada/article/details/79909962 -------------------------------------------------------------------------------- /post/HTTPS实现原理.md: -------------------------------------------------------------------------------- 1 | HTTPS(Hypertext Transfer Protocol Secure)是一种通过计算机网络进行安全通信的传输协议。**HTTPS经由HTTP进行通信,但利用TLS来加密数据包。**HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。 2 | 3 | > HTTPS: 在HTTP之下增加一个安全层TLS,来保证HTTP的加密传输 4 | 5 | TLS是传输层加密协议,前身是SSL协议。由网景公司于1995年发布。后改名为TLS。常用的 TLS 协议版本有:TLS1.2, TLS1.1, TLS1.0 和 SSL3.0。其中 SSL3.0 由于 POODLE 攻击已经被证明不安全。TLS1.0 也存在部分安全漏洞,比如 RC4 和 BEAST 攻击。 6 | 7 | 由于HTTP协议采用明文传输,我们可以通过抓包很轻松的获取到HTTP所传输的数据。因此,采用HTTP协议是不安全的。这才催生了HTTPS的诞生。 8 | 9 | HTTPS相对HTTP提供了更安全的数据传输保障。主要体现在三个方面: 10 | 11 | - 1, 内容加密。客户端到服务器的内容都是以加密形式传输,中间者无法直接查看明文内容。 12 | - 2, 身份认证。通过校验保证客户端访问的是自己的服务器。 13 | - 3, 数据完整性。防止内容被第三方冒充或者篡改。 14 | 15 | 其实为了提高安全性和效率HTTPS结合了对称和非对称两种加密方式。即客户端使用对称加密生成密钥(key)对传输数据进行加密,然后使用非对称加密的公钥再对key进行加密。因此网络上传输的数据是被key加密的密文和用公钥加密后的密文key,因此即使被黑客截取,由于没有私钥,无法获取到明文key,便无法获取到明文数据。所以HTTPS的加密方式是安全的。 16 | 17 | ### 对称加密 18 | 19 | 对称加密,顾名思义就是加密和解密都是使用同一个密钥,常见的对称加密算法有 DES、3DES 和 AES 等,其优缺点如下: 20 | 21 | 优点:算法公开、计算量小、加密速度快、加密效率高,适合加密比较大的数据。 22 | 23 | 缺点:交易双方需要使用相同的密钥,也就无法避免密钥的传输,而密钥在传输过程中无法保证不被截获,因此对称加密的安全性得不到保证。 24 | 25 | 每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一密钥,这会使得发收信双方所拥有的钥匙数量急剧增长,密钥管理成为双方的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高 26 | 27 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/8ef74a4247efc6956fd52049ffcf5057.png#pic_center) 28 | 29 | ### 非对称加密 30 | 31 | 非对称加密,顾名思义,就是加密和解密需要使用两个不同的密钥:公钥(public key)和私钥(private key)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公钥对外公开;得到该公钥的乙方使用公钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的私钥对加密后的信息进行解密。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。常用的非对称加密算法是 RSA 算法,想详细了解的同学点这里:RSA 算法详解一、RSA 算法详解二,其优缺点如下: 32 | 33 | 优点:算法公开,加密和解密使用不同的钥匙,私钥不需要通过网络进行传输,安全性很高。 34 | 35 | 缺点:计算量比较大,加密和解密速度相比对称加密慢很多。 36 | 37 | 由于非对称加密的强安全性,可以用它完美解决对称加密的密钥泄露问题,效果图如下: 38 | 39 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/90549163fc4db4f2bc2d689a8b34a7e7.png#pic_center) 40 | 41 | 在上述过程中,客户端在拿到服务器的公钥后,会生成一个随机码 (用 KEY 表示,这个 KEY 就是后续双方用于对称加密的密钥),然后客户端使用公钥把 KEY 加密后再发送给服务器,服务器使用私钥将其解密,这样双方就有了同一个密钥 KEY,然后双方再使用 KEY 进行对称加密交互数据。在非对称加密传输 KEY 的过程中,即便第三方获取了公钥和加密后的 KEY,在没有私钥的情况下也无法破解 KEY (私钥存在服务器,泄露风险极小),也就保证了接下来对称加密的数据安全。而上面这个流程图正是 HTTPS 的雏形,HTTPS 正好综合了这两种加密算法的优点,不仅保证了通信安全,还保证了数据传输效率。 42 | 43 | ### HTTPS的连接实现 44 | 45 | HTTPS的连接过程可以简单分为五步: 46 | 47 | 1. 客户端请求建立TLS连接 48 | 2. 服务端发回证书 49 | 3. 客户端验证服务器证书 50 | 4. 客户端信任服务器证书后和服务器协商对称秘钥 51 | 5. 使用对称秘钥开始通信 52 | 53 | 详细的流程如下: 54 | 55 | 1. 客户端发送一个字节的“Client Hello”请求连接服务器,另外还会附加客户端支持的TLS可选的版本集合、可选的加密套件(英文Cipher Suites 包括可选的对称加密算法、可选的非对称加密算法及可选的 Hash 算法)以及一个客户端随机数。 56 | 2. 服务端根据客户端发送的信息选取TLS版本、加密算法,然后向客户端发送一个字节的Server Hello,并附加选中的TLS版本、加密算法及一个服务端随机数。服务端和客户端协商成功后各自保都存了使用的TLS版本、加密套件,以及客户端随机数和服务端随机数。 57 | 3. 服务器将证书发送到客户端,证书中包含了 58 | - 服务器的公钥 59 | - 服务器公钥的签名,签名是用私钥对数据的Hash值进行非对称加密的计算得到的结果。这里加密的私钥是另选的一对,与服务器公钥没有关系。这个签名可以被加密时的私钥对应的公钥解开,这个公钥也是由证书签发机构提供的。但是,现在并不能确定这个证书签发机构提供的公钥是否可信,所以又对这个[公钥签名]的公钥用另一个公钥再次进行签名,最终可以追溯到系统内置的根证书中的公钥。系统中根证书是无条件被信任的。 60 | - 证书所属的主机名,客户端需要验证主机名是否是自己要访问的,防止中间人使用CA证书进行攻击。 61 | 4. 客户端收到后需要验证公钥的合法性后,会发送一个Pre-Master secret,这是一个使用服务器公钥加密后的随机数。 62 | 5. 服务端收到Pre-Master secret后用其私钥进行解密,并且客户端与服务端都会根据客户端随机数、服务端随机数以及这个Pre-Master secret来算出一个Master secret。这里的客户端随机数、服务端随机数每次都会重新生成,这样可以防止 replay attack。 63 | 6. 接着,客户端与服务端还会通过 Master secret生成客户端加密秘钥、服务端加密秘钥、客户端Mac Secret、服务端Mac Secret。 64 | 7. 客户端与服务端通过对称加密进行通信。客户端使用客户端秘钥加密数据发送到服务端,服务端接收到数据后使用客户端秘钥进行解密。接着服务端使用服务端秘钥对返回数据进行加密并返回到客户端,客户端使用服务端秘钥进行解密得到返回数据。 65 | 66 | 为什么要把服务端秘钥和客户端秘钥分开? 67 | 68 | 也是为了防止中间人攻击。如果客户端与服务端使用相同的秘钥,虽然中间人截取数据后无法解码数据,但可以原封不动的返回,而由于客户端与服务端秘钥相同,客户端并不能确定是自己法的数据还是服务端返回的数据。 69 | 70 | -------------------------------------------------------------------------------- /post/HandlerThread实现原理.md: -------------------------------------------------------------------------------- 1 | HandlerThread 继承于 Thread,所以它本质就是个 Thread。与普通 Thread 的区别在于,它不仅建立了一个线程,并且创建了消息队列,有自己的 Looper,可以让我们在自己的线程中分发和处理消息,并对外提供自己的 Looper 的 get 方法。 2 | 3 | HandlerThread 自带 Looper 使它可以通过消息队列来重复使用当前线程,节省系统资源开销。这是它的优点也是缺点,每个任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。 4 | 5 | ### HandlerTread使用 6 | 7 | ``` 8 | public class HandlerThreadActivity extends AppCompatActivity { 9 | 10 | private Button mButton; 11 | private HandlerThread mHandlerThread; 12 | private Handler mUiHandler; 13 | private Handler mChildHandler; 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_handler); 19 | initView(); 20 | 21 | mHandlerThread = new HandlerThread("HandlerThread"); 22 | mHandlerThread.start(); 23 | mUiHandler = new Handler(new Handler.Callback() { 24 | @Override 25 | public boolean handleMessage(Message msg) { 26 | if (msg.what == 2) { 27 | mButton.setText("子线程更新"); 28 | } 29 | return false; 30 | } 31 | }); 32 | mChildHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() { 33 | @Override 34 | public boolean handleMessage(Message msg) { 35 | if (msg.what == 1) { 36 | try { 37 | //子线程模拟延迟处理 38 | Thread.sleep(2000); 39 | mUiHandler.sendEmptyMessage(2); 40 | } catch (InterruptedException e) { 41 | e.printStackTrace(); 42 | } 43 | } 44 | return false; 45 | } 46 | }); 47 | 48 | } 49 | 50 | public void initView() { 51 | mButton = findViewById(R.id.btn_show); 52 | mButton.setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick(View v) { 55 | mChildHandler.sendEmptyMessage(1); 56 | } 57 | }); 58 | } 59 | } 60 | ``` 61 | 62 | ### 源码分析 63 | ``` 64 | public class HandlerThread extends Thread { 65 | int mPriority; 66 | int mTid = -1; 67 | Looper mLooper; 68 | private @Nullable Handler mHandler; 69 | 70 | public HandlerThread(String name) { 71 | super(name); 72 | mPriority = Process.THREAD_PRIORITY_DEFAULT; 73 | } 74 | 75 | public HandlerThread(String name, int priority) { 76 | super(name); 77 | mPriority = priority; 78 | } 79 | 80 | protected void onLooperPrepared() { 81 | } 82 | 83 | @Override 84 | public void run() { 85 | mTid = Process.myTid(); 86 | Looper.prepare(); 87 | synchronized (this) { 88 | mLooper = Looper.myLooper(); 89 | // 通知取 Looper 的线程,此时 Looper 已经创建好了 90 | notifyAll(); 91 | } 92 | Process.setThreadPriority(mPriority); 93 | onLooperPrepared(); 94 | Looper.loop(); 95 | mTid = -1; 96 | } 97 | 98 | public Looper getLooper() { 99 | if (!isAlive()) { 100 | return null; 101 | } 102 | 103 | synchronized (this) { 104 | while (isAlive() && mLooper == null) { 105 | try { 106 | // 如果新线程还未创建 Looper,则等待 107 | wait(); 108 | } catch (InterruptedException e) { 109 | } 110 | } 111 | } 112 | return mLooper; 113 | } 114 | 115 | @NonNull 116 | public Handler getThreadHandler() { 117 | if (mHandler == null) { 118 | mHandler = new Handler(getLooper()); 119 | } 120 | return mHandler; 121 | } 122 | 123 | public boolean quit() { 124 | Looper looper = getLooper(); 125 | if (looper != null) { 126 | looper.quit(); 127 | return true; 128 | } 129 | return false; 130 | } 131 | 132 | public boolean quitSafely() { 133 | Looper looper = getLooper(); 134 | if (looper != null) { 135 | looper.quitSafely(); 136 | return true; 137 | } 138 | return false; 139 | } 140 | 141 | public int getThreadId() { 142 | return mTid; 143 | } 144 | } 145 | ``` 146 | 源码很简单,就是在 run 方法中执行 Looper.prepare()、Looper.loop() 构造消息循环系统。外界可以通过 getLooper() 这个方法拿到这个 Looper。 147 | 148 | 总结为下: 149 | - HandlerThread 是一个自带 Looper 的线程,因此只能作为子线程使用 150 | - HandlerThread 必须配合 Handler 使用,HandlerThread 线程中具体做什么事,需要在 Handler 的 callback 中进行,因为它自己的 run 方法被写死了 151 | - 子线程的 Handler 与 HandlerThread 关系建立是通过构造子线程的Handler 传入 HandlerThread 的 Looper 。所以在此之前,必须先调用 mHandlerThread.start 让 run 方法跑起来 Looper 才能创建。 152 | 153 | https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/HandlerThread.md -------------------------------------------------------------------------------- /post/Handler的实现原理.md: -------------------------------------------------------------------------------- 1 | 2 | Android 应用是通过消息驱动运行的,在 Android 中一切皆消息,包括触摸事件,视图的绘制、显示和刷新等等都是消息。Handler 是消息机制的上层接口,平时开发中我们只会接触到 Handler 和 Message,内部还有 MessageQueue 和 Looper 两大助手共同实现消息循环系统。 3 | **(1)Handler** 4 | 通过Handler的sendXXX或者postXXX来发送一个消息,这里要注意post(Runnable r)方法也会将Runnable包装成一个Message,代码如下: 5 | 6 | ```java 7 | public final boolean post(Runnable r){ 8 | return sendMessageDelayed(getPostMessage(r), 0); 9 | } 10 | public final boolean postDelayed(Runnable r, long delayMillis){ 11 | return sendMessageDelayed(getPostMessage(r), delayMillis); 12 | } 13 | private static Message getPostMessage(Runnable r) { 14 | Message m = Message.obtain(); 15 | m.callback = r; 16 | return m; 17 | } 18 | 19 | ``` 20 | 从代码中可以看到将Runnable赋值给了Message.callback了。最终sendXXX和postXXX都会调用到sendMessageAtTime,代码如下: 21 | 22 | ```java 23 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 24 | MessageQueue queue = mQueue; 25 | if (queue == null) { 26 | RuntimeException e = new RuntimeException( 27 | this + " sendMessageAtTime() called with no mQueue"); 28 | Log.w("Looper", e.getMessage(), e); 29 | return false; 30 | } 31 | return enqueueMessage(queue, msg, uptimeMillis); 32 | } 33 | ``` 34 | 在这个方法中最终调用了enqueueMessage方法,这里注意将this赋值给了Message.target,而此处this就是Handler。 35 | 36 | ```java 37 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { 38 | msg.target = this; 39 | if (mAsynchronous) { 40 | msg.setAsynchronous(true); 41 | } 42 | //转到 MessageQueue 的 enqueueMessage 方法 43 | return queue.enqueueMessage(msg, uptimeMillis); 44 | } 45 | 46 | ``` 47 | enqueueMessage方法最终调用了MessageQueue的enqueueMessage方法,将消息放入队列。 48 | **(2)MessageQueue** 49 | MessageQueue是一个优先级队列,核心方法是enqueueMessage和next方法,也就是将插入队列,将消息取出队列的操作。 50 | 之所以说MessageQueue是一个优先级队列是因为enqueueMessage方法中会根据Message的执行时间来对消息插入,这样越晚执行的消息会被插入到队列的后边。 51 | 52 | 而next方法是一个死循环,如果队列中有消息,则next方法会将Message移除队列并返回该Message,如果队列中没有消息该方法则会处于阻塞状态。 53 | 54 | **(3)Looper** 55 | Looper可以理解为一个消息泵,Looper的核心方法是loop。注意loop方法的第一行会首先通过myLooper来得到当前线程的Looper,接着拿到Looper中的MessageQueue,然后开启一个死循环,它会不断的通过MessageQueue的next方法将消息取出来,并执行。代码如下: 56 | 57 | ```java 58 | public static void loop() { 59 | final Looper me = myLooper();// 这里要特别注意,是从ThreadLocal中拿到当前线程的Looper。 60 | if (me == null) { 61 | throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); 62 | } 63 | final MessageQueue queue = me.mQueue; 64 | 65 | for (;;) { 66 | //从 MessageQueue 中取消息 67 | Message msg = queue.next(); // might block 68 | if (msg == null) { 69 | // No message indicates that the message queue is quitting. 70 | return; 71 | } 72 | //通过 Handler 分发消息 73 | msg.target.dispatchMessage(msg); 74 | //回收消息 75 | msg.recycleUnchecked(); 76 | } 77 | } 78 | ``` 79 | 可以看到在取出Message后则会调用Message.target调用dispatchMessage方法,这里target就是Handler,它是在Handler的enqueueMessage时赋值的。紧接着将Message进行了回收。 80 | 接下来再回到Handler看dispatchMessage,代码如下: 81 | 82 | ```java 83 | public void dispatchMessage(Message msg) { 84 | if (msg.callback != null) { 85 | //通过 handler.postXxx 形式传入的 Runnable 86 | handleCallback(msg); 87 | } else { 88 | if (mCallback != null) { 89 | //以 Handler(Handler.Callback) 写法 90 | if (mCallback.handleMessage(msg)) { 91 | return; 92 | } 93 | } 94 | //以 Handler(){} 内存泄露写法 95 | handleMessage(msg); 96 | } 97 | } 98 | ``` 99 | 可以看到,这里最终会调用到我们自己的实现方法。 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /post/HashMap实现原理.md: -------------------------------------------------------------------------------- 1 | ## 1.HashMap的工作原理 2 | HashMap是基于哈希表实现的,用于存储key-value的键值对,并允许使用null值和null键。由于是基于Hash表实现的,因此HashMap具有较高的查询效率,理想情况下HashMap的查找时间复杂度可达到O(1)。 3 | 4 | **(1)HashMap的存储结构** 5 | HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体(链地址法处理冲突)。HashMap内部封装了一个包含key和value的Entry类,用于存储键值对。在put操作中会根据key的hashcode计算在哈希表中的存储位置,并将Entry存入该位置。由于存在Hash冲突的情况,HashMap采用了链地址法来处理Hash冲突。即使用链表的形式将相同哈希值的元素连起来。如下图所示: 6 | ![在这里插入图片描述](https://img-blog.csdn.net/20180422235248719?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Zpc2FudA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 7 | 取元素时在HashMap的get方法中计算key的哈希值,并定位到元素所在桶的位置,接着使用equals方法查找到目标元素。 8 | 9 | **(2) HashMap的扩容与ReHash** 10 | 由于HashMap存的长度是确定的,可以初始化时候指定长度或者默认长度16。随着HashMap中插入的元素越来越多,发生哈希冲突的概率会越来越大,相应的查找的效率就会越来越低。这意味着影响哈希表性能的因素除了哈希函数与处理冲突的方法之外,还与哈希表的装填因子大小有关。 11 | 12 | > 我们将哈希表中元素数与哈希表长度的比值称为**装填因子**。装填因子 **α= $\frac{哈希表中元素数}{哈希表长度}$** 13 | 14 | 在HashMap中,装填因子的阈值为0.75,当装填因子大于0.75时则会出发HashMap的扩容机制。这里我们应该知道,扩容并不是在原数组基础上扩大容量,而是需要申请一个长度为原来2倍的新数组。因此,扩容之后就需要将原来的数据从旧数组中重新散列存放到扩容后的新数组。这个过程我们称之为Rehash。 15 | 16 | Rehash的操作将会重新散列扩容前已经存储的数据,这一操作涉及大量的元素移动,是一个非常消耗性能的操作。因此,在开发中我们应该尽量避免Rehash的出现。比如,可以预估元素的个数,事先指定哈希表的长度,这样可以有效减少Rehash。 17 | 18 | **(3)JDK1.8中对HashMap的优化** 19 | JDK1.7 中,HashMap 采用位桶 + 链表的实现,即使用链表来处理冲突,同一 hash 值的链表都存储在一个数组中。但是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。如下图所示的元素查找效率直接降低为了链表的时间复杂度o(n) 20 | 21 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200923232503838.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 22 | 为了优化这一问题,JDK 1.8 在底层结构方面做了一些改变,当每个桶中元素大于 8 的时候,会转变为红黑树,而红黑树的查找效率为o(logn),高于链表的查找效率。 23 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200927225706751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 24 | 详情参考: 25 | [面试官:哈希表都不知道,你是怎么看懂HashMap的?](https://blog.csdn.net/qq_20521573/article/details/108701471) 26 | [HashMap原理深入理解](https://blog.csdn.net/visant/article/details/80045154) 27 | [HashMap的工作原理](https://blog.csdn.net/ty564457881/article/details/78206049) 28 | 29 | ## 2.HashMap扩容为什么是2的幂次方? 30 | 31 | - 为了保证扩容后的数组索引与扩容前的数组索引一致 32 | 33 | - Rehash时的取余操作hash % length == hash & (length - 1)这个关系只有在length等于二的幂次方时成立 34 | 35 | ### 1). 保证新数组与老数组的索引一致 36 | 37 | HashMap的初始容量是2的4次幂,扩容时候也是2的倍数。这样可以使添加的元素均匀的分布在HashMap中的数组上,减少Hash冲突。避免形成链表结构,进而提升查询效率。 38 | 39 | 16的二进制表示为10000,length-1的二进制位表示为01111,扩容之后变为32,二进制表示为100000,减1之后的二进制位为011111。 40 | 41 | 扩容前后只有一位的差距,即相当于(32-1)>>1==(16-1)。如下图所示: 42 | 43 | 44 | 45 | ![1b9f4958eac5224f0e625dc679b3282d](https://img-blog.csdnimg.cn/img_convert/1b9f4958eac5224f0e625dc679b3282d.png) 46 | 47 | 这样在通过has&(length-1)时,只要hash对应的最左边的哪一个差异位为0,就能保证到扩容后的数组和老数组的索引一致。 48 | 49 | 当数组长度保持2的次幂,length-1的低位都为1,这样会使得数组索引index更加均匀。如下图: 50 | 51 | ![a488d3b9cf221fac3f00681604fba489](https://img-blog.csdnimg.cn/img_convert/a488d3b9cf221fac3f00681604fba489.png) 52 | 53 | 上面的&运算,高位是不会对结果产生影响的(hash函数采用各种位运算也是为了使得低位更加散列)。只关注低位的bit,如果低位全部为1,那么对于h低位部分来说任何一位的变化都会对结果产生影响。也就是说,只要得到index=21这个存储位置,h的低位只有这一种组合。这也是数组长度设计为2次幂的原因 54 | 55 | ![c28b9183a595598f0a4f2561797d51fe](https://img-blog.csdnimg.cn/img_convert/c28b9183a595598f0a4f2561797d51fe.png) 56 | 57 | 如果不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分不再具有唯一性了,哈希冲突的几率会变的更大,同时,index对应的这个bit位无论如何不会等于1了,而对应的那些数组位置也就被白白浪费了。 58 | 59 | 60 | https://www.cxyzjd.com/article/weixin_44273302/113733422 61 | 62 | https://blog.csdn.net/samniwu/article/details/90550196 63 | 64 | -------------------------------------------------------------------------------- /post/Hash表与HashMap.md: -------------------------------------------------------------------------------- 1 | ## 一、什么是哈希表? 2 | 在回答这个问题之前我们先来思考一个问题:**如何在一个无序的线性表中查找一个数据元素?** 3 | 4 | 注意,这是一个无序的线性表,也就是说要查找的这个元素在线性表中的位置是随机的。对于这样的情况,想要找到这个元素就必须对这个线性表进行遍历,然后与要查找的这个元素进行比较。这就意味着查找这个元素的时间复杂度为o(n)。对于o(n)的时间复杂度,在查找海量数据的时候也是一个非常消耗性能的操作。那么有没有一种数据结构,这个数据结构中的元素与它所在的位置存在一个对应关系,这样的话我们就可以通过这个元素直接找到它所在的位置,而此时查找这个元素的时间复杂度就变成了o(1),可以大大节省程序的查找效率。当然,这种数据结构是存在的,它就是我们今天要讲的**哈希表**。 5 | 6 | 我们先来看一下哈希表的定义: 7 | 8 | > **哈希表**又叫**散列表**,是一种根据设定的映射函数f(key)将一组关键字映射到一个有限且连续的地址区间上,并以关键字在地址区间中的“像”作为元素在表中的存储位置的一种数据结构。这个映射过程称为**哈希造表**或者**散列**,这个映射函数f(key)即为**哈希函数**也叫**散列函数**,通过哈希函数得到的存储位置称为**哈希地址**或**散列地址** 9 | 10 | 定义总是这么的拗口且难以理解。简单来说,哈希表就是通过一个映射函数f(key)将一组数据散列存储在数组中的一种数据结构。在这哈希表中,每一个元素的key和它的存储位置都存在一个f(key)的映射关系,我们可以通过f(key)快速的查找到这个元素在表中的位置。 11 | 12 | 举个例子,有一组数据:[19,24,6,33,51,15],我们用散列存储的方式将其存储在一个长度为11的数组中。采用**除留取余法**,将这组数据分别模上数组的长度(即f(key)=key % 11),以余数作为该元素在数组中的存储的位置。则会得到一个如下图所示的哈希表: 13 | 14 | ![哈希表示例](https://img-blog.csdnimg.cn/20200922233650707.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 15 | 16 | 17 | 此时,如果我们想从这个表中找到值为15的元素,只需要将15模上11即可得到15在数组中的存储位置。可见哈希表对于查找元素的效率是非常高的。 18 | 19 | ## 二、什么是哈希冲突 20 | 上一节中我们举了一个很简单的例子来解释什么是哈希表,例子中的这组数据只有6个元素。假如我们向这组数据中再插入一些元素,插入后的数据为:[19,24,6,33,51,15,25,72],新元素25模11后得到3,存储到3的位置没有问题。而接下来我们对72模11之后得到了6,而此时在数组中6的位置已经被其他元素给占据了。“72“只能很无奈的表示我放哪呢? 21 | ![哈希冲突](https://img-blog.csdnimg.cn/20200922233756827.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 22 | 对于上述情况我们将其称之为哈希冲突。哈希冲突比较官方的定义为: 23 | 24 | > 对于不同的关键字,可能得到同一个哈希地址,即key1≠key2,而 f(key1)=f(key2),对于这种现象我们称之为**哈希冲突**,也叫**哈希碰撞** 25 | 26 | 一般情况下,哈希冲突只能尽可能的减少,但不可能完全避免。因为哈希函数是从关键字集合到地址集合的映射,通常来说关键字集合比较大,它的元素理论上包括所有可能的关键字,而地址集合的元素仅为哈希表中的地址值。这就导致了哈希冲突的必然性。 27 | 28 | ### 1.如何减少哈希冲突? 29 | 尽管哈希冲突不可避免,但是我们也要尽可能的减少哈希冲突的出现。一个好的哈希函数可以有效的减少哈希冲突的出现。那什么样的哈希函数才是一个好的哈希函数呢?通常来说,一个好的哈希函数对于关键字集合中的任意一个关键字,经过这个函数映射到地址集合中任何一个集合的概率是相等的。 30 | 31 | 常用的构造哈希函数的方法有以下几种: 32 | **(1)除留取余法** 33 | 34 | 这个方法我们在上边已经有接触过了。取关键字被某个不大于哈希表长m的数p除后所得余数为哈希地址。即:f(key)=key % p, p≤m; 35 | 36 | **(2)直接定址法** 37 | 38 | 直接定址法是指取关键字或关键字的某个线性函数值为哈希地址。即: f(key)=key 或者 f(key)=a*key+b、 39 | 40 | **(3)数字分析法** 41 | 42 | 假设关键字是以为基的数(如以10为基的十进制数),并且哈希表中可能出现的关键字都是事先知道的,则可以选取关键字的若干位数组成哈希表。 43 | 44 | 当然,除了上边列举的几种方法,还有很多种选取哈希函数的方法,就不一一列举了。我们只要知道,选取合适的哈希函数可以有效减少哈希冲突即可。 45 | 46 | ### 2.如何处理哈希冲突? 47 | 48 | 虽然我们可以通过选取好的哈希函数来减少哈希冲突,但是哈希冲突终究是避免不了的。那么,碰到哈希冲突应该怎么处理呢?接下来我们来介绍几种处理哈希冲突的方法。 49 | 50 | **(1)开放定址法** 51 | 52 | 开放定址法是指当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。 53 | 54 | 我们以本节开头的例子来讲解开放定址法是如何处理冲突的。72模11后得到6,而此时6的位置已经被其他元素占用了,那么将6加1得到7, 55 | 此时发现7的位置也被占用了,那就再加1得到下一个地址为8,而此时8仍然被占用,再接着加1得到9,此时9处为空,则将72存入其中,即得到如下哈希表: 56 | 57 | ![线性探测再散列](https://img-blog.csdnimg.cn/20200923005618516.png#pic_center) 58 | 像上边的这种探测方法称为**线性探测再散列**。当然除了线性探测再散列之外还有二次探测再散列,探测地址的方式为原哈希地址加上d (d= $±1^2$、$±2^2$、$±3^2$......$±m^2$),经过二次探测再散列后会得到求得72的哈希地址为5,存储如下图所示: 59 | 60 | ![二次探测再散列](https://img-blog.csdnimg.cn/20200923010619960.png#pic_center) 61 | 62 | **(2)再哈希法** 63 | 64 | 再哈希法即选取若干个不同的哈希函数,在产生哈希冲突的时候计算另一个哈希函数,直到不再发生冲突为止。 65 | 66 | **(3)建立公共溢出区** 67 | 68 | 专门维护一个溢出表,当发生哈希冲突时,将值填入溢出表。 69 | 70 | **(4)链地址法** 71 | 72 | 链地址法是指在碰到哈希冲突的时候,将冲突的元素以链表的形式进行存储。也就是凡是哈希地址为**i**的元素都插入到同一个链表中,元素插入的位置可以是表头(**头插法**),也可以是表尾(**尾插法**)。我们以仍然以[19,24,6,33,51,15,25,72] 73 | 这一组数据为例,用链地址法来进行哈希冲突的处理,得到如下图所示的哈希表: 74 | 75 | ![链地址法](https://img-blog.csdnimg.cn/20200923223502759.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 76 | 我们可以向这组数据中再添加一些元素,得到一组新的数据[19,24,6,33,51,15,25,72,37,17,4,55,83]。使用链地址法得到如下哈希表: 77 | 78 | ![链地址法](https://img-blog.csdnimg.cn/20200923224438164.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 79 | 80 | ## 三、链地址法的弊端与优化 81 | 上一节中我们讲解了几种常用的处理哈希冲突的方法。其中比较常用的是链地址法,比如HashMap就是基于链地址法的哈希表结构。虽然链地址法是一种很好的处理哈希冲突的方法,但是在一些极端情况下链地址法也会出现问题。举个例子,我们现在有这样一组数据:[48,15,26,4,70,82,59]。我们将这组数据仍然散列存储到长度为11的数组中,此时则得到了如下的结果: 82 | 83 | ![链地址法存在的问题](https://img-blog.csdnimg.cn/20200923232503838.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 84 | 85 | 可以发现,此时的哈希表俨然已经退化成了一个链表,当我们在这样的数据结构中去查找某个元素的话,时间复杂度又变回了o(n)。这显然不符合我们的预期。因此,当哈希表中的链表过长时就需要我们对其进行优化。我们知道,二叉查找树的查询效率是远远高于链表的。因此,当哈希表中的链表过长时我们就可以把这个链表变成一棵红黑树。上面的一组数据优化后可得到如下结果: 86 | 87 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200927225706751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 88 | 89 | 90 | 91 | 红黑树是一个可以自平衡的二叉查找树。它的查询的时间复杂度为o(lgn)。通过这样的优化可以提高哈希表的查询效率。 92 | ## 四、哈希表的扩容与Rehash 93 | 94 | 在哈希表长度不变的情况下,随着哈希表中插入的元素越来越多,发生哈希冲突的概率会越来越大,相应的查找的效率就会越来越低。这意味着影响哈希表性能的因素除了哈希函数与处理冲突的方法之外,还与哈希表的**装填因子**大小有关。 95 | 96 | 97 | >我们将哈希表中元素数与哈希表长度的比值称为**装填因子**。装填因子 **α= $\frac{哈希表中元素数}{哈希表长度}$** 98 | 99 | 很显然,**α**的值越小哈希冲突的概率越小,查找时的效率也就越高。而减小**α**的值就意味着降低了哈希表的使用率。显然这是一个矛盾的关系,不可能有完美解。为了兼顾彼此,装填因子的最大值一般选在0.65~0.9之间。比如HashMap中就将装填因子定为0.75。一旦HashMap的装填因子大于0.75的时候,为了减少哈希冲突,就需要对哈希表进行**扩容**操作。比如我们可以将哈希表的长度扩大到原来的2倍。 100 | 101 | 这里我们应该知道,扩容并不是在原数组基础上扩大容量,而是需要申请一个长度为原来2倍的新数组。因此,扩容之后就需要将原来的数据从旧数组中重新散列存放到扩容后的新数组。这个过程我们称之为**Rehash**。 102 | 103 | 接下来我们仍然以[19,24,6,33,51,15,25,72,37,17,4,55,83]这组数据为例来演示哈希表扩容与Rehash的过程。假设哈希表的初始长度为11,装载因子的最大值定位0.75,扩容前的数据插入如下图所示: 104 | 105 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201128120210212.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 106 | 107 | 当我们插入第9个元素的时候发现此时的装填因子已经大于了0.75,因此触发了扩容操作。为了方便画图,这里将数组长度扩展到了18。扩容后将[19,24,6,33,51,15,25,72,37,17,4,55,83]这组数据重新散列,会得到如下图所示的结果: 108 | 109 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200925142348236.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70#pic_center) 110 | 111 | 112 | 可以看到扩容前后元素存储位置大相径庭。Rehash的操作将会重新散列扩容前已经存储的数据,这一操作涉及大量的元素移动,是一个非常消耗性能的操作。因此,在开发中我们应该尽量避免Rehash的出现。比如,可以预估元素的个数,事先指定哈希表的长度,这样可以有效减少Rehash。 113 | 114 | ## 五、总结 115 | 116 | 哈希表是数据结构中非常重要的一个知识点,本篇文章详细的讲解了哈希表的相关概念,让大家对哈希表有了一个清晰的认识。哈希表弥补了线性表或者树的查找效率低的问题,通过哈希表在理想的情况下可以将查找某个元素的时间复杂度降低到o(1),但是由于哈希冲突的存在,哈希表的查找效率很难达到理想的效果。另外,哈希表的扩容与Rehash的操作对哈希表存储时的性能也有很大的影响。由此可见使用哈希表存储数据也并非一个完美的方案。但是,对于查找性能要求高的情况下哈希表的数据结构还是我们的不二选择。 117 | 118 | 最后了解了哈希表对于理解HashMap会有莫大的帮助。毕竟HashMap本身就是基于哈希表实现的。 119 | 120 | [面试官:哈希表都不知道,你是怎么看懂HashMap的?](https://juejin.cn/post/6876105622274703368) -------------------------------------------------------------------------------- /post/Http协议.md: -------------------------------------------------------------------------------- 1 | ## 什么是HTTP? 2 | 3 | HTTP是 Hyper Text Transfer Protocol的简称,中文名超文本传输协议,是一个基于TCP/IP 协议来传输超文本的应用层协议。 4 | 5 | HTTP协议工作与客户端-服务器架构上,浏览器作为HTTP的客户端通过URL向HTTP服务器端发送请求。服务端收到请求后向客户端发送相应信息。HTTP的默认端口为80. 6 | 7 | - HTTP是无连接的。即每次只处理一个请求,服务器处理完客户端的请求,并收到客户端的应答后就断开连接。 8 | - HTTP是无状态的。对于请求的处理没有记忆能力,如果后续处理需要前面的信息,则必须重传。 9 | 10 | ## HTTP 消息结构 11 | 12 | 13 | 14 | ### 客户端请求 15 | 16 | 客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。 17 | 18 | ![](https://www.runoob.com/wp-content/uploads/2013/11/2012072810301161.png) 19 | 20 | 21 | 22 | ### 服务端响应 23 | 24 | HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。 25 | 26 | ![](https://www.runoob.com/wp-content/uploads/2013/11/httpmessage.jpg) 27 | 28 | 29 | 30 | ## HTTP的请求方法 31 | 32 | HTTP1.0定义了三种方法,分别为GET、POST和HEAD方法。HTTP1.1新增了六种方法PUT、DELETE、OPTIONS、PATCH、TRACE 和 CONNECT 方法。这里只介绍常用的五种方法: 33 | 34 | ### 1.GET 35 | 36 | 用来获取资源,不进行服务器数据操作,因此请求数据中没有Body。 37 | 38 | ### 2.POST 39 | 40 | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 41 | 42 | ### 3.PUT 43 | 44 | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。用于修改资源,有Body 45 | 46 | ### 4.DELETE 47 | 48 | 请求服务器删除指定的页面。没有Body 49 | 50 | ### 5.HEAD 51 | 52 | 类似于 GET 请求,只不过返回的响应中没Body内容,用于获取报头 53 | 54 | ## HTTP 的状态码 55 | 56 | 1xx : 临时性消息,服务器收到请求,需要请求者继续执行操作 57 | 58 | 2xx : 成功,操作被成功接收并处理 59 | 60 | 3xx : 重定向,需要进一步的操作以完成请求 61 | 62 | 4xx : 客户端错误,请求包含语法错误或无法完成请求 63 | 64 | 5xx : 服务器错误,服务器在处理请求的过程中发生了错误 65 | 66 | ## HTTP的响应头 67 | 68 | HTTP 的请求头示例如下: 69 | 70 | ``` 71 | GET /home.html HTTP/1.1 72 | Host: developer.mozilla.org 73 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0 74 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 75 | Accept-Language: en-US,en;q=0.5 76 | Accept-Encoding: gzip, deflate, br 77 | Content-Type:text/html;charset=UTF-8 78 | Referer: https://developer.mozilla.org/testpage.html 79 | Connection: keep-alive 80 | Upgrade-Insecure-Requests: 1 81 | If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT 82 | If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a" 83 | Cache-Control: max-age=0 84 | ``` 85 | 86 | 87 | 88 | | 应答头 | 说明 | 89 | | :--------------- | :----------------------------------------------------------- | 90 | | Allow | 服务器支持哪些请求方法(如GET、POST等)。 | 91 | | Content-Encoding | 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept-Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。 | 92 | | Content-Length | 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStream,完成后查看其大小,然后把该值放入Content-Length头,最后通过byteArrayStream.writeTo(response.getOutputStream()发送内容。 | 93 | | Content-Type | 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。 | 94 | | Date | 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。 | 95 | | Expires | 应该在什么时候认为文档已经过期,从而不再缓存它? | 96 | | Last-Modified | 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。 | 97 | | Location | 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。 | 98 | | Refresh | 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader("Refresh", "5; URL=http://host/path")让浏览器读取指定的页面。 注意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV="Refresh" CONTENT="5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。 注意Refresh的意义是"N秒之后刷新本页面或访问指定页面",而不是"每隔N秒刷新本页面或访问指定页面"。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV="Refresh" ...>。 注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。 | 99 | | Server | 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。 | 100 | | Set-Cookie | 设置和页面关联的Cookie。Servlet不应使用response.setHeader("Set-Cookie", ...),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。 | 101 | | WWW-Authenticate | 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的应答中这个头是必需的。例如,response.setHeader("WWW-Authenticate", "BASIC realm=\"executives\"")。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。 | 102 | 103 | 104 | 105 | ### Content-Type 106 | 107 | Content-Type 用于定义网络文件的类型和网页的编码,决定浏览器将以什么样的形式,什么样的编码读取这个文件。语法格式: 108 | 109 | ``` 110 | Content-Type: text/html; charset=utf-8 111 | Content-Type: multipart/form-data; boundary=something 112 | ``` 113 | 114 | 常见的媒体格式类型如下: 115 | 116 | - text/html : HTML格式 117 | - text/plain :纯文本格式 118 | - text/xml : XML格式 119 | - image/gif :gif图片格式 120 | - image/jpeg :jpg图片格式 121 | - image/png:png图片格式 122 | 123 | 以application开头的媒体格式类型: 124 | 125 | - application/xhtml+xml :XHTML格式 126 | - application/xml: XML数据格式 127 | - application/atom+xml :Atom XML聚合格式 128 | - application/json: JSON数据格式 129 | - application/pdf:pdf格式 130 | - application/msword : Word文档格式 131 | - application/octet-stream : 二进制流数据(如常见的文件下载) 132 | - application/x-www-form-urlencoded :
中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式) 133 | 134 | 另外一种常见的媒体格式是上传文件之时使用的: 135 | 136 | - multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式 137 | 138 | ## 一次完整的HTTP请求 139 | 140 | 1. 首先进行DNS域名解析(本地浏览器缓存、操作系统缓存或者DNS服务器),首先会搜索浏览器自身的DNS缓存(缓存时间比较短,大概只有1分钟,且只能容纳1000条缓存) 141 | - 如果浏览器自身的缓存里面没有找到,那么浏览器会搜索系统自身的DNS缓存 142 | - 如果还没有找到,那么尝试从 hosts文件里面去找 143 | - 在前面三个过程都没获取到的情况下,就去域名服务器去查找, 144 | 145 | 2. 三次握手建立 TCP 连接 146 | 147 | 在HTTP工作开始之前,客户端首先要通过网络与服务器建立连接,HTTP连接是通过 TCP 来完成的。HTTP 是比 TCP 更高层次的应用层协议,根据规则,只有低层协议建立之后,才能进行高层协议的连接,因此,首先要建立 TCP 连接,一般 TCP 连接的端口号是80; 148 | 149 | 3. 客户端发起HTTP请求 150 | 151 | 4. 服务器响应HTTP请求 152 | 153 | 5. 客户端解析html代码,并请求html代码中的资源 154 | 155 | 浏览器拿到html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,就向服务器端去请求下载 156 | 157 | 6. 客户端渲染展示内容 158 | 159 | 7. 关闭 TCP 连接 160 | 161 | 一般情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接,然后如果客户端或者服务器在其头信息加入了这行代码 Connection:keep-alive ,TCP 连接在发送后将仍然保持打开状态,于是,客户端可以继续通过相同的连接发送请求,也就是说前面的3到6,可以反复进行。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。 162 | 163 | https://www.runoob.com/http/http-tutorial.html -------------------------------------------------------------------------------- /post/IdleHandler.md: -------------------------------------------------------------------------------- 1 | IdleHandler是一个位于MessageQueue中的接口,源码如下: 2 | 3 | ```java 4 | public static interface IdleHandler { 5 | /** 6 | * Called when the message queue has run out of messages and will now 7 | * wait for more. Return true to keep your idle handler active, false 8 | * to have it removed. This may be called if there are still messages 9 | * pending in the queue, but they are all scheduled to be dispatched 10 | * after the current time. 11 | */ 12 | boolean queueIdle(); 13 | } 14 | ``` 15 | 16 | IdleHandler中有一个queueIdle方法,根据方法上的注释可以知道这个方法会在MessageQueue中没有消息时或者只有延迟消息时才会被调用。方法的返回值是一个boolean值,返回true会将IdleHandler保留,否则会将其移除。 17 | 18 | MessageQueue中关于IdleHandler的方法有两个: 19 | 20 | ```java 21 | // 存放IdleHandler的集合 22 | private final ArrayList mIdleHandlers = new ArrayList(); 23 | 24 | // 添加IdleHandler 25 | public void addIdleHandler(@NonNull IdleHandler handler) { 26 | if (handler == null) { 27 | throw new NullPointerException("Can't add a null IdleHandler"); 28 | } 29 | synchronized (this) { 30 | mIdleHandlers.add(handler); 31 | } 32 | } 33 | 34 | // 移除IdleHandler 35 | public void removeIdleHandler(@NonNull IdleHandler handler) { 36 | synchronized (this) { 37 | mIdleHandlers.remove(handler); 38 | } 39 | } 40 | ``` 41 | 42 | 在MessageQueue中维护了一个IdleHandler集合,并且提供了添加IdleHandler和移除IdleHandler的方法。可以通过 `Looper.myQueue().addIdleHandler(new Idler())` 添加一个IdleHandler。 43 | 44 | mIdleHandlers是在MessageQueue中被执行的,相关代码如下; 45 | 46 | ```java 47 | Message next() { 48 | // 初次执行pendingIdleHandlerCount 的值是-1 49 | int pendingIdleHandlerCount = -1; 50 | int nextPollTimeoutMillis = 0; 51 | for (;;) { 52 | if (nextPollTimeoutMillis != 0) { 53 | Binder.flushPendingCommands(); 54 | } 55 | // 阻塞方法,调用native层的epoll监听文件描述符的写入事件来实现 56 | // 如果 nextPollTimeoutMillis = -1,则会一直阻塞,不会超时 57 | // 如果 nextPollTimeoutMillis = 0,不会阻塞,立即返回 58 | // 如果 nextPollTimeoutMillis >0,表示最长的阻塞时间为nextPollTimeoutMillis,如果期间被唤醒则立即返回 59 | nativePollOnce(ptr, nextPollTimeoutMillis); 60 | 61 | synchronized (this) { 62 | // ... 省略消息处理相关逻辑 63 | 64 | // 1.pendingIdleHandlerCount默认值为-1,满足第一个条件 65 | // 2.没有消息,或者当前没有要执行的消息 66 | // 同时满足以上两个条件才会进入if语句 67 | if (pendingIdleHandlerCount < 0 68 | && (mMessages == null || now < mMessages.when)) { 69 | // 获取IdleHandler的个数并赋值给pendingIdleHandlerCount 70 | pendingIdleHandlerCount = mIdleHandlers.size(); 71 | } 72 | 73 | if (pendingIdleHandlerCount <= 0) { 74 | // 没有要执行的IdleHandler 75 | mBlocked = true; 76 | continue; 77 | } 78 | 79 | if (mPendingIdleHandlers == null) { 80 | // 初始化一个最小值为4的IdleHandler数组 81 | mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; 82 | } 83 | // 将mIdleHandlers添加到数组中 84 | mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); 85 | } 86 | 87 | // 遍历数组执行IdleHandler 88 | for (int i = 0; i < pendingIdleHandlerCount; i++) { 89 | // 取出IdleHandler 90 | final IdleHandler idler = mPendingIdleHandlers[i]; 91 | mPendingIdleHandlers[i] = null; // release the reference to the handler 92 | 93 | boolean keep = false; 94 | try { 95 | // 执行IdleHandler的queueIdle,返回值表示是否要保留IdleHandler 96 | keep = idler.queueIdle(); 97 | } catch (Throwable t) { 98 | Log.wtf(TAG, "IdleHandler threw exception", t); 99 | } 100 | 101 | if (!keep) { 102 | synchronized (this) { 103 | // 不保留则从集合中移除IdleHandler 104 | mIdleHandlers.remove(idler); 105 | } 106 | } 107 | } 108 | 109 | pendingIdleHandlerCount = 0; 110 | 111 | nextPollTimeoutMillis = 0; 112 | } 113 | } 114 | ``` 115 | 116 | 117 | 118 | - IdleHandler是在MessageQueue中没有可用消息时发生作用的,如果想要在消息处理空闲时做一些事情,就可以在当前线程的消息队列中加入IdleHandler,并重写Idle的queueIdle函数。 119 | 120 | - IdleHandler的作用次数可为一次或者多次,取决于queueIdle方法的返回值,如果返回false,那么执行完这个方法后便会从集合中移除,如果返回true,意味着每次只要消息队列空闲就会调用一次queueIdle方法。 121 | 122 | ## 相关面试题 123 | 124 | ### 1. IdleHandler是什么,有什么用? 125 | 126 | - IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的机制; 127 | 128 | - 当 MessageQueue 当前没有立即需要处理的消息时(消息队列为空,或者消息未到执行时间),会执行 IdleHandler; 129 | 130 | ### 2. 如果消息队列一直没有消息,为什么queueIdle方法不会被无限调用(死循环)? 131 | 132 | 当MessageQueue为空时,会循环遍历一遍mIdleHandler,并执行IdleHandler.queueIdle方法。如果某些IdleHandler的queueIdle返回了true,则会被保留在mIdleHandlers中,下次消息队列空闲时依然会执行。 133 | 134 | 在调用next方法的时候pendingIdleHandlerCount被赋值为-1,也就是开始循环前值是-1,在循环中如果pendingIdleHandlerCount小于0时才会通过mIdleHandlers.size给pendingIdleHandlerCount赋值,即只有第一次循环才会改变pendingIdleHandlerCount的值。然后在MessageQueue空闲的时候执行一遍IdleHandler。执行完之后pendingIdleHandlerCount被赋值了0,再次进入循环的时候判断pendingIdleHandlerCount <= 0则直接执行continue跳过了后边的执行逻辑。 135 | 136 | ### 2. MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成对使用? 137 | 138 | 不是必须要成对使用。MessageQueue中会根据IdleHandler中queueIdle方法的返回值来决定是否移除MessageQueue中的IdleHandler。 139 | 140 | ### 3. 是否可以将一些不重要的启动服务移到IdleHandler中处理? 141 | 142 | 不建议这样做,因为IdleHandler的执行时机不可控,如果MessageQueue一直有待处理的消息,那么IdleHandler会一直得不到执行。 143 | 144 | ### 4. IdleHandler 的 queueIdle运行在哪个线程? 145 | 146 | queueIdle() 运行的线程,只和当前 MessageQueue 的 Looper 所在的线程有关;子线程一样可以构造 Looper,并添加 IdleHandler; 147 | 148 | ### 5. 为什么 Activity.finish() 之后 10s 才 onDestroy ? 149 | 150 | **问题描述:** 在A Activity启动B Activity,并结束A 页面,B页面在启动时进行大量的动画场景,源源不断的向主线程消息队列发送消息。A Activity的onPause正常执行,但是onStop与onDestory都延迟了10s才执行。为什么会出现这样的情况? 151 | 152 | Activity 的 onStop/onDestroy 是依赖 IdleHandler 来回调的,正常情况下当主线程空闲时会调用。但是由于某些特殊场景下的问题,导致主线程迟迟无法空闲,onStop/onDestroy 也会迟迟得不到调用。但这并不意味着 Activity 永远得不到回收,系统提供了一个兜底机制,当 onResume 回调 10s 之后,如果仍然没有得到调用,会主动触发。 -------------------------------------------------------------------------------- /post/Instrumentation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhpanvip/AndroidNote/913040ea47149474d078959e648b0855e7bbafbe/post/Instrumentation.md -------------------------------------------------------------------------------- /post/IntentService实现原理.md: -------------------------------------------------------------------------------- 1 | IntentService 是继承于 Service 并处理异步请求的一个类,内部实现是 HandlerThread,处理完子线程的事后自动 stopService。 2 | 3 | ### IntentService的使用 4 | 5 | #### 1.创建IntentService 6 | ``` 7 | public class MyIntentService extends IntentService { 8 | 9 | private static final String TAG = "MyIntentService"; 10 | 11 | public MyIntentService() { 12 | //IntentService 工作线程的名字 13 | super("MyIntentService"); 14 | } 15 | 16 | @Override 17 | public void onCreate() { 18 | super.onCreate(); 19 | Log.i(TAG, "onCreate: "); 20 | } 21 | 22 | @Override 23 | public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 24 | Log.i(TAG, "onStartCommand: "); 25 | return super.onStartCommand(intent, flags, startId); 26 | } 27 | 28 | @Override 29 | public void onDestroy() { 30 | super.onDestroy(); 31 | Log.i(TAG, "onDestroy: "); 32 | } 33 | 34 | @Override 35 | protected void onHandleIntent(@Nullable Intent intent) { 36 | if (intent != null) { 37 | String taskName = intent.getStringExtra("taskName"); 38 | Log.i(TAG, "onHandleIntent: " + taskName); 39 | switch (taskName) { 40 | case "task1": 41 | //任务一 42 | try { 43 | Thread.sleep(3000); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | break; 48 | case "task2": 49 | //任务二 50 | break; 51 | default: 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | ``` 58 | #### 2.Activity中使用 59 | ``` 60 | public void onClick(View v) { 61 | Intent intent = new Intent(this, MyIntentService.class); 62 | switch (v.getId()) { 63 | case R.id.btn1: 64 | intent.putExtra("taskName", "task1"); 65 | startService(intent); 66 | break; 67 | case R.id.btn2: 68 | intent.putExtra("taskName", "task2"); 69 | startService(intent); 70 | break; 71 | default: 72 | break; 73 | } 74 | } 75 | ``` 76 | 这里如果点击按钮一后立马点击按钮二,日志打印如下: 77 | ``` 78 | onCreate、onStartCommand、onHandleIntent: task1、 79 | onStartCommand、onHandleIntent: task2、onDestroy 80 | ``` 81 | 82 | 如果是等三秒后再点击按钮二,就是: 83 | ``` 84 | onCreate、onStartCommand、onHandleIntent: task1、onDestroy、 85 | onCreate、onStartCommand、onHandleIntent: task2、onDestroy 86 | ``` 87 | 88 | ### 源码分析 89 | ``` 90 | public abstract class IntentService extends Service { 91 | private volatile Looper mServiceLooper; 92 | private volatile ServiceHandler mServiceHandler; 93 | private String mName; 94 | private boolean mRedelivery; 95 | 96 | private final class ServiceHandler extends Handler { 97 | public ServiceHandler(Looper looper) { 98 | super(looper); 99 | } 100 | 101 | @Override 102 | public void handleMessage(Message msg) { 103 | //重写的方法,子线程需要做的事情 104 | onHandleIntent((Intent)msg.obj); 105 | //做完事,自动停止 106 | stopSelf(msg.arg1); 107 | } 108 | } 109 | 110 | public IntentService(String name) { 111 | super(); 112 | //IntentService 的线程名 113 | mName = name; 114 | } 115 | 116 | public void setIntentRedelivery(boolean enabled) { 117 | mRedelivery = enabled; 118 | } 119 | 120 | @Override 121 | public void onCreate() { 122 | super.onCreate(); 123 | HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); 124 | thread.start(); 125 | 126 | //构造子线程 Handler 127 | mServiceLooper = thread.getLooper(); 128 | mServiceHandler = new ServiceHandler(mServiceLooper); 129 | } 130 | 131 | @Override 132 | public void onStart(@Nullable Intent intent, int startId) { 133 | Message msg = mServiceHandler.obtainMessage(); 134 | msg.arg1 = startId; 135 | msg.obj = intent; 136 | //在 Service 启动的时候发送消息,子线程开始工作 137 | mServiceHandler.sendMessage(msg); 138 | } 139 | 140 | @Override 141 | public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 142 | //调用上面的那个方法,促使子线程开始工作 143 | onStart(intent, startId); 144 | return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; 145 | } 146 | 147 | @Override 148 | public void onDestroy() { 149 | mServiceLooper.quit(); 150 | } 151 | 152 | @Override 153 | @Nullable 154 | public IBinder onBind(Intent intent) { 155 | return null; 156 | } 157 | 158 | @WorkerThread 159 | protected abstract void onHandleIntent(@Nullable Intent intent); 160 | } 161 | ``` 162 | 总结如下: 163 | 164 | - Service onCreate 的时候通过 HandlerThread 构建子线程的 Handler 165 | - Service onStartCommand 中通过子线程 Handler 发送消息 166 | - 子线程 handlerMessage 中调用我们重写的 onHandlerIntent 执行异步任务,执行完之后 Service 销毁 167 | 168 | https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/IntentService.md -------------------------------------------------------------------------------- /post/JMM与volatile关键字.md: -------------------------------------------------------------------------------- 1 | ## 现代计算机内存模型 2 | 现代计算机CPU执行指令的速断远远超出了内存的存储速度,计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以现代计算机系统多不得不加入一层或多层读写速度读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要的数据复制到高速缓存中,让运算能快速进行,运算结束后再从高速缓存同步回内存,这样处理器就无需等待缓慢的内存的读写了。 3 | 4 | 基于高速缓存的存储交互很好的解决了处理器与内存速度之间的矛盾,但也引入了一个新的问题:缓存的一致性。在多路处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主内存。当多个处理器的运算任务都涉及到同一块主内存区域的时候,将导致各自的缓存数据不一致的情况出现。为了解决这一问题,需要各个处理器访问缓存的时候都遵循一些协议(例如MSI、MESI、MOSI等),在读写的时候都根据协议来操作。而所谓的“内存模型”就可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的物理机器可以拥有不一样的内存模型,Java虚拟机也有自己的内存模型。 5 | 6 | ![操作系统内存模型](https://user-gold-cdn.xitu.io/2020/4/29/171c47561b7af1b1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 7 | 8 | ## Java虚拟机内存模型(JMM) 9 | Java内存模型(以下简称JMM)用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的缓存访问效果。JMM定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。 10 | 11 | JMM规定了所有的变量都存储在主内存中(这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程是有的,因此不存在竞争问题)。每条线程还有自己的工作内存,线程的工作内存中保存了被线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量。线程变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下图: 12 | 13 | ![Java内存模型](https://user-gold-cdn.xitu.io/2020/4/29/171c47561d31ab88?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 14 | 15 | 与计算机的内存模型类似,Java内存模型同样也会带来缓存一致性问题,即假如一个线程通过自己的工作内存修改了主内存中的变量,那么对于另外一个线程由于有自己的工作内存,且没能立即同步主内存中的数据,就造成了数据数据错误的问题。即所谓的**可见性**问题。 16 | 17 | > 可见性指的是一个线程对共享变量的写操作对其它线程后续的读操作可见,即当一个线程修改了共享变量后,其他线程能够立即得知这个修改。 18 | 19 | ## volatile关键字 20 | 21 | volatile关键字是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义为volatile之后,它具备两项特性: 22 | - 1.保证此变量对所有线程的可见性。 23 | - 2.禁止指令重排序优化。 24 | 25 | 另外,要注意的是volatile并不能保证原子性。 26 | 27 | 那volatile是如何保证共享变量的可见性的呢? 28 | 29 | 在计算机的内存模型中我们提到,为了解决缓存一致性问题,需要各个处理器访问缓存的时候都遵循一些协议。而JVM解决缓存一致性问题也是如此。 30 | 31 | 以Intel的MESI为例,当CPU写数据时,如果发现操作的是共享变量,会发出信号通知其他CPU将该变量的缓存行置为无效,当其他CPU需要读取这个变量时,发现自己缓存中的该变量的缓存行是无效的,那么就会从内存重新读取。 32 | 33 | 由于volatile的MESI缓存一致性协议需要不断的从主内存休干和CAS不断循环,无效交互会导致总线宽带达到峰值。因此不要大量使用volatile关键字。 34 | 35 | ### 指令重排序 36 | 为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。一般重排序可以分为如下三种: 37 | - 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; 38 | - 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序; 39 | - 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。 40 | 41 | 42 | 43 | 44 | 45 | 46 | [面试官没想到一个Volatile,我都能跟他扯半小时](https://juejin.cn/post/6844904149536997384) -------------------------------------------------------------------------------- /post/JVM.md: -------------------------------------------------------------------------------- 1 | 2 | ### JVM的内存分配 3 | [深入JVM--Java运行时内存区域详解](https://zhpanvip.gitee.io/2020/09/04/26.JVM%20memory/) 4 | 5 | ### Java的垃圾回收机制 6 | [深入JVM--垃圾回收机制全面解析 |](https://zhpanvip.gitee.io/2020/09/19/29.Java%20GC/) 7 | 8 | ### JVM类加载的过程 9 | 10 | [深入JVM--探索Java虚拟机的类加载机制 |](https://zhpanvip.gitee.io/2020/12/25/33.jvm-class-load/) -------------------------------------------------------------------------------- /post/Java线程中断机制.md: -------------------------------------------------------------------------------- 1 | ## Java线程中断机制 2 | 3 | 线程中断即线程运行过程中被其他线程给打断了,线程中断相关的方法有: 4 | 5 | - 1、java.lang.Thread#interrupt 6 | 7 | 中断目标线程,给目标线程发一个中断信号,线程被打上中断标记。 8 | 9 | - 2.java.lang.Thread#isInterrupted() 10 | 11 | 判断目标线程是否被中断,不会清除中断标记。 12 | 13 | - 3.java.lang.Thread#interrupted 14 | Thread中的一个静态方法,判断目标线程是否被中断,如果是被标记了中断状态,会清除中断标记。 15 | 16 | interrupt方法中断线程仅仅是设置了一个中断标记,如果在线程run方法中不去相应中断,则中断标记不会对线程产生任何影响。 17 | 18 | 如下代码,thread线程并不会被中断 19 | 20 | ```java 21 | public static void testInterrupt() { 22 | Thread thread = new Thread(new Runnable() { 23 | @Override 24 | public void run() { 25 | while (true) { 26 | System.out.println("thread " + Thread.currentThread().getName() + " is running"); 27 | } 28 | } 29 | }); 30 | thread.start(); 31 | thread.interrupt(); 32 | } 33 | ``` 34 | 如果要中断线程,则需要判断interrupt标记位,然后结束线程 35 | 36 | ```java 37 | public static void testInterrupt() { 38 | Thread thread = new Thread(new Runnable() { 39 | @Override 40 | public void run() { 41 | while (true) { 42 | if (Thread.currentThread().isInterrupted()) { 43 | System.out.println("thread " + Thread.currentThread().getName() + " is interrupted"); 44 | return; 45 | } 46 | System.out.println("thread " + Thread.currentThread().getName() + " is running"); 47 | } 48 | } 49 | }); 50 | thread.start(); 51 | thread.interrupt(); 52 | } 53 | ``` 54 | 当让线程进入休眠状态后,中断标记失效,代码如下: 55 | 56 | ```java 57 | public static void testInterrupt2() throws InterruptedException { 58 | Thread thread = new Thread(new Runnable() { 59 | @Override 60 | public void run() { 61 | while (true) { 62 | if (Thread.currentThread().isInterrupted()) { 63 | System.out.println("thread " + Thread.currentThread().getName() + " is interrupted"); 64 | return; 65 | } 66 | System.out.println("thread " + Thread.currentThread().getName() + " is running"); 67 | try { 68 | Thread.sleep(3000); 69 | } catch (InterruptedException e) { 70 | e.printStackTrace(); 71 | } 72 | } 73 | } 74 | }); 75 | thread.start(); 76 | Thread.sleep(2000); 77 | thread.interrupt(); 78 | } 79 | ``` 80 | 81 | 输出结果如下: 82 | 83 | ```java 84 | thread Thread-0 is running 85 | java.lang.InterruptedException: sleep interrupted 86 | at java.base/java.lang.Thread.sleep(Native Method) 87 | at thread.InterruptDemo$2.run(InterruptDemo.java:33) 88 | at java.base/java.lang.Thread.run(Thread.java:834) 89 | thread Thread-0 is running 90 | thread Thread-0 is running 91 | thread Thread-0 is running 92 | thread Thread-0 is running 93 | thread Thread-0 is running 94 | thread Thread-0 is running 95 | thread Thread-0 is running 96 | thread Thread-0 is running 97 | ... 98 | 99 | ``` 100 | 101 | 可以看到线程抛出了一个中断异常,之后线程仍然在执行。 102 | 103 | sleep状态的线程在调用interrupt后会抛出InterruptedException,并清除interrupt标记,因此线程会继续运行。如果需要打断sleep状态的线程,需要捕获InterruptedException异常后再次调用interrupt方法。如下: 104 | ```java 105 | public static void testInterrupt2() throws InterruptedException { 106 | Thread thread = new Thread(new Runnable() { 107 | @Override 108 | public void run() { 109 | while (true) { 110 | if (Thread.currentThread().isInterrupted()) { 111 | System.out.println("thread " + Thread.currentThread().getName() + " is interrupted"); 112 | return; 113 | } 114 | System.out.println("thread " + Thread.currentThread().getName() + " is running"); 115 | try { 116 | Thread.sleep(3000); 117 | } catch (InterruptedException e) { 118 | e.printStackTrace(); 119 | Thread.currentThread().interrupt(); 120 | } 121 | } 122 | } 123 | }); 124 | thread.start(); 125 | Thread.sleep(2000); 126 | thread.interrupt(); 127 | } 128 | ``` 129 | 130 | 输出结果如下: 131 | ```java 132 | thread Thread-0 is running 133 | java.lang.InterruptedException: sleep interrupted 134 | at java.base/java.lang.Thread.sleep(Native Method) 135 | at thread.InterruptDemo$2.run(InterruptDemo.java:33) 136 | at java.base/java.lang.Thread.run(Thread.java:834) 137 | thread Thread-0 is interrupted 138 | ``` 139 | -------------------------------------------------------------------------------- /post/Java集合框架.md: -------------------------------------------------------------------------------- 1 | 2 | ---- 3 | ![这里写图片描述](https://img-blog.csdn.net/20180907225150827?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 4 | 5 | 6 | ![这里写图片描述](https://img-blog.csdn.net/20180907225012225?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 7 | 8 | 9 | ## 2.为什么HashMap在多线程并发存在死循环的问题,JDK1.8中做了哪些优化? 10 | 详情参考 11 | [《我们一起进大厂》系列-HashMap](https://juejin.cn/post/6844904017269637128) 12 | [老生常谈,HashMap的死循环](https://juejin.cn/post/6844903554264596487) 13 | [HashMap为何从头插入改为尾插入](https://juejin.cn/post/6844903682664824845) 14 | 15 | ## 3.Hashtable与HashMap有什么区别? 16 | **1.安全性** 17 | Hashtable是线程安全,HashMap是非线程安全。HashMap的性能会高于Hashtable,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步 18 | 19 | **2.是否可以使用null作为key** 20 | 21 | HashMap可以使用null作为key,不过建议还是尽量避免这样使用。HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key 22 | 23 | **3.继承了什么,实现了什么** 24 | 25 | HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口 26 | 27 | **4.默认容量及如何扩容** 28 | HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩容时是当前容量翻倍即:capacity 2,Hashtable扩容时是容量翻倍+1即:capacity (2+1) 29 | 30 | **5.底层实现** 31 | 32 | HashMap和Hashtable的底层实现都是数组+链表结构实现 33 | 34 | **6.计算hash的方法不同** 35 | 36 | Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模,HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模 37 | 38 | ## 4.了解ConcurrentHashMap吗?它是怎么实现的? 39 | 在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap。 40 | 41 | HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分段技术。它使用了多个锁来控制对hash表的不同部分进行的修改。对于JDK1.7版本的实现, ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。 42 | 43 | **实现原理** 44 | 45 | 对于JDK1.7版本的实现,ConcurrentHashMap 为了提高本身的并发能力,在内部采用了一个叫做 Segment 的结构,一个 Segment 其实就是一个类 Hash Table 的结构,Segment 内部维护了一个链表数组,我们用下面这一幅图来看下 ConcurrentHashMap 的内部结构,从下面的结构我们可以了解到,ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是 Hash 的过程要比普通的 HashMap 要长,但是带来的好处是写操作的时候可以只对元素所在的 Segment 进行操作即可,不会影响到其他的 Segment,这样,在最理想的情况下,ConcurrentHashMap 可以最高同时支持 Segment 数量大小的写操作(刚好这些写操作都非常平均地分布在所有的 Segment上),所以,通过这一种结构,ConcurrentHashMap 的并发能力可以大大的提高。我们用下面这一幅图来看下ConcurrentHashMap的内部结构详情图,如下: 46 | 47 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/571961bfa9ba57846aaeb457326c138d.png#pic_center) 48 | 49 | 为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使concurrentHashmap。 50 | 51 | JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化. 52 | 53 | 参考:[一文读懂Java ConcurrentHashMap原理与实现](https://zhuanlan.zhihu.com/p/104515829) 54 | 55 | ## 5.可以使用CocurrentHashMap来代替Hashtable吗? 56 | 我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 57 | 58 | ## 6.ConcurrentHashMap有什么缺陷吗? 59 | ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。 60 | 61 | ## 7.ConcurrentHashMap在JDK 7和8之间的区别 62 | 63 | JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点) 64 | 65 | JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了 66 | 67 | JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档 68 | ## 9.Java中HashMap和HashTable的区别? 69 | 70 | **相同点** 71 | 72 | HashMap 和 HashTable 都是基于哈希表实现的,其内部每个元素都是 key-value 键值对,HashMap 和 HashTable 都实现了 Map、Cloneable、Serializable 接口。 73 | 74 | **不同点** 75 | 76 | - **(1)继承的父类不同** 77 | 78 | HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口 79 | - **(2) 线程安全性不同** 80 | 81 | Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步 82 | HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。具体的原因在下一篇文章中会详细进行分析。使用HashMap时就必须要自己增加同步处理,虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。 83 | 84 | - **(3)空值不同** 85 | 86 | - HashMap 允许空的 key 和 value 值,HashTable 不允许空的 key 和 value 值。HashMap 会把 Null key 当做普通的 key 对待。不允许 null key 重复。 87 | 88 | - **(4) 初始容量大小和每次扩充容量大小的不同** 89 | 90 | Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。 91 | 92 | ## 10.HashMap 和 HashSet 的区别 93 | 94 | HashSet 继承于 AbstractSet 接口,实现了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不允许集合中出现重复的值。HashSet 其实就是用HashMap来实现的,所有对 HashSet 的操作其实就是对 HashMap 的操作。所以 HashSet 也不保证集合的顺序,也不是线程安全的容器。 95 | 96 | ## 11.请说出 ArrayList和LinkedList的区别? 97 | 1)ArrayList和LinkedList可想从名字分析,它们一个是Array(动态数组)的数据结构,一个是Link(链表)的数据结构,此外,它们两个都是对List接口的实现。 98 | 前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列 99 | 100 | 2)当随机访问List时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。 101 | 102 | 3)当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。 103 | 104 | 4)从利用效率来看,ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。 105 | 106 | 5)ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。 107 | 108 | ## 12.Java 中 Set 与 List 有什么不同? 109 | List,Set都是继承自Collection接口 110 | 111 | List特点:元素有放入顺序,元素可重复 , 112 | 113 | Set特点:元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的) 114 | 115 | List接口有三个实现类:LinkedList,ArrayList,Vector , 116 | 117 | Set接口有两个实现类:HashSet(底层由HashMap实现),LinkedHashSet 118 | 119 | ## 13.请说出 ArrayList和Vector的区别? 120 | 121 | 1)不同的在于序列化方面:ArrayList比Vector安全 122 | 123 | 在ArrayList集合中: 124 | 125 | //采用elementData数组来存储集合元素 126 | private transient Object[] elementData; 127 | 128 | 在Vector集中中: 129 | 130 | //采用elementData数组来保存集合元素 131 | private Object[] elementData; 132 | 133 | 从源码可以看出,ArrayList提供的writeObject和readObject方法来实现定制序列化,而Vector只是提供了writeObject方法,并没有完全实现定制序列化。 134 | 135 | 2)不同点在于Vector是线性安全的,ArrayList是非线性安全的 136 | 137 | ArrayList和Vector的绝大部分方法都是一样的,甚至连方法名都一样,只是Vector的方法大都添加关键之synchronized修饰。 138 | 139 | 在add方法中,Vector电泳的是insertElementAt(element,index); 140 | 141 | public synchronized void isnertElementAt(E obj,int index); 142 | 143 | 将 ArrayList中的add(int index,E element)方法和Vector的isnertElementAt(E Obj,int index)方法进行对比,可以发现vectorde insertElementAt(E obj,int index)方法只是多了synchronized修饰。 144 | 145 | 3)扩容上区别 146 | 147 | ArrayList集合和Vector集合底层都是数组实现的,在数组容量不足的时候采取的扩容机制不同。 148 | 149 | ArrayList集合容量不足,采取在原有容量基础上扩充为原来的1.5倍。 150 | 151 | 而Vector则多了一个选择:当capacityIncrement实例变量大于0时,扩充为原有容量加上capacityIncrement的容量值。否则采取在原有容量基础上扩充为原来的1.5倍。 152 | 153 | ## 14.说出ArrayList,Vector, LinkedList的存储性能和特性 154 | 155 | ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索 引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程 安全),通常性能上较ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数 据时只需要记录本项的前后项即可,所以插入速度较快。 156 | 157 | ## 15.Collection 和 Collections的区别? 158 | 159 | Collection是集合类的上级接口,继承与他的接口主要有Set 和List。 160 | 161 | Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。 -------------------------------------------------------------------------------- /post/LeakCanary 实现原理.md: -------------------------------------------------------------------------------- 1 | 2 | ## 1. ReferenceQueue 3 | 4 | ReferenceQueue是一个链表结构的存储队列,节点是reference,节点之间通过next连接。ReferenceQueue的意义在于能够在外部对queue中的引用进行监控。当引用中的对象没回收后可以对引用对象本身继续做一些清理操作。使用ReferenceQueue结合弱引用(或者软引用)可以监测弱引用中的对象是否被回收。如果弱引用中的对象被回收,那么弱引用会被加入到这个关联的ReferenceQueue中。 5 | 6 | 7 | 8 | ```Java 9 | // 引用队列 10 | ReferenceQueue queue = new ReferenceQueue<>(); 11 | Object obj = new Object() 12 | // 将queue与WeakReference关联 13 | WeakReference weakRef = new WeakReference(obj,queue); 14 | obj = null; 15 | // 调用GC进行垃圾回收 16 | System.gc(); 17 | // 从队列中取出 reference 18 | Reference reference = queue.remove(); 19 | if (reference != null){ 20 | System.out.println("对象已被回收: "+ reference.get()); // 对象为null 21 | } 22 | ``` 23 | 24 | 上述代码中将ReferenceQueue与WeakReference关联,如果WeakReference中的obj对象被回收了,那么WeakReference就会被加入到这个ReferenceQueue中。 25 | 26 | 因此,可以使用该方法来检测对象是否被回收。 27 | 28 | 29 | 30 | ## 2.LeakCanary原理 31 | 32 | LeakCanary的核心原理即使用ReferenceQueue对Activity进行监测。当Activity执行完onDestory后,就将Activity放入到WeakReference中。然后将这个WeakReference类型的Activity与ReferenceQueque关联,此时查看ReferenceQueque中是否有这个WeakReference对象。如果没有,则执行GC,再次查看,如果还没有则证明这个Activity发生了内存泄漏。然后用HaHa这个开源库去分析dump之后的heap内存。 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /post/Lifecycle实现原理.md: -------------------------------------------------------------------------------- 1 | Lifecycle实现原理 -------------------------------------------------------------------------------- /post/MVC、MVP与MVVM.md: -------------------------------------------------------------------------------- 1 | ## 一、MVC (Model View Controller) 2 | 3 | MVC要解决的问题是控制层、数据处理层和界面交互进行解耦。 4 | 5 | - **Model:** 负责数据的处理和加载 6 | - **View:** 负责界面的展示 7 | - **Controller:** 负责逻辑控制。 8 | 9 | MVC的架构即通过 Controller 的控制操作 Model 层的数据,并返回给 View 展示。其关系如下图: 10 | 11 | ![](https://pic1.zhimg.com/80/v2-9d1b8b206bc3b782bb5dbb103bbb73e4_720w.jpg) 12 | 13 | 用户触发View层事件通知 Controller 层,Controller 层通过访问服务器或者数据库,然后通知Model更新数据,Model层更新数据后会将数据通知给View层去展示。 14 | 15 | ### 1. MVC 的优点 16 | 17 | - 结构清晰,职责划分清晰 18 | - 降低耦合 19 | - 有利于组件重用 20 | 21 | ### 2. MVC的缺点 22 | 23 | - Android中 Activity/Fragment 承担了View 和Controller两个角色,导致Activity/Fragment中代码庞大。 24 | 25 | - View 层与 Model 层存在依赖关系,Model层直接操作View,View的修改会导致Controller和Model都需要改动 26 | 27 | 28 | 29 | ## 二、MVP (Model View Presenter) 30 | 31 | MVP要解决的问题与MVC大同小异,即控制逻辑,数据处理逻辑以及界面交互解耦,同时,将MVC中的View和Model解耦。 32 | 33 | MVP 架构里,将逻辑,数据,界面的处理划分为三个部分,模型(Model)-视图(View)-控制器(Presenter)。各个部分的功能如下: 34 | 35 | - **Model模型:** 负责数据的处理和加载 36 | - **View视图:** 负责界面的展示 37 | - **Presenter控制器:** 负责逻辑控制 38 | 39 | MVP和MVC最大的不同就是View层和Model层不互相持有,都通过Presenter交互。View产生事件通知Presenter,Presenter中进行逻辑处理后通知Model更新数据,Model更新数据后通知数据给Presenter,Presenter再通知View更新界面。示意图如下: 40 | 41 | ![](https://pic1.zhimg.com/80/v2-cdfc6c60e8be1a3b8caa7fe0697a2e0c_720w.jpg) 42 | 43 | 44 | 45 | ### 1. MVP 的优点 46 | 47 | - 结构清晰,职责划分明确 48 | - 模块间充分解耦 49 | - 有利于组件的重用 50 | 51 | ### 2. MVP 的缺点 52 | 53 | - 会引入大量的接口,导致项目文件数量激增 54 | - 增大代码结构的复杂性 55 | 56 | ### MVVM (Model View ViewModel) 57 | 58 | MVVM要解决的问题是将控制逻辑、数据处理逻辑以及界面交互进行解耦,并且能将MVC中的View和Model解耦,还可以把MVP中的Presenter和View也解耦。 59 | 60 | MVVM架构中,将逻辑、数据、界面的处理分为三部分,即模型(Model)、视图(View)以及逻辑(ViewModel)。各个部分的功能如下: 61 | 62 | - **Model模型**:负责数据的加载和存储 63 | - **View视图**:负责界面的展示 64 | - **ViewModel控制器**:负责逻辑控制 65 | 66 | 在 MVP 中,就是 View 和 Model 不相互持有,都通过 Presenter 做中转。这样可以使 View 和 Model 解耦。而在MVVM中解耦做的更彻底,ViewModel也不会持有View,其中ViewModel中的改动会自动反馈给View进行界面更新,而View的事件也会自动反馈给ViewModel。 67 | 68 | ![](https://pic4.zhimg.com/80/v2-f6f7c0e53f42e7d3291c7784b1b2d157_720w.jpg) 69 | 70 | 要达到这个效果,需要一些辅助工具,比较常用的是DataBinding。在MVVM中,数据流是由View产生事件,自动通知给ViewModel,ViewModel进行逻辑处理后通知Model更新数据。Model更新数据后,通知数据给ViewModel,ViewModel自动通知View更新界面。 71 | 72 | 73 | 74 | https://zhuanlan.zhihu.com/p/83635530 75 | 76 | -------------------------------------------------------------------------------- /post/PMS安装与签名校验.md: -------------------------------------------------------------------------------- 1 | ## PMS安装与签名校验 -------------------------------------------------------------------------------- /post/RecyclerView实现原理.md: -------------------------------------------------------------------------------- 1 | ## RecyclerView 的缓存机制 2 | 3 | 4 | `Recycler`有4个层次用于缓存`ViewHolder`对象,优先级从高到底依次为`ArrayList mAttachedScrap`、`ArrayList mCachedViews`、`ViewCacheExtension mViewCacheExtension`、`RecycledViewPool mRecyclerPool`。如果四层缓存都未命中,则重新创建并绑定`ViewHolder`对象 5 | 6 | **缓存性能:** 7 | 8 | | 缓存 | 重新创建`ViewHolder` | 重新绑定数据 | 9 | | -------------- | -------------------- | ------------ | 10 | | mAttachedScrap | false | false | 11 | | mCachedViews | false | false | 12 | | mRecyclerPool | false | true | 13 | 14 | **缓存容量:** 15 | 16 | - `mAttachedScrap`:没有大小限制,但最多包含屏幕可见表项。 17 | - `mCachedViews`:默认大小限制为2,放不下时,按照先进先出原则将最先进入的`ViewHolder`存入回收池以腾出空间。 18 | - `mRecyclerPool`:对`ViewHolder`按`viewType`分类存储(通过`SparseArray`),同类`ViewHolder`存储在默认大小为5的`ArrayList`中。 19 | 20 | 21 | **缓存用途:** 22 | 23 | - `mAttachedScrap`:用于布局过程中屏幕可见表项的回收和复用。 24 | - `mCachedViews`:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到`mCachedViews`,当它放不下的时候,按照先进先出原则将最先进入的`ViewHolder`存入回收池。 25 | - `mRecyclerPool`:用于移出屏幕表项的回收和复用,且只能用于指定`viewType`的表项 26 | 27 | 1. **缓存结构:** 28 | 29 | - `mAttachedScrap`:`ArrayList` 30 | - `mCachedViews`:`ArrayList` 31 | - `mRecyclerPool`:对`ViewHolder`按`viewType`分类存储在`SparseArray`中,同类`ViewHolder`存储在`ScrapData`中的`ArrayList`中 32 | 33 | 34 | 35 | https://juejin.cn/post/6844903780006264845 36 | 37 | -------------------------------------------------------------------------------- /post/RxJava中的线程池.md: -------------------------------------------------------------------------------- 1 | Rxjava中可以通过subscribeOn和observeOn来切换被观察者与观察者运行的线程 2 | 3 | ```java 4 | Observable.create(new ObservableOnSubscribe() { 5 | @Override public void subscribe(ObservableEmitter emitter) 6 | throws Exception { 7 | emitter.onNext(1); 8 | } 9 | }).subscribeOn(Schedulers.io()) // 指定被观察者运行在IO线程,适用于IO密集型 10 | .observeOn(AndroidSchedulers.mainThread()) // 指定观察者运行在子线程,需要依赖RxAndroid 11 | .subscribe(new Observer() { 12 | ... 13 | }); 14 | ``` 15 | 16 | 17 | 18 | ## 一、RxJava中的线程池 19 | 20 | ### 1. IO密集型线程池 21 | 22 | subscribeOn方法中接收一个通过Schedulers.io()方法得到的Scheduler实例,其源码如下: 23 | 24 | ```java 25 | public final class Schedulers { 26 | static final Scheduler IO; 27 | public static Scheduler io() { 28 | return RxJavaPlugins.onIoScheduler(IO); 29 | } 30 | 31 | static { 32 | // 实例化一个线程的xian'cheng'c 33 | SINGLE = RxJavaPlugins.initSingleScheduler(new SingleTask()); 34 | // CPU密集型 35 | COMPUTATION = RxJavaPlugins.initComputationScheduler(new ComputationTask()); 36 | // IO 密集型线程池 37 | IO = RxJavaPlugins.initIoScheduler(new IOTask()); 38 | 39 | TRAMPOLINE = TrampolineScheduler.instance(); 40 | // 实例化新线程, 41 | NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask()); 42 | } 43 | 44 | // ... 45 | } 46 | ``` 47 | 48 | 这个方法中通过RxJavaPlugins.onIoScheduler(IO)返回了一个Scheduler实例,这个实例其实就是Schedulers的成员变量IO。代码中可以看到IO是一个静态的Scheduler成员,并且在Schedulers静态代码块中通过 ` RxJavaPlugins.initIoScheduler(new IOTask())` 来初始化Scheduler。这个方法接受一个IOTask参数,IOTask源码如下: 49 | 50 | ```java 51 | // Schedulers.java 52 | 53 | static final class IOTask implements Callable { 54 | @Override 55 | public Scheduler call() throws Exception { 56 | return IoHolder.DEFAULT; 57 | } 58 | } 59 | 60 | static final class IoHolder { 61 | static final Scheduler DEFAULT = new IoScheduler(); 62 | } 63 | ``` 64 | 65 | 可以看到这里实例化了一个IoScheduler,IoScheduler继承自Scheduler,这个类中封装了一个适用于IO密集型的java线程池。先看下它的构造方法 66 | 67 | ```java 68 | static final RxThreadFactory WORKER_THREAD_FACTORY; 69 | 70 | static { 71 | // 线程工厂 72 | WORKER_THREAD_FACTORY = new RxThreadFactory(WORKER_THREAD_NAME_PREFIX, priority); 73 | // ... 74 | } 75 | 76 | public IoScheduler() { 77 | // 传入默认的线程工厂 78 | this(WORKER_THREAD_FACTORY); 79 | } 80 | 81 | public IoScheduler(ThreadFactory threadFactory) { 82 | this.threadFactory = threadFactory; 83 | this.pool = new AtomicReference(NONE); 84 | // 调用start 85 | start(); 86 | } 87 | ``` 88 | 89 | start方法中初始化了线程池,不过是通过CachedWorkerPool进行初始化的,线程池被封装到了这个CachedWorkerPool中。 90 | 91 | ```java 92 | public void start() { 93 | // KEEP_ALIVE_TIME默认60s,即空闲线程的存活时间是60s 94 | CachedWorkerPool update = new CachedWorkerPool(KEEP_ALIVE_TIME, KEEP_ALIVE_UNIT, threadFactory); 95 | if (!pool.compareAndSet(NONE, update)) { 96 | update.shutdown(); 97 | } 98 | } 99 | ``` 100 | 101 | 接着看CachedWorkerPool的构造方法 102 | 103 | ```java 104 | CachedWorkerPool(long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory) { 105 | this.keepAliveTime = unit != null ? unit.toNanos(keepAliveTime) : 0L; 106 | this.expiringWorkerQueue = new ConcurrentLinkedQueue(); 107 | this.allWorkers = new CompositeDisposable(); 108 | this.threadFactory = threadFactory; 109 | 110 | ScheduledExecutorService evictor = null; 111 | Future task = null; 112 | if (unit != null) { 113 | // 通过Executors工具进行初始化线程池,核心线程数是1, 114 | evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY); 115 | // 设置空闲线程存活时间 116 | task = evictor.scheduleWithFixedDelay(this, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS); 117 | } 118 | evictorService = evictor; 119 | evictorTask = task; 120 | } 121 | ``` 122 | 123 | 了解过线程池的话应该知道Executors这个线程池工具类,它可以方便快捷的创建线程池。看下它newScheduledThreadPool方法的源码: 124 | 125 | ```java 126 | // Executors.java 127 | public static ScheduledExecutorService newScheduledThreadPool( 128 | int corePoolSize, ThreadFactory threadFactory) { 129 | // 调用线程池构造方法,传入核心线程数为1 130 | return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); 131 | } 132 | ``` 133 | 134 | 135 | 136 | ```java 137 | // ScheduledThreadPoolExecutor.java 138 | public ScheduledThreadPoolExecutor(int corePoolSize, 139 | ThreadFactory threadFactory) { 140 | // 调用自身构造方法,传入最大线程数为最大Int值,阻塞队列是DelayedWorkQueue 141 | super(corePoolSize, Integer.MAX_VALUE, 142 | DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, 143 | new DelayedWorkQueue(), threadFactory); 144 | } 145 | ``` 146 | 147 | 最终可以看到,线程池配置的参数为: 148 | 149 | > 核心线程数是1,最大线程数是Integer.MAX_VALUE,阻塞队列为DelayedWorkQueue。 150 | 151 | DelayedWorkQueue是一个高度定制化的延迟阻塞队列,它的核心数据结构是二叉最小堆的优先队列。队列满时会自动扩容,所以offer方法用于不会被阻塞,这也意味着maximumPoolSize其实并没有用,线程池中始终会保持最多corePoolSize个线程运行。 152 | 153 | 从这个线程池配置的参数可以看得出来,使用Schedulers.IO创建的线程池,会始终保持只有1个核心线程在运行,所有来不及执行的任务都会被保存到DelayedWorkQueue中。 154 | 155 | ### 2. CPU 密集型线程池 156 | 157 | Scheduler中除了提供 io 方法创建IO密集型线程池外还提供了computation方法来创建一个CPU密集型的线程池,如下: 158 | 159 | ```java 160 | public static Scheduler computation() { 161 | return RxJavaPlugins.onComputationScheduler(COMPUTATION); 162 | } 163 | ``` 164 | 165 | 它的创建过程与上一节IO的创建是一致的,过程就不再分析,主要看一下这里创建线程池的参数 166 | 167 | ComputationScheduler中start方法如下: 168 | 169 | ```java 170 | public void start() { 171 | 172 | FixedSchedulerPool update = new FixedSchedulerPool(MAX_THREADS, threadFactory); 173 | if (!pool.compareAndSet(NONE, update)) { 174 | update.shutdown(); 175 | } 176 | } 177 | ``` 178 | 179 | 而MAX_THREADS是通过` cap(Runtime.getRuntime().availableProcessors(), Integer.getInteger(KEY_MAX_THREADS, 0));` 计算得到的,availableProcessors方法即计算虚拟机可用的核心数。 180 | 181 | FixedSchedulerPool的构造方法如下: 182 | 183 | ```java 184 | FixedSchedulerPool(int maxThreads, ThreadFactory threadFactory) { 185 | // initialize event loops 186 | this.cores = maxThreads; 187 | this.eventLoops = new PoolWorker[maxThreads]; 188 | for (int i = 0; i < maxThreads; i++) { 189 | this.eventLoops[i] = new PoolWorker(threadFactory); 190 | } 191 | } 192 | ``` 193 | 194 | 先实例化了一个长度为maxThreads的PoolWorker数组,然后又实例化了maxThreads个PoolWorker实例,PoolWorker是什么东西?其源码如下: 195 | 196 | ```java 197 | static final class PoolWorker extends NewThreadWorker { 198 | PoolWorker(ThreadFactory threadFactory) { 199 | // 调用父类的构造方法 200 | super(threadFactory); 201 | } 202 | } 203 | ``` 204 | 205 | NewThreadWorker的构造方法如下: 206 | 207 | ```java 208 | // NewThreadWorker.java 209 | public NewThreadWorker(ThreadFactory threadFactory) { 210 | executor = SchedulerPoolFactory.create(threadFactory); 211 | } 212 | ``` 213 | 214 | 通过SchedulerPoolFactory创建线程池: 215 | 216 | ```java 217 | // SchedulerPoolFactory.java 218 | public static ScheduledExecutorService create(ThreadFactory factory) { 219 | final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1, factory); 220 | tryPutIntoPool(PURGE_ENABLED, exec); 221 | return exec; 222 | } 223 | ``` 224 | 225 | 通过线程池工具类的newScheduledThreadPool方法实例化线程池,同时,核心线程数还是1,最终调用线程池的构造方法如下: 226 | 227 | ```java 228 | // ScheduledThreadPoolExecutor.java 229 | public ScheduledThreadPoolExecutor(int corePoolSize, 230 | ThreadFactory threadFactory) { 231 | super(corePoolSize, Integer.MAX_VALUE, 232 | DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, 233 | new DelayedWorkQueue(), threadFactory); 234 | } 235 | ``` 236 | 237 | 可以看到依然是核心线程数为1,最大线程数为Integer.MAX_VALUE,阻塞队列为DelayedWorkQueue的线程池。 238 | 239 | 跟IO的区别是,这里创建了多个线程池。。。。 240 | 241 | ## 二、切换线程操作符 242 | 243 | 244 | 245 | ```java 246 | public final Observable subscribeOn(Scheduler scheduler) { 247 | ObjectHelper.requireNonNull(scheduler, "scheduler is null"); 248 | return RxJavaPlugins.onAssembly(new ObservableSubscribeOn(this, scheduler)); 249 | } 250 | ``` 251 | 252 | -------------------------------------------------------------------------------- /post/Socket.md: -------------------------------------------------------------------------------- 1 | ## Socket是什么? 2 | 3 | Socket是一种编程模型,从编程角度来看,客户端数据发送给在客户端侧的Socket对象,然后客户端侧的Socket对象将数据发送给服务端侧的Socket对象。Socket对象负责提供数据通信能力,并处理底层的TCP/UDP 连接。对服务端而言,每一个客户端接入,就会形成一个和客户端对应的Socket对象。如果服务器要读取客户端发送的信息,或者向客户端发送信息,就会需要通过这个客户端Socket对象 4 | 5 | ![Cgp9HWCZ8deAY_UqAAFeGtcsKIg099](https://user-images.githubusercontent.com/19853475/129469380-1155f09f-d2dd-4358-b668-b6f6a4972559.png) 6 | 7 | 8 | 9 | 从另一个角度去分析,Socket还是一种文件,准确的所说是一种双向管道文件。管道文件会将一个程序的输出导向另一个程序的输入。双向管道文件连接的程序是对等的,都可以作为输入输出。 10 | 11 | ```java 12 | var serverSocket = new ServerSocket(); 13 | serverSocket.bind(new InetSocketAddress(80)); 14 | ``` 15 | 16 | 上述代码创建了一个服务端的Socket对象,如果从管道文件的层面可以理解问这是一个文件,它里面存储了所有客户端Socket文件的文件描述符。 17 | 18 | 当一个客户端连接到服务的时候,操作系统就会创建一个客户端Socket文件。然后,操作系统将这个文件的文件描述符写入服务端程序创建的服务端Socket文件中。服务端Socket文件,是一个管道文件。如果读取这个文件的内容,就相当于从管道中取出了一个客户端文件描述符。 19 | 20 | ![Cgp9HWCZ8eSANiNKAAHGwf-mH5U069](https://user-images.githubusercontent.com/19853475/129474991-33a796b7-a22a-4bdf-b912-6f3dd77c63e0.png) 21 | 22 | 如上图所示,服务端Socket文件相当于一个客户端Socekt的目录,线程可以通过accept操作每次拿走一个客户端文件描述符。拿到文件描述符就相当于拿到了和客户端进行通信的接口。 23 | 24 | 当线程想要读取客户端传输来的数据时,就从客户端socket文件中读取数据;当线程想要发送数据到客户端时,就向客户端Socket文件中写入数据。客户端Socket是一个双向管道,操作系统将客户端传来的数据写入管道,也将线程写入管道的数据发送到客户端。 25 | 26 | 既然Socket可以双向传送,那么是两个单向管道拼凑在一起实现的吗?这取决于操作系统。Linux中的管道是单向的。因此Socket文件是一种区别于操作系统管道的单独实现。 27 | 28 | 29 | 30 | **总结一下,Socket首先是文件,存储的是数据。对服务端而言,分成服务端Socket文件和客户端Socket文件。服务端文件存储的是客户端文件描述符;客户端Socket文件存储的是传输数据。读取客户端Socket文件就是读取客户端发来的数据;写入客户端文件就是向客户端发送数据。对一个客户端而言,Socket文件存储的是发送给(或接收)服务端的数据** 31 | 32 | **综上,Socket首先是文件,在文件的基础上又封装了一段程序,这段程序提供了API负责最终的数据传输。** 33 | 34 | 35 | 36 | ## 服务端Socket的绑定 37 | 38 | 为了区别应用,对于一个服务端Socket文件,我们要设置它的监听端口,比如Nginx监听80端口、Node监听3000端口、SSH监听22端口、Tomcat监听8080端口。端口的监听不能重复,不然客户端连接进来创建客户端Socket文件,文件描述符就不知道写入哪个服务端Socket了。这样操作系统就会把连接到不同端口的客户端分类。将客户端Socket文件描述符存到对应不同端口的服务端Socket文件中。 39 | 40 | 41 | 42 | 因此,服务端监听端口的本质是将服务端Socket文件和端口绑定,这个操作也称为bind。有时候不仅仅要绑定端口,还要绑定IP地址。因为有时候我们只允许指定IP访问我们的服务器程序。 43 | 44 | 45 | 46 | ## 扫描和监听 47 | 48 | 49 | 50 | 对于服务端程序,可以定期扫描服务端文件的变更,来了解有哪些客户端想要连接进来。如果在服务端Socket文件中读取杜鳌一个客户端文件描述符,就可以将这个文件描述符实例化成一个Socket对象。 51 | 52 | ![CioPOWCZ8fOAaVwEAAJ4CITeHSs003](https://user-images.githubusercontent.com/19853475/129475595-e3177cc8-82a5-4bc5-b503-7a8bd1080bc6.png) 53 | 54 | 之后,服务端可以将这个Socket对象加入到一个集合,通过定期遍历所有客户端Socket对象,查找背后Socket文件的状态,从而确定是否有新的数据从客户端传输过来。 55 | 56 | ![Cgp9HWCZ8fyAJIK7AAFzaGqyFsw603](https://user-images.githubusercontent.com/19853475/129475625-72673df7-820f-4227-b565-3e23056a4938.png) 57 | 58 | 上述过程通过一个线程就可以响应多个客户端连接,也被称作**I/O多路复用技术** 59 | 60 | 61 | 62 | ## 响应式 63 | 64 | 65 | 66 | 在I/O多路复用技术中,服务端程序需要维护一个Socket的集合,然后定期遍历这个集合。这样的做法在客户端Socket较少的情况下没有问题,但是,如果接入的客户端Socket较多,比如达到上万,每次轮训的开销就会很大。 67 | 68 | 69 | 70 | 从程序设计来看,像这样主动遍历,比如遍历一个Socket集合看看有没有发生写入称为命令式编程。这样的程序设计好像在执行一条条命令一样,程序主动的查看每个Socket的状态。 71 | 72 | 命令式会让负责下命令的程序负载过重。例如,在高并发场景下,上述讨论中循环遍历Socket集合的线程会因为负担过重导致系统吞吐量下降。 73 | 74 | 75 | 76 | 与命令式相反的是响应式。响应式的程序当中,每一个参与者有独立的思考方式。就好像拥有独立的人格,可以自己针对不同的环境出发不同的行为。 77 | 78 | 79 | 80 | 从响应式的角度看Socket编程,应该是有某个观察者会观察到Socket文件状态的变化,从而通知处理线程响应。线程不再需要遍历Socket集合,而是等待观察者程序的通知。 81 | 82 | 而最合适的观察者其实是操作系统本身,因为只有操作系统非常清楚每一个Socket文件的状态。原因是对Socket文件读写都要经过操作系统。在实现这个模型的时候,有几件事要注意。 83 | 84 | 1. 线程需要高速中间的观察者自己要观察什么,或者说在什么情况下响应。比如具体到哪个Socket发生什么变化,是读写还是其他事件,这一步称为**注册** 85 | 2. 中间的观察者需要实现一个高效的数据结构(通常是基于红黑树的二叉搜索树)。这是因为中间观察者不仅仅是服务某个线程,而是服务与很多线程。当一个Socket文件发生变化时,中间观察者要立刻知道究竟是哪个线程需要这个信息,而不是将所有线程都遍历一遍。 86 | 87 | 88 | 89 | ## 总结 90 | 91 | 92 | 93 | Socket即是一种编程模型或者说一端程序,同时也是一个文件,一个双向管道文件。Socket API是在Socket文件基础上进行一层封装,而Socket文件是操作系统提供支持的一种文件格式。 94 | 95 | 96 | 97 | 在服务端有两种Socket文件,每个客户端接入后会生成一个客户端Socket文件,客户端文件的文件描述符会存入服务端Socket文件。通过这种方式,一个线程可以通过读取服务端Socket文件中的内容拿到所有客户端Socket。这样一个线程就可以负责响应所有客户端的I/O,这个技术称为I/O多路复用 98 | 99 | 100 | 101 | 主动式的I/O多路复用对负责I/O的线程压力过大。因此,通常会涉及一个高线的中间数据结构作为I/O事件的观察者。线程通过订阅事件被动响应,这就是响应式模型。在Socket编程中,最适合提供这种中间数据结构的就是操作系统内核。事实上epoll模型也是在操作系统的内核中提供了红黑树结构。 102 | 103 | 104 | 105 | https://kaiwu.lagou.com/course/courseInfo.htm?courseId=837#/detail/pc?id=7276 -------------------------------------------------------------------------------- /post/TCP与UDP.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 网络层是为主机之间提供逻辑通信,而传输层为应用进程之间提供端到端的逻辑通信。根据应用程序的不同需求,传输层需要有两种不同的传输协议,即面向连接的TCP和无连接的UDP。 4 | 5 | TCP 和 UDP 是今天应用最广泛的传输层协议,无论是应用开发、框架设计选型、做底层和优化,还是定位线上问题,只要碰到网络,就逃不开 TCP 协议相关的知识。 6 | 7 | 传输层需要有复用和分用的功能,即应用层所有的进程都可以通过传输层再传输到网络层,这是复用。传输层从网络层收到数据后必须交付给指明的应用进程,这是分用。主机的通信实际上是主机上的两个进程之间的通信,网络层通过IP地址建立起来两台主机的联系,但并不能建立起两个进程间的联系,因此,给应用层的每个应用进程分配一个明确的标志是非常重要的。这个标志就是协议端口(protocol port number),简称端口。虽然通信的终点是进程,但是只要把传送的报文交到目的主机的某一个合适的端口,剩下的工作就可以由TCP来完成。 8 | 9 | 10 | 在TCP/IP体系中,根据使用的协议是TCP还是UDP,分别称之为TCP报文段(Segment)或UDP用户数据报。UDP传送数据前不需要先建立连接。远程主机收到UDP报文后不需要给出任何确认。TCP则是面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后释放连接。由于TCP要提供可靠的、面向连接的传输服务,因此不可避免的增加了许多开销,如确认、流量控制、计时器以及连接管理等。 11 | 12 | ## UDP协议 13 | S 14 | 用户数据报协议UDP在IP的数据报服务纸上增加了很少的功能,即复用和分用的功能以及差错检测的功能。UDP的主要特点如下: 15 | 16 | 1. UDP是无连接的,即发送前不需要建立连接,因此减少了开销和发送数据之前的延迟。 17 | 2. UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的连接状态 18 | 3. UDP是面向报文的。 19 | 4. UDP没有拥塞控制。网络出现阻塞不会使源主机发送的速率降低,这对某些实时性强的应用是很重要的。 20 | 5. UDP支持一对一、一对多、多对一和多对多的交互通信。 21 | 6. UDP的首部开销小,只有8个字节。 22 | 23 | 24 | 25 | ### UDP的首部格式 26 | 27 | 用户数据报UDP有两个字段:数据字段和首部字段。首部字段有8个字节,由四个四段构成。每个字段长度为两个字节。各个字段的意义如下: 28 | 29 | ![save_share_review_picture_1628950684](https://user-images.githubusercontent.com/19853475/129449313-5296a13a-d1b0-4977-ae33-bd87969af573.jpeg) 30 | 31 | 1. 源端口 在需要对方回信时选用,不需要时可全为0 32 | 2. 目的端口 在终点交付报文时必须用到。 33 | 3. 长度 UDP用户数据报的长度,最小值为8 34 | 4. 校验和 检测UDP用户数据报在传输中是否有错,有错则丢弃。 35 | 36 | ## TCP协议 37 | 38 | ### TCP的主要特点 39 | 40 | 1. TCP是面向连接的传输层协议 41 | 2. 每条TCP连接只能有两个端点,即每条TCP连接都是点对点的。 42 | 3. TCP提供可靠的交付服务 43 | 4. TCP提供全双工通信 44 | 5. 面向字节流。 45 | 46 | ### TCP的连接 47 | 48 | TCP(Transport Control Protocol)是一个传输层协议,提供 Host-To-Host 数据的可靠传输,支持全双工,是一个连接导向的协议。表面上看TCP是主机与主机的通信,但事实上是主机的应用到主机的应用的通信。TCP连接的端点叫做套接字(Socket),通过IP地址拼接端口号即可构成套接字。套接字的表示方法是在点分十进制的IP地址后面写上端口号,中间用冒号隔开,如192.168.4.5:80 49 | 50 | ### 可靠传输的工作原理 51 | 52 | TCP发送的报文段是交给IP层传送的,但是IP层不提供可靠传输,因此TCP必须采用适当的措施让两个传输层之间的通信变得可靠。 53 | 54 | #### 停止等待协议 55 | 56 | 全双工通信的双方即是发送方,也是接收方。此处仅考虑A发送数据,B接收数据并确认发送的情况。传送的数据单元称为分组,停止等待协议是每次发完一个分组就停止发送,等待接收方的确认,收到确认后再发送下一个分组。 57 | 58 | 59 | 在传送过程中避免不了出现错误,接收方在检测出数据出现差错就丢弃分组,并且不会通知A收到的分组有差错。另外,分组在传送过程中也可能丢失,这时接收方什么都不知道。 60 | 61 | 而发送方如果超过了一定的时间没有收到接收方的确认,则会进行超时重传。实现超时重传需要发送完一个分组后设置一个超时计时器,如果收到接收方的确认则移除计时器。 62 | 63 | 等待协议的优点是简单,但缺点是信道利用率太低。为了提高效率,发送方一般采用**流水线传输。** 流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就等待确认。这样使得信道上的数据可以不间断的传送。当使用流水线传输时,需要使用到连续**ARQ协议** 和**滑动窗口协议** 64 | 65 | #### 连续ARQ协议 66 | 67 | ![WechatIMG25](https://user-images.githubusercontent.com/19853475/129452195-cb3df4cf-2de6-48c6-99a8-0b4a93928c50.jpeg) 68 | 69 | 连续ARQ协议规定,发送方没收到一个确认就把发送窗口向前滑动一个分组的位置。如上图,收到了第一个分组的确认就把发送窗口先前移动了一个分组位置。 70 | 71 | 而接收方一般都是采用累计确认的方式,就是说接收方不会对收到的分组逐个发送确认,而是收到几个分组后,按序到达最后一个分组发送确认。 72 | 73 | 累计确认的优点是容易实现,即使丢失也不用重传。但是,缺点是不能向发送方反映出接收方已经正确收到的分组信息,例如,如果发送方发送了前五个分组,而中间第三个分组丢失了。这时接收方只能对前两个分组发送确认,发送方无法知道后面三个分组的下落,只好把后面三个分组都再重传一次。 74 | 75 | #### TCP报文段的首部格式 76 | 77 | TCP传输的数据单元是报文段,一个TCP报文段分为首部和数据两部分,而TCP的全部功能体现在它首部中各个字段的作用。TCP报文首部的前20个字节是固定的,后面有4n字节是根据需要增加的选项。因此,TCP首部最小长度是20字节。如下: 78 | 79 | ![WechatIMG26](https://user-images.githubusercontent.com/19853475/129452638-596e60ba-6ccd-4ce1-9d35-c07861b1fbe6.jpeg) 80 | 81 | 82 | 83 | ## TCP与UDP常见面试题 84 | 85 | ### 什么是连接会话? 86 | 87 | 连接是通信双方的一个约定,目标是让两个在通信的程序之间产生一个默契,保证两个程序都在线,而且尽快的相应对方的请求,这就是连接。设计上连接是一种传输数据的行为,传输之前先建立一个连接,具体的说就是数据收发双方的内存中都建立一个用于维护数据传输状态的对象,比如对方的IP地址和端口号是多少,当前发送了多少数据,传输速度如何等。 88 | 89 | 和连接相关的一个名词叫**会话(Session)。** 会话是应用的行为。比如,微信里和别人聊天创建了一个聊天窗口,这个就是会话,开始Typing,发送的时候就和微信服务器之间建立了一个连接。聊天结束后不关闭微信,那么连接断开,但因为窗口没关,所以会话还在。 90 | 91 | 总的来说,会话是应用层的概念,而连接是传输层的概念。 92 | 93 | 94 | 95 | ### 什么是单工和双工? 96 | 97 | 单工是指在任何一个时刻,数据只能单向发送,即A可以向B发送数据,但B不能向A发送数据。 98 | 99 | 半双工是指数据可以向一个方向传输,也可以反向传输,但是是交替进行的。 100 | 101 | 全双工是指任何时刻数据都可以双向发送。 102 | 103 | TCP与UDP都是双工协议。数据任何时候都可以双向传输。 104 | 105 | 106 | 107 | ### TCP协议与UDP协议的区别 108 | 109 | TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP。 110 | 111 | TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。 112 | UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。 113 | TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用。 114 | 115 | ### TCP协议的三次握手 116 | 117 | TCP有几个基本操作: 118 | 119 | - 如果一个Host主动向另一个Host发起连接,称为SYN(Synchronization),即请求同步 120 | - 如果一个Host主动断开连接,称为FIN(Finish),即请求完成。 121 | - 如果一个Host给另一个Host发送数据,称为PSH(Push),即数据推送。 122 | 123 | 以上三种情况,接收方收到数据后,都需要给对方一个ACK(Acknowledge)相应。请求/响应的模型是可靠性的要求。如果没有响应,发送方可能认为自己需要重发这个请求。 124 | 125 | TCP建立连接的过程(三次握手) 126 | 127 | TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作。 128 | 所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发。 129 | 130 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/d386163fa4b8a84241e693e2084cfcac.png) 131 | 132 | 建立连接的具体流程如下: 133 | 134 | 1. 客户端发送一个SYN消息给服务端,请求与服务端建立连接。 135 | 2. 服务端准备就绪,并向客户端发送一个ACK,确认可以连接。 136 | 3. 服务端发送一个SYN消息给客户端,请求与客户端建立连接。 137 | 4. 客户端准备就绪,并向服务端发送一个ACK,确认可以连接。 138 | 139 | 乍一看,这个连接过程是4步,但其实2,3步是同时发生的,即可以合并成一个ACK+SYN的响应一起发送给客户端。因此实际上双方只进行了三次握手,如下图所示: 140 | 141 | ![CioPOWB-RYSASfPkAAEen4ZR3gw297](https://user-images.githubusercontent.com/19853475/129468667-fc907302-06c2-46f5-84d5-939f8936b9b2.png) 142 | 143 | 144 | ### TCP协议的四次挥手 145 | 146 | 四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。 147 | 148 | 由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。 149 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/afa72d9ea7ed4a5853fc724ce84767be.png) 150 | 151 | 中断连接端可以是客户端,也可以是服务器端。以下假设客户端先进行断开连接请求: 152 | 153 | 1. 客户端请求断开连接,发送一个断开请求FIN。 154 | 2. 服务端收到请求,然后向客户端发送一个ACK,作为客户端断开连接请求的响应。 155 | 3. 服务端经过一个等待,确定服务端的数据已经发送完成,则向客户端发送一个FIN,请求与客户端断开连接。 156 | 4. 客户端收到服务端的FIN后,知道可以关闭连接了,但是可能有自己的事情还没处理,比如客户端发送给服务端没有收到ACK的请求,客户端处理完自己的事情后,会再次发送一个ACK响应服务端的断开请求。 157 | 158 | 断开连接的过程与建立连接不同的是第2,3步不是合并发送的。主要是因为服务端可能还要处理很多问题,比如服务端还有发送出去的消息没有得到ACK,也可能是自身资源要释放。因此,断开连接不能像握手那样将两条消息合并发送。 159 | 160 | ### 为什么TCP的连接需要三次握手,而断开连接需要四次挥手? 161 | 162 | 参考TCP协议的三次握手和四次挥手。 163 | 164 | ### TCP的滑动窗口与流速控制是什么? 165 | 166 | 滑动窗口是TCP协议控制可靠传输的核心,发送方将数据拆包,变成多个分组,然后将数据放入一个拥有滑动窗口的数据以此发出,仍然遵循先进先出的顺序,但是窗口中的分组会一次性发送。窗口中序号最小的分组如果收到ACK,窗口就发生滑动,如果最小序号的分组长时间没有收到ACK,就会触发整个窗口的重新发送。 167 | 168 | 169 | -------------------------------------------------------------------------------- /post/TM项目优化方案.md: -------------------------------------------------------------------------------- 1 | ### 1.面临的问题 2 | 3 | 由于TM平台对于APP性能做出了要求,只有性能符合他们的标准才能在该平台上线。主要要求有一下几个方面: 4 | 5 | - APP包体积不能超过30M 6 | - CPU的最高使用率不能超过70% 7 | - 运行时的内存占用不超过200M 8 | 9 | 上线前项目情况: 10 | 11 | - 包体积大小200+M; 12 | - 在AI录播课页面存在CPU使用率超过70%的情况; 13 | - APP运行占用内存有优化的空间 14 | - 启动速度太慢,APP在debug模式下的启动时间在8s(机器性能差)左右,有大幅的优化空间。 15 | 16 | ### 2.启动优化 17 | 18 | #### (1)检测启动时间 19 | 20 | ##### 查看APP启动时间 21 | 22 | 使用adb命令查看启动时间: 23 | 24 | > adb shell am start -S -W [packageName]/[ packageName. AppstartActivity] 25 | 26 | ``` 27 | Stopping: com.example.app 28 | Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.app/.MainActivity } 29 | Status: ok 30 | LaunchState:COLD 31 | Activity: com.example.app/.MainActivity 32 | ThisTime: 1059 33 | TotalTime: 1059 34 | WaitTime: 1073 35 | Complete 36 | ``` 37 | 38 | LaunchState 为 COLD,代表的是冷启动。ThisTime为最后一个Activity启动耗时, TotalTime 所有Activity启动耗时,WaitTime为AMS启动Activity的总耗时。一般查看得到的TotalTime,即应用的启动时间,包括创建进程 + Application初始化 + Activity初始化到界面显示的过程。 39 | 40 | ##### 查看每个方法执行时间 41 | 42 | - 使用startMethodTracing 43 | 44 | ```java 45 | // 开启方法追踪 46 | Debug.startMethodTracing(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000); 47 | // 停止方法追踪 48 | Debug.stopMethodTracing() 49 | 50 | ``` 51 | 52 | 通过上述方法会在data/data/package下边生成trace文件,记录每个方法的时间,CPU信息。但是对运行时性能有较大影响。 53 | 54 | - 使用startMethodTracingSampling 55 | 56 | ```java 57 | // 开启方法采样追踪 58 | Debug.startMethodTracingSampling(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000); 59 | // 停止方法追踪 60 | Debug.stopMethodTracing(); 61 | ``` 62 | 63 | 相比于Trace Java Methods会记录每个方法的时间、CPU信息,它会在应用的Java代码执行期间频繁捕获应用的调用堆栈,对运行时性能的影响比较小,能够记录更大的数据区域。 64 | 65 | 66 | 67 | #### (2)启动优化方案 68 | 69 | ##### 子线程启动 70 | 71 | 将不紧急的第三方库或者一些初始化工作放入到子线程中启动。例如分享库、录音SDK、推送SDK等第三方库的初始化工作可以放到子线程中执行。另外,在启动时候执行检查APP内存等操作均可放入子线程中执行。 72 | 73 | ##### ARouter的启动优化 74 | 75 | 通过监测,发现项目中ARouter启动时耗时特别长,达到了4秒左右,故将启动优化重点放在ARouter上。 76 | 77 | 研究ARouter源码发现,由于ARouter会在编译期间通过APT生成路由表相关类,在APP启动时候回扫描APP中的所有类,然后找到编译时生成的路由表去做一些初始化的工作。启动耗时主要浪费在了扫描APP下的所有类。 78 | 79 | 通过研究ARouter的源码,发现在ARouter扫描类前有一个hook点,可以通过字节码插装的方式禁止ARouter全盘扫描类文件,然后自行指定要加载的类。这一操作可以通过一个第三方库来实现-- [AutoRegister](https://github.com/luckybilly/AutoRegister)。 80 | 81 | 82 | 83 | ### 3.包体积优化 84 | 85 | (包体积优化)[https://github.com/zhpanvip/AndroidNote/wiki/%E5%8C%85%E4%BD%93%E7%A7%AF%E4%BC%98%E5%8C%96] 86 | 87 | ### 4.CPU优化 88 | 89 | 可以通过Profiler或者shell命令来监控APP运行时CPU的占用率。 90 | 91 | #### CPU优化方向 92 | 93 | ##### (1)UI绘制优化 94 | 95 | 复杂的UI绘制会占用一定的内存,因此是一个优化方向。通过Profiler工具Trace System Call记录页面加载和绘制过程中各个方法的耗时,通过对方高耗时的方法进行分析,排查问题所在,比如是不是布局界面过于复杂,是否有可以懒加载的View。当然,通过这种方法也可以查到页面是不是在主线程中执行了耗时操作。根据自己的情况优化即可。 96 | 97 | ##### (2)AI互动课页面优化 98 | 99 | 该页面是有视频播放器+WebView构成。占用CPU主要是播放视频与WebView的互动,因此客户端能做的只是对于视频播放的优化,webview需要前端人员优化处理。降低视频分辨率,代码上可以降低视频码率、帧率、采样率等优化手段。 100 | 101 | -------------------------------------------------------------------------------- /post/ThreadLoacal的实现原理.md: -------------------------------------------------------------------------------- 1 | ## ThreadLoacal是什么? 2 | 3 | ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,且存储的数据只有在该线程中才能获取的到,其他线程无法获取。 4 | 5 | ### 1.ThreadLocal的基本使用: 6 | ``` 7 | public static void main(String[] args) throws InterruptedException { 8 | 9 | ThreadLocal threadLocal=new ThreadLocal<>(); 10 | 11 | new Thread(){ 12 | @Override 13 | public void run() { 14 | super.run(); 15 | threadLocal.set(true); 16 | System.out.println("在子线程中获得threadLocal中的值:"+threadLocal.get()); 17 | } 18 | }.start(); 19 | Thread.sleep(100); 20 | 21 | System.out.println("在主线程中获得threadLocal中的值:"+threadLocal.get()); 22 | 23 | } 24 | ``` 25 | 输出结果: 26 | 27 | ``` 28 | 在子线程中获得threadLocal中的值:true 29 | 在主线程中获得threadLocal中的值:null 30 | ``` 31 | 32 | ## ThreadLocal的实现原理 33 | 34 | ThreadLocal是一个泛型类。它的重点是有一个ThreadLocalMap的静态内部类和两个核心方法,get和set。 35 | 36 | ### 1.ThreadLocalMap 37 | ThreadLocalMap是一个存储(key,value)键值对的类,它的实现与HashMap有些类似,这里不再详细赘述。它比较特别的地方是它内部的节点Entry: 38 | 39 | ``` 40 | 41 | static class Entry extends WeakReference> { 42 | /** The value associated with this ThreadLocal. */ 43 | Object value; 44 | 45 | Entry(ThreadLocal k, Object v) { 46 | super(k); 47 | value = v; 48 | } 49 | } 50 | ``` 51 | Entry是位于ThreadLocalMap中的静态内部类,表示Map的节点(key,value),并且它继承了WeakReference,WeakReference泛型为ThreadLocal。看Entry的构造方法有两个参数ThreadLocal与一个Object,分别表示key和value。这里注意,在构造方法中将ThreadLocal作为参数调用了父类的构造方法,也就是ThreadLocalMap的key是一个弱引用,而value是一个强引用。意味着,当没有引用指向key时,key会在下一次垃圾回收时被JVM回收。 52 | 53 | 那ThreadLocalMap在哪里使用了呢?其实就在线程Thread类中: 54 | ``` 55 | class Thread implements Runnable { 56 | ThreadLocal.ThreadLocalMap threadLocals = null; 57 | } 58 | ``` 59 | 可以看到Thread类中维护了一个ThreadLocalMap的成员变量。 60 | 61 | 62 | ### 2.ThreadLocal的set方法分析 63 | 64 | set方法的源码如下: 65 | ``` 66 | public void set(T value) { 67 | Thread t = Thread.currentThread(); 68 | ThreadLocalMap map = getMap(t); 69 | if (map != null) { 70 | map.set(this, value); 71 | } else { 72 | createMap(t, value); 73 | } 74 | } 75 | // 获取当前线程的ThreadLocalMap 76 | ThreadLocalMap getMap(Thread t) { 77 | return t.threadLocals; 78 | } 79 | 80 | // 为线程t创建ThreadLocalMap 81 | void createMap(Thread t, T firstValue) { 82 | t.threadLocals = new ThreadLocalMap(this, firstValue); 83 | } 84 | ``` 85 | set方法的源码很简单,就是以当前的ThreadLocal作为key将value放到ThreadLocalMap中,ThreadLocalMap是位于ThreadLocal中的一个静态内部类。 86 | 87 | 88 | ### 3.为什么要将ThreadLocalMap的key声明成弱引用呢? 89 | 90 | 这么做其实是为了有利于垃圾回收。想一下如果没有将ThreadLocal的key声明为弱引用会有什么问题?当使用完了ThreaLocal之后,解除了对它的引用,希望它能够被回收,但是此时JVM发现ThreadLocal无法被回收,因为在ThreadLocalMap中,ThreadLocal还作为key被引用,虽然ThreadLocal已经不再使用,但是仍然无法被回收。而如果将ThreadLocalMap的key声明为弱引用就不会存在无法回收ThreadLocal的问题。 91 | 92 | 93 | ### 4.ThreadLocal的get方法分析 94 | 95 | get方法就是从ThreadLocal中取值,它的源码如下: 96 | 97 | ``` 98 | public T get() { 99 | Thread t = Thread.currentThread(); 100 | ThreadLocalMap map = getMap(t); 101 | if (map != null) { 102 | ThreadLocalMap.Entry e = map.getEntry(this); 103 | if (e != null) { 104 | @SuppressWarnings("unchecked") 105 | T result = (T)e.value; 106 | return result; 107 | } 108 | } 109 | return setInitialValue(); 110 | } 111 | ``` 112 | 可以看到get方法的源码也不难,就是拿到当前线程中的ThreadLocalMap,然后将当前ThreadLocal作为key查找set进去的值。 113 | 114 | 了解了ThreadLocal的内部实现后,我们通过开篇的例题来分析以下ThreadLoca的存储过程: 115 | - (1)threadLoca在子线程(称为threadA)中通过threadLocal的set方法将值设置为了true。在set方法中会首先获得threadA线程,然后拿到threadA中的ThreadLocalMap,并将threadLocal作为key,将true作为value存储到了ThreadLocalMap中; 116 | 117 | - (2)当在threadA线程中调用threadLocal的get方法时,get方法同样会先获取到threadA,然后拿到threadA中的ThreadLocalMap,接着以threadLocal作为key来获取ThreadLocalMap中对应的值,此时的ThreadLocalMap与第(1)不中的ThreadLocalMap是同一个,且key都是threadLocal。因此,在threadLocal中拿到的值为true; 118 | 119 | - (3)在主线程中调用threadLocal的get操作,此时get方法会首先获取到当前线程为主线程,然后拿到了主线程中的ThreadLocalMap,接着以threadLocal作为key来获取主线程的ThreadLocalMap中对应的值,此时的ThreadLocalMap与(1)步中的ThreadLocalMap并非同一个。因此,主线程中获取到的值是null。 120 | 121 | ThreadLocal的示意图如下: 122 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/thread/threadlocal.png) 123 | 124 | 125 | [Java并发系列番外篇:ThreadLocal原理其实很简单](https://juejin.cn/post/6986301941269659656) 126 | -------------------------------------------------------------------------------- /post/UI界面及卡顿优化.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。Android系统每隔大概16.6ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。 3 | 4 | ## 1.Android界面滑动卡顿主要有两个原因 5 | 6 | - (1)UI线程(main)有耗时操作 7 | 8 | - (2)视图渲染时间过长,导致卡顿 9 | 10 | Android系统每隔16ms就会发送一个VSYNC信号(VSYNC:vertical synchronization 垂直同步,帧同步),触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的正常帧率:60fps。一旦这时候系统正在做大于16ms的耗时操作,系统就会无法响应VSYNC信号,执行渲染工作,导致发生丢帧现象。 11 | 12 | 关于Android屏幕刷新机制可以参考[Android屏幕刷新机制](https://github.com/zhpanvip/AndroidNote/wiki/%E5%B1%8F%E5%B9%95%E5%88%B7%E6%96%B0%E6%9C%BA%E5%88%B6) 13 | 14 | 用户容易在UI执行动画、ListView、RecyclerView滑动的时候感知到界面的卡顿与不流畅现象。所以开发者一定要注意在设计布局时不要嵌套太多层,多使用 include方法引入布局。同时不要让动画执行次数太多,导致CPU或者GPU负载过重。 15 | 16 | ## 2.UI优化的三个方案 17 | 18 | ### (1)布局优化 19 | - 减少View的嵌套层级 20 | - LayoutInflater这个类会通过Pull解析器取解析xml文件,然后通过反射的方式来生成对应的View。而解析xml与反射都是一个相对消耗性能的操作。因此,在复杂的页面中,应该尽可能的减少View的嵌套层级,以此较少解析xml的性能消耗。 21 | - 使用merge减少一层嵌套 22 | - 使用new来实例化View 23 | 对于简单的布局,比如只有一个View的情况,可以考虑直接使用new来实例化View,以此较少解析xml和反射带来的性能损耗 24 | 25 | ### (2)异步加载布局 26 | 27 | 对于布局特别复杂的页面,可以采用异步加载布局的方式来优化提升加载效率。Android为我们提供了 Asynclayoutinflater 把耗时的加载操作在异步线程中完成,最后把加载结果再回调给主线程。 28 | 29 | ``` 30 | dependencies { implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" } 31 | ``` 32 | 33 | ``` 34 | new AsyncLayoutInflater(this) 35 | .inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() { 36 | @Override 37 | public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) { 38 | setContentView(view); 39 | //...... 40 | } 41 | }); 42 | ``` 43 | **需要注意:** 44 | 45 | - 1、使用异步 inflate,那么需要这个 layout 的 parent 的 generateLayoutParams 函数是线程安全的; 46 | - 2、所有构建的 View 中必须不能创建 Handler 或者是调用 Looper.myLooper;(因为是在异步线程中加载的,异步线程默认没有调用 Looper.prepare ); 47 | - 3、AsyncLayoutInflater 不支持设置 LayoutInflater.Factory 或者 LayoutInflater.Factory2; 48 | - 4、不支持加载包含 Fragment 的 layout; 49 | - 5、如果 AsyncLayoutInflater 失败,那么会自动回退到UI线程来加载布局。 50 | 51 | ### (3)避免过度绘制 52 | 53 | 过渡绘制是指屏幕上某个像素在同一帧的时间内绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制操作,这就会导致某些像素区域被绘制了多次,这就是很大程度上浪费了CPU和GPU资源。最最常见的过度绘制,就是设置了无用的背景颜色!!! 54 | 55 | 对于Overdraw这个问题还是很容易发现的,我们可以通过以下步骤打开显示GPU过度绘制(Show GPU Overrdraw)选项 56 | 57 | > 设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制 58 | 59 | 打开以后之后,你会发现屏幕上有各种颜色,此时你可以切换到需要检测的程序与界面,对于各个色块的含义,请看下图: 60 | ![这里写图片描述](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzcyNzc5MC0yNzg4ZjdlOGY4MWQ4ZmI2LnBuZw?x-oss-process=image/format,png) 61 | 62 | 蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况: 63 | 64 | - **蓝色**: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。 65 | - **绿色**: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。 66 | - **淡红**: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。 67 | - **深红**: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。 68 | 69 | 我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。 70 | 71 | #### OverDraw的解决方案 72 | 73 | - 通过Show GPU Overdraw去检测Overdraw,最终可以通过移除不必要的背景。 74 | - 通过Layout Inspector去检测渲染效率,去除不必要的嵌套; 75 | 76 | ## 3.卡顿分析常用工具 77 | 78 | ### (1)Systrace 79 | Systrace 是Android平台提供的一款工具,用于记录短期内的设备活动。该工具会生成一份报告,其中汇总了Android 内核中的数据,例如 CPU 调度程序、磁盘活动和应用线程。Systrace主要用来分析绘制性能方面的问题。在发生卡顿时,通过这份报告可以知道当前整个系统所处的状态,从而帮助开发者更直观的分析系统瓶颈,改进性能。 80 | 81 | 在抓取systrace文件的时候,切记不要抓取太长时间,也不要太多不同操作。 82 | 83 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/performance/systrace1.png) 84 | 85 | 打开抓取的html文件,可以看到我们应用存在非常严重的掉帧,不借助工具直接用肉眼看应用UI是看不出来的。如果只是单独存在一个红色或者黄色的都是没关系的。关键是连续的红/黄色或者两帧间隔非常大那就需要我们去仔细观察。按“W” 放大视图,在UIThread(主线程)上面有一条很细的线,表示线程状态。 86 | 87 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/performance/systrace2.png) 88 | 89 | Systrace 会用不同的颜色来标识不同的线程状态, 在每个方法上面都会有对应的线程状态来标识目前线程所处的状态。通过查看线程状态我们可以知道目前的瓶颈是什么, 是 CPU 执行慢还是因为 Binder 调用, 又或是进行 IO 操作,又或是拿不到 CPU 时间片。 通过查看线程状态我们可以知道目前的瓶颈是什么, 是 CPU 执行慢还是因为 Binder调用, 又或是进行 IO 操作, 又或是拿不到CPU事件片。 90 | 91 | 线程状态主要有下面几个: 92 | 93 | - 绿色:表示正在运行; 94 | - 是否频率不够?(CPU处理速度) 95 | - 是否跑在了小核上?(不可控,但实际上很多手机都会有游戏模式,如果我们应用是手游,那系统会优先把手游中的任务放到大核上跑。) 96 | - 蓝色:表示可以运行,但是CPU在执行其他线程; 97 | - 是否后台有太多的任务在跑?Runnable 状态的线程状态持续时间越长,则表示 cpu 的调度越忙,没有及时处理到这个任务 98 | - 没有及时处理是因为频率太低? 99 | 100 | 紫色:表示休眠,一般表示IO; 101 | 102 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/performance/systrace3.png) 103 | 104 | - 橙色:不可中断的休眠 105 | - 线程在遇到 I/O 操作时被阻止或正在等待磁盘操作完成。 106 | 107 | - 紫色:可中断的休眠 108 | - 线程在遇到另一项内核操作(通常是内存管理)时被阻止。 109 | - 但是实际从Android 9模拟器中拉取数据,遇到IO显示紫色,没有橙色状态显示。 110 | - 白色:表示休眠,可能是因为线程在互斥锁上被阻塞 ,如Binder堵塞/Sleep/Wait等 111 | 112 | ### (2)Trace API 113 | 其实对于APP开发而言,使用systrace的帮助并不算非常大,大部分内容用于设备真机优化之类的系统开发人员观察。systrace无法帮助应用开发者定位到准确的错误代码位置,我们需要凭借很多零碎的知识点与经验来猜测问题原因。如果我们有了大概怀疑的具体的代码块或者有想了解的代码块执行时系统的状态,还可以结合 Trace API 打标签。 114 | 115 | Android 提供了Trace API能够帮助我们记录收集自己应用中的一些信息 : Trace.beginSection() 与 Trace.endSection(); 116 | 117 | ``` 118 | public class MainActivity extends AppCompatActivity { 119 | @Override protected void onCreate(Bundle savedInstanceState) { 120 | super.onCreate(savedInstanceState); 121 | TraceCompat.beginSection("enjoy_launcher"); //Trace.beginSection() 122 | setContentView(R.layout.activity_main); 123 | TraceCompat.endSection(); //Trace.endSection() 124 | } } 125 | ``` 126 | 127 | ### BlockCanary 128 | Android主线程更新UI。如果界面1秒钟刷新少于60次,即FPS小于60,用户就会产生卡顿感觉。简单来说, 129 | Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的 130 | Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。 131 | 132 | 只要检测 msg.target.dispatchMessage(msg) 的执行时间,就能检测到部分UI线程是否有耗时的操作。注意到这行 133 | 执行代码的前后,有两个logging.println函数,如果设置了logging,会分别打印出>>>>> Dispatching to和 134 | <<<<< Finished to 这样的日志,这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时 135 | 间,从而设置阈值判断是否发生了卡顿。 136 | 137 | Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我们可以自己实现一个Printer,在 138 | 通过setMessageLogging()方法传入即可 139 | 140 | 这种方式也就是 BlockCanary 原理。 141 | 142 | ### 抓取trace日志结合Profile分析方法执行时间 143 | 开启trace日志抓取,并将日志信息存储到APP目录下data/data/app包名。 144 | ``` 145 | Debug.startMethodTracingSampling(new File(getExternalFilesDir(""),"trace").getAbsolutePath(),8*1024*1024,1_000); 146 | ``` 147 | 停止日志抓取 148 | ``` 149 | Debug.stopMethodTracing() 150 | ``` 151 | 启动APP,执行trace日志抓取后会生成一个trace文件,将trace文件拖到Android Studio中,可以通过右侧TopDown分析方法执行的耗时时间 152 | -------------------------------------------------------------------------------- /post/XML解析原理.md: -------------------------------------------------------------------------------- 1 | XML解析原理 -------------------------------------------------------------------------------- /post/synchronized的实现原理.md: -------------------------------------------------------------------------------- 1 | 2 | ## synchronized使用 3 | 4 | synchronized可以修饰方法或者同步代码块,主要确保多个线程在同一时刻只能有一个线程处于该方法或者同步代码块中,它保证了线程对变量访问的可见性和排他性。 5 | 6 | 1.修饰代码块,对指定的对象加锁 7 | ``` 8 | public void add() { 9 | synchronized (this) { 10 | i++; 11 | } 12 | } 13 | ``` 14 | 15 | 2.修饰实例方法,对当前实例对象this加锁 16 | 17 | ``` 18 | public synchronized void add(){ 19 | i++; 20 | } 21 | ``` 22 | 23 | 3.修饰静态方法,对该类的Class对象加锁 24 | 25 | ``` 26 | public static synchronized void add(){ 27 | i++; 28 | } 29 | ``` 30 | 31 | 要注意synchronized关键字是一个对象锁,无论怎么使用,它一定是对某个对象加锁。 32 | 33 | ## Java对象头与monitor对象 34 | 35 | Java中对象由三部分构成,分别为对象头、实例变量、填充字节。 36 | - (1) 实例数据:存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。 37 | - (2) 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。 38 | - (3) 对象头:它是实现synchronized的锁对象的基础,这点我们重点讨论它。 39 | 40 | synchronized锁是存储在Java对象头中的,JVM中采用两个字节来存储对象头,以Hotspot为例,对象头主要包括三部分:Mark Word(标记字段)、Klass Pointer(类型指针)以及数组数组长度(只有数组对象有)。其中Mark Word用于存放对象自身的运行时数据。 41 | 42 | Mark Word主要用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳等。Mark Word同时也记录了对象和锁有关的信息。当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。Mark Word在不同的锁状态下存储的内容不同,在32位JVM中的存储内容如下图,其中无锁和偏向锁的标记状态都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。 43 | ![](https://img-blog.csdnimg.cn/20191022155910999.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTI3MjM2NzM=,size_16,color_FFFFFF,t_70) 44 | 45 | 可以看到,当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。 46 | 47 | 48 | ## monitor对象 49 | 50 |  由上述内容可知,重量级锁synchronized的标识位是10,其中指针指的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象实例都会有一个 monitor与之关联。monitor既可以与对象一起创建、销毁;也可以在线程试图获取对象锁时自动生成。但当一个 monitor 被某个线程持有后,它便处于锁定状态。 51 | 52 |   在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件中,使用C++实现) 53 | 54 | ``` 55 | ObjectMonitor() { 56 | _header = NULL; 57 | _count = 0; //记录个数 58 | _waiters = 0, 59 | _recursions = 0; 60 | _object = NULL; 61 | _owner = NULL; 62 | _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet 63 | _WaitSetLock = 0 ; 64 | _Responsible = NULL ; 65 | _succ = NULL ; 66 | _cxq = NULL ; 67 | FreeNext = NULL ; 68 | _EntryList = NULL ; //处于获取锁失败的线程,会被加入到该列表 69 | _SpinFreq = 0 ; 70 | _SpinClock = 0 ; 71 | OwnerIsThread = 0 ; 72 | } 73 | ``` 74 | 75 | ## synchronized实现原理 76 | 77 | ### 1.同步代码块 78 | synchronized底层是通过monitorenter和moniterexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束和异常处。 79 | 通过javap的工具对上述同步代码块进行反汇编 80 | ``` 81 | public void add() { 82 | synchronized (this) { 83 | i++; 84 | } 85 | } 86 | ``` 87 | 可以看到字节码指令: 88 | 89 | ``` 90 | public class com.zhangpan.text.TestSync { 91 | public com.zhangpan.text.TestSync(); 92 | Code: 93 | 0: aload_0 94 | 1: invokespecial #1 // Method java/lang/Object."":()V 95 | 4: return 96 | 97 | public void add(); 98 | Code: 99 | 0: aload_0 100 | 1: dup 101 | 2: astore_1 102 | 3: monitorenter // synchronized关键字的入口 103 | 4: getstatic #2 // Field i:I 104 | 7: iconst_1 105 | 8: iadd 106 | 9: putstatic #2 // Field i:I 107 | 12: aload_1 108 | 13: monitorexit // synchronized关键字的出口 109 | 14: goto 22 110 | 17: astore_2 111 | 18: aload_1 112 | 19: monitorexit // synchronized关键字的出口 113 | 20: aload_2 114 | 21: athrow 115 | 22: return 116 | Exception table: 117 | from to target type 118 | 4 14 17 any 119 | 17 20 17 any 120 | } 121 | 122 | ``` 123 | 当代码执行到monitorenter 指令时,将会尝试获取该对象对应的Monitor的所有权,即尝试获得该对象的锁。当该对象的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有该对象monitor的持有权,那它可以重入这个 monitor ,计数器的值也会加 1。与之对应的执行monitorexit指令时,锁的计数器会减1。倘若其他线程已经拥有monitor 的所有权,那么当前线程获取锁失败将被阻塞并进入到_WaitSet 中,直到等待的锁被释放为止。也就是说,当所有相应的monitorexit指令都被执行,计数器的值减为0,执行线程将释放 monitor(锁),其他线程才有机会持有 monitor 。 124 | 125 | 需要注意的是,字节码中有两个monitorexit指令,因为编译器需要确保方法中调用过的每条monitorenter指令都有执行对应的monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常时,monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个可以处理所有异常的异常处理器,它的目的就是用来执行异常的monitorexit指令。而字节码中多出的monitorexit指令,就是异常结束时用来释放monitor的指令。 126 | 127 | 上述过程可以总结如下: 128 | - (1) 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 129 | - (2) 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. 130 | - (3) 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 131 | 132 | ### 2.同步方法的实现 133 | ``` 134 | public synchronized void add(){ 135 | i++; 136 | } 137 | ``` 138 | 139 | 通过javap -v 获取上面代码字节码的附件信息,得到如下结果: 140 | 141 | ``` 142 | public synchronized void add(); 143 | descriptor: ()V 144 | flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED 145 | Code: 146 | stack=3, locals=1, args_size=1 147 | 0: aload_0 148 | 1: dup 149 | 2: getfield #2 // Field i:I 150 | 5: iconst_1 151 | 6: iadd 152 | 7: putfield #2 // Field i:I 153 | 10: return 154 | LineNumberTable: 155 | line 5: 0 156 | line 6: 10 157 | 158 | ``` 159 | 160 | 从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。 161 | 162 | ## Java虚拟机对synchronized的优化 163 | 164 | 需要注意的是,在Java早期版本中,synchronized属于重量级锁,效率低下。这是因为在实现上,JVM会阻塞未获取到锁的线程,直到锁被释放的时候才唤醒这些线程。阻塞和唤醒操作是依赖操作系统来完成的,所以需要从用户态切换到内核态,开销很大。并且monitor调用的是操作系统底层的互斥量(mutex),本身也有用户态和内核态的切换。Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单介绍一下Java官方在JVM层面对synchronized锁的优化。 165 | 166 | ### 1.Java6之后synchronized引入的多种锁机制 167 | 168 |   锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。前面已经详细分析过重量级锁,下面将介绍偏向锁和轻量级锁以及JVM的其他优化手段。 169 | 170 | - 1.偏向锁 171 |   偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段.经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是被同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 172 | 173 | - 2.轻量级锁 174 |   如果偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(Java1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。轻量级锁在实际没有锁竞争的情况下,将申请互斥量这步也省掉。 175 | 176 | - 3.自旋锁 177 |   轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。 178 |   自旋会跑一些无用的CPU指令,所以会浪费处理器时间,如果锁被其他线程占用的时间短的话确实是合适的。但是如果长的话就不如直接使用阻塞。那么JVM怎么知道锁被占用的时间到底是长还是短呢?因为JVM不知道锁被占用的时间长短,所以使用的是自适应自旋。就是线程空循环的次数时会动态调整的。可以看出,自旋会导致不公平锁,不一定等待时间最长的线程会最先获取锁。 179 | 180 | - 4.锁消除 181 |   消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。例如,StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。 182 | 183 | 184 | ### 2.synchronized关键字锁升级过程 185 | 186 | - (1)当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0; 187 | - (2)当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态; 188 | - (3) 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步中的代码; 189 | - (4) 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步代码。如果抢锁失败,则继续执行步骤5; 190 | - (5) 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6; 191 | - (6) 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步代码,如果失败则继续执行步骤7; 192 | - (7) 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。 193 | 194 | https://juejin.cn/post/6844903726545633287 195 | 196 | [深入理解Java中synchronized关键字的实现原理](https://blog.csdn.net/u012723673/article/details/102681942) 197 | 198 | [死磕synchronized底层实现](https://juejin.cn/post/6844904196676780040) -------------------------------------------------------------------------------- /post/内存优化.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 一 、 为什么要做内存优化 4 | 5 | 在开始之前需要先搞明白一个问题,为什么要做内存优化?或者说做内存优化的目的是什么? 6 | 7 | ### 1. 内存大的进程会被优先回收 8 | 9 | Android 是基于 Linux 内核实现的。Linux的内存管理哲学是:Free memory is wasted memory。即内存没有得到充分利用就是在浪费内存。因此 Linux 希望尽可能多的使用内存,较少磁盘 IO 。Android 系统继承了 Linux 的优点,同样是尽最大限度使用原则。但与Linux不同的是 Android 侧重于可能多的缓存进程以提高应用启动和切换速度。即,Android系统会在内存中尽量的长时间的保持应用进程,直到系统分配内存不足时才会去根据进程优先级、内存代销等条件回收进程。这些保留在内存中的进程通常不会影响系统整体的运行速度,反而会在用户再次激活这些进程时,加快进程的启动速度。 10 | 11 | 当Android系统需要为其他应用分配更多内存是,如果发现内存不足,便会根据条件关闭某些后台进程以回收内存。 12 | 13 | - 在Android 7之后(之前是oom_adj)的系统中有一个oom_score_adj的值表示应用的优先级,**oom_score_adj**越大,进程的优先级越低,越容易被回收。 14 | - 系统会使用LRU算法来优先从最近最少使用的进程开始遍历,因此最近最少使用的进程被回收的概率比较高。 15 | - 系统还会考虑使用内存较多的进程优先回收。 16 | 17 | 概括来说,当系统分配内存时发现内存不足,则会根据进程优先级、最近最少使用、进程占用内存等条件综合评判进行进程回收。显然低内存的进程被回收的可能性比较低,因此,为了保证用户体验应该尽可能的减少进程占用的内存。 18 | 19 | ### 2. 单个进程使用的内存是有限的。 20 | 21 | Android 系统的java虚拟机会对单个进程使用的最大内存做限制,这个属性值定义在/system/build.prop文件中,厂商一般会根据设备自身内存大小来设定这个值,因此,不同的设备分配给APP的最大可用内存是不相同的。当进程启动时,系统会先为APP分配一定的内存空间,当分配的内存快要耗尽时,系统会再次为App 分配更多的内存,但是每个APP都有内存使用上限,一旦进程分配了最大可用内存后,内存依然不足则会直接抛出OOM异常,终止程序的运行。 22 | 23 | 因此,占用较大内存的APP会导致OOM异常的可能性,基于这点更需要我们针对性的做内存优化,避免OOM。 24 | 25 | ### 3. GC 时 STW 影响程序性能 26 | 27 | 当进程使用内存达到设定的阈值时,就会触发虚拟机的GC机制,无论是java的Hotspot虚拟机还是Android的Dalvik 或者ART虚拟机,在进行垃圾回收时都会存在暂停其他线程的问题,被称作Stop The World(简称SWT)。当发生SWT时,所有的其他线程都会被停止运行,等待GC结束后才会再次执行。虽然JDK中的较新的垃圾收集器向ZGC、Shenandoha以及ART自身的垃圾收集器等已经将STW的时间减小,但还是STW还是不能避免。 28 | 29 | 当内存不足或者出现内存抖动时都会频繁的出发GC机制进行垃圾回收。而由于频繁的GC导致频繁的的STW,进而导致严重的程序的性能问题。因此,为了提升程序性能,有必要进行内存优化。 30 | 31 | 32 | 33 | ## 二、内存优化策略 34 | 35 | 内存优化可以考虑从两个方面入手,一方面是大对象的优化,应该想办法减小大对象占用的内存;另一方面则是从内存泄漏及内存抖动等方面去优化内存。 36 | 37 | ### 1. Bitmap等大对象的优化策略 38 | 39 | APP中的内存问题多半是因为Bitmap引起的。因此,解决Bitmap的问题就解决了一半的内存问题。要解决Bitmap的内存首先要知道Bitmap占用的内存是如何计算的。Bitmap的内存计算公式如下: 40 | 41 | > Bitmap占用内存 = 分辨率 * 单个像素点的内存 42 | 43 | 比如说一个 `1920 * 1080` 的图片,它所占用的内存就是`1920 * 1080 * 单个像素点内存`。因此,对于Bitmap的优化就可以从分辨率和单个像素点两个方面来进行优化。 44 | 45 | #### (1) 优化Bitmap分辨率 46 | 47 | 通常APP加载一张图片时候,ImageView的大小是确定的,比如一个ImageView的大小设置为 `100 * 100` ,但是被加载的Bitmap的分辨率是 `200 * 200`,那么就可以通过采样压缩将该 'Bitmap' 的分辨率压缩到 '100 * 100'。通过这一压缩操作可以直接减少4倍的内存大小。代码如下: 48 | 49 | ```kotlin 50 | val options = BitmapFactory.Options() 51 | options.inSampleSize = 2 // 设置采样率为2,则会每两个像素点采一个像素,最终分辨率宽高变为原来的 1/2 52 | val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options) 53 | ``` 54 | 55 | #### (2) 优化单个像素点内存 56 | 57 | 计算机中的图像一般都是由 红、绿、蓝 三个通道加上一个透明通道组成的,因此,一般来说一个像素点也是由红、绿、蓝,以及一个透明通道组成,对应到内存就是通过byte来表示,比如用两个 byte 来存储一个像素点,那么每个通道就占用 4 bit 的内存,而如果用 4 个 byte 来存储一个像素点,那么每个通道就占用 1 个byte。4 字节的像素点,相比2字节的像素点可以表示的色彩会更加丰富,因此四字节的像素点组成的图像质量也更加清晰。 58 | 59 | 在 Android 的 Bitmap 中单个像素点占用的内存与 Bitmap 的 inPreferredConfig 参数配置有关系,这个参数的可选值如下表所示: 60 | 61 | | Config | 占用内存(byte) | 说明 | 62 | | --------- | ---------------- | ------------------------------------------------------------ | 63 | | ALPH_8 | 1 | 只包含一个透明通道,透明通道占用 8bit,即 1byte | 64 | | RGB_565 | 2 | 包含R/G/B三个颜色通道,不包含透明通道,三个通道占用的内存分别为5bit/6bit/5bit | 65 | | ARGB_4444 | 2 | 已废弃,包含A/R/G/B四个颜色通道,每个通道占用4bit | 66 | | ARGB_8888 | 4 | 24位真彩色,Android默认配置,每个通道占用 8bit | 67 | | RGBA_F16 | 8 | Android 8.0 新增,每个通道占用16bit,即两个字节 | 68 | 69 | 在Android系统中 Bitmap 的默认色彩模式为 ARGB_8888, 即每个像素占用了4byte,那么在默认情况下,一张分辨率为`1920 * 1080` 的图片,加载到内存后占用的内存大小为`1920 * 1080 * 4 = 7.91M` 70 | 71 | 可以通过设置 inPreferredConfig 参数来设置对应的色彩模式,例如,一个不包含透明通道的图片,我们可以将其设置为RGB_565,即保证了图片的质量,又减少了内存的占用。代码如下: 72 | 73 | ``` kotlin 74 | val options = BitmapFactory.Options() 75 | options.inPreferredConfig = Bitmap.Config.RGB_565 // 设置 bitmap 的色彩模式为 RGB_565 76 | val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image, options) 77 | ``` 78 | 79 | 此时,一张 `1920 * 1080` 的图片加载到内存后的内存大小为 `1920 * 1080 * 2 = 3.955M`,比默认情况下的内存占用减小了一半。 80 | 81 | #### (3) Bitmap的缓存策略 82 | 83 | 通过缓存策略也可以一定程度上的优化内存占用问题,比如 Glide 框架中采用了三级本地缓存策略来实现Bitmap的优化,通过设置活动缓存、LRU内存缓存和本地缓存。对于相同参数的ImageView,在内存中只保存一份,以此来减少内存大小。 84 | 85 | #### (4) drawable资源选择合适的drawable文件夹存放 86 | 87 | 例如我们只在 hdpi 的目录下放置了一张 `100 * 100` 的图片,那么根据换算关系,分辨率匹配到 xxhdpi 的手机去引用这张图片时就会被拉伸到 `200*200`。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到 assets 或者 nodpi 的目录下。 88 | 89 | #### (5) 其他大对象的优化 90 | 91 | 可以使用更加轻量级的数据结构。例如,我们可以考虑使用 ArrayMap/SparseArray 而不是 HashMap 等传统数据结构,相比起 Android 系统专门为移动操作系统编写的 ArrayMap 容器,在大多数情况下,HashMap 都显示效率低下,更占内存。另外,SparseArray更加高效在于,避免了对key与value的自动装箱,并且避免了装箱后的解箱。 92 | 93 | #### (6) 避免内存抖动 94 | 95 | 内存抖动是指在短时间内突然创建大量的对象,频繁的引发GC回收,造成内存波动的情况。在开发中应该避免频繁的创建对象,来避免内存抖动。因为内存抖动会频繁触发 GC,而GC又会引起 STW 问题,直接影响程序的性能。 96 | 97 | 比如在绘制自定义View的时候一定要避免在onDraw或者onMeasure中创建对象。 98 | 99 | ### 2. 避免内存泄漏 100 | 101 | Java的内存泄漏是指问题是指在对象使用结束后,由于一些地方持有该对象,虽然已经无用,但是无法被GC正常回收的情况。内存泄漏会引起很严重的性能问题,比如内存泄漏引起内存紧张,从而频繁的出发GC,而GC由于存在STW问题,又会引发更严重的性能问题。最终在分配新的对象时无法获得足够的内存空间时导致OOM的产生。 102 | 103 | #### 常见的内存泄漏 104 | 105 | 在实践操作当中,可以从四个方面着手减小内存使用,首先是减小对象的内存占用,其次是内存对象的重复利用,然后是避免对象的内存泄露,最后是内存使用策略优化。 106 | 107 | ##### (1) 单例模式引起的内存泄漏(Singleton) 108 | 109 | 为了完美解决我们在程序中反复创建同一对象的问题,我们选用了单例模式,单例在我们的程序中随处可见,但是由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,一不小心让单例无限制的持有Activity的强引用就会导致内存泄漏。 110 | 111 | ##### (2) Handler引起的内存泄漏 112 | 113 | Handler引起的内存泄漏在我们开发中最为常见的。我们知道Handler、Message、MessageQueue都是相互关联在一起的,万一Handler发送的Message尚未被处理,那么该Message以及发送它的Handler对象都会被线程MessageQueue一直持有。 114 | 115 | ##### (3) 匿名内部类在异步线程中的使用 116 | 117 | 它们方便却暗藏杀机。Android开发经常会继承实现 Activity 或者 Fragment 或者 View。如果你使用了匿名类,而又被异步线程所引用,那得小心,如果没有任何措施同样会导致内存泄漏的 118 | 由于Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的,因此这种实现方式很难保证跟Activity的生命周期一直,所以很容易无法释放内存。 119 | 120 | ##### (4) static引起的内存泄漏 121 | 122 | 从前面的介绍我们知道,static修饰的变量位于内存的静态存储区,此变量与App的生命周期一致 123 | 这必然会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,因为如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。 124 | 这里修复的方法是: 125 | 不要在类初始时初始化静态成员。可以考虑lazy初始化(延迟加载)。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。 126 | 127 | ##### (5) 非静态内部类引起的内存泄漏 128 | 129 | 非静态内部类的静态实例容易造成内存泄漏:即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。 130 | 131 | ##### (6) 线程引起的内存泄漏 132 | 133 | 警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit()才不会泄露。 134 | 135 | ##### (7) 其他原因引起的内存泄漏 136 | 137 | 对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。 138 | 139 | 创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。 140 | 141 | 避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。 142 | 143 | ### 3.使用系统提供的API来释放内存 144 | 145 | Android系统提供了一些回调来通知当前应用的内存使用情况,比如下边的两个方法: 146 | 147 | - onLowMemory() 通常来说,当所有的Background应用都被kill掉的时候,forground应用会收到onLowMemory()的回调。在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。尤其是要释放Glide中缓存的Bitmap资源,通过调用Glide.onLowMemory方法进行资源回收。 148 | 149 | - onTrimMemory() Android系统从4.0开始还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递以下的参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用。例如调用Glide.onTrimMemory()来进行bitmap的回收。 150 | 151 | ## 三、常用排查内存问题的工具 152 | 153 | ### (1)LeakCanary监测内存泄漏 154 | 155 | 在debug模式下会一直开着LeakCanary来检测内存泄漏问题,根据LeanCannary提供的引用连可以快速定位到内存泄漏的位置。 156 | 157 | ### (2)通过Proflier监控内存 158 | 159 | 在一个功能开发完成后可以通过Profiler来检测APP的内存使用情况。反复的打开关闭页面,然后触发GC,内存是否能够减少。 160 | 161 | ### (3)通过MAT工具排查内存泄漏 162 | 163 | MAT提供了很强大的功能,可以查看对象的深堆、浅堆的内存大小等。 164 | 165 | -------------------------------------------------------------------------------- /post/动态代理.md: -------------------------------------------------------------------------------- 1 | 2 | 建议先了解[静态代理](https://juejin.cn/post/6844904003524886536) 3 | 4 | ### 1.静态代理的缺点 5 | 6 | - 由于代理类要实现与被代理类一致的接口,当有多个类需要被代理时,要么代理类实现所有被代理类的接口,这样会使代理类过于庞大;要么使用多个代理类,每个代理类只代理一个被代理类,但是这样又会需要构造多个代理类。 7 | 8 | - 当接口需要增加、删除、修改方法时,被代理类与代理类都需要修改,不易维护。 9 | 10 | 为了解决上述问题,可以使用动态代理,自动生成代理类。 11 | 12 | ### 2.使用JDK提供的接口实现动态代理 13 | 14 | 这里涉及到两个类:java.lang.reflect.Proxy` 和 `java.lang.reflect.InvocationHandler接口 15 | 16 | 举个例子: 17 | 18 | > Ryan想在上海买一套房子,但是他又不懂房地产的行情,于是委托了中介(Proxy)来帮助他买房子。 19 | 20 | 使用动态代理实现,首先定义一个IPersonBuyHouse的接口,且有一个buyHouse的方法: 21 | 22 | ```java 23 | public interface IPersonBuyHouse { 24 | // 购买房子的方法,返回值代表是否成功购买 25 | boolean buyHouse(String name); 26 | } 27 | ``` 28 | 29 | 使用动态代理实现Ryan买房子的需求: 30 | 31 | ```java 32 | public static void main(String[] args) { 33 | InvocationHandler handler = new InvocationHandler() { 34 | @Override 35 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 36 | // 在执行购买逻辑前可以先做一些校验,对于不符合要求的不予执行,并返回false。这里省略不写了... 37 | 38 | if (method.getName().equals("buyHouse")) { 39 | System.out.println(args[0] + " will buy a house."); 40 | } 41 | // 返回true,表示成功购买 42 | return true; 43 | } 44 | }; 45 | IPersonBuyHouse person = (IPersonBuyHouse) Proxy 46 | .newProxyInstance(IPersonBuyHouse.class.getClassLoader(), // ClassLoader 47 | new Class[]{IPersonBuyHouse.class}, // 传入要实现的接口 48 | handler); // 传入处理调用方法的InvocationHandler 49 | person.buyHouse("Ryan"); 50 | } 51 | ``` 52 | 53 | 54 | 55 | ### 2. 动态代理原理 56 | 57 | 动态代理是在运行时根据某个接口生成对应的代理类的字节码,然后加载到JVM的,并创建这个接口的实例的过程。 58 | 59 | 对于第二节中的例子进行分析,通过Proxy.newProxyInstance这个方法会生成一个IPersonBuyHouse的实现类。假设生成的这个类叫Ryan,Ryan这个类实现了IPersonBuyHouse,并重写了buyHouse的方法。在这个buyHouse的方法中会调用InvocationHandler的invoke方法。 60 | 61 | Ryan类的代码大致如下: 62 | 63 | ```java 64 | public class Ryan implements IPersonBuyHouse { 65 | // buyHouse的真正逻辑在这个匿名内部类的invoke方法中 66 | private InvocationHandler handler = new InvocationHandler() { 67 | @Override 68 | public Object invoke(Object proxy, Method method, Object[] args) { 69 | // 被代理类真正执行的逻辑 70 | if (method.getName().equals("buyHouse")) { 71 | System.out.println(args[0] + " will buy a house."); 72 | } 73 | return true; 74 | } 75 | }; 76 | 77 | @Override 78 | public boolean buyHouse(String name) { 79 | try { 80 | // 反射获取buyHouse这个Method 81 | Method buyHouseMethod = IPersonBuyHouse.class.getDeclaredMethod("buyHouse", String.class); 82 | // 将buyHouse的参数封装成一个数组 83 | Object[] params = new Object[1]; 84 | params[0] = name; 85 | // 实际调用了InvocationHandler的invoke方法 86 | return (boolean) handler.invoke(this, buyHouseMethod, params); 87 | } catch (Throwable e) { 88 | e.printStackTrace(); 89 | } 90 | return false; 91 | } 92 | } 93 | ``` 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /post/协程实现原理.md: -------------------------------------------------------------------------------- 1 | ### 1.什么是协程? 2 | 3 | 协程是一种非抢占式(协作式)的任务调度模式,程序可以主动挂起或者恢复执行。Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。 4 | 5 | ### 2.协程与线程的区别 6 | 7 | 协程基于线程,但相对于线程轻量很多。可以理解为在用户层模拟线程操作; 8 | 9 | 线程的上下文切换涉及到用户态和内核态的切换,而协程的上下文切换完全是在用户态控制的,避免了大量的中断参与,减少了线程上下文切换与调度的资源消耗 10 | 11 | > CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,**从任务保存到再加载的过程就是一次上下文切换**。 12 | 13 | ### 3.kotlin中的协程 14 | 15 | `Kotlin`在语言级别并没有实现一种同步机制(锁),还是依靠`Kotlin-JVM`的提供的`Java`关键字(如`synchronized`),即锁的实现还是交给线程处理 16 | 因而`Kotlin`协程本质上只是一套基于原生`Java线程池` 的封装。 17 | 18 | `Kotlin` 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。 19 | 下面介绍一些`kotin`协程中的基本概念 20 | 21 | 22 | 23 | ### 4.挂起函数 24 | 25 | 使用suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程内或其他挂起函数内使用。 26 | 27 | 协程内部挂起函数的调用处被称为挂起点,挂起点如果出现异步调用就会挂起当前协程,知道对应的continuation的resume函数被调用才会恢复执行。 28 | 29 | ![suspend](https://user-images.githubusercontent.com/19853475/130308694-b38c5251-4b02-4378-ab66-51c0db769812.gif) 30 | 31 | ### 5.suspend原理 32 | 33 | `suspend` 的本质,就是 `CallBack`。 34 | 35 | ```kotlin 36 | suspend fun getUserInfo(): String { 37 | withContext(Dispatchers.IO) { 38 | delay(1000L) 39 | } 40 | return "BoyCoder" 41 | } 42 | ``` 43 | 44 | 将挂起函数的字节码反编译成java代码简化后得到如下结果: 45 | 46 | ```java 47 | public final Object getUserInfo(@NotNull Continuation var1) { 48 | 49 | 50 | return "BoyCoder"; 51 | } 52 | 53 | ``` 54 | 55 | ```kotlin 56 | public interface Continuation { 57 | 58 | public val context: CoroutineContext 59 | 60 | public fun resumeWith(result: Result) 61 | } 62 | ``` 63 | 64 | 可以看到,编译器会给挂起函数添加一个`Continuation`参数,这被称为`CPS 转换(Continuation-Passing-Style Transformation)` 65 | 66 | `suspend`函数不能在协程体外调用的原因也可以知道了,就是因为这个`Continuation`实例的传递。 67 | 68 | 动画演示挂起函数CPS转换过程: 69 | 70 | ![cps](https://user-images.githubusercontent.com/19853475/130308691-dabf10f6-2314-4292-b890-bb6a1d34dff9.gif) 71 | 72 | 1. 增加了`Continuation`类型的参数 73 | 2. 返回类型从`String`转变成了`Any` 74 | 75 | `CPS` 转换,其实就是将原本的`同步挂起函数`转换成`CallBack` 异步代码的过程。 76 | 这个转换是编译器在背后做的,我们对此无感知。 77 | 78 | ![continuation](https://user-images.githubusercontent.com/19853475/130308689-b05c677f-cdbd-4cee-b369-a20c17de592d.gif) 79 | 80 | 81 | 82 | 参考链接:https://juejin.cn/post/6973650934664527885 83 | -------------------------------------------------------------------------------- /post/单例模式.md: -------------------------------------------------------------------------------- 1 | ### 1.双重校验锁实现单例 2 | 3 | 4 | ```java 5 | public class DoubleCheckSingleton { 6 | 7 | private volatile static DoubleCheckSingleton singleton; 8 | 9 | private DoubleCheckSingleton() { 10 | } 11 | 12 | public static DoubleCheckSingleton getInstance() { 13 | if (singleton == null) { 14 | synchronized (DoubleCheckSingleton.class) { 15 | if (singleton == null) { 16 | singleton = new DoubleCheckSingleton(); 17 | } 18 | } 19 | } 20 | return singleton; 21 | } 22 | } 23 | ``` 24 | 25 | 26 | 27 | #### (1)为什么要进行双重判空? 28 | 29 | - **第一次判空** 在同步代码块外部进行判断,在单例已经创建的情况下,避免进入同步代码块,提升效率 30 | - **第二次判空** 为了避免创建多个单例。假设线程1首先通过第一次判空,还未获得锁时时间片就用完了,此时,线程2获得CPU时间片,并调用单单例方法,此时singleton仍然为空,于是线程2顺利创建了singleton对象。稍后,线程1获得时间片,由于已经执行过了第一层判空,此时如果没有第二次判空,那么线程1也会再创建一个singleton实例,即不满足单例的要求。所以第二此判空很有必要。 31 | 32 | #### (2)成员变量singleton为什么要使用volatile修饰? 33 | 34 | 编译器为了优化程序性能,可能会在编译时对字节码指令进行重排序。重排序后的指令在单线程中运行时没有问题的,但是如果在多线程中,重排序后的代码则可能会出现问题。 35 | 36 | 双重锁校验中实例化singleton的代码在编译成字节指令后并不是一个原子操作,而是会分为三个指令: 37 | 38 | - 1.分配对象内存:memory = allocate(); 39 | - 2.初始化对象:instance(memory); 40 | - 3.instance指向刚分配的内存地址:instance = memory; 41 | 42 | 但是由于编译器指令重排序,上述指令可能会出现以下顺序: 43 | 44 | - 1.分配对象内存:memory = allocate(); 45 | - 2.instance指向刚分配的内存地址:instance = memory; 46 | - 3.初始化对象:instance(memory); 47 | 48 | 假设这个singleton的实例化经过了编译器的重排序,此时有一个线程1调用了该单例方法,并在执行完上述第2步后用完了CPU事件片,此时singleton对象实际上还没有被初始化,但是singleton却被赋值指向了第一步分配的内存。此时,一个线程2获得CPU时间片,并调用这个单例方法,发现singleton不为null,随即return了singleton,但singleton实际上并没有初始化,因此可能造成程执行序异常。 49 | 50 | 如果给成员变量加了volatile关键字,那么编译器便不会对其进行指令重排序,也就不会出现上边的问题。 51 | 52 | #### (3)双重校验锁优缺点 53 | 54 | 既能保证线程安全,又能实现延迟加载。缺点时使用synchronized关键字会影响性能。 55 | 56 | ### 2.静态内部类实现单例 57 | 58 | 59 | 60 | ```java 61 | public class StaticSingleton { 62 | private StaticSingleton singleton; 63 | 64 | private StaticSingleton() { 65 | } 66 | 67 | private static class SingletonHolder { 68 | public static StaticSingleton INSTANCE = new StaticSingleton(); 69 | } 70 | 71 | public static StaticSingleton getInstance() { 72 | return SingletonHolder.INSTANCE; 73 | } 74 | } 75 | ``` 76 | 77 | 78 | 79 | 这种单例利用了类加载的特性,在《Java虚拟机规范》中对于类初始化的时机有着严格的约束: 80 | 81 | ① 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 82 | 83 | ② 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 84 | 85 | ③ 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 86 | 87 | ④ 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 88 | 89 | ⑤ 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。 90 | 91 | ⑥ 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 92 | 93 | 从上述类初始化的条件可以看出,如果不调用SingletonHolder.INSTANCE,SingletonHolder类就不会被加载到虚拟机,SingletonHolder不被加载到虚拟机那么INSTANCE实例也不会被实例化。 94 | 95 | 而当调用了getInstance后会调用SingletonHolder.INSTANCE,这里是一个getstatic指令,所有会触发SingleHolder的初始化,初始化前会进行SingletonHolder的类加载,在类加载的初始化过程中INSTANCE会被实例化。 96 | 97 | 98 | 99 | ### 3.枚举实现单例 100 | 101 | ```java 102 | // 枚举单例 103 | public enum EnumSingleton { 104 | 105 | INSTANCE; 106 | 107 | public void doSomething() { 108 | System.out.println("通过枚举单利打印日志..."); 109 | } 110 | 111 | } 112 | 113 | // 测试类 114 | public class Test { 115 | public static void main(String[] args) { 116 | EnumSingleton.INSTANCE.doSomething(); 117 | } 118 | } 119 | ``` 120 | 121 | 枚举实现的单利是最完美的一种方式。这种方式可以防止序列化与反序列化造成创建多个实例的问题,而前面的几种方式都无法解决这个问题。 -------------------------------------------------------------------------------- /post/字符串相关算法.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | 3 | - [3.无重复字符的最长子串](#3-%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2) 4 | - [125. 验证回文串](#125-%E9%AA%8C%E8%AF%81%E5%9B%9E%E6%96%87%E4%B8%B2/) 5 | - [20.有效括号](#20-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7) 6 | - [344.反转字符串](#344-%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2) 7 | - [557.反转字符串中的单词 III](#557-%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E5%8D%95%E8%AF%8D-iii) 8 | - [567. 字符串的排列](#567-%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%8E%92%E5%88%97) 9 | 10 | ## 题目 11 | 12 | #### [3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) 13 | 14 | `滑动窗口` 15 | 16 | ```java 17 | 18 | /** 19 | * 从头到尾遍历字符串,如果List集合中不包含遍历到的字符 c ,则将字符串 c 放入 List 集合中, 20 | * 如果包含遍历到的字符串,那么这个结合中的 size 可能就是最大值,与 maxSize 对比,将最大值保存到 21 | * maxSize,然后从集合头部开始移除元素,直到集合中不包含遍历到的这个字符 c 为止,然后将字符串 c 存入集合中。 22 | * 字符串遍历结束后 maxSize 与 List size 的最大值即为最大字符串。 23 | */ 24 | public static int lengthOfLongestSubstring(String s) { 25 | char[] chars = s.toCharArray(); 26 | List list = new ArrayList<>(); 27 | int maxSize = 0; 28 | for (Character c : chars) { 29 | if (list.size() < chars.length) { 30 | if (list.contains(c)) { 31 | maxSize = Math.max(list.size(), maxSize); 32 | while (list.size() > 0 && list.contains(c)) { 33 | list.remove(0); 34 | } 35 | } 36 | list.add(c); 37 | } 38 | } 39 | 40 | return Math.max(maxSize, list.size()); 41 | } 42 | 43 | ``` 44 | 45 | 46 | 47 | #### [125. 验证回文串](https://leetcode-cn.com/problems/valid-palindrome/) 48 | 49 | 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 50 | 51 | 说明:本题中,我们将空字符串定义为有效的回文串。 52 | 53 | 示例 1: 54 | 55 | 输入: "A man, a plan, a canal: Panama" 56 | 输出: true 57 | 解释:"amanaplanacanalpanama" 是回文串 58 | 示例 2: 59 | 60 | 输入: "race a car" 61 | 输出: false 62 | 解释:"raceacar" 不是回文串 63 | 64 | 65 | 提示: 66 | 67 | 1 <= s.length <= 2 * 105 68 | 字符串 s 由 ASCII 字符组成 69 | 70 | **解题思路** 71 | 72 | ```java 73 | public static boolean isPalindrome(String s) { 74 | int right = s.length() - 1; 75 | int left = 0; 76 | while (left < right) { 77 | char leftChar = s.charAt(left); 78 | while (!Character.isLetterOrDigit(leftChar) && left < right) { 79 | leftChar = s.charAt(++left); 80 | } 81 | char rightChar = s.charAt(right); 82 | while (!Character.isLetterOrDigit(rightChar) && left < right) { 83 | rightChar = s.charAt(--right); 84 | } 85 | if (Character.toLowerCase(leftChar) != Character.toLowerCase(rightChar)) { 86 | return false; 87 | } 88 | left++; 89 | right--; 90 | } 91 | return true; 92 | } 93 | ``` 94 | 95 | #### [20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/) 96 | 97 | 给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 98 | 99 | 有效字符串需满足: 100 | 101 | 左括号必须用相同类型的右括号闭合。 102 | 左括号必须以正确的顺序闭合。 103 | 104 | 105 | 示例 1: 106 | 107 | 输入:s = "()" 108 | 输出:true 109 | 示例 2: 110 | 111 | 输入:s = "()[]{}" 112 | 输出:true 113 | 示例 3: 114 | 115 | 输入:s = "(]" 116 | 输出:false 117 | 示例 4: 118 | 119 | 输入:s = "([)]" 120 | 输出:false 121 | 示例 5: 122 | 123 | 输入:s = "{[]}" 124 | 输出:true 125 | 126 | 127 | 提示: 128 | 129 | 1 <= s.length <= 104 130 | s 仅由括号 '()[]{}' 组成 131 | 132 | **解题思路** 133 | 134 | ```java  135 | public boolean isValid(String s) { 136 | int length = s.length(); 137 | if (length % 2 == 1) { 138 | return false; 139 | } 140 | HashMap pairs = new HashMap<>() { 141 | { 142 | put('(', ')'); 143 | put('[', ']'); 144 | put('{', '}'); 145 | } 146 | }; 147 | LinkedList stack = new LinkedList<>(); 148 | 149 | for (int i = 0; i < length; i++) { 150 | char c = s.charAt(i); 151 | if (pairs.get(c) == null) { 152 | if (stack.size() > 0) { 153 | Character pop = stack.pop(); 154 | if (c != pairs.get(pop)) { 155 | return false; 156 | } 157 | } else { 158 | return false; 159 | } 160 | 161 | } else { 162 | stack.push(c); 163 | } 164 | } 165 | return stack.size() == 0; 166 | } 167 | ``` 168 | 169 | #### [344. 反转字符串](https://leetcode-cn.com/problems/reverse-string/) 170 | 171 | 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 172 | 173 | 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 174 | 175 | 你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 176 | 177 | 178 | 179 | 示例 1: 180 | 181 | 输入:["h","e","l","l","o"] 182 | 输出:["o","l","l","e","h"] 183 | 示例 2: 184 | 185 | 输入:["H","a","n","n","a","h"] 186 | 输出:["h","a","n","n","a","H"] 187 | 188 | 189 | 190 | **解题思路** 191 | 192 | 通过双指针方式实现,第一个指针指向数组头,第二个指针指向数组尾,交换指针出的元素,然后左指针+1,右指针-1.直到左指针大于等于右指针停止。 193 | 194 | ``` 195 | public void reverseString(char[] s) { 196 | int p1 = 0, p2 = s.length - 1; 197 | while (p1 < p2) { 198 | swap(s, p1, p2); 199 | p1++; 200 | p2--; 201 | } 202 | } 203 | 204 | public void swap(char[] c, int p1, int p2) { 205 | char temp = c[p1]; 206 | c[p1] = c[p2]; 207 | c[p2] = temp; 208 | } 209 | ``` 210 | 211 | #### [557. 反转字符串中的单词 III](https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/) 212 | 213 | 给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。 214 | 215 | 示例: 216 | 217 | 输入:"Let's take LeetCode contest" 218 | 输出:"s'teL ekat edoCteeL tsetnoc" 219 | 220 | 221 | 提示: 222 | 223 | 在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。 224 | 225 | 226 | 227 | **解题思路** 228 | 229 | ```java 230 | public String reverseWords(String s) { 231 | char[] chars = s.toCharArray(); 232 | int p1 = 0, p2 = 0, index = 0; 233 | for (int i = 0; i < chars.length; i++) { 234 | if (chars[index] == ' ' || i == chars.length - 1) { 235 | p2 = index; 236 | reverseWord(chars, p1, p2); 237 | p1 = index + 2; 238 | } 239 | index++; 240 | } 241 | 242 | return new String(chars); 243 | } 244 | 245 | private void reverseWord(char[] chars, int p1, int p2) { 246 | while (p1 < p2) { 247 | char temp = chars[p1]; 248 | chars[p1] = chars[p2]; 249 | chars[p2] = temp; 250 | p1++; 251 | p2--; 252 | } 253 | } 254 | ``` 255 | 256 | #### [567. 字符串的排列](https://leetcode-cn.com/problems/permutation-in-string/) 257 | ```java 258 | 259 | /** 260 | * s1是较短的字符串,如果s1的排列是s2的字串,那么s1中各个字符的个数与s2 261 | * 某个字串各个字符串的个数是相等的,则条件成立。因此可以申请两个长度为26的数组 262 | * 第一个数组统计s1中各个字符串的个数,第二个数组统计s2长度为s1.length的字串的各个字符个数。 263 | * 如果s2中存在条件成立的字串,那么两个数组是相等的。 264 | */ 265 | public static boolean checkInclusion(String s1, String s2) { 266 | if (s1.length() > s2.length()) { 267 | return false; 268 | } 269 | if (s1.equals(s2)) { 270 | return true; 271 | } 272 | int[] ints1 = new int[26]; 273 | int[] ints2 = new int[26]; 274 | for (int i = 0; i < s1.length(); i++) { 275 | ++ints1[s1.charAt(i) - 'a']; 276 | ++ints2[s2.charAt(i) - 'a']; 277 | } 278 | if (Arrays.equals(ints1, ints2)) { 279 | return true; 280 | } 281 | for (int i = s1.length(); i < s2.length(); i++) { 282 | ++ints2[s2.charAt(i) - 'a']; 283 | --ints2[s2.charAt(i - s1.length()) - 'a']; 284 | if (Arrays.equals(ints1, ints2)) { 285 | return true; 286 | } 287 | } 288 | return false; 289 | } 290 | ``` 291 | -------------------------------------------------------------------------------- /post/屏幕刷新机制.md: -------------------------------------------------------------------------------- 1 | ## 一、屏幕刷新机制概述 2 | 3 | 在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display负责把buffer里的数据呈现到屏幕上。很多时候,我们可以把CPU、GPU放在一起说,那么就是包括2部分,CPU/GPU 和display。 4 | 5 | - tearing: 一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感 jank: 一个帧在屏幕上连续出现2次 6 | - lag:从用户体验来说,就是点击下去到呈现效果之间存在延迟 7 | - 屏幕刷新频率:一秒内屏幕刷新多少次,或者说一秒内显示了多少帧的图像,屏幕扫描是从左到右,从上到下执行的。显示器并不是一整个屏幕一起输出的,而是一个个像素点输出的,我们看不出来,是因为速度太快了,人有视觉暂留,所以看不出来。 8 | 9 | 为什么会产生tearing? 10 | 11 | 显示过程,简单的说就是CPU/GPU准备好数据,存入buffer,display从buffer中取出数据,然后一行一行显示出来。display处理的频率是固定的,比如每隔60ms显示完一帧,但是CPU/GPU写数据是不可控的,所以会出现有些数据根本没显示出来就被重写了,buffer里的数据可能是来自不同的帧的, 所以出现画面“割裂”。 12 | 13 | 怎么解决tearing问题? 14 | 15 | 可以使用双缓存来解决tearing问题,基本原理就是采用两块buffer。一块back buffer用于CPU/GPU后台绘制,另一块framebuffer则用于显示,当back buffer准备就绪后,它们才进行交换。不可否认,double buffering可以在很大程度上降低screen tearing错误。 16 | 17 | double buffering存在什么问题? 18 | 19 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200819205135422.png#pic_center#pic_center) 20 | 21 | 以时间的顺序来看下将会发生的异常: 22 | 23 | - Step1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,而且赶在Display显示下一帧前完成 24 | - Step2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,正常显示第1帧 25 | - Step3. 由于某些原因,比如CPU资源被占用,系统没有及时地开始处理第2帧,直到第2个VSync快来前才开始处理 26 | - Step4. 第2个VSync来时,由于第2帧数据还没有准备就绪,显示的还是第1帧。这种情况被Android开发组命名为“Jank”。 27 | - Step5. 当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync。 28 | 29 | 所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因大家应该都看到了,就是CPU没有及时地开始着手处理第2帧的渲染工作,以致“延误军机”。 Android在4.1之前一直存在这个问题。 30 | 31 | Android系统是如何解决双缓存存在的问题的? 32 | 33 | 为了优化显示性能,android 4.1版本对Android Display系统进行了重构,实现了Project Butter,引入了三个核心元素,即VSYNC、Triple Buffer和Choreographer。 34 | 35 | 36 | ## 二、UI渲染流程 37 | 38 | ### 1. scheduleTraversals() 39 | 40 | 界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务。 41 | 42 | scheduleTraversals() 会先过mTraversalScheduled滤掉同一帧内的重复调用,确保同一帧内只需要安排一次遍历绘制 View 树的任务,遍历过程中会将所有需要刷新的 View 进行重绘。 43 | 44 | scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作。 45 | 46 | 发完同步屏障后 scheduleTraversals() 将 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法。 47 | 48 | **简述:View的刷新都会从ViewRootImpl中的 scheduleTraversals() ,这个方法里边首先会发送一个同步屏障,阻塞同步消息,接下来通过mChoreographer post出一个Runnable** 49 | 50 | 代码如下: 51 | 52 | 53 | ```java 54 | // ViewRootImpl 55 | @UnsupportedAppUsage 56 | void scheduleTraversals() { 57 | if (!mTraversalScheduled) { 58 | mTraversalScheduled = true; 59 | mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); 60 | mChoreographer.postCallback( 61 | Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 62 | if (!mUnbufferedInputDispatch) { 63 | scheduleConsumeBatchedInput(); 64 | } 65 | notifyRendererOfFramePending(); 66 | pokeDrawLockIfNeeded(); 67 | } 68 | } 69 | ``` 70 | 71 | 72 | 73 | ### 2. Choreographer与Vsync 74 | postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后会调用一个native 层方法,这个native方法是用来向底层订阅下一个屏幕刷新信号Vsync,当下一个屏幕刷新信号发出时,底层就会通过 FrameDisplayEventReceiver 的onVsync() 方法来通知上层 app。onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的异步消息。doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作。 75 | 76 | **简述:mChoreographer中会将Runable放入执行队列,然后等待接受Vsync的信号,信号到来时通过FrameDisplayEventReceiver调用这个Runable,并最终执行ViewRootImpl中的doTraversal方法** 77 | 78 | ```java 79 | // Choreographer 80 | public void postCallback(int callbackType, Runnable action, Object token) { 81 | postCallbackDelayed(callbackType, action, token, 0); 82 | } 83 | 84 | public void postCallbackDelayed(int callbackType, 85 | Runnable action, Object token, long delayMillis) { 86 | postCallbackDelayedInternal(callbackType, action, token, delayMillis); 87 | } 88 | 89 | private void postCallbackDelayedInternal(int callbackType, 90 | Object action, Object token, long delayMillis) { 91 | synchronized (mLock) { 92 | final long now = SystemClock.uptimeMillis(); 93 | final long dueTime = now + delayMillis; 94 | mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); 95 | 96 | if (dueTime <= now) { 97 | scheduleFrameLocked(now); 98 | } else { 99 | Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); 100 | msg.arg1 = callbackType; 101 | msg.setAsynchronous(true); 102 | mHandler.sendMessageAtTime(msg, dueTime); 103 | } 104 | } 105 | } 106 | 107 | private final class FrameDisplayEventReceiver extends DisplayEventReceiver 108 | implements Runnable { 109 | 110 | public FrameDisplayEventReceiver(Looper looper, int vsyncSource) { 111 | super(looper, vsyncSource); 112 | } 113 | @Override 114 | public void onVsync(long timestampNanos, long physicalDisplayId, int frame) { 115 | 116 | long now = System.nanoTime(); 117 | if (timestampNanos > now) { 118 | timestampNanos = now; 119 | } 120 | 121 | if (mHavePendingVsync) { 122 | } else { 123 | mHavePendingVsync = true; 124 | } 125 | mTimestampNanos = timestampNanos; 126 | mFrame = frame; 127 | Message msg = Message.obtain(mHandler, this); 128 | msg.setAsynchronous(true); 129 | mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); 130 | } 131 | 132 | @Override 133 | public void run() { 134 | mHavePendingVsync = false; 135 | doFrame(mTimestampNanos, mFrame); 136 | } 137 | } 138 | 139 | 140 | ``` 141 | 142 | 143 | 144 | ### 3. 开启绘制流程 145 | doTraversal()中首先移除同步屏障,再会调用performTraversals() 方法根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View。等到下一个Vsync信号到达,将上面计算好的数据渲染到屏幕上,同时如果有必要开始下一帧的数据处理。 146 | 147 | **简述:doTraversal()中首先移除同步屏障,然后调用performTraversals()方法根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程** 148 | 149 | ```java 150 | // ViewRootImpl 151 | void doTraversal() { 152 | if (mTraversalScheduled) { 153 | mTraversalScheduled = false; 154 | mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); 155 | 156 | if (mProfile) { 157 | Debug.startMethodTracing("ViewAncestor"); 158 | } 159 | // 开启View的绘制流程 160 | performTraversals(); 161 | 162 | if (mProfile) { 163 | Debug.stopMethodTracing(); 164 | mProfile = false; 165 | } 166 | } 167 | } 168 | ``` 169 | 170 | 171 | 172 | [Android 屏幕刷新机制](https://juejin.cn/post/6844904050496897031) 173 | 174 | [“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!](https://juejin.cn/post/6863756420380196877) 175 | 176 | https://blog.csdn.net/chenzhiqin20/article/details/8628952 177 | 178 | ## 三、相关面试题 179 | 180 | ### 1.丢帧一般是什么原因引起的? 181 | 182 | 1)布局过于复杂或者存在大量OverDraw,致使解析绘制流程事件过长,CPU/GPU不能在一个刷新周期内完成数据的计算和绘制造成丢帧。 183 | 184 | 2)主线程有耗时操作,耽误了View的绘制。 185 | 186 | 187 | ### 2.Android刷新频率60帧/秒,每隔16ms调onDraw绘制一次? 188 | 189 | 显示器每隔16ms会刷新一次,但是只有用户发起重绘请求才会调用onDraw。 190 | 191 | ### 3.onDraw完之后屏幕会马上刷新么? 192 | 193 | 不会,会等待下一个Vsync信号。 194 | 195 | ### 4.如果界面没有重绘,还会每隔16ms刷新屏幕么? 196 | 197 | 对于底层显示器,每间隔16.6ms接收到VSYNC信号时,就会用buffer中数据进行一次显示。所以一定会刷新。(用的旧的数据) 198 | 199 | ### 5.如果在屏幕快刷新的时候才去onDraw绘制会丢帧么 200 | 201 | 代码发起的View的重绘不会马上执行,会等待下次VSYNC信号来的时候才开始。什么时候绘制没影响。 202 | 203 | ## 6.如果快速调用10次requestLayout,会调用10次onDraw吗? 204 | 205 | mTraversalScheduled这个变量是为了过滤一帧内重复的刷新请求,初始值是false,在开始这一帧的绘制流程时候也会重新置为false(doTraversal()中,一会儿分析),同时,在取消遍历绘制 View 操作 unscheduleTraversals() 里也会设置为false。也就是说一般情况下在开始这一帧的正式绘制前,在这期间重复调用scheduleTraversals()只有一次会生效。这么设计的原因是前面已经说了和ViewRootImpl绑定的是DecorView,当刷新时候会对整个DecorView进行一次处理,所以不同view触发的scheduleTraversals()作用都是一样的,所以在这一帧里面只要有一次和多次刷新请求效果是一样的。 206 | 207 | ```java 208 | void scheduleTraversals() { 209 | if (!mTraversalScheduled) { 210 | mTraversalScheduled = true; //防止多次调用 211 | // 发送同步屏障 212 | mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); 213 | mChoreographer.postCallback( 214 | Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 215 | ... 216 | } 217 | } 218 | 219 | void doTraversal() { 220 | if (mTraversalScheduled) { 221 | mTraversalScheduled = false; 222 | // 移除同步屏障 223 | mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); 224 | ... 225 | performTraversals(); 226 | ... 227 | } 228 | } 229 | ``` 230 | 231 | 232 | ### 7.View 刷新机制 233 | 234 | 当我们调用 View 的 invalidate 时刷新视图时,它会调到 ViewRootImp 的 invalidateChildInParent,这个方法首先会 checkThread 检查是否是主线程,然后调用其 scheduleTraversals 方法。这个方法就是视图绘制的开始,但是它并不是立即去执行 View 的三大流程,而是先往消息队列里面添加一个同步屏障,然后在往 Choreographer 里面注册一个 TRAVERSAL 的回调。在下一次 Vsync 信号到来时,会去执行 doTraversals 方法。 235 | 236 | Choreographer 主要是用来接收 Vsync 信号,并且在信号到来时去处理一些回调事件。事件类型有四种,分别是 Input、Animation、Traversal、Commit。在 Vsync 信号到来时,会依次处理这些事件,前三种比较好理解,第四种 Commit 是用来执行组件的 onTrimMemory 函数的。Choreographer 是通过 FrameDisplayEventReceiver 来监听底层发出的 Vsync 信号的,然后在它的回调函数 onVsync 中去处理,首先会计算掉帧,然后就是 doCallbacks 处理上面所说的回调事件。 237 | 238 | Vsync 信号可以理解为底层硬件的一个消息脉冲,它每 16ms 发出一次,它有两种方式发出,一种是 HWComposer 硬件产生,一种是用软件模拟,即 VsyncThread。不管使用哪种方式,都统一由 DispSyncThread 进行分发。 239 | 240 | [View 体系相关口水话](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/%E5%8F%A3%E6%B0%B4%E8%AF%9D/View%20%E4%BD%93%E7%B3%BB%E7%9B%B8%E5%85%B3%E5%8F%A3%E6%B0%B4%E8%AF%9D.md) -------------------------------------------------------------------------------- /post/屏幕适配.md: -------------------------------------------------------------------------------- 1 | ## 屏幕适配相关参数 2 | 3 | ### 1.dpi的计算公式 4 | 5 | ![dpi计算公式](https://gitee.com/zhpanvip/images/raw/master/project/article/screen/dpi.png) 6 | 7 | ![dpi计算公式](https://gitee.com/zhpanvip/images/raw/master/project/article/screen/dpi_screen.png) 8 | 9 | ### 2.density 10 | 11 | density表示1dp有多少像素,它的计算公式如下: 12 | 13 | > density = dpi / 160; 14 | 15 | ### 3.dp与px的关系 16 | 17 | 根据density的含义可以得出px的计算方式: 18 | 19 | > px = dp * density; 20 | 21 | ## 今日头条屏幕适配方案原理 22 | 23 | 今日头条适配方案默认项目中只能以宽作为基准,进行适配。我们根据density的计算公式,以设计稿的宽度作为标准,可得出如下公式: 24 | 25 | > 设计图总宽度(单位为 dp) = 当前设备屏幕总宽度(单位为像素)/ density 26 | 27 | 上述公式中因为设计稿的宽度是不变的,当前设备屏幕总宽度也是无法改变的,因此只能通过修改density的值来使得等式两边相等。那么可以得出以下公式: 28 | 29 | > density = 当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) 30 | 31 | 在求得density的之后,通过代码来修改系统的density值即可完成适配。 32 | 33 | 34 | ### 为什么使用dp无法适配所有屏幕? 35 | 36 | 举个例子 37 | 一个5英寸的手机,分辨率为1080*1920,根据公式计算出dpi为440,density为2.75,因此这款手机的宽度为1080/2.75=392.73dp 38 | 一个5英寸的手机,分辨率为1280*720 ,根据公式计算出dpi为293,density为4.3 ,因此这款手机的宽度为1280/4.3 = 297dp 39 | 因此,如果在第二款手机上设置宽度为297dp,刚好充满屏幕,而如果在第一款手机上则无法充满屏幕。因此可以看出dp并不能适配所有屏幕。 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /post/排序算法.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```java 4 | public static int[] swap(int[] nums, int i, int j) { 5 | int temp = nums[i]; 6 | nums[i] = nums[j]; 7 | nums[j] = temp; 8 | return nums; 9 | } 10 | ``` 11 | 12 | ### 1.快速排序 13 | 14 | 核心的思路是取第一个元素(或者最后一个元素)作为分界点,把整个数组分成左右两侧,左边的元素小于或者等于分界点元素,而右边的元素大于分界点元素,然后把分界点移到中间位置,对左右子数组分别进行递归,最后就能得到一个排序完成的数组。当子数组只有一个或者没有元素的时候就结束这个递归过程。 15 | 16 | ```java 17 | 18 | public static void quickSort(int[] nums, int left, int right) { 19 | if (left > right) return; 20 | int key = nums[left]; 21 | int l = left; 22 | int r = right; 23 | while (l < r) { 24 | while (nums[r] >= key && l < r) 25 | r--; 26 | while (nums[l] <= key && l < r) 27 | l++; 28 | if (l < r) 29 | swap(nums, r, l); 30 | } 31 | 32 | nums[left] = nums[r]; 33 | nums[r] = key; 34 | 35 | quickSort(nums, left, r - 1); 36 | quickSort(nums, r + 1, right); 37 | } 38 | ``` 39 | ### 2.归并排序 40 | 41 | 归并排序是建立在归并操作上的一种有效排序算反,采用的是分治思想。这一算法充分利用了完全二叉树深度是log2(n+1)的特性,因此效率比较高。其基本原理如下: 42 | 43 | 对于给定的一组记录,利用递归与分支技术将数据划分为越来越小的半子表,再对班子表排序,最后利用递归方法将排序好的半子表合并为越来越大的有序表。 44 | 45 | 经过第一轮比较后得到最小的记录,然后将该记录的位置与第一个记录的位置交换;接着对不包括第一个记录以外的其他记录进行第二次比较,得到最小记录与第二个位置记录交换;重复这个过程直到进行比较的记录剩下一个为止。 46 | 47 | [动画演示](https://www.runoob.com/wp-content/uploads/2019/03/mergeSort.gif) 48 | 49 | ```java 50 | public static void mergeSort(int[] a, int left, int right) { 51 | int mid = left + (right - left) / 2; 52 | if (left < right) { 53 | mergeSort(a, left, mid); 54 | mergeSort(a, mid + 1, right); 55 | merge(a, left, mid, right); 56 | } 57 | } 58 | 59 | public static void merge(int[] a, int left, int mid, int right) { 60 | int[] temp = new int[right - left + 1]; 61 | int leftPointer = left; 62 | int rightPointer = mid + 1; 63 | int i = 0; 64 | while (leftPointer <= mid && rightPointer <= right) { 65 | if (a[leftPointer] <= a[rightPointer]) { 66 | temp[i++] = a[leftPointer++]; 67 | } else { 68 | temp[i++] = a[rightPointer++]; 69 | } 70 | } 71 | while (leftPointer <= mid) { 72 | temp[i++] = a[leftPointer++]; 73 | } 74 | while (rightPointer <= right) { 75 | temp[i++] = a[rightPointer++]; 76 | } 77 | if (temp.length >= 0) { 78 | System.arraycopy(temp, 0, a, left, temp.length); 79 | } 80 | } 81 | ``` 82 | [参考连接](https://blog.csdn.net/jianyuerensheng/article/details/51262984) 83 | 84 | ### 3.冒泡排序 85 | 86 | 冒泡排序从左到右依次比较两个相邻的元素,如果前一个元素比较大,就把前一个元素和后一个元素交换位置,完成一趟循环后保证了最大的元素在最后一位。接下来进行第二趟排序,第二趟排序完成后第二大的元素在倒数第二位。依次遍历直至整个数组排序完成。 87 | 88 | 冒泡排序的时间复杂度是O(n2),空间复杂度为 89 | 90 | ```java 91 | 92 | public static void bubbleSort(int[] nums) { 93 | for (int i = 0; i < nums.length; i++) { 94 | for (int j = 0; j < nums.length - 1 - i; j++) { 95 | if (nums[j] > nums[j + 1]) { 96 | swap(nums, j, j + 1); 97 | } 98 | } 99 | } 100 | } 101 | ``` 102 | 103 | -------------------------------------------------------------------------------- /post/查找算法.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### [Leetcode 704. 二分查找](https://leetcode-cn.com/problems/binary-search/) 4 | 5 | ```java 6 | public static int binSearch(int[] arr, int key) { 7 | int left = 0; 8 | int right = arr.length - 1; 9 | int mid = 0; 10 | while (left <= right) { 11 | // 如果left与right都超过了int最大值的1/2,那么(left+right)会发生溢出, 12 | // 因此不能使用(left+right)/2求mid,而是应该用left+(right-left)/2 13 | mid = left + (right - left) / 2; 14 | if (key < arr[mid]) { 15 | right = mid - 1; 16 | } else if (key > arr[mid]) { 17 | left = mid + 1; 18 | } else { 19 | return mid; 20 | } 21 | } 22 | return -1; 23 | } 24 | ``` 25 | ### [剑指 Offer 53 - II. 0~n-1中缺失的数字](https://leetcode-cn.com/problems/que-shi-de-shu-zi-lcof/) 26 | ```java 27 | /** 28 | * 解法1:遍历数组,查找缺失的数字 29 | */ 30 | public int missingNumber(int[] nums) { 31 | for (int i = 0; i < nums.length; i++) { 32 | if (nums[i] != i) { 33 | return i; 34 | } 35 | } 36 | return nums.length; 37 | } 38 | 39 | /** 40 | * 解法二:使用二分法查找丢失数字 41 | */ 42 | public int missingNumberBinarySearch(int[] nums) { 43 | int left = 0, right = nums.length - 1; 44 | while (left <= right) { 45 | int mid = left + (left - right) / 2; 46 | if (nums[mid] == mid) { // 说明缺失的数字不在前半段数组 47 | left = mid + 1; 48 | } else { 49 | right = mid - 1; 50 | } 51 | } 52 | return left; 53 | } 54 | ``` 55 | 56 | ### [35. 搜索插入位置](https://leetcode-cn.com/problems/search-insert-position) 57 | ```java 58 | public int searchInsert(int[] nums, int target) { 59 | int left = 0, right = nums.length - 1, mid; 60 | while (left <= right) { 61 | mid = left + (right - left) / 2; 62 | if (target < nums[mid]) { 63 | right = mid - 1; 64 | } else if (target > nums[mid]) { 65 | left = mid + 1; 66 | } else { 67 | return mid; 68 | } 69 | } 70 | return left; 71 | } 72 | ``` 73 | ### [35. 第一个错误的版本](https://leetcode-cn.com/problems/first-bad-version) 74 | ```java 75 | public int firstBadVersion(int n) { 76 | int left = 1, right = n, mid; 77 | while (left < right) { 78 | // 如果left与right都超过了int最大值的1/2,那么(left+right)会发生溢出, 79 | // 因此不能使用(left+right)/2求mid,而是应该用left+(right-left)/2 80 | mid = left + ((right - left) >> 1); 81 | if (isBadVersion(mid)) { 82 | right = mid; 83 | } else { 84 | left = mid + 1; 85 | } 86 | } 87 | return left; 88 | } 89 | ``` -------------------------------------------------------------------------------- /post/电池电量优化.md: -------------------------------------------------------------------------------- 1 | ## 电池电量优化 -------------------------------------------------------------------------------- /post/算法:动态规划.md: -------------------------------------------------------------------------------- 1 | #### [5. 最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/) 2 | 3 | ```java 4 | public String longestPalindrome(String s) { 5 | int len = s.length(); 6 | if (len < 2) { 7 | return s; 8 | } 9 | int maxLen = 1; 10 | int begin = 0; 11 | boolean[][] dp = new boolean[len][len]; 12 | for (int i = 0; i < len; i++) { 13 | // 长度为1的字串一定是回文串 14 | dp[i][i] = true; 15 | } 16 | char[] chars = s.toCharArray(); 17 | // l表示枚举字符串长度,长度应该小于等于s的长度 18 | for (int l = 2; l <= len; l++) { 19 | // i表示子串的起点,i表示坐标,所以只能小于s的长度 20 | for (int i = 0; i < len; i++) { 21 | // j 表示字串的终点 22 | int j = i + l - 1; 23 | // 超过字符串的长度,重新开始 24 | if (j >= len) { 25 | break; 26 | } 27 | // 只有起点和终点相等,i到j的这个字串才有可能是回文串 28 | if (chars[i] == chars[j]) { 29 | if (j - i <= 2) { 30 | // 当字串长度为2或者3时,起点和终点相等,那么一定是回文串 31 | dp[i][j] = true; 32 | } else { 33 | // 当字串长度大于3时,除了起点与终点相等之外,中间的字串必须也要满足是回文串 34 | dp[i][j] = dp[i + 1][j - 1]; 35 | } 36 | } 37 | if (dp[i][j] && j - i + 1 > maxLen) { 38 | maxLen = j - i + 1; 39 | begin = i; 40 | } 41 | } 42 | } 43 | return s.substring(begin, begin + maxLen); 44 | } 45 | ``` 46 | 47 | #### [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) 48 | 49 | - 解法一:动态规划 50 | 51 | ```java 52 | public int climbStairs2(int n) { 53 | int[] dp = new int[n + 1]; 54 | // 0阶有1种走法 55 | dp[0] = 1; 56 | // 1阶1种走法 57 | dp[1] = 1; 58 | for (int i = 2; i <= n; i++) { 59 | // 到达第i阶可能是一步到达,也可能是两步到达,因此到第i阶的走法应该是i-1和i-2的总和。 60 | dp[i] = dp[i - 1] + dp[i - 2]; 61 | } 62 | return dp[n]; 63 | } 64 | ``` 65 | 66 | - 解法二:递归 67 | 68 | ```java 69 | public int climbStairs1(int n) { 70 | if (n == 1) return 1; 71 | if (n == 2) return 2; 72 | if (map.containsKey(n)) { 73 | return map.get(n); 74 | } 75 | int sum = climbStairs1(n - 1) + climbStairs1(n - 2); 76 | map.put(n, sum); 77 | return sum; 78 | } 79 | ``` 80 | 81 | #### [121. 买卖股票的最佳时机](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/) 82 | 83 | - 解法一:动态规划 84 | 85 | ```java 86 | public int maxProfit(int[] prices) { 87 | int len = prices.length; 88 | // 特殊判断 89 | if (len < 2) { 90 | return 0; 91 | } 92 | int[][] dp = new int[len][2]; 93 | 94 | // dp[i][0] 下标为 i 这天结束的时候,不持股,手上拥有的现金数 95 | // dp[i][1] 下标为 i 这天结束的时候,持股,手上拥有的现金数 96 | 97 | // 初始化:不持股显然为 0,持股就需要减去第 1 天(下标为 0)的股价 98 | dp[0][0] = 0; 99 | dp[0][1] = -prices[0]; 100 | 101 | // 从第 2 天开始遍历 102 | for (int i = 1; i < len; i++) { 103 | dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); 104 | dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); 105 | } 106 | return dp[len - 1][0]; 107 | } 108 | ``` 109 | 110 | - 解法二:暴力法 111 | 112 | ```java 113 | public int maxProfit(int[] prices) { 114 | int maxProfit = 0; 115 | for (int i = 0; i < prices.length; i++) { 116 | for (int j = i + 1; j < prices.length; j++) { 117 | int res = prices[j] - prices[i]; 118 | if (res > 0) { 119 | maxProfit = Math.max(maxProfit, res); 120 | } 121 | } 122 | } 123 | return maxProfit; 124 | } 125 | ``` 126 | 127 | - 解法三:一次循环 128 | 129 | ```java 130 | public int maxProfit1(int[] prices) { 131 | if(prices.length == 0){ 132 | return 0; 133 | } 134 | int minPrice = prices[0], maxProfile = 0; 135 | for (int i = 1; i < prices.length; i++) { 136 | if (prices[i] < minPrice) { 137 | minPrice = prices[i]; 138 | } else { 139 | maxProfile = Math.max(maxProfile,prices[i] - minPrice); 140 | } 141 | } 142 | return maxProfile; 143 | } 144 | ``` 145 | 146 | -------------------------------------------------------------------------------- /post/线上性能监控2-Matrix实现原理.md: -------------------------------------------------------------------------------- 1 | ## Matrix实现原理 2 | 3 | [微信APM-Matrix 原理篇-Matrix TraceCanary源码分析](https://www.jianshu.com/p/e8b6db3c63eb) 4 | 5 | [Matrix Android TraceCanary](https://github.com/Tencent/matrix/wiki/Matrix-Android-TraceCanary) -------------------------------------------------------------------------------- /post/组件化WebView架构搭建.md: -------------------------------------------------------------------------------- 1 | 组件化WebView架构搭建 -------------------------------------------------------------------------------- /post/观察者模式.md: -------------------------------------------------------------------------------- 1 | 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。 2 | 3 | 以订阅微信公众号为例: 4 | 5 | > 用户在微信订阅了微信公众号后,会收到公众号的消息推送。如果用户取消了订阅,那么就不会再收到推送。 6 | 7 | 8 | 9 | ### 1.观察者Observer抽象层 10 | 11 | 观察者订阅公众号后会监听公众号推送的消息,推送后观察者会收到更新。可以抽象出一个观察者接口: 12 | 13 | ```java 14 | /** 15 | * 观察者抽象层 16 | */ 17 | public interface Observer { 18 | void update(String obj); 19 | } 20 | ``` 21 | 22 | 23 | 24 | ### 2.观察者实现 25 | 26 | 用户实现观察者接口,收到消息后将消息打印: 27 | 28 | ```java 29 | /** 30 | * 观察者实现 31 | */ 32 | public class UserObserver implements Observer { 33 | 34 | private String name; 35 | private String message; 36 | 37 | public UserObserver(String name) { 38 | this.name = name; 39 | } 40 | 41 | @Override 42 | public void update(String message) { 43 | this.message = message; 44 | readMessage(); 45 | } 46 | 47 | private void readMessage() { 48 | System.out.println(name + "收到一条消息:" + message); 49 | } 50 | } 51 | ``` 52 | 53 | 54 | 55 | ### 3.被观察者抽象层 56 | 57 | 被观察者提供订阅、取消订阅、发布消息、以及发布消息后通知观察者的功能: 58 | 59 | ```java 60 | /** 61 | * 被观察者抽象层 62 | */ 63 | public interface Observable { 64 | void addObserver(Observer observer); 65 | 66 | void removeObserver(Observer observer); 67 | 68 | void notifyObservers(); 69 | 70 | // 发布消息 71 | void pushMessage(String message); 72 | } 73 | ``` 74 | 75 | 76 | 77 | ### 4.被观察者实现 78 | 79 | 定义一个公众号的被观察者,并实现Observable: 80 | 81 | ```java 82 | /** 83 | * 被观察者 84 | */ 85 | public class WechatObservable implements Observable { 86 | 87 | private final List list = new ArrayList<>(); 88 | private String message; 89 | 90 | @Override 91 | public void addObserver(Observer observer) { 92 | list.add(observer); 93 | } 94 | 95 | @Override 96 | public void removeObserver(Observer observer) { 97 | list.remove(observer); 98 | } 99 | 100 | 101 | @Override 102 | public void notifyObservers() { 103 | for (Observer observer : list) { 104 | observer.update(message); 105 | } 106 | } 107 | 108 | @Override 109 | public void pushMessage(String message) { 110 | this.message = message; 111 | // 通知订阅的用户 112 | notifyObservers(); 113 | } 114 | } 115 | ``` 116 | 117 | 118 | 119 | ### 5.测试代码 120 | 121 | ```java 122 | public class ObserverTest { 123 | 124 | public static void main(String[] args) { 125 | WechatObservable wechatObservable = new WechatObservable(); 126 | UserObserver ryan = new UserObserver("Ryan"); 127 | UserObserver frank = new UserObserver("Mike"); 128 | 129 | wechatObservable.addObserver(ryan); 130 | wechatObservable.addObserver(frank); 131 | 132 | wechatObservable.pushMessage("第三次分配来了!实现共同富裕再放大招!"); 133 | } 134 | } 135 | ``` 136 | 137 | 打印结果: 138 | ``` 139 | Ryan收到一条消息:第三次分配来了!实现共同富裕再放大招! 140 | Mike收到一条消息:第三次分配来了!实现共同富裕再放大招! 141 | ``` -------------------------------------------------------------------------------- /post/计算机网络.md: -------------------------------------------------------------------------------- 1 | ## 一、OSI七层协议简介 2 | 3 | 4 | 5 | OSI参考模型为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层 6 | 7 | 8 | 9 | 10 | 11 | ![OSI参考模型与TCP/IP参考模型](https://img-blog.csdnimg.cn/img_convert/6a8e6e8545002f92160f0230c217ac6f.png#pic_center) 12 | 13 | 14 | 15 | ### 1.物理层 16 | 17 | 在OSI参考模型中,物理层(Physical Layer)是参考模型的最低层,也是OSI模型的第一层。 18 | 19 | 物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输。 20 | 21 | 物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。 22 | 23 | ### 2.数据链路层 24 | 25 | 数据链路层(Data Link Layer)是OSI模型的第二层,负责建立和管理节点间的链路。该层的主要功能是:通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路。 26 | 27 | 在计算机网络中由于各种干扰的存在,物理链路是不可靠的。因此,这一层的主要功能是在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。 28 | 29 | 该层通常又被分为介质访问控制(MAC)和逻辑链路控制(LLC)两个子层。 30 | 31 | MAC子层的主要任务是解决共享型网络中多用户对信道竞争的问题,完成网络介质的访问控制; 32 | 33 | LLC子层的主要任务是建立和维护网络连接,执行差错校验、流量控制和链路控制。 34 | 35 | 数据链路层的具体工作是接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层;并且,还负责处理接收端发回的确认帧的信息,以便提供可靠的数据传输。 36 | 37 | ### 3.网络层 38 | 39 | 网络层(Network Layer)是OSI模型的第三层,它是OSI参考模型中最复杂的一层,也是通信子网的最高一层。它在下两层的基础上向资源子网提供服务。其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。该层控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。 40 | 41 | 一般地,数据链路层是解决同一网络内节点之间的通信,而网络层主要解决不同子网间的通信。例如在广域网之间通信时,必然会遇到路由(即两节点间可能有多条路径)选择问题。 42 | 43 | 在实现网络层功能时,需要解决的主要问题如下: 44 | 45 | - 寻址:数据链路层中使用的物理地址(如MAC地址)仅解决网络内部的寻址问题。在不同子网之间通信时,为了识别和找到网络中的设备,每一子网中的设备都会被分配一个唯一的地址。由于各子网使用的物理技术可能不同,因此这个地址应当是逻辑地址(如IP地址)。 46 | 47 | - 交换:规定不同的信息交换方式。常见的交换技术有:线路交换技术和存储转发技术,后者又包括报文交换技术和分组交换技术。 48 | 49 | - 路由算法:当源节点和目的节点之间存在多条路径时,本层可以根据路由算法,通过网络为数据分组选择最佳路径,并将信息从最合适的路径由发送端传送到接收端。 50 | 51 | - 连接服务:与数据链路层流量控制不同的是,前者控制的是网络相邻节点间的流量,后者控制的是从源节点到目的节点间的流量。其目的在于防止阻塞,并进行差错检测。 52 | 53 | ### 4.传输层 54 | 55 | OSI下3层的主要任务是数据通信,上3层的任务是数据处理。而传输层(Transport Layer)是OSI模型的第4层。因此该层是通信子网和资源子网的接口和桥梁,起到承上启下的作用。 56 | 该层的主要任务是:向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输。传输层的作用是向高层屏蔽下层数据通信的细节,即向用户透明地传送报文。该层常见的协议: 57 | 58 | TCP/IP中的TCP协议、Novell网络中的SPX协议和微软的NetBIOS/NetBEUI协议。 59 | 60 | 传输层提供会话层和网络层之间的传输服务,这种服务从会话层获得数据,并在必要时,对数据进行分割。然后,传输层将数据传递到网络层,并确保数据能正确无误地传送到网络层。因此,传输层负责提供两节点之间数据的可靠传送,当两节点的联系确定之后,传输层则负责监督工作。综上,传输层的主要功能如下: 61 | 62 | 传输连接管理:提供建立、维护和拆除传输连接的功能。传输层在网络层的基础上为高层提供“面向连接”和“面向无接连”的两种服务。 63 | 64 | 处理传输差错:提供可靠的“面向连接”和不太可靠的“面向无连接”的数据传输服务、差错控制和流量控制。在提供“面向连接”服务时,通过这一层传输的数据将由目标设备确认,如果在指定的时间内未收到确认信息,数据将被重发。 65 | 66 | 监控服务质量。 67 | 68 | ### 5.会话层 69 | 70 | 会话层(Session Layer)是OSI模型的第5层,是用户应用程序和网络之间的接口,主要任务是:向两个实体的表示层提供建立和使用连接的方法。将不同实体之间的表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理。 71 | 72 | 用户可以按照半双工、单工和全双工的方式建立会话。当建立会话时,用户必须提供他们想要连接的远程地址。而这些地址与MAC(介质访问控制子层)地址或网络层的逻辑地址不同,它们是为用户专门设计的,更便于用户记忆。域名(DN)就是一种网络上使用的远程地址例如:www.3721.com就是一个域名。会话层的具体功能如下: 73 | 74 | 会话管理:允许用户在两个实体设备之间建立、维持和终止会话,并支持它们之间的数据交换。例如提供单方向会话或双向同时会话,并管理会话中的发送顺序,以及会话所占用时间的长短。 75 | 会话流量控制:提供会话流量控制和交叉会话功能。 76 | 77 | 寻址:使用远程地址建立会话连接。 78 | 79 | 出错控制:从逻辑上讲会话层主要负责数据交换的建立、保持和终止,但实际的工作却是接收来自传输层的数据,并负责纠正错误。会话控制和远程过程调用均属于这一层的功能。但应注意,此层检查的错误不是通信介质的错误,而是磁盘空间、打印机缺纸等类型的高级错误。 80 | 81 | ### 6.表示层 82 | 83 | 表示层(Presentation Layer)是OSI模型的第六层,它对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是“处理用户信息的表示问题,如编码、数据格式转换和加密解密”等。表示层的具体功能如下: 84 | 85 | 数据格式处理:协商和建立数据交换的格式,解决各应用程序之间在数据格式表示上的差异。 86 | 87 | 数据的编码:处理字符集和数字的转换。例如由于用户程序中的数据类型(整型或实型、有符号或无符号等)、用户标识等都可以有不同的表示方式,因此,在设备之间需要具有在不同字符集或格式之间转换的功能。 88 | 89 | 压缩和解压缩:为了减少数据的传输量,这一层还负责数据的压缩与恢复。 90 | 91 | 数据的加密和解密:可以提高网络的安全性。 92 | 93 | ### 7.应用层 94 | 95 | 应用层(Application Layer)是OSI参考模型的最高层,它是计算机用户,以及各种应用程序和网络之间的接口,其功能是直接向用户提供服务,完成用户希望在网络上完成的各种工作。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。 96 | 97 | 应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。上述的各种网络服务由该层的不同应用协议和程序完成,不同的网络操作系统之间在功能、界面、实现技术、对硬件的支持、安全可靠性以及具有的各种应用程序接口等各个方面的差异是很大的。应用层的主要功能如下: 98 | 99 | 用户接口:应用层是用户与网络,以及应用程序与网络间的直接接口,使得用户能够与网络进行交互式联系。 100 | 101 | 实现各种服务:该层具有的各种应用程序可以完成和实现用户请求的各种服务。 102 | 103 | OSI7层模型的小结 104 | 105 | 由于OSI是一个理想的模型,因此一般网络系统只涉及其中的几层,很少有系统能够具有所有的7层,并完全遵循它的规定。 106 | 在7层模型中,每一层都提供一个特殊的网络功能。从网络功能的角度观察:下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主;第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;而上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。 107 | 108 | 109 | 110 | 111 | 112 | ## 二、TCP/IP模型 113 | 114 | OSI模型比较复杂且学术化,所以我们实际使用的TCP/IP模型,共分4层,**链路层、网络层、传输层、应用层**。两个模型之间的对应关系如图所示: 115 | 116 | ![OSI参考模型与TCP/IP参考模型](https://img-blog.csdnimg.cn/img_convert/6a8e6e8545002f92160f0230c217ac6f.png#pic_center) 117 | 118 | 无论什么模型,每一个抽象层建立在低一层提供的服务上,并且为高一层提供服务。 119 | 120 | ### 1. 数据在网络的传输 121 | 122 | 每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。 123 | 124 | 网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。 125 | 126 | ![image-20210915011107200](https://gitee.com/zhpanvip/images/raw/master//project/article/image-20210915011107200.png) 127 | 128 | ① 应用程序处理 129 | 130 | 首先应用程序会进行编码处理,这些编码相当于 OSI 的表示层功能; 131 | 编码转化后,邮件不一定马上被发送出去,这种何时建立通信连接何时发送数据的管理功能,相当于 OSI 的会话层功能。 132 | 133 | ② TCP 模块的处理 134 | 135 | TCP 根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要在应用层数据的前端附加一个 TCP 首部。 136 | 137 | ③ IP 模块的处理 138 | 139 | IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。IP 包生成后,参考路由控制表决定接受此 IP 包的路由或主机。 140 | 141 | ④ 网络接口(以太网驱动)的处理 142 | 143 | 从 IP 传过来的 IP 包对于以太网来说就是数据。给这些数据附加上以太网首部并进行发送处理,生成的以太网数据包将通过物理层传输给接收端。 144 | 145 | ⑤ 网络接口(以太网驱动)的处理 146 | 147 | 主机收到以太网包后,首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包,若不是则丢弃数据。 148 | 如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如 IP、ARP 等。这里的例子则是 IP 。 149 | 150 | ⑥ IP 模块的处理 151 | 152 | IP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP。 153 | 另外吗,对于有路由器的情况,接收端地址往往不是自己的地址,此时,需要借助路由控制表,在调查应该送往的主机或路由器之后再进行转发数据。 154 | 155 | ⑦ TCP 模块的处理 156 | 157 | 在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。 158 | 159 | ⑧ 应用程序的处理 160 | 161 | 接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。 162 | 163 | 164 | ### 2. TCP/IP协议族 165 | 166 | 167 | 168 | TCP/IP 协议是一个协议族,它包含了IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、以及 HTTP 等一系列协议。 169 | 170 | 171 | 172 | ![](https://gitee.com/zhpanvip/images/raw/master//project/article/20190105161812494.png) 173 | 174 | 175 | 176 | ![](https://gitee.com/zhpanvip/images/raw/master/project/article/osi/osi_table.png) 177 | 178 | 179 | 180 | ### 常用协议简介 181 | 182 | IP 旨在让最终目标主机收到数据包,但是在这一过程中仅仅有 IP 是无法实现通信的。必须还有能够解析主机名称和 MAC 地址的功能,以及数据包在发送过程中异常情况处理的功能。 183 | 184 | #### 1 DNS 185 | 186 | 我们平常在访问某个网站时不适用 IP 地址,而是用一串由罗马字和点号组成的字符串。而一般用户在使用 TCP/IP 进行通信时也不使用 IP 地址。能够这样做是因为有了 DNS (Domain Name System)功能的支持。DNS 可以将那串字符串自动转换为具体的 IP 地址。 187 | 这种 DNS 不仅适用于 IPv4,还适用于 IPv6。 188 | 189 | #### 2 ARP 190 | 191 | 只要确定了 IP 地址,就可以向这个目标地址发送 IP 数据报。然而,在底层数据链路层,进行实际通信时却有必要了解每个 IP 地址所对应的 MAC 地址。 192 | ARP 是一种解决地址问题的协议。以目标 IP 地址为线索,用来定位下一个应该接收数据分包的网络设备对应的 MAC 地址。不过 ARP 只适用于 IPv4,不能用于 IPv6。IPv6 中可以用 ICMPv6 替代 ARP 发送邻居探索消息。 193 | RARP 是将 ARP 反过来,从 MAC 地址定位 IP 地址的一种协议。 194 | 195 | #### 3 ICMP 196 | 197 | ICMP 的主要功能包括,确认 IP 包是否成功送达目标地址,通知在发送过程当中 IP 包被废弃的具体原因,改善网络设置等。 198 | IPv4 中 ICMP 仅作为一个辅助作用支持 IPv4。也就是说,在 IPv4 时期,即使没有 ICMP,仍然可以实现 IP 通信。然而,在 IPv6 中,ICMP 的作用被扩大,如果没有 ICMPv6,IPv6 就无法进行正常通信。 199 | 200 | ### 4 DHCP 201 | 202 | 如果逐一为每一台主机设置 IP 地址会是非常繁琐的事情。特别是在移动使用笔记本电脑、只能终端以及平板电脑等设备时,每移动到一个新的地方,都要重新设置 IP 地址。 203 | 于是,为了实现自动设置 IP 地址、统一管理 IP 地址分配,就产生了 DHCP(Dynamic Host Configuration Protocol)协议。有了 DHCP,计算机只要连接到网络,就可以进行 TCP/IP 通信。也就是说,DHCP 让即插即用变得可能。 204 | DHCP 不仅在 IPv4 中,在 IPv6 中也可以使用。 205 | 206 | [一篇文章带你熟悉 TCP/IP 协议(网络协议篇二)]( -------------------------------------------------------------------------------- /post/责任链模式.md: -------------------------------------------------------------------------------- 1 | ## 责任链模式 2 | 3 | 责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。责任链模式吧多个处理器串成链,然后请求在链上传递。 4 | 5 | 假如有以下场景: 6 | 7 | > 公司的费用报销审核制度规定,Manager只能审核1000元以下的报销,Director只能审核10000元以下的报销,CEO可以审核任意额度。 8 | 9 | 用责任链模式设计此报销流程时,每个审核者只关心自己的责任范围内的请求,并处理它。对于超出自己责任的范围的请求则扔给下一个审核者处理。假设将来在继续添加审核者的时候就不需要改动现有逻辑。 10 | 11 | 12 | 13 | 首先抽象出一个请求对象,它将在责任链上传递 14 | 15 | ```java 16 | /** 17 | * 抽象一个请求对象,负责在责任链上传递 18 | */ 19 | public class Request { 20 | private String name; 21 | private double amount; 22 | 23 | public Request(String name, double amount) { 24 | this.name = name; 25 | this.amount = amount; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public double getAmount() { 33 | return amount; 34 | } 35 | } 36 | ``` 37 | 38 | 接着,抽象出处理器: 39 | 40 | ```java 41 | /** 42 | * 抽象处理器 43 | */ 44 | public interface Handler { 45 | /** 46 | * 47 | * @param request 48 | * @return true 成功,false 拒绝,null 交给下一个处理 49 | */ 50 | Boolean process(Request request); 51 | } 52 | ``` 53 | 54 | 并且做好约定:如果返回 `TRUE`,表示处理成功,如果返回`FALSE`,表示处理失败(请求被拒绝),如果返回`null`,则交由下一个`Handler`处理。 55 | 56 | 然后,依次编写ManagerHandler、DirectorHandler和CEOHandler。以ManagerHandler为例: 57 | 58 | ```java 59 | public class ManagerHandler implements Handler { 60 | @Override 61 | public Boolean process(Request request) { 62 | if (request.getAmount() >= 1000) { 63 | // 超过1000元,处理不了,交给上级处理 64 | return null; 65 | } 66 | // 对Bob有偏见,Bob的请求直接拒绝 67 | return !request.getName().equals("Bob"); 68 | } 69 | } 70 | ``` 71 | 72 | 73 | 74 | 有了不同的Handler后,我们还要把这些Handler组合起来,变成一个链,并通过统一入口处理: 75 | 76 | ```java 77 | public class HandlerChain { 78 | private List handlers = new ArrayList<>(); 79 | 80 | public void addHandler(Handler handler) { 81 | this.handlers.add(handler); 82 | } 83 | 84 | public boolean process(Request request) { 85 | for (Handler handler : 86 | handlers) { 87 | Boolean process = handler.process(request); 88 | if (process != null) { 89 | System.out.println(request.getName() + "'s request " + (process ? "Approved by" : "Denied by") + handler.getClass().getSimpleName()); 90 | return process; 91 | } 92 | } 93 | throw new RuntimeException("Count not handle request: " + request); 94 | } 95 | } 96 | ``` 97 | 98 | 现在可以在客户端组装出责任链,然后通过责任链来处理请求: 99 | 100 | ```java 101 | public static void main(String[] args) { 102 | // 构造责任链: 103 | HandlerChain chain = new HandlerChain(); 104 | chain.addHandler(new ManagerHandler()); 105 | chain.addHandler(new DirectorHandler()); 106 | chain.addHandler(new CEOHandler()); 107 | 108 | // 处理请求: 109 | chain.process(new Request("Bob", 123.45)); 110 | chain.process(new Request("Alice", 1234.56)); 111 | chain.process(new Request("Bill", 12345.67)); 112 | chain.process(new Request("John", 123456.78)); 113 | 114 | } 115 | ``` 116 | 117 | 责任链模式本身很容易理解,需要注意的是,`Handler`添加的顺序很重要,如果顺序不对,处理的结果可能就不是符合要求的。 118 | 119 | 此外,责任链模式有很多变种。有些责任链的实现方式是通过某个`Handler`手动调用下一个`Handler`来传递`Request`,例如: 120 | 121 | ```java 122 | public class AHandler implements Handler { 123 | private Handler next; 124 | public void process(Request request) { 125 | if (!canProcess(request)) { 126 | // 手动交给下一个Handler处理: 127 | next.process(request); 128 | } else { 129 | ... 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | 还有一些责任链模式,每个`Handler`都有机会处理`Request`,通常这种责任链被称为拦截器(Interceptor)或者过滤器(Filter),它的目的不是找到某个`Handler`处理掉`Request`,而是每个`Handler`都做一些工作,比如OkHttp中的拦截器: 136 | 137 | - 重定向拦截器负责重试与重定向 138 | - 桥接拦截器是应用程序与服务器的桥梁,请求会经过它来补全请求头 139 | - 缓存拦截器在发出请求前判断是否命中缓存,如果命中则可以不请求,直接使用缓存。 140 | - 连接拦截器打开目标服务器的连接,并执行下一个拦截器 141 | - 请求服务器拦截器利用HttpCodec发出请求并解析生成Reponse 142 | 143 | https://www.liaoxuefeng.com/wiki/1252599548343744/1281319474561057 -------------------------------------------------------------------------------- /post/递归算法.md: -------------------------------------------------------------------------------- 1 | #### [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/) 2 | 3 | ```java 4 | /** 5 | * 递归法 6 | * 递归的递流程直接找到递归的最后一个节点,最后一个节点的next为null,返回自身,然后开始递归的归流程 7 | * 递归的归流程,从最后一个节点向上回溯,将最后一个节点的next指向倒数第二个节点,倒数第二个节点的next指向null 8 | * 以此回溯到第一个节点完成链表的反转 9 | */ 10 | public static ListNode reverseLink2(ListNode head) { 11 | // head == null这个条件一定要判断啊!!!! 12 | if (head == null || head.next == null) { 13 | return head; 14 | } 15 | // 返回最后一个节点 16 | ListNode lastNode = reverseLink2(head.next); 17 | head.next.next = head; 18 | head.next = null; 19 | return lastNode; 20 | } 21 | ``` 22 | 23 | #### [206 反转链表 扩展]() 24 | 25 | ```java 26 | public static ListNode cursor; 27 | 28 | public static ListNode reverseLink(ListNode head, int n) { 29 | if (n == 1) { 30 | cursor = head.next; 31 | return head; 32 | } 33 | ListNode lastNode = reverseLink(head.next, n - 1); 34 | head.next.next = head; 35 | head.next = cursor; 36 | return lastNode; 37 | } 38 | ``` 39 | 40 | 41 | 42 | #### [92. 反转链表 II](https://leetcode-cn.com/problems/reverse-linked-list-ii/) 43 | 44 | 45 | 46 | ```java 47 | public ListNode reverseBetween(ListNode head, int left, int right) { 48 | if (head == null || head.next == null) { 49 | return head; 50 | } 51 | return reverse(head, left, right); 52 | } 53 | 54 | private ListNode reverse(ListNode head, int left, int right) { 55 | if (left == 1) { 56 | return LeetCode92$.reverseLink(head, right); 57 | } 58 | head.next = reverse(head.next, left - 1, right - 1); 59 | return head; 60 | } 61 | ``` 62 | 63 | #### [21. 合并两个有序链表](https://leetcode-cn.com/problems/merge-two-sorted-lists/) 64 | 65 | ```java 66 | /** 67 | * 递归法 68 | */ 69 | public ListNode mergeTwoList2(ListNode l1, ListNode l2) { 70 | if (l1 == null) { 71 | return l2; 72 | } else if (l2 == null) { 73 | return l1; 74 | } else { 75 | if (l1.val < l2.val) { 76 | l1.next = mergeTwoList2(l1.next, l2); 77 | return l1; 78 | } else { 79 | l2.next = mergeTwoList2(l1, l2.next); 80 | return l2; 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | #### 快速排序 87 | 88 | ```java 89 | /** 90 | * 快速排序 91 | *

92 | * 核心的思路是取第一个元素(或者最后一个元素)作为分界点,把整个数组分成左右两侧,左边的元素小于或者等于分界点元素, 93 | * 而右边的元素大于分界点元素,然后把分界点移到中间位置,对左右子数组分别进行递归,最后就能得到一个排序完成的数组。 94 | * 当子数组只有一个或者没有元素的时候就结束这个递归过程。 95 | */ 96 | public static void quickSort(int[] nums, int left, int right) { 97 | if (left > right) return; 98 | int key = nums[left]; 99 | int l = left; 100 | int r = right; 101 | while (l < r) { 102 | while (nums[r] >= key && l < r) 103 | r--; 104 | while (nums[l] <= key && l < r) 105 | l++; 106 | if (l < r) 107 | swap(nums, r, l); 108 | } 109 | 110 | nums[left] = nums[r]; 111 | nums[r] = key; 112 | 113 | quickSort(nums, left, r - 1); 114 | quickSort(nums, r + 1, right); 115 | } 116 | ``` 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /post/项目中遇到的问题.md: -------------------------------------------------------------------------------- 1 | ## 一、一个非静态内部类引起的空指针 2 | 3 | 这是前段时间同事在项目中碰到的一个问题,由非静态内部类引起的一个空指针。 4 | ### 1.问题复现 5 | 现在有一个Book类,嵌套了一个非静态内部类Picture,结构如下: 6 | 7 | ``` 8 | public class Book { 9 | 10 | private String bookName; 11 | private Picture picture; 12 | 13 | public String getBookName() { 14 | return bookName; 15 | } 16 | 17 | public Picture getPicture() { 18 | return picture; 19 | } 20 | 21 | public class Picture { 22 | private String pictureName; 23 | 24 | public String getPictureName() { 25 | return pictureName; 26 | } 27 | 28 | public String getBookName() { 29 | return bookName; 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ``` 36 | { 37 | bookName:"Nice Book", 38 | picture:{ 39 | "pictureName":"Nice Picture" 40 | } 41 | } 42 | 43 | ``` 44 | 有如上述Json数据,将其通过Gson解析成Book,代码如下: 45 | ``` 46 | public static void main(String[] args) { 47 | Gson gson = new Gson(); 48 | String jsonStr = "{\n" + 49 | " bookName:\"Nice Book\",\n" + 50 | " picture:{\n" + 51 | " \"pictureName\":\"Nice Picture\"\n" + 52 | " }\n" + 53 | "}"; 54 | Book book = gson.fromJson(jsonStr, Book.class); 55 | System.out.println(book.getBookName() + " " + book.getPicture().getPictureName()); 56 | } 57 | ``` 58 | 上述代码运行正常,打印结果: 59 | > Nice Book Nice Picture 60 | 61 | 接下来将book.getPicture().getPictureName()换成book.getPicture().getBookName()再次运行,程序出现空指针异常: 62 | ``` 63 | Exception in thread "main" java.lang.NullPointerException 64 | at Book$Picture.getBookName(Book.java:22) 65 | at Test.main(Test.java:13) 66 | ``` 67 | ### 2.NullPointerException异常分析 68 | 首先来看这个空指针应该是哪里抛出来的,首先book肯定不为null,而book.getPicture()这段代码在getPictureName()时候可以正常运行,说明Book实例中的picture实例也不为null,那么出现问题的一定是在getBookName()这句代码,而看getBookName的代码似乎也没有任何可能引起空指针的情况: 69 | ``` 70 | public String getBookName() { 71 | return bookName; 72 | } 73 | ``` 74 | 75 | 其实,回想一下大家应该都知道“非静态内部类默认持有外部类的引用”这句话。但是很多人并不知道其原理。我们将Book$Picture.class字节码文件反编译得到如下: 76 | 77 | ``` 78 | public class Book$Picture { 79 | private String pictureName; 80 | 81 | public Book$Picture(Book var1) { 82 | this.this$0 = var1; 83 | } 84 | 85 | public String getPictureName() { 86 | return this.pictureName; 87 | } 88 | 89 | public String getBookName() { 90 | return this.this$0.bookName; 91 | } 92 | } 93 | ``` 94 | 可以看到在getBookName中调用的是this.this$0.bookName,而这个崩溃的原因就出现在这个this$0上,即this$0即为外部类Book的引用,此时this$0为null,引发了空指针。 95 | 96 | 有些同学可能注意到反编译后的代码,Picture多了一个Book参数的构造方法,但是在Picture的源代码中并没有添加任何构造方法。这个构造方法中Book赋值给了this$0,而this$0的角色更像是Picture的一个成员变量,只不过反编译的代码中并没有体现出来。其实到这里已经可以解释空指针出现的原因了,是因为Gson在初始化时候没有传入Book的实例,导致的。 97 | 98 | 但是,不知道你会不会好奇,Gson是怎么实例化Picture类的呢?是通过反射调用Picture的默认的构造方法?或者说是通过反射调用Picture的有参构造方法,然后Book参数传入了null? 99 | 100 | 不妨先通过反射试验一下: 101 | ``` 102 | Class pictureClass = Book.Picture.class; 103 | try { 104 | Book.Picture picture = pictureClass.newInstance(); 105 | } catch (InstantiationException | IllegalAccessException e) { 106 | e.printStackTrace(); 107 | } 108 | ``` 109 | 运行结果出现了崩溃: 110 | ``` 111 | java.lang.InstantiationException: Book$Picture 112 | at java.base/java.lang.Class.newInstance(Class.java:571) 113 | at Test.main(Test.java:20) 114 | Caused by: java.lang.NoSuchMethodException: Book$Picture.() 115 | at java.base/java.lang.Class.getConstructor0(Class.java:3349) 116 | at java.base/java.lang.Class.newInstance(Class.java:556) 117 | ... 1 more 118 | ``` 119 | 可见编译器其实并没有生成无参构造方法。那来尝试一下调用Picture的有有参构造: 120 | ``` 121 | Class pictureClass = Book.Picture.class; 122 | try { 123 | Book book = new Book(); 124 | Constructor declaredConstructor = pictureClass.getDeclaredConstructor(Book.class); 125 | Book.Picture picture = declaredConstructor.newInstance(book); 126 | System.out.println(picture.getBookName()); 127 | } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { 128 | e.printStackTrace(); 129 | } 130 | ``` 131 | 此时,出现了跟开始时候一样的NullPointerException 132 | ``` 133 | Exception in thread "main" java.lang.NullPointerException 134 | at Book$Picture.getBookName(Book.java:22) 135 | at Test.main(Test.java:23) 136 | ``` 137 | 虽然,我们通过反射传入null的方式复现除了项目中的异常,但是就代表Gson是通过反射传入null的方式解析的吗?这道真未必,接下俩看下Gosn的源码 138 | 139 | ### 3.Gson是如何实例化非静态内部类的? 140 | 141 | 通过追踪Gson的fromJson方法,最终看到了如下代码: 142 | ``` 143 | public T fromJson(String json, Class classOfT) throws JsonSyntaxException { 144 | Object object = this.fromJson((String)json, (Type)classOfT); 145 | return Primitives.wrap(classOfT).cast(object); 146 | } 147 | 148 | public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException { 149 | boolean isEmpty = true; 150 | boolean oldLenient = reader.isLenient(); 151 | reader.setLenient(true); 152 | 153 | AssertionError error; 154 | try { 155 | try { 156 | reader.peek(); 157 | isEmpty = false; 158 | TypeToken typeToken = TypeToken.get(typeOfT); 159 | // 根据TypeToke获取TypeAdapter 160 | TypeAdapter typeAdapter = this.getAdapter(typeToken); 161 | T object = typeAdapter.read(reader); 162 | Object var8 = object; 163 | return var8; 164 | } catch (EOFException var15) { 165 | // ... 166 | } finally { 167 | reader.setLenient(oldLenient); 168 | } 169 | 170 | return error; 171 | } 172 | ``` 173 | 上述代码中核心是通过TypeToken获取到TypeAdapter,在这里TypeAdapter对应的是TypeAdapterFactory.create()出来的Adapter。TypeAdapterFactory是一个接口,在Gson中有众多类实现它的工厂类,而在我们当前场景下对应的是ReflectiveTypeAdapterFactory。那接下来看下ReflectiveTypeAdapterFactory的create方法: 174 | 175 | ``` 176 | public TypeAdapter create(Gson gson, TypeToken type) { 177 | Class raw = type.getRawType(); 178 | if (!Object.class.isAssignableFrom(raw)) { 179 | return null; 180 | } else { 181 | // 此处get为核心代码 182 | ObjectConstructor constructor = this.constructorConstructor.get(type); 183 | return new ReflectiveTypeAdapterFactory.Adapter(constructor, this.getBoundFields(gson, type, raw)); 184 | } 185 | } 186 | ``` 187 | create方法中调用了constructorConstructor.get方法,跟进来看: 188 | ``` 189 | public ObjectConstructor get(TypeToken typeToken) { 190 | final Type type = typeToken.getType(); 191 | Class rawType = typeToken.getRawType(); 192 | 193 | // ... 省略从缓存中取ObjectConstructor代码 194 | 195 | // 这个方法中会通过反射调尝试用类的的无参构造方法 196 | ObjectConstructor defaultConstructor = this.newDefaultConstructor(rawType); 197 | if (defaultConstructor != null) { 198 | return defaultConstructor; 199 | } else { // 调用无参构造方法失败 200 | // 这个方法里面都是一些集合类相关对象的逻辑 201 | ObjectConstructor defaultImplementation = this.newDefaultImplementationConstructor(type, rawType); 202 | return defaultImplementation != null ? defaultImplementation : this.newUnsafeAllocator(type, rawType); 203 | } 204 | } 205 | ``` 206 | 从上述代码中可以分析,通过反射调用无参构造方法失败后则会通过newUnsafeAllocator来实例化对象: 207 | ``` 208 | private ObjectConstructor newUnsafeAllocator(final Type type, final Class rawType) { 209 | return new ObjectConstructor() { 210 | private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); 211 | 212 | public T construct() { 213 | try { 214 | Object newInstance = this.unsafeAllocator.newInstance(rawType); 215 | return newInstance; 216 | } catch (Exception var2) { 217 | throw new RuntimeException("Unable to invoke no-args constructor for " + type + ". Registering an InstanceCreator with Gson for this type may fix this problem.", var2); 218 | } 219 | } 220 | }; 221 | } 222 | ``` 223 | 通过UnsafeAllocator初始化对象,这个类也是Gson中提供的一个类,用于不安全的实例化对象,源码如下: 224 | 225 | ``` 226 | // UnsafeAllocator 227 | 228 | public abstract T newInstance(Class var1) throws Exception; 229 | 230 | public static UnsafeAllocator create() { 231 | try { 232 | Class unsafeClass = Class.forName("sun.misc.Unsafe"); 233 | Field f = unsafeClass.getDeclaredField("theUnsafe"); 234 | f.setAccessible(true); 235 | final Object unsafe = f.get((Object)null); 236 | final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); 237 | return new UnsafeAllocator() { 238 | public T newInstance(Class c) throws Exception { 239 | assertInstantiable(c); 240 | return allocateInstance.invoke(unsafe, c); 241 | } 242 | }; 243 | } catch (Exception var6) { 244 | // ...省略异常处理 245 | } 246 | } 247 | ``` 248 | 可以看到这里是通过Java提供的Unsafe这个类来实例化出的一个不安全对象。 -------------------------------------------------------------------------------- /post/高阶函数实现原理.md: -------------------------------------------------------------------------------- 1 | 一、Kotlin的高阶函数 2 | 3 | 如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。 4 | 5 | > 高阶函数的基本语法:(String,Int)-> Unit 6 | 7 | ->左边部分用来声明函数接收什么类型的参数,多个参数之间用逗号隔开,如果不接收任何参数,可用括号表示。而->右边部分用于声明该函数的返回类型,没有返回值就使用Unit,相当于void。 8 | 9 | 举个例子: 10 | 11 | ```kotlin 12 | fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { 13 | return operation(num1, num2) 14 | } 15 | ``` 16 | 17 | 上述函数接收两个Int类型的参数,和一个函数类型的参数。这个函数即为一个高阶函数。接下来看如何调用高阶函数。 18 | 19 | 首先定义两个函数 20 | 21 | ```kotlin 22 | fun plus(num1: Int, num2: Int): Int { 23 | return num1 + num2 24 | } 25 | 26 | fun minus(num1: Int, num2: Int): Int { 27 | return num1 - num2 28 | } 29 | 30 | ``` 31 | 32 | 接着讲上述两个函数作为num1AndNum2函数的参数如下: 33 | 34 | ```kotlin 35 | fun main() { 36 | val num1 = 100 37 | val num2 = 80 38 | val result1 = num1AndNum2(num1, num2, ::plus) 39 | val result2 = num1AndNum2(num1, num2, ::minus) 40 | print("result1 = $result1") 41 | print("result2 = $result2") 42 | } 43 | ``` 44 | 45 | 打印结果: 46 | 47 | ``` 48 | result1 = 180 49 | result2 = 20 50 | ``` 51 | 52 | 可以看到,想要使用高阶函数需要定义一个与高阶函的数参数匹配的函数作为参数才行,这样写相当麻烦。 53 | 54 | 其实,kotlin支持 多种方式调用高阶函数,如lambada表达式、匿名函数、成员引用等。其中Lambda表达式是最常见的高阶函数调用方式。上述代码使用Lambda表达式的写法如下: 55 | 56 | ```kotlin 57 | fun main() { 58 | val num1 = 100 59 | val num2 = 80 60 | val result1 = num1AndNum2(num1, num2) { n1, n2 -> 61 | n1 + n2 62 | } 63 | val result2 = num1AndNum2(num1, num2){ n1, n2 -> 64 | n1 - n2 65 | } 66 | print("result1 = $result1") 67 | print("result2 = $result2") 68 | } 69 | ``` 70 | 71 | ## 二、高阶函数实现原理 72 | 73 | 因为kotlin代码最终也是会被编译成字节码的,因此可以将上述高阶函数编译成的字节码,再反编译成java的代码,得到如下结果: 74 | 75 | ```java 76 | public final int num1AndNum2(int num1, int num2, @NotNull Function2 operation) { 77 | Intrinsics.checkParameterIsNotNull(operation, "operation"); 78 | return ((Number)operation.invoke(num1, num2)).intValue(); 79 | } 80 | ``` 81 | 82 | 可以看到,将kotlin代码编程java代码后,高阶函数变成了一个 Function2 参数。而这个Function2是kotlin中定义的一个接口: 83 | 84 | ```kotlin 85 | public interface Function2 : Function { 86 | /** Invokes the function with the specified arguments. */ 87 | public operator fun invoke(p1: P1, p2: P2): R 88 | } 89 | ``` 90 | 91 | 也就是说,kotlin 高阶函数的实现其实使用的是匿名内部类。 --------------------------------------------------------------------------------