【小木箱生长营】发动优化系列文章(排期中):
发动优化 东西论 发动优化常见的六种东西
发动优化 办法论 这样做发动优化时长下降70%
发动优化 实战论 手把手教你破解发动优化十大难题
一、导言
Hello,我是小木箱,欢迎来到小木箱生长营系列教程,今天将分享发动优化根底论浅析Android发动优化。小木箱从四个维度将Android发动优化根底论解说清楚。
本文首要说了四部分内容,榜首部分内容是发动根底,第二部分内容是发动优化价值,第三部分内容是发动优化事务痛点,第四部分内容是总结与展望。
发动优化事务痛点首要分为五个方面,榜首个方面是事务问题背景,第二个方面是防劣化机制建造,第三个方面是优化思路,第四个方面是调度结构,第五个方面是事务结构。
假如学完小木箱生长营发动优化的根底论、东西论、办法论和实战论,那么任何人做发动优化都能够拿到成果。
二、发动根底
咱们先进入第二部分内容发动根底,发动根底有六个要害点能够和咱们分享一下,榜首个要害点是发动进程。第二个要害点是发动办法。第三个要害点是发动流程。第四个要害点是归因剖析。第五个要害点是优化方向。第六个要害点是发动目标。
2.1 发动进程
首要,小木箱说说榜首个要害点发动进程,依照事务是否可直接操作分为SystemServer 和 App Process 。 其职责区分如下:
SystemServer 担任运用的发动流程调度、进程的创立和办理、窗口的创立和办理(StartingWindow 和 AppWindow) 等
运用进程被 SystemServer 创立后,进行一系列的进程初始化、组件初始化(Activity、Service、ContentProvider、Broadcast)、主界面的构建、内容填充等
2.2 发动办法
接着,小木箱说说第二个要害点发动办法,Android运用的发动办法大约分为热发动、冷发动、温发动三种,关于冷发动、热发动、温发动三者发动办法比照能够参阅下面的流程图学习。
2.1.1 冷发动
冷发动具有耗时最多,衡量标准的特征,冷发动常见的场景是APP初次发动或APP被彻底杀死,冷发动、热发动和温发动中冷发动CPU时刻开支最大。发动流程简化如下,后文会详细介绍。
2.1.2 温发动
当发动运用时,后台已有该运用的进程,可是Activity或许由于内存缺乏被收回。这样体系会从已有的进程中来发动这个Activity,这个发动办法叫温发动。
温发动常见的场景有两种:榜首种是首要用户按连续按回来退出了app,最终重新发动app,第二种是首要体系收回了app的内存,最终重新发动app。
2.1.3 热发动
热发动只履行了冷发动的第二阶段,假如由于内存缺乏导致目标被收回,那么需求在热发动时重建目标,后面与冷发动时将界面显现到手机屏幕流程是相同的。
热发动时,体系将activity带回前台。假如运用程序的一切activity存在内存中,那么运用程序能够防止重复目标初始化、烘托、制作操作。
热发动常见的场景如: 当咱们按了Home键或其它状况app被切换到后台,再次发动app的进程。
2.3 发动流程
其次,小木箱说说第三个要害点发动流程,运用的发动流程一般指的是冷发动流程,即运用进程不存在的状况下,从点击桌面运用图标,到运用发动的进程。
首要,用户进行了一个点击操作,这个点击事情它会触发一个IPC的操作,之后便会履行到Process的start办法中,这个办法是用于进程创立的。
然后,便会履行到ActivityThread的main办法,这个办法能够看做是咱们单个App进程的入口,相当于Java进程的main办法,在其中会履行音讯循环的创立与主线程Handler的创立。
接着,创立完结之后,就会履行到 bindApplication 办法,在这儿运用了反射去创立 Application以及调用了 Application相关的生命周期。
最终,Application结束之后,便会履行Activity的生命周期,在Activity LifeCycle结束之后,就会履行到 ViewRootImpl,这时才会进行真实的一个页面的制作。
2.4 优化方向
其四,咱们说说第四个要害点优化方向,创立Application、发动主线程、创立HomeActivity、加载布局、布置屏幕和界面首帧制作完结后,咱们就能够以为发动现已结束了。
综上所述,除了Application atttachBaseContext、Appication onCreate 和 Activity LifeCycle,无需Hook源码,其他流程都是体系层面的。因而,咱们能优化的空间只要Application atttachBaseContext、Appication onCreate 和 Activity LifeCycle三个流程。
2.5 归因剖析
其五,咱们先说说第五个要害点归因剖析,程序作业最底子的是需求得到CPU时刻片,假如一个使命需求较多的CPU时刻履行,那么它将影响其他使命的履行,然后影响全体使命行列的作业;
线程切换涉及到 CPU调度,而CPU调度会有体系资源的开支,所以大量的线程频繁切换也会产生巨大的功用损耗;
IO和锁的等候会直接堵塞使命的履行,不能充分地运用CPU等体系资源。
因而,做发动优化的要害点是找到占用过多CPU时刻、频繁的CPU调度、I/O等候和锁抢占等不合理耗费资源三个因素,这儿先简略的看一下Profile剖析文件,东西论会带咱们详细学习怎样运用东西进行线下监控与治理。
2.6 发动目标
最终,咱们说说第六个要害点发动目标,关于发动优化监控当然是在冷发动阶段进行健康预测的。有三个发动目标咱们需求额外注意,
榜首个是发动开端,发动开端是进程创立的时刻
第二个是发动结束,首页首屏烘托完结的时刻;
第三个是发动时长,发动时长是指发动结束的时刻戳减去发动开端的时刻戳;
三、发动优化价值
说完发动根底,咱们进入第三部分内容发动优化价值,发动耗时增加或许减缩App用户的留存,因而,发动功用优化是每一家互联网公司在体会优化方向上有必要要做的要害技能打破。
发动功用优化目标是以低端机为要点,辐射中高端机,经过技能和产品上的深度优化,可感知的进步用户体会,完结扩大用户规划、进步留存和进步收入。
四、发动优化事务痛点
说完发动优化价值,咱们进入第四部分内容发动优化事务痛点。带着问题动身,关于发动优化有低端机功用问题复杂、缺少全体调度机制、问题定位本钱高和监控机制不完善四大事务痛点亟需处理。关于这四大事务痛点咱们对怎样界说低端机?怎样快速发现功用问题? 怎样体系化的优化功用问题?怎样防止功用优化的一同呈现劣化问题,并打造劣化问题修正自作业的飞轮?有了进一步考虑。
4.1 怎样界说低端机?
传送门: www.jianshu.com/p/76d68d13c…
关于怎样界说低端机?低端机界说要双端对齐考虑
Android
Android方面,内存和CPU是描述低端机型的比较要害的两个目标,咱们依据Android用户的不同设备做了功用区分,初步可区分为高、中、低3种等级。
依据现有CPU、GPU的跑分软件安兔兔公测跑分维护一份CPU、GPU的设备功用档位表,依照不同档位区分为高、中、低三档。
先判别设备的Android体系版别号,假如Android体系低于Android6.0,能够直接区分为低档机再判别设备的内存和内核数:
public static void isLowerDevice() {
return Build.VERSION.RELEASE < 6;
}
//获取RAM容量
public static long getTotalMemory(Context c) {
// memInfo.totalMem not supported in pre-Jelly Bean APIs.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
ActivityManager am = (ActivityManager) c.getSystemService(Context.ACTIVITY_SERVICE);
am.getMemoryInfo(memInfo);
if (memInfo != null) {
return memInfo.totalMem;
} else {
return DEVICEINFO_UNKNOWN;
}
} else {long totalMem = DEVICEINFO_UNKNOWN;
try {
FileInputStream stream = new FileInputStream("/proc/meminfo");
try {
totalMem = parseFileForValue("MemTotal", stream);
totalMem *= 1024;
} finally {
stream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return totalMem;
}
}
// 获取CPU中心数
public static int getNumberOfCPUCores() {
int cores;
try {
cores = getCoresFromFileInfo("/sys/devices/system/cpu/possible");if (cores == DEVICEINFO_UNKNOWN) {
cores = getCoresFromFileInfo("/sys/devices/system/cpu/present");}
if (cores == DEVICEINFO_UNKNOWN) {
cores = new File("/sys/devices/system/cpu/").listFiles(CPU_FILTER).length;
}
} catch (SecurityException e) {
cores = DEVICEINFO_UNKNOWN;
} catch (NullPointerException e) {
cores = DEVICEINFO_UNKNOWN;}return cores;
}
当没有取到CPU、GPU型号或许CPU、GPU型号在设备功用档位表里边不存在时,经过设备的CPU和RAM组合信息来判定。判定规矩如下:
高端机型: CPU为骁龙845或麒麟980,RAM大于等于6GB
低端机型: 骁龙或联发科系列,CPU最大主频小于等于1.8GHz且RAM小于4GB。麒麟系列,CPU最大主频小于等于2.1GHz且RAM小于等于4GB
中端机型: 剩余法
//获取CPU型号
public static String getCPUName() {
try {
FileReader fr = new FileReader("/proc/cpuinfo");
BufferedReader br = new BufferedReader(fr);
String text;
String last = "";
while ((text = br.readLine()) != null) {
last = text;
}
//一般机型的cpu型号都会在cpuinfo文件的最终一行
if (last.contains("Hardware")) {
String[] hardWare = last.SharedPreferenceslit(":\s+", 2);
return hardWare[1];
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return Build.HARDWARE;}
iOS
iOS方面,机型种类优先,可枚举,因而可经过装备表直接读取机型评分分数,低端机占大盘比例 15%。
4.2 怎样快速发现功用问题?
关于怎样快速发现功用问题?咱们是依据设备类型、Android体系版别号、手机品牌、发动时长、体系渠道、app版别号、app称号、闪屏广告阻断次数和设备ID等字段结合数据大盘剖析渠道,针对预发布环境和正式环境建造运用级的根底调度机制,服务于事务,并协助事务优化功用;假如预发布环境发动超标,那么向开发主力团队发送飞书机器人告警提醒。
4.3 怎样体系化的优化功用问题?
关于怎样体系化的优化功用问题?咱们凭借了Dokit东西提效,自建安稳且高效功用东西,经过DoKit进行源码魔改,进步发现问题功率;慢函数闭环监控凭借了ASM插桩打点完结,详细内容咱们能够参阅后续的发动优化 实战论 手把手教你破解发动优化十大难题。
4.4 怎样防止功用优化的一同呈现劣化问题,并打造劣化问题修正自作业的飞轮?
关于怎样防止功用优化的一同呈现劣化问题,并打造劣化问题修正自作业的飞轮?咱们首要运用发动器将发动使命颗粒化,然后针对使命的时长核算上报,最终经过 Appium 、Mockio、Hamcrest、UIAutomator等主动化测验架构进行测验,app每个版别集成回归时,测验同学会在测验渠道跑一遍功用测验并输出测验陈述。
陈述包含了界说的中心场景下,app的内存、CPU、发动时长等功用目标数据及版别比照动摇值。
在允许的动摇范围内,比方发动时长动摇<100ms,那么就以为测验经过,不然就以为数据有恶化趋势,测验不经过,需求研制排查优化,直到测验经过。
当然未来期望凭借已树立云真机测验渠道,能够考虑经过Docker容器化技能在云真机上主动化测验。测验渠道直接主动化剖析、主动化转发,全程自助,无人工干预。
下面咱们一同来学习一下百度低端机发动功用优化观测设施、根底设施和事务优化吧。
4.4.1 防劣化机制建造
百度的观测设施有三个要害点,榜首个要害点是低端机标准建造,上文怎样界说低端机现已供给了不错的处理计划。
第二个要害点是中心目标建造,设备类型、Android体系版别号、手机品牌、发动时长、体系渠道、app版别号、app称号、闪屏广告阻断次数和设备ID等字是咱们常用的上报字段。
第三个要害点是防劣化机制建造,是本文的重中之重。客户端防劣化机制建造首要考虑两个方向,榜首个方向是线下防劣化。第二个方向是线上防劣化。
线下防劣化
首要咱们来说一下线下防劣化,关于线下防劣化首要分为四个部分,榜首个部分是打包主动化。第二个部分是测验主动化。第三个部分是剖析主动化。第四个部分是分发主动化。
打包主动化,一般中大型的互联网公司都有做,如阿里的摩天轮、美团的MCI和货拉拉的MDAP等等。打包主动化是客户端持续化布置要害一步,经过打包主动化的办法便利咱们严控开发版别权限,下降发版危险。
测验主动化,经过Docker镜像完结快速布置和迁移Appium主动化测验结构,履行定制化case完结app在云真机发动进程中进行主动化测验。货拉拉如同也在做这件事,现在云真机渠道建造有了根底雏形。
剖析主动化,Android端上建议参阅字节跳动的btrace。咱们能够运用该东西记录事情的CPU履行时刻开支,写到本地日志后上传剖析渠道,这样会更便利排查问题。
反混杂解析首要能够参阅progard的mapping.txt文件,然后再经过retrace.sh -verbose mapping.txt obfuscated_trace.txt
进行反混杂,最终将obfuscated_trace.txt
透传给测验渠道进行烘托即可。
分发主动化,首要经过脚本对劣化问题进行去重、相信度过滤,然后经过分发服务进行问题归属定位,最终经过企业办公软件QA机器人分发。
线上防劣化
线上防劣化首要分为两种,榜首种是试验防劣化,第二种是函数级防劣化。
试验防劣化
关于试验防劣化,假如云真机测验渠道树立好了,首要能够经过主动化,发动录制视频,然后将视频分帧,经过算法筛选出点击帧和烘托完结帧。最终检测跑屡次数据,重视动摇曲线平和均值。
函数级防劣化
关于函数防劣化,用的是ASM字节码插桩技能,发动耗时核算现在有两种办法,即线上核算和线下监测,线上核算是指经过剖析核算一切手机的耗时状况,求取每个运用的不同发动时刻段占比。
一定程度能够反应每个版别发动耗时状况,能够针对不同版别差异化代码进行优化排查。
线下监测是指运用adb命令或Systrace东西在严格控制的环境下监控运用,该计划存在显着的缺乏:无法精确到每个函数等级,核算进程会比较复杂,数据也不行直观。
工程编译进程的视点动身,阻拦Android构建由Class字节码文件转换成Dex文件的进程,由于每个字节码文件都有来历途径,假如在当时字节码文件内容中能够检测出符合命中战略的指令,咱们就能够知道当时的指令所处文件名,调用方位、文件途径,进行插入日志信息,首要咱们运用的是自界说注解Anotaton办法处理咱们特别的字节码来历,然后依据来历进行周期性阻拦,并核算代码履行周期内耗时信息。
最终,咱们给每一个发动履行周期设置一个卡口时刻,假如超出卡口时刻就证明测验是不经过的。并且输出每一个子函数的耗时长度。
4.4.2 优化思路
关于优化思路,首要有两个方面,榜首个方面是SharedPreferences优化,第二个方面是锁优化。
SharedPreferences优化
首要,咱们来说一说SharedPreferences优化,为什么要对SharedPreferences进行优化呢?SharedPreferences的缺陷很显着,榜首明文存储,第二多进程存储数据易丢掉,第三功率低,IO读写运用xml数据格局,全量更新功率低。
SharedPreferences存储格局
咱们首要来看一下SharedPreferences的数据存储和编码格局,SharedPreferences数据存储和编码格局选用的是Xml,明文存储,可读性强,数据冗余度较高。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="MicroKibaco" value="小木箱生长营" />
</map>
SharedPreferences低效读取
SharedPreferences初始化的时分,子线程运用IO读取整个文件,进行XML解析,由于存入内存,SharedPreferences具有Map集合的数据结构特征,在咱们每次追加数据更新的时分,只要选用全量更新办法,才干把map中的数据全部序列化为XML,假如文件较大,那么会导致存储功率下降。
SharedPreferences多进程操作
其实SharedPreferences也有支撑多进程的形式MODE_MULTI_PROCESS,不过API过时了。
SharedPreferences在MODE_MULTI_PROCESS多进程形式下,读写数据或许呈现数据丢掉,详细能够参阅一下下面的流程图和源码。
class ContextImpl extends Context {
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < VERSION_CODES。HONEYCOMB) {
//重新去加载磁盘文件
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
}
Sharepreferences在Andorid 7.0及以上进行多进程读写操作的时分,会抛出反常,由于Sharepreferences不支撑多进程形式。多进程同享文件会呈现问题的实质在于,不同进程磁盘读写,线程同步会失效。怎样了解呢?
异步提交进程中,假如此时SharePreference正在IO磁盘文件,但用户退出当时进程,数据没有及时更新文件,提交操作却提早打断,那么数据就丢掉。
要处理Sharepreferences数据丢掉问题,咱们能够选用跨进程计划,如ContentProvider、AIDL、Service。但ContentProvider、AIDL、Service操作文件有点大才小用,咱们能够考虑MMKV或DataStore。
SharedPreferences IO磁盘读写
虚拟内存被操作体系区分成两块:用户空间和内核空间,用户空间是用户程序代码作业的当地,内核空间是内核代码作业的当地。为了安全,用户空间和内核空间是隔离的,即运用户的程序溃散了,内核也不受影响。
那么, IO读取数据为什么会形成数据不同步呢?以用户修正文件为例,假如是IO读取文件遵从下面的流程,首要调用 write,告知内核需求写入数据的开端地址与长度,然后内核将数据复制到内核缓存,最终由操作体系调用,将数据复制到磁盘,完结写入。
假如用户对EditText进行Input输入事情,其他耗时事情导致Input输入事情处于等候状况,时刻超越5s,那么会堵塞主线程,导致ANR。经测验发现,SharedPreferences同步更新进程中,大文件读写操作,耗时超越5s很简略呈现。因而,为了防止呈现ANR,不要运用SharedPreferences进行大文件读写。
MMKV的原理
MMKV的C++层代码比较复杂,小木箱从内存预备、数据安排和写入优化三个方面简略的和咱们聊一下原理。
内存预备
榜首,内存预备方面,经过 mmap 内存映射文件,供给一段可供随时写入的内存块,App 只管往内存写数据,由操作体系担任将内存回写到文件,不用忧虑 crash 导致数据丢掉。
数据安排
第二,数据安排方面,数据序列化方面小木箱选用protobuf协议,pb在功用和空间占用上都有不错的体现。
写入优化
第三,写入优化方面,考虑到首要运用场景是频繁地进行写入更新,咱们需求有增量更新的能力,咱们考虑将增是 kv 目标序列化后,append 到内存结尾。
MMKV之mmap内存读写
由于SharedPreferences有不行精简的xml数据格局、操作文件耗时长、堵塞主线程易呈现数据丢掉和不支撑增量更新坏处,所以有没有SharedPreferences的备胎计划呢?
有! 腾讯的MMKV, MMKV有四大长处,榜首是mmap内存映射,读写快,操作内存相当于操作文件,不用忧虑crash导致数据存储失利。第二是选用protobuf数据格局,功用和巨细更有优势。第三是写入优化,增量更新,巨细缺乏时进行扩容。第四是支撑多进程形式。首要小木箱说一下榜首部分内容mmap,mmap有四个问题需求聊一下。
问题一: mmap是什么?
Linux 经过将一个虚拟内存区域与一个磁盘上的目标关联起来,以初始化这个虚拟内存区域的内容,这个进程称为内存映射(memory mapping)。
对文件进行 mmap,会在进程的虚拟内存分配地址空间,创立映射联系。完结这样的映射联系后,就能够选用指针的办法读写操作这一段内存,而体系会主动回写到对应的文件磁盘上。
问题二: mmap相关于IO磁盘读写有什么长处?
榜首,mmap 对文件的读写操作只需求从磁盘到用户主存的一次数据复制进程,削减了数据的复制次数,进步了文件读写功率,mmap读写操作能够看一下下面的图。
第二,mmap 运用逻辑内存对磁盘文件进行映射,操作内存就相当于操作文件,不需求敞开线程,操作 mmap的速度和操作内存的速度相同快
第三,mmap 供给一段可供随时写入的内存块,App 只管往里边写数据,由操作体系如内存缺乏、进程退出等时分担任将内存回写到文件,不用忧虑 crash 导致数据丢掉
下面来比照一下SharedPreferences 和 MMKV一同存储1000条数据的耗时:
由于MMKV和SharedPreferences都是从map里边读数据,所以读取速度相差不大。由于mmap内存写文件0次复制,SharedPreferences IO磁盘写文件屡次复制,所以MMKV的写入速度优于SharedPreferences。
问题三: mmap映射的内存到磁盘的机遇是什么时分?
mmap映射的内存到磁盘的机遇有四个,榜首个是主动调用msync,第二个是mmap免除映射,第三个是进程退出,第四个是体系关机。
问题四: 怎样更深入的学习mmap?
假如想深入的学习mmap的实践,那么小木箱推荐咱们看一下微信的Mars、货拉拉的Glog、美团的Logan和网易七鱼日志写入计划。
MMKV数据存储和编码格局
数据存储和编码格局方面,mmkv选用protobuf,protobuf数据紧凑,非明文存储。protobuf底层是依据二进制保存的,保存文件十分小,解析速度更快! protobuf比较XML、JSON、Lua有如下优势:
protobuf
数据结构
protobuf底层是依据二进制保存的,保存文件十分小,解析速度更快!
MMKV 默许把文件存放在 $(FilesDir)/mmkv/ 目录, 咱们用010Editor看到protobuf映射文件二进制存储格局如下:
前4个字节表明整个protobuf映射文件中数据的有用长度。那么为什么需求有用长度?
其实,由于mmap内存映射的时分,文件巨细有必要是4096(这个数字与与操作体系位数有关)或许其整数倍,所以并非整个文件内容都是有用数据,需求用有用数据长度来标明有用数据。
从第五个字节开端,依次为k1长-->k1值-->v1长-->v1值-->k2长->k2值-->v2长-->V2值...>v2值-->
那么问题来了,protobuf是怎样做到数据紧凑的呢?咱们来了解一下protobuf的解码规矩:
protobuf
解码规矩
咱们以整型数据来说明,每一个字节的首位作标志位,标志位为1
,则表明该字节无法完好表明数据,需求更多的字节;标志位为0,则表明该字节现已是表明该数据的最终一个字节,该数据的的读取到该字节停止。每一个字节的后七位保存数据。
<=0x7f
的10进制表明为127
,2进制表明为0111111111
。假如写入的数据<=0x7f
,那么一个字节的七个数据位满足表明这个数据,则字节首方位0,后七位写入数据。
假如写入的数据>0x7
,那么一个字节的七个数据位缺乏以表明这个数据,则字节首方位11后七位写入数据,并将原数右移7位,继续履行判别。
protobuf解码事例剖析
编码128
, 即10000000
读取128的protobuf编码, 即1000 000000000 000
编码318242,即00000100110110110010 0010
318242的protobuf
解码
综上所述,一般办法存储是定长的,而protobuf存储办法是变长的。所以,在多数状况下,protobuf的存储办法,会使得数据更小。protobuf的数据格局特征解说了为什么MMKV比SharedPreferences的功用和巨细更有优势。
MMKV增量修正
好了,小木箱介绍完了MMKV数据在文件中的存储结构,这种结构怎样完结增量修正的呢?
原因是MMKV在内存中是一个map表结构。
完结初次mmap映射系的树立后,之后再次写入一个同名key的键值对,该键值对直接存储在映射文件的结尾,并修正有用长度。
当映射联系断开后重新树立映射联系的时分,旧的键值对先写入map表,新的键值对后读入,将会掩盖掉旧的键值对,也就完结了增量修正。
MMKV是在结尾追加新数据,重复数据不进行掩盖,当要产生扩容时进行数据重整。
MMKV多进程操作
Linux中多进程锁一般考虑pthread_mutex,创立于同享内存的 pthread_mutex 是能够用作进程锁的。
可是Android版别的健壮性缺乏,进程被kill时并不会开释锁,导致其他进程一向堵塞。
所以mmkv选用的是文件锁进行多进程同步,可是文件锁存在两个问题:
榜首个问题是不支撑递归加锁:由于文件锁是状况锁,没有计数器,不管加了多少次锁,一个解锁操作就全解掉。只需用到子函数,就十分需求递归锁。
第二个问题是不支撑读写锁晋级/降级:读锁晋级为写锁,写锁能够降级为读锁。关于读锁,咱们允许多进程拜访,写锁则不允许多进程拜访。所以mmkv添加了读写计数器以此支撑这个功用,添加CRC文件校验。
详细逻辑:
-
确保每一个文件存储的数据都比较小,也就说需求把数据依据事务线存储涣散。内存耗费不会过快,数据不用了能够进行开释。
-
还需求在内存缺乏的时分开释一部分内存数据,比方在App中监听onTrimMemory办法,在Java内存吃紧的状况下进行MMKV的trim操作。
-
在不需求运用的时分,最好把MMKV给close掉。
MMKV与SharedPreferences优缺陷比较
写了这么多,小木箱首要简略的总结一下SharedPreferences和MMKV的存储格局、长处和缺陷。最终再经过试验剖析验证一下结论。
MMKV与SharedPreferences功用测验
下面进入咱们的MMKV与SharedPreferences功用测验环节。读写功用方面,不管是ios仍是android上,MMKV的体现均优于SharedPreferences。iOS的MMKV 和 NSUserDefaults 进行比照,重复读写操作 1w 次。功用比较数据如下:
Android,MMKV 和 SharedPreferences、SQLite 进行比照, 重复读写操作 1k 次。成果如下图表。
Android,MMKV 和 SharedPreferences、SQLite 进行比照, 多进程操作功用如下图表。
MMKV 不管是在写入功用仍是在读取功用,都远远超越 MultiProcessSharedPreferences & SQLite & SQLite, 依据SharedPreference以上缺陷,小木箱的团队要抛弃SharedPreferences运用。
MMKV二次开发与迭代
尽管MMKV现已满足优异,可是美中缺乏是不支撑强类型、和SharedPreferences接口无法对齐,国内能兼顾这两个优势的数据存储模型的只要Booster和UniKV。但比较MMKV,Booster有点相形见绌了,由于Booster不支撑多进程并且线程优化个数体现不佳。在百度App 低端机优化-发动功用优化(概述篇)一文中,UniKV,打破体系限制,彻底处理原生 SharedPreferences 有初次读取功用差、创立线程多、卡顿/ANR、多进程支撑差等缺陷,完结流程图大约如下:
UniKV是闭源的,从SDK研制到落地上线,百度应该踩了不少坑,未来小木箱期望能够开发一套便利从MMKV切换到SharedPreferences的开源东西,SharedPreferences0危险替换MMKV。由于篇幅有限,关于MMKV的改造提效,能够参阅后续文章 架构优化 结构论 什么! 从SharedPreferences过渡MMKV,线上溃散率进步3%?
锁优化
说完SharedPreferences优化,咱们进入优化思路的第二个环节锁优化, 学习锁优化之前,简略的和咱们过一下Java的干流锁。
Java干流锁
Java的干流锁一共有六个问题需求搞清楚,搞清楚这6个问题,Java干流锁根底知识掌握的也差不多了。
问题一: 线程要不要锁住同步资源?
从线程是否需求同步视点动身,咱们把锁分为两种。榜首种是失望锁,第二种是达观锁。synchronized、Lock完结类都是失望锁,作用在于其他线程拜访数据的时分,维护数据不会被其他线程修正。很好了解这个概念,只要患得患失,才给自己数据设置修正权限。患得患失的锁普遍失望。
达观锁就不相同了,达观锁比较敞开,唯我独尊,以为没人敢乱动自己的数据,所以从不给自己的数据加锁。
仅仅别的线程拜访自己的数据的时分,判别一下有没有偷偷更新自己的数据,假如数据没有被更新,那么当时线程将自己修正的数据成功写入。
假如数据现已被别的线程更新,那么依据不同的完结办法履行抛出反常或许主动重试操作。
那么达观锁在Java中是怎样完结的呢?
AtomicBoolean等原子类中的递加操作经过CAS自旋完结的。而达观锁在Java中也是依据类似CAS算法等办法来完结
能不能都用失望锁确保数据正确性呢?
其实是不建议的,咱们都知道假如加锁会使读操作的功用大幅下降。那么什么时分不需求加锁呢?
当然是在不更改数据的场景下啦,比方: 批量读文件、批量删去文件等等。这也是达观锁合适的场景。
失望锁相反,加锁能够确保写操作时数据正确,因而,失望锁合适写操作多的场景。
失望锁和达观锁在Java编程中调用办法是怎样的呢?
// ------------------------- 失望锁的调用办法 -------------------------
// synchronized
public synchronized void testMethod() {
// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需求确保多个线程运用的是同一个锁
public void modifyPublicResources() {
lock.lock();
// 操作同步资源
lock.unlock();
}
// ------------------------- 达观锁的调用办法 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger(); // 需求确保多个线程运用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //履行自增1
为什么达观锁能够做到不锁定同步资源也能够正确的完结线程同步呢?
首要是经过CAS办法完结的,是一种无锁算法。在没有线程被堵塞的状况下完结多线程之间的变量同步。JUC包中的原子类便是经过CAS来完结了达观锁。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// unsafe: 获取并操作内存的数据。
private static final Unsafe U = Unsafe.getUnsafe();
// VALUE: 存储value在AtomicInteger中的偏移量。
private static final long VALUE;
// value: 存储AtomicInteger的int值,该属性需求凭借volatile要害字确保其在线程间是可见的。
private volatile int value;
static {
try {
VALUE = U.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
}
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增办法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe。class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe。java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
CAS尽管很高效,可是CAS有三大缺陷:
- 缺陷一: ABA问题。
CAS需求在操作值的时分查看内存值是否产生改变,没有产生改变才会更新内存值。可是假如内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行查看时会发现值没有产生改变,可是实际上是有改变的。ABA问题的处理思路便是在变量前面添加版别号,每次变量更新的时分都把版别号加一,这样改变进程就从“A-B-A”变成了“1A-2B-3A”。 – JDK从1.5开端供给了AtomicStampedReference类来处理ABA问题,详细操作封装在compareAndSet()中。compareAndSet()首要查看当时引证和当时标志与预期引证和预期标志是否相等,假如都相等,则以原子办法将引证值和标志的值设置为给定的更新值。
- 缺陷二: 循环时刻长开支大
CAS操作假如长时刻不成功,会导致其一向自旋,给CPU带来十分大的开支。
- 缺陷三: 只能确保一个同享变量的原子操作
对一个同享变量履行操作时,CAS能够确保原子操作,可是对多个同享变量操作时,CAS是无法确保操作的原子性的。Java从1.5开端JDK供给了AtomicReference类来确保引证目标之间的原子性,能够把多个变量放在一个目标里来进行CAS操作。
问题二: 锁住同步资源失利,线程要不要堵塞
加锁的时分为了让当时线程不堵塞,HotSpot底层引进自旋锁,自旋锁九字真言便是循环加锁 -> 等候的机制,指的是当一个线程测验去获取某一把锁的时分,假如这个锁此时现已被他人获取(占用),那么此线程就无法获取到这把锁,该线程将会堵塞,距离一段时刻后会再次测验获取。详细流程参阅如下:
自旋锁的长处在于削减CPU切换以及恢复现场导致的耗费。
自旋锁缺陷是不能代替堵塞。自旋等候尽管防止了线程切换的开支,但它要占用处理器时刻。假如锁被占用的时刻很短,自旋等候的作用就会十分好。反之,假如锁被占用的时刻很长,那么自旋的线程只会白糟蹋处理器资源。
自旋锁完结原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环便是一个自旋操作,假如修正数值失利则经过循环来履行自旋,直至修正成功。
在自旋锁,中还有三种常见的锁形式:TicketLock、CLHlock和MCSlock
问题三: 多个线程同步竞赛资源的流程细节有没有差异?
说完自旋锁,依照多个线程同步竞赛资源的流程细节有没有差异,小木箱把锁区分为四类,榜首类是无锁,第二类是倾向锁,第三类是轻量级锁,第四类是分量级锁。
这四种锁是指锁的状况,专门针对synchronized的。
那么Synchronized底层的锁优化机制是怎样的呢?
Synchronized底层的锁优化机制一张图能够解说原理。下面小木箱依据下面这张图详细的和咱们聊一下Synchronized。
提到Synchronized底层锁优化机制,咱们不得不提到两个概念,Java头目标和Monitor。
咱们以Hotspot虚拟机为例,Hotspot的目标头首要包括两部分数据:Mark Word(符号字段)、Klass Pointer(类型指针)。
Mark Word首要存储本身的作业时数据,例如 HashCode、GC 年纪、锁相关信息。而Klass Pointer首要指的是指针指向它的类元数据的指针。
为了探究锁的晋级和降级进程,小木箱运用Maven引进openjdk,小木箱打印ClassLayout.parseInstance().toPrintable()办法能够看到object header参数即Java头目标。
咱们能够经过-XX:+UseCompressedOops 翻开指针紧缩,一般目标紧缩后目标头结构为:
咱们也能够经过- -XX:-UseCompressedOops 翻开指针紧缩,一般目标目标头结构为:
说完目标头,小木箱再说说Mark Word(符号字段),Mark Word(符号字段)首要用来存储目标本身的作业时数据,如 hashcode、gc 分代年纪等。Mark Word 的位长度为 JVM 的一个 Word 巨细。Mark Word锁状况能够参阅如下图:
小木箱经过内存信息剖析锁状况如下:
public class Main{
public static void main(String[] args) throws InterruptedException {
L l = new L();
Runnable RUNNABLE = () -> {
while (!Thread.interrupted()) {
synchronized (l) {
String SPLITE_STR = "===========================================";
System.out.println(SPLITE_STR);
System.out.println(ClassLayout.parseInstance(l).toPrintable());
System.out.println(SPLITE_STR);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 3; i++) {
new Thread(RUNNABLE).start();
}
}
}
class L{
private boolean myboolean = true;
}
====================================输出调用日志==================================================
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 5a 97 02 c1 (01011010 10010111 00000010 11000001) (-1056794790)
4 4 (object header) d7 7f 00 00 (11010111 01111111 00000000 00000000) (32727)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 1 boolean L.myboolean true
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
能够看到在榜首行 object header 中 value=5a 对应的 2 进制为 01011010,倒数第三位为 0 表明不是偏量锁,后两位为 10 表明为分量锁。
说完Mark Word(符号字段),小木箱再说说Klass Pointer(类型指针),Klass Pointer拜访办法首要有两种:句柄池和直接指针拜访。
句柄池拜访办法如下:
直接指针拜访办法如下:
最终,小木箱全体比照一下句柄拜访和指针拜访的差异 。
说完Klass Pointer,咱们讨论一下Monitor,Monitor能够了解为一个同步东西或一种同步机制,一般用于描述为一个目标。一般经过成对的MonitorEnter和MonitorExit指令来完结。
Monitor在HotSpot是以ObjectMonitor来完结的,从以下源码,
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0, // 等候中的线程数
_recursions = 0; // 线程重入次数
_object = NULL; // 存储该 monitor 的目标
_owner = NULL; // 指向具有该 monitor 的线程
_WaitSet = NULL; // 等候线程 双向循环链表_WaitSet 指向榜首个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞赛锁时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; // _owner 从该双向循环链表中唤醒线程,
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0; // 前一个具有此监视器的线程 ID
}
关于ObjectMonitor同步行列协作流程能够参阅下图:
咱们能够看出ObjectMonitor,有两个行列,分别是_WaitSet
、_EntryList
,作用是保存 ObjectWaiter 目标列表。
_owner 是一个临界资源, JVM 是经过 CAS 操作来确保其线程安全的,当获取 Monitor 目标的线程进入 _owner 区时, _count 会 + 1。
假如线程调用了 wait() 办法,此时会开释 Monitor 目标, _owner 恢复为空, _count 会- 1。
_cxq:竞赛行列一切恳求锁的线程首要会被放在这个行列中(单向)。_cxq 是一个临界资源 JVM 经过 CAS 原子指令来修正_cxq 行列,每逢有新来的节点入队,_cxq的next 指针总是指向之前行列的头节点,而_cxq 指针会指向该新入队的节点,所以是后发先至。
一同该等候线程进入 _WaitSet 等候行列中,等候被唤醒。锁的完好履行流程能够看一下下图:
Monitor全体进程能够从竞赛、等候、开释和唤醒四个维度剖析。首要咱们说一下竞赛进程
然后咱们说一下,等候进程:
接着咱们说一下开释进程,当某个持有锁的线程履行完同步代码块时,会开释锁并 unpark
后续线程,这便是开释进程。
最终咱们说一下唤醒进程,notify 或许 notifyAll 办法能够唤醒同一个锁监视器下调用 wait 挂起的线程,这便是唤醒进程。
源码就不带咱们过了,感爱好的能够看一下小米的synchronized 完结原理。
总体上来说这四种锁状况晋级流程如下:
无锁指的是不锁住资源,多个线程中只要一个能修正资源成功,其他线程会重试。
倾向锁指的是同一个线程履行同步资源时主动获取资源。 持有倾向锁的线程今后每次进入这个锁相关的同步块时,只需比对一下 mark word 的线程 id 是否为本线程,假如是则获取锁成功。如线程拜访同步代码并获取锁的处理流程如下:
那么假如获取锁之后,产生线程竞赛的状况则怎样撤销倾向锁呢?
多个线程竞赛倾向锁导致倾向锁晋级为轻量级锁,轻量级锁指的是多个线程竞赛同步资源时,没有获取资源的线程自旋等候锁开释。下面咱们来看一下轻量级锁的加锁进程。
说完加锁进程,咱们再来看一下,轻量级锁的解锁进程。
分量级锁指的是多个线程竞赛同步资源时,没有获取资源的线程堵塞等候唤。
synchronized 要害字及 wait
、notify
、notifyAll
这三个办法都是管程的组成部分。能够说管程便是一把处理并发问题的万能钥匙。有两大中心问题管程都是能够处理的:
synchronized
的 monitor
锁机制和 JDK 并发包中的 AQS
是很相似的,只不过 AQS
中是一个同步行列多个等候行列。了解 AQS
的同学能够拿来做个比照。
说了这么多,或许咱们仍是有点乱,倾向锁、轻量级锁、分量级锁概念和优缺陷是什么,小木箱简略的给咱们总结一下:
问题四: 多个线程竞赛锁的一同要不要排队?
说完锁的晋级进程,咱们来讨论一下多个线程竞赛锁的一同要不要排队,依据这个问题咱们把锁分为两类,榜首类是公正锁,第二类对错公正锁。公正锁和非公正锁的概念、长处和缺陷能够参阅下面的图比照。
关于公正锁咱们能够看一下下图进行图形化了解:
关于非公正锁咱们能够看一下下图进行图形化了解:
ReentrantLock里边有一个内部类Sync,Sync承继AQS(AbstractQueuedSynchronizer),添加锁和开释锁的大部分操作实际上都是在Sync中完结的。ReentrantLock有公正锁FairSync和非公正锁NonfairSync两个子类。ReentrantLock默许运用非公正锁,也能够经过结构器来显现的指定运用公正锁。
咱们比照一下公正锁和非公正锁的源码,公正锁比非公正锁多了一个hasQueuedPredecessors()判别条件。
进入hasQueuedPredecessors(),能够看到hasQueuedPredecessors()办法首要做一件事情:首要是判别当时线程是否位于同步行列中的榜首个。假如是则回来true,不然回来false。
综上,公正锁便是经过同步行列来完结多个线程依照请求锁的次序来获取锁,然后完结公正的特性。非公正锁加锁时不考虑排队等候问题,直接测验获取锁,所以存在后请求却先获得锁的状况。
公正锁和非公正锁的差异是公正锁假如在当时线程不是具有有锁的线程时,就不能添加锁。所以公正锁和非公正锁添加的都是独享锁。独享锁咱们在问题六详细了解。
问题五: 一个线程能不能获取同一把锁?
提到一个线程能不能获取同一把锁,咱们不得不提到可重入锁和非可重入锁,可重入锁和非可重入锁差异能够看看下面的表格
- synchronized可重入锁
//可重入,便是能够重复获取相同的锁,
//synchronized和ReentrantLock都是可重入的
// 可重入下降了编程复杂性
public class WhatReentrant2 {
public static void main (String[] args) {
ReentrantLock lock = new ReentrantLock ();
new Thread ( new Runnable () {
@Override public void run () {
try { lock. lock ();
System.out. println ( "第1次获取锁,这个锁是:" + lock);
int index = 1 ;
while ( true ) {
try { lock. lock (); System.out. println ( "第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread. sleep ( new Random (). nextInt ( 200 ));
} catch (InterruptedException e)
{ e。 printStackTrace (); }
if (index == 10 ) { break ; } }
finally { lock. unlock (); } } }
finally { lock. unlock (); } } }). start ();
}
- 不可重入锁
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
关于可重入锁咱们能够看一下下图进行图形化了解:
关于非可重入锁咱们能够看一下下图进行图形化了解:
为什么可重入锁就能够在嵌套调用时能够主动获得锁呢?
ReentrantLock和NonReentrantLock都承继父类AQS。
其父类AQS中维护了一个同步状况status来计数重入次数,status初始值为0。当线程测验获取锁时,可重入锁先测验获取并更新status值,假如status == 0表明没有其他线程在履行同步代码,则把status置为1,当时线程开端履行。
假如status != 0,则判别当时线程是否是获取到这个锁的线程,假如是的话履行status+1,且当时线程能够再次获取锁。
非可重入锁是直接去获取并测验更新当时status的值,假如status != 0的话会导致其获取锁失利,当时线程堵塞。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 取到当时锁的个数
int w = exclusiveCount(c); // 取写锁的个数w
if (c != 0) { // 假如现已有线程持有了锁(c!=0)
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) // 假如写线程数(w)为0(换言之存在读锁) 或许持有锁的线程不是当时线程就回来失利
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // 假如写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 假如当且写线程数为0,并且当时线程需求堵塞那么就回来失利;或许假如经过CAS添加写线程数失利也回来失利。
return false;
setExclusiveOwnerThread(current); // 假如c=0,w=0或许c>0,w>0(重入),则设置当时线程或锁的具有者
return true;
}
开释锁时,可重入锁相同先获取当时status的值,在当时线程是持有锁的线程的条件下。假如status-1 == 0,则表明当时线程一切重复获取锁的操作都现已履行结束,然后该线程才会真实开释锁。而非可重入锁则是在确认当时线程是持有锁的线程之后,直接将status置为0,将锁开释。
public void unlock() {
sync。releaseShared(1);
}
//.............................................
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//.............................................
protected final boolean tryReleaseShared(int unused) {
// ...............
for (;;) {
// 可重入锁相同先获取当时status的值,
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 在当时线程是持有锁的线程的条件下。假如status-1 == 0,调用doReleaseShared
return nextc == 0;
}
}
//.............................................
// 真实开释锁
private void doReleaseShared() {
}
重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来比照剖析为什么非可重入锁在重复调用同步资源时会呈现死锁。
问题六: 多个线程能不能同享同一把锁?
在同一个线程在外层办法获取锁的时分,再进入该线程的内层办法会主动获取锁(条件锁目标得是同一个目标或许class),不会由于之前现已获取过还没开释而堵塞。
依据多个线程能不能同享同一把锁,咱们把锁分为独享锁和同享锁,独享锁和同享锁的差异能够参阅下面的表格:
首要咱们来说一下同享锁ReentrantReadWriteLock有两把锁:ReadLock(读锁)和WriteLock(写锁)。
ReadLock和WriteLock是靠内部类Lock完结的锁,而Lock是Sync的完结接口。因而,ReadLock和WriteLock是靠内部类Sync完结的锁。详细代码如下:
在ReentrantReadWriteLock里边,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁办法不相同。读锁是同享锁,写锁是独享锁。
读锁的同享锁可确保并发读十分高效,而读写、写读、写写的进程互斥,由于读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性比较一般的互斥锁有了很大进步。
那读锁和写锁的详细加锁办法有什么差异呢?首要先看一下写锁的加锁源码:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 取到当时锁的个数
int w = exclusiveCount(c); // 取写锁的个数w
if (c != 0) { // 假如现已有线程持有了锁(c!=0)
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) // 假如写线程数(w)为0(换言之存在读锁) 或许持有锁的线程不是当时线程就回来失利
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // 假如写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 假如当且写线程数为0,并且当时线程需求堵塞那么就回来失利;或许假如经过CAS添加写线程数失利也回来失利。
return false;
setExclusiveOwnerThread(current); // 假如c=0,w=0或许c>0,w>0(重入),则设置当时线程或锁的具有者
return true;
}
接着看一下读锁的加锁源码:
protected final int tryAcquireShared ( int unused) { Thread current = Thread。currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return - 1 ; // 假如其他线程现已获取了写锁,则当时线程获取读锁失利,进入等候状况 int r = sharedCount(c);
// 假如当时线程获取了写锁或许写锁未被获取 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// ,则当时线程(线程安全,依托CAS确保)添加读状况,成功获取读锁。 if (r == 0 ) {
// 读锁的每次开释(线程安全的,或许有多个读线程一同开释读锁)均削减读状况,削减的值是“1<<16”。 firstReader = current; firstReaderHoldCount = 1 ; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh。tid != getThreadId(current)) cachedHoldCounter = rh = readHolds。get(); else if (rh.count == 0 ) readHolds.set(rh); rh。count++; } return 1 ; } return fullTryAcquireShared(current);
// 所以读写锁才干完结读读的进程同享,而读写、写读、写写的进程互斥。
}
综上所述:当某一个线程调用lock办法获取锁时,假如同步资源没有被其他线程锁住,那么当时线程在运用CAS更新state成功后就会成功抢占该资源。
假如公共资源被占用且不是被当时线程占用,那么就会加锁失利。因而,能够确认ReentrantLock不管读操作仍是写操作,添加的锁都是都是独享锁。
由于篇幅有限,关于并发编程根底知识,能够参阅后续文章并发编程 根底篇 Android这样学锁,你也能做好功用监控!
Java锁监控
说完Java干流的锁,咱们再来聊一聊Java的锁监控。客户端监控的锁有synchronized 锁、 CAS、Native 锁等,但在咱们日常开发中synchronized 锁占比是最大的,作为一定量级的国民运用抖音也只对synchronized锁进行了监控。因而,Java的锁优化咱们只谈及synchronized 锁的优化。
Java锁的优化评判的点首要有以下4个,榜首个是安稳性, 第二个是准确性 ,第三个是拓展性。最终一个是防劣化。首要咱们聊聊准确性,准确性当然是指精准的Hook持锁时长。
在上文Java干流锁咱们知道了,假如乱用锁,那么成果是堵塞UI线程,导致制作推迟,呈现卡顿,乃至 ANR等。怎样监控锁持有时刻是咱们APM建造亟需处理的痛点。之前说过Monitor全体进程分为竞赛、等候、开释和唤醒四个维度。而影响锁占用时长产生在锁竞赛阶段和锁等候阶段。
首要咱们说一下锁的竞赛阶段,咱们经过monitor.cc源码能够看到,锁的开端和锁的结束,分别调用ATRACE_BEGIN(...)
和 ATRACE_END()
,那么ATRACE_BEGIN(...)
和 ATRACE_END()
办法作用是什么呢?
咱们经过trace-dev源码能够看到ATRACE_BEGIN(...)
和 ATRACE_END()
的完结,ATRACE_BEGIN(...)
和 ATRACE_END(...)
底层运用 write 将字符串写入一个特别的 atrace_marker_fd
线上HookATRACE_BEGIN(...)
和 ATRACE_END()
两个点位,ATRACE_END()
时刻戳减去ATRACE_BEGIN(...)
时刻戳得到的是持锁时长。
因而经过 hook libcutils。so 的 write 办法,并按 atrace_marker_fd 过滤,就完结了对 ATRACE_BEGIN(...)
和 ATRACE_END()
的阻拦,核算出堵塞时长,解析 monitor contention with owner...
日志能够监控到线上用户的锁问题。
Native的Hook计划推荐运用爱奇艺的XHook,XHook咱们能够看一下 运用xhook安卓体系底层抹机原理 文章进行学习。
然后咱们说一下锁的等候阶段,其实便是获取 Java 调用栈,那么怎样获取Java调用栈呢?能够运用Thread.getStackTrace()
办法。
异步获取仓库,在 MonitorEnter 的时分通知子线程 5ms 之后抓取仓库,MonitorExit 核算堵塞时长,并结合仓库数据一同放入行列,等候上报APM监控渠道。假如 MonitorExit 时不满足指定的阈值,那么取消抓栈和上报。
当然,由于锁监控会批量写日志加上Hook计划本身就有一定功用开支,所以仅对灰度测验中的部分用户敞开了锁监控。字节的PRD计划如下:
咱们能看到设备信息、堵塞时长、调用仓库等信息。
这样,能够经过日志很快定位到一切超越阈值持有时长的锁并进行相应的优化。
至此关于Java锁监控思路讲解结束,待东西链上线后咱们需求灰度一部分用户进行安稳性测验。为了进步扩展性, 事务能够依据场景敞开和封闭采集功用,也能够搜集指定时刻内的锁,比方发动阶段能够搜集 32ms 的锁,其它阶段搜集 16ms 的锁。为了防劣化,当锁占用时长总数量采集超标,预发步环境运用QA机器人进行预警。
4.4.3 调度结构
说完优化思路,咱们再说一下调度结构,关于大型App来说,发动使命多,使命依靠复杂。保证使命逻辑的单一性,解耦发动使命逻辑,合理运用多核CPU优势,进步线程作业功率是要点重视的问题。
为了运用多核cpu,进步使命履行功率,让单个使命职责愈加明晰,代码愈加高雅从而进步发动速度,咱们会尽或许让这些作业并发进行。
但这些作业之间或许存在前后依靠的联系,咱们又需求想办法确保他们履行次序的正确性。
所以咱们要做的作业是将使命颗粒化,界说好自己的使命,并描述它依靠的使命,将它添加到Project中。结构会主动并发有序地履行这些使命,并将履行的成果抛出来。
那么怎样对使命进行分类呢?
使命进行分类策阅能够经过Alpha等发动器把发动使命办理起来。详细分为四个过程: 榜首个过程是将发动使命原子化,分为各个使命。
第二个过程是运用有向无环图办理发动使命。前后依靠的使命串行,无依靠的使命线程池化并行。优先级高的使命在前,优先级低的使命在后。
第三个过程是发动使命集中化,分使命区块:中心使命,首要使命,推迟使命,懒加载使命。中心使命在attachBaseContext中履行,首要使命在发动页或首页履行,推迟使命在首页后闲暇时刻履行,懒加载使命在特定的机遇履行。
最终一个过程是发动使命核算化,供给使命的耗时核算和卡口。
使命办理结构图参阅如下:
使命分类办理与事务初始化机遇有关,比方像热修正和网络等中心使命,需求优先初始化,推送、地图首要使命优先级比中心使命低。关于耗时长使命,不但要落库并且要凭借QA机器人进行监测告警。
为了校验发动器优化体会正向仍是负向的,供给安稳的降级计划并随时回归对照版别必不可少。
4.4.4 事务结构
说完调度结构,咱们说一下事务结构,事务结构分为发现问题、问题问题和谐、问题优化和优化作用验证四个方向
首要,问题发现方面,线下发现问题首要经过东西,如 Trace 东西发现主线程耗时严重问题,主线程锁等候问题等,Hook 东西发现主线程 I/O 问题,线程创立问题等;线上发现问题首要经过线上打点和试验防劣化机制;
然后,问题协同方面,架构组与事务沟通问题及技能计划,必要时协助其剖析并优化,确认上线排期;
接着,问题优化方面,首要由事务来完结详细优化,假如涉及根底机制相关作业,则会由架构组来主导优化,在整个优化中,调度优化为首要优化办法,事务可快速接入调度机制完结优化。
最终,问题优化和优化作用验证方面,及时跟进问题修正状况,在发版前回归问题,跟进线上优化作用,有些优化需求平衡事务目标和功用目标,假如优化不及预期需继续协同优化。
总结下来就两个字: 闭环。整个团队开发进程中,咱们都是在明确职责鸿沟状况下,做自己可控的使命。不管优化成果正向仍是负向,要有始有终,让领导和团队觉的你是一个靠谱的人。
五、总结与展望
浅析Android发动优化首要说了四部分内容,榜首部分内容是发动根底,第二部分内容是发动优化价值,第三部分内容是发动优化事务痛点,第四部分内容是总结与展望。
第三部分内容首要分为四个方面,榜首个方面是防劣化机制建造,第二个方面是高功用东西,第三个方面是调度结构,第四个方面是事务结构。
2022年企业对app
的发动功用做了愈加苛刻的要求。经企业内部安稳性大数据渠道剖析,发动耗时每增加200ms
将带来5w
用户的留存减缩。
发动功用是app
运用体会的门面,发动进程耗时较长很或许导致用户丢失,导致用户对公司产品爱好骤减。
因而,发动功用优化成为了团队的重中之重的优化专项。而发动防劣化机制建造和锁监控是发动功用优化要害打破口。运用东西下降企业app发动速度是小木箱下一篇侧重讲解的话题。
下一篇东西论会从上而下带咱们揭秘常见发动优化东西。我是小木箱,咱们下一篇见~
优质技能计划参阅
- github.com/appium/appi…
- github.com/google/perf…
- github.com/bytedance/t…
- 开发必读:网易专家解读Android ABTest 结构规划
- GitHub – TJHello/ABTest: ABTest for Umeng
- Android SDK 集成(A/B Testing)
- Android ABTest 规划与原理
- hook 运用 trace 日志
- 百度App 低端机优化-发动功用优化(概述篇)
- 货拉拉用户端体会优化–发动优化篇
- MMKV for Android 多进程规划与完结
- 抖音 Android 功用优化系列:Java 锁优化
- 不可不说的Java“锁”事
- 火山引擎 A/B 测验的考虑与实践