//优点:避免启动app时白屏黑屏等现象//缺点:容易造成点击桌面图标无响应//(可以配合三方库懒加载,异步初始化等方案使用,减少初始化时长)//实现如下//0. appTheme <!-- Base application theme. --><style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/c_ff000000</item> <item name="colorPrimaryDark">@color/c_ff000000</item> <item name="colorAccent">@color/c_ff000000</item> <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item></style>//1. styles.xml中设置//1.1 禁用预览窗口<style name="AppTheme.Launcher"> <item name="android:windowBackground">@null</item> <item name="android:windowDisablePreview">true</item></style>//1.2 指定透明背景<style name="AppTheme.Launcher"> <item name="android:windowBackground">@color/c_00ffffff</item> <item name="android:windowIsTranslucent">true</item></style>//2. 为启动页/闪屏页Activity设置theme<activity android:name=".splash.SplashActivity" android:screenOrientation="portrait" android:theme="@style/AppTheme.Launcher"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter></activity>//3. 在该Activity.onCreate()中设置AppTheme(设置布局id之前)//比如我是基类中单独抽取的获取布局id方法,那么在启动页中重写此方法时加入如下配置: @Overrideprotected int getContentViewId() { setTheme(R.style.AppTheme_Launcher); return R.layout.activity_splash;}//优点:避免点击桌面图标无响应 //缺点:拉长总的闪屏时长//(可以配合三方库懒加载,异步初始化等方案使用,减少初始化时长)//1. 就是给windowBackground设置一个背景图片<style name="AppTheme.Launcher"> <item name="android:windowBackground">@drawable/bg_splash</item> <item name="android:windowFullscreen">true</item></style> //2. bg_splash文件如下(使用layer-list实现) <?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/color_ToolbarLeftItem" /> <item> <bitmap android:antialias="true" android:gravity="center" android:src="@drawable/ic_splash" /> </item></layer-list>//3. 为启动页/闪屏页Activity设置theme<activity android:name=".splash.SplashActivity" android:screenOrientation="portrait" android:theme="@style/AppTheme.Launcher"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter></activity> //4. 在该Activity.onCreate()中设置AppTheme(设置布局id之前)//比如我是基类中单独抽取的获取布局id方法,那么在启动页中重写此方法时加入如下配置: @Overrideprotected int getContentViewId() { setTheme(R.style.AppTheme_Launcher); return R.layout.activity_splash;} //通过sched查看线程切换数据 proc/[pid]/sched: nr_voluntary_switches: 主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是IO。 nr_involuntary_switches: 被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU。//- 注意区分任务类型:// - IO密集型任务:不消耗CPU,核心池可以很大,如文件读写,网络请求等。// - CPU密集型任务:核心池大小和CPU核心数相关,如复杂的计算,需要使用大量的CPU计算单元。//// 执行的任务是CPU密集型DispatcherExecutor.getCPUExecutor().execute(YourRunable());// 执行的任务是IO密集型DispatcherExecutor.getIOExecutor().execute(YourRunable());/** * @Author: LiuJinYang
* @CreateDate: 2020/12/16
*
* 实现用于执行多类型任务的基础线程池
*/
public class DispatcherExecutor { /** * CPU 密集型任务的线程池
*/
private static ThreadPoolExecutor sCPUThreadPoolExecutor; /** * IO 密集型任务的线程池
*/
private static ExecutorService sIOThreadPoolExecutor; /** * 当前设备可以使用的 CPU 核数
*/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); /** * 线程池核心线程数,其数量在2 ~ 5这个区域内
*/
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5)); /** * 线程池线程数的最大值:这里指定为了核心线程数的大小
*/
private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE; /** * 线程池中空闲线程等待工作的超时时间,当线程池中
* 线程数量大于corePoolSize(核心线程数量)或
* 设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,
* 线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。
* 否则,线程会永远等待新的工作。
*/
private static final int KEEP_ALIVE_SECONDS = 5; /** * 创建一个基于链表节点的阻塞队列
*/
private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>(); /** * 用于创建线程的线程工厂
*/
private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory(); /** * 线程池执行耗时任务时发生异常所需要做的拒绝执行处理
* 注意:一般不会执行到这里
*/
private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
Executors.newCachedThreadPool().execute(r);
}
}; /** * 获取CPU线程池
*
* @return CPU线程池
*/
public static ThreadPoolExecutor getCPUExecutor() {
return sCPUThreadPoolExecutor;
} /** * 获取IO线程池
*
* @return IO线程池
*/
public static ExecutorService getIOExecutor() {
return sIOThreadPoolExecutor;
} /** * 实现一个默认的线程工厂
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "TaskDispatcherPool-" + POOL_NUMBER.getAndIncrement() + "-Thread-"; } @Override public Thread newThread(Runnable r) { // 每一个新创建的线程都会分配到线程组group当中 Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) { // 非守护线程 t.setDaemon(false); } // 设置线程优先级 if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } } static { sCPUThreadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER); // 设置是否允许空闲核心线程超时时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。否则,线程会永远等待新的工作。 sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true); // IO密集型任务线程池直接采用CachedThreadPool来实现, // 它最多可以分配Integer.MAX_VALUE个非核心线程用来执行任务 sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY); }} //1. 通过 systrace 单独查看整个启动过程 GC 的时间 python systrace.py dalvik -b 90960 -a com.sample.gc //2. 通过Debug.startAllocCounting监控启动过程总GC的耗时情况 // GC使用的总耗时,单位是毫秒 Debug.getRuntimeStat("art.gc.gc-time"); // 阻塞式GC的总耗时 Debug.getRuntimeStat("art.gc.blocking-gc-time"); //如果发现主线程出现比较多的 GC 同步等待,就需要通过 Allocation 工具做进一步的分析 Linux 文件系统从磁盘读文件的时候,会以 block 为单位去磁盘读取,一般 block 大小是 4KB。也就是说一次磁盘读写大小至少是 4KB,然后会把 4KB 数据放到页缓存 Page Cache 中。如果下次读取文件数据已经在页缓存中,那就不会发生真实的磁盘 I/O,而是直接从页缓存中读取,大大提升了读的速度。例如读取文件中1KB数据,因为Buffer不小心写成了 1 byte,总共要读取 1000 次。那系统是不是真的会读1000次磁盘呢?事实上1000次读操作只是我们发起的次数,并不是真正的磁盘 I/O 次数,我们虽然读了 1000 次,但事实上只会发生一次磁盘 I/O,其他的数据都会在页缓存中得到。Dex文件用的到的类和安装包APK里面各种资源文件一般都比较小,但是读取非常频繁。
我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数;
// 启动过程类加载顺序可以通过复写 ClassLoader 得到class MyClassLoader extends PathClassLoader { public Class<?> findClass(String name) { //将name记录到文件 writeToFile(name,"coldstart_classes.txt"); return super.findClass(name); }}//然后通过ReDex的Interdex调整类在Dex中的排列顺序,最后可以利用 010 Editor 查看修改后的效果。 //Hook,利用 Frida 实现获得 Android 资源加载顺序的方法resourceImpl.loadXmlResourceParser.implementation=function(a,b,c,d){ send('file:'+a) return this.loadXmlResourceParser(a,b,c,d)}resourceImpl.loadDrawableForCookie.implementation=function(a,b,c,d,e){ send("file:"+a) return this.loadDrawableForCookie(a,b,c,d,e)}//Frida相对小众,后面会替换其他更加成熟的 Hook 框架//调整安装包文件排列需要修改 7zip 源码实现支持传入文件列表顺序,同样最后可以利用 010 Editor 查看修改后的效果;* 所谓创新,不一定是创造前所未有的东西。我们将已有的方案移植到新的平台,并且很好地结合该平台的特性将其落地,就是一个很大的创新//Dalvik 平台: 将 classVerifyMode 设为 VERIFY_MODE_NONE// Dalvik Globals.hgDvm.classVerifyMode = VERIFY_MODE_NONE;// Art runtime.ccverify_ = verifier::VerifyMode::kNone;//ART 平台要复杂很多,Hook 需要兼容几个版本//在安装时大部分 Dex 已经优化好了,去掉 ART 平台的 verify 只会对动态加载的 Dex 带来一些好处//Atlas 中的dalvik_hack-3.0.0.5.jar可以通过下面的方法去掉 verifyAndroidRuntime runtime = AndroidRuntime.getInstance();runtime.init(context);runtime.setVerificationEnabled(false);//这个黑科技可以大大降低首次启动的速度,代价是对后续运行会产生轻微的影响。同时也要考虑兼容性问题,暂时不建议在 ART 平台使用1. 打包资源文件,生成R.java文件(使用工具AAPT)
2. 处理AIDL文件,生成java代码(没有AIDL则忽略)
3. 编译 java 文件,生成对应.class文件(java compiler)
4. .class 文件转换成dex文件(dex)
5. 打包成没有签名的apk(使用工具apkbuilder)
6. 使用签名工具给apk签名(使用工具Jarsigner)
7. 对签名后的.apk文件进行对齐处理,不进行对齐处理不能发布到Google Market(使用工具zipalign)其中第4步,单个dex文件中的方法数不能超过65536,不然编译会报错:Unable to execute dex: method ID not in
0, 0xffff: 65536, 所以我们项目中一般都会用到multidex:
1. gradle中配置
defaultConfig {
...
multiDexEnabled true
}
implementation 'androidx.multidex:multidex:2.0.1'2. Application中初始化
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}然鹅,这个multidex过程是比较耗时的,那么能否针对这个问题进行优化呢?
需要注意的是闪屏页的Activity,包括闪屏页中引用到的其它类必须在主dex中,不然在MultiDex.install之前加载这些不在主dex中的类会报错Class Not Found。这个可以通过gradle配置,如下:defaultConfig { //分包,指定某个类在main dex multiDexEnabled true multiDexKeepProguard file('multiDexKeep.pro') // 打包到main dex的这些类的混淆规制,没特殊需求就给个空文件 multiDexKeepFile file('maindexlist.txt') // 指定哪些类要放到main dex}DispatcherExecutor.getCPUExecutor().execute(new Runnable() { @Override public void run() { long startTime = System.currentTimeMillis(); MainActivity mainActivity = new MainActivity(); LjyLogUtil.d( "preNewActivity 耗时: " + (System.currentTimeMillis() - startTime)); }});对象第一次创建的时候,java虚拟机首先检查类对应的Class
对象是否已经加载。如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。
- CPU工作模式
performance:最高性能模式,即使系统负载非常低,cpu也在最高频率下运行。
powersave:省电模式,与performance模式相反,cpu始终在最低频率下运行。
ondemand:CPU频率跟随系统负载进行变化。
userspace:可以简单理解为自定义模式,在该模式下可以对频率进行设定。adb shell am start -W com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity执行结果:Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivity }Status: okLaunchState: COLD Activity: com.ljy.publicdemo.lite/com.ljy.publicdemo.activity.MainActivityTotalTime: 2065WaitTime: 2069Complete//LaunchState表示冷热温启动//TotalTime:表示所有Activity启动耗时。(主要数据,包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程)//WaitTime:表示AMS启动Activity的总耗时。除了指标的监控,启动的线上堆栈监控更加困难。Facebook 会利用 Profilo 工具对启动的整个流程耗时做监控,并且在后台直接对不同的版本做自动化对比,监控新版本是否有新增耗时的函数。对于启动优化要警惕 KPI 化,要解决的不是一个数字,而是用户真正的体验问题。
/** * @Author: LiuJinYang
* @CreateDate: 2020/12/14
*
* 在项目中需要统计时间的地方加入打点, 比如
* 应用程序的生命周期节点。
* 启动时需要初始化的重要方法,例如数据库初始化,读取本地的一些数据。
* 其他耗时的一些算法。
*/
public class TimeMonitor {
private int mMonitorId = -1; /** * 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
*/
private HashMap<String, Long> mTimeTag = new HashMap<>();
private long mStartTime = 0; public TimeMonitor(int mMonitorId) { LjyLogUtil.d("init TimeMonitor id: " + mMonitorId); this.mMonitorId = mMonitorId; } public int getMonitorId() { return mMonitorId; } public void startMonitor() { // 每次重新启动都把前面的数据清除,避免统计错误的数据 if (mTimeTag.size() > 0) { mTimeTag.clear(); } mStartTime = System.currentTimeMillis(); } /** * 每打一次点,记录某个tag的耗时
*/
public void recordingTimeTag(String tag) {
// 若保存过相同的tag,先清除
if (mTimeTag.get(tag) != null) {
mTimeTag.remove(tag);
}
long time = System.currentTimeMillis() - mStartTime;
LjyLogUtil.d(tag + ": " + time);
mTimeTag.put(tag, time);
} public void end(String tag, boolean writeLog) { recordingTimeTag(tag); end(writeLog); } public void end(boolean writeLog) { if (writeLog) { //写入到本地文件 } } public HashMap<String, Long> getTimeTags() { return mTimeTag; }}/** * @Author: LiuJinYang
* @CreateDate: 2020/12/14
*/
public class TimeMonitorManager {
private HashMap<Integer, TimeMonitor> mTimeMonitorMap; private TimeMonitorManager() { this.mTimeMonitorMap = new HashMap<>(); } private static class TimeMonitorManagerHolder { private static TimeMonitorManager mTimeMonitorManager = new TimeMonitorManager(); } public static TimeMonitorManager getInstance() { return TimeMonitorManagerHolder.mTimeMonitorManager; } /** * 初始化打点模块
*/
public void resetTimeMonitor(int id) {
if (mTimeMonitorMap.get(id) != null) {
mTimeMonitorMap.remove(id);
}
getTimeMonitor(id).startMonitor();
} /** * 获取打点器
*/
public TimeMonitor getTimeMonitor(int id) {
TimeMonitor monitor = mTimeMonitorMap.get(id);
if (monitor == null) {
monitor = new TimeMonitor(id);
mTimeMonitorMap.put(id, monitor);
}
return monitor;
}
}//1. 集成aspectj //根目录build.gradle中 classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10' //app module的build.gradle中 apply plugin: 'android-aspectjx' //如果遇到报错Cause: zip file is empty,可添加如下配置 android{ aspectjx { include 'com.ljy.publicdemo' } }//2. 创建注解类 @Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface GetTime { String tag() default "";}//3. 使用aspectj解析注解并实现耗时记录@Aspectpublic class AspectHelper { @Around("execution(@GetTime * *(..))") public void getTime(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature joinPointObject = (MethodSignature) joinPoint.getSignature(); Method method = joinPointObject.getMethod(); boolean flag = method.isAnnotationPresent(GetTime.class); LjyLogUtil.d("flag:"+flag); String tag = null; if (flag) { GetTime getTime = method.getAnnotation(GetTime.class); tag = getTime.tag(); } if (TextUtils.isEmpty(tag)) { Signature signature = joinPoint.getSignature(); tag = signature.toShortString(); } long time = System.currentTimeMillis(); try { joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } LjyLogUtil.d( tag+" get time: " + (System.currentTimeMillis() - time)); }}//目前 Epic 支持 Android 5.0 ~ 11 的 Thumb-2/ARM64 指令集,arm32/x86/x86_64/mips/mips64 不支持。//1. 添加epic依赖implementation 'me.weishu:epic:0.11.0'//2. 使用epicpublic static class ActivityMethodHook extends XC_MethodHook{ private long startTime; @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); startTime = System.currentTimeMillis(); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); Activity act = (Activity) param.thisObject; String methodName=param.method.getName(); LjyLogUtil.d( act.getLocalClassName()+"."+methodName+" get time: " + (System.currentTimeMillis() - startTime)); }}private void initEpic() { //对所有activity的onCreate执行耗时进行打印 DexposedBridge.hookAllMethods(Activity.class, "onCreate", new ActivityMethodHook());}//也可以用于锁定线程创建者DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); Thread thread = (Thread) param.thisObject; LjyLogUtil.i("stack " + Log.getStackTraceString(new Throwable())); }});原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。