安卓优化

274 阅读19分钟
1APP启动 首页白屏
第一种 设置一个图片<br>
<item name="android:windowBackground">@mipmap/ic_launcher</item><br>
第二种设置为透明<br>
<item name="android:windowIsTranslucent">true</item><br>
第三种<br>
<item name="android:windowBackground">@null</item><br>
<item name="android:windowDisablePreview">true</item><br>
代码设置setTheme(R.style.)

TRACE工具使用

File file = new File(Environment.getExternalStorageDirectory(), "app1.trace");
Log.i(TAG, "onCreate: " + file.getAbsolutePath());
//开始
Debug.startMethodTracing(file.getAbsolutePath());
//结束
Debug.stopMethodTracing();
adb pull /storage/emulated/0/app1.trace把文件拉出来分析

总结优化方案:

1.异步加载     没建handler    没操作UI 对异步要求不高(采用CountDownLatch确保异步任务完成后才到下一个阶段)->异步启动器
    val cnt = CountDownLatch(2)
    val thread = Thread() {
        Thread.sleep(1000)
        cnt.countDown()//计数器-1
    }
    thread.start()
    cnt.await() //等待任务完成在向下执行
2.懒加载     用到的时候再初始化,如网络,数据库操作
    启动主要优化 application调用oncreate->加载 XML->activity 调用oncreate
    写一个专门记时的类
    使用aop(AspectJx)获取每个类的时间
3 预加载
    吧任务放到集合中-> CPU空闲是mIdleHandler便会回调自身的queueIdle方法,在将任务拿出来执行
        IdleHandler = new MessageQueue.IdleHandler
        Looper.myQueue().addIdleHandler(mIdleHandler);
        
2 UI绘制 使用的工具
Choreographer 可以实时显示fps
AOP Hook 没有侵入性可以知道每个控件的耗时
dp = px / density(密度),
DPI / 160 = density,

刷新机制

垂直同步:每16ms发送一次 VSYNC信号,如果每次16ms内操作没有计算完就会丢帧
双缓存:
    如果上一帧还没有显示完,数据就更新了,会有闪烁
    UI总是先在一个buffer中绘制,在和另一个交换,就是说当buffer数据准备好了在切换buffer

解决屏幕卡顿

view的绘制经理 measure layout draw 3个阶段,这3个阶段是有CPU来负责的都是在UI线程完成的
在RederThread线程中将部分数据交给GPU,并将数据缓存在buff中
手机从buff中读取数据显示到屏幕上
GPU向buffer存,屏幕向buffer取这就有并发问题
    引入双缓存机制,两个buffer
UI卡顿就是因为丢帧 16ms以内没有完成CPU和GPU的计算 渲染
choreographer编舞者,收到垂直同步信号(vsync),并进行view绘制操作的 ,是一个承上启下的角色
    承上: 接受应用层的各种callback输入,包括input,动画 绘制,不会直接执行会缓存到队列中
    启下: 负责接受硬件发出的vsync信号,收到信号从队列中取出任务,调用run方法进行绘制
同步屏障消息
    handler设置异步消息msg.setAsynchronous(true); 
    如果给msgQ设置同步屏障,会优先执行异步msg
卡顿的分析->
     1 使用shell命令分析CPU耗时
        造成卡顿的原因很多种,最终反映到CPU时间上
        CPU资源使用: 算法效率低,没有缓存
        CPU资源争抢
        CPU资源利用率低
      2 卡顿检测方案
        loop中有一个mLogging对象,msg处理前后都会调用他进行打印
        msg执行前mLogging打印 Dispatching to 执行后打印 finished to
        主线程发送卡顿肯定是在dispathMessage中发生了耗时操作
        
        通过Looper.getMainLooper().setMessageLogging()传入自定义的Printer对象
        执行消息前后都会回调自己,从而判断前后打印的间隔

总结优化方案

setcontentview 布局加载的源码流程
    1 setcontentview() 获取context的viewGroup 将id通过 layoutInflater.form().inflate设置给viewGroup
    2 inflate()  获取到XmlResourceParser() xml的解析器
    3 解析器会调用一个Native方法 openXMLAssetNative()这个方法是通过IO流的方法将xml加载到内存中,因为捕获了IO异常
    4 会通过createViewTag中的每一个tag创建具体的view对象
    5 内部按照优先顺序Factory2和Factory的onCreateView(), createView(),在方法采用了构造器反射的方式实现
inflate第三个参数 true false左右
    false系统会来加载view true是用户自定义view系统不会加载
发现问题
    1 xml文件映射到内存会有io操作.io过程可能会卡顿
    2 布局加载会反射,反射也有可能卡顿
    3 布局的层级
    4 不合理的嵌套,会重绘
解决问题
    1 异步加载xml文件使用AsyncLayoutInflater,在子线程初始化好通过handler发送给主线程显示
    2 使用ConstraintLayout 和merge,减少布局的嵌套
    3 减少过渡
    4 textview settext()   如果是wrap_content比较耗时,底层会调用checkForRelayout,会根据文字的多少重新计算长度,将宽度设置为固定值或者match_parent的时候会大幅度减少绘制时间
    5 使用include复用布局
    6 少用wrap_content会增加计算成本,设置已知宽高
1 CPU 减少xml转换成对象的时间
    减少嵌套
    相同布局抽取(会从GPU缓存中直接使用)
    使用merge包裹view合并到上一层, CPU减少一次计算
    不合理的使用根布局也有可能会过度重绘
    减少重绘 
        1 去掉activity的主题色 <item name="android:windowBackground">@null</item>  或者 getWindow().setBackgroundDrawable(null)
2 GPU 减少重复绘制的时间
    有选择性地移除窗口背景:getWindow().setBackgroundDrawable(null)
    将activity默认的背景设置为null
    移除xml中非必要的背景,或者条件设置
    
3 AsyncLayoutInflater
    子线程加载布局 ->回调主线程->节约主线程时间
4 X2C框架使用
    annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
    implementation 'com.zhangyue.we:x2c-lib:1.0.6'
    使用的new的方式加载view避免了反射
5 recyclerview 优化
    RecycledViewPool pool = new RecycledViewPool();
    recyclerView.setRecycledViewPool(pool);
    adapter.setTypePool(pool.getTypePool());
    
3 内存优化 安卓内存管理机制
分配值和最大值受设备影响
Dalvik与Art区别
    dalvik固定回收算法
    art运行期选择

内存泄漏检测(as自带工具中 Total列显示多个就没有回收)

1 查看工具MAT(Memory Analyzer) memory Profiler  LeakCanary
2 格式转换工具tools->sdk->platform-tools->hprof_conv.exe(命令转换 hprof-conv -z 1.hprof 1-mat.hprof)
    E:\AndroidSDK\platform-tools\hprof-conv.exe hprof-conv 
    C:\Users\Beepay\Desktop\1.hprof 
    C:\Users\Beepay\Desktop\1-mat.hprof
3 用Memory Analyzer打开1-mat.hprof (file->Open Heap Dump)
    点击Histogram->查找对应的activity->右键GC Root(选择 exclude all) 或者 List(with incomin)右键GC(第一个)
通过反射将对象设置为空
public void z(String name){//传入变量名
    try {
        InputMethodManager systemService = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        Field declaredField = InputMethodManager.class.getDeclaredField(name);
        declaredField.setAccessible(true);
        Object view = declaredField.get(systemService);
        if(null !=view){
            Context context = ((View) view).getContext();
            if(context==this){
                declaredField.set(systemService,null);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

编码习惯

1 基本数据类型
2 循环用foreach
3 集合 sparseArry和arrayMap 性能不如HaspMap但是使用内存少
4 不用枚举改成常量
5 static final 不会申请内存
6 减少字符串拼接
7 activity组件 
    减少将activity当上下文传递
    不将activity有关的对象用startic修饰(比如 Button)
    单利禁止持有activity
    非静态内部类和匿名内部类会持有activity
8 service改成intentService
9 使用webview单开一个进程 在xml中添加 android:process=":p"
10资源未关闭->IO bitmap
11注册对象未销毁->Eventbus
12类的静态变量持有大数据对象
13单利
14非静态内部类
15handler 匿名内部类
16webview

==内存优化面试回答点==

内存优化可以 减少卡顿->增加应用存活时间->降低oom
内存优化需要 对象可达性->强软虚弱引用->垃圾回收算法
可能会出现 内存泄漏的情况->内存抖动情况->最后造成oom->内存监控

内存监控

1 通过ActivityManager获取Debug.MemoryInfo获取内存信息数据
2 通过hook住 Debug.MemoryInfo的getMemoryStat(),获取memory中的多项数据,细分内存使用的情况
3 通过Runtime 获取dalvikHeap
4 通过Debug.getnATIVEhEAPallocatedSize获取NativeHeap

优化项目的过程
    1 分析现状 确认问题
        项目整体运行使用as查看activity是否泄漏,别结合MAT使用 ( LeakCanary->Memory Profiler -> memory Analyzer )
    2 针对性优化
        可能出现内存抖动.内存泄漏,
        减少不必要的内存开销->内存服用
    3 效率提高
        对bitmap的处理使用弱引用->lruCache 使用ARTHooK解决图片过于大于view控件的大小hook住setImageBitmap进行判断图片是不是太大
    4 内存抖动
        大对象内存复用
        避免频繁回收和开辟内存
        webview的activity在新的进程运行  在xml中添加 android:process=":p"
做了内存优化最大的感受
    1 学习了一些工具的使用MAT,如果出现问题可以直接查看解决
    2 技术优化结合业务代码->删除一些重复的3方库 减少初始化的代码量
    3 系统化完善->在线上运行时,将一些数据传递给服务器
oom 优化
    1 Java中出现内存溢出的内存有堆内存,方法区内存,虚拟机栈内存,native方法区内存,一般说的oom是堆内存
    2 主要原因, APP进程内存上限,手机无可用内存
    3 APP进程内存上限->申请内存超过gc回收内存,无用资源无法回收
    4 内存无法回收
    资源对象没有关闭->全局集合类强引用没清理->activity内存泄漏->handler内存泄漏
内存抖动
    1.尽量避免在循环体中创建对象。
    2.尽量不要在自定义View的onDraw()方法中创建对象,因为这个方法会被频繁调用。
    3.对于能够复用的对象,可以考虑使用对象池把它们缓存起来。
    

ANR分析

在Android中应用响应性是有activityManager和WindowManager系统服务监视的,当出现卡顿就对特定的应用程序显示anr
常见原因
    在ams有个最大超时时间
        当前时间没有机会得到处理
        当前事件正在处理,没有及时完成
    1 主线程耗时
    2 死锁
    3 主线程等锁耗时
    4 主线程太多消息
ANR执行流程
    应用发生anr->我们进程收到异常终止信息,写入anr信息(堆栈,cpu,io)->弹出提示框
获取ANR产生的trace文件
	ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:
	$adb pull data/anr/traces.txt .	 
anr文档可以看到的信息
    线程名,优先级,线程内部id,线程状态
    线程所属的线程组,线程挂起次数,线程地址
    调度优先级,调度策略
    栈的大小,CPU调度次数,rq队列等待时间
你是怎么做卡顿优化的
    最初我们项目中一些模块卡顿,通过as自带的工具查看CPU状况,结合代码将一些功能异步加载
    随着项目增加,线上线下实时检测
    减少activity中oncreate和onResume进行延迟操作
anr线上监控-> 第三方ANR-WatchDog
    通过主线程的handler post 一个消息将主线程的某个值改变
    post之后 sleep一段时间之后,检查值有没有改变
    改变了就是没有卡顿
自动化的获取卡顿信息
    自定义mLogging对象,主线程发送一个消息
    子线程发送延迟消息,延迟时间就是阈值,
    主线程消息在规定时间内执行,就取消子线程的消息
    主线程消息没有在规定时间内执行,子线程的消息就是上报当前的堆栈信息
    在一个周期内多次获取堆栈信息,出现卡顿就压缩上报后台
分析步骤
    定位发生的时间点->查看日志->分析是否存在耗时->结合具体代码
service anr流程
    1 APP进程 startSercive通知system_service进程 发起启动服务的请求
    2 system_service通过binder线程接受请求,向activityManager线程发送消息开始计时
    3 binder通过service进程开始进行初始化工作
    4 初始化成功通知system_service进程取消计时
    
4图片压缩 AndroidStudio使用lint清除无用的资源
Analyze -> Run Inspection by Name -> 输入:Unused resources -> 跳出弹框选择范围即可 去掉无用资源:打开shrinkResources

Epic第三方 采用的是hook

Https://github.com/tiann/epic
图片压缩代码
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.jett);
    /**
     * 质量压缩
     */
    compress(bitmap, Bitmap.CompressFormat.JPEG,50,Environment.getExternalStorageDirectory()+"/test_q.jpeg");
    /**
     * 尺寸压缩
     */
    //filter 图片滤波处理 色彩更丰富
    Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 300, 300, true);
    compress(scaledBitmap, Bitmap.CompressFormat.JPEG,100,Environment.getExternalStorageDirectory()+"/test_scaled.jpeg");

    //png格式
    compress(bitmap, Bitmap.CompressFormat.PNG,100,Environment.getExternalStorageDirectory()+"/test.png");
    //webp格式
    compress(bitmap, Bitmap.CompressFormat.WEBP,100,Environment.getExternalStorageDirectory()+"/test.webp");
   /**
     * 压缩图片到制定文件
     * @param bitmap 待压缩图片
     * @param format 压缩的格式
     * @param q      质量
     * @param path  文件地址
     */
    private void compress(Bitmap bitmap, Bitmap.CompressFormat format, int q, String path) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(path);
            bitmap.compress(format,q,fos);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (null != fos){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
bitmap开启异变(options.inMutable=true)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable=true;//支持异变
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher,options);
for(int a=0;a<5;a++){
    options.inBitmap=bitmap;
    //就不会重开辟内存
    bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher,options);
} 
bitmap 压缩 lru使用
https://github.com/jakeWharton/DiskLruCache github地址
图片存储
1 如果lru设置的最大缓存数满了,lru就会返回的旧的需要删除的bitmap如果添加了异变复用标记(options.inMutable=true)就将他放到复用池(软引用,是保留图片的内存)中
2 开个线程循环扫描软引用的队列是不是有数据,如果有数据就将bitmap调用recycle()交给native(乃提物)层进行处理,并且清空队列(手动回收比GC快) 
3 磁盘缓存 把复用池中多出来的数据拿出来,可以复用就复用不能就直接删除(API19之前只能复用宽高都相同的)之后存入磁盘中. 
图片加载
先从内存中获取->如果没有从复用池获取->如果没有就从磁盘存储,并且添加到内存中

图片简单优化

1 质量要求不高使用RGB_565
2 inSampleSize 实现缩放功能
3 inScaled,inDensity和inTargetDensity实现更细的缩放
4 inBitmap 结合lruCache来使用->bitmap放到软引用中
5 图片放到 deawable-xxhdpi下->低分辨设备中图片大小只是压缩,不会增加内存
6 bitmap服用
    options.inMutable = true;
      Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
     if (inBitmap != null) {
         options.inBitmap = inBitmap;
     }
7 是用epic->hook ImageView的setImageBitmap()
    通过从写afterHookedMethod()获取图片大小和view大小作比较,图片过大进行压缩

项目重复图片检测

重复图片指的是 bitmap像素数据完全一致,但是有多个不同的对象,获取hprof文件
获取本地drawable下的所有图片,转换成MD5对比

动脑

使用第三方(libjpeg)  https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/BUILDING.md
    1 安装ndk->用c语言 把所有的像素取到
    2 从每一个像素中获取R色,G色,B色->1个像素占3个字节()
    3 把RGB存储方式 改成BGR方式
    4 去掉了一个阿尔法通道->吧所有的颜色做成二叉树(相同数量最多的颜色做为树根)->哈夫曼算法

加载长图

1 读取图片的宽高
    options.inJustDecodeBounds = true//可以读取到图片的宽高 图片不会加载到内存中
2 读取图片
3 开启裂变 内存服用
    options.inMutable = true
4 压缩色值 设置成RGB565 
5 重建一个解码器
    BitmapRegionDecoder.newInstance(inputStream, false) 会吧像素点记录下来
6OnMeasure()中获取测量的view的大小
7 创建 Rect()对象设置图片的加载区域
8 获取缩放比例
9OnDraw()中开始绘制
10 用解码器弄出刚才设置的区域来加载bitmap
11 通过 matrix对矩阵大小进行缩放
12 画出来
13 处理一下手势滑动防止滑出屏幕
5 电量
使用命令
	1 adb shell dumpsys batterystats --reset
获取完整的wakelock信息:
	2 adb shell dumpsys batterystats --enable full-wake-history 
拔掉USB(让设备不处于充电状态),等待一段时间
获得报告:
	>7.0 adb bugreport bugreport.zip
	<=6.0 adb bugreport > bugreport.txt
6进程包活
1activity提权 开启一个1像素的activity
2service提权 发送通知
进程划分
    前台进程	与用户正在交互 一般系统不会杀死
    可见进程 用户看不到没有前台组件  一般不会杀死
    服务进程	通过startService开启 与用户看到的界面没有关联 会被杀死
    后台进程	随时回收
    空进程	杀死
包活方案
    oom_adj(系统分配给进程的一个值)值越小 优先级越高 系统的进程都是小于0的 我们的是大于0的	当前进程等于0
    1 开启一个像素的activity
        在一个新进程中开一个service在其中判断锁屏开屏广播 开启一个像素大小的activity
    2 前台服务
        在一个服务里面发送通知消息 同时在开启一个服务发送同一个ID的通知消息的取消
    3 相互保活
    4 jobSheduler
        继承JonService
        是作为进程死后复活的一种手段 在形成中通过循环活定时间监听主进程的存活,死了拉起来,在5.0上不支持但是 jobSheduler可以代替
    5 黏性服务 与系统服务捆绑
        NotificationListenerService就是一个监听通知的服务, 在项目中继承这个service  如果有推送通知的权限就行
    excludeFromRecents 任务栏隐藏
        其实,Android 是允许我们在任务列表里隐藏的,而且很简单,只要在清单中声明了 android:excludeFromRecents="true" 就好了。
    6 SyncAdapter
        是安卓系统提供一个账号同步机制,属于核心进程基本->进程优先级变为1,仅低于前台正在运行的进程->降低杀掉概率
7apk大小 APK组成
1 代码相关:classes.dex
    dex是安卓的可执行文件,包含应用程序的全部指令以及运行时数据
    Java编译成class->dx工具将class文件整合到一个dex文件中
    文件结构更紧凑,比传统jar相比大小缩小
2 资源文件
3 so相关
4 签名信息

混淆的左右和形式

将代码的类文字改成无意义->重写代码中的部分逻辑->打乱代码格式
可以移除未使用的类 方法对字节码进行优化,安全

D8 与 R8 优化

dex的编译时间更短->dex文件小->编译后的dex更好的性能
android.enableD8 = true
android.enableR8=true
android.enableR8.libraries=true

总结

代码 混淆 统一三方库 删除无用代码
dex优化
    跨dex调用方法会导致保存被调用dex的方法id->存放的class就会变少
    使用redex进可能的吧互相调用的放到一起和dex优化之间取一个平衡点->使用xz utils对dex进行压缩
资源 图片压缩
so 只保留了Armeabi
as提供来一个可以查看apk包各项的大小
lint 是一个静态扫码工具,可以扫描出没有被引用的资源文件( Analyze->Inspect coe)
图片转webp
引入三方库: 考虑是否需要将其代码全部引入,还是只是一部分
相同图片不同颜色使用 android:tint
8加固
混淆
 -keep 指定类和类成员(变量和方法)不被混淆。
	-keep class com.dongnao.proxy.guard.test.Bug
	(保护了类名)
	-keep class com.dongnao.proxy.guard.test.Bug{
   		public static void *();
	}
	(保护了 public static void的没有参数的函数)
	-keep class com.dongnao.proxy.guard.test.Bug{
   		*;
	}
	(保护所有)
-keepclassmembers 指定类成员不被混淆(就是-keep的缩小版,不管类名了)。
	-keepclassmembers
	class com.dongnao.proxy.guard.test.Bug
	(都被混淆了)
-keepclasseswithmembers 指定类和类成员不被混淆,前提是指定的类成员存在。
	-keepclasseswithmembers class 	com.dongnao.proxy.guard.test.Bug
	(保护类名,但是没指定成员,所以函数名被混淆)
	-keepclasseswithmembers class   	com.dongnao.proxy.guard.test.Bug{
		native <methods>;
	}
类名被混淆出现错误找不到位置恢复方法
1 先配置  -keepattributes SourceFile,LineNumberTable
2 再执行  retrace.bat  -verbose mappint文件  bug文件(build/outputs/mapping/debug/mapping.txt)
3 使用工具 sdk/tools/groguard/bin/retrace.bat
加固
创建一个代理application->dex通过tools加密->加密后的dex和res什么的在组成一个新的apk,交给代理application解码->交给android去加载dex
    1 通过代码执行cmd命令 获取dex文件
    RunTime.getRunTime.exec("命令") 可以执行命令
    2 加密apk中所有的dex文件(比如使用AES加密),将加密好的文件重新压缩成一个apk文件(包括代理的application和自定义的dexClassLoader)
    3 用官方通过的cmd命令 进行对齐和签名
    4 系统里面会吧dex放到dexElements数组里面->循环数组里面的数据,通过反射转换成class进行执行
        将dex加密放到数组中,在通过反射指向到系统中的dexElements数组
    5 通过file(getApplicationInfo().sourceDir) 得到加密后的apk文件,解压到一个目录,读取文件进行解密在写到指定的目录 用流进行读写
    6 通过反射指向到系统中的dexElements数组,将解密后的文件加载到系统的数组中

代理的application,通过自定义的dexClassLoader读取加密后的dex,
通过反射activityThread,将代理的application执行原来的application,调用oncreate()

9稳定性
1 重在预防,使用第三方线上监控软件
2 长效保存->建立相关的规范
3 崩溃处理
    采集信息->处理信息->展示信息设备 数据 版本 使用时长->上报相关同学
4 崩溃优化
    出现未捕获异常->使用钩子 UncaughtExceptionHandler 对单进程处理
    反混淆上传堆栈信息-> 安卓原生反混淆工具包 retrace.jar 实时解析每个上报的崩溃
    java崩溃
        1 首先发送crash进程,在创建之初便准备好了defaultUncaughtHandler,处理并输出错误信息->写类实现Thread.UncaughtExceptionHandler接口,崩溃会回调uncaughtException()
        2 AMP.handleApplicationCrash,通过binder 传递给 system_server进程
        3 调用 binder服务端执行 ams.handleApplicationCrash
        4 如果1分钟没有出现2次崩溃,结束栈顶activity,如果出现结束所有activity
	native解决方案 都是信号机制
        google Beakpad 代码量大
        LogCat 需要过滤日志
        coffeecatch 存在兼容性
    信号机制->信号会在内核态和用户态直接切换
        用户空间->中断->内核空间 调用中断服务->用户空间 信号处理->执行完返回内核 恢复内核栈->从内核返回用户空间 继续执行

image

    信号接收->内核代理接收->会将其放到对应进程的消息队列中,向进程发送一个中断->使其陷入内核态
    信号处理->用户空间处理
如何解决 SharedPreference apply 引起的 ANR 问题
    分析 sp调用 apply 方法->会创建一个锁放到QueuedWork中
        将数据持久化封装成一个任务放到队列中->任务结束释放锁
        activity退出 会执行 QueuedWork.waitToFinish()等待所有的等待锁释放
    解决 Hook activityThrad的handler变量_>为他设置一个callback
        handler.dispatchMessage中会先处理callback->在回调中执行队列的清除QueuedWork.waitToFinish()