[笔记]Android性能优化 上
[笔记]Android性能优化 中
[笔记]Android性能优化 下
8.Android性能优化典范-第5季
多线程大部分内容源自凯哥的课程,个人觉得比优化典范写得清晰得多
1.线程
-
线程就是代码线性执行,执行完毕就结束的一条线.UI线程不会结束是因为其初始化完毕后会执行死循环,所以永远不会执行完毕.
-
如何简单创建新线程:
//1:直接创建Thread,执行其start方法 Thread t1 = new Thread(){ @Override public void run() { System.out.println("Thread:run"); } }; t1.start(); //2:使用Runnable实例作为参数创建Thread,执行start Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Runnable:run"); } }; Thread t2 = new Thread(runnable); t2.start();
- 两种方式创建新线程性能无差别,使用Runnable实例适用于希望Runnable复用的情形
- 常用的创建线程池2种方式
- Executors.newCachedThreadPool():一般情况下使用newCachedThreadPool即可.
- Executors.newFixedThreadPool(int number):短时批量处理/比如要并行处理多张图片,可以直接创建包含图片精确数量的线程的线程池并行处理.
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("runnable:run()"); } }; ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(runnable); executorService.execute(runnable); executorService.execute(runnable); executorService.shutdown(); //比如有40张图片要同时处理 //创建包含40个线程的线程池,每个线程处理一张图片,处理完毕后shutdown ExecutorService service = Executors.newFixedThreadPool(40); for(Bitmap item:bitmaps){ //比如runnable就是处理单张图片的 service.execute(runnable); } service.shutdown();
- 《阿里巴巴Java开发手册》规定:
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式.这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - 看Android中Executors源码.Executors.newCachedThreadPool/newScheduledThreadPool允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM.而newFixedThreadPool,newSingleThreadExecutor不会存在这种风险.
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式.这样
- 如何正确创建ThreadPoolExecutor:有点麻烦,晚点详述
- ExecutorService的shutdown和shutdownNow
- shutdown:在调用shutdown之前ExecutorService中已经启动的线程,在调用shutdown后,线程如果执行未结束会继续执行完毕并结束,但不会再启动新的线程执行新任务.
- shutdownNow:首先停止启动新的线程执行新任务;并尝试结束所有正在执行的线程,正在执行的线程可能被终止也可能会继续执行完成.
-
如何正确创建ThreadPoolExecutor
3.1:ThreadPoolExecutor构造参数public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- int corePoolSize:该线程池中核心线程最大数量.默认情况下,即使核心线程处于空闲状态也不会被销毁.除非通过allowCoreThreadTimeOut(true),则核心线程在空闲时间达到keepAliveTime时会被销毁
- int maximumPoolSize:该线程池中线程最大数量
- long keepAliveTime:该线程池中非核心线程被销毁前最大空闲时间,时间单位由unit决定.默认情况下核心线程即使空闲也不会被销毁,在调用allowCoreThreadTimeOut(true)后,该销毁时间设置也适用于核心线程
- TimeUnit unit:keepAliveTime/被销毁前最大空闲时间的单位
- BlockingQueue<Runnable> workQueue:该线程池中的任务队列.维护着等待被执行的Runnable对象.BlockingQueue有几种类型,下面会详述
- ThreadFactory threadFactory:创建新线程的工厂.一般情况使用Executors.defaultThreadFactory()即可.当然也可以自定义.
- RejectedExecutionHandler handler:拒绝策略.当需要创建的线程数量达到maximumPoolSize并且等待执行的Runnable数量超过了任务队列的容量,该如何处理.
3.2:当1个任务被放进线程池,ThreadPoolExecutor具体执行策略如下:
- 如果线程数量没有达到corePoolSize,有核心线程空闲则核心线程直接执行,没有空闲则直接新建核心线程执行任务;
- 如果线程数量已经达到corePoolSize,且核心线程无空闲,则将任务添加到等待队列;
- 如果等待队列已满,则新建非核心线程执行该任务;
- 如果等待队列已满且总线程数量已达到maximumPoolSize,则会交由RejectedExecutionHandler handler处理.
3.3:阻塞队列/BlockingQueue<Runnable> workQueue
- BlockingQueue有如下几种:SynchronousQueue/LinkedBlockingQueue/LinkedTransferQueue/ArrayBlockingQueue/PriorityBlockingQueue/DelayQueue.
- SynchronousQueue:SynchronousQueue的容量是0,不存储任何Runnable实例.新任务到来会直接尝试交给线程执行,如所有线程都在忙就创建新线程执行该任务.
- LinkedBlockingQueue:默认情况下没有容量限制的队列.
- ArrayBlockingQueue:一个有容量限制的队列.
- DelayQueue:一个没有容量限制的队列.队列中的元素必须实现了Delayed接口.元素在队列中的排序按照当前时间的延迟值,延迟最小/最早要被执行的任务排在队列头部,依次排序.延迟时间到达后执行指定任务.
- PriorityBlockingQueue:一个没有容量限制的队列.队列中元素必须实现了Comparable接口.队列中元素排序依赖元素的自然排序/compareTo的比较结果.
- 各种BlockingQueue的问题
1.SynchronousQueue缺点:因为不具备存储元素的能力,因而当任务很频繁时候,为了防止线程数量超标,我们往往设置maximumPoolSize是Integer.MAX_VALUE,创建过多线程会导致OOM.《阿里巴巴Java开发手册》中强调不能使用Executors直接创建线程池,就是对应Android源码中newCachedThreadPool和newScheduledThreadPool,本质上就是创建了maximumPoolSize为Integer.MAX_VALUE的ThreadPoolExecutor.
2.LinkedBlockingQueue因为没有容量限制,所以我们使用LinkedBlockingQueue创建ThreadPoolExecutor,设置maximumPoolSize是无意义的,如果线程数量已经达到corePoolSize,且核心线程都在忙,那么新来的任务会一直被添加到队列中.只要核心线程无空闲则一直得不到被执行机会.
3.DelayQueue和PriorityBlockingQueue也具有同样的问题.所以corePoolSize必须设置合理,否则会导致超出核心线程数量的任务一直得不到机会被执行.这两类队列分别适用于定时及优先级明确的任务.
3.4:RejectedExecutionHandler handler/拒绝策略有4种
1.hreadPoolExecutor.AbortPolicy:丢弃任务,并抛出RejectedExecutionException异常.ThreadPoolExecutor默认就是使用AbortPolicy.
2.ThreadPoolExecutor.DiscardPolicy:丢弃任务,但不会抛出异常.
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃排在队列头部的任务,不抛出异常,并尝试重新执行任务.
4.ThreadPoolExecutor.CallerRunsPolicy:丢弃任务,但不抛出异常,并将该任务交给调用此ThreadPoolExecutor的线程执行. - int corePoolSize:该线程池中核心线程最大数量.默认情况下,即使核心线程处于空闲状态也不会被销毁.除非通过allowCoreThreadTimeOut(true),则核心线程在空闲时间达到keepAliveTime时会被销毁
-
synchronized 的本质
- 保证synchronized方法或者代码块内部资源/数据的互斥访问
- 即同一时间,由同一个Monitor监视的代码,最多只有1个线程在访问
- 保证线程之间对监视资源的数据同步.
- 任何线程在获取Monitor后,会第一时间将共享内存中的数据复制到自己的缓存中;
- 任何线程在释放Monitor后,会第一时间将缓存中的数据复制到共享内存中
- 保证synchronized方法或者代码块内部资源/数据的互斥访问
-
volatile
- 保证被volatile修饰的成员的操作具有原子性和同步性.相当于简化版的synchronized
- 原子性就是线程间互斥访问
- 同步性就是线程之间对监视资源的数据同步
- volatile生效范围:基本类型的直接复制赋值 + 引用类型的直接赋值
//引用类型的直接赋值操作有效 private volatile User u = U1; //修改引用类型的属性,则不是原子性的,volatile无效 U1.name = "吊炸天" //对引用类型的直接赋值是原子性的 u = U2; private volatile int a = 0; private int b = 100; //volatile无法实现++/--的原子性 a++;
- volatile型变量自增操作的隐患
- volatile类型变量每次在读取的时候,会越过线程的工作内存,直接从主存中读取,也就不会产生脏读
- ++自增操作,在Java对应的汇编指令有三条
- 从主存读取变量值到cpu寄存器
- 寄存器里的值+1
- 寄存器的值写回主存
- 如果N个线程同时执行到了第1步,那么最终变量会损失(N-1).第二步第三步只有一个线程是执行成功.
- 对变量的写操作不依赖于当前值,才能用volatile修饰.
- volatile型变量自增操作的隐患
- 保证被volatile修饰的成员的操作具有原子性和同步性.相当于简化版的synchronized
-
针对num++这类复合类的操作,可以使用java并发包中的原子操作类原子操作类:AtomicInteger AtomicBoolean等来保证其原子性.
public static AtomicInteger num = new AtomicInteger(0); num.incrementAndGet();//原子性的num++,通过循环CAS方式
2.线程间交互
- 一个线程终结另一个线程
- Thread.stop不要用:
- 因为线程在运行过程中随时有可能会被暂停切换到其他线程,stop的效果相当于切换到其他线程继续执行且以后再也不会切换回来.我们执行A.stop的时候,完全无法预知A的run方法已经执行了多少,执行百分比完全不可控.
下面的代码,每次执行最后打印的结果都不同,即我们完全不可预知调用stop时候当前线程执行了百分之多少. private static void t2(){ Thread t = new Thread(){ @Override public void run() { for(int i=0;i<1000000;i++){ System.out.println(""+i); } } }; t.start(); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } t.stop(); }
- Thread.interrupt:仅仅设置当前线程为被中断状态.在运行的线程依然会继续运行.
- Thread.isInterrupted:获取当前线程是否被中断
- Thread.interrupted():如果线程A调用了Thread.interrupted()
- 如果A之前已经被中断,调用Thread.interrupted()返回false,A已经不是被中断状态
- 如果A之前不是被中断状态,调用Thread.interrupted()返回true,A变成被中断状态.
- 单纯调用A.interrupt是无效果的,interrupt需要和isInterrupted联合使用
- 用于我们希望线程处于被中断状态时结束运行的场景.
- interrupt和stop比较的优点:stop后,线程直接结束,我们完全无法控制当前执行到哪里;
interrupt后线程默认会继续执行,我们通过isInterrupted来获取被中断状态,只有被中断且满足我们指定条件才return,可以精确控制线程的执行百分比.
private static void t2(){ Thread t = new Thread(){ @Override public void run() { for(int i=0;i<1000000;i++){ //检查线程是否处于中断状态,且检查是否满足指定条件 //如果不满足指定条件,即使处于中断状态也继续执行. if(isInterrupted()&&i>800000){ //先做收尾工作 //return 结束 return; } System.out.println(""+i); } } }; t.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //调用了interrupt后,在run中监查是否已经被打断,如果已经被打断,且满足指定条件, //就return,线程就执行完了 t.interrupt(); } ...... 799999 800000 Process finished with exit code 0
- InterruptedException:
- 如果线程A在sleep过程中被其他线程调用A.interrupt(),会触发InterruptedException.
- 如果调用A.interrupt()时候,A并不在sleep状态,后面再调用A.sleep,也会立即抛出InterruptedException.
private static void t3(){ Thread thread = new Thread(){ @Override public void run() { long t1 = System.currentTimeMillis(); try { Thread.sleep(3000); } catch (InterruptedException e) { long t2 = System.currentTimeMillis(); System.out.println("老子被叫醒了:睡了"+(t2-t1)+"ms"); //用于做线程收尾工作,然后return return; } System.out.println("AAAAAAAA"); } }; thread.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); } 老子被叫醒了:睡了493ms Process finished with exit code 0
- Thread.stop不要用:
- 线程等待:wait,notifyAll,notify
- wait,notifyAll,notify是属于Object的方法.用于线程等待的场景,需用Monitor进行调用
- wait:
- 当1个线程A持有Monitor M.
- 此时调用M.wait,A会释放M并处于等待状态.并记录A在当前代码执行的位置Position.
- notify:
- 当调用M.notify(),就会唤醒1个因为调用M.wait()而处于等待状态的线程
- 如果有A,B,C--多个线程都是因为调用M.wait()而处于等待状态,不一定哪个会被唤醒并尝试获取M
- notifyAll:
- 当调用M.notifyAll(),所有因为调用M.wait()而处于等待状态的线程都被唤醒,一起竞争尝试获取M
- 调用notify/notifyAll被唤醒并获取到M的线程A,会接着之前的代码执行位置Position继续执行下去
private String str = null; private synchronized void setStr(String str){ System.out.println("setStr时间:"+System.currentTimeMillis()); this.str = str; notifyAll(); } private synchronized void printStr(){ while (str==null){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程:"+Thread.currentThread().getName()+ " printStr时间:"+System.currentTimeMillis()); System.out.println("str:"+str); } private void t4(){ (new Thread(){ @Override public void run() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } setStr("老子设置一下"); } }).start(); (new Thread(){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:"+Thread.currentThread().getName()+ " 尝试printStr时间:"+System.currentTimeMillis()); printStr(); } }).start(); (new Thread(){ @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:"+Thread.currentThread().getName()+ " 尝试printStr时间:"+System.currentTimeMillis()); printStr(); } }).start(); } 线程:Thread-2 尝试printStr时间:1539247468146 线程:Thread-1 尝试printStr时间:1539247468944 setStr时间:1539247469944 线程:Thread-1 printStr时间:1539247469944 str:老子设置一下 线程:Thread-2 printStr时间:1539247469944 str:老子设置一下
3.Executor、 AsyncTask、 HandlerThead、 IntentService 如何选择
- HandlerThead就不要用,HandlerThead设计目的就是为了主界面死循环刷新界面,无其他应用场景.
- 能用线程池就用线程池,因为最简单.
- 涉及后台线程推送任务到UI线程,可以使用Handler或AsyncTask
- Service:就是为了做后台任务,不要UI界面,需要持续存活.有复杂的需要长期存活/等待的场景使用Service.
- IntentService:属于Service.当我们需要使用Service,且需要后台代码执行完毕后该Service自动被销毁,使用IntentService.
4.AsyncTask的内存泄漏
- GC Roots:由堆外指向堆内的引用,包括:
- Java方法栈帧中的局部变量
- 已加载类的静态变量
- native代码的引用
- 运行中的Java线程
- AsyncTask内存泄漏本质:正在运行的线程/AsyncTask 在虚拟机中属于GC ROOTS,AsyncTask持有外部Activity的引用.被GC ROOTS引用的对象不能被回收.
- 所以AsyncTask和其他线程工具一样,只要是使用线程,都有可能发生内存泄漏,都要及时关闭,AsyncTask并不比其他工具更差.
- 如何避免AsyncTask内存泄漏:使用弱引用解决AsyncTask在Activity销毁后依然持有Activity引用的问题
5.RxJava.
讲的太多了这里推荐1个专题RxJava2.x
下面记录一下自己不太熟的几点
- RxJava整体结构:
- 链的最上游:生产者Observable
- 链的最下游:观察者Observer
- 链的中间多个节点:双重角色.即是上一节点的观察者Observer,也是下一节点的生产者Observable.
- Scheduler切换线程的原理:源码跟踪下去,实质是通过Excutor实现了线程切换.
6.Android M对Profile GPU Rendering工具的更新
image- Swap Buffers:CPU等待GPU处理的时间
- Command Issur:OpenGL渲染Display List所需要的时间
- Sync&Upload:通常表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片本身的大小
- Draw:测量绘制Display List的时间
- Measure & Layout:这里表示的是布局的onMeasure与onLayout所花费的时间.一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题
- Animation:表示的是计算执行动画所需要花费的时间.包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等.一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等
- Input Handling:表示的是系统处理输入事件所耗费的时间,粗略等于对于的事件处理方法所执行的时间.一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作
- Misc/Vsync Delay:如果稍加注意,我们可以在开发应用的Log日志里面看到这样一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。这意味着我们在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况
9.Android性能优化典范-第6季
1.启动闪屏
- 当点击桌面图标启动APP的时候,App会出现短暂的白屏,一直到第一个Activity的页面的渲染加载完毕
- 为了消除白屏,我们可以为App入口Activity单独设置theme.
- 在单独设置的theme中设置android:background属性为App的品牌宣传图片背景.
- 在代码执行到入口Activity的onCreate的时候设置为程序正常的主题.
styles.xml <!-- Base application theme. --> //Activity默认主题 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> //默认主题窗口背景设置为白色 <item name="android:background">@android:color/white</item> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="android:windowNoTitle">true</item> <item name="android:windowFullscreen">true</item> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> </style> //入口Activity的theme单独设置 <style name="ThemeSplash" parent="Theme.AppCompat.Light.NoActionBar"> //入口Activity初始窗口背景设置为品牌宣传图片 <item name="android:background">@mipmap/startbg</item> <item name="android:windowNoTitle">true</item> <item name="android:windowFullscreen">true</item> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> </style> AndroidManifest.xml <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:theme="@style/ThemeSplash">//为入口Activity单独指定theme <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </manifest> public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { //在代码执行到入口Activity时候设置入口Activity为默认主题 setTheme(R.style.AppTheme); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv_all = findViewById(R.id.tv_all); tv_local = findViewById(R.id.tv_local); //注册全局广播 registerReceiver(globalReceiver,new IntentFilter("global")); //注册本地广播 LocalBroadcastManager.getInstance(this).registerReceiver(localBroadReceiver,new IntentFilter("localBroadCast")); } }
2.为App提供对应分辨率下的图片,系统会自动匹配最合适分辨率的图片执行拉伸或压缩的处理.
- 如果是只有1张图片,放在mipmap-nodpi,或mipmap-xxxhdpi下
- 所有的大背景图片,统一放在mipmap-nodpi目录,用一套1080P素材可以解决大部分手机适配问题,不用每个资源目录下放一套素材
- 经过试验,不论ImageView宽高是否是wrap_content,只要图片所在文件夹和当前设备分辨率不匹配,都会涉及到放大或压缩,占用的内存都会相应的变化.尤其对于大图,放在低分辨率文件夹下直接OOM.
具体原因:
郭霖:Android drawable微技巧,你所不知道的drawable的那些细节
当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用android_logo这张图时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果drawable-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,我们当前的场景就是drawable-xxxhdpi文件夹,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
总体匹配规则就是这样,那么比如说现在终于在drawable-mdpi文件夹下面找到android_logo这张图了,但是系统会认为你这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。
那么同样的道理,如果系统是在drawable-xxxhdpi文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作
3.尽量复用已经存在的图片.
比如一张图片O已经存在,如果有View的背景就是O旋转过后的样子,可以直接用O创建RotateDrawable.然后将设置给View使用.
注意:RotateDrawable已经重写了其onLevelChange方法,所以一定要设置level才会生效
@Override
protected boolean onLevelChange(int level) {
super.onLevelChange(level);
final float value = level / (float) MAX_LEVEL;
final float degrees = MathUtils.lerp(mState.mFromDegrees, mState.mToDegrees, value);
mState.mCurrentDegrees = degrees;
invalidateSelf();
return true;
}
实例:
1.首先创建xml文件
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@mipmap/close10"
android:fromDegrees="90"
android:toDegrees="120"
android:pivotX="50%"
android:pivotY="50%"
>
</rotate>
2.在Java代码中获取该xml对应的Drawable实例,并设置level为10000
Drawable drawable = getResources().getDrawable(R.drawable.rotate_close);
drawable.setLevel(10000);
3.将Drawable设置为View的背景
findViewById(R.id.v).setBackgroundDrawable(drawable);
4.开启混淆和资源压缩:在app模块下的的build.gradle中
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
5.对于简单/规则纹理的图片,使用VectorDrawable来替代多个分辨率图片.VectorDrawable 有很多注意事项,后面单独一篇文章总结
10.网络优化
网络优化主要有几个方面:降低网络请求数量,降低单次请求响应的数据量,在弱网环境下将非必要网络请求延缓至网络环境好的时候.
1.降低网络请求数量:获取同样的数据,多次网络请求会增加电量消耗,且多次请求总体上将消耗服务端更多的时间及资源
- 接口Api设计要合理.可以将多个接口合并,多次请求显示1个界面,改造后1个接口即可提供完整数据.
- 根据具体场景实时性需求,在App中加入网络缓存,在实时性有效区间避免重复请求:主要包括网络框架和图片加载框架的缓存.
2.降低单次请求的数据量
- 网络接口Api在设计时候,去除多余的请求参数及响应数据.
- 网络请求及响应数据的传输开启GZIP压缩,降低传输数据量.
- okHttp对gzip的支持前面已记录
- Protocal Buffers,Nano-Proto-Buffers,FlatBuffers代替GSON执行序列化.
- Protocal Buffers网上有使用的方法,相对GSON有点繁琐.如果对网络传输量很敏感,可以考虑使用.其他几种方案的文章不多.
- 网络请求图片,添加图片宽高参数,避免下载过大图片增加流量消耗.
3.弱网环境优化这块没有经验,直接看anly_jun的文章
App优化之网络优化
文章中提到:用户点赞操作, 可以直接给出界面的点赞成功的反馈, 使用JobScheduler在网络情况较好的时候打包请求.
11.电量优化
12.JobScheduler,AlarmManager和WakeLock
JobScheduler在网络优化中出现过,WakeLock涉及电量优化,AlarmManager和WakeLock有相似,但侧重点不同.
- WakeLock:比如一段关键逻辑T已经在执行,执行未完成Android系统就进入休眠,会导致T执行中断.WakeLock目的就在于阻止Android系统进入休眠状态,保证T得以继续执行.
- 休眠过程中自定义的Timer、Handler、Thread、Service等都会暂停
- AlarmManager:Android系统自带的定时器,可以将处于休眠状态的Android系统唤醒
- 保证Android系统在休眠状态下被及时唤醒,执行 定时/延时/轮询任务
- JobScheduler:JobScheduler目的在于将当下不紧急的任务延迟到后面更合适的某个时间来执行.我们可以控制这些任务在什么条件下被执行.
- JobScheduler可以节约Android设备当下网络,电量,CPU等资源.在指定资源充裕情况下再执行"不紧要"的任务.
JobScheduler:
Android Jobscheduler使用
Android开发笔记(一百四十三)任务调度JobScheduler
WakeLock:
Android WakeLock详解
Android PowerManager.WakeLock使用小结
Android的PowerManager和PowerManager.WakeLock用法简析
AlarmManager和WakeLock使用:
后台任务 - 保持设备唤醒状态
13.性能检测工具
1.Android Studio 3.2之后,Android Device Monitor已经被移除.Android Device Monitor原先包含的工具由新的方案替代.Android Device Monitor
image- DDMS:由Android Profiler代替.可以进行CPU,内存,网络分析.
- TraceView:可以通过Debug类在代码中调用Debug.startMethodTracing(String tracePath)和Debug.stopMethodTracing()来记录两者之间所有线程及线程中方法的耗时,生成.trace文件.通过abd命令可以将trace文件导出到电脑,通过CPU profiler分析.
- Systrace:可以通过命令行生成html文件,通过Chrome浏览器进行分析.
- 生成html文件已实现.但文件怎么分析暂未掌握,看了网上一些文章说实话还是没搞懂
- Hierarchy Viewer:由Layout Inspector代替.但当前版本的Layout Inspector不能查看每个View具体的onMeasure,onLayout,onDraw耗时,功能是不足的.我们可使用系统提供的Window.OnFrameMetricsAvailableListener来计算指定View的onLayout及onDraw耗时.
- Network Traffic tool:由Network Profiler代替.
2.其中Android Profiler如何使用,直接看官网即可.Profile your app performance.
3.TraceView
-
TraceView可以直接通过CPU profiler中点击Record按钮后,任意时间后点击Stop按钮.即可生成trace文件.并可将.trace文件导出.
image
image - TraceView也可以通过Debug类在代码中精确控制要统计哪个区间代码/线程的CPU耗时.
这种用法是 anly_jun大神文章里学到的
代码执行完毕,会在Android设备中生成JetApp.trace文件.通过Device File Explorer,找到sdcard/Android/data/app包名/files/JetApp.tracepublic class SampleApplication extends Application { @Override public void onCreate() { Debug.startMethodTracing("JetApp"); super.onCreate(); LeakCanary.install(this); // init logger. AppLog.init(); // init crash helper CrashHelper.init(this); // init Push PushPlatform.init(this); // init Feedback FeedbackPlatform.init(this); Debug.stopMethodTracing(); }
在JetApp.trace上点击右键->Copy Path,将trace文件路径复制下来.
Windows下cmd打开命令行,执行 adb pull 路径,即可trace文件导出到电脑.
image - trace文件分析很简单.我们可以看到每个线程及线程中每个方法调用消耗的时间.
4.Layout Inspector很简单,在App运行后,点击Tools->Layout Inspector即可.
下面只看Window.OnFrameMetricsAvailableListener怎么用.
从Android 7.0 (API level 24)开始,Android引入Window.OnFrameMetricsAvailableList接口用于提供每一帧绘制各阶段的耗时,数据源与GPU Profile相同.
public interface OnFrameMetricsAvailableListener {
void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics,int dropCountSinceLastInvocation);
}
/**
* 包含1帧的周期内,渲染系统各个方法的耗时数据.
*/
public final class FrameMetrics {
****
//通过getMetric获取layout/measure耗时所用的id
public static final int LAYOUT_MEASURE_DURATION = 3;
public static final int DRAW_DURATION = 4;
/**
* 获取当前帧指定id代表的方法/过程的耗时,单位是纳秒:1纳秒(ns)=10的负6次方毫秒(ms)
*/
public long getMetric(@Metric int id) {
****
}
}
- 在Activity中使用OnFrameMetricsAvailableListener:
- 通过调用this.getWindow().addOnFrameMetricsAvailableListener(@NonNull OnFrameMetricsAvailableListener listener,Handler handler)来添加监听.
- 通过调用this.getWindow().removeOnFrameMetricsAvailableListener(OnFrameMetricsAvailableListener listener)取消监听.
-
解析ConstraintLayout的性能优势中引用了Google使用OnFrameMetricsAvailableListener的例子android-constraint-layout-performance.其中Activity是Kotlin写的,尝试将代码转为java,解决掉报错后运行.
package p1.com.p1; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.support.annotation.RequiresApi; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.FrameMetrics; import android.view.View; import android.view.View.MeasureSpec; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.view.Window.OnFrameMetricsAvailableListener; import android.widget.Button; import android.widget.TextView; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; import java.util.Arrays; import kotlin.TypeCastException; import kotlin.jvm.internal.Intrinsics; public final class KtMainActivity extends AppCompatActivity { private final Handler frameMetricsHandler = new Handler(); @RequiresApi(24) private final OnFrameMetricsAvailableListener frameMetricsAvailableListener = new OnFrameMetricsAvailableListener() { @Override public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) { long costDuration = frameMetrics.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION); Log.d("Jet", "layoutMeasureDurationNs: " + costDuration); } }; private static final String TAG = "KtMainActivity"; private static final int TOTAL = 100; private static final int WIDTH = 1920; private static final int HEIGHT = 1080; @RequiresApi(3) protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.activity_for_test); final Button traditionalCalcButton = (Button) this.findViewById(R.id.button_start_calc_traditional); final Button constraintCalcButton = (Button) this.findViewById(R.id.button_start_calc_constraint); final TextView textViewFinish = (TextView) this.findViewById(R.id.textview_finish); traditionalCalcButton.setOnClickListener((OnClickListener) (new OnClickListener() { public final void onClick(View it) { Button var10000 = constraintCalcButton; Intrinsics.checkExpressionValueIsNotNull(constraintCalcButton, "constraintCalcButton"); var10000.setVisibility(View.INVISIBLE); View var4 = KtMainActivity.this.getLayoutInflater().inflate(R.layout.activity_traditional, (ViewGroup) null); if (var4 == null) { throw new TypeCastException("null cannot be cast to non-null type android.view.ViewGroup"); } else { ViewGroup container = (ViewGroup) var4; String var10002 = KtMainActivity.this.getString(R.string.executing_nth_iteration); Intrinsics.checkExpressionValueIsNotNull(var10002, "getString(R.string.executing_nth_iteration)"); KtMainActivity.MeasureLayoutAsyncTask asyncTask = new KtMainActivity.MeasureLayoutAsyncTask(var10002, new WeakReference(traditionalCalcButton), new WeakReference(textViewFinish), new WeakReference(container)); asyncTask.execute(new Void[0]); } } })); constraintCalcButton.setOnClickListener((OnClickListener) (new OnClickListener() { public final void onClick(View it) { Button var10000 = traditionalCalcButton; Intrinsics.checkExpressionValueIsNotNull(traditionalCalcButton, "traditionalCalcButton"); var10000.setVisibility(View.INVISIBLE); View var4 = KtMainActivity.this.getLayoutInflater().inflate(R.layout.activity_constraintlayout, (ViewGroup) null); if (var4 == null) { throw new TypeCastException("null cannot be cast to non-null type android.view.ViewGroup"); } else { ViewGroup container = (ViewGroup) var4; String var10002 = KtMainActivity.this.getString(R.string.executing_nth_iteration); Intrinsics.checkExpressionValueIsNotNull(var10002, "getString(R.string.executing_nth_iteration)"); KtMainActivity.MeasureLayoutAsyncTask asyncTask = new KtMainActivity.MeasureLayoutAsyncTask(var10002, new WeakReference(constraintCalcButton), new WeakReference(textViewFinish), new WeakReference(container)); asyncTask.execute(new Void[0]); } } })); } @RequiresApi(24) protected void onResume() { super.onResume(); this.getWindow().addOnFrameMetricsAvailableListener(this.frameMetricsAvailableListener, this.frameMetricsHandler); } @RequiresApi(24) protected void onPause() { super.onPause(); this.getWindow().removeOnFrameMetricsAvailableListener(this.frameMetricsAvailableListener); } @RequiresApi(3) private static final class MeasureLayoutAsyncTask extends AsyncTask { @NotNull private final String executingNthIteration; @NotNull private final WeakReference startButtonRef; @NotNull private final WeakReference finishTextViewRef; @NotNull private final WeakReference containerRef; @Nullable protected Void doInBackground(@NotNull Void... voids) { Intrinsics.checkParameterIsNotNull(voids, "voids"); int i = 0; for (int var3 = KtMainActivity.TOTAL; i < var3; ++i) { this.publishProgress(new Integer[]{i}); try { Thread.sleep(100L); } catch (InterruptedException var5) { ; } } return null; } // $FF: synthetic method // $FF: bridge method public Object doInBackground(Object[] var1) { return this.doInBackground((Void[]) var1); } protected void onProgressUpdate(@NotNull Integer... values) { Intrinsics.checkParameterIsNotNull(values, "values"); Button var10000 = (Button) this.startButtonRef.get(); if (var10000 != null) { Button startButton = var10000; Intrinsics.checkExpressionValueIsNotNull(startButton, "startButton"); // StringCompanionObject var3 = StringCompanionObject.INSTANCE; String var4 = this.executingNthIteration; Object[] var5 = new Object[]{values[0], KtMainActivity.TOTAL}; String var9 = String.format(var4, Arrays.copyOf(var5, var5.length)); Intrinsics.checkExpressionValueIsNotNull(var9, "java.lang.String.format(format, *args)"); String var7 = var9; startButton.setText((CharSequence) var7); ViewGroup var10 = (ViewGroup) this.containerRef.get(); if (var10 != null) { ViewGroup container = var10; Intrinsics.checkExpressionValueIsNotNull(container, "container"); this.measureAndLayoutExactLength(container); this.measureAndLayoutWrapLength(container); } } } // $FF: synthetic method // $FF: bridge method public void onProgressUpdate(Object[] var1) { this.onProgressUpdate((Integer[]) var1); } protected void onPostExecute(@Nullable Void aVoid) { TextView var10000 = (TextView) this.finishTextViewRef.get(); if (var10000 != null) { TextView finishTextView = var10000; Intrinsics.checkExpressionValueIsNotNull(finishTextView, "finishTextView"); finishTextView.setVisibility(View.VISIBLE); Button var4 = (Button) this.startButtonRef.get(); if (var4 != null) { Button startButton = var4; Intrinsics.checkExpressionValueIsNotNull(startButton, "startButton"); startButton.setVisibility(View.GONE); } } } // $FF: synthetic method // $FF: bridge method public void onPostExecute(Object var1) { this.onPostExecute((Void) var1); } private final void measureAndLayoutWrapLength(ViewGroup container) { int widthMeasureSpec = MeasureSpec.makeMeasureSpec(KtMainActivity.WIDTH, View.MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(KtMainActivity.HEIGHT, View.MeasureSpec.AT_MOST); container.measure(widthMeasureSpec, heightMeasureSpec); container.layout(0, 0, container.getMeasuredWidth(), container.getMeasuredHeight()); } private final void measureAndLayoutExactLength(ViewGroup container) { int widthMeasureSpec = MeasureSpec.makeMeasureSpec(KtMainActivity.WIDTH, View.MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(KtMainActivity.HEIGHT, View.MeasureSpec.EXACTLY); container.measure(widthMeasureSpec, heightMeasureSpec); container.layout(0, 0, container.getMeasuredWidth(), container.getMeasuredHeight()); } @NotNull public final String getExecutingNthIteration() { return this.executingNthIteration; } @NotNull public final WeakReference getStartButtonRef() { return this.startButtonRef; } @NotNull public final WeakReference getFinishTextViewRef() { return this.finishTextViewRef; } @NotNull public final WeakReference getContainerRef() { return this.containerRef; } public MeasureLayoutAsyncTask(@NotNull String executingNthIteration, @NotNull WeakReference startButtonRef, @NotNull WeakReference finishTextViewRef, @NotNull WeakReference containerRef) { super(); Intrinsics.checkParameterIsNotNull(executingNthIteration, "executingNthIteration"); Intrinsics.checkParameterIsNotNull(startButtonRef, "startButtonRef"); Intrinsics.checkParameterIsNotNull(finishTextViewRef, "finishTextViewRef"); Intrinsics.checkParameterIsNotNull(containerRef, "containerRef"); this.executingNthIteration = executingNthIteration; this.startButtonRef = startButtonRef; this.finishTextViewRef = finishTextViewRef; this.containerRef = containerRef; } } } D/Jet: layoutMeasureDurationNs: 267344 D/Jet: layoutMeasureDurationNs: 47708 D/Jet: layoutMeasureDurationNs: 647240 D/Jet: layoutMeasureDurationNs: 59636 D/Jet: layoutMeasureDurationNs: 50052 D/Jet: layoutMeasureDurationNs: 49739 D/Jet: layoutMeasureDurationNs: 75990 D/Jet: layoutMeasureDurationNs: 296198 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 894375 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 1248021 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 0 D/Jet: layoutMeasureDurationNs: 1290677 D/Jet: layoutMeasureDurationNs: 2936563 D/Jet: layoutMeasureDurationNs: 1387188 D/Jet: layoutMeasureDurationNs: 2325521 D/Jet: layoutMeasureDurationNs: 1940052 D/Jet: layoutMeasureDurationNs: 1539271 D/Jet: layoutMeasureDurationNs: 803750 D/Jet: layoutMeasureDurationNs: 1405000 D/Jet: layoutMeasureDurationNs: 1188437 D/Jet: layoutMeasureDurationNs: 1748802 D/Jet: layoutMeasureDurationNs: 3422240 D/Jet: layoutMeasureDurationNs: 1400677 D/Jet: layoutMeasureDurationNs: 2416094 D/Jet: layoutMeasureDurationNs: 1532864 D/Jet: layoutMeasureDurationNs: 1684063 D/Jet: layoutMeasureDurationNs: 1092865 D/Jet: layoutMeasureDurationNs: 1363177 D/Jet: layoutMeasureDurationNs: 1067188 D/Jet: layoutMeasureDurationNs: 1358333 D/Jet: layoutMeasureDurationNs: 2999895 D/Jet: layoutMeasureDurationNs: 2113021 D/Jet: layoutMeasureDurationNs: 1957395 D/Jet: layoutMeasureDurationNs: 1319740 D/Jet: layoutMeasureDurationNs: 2207239 D/Jet: layoutMeasureDurationNs: 1514167 D/Jet: layoutMeasureDurationNs: 949114 D/Jet: layoutMeasureDurationNs: 1691250 D/Jet: layoutMeasureDurationNs: 1387448 D/Jet: layoutMeasureDurationNs: 932552 D/Jet: layoutMeasureDurationNs: 1223802 D/Jet: layoutMeasureDurationNs: 2024740 D/Jet: layoutMeasureDurationNs: 1242292 D/Jet: layoutMeasureDurationNs: 2228230 D/Jet: layoutMeasureDurationNs: 1382083 D/Jet: layoutMeasureDurationNs: 2233282 D/Jet: layoutMeasureDurationNs: 1907187 D/Jet: layoutMeasureDurationNs: 2287552 D/Jet: layoutMeasureDurationNs: 776354 D/Jet: layoutMeasureDurationNs: 1225000 D/Jet: layoutMeasureDurationNs: 875417 D/Jet: layoutMeasureDurationNs: 1271302 D/Jet: layoutMeasureDurationNs: 1211614 D/Jet: layoutMeasureDurationNs: 1346459 D/Jet: layoutMeasureDurationNs: 1978854 D/Jet: layoutMeasureDurationNs: 2915677 D/Jet: layoutMeasureDurationNs: 1330573 D/Jet: layoutMeasureDurationNs: 2195364 D/Jet: layoutMeasureDurationNs: 775208 D/Jet: layoutMeasureDurationNs: 2492292 D/Jet: layoutMeasureDurationNs: 400104 D/Jet: layoutMeasureDurationNs: 2844375 D/Jet: layoutMeasureDurationNs: 1563750 D/Jet: layoutMeasureDurationNs: 3689531 D/Jet: layoutMeasureDurationNs: 2019323 D/Jet: layoutMeasureDurationNs: 1663906 D/Jet: layoutMeasureDurationNs: 1004531 D/Jet: layoutMeasureDurationNs: 738125 D/Jet: layoutMeasureDurationNs: 1299166 D/Jet: layoutMeasureDurationNs: 1223854 D/Jet: layoutMeasureDurationNs: 1942240 D/Jet: layoutMeasureDurationNs: 1392396 D/Jet: layoutMeasureDurationNs: 1906458 D/Jet: layoutMeasureDurationNs: 691198 D/Jet: layoutMeasureDurationNs: 2620468 D/Jet: layoutMeasureDurationNs: 1953229 D/Jet: layoutMeasureDurationNs: 1120365 D/Jet: layoutMeasureDurationNs: 3165417 D/Jet: layoutMeasureDurationNs: 537709 D/Jet: layoutMeasureDurationNs: 3019531 D/Jet: layoutMeasureDurationNs: 706250 D/Jet: layoutMeasureDurationNs: 1129115 D/Jet: layoutMeasureDurationNs: 539427 D/Jet: layoutMeasureDurationNs: 1633438 D/Jet: layoutMeasureDurationNs: 1784479 D/Jet: layoutMeasureDurationNs: 743229 D/Jet: layoutMeasureDurationNs: 1851615 D/Jet: layoutMeasureDurationNs: 851927 D/Jet: layoutMeasureDurationNs: 1847916 D/Jet: layoutMeasureDurationNs: 836718 D/Jet: layoutMeasureDurationNs: 2892552 D/Jet: layoutMeasureDurationNs: 1230573 D/Jet: layoutMeasureDurationNs: 3886563 D/Jet: layoutMeasureDurationNs: 2138281 D/Jet: layoutMeasureDurationNs: 2198021 D/Jet: layoutMeasureDurationNs: 1805885 D/Jet: layoutMeasureDurationNs: 2316927 D/Jet: layoutMeasureDurationNs: 1990937 D/Jet: layoutMeasureDurationNs: 2261041 D/Jet: layoutMeasureDurationNs: 2159010 D/Jet: layoutMeasureDurationNs: 666562 D/Jet: layoutMeasureDurationNs: 2332031 D/Jet: layoutMeasureDurationNs: 1061875 D/Jet: layoutMeasureDurationNs: 1879062 D/Jet: layoutMeasureDurationNs: 1411459 D/Jet: layoutMeasureDurationNs: 154635
- 在Application中使用OnFrameMetricsAvailableListener,则可以统一设置,不需要每个Activity单独设置,推荐使用开源项目ActivityFrameMetrics
- 在Application的onCreate中设置单帧渲染总时间超过W毫秒和E毫秒,会在Logcat中打印警告和错误的Log信息.
public class SampleApplication extends Application { @Override public void onCreate() { registerActivityLifecycleCallbacks(new ActivityFrameMetrics.Builder() .warningLevelMs(10) //default: 17ms .errorLevelMs(10) //default: 34ms .showWarnings(true) //default: true .showErrors(true) //default: true .build()); } }
- Application设置完成运行App,出现单帧渲染总耗时超过指定时间,即可看到Logcat中的信息.
E/FrameMetrics: Janky frame detected on KtMainActivity with total duration: 16.91ms Layout/measure: 1.66ms, draw:2.51ms, gpuCommand:3.13ms others:9.61ms Janky frames: 72/107(67.28972%) E/FrameMetrics: Janky frame detected on KtMainActivity with total duration: 15.47ms Layout/measure: 1.00ms, draw:2.05ms, gpuCommand:3.44ms others:8.98ms Janky frames: 73/108(67.59259%) E/FrameMetrics: Janky frame detected on KtMainActivity with total duration: 15.09ms Layout/measure: 1.30ms, draw:1.44ms, gpuCommand:2.91ms others:9.44ms Janky frames: 74/110(67.27273%) ****
- 在Application的onCreate中设置单帧渲染总时间超过W毫秒和E毫秒,会在Logcat中打印警告和错误的Log信息.
5.Systrace:通过命令行生成html文件,通过Chrome浏览器进行分析
- 首先电脑要安装python,这里有几个坑:
- Python要安装2.7x版本,不能安装最新的3.x.
- 比如自己电脑中systrace文件夹路径是:C:\Users\你的用户名\AppData\Local\Android\Sdk\platform-tools\systrace,如果我们安装的是3.x版本,在这个路径下执行python systrace.py *** 命令会报错,提示你应该安装2.7
- Python安装时候,要记得勾选"Add python.exe to Path".
- 这时候直接执行python systrace.py ***命令还是会报错:ImportError: No module named win32com
- Python要安装2.7x版本,不能安装最新的3.x.
- 生成html及如何分析
网友评论