【0176】Android 面试- Android异常与性能优化相关

1.anr异常

1.1 异常的认识

  产生的主要原因是在主线程中做了耗时的操作;

1.2 主线程有哪些

 

1.3 解决anr

【摘抄文章】

1, 你碰到ANR了吗
在App使用过程中, 你可能遇到过这样的情况:

ANR
恭喜你, 这就是传说中的ANR.

1.1 何为ANR
ANR全名Application Not Responding, 也就是"应用无响应". 当操作在一段时间内系统无法处理时, 系统层面会弹出上图那样的ANR对话框.

1.2 为什么会产生ANR
在Android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的. 通常在如下两种情况下会弹出ANR对话框:

5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).
BroadcastReceiver在10s内无法结束.
造成以上两种情况的首要原因就是在主线程(UI线程)里面做了太多的阻塞耗时操作, 例如文件读写, 数据库读写, 网络查询等等.
1.3 如何避免ANR 知道了ANR产生的原因, 那么想要避免ANR, 也就很简单了, 就一条规则: 不要在主线程(UI线程)里面做繁重的操作. 这里面实际上涉及到两个问题: 哪些地方是运行在主线程的? 不在主线程做, 在哪儿做? 稍后解答. 2, ANR分析 2.1 获取ANR产生的trace文件 ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/. 可以通过adb命令将其导出到本地: $adb pull data/anr/traces.txt .
2.2 分析traces.txt 2.2.1 普通阻塞导致的ANR 获取到的tracs.txt文件一般如下: 如下以GithubApp代码为例, 强行sleep thread产生的一个ANR. ----- pid 2976 at 2016-09-08 23:02:47 ----- Cmd line: com.anly.githubapp // 最新的ANR发生的进程(包名) ... DALVIK THREADS (41): "main" prio=5 tid=1 Sleeping | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000 | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0 | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100 | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB | held mutexes= at java.lang.Thread.sleep!(Native method) - sleeping on <0x35fc9e33> (a java.lang.Object) at java.lang.Thread.sleep(Thread.java:1031) - locked <0x35fc9e33> (a java.lang.Object) at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应. at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258) - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c) at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166) // 产生ANR的那个函数调用 - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>) at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23) at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点 at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47) at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22) at android.view.View.performClick(View.java:4780) at android.view.View$PerformClick.run(View.java:19866) at android.os.Handler.handleCallback(Handler.java:739) at android.os.Handler.dispatchMessage(Handler.java:95) at android.os.Looper.loop(Looper.java:135) at android.app.ActivityThread.main(ActivityThread.java:5254) at java.lang.reflect.Method.invoke!(Native method) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698) 拿到trace信息, 一切好说. 如上trace信息中的添加的中文注释已基本说明了trace文件该怎么分析: 文件最上的即为最新产生的ANR的trace信息. 前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名). 寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源. 以上的ANR trace是属于相对简单, 还有可能你并没有在主线程中做过于耗时的操作, 然而还是ANR了. 这就有可能是如下两种情况了: 2.2.2 CPU满负荷 这个时候你看到的trace信息可能会包含这样的信息: Process:com.anly.githubapp ... CPU usage from 3330ms to 814ms ago: 6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major 4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major 0.9% 252/com.android.systemui: 0.9% user + 0% kernel ... 100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait 最后一句表明了: 当是CPU占用100%, 满负荷了. 其中绝大数是被iowait即I/O操作占用了. 此时分析方法调用栈, 一般来说会发现是方法中有频繁的文件读写或是数据库读写操作放在主线程来做了. 2.2.3 内存原因 其实内存原因有可能会导致ANR, 例如如果由于内存泄露, App可使用内存所剩无几, 我们点击按钮启动一个大图片作为背景的activity, 就可能会产生ANR, 这时trace信息可能是这样的: // 以下trace信息来自网络, 用来做个示例 Cmdline: android.process.acore DALVIK THREADS: "main"prio=5 tid=3 VMWAIT |group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8 | sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376 atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod) atandroid.graphics.Bitmap.nativeCreate(Native Method) atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468) atandroid.view.View.buildDrawingCache(View.java:6324) atandroid.view.View.getDrawingCache(View.java:6178) ... MEMINFO in pid 1360 [android.process.acore] ** native dalvik other total size: 17036 23111 N/A 40147 allocated: 16484 20675 N/A 37159 free: 296 2436 N/A 2732 可以看到free的内存已所剩无几. 当然这种情况可能更多的是会产生OOM的异常... 2.2 ANR的处理
针对三种不同的情况, 一般的处理情况如下 【1】主线程阻塞的:开辟单独的子线程来处理耗时阻塞事务. 【2】CPU满负荷, I
/O阻塞的 I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行. 【3】内存不够用的 增大VM内存, 使用largeHeap属性, 排查内存泄露(这个在内存优化那篇细说吧)等. 3, 深入一点 没有人愿意在出问题之后去解决问题. 高手和新手的区别是, 高手知道怎么在一开始就避免问题的发生. 那么针对ANR这个问题, 我们需要做哪些层次的工作来避免其发生呢? 3.1 哪些地方是执行在主线程的
Activity的所有生命周期回调都是执行在主线程的. Service默认是执行在主线程的. BroadcastReceiver的onReceive回调是执行在主线程的. 没有使用子线程的looper的Handler的handleMessage, post(Runnable)是执行在主线程的. AsyncTask的回调中除了doInBackground, 其他都是执行在主线程的. View的post(Runnable)是执行在主线程的.
3.2 使用子线程的方式有哪些 上面我们几乎一直在说, 避免ANR的方法就是在子线程中执行耗时阻塞操作. 那么在Android中有哪些方式可以让我们实现这一点呢. 3.2.1 启Thread方式 这个其实也是Java实现多线程的方式. 有两种实现方法, 继承Thread 或 实现Runnable接口: 继承Thread class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } PrimeThread p = new PrimeThread(143); p.start(); 实现Runnable接口 class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } PrimeRun p = new PrimeRun(143); new Thread(p).start(); 3.2.2 使用AsyncTask 这个是Android特有的方式, AsyncTask顾名思义, 就是异步任务的意思. private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { // Do the long-running work in here // 执行在子线程 protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); // Escape early if cancel() is called if (isCancelled()) break; } return totalSize; } // This is called each time you call publishProgress() // 执行在主线程 protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); } // This is called when doInBackground() is finished // 执行在主线程 protected void onPostExecute(Long result) { showNotification("Downloaded " + result + " bytes"); } } // 启动方式 new DownloadFilesTask().execute(url1, url2, url3); 3.2.3 HandlerThread Android中结合Handler和Thread的一种方式. 前面有云, 默认情况下Handler的handleMessage是执行在主线程的, 但是如果我给这个Handler传入了子线程的looper, handleMessage就会执行在这个子线程中的. HandlerThread正是这样的一个结合体: // 启动一个名为new_thread的子线程 HandlerThread thread = new HandlerThread("new_thread"); thread.start(); // 取new_thread赋值给ServiceHandler private ServiceHandler mServiceHandler; mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { // 此时handleMessage是运行在new_thread这个子线程中了. } } 3.2.4 IntentService Service是运行在主线程的, 然而IntentService是运行在子线程的. 实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式. 以上HandlerThread的使用代码示例也就来自于IntentService源码. 3.2.5 Loader Android 3.0引入的数据加载器, 可以在Activity/Fragment中使用. 支持异步加载数据, 并可监控数据源在数据发生变化时传递新结果. 常用的有CursorLoader, 用来加载数据库数据. // Prepare the loader. Either re-connect with an existing one, // or start a new one. // 使用LoaderManager来初始化Loader getLoaderManager().initLoader(0, null, this); //如果 ID 指定的加载器已存在,则将重复使用上次创建的加载器。 //如果 ID 指定的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以实现代码以实例化并返回新加载器 // 创建一个Loader public Loader<Cursor> onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. // First, pick the base URI to use depending on whether we are // currently filtering. Uri baseUri; if (mCurFilter != null) { baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(mCurFilter)); } else { baseUri = Contacts.CONTENT_URI; } // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); } // 加载完成 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data); } 具体请参看官网Loader介绍. 3.2.6 特别注意 使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点: Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); 因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的. 结语 对于ANR问题, 个人认为还是预防为主, 认清代码中的阻塞点, 善用线程. 同时形成良好的编程习惯, 要有MainThread和Worker Thread的概念的...(实际上人的工作状态也是这样的~~哈哈) 作者:anly_jun 链接:https://www.jianshu.com/p/6d855e984b99

2.oom异常

2.1 什么是oom?

【什么】Android系统会为每个app分配固定的内存空间

 

2.2  概念的分辨

内存抖动】内存抖动是指在时间内有大量对象被创建或者被回收的现象,主要是循环中大量创建、回收对象。这种情况应当尽量避免。

【内存泄露】程序通过new分配内存,在使用完毕后没有释放,造成内存占用。这块内存不受GC控制,无法通过GC回收

主要表现在:当一个对象已经不再使用,本该被回收的,但是另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种对象存在堆内存中,就产生了内存泄漏。
危害?内存泄漏对于app没有直接的危害,即使app有发生内存泄漏的情况,也不一定会引起app崩溃,但是会增加app内存的占用。内存得不到释放,慢慢的会造成app内存溢出。解决内存泄漏目的就是防止app发生内存溢出。
【内存溢出】当前占用的内存加上申请的内存资源超过了Dalvik虚拟机的最大的内存限制就会抛出oom异常;

 2.3 内存溢出的解决--与图片相关

【图片的请求】在显示缩略图的时候,不要调用网络请求加载大图;例如:在listView的滑动的时候不要调用网络请求加载图片,只有在不滑动的时候再加载图片;

【及时释放内存】bitmap实际上的生成是java和c两部分的内存同时生成的;

【图片的压缩】在加载大图之前需要bitmap的计算然后进行压缩;

 【isBitmap高级属性使用】参考下面的文章

【捕获outMemoryError属性】

安卓编程:可否用try-catch捕获Out Of Memory Error以避免其发生?
现已知代码A可能引发OOM。代码B可替代代码A但可维护性差。我希望能先尝试执行代码A,如果发生OOM,则退回来执行代码B。
请问以下解决方案是否可行。
try {
    代码A
} catch (OutOfMemoryError ignored) {
    代码B
}

试验了一下似乎可行。但有同事认为OOM发生在系统层级,上述代码无法获得我期望的效果。
=================
只有在一种情况下,这样做是可行的:在try语句中声明了很大的对象,导致OOM,并且可以确认OOM是由try语句中的对象声明导致的,那么在catch语句中,可以释放掉这些对象
解决OOM的问题,继续执行剩余语句。但是这通常不是合适的做法。Java中管理内存除了显式地catch OOM之外还有更多有效的方法:比如SoftReference, WeakReference,
硬盘缓存等。在JVM用光内存之前,会多次触发GC,这些GC会降低程序运行的效率。如果OOM的原因不是try语句中的对象(比如内存泄漏),那么在catch语句中会继续抛出OOM

2.4 其他的方法

 

2.4 内存相关内容

【参考内容】

内存

JAVA是在JVM所虚拟出的内存环境中运行的,内存分为三个区:堆、栈和方法区。
栈(stack):是简单的数据结构,程序运行时系统自动分配,使用完毕后自动释放。优点:速度快。
堆(heap):用于存放由new创建的对象和数组。在堆中分配的内存,一方面由java虚拟机自动垃圾回收器来管理,另一方面还需要程序员提供修养,防止内存泄露问题。
方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量

Java GC

GC可以自动清理堆中不在使用(不在有对象持有该对象的引用)的对象。

在JAVA中对象如果再没有引用指向该对象,那么该对象就无从处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占据的内存。

对android来说,内存使用尤为吃紧,最开始的app进程最大分配才16M的内存,渐渐增加到32M、64M,但是和服务端相比还是很渺小的。如果对象回收不及时,很容易出现OOM错误。

内存泄露

什么是内存泄露?程序通过new分配内存,在使用完毕后没有释放,造成内存占用。这块内存不受GC控制,无法通过GC回收。
主要表现在:当一个对象已经不再使用,本该被回收的,但是另外一个正在使用的对象持有它的引用从而就导致对象不能被回收。这种对象存在堆内存中,就产生了内存泄漏。危害?内存泄漏对于app没有直接的危害,即使app有发生内存泄漏的情况,也不一定会引起app崩溃,但是会增加app内存的占用。内存得不到释放,慢慢的会造成app内存溢出。解决内存泄漏目的就是防止app发生内存溢出。
内存泄露主要表现的当Activity在finish的时候,由于对象持有对Activity的引用,造成Activity没有被及时回收。
总结了下大致有5种情况造成内存泄露,
(1)static变量、匿名类的使用
(2)线程执行处理
(3)各种监听回调处置
(4)Bitmap等回收处置
(5)集合类只有增操作却没有减操作。
 
常见情况
1)外部类持有Activity的静态引用
  1. public class MainActivity extends AppCompatActivity {  
  2.     static Activity activity;  
  3.   
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);  
  8.         CommUtil commUtil = CommUtil.getInstance(this);  
  9.     }  
[java] view plain copy
 
  1. public class CommUtils {  
  2.     private static CommUtils instance;  
  3.     private Context context;  
  4.   
  5.     private CommUtils(Context context) {  
  6.         this.context = context;  
  7.     }  
  8.   
  9.     public static CommUtils getInstance(Context context) {  
  10.         if (instance == null) {  
  11.             instance = new CommUtils(context);  
  12.         }  
  13.         return instance;  
  14.     }  
  15. }  
2)异步执行耗时任务期间时,Thread、AsyncTask、TimeTask持有的Activty进行finish时,Activity实例不会被回收。
 
  1. protected void onCreate(Bundle savedInstanceState) {  
  2.         super.onCreate(savedInstanceState);  
  3.         setContentView(R.layout.activity_main);  
  4.         new AsyncTask<String, Void, String>() {  
  5.             @Override  
  6.             protected String doInBackground(String... params) {  
  7.                 for (int i = 0; i < 15; i++) {  
  8.                     try {  
  9.                         Log.e("MainActivity2", "dddd" + i + MainActivity2.this.getLocalClassName());  
  10.                         Thread.sleep(1000);  
  11.                     } catch (InterruptedException e) {  
  12.                         e.printStackTrace();  
  13.                     }  
  14.                 }  
  15.                 return null;  
  16.             }  
  17.   
  18.             @Override  
  19.             protected void onPostExecute(String s) {  
  20.                 super.onPostExecute(s);  
  21.             }  
  22.         }.execute();  
  23.     }  
3)Handler内部类造成内存泄露。
Handler为非静态内部类时会隐式持有当前activity引用。当Activity被 finish()时,若Handler有未处理完或延迟的消息(主要是Handler牵扯到线程问题),会造成activity不能被回收。
[java] view plain copy
 
  1. MyHandler myHandler = new MyHandler();  
  2.   
  3.    @Override  
  4.    protected void onCreate(@Nullable Bundle savedInstanceState) {  
  5.        super.onCreate(savedInstanceState);  
  6.        myHandler.postDelayed(new Runnable() {  
  7.            @Override  
  8.            public void run() {  
  9.   
  10.            }  
  11.        }, 50 * 1000);  
  12.    }  
  13.   
  14.    class MyHandler extends Handler {  
  15.   
  16.        @Override  
  17.        public void handleMessage(Message msg) {  
  18.            super.handleMessage(msg);  
  19.        }  
  20.    }  
解决办法:在Activity生命周期结束前,确保Handler移除消息(mMyHanlder.removeCallbacksAndMessages(null);)或者使用静态Handler内部类。
如:使用了弱引用替代强引用.
[java] view plain copy
 
  1. static MyHandler myHandler;  
  2.     @Override  
  3.     protected void onCreate(@Nullable Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         myHandler = new MyHandler(this);  
  6.     }  
  7.     static class MyHandler extends Handler {  
  8.         WeakReference<Activity> mActivityReference;  
  9.   
  10.         MyHandler(Activity activity) {  
  11.             mActivityReference = new WeakReference<Activity>(activity);  
  12.         }  
  13.   
  14.         @Override  
  15.         public void handleMessage(Message msg) {  
  16.             final Activity activity = mActivityReference.get();  
  17.             if (activity != null) {  
  18.                 //....  
  19.             }  
  20.         }  
  21.     }  
建议熟悉下:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)
 
4)匿名内部类的使用。
  1. public class DemoActivity extends AppCompatActivity {  
  2.       
  3.     Runnable runnable = new Runnable() {  
  4.         @Override  
  5.         public void run() {  
  6.   
  7.         }  
  8.     };  
runnable默认会持有DemoActivity的引用。若Activity被finish的时候,如线程在使用runnable,则会造成内存泄露。
 
5)构造Adapter时没有使用缓存的 convertView
 
[java] view plain copy
 
  1. public View getView(int position, View convertView, ViewGroup parent) {  
  2.         View view = null;  
  3.         if (convertView == null)  
  4.             convertView = View.inflate(this, R.layout.item_layout, false);  
  5.         view = convertView;  
  6.         return view;  
  7.     }  
6) 当使用了BraodcastReceiver、Cursor、Bitmap等资源时,若没有及时释放,则会引起内存泄漏。
 
7)集合类的不当使用。

更多内存泄露可以通过检测工具发现。检测工具主要有MAT、Memory Monitor 、Allocation Tracker 、Heap Viewer、LeakCanary

卡顿 是怎么形成的 卡顿的解决方式 ANR

讲下 GC 回收导致 画面卡顿的问题:

这里写图片描述

比如自定义view 中 
绘制第一个画面 ,绘制完 以后 再绘制第二个画面 把第一个页面会的回收掉.时间16毫秒

一般手机 刷新频率60hz, 1秒钟 刷新60次 , 那么 每隔16ms 就需要重新绘制一次 ,假如自定义view的计算超过16ms ,那么就会形成卡顿的情况 ,第二个画面就没有刷新上去. 就会形成卡顿.

例如 : 在一个长期运行循环的最内侧创建对象, 那么就会不断增加,很多数据会污染内存堆, 导致GC 回收,由于 这中额外的内存压力导致 GC非正常回收,导致多次回收.就形成卡顿.

需要内存优化.

工具1 momory monitor

作用 : 跟踪APP内存变化的情况. 
含义: 内存 监测 
视图方式观看 内存变化.

工具2 heap viewer

作用: 获取内存堆 的快照. 
含义: 堆栈快照 
位置 : 在device monitor 中

问题 1 : 内存泄露, 观察 momory monitor 出现,内存不断增加 内存不断增加情况 然后降低.

工具 使用

通过 heap viewer 查看

说下内存泄露的情况: 
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.

http://blog.csdn.net/cyq1028/article/details/19980369 这篇文章讲的不错 不过他用的是Eclipse的查看 工具,我用的studio . studio 的这个比较好.

解决方式: 最简单的方式. 可以添加一个清理方法. 防止内存泄露 .

解决: 跟踪内存分配

问题2 内存抖动: 通过momory monitor 发现 出现内存忽上忽下 形成针尖状的情况.

这里写图片描述

工具 3 allocation tracking 使用

含义: 分配-跟踪 
最重要的功能 : 
可以跟踪出现问题的源代码, 上面两个工具只能观察现象.

http://www.th7.cn/Program/Android/201602/764859.shtml 所用工具的具体使用

工具4 Trace View

含义: 分配 视图 
http://blog.jobbole.com/78995/ 连接

现在真实测试结果:  

1,为了搞清楚每个应用程序在Android系统中最多可分配多少内存空间,我们使用了真机进行测试,测试机型为魅族MX4 Pro,3G内存。

测试方法是直接申请一块较大的内存空间,看应用程序在最多申请多大的内存空间时会崩溃。

  结果:(1)未设定属性android:largeheap = "true"时,可以申请到的最大内存空间为221M。

     (2)设定属性android:largeheap = "true"时, 可以申请的最大内存空间为478M,是原来的两倍多一些。

  网上有网友提出可申请到的最大内存空间与手机配置有关,以后会加以验证。

2.实测,不准确, 准确的说话是 google原生OS的默认值是16M,但是各个厂家的OS会对这个值进行修改。

比如本人小米2S为例,这个值应该是96M。


Runtime rt=Runtime.getRuntime();
long maxMemory=rt.maxMemory();
log.i("maxMemory:",Long.toString(maxMemory/(1024*1024)));
這個可以直接得到app可使用的最大memory size算出來是MB, 获得的是heapgrowthlimit


先看机器的内存限制,在/system/build.prop文件中:
heapgrowthlimit就是一个普通应用的内存限制,用ActivityManager.getLargeMemoryClass()获得的值就是这个。
而heapsize是在manifest中设置了largeHeap=true 之后,可以使用的最大内存值
结论就是,设置largeHeap的确可以增加内存的申请量。但不是系统有多少内存就可以申请多少,而是由dalvik.vm.heapsize限制。
你可以在app manifest.xml加 largetHeap=true
可以申請較多的記憶體 ,但還是有機會爆掉.

<application
     .....
     android:label="XXXXXXXXXX"
     android:largeHeap="true">
    .......
</application>


cat /system/build.prop   //读取这些值
getprop dalvik.vm.heapsize  //如果build.prop里面没有heapsize这些值,可以用这个抓取默认值
setprop dalvik.vm.heapsize 256m  //设置

-----------------------    build.prop 部分内容 ---------------------

dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=96m
dalvik.vm.heapsize=384m
dalvik.vm.heaputilization=0.25
dalvik.vm.heapidealfree=8388608
dalvik.vm.heapconcurrentstart=2097152
ro.setupwizard.mode=OPTIONAL
ro.com.google.gmsversion=4.1_r6
net.bt.name=Android
dalvik.vm.stack-trace-file=/data/anr/traces.txt


最早的说法:

1、APP默认分配内存大小

在Android里,程序内存被分为2部分:native和dalvik,dalvik就是我们普通的java使用内存,也就是我们上一篇文章分析堆栈的时候使用的内存。我们创建的对象是在这里面分配的,
对于内存的限制是 native
+dalvik 不能超过最大限制。android程序内存一般限制在16M,也有的是24M(早期的Android系统G1,就是只有16M)。具体看定制系统的设置,
在Linux初始化代码里面Init.c,可以查到到默认的内存大小。有兴趣的朋友,可以分析一下虚拟机启动相关代码。这块比较深入,目前我也没时间去分析,后面有空会去钻研一下。   gDvm.heapSizeStart
= 2 * 1024 * 1024; // heap初始化大小为2M   gDvm.heapSizeMax = 16 * 1024 * 1024; // 最大的heap为16M      2、Android的GC如何回收内存 Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,
它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,
则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。 做应用开发的时候,你需要了解系统的GC(垃圾回收)机制是如何运行的,Android里面使用有向图作为遍历回收内存的机制。Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,
该图就是从起始顶点开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。   因此对于我们已经不需要使用的对象,我们可以把它设置为null,这样当GC运行的时候,就好遍历到你这个对象已经没有引用,会自动把该对象占用的内存回收。我们没法像C
++那样马上释放不需要的内存,但是我们可以主动告诉系统,哪些内存可以回收了。   3、查看应用内存使用情况   下面我们看看如何在开发过程中查看我们程序运行时内存使用情况。我们可以通过ADB的一个命令查看:   //$package_name:应用包名   //$pid:应用进程ID,可以用PS命令查看   adb shell dumpsys meminfo $package_name or $pid   上面是我使用包名查看Gallery例子的内存使用情况图,里面信息很多,不过我们主要关注的是native和Davilk的使用情况。(Android2.X和Android4.X查看的信息排序是不一样的,内容差不多,不过排布有差异,我上面是4.0的截图) Android底层内核是基于Linux的,而Linux里面相对Window来说,有一点很特别的是,会尽量使用系统内存加载一些缓存数据或者进程间共享数据。Linux本着不用白不用的原则,会尽量使用系统内存,加快我们应用的运行速度。当然,如果我们期待某个需要大内存的应用,
系统也能马上释放出一定的内存使用,这是系统内部调度实现。因此严格来说,我们要准备计算Linux下某个进程内存大小比较困难。 因为有paging
out to disk(换页),所以如果你把所有映射到进程的内存相加,它可能大于你的内存的实际物理大小。   dalvik:是指dalvik所使用的内存。   native:是被native堆使用的内存。应该指使用C\C++在堆上分配的内存。   other:是指除dalvik和native使用的内存。但是具体是指什么呢?至少包括在C\C++分配的非堆内存,比如分配在栈上的内存。puzlle!   Pss:它是把共享内存根据一定比例分摊到共享它的各个进程来计算所得到进程使用内存。网上又说是比例分配共享库占用的内存,也就是上面所说的进程共享问题。   PrivateDirty:它是指非共享的,又不能换页出去(can not be paged to disk )的内存的大小。比如Linux为了提高分配内存速度而缓冲的小对象,即使你的进程结束,该内存也不会释放掉,它只是又重新回到缓冲中而已。   SharedDirty:参照PrivateDirty我认为它应该是指共享的,又不能换页出去(can not be paged to disk )的内存的大小。比如Linux为了提高分配内存速度而缓冲的小对象,即使所有共享它的进程结束,该内存也不会释放掉,它只是又重新回到缓冲中而已。   上面针对meminfo里面的信息给出解析,这些很多我是参考了网上一些文章,所以如果有理解不到位的,欢迎各位指出。   4、程序中获取内存信息   通过ActivityManager获取相关信息,下面是一个例子代码:   privatevoid displayBriefMemory()   {    final ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);    ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();    activityManager.getMemoryInfo(info);    Log.i(tag,"系统剩余内存:"+(info.availMem >> 10)+"k");    Log.i(tag,"系统是否处于低内存运行:"+info.lowMemory);    Log.i(tag,"当系统剩余内存低于"+info.threshold+"时就看成低内存运行");   }   另外通过Debug的getMemoryInfo(Debug.MemoryInfo memoryInfo)可以得到更加详细的信息。跟我们在ADB Shell看到的信息一样比较详细。   5、总结 今天主要是分析了如何获取我们应用的内存使用情况信息,关于这方面的信息,其实还有其他一些方法。另外还介绍APP应用的默认内存已经Android的GC回收,
不过上面只是很浅薄地分析了一下,让大家有个印象。这些东西真要深入分析得花不少精力。因为我们的目的只是解决OOM问题,所以目前没打算深入分析,后面有时间进行Android系统分析的时候,我们再深入分析。下一次我们用以前写的Gallery例子讲解如何避免OOM问题,以及内存优化方法。

2.5 图片压缩相关

【第一篇文章】

在网上调查了图片压缩的方法并实装后,大致上可以认为有两类压缩:质量压缩(不改变图片的尺寸)和尺寸压缩(相当于是像素上的压缩);
质量压缩一般可用于上传大图前的处理,这样就可以节省一定的流量,毕竟现在的手机拍照都能达到3M左右了,尺寸压缩一般可用于生成缩略图。
两种方法都实装在了我的项目中,结果却发现在质量压缩的模块中,本来1.9M的图片压缩后反而变成3M多了,很是奇怪,再做了进一步调查终于知道原因了。下面这个博客说的比较清晰:
android图片压缩总结

总结来看,图片有三种存在形式:硬盘上时是file,网络传输时是stream,内存中是stream或bitmap,所谓的质量压缩,它其实只能实现对file的影响,你可以把一个file转成bitmap再转成file,或者直接将一个bitmap转成file时,这个最终的file是被压缩过的,但是中间的bitmap并没有被压缩(或者说几乎没有被压缩,我不确定),因为bigmap在内存中的大小是按像素计算的,也就是width * height,对于质量压缩,并不会改变图片的像素,所以就算质量被压缩了,但是bitmap在内存的占有率还是没变小,但你做成file时,它确实变小了;

而尺寸压缩由于是减小了图片的像素,所以它直接对bitmap产生了影响,当然最终的file也是相对的变小了;

最后把自己总结的工具类贴出来:
[java] view plain copy
 
  1. import java.io.ByteArrayInputStream;  
  2. import java.io.ByteArrayOutputStream;  
  3. import java.io.File;  
  4. import java.io.FileNotFoundException;  
  5. import java.io.FileOutputStream;  
  6. import java.io.IOException;  
  7.   
  8. import android.graphics.Bitmap;  
  9. import android.graphics.Bitmap.Config;  
  10. import android.graphics.BitmapFactory;  
  11.   
  12. /** 
  13.  * Image compress factory class 
  14.  *  
  15.  * @author  
  16.  * 
  17.  */  
  18. public class ImageFactory {  
  19.   
  20.     /** 
  21.      * Get bitmap from specified image path 
  22.      *  
  23.      * @param imgPath 
  24.      * @return 
  25.      */  
  26.     public Bitmap getBitmap(String imgPath) {  
  27.         // Get bitmap through image path  
  28.         BitmapFactory.Options newOpts = new BitmapFactory.Options();  
  29.         newOpts.inJustDecodeBounds = false;  
  30.         newOpts.inPurgeable = true;  
  31.         newOpts.inInputShareable = true;  
  32.         // Do not compress  
  33.         newOpts.inSampleSize = 1;  
  34.         newOpts.inPreferredConfig = Config.RGB_565;  
  35.         return BitmapFactory.decodeFile(imgPath, newOpts);  
  36.     }  
  37.       
  38.     /** 
  39.      * Store bitmap into specified image path 
  40.      *  
  41.      * @param bitmap 
  42.      * @param outPath 
  43.      * @throws FileNotFoundException  
  44.      */  
  45.     public void storeImage(Bitmap bitmap, String outPath) throws FileNotFoundException {  
  46.         FileOutputStream os = new FileOutputStream(outPath);  
  47.         bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);  
  48.     }  
  49.       
  50.     /** 
  51.      * Compress image by pixel, this will modify image width/height.  
  52.      * Used to get thumbnail 
  53.      *  
  54.      * @param imgPath image path 
  55.      * @param pixelW target pixel of width 
  56.      * @param pixelH target pixel of height 
  57.      * @return 
  58.      */  
  59.     public Bitmap ratio(String imgPath, float pixelW, float pixelH) {  
  60.         BitmapFactory.Options newOpts = new BitmapFactory.Options();    
  61.         // 开始读入图片,此时把options.inJustDecodeBounds 设回true,即只读边不读内容  
  62.         newOpts.inJustDecodeBounds = true;  
  63.         newOpts.inPreferredConfig = Config.RGB_565;  
  64.         // Get bitmap info, but notice that bitmap is null now    
  65.         Bitmap bitmap = BitmapFactory.decodeFile(imgPath,newOpts);  
  66.             
  67.         newOpts.inJustDecodeBounds = false;    
  68.         int w = newOpts.outWidth;    
  69.         int h = newOpts.outHeight;    
  70.         // 想要缩放的目标尺寸  
  71.         float hh = pixelH;// 设置高度为240f时,可以明显看到图片缩小了  
  72.         float ww = pixelW;// 设置宽度为120f,可以明显看到图片缩小了  
  73.         // 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可    
  74.         int be = 1;//be=1表示不缩放    
  75.         if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放    
  76.             be = (int) (newOpts.outWidth / ww);    
  77.         } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放    
  78.             be = (int) (newOpts.outHeight / hh);    
  79.         }    
  80.         if (be <= 0) be = 1;    
  81.         newOpts.inSampleSize = be;//设置缩放比例  
  82.         // 开始压缩图片,注意此时已经把options.inJustDecodeBounds 设回false了  
  83.         bitmap = BitmapFactory.decodeFile(imgPath, newOpts);  
  84.         // 压缩好比例大小后再进行质量压缩  
  85. //        return compress(bitmap, maxSize); // 这里再进行质量压缩的意义不大,反而耗资源,删除  
  86.         return bitmap;  
  87.     }  
  88.       
  89.     /** 
  90.      * Compress image by size, this will modify image width/height.  
  91.      * Used to get thumbnail 
  92.      *  
  93.      * @param image 
  94.      * @param pixelW target pixel of width 
  95.      * @param pixelH target pixel of height 
  96.      * @return 
  97.      */  
  98.     public Bitmap ratio(Bitmap image, float pixelW, float pixelH) {  
  99.         ByteArrayOutputStream os = new ByteArrayOutputStream();  
  100.         image.compress(Bitmap.CompressFormat.JPEG, 100, os);  
  101.         if( os.toByteArray().length / 1024>1024) {//判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出      
  102.             os.reset();//重置baos即清空baos    
  103.             image.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中    
  104.         }    
  105.         ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());    
  106.         BitmapFactory.Options newOpts = new BitmapFactory.Options();    
  107.         //开始读入图片,此时把options.inJustDecodeBounds 设回true了    
  108.         newOpts.inJustDecodeBounds = true;  
  109.         newOpts.inPreferredConfig = Config.RGB_565;  
  110.         Bitmap bitmap = BitmapFactory.decodeStream(is, null, newOpts);    
  111.         newOpts.inJustDecodeBounds = false;    
  112.         int w = newOpts.outWidth;    
  113.         int h = newOpts.outHeight;    
  114.         float hh = pixelH;// 设置高度为240f时,可以明显看到图片缩小了  
  115.         float ww = pixelW;// 设置宽度为120f,可以明显看到图片缩小了  
  116.         //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可    
  117.         int be = 1;//be=1表示不缩放    
  118.         if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放    
  119.             be = (int) (newOpts.outWidth / ww);    
  120.         } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放    
  121.             be = (int) (newOpts.outHeight / hh);    
  122.         }    
  123.         if (be <= 0) be = 1;    
  124.         newOpts.inSampleSize = be;//设置缩放比例    
  125.         //重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了    
  126.         is = new ByteArrayInputStream(os.toByteArray());    
  127.         bitmap = BitmapFactory.decodeStream(is, null, newOpts);  
  128.         //压缩好比例大小后再进行质量压缩  
  129. //      return compress(bitmap, maxSize); // 这里再进行质量压缩的意义不大,反而耗资源,删除  
  130.         return bitmap;  
  131.     }  
  132.       
  133.     /** 
  134.      * Compress by quality,  and generate image to the path specified 
  135.      *  
  136.      * @param image 
  137.      * @param outPath 
  138.      * @param maxSize target will be compressed to be smaller than this size.(kb) 
  139.      * @throws IOException  
  140.      */  
  141.     public void compressAndGenImage(Bitmap image, String outPath, int maxSize) throws IOException {  
  142.         ByteArrayOutputStream os = new ByteArrayOutputStream();  
  143.         // scale  
  144.         int options = 100;  
  145.         // Store the bitmap into output stream(no compress)  
  146.         image.compress(Bitmap.CompressFormat.JPEG, options, os);    
  147.         // Compress by loop  
  148.         while ( os.toByteArray().length / 1024 > maxSize) {  
  149.             // Clean up os  
  150.             os.reset();  
  151.             // interval 10  
  152.             options -= 10;  
  153.             image.compress(Bitmap.CompressFormat.JPEG, options, os);  
  154.         }  
  155.           
  156.         // Generate compressed image file  
  157.         FileOutputStream fos = new FileOutputStream(outPath);    
  158.         fos.write(os.toByteArray());    
  159.         fos.flush();    
  160.         fos.close();    
  161.     }  
  162.       
  163.     /** 
  164.      * Compress by quality,  and generate image to the path specified 
  165.      *  
  166.      * @param imgPath 
  167.      * @param outPath 
  168.      * @param maxSize target will be compressed to be smaller than this size.(kb) 
  169.      * @param needsDelete Whether delete original file after compress 
  170.      * @throws IOException  
  171.      */  
  172.     public void compressAndGenImage(String imgPath, String outPath, int maxSize, boolean needsDelete) throws IOException {  
  173.         compressAndGenImage(getBitmap(imgPath), outPath, maxSize);  
  174.           
  175.         // Delete original file  
  176.         if (needsDelete) {  
  177.             File file = new File (imgPath);  
  178.             if (file.exists()) {  
  179.                 file.delete();  
  180.             }  
  181.         }  
  182.     }  
  183.       
  184.     /** 
  185.      * Ratio and generate thumb to the path specified 
  186.      *  
  187.      * @param image 
  188.      * @param outPath 
  189.      * @param pixelW target pixel of width 
  190.      * @param pixelH target pixel of height 
  191.      * @throws FileNotFoundException 
  192.      */  
  193.     public void ratioAndGenThumb(Bitmap image, String outPath, float pixelW, float pixelH) throws FileNotFoundException {  
  194.         Bitmap bitmap = ratio(image, pixelW, pixelH);  
  195.         storeImage( bitmap, outPath);  
  196.     }  
  197.       
  198.     /** 
  199.      * Ratio and generate thumb to the path specified 
  200.      *  
  201.      * @param image 
  202.      * @param outPath 
  203.      * @param pixelW target pixel of width 
  204.      * @param pixelH target pixel of height 
  205.      * @param needsDelete Whether delete original file after compress 
  206.      * @throws FileNotFoundException 
  207.      */  
  208.     public void ratioAndGenThumb(String imgPath, String outPath, float pixelW, float pixelH, boolean needsDelete) throws FileNotFoundException {  
  209.         Bitmap bitmap = ratio(imgPath, pixelW, pixelH);  
  210.         storeImage( bitmap, outPath);  
  211.           
  212.         // Delete original file  
  213.                 if (needsDelete) {  
  214.                     File file = new File (imgPath);  
  215.                     if (file.exists()) {  
  216.                         file.delete();  
  217.                     }  
  218.                 }  
  219.     }  
  220.       
  221. }  
 
 
如果上面的工具类不满足你,那么看看下面的方法。
 
一、图片质量压缩
[java] view plain copy
 
  1. /** 
  2.  * 质量压缩方法 
  3.  * 
  4.  * @param image 
  5.  * @return 
  6.  */  
  7. public static Bitmap compressImage(Bitmap image) {  
  8.   
  9.     ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  10.     image.compress(Bitmap.CompressFormat.JPEG, 100, baos);// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中  
  11.     int options = 90;  
  12.   
  13.     while (baos.toByteArray().length / 1024 > 100) { // 循环判断如果压缩后图片是否大于100kb,大于继续压缩  
  14.         baos.reset(); // 重置baos即清空baos  
  15.         image.compress(Bitmap.CompressFormat.JPEG, options, baos);// 这里压缩options%,把压缩后的数据存放到baos中  
  16.         options -= 10;// 每次都减少10  
  17.     }  
  18.     ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中  
  19.     Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片  
  20.     return bitmap;  
  21. }  

二、按比例大小压缩 (路径获取图片)
 
[java] view plain copy
 
  1. /** 
  2.  * 图片按比例大小压缩方法 
  3.  * 
  4.  * @param srcPath (根据路径获取图片并压缩) 
  5.  * @return 
  6.  */  
  7. public static Bitmap getimage(String srcPath) {  
  8.   
  9.     BitmapFactory.Options newOpts = new BitmapFactory.Options();  
  10.     // 开始读入图片,此时把options.inJustDecodeBounds 设回true了  
  11.     newOpts.inJustDecodeBounds = true;  
  12.     Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newOpts);// 此时返回bm为空  
  13.   
  14.     newOpts.inJustDecodeBounds = false;  
  15.     int w = newOpts.outWidth;  
  16.     int h = newOpts.outHeight;  
  17.     // 现在主流手机比较多是800*480分辨率,所以高和宽我们设置为  
  18.     float hh = 800f;// 这里设置高度为800f  
  19.     float ww = 480f;// 这里设置宽度为480f  
  20.     // 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可  
  21.     int be = 1;// be=1表示不缩放  
  22.     if (w > h && w > ww) {// 如果宽度大的话根据宽度固定大小缩放  
  23.         be = (int) (newOpts.outWidth / ww);  
  24.     } else if (w < h && h > hh) {// 如果高度高的话根据宽度固定大小缩放  
  25.         be = (int) (newOpts.outHeight / hh);  
  26.     }  
  27.     if (be <= 0)  
  28.         be = 1;  
  29.     newOpts.inSampleSize = be;// 设置缩放比例  
  30.     // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了  
  31.     bitmap = BitmapFactory.decodeFile(srcPath, newOpts);  
  32.     return compressImage(bitmap);// 压缩好比例大小后再进行质量压缩  
  33. }  
三、按比例大小压缩 (Bitmap)
 
[java] view plain copy
 
  1. /** 
  2.  * 图片按比例大小压缩方法 
  3.  * 
  4.  * @param image (根据Bitmap图片压缩) 
  5.  * @return 
  6.  */  
  7. public static Bitmap compressScale(Bitmap image) {  
  8.   
  9.     ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  10.     image.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
  11.   
  12.     // 判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出  
  13.     if (baos.toByteArray().length / 1024 > 1024) {  
  14.         baos.reset();// 重置baos即清空baos  
  15.         image.compress(Bitmap.CompressFormat.JPEG, 80, baos);// 这里压缩50%,把压缩后的数据存放到baos中  
  16.     }  
  17.     ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());  
  18.     BitmapFactory.Options newOpts = new BitmapFactory.Options();  
  19.     // 开始读入图片,此时把options.inJustDecodeBounds 设回true了  
  20.     newOpts.inJustDecodeBounds = true;  
  21.     Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);  
  22.     newOpts.inJustDecodeBounds = false;  
  23.     int w = newOpts.outWidth;  
  24.     int h = newOpts.outHeight;  
  25.     Log.i(TAG, w + "---------------" + h);  
  26.     // 现在主流手机比较多是800*480分辨率,所以高和宽我们设置为  
  27.     // float hh = 800f;// 这里设置高度为800f  
  28.     // float ww = 480f;// 这里设置宽度为480f  
  29.     float hh = 512f;  
  30.     float ww = 512f;  
  31.     // 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可  
  32.     int be = 1;// be=1表示不缩放  
  33.     if (w > h && w > ww) {// 如果宽度大的话根据宽度固定大小缩放  
  34.         be = (int) (newOpts.outWidth / ww);  
  35.     } else if (w < h && h > hh) { // 如果高度高的话根据高度固定大小缩放  
  36.         be = (int) (newOpts.outHeight / hh);  
  37.     }  
  38.     if (be <= 0)  
  39.         be = 1;  
  40.     newOpts.inSampleSize = be; // 设置缩放比例  
  41.     // newOpts.inPreferredConfig = Config.RGB_565;//降低图片从ARGB888到RGB565  
  42.   
  43.     // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了  
  44.     isBm = new ByteArrayInputStream(baos.toByteArray());  
  45.     bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);  
  46.   
  47.     return compressImage(bitmap);// 压缩好比例大小后再进行质量压缩  
  48.   
  49.     //return bitmap;  
  50. }  
--------------------------------------------------------------------------------------------------------------------------------
 
分享个按照图片尺寸压缩:
 
[java] view plain copy
 
  1. public static void compressPicture(String srcPath, String desPath) {  
  2.         FileOutputStream fos = null;  
  3.         BitmapFactory.Options op = new BitmapFactory.Options();  
  4.   
  5.         // 开始读入图片,此时把options.inJustDecodeBounds 设回true了  
  6.         op.inJustDecodeBounds = true;  
  7.         Bitmap bitmap = BitmapFactory.decodeFile(srcPath, op);  
  8.         op.inJustDecodeBounds = false;  
  9.   
  10.         // 缩放图片的尺寸  
  11.         float w = op.outWidth;  
  12.         float h = op.outHeight;  
  13.         float hh = 1024f;//  
  14.         float ww = 1024f;//  
  15.         // 最长宽度或高度1024  
  16.         float be = 1.0f;  
  17.         if (w > h && w > ww) {  
  18.             be = (float) (w / ww);  
  19.         } else if (w < h && h > hh) {  
  20.             be = (float) (h / hh);  
  21.         }  
  22.         if (be <= 0) {  
  23.             be = 1.0f;  
  24.         }  
  25.         op.inSampleSize = (int) be;// 设置缩放比例,这个数字越大,图片大小越小.  
  26.         // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了  
  27.         bitmap = BitmapFactory.decodeFile(srcPath, op);  
  28.         int desWidth = (int) (w / be);  
  29.         int desHeight = (int) (h / be);  
  30.         bitmap = Bitmap.createScaledBitmap(bitmap, desWidth, desHeight, true);  
  31.         try {  
  32.             fos = new FileOutputStream(desPath);  
  33.             if (bitmap != null) {  
  34.                 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);  
  35.             }  
  36.         } catch (FileNotFoundException e) {  
  37.             e.printStackTrace();  
  38.         }  
  39.     }  


需要注意两个问题:

 

一、调用getDrawingCache()前先要测量,否则的话得到的bitmap为null,这个我在OnCreate()、OnStart()、OnResume()方法里都试验过。

二、当调用bitmap.compress(CompressFormat.JPEG, 100, fos);保存为图片时发现图片背景为黑色,如下图:

这时只需要改成用png保存就可以了,bitmap.compress(CompressFormat.PNG, 100, fos);,如下图:

在实际开发中,有时候我们需求将文件转换为字符串,然后作为参数进行上传。

必备工具类图片bitmap转成字符串string与String字符串转换为bitmap图片格式

[java] view plain copy
 
  1. import android.graphics.Bitmap;  
  2. import android.graphics.BitmapFactory;  
  3. import android.util.Base64;  
  4.   
  5. import java.io.ByteArrayOutputStream;  
  6.   
  7. /** 
  8.  *  
  9.  *  
  10.  * 功能描述:Android开发之常用必备工具类图片bitmap转成字符串string与String字符串转换为bitmap图片格式 
  11.  */  
  12. public class BitmapAndStringUtils {  
  13.     /** 
  14.      * 图片转成string 
  15.      * 
  16.      * @param bitmap 
  17.      * @return 
  18.      */  
  19.     public static String convertIconToString(Bitmap bitmap)  
  20.     {  
  21.         ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream  
  22.         bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);  
  23.         byte[] appicon = baos.toByteArray();// 转为byte数组  
  24.         return Base64.encodeToString(appicon, Base64.DEFAULT);  
  25.   
  26.     }  
  27.   
  28.     /** 
  29.      * string转成bitmap 
  30.      * 
  31.      * @param st 
  32.      */  
  33.     public static Bitmap convertStringToIcon(String st)  
  34.     {  
  35.         // OutputStream out;  
  36.         Bitmap bitmap = null;  
  37.         try  
  38.         {  
  39.             // out = new FileOutputStream("/sdcard/aa.jpg");  
  40.             byte[] bitmapArray;  
  41.             bitmapArray = Base64.decode(st, Base64.DEFAULT);  
  42.             bitmap =  
  43.                     BitmapFactory.decodeByteArray(bitmapArray, 0,  
  44.                             bitmapArray.length);  
  45.             // bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);  
  46.             return bitmap;  
  47.         }  
  48.         catch (Exception e)  
  49.         {  
  50.             return null;  
  51.         }  
  52.     }  
如果你的图片是File文件,可以用下面代码:
[java] view plain copy
 
  1. /** 
  2.  * 图片文件转换为指定编码的字符串 
  3.  * 
  4.  * @param imgFile  图片文件 
  5.  */  
  6. public static String file2String(File imgFile) {  
  7.     InputStream in = null;  
  8.     byte[] data = null;  
  9.     //读取图片字节数组  
  10.     try{  
  11.         in = new FileInputStream(imgFile);  
  12.         data = new byte[in.available()];  
  13.         in.read(data);  
  14.         in.close();  
  15.     } catch (IOException e){  
  16.         e.printStackTrace();  
  17.     }  
  18.     //对字节数组Base64编码  
  19.     BASE64Encoder encoder = new BASE64Encoder();  
  20.     String result = encoder.encode(data);  
  21.     return result;//返回Base64编码过的字节数组字符串  
  22. }  

【第二篇文章】Android压缩图片到100K以下并保持不失真的高效方法

在开发Android企业应用时,会经常上传图片到服务器,而我们公司目前维护的一个项目便是如此。该项目是通过私有apn与服务器进行交互的,联通的还好,但移动的速度实在太慢,客户在使用软件的过程中,由于上传的信息中可能包含多张图片,会经常出现上传图片失败的问题,为了解决这个问题,我们决定把照片压缩到100k以下,并且保证图片不失真(目前图片经过压缩后,大约300k左右)。于是我就重新研究了一下Android的图片压缩技术。

Android端目录结构如下图所示:


使用的第三方库jar包,如下图所示:

其中ksoap2-android-xxx.jar是Android用来调用webservice的,gson-xx.jar是把JavaBean转成Json数据格式的。
本篇博客主要讲解图片压缩的,核心代码如下:

[java] view plain copy
 
  1. //计算图片的缩放值  
  2. public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth, int reqHeight) {  
  3.     final int height = options.outHeight;  
  4.     final int width = options.outWidth;  
  5.     int inSampleSize = 1;  
  6.   
  7.     if (height > reqHeight || width > reqWidth) {  
  8.              final int heightRatio = Math.round((float) height/ (float) reqHeight);  
  9.              final int widthRatio = Math.round((float) width / (float) reqWidth);  
  10.              inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
  11.     }  
  12.         return inSampleSize;  
  13. }  
[java] view plain copy
  1. // 根据路径获得图片并压缩,返回bitmap用于显示  
  2. public static Bitmap getSmallBitmap(String filePath) {  
  3.         final BitmapFactory.Options options = new BitmapFactory.Options();  
  4.         options.inJustDecodeBounds = true;  
  5.         BitmapFactory.decodeFile(filePath, options);  
  6.   
  7.         // Calculate inSampleSize  
  8.     options.inSampleSize = calculateInSampleSize(options, 480, 800);  
  9.   
  10.         // Decode bitmap with inSampleSize set  
  11.     options.inJustDecodeBounds = false;  
  12.   
  13.     return BitmapFactory.decodeFile(filePath, options);  
  14.     }  
[java] view plain copy
  1. //把bitmap转换成String  
  2. public static String bitmapToString(String filePath) {  
  3.   
  4.         Bitmap bm = getSmallBitmap(filePath);  
  5.         ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  6.         bm.compress(Bitmap.CompressFormat.JPEG, 40, baos);  
  7.         byte[] b = baos.toByteArray();  
  8.         return Base64.encodeToString(b, Base64.DEFAULT);  
  9.     }  

查看全部源码,请访问:
https://github.com/feicien/StudyDemo/tree/master/FileUploadDemo

压缩原理讲解:压缩一张图片。我们需要知道这张图片的原始大小,然后根据我们设定的压缩比例进行压缩。
这样我们就需要做3件事:
1.获取原始图片的长和宽

[java] view plain copy
 
  1. BitmapFactory.Options options = new BitmapFactory.Options();  
  2.        options.inJustDecodeBounds = true;  
  3.        BitmapFactory.decodeFile(filePath, options);  
  4.                int height = options.outHeight;  
  5.            int width = options.outWidth;  

以上代码是对图片进行解码,inJustDecodeBounds设置为true,可以不把图片读到内存中,但依然可以计算出图片的大小,这正好可以满足我们第一步的需要。
2.计算压缩比例

[java] view plain copy
  1. int height = options.outHeight;  
  2.     int width = options.outWidth;   
  3.     int inSampleSize = 1;  
  4.     int reqHeight=800;  
  5.     int reqWidth=480;  
  6.     if (height > reqHeight || width > reqWidth) {  
  7.    final int heightRatio = Math.round((float) height/ (float) reqHeight);  
  8.    final int widthRatio = Math.round((float) width / (float) reqWidth);              
  9.    inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
  10.     } 

一般手机的分辨率为 480*800 ,所以我们压缩后图片期望的宽带定为480,高度设为800,这2个值只是期望的宽度与高度,实际上压缩后的实际宽度也高度会比期望的要大。如果图片的原始高度或者宽带大约我们期望的宽带和高度,我们需要计算出缩放比例的数值。否则就不缩放。heightRatio是图片原始高度与压缩后高度的倍数,widthRatio是图片原始宽度与压缩后宽度的倍数。inSampleSize为heightRatio与widthRatio中最小的那个,inSampleSize就是缩放值。 inSampleSize为1表示宽度和高度不缩放,为2表示压缩后的宽度与高度为原来的1/2
3.缩放并压缩图片

[java] view plain copy
 
  1. //在内存中创建bitmap对象,这个对象按照缩放大小创建的  
  2.             options.inSampleSize = calculateInSampleSize(options, 480, 800);  
  3.        options.inJustDecodeBounds = false;  
  4.        Bitmap bitmap= BitmapFactory.decodeFile(filePath, options);  
  5.   
  6.        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
  7.        bm.compress(Bitmap.CompressFormat.JPEG, 60, baos);  
  8.        byte[] b = baos.toByteArray();  

前3行的代码其实已经得到了一个缩放的bitmap对象,如果你在应用中显示图片,就可以使用这个bitmap对象了。由于考虑到网络流量的问题。我们好需要牺牲图片的质量来换取一部分空间,这里调用bm.compress()方法进行压缩,这个方法的第二个参数,如果是100,表示不压缩,我这里设置的是60,你也可以更加你的需要进行设置,在实验的过程中我设置为30,图片都不会失真。

压缩效果:本demo可以把1.5M左右的图片压缩到100K左右,并且没有失真。
效果图如下:

更新:

[java] view plain copy
  1. /* 
  2. 压缩图片,处理某些手机拍照角度旋转的问题 
  3. */  
  4. public static String compressImage(Context context,String filePath,String fileName,int q) throws FileNotFoundException {  
  5.   
  6.         Bitmap bm = getSmallBitmap(filePath);  
  7.   
  8.         int degree = readPictureDegree(filePath);  
  9.   
  10.         if(degree!=0){//旋转照片角度  
  11.             bm=rotateBitmap(bm,degree);  
  12.         }  
  13.   
  14.         File imageDir = SDCardUtils.getImageDir(context);  
  15.   
  16.         File outputFile=new File(imageDir,fileName);  
  17.   
  18.         FileOutputStream out = new FileOutputStream(outputFile);  
  19.   
  20.         bm.compress(Bitmap.CompressFormat.JPEG, q, out);  
  21.   
  22.         return outputFile.getPath();  
  23.     }  

判断照片角度

[java] view plain copy
 
  1. public static int readPictureDegree(String path) {  
  2.         int degree = 0;  
  3.         try {  
  4.             ExifInterface exifInterface = new ExifInterface(path);  
  5.             int orientation = exifInterface.getAttributeInt(  
  6.                     ExifInterface.TAG_ORIENTATION,  
  7.                     ExifInterface.ORIENTATION_NORMAL);  
  8.             switch (orientation) {  
  9.             case ExifInterface.ORIENTATION_ROTATE_90:  
  10.                 degree = 90;  
  11.                 break;  
  12.             case ExifInterface.ORIENTATION_ROTATE_180:  
  13.                 degree = 180;  
  14.                 break;  
  15.             case ExifInterface.ORIENTATION_ROTATE_270:  
  16.                 degree = 270;  
  17.                 break;  
  18.             }  
  19.         } catch (IOException e) {  
  20.             e.printStackTrace();  
  21.         }  
  22.         return degree;  
  23.     }  

旋转照片

[java] view plain copy
 
    1. public static Bitmap rotateBitmap(Bitmap bitmap,int degress) {  
    2.         if (bitmap != null) {  
    3.             Matrix m = new Matrix();  
    4.             m.postRotate(degress);   
    5.             bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),  
    6.                     bitmap.getHeight(), m, true);  
    7.             return bitmap;  
    8.         }  
    9.         return bitmap;  
    10.     }  

 【第三篇文章】

1 分类

Android图片压缩结合多种压缩方式,常用的有尺寸压缩、质量压缩、采样率压缩以及通过JNI调用libjpeg库来进行压缩。 
参考此方法:Android-BitherCompress

备注:对于资源图片直接使用:tiny压缩

2 质量压缩

(1)原理:保持像素的前提下改变图片的位深及透明度,(即:通过算法抠掉(同化)了图片中的一些某个些点附近相近的像素),达到降低质量压缩文件大小的目的。

注意:它其实只能实现对file的影响,对加载这个图片出来的bitmap内存是无法节省的,还是那么大。因为bitmap在内存中的大小是按照像素计算的,也就是width*height,对于质量压缩,并不会改变图片的真实的像素(像素大小不会变)。

(2)使用场景:将图片压缩后将图片上传到服务器,或者保存到本地。根据实际需求来。

(3)源码示例

/**
     * 3.质量压缩
     * 设置bitmap options属性,降低图片的质量,像素不会减少
     * 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
     * 设置options 属性0-100,来实现压缩
     *
     * @param bmp
     * @param file
     */
    public static void qualityCompress(Bitmap bmp, File file) {
        // 0-100 100为不压缩
        int quality = 20;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3 尺寸压缩

(1)原理:通过减少单位尺寸的像素值,正真意义上的降低像素。1020*8880–

(2)使用场景:缓存缩略图的时候(头像处理)

(3)源码示例

/**
     * 4.尺寸压缩(通过缩放图片像素来减少图片占用内存大小)
     *
     * @param bmp
     * @param file
     */

    public static void sizeCompress(Bitmap bmp, File file) {
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
        canvas.drawBitmap(bmp, null, rect, null);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4 采样率压缩

(1)原理:设置图片的采样率,降低图片像素

(2) 好处:是不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜。

(3)问题:因为采样率是整数,所以不能很好的保证图片的质量。如我们需要的是在2和3采样率之间,用2的话图片就大了一点,但是用3的话图片质量就会有很明显的下降,这样也无法完全满足我的需要。

(4)源码示例

/**
     * 5.采样率压缩(设置图片的采样率,降低图片像素)
     *
     * @param filePath
     * @param file
     */
    public static void samplingRateCompress(String filePath, File file) {
        // 数值越高,图片像素越低
        int inSampleSize = 8;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
//          options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
        //采样率
        options.inSampleSize = inSampleSize;
        Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            if (file.exists()) {
                file.delete();
            } else {
                file.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

5 JNI终极压缩

5.1 Android图像处理引擎的缺漏

为什么IOS拍照1M的图片要比安卓拍照排出来的5M的图片还要清晰。都是在同一个环境下,保存的都是JPEG?

(1)历程 
95年 JPEG处理引擎,用于最初的在PC上面处理图片的引擎。 
05年 skia开源的引擎, 开发了一套基于JPEG处理引擎的第二次开发。便于浏览器的使用。 
07年安卓用的skia引擎(阉割版),谷歌拿了skia,去掉一个编码算法—哈夫曼算法。采用定长编码算法。但是解码还是保留了哈夫曼算法,导致了图片处理后文件变大了。 
(2)原因 
当时由于CPU和内存在手机上都非常吃紧 性能差,由于哈夫曼算法非常吃CPU,被迫用了其他的算法。 
(3)优化方案 
绕过安卓Bitmap API层,来自己编码实现—-修复使用哈夫曼算法。

5.2 哈夫曼算法

哈夫曼树详解点击—数据结构与算法之二叉树+遍历+哈夫曼树

(1)ARGB:一个像素点包涵四个信息:alpha,red,green,blue 
(2)如何得到每一个字母出现的权重?需要去扫描整个信息(图片信息–每一个像素包括ARGB),要大量计算,很耗CPU,1280*800像素*4。 

5.3 JNI开发步骤(大概步骤,具体实现未定)

(1)准备工作

1)http://www.ijg.org/下载JPEG引擎使用的库---libjpeg库,
基于该引擎来做一定的开发----自己实现编码,JNI开发。
(2)导入库文件libjpegbither.so
(3)导入头文件
(4)写mk文件——Android.mk、Applicatoin.mk
(5)写代码——C++:XX.cpp、C:XX.c

(2)开发过程

1)将android的bitmap解码,并转换成RGB数据
一个图片信息---像素点(argb),alpha去掉
(2)JPEG对象分配空间以及初始化
(3)指定压缩数据源
(4)获取文件信息
(5)为压缩设置参数,比如图像大小、类型、颜色空间
  boolean arith_code;       
  /* TRUE=arithmetic coding, FALSE=Huffman */
(6)开始压缩——jpeg_start_compress()
(7)压缩结束——jpeg_finish_compress()
(8)释放资源

5.4 源码示例

/**
     * 1.JNI终极压缩(通过JNI图片压缩把Bitmap保存到指定目录)
     *
     * @param image    bitmap对象
     * @param filePath 要保存的指定目录
     * @Description: 通过JNI图片压缩把Bitmap保存到指定目录
     */
    public static void jniUltimateCompress(Bitmap image, String filePath) {
        // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int quality = 20;
        // JNI调用保存图片到SD卡 这个关键
        NativeUtil.saveBitmap(image, quality, filePath, true);
    }

    /**
     * 1.JNI基本压缩(不保存Bitmap)
     *
     * @param bit      bitmap对象
     * @param fileName 指定保存目录名
     * @param optimize 是否采用哈弗曼表数据计算 品质相差5-10倍
     * @Description: JNI基本压缩
     */
    public static void jniBasicCompress(Bitmap bit, String fileName, boolean optimize) {
        saveBitmap(bit, DEFAULT_QUALITY, fileName, optimize);
    }

/**
     * 调用native方法
     *
     * @param bit
     * @param quality
     * @param fileName
     * @param optimize
     * @Description:函数描述
     */
    private static void saveBitmap(Bitmap bit, int quality, String fileName, boolean optimize) {
        compressBitmap(bit, bit.getWidth(), bit.getHeight(), quality, fileName.getBytes(), optimize);
    }

    /**
     * 调用底层 bitherlibjni.c中的方法
     *
     * @param bit
     * @param w
     * @param h
     * @param quality
     * @param fileNameBytes
     * @param optimize
     * @return
     * @Description:函数描述
     */
    private static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
                                                boolean optimize);

    /**
     * 加载lib下两个so文件
     */
    static {
        System.loadLibrary("jpegbither");
        System.loadLibrary("bitherjni");
    }

6 混合终极方法

(1)原理:三种方式结合使用实现指定图片内存大小,清晰度达到最优。

(2)使用场景:大图压缩,同时对图片质量要求较高。

(3)源码示例

/**
     * 2.混合终极方法(尺寸、质量、JNI压缩)
     *
     * @param image    bitmap对象
     * @param filePath 要保存的指定目录
     * @Description: 通过JNI图片压缩把Bitmap保存到指定目录
     */
    public static void mixCompress(Bitmap image, String filePath) {
        // 最大图片大小 1000KB
        int maxSize = 1000;
        // 获取尺寸压缩倍数
        int ratio = NativeUtil.getRatioSize(image.getWidth(), image.getHeight());
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(image.getWidth() / ratio, image.getHeight() / ratio, Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, image.getWidth() / ratio, image.getHeight() / ratio);
        canvas.drawBitmap(image, null, rect, null);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int quality = 100;
        result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        // 循环判断如果压缩后图片是否大于最大值,大于继续压缩
        while (baos.toByteArray().length / 1024 > maxSize) {
            // 重置baos即清空baos
            baos.reset();
            // 每次都减少10
            quality -= 10;
            // 这里压缩options%,把压缩后的数据存放到baos中
            result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        }
        // JNI调用保存图片到SD卡 这个关键
        NativeUtil.saveBitmap(result, quality, filePath, true);
        // 释放Bitmap
        if (result != null && !result.isRecycled()) {
            result.recycle();
            result = null;
        }
    }

    /**
     * 计算缩放比
     *
     * @param bitWidth  当前图片宽度
     * @param bitHeight 当前图片高度
     * @return
     * @Description:函数描述
     */
    public static int getRatioSize(int bitWidth, int bitHeight) {
        // 图片最大分辨率
        int imageHeight = 1920;
        int imageWidth = 1080;
        // 缩放比
        int ratio = 1;
        // 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        if (bitWidth > bitHeight && bitWidth > imageHeight) {
            // 如果图片宽度比高度大,以宽度为基准
            ratio = bitWidth / imageHeight;
        } else if (bitWidth < bitHeight && bitHeight > imageHeight) {
            // 如果图片高度比宽度大,以高度为基准
            ratio = bitHeight / imageHeight;
        }
        // 最小比率为1
        if (ratio <= 0)
            ratio = 1;
        return ratio;
    }

7 多种方法对比

7.1 当图片内存大于1MB

(1)原图内存是3.22MB 
(2)截图如下: 
这里写图片描述 
(3)效果: 
尺寸压缩后图片太模糊; 
混合终极方法压缩效果更佳,与jni终极方法压缩内存区别不大; 
质量压缩后图片与jni终极方法压缩后图片效果接近,略显模糊; 
(4)总结:可以考虑使用混合终极方法。

7.2 当图片内存小于1MB

(1)原图内存是109.94KB 
(2)截图如下: 
这里写图片描述 
(3)效果:

尺寸压缩后图片太模糊;
混合终极方法压缩后内存反而增加了一半;
质量压缩后图片和jni终极方法压缩后图片效果接近,但是内存更大;
jni终极方法压缩后图片效果与原图相差不大,内存也不大。

(4)总结:可以考虑使用jni终极方法。

8 参考链接

Android图片压缩(质量压缩和尺寸压缩)&Bitmap转成字符串上传

 2.6 inBitmap属性的使用

SDK版本

需要注意的是inBitmap只能在3.0以后使用。2.3上,bitmap的数据是存储在native的内存区域,并不是在Dalvik的内存堆上。

在android3.0开始,系统在BitmapFactory.Options里引入了inBitmap机制来配合缓存机制。如果在载入图片时传入了inBitmap那么载入的图片就是inBitmap里的值。

这样可以统一有缓存和无缓存的载入方式。

使用inBitmap,在4.4之前,只能重用相同大小的bitmap的内存区域,而4.4之后你可以重用任何bitmap的内存区域,只要这块内存比将要分配内存的bitmap大就可以

例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。

解码

新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。

DisplayingBitmaps

Managing Bitmap Memory 上的demo的DisplayingBitmaps.zip,代码也有用到inBitmap,但是DisplayingBitmaps功能还是很弱,因为遇到过不同的ImageView设置不同ScaleType,然后使用同一张图片会造成相互影响,设置图片圆角也是,所以这也是使用inBitmap要注意的地方。

使用

使用此方法需要inMutable=true,inSampleSize=1

测试

开发完APP最好用一些APP在线自动化测试工具进行一下测试:www.ineice.com

 +++++++++++++++++++++++++++++++++++++=

在Android3.0之前,Bitmap的内存分配分为两部分,一部分是分配在Dalvik的VM堆中,

而像素数据的内存是分配在Native堆中,而到了Android3.0之后,Bitmap的内存则已经全部分配在VM堆上,

这两种分配方式的区别在于,Native堆的内存不受Dalvik虚拟机的管理,我们想要释放Bitmap的内存,

必须手动调用Recycle方法,而到了Android 3.0之后的平台,我们就可以将Bitmap的内存完全放心的交给虚拟机管理了,

我们只需要保证Bitmap对象遵守虚拟机的GC Root Tracing的回收规则即可。

2.使用缓存,LruCache和DiskLruCache的结合 
关于LruCache和DiskLruCache,大家一定不会陌生(有疑问的朋友可以去API官网搜一下LruCache,而DiskLrucCache可以参考一下这篇不错的文章:DiskLruCache使用介绍),出于对性能和app的考虑,我们肯定是想着第一次从网络中加载到图片之后,能够将图片缓存在内存和sd卡中,这样,我们就不用频繁的去网络中加载图片,为了很好的控制内存问题,则会考虑使用LruCache作为Bitmap在内存中的存放容器,在sd卡则使用DiskLruCache来统一管理磁盘上的图片缓存。

3.SoftReference和inBitmap参数的结合 
在第二点中提及到,可以采用LruCache作为存放Bitmap的容器,而在LruCache中有一个方法值得留意,那就是entryRemoved,按照文档给出的说法,在LruCache容器满了需要淘汰存放其中的对象腾出空间的时候会调用此方法(注意,这里只是对象被淘汰出LruCache容器,但并不意味着对象的内存会立即被Dalvik虚拟机回收掉),此时可以在此方法中将Bitmap使用SoftReference包裹起来,并用事先准备好的一个HashSet容器来存放这些即将被回收的Bitmap,有人会问,这样存放有什么意义?之所以会这样存放,还需要再提及到inBitmap参数(在Android3.0才开始有的,详情查阅API中的BitmapFactory.Options参数信息),这个参数主要是提供给我们进行复用内存中的Bitmap,如果设置了此参数,且满足以下条件的时候:

  • Bitmap一定要是可变的,即inmutable设置一定为ture;
  • Android4.4以下的平台,需要保证inBitmap和即将要得到decode的Bitmap的尺寸规格一致;
  • Android4.4及其以上的平台,只需要满足inBitmap的尺寸大于要decode得到的Bitmap的尺寸规格即可;

在满足以上条件的时候,系统对图片进行decoder的时候会检查内存中是否有可复用的Bitmap,避免我们频繁的去SD卡上加载图片而造成系统性能的下降,毕竟从直接从内存中复用要比在SD卡上进行IO操作的效率要提高几十倍。写了太多文字,下面接着给出几段Demo Code

Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

// 用来盛放被LruCache淘汰出列的Bitmap
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // 当LruCache淘汰对象的时候被调用,用于在内存中重用Bitmap,提高加载图片的性能
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {

        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {

            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {

            if (Utils.hasHoneycomb()) {

                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
        //将inMutable设置true,inBitmap生效的条件之一
    options.inMutable = true;

    if (cache != null) {
        // 尝试寻找可以内存中课复用的的Bitmap
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {

            options.inBitmap = inBitmap;
        }
    }
}

// 获取当前可以满足复用条件的Bitmap,存在则返回该Bitmap,不存在则返回null
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;

    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        synchronized (mReusableBitmaps) {
            final Iterator<SoftReference<Bitmap>> iterator
                    = mReusableBitmaps.iterator();
            Bitmap item;

            while (iterator.hasNext()) {
                item = iterator.next().get();

                if (null != item && item.isMutable()) {

                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;
                        iterator.remove();
                        break;
                    }
                } else {

                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

//判断是否满足使用inBitmap的条件
static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // Android4.4开始,被复用的Bitmap尺寸规格大于等于需要的解码规格即可满足复用条件
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());return byteCount <= candidate.getAllocationByteCount();}// Android4.4之前,必须满足被复用的Bitmap和请求的Bitmap尺寸规格一致才能被复用return candidate.getWidth()== targetOptions.outWidth
            && candidate.getHeight()== targetOptions.outHeight
            && targetOptions.inSampleSize ==1;}

4.降低采样率,inSampleSize的计算 
相信大家对inSampleSize是一定不会陌生的,所以此处不再做过多的介绍,关于降低采样率对inSampleSize的计算方法,我看到网上的算法有很多,下面的这段算法应该是最好的算法了,其中还考虑了那种宽高相差很悬殊的图片(例如:全景图)的处理。

public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }

            long totalPixels = width / inSampleSize * height / inSampleSize ;

            final long totalReqPixelsCap = reqWidth * reqHeight * 2;

            while (totalPixels > totalReqPixelsCap) {
                inSampleSize *= 2;
                totalPixels /= 2;
            }
        }
        return inSampleSize;

5.采用decodeFileDescriptor来编码图片(暂时不知道原理,欢迎高手指点迷津) 
关于采用decodeFileDescriptor去处理图片可以节省内存这方面,我在写代码的时候进行过尝试,确实想比其他的decode方法要节省内存,查询了网上的解释,不是很清楚,自己看了一些源代码也弄不出个名堂,为什么使用这种方式就能够节省内存一些呢,如果有明白其中原理的高手,欢迎解答我的疑惑

 2.7 Bitmap

Bitmap在Android中指的是一张图片,可以是png,也可以是jpg等其他图片格式。

一、Bitmap的基本加载

Bitmap的加载离不开BitmapFactory类,关于Bitmap官方介绍Creates Bitmap objects from various sources, including files, streams, and byte-arrays.查看api,发现和描述的一样,BitmapFactory类提供了四类方法用来加载Bitmap:

  1. decodeFile 从文件系统加载
    a. 通过Intent打开本地图片或照片
    b. 在onActivityResult中获取图片uri
    c. 根据uri获取图片的路径
    d. 根据路径解析bitmap:Bitmap bm = BitmapFactory.decodeFile(sd_path)
  2. decodeResource 以R.drawable.xxx的形式从本地资源中加载
    Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.aaa);
  3. decodeStream 从输入流加载
    a.开启异步线程去获取网络图片
    b.网络返回InputStream
    c.解析:Bitmap bm = BitmapFactory.decodeStream(stream),这是一个耗时操作,要在子线程中执行
  4. decodeByteArray 从字节数组中加载
    接3.a,3.b,
    c. 把InputStream转换成byte[]
    d. 解析:Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);

注意:decodeFile和decodeResource间接调用decodeStream方法。
关于图片的基本加载既不是本文的重点,也不是什么难点,所以这里就不贴详细代码了,这里只写几句关键代码和伪代码,【详细代码】可以下载查看

二、高效的加载Bitmap

我们在使用bitmap时,经常会遇到内存溢出等情况,这是因为图片太大或者android系统对单个应用施加的内存限制等原因造成的,

比如上述方法1加载一张照片时就会报:06-28 10:43:30.777 26007-26036/com.peak.app W/OpenGLRenderer: Bitmap too large to be uploaded into a texture (3120x4160, max=4096x4096)

而方法2加载一个3+G的照片时会报Caused by: java.lang.OutOfMemoryError: Failed to allocate a 144764940 byte allocation with 16765264 free bytes and 109MB until OOM

所以,高效的使用bitmap就显得尤为重要,对他效率的优化也是如此。

高效加载Bitmap的思想也很简单,就是使用系统提供给我们Options类来处理Bitmap。翻看Bitmap的源码,发现上述四个加载bitmap的方法都是支持Options参数的。

通过BitmapFactory.Options按一定的采样率来加载缩小后的图片,然后在ImageView中使用缩小的图片这样就会降低内存占用避免【OOM】,提高了Bitamp加载时的性能。

这其实就是我们常说的图片尺寸压缩。尺寸压缩是压缩图片的像素,一张图片所占内存的大小 图片类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。

android 色彩模式说明:

  • ALPHA_8:每个像素占用1byte内存。
  • ARGB_4444:每个像素占用2byte内存
  • ARGB_8888:每个像素占用4byte内存
  • RGB_565:每个像素占用2byte内存

Android默认的色彩模式为ARGB_8888,这个色彩模式色彩最细腻,显示质量最高。但同样的,占用的内存也最大。

BitmapFactory.Options的inPreferredConfig参数可以 指定decode到内存中,手机中所采用的编码,可选值定义在Bitmap.Config中。缺省值是ARGB_8888。

假设一张1024*1024,模式为ARGB_8888的图片,那么它占有的内存就是:1024*1024*4 = 4MB

1、采样率inSampleSize
  • inSampleSize的值必须大于1时才会有效果,且采样率同时作用于宽和高;
  • 当inSampleSize=1时,采样后的图片为图片的原始大小
  • 当inSampleSize=2时,采样后的图片的宽高均为原始图片宽高的1/2,这时像素为原始图片的1/(22),占用内存也为原始图片的1/(22);
  • inSampleSize的取值应该总为2的整数倍,否则会向下取整,取一个最接近2的整数倍,比如inSampleSize=3时,系统会取inSampleSize=2

假设一张1024*1024,模式为ARGB_8888的图片,inSampleSize=2,原始占用内存大小是4MB,采样后的图片占用内存大小就是(1024/2) * (1024/2 )* 4 = 1MB

2、获取采样率遵循以下步骤
  1. 将BitmapFacpry.Options的inJustDecodeBounds参数设为true并加载图片当inJustDecodeBounds为true时,执行decodeXXX方法时,BitmapFactory只会解析图片的原始宽高信息,并不会真正的加载图片
  2. 从BitmapFacpry.Options取出图片的原始宽高(outWidth,outHeight)信息
  3. 选取合适的采样率
  4. 将BitmapFacpry.Options的inSampleSize参数设为false并重新加载图片

经过上面过程加载出来的图片就是采样后的图片,代码如下:

public void decodeResource(View view) {
    Bitmap bm = decodeBitmapFromResource();
    imageview.setImageBitmap(bm);
}

private Bitmap decodeBitmapFromResource(){
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(getResources(), R.drawable.bbbb, options);
    options.inSampleSize = calculateSampleSize(options,300,300);
    options.inJustDecodeBounds =false;
    return  BitmapFactory.decodeResource(getResources(),R.drawable.bbbb,options);
}

// 计算合适的采样率(当然这里还可以自己定义计算规则),reqWidth为期望的图片大小,单位是px
private int calculateSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
    Log.i("========","calculateSampleSize reqWidth:"+reqWidth+",reqHeight:"+reqHeight);
    int width = options.outWidth;
    int height =options.outHeight;
    Log.i("========","calculateSampleSize width:"+width+",height:"+height);
    int inSampleSize = 1;
    int halfWidth = width/2;
    int halfHeight = height/2;
    while((halfWidth/inSampleSize)>=reqWidth&& (halfHeight/inSampleSize)>=reqHeight){
        inSampleSize*=2;
        Log.i("========","calculateSampleSize inSampleSize:"+inSampleSize);
    }
    return inSampleSize;
}

三、使用Bitmap时的一些注意事项

1、不用的Bitmap及时释放
if (!bmp.isRecycle()) {
    bmp.recycle();   //回收图片所占的内存
    bitmap = null;
    system.gc();  //提醒系统及时回收
}

虽然调用recycle()并不能保证立即释放占用的内存,但是可以加速Bitmap的内存的释放。
释放内存以后,就不能再使用该Bitmap对象了,如果再次使用,就会抛出异常。所以一定要保证不再使用的时候释放。比如,如果是在某个Activity中使用Bitmap,就可以在Activity的onStop()或者onDestroy()方法中进行回收。

2、捕获异常

因为Bitmap非常耗内存,了避免应用在分配Bitmap内存的时候出现OutOfMemory异常以后Crash掉,需要特别注意实例化Bitmap部分的代码。

通常,在实例化Bitmap的代码中,一定要对OutOfMemory异常进行捕获。

很多开发者会习惯性的在代码中直接捕获Exception。

但是对于OutOfMemoryError来说,这样做是捕获不到的。因为OutOfMemoryError是一种Error,而不是Exception

    Bitmap bitmap = null;
    try {
        // 实例化Bitmap
        bitmap = BitmapFactory.decodeFile(path);
    } catch (OutOfMemoryError e) { //捕获不到
    
    }
    if (bitmap == null) {
        return defaultBitmapMap; // 如果实例化失败 返回默认的Bitmap对象
    }
3、【缓存通用的Bitmap对象】

有时候,可能需要在一个Activity里多次用到同一张图片。比如一个Activity会展示一些用户的头像列表,而如果用户没有设置头像的话,则会显示一个默认头像,

而这个头像是位于应用程序本身的资源文件中的。

如果有类似上面的场景,就可以对同一Bitmap进行缓存。

如果不进行缓存,尽管看到的是同一张图片文件,但是使用BitmapFactory类的方法来实例化出来的Bitmap,是不同的Bitmap对象。

缓存可以避免新建多个Bitmap对象,避免内存的浪费。

在Android应用开发过程中所说的缓存有两个级别,一个是硬盘缓存,一个是内存缓存。

4、图片的质量压缩

上述用inSampleSize压缩是尺寸压缩,Android中还有一种压缩方式叫质量压缩。

质量压缩是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小(kb)会有改变,但是导入成bitmap后占得内存是不变的,

宽高也不会改变。因为要保持像素不变,所以它就无法无限压缩,到达一个值之后就不会继续变小了。

显然这个方法并不适用与缩略图,其实也不适用于想通过压缩图片减少内存的适用,仅仅适用于想在保证图片质量的同时减少文件大小的情况而已

    private void compressImage(Bitmap image, int reqSize) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);// 质量压缩方法,这里100表示不压缩,
        int options = 100;
        while (baos.toByteArray().length / 1024 > reqSize) { // 循环判断压缩后的图片是否大于reqSize,大于则继续压缩
            baos.reset();//清空baos
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);// 这里压缩options%,把压缩后的数据放到baos中
            options -= 10;
        }
        // 把压缩后的baos放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
        //decode图片
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);
    }
5、Android加载大量图片内存溢出解决方案:
  1. 尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存,可以通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source
  2. 使用BitmapFactory.Options对图片进行压缩(上述第二部分)
  3. 运用Java软引用,进行图片缓存,将需要经常加载的图片放进缓存里,避免反复加载

四、Bitmap一些其他用法

1、图片旋转指定角度
// 图片旋转指定角度
private Bitmap rotateImage(Bitmap image, final int degree) {
    int width = image.getWidth();
    int height = image.getHeight();
    if (width > height) {
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        if (image != null && !image.isRecycled()) {
            Bitmap resizedBitmap = Bitmap.createBitmap(image, 0, 0, width, height, matrix, true);
            return resizedBitmap;
        } else {
            return null;
        }
    } else {
        return image;
    }
}
2、图片合成
    private Bitmap createStarBitmap(float grade, int maxGrade) {
        Bitmap empty_star = BitmapFactory.decodeResource(getResources(), R.drawable.empty_star); // 空星
        Bitmap normal_star = BitmapFactory.decodeResource(getResources(), R.drawable.normal_star); // 实星
        Bitmap half_star = BitmapFactory.decodeResource(getResources(), R.drawable.half_star);
        ; // 半星
        int star_width = empty_star.getWidth();
        int star_height = empty_star.getHeight();
        Bitmap newb = Bitmap.createBitmap(star_width * 5, star_height, Bitmap.Config.ARGB_8888);// 创建一个底层画布
        Canvas cv = new Canvas(newb);
        for (int i = 0; i < maxGrade; i++) {
            if (i < grade && i + 1 > grade) // 画半星
            {
                cv.drawBitmap(half_star, star_width * i, 0, null);// 画图片的位置
            } else if (i < grade) // 画实心
            {
                cv.drawBitmap(normal_star, star_width * i, 0, null);// 画图片的位置
            } else
            // 画空心
            {
                cv.drawBitmap(empty_star, star_width * i, 0, null);// 画图片的位置
            }
        }
        // save all clip
        cv.save(Canvas.ALL_SAVE_FLAG);// 保存
        // store
        cv.restore();// 存储
        return newb;
    }

activity中调用
Bitmap bm = createStarBitmap(3.5f, 5);
imageview.setImageBitmap(bm);

上述代码展示的是通过右图的三张图片动态合成评分组件:


 
QQ截图20160628161121.png
3、图片圆角
    public Bitmap toRoundCorner(Bitmap bitmap, int pixels) {
        Bitmap roundCornerBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(roundCornerBitmap);
        int color = 0xff424242;// int color = 0xff424242;
        Paint paint = new Paint();
        paint.setColor(color);
        // 防止锯齿
        paint.setAntiAlias(true);
        Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        RectF rectF = new RectF(rect);
        float roundPx = pixels;
        // 相当于清屏
        canvas.drawARGB(0, 0, 0, 0);
        // 先画了一个带圆角的矩形
        canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        // 再把原来的bitmap画到现在的bitmap!!!注意这个理解
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return roundCornerBitmap;
    }
 
QQ截图20160628161823.png
4、将Bitmap转换成drawable

Drawable newBitmapDrawable = new BitmapDrawable(bitmap);
还可以从BitmapDrawable中获取Bitmap对象
Bitmap bitmap = new BitmapDrawable.getBitmap();

5、drawable转换成Bitmap
    public static Bitmap drawableToBitmap(Drawable drawable) {
        Bitmap bitmap = Bitmap.createBitmap(
                drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight(),
                drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565
        );
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        drawable.draw(canvas);
        return bitmap;
    }   
6、图片的放大和缩小
public Bitmap scaleMatrixImage(Bitmap oldbitmap, float scaleWidth, float scaleHeight) {
    Matrix matrix = new Matrix();
    matrix.postScale(scaleWidth,scaleHeight);// 放大缩小比例
    Bitmap ScaleBitmap = Bitmap.createBitmap(oldbitmap, 0, 0, oldbitmap.getWidth(), oldbitmap.getHeight(), matrix, true);
    return ScaleBitmap;
}
7、图片裁剪
public Bitmap cutImage(Bitmap bitmap, int reqWidth, int reqHeight) {
    Bitmap newBitmap = null;
    if (bitmap.getWidth() > reqWidth && bitmap.getHeight() > reqHeight) {
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, reqWidth, reqHeight);
    } else {
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight());
    }
    return bitmap;
}
8、图片保存到sd
    public void savePic(Bitmap bitmap,String path) {
        File file = new File(path);
        FileOutputStream fileOutputStream = null;
        try {
            file.createNewFile();
            fileOutputStream = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
5楼 

我们项目要求聊天发送图片要做图片优化超过2M压缩,,但是。。

我们是集成环信聊天,我选择做好了拍照压缩,选择相册的图片压缩还没做好,

不知道怎么把已经的到的本地路径变成Bitmap,然后传我还得把bitmap转换成路径,

哎,这都不是重点,重点是我压缩了传到环信服务器不是一张高清大图,那么对方接收到的也是模模糊糊的,这样好么?我想知道大家的是怎样

 环信的 Message,包含缩略图和高清图,默认加载的可以是缩略图,点击看大图加载原图

decodeFile和decodeResource间接调用decodeStream方法。
这个好像是4.3版本之后去掉了,性能下降了,所以得自己封装Stream性能好点

 

获取采样率遵循以下步骤中的第四条:应该是将inJustDecodeBounds参数设为false并重新加载图片

3.Bitmap

3.1 Recycle

【说明】

【1】Bitmap的内存的回收,分为两部分,需要对两部分都要回收;

【2】bitmap官网:recycle可以调用,调用之前确认图片不再使用,否则一旦清理,无法恢复,会报异常;

3.2 LRU算法

【说明】存储bitmap作为三级缓存使用的,LRU:最近最少使用的对象会从队列中清除;

什么是LRU】内部使用了LinkedHashMap,里面提供了get/post方法完成缓存的添加和获取操作;当缓存慢的时候会提供一个trimToSize方法,将较早的缓存移除,然后添加新的缓存;

3.3 计算inSampleSize 

【说明】下面是官网提供的设置SampleSize的方法

3.4 缩略图

【说明】必问:inJustDecodeBounds;

 

3.5 三级缓存

【说明】本地、内存、网络三级缓存;减少用户流量的使用和不必要的网络数据的加载;

什么是三级缓存?

  1. 内存缓存,优先加载,速度最快
  2. 本地缓存,次优先加载,速度快
  3. 网络缓存,最后加载,速度慢,浪费流量

为什么要进行三级缓存

三级缓存策略,最实在的意义就是 减少不必要的流量消耗,增加加载速度

如今的 APP 网络交互似乎已经必不可少,通过网络获取图片再正常不过了。但是,每次启动应用都要从网络获取图片,或者是想重复浏览一些图片的时候,每次浏览都需要网络获取,消耗的流量就多了,在如今的流量资费来说,肯定会容易影响用户数量。

还有就是网络加载图片,有时候会加载很慢,影响了用户体验。

另外从开发角度来说,Bitmap 的创建非常消耗时间和内存,可能导致频繁GC。而使用缓存策略,会更加高效地加载 Bitmap,减少卡顿,从而减少读取时间。

而内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,硬盘缓存则是防止应用重复从网络或其他地方重复下载和读取数据。

三级缓存的原理

  1. 首次加载的时候通过网络加载,获取图片,然后保存到内存和 SD 卡中。
  2. 之后运行 APP 时,优先访问内存中的图片缓存。
  3. 如果内存没有,则加载本地 SD 卡中的图片。

具体的缓存策略可以是这样的:内存作为一级缓存,本地作为二级缓存,网络加载为最后。

其中,内存使用 LruCache ,其内部通过 LinkedhashMap 来持有外界缓存对象的强引用;

对于本地缓存,使用 DiskLruCache。加载图片的时候,首先使用 LRU 方式进行寻找,找不到指定内容,按照三级缓存的方式,进行本地搜索,还没有就网络加载。

图片缓存代码实现

自己实现一个三级缓存的工具类并不困难。大概可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class BitmapUtil{
 
  //单例模式
 //···
  
 public void displayImage(ImageView img, String url){
   Bitmap bitmap;
   //内存缓存,url做唯一标识符
   bitmap = loadBitmapFromMemoryCache(url);
   if(bitmap != null){
     img.setImageBitmap(bitmap);
     return ;
   }
   //本地缓存
   bitmap = loadBitmapFromDiskCache(url);
   if(bitmap != null){
     img.setImageBitmap(bitmap);
     //然后将本地缓存保存到内存缓存中
     return ;
   }
   //网络缓存
   bitmap = loadBitmapFromNet(url);
   if(bitmap != null){
     img.setImageBitmap(bitmap);
     //同理将缓存保存到内存和本地中
     return;
   }
 }
  
}

详细不说了,网上有很多类似的文章可以参考。

关于内存缓存的实现核心基本就是获取APP最大内存,然后set的时候用 LruCache< url , bitmap> put 进去。他会按照最近最少使用的算法将内存控制在一定大小内,超出的时候自动回收。

还有一点注意的是,一般url作为 key 的时候,会用MD5算法处理一下,最后是用其 MD5 值作为key的,这可能是为了避免一些特殊字符影响使用。

关于Glide的缓存

事实上,现在已经很少自己封装一个三级缓存策略,在众多的图片框架中都加入缓存策略,实现起来更简单。这里以 Glide 为例。

Glide 的使用基本就是一行代码就解决了。像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 加载本地图片
File file = new File(getExternalCacheDir() + "/image.jpg");
Glide.with(this).load(file).into(imageView);
 
// 加载应用资源
int resource = R.drawable.image;
Glide.with(this).load(resource).into(imageView);
 
// 加载二进制流
byte[] image = getImageBytes();
Glide.with(this).load(image).into(imageView);
 
// 加载Uri对象
Uri imageUri = getImageUri();
Glide.with(this).load(imageUri).into(imageView);

当然应用到项目里面最好二次封装一下。这些不是这次文章的主题。我们回到缓存上面来。

Glide 的内存缓存

Glide 是默认开启了内存缓存的,只要你通过 Glide 加载一张图片,他就会缓存到内存中,只要他还没被从内存中清理之前,下次使用 Glide 都会从内存缓存中加载。大大提升了图片加载的效率。

当然如果你有特殊要求,可以添加一行代码把默认开启的内存缓存关闭掉。

1
2
3
4
Glide.with(this)
   .load(url)
   .skipMemoryCache(true)//关闭内存缓存
   .into(imageView);

Glide 的内存缓存实际上和我们上面说的差别不大,使用的也是LruCache算法,不过他还结合了一种弱引用机制,共同完成了内存缓存功能。

Glide 的硬盘缓存

关于 Glide 硬盘缓存使用也是十分简单。

1
2
3
4
Glide.with(this)
   .load(url)
   .diskCacheStrategy(DiskCacheStrategy.RESULT)
   .into(imageView);

一个 diskCacheStrategy( ) 方法就可以调整他的硬盘缓存策略。其中可以传入的参数有四种:

  1. DiskCacheStrategy.NONE: 表示不缓存任何内容。
  2. DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
  3. DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。
  4. DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

Glide 的硬盘缓存是默认将图片压缩转换后再缓存到硬盘中,这种处理方式再避免OOM的时候会经常看见。

如果需要改变硬盘缓存策略只需要改变其传入的参数即可。

面试题-bitmap使用的注意事项

Bitmap使用的时候主要从下面几个方面优化
【1】三级缓存使用:内存缓存+文件缓存 这个应该不能算是内存方面的优化,应该算性能上面的优化。
【2】及时释放Bitmap的内存:正如上面说到的Android 3.0之前有部分内存是分配在native上的,必须手动去释放。
【3】复用内存:BitmapFactory.Options 参数inBitmap的使用。inMutable设置为true,并且配合SoftReference软引用使用(内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些软引用对象的内存)。
【4】有一点要注意Android4.4以下的平台,需要保证inBitmap和即将要得到decode的Bitmap的尺寸规格一致,Android4.4及其以上的平台,只需要满足inBitmap的尺寸大于要decode得到的Bitmap的尺寸规格即可。 【5】降低采样率,必问:inJustDecodeBounds,BitmapFactory.Options 参数inSampleSize的使用,从而减少内存的使用。

 

 4.UI卡顿

 4.1 UI卡顿的原理

 

 

UI界面的渲染要在16ms内完成;

虚拟机在gc的时候,会暂停所有线程的运行,如果刚好界面在滑动的时候遇到了gc,则卡顿;

所以放置内存的抖动;

【出现卡顿原因】主要原因来自与Android的渲染时做了太多的耗时操作

【太多耗时的原因】:可能是UI布局太复杂;可能是UI布局层叠了太多的布局项;动画执行次数过多; 

 【overDraw】屏幕上同一帧的数据被绘制了很多次,经常出现在多层次的UI结构中;

 解决UI卡顿的问题:可以使用手机当中的开发者选项当中的GPU选项,有蓝色、淡绿色、深红色;减少红色,尽量出现蓝色;

4.2 UI卡顿的原因

【注意】background的设置和子view的设置一定要谨慎谨慎再谨慎;

 

4.3 UI卡顿的总结

【merge标签】

每个View都会经历这三个步骤,如果这个View有子View的话,在onMeasure onlayout ondraw 这三个方法处理的方式 基本都是相同的,先是遍历子View树,然后测量、layout、draw。每一步测量都是耗时的,view树越深,耗时越多,所以,在我们在书写xml布局的时候,尽量要减少多余层级的嵌套。这时候,merge标签就很有用了。

我们先看一个简单的例子,因为可能很多人也都有写过。<b>自定义组合View</b>

public class MyView extends RelativeLayout {
    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initViews();
    }

    private void initViews() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_myview, this);
    }
}

然后布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
        android:id="@+id/first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:text="我爱稀土掘金"/>


    <TextView
        android:layout_below="@+id/first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我爱稀土掘金"/>

</RelativeLayout>

运行结果

 
Paste_Image.png

也是so easy

OK,你可能会问了,这有什么东西吗?
我们来看一下View的层级

 
Paste_Image.png

没错,多了一层,也就是MyView明明可以很好的承载两个TextView,为什么还要多一个ViewGroup承载呢?我们作为一名开发者,不能容忍任何性能的消耗,所以这种情况,我们可能需要使用merge标签

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">

<TextView
    android:id="@+id/first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:text="我爱稀土掘金"/>


<TextView
    android:layout_below="@+id/first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我爱稀土掘金"/>

</merge>

OK在运行一下

 
Paste_Image.png
 
Paste_Image.png

成功消去那一层,胜利了。
那是不是什么情况都可以这么搞呢?其实肯定不是的,比如说

public class MyView extends LinearLayout {
    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initViews();
    }

    private void initViews() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_myview, this);
    }
}

我将相对布局换成线性布局了。然后看结果

 
Paste_Image.png

不一样了哦,这是为什么?我们只能从源码入手,来解释这个问题。由于已经有大神写的非常好了,所以我就不班门弄斧了。
我推荐一篇博客给大家<a href='http://blog.csdn.net/bboyfeiyu/article/details/45869393'>源码解析</a>
关键源码

         final View view = createViewFromTag(parent, name, attrs);  
               // 获取merge标签的parent  
               final ViewGroup viewGroup = (ViewGroup) parent;  
               // 获取布局参数  
               final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
               // 递归解析每个子元素  
               rInflate(parser, view, attrs, true);  
               // 将子元素直接添加到merge标签的parent view中  
               viewGroup.addView(view, params);  

原因就是,其实我们使用merge标签后,Layoutinflater解析遇到merge标签,直接解析成View,然后把解析出来的属性通过generateLayoutParams方法,变成我们熟悉的params,然后再添加到merge父容器里面去。父容器,在我们这里对应着MyView,MyView也就是LinearLayout,所以才会出现上面那样的效果,虽然,我们在merge标签里面写了below,但是LinearLayout是无法识别这个属性,还是按照默认的横向走向布局2个子View,导致出现了以上的情况,所以,使用merge还是有很大的局限性的.

我们要注意。要<b>自定义组合View会增加一层层级,所以,如果可以的话,我们要加上merge标签。而且保证我们在merge标签里面写的一些属性,要可以被merge标签的父容器识别才可以。</b>

作者:suwanroy
链接:https://www.jianshu.com/p/fbcc6a17d11e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

ViewStub用法
在开发应用程序的时候,经常会遇到这样的情况,会在运行时动态根据条件来决定显示哪个View或某个布局。那么最通常的想法就是把可能用到的View都写在上面,
先把它们的可见性都设为View.GONE,然后在代码中动态的更改它的可见性。这样的做法的优点是逻辑简单而且控制起来比较灵活。
但是它的缺点就是,耗费资源。虽然把View的初始可见View.GONE但是在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化,会被设置属性。也就是说,会耗费内存等资源。 推荐的做法是使用android.view.ViewStub,ViewStub 是一个轻量级的View,它一个看不见的,不占布局位置,占用资源非常小的控件。
可以为ViewStub指定一个布局,在Inflate布局的时候,只有 ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,V
iewStub所向 的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。这样,就可以使用ViewStub来方便的在运行时,要还 是不要显示某个布局。 但ViewStub也不是万能的,下面总结下ViewStub能做的事儿和什么时候该用ViewStub,什么时候该用可见性的控制。 首先来说说ViewStub的一些特点:
1. ViewStub只能Inflate一次,之后ViewStub对象会被置为空。按句话说,某个被ViewStub指定的布局被Inflate后,就不会够再通过ViewStub来控制它了。 2. ViewStub只能用来Inflate一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。 基于以上的特点,那么可以考虑使用ViewStub的情况有: 1. 在程序的运行期间,某个布局在Inflate后,就不会有变化,除非重新启动。 因为ViewStub只能Inflate一次,之后会被置空,所以无法指望后面接着使用ViewStub来控制布局。所以当需要在运行时不止一次的显示和 隐藏某个布局,那么ViewStub是做不到的。这时就只能使用View的可见性来控制了。 2. 想要控制显示与隐藏的是一个布局文件,而非某个View。 因为设置给ViewStub的只能是某个布局文件的Id,所以无法让它来控制某个View。 所以,如果想要控制某个View(如Button或TextView)的显示与隐藏,或者想要在运行时不断的显示与隐藏某个布局或View,只能使用View的可见性来控制。 下面来看一个实例 在这个例子中,要显示二种不同的布局,一个是用TextView显示一段文字,另一个则是用ImageView显示一个图片。
这二个是在onCreate()时决定是显示哪一个,这里就是应用ViewStub的最佳地点。 先来看看布局,一个是主布局,里面只定义二个ViewStub,一个用来控制TextView一个用来控制ImageView,
另外就是一个是为显示文字的做的TextView布局,一个是为ImageView而做的布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center_horizontal"> <ViewStub --显示文字 android:id="@+id/viewstub_demo_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_marginTop="10dip" android:layout="@layout/viewstub_demo_text_layout"/> --分别引用了textView的布局 <ViewStub --显示图片 android:id="@+id/viewstub_demo_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout="@layout/viewstub_demo_image_layout"/> --分别引用了image的布局 </LinearLayout> 为TextView的布局: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/viewstub_demo_textview" android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#aa664411" android:textSize="16sp"/> </LinearLayout> 为ImageView的布局: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/viewstub_demo_imageview" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout> 下面来看代码,决定来显示哪一个,只需要找到相应的ViewStub然后调用其infalte()就可以获得相应想要的布局: package com.effective; import android.app.Activity; import android.os.Bundle; import android.view.ViewStub; import android.widget.ImageView; import android.widget.TextView; public class ViewStubDemoActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.viewstub_demo_activity); if ((((int) (Math.random() * 100)) & 0x01) == 0) { // to show text // all you have to do is inflate the ViewStub for textview ViewStub stub = (ViewStub) findViewById(R.id.viewstub_demo_text); stub.inflate(); TextView text = (TextView) findViewById(R.id.viewstub_demo_textview); text.setText("The tree of liberty must be refreshed from time to time" + " with the blood of patroits and tyrants! Freedom is nothing but " + "a chance to be better!"); } else { // to show image // all you have to do is inflate the ViewStub for imageview ViewStub stub = (ViewStub) findViewById(R.id.viewstub_demo_image); stub.inflate(); ImageView image = (ImageView) findViewById(R.id.viewstub_demo_imageview); image.setImageResource(R.drawable.happy_running_dog); } } } 运行结果:

使用的时候的注意事项:

1. 某些布局属性要加在ViewStub而不是实际的布局上面,才会起作用,比如上面用的android:layout_margin*系列属性,如果加在 TextView上面,则不会起作用,需要放在它的ViewStub上面才会起作用。而ViewStub的属性在inflate()后会都传给相应的布 局。

1.ViewStub之所以常称之为“延迟化加载”,是因为在较多数情况下,程序无需显示ViewStub所指向的布局文件,只有在特定的某些较少条件下,此时ViewStub所指向的布局文件才需要被inflate,且此布局文件直 接将当前ViewStub替换掉,具体是通过viewStub.infalte()或 viewStub.setVisibility(View.VISIBLE)来完成;

2.正确把握住ViewStub的应用场景非常重要,正如如1中所描述需求场景下,使用ViewStub可以优化布局;

3.对ViewStub的inflate操作只能进行一次,因为inflate的 时候是将其指向的布局文件解析inflate并替换掉当前ViewStub本身(由此体现出了ViewStub“占位符”性质),一旦替换后,此时原来的 布局文件中就没有ViewStub控件了,
因此,如果多次对ViewStub进行infalte,会出现错误信息:ViewStub must have a non-null ViewGroup viewParent。 4.3中所讲到的ViewStub指向的布局文件解析inflate并替换掉当前 ViewStub本身,并不是完全意义上的替换(与include标签还不太一样),替换时,布局文件的layout params是以ViewStub为准,其他布局属性是以布局文件自身为准。 5.ViewStub本身是不可见的,对 ViewStub setVisibility(..)与其他控件不一样,ViewStub的setVisibility 成View.VISIBLE或INVISIBLE如果是首次使用,都会自动inflate其指向的布局文件,并替换ViewStub本身,再次使用则是相 当于对其指向的布局文件设置可见性。 viewStub中加载layout的代码是 [html] view plain copy 在CODE上查看代码片派生到我的代码片 public View inflate() { final ViewParent viewParent = getParent();// 获取当前view的父view,用于获取需要加载的layout的index if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { final ViewGroup parent = (ViewGroup) viewParent; final LayoutInflater factory; if (mInflater != null) { factory = mInflater; } else { factory = LayoutInflater.from(mContext); } final View view = factory.<strong><span style="font-size:14px;color:#ff0000;">inflate</span></strong>(mLayoutResource, parent, false);// 获取需要加载的layout if (mInflatedId != NO_ID) { view.setId(mInflatedId); } final int index = parent.indexOfChild(this); parent.removeViewInLayout(this);// 删除之前加载的view final ViewGroup.LayoutParams layoutParams = getLayoutParams(); if (layoutParams != null) { parent.addView(view, index, layoutParams); } else { parent.<strong><span style="font-size:14px;color:#ff0000;">addView</span></strong>(view, index);// 添加view } mInflatedViewRef = new WeakReference<View>(view); if (mInflateListener != null) { mInflateListener.onInflate(this, view); } return view; } else { throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } else { throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent"); } } 作者: 一点点征服 出处:http://www.cnblogs.com/ldq2016/

 

使用ViewStub来提高加载性能吧!
2016年12月08日 12:49:57【原文地址】https://blog.csdn.net/cx1229/article/details/53505903
什么是ViewStub?
ViewStub其实本质上也是一个View,其继承关系如图所示: 
ViewStub继承关系

为什么ViewStub可以提高加载性能?
ViewStub使用的是惰性加载的方式,即使将其放置于布局文件中,如果没有进行加载那就为空,不像其它控件一样只要布局文件中声明就会存在。 
那ViewStub适用于场景呢?通常用于网络请求页面失败的显示。一般情况下若要实现一个网络请求失败的页面,我们是不是使用两个View呢,一个隐藏,一个显示。试想一下,
如果网络状况良好,并不需要加载失败页面,但是此页面确确实实已经加载完了,无非只是隐藏看不见而已。如果使用ViewStub,在需要的时候才进行加载,不就达到节约内存提高性能的目的了吗?

ViewStub的加载原理 ViewStub只能加载一次,重复加载会导致异常,这是因为ViewStub只要加载过一次,其自身就会被移除,把并自身所包含的内容全部传给父布局。来张图感受一下 ViewStub的加载原理

ViewStub如何使用
父布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.example.administrator.myviewstub.MainActivity">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="inflate"
        android:text="inflate" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="setData"
        android:text="setdata"/>
    <ViewStub
        android:id="@+id/vs"
        android:layout="@layout/viewstub"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
可以看到ViewStub又引用了另外一个布局。

ViewStub布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/hello_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="DATA EMPTY!"/>
</FrameLayout>
MainActivity
public class MainActivity extends AppCompatActivity {
    private ViewStub viewStub;
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewStub = (ViewStub) findViewById(R.id.vs);
        //textView  = (TextView) findViewById(R.id.hello_tv);空指针
    }
    public  void inflate(View view){
        viewStub.inflate();
        //textView  = (TextView) viewStub.findViewById(R.id.hello_tv);空指针
        textView  = (TextView) findViewById(R.id.hello_tv);
    }
    public void setData(View view){
        textView.setText("DATA!!");

    }

注意:这里有几个坑,前面我们说了ViewStub只有在初始化之后才会存在,所以第一个注释中的空指针是因为ViewStub还未初始化。那第二个注释中的空指针是怎么回事呢?
前面我们也说了,ViewStub在加载完成之后会移除自身,并把自身的内容转给父布局,所以此时viewStub中的内容已经不存在了,textView已经是父布局的东西了,所以不能使用viewStub来findViewById。另外,前面我们说了ViewStub只能加载一次,若调用两次inflate()的话会导致异常。 加载异常
为了验证所得出的结论是不是正确的,我截了两张ViewStub加载前后的图。

加载前: 
加载前
从图中可以看到ViewStub还没加载,是灰色的。
加载后: 
加载后
当点击了INFLATE之后,可以看到,ViewStub消失了,取而代之的是一个FrameLayout,其中包含了一个AppCompatTextView(ps.其实就是TextView,只是Google在5.0之后提供了向前兼容,就好比AppCompatActivity和Activity一样)。咳,这个FrameLayout是不是很眼熟,没错!就是我们之前写的ViewStub的布局,忘记的童鞋翻回去看看。

顺便验证一下TextView是不是能用,点击SETDATA: 
这里写图片描述
没有问题。

关于这个图是怎么来的,童鞋们只要点击Android Montior中的Layout Inspector就行啦,就是介个: 
Layout Inspector 
感兴趣的童鞋可以自己去试试。

源码分析
ViewStub是如何实现的呢,接下来我们来一探究竟:

 public View inflate() {
        final ViewParent viewParent = getParent();//获取父View

        if (viewParent != null && viewParent instanceof ViewGroup) {
        //若父不是ViewGroup就会抛出异常("ViewStub must have a non-null ViewGroup viewParent")
            if (mLayoutResource != 0) {
            //这个就是ViewStub只能加载一次的原因,第二次加载则抛出异常(throw new IllegalArgumentException("ViewStub must have a valid layoutResource"))
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(mContext);
                }
                final View view = factory.inflate(mLayoutResource, parent,
                        false);
                //创建一个View,这个View为ViewStub的内容,mLayoutResource为ViewStub自身的Layout资源文件id
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId);
                    //若mInflatedId存在,则将id重新赋值给新的View
                }

                final int index = parent.indexOfChild(this);
                parent.removeViewInLayout(this);
                //通过父布局将ViewStub移除
                final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                if (layoutParams != null) {
                    parent.addView(view, index, layoutParams);
                } else {
                    parent.addView(view, index);
                }
                //将ViewStub中的内容添加到父容器中
                mInflatedViewRef = new WeakReference<View>(view);
                //设置为弱引用,当VIewStub设置为空时将立即执行GC
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }

                return view;
                //最后将View返回出去
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }
ViewStub中还有一个setVisibility(int visibility)值得我们注意:

public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            //拿到关联的Layout
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

可以看到,ViewStub若之前没有进行inflate,setVisibility(View.VISIBLE)或setVisibility(View.INVISIBLE)会自动先进行加载,
而加载之后可以设置显示和隐藏。并且ViewStub设置的不是自己,而是拿到关联的那个Layout设置visible。ViewStub此时并未销毁,所以建议初始化后将其设置为空。

 

5.内存泄露

【内存泄露】生成的实例一致被持有,使用结束后无法被gc;

5.1 java的内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

  • 栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

栈与堆的区别:

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。

当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。

mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。

Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。

另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。

在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),

那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。以下,我们举一个例子说明如何用有向图表示内存管理。

对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,

那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,

例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

 

什么是Java中的内存泄露

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,

首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;

其次,这些对象是无用的,即程序以后不会再使用这些对象。

如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。

在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。

通过这种方式,Java提高了编程的效率。

因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。

因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

同样给出一个 Java 内存泄漏的典型例子,

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

 

5.3 内存泄露

【单例引起的内存泄露】

 

 【匿名内部类引起的内存泄露】正确的写法:将内部类写为static的静态内部类;这样,静态内部类就不会持有外部类的实例,不会导致外部实例无法正常的回收;

 

【handler的内存泄露】正确写法如下

【尽量避免使用static成员变量】

【资源的内存泄露】socket的关闭;io流的关闭;数据库course的关闭等等;

【AsyncTask内存泄露】在onDestory中增加移除消息的方法;

 

【bitmap】使用结束之后需要调动recycle方法,回收c端的内存实例;

【listView的内存泄露】

 6.android内存管理

 6.1 内存管理机制概述

 【说明】操作系统会为每个应用合理的分配内存资源,

 【内存的分配机制】操作系统会为每个应用分配合理的内存大小,从而保证每个应用能够正常的运行;

 不至于内存不够使用或者每个应用进程使用太多的内存;

【内存的回收机制】在内存不足的时候会存在内存的合理回收和内存资源再回收的机制;

 从而保证新的进程可以正常的运行,杀死正在占有内存的进程;

操作系统需要提供一个合理的杀死进程的机制,从而保证副作用降低最低;

6.2 android中的内存管理机制

 【内存分配机制】android为每个应用分配内存时候使用了弹性机制,  开始不会为app分配太多的内存;当app运行起来之后,

再根据情况分配额外的内存;注意:并非随意分配内存的大小;

是为了让更多的进程存活在内存中, 当下次app启动的时候就需要再次重新启动进程,只要回复已有的进程即可;减少了应用启动的时间,提高了用户的体验;

【android的回收机制】android的内存的使用是尽最大的使用内存资源;

在内存不足的时候会存在内存的合理回收和内存资源再回收的机制;

 从而保证新的进程可以正常的运行,杀死正在占有内存的进程;

内存的分配具有优先级的概念,主要分为5个级别:前台进程、可见进程、服务进程(消息的推送、开启定位等等)、后台进程(后台进程计算)、空进程;

前3个进程在系统一般是不会被杀死的;

后台进程会存放在缓存列表LRU中,先杀死的进程处于列的尾部,

【回收效率】系统在杀死进程之前,会判断杀死进程带来的回收效率;系统倾向于杀死一个能够回收更多内存资源的进程;

希望能够杀死的进程越少,能够保留的数据越多,app启动加载的效率越高;

 6.3 内存管理机制的特点

 【说明】在app开发的过程中,会给app定内存目标:

6.4 内存优化的方法

 

 

 内存泄露第三方的框架;eclipse MAT内存泄露分析工具 分析程序运行时的内存的印象文件,查找到内存泄露的代码;

 6.5 参考文章:Android工具:LeakCanary—内存泄露检测神器

一、LeakCanary简介
LeakCanary是Square公司开源的一个检测内存的泄露的函数库,可以方便地和你的项目进行集成,在Debug版本中监控Activity、Fragment等的内存泄露;
LeakCanary集成到项目中之后,在检测到内存泄露时,会发送消息到系统通知栏。点击后打开名称DisplayLeakActivity的页面,并显示泄露的跟踪信息,Logcat上面也会有对应的日志输出。同时如果跟踪信息不足以定位时,DisplayLeakActivity还为开发者默认保存了最近7个dump文件到App的目录中,可以使用MAT等工具对dump文件进行进一步的分析;
二、内存泄漏简介
在了解了LeakCanary的接入方式后,我们肯定着急想见识见识LeakCanary的威力。在跟大家演示LeakCanary检测和处理构成之前,大家应该明应该对内存泄露有基本的了解和认识;
1.为什么会产生内存泄漏?
当一个对象不需要使用本该回收时,有另外一个正在使用的对象持有它的引用,从而导致它不能回收停留在堆内存中,这就产生了内存泄漏;
2.内存泄露对程序产生的影响?
内存泄漏是造成应用程序OOM的主要原因之一。Android系统为每个应用程序分配有限的内存,当应用中内存泄漏较多时,就难免会导致应用所需要的内存超出系统分配限额,从而导致OOM应用Crash;
3.Android常见的内存泄露?
相信内存泄露对大家都早有耳闻,但是它不像一些Java异常情况,会立即造成程序的Crash,却有让大家比较“陌生”。下面我们就列举出日常开发中常见的内存泄露类型,让大家对内存泄露的认识不仅仅停留在“有所耳闻 ”的层面;
 单例造成:由于单例静态特性使得单例的生命周期和应用的生命周期一样长,如果一个对象(如Context)已经不使用了,而单例对象还持有对象的引用造成这个对象不能正常被回收;
 非静态内部类创建静态实例造成:在Acitivity内存创建一个非静态内部类单例,避免每次启动资源重新创建。但是因为非静态内部类默认持有外部类(Activity)的引用,并且使用该类创建静态实例。造成该实例和应用生命周期一样长,导致静态实例持有引用的Activity和资源不能正常回收;
 Handler造成:子线程执行网络任务,使用Handler处理子线程发送消息。由于handler对象是非静态匿名内部类的对象,持有外部类(Activity)的引用。在Handler-Message中Looper线程不断轮询处理消息,当Activity退出还有未处理或者正在处理的消息时,消息队列中的消息持有handler对象引用,handler又持有Activity,导致Activity的内存和资源不能及时回收;
 线程造成:匿名内部类Runnalbe和AsyncTask对象执行异步任务,对当前Activity隐式引用。当Activity销毁之前,任务还没有执行完,将导致Activity的内存和资源不能及时回收;
 资源未关闭造成的内存泄露:对于使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄露;
三、LeakCanary接入
下面我们还是以QProject项目进行演示如何在项目中接入LeakCanary,项目目录如下:

1.添加LeakCanary依赖
在主项目main模块的build.gradle文件中添加LeakCanary相关依赖;
/main/build.gradle文件

[plain] view plain copy
 
  1. apply plugin: 'com.android.application'  
  2. android {  
  3.     ...  ...  
  4. }  
  5. dependencies {  
  6.     ... ...  
  7.     //添加leakcanary相关的依赖  
  8.     //在release和test版本中,使用的是LeakCanary的no-op版本,也就是没有实际代码和操作的Wrapper版本,只包含LeakCanary和RefWatcher类的空实现,这样不会对生成的APK包体积和应用性能造成影响  
  9.     debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'  
  10.     releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'  
  11.     testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'  
  12.     ... ...  
  13.     compile project(':test')  
  14. }  

2.初始化LeakCanary
在主项目main模块的QApplication的onCreate()方法中初始化LeakCanary;
/main/src/main/java/com/qproject/main/QApplication.java文件

[java] view plain copy
 
  1. public class QAplication extends Application{  
  2.     @Override  
  3.     public void onCreate() {  
  4.         super.onCreate();  
  5.         ... ...  
  6.         //初始化LeakCanary  
  7.         if (LeakCanary.isInAnalyzerProcess(this)) {  
  8.             return;  
  9.         }  
  10.         LeakCanary.install(this);  
  11.     }  
  12. }  

OK,到这里我们就完成了一个项目的LeankCanary的简单接入;
提示1:集成LeakCanary后,构建和安装apk的时候报错如下:
Error:Error converting bytecode to dex:
Cause: com.android.dex.DexException: Multiple dex files define Lcom/squareup/leakcanary/LeakCanary;
Error:Execution failed for task ':main:transformClassesWithDexForDebug'.
> com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: java.util.concurrent.ExecutionException: java.lang.UnsupportedOperationException
处理1:添加依赖debugCompile 'com.squareup.haha:haha:2.0.3',修改依赖的版本为1.4-beta2;
四、LeakCanary检测
通过对一些常见的内存泄露的学习,我们已经对内存泄露有所见闻了。那么下面我们通过LeakCanary工具,让大家感受下它在你的日常开发中的真实存在。我们以常见内存—单例造成的内存泄露为例进行实践;
1.单例内存泄露模拟
test/src/main/com/qproject/test/TestManager.java

[java] view plain copy
 
  1. public class TestManager {  
  2.     //单例静态特性使得单例的生命周期和应用的生命周期一样长  
  3.     private static TestManager instance;  
  4.     private Context context;  
  5.   
  6.     /** 
  7.      * 传入的Context的生命周期很重要: 
  8.      *   如果传入的是Application的Context,则生命周期和单例生命周期一样长; 
  9.      *   如果传入的是Activity的Context,由于该Context和Activity的生命周期一样长,当Activity退出的时候它的内存不会被回收,因为单例对象持有它的引用; 
  10.      */  
  11.     private TestManager(Context context) {  
  12.         this.context = context;  
  13.     }  
  14.   
  15.     public static TestManager getInstance(Context context) {  
  16.         if (instance == null) {  
  17.             instance = new TestManager(context);  
  18.         }  
  19.         return instance;  
  20.     }  
  21. }  

test/src/main/com/qproject/test/leakcanary/LeakCanaryActivity.java

[java] view plain copy
 
  1. public class LeakCanaryActivity extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_leakcanary);  
  7.         //获取单例对象,退出Activity即可模拟出内存泄露  
  8.         TestManager testManager = TestManager.getInstance(this);  
  9.     }  
  10. }  

2.检测消息通知
运行App到LeakCanaryActivit页面并退出,在检测到内存泄露的时候,会发送消息到系统通知栏;
 
3.查看检测详情
点击通知消息,打开名为DisplayLeakActivity的页面,并显示泄漏的跟踪信息;

4.查看LogCat日志
除了以上泄漏信息的显示,Logcat上面也会有对应的日志输出;

[plain] view plain copy
 
  1. //内存泄露对象com.qproject.test.leakcanary.LeakCanaryActivity  
  2. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * com.qproject.test.leakcanary.LeakCanaryActivity has leaked:  
  3. //static com.qproject.test.TestManager.instance的com.qproject.test.TestManager.context引用了回收的内存对象  
  4. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * GC ROOT static com.qproject.test.TestManager.instance  
  5. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * references com.qproject.test.TestManager.context  
  6. //内存泄露对象大小,Reference Key,Device和Android Version等信息  
  7. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * leaks com.qproject.test.leakcanary.LeakCanaryActivity instance  
  8. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Retaining: 46 KB.  
  9. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Reference Key: 3d74d294-70dc-4447-a9a2-64e656ea86b8  
  10. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Device: Genymotion Android PREVIEW - Google Nexus 5X - 7.0.0 - API 24 - 1080x1920 vbox86p  
  11. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Android Version: 7.0 API: 24 LeakCanary: 1.5 00f37f5  
  12. 12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Durations: watch=5038ms, gc=137ms, heap dump=2390ms, analysis=27325ms  
  13. //内存泄露对象详细信息  
  14. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Details:  
  15. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Class com.qproject.test.TestManager  
  16. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static instance = com.qproject.test.TestManager@316184816 (0x12d898f0)  
  17. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static $classOverhead = byte[308]@316175745 (0x12d87581)  
  18. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Instance of com.qproject.test.TestManager  
  19. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static instance = com.qproject.test.TestManager@316184816 (0x12d898f0)  
  20. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static $classOverhead = byte[308]@316175745 (0x12d87581)  
  21. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   context = com.qproject.test.leakcanary.LeakCanaryActivity@315059712 (0x12c76e00)  
  22. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_klass_ = com.qproject.test.TestManager  
  23. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_monitor_ = 0  
  24. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Instance of com.qproject.test.leakcanary.LeakCanaryActivity  
  25. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   static $classOverhead = byte[2228]@316203009 (0x12d8e001)  
  26. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mDelegate = android.support.v7.app.AppCompatDelegateImplN@315842128 (0x12d35e50)  
  27. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mEatKeyUpEvent = false  
  28. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mResources = null  
  29. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mThemeId = 2131230884  
  30. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mCreated = true  
  31. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mFragments = android.support.v4.app.FragmentController@316183584 (0x12d89420)  
  32. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mHandler = android.support.v4.app.FragmentActivity$1@316163360 (0x12d84520)  
  33. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mNextCandidateRequestIndex = 0  
  34. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mOptionsMenuInvalidated = false  
  35. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mPendingFragmentActivityResults = android.support.v4.util.SparseArrayCompat@316172368 (0x12d86850)  
  36. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mReallyStopped = true  
  37. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mRequestedPermissionsFromFragment = false  
  38. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mResumed = false  
  39. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mRetaining = false  
  40. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mStopped = true  
  41. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mStartedActivityFromFragment = false  
  42. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mStartedIntentSenderFromFragment = false  
  43. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mExtraDataMap = android.support.v4.util.SimpleArrayMap@316171864 (0x12d86658)  
  44. 12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: |   mActionBar = null  
  45. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mActionModeTypeStarting = 0  
  46. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mActivityInfo = android.content.pm.ActivityInfo@315841984 (0x12d35dc0)  
  47. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mActivityTransitionState = android.app.ActivityTransitionState@316207336 (0x12d8f0e8)  
  48. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mApplication = com.qproject.main.QAplication@314916416 (0x12c53e40)  
  49. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mCalled = true  
  50. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mChangeCanvasToTranslucent = false  
  51. 12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: |   mChangingConfigurations = false  
  52. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mComponent = android.content.ComponentName@315998320 (0x12d5c070)  
  53. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mConfigChangeFlags = 0  
  54. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mCurrentConfig = android.content.res.Configuration@316178888 (0x12d881c8)  
  55. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDecor = null  
  56. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDefaultKeyMode = 0  
  57. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDefaultKeySsb = null  
  58. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDestroyed = true  
  59. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mDoReportFullyDrawn = false  
  60. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEatKeyUpEvent = false  
  61. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEmbeddedID = null  
  62. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEnableDefaultActionBarUp = false  
  63. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mEnterTransitionListener = android.app.SharedElementCallback$1@1887062680 (0x707a4a98)  
  64. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mExitTransitionListener = android.app.SharedElementCallback$1@1887062680 (0x707a4a98)  
  65. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mFinished = true  
  66. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mFragments = android.app.FragmentController@316183536 (0x12d893f0)  
  67. 12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: |   mHandler = android.os.Handler@316163296 (0x12d844e0)  
  68. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mHasCurrentPermissionsRequest = false  
  69. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mIdent = 20356640  
  70. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mInstanceTracker = android.os.StrictMode$InstanceTracker@316183552 (0x12d89400)  
  71. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mInstrumentation = android.app.Instrumentation@314950632 (0x12c5c3e8)  
  72. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mIntent = android.content.Intent@316215416 (0x12d91078)  
  73. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mLastNonConfigurationInstances = null  
  74. 12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: |   mMainThread = android.app.ActivityThread@314966272 (0x12c60100)  
  75. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mManagedCursors = java.util.ArrayList@316171816 (0x12d86628)  
  76. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mManagedDialogs = null  
  77. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mMenuInflater = null  
  78. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mParent = null  
  79. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mReferrer = java.lang.String@316215864 (0x12d91238)  
  80. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mResultCode = 0  
  81. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mResultData = null  
  82. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mResumed = false  
  83. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mSearchEvent = null  
  84. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mSearchManager = null  
  85. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mStartedActivity = false  
  86. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mStopped = true  
  87. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTaskDescription = android.app.ActivityManager$TaskDescription@316163328 (0x12d84500)  
  88. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTemporaryPause = false  
  89. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTitle = java.lang.String@315129824 (0x12c87fe0)  
  90. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTitleColor = 0  
  91. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTitleReady = true  
  92. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mToken = android.os.BinderProxy@315867328 (0x12d3c0c0)  
  93. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mTranslucentCallback = null  
  94. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mUiThread = java.lang.Thread@1959751680 (0x74cf7000)  
  95. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVisibleBehind = false  
  96. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVisibleFromClient = true  
  97. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVisibleFromServer = true  
  98. 12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: |   mVoiceInteractor = null  
  99. 12-25 07:50:51.716 4941-5795/com.qproject.main D/LeakCanary: |   mWindow = com.android.internal.policy.PhoneWindow@315116864 (0x12c84d40)  
  100. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mWindowAdded = true  
  101. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mWindowManager = android.view.WindowManagerImpl@316172152 (0x12d86778)  
  102. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mInflater = com.android.internal.policy.PhoneLayoutInflater@316010352 (0x12d5ef70)  
  103. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mOverrideConfiguration = null  
  104. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mResources = android.content.res.Resources@316235992 (0x12d960d8)  
  105. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mTheme = android.content.res.Resources$Theme@316183728 (0x12d894b0)  
  106. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mThemeResource = 2131230884  
  107. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   mBase = android.app.ContextImpl@316155392 (0x12d82600)  
  108. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_klass_ = com.qproject.test.leakcanary.LeakCanaryActivity  
  109. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: |   shadow$_monitor_ = 1316364430  
  110. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: * Excluded Refs:  
  111. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)  
  112. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:FinalizerWatchdogDaemon (always)  
  113. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:main (always)  
  114. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:LeakCanary-Heap-Dump (always)  
  115. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.WeakReference (always)  
  116. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.SoftReference (always)  
  117. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.PhantomReference (always)  
  118. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.Finalizer (always)  
  119. 12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.FinalizerReference (always)  

5.获取dump日志文件
如果觉得跟踪信息不足以定位时,DisplayLeakActivity还为开发者默认保存了最近7个dump文件到APP的目录中,可以使用MAT等工具对dump文件进行进一步的分析;

[plain] view plain copy
 
  1. vbox86p:/data/data/com.qproject.main/files/leakcanary # ls  
  2. 2016-12-25_07-50-51_718.hprof 2016-12-25_07-50-51_718.hprof.result  
  3. D:\>adb pull ./data/data/com.qproject.main/files/leakcanary/2016-12-25_07-50-51_ 718.hprof  
  4. [100%] ./data/data/com.qproject.main/f...akcanary/2016-12-25_07-50-51_718.hprof  

Android Studio->View->Tool Windows->Captures,打开Captures窗口,将pull获取的hprof文件剪切到Capture中文件的目录下,双击打开即可;

具体的分析过程,这里就不重点讲述,大家去查询MAT相关的资料;
五、检测其他对象
如果想监听其他的对象(例如Fragment ),可以通过RefWatcher的实例来实现;
 
六、LeakCanary原理
1.RefWatcher.watch()函数会为被监控的对象创建一个KeyedWeakReference弱引用对象,是WeakReference对的子类,增加了键值对信息,后面会根据指定的键key找到弱引用对象;
2.在后台线程AndroidWatchExecutor中,检查KeyedWeakReference弱引用是否已经被清楚。如果还存在,则触发一次垃圾回收之后。垃圾回收之后,如果弱引用对象依然存在,说明发生了内存泄露;

7.冷启动优化 

 7.1 冷启动的相关概念

冷启动:属于你第一次打开APP,系统给你开一个进程,启动应用前,系统中没有任何该应用的任何进程信息的启动方式就称为冷启动;

热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。

 7.2  冷启动和热启动区别:

从定义和特点两方面来说:

  1、冷启动:冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。

  2、热启动:热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application

  因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次。

【冷启动白屏】冷启动启动的时候具有短暂的白屏时间;

 

7.3 冷启动的时间计算

  • API19 之后,系统会出打印日志输出启动的时间;
  • 冷启动时间 = 应用启动(创建进程) —> 完成视图的第一次绘制(Activity内容对用户可见);

7.4 冷启动的流程

  • Zygote进程中fork创建出一个新的进程;
  • 创建和初始化Application类、创建MainActivity;
  • 创建MainActivity类结束之后,开始inflate布局、当onCreate/onStart/onResume方法都走完;
  • 然后将:contentView的measure/layout/draw显示在界面上;

总结: 
Application构造方法 –> attachBaseContext() –>Application的onCreate() –> Activity构造方法 –> Activity的onCreate() –> 配置主题中背景等属性 –> onStart() –> onResume() –> 测量布局绘制显示在界面上

 7.5 冷启动的优化

  • 减少在Application和第一个Activity的onCreate()方法的工作量;
  • 不要让Application参与业务的操作;
  • 不要在Application进行耗时操作,比如图片的加载等等;
  • 不要以静态变量的方式在Application中保存数据;
  • 减少布局的复杂性和深度,①可以使用viewStub类,需要的时候再加载就可以;②在MainThread减少初始化的操作,可以通过懒加载,延迟所有的初始化;

 7.6 参考文章

【Android冷启动实现APP秒开】链接地址:https://blog.csdn.net/xx326664162/article/details/65630815

在冷启动的时间段内发生了什么?

首先我们要知道当打开一个Activity的时候发生了什么,在一个Activity打开时,如果该Activity所属的Application还没有启动,那么系统会为这个Activity创建一个进程(每创建一个进程都会调用一次Application,所以Application的onCreate()方法可能会被调用多次),在进程的创建和初始化中,势必会消耗一些时间,在这个时间里,WindowManager会先加载APP里的主题样式里的窗口背景(windowBackground)作为预览元素,然后才去真正的加载布局,如果这个时间过长,而默认的背景又是黑色或者白色,这样会给用户造成一种错觉,这个APP很卡,很不流畅,自然也影响了用户体验。

热启动

热启动:当启动应用时,后台存在该应用的进程(back键,home键,应用退出,但是没有销毁),从已有的进程中启动

热启动的特点:从已有的进程中启动,不需要创建和初始化Application ,直接创建和初始化它的Launch Activity

先来看下,未优化和优化后的对比图:

未优化,冷启动app会出现短暂的白屏

这里写图片描述

优化方案一:

使用背景图

这里写图片描述

优化方案二:

使用透明背景

这里写图片描述

消除启动时的白屏/黑屏

在用户点击手机桌面APP的时候,看到的黑屏或者白屏其实是界面渲染前的第一帧,如果你看懂了文章头的那2个问题,那么解决这个问题就非常轻松了,无非就是将Theme里的windowBackground设置成我们想要让用户看到的画面就可以了,这里有2种做法:

1、将背景图设置成我们APP的Logo图,作为APP启动的引导,现在市面上大部分的APP也是这么做的。

1 <style name="AppWelcome" parent="AppTheme">
2         <item name="android:windowBackground">@mipmap/bg_welcome_start</item>
3 </style>

2、将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会”立即”进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们心机的把Theme里的windowBackground的颜色设置成透明的,强行把锅甩给了手机应用厂商(手机反应太慢了啦,哈哈),其实现在微信也是这样做的,不信你可以试试。

1 <item name="android:windowIsTranslucent">true</item>
2 
3 <item name="android:windowFullscreen">true</item>

或者使用同名主题

1 <style name="Appwelcome" parent="android:Theme.Translucent.NoTitleBar.Fullscreen"/>

透明化这种做法需要注意的一点,如果直接把Theme引入Activity,在运行的时候可能会出现如下异常:

java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

这个是因为使用了不兼容的Theme,例如我这里的Activity继承了AppCompatActivity,解决方案很简单:
1、让其Activity集成Activity而不要集成兼容性的AppCompatActivity
2、在onCreate()方法里的super.onCreate(savedInstanceState)之前设置我们原来APP的Theme

1 public class MainActivity extends AppCompatActivity {
2     @Override
3     protected void onCreate(Bundle savedInstanceState) {
4             setTheme(R.style.AppTheme);
5             super.onCreate(savedInstanceState);
6     }
7 }

上面的2种做法,我们都需要将Theme引入对应的Activity

1         <activity
2             android:name=".app.main.MainActivity"
3             android:theme="@style/AppWelcome"
4             android:screenOrientation="portrait">
5             <intent-filter>
6                 <action android:name="android.intent.action.MAIN" />
7                 <category android:name="android.intent.category.LAUNCHER" />
8             </intent-filter>
9         </activity>

二、关于启动优化

上面的做法其实可以达到”秒开”APP的效果,不过确不是真实的速度,在Activity创建过程中其实是会经过一系列framework层的操作,在日常开发中,我们都会去重写Application类,然后在Application里进行一些初始化操作,比如存放用户标识的静态化TOKEN,第三方SDK的初始化等。 
这里给出几点建议: 
1、不要让Application参与业务的操作 
2、不要在APPlication进行耗时操作,比如有些开发者会在自己的APP里一系列文件夹或文件(比如我自己),这些I/O操作应该放到”确实该使用的时候再去创建”亦或者是数据库的一些操作。 
3、不要以静态变量的方式在Application中保存数据等。 
4、减少LaunchActivity的View层级,减少View测量绘制时间

当然这是绝对的理想主义,把上面的”不要”2字之前添上”尽量”2字吧,毕竟在实际开发中,这样做确实会让我们方便许多。

对了,补充一点,布局也是很重要的,尽量的去减少布局的复杂性,布局深度,因为在View绘制的过程中,测量也是很耗费性能的。

三、如何测量应用启动时间

android是有命令可以计算启动时间的

1 adb shell am start -W [packageName]/[packageName.launchActivity]

那就拿自己的项目来给大家看看上面两种启动的时间差别

冷启动:

这里写图片描述

热启动

这里写图片描述

参考: 
Android冷启动实现APP秒开 
探究Android的冷启动优化 
Android Application启动流程分析 
[Android从头再来] App启动过程 
Android冷启动时间优化

 【参考文章】Android Application启动流程分析 

1, App基础理论

要想优化App启动时间, 第一步就是了解App启动进程的工作原理. 有几个基础理论:

Android Application与其他移动平台有两个重大不同点:

  1. 每个Android App都在一个独立空间里, 意味着其运行在一个单独的进程中, 拥有自己的VM, 被系统分配一个唯一的user ID.
  2. Android App由很多不同组件组成, 这些组件还可以启动其他App的组件. 因此, Android App并没有一个类似程序入口的main()方法.

Android Application组件包括:

  • Activities: 前台界面, 直接面向User, 提供UI和操作.
  • Services: 后台任务.
  • Broadcast Receivers: 广播接收者.
  • Contexnt Providers: 数据提供者.

Android进程与Linux进程一样. 默认情况下, 每个apk运行在自己的Linux进程中. 另外, 默认一个进程里面只有一个线程---主线程. 这个主线程中有一个Looper实例, 通过调用Looper.loop()从Message队列里面取出Message来做相应的处理.

那么, 这个进程何时启动的呢?
简单的说, 进程在其需要的时候被启动. 任意时候, 当用户或者其他组件调取你的apk中的任意组件时, 如果你的apk没有运行, 系统会为其创建一个新的进程并启动. 通常, 这个进程会持续运行直到被系统杀死. 关键是: 进程是在被需要的时候才创建的.

举个例子, 如果你点击email中的超链接, 会在浏览器里面打开一个网页. Email App和浏览器App是两个不同的App, 运行在不同的进程中. 这次点击事件促使Android系统去创建了一个新的进程来实例化浏览器的组件.

首先, 让我们快速看下Android启动流程. 与众多基于Linux内核的系统类似, 启动系统时, bootloader启动内核和init进程. init进程分裂出更多名为"daemons(守护进程)"的底层的Linux进程, 诸如android debug deamon, USB deamon等. 这些守护进程处理底层硬件相关的接口.

随后, init进程会启动一个非常有意思的进程---"Zygote". 顾名思义, 这是一个Android平台的非常基础的进程. 这个进程初始化了第一个VM, 并且预加载了framework和众多App所需要的通用资源. 然后它开启一个Socket接口来监听请求, 根据请求孵化出新的VM来管理新的App进程. 一旦收到新的请求, Zygote会基于自身预先加载的VM来孵化出一个新的VM创建一个新的进程.

启动Zygote之后, init进程会启动runtime进程. Zygote会孵化出一个超级管理进程---System Server. SystemServer会启动所有系统核心服务, 例如Activity Manager Service, 硬件相关的Service等. 到此, 系统准备好启动它的第一个App进程---Home进程了.

2, 启动App流程

用户点击Home上的一个App图标, 启动一个应用时:

 
 

Click事件会调用startActivity(Intent), 会通过Binder IPC机制, 最终调用到ActivityManagerService. 该Service会执行如下操作:

  • 第一步通过PackageManager的resolveIntent()收集这个intent对象的指向信息.
  • 指向信息被存储在一个intent对象中.
  • 下面重要的一步是通过grantUriPermissionLocked()方法来验证用户是否有足够的权限去调用该intent对象指向的Activity.
  • 如果有权限, ActivityManagerService会检查并在新的task中启动目标activity.
  • 现在, 是时候检查这个进程的ProcessRecord是否存在了.

如果ProcessRecord是null, ActivityManagerService会创建新的进程来实例化目标activity.

2.1 创建进程

ActivityManagerService调用startProcessLocked()方法来创建新的进程, 该方法会通过前面讲到的socket通道传递参数给Zygote进程. Zygote孵化自身, 并调用ZygoteInit.main()方法来实例化ActivityThread对象并最终返回新进程的pid.

ActivityThread随后依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环.

流程图如下:

2.2 绑定Application

接下来要做的就是将进程和指定的Application绑定起来. 这个是通过上节的ActivityThread对象中调用bindApplication()方法完成的. 该方法发送一个BIND_APPLICATION的消息到消息队列中, 最终通过handleBindApplication()方法处理该消息. 然后调用makeApplication()方法来加载App的classes到内存中.

流程如下:

 

2.3 启动Activity

经过前两个步骤之后, 系统已经拥有了该application的进程. 后面的调用顺序就是普通的从一个已经存在的进程中启动一个新进程的activity了.

实际调用方法是realStartActivity(), 它会调用application线程对象中的sheduleLaunchActivity()发送一个LAUNCH_ACTIVITY消息到消息队列中, 通过 handleLaunchActivity()来处理该消息.

假设点击的是一个视频浏览的App, 其流程如下:

 

8.其他优化的问题

 8.1 尽量不使用静态变量保存数据

本文讲解的其实并不是一个技术方面,而是一个Android产品研发过程中的技巧:尽量不使用静态变量保存核心数据。

这是为什么呢?这是因为Android系统中的应用进程并不是安全的,包括application对象、静态变量在内的进程级别变量并不会一直呆着内存里面,它会被kill掉,它真的有可能会被kill掉,真的真的,重要的事情说三遍。

与大家普遍的看法不同之处在于,当进程被干掉之后,实际上app不会重新开始启动。Android系统会创建一个新的Application 对象,然后启动上次用户离开时的activity以造成这个app从来没有被kill掉得假象。

而这时候静态变量等数据由于进程已经被杀死而被初始化,所以就有了我们的不推荐在静态变量(包括Application中保存全局数据静态数据)的观点。

总结: 
在实际的Android开发过程中我们不建议使用静态变量传递数据,这样会因为进程被杀死而使静态变量初始化,我们可以使用其他数据传输方式:

    • 直接将数据通过intent传递给 Activity 。

    • 使用官方推荐的几种方式将数据持久化到磁盘上。

    • 在使用数据的时候总是要对变量的值进行非空检查。

最近我们的产品友友用车就遇到了这样的问题,下面我们详细的说明一下这个问题以及解决方案。在友友用车下单的过程中有一个当前行程的页面,该页面就是用户开车过程中需要展示的页面,主要用于展示当前用户的用车费用,行驶里程,用车时长,还车网点等信息。具体如下图: 
这里写图片描述

而在当前行程页面中App端有一个轮训请求,大概每隔一分钟会请求一次服务器,用于更新用户的用车费用,行驶里程,用车时长还车网点等信息。用户可以在当前行程页面停留很长的时间。

需要注意的是这里还有一个更换还车网点的按钮,点击这个按钮会跳转到更换还车网点的页面,从中会选择用户的常用网点,而这时候需要传递一个参数:用户的订单ID,此时用户的订单ID保存在了系统的静态变量中。

在前几天服务器端报了一个bug,说的是在当前行程页面点击更改换车网点请求常用网点时没有上传订单ID,而App端的代码是判断此时的订单ID是否为空,若不为空的话则上传.

/**
 * 判断订单ID是否为空,若不为空的话则上传
 */
if (!TextUtils.isEmpty(orderId)) {
                builder.setOrderId(orderId);
            }

可以看到,如果这时候我们的orderId为空,那么我们就不会上传orderId,而服务器端说我们没有上传orderId,可能的原因就是我们的orderId这时候为空了,那么这里的orderId是一个静态变量,而且我们没有手动的置空,为什么orderId会变为空了呢?

好吧,这时候就需要看一下服务器端的用户请求日志。

下面的这一段日志是用户这段时间所有的网络请求日志,当然也包括更改还车地点的请求服务器的日志:

[log@iZ28tw9apzsZ UUAccess-App]$ cat CmdAccess4Stat.log.2016-06-08 | grep '2119489256'
2016-06-08 11:22:52,778 1100004 229BAEC4B95E4C6ABB83F26FB7A48C88        2119489256      117.136.0.134   1465356172778   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:22:58,409 1070002 BABEDA5E46494FB4B00A2E50FD2F8F4D        2119489256      117.136.0.134   1465356178409   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:22:58,409 1130001 5F9D626DB30D442880A67D1C3A2A1C67        2119489256      117.136.0.134   1465356178409   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:22:58,862 1070002 68767819033342E5A31D03956C3FA84C        2119489256      117.136.0.134   1465356178862   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:22:58,862 1110001 6EB7CFC572E7404782512BAA26FE9D10        2119489256      117.136.0.134   1465356178862   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:22:59,029 1110002 C1039D50827D40098E3654FAD1260404        2119489256      117.136.0.134   1465356179029   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:22:59,498 1050007 DD0198099023454C889522A148F82955        2119489256      117.136.0.134   1465356179498   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:23:00,168 1130002 5E2A776EBF8743869202FB86A419580A        2119489256      117.136.0.134   1465356180168   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:23:10,038 1010003 0311997A14544D85BAA0F942650EFA84        2119489256      117.136.0.134   1465356190038   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 11:23:39,098 1070002 4B495330AC804EC3932BEBB6B6586595        2119489256      117.136.0.134   1465356219098   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:32,322 1070003 5F3F2146CDF54662A4CD5E4849D07422        2119489256      123.118.169.6   1465365692322   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:32,953 1050007 29A5FF713B11481CAABBD9084145AAA1        2119489256      123.118.169.6   1465365692953   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:33,177 1070002 AFB545D58AF648B69328D1417E73DAE6        2119489256      123.118.169.6   1465365693177   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:37,157 1110001 5CD82483782E4AF98A5FD34F2C8FB96A        2119489256      123.118.169.6   1465365697157   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:02:52,246 1050007 AF177A8CDCEC4B97B7C0EAA92B0DD7F5        2119489256      123.118.169.6   1465365772246   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:03:52,277 1050007 778CA2EC92094513A16E6EDCBB0664F0        2119489256      123.118.169.6   1465365832277   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:13,395 1010003 2DFA5673D9104412B6C5621B9E9FE19A        2119489256      123.118.169.6   1465372093395   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:15,019 1010003 D95851D7A08F4227A91A445E12105FEE        2119489256      123.118.169.6   1465372095019   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:16,984 1100004 CB208E17087C422DB56DB681E774B7C9        2119489256      123.118.169.6   1465372096984   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:17,382 1110001 CBABF14385374911BDCAEA427C1C0780        2119489256      123.118.169.6   1465372097382   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:18,011 1110002 F1D79FF8481D43F0B8861B986B458EFD        2119489256      123.118.169.6   1465372098011   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:18,716 1050007 58BDBF796411497CBD1A7CCD2F3E21AA        2119489256      123.118.169.6   1465372098716   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:18,716 1110002 8CF8D68D0A4A420C8D83A78818DBD4B7        2119489256      123.118.169.6   1465372098716   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:51:01,855 1050007 284E391D94EA48C6877F81B94E8E51DD        2119489256      123.118.169.6   1465372261855   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:51:04,784 1010002 6085D946E4AA46C884E775DE9B40987E        2119489256      123.118.169.6   1465372264784   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:51:09,249 1050007 4147044DC8014FC4B64488FFC1A0D83C        2119489256      123.118.169.6   1465372269249   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509

-------------------------------------------------------------------------------------------------------------------------------------------------

[log@iZ28foila6mZ UUAccess-App]$  cat CmdAccess4Stat.log.2016-06-08 | grep '2119489256'
2016-06-08 14:01:32,273 1100004 C6CC832BE038415CB6590D9174025FE9        2119489256      123.118.169.6   1465365692273   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:32,577 1070002 A23766F7F63C405080C3E7FEA4C6D1E1        2119489256      123.118.169.6   1465365692577   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:37,248 1110002 4C9A9F7521954E439C25B41CB4EB1E1A        2119489256      123.118.169.6   1465365697248   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:51,142 1010003 8EBB280697D349669A9C01925D2965E8        2119489256      123.118.169.6   1465365711142   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:01:51,955 1010002 0F35494F31ED475D93AC79F08D50CAF5        2119489256      123.118.169.6   1465365711955   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:06:44,272 1110001 2AF0F04090714B6FAAC15AD4E5F7924A        2119489256      123.118.169.6   1465366004272   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 14:06:44,365 1110002 728B5A82C759443A9DBBC91D3812C14F        2119489256      123.118.169.6   1465366004365   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:13,447 1100004 40AC6AFA4A5D4E75867A6459CAFA50AD        2119489256      123.118.169.6   1465372093447   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:17,025 1070003 3A2DAC87041E4B4BAD4A73596D5A1785        2119489256      123.118.169.6   1465372097025   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:17,758 1070002 BA4F5C07FB504882826DB2B99FA8CB5D        2119489256      123.118.169.6   1465372097758   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:48:18,015 1110001 CC2F250F823044298B85C23344D36B2A        2119489256      123.118.169.6   1465372098015   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:51:02,089 1070002 BF580DD1CA2F4F29800EE631C7940747        2119489256      123.118.169.6   1465372262089   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:51:03,518 1010003 9047E26C47C94F4C90A4FA9D460C2F54        2119489256      123.118.169.6   1465372263518   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:57:26,699 1050007 A283E7F6F70E4902946716474231F01A        2119489256      123.118.169.6   1465372646699   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 15:59:04,659 1050007 D6AF179627014BBF885C41292B59F26E        2119489256      123.118.169.6   1465372744659   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:00:45,601 1050007 D8029E9094B44737ADB47C94803D2753        2119489256      123.118.169.6   1465372845601   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:00:45,890 1070002 0475136678324CAABD0D18637ED97D3C        2119489256      123.118.169.6   1465372845890   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:01:12,120 1110001 6E784DF19F3C479183A8D06FD84DBF1B        2119489256      123.118.169.6   1465372872120   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:01:48,705 1050007 AA87FE1B960B40889A9F0511187D2F3D        2119489256      123.118.169.6   1465372908705   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:02:03,148 1010003 6801B16D60364BC8826C7AB9E7D29051        2119489256      123.118.169.6   1465372923148   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:02:03,501 1010002 CCA10A3EE4D9415384A761E623FE9959        2119489256      123.118.169.6   1465372923501   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:02:05,096 1050007 837721D2E671453888E170C95849E926        2119489256      123.118.169.6   1465372925096   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:02:09,446 1070002 D161AA4123354BA4A653582B65C8077E        2119489256      123.118.169.6   1465372929446   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:04:04,784 1050007 3B8B45A4066747AD8E43DD197B47C223        2119489256      123.118.169.6   1465373044784   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:05:04,778 1050007 18AA8CE0E8C446928B230D54F068D2E6        2119489256      123.118.169.6   1465373104778   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:20,860 1070003 BC4E84F58F654CB49FD76D8BA4854D85        2119489256      117.136.0.154   1465376180860   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:20,883 1100004 184A5E0784F94A64AE5D5D7DD6E6FB86        2119489256      117.136.0.154   1465376180883   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:21,291 1070002 25738C17D2314B4382089F600EF3BEB9        2119489256      117.136.0.154   1465376181291   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:22,233 1070002 FDACF66E84DC4FF696E2FC61C368DAB0        2119489256      117.136.0.154   1465376182233   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:25,967 1110002 B5B3CC24758B4AF7AB6F3FD6185D6213        2119489256      117.136.0.154   1465376185967   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:29,674 1050003 3EB8C2993802494998603A6FBCC4243A        2119489256      117.136.0.154   1465376189674   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:35,833 1070002 655CDEEDF98E4718A17431AB9B90060C        2119489256      117.136.0.154   1465376195833   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:57,419 1050004 FD6EEDA543744A3998FB7B1E4CCFABD2        2119489256      117.136.0.154   1465376217419   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509
2016-06-08 16:56:57,837 1070002 A4BE993338094CFDBB28C1C7C72BF0C6        2119489256      117.136.0.154   1465376217837   MI 4LTE Android xiaomi  2.2.0   c671b0b2b6de7aacdc2eabfa93ed4601        865372024757509

这就是出现问题的用户的网络请求日志,需要说明的是后台的日志分为两个节点,所以用户的请求日志信息被随机的分到了两个节点中,可以按照时间将这两段日志信息看成是一段日志信息。

可以发现在2016-06-08 14:00:00的时候用户每个一分钟执行一次请求1050007请求,而这里的1050007就是当前行程页面的轮训请求,大概每隔一分钟执行一次,然后在2016-06-08 14:03的的时候就不开始执行了,这应该是用户锁屏之后小米手机限制后台的网络请求,而这时候直接到了15:48分钟,这时候用户开始请求1010003接口,该接口就是用户请求更换网点时请求的接口,但是这时候我们发现用户同时调用了:1100004接口,而这个接口只有在Application的onCreate方法中调用了,所以这说明这时候应用进程已经重新启动了,但是这时候用户还是在当前行程页面,并且用户点击了更换还车网点按钮,而这时候由于点击更换还车网点需要静态变量orderId,而这时候上传的orderId为空,所以这时候的进程静态变量已经被初始化了。

所以综上可以还原一下bug的出现流程:

  • 用户在14:00的时候停留在当前形成页面;

  • 用户锁屏,系统限制后台网络请求,轮训操作被限制;

  • 系统内存吃紧,用户应用进程被杀死,进程的静态变量被初始化,activity界面被保留;

  • 用户打开屏幕,点击更换还车网点,这时候由于进程已经被杀死,静态变量被初始化,所以上报给服务器的orderId为空

  • 在用户打开屏幕的同时,系统重启应用进程,造成应用进程没有被杀死的假象;

好吧,既然已经知道了bug出现的原因,那么就好解决了,既然在内存中保存数据可能被系统杀死,那么我们可以有针对性的:

  • 直接将数据通过intent传递给 Activity 。

  • 使用官方推荐的几种方式将数据持久化到磁盘上。

  • 在使用数据的时候总是要对变量的值进行非空检查。

最后我们决定将该数据持久化,具体而言使用SharedPreferences来保存数据,相对来说这种方式比较简单。下面是使用sharedpreferences保存数据的静态工具方法:

/**
     * 获取SP中保存的订单ID
     *
     * @param context
     * @return
     */
    public static String getOrderId(Context context) {
        if (context == null) {
            L.i("从sp中获取orderId失败,context对象为空...");
            return "";
        }
        SharedPreferences sp = context.getSharedPreferences("morder", Context.MODE_PRIVATE);
        return sp.getString("morderid", "");
    }

    /**
     * 向Sp中保存orderId信息
     * @param context
     * @param orderId
     */
    public static void setOrderId(Context context, String orderId) {
        if (context == null) {
            L.i("向sp中设置orderId失败,context对象为空...");
            return;
        }
        // 若当前orderId为null,则设置orderId为""
        if (orderId == null) {
            orderId = "";
        }

        SharedPreferences sp = context.getSharedPreferences("morder", Context.MODE_PRIVATE);
        boolean isCommitSuccess = sp.edit().putString("morderid", orderId).commit();
        if (!isCommitSuccess) {
            sp.edit().putString("morderid", orderId).commit();
        }
    }

    /**
     * 清空sp中的orderId
     * @param context
     */
    public static void clearOrderId(Context context) {
        if (context == null) {
            L.i("向sp中设置orderId失败,context对象为空...");
            return;
        }

        SharedPreferences sp = context.getSharedPreferences("morder", Context.MODE_PRIVATE);
        sp.edit().putString("morderid", "").commit();
    }

这样我们在使用order数据的时候可以通过sharedpreferences方法来获取。这样静态变量数据被销毁的情况就不会出现了。

但是需要注意的是,当数据持久化的时候一定要在app启动的时候清空。

9. sharePrefrence使用问题

小结

总价一下,sp是一种轻量级的存储方式,使用方便,但是也有它适用的场景。要优雅滴使用sp,要注意以下几点:

  1. 不要存放大的key和value!我就不重复三遍了,会引起界面卡、频繁GC、占用内存等等,好自为之!
  2. 毫不相关的配置项就不要丢在一起了!文件越大读取越慢,不知不觉就被猪队友给坑了;蓝后,放进defalut的那个简直就是愚蠢行为!
  3. 读取频繁的key和不易变动的key尽量不要放在一起,影响速度。(如果整个文件很小,那么忽略吧,为了这点性能添加维护成本得不偿失)
  4. 不要乱edit和apply,尽量批量修改一次提交!
  5. 尽量不要存放JSON和HTML,这种场景请直接使用json!
  6. 不要指望用这货进行跨进程通信!!!

9.1不能用来跨进程

【说明】每个进程都会维护自己的sp副本,在其运行的过程中,其他的进程是无法获取此进程创建的sp副本的,只有在应用结束之后才会将数据持久化的写到副本当中;

 还有童鞋发现sp有一个貌似可以提供「跨进程」功能的FLAG——MODE_MULTI_PROCESS,我们看看这个FLAG的文档:

@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

文档也说了,这玩意在某些Android版本上不可靠,并且未来也不会提供任何支持,要是用跨进程数据传输需要使用类似ContentProvider的东西。而且,SharedPreference的文档也特别说明:

Note: This class does not support use across multiple processes.

那么我们姑且看一看,设置了这个Flag到底干了啥;在SharedPreferenceImpl里面,没有发现任何对这个Flag的使用;然后我们去ContextImpl类里面找找getSharedPreference的时候做了什么:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

这个flag保证了啥?保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个sp的时候,如果有这个flag,会

重新读一遍文件,仅此而已!所以,如果仰仗这个Flag做跨进程存取,简直就是丢人现眼。

9.2 sp存储文件会存在文件过大的问题

【android5大存储】网络、数据库、sp、文件、contentProvide;

 【说明】文件过大导致的问题

【1】app获取sp数据时候会阻塞主线程;导致时间过长导致了UI卡顿;

【2】解析sp大文件会出现大量的临时对象,造成大量的gc操作,导致UI卡顿;导致内存抖动;内存泄露等,有可能出现oom;

 【3】sp存储是以key-value形式存储在内存中,特别消耗内存资源;

下面是默认的sp实现SharedPreferenceImpl这个类的getString函数:

public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

继续看看这个awaitLoadedLocked:

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

一把锁就是挂在那里!!这意味着,如果你直接调用getString,主线程会等待加载sp的那么线程加载完毕!这不就把主线程卡住了么?

另外,有一个叫诀窍可以节省一下等待的时间:既然getString之类的操作会等待sp加载完成,而加载是在另外一个线程执行的,我们可以让sp先去加载,做一堆事情,然后再getString!如下:

// 先让sp去另外一个线程加载
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
// 做一堆别的事情
setContentView(testSpJson);
// ...

// OK,这时候估计已经加载完了吧,就算没完,我们在原本应该等待的时间也做了一些事!
String testValue = sp.getString("testKey", null);

更为严重的是,被加载进来的这些大对象,会永远存在于内存之中,不会被释放。我们看看ContextImpl这个类,在getSharedPreference的时候会把所有的sp放到一个静态变量里面缓存起来:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

注意这个static的sSharedPrefsCache,它保存了你所有使用的sp,然后sp里面有一个成员mMap保存了所有的键值对;

这样,你程序中使用到的那些个sp永远就呆在内存中。

9.3 存储JSON等特殊符号很多的value

还有一些童鞋,他在sp里面存json或者HTML;这么做不是不可以,但是,如果这个json相对较大,那么也会引起sp读取速度的急剧下降。

JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多 & 这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。

而JSON本来就是可以用来做配置文件的,你干嘛又把它放在sp里面呢?多此一举。下面我写个demo验证一下。

下面这个sp是某个app的换肤配置:

我们先用sp进行读取,然后用直接把它丢json文件,直接读取并且解析;json使用的代码如下:

public int getValueByJson(Context context, String key) {
    File jsonFile = new File(context.getFilesDir().getParent() + File.separator + SP_DIR_NAME, "skin_beta2.json");
    FileInputStream fis = null;
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    try {
        fis = new FileInputStream(jsonFile);
        FileChannel channel = fis.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1 << 13); // 8K
        int i1;
        while ((i1 = channel.read(buffer)) != -1) {
            buffer.flip();
            bao.write(buffer.array(), 0, i1);
            buffer.clear();
        }

        String content = bao.toString();
        JSONObject jsonObject = new JSONObject(content);
        return jsonObject.getInt(key);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        throw new RuntimeException("not a json file");
    } finally {
        close(fis);
        close(bao);
    }
    return 0;
}

然后我的测试结果是:直接解析JSON比在xml里面要快一倍!在小米1S上结果如下:

时间jsonspMi 1S8038Nexus5X6.53.5

这个JSON的读取还没有做任何的优化,提升潜力巨大!因此,如果你需要用JSON做配置,请不要把它存放在sp里面!!

9.4 多次edit多次apply

我见过这样的使用代码:

SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();

每次edit都会创建一个Editor对象,额外占用内存;当然多创建几个对象也影响不了多少;但是,多次apply也会卡界面你造吗?

有童鞋会说,apply不是在别的线程些磁盘的吗,怎么可能卡界面?我带你仔细看一下源码。

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    QueuedWork.add(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

注意两点,第一,把一个带有await的runnable添加进了QueueWork类的一个队列;

第二,把这个写入任务通过enqueueDiskWrite丢给了一个只有单个线程的线程池执行。

到这里一切都OK,在子线程里面写入不会卡UI。但是,你去ActivityThread类的handleStopActivity里看一看:

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {

    // 省略无关。。
    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    // 省略无关。。
}

waitToFinish?? 又要等?源码如下:

public static void waitToFinish() {
    Runnable toFinish;
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}

还记得这个toFinish的Runnable是啥吗?就是上面那个awaitCommit它里面就一句话,等待写入线程!!

如果在Activity Stop的时候,已经写入完毕了,那么万事大吉,不会有任何等待,这个函数会立马返回。

但是,如果你使用了太多次的apply,那么意味着写入队列会有很多写入任务,而那里就只有一个线程在写。

当App规模很大的时候,这种情况简直就太常见了!

因此,虽然apply是在子线程执行的,但是请不要无节制地apply;

commit我就不多说了吧?直接在当前线程写入,如果你在主线程干这个,小心挨揍。

10. 内存对象的序列化

 

序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

为什么需要序列化:

知道前面的序列化定义,内存对象什么需要实现序列化呢?

  • 永久性保存对象,保存对象的字节序列到本地文件

  • 通过序列化对象在网络中传递对象。

  • 通过序列化对象在进程间传递对象。

实现序列化的两种方式:

那么我们如何实现序列化的操作呢?在Android开发中我们实现序列化有两种方式:

  • 实现Serializable接口

  • 实现parcelable接口

两种序列化方式的区别:

都知道在Android studio中序列化有两种方式:serializable与parcelable。那么这两种实现序列化的方式有什么区别呢?下面是这两种实现序列化方式的区别:

  1. Serializeble是java的序列化方式,Parcelable是Android特有的序列化方式;

  2. 使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable。

  3. Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。

  4. Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性在外界有变化的情况下。  尽管Serializable效率低点, 也不提倡用,但在这种情况下,还是建议你用Serializable。

最后还有一点就是Serializeble序列化的方式比较简单,直接集成一个接口就好了,而parcelable方式比较复杂,不仅需要集成Parcelable接口还需要重写里面的方法。

对象实现序列化的实例:

通过实现Serializable接口实现序列化:

上面介绍了那么多概念上的知识,下面我们就具体看一下如何通过这两种方式实现序列化,我们首先看一下如何通过实现Serializable接口实现序列化,

通过实现Serializable接口实现序列化,只需要简单的实现Serialiizable接口即可。通过实现Serializable接口就相当于标记类型为序列化了,不需要做其他的操作了。

 1 /**
 2  * Created by aaron on 16/6/29.
 3  */
 4 public class Person implements Serializable{
 5 
 6     public static final long serialVersionUID = 1;
 7 
 8     private int age;
 9     private String name;
10     private String address;
11 
12     public int getAge() {
13         return age;
14     }
15 
16     public void setAge(int age) {
17         this.age = age;
18     }
19 
20     public String getName() {
21         return name;
22     }
23 
24     public void setName(String name) {
25         this.name = name;
26     }
27 
28     public String getAddress() {
29         return address;
30     }
31 
32     public void setAddress(String address) {
33         this.address = address;
34     }
35 }

 

可以发现我们定义了一个普通的实体Person类,并设置了三个成员属性以及各自的set,get方法,然后我们就只是简单的实现了Serializable接口就相当于将该类序列化了,

当我们在程序中传输该类型的对象的时候就没有问题了。

并且我们在Person类中定义了一个属性为serialVersionUID的成员变量,这个成员变量是做什么的呢? 
在Java中,软件的兼容性是一个大问题,尤其在使用到对象串行性的时候,那么在某一个对象已经被串行化了,

可是这个对象又被修改后重新部署了,那么在这种情况下, 用老软件来读取新文件格式虽然不是什么难事,但是有可能丢失一些信息。 
serialVersionUID来解决这些问题,新增的serialVersionUID必须定义成下面这种形式:

public static final long serialVersionUID=1;

其中数字后面加上的L表示这是一个long值。 通过这种方式来解决不同的版本之间的串行化的问题。

简单来说就是用serialVersionUID标识class类的版本,当序列化的class源文件发生变化时,反序列化的一端由于该标识不一致会反序列化失败,进而保证了两端源文件的一致性。

通过实现Parcelable接口实现序列化:

然后我们在看一下通过实现Parcelable接口来实现序列化的方式,通过实现Parcelable接口实现序列化相当于实现Serialiable接口稍微复杂一些,因为其需要实现一些特定的方法,下面我们还是以我们定义的Person类为例子,看一下如果是实现Parcelable接口具体是如何实现的:

 1 /**
 2  * Created by aaron on 16/6/29.
 3  */
 4 public class Person implements Parcelable{
 5 
 6     private int age;
 7     private String name;
 8     private String address;
 9 
10     public int getAge() {
11         return age;
12     }
13 
14     public void setAge(int age) {
15         this.age = age;
16     }
17 
18     public String getName() {
19         return name;
20     }
21 
22     public void setName(String name) {
23         this.name = name;
24     }
25 
26     public String getAddress() {
27         return address;
28     }
29 
30     public void setAddress(String address) {
31         this.address = address;
32     }
33 
34     @Override
35     public int describeContents() {
36         return 0;
37     }
38 
39     @Override
40     public void writeToParcel(Parcel dest, int flags) {
41         dest.writeInt(this.age);
42         dest.writeString(this.name);
43         dest.writeString(this.address);
44     }
45 
46     public Person() {
47     }
48 
49     protected Person(Parcel in) {
50         this.age = in.readInt();
51         this.name = in.readString();
52         this.address = in.readString();
53     }
54 
55     public static final Creator<Person> CREATOR = new Creator<Person>() {
56         @Override
57         public Person createFromParcel(Parcel source) {
58             return new Person(source);
59         }
60 
61         @Override
62         public Person[] newArray(int size) {
63             return new Person[size];
64         }
65     };
66 }

 

可以发现当我们通过实现Parcelable接口实现序列化还需要重写里面的成员方法,并且这些成员方法的写法都比较固定。

实现Parcelable序列化的Android studio插件:

顺便说一下最近发现一个比较不错的Parcelable序列化插件。下面就来看一下如何安装使用该插件。

  • 打开Android studio –> settings –> Plugins –> 搜索Parcelable –> Install –> Restart,这样安装好了Parcelable插件;

这里写图片描述

  • 然后在源文件中右键 –> Generate… –> Parcelable

这里写图片描述

  • 点击Parcelable之后可以看到,源文件中已经实现了Parcelable接口,并重写了相应的方法:

这里写图片描述

这样我们就安装好Parcelable插件了,然后当我们执行Parcelable操作的时候就重写了Parcelable接口的相应序列化方法了。

总结:

  • 可以通过实现Serializable和Parcelable接口的方式实现序列化

  • 实现Serializable接口是java中实现序列化的方式,而实现Parcelable是Android中特有的实现序列化的方式,更适合Android环境

  • 实现Serializable接口只需要实现该接口即可无需其他操作,而实现Parcelable接口需要重写相应的方法

  • Android studio中有实现Parcelable接口的相应插件,可安装该插件很方便的实现Parcelable接口,实现序列化

11.UI卡顿的工具

【1】StrackMode   UI卡顿追踪工具;没有找到;

 Android 性能优化:使用 TraceView 找到卡顿的元凶

https://blog.csdn.net/u011240877/article/details/54347396

【2】Android UI性能优化 检测应用中的UI卡顿

 https://blog.csdn.net/lmj623565791/article/details/58626355

posted @ 2018-05-24 09:22  OzTaking  阅读(1052)  评论(0)    收藏  举报