├── screenshot ├── context.png ├── ddms_hprof.png ├── ddms_update_heap.png └── dumpsysmeminfo.png ├── Android 数据库操作相关.md ├── README.md ├── 程序异常数据的丢失.md ├── Android 6 与之前的一些变化.md ├── 进程保活机制.md ├── 界面卡顿的优化.md ├── 内存的泄露和优化相关.md └── Activity + 多Frament 使用时的一些坑.md /screenshot/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpfduoduo/android-article/HEAD/screenshot/context.png -------------------------------------------------------------------------------- /screenshot/ddms_hprof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpfduoduo/android-article/HEAD/screenshot/ddms_hprof.png -------------------------------------------------------------------------------- /screenshot/ddms_update_heap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpfduoduo/android-article/HEAD/screenshot/ddms_update_heap.png -------------------------------------------------------------------------------- /screenshot/dumpsysmeminfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpfduoduo/android-article/HEAD/screenshot/dumpsysmeminfo.png -------------------------------------------------------------------------------- /Android 数据库操作相关.md: -------------------------------------------------------------------------------- 1 | # **数据库操作** 2 | 3 | **1 。 数据库的创建和升级** 4 |   继承SQLiteOpenHelper,实现构造方法、onCreate方法和onUpgrade方法。 5 |   构造函数的逻辑处理:需要注意的是你数据库版本号需要处理好,只能增加,不能减少。否则你使用app的时候就会出现crash。 6 |   onCreate函数中的逻辑处理:主要是处理数据库的创建操作。 7 |   onUpgrade函数中的逻辑处理:主要是处理数据库的升级操作,这个地方需要注意的是,数据库版本升级后,你需要兼容老版本的数据库,需要根据不同的oldVersion来进行处理,来保证老用户升级后,是可以使用新版本数据库。 8 | 9 | **2 。 数据库主键的使用** 10 |   假如你在进行数据库创建的时候, 11 | 12 | **3 。 数据库插入数据的优化** 13 |   数据库插入 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **android-article** 2 | 3 | 平时开发遇到的问题总结,希望大家多提一些建议和添加相应内容 4 | 5 | **1. [单个acticity+多个fragment or 多activity](Activity + 多Frament 使用时的一些坑.md) ** 6 |   早期的Android开发,由于没有Fragment的概念,因此每一个页面基本都是Activity。Google从3.0开始引入了Fragment的概念,慢慢的单Activity + 多Fragment成为一种趋势。目前基本所有的主流应用都是这种模式。 7 |   但是Fragment的使用一直有一些问题,并不能让开发者得心应手的使用,因此也出现了[反对使用Fragment](https://corner.squareup.com/2014/10/advocating-against-android-fragments.html)的一些声音,有兴趣的可以看一下。 8 |   本章节就是阐述了使用Fragment时需要注意的一些问题及其解决方法。 9 | 10 | **2. [内存相关](内存的泄露和优化相关.md)** 11 |   Android为每一个app提供了一定大小的内存,如果你的应用超过了这个范围,就会出现OOM,或者当你接近这个内存的时候,就会引起大量的GC,从而导致UI卡顿,直接影响了用户的体验。 12 |   本章节主要阐述了内存泄露常见错误、内存的优化等知识。 13 | 14 | **3. [进程保活](进程保活机制.md)** 15 |   本章节主要是阐述了目前常用的进程保活的主要机制。可以让你的app在系统中呆的时间更长,从而间接的提高了用户体验。 16 | 17 | **4. [界面卡顿的优化](界面卡顿的优化.md)** 18 |   当用户允许你开发的应用时,若经常出现莫名的卡顿,那么你这个应用距离被卸载已经不远了。 19 |   本章节阐述了目前常见的一些卡顿错误处理以及怎样才能定位出你卡顿的地方,是什么原因导致你app的卡顿,最后阐述则那样可以有效的预防卡顿的发生。 20 | 21 | 22 | **5. [程序异常数据的丢失](程序异常数据的丢失.md)** 23 |   当你的Rom内存较低的时候,Android就开始根据low meomory killer进行内存清理了,这时候如果你的app放在后台,很有可能就会被回收掉。但是经常是回收掉了你的页面资源,你的页面的栈管理没有被清除,这样就造成了一个现象:当你回到你的app时,你的界面控件内容都不见了。 这就是你没有对你的app做好异常数据丢失的处理,比如:onSaveInstanceState没有进行处理,你的Fragment页面出现了重叠等。 本章节就是阐述这个问题的处理。 24 | 25 | **6. [Android 6.0与之前的一些变化](Android 6 与之前的一些变化.md)** 26 |   本章节主要阐述了Android6.0与之前的重点不同之处,特别是对于开发者来说,一些常用的变化。比如:mac地址的获取,悬浮窗、动态权限的申请等。 27 | 28 | **6. [数据库的处理](Android 数据库操作相关.md)** 29 |   本章节主要针对数据的封装(基于原生的Android接口,并不会涉及ORM相关)和数据库的一些优化处理进行详细的解析。 -------------------------------------------------------------------------------- /程序异常数据的丢失.md: -------------------------------------------------------------------------------- 1 | # **程序异常的数据丢失** 2 | 3 |   当用户开启一个task,里面的activities都会被保存在这个栈的back stack中。当前的activity在栈顶并且拥有焦点,之前的activities都被压入栈中,处于stopped的状。但是当你开发者程序的时候,莫名的碰到,应用程序返回上一级页面发现内容为空,或者按home键一会后再次返回,会发现你的界面上控件的内容都是空。这说明你的手机内存较低,你的页面已经被回收了。 4 | 5 | 1。 **onSaveInstanceState的使用** 6 | 7 |   首先看一下默认的实现: 8 | 9 | ``` 10 | protected void onSaveInstanceState(Bundle outState) { 11 | super.onSaveInstanceState(outState); 12 | } 13 | ``` 14 | 15 |   如上所示,首先可以看到:onaveInsatnceState的入参是一个Bundle对象,由此决定了**要保存的内容,必须是序列化的内容**。 16 | 17 |   关于序列化,Android提供了两种:Serializable和Parcelable,**建议使用Parcelable**,效率更高。 18 |   当你的界面被回收之后,如果你再次返回,回到用onCreate函数,如下: 19 | 20 | ``` 21 | protected void onCreate(@Nullable Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | } 24 | ``` 25 |   同时你会发现入参savedInstanceState != null。 你可以通话此参数来判断是否该页面已经被回收,从而进行相应的逻辑处理。 26 |   有时候调用onSaveInstanceState,当你再次返回,有时候并不会调用onCreate函数,而是调用onRestoreInstanceState,而且这个函数一定会被调用。onSaveInstanceState和onRestoreInstanceState是一对。 27 | 28 |   onSaveInstanceState在什么时候会被调用? 有这么几种情况: 29 |   1. 当用户按下Home键的时候(包括长按Home运行其他程序) 30 |   2. 按下电源键,关闭屏幕 31 |   3. Activity A启动进入Activity B 32 |   4. 屏幕方向切换 33 | 34 |   总而言之,onSaveInstanceState的调用遵循一个重要原则,即当系统“未经你许可”时销毁了你的activity,则onSaveInstanceState会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据(当然你不保存那就随便你了) 35 |   虽然onSaveInstanceState和onRestoreInstanceState是一对,但是并不是同时都会被调用。比如: 用户按下HOME键回到主界面,然后用户紧接着又返回到activity A,这种情况下activity A一般不会因为内存的原因被系统销毁,故activity A的onRestoreInstanceState方法不会被执行。 36 | 37 | 2。 **模拟内存回收Activity** 38 | 39 |   开发的时候,你的测试机有时候怎么也不会回收你的app页面,怎么进行调试你的保护代码呢。 40 |   实际上,android提供了这种方式: **设置-开发者选项-选择不保留当前的活动(Do not keep activities)**。当你设置过之后,只要是当前的页面不存在了(当Activity处于onStop状态),当前的activity都会被回收。 需要注意的是:**即便是系统kill掉的activity,但是系统会记住他在back stack中的位置,当这个activity需要返回栈顶的时候,系统就会重新创建他(不是恢复onResume)**。这样你就可以很好地调试这种场景了。 41 | 42 |   以Activity A 启动 Activity B 为例: 43 |   当B打开以后, A不仅调用了onStop(), 还调用了onDestroy(), 当B finish 自己,再返回A的时候, A重新调用了onCreate() -> onResume()。 44 | 45 |   另外还需要注意的是:**国内很多rom优化过相机,有些手机当你在你的app内调用系统的相机,返回后上一个界面经常会被回收。** 46 | 47 |   需要注意的是:这个开关只处理了Activity, 对于系统在内存不足时杀死Service的情况并不能模拟出来。 48 | 49 |   **对Fragment生命周期的影响** 50 | 51 |   正常情况下,在创建阶段,Activity总是先调用的,Fragment的几个回调后面再调用。而在暂停和销毁的阶段(onPause 和 onStop 之后),先调用Fragment的几个回调,然后才是Activity的。具体的如下所示: 52 | 53 |   创建的时候 54 | 55 | ``` 56 | Activity onCreate(); 57 | Fragment onAttach(); 58 | Fragment onCreaet(); 59 | Fragment onCreateView(); 60 | Fragment onActivityCreated(); 61 | Activity onStart(); 62 | Fragment onStart(); 63 | Activity onResume(); 64 | Fragment onResume(); 65 | ``` 66 | 67 |   销毁的时候 68 | 69 | ``` 70 | Fragment onPause(); 71 | Activity onPause(); 72 | Fragment onStop(); 73 | Activity onStop(); 74 | Fragment onDestroyView(); 75 | Fragment onDestory(); 76 | Fragment onDetach(); 77 | Activity onDestroy(); 78 | ``` 79 | 80 |   打开这个开关之后,一旦Home键退出,Fragment被销毁,再次进来时重建,并且恢复到原来所在的布局中去。这是因为Activity在onSaveInstanceState()中保存了View和Fragment的状态, 在onRestoreInstanceState()恢复了这些状态(下面的举例中,只是在Activity中实现了该方法)。需要注意的是:override这两个方法保存其他数据时, 一般都需要调用super的方法, 81 | 82 |   按Home键销毁的生命周期: 83 | 84 | ``` 85 | Fragment onPause(); 86 | Activity onPause(); 87 | Fragment onStop(); 88 | Activity onStop(); 89 | Activity onSaveInstanceState(); 90 | Fragment onDestroyView(); 91 | Fragment onDestory(); 92 | Fragment onDetach(); 93 | Activity onDestroy(); 94 | ``` 95 |   再次进来时重建的生命周期如下所示: 96 | 97 | ``` 98 | Activity onCreate(); 99 | Fragment onAttach(); 100 | Fragment onCreaet(); 101 | Fragment onCreateView(); 102 | Fragment onActivityCreated(); 103 | Activity onStart(); 104 | Fragment onStart(); 105 | Actitvity onRestoreInstanceState bundle:xxxxxx 106 | Activity onResume(); 107 | Fragment onResume(); 108 | ``` 109 | 110 |   在使用Fragment的时候,会遇到各种各样的坑,需要注意,请看[Activity + 多Frament 使用时的一些坑](Activity + 多Frament 使用时的一些坑.md). 111 | 112 | 3。 113 | 114 | 115 | -------------------------------------------------------------------------------- /Android 6 与之前的一些变化.md: -------------------------------------------------------------------------------- 1 | ## **Android 6.0的一些变化** 2 | 3 | 4 | 本文列出开发过程中遇到的一些Android6.0的变化 5 | 6 | **1。 Mac地址的获取**    7 | 8 | 9 |   android 6.0之前,通过WifiManager-WifiInfo-getMacAddress()就可以获取到设备的mac地址 10 | 11 | ``` 12 | public static String getWiFiMac(Context context) 13 | { 14 | WifiInfo wifiInfo = getWifiInfo(context); 15 | if (wifiInfo == null) 16 | return null; 17 | String wifiMac = wifiInfo.getMacAddress(); 18 | return wifiMac; 19 | } 20 | 21 | public static WifiInfo getWifiInfo(Context context) 22 | { 23 | WifiManager wifiManager = getWifiManager(context); 24 | if (wifiManager != null) 25 | { 26 | WifiInfo wifiInfo = wifiManager.getConnectionInfo(); 27 | return wifiInfo; 28 | } 29 | else 30 | return null; 31 | } 32 | 33 | public static WifiManager getWifiManager(Context context) 34 | { 35 | WifiManager wifiManager = (WifiManager) context 36 | .getSystemService(Context.WIFI_SERVICE); 37 | return wifiManager; 38 | } 39 | 40 | ``` 41 |    但是Android6.0通过该方法只能获得一个固定的字符串“02:00:00:00:00:00” 42 |   43 |   解决方法: 44 | 45 | ``` 46 | public static String getMac() 47 | { 48 | String str = ""; 49 | String macSerial = ""; 50 | try 51 | { 52 | Process pp = Runtime.getRuntime().exec("cat /sys/class/net/wlan0/address "); 53 | InputStreamReader ir = new InputStreamReader(pp.getInputStream()); 54 | LineNumberReader input = new LineNumberReader(ir); 55 | 56 | for (; null != str;) 57 | { 58 | str = input.readLine(); 59 | if (str != null) 60 | { 61 | Log.d(tag, "cat sys/class/net/wlan0/address"); 62 | macSerial = str.trim();// 去空格 63 | break; 64 | } 65 | } 66 | } 67 | catch (Exception ex) 68 | { 69 | Log.e(tag, "error = " + ex.getMessage().toString()); 70 | } 71 | 72 | if (macSerial == null || "".equals(macSerial)) 73 | { 74 | try 75 | { 76 | Log.d(tag, "sys/class/net/eth0/address"); 77 | return loadFileAsString("/sys/class/net/eth0/address").toUpperCase( 78 | Locale.getDefault()).substring(0, 17); 79 | } 80 | catch (Exception e) 81 | { 82 | Log.e(tag, "error = " + e.getMessage().toString()); 83 | } 84 | 85 | } 86 | return macSerial; 87 | } 88 | 89 | public static String loadFileAsString(String fileName) throws Exception 90 | { 91 | FileReader reader = new FileReader(fileName); 92 | String text = loadReaderAsString(reader); 93 | reader.close(); 94 | return text; 95 | } 96 | 97 | public static String loadReaderAsString(Reader reader) throws Exception 98 | { 99 | StringBuilder builder = new StringBuilder(); 100 | char[] buffer = new char[4096]; 101 | int readLength = reader.read(buffer); 102 | while (readLength >= 0) 103 | { 104 | builder.append(buffer, 0, readLength); 105 | readLength = reader.read(buffer); 106 | } 107 | return builder.toString(); 108 | } 109 | ``` 110 | 111 | 112 | 113 | **2。 动态权限的申请** 114 | 115 |   Android 6.0之前,开发者只需要在AndroidManifesxt.xml中配置自己想要的权限就可以,当你安装应用程序的时候,会有一个页面提示你当前的应用需要你哪些权限。当前手机的ROM一般的都有管理应用程序权限的设置。 116 |   Android6 Google推出了动态权限的获取,如下危险的权限需要动态的症的用户的同意,比如:身体传感器、日历、摄像头、通讯录、地理位置、麦克风、电话、短信和存储空间。 Android6.0默认的为targetSdkVersion<23的应用程序默认授权了所申请的所有权限,所以如果你以前 APP设置的targetSdkVersion<23,在运行时也不会崩溃,但是这只是一个临时的救急策略。 117 | 118 |   下面以申请SdCard的写权限为例说明: 119 | 120 | ``` 121 | private void requestPermission() 122 | { 123 | if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) 124 | { 125 | if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) 126 | { //如果你上次拒绝了该权限 127 | Toast.makeText(this, "必须具有sdcard权限", Toast.LENGTH_LONG).show(); 128 | requestPermissions( 129 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 130 | EXTERNAL_STORAGE_REQ_CODE); 131 | } 132 | else 133 | { 134 | requestPermissions( 135 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 136 | EXTERNAL_STORAGE_REQ_CODE); 137 | } 138 | } 139 | } 140 | 141 | @Override 142 | public void onRequestPermissionsResult(int requestCode, String permissions[], 143 | int[] grantResults) 144 | { 145 | if (grantResults.length <= 0) 146 | return; 147 | 148 | if (grantResults[0] == PackageManager.PERMISSION_DENIED) 149 | {//用户拒绝了该权限,需要进行界面逻辑处理。 150 | } 151 | else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) 152 | { 153 | switch (requestCode) 154 | { 155 | case EXTERNAL_STORAGE_REQ_CODE : 156 | { 157 | createFile("hello.txt"); 158 | } 159 | } 160 | } 161 | 162 | } 163 | ``` 164 | 165 | 166 | **3。 悬浮窗的处理** 167 | 168 |   Android 6.0之(这时候你的targetSdkVersion<23),创建悬浮窗是需要申请权限:**android.permission.SYSTEM_ALERT_WINDOW**。 让后通过WindowManager就可以创建并显示一个悬浮窗(当前你必须授权该权限,MIUI这种Rom默认的是关闭的,需要在设置中打开)。 169 |   在Android6.0之后,如果你修改了targetSdkVersion为23(这个只能增大,不能变小回退)。 你再次通过WindowManager来创建悬浮窗,你的APP就直接崩溃了。 170 |   解决办法: 如果你将targetSdkVersion设置为23或者更高,在使用SYSTEM_ALERT_WINDOW权限是,需要先调用**Setting.canDrawOverlays()**来判断是否允许创建悬浮窗。允许直接创建;如果不允许需要发送一个action值为**ACTION_MANAGER_OVERLAY_PERMISSION**的Intent来让用户同意创建悬浮窗。 具体你代码如下所示: 171 | 172 | ``` 173 | if(Build.VERSION.SDK_INT >=23) { 174 | if(Seetings.canDrawOverlays(context) { 175 | //显示你的的悬浮窗 176 | } 177 | else { 178 | Intent inent = new Intent(Setting.ACTION_MANAGER_OVERLAT_PERMISSION); 179 | startActivity(intent); 180 | } 181 | } 182 | ``` 183 | 184 |   **“通过将WindowManager.LayoutParams的type设置为TYPE_TOAST”,是否还管用?** 185 |   之前有人通过逆向的方法得出结论:[将WindowManager.LayoutParams的type设置为TYPE_TOAST,可以不申请SYSTEM_ALERT_WINDOW权限就可以显示悬浮窗](http://www.jianshu.com/p/634cd056b90c),但是这种方法需要处兼容问题,比如:在MIUI下还是需要权限(同理其他ROM可能也会面临同样的问题),并且需要API level>=19(Android4.4)(老版本不响应触摸事件)。 具体的结论如下所示: 186 | 187 | ``` 188 | 在4.0.1以前, 当我们使用TYPE_TOAST, Android会偷偷给我们加上FLAG_NOT_FOCUSABLE和FLAG_NOT_TOUCHABLE, 4.0.1开始, 会额外再去掉FLAG_WATCH_OUTSIDE_TOUCH, 这样真的是什么事件都没了. 而4.4开始, TYPE_TOAST被移除了, 所以从4.4开始, 使用TYPE_TOAST的同时还可以接收触摸事件和按键事件了, 而4.4以前只能显示出来, 不能交互。 189 | API level 18及以下使用TYPE_TOAST无法接收触摸事件的原因也找到了. 190 | 文/Shawon(简书作者) 191 | 原文链接:http://www.jianshu.com/p/634cd056b90c 192 | ``` 193 |   那么这种方法在Androi6.0中(排除MIUI这种ROM),还可以吗? 194 |   我们拿一个Android 6.0的手机(天机AXON),经过测试([demo](https://github.com/liaohuqiu/android-UCToast)):**是可以的** 195 |   当然,最可靠的方案还是:**read the fucking sourecode !!!** 196 | 197 | **4。 Apache HttpClient的移除** 198 | 199 |   早在Android2.3 Android就建议使用HttpURLConnection来进行网络开发。而且Google退出的一些开源都是这么做的,如:Volley等。 200 | 201 | **5。 WiFi和网络变化** 202 | 203 |   1. 你的app智能修改自己的创建的WifiConfiguration对象的状态,不能修改其他App创建的WifiConfiguration对象。 204 |   2. Android 6.0之前,你可以通过enableNetwork(),设置disableAllOthers = true 205 | ,来是的设备软开其他网络,如蜂窝网络,而强制连接指定的Wifi网络。此版本上设备将不会从其他网络断开。 206 | -------------------------------------------------------------------------------- /进程保活机制.md: -------------------------------------------------------------------------------- 1 | #**进程保活机制** 2 | 3 |   进程保活:说白了就是尽量的保证你的App不被系统杀死,或者被杀死后,还能后“复活”。以下介绍进程保活的几种常用方式。 4 |   Android杀死进程的机制来自于linux的low memory killer,他会对所有的进程进行一个排名,衡量的参数就是oom_adj。此值越大,进程越容易被系统杀死。 一般系统应用oom_adj的值都是小于0的,比如系统的启动init进程,oom_ajd = -16 5 | 6 |   **查看oom_adj值:**oom_adj:这个值越大,标明此进程越容易被回收。系统进程这个值都是小于0的。 7 |   adb shell cat /proc/pid(你的进程id)/oom_adj 8 |   pid的获取命令:ps | grep 进程名 9 |    10 | 11 |   首先我们看一下系统杀死app的优先级(从高到低,越低越容易被杀掉): 12 |   **前台进程:** 你当前正在使用的app进程,这是最后被系统杀死的进程。查看oom_adj,值为0 13 |   **可见进程:** 可见进程不包含任何的前台组件,可见进程依然会影响用户在屏幕上可以看到的内容。比如:activity弹出来一个不全屏的Dialog。从代码角度来说:就是调用了onPause,但是没有调用onStop。 查看oom_adj,值为1 14 |   **服务进程:** app进程启动了一个Servie,并且该进程并没有和任何的界面做绑定。 oom_adj 值为 15 |   **后台进程:** app退出后,会持有一个Activity,从代码逻辑上来说,就是activity没有调用onDestroy。 oom_adj值为7 16 |   **空进程:** app退出后,不包含任何活动的组件,从代码角度来说就是activity调用了onDestroy,但是系统还没有进行回收。该进程是最先被系统kill的。 oom_adj值为9 17 | 18 |   以上不同进程的oom_adj根据不同的具体手机,可能会有不同,仅仅是反应了一个趋势。 19 | 20 |   在Android系统里,进程被杀的原因通常为以下几个方面: 21 |   a. **应用Crash** 22 |   b. **系统回收内存** 23 |   c. 用户触发 24 |   d. 第三方root权限app. 25 | 26 |   本文仅仅是讨论前两种,通过以下发发实现后,你也会发现oom_adj的值确实变小了,这就意味着从low memeo killer 的角度来说,已经完成。一般来说:你通过用户触发的方式,你会得到onDestory的响应,这样你是可以做一些操作处理的。但是通过X60这种方式,你会发现:你根本不会相应onDestroy。而且它会把你杀的干干净净。由此我们也可以加单的猜测:X60这种清理方式:不仅仅是清理你的原始app的进程,他也会把你启动的另一个进程kill掉(可以根据包名查到你启动的相关的进程)从而无法相互唤醒(猜测而已)。 27 | 28 | **1。 前台服务(带有Notification)** 29 | 30 |   设置前台服务,通过这样的方式,就会出现一个Notification来告知用户,比如QQ音乐。这种实现的方式比较简单:实现Service的时候,通过接口:setForeground(int id, Notification notification)就可以实现前台服务。这个属于上述中的“前台进程”; 31 |   缺点:会出现一个Notification,这个用户是的感知的,一般用户不喜欢这个东西的存在。 32 | 33 | **2。 通过Android的系统漏洞实现前台服务(无Notification)** 34 | 35 |   利用系统的漏洞来启动一个前台Service进程。与普通启动方式的区别在于:没有生成一个Notification,用户感知不到(这是一个漏洞,android后续版本可能会随时的关闭)。 36 |   API<18:启动前台Service时直接传入一个new Notification()即可。 37 |   API>18:同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理(这是个android漏洞,我**自己测试了galaxy s6 android 6.0是可以的**) 38 |   **查看是否启动成功:**adb shell dumpsys activity services packagename。回车,查看isForground字段是否为true。 39 | 40 |   具体的代码如下所示: 41 | 42 | ``` 43 | @Override public int onStartCommand(Intent intent, int flags, int startId) { 44 | if (Build.VERSION.SDK_INT < 18) { 45 | startForeground(GRAY_SERVICE_ID, new Notification());//API < 18 ,此方法能有效隐藏Notification上的图标 46 | } 47 | else { 48 | Intent innerIntent = new Intent(this, SecondService.class); 49 | startService(innerIntent); 50 | startForeground(GRAY_SERVICE_ID, new Notification()); 51 | } 52 | } 53 | 54 | public static class SecondService extends Service { 55 | 56 | @Override public void onCreate() { 57 | super.onCreate(); 58 | } 59 | 60 | @Override public int onStartCommand(Intent intent, int flags, int startId) { 61 | startForeground(GRAY_SERVICE_ID, new Notification()); 62 | stopSelf(); 63 | return super.onStartCommand(intent, flags, startId); 64 | } 65 | 66 | 67 | @Override public IBinder onBind(Intent intent) { 68 | return null; 69 | } 70 | } 71 | ``` 72 |   通过上面的代码后,你在查看你进程的oom_adj。你会发现发生了很大的变化。   73 | 74 | **3。 通过多进程相互唤醒机制** 75 | 76 |   **多进程启动:**在老的进程中新启动一个前台进程ServcieNew(通过上述的方案2),SericeNew进程每个一段时间检查换上一个进程是否存在,如果不存在立马启动,或者通过广播的方式发送广播,通知老的进程重新启动业务逻辑。 77 |   多进程需要注意的地方:你的老的进程的Application的onCreate会被调用两次,这个地方一般都是用来初始化操作,多进程需要通过AIDL或者广播的方式进程通信。 78 |   Application的onCreate会被调用两次的解决方法: 79 | 80 | ``` 81 | @Override public void onCreate() { 82 | super.onCreate(); 83 | Log.d(TAG, "onCreate function"); 84 | 85 | int pid = Process.myPid(); 86 | String processName = null; 87 | ActivityManager actManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); 88 | for (ActivityManager.RunningAppProcessInfo appProcess : actManager.getRunningAppProcesses()) { 89 | if (appProcess.pid == pid) { 90 | processName = appProcess.processName; 91 | } 92 | } 93 | 94 | if (processName.equals(getPackageName())) { 95 | init(); 96 | } 97 | } 98 | 99 | ``` 100 |   以下通过广播的方式来说明,具体的实现方式如下: 101 |   首先在你的老的进程中实现一个**静态广播**(在Androidmaniest.xml中声明该广播,这样程序就算被杀掉了也可以收到该广播)BroadcastReceiver : 102 | 103 | ``` 104 | public class RecoveryReceiver extends BroadcastReceiver { 105 | 106 | private final static String TAG = WakeReceiver.class.getSimpleName(); 107 | private final static int RECOVERY_SERVICE_ID = -110; 108 | public final static String RECOVERY_ACTION = "com.recovery.receiver"; 109 | 110 | @Override public void onReceive(Context context, Intent intent) { 111 | String action = intent.getAction(); 112 | if (RECOVERY_ACTION.equals(action)) { 113 | Intent wakeIntent = new Intent(context, WakeNotifyService.class); 114 | //收到此消息后启动自己的业务逻辑 115 | } 116 | } 117 | } 118 | 119 | ``` 120 |   其次在新启动的进程中,每隔一段时间发送一次广播(没有检测老的进行是否还存在),通知老进程启动业务逻辑,需要注意的是:新启动的进程也要使用进程保活机制,尽量的保证不能轻易的被系统杀掉: 121 | 122 | ``` 123 | private final static int ALARM_INTERVAL = 5 * 60 * 1000; //每个5分钟发送一次广播 124 | 125 | AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 126 | Intent alarmIntent = new Intent(); 127 | alarmIntent.setAction(RecoveryReceiver.RECOVERY_ACTION); 128 | PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent, 129 | PendingIntent.FLAG_UPDATE_CURRENT); 130 | alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, 131 | operation); 132 | ``` 133 | 134 |   **验证方式:**adb shell之后,kill掉你的第一个进程,然后等待看两一个Service能不能将该进程启动。 135 | 136 | **4。 通过系统广播来启动进程** 137 | 138 |   Android提供了很多的系统广播,如: 139 |   系统重新启动 140 |   调用系统相拍照的广播(android4.0新增加了android.hardware.action.NEW_PICTURE、android.hardware.action.NEW_VIDEO,针对照片类型的APP,应用场景很大)等。 141 |   以拍照为例: 142 |   注册一个静态广播(静态广播在你程序不启动的时候就可以接收到,注意广播的exprote务必设置为true,否则你会接收不到,另外,这个不需要Camera等权限) 143 | 144 | ``` 145 | 149 | 150 | 151 | 152 | 153 | 154 | ``` 155 | 156 |   **验证方式:**adb shell之后,kill掉你的进程,然后拍照片看是否可以启动。 157 | 158 | **5。 通过SynAdapter** 159 | 160 |   [一种提高Android应用进程存活率新方法](http://blog.csdn.net/zgzhaobo/article/details/51462292) 来自csdn 作者:zgzhaobo 161 |   上述这种方法“利用Android系统提供的账号和同步机制实现”,进行被kill掉后,进程被系统kill后,可以由syn拉起。 缺点:用户可以单独的停止或者删除,而且有些手机是默认不同步,需要用户参与。个人感觉这种方案通用性不是很大。 162 |   关于Android系统账号可以参考文章: 163 |   [Write your own Android Sync Adapter](http://blog.udinic.com/2013/07/24/write-your-own-android-sync-adapter/) 164 |   [Write your own Android Authenticator](http://blog.udinic.com/2013/04/24/write-your-own-android-authenticator/) 165 | 166 | **6。 通过第三方的推送SDK** 167 |   目前业界很多的第三方推送SDK(友盟、极光推送等),都是通过长链接的方式完成推送,我们就可以通过这些sdk来实现进程的保活。 168 | 169 | 170 |   **总结:** 以上介绍了low memoey killer 下的进程保活。其实,不管你采用哪种方式,当你的进程内存太大了,系统都会将你进程杀掉。以上手段只是辅助手段,**降低进程的内存**才是最终的手段。 171 | 172 | 173 | **7。应用Crash后的重新启动** 174 |   在你的应用Crash之后,你是可以通过Android给你提供的消息回调进行相应的处理的,比如:保存crash日志等各种业务逻辑。目前很多手机Rom在你crash之后,会立马回到你刚才crash的页面,但是由于你代码的逻辑问题,你会发现该页面根本没有进行业务操作。有的时候,你当前的页面没有办法进行任何的操作来串起你的流程来。 175 |   这个时候建议crash之后,让你的app重启。不要让默认的Rom操作回到你原来crash的页面(当然有的Rom在你crash之后会重启应用,为了统一处理,这个逻辑最好还是我们自己来进行处理。这个逻辑最好放在你程序的基类当中,这样不管是那个一页面发生了crash,重新启动你都会进入到自己的重启流程。 176 | 177 | **推荐 **: 178 |   [微信Android客户端后台保活经验分享](https://mp.weixin.qq.com/s?__biz=MzA3ODg4MDk0Ng==&mid=403254393&idx=1&sn=8dc0e3a03031177777b5a5876cb210cc&utm_source=tuicool&utm_medium=referral) 179 | -------------------------------------------------------------------------------- /界面卡顿的优化.md: -------------------------------------------------------------------------------- 1 | # **界面卡顿的优化** 2 | 3 | 4 | ## **原因** 5 |   界面卡顿的原因很简单:UI线程做了太多的工作,从而导致16ms(1000ms/60帧)无法完成一帧的刷新。造成了丢帧。 6 | 7 | *** 8 | 9 | ## **卡顿的定位** 10 |   **1. [通过TraceView来定位](http://blog.csdn.net/innost/article/details/9008691)** 11 | 12 |   TraceView是Android提供的DDMS中的一个小工具,用来进行数据采集和分析。目前采集数据的方法有两种: 13 |   开发者可通过**DDMS中快捷键**来进行。对开发者而言,此方法适用于没有目标应用源代码的情况。 14 |   若开发者和想针对一些关键的代码进行数据的采集和分析,可以使用**Debug类的方法:startMethodTracing和stopMethodTracing**。这两个函数运行过程中将采集运行时间内该应用所有线程(注意,只能是Java线程)的函数执行情况,并将采集数据保存到/mnt/sdcard/下的一个文件中。开发者然后需要利用SDK中的Traceview工具来分析这些数据。 15 |   TraceView显示了两个界面:Timeline Panel和Profile Panel。其中Profile Pannel是TraceView的核心界面,主要展示了某个线程(先在Timeline Panel中选择线程)中各个函数调用的情况,包括CPU使用时间、调用次数等信息。 16 |   各列参数含义的说明: 17 |   Incl Cpu Time 某函数占用的CPU时间,包含内部调用其它函数的CPU时间 18 |   Excl Cpu Time 某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间 19 |   Incl Real Time 某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间 20 |   Excl Real Time 某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间 21 |   **Call+Recur Calls/Total 某函数被调用次数以及递归调用占总调用次数的百分比** 22 |   **Cpu Time/Call 某函数调用CPU时间与调用次数的比。相当于该函数平均执行时间** 23 |   Real Time/Call 同CPU Time/Call类似,只不过统计单位换成了真实时间 24 | 25 |   一般而言:你需要关注的点有两个:**一类是调用次数不多,但每次调用却需要花费很长时间的函数。一类是那些自身占用时间不长,但调用却非常频繁的函数**。 26 | 27 | 28 |   **2. [使用开源库BlockCanary](http://blog.zhaiyifan.cn/2016/01/16/BlockCanaryTransparentPerformanceMonitor/?utm_source=tuicool&utm_medium=referral)** 29 | 30 |   **特点:** 31 |   非侵入式,简单的两行就可以打开监控,也不需要到处的打点; 32 |   精准,输出的信息可以精确到行的方式定位到问题所在; 33 | 34 |   **实现原理分析:** 35 |   App整个应用的主线程只有一个Looper,不管你有多少个Handler,最后进入UI都是回到主线程的这个looper。在Looper的loop方法中有这么一段: 36 | 37 | ``` 38 | public static void loop() { 39 | ... 40 | for (;;) { 41 | ... 42 | // This must be in a local variable, in case a UI event sets the logger 43 | Printer logging = me.mLogging; 44 | if (logging != null) { 45 | logging.println(">>>>> Dispatching to " + msg.target + " " + 46 | msg.callback + ": " + msg.what); 47 | } 48 | msg.target.dispatchMessage(msg); 49 | if (logging != null) { 50 | logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); 51 | } 52 | ... 53 | } 54 | } 55 | ``` 56 |   可以看到有一个Printer的对象loogging, 它在每个message处理的前后被调用,而如果主线程卡住了,不就是在dispatchMessage里卡住了吗? 57 |   因此方法就是:利用主线程的消息队列处理机制,通过: 58 | 59 | ``` 60 | Looper.getMainLooper().setMessageLogging(mainLooperPrinter); 61 | ``` 62 |   并在mainLooperPrinter中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。 63 | 64 | ``` 65 | @Override 66 | public void println(String x) { 67 | if (!mStartedPrinting) { 68 | mStartTimeMillis = System.currentTimeMillis(); 69 | mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis(); 70 | mStartedPrinting = true; 71 | } else { 72 | final long endTime = System.currentTimeMillis(); 73 | mStartedPrinting = false; 74 | if (isBlock(endTime)) { 75 | notifyBlockEvent(endTime); 76 | } 77 | } 78 | } 79 | 80 | private boolean isBlock(long endTime) { 81 | return endTime - mStartTimeMillis > mBlockThresholdMillis; 82 | } 83 | ``` 84 | 85 |   在发生Block的时候,就可以将你的堆栈信息和cpu的一些信息获取到,从而定位出到底是哪里发生了卡顿。 86 | 87 |   **3. ANR日志的分析** 88 | 89 |   Activity如果超过5s无法响应触摸事件或者键盘输入事件就会触发ANR,BroadcastReceiver如果10s未执行完成操作也会触发ANR,Service在20s未执行完操作也会触发ANR。 90 |   当一个进程触发ANR之后,系统会在**/data/anr/**目录下创建一个traces.ext文件,通过分析该文件可以定位出ANR的原因。 91 | *** 92 | 93 | ## **措施** 94 | 95 | **1 。 不要在UI线程大量的进行如下操作:** 96 | 97 |   **a。文件读写操作** 98 |   **b。数据库操作** 99 |   **c。动画操作** 100 |   **d。循环操作** 101 | 102 |   注意:不是进行大量的操作,比如:就在sdcard建立一个文件夹,整个工程代码就调用了一次,再来开一个线程就是浪费资源了,何况有些东西必须在主线程实现,比如动画。 103 | 104 | 105 | **2 。内存优化,不要引起虚拟机大量的GC** 106 | 107 | 108 |   关于内存优化请阅读[内存的泄露和优化相关](内存的泄露和优化相关.md) 109 | 110 | 111 | **3 。 布局的优化 避免OverDraw** 112 | 113 | 114 |   很多的初学习者都没有听说过OverDraw(屏幕上的一个像素点被渲染了多少次,就像关于画画一样,被重复覆盖画了多少次),更不知道如何打开这个设置,来查看自己界面是否合理最优: 115 |   设置-开发者选项-show GPU OverDraw 116 |   其中: 117 |   没有颜色: 意味着没有overdraw。像素只画了一次。 118 |   蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。 119 |   绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。 120 |   浅红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。 121 |   暗红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。 122 |   当你的程序已经很庞大,再来修改这些东西就很繁琐了。所以建议一开始进行你项目开发的时候就要想到这这些。尽量使得自己overdraw成都最低,甚至没有。 123 | 124 |   优化策略: 125 |   **a。合并冗余的布局** 126 |   可以使用lint工具检测你代码及其布局layout,并学会使用merge来处理 127 |   **b。去掉不需要的android:background** 128 |   当你的父布局和子布局的背景一致时,应该考虑取出子背景 129 |   **c。去掉系统默认的窗口背景** 130 |   需要注意的是:setContentView之后执行,否则是无效的 131 | 132 | ``` 133 | getWindow().setBackgroundDrawable(null) 134 | ``` 135 |   **d。使用.9图来做背景** 136 |   这种情况经常发生在View需要两层背景,比如ImageView需要设置一个前景和一个背景(其中背景用来做边框),将背景drawable制作成9patch,并且将和前景重叠的部分设置为。 137 |   **e。谨慎使用alpha** 138 |   假如对一个View做Alpha转化,需要先将View绘制出来,然后做Alpha转化,最后将转换后的效果绘制在界面上。通俗点说,做Alpha转化就需要对当前View绘制两遍。 139 |   **f。学会使用ClipRect & QuickReject 进行局部绘图** 140 |   canvas.clipRect()用来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。 141 |   canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。    142 | 143 | **4 。 动画处理** 144 | 145 |   Android 5.0之后提供了**RenderThread**:是一个新的由系统控制的处理线程,它可以在UI线程阻塞时保持动画平滑(这是一个很模糊的定义)。它可以处理优化操作与GPU分发,减轻UI线程的压力。这一点从Android 5.0提供的一些列水波纹动画就可以看出来。 146 |   如何让自己的动画允许在RenderThread中? 147 |    148 | 149 | **5 。 线程优先级的处理** 150 | 151 |   许多人在Java中使用线程的时候,就是很简单的new Thread。但是从来不考虑线程的优先级相关。在Android中,如果你不设置线程的优先级,系统就认为该线程与UI线程优先级一致,系统分配时间片就没有轻重缓急。这样你的UI就受到了你业务逻辑的制约。那么怎样在添加优先级呢,具体如下所示: 152 | 153 | ``` 154 | new Thread(new Runnable() { 155 | @Override public void run() { 156 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 157 | //do something 158 | } 159 | }).start(); 160 | ``` 161 | 162 |   不同的优先级如下所示: 163 |   int THREAD_PRIORITY_AUDIO //标准音乐播放使用的线程优先级 164 |   int THREAD_PRIORITY_BACKGROUND //标准后台程序 165 |   int THREAD_PRIORITY_DEFAULT // 默认应用的优先级 166 |   int THREAD_PRIORITY_DISPLAY //标准显示系统优先级,主要是改善UI的刷新 167 |   int THREAD_PRIORITY_FOREGROUND //标准前台线程优先级 168 |   int THREAD_PRIORITY_LESS_FAVORABLE //低于favorable 169 |   int THREAD_PRIORITY_LOWEST //有效的线程最低的优先级 170 |   int THREAD_PRIORITY_MORE_FAVORABLE //高于favorable 171 |   int THREAD_PRIORITY_URGENT_AUDIO //标准较重要音频播放优先级 172 |   int THREAD_PRIORITY_URGENT_DISPLAY //标准较重要显示优先级,对于输入事件同样适用。 173 | 174 | 175 | **6 。 一些接口的优化处理** 176 | 177 |   **1. List的removeAll接口** 178 |   这个接口在进行大量数据(上千)的操作时,有些机器卡顿很明显。解决方法: 179 | 180 | ``` 181 | HashSet delSet = new HashSet<>(list); 182 | mList.removeAll(delSet); 183 | ``` 184 |   **2.时间转换的优化** 185 |   你使用SimpleDateFormat进行时间转换,这各函数相对还时有点耗时的。举例说明: 186 |   有时候,你的服务器传过来的具体的时间yy-mm-dd hh:mm:ss,你需要是现实的是yy-mm-dd。这是你和你的服务器约定好的,此时只需要进行String的简单处理,就可以获取yy-mm-dd,而不需要使用SimpeDateFormat来进处理。举例如下所示: 187 | 188 | ``` 189 | final String time = "2016:12:12 10:22:33"; 190 | new Thread(new Runnable() { 191 | @Override public void run() { 192 | Log.d(tag, "simple date format start"); 193 | for (int i = 0; i < 1000; i++) { 194 | DateUtils.convertPhotoTimeToYM(time); 195 | } 196 | Log.d(tag, "simple date format end"); 197 | } 198 | }).start(); 199 | 200 | new Thread(new Runnable() { 201 | @Override public void run() { 202 | Log.d(tag, "subString start"); 203 | for (int i = 0; i < 1000; i++) { 204 | DateUtils.getYMD(time); 205 | } 206 | Log.d(tag, "subString end"); 207 | } 208 | }).start(); 209 | ``` 210 |   两者的耗时对比是巨大的(在一个比较老的机型:zte u880 很经典的智能机): 211 |   前者耗时;1s又160ms 212 | 02-28 **05:17:08.410** 19376-19410/com.example.androiddemo D/MainActivity: simple date format start 213 | 02-28 **05:17:09.570** 19376-19410/com.example.androiddemo D/MainActivity: simple date format end 214 |   后者耗时:30ms 215 | 02-28 **05:20:02.300** 19376-22409/com.example.androiddemo D/MainActivity: subString start 216 | 02-28 **05:20:02.330** 19376-22409/com.example.androiddemo D/MainActivity: subString end 217 | 218 |   再一次强调:**约定即规范** 219 | 220 |   **3. 加载图片时的优化处理** 221 | 222 |   当你滑动你的ListView, GridView, 或者最新的recycleView的时候,如何做到图片加载不卡顿?最基本的你需要做到以下几点: 223 |   **a. 加载图片的线程必须设置线程的优先级** 224 |   很多人在使用线程的时候是不设置线程优先级的,这样的话系统会认为你的线程和UI线程一样重要,从而导致系统分配时间片等没有倾向于UI线程。当进行像加载图片这样耗时和好内存操作时,你的UI线程会很吃力。如何添加线程优先级?请查看本章节**“线程优先级的处理”** 225 |   **b. 必须使用缓存策略** 226 |   对于已经加载过的图片,使用LRU等缓存到内存中,这样下次显示该图片的时候,不需要进行耗时和耗内存的图片下载和解码操作,而是直接从内存中读取,如果读取不到再进行图片下载或者解码操作。 227 |   **c. 必须对图片进行Option操作** 228 |   随着相机像素的增大,一张图片可以达到5M。如果你没有进行特殊处理,这样没有加载几张图片,你的内存就爆掉了。 229 |   **d. 快速滑动时不加载图片** 230 |   在快速滑动时控制任务的执行频率。在图片加载中最简单的就是不执行图片加载任务,让列表显示默认的图片,当停止滑动时再执行图片加载任务。 231 |   具体的如下所示: 232 | 233 | ``` 234 | 235 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 236 | int totalItemCount) 237 | { 238 | mStartIndex = firstVisibleItem; 239 | mEndIndex = firstVisibleItem + visibleItemCount; 240 | } 241 | 242 | @Override 243 | public void onScrollStateChanged(AbsListView view, int scrollState) 244 | { 245 | switch (scrollState) 246 | { 247 | case OnScrollListener.SCROLL_STATE_IDLE : 248 | loadImage(mStartIndex, mEndIndex); 249 | break; 250 | } 251 | } 252 | ``` 253 | 254 | 255 |   **4. Arrays.asList()接口** 256 |   当你使用ArrayList list = Arrays.asList()。你很有可能需要使用到List的clear等接口来对asList()返回的列表进行清空等操作处理,但是你会发现你的程序crash了。 257 | 258 | ``` 259 | Exception in thread "main" java.lang.UnsupportedOperationException 260 | at java.util.AbstractList.remove(AbstractList.java:144) 261 | at java.util.AbstractList$Itr.remove(AbstractList.java:360) 262 | at java.util.AbstractCollection.remove(AbstractCollection.java:252) 263 | at com.test.test01.Test.main(Test.java:27) 264 | ``` 265 |   原因是什么呢? 266 |   Arrays.asLisvt() 返回java.util.Arrays$ArrayList, 而不是ArrayList。Arrays$ArrayList和ArrayList都是继承AbstractList。remove、add等 method在AbstractList中是默认throw UnsupportedOperationException而且不作任何操作。ArrayList override这些method来对list进行操作,但是Arrays$ArrayList没有override remove(int),add(int)等,所以throw UnsupportedOperationException。 267 |   正确的做法是: 268 | 269 | ``` 270 | List list = Arrays.asList(a[]); 271 | List arrayList = new ArrayList(list); 272 | ``` 273 | 274 |   **5. ** 275 | 276 | 277 | -------------------------------------------------------------------------------- /内存的泄露和优化相关.md: -------------------------------------------------------------------------------- 1 | # **内存泄露和内存优化** 2 |   对于Android来说,每一个APP的内存是有限的。你过你的内存出现问题:泄露,长期占用过高,就会导致app易于被杀掉。频繁的gc导致app卡顿等现象。 3 | 4 | ## **常见情况** 5 | * **Activity的Context的使用** 6 | * 界面的Context静态化 7 | * 单例式将界面的Context作为初始化入参数,并且在单例模式保存 8 | * 特殊的,在Android 6.0中,不能使用Activity的Context通过接口getSystemService()来获取各种Manager,如下所示: 9 | 10 | ``` 11 | AActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE); 12 | ``` 13 | 如上所示,在Android 6.0 中就会造成内存泄露 14 | 15 | 16 | * **非静态内部类持有外部类的引用** 17 | 18 |   在Java中,非静态内部类(包括匿名内部类)都会持有外部类(一般是指Activity等页面)的引用,当两者的生命周期出现不一致的时候,很容易导致内存泄露。 19 |   如下所示,非常常见的几种情况: 20 | **Hanlder** 21 | 22 | ``` 23 | private Handler mHandler = new Handler() { 24 | @Override 25 | public void handleMessage(Message msg) 26 | { 27 |   super.handleMessage(msg); 28 | } 29 | }; 30 | ``` 31 |   这里的Handler会引用Activity的引用,当handler调用postDelay的时候,若Activity已经finish掉了,因为这个 handler 会在一段时间内继续被 main Looper 持有,导致引用仍然存在,在这段时间内,如果内存吃紧至超出,是很危险的。 32 | 33 | **Thread** 34 | 35 | ``` 36 | public class ThreadActivity extends Activity { 37 | public void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | new MyThread().start(); 41 | } 42 | 43 | private class MyThread extends Thread { 44 | @Override 45 | public void run() { 46 | super.run(); 47 | dosomthing(); 48 | } 49 | } 50 | private void dosomthing(){ 51 | 52 | } 53 | } 54 | ``` 55 |   假设MyThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。 56 | 57 | **Runnable** 58 | 59 | ``` 60 | public class MainActivity extends Activity { 61 | ... 62 | Runnable ref1 = new MyRunable(); 63 | Runnable ref2 = new Runnable() { 64 | @Override 65 | public void run() { 66 | 67 | } 68 | }; 69 | ... 70 | } 71 | ``` 72 | 73 |   ref1和ref2的区别是,ref2使用了**匿名内部类**,也就是说当前的Activity会被ref2所应用,如果将这个引用传入到了一个异步线程,该线程的生命周期与Activity的生命周期不一致的时候,就会导致内存泄露。 74 | 75 | * **Static变量造成内存泄露** 76 | 77 |   1. 界面类的静态化: 静态Activity 78 |   2. 界面中View的静态化: 静态View 79 |   界面中View的静态化一定会导致页面内存泄露。界面中的View都是持有界面引用的,静态变量的生命周期与整个app的生命周期一致。 80 |   3. 非静态内部类的静态化 81 |   具体的 如下所示: 82 | 83 | ``` 84 | public class MainActivity extends AppCompatActivity { 85 | 86 | private static Drawable sDrawable; 87 | 88 | @Override protected void onCreate(Bundle savedInstanceState) { 89 | super.onCreate(savedInstanceState); 90 | TextView lableView = new TextView(this); 91 | if(sDrawable == null) { 92 | sDrawable = getDrawable(R.drawable.icon); 93 | } 94 | labelView.setBackgroundDrawable(sDrawable); 95 | setContentView(lableView); 96 | } 97 | } 98 | ``` 99 | 100 |   View的setBackgroundDrawable()的源码如下所示: 101 | 102 | ``` 103 | public void setBackgroundDrawable(Drawable background) { 104 | ... 105 | 106 | if (background != null) { 107 | ... 108 | 109 | background.setCallback(this); 110 | ... 111 | } else { 112 | ... 113 | } 114 | 115 | ... 116 | } 117 | ``` 118 |   其中有一个background.setCallback(this);,所以这就导致这个静态变量指向的对象又持有了TextView这个对象的引用,TextView持有的确实整个Activity的引用。这样就导致了内存泄露。 119 | 120 |   我们再来看一个例子: 121 | 122 | ``` 123 | public class MainActivity extends AppCompatActivity { 124 | private static InnerClass sInnerClass; 125 | 126 | @Override protected void onCreate(Bundle savedInstanceState) { 127 | super.onCreate(savedInstanceState); 128 | setContentView(R.layout.activity_leak); 129 | sHello = new Hello(); 130 | } 131 | public class InnerClass {} 132 | } 133 | ``` 134 |   静态的非静态内部类对象sInnerClass持有了外部Acitivity的引用,当屏幕发生变化时,不会被释放。 135 | 136 | * **资源没有关闭** 137 |   1. Cursor游标没有关闭 138 |   数据库中才操作经常碰到cursor。 139 |   2. InputStream、OutputStream等没有关闭 140 |   文件读写、Socket读写等经常碰到 141 |   3. 注册的广播等没有unRegister 142 |   4. 一些CallBack的Listener没有被清除,举例: 143 | 144 | ``` 145 | void registerListener() { 146 | SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); 147 | Sensor snedor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL); 148 | sensorManager.registerListneer(this, sensor, SensorManager.SENSOR_DELAY_FASTEST); 149 | } 150 | ``` 151 |   getSystemService负责执行某些后台任务,或为硬件提供接口,如果context对象想要在服务内部的事件发生时被通知,需要注册监听器。然而这让服务持有了activity的引用,如果activity销毁时没有取消注册,那么你的activity就泄露了。 152 | 153 | * **View添加到没有删除机制的容器中** 154 | 155 | * **属性动画导致的内存泄露** 156 |   如果你设置你的动画为无限循环,而且没有在onDestroy中停止该动画,那么动画会一直播放下去,Activity的View会被动画吃持有,而View持有了Activiy。从而导致内存泄露。 157 | 158 | * **过期引用** 159 | 160 |   当一个数组扩容后又被缩减,比如size从0->200->100(一个栈先增长,后收缩),那么元素的index>=100的那些元素(被Pop掉的)都算是过期的元素,那些引用就是过期的引用(永远不会再被接触的应用)-来自Effective Java 161 | 162 | ``` 163 | public Object pop(){ 164 | if(size==0) throw new EmptyStackException(); 165 | Object result = elements[--size]; 166 | elements[size] = null; //消除过期引用 167 | return result; 168 | } 169 | 170 | ``` 171 |   由于过期引用的存在,GC并不会去回收他们,我们需要手动的释放他们。 172 | 173 | 174 | ## **内存溢出和内存的查看方法** 175 | 176 | 177 | 178 | * **使用第三方开源库** 179 | 180 | [LeakCanary](https://github.com/square/leakcanary) 181 | 在这里就不做具体的介绍了。网上的使用demo: 182 | [leakCanary Demo](https://github.com/liaohuqiu/leakcanary-demo) 183 | 184 | * **adb shell命令** 185 | 186 |   通过以下命令可以查看你APP的内存使用情况已经Activity和View等的个数情况,具体的 187 | 188 | ``` 189 | adb shell dumpsys meminfo packagename 190 | ``` 191 |   其中,packagename就是你程序的报名,具体的示例,如下图所示: 192 | ![](screenshot/dumpsysmeminfo.png) 193 | 194 |   如上图所示: 195 |   上面部分显示的是你的app所占用的内存总数(主要是看TOTAL,内存所实际占用的值) 196 |   下面的部分可以看到你的一些对象的个数:如Views、Activities等。 197 | 当你进入一个acitivity的时候,activity的个数会增加,退出后会减少,如果只增加、不减少,就说明出现了内存泄露的问题。 (经过实际的测试,这个对有些手机,好不管用,就算我写个demo:只有一个Activity,什么也没有做,进来、退出、进来、activities个数会变大,不会立即变小,需要等一段时间才会变小) 198 | 199 | 200 | * **DDMS** 201 | 202 |   DDMS是Android开发环境中的Dalvik虚拟机(andoid4.4之前,4.4及其之后引入了ART虚拟机)调试监控服务。 203 | 204 |   1. update heap 205 | 206 | ![](screenshot/ddms_update_heap.png) 207 | 208 |   对一个activity进入退出反复多次看data object是否稳定在一个范围 209 |   2. MAT(Memory Analyzer Tool) 210 | 211 | ![](screenshot/ddms_hprof.png) 212 | 213 |   dump hprof file : 点击后等待一会,会生成一个hprof文件。插件版本的MAT可以直接打开该文件,否则需要进行一步转换操作。 提供了这个工具 hprof-conv (位于 sdk/tools下), 转换命令如下所示: 214 | 215 | ``` 216 | ./hprof-conv xxx-a.hprof xxx-b.hprof 217 | ``` 218 |   最后通过DDMS-File-open,打开的hprof文件即可进行分析内存泄露相关。 219 | 220 | 221 | ## **内存优化建议** 222 | 223 | * **了解你机器的内存情况** 224 | 225 |   通过以下代码可以查看**每个进程可用的最大内存**,即heapgrowthlimit值 226 | 227 | ``` 228 | ActivityManager actManager = getApplicationContext.getSystemService(Context.ACTIVITY_SERVICE);s int memClass = actManager.getMemeoryClass(); //以M为单位 229 | ``` 230 | 231 |   通过以下代码可以获取 **应用程序的最大可用内存** 232 | 233 | ``` 234 | long maxMemory = Runtime.getRuntime().maxMemeory(); //以字节为单位 235 | ``` 236 | 237 |   **两者的区别:** 238 |   单位不一致 前者以M为单位,后者以字节为单位。 239 |   具体的以lenovo的一款手机(S850T, Android版本为4.4.2)为例: 经过测试两者得到的值一致均是128M。 240 | 241 |   **使用场景** 242 |   当你进行图片加载的时候,都会使用到LRUCache,初始化的时候设置缓存的大小。一般来说都设置为当前最大内存的1/8,如果你就是一个图片应用你直接1/4也可以。 243 | 244 | ``` 245 | long cacheSize = Runtime.getRuntime().maxMemeory(); 246 | mLruCache = new LruCache(cacheSize) 247 | { 248 | @Override 249 | protected int sizeOf(String key, Bitmap value) 250 | { 251 | return value.getRowBytes() * value.getHeight(); 252 | }; 253 | }; 254 | ``` 255 | 256 | * **当界面不可见、内存紧张的时候释放内存** 257 | 258 |   android4.0(包含4.0)之后引入了onTrimMemory(int level)(4.0之前为onLowMemory) ,系统会根据不同的内存状态来毁掉,参数 level 代表了你app的不同状态,Application、Activity、Fragment、Service、ContentProvider均可以响应。具体如下: 259 | 260 |   **TRIM_MEMORY_UI_HIDDEN**: 应用程序被隐藏了,如按了Home或者Back导致UI不可见,这个时候,我们应该释放一些内存。 261 | 262 |   *以下三个是我们的应用程序真正运行时的回调:* 263 |   **TRIM_MEMORY_RUNNING_MODERATE**: 程序正常运行,并不会被杀掉,但是手机的内存有点低了,系统可能开始根据LRU规则来杀死进程了。 264 |   **TRIM_MEMORY_RUNNING_LOW**: 程序正常运行,并不会被杀掉,但是手机内存非常的低了,应该释放一些资源了,否则影响性能。 265 |   **TRIM_MEMORY_RUNNING_CRITICAL**: 程序正在运行,但是系统已经根据LRU杀死了大部分缓存的进程了,此时我们需要释放内存,否则系统可能会干掉你。 266 | 267 |   *以下三个是当应用程序是缓存时候的回调:* 268 |   **TRIM_MEMORY_BACKGROUND**: 内存不足,并且该进程是后台进程。 269 |   **TRIM_MEMORY_MODERATE**: 内存不足,并且该进程在后台进程列表的中部。 270 |   **TRIM_MEMORY_COMPLETE**:内存不足,并且该进程在后台进程列表的最后一个,马上就要被清理了,这个时候应该把一切尽可能释放的都释放掉。 271 | 272 | 273 |   通常在我们开始进行架构设计的时候,就要考虑到哪些东西是要常驻的,哪些东西是缓存后要被清理, 一般情况下,以下资源都要被清理: 274 | **缓存**:包括文件缓存、图片的缓存、比如第三方图片缓存库。 275 | **一些动态生成的View**: 比如一般应用的图片轮播View,在你的应用隐藏后,根本不需要轮播。 276 | 277 |   **案例分析:** 278 |   1. LRUCache缓存的清理方式:trimToSize()接口可以重新设置缓存的大小。evictAll()接口可以清楚所有的LRUCache缓存内容。 279 |   2. 暴力清理界面中的View 280 | 281 | * **图片资源的压缩** 282 |   1. res中资源到压缩: 使用有损压缩工具,比如:[tinyPng](https://tinypng.com/),压缩后的图片肉眼根本看不出来,压缩率可以达到50%以上。 283 |   2. BitmapFactory的压缩。 284 |   通过BitmapFactory的Options设置,降低采样率,压缩图片到适合的大小,同时注意使用若引用和缓存机制。 285 |   Bitmap.Config设置图片的格式为RGB565,这个设置肉眼是看不出色彩的丢失,而且比RGB8888占存小的多。 286 |   使用BitmapFactory.Options.inBitmap字段。如果这个选项被设置,那么使用该Options 的decode方法将会尝试复用一个已经存在的bitmap来加载新的bitmap。这意味着bitmap的内存将被复用,避免分配和释放内存来提升性能。然后,使用inBitmap有一些限制。特别是在Android4.4(API level19)之前,只有尺寸相同的bitmap才能使用该特性。具体的见[使用示例](http://www.2cto.com/kf/201502/375084.html) 287 |   3. 将图片资源放在合适的drawable目录下。 288 | 289 | 290 | * **使用Android优化过的类和集合** 291 |   1. SparseArrry来替代HashMap 292 |   2. LongSparseArray, key为long,替代HashMap 293 |   3. SimpleArrayMap和ArrayMap替代HashMap, ArrayMap是通过时间来换取效率,在数千之内建议使用ArrayMap。 294 | 295 | 296 | * **避免创建不必要的对象** 297 | 298 |   在短时间内创建了大量的对象,然后有释放,这样就引起了**内存抖动**。频繁的引起GC操作,会导致内存的卡顿。 299 |   1. 字符串的拼接:StringBuffer(非线程安全)和StringBuilder(线程安全)的使用 300 |   2. 自定义View中不要在onDraw中定义画笔等对象 301 |   3. 在循环函数内避免创建重复的对象,将多个函数都经常用到的不可变对象拿出来统一进行初始化,在一开始写的时候就要特别的注意,否则后边修改起来很是麻烦(主要是再找到他很麻烦) 302 |   4. 在循环的内部不要使用try catch操作,将其拿到外面来。 303 |   5. 不要在循环中进行文件的操作:比如判断文件是否存在,这相对是一个很耗时的操作 304 | 305 |   **案例说明** 306 |   SimpleDateFromat是用来时间转换的,一般的,开发者都会定义个专门用于时间转化的static的函数: 307 | 308 | ``` 309 | public static String paserTimeToYM(long time) 310 | { 311 | SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault()); 312 | return format.format(new Date(time)); 313 | } 314 | ``` 315 |   假如你在for循环中调用此函数。就不停的重复创建SimpleDateFromat对象。你应该将对象创建拿出来,放在类中,或者是重新定义一个时间转换函数,入惨为已经创建好的SimpleDateFormat对象。 316 |   还需要注意的是:假如你的循环量很大,不建议在for循环中进行时间转换,而是在你用到的时候才进行转换,比如显示出来。 317 | 318 | 319 | * **不要扩大变量的作用域** 320 | 321 | ``` 322 | classs A 323 | { 324 | private B mB; 325 | public A(B b) { 326 | this.mB = b; 327 | //就在构造函数中进行了对mB进行了一些操作 328 | } 329 | //后续再也没有用到过mB 330 | } 331 | 332 | class B 333 | { 334 | public B() { 335 | } 336 | public static void main(String[] args) 337 | { 338 | } 339 | } 340 | ``` 341 |   如上所示的简单代码:类A的构造函数中,传入了类B的对象,并且类A中定义了成员变量mB,但是mB就在构造函数中用了一下,后续再也没有用,在类A中mB的生命周期和A一致。本来mB的作用域就在构造函数,结果扩大为整个类。 342 | 343 | 344 | * **不要让生命周期比Activity长的对象持有Activity的引用** 345 | 346 |   这样的错误很多,比如:将Activity的Context传给单例模式,毫不知情的将Activity的Context传给非静态内部类或者是匿名内部类。 347 | 348 | * **尽量的使用Application的Context** 349 | 350 |   Application的生命周期是整个app,他会一直在。 351 |   1. 在界面类中直接使用getApplicationContext。 352 |   2. 在其他地方使用MyApplication(extends Application)的getInstance操作。如下所示: 353 | 354 | ``` 355 | public class MyApplication extends Application 356 | { 357 | private static Context sContext; 358 | 359 | @Override 360 | public void onCreate() 361 | { 362 | Log.d(tag, "onCreate"); 363 | sContext = this; 364 | } 365 | 366 | public static Context getAppContext() 367 | { 368 | return sContext; 369 | } 370 | } 371 | ``` 372 |   总之一句话:能使用Application的Context,就不要使用Activity的。关于Android不同组件的Context能做到的事情如下图所示: 373 | 374 | ![](screenshot/context.png) 375 | 376 |   其中:NO1表示Application和Service可以启动一个Activity,不过需要创建一个新的task任务队列。而对于Dialog而言,只有在Activity中才能创建。 377 | 378 | * **移除回调** 379 |   1. handler的removeCallbacksAndMessages(null) 380 |   2. setXXXCallback(null)、 setXXXListener(null),需要注意的是,要进行callback调用的地方就需要进行判断了 381 | 382 | 383 | * **常量的使用** 384 | 385 |   关于enum和static。Android强烈建议不要使用enum,他会使得内存消耗变大为原来的2倍以上。 386 | 387 | 388 | * **使用代码混淆剔除不需要的代码** 389 | 390 | * [代码混淆的说明](https://github.com/gpfduoduo/Article/blob/master/%E6%B7%B7%E6%B7%86%E8%AF%A6%E8%A7%A3.md) 391 | 392 | * jar包的混淆:使用proguardgui.bat 393 | 394 | * jar包的合并:使用插件fatjar 395 | 396 | * **请使用静态内部类+WeakReference的方式** 397 | 398 |   非静态内部类和匿名内部类会持有页面的应用,请使用静态内部类,并将页面的引用通过WeakReference的方式传递过去。 399 | 400 | * **合理的使用多进程** 401 | 402 |   android对单个进程都有一个内存允许的最大内存限制。加入你在你的app中又启动一个进程,这样你的内存限制就变为了原来的2倍。 403 |   启动多进程的方法很简单,只需要在AndroidManifest.xml声明的四大组件的标签中增加"android:process"属性即可。 404 |   进程分为两种:私有进程和全局进程。私有进程在名称签名添加冒号即可。 405 |   但是多进程有一些需要注意的地方: 406 |   1. Application的onCreate会被调用多次。一般程序会将程序的一些初始化的操作放在这里,这点需要注意。 407 |   2. 多进程之间的通讯必须使用AIDL接口,需要注意的一点是:AIDL之间传递大量数据是有一个限制的。 传递内容过大会出现:TransactionToolLargeException。官方文档说明:最大的限制为1M。 408 |   3. 多进程导致 静态成员、单例模式和SharedPreference 都变的不可靠。 409 |   4. 多进程之间传递数据的效率:有些手机在传递大量数据的时候,效率很差。 410 |   5. 多进程传递对象需要实现序列化操作。 411 |   6. AIDL支持的数据类型:基本数据类型;String和CharSequence;List仅仅支持ArrayList,里面的每一个对象都必须支持序列化,Map只支持HashMap,里面的key和value都必须支持序列化(必须被AIDL支持)。 412 |   7. AIDL服务端可以使用CopyOnWriteArrayList和ConcurrentHashMap来进行自动线程同步,客户端拿到的依然是ArrayList和HashMap。 413 |   8.AIDL服务端和客户端之间做监听器,服务端需要使用RemoteCallbackList,否则客户端的监听器无法收到通知(因为服务端实质还是一份新的序列化后的监听器实例,并不是客户端那份)。 414 |   9.客户端调用远程服务方法时,因为远程方法运行在服务端的binder线程池中,同时客户端线程会被挂起,所以如果该方法过于耗时,而客户端又是UI线程,会导致ANR,所以当确认该远程方法是耗时操作时,应避免客户端在UI线程中调用该方法。同理,当服务器调用客户端的listener方法时,该方法也运行在客户端的binder线程池中,所以如果该方法也是耗时操作,请确认运行在服务端的非UI线程中。另外,因为客户端的回调listener运行在binder线程池中,所以更新UI需要用到handler。 415 | 416 |   我们将在进程常驻中进行简单的[示例分析](进程保活机制.md),实现多进程的相互唤醒操作。 417 | 418 | * **请不要使用注解框架** 419 | 420 |   程序注解框架极大的方便了程序开发者,不需要开发者大量的写findViewById(), setOnclickListener()等方法,但是程序注解框架是将类中的所有相关方法都缓存在内容中不会释放,这些内存就会越来越大,从而得不到释放。而且一般程序注解方法都是用到了Java的反射机制。这个是不建议使用的(虽然有时候反射不得不使用)。 421 | -------------------------------------------------------------------------------- /Activity + 多Frament 使用时的一些坑.md: -------------------------------------------------------------------------------- 1 | # **Activity + 多Fragment的一些坑** 2 | 3 |   目前单个(多个)Activity + 多个Fragment已经成为主流APP的页面呈现方式,例如:微信、今日头条等。下面就阐述一下Activity + 多个Fragment的使用过程中的一些问题和经验教训。**需要注意的是:本文说讲的Fragment都是support.v4下的Fragment,具体的 support-v4-23.1.1**。 4 | 5 | **1 。 getActivity() = null** 6 | 7 |   当你用Fragment的时候,经常会碰到这个问题,容器对象为空。 8 |   **原因:** 9 |   当你将你的Fragment出栈之后,该Fragment仍然在异步执行任务,并且执行完之后调用了getActivity()方法。 10 |   **解决方法:** 11 |   晓得原因,解决办法很简单了。Fragment出栈之后,不让你的Fragment执行异步任务,或者已补任务执行后,不调用getActivity方法就是了。但是这个说起来容易,当你的团队人多,能力参差不齐的时候,往往会出现问题。 12 | 13 | **2 。 Fragment接口的选择:replace or show hide** 14 | 15 |   show(), hide()最终是让你的Fragment的View调用setVisibility(),不会调用生命周期。 16 |   replace()会销毁视图,调用onDestroyView、onCreateView等一系列的生命周期。 17 |   如果你业务有一个很高的概率经常使用当前的Fragment,建议使用hide和show,他不会调用Fragment 的onStop方法,比如微信底部的tab场景等。 18 | 19 | **3 。 Fragment导致的界面重叠** 20 | 21 |   若你使用add(), show(), hide()方法进行Fragment的控制,当你将程序放在后台,多一段时间,会发现出现了问题:你的几个Fragment界面发生了重叠。 22 |   **原因:**  23 |   系统在页面重启前,帮我们保存了Fragment的状态,但是在重启后恢复时,视图的可见状态没帮我们保存,而Fragment默认的是show状态。这样就导致了再次启动进入系统会从栈底向栈顶的顺序恢复Fragments,并且每一个恢复的Fragment都是以show()的方式显示出来,从而导致界面的重叠。 24 | 25 |   **解决办法:** 26 |   FragmentManager在任何情况都会帮你存储Fragment,你要做的仅仅是在“内存重启”后,找回这些Fragment即可。 27 | 28 |   **1)通过findFragmentByTag** 29 |   在add Fragment的时候,绑定一个tag(一般都是使用Fragment的类名),发生内存重启的时候,通过findFragmentByTag找到对应的Fragment,并hide掉需要隐藏的Fragment(可以在Activity的onSaveInstance中保存需要显示的Fragment的tag值)。 30 | 31 | ``` 32 | @Overrideprotected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity); 35 | 36 | ShowFragment showFragment; 37 | HideFragment hideFragment; 38 | 39 | if (savedInstanceState != null) { 40 | showFragment = getSupportFragmentManager().findFragmentByTag(showFragment.getClass().getName); 41 | hideFragment = getSupportFragmentManager().findFragmentByTag(hideFragment.getClass().getName); 42 | getFragmentManager().beginTransaction() 43 | .show(showFragment) 44 | .hide(hideFragment) 45 | .commit(); 46 | }else{ // 正常时 47 | showFragment = ShowFragment.newInstance(); 48 | hideFragment = HideFragment.newInstance(); 49 | 50 | getFragmentManager().beginTransaction() 51 | .add(R.id.container, showFragment, showFragment.getClass().getName()) 52 | .add(R.id,container,hideFragment,hideFragment.getClass().getName()) 53 | .hide(hideFragment) 54 | .commit(); 55 | } 56 | } 57 | ``` 58 |   **2)通过Fragment自身的saveInstanceState** 59 |   Fragment像Activity一样,可以通过onSaveInstanceState来保存序列化的数据,以防止程序突然丢失数据(内存重启)。具体的通过onSaveInstanceState来实现防止Fragment重叠的方法如下所示: 60 | 61 | ``` 62 | @Override public void onCreate(@Nullable Bundle savedInstanceState) { 63 | super.onCreate(savedInstanceState); 64 | 65 | if (savedInstanceState != null) { 66 | mIsHidden = savedInstanceState.getBoolean(FragmentOpe.FRAGMENT_SAVE_STATE_HIDDEN); 67 | processRestoreState(); 68 | } 69 | } 70 | 71 | @Override public void onSaveInstanceState(Bundle outState) { 72 | super.onSaveInstanceState(outState); 73 | outState.putBoolean(FragmentOpe.FRAGMENT_SAVE_STATE_HIDDEN, isHidden()); 74 | } 75 | 76 | 77 | private void processRestoreState() { 78 | FragmentTransaction ft = getFragmentManager().beginTransaction(); 79 | if (mIsHidden) { 80 | ft.hide(this); 81 | } 82 | else { 83 | ft.show(this); 84 | } 85 | ft.commit(); 86 | } 87 | ``` 88 | 89 |   **方式1和的2区别是:由Activity/父Fragment来管理子Fragment的显示/隐藏状态转变为由Fragment自己来管理自己的显示/隐藏状态。** 90 | 91 | 92 | **4 。 Frament的出栈remove的使用问题** 93 | 94 |   FragmentManager提供了remove方法用于Fragment的出栈,但是这个方法是有问题的。 95 |   如下面的代码所示:我添加了两个Fragment到Activity容器中,当用户点击back按键的时候,我通过如下方法进行出栈操作,你会发现:getBackStackEntryCount()方法永远都是返回2个。但是通过日志你客可看到你的两个Fragment的onDetach方法都被调用了。 96 | 97 | ``` 98 | public void onBackPressed() { 99 | int count = mFragmentManager.getBackStackEntryCount(); 100 | Log.d(tag, "fragment count = " + count); 101 | if (count > 0) { 102 | String tag = mFragmentManager.getBackStackEntryAt(count - 1).getName(); 103 | Fragment fragment = mFragmentManager.findFragmentByTag(tag); 104 | FragmentTransaction ft = mFragmentManager.beginTransaction().remove(fragment); 105 | ft.commit(); 106 | } 107 | else { 108 | super.onBackPressed(); 109 | } 110 | } 111 | ``` 112 | 113 |   如上所示,正确的使用方法是调用popBackStack系列函数。remove函数仅仅是能让Fragment的视图从容器内移除。正确的回退实现: 114 | 115 | ``` 116 | public void onBackPressed() { 117 | int count = mFragmentManager.getBackStackEntryCount(); 118 | Log.d(tag, "fragment count = " + count); 119 | if (count > 0) { 120 | mFragmentManager.popBackStackImmediate(); 121 | } 122 | else { 123 | super.onBackPressed(); 124 | } 125 | } 126 | ``` 127 | 128 |   调用上面的方法进行出栈操作,你会发现:**getBackStackEntryCount()得到了正确的值。此处还需要注意的是:如果你调用FragmentMananger的getFragments()方法返回的List,你会发现他的size 129 | 不会变化。但是,List中的Fragment缺随着后退变为null**。 130 | 131 |   如果你觉着popBackStack系列函数是木有问题的, 那么你就错了。 132 | 133 | **5。 Fragment的popBackStack系列接口的使用问题** 134 | 135 |   popBackStack() 136 |   popBackStackImmediate() 137 |   popBackStack(String tag,int flags) 138 |   popBackStack(int id,int flags) 139 |   popBackStackImmediate(String tag,int flags) 140 |   popBackStackImmediate(int id,int flags) 141 | 142 |   Android总共提供了6个popBackStack()系列函数,区别如下: 143 |   a、前两个函数用于单个Fragment的出栈,后面四个用户多个Fragments的出栈; 144 |   b、popBackStack和popBackStackImmediate系列函数的区别在于:前者是加入到主线队列的末尾,等其它任务完成后才开始出栈,后者是立刻出栈。 145 | 146 |   **1)单个Fragment出栈时事物和动画的问题** 147 |   单个Fragment出栈,调用popBackStack()或者popBackStackImmediate()接口,但是如果你在调用这两个接口之后紧接着调用类似如下的事物方法,**此时如果出栈动画还没有完成**,会导致内存重启后按返回键报错的问题: 148 | 149 | 150 | ``` 151 | getSupportFragmentManager().popBackStackImmdiate(); 152 | getSupportFragmentManager().beginTransaction() 153 | .add(R.id.container, fragment , tag) 154 | .hide(currentFragment) 155 | .commit; 156 | ``` 157 | 158 |   正确的使用方法如下所示,使用主线程的Handler: 159 | 160 | ``` 161 | getSupportFragmentManager().popBackStackImmdiate(); 162 | new Handler().post(new Runnable(){ 163 | @Override 164 | public void run() { 165 | // 在这里执行Fragment事务 166 | } 167 | }); 168 | ``` 169 |   **需要注意的是:假如你的Fragment没有设置任何的动画,那么上面的问题是不存在的。** 170 | 171 |   **2)多个Fragment出栈的问题** 172 |   在进行多个Fragment的出栈接口中,popBackStack是有问题的,不建议使用,**推荐使用popBackStackImmediate系列接口**。 173 |   当你调用popBackStack系列的接口实现多个Fragment出栈时,进行多个出栈和入栈操作后,会导致栈内的Fragment顺序不正确的问题。stackoverflow上的大神给出了:[解决方案](http://stackoverflow.com/questions/25520705/android-cant-retain-fragments-that-are-nested-in-other-fragments),具体的如下所示: 174 | 175 | ``` 176 | public class FragmentTransactionBugFixHack { 177 | 178 | public static void reorderIndices(FragmentManager fragmentManager) { 179 | if (!(fragmentManager instanceof FragmentManagerImpl)) 180 | return; 181 | FragmentManagerImpl fragmentManagerImpl = (FragmentManagerImpl) fragmentManager; 182 | if (fragmentManagerImpl.mAvailIndices != null && fragmentManagerImpl.mAvailIndices.size() > 1) { 183 | Collections.sort(fragmentManagerImpl.mAvailIndices, Collections.reverseOrder()); 184 | } 185 | } 186 | } 187 | ``` 188 | 189 |   怎么使用呢? 190 |   在你调用popBackStackImmediate出栈多个Fragment之后,调用reorderIndices方法就可以。具体的使用方法如下所示,必须在主线程总调用: 191 | 192 | ``` 193 | hanler.post(new Runnable(){ 194 | @Override 195 | public void run() { 196 | FragmentTransactionBugFixHack.reorderIndices(fragmentManager)); 197 | } 198 | }); 199 | ``` 200 |   为了防止出栈动画带来的问题,在调用reorderIndices接口之前,最好获取一下动画动画的时间,调动postDelay来进行reorderIndices操作。 201 | 202 | 203 | **6 。 Fragment转场动画的问题** 204 |   Activity的转场动画通过overridePendingTransition(int enterAnim, int exitAnim)就可以实现。 205 |   Fragment的动画分为出栈动画和入栈动画。FragmentManager提供了setCustomAnimations(enter, exit)接口来设置入栈动画:第一个参数,需要注意的是:第二个参数并不是出栈动画。当你设置了出栈动画,好像根本就不起作用。 206 | 207 |   那么怎么实现Fragment的入栈、出栈动画呢? 208 |   通过Fragment的setTransition接口,并且继承实现Fragment的onCreateAnimation()接口,具体如下所示: 209 | 210 |   **Ativity中启动FragmentA:** 211 | 212 | ``` 213 | FragmentTransaction ft = mFragmentManager.beginTransaction(); 214 | ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 215 | ft.add(R.id.fl_container, FragmentDemo.newInstance(), FragmentDemo.class.getName()); 216 | ft.addToBackStack(FragmentDemo.class.getName()); 217 | ft.commit(); 218 | ``` 219 | 220 |   **FragmentA启动FragmentB:** 221 | 222 | ``` 223 | FragmentManager fragmentManager = getFragmentManager(); 224 | FragmentTransaction ft = fragmentManager.beginTransaction(); 225 | ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 226 | ft.add(R.id.fl_container, FragmentDemo2.newInstance(), 227 | FragmentDemo2.class.getName()); 228 | ft.hide(FragmentDemo.this); 229 | ft.addToBackStack(FragmentDemo2.class.getName()); 230 | ft.commit(); 231 | ``` 232 | 233 |   **Fragment继承实现onCreateAnimation接口:** 234 | 235 | ``` 236 | @Override public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { 237 | Log.d(tag, "onCreateAnimation transit = " + transit + "; enter = " + enter); 238 | if (transit == FragmentTransaction.TRANSIT_FRAGMENT_OPEN) { 239 | if (enter) { 240 | return mEnterAnim; 241 | } 242 | else { 243 | return mPopExitAnim; 244 | } 245 | } 246 | else if (transit == FragmentTransaction.TRANSIT_FRAGMENT_CLOSE) { 247 | if (enter) { 248 | return mPopEnterAnim; 249 | } 250 | else { 251 | return mExitAnim; 252 | } 253 | } 254 | 255 | return super.onCreateAnimation(transit, enter, nextAnim); 256 | } 257 | ``` 258 | 259 |   **Activity的onBackPressed:** 260 | 261 | ``` 262 | int count = mFragmentManager.getBackStackEntryCount(); 263 | Log.d(tag, "fragment count = " + count); 264 | if (count > 0) { 265 | mFragmentManager.popBackStackImmediate(); 266 | } 267 | else { 268 | super.onBackPressed(); 269 | } 270 | ``` 271 | 272 |   上述代码的使用场景:Activity启动FragmentA,FragmentA启动Fragment 273 | B,然后按返回键知道Activity销毁。 onCreateAnimation日志信息如下: 274 |   **FragmentA启动时: 275 |   onCreateAnimation transit = 4097; enter = true //A执行mEnterAnim动画 276 |   FragmentA启动FragmentB时,Fragment hihe: 277 |   onCreateAnimation transit = 4097; enter = false//A执行PopExitAnim动画 278 |   onCreateAnimation transit = 4097; enter = true//B执启动mEnterAnim动画 279 |   按返回键第一次: 280 |   onCreateAnimation transit = 8194; enter = true//A执行mPopEnterAnim动画 281 |   onCreateAnimation transit = 8194; enter = false//B执行mExitAnim动画 282 |   按返回键第二次: 283 |   onCreateAnimation transit = 8194; enter = false//A执行mExitAnim动画** 284 | 285 |   通过上面日志的打印信息,我们就可以知道应该怎样使用Fragment的转场动画。 286 | 287 | 288 | **7 。 Fragment与Fragment/Activity之间的数据传输** 289 | 290 |   **a. 通过getctivity和findFragmentByTag/findFragmentById** 291 |   尽管Fragment是独立于Activity的一个对象,但是Fragment可以通过getActivity()方法获取Activity的实例,这样就可以执行Activity中的公有方法了。 292 |   同样的,Activity可以通过FragmentManager获取一个引用来调用Fragment中的方法,使用findFragmentByTag()或者findFragmentById();  293 | 294 |   **b. 通过interface接口** 295 |   在Fragment中: 296 | 297 | ``` 298 | public class FragmentDemo extends Fragment { 299 | 300 | public interface OnComListener { 301 | public void onComListener(); 302 | } 303 | 304 | OnComListener mListener; 305 | 306 | @Override public void onAttach(Context context) { 307 | super.onAttach(context); 308 | try { 309 | mListener = (MainActivity) context; 310 | } catch (ClassCastException e) { 311 | throw new ClassCastException(""); 312 | } 313 | } 314 | } 315 | ``` 316 | 317 |   在Activity中: 318 | 319 | ``` 320 | public class MainActivity extends AppCompatActivity implements FragmentDemo.OnComListener { 321 | 322 | @Override public void onComListener() { 323 | } 324 | } 325 | ``` 326 |   这样你就可以通过mListener在Fragment中调用Activity的接口了。同理,在Activity中也可以调用Fragment的方法。 327 | 328 |   **再进一步**: 通过Interface的方法可以实现Fragment与Fragment之间的通信,但是必须借助Activity进行中转,或者综合使用Interface和findFragmentByTag()方法实现Fragment通信,也必须通过Activity中转。 329 | 330 |   **c. 通过startActivityForResult()** 331 |   Fragment中启动Activity:在support 23.2.0以下的支持库中,对于在嵌套子Fragment的startActivityForResult(),会发现无论如何都不能在onActivityResult()中接收到返回值,只有最顶层的父Fragment才能接收到,这是一个support v4库的一个BUG,不过在最新发布的support 23.2.0库中,已经修复了该问题,嵌套的子Fragment也能正常接收到返回数据了! 332 | 333 |   其实在Fragment中可以自定义这样的方法:FragmentA启动FragmengB,FragmentB回退的时候讲内容返回给FragmentA。 下面我们就实现这个方法。 334 | 335 | 336 | **8 。 Fragment栈内容的查看** 337 | 338 |   每个Fragment以及容器Activiy都会在创建时初始化一个FragmentManager对象。 339 | 340 |   **a.** 对于容器Activity,通过FragmentManager可以查看Activity作为容器的Fragment。 341 | 342 | ``` 343 | List fragmentList = mActivity.getSupportFragmentManager().getFragments(); 344 | ``` 345 |   **b.** 对于容器Fragment,通过Fragment的getChildFragmentManager可以查看Fragment作为容器的FragmentManager对象 346 | 347 | ``` 348 | List fragmentList = fragment.getChildFragmentManager().getFragments(); 349 | ``` 350 | 351 |   **c.** 对于容器Fragment,通过getFragmentManager是获取的父Fragment(若没有,获取的是Activity容器)的FragmentManager对象。 352 | 353 |   通过查看Fragment的栈关系就可以知道自己的程序是不是出现错误,有没有内存泄露的风险等。 354 | 355 | **9 。懒加载(延迟加载)技术** 356 | 357 |   我们在做应用开发的时候,如果你的activity里可能会用到多个Fragment,比如微信的首页有四个Fragment。如果每个Fragment都需要去加载数据资源(from本地或者network),那么这个Activity在创建的时候需要初始化大量的资源。正确的做法应该是当切换到这个Fargment的时候,采取初始化。 358 |   以实际实现微信主页UI的为例实现懒加载: 359 |   在本次处理中我们将微信的几个主页面都加在出来,并且通过show hide接口控制,仅仅让一个界面显示出来。具体的如下所示: 360 | 361 | ``` 362 | FragmentTransaction ft = fragmentManager.beginTransaction(); 363 | ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 364 | 365 | for (int i = 0; i < fragments.length; i++) { 366 | BaseFragment fragment = fragments[i]; 367 | if (fragment == null) { 368 | throw new IllegalArgumentException( 369 | "loadMultipleFragments fragment in list can not be null "); 370 | } 371 | String tag = fragment.getClass().getName(); 372 | ft.add(containerId, fragment, tag); 373 | if (showPos != i) { //showPos为我们需要显示的Fragment 374 | ft.hide(fragment); 375 | } 376 | } 377 | ft.commit(); 378 | ``` 379 |   Fragment的加载时的生命周期为:onAttach-onCreate-onCreateView-onActivityCreated。我们可以在onActivityCreated真判断当前的Fragment是否已为Hidden,如果不为Hidden,我们就可以加载Fragment。 380 | 381 | 382 | ``` 383 | if (!isHidden()) { 384 | init(null); 385 | } 386 | ``` 387 |   当用户点击Tab也进行切换的时候,之前家加载过(之前可能是show也可能是hide状态)的Fragment会调用onHiddenChanged接口。我们就可以在这个接口中进行懒加载: 388 | 389 | ``` 390 | public abstract void initLazyView(Bundle saveInstanceState); 391 | public void init(Bundle saveInstanceState) { 392 | initLazyView(saveInstanceState); 393 | } 394 | ``` 395 | 396 | 397 | 398 | **10 。防止多次点击加载Fragment** 399 | 400 |   问题的背景是这样的,有时候你会一个按钮点击了两次,导致Fragment被加载了两次,这样就会造成Fragment的重叠。这样子肯定是开发者和用户都不愿意看到的。如何解决这个问题呢? 401 |   解决的方法很多:你可以使用RxJava等第三方的开源库进行处理。你也可以进行封装处理。 402 | 403 | 404 | **11 。onBackPressed的封装和使用** 405 |   我们都知道Fragment是不会相应onBackPressed接口的,只有Activity才可以。那么我们就需要在Activity的onBackPressed中进行相应的逻辑处理: 406 | 407 | 408 | ``` 409 | private void handleBackPressed() { 410 | if (getSupportFragmentManager().getBackStackEntryCount() > 1) { 411 | getSupportFragmentManager().popBackStack(); 412 | } 413 | else { 414 | finish(); 415 | } 416 | } 417 | ``` 418 |   在onBackPressed中调用上面的实现就可以。但是上面只是实现了出栈。如果你在某一个Fragment中需要进行回退时的逻辑处理,你需要判断你的Fragment是否支持BackPress。    419 | 420 | ``` 421 | public boolean onBackPressedSupport() { 422 | return false; 423 | } 424 | 425 | ``` 426 | 427 |   然后在你的Activity的onBackPressed中这样实现: 428 | 429 | ``` 430 | @Override public void onBackPressed() { 431 | 432 | BaseFragment frontFragment = mFragmentUtil.getActiveFragment(null, 433 | getSupportFragmentManager()); //获取栈顶部的Fragment 434 | 435 | Log.d(tag, "onBackPressed fragment = " + frontFragment.getTag()); 436 | 437 | if (dispatchBackPressedEvent(frontFragment)) {//判断Fragment是否支持BackPressed 438 | return; 439 | } 440 | handleBackPressed(); //出栈 441 | } 442 | ``` 443 |   首先获取栈顶的Fragment,然后以判断Fragment是否支持onBackPressedSupport,如果支持进行自己 Fragment内部逻辑处理,否则执行Fragment的出栈操作 444 | 445 | --------------------------------------------------------------------------------