iOS初始化UIWindow并且设置级别

在自从Xcode6以来,我们已经很少接触Window这个概念了。但是还是有很多iOS程序员喜欢。今天我们就用OC代码来简单了解WIndow。

(1)首先创建一个OC语言的项目,看到AppDelegate.h中:系统首先默认给我们创建了一个UIWindow对象。

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

 

(2)在AppDelegate.m中,*个方法的定义如下:
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
_window.backgroundColor = [UIColor grayColor];//设置背景颜色;
[_window makeKeyAndVisible];//设置主界面并可见;

_window.windowLevel = UIWindowLevelNormal;

return YES;
}

(3)运行程序,如下:

%title插图%num

(4)屏幕旋转快捷键,左旋:command+方向左键。 command+方向右键。

LeakCanary 中文使用说明

 


LeakCanary

Android 和 Java 内存泄露检测。

“A small leak will sink a great ship.” – Benjamin Franklin

千里之堤, 毁于蚁穴。 — 《韩非子·喻老》

%title插图%num

demo

一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo

开始使用

在 build.gradle 中加入引用,不同的编译使用不同的引用:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
 }

在 Application 中:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

这样,就万事俱备了! 在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。

为什么需要使用 LeakCanary?

问得好,看这个文章LeakCanary: 让内存泄露无所遁形

如何使用

使用 RefWatcher 监控那些本该被回收的对象。

RefWatcher refWatcher = {...};

// 监控
refWatcher.watch(schrodingerCat);

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = LeakCanary.install(this);
  }
}

使用 RefWatcher 监控 Fragment:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

工作机制

  1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
  2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
  3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
  4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
  5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
  6. HeapAnalyzer 计算 到 GC roots 的*短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
  7. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

如何复制 leak trace?

在 Logcat 中,你可以看到类似这样的 leak trace:

In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:

* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$3.this$0 (anonymous class extends android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance

* Reference Key: e71f3bf5-d786-4145-8539-584afaecad1d
* Device: Genymotion generic Google Nexus 6 - 5.1.0 - API 22 - 1440x2560 vbox86p
* Android Version: 5.1 API: 22
* Durations: watch=5086ms, gc=110ms, heap dump=435ms, analysis=2086ms

你甚至可以通过分享按钮把这些东西分享出去。

SDK 导致的内存泄露

随着时间的推移,很多SDK 和厂商 ROM 中的内存泄露问题已经被尽快修复了。但是,当这样的问题发生时,一般的开发者能做的事情很有限。

LeakCanary 有一个已知问题的忽略列表,AndroidExcludedRefs.java,如果你发现了一个新的问题,请提一个 issue 并附上 leak trace, reference key, 机器型号和 SDK 版本。如果可以附带上 dump 文件的 链接那就再好不过了。

对于*新发布的 Android,这点尤其重要。你有机会在帮助在早期发现新的内存泄露,这对整个 Android 社区都有*大的益处。

开发版本的 Snapshots 包在这里: Sonatype’s snapshots repository。

leak trace 之外

有时,leak trace 不够,你需要通过 MAT 或者 YourKit 深挖 dump 文件。

通过以下方法,你能找到问题所在:

  1. 查找所有的 com.squareup.leakcanary.KeyedWeakReference 实例。
  2. 检查 key 字段
  3. Find the KeyedWeakReference that has a key field equal to the reference key reported by LeakCanary.
  4. 找到 key 和 和 logcat 输出的 key 值一样的 KeyedWeakReference
  5. referent 字段对应的就是泄露的对象。
  6. 剩下的,就是动手修复了。*好是检查到 GC root 的*短强引用路径开始。

自定义

UI 样式

DisplayLeakActivity 有一个默认的图标和标签,你只要在你自己的 APP 资源中,替换以下资源就可。

res/
  drawable-hdpi/
    __leak_canary_icon.png
  drawable-mdpi/
    __leak_canary_icon.png
  drawable-xhdpi/
    __leak_canary_icon.png
  drawable-xxhdpi/
    __leak_canary_icon.png
  drawable-xxxhdpi/
    __leak_canary_icon.png
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="__leak_canary_display_activity_label">MyLeaks</string>
</resources>

保存 leak trace

DisplayLeakActivity saves up to 7 heap dumps & leak traces in the app directory. You can change that number by providing R.integer.__leak_canary_max_stored_leaks in your app:

在 APP 的目录中,DisplayLeakActivity 保存了 7 个 dump 文件和 leak trace。你可以在你的 APP 中,定义 R.integer.__leak_canary_max_stored_leaks 来覆盖类库的默认值。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <integer name="__leak_canary_max_stored_leaks">20</integer>
</resources>

上传 leak trace 到服务器

你可以改变处理完成的默认行为,将 leak trace 和 heap dump 上传到你的服务器以便统计分析。

创建一个 LeakUploadService, *简单的就是继承 DisplayLeakService :

public class LeakUploadService extends DisplayLeakService {
  @Override
  protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
    if (!result.leakFound || result.excludedLeak) {
      return;
    }
    myServer.uploadLeakBlocking(heapDump.heapDumpFile, leakInfo);
  }
}

请确认 release 版本 使用 RefWatcher.DISABLED

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context.getApplicationContext();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate() {
    super.onCreate();
    refWatcher = installLeakCanary();
  }

  protected RefWatcher installLeakCanary() {
    return RefWatcher.DISABLED;
  }
}

自定义 RefWatcher

public class DebugExampleApplication extends ExampleApplication {
  protected RefWatcher installLeakCanary() {
    return LeakCanary.install(app, LeakUploadService.class);
  }
}

别忘了注册 service:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >
  <application android:name="com.example.DebugExampleApplication">
    <service android:name="com.example.LeakUploadService" />
  </application>
</manifest>
%title插图%num

iOS开发·UIWindow与视图层级调整技巧

iOS开发·UIWindow与视图层级调整技巧(makeKeyWindow,resignKeyWindow,makeKeyAndVisible,keyWindow,windowLevel,UIWindow
iOS开发过程中,多人开发或者导入第三方框架的时候,可能碰到UIWindow层级冲突的问题。

例如,很多人习惯在keyWindow上添加一个自定义浮层视图,但是,当自己或者其它第三方框架曾经调高过其它自定义UIWindow属性windowLevel,或者有其它同级windowLevel的UIWindow后来改变过显示状态(如.hidden=NO,makeKeyAndVisible等),而且又*没有* 设将其设置为keyWindow,结果导致正在显示的UIWindow不是keyWindow,从而导致添加到keyWindow上自定义视图无法显示(被覆盖了)。

如何查看App的UIWindow层级

一. 为App初始化一个默认UIWindow对象
在AppDelegate.m中需要初始化一个window属性,作为后面往App添加视图的容器

1. 初始化操作写在如下UIApplicationDelegate代理方法中
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

 

2. 一个初始化window操作示例如下,具体根据产品需求设置
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];

 

3. 接下来需指定window的rootViewController
//判断该用首次欢迎页or密码登录页等等
if ([CommonUtils isStringNilOrEmpty:[CommonUtils getStrValueInUDWithKey:kIsGuide]]) {
[CommonUtils saveStrValueInUD:@”1″ forKey:kGuide];
// 欢迎页置为rootViewController
self.window.rootViewController = [[WHGuidePagesController alloc]initWithNibName:@”WHGuidePagesController” bundle:nil];
}else{
// 检查各种密码决定登录方式,并分别设置rootViewController
[self chooseVerifyMethod];
}

 

4. 注意点:rootViewController属性
目前只有UIWindow**有*rootViewController这个属性*,不要跟UINavigationController里面的根视图概念混淆。
UINavigationController其实*并没有* rootViewController这个属性!也就没有自带的setter方法。要设置其根视图只能通过如下方法,而不能通过属性的setter方法和点语法设置根视图。
– (instancetype)initWithRootViewController:(UIViewController *)rootViewController;
// Convenience method pushes the root view controller without animation.

 

5. 大多数APP的视图层级关系(以有底部TabBar的App为例)
1). [UIApplication sharedApplication].keyWindow为UIWindow对象。比如,获取APP的keyWindow并往上添加视图的代码:
[[UIApplication sharedApplication].keyWindow addSubview:self.signView];

 

2). 假设APP的keyWindow对象为uiWindow,则uiWindow.rootViewController为UITabBarController对象(也只有UIWindow可以用点语法设置根视图)。比如,为设置rootViewController代码:
self.window.rootViewController = customTabBarVC;//AppDelegate.m里面

 

3). UITabBarController对象的viewControllers包含UINavigationController对象。设置其viewControllers的方法:
– (void)setViewControllers:(NSArray<__kindof UIViewController *> * __nullable)viewControllers animated:(BOOL)animated;

 

4). UINavigationController对象的rootViewController为UIViewController对象。初始化其rootViewController的方法为:
– (instancetype)initWithRootViewController:(UIViewController *)rootViewController;

 

6. 获取keyWindow(它并不一定是当前*上层显示的window)的rootViewController
可以通过如下方法找到当前UIWindow的rootViewController,前提是当keyWindow真的显示在*上层。

#pragma mark – 获取根视图的(导航、标签)视图控制器
+ (UINavigationController *)getRootVCformViewController
{
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UINavigationController *nav = nil;
if ([rootVC isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabbar = (UITabBarController *)rootVC;
NSInteger index = tabbar.selectedIndex;
nav = tabbar.childViewControllers[index];
}else if ([rootVC isKindOfClass:[UINavigationController class]]) {
nav = (UINavigationController *)rootVC;
}else if ([rootVC isKindOfClass:[UIViewController class]]) {
NSLog(@”This no UINavigationController…”);
}
return nav;
}

 

二. 在自定义的UIWindow添加自定义视图
假设想为一个APP添加一个手势验证的页面,当进入APP弹出这个手势验证页面。如果不想影响原来的UIWindow,可以考虑新建一个UIWindow并覆盖原来的UIWindow,并往新建的UIWindow上添加各种手势相关的视图及控制器。但在手势验证完后,务必销毁这个自定义的UIWindow,否则可能导致看不见的UIWindow越积越多。

1. 自定义UIWindow
_window = [[UIWindow alloc]initWithFrame:[[UIScreen mainScreen] bounds]];
_window.hidden = NO;
[self.window makeKeyAndVisible];

 

2. 指定自定义视图控制器
UIViewController *vc = [[UIViewController alloc]init];
_window.rootViewController = vc;

3. 销毁自定义UIWindow
自定义视图用完后,记得要销毁自定义的UIWindow,否则导致APP以后会有越来越多没用到的UIWindow,即使再也没有显示过它们,但是可以用调试工具看到许多废弃的window。可参考方法如下

– (void)dismiss {

[self.window resignKeyWindow];
self.window.windowLevel = -1000;
self.window.hidden = YES;
[self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];

self.window = nil;
}

三. UIWindow的显示特性
1. 相同windowLevel下,调整UIWindow显示层的基本方法
1). 显示相关属性:hidden
如果仅仅想显示一个UIWindow
customWindow.hidden = NO;

PS: 虽然设置自己的hidden即可显示出来,但上述方法并不会”自动”影响之前显示的UIWindow对象的hidden属性。如果,之前UIWindow的hidden = NO,设置新UIWindow的hidden将旧UIWindow覆盖后,旧UIWindow的hidden属性依旧为NO。

如果仅仅想隐藏一个UIWindow
customWindow.hidden = YES;

PS: 如果你没有专门设置过hidden属性,系统默认为YES。上述代码会将UIWindow*对隐藏,不管有没其他UIWindow覆盖。当也没有其它非隐藏的UIWindow的时候,APP屏幕完全黑屏。

如果想显示一个UIWindow,同时设置为keyWindow,并将其显示在同一windowLevel的其它任何UIWindow之上
– (void)makeKeyAndVisible

PS: 上述方法真的会将其显示在同一windowLevel的其它任何UIWindow之上!显示*上层的UIWindow以*后执行过该代码的UIWindow为准。

2). 显示相关方法:makeKeyAndVisible的作用
[self.window makeKeyAndVisible];

其执行效果包括 *但不限于* 执行了如下代码(因为还会覆盖同level的所有window):

[self.window makeKeyWindow];
self.window.hidden = NO;

讲真,makeKeyAndVisible真的会自动改变hidden属性值为NO。

3). UIWindow对象的hidden属性默认值
默认值:YES
PS:如果你仅仅创建一个UIWindow,而又不专门设置hidden属性(或者makeKeyAndVisible),系统默认分配的默认值为true。例如,我们把影响到hidden属性的方法屏蔽掉:

self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
// [self.window makeKeyAndVisible];
// [self.window makeKeyWindow];
// self.window.hidden = NO;

再来打印hidden属性如下:

po self.window.isHidden
true

4). 误区:关于keyWindow的混淆易错点
设置keyWindow与否并*不* 影响视图层级*显示*,仅来接收键盘及其它非触摸事件。如果没有专门设置过keyWindow的hiden为NO,而且也没有其它非隐藏的UIWindow,那么APP会黑屏。

如果仅仅设置为keyWindow
– (void)makeKeyWindow

如果仅仅解除为keyWindow
– (void)resignKeyWindow

app的keyWindow与是否在*上层显示没有任何关系。比如,你如果想通过[[UIApplication sharedApplication] keyWindow]获取正在显示的UIWindow是**其不准确* 的。有时候通过这个代码获取的如果真的是正在显示的UIWindow,仅仅是因为碰巧而已。

5). 警惕点:有多个hidden属性=NO的UIWindow,该显示谁?
如上所见,makeKeyAndVisible与hidden的setter方法均可以改变hidden的值,但有个问题,经过多次调整,可能有多个UIWindow的hidden都为NO,那么应该显示谁?

对于hidden的setter方法,*终显示的以**后* 执行过 *.hidden=NO* 的UIWindow为准,且执行 *.hidden=NO* 之前hidden的值为YES。(hidden如果是从NO改为NO的*不* 算 **后* 改变UIWindow的显示状态)
对于makeKeyAndVisible方法,*终显示的以**后* 执行过 *makeKeyAndVisible* 的UIWindow为准。
对于先后分别用makeKeyAndVisible方法和hidden的setter方法,还是先后分别用hidden的setter方法和makeKeyAndVisible方法,结局同样以*后改变显示状态的UIWindow为准。
2. 基于windowLevel,调整UIWindow显示层的拓展方法
先去UIWindow.h里面看看UIWindowLevel的定义:

typedef CGFloat UIWindowLevel;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert;
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar __TVOS_PROHIBITED;

例如,在手势相关类中调整自定义的UIWindow层级

[self.window makeKeyAndVisible];
_window.windowLevel = UIWindowLevelAlert;

打印代表UIWindowLevelAlert层级的数据值
(lldb) po self.window.windowLevel
2000

同理,打印代表UIWindowLevelStatusBar层级的数据值
(lldb) po self.window.windowLevel
1000

 

同理,打印代表UIWindowLevelNormal层级的数据值
(lldb) po self.window.windowLevel

*小结:*

windowLevel数值越大的显示在窗口栈的越上面
*显示层的优先级* 为: UIWindowLevelAlert > UIWindowLevelStatusBar > UIWindowLevelNormal
系统给UIWindow默认的windowLevel为UIWindowLevelNormal
%title插图%num

Xcode查看窗口栈

四. UIWindow常见操作方法总结
1. 获取App所有window的windows数组
[[UIApplication sharedApplication] windows]

例如,第三方加载动画框架KVNProcess中KVNProgress.m文件会有一段这样的代码:

– (void)addToCurrentWindow
{
UIWindow *currentWindow = nil;

NSEnumerator *frontToBackWindows = [[[UIApplication sharedApplication] windows] reverseObjectEnumerator];

for (UIWindow *window in frontToBackWindows) {
if (window.windowLevel == UIWindowLevelNormal) {
currentWindow = window;
break;
}
}

if (self.superview != currentWindow) {
[self addToView:currentWindow];
}
}

 

2. keyWindow
[[UIApplication sharedApplication] keyWindow]

例如,第三方下拉菜单框架FFDropDownMenu的FFDropDownMenuView.m文件中有这样一段代码:

UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[keyWindow addSubview:self];

这段代码的目的是添加到*上层UIWindow,但实际操作是把自己的视图添加到keyWindow上。其实,如果我们在编写代码时严谨地保证keyWindow是显示在*上层的UIWindow,这样写没有问题。但如果:自己或者其它第三方框架曾经调高过其它UIWindow属性windowLevel,或者有同级windowLevel的其它UIWindow后来改变过显示状态(如.hidden=NO,makeKeyAndVisible等),可能会导致下拉菜单的弹出视图无法显示(被覆盖)。

3. 获取AppDelegate单例的window属性
专门获取AppDelegate.m文件中的window属性,不包含其它其定义的window

[[[UIApplication sharedApplication] delegate] window]

 

拓展一下,获取AppDelegate单例的方法为

+ (AppDelegate *)sharedDelegate
{
return (AppDelegate *)[[UIApplication sharedApplication] delegate];
}

 

附. 调试打印例子
启动APP,AppDelegate.m中的window属性
(lldb) po self.window
<UIWindow: 0x15fd24390; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1700567a0>; layer = <UIWindowLayer: 0x170233700>>

跳转手势,GestureScreen.m中的window属性
(lldb) po _window
<UIWindow: 0x15fd29160; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x170057f70>; layer = <UIWindowLayer: 0x1702345c0>>

此时,可查看所有window
(lldb) po [[UIApplication sharedApplication] windows]
<__NSArrayM 0x17405c290>(
<UIWindow: 0x15fd24390; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1700567a0>; layer = <UIWindowLayer: 0x170233700>>,
<UIWindow: 0x15fd29160; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x170057f70>; layer = <UIWindowLayer: 0x1702345c0>>
)

此时,断点在手势相关类中,也可专门查看AppDelegate.m中的window属性:假设UIWindow *delegateWindow = [[[UIApplication sharedApplication] delegate] window]; 打印如下
(lldb) po delegateWindow
<UIWindow: 0x15fd24390; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x1700567a0>; layer = <UIWindowLayer: 0x170233700>>

 

Android内存优化总结&实践-腾讯Bugly干货分享

 

导语

智能手机发展到今天已经有十几个年头,手机的软硬件都已经发生了翻天覆地的变化,特别是Android阵营,从一开始的一两百M到今天动辄4G,6G内存。然而大部分的开发者观看下自己的异常上报系统,还是会发现各种内存问题仍然层出不穷,各种OOM为crash率贡献不少。Android开发发展到今天也是已经比较成熟,各种新框架,新技术也是层出不穷,而内存优化一直都是Android开发过程一个不可避免的话题。 恰好*近做了内存优化相关的工作,这里也对Android内存优化相关的知识做下总结。

在开始文章之前推荐下公司同事翻译整理版本《Android性能优化典范 – 第6季》,因为篇幅有限这里我对一些内容只做简单总结,同时如果有不正确内容也麻烦帮忙指正。

本文将会对Android内存优化相关的知识进行总结以及*后案例分析(一二部分是理论知识总结,你也可以直接跳到第三部分看案例):

一、 Android内存分配回收机制
二 、Android常见内存问题和对应检测,解决方式。
三、 JOOX内存优化案例
四 、总结

工欲善其事必先利其器,想要优化App的内存占用,那么还是需要先了解Android系统的内存分配和回收机制。

一 ,Android内存分配回收机制

参考Android 操作系统的内存回收机制[1],这里简单做下总结:

从宏观角度上来看Android系统可以分为三个层次
1. Application Framework,
2. Dalvik 虚拟机
3. Linux内核。

这三个层次都有各自内存相关工作:

1. Application Framework

Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:

%title插图%num

  • Empty process(空进程)
  • Background process(后台进程)
  • Service process(服务进程)
  • Visible process(可见进程)
  • Foreground process(前台进程)

系统需要进行内存回收时*先回收空进程,然后是后台进程,以此类推*后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。

由此也衍生了很多进程保活的方法(提高优先级,互相唤醒,native保活等等),出现了国内各种全家桶,甚至各种杀不死的进程。

Android中由ActivityManagerService 集中管理所有进程的内存资源分配。

2. Linux内核

%title插图%num

参考QCon大会上阿里巴巴的Android内存优化分享[2],这里*简单的理解就是ActivityManagerService会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收(lowmemorykillerOom_killer)。这里只是大概的流程,中间过程还是很复杂的,有兴趣的同学可以一起研究,代码在系统源码ActivityManagerService.Java中。

3. Dalvik虚拟机

Android进程的内存管理分析[3],对Android中进程内存的管理做了分析。

Android中有Native Heap和Dalvik Heap。Android的Native Heap言理论上可分配的空间取决了硬件RAM,而对于每个进程的Dalvik Heap都是有大小限制的,具体策略可以看看android dalvik heap 浅析[4]。

Android App为什么会OOM呢?其实就是申请的内存超过了Dalvik Heap的*大值。这里也诞生了一些比较”黑科技”的内存优化方案,比如将耗内存的操作放到Native层,或者使用分进程的方式突破每个进程的Dalvik Heap内存限制。

Android Dalvik Heap与原生Java一样,将堆的内存空间分为三个区域,Young Generation,Old Generation, Permanent Generation。

%title插图%num

*近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,*后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。

GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是*短的,Old Generation其次,Permanent Generation*长。

GC时会导致线程暂停,导致卡顿,Google在新版本的Android中优化了这个问题, 在ART中对GC过程做了优化揭秘 ART 细节 —— Garbage collection[5],据说内存分配的效率提高了10倍,GC的效率提高了2-3倍(可见原来效率有多低),不过主要还是优化中断和阻塞的时间,频繁的GC还是会导致卡顿。

上面就是Android系统内存分配和回收相关知识,回过头来看,现在各种手机厂商鼓吹人工智能手机,号称18个月不卡顿,越用越快,其实很大一部分Android系统的内存优化有关,无非就是利用一些比较成熟的基于统计,机器学习的算法定时清理数据,清理内存,甚至提前加载数据到内存。

二 ,Android常见内存问题和对应检测,解决方式

1. 内存泄露

不止Android程序员,内存泄露应该是大部分程序员都遇到过的问题,可以说大部分的内存问题都是内存泄露导致的,Android里也有一些很常见的内存泄露问题[6],这里简单罗列下:

  • 单例(主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放)
  • 静态变量(同样也是因为生命周期比较长)
  • Handler内存泄露[7]
  • 匿名内部类(匿名内部类会引用外部类,导致无法释放,比如各种回调)
  • 资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)

对Android内存泄露业界已经有很多优秀的组件其中LeakCanary*为知名(Square出品,Square可谓Android开源界中的业界良心,开源的项目包括okhttp, retrofit,otto, picasso, Android开发大神Jake Wharton就在Square),其原理是监控每个activity,在activity ondestory后,在后台线程检测引用,然后过一段时间进行gc,gc后如果引用还在,那么dump出内存堆栈,并解析进行可视化显示。使用LeakCanary可以快速地检测出Android中的内存泄露。

正常情况下,解决大部分内存泄露问题后,App稳定性应该会有很大提升,但是有时候App本身就是有一些比较耗内存的功能,比如直播,视频播放,音乐播放,那么我们还有什么能做的可以降低内存使用,减少OOM呢?

2. 图片分辨率相关

分辨率适配问题。很多情况下图片所占的内存在整个App内存占用中会占大部分。我们知道可以通过将图片放到hdpi/xhdpi/xxhdpi等不同文件夹进行适配,通过xml android:background设置背景图片,或者通过BitmapFactory.decodeResource()方法,图片实际上默认情况下是会进行缩放的。在Java层实际调用的函数都是或者通过BitmapFactory里的decodeResourceStream函数

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个*佳的显示效果,并且Bitmap的大小将比原始的大,可以参考下腾讯Bugly的详细分析Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?。

关于Density、分辨率、-hdpi等res目录之间的关系:

%title插图%num

举个例子,对于一张1280×720的图片,如果放在xhdpi,那么xhdpi的设备拿到的大小还是1280×720而xxhpi的设备拿到的可能是1920×1080,这两种情况在内存里的大小分别为:3.68M和8.29M,相差4.61M,在移动设备来说这几M的差距还是很大的。

尽管现在已经有比较先进的图片加载组件类似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,但是有时就是需要手动拿到一个bitmap或者drawable,特别是在一些可能会频繁调用的场景(比如ListView的getView),怎样尽可能对bitmap进行复用呢?这里首先需要明确的是对同样的图片,要 尽可能复用,我们可以简单自己用WeakReference做一个bitmap缓存池,也可以用类似图片加载库写一个通用的bitmap缓存池,可以参考GlideBitmapPool[8]的实现。

我们也来看看系统是怎么做的,对于类似在xml里面直接通过android:background或者android:src设置的背景图片,以ImageView为例,*终会调用Resource.java里的loadDrawable:

Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {

    // Next, check preloaded drawables. These may contain unresolved theme
    // attributes.
    final ConstantState cs;
    if (isColorDrawable) {
        cs = sPreloadedColorDrawables.get(key);
    } else {
        cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
    }

    Drawable dr;
    if (cs != null) {
        dr = cs.newDrawable(this);
    } else if (isColorDrawable) {
        dr = new ColorDrawable(value.data);
    } else {
        dr = loadDrawableForCookie(value, id, null);
    }

    ...

    return dr;
}  

可以看到实际上系统也是有一份全局的缓存,sPreloadedDrawables, 对于不同的drawable,如果图片时一样的,那么*终只会有一份bitmap(享元模式),存放于BitmapState中,获取drawable时,系统会从缓存中取出这个bitmap然后构造drawable。而通过BitmapFactory.decodeResource()则每次都会重新解码返回bitmap。所以其实我们可以通过context.getResources().getDrawable再从drawable里获取bitmap,从而复用bitmap,然而这里也有一些坑,比如我们获取到的这份bitmap,假如我们执行了recycle之类的操作,但是假如在其他地方再使用它是那么就会有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”异常。

3. 图片压缩

BitmapFactory 在解码图片时,可以带一个Options,有一些比较有用的功能,比如:

  • inTargetDensity 表示要被画出来时的目标像素密度
  • inSampleSize 这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4
  • inJustDecodeBounds 字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。
  • inPreferredConfig 默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte,一下可以省下50%内存。
  • inPurgeableinInputShareable 这两个需要一起使用,BitmapFactory.java的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个bitmap,有点类似软引用,但是实际在5.0以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题
  • inBitmap 官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在4.4以前只有相同大小的图片内存区域可以复用,4.4以后只要原有的图片比将要解码的图片大既可以复用了。

4. 缓存池大小

现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似Glide 默认使用的事LruCache,因为软引用 弱引用都比较难以控制,使用LruCache可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收,所以需要根据每个App的情况,以及设备的分辨率,内存计算出一个比较合理的初始值,可以参考Glide的做法。

5. 内存抖动

什么是内存抖动呢?Android里内存抖动是指内存频繁地分配和回收,而频繁的gc会导致卡顿,严重时还会导致OOM。

%title插图%num

一个很经典的案例是string拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的log的时候), 见Android优化之String篇[9]。

而内存抖动为什么会引起OOM呢?

主要原因还是有因为大量小的对象频繁创建,导致内存碎片,从而当需要分配内存时,虽然总体上还是有剩余内存可分配,而由于这些内存不连续,导致无法分配,系统直接就返回OOM了。

比如我们坐地铁的时候,假设你没带公交卡去坐地铁,地铁的售票机就只支持5元,10元,而哪怕你这个时候身上有1万张1块的都没用(是不是觉得很反人类..)。当然你可以去兑换5元,10元,而在Android系统里就没那么幸运了,系统会直接拒*为你分配内存,并扔一个OOM给你(有人说Android系统并不会对Heap中空闲内存区域做碎片整理,待验证)。

其他

常用数据结构优化,ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的。用于在一定情况下取代HashMap而达到节省内存的目的,具体性能见HashMap,ArrayMap,SparseArray源码分析及性能对比[10],对于key为int的HashMap尽量使用SparceArray替代,大概可以省30%的内存,而对于其他类型,ArrayMap对内存的节省实际并不明显,10%左右,但是数据量在1000以上时,查找速度可能会变慢。

枚举,Android平台上枚举是比较争议的,在较早的Android版本,使用枚举会导致包过大,在个例子里面,使用枚举甚至比直接使用int包的size大了10多倍 在stackoverflow上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在Android平台性能问题已经不大,而目前Android官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用int多使用2倍的内存。

ListView复用,这个大家都知道,getView里尽量复用conertView,同时因为getView会频繁调用,要避免频繁地生成对象

谨慎使用多进程,现在很多App都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M左右),对于使用完的进程,服务都要及时进行回收。

尽量使用系统资源,系统组件,图片甚至控件的id

减少view的层级,对于可以 延迟初始化的页面,使用viewstub

数据相关:序列化数据使用protobuf可以比xml省30%内存,慎用shareprefercnce,因为对于同一个sp,会将整个xml文件载入内存,有时候为了读一个配置,就会将几百k的数据读进内存,数据库字段尽量精简,只读取所需字段。

dex优化,代码优化,谨慎使用外部库, 有人觉得代码多少于内存没有关系,实际会有那么点关系,现在稍微大一点的项目动辄就是百万行代码以上,多dex也是常态,不仅占用rom空间,实际上运行的时候需要加载dex也是会占用内存的(几M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库,此时可以考虑抽取必要部分,开启proguard优化代码,使用Facebook redex使用优化dex(好像有不少坑)。

三 案例

JOOX是IBG一个核心产品,2014年发布以来已经成为5个国家和地区排名*的音乐App。东南亚是JOOX的主要发行地区,实际上这些地区还是有很多的低端机型,对App的进行内存优化势在必行。

上面介绍了Android系统内存分配和回收机制,同时也列举了常见的内存问题,但是当我们接到一个内存优化的任务时,我们应该从何开始?下面是一次内存优化的分享。

1. 首先是解决大部分内存泄露。

不管目前App内存占用怎样,理论上不需要的东西*好回收,避免浪费用户内存,减少OOM。实际上自JOOX接入LeakCanary后,每个版本都会做内存泄露检测,经过几个版本的迭代,JOOX已经修复了几十处内存泄露。

%title插图%num

2. 通过MAT查看内存占用,优化占用内存较大的地方。

JOOX修复了一系列内存泄露后,内存占用还是居高不下,只能通过MAT查看到底是哪里占用了内存。关于MAT的使用,网上教程无数,简单推荐两篇MAT使用教程[11],MAT – Memory Analyzer Tool 使用进阶[12]。

点击Android Studio这里可以dump当前的内存快照,因为直接通过Android Sutdio dump出来的hprof文件与标准hprof文件有些差异,我们需要手动进行转换,利用sdk目录/platform-tools/hprof-conv.exe可以直接进行转换,用法:hprof-conv 原文件.hprof 新文件.hprof。只需要输入原文件名还有目标文件名就可以进行转换,转换完就可以直接用MAT打开。

%title插图%num

下面就是JOOX打开App,手动进行多次gc的hprof文件。

这里我们看的是Dominator Tree(即内存里占用内存*多的对象列表)。

%title插图%num

  • Shallo Heap:对象本身占用内存的大小,不包含其引用的对象内存。
  • Retained Heap: Retained heap值的计算方式是将retained set中的所有对象大小叠加。或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。

*眼看去 居然有3个8M的对象,加起来就是24M啊 这到底是什么鬼?

%title插图%num

我们通过List objects->with incoming references查看(这里with incoming references表示查看谁引用了这个对象,with outgoing references表示这个对象引用了谁)

%title插图%num

通过这个方式我们看到这三张图分别是闪屏,App主背景,App抽屉背景。

%title插图%num

这里其实有两个问题:

  • 这几张图原图实际都是1280×720,而在1080p手机上实测这几张图都缩放到了1920×1080
  • 闪屏页面,其实这张图在闪屏显示过后应该可以回收,但是因为历史原因(和JOOX的退出机制有关),这张图被常驻在后台,导致无谓的内存占用。

优化方式:我们通过将这三张图从xhdpi挪动到xxhdpi(当然这里需要看下图片显示效果有没很大的影响),以及在闪屏显示过后回收闪屏图片。
优化结果:

%title插图%num

从原来的8.29×3=24.87M 到 3.68×2=7.36M 优化了17M(有没一种万马奔腾的感觉。。可能有时费大力气优化很多代码也优化不了几百K,所以很多情况下内存优化时优化图片还是比较立竿见影的)。

同样方式我们发现对于一些默认图,实际要求的显示要求并不高(图片相对简单,同时大部分情况下图片加载会成功),比如下面这张banner的背景图:

%title插图%num

优化前1.6M左右,优化后700K左右。

同时我们也发现了默认图片一个其他问题,因为历史原因,我们使用的图片加载库,设置默认图片的接口是需要一个bitmap,导致我们原来几乎每个adapter都用BitmapFactory decode了一个bitmap,对同一张默认图片,不但没有复用,还保存了多份,不仅会造成内存浪费,而且导致滑动偶尔会卡顿。这里我们也对默认图片使用全局的bitmap缓存池,App全局只要使用同一张bitmap,都复用了同一份。

另外对于从MAT里看到的图片,有时候因为看不到在项目里面对应的ID,会比较难确认到底是哪一张图,这里stackoverflow上有一种方法,直接用原始数据通过GIM还原这张图片。

这里其实也看到JOOX比较吃亏一个地方,JOOX不少地方都是使用比较复杂的图片,同时有些地方还需要模糊,动画这些都是比较耗内存的操作,Material Design出来后,很多App都遵循MD设计进行改版,通常默认背景,默认图片一般都是纯色,不仅App看起来比较明亮轻快,实际上也省了很多的内存,对此,JOOX后面对低端机型做了对应的优化。

3. 我们也对RDM上的OOM进行了分析,发现其实有些OOM是可以避免的。

下面这个crash就是上面提到的在LsitView的adapter里不停创建bitmap,这个地方是我们的首页banner位,理论上App一打开就会缓存这张默认背景图片了,而实际在使用过一段时间后,才因为为了解码这张背景图而OOM, 改为用全局缓存解决。

%title插图%num

下面这个就是传说中的内存抖动

%title插图%num

实际代码如下,因为打Log而进行了字符串拼接,一旦这个函数被比较频繁地调用,那么就很有可能会发生内存抖动。这里我们新版本已经改为使用stringbuilder进行优化。

%title插图%num

还有一些比较奇怪的情况,这里是我们扫描歌曲文件头的时候发生的,有些文件头居然有几百M大,导致一次申请了过大的内存,直接OOM,这里暂时也无法修复,直接catch住out of memory error。

%title插图%num

4. 同时我们对一些逻辑代码进行调整,比如我们的App主页的第三个tab(Live tab)进行了数据延迟加载,和定时回收。

%title插图%num

这里因为这个页面除了有大图还有轮播banner,实际强引用的图片会有多张,如果这个时候切到其他页面进行听歌等行为,这个页面一直在后台缓存,实际是很浪费耗内存的,同时为优化体验,我们又不能直接通过设置主页的viewpager的缓存页数,因为这样经常都会回收,导致影响体验,所以我们在页面不可见后过一段时间,清理掉adapter数据(只是清空adapter里的数据,实际从网络加载回来的数据还在,这里只是为了去掉界面对图片的引用),当页面再次显示时再用已经加载的数据显示,即减少了很多情况下图片的引用,也不影响体验。

5. *后我们也遇到一个比较奇葩的问题,在我们的RDM上报上有这样一条上报

%title插图%num

我们在stackoverflow上看到了相关的讨论,大致意思是有些情况下比如息屏,或者一些省电模式下,频繁地调System.gc()可能会因为内核状态切换超时的异常。这个问题貌似没有比较好的解决方法,只能是优化内存,尽量减少手动调用System.gc()

优化结果

我们通过启动App后,切换到我的音乐界面,停留1分钟,多次gc后,获取App内存占用

%title插图%num

多次试验结果都差不多,这里只截取了其中一次,有28M的优化效果。
当然不同的场景内存占用不同,同时上面试验结果是通过多次手动触发gc稳定后的结果。对于使用其他第三方工具不手动gc的情况下,试验结果可能会差异比较大。

对于上面提到的JOOX里各种图片背景等问题,我们做了动态的优化,对不同的机型进行优化,对特别低端的机型设置为纯色背景等方式,*终优化效果如下:

%title插图%num

%title插图%num

平均内存降低41M。

本次总结主要还是从图片方面下手,还有一点逻辑优化,已经基本达到优化目标。

四 总结

上面写了很多,我们可以简单总结,目前Andorid内存优化还是比较重要一个话题,我们可以通过各种内存泄露检测组件,MAT查看内存占用,Memory Monitor跟踪整个App的内存变化情况, Heap Viewer查看当前内存快照, Allocation Tracker追踪内存对象的来源,以及利用崩溃上报平台从多个方面对App内存进行监控和优化。上面只是列举了一些常见的情况,当然每个App功能,逻辑,架构也都不一样,造成内存问题也是不尽相同,掌握好工具的使用,发现问题所在,才能对症下药。

 


%title插图%num

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数*多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配*新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用!

iOS 去掉tableViewCell 点击效果 三个简单方法

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{

// 1 松开手选中颜色消失

[tableView deselectRowAtIndexPath:indexPath animated:YES];

UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// 2
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
// 3点击没有颜色改变
cell.selected = NO;

}

 

iOS UICollectionView报错The behavior of the UICollectionViewFlowLayout is not defined because

报错信息
The behavior of the UICollectionViewFlowLayout is not defined because:
Make a symbolic breakpoint at UICollectionViewFlowLayoutBreakForInvalidSizes to catch this in the debugger.
滚动视图即(UICollectionView或者UIScrollView 等继承自UIScrollView的视图适用)

//设置控制器不自动计算滚动视图的内容边距(滚动视图会计算导致)
self.automaticallyAdjustsScrollViewInsets = NO;
1
2
或者

// 设置滚动视图的contentInsetAdjustmentBehavior为Never
if (@available(iOS 11.0, *)) {
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
// Fallback on earlier versions
}

iOS keychain 卸载后依然存在的持久化存储

// 封装
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SecItemHelper : NSObject
+ (void)saveFromName:(NSString *)name data:(id)data;
+ (id)loadFromName:(NSString *)name;
+ (void)deletedataFromName:(NSString *)name;
@end

NS_ASSUME_NONNULL_END

#import “SecItemHelper.h”

@implementation SecItemHelper
+ (void)saveFromName:(NSString *)name data:(id)data
{
NSMutableDictionary *keychainQuery = [self getKeychainQuery:name];
// 清空
SecItemDelete((CFDictionaryRef)keychainQuery);
// 赋值
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
// 添加
SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}

+ (id)loadFromName:(NSString *)name
{
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:name];
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr)
{
@try
{
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
}
@catch (NSException *e)
{
NSLog(@”Unarchive of %@ failed: %@”, name, e);
}
@finally
{

}
}
if (keyData)
{
CFRelease(keyData);
}
return ret;
}

+ (void)deletedataFromName:(NSString *)name
{
NSMutableDictionary *keychainQuery = [self getKeychainQuery:name];
SecItemDelete((CFDictionaryRef)keychainQuery);
}

+ (NSMutableDictionary *)getKeychainQuery:(NSString *)name
{
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setValue:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[dic setValue:name forKey:(id)kSecAttrAccount];
[dic setValue:name forKey:(id)kSecAttrService];
// 解锁后可以的安全蛇者
[dic setValue:(id)kSecAttrAccessibleAfterFirstUnlock forKey:(id)kSecAttrAccessible];
return dic;
}

@end
// 调用
#import “ViewController.h”
#import “SecItemHelper.h”

@interface ViewController ()

@end

@implementation ViewController

– (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

NSString *name = NSStringFromClass(ViewController.class);

NSMutableDictionary *dic =[NSMutableDictionary dictionary];
[dic setValue:@”xiaoming” forKey:@”user”];
[dic setValue:@”123456″ forKey:@”password”];

[SecItemHelper saveFromName:name data:dic];

id data = [SecItemHelper loadFromName:name];

NSLog(@”%@”,data);

// [SecItemHelper deletedata:name];
// id data2 = [SecItemHelper load:name];
// NSLog(@”%@”,data2);
}

– (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end
// 译文

kSecAttrAccessibleWhenUnlocked
当解锁时,kSec具有可访问性 推荐使用 iCloud同步
kSecAttrAccessibleAfterFirstUnlock
*次解锁后可访问kSec iCloud同步
kSecAttrAccessibleAlways
一直访问 不建议使用 iCloud同步
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
当密码仅设置此设备时,kSec Attr可访问 iCloud不同步
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
只有在解锁此设备时才能访问kSec Attr iCloud不同步
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
只有在首次解锁此设备后才能访问kSec Attr iCloud不同步
kSecAttrAccessibleAlwaysThisDeviceOnly
kSec Attr始终只能访问此设备 iCloud不同步

kSecClassInternetPassword
表示Internet密码项的值。

kSecClassCertificate
表示证书项的值。

kSecClassKey
表示加密密钥项的值。

kSecClassIdentity
表示标识项的值。

(译)关于使用Eclipse Memory Analyzer的10点小技巧

分析和理解应用的内存使用情况是开发过程中一项不小的挑战。一个微小的逻辑错误可能会导致监听器没法被释放回收,*终导致可怕的内存溢出问题。甚至有时你已经释放了所有空对象,但是你的应用却多消耗了十倍甚至百倍的内存导致效率很低。

幸运的是,Eclipse Memory Analyzer(MAT)能给我提供应用的内存使用情况的详细信息帮助我们进行内存分析。这款工具不仅能有效的追踪内存泄漏,还能周期性的审查系统的状态。在本课程我将列出10条小技巧帮助你更高效的使用MAT。如果你是一名Java开发者,Eclipse Memory Analyzer Tool是你调试工具箱里必不可少的。

[ 你还在寻找更多工具吗? 查看Eclipse Tools页面. | 使用Yoxos.Create a free profile now让你更方便的管理你的Eclipse workspace. ]

可以使用Install New Software对话框或者通过EclipseMarketPlace来安装MAT。你也可以安装使用Yoxos将其囊括到你自己的Eclipse中。

在本例中,我们使用一个非常简单的方案,通过分配100,000监听器,并将它们存储到4个列表中。在未将列表清空回收的情况下让应用休眠。

1.获取内存快照(Heap Dump)


你可以通过下面的几种方式使用MAT:

1.配置一款应用,当其发生内存溢出错误的时候将其内存镜像导出,

2.将MAT连接到一个已存在的Java进程,或者

3.手动获取heap dump并加载到MAT中。

无论哪种情况,你都需要记住这只是内存在某一时间节点的快照。MAT不能告诉你为什么一个对象会被创建,也不能显示那些已经被回收掉的对象。但是,如果你使用MAT结合其他的调试工具和调试技术,通常会非常快的解决内存泄漏。

你可以通过添加下面的vm argument,配置你的应用当其抛出OutOfMemory错误的时候导出heap dump:

-XX:+HeapDumpOnOutOfMemoryError

另外,你也可以使用jstack从正在运行的Java进程中获取Heap dump.

jmap -dump:file=heap.bin

*后,你还可以使用MAT的Acquire Heap Dump动作选中你本地机器上已经存在的Java进程。

%title插图%num

当你*次加载Heap dump的时候,MAT需要花几分钟时间来给Heap dump编辑索引。其结果会保留所以后续的再次加载会很快。

2.理解Histogram


当你*次获取heap dump的时候,MAT会给你展示应用的内存使用情况的overview。

%title插图%num

中间的饼状图给你展示的是retained size*大的对象。也就是说,如果我们能释放一个特殊的java.lang.Thread对象,就能保留11.2Mb的内存,超过你当前应用使用内存的90%。有趣的是,java.lang.Thread并不像是问题的症结所在。为了更好的理解到系统当前存在的对象,我们可以使用Histogram。

%title插图%num

Histogram可以展示某个特定类的对象个数和每个对象使用的内存。当然char[],String和Object[]都不太会导致内存问题。为了更好的组织这个视图,你可以通过classloader或者package来分组。这会让你更好的专注在你自己的对象上。

%title插图%num

Histogram 也能使用正则表达式来过滤。例如,我们可以只展示那些匹配com.example.mat.*的类。

%title插图%num

通过这个视图我们可以看见在系统中存在100,000个Listener的对象我们也可以看见每一个对象正在占用的内存数量。这里有两个数值,Shallow HeapRetained Heap。Shallow heap是一个对象消费的内存数量每个对象的引用需要32(或者64 bits,基于你的CPU架构)。基本数据类型例如整形和长整形需要4或者8 bytes以及其他的。其实更有用的参数是Retained Heap.

3.理解Retained Heap


Retained Heap显示的是那些当垃圾回收时候会清理的所有对象的Shallow Heap的总和。举例说明,如果一个ArrayList包含100,000成员项,每个成员需要16 bytes,当移除这个ArrayList的时候会释放16×100,000+X(bytes),X是ArrayList的shallow size。(注:这是假设这些对象只被这个ArrayList引用,没有其他地方引用)。

Retained heapRetained set(保留集)里面所有对象大小的求和计算结果。Retained set of X指的是这样的对象集合: X 对象被 GC 回收后,所有能被回收的对象集合。

Retained heap有两种不同的计算方式, 使用quick approximation或者precise retained size.

%title插图%num
%title插图%num

通过计算Retained Heap我们可以看见com.example.mat.Controller持有了大部分的内存,尽管他自身只占用了24 bytes。所以通过找到方法释放Controller,我们就能毫无疑问的控制好内存问题。

4. Dominator Tree(支配树)


查看Dominator tree是理解Retained heap的关键。Dominator tree是由你系统中的复杂的Object graph(对象引用图)生成的树状图。Dominator tree可以让你分别出*大内存图表。如果所有指向对象Y的路径都经过对象X,则认为对象X支配对象Y。通过查看本例的Dominator tree,我们开始明白到底是哪些内存块发生了泄露。

%title插图%num

通过查看dominator tree,我们可以轻易的了解到并不是java.lang.Thread导致的问题,反而是ControllerAllocator持有内存。Controller保留了全部100,000个Listeners对象。我们可以通过释放这些对象,或释放他们所包含的lists来改善内存情况。下面列出几条dominator tree的属性:

● 对象X的子树中的所有对象(本例中的com.example.mat.Controller)被称作对象A的Retained set(保留集)。

● 如果对象X是对象Y的直接支配者(Controller就是Allocator的直接支配者),那么X的直接支配者(本例中的java.lang.Thread)也只配Y对象。

● 支配树中节点的父子关系跟对象引用图中的不直接对应。

通过Histogram你也可以选择某个类,然后找到所有支配该类的实例的对象。

%title插图%num

5. 探索Paths to the GC Roots


有时候有一些你确信已经处理了的大的对象集合。通过查找支配者可能会有用,但是通常我们希望能得到这个对象节点到GC根节点的路径。例如,如果我现在释放了Controller对象,会理所当然的以为已经解决内存问题,不幸的是这并没有用。如果现在选中一个Listener的对象,然后查看他到GC根节点的路径。我们可以看见Controller类(注:是类,而不是对象)引用到了一个Listener队列。这是因为这些队列当中有一个被声明成静态队列。

%title插图%num

你也可以查看到这个对象所有被引用到的地方和这个对象持有的引用。当你想要在对象引用图中查看某个特定对象的所有引用关系的时候,这是非常有用的。

6. Inspector


Inspector展示的是当前选中类或对象的详细信息。在本例中我们可以看见选中的ArrayList包含100,000元素和一个指向地址为0x7f354ea68的对象数组的引用。

%title插图%num

Inspector和Snapshot linked会给你提供一些选中项的重要统计数据。

7. Common Memory Anti-Patterns


MAT使用反模式提供了公用存储器的详细报告。.能用其来搞明白哪里的发生了内存泄漏,或通过它找到一些简单的清理手段来优化性能。

%title插图%num

Heap Dump Overview展示了Heap Dump的详细信息和一些常用工具的链接(比如Histogram)。信息主要有系统中正在运行的线程、对象的总数、堆的大小等。

%title插图%num

Leak Suspects报告显示了MAT发现的可能导致内存泄漏的地方,和用于分析这些发现的工具和图表的链接。

%title插图%num

另一个使用到反模式的情况是,当系统有大量的集合,但是每个集合只有少量元素的时候。例如,如果每一个监听器都对应一组通知者(需要某些事件来触发的列表项),但是这些通知者只是偶尔触发,我们就应该制止这种浪费内存的行为。Java Collections工具可以帮你处理这类问题。

%title插图%num
%title插图%num
%title插图%num

通过Collection -> Fill Ratio Report我们可以看见100,000个队列是空的。如果我们能够用一种便捷的方式来分配这些内存(当我们需要的时候),我们可以节约大概8Mb内存。

我们也可以通过分析集合来查看array fill ratioscollection size statisticsmap collision ratios

8. Java工具


MAT量身定制了许多内置的工具用来生成Java运行环境细节的相关报表。For example, thereport will show details about all the treads in the system.例如,Threads and Stack可以展示系统中所有线程的细节。你可以看见每个栈中当前存在的本地变量

 

%title插图%num

你可以通过特定的模板来检索所有匹配的字符串:

%title插图%num

甚至可以检索那些包含了浪费内存的字符数组的字符串(这种情况经常是因为反复是用substring方法导致的)。

%title插图%num

9. Object Query Language


综合以上所说,Eclipse Memory Analyzer提供了很多用来追踪内存泄漏和内存过量使用的工具。大多数的内存问题可以通过上面的工具定位到,但是Heap Dump包含了更多的信息。Object Query Language  (OQL)让你可以基于Heap Dump创建你自己的报表。

OQL是一种类似于SQL的语言。只需要将类当成表,对象看做行,字段看做列。例如,想要查询com.example.mat.Listener的所有对象,只需要写:

select * from com.example.mat.Listener

%title插图%num

表的列可以通过不同的字段来设置,例如:

SELECT toString(l.message), l.message.count FROM com.example.mat.Listener l

%title插图%num

And finally, the WHERE clause can be used to specify particular criteria, such as all the Strings in the system which are not of the format “message:.*”*后WHRER子句可以用来筛选特定的条件,例如可以通过下列语句找出系统中所有不匹配”message:.*”的字符串:

SELECT toString(s), s.count FROM java.lang.String s WHERE (toString(s) NOT LIKE “message.*”)

%title插图%num

10.导出结果


MAT是一款用来导出应用内存状态相关报告的利器。Heap Dump包含了有关你系统的非常有价值的信息。并且MAT提供了相关的工具来接入这些数据。然而,就像很多开源工具一样,如果你对于某些失误不太敏感,或者你运气不好。使用MAT可以将结果导出成包括HTML,CSV甚至纯文本格式。你可以使用电子表格程序(或者你自己的工具)来继续进行分析。

%title插图%num

MAT是一款强大的工具,一款Java开发者应该熟知的工具。追踪内存泄漏和其他的一些内存问题对开发者来说是常见的难点,可喜的是有MAT可以迅速的帮你找到与你内存问题的源头所在。

关于 macOS keychain 误删与恢复的二三事

起因
以 Mac 为主力机的我*习以为常的事情就是使用 keychain access 来记录各种密码,比如软件的密码 备份硬盘 印象笔记 ,网页的密码 Safari 密码管理。

可是*近在捣鼓 iOS 越狱开发,需要生成临时证书 Apple Development,随后开始了作死之旅,由于*次生成的证书不过关,按照给的操作提示,我需要删除证书重新添加。然后我手抽不知道是什么误操作把 keychain 部分清了,没错,就是清了

开始是触摸解锁用不了,我还以为是电脑卡了,后来用 Safari 打开密码管理也用不了,等手动输入密码后发现里面 空空如也

%title插图%num

瞬间整个人都不好了,心如死灰,那可是我赖以生存的为数不多的家当啊!
可是人活着总是要干饭的,没办法,尝试挽救

挽救
怎么才能把密码全部找回呢,刚开始上网查找之后
发现 keychain 存放的两个位置分别是 /Library/Keychains/ 和 /User/$username/Library/Keychains/,凭借经验直接猜测分别对应系统和登陆(用户)选项

%title插图%num

因为经常备份,知道 Mac 的时间机器功能,去寻找*近未被修改的两个 Keychain 文件夹,直接群体恢复,*次没用,然后我又试了第二次,莫名其妙的好了

喜大普奔~!

思考
怕以后还是不小心脑抽删 Keychain,决定找一下更细一点的对应文件

打开 keychain access 软件,翻能不能找到对应存储位置,怎么翻都翻不到
结果随手往 钥匙串 几个选项上一放,DING!它出现了!

%title插图%num

%title插图%num
这下知道了,对应密码存放文件即为login.keychain-db 和 System.keychain
然后我们开始观察这两个文件所存放的内容,由于我们主要用来存放密码,暂时不太考虑证书之类的东西,手动调整种类为密码

%title插图%num

首先是系统密码,我们可以看到里面存放的都是 Wifi 密码,那就下一个登陆密码,由于登陆密码是关联用户账户的,所以我们可以看到很多都是关于用户账户功能的密码,比如 身份识别认证,密码重置密钥,设备识别,这有可能会涉及到用户登陆的

%title插图%num

当然这里面还有的就是设备上安装的软件密码,比如印象笔记,Reveal2,Docker

%title插图%num

*后我们直接查找 Safari 相关,然后看到了比较有趣的名称,继续开始猜测

Safari Forms AutoFill Encryption Key:即与自动填充密码相关的密钥
Safari History Key:与历史记录相关的密钥
Safari Session State Key:与网站链接 Session 状态的密钥
“Safari 浏览器”WebCrypto 主密钥:网页加密所用密钥

%title插图%num

为了印证猜测,去 Safari 存放数据的地方翻一翻,路径:/User/$username/Library/Safari/
随机选一个文件按一下 Space 快速预览进行判断,比如AutomaticBookmarksBackup.html,就可以看出是书签,可是跟我目前的书签列表有些许出入

%title插图%num

暂且不管,继续看:
AutoFillCorrections.db CloudAutoFillCorrections.db:猜这里就是存放自动填充密码的地方
Databases:对应 WebCrypto 加密的 IndexedDB
History.db:对应 Safari History key
LastSession.plist:对应目前 Safari 所开的标签页
LocalStorage:对应 Safari 打开过的网页
PerSitePreferences.db:对应每个网站的对应偏好设置
其他的 .plist 文件就暂时不说了

结论
Mac 的时间机器功能真是太棒了,使劲吹~!

为了防止以后这种浏览器密码再次被删修复困难,注意:

打开 Apple ID 使钥匙串使用 iCloud,这样应该只要一同步就可以恢复

%title插图%num

多备份,*好每周一次
就别没事修改钥匙串了

Android中Handler引起的内存泄露

在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用。通常我们的代码会这样实现。

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ... 
    }
  }
}

但是,其实上面的代码可能导致内存泄露,当你使用Android lint工具的话,会得到这样的警告

In Android, Handler classes should be static or leaks might occur, Messages enqueued on the application thread’s MessageQueue also retain their target Handler. If the Handler is an inner class, its outer class will be retained as well. To avoid leaking the outer class, declare the Handler as a static nested class with a WeakReference to its outer class

看到这里,可能还是有一些搞不清楚,代码中哪里可能导致内存泄露,又是如何导致内存泄露的呢?那我们就慢慢分析一下。

1.当一个Android应用启动的时候,会自动创建一个供应用主线程使用的Looper实例。Looper的主要工作就是一个一个处理消息队列中的消息对象。在Android中,所有Android框架的事件(比如Activity的生命周期方法调用和按钮点击等)都是放入到消息中,然后加入到Looper要处理的消息队列中,由Looper负责一条一条地进行处理。主线程中的Looper生命周期和当前应用一样长。

2.当一个Handler在主线程进行了初始化之后,我们发送一个target为这个Handler的消息到Looper处理的消息队列时,实际上已经发送的消息已经包含了一个Handler实例的引用,只有这样Looper在处理到这条消息时才可以调用Handler#handleMessage(Message)完成消息的正确处理。

3.在Java中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用。静态的内部类不会持有外部类的引用。关于这一内容可以查看细话Java:”失效”的private修饰符

确实上面的代码示例有点难以察觉内存泄露,那么下面的例子就非常明显了

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

分析一下上面的代码,当我们执行了Activity的finish方法,被延迟的消息会在被处理之前存在于主线程消息队列中10分钟,而这个消息中又包含了Handler的引用,而Handler是一个匿名内部类的实例,其持有外面的SampleActivity的引用,所以这导致了SampleActivity无法回收,进行导致SampleActivity持有的很多资源都无法回收,这就是我们常说的内存泄露。

注意上面的new Runnable这里也是匿名内部类实现的,同样也会持有SampleActivity的引用,也会阻止SampleActivity被回收。

要解决这种问题,思路就是避免使用非静态内部类,继承Handler时,要么是放在单独的类文件中,要么就是使用静态内部类。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露。当你需要在静态内部类中调用外部的Activity时,我们可以使用弱引用来处理。另外关于同样也需要将Runnable设置为静态的成员属性。注意:一个静态的匿名内部类实例不会持有外部类的引用。 修改后不会导致内存泄露的代码如下

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

其实在Android中很多的内存泄露都是由于在Activity中使用了非静态内部类导致的,就像本文提到的一样,所以当我们使用时要非静态内部类时要格外注意,如果其实例的持有对象的生命周期大于其外部类对象,那么就有可能导致内存泄露。个人倾向于使用文章的静态类和弱引用的方法解决这种问题。