日期: 2021 年 6 月 22 日

8. 字符串转换整数 (atoi)(JS实现)

8. 字符串转换整数 (atoi)(JS实现)

1 题目
请你来实现一个 atoi 函数,使其能将字符串转换成整数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到*个非空格的字符为止。接下来的转化规则如下:
如果*个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字字符组合起来,形成一个有符号整数。
假如*个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成一个整数。
该字符串在有效的整数部分之后也可能会存在多余的字符,那么这些字符可以被忽略,它们对函数不应该造成影响。
注意:假如该字符串中的*个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换,即无法进行有效转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0 。
提示:
本题中的空白字符只包括空格字符 ’ ’ 。
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
示例 1:
输入: “42”
输出: 42
示例 2:
输入: ” -42″
输出: -42
解释: *个非空白字符为 ‘-’, 它是一个负号。
我们尽可能将负号与后面所有连续出现的数字组合起来,*后得到 -42 。
示例 3:
输入: “4193 with words”
输出: 4193
解释: 转换截止于数字 ‘3’ ,因为它的下一个字符不为数字。
示例 4:
输入: “words and 987”
输出: 0
解释: *个非空字符是 ‘w’, 但它不是数字或正、负号。
因此无法执行有效的转换。
示例 5:
输入: “-91283472332”
输出: -2147483648
解释: 数字 “-91283472332” 超过 32 位有符号整数范围。
因此返回 INT_MIN (−231) 。

2 思路
这道题边界条件超多,如果直接写if语句,可能会漏掉,参考官方代码,此时需要考虑使用状态机来实现,用一个二维表格进行维护

3代码
/**
* @param {string} str
* @return {number}
*/
var myAtoi = function(str) {
var INT_MAX = 2 ** 31 – 1;
var INT_MIN = (-2) ** 31;
var ans = 0;
var sign = 1;
var table = {
‘start’: [‘start’, ‘sign’, ‘number’, ‘end’],
‘sign’: [‘end’, ‘end’, ‘number’, ‘end’],
‘number’: [‘end’, ‘end’, ‘number’, ‘end’],
‘end’: [‘end’, ‘end’, ‘end’, ‘end’],
}
var state = ‘start’;

function getCols(c) {
if (c === ‘ ‘) { //空格
return 0;
} else if (c === ‘+’ || c === ‘-‘) { //符号
return 1;
} else if (!isNaN(c % 1)) { //整数
return 2
} else { //其他字符
return 3;
}
}

function input(c) {
var inputState = table[state][getCols(c)];
state = inputState;
if (inputState === ‘sign’) {
sign = c === ‘-‘ ? -1 : 1;
} else if (inputState === ‘number’) {
ans = ans * 10 + parseInt(c);
if (ans > INT_MAX || ans * sign < INT_MIN) ans = sign > 0 ? INT_MAX : -INT_MIN;
} else if (inputState === ‘end’) { //退出循环
return true;
}
}

for (var i=0; i<str.length; i++) {
if (input(str[i])) {
break;
}
}
return sign * ans;
};

iOS数据层处理

前言
刚毕业就做iOS开发,到现在也有些年头了,感觉自己在工作中慢慢摸索、总结出了一套自己的程序设计思路,而且这套思路也不错的经受住了很多个不同类型项目、不同团队的实际考验,一直都有着不错的表现,*近不是很忙,借着博客记录一下在程序设计上自己的收获与心得。

个人总结,app客户端程序可以划分为两个部分,下层部分与数据打交道,包含网络请求与本地数据操作(存取);上层部分就是应用层部分,负责UI交互与业务逻辑的串联,如下图:

%title插图%num
一般在项目实施时,我们*步都会先商量、定义好底层的数据模型、本地数据存储接口以及网络层接口(本地的+服务器的)。然后,两组人同时起头并进进行开发,负责上层的同事完全不用关心下层(数据相关)的具体实现(数据是怎么从网上请求回来的;请求返回后如何处理、存储数据;本地数据是如何存储获取的;底层的存储是怎么实现的,等等),他们只需要知道之前定义好的本地网络层及数据层接口就行了,在合适的时机去调用即可。而下层开发的同事,也是不用去考虑上层的具体实现,两拨人的工作是不会产生任何冲突的。一般情况下,下层的开发工作会相对更快的完成,然后下层的开发同事就可以返过头来去参与上层同事的开发工作,*终完成项目。

当然,要达到上面所说的效果,就离不开前期一定的设计工作,下面就从数据层、网络层和应用层三个层次讲一下我一直以来采用的设计方法。

总的来说,明确职责,各司其职,尽力而为,是我在程序设计时的基本指导思想;

点到为止、量力而行,不要过度设计、过度模式是我的设计原则,够用就行。其实设计的目的主要就是为了让代码有更高的独立性、纯粹性(不臃肿、职责明确)、通用性和扩展性,手段一般也就是不断的换着花样分层或解藕。个人感觉,这部分工作在大型互联网企业、大团队、特大项目中还是很有必要的,是可以不断追求的,因为大团队人多,业务繁杂,如果设计层面上不做好,不花精力去实现、改进,都揉杂在一起,团队间相互牵制,基本无法配合,而且还会带来后续扩展性差、可能会重复实现、重复造轮子等问题;但是,设计都是需要花费精力去思考和实现的,同时往往也会在某种程度上增加代码的”复杂性”与代码量,像中小型团队(非特大型项目),人不多,项目相对独立、业务也不是太过庞杂等,沟通与后续开发修改成本本身不会太大,所以权衡设计、把握设计的尺度就显的尤为重要。

数据层设计
1.基本设计
数据层负责模型定义+本地数据存取(数据操作),其实看看realm或者coredata这类框架的设计,当你定义好模型后,数据的存取操作也随之完成,也印证了我们对这个层次在职责划分上的合理性。这个层次可以说是**单纯的一层,在代码层面上与其他层次完全没有任何缠绕,在实现上可以做到完全的独立,有着很好的复用与迁移性。

这个层次位于整个架构的*底层,是其它所有层次的根基与”工具”。

在数据层里定义好项目中所有可能用到的模型类,同时还要实现好每种模型对应的所有数据操作

%title插图%num

[objc]  view plain  copy
//模型定义
@interface JHSMS : NSObject

@property (nonatomic,copy) NSString *uniqueId;
@property (nonatomic,copy) NSString *phoneNum;
@property (nonatomic,copy) NSString *content;
@property (nonatomic,copy) NSString *relatedWorkNum;
@property (nonatomic,strong) NSDate *msgDate;
@property (nonatomic,assign) BOOL isInComing;
@property (nonatomic,assign) SMS_SENDSTATUS sendStatus;
……..

//数据操作
+(void)saveSmsAry:(NSMutableArray *)smsAry;
+(BOOL)saveSingleSms:(JHSMS *)sms;
+(void)updateMsgStatus:(SMS_SENDSTATUS)sendStatus uniqueId:(NSString *)uniqueId;
+(NSMutableArray *)getRecentlySMSs:(NSString *)phoneNum;
+(NSMutableArray *)getSMSs:(NSString *)phoneNum beforeSms:(JHSMS *)sms;
+(void)resendSms:(JHSMS *)sms;
…….
@end
具体数据操作实现
[objc]  view plain  copy
+(BOOL)saveSingleSms:(JHSMS *)sms
{
BOOL __block result = YES;
[_shareQueue inTransaction:^(FMDatabase *db, BOOLBOOL *rollback) {

@try {
NSString *insertStr = @”insert into ‘SMS'(‘uniqueID’,’appUserWorkId’,’phoneNum’,’content’,’msgDate’,’contactType’,’friendContactId’,’relatedWorkNum’,’isInComing’,’sendStatus’) values (?,?,?,?,?,?,?,?,?,?)”;
CONATACT_TYPE contactType;

FMResultSet *rs = [db executeQuery:@”select relatedWorkId from PhonesInCorpContacts where phoneNum = ?”,sms.phoneNum];
if ([rs next])
{
sms.friendContactId = [rs stringForColumn:@”relatedWorkId”];
contactType = CORP_CONTACT;
}
else
{
FMResultSet *rs2 = [db executeQuery:@”select relatedRecordId from PhonesInLocalContacts where appUserWorkId = ? and phoneNum = ?”,appUserWorkId,sms.phoneNum];
…………..
[rs2 close];
}
…………
}
}];
return result;
}
2.数据层的设计改进
数据层的设计改进主要针对的是数据操作部分,上面的设计中存在两个问题,导致了模型类的过于臃肿和耦合性过大

(1)模型定义与数据操作耦合性过大

数据的本地存取操作可能会有很多不同方式(nsuerdefault、数据库、文件等等)及不同的业务需求,如果按照上面的实现,我们没有一个单独的层次去分离出数据操作的具体实现,那么如果以后要更换底层的存储方式,或是修改、添加数据操作实现时,那么就会对模型类造成影响,导致模型类的暴露与修改。所以我们还可以在数据层里单独划分出模型层+数据操作层.

%title插图%num

%title插图%num

在另一个项目中,我加入saver层,在这个层次里完成具体的数据存储操作,可以是数据库操作,也可以文件操作或是别的,甚至我还完成过同时支持多种存储形式,然后交由调用者选择的需求,无论怎样,这些不同的底层实现都封装可以在这个层次中单独的处理,对使用者透明,修改、实现时不会对模型类产生任何影响,同时大大减少了模型类的代码体量与功能承载压力

(2)数据操作与业务有缠绕

第二个问题是,针对每种模型(数据),其实**纯粹根本的操作就是对一个(一组)数据的增、删、改、查,而在我上面的代码示例中

[objc]  view plain  copy
+(void)updateMsgStatus:(SMS_SENDSTATUS)sendStatus uniqueId:(NSString *)uniqueId;
+(void)resendSms:(JHSMS *)sms;
+(NSMutableArray *)getRecentlySMSs:(NSString *)phoneNum;
这些操作明显是依据业务而定义(衍生)的,不具备普遍的通用性,没有复用的价值,同时造成了数据操作层与业务的耦合,于是我们可以对数据操作层再细分

%title插图%num

划分后,基础操作层可能包含下列这类函数实现,分离后它们具有很好的”原子性”、通用性与复用性

[objc]  view plain  copy
+(BOOL)saveSmsAry:(NSMutableArray *)smsAry;
+(BOOL)saveSingleSms:(JHSMS *)sms;
+(BOOL)deleteSms:(JHSMS *)sms;
+(NSMutableArray *)getSMSsWithCondition:(NSString *)condition;
+(BOOL)updateSmsForProperty:(NSString *)property value:(NSString *)value;
而业务操作层则是根据你具体的项目业务需求,对基础操作层进行不同的组合、调用

在数据层的实际设计开发中,我大部分情况下都是按基础设计去做的,除非有特殊需要,比较少的对数据操作层进行进一步细分.数据层比较单纯、独立、基础,其实修改、实现成本本身就不大,除非有特殊需求,一般我也就模型、数据操作写一起了,这样开发起来速度*快,复用、修改起来的工作量与难度也并不会很大.

iOS 数据存储的几种方式

在iOS开发过程中常用的本地化存储有五种方式:

1.plist (XML属性列表归档 NSArray\NSDictionary)

2.preference (偏好设置\NSUserDefaults) (本质还是通过plist来存储数据,但是使用更加简单,无需关注文件、文件夹路径和名称)

3.NSCoding (NSKeyedArchiver\NSKeyedUnarchiver)  (能把任何对象都直接保存成文件的方式)
4.SQLite3  (当非常大量的数据时候才会使用)
5.Core Data (是对SQLite3的封装,更加面向对象,效率没有SQLite3高)

沙盒(sandbox):每个iOS应用都有自己的应用沙盒(应用沙盒就是应用的文件夹),与其他文件系统隔离。应用必须待在自己的沙盒里,其他应用不能访问该沙盒
应用沙盒的文件系统目录,如下图所示(假设应用的名称叫Layer)

沙盒结构分析:
应用程序包:(上图中的Layer)包含了所有的资源文件和可执行文件
Documents:保存应用运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。例如,游戏应用可将游戏存档保存在该目录。保存相对重要的数据。

tmp:保存应用运行时所需的临时数据,使用完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。保存不重要的并且大的数据。

Library/Caches:保存应用运行时生成的需要持久化的数据,iTunes同步设备时不会备份该目录。一般存储体积大、不需要备份的非重要数据

Library/Preference:保存应用的所有偏好设置,iOS的Settings(设置)应用会在该目录中查找应用的设置信息。iTunes同步设备时会备份该目录。该目录由系统管理, 无需我们来管理。通常用来存储一些基本的软件配置信息, 比如记住密码、自动登录等。

总结: 我们平时操作数据主要使用Documents目录。

沙盒的目录的获取方式:
沙盒根目录的获取:
NSString *home = NSHomeDirectory();
1.利用沙盒根目录拼接”Documents“字符串:
//这种方式不建议使用,因为如果新版本的操作系统可能会修改目录的名称
NSString *home = NSHomeDirectory();
NSString *documents = [home stringByAppendingPathComponent:@“Documents”];

2.利用NSSearchPathForDirectoriesInDomains函数
// NSUserDomainMask 代表从用户文件夹下找
// YES 代表展开路径中的波浪字符“~”
NSArray *array =  NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// 在iOS中,只有一个目录跟传入的参数匹配,所以这个集合里面只有一个元素
NSString *documents = [array objectAtIndex:0];

常见沙盒目录获取方式:
tmp:NSString *tmp = NSTemporaryDirectory();
Library/Caches:(跟Documents类似的2种方法)
利用沙盒根目录拼接”Caches”字符串
利用NSSearchPathForDirectoriesInDomains函数(将函数的第2个参数改为:NSCachesDirectory即可)
Library/Preference:通过NSUserDefaults类存取该目录下的设置信息

1、XML属性列表(plist):(不能存储基本数据类型只能存储OC对象)

属性列表是一种 xml格式的文件,拓展名为plist,如果对象是(必须是OC对象,不能存储基本数据类型)NSString、NSDictionary/NSArray、NSData 、NSNumber等类型,就可以使用writeToFile:atomically:方法直接将对象写到属性列表文件中,然后用dictionaryWithContentsOfFile读取数据。

示例代码如下:

//保存
– (IBAction)save {

//1.获取沙盒根路径
NSString *home = NSHomeDirectory();
//2.document路径
NSString *docPath = [home stringByAppendingPathComponent:@”Documents”];
//3.新建数据  数据一般只能存储oc对象  字典也可以
NSArray *data = @[@”jack”,@”mack”,@15];
//4.写数据
NSString *filePath = [docPath stringByAppendingPathComponent:@”data.plist”];
[data writeToFile:filePath atomically:YES];
}
//读取
– (IBAction)read {

//1.获取沙盒根路径
NSString *home = NSHomeDirectory();
//2.document路径
NSString *docPath = [home stringByAppendingPathComponent:@”Documents”];
//3.文件路径
NSString *filePath = [docPath stringByAppendingPathComponent:@”data.plist”];
//4.读取数据
NSArray *data = [NSArray arrayWithContentsOfFile:filePath];
NSLog(@”%@”,data);
}

//将一个NSDictionary对象归档到一个plist属性列表中
// 将数据封装成字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@”母鸡” forKey:@”name”];
[dict setObject:@”15013141314″ forKey:@”phone”];
[dict setObject:@”27″ forKey:@”age”];
// 将字典持久化到Documents/stu.plist文件中
[dict writeToFile:path atomically:YES];

//读取属性列表,恢复NSDictionary对象
// 读取Documents/stu.plist的内容,实例化NSDictionary
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:path];
NSLog(@”name:%@”, [dict objectForKey:@”name”]);
NSLog(@”phone:%@”, [dict objectForKey:@”phone”]);
NSLog(@”age:%@”, [dict objectForKey:@”age”]);
2、prefrrence (偏好设置)

很多iOS应用都支持偏好设置,比如保存用户名、密码、字体大小等设置,iOS提供了一套标准的解决方案来为应用加入偏好设置功能
每个应用都有个NSUserDefaults实例,通过它来存取偏好设置,比如,保存用户名、字体大小、是否自动登录等。

//存储数据
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:@”itcast” forKey:@”username”];
[defaults setFloat:18.0f forKey:@”text_size”];
[defaults setBool:YES forKey:@“auto_login”];
//读取上次保存的设置
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *username = [defaults stringForKey:@”username”];
float textSize = [defaults floatForKey:@”text_size”];
BOOL autoLogin = [defaults boolForKey:@”auto_login”];
[defaults synchornize];(立刻同步)
注意:UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法强制写入
示例代码如下:

//偏好设置数据存储:(一般用来存储软件的配置,存储在Library/Preferences中,不需要关注文件夹的路径)

//存储
– (IBAction)save {

//1.利用NSUserDefaults,就能直接访问软件的偏好设置(Library/Preferences
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
//2.存储数据
[defaults setObject:@”jack” forKey:@”name”];
[defaults setInteger:10 forKey:@”age”];
[defaults setBool:YES forKey:@”Login”];
//3.立刻同步
[defaults synchronize];
}

//读取
– (IBAction)read {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *name = [defaults objectForKey:@”name”];
BOOL login = [defaults objectForKey:@”Login”];
NSLog(@”%@– %d”,name,login);
}

3、NSCoding (NSKeyedArchiver\NSKeyedUnarchiver)
如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复。
不是所有的对象(非OC对象)都可以直接用这种方法进行归档,只有遵守了NSCoding协议的对象才可以
NSCoding协议有2个方法:
encodeWithCoder:每次归档对象时,都会调用这个方法。一般在这个方法里面指定如何归档对象中的每个实例变量,可以使用encodeObject:forKey:方法归档实例变量
initWithCoder:每次从文件中恢复(解码)对象时,都会调用这个方法。一般在这个方法里面指定如何解码文件中的数据为对象的实例变量,可以使用decodeObject:forKey方法解码实例变量

示例代码如下:

NSKeyedArchiver数据存储,不仅可以用来存储OC对象,也可以用来存储非OC对象,以一个Person对象为例:(想归档某个对象,这个对象一定要遵守NSCoding这个协议)
#import <Foundation/Foundation.h>
@interface TWPerson : NSObject <NSCoding>
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) double height;
@end

@implementation TWPerson
//将对象归档的时候调用(将对象写入文件之前调用)
//在这个方法里说清楚:1.哪些属性需要存储  2.怎样存储这些属性
-(void)encodeWithCoder:(NSCoder *)enCoder
{
//将_name属性进行编码(会将_name的值存进文件)
[enCoder encodeObject:_name forKey:@”name”];
[enCoder encodeInt:_age forKey:@”age”];
[enCoder encodeDouble:_height forKey:@”height”];

}
//当从文件中解析对象时使用
//在这个方法说清楚: 1.那个属性需要解析(读取)  2.怎样解析(读取)这些属性
– (id)initWithCoder:(NSCoder *)Decoder
{

if (self = [super init]) {
_name = [Decoder decodeObjectForKey:@”name”];
_age = [Decoder decodeIntForKey:@”age”];
_height = [Decoder decodeDoubleForKey:@”height”];
}
return self;
}
@end

//存储数据
– (IBAction)save {
TWPerson *p = [[TWPerson alloc]init];
p.name = @”jack”;
p.age = 10;
p.height = 1.82;
NSString *path = @”/Users/wtw/Desktop/person.data”;
//归档
[NSKeyedArchiver archiveRootObject:p toFile:path];
}
//读取数据
– (IBAction)read {
NSString *path = @”/Users/wtw/Desktop/person.data”;
//读档(反归档)
TWPerson *p = [NSKeyedUnarchiver  unarchiveObjectWithFile:path];
NSLog(@”%@ — %f — %d”,p.name,p.height,p.age);
}
4、SQLite3
SQLite3 是一款开源的轻型的嵌入式关系型数据库,可移植性好、易使用、内存开销小,SQLite3 是无类型的,可以保存任何类型的数据到任意的字段中。
数据库存储数据的步骤:

》新建一个数据库(DataBase)

》新建一张表(table)

》添加多个字段(column,列,属性)

》添加多行记录(row,每行存放多个字段对应的值)

SQL语句的分类:

数据定义语句:(DDL :Data Definition Language)

包括create 和 drop 等操作。

在数据库中创建新表或者删除表(creat table 或 drop table)
数据库操作语句: (DML : Data manipulation Language)

包括 insert 、update 、delete 等操作。

上面的3种操作分别用于添加、修改、删除表中的数据

数据查询语句 :(DQL : Data Query Language)

可以用于查询获得表中的数据

关键字select 是 DQL 用的*多的操作

其他DQL 常用的关键字有 where 、order by、group by 和 having 等。
创建表:

//创建表:
/*
创建数据表
CREATE TABLE ‘表名’ (
‘字段1’ 类型(INTEGER, REAL, TEXT, BLOB)
NOT NULL 不允许为空
PRIMARY KEY 主键
AUTOINCREMENT 自增长,
‘字段名2′ 类型,

)
*/
提示:可以从navicat先创建好表,复制粘贴,需要自己增加 IF NOT EXISTS

CREATE TABLE IF NOT EXISTS “T_Person” (
“id” INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
“name” TEXT,
“age” INTEGER,
“heigth” REAL
)
删除表:
//删除表
//DROP TABLE t_student;
DROP TABLE IF EXISTS t_student;
插入数据:

//插入命令:
INSERT INTO 表名
(字段名1,字段名2…..)
VALUES
(值1,值2。。。。)
//注意:值和字段一定要一致
//提示:SQLite的字段属性,只是给程序员看的,存什么都行

INSERT INTO t_student
(age, score, name)
VALUES
(’28’, 100, ‘zhangsan’);
更新数据:

//更新数据:
UPDATE 表名
SET
字段1 = ‘值1’ , //提示: 如果是字符串,需要使用单引号引起来
字段2 = 值2 ,…
/*更新记录的name*/
UPDATE t_student SET name = ‘zhangsan’;
//注意:如果更新时没有指定更新哪一条记录,那么会更新所有的数据

删除数据:

//删除指令:
//注意:删除数据的时候,一定要设置条件
//如果条件不满足,什么也不会发生

DELETE FROM 表名
WHERE 主键 = 值;

//例如:
DELETE FROM t_student;
DELETE FROM t_student WHERE age < 50;

查询数据:

//查询指令:
/* 分页 */
SELECT * FROM t_student
ORDER BY id ASC LIMIT 30, 10;
/* 排序 */
SELECT * FROM t_student
WHERE score > 50
ORDER BY age DESC;
SELECT * FROM t_student
WHERE score < 50
ORDER BY age ASC , score DESC;
/* 计量 */
SELECT COUNT(*)
FROM t_student
WHERE age > 50;
/* 别名 */
SELECT name as myName, age as myAge, score as myScore
FROM t_student;
SELECT name myName, age myAge, score myScore
FROM t_student;
SELECT s.name myName, s.age myAge, s.score myScore
FROM t_student s
WHERE s.age > 50;
/* 查询 */
SELECT name, age, score FROM t_student;
SELECT * FROM t_student;
数据库的基本使用:

@property (nonatomic, assign) sqlite3 *db;
/*
1.创建数据库
2.创建表(指定字段)
3.操作数据库
*/
/*
*个参数:是告诉系统数据库文件在什么地方.
>如果文件不存在就会自动创建, 然后再打开
>如果数据库存在就会自动打开
第二个参数:返回打开的数据库对象
*/
// 1.拼接数据库地址
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject];
NSString *sqlFile = [path stringByAppendingPathComponent:@”student.sqlite”];
// sqlite3 *db = NULL;
// 2.打开数据库
int result = sqlite3_open(sqlFile.UTF8String, &_db);// [self db]
// 3.判断是否打开成功
if (result == SQLITE_OK) {
NSLog(@”打开成功”);
// 3.1创建表
/*
*个参数: 需要执行SQL语句的数据库对象
第二个参数: 需要执行的SQL语句
第三个参数: 回调函数
第四个参数: 第三个参数的参数
第五个参数: 接收错误信息
*/
NSString *sql = @”CREATE TABLE IF NOT EXISTS t_student(id INTEGER PRIMARY KEY AUTOINCREMENT , name TEXT, age INTEGER, score REAL);”;
result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
NSLog(@”创建表成功”);
}else
{
NSLog(@”创建表失败”);
}
}else
{
NSLog(@”打开失败”);
}
– (IBAction)insertClick:(id)sender {

NSString *sql = @”INSERT INTO t_student(age, score, name) VALUES (’28’, 100, ‘jonathan’);”;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
NSLog(@”插入成功”);
}
}

– (IBAction)updateClick:(id)sender {
NSString *sql = @”UPDATE t_student SET name = ‘XXX’;”;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
NSLog(@”修改成功”);
}
}

– (IBAction)deleteClick:(id)sender {
NSString *sql = @”DELETE FROM t_student WHERE id = 1; “;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
NSLog(@”删除成功”);
}6
}

– (IBAction)selectClick:(id)sender {
/*
sqlite3操作中, 所有DML语句都是使用sqlite3_exec函数执行SQL语句即可,
但是如果是需要查询数据库, 不能使用sqlite3_exec, 因为它并没有返回查询到得结果发给我们
*/
/*
NSString *sql = @”SELECT * FROM t_student;”;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
NSLog(@”查询成功”);
}
*/
// 1.准备查询
/*
*个参数:需要执行SQL语句的数据库
第二个参数:需要执行的SQL语句
第三个参数: 告诉系统SQL语句的长度, 如果传入一个小于0的数, 系统会自动计算
第四个参数:结果集, 里面存放所有查询到的数据(不严谨)
*/
NSString *sql = @”SELECT * FROM t_student;”;
sqlite3_stmt *stemt = NULL;
sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stemt, NULL);
// 2.判断有没有查询结果
// int result = sqlite3_step(stemt);
// if (result == SQLITE_ROW) {
while (sqlite3_step(stemt) == SQLITE_ROW) {
// NSLog(@”查询到了数据”);
// 3.取出查询到得结果
const unsigned char *name = sqlite3_column_text(stemt, 1);
int age = sqlite3_column_int(stemt, 2);
double score = sqlite3_column_double(stemt, 3);
NSLog(@”%s %d %f”, name, age, score);
}
}

数据库的基本的封装:
@interface SQLManager : NSObject

+ (instancetype)shareManage;
//插入新数据
– (BOOL)insertStudent:(HMStudent *)student;
//删除数据
– (BOOL)deleteWithStudent:(HMStudent *)student;
//更新数据
– (BOOL)updateWithStudent:(HMStudent *)student;
//查询数据
– (NSArray *)query;
@end
#import “SQLManager.h”
#import <sqlite3.h>

@implementation SQLManager

static SQLManager *_instance;
+ (instancetype)shareManage
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[SQLManager alloc] init];
});
return _instance;
}

sqlite3 *_db;
+ (void)initialize
{
// 1.拼接数据库地址
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject];
NSString *sqlFile = [path stringByAppendingPathComponent:@”student.sqlite”];
// 2.打开数据库
int result = sqlite3_open(sqlFile.UTF8String, &_db);// [self db]
// 3.判断是否打开成功
if (result == SQLITE_OK) {
NSLog(@”打开成功”);
// 3.1创建表
NSString *sql = @”CREATE TABLE IF NOT EXISTS t_student(id INTEGER PRIMARY KEY AUTOINCREMENT , name TEXT, age INTEGER, score REAL);”;
result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
}
}

– (BOOL)insertStudent:(HMStudent *)student
{
NSString *sql = [NSString stringWithFormat: @”INSERT INTO t_student(age, score, name) VALUES (%d, %f, ‘%@’);”, student.age, student.score, student.name];
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
return YES;
}
return NO;
}
– (BOOL)deleteWithStudent:(HMStudent *)student
{
NSString *sql = [NSString stringWithFormat:@”DELETE FROM t_student WHERE id = %d;”, student.ID];
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
return YES;
}
return NO;
}
– (BOOL)updateWithStudent:(HMStudent *)student
{

NSString *sql = [NSString stringWithFormat:@”UPDATE t_student SET name = ‘%@’;”, student.name];
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, NULL);
if (result == SQLITE_OK) {
return YES;
}
return NO;
}
– (NSArray *)query
{
NSString *sql = @”SELECT * FROM t_student;”;
sqlite3_stmt *stemt = NULL;
sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stemt, NULL);
// 2.判断有没有查询结果
NSMutableArray *arrM = [NSMutableArray array];
while (sqlite3_step(stemt) == SQLITE_ROW) {
// 3.取出查询到得结果
const unsigned char *name = sqlite3_column_text(stemt, 1);
int age = sqlite3_column_int(stemt, 2);
double score = sqlite3_column_double(stemt, 3);
HMStudent *stu = [[HMStudent alloc] init];
stu.name = [NSString stringWithUTF8String:name];
stu.age = age;
stu.score = score;
[arrM addObject:stu];
}
return arrM;
}
@end
FMDB的基本使用:

以OC的方式封装了SQLite的C语言API.

优点是:

使用起来更加面向对象,省去麻烦冗余的C语言的代码,对比苹果的Core Data 框架,更加的轻量级和灵活,提供了多线程安全的数据库操作方法,有效防止数据混乱。

FMDB有三个三个主要的类:

FMDataBase :一个FMDataBase 对象就代表一个单独的DataBase数据库,用来执行SQL语句。

FMResultSet: 使用FMResultSet执行查询后的结果集。

FMDataBaseQueue: 用于多线程执行多个查询或者更新,他是线程安全的。
@property (nonatomic, strong) FMDatabase *db;
@implementation ViewController
– (void)viewDidLoad {
[super viewDidLoad];
/*
1.创建数据库
2.创建表
3.操作
*/
// 1.拼接数据库地址
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject];
NSString *sqlFile = [path stringByAppendingPathComponent:@”student.sqlite”];
//该方法接收一个路径, 它会根据地址创建一个数据库, 如果不存在就会自动创建
FMDatabase *db = [FMDatabase databaseWithPath:sqlFile];
self.db = db;
if([db open]){
// 2.创建表
NSString *sql = @”CREATE TABLE IF NOT EXISTS t_student(id INTEGER PRIMARY KEY AUTOINCREMENT , name TEXT, age INTEGER, score REAL);”;

// 注意: 在FMDB中除了查询以外的操作都称之为更新
if([db executeUpdate:sql]){
NSLog(@”创建表成功”);
}
}
}
– (IBAction)insertClick:(id)sender
{ NSString *sql = @”INSERT INTO t_student(age, score, name) VALUES (’28’, 100, ‘jonathan’);”;
if([self.db executeUpdate:sql])
{
NSLog(@”插入成功”);
}
}
– (IBAction)selectClick:(id)sender
{
NSString *sql = @”SELECT * FROM t_student;”;
// 查询数据, 会将所有查询到得数据, 放到results中
FMResultSet *results = [self.db executeQuery:sql];
// 从results中获取数据
while ([results next]) {
NSString *name = [results stringForColumn:@”name”];
int age = [results intForColumn:@”age”];
double score = [results doubleForColumn:@”score”];
NSLog(@”%@ %d %f”, name, age, score);
}
}
FMDataBaseQueue:

@property (nonatomic, strong) FMDatabaseQueue *dbQueue;
@implementation ViewController

– (void)viewDidLoad {
[super viewDidLoad];

// 1.拼接数据库地址
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject];
NSString *sqlFile = [path stringByAppendingPathComponent:@”student.sqlite”];
// 创建一个数据库队列
FMDatabaseQueue *dbQueue = [FMDatabaseQueue databaseQueueWithPath:sqlFile];
self.dbQueue = dbQueue;
//只要调用dbQueue的inDatabase方法, 系统就会传递一个已经打开并且线程安全的数据库给我们
[dbQueue inDatabase:^(FMDatabase *db) {
// 2.创建表
NSString *sql = @”CREATE TABLE IF NOT EXISTS t_student(id INTEGER PRIMARY KEY AUTOINCREMENT , name TEXT, age INTEGER, score REAL);”;

// 注意: 在FMDB中除了查询以外的操作都称之为更新
if([db executeUpdate:sql]){
NSLog(@”创建表成功”);
}
}];
}
– (IBAction)insertClick:(id)sender
{
[self.dbQueue inDatabase:^(FMDatabase *db) {
NSString *sql = @”INSERT INTO t_student(age, score, name) VALUES (’28’, 100, ‘jonathan’);”;
if([db executeUpdate:sql])
{
NSLog(@”插入成功”);
}
}];
}
– (IBAction)selectClick:(id)sender
{
[self.dbQueue inDatabase:^(FMDatabase *db) {
NSString *sql = @”SELECT * FROM t_student;”;
// 查询数据, 会将所有查询到得数据, 放到results中
FMResultSet *results = [db executeQuery:sql];
// 从results中获取数据
while ([results next]) {
NSString *name = [results stringForColumn:@”name”];
int age = [results intForColumn:@”age”];
double score = [results doubleForColumn:@”score”];
NSLog(@”%@ %d %f”, name, age, score);
}
}];
}
@end

事务:可以将多条SQL数据绑定在一起,要么一起执行成功要么一起执行失败。
CoreData:
Core Data框架提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite3数据库文件中,也能够将保存在数据库中的数据还原成OC对象。在此数据操作期间,不需要编写任何SQL语句。

 

Oracle Cloud 可能把测试版界面放上来了

不小心按了一下 F12,发现 console 里不停地蹦调试信息,难怪很多人说这是*难用的云计算操作台呢,简直是搞笑来的

F12 操作台 Oracle console37 条回复 • 2021-06-22 07:20:47 +08:00
Mitt 1
Mitt 2 天前 ❤️ 2
你是真不理解 DevTools 是干啥用的吗?
harwck 2
harwck 2 天前
也不知道是你在搞笑還是別人在搞笑
msg7086 3
msg7086 2 天前
笑笑,有谢到。
Rocketer 4
Rocketer 2 天前 via iPhone ❤️ 2
@Mitt 真不理解,求解惑。

我工作过的几个公司,都禁止在生产环境中打印信息,难道我是少数派,打印才是正常的?
Rocketer 5
Rocketer 2 天前 via iPhone ❤️ 1
@harwck 笑完了请解释一下为什么笑
ericls 6
ericls 2 天前 via iPhone
我觉得这个肯定是不对的,生产环境的错误信息应该直接到日志服务器 而不是用户
Kinnice 7
Kinnice 2 天前
从登录到创建完一台 VM 打印了大约 400 条日志。
mxT52CRuqR6o5 8
mxT52CRuqR6o5 2 天前 via Android
B 端项目不太在意这些东西吧
yohole 9
yohole 2 天前
同样上周开了 Oracle Cloud 用了几天了,发现它落后还是有原因的,控制台*其难用,很多常用的工具和信息混乱不堪,布局不合理
iAIR 10
iAIR 2 天前 ❤️ 2
我至今都搞不懂 Oracle 的账号系统为什么要设计得如此扭曲:
* 首先右上角登陆有 Cloud Account 和 Oracle Account 两个选项
* Cloud Account 有 Cloud Account Name (不是邮箱)
* 填完之后让你用 oracleidentitycloudservice 服务进行 SSO 登陆
* 然后又让你用 Oracle Cloud Account 的邮箱(不是用户名)和密码登陆

我知道这是先选择 Tenant,然后登陆该 Tenant 的 User 账号,但就不能学一下巨硬和谷歌根据 User 的邮箱自动判断属于哪个 Tenant 吗,而且全都叫 Oracle, Cloud, Account 这类词。

j3n5en 11
j3n5en 1 天前 via Android
都一样,你去 azure 看注册路上就一直蹦
wazggcd 12
wazggcd 1 天前 via Android
*坑的是账号强行绑定主区域,还不能换区,不知道为啥要这样设计
darknoll 13
darknoll 1 天前
oracle 是个不思进取的企业,就是个垃圾
labulaka521 14
labulaka521 1 天前 via iPhone
那个 sdk 超级复杂
changwei 15
changwei 1 天前
前三楼也挺自以为是的的
你们可以去看看像 Facebook,Twitter 这些国际大厂的 SPA 项目,除了找不到 map 文件和 xhr 消息以外,生产环节几乎是没有任何业务有关的日志信息的,稍微大一点的厂都知道用 sentry 等服务收集 C 端日志信息
反倒是国内的大厂,像百度一些 PC 端常年没有更新过的老旧网站也是和楼主说的 Oracle 一样在 console 里面打印一大堆的 warn 和 error
yuguorui96 16
yuguorui96 1 天前
楼主说的没问题,不停的弹调试信息基本就意味着赶工和质量堪忧。
learningman 17
learningman 1 天前
前面的别太自以为是了啊。。。大家都是程序员,生产环境一堆 console.log 肯定不合理啊
AkideLiu 18
AkideLiu 1 天前 via iPhone
可能 bug 太对改不过来,想一想用户大多也是程序员 /技术人员。没准顺手帮他们解决俩 bug 。狗头
jones2000 19
jones2000 1 天前
f12 调试多方便, 用后台日志收集多麻烦, 费人费时, 预算充足,人员超配的情况下可以考虑用. 把 console.log 覆盖了, 写一个提交后台日志不就可以了. 但是这样你起码要多配 1-2 个后台人员. 根据 2W 一个人每月, 1 年就要增加 24W-48W 成本, 还不算 5 险 1 金的钱. 说白了就钱没到位.
vk42 20
vk42 1 天前
@iAIR 感觉 Oracle 就是故意用屎一样的 UX 来劝退不挣钱的 C 端用户,B 端用户真正做决定的都是采购或者领导,真正会被恶心到的开发和维护也没啥话语权。另外还能顺便卖培训和认证证书服务,老本行了……
jhdxr 21
jhdxr 1 天前
说生产环境错误信息直接收集到日志服务器的都是后端开发吧???
Mitt 22
Mitt 1 天前
@Rocketer #4 你知道 windows 下有个软件叫 DebugView 吗,如果你打开这个软件你会发现 windows 上所有软件都在不停的给你报调试信息,包括 windows 自己,你会觉得 windows 是来搞笑的吗? DevTools 和 DebugView 性质一样,从你打开的那一刻你就开启了网站本身的调试模式,只不过是生产模式下的调试信息,你公司禁止在生产环境打印信息我不知道是出于什么目的,但是生产环境只要不是把开发版本发布出去,我不觉得这有啥问题

而且楼上说的前端收集日志反馈给后端的,这个是常规操作没错,但这不是唯一选啊,如果因为网络问题这些日志无法收集,难道前端 console 还不能把错误信息报出来吗,而且你作为一个用户你用 DevTools 本身就是个迷惑行为,大厂还用那么大的个字告诉你 console 粘贴代码执行是不安全的,从你决定打开 DevTools 的时候,这些调试信息就是给你看的,用 DevTools 不管是看 DOM 、console 、还是 network 本身都是“调试行为”

说真的,除非 console 里面把 oracle 内部私钥都给你打印出来,否则就算打印你输入的密码我也不觉得有啥大问题
ericls 23
ericls 1 天前
@jhdxr C 端一样可以,C 端没有 source map 的情况下 错误信息本来就不好看。

而且如果等到用户遇到问题的时候,你会让用户手动去打开 devtool 截图给你看吗?这就已经晚了。
ericls 24
ericls 1 天前
@Mitt 就像你说的,这些都是调试行为,所以你会在生产环境调试吗?
Rocketer 25
Rocketer 1 天前 via iPhone
@Mitt 你说明的也只是“可以”这么做,而不是“应该”这么做。

我不知道具体为什么我工作过的公司都禁止在生产环境用 console.log ,也许我们只是“猴子不能吃香蕉”的受害者。

但我确实亲自体验过 console.log 导致的内存泄漏——有时下班懒得关闭浏览器,放在那儿继续运行,打印量大的话第二天早上就很可能看到浏览器死住或者崩溃。

所以我猜不在生产环境打印调试信息应该有这方面的考虑,毕竟总有一些用户的配置很低,可能很快就内存泄漏了。

再就是楼上的问题——在生产环境打印调试信息,这是要给谁看呢?
rannnn 26
rannnn 1 天前
前几楼也挺自以为是的,显示和不显示都有自己的道理。我们公司发到日志服务器,也打印一部分 log,另一部分 dev 可以用工具开启。2B 每个客户有各种各样的 integration,很多时候 dev 环境是不能复现问题的。所以我们的确会在生产环境经过用户授权后 impersonate 调试。
rannnn 27
rannnn 1 天前
还有 B 端用户 report 问题一般都是通过 IT 部门反馈的,所以本身反馈的人也是技术人员。直接打印出来也是为了他们在 file ticket 的时候就能直接附上一些常见信息。前面列举的 Facebook Twitter 都是 2C 的产品,你开几个 2B 比如 Azure,SAP,Workday 就可以看到多多少少会 log 一些东西。
JoJoJoJ 28
JoJoJoJ 1 天前 via iPhone
@yohole aws 一样难用
Rocketer 29
Rocketer 1 天前 via iPhone
@rannnn 谢谢,27 楼的说法比较合理
netnr 30
netnr 1 天前 via Android
歪,昨天刚注册,密码搞忘了(规则跟常用密码冲突),邮件重置密码链接打开报 404,无语
Mitt 31
Mitt 1 天前 via iPhone
@Rocketer 这就纯搞笑了 首先如果 devtools 不打开 console.log 不会产生任何内存数据,其次哪个正常用户会开着 console 用网页,请不要吧把自己开发者身份代入,还是我说的,windows 自己就有 debugview 照你这么说 windows 岂不是每秒都在导致内存爆炸
Rocketer 32
Rocketer 1 天前 via iPhone
@Mitt 我是应用开发者,不是浏览器开发者,不懂浏览器的内存管理,谢谢扫盲
learningman 33
learningman 1 天前
@Mitt #22 你说的是事件管理器吧。。。我是没见几个没事往这里面冒泡的,设备驱动之类的倒是会,但是人家本来就是这么通信的啊
jhdxr 34
jhdxr 20 小时 51 分钟前
@ericls 我不知道是我还是你对于调试这个词有什么误解。『打印日志』都能算作调试了???另外,C 端(客户端为主,网页有但不多)的确也有日志采集,但这和把日志打印出来并不冲突。

@Rocketer 你主贴里表达的是『不应该』这么做。 @Mitt 已经说明了『可以』这么做,这就足以反驳『不应该』了。
Rocketer 35
Rocketer 16 小时 3 分钟前
@jhdxr 我就不抠字眼玩文字游戏了。

本质上就是我见识太少,因为我工作过的公司都只在测试版里才打印日志,我就以为这是行业惯例了。但真理往往掌握在少数人手里,所以你是对的。
Mitt 36
Mitt 8 小时 14 分钟前
@jhdxr #34 没有 我的意思是 DevTools 这个工具本身是属于调试工具,console.log 本身可能不完全算
Mitt 37
Mitt 8 小时 2 分钟前
@jhdxr #34 而且其实打印日志本身的主要目的还是为了调试( Debug ),包括 mac 上的 console 日志,其目的还是为了辅助调试,mac 崩溃的时候会把日志本身上传,其目的就是辅助回溯调试信息,因为这些信息都不是给正常用户看或用的,而是为了开发者。

求教, docker 安装 redis cluster 后, 客户端/redis-cli -c 跳转啥的访问到的是 docker 内部的 ip. 连接失败.

创建了一个 bridge network, 解决了容器之间网络访问问题, 但客户端连接 redis cluster 后, 获取到的节点的 ip 是 docker 分配给容器的 ip. 这肯定访问不了.

有大侠知道解决办法吗.. 头都秃噜皮了.

Docker Redis Cluster 容器4 条回复 • 2021-06-22 13:48:34 +08:00
nvkou 1
nvkou 3 小时 26 分钟前 via Android
容器之间使用容器名称访问。内部自带 Host 。
对外服务的端口映射回主机即可
BBCCBB 2
BBCCBB 3 小时 15 分钟前
@nvkou
> 对外服务的端口映射回主机即可

请问这个要咋操作呢? 添加一个 host network 吗?
nvkou 3
nvkou 2 小时 5 分钟前
@BBCCBB 这个拓扑不好说呢
在创建容器时,比如说 docker-compose 文件里给每个容器指定名称(比如 A,B), 然后这个容器组里指定一个网络(比如 network1). 然后你配置文件里就可以用名称指代容器.假设在容器创建时 A 获得 ip 10.0.0.5. 那么 b 的 redis 配置文件里可以简单用 A:6379 指代 10.0.0.5:6379 简单理解为 docker 内部有 host 帮你路由即可. 因为是桥接网络,从容器组外部访问容器(包括从宿主机)就必须走网桥. 因此你的宿主机要做好这一块的路由以便对本地或外部访问

端口映射是针对容器而言的. 对应 docker 的 -p 参数. 假设你的集群是固定的一个容器对外暴露服务,那么只需要使用 -p 参数 映射容器内部端口到宿主机端口即可.这部分魔法由 docker 提供.

举个例子 docker run tomcat -p 8080:80 起来一个容器之后, 在宿主机访问 localhost:80 即可访问到内部容器.
BBCCBB 4
BBCCBB 1 小时 33 分钟前
@nvkou 我现在容器之间访问没问题了. 外部也能访问.

但每个 redis 实例的 ip 是 docker 分配的. 比如

redis1: 172.18.0.1:6379, 这些 ip 都是 docker network 内部分配, 宿主机访问不到
redis2: 172.18.0.2:6379
redis3: 172.18.0.1:6379

redis1 的 port 映射比如是 50000:6379, 我在宿主机是能通过 127.0.0.1:50000 访问到单个节点的.

用 redis 客户端连接上之后, 获取到的 redis 集群的 ip 列表就是上面几个, 然后连接的时候就会 无法访问.

LeakCanary工具使用

LeakCanary工具使用

添加LeakCanary依赖包
https://github.com/square/leakcanary
在主模块app下的build.gradle下添加如下依赖:
debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.3.1’
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.3.1’

%title插图%num

开启LeakCanary
添加Application子类
首先创建一个ExampleApplication,该类继承于Application,在该类的onCreate方法中添加如下代码开启LeakCanary监控:
LeakCanary.install(this);

%title插图%num

在配置文件中注册ExampleApplication
在AndroidManifest.xml中的application标签中添加如下信息:
android:name=”.ExampleApplication”

%title插图%num

这个时候安装应用到手机,会自动安装一个Leaks应用,如下图:

%title插图%num
制造一个内存泄漏的点
建立一个ActivityManager类,单例模式,里面有一个数组用来保存Activity:
package com.example.android.sunshine.app;
import android.app.Activity;
import android.util.SparseArray;
import android.view.animation.AccelerateInterpolator;
import java.util.List;
public class ActivityManager {
private SparseArray container = new SparseArray();
private int key = 0;
private static ActivityManager mInstance;
private ActivityManager(){}
public static ActivityManager getInstance(){
if(mInstance == null){
mInstance = new ActivityManager();
}
return mInstance;
}

public void addActivity(Activity activity){
container.put(key++,activity);
}

}
然后在DetailActivity中的onCreate方法中将当前activity添加到ActivityManager的数组中:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ActivityManager.getInstance().addActivity(this);
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.

Bundle arguments = new Bundle();
arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());

DetailFragment fragment = new DetailFragment();
fragment.setArguments(arguments);

getSupportFragmentManager().beginTransaction()
.add(R.id.weather_detail_container, fragment)
.commit();
}
}

我们从首页跳转到详情页的时候会进入DetailActivity的onCreate的方法,然后就将当前activity添加到了数组中,当返回时,我们没把他从数组中删除。再次进入的时候,会创建新的activity 并添加到数组中,但是之前的activity仍然被引用,无法释放,但是这个activity不会再被使用,这个时候就造成了内存泄漏。我们来看看LeakCanary是如何报出这个问题的。
演示

%title插图%num

解析的过程有点耗时,所以需要等待一会才会在Leaks应用中,当我们点开某一个信息时,会看到详细的泄漏信息:

%title插图%num

————————————————

群晖用哪种硬盘格式比较好呢?

EXT4 ? BTRFS ?其他? Raid 在磁盘出现故障时,恢复数据的几率大吗? 比如 2 块盘,坏了 1 块,会不会出现一定几率无法恢复 是 1 台 nas 开启 raid 好呢?还是 2 台 nas 备份好呢

NAS raid 几率 ext49 条回复 • 2021-06-22 15:09:03 +08:00
XiLingHost 1
XiLingHost 41 分钟前
数据的安全还是靠备份,硬盘格式用成熟稳定的就行,没必要开 raid,raid 是为了可以热恢复
banricho 2
banricho 39 分钟前
双盘 raid 1 或者三盘以上 raid 5
常见四槽可以三个组 raid 5,剩下那个当下载盘或者 SSD (有 M.2 的 NAS 就不用纠结这个问题)

至于你想保证数据安全的话,光 NAS 肯定不能 100% 保证,重要资料*好用 Cloud Sync 同步到网盘。
HDMItoDP 3
HDMItoDP 33 分钟前
@banricho 哦哦,重要数据我准备在 onedrive 存一份了
HDMItoDP 4
HDMItoDP 28 分钟前
就是担心 raid 打开了,数据依然有无法恢复的可能
LokiSharp 5
LokiSharp 25 分钟前
用默认的 btrfs,用其他的有些特性会用不了。然后可靠性的话,重要数据 Glacier Backup 备份一份到 AWS
gscsnm 6
gscsnm 25 分钟前
就用 ext4 就行,我的方案如下,在加上同步云盘就更好了(还没做)。另外,raid0 就是单纯的为了提高老硬盘的读写速度:
重要资料(照片、个人文档之类的):2 块 2T 硬盘组 raid1,+ 脚本自动同步到第 3 块硬盘上 +定期移动硬盘备份;
没那么重要的(可以接受短期内更新丢失的):raid0 + 定期移动硬盘备份;
不重要的(随意丢的):raid0 ;
hotcool100 7
hotcool100 21 分钟前
920+ 默认格式,raid1 8T 资料 /相册 + 8T 影视 + 4T 下载 ,onedirve 云备份
HDMItoDP 8
HDMItoDP 7 分钟前
@LokiSharp 这个格式稳定吗
HDMItoDP 9
HDMItoDP 6 分钟前
@hotcool100 脚本自动同步到第 3 块硬盘上。这个第三块硬盘也在本台 nas 里面吗?

LeakCanary原理解析

简介

LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。github的地址为https://github.com/square/leakcanary

使用方式解析

将LeakCanary引入AS,在Application中调用如下方法,可以跟踪Activity是否被GC回收。

%title插图%num

入口函数

LeakCanary.install()方法的调用流程如下所示:

%title插图%num

install方法调用流程

Install方法如下:

%title插图%num

install方法

其中listenerServiceClass方法传入了展示分析结果的Service(DisplayLeakService);excludedRefs方法排除了开发中可以忽略的泄漏路径;buildAndInstall是主要的函数,实现了activity是否能被释放的监听。

%title插图%num

buildAndInstall

buildAndInstall会调用ActivityRefWatcher.install来监测Activity。

%title插图%num

install

*终调用了watchActivities():

%title插图%num

watchActivities

通过registerActivityLifecycleCallbacks来监听Activity的生命周期:

%title插图%num

lifecycleCallbacks

lifecycleCallbacks监听Activity的onDestroy方法,正常情况下activity在onDestroy后需要立即被回收,onActivityDestroyed方法*终会调用RefWatcher.watch方法:

%title插图%num

watch

监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收。检查方法如下:

%title插图%num

ensureGone

1、  首先通过removeWeaklyReachablereference来移除已经被回收的Activity引用

2、 通过gone(reference)判断当前弱引用对应的Activity是否已经被回收,如果已经回收说明activity能够被GC,直接返回即可。

3、  如果Activity没有被回收,调用GcTigger.runGc方法运行GC,GC完成后在运行第1步,然后运行第2步判断Activity是否被回收了,如果这时候还没有被回收,那就说明Activity可能已经泄露。

4、  如果Activity泄露了,就抓取内存dump文件(Debug.dumpHprofData)

%title插图%num

dumpHeap

5、  之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析

%title插图%num

分析dump

接着通过HeapAnalyzer(checkForLeak—findLeakingReference—findLeakTrace)来进行内存泄漏分析。

6、  *后通过DisplayLeakService进行内存泄漏的展示。

LeakCanary原理分析

导语:

提到Java语言的特点,无论是教科书还是程序员一般都会罗列出面向对象、可移植性及安全等特点。但如果你是一位刚从C/C++转到Java的程序员,对Java语言的特性除了面向对象之外,*外直接的应当是在Java虚拟机(JVM)在内存管理方面给我们变成带来的便利。JVM的这一大特性使Java程序员从繁琐的内存管理工作中得到了一定解放,但是JVM的这个特点的实现也是有代价的,并且它也并非万能。因此如果一个编程习惯不好的Java程序员如果完全将内存回收寄希望于JVM,那么OOM(Out Of Memory)就已经悄悄潜伏在了他的程序之中。

Android应用基于Java实现,因此它也将Java的优缺点继承了过来。相对来说,移动设备对于内存问题更为敏感,程序在申请一定的内存但又没有及时得到释放后就很容易发生OOM而导致crash。因此Android程序员开发过程中一般都会定时排查自己程序中可能出现的这些雷点,尽可能地避免因为crash问题而影响用户体验。

1.LeakCanary简介

目前Java程序*常用的内存分析工具应该是MAT(Memory Analyzer Tool),它是一个Eclipse插件,同时也有单独的RCP客户端,也可以通过官网的SVN下载到它的源码(具体见另一篇《compile-MAT》)并编译成jar包。LeakCanary本质上就是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,通过集成这个工具代码到自己的Android工程当中就能够在程序调试开发过程中通过一个漂亮的界面(如下图)随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

LeakCanary_result

总的来说,LeakCanary有如下几个明显优点:

  • 针对Android Activity组件完全自动化的内存泄漏检查。
  • 可定制一些行为(dump文件和leaktrace对象的数量、自定义例外、分析结果的自定义处理等)。
  • 集成到自己工程并使用的成本很低。
  • 友好的界面展示和通知。

假如你现在想集成LeakCanary到自己的工程中,那么你只需要做以下工作:1. 导入leakcanary的jar包到自己工程(下载链接:leakcanary.zip)2. 在4.0以上,只需要在工程的Application的onCreate函数中按照如下的方式加入一行代码:

  1. public class ExampleApplication extends Application {
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. LeakCanary.install(this);
  6. }
  7. }

4.0以下在需要进行内存泄漏监控的Activity的onDestroy方法中按如下加入代码:

  1. protected void onDestroy() {
  2. super.onDestroy();
  3. // start watch
  4. HeapDump.Listener heapDumpListener =
  5. new ServiceHeapDumpListener(this, listenerServiceClass);
  6. DebuggerControl debuggerControl = new AndroidDebuggerControl();
  7. AndroidHeapDumper heapDumper = new AndroidHeapDumper();
  8. heapDumper.cleanup();
  9. ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults().build();
  10. RefWatcher refWatcher = new RefWatcher(new AndroidWatchExecutor(), debuggerControl, GcTrigger.DEFAULT,
  11. heapDumper, heapDumpListener, excludedRefs);
  12. }   第二种情况下,在有多个Activity需要检测的情况看起来稍显繁琐,实际上可以用以上方法实现一个基类Activity,之后需要内存泄漏检测的Activity直接继承这个基类Activity就不需要每次都重复处理oonDestroy方法了。并且以上代码只作为示例,实际上每次watch的时候并不需要重新new一个RefWatcher对象,因为这个对象是可以重复使用的。

完成了以上两个步骤后,LeakCanary就可以为你的工程服务了,这之中需要我们自己处理的工作很少,相比较我们自己手工用MAT进行内存泄漏检测而言,确实方便了很多。## 2.LeakCanary原理分析 ##这么强大的工具,它是如何实现的呢,引用LeakCanary中文使用说明,它的基本工作原理如下:

  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, 并以通知的形式展示出来。

但事实上一切并没那么简单,LeakCanary的设计者在实现的时候实际上为我们考虑了很多细节。可以通过源码分析来走一遍一次内存泄漏检查的流程。在一个Activity生命周期结束调用oonDestroy方法的时候会触发LeakCanary进行一次内存泄漏检查,LeakCanary开始进行检查的入口函数实际上是RefWatcher类的,watch方法,其源码如下:

  1. public void watch(Object watchedReference, String referenceName) {
  2. String key = UUID.randomUUID().toString();
  3. retainedKeys.add(key);
  4. final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
  5. watchExecutor.execute(new Runnable() {
  6. @Override
  7. public void run() {
  8. ensureGone(reference, watchStartNanoTime);
  9. }
  10. });
  11. } 这个函数做的主要工作就是为需要进行泄漏检查的Activity创建一个带有唯一key标志的弱引用,并将这个弱引用key保存至retainedKeys中,然后将后面的工作交给watchExecutor来执行。这个watchExecutor在LeakCanary中是AndroidWatchExecutor的实例,调用它的execute方法实际上就是向主线程的消息队列中插入了一个IdleHandler消息,这个消息只有在对应的消息队列为空的时候才会去执行,因此RefWatcher的watch方法就保证了在主线程空闲的时候才会去执行ensureGone方法,防止因为内存泄漏检查任务而严重影响应用的正常执行。ensureGone的主要源码如下:
  12. void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
  13. removeWeaklyReachableReferences();
  14. if (gone(reference) || debuggerControl.isDebuggerAttached()) {
  15. return;
  16. }
  17. gcTrigger.runGc(); // 手动执行一次gc
  18. removeWeaklyReachableReferences();
  19. if (!gone(reference)) {
  20. long startDumpHeap = System.nanoTime();
  21. long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap – gcStartNanoTime);
  22. File heapDumpFile = heapDumper.dumpHeap();
  23. if (heapDumpFile == null) {
  24. // Could not dump the heap, abort.
  25. Log.d(TAG, “Could not dump the heap, abort.”);
  26. return;
  27. }
  28. long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() – startDumpHeap);
  29. heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
  30. watchDurationMs, gcDurationMs, heapDumpDurationMs));
  31. }
  32. } 因为这个方法是在主线程中执行的,因此一般执行到这个方法中的时候之前被destroy的那个Activity的资源应该被JVM回收了,因此这个方法首先调用removeWeaklyReachableReferences方法来将引用队列中存在的弱引用从retainedKeys中删除掉,这样,retainedKeys中保留的就是还没有被回收对象的弱引用key。之后再用gone方法来判断我们需要检查的Activity的弱引用是否在retainedKeys中,如果没有,说明这个Activity对象已经被回收,检查结束。否则,LeakCanary主动触发一次gc,再进行以上两个步骤,如果发现这个Activity还没有被回收,就认为这个Activity很有可能泄漏了,并dump出当前的内存文件供之后进行分析。

之后的工作就是对内存文件进行分析,由于这个过程比较耗时,因此*终会把这个工作交给运行在另外一个进程中的HeapAnalyzerService来执行。HeapAnalyzerService通过调用HeapAnalyzer的checkForLeak方法进行内存分析,其源码如下:

  1. public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
  2. ISnapshot snapshot = null;
  3. try {
  4. snapshot = openSnapshot(heapDumpFile);
  5. IObject leakingRef = findLeakingReference(referenceKey, snapshot);
  6. // False alarm, weak reference was cleared in between key check and heap dump.
  7. if (leakingRef == null) {
  8. return noLeak(since(analysisStartNanoTime));
  9. }
  10. String className = leakingRef.getClazz().getName();
  11. AnalysisResult result =
  12. findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
  13. if (!result.leakFound) {
  14. result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
  15. }
  16. return result;
  17. } catch (SnapshotException e) {
  18. return failure(e, since(analysisStartNanoTime));
  19. } finally {
  20. cleanup(heapDumpFile, snapshot);
  21. }
  22. }

这个方法进行的*步就是利用HAHA将之前dump出来的内存文件解析成Snapshot对象,其中调用到的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。解析得到的Snapshot对象直观上和我们使用MAT进行内存分析时候罗列出内存中各个对象的结构很相似,它通过对象之间的引用链关系构成了一棵树,我们可以在这个树种查询到各个对象的信息,包括它的Class对象信息、内存地址、持有的引用及被持有的引用关系等。到了这一阶段,HAHA的任务就算完成,之后LeakCanary就需要在Snapshot中找到一条有效的到被泄漏对象之间的引用路径。首先它调用findLeakTrace方法来从Snapshot中找到被泄漏对象,源码如下:

  1. private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException {
  2. Collection<IClass> refClasses =
  3. snapshot.getClassesByName(KeyedWeakReference.class.getName(), false);
  4. if (refClasses.size() != 1) {
  5. throw new IllegalStateException(
  6. “Expecting one class for “ + KeyedWeakReference.class.getName() + ” in “ + refClasses);
  7. }
  8. IClass refClass = refClasses.iterator().next();
  9. int[] weakRefInstanceIds = refClass.getObjectIds();
  10. for (int weakRefInstanceId : weakRefInstanceIds) {
  11. IObject weakRef = snapshot.getObject(weakRefInstanceId);
  12. String keyCandidate =
  13. PrettyPrinter.objectAsString((IObject) weakRef.resolveValue(“key”), 100);
  14. if (keyCandidate.equals(key)) { // 匹配key
  15. return (IObject) weakRef.resolveValue(“referent”); // 定位泄漏对象
  16. }
  17. }
  18. throw new IllegalStateException(“Could not find weak reference with key “ + key);
  19. }

为了能够准确找到被泄漏对象,LeakCanary通过被泄漏对象的弱引用来在Snapshot中定位它。因为,如果一个对象被泄漏,一定也可以在内存中找到这个对象的弱引用,再通过弱引用对象的referent就可以直接定位被泄漏对象。下一步的工作就是找到一条有效的到被泄漏对象的*短的引用,这通过findLeakTrace来实现,实际上寻找*短路径的逻辑主要是封装在PathsFromGCRootsComputerImpl这个类的getNextShortestPath和processCurrentReferrefs这两个方法当中,其源码如下:

  1. public int[] getNextShortestPath() throws SnapshotException {
  2. switch (state) {
  3. case 0: // INITIAL
  4. {
  5. }
  6. case 1: // FINAL
  7. return null;
  8. case 2: // PROCESSING GC ROOT
  9. {
  10. }
  11. case 3: // NORMAL PROCESSING
  12. {
  13. int[] res;
  14. // finish processing the current entry
  15. if (currentReferrers != null) {
  16. res = processCurrentReferrefs(lastReadReferrer + 1);
  17. if (res != null) return res;
  18. }
  19. // Continue with the FIFO
  20. while (fifo.size() > 0) {
  21. currentPath = fifo.getFirst();
  22. fifo.removeFirst();
  23. currentId = currentPath.getIndex();
  24. currentReferrers = inboundIndex.get(currentId);
  25. if (currentReferrers != null) {
  26. res = processCurrentReferrefs(0);
  27. if (res != null) return res;
  28. }
  29. }
  30. return null;
  31. }
  32. default:
  33. }
  34. }
  35. private int[] processCurrentReferrefs(int fromIndex) throws SnapshotException {
  36. GCRootInfo[] rootInfo = null;
  37. for (int i = fromIndex; i < currentReferrers.length; i++) {
  38. }
  39. for (int referrer : currentReferrers) {
  40. if (referrer >= 0 && !visited.get(referrer) && !roots.containsKey(referrer)) {
  41. if (excludeMap == null) {
  42. fifo.add(new Path(referrer, currentPath));
  43. visited.set(referrer);
  44. } else {
  45. if (!refersOnlyThroughExcluded(referrer, currentId)) {
  46. fifo.add(new Path(referrer, currentPath));
  47. visited.set(referrer);
  48. }
  49. }
  50. }
  51. }
  52. return null;
  53. }
  54. }

为了是逻辑更清晰,在这里省略了对GCRoot的处理。这个类将整个内存映像信息抽象成了一个以GCRoot为根的树,getNextShortestPath的状态3是对一般节点的处理,由于之前已经定位了被泄漏的对象在这棵树中的位置,为了找到一条到GCRoot*短的路径,PathsFromGCRootsComputerImpl采用的方法是类似于广度优先的搜索策略,在getNextShortestPath中从被泄漏的对象开始,调用一次processCurrentReferrefs将持有它引用的节点(父节点),加入到一个FIFO队列中,然后依次再调用getNextShortestPath和processCurrentReferrefs来从FIFO中取节点及将这个节点的父节点再加入FIFO队列中,一层一层向上寻找,哪条路径*先到达GCRoot就表示它应该是一条*短路径。由于FIFO保存了查询信息,因此如果要找次*短路径只需要再调用一次getNextShortestPath触发下一次查找即可,其算法原理如下图所示。

LeakCanary_result

至此,主要的工作就完成了,后面就是调用buildLeakTrace构建查询结果,这个过程相对简单,仅仅是将之前查找的*短路径转换成*后需要显示的LeakTrace对象,这个对象中包括了一个由路径上各个节点LeakTraceElement组成的链表,代表了检查到的*短泄漏路径。*后一个步骤就是将这些结果封装成AnalysisResult对象然后交给DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便之后能够直接看到*近几次内存泄露的分析结果,同时以notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。

以上就是对LeakCanary源码的分析,中间省略了一些细节处理的说明,但不得不提的是LeakCanary支持自定义泄漏豁对象ExcludedRefs的集合,这些豁免对象一般都是一些已知的系统泄漏问题或者自己工程中已知但又需要被排除在检查之外的泄漏问题构成的。LeakCanary在findLeakTrace方法中如果发现这个集合中的对象存在于泄漏路径上,就会排除掉这条泄漏路径并尝试寻找下一条。

LeakCanary原理及使用

一、LeakCanary简介
LeakCanary是Square公司为Android开发者提供的一个自动检测内存泄漏的工具,LeakCanary本质上是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,我们可以通过集成LeakCanary提供的jar包到自己的工程中,一旦检测到内存泄漏,LeakCanary就会dump Memory信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

二、LeakCanary工作机制
RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP对应的文件系统中的一个 .hprof 文件中。
在另外一个进程中的 HeapAnalyzerService 有一个HeapAnalyzer 使用HAHA 解析这个文件。
得益于唯一的 reference key, HeapAnalyzer 找到KeyedWeakReference,定位内存泄露。
HeapAnalyzer 计算到 GC roots的*短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
引用链传递到 APP 进程中的DisplayLeakService, 并以通知的形式展示出来。
三、LeakCanary的Android Studio集成
1. 在build.gradle中添加LeakCanary的依赖包
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’
releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’

注意: debug和release版本要一致,否则会报错。

2. 在我们自定义Application的onCreate方法中注册LeakCanary
public class LeakApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) { //1
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

注释1处的代码用来进行过滤操作,如果当前的进程是用来给LeakCanary 进行堆分析的则return,否则会执行LeakCanary的install方法。这样我们就可以使用LeakCanary了,如果检测到某个Activity 有内存泄露,LeakCanary 就会给出提示。

3. 重写Application
上述代码只能够检测Activity的内存泄漏,当然还存在其他类的内存泄漏,这时我们就需要使用RefWatcher来进行监控。重写Application,如下所示:

public class LeakApplication extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}

install方法会返回RefWatcher用来监控对象,LeakApplication中还要提供getRefWatcher静态方法来返回全局RefWatcher。
注意: 需要在AndroidManifest.xml文件中添加android:name=”.LeakApplication”,指定Application子类,当应用启动时,这个类的实例被*个创建。这个属性是可选的,大多数APP都不需要这个属性。在没有这个属性的时候,Android会启动一个Application类的实例。

%title插图%num
4. 在activity或fragment中使用leak canary举例
public class SearchActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(6 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LeakApplication.getRefWatcher(this);//1
refWatcher.watch(this);
}
}

SearchActivity存在内存泄漏,原因就是非静态内部类LeakThread持有外部类SearchActivity的引用,LeakThread中做了耗时操作,导致SearchActivity无法被释放。
在注释1处得到RefWatcher,并调用它的watch方法,watch方法的参数就是要监控的对象。当然,在这个例子中onDestroy方法是多余的,因为LeakCanary在调用install方法时会启动一个ActivityRefWatcher类,它用于自动监控Activity执行onDestroy方法之后是否发生内存泄露。这里只是为了方便举例,如果想要监控Fragment,在Fragment中添加如上的onDestroy方法是有用的。

public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

运行程序,这时会在界面生成一个名为Leaks的应用图标。接下来不断的切换横竖屏,这时会闪出一个提示框,提示内容为:“Dumping memory, app will freeze.Brrrr.”。再稍等片刻,内存泄漏信息就会通过Notification展示出来,比如荣耀magic的通知栏如下图1所示。
Notification中提示了SearchActivity发生了内存泄漏。点击Notification就可以进入内存泄漏详细页,除此之外也可以通过Leaks应用的列表界面进入,列表界面如下图2所示。内存泄漏详细页如下图3所示。
点击加号就可以查看具体类所在的包名称。整个详情就是一个引用链:SearchActivity的内部类LeakThread引用了LeakThread的this$0,this$0的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是详情*后一行所给出的SearchActivity的实例,这将会导致SearchActivity无法被GC(garbage collection,垃圾回收),从而产生内存泄漏。
图1:%title插图%num

图2:%title插图%num

图3:%title插图%num

解决该内存泄露的方法就是将LeakThread改为静态内部类。再次运行程序LeakThread就不会给出内存泄漏的提示了。


static class LeakThread extends Thread {

}

四、LeakCanary2使用
1. 和 LeakCanary1 相比,LeakCanary2 有以下改动
完全使用 Kotlin 重写。
使用新的Heap分析工具Shark,替换到之前的haha,按官方的说法,内存占用减少了10倍。
泄露类型分组。
2. LeakCanary2集成
只需要增加以下依赖即可:

debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.2’
1
LeakCanary2 实现了自动调用 install() 方法,实现方式是使用的 ContentProvider,相关代码位于 leakcanary-object-watcher-android 模块中的 AppWatcherInstaller.kt 中。
AppWatcherInstaller 继承 ContentProvider,重写了 onCreate() 方法,这里利用的是,注册在 Manifest 文件中的 ContentProvider,会在应用启动时,由 ActivityThread 创建并初始化。
————————————————

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速