iOS开发 - 第05篇 - 项目 - 13 - 离线缓存

1、离线缓存


对于之前的微博项目,同理其他类似的新闻类应用,为了节省用户流量,并且保存上一次的浏览记录,应该制作离线缓存,每次打开的时候自动加载离线缓存,显示数据, 用户需要查看最新数据的时候进行手动刷新。

1:微博数据量很大,必须使用数据库来制作离线缓存;

2:每次加载离线缓存的时候,不是一下子将离线缓存全部加载进内存,而是加载最新的20条,上拉刷新的时候,再从缓存中加载20条,直到缓存数据全部加载完,还需要加载以前的微博数据的时候再去发送网络请求;


2、设计思路


iOS开发 - 第05篇 - 项目 - 13 - 离线缓存_第1张图片


1:在移动端使用数据库的时候,不需像服务器那样,每个模型对应一张表,同时还需要建立表与表之间的关系(比如微博模型中有用户模型、图片模型数组等等),这样在移动端太复杂;

2:可以将一条微博数据模型或者新浪返回的一条微博字典,打包成NSData的二进制数据,作为一条数据;

3:再添加一个idstr字段,用于到时候判断需要加载比idstr小或者大的微博数据,即最新或者以前的微博数据,来方便查询;


iOS开发 - 第05篇 - 项目 - 13 - 离线缓存_第2张图片


3、对象存入数据库方法


实现:将HMShop模型存入数据库

注:存对象的时候要将对象打包成NSData!!!

#import "HMShop.h"

@implementation HMShop

- (void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeDouble:self.price forKey:@"price"];
}

- (id)initWithCoder:(NSCoder *)decoder
{
    if (self = [super init]) {
        self.name = [decoder decodeObjectForKey:@"name"];
        self.price = [decoder decodeDoubleForKey:@"price"];
    }
    return self;
}

@end
#import "HMViewController.h"
#import "HMShop.h"
#import "FMDB.h"

@interface HMViewController ()
@property (nonatomic, strong) FMDatabase *db;
@end

@implementation HMViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [self setup];
    
//  [self addShops];
    [self readShops];
}

- (void)setup
{
    // 初始化
    NSString *path = @"/Users/apple/Desktop/shops.data";
    self.db = [FMDatabase databaseWithPath:path];
    [self.db open];
    
    // 2.创表
    [self.db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_shop (id integer PRIMARY KEY, shop blob NOT NULL);"];
}

- (void)addShops
{
    /* 直接利用NSKeyedArchiver存储,不适用于大批量数据!!! */
//    NSMutableArray *shops = [NSMutableArray array];
//    for (int i = 0; i<1000; i++) {
//        HMShop *shop = [[HMShop alloc] init];
//        shop.name = [NSString stringWithFormat:@"商品--%d", i];
//        shop.price = arc4random() % 10000;
//        [shops addObject:shop];
//    }
//    [NSKeyedArchiver archiveRootObject:shops toFile:@"/Users/apple/Desktop/shops.data"];

    /* 利用数据库存储 */
    for (int i = 0; i<100; i++) {
        HMShop *shop = [[HMShop alloc] init];
        shop.name = [NSString stringWithFormat:@"商品--%d", i];
        shop.price = arc4random() % 10000;
        
        NSData *data = [NSKeyedArchiver archivedDataWithRootObject:shop];
        [self.db executeUpdateWithFormat:@"INSERT INTO t_shop(shop) VALUES (%@);", data];
    }
}

- (void)readShops
{
//    NSMutableArray *shops = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/apple/Desktop/shops.data"];
//    NSLog(@"%@", [shops subarrayWithRange:NSMakeRange(20, 10)]);
    
    FMResultSet *set = [self.db executeQuery:@"SELECT * FROM t_shop LIMIT 10,10;"];
    while (set.next) {
        NSData *data = [set objectForColumnName:@"shop"];
        HMShop *shop = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        NSLog(@"%@", shop);
    }
}

@end

4、微博离线缓存实现


1>添加HWStatusTool类,专门用来对微博数据进行数据库存储/读取

2>将新浪返回的字典数组存入数据库,注意:一个字典对应一条数据,并且需要添加idstr字段,用于判断到时候需要加载最新的还是以前的数据

//  微博工具类:用来处理微博数据的缓存

#import 

@interface HWStatusTool : NSObject
/**
 *  根据请求参数去沙盒中加载缓存的微博数据
 *
 *  @param params 请求参数
 */
+ (NSArray *)statusesWithParams:(NSDictionary *)params;

/**
 *  存储微博数据到沙盒中
 *
 *  @param statuses 需要存储的微博数据
 */
+ (void)saveStatuses:(NSArray *)statuses;
@end
#import "HWStatusTool.h"
#import "FMDB.h"

@implementation HWStatusTool

static FMDatabase *_db;
+ (void)initialize
{
    // 1.打开数据库
    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 
                        NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"statuses.sqlite"];
    _db = [FMDatabase databaseWithPath:path];
    [_db open];
    
    // 2.创表
    [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_status (id integer PRIMARY KEY, status blob NOT NULL, idstr text NOT NULL);"];
}

+ (NSArray *)statusesWithParams:(NSDictionary *)params
{
    // 根据请求参数生成对应的查询SQL语句
    NSString *sql = nil;
    if (params[@"since_id"]) {
        sql = [NSString stringWithFormat:@"SELECT * FROM t_status WHERE idstr > %@ ORDER BY idstr DESC LIMIT 20;", params[@"since_id"]];
    } else if (params[@"max_id"]) {
        sql = [NSString stringWithFormat:@"SELECT * FROM t_status WHERE idstr <= %@ ORDER BY idstr DESC LIMIT 20;", params[@"max_id"]];
    } else {
        sql = @"SELECT * FROM t_status ORDER BY idstr DESC LIMIT 20;";
    }
    
    // 执行SQL
    FMResultSet *set = [_db executeQuery:sql];
    NSMutableArray *statuses = [NSMutableArray array];
    while (set.next) {
        NSData *statusData = [set objectForColumnName:@"status"];
        NSDictionary *status = [NSKeyedUnarchiver unarchiveObjectWithData:statusData];
        [statuses addObject:status];
    }
    return statuses;
}

+ (void)saveStatuses:(NSArray *)statuses
{
    // 要将一个对象存进数据库的blob字段,最好先转为NSData
    // 一个对象要遵守NSCoding协议,实现协议中相应的方法,才能转成NSData
    for (NSDictionary *status in statuses) {
        // NSDictionary --> NSData
        NSData *statusData = [NSKeyedArchiver archivedDataWithRootObject:status];
        [_db executeUpdateWithFormat:@"INSERT INTO t_status(status, idstr) VALUES (%@, %@);", statusData, status[@"idstr"]];
    }
}
@end

在首页控制器中进行上拉/下拉刷新部分代码如下:

/**
 *  UIRefreshControl进入刷新状态:加载最新的数据
 */
- (void)loadNewStatus
{
    // 1.拼接请求参数
    HWAccount *account = [HWAccountTool account];
    NSMutableDictionary *params = [NSMutableDictionary dictionary];
    params[@"access_token"] = account.access_token;
    
    // 取出最前面的微博(最新的微博,ID最大的微博)
    HWStatusFrame *firstStatusF = [self.statusFrames firstObject];
    if (firstStatusF) {
        // 若指定此参数,则返回ID比since_id大的微博(即比since_id时间晚的微博),默认为0
        params[@"since_id"] = firstStatusF.status.idstr;
    }
    
    // 定义一个block处理返回的字典数据
    void (^dealingResult)(NSArray *) = ^(NSArray *statuses){
        // 将 "微博字典"数组 转为 "微博模型"数组
        NSArray *newStatuses = [HWStatus objectArrayWithKeyValuesArray:statuses];
        
        // 将 HWStatus数组 转为 HWStatusFrame数组
        NSArray *newFrames = [self stausFramesWithStatuses:newStatuses];
        
        // 将最新的微博数据,添加到总数组的最前面
        NSRange range = NSMakeRange(0, newFrames.count);
        NSIndexSet *set = [NSIndexSet indexSetWithIndexesInRange:range];
        [self.statusFrames insertObjects:newFrames atIndexes:set];
        
        // 刷新表格
        [self.tableView reloadData];
        
        // 结束刷新
        [self.tableView headerEndRefreshing];
        
        // 显示最新微博的数量
        [self showNewStatusCount:newStatuses.count];
    };
    
    // 2.先尝试从数据库中加载微博数据
    NSArray *statuses = [HWStatusTool statusesWithParams:params];
    if (statuses.count) { // 数据库有缓存数据
        dealingResult(statuses);
    } else {
        // 2.发送请求
        [HWHttpTool get:@"https://api.weibo.com/2/statuses/friends_timeline.json" params:params success:^(id json) {
            [HWStatusTool saveStatuses:json[@"statuses"]];
            
            dealingResult(json[@"statuses"]);
        } failure:^(NSError *error) {
            HWLog(@"请求失败-%@", error);
            
            // 结束刷新刷新
            [self.tableView headerEndRefreshing];
        }];
    }
}

/**
 *  加载更多的微博数据
 */
- (void)loadMoreStatus
{
    // 1.拼接请求参数
    HWAccount *account = [HWAccountTool account];
    NSMutableDictionary *params = [NSMutableDictionary dictionary];
    params[@"access_token"] = account.access_token;
    
    // 取出最后面的微博(最新的微博,ID最大的微博)
    HWStatusFrame *lastStatusF = [self.statusFrames lastObject];
    if (lastStatusF) {
        // 若指定此参数,则返回ID小于或等于max_id的微博,默认为0。
        // id这种数据一般都是比较大的,一般转成整数的话,最好是long long类型
        long long maxId = lastStatusF.status.idstr.longLongValue - 1;
        params[@"max_id"] = @(maxId);
    }
    
    // 处理字典数据
    void (^dealingResult)(NSArray *) = ^(NSArray *statuses) {
        // 将 "微博字典"数组 转为 "微博模型"数组
        NSArray *newStatuses = [HWStatus objectArrayWithKeyValuesArray:statuses];
        
        NSArray *newFrames = [self stausFramesWithStatuses:newStatuses];
        
        // 将更多的微博数据,添加到总数组的最后面
        [self.statusFrames addObjectsFromArray:newFrames];
        
        // 刷新表格
        [self.tableView reloadData];
        
        // 结束刷新(隐藏footer)
        [self.tableView footerEndRefreshing];
    };
    
    // 2.加载沙盒中的数据
    NSArray *statuses = [HWStatusTool statusesWithParams:params];
    if (statuses.count) {// 将 HWStatus数组 转为 HWStatusFrame数组
        dealingResult(statuses);
    } else {
        // 3.发送请求
        [HWHttpTool get:@"https://api.weibo.com/2/statuses/friends_timeline.json" params:params success:^(id json) {
            // 缓存新浪返回的字典数组
            [HWStatusTool saveStatuses:json[@"statuses"]];
            
            dealingResult(json[@"statuses"]);
        } failure:^(NSError *error) {
            HWLog(@"请求失败-%@", error);
            
            // 结束刷新
            [self.tableView footerEndRefreshing];
        }];
    }
}

5、清除图片缓存


一般情况下,清楚缓存往往是清除图片缓存(比较大),如果利用SDWebImage,一般使用其自带的方法就可以清除图片缓存,但是若一些应用是下载文件的应用,需要清除缓存则需自己处理。

注:这里为显示方便,直接将缓存大小显示在导航栏标题,用于测试图片缓存清除

// 字节大小
int byteSize = [SDImageCache sharedImageCache].getSize;
// M大小
double size = byteSize / 1000.0 / 1000.0;
self.navigationItem.title = [NSString stringWithFormat:@"缓存大小(%.1fM)", size];
- (void)clearCache
{
    // 提醒
    UIActivityIndicatorView *circle = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
    [circle startAnimating];
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:circle];
    
    // 清除缓存
    [[SDImageCache sharedImageCache] clearDisk];
    
    // 显示按钮
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"清除缓存" style:0 target:self action:@selector(clearCache)];
    self.navigationItem.title = [NSString stringWithFormat:@"缓存大小(0M)"];
}

6、文件操作 --- 补充


NSString分类方法:计算当前文件\文件夹的内容大小

/**
 *  计算当前文件\文件夹的内容大小
 */
- (NSInteger)fileSize
{
    NSFileManager *mgr = [NSFileManager defaultManager];
    // 判断是否为文件
    BOOL dir = NO;
    BOOL exists = [mgr fileExistsAtPath:self isDirectory:&dir];
    // 文件\文件夹不存在
    if (exists == NO) return 0;
    
    if (dir) { // self是一个文件夹
        // 遍历caches里面的所有内容 --- 直接和间接内容
        NSArray *subpaths = [mgr subpathsAtPath:self];
        NSInteger totalByteSize = 0;
        for (NSString *subpath in subpaths) {
            // 获得全路径
            NSString *fullSubpath = [self stringByAppendingPathComponent:subpath];
            // 判断是否为文件
            BOOL dir = NO;
            [mgr fileExistsAtPath:fullSubpath isDirectory:&dir];
            if (dir == NO) { // 文件
                totalByteSize += [[mgr attributesOfItemAtPath:fullSubpath error:nil][NSFileSize] integerValue];
            }
        }
        return totalByteSize;
    } else { // self是一个文件
        return [[mgr attributesOfItemAtPath:self error:nil][NSFileSize] integerValue];
    }
}

计算Caches文件夹大小

- (void)fileOperation
{
    // 文件管理者
    NSFileManager *mgr = [NSFileManager defaultManager];
    // 缓存路径
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    HWLog(@"%d", [caches fileSize]);

    [mgr removeItemAtPath:caches error:nil];
}

7、性能分析(内存分析)


7.1 静态分析


1>检测代码是否有潜在的内存泄露

2>编译器觉得不太合适的代码

3>工具:Analyze


7.2 动态分析


1>检测程序在运行过程中的内存变化

2>工具:Profile

1.Allocations:能看清楚app的内存变化


iOS开发 - 第05篇 - 项目 - 13 - 离线缓存_第3张图片


2.Leaks:能看清楚app在何时产生了内存泄露

注:内存泄露和内存溢出区别

内存泄露:该释放的对象没有被释放;

内存溢出:内存不够用;


iOS开发 - 第05篇 - 项目 - 13 - 离线缓存_第4张图片


你可能感兴趣的:(iOS开发,iOS开发笔记)