Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

iOS多线程总结(3)——NSOperation与NSOperationQueue

$
0
0

本篇是多线程总结的第三篇,关于多线程的概念和NSThread的使用写在第一篇,《iOS多线程总结(1)——多线程相关概念及NSObject/NSThread的使用》,第二篇《iOS多线程总结(2)——GCD》主要讲解GCD的使用,本编是线程实现总结的最后一篇,主要讲解NSOperation的使用。

一. NSOperation

在MacOSXv10.6和iOS4之前,NSOperation与NSOperationQueue不同于GCD,他们使用了完全不同的机制。从MacOSXv10.6和iOS4开始,NSOperation和NSOperationQueue是建立在GCD上的。NSOperation和NSOperationQueue对比GCD会带来一点额外的系统开销,但是你可以在多个操作Operation中添加附属。你可以重用操作,取消或者暂停他们。NSOperation和 Key-Value Observation (KVO)是兼容的;例如,你可以通过监听NSNotificationCenter去让一个操作开始执行。

NSOperation调用start方法即可开始执行操作,NSOperation对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行。NSOperation对象的isConcurrent方法会告诉我们这个操作相对于调用start方法的线程,是同步还是异步执行。isConcurrent方法默认返回NO,表示操作与调用线程同步执行。

(一). NSOperation的API

@interface NSOperation : NSObject {
@private
    id _private;
    int32_t _private1;
#if __LP64__
    int32_t _private1b;
#endif
}

// 开始操作
- (void)start;

// 操作任务的入口,一般用于自定义NSOperation的子类 
- (void)main;

// 判断是否已经被取消
@property (readonly, getter=isCancelled) BOOL cancelled;

// 取消操作任务,调用后不会自动马上取消,需要通过isCancelled方法检查是否被取消,然后自己编写代码退出任务
- (void)cancel;

// 是否正在执行
@property (readonly, getter=isExecuting) BOOL executing;

// 是否执行完
@property (readonly, getter=isFinished) BOOL finished;

// 判定该线程是否是并发线程,即调用该operation的start方法的线程是否与operation所在线程相同
// 此属性即将被弃用,之后使用asynchronous属性代替
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below

// 是否异步执行
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);

// 是否准备好
// 在start方法开始之前,需要确定operation是否ready,默认为YES,如果该operation没有ready,则不会start。
@property (readonly, getter=isReady) BOOL ready;

// 添加依赖关系
- (void)addDependency:(NSOperation *)op;

// 取消依赖,注意:操作对象的依赖不能在操作队列执行时取消
- (void)removeDependency:(NSOperation *)op;

// 获取有依赖关系的operation所组成的数组
@property (readonly, copy) NSArray<NSOperation *> *dependencies;

// 优先级的枚举
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

// 优先级
@property NSOperationQueuePriority queuePriority;

// 操作完成后调用的代码块
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);

// 是否等待operation执行结束,如果为YES,会堵塞当前线程,直到operation执行结束,才会执行接下来的代码
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);

// 设定operation的线程优先级,线程优先级默认为0.5,取值范围0~1
// 即使设定了线程优先级,也只能保证其在main方法范围内有效,operation的其他代码仍然执行在默认线程
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);

// 用于系统自动合理的管理队列的资源分配
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

// 操作任务的名字
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);

@end

(二). Quality of Service(QoS)

这是在iOS8之后提供的新功能,苹果提供了几个Quality of Service枚举来使用:user interactive, user initiated, utility 和 background,通过这些枚举告诉系统我们在进行什么样的工作,然后系统会通过合理的资源控制来最高效的执行任务代码,其中主要涉及到CPU调度的优先级、IO优先级、任务运行在哪个线程以及运行的顺序等等,我们可以通过一个抽象的Quality of Service枚举参数来表明任务的意图以及类别。

  • NSQualityOfServiceUserInteractive
    与用户交互的任务,这些任务通常跟UI级别的刷新相关,比如动画,这些任务需要在一瞬间完成.

  • NSQualityOfServiceUserInitiated
    由用户发起的并且需要立即得到结果的任务,比如滑动scroll view时去加载数据用于后续cell的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成

  • NSQualityOfServiceUtility
    一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间

  • NSQualityOfServiceBackground
    这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时

  • NSQualityOfServiceDefault
    优先级介于user-initiated 和 utility,当没有 QoS信息时默认使用,开发者不应该使用这个值来设置自己的任务

二. NSBlockOperation

(一). 知识点

  1. 如果是通过start方法启动操作,第一个操作不管是用blockOperationWithBlock:添加或者用addExecutionBlock:方法添加,第一个操作默认在主线程上执行,其余的操作会开辟新线程执行操作。

  2. 如果通过completionBlock属性添加操作,系统会把completionBlock属性上的任务直接添加到Operation开辟的最后一条线程上;也就是说,创建一个NSOperation对象,然后往里面添加多个任务,当启动时,开辟多线程,completionBlock属性上的任务就会添加到开启的多个线程中的最后开启的那条线程上。

  3. 一旦Operation添加到操作队列queue中,queue就拥有了这个Operation对象并且不能被删除,唯一能做的事情是取消;通过cancel方法取消任务,Operation不会马上被取消,只能通过[op isCancelled]自己检测是否被取消,然后编写退出操作的代码。

  4. 如果需要在当前线程中处理operation完成后的结果,可以使用NSOperation的waitUntilFinished方法阻塞当前线程,等待operation完成。通常我们应该避免编写这样的代码,阻塞当前线程可能是一种简便的解决方案,但是它引入了更多的串行代码,限制了整个应用的并发性,同时也降低了用户体验。绝对不要在应用主线程中等待一个Operation,只能在第二或次要线程中等待。阻塞主线程将导致应用无法响应用户事件,应用也将表现为无响应。

(二). NSBlockOperation的API

@interface NSBlockOperation : NSOperation {
@private
    id _private2;
    void *_reserved2;
}

// 添加操作任务
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

// 添加操作任务
- (void)addExecutionBlock:(void (^)(void))block;

// 获取所有的操作任务所组成的数组
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;

@end

(三). NSBlockOperation的使用

1. 不使用NSOperationQueue

    // 使用blockOperationWithBlock快速创建操作对象
//    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
//        
//        NSLog(@"---%@", [NSThread currentThread]);
//    }];

    // 使用传统的方法创建操作对象
    NSBlockOperation *op = [[NSBlockOperation alloc] init];

    // 创建一个NSOperation对象,然后往里面添加多个任务,当启动时,开辟多线程,
    // completionBlock属性上的任务就会添加到开启的多个线程中的最后开启的那条线程上
    op.completionBlock = ^{

        NSLog(@"---%@", [NSThread currentThread]);
    };

    // 添加操作
    // 如果是通过start方法启动操作,第一个操作不管是用blockOperationWithBlock:添加或者用addExecutionBlock:方法添加,
    // 第一个操作默认在主线程上执行,其余的操作会开辟新线程执行操作
    [op addExecutionBlock:^{

        NSLog(@"%@", [NSThread currentThread]);
    }];
    [op addExecutionBlock:^{

        NSLog(@"%@", [NSThread currentThread]);
    }];
    [op addExecutionBlock:^{

        NSLog(@"%@", [NSThread currentThread]);
    }];

    // 开始操作任务
    [op start];

2. completionBloc属性与NSOperationQueue

    NSBlockOperation *op1 = [[NSBlockOperation alloc] init];
    NSBlockOperation *op2 = [[NSBlockOperation alloc] init];
    NSBlockOperation *op3 = [[NSBlockOperation alloc] init];

    op1.completionBlock = ^{

        NSLog(@"---%@", [NSThread currentThread]);
    };
    op2.completionBlock = ^{

        NSLog(@"---%@", [NSThread currentThread]);
    };
    op3.completionBlock = ^{

        NSLog(@"---%@", [NSThread currentThread]);
    };

    // 会开辟多条线程并运行并执行completionBlock属性中得操作
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];

3. 使用NSOperationQueue

    NSBlockOperation *op1 =[NSBlockOperation blockOperationWithBlock:^{

        for (int i = 0; i < 10; i++) {
            NSLog(@"op----%d----%@", i, [NSThread currentThread]);
        }
    }];
    NSBlockOperation *op2 =[NSBlockOperation blockOperationWithBlock:^{

        for (int i = 0; i < 10; i++) {
            NSLog(@"op2----%d----%@", i, [NSThread currentThread]);
        }
    }];
    NSBlockOperation *op3 =[NSBlockOperation blockOperationWithBlock:^{

        for (int i = 0; i < 10; i++) {
            NSLog(@"op3----%d----%@", i, [NSThread currentThread]);
        }
    }];

    // 添加依赖
    [op1 addDependency:op3];
    [op1 addDependency:op2];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];

    NSLog(@"%@",[op1 dependencies]);

4. 取消Operation

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSBlockOperation *op;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

     _op = [NSBlockOperation blockOperationWithBlock:^{

        for (int i = 0; i < 5000; i++) {

            // 判断是否被取消
            if ([_op isCancelled]) {
                NSLog(@"----op isCancelled----");
                break;

            }

            NSLog(@"%d----%@", i, [NSThread currentThread]);
        }

    }];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:_op];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    [_op cancel];

}

三. NSInvocationOperation

(一). NSInvocationOperation的API

@interface NSInvocationOperation : NSOperation {
@private
    id _inv;
    id _exception;
    void *_reserved2;
}

// 初始化Operation
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;

// 使用NSInvocation对象初始化Operation
- (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;

// 获取NSInvocation对象
@property (readonly, retain) NSInvocation *invocation;

// 获取操作的返回值
// 注意:方法的返回值必须是对象才能获取正确,不能是数值类型,且必须操作Operation完成才可以获取
@property (nullable, readonly, retain) id result;

@end

(二). NSInvocationOperation的使用

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self InvocationOperation1];

    NSLog(@"-------------------打印结果分割线----------------------");

    [self InvocationOperation2];

}

#pragma mark - 基本使用

- (void)InvocationOperation1 {

    // 创建方式一
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [op1 start];


    // 创建方式二
    // 根据方法来初始化方法签名对象,NSMethodSignature:方法签名类
    NSMethodSignature  *sig = [ViewController instanceMethodSignatureForSelector:@selector(run:two:)];
    // 根据方法签名对象来创建一个调用对象,NSInvocation:调用类
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    // 设置调用者
    invocation.target = self;
    // 设置调用方法
    invocation.selector = @selector(run:two:);
    // 设置参数
    int value1 = 100;
    int value2 = 200;
    [invocation setArgument:&value1 atIndex:2];
    [invocation setArgument:&value2 atIndex:3];

    // 根据方法调用对象初始化Operation对象
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithInvocation:invocation];
    [op2 start];

    // 获取OP2的返回值
    // 注意:方法的返回值必须是对象才能获取正确,不能是数值类型,且必须操作Operation完成才可以获取
    NSLog(@"OP2的返回值:%@", op2.result );

}

- (void)run {

    NSLog(@"run ---- 线程%@", [NSThread currentThread]);
}

- (NSNumber *)run:(int)value1 two:(int)value2 {

    NSLog(@"run:two: --- %d --- %d --- %@", value1, value2, [NSThread currentThread]);

    return @(value1 + value2);
}

#pragma mark - 配合NSOperationQueue使用

- (void)InvocationOperation2 {

    // 1. 创建操作队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];


    // 2. 创建操作方法
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

    NSMethodSignature  *sig1 = [[self class] instanceMethodSignatureForSelector:@selector(run:two:)];
    NSInvocation *invocation1 = [NSInvocation invocationWithMethodSignature:sig1];
    invocation1.target = self;
    invocation1.selector = @selector(run:two:);
    int value1 = 100;
    int value2 = 200;
    [invocation1 setArgument:&value1 atIndex:2];
    [invocation1 setArgument:&value2 atIndex:3];
    NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithInvocation:invocation1];

    NSMethodSignature  *sig2 = [[self class] instanceMethodSignatureForSelector:@selector(run:two:)];
    NSInvocation *invocation2 = [NSInvocation invocationWithMethodSignature:sig2];
    invocation2.target = self;
    invocation2.selector = @selector(run:two:);
    int value3 = 400;
    int value4 = 550;
    [invocation2 setArgument:&value3 atIndex:2];
    [invocation2 setArgument:&value4 atIndex:3];
    NSInvocationOperation *op4 = [[NSInvocationOperation alloc] initWithInvocation:invocation2];


    // 3. 添加操作任务
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    [queue addOperation:op4];

}

@end

输出结果:

这里写图片描述

四. 自定义NSOperation子类

如果NSInvocationOperation和NSBlockOperation对象不能满足需求, 可以直接继承NSOperation, 并添加任何你想要的行为。继承所需的工作量主要取决于你要实现非并发还是并发的NSOperation。定义非并发的NSOperation要简单许多,只需要重载-(void)main这个方法,在这个方法里面执行主任务,并正确地响应取消事件; 当要通过cancel取消操作时,它不会马上就取消,它会在未来的某个时候在“main”函数中明确地检查isCancelled==YES时被取消掉;否则,操作会一直执行到完成为止。

如果需要创建一个并发的Operation,并手动执行,则需要重写main,start,isCancelled,isReady,isConcurrent,isFinished,isExcluting,dependency,queuepripority方法,并维护每个property的KVO,同时确保每个property的原子性。

非并发的实现步骤:

  • 继承NSOperation类
  • 重写main()方法
  • 在main()方法中创建一个@autoreleasepool
  • 将要做的操作放在@autoreleasepool中
    提示:创建自己的自动释放池的原因是,因为我们不能访问主线程的自动释放池,所以应该自己创建一个。

非并发的实现代码:

#import "SJMSerialOperation.h"

@implementation SJMSerialOperation

- (void)main {

    @autoreleasepool {

        for (int i = 0; i < 100; i++) {
            NSLog(@"%i --- %@", i, [NSThread currentThread]);
        }

        // 被取消时退出
        if (self.isCancelled == YES) return;

        for (int i = 0; i < 100; i++) {
            NSLog(@"%i --- %@", i, [NSThread currentThread]);
        }

        // 被取消时退出
        if (self.isCancelled == YES) return;

    }
}

@end

#import "ViewController.h"
#import "SJMSerialOperation.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];


    SJMSerialOperation *op = [[SJMSerialOperation alloc] init];
    [op start];


    SJMSerialOperation *op1 = [[SJMSerialOperation alloc] init];
    SJMSerialOperation *op2 = [[SJMSerialOperation alloc] init];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    [queue addOperation:op1];
    [queue addOperation:op2];

}

@end

五. NSOperationQueue

(一). 知识点

  1. NSOperationQueue称为操作队列,它的作用是管理子线程的生命周期和线程同步问题,有了操作队列,我们只需要关注具体的操作任务,而不用去管线程的生命周期和线程同步问题等问题,大大提高了开发效率。

  2. 只要在操作队列中加入操作任务,程序运行时它就会去检查maxConcurrentOperationCount(最大并发数属性),如果值是1,它就相当于串行队列,只开辟一条线程,如果大于1,就根据值开辟相应条数的子线程,当任务操作完后回自动销毁线程。

  3. 当通过设置suspended属性来暂停操作队列时,队列不会马上暂停,系统会执行完当前正在执行的任务才暂停队列。

  4. 一旦Operation对象添加到操作队列就不能被删除,只能通过Operation对象方法cancel取消;取消全部操作队列中的任务的方法是cancelAllOperations;但要成功取消,必须自己编写监听代码和取消代码,使用NSOperation对象的isCancelled方法检查是否被取消,如果被取消了,可以通过return等方法退出任务。

(二). NSOperationQueue的API

// 默认最大操作数为-1
static const NSInteger NSOperationQueueDefaultMaxConcurrentOperationCount = -1;


@interface NSOperationQueue : NSObject {
@private
    id _private;
    void *_reserved;
}

// 添加操作对象(NSOperation对象)
- (void)addOperation:(NSOperation *)op;

// 添加操作对象组(NSOperation对象),waitUntilFinished:是否阻塞当前线程,等待所有操作都完成
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);

// 添加操作任务
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);

// 获取操作任务对象组
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;

// 获取操作任务对象总数
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);

// 最大并发数
@property NSInteger maxConcurrentOperationCount;

// 是否暂停队列
@property (getter=isSuspended) BOOL suspended;

// 队列名字
@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);

// 用于系统自动合理的管理队列的资源分配
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

// underlyingQueue属性的值是主线程的调度队列,此属性不能设置为其它值
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);

// 取消所有操作任务,配合NSOperation对象的isCancelled方法使用
- (void)cancelAllOperations;

// 阻塞当前线程,等待所有操作执行完毕 
- (void)waitUntilAllOperationsAreFinished;

// 获取当前操作队列
+ (nullable NSOperationQueue *)currentQueue NS_AVAILABLE(10_6, 4_0);

// 获取主操作队列
+ (NSOperationQueue *)mainQueue NS_AVAILABLE(10_6, 4_0);

@end

(三). 使用方法

1. NSOperationQueue单独使用

NSOperationQueue可以单独使用,不用自己创建NSOperation对象,可以直接通过addOperationWithBlock:方法往操作队列添加一个任务,它的底层会自动创建一个NSBlockOperation对象,再通过addOperation:方法把NSBlockOperation对象加入到操作队列中。

代码举例:

  • 继承NSOperationQueue,创建一个子类SJMOperationQueue,重写addOperationWithBlock:和addOperation:方法
#import "SJMOperationQueue.h"

@implementation SJMOperationQueue

-(void)addOperationWithBlock:(void (^)(void))block {

    NSLog(@"%s",__func__);

    [super addOperationWithBlock:block];
}

- (void)addOperation:(NSOperation *)op {

    NSLog(@"%s",__func__);

    [super addOperation:op];
}

@end
  • 使用addOperationWithBlock:方法直接添加任务
#import "ViewController.h"
#include "SJMOperationQueue.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    [self operationQueue];
}

- (void)operationQueue {

    SJMOperationQueue *queue = [[SJMOperationQueue alloc] init];

    [queue addOperationWithBlock:^{

        NSLog(@"-----%@",[NSThread currentThread]);

    }];
    [queue addOperationWithBlock:^{

        NSLog(@"+++++%@",[NSThread currentThread]);

    }];

    NSLog(@"%@",queue.operations);
}

@end

打印结果:

这里写图片描述

2. NSOperationQueue配合NSOperation的子类使用

  • 自定义NSOperation的子类
#import "SJMNSOperation.h"

@implementation SJMNSOperation

- (void)main {

    for (int i = 0; i < 1000; i++) {
        NSLog(@"op3---1---%@", [NSThread currentThread]);
    }  
}

@end
  • 自定义NSOperation的子类的使用
#import "ViewController.h"
#include "SJMNSOperation.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {

    // 1. 创建操作队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2. 添加操作任务

    // 2.1 通过NSInvocationOperation创建操作任务
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

    // 2.2 通过NSBlockOperation创建操作任务
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

        NSLog(@"%@",[NSThread currentThread]);
    }];

    // 2.3 通过自定义的NSOperation子类创建操作任务
    SJMNSOperation *op3 = [[SJMNSOperation alloc] init];

    // 3. 添加到操作队列
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
}

- (void)run {

    NSLog(@"%@",[NSThread currentThread]);
}

3. 取消队列中的所有操作

#import "ViewController.h"
#include "SJMOperationQueue.h"
#include "SJMNSOperation.h"

@interface ViewController ()

@property (nonatomic, strong) NSOperationQueue *queue;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建操作队列
    _queue = [[NSOperationQueue alloc] init];

    // 添加操作任务
    [self operationQueue2];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // 取消所有操作任务
    [_queue cancelAllOperations];  
}

#pragma mark - 取消操作队列里所有的任务

- (void)operationQueue2 {

    // 通过自定义的NSOperation子类创建操作任务
    SJMNSOperation *op1 = [[SJMNSOperation alloc] init];
    SJMNSOperation *op2 = [[SJMNSOperation alloc] init];
    SJMNSOperation *op3 = [[SJMNSOperation alloc] init];
    SJMNSOperation *op4 = [[SJMNSOperation alloc] init];
    SJMNSOperation *op5 = [[SJMNSOperation alloc] init];

    // 添加到操作队列
    [_queue addOperation:op1];
    [_queue addOperation:op2];
    [_queue addOperation:op3];
    [_queue addOperation:op4];
    [_queue addOperation:op5];

}
作者:SSIrreplaceable 发表于2016/11/26 23:22:43 原文链接
阅读:20 评论:0 查看评论

Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构

$
0
0

点击查看 Java 集合框架深入理解 系列, - ( ゜- ゜)つロ 乾杯~

上篇文章我们介绍了 HashMap 的主要特点和关键方法源码解读,这篇文章我们介绍 HashMap 在 JDK1.8 新增树形化相关的内容。

读完本文你将了解到:

传统 HashMap 的缺点

JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。

当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。

针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。

HashMap 在 JDK 1.8 中新增的数据结构 – 红黑树

shixinzhang

JDK 1.8 中 HashMap 中除了链表节点:

static class Node<K,V> implements Map.Entry<K,V> {
    //哈希值,就是位置
    final int hash;
    //键
    final K key;
    //值
    V value;
    //指向下一个几点的指针
    Node<K,V> next;
    //...
}

还有另外一种节点:TreeNode,它是 1.8 新增的,属于数据结构中的 红黑树(不了解红黑树的同学可以 点击这里了解红黑树):

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
}

可以看到就是个红黑树节点,有父亲、左右孩子、前一个元素的节点,还有个颜色值。

另外由于它继承自 LinkedHashMap.Entry ,而 LinkedHashMap.Entry 继承自 HashMap.Node ,因此还有额外的 6 个属性:

//继承 LinkedHashMap.Entry 的
Entry<K,V> before, after;

//HashMap.Node 的
final int hash;
final K key;
V value;
Node<K,V> next;

HashMap 中关于红黑树的三个关键参数

HashMap 中有三个关于红黑树的关键参数:

  • TREEIFY_THRESHOLD
  • UNTREEIFY_THRESHOLD
  • MIN_TREEIFY_CAPACITY

值及作用如下:

//一个桶的树化阈值
//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
//这个值必须为 8,要不然频繁转换效率也不高
static final int TREEIFY_THRESHOLD = 8;

//一个树的链表还原阈值
//当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构
//这个值应该比上面那个小,至少为 6,避免频繁转换
static final int UNTREEIFY_THRESHOLD = 6;

//哈希表的最小树形化容量
//当哈希表中的容量大于这个值时,表中的桶才能进行树形化
//否则桶内元素太多时会扩容,而不是树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap 在 JDK 1.8 中新增的操作:桶的树形化 treeifyBin()

在Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。

这个替换的方法叫 treeifyBin() 即树形化。

//将桶内所有的 链表节点 替换成 红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //如果哈希表中的元素个数超过了 树形化阈值,进行树形化
        // e 是哈希表中指定位置桶里的链表节点,从第一个开始
        TreeNode<K,V> hd = null, tl = null; //红黑树的头、尾节点
        do {
            //新建一个树形节点,内容和当前链表节点 e 一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null) //确定树头节点
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);  
        //让桶的第一个元素指向新建的红黑树头结点,以后这个桶里的元素就是红黑树而不是链表了
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}


    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

上述操作做了这些事:

  • 根据哈希表中元素个数确定是扩容还是树形化
  • 如果是树形化
    • 遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
    • 然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容

但是我们发现,之前的操作并没有设置红黑树的颜色值,现在得到的只能算是个二叉树。在 最后调用树形节点 hd.treeify(tab) 方法进行塑造红黑树,来看看代码:

       final void treeify(Node<K,V>[] tab) {
        TreeNode<K,V> root = null;
        for (TreeNode<K,V> x = this, next; x != null; x = next) {
            next = (TreeNode<K,V>)x.next;
            x.left = x.right = null;
            if (root == null) { //头回进入循环,确定头结点,为黑色
                x.parent = null;
                x.red = false;
                root = x;
            }
            else {  //后面进入循环走的逻辑,x 指向树中的某个节点
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                //又一个循环,从根节点开始,遍历所有节点跟当前节点 x 比较,调整位置,有点像冒泡排序
                for (TreeNode<K,V> p = root;;) {
                    int dir, ph;        //这个 dir 
                    K pk = p.key;
                    if ((ph = p.hash) > h)  //当比较节点的哈希值比 x 大时, dir 为 -1
                        dir = -1;
                    else if (ph < h)  //哈希值比 x 小时 dir 为 1
                        dir = 1;
                    else if ((kc == null &&
                              (kc = comparableClassFor(k)) == null) ||
                             (dir = compareComparables(kc, k, pk)) == 0)
                        // 如果比较节点的哈希值、 x 
                        dir = tieBreakOrder(k, pk);

                        //把 当前节点变成 x 的父亲
                        //如果当前比较节点的哈希值比 x 大,x 就是左孩子,否则 x 是右孩子 
                    TreeNode<K,V> xp = p;
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        root = balanceInsertion(root, x);
                        break;
                    }
                }
            }
        }
        moveRootToFront(tab, root);
    }

可以看到,将二叉树变为红黑树时,需要保证有序。这里有个双重循环,拿树中的所有节点和当前节点的哈希值进行对比(如果哈希值相等,就对比键,这里不用完全有序),然后根据比较结果确定在树种的位置。

HashMap 在 JDK 1.8 中新增的操作: 红黑树中添加元素 putTreeVal()

上面介绍了如何把一个桶中的链表结构变成红黑树结构。

在添加时,如果一个桶中已经是红黑树结构,就要调用红黑树的添加元素方法 putTreeVal()。

    final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                   int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        TreeNode<K,V> root = (parent != null) ? root() : this;
        //每次添加元素时,从根节点遍历,对比哈希值
        for (TreeNode<K,V> p = root;;) {
            int dir, ph; K pk;
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))  
            //如果当前节点的哈希值、键和要添加的都一致,就返回当前节点(奇怪,不对比值吗?)
                return p;
            else if ((kc == null &&
                      (kc = comparableClassFor(k)) == null) ||
                     (dir = compareComparables(kc, k, pk)) == 0) {
                //如果当前节点和要添加的节点哈希值相等,但是两个节点的键不是一个类,只好去挨个对比左右孩子 
                if (!searched) {
                    TreeNode<K,V> q, ch;
                    searched = true;
                    if (((ch = p.left) != null &&
                         (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                         (q = ch.find(h, k, kc)) != null))
                        //如果从 ch 所在子树中可以找到要添加的节点,就直接返回
                        return q;
                }
                //哈希值相等,但键无法比较,只好通过特殊的方法给个结果
                dir = tieBreakOrder(k, pk);
            }

            //经过前面的计算,得到了当前节点和要插入节点的一个大小关系
            //要插入的节点比当前节点小就插到左子树,大就插到右子树
            TreeNode<K,V> xp = p;
         //这里有个判断,如果当前节点还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环 
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                Node<K,V> xpn = xp.next;
                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                if (dir <= 0)
                    xp.left = x;
                else
                    xp.right = x;
                xp.next = x;
                x.parent = x.prev = xp;
                if (xpn != null)
                    ((TreeNode<K,V>)xpn).prev = x;
                //红黑树中,插入元素后必要的平衡调整操作
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    }

    //这个方法用于 a 和 b 哈希值相同但是无法比较时,直接根据两个引用的地址进行比较
    //这里源码注释也说了,这个树里不要求完全有序,只要插入时使用相同的规则保持平衡即可
     static int tieBreakOrder(Object a, Object b) {
        int d;
        if (a == null || b == null ||
            (d = a.getClass().getName().
             compareTo(b.getClass().getName())) == 0)
            d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                 -1 : 1);
        return d;
    }

通过上面的代码可以知道,HashMap 中往红黑树中添加一个新节点 n 时,有以下操作:

  • 从根节点开始遍历当前红黑树中的元素 p,对比 n 和 p 的哈希值;
  • 如果哈希值相等并且键也相等,就判断为已经有这个元素(这里不清楚为什么不对比值);
  • 如果哈希值就通过其他信息,比如引用地址来给个大概比较结果,这里可以看到红黑树的比较并不是很准确,注释里也说了,只是保证个相对平衡即可;
  • 最后得到哈希值比较结果后,如果当前节点 p 还没有左孩子或者右孩子时才能插入,否则就进入下一轮循环;
  • 插入元素后还需要进行红黑树例行的平衡调整,还有确保根节点的领先地位。

HashMap 在 JDK 1.8 中新增的操作: 红黑树中查找元素 getTreeNode()

HashMap 的查找方法是 get():

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

它通过计算指定 key 的哈希值后,调用内部方法 getNode();

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

这个 getNode() 方法就是根据哈希表元素个数与哈希值求模(使用的公式是 (n - 1) &hash)得到 key 所在的桶的头结点,如果头节点恰好是红黑树节点,就调用红黑树节点的 getTreeNode() 方法,否则就遍历链表节点。

    final TreeNode<K,V> getTreeNode(int h, Object k) {
        return ((parent != null) ? root() : this).find(h, k, null);
    }

getTreeNode 方法使通过调用树形节点的 find() 方法进行查找:

    //从根节点根据 哈希值和 key 进行查找
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
        TreeNode<K,V> p = this;
        do {
            int ph, dir; K pk;
            TreeNode<K,V> pl = p.left, pr = p.right, q;
            if ((ph = p.hash) > h)
                p = pl;
            else if (ph < h)
                p = pr;
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            else if (pl == null)
                p = pr;
            else if (pr == null)
                p = pl;
            else if ((kc != null ||
                      (kc = comparableClassFor(k)) != null) &&
                     (dir = compareComparables(kc, k, pk)) != 0)
                p = (dir < 0) ? pl : pr;
            else if ((q = pr.find(h, k, kc)) != null)
                return q;
            else
                p = pl;
        } while (p != null);
        return null;
    }

由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率很高。

这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断 key 是否相等,相等就直接返回(也没有判断值哎);不相等就从子树中递归查找。

HashMap 在 JDK 1.8 中新增的操作: 树形结构修剪 split()

HashMap 中, resize() 方法的作用就是初始化或者扩容哈希表。当扩容时,如果当前桶中元素结构是红黑树,并且元素个数小于链表还原阈值 UNTREEIFY_THRESHOLD (默认为 6),就会把桶中的树形结构缩小或者直接还原(切分)为链表结构,调用的就是 split():

    //参数介绍
    //tab 表示保存桶头结点的哈希表
    //index 表示从哪个位置开始修剪
    //bit 要修剪的位数(哈希值)
    final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
        TreeNode<K,V> b = this;
        // Relink into lo and hi lists, preserving order
        TreeNode<K,V> loHead = null, loTail = null;
        TreeNode<K,V> hiHead = null, hiTail = null;
        int lc = 0, hc = 0;
        for (TreeNode<K,V> e = b, next; e != null; e = next) {
            next = (TreeNode<K,V>)e.next;
            e.next = null;
            //如果当前节点哈希值的最后一位等于要修剪的 bit 值
            if ((e.hash & bit) == 0) {
                    //就把当前节点放到 lXXX 树中
                if ((e.prev = loTail) == null)
                    loHead = e;
                else
                    loTail.next = e;
                //然后 loTail 记录 e
                loTail = e;
                //记录 lXXX 树的节点数量
                ++lc;
            }
            else {  //如果当前节点哈希值最后一位不是要修剪的
                    //就把当前节点放到 hXXX 树中
                if ((e.prev = hiTail) == null)
                    hiHead = e;
                else
                    hiTail.next = e;
                hiTail = e;
                //记录 hXXX 树的节点数量
                ++hc;
            }
        }


        if (loHead != null) {
            //如果 lXXX 树的数量小于 6,就把 lXXX 树的枝枝叶叶都置为空,变成一个单节点
            //然后让这个桶中,要还原索引位置开始往后的结点都变成还原成链表的 lXXX 节点
            //这一段元素以后就是一个链表结构
            if (lc <= UNTREEIFY_THRESHOLD)
                tab[index] = loHead.untreeify(map);
            else {
            //否则让索引位置的结点指向 lXXX 树,这个树被修剪过,元素少了
                tab[index] = loHead;
                if (hiHead != null) // (else is already treeified)
                    loHead.treeify(tab);
            }
        }
        if (hiHead != null) {
            //同理,让 指定位置 index + bit 之后的元素
            //指向 hXXX 还原成链表或者修剪过的树
            if (hc <= UNTREEIFY_THRESHOLD)
                tab[index + bit] = hiHead.untreeify(map);
            else {
                tab[index + bit] = hiHead;
                if (loHead != null)
                    hiHead.treeify(tab);
            }
        }
    }

从上述代码可以看到,HashMap 扩容时对红黑树节点的修剪主要分两部分,先分类、再根据元素个数决定是还原成链表还是精简一下元素仍保留红黑树结构。

1.分类

指定位置、指定范围,让指定位置中的元素 (hash & bit) == 0 的,放到 lXXX 树中,不相等的放到 hXXX 树中。

2.根据元素个数决定处理情况

符合要求的元素(即 lXXX 树),在元素个数小于 6 时还原成链表,最后让哈希表中修剪的痛 tab[index] 指向 lXXX 树;在元素个数大于 6 时,还是用红黑树,只不过是修剪了下枝叶;

不符合要求的元素(即 hXXX 树)也是一样的操作,只不过最后它是放在了修剪范围外 tab[index + bit]。

总结

JDK 1.8 以后哈希表的 添加、删除、查找、扩容方法都增加了一种 节点为 TreeNode 的情况:

  • 添加时,当桶中链表个数超过 8 时会转换成红黑树;
  • 删除、扩容时,如果桶中结构为红黑树,并且树中元素个数太少的话,会进行修剪或者直接还原成链表结构;
  • 查找时即使哈希函数不优,大量元素集中在一个桶中,由于有红黑树结构,性能也不会差。

(图片来自:http://tech.meituan.com/java-hashmap.html)

shixinzhang

这篇文章根据源码分析了 HashMap 在 JDK 1.8 里新增的 TreeNode 的一些关键方法,可以看到,1.8 以后的 HashMap 结合了哈希表和红黑树的优点,不仅快速,而且在极端情况也能保证性能,设计者苦心孤诣可见一斑,写到这里不禁仰天长叹:什么时候我才能写出这么 NB 的代码啊!!!

Thanks

http://openjdk.java.net/jeps/180

http://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

http://www.importnew.com/14417.html

http://tech.meituan.com/java-hashmap.html

作者:u011240877 发表于2016/11/26 23:24:32 原文链接
阅读:91 评论:0 查看评论

微信小程序开发之录音机 音频播放 动画 (真机可用)

$
0
0

趁着周末用微信小程序做了个简易录音机.跟大家分享,欢迎批评!

老规矩,先几张图.

1.为了进来看得清楚.刚开始没有加载音频列表.代码往前挪一挪即可.


2.按住 录音按钮的时候会出现麦克风.中间的麦克风是个帧动画.

其实就是用js控制图片显示隐藏.没啥好说的.这里值得说一说的是录音.微信的录音API后,如果录音时间太短,会录音失败.所以fail的时候还是需要处理一下.录音时间的限制和微信语音是一样的.60秒.


3.我在录音完成后才加载列表.

下图就是从微信存储的文件里获取到的列表信息.有储存路径,创建时间,文件大小.

这里的文件可能不只是音频.这里我没做判断.下面的路径都是wx:file//store_...

我也去找了下.在Tencent/micromsg/wxafiles/wx..../这一级目录就能找到了.

时间是格式化之后的.文件大小是B,转成KB如下.


手机目录如下.但是打开之后播放不了.目前原因不明.



下面是文件全名称.

1.tempFilePath : 录音之后的临时文件.第二次进入小程序就不能正常使用了.

2.savedFilePath :持久保存的文件路径.值得注意的是微信只给100M的储存空间.还是尽早上传到后台吧.



4.播放录音音频.

点击item就能听到你的声音了.别被自己吓住.哈哈.




上代码:

1.index.wxml

<!--index.wxml-->
<scroll-view>
 <view wx:if="{{voices}}" class="common-list" style="margin-bottom:120rpx;">
<block  wx:for="{{voices}}">
     <view class="board">
	                <view class="cell"  >
	                    <view class="cell-bd" data-key="{{item.filePath}}" bindtap="gotoPlay" > 
                       		<view  class="date">存储路径:{{item.filePath}}</view>
	                        <view  class="date" >存储时间:{{item.createTime}}</view>
	                        <view  class="date">音频大小:{{item.size}}KB</view>
	                    </view>  
						
	                </view>
        </view>
</block>
 </view>
 </scroll-view>
 
<view  wx:if="{{isSpeaking}}"  class="speak-style">
<image class="sound-style" src="../../images/voice_icon_speech_sound_1.png" ></image>
<image wx:if="{{j==2}}" class="sound-style" src="../../images/voice_icon_speech_sound_2.png" ></image>
<image wx:if="{{j==3}}" class="sound-style" src="../../images/voice_icon_speech_sound_3.png" ></image>
<image wx:if="{{j==4}}" class="sound-style" src="../../images/voice_icon_speech_sound_4.png" ></image>
<image wx:if="{{j==5}}"class="sound-style" src="../../images/voice_icon_speech_sound_5.png" ></image>
 </view>
 <view class="record-style">
 <button class="btn-style" bindtouchstart="touchdown" bindtouchend="touchup">按住 录音</button>
 </view>



2.index.wxss

/**index.wxss**/
.speak-style{
    position: relative;
    height: 240rpx;
    width: 240rpx;
    border-radius: 20rpx;
    margin: 50% auto;
    background: #26A5FF;
}
.item-style{
    margin-top: 30rpx;
    margin-bottom: 30rpx;
}
.text-style{
    text-align: center;

}
.record-style{
    position: fixed;
    bottom: 0;
    left: 0;
    height: 120rpx;
    width: 100%;
}
.btn-style{
  margin-left: 30rpx;
  margin-right: 30rpx;
}

.sound-style{
  position: absolute;
  width: 74rpx;
  height:150rpx;
  margin-top: 45rpx;
  margin-left: 83rpx;
}


.board {
  overflow: hidden;
  border-bottom: 2rpx solid #26A5FF;  
}
/*列布局*/
.cell{
    display: flex;
    margin: 20rpx;
}
.cell-hd{
    margin-left: 10rpx;
    color: #885A38;
}
.cell .cell-bd{
    flex:1;
    position: relative;
   
}
/**只显示一行*/
.date{
    font-size: 30rpx;
    text-overflow: ellipsis; 
    white-space:nowrap;
    overflow:hidden; 
}

3.index.js

//index.js
//获取应用实例
var app = getApp()
Page({
  data: {
    j: 1,//帧动画初始图片
    isSpeaking: false,//是否正在说话
    voices: [],//音频数组
  },
  onLoad: function () {
  },
  //手指按下
  touchdown: function () {
    console.log("手指按下了...")
    console.log("new date : " + new Date)
    var _this = this;
    speaking.call(this);
    this.setData({
      isSpeaking: true
    })
    //开始录音
    wx.startRecord({
      success: function (res) {
        //临时路径,下次进入小程序时无法正常使用
        var tempFilePath = res.tempFilePath
        console.log("tempFilePath: " + tempFilePath)
        //持久保存
        wx.saveFile({
          tempFilePath: tempFilePath,
          success: function (res) {
            //持久路径
            //本地文件存储的大小限制为 100M
            var savedFilePath = res.savedFilePath
            console.log("savedFilePath: " + savedFilePath)
          }
        })
        wx.showToast({
          title: '恭喜!录音成功',
          icon: 'success',
          duration: 1000
        })
        //获取录音音频列表
        wx.getSavedFileList({
          success: function (res) {
            var voices = [];
            for (var i = 0; i < res.fileList.length; i++) {
              //格式化时间
              var createTime = new Date(res.fileList[i].createTime)
              //将音频大小B转为KB
              var size = (res.fileList[i].size / 1024).toFixed(2);
              var voice = { filePath: res.fileList[i].filePath, createTime: createTime, size: size };
              console.log("文件路径: " + res.fileList[i].filePath)
              console.log("文件时间: " + createTime)
              console.log("文件大小: " + size)
              voices = voices.concat(voice);
            }
            _this.setData({
              voices: voices
            })
          }
        })
      },
      fail: function (res) {
        //录音失败
        wx.showModal({
          title: '提示',
          content: '录音的姿势不对!',
          showCancel: false,
          success: function (res) {
            if (res.confirm) {
              console.log('用户点击确定')
              return
            }
          }
        })
      }
    })
  },
  //手指抬起
  touchup: function () {
    console.log("手指抬起了...")
    this.setData({
      isSpeaking: false,
    })
    clearInterval(this.timer)
    wx.stopRecord()
  },
  //点击播放录音
  gotoPlay: function (e) {
    var filePath = e.currentTarget.dataset.key;
    //点击开始播放
    wx.showToast({
      title: '开始播放',
      icon: 'success',
      duration: 1000
    })
    wx.playVoice({
      filePath: filePath,
      success: function () {
        wx.showToast({
          title: '播放结束',
          icon: 'success',
          duration: 1000
        })
      }
    })
  }
})
//麦克风帧动画
function speaking() {
  var _this = this;
  //话筒帧动画
  var i = 1;
  this.timer = setInterval(function () {
    i++;
    i = i % 5;
    _this.setData({
      j: i
    })
  }, 200);
}


注意:

1.录音的音频默认是存在本地的临时路径下.第二次进入小程序无法正常使用,可以存持久,但是本地文件大小的限制是100M,最好还是上传后台.

2.录音的时间不能太短.否则会失败;也不能超过60秒.到了60秒会自动停止录音.

3.音频播放不能同时播放多个音频.看文档.微信小程序 播放音频文档



demo代码下载


我的博客:http://blog.csdn.net/qq_31383345

欢迎批评!




作者:qq_31383345 发表于2016/11/27 0:08:53 原文链接
阅读:22 评论:0 查看评论

[webAPP项目]基于MUI框架webAPP开发功能流程之引导图制作详解01

$
0
0

前言

我们在构建一个APP时,少不了APP运行前的开机引导图,现在来详细介绍一下MUI框架关于这一操作的详细步骤!

原理

如果用户第一次下载使用该软件,就显示引导图;如果非第一次使用,则跳过这一步.

效果

这里写图片描述
在正式介绍之前首先要明白几个概念

概念1 页面初始化

1.mui.plusReady()方法

语法:mui.plusReady(function);
参数:是函数

在app开发中,若要使用[HTML5+扩展api](http://www.html5plus.org/doc/h5p.html),必须等plusready事件发生后才能正常使用,mui将该事件封装成了mui.plusReady()方法,涉及到HTML5+的api,建议都写在mui.plusReady方法中.

例如,获取当前网页的URL:

mui.plusReady(function(){
     console.log("当前页面URL:"+plus.webview.currentWebview().getURL());
});

2.mui.init()

ui框架将很多功能配置都集中在mui.init方法中,要使用某项功能,只需要在mui.init方法中完成对应参数配置即可
所以每次在每一个页面都调用一下mui.init()这个方法.

3.mui.ready()

当DOM准备就绪时,指定一个函数来执行

4.HTML5+与MUI 的区别

HTML5+项目 是用的原生的HTML5+的API做的。

mui项目是封装了原生的HTML5+的API的一个框架,方便开发者迅速开发应用。含mui.css(提供了很多页面样式)和mui.js(封装了原生HTML5+API)。

举个例子。你在做web前端开发的时候,用原生的js就相当于这里的原生HTML5+,用jquery就相当于这里的mui。

5. mui.plusReady()与mui.ready()那个先执行

一般情况是ready先,plusReady后;
ready代表DomcontentLoaded,plusReady代表plus基座ok

好明白了这些概念之后,我们就开始我们的代码:

页面A

//引导图函数
 function launchScreen() {
        //读取本地存储,检查是否为首次启动
        var showGuide = plus.storage.getItem("lauchFlag");
        if(showGuide){ 
            //有值,说明已经显示过了,无需显示;
            //关闭splash页面;
            plus.navigator.closeSplashscreen();
            plus.navigator.setFullscreen(false);
        }else{
            //显示启动导航
            mui.openWindow({
                id:'guid',//子页面的ID
                url:'templete/guid.html',//子页面
                show:{
                    aniShow:'none'
                },
                waiting:{
                    autoShow:false
                }
            });
        }
    }

子B页面:

            mui.plusReady(function() {
                plus.navigator.setFullscreen(true);
                plus.navigator.closeSplashscreen();
            });
            //立即体验按钮点击事件
            document.getElementById("close").addEventListener('tap', function(event) {
                plus.storage.setItem("lauchFlag", "true"); 
                plus.navigator.setFullscreen(false);
                plus.webview.currentWebview().close();
            }, false);

在A页面进行调用

 mui.plusReady(function(){
        launchScreen();
        mui.init();
    });

这面还要明白几个概念

打开新页面

这里有我就引用官方文档给大家解释一下:

  做web app,一个无法避开的问题就是转场动画;web是基于链接构建的,从一个页面点击链接跳转到另一个页面,如果通过有刷新的打开方式,用户要面对一个空白的页面等待;如果通过无刷新的方式,用Javascript移入DOM节点(常见的SPA解决方案),会碰到很高的性能挑战:DOM节点繁多,页面太大,转场动画不流畅甚至导致浏览器崩溃; mui的解决思路是:单webview只承载单个页面的dom,减少dom层级及页面大小;页面切换使用原生动画,将最耗性能的部分交给原生实现.

原理:

mui.openWindow({
    url:new-page-url,
    id:new-page-id,
    styles:{
      top:newpage-top-position,//新页面顶部位置
      bottom:newage-bottom-position,//新页面底部位置
      width:newpage-width,//新页面宽度,默认为100%
      height:newpage-height,//新页面高度,默认为100%
      ......
    },
    extras:{
      .....//自定义扩展参数,可以用来处理页面间传值
    },
    createNew:false,//是否重复创建同样id的webview,默认为false:不重复创建,直接显示
    show:{
      autoShow:true,//页面loaded事件发生后自动显示,默认为true
      aniShow:animationType,//页面显示动画,默认为”slide-in-right“;
      duration:animationTime//页面动画持续时间,Android平台默认100毫秒,iOS平台默认200毫秒;
    },
    waiting:{
      autoShow:true,//自动显示等待框,默认为true
      title:'正在加载...',//等待对话框上显示的提示内容
      options:{
        width:waiting-dialog-widht,//等待框背景区域宽度,默认根据内容自动计算合适宽度
        height:waiting-dialog-height,//等待框背景区域高度,默认根据内容自动计算合适高度
        ......
      }
    }
})

参数解释:

1.styles

窗口参数,参考5+规范中的WebviewStyle;特别注意,height和width两个属性,即使不设置,也默认按100%计算;因此若设置了top值为非"0px"的情况,建议同时设置bottom值,否则5+ runtime根据高度100%计算,可能会造成页面真实底部位置超出屏幕范围的情况;left、right同理。

2.extras

新窗口的额外扩展参数,可用来处理页面间传值;例如:
var webview = mui.openWindow({
    url:'info.html',
    extras:{
        name:'mui'  //扩展参数
    }
});
console.log(webview.name);//输出mui字符串
注意:扩展参数仅在打开新窗口时有效,若目标窗口为预加载页面,则通过mui.openWindow方法打开时传递的extras参数无效。

3.createNew

是否重复创建相同id的webview;

为优化性能、避免app中重复创建webview,mui v1.7.0开始增加createNew参数,默认为false;判断逻辑如下:

createNew参数为为true,则不判断重复,每次都新建webview;

createNew参数为为fasle,则先查找当前App中是否已存在同样id的

webview,若存在则直接显示;否则新创建并根据show参数执行显示逻辑;
注意:plusReady事件仅在webview首次创建时触发,使用mui.openWindow方法多次打开已存在的同样id的webview时,是不会重复触发plusReady事件的; 因此若业务写在plusReady事件中,可能会出现执行结果和预期不一致的情况;此时可通过自定义事件触发; 案例参考:mui.plusReady有时会失效;

4.show

窗口显示控制参数,具体参数如下:

autoShow:目标窗口loaded事件发生后,是否自动显示,默认为true;若为false,则仅创建但不显示webview;若目标页面为预加载页面,则该参数无效;

aniShow表示页面显示动画,比如从右侧划入、从下侧划入等,具体可参考5+规范中的AnimationTypeShow

duration:显示Webview窗口动画的持续时间,单位为ms
waiting

5.系统等待框参数

mui框架在打开新页面时等待框的处理逻辑为:

显示等待框–>创建目标页面webview–>目标页面loaded事件发生–>关闭等待框;

因此,只有当新页面为新创建页面(webview)时,会显示等待框,否则若为预加载好的页面,则直接显示目标页面,不会显示等待框。waiting中的具体参数:

autoShow:是否自动显示等待框,默认为true;若为false,则不显示等待框;注意:若waiting框的autoShow为true,但目标页面不自动显示,则需在目标页面中通过如下代码关闭等待框:plus.nativeUI.closeWaiting();

title:等待框上的提示文字

options表示等待框显示参数,比如宽高、背景色、提示文字颜色等.

涉及到的知识点差不多就讲完了,请看下面介绍

[webAPP项目]基于MUI框架webAPP开发功能流程之头部导航与底部导航制作详解02]

明天给大家更新!

注公众号:
这里写图片描述

作者:BaiHuaXiu123 发表于2016/11/27 0:10:02 原文链接
阅读:32 评论:0 查看评论

二维码(QR code)基本结构及生成原理

$
0
0

什么是二维码

二维码 (2-dimensional bar code),是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的。

在许多种类的二维条码中,常用的码制有:Data Matrix, Maxi Code, Aztec, QR Code, Vericode, PDF417, Ultracode, Code 49, Code 16K等。

二维条码/二维码可以分为堆叠式/行排式二维条码和矩阵式二维条码。

1.堆叠式/行排式二维条码,堆叠式/行排式二维条码又称堆积式二维条码或层排式二维条码,其编码原理是建立在一维条码基础之上,按需要堆积成二行或多行。它在编码设计、校验原理、识读方式等方面继承了一维条码的一些特点,识读设备与条码印刷与一维条码技术兼容。但由于行数的增加,需要对行进行判定,其译码算法与软件也不完全相同于一维条码。有代表性的行排式二维条码有:Code 16K、Code 49、PDF417、MicroPDF417 等。

2.矩阵式二维码,最流行莫过于QR CODE ,我们常说的二维码就是它了。矩阵式二维条码(又称棋盘式二维条码)它是在一个矩形空间通过黑、白像素在矩阵中的不同分布进行编码。在矩阵相应元素位置上,用点(方点、圆点或其他形状)的出现表示二进制“1”,点的不出现表示二进制的“0”,点的排列组合确定了矩阵式二维条码所代表的意义。矩阵式二维条码是建立在计算机图像处理技术、组合编码原理等基础上的一种新型图形符号自动识读处理码制。具有代表性的矩阵式二维条码有:Code One、MaxiCode、QR Code、 Data Matrix、Han Xin Code、Grid Matrix 等。

二维码在现实生活中的应用越来越普遍由于QR CODE的流行,二维码又称QR code。

QR码的特点

1.存储大容量信息

传统的条形码只能处理20位左右的信息量,与此相比,QR码可处理条形码的几十倍到几百倍的信息量。

另外,QR码还可以支持所有类型的数据。(如:数字、英文字母、日文字母、汉字、符号、二进制、控制码等)。一个QR码最多可以处理7089字(仅用数字时)的巨大信息量。

2.在小空间内打印

QR码使用纵向和横向两个方向处理数据,如果是相同的信息量,QR码所占空间为条形码的十分之一左右。(还支持Micro QR码,可以在更小空间内处理数据。)

这里写图片描述

3.有效表现各种字母

QR码是日本国产的二维码,因此非常适合处理日文字母和汉字。QR码字集规格定义是按照日本标准“JIS第一级和第二级的汉字”制定的,因此在日语处理方面,每一个全角字母和汉字都用13比特的数据处理,效率较高,与其他二维码相比,可以多存储20%以上的信息。

这里写图片描述

4.对变脏和破损的适应能力强

QR码具备“纠错功能”,即使部分编码变脏或破损,也可以恢复数据。数据恢复以码字为单位(是组成内部数据的单位,在QR码的情况下,每8比特代表1码字),最多可以纠错约30%(根据变脏和破损程度的不同,也存在无法恢复的情况)。

5.可以从任意方向读取

QR码从360°任一方向均可快速读取。其奥秘就在于QR码中的3处定位图案,可以帮助QR码不受背景样式的影响,实现快速稳定的读取。

这里写图片描述

6.支持数据合并功能

QR码可以将数据分割为多个编码,最多支持16个QR码。使用这一功能,还可以在狭长区域内打印QR码。另外,也可以把多个分割编码合并为单个数据。

这里写图片描述

QR码的信息量和版本

QR码设有1到40的不同版本(种类),每个版本都具备固有的码元结构(码元数)。(码元是指构成QR码的方形黑白点。)

“码元结构”是指二维码中的码元数。从版本1(21码元×21码元)开始,在纵向和横向各自以4码元为单位递增,一直到版本40(177码元×177码元)。

这里写图片描述

QR码的各个版本结合数据量、字符类型和纠错级别,均设有相对应的最多输入字符数。也就是说,如果增加数据量,则需要使用更多的码元来组成QR码,QR码就会变得更大。

例如,需要输入的数据为100位的数字时,通过以下步骤来选定。
1.假设要输入的数据种类为“数字”
2.从“L”“M”“Q”“H”中选择纠错级别。(假设选择“M”)
3.查看下表,先从数字列找出数字为100以上且接近100的,其次找出纠错级别“M”,两者交叉的部分就是最佳版本。

这里写图片描述

通过下面的计算为每个字符类型,总比特数的计算方法。
这里写图片描述

QR码的纠错

QR码具有“纠错功能”。即使编码变脏或破损,也可自动恢复数据。这一“纠错能力”具备4个级别,用户可根据使用环境选择相应的级别。调高级别,纠错能力也相应提高,但由于数据量会随之增加,编码尺寸也也会变大。
用户应综合考虑使用环境、编码尺寸等因素后选择相应的级别。 在工厂等容易沾染赃物的环境下,可以选择级别Q或H,在不那么脏的环境下,且数据量较多的时候,也可以选择级别L。一般情况下用户大多选择级别M(15%)。
这里写图片描述

纠错级别的比率,是指全部码字与可以纠错的码字的比率。
例如,需要编码的码字数据有100个,并且想对其中的一半,也就是50个码字进行纠错,则计算方法如下。纠错需要相当于码字2倍的符号(RS编码※),因此在这种情况下的数量为50个×2=100码字。因此,全部码字数量为200个,其中用作纠错的码字为50个,所以计算得出,相对于全部码字的纠错率就是25%。这一比率相当于QR码纠错级别中的“Q”级别。

另外,在上述例子当中,也可以认为相对于码字数据的纠错率为50%,但变脏或破损的部位不仅仅局限于码字数据部分,因此,在QR码中,还是用相对于全部码字的比率来描述纠错率。

※ RS编码:QR码的纠错功能是通过将RS编码附加到原数据中的方式实现的。RS编码是应用于音乐CD等用途的数学纠错方法。它能以字节为单位进行纠错,适合用于错误位置会集中的突发错误。

QR码的种类

这里写图片描述

QR码的基本结构

QR(Quick-Response) code是被广泛使用的一种二维码,解码速度快。它可以存储多用类型,下图是qrcode的基本结构:

这里写图片描述

位置探测图形、位置探测图形分隔符、定位图形:用于对二维码的定位,对每个QR码来说,位置都是固定存在的,只是大小规格会有所差异;
校正图形:规格确定,校正图形的数量和位置也就确定了;
格式信息:表示改二维码的纠错级别,分为L、M、Q、H;

版本信息:即二维码的规格,QR码符号共有40种规格的矩阵(一般为黑白色),从21x21(版本1),到177x177(版本40),每一版本符号比前一版本 每边增加4个模块。

数据和纠错码字:实际保存的二维码信息,和纠错码字(用于修正二维码损坏带来的错误)。

QR码的编码过程

下面是简要QR编码过程:

1.数据分析:确定编码的字符类型,按相应的字符集转换成符号字符; 选择纠错等级,在规格一定的条件下,纠错等级越高其真实数据的容量越小。

2.数据编码:将数据字符转换为位流,每8位一个码字,整体构成一个数据的码字序列。其实知道这个数据码字序列就知道了二维码的数据内容。

容量如下:

格式 容量
数字 最多7089字符
字母 最多4296字符
二进制数(8 bit) 最多2953字节
日文汉字/片假名 最多1817字符(采用Shift JIS)
中文汉字 最多984字符(采用UTF-8)
中文汉字 最多1800字符(采用BIG5)

模式编码如下:

模式 指示符
ECI 0111
数字 0001
字母数字 0010
8位字节 0100
日本汉字 1000
中文汉字 1101
结构链接 0011
FNCI 0101(第一位置)
1001(第二位置)
终止符(信息结尾) 0000

数据可以按照一种模式进行编码,以便进行更高效的解码,例如:对数据:01234567编码(版本1-H),
1)分组:012 345 67
2)转成二进制:
012→0000001100
345→0101011001
67 →1000011
3)转成序列:0000001100 0101011001 1000011
4)字符数 转成二进制:8→0000001000
5)加入模式指示符(上图数字)0001:0001 0000001000 0000001100 0101011001 1000011

对于字母、中文、日文等只是分组的方式、模式等内容有所区别。基本方法是一致的。

3.纠错编码:按需要将上面的码字序列分块,并根据纠错等级和分块的码字,产生纠错码字,并把纠错码字加入到数据码字序列后面,成为一个新的序列。

纠错等级 纠错水平
L 7%字码修正
M 15%字码修正
Q 25%字码修正
H 30%字码修正

在二维码规格和纠错等级确定的情况下,其实它所能容纳的码字总数和纠错码字数也就确定了,比如:版本10,纠错等级时H时,总共能容纳346个码字,其中224个纠错码字。

就是说二维码区域中大约1/3的码字时冗余的。对于这224个纠错码字,它能够纠正112个替代错误(如黑白颠倒)或者224个据读错误(无法读到或者无法译码),这样纠错容量为:112/346=32.4%。

4.构造最终数据信息:在规格确定的条件下,将上面产生的序列按次序放如分块中。

按规定把数据分块,然后对每一块进行计算,得出相应的纠错码字区块,把纠错码字区块 按顺序构成一个序列,添加到原先的数据码字序列后面。
如:D1, D12, D23, D35, D2, D13, D24, D36, … D11, D22, D33, D45, D34, D46, E1, E23,E45, E67, E2, E24, E46, E68,…

5.构造矩阵:将探测图形、分隔符、定位图形、校正图形和码字模块放入矩阵中。

这里写图片描述

把上面的完整序列填充到相应规格的二维码矩阵的区域中。

6.掩摸:将掩摸图形用于符号的编码区域,使得二维码图形中的深色和浅色(黑色和白色)区域能够比率最优的分布。

7.格式和版本信息:生成格式和版本信息放入相应区域内。

版本7-40都包含了版本信息,没有版本信息的全为0。二维码上两个位置包含了版本信息,它们是冗余的。

版本信息共18位,6X3的矩阵,其中6位时数据为,如版本号8,数据位的信息时 001000,后面的12位是纠错位。

QR码的应用

QR码可以很“方便”地应用于各种场合。除了传单和名片等印刷物之外,还可以应用于各种广泛领域,如结算系统等与生活息息相关的领域以及工厂、流通等各种商业领域。QR码已经成为日常生活不可或缺的工具。2012年,公益财团法人日本设计振兴会对QR码表示了高度评价,由于QR码的功能非常贴近生活,设计精致,在其主办的设计推荐制度中授予QR码“优秀设计奖”。

QR码的标准化

“想让更多人使用QR码”,秉承这一理念,DENSO WAVE全面公开了QR码的相关标准。目前,QR码已经在国家标准和国际标准中实现标准化,任何人都可以随意查看该标准。

※DENSO WAVE INCORPORATED已宣布,不行使本公司就标准QR码拥有的专利权(专利第2938338号)。

1997年 10月 被采纳为AIM International(国际自动识别工业会)标准(ISS - QR Code)
1998年 3月 被采纳为JEIDA(日本电子工业振兴协会)标准(JEIDA-55)
1999年 1月 被采纳为JIS(日本工业标准)(JIS X 0510)
2000年 6月 被采纳为ISO国际标准 (ISO/IEC18004)
2004年 11月 Micro QR码被采纳为JIS(日本工业标准)(JIS X 0510)
2011年 12月 国际标准化组织GS1将QR码采纳为面向移动终端的标准

QR码的简要标准
这里写图片描述

QR码标准文件下载

QR码在ISO(ISO/IEC18004)中得到 标准化。因此,请从以下团体的网站购买QR码标准文件,在下面网址中搜索18004。

http://www.iso.ch/iso/en/prods-services/ISOstore/store.html

不过2016标准版的价格略贵,可到本人的资源中下载2006标准版的,链接如下:

http://download.csdn.net/detail/u012611878/9687105

欢迎下崽。

之后有时间会试着用代码实现。

作者:u012611878 发表于2016/11/27 0:14:49 原文链接
阅读:10 评论:0 查看评论

Java GC 机制与内存分配策略

$
0
0

收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现

自动内存管理解决的是:给对象分配内存 以及 回收分配给对象的内存

为什么我们要了解学习 GC 与内存分配呢? 
在 JVM 自动内存管理机制的帮助下,不再需要为每一个new操作写配对的delete/free代码。但出现内存泄漏和溢出的问题时,如果不了解虚拟机是怎样使用内存的,那么排查错误将是一项非常艰难的工作。

GC(垃圾收集器)在对堆进行回收前,会先确定哪些对象“存活”,哪些已经“死去”。那么就有了对象存活判定算法

对象存活判定算法

算法思想:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,当引用失效时,计数器值减1,任何时刻计数器为0的对象就是不可能再被使用的。 优点:实现简单,判断效率也很高

缺点:很难解决对象之间相互循环引用的问题

可达性分析算法:

算法思想:通过一系列的“GC Roots”对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

如图:object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。 
可达性分析算法

可作为 GC Roots 的对象包括以下: 1.虚拟机栈中引用的对象 2.方法区中类静态属性引用的对象 3.方法区中常量引用的对象

4.本地方法栈中 JNI 引用的对象

生存还是死亡?

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候他们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程: 
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它,这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是:如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统的奔溃。 
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法中成功拯救自己,只需重新与引用链上的任何一个对象建立关联即可,比如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收“的集合,如果对象这时候还没有逃脱,那基本上它就真的被回收了。

另外,任何一个对象的finalize()方法都只会被系统自动调用一次。finalize()能做的所有工作,使用try-finally或者其他方式都可以做的更好,更及时,所以建议大家完全可以忘掉Java语言中有这个方法的存在。详见《深入理解Java虚拟机》

垃圾收集算法

算法分为“标记“和”清除“两个阶段: 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 不足之处: 1. 效率问题,标记和清除两个过程的效率都不高。

2. 空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法:

算法实现:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。 
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。实现简单,运行高效。算法的代价是讲内存缩小为了原来的一半,未免太高了点。

标记-整理算法:

算法实现:标记出所有需要回收的对象、让所有存活的对象都向一端移动。然后直接清理掉端边界以外的内存。

分代收集算法:

算法实现:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理“或者”标记-整理“算法来进行回收。

新生代 : 分为三个空间,一个Eden空间 ,两个Survivor空间。 
绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。

老年代 : 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)

绝大多数刚刚被创建的对象会存放在Eden空间。在Eden空间执行了第一次GC之后,存活的对象被移动到其中一个Survivor空间。此后,在Eden空间执行GC之后,存活的对象会被堆积在同一个Survivor空间。当一个Survivor空间饱和,还在存活的对象会被移动到另一个Survivor空间。之后会清空已经饱和的那个Survivor空间。 
在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

垃圾收集器

垃圾收集器

如图是作用于不同分代的垃圾收集器,如果两个收集器之间存在连线,就可以搭配使用。虚拟机所在的区域,则表示它是属于新生代收集器还是老年代收集器。

学习各种垃圾收集器之前先了解下“Stop the World“。“Stop the World“会在任何一种GC算法中发生。“Stop the World“意味着 JVM 因为要执行GC而停止了应用程序的执行。当“Stop the World“发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。GC优化很多时候就是指减少“Stop the World“发生的时间。 
“Stop the World“这样理解很形象:你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间外等待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?

Serial收集器:单线程,新生代收集器,使用复制算法。它只会使用一个CPU或一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须“Stop the World“,暂停替他所有的工作线程,直到它收集结束。

ParNew收集器: Serial收集器的多线程版本,控制参数、收集算法、Stop the World、对象分配规则、回收策略都与Serial收集器完全一样

Parallel Scavenge收集器: 生代收集器,使用复制算法,并行多线程。

Serial Old收集器: Serial收集器的老年代版本,单线程,使用标记-整理算法。

Parallel Old收集器: Parallel Scavenge收集器的老年代版本,多线程,使用标记-整理算法

CMS收集器:一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法。运作过程分四个步骤:初始标记 、并发标记、重新标记、并发清除。 初始标记、重新标记这两个步骤仍然需要“Stop the World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这一阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间段。 整个过程中耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,所以,CMS收集器的内存回收过程是与用户线程一起并发执行的。 优点:并发收集,低停顿

缺点:对CPU资源非常敏感、无法处理浮动垃圾、基于标记清除算法,收集结束时有大量控件碎片产生

G1收集器: G1收集器是当今收集器技术发展最前沿成果之一,一种面向服务端应用的垃圾收集器。 G1的特点:并行与并发、分代手机、空间整合、可预测的停顿 运作过程如下:初始标记、并发标记、最终标记、筛选回收。 初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。 并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。 而最终标记阶段则是则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面。最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序。根据用户所期望的GC停顿时间来制定回收计划。

内存分配与回收策略

对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代:大对象是指需要大量连续内存控件的Java对象,最典型的大对象就是那种很长的字符串以及数组。

长期存活的对象将进入老年代:虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代。

动态对象年龄判定:虚拟机并不是永远要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

总结

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也就没有什么必然的内存回收行为。

以上是我学习《深入理解Java虚拟机》一书的整理笔记。

参考资料《深入理解Java虚拟机》

JavaAndroid

作者:qianhaohong 发表于2016/11/27 19:25:30 原文链接
阅读:45 评论:0 查看评论

对于 Java 回调的最深刻解析

$
0
0

先说点什么

这些天,没啥事的时候,就想到什么,就去找点资料看,补充学习一下,以前老是忘记的东西,或者新听到的东西。这次,就记录一下之前一直很容易弄混的概念,回调。其实随便谷歌百度一下,都能找到各种例子解析什么的,不过,我相信你也会和我一样,看的时候觉得很简单,也很容易理解,不过,过一阵子之后,突然需要用的时候,又不清不楚了,又去找资料啊,看代码啊,找例子啊。说多都是泪,我就是这样过来的。! 
这里写图片描述

第一站,来个骚问题

开始之前,先想象一个场景:幼稚园的小朋友(邱同学)刚刚学习了10以内的加法。

老师大人在黑板上写一个式子 “1 + 1 = ”,由邱同学来填空。由于已经学习了10以内的加法,邱同学可以完全靠自己来计算这个题目,额,这个需求简单吧,就写一个add(int a,int b) 不就解决了么,是的。那动手吧:

public class Student
{
    private String name = null;
    public Student(String name)
    {   
        this.name = name;
    }
    public void setName(String name)
    {        this.name = name;
    }
    private int add(int a, int b)
    {        return a + b;
    }
    public void fillBlank(int a, int b)
    {
        int result = add(a, b);
        System.out.println(name + "心算:" + a + " + " + b + " = " + result);
    }
}

邱同学在填空(fillBalnk)的时候,直接心算(add)了一下,得出结果是2,并将结果写在空格里,测试:

public class Test
{
    public static void main(String[] args)
    {
        int a = 1;
        int b = 1;
        Student s = new Student("邱同学");
        s.fillBlank(a, b);
    }
}

运行结果如下:

邱同学心算:1 + 1 = 2

该过程完全由Student类的实例对象单独完成,并未涉及回调机制。

第二站,进击的老师

课间,老师突发奇想在黑板上写了“168 + 291 = ”让邱同学完成,然后回办公室了。

呀儿咯!为什么所有老师都跟邱同学过不去啊?明明超纲了好不好~~ 
这里写图片描述 
这时候邱同学然而明显不能再像上面那样靠心算来完成了,正在一脸懵逼的时候,同桌的小娴娴同学递过来一个只能计算加法的计算器,而邱同学恰好知道怎么用计算器,于是通过计算器计算得到结果并完成了填空。

计算器的代码为:

public class Calculator
{
    public int add(int a, int b)
    {        return a + b;
    }
}

修改Student类,添加使用计算器的方法:

public class Student
{
    private String name = null;
    public Student(String name)
    {        
        this.name = name;
    }
    public void setName(String name)
    {        this.name = name;
    }
    @SuppressWarnings("unused")
    private int calcADD(int a, int b)
    {        return a + b;
    }
    private int useCalculator(int a, int b)
    {        return new Calculator().add(a, b);
    }
    public void fillBlank(int a, int b)
    {
        int result = useCalculator(a, b);
        System.out.println(name + "使用计算器:" + a + " + " + b + " = " + result);
    }
}

测试代码如下:

public class Test
{
    public static void main(String[] args)
    {
        int a = 168;
        int b = 291;
        Student s = new Student("邱同学");
        s.fillBlank(a, b);
    }
}

运行结果如下:

邱同学使用计算器:168 + 291 = 459

该过程中仍未涉及到回调机制,但是部分邱同学的部分工作已经实现了转移,由计算器来协助实现。

第三站,暴走的老师

发现邱同学完成了3位数的加法,老师觉得邱同学很聪明,是个可塑之才。于是又在黑板上写下了“26549 + 16487 = ”,让邱同学上课之前完成填空,然后又回办公室了。

邱同学看着教室外面撒欢儿的小伙伴,不禁悲从中来。再不出去玩,这个课间就要废了啊!!!! 看着小娴娴再一次递上来的计算器,邱同学心生一计:让小娴娴代劳。

邱同学告诉小娴娴题目是“26549 + 16487 = ”,然后指出填写结果的具体位置,然后就出去快乐的玩耍了。

这里,不把小娴娴单独实现出来,而是把这个只能算加法的计算器和小娴娴看成一个整体,一个会算结果还会填空的超级计算器。这个超级计算器需要传的参数是两个加数和要填空的位置,而这些内容需要邱同学提前告知,也就是邱同学要把自己的一部分方法暴漏给小娴娴,最简单的方法就是把自己的引用和两个加数一块告诉小娴娴。

因此,超级计算器的add方法应该包含两个操作数和邱同学自身的引用,代码如下:

public class SuperCalculator
{
    public void add(int a, int b, Student  xiaoming)
    {
        int result = a + b;
        xiaoming.fillBlank(a, b, result);
    }
}

邱同学这边现在已经不需要心算,也不需要使用计算器了,因此只需要有一个方法可以向小娴娴寻求帮助就行了,代码如下:

public class Student
{
    private String name = null;
    public Student(String name)
    {        
        this.name = name;
    }
    public void setName(String name)
    {        this.name = name;
    }
    public void callHelp (int a, int b)
    {        new SuperCalculator().add(a, b, this);
    }
    public void fillBlank(int a, int b, int result)
    {
        System.out.println(name + "求助小娴娴计算:" + a + " + " + b + " = " + result);
    }
}

测试代码如下:

public class Test
{
    public static void main(String[] args)
    {
        int a = 26549;
        int b = 16487;
        Student s = new Student("邱同学");
        s.callHelp(a, b);
    }
}

运行结果为:

邱同学求助小娴娴计算:26549 + 16487 = 43036

执行流程为:邱同学通过自身的callHelp方法调用了小娴娴(new SuperCalculator())的add方法,在调用的时候将自身的引用(this)当做参数一并传入,小娴娴在使用计算器得出结果之后,回调了邱同学的fillBlank方法,将结果填在了黑板上的空格里。

灯灯灯!到这里,回调功能就正式登场了,邱同学的fillBlank方法就是我们常说的回调函数。

通过这种方式,可以很明显的看出,对于完成老师的填空题这个任务上,邱同学已经不需要等待到加法做完且结果填写在黑板上才能去跟小伙伴们撒欢了,填空这个工作由超级计算器小娴娴来做了。回调的优势已经开始体现了。

第四站,不明真相的吃瓜婆婆

幼稚园的门口有一个头发花白的老婆婆,每天风雨无阻在那里摆着地摊卖一些快过期的垃圾食品。由于年纪大了,脑子有些糊涂,经常算不清楚自己挣了多少钱。有一天,她无意间听到了邱同学跟小伙伴们吹嘘自己如何在小娴娴的帮助下与幼师斗智斗勇。于是,婆婆决定找到小娴娴牌超级计算器来做自己的小帮手,并提供一包卫龙辣条作为报酬。小娴娴经不住诱惑,答应了。这里写图片描述

之前,我们发现小娴娴牌超级计算器的add方法需要的参数是两个整型变量和一个Student对象,但是老婆婆她不是学生,是个小商贩啊,这是最尴尬的,所以这里肯定要做修改。这种情况下,我们很自然的会想到继承和多态。如果让邱同学这个学生和老婆婆这个小商贩从一个父类进行继承,那么我们只需要给小娴娴牌超级计算器传入一个父类的引用就可以啦。

不过,实际使用中,考虑到Java的单继承,以及不希望把自身太多东西暴漏给别人,这里使用从接口继承的方式配合内部类来做。

换句话说,小娴娴希望以后继续向班里的小朋友们提供计算服务,同时还能向老婆婆提供算账服务,甚至以后能够拓展其他人的业务,于是她向所有的顾客约定了一个办法,用于统一的处理,也就是自己需要的操作数和做完计算之后应该怎么做。这个统一的方法,小娴娴做成了一个接口,提供给了大家,代码如下:

public interface doJob
{
public void fillBlank(int a, int b, int result);
}

同时,小娴娴修改了自己的计算器,使其可以同时处理不同的实现了doJob接口的人,代码如下:

public class SuperCalculator
{
    public void add(int a, int b, doJob  customer)
    {
        int result = a + b;
        customer.fillBlank(a, b, result);
    }
}

邱同学和老婆婆拿到这个接口之后,只要实现了这个接口,就相当于按照统一的模式告诉小娴娴得到结果之后的处理办法,按照之前说的使用内部类来做,代码如下:

邱同学的:

public class Student
{
    private String name = null;
    public Student(String name)
    {      
        this.name = name;
    }
    public void setName(String name)
    {        this.name = name;
    }
    public class doHomeWork implements doJob
    {
        @Override
        public void fillBlank(int a, int b, int result)
        {         
            System.out.println(name + "求助小娴娴计算作业:" + a + " + " + b + " = " + result);
        }
    } 
    public void callHelp (int a, int b)
    {        new SuperCalculator().add(a, b, new doHomeWork());
    }
}

老婆婆的:

public class Popo
{
    private String name = null;
    public Seller(String name)
    {       
        this.name = name;
    }
    public void setName(String name)
    {        this.name = name;
    }
    public class doHomeWork implements doJob
    {
        @Override
        public void fillBlank(int a, int b, int result)
        {      
            System.out.println(name + "求助小娴娴帮忙算数:" + a + " + " + b + " = " + result + "元");
        }
    }
    public void callHelp (int a, int b)
    {        new SuperCalculator().add(a, b, new doHomeWork());
    }
}

测试程序如下:

public class Test
{
    public static void main(String[] args)
    {
        int a = 60;
        int b = 50;
        int c = 40;
        int d = 30;
        Student s = new Student("邱同学");
        s.callHelp(a, b);
        Popo p= new Popo("不明真相的婆婆");
        p.callHelp(c, d);
    }
}

运行结果为:

邱同学求助小娴娴计算作业:a+b=110;

不明真相的婆婆求助小娴娴帮忙算数:c+d=70;


作者:qianhaohong 发表于2016/11/27 19:26:49 原文链接
阅读:49 评论:0 查看评论

泛型详解 高级进阶

$
0
0

前言:被温水煮惯了,梦想的东西总是不敢于尝试,失败了又怎样,最多从头来过。

相关文章:

1、《夯实JAVA基本之一 —— 泛型详解(1):基本使用》
2、《夯实JAVA基本之一——泛型详解(2):高级进阶》
3、《夯实JAVA基本之二 —— 反射(1):基本类周边信息获取》
4、《夯实JAVA基本之二 —— 反射(2):泛型相关周边信息获取》
5、《夯实JAVA基本之二 —— 反射(3):类内部信息获取》


上一篇给大家初步讲解了泛型变量的各种应用环境,这篇将更深入的讲解一下有关类型绑定,通配符方面的知识。

一、类型绑定

1、引入

我们重新看上篇写的一个泛型:
class Point<t> {  
    private T x;       // 表示X坐标  
    private T y;       // 表示Y坐标  
  
    public void setX(T x) {  
        this.x = x;  
    }  
  
    public void setY(T y) {  
        this.y = y;  
    }  
  
    public T getX() {  
        return this.x;  
    }  
  
    public T getY() {  
        return this.y;  
    }  
}  
  
//使用  
Point<integer> p1 = new Point<integer>();  
p1.setX(new Integer(100));  
System.out.println(p1.getX());</integer></integer></t>
首先,我们要知道一点,任何的泛型变量(比如这里的T)都是派生自Object,所以我们在填充泛型变量时,只能使用派生自Object的类,比如String,Integer,Double,等而不能使用原始的变量类型,比如int,double,float等。然后,问题来了,那在泛型类Point内部,利用泛型定义的变量T x能调用哪些函数呢?
private T x; 
当然只能调用Object所具有的函数,因为编译器根本不知道T具体是什么类型,只有在运行时,用户给什么类型,他才知道是什么类型。编译器唯一能确定的是,无论什么类型,都是派生自Object的,所以T肯定是Object的子类,所以T是可以调用Object的方法的。那么问题又来了,如果我想写一个找到最小值的泛型类;由于不知道用户会传什么类型,所以要写一个接口,让用户实现这个接口来自已对比他所传递的类型的大小。接口如下:
public interface Comparable<t>{  
    public boolean compareTo(T i);  
}  
</t>
但如果我们直接利用T的实例来调用compareTo()函数的话,会报错,编译器截图如下:

这是因为,编译器根本无法得知T是继承自Comparable接口的函数。那怎么样才能让编译器知道,T是继承了Comparable接口的类型呢?这就是类型绑定的作用了。

2、类型绑定:extends

(1)、定义有时候,你会希望泛型类型只能是某一部分类型,比如操作数据的时候,你会希望是Number或其子类类型。这个想法其实就是给泛型参数添加一个界限。其定义形式为:
<t extends="" boundingtype="">  </t>
此定义表示T应该是BoundingType的子类型(subtype)。T和BoundingType可以是类,也可以是接口。另外注意的是,此处的”extends“表示的子类型,不等同于继承。一定要非常注意的是,这里的extends不是类继承里的那个extends!两个根本没有任何关联。在这里extends后的BoundingType可以是类,也可以是接口,意思是说,T是在BoundingType基础上创建的,具有BoundingType的功能。目测是JAVA的开发人员不想再引入一个关键字,所以用已有的extends来代替而已。

(2)、实例:绑定接口

同样,我们还使用上面对比大小的接口来做例子首先,看加上extends限定后的min函数:
public interface Comparable<t> {  
    public boolean compareTo(T i);  
}  
//添加上extends Comparable之后,就可以Comparable里的函数了  
public static <t extends="" comparable="">  T min(T...a){  
    T smallest = a[0];  
    for(T item:a){  
        if (smallest.compareTo(item)){  
            smallest = item;  
        }  
    }  
    return smallest;  
}  </t></t>
这段代码的意思就是根据传进去的T类型数组a,然后调用其中item的compareTo()函数,跟每一项做对比,最终找到最小值。从这段代码也可以看出,类型绑定有两个作用:1、对填充的泛型加以限定 2、使用泛型变量T时,可以使用BoundingType内部的函数。这里有一点非常要注意的是,在这句中smallest.compareTo(item),smallest和item全部都是T类型的,也就是说,compareTo对比的是同一种类型。然后我们实现一个派生自Comparable接口的类:
public class StringCompare implements Comparable<stringcompare> {  
    private String mStr;  
  
    public StringCompare(String string){  
        this.mStr = string;  
    }  
  
    @Override  
    public  boolean compareTo(StringCompare str) {  
        if (mStr.length() > str.mStr.length()){  
            return true;  
        }  
        return false;  
    }  
}  </stringcompare>
在这段代码,大家可能会疑惑为什么把T也填充为StringCompare类型,记得我们上面说的吗:smallest.compareTo(item),smallest和item是同一类型!!所以compareTo的参数必须是与调用者自身是同一类型,所以要把T填充为StringCompare;在这段代码中compareTo的实现为,对比当前mstr的长度与传进来实例的mstr长度进行比较,如果超过,则返回true,否则返回false;最后是使用min函数:
StringCompare result = min(new  StringCompare("123"),new StringCompare("234"),new StringCompare("59897"));  
Log.d(TAG,"min:"+result.mStr);  
结果如下:

这里有extends接口,我们开篇说过,extends表示绑定,后面的BindingType即可以是接口,也可以是类,下面我们就再举个绑定类的例子。

源码在文章底部给出

(3)、实例:绑定类我们假设,我们有很多种类的水果,需要写一个函数,打印出填充进去水果的名字:为此,我们先建一个基类来设置和提取名字:

class Fruit {  
    private String name;  
  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
}  
然后写个泛型函数来提取名字:
public static <t extends="" fruit=""> String getFruitName(T t){  
    return t.getName();  
}  
</t>
这里泛型函数的用法就出来了,由于我们已知水果都会继承Fruit基类,所以我们利用就可以限定填充的变量必须派生自Fruit的子类。一来,在T中,我们就可以利用Fruit类中方法和函数;二来,如果用户填充进去的类没有派生自Fruit,那编译器就会报错。然后,我们新建两个类,派生自Fruit,并填充进去它们自己的名字:
class Banana extends Fruit{  
    public Banana(){  
        setName("bababa");  
    }  
}  
class Apple extends Fruit{  
    public Apple(){  
        setName("apple");  
    }  
}  
最后调用:
String name_1 = getFruitName(new Banana());  
String name_2 = getFruitName(new Apple());  
Log.d(TAG,name_1);  
Log.d(TAG,name_2); 
结果如下:

源码在文章底部给出

(4)、绑定多个限定上面我们讲了,有关绑定限定的用法,其实我们可以同时绑定多个绑定,用&连接,比如:
public static <t extends="" fruit&serializable=""> String getFruitName(T t){  
    return t.getName();  
}  </t>
再加深下难度,如果我们有多个泛型,每个泛型都带绑定,那应该是什么样子的呢:
public static <t extends="" comparable="" &="" serializable,="" u="" runnable=""> T foo(T a, U b){  
    …………  
}  
</t>
大家应该看得懂,稍微讲一下:这里有两个泛型变量T和U,将T与Comparable & Serializable绑定,将U与Runnable绑定。好了,这部分就讲完了,下面讲讲有关通配符的用法。通配符是一个非常令人头疼的一个功能,理解与掌握难度比较大,下面我尽力去讲明白它与泛型变量的区别与用法。
重新来看我们上篇用的Point泛型定义:
class Point<t> {  
    private T x;        
    private T y;        
      
    public Point(){  
          
    }  
    public Point(T x,T y){  
        this.x = x;  
        this.y = y;  
    }  
  
    public void setX(T x) {  
        this.x = x;  
    }  
  
    public void setY(T y) {  
        this.y = y;  
    }  
  
    public T getX() {  
        return this.x;  
    }  
  
    public T getY() {  
        return this.y;  
    }  
}  </t>
这段代码很简单,引入了一个泛型变量T,然后是有两个构造函数,最后分别是利用set和get方法来设置和获取x,y的值。这段代码没什么难度,不再细讲。我们看看下面这段使用的代码:
Point<integer> integerPoint = new Point<integer>(3,3);  
…………  
Point<float> floatPoint = new Point<float>(4.3f,4.3f);  
…………  
Point<double> doublePoint = new Point<double>(4.3d,4.90d);  
…………  
Point<long> longPoint = new Point<long>(12l,23l);  
…………  </long></long></double></double></float></float></integer></integer>
在这段代码中,我们使用Point生成了四个实例:integerPoint,floatPoint,doublePoint和longPoint;在这里,我们生成四个实例,就得想四个名字。如果我们想生成十个不同类型的实例呢?那不得想十个名字。光想名字就是个事,(其实我并不觉得想名字是个什么大事…… T _ T ,没办法,想不出更好的例子了…… )那有没有一种办法,生成一个变量,可以将不同类型的实例赋值给他呢?(1)、概述先不讲无边界通配符是什么,同样拿上面的例子来看,如果我们这样实现:
Point<?> point;  
  
point = new Point<integer>(3,3);  
point = new Point<float>(4.3f,4.3f);  
point = new Point<double>(4.3d,4.90d);  
point = new Point<long>(12l,23l);  </long></double></float></integer>
在这里,我们首先,利用下面的代码生成一个point实例,注意到,在填充泛型时,用的是?
Point<?> point;
然后,各种类型的Point实例,都可以赋值给point了:
point = new Point<integer>(3,3);  
point = new Point<float>(4.3f,4.3f);  
point = new Point<double>(4.3d,4.90d);  
point = new Point<long>(12l,23l);</long></double></float></integer>
这里的?就是无边界通配符。通配符的意义就是它是一个未知的符号,可以是代表任意的类。所以这里可能大家就明白了,这里不光能将泛型变量T填充为数值类型,其实任意Point实例都是可以传给point的:比如这里的Point(),Point()都是可以的


(2)、?与T的区别大家可能会有疑问,那无边界通配符?与泛型变量T有什么区别呢?答案是:他们俩没有任何联系!!!!!泛型变量T不能在代码用于创建变量,只能在类,接口,函数中声明以后,才能使用。比如:

public class Box<t> {  
   public T get(){  
       …………  
   };  
   public void put(T element){  
       …………  
   };  
}  </t>
而无边界通配符?则只能用于填充泛型变量T,表示通配任何类型!!!!再重复一遍:?只能用于填充泛型变量T。它是用来填充T的!!!!只是填充方式的一种!!!比如:
//无边界通配符填充  
Box<?> box;  
//其它类型填充  
Box<string> stringBox;  </string>
(3)、通配符只能用于填充泛型变量T,不能用于定义变量大家一定要记得,通配符的使用位置只有:
Box<?> box;  
box = new Box<string>();  </string>
即填充泛型变量T的位置,不能出现在后面String的位置!!!!下面的第三行,第四行,都是错误的。通配符不能用于定义变量。


再次强调,?只能出现在Box box;中,其它位置都是不对的。

3、通配符?的extends绑定

(1)、概述从上面我们可以知道通配符?可以代表任意类型,但跟泛型一样,如果不加以限定,在后期的使用中编译器可能不会报错。所以我们同样,要对?加以限定。 绑定的形式,同样是通过extends关键字,意义和使用方法都用泛型变量一致。 同样,以我们上面的Point泛型类为例,因为Point在实例意义中,其中的值是数值才有意义,所以将泛型变量T填充为Object类型、String类型等都是不正确的。 所以我们要对Point point加以限定:只有数值类型才能赋值给point; 我们把代码改成下面的方式:


我们给通配符加上限定: Point point;此时,最后两行,当将T填充为String和Object时,赋值给point就会报错!这里虽然是指派生自Number的任意类型,但大家注意到了没: new Point();也是可以成功赋值的,这说明包括边界自身。再重复一遍:无边界通配符只是泛型T的填充方式,给他加上限定,只是限定了赋值给它(比如这里的point)的实例类型。如果想从根本上解决乱填充Point的问题,需要从Point泛型类定义时加上:

class Point<t extends="" number=""> {  
    private T x;       // 表示X坐标  
    private T y;       // 表示Y坐标  
  
   …………  
}  </t>
(2)注意:利用定义的变量,只可取其中的值,不可修改看下面的代码:


明显在point.setX(Integer(122));时报编译错误。但point.getX()却不报错。这是为什么呢?首先,point的类型是由Point决定的,并不会因为point = new Point(3,3);而改变类型。即便point = new Point(3,3);之后,point的类型依然是Point,即派生自Number类的未知类型!!!这一点很好理解,如果在point = new Point(3,3);之后,point就变成了Point类型,那后面point = new Point(12l,23l);操作时,肯定会因为类型不匹配而报编译错误了,正因为,point的类型始终是Point,因此能继续被各种类型实例赋值。回到正题,现在说说为什么不能赋值正因为point的类型为 Point point,那也就是说,填充Point的泛型变量T的为,这是一个什么类型?未知类型!!!怎么可能能用一个未知类型来设置内部值!这完全是不合理的。但取值时,正由于泛型变量T被填充为,所以编译器能确定的是T肯定是Number的子类,编译器就会用Number来填充T

也就是说,编译器,只要能确定通配符类型,就会允许,如果无法确定通配符的类型,就会报错。


4、通配符?的super绑定

(1)、概述如果说 指填充为派生于XXX的任意子类的话,那么则表示填充为任意XXX的父类!我们先写三个类,Employee,Manager,CEO,分别代表工人,管理者,CEO其中Manager派生于Employee,CEO派生于Manager,代码如下:
class CEO extends Manager {  
}  
  
class Manager extends Employee {  
}  
  
class Employee {  
}  
然后,如果我这样生成一个变量:
 List<? super Manager> list;
它表示的意思是将泛型T填充为,即任意Manager的父类;也就是说任意将List中的泛型变量T填充为Manager父类的List变量,都可以赋值给list;

从上面的代码中可以看出new ArrayList(),new ArrayList()都是正确的,而new ArrayList()却报错,当然是因为CEO类已经不再是Manager的父类了。所以会报编译错误。这里还要注意一个地方,从代码中可以看出new ArrayList()是可以成功赋值给 List list的,可见,super关键字也是包括边界的。即边界类型(这里是Manager)组装的实例依然可以成功赋值。

(2)、super通配符实例内容:能存不能取

上面我们讲了,extends通配符,能取不能存,那super通配符情况又怎样呢?我们试试看:


先看存的部分:

List<? super Manager> list;  
list = new ArrayList<employee>();  
//存  
list.add(new Employee()); //编译错误  
list.add(new Manager());  
list.add(new CEO());  </employee>
首先,需要声明的是,与Point point中point的类型是由Point确定的,相同的是list的类型是也是由List ;list的item的类型始终是,即Manager类的任意父类,即可能是Employee或者Object.大家可能疑惑的地方在于,为什么下面这两个是正确的!而list.add(new Employee()); 却是错误的!
list.add(new Manager());  
list.add(new CEO());  
因为list里item的类型是,即Manager的任意父类,我们假如是Employee,那下面这段代码大家能理解了吧:
List<employee> list = new ArrayList<employee>();  
list.add(new Manager());  
list.add(new CEO()); </employee></employee>
在这里,正因为Manager和CEO都是Employee的子类,在传进去list.add()后,会被强制转换为Employee!现在回过头来看这个:
List<? super Manager> list;  
list = new ArrayList<employee>();  
//存  
list.add(new Employee()); //编译错误  
list.add(new Manager());  
list.add(new CEO());</employee>
编译器无法确定的具体类型,但唯一可以确定的是Manager()、CEO()肯定是的子类,所以肯定是可以add进去的。但Employee不一定是的子类,所以不能确定,不能确定的,肯定是不允许的,所以会报编译错误。最后再来看看取:


在这段代码中,Object object = list.get(0);是不报错的,而Employee employee = list.get(0);是报错的;我们知道list中item的类型为,那编译器能肯定的是肯定是Manger的父类;但不能确定,它是Object还是Employee类型。但无论是填充为Object还是Employee,它必然是Object的子类!所以Object object = list.get(0);是不报错的。因为 list.get(0);肯定是Object的子类;而编译器无法判断list.get(0)是不是Employee类型的,所以Employee employee = list.get(0);是报错的。这里虽然看起来是能取的,但取出来一个Object类型,是毫无意义的。所以我们认为super通配符:能存不能取;

5、通配符?总结

总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:

◆ 如果你想从一个数据类型里获取数据,使用 ? extends 通配符(能取不能存)◆ 如果你想把对象写入一个数据结构里,使用 ? super 通配符(能存不能取)

◆ 如果你既想存,又想取,那就别用通配符。


6、常见问题注意

(1)、Point与Point构造泛型实例的区别同样以Point泛型类为例:
class Point<t> {  
    private T x;       // 表示X坐标  
    private T y;       // 表示Y坐标  
  
    public Point(){  
  
    }  
    public Point(T x,T y){  
        this.x = x;  
        this.y = y;  
    }  
  
    public void setX(T x) {  
        this.x = x;  
    }  
  
    public void setY(T y) {  
        this.y = y;  
    }  
  
    public T getX() {  
        return this.x;  
    }  
  
    public T getY() {  
        return this.y;  
    }  
}  </t>
我们来看看下面这种构造Point泛型实例有什么区别:
//使用Point<?>  
Point<?> point1 = new Point(new Integer(23),new Integer(23));  
Point<?> point2 = new Point(new String(""),new String(""));  
//直接使用Point  
Point point3 = new Point(new Integer(23),new Integer(23));  
Point point4 = new Point(new String(""),new String("")); 
上面的四行代码中,point1,point2生成的是Point的实例,填充的是无边界通配符。而point3和point4则非常奇怪,没有了泛型的<>标识,直接使用Point生成的实例,那它填充的是什么呢?这四行代码在编译和运行时,都没有报错,而且输出结果也一样!那么问题就来了:
Point<?> point1 = new Point(new Integer(23),new Integer(23));  
Point<?> point2 = new Point(new String(""),new String(""));  
在上面的代码中,使用了无界通配符,所以能够将各种Point实例赋值给Pointpoint1而省略了泛型标识的构造方法,依然能将各种Point实例赋值给它:
Point point3 = new Point(new Integer(23),new Integer(23));  
Point point4 = new Point(new String(""),new String(""));  
这说明:构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!所以下面这两个是对等的:
Point point3 = new Point(new Integer(23),new Integer(23));  
Point<?> point3 = new Point(new Integer(23),new Integer(23));  
最后重复一遍:构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!好了,快累死了,这部分真是太难讲了,有关通配符捕获和编译器类型擦除的知识,就不讲了,在实际项目中基本用不到,有兴趣的同学可以自行去补充下。下篇给大家讲下反射。

如果本文有帮到你,记得加关注哦

本文涉及源码下载地址:http://download.csdn.net/detail/harvic880925/9275551

请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/49883589 谢谢

参考文章:

1、《 java 泛型编程(一)》
2、《Java泛型--泛型应用--泛型接口、泛型方法、泛型数组、泛型嵌套》
3、《Java泛型编程最全总结》
4、《java 通配符解惑》
5、《《Java编程思想》学习笔记8——泛型编程高级》
6、《步步理解 JAVA 泛型编程(三)》
7、《Java-泛型编程-类型擦除(Type Erasure)》
8、《Java泛型--泛型入门》
9、《Java泛型--通配符》
10、《在 Java 的泛型类型中使用通配符》
11、《Java 理论与实践: 使用通配符简化泛型使用》
12、《Java 泛型学习三 通配符》
13、《Java泛型-- 通配符(转载)》

Java

作者:qianhaohong 发表于2016/11/27 19:30:29 原文链接
阅读:46 评论:0 查看评论

Android程序员必须掌握的知识点-多进程和多线程

$
0
0

当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。 如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。 但是,您可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。

本文介绍进程和线程在 Android 应用中的工作方式。

进程

默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。

各类组件元素的清单文件条目—activity、service、receiver 和 provider—均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。 此外,您还可以设置 android:process,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。

此外,application 元素还支持 android:process 属性,以设置适用于所有组件的默认值。

如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件需要再次运行时,系统将为它们重启进程。

决定终止哪个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,它更有可能关闭托管屏幕上不再可见的 Activity 的进程。 因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。 下面,我们介绍决定终止进程所用的规则。

进程生命周期

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。

重要性层次结构一共有 5 级。

以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

前台进程

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:
托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
托管某个 Service,后者绑定到用户正在交互的 Activity
托管正在“前台”运行的 Service(服务已调用 startForeground())
托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
托管正执行其 onReceive() 方法的 BroadcastReceiver
通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:
托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
托管绑定到可见(或前台)Activity 的 Service。
可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

服务进程

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 有关保存和恢复状态的信息,请参阅 Activity文档。

空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
根据进程中当前活动组件的重要程度,Android 会将进程评定为它可能达到的最高级别。例如,如果某进程托管着服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进程其级别永远不会低于其所服务的进程。 例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

由于运行服务的进程其级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备“服务进程”优先级。 同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

线程

应用启动时,系统会为应用创建一个名为“主线程”的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面小部件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 android.widget 和 android.view 软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。

系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行。

例如,当用户触摸屏幕上的按钮时,应用的 UI 线程会将触摸事件分派给小部件,而小部件反过来又设置其按下状态,并将失效请求发布到事件队列中。 UI 线程从队列中取消该请求并通知小部件应该重绘自身。

在应用执行繁重的任务以响应用户交互时,除非正确实现应用,否则这种单线程模式可能会导致性能低下。 具体地讲,如果 UI 线程需要处理所有任务,则执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。 一旦线程被阻塞,将无法分派任何事件,包括绘图事件。 从用户的角度来看,应用显示为挂起。 更糟糕的是,如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个让人厌烦的“应用无响应”(ANR) 对话框。如果引起用户不满,他们可能就会决定退出并卸载此应用。

此外,Android UI 工具包并非线程安全工具包。因此,您不得通过工作线程操纵 UI,而只能通过 UI 线程操纵用户界面。 因此,Android 的单线程模式必须遵守两条规则:

不要阻塞 UI 线程

不要在 UI 线程之外访问 Android UI 工具包

工作线程

根据上述单线程模式,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。 如果执行的操作不能很快完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。

例如,以下代码演示了一个点击侦听器从单独的线程下载图像并将其显示在 ImageView 中:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

乍看起来,这段代码似乎运行良好,因为它创建了一个新线程来处理网络操作。 但是,它违反了单线程模式的第二条规则:不要在 UI 线程之外访问 Android UI 工具包 — 此示例从工作线程(而不是 UI 线程)修改了 ImageView。 这可能导致出现不明确、不可预见的行为,但要跟踪此行为困难而又费时。

为解决此问题,Android 提供了几种途径来从其他线程访问 UI 线程。

以下列出了几种有用的方法:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)

例如,您可以通过使用 View.post(Runnable) 方法修复上述代码:

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            final Bitmap bitmap =
                    loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable() {
                public void run() {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

现在,上述实现属于线程安全型:在单独的线程中完成网络操作,而在 UI 线程中操纵 ImageView。

但是,随着操作日趋复杂,这类代码也会变得复杂且难以维护。 要通过工作线程处理更复杂的交互,可以考虑在工作线程中使用 Handler 处理来自 UI 线程的消息。当然,最好的解决方案或许是扩展 AsyncTask 类,此类简化了与 UI 进行交互所需执行的工作线程任务。

使用 AsyncTask

AsyncTask 允许对用户界面执行异步操作。 它会先阻塞工作线程中的操作,然后在 UI 线程中发布结果,而无需您亲自处理线程和/或处理程序。

要使用它,必须创建 AsyncTask 的子类并实现 doInBackground() 回调方法,该方法将在后台线程池中运行。 要更新 UI,应该实现 onPostExecute() 以传递 doInBackground() 返回的结果并在 UI 线程中运行,以便您安全地更新 UI。 稍后,您可以通过从 UI 线程调用 execute() 来运行任务。

例如,您可以通过以下方式使用 AsyncTask 来实现上述示例:

public void onClick(View v) {
    new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    /** The system calls this to perform work in a worker thread and
      * delivers it the parameters given to AsyncTask.execute() */
    protected Bitmap doInBackground(String... urls) {
        return loadImageFromNetwork(urls[0]);
    }

    /** The system calls this to perform work in the UI thread and delivers
      * the result from doInBackground() */
    protected void onPostExecute(Bitmap result) {
        mImageView.setImageBitmap(result);
    }
}

现在 UI 是安全的,代码也得到简化,因为任务分解成了两部分:一部分应在工作线程内完成,另一部分应在 UI 线程内完成。

下面简要概述了 AsyncTask 的工作方法,但要全面了解如何使用此类,您应阅读 AsyncTask 参考文档:

可以使用泛型指定参数类型、进度值和任务最终值
方法 doInBackground() 会在工作线程上自动执行
onPreExecute()、onPostExecute() 和 onProgressUpdate() 均在 UI 线程中调用
doInBackground() 返回的值将发送到 onPostExecute()
您可以随时在 doInBackground() 中调用publishProgress(),以在 UI 线程中执行 onProgressUpdate()
您可以随时取消任何线程中的任务
注意:使用工作线程时可能会遇到另一个问题,即:运行时配置变更(例如,用户更改了屏幕方向)导致 Activity 意外重启,这可能会销毁工作线程。 要了解如何在这种重启情况下坚持执行任务,以及如何在 Activity 被销毁时正确地取消任务,请参阅书架示例应用的源代码。

线程安全方法

在某些情况下,您实现的方法可能会从多个线程调用,因此编写这些方法时必须确保其满足线程安全的要求。

这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果对 IBinder 中所实现方法的调用源自运行 IBinder 的同一进程,则该方法在调用方的线程中执行。但是,如果调用源自其他进程,则该方法将在从线程池选择的某个线程中执行(而不是在进程的 UI 线程中执行),线程池由系统在与 IBinder 相同的进程中维护。 例如,即使服务的 onBind() 方法将从服务进程的 UI 线程调用,在 onBind() 返回的对象中实现的方法(例如,实现 RPC 方法的子类)仍会从线程池中的线程调用。 由于一个服务可以有多个客户端,因此可能会有多个池线程在同一时间使用同一 IBinder 方法。因此,IBinder 方法必须实现为线程安全方法。

同样,内容提供程序也可接收来自其他进程的数据请求。尽管 ContentResolver 和 ContentProvider 类隐藏了如何管理进程间通信的细节,但响应这些请求的 ContentProvider 方法(query()、insert()、delete()、update() 和 getType() 方法)将从内容提供程序所在进程的线程池中调用,而不是从进程的 UI 线程调用。 由于这些方法可能会同时从任意数量的线程调用,因此它们也必须实现为线程安全方法。

进程间通信

Android 利用远程过程调用 (RPC) 提供了一种进程间通信 (IPC) 机制,通过这种机制,由 Activity 或其他应用组件调用的方法将(在其他进程中)远程执行,而所有结果将返回给调用方。 这就要求把方法调用及其数据分解至操作系统可以识别的程度,并将其从本地进程和地址空间传输至远程进程和地址空间,然后在远程进程中重新组装并执行该调用。 然后,返回值将沿相反方向传输回来。 Android 提供了执行这些 IPC 事务所需的全部代码,因此您只需集中精力定义和实现 RPC 编程接口即可。

要执行 IPC,必须使用 bindService() 将应用绑定到服务上。

我的微信二维码如下,欢迎交流讨论

这里写图片描述

欢迎关注《IT面试题汇总》微信订阅号。每天推送经典面试题和面试心得技巧

微信订阅号二维码如下:

这里写图片描述

作者:u010321471 发表于2016/11/27 19:31:37 原文链接
阅读:126 评论:0 查看评论

Android模拟器Genymotion安装及使用教程详解

$
0
0

尊重版权,未经授权不得转载
本文出自:贾鹏辉的技术博客(http://www.devio.org)

一直以来都有不少朋友问我关于Android模拟器的事情,比如:那个Android模拟比较快,如何安装使用之类。今天我整理出来这篇教程,供我的朋友以及各位小伙伴们查阅借鉴。

Android的模拟器很多,但最原汁原味的还数Android SDK中的模拟器,毕竟是Google的亲儿子。之前Android SDK中的模拟器运行速度一直是大家吐槽的对象,但自从支持了x86架构,运行速度可谓是提高了不少。但Android SDK中的模拟器对使用者的要求太高,光配置这一项就已将不少小白拒之门外。那现在就跟大家分享一款即适合小白又适合开发者使用的一款比较快的Android模拟器Genymotion。

Genymotion安装及使用教程

安装Genymotion

因为Genymotion的运行需要依赖VirtualBox,因此安装Genymotion之前需要先安装VirtualBox。

下载VirtualBox

前往https://www.virtualbox.org/wiki/Downloads,根据需要下载相应的VirtualBox,并安装。

下载安装VitualBox

上图是VitualBox打开之后的界面,从上图可以看出已经有个一模拟器,这个模拟器是在Genymotion中创建的。

提示:启动上图中的模拟器正确方式是在Genymotion中启动,虽然,在VitualBox中也能将Genymotion创建的Android模拟器启动,但界面显示的很不友好,并且Genymotion的一些功能都无法使用。另外需要提醒大家的是,VitualBox安装好之后就不用管了,以后使用和配置模拟器都是在Genymotion中进行的。

下载Genymotion

下载Genymotion需要到Genymotion官网注册Genymotion,前往https://www.genymotion.com/,注册Genymotion并获取下载地址,将下载好的Genymotion安装即可。

Genymotion打开

将安装好的Genymotion打开,便会看到上图的窗口,上图中已经有两个创建好的的模拟器,这是因为在写这篇教程前,我已经创建了两个模拟器,第一次安装Genymotion的小伙伴,看到的界面是没有创建模拟器的。

Genymotion的使用

Genymotion安装好之后,接下来就到了创建和使用Android模拟器时候了。想必小伙伴已经迫不及待的想创建一个模拟器看一下运行效果了吧。

创建Android模拟器

在Genymotion中创建一个Android模拟器可谓是简单的不能再简单了。

Genymotion创建模拟器

如上图,单击Add按钮便会弹出一个右侧创建模拟器的窗口,在“Android Version”下拉框中选择你想创建的对应版本的模拟器,在“Device Model”下拉框中选择相应的设备,然后Genymotion会为你筛选出可用的设备,选择一个可用的设备然后按“Next”下一步便会弹出一个确认界面,确认界面会显示你创建的模拟器的一些配置,这些配置都是Genymotion默认为你创建好的,然后再次单击“Next”下一步,Genymotion便会将该模拟器对应的一些镜像下载下来并创建模拟器,创建完成便会看大如下界面:

Genymotion模拟器创建完成

最后,回到Genymotion主界面,便会在模拟器列表中看到我们刚才创建的模拟器。

启动模拟器

选择对于的模拟器,单击“Start”启动模拟器。

启动模拟器

Genymotion模拟器安装第三方应用

在Genymotion模拟器中安装应用也是相当的简单,只需要将应用的Android包下载好,将xxx.apk文件拖到模拟器的界面上即可完成安装,但你在使用操作中可能会遇到很多应用无法安装的问题,如图:

INSTALL_FAIL_CPU_ABI_INCOMPATIBLE

INSTALL_FAIL_CPU_ABI_INCOMPATIBLE.png

上图是Genymotion安装应用时的一个错误弹框,大致意思是说:Genymotion不能安装和运行ARM架构的APP。

提示:Genymotion模拟器,以及市场上大部分以快著称的Android模拟器基本上都是基于x86架构的,x86架构的模拟器是无法兼容ARM架构的APP的,但市场长大部分APP都是基于ARM架构,也就意味着x86模拟器无法兼容市场上大部分模拟器,这也是快的代价。

那有没有方法在x86模拟器上安装并运行ARM架构的APP呢?如果你用的是Android SDK中的模拟器,我会建议你直接创建一个ARM架构的模拟器即可,因为在Android SDK管理器中你可以下到ARM架构的Android镜像。

但在Genymotion中能够选择使用的镜像中都是基于x86,那么如果让Genymotion运行ARM架构的APP呢?

有心的小伙伴,可能从上图已经找到了答案,在上图报错的提示信息中Genymotion已经给到我们答案:安装一个ARM translation来让Genymotion支持ARM架构的APP。

ARM translation,是一个ARM转换包,也被翻译成ARM翻译器,它可以让x86架构的模拟器运行ARM架构的APP。

为了方便大家使用,我已将本教程中用到的ARM translation放到了网盘上,供大家下载使用。

Genymotion-ARM-Translation使用方法

  1. 将下载好的Genymotion-ARM-Translation.zip拖到Genymotion模拟器界面,进行安装。
  2. 安装好后,重启模拟器即可。

安装Genymotion-ARM-Translation.png

安装过程中,会弹出上图,单击”OK”,确认即可。

安装Genymotion-ARM-Translation成功.png
安装成功后会看到上图弹框,此时重启模拟器,便可以安装ARM架构的APP了。

将下载好APP拖到模拟器界面,便可以轻松安装。

Genymotion安装APK

心得:至此为止,我们既可体验Genymotion的快,又可以安装使用ARM架构的APP了。

Genymotion启用adb(使用Genymotion模拟器开发调试Android应用)

这一部分是写给做开发的小伙们的,如果你不做开发使用Genymotion可以跳过这一部分。

默认情况下,在Genymotion模拟器启动的状态下,会导致Android adb无法使用。最明显的特征是,在AndroidStuio中运行一个项目的时候,无法找到已连接的Android设备,也看不到可用的虚拟设备。

如图:

选择模拟器.png

这是因为Genymotion在默认情况下,使用的是“Genymotion Android tools”,所以会导致上述问题,如图:

Genymotion ADB.png

最简单的解决办法是关闭Genymotion模拟器,便可以让Android adb恢复正常。

选择模拟器-2.png

那么,如何使用Genymotion模拟器进行开发调试Android项目呢?

使用Genymotion模拟器进行开发调试Android项目(Genymotion启用adb)

为了在开发调试Android应用的时候能够使用Genymotion模拟器,我们需要改变Genymotion模拟器adb配置,如图:

Genymotion启用ADB-2.png

在Genymotion主界面,单击Settings按钮,打开Genymotion的设置页面,将ADB tool改为“Use custom Android SDK tools”,同时将Android SDK路径指向你电脑上的Android SDK路径即可。配置好之后关闭该对话框,再次运行Android项目你会在已连接的项目中看到,Genymotion中的模拟器这一项如图:

Genymotion启用ADB.png

Genymotion安装及使用过程中遇到的问题及解决办法

下面这部分,是我在安装及使用Genymotion过程中遇到的一些问题及解决办法,整理出来分享给大家。

Unable to start the viutual device

Unable to start the viutual device.png

上述这个问题通常是由于Virtual Box安装不正确导致的。

解决办法

第一步:重装Virtual Box

卸载Virtual Box,前往https://www.virtualbox.org/wiki/Downloads,根据需要下载相应的VirtualBox,在次安装即可。

第二步:打开重装后的Virtual Box,在模拟器列表选择一个模拟器单击启动按钮启动它,如果能正常启动,说明这个问题已解决,关闭模拟器,在Genymotion中启动模拟器即可。

Error In supR3HardenedWinReSpawn

Error In supR3HardenedWinReSpawn.png

如果在Virtual Box中启动模拟器出现上图的问题,通常是因为你所安装的Virtual Box版本和你的电脑不兼容导致的。

解决办法

卸载Virtual Box,前往https://www.virtualbox.org/wiki/Downloads下载一个稍低版本的Virtual Box重装便可解决这个问题。

最后

既然来了,留下个喜欢再走吧,鼓励我继续创作(^_^)∠※

如果喜欢我的文章,那就关注我的博客@ devio.org吧,让我们一起做朋友~~

戳这里,加关注哦:

微博:第一时间获取推送
个人博客:干货文章都在这里哦
GitHub:我的开源项目

作者:fengyuzhengfan 发表于2016/11/27 19:43:25 原文链接
阅读:43 评论:0 查看评论

[Unity3D]ResourceManager类及其编辑器扩展

$
0
0

为了方便程序中动态加载resources目录中物体,编写了一个ResourceManager的单例类,使用时需要将其附加到一个游戏物体上。

可以实现对物体的分类、重命名、异步加载。

可用方法如下:

//通过名字和类型加载,类型为0时,不判断类型
ResourceRes<T> Load<T>(string name, ResourceType type = 0) where T : UnityEngine.Object

//通过名字和类型加载所有,类型为0时,不判断类型
T[] LoadAll<T>(string name, ResourceType type = 0) where T : UnityEngine.Object

//通过类型加载所有,可以用与的方式,例如加载两个类型:(ResourceType.BGM|ResourceType.Model)
ResourceRes<T>[] LoadAllFromTypeMask<T>(int typeMask) where T : UnityEngine.Object

//异步通过类型加载所有
public void LoadAllFromTypeMaskAsync<T>(int typeMask, Action<ResourceRes<T>[]> onComplete) where T : UnityEngine.Object

//异步通过名字和类型加载所有
void LoadAsync<T>(string name, Action<ResourceRes<T>> onComplete, ResourceType type = 0) where T : UnityEngine.Object

使用方式:

GameObject obj = ResourceManager.Instance.Load<GameObject>("Main").res;

ResourceManager.Instance.LoadAsync<GameObject>("Main", (res) =>
{
    if (res != null)
    {
        GameObject obj = res.res;
    }
});

这里写图片描述

源码如下:

using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;

#if UNITY_EDITOR
using UnityEditor;
using System.IO;
#endif

public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static T _Instance;

    public static T Instance
    {
        get
        {
            if (_Instance == null)
            {
                _Instance = FindObjectOfType<T>();
            }
            return _Instance;
        }
    }
}

//物体分类,一个物体可以是多种类型
public enum ResourceType
{
    BGM = 0x0001,
    Model = 0x0002,
    Scene = 0x0004,
    PromptAudio = 0x0008,
}

[System.Serializable]
public class ResourceItem
{
    public string name;//资源名字
    public string path;//资源resources路径
    public ResourceType type;//资源类型

    public ResourceItem(string name, string path, ResourceType type)
    {
        this.name = name;
        this.path = path;
        this.type = type;
    }
}

[Serializable]
public class ResourceRes<T> where T : UnityEngine.Object
{
    public string name;//资源名字
    public T res;//资源

    public ResourceRes(string name, T res)
    {
        this.name = name;
        this.res = res;
    }
}


[Serializable]
public class ResourceList
{
    public List<ResourceItem> value = new List<ResourceItem>();
}


public class ResourceManager : Singleton<ResourceManager>
{
    /// <summary>
    /// 资源配置文件
    /// </summary>
    private string _configFilePath = "configs/resourcemanager";

    public ResourceList _resLst;

    public void Awake()
    {
        LoadConfig();
    }

    private void LoadConfig()
    {
        TextAsset config = Resources.Load<TextAsset>(_configFilePath);
        if (null == config)
        {
            Debug.LogError("LoadConfig config is null");
            return;
        }
        if (string.IsNullOrEmpty(config.text))
        {
            Debug.LogError("LoadConfig config.text is null or empty.");
            return;
        }

        _resLst = JsonUtility.FromJson<ResourceList>(config.text);
        Debug.Assert(null != _resLst, "LoadConfig _audioResLst is null.");
    }

    private ResourceItem GetItemFromName(ResourceList resLst, string name)
    {
        ResourceItem item = resLst.value.Find((rt) =>
        {
            return rt.name == name;
        });

        return item;
    }

    private ResourceItem GetItemFromNameType(ResourceList resLst, string name, ResourceType type)
    {
        ResourceItem item = resLst.value.Find((rt) =>
        {
            return (rt.name == name) && (rt.type == type);
        });

        return item;
    }

    private List<ResourceItem> GetAllResourceItemFromTypeMask(ResourceList resLst, int typeMask)
    {
        return resLst.value.FindAll((t) =>
        {
            return (((int)t.type & typeMask) != 0);
        });
    }

    public ResourceRes<T> Load<T>(string name, ResourceType type = 0) where T : UnityEngine.Object
    {
        ResourceItem item = null;
        if (0 == type)
        {
            item = GetItemFromName(_resLst, name);
        }
        else
        {
            item = GetItemFromNameType(_resLst, name, type);
        }

        if (null == item)
        {
            return null;
        }

        T t = Resources.Load<T>(item.path);
        Debug.Assert(t != null, "srcpath:" + item.path);
        return new ResourceRes<T>(name, t);
    }

    public void LoadAsync<T>(string name, Action<ResourceRes<T>> onComplete, ResourceType type = 0) where T : UnityEngine.Object
    {
        StartCoroutine(AsyncLoading<T>(_resLst, name, onComplete, type));
    }

    public T[] LoadAll<T>(string name, ResourceType type = 0) where T : UnityEngine.Object
    {
        ResourceItem item = null;
        if (0 == type)
        {
            item = GetItemFromName(_resLst, name);
        }
        else
        {
            item = GetItemFromNameType(_resLst, name, type);
        }

        if (null == item)
        {
            return null;
        }

        T[] t = Resources.LoadAll<T>(item.path);
        Debug.Assert(t != null, "srcpath:" + item.path);
        return t;
    }

    public ResourceRes<T>[] LoadAllFromTypeMask<T>(int typeMask) where T : UnityEngine.Object
    {
        ResourceRes<T>[] obj = null;
        List<ResourceItem> resItemLst = GetAllResourceItemFromTypeMask(_resLst, typeMask);
        Debug.Assert(null != resItemLst, "resItemLst is Null");
        if (null != resItemLst)
        {
            List<ResourceRes<T>> tempLst = new List<ResourceRes<T>>();
            T tempObj = null;
            foreach (ResourceItem item in resItemLst)
            {
                tempObj = Resources.Load<T>(item.path);
                Debug.Assert(null != tempObj, "tempObj is Null");
                if (null != tempObj)
                {
                    tempLst.Add(new ResourceRes<T>(item.name, tempObj));
                }
            }

            obj = tempLst.ToArray();
        }

        return obj;
    }

    public void LoadAllFromTypeMaskAsync<T>(int typeMask, Action<ResourceRes<T>[]> onComplete) where T : UnityEngine.Object
    {
        StartCoroutine(AsyncLoadingAllFromTypeMask<T>(_resLst, typeMask, onComplete));
    }

    private IEnumerator AsyncLoadingAllFromTypeMask<T>(ResourceList resLst, int typeMask, Action<ResourceRes<T>[]> onComplete) where T : UnityEngine.Object
    {
        List<ResourceItem> resItemLst = GetAllResourceItemFromTypeMask(resLst, typeMask);
        yield return AsyncLoadingAllFromItems<T>(resItemLst.ToArray(), onComplete);
    }

    private IEnumerator AsyncLoadingAllFromItems<T>(ResourceItem[] resLst, Action<ResourceRes<T>[]> onComplete) where T : UnityEngine.Object
    {
        ResourceRes<T>[] obj = null;
        Debug.Assert(null != resLst, "itemLst is Null");
        bool nextStep = false;
        if (null != resLst)
        {
            List<ResourceRes<T>> tempLst = new List<ResourceRes<T>>();
            foreach (ResourceItem item in resLst)
            {
                nextStep = false;
                while (!nextStep)
                {
                    yield return AsyncLoading<T>(item, (t) =>
                    {
                        if (null != t)
                        {
                            tempLst.Add(t);
                        }
                        nextStep = true;
                    });
                }
            }
            obj = tempLst.ToArray();
        }

        if (null != onComplete)
        {
            onComplete(obj);
        }
    }

    private IEnumerator AsyncLoading<T>(ResourceItem item, Action<ResourceRes<T>> onComplete) where T : UnityEngine.Object
    {
        T obj = null;
        Debug.Assert(null != item, "item is Null");
        if (!string.IsNullOrEmpty(item.path))
        {
            ResourceRequest req = Resources.LoadAsync<T>(item.path);
            Debug.Assert(req != null, "srcpath:" + item.path);
            if (req != null)
            {
                while (!req.isDone)
                {
                    yield return null;
                }

                obj = req.asset as T;
            }
        }
        else
        {
            Debug.Log("srcpath is Null or Empty");
        }

        if (null != onComplete)
        {
            onComplete(new ResourceRes<T>(item.name, obj));
        }

        yield return null;
    }

    private IEnumerator AsyncLoading<T>(ResourceList resLst, string name, Action<ResourceRes<T>> onComplete, ResourceType type = 0) where T : UnityEngine.Object
    {
        ResourceItem item = null;

        if (0 == type)
        {
            item = GetItemFromName(resLst, name);
        }
        else
        {
            item = GetItemFromNameType(resLst, name, type);
        }

        yield return AsyncLoading(item, onComplete);
    }
}

#if UNITY_EDITOR
//编辑器扩展

[CustomEditor(typeof(ResourceManager))]
[CanEditMultipleObjects()]
public class ResourceManagerEdit : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Edit Config"))
        {
            ResourceManagerWindow.ShowWindow();
        }
    }
}

public class ResourceManagerWindow : EditorWindow
{
    [MenuItem("Custom/Resource Mgr")]
    public static void ShowWindow()
    {
        ResourceManagerWindow window = GetWindow<ResourceManagerWindow>("Resource Mgr");
        window.Show();
    }

    [Serializable]
    public class ResourceManagerConfig
    {
        public string configPath = "resources/configs/resourcemanager";
    }

    string editConfigPath = "";
    ResourceManagerConfig editConfig = null;
    ResourceList _lst = null;

    public void OnEnable()
    {
        editConfigPath = Path.Combine(Application.dataPath, "../ProjectSettings/ResourceManagerConfig.json");
        if (File.Exists(editConfigPath))
        {
            string editConfigMsg = File.ReadAllText(editConfigPath);
            editConfig = JsonUtility.FromJson<ResourceManagerConfig>(editConfigMsg);
        }
        if (null == editConfig)
        {
            editConfig = new ResourceManagerConfig();
        }

        Load();
    }

    public void OnDisable()
    {
        Save();

        string editConfigMsg = JsonUtility.ToJson(editConfig);
        File.WriteAllText(editConfigPath, editConfigMsg);
    }

    void Load()
    {
        string path = Path.Combine(Application.dataPath, editConfig.configPath);
        if (File.Exists(path))
        {
            string configMsg = File.ReadAllText(path);
            _lst = JsonUtility.FromJson<ResourceList>(configMsg);
        }
        if (null == _lst)
        {
            _lst = new ResourceList();
        }
    }

    void Save()
    {
        if (!string.IsNullOrEmpty(editConfig.configPath))
        {
            string path = Path.Combine(Application.dataPath, editConfig.configPath);
            if (!Directory.Exists(Path.GetDirectoryName(path)))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(path));
            }

            string configMsg = JsonUtility.ToJson(_lst);
            File.WriteAllText(path, configMsg);

            if (File.Exists(path))
            {
                AssetDatabase.ImportAsset("Assets/" + editConfig.configPath, ImportAssetOptions.ForceSynchronousImport);
            }
        }
    }

    Vector2 _scrollPos;
    public void OnGUI()
    {
        GUILayout.Label("Root is Assets folder path.");
        GUILayout.BeginHorizontal();
        GUILayout.Label("Path:", GUILayout.MaxWidth(50));
        editConfig.configPath = GUILayout.TextField(editConfig.configPath);
        if (GUILayout.Button("Load", GUILayout.Width(40), GUILayout.Height(14)))
        {
            Load();
        }
        GUILayout.EndHorizontal();
        GUILayout.BeginVertical();

        GUILayout.BeginHorizontal();
        GUILayout.Label("Name:", GUILayout.Width(100));
        GUILayout.Label("Path:");
        GUILayout.Label("Type:", GUILayout.Width(100));
        GUILayout.EndHorizontal();

        _scrollPos = GUILayout.BeginScrollView(_scrollPos);
        for (int index = 0, cnt = _lst.value.Count; index < cnt; index++)
        {
            GUILayout.BeginHorizontal();
            ResourceItem item = _lst.value[index];

            item.name = GUILayout.TextField(item.name, GUILayout.Width(100));
            item.path = GUILayout.TextField(item.path);
            item.type = (ResourceType)EditorGUILayout.EnumMaskField(item.type, GUILayout.Width(100));
            if (GUILayout.Button("-", GUILayout.Width(20), GUILayout.Height(14)))
            {
                _lst.value.RemoveAt(index);
                --index;
                --cnt;

                continue;
            }

            GUILayout.EndHorizontal();
        }
        GUILayout.EndScrollView();

        if (GUILayout.Button("Add"))
        {
            _lst.value.Add(new ResourceItem("", "", 0));
        }
        GUILayout.EndVertical();
    }
}
#endif

这里写图片描述

作者:u012741077 发表于2016/11/27 19:46:31 原文链接
阅读:37 评论:0 查看评论

Android常用:手把手教你实现搜索框(含历史搜索记录)

$
0
0


前言

  • 像下图的搜索功能在Android开发中非常常见

搜索功能

  • 今天我将手把手教大家如何实现具备历史搜索记录的搜索框

目录

目录


1. 使用场景

在敲下代码前,理解用户的功能使用场景是非常重要的,这样有助于我们更好地去进行功能的实现,使用场景如下:

  • 用户需要进行某类事物的搜索(通过文字输入进行精确搜索)
  • 在搜索框输入时,通过显示搜索历史从而降低用户二次搜索的成本

简单来说,就是输入过字段会保存,当用户再次搜索该字段时,能快速帮助用户输入


2. 功能业务流程

搜索功能场景


3. 明确功能点

  • 功能1:关键字搜索
  • 功能2:实时显示历史搜索记录
  • 功能3:历史搜索记录保存
  • 功能4:将软键盘上的”回车按钮“改为”搜索按钮“

4. 涉及到的知识点

知识点

4.1 SQLite数据库的增删改查操作

  • 在数据库建立一个叫records的表用于存储搜索历史记录
    里面只有一列name来存储历史记录
        db.execSQL("create table records(id integer primary key autoincrement,name varchar(200))");
  • 通过插入和删除操作数据库里面的数据
  • 通过查询操作来显示数据库里面的数据到ListView上

    已搜索的关键字再次搜索不重复添加到数据库


注:对于SQLlite数据库的操作详情请看我写的文章:Android:SQLlite数据库操作最详细解析

4.2 ListView和ScrollView的嵌套冲突

  • 问题:ListView和ScrollView一起使用会有冲突,导致ListView显示不全
  • 解决方案:本人采用继承ListView并重写它的onMeasure()方法来解决冲突。

    1. 至于两者的滑动冲突则暂时不需要处理(默认拉动ScrollView),这样用户就会感觉里面的搜索历史项和清空搜索记录项是在ListView里面一样,符合设计。
    2. 更多解决方法请看:ListView和ScrollView的嵌套冲突解决方案
  • 具体代码如下:

//解决ListView和ScrollView的冲突
public class Search_Listview extends ListView {
    public Search_Listview(Context context) {
        super(context);
    }

    //通过复写其onMeasure方法、达到对ScrollView适配的效果
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}

4.3 监听软键盘回车按钮设置为搜索按钮

给搜索框EditText添加一个OnKeyListener监听器,重写里面的回车键,实现按下回车键搜索。

et_search.setOnKeyListener(new View.OnKeyListener() {// 输入完后按键盘上的搜索键
            // 修改回车键功能
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                   写入搜索操作和数据库插入搜索记录操作
}
        });

4.4 使用TextWatcher实时筛选

给搜索框EditText添加一个TextWatcher来检测EditText里面的每个字符变化,根据变化之后的内容查询数据库得到结果。

 et_search.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            //输入有变化调用该方法
            @Override
            public void afterTextChanged(Editable s) {       
            //设置查询数据库并显示在列表的操作    
            }
        });

5. 实例Demo

接下来,我将给出详细代码手把手教你实现具备历史搜索记录的搜索框

先下载Demo再进行阅读效果会更好:Carson的Search_Layout_Demo地址

5.1 目录结构

目录结构

5.2 具体实现如下:

MainActivity.java

  • 作用:显示搜索框
  • 具体代码如下:
package scut.carson_ho.search_layout;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

RccordSQLiteOpenHelper.java


  • 作用:继承自SQLiteOpenHelper数据库类的子类,用于创建、管理数据库和版本控制
  • 具体代码如下:

对于SQLlite数据库的操作详情请看我写的文章:Android:SQLlite数据库操作最详细解析

package scut.carson_ho.search_layout;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * Created by Carson_Ho on 16/11/15.
 */
//SQLiteOpenHelper子类用于打开数据库并进行对用户搜索历史记录进行增删减除的操作
public class RecordSQLiteOpenHelper extends SQLiteOpenHelper {

    private static String name = "temp.db";
    private static Integer version = 1;

    public RecordSQLiteOpenHelper(Context context) {
        super(context, name, null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        //打开数据库,建立了一个叫records的表,里面只有一列name来存储历史记录:
        db.execSQL("create table records(id integer primary key autoincrement,name varchar(200))");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }


}

Search_ListView.java

  • 作用:用于解决ListView和ScrollView的嵌套冲突
  • 具体代码如下:
package scut.carson_ho.search_layout;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListView;

/**
 * Created by Carson_Ho on 16/11/15.
 */
//解决ListView和ScrollView的冲突
public class Search_Listview extends ListView {
    public Search_Listview(Context context) {
        super(context);
    }

    public Search_Listview(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Search_Listview(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    //通过复写其onMeasure方法、达到对ScrollView适配的效果

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

}

Search_View.java

  • 作用:用于封装搜索框功能的所有操作(涵盖历史搜索记录的插入、删除、查询和显示)
  • 具体代码如下:
package scut.carson_ho.search_layout;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CursorAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.Toast;

/**
 * Created by Carson_Ho on 16/11/15.
 */
public class Search_View extends LinearLayout {

    private Context context;

    /*UI组件*/
    private TextView tv_clear;
    private EditText et_search;
    private TextView tv_tip;
    private ImageView iv_search;

    /*列表及其适配器*/
    private Search_Listview listView;
    private BaseAdapter adapter;

    /*数据库变量*/
    private RecordSQLiteOpenHelper helper ;
    private SQLiteDatabase db;


    /*三个构造函数*/
    //在构造函数里直接对搜索框进行初始化 - init()
    public Search_View(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public Search_View(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
    }

    public Search_View(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
    }


    /*初始化搜索框*/
    private void init(){

        //初始化UI组件
        initView();


        //实例化数据库SQLiteOpenHelper子类对象
        helper = new RecordSQLiteOpenHelper(context);

        // 第一次进入时查询所有的历史记录
        queryData("");

        //"清空搜索历史"按钮
        tv_clear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                //清空数据库
                deleteData();
                queryData("");
            }
        });

        //搜索框的文本变化实时监听
        et_search.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            //输入后调用该方法
            @Override
            public void afterTextChanged(Editable s) {

                if (s.toString().trim().length() == 0) {
                    //若搜索框为空,则模糊搜索空字符,即显示所有的搜索历史
                    tv_tip.setText("搜索历史");
                } else {
                    tv_tip.setText("搜索结果");
                }

                //每次输入后都查询数据库并显示
                //根据输入的值去模糊查询数据库中有没有数据
                String tempName = et_search.getText().toString();
                queryData(tempName);

            }
        });


        // 搜索框的键盘搜索键
        // 点击回调
        et_search.setOnKeyListener(new View.OnKeyListener() {// 输入完后按键盘上的搜索键


            // 修改回车键功能
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                    // 隐藏键盘,这里getCurrentFocus()需要传入Activity对象,如果实际不需要的话就不用隐藏键盘了,免得传入Activity对象,这里就先不实现了
//                    ((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(
//                            getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);

                    // 按完搜索键后将当前查询的关键字保存起来,如果该关键字已经存在就不执行保存
                    boolean hasData = hasData(et_search.getText().toString().trim());
                    if (!hasData) {
                        insertData(et_search.getText().toString().trim());

                        queryData("");
                    }
                    //根据输入的内容模糊查询商品,并跳转到另一个界面,这个需要根据需求实现
                    Toast.makeText(context, "点击搜索", Toast.LENGTH_SHORT).show();

                }
                return false;
            }
        });



        //列表监听
        //即当用户点击搜索历史里的字段后,会直接将结果当作搜索字段进行搜索
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

                //获取到用户点击列表里的文字,并自动填充到搜索框内
                TextView textView = (TextView) view.findViewById(android.R.id.text1);
                String name = textView.getText().toString();
                et_search.setText(name);
                Toast.makeText(context, name, Toast.LENGTH_SHORT).show();

            }
        });


        //点击搜索按钮后的事件
        iv_search.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                boolean hasData = hasData(et_search.getText().toString().trim());
                if (!hasData) {
                    insertData(et_search.getText().toString().trim());

                    //搜索后显示数据库里所有搜索历史是为了测试
                    queryData("");
                }
                //根据输入的内容模糊查询商品,并跳转到另一个界面,这个根据需求实现
                Toast.makeText(context, "clicked!", Toast.LENGTH_SHORT).show();
            }
        });




    }





    /**
     * 封装的函数
     */

    /*初始化组件*/
    private void initView(){
        LayoutInflater.from(context).inflate(R.layout.search_layout,this);
        et_search = (EditText) findViewById(R.id.et_search);
        tv_clear = (TextView) findViewById(R.id.tv_clear);
        tv_tip = (TextView) findViewById(R.id.tv_tip);
        listView = (Search_Listview) findViewById(R.id.listView);
        iv_search = (ImageView) findViewById(R.id.iv_search);
    }

    /*插入数据*/
    private void insertData(String tempName) {
        db = helper.getWritableDatabase();
        db.execSQL("insert into records(name) values('" + tempName + "')");
        db.close();
    }

    /*模糊查询数据 并显示在ListView列表上*/
    private void queryData(String tempName) {

        //模糊搜索
        Cursor cursor = helper.getReadableDatabase().rawQuery(
                "select id as _id,name from records where name like '%" + tempName + "%' order by id desc ", null);
        // 创建adapter适配器对象,装入模糊搜索的结果
        adapter = new SimpleCursorAdapter(context, android.R.layout.simple_list_item_1, cursor, new String[] { "name" },
                new int[] { android.R.id.text1 }, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        // 设置适配器
        listView.setAdapter(adapter);
        adapter.notifyDataSetChanged();
    }

    /*检查数据库中是否已经有该条记录*/
    private boolean hasData(String tempName) {
        //从Record这个表里找到name=tempName的id
        Cursor cursor = helper.getReadableDatabase().rawQuery(
                "select id as _id,name from records where name =?", new String[]{tempName});
        //判断是否有下一个
        return cursor.moveToNext();
    }

    /*清空数据*/
    private void deleteData() {
        db = helper.getWritableDatabase();
        db.execSQL("delete from records");
        db.close();
    }
}

search_layout.xml

  • 作用:搜索框的布局
  • 具体代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:background="#E54141"
        android:orientation="horizontal"
        android:paddingRight="16dp">

        <ImageView
            android:layout_width="45dp"
            android:layout_height="45dp"
            android:layout_gravity="center_vertical"
            android:padding="10dp"
            android:src="@drawable/back" />

        <EditText
            android:id="@+id/et_search"
            android:layout_width="0dp"
            android:layout_height="fill_parent"
            android:layout_weight="264"
            android:background="@null"
            android:drawablePadding="8dp"
            android:gravity="start|center_vertical"
            android:hint="输入查询的关键字"
            android:imeOptions="actionSearch"
            android:singleLine="true"
            android:textColor="@android:color/white"
            android:textSize="16sp" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/search"
            android:layout_gravity="center_vertical"
            android:id="@+id/iv_search"/>

    </LinearLayout>


    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:paddingLeft="20dp"
                >

                <TextView
                    android:id="@+id/tv_tip"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:gravity="left|center_vertical"
                    android:text="搜索历史" />

                <View
                    android:layout_width="match_parent"
                    android:layout_height="1dp"
                    android:background="#EEEEEE"/>

                <scut.carson_ho.search_layout.Search_Listview
                    android:id="@+id/listView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                </scut.carson_ho.search_layout.Search_Listview>


            </LinearLayout>

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#EEEEEE"/>

            <TextView
                android:id="@+id/tv_clear"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:background="#F6F6F6"
                android:gravity="center"
                android:text="清除搜索历史" />

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:layout_marginBottom="20dp"
                android:background="#EEEEEE"/>
        </LinearLayout>

    </ScrollView>
</LinearLayout>

activity_main.xml

  • 作用:显示搜索框
  • 具体代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.search_layout.MainActivity">

    <scut.carson_ho.search_layout.Search_View
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/search_layout"/>
</RelativeLayout>

5.3 测试结果

测试结果

5.4 Demo地址

Carson的Github地址:Search_Layout_Demo


6. 总结

  • 通过阅读本文,你已经全面了解Android中如何实现具备历史搜索记录的搜索框
  • 接下来会介绍继续介绍Android开发中的常用知识,有兴趣可以继续关注Carson_Ho的安卓开发笔记

请点赞!因为你们的赞同/鼓励是我写作的最大动力!

相关文章阅读
Android开发:最全面、最易懂的Android屏幕适配解决方案
Android开发:Handler异步通信机制全面解析(包含Looper、Message Queue)
Android开发:顶部Tab导航栏实现(TabLayout+ViewPager+Fragment)
Android开发:底部Tab菜单栏实现(FragmentTabHost+ViewPager)
Android开发:JSON简介及最全面解析方法!
Android开发:XML简介及DOM、SAX、PULL解析对比


欢迎关注Carson_Ho的简书!

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度

作者:carson_ho 发表于2016/11/27 20:25:37 原文链接
阅读:35 评论:0 查看评论

面试题

$
0
0

volatile

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。

fina

final可以修饰非抽象类、非抽象类成员方法和变量。
final类不能被继承,没有子类,final类中的方法默认是final的。
final方法不能被子类的方法覆盖,但可以被继承。
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
final不能用于修饰构造方法。
注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。
1、final类
final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
2、final方法
如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
使用final方法的原因有二:
第一、把方法锁定,防止任何继承类修改它的意义和实现。
第二、高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
3、final变量(常量)
用final修饰的成员变量表示常量,值一旦给定就无法改变!
final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
一旦给final变量初值后,值就不能再改变了。
另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。
4、final参数
当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。

static

static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块
被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。
static变量前可以有private修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用(当然也可以在非静态成员方法中使用–废话),但是不能在其他类中通过类名来直接引用,这一点很重要。实际上你需要搞明白,private是访问权限限定,static表示不要实例化就可以使用,这样就容易理解多了。static前面加上其它访问权限关键字的效果也以此类推。

    static修饰的成员变量和成员方法习惯上称为静态变量和静态方法,可以直接通过类名来访问,访问语法为:

类名.静态方法名(参数列表…)
类名.静态变量名
用static修饰的代码块表示静态代码块,当Java虚拟机(JVM)加载类时,就会执行该代码块
1、static变量
按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。
对于实例变量,每创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。

2、静态方法
静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!这个需要去理解,想明白其中的道理,不是记忆!!!
因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

3、static代码块
static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
4、static和final一块用表示什么
static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!
对于变量,表示一旦给值就不可修改,并且通过类名可以访问。
对于方法,表示不可覆盖,并且可以通过类名直接访问。

    特别要注意一个问题:
    对于被static和final修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayList、HashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,这一点在编程中用到很多。
    http://lavasoft.blog.51cto.com/62575/18771/

Java初始化顺序总结 - 静态变量、静态代码块、成员变量、构造函数

Java初始化顺序(转来的)
1在new B一个实例时首先要进行类的装载。(类只有在使用New调用创建的时候才会被java类装载器装入)
2,在装载类时,先装载父类A,再装载子类B
3,装载父类A后,完成静态动作(包括静态代码和变量,它们的级别是相同的,安装代码中出现的顺序初始化)
4,装载子类B后,完成静态动作
类装载完成,开始进行实例化
1,在实例化子类B时,先要实例化父类A
2,实例化父类A时,先成员实例化(非静态代码)
3,父类A的构造方法
4,子类B的成员实例化(非静态代码)
5,子类B的构造方法

先初始化父类的静态代码—>初始化子类的静态代码–>初始化父类的非静态代码—>初始化父类构造函数—>初始化子类非静态代码—>初始化子类构造函数
静态方法可以被继承
静态方法大家应该都比较熟悉,在这里主要谈一下静态方法在继承时的一些注意事项。

1、父类方法如果是静态方法,子类不能覆盖为非静态方法;

2、父类方法如果是非静态方法,子类不能覆盖为静态方法;

3、父类静态方法可以被覆盖,允许在子类中定义同名的静态方法,但是没有多态。

synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

抽象类

抽象类可以有构造方法
在使用抽象类时需要注意几点:

     1、抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。

     2、抽象方法必须由子类来进行重写。

     3、只要包含一个抽象方法的抽象类,该方法必须要定义成抽象类,不管是否还包含有其他方法。

     4、抽象类中可以包含具体的方法,当然也可以不包含抽象方法。

     5、子类中的抽象方法不能与父类的抽象方法同名。

     6、abstract不能与final并列修饰同一个类。

     7、abstract 不能与private、static、final或native并列修饰同一个方法。

javadoc

javadoc是Sun公司提供的一个技术,它从程序源代码中抽取类、方法、成员等注释形成一个和源代码配套的API帮助文档。也就是说,只要在编写程序时以一套特定的标签作注释,在程序编写完成后,通过Javadoc就可以同时形成程序的开发文档了。

float double 区别

单精度浮点数在机内占4个字节,用32位二进制描述。
双精度浮点数在机内占8个字节,用64位二进制描述。

浮点数在机内用指数型式表示,分解为:数符,尾数,指数符,指数四部分。
数符占1位二进制,表示数的正负。
指数符占1位二进制,表示指数的正负。
尾数表示浮点数有效数字,0.xxxxxxx,但不存开头的0和点
指数存指数的有效数字。

指数占多少位,尾数占多少位,由计算机系统决定。
可能是数符加尾数占24位,指数符加指数占8位 – float.
数符加尾数占48位,指数符加指数占16位 – double.

  1. float i=(float) (1/3.0f); i=0.33333334 8位
    1. double i= 1/3.0; i=0.3333333333333333 16位
  2. System.out.println(“i=”+(2.0-1.1)); i=0.8999999999999999 默认是double
  3. byte b = (byte)130; //结果为-126
    //System.out.println((byte)129);-127
    计算方法:
    数据130默认是int类型的十进制数据
    第一步十进制的130转换成二进制数据
    1 0 0 0 0 0 1 0
    第二步130是int类型是占4个字节的,所以在内存的表现形式是
    00000000 00000000 00000000 10000010
    做了截取后的结果为
    10000010
    通过观察最高位符号位是1,这是一个负数,因为在计算机中所有的数据都是以补码的形式出现的所以要算它的补码
    原码:1 0000010 原码变反码,最高位符号位不变,其他数值位1变0,0变1得反码
    反码:1 1111101 反码变补码就是反码加1得
    补码;1 1111110
    最终的看到的结果就是
    1 1111110最高为是符号位不运算,数值为转化为十进制是64+32+16+8+4+2=126符号位负
    所以为-126

strictfp用法

strictfp 的意思是FP-strict,也就是说精确浮点的意思。
strictfp 关键字可应用于类、接口或方法。使用 strictfp 关键字声明一个方法时,该方法中所有的float和double表达式都严格遵守FP-strict的限制,符合IEEE-754规范。当对一个类或接口使用 strictfp 关键字时,该类中的所有代码,包括嵌套类型中的初始设定值和代码,都将严格地进行计算。严格约束意味着所有表达式的结果都必须是 IEEE 754 算法对操作数预期的结果,以单精度和双精度格式表示。
url:http://www.cnblogs.com/iwinson/p/6075362.html
url:http://www.233.com/Java/zhuangye/20071014/103440980.html

静态导入

静态导入的语法是:

  import static 包名.类名.静态成员变量;

  import static 包名.类名.静态成员函数;
  **缺点
  过度地使用静态导入会在一定程度上降低代码的可读性。**
  http://www.cnblogs.com/mengdd/archive/2013/01/23/2873312.html

BigDecimal用法

在使用BigDecimal类来进行计算的时候,主要分为以下步骤:

          1、用float或者double变量构建BigDecimal对象。

         2、通过调用BigDecimal的加,减,乘,除等相应的方法进行算术运算。

         3、把BigDecimal对象转换成float,double,int等类型。
         一般来说,可以使用BigDecimal的构造方法或者静态方法的valueOf()方法把基本类型的变量构建成BigDecimal对象。

1 BigDecimal b1 = new BigDecimal(Double.toString(0.48));
2 BigDecimal b2 = BigDecimal.valueOf(0.48);

    对于常用的加,减,乘,除,BigDecimal类提供了相应的成员方法。

1 public BigDecimal add(BigDecimal value); //加法
2 public BigDecimal subtract(BigDecimal value); //减法
3 public BigDecimal multiply(BigDecimal value); //乘法
4 public BigDecimal divide(BigDecimal value); //除法

作者:jueshihaidao 发表于2016/11/27 20:59:47 原文链接
阅读:26 评论:0 查看评论

Google官方推荐MVP基础示例学习总结

$
0
0

对于MVP架构,最近一段时间谷歌推出了官方的示例,包含多种不同的方式,先调了一个最基础的用来学习一下。Google官方示例的Git-Hub地址:Google官方示例地址,大家可以去参观学习。

本文参考自http://mp.weixin.qq.com/s?__biz=MzI0MjE3OTYwMg==&mid=2649547602&idx=1&sn=571db7e744c998ef4ca762ec6a1398d5&scene=21#wechat_redirect方便学习。

基础的MVP框架:

Google提供的最基础的MVP架构只是单纯的通过MVP的思想来组织代码,所有的注入都是通过手动注入的。
首先来看一下Google对于这个项目的简介:

框架描述

可以在图中很直观的看到,MVP的分层还是很明确的,通过Presenter来完成View和Model的交互。

总览:

现在来看一下项目结构:

项目结构

项目主要是根据功能模块来分包的,从上到下依次是:

  • 添加待完成任务模块
  • model模块:封装了一些数据的增删改查操作
  • 任务完成数据统计模块
  • 任务详情模块
  • 任务列表模块
  • 工具类模块
  • 两个最基础的Presenter和View接口

最基础的接口:

首先是最基础的两个接口,BasePresenter和BaseView:

BasePresenter:
定义了Presenter最基础的方法,调用该方法可以通知Presenter开始工作
,具体的方法会在具体的Presenter中定义
BasePresenter

BaseView:
定义了View的最基础的方法,提供了设置Presenter的方法,具体的方法会在具体的View中定义

BaseView

Model层实现:

因为Model层是和View层和Presenter层是分离开的,而且是最底层的一层,所以先看这层怎么实现:

Model层

这是Model层的结构,依次是:

  • 本地数据存取
  • 远程数据存取
  • 定义的数据源接口,主要定义了的数据操作的方法
  • 数据仓库,用于测试
  • 待做任务的实体类Task

首先是TaskDataSource这个接口:

在TaskDataSource中定义了数据存取的一些操作方法,对于数据存取的类都要实现这个接口中的方法。

/**
 * Main entry point for accessing tasks data.
 * <p>
 * For simplicity, only getTasks() and getTask() have callbacks. Consider adding callbacks to other
 * methods to inform the user of network/database errors or successful operations.
 * For example, when a new task is created, it's synchronously stored in cache but usually every
 * operation on database or network should be executed in a different thread.
 *
 * 封装了一些对于任务的数据操作方法,包括数据保存,查询,删除等方法
 */
public interface TasksDataSource {

    /**
     * 获取任务列表的回调接口,对应于获取多个任务的回调
     */
    interface LoadTasksCallback {

        void onTasksLoaded(List<Task> tasks);

        void onDataNotAvailable();
    }

    /**
     * 获取指定任务的回调接口,对应于获取单个任务的回调
     */
    interface GetTaskCallback {

        void onTaskLoaded(Task task);

        void onDataNotAvailable();
    }
    //获取任务列表
    void getTasks(@NonNull LoadTasksCallback callback);

    //根据任务的Id获取指定任务
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);

    //保存任务
    void saveTask(@NonNull Task task);

    //已完成的任务,根据实体来查询
    void completeTask(@NonNull Task task);

    //已完成的任务,根据任务Id来查询
    void completeTask(@NonNull String taskId);

    //正在活动的任务
    void activateTask(@NonNull Task task);

    //正在活动的任务,根据id来查询
    void activateTask(@NonNull String taskId);

    //清除所有的已完成的任务
    void clearCompletedTasks();

    //刷新任务列表
    void refreshTasks();

    //删除所有的任务
    void deleteAllTasks();

    //根据任务id删除指定任务
    void deleteTask(@NonNull String taskId);
}

上边代码中添加的注释已经很好的解释了该接口中定义的方法,基本已经满足了这个项目的需求。

具体的实现:

然后对于本地数据的存取和远程数据的存取,分别封装了以下的类:

具体实现

  • local代表本地数据存取:

    • TaskDbHelper继承自SQLiteOpenHelper,用于封装了一些数据库的操作

    • TaskLocalDataSource是具体实现了TasksDataSource接口的实现类,里边通过Sqlite数据库完成了数据的存取

    • TasksPersistenceContract是数据库的契约类,里边主要是包含一些数据库中用到的常量信息

  • remote代表远程数据存取:

    • TaskRemoteDataSource也是实现了TaskDataSource接口的实现类,主要是进行远程数据的加载。

    以TaskLocalDataSource为例来看一下里边的实现:

 //这是TaskLocalDataSource中保存Task的方法,其实就是通过DBHelper将数据持久化,其他的方法也类似

 @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        SQLiteDatabase db = mDbHelper.getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put(TaskEntry.COLUMN_NAME_ENTRY_ID, task.getId());
        values.put(TaskEntry.COLUMN_NAME_TITLE, task.getTitle());
        values.put(TaskEntry.COLUMN_NAME_DESCRIPTION, task.getDescription());
        values.put(TaskEntry.COLUMN_NAME_COMPLETED, task.isCompleted());

        db.insert(TaskEntry.TABLE_NAME, null, values);

        db.close();
    }

以上这些就是Model层的主要知识点,梳理一遍可以很清楚的明白流程:

  • 首先定义关于数据操作的接口(TaskDataSource)

  • 然后根据需要实现的数据存储方式来分别实现对应的数据存取实现类(TaskLocalDataSource和TaskRemoteDataSource),均继承自之前定义好的数据操作接口。

以上这些就是关于Model层的实现步骤和原理,更多细节的实现的可以阅读源码了解。


View层和Presenter层的实现:

以添加新的待做任务这个模块为例来看一下Google在推荐的MVP实现实例中是怎么处理View层和Presenter层的:

View层和Presenter层结构:

MVP中的对应关系:

Model:TaskLocalDataSource

View:AddEditTaskFragment

Presenter:AddEditTaskPresenter

其中两个之间都是通过契约类来进行连接的,也就是图中的AddEditTaskContract。在这里Google引入了一个新的叫做契约类的中间部分,包括对于某一具体模块中对于View和Presenter的方法的定义。我们可以以AddEditTaskContract为例来看一下这个契约类具体是用来干什么的。

package com.example.android.architecture.blueprints.todoapp.addedittask;

import com.example.android.architecture.blueprints.todoapp.BasePresenter;
import com.example.android.architecture.blueprints.todoapp.BaseView;

/**
 * This specifies the contract between the view and the presenter.
 * 添加任务的契约类
 *
 */
public interface AddEditTaskContract {

    interface View extends BaseView<Presenter> {

        //显示没有任务的错误信息提示
        void showEmptyTaskError();

        //显示任务列表
        void showTasksList();

        //设置标题
        void setTitle(String title);

        //设置描述信息
        void setDescription(String description);

        //是否处于活动
        boolean isActive();
    }

    interface Presenter extends BasePresenter {

        //保存任务
        void saveTask(String title, String description);

        //填充任务
        void populateTask();

        //是否数据缺失
        boolean isDataMissing();
    }
}

这里边主要内容是根据当前的模块来派生一个适合于当前模块的View和Presenter,在这里集中的显示了View和Presenter的功能,也是当前模块中具体View和具体Presenter定义的。

具体的View和Presenter的实现:

AddEditTaskFragment.java:

package com.example.android.architecture.blueprints.todoapp.addedittask;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.example.android.architecture.blueprints.todoapp.R;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Main UI for the add task screen. Users can enter a task title and description.
 * 主要添加任务的界面,用户可以输入任务的标题和描述
 */
public class AddEditTaskFragment extends Fragment implements AddEditTaskContract.View {

    public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";

    //在这个View中含有一个Presenter的引用
    private AddEditTaskContract.Presenter mPresenter;

    private TextView mTitle;

    private TextView mDescription;

    public static AddEditTaskFragment newInstance() {
        return new AddEditTaskFragment();
    }

    public AddEditTaskFragment() {
        // Required empty public constructor
    }

    @Override
    public void onResume() {
        super.onResume();
        //开启Presenter
        mPresenter.start();
    }

    @Override
    public void setPresenter(@NonNull AddEditTaskContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        FloatingActionButton fab =
                (FloatingActionButton) getActivity().findViewById(R.id.fab_edit_task_done);
        fab.setImageResource(R.drawable.ic_done);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //在View中通过Presenter间接的和Model进行交互,
                // 在这里就体现出所有的业务逻辑都通过Presenter来完成
                mPresenter.saveTask(mTitle.getText().toString(), mDescription.getText().toString());
            }
        });
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.addtask_frag, container, false);
        mTitle = (TextView) root.findViewById(R.id.add_task_title);
        mDescription = (TextView) root.findViewById(R.id.add_task_description);
        setHasOptionsMenu(true);
        return root;
    }

    @Override
    public void showEmptyTaskError() {
        Snackbar.make(mTitle, getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
    }

    @Override
    public void showTasksList() {
        getActivity().setResult(Activity.RESULT_OK);
        getActivity().finish();
    }

    @Override
    public void setTitle(String title) {
        mTitle.setText(title);
    }

    @Override
    public void setDescription(String description) {
        mDescription.setText(description);
    }

    @Override
    public boolean isActive() {
        return isAdded();
    }
}

上边就是示例项目中给出的View层的实现,可以体会一下其“只是进行数据的显示,并不进行业务逻辑的处理”这种想法。

接下来看一下Presenter层的实现示例:
AddEditTaskPresenter.java:

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {

    //有TasksDataSource的引用,连接 Model层
   @NonNull
    private final TasksDataSource mTasksRepository;

    //有AddEditTaskContract.View的应用,连接View层
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;

    @Nullable
    private String mTaskId;

    private boolean mIsDataMissing;

    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     * @param shouldLoadDataFromRepo whether data needs to be loaded or not (for config changes)
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mIsDataMissing = shouldLoadDataFromRepo;
    }

    @Override
    public void start() {
        if (!isNewTask() && mIsDataMissing) {
            populateTask();
        }
    }

    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }

    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }

    /**
     * TasksDataSource.GetTaskCallback接口的实现方法,用于加载完数据之后的回调
     */
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
        mIsDataMissing = false;
    }

    /**
     * TasksDataSource.GetTaskCallback接口的实现方法,用于数据无法获取到时候回调
     */
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }

    @Override
    public boolean isDataMissing() {
        return mIsDataMissing;
    }

    private boolean isNewTask() {
        return mTaskId == null;
    }

    /**
     * 创建新的Task任务
     * @param title
     * @param description
     */
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }

    /**
     * 更新Task任务
     * @param title
     * @param description
     */
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        //在Presenter中与
        mTasksRepository.saveTask(new Task(title, description, mTaskId));

        // After an edit, go back to the list.
        mAddTaskView.showTasksList();
    }
}

Presenter中主要是实现View和Model的交互,并且将两者隔离开来,实现了接口隔离。

最后就剩下AddEditTaskActivity这个类,其实其主要是做一个承载的作用,类似于一个容器:

AddEditTaskActivity.java:

/**
 * Displays an add or edit task screen.
 * 展示添加任务或者是修改任务的界面
 */
public class AddEditTaskActivity extends AppCompatActivity {

    public static final int REQUEST_ADD_TASK = 1;

    public static final String SHOULD_LOAD_DATA_FROM_REPO_KEY = "SHOULD_LOAD_DATA_FROM_REPO_KEY";

    private AddEditTaskPresenter mAddEditTaskPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);

        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);

        //初始化View
        AddEditTaskFragment addEditTaskFragment =
                (AddEditTaskFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);

        String taskId = getIntent().getStringExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID);

        if (addEditTaskFragment == null) {
            addEditTaskFragment = AddEditTaskFragment.newInstance();

            if (getIntent().hasExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID)) {
                actionBar.setTitle(R.string.edit_task);
                Bundle bundle = new Bundle();
                bundle.putString(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId);
                addEditTaskFragment.setArguments(bundle);
            } else {
                actionBar.setTitle(R.string.add_task);
            }

            //将Fragmment添加到Activity中
            ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                    addEditTaskFragment, R.id.contentFrame);
        }

        boolean shouldLoadDataFromRepo = true;

        // Prevent the presenter from loading data from the repository if this is a config change.
        if (savedInstanceState != null) {
            // Data might not have loaded when the config change happen, so we saved the state.
            shouldLoadDataFromRepo = savedInstanceState.getBoolean(SHOULD_LOAD_DATA_FROM_REPO_KEY);
        }

        // Create the presenter
        //实例化一个Presenter
        mAddEditTaskPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskFragment,
                shouldLoadDataFromRepo);

        //为View设置Presenter
        addEditTaskFragment.setPresenter(mAddEditTaskPresenter);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        // Save the state so that next time we know if we need to refresh data.
        outState.putBoolean(SHOULD_LOAD_DATA_FROM_REPO_KEY, mAddEditTaskPresenter.isDataMissing());
        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }

    @VisibleForTesting
    public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
    }
}

总结下来,感觉就是Activity像是一个舞台,承载着上边所有的元素进行表演,而Presenter这是提线木偶中提线的那个人,操控着任务的交互,View则是木偶的动作表演,而Model这是推动故事发展的剧情。

因为剧情的发展(Model的改变)导致提线人知道剧情发生变化(Presenter得知Model发生了变化)从而操作木偶(View发生变化)产生动作。

同样的,当木偶产生动作时(View发生变化),Presenter知道木偶发生了这个动作(Presenter知道View发生了变化)之后下一步剧情该怎么发展(Model怎么改变)。

链接

有兴趣的可以Git-Hub上看看Google官方给出的MVP实例的源码。

Git-Hub地址:https://github.com/googlesamples/android-architecture

作者:wqc_CSDN 发表于2016/11/27 21:37:18 原文链接
阅读:15 评论:0 查看评论

wifi连接流程

$
0
0

在平时的android开发中,经常会用到wifi相关操作,其实就应用而言,系统都是通过WifiManager对应的api来进行对应的操作
我们可以从源码的frameworks/base/api目录中看到当前系统提供的所有api

public class WifiManager {
    method public int addNetwork(android.net.wifi.WifiConfiguration);
    method public static int calculateSignalLevel(int, int);
    method public void cancelWps(android.net.wifi.WifiManager.WpsCallback);
    method public static int compareSignalLevel(int, int);
    method public android.net.wifi.WifiManager.MulticastLock createMulticastLock(java.lang.String);
    method public android.net.wifi.WifiManager.WifiLock createWifiLock(int, java.lang.String);
    method public android.net.wifi.WifiManager.WifiLock createWifiLock(java.lang.String);
    method public boolean disableNetwork(int);
    method public boolean disconnect();
    method public boolean enableNetwork(int, boolean);
    method public java.util.List<android.net.wifi.WifiConfiguration> getConfiguredNetworks();
    method public android.net.wifi.WifiInfo getConnectionInfo();
    method public android.net.DhcpInfo getDhcpInfo();
    method public java.util.List<android.net.wifi.ScanResult> getScanResults();
    method public int getWifiState();
    method public boolean is5GHzBandSupported();
    method public boolean isDeviceToApRttSupported();
    method public boolean isEnhancedPowerReportingSupported();
    method public boolean isP2pSupported();
    method public boolean isPreferredNetworkOffloadSupported();
    method public boolean isScanAlwaysAvailable();
    method public boolean isTdlsSupported();
    method public boolean isWifiEnabled();
    method public boolean pingSupplicant();
    method public boolean reassociate();
    method public boolean reconnect();
    method public boolean removeNetwork(int);
    method public boolean saveConfiguration();
    method public void setTdlsEnabled(java.net.InetAddress, boolean);
    method public void setTdlsEnabledWithMacAddress(java.lang.String, boolean);
    method public boolean setWifiEnabled(boolean);
    method public boolean startScan();
    method public void startWps(android.net.wifi.WpsInfo, android.net.wifi.WifiManager.WpsCallback);
    method public int updateNetwork(android.net.wifi.WifiConfiguration);
    field public static final java.lang.String ACTION_PICK_WIFI_NETWORK = "android.net.wifi.PICK_WIFI_NETWORK";
    field public static final java.lang.String ACTION_REQUEST_SCAN_ALWAYS_AVAILABLE = "android.net.wifi.action.REQUEST_SCAN_ALWAYS_AVAILABLE";
    field public static final int ERROR_AUTHENTICATING = 1; // 0x1
    field public static final java.lang.String EXTRA_BSSID = "bssid";
    field public static final java.lang.String EXTRA_NETWORK_INFO = "networkInfo";
    field public static final java.lang.String EXTRA_NEW_RSSI = "newRssi";
    field public static final java.lang.String EXTRA_NEW_STATE = "newState";
    field public static final java.lang.String EXTRA_PREVIOUS_WIFI_STATE = "previous_wifi_state";
    field public static final java.lang.String EXTRA_RESULTS_UPDATED = "resultsUpdated";
    field public static final java.lang.String EXTRA_SUPPLICANT_CONNECTED = "connected";
    field public static final java.lang.String EXTRA_SUPPLICANT_ERROR = "supplicantError";
    field public static final java.lang.String EXTRA_WIFI_INFO = "wifiInfo";
    field public static final java.lang.String EXTRA_WIFI_STATE = "wifi_state";
    field public static final java.lang.String NETWORK_IDS_CHANGED_ACTION = "android.net.wifi.NETWORK_IDS_CHANGED";
    field public static final java.lang.String NETWORK_STATE_CHANGED_ACTION = "android.net.wifi.STATE_CHANGE";
    field public static final java.lang.String RSSI_CHANGED_ACTION = "android.net.wifi.RSSI_CHANGED";
    field public static final java.lang.String SCAN_RESULTS_AVAILABLE_ACTION = "android.net.wifi.SCAN_RESULTS";
    field public static final java.lang.String SUPPLICANT_CONNECTION_CHANGE_ACTION = "android.net.wifi.supplicant.CONNECTION_CHANGE";
    field public static final java.lang.String SUPPLICANT_STATE_CHANGED_ACTION = "android.net.wifi.supplicant.STATE_CHANGE";
    field public static final int WIFI_MODE_FULL = 1; // 0x1
    field public static final int WIFI_MODE_FULL_HIGH_PERF = 3; // 0x3
    field public static final int WIFI_MODE_SCAN_ONLY = 2; // 0x2
    field public static final java.lang.String WIFI_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_STATE_CHANGED";
    field public static final int WIFI_STATE_DISABLED = 1; // 0x1
    field public static final int WIFI_STATE_DISABLING = 0; // 0x0
    field public static final int WIFI_STATE_ENABLED = 3; // 0x3
    field public static final int WIFI_STATE_ENABLING = 2; // 0x2
    field public static final int WIFI_STATE_UNKNOWN = 4; // 0x4
    field public static final int WPS_AUTH_FAILURE = 6; // 0x6
    field public static final int WPS_OVERLAP_ERROR = 3; // 0x3
    field public static final int WPS_TIMED_OUT = 7; // 0x7
    field public static final int WPS_TKIP_ONLY_PROHIBITED = 5; // 0x5
    field public static final int WPS_WEP_PROHIBITED = 4; // 0x4
  }

WifiManager和WifiServiceImpl远程通信

其实关于WifiManager操作基本都是通过远程服务端实现,这一点稍后讨论,WifiManager中的常用方法:

// 连接到指定网络
public void connect(int networkId, ActionListener listener) {
        if (networkId < 0) throw new IllegalArgumentException("Network id cannot be negative");
        validateChannel();
        sAsyncChannel.sendMessage(CONNECT_NETWORK, networkId, putListener(listener));
}

// 保存网络
public void save(WifiConfiguration config, ActionListener listener) {
        if (config == null) throw new IllegalArgumentException("config cannot be null");
        validateChannel();
        sAsyncChannel.sendMessage(SAVE_NETWORK, 0, putListener(listener), config);
}

// 忘记网络
public void forget(int netId, ActionListener listener) {
        if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
        validateChannel();
        sAsyncChannel.sendMessage(FORGET_NETWORK, netId, putListener(listener));
}

可以面这种操作网络的实际上都是通过sAsyncChannel中进一步实现的,sAsyncChannel是一个AsyncChannel类型,其实AsyncChannel是连接WifiManager和WifiServiceImpl的桥梁

AsyncChannel

在WifiManager构造方法中,调用了init()方法来初始化一些参数

private void init() {
        synchronized (sThreadRefLock) {
            if (++sThreadRefCount == 1) {
                // 获取服务端(WifiServiceImpl)中对应的mClientHandler,WifiServiceImpl继承自IWifiManager.Stub
                Messenger messenger = getWifiServiceMessenger();
                if (messenger == null) {
                    sAsyncChannel = null;
                    return;
                }

                sHandlerThread = new HandlerThread("WifiManager");
                sAsyncChannel = new AsyncChannel();
                // CountDownLatch是一个同步辅助类,犹如倒计时计数器,创建对象时通过构造方法设置初始值,调用CountDownLatch对象的await()方法则处于等待状态,调用countDown()方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
                sConnected = new CountDownLatch(1);

                sHandlerThread.start();
                Handler handler = new ServiceHandler(sHandlerThread.getLooper());
                // 1. 调用连接AsyncChannel.connect进行半连接
                sAsyncChannel.connect(mContext, handler, messenger);
                try {
                    sConnected.await();
                } catch (InterruptedException e) {
                    Log.e(TAG, "interrupted wait at init");
                }
            }
        }
}

上面代码主要做了两件事:
1.通过getWifiServiceMessenger()获取WifiServiceImpl中对应的mClientHandler
2.调用连接AsyncChannel.connect进行半连接
为什么第二步是半连接?分析代码吧

我们看下AsyncChannel#connect方法

public class AsyncChannel {
    ....
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // 连接srcHandler和dstMessenger
        connected(srcContext, srcHandler, dstMessenger);
        // 在replyHalfConnected中会发送CMD_CHANNEL_HALF_CONNECTED到srcHandler处理
        replyHalfConnected(STATUS_SUCCESSFUL);
}

public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        if (DBG) log("connected srcHandler to the dstMessenger  E");

        // Initialize source fields
        mSrcContext = srcContext;
        mSrcHandler = srcHandler;
        mSrcMessenger = new Messenger(mSrcHandler);

        // 这里的dstMessenger就是传递过来的WifiServiceImpl中对应的mClientHandler
        mDstMessenger = dstMessenger;

        if (DBG) log("connected srcHandler to the dstMessenger X");
}


private void replyHalfConnected(int status) {
        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);
        msg.arg1 = status;
        msg.obj = this;
        msg.replyTo = mDstMessenger;
        // 创建一个死亡监听,当当前mDstMessenger对应的binder死亡以后,会在DeathMonitor中发送一个"STATUS_REMOTE_DISCONNECTION"消息
        if (mConnection == null) {
            mDeathMonitor = new DeathMonitor();
            try {
                mDstMessenger.getBinder().linkToDeath(mDeathMonitor, 0);
            } catch (RemoteException e) {
                mDeathMonitor = null;
                // Override status to indicate failure
                msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;
            }
        }
        // 发送CMD_CHANNEL_HALF_CONNECTED消息到mSrcHandler也就是在init方法中1.中传递进来的ServiceHandler
        mSrcHandler.sendMessage(msg);
    }
    ....
}

上述mSrcHandler.sendMessage(msg);中mSrcHandler是WifiManager$ServiceHandler的一个内部类

private static class ServiceHandler extends Handler {
        ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message message) {
            Object listener = removeListener(message.arg2);
            switch (message.what) {
                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
                    if (message.arg1 == AsyncChannel) {
                        sAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
                    } else {
                        sAsyncChannel = null;
                    }
                    sConnected.countDown();
                    break;
           }
      }
      ....
}

在ServiceHandler处理” CMD_CHANNEL_HALF_CONNECTED”消息,会继续通过AsyncChannel发送一个”CMD_CHANNEL_FULL_CONNECTION”消息

public void sendMessage(Message msg) {
        // 2.这里会向mDstMessenger对应的handler发送一条消息,然后由其处理
        msg.replyTo = mSrcMessenger;
        try {
            mDstMessenger.send(msg);
        } catch (RemoteException e) {
            replyDisconnected(STATUS_SEND_UNSUCCESSFUL);
        }
}

上述mDstMessenger就是在1.处传递的Messenger,也就是通过WifiServiceImpl#getWifiServiceMessenger的

public Messenger getWifiServiceMessenger() {
        enforceAccessPermission();
        enforceChangePermission();
        return new Messenger(mClientHandler);
}

因此,2.处发送的”CMD_CHANNEL_FULL_CONNECTION”消息,将会交给WifiServiceImpl中的ClientHandler来处理

看下WifiServiceImpl的内部类ClientHandler,ClientHandler#handleMessage方法

public void handleMessage(Message msg) {
    ....
    public void handleMessage(Message msg) {
        case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
                    AsyncChannel ac = new AsyncChannel();
                    // 这里的msg.replyTo就是上一步的mSrcMessenger
                    ac.connect(mContext, this, msg.replyTo);
                    break;
                }
    }
    ....

}

此时程序又走了一边AsyncChannel#connect方法

public class AsyncChannel {
    ....
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // 连接srcHandler(WifiServiceImpl$ClientHandler)和dstMessenger(对应WifiManager$ServiceHandler)
        connected(srcContext, srcHandler, dstMessenger);
        // 在replyHalfConnected中会发送CMD_CHANNEL_HALF_CONNECTED到ClientHandler处理
        replyHalfConnected(STATUS_SUCCESSFUL);
}
只不过此时的mSrcHandle就是ClientHandler自己,mDstMessenger对应处理的Handler是WifiManager中的ServiceHandler,在replyHalfConnected方法中
private void replyHalfConnected(int status) {
        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);
        msg.arg1 = status;
        msg.obj = this;
        msg.replyTo = mDstMessenger;
        // 创建一个死亡监听,当当前mDstMessenger对应的binder死亡以后,会在DeathMonitor中发送一个"STATUS_REMOTE_DISCONNECTION"消息
        if (mConnection == null) {
            mDeathMonitor = new DeathMonitor();
            try {
                mDstMessenger.getBinder().linkToDeath(mDeathMonitor, 0);
            } catch (RemoteException e) {
                mDeathMonitor = null;
                // Override status to indicate failure
                msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;
            }
        }
        // 发送CMD_CHANNEL_HALF_CONNECTED消息到ClientHandler
        mSrcHandler.sendMessage(msg);
    }
    ....
}

所以这次发出的”CMD_CHANNEL_HALF_CONNECTED”消息,交给了WifiServiceImpl$ClientHandler处理

public void handleMessage(Message msg) {
            switch (msg.what) {
                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: {
                    if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
                        // 在WifiTrafficPoller中将当前客户端的所有Messenger(和WifiManager$ServiceHandler相关联的),添加到mClients对应的list集合中
                        mTrafficPoller.addClient(msg.replyTo);
                    } else {
                        Slog.e(TAG, "Client connection failure, error=" + msg.arg1);
                    }
                    break;
}

到此为止WifiManager和WifiServiceImpl通过AsyncChannel(实质上是通过messenger和handler)建立的连接就完成了,这里类似于TCP中的三次握手,猜测是确保建立连接万无一失

总结一下:

  • 在WifiManager的init方法中,通过AsyncChannel的connect方法

    a. 先发送CMD_CHANNEL_HALF_CONNECTED到自己WifiManager内部类ServiceHandler的处理
    b. 在WifiManager内部类ServiceHandler处理CMD_CHANNEL_HALF_CONNECTED消息时候,发送CMD_CHANNEL_FULL_CONNECTION消息,由于上一步在AsyncChannel#connect方法中的初始化,这里会交给WifiServiceImpl内部类ClientHandler处理

  • 在WifiServiceImpl内部类ClientHandler处理”CMD_CHANNEL_FULL_CONNECTION”消息

    会再次建立连接,只不过此时的mSrcHandle就是ClientHandler自己,mDstMessenger对应处理的Handler是WifiManager中的ServiceHandler,最终会在Service服务端的WifiTrafficPoller中将当前和客户端建立的所有Messenger(和WifiManager内部类ServiceHandler相关联的),添加到mClients对应的list集合中

如下图:
这里写图片描述

连接流程

WifiManager#connect

public void connect(WifiConfiguration config, ActionListener listener) {
        if (config == null) throw new IllegalArgumentException("config cannot be null");
        validateChannel();
        sAsyncChannel.sendMessage(CONNECT_NETWORK, WifiConfiguration.INVALID_NETWORK_ID,
                putListener(listener), config);
}

这里通过sAsyncChannel发送了一个”CONNECT_NETWORK”消息到WifiServiceImpl服务端处理,在WifiServiceImpl$ClientHandler中对于CONNECT_NETWORK消息进一步交给WifiStateMachine处理

class ConnectModeState extends State {
    public boolean processMessage(Message message) {
         case WifiManager.CONNECT_NETWORK:
                    /**
                     *  The connect message can contain a network id passed as arg1 on message or
                     * or a config passed as obj on message.
                     * For a new network, a config is passed to create and connect.
                     * For an existing network, a network id is passed
                     */
                    netId = message.arg1;
                    config = (WifiConfiguration) message.obj;
                    mWifiConnectionStatistics.numWifiManagerJoinAttempt++;
                    boolean updatedExisting = false;

                    /* Save the network config */
                    if (config != null) {
                        // When connecting to an access point, WifiStateMachine wants to update the
                        // relevant config with administrative data. This update should not be
                        // considered a 'real' update, therefore lockdown by Device Owner must be
                        // disregarded.
                        if (!recordUidIfAuthorized(config, message.sendingUid,
                                /* onlyAnnotate */ true)) {
                            replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                           WifiManager.NOT_AUTHORIZED);
                            break;
                        }

                        String configKey = config.configKey(true /* allowCached */);
                        WifiConfiguration savedConfig =
                                mWifiConfigStore.getWifiConfiguration(configKey);
                        if (savedConfig != null) {
                            config = savedConfig;
                            logd("CONNECT_NETWORK updating existing config with id=" +
                                    config.networkId + " configKey=" + configKey);
                            config.ephemeral = false;
                            config.autoJoinStatus = WifiConfiguration.AUTO_JOIN_ENABLED;
                            updatedExisting = true;
                        }
                        // 1.保存当前网络
                        result = mWifiConfigStore.saveNetwork(config, message.sendingUid);
                        netId = result.getNetworkId();
                    }
                    config = mWifiConfigStore.getWifiConfiguration(netId);

                    if (config == null) {
                        logd("CONNECT_NETWORK no config for id=" + Integer.toString(netId) + " "
                                + mSupplicantStateTracker.getSupplicantStateName() + " my state "
                                + getCurrentState().getName());
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                WifiManager.ERROR);
                        break;
                    } else {
                        String wasSkipped = config.autoJoinBailedDueToLowRssi ? " skipped" : "";
                    }

                    autoRoamSetBSSID(netId, "any");

                    if (message.sendingUid == Process.WIFI_UID
                        || message.sendingUid == Process.SYSTEM_UID) {
                        clearConfigBSSID(config, "CONNECT_NETWORK");
                    }

                    if (deferForUserInput(message, netId, true)) {
                        break;
                    } else if (mWifiConfigStore.getWifiConfiguration(netId).userApproved ==
                                                                    WifiConfiguration.USER_BANNED) {
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                WifiManager.NOT_AUTHORIZED);
                        break;
                    }

                    mAutoRoaming = WifiAutoJoinController.AUTO_JOIN_IDLE;

                    boolean persist =
                        mWifiConfigStore.checkConfigOverridePermission(message.sendingUid);
                    mWifiAutoJoinController.updateConfigurationHistory(netId, true, persist);

                    mWifiConfigStore.setLastSelectedConfiguration(netId);

                    didDisconnect = false;
                    if (mLastNetworkId != WifiConfiguration.INVALID_NETWORK_ID
                            && mLastNetworkId != netId) {
                        /** Supplicant will ignore the reconnect if we are currently associated,
                         * hence trigger a disconnect
                         */
                        didDisconnect = true;
                        mWifiNative.disconnect();
                    }

                    // Make sure the network is enabled, since supplicant will not reenable it
                    mWifiConfigStore.enableNetworkWithoutBroadcast(netId, false);
                    // 2.选择当前网络,通过mWifiNative.reconnect()连接
                    if (mWifiConfigStore.selectNetwork(config, /* updatePriorities = */ true,
                            message.sendingUid) && mWifiNative.reconnect()) {
                        lastConnectAttemptTimestamp = System.currentTimeMillis();
                        targetWificonfiguration = mWifiConfigStore.getWifiConfiguration(netId);

                        /* The state tracker handles enabling networks upon completion/failure */
                        mSupplicantStateTracker.sendMessage(WifiManager.CONNECT_NETWORK);
                        3. 回调回去WifiManager$ServiceHandler处理
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_SUCCEEDED);
                        if (didDisconnect) {
                            /* Expect a disconnection from the old connection */
                            transitionTo(mDisconnectingState);
                        } else if (updatedExisting && getCurrentState() == mConnectedState &&
                                getCurrentWifiConfiguration().networkId == netId) {
                            // Update the current set of network capabilities, but stay in the
                            // current state.
                            updateCapabilities(config);
                        } else {
                            /**
                             *  Directly go to disconnected state where we
                             * process the connection events from supplicant
                             **/
                            transitionTo(mDisconnectedState);
                        }
                    } else {
                        loge("Failed to connect config: " + config + " netId: " + netId);
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                WifiManager.ERROR);
                        break;
                    }
                    break;
     }
}

忘记网络

public void forget(int netId, ActionListener listener) {
        if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
        validateChannel();
        sAsyncChannel.sendMessage(FORGET_NETWORK, netId, putListener(listener));
}

同样的发送消息到WifiServiceImpl服务端,在由WifiServiceImpl发送消息到WifiStateMachine

case WifiManager.FORGET_NETWORK:
                    // Debug only, remember last configuration that was forgotten
                    WifiConfiguration toRemove
                            = mWifiConfigStore.getWifiConfiguration(message.arg1);
                    if (toRemove == null) {
                        lastForgetConfigurationAttempt = null;
                    } else {
                        lastForgetConfigurationAttempt = new WifiConfiguration(toRemove);
                    }
                    // check that the caller owns this network
                    netId = message.arg1;

                    if (!mWifiConfigStore.canModifyNetwork(message.sendingUid, netId,
                            /* onlyAnnotate */ false)) {
                        logw("Not authorized to forget network "
                             + " cnid=" + netId
                             + " uid=" + message.sendingUid);
                        replyToMessage(message, WifiManager.FORGET_NETWORK_FAILED,
                                WifiManager.NOT_AUTHORIZED);
                        break;
                    }
                    // 忘记网络
                    if (mWifiConfigStore.forgetNetwork(message.arg1)) {
                        replyToMessage(message, WifiManager.FORGET_NETWORK_SUCCEEDED);
                        broadcastWifiCredentialChanged(WifiManager.WIFI_CREDENTIAL_FORGOT,
                                (WifiConfiguration) message.obj);
                    } else {
                        loge("Failed to forget network");
                        replyToMessage(message, WifiManager.FORGET_NETWORK_FAILED,
                                WifiManager.ERROR);
                    }
                    break;

其他的操作都是类似的,算是简单的一个记录,今天就到这里了。

作者:mockingbirds 发表于2016/11/27 22:05:12 原文链接
阅读:37 评论:0 查看评论

自定义侧滑菜单

$
0
0

侧滑菜单的简单实现

不少APP中都有这种侧滑菜单,例如QQ这类的,比较有名开源库如slidingmenu。

我们自己如何实现这种侧滑的功能呢?

首先看效果图:

image

这里我们实现的侧滑菜单,是将左侧隐藏的菜单和主面板看作一个整体来实现的,而左侧隐藏的菜单和主面板相当于是这个自定义View的子View。

首先来构造该自定义View的布局:

自定义的SlideMenuView包含两个子view,一个是menuView,另一个是mainView(主面板)。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="${relativePackage}.${activityClass}" >

    <com.wind.view.SlideMenuView
        android:id="@+id/slideMenu"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <include layout="@layout/layout_menu"/>

        <include layout="@layout/layout_main"/>
    </com.wind.view.SlideMenuView>

</RelativeLayout>

接着我们需要实现SlideMenuView的 java代码。

自定义VIew,需要继承某个父类,通过重写父类的某些方法来增强父类的功能。
这里我们选择继承ViewGroup,一般自定义View,需要重写onMeasure,onLayout和onDraw,正常情况下只要重写onDraw就可以了,特殊情况下,需要重写onMeasure 和onLayout。

package com.wind.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class SlideMenuView extends ViewGroup { // FrameLayout {
    private static final String TAG = "SlideMenuView";
    private View menuView, mainView;
    private int menuWidth = 0;

    private Scroller scroller;

    public SlideMenuView(Context context) {
        super(context);
        init();
    }

    public SlideMenuView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        scroller = new Scroller(getContext());
    }

    /**
     * 当一级的子View全部加载玩后调用,可以用于初始化子View的引用 
     * 
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        menuView = getChildAt(0);
        mainView = getChildAt(1);
        menuWidth = menuView.getLayoutParams().width;

        Log.d(TAG, "onFinishInflate() menuWidth: " + menuWidth);
    }

        /**
         * widthMeasureSpec和heightMeasureSpec是系统测量SlideMenu时传入的参数,
         * 这2个参数测量出的宽高能让SlideMenu充满窗体,其实是正好等于屏幕宽高
         */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        Log.d(TAG, "onMeasure: widthMeasureSpec: " + widthMeasureSpec
                + "heightMeasureSpec: " + heightMeasureSpec);

        // int measureSpec = MeasureSpec.makeMeasureSpec(menuWidth,
        // MeasureSpec.EXACTLY);
        //
        // Log.d(TAG,"onMeasure: measureSpec: " +measureSpec);
        // 测量所有子view的宽高
        // 通过getLayoutParams方法可以获取到布局文件中指定宽高
        menuView.measure(widthMeasureSpec, heightMeasureSpec);
        // 直接使用SlideMenu的测量参数,因为它的宽高都是充满父窗体
        mainView.measure(widthMeasureSpec, heightMeasureSpec);

    }

         /**
         * 重新摆放子View的位置
         * l: 当前子view的左边在父view的坐标系中的x坐标
         * t: 当前子view的顶边在父view的坐标系中的y坐标
         */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "onLayout() changed: " + changed + "l: " + l + "t: " + t
                + "r: " + r + "b: " + b);
        Log.d(TAG,"menuView.getMeasuredHeight(): " + menuView.getMeasuredHeight());
        menuView.layout(-menuWidth, 0, 0, menuView.getMeasuredHeight());
        mainView.layout(0, 0, r, b);
    }



    private int downX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downX = (int) event.getX();
            break;

        case MotionEvent.ACTION_MOVE:
            int moveX = (int) event.getX();
            int deltaX = (moveX - downX);
            Log.e(TAG, "scrollX: " + getScrollX());

            int newScrollX = getScrollX() - deltaX;
            //使得SlideMenuView不会滑动出界
            if(newScrollX > 0) newScrollX = 0;
            if(newScrollX < -menuWidth) newScrollX = -menuWidth;

            scrollTo(newScrollX, 0);
            Log.e(TAG, "moveX: " + moveX);
            downX = moveX;
            break;

        case MotionEvent.ACTION_UP:

            //①.使用自定义动画
//          ScrollAnimation scrollAnimation;
//          if(getScrollX()>-menuWidth/2){
//              //关闭菜单
////                scrollTo(0, 0);
//              scrollAnimation = new ScrollAnimation(this, 0);
//          }else {
//              //打开菜单
////                scrollTo(-menuWidth, 0);
//              scrollAnimation = new ScrollAnimation(this, -menuWidth);
//          }
//          startAnimation(scrollAnimation);


            //②使用Scroller
            if(getScrollX()>-menuWidth/2){
//              //关闭菜单
                closeMenu();
            }else {
                //打开菜单
                openMenu();
            }
            break;
        }

        return true;
    }

    private void openMenu() {
        Log.d(TAG, "openMenu...");
        scroller.startScroll(getScrollX(), 0, -menuWidth-getScrollX(), 400);
        invalidate();
    }

    private void closeMenu() {
        Log.d(TAG, "closeMenu...");
        scroller.startScroll(getScrollX(), 0, 0-getScrollX(), 400);
        invalidate();
    }

        /**
         * Scroller不主动去调用这个方法
         * 而invalidate()可以掉这个方法
         * invalidate->draw->computeScroll
         */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if(scroller.computeScrollOffset()){//返回true,表示动画没结束
            scrollTo(scroller.getCurrX(), 0);
            invalidate();
        }
    }

    public void switchMenu() {
        if(getScrollX() == 0) {
            openMenu();
        } else {
            closeMenu();
        }
    }
}
  • 这里继承ViewGroup,由于子View都是利用系统的控件填充,所以不需要重写onDraw方法。
  • 由于是继承自ViewGroup,需要实现onMeasure来测量两个子View的高度和宽度。我们还可以继承FrameLayout,则不需要实现onMeasure,因为FrameLayout已经帮我们实现onMeasure。
  • 重写onLayout方法,重新定义自定义的位置摆放,左侧的侧滑菜单需要使其处于隐藏状态。
  • 重写onTouch方法,通过监测Touch的Up和Down来滑动View来实现侧滑的效果。

关于平滑滚动

我们可以采用Scroller类中的startScroll来实现平滑滚动,同样我们可以使用自定义动画的方式来实现。

package com.wind.view;

import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;

/**
 * 让指定view在一段时间内scrollTo到指定位置
 * @author Administrator
 *
 */
public class ScrollAnimation extends Animation{

    private View view;
    private int targetScrollX;
    private int startScrollX;
    private int totalValue;

    public ScrollAnimation(View view, int targetScrollX) {
        super();
        this.view = view;
        this.targetScrollX = targetScrollX;

        startScrollX = view.getScrollX();
        totalValue = this.targetScrollX - startScrollX;

        int time = Math.abs(totalValue);
        setDuration(time);
    }



    /**
     * 在指定的时间内一直执行该方法,直到动画结束
     * interpolatedTime:0-1  标识动画执行的进度或者百分比
     * time :  0   - 0.5  - 0.7  -   1
     * value:  10  -  60  -  80  -  110
     * 当前的值 = 起始值 + 总的差值*interpolatedTime
     */
    @Override
    protected void applyTransformation(float interpolatedTime,
            Transformation t) {
        super.applyTransformation(interpolatedTime, t);
        int currentScrollX = (int) (startScrollX + totalValue*interpolatedTime);
        view.scrollTo(currentScrollX, 0);
    }
}

如上面的代码:
通过自定义动画来让View在一段时间内重复执行这个动作。

关于getScrollX

将View向右移动的时候,通过View.getScrollX得到的值是负的。
其实可以这样理解:
*getScrollX()表示的是当前的屏幕x坐标的最小值-移动的距离(向右滑动时移动的距离为正值,
向左滑动时移动的距离为负值)。*

对scrollTo和scrollBy的理解

我们查看View的源码发现,scrollBy其实调用的就是scrollTo,scrollTo就是把View移动到屏幕的X和Y位置,也就是绝对位置。而scrollBy其实就是调用的scrollTo,但是参数是当前mScrollX和mScrollY加上X和Y的位置,所以ScrollBy调用的是相对于mScrollX和mScrollY的位置。

我们在上面的代码中可以看到当我们手指不放移动屏幕时,就会调用scrollBy来移动一段相对的距离。而当我们手指松开后,会调用mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);来产生一段动画来移动到相应的页面,在这个过程中系统回不断调用computeScroll(),我们再使用scrollTo来把View移动到当前Scroller所在的绝对位置。

/**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                invalidate(true);
            }
        }
    }
   /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
作者:H_Gao 发表于2016/11/27 22:14:13 原文链接
阅读:29 评论:0 查看评论

学习GestureDetectorCompat,实现卡片左右滑动消失效果

$
0
0

转载请标明出处:
http://blog.csdn.net/iamzgx/article/details/53239874
本文出自:【iGoach的博客
这几天,android studio2.2.2和android7.0来袭,于是就更新下了哦。配置如下

compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
    minSdkVersion 15
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
}
dependencies {
       classpath 'com.android.tools.build:gradle:2.2.2'
   }

结果包名报错

The SDK platform-tools version (24.0.4) is too old to check APIs compiled with API 25; please update

解决办法就是

这里写图片描述

使用SDK Manager把Android SDK Platform-Tools 24.x.x升级到Android SDK Platform-Tools 25.x.x。然后安装,最后重新Restart下Android Studio就好了。

以上是题外之外,接下来进入这篇博客的正题。

第一次看到这种效果是在探探那里看到的,里面美女多多呀。其他一些社交类应用首页推荐好友的时候也有遇到这种效果,所以就来看看他是怎么实现的。通过网上搜索,发现网上也有很多实现的方案,其中我知道的就是通过GestureDetectorCompat实现,或者通过ViewDragHelper实现。以下,就来说说通过GestureDetectorCompat是怎么实现的。

GestureDetectorCompat类

其实GestureDetectorCompat这个类,很早之前android就提供了。这个类是为了减轻开发者在onTouchEvent处理android处理触摸和手势事件的复杂度,它提供了两个接口,OnGestureListener和OnDoubleTapListener。还有一个静态内部类SimpleOnGestureListener,实际SimpleOnGestureListener也是实现前两个接口实现的。

其中OnGestureListener有以下几个方法回调

  • onDown 手指按下触摸屏的那个瞬间回调
  • onShowPress 手指按下滑动而不是长按的时候回调
  • onSingleTapUp 手指按下迅速松开的那个瞬间回调
  • onScroll 手指在触摸屏滑动的时候回调
  • onLongPress 手指长按触摸屏的时候回调
  • onFling 手指迅速移动并松开的时候回调

而OnDoubleTapListener有以下几个方法回调

  • onSingleTapConfirmed 手指单击的时候回调
  • onDoubleTap 手指双击的时候回调
  • onDoubleTapEvent 手指双击之后触发的按下滑动松开等操作事件的回调

简单概括

上面简单介绍了GestureDetectorCompat这个类,这里实现卡片左右滑动消失效果只需要重写SimpleOnGestureListener的onScroll和onSingleTapUp两个方法。知道这些之后,那就开始动手写吧!从哪里开始呢?当然是先布局咯。怎么布局?先来看下探探的效果

这里写图片描述

忽略美女再看会发现,这个布局是一层层叠加起来的,然后每个卡片类似于RecyclerView的一个item。好了,想到这里,那我们就把父布局定位为RelativeLayout,它具有叠加效果,姑且命名为CardStack。然后每个item的父View用CardView,然后通过margin来初始化每个item的间距来控制位置。当顶部CardView滑动的时候,通过GestureDetectorCompat监听它滑动的变化值,通过变化值来改变每个item的margin,从而产生左右滑动的效果,当手指松开的时候,通过属性动画完成消失或者恢复原位的动画。

大概思路就是这样,通过这个思路,先来准备几个帮助类。

DragGestureDetector帮助类

它主要是通过实现GestureDetector.SimpleOnGestureListener来监听滑动的值。既然是监听,那么就要设计一个对外的接口DragListener。它主要回调的方法有

public interface DragListener{
      boolean onDragStart(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
      boolean onDragContinue(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
      boolean onDragEnd(MotionEvent e1, MotionEvent e2);
      boolean onTapCap();
  }
  • onDragStart 开始滑动卡片的回调
  • onDragContinue 滑动过程的回调
  • onDragEnd 手指松开的时候回调
  • onTapCap 手指按下没有滑动然后松开的回调,也就是onSingleTapUp ,比如
    点击一下非常快的(不滑动)Touchup:OnGestureListener回调为onDown->onSingleTapUp->onSingleTapConfirmed
    点击一下稍微慢点的(不滑 动)Touchup:OnGestureListener回调为onDown->onShowPress->onSingleTapUp->onSingleTapConfirmed

外部通过传递DragGestureDetector帮助类的构造器参数传入DragListener接口,同时传入Context,供GestureDetectorCompat初始化使用,构造器如下

public DragGestureDetector(Context context,DragListener mListener){
       mGestureDetectorCompat = new GestureDetectorCompat(context,new DragGestureListener());
       this.mListener = mListener ;
   }

其中,GestureDetectorCompat的初始化,需要传入Context参数和一个实现OnGestureListener接口的参数,这里使用 继承GestureDetector.SimpleOnGestureListener静态内部类的DragGestureListener。而DragGestureListener需要重写onScroll和onSingleTapUp两个方法。在这两个方法里面调用DragListener的不同方法,代码如下

private class DragGestureListener extends GestureDetector.SimpleOnGestureListener{
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if(mListener==null)
                return true ;
            if(!mStarted){
                mListener.onDragStart(e1,e2,distanceX,distanceY);
                mStarted = true;
            }else{
                mListener.onDragContinue(e1,e2,distanceX,distanceY);
            }
            mOriginalEvent = e1 ;
            return true;
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return mListener.onTapCap();
        }
    }

上面主要是通过mStarted这个标识来判断是否刚开始滑动,同时把事件和滑动距离传递出去。以及在onSingleTapUp里面回调onTapCap。而另外的onDragEnd调用,我们实现一个方法onTouchEvent,供外部传递MotionEvent事件,然后处理onDragEnd的调用,代码如下

public void onTouchEvent(MotionEvent event){
        mGestureDetectorCompat.onTouchEvent(event);
        int action = MotionEventCompat.getActionMasked(event);
        switch (action){
            case MotionEvent.ACTION_DOWN:
                mOriginalEvent = event;
                break;
            case MotionEvent.ACTION_UP:
                if(mStarted) {
                    mListener.onDragEnd(mOriginalEvent, event);
                }
                mStarted = false;
                break;
        }
    }

通过调用mGestureDetectorCompat的onTouchEvent方法,就可以把MotionEvent 事件指给GestureDetectorCompat管理了。至于onDragEnd的调用,也就是当手指松开的时候,所以在ACTION_UP里面处理,同时把mStarted 标示复位。

CardStackUtils帮助类
这个帮助类,主要是提供一些静态方法,以备后用
clone一个RelativeLayout.LayoutParams的方法

  public static RelativeLayout.LayoutParams cloneParams(RelativeLayout.LayoutParams layoutParams){
        RelativeLayout.LayoutParams cloneParams = new RelativeLayout.LayoutParams(layoutParams.width,layoutParams.height);
        cloneParams.leftMargin = layoutParams.leftMargin ;
        cloneParams.topMargin = layoutParams.topMargin ;
        cloneParams.rightMargin = layoutParams.rightMargin;
        cloneParams.bottomMargin = layoutParams.bottomMargin;
        int[] rules = layoutParams.getRules();
        for (int i = 0; i < rules.length; i++) {
            cloneParams.addRule(i,rules[i]);
        }
        return cloneParams;
    }

计算滑动的距离方法

public static float distance(float x1,float y1,float x2,float y2){
        return (float) Math.sqrt((x2-x1)*(x2-x1)-(y2-y1)*(y2-y1));
    }

判断滑动方向的方法

  private static final int LEFT_TOP_DIRECTION = 0 ;
  private static final int LEFT_BOTTOM_DIRECTION = 1 ;
  private static final int RIGHT_TOP_DIRECTION = 2 ;
  private static final int RIGHT_BOTTOM_DIRECTION = 3;
  public static int direction(float x1,float y1,float x2,float y2){
        if(x1>x2){
            if(y1>y2){
                return LEFT_TOP_DIRECTION;
            }
            return LEFT_BOTTOM_DIRECTION;
        }
        if(y1>y2){
            return RIGHT_TOP_DIRECTION;
        }
        return RIGHT_BOTTOM_DIRECTION;
    }

计算左右滑动消失动画滑动结束的位置方法

  public static RelativeLayout.LayoutParams getMoveParams(CardView cardView
            ,int leftMargin,int topMargin){
        RelativeLayout.LayoutParams originParams = (RelativeLayout.LayoutParams) cardView.getLayoutParams();
        RelativeLayout.LayoutParams targetLayoutParams = cloneParams(originParams);
        targetLayoutParams.leftMargin += leftMargin ;
        targetLayoutParams.rightMargin -= leftMargin ;
        targetLayoutParams.topMargin += topMargin;
        targetLayoutParams.bottomMargin -= topMargin;
        return targetLayoutParams;
    }

另外两个方法,一个dp转换为px和取值范围限制方法

public static int dpToPx(Context context, float dpValue){
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue*scale+0.5f);
    }
    public static int cling(int min,int max,int value){
        return Math.min(max,Math.max(min,value));
    }

RelativeLayoutParamsEvaluator类
通过 实现TypeEvaluator自定义左右滑动消失的属性动画。主要是改变margin值,代码如下

public class RelativeLayoutParamsEvaluator implements TypeEvaluator<RelativeLayout.LayoutParams> {
    @Override
    public RelativeLayout.LayoutParams evaluate(float fraction, RelativeLayout.LayoutParams startValue
            , RelativeLayout.LayoutParams endValue) {
        RelativeLayout.LayoutParams currentParams = CardStackUtils.cloneParams(startValue);
        currentParams.leftMargin += (endValue.leftMargin-startValue.leftMargin)*fraction;
        currentParams.topMargin += (endValue.topMargin-startValue.topMargin)*fraction;
        currentParams.rightMargin += (endValue.rightMargin-startValue.rightMargin)*fraction;
        currentParams.bottomMargin += (endValue.bottomMargin-startValue.bottomMargin)*fraction;
        return currentParams;
    }
}

CardStack对外提供接口CardEventListener

   public interface CardEventListener{
        boolean swipeStart(int direction,float distance);
        boolean swipeContinue(int direction,float distanceX,float distanceY);
        boolean swipeEnd(int direction,float distance);
        void topCardTapped();
    }

以及默认实现这个接口的DefaultStackEventListener

public class DefaultStackEventListener implements CardStack.CardEventListener {

    private float mThreshold;

    public DefaultStackEventListener(int i) {
        mThreshold = i;
    }

    @Override
    public boolean swipeEnd(int section, float distance) {
        return distance > mThreshold;
    }

    @Override
    public boolean swipeStart(int section, float distance) {
        return false;
    }

    @Override
    public boolean swipeContinue(int section, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void topCardTapped() {

    }
}

其中mThreshold控制滑动消失和恢复原位移动距离的界限,通过调用swipeEnd来判断。

CardStack的实现
首先定义CardStack的属性

<declare-styleable name="CardStack">
        <attr name="stackMargin" format="dimension"/>
        <attr name="backgroundColor" format="color"/>
        <attr name="showNum" format="integer"/>
        <attr name="cardView_radius" format="dimension"/>
    </declare-styleable>

这里我总结的有上面几个属性配置。开发中,有需要,可以再加一些属性。这里暂时就这几个,通过定义stackMargin配置底部每个item之间的间距,通过backgroundColor属性配置每个item的背景颜色,然后通过showNum配置默认需要显示多少个item显示,最后通过cardView_radius配置CardView的圆角角度。

定义好属性之后,接下来就是定义CardStack的一些局部变量和常量

//默认背景颜色
private static final int DEFAULT_BACKGROUND_COLOR = Color.WHITE;
//默认显示item的个数
private static final int DEFAULT_SHOW_NUM = 4 ;
//默认CardView的圆角角度,单位dp
private static final int DEFAULT_CARD_VIEW_RADIUS = 5;
//默认底部的间距
private static final int DEFAULT_MARGIN_BOTTOM_DP = 10;
//item背景色的局部变量
private int mBackgroundColor = DEFAULT_BACKGROUND_COLOR ;
//显示item个数的局部变量
private int mShowNum = DEFAULT_SHOW_NUM ;
//CardView圆角角度变量
private int mCardViewRadius = DEFAULT_CARD_VIEW_RADIUS ;
//底部margin变量
private int mStackMargin = DEFAULT_MARGIN_BOTTOM_DP ;
//List保存显示的item的父布局CardView
private List<CardView> viewCollection;
//保存各个CardView的LayoutParams
private HashMap<View,LayoutParams> mLayoutsMap;
//绑定的adapter
private BaseAdapter mBaseAdapter;
//控制手势帮助类
private DragGestureDetector dragGestureDetector;
//对外监听事件的接口,默认通过DefaultStackEventListener实现
private CardEventListener cardEventListener = new DefaultStackEventListener(300);
//滑动效果动画的RelativeLayout.LayoutParams
private RelativeLayout.LayoutParams[] endAnimLayouts = new RelativeLayout.LayoutParams[4];
//滑动效果动画的滑动距离
private static final int ANIM_DISTANCE = 4000;
//是否可以滑动
private boolean canSwipe = true;
//滑动的时候旋转角度
private float mRotation;
//移除后开始的标志
private int mIndex ; 

定义好局部变量后,然后再初始化局部变量

  • 查找CardStack定义的那些属性
 private void initStyle(AttributeSet attrs){
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs,R.styleable.CardStack);
        mBackgroundColor = typedArray.getColor(R.styleable.CardStack_backgroundColor,
                DEFAULT_BACKGROUND_COLOR);
        mShowNum = typedArray.getInteger(R.styleable.CardStack_showNum,DEFAULT_SHOW_NUM);
        mCardViewRadius = typedArray.getDimensionPixelSize(R.styleable.CardStack_showNum,
                CardStackUtils.dpToPx(getContext(),DEFAULT_CARD_VIEW_RADIUS));
        mStackMargin = typedArray.getDimensionPixelSize(R.styleable.CardStack_stackMargin,
                CardStackUtils.dpToPx(getContext(),DEFAULT_MARGIN_BOTTOM_DP));
        typedArray.recycle();
    }
  • 初始化几个变量
 private void initData(){
   viewCollection = new ArrayList<>();
   mLayoutsMap = new HashMap<>();
   dragGestureDetector = new DragGestureDetector(getContext(),this);
    }

这几个变量主要是viewCollection ,保存显示item的父布局CardView的集合;mLayoutsMap ,保存显示item父布局的LayoutParams的结合;dragGestureDetector ,初始化DragGestureDetector帮助类。

  • 添加CardStack的子View实现叠加效果,以及添加viewCollection 集合数据
private void initView(){
      removeAllViews();
      viewCollection.clear();
      for (int i = 0; i < mShowNum; i++) {
          CardView cardView = createDefaultCardView();
          viewCollection.add(cardView);
          addView(cardView,new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT));
      }
      if(mBaseAdapter!=null)
      bindData();
    }
 private CardView createDefaultCardView(){
        CardView cardView = new CardView(getContext());
        cardView.setCardBackgroundColor(mBackgroundColor);
        cardView.setRadius(mCardViewRadius);
        return cardView;
    }
  • 初始化左右滑动消失动画结束位置
private void initEndAnimParams(){
        CardView topView = getTopCardView();
        endAnimLayouts[0] = CardStackUtils.getMoveParams(topView, -ANIM_DISTANCE, -ANIM_DISTANCE);
        endAnimLayouts[1] = CardStackUtils.getMoveParams(topView, -ANIM_DISTANCE, ANIM_DISTANCE);
        endAnimLayouts[2] = CardStackUtils.getMoveParams(topView, ANIM_DISTANCE, -ANIM_DISTANCE);
        endAnimLayouts[3] = CardStackUtils.getMoveParams(topView, ANIM_DISTANCE, ANIM_DISTANCE);

    }

这里移动位移为ANIM_DISTANCE(4000),以达到移除屏幕外的效果

初始化这些变量之后,接下来就是实现adapter绑定数据的方法,主要是传入BaseAdapter,代码如下

  public void setAdapter(BaseAdapter baseAdapter){
        if(baseAdapter==null)
            throw new IllegalArgumentException("传入Adapter不能为null");
        if(this.mBaseAdapter!=null)
            this.mBaseAdapter.unregisterDataSetObserver(mDb);
        this.mBaseAdapter = baseAdapter ;
        mBaseAdapter.registerDataSetObserver(mDb);
        bindData();
    }
private DataSetObserver mDb = new DataSetObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            //这里是notifyDataSetChanged更新数据用的
            initView();
        }
    };

上面使用了DataSetObserver,这个方法的主要原理就是广播机制,通过registerDataSetObserver进行广播注册,这样notifyDataSetChanged就可以实现更新数据的效果。

然后通过的BaseAdapter获取数据,对显示的CardView添加子View,同时绑定数据,其中子View通过BaseAdapter的getView方法获取,BaseAdapter的getCount比默认显示CardView小的话,就把CardView暂时隐藏,否则就显示绑定数据。代码如下

  private void bindData(){
        mLayoutsMap.clear();
        int rootViewSize = viewCollection.size();
        for(int i = rootViewSize-1 ; i>=0 ;i--){
            CardView parent = viewCollection.get(i);
            int position = mIndex+rootViewSize - i - 1;
            if(position>mBaseAdapter.getCount()-1){
                parent.setVisibility(View.GONE);
            }else{
                parent.setVisibility(View.VISIBLE);
                View childView = mBaseAdapter.getView(position,null,parent);
                parent.addView(childView);
                if(i!=0)
                    position+=1;
                initLayout(parent,((position-mIndex)*mStackMargin));
            }
        }
        CardView topCardView = getTopCardView();
        topCardView.setOnTouchListener(this);
    }

添加CardView的子View之后,接下来就是通过initLayout方法初始化各个CardView的位置和间距了。上面的计算margin的方法里面,其中最后一张和倒数第二张margin是一样的,其他是上一张的mStackMargin倍。主要代码如下

 private void initLayout(CardView childView,int pixel){
        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) childView.getLayoutParams();
        layoutParams.leftMargin += pixel  ;
        layoutParams.topMargin += pixel ;
        layoutParams.rightMargin += pixel;
        childView.setLayoutParams(layoutParams);
        mLayoutsMap.put(childView,CardStackUtils.cloneParams(layoutParams));
    }

同时在上面通过mLayoutsMap保存每个CardView的初始化LayoutParms

  • 处理每个item的位置之后,接下来就是通过setOnTouchListener为顶部item设置触摸事件监听
@Override
    public boolean onTouch(View v, MotionEvent event) {
        dragGestureDetector.onTouchEvent(event);
        return true;
    }

在上面bindData方法里面,我们通过getTopCardView获取到顶部CardView,然后设置了TouchEvent事件,在DragGestureDetector帮助类里面需要传递TouchEvent事件,所以我们把事件传递进去,代码如下

  public CardView getTopCardView(){
        if(viewCollection.isEmpty())
            return null;
        return viewCollection.get(viewCollection.size() - 1);
    }
  @Override
    public boolean onTouch(View v, MotionEvent event) {
        dragGestureDetector.onTouchEvent(event);
        return true;
    }

然后重写DragListener的每个方法,在DragListener不同方法里面处理不同的滑动值和效果

@Override
    public boolean onDragStart(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if(canSwipe){
            startDrag(e1,e2);
        }
        float x1 = e1.getRawX();
        float y1 = e1.getRawY();
        float x2 = e2.getRawX();
        float y2 = e2.getRawY();
        final int direction = CardStackUtils.direction(x1, y1, x2, y2);
        float distance = CardStackUtils.distance(x1, y1, x2, y2);
        cardEventListener.swipeStart(direction, distance);
        return true;
    }

    @Override
    public boolean onDragContinue(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (canSwipe) {
            startDrag(e1, e2);
        }
        float x1 = e1.getRawX();
        float y1 = e1.getRawY();
        float x2 = e2.getRawX();
        float y2 = e2.getRawY();
        final int direction = CardStackUtils.direction(x1,y1,x2,y2);
        cardEventListener.swipeContinue(direction, Math.abs(x2-x1), Math.abs(y2-y1));
        return true;
    }

    @Override
    public boolean onDragEnd(MotionEvent e1, MotionEvent e2) {
        float x1 = e1.getRawX();
        float y1 = e1.getRawY();
        float x2 = e2.getRawX();
        float y2 = e2.getRawY();
        float distance = CardStackUtils.distance(x1,y1,x2,y2);
        int direction = CardStackUtils.direction(x1,y1,x2,y2);
        boolean isSwipe = cardEventListener.swipeEnd(direction,distance);
        if(isSwipe){
            if(canSwipe){
                AnimatorSet animatorSet = new AnimatorSet();
                ArrayList<Animator> collectionAnim = new ArrayList<>();
                final CardView topCardView = getTopCardView();
                RelativeLayout.LayoutParams topLayoutParams = (LayoutParams) topCardView.getLayoutParams();
                RelativeLayout.LayoutParams currentLayoutParams = CardStackUtils.cloneParams(topLayoutParams);
                ValueAnimator topCardViewAnim = ValueAnimator.ofObject(new RelativeLayoutParamsEvaluator()
                        ,currentLayoutParams, endAnimLayouts[direction]);
                topCardViewAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        topCardView.setLayoutParams((LayoutParams) animation.getAnimatedValue());
                    }
                });
                topCardViewAnim.setDuration(250);
                collectionAnim.add(topCardViewAnim);
                int viewSize = viewCollection.size();
                for(int i = 0 ;i <viewSize ;i++){
                    final CardView cardView = viewCollection.get(i);
                    if(cardView==topCardView)
                        continue;
                    RelativeLayout.LayoutParams startLayoutParams = CardStackUtils.cloneParams((LayoutParams)cardView.getLayoutParams());
                    CardView nextCardView = viewCollection.get(i+1);
                    RelativeLayout.LayoutParams endLayoutParams = mLayoutsMap.get(nextCardView);
                    if(endLayoutParams==null)
                        continue;
                    ValueAnimator otherViewAnim = ValueAnimator.ofObject(new RelativeLayoutParamsEvaluator()
                            ,startLayoutParams, endLayoutParams);
                    otherViewAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            cardView.setLayoutParams((LayoutParams) animation.getAnimatedValue());
                        }
                    });
                    otherViewAnim.setDuration(250);
                    collectionAnim.add(otherViewAnim);
                }
                animatorSet.playTogether(collectionAnim);
                animatorSet.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        //移除滑出去的view
                        mIndex++;
                        initView();
                    }
                });
                animatorSet.start();
            }
        }else{
            if (canSwipe) {
                final CardView topView =  getTopCardView();
                ValueAnimator rotationAnim = ValueAnimator.ofFloat(mRotation, 0f);
                rotationAnim.setDuration(250);
                rotationAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator v) {
                        topView.setRotation(((Float) (v.getAnimatedValue())).floatValue());
                    }
                });
                rotationAnim.start();

                for(final View v : viewCollection){
                    RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) v.getLayoutParams();
                    RelativeLayout.LayoutParams endLayout = CardStackUtils.cloneParams(layoutParams);
                    RelativeLayout.LayoutParams endLayoutParams = mLayoutsMap.get(v);
                    if(endLayoutParams==null)
                        continue;
                    ValueAnimator layoutAnim = ValueAnimator.ofObject(new RelativeLayoutParamsEvaluator(),
                            endLayout,endLayoutParams);
                    layoutAnim.setDuration(250);
                    layoutAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator value) {
                            v.setLayoutParams((LayoutParams)value.getAnimatedValue());
                        }
                    });
                    layoutAnim.start();
                }
            }
        }
        return true;
    }

    @Override
    public boolean onTapCap() {
        cardEventListener.topCardTapped();
        return true;
    }

在上面代码中,在onDragStart和onDragContinue方法里面,分别调用了startDrag这个方法。然后在onDragEnd方法里面,处理了左右滑动消失的动画和恢复原位的动画,其中滑动消失动画和恢复原位使用前面定义的RelativeLayoutParamsEvaluator来实现,同理,每个CardView都会绑定一个动画,从而产生放大的效果。其中,startDrag主要处理是手指滑动的时候改变CardView的margin从而产生滑动的效果,代码如下

private void startDrag(MotionEvent e1,MotionEvent e2){
        float rotation_coefficient = 50f;
        CardView topCardView = getTopCardView();
        if(topCardView==null)
            return;
        int diffX = (int) (e2.getRawX()-e1.getRawX());
        int diffY = (int)(e2.getRawY()-e1.getRawY());
        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) topCardView.getLayoutParams();
        RelativeLayout.LayoutParams topViewLayouts = mLayoutsMap.get(topCardView);
        layoutParams.leftMargin = topViewLayouts.leftMargin+diffX;
        layoutParams.rightMargin = topViewLayouts.rightMargin-diffX;
        layoutParams.topMargin = topViewLayouts.topMargin + diffY;
        layoutParams.bottomMargin = topViewLayouts.bottomMargin -diffY;
        mRotation = diffX/rotation_coefficient;
        topCardView.setRotation(mRotation);
        topCardView.setLayoutParams(layoutParams);
        for(CardView cardView:viewCollection){
            if(cardView==topCardView)
                continue;
            int index = viewCollection.indexOf(cardView);
            Log.d("zgx","index====="+index);
            if(index!=0)
                scaleFrom(cardView, -Math.abs(CardStackUtils.cling(-mStackMargin,mStackMargin,(int)(diffX*0.05))));
        }
    }
    public void scaleFrom(CardView fromCardView,int pixel){
        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) fromCardView.getLayoutParams();
        RelativeLayout.LayoutParams cardViewLayouts = mLayoutsMap.get(fromCardView);
        if(cardViewLayouts==null)
            return;
        layoutParams.leftMargin = cardViewLayouts.leftMargin+pixel;
        layoutParams.rightMargin = cardViewLayouts.rightMargin+pixel;
        layoutParams.topMargin = cardViewLayouts.topMargin + pixel;
    }

首先是改变顶部CardView的旋转角度,然后就是改变顶部CardView的margin,接着就是改变其他CardView的margin。最后来看下实现的动画效果
这里写图片描述

以上就是实现的卡片左右滑动消失效果的主要讲解,详细可查看源码。
下载源码

作者:Iamzgx 发表于2016/11/27 22:17:19 原文链接
阅读:28 评论:0 查看评论

重拾百度定位之踩坑篇

$
0
0

源码传送门


前言

最近更新项目中用的百度定位SDK时遇见了一个奇葩的问题。当升级SDK后百度定位一直返回505,通过百度定位官网查看该码表示AK非法或者不存在。很纠结,于是自己又写了一个demo来研究一下百度定位以及大家使用百度定位经常出现的问题,特此记录。这篇文章我先将百度定位的实现也介绍一下,最后再分析遇到的问题及解决方案。

定位分析

目前百度定位提供了WIFI,基站,GPS等多种定位方式,适用于室内、室外多种定位场景,具有出色的定位性能:定位精度高(其实我是想吐槽的)、覆盖率广、网络定位请求流量小、定位速度快。图片来自百度官网

集成定位SDK

现在官网提供的最新的定位SDK版本是v7.0,官网SDK下载地址请戳 定位SDK,可根据自己的需要下载,在这里我进入全部下载,只下载了全量定位。在新版本V7.0中百度将定位对开发包实现了分离
(1)基础定位:开发包体积最小,但只包含基础定位能力(GPS/WiFi/基站)、基础位置描述能力;
(2)离线定位:在基础定位能力基础之上,提供离线定位能力,可在网络环境不佳时,进行精准定位;
(3)室内定位:在基础定位能力基础之上,提供室内高精度定位能力,精度可达1-3米;
(4)全量定位:包含离线定位、室内高精度定位能力,同时提供更人性化的位置描述服务;
对于这四种类型定位开发包是互斥的,一个应用中只需集成一种定位开发包即可。下载成功之后,将jar包和.so文件放到对应的文件下即可。

申请秘钥

使用百度定位,我们需要在官网申请一个AK,项目定位时需要使用这个Ak,一个应用对于一个AK,AK申请时需要提供包名及SHA1值。具体方式
可去官网查看。在这里我简单介绍下SHA1获取方式。在申请Ak时,页面填写发布版SHA1和开发版SHA1。下面我提供两种方式获取SHA1值。

AndroidStudio Terminal获取

 -rfc                            以 RFC 样式输出                                                                               
 -alias <alias>                  要处理的条目的别名                                                                            
 -keystore <keystore>            密钥库名称                                                                                    
 -storepass <arg>                密钥库口令                                                                                    
 -storetype <storetype>          密钥库类型                                                                                    
 -providername <providername>    提供方名称                                                                                    
 -providerclass <providerclass>  提供方类名                                                                                    
 -providerarg <arg>              提供方参数                                                                                    
 -providerpath <pathlist>        提供方类路径                                                                                  
 -v                              详细输出                                                                                      
 -protected                      通过受保护的机制的口令            

上面是获取密钥库信息的一些命令,则在此获取SHA1可以

keytool -v -list -keystore 【密钥库文件路径】 -storepass 【密钥库文件密码】

这里写图片描述
在Terminal执行命令后就出现上面的详细信息。SHA1后面的那一串字符就是我们需要的SHA1.

CMD方式

如果要在CMD中获取,必须先要设置环境变量,具体设置方式可谷歌搜索。当然获取的命令和在AndroidStudio中获取是一样的。在上面我获取下开发版SHA1。对于debug版一般存用户下的.android目录下,我们打开CMD后执行 cd .android然后通过dir就可以看到目录下会有一个debug.keystore文件,我们找的就是它。
这里写图片描述
在图中你会看到没有写-storepass参数(当然也可和上面一样)。在回车后会提示输入密钥库口令,对于我们的debug版本口令默认是android,输入后回车即可看到详细信息了。

环境配置

要想实现定位,我们必须在清单文件中加入一些必要的权限以及key等信息,如下

    <!--百度定位权限相关-->
    <!-- 这个权限用于进行网络定位-->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
    <!-- 这个权限用于访问GPS定位-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
    <!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位-->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
    <!-- 获取运营商信息,用于支持提供运营商信息相关的接口-->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
    <!-- 这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission>
    <!-- 用于读取手机当前的状态-->
    <uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
    <!-- 写入扩展存储,向扩展卡写入数据,用于写入离线定位数据-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <!-- 访问网络,网络定位需要上网-->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- SD卡读取权限,用户写入离线定位数据-->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS">       </uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <service
            android:name="com.baidu.location.f"
            android:enabled="true"
            android:process=":remote" >
        </service>
        <meta-data
            android:name="com.baidu.lbsapi.API_KEY"
            android:value="w7NQOKL8SpxHrs6lixBNoe90" />
     </application>

定位实现

对于定位的实现我们可以分为三步,第一步:初始化LocationClient;第二步:通过LocationClientOption设置定位参数;第三步:实现BDLocationListener接口。看着是不是很简单,你没看错,确实很简单。

初始化LocationClient

  /**
     * 获取LocationService实例
     *
     * @param context
     * @return
     */
    public static LocationService getInstance(Context context) {
        if (locationClient == null) {
            synchronized (LocationService.class) {
                locationService= new LocationService(context);
            }
        }
        return locationService;
    }

    private LocationService(Context context) {
        if (locationClient == null) {
            locationClient = new LocationClient(context);
            locationClient.setLocOption(getDefaultLocationClientOption());
        }
    }

设置定位参数

 /***
     * 配置参数
     *
     * @return DefaultLocationClientOption
     */
    public LocationClientOption getDefaultLocationClientOption() {
        if (locationClientOption == null) {
            locationClientOption = new LocationClientOption();
            locationClientOption.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
            locationClientOption.setCoorType("bd09ll");//可选,默认gcj02,设置返回的定位结果坐标系,如果配合百度地图使用,建议设置为bd09ll;
            locationClientOption.setScanSpan(3000);//可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
            locationClientOption.setIsNeedAddress(true);//可选,设置是否需要地址信息,默认不需要
            locationClientOption.setIsNeedLocationDescribe(true);//可选,设置是否需要地址描述
            locationClientOption.setNeedDeviceDirect(true);//可选,设置是否需要设备方向结果
            locationClientOption.setLocationNotify(true);//可选,默认false,设置是否当gps有效时按照1S1次频率输出GPS结果
            locationClientOption.setIgnoreKillProcess(true);//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
            locationClientOption.setIsNeedLocationDescribe(true);//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
            locationClientOption.setIsNeedLocationPoiList(true);//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
            locationClientOption.SetIgnoreCacheException(false);//可选,默认false,设置是否收集CRASH信息,默认收集

            locationClientOption.setIsNeedAltitude(false);//可选,默认false,设置定位时是否需要海拔信息,默认不需要,除基础定位版本都可用
        }
        return locationClientOption;
    }

实现BDLocationListener接口

 /*****
     * 定位结果回调,重写onReceiveLocation方法
     *
     */
    private BDLocationListener mListener = new BDLocationListener() {

        @Override
        public void onReceiveLocation(BDLocation location) {
            // TODO Auto-generated method stub
            if (null != location ) {
                StringBuffer sb = new StringBuffer(256);
                sb.append("time : ");
                /**
                 * 时间也可以使用systemClock.elapsedRealtime()方法 获取的是自从开机以来,每次回调的时间;
                 * location.getTime() 是指服务端出本次结果的时间,如果位置不发生变化,则时间不变
                 */
                sb.append(location.getTime());
                sb.append("\nlocType : ");// 定位类型
                sb.append(location.getLocType());
                sb.append("\nlocType description : ");// *****对应的定位类型说明*****
                //sb.append(location.getLocTypeDescription());
                sb.append("\nlatitude : ");// 纬度
                sb.append(location.getLatitude());
                sb.append("\nlontitude : ");// 经度
                sb.append(location.getLongitude());
                sb.append("\nradius : ");// 半径
                sb.append(location.getRadius());
                sb.append("\nCountryCode : ");// 国家码
                sb.append(location.getCountryCode());
                sb.append("\nCountry : ");// 国家名称
                sb.append(location.getCountry());
                sb.append("\ncitycode : ");// 城市编码
                sb.append(location.getCityCode());
                sb.append("\ncity : ");// 城市
                sb.append(location.getCity());
                sb.append("\nDistrict : ");// 区
                sb.append(location.getDistrict());
                sb.append("\nStreet : ");// 街道
                sb.append(location.getStreet());
                sb.append("\naddr : ");// 地址信息
                sb.append(location.getAddrStr());
                sb.append("\nUserIndoorState: ");// *****返回用户室内外判断结果*****
                //sb.append(location.getUserIndoorState());
                sb.append("\nDirection(not all devices have value): ");
                sb.append(location.getDirection());// 方向
                sb.append("\nlocationdescribe: ");
                sb.append(location.getLocationDescribe());// 位置语义化信息
                sb.append("\nPoi: ");// POI信息
                if (location.getPoiList() != null && !location.getPoiList().isEmpty()) {
                    for (int i = 0; i < location.getPoiList().size(); i++) {
                        Poi poi = (Poi) location.getPoiList().get(i);
                        sb.append(poi.getName() + ";");
                    }
                }
                if (location.getLocType() == BDLocation.TypeGpsLocation) {// GPS定位结果
                    sb.append("\nspeed : ");
                    sb.append(location.getSpeed());// 速度 单位:km/h
                    sb.append("\nsatellite : ");
                    sb.append(location.getSatelliteNumber());// 卫星数目
                    sb.append("\nheight : ");
                    sb.append(location.getAltitude());// 海拔高度 单位:米
                    sb.append("\ngps status : ");
                    //sb.append(location.getGpsAccuracyStatus());// *****gps质量判断*****
                    sb.append("\ndescribe : ");
                    sb.append("gps定位成功");
                } else if (location.getLocType() == BDLocation.TypeNetWorkLocation) {// 网络定位结果
                    // 运营商信息
                    if (location.hasAltitude()) {// *****如果有海拔高度*****
                        sb.append("\nheight : ");
                        sb.append(location.getAltitude());// 单位:米
                    }
                    sb.append("\noperationers : ");// 运营商信息
                    sb.append(location.getOperators());
                    sb.append("\ndescribe : ");
                    sb.append("网络定位成功");
                } else if (location.getLocType() == BDLocation.TypeOffLineLocation) {// 离线定位结果
                    sb.append("\ndescribe : ");
                    sb.append("离线定位成功,离线定位结果也是有效的");
                } else if (location.getLocType() == BDLocation.TypeServerError) {
                    sb.append("\ndescribe : ");
                    sb.append("服务端网络定位失败,可以反馈IMEI号和大体定位时间到loc-bugs@baidu.com,会有人追查原因");
                } else if (location.getLocType() == BDLocation.TypeNetWorkException) {
                    sb.append("\ndescribe : ");
                    sb.append("网络不同导致定位失败,请检查网络是否通畅");
                } else if (location.getLocType() == BDLocation.TypeCriteriaException) {
                    sb.append("\ndescribe : ");
                    sb.append("无法获取有效定位依据导致定位失败,一般是由于手机的原因,处于飞行模式下一般会造成这种结果,可以试着重启手机");
                }
                tv_location.setText(sb+"\n定位结束");
                locationService.stop();
            }else{
                tv_location.setText("\n定位失败");
            }
        }

    };

通过上面的实现后,我们在想要定位的地方注册下回调,并调用start()方法即可以获取位置了,我对注册开始暂停做了下简单封装,具体代码参考LocationService。如果要写的项目里也要把回调接口封装,自定义一个接口回调返回定位后的详细位置信息。到这里即可成功定位了,下面就开始介绍下这个过程会出现的问题。

定位问题分析

在分析之前我们先看下百度定位返回的错误码,分析定位的问题也就是分析出现错误码的原因。

获取定位返回错误码::
public int getLocType ( )
返回值:
61 : GPS定位结果,GPS定位成功。
62 : 无法获取有效定位依据,定位失败,请检查运营商网络或者WiFi网络是否正常开启,尝试重新请求定位。
63 : 网络异常,没有成功向服务器发起请求,请确认当前测试手机网络是否通畅,尝试重新请求定位。
65 : 定位缓存的结果。
66 : 离线定位结果。通过requestOfflineLocaiton调用时对应的返回结果。
67 : 离线定位失败。通过requestOfflineLocaiton调用时对应的返回结果。
68 : 网络连接失败时,查找本地离线定位时对应的返回结果。
161: 网络定位结果,网络定位成功。
162: 请求串密文解析失败,一般是由于客户端SO文件加载失败造成,请严格参照开发指南或demo开发,放入对应SO文件。
167: 服务端定位失败,请您检查是否禁用获取位置信息权限,尝试重新请求定位。
502: AK参数错误,请按照说明文档重新申请AK。
505:AK不存在或者非法,请按照说明文档重新申请AK。
601: AK服务被开发者自己禁用,请按照说明文档重新申请AK。
602: key mcode不匹配,您的AK配置过程中安全码设置有问题,请确保:SHA1正确,“;”分号是英文状态;且包名是您当前运行应用的包名,请按照说明文档重新申请AK。
501700:AK验证失败,请按照说明文档重新申请AK。

其实知道上面错误码代表的含义后,我们就很快速的定位问题出现地方。当然有些时候不如此,可能需要走一些弯路。

505错误

在我升级定位SDK版本后遇到得到就是这个问题,没有更改任何代码但是就是一直返回错误码是505.通过上面错误码表我们看到时AK不存在或者非法,但是依然很纠结,因为代码时点儿也没有改,只是替换了jar和.so文件为最新版就不能用了。定位一直返回505,最后在官网更新日志看到V7.0版本有一条记录 是优化、完善AK校验机制,充分保证开发者合法权益,保证开发者应用的安全性。具体怎么优化并没有说明。不过也能猜测应该是SHA1的值问题。我先将demo用的定位SDK用V6.2.2(项目中用的此版本),然后更改SHA1的值,不管怎么改依然能成功定位。但是更改为了V7.0版本发现SHA1的值并不能随便改,只能是运行程序用的key文件的SHA1的值,否则就出现505错误。至此问题解决。在V7.0之前版本虽说让填写SHA1的值,但是并没有什么有效作用,在V7.0版本开始加入了严格的校验。在这里提供一个软件可以校验APK的SHA1值,他提供了SHA1的和AK的校验功能。
这里写图片描述
如上图,这上面显示的SHA1的值应该和你开发版或者发布版中至少其中的一个相同。否则V7.0定位就不会成功。校验工具百度网盘下载链接,提取码:je4r。

162错误

162错误一般是.so文件加载失败引起的。在AndroidStudio中.so文件的位置和Eclipse中的是不一样的。默认情况下,AndroidStudio中.so文件放在main目录下,在该文件夹下创建jniLibs,然后将不同内核的.so文件放到该文件夹下就可以了。当然一些人延续了Eclipse位置,将.so文件放置在libs目录下,如果此时没有其他一些配置.so文件是不能加载的。此时再gradle文件加入下面代码即可

 sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

这里写图片描述
其实最多出现的问题也就是这两种情况。正常情况下百度定位成功返回的是161也就是网络定位结果(如上图),但是有时候无网络会返回66机离线定位结果,离线定位是小区定位,需要手机手机中有SIM卡,否则不会返回66,你可以尝试下,把手机调到飞行模式,发现离线定位会失败。百度定位默认GPS定位是关闭的,如果想用GPS定位可以通过下面代码打开,

 locationClientOption.setOpenGps(true);

BDLocationListener只回调一次

对于很多刚接触定位的人可能还会遇到一个问题就是,为何多次调用start()方法但是BDLocationListener回调只执行一次。每次只要程序刚启动时才能定位成功。之后再定位就没有反应了。如果你第一次遇到这个问题,确实很棘手,不管怎么改定位相关的代码,并不能解决问题。其实此时只需要在清单文件加入下面代码既可以解决BDLocationListener只会回调一次的问题

        <service
            android:name="com.baidu.location.f"
            android:enabled="true"
            android:process=":remote" >
        </service>

好了,到此,本篇文章真的结束了,若文章有不足或者错误的地方,欢迎指正,以防止给其他读者错误引导

作者:xiehuimx 发表于2016/11/27 22:29:04 原文链接
阅读:37 评论:0 查看评论

iOS - runtime机制

$
0
0

开篇

学习一门编程语言。不仅仅是用它来做项目,要懂得它的原理,这样做心里踏实。想要更加深入的掌握OC或者做好iOS开发,runtime无疑是打开这个门的钥匙。

那儿_并不远

OC语言中的runtime机制

  • OC语言是一门动态语言,他将很多其他的静态语言在编译和连接时期做的事放在了运行时来处理。所以说OC不仅需要编译器,他还需要一个运行时系统来处理编译的代码,这个运行时系统就像一个操作系统一样,让所有的工作可以正常顺利的进行。
  • 运行时系统就是我们所说的运行时机制(Objc Runtime),它是一套由C语言和汇编语言开发的开源库,由很多数据结构和函数组成,通过runtime我们可以动态的修改类,对象中的属性或者方法,无论公有私有。

类和对象的基础数据结构

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;  // 指针,实例的isa指向类的对象,类对象的isa指向元类。

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;  //  父类 
    const char *name                                         OBJC2_UNAVAILABLE;  //  类名
    long version                                             OBJC2_UNAVAILABLE;  //  类的版本信息,默认为0
    long info                                                OBJC2_UNAVAILABLE;  //  类信息,供运行期使用的一些位标识
    long instance_size                                       OBJC2_UNAVAILABLE;  //  该类的实例变量大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;  //  该类的成员变量链表
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;  //  方法定义链表 
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;  //  方法缓存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;  //  协议链表
#endif

} OBJC2_UNAVAILABLE;

在上述的runtime封装的类的数据结构中通常我们会对一下几个变量感兴趣:
* isa : 学习过C语言的同学知道指针的作用,指针其实就是我们将数据存放到内存中的地址,同理isa就是我们创建的类或者对象存放在内存中的地址(在这里我们需要注意,在OC中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)),当我们向一个OC对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类,Runtime库会在类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法.找到后即运行这个方法.
* cache : 用于缓存我们最近调用的方法,当我们调用一个对象的方法时,会根据isa指针找到这个对象,然后遍历方法链表,但是,一个对象中会存在很多的方法,很多都是很少用的,如果每调用一次对象的方法都去遍历一次方法链表,势必会降低性能,所以这是,我们把常用的方法缓存到cache中,这样每次调用方法时,runtime会优先到cache中查找,如果没有找到再去遍历方法链表。

获取属性,变量,方法,协议链表

  • 这里先创建了一个Person类,下面分别为Person.h,Person.m文件
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, copy) NSString *age;

- (void)sayHi;
@end
#import "Person.h"
#import <objc/runtime.h>

@interface Person ()
@property (nonatomic, copy) NSString *privacy;
@end

@implementation Person

- (void)sayHi {
    NSLog(@"my name is coco");
}

 
}

@end
  • 接下来我们在来获取Person中链表
// 获取属性
- (void)runtimeTestForProperName {
    unsigned int count;
    objc_property_t *properList = class_copyPropertyList([Person class], &count);
    for (unsigned i = 0; i < count; i++) {
        const char *properName = property_getName(properList[i]);
        NSLog(@"属性名称 %@", [NSString stringWithUTF8String:properName]);
    }
}
// 获取变量
- (void)runtimeTestForIvar {
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([Person class], &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *ivarName = ivar_getName(ivarList[i]);
        NSLog(@"成员变量 %@", [NSString stringWithUTF8String:ivarName]);
    }
}
// 获取方法
- (void)runtimeForMethod {
    unsigned int count;
    Method *methodList = class_copyMethodList([Person class], &count);
    for (unsigned int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL methodName = method_getName(method);
        NSLog(@"方法名称 %@", NSStringFromSelector(methodName));
    }
}
// 获取协议
- (void)runtimeForProtocol {
    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([Person class], &count);
    for (unsigned int i; i<count; i++) {
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"协议方法 %@", [NSString stringWithUTF8String:protocolName]);
    }
}

动态添加属性

  • 向person对象中添加school属性
- (void)setupProperty {
 
    /** objc_setAssociatedObject: 方法的参数说明
     object: 添加的对象
     key: 添加的属性名称(key值,获取时会用到)
     value: 添加对应的属性值 (value值)
     policy: 添加的策略(属性对应的修饰)
     **/
    Person *person = [[Person alloc] init];
    objc_setAssociatedObject(person, @"school", @"吉林师范", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSString *associatedValue = objc_getAssociatedObject(person, @"school");
    NSLog(@"设置的属性 :%@", associatedValue);
    
}

其中 我们可以点进policy这个参数中,我们可以清除的看到这是关于我们定义属性的时候用到的几种修饰。

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,      
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  
    OBJC_ASSOCIATION_RETAIN = 01401,  
    OBJC_ASSOCIATION_COPY = 01403
};
  • 上述是向现有类中添加属性,如果你查看runtime的API你就会发现,还有一个 class_addIvar:方法可以添加变量,但是这个方法不支持现有类的添加,官方是这样说的
// 此方法仅可在objc_allocateclasspair方法和objc_registerclasspair之间使用
* This function may only be called after objc_allocateClassPair and before objc_registerClassPair. 
// 不支持在现有类中添加一个实例变量
 * Adding an instance variable to an existing class is not supported.

接下来我们通过动态创建一个类,并向其中添加变量。看代码

- (void)createMyClass
{

    /** objc_allocateClassPair: 参数说明
     superclass: 父类
     name: 类名
     extraButes: 额外分配的字节(写0就可以)
     **/

    /** class_addIvar: 参数说明
     cls: 类
     name: 变量名
     size: 变量的所占内存的字节
     alignment: 对齐方式(写0就可以)
     types: 变量类型
     **/

    // rumtime 动态创建一个类
    Class MyClass = objc_allocateClassPair([NSObject class], "myclass", 0);
    / /  添加一个NSString的变量
    if (class_addIvar(MyClass, "motto", sizeof(NSString *), 0, "@")) {
        NSLog(@"add ivar success");
    }
    //  注册这个类到runtime系统中才可以使用它
    objc_registerClassPair(MyClass);

    //  生成了一个实例化对象
    id myobj = [[MyClass alloc] init];
    NSString *string = @"it's my live";
    //   赋值
    [myobj setValue: string forKey:@"motto"];
    Ivar ivarOFset = class_getInstanceVariable([myobj class], "motto");
    id value = object_getIvar(myobj, ivarOFset);
    NSLog(@"添加的变量的值  = %@", value);
}

动态添加方法

- (void)setupMethod {

    /** class_addMethod: 参数说明
     cls: 类
     name: 方法名
     imp: 方法的地址
     types: 方法类型
     **/    

   // 获取 song 方法的类型
    Method method = class_getInstanceMethod([self class], @selector(song));
    const char *methodType = method_getTypeEncoding(method);
    NSLog(@"参数类型 %@", [NSString stringWithUTF8String:methodType]);

    // 添加 song 方法
    class_addMethod([Person class], @selector(song), class_getMethodImplementation([self class], @selector(song)), methodType);
    // 创建实例对象
    Person *person = [[Person alloc] init];
    // 调用添加的方法
    [person performSelector:@selector(song) withObject:nil];

}

- (void)song {
    NSLog(@"大家好,为大家来一首 - 《那就这样吧》");  
}

通过ruantime交换,替换方法

  • 交换两个方法(方法A,方法B),当两个方法交换成功后,调用方法A后就会执行方法B中的代码。
  • 替换方法,很好理解,就是将方法A替换成方法B。

* 无论是交换还是替换方法,实质rumtime就是将方法的指针做了交换或者替换 *

- (void)exchangeMethod {

    // 获取两个方法的Method
    Method funcA = class_getInstanceMethod([self class], @selector(functionA));
    Method funcB = class_getInstanceMethod([self class], @selector(functionB));
    // 交换两个方法
    method_exchangeImplementations(funcA, funcB);
    [self functionA];
    [self functionB];

    /** 官方文档是这样介绍 method_exchangeImplementations 方法实现的:
    * @note This is an atomic version of the following:
    *  \code 
    *  IMP imp1 = method_getImplementation(m1);
    *  IMP imp2 = method_getImplementation(m2);
    *  method_setImplementation(m1, imp2);
    *  method_setImplementation(m2, imp1);
    *  \endcode
     **/

}

- (void)replaceMethod {

    Method funcB = class_getInstanceMethod([self class], @selector(functionB));
   // 将方法A替换成方法B
    class_replaceMethod([self class], @selector(functionA), method_getImplementation(funcB), method_getTypeEncoding(funcB));
    [self functionA];
    [self functionB];

}

- (void)functionA {
    NSLog(@"我是方法A");
}

- (void)functionB {
    NSLog(@"我是方法B");
}

总结

通过上述的介绍我想大家对runtime有了一个初步的了解,同时对于OC也会又一个更深的认识。上面代码的运行结果我就不给大家一一截图了,我都是验证后才写在博客上的。runtime中还有很多方法,但是都是大同小异,用法都是可以举一反三的。希望这篇博客会对大家有所启发,同时欢迎大家提出建议和不同的看法,见解,分享可以让我们共同进步。

作者:xurimingyue123 发表于2016/11/27 23:32:18 原文链接
阅读:24 评论:0 查看评论

Android 项目构建过程

$
0
0

Android项目构建过程:平常开发中使用ide可以很方便构建打包编译成一个Apk包安装到手机,那么整个流程到底是啥样的呢?
我们可以根据Google官方提供的流程图来具体了解构建的过程, 构建工具放在Android sdk目录下面的build-tools文件夹下:
这里写图片描述

下面是具体描述:

1.AAPT(Android Asset Packaging Tool)工具会打包应用中的资源文件,如AndroidManifest.xml、layout布局中的xml等,并将xml文件编译为二进制形式,当然assets文件夹中的文件不会被编译,图片及raw文件夹中的资源也会保持原来的形态,需要注意的是raw文件夹中的资源也会生成资源id。AAPT编译完成之后会生成R.Java文件。
详细命令参考: aapt 命令可应用于查看apk包名、主activity、版本等很多信息

1.  aapt l[ist] [-v] [-a] file.{zip,jar,apk} 列出压缩文件目录
2. aapt d[ump] [--values] WHAT file.{apk} [asset [asset ...]]
3.查看apk包的packageName、versionCode、applicationLabel、launcherActivity、permission等各种详细信息
aapt dump badging <file_path.apk>
...

2.AIDL工具会将所有的aidl接口转化为java接口。

Android:学习AIDL,这一篇文章就够了

1.AIDL是什么?
///AIDL是一个缩写,全称是Android Interface Definition Language,也就是Android接口定义语言。
2.AIDL是如何工作的?
///一类是用来定义方法接口,以供系统使用来完成跨进程通信的
///一类是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型
使用:
第一块是 初始化 。在 onCreate() 方法里面我进行了一些数据的初始化操作。第二块是 重写 BookManager.Stub 中的方法 。在这里面提供AIDL里面定义的方法接口的具体实现逻辑。第三块是 重写 onBind() 方法 。在里面返回写好的 BookManager.Stub 。
3.为什么设计AIDL?
///AIDL目的是为了实现 进程间通信 。

客户端:调用AIDL-->客户端与服务端交互-->IBinder 的一个代理类Proxy-->transact()方法-->将客户端的数据和请求发送到服务端去。

服务端:onTransact()方法
1,获取客户端传过来的数据,根据方法 ID 执行相应操作。
2,将传过来的数据取出来,调用本地写好的对应方法。
3,将需要回传的数据写入 reply 流,传回客户端。

3.所有的java代码,包括R.java与aidl文件都会被Java编译器编译成.class文件。

4.Dex工具会将上述产生的.class文件及第三库及其他.class文件编译成.dex文件(dex文件是Dalvik虚拟机可以执行的格式),dex文件最终会被打包进APK文件。

5.ApkBuilder工具会将编译过的资源及未编译过的资源(如图片等)以及.dex文件打包成APK文件。

6.生成APK文件后,需要对其签名才可安装到设备,平时测试时会使用debug keystore,当正式发布应用时必须使用release版的keystore对应用进行签名。

7.如果对APK正式签名,还需要使用zipalign工具对APK进行对齐操作,这样做的好处是当应用运行时会提高速度,但是相应的会增加内存的开销。

参考: Android应用程序Zipalign化

zipalign使用了4字节的边界对齐方式来映射内存,通过空间换时间的方式提高执行效率。
//zipalign优化的最根本目的是帮助操作系统更高效率的根据请求索引资源,将resource-handling code统一将Data structure alignment(数据结构对齐标准:DSA)限定为4-byte boundaries。如果第一次接触有关Data structurealignment的内容,强烈建议搜索更多与其相关的内容来充分理解这样做的最终目的,这也是理解zipalign工作原理的关键。 如果不采取对齐的标准,处理器无法准确和快速的在内存地址中定位相关资源。

zipalign位于android sdk的tools文件夹下面所以cmd进入命令行模式进入到tools目录:执行
//zipalign -v 4 source.apk destination.apk(目标apk)
其中这里-v代表详细输出, 4代表对齐为4个字节
//zipalign -c -v 4 destination.apk 
来查看是否成功的优化了你的apk文件,这里-c参数代表检查对齐,可以看作是只读执行,最后提示大家这步可能会造成文件签名问题,注意和apk签名执行的顺序。

数据的字节对齐(data structure alignment)

1. 为什么需要字节对齐

1、处理器的差异
2、出于CPU读取内存数据效率的考虑。

2.结构对齐的基本原则:

1、结构体对齐的字节数是此结构体所有成员对齐的字节数中的最大值。
2、结构体每一个成员自身要保持字节对齐。
3、如果结构体中包含嵌套子结构体,那么子结构体自身必须对齐。
4、对于整个结构体,要根据规则1中“结构体对齐的字节数”满足字节对齐,对于没有对齐的情况,需要在最后增加pading。

C语言中,基本数据类型默认的对齐方式:

类型 长度(字节) 对齐方式
Cha 1 1字节对齐
short 2 2字节对齐
int 4 4字节对齐
long 4 4字节对齐
Float 4 4字节对齐
double 8 8字节对齐(windows);4字节对齐(Linux)
指针 4 4字节对齐

从上面的结构体规则以及表格的基本数据类型对其字节数了解为什么android zipAlign采用4字节为边界对齐。

如何用提问方式促进深度思考?

最后看到一篇文章:用提问来促进思维——谈 WHAT HOW WHY 三部曲

WHAT HOW WHY 的步骤:
1. WHAT(相当于初级工程师–攻克初级使用层次~)
当我们开始了解某个事物的时候,总是先从 WHAT 类型的问题开始入手。而所谓的【WHAT】也就是“What is it?”

从 WHAT 类型的问题得到的答案,通常只反映出事情的表面现象。而表象跟本质,往往是不同的,甚至是相反的。如果你仅仅停留在 WHAT 层面,很可能会被表象所误导。

2.HOW(相当于中级工程师–攻克原理性层次~)
所谓的【HOW】就是“How to do?”。

和 WHAT 层次不同的是,HOW 层次光靠记忆力是远远不够滴。你还需要具备一定的分析推理能力,还需要懂得查阅相关资料,才能搞明白上述 HOW 类型的问题。

3.WHY(相当于高级工程师–攻克系统层次~)
当你能够比较透彻地厘清 HOW 层面的各类问题,就可以开始思考 WHY 类型的问题了。

WHY 类型的问题比 HOW 类型的问题更难回答——在回答 WHY 类型问题的过程中,你不但需要动用分析、推理、归纳、总结等各种思维能力,可能还要运用到跨领域、跨学科的知识。但是收获也是很大的。一旦把这些 WHY 类型的问题想明白,你就对整个事件有一个既宏观又深刻的认识。能达到这个境界,看问题通常会比较深刻。可惜这样的人比例太低了。
作者:qq_16318981 发表于2016/11/27 23:57:18 原文链接
阅读:20 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>