注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

从retrofit来学动态代理

个人感觉,retrofit中的动态代理比较典型,我就拿出来解读一下:先来阅读一下retrofit 的源码,看retrofit怎么来实现动态代理ApiService apiService = retrofit.create(ApiService.class); ...
继续阅读 »

个人感觉,retrofit中的动态代理比较典型,我就拿出来解读一下:

先来阅读一下retrofit 的源码,看retrofit怎么来实现动态代理

ApiService apiService = retrofit.create(ApiService.class);

public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();

@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}

retrofit这段代码主要作用是将类里的注解等参数解析,并包装成网络请求真正的数据,来进行请求数据。

咱模仿retrofit写一套动态代理:

定义注解:

@LeftFace

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LeftFace {
String value() default "左面脸";
}


@UpFace

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UpFace {
String value() default "上面脸";
}
创建接口

public interface IFaceListener {

@LeftFace
String getFace(String name);

@UpFace
String getFacePoint(String name);
}

创建动态代理

public class FaceCreate {

public <T> T create(final Class<T> face){
return (T) Proxy.newProxyInstance(face.getClassLoader(), new Class<?>[]{face}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String result = null;
if(method.isAnnotationPresent(LeftFace.class)){
LeftFace leftFace = method.getAnnotation(LeftFace.class);
result = leftFace.value();
}

if(method.isAnnotationPresent(UpFace.class)){
UpFace upFace = method.getAnnotation(UpFace.class);
result = upFace.value();
}

result = HString.concatObject(null,args)+result;
return result;
}
});
}

}

如此我们就模仿的建造了动态代理,动态代理在开发中相对与静态代理,灵活性更强。

解析 

new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) 


Object proxy:我们的真实对象
Method method:对象的方法 
Object[] args:对象的参数

Proxy.newProxyInstance(face.getClassLoader(), new Class<?>[]{face}, new InvocationHandler() {


ClassLoader loader:定义了由哪个ClassLoader对象来对生成的代理对象进行加载

Class<?>[] interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了

InvocationHandler :InvocationHandler对象


收起阅读 »

你真的会用单例么?

单例是什么?是一种对象创建模式,可以确保项目中一个类只产生一个实例。好处对于频繁使用的对象可以减少创建对象所花费的时间,这对于重量级对象来说,简直是福音。由于new的减少,对系统内存使用频率也会降低,减少GC的压力,并缩短GC停顿时间,这也会减少Android...
继续阅读 »

单例是什么?

是一种对象创建模式,可以确保项目中一个类只产生一个实例。

好处

对于频繁使用的对象可以减少创建对象所花费的时间,这对于重量级对象来说,简直是福音。由于new的减少,对系统内存使用频率也会降低,减少GC的压力,并缩短GC停顿时间,这也会减少Android项目的UI卡顿。

如何实现单例

1、饿汉模式

public class TestSingleton {

private static final TestSingleton testSingleton = new TestSingleton();

private TestSingleton(){

}

public static TestSingleton getInstance(){
return testSingleton;
}

}

细节我就不多写了,大家都应该知道,构造函数为private,用getInstance来获取实例

2.、懒汉模式

public class TestSingleton {

private static TestSingleton testSingleton;

private TestSingleton(){

}

public static TestSingleton getInstance(){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
return testSingleton;
}

}

比饿汉式的优点在于用时再加载,比较重量级的单例,就不适用与饿汉了。

3、线程安全的懒汉模式

public class TestSingleton {

private static TestSingleton testSingleton;

private TestSingleton(){

}

public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
testSingleton = new TestSingleton();
}
}
return testSingleton;
}

}

可以看到的是比上面的单例多了一个对象锁,着可以保证在创建对象的时候,只有一个线程能够创建对象。

4、线程安全的懒汉模式-DCL双重检查锁机制

public class TestSingleton {

private static volatile TestSingleton testSingleton;

private TestSingleton(){

}

public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
}
}
return testSingleton;
}

}

双重检查,同步块加锁机制,保证你的单例能够在加锁后的代码里判断空,还有增加了一个volatile 关键字,保证你的线程在执行指令时候按顺序执行。这也是市面上见的最多的单例。

敲黑板!!知识点:原子操作、指令重排。

什么是原子操作?

简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。

m = 6; // 这是个原子操作

假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

而,声明并赋值就不是一个原子操作:

int n = 6; // 这不是一个原子操作

对于这个语句,至少有两个操作:

  1. 声明一个变量n
  2. 给n赋值为6

这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。

在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。

什么是指令重排?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。

int a ;   // 语句1 

a = 8 ; // 语句2

int b = 9 ; // 语句3

int c = a + b ; // 语句4

正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。

但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。

由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。

也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

主要在于testSingleton = new TestSingleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 testSingleton 分配内存
  2. 调用 testSingleton 的构造函数来初始化成员变量,形成实例
  3. 将testSingleton 对象指向分配的内存空间(执行完这步 testSingleton 才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 testSingleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

--------------------------------------------一部分的文章可能讲到如上就嘎然而止了----------------------------------------

推荐后两种

5、静态内部类来实现单例

public class TestSingleton {

private TestSingleton(){

}

public static TestSingleton getInstance(){
return TestSingletonInner.testSingleton;
}

private static class TestSingletonInner{
static final TestSingleton testSingleton = new TestSingleton();
}

}

static 保证数据独一份

final 初始化完成后不能被修改,线程安全。

敲黑板!!知识点:java在加载类的时候不会将其内部的静态内部类加载,只有在使用该内部类方法时才被调用。这明显是最好的单例,并不需要什么锁一类的机制。

利用了类中静态变量的唯一性

优点:

  1. jvm本身机制保证线程安全。
  2. synchronized 会导致性能问题。
  3. TestSingletonInner 是私有的,除了通过TestSingleton 访问,没有其他访问的可能性。

6、枚举单例

public enum  TestSingleton {

INSTANCE;

public void toSave(){
}

}

使用TestSingleton.INSTANCE.toSave();

创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。如果你要自己添加一些线程安全的方法,记得控制线程安全哦。

优点:写法简单/线程安全

Android中的单例实际应用

1、application

本身就是单例,生命周期为整个程序的生命周期,可以通过这个特性,能够用来存储一些数据

2、单例模式引起的内存泄漏

在使用Context注意用application中的context

收起阅读 »

iOS 实例对象,类对象,元类对象

OC对象的分类OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)实例对象:实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据...
继续阅读 »

OC对象的分类

OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)

  • 实例对象:

    实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:

        NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];

NSLog(@"instance----%p %p",objc1,objc2);
输出结果:



instance实例对象存储的信息:
1.isa指针

2.其他成员变量


  • 我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址
  • 类对象

  • 类对象的获取
        Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

打印结果

2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140

从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个


  • 类对象的作用

类对象存储的信息:
1.isa指针
2.superclass指针
3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)




元类对象

1.元类对象的获取

        Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);

打印指针地址

NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个

特别注意一点:

Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];

元类存储结构:
元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
1.isa指针
2.superclass指针
3.类方法(即加号方法)




从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空

  • 判断是否为元类
    class_isMetaClass(objcL);
收起阅读 »

iOS 对象的关联---isa/superclass指针

类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联实例对象的isa指针当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用...
继续阅读 »

类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联




  • 实例对象的isa指针

    • 当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用
  • 类对象的isa指针

    • 类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用




二.类对象的superclass 指针

  • 先两个类,一个Person继承自NSObject,一个类继承自Person
/// Person继承自NSObject
@interface Person : NSObject
-(void)perMethod;
+(void)perEat;
@end

@implementation Person

-(void)perMethod{

}
+(void)perEat{

}

@end



/// student继承自Person
@interface Student : Person
-(void)StudentMethod;
+(void)StudentEat;
@end
@implementation Student

-(void)StudentMethod{

}
+(void)StudentEat{

}

  • 当实例对象调用自身的对象方法时,它在自身的class对象中找到StudentMethod方法

        Student *student = [[Student alloc]init];
[student StudentMethod]
  • 当实例对象调用父类的方法的时候
        Student *student = [[Student alloc]init];
[student perMethod];



当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)

三.元类对象的superclass 指针

当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用

[Student perEat];



总的来说,isa,superclass的的关系可以用一副经典的图来表示




instance的isa指向class

class的isa指向meta-class

meta-class的isa指向基类的meta-class

class的superclass指向父类的class



作者:枫紫_6174
链接:https://www.jianshu.com/p/ffb021a4b97c
收起阅读 »

iOS KVO底层原理&&KVO的isa指向

一.简单复习一下KVO的使用定义一个类,继承自NSObject,并添加一个name的属性#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TCPerson ...
继续阅读 »

一.简单复习一下KVO的使用

  • 定义一个类,继承自NSObject,并添加一个name的属性
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END
  • 在ViewController我们简单的使用一下KVO
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

当点击屏幕的时候,控制台输出

2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到<TCPerson: 0x600003444d10>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二.深入剖析KVO的底层

在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}我们知道self.person1.name的本质是[self.person1 setName:@"cang lao shi"];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}

在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法

- (void)setName:(NSString *)name{
_name = name;
}
  • 在ViewController我们新建一个person2,代码变成了:
#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

  • 注意:当我们点击屏幕的时候输出的结果是:
2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到<TCPerson: 0x600002ce8230>对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

 
既然我们改变name的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?

三.KVO的isa指向

既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:
打开lldb

(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
  • 从上面的打印我们看到 self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson
  • NSKVONotifying_TCPerson是runtime动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面代码不能在xcode中运行):

  • #import "NSKVONotifying_TCPerson.h"

    @implementation NSKVONotifying_TCPerson
    //NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
    - (void)setName:(NSString *)name{
    _NSSetIntVaueAndNotify();
    }
    //改变过程
    void _NSSetIntVaueAndNotify(){
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
    }
    //通知观察者
    - (void)didChangeValueForKey:(NSString *key){
    [observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context];
    }
    @end
    未添加观察self.person2实例对象的isa指向流程图:



    添加观察self.person1实例对象的isa指向流程图



    所以KVO其本质是动态生成一个NSKVONotifying_TCPerson类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法


    作者:枫紫_6174
    链接:https://www.jianshu.com/p/0b6083b91b04
    收起阅读 »

    iOS 音视频编解码----H264-I(关键)帧,B/P(参考)帧

    回顾一下视频里面到底是什么内容元素1.图像(image)2.音频(Audio)3.元素信息(Meta-data)编码格式1.Video:H2642.Audio:AAC(后面文章讲)3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对...
    继续阅读 »

    回顾一下视频里面到底是什么



  • 内容元素

    1.图像(image)
    2.音频(Audio)
    3.元素信息(Meta-data)

  • 编码格式

    1.Video:H264
    2.Audio:AAC(后面文章讲)
    3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI

  • H264

    当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。

  • 编码的本质



  • 为什么要编码?

    举个例子: (具体大小我没数过,只做讲解参考)
    你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
    ----------------------23 * 2 + 10 = 56个字符--------------------------
    你的老公将于明天晚上6点零5分在江北机场接你
    ----------------------20 * 2 + 2 = 42个字符----------------------------
    Helen将于明天晚上6点在机场接你
    ----------------------10 * 2 + 2 = 26个字符----------------------------

    相信大家看了这个例子之后,心里应该大概明白编码的本质:只要不接收方不会产生误解,就可以产生数据的承载量,编码视频的本质也是如此,编码的本质就是减少数据的冗余

    • 在引入I(关键)帧,B/P(参考)帧之前,我们先来了解一下人眼和视频摄像头的对帧的识别

      我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同的数据,对于这些相同或者不同的数据的处理过程,就是编码





    I帧(I-frames,也叫关键帧)

    • 我们知道在视频的传输过程中,它是分片段传输的,这个片段的第一帧,也就是第一张图片,就是I帧,也就是传说中的关键帧
    • I帧:也就是关键帧,帧内压缩(也就是压缩独立视频帧,被称为帧内压缩),帧内压缩通过消除包含在每个独立视频帧内的色彩以及结构中的冗余信息来进行压缩,因此可在不降低图片适量的情况下尽可能的缩小尺寸,这类似于JEPG压缩的原理,帧内压缩也可以称为有损压缩算法,但通常用于对原因图片的一部分进行处理,以生成极高质量的照片,通过这一过程的创建的帧称为I-frames;将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

    P帧(P-frames,又称为预测帧)

    • P帧:向前参考帧(在I帧(关键帧)后,P帧参考关键帧,保存下一帧和前一帧的不同数据).压缩时只参考前一个帧.属于帧间压缩技术.
    • 帧间压缩技术:很多帧被组合在一起作为一组图片(简称GOP),对于GOP所存在的时间维度的冗余可以被消除,如果想象视频文件中的典型场景,就会有一些特定的运动元素的概念,比如行驶中的汽车或者街道上行走的路人,场景的背景环信通道是固定的,或者在一定的时间内,有些元素的改变很小或者不变,这些数据就称为时间上的冗余,这些数据就可以通过帧间压缩的方式进行消除,也就是帧间压缩,视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.这就是P帧,保存前后两帧不通的数据

    B帧(B-frames,又称为双向帧)

    • B帧,又叫双向帧,它是基于使用前后两帧进行编码后得到的帧,几乎不需要存储空间,但是解压过程会消耗较长的时间,因为它依赖周围其他的帧
    • B帧的特点:B帧使得视频的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

    GOP一组帧的理解

    • 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.

    • 什么叫一组帧?
      就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP




    • 视频花屏/卡顿原因

      • 我们平常在观看视频的时候,出现视频的花屏或者卡顿,第一反应就是我们的网络出现了问题,其实我们的网络没有问题,是我们在解码的时候I帧,B/P帧出现了丢失
      • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
      • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
      • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/94d2a8bbc3ac


    收起阅读 »

    requestLayout竟然涉及到这么多知识点

    1. 背景 最近有个粉丝跟我提了一个很有深度的问题。 粉丝:锁屏后,调用View.requestLayout()方法后会不会postSyncBarrier? 乍一看有点超纲了。细细一想,我把这个问题拆分成了两个问题,本文我将紧紧围绕这两个问题,讲解requ...
    继续阅读 »

    1. 背景


    最近有个粉丝跟我提了一个很有深度的问题。



    粉丝:锁屏后,调用View.requestLayout()方法后会不会postSyncBarrier?



    乍一看有点超纲了。细细一想,我把这个问题拆分成了两个问题,本文我将紧紧围绕这两个问题,讲解requestLayout背后的故事。



    其一:锁屏后,调用View.requestLayout(),会往上层层调用requestLayout()吗?


    其二:锁屏后,调用View.requestLayout(),会触发View的测量和布局操作吗?



    postSyncBarrier我知道,Handler的同步屏障机制嘛,但是锁屏之后为什么还要调用requestLayout()呢?于是我脑补了一个场景。



    假设在Activity onResume()中每隔一秒调用View.requestLayout(),但是在onStop()方法中没有停止调用该方法。当用户锁屏或者按Home键时。



    我脑补的这个场景,用罗翔老师的话来讲是 “法律允许,但是不提倡”。当Activity不在前台的时候,就应该把requestLayout()方法停掉嘛,我们知道的,这个方法会从调用的View一层一层往上调用直到ViewRootImpl.requestLayout()方法,然后会从上往下触发View的测量和布局甚至绘制方法。非常之浪费嘛!错误非常之低级!但是果真如此吗?


    电竞主播芜湖大司马,有一句网络流行语你以为我在第一层,其实我在第十层。下面我将用层级来表示对requestLayout方法的了解程度,层级越高,表示了解越深刻。


    了解我的粉丝都知道,我喜欢用树形图来分析Android View源码。上图:


    2. 第一层(往上,层层遍历)


    假设调用I.requestLayout(),会触发哪些View的requestLayout方法?


    答:会依次触发I.requestLayout() -> C.requestLayout() -> A.requestLayout() -> ...省略一些View -> ViewRootImpl.requestLayout()


    //View.java
    public void requestLayout() {
    // 1. 清除测量记录
    if (mMeasureCache != null) mMeasureCache.clear();

    // 2. 增加PFLAG_FORCE_LAYOUT给mPrivateFlags
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    // 3. 如果mParent没有调用过requestLayout,则调用之。换句话说,如果调用过,则不会继续调用
    if (mParent != null && !mParent.isLayoutRequested()) {
    mParent.requestLayout();
    }
    }
    复制代码

    该方法作用如下:



    1. 清除测量记录

    2. 增加PFLAG_FORCE_LAYOUT给mPrivateFlags

    3. 如果mParent没有调用过requestLayout,则调用之。换句话说,如果调用过,则不会继续调用


    重点看下mParent.isLayoutRequested()方法,它在View.java中有具体实现


    //View.java
    public boolean isLayoutRequested() {
    return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }
    复制代码

    如果mPrivateFlags增加PFLAG_FORCE_LAYOUT标志位,则认为View已经请求过布局。由前文可知,在requestLayout的第二步会增加该标志位。熟悉位操作的朋友就会知道,有增加操作就会有对应的清除操作。 经过一番搜索,找到:


    //View.java
    public void layout(int l, int t, int r, int b) {
    // ... 省略代码
    //在View调用完layout方法,会将PFLAG_FORCE_LAYOUT标志位清除掉
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    // ... 省略代码
    }
    复制代码

    在View调用完layout方法,会将PFLAG_FORCE_LAYOUT标志位清除掉。当View下次再调用requestLayout方法时,依旧能往上层层调用。但是如果当layout()方法没有执行时,下次再调用requestLayout方法时,就不会往上层层调用了。


    所以先回答文章开始的第一个问题:



    其一:锁屏后,调用View.requestLayout(),会往上层层调用requestLayout()吗?


    答:锁屏后,除了第一次调用会往上层层调用,其它的都不会




    为什么,只有第一次调用会呢?那必定是因为layout方法没有得到执行,导致PFLAG_FORCE_LAYOUT无法被清除。欲知后事,接着往下看呗



    如果你知道requestLayout调用是一个层级调用,那么恭喜你,你已经处于认知的第一层了。送你一张二层入场券。

    3. 第二层(ViewRootImpl.requestLayout)


    我们来看看第一层讲到的ViewRootImpl.requestLayout()


    //ViewRootImpl.java
    @Override
    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
    checkThread();
    mLayoutRequested = true;
    scheduleTraversals();
    }
    }

    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    mTraversalScheduled = true;
    //1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    //2. 将mTraversalRunnable保存到Choreographer中
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }
    复制代码

    该方法主要作用如下:



    1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息

    2. 将mTraversalRunnable保存到Choreographer中


    此处有三个特别重要的知识点:



    1. mTraversalRunnable

    2. MessageQueue的同步屏障

    3. Choreographer机制


    mTraversalRunnable相对比较简单,它的作用就是从ViewRootImpl 从上往下执行performMeasure、performLayout、performDraw。[重点:敲黑板]它的执行时机是当Vsync信号来到时,会往主线程的Handler对应的MessageQueue中发送一条异步消息,由于在scheduleTraversals()中给MessageQueue中发送过一条同步屏障消息,那么当执行到同步屏障消息时,会将异步消息取出执行


    4. 第三层(TraversalRunnable)


    当vsync信号量到达时,Choreographer会发送一个异步消息。当异步消息执行时,会调用ViewRootImpl.mTraversalRunnable回调。


    final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
    doTraversal();
    }
    }
    复制代码

    void doTraversal() {
    if (mTraversalScheduled) {
    mTraversalScheduled = false;
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

    if (mProfile) {
    Debug.startMethodTracing("ViewAncestor");
    }

    performTraversals();

    if (mProfile) {
    Debug.stopMethodTracing();
    mProfile = false;
    }
    }
    }
    复制代码

    它的作用:



    1. 移除同步屏障

    2. 执行performTraversals方法


    performTraversals()方法特别复杂,给出伪代码如下


    private void performTraversals() {
    if (!mStopped || mReportNextDraw) {
    performMeasure()
    }

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
    performLayout(lp, mWidth, mHeight);
    }

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
    performDraw();
    }
    }
    复制代码

    该方法的作用:



    1. 满足条件的情况下调用performMeasure()

    2. 满足条件的情况下调用performLayout()

    3. 满足条件的情况下调用performDraw()


    mStopped表示Activity是否处于stopped状态。如果Activity调用了onStop方法,performLayout方法是不会调用的。


    //ViewRootImpl.java
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
    int desiredWindowHeight) {
    // ... 省略代码
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    // ... 省略代码
    }
    复制代码

    回答文章开始的第二个问题:



    其二:锁屏后,调用View.requestLayout(),会触发View的测量和布局操作吗?


    答:不会,因为当前Activity处于stopped状态了



    至此第一层里面留下的小悬念也得以解开,因为不会执行View.layout()方法,所以PFLAG_FORCE_LAYOUT不会被清除,导致接下来的requestLayout方法不会层层往上调用。


    至此本文的两个问题都已经得到了答案。


    当我把问题提交给鸿洋大佬的wanandroid上时,大佬又给我提了一个问题。



    鸿洋大佬:既然Activity的onStop会导致requestLayout layout方法得不到执行,那么onResume方法会不会让上一次的requestLayout没有执行的layout方法执行一次呢?



    于是我写了个demo来验证


    //MyDemoActivity.kt
    override fun onStop() {
    super.onStop()
    root.postDelayed(object : Runnable {
    override fun run() {
    root.requestLayout()
    println("ChoreographerActivity reqeustLayout")
    }
    }, 1000)
    }
    复制代码

    在自定义布局的onLayout方法中打印日志


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    System.out.println("ChoreographerActivity onLayout");
    super.onLayout(changed, left, top, right, bottom);
    }
    复制代码

    锁屏,1s后调用requestLayout,日志没有打印,1s后亮屏,发现日志打印了。


    所以



    鸿洋大佬:既然Activity的onStop会导致requestLayout layout方法得不到执行,那么onResume方法会不会让上一次的requestLayout没有执行的layout方法执行一次呢?


    我:经过demo验证会。原因且听我道来



    有了demo找原因就很简单了。正面不好攻破,那就祭出调试大法呗。但是断点放在哪好呢?思考了一番。我觉得断点放在发送同步屏障的地方比较好,ViewRootImpl.scheduleTraversals()。为什么断点放这里?(那你就得了解同步屏障和vsync刷新机制了,后文会讲) 


    亮屏后,发现断点执行了。从堆栈中可以看出Activity的performRestart()方法执行了ViewRootImpl的scheduleTraversals方法。

    虽然,亮屏的时候没有执行View.requestLayout方法,由于锁屏后1s执行了View.requestLayout方法,所以PFLAG_FORCE_LAYOUT标记位还是有的。亮屏调用了performTraversals方法时,会执行Measure、Layout、Draw等操作。


    至此,完美回答了粉丝和鸿洋大佬的问题

    5. 第四层(Handler同步屏障)


    Handler原理,也是面试必问的问题。涉及到很多知识点。线程、Looper、MessageQueue、ThreadLocal、链表、底层等技术。本文我就不展开讲了。如果对Handler不是很了解。也不影响本层次的学习。但是还是强烈建议看完本文后再另行补课。



    A同学:同步屏障。感觉好高大上的样子?能给我讲讲吗?


    我:乍一看,是挺高大上的。让人望而生畏。但是细细一想,也不是那么难,说白了就是将Message分成三种不同类型


    A同学:此话怎讲,愿闻其详~


    我:如下代码应该看得懂吧?


    class Message{
    int mType;
    //同步屏障消息
    public static final int SYNC_BARRIER = 0;
    //普通消息
    public static final int NORMAL = 1;
    //异步消息
    public static final int ASYNCHRONOUS = 2;
    }
    复制代码

    A同学:这很简单呀,平时开发中经常用不同的值表示不同的类型,但是android中的Message类并没有这几个不同的值呀?


    我:Android Message 类确实没有用不同的值来表示不同类型的Message。它是通过target和isAsynchronous()组合出三种不同类型的Message。
































    消息类型targetisAsynchronous()
    同步屏障消息null无所谓
    异步消息不为null返回true
    普通消息不为null返回false
    A同学:理解了,那么它们有什么区别呢?

    我:世界上本来只有普通消息,但是因为事情有轻重缓急,所以诞生了同步屏障消息和异步消息。它们两是配套使用的。当消息队列中同时存在这三种消息时,如果碰到了同步屏障消息,那么会优先执行异步消息。


    A同学:有点晕~


    我:别急,且看如下图解






    1. 绿色表示普通消息,很守规矩,按照入队顺序依次出队。

    2. 红色表示异步消息,意味着它比较着急,有优先执行的权利。

    3. 黄色表示同步屏障消息,它的作用就是警示,后续只会让异步消息出队,如果没有异步消息,则会一直等待。


     如上图,消息队列中全是普通消息。那么它们会按照顺序,从队首依次出队列。msg1->msg2->msg3


     如上图,三种类型消息全部存在,msg1是同步屏障消息。同步屏障消息并不会真正执行,它也不会主动出队列,需要调用MessageQueue的removeSyncBarrier()方法。它的作用就是"警示",后续优先让红色的消息出队列。



    1. msg3出队列


     2. msg5出队列 



    1. 此刻msg2并不会出队列,队列中已经没有了红色消息,但是存在黄色消息,所以会一直等红色消息,绿色消息得不到执行机会


    1. 调用removeSyncBarrier()方法,将msg1出队列


    1. 绿色消息按顺序出队



    postSyncBarrier()和removeSyncBarrier()必须成对出现,否则会导致消息队列出现假死情况。



    同步屏障就介绍到这,如果没明白的话,建议网上搜索其它资料阅读。


    6. 第五层(Choreographer vsync机制)



    B同学:vsync机制感觉好高大上的样子?能给我讲讲吗


    我:这个东西比较底层了,我也太清楚,但是有一个比较取巧的理解方式。


    B同学:说来听听。


    我:观察者模式听过吧,vsync信号是由底层发出的。具体情况我不清楚,但是上层有个类监听vsync的信号,当接收到信号时,就会通过Choreographer向消息队列发送异步消息,这个消息的作用之一就是通知ViewRootImpl去执行测量,布局,绘制操作。



    //Choreographer.java
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;


    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

    //...省略其他代码
    long now = System.nanoTime();
    if (timestampNanos > now) {
    Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
    + " ms in the future! Check that graphics HAL is generating vsync "
    + "timestamps using the correct timebase.");
    timestampNanos = now;
    }

    if (mHavePendingVsync) {
    Log.w(TAG, "Already have a pending vsync event. There should only be "
    + "one at a time.");
    } else {
    mHavePendingVsync = true;
    }

    mTimestampNanos = timestampNanos;
    mFrame = frame;
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }
    复制代码

    7. 第六层(绘制机制)


    ViewRootImpl和Choreographer是绘制机制的两大主角。他们负责功能如下。具体就不展开写了。




    作者:字节小站
    链接:https://juejin.cn/post/6952624592514973732
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(2)

    Groovy 语法 再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习...
    继续阅读 »

    Groovy 语法


    再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习的多深入,能基本使用就可以了,语法糖也没多少,最要的闭包明白就大成了。用的很少的专业一些的 API 大家 baidu 一下就出来了


    1. 不用写 ; 号


    一看这个就知道也是往高阶语言上靠 <( ̄3 ̄)> 表!,比较新的语言都这样,基本都是大同小异


    int name = 10
    int age = "AAA"
    复制代码

    2. 支持动态类型,但是必须用 def 前缀


    def name = 10
    def age = "AAA"

    name = "111"
    println(name)
    复制代码

    3. 没有基本数据类型了,全是包装类型


    Groovy 基于 java,所以 java 的基本数据类型都支持,但是 Groovy 中这些基本数据类型使用的都是包装类型:Integer、Boolean 等


    int index = 0
    println("index == "+index.class)
    复制代码


    4. 方法变化



    • 使用 def 修饰,方法可以不用指定返回类型、参数类型,直接返回最后一行。

    • 方法调用可以不写 (),最好还是加上()的好,要不真不好阅读

    • 实际上不管有没有返回值,Groovy 中返回的都是 Object 类型


    def to(x, y){
    x+y
    }

    def name = 10
    def age = 12

    name = to name,age

    println(name)
    复制代码

    5. 字符串变化


    Groovy 支持单、双、三引号来表示字符串${} 引用变量值,三引号是带输出格式的


    def world = 'world'
    def str1 = 'hello ${world}'
    def str2 = "hello ${world}"
    def str3 =
    '''hello
    &{world}'''
    复制代码

    6. 不用写 get/set


    Groovy ⾃动对成员属性创建 getter / setter,按照下面这个用法调用


    class Person{
    def name
    def age
    }

    Person person = new Person()
    person.name = "AA"
    person.setAge(123)
    person.@age = 128

    println(person.name + " / " + person.age)
    复制代码

    7. Class 类型,可以省略 .class


    8. 没有 ===


    Groovy 中 == 就是 equals,没有 === 了。而是用 .is() 代替,比较是不是同一个对象


    class Person {
    def name
    def age
    }

    Person person1 = new Person()
    Person person2 = new Person()
    person1.name = "AA"
    person2.name = "BB"

    println("person1.name == person2.name" + (person1.name == person2.name))
    println("person1 is person2" + person1.is(person2))
    复制代码

    9. 支持 xx次方运算符


    2 ** 3 == 8
    复制代码

    10. 三木运算符


    def result = name ?: ""
    复制代码

    11. 支持非空判断


    println order?.customer?.address
    复制代码

    12. Switch 变化


    def num = 5.21

    switch (num) {
    case [5.21, 4, "list"]:
    return "ok"
    break
    default:
    break
    }
    复制代码

    13. 集合类型


    Groovy 支持三种集合类型:



    • List --> 链表,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类

    • Map --> 哈希表,对应 Java 中的 LinkedHashMap

    • Range --> 范围,它其实是 List 的一种拓展


    // --> list 
    def data = [666,123,"AA"]
    data[0] = "BB"
    data[100] = 33
    println("size --> " + data.size()) // 101个元素

    ----------------------我是分割线------------------------

    // --> map
    def key = "888"
    def data = ["key1": "value", "key2": 111, (key): 888] // 使用 () key 使用动态值

    data.key1
    data.["key1"]
    data.key2 = "new"

    def name2 = "name"
    def age2 = 578
    data.put(name2, age2)

    println("size --> " + data.size()) // 4
    println("map --> " + data) // [key1:value, key2:new, 888:888, name:578]
    println("key--> " + data.get(key)) // key--> 888

    ----------------------我是分割线------------------------

    // --> range
    def data = 1..10
    data.getFrom()
    data.to()

    println("size --> " + data.size())
    println("range --> " + data) // range --> 1..10
    复制代码

    14. 闭包



    这个是绝对重点,大家到这里认真学呀 (○` 3′○) 学会这个后面就容易理解了,后面都是闭包的应用



    闭包(Closure) 是 Groovy 最重要的语法糖了,我们把闭包当做高阶语法中的对象式函数就行了


    官方定义:Groovy中的闭包是一个开放,匿名的代码块,可以接受参数,返回值并分配给变量


    // 标准写法,method1 就是一个闭包 (>▽<)
    def method1 = { name,age ->
    name + age
    }

    // 调用方式
    method1.call(123,888)
    method1(123,888)

    // 默认有一个 it 表示单个参数
    def method3 = { "Hello World!$it" }

    // 强制不带参数
    def method2 = { ->
    name + age
    }

    // 作为方法参数使用
    def to(x, y,Closure closure) {
    x + y + closure(111)
    }
    复制代码

    后面大家会经常见到闭包的应用,比如这个自定义 task 任务


    task speak{
    doLast {
    println("AAA")
    }
    }
    复制代码

    举这个例子是为了说明,实际闭包都是嵌套很多层使用



    • speak 是个方法,接收一个闭包作为参数,整个外层 {...} 都是一个闭包

    • 外层闭包内 doLast 方法又接收一个闭包作为参数,内层 {...} 又是一个闭包




    通过这个例子大家搞清楚这个嵌套关系就好学了,实际就是一层套一层,有的插件写的我都看吐了


    Closure 这东西方便是方便,但是 Closure 里面传什么类型的参数,有几个参数
    这些可没有自动提示,想知道详细就得查文档了,这点简直不能忍,我想说官方就不能做过自动提示出来嘛~
    复制代码

    15. delegate 闭包委托


    这是 Gradle 闭包常见方式:


    class Person {
    String name
    int age
    }

    def cc = {
    name = "hanmeimei"
    age = 26
    }

    Person person = new Person()
    cc.call()
    复制代码

    cc 是闭包,cc.call() 调用闭包,cc.call(persen) 这是给闭包传入参数,我们换个写法:



    • cc.delegate = person 就相当于 cc.call(persen)


    这个写法就是:委托 了,没什么难理解的,我这里就是按照最简单的解释来


    至于为什么要有委托这种东西,必然是有需求的。我们写的都是 .gradle 脚本,这些脚本实际要编译成 .class 才能运行。也就是说代码实际上动态根据我们配置生成的,传参数也是动态的,委托这一特性就是为了动态生成代码、传参准备的


    后面很多 Gradle 中的插件,其 {...} 里面写配置其实走的都是委托这个思路


    举个常见的例子,Android {...} 代码块大家熟悉不熟悉,这个就是闭包嵌套,闭包里还有闭包 -->


    android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    defaultConfig {
    minSdkVersion 15
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    }
    }
    复制代码

    16. 插件中使用 delegate + 闭包思路


    其实思路很简单,每一个 {...} 闭包都要有一个对应的数据 Bean 存储数据,在合适的时机 .delegate 即可



    1. 闭包定义


    def android = {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    // 这个对应相应的方法
    defaultConfig {
    minSdkVersion 15
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    }
    }
    复制代码


    1. 准备数据 Bean


    class Android {
    int mCompileSdkVersion
    String mBuildToolsVersion
    BefaultConfig mBefaultConfig

    Android() {
    this.mBefaultConfig = new BefaultConfig()
    }

    void defaultConfig(Closure closure) {
    closure.setDelegate(mProductFlavor)
    closure.setResolveStrategy(Closure.DELEGATE_FIRST)
    closure.call()
    }
    }

    class BefaultConfig {
    int mVersionCode
    String mVersionName
    int mMinSdkVersion
    int mTargetSdkVersion
    }
    复制代码


    1. .delegate 绑定数据


    Android bean = new Android()
    android.delegate = bean
    android.call()
    复制代码

    17. 一样需要 import 导入包、文件



    Groovy 常用 API


    1. xml 解析


    <response version-api="2.0">
    <value>
    <books>
    <book available="20" id="1">
    <title>Don Xijote</title>
    <author id="1">Manuel De Cervantes</author>
    </book>
    <book available="14" id="2">
    <title>Catcher in the Rye</title>
    <author id="2">JD Salinger</author>
    </book>
    <book available="13" id="3">
    <title>Alice in Wonderland</title>
    <author id="3">Lewis Carroll</author>
    </book>
    <book available="5" id="4">
    <title>Don Xijote</title>
    <author id="4">Manuel De Cervantes</author>
    </book>
    </books>
    </value>
    </response>
    复制代码

    1)xml 解析


    def xparser = new XmlSlurper()
    def targetFile = new File("test.xml")
    GPathResult gpathResult = xparser.parse(targetFile)

    def book4 = gpathResult.value.books.book[3]
    def author = book4.author
    author.text()
    author.@id
    author['@id']
    author.@id.toInteger()
    复制代码

    遍历 XML 数据


    def titles = response.depthFirst().findAll { book ->
    return book.author.text() == '李刚' ? true : false
    }

    def name = response.value.books.children().findAll { node ->
    node.name() == 'book' && node.@id == '2'
    }.collect { node ->
    return node.title.text()
    }

    复制代码

    2)获取 AndroidManifest 配置文件参数


    Gradle 解析 xml 的意义也就是 AndroidManifest 配置文件了,不难


    def androidManifest = new XmlSlurper().parse("./app/src/main/AndroidManifest.xml")
    def app = androidManifest.application
    println("value -->" + app.@"android:supportsRtl")
    复制代码

    3)生成 xml


    /**
    * 生成 xml 格式数据
    * <langs type='current' count='3' mainstream='true'>
    <language flavor='static' version='1.5'>Java</language>
    <language flavor='dynamic' version='1.6.0'>Groovy</language>
    <language flavor='dynamic' version='1.9'>JavaScript</language>
    </langs>
    */
    def sw = new StringWriter()
    // 用来生成 xml 数据的核心类
    def xmlBuilder = new MarkupBuilder(sw)
    // 根结点 langs 创建成功
    xmlBuilder.langs(type: 'current', count: '3',
    mainstream: 'true') {
    //第一个 language 结点
    language(flavor: 'static', version: '1.5') {
    age('16')
    }
    language(flavor: 'dynamic', version: '1.6') {
    age('10')
    }
    language(flavor: 'dynamic', version: '1.9', 'JavaScript')
    }

    // println sw

    def langs = new Langs()
    xmlBuilder.langs(type: langs.type, count: langs.count,
    mainstream: langs.mainstream) {
    //遍历所有的子结点
    langs.languages.each { lang ->
    language(flavor: lang.flavor,
    version: lang.version, lang.value)
    }
    }

    println sw

    // 对应 xml 中的 langs 结点
    class Langs {
    String type = 'current'
    int count = 3
    boolean mainstream = true
    def languages = [
    new Language(flavor: 'static',
    version: '1.5', value: 'Java'),
    new Language(flavor: 'dynamic',
    version: '1.3', value: 'Groovy'),
    new Language(flavor: 'dynamic',
    version: '1.6', value: 'JavaScript')
    ]
    }
    //对应xml中的languang结点
    class Language {
    String flavor
    String version
    String value
    }
    复制代码

    2. 解析 json


    def reponse = getNetworkData('http://yuexibo.top/yxbApp/course_detail.json')

    def getNetworkData(String url) {
    //发送http请求
    def connection = new URL(url).openConnection()
    connection.setRequestMethod('GET')
    connection.connect()
    def response = connection.content.text
    //将 json 转化为实体对象
    def jsonSluper = new JsonSlurper()
    return jsonSluper.parseText(response)
    }
    复制代码

    3. IO


    Gradle 中操作文件是比不可少的工作了,Grovvy IO API 大家一定要清楚


    1) 获取文件地址


    o(^@^)o 大家写插件、task 时获取项目地址这个点总是要会的,下面的代码不光可以使用 rootProject,每个脚本中的 Project 对象也可以使用的,path 和 absolutePath 都行


    println(rootProject.projectDir.path/absolutePath)
    println(rootProject.rootDir.path)
    println(rootProject.buildDir.path)

    /Users/zbzbgo/worksaplce/flutter_app4/MyApplication22
    /Users/zbzbgo/worksaplce/flutter_app4/MyApplication22
    /Users/zbzbgo/worksaplce/flutter_app4/MyApplication22/build
    复制代码

    2) 文件定位


    思路就是把指定 path 加入当年项目的根路径中,再构建 File 对象使用


    //文件定位
    this.getContent("config.gradle", "build.gradle")

    // 不同与 new file 的需要传入 绝对路径 的方式
    // file 从相对于当前的 project 工程开始查找
    def mFiles = files(path1, path2)
    println mFiles[0].text + mFiles[1].text
    复制代码

    或者这样写也是可以的,会在相应的子项目目录下生成文件,这种不用写 this.getContent(XXX)


     def file = project.file(fileName)
    复制代码

    3)eachLine 一次读一行


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")

    fromFile.eachLine { String line ->
    println("line -->" + line)
    }
    复制代码

    line 的 API 还有好几个



    4)获取输入流、输出流


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")

    fromFile.withInputStream { InputStream ins ->
    ...... 这里系统会自动关闭流,不用我们自己关
    }

    def ins = fromFile.newInputStream()
    ins.close()
    复制代码

    5)<< 复制 文件


    Grovvy 的语法糖写起来的简便


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
    File toFile = new File(rootProject.projectDir.path + "/build2.gradle")

    fromFile.withInputStream { InputStream ins ->
    toFile.withOutputStream { OutputStream out ->
    out << ins
    }
    }
    复制代码

    6)<< copy API 复制文件


    copy {
    from file(rootProject.rootDir.path+"/build.gradle") // 源文件
    into rootProject.rootDir.path // 复制目标地址,这里不用带文件名

    exclude()
    rename { "build.gradle2" } // 复制后重命名,不写的话默认还是目标文件名
    }
    复制代码

    7)reader/writer


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
    File toFile = new File(rootProject.projectDir.path + "/build2.gradle")

    fromFile.withReader { reader ->
    def lines = reader.lines()
    toFile.withWriter { writer ->
    lines.each { line ->
    writer.writeLine(line)
    }
    }
    }
    复制代码

    8)Object


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
    File toFile = new File(rootProject.projectDir.path + "/build2.gradle")

    fromFile.withObjectInputStream { input ->
    toFile.withObjectOutputStream { out ->
    out.writeObject( input.readObject() )
    }
    }
    复制代码

    9)获取文件字节数组


    def file = new File(baseDir, 'test.txt')
    byte[] contents = file.bytes
    复制代码

    10)遍历文件树


    def dir = new File("/")
    //eachFile()方法返回该目录下的所有文件和子目录,不递归
    dir.eachFile { file ->
    println file.name
    }
    dir.eachFileMatch(~/.*\.txt/) {file ->
    println file.name
    }

    -------------------分割线-------------------

    def dir = new File("/")
    //dir.eachFileRecurse()方法会递归显示该目录下所有的文件和目录
    dir.eachFileRecurse { file ->
    println file.name
    }
    dir.eachFileRecurse(FileType.FILES) { file ->
    println file.name
    }

    -------------------分割线-------------------

    dir.traverse { file ->
    //如果当前文件是一个目录且名字是bin,则停止遍历
    if (file.directory && file.name=='bin') {
    FileVisitResult.TERMINATE
    //否则打印文件名字并继续
    } else {
    println file.name
    FileVisitResult.CONTINUE
    }
    }
    复制代码

    11)序列化


    boolean b = true
    String message = 'Hello from Groovy'
    def file = new File(baseDir, 'test.txt')
    // 序列化数据到文件
    file.withDataOutputStream { out ->
    out.writeBoolean(b)
    out.writeUTF(message)
    }
    // ...
    // 从文件读取数据并反序列化
    file.withDataInputStream { input ->
    assert input.readBoolean() == b
    assert input.readUTF() == message
    }
    复制代码

    12)程序中执行shell命令


    def process = "ls -l".execute()
    println(process)

    作者:前行的乌龟
    链接:https://juejin.cn/post/6888977881679495175
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(2)

    Gradle 安装 上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境 1. 下载 Gradle 版本 从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all 的包下载,我下的是 6.6.1,尽...
    继续阅读 »

    Gradle 安装


    上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境


    1. 下载 Gradle 版本


    从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all 的包下载,我下的是 6.6.1,尽量选择较新的版本



    2. 配置项目根目录 build.gradle 脚本文件 Gradle 工具版本号


    buildscript {

    repositories {
    google()
    jcenter()
    }
    dependencies {
    ...
    classpath 'com.android.tools.build:gradle:4.0.1'
    ...
    }
    }
    复制代码

    这里 Gradle 工具的版本号要跟着 AS 的版本号走,AS 是哪个版本,这里就写哪个版本。Gradle 工具中的 API 是给 AS 用的,自然要跟着 AS 的版本变迁


    当然这也会对 Gradle 构建工具版本有要求:



    • 第一,大家进来使用比较新的版本号

    • 第二,若是 Gradle 版本太低,编译时会有提示的,告诉你最低 Gradle 构建工具版本是多少


    3. 使用本地 Gradle 文件编译项目


    Gradle 拥有良好的兼容性,为了在没有 Gradle 环境的机器上也能顺利使用 Gradle 构建项目,AS 新创建的项目默认会在根目录下添加 wrapper 配置




    其中 gradle-wrapper.properties 文件中提供了该项目使用的 Gradle 构建工具远程下载地址,这里会对应一个具体的版本号,IDE 开发工具默认会根据这个路径去下载 Gradle 给该项目使用


    distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
    复制代码

    这样就会产生一个问题:



    • 每个项目单独管理自己的 gradle,很可能会造成机器上同时存在多个版本的 Gradle,进而存在多个版本的 Daemon 进程,这会造成机器资源吃紧,即便关闭 AS 开发工具也没用,只能重启机器才会好转



    所以这里我推荐,尤其是给使用 AS 的朋友推荐:在本地创建 Gradle 环境,统一管理 Gradle 构建工具,避免出现多版本同时运行的问题。AS 本身就很吃内存了,每一个 Daemon 构建进程起码都是 512M 内存起步的,多来几个 Daemon 进程,我这 8G 的 MAC 真的搂不住




    1. 打开 AS 中 Gradle 配置:





    • gradle-wrapper.properties -- 使用 wrapper 也就是 AS 来管理 Gradle

    • Specifiled location -- 使用本地文件,也就是我们自己管理 Gradle



    1. 在本地解压 Gradle 压缩包,记住路径,下面配 path 需要这样配置后,AS 会忽略 gradle-wrapper.properties 文件

    4. 配置 path


    这里只说 MAC 环境



    1. open -e .bash_profile 打开配置文件

    2. 添加 GRADLE_HOMEPATH


    export GRADLE_HOME=/Users/zbzbgo/gradle/gradle-6.6.1
    export PATH=${PATH}:/Users/zbzbgo/gradle/gradle-6.6.1/bin

    ----------------官方写法如下--------------------------------

    export GRADLE_HOME=/Users/zbzbgo/gradle/gradle-6.6.1
    export PATH=$PATH:$GRADLE_HOME/bin
    复制代码


    1. source .bash_profile 重置配置文件,以便新 path 生效

    2. open -e ~/.zshrc 打开另一个配置

    3. 在最后一行添加 source ~/.bash_profile

    4. source ~/.zshrc 重置配置文件


    配置 zshrc 是因为有的机器 bash_profile 配置不管用,添加这个就行了


    5. 测试 Gradle 安装是否成功


    运行 gradle --version,出现版本号则 Gradle 配置成功


    6. 执行一次 Gradle 命令


    学习新语言我们都喜欢来一次 hello world,这里我们也来一次


    随便创建一个文件夹,在其中创建一个文件,以.gradle结尾,使用 text 编辑器打开,输入:


    println("hello world!")
    复制代码

    然后 gradle xxx.gradle 执行该文件



    OK,成功,大家体验一下,groovy 是种语言,gradle 是种构建工具,可以编译 .gradle 文件

    Gradle init 初始化命令


    我们平时都是用 AS 开发的,AS 创建 Android 项目时默认就会把 Gradle 相关文件都创建出来。其实 Gradle 和 git 一样,也提供了 init 初始化方法,创建相关文件


    运行 init 命令要选择一些参数,其过程如下:



    1. 创建一个文件夹,cd 到该目录,执行 gradle init 命令

    2. 命令行提示选择项目模板

    3. 命令行提示选择开发语言

    4. 命令行提示选择脚本语言

    5. 输入工程名

    6. 输入资源名




    了解 Gradle Wrapper 文件

    上面虽然说了用 AS 开发我们最好使用本地 Gradle 文件的方式统一配置、管理 Gradle 构建工具,但是 AS Android 项目中的 Wrapper 文件夹的内容还是有必要了解一下的,这可以加深我们对 Gradle 下载、管理的了解



    Gradle Wrapper 文件的作用就是可以让你的电脑在不安装配置 Gradle 环境的前提下运行 Gradle 项目,你的机器要是没有配 Gradle 环境,那么你 clone gradle 项目下来,执行 init 命令,会根据 gradle-wrapper.properties 文件中声明的 gradle URL 远程路径去下载 gradle 构建工具,cd 进该项目



    • gradle -v --> linux 平台命令

    • gradlew -v --> window 平台命令


    然后就可以在项目目录下运行 gradle 命令了,不过还是推荐大家在机器配置统一的 Gradle 环境



    • gradlew --> linux 平台脚本

    • gradlew.bat --> window 平台脚本

    • gradle-wrapper.jar --> Gradle 下载、管理相关代码

    • gradle-wrapper.properties --> Gradle 下载、管理配置参数


    gradle-wrapper.properties 文件中参数详解:



    • distributionUrl --> Gradle 压缩包下载地址

    • zipStoreBase --> 本机存放 Gradle 压缩包主地址

    • zipStorePath --> 本机存放 Gradle 压缩包主路径

      • Gradle 压缩包完整的路径是 zipStoreBase + zipStorePath



    • distributionBase --> 本机 Gradle 压缩包解压后主地址

    • distributionPath --> 本机 Gradle 压缩包解压后路径

      • Gradle 解压完整的路径是 distributionBase + distributionPath

      • distributionBase 的路径是环境 path 中 GRADLE_USER_HOME 的地址

      • Windows:C:/用户/你电脑登录的用户名/.gradle/

      • MAC:~/.gradle/

      • 你 MAC 要是配了 Gradle 环境变量,distributionBase 就是你自己解压缩的 gradle 路径




    这几个地址还是要搞清楚的~

    作者:前行的乌龟
    链接:https://juejin.cn/post/6888977881679495175
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(1)

    理解 Gradle、Groovy 对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧ 1. 什么是构建工具 简单的说就是自动化的编译、打包程序 我们来回忆一下,入门 java 那会,大家都写过 Hello Wr...
    继续阅读 »

    理解 Gradle、Groovy


    对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧


    1. 什么是构建工具


    简单的说就是自动化的编译、打包程序


    我们来回忆一下,入门 java 那会,大家都写过 Hello Wrold!吧。然后老师让我们干啥,javac 编译, java 运行。在这里编译需要我们手动执行一次 javac,大家想过没有,要是有100个文件呢?那我们就得手动 100次 javac 编译指令


    到这里大家都会想到自动化吧,是的,自动化编译工具就是最早的构建工具了。然后我们拓展其功能,比如说:



    • 100个文件,编译后我要分10个文件夹保存

    • 哎呀,文件夹不好使了,别人要我提供 .jar 文件

    • 我去,产品加功能了,要加入 C++ 文件进来,C、java 文件要一起编译

    • 产品要有展示图片,还要有声音,多媒体资源也要加进来

    • 业务拓展了好几个渠道,每一个渠道都要提供一个定制化的 .jar 出来

    • 业务拓展了,要全平台了,win、android、ios 都要支持


    上面都是我臆想的,不过我觉得发展的历程大同小异。随着需求叠加、平台扩展,对代码最终产品也是有越来越多的要求。jar/aar/exe 这些打包时有太多的不一样,我们是人不是机器,不可能记得住的这些差异不同。那就必须依靠自动化技术、工具,要能支持平台、需求等方面的差异、能添加自定义任务的、专门的用来打包生成最终产品的一个程序、工具,这个就是构建工具。构建工具本质上还是一段代码程序


    我这样说是想具体一点,让大家有些代入感好理解构建工具是什么。就像下图,麦子就是我们的代码、资源等,草垛就是最终打包出来的成品,机器就是构建工具。怎么打草垛我们不用管,只要我们会用机器就行了

    打包也没什么神奇的,就是根据不同平台的要求,怎么把烂七八糟的都装一块,和妹子们出门前收拾衣服打包装箱一样。打包的目的是减少文件体积,方便安装,任何打包出来的安装包,本质都是一个压缩包

    2. Gradle 也是一种构建工具



    Android 项目这么多东西,既有我们自己写的 java、kotlin、C++、Dart 代码,也有系统自己的 java、C++ 代码,还有引入的第三方代码,还有图片、音乐、视频文件,这么多代码、资源打包成 APK 文件肯定要有一个规范,干这个活的就是我们熟悉的 gradle 了


    APK 文件我们解压可以看到好多文件和文件夹,具体不展开了


    不用把 Gradle 想的太难了,Gradle 就是帮我们打包生成 apk 的一个程序。难点的在于很灵活,我们可以在其中配置、声明参数、执行自己写的脚本、甚至导入自己的写的插件,来完成我们自定义的额外的任务。但是不要本末倒置,Gradle 就是帮我们打包 APK 的一个工具罢了


    下面3段话大家理解下,我觉得说的都挺到位的,看过后面还可以翻回来看这3句话,算是对 Gradle 的总结性文字了,很好~



    Gradle 是通用构建、打包程序,可以支持 java、web、android 等项目,具体到你的平台怎么打包,还得看你引入的什么插件,插件会具体按照我们平台的要求去编译、打包。比如我引入的:apply plugin: 'com.android.application',我导入的是 android 编译打包插件,那么最终会生成 APK 文件,就是这样。我引入的:apply plugin: 'com.android.library' android lib 库文件插件,那么最终会生成 aar 文件




    Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西




    Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件。Gradle中每一个待编译的工程都是一个Project,一个具体的编译过程是由一个一个的Task来定义和执行的。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西

    3. Gradle 是个程序、Groovy 是特定领域 DSL 语言



    • Gradle 是运行在 JVM 实例上的一个程序,内部使用 Groovy 语言

    • Groovy 是一种 JVM 上的脚本语言,基于 java 扩展的动态语言


    Gradle 简单来说就是在运行在 JVM 上的一个程序罢了,虽然其使用的是 Groovy 这种脚本语言,但是 Gradle 会把 .gradle Groovy 脚本编译成 .class java字节码文件在 JVM 上运行,最终还是 java 这套东西


    Android 项目里 settings.gradle、诸多build.gradle 脚本都会编译成对应的 java 类:SettingProject 再去运行,引入的插件也是会编译成对应的 java 对象再执行构建任务


    Gradle 内部是一个个编译、打包、处理资源的函数或者插件(函数库),可以说 Gradle 其实就是 API 集合,和我们日常使用的 Okhttp 框架没什么区别,里面都是一个个 API,区别是干的活不同罢了


    打开 Gradle 文件目录看看,核心的 bin 文件就一个 gradle 脚本,这个脚本就是 Gradle 核心执行逻辑了,他会启动一个 JVM 实例去加载 lib 中的各种函数去构建项目,这么看 gradle 其实很简单、不难理解




    红框里的是 Gradle 自带的内置插件,apply plugin: 'com.android.library'apply plugin: 'com.android.application' 这些都是 gradle 自带的内置插件



    19 年 Gradle 提供了中国区 CDN,AS 下载 Gradle 不再慢的和蜗牛一样了




    Gradle JVM 进程


    Gradle 构建工具在不同场景下会分别使用3个 JVM 进程:



    • client

    • Daemon

    • wrapper


    来自Gradle开发团队的Gradle入门教程 --> 官方宣传中这里解释的很清楚,比官方文档都清楚的多


    1. client 进程


    client 进程是个轻量级进程,每次构建开始都会创建这个进程,构建结束会销毁这个进程。client 进程的任务是查找并和 Daemon 进程通信:



    • Daemon 进程没启动,client 进程会启动一个新的 Daemon 进程

    • Daemon 进程已经存在了,client 进程就给 Daemon 进程传递本次构建相关的参数和任务,然后接收 Daemon 进程发送过来的日志


    gradle.properties 里面设置的参数,全局 init.gradle 初始化脚本的任务这些都需要 client 进程传递给 Daemon 进程


    2. Daemon 进程


    Daemon 进程负责具体的构建任务。我们使用 AS 打包 APK 这依靠的不是 AS 这个 IDEA 开发工具,而是 Gradle 构建工具自己启动的、专门的一个负责构建任务的进程:Daemon。每一个版本的 Gradle 都会对应创建一个 Daemon 进程


    Daemon 进程不依赖 AS 而是独立存在,是一个守护进程,构建结束 Daemon 进程也不会销毁,而是会休眠,等待下一次构建,这样做是为了节省系统资源,加快构建速度,Daemon 进程会缓存插件、依赖等资源


    必须注意: 每一个 Gradle 版本都会对应一个 Daemon 进程,机器内若是运行过多个版本的 Gradle,那么机器内就会存在多个 Daemon 进程,AS 开发 android 项目,我推荐使用 Gradle 本地文件,不依靠每个 android 项目中 wrapper 管理 gradle 版本,具体后面会说明


    从性能上讲:



    • Gradle 在 JVM 上运行,会使用一些支持库,这些库都需要初始化时间,一个长期存在的后台进程有利于节省编译时间

    • daemon 进程会跨构建缓存一些插件、库等缓存数据,这样对加快构建速度的确非常有意义


    gradle --status 命令可以查看已启动的 daemon 进程情况:


    ➜  ~ jps
    39554 KotlinCompileDaemon
    39509 GradleDaemon
    39608
    39675 Jps
    ➜ ~ gradle --status
    PID STATUS INFO
    39509 IDLE 6.6.1

    // INFO 是 gradle 版本号
    // Kotlin 语言编写的 Gradle 脚本需要一个新的 daemon 进程出来
    复制代码

    若是机器内已经启动了多个 Daemon 进程也不要紧,自己手动杀进程就是了


    Daemon 进程在以下情况时会失效,需要启动新的 Daemon 进程,判断 Daemon 进程是否符合要求是上面说的 client 进程的任务:



    • 修改 JVM 配置这回造成启动新的构建进程

    • Gradle 将杀死任何闲置了3小时或更长时间的守护程序

    • 一些环境变量的变化,如语言、keystore、keyStorePassword、keyStoreType 这些变化都会造成旧有的守护进程失效


    即便时同一个版本的 Gradle,也会因为 VM 配置不同而存在多个相同 Gradle 版本的 Daemon 进程。比如同时启动好几个项目,项目之间使用的 Gradle 版本相同,但是 VM 使用的不同配置


    wrapper 进程



    wrapper 进程啥也不干,不参与项目构建,唯一任务就是负责下载管理 Gradle 版本。我们导入 Gradle 项目进来,client 进程发现所需版本的 Gradle 本机没有,那么就会启动 wrapper 进程,根据 gradle.properties 里面的参数去自行 gradle-wrapper.jar 里面的下载程序去下载 Gradle 文件


    其他开发工具,我们直接使用 wrapper 来管理 Gradle 的话也是会启动 wrapper 进程的,完事 wrapper 进程会关闭



    作者:前行的乌龟
    链接:https://juejin.cn/post/6888977881679495175
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    iOS 音视频编解码基本概念

    来看看视频里面到底有什么内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.j...
    继续阅读 »

    来看看视频里面到底有什么


    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 视频相关基础概念


      1.视频文件格式相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。

      2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.





      • 相关视频封装格式的优缺点:

        • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
        • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
        • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
        • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
        • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
        • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
        • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
      • 视频的编码格式

      • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

        • 视频的质量、
        • 用来表示视频所需要的数据量(通常称之为码率)、
        • 编码算法和解码算法的复杂度
        • 针对数据丢失和错误的鲁棒性(Robustness)
        • 编辑的方便性
        • 随机访问
        • 编码算法设计的完美性
        • 端到端的延时以及其它一些因素
      • 常见的编码方式:

      • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

        • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
        • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
        • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
        • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
        • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
      • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

      • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

        • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
        • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
        • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
        • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
          其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
      • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

      • 音频编码方式

        • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

        • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

        • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

        • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

      直播/小视频中的编码格式

      • 视频编码格式

        • H264编码的优势:
          低码率
          高质量的图像
          容错能力强
          网络适应性强
      • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
        举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
        音频编码格式:

      • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

      • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

      • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

      • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

      关于H264

      • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

      • 图像

        • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

      当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

      当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

      「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好

      • 片(Slice),每一帧图像可以分为多个片

      网络提取层单元(NALU, Network Abstraction Layer Unit),
      NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

      宏块(Macroblock),分片是由宏块组成。



      作者:枫紫_6174
      链接:https://www.jianshu.com/p/9602f3c9b82b



    收起阅读 »

    iOS Cateogry的深入理解

    首先先看几个面试问题Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类2.新建一个TCStudent类继承自TCPerson,并且给T...
    继续阅读 »

    首先先看几个面试问题

    • Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?

    1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类


    2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类



    Cateogry里面有load方法么?

    答:分类里面肯定有load

    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{

    }
    @end


    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{

    }
    @end
    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{

    }
    @end

    load方法什么时候调用?

    load方法在runtime加载类和分类的时候调用load

    #import <Foundation/Foundation.h>

    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
    }


    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    @end


    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    @end
    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    @end
    可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:



    从输出结果我们可以看出,三个load方法都被调用

    问题:分类重写方法,真的是覆盖原有类的方法么?如果不是,到底分类的方法是怎么调用的?

    首先我们在TCPerson申明一个方法+ (void)test并且在它的两个分类都重写+ (void)test

    #import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

    @interface TCPerson : NSObject
    + (void)test;
    @end

    NS_ASSUME_NONNULL_END

    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    + (void)test{
    NSLog(@"TCPerson +test");
    }
    @end

    分类重写test
    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest1) +test1");
    }
    @end

    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest2) +test2");
    }
    @end

    在main里面我们调用test

    #import <Foundation/Foundation.h>
    #import "TCPerson.h"
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    }
    return 0;
    }

    输出结果:



    从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?



    因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(拖动文件就行了)




    其输出结果为:



    细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test覆盖了,所以才出现这种情况么?


    我们打印TCPerson的类方法


    void printMethodNamesOfClass(Class cls)
    {
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);

    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];

    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
    // 获得方法
    Method method = methodList[I];
    // 获得方法名
    NSString *methodName = NSStringFromSelector(method_getName(method));
    // 拼接方法名
    [methodNames appendString:methodName];
    [methodNames appendString:@", "];
    }

    // 释放
    free(methodList);

    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
    }
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    printMethodNamesOfClass(object_getClass([TCPerson class]));
    }
    return 0;
    }

    输出结果:




    可以看到,TCPerson的所有类方法名,并不是覆盖,三个load,三个test,方法都在

    load源码分析:查看objc底层源码我们可以看到:

    void call_load_methods(void)
    {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
    // 1. Repeatedly call class +loads until there aren't any more
    while (loadable_classes_used > 0) {
    call_class_loads();
    }

    // 2. Call category +loads ONCE
    more_categories = call_category_loads();

    // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0 || more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
    }

    load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
    我们查看call_class_loads()方法

    static void call_class_loads(void)
    {
    int I;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue;

    if (PrintLoading) {
    _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    if (classes) free(classes);
    }

    其通过的是load_method_t函数指针直接调用
    函数指针直接调用

    typedef void(*load_method_t)(id, SEL);
    其分类load方法调用也是一样

    static bool call_category_loads(void)
    {
    int i, shift;
    bool new_categories_added = NO;

    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Category cat = cats[i].cat;
    load_method_t load_method = (load_method_t)cats[i].method;
    Class cls;
    if (!cat) continue;

    cls = _category_getClass(cat);
    if (cls && cls->isLoadable()) {
    if (PrintLoading) {
    _objc_inform("LOAD: +[%s(%s) load]\n",
    cls->nameForLogging(),
    _category_getName(cat));
    }
    (*load_method)(cls, SEL_load);
    cats[i].cat = nil;
    }
    }

    为什么test不一样了

    因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的



    load只在加载类的时候调用一次,且先调用类的load,再调用分类的

    load的继承关系调用
    首先我们先看TCStudent

    #import "TCStudent.h"

    @implementation TCStudent

    @end
    不写load方法调用


    TCStudent写上load


    从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的

    总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/f66921e24ffe




    收起阅读 »

    iOS Cateogry的深入理解&&initialize方法调用理解

    上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类   类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收...
    继续阅读 »
    • 上一篇文章我们讲到了load方法,今天我们来看看initialize

    新建项目,新建类   类结构图如下


    将原来的load方法换成initialize


    先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize

    相信大家在想什么叫第一次接收消息了,我们回到main()



    运行程序,输出结果:



    说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码

    从输出结果可以看到没有任何关于initialize的打印,程序直接退出

    • 2.initialize的打印
    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    [TCPerson alloc];
    }
    return 0;
    }


    运行结果:
    2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0

    从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印

    load是直接函数指针直接调用,类,分类,继承等等

    [TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用

    多次向TCPerson发送消息的输出结果
    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    [TCPerson alloc];
    [TCPerson alloc];
    [TCPerson alloc];
    [TCPerson alloc];
    }
    return 0;
    }

    输出结果:
    2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0

    initialize只会调用一次

    我们再来看看继承关系中,initialize的调用

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCStudent alloc];

    }
    return 0;
    }

    输出结果:

    2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
    2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0
    从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson alloc];
    [TCPerson alloc];
    [TCStudent alloc];
    [TCStudent alloc];
    }
    return 0;
    }

    输出结果:

    020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
    2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0

    如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

    #import "TCStudent.h"

    @implementation TCStudent
    //+ (void)initialize{
    // NSLog(@"TCStudent +initialize");
    //}
    @end
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson alloc];
    [TCStudent alloc];
    }
    return 0;
    }


    输出结果:

    2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
    2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0


    如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

    #import "TCStudent.h"

    @implementation TCStudent
    + (void)initialize{
    NSLog(@"TCStudent +initialize");
    }
    @end
    #import "TCStudent+TCStudentTest1.h"

    @implementation TCStudent (TCStudentTest1)
    + (void)initialize{
    NSLog(@"TCStudent (TCStudentTest1) +initialize");
    }
    @end#import "TCStudent+TCStudentTest2.h"

    @implementation TCStudent (TCStudentTest2)
    + (void)initialize{
    NSLog(@"TCStudent (TCStudentTest2) +initialize");
    }
    @end
    输出结果:

    2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
    2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0

    作者:枫紫_6174
    链接:https://www.jianshu.com/p/f0150edc0f42




    收起阅读 »

    深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(3)

    四、文件处理 1、常规文件处理 1)、读文件 eachLine 方法 我们可以使用 eachLine 方法读该文件中的每一行,它唯一的参数是一个 Closure,Closure 的参数是文件每一行的内容。示例代码如下所示: def file = new Fil...
    继续阅读 »

    四、文件处理


    1、常规文件处理


    1)、读文件


    eachLine 方法


    我们可以使用 eachLine 方法读该文件中的每一行,它唯一的参数是一个 Closure,Closure 的参数是文件每一行的内容。示例代码如下所示:


    def file = new File(文件名)
    file.eachLine{ String oneLine ->
    println oneLine
    }

    def text = file.getText()
    def text2 = file.readLines()

    file.eachLine { oneLine, lineNo ->
    println "${lineNo} ${oneLine}"
    }
    复制代码

    然后,我们可以使用 'targetFile.bytes' 直接得到文件的内容。


    使用 InputStream


    此外,我们也可以通过流的方式进行文件操作,如下代码所示:


    //操作 ism,最后记得关掉
    def ism = targetFile.newInputStream()
    // do sth
    ism.close
    复制代码

    使用闭包操作 inputStream


    利用闭包来操作 inputStream,其功能更加强大,推荐使用这种写法,如下所示:


    targetFile.withInputStream{ ism ->
    // 操作 ism,不用 close。Groovy 会自动替你 close
    }
    复制代码

    2)、写文件


    关于写文件有两种常用的操作形式,即通过 withOutputStream/withInputStream 或 withReader/withWriter 的写法。示例代码如下所示:


    通过 withOutputStream/、withInputStream copy 文件


    def srcFile = new File(源文件名)
    def targetFile = new File(目标文件名) targetFile.withOutputStream{ os->
    srcFile.withInputStream{ ins->
    os << ins //利用 OutputStream 的<<操作符重载,完成从 inputstream 到 OutputStream //的输出
    }
    }
    复制代码

    通过 withReader、withWriter copy 文件


    def copy(String sourcePath, String destationPath) {
    try {
    //首先创建目标文件
    def desFile = new File(destationPath)
    if (!desFile.exists()) {
    desFile.createNewFile()
    }

    //开始copy
    new File(sourcePath).withReader { reader ->
    def lines = reader.readLines()
    desFile.withWriter { writer ->
    lines.each { line ->
    writer.append(line + "\r\n")
    }
    }
    }
    return true
    } catch (Exception e) {
    e.printStackTrace()
    }
    return false
    }
    复制代码

    此外,我们也可以通过 withObjectOutputStream/withObjectInputStream 来保存与读取 Object 对象。示例代码如下所示:


    保存对应的 Object 对象到文件中


    def saveObject(Object object, String path) {
    try {
    //首先创建目标文件
    def desFile = new File(path)
    if (!desFile.exists()) {
    desFile.createNewFile()
    }
    desFile.withObjectOutputStream { out ->
    out.writeObject(object)
    }
    return true
    } catch (Exception e) {
    }
    return false
    }
    复制代码

    从文件中读取 Object 对象


    def readObject(String path) {
    def obj = null
    try {
    def file = new File(path)
    if (file == null || !file.exists()) return null
    //从文件中读取对象
    file.withObjectInputStream { input ->
    obj = input.readObject()
    }
    } catch (Exception e) {

    }
    return obj
    }
    复制代码

    2、XML 文件操作


    1)、获取 XML 数据


    首先,我们定义一个包含 XML 数据的字符串,如下所示:


    final String xml = '''
    <response version-api="2.0">
    <value>
    <books id="1" classification="android">
    <book available="20" id="1">
    <title>疯狂Android讲义</title>
    <author id="1">李刚</author>
    </book>
    <book available="14" id="2">
    <title>第一行代码</title>
    <author id="2">郭林</author>
    </book>
    <book available="13" id="3">
    <title>Android开发艺术探索</title>
    <author id="3">任玉刚</author>
    </book>
    <book available="5" id="4">
    <title>Android源码设计模式</title>
    <author id="4">何红辉</author>
    </book>
    </books>
    <books id="2" classification="web">
    <book available="10" id="1">
    <title>Vue从入门到精通</title>
    <author id="4">李刚</author>
    </book>
    </books>
    </value>
    </response>
    '''
    复制代码

    然后,我们可以 使用 XmlSlurper 来解析此 xml 数据,代码如下所示:


    def xmlSluper = new XmlSlurper()
    def response = xmlSluper.parseText(xml)

    // 通过指定标签获取特定的属性值
    println response.value.books[0].book[0].title.text()
    println response.value.books[0].book[0].author.text()
    println response.value.books[1].book[0].@available

    def list = []
    response.value.books.each { books ->
    //下面开始对书结点进行遍历
    books.book.each { book ->
    def author = book.author.text()
    if (author.equals('李刚')) {
    list.add(book.title.text())
    }
    }
    }
    println list.toListString()
    复制代码

    2)、获取 XML 数据的两种遍历方式


    获取 XML 数据有两种遍历方式:深度遍历 XML 数据 与 广度遍历 XML 数据,下面我们看看它们各自的用法,如下所示:


    深度遍历 XML 数据


    def titles = response.depthFirst().findAll { book ->
    return book.author.text() == '李刚' ? true : false
    }
    println titles.toListString()
    复制代码

    广度遍历 XML 数据


    def name = response.value.books.children().findAll { node ->
    node.name() == 'book' && node.@id == '2'
    }.collect { node ->
    return node.title.text()
    }
    复制代码

    在实际使用中,我们可以 利用 XmlSlurper 求获取 AndroidManifest.xml 的版本号(versionName),代码如下所示:


    def androidManifest = new XmlSlurper().parse("AndroidManifest.xml") println androidManifest['@android:versionName']
    或者
    println androidManifest.@'android:versionName'
    复制代码

    3)、生成 XML 数据


    除了使用 XmlSlurper 解析 XML 数据之外,我们也可以 使用 xmlBuilder 来创建 XML 文件,如下代码所示:


    /**
    * 生成 xml 格式数据
    * <langs type='current' count='3' mainstream='true'>
    <language flavor='static' version='1.5'>Java</language>
    <language flavor='dynamic' version='1.6.0'>Groovy</language>
    <language flavor='dynamic' version='1.9'>JavaScript</language>
    </langs>
    */
    def sw = new StringWriter()
    // 用来生成 xml 数据的核心类
    def xmlBuilder = new MarkupBuilder(sw)
    // 根结点 langs 创建成功
    xmlBuilder.langs(type: 'current', count: '3',
    mainstream: 'true') {
    //第一个 language 结点
    language(flavor: 'static', version: '1.5') {
    age('16')
    }
    language(flavor: 'dynamic', version: '1.6') {
    age('10')
    }
    language(flavor: 'dynamic', version: '1.9', 'JavaScript')
    }

    // println sw

    def langs = new Langs()
    xmlBuilder.langs(type: langs.type, count: langs.count,
    mainstream: langs.mainstream) {
    //遍历所有的子结点
    langs.languages.each { lang ->
    language(flavor: lang.flavor,
    version: lang.version, lang.value)
    }
    }

    println sw

    // 对应 xml 中的 langs 结点
    class Langs {
    String type = 'current'
    int count = 3
    boolean mainstream = true
    def languages = [
    new Language(flavor: 'static',
    version: '1.5', value: 'Java'),
    new Language(flavor: 'dynamic',
    version: '1.3', value: 'Groovy'),
    new Language(flavor: 'dynamic',
    version: '1.6', value: 'JavaScript')
    ]
    }
    //对应xml中的languang结点
    class Language {
    String flavor
    String version
    String value
    }
    复制代码

    4)、Groovy 中的 json


    我们可以 使用 Groovy 中提供的 JsonSlurper 类去替代 Gson 解析网络响应,这样我们在写插件的时候可以避免引入 Gson 库,其示例代码如下所示:


    def reponse =
    getNetworkData(
    'http://yuexibo.top/yxbApp/course_detail.json')

    println reponse.data.head.name

    def getNetworkData(String url) {
    //发送http请求
    def connection = new URL(url).openConnection()
    connection.setRequestMethod('GET')
    connection.connect()
    def response = connection.content.text
    //将 json 转化为实体对象
    def jsonSluper = new JsonSlurper()
    return jsonSluper.parseText(response)
    }
    复制代码

    五、总结


    在这篇文章中,我们从以下 四个方面 学习了 Groovy 中的必备核心语法:



    • 1)、groovy 中的变量、字符串、循环等基本语法。

    • 2)、groovy 中的数据结构:数组、列表、映射、范围。

    • 3)、groovy 中的方法、类等面向对象、强大的运行时机制。

    • 4)、groovy 中对普通文件、XML、json 文件的处理。


    在后面我们自定义 Gradle 插件的时候需要使用到这些技巧,因此,掌握好 Groovy 的重要性不言而喻,只有扎实基础才能让我们走的更远。


    作者:jsonchao
    链接:https://juejin.cn/post/6844904128594853902
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(2)

    三、Groovy 基础语法Groovy 的基础语法主要可以分为以下 四个部分:1)、Groovy 核心基础语法。2)、Groovy 闭包。3)、Groovy 数据结构。4)、Groovy 面向对象1、Groovy 核心基础语法Groovy 中的变量变...
    继续阅读 »

    三、Groovy 基础语法

    Groovy 的基础语法主要可以分为以下 四个部分:

    • 1)、Groovy 核心基础语法。
    • 2)、Groovy 闭包。
    • 3)、Groovy 数据结构。
    • 4)、Groovy 面向对象

    1、Groovy 核心基础语法

    Groovy 中的变量

    变量类型

    Groovy 中的类型同 Java 一样,也是分为如下 两种:

    • 1)、基本类型。
    • 2)、对象类型。

    但是,其实 Groovy 中并没有基本类型,Groovy 作为动态语言, 在它的世界中,所有事物都是对象,就如 Python、Kotlin 一样:所有的基本类型都是属于对象类型。为了验证这个 Case,我们可以新建一个 groovy 文件,创建一个 int 类型的变量并输出它,会得到输出结果为 'class java.lang.Integer',因此可以验证我们的想法是正确的。实际上,Groovy 的编译器会将所有的基本类型都包装成对象类型

    变量定义

    groovy 变量的定义与 Java 中的方式有比较大的差异,对于 groovy 来说,它有 两种定义方式,如下所示:

    • 1)、强类型定义方式:groovy 像 Java 一样,可以进行强类型的定义,比如上面直接定义的 int 类型的 x,这种方式就称为强类型定义方式,即在声明变量的时候定义它的类型。
    • 2)、弱类型定义方式:不需要像强类型定义方式一样需要提前指定类型,而是通过 def 关键字来定义我们任何的变量,因为编译器会根据值的类型来为它进行自动的赋值。

    那么,这两种方式应该分别在什么样的场景中使用呢?

    如果这个变量就是用于当前类或文件,而不会用于其它类或应用模块,那么,建议使用 def 类型,因为在这种场景下弱类型就足够了

    但是,如果你这个类或变量要用于其它模块的,建议不要使用 def,还是应该使用 Java 中的那种强类型定义方式,因为使用强类型的定义方式,它不能动态转换为其它类型,它能够保证外界传递进来的值一定是正确的。如果你这个变量要被外界使用,而你却使用了 def 类型来定义它,那外界需要传递给你什么才是正确的呢?这样会使调用方很疑惑。

    如果此时我们在后面的代码中改变上图中 x1 的值为 String 类型,那么 x1 又会被编译器推断为 String 类型,于是我们可以猜测到,其实使用 def 关键字定义出来的变量就是 Obejct 类型。

    Groovy 中的字符串

    Groovy 中的字符串与 Java 中的字符串有比较大的不同,所以这里我们需要着重了解一下。

    Groovy 中的字符串除了继承了 Java 中传统 String 的使用方式之前,还 新增 了一个 GString 类型,它的使用方式至少有七、八种,但是常用的有三种定义方式。此外,在 GString 中新增了一系列的操作符,这能够让我们对 String 类型的变量有 更便捷的操作。最后,在 GString 中还 新增 了一系列好用的 API,我们也需要着重学习一下。

    Groovy 中常用的三种字符串定义方式

    在 Groovy 中有 三种常用 的字符串定义方式,如下所示:

    • 1)、单引号 '' 定义的字符串
    • 2)、双引号 "" 定义的字符串
    • 3)、三引号 '""' 定义的字符串

    首先,需要说明的是,'不管是单引号、双引号还是三引号,它们的类型都是 java.lang.String'。

    那么,单引号与三引号的区别是什么呢?

    既生瑜何生亮,其实不然。当我们编写的单引号字符串中有转义字符的时候,需要添加 '',并且,当字符串需要具备多行格式的时候,强行将单引号字符串分成多行格式会变成由 '+' 号组成的字符串拼接格式

    那么,双引号定义的变量又与单引号、三引号有什么区别呢?

    双引号不同与单、三引号,它定义的是一个可扩展的变量。这里我们先看看两种双引号的使用方式,如下图所示:

    在上图中,第一个定义的 name 字符串就是常规的 String 类型的字符串,而下面定义的 sayHello 字符串就是可扩展的字符串,因为它里面使用了 '${name}' 的方式引用了 name 变量的内容。而且,从其最后的类型输出可以看到,可扩展的类型就是 'org.codehaus.groovy.runtime.GStringImpl' 类型的。

    需要注意的是,可扩展的字符串是可以扩展成为任意的表达式,例如数学运算,如上图中的 sum 变量。

    有了 Groovy 的这种可扩展的字符串,我们就可以 避免 Java 中字符串的拼接操作,提升 Java 程序运行时的性能

    那么,既然有 String 和 GString 两种类型的字符串,它们在相互赋值的场景下需要不需要先强转再赋值呢?

    不需要,编译器可以帮我们自动在 String 和 GString 之间相互转换,我们在编写的时候并不需要太过关注它们的区别

    2、Groovy 闭包(Closure)

    闭包的本质其实就是一个代码块,闭包的核心内容可以归结为如下三点:

    • 1)、闭包概念
      • 定义
      • 闭包的调用
    • 2)、闭包参数
      • 普通参数
      • 隐式参数
    • 3)、闭包返回值
      • 总是有返回值

    闭包的调用

    clouser.call()
    clouser()
    def xxx = { paramters -> code }
    def xxx = { 纯 code }
    复制

    从 C/C++ 语言的角度看,闭包和函数指针很像,闭包可以通过 .call 方法来调用,也可以直接调用其构造函数,代码如下所示:

    闭包对象.call(参数)
    闭包对象(参数)
    复制代码

    如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it,和 this 的作用类似。it 代表闭包的参数。表示闭包中没有参数的示例代码:

    def noParamClosure = { -> true }
    复制代

    注意点:省略圆括号

    函数最后一个参数都是一个闭包,类似于回调函数的用法,代码如下所示:

    task JsonChao {
    doLast ({
    println "love is peace~"
    }
    })

    // 似乎好像doLast会立即执行一样
    task JsonChao {
    doLast {
    println "love is peace~"
    }
    }
    复制代码

    闭包的用法

    闭包的常见用法有如下 四种:

    • 1)、与基本类型的结合使用。
    • 2)、与 String 类的结合使用。
    • 3)、与数据结构的结合使用。
    • 4)、与文件等结合使用。

    闭包进阶

    • 1)、闭包的关键变量
      • this
      • owner
      • delegate
    • 2)、闭包委托策略

    闭包的关键变量

    this 与 owner、delegate

    其差异代码如下代码所示:

    def scrpitClouser = {
    // 代表闭包定义处的类
    printlin "scriptClouser this:" + this
    // 代表闭包定义处的类或者对象
    printlin "scriptClouser this:" + owner
    // 代表任意对象,默认与 ownner 一致
    printlin "scriptClouser this:" + delegate
    }

    // 输出都是 scrpitClouse 对象
    scrpitClouser.call()

    def nestClouser = {
    def innnerClouser = {
    // 代表闭包定义处的类
    printlin "scriptClouser this:" + this
    // 代表闭包定义处的类或者对象
    printlin "scriptClouser this:" + owner
    // 代表任意对象,默认与 ownner 一直
    printlin "scriptClouser this:" + delegate
    }
    innnerClouser.call()
    }

    // this 输出的是 nestClouser 对象,而 owner 与 delegate 输出的都是 innnerClouser 对象
    nestClouser.call()
    复制

    可以看到,如果我们直接在类、方法、变量中定义一个闭包,那么这三种关键变量的值都是一样的,但是,如果我们在闭包中又嵌套了一个闭包,那么,this 与 owner、delegate 的值就不再一样了。换言之,this 还会指向我们闭包定义处的类或者实例本身,而 owner、delegate 则会指向离它最近的那个闭包对象

    delegate 与 this、owner 的差异

    其差异代码如下代码所示:

    def nestClouser = {
    def innnerClouser = {
    // 代表闭包定义处的类
    printlin "scriptClouser this:" + this
    // 代表闭包定义处的类或者对象
    printlin "scriptClouser this:" + owner
    // 代表任意对象,默认与 ownner 一致
    printlin "scriptClouser this:" + delegate
    }

    // 修改默认的 delegate
    innnerClouser.delegate = p
    innnerClouser.call()
    }

    nestClouser.call()
    复制代

    可以看到,delegate 的值是可以修改的,并且仅仅当我们修改 delegate 的值时,delegate 的值才会与 ownner 的值不一样

    闭包的委托策略

    其示例代码如下所示:

    def stu = new Student()
    def tea = new Teacher()
    stu.pretty.delegate = tea
    // 要想使 pretty 闭包的 delegate 修改生效,必须选择其委托策略为 Closure.DELEGATE_ONLY,默认是 Closure.OWNER_FIRST。
    stu.pretty.resolveStrategy = Closure.DELEGATE_ONLY
    println stu.toString()
    复制

    需要注意的是,要想使上述 pretty 闭包的 delegate 修改生效,必须选择其委托策略为 Closure.DELEGATE_ONLY,默认是 Closure.OWNER_FIRST 的。

    3、Groovy 数据结构

    Groovy 常用的数据结构有如下 四种:

    • 1)、数组
    • 2)、List
    • 3)、Map
    • 4)、Range

    数组的使用和 Java 语言类似,最大的区别可能就是定义方式的扩展,如下代码所示:

    // 数组定义
    def array = [1, 2, 3, 4, 5] as int[]
    int[] array2 = [1, 2, 3, 4, 5]
    复制代

    下面,我们看看其它三种数据结构。

    1、List

    即链表,其底层对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类,List 变量由[]定义,其元素可以是任何对象

    链表中的元素可以通过索引存取,而且 不用担心索引越界。如果索引超过当前链表长度,List 会自动往该索引添加元素。下面,我们看看 List 最常使用的几个操作。

    1)、排序

    def test = [100, "hello", true]
    // 左移位表示向List中添加新元素
    test << 200
    // list 定义
    def list = [1, 2, 3, 4, 5]
    // 排序
    list.sort()
    // 使用自己的排序规则
    sortList.sort { a, b ->
    a == b ?0 :
    Math.abs(a) < Math.abs(b) ? 1 : -1
    }
    复制

    2)、添加

    // 添加
    list.add(6)
    list.leftShift(7)
    list << 8
    复制代码

    3)、删除

    // 删除
    list.remove(7)
    list.removeAt(7)
    list.removeElement(6)
    list.removeAll { return it % 2 == 0 }
    复制代码

    4)、查找

    // 查找
    int result = findList.find { return it % 2 == 0 }
    def result2 = findList.findAll { return it % 2 != 0 }
    def result3 = findList.any { return it % 2 != 0 }
    def result4 = findList.every { return it % 2 == 0 }

    5)、获取最小值、最大值

    // 最小值、最大值
    list.min()
    list.max(return Math.abs(it))
    复制代码

    6)、统计满足条件的数量

    // 统计满足条件的数量
    def num = findList.count { return it >= 2 }
    复制代

    Map

    表示键-值表,其 底层对应 Java 中的 LinkedHashMap

    Map 变量由[:]定义,冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象。另外,key 可以用 '' 或 "" 包起来,也可以不用引号包起来。下面,我们看看 Map 最常使用的几个操作。

    1)、存取

    其示例代码如下所示:

    aMap.keyName
    aMap['keyName']
    aMap.anotherkey = "i am map"
    aMap.anotherkey = [a: 1, b: 2]
    复制代码

    2)、each 方法

    如果我们传递的闭包是一个参数,那么它就把 entry 作为参数。如果我们传递的闭包是 2 个参数,那么它就把 key 和 value 作为参数。

    def result = ""
    [a:1, b:2].each { key, value ->
    result += "$key$value"
    }

    assert result == "a1b2"

    def socre = ""
    [a:1, b:2].each { entry ->
    result += entry
    }

    assert result == "a=1b=2"

    3)、eachWithIndex 方法

    如果闭包采用两个参数,则将传递 Map.Entry 和项目的索引(从零开始的计数器);否则,如果闭包采用三个参数,则将传递键,值和索引。

    def result = ""
    [a:1, b:3].eachWithIndex { key, value, index -> result += "$index($key$value)" }
    assert result == "0(a1)1(b3)"

    def result = ""
    [a:1, b:3].eachWithIndex { entry, index -> result += "$index($entry)" }
    assert result == "0(a=1)1(b=3)"

    4)、groupBy 方法

    按照闭包的条件进行分组,代码如下所示:

    def group = students.groupBy { def student ->
    return student.value.score >= 60 ? '及格' : '不及格'
    }
    复制代

    5)、findAll 方法

    它有两个参数,findAll 会将 Key 和 Value 分别传进 去。并且,如果 Closure 返回 true,表示该元素是自己想要的,如果返回 false 则表示该元素不是自己要找的。

    Range

    表示范围,它其实是 List 的一种拓展。其由 begin 值 + 两个点 + end 值表示。如果不想包含最后一个元素,则 begin 值 + 两个点 + < + end 表示。我们可以通过 aRange.from 与 aRange.to 来获对应的边界元素

    如果需要了解更多的数据结构操作方法,我们可以直接查 Groovy API 详细文档 即可。

    4、Groovy 面向对象

    如果不声明 public/private 等访问权限的话,Groovy 中类及其变量默认都是 public 的

    1)、元编程(Groovy 运行时)

    Groovy 运行时的逻辑处理流程图如下所示:

    为了更好的讲解元编程的用法,我们先创建一个 Person 类并调用它的 cry 方法,代码如下所示:

    // 第一个 groovy 文件中
    def person = new Person(name: 'Qndroid', age: 26)
    println person.cry()

    // 第二个 groovy 文件中
    class Person implements Serializable {

    String name

    Integer age

    def increaseAge(Integer years) {
    this.age += years
    }

    /**
    * 一个方法找不到时,调用它代替
    * @param name
    * @param args
    * @return
    */
    def invokeMethod(String name, Object args) {

    return "the method is ${name}, the params is ${args}"
    }


    def methodMissing(String name, Object args) {

    return "the method ${name} is missing"
    }
    }
    复制

    为了实现元编程,我们需要使用 metaClass,具体的使用示例如下所示:

    ExpandoMetaClass.enableGlobally()
    //为类动态的添加一个属性
    Person.metaClass.sex = 'male'
    def person = new Person(name: 'Qndroid', age: 26)
    println person.sex
    person.sex = 'female'
    println "the new sex is:" + person.sex
    //为类动态的添加方法
    Person.metaClass.sexUpperCase = { -> sex.toUpperCase() }
    def person2 = new Person(name: 'Qndroid', age: 26)
    println person2.sexUpperCase()
    //为类动态的添加静态方法
    Person.metaClass.static.createPerson = {
    String name, int age -> new Person(name: name, age: age)
    }
    def person3 = Person.createPerson('renzhiqiang', 26)
    println person3.name + " and " + person3.age

    需要注意的是通过类的 metaClass 来添加元素的这种方式每次使用时都需要重新添加,幸运的是,我们可以在注入前调用全局生效的处理,代码如下所示:

    ExpandoMetaClass.enableGlobally()
    // 在应用程序初始化的时候我们可以为第三方类添加方法
    Person.metaClass.static.createPerson = { String name,
    int age ->
    new Person(name: name, age: age)
    }
    复制代码

    2)、脚本中的变量和作用域

    对于每一个 Groovy 脚本来说,它都会生成一个 static void main 函数,main 函数中会调用一个 run 函数,脚本中的所有代码则包含在 run 函数之中。我们可以通过如下的 groovyc 命令用于将编译得到的 class 文件拷贝到 classes 文件夹下:

    // groovyc 是 groovy 的编译命令,-d classes 用于将编译得到的 class 文件拷贝到 classes 文件夹 下
    groovyc -d classes test.groovy
    复制代码

    当我们在 Groovy 脚本中定义一个变量时,由于它实际上是在 run 函数中创建的,所以脚本中的其它方法或其他脚本是无法访问它的。这个时候,我们需要使用 @Field 将当前变量标记为成员变量,其示例代码如下所示:

    import groovy.transform.Field; 

    @Field author = JsonCh


    作者:jsonchao
    链接:https://juejin.cn/post/6844904128594853902
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(1)

    前言 成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。 Groovy 作为 Gradle 这一强大构建工具的核心语言,其重要性不言而喻,但是 Groovy 本身是十分复杂的,要想全面地掌握它,我想几十篇万字...
    继续阅读 »

    前言


    成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。


    Groovy 作为 Gradle 这一强大构建工具的核心语言,其重要性不言而喻,但是 Groovy 本身是十分复杂的,要想全面地掌握它,我想几十篇万字长文也无法将其彻底描述。所幸的是,在 Gradle 领域中涉及的 Groovy 知识都是非常基础的,因此,本篇文章的目的是为了在后续深入探索 Gradle 时做好一定的基础储备。


    一、DSL 初识


    DSL(domain specific language),即领域特定语言,例如:Matliba、UML、HTML、XML 等等 DSL 语言。可以这样理解,Groovy 就是 DSL 的一个分支。


    特点



    • 1)、解决特定领域的专有问题。

    • 2)、它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做服务器开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。


    总的来说,DSL 的 核心思想 就是:“求专不求全,解决特定领域的问题”。


    二、Groovy 初识


    1、Groovy 的特点


    Groovy 的特点具有如下 三点:



    • 1)、Groovy 是一种基于 JVM 的敏捷开发语言。

    • 2)、Groovy 结合了 Python、Ruby 和 Smalltalk 众多脚本语言的许多强大的特性。

    • 3)、Groovy 可以与 Java 完美结合,而且可以使用 Java 所有的库。


    那么,在已经有了其它脚本语言的前提下,为什么还要制造出 Grvooy 语言呢?


    因为 Groovy 语言相较其它编程语言而言,其 入门的学习成本是非常低的,因为它的语法就是对 Java 的扩展,所以,我们可以用学习 Java 的方式去学习 Groovy。


    2、Groovy 语言本身的特性


    其特性主要有如下 三种:



    • 1)、语法上支持动态类型,闭包等新一代语言特性。并且,Groovy 语言的闭包比其它所有语言类型的闭包都要强大。

    • 2)、它可以无缝集成所有已经存在的 Java 类库,因为它是基于 JVM 的。

    • 3)、它即可以支持面向对象编程(基于 Java 的扩展),也可以支持面向过程编程(基于众多脚本语言的结合)。


    需要注意的是,在我们使用 Groovy 进行 Gradle 脚本编写的时候,都是使用的面向过程进行编程的


    3、Groovy 的优势


    Groovy 的优势有如下 四种:



    • 1)、它是一种更加敏捷的编程语言:在语法上构建除了非常多的语法糖,许多在 Java 层需要写的代码,在 Groovy 中是可以省略的。因此,我们可以用更少的代码实现更多的功能。

    • 2)、入门简单,但功能非常强大。

    • 3)、既可以作为编程语言也可以作为脚本语言

    • 4)、熟悉掌握 Java 的同学会非常容易掌握 Groovy。


    4、Groovy 包的结构



    Groovy 官方网址



    从官网下载好 Groovy 文件之后,我们就可以看到 Groovy 的目录结构,其中我们需要 重点关注 bin 和 doc 这个两个文件夹


    bin 文件夹


    bin 文件夹的中我们需要了解下三个重要的可执行命令文件,如下所示:



    • 1)、groovy 命令类似于 Java 中的 java 命令,用于执行 groovy Class 字节码文件。

    • 2)、groovyc 命令类似于 Java 中的 javac 命令,用于将 groovy 源文件编译成 groovy 字节码文件。

    • 3)、groovysh 命令是用来解释执行 groovy 脚本文件的。


    doc 文件夹


    doc 文件夹的下面有一个 html 文件,其中的 api 和 documentation 是我们需要重点关注的,其作用分别如下所示:



    • api:groovy 中为我们提供的一系列 API 及其 说明文档。

    • documentation:groovy 官方为我们提供的一些教程。


    5、Groovy 中的关键字


    下面是 Groovy 中所有的关键字,命名时尤其需要注意,如下所示:


    as、assert、break、case、catch、class、const、continue、def、default、
    do、else、enum、extends、false、finally、for、goto、if、implements、
    import、in、instanceof、interface、new、null、package、return、super、
    switch、this、throw、throws、trait、true、try、while
    复制代码

    6、Groovy && Java 差异学习


    1)、getter / setter


    对于每一个 field,Groovy 都会⾃动创建其与之对应的 getter 与 setter 方法,从外部可以直接调用它,并且 在使⽤ object.fieldA 来获取值或者使用 object.fieldA = value 来赋值的时候,实际上会自动转而调⽤ object.getFieldA() 和 object.setFieldA(value) 方法


    如果我们不想调用这个特殊的 getter 方法时则可以使用 .@ 直接域访问操作符


    2)、除了每行代码不用加分号外,Groovy 中函数调用的时候还可以不加括号。


    需要注意的是,我们在使用的时候,如果当前这个函数是 Groovy API 或者 Gradle
    API 中比较常用的,比如 println,就可以不带括号。否则还是带括号。不然,Groovy 可能会把属性和函数调用混淆


    3)、Groovy 语句可以不用分号结尾。


    4)、函数定义时,参数的类型也可以不指定。


    5)、Groovy 中函数的返回值也可以是无类型的,并且无返回类型的函数,其内部都是按返回 Object 类型来处理的。


    6)、当前函数如果没有使用 return 关键字返回值,则会默认返回 null,但此时必须使用 def 关键字。


    7)、在 Groovy 中,所有的 Class 类型,都可以省略 .class。


    8)、在 Groovy 中,== 相当于 Java 的 equals,,如果需要比较两个对象是否是同一个,需要使用 .is()。


    9)、Groovy 非运算符如下:


    assert (!"android") == false                      
    复制代码

    10)、Groovy 支持 ** 次方运算符,代码如下所示:


    assert  2 ** 3 == 8
    复制代码

    11)、判断是否为真可以更简洁:


        if (android) {}
    复制代码

    12)、三元表达式可以更加简洁:


    // 省略了name
    def result = name ?: "Unknown"
    复制代码

    13)、简洁的非空判断


    println order?.customer?.address
    复制代码

    14)、使用 assert 来设置断言,当断言的条件为 false 时,程序将会抛出异常。


    15)、可以使用 Number 类去替代 float、double 等类型,省去考虑精度的麻烦。


    16)、switch 方法可以同时支持更多的参数类型。


    注意,swctch 可以匹配列表当中任一元素,示例代码如下所示:


    // 输出 ok
    def num = 5.21
    switch (num) {
    case [5.21, 4, "list"]:
    return "ok"
    break
    default:
    break
    }

    作者:jsonchao
    链接:https://juejin.cn/post/6844904128594853902
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    "Gradle"系列: 一、Gradle相关概念理解,Groovy基础(4)

    五、Groovy数据结构 通过这个模块的学习,我会结合具体的例子来说明如何查阅文档来确定闭包中的参数,在讲 Map 的时候我会讲到 Groovy 常用的数据结构有如下 四种: 1)、数组 2)、List 3)、Map 4)、Range 1、数组 在 Gro...
    继续阅读 »

    五、Groovy数据结构


    通过这个模块的学习,我会结合具体的例子来说明如何查阅文档来确定闭包中的参数,在讲 Map 的时候我会讲到


    Groovy 常用的数据结构有如下 四种:



    • 1)、数组

    • 2)、List

    • 3)、Map

    • 4)、Range


    1、数组


    在 Groovy 中使用 [ ] 表示的是一个 List 集合,如果要定义 Array 数组,我们就必须强制指定为一个数组的类型

    //在 Java 中,我们一般会这样去定义一个数组
    String[] javaArray = ["Java", "Groovy", "Android"]

    //在 Groovy 中,我们一般会使用 as 关键字定义数组
    def groovyArray = ["Java", "Groovy", "Android"] as String[]

    2、List


    1)、列表集合定义


    1、List 即列表集合,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类


    2、定义一个列表集合的方式有点像 Java 中定义数组一样


    3、集合元素可以接收任意的数据类型

    //在 Groovy 中定义的集合默认就是对应于 Java 中 ArrayList 集合
    def list1 = [1,2,3]
    //打印 list 类型
    print list1.class
    //打印结果
    class java.util.ArrayList

    //集合元素可以接收任意的数据类型
    def list2 = ['erdai666', 1, true]

    那么问题来了,如果我想定义一个 LinkedList 集合,要怎么做呢?有两种方式:


    1、通过 Java 的强类型方式去定义


    2、通过 as 关键字来指定

    //方式1:通过 Java 的强类型方式去定义
    LinkedList list3 = [4, 5, 6]

    //方式2:通过 as 关键字来指定
    def list4 = [1, 2, 3] as LinkedList

    2)、列表集合增删改查

    def list = [1,2,3]
    //-------------------------- 增加元素 ---------------------------------
    //有以下几种方式
    list.add(20)
    list.leftShift(20)
    list << 20

    //-------------------------- 删除元素 ---------------------------------
    //根据下标移除元素
    list.remove(0)

    //-------------------------- 修改元素 ---------------------------------
    //根据下标修改元素
    list[0] = 100

    //-------------------------- 查询元素 ---------------------------------
    //调用闭包的 find 方法,方法中接收一个闭包,闭包的参数就是 list 中的元素
    list.find {
    println it
    }

    列表集合 Api 挺多的,对于一些其他Api,使用到的时候自行查阅文档就好了,我会在下面讲 Map 的时候演示查阅 Api 文档确定闭包的参数


    3、Map


    1)、定义


    1、Map 表示键-值表,其底层对应 Java 中的 LinkedHashMap


    2、Map 变量由[:]定义,冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象


    3、Map 的 key 可以用 '' 或 "" 或 ''' '''包起来,也可以不用引号包起来

    def map = [a: 1, 'b': true, "c" : "Groovy", '''d''' : '''ddd''']

    2)、Map 常用操作


    这里列举一些 Map 的常用操作,一些其他的 Api 使用到的时候自行查阅文档就好了

    //---------------------------- Map 中元素访问操作 ----------------
    /**
    * 有如下三种方式:
    * 1、map.key
    * 2、map[key]
    * 3、map.get(ket)
    */
    println map.a
    println map['b']
    println map.get('c')
    //打印结果
    1
    true
    Groovy

    //---------------------------- Map 中添加和修改元素 -------------------
    //如果当前 key 在 map 中不存在,则添加该元素,如果存在则修改该元素
    map.put('key','value')
    map['key'] = "value"

    3)、Map 遍历,演示查阅官方文档


    现在我要去遍历 map 中的元素,但是我不知道它的 Api 是啥,那这个时候就要去查官方 Api 文档了:


    http://docs.groovy-lang.org/latest/html/groovy-jdk/java/util/Map.html



    通过官方文档我们可以发现: each 和 eachWithIndex 的闭包参数还是不确定的,如果我们使用 each 方法,如果传递给闭包是一个参数,那么它就把 entry 作为参数,如果我们传递给闭包是两个参数,那么它就把 key 和 value 作为参数,eachWithIndex 比 each 多了个 index 下标而已.


    那么我们现在就使用以下这两个 Api :

    //下面为了打印输出的格式清晰,做了一些额外的操作
    def map = [a: 1, 'b': true, "c" : "Groovy", '''d''' : '''ddd''']

    map.each {
    print "$it.key $it.value \t"
    }
    println()

    map.each {key,value ->
    print "$key $value \t"
    }
    println()

    map.eachWithIndex {entry,index ->
    print "$entry.key $entry.value $index \t"
    }
    println()

    map.eachWithIndex { key,value,index ->
    print "$key $value $index \t"
    }
    //打印结果
    a 1 b true c Groovy d ddd
    a 1 b true c Groovy d ddd
    a 1 0 b true 1 c Groovy 2 d ddd 3
    a 1 0 b true 1 c Groovy 2 d ddd 3

    4、Range


    Range 表示范围,它其实是 List 的一种拓展。其由 begin 值 + 两个点 + end 值表示。如果不想包含最后一个元素,则 begin 值 + 两个点 + < + end 表示。我们可以通过 aRange.from 与 aRange.to 来获对应的边界元素,实际操作感受一下:

    //定义一个两端都是闭区间的范围
    def range = 1..10
    range.each {
    print it + " "
    }
    //打印值
    1 2 3 4 5 6 7 8 9 10

    //如果不想包含最后一个元素
    def range1 = 1..<10
    range1.each {
    print it + " "
    }
    //打印结果
    1 2 3 4 5 6 7 8 9

    //打印头尾边界元素
    println "$range1.from $range1.to"
    //打印结果
    1 9

    六、Groovy 文件处理


    1、IO


    下面我们开始来操作这个文件,为了闭包的可读性,我会在闭包上加上类型和参数:

    //-------------------------------1、文件定位 --------------------------------
    def file = new File('testFile.txt')

    //-----------------------2、使用 eachLine Api 每次读取一行, 闭包参数是每一行的字符串------------
    file.eachLine { String line ->
    println line
    }
    //打印结果
    erdai666
    erdai777
    erdai888

    //------------------------3、获取输入流,输出流读文件和写文件---------------------------------
    //获取输入流读取文件的每一行
    //1
    file.withInputStream { InputStream inputStream ->
    inputStream.eachLine { String it ->
    println it
    }
    }

    //2
    file.withReader { BufferedReader it ->
    it.readLines().each { String it ->
    println it
    }
    }

    //打印结果
    erdai666
    erdai777
    erdai888

    //获取输出流将字符串写入文件 下面这两种方式写入的文件内容会把之前的内容给覆盖
    //1
    file.withOutputStream { OutputStream outputStream ->
    outputStream.write("erdai999".getBytes())
    }

    //2
    file.withWriter { BufferedWriter it ->
    it.write('erdai999')
    }

    //------------------------4、通过输入输出流实现文件拷贝功能---------------------------------
    //1、通过 withOutputStream withInputStream 实现文件拷贝
    def targetFile = new File('testFile1.txt')
    targetFile.withOutputStream { OutputStream outputStream ->
    file.withInputStream { InputStream inputStream ->
    outputStream << inputStream
    }
    }

    //2、通过 withReader、withWriter 实现文件拷贝
    targetFile.withWriter {BufferedWriter bufferedWriter ->
    file.withReader {BufferedReader bufferedReader ->
    bufferedReader.eachLine {String line ->
    bufferedWriter.write(line + "\r\n")
    }
    }
    }

    2、XML 文件操作


    1)、解析 XML 文件

    //定义一个带格式的 xml 字符串
    def xml = '''
    <response>
    <value>
    <books id="1" classification="android">
    <book available="14" id="2">
    <title>第一行代码</title>
    <author id="2">郭霖</author>
    </book>
    <book available="13" id="3">
    <title>Android开发艺术探索</title>
    <author id="3">任玉刚</author>
    </book>
    </books>
    </value>
    </response>
    '''
    //创建 XmlSlurper 类对象,解析 XML 文件主要借助 XmlSlurper 这个类
    def xmlSlurper = new XmlSlurper()
    //解析 mxl 返回 response 根结点对象
    def response = xmlSlurper.parseText(xml)
    //打印一些结果
    println response.value.books[0].book[0].title.text()
    println response.value.books[0].book[0].author.text()
    //打印结果
    第一行代码
    郭霖

    //1、使用迭代器解析
    response.value.books.each{ books ->
    books.book.each{ book ->
    println book.title
    println book.author
    }
    }
    //打印结果
    第一行代码
    郭霖
    Android开发艺术探索
    任玉刚

    //2、深度遍历 XML 数据
    def str1 = response.depthFirst().findAll { book ->
    return book.author == '郭霖'
    }
    println str1
    //打印结果
    [第一行代码郭霖]

    //3、广度遍历 XML 数据
    def str2 = response.value.books.children().findAll{ node ->
    node.name() == 'book' && node.@id == '2'
    }.collect { node ->
    "$node.title $node.author"
    }
    println str2
    //打印结果
    [第一行代码 郭霖]

    2)、生成 XML 文件


    上面我们使用 XmlSlurper 这个类解析了 XML,现在我们借助 MarkupBuilder 来生成 XML ,代码如下:

    /**
    * <response>
    * <value>
    * <books id="1" classification="android">
    * <book available="14" id="2">
    * <title>第一行代码</title>
    * <author id="2">郭霖</author>
    * </book>
    * <book available="13" id="3">
    * <title>Android开发艺术探索</title>
    * <author id="3">任玉刚</author>
    * </book>
    * </books>
    * </value>
    * </response>
    */
    //方式1:通过下面这种方式 就可以实现上面的效果,但是这种方式有个弊端,数据都是写死的
    def sw = new StringWriter()
    def xmlBuilder = new MarkupBuilder(sw)
    xmlBuilder.response{
    value{
    books(id: '1',classification: 'android'){
    book(available: '14',id: '2'){
    title('第一行代码')
    author(id: '2' ,'郭霖')
    }
    book(available: '13',id: '3'){
    title('Android开发艺术探索')
    author(id: '3' ,'任玉刚')
    }
    }
    }
    }
    println sw

    //方式2:将 XML 数据对应创建相应的数据模型,就像我们解析 Json 创建相应的数据模型是一样的
    //创建 XML 对应数据模型
    class Response {

    def value = new Value()

    class Value {

    def books = new Books(id: '1', classification: 'android')

    class Books {
    def id
    def classification
    def book = [new Book(available: '14', id: '2', title: '第一行代码', authorId: 2, author: '郭霖'),
    new Book(available: '13', id: '3', title: 'Android开发艺术探索', authorId: 3, author: '任玉刚')]

    class Book {
    def available
    def id
    def title
    def authorId
    def author
    }
    }
    }
    }

    //创建 response 对象
    def response = new Response()
    //构建 XML
    xmlBuilder.response{
    value{
    books(id: response.value.books.id,classification: response.value.books.classification){
    response.value.books.book.each{
    def book1 = it
    book(available: it.available,id: it.id){
    title(book1.title)
    author(authorId: book1.authorId,book1.author)
    }
    }
    }
    }
    }
    println sw

    3、Json 解析


    Json解析主要是通过 JsonSlurper 这个类实现的,这样我们在写插件的时候就不需要额外引入第三方的 Json 解析库了,其示例代码如下所示:

    //发送请求获取服务器响应的数据
    def response = getNetWorkData("https://www.wanandroid.com/banner/json")
    println response.data[0].desc
    println response.data[0].imagePath

    def getNetWorkData(String url){
    def connect = new URL(url).openConnection()
    connect.setRequestMethod("GET")
    //这个会阻塞线程 在Android中不能这样操作 但是在桌面程序是可以的
    connect.connect()
    def response = connect.content.text

    //json转实体对象
    def jsonSlurper = new JsonSlurper()
    jsonSlurper.parseText(response)
    }
    //打印结果
    扔物线
    https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png

    7、总结


    在本篇文章中,我们主要介绍了以下几个部分:


    1、一些关于 Gradle ,Groovy 的问题


    2、搭建 Groovy 开发环境,创建一个 Groovy 工程


    3、讲解了 Groovy 的一些基础语法


    4、对闭包进行了深入的讲解


    5、讲解了 Groovy 中的数据结构和常用 Api 使用,并以 Map 举例,查阅官方文档去确定 Api 的使用和闭包的参数


    6、讲解了 Groovy 文件相关的处理


    学习了 Groovy ,对于我们后续自定义 Gradle 插件迈出了关键的一步。其次如果你学习过 Kotlin ,你会发现,它们的语法非常的类似,因此对于后续学习 Kotlin 我们也可以快速去上手。


    作者:妖孽那里逃
    链接:https://www.jianshu.com/p/124effa509bb
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    "Gradle"系列: 一、Gradle相关概念理解,Groovy基础(3)

    四、Groovy 闭包 在 Groovy 中,闭包非常的重要,因此单独用一个模块来讲 1、闭包定义 引用 Groovy 官方对闭包的定义:A closure in Groovy is an open, anonymous, block of code that...
    继续阅读 »

    四、Groovy 闭包


    在 Groovy 中,闭包非常的重要,因此单独用一个模块来讲


    1、闭包定义


    引用 Groovy 官方对闭包的定义:A closure in Groovy is an open, anonymous, block of code that can take arguments, return a value and be assigned to a variable. 翻译过来就是:Groovy 中的闭包是一个开放的、匿名的代码块,它可以接受参数、返回值并将值赋给变量。 通俗的讲,闭包可以作为方法的参数和返回值,也可以作为一个变量而存在,闭包本质上就是一段代码块,下面我们就由浅入深的来学习闭包


    2、闭包声明


    1、闭包基本的语法结构:外面一对大括号,接着是申明参数,参数类型可省略,在是一个 -> 箭头号,最后就是闭包体里面的内容


    2、闭包也可以不定义参数,如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it

    //1
    { params ->
    //do something
    }

    //2
    {
    //do something
    }

    3、闭包调用


    1、闭包可以通过 .call 方法来调用


    2、闭包可以直接用括号+参数来调用

    //定义一个闭包赋值给 closure 变量
    def closure = { params1,params2 ->
    params1 + params2
    }

    //闭包调用方式1: 闭包可以通过 .call 方法来调用
    def result1 = closure('erdai ','666')
    //闭包调用方式2: 闭包可以直接用括号+参数来调用
    def result2 = closure.call('erdai ','777')
    //打印值
    println result1
    println result2
    //打印结果
    erdai 666
    erdai 777

    //定义一个无参闭包
    def closure1 = {
    println('无定义参数闭包')
    }
    closure1() //或者调用 closure1.call()
    //打印结果
    无定义参数闭包

    4、闭包进阶


    1)、闭包中的关键变量


    每个闭包中都含有 this、owner 和 delegate 这三个内置对象,那么这三个三个内置对象有啥区别呢?我们用代码去验证一下


    注意


    1、getThisObject() 方法 和 thisObject 属性等同于 this


    2、getOwner() 方法 等同于 owner


    3、getDelegate() 方法 等同于 delegate


    这些去看闭包的源码你就会有深刻的体会


    1、我们在 GroovyGrammar.groovy 这个脚本类中定义一个闭包打印这三者的值看一下:

    //定义一个闭包
    def outerClosure = {
    println "this: " + this
    println "owner: " + owner
    println "delegate: " + delegate
    }
    //调用闭包
    outerClosure.call()
    //打印结果
    this: variable.GroovyGrammar@39dcf4b0
    owner: variable.GroovyGrammar@39dcf4b0
    delegate: variable.GroovyGrammar@39dcf4b0
    //证明当前三者都指向了GroovyGrammar这个脚本类对象

    2、我们在这个 GroovyGrammar.groovy 这个脚本类中定义一个类,类中定义一个闭包,打印看下结果:

    //定义一个 OuterClass 类
    class OuterClass {
    //定义一个闭包
    def outerClosure = {
    println "this: " + this
    println "owner: " + owner
    println "delegate: " + delegate
    }
    }

    def outerClass = new OuterClass()
    outerClass.outerClosure.call()

    //打印结果如下:
    this: variable.OuterClass@1992eaf4
    owner: variable.OuterClass@1992eaf4
    delegate: variable.OuterClass@1992eaf4
    //结果证明这三者都指向了当前 OuterClass 类对象

    3、我们在 GroovyGrammar.groovy 这个脚本类中,定义一个闭包,闭包中在定义一个闭包,打印看下结果:

    def outerClosure = {
    def innerClosure = {
    println "this: " + this
    println "owner: " + owner
    println "delegate: " + delegate
    }
    innerClosure.call()

    }
    println outerClosure
    outerClosure.call()

    //打印结果如下
    variable.GroovyGrammar$_run_closure4@64beebb7
    this: variable.GroovyGrammar@5b58ed3c
    owner: variable.GroovyGrammar$_run_closure4@64beebb7
    delegate: variable.GroovyGrammar$_run_closure4@64beebb7
    //结果证明 this 指向了当前GroovyGrammar这个脚本类对象 owner 和 delegate 都指向了 outerClosure 闭包对象

    我们梳理一下上面的三种情况:


    1、闭包定义在GroovyGrammar.groovy 这个脚本类中 this owner delegate 就指向这个脚本类对象


    2、我在这个脚本类中创建了一个 OuterClass 类,并在他里面定义了一个闭包,那么此时 this owner delegate 就指向了 OuterClass 这个类对象


    3、我在 GroovyGrammar.groovy 这个脚本类中定义了一个闭包,闭包中又定义了一个闭包,this 指向了当前GroovyGrammar这个脚本类对象, owner 和 delegate 都指向了 outerClosure 闭包对象


    因此我们可以得到结论:


    1、this 永远指向定义该闭包最近的类对象,就近原则,定义闭包时,哪个类离的最近就指向哪个,我这里的离得近是指定义闭包的这个类,包含内部类


    2、owner 永远指向定义该闭包的类对象或者闭包对象,顾名思义,闭包只能定义在类中或者闭包中


    3、delegate 和 owner 是一样的,我们在闭包的源码中可以看到,owner 会把自己的值赋给 delegate,但同时 delegate 也可以赋其他值


    注意:在我们使用 this , owner , 和 delegate 的时候, this 和 owner 默认是只读的,我们外部修改不了它,这点在源码中也有体现,但是可以对 delegate 进行操作


    2)、闭包委托策略


    下面我们就来对修改闭包的 delegate 进行实操:

    //创建一个香蕉类
    class Banana{
    def name
    }

    //创建一个橘子类
    class Orange{
    def name
    }

    //定义一个香蕉对象
    def banana = new Orange(name: '香蕉')
    //定义一个橘子对象
    def orange = new Orange(name: '橘子')
    //定义一个闭包对象
    def closure = {
    //打印值
    println delegate.name
    }
    //调用闭包
    closure.call()

    //运行一下,发现结果报错了,如下
    Caught: groovy.lang.MissingPropertyException: No such property: name for class: variable.GroovyGrammar
    //大致意思就是GroovyGrammar这个脚本类对象没有这个 name 对象

    我们来分析下报错的原因原因,分析之前我们要明白一个知识点:


    闭包的默认委托策略是 OWNER_FIRST,也就是闭包会先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找


    1、closure 这个闭包是生明在 GroovyGrammar 这个脚本类当中


    2、根据我们之前学的知识,在不改变 delegate 的情况下 delegate 和 owner 是一样的,都会指向 GroovyGrammar 这个脚本类对象


    3、GroovyGrammar 这个脚本类对象,根据闭包默认委托策略,找不到 name 这个属性


    因此报错了,知道了报错原因,那我们就修改一下闭包的 delegate , 还是上面那段代码,添加如下这句代码:

    //修改闭包的delegate
    closure.delegate = orange
    //我们在运行一下,打印结果:
    橘子

    此时闭包的 delegate 指向了 orange ,因此会打印 orange 这个对象的 name ,那么我们把 closure 的 delegate 改为 banana,肯定就会打印香蕉了

    //修改闭包的delegate
    closure.delegate = banana
    //我们在运行一下,打印结果:
    香蕉

    3)、深入闭包委托策略

    //定义一个 ClosureDepth 类
    class ClosureDepth{
    //定义一个变量 str1 赋值为 erdai666
    def str1 = 'erdai666'
    //定义一个闭包
    def outerClosure = {
    //定义一个变量 str2 赋值为 erdai777
    def str2 = 'erdai777'
    //打印str1 分析1
    println str1

    //闭包中在定义一个闭包
    def innerClosure = {
    //分析2
    println str1
    println str2
    }
    //调用内部这个闭包
    innerClosure.call()
    }
    }

    //创建 ClosureDepth 对象
    def closureDepth = new ClosureDepth()
    //调用外部闭包
    closureDepth.outerClosure.call()
    //运行程序,打印结果如下
    erdai666
    erdai666
    erdai777

    上面代码注释写的很清楚,现在我们来重点分析下分析1和分析2处的打印值:


    分析1:


    分析1处打印了 str1 , 它处于 outerClosure 这个闭包中,此时 outerClosure 这个闭包的 owner , delegate 都指向了 ClosureDepth 这个类对象,因此 ClosureDepth 这个类对象的属性和方法我们就都能调用到,因此分析1处会打印 erdai666


    分析2:


    分析2处打印了 str1和 str2,它处于 innerClosure 这个闭包中,此时 innerClosure 这个闭包的 owner 和 delegate 会指向 outerClosure 这个闭包对象,我们会发现 outerClosure 有 str2 这个属性,但是并没有 str1 这个属性,因此 outerClosure 这个闭包会向它的 owner 去寻找,因此会找到 ClosureDepth 这个类对象的 str1 属性,因此打印的 str1 是ClosureDepth 这个类对象中的属性,打印的 str2 是outerClosure 这个闭包中的属性,所以分析2处的打印结果分别是 erdai666 erdai777


    上面的例子中没有显式的给 delegate 设置一个接收者,但是无论哪层闭包都能成功访问到 str1、str2 值,这是因为默认的解析委托策略在发挥作用,Groovy 闭包的委托策略有如下几种:




    1. OWNER_FIRST:默认策略,首先从 owner 上寻找属性或方法,找不到则在 delegate 上寻找


    2. DELEGATE_FIRST:和上面相反,首先从 delegate 上寻找属性或者方法,找不到则在 owner 上寻找


    3. OWNER_ONLY:只在 owner 上寻找,delegate 被忽略


    4. DELEGATE_ONLY:和上面相反,只在 delegate 上寻找,owner 被忽略


    5. TO_SELF:高级选项,让开发者自定义策略,必须要自定义实现一个 Closure 类,一般我们这种玩家用不到


    下面我们就来修改一下闭包的委托策略,加深理解:

    class People1{
    def name = '我是People1'

    def action(){
    println '吃饭'
    }

    def closure = {
    println name
    action()
    }
    }

    class People2{
    def name = '我是People2'

    def action(){
    println '睡觉'
    }
    }

    def people1 = new People1()
    def people2 = new People2()
    people1.closure.delegate = people2
    people1.closure.call()
    //运行下程序,打印结果如下:
    我是People1
    吃饭

    what? 这是啥情况,我不是修改了 delegate 为 people2 了,怎么打印结果还是 people1 的?那是因为我们忽略了一个点,没有修改闭包委托策略,他默认是 OWNER_FIRST ,因此我们修改一下就好了,还是上面这段代码,添加一句代码如下:

    people1.closure.resolveStrategy = Closure.DELEGATE_FIRST
    //运行下程序,打印结果如下:
    我是People2
    睡觉

    到这里,相信你对闭包了解的差不多了,下面我们在看下闭包的源码就完美了


    4)、闭包 Closure 类源码


    仅贴出关键源码

    public abstract class Closure<V> extends GroovyObjectSupport implements Cloneable, Runnable, GroovyCallable<V>, Serializable {
    /**
    * 熟悉的一堆闭包委托代理策略
    */
    public static final int OWNER_FIRST = 0;
    public static final int DELEGATE_FIRST = 1;
    public static final int OWNER_ONLY = 2;
    public static final int DELEGATE_ONLY = 3;
    public static final int TO_SELF = 4;
    /**
    * 闭包对应的三个委托对象 thisObject 对应的就是 this 属性,都是用 private 修饰的,外界访问不到
    */
    private Object delegate;
    private Object owner;
    private Object thisObject;
    /**
    * 闭包委托策略
    */
    private int resolveStrategy;

    /**
    * 在闭包的构造方法中:
    * 1、将 resolveStrategy 赋值为0,也是就默认委托策略OWNER_FIRST
    * 2、thisObject ,owner ,delegate都会被赋值,delegate 赋的是 owner的值
    */
    public Closure(Object owner, Object thisObject) {
    this.resolveStrategy = 0;
    this.owner = owner;
    this.delegate = owner;
    this.thisObject = thisObject;
    CachedClosureClass cachedClass = (CachedClosureClass)ReflectionCache.getCachedClass(this.getClass());
    this.parameterTypes = cachedClass.getParameterTypes();
    this.maximumNumberOfParameters = cachedClass.getMaximumNumberOfParameters();
    }

    /**
    * thisObject 只提供了 get 方法,且 thisObject 是用 private 修饰的,因此 thisObject 即 this 只读
    */
    public Object getThisObject() {
    return this.thisObject;
    }

    /**
    * owner 只提供了 get 方法,且 owner 是用 private 修饰的,因此 owner 只读
    */
    public Object getOwner() {
    return this.owner;
    }

    /**
    * delegate 提供了 get 和 set 方法,因此 delegate 可读写
    */
    public Object getDelegate() {
    return this.delegate;
    }

    public void setDelegate(Object delegate) {
    this.delegate = delegate;
    }

    /**
    * 熟悉的委托策略设置
    */
    public void setResolveStrategy(int resolveStrategy) {
    this.resolveStrategy = resolveStrategy;
    }
    public int getResolveStrategy() {
    return resolveStrategy;
    }
    }

    到这里闭包相关的知识点就都讲完了,但是,但是,但是,重要的事情说三遍:我们使用闭包的时候,如何去确定闭包的参数呢?,这个真的很蛋疼,作为 Android 开发者,在使用 AndroidStudio 进行 Gradle 脚本编写的时候,真的是非常不友好,上面我讲了可以使用一个小技巧去解决这个问题,但是这种情况是在你知道要使用一个 Api 的情况下,比如你知道 Map 的 each 方法可以遍历,但是你不知道参数,这个时候就可以去使用。那如果你连 Api 都不知道使用,那就更加不知道闭包的参数了,因此要解决这种情况,我们就必须去查阅 Groovy 官方文档:


    http://www.groovy-lang.org/api.html


    http://docs.groovy-lang.org/latest/html/groovy-jdk/index-all.html



    作者:妖孽那里逃
    链接:https://www.jianshu.com/p/124effa509bb
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    "Gradle"系列: 一、Gradle相关概念理解,Groovy基础(2)

    三、Groovy 基础语法 再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的,既然如此,Groovy 的优势在哪里呢? 在于 Groovy 提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和...
    继续阅读 »

    三、Groovy 基础语法


    再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的,既然如此,Groovy 的优势在哪里呢?


    在于 Groovy 提供了更加灵活简单的语法,大量的语法糖以及闭包特性可以让你用更少的代码来实现和Java同样的功能。比如解析xml文件,Groovy 就非常方便,只需要几行代码就能搞定,而如果用 Java 则需要几十行代码。


    1、支持动态类型,使用 def 关键字来定义一个变量


    在 Groovy 中可以使用 def 关键字定义一个变量,当然 Java 里面定义数据类型的方式,在 Groovy 中都能用

    //Java 中,我们一般会这么定义
    int age = 16
    String name = "erdai"

    //Groovy 中,我们可以这样定义,在变量赋值后,Groovy 编译器会推断出变量的实际类型
    def age = 16
    def name = 'erdai'

    2、不用写 ; 号


    现在比较新的语言都不用写,如 Kotlin

    def age = 16
    def name = 'erdai'

    3、没有基本数据类型了,全是引用类型


    上面说到,定义一个变量使用 def 关键字,但是 Groovy 是基于 Java 扩展的,因此我们也可以使用 Java 里面的类型,如 Java 中8大基本类型:byte , short , int , long , float , double ,char,boolean

    //定义8大基本类型
    byte mByte = 1
    short mShort = 2
    int mInt = 3
    long mLong = 4
    float mFloat = 5
    double mDouble = 6
    char mChar = 'a'
    boolean mBoolean = true
    //对类型进行打印
    println(mByte.class)
    println(mShort.class)
    println(mInt.class)
    println(mLong.class)
    println(mFloat.class)
    println(mDouble.class)
    println(mChar.class)
    println(mBoolean.class)

    //打印结果如下:
    class java.lang.Byte
    class java.lang.Short
    class java.lang.Integer
    class java.lang.Long
    class java.lang.Float
    class java.lang.Double
    class java.lang.Character
    class java.lang.Boolean

    因此我们可以得出结论:Groovy中没有基本数据类型,全是引用类型,即使定义了基础类型,也会被转换成对应的包装类


    4、方法变化


    1、使用 def 关键字定义一个方法,方法不需要指定返回值类型,参数类型,方法体内的最后一行会自动作为返回值,而不需要return关键字


    2、方法调用可以不写 () ,最好还是加上 () 的好,不然可读性不好


    3、定义方法时,如果参数没有返回值类型,我们可以省略 def,使用 void 即可


    4、实际上不管有没有返回值,Groovy 中返回的都是 Object 类型


    5、类的构造方法,避免添加 def 关键字

    def sum(a,b){
    a + b
    }
    def sum = sum(1,2) //还可以写成这样,但是可读性不好 def sum = sum 1,2
    println(sum)

    //打印结果
    3

    //如果方法没有返回值,我们可以这样写:
    void doSomething(param1, param2) {

    }

    //类的构造方法,避免添加 def 关键字
    class MyClass {
    MyClass() {

    }
    }

    5、字符串变化


    在 Groovy 中有三种常用的字符串定义方式,如下所示:


    这里先解释一下可扩展字符串的含义,可扩展字符串就是字符串里面可以引用变量,表达式等等


    1 、单引号 '' 定义的字符串为不可扩展字符串


    2 、双引号 "" 定义的字符串为可扩展字符串,可扩展字符串里面可以使用 ${} 引用变量值,当 {} 里面只有一个变量,非表达式时,{}也可以去掉


    3 、三引号 ''' ''' 定义的字符串为输出带格式的不可扩展字符串

    def age = 16
    def name = 'erdai'
    //定义一个不可扩展字符串,和我门在Java中使用差不多
    def str1 = 'hello ' + name
    //定义可扩展字符串,字符串里面可以引用变量值,当 {} 里面只有一个变量时,{}也可以去掉
    def str2 = "hello $name ${name + age}"
    //定义带输出格式的不可扩展字符串 使用 \ 字符来分行
    def str3 = '''
    \
    hello
    name
    '''
    //打印类型和值 下面代码我省略了 println 方法的(),上面有讲到这种语法也是允许的
    println 'str1类型: ' + str1.class
    println 'str1输出值: ' + str1
    println 'str2类型: ' + str2.class
    println 'str2输出值: ' + str2
    println 'str3类型: ' + str3.class
    println 'str3输出值: ' + str3

    //打印结果
    str1类型: class java.lang.String
    str1输出值: hello erdai
    str2类型: class org.codehaus.groovy.runtime.GStringImpl
    str2输出值: hello erdai erdai16
    str3类型: class java.lang.String
    str3输出值:
    hello
    name

    从上面代码我们可以看到,str2 是 GStringImpl 类型的,而 str1 和 str3 是 String 类型的,那么这里我就会有个疑问,这两种类型在相互赋值的情况下是否需要强转呢?我们做个实验在测试下:

    //定义一个 String 类型的变量接收 GStringImpl 类型的变量,并没有强转
    String str4 = str2
    println 'str4类型: ' + str4.class
    println 'str4输出值: ' + str4

    //打印类型和值
    str4类型: class java.lang.String
    str4输出值: hello erdai erdai16

    因此我们可以得出结论:编码的过程中,不需要特别关注 String 和 GString 的区别,编译器会帮助我们自动转换类型


    6. 不用写 get 和 set 方法


    1、在我们创建属性的时候,Groovy会帮我们自动创建 get 和 set 方法


    2、当我们只定义了一个属性的 get 方法,而没有定义这个属性,默认这个属性只读


    3、我们在使⽤对象 object.field 来获取值或者使用 object.field = value 来赋值的时候,实际上会自动转而调⽤ object.getField() 和 object.setField(value) 方法,如果我们不想调用这个特殊的 get 方法时则可以使用 .@ 直接域访问操作符访问属性本身


    我们来模拟1,2,3这三种情况

    //情况1:在我们创建属性的时候,Groovy会帮我们自动创建 get 和 set 方法
    class People{
    def name
    def age
    }

    def people = new People()
    people.name = 'erdai'
    people.age = 19
    println "姓名: $people.name 年龄: $people.age"
    //打印结果
    姓名: erdai 年龄: 19

    //情况2 当我们定义了一个属性的 get 方法,而没有定义这个属性,默认这个属性只读
    //我们修改一下People类
    class People{
    def name
    def getAge(){
    12
    }
    }

    def people = new People()
    people.name = 'erdai'
    people.age = 19
    println "姓名: $people.name 年龄: $people.age"
    //运行一下代码 打印结果报错了,如下:
    Caught: groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: age for class: variable.People
    //大概错误意思就是我们不能修改一个只读的属性

    //情况3: 如果我们不想调用这个特殊的 get 方法时则可以使用 .@ 直接域访问操作符访问属性本身
    class People{
    def name
    def age

    def getName(){
    "My name is $name"
    }
    }
    //这里使用了命名的参数初始化和默认的构造器创建people对象,后面会讲到
    def people = new People(name: 'erdai666')
    people.age = 19
    def myName = people.@name

    //打印值
    println myName
    println "姓名: $people.name 年龄: $people.age"

    //打印结果
    erdai666
    姓名: My name is erdai666 年龄: 19
    //看到区别了吗?使用 people.name 则会去调用这个属性的get方法,而 people.@name 则会访问这个属性本身

    7、Class 是一等公民,所有的 Class 类型可以省略 .Class

    //定义一个Test类
    class Test{

    }

    //定义一个测试class的方法,从前面的语法我们知道,方法的参数类型是可以省略的
    def testClass(myClass){

    }

    //测试
    testClass(Test.class)
    testClass(Test)

    8、== 和 equals


    在 Groovy 中,== 就相当于 Java 的 equals,如果需要比较两个对象是否是同一个,需要使用 .is()

    class People{
    def name
    def age
    }

    def people1 = new People(name: 'erdai666')
    def people2 = new People(name: 'erdai666')

    println("people1.name == people2.name is: " + (people1.name == people2.name))
    println("people1 is people2 is: " + people1.is(people2))

    //打印结果
    people1.name == people2.name is: true
    people1 is people2 is: false

    9、使用 assert 来设置断言,当断言的条件为 false 时,程序将会抛出异常

    assert  2 ** 4 == 15
    //运行程序,报错了,结果如下:
    Caught: Assertion failed:
    assert 2 ** 4 == 15
    | |
    16 false

    10、支持 ** 次方运算符

    assert  2 ** 4 == 16

    11、简洁的三元表达式

    //在java中,我们会这么写
    String str = obj != null ? obj : ""

    //在Groovy中,我们可以这样写,?: 操作符表示如果左边结果不为空则取左边的值,否则取右边的值
    String str = obj ?: ""

    12、简洁的非空判断

    //在java中,我们可能会这么写
    if(obj != null){
    if(obj.group != null){
    if(obj.group.artifact != null){
    //do something
    }
    }
    }

    //在Groovy中,我们可以这样写 ?. 操作符表示如果当前调用对象为空就不执行了
    obj?.group?.artifact


    13、强大的 Switch


    在 Groovy 中,switch 方法变得更加灵活,强大,可以同时支持更多的参数类型,比在 Java 中增强了很多

    def result = 'erdai666'
    switch (result){
    case [1,2,'erdai666']:
    println "匹配到了result"
    break
    default:
    println 'default'
    break
    }
    //打印结果
    匹配到了result

    14、判断是否为 null 和 非运算符


    在 Groovy 中,所有类型都能转成布尔值,比如 null 就相当于0或者相当于false,其他则相当于true

    //在 Java 中,我们会这么用
    if (name != null && name.length > 0) {

    }

    //在 Groovy 中,可以这么用,如果name为 null 或 0 则返回 false,否则返回true
    if(name){

    }

    //非运算符 erdai 这个字符串为非 null ,因此为true,而 !erdai 则为false
    assert (!'erdai') = false

    15、可以使用 Number 类去替代 float、double 等类型,省去考虑精度的麻烦


    16、默认是 public 权限


    默认情况下,Groovy 的 class 和 方法都是 public 权限,所以我们可以省略 public 关键字,除非我们想使用 private 修饰符

    class Server { 
    String toString() { "a server" }
    }

    17、使用命名的参数初始化和默认的构造器


    Groovy中,我们在创建一个对象实例的时候,可以直接在构造方法中通过 key value 的形式给属性赋值,而不需要去写构造方法,说的有点抽象,上代码感受一下:

    //定义一个people
    class People{
    def name
    def age
    }

    //我们可以通过以下几种方式去实例化一个对象,注意我们People类里面没有写任何一个构造方法哦
    def people1 = new People()
    def people1 = new People(age: 15)
    def people2 = new People(name: 'erdai')
    def people3 = new People(age: 15,name: 'erdai')

    18、使用 with 函数操作同一个对象的多个属性和方法


    with 函数接收一个闭包,闭包下面会讲,闭包的参数就是当前调用的对象

    class People{
    def name
    def age

    void running(){
    println '跑步'
    }
    }
    //定义一个 people 对象
    def people = new People()
    //调用 with 函数 闭包参数即为peopeo 如果闭包不写参数,默认会有一个 it 参数
    people.with{
    name = "erdai"
    age = 19
    println "$name $age"
    running()
    }
    //打印结果
    erdai 19
    跑步

    19、异常捕获


    如果你实在不想关心 try 块里抛出何种异常,你可以简单的捕获所有异常,并且可以省略异常类型:

    //在 java 中我们会这样写
    try {
    // ...
    } catch (Exception e) {
    // do something
    }

    //在 Groovy 中,我们可以这样写
    try {
    // ...
    } catch (any) {
    // do something
    }


    上面 Groovy 的写法其实就是省略了参数类型,实际上 any 的参数类型也是 Exception, 并不包括 Throwable ,如果你想捕获所有的异常,你可以明确捕获异常的参数类型



    作者:妖孽那里逃
    链接:https://www.jianshu.com/p/124effa509bb
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    "Gradle"系列: 一、Gradle相关概念理解,Groovy基础(1)

    前言 引用 Gradle 官方一段对Gradle的介绍:Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build sc...
    继续阅读 »

    前言


    引用 Gradle 官方一段对Gradle的介绍:Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.翻译过来就是:Gradle 是一个开源的自动化构建工具,专注于灵活性和性能。Gradle 构建脚本是使用 Groovy 或 Kotlin DSL 编写的。 之前官网的介绍是说 Gradle 是基于 Groovy 的 DSL,为啥现在又多了个 Kotlin 呢?因为 Gradle 从5.0开始,开始支持了 Kotlin DSL,现在已经发展到了6.8.3,因此我们可以使用 Groovy 或者 Kotlin 来编写 Gradle脚本。Kotlin 现作为 Android 第一开发语言,重要性不言而喻,作为一个 Android开发者,Kotlin 是必学的,后续我也会出个 Kotlin 系列文章。今天我们的重点是介绍一些 Gradle 的相关概念,以及对 Groovy 语言的学习


    一、问题


    我学习知识喜欢以问题为导向,这样可以让我明确学习的目的,提高学习效率,下面也是我在学习 Gradle 的过程中,由浅入深所产生的一些疑问,我们都知道,Android 应用是用 Gradle 构建的,在刚开发 Android 的时候我会想:


    1、什么是自动化构建工具?


    2、Gradle 是什么?


    3、什么是 DSL?


    4、什么是 Groovy?


    5、Gradle 和 Groovy 有什么区别?


    6、静态编程语言和动态编程语言有什么区别?


    带着这些疑问,我们继续学习


    1、自动化构建工具


    在 Android 上的体现,简单的说就是自动化的编译、打包程序


    在上大学学习Java那会,老师为了让我们深刻的体验撸码的魅力,都是通过文本直接敲代码的,敲完之后把扩展名改成.java后缀,然后通过javac命令编译,编译通过后,在执行java命令去运行,那么这种文件一多,我们每次都得手动去操作,效率会大大的降低,这个时候就出现了自动化编译工具,我们只需要在编译工具中,点击编译按钮,编译完成后,无需其他手动操作,程序就可以直接运行了,自动化编译工具就是最早的自动化构建工具。那么随着业务功能的不断扩展,我们的产品需要加入多媒体资源,需要打不同的渠道包发布到不同的渠道,那就必须依靠自动化构建工具,要能支持平台、需求等方面的差异、能添加自定义任务、专门的用来打包生成最终产品的一个程序、工具,这个就是自动化构建工具。自动化构建工具本质上还是一段代码程序。这就是自动化构建工具的一个发展历程,自动化构建工具在这个过程中不断的发展和优化


    2、Gradle 是什么?


    理解了自动化构建工具,那么理解 Gradle 就比较简单了,还是引用官方的那一段话:


    Gradle 是一个开源的自动化构建工具,专注于灵活性和性能。Gradle 构建脚本是使用 Groovy 或 Kotlin DSL 编写的。


    Gradle 是 Android 的默认构建工具,Android 项目这么多东西,既有我们自己写的 java、kotlin、C++、Dart 代码,也有系统自己的 java、C,C++ 代码,还有引入的第三方代码,还有多媒体资源,这么多代码、资源打包成 APK 文件肯定要有一个规范,干这个活的就是我们熟悉的 gradle 了,总而言之,Gradle就是一个帮我们打包 APK 的工具


    3、什么是DSL?


    DSL英文全称:domain specific language,中文翻译即领域特定语言,例如:HTML,XML等 DSL 语言


    特点



    • 解决特定领域的专有问题

    • 它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做后台开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。


    总的来说,DSL 的核心思想就是:“求专不求全,解决特定领域的问题”。


    4、什么是 Groovy?


    Groovy 是基于 JVM 的脚本语言,它是基于Java扩展的动态语言


    基于 JVM 的语言有很多种,如:Groovy,Kotlin,Java,Scala等等,他们都拥有一个共同的特性:最终都会编译生成 Java 字节码文件并在 JVM 上运行。


    因为 Groovy 就是对 Java 的扩展,所以,我们可以用学习 Java 的方式去学习 Groovy 。 学习成本相对来说还是比较低的,即使开发过程中忘记 Groovy 语法,也可以用 Java 语法继续编码


    5、Gradle 和 Groovy 有什么区别?


    Gradle是基于 Groovy 的一种自动化构建工具,是运行在JVM上的一个程序,Groovy是基于JVM的一种语言,Gradle 和 Groovy 的关系就像 Android 和 Java 的关系一样


    6、静态编程语言和动态编程语言有什么区别?


    静态编程语言是在编译期就要确定变量的数据类型,而动态编程语言则是在运行期确定变量的数据类型。就像静态代理和动态代理一样,一个强调的是编译期,一个强调的是运行期,常见的静态编程语言有Java,Kotlin等等,动态编程语言有Groovy,Python等语言。

    二、Groovy 开发环境搭建与工程创建

    1、到官网下载JDK安装,并配置好 JDK 环境



    2、到官网下载好 Groovy SDK,并解压到合适的位置



    3、配置 Groovy 环境变量



    4、到官网下载 IntelliJ IDEA 开发工具并安装



    5、创建 Groovy 工程即可


    小技巧: 作为 Android 开发者,我们一般都是使用 AndroidStudio 进行开发的,但是 AndroidStudio 对于 Groovy 支持不是很友好,各种没有提示,涉及到闭包,你也不知道闭包的参数是啥?因此这个时候,你就可以使用 IntelliJ IDEA 先弄好,在复制过去,IntelliJ IDEA 对Groovy 的支持还是很友好的


    作者:妖孽那里逃
    链接:https://www.jianshu.com/p/124effa509bb
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    协程+Retrofit 让你的代码足够优雅

    目标 简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。 如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。 一、引入依赖 协程和 Retrofit 的版本:dependen...
    继续阅读 »

    目标


    简单起见,我们使用 Github 官方的 Api,查询官方返回的仓库列表。


    如果你学习过官方 Paging Demo 的源码,会发现这份代码很熟悉,因为这份代码很大一部分来自这个Demo。


    一、引入依赖


    协程和 Retrofit 的版本:

    dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2"
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0'
    }

    二、使用Retrofit


    创建一个 interface

    interface GithubApi {
    @GET("search/repositories?sort=stars")
    suspend fun searchRepos(
    @Query("q") query: String,
    @Query("page") page: Int,
    @Query("per_page") itemsPerPage: Int
    ): RepoSearchResponse
    }

    和我们平时使用 Retrofit 有两点不一样:



    1. 需要在函数前加上 suspend 修饰符

    2. 直接使用我们需要返回的类型,不需要包上 Call<T> 或者 Observable<T>


    RepoSearchResponse 是返回的数据:

    data class RepoSearchResponse(
    @SerializedName("total_count") val total: Int = 0,
    @SerializedName("items") val items: List<Repo> = emptyList()
    )

    data class Repo(
    @SerializedName("id") val id: Long,
    @SerializedName("name") val name: String,
    @SerializedName("full_name") val fullName: String,
    @SerializedName("description") val description: String?,
    @SerializedName("html_url") val url: String,
    @SerializedName("stargazers_count") val stars: Int,
    @SerializedName("forks_count") val forks: Int,
    @SerializedName("language") val language: String?
    )

    之后的步骤就和我们平常使用 Retrofit 一致:



    1. 创建一个 OkHttpClient

    2. 创建一个 Retrofit

    3. 返回上面创建的接口


    代码:

    interface GithubApi {
    //... 代码省略

    companion object {
    private const val BASE_URL = "https://api.github.com/"

    fun createGithubApi(): GithubApi {
    val logger = HttpLoggingInterceptor()
    logger.level = HttpLoggingInterceptor.Level.BASIC

    val client = OkHttpClient.Builder()
    .addInterceptor(logger)
    .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(),
    object : X509TrustManager {
    override fun checkClientTrusted(
    chain: Array<X509Certificate>,
    authType: String
    ) {}
    override fun checkServerTrusted(
    chain: Array<X509Certificate>,
    authType: String
    ) {}
    override fun getAcceptedIssuers(): Array<X509Certificate> {
    return arrayOf()
    }
    })
    .hostnameVerifier(SSLSocketClient.getHostnameVerifier())
    .build()
    return Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(GithubApi::class.java)
    }
    }
    }

    因为接口是 Https 请求,所以需要加上忽略 SSL 的验证,其他都一样了。


    三、使用协程去请求


    初始化 RecyclerView 的代码就不放了,比较简单:

    class MainActivity : AppCompatActivity() {
    val scope = MainScope()
    private val mAdapter by lazy {
    MainAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // ... 初始化RecyclerView
    fetchData()
    }

    private fun fetchData(){
    scope.launch {
    try {
    val result = GithubApi.createGithubApi().searchRepos("Android", 0, 20)
    if(result != null && !result.items.isNullOrEmpty()){
    mAdapter.submitList(result.items)
    }
    }catch (e: Exception){
    e.printStackTrace()
    }
    }
    }

    override fun onDestroy() {
    super.onDestroy()
    scope.cancel()
    }
    }

    协程中最方便的还是省去切线程的步骤,用同步代码处理耗时的异步网络请求


    需要注意的是,由于没有使用 LifecycleKTX 的扩展库,所以协程作用域的生命周期得我们自己去释放,在上面的代码中,我是在 onCreate 方法中启动了一个协程,然后在 onDestroy 方法中去取消了正在执行的任务,以防内存泄漏。


    总结


    协程 + Retrofit 的方便之处在于:使用同步代码处理异步的网络请求,减去 Retrofit 中网络回调或者 RxJava + Retrofit 的请求回调


    作者:九心_
    链接:https://www.jianshu.com/p/dd3a9323b81a
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Jetpack新成员,Paging3从吐槽到真香

    各位小伙伴们大家早上好。 随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。 现在,关于App Start和Hilt的文章我都...
    继续阅读 »

    各位小伙伴们大家早上好。


    随着Android 11的正式发布,Jetpack家族也引入了许多新的成员。我之前有承诺过,对于新引入的App Startup、Hilt、Paging 3,我会分别写一篇文章进行介绍。


    现在,关于App Start和Hilt的文章我都已经写完了,请参考 Jetpack新成员,App Startup一篇就懂 和 Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 。


    那么本篇文章,我们要学习的自然就是Paging 3了。


    Paging 3简介


    Paging是Google推出的一个应用于Android平台的分页加载库。


    事实上,Paging并不是现在才刚刚推出的,而是之前就已经推出过两个版本了。


    但Paging 3和前面两个版本的变化非常大,甚至可以说是完全不同的东西了。所以即使你之前没有学习过Paging的用法也没有关系,把Paging 3当成是一个全新的库去学习就可以了。


    我相信一定会有很多朋友在学习Paging 3的时候会产生和我相同的想法:本身Android上的分页功能并不难实现,即使没有Paging库我们也完全做得出来,但为什么Paging 3要把一个本来还算简单的功能设计得如此复杂呢?


    是的,Paging 3很复杂,至少在你还不了解它的情况下就是如此。我在第一次学习Paging 3的时候就直接被劝退了,心想着何必用这玩意委屈自己呢,自己写分页功能又不是做不出来。


    后来本着拥抱新技术的态度,我又去学习了一次Paging 3,这次算是把它基本掌握了,并且还在我的新开源项目 Glance 当中应用了Paging 3的技术。


    如果现在再让我来评价一下Paging 3,那么我大概是经历了一个由吐槽到真香的过程。理解了Paging 3之后,你会发现它提供了一套非常合理的分页架构,我们只需要按照它提供的架构去编写业务逻辑,就可以轻松实现分页功能。我希望大家在看完这篇文章之后,也能觉得Paging 3香起来。


    不过,本篇文章我不能保证它的易懂性。虽然很多朋友都觉得我写的文章简单易懂,但Paging 3的复杂性在于它关联了太多其他的知识,如协程、Flow、MVVM、RecyclerView、DiffUtil等等,如果你不能将相关联的这些知识都有所了解,那么想要掌握Paging 3就会更有难度。


    另外,由于Paging 3是Google基于Kotlin协程全新重写的一个库,所以它主要是应用于Kotlin语言(Java也能用,但是会更加复杂),并且以后这样的库会越来越多,比如Jetpack Compose等等。如果你对于Kotlin还不太了解的话,可以去参郭霖的新书《第一行代码 Android 第3版》。


    上手Paging 3


    经过我自己的总结,我发现如果零散去介绍一些Paging 3的知识点是很难能掌握得了这个库的。最好的学习方式就是直接上手,用Paging 3去做一个项目,项目做完了,你也基本就掌握了。本篇文章中我们就会采用这种方式来学习。


    另外,我相信大家之前应该都做过分页功能,正如我所说,这个功能并不难实现。但是现在,请你完全忘掉过去你所熟知的分页方案,因为它不仅对理解Paging 3没有帮助,反而在很大程度上会影响你对Paging 3的理解。


    是的,不要想着去监听列表滑动事件,滑动到底部的时候发起一个网络请求加载下一页数据。Paging 3完全不是这么用的,如果你还保留着这种过去的实现思路,在学习Paging 3的时候会很受阻。


    那么现在就让我们开始吧。


    首先新建一个Android项目,这里我给它起名为Paging3Sample。


    接下来,我们在build.gradle的dependencies当中添加必要的依赖库:

    dependencies {
    ...
    implementation 'androidx.paging:paging-runtime:3.0.0-beta01'
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    }

    注意虽然我刚才说,Paging 3是要和很多其他关联库结合到一起工作的,但是我们并不需要将这些关联库一一手动引入,引入了Paging 3之后,所有的关联库都会被自动下载下来。


    另外这里还引入了Retrofit的库,因为待会我们会从网络上请求数据,并通过Paging 3进行分页展示。


    那么在正式开始涉及Paging 3的用法之前,让我们先来把网络相关的代码搭建好,方便为Paging 3提供分页数据。


    这里我准备采用GitHub的公开API来作为我们这个项目的数据源,请注意GitHub在国内虽然一般都是可以访问的,但有时接口并不稳定,如果你无法正常请求到数据的话,请自行科学上网。


    我们可以尝试在浏览器中请求如下接口地址:

    https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1

    这个接口表示,会返回GitHub上所有Android相关的开源库,以Star数量排序,每页返回5条数据,当前请求的是第一页。


    服务器响应的数据如下,为了方便阅读,我对响应数据进行了简化:

    {
    "items": [
    {
    "id": 31792824,
    "name": "flutter",
    "description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.",
    "stargazers_count": 112819,
    },
    {
    "id": 14098069,
    "name": "free-programming-books-zh_CN",
    "description": ":books: 免费的计算机编程类中文书籍,欢迎投稿",
    "stargazers_count": 76056,
    },
    {
    "id": 111583593,
    "name": "scrcpy",
    "description": "Display and control your Android device",
    "stargazers_count": 44713,
    },
    {
    "id": 12256376,
    "name": "ionic-framework",
    "description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.",
    "stargazers_count": 43041,
    },
    {
    "id": 55076063,
    "name": "Awesome-Hacking",
    "description": "A collection of various awesome lists for hackers, pentesters and security researchers",
    "stargazers_count": 42876,
    }
    ]
    }

    简化后的数据格式还是非常好理解的,items数组中记录了第一页包含了哪些库,其中name表示该库的名字,description表示该库的描述,stargazers_count表示该库的Star数量。


    那么下面我们就根据这个接口来编写网络相关的代码吧,由于这部分都是属于Retrofit的用法,我会介绍的比较简略。


    首先根据服务器响应的Json格式定义对应的实体类,新建一个Repo类,代码如下所示:

    data class Repo(
    @SerializedName("id") val id: Int,
    @SerializedName("name") val name: String,
    @SerializedName("description") val description: String?,
    @SerializedName("stargazers_count") val starCount: Int
    )

    然后定义一个RepoResponse类,以集合的形式包裹Repo类:

    class RepoResponse(
    @SerializedName("items") val items: List<Repo> = emptyList()
    )

    接下来定义一个GitHubService用于提供网络请求接口,如下所示:

    interface GitHubService {

    @GET("search/repositories?sort=stars&q=Android")
    suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse

    companion object {
    private const val BASE_URL = "https://api.github.com/"

    fun create(): GitHubService {
    return Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(GitHubService::class.java)
    }
    }

    }

    这些都是Retrofit的标准用法,现在当调用searchRepos()函数时,Retrofit就会自动帮我们向GitHub的服务器接口发起一条网络请求,并将响应的数据解析到RepoResponse对象当中。


    好了,现在网络相关的代码都已经准备好了,下面我们就开始使用Paging 3来实现分页加载功能。


    Paging 3有几个非常关键的核心组件,我们需要分别在这几个核心组件中按部就班地实现分页逻辑。


    首先最重要的组件就是PagingSource,我们需要自定义一个子类去继承PagingSource,然后重写load()函数,并在这里提供对应当前页数的数据。


    新建一个RepoPagingSource继承自PagingSource,代码如下所示:

    class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
    return try {
    val page = params.key ?: 1 // set page 1 as default
    val pageSize = params.loadSize
    val repoResponse = gitHubService.searchRepos(page, pageSize)
    val repoItems = repoResponse.items
    val prevKey = if (page > 1) page - 1 else null
    val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
    LoadResult.Page(repoItems, prevKey, nextKey)
    } catch (e: Exception) {
    LoadResult.Error(e)
    }
    }

    override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null

    }

    这段代码并不长,但却需要好好解释一下。


    在继承PagingSource时需要声明两个泛型类型,第一个类型表示页数的数据类型,我们没有特殊需求,所以直接用整型就可以了。第二个类型表示每一项数据(注意不是每一页)所对应的对象类型,这里使用刚才定义的Repo。


    然后在load()函数当中,先通过params参数得到key,这个key就是代表着当前的页数。注意key是可能为null的,如果为null的话,我们就默认将当前页数设置为第一页。另外还可以通过params参数得到loadSize,表示每一页包含多少条数据,这个数据的大小我们可以在稍后设置。


    接下来调用刚才在GitHubService中定义的searchRepos()接口,并把page和pageSize传入,从服务器获取当前页所对应的数据。


    最后需要调用LoadResult.Page()函数,构建一个LoadResult对象并返回。注意LoadResult.Page()函数接收3个参数,第一个参数传入从响应数据解析出来的Repo列表即可,第二和第三个参数分别对应着上一页和下一页的页数。针对于上一页和下一页,我们还额外做了个判断,如果当前页已经是第一页或最后一页,那么它的上一页或下一页就为null。


    这样load()函数的作用就已经解释完了,可能你会发现,上述代码还重写了一个getRefreshKey()函数。这个函数是Paging 3.0.0-beta01版本新增的,以前的alpha版中并没有。它是属于Paging 3比较高级的用法,我们本篇文章涉及不到,所以直接返回null就可以了。


    PagingSource相关的逻辑编写完成之后,接下来需要创建一个Repository类。这是MVVM架构的一个重要组件,还不了解的朋友可以去参考《第一行代码 Android 第3版》第15章的内容。

    object Repository {

    private const val PAGE_SIZE = 50

    private val gitHubService = GitHubService.create()

    fun getPagingData(): Flow<PagingData<Repo>> {
    return Pager(
    config = PagingConfig(PAGE_SIZE),
    pagingSourceFactory = { RepoPagingSource(gitHubService) }
    ).flow
    }

    }

    这段代码虽然很短,但是却不易理解,因为用到了协程的Flow。我无法在这里展开解释Flow是什么,你可以简单将它理解成协程中对标RxJava的一项技术。


    当然这里也没有用到什么复杂的Flow技术,正如你所见,上面的代码很简短,相比于理解,这更多是一种固定的写法。


    我们定义了一个getPagingData()函数,这个函数的返回值是Flow<PagingData<Repo>>,注意除了Repo部分是可以改的,其他部分都是固定的。


    在getPagingData()函数当中,这里创建了一个Pager对象,并调用.flow将它转换成一个Flow对象。在创建Pager对象的时候,我们指定了PAGE_SIZE,也就是每页所包含的数据量。又指定了pagingSourceFactory,并将我们自定义的RepoPagingSource传入,这样Paging 3就会用它来作为用于分页的数据源了。


    将Repository编写完成之后,我们还需要再定义一个ViewModel,因为Activity是不可以直接和Repository交互的,要借助ViewModel才可以。新建一个MainViewModel类,代码如下所示:

    class MainViewModel : ViewModel() {

    fun getPagingData(): Flow<PagingData<Repo>> {
    return Repository.getPagingData().cachedIn(viewModelScope)
    }

    }

    代码很简单,就是调用了Repository中定义的getPagingData()函数而已。但是这里又额外调用了一个cachedIn()函数,这是用于将服务器返回的数据在viewModelScope这个作用域内进行缓存,假如手机横竖屏发生了旋转导致Activity重新创建,Paging 3就可以直接读取缓存中的数据,而不用重新发起网络请求了。


    写到这里,我们的这个项目已经完成了一大半了,接下来开始进行界面展示相关的工作。


    由于Paging 3是必须和RecyclerView结合使用的,下面我们定义一个RecyclerView的子项布局。新建repo_item.xml,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:orientation="vertical">

    <TextView
    android:id="@+id/name_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:maxLines="1"
    android:ellipsize="end"
    android:textColor="#5194fd"
    android:textSize="20sp"
    android:textStyle="bold" />

    <TextView
    android:id="@+id/description_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    android:maxLines="10"
    android:ellipsize="end" />

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    android:gravity="end"
    tools:ignore="UseCompoundDrawables">

    <ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:layout_marginEnd="5dp"
    android:src="@drawable/ic_star"
    tools:ignore="ContentDescription" />

    <TextView
    android:id="@+id/star_count_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical" />

    </LinearLayout>

    </LinearLayout>

    这个布局中使用到了一个图片资源,可以到本项目的源码中去获取,源码地址见文章最底部。


    接下来定义RecyclerView的适配器,但是注意,这个适配器也比较特殊,必须继承自PagingDataAdapter,代码如下所示:

    class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {

    companion object {
    private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
    override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
    return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
    return oldItem == newItem
    }
    }
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val name: TextView = itemView.findViewById(R.id.name_text)
    val description: TextView = itemView.findViewById(R.id.description_text)
    val starCount: TextView = itemView.findViewById(R.id.star_count_text)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)
    return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val repo = getItem(position)
    if (repo != null) {
    holder.name.text = repo.name
    holder.description.text = repo.description
    holder.starCount.text = repo.starCount.toString()
    }
    }

    }

    相比于一个传统的RecyclerView Adapter,这里最特殊的地方就是要提供一个COMPARATOR。因为Paging 3在内部会使用DiffUtil来管理数据变化,所以这个COMPARATOR是必须的。如果你以前用过DiffUtil的话,对此应该不会陌生。


    除此之外,我们并不需要传递数据源给到父类,因为数据源是由Paging 3在内部自己管理的。同时也不需要重写getItemCount()函数了,原因也是相同的,有多少条数据Paging 3自己就能够知道。


    其他部分就和普通的RecyclerView Adapter没什么两样了,相信大家都能够看得明白。


    接下来就差最后一步了,让我们把所有的一切都集成到Activity当中。


    修改activity_main.xml布局,在里面定义一个RecyclerView和一个ProgressBar:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <ProgressBar
    android:id="@+id/progress_bar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center" />

    </FrameLayout>

    然后修改MainActivity中的代码,如下所示:

    class MainActivity : AppCompatActivity() {

    private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }

    private val repoAdapter = RepoAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
    val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
    recyclerView.layoutManager = LinearLayoutManager(this)
    recyclerView.adapter = repoAdapter
    lifecycleScope.launch {
    viewModel.getPagingData().collect { pagingData ->
    repoAdapter.submitData(pagingData)
    }
    }
    repoAdapter.addLoadStateListener {
    when (it.refresh) {
    is LoadState.NotLoading -> {
    progressBar.visibility = View.INVISIBLE
    recyclerView.visibility = View.VISIBLE
    }
    is LoadState.Loading -> {
    progressBar.visibility = View.VISIBLE
    recyclerView.visibility = View.INVISIBLE
    }
    is LoadState.Error -> {
    val state = it.refresh as LoadState.Error
    progressBar.visibility = View.INVISIBLE
    Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    }

    这里最重要的一段代码就是调用了RepoAdapter的submitData()函数。这个函数是触发Paging 3分页功能的核心,调用这个函数之后,Paging 3就开始工作了。


    submitData()接收一个PagingData参数,这个参数我们需要调用ViewModel中返回的Flow对象的collect()函数才能获取到,collect()函数有点类似于Rxjava中的subscribe()函数,总之就是订阅了之后,消息就会源源不断往这里传。


    不过由于collect()函数是一个挂起函数,只有在协程作用域中才能调用它,因此这里又调用了lifecycleScope.launch()函数来启动一个协程。


    其他地方应该就没什么需要解释的了,都是一些传统RecyclerView的用法,相信大家都能看得懂。


    好了,这样我们就把整个项目完成了,在正式运行项目之前,别忘了在你的AndroidManifest.xml文件中添加网络权限:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.paging3sample">

    <uses-permission android:name="android.permission.INTERNET" />
    ...

    </manifest>



    可以看到,GitHub上Android相关的开源库已经成功显示出来了。并且你可以不断往下滑,Paging 3会自动加载更多的数据,仿佛让你永远也滑不到头一样。


    如次一来,使用Paging 3来进行分页加载的效果也就成功完成了。


    总结一下,相比于传统的分页实现方案,Paging 3将一些琐碎的细节进行了隐藏,比如你不需要监听列表的滑动事件,也不需要知道知道何时应该加载下一页的数据,这些都被Paging 3封装掉了。我们只需要按照Paging 3搭建好的框架去编写逻辑实现,告诉Paging 3如何去加载数据,其他的事情Paging 3都会帮我们自动完成。


    在底部显示加载状态


    根据Paging 3的设计,其实我们理论上是不应该在底部看到加载状态的。因为Paging 3会在列表还远没有滑动到底部的时候就提前加载更多的数据(这是默认属性,可配置),从而产生一种好像永远滑不到头的感觉。


    然而凡事总有意外,比如说当前的网速不太好,虽然Paging 3会提前加载下一页的数据,但是当滑动到列表底部的时候,服务器响应的数据可能还没有返回,这个时候就应该在底部显示一个正在加载的状态。


    另外,如果网络条件非常糟糕,还可能会出现加载失败的情况,此时应该在列表底部显示一个重试按钮。


    那么接下来我们就来实现这个功能,从而让项目变得更加完善。


    创建一个footer_item.xml布局,用于显示加载进度条和重试按钮:

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <ProgressBar
    android:id="@+id/progress_bar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center" />

    <Button
    android:id="@+id/retry_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="Retry" />

    </FrameLayout>

    然后创建一个FooterAdapter来作为RecyclerView的底部适配器,注意它必须继承自LoadStateAdapter,如下所示:

    class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
    val retryButton: Button = itemView.findViewById(R.id.retry_button)
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)
    val holder = ViewHolder(view)
    holder.retryButton.setOnClickListener {
    retry()
    }
    return holder
    }

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
    holder.progressBar.isVisible = loadState is LoadState.Loading
    holder.retryButton.isVisible = loadState is LoadState.Error
    }

    }

    这仍然是一个非常简单的Adapter,需要注意的地方大概只有两点。


    第一点,我们使用Kotlin的高阶函数来给重试按钮注册点击事件,这样当点击重试按钮时,构造函数中传入的函数类型参数就会被回调,我们待会将在那里加入重试逻辑。


    第二点,在onBindViewHolder()中会根据LoadState的状态来决定如何显示底部界面,如果是正在加载中那么就显示加载进度条,如果是加载失败那么就显示重试按钮。


    最后,修改MainActivity中的代码,将FooterAdapter集成到RepoAdapter当中:

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    ...
    recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
    ...
    }

    }

    代码非常简单,只需要改动一行,调用RepoAdapter的withLoadStateFooter()函数即可将FooterAdapter集成到RepoAdapter当中。


    另外注意这里使用Lambda表达式来作为传递给FooterAdapter的函数类型参数,在Lambda表示式中,调用RepoAdapter的retry()函数即可重新加载。


    这样我们就把底部显示加载状态的功能完成了,现在来测试一下吧,效果如下图所示。




    可以看到,首先我在设备上开启了飞行模式,这样当滑动到列表底部时就会显示重试按钮。


    然后把飞行模式关闭,并点击重试按钮,这样加载进度条就会显示出来,并且成功加载出新的数据了。


    最后


    本文到这里就结束了。


    不得不说,我在文章中讲解的这些知识点仍然只是Paging 3的基本用法,还有许多高级用法文中并没有涵盖。当然,这些基本用法也是最最常用的用法,所以如果你并不打算成为Paging 3大师,掌握文中的这些知识点就已经足够应对日常的开发工作了。


    如果你还想要进一步进阶学习Paging 3,可以参考Google官方的Codelab项目,地址是:

    https://developer.android.com/codelabs/android-paging


    我们刚才一起编写的Paging3Sample项目其实就是从Google官方的Codelab项目演化而来的,我根据自己的理解重写了这个项目并进行了一定的简化。直接学习原版项目,你将能学到更多的知识。


    最后,如果你需要获取Paging3Sample项目的源码,请访问以下地址:

    https://github.com/guolindev/Paging3Sample

    作者:飞鱼_9d08
    链接:https://www.jianshu.com/p/588562fbd19d
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android 常见的错误日志及相应的解决方案总结

    之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步) android.view.WindowMana...
    继续阅读 »

    之前整理过一些关于常见的错误日志,基于生产的bug日志系统,我这边会不间断的更新错误日志及相应的解决方案,抛砖引玉(PS:也许解决的方法有点菜,希望大家能给出更优的解决方案及意见反馈,非常欢迎,相互学习共同进步)


    android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

    at android.view.ViewRootImpl.setView(ViewRootImpl.java:635)
    at android.view.ColorViewRootImpl.setView(ColorViewRootImpl.java:60)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:321)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86)
    at android.widget.PopupWindow.invokePopup(PopupWindow.java:1262)
    at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1110)
    at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1069)

    以上bug出现的原因是因为PopupWindow需要依附在一个创建好的Activity上,那么出现这个异常就说明此时你的Activity还没有创建好,出现这种情况,很可能是在onCreate()或者是onStart()中调用导致的。

    下面有两种方法可以解决这个问题:



    方法一:重载Activity的onWindowFocusChanged方法,然后在里面实现相应的逻辑如下:

    public void onWindowFocusChanged(boolean hasFocus) {  
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus) {
    //执行PopupWindow相应的操作
    }
    }

    下面给大家看下这个方法的源码,有兴趣的小伙伴可以看看

     /**
    * Called when the current {@link Window} of the activity gains or loses
    * focus. This is the best indicator of whether this activity is visible
    * to the user. The default implementation clears the key tracking
    * state, so should always be called.
    *
    * <p>Note that this provides information about global focus state, which
    * is managed independently of activity lifecycles. As such, while focus
    * changes will generally have some relation to lifecycle changes (an
    * activity that is stopped will not generally get window focus), you
    * should not rely on any particular order between the callbacks here and
    * those in the other lifecycle methods such as {@link #onResume}.
    *
    * <p>As a general rule, however, a resumed activity will have window
    * focus... unless it has displayed other dialogs or popups that take
    * input focus, in which case the activity itself will not have focus
    * when the other windows have it. Likewise, the system may display
    * system-level windows (such as the status bar notification panel or
    * a system alert) which will temporarily take window input focus without
    * pausing the foreground activity.
    *
    * @param hasFocus Whether the window of this activity has focus.
    *
    * @see #hasWindowFocus()
    * @see #onResume
    * @see View#onWindowFocusChanged(boolean)
    */
    public void onWindowFocusChanged(boolean hasFocus) {
    }


    方法二:上面的那种方法是需要实现Activity的一个方法并在方法中做操作,一般我们在项目中会在一些逻辑里面showPopupWindow或者其他的,那这样就会影响一些,然后我们就针对这个源码,追溯一下会发现另外一个方法:hasWindowFocus

    /**
    * Returns true if this activity's <em>main</em> window currently has window focus.
    * Note that this is not the same as the view itself having focus.
    *
    * @return True if this activity's main window currently has window focus.
    *
    * @see #onWindowAttributesChanged(android.view.WindowManager.LayoutParams)
    */
    public boolean hasWindowFocus() {
    Window w = getWindow();
    if (w != null) {
    View d = w.getDecorView();
    if (d != null) {
    return d.hasWindowFocus();
    }
    }
    return false;
    }

    查看上面的源码,我们会发现,我们可以直接使用hasWindowFocus来判断当前的Activity有没有创建好,再去做其他操作;以上就是这个错误日志相应的解决方案,如果还有其他的希望大家补充。


    java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity


    这个问题是在使用Glide的时候生产上面爆出来的,如果遇到其他相似的错误也可以试一下,以下有两种解决方案:



    方法一:参考博文

    在使用Glide的地方加上这个判断;Util是系统自带的;

    if(Util.isOnMainThread()) {
    Glide.with(AppUtil.getContext()).load``(R.mipmap.iclunch).error(R.mipmap.cuowu).into(imageView);
    }

    在使用的Glide的界面的生命周期onDestroy中添加如下代码:

    @Override
    protected void onDestroy() {
    super.onDestroy();
    if(Util.isOnMainThread()) {
    Glide.with(this).pauseRequests();
    }
    }

    上面Destroy中with(this),改成with(AppUtil.getContext());

    不然会报: java.lang.IllegalStateException: Activity has been destroyed

    扩展:

    Glide.with(AppUtil.getContext()).resumeRequests()和 Glide.with(AppUtil.getContext()).pauseRequests()的区别:

    1.当列表在滑动的时候,调用pauseRequests()取消请求;

    2.滑动停止时,调用resumeRequests()恢复请求;

    另外Glide.clear():当你想清除掉所有的图片加载请求时,这个方法可以用到。

    ListPreloader:如果你想让列表预加载的话,可以试试这个类。

    请记住一句话:不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext;



    方法二:使用Activity提供的isFinishing和isDestroyed方法,来判断当前的Activity是不是已经销毁了或者说正在finishing,下面贴出来相应的源码:

    /**
    * Check to see whether this activity is in the process of finishing,
    * either because you called {@link #finish} on it or someone else
    * has requested that it finished. This is often used in
    * {@link #onPause} to determine whether the activity is simply pausing or
    * completely finishing.
    *
    * @return If the activity is finishing, returns true; else returns false.
    *
    * @see #finish
    */
    public boolean isFinishing() {
    return mFinished;
    }

    /**
    * Returns true if the final {@link #onDestroy()} call has been made
    * on the Activity, so this instance is now dead.
    */
    public boolean isDestroyed() {
    return mDestroyed;
    }

    附上项目相应的源码,希望有所帮助:

    final WeakReference<ImageView> imgBankLogoWeakReference = new WeakReference<>(imgBankLogo);
    final WeakReference<ImageView> imgBankBgWeakReference = new WeakReference<>(imgBankBg);
    ImageView imgBankLogoTarget = imgBankLogoWeakReference.get();
    ImageView imgBankBgTarget = imgBankBgWeakReference.get();
    if (imgBankLogoTarget != null && imgBankBgTarget != null) {
    if (!(isFinishing() || isDestroyed())) {
    Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankLogo())
    .centerCrop().into(imgBankLogoTarget);
    Glide.with(xxx.this).load(_bankCardInfo.getBankInfo().getBankBg())
    .centerCrop().into(imgBankBgTarget);
    }
    }

    java.lang.IllegalStateException: Could not find a method OnButtonClick(View) in the activity class android.view.ContextThemeWrapper for onClick handler on view class android.widget.ImageView with id 'img_apply_result'
    at android.view.View$1.onClick(View.java:4061)
    at android.view.View.performClick(View.java:4848)
    at android.view.View$PerformClick.run(View.java:20262)
    at android.os.Handler.handleCallback(Handler.java:815)
    at android.os.Handler.dispatchMessage(Handler.java:104)
    at android.os.Looper.loop(Looper.java:194)
    at android.app.ActivityThread.main(ActivityThread.java:5714)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:984)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
    Caused by: java.lang.NoSuchMethodException: OnButtonClick [class android.view.View]
    at java.lang.Class.getMethod(Class.java:664)
    at java.lang.Class.getMethod(Class.java:643)
    at android.view.View$1.onClick(View.java:4054)


    以上的错误日志出现的有点low,但是呢有时候部分人还是容易忽略:我们一般在Activity或fragment等Java代码中使用资源文件时,例如:在Java代码中对一个Imageview附一张图片,我们不能img.setImageResource(图片相应的资源ID);需要img.setImageResource(context.getResources().getDrawable(图片相应的资源ID));需要先获取文件资源,再去拿图片,但是刚刚写的那个方法现在已经过时了,下面我贴出Google官方给出的最新的方法img.setImageDrawable(ContextCompat.getDrawable(context, imgResId));其实setImageDrawable是最省内存高效的,如果担心图片过大或者图片过多影响内存和加载效率,可以自己解析图片然后通过调用setImageDrawable方法进行设置


    java.lang.NoClassDefFoundError: android.app.AppOpsManager


    Appops是Application Operations的简称,是关于应用权限管理的一套方案,但这里的应用指的是系统应用,这些API不对第三方应用开放。Appops的两个重要组成部分是AppOpsManager和AppOpsService,它们是典型的客户端和服务端设计,通过Binder跨进程调用。AppOpsManager提供标准的API供APP调用,但google有明确说明,大部分只针对系统应用。AppOpsService是做最终检查的系统服务,它的注册名字是appops, 应用可以类似于

    mAppOps=(AppOpsManager)getContext().getSystemService(Context.APP_OPS_SERVICE);的方式来获取这个服务。


    解决方法:这个api是在19新加入的,所以要注意加个判断,参考项目代码如下:



    判断是否开启通知权限(解释比较好的博客推荐

    private boolean isNotificationEnabled(Context context) {

    String CHECK_OP_NO_THROW = "checkOpNoThrow";
    String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
    if (Build.VERSION.SDK_INT < 19) {
    return true;
    }
    try {
    AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
    ApplicationInfo appInfo = context.getApplicationInfo();
    String pkg = context.getApplicationContext().getPackageName();
    int uid = appInfo.uid;
    Class appOpsClass = null;
    /* Context.APP_OPS_MANAGER */
    appOpsClass = Class.forName(AppOpsManager.class.getName());
    Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE,
    String.class);
    Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);

    int value = (Integer) opPostNotificationValue.get(Integer.class);
    return ((Integer) checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) == AppOpsManager.MODE_ALLOWED);

    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    } catch (NoSuchMethodException e) {
    e.printStackTrace();
    } catch (NoSuchFieldException e) {
    e.printStackTrace();
    } catch (InvocationTargetException e) {
    e.printStackTrace();
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    } catch (NoClassDefFoundError e) {
    e.printStackTrace();
    }
    return false;
    }

    java.lang.SecurityException: getDeviceId: Neither user 10185 nor current process has android.permission.READ_PHONE_STATE.


    这里的getDeviceId可能是获取系统状态或内容的操作,需要授予android.permission.READ_PHONE_STATE 权限,首先我们来看一下危险权限组



    我们会发现android.permission.READ_PHONE_STATE 这个权限在PHONE组里面,在Android M版本及以后,当你的应用运行在Android6.0系统上如果设置targetSdkVersion小于23的时候,它也会默认采用以前的权限管理机制,当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它会采用新的这套权限管理机制。相关动态权限爬坑这块可以看一下之前的博文(传送门)

    ,当你配置了targetSdkVersion>=23时,默认第一次安装会打开android.permission.READ_PHONE_STATE这个权限,部分手机亲测,那样依旧可以获取getDeviceId,但这个权限是可见的,用户在后续是可以关闭的。当用户关闭了这个权限,下次进来会动态弹出授权页面提醒用户授权,如果用户依旧关闭权限将获取不到DeviceId。但是国产手机的各种自定义导致部分手机会出现动态权限返回0,(PS:当用户禁止了权限,返回回调还是为已授权,例如:OPPO meizu等兼容),这样就尴尬了,如果我们拿到用户已经授权(但实际上是禁止的)就去调用

    TelephonyManager tm = (TelephonyManager) getApplicationContext()
    .getSystemService(Context.TELEPHONY_SERVICE);
    _clientInfo.setDeviceId(tm.getDeviceId());

    就会闪退,目前这边处理的思路为:第一次如果拿到就放在SharedPreferences里面存起来,当下次用户再次关闭权限也不用担心报错;


    java.util.ConcurrentModificationExceptionat java.util.ArrayList$ArrayListIterator.next(ArrayList.java:578)
    at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:637)
    at com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter.serialize(DefaultTypeAdapters.java:624)
    at com.google.gson.JsonSerializationVisitor.findAndInvokeCustomSerializer(JsonSerializationVisitor.java:184)
    at com.google.gson.JsonSerializationVisitor.visitUsingCustomHandler(JsonSerializationVisitor.java:160)
    at com.google.gson.ObjectNavigator.accept(ObjectNavigator.java:101)
    at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:62)
    at com.google.gson.JsonSerializationContextDefault.serialize(JsonSerializationContextDefault.java:53)
    at com.google.gson.Gson.toJsonTree(Gson.java:220)
    at com.google.gson.Gson.toJson(Gson.java:260)
    at com.google.gson.Gson.toJson(Gson.java:240)



    在ArrayList.addAll()中对传进来的参数没有做null判断,于是,在调用collection.toArray()函数的时候就抛异常了,activity就崩溃了


    在使用ArrayList.addAll()的时候一定要注意传入的参数会不会出现为null的情况,如果有,那么我们可以做以下判断

    if (collection!= null)
    mInfoList.addAll(Collection<? extends E> collection);

    如果为null,就不执行下面的了,我们也不能确保是不是存在null的情况,所以为了确保不会出错,在前面加个判断是一个有经验的程序员该做的。以上错误日志的原因,可以看下源码大家就可以理解了:(这个问题虽小但容易忽略,希望各位注意)

    /**
    * Appends all of the elements in the specified collection to the end of
    * this list, in the order that they are returned by the
    * specified collection's Iterator. The behavior of this operation is
    * undefined if the specified collection is modified while the operation
    * is in progress. (This implies that the behavior of this call is
    * undefined if the specified collection is this list, and this
    * list is nonempty.)
    *
    * @param c collection containing elements to be added to this list
    * @return <tt>true</tt> if this list changed as a result of the call
    * @throws NullPointerException if the specified collection is null
    */
    public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacity(size + numNew); // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
    }

    android.content.ActivityNotFoundException: Unable to find explicit activity class {com.android.browser/com.android.browser.BrowserActivity}; have you declared this activity in your AndroidManifest.xml?


    以上错误日志出现的背景是调用系统自带的浏览器出现的,原因是因为部分手机设备商更改Android原生自带的com.android.browser/com.android.browser.BrowserActivity自己弄了一个其他的,例如,生产就出现一款手机 HTC 802t,这款手机自带浏览器的代码包名为:com.htc.sense.browser,看到这块是不是想吐槽一下,所以说如果直接写以下代码,就会出现以上错误日志:

     Intent intent = new Intent();
    intent.setAction("android.intent.action.VIEW");
    intent.addCategory(Intent.CATEGORY_BROWSABLE);
    Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
    intent.setData(contentUri);
    intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));


    解决方案(PS:获取系统安装的所有的浏览器应用 过滤):

    Intent intent = new Intent();
    intent.setAction("android.intent.action.VIEW");
    intent.addCategory(Intent.CATEGORY_BROWSABLE);
    Uri contentUri = Uri.parse(_versionInfo.getAppUrl());
    intent.setData(contentUri);
    // HTC com.htc.sense.browser
    List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent,
    PackageManager.MATCH_DEFAULT_ONLY);//通过查询,获得所有ResolveInfo对象.
    for (ResolveInfo resolveInfo : resolveInfos) {
    browsers.add(resolveInfo.activityInfo.packageName);
    System.out.println(resolveInfo.activityInfo.packageName);
    }
    if (browsers.contains("com.android.browser")) {
    intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));
    }
    context.startActivity(intent);

    android.os.FileUriExposedException/NullPointerException: Attempt to invoke virtual method 'java.lang.String android.net.Uri.getPath()' on a null object reference:


    上述错误日志是Android 7.0应用间共享文件(FileProvider)兼容的问题,后续会出一篇博文来讲解:

    下面提供代码:

    /**
    * 适配7.0及以上
    *
    * @param context
    * @param file
    * @return
    */
    private static Uri getUriForFile(Context context, File file) {
    if (context == null || file == null) {
    throw new NullPointerException();
    }
    Uri uri;
    if (Build.VERSION.SDK_INT >= 24) {
    uri = FileProvider.getUriForFile(context.getApplicationContext(), "xxx.fileprovider", file);
    } else {
    uri = Uri.fromFile(file);
    }
    return uri;
    }

    AndroidManifest中配置provider:

    <provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.crfchina.market.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true" >
    <meta-data
    android:name="android.support.FILE_PROVIDER_PATHS"
    android:resource="@xml/filepaths" />
    </provider>

    下面是很久之前的备忘的,也贴出来给大家分享一下。可能涉及到其他博文的内容,如有发现,麻烦私信,我后续加上 ……


    android java.net.UnknownHostException: Unable to resolve host "...": No address associated 错误



    解决方法:




    • (1)手机3G或者WIFI没有开启



    • (2).Manifest文件没有标明网络访问权限

      如果确认网络已经正常连接并且还是出这种错误的话,那么请看下你的Manifest文件是否标明应用需要网络访问权限,如果没标明的话,也访问不了网络,也会造成这种情况的.


      //网络访问权限


      <uses-permission android:name="android.permission.INTERNET" />




    java.lang.NullPointerException: missing IConnectivityManager

    at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:52)
    at android.net.ConnectivityManager.<init>(ConnectivityManager.java:919)
    at android.app.ContextImpl$11.createService(ContextImpl.java:387)
    at android.app.ContextImpl$ServiceFetcher.getService(ContextImpl.java:278)
    at android.app.ContextImpl.getSystemService(ContextImpl.java:1676)
    at android.content.ContextWrapper.getSystemService(ContextWrapper.java:540)
    at com.crfchina.market.util.NetUtil.getNetworkState(NetUtil.java:28)


    错误日志产生原因:



    Android里面内存泄漏问题最突出的就是Activity的泄漏,而泄漏的根源大多在于单例的使用,也就是一个静态实例持有了Activity的引用。静态变量的生命周期与应用(Application)是相同的,而Activity生命周期通常比它短,也就会造成在Activity生命周期结束后,还被引用导致无法被系统回收释放。

    生成静态引用内存泄漏可能有两种情况:

    1. 应用级:应用程序代码实现的单例没有很好的管理其生命周期,导致Activity退出后仍然被引用。
    2. 系统级:Android系统级的实现的单例,被应用不小心错误调用(当然你也可以认为是系统层实现地不太友好)。

    这个主要讲下系统级的情况,这样的情况可能也有很多,举个最近发现的问题ConnectivityManager。

    通常我们获取系统服务时采用如下方式:

    context.getSystemService()

    在Android6.0系统上,如果这里的Context如果是Activity的实例,那么即使你什么也不干也会造成内存泄漏。

    这个Context在ConnectivityManager 创建时传入,这个Context在StaticOuterContextServiceFetcher中由ContextImpl对象转换为OuterContext,与就是Activity对象,所以最终ConnectivityManager的单实例持有了Activity的实例引用。这样即使Activity退出后仍然无法释放,导致内存泄漏。

    这个问题仅在6.0上出现,在5.1上ConnectivityManager实现为单例但不持有Context的引用,在5.0有以下版本ConnectivityManager既不为单例,也不持有Context的引用。

    其他服务没认真研究,不确定有没有这个问题。不过为了避免类似的情况发生,

    最好的解决办法就是:



    解决方案:



    获取系统服务getSystemService时使用ApplicationContext

    context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);


    java.lang.IllegalArgumentException: View not attached to window manager



    错误日志产生原因:



    在延时线程里调用了ProgressDialog.dismiss,但此时主Activity已经destroy了。于是应用崩溃,我写了一个 SafeProgressDialog 来避免这个问题,主要原理是覆写dismiss方法,在ProgressDialog.dismiss之前判断Activity是否存在。



    解决方案:

    class SafeProgressDialog extends ProgressDialog
    {
    Activity mParentActivity;
    public SafeProgressDialog(Context context)
    {
    super(context);
    mParentActivity = (Activity) context;
    }

    @Override
    public void dismiss()
    {
    if (mParentActivity != null && !mParentActivity.isFinishing())
    {
    super.dismiss(); //调用超类对应方法
    }
    }
    }

    Android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.test.testFragment: make sure class name exists, is public, and has an empty constructor that is public



    错误日志产生原因及解决方案:



    根据报错提示 “make sure class name exists, is public, and has an empty constructor that is public” ,若Fragement定义有带参构造函数,则一定要定义public的默认的构造函数。即可解决此问题。

    除了他说的public.还有一个就是弄个空的构造函数。

    例如我是这样定义了我的fragment。带有了构造函数

    public TestFragment(int index){
    mIndex = index;
    }

    然后我添加了一个这样的构造函数

    public TestFragment(){
    }

    java.lang.IllegalStateException: Unable to get package info for com.crfchina.market; is package not installed?



    错误日志产生原因:



    简单的卸载app 没有卸载干净,然后再次运行,当dalvik重新安装。apk文件并试图重用以前的活动从同一个包




    好了目前就总结这么多,后续还会继续更新补充!毕竟太长也没有人愿意耐下心去看,以上也是曾经遇到过坑,希望有遇到的兄弟能从中受益!欢迎大家贴一些内容作为补充,相互学习共同进步……



    作者:大荣言午
    链接:https://www.jianshu.com/p/dd9714beb7ea
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    错过了金三银四,还不赶紧准备金九银十?这份Android大厂面试大纲静下心应对,九月就是你的战场!

    感悟这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。而面试官也一样,他也可能只掌握了20%的知识,而且一个面...
    继续阅读 »

    感悟

    这个世界有一个“二八原则”在好多地方都发挥着作用,在Android开发上我认为也一样有用。做为一名Android开发者,你也许只会用到Android开发知识中的20%,有80%其实你学了也不一定会用。

    而面试官也一样,他也可能只掌握了20%的知识,而且一个面试也不会有足够多的时间给你展示你全部的知识,而往往只会注意开发中最常遇到的20%。

    这时候,你对这些问题理解的深度就显得尤为重要。回答了10个问题,而每个问题都只是浅显分析,还没有你将一个问题讲得透彻、全面更能让面试官加分。

    当然这并不意味着当你要准备跳槽,要做面试准备的时候,你就只盯着几个自己感兴趣的课题,使劲背,使劲学,而其他的知识点就完全不学了。

    想要面试的时候完胜面试官,最简便的,最稳妥的办法就是将一套完整系统的面试题全部刷完,然后再进行自我总结。

    我知道有很多人最近都在为跳槽换工作面试做准备,所以在这里,我把我所收集到的面试大纲,分享给大家。

    • 阿里巴巴

    • LRUCache原理
    • 图片加载原理
    • 模块化实现(好处,原因)
    • JVM
    • 视频加密传输
    • 统计启动时长,标准
    • 如何保持应用的稳定性
    • ThreadLocal 原理
    • 谈谈classloader
    • 动态布局
    • 热修复,插件化
    • HashMap源码,SpareArray原理
    • 性能优化,怎么保证应用启动不卡顿
    • 怎么去除重复代码
    • SP是进程同步的吗?有什么方法做到同步
    • 介绍下SurfView
    • HashMap实现原理,ConcurrentHashMap 的实现原理
    • BroadcastReceiver,LocalBroadcastReceiver 区别
    • Bundle 机制
    • Handler 机制
    • android 事件传递机制
    • 线程间 操作 List
    • App启动流程,从点击桌面开始
    • 动态加载
    • 类加载器
    • OSGI
    • Https请求慢的解决办法,DNS,携带数据,直接访问IP
    • GC回收策略
    • 画出 Android 的大体架构图
    • 描述清点击 Android Studio 的 build 按钮后发生了什么
    • 大体说清一个应用程序安装到手机上时发生了什么;
    • 对 Dalvik、ART 虚拟机有基本的了解;
    • Android 上的 Inter-Process-Communication 跨进程通信时如何工作的;
    • App 是如何沙箱化,为什么要这么做;
    • 权限管理系统(底层的权限是如何进行 grant 的)
    • 进程和 Application 的生命周期;
    • 系统启动流程 Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程
    • recycleview listview 的区别,性能
    • 排序,快速排序的实现
    • 树:B+树的介绍
    • 图:有向无环图的解释
    • TCP/UDP的区别
    • synchronized与Lock的区别
    • volatile
    • Java线程池
    • Java中对象的生命周期
    • 类加载机制
    • 双亲委派模型
    • Android事件分发机制
    • MVP模式
    • RxJava
    • 抽象类和接口的区别
    • 集合 Set实现 Hash 怎么防止碰撞
    • JVM 内存区域 开线程影响哪块内存
    • 垃圾收集机制 对象创建,新生代与老年代
    • 二叉树 深度遍历与广度遍历
    • B树、B+树
    • 消息机制
    • 进程调度
    • 进程与线程
    • 死锁
    • 进程状态
    • JVM内存模型
    • 并发集合了解哪些
    • ConCurrentHashMap实现
    • CAS介绍
    • 开启线程的三种方式,run()和start()方法区别
    • 线程池
    • 常用数据结构简介
    • 判断环(猜测应该是链表环)
    • 排序,堆排序实现
    • 链表反转

    • 腾讯

    • synchronized用法
    • volatile用法
    • 动态权限适配方案,权限组的概念
    • 网络请求缓存处理,okhttp如何处理网络缓存的
    • 图片加载库相关,bitmap如何处理大图,如一张30M的大图,如何预防OOM
    • 进程保活
    • listview图片加载错乱的原理和解决方案
    • https相关,如何验证证书的合法性,https中哪里用了对称加密,哪里用了非对称加密,对加密算法(如RSA)等是否有了解

    • 滴滴

    • MVP
    • 广播(动态注册和静态注册区别,有序广播和标准广播)
    • service生命周期
    • handler实现机制(很多细节需要关注:如线程如何建立和退出消息循环等等)
    • 多线程(关于AsyncTask缺陷引发的思考)
    • 数据库数据迁移问题
    • 设计模式相关(例如Android中哪里使用了观察者模式,单例模式相关)
    • x个苹果,一天只能吃一个、两个、或者三个,问多少天可以吃完
    • TCP与UDP区别与应用(三次握手和四次挥手)涉及到部分细节(如client如何确定自己发送的消息被server收到) HTTP相关 提到过Websocket 问了WebSocket相关以及与socket的区别
    • 是否熟悉Android jni开发,jni如何调用java层代码
    • 进程间通信的方式
    • java注解
    • 计算一个view的嵌套层级
    • 项目组件化的理解
    • 多线程断点续传原理
    • Android系统为什么会设计ContentProvider,进程共享和线程安全问题
    • jvm相关
    • Android相关优化(如内存优化、网络优化、布局优化、电量优化、业务优化)
    • EventBus实现原理

    • 美团

    • static synchronized 方法的多线程访问和作用,同一个类里面两个synchronized方法,两个线程同时访问的问题
    • 内部类和静态内部类和匿名内部类,以及项目中的应用
    • handler发消息给子线程,looper怎么启动
    • View事件传递
    • activity栈
    • 封装view的时候怎么知道view的大小
    • arraylist和linkedlist的区别,以及应用场景
    • 怎么启动service,service和activity怎么进行数据交互
    • 下拉状态栏是不是影响activity的生命周期,如果在onStop的时候做了网络请求,onResume的时候怎么恢复
    • view渲染

    • 今日头条

    • 数据结构中堆的概念,堆排序
    • 死锁的概念,怎么避免死锁
    • ReentrantLock 、synchronized和volatile(n面)
    • HashMap
    • singleTask启动模式
    • 用到的一些开源框架,介绍一个看过源码的,内部实现过程。
    • 消息机制实现
    • ReentrantLock的内部实现
    • App启动崩溃异常捕捉
    • 事件传递机制的介绍
    • ListView的优化
    • 二叉树,给出根节点和目标节点,找出从根节点到目标节点的路径
    • 模式MVP,MVC介绍
    • 断点续传的实现
    • 集合的接口和具体实现类,介绍
    • TreeMap具体实现
    • synchronized与ReentrantLock
    • 手写生产者/消费者模式
    • 逻辑地址与物理地址,为什么使用逻辑地址
    • 一个无序,不重复数组,输出N个元素,使得N个元素的和相加为M,给出时间复杂度、空间复杂度。手写算法
    • Android进程分类
    • 前台切换到后台,然后再回到前台,Activity生命周期回调方法。弹出Dialog,生命值周期回调方法。
    • Activity的启动模式

    • 爱奇艺

    • RxJava的功能与原理实现
    • RecycleView的使用,原理,RecycleView优化
    • ANR的原因
    • 四大组件
    • Service的开启方式
    • Activity与Service通信的方式
    • Activity之间的通信方式
    • HashMap的实现,与HashSet的区别
    • JVM内存模型,内存区域
    • Java中同步使用的关键字,死锁
    • MVP模式
    • Java设计模式,观察者模式
    • Activity与Fragment之间生命周期比较
    • 广播的使用场景

    • 百度

    • Bitmap 使用时候注意什么?
    • Oom 是否可以try catch ?
    • 内存泄露如何产生?
    • 适配器模式,装饰者模式,外观模式的异同?
    • ANR 如何产生?
    • String buffer 与string builder 的区别?
    • 如何保证线程安全?
    • java四中引用
    • Jni 用过么?
    • 多进程场景遇见过么?
    • 关于handler,在任何地方new handler 都是什么线程下
    • sqlite升级,增加字段的语句
    • bitmap recycler 相关
    • 强引用置为null,会不会被回收?
    • glide 使用什么缓存?
    • Glide 内存缓存如何控制大小?
    • 如何保证多线程读写文件的安全?

    • 携程

    • Activity启动模式
    • 广播的使用方式,场景
    • App中唤醒其他进程的实现方式
    • AndroidManifest的作用与理解
    • List,Set,Map的区别
    • HashSet与HashMap怎么判断集合元素重复
    • Java中内存区域与垃圾回收机制
    • EventBus作用,实现方式,代替EventBus的方式
    • Android中开启摄像头的主要步骤

    • 网易

    • 集合
    • concurrenthashmap
    • volatile
    • synchronized与Lock
    • Java线程池
    • wait/notify
    • NIO
    • 垃圾收集器
    • Activity生命周期
    • AlertDialog,popupWindow,Activity区别

    • 小米

    • String 为什么要设计成不可变的?
    • fragment 各种情况下的生命周期
    • Activity 上有 Dialog 的时候按 home 键时的生命周期
    • 横竖屏切换的时候,Activity 各种情况下的生命周期
    • Application 和 Activity 的 context 对象的区别
    • 序列化的作用,以及 Android 两种序列化的区别。
    • List 和 Map 的实现方式以及存储方式。
    • 静态内部类的设计意图。
    • 线程如何关闭,以及如何防止线程的内存泄漏

    • 360

    • 软引用、弱引用区别
    • 垃圾回收
    • 多线程:怎么用、有什么问题要注意;Android线程有没有上限,然后提到线程池的上限
    • JVM
    • OOM,内存泄漏
    • ANR怎么分析解决
    • LinearLayout、RelativeLayout、FrameLayout的特性、使用场景
    • 如何实现Fragment的滑动
    • ViewPager使用细节,如何设置成每次只初始化当前的Fragment,其他的不初始化
    • ListView重用的是什么
    • 进程间通信的机制
    • AIDL机制
    • AsyncTask机制
    • 如何取消AsyncTask
    • 序列化
    • Android为什么引入Parcelable
    • 有没有尝试简化Parcelable的使用
    • AIDL机制
    • 项目:拉活怎么做的
    • 应用安装过程
    • 某海外直播公司
    • 线程和进程的区别?
    • 为什么要有线程,而不是仅仅用进程?
    • 算法判断单链表成环与否?
    • 如何实现线程同步?
    • hashmap数据结构?
    • arraylist 与 linkedlist 异同?
    • object类的equal 和hashcode 方法重写,为什么?
    • hashmap如何put数据(从hashmap源码角度讲解)?
    • 简述IPC?
    • fragment之间传递数据的方式?
    • 简述tcp四次挥手?
    • threadlocal原理
    • 内存泄漏的可能原因?
    • 用IDE如何分析内存泄漏?
    • OOM的可能原因?
    • 线程死锁的4个条件?
    • 差值器&估值器
    • 简述消息机制相关
    • 进程间通信方式?
    • Binder相关?
    • 触摸事件的分发?
    • 简述Activity启动全部过程?
    • okhttp源码?
    • RxJava简介及其源码解读?
    • 性能优化如何分析systrace?
    • 广播的分类?
    • 点击事件被拦截,但是相传到下面的view,如何操作?
    • Glide源码?
    • ActicityThread相关?
    • volatile的原理
    • synchronize的原理
    • lock原理
    • 翻转一个单项链表
    • string to integer
    • 合并多个单有序链表(假设都是递增的)
    • 其他公司
    • 四大组件
    • Android中数据存储方式
    • 微信主页面的实现方式
    • 微信上消息小红点的原理
    • 两个不重复的数组集合中,求共同的元素。
    • 上一问扩展,海量数据,内存中放不下,怎么求出。
    • Java中String的了解。
    • ArrayList与LinkedList区别
    • 堆排序过程,时间复杂度,空间复杂度
    • 快速排序的时间复杂度,空间复杂度
    • RxJava的作用,与平时使用的异步操作来比,优势
    • Android消息机制原理
    • Binder机制介绍
    • 为什么不能在子线程更新UI
    • JVM内存模型
    • Android中进程内存的分配,能不能自己分配定额内存
    • 垃圾回收机制与调用System.gc()区别
    • Android事件分发机制
    • 断点续传的实现
    • RxJava的作用,优缺点

    版权声明:本文为CSDN博主「Young-G2333」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/YoungOne2333/article/details/115679291

    收起阅读 »

    Material Design实战之可折叠式标题栏

    CollapsingToolbarLayout1.基本介绍CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果注意:Collapsin...
    继续阅读 »

    CollapsingToolbarLayout

    1.基本介绍

    CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,还可以实现更加华丽的效果

    注意:
    CollapsingToolbarLayout是不能独立存在的,它在设计的时候就被限定只能作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout的子布局。。

    2.具体使用

    ①我们需要一个具体展示水果详情的页面

    在这里就新建一个FruitActivity。

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FruitActivity">
    <com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:id="@+id/appBar">
    <com.google.android.material.appbar.CollapsingToolbarLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/mCollapsingToolbarLayout"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:contentScrim="@color/purple_500"
    app:layout_scrollFlags="scroll|exitUntilCollapsed"
    >
    <ImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fruitImageView"
    android:scaleType="centerCrop"
    app:layout_collapseMode="parallax"/>
    <androidx.appcompat.widget.Toolbar
    android:layout_width="match_parent"
    android:layout_height="?actionBarSize"
    android:id="@+id/toolbar"
    app:layout_collapseMode="pin"/>
    </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>



    app:contentScrim用于指定CollapsingToolbarLayout在趋于折叠状态以及折叠之后的背景色。
    app:layout_scrollFlags的exitUntilCollapsed表示当CollapsingToolbarLayout随着滚动完成折叠之后就保留在界面上,不再移出屏幕。
    里面的ImageView和Toolbar就是标题栏的具体内容。
    app:layout_collapseMode表示设置折叠过程中的折叠样式。
    然后我们在加一个NestedScrollView,它不仅允许使用滚动来查看屏幕以外的数据,而且还增加了嵌套响应滚动事件的功能。
    布局文件全部代码

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FruitActivity">
    <com.google.android.material.appbar.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:id="@+id/appBar">
    <com.google.android.material.appbar.CollapsingToolbarLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/mCollapsingToolbarLayout"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:contentScrim="@color/purple_500"
    app:layout_scrollFlags="scroll|exitUntilCollapsed"
    >
    <ImageView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fruitImageView"
    android:scaleType="centerCrop"
    app:layout_collapseMode="parallax"/>
    <androidx.appcompat.widget.Toolbar
    android:layout_width="match_parent"
    android:layout_height="?actionBarSize"
    android:id="@+id/toolbar"
    app:layout_collapseMode="pin"/>
    </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >
    <com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="15dp"
    android:layout_marginTop="35dp"
    android:layout_marginLeft="15dp"
    android:layout_marginRight="15dp"
    app:cardCornerRadius="4dp">
    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/fruitContentText"
    android:layout_margin="10dp"
    />
    </com.google.android.material.card.MaterialCardView>
    </LinearLayout>
    </androidx.core.widget.NestedScrollView>
    </androidx.coordinatorlayout.widget.CoordinatorLayout>



    很好理解,就不解释了

    ②编写功能逻辑

    public class FruitActivity extends AppCompatActivity {
    static String FRUIT_NAME = "fruit_name";
    static String FRUIT_IMAGE_ID = "fruit_image_id";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fruit);
    String fruitName = getIntent ().getStringExtra(FRUIT_NAME);
    String fruitImageId = getIntent ().getStringExtra(FRUIT_IMAGE_ID);

    Toolbar toolbar = findViewById (R.id.toolbar);
    setSupportActionBar(toolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

    CollapsingToolbarLayout collapsingToolbarLayout = findViewById (R.id.mCollapsingToolbarLayout);
    collapsingToolbarLayout.setTitle(fruitName);
    Glide.with(this).load(fruitImageId).into((ImageView) findViewById (R.id.fruitImageView));

    TextView textView = findViewById (R.id.fruitContentText);
    }
    }


    textView.setText("声卡的那句阿奎那飞机咔叽脑筋那就是可能安东尼上级领导那就ask的年纪ask" +
    "打卡时间开机卡死的你课件撒的就看撒贷记卡十多年按实际困难贷记卡大卡司你可记得是" +
    "多久啊是当年就卡死的你叫阿三的");

    toolbar.setNavigationOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    finish();

    }
    });



    没啥难度,不说了

    ③在RecyclerView的适配器中增加点击事件

    public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.MyViewHolder> {
    List<Fruit> fruits = new ArrayList<>();
    Context context;
    FruitAdapter(Context context){
    this.context = context;
    for (int i = 0; i < 30; i++) {
    fruits.add(new Fruit("香蕉",R.mipmap.banana));
    }
    }
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
    MyViewHolder holder = new MyViewHolder(view);
    holder.itemView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    int position = holder.getAdapterPosition();
    Fruit fruit = fruits.get(position);
    Intent intent = new Intent(context, FruitActivity.class);
    intent.putExtra(FruitActivity.FRUIT_NAME,fruit.name);
    intent.putExtra(FruitActivity.FRUIT_IMAGE_ID,fruit.imageId);
    context.startActivity(intent);
    }
    });
    return holder;
    }
    }


    ————————————————
    版权声明:本文为CSDN博主「独饮敌敌畏丶」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/afdafvdaa/article/details/115583226

    收起阅读 »

    Android Handler消息传递机制

    Android中只允许UI线程(也就是主线程)修改Activity里的UI组件。实际开发中,新启动的线程需要周期性地改变界面组件的属性值就需要借助Handler的消息传递机制。Handler类Handler类的主要作用:在新启动的线程中发送消息在主线程中获取、...
    继续阅读 »

    Android中只允许UI线程(也就是主线程)修改Activity里的UI组件。实际开发中,新启动的线程需要周期性地改变界面组件的属性值就需要借助Handler的消息传递机制。

    Handler类

    Handler类的主要作用:

    在新启动的线程中发送消息
    在主线程中获取、处理消息
    Handler类包含如下方法用于发送、处理消息。

    handleMessage(Message msg):处理消息的方法。该方法通常用于被重写。
    hasMessages(int what):检查消息队列中是否包含what属性为指定值的消息。
    hasMessages(int what,Object object):检查消息队列中是否包含what属性为指定值且object属性为指定对象的消息。
    多个重载的 Message obtainMessage():获取消息。
    sendEmptyMessage(int what):发送空消息。
    sendEmptyMessageDelayed(int what,long delayMillis):指定多少毫秒之后发送空消
    sendMessage(Message msg):立即发送消息。
    sendMessageDelayed(Message msg,long delayMillis):指定多少毫秒之后发送消息。
    借助于上面这些方法,程序可以方便地利用Handler来进行消息传递。
    关于Handler的源码解读,可参考别人写的《Android 多线程之 Handler 源码分析》

    实例:自动轮播图片

    本实例通过一个新线程来周期性的修改ImageView所显示的图片(因为不允许其他线程访问Activity的界面组件,故在程序中发送消息通知系统更新ImageView组件,故不需要实例Looper),布局文件非常简单,故直接给程序代码:

    package com.example.testapp1.activity;

    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;

    import androidx.annotation.NonNull;
    import androidx.appcompat.app.AppCompatActivity;

    import com.example.testapp1.R;
    import com.example.testapp1.control.RoundImageView;

    import java.lang.ref.WeakReference;
    import java.util.Timer;
    import java.util.TimerTask;

    public class NextActivity extends AppCompatActivity {
    private RoundImageView imageShow;

    static class ImageHandler extends Handler {
    private WeakReference nextActivityWeakReference;

    public ImageHandler(WeakReference nextActivityWeakReference) {
    this.nextActivityWeakReference = nextActivityWeakReference;
    }

    private int[] imageIds = new int[]{R.drawable.a383f7735d8cd09fb81ff979b2f3d599
    , R.drawable.b6ab4abe4db592b27ea678345b0c3416
    , R.mipmap.head1
    , R.drawable.b6ab4abe4db592b27ea678345b0c3416};
    private int currentImageId = 0;

    @Override
    public void handleMessage(@NonNull Message msg) {
    super.handleMessage(msg);
    if (msg.what == 0x1233) {
    nextActivityWeakReference.get().imageShow.setImageResource(imageIds[currentImageId++ % imageIds.length]);
    }
    }
    }

    ImageHandler imageHandler = new ImageHandler(new WeakReference<>(this));

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.next);
    imageShow = findViewById(R.id.headImg);
    new Timer().schedule(new TimerTask() {
    @Override
    public void run() {
    imageHandler.sendEmptyMessage(0x1233);
    }
    }, 0, 2000);
    }
    }




    上述代码中,TimeTask对象的本质就是启动一条新线程。

    Handler、Loop、MessageQueue的工作原理

    Message: Handler接收和处理的消息对象。
    Looper:每个线程只能拥有一个Looper。它的loop方法负责读取 MessageQueue中的消息,读到信息之后就把消息交给发送该消息的Handler进行处理。
    MessageQueue:消息队列,它采用先进先出的方式来管理Message。程序创建Looper对象时,会在它的构造器中创建MessageQueue对象。Looper的构造器源代码如下:

    private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
    }



    该构造器使用了private修饰,表明程序员无法通过构造器创建Looper对象。从上面的代码不难看出,程序在初始化Looper时会创建一个与之关联的 MessagQueue,这个MessageOuee就负责管理消息。

    Handler:它的作用有两个,即发送消息和处理消息,程序使用Handler发送消息,由Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果希望Handler正常工作,必须在当前线程中有一个MessageQueue;否则消息就没有 MessageQueue进行保存了。不过MessageQueue是由Looper负责管理的,也就是说,如果希望Handler正常工作,必须在当前线程中有一个Looper对象。为了保证当前线程中有Looper对象,可以分如下两种情况处理。
    在主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可通过Handler来发送消息、处理消息了。
    程序员自己启动的子线程,必须自己创建一个Looper对象,并启动它。创建 Looper对象调用它的prepare(方法即可。
    prepare()方法保证每个线程最多只有一个Looper对象。prepare()方法的源代码如下:

    public static void prepare() {
    prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
    }



    接下来调用Looper的静态loop()方法来启动它。loop()方法使用一个死循环不断取出MessageQueue中的消息,并将取出的消息分给该消息对应的Handler进行处理。下面是Looper类的loop()方法的源代码:

    /**
    * Run the message queue in this thread. Be sure to call
    * {@link #quit()} to end the loop.
    */
    public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
    throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
    Slog.w(TAG, "Loop again would have the queued messages be executed"
    + " before this one completed.");
    }

    me.mInLoop = true;
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
    SystemProperties.getInt("log.looper."
    + Process.myUid() + "."
    + Thread.currentThread().getName()
    + ".slow", 0);

    boolean slowDeliveryDetected = false;

    for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
    // No message indicates that the message queue is quitting.
    return;
    }

    // This must be in a local variable, in case a UI event sets the logger
    final Printer logging = me.mLogging;
    if (logging != null) {
    logging.println(">>>>> Dispatching to " + msg.target + " " +
    msg.callback + ": " + msg.what);
    }
    // Make sure the observer won't change while processing a transaction.
    final Observer observer = sObserver;

    final long traceTag = me.mTraceTag;
    long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
    long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
    if (thresholdOverride > 0) {
    slowDispatchThresholdMs = thresholdOverride;
    slowDeliveryThresholdMs = thresholdOverride;
    }
    final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
    final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

    final boolean needStartTime = logSlowDelivery || logSlowDispatch;
    final boolean needEndTime = logSlowDispatch;

    if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
    Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
    }

    final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
    final long dispatchEnd;
    Object token = null;
    if (observer != null) {
    token = observer.messageDispatchStarting();
    }
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
    try {
    msg.target.dispatchMessage(msg);
    if (observer != null) {
    observer.messageDispatched(token, msg);
    }
    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } catch (Exception exception) {
    if (observer != null) {
    observer.dispatchingThrewException(token, msg, exception);
    }
    throw exception;
    } finally {
    ThreadLocalWorkSource.restore(origWorkSource);
    if (traceTag != 0) {
    Trace.traceEnd(traceTag);
    }
    }
    if (logSlowDelivery) {
    if (slowDeliveryDetected) {
    if ((dispatchStart - msg.when) <= 10) {
    Slog.w(TAG, "Drained");
    slowDeliveryDetected = false;
    }
    } else {
    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
    msg)) {
    // Once we write a slow delivery log, suppress until the queue drains.
    slowDeliveryDetected = true;
    }
    }
    }
    if (logSlowDispatch) {
    showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
    }

    if (logging != null) {
    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

    // Make sure that during the course of dispatching the
    // identity of the thread wasn't corrupted.
    final long newIdent = Binder.clearCallingIdentity();
    if (ident != newIdent) {
    Log.wtf(TAG, "Thread identity changed from 0x"
    + Long.toHexString(ident) + " to 0x"
    + Long.toHexString(newIdent) + " while dispatching to "
    + msg.target.getClass().getName() + " "
    + msg.callback + " what=" + msg.what);
    }

    msg.recycleUnchecked();
    }
    }


    归纳起来,Looper、MessageQueue、Handler各自的作用如下:

    Looper:每个线程只有一个Looper,它负责管理MessageQueue,会不断地从MessageQueag中取出消息,并将消息分给对应的Handler处理。
    MessageQueue:由Looper负责管理。它采用先进先出的方式来管理Message。
    Handler:它能把消息发送给 Looper管理的MessageQueue,并负责处理 Looper分给它的消息。

    在线程中使用Handler的步骤如下:

    调用Looper的 prepare()方法为当前线程创建Looper对象,创建Looper对象时,它的构造器会创建与之配套的MessageQueue。
    有了Looper之后,创建 Handler子类的实例,重写 handleMessage(方法,该方法负责处理来自其他线程的消息。
    调用Looper的loopO方法启动Looper。

    实例:使用新线程实现点击图片弹出图片内容

    1.布局文件:


    android:id="@+id/constraintlayout2"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_gravity="center"
    android:layout_height="wrap_content">

    android:id="@+id/imageView"
    android:layout_width="50dp"
    android:layout_height="50dp"
    tools:ignore="MissingConstraints"
    tools:src="@drawable/ic_launcher_foreground" />

    android:id="@+id/textView3"
    android:layout_width="100dp"
    android:layout_height="50dp"
    android:gravity="center"
    android:visibility="gone"
    app:layout_constraintStart_toEndOf="@+id/imageView"
    app:layout_constraintTop_toTopOf="@+id/constraintlayout2"
    tools:text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    tools:visibility="visible" />


    布局文件比较简单,就是使用约束布局,在其中放入一个图片控件和文本控件(不展示)。
    JAVA代码:

    private ImageThread imageThread;

    class ImageHandler extends Handler {
    @Override
    public void handleMessage(@NonNull Message msg) {
    super.handleMessage(msg);
    if(msg.what == 0x123){
    String imageText = msg.getData().getString("ImageText");
    Toast.makeText(mContext, imageText, Toast.LENGTH_LONG).show();
    }
    }
    }

    class ImageThread extends Thread {
    private Handler mHandler;

    @Override
    public void run() {
    Looper.prepare();
    mHandler = new ImageHandler();
    Looper.loop();
    }
    }



    上述代码定义了一个线程的子类和Handler的子类,在Android Studio的比较新的版本不能直接使用Handler类实例对象并重新handleMessage(已废弃,旧版本可以),必须通过Handler子类实例对象
    在Activity的onCreate()或者Fragment的onCreateView()方法中加入以下代码:启动新线程,监听图片的点击事件,向新线程中的Handler发送消息。

    imageView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Message msg = new Message();
    msg.what = 0x123;
    Bundle bundle = new Bundle();
    bundle.putString("ImageText", imageData.getImageText());
    msg.setData(bundle);
    imageThread.mHandler.sendMessage(msg);
    }
    });
    imageThread = new ImageThread();
    imageThread.start();


    ————————————————
    版权声明:本文为CSDN博主「maisomgan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_45828419/article/details/115523133

    收起阅读 »

    Android三方库glide的使用

    glideGlide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。glide库的优点:加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。生命周期的绑定:图片请求与页面生命...
    继续阅读 »

    glide

    Glide 是一个图片加载库,跟它同类型的库还有 Picasso、Fresco、Universal-Image-Loader 等。

    glide库的优点:

    加载类型多样化:Glide 支持 Gif、WebP 等格式的图片。
    生命周期的绑定:图片请求与页面生命周期绑定,避免内存泄漏。
    使用简单(链式调用),且提供丰富的 Api 功能 (如: 图片裁剪等功能)。
    高效的缓存策略:
    支持多种缓存策略 (Memory 和 Disk 图片缓存)。
    根据 ImageView 的大小来加载相应大小的图片尺寸。
    内存开销小,默认使用 RGB_565 格式 (3.x 版本)。
    使用 BitmapPool 进行 Bitmap 的复用。
    首先,使用glide需要添加依赖,在当前项目的build.gradle下加入以下代码:

    implementation 'com.github.bumptech.glide:glide:4.8.0'
    其次,在加载图片时,若需要网络请求或者本地内存的访问,需要在当前项目的AndroidManifest.xml中加入请求权限代码:

    //用于网络请求

    //它可以监听用户的连接状态并在用户重新连接到网络时重启之前失败的请求

    //用于硬盘缓存和读取

    glide的使用

    Glide.with(MainActivity) .load(R.mipmap.image) .into(imageView);

    with()方法可以接收Context、Activity或者Fragment类型的参数。
    load()方法中不仅可以传入图片地址,还可以传入图片文件File,resource,图片的byte数组等。
    into()参数可以直接写图片控件,如需要给其他控件添加背景图片,则需要:

    .into(new SimpleTarget(){
    @Override
    public void onResourceReady(Bitmap resource, Transition transition) {
    Drawable drawable = new BitmapDrawable(resource);
    mConstraintLayout.setBackground(drawable);
    }
    });

    加载本地图片:

    File file = new File(getExternalCacheDir() + "/image.jpg");
    Glide.with(this).load(file).into(imageView);

    加载应用资源:

    int resource = R.drawable.image;
    Glide.with(this).load(resource).into(imageView);

    加载二进制流:

    byte[] image = getImageBytes();
    Glide.with(this).load(image).into(imageView);

    加载Uri对象:

    Uri imageUri = getImageUri();
    Glide.with(this).load(imageUri).into(imageView);

    注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。
    取消图片:Glide.with(this).load(url).clear();
    ————————————————
    版权声明:本文为CSDN博主「maisomgan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_45828419/article/details/115632155

    收起阅读 »

    Android三方库OKHTTP请求的使用

    okhttpOkhttp是网络请求框架。OkHttp主要有Get请求、Post请求等功能。使用前,需要添加依赖,在当前项目的build.gradle下加入以下代码:implementation 'com.squareup.okhttp3:okhttp:3.5....
    继续阅读 »

    okhttp

    Okhttp是网络请求框架。OkHttp主要有Get请求、Post请求等功能。
    使用前,需要添加依赖,在当前项目的build.gradle下加入以下代码:

    implementation 'com.squareup.okhttp3:okhttp:3.5.0'

    Okhttp的Get请求
    使用OkHttp进行Get请求只需要完成以下四步:

    获取OkHttpClient对象
    OkHttpClient okHttpClient = new OkHttpClient();

    构造Request对象
    Request request = new Request.Builder() .get() .url("https://v0.yiketianqi.com/api?version=v62&appid=12646748&appsecret=SLB1jIr8&city=北京") .build();

    将Request封装为Call
    Call call = okHttpClient.newCall(request);

    根据需要调用同步或者异步请求方法
    //同步调用,返回Response,会抛出IO异常
    Response response = call.execute();

    //异步调用,并设置回调函数

    call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    Toast.makeText(OkHttpActivity.this, "get failed", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onResponse(Call call, final Response response) throws IOException {
    final String res = response.body().string();
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    textView.setText(res);
    }
    });
    }
    });



    OkHttp进行Post请求
    使用OkHttp进行Post请求和进行Get请求很类似,只需要以下五步:

    获取OkHttpClient对象
    OkHttpClient okHttpClient = new OkHttpClient();
    1
    构建FormBody或RequestBody或构架我们自己的RequestBody,传入参数

    //OkHttp进行Post请求提交键值对
    FormBody formBody = new FormBody.Builder()
    .add("username", "admin")
    .add("password", "admin")
    .build();

    //OkHttp进行Post请求提交字符串
    RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), "{username:admin;password:admin}");

    //OkHttp进行Post请求上传文件
    File file = new File(Environment.getExternalStorageDirectory(), "1.png");
    if (!file.exists()){
    Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
    }else{
    RequestBody requestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file);
    }

    //OkHttp进行Post请求提交表单
    File file = new File(Environment.getExternalStorageDirectory(), "1.png");
    if (!file.exists()){
    Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
    return;
    }
    RequestBody muiltipartBody = new MultipartBody.Builder()
    //一定要设置这句
    .setType(MultipartBody.FORM)
    .addFormDataPart("username", "admin")//
    .addFormDataPart("password", "admin")//
    .addFormDataPart("myfile", "1.png", RequestBody.create(MediaType.parse("application/octet-stream"), file))
    .build();
    构建Request,将FormBody作为Post方法的参数传入
    final Request request = new Request.Builder()
    .url("http://www.jianshu.com/")
    .post(formBody)
    .build();

    将Request封装为Call
    Call call = okHttpClient.newCall(request);
    1
    调用请求,重写回调方法
    call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    Toast.makeText(OkHttpActivity.this, "Post Failed", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
    final String res = response.body().string();
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    textView.setText(res);
    }
    });
    }
    });


    ————————————————
    版权声明:本文为CSDN博主「maisomgan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_45828419/article/details/115632155

    收起阅读 »

    即学即用Android Jetpack - Navigation

    前言 即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇。 记得去年第一次参加谷歌开发者大会的时候,就被Navigation的...
    继续阅读 »

    前言


    即学即用Android Jetpack系列Blog的目的是通过学习Android Jetpack完成一个简单的Demo,本文是即学即用Android Jetpack系列Blog的第一篇。


    记得去年第一次参加谷歌开发者大会的时候,就被Navigation的图形导航界面给迷住了,一句卧槽就代表了小王的全部心情~


    目录


    一、简介


    1. 定义


    Navigation是什么呢?谷歌的介绍视频上说:



    Navigation是一个可简化Android导航的库和插件



    更确切的来说,Navigation是用来管理Fragment的切换,并且可以通过可视化的方式,看见App的交互流程。这完美的契合了Jake Wharton大神单Activity的建议。


    2. 优点



    • 处理Fragment的切换(上文已说过)

    • 默认情况下正确处理Fragment的前进和后退

    • 为过渡和动画提供标准化的资源

    • 实现和处理深层连接

    • 可以绑定ToolbarBottomNavigationViewActionBar


    • SafeArgs(Gradle插件) 数据传递时提供类型安全性


    • ViewModel支持


    3. 准备


    如果想要进行下面的学习,你需要 3.2 或者更高的Android studio


    4. 学习方式


    最好的学习方式仍然是通过官方文档,下面是官方的学习地址:

    谷歌官方教程:Navigation Codelab

    谷歌官方文档:Navigation

    官方Demo:Demo地址


    二、实战

    可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图,NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController,虽然它取决于开车人(用户)。



    第一步 添加依赖


    模块层的build.gradle文件需要添加:

    ext.navigationVersion = "2.0.0"
    dependencies {
    //...
    implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
    implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
    }

    如果你要使用SafeArgs插件,还要在项目目录下的build.gradle文件添加:

    buildscript {
    ext.navigationVersion = "2.0.0"
    dependencies {
    classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
    }

    以及模块下面的build.gradle文件添加:

    apply plugin: 'kotlin-android-extensions'
    apply plugin: 'androidx.navigation.safeargs'

    第二步 创建navigation导航



    1. 创建基础目录:资源文件res目录下创建navigation目录 -> 右击navigation目录New一个Navigation resource file

    2. 创建一个Destination,如果说navigation是我们的导航工具,Destination是我们的目的地,在此之前,我已经写好了一个WelcomeFragmentLoginFragmentRegisterFragment

    除了可视化界面之外,我们仍然有必要看一下里面的内容组成,login_navigation.xml

    <navigation
    ...
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcome">

    <fragment
    android:id="@+id/login"
    android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
    android:label="LoginFragment"
    tools:layout="@layout/fragment_login"
    />

    <fragment
    android:id="@+id/welcome"
    android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
    android:label="LoginFragment"
    tools:layout="@layout/fragment_welcome">
    <action
    .../>
    <action
    .../>
    </fragment>

    <fragment
    android:id="@+id/register"
    android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
    android:label="LoginFragment"
    tools:layout="@layout/fragment_register"
    >

    <argument
    .../>
    </fragment>
    </navigation>



    我在这里省略了一些不必要的代码


    第三步 建立NavHostFragment


    我们创建一个新的LoginActivity,在activity_login.xml文件中:

        ...>

    android:id="@+id/my_nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/login_navigation"
    app:defaultNavHost="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>





    第四步 界面跳转、参数传递和动画

    WelcomeFragment中,点击登录和注册按钮可以分别跳转到LoginFragmentRegisterFragment中。

    这里我使用了两种方式实现:


    方式一 利用ID导航

    目标:WelcomeFragment携带keyname的数据跳转到LoginFragmentLoginFragment接收后显示。

    Have a account ? Login按钮的点击事件如下:

    btnLogin.setOnClickListener {
    // 设置动画参数
    val navOption = navOptions {
    anim {
    enter = R.anim.slide_in_right
    exit = R.anim.slide_out_left
    popEnter = R.anim.slide_in_left
    popExit = R.anim.slide_out_right
    }
    }
    // 参数设置
    val bundle = Bundle()
    bundle.putString("name","TeaOf")
    findNavController().navigate(R.id.login, bundle,navOption)
    }

    后续LoginFragment的接收代码比较简单,直接获取Fragment中的Bundle即可,这里不再出示代码。

    方式二 利用Safe Args

    目标:WelcomeFragment通过Safe Args将数据传到RegisterFragmentRegisterFragment接收后显示。

    再看一下已经展示过的login_navigation.xml

    <navigation
    ...
    android:id="@+id/login_navigation"
    app:startDestination="@id/welcome">

    <fragment
    android:id="@+id/login"
    android:name="com.joe.jetpackdemo.ui.fragment.login.LoginFragment"
    android:label="LoginFragment"
    tools:layout="@layout/fragment_login"
    />

    <fragment
    android:id="@+id/welcome"
    android:name="com.joe.jetpackdemo.ui.fragment.login.WelcomeFragment"
    android:label="LoginFragment"
    tools:layout="@layout/fragment_welcome">
    <action
    .../>
    <action
    .../>
    </fragment>

    <fragment
    android:id="@+id/register"
    android:name="com.joe.jetpackdemo.ui.fragment.login.RegisterFragment"
    android:label="LoginFragment"
    tools:layout="@layout/fragment_register"
    >

    <argument
    .../>
    </fragment>
    </navigation>



    细心的同学可能已经观察到navigation目录下的login_navigation.xml资源文件中的action标签和argument标签,这里需要解释一下:
    点击Android studio中的Make Project按钮,可以发现系统为我们生成了两个类



    WelcomeFragment中的JOIN US按钮点击事件:
    btnRegister.setOnClickListener {
    val action = WelcomeFragmentDirections
    .actionWelcomeToRegister()
    .setEMAIL("TeaOf1995@Gamil.com")
    findNavController().navigate(action)
    }

    RegisterFragment中的接收:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // ...
    val safeArgs:RegisterFragmentArgs by navArgs()
    val email = safeArgs.email
    mEmailEt.setText(email)
    }
    需要提及的是,如果不用Safe Argsaction可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成,感兴趣的同学可以自行编写。


    三、更多


    Navigation可以绑定menusdrawersbottom navigation,这里我们以bottom navigation为例,我先在navigation目录下新创建了main_navigation.xml,接着新建了MainActivity,下面则是activity_main.xml:

    <LinearLayout
    ...>

    <fragment
    android:id="@+id/my_nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    app:navGraph="@navigation/main_navigation"
    app:defaultNavHost="true"
    android:layout_height="0dp"
    android:layout_weight="1"/>

    <com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/navigation_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    app:itemIconTint="@color/colorAccent"
    app:itemTextColor="@color/colorPrimary"
    app:menu="@menu/menu_main"/>

    </LinearLayout>



    MainActivity中的处理也十分简单:

    class MainActivity : AppCompatActivity() {

    lateinit var bottomNavigationView: BottomNavigationView

    override fun onCreate(savedInstanceState: Bundle?) {
    //...
    val host: NavHostFragment = supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
    val navController = host.navController
    initWidget()
    initBottomNavigationView(bottomNavigationView,navController)
    }

    private fun initBottomNavigationView(bottomNavigationView: BottomNavigationView, navController: NavController) {
    bottomNavigationView.setupWithNavController(navController)
    }

    private fun initWidget() {
    bottomNavigationView = findViewById(R.id.navigation_view)
    }
    }


    作者:九心_
    链接:https://www.jianshu.com/p/66b93df4b7a6
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android日记之View的绘制流程(二)

    Android日记之View的绘制流程(一)Measure过程Measure过程要分情况来看,如果只是原始的View,那么通过measure()方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure...
    继续阅读 »


    Android日记之View的绘制流程(一)

    Measure过程

    Measure过程要分情况来看,如果只是原始的View,那么通过measure()方法就完成了测量的过程。如果是一个ViewGroup,除了完成自己的测量过程外,还要遍历去调用子元素的measure(),下面分两种情况进行讨论。

    View的measure过程

    View的measure过程就是会调用View的measure()方法,它是一个final类型的方法,在measure()方法还会调用View的onMeasure()方法,我们看这个方法写了什么。


    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    代码很少,这里的setMeasuredDimension()方法会设置View的宽和高的测量值,因此我们只要看getDefaultSize()即可。


    public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
    result = size;
    break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
    result = specSize;
    break;
    }
    return result;
    }

    从源码可以看出,逻辑还是很简单的,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况,简单理解,其实getDefaultSize()返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终得大小是在layout阶段确定的,所以这里必须要区分,但几乎所有情况下View的测量大小和最终大小是相等的。

    至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize()的第一个参数size,即宽和高为getSuggestedMinimunWidth()getSuggestedMinimunHeight()的返回值:


    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    这里就只分析getSuggestedMinimumWidth()方法的实现,getSuggestedMinimunHeight()和它的实现原理是一样的,从代码中可以看出,如果View没有设置背景,那么宽度就为mMinWidth,而mMinWidth对应android:Width这个属性所指定的值。如果不指定,那么mMinWidth则默认为0,如果设置了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth()。那mBackground.getMinimumWidth()是什么呢?



    public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

    可以看出,getMinimumWidth()返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。这里说明一下,ShapeDrawable无原始宽和高,BitmapDrawab有原始宽和高。

    从getDefaultSize可以看出,View的宽和高由specSize决定。直接继承View的自定义控件需要重写onMeasure()方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent。

    ViewGroup的measure过程

    对于ViewGroup来说,除了完成自己的mesaure过程以外,还会遍历去调用所有子元素的measure()方法,各个子元素在递归的去执行这个过程,和View不同的是,ViewGroup时候是一个抽象类,因此它没有重写View的onMeasure(),但是它提供了一个叫measureChildren()的方法。


    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
    final View child = children[i];
    if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
    }
    }


    protected void measureChild(View child, int parentWidthMeasureSpec,
    int parentHeightMeasureSpec)
    {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
    mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    measureChildren()会调用measureChild()方法,在measureChild()方法里,就是去除子元素的LayoutParams,然后在通过getChildMeasureSpec()来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure()方法进行测量。

    我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure()方法需要由各个子类去具体实现,比如LinearLayout、RelativeLayout等,ViewGroup不做统一的onMeasure()实现,是因为不同的ViewGroup子类有不同的布局特性,导致它们的测量细节个不不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因为无法做统一,以后会专门写一篇文章来讲解每个layout的布局是怎么实现的。

    Layout过程

    Layout的作用就是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用layout()方法,在layout()方法中onLayout()方法又会被调用。layout过程相比measure过程就简单多了,layout()方法确定View本身的位置,而onLayout()方法则会确定所有子元素的位置,先看View的layout()方法。


    public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
    onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    onLayout(changed, l, t, r, b);

    if (shouldDrawRoundScrollbar()) {
    if(mRoundScrollbarRenderer == null) {
    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
    }
    } else {
    mRoundScrollbarRenderer = null;
    }

    mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLayoutChangeListeners != null) {
    ArrayList listenersCopy =
    (ArrayList)li.mOnLayoutChangeListeners.clone();
    int numListeners = listenersCopy.size();
    for (int i = 0; i < numListeners; ++i) {
    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
    }
    }
    }

    final boolean wasLayoutValid = isLayoutValid();

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

    if (!wasLayoutValid && isFocused()) {
    mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
    if (canTakeFocus()) {
    // We have a robust focus, so parents should no longer be wanting focus.
    clearParentsWantFocus();
    } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
    // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
    // layout. In this case, there's no guarantee that parent layouts will be evaluated
    // and thus the safest action is to clear focus here.
    clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
    clearParentsWantFocus();
    } else if (!hasParentWantsFocus()) {
    // original requestFocus was likely on this view directly, so just clear focus
    clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
    }
    // otherwise, we let parents handle re-assigning focus during their layout passes.
    } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
    mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
    View focused = findFocus();
    if (focused != null) {
    // Try to restore focus as close as possible to our starting focus.
    if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
    // Give up and clear focus once we've reached the top-most parent which wants
    // focus.
    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
    }
    }
    }

    if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
    mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
    notifyEnterOrExitForAutoFillIfNeeded(true);
    }
    }

    首先会通过setFrame()方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了。接着就会调用onLayout()方法,这个方法的用途是父容器确定子元素的位置,和onMeasure()类似,onLayout()的具体实现同样和具体的布局有关,所以View和ViewGroup都没有真正实现onLayout()方法。

    Draw过程

    Draw过程就简单很多了,作用就是将View会知道屏幕上面。View的绘制过程遵循着如下几步。
    (1)background.draw(canvas) - 绘制背景
    (2)onDraw() - 绘制自己
    (3)dispatchDraw()- 绘制children
    (4)onDrawScrollBars() - 绘制装饰

    绘制流程从ViewRootImpl的performDraw()开始,performDraw()方法在类 ViewRootImpl内,其核心代码如下:

    private void performDraw() {



    boolean canUseAsync = draw(fullRedrawNeeded);
    }

    private boolean draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
    scalingRequired, dirty, surfaceInsets)) {
    return false;
    }
    }

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
    boolean scalingRequired, Rect dirty, Rect surfaceInsets)
    {
    ...
    mView.draw(canvas);
    ...
    }

    然后查看View的draw()方法的源代码:

    public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
    * Draw traversal performs several drawing steps which must be executed
    * in the appropriate order:
    *
    * 1. Draw the background
    * 2. If necessary, save the canvas' layers to prepare for fading
    * 3. Draw view's content
    * 4. Draw children
    * 5. If necessary, draw the fading edges and restore layers
    * 6. Draw decorations (scrollbars for instance)
    */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
    drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas); //这里进行传递的实现!!!

    drawAutofilledHighlight(canvas);

    // Overlay is part of the content and draws beneath Foreground
    if (mOverlay != null && !mOverlay.isEmpty()) {
    mOverlay.getOverlayView().dispatchDraw(canvas);
    }

    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);

    // Step 7, draw the default focus highlight
    drawDefaultFocusHighlight(canvas);

    if (debugDraw()) {
    debugDrawFocus(canvas);
    }

    // we're done...
    return;
    }

    ......
    }


    View的绘制过程的传递是通过dispatchDraw()来实现的,dispatchDraw()会遍历调用所有子元素的draw()方法,如此一来draw事件就一层层的传递了下去。这里补充一下View还有一个特殊的方法叫setWillNotDraw(),代码如下:

    /**
    * If this view doesn't do any drawing on its own, set this flag to
    * allow further optimizations. By default, this flag is not set on
    * View, but could be set on some View subclasses such as ViewGroup.
    *
    * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
    * you should clear this flag.
    *
    * @param willNotDraw whether or not this View draw on its own
    */
    public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }


    如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化,默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个标记位。这个标记位对开发的实际意义是:当我们的自定义控件继承与ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位进行后续的优化。当然如果确定ViewGroup要绘制内容的话,就要关闭这个标记位。



    作者:居居居居居居x
    链接:https://www.jianshu.com/p/f4afebd50a2b
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    Android日记之View的绘制流程(一)

    前言 View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK...
    继续阅读 »

    前言


    View的绘制流程,其实也就是工作流程,指的就是Measure(测量)、Layout(布局)和Draw(绘制)。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View,这里解析的Android SDK为为Android 9.0版本。



    Activity的构成


    在了解绘制流程之前,我们首先要了解Activity的构成,我们都知道Activity要用setcontentView()来加载布局,但是这个方法具体是怎么实现的呢,如下所示:


    public void setContentView(@LayoutRes int layoutResID) {


    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
    }

    public Window getWindow() {
    return mWindow;
    }

    这里有一个getWindow(),返回的是一个mWindow,那这个是什么呢,我们接在在Activity原来里的attach()方法里可以看到如下代码:


    final void attach(Context context, ActivityThread aThread,
    Instrumentation instr, IBinder token, int ident,
    Application application, Intent intent, ActivityInfo info,
    CharSequence title, Activity parent, String id,
    NonConfigurationInstances lastNonConfigurationInstances,
    Configuration config, String referrer, IVoiceInteractor voiceInteractor,
    Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);

    ......
    }

    我们发现了原来mWindow实例化了PhoneWindow,那意思就是说Activity通过setcontentView()加载布局的方法其实就是调用了PhoneWindow的setcontentView()方法,那我们接着往下看PhoneWindow类里面具体是怎么样的。



    @Overridepublic void setContentView(int layoutResID) {

    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
    installDecor(); //在这里
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
    getContext());
    transitionTo(newScene);
    } else {
    mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
    cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
    }

    点进PhoneWindow这个类我们可以知道这里的PhoneWindow是继承Window的,Window是一个抽象类。然后我们看里面的一些关键地方,比如installDecor()方法,我们看看里面具体做了啥。


    private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
    mDecor = generateDecor(-1); //注释1
    mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
    mDecor.setIsRootNamespace(true);
    if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
    }
    } else {
    mDecor.setWindow(this);
    }
    if (mContentParent == null) {
    mContentParent = generateLayout(mDecor); //注释2

    ......
    }
    }

    里面有一个标记注释为1的generateDecor()方法,我们看看具体做了啥。

    protected DecorView generateDecor(int featureId) {


    // System process doesn't have application context and in that case we need to directly use
    // the context we have. Otherwise we want the application context, so we don't cling to the
    // activity.
    Context context;
    if (mUseDecorContext) {
    Context applicationContext = getContext().getApplicationContext();
    if (applicationContext == null) {
    context = getContext();
    } else {
    context = new DecorContext(applicationContext, getContext());
    if (mTheme != -1) {
    context.setTheme(mTheme);
    }
    }
    } else {
    context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
    }

    发现创建了一个DecorView,而这个DecorView继承了Framelayout,DecorView就是Activity的根View,接着我们回到标记注释2的generateLayout()方法,看看做了啥。

        

    protected ViewGroup generateLayout(DecorView decor) {


    ......

    // Inflate the window decor.
    //根据不同情况加载不同的布局给layoutResource

    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
    layoutResource = R.layout.screen_swipe_dismiss;
    setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
    if (mIsFloating) {
    TypedValue res = new TypedValue();
    getContext().getTheme().resolveAttribute(
    R.attr.dialogTitleIconsDecorLayout, res, true);
    layoutResource = res.resourceId;
    } else {
    layoutResource = R.layout.screen_title_icons;
    }
    // XXX Remove this once action bar supports these features.
    removeFeature(FEATURE_ACTION_BAR);
    // System.out.println("Title Icons!");
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
    && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
    // Special case for a window with only a progress bar (and title).
    // XXX Need to have a no-title version of embedded windows.
    layoutResource = R.layout.screen_progress;
    // System.out.println("Progress!");
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
    // Special case for a window with a custom title.
    // If the window is floating, we need a dialog layout
    if (mIsFloating) {
    TypedValue res = new TypedValue();
    getContext().getTheme().resolveAttribute(
    R.attr.dialogCustomTitleDecorLayout, res, true);
    layoutResource = res.resourceId;
    } else {
    layoutResource = R.layout.screen_custom_title;
    }
    // XXX Remove this once action bar supports these features.
    removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
    // If no other features and not embedded, only need a title.
    // If the window is floating, we need a dialog layout
    if (mIsFloating) {
    TypedValue res = new TypedValue();
    getContext().getTheme().resolveAttribute(
    R.attr.dialogTitleDecorLayout, res, true);
    layoutResource = res.resourceId;
    } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
    layoutResource = a.getResourceId(
    R.styleable.Window_windowActionBarFullscreenDecorLayout,
    R.layout.screen_action_bar);
    } else {
    layoutResource = R.layout.screen_title; //在这里!!!!!!!
    }
    // System.out.println("Title!");
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
    layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
    // Embedded, so no decoration is needed.
    layoutResource = R.layout.screen_simple;
    // System.out.println("Simple!");
    }

    ......

    mDecor.finishChanging();

    return contentParent;
    }

    generateLayout()方法代码比较长,这里截取了一部分,主要内容就是根据不同的情况加载不同的布局给layoutresource,在这里面有一个基础的XML,就是R.layout.screen_title。这个XMl文件里面有一个ViewStub和两个FrameLayout,ViewStub主要是显示Actionbar的,两个FarmeLayout分别显示TitleView和ContentView,也就是我们的标题和内容。看到这里我们也就知道,一个Activity包含一个Window对象,而Window是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又分为两个区域,分别就是TitleView和ContentView,平常所显示的布局就是在ContentView中。


    View的整体绘制流程路口


    上一节讲了Activity的构成,最后讲到了DecorView的创建和加载它的资源,但是这个时候DecorView的内容还无法显示,因为还没用加载到Window中,接下来我们看它是怎么被加载到Window中。


    还是老样子,DecorView创建完毕的时并且要加载到Window中,我们还是要先了解Activity的创建流程,当我们使用Activit的startActivity()方法时,最终就是调用ActivityThread的handleLaunchActivity方法来创建Activity。而绘制会从根视图ViewRootImpl的performTraversals()方法开始从上到下遍历整个视图树,每个 View 控制负责绘制自己,而ViewGroup还需要负责通知自己的子 View进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)。


    ViewRootImpl是WindowManager和DecorView的纽带,View的三大流程均是通过这里来完成的,在ActivityThread中,当Activity对象被创建,会将DecorView加入到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。


    刚刚也说过了,View的绘制流程是从ViewRootImpl类的performTraversals()方法开始的,它会依次调用performMeasure()performLayout()performDraw()三个方法。其中在performTraversals()调用measure()方法,在measure()方法又会调用onMeasure()方法,在onMeasure()方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了依次measure过程,接着子元素会重复父容器的measure过程,如此返回完成了整个View数的遍历。



    理解MeasureSpec


    为了理解View的测量过程,我们还需理解MeasureSpec,它在很大程度上决定了一个View的尺寸规格,而且也参与到了View的measure过程中。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在根据这个measureSpec来测量出View的宽和高。


    MeasureSpec是View的内部类,它代表哦啦一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小,如以下代码所示:


    public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    ......

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    public static final int EXACTLY = 1 << MODE_SHIFT;

    public static final int AT_MOST = 2 << MODE_SHIFT;

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
    @MeasureSpecMode
    int mode)
    {
    if (sUseBrokenMakeMeasureSpec) {
    return size + mode;
    } else {
    return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
    }

    public static int makeSafeMeasureSpec(int size, int mode) {
    if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
    return 0;
    }
    return makeMeasureSpec(size, mode);
    }


    @MeasureSpecMode
    public static int getMode(int measureSpec) {
    //noinspection ResourceType
    return (measureSpec & MODE_MASK);
    }


    public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
    }

    static int adjust(int measureSpec, int delta) {
    final int mode = getMode(measureSpec);
    int size = getSize(measureSpec);
    if (mode == UNSPECIFIED) {
    // No need to adjust size for UNSPECIFIED mode.
    return makeMeasureSpec(size, UNSPECIFIED);
    }
    size += delta;
    if (size < 0) {
    Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
    ") spec: " + toString(measureSpec) + " delta: " + delta);
    size = 0;
    }
    return makeMeasureSpec(size, mode);
    }


    ......
    }

    MeasureSpec通过SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,而也可以通过解包得到SpecMode和SpecSize。这里说一下打包成的MeasureSpec是一个int值,不是这个类本身。


    对于SpecMode,有三种类型:



    • UPSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。


    • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。


    • AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。


    Android日记之View的绘制流程(二)

    作者:居居居居居居x
    链接:https://www.jianshu.com/p/f4afebd50a2b
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android日记之线程池

    前言 在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池...
    继续阅读 »

    前言


    在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要一个新进程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时候就需要线程池来对线程进行管理。在Java 1.5中提供了Executor框架用于把任务的提交和执行解耦。任务的提交交给RUnnable或者Callable,而Executor框架用来处理任务。Executor框架中最核心的成员就是ThreadPoolExecutor,它是线程池的核心实现类。本篇文章就着重讲解ThreadPoolExecutor。


    ThreadPoolExecutor介绍


    可以通过ThreadPoolExecutor开创建一个线程池,ThreadPoolExecutor类一共有四个构造方法。下面展示的都是拥有最多参数的的构造方法。



    public ThreadPoolExecutor(int corePoolSize,

    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
    maximumPoolSize <= 0 ||
    maximumPoolSize < corePoolSize ||
    keepAliveTime < 0)
    throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
    throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
    }


    • corePoolSize:核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则会创建新线程来处理任务;如果等于或者多于corePoolSize,则不会创建,如果调用线程池的prestartAllcoreThread()方法,线程池会提前创建并启动所有核心线程来等待任务。


    • maximumPoolSize:线程池允许创建的最大线程数,如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。


    • keepAliveTime:非核心线程闲置的超时时间,超过这个事件则回收,如果任务很多,并且每个任务的执行事件很短,则可以调用keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上。


    • TimeUnit:keepAliveTime参数的时间单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、秒(SECONDS)、毫秒(MILLOSECONDS)等。


    • BlockingQueue任务队列,如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockiingQueue类型,也就是阻塞队列。


    • ThreadFactory:线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置参数。


    • RejectedExecutionHandler :饱和策略,这是当任务队列中和线程池都满了时所采取的对应策略,默认是ABordPolicy,表示无法处理新任务,并抛出RejetctedExecutionException异常。此外还有3种策略,它们分别如下:



    (1)CallerRunsPolicy:用调用者所在的线程来处理任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

    (2)DiscardPolicy:不能执行的任务,并将该任务删除。

    (3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。


    ThreadPoolExecutor的基本使用

    package com.ju.executordemo;


    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;

    import java.util.Random;
    import java.util.concurrent.Executors;
    import java.util.concurrent.LinkedBlockingDeque;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;

    public class MainActivity extends AppCompatActivity{


    private Button btnStart;
    private final int CORE_POOL_SIZE = 4;//核心线程数
    private final int MAX_POOL_SIZE = 5;//最大线程数
    private final long KEEP_ALIVE_TIME = 10;//空闲线程超时时间
    private ThreadPoolExecutor executorPool;
    private int songIndex = 0;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    initView();
    //创建线程池
    initExec();
    }

    private void initView() {
    btnStart = findViewById(R.id.btn_start);
    btnStart.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    begin();
    }
    });
    }


    public void begin() {
    songIndex++;
    try {
    executorPool.execute(new WorkerThread("歌曲" + songIndex));
    } catch (Exception e) {
    Log.e("threadtest", "AbortPolicy...已超出规定的线程数量,不能再增加了....");
    }

    // 所有任务已经执行完毕,我们在监听一下相关数据
    new Thread(new Runnable() {
    @Override
    public void run() {
    try {
    Thread.sleep(20 * 1000);
    } catch (Exception e) {

    }
    sout("monitor after");
    }
    }).start();

    }

    private void sout(String msg) {
    Log.i("threadtest", "monitor " + msg
    + " CorePoolSize:" + executorPool.getCorePoolSize()
    + " PoolSize:" + executorPool.getPoolSize()
    + " MaximumPoolSize:" + executorPool.getMaximumPoolSize()
    + " ActiveCount:" + executorPool.getActiveCount()
    + " TaskCount:" + executorPool.getTaskCount()
    );
    }



    private void initExec() {
    executorPool = new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME, TimeUnit.SECONDS,
    new LinkedBlockingDeque(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
    }

    class WorkerThread implements Runnable {

    private String threadName;

    public WorkerThread (String name){
    threadName = name;
    }


    @Override
    public void run() {
    boolean flag = true;
    try {
    while (flag){
    String tn = Thread.currentThread().getName();
    //模拟耗时操作
    Random random = new Random();
    long time = (random.nextInt(5) + 1) * 1000;
    Thread.sleep(time);
    Log.e("threadtest","线程\"" + tn + "\"耗时了(" + time / 1000 + "秒)下载了第<" + threadName + ">");
    //下载完毕跳出循环
    flag = false;
    }
    }catch (Exception e){
    e.printStackTrace();
    }
    }
    }

    }


    上述代码模拟一个下载音乐的例子来演示ThreadPoolExecutor的基本使用,启动ThreadPoolExecutor的函数是execute()方法,然后他需要一个Runnable的参数来进行启动。



    ThreadPoolExecutor的其它种类


    通过直接或者间接地配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor,其中有 4 种线程池比较常用,它们分别是 FixedThreadPool、CachedThreadPool、SingleThreadExecutor和 ScheduledThreadPool。下面分别介绍这4种线程池。



    • FixedThreadPool


    FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建FixedThreadPool的方法, 如下所示:


    public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue());
    }

    FixedThreadPool的corePoolSize和maximumPoolSize都设置为创建FixedThreadPool指定的参数nThreads,也就意味着FixedThreadPool只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime设置为0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以keepAliveTime是无效的参数。另外,任 务队列采用了无界的阻塞队列LinkedBlockingQueue。



    当执行execute()方法时,如果当前运行的线程未达到corePoolSize(核心线程数)时 就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到LinkedBlockingQueue中。 FixedThreadPool就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过corePoolSize时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行




    • CachedThreadPool


    CachedThreadPool是一个根据需要创建线程的线程池,创建CachedThreadPool的代码如下所示:


    public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue());
    }

    CachedThreadPool的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,这意味着 CachedThreadPool没有核心线程,非核心线程是无界的。keepAliveTime设置为60L,则空闲线程等待新任务 的最长时间为 60s。在此用了阻塞队列 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作 必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。



    当执行execute()方法时,首先会执行SynchronousQueue的offer()方法来提交任务,并且查询线程池中是否有空闲的线程执行SynchronousQueue的poll()方法来移除任务。如果有则配对成功,将任务交给这个空闲的线程处理;如果没有则配对失败,创建新的线程去处理任务。当线程池中的线程空闲时,它会执行 SynchronousQueue的poll()方法,等待SynchronousQueue中新提交的任务。如果超过 60s 没有新任务提交到 SynchronousQueue,则这个空闲线程将终止。因为maximumPoolSize 是无界的,所以如果提交的任务大于线 程池中线程处理任务的速度就会不断地创建新线程。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务。




    • SingleThreadExecutor


    SingleThreadExecutor是使用单个工作线程的线程池,其创建源码如下所示:


    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue()));
    }

    corePoolSize和maximumPoolSize都为1,意味着SingleThreadExecutor只有一个核心线程,其他的参数都 和FixedThreadPool一样,这里就不赘述了。SingleThreadExecutor的execute()方法的执行示意图如图5所示。


    当执行execute()方法时,如果当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务。如果当前有运行的线程,则将任务添加到阻塞队列LinkedBlockingQueue中。因此,SingleThreadExecutor能确保所有的任务在一个线程中按照顺序逐一执行。




    • ScheduledThreadPool


    ScheduledThreadPool是一个能实现定时和周期性任务的线程池,它的创建源码如下所示



    static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);

    }

    这里创建了ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下所示:

    public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
    DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
    new DelayedWorkQueue());
    }

    从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是ThreadPoolExecutor的 构造方法。corePoolSize是传进来的固定数值,maximumPoolSize的值是Integer.MAX_VALUE。因为采用的 DelayedWorkQueue是无界的,所以maximumPoolSize这个参数是无效的。


    当执行ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者scheduleWithFixedDelay()方法时,会向DelayedWorkQueue添加一个 实现RunnableScheduledFuture接口的ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到corePoolSize。如果没有则新建线程并启动它,但并不是立即去执行任务,而是去DelayedWorkQueue中取ScheduledFutureTask,然后去执行任务。如果运行的线程达到了corePoolSize时,则将任务添加到DelayedWorkQueue中。DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask中time变量改为下次要执行的时间并放回到DelayedWorkQueue中。


    作者:居居居居居居x
    链接:https://www.jianshu.com/p/70a8d302fa0f
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    揭开 LiveData 的通知机制的神秘面纱

    LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 ...
    继续阅读 »

    LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 LiveData 的通知机制是它们的主要责任。所以,就这个机会我们也来分析一下 LiveData 通知的实现过程。

    1、一个 LiveData 的问题

    LiveData的问题
    有两个页面 A 和 B,A 是一个 Fragment ,是一个列表的展示页;B 是其他的页面。首先,A 会更新页面,并且为了防止连续更新,在每次更新之前需要检查一个布尔值,只有为 false 的时候才允许从网络加载数据。每次加载数据之前会将该布尔值置为 true,拿到了结果之后置为 false. 这里拿到的结果是借助 LiveData 来通知给页面进行更新的。

    现在,A 打开了 B,B 中对列表中的数据进行了更新,然后发了一条类似于广播的消息。此时,A 接收了消息并进行数据加载。过了一段时间,B 准备退出,再退出的时候又对列表中的项目进行了更新,所以此时又发出了一条消息。

    B 关闭了,我们回到了 A 页面。但是,此时,我们发现 A 页面中的数据只包含了第一次的数据更新,第二次的数据更新没有体现在列表中。

    用代码来描述的话大致是下面这样,

    //  A
    public class A extends Fragment {

    private boolean loading = false;

    private MyViewModel vm;

    // ......

    /**
    * Register load observer.
    */
    public void registerObservers() {
    vm.getData().observe(this, resources -> {
    loading = false;
    // ... show in list
    })
    }

    /**
    * Load data from server.
    */
    public void loadData() {
    if (loading) return;
    loading = true;
    vm.load();
    }

    /**
    * On receive message.
    */
    public void onReceive() {
    loadData();
    }
    }

    public class B extends Activity {

    public void doBusiness1() {
    sendMessage(MSG); // Send message when on foreground.
    }

    @Override
    public void onBackpressed() {
    // ....
    sendMessage(MSG); // Send message when back
    }
    }

    public class MyViewModel extends ViewModel {

    private MutableLiveData> data;

    public MutableLiveData> getData() {
    if (data == null) {
    data = new MutableLiveData<>();
    }
    return data;
    }

    public void load() {
    Object result = AsyncGetData.getData(); // Get data
    if (data != null) {
    data.setValue(Resouces.success(result));
    }
    }
    }


    A 打开了 B 之后,A 处于后台,B 处于前台。此时,B 调用 doBusiness1() 发送了一条消息 MSG,A 中在 onReceive() 中收到消息,并调用 loadData() 加载数据。然后,B 处理完了业务,准备退出的时候发现其他数据发生了变化,所以又发了一条消息,然后 onReceive() 中收到消息,并调用 loadData(). 但此时发现 loading 为 true. 所以,我们后来对数据的修改没有体现到列表上面。

    2、问题的原因

    如果用上面的示例代码作为例子,那么出现问题的原因就是当 A 处于后台的时候。虽然调用了 loadData() 并且从网络中拿到了数据,但是调用 data.setValue() 方法的时候无法通知到 A 中。所以,loading = false 这一行无法被调用到。第二次发出通知的时候,一样调用到了 loadData(),但是因为此时 loading 为 true,所以并没有执行加载数据的操作。而当从 B 中完全回到 A 的时候,第一次加载的数据被 A 接收到。所以,列表中的数据是第一次加载时的数据,第二次加载事件丢失了。

    解决这个问题的方法当然比较简单,可以当接收到事件的时候使用布尔变量监听,然后回到页面的时候发现数据发生变化再执行数据加载:

    // 类 A
    public class A extends Fragment {

    private boolean dataChanged;

    /**
    * On receive message.
    */
    public void onReceive() {
    dataChanged = true;
    }

    @Override
    public void onResume() {
    // ...
    if (dataChanged) {
    loadData();
    }
    }
    }


    对于上面的问题,当我们调用了 setValue() 之后将调用到 LiveData 类的 setValue() 方法,

    @MainThread
    protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
    }


    这里表明该方法必须在主线程中被调用,最终事件的分发将会交给 dispatchingValue() 方法来执行:

    private void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
    mDispatchInvalidated = true;
    return;
    }
    mDispatchingValue = true;
    do {
    mDispatchInvalidated = false;
    if (initiator != null) {
    considerNotify(initiator);
    initiator = null;
    } else {
    for (Iterator, ObserverWrapper>> iterator =
    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
    // 发送事件
    considerNotify(iterator.next().getValue());
    if (mDispatchInvalidated) {
    break;
    }
    }
    }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
    }


    然后,会调用 considerNotify() 方法来最终将事件传递出去,

    private void considerNotify(ObserverWrapper observer) {
    // 这里会因为当前的 Fragment 没有处于 active 状态而退出方法
    if (!observer.mActive) {
    return;
    }
    if (!observer.shouldBeActive()) {
    observer.activeStateChanged(false);
    return;
    }
    if (observer.mLastVersion >= mVersion) {
    return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
    }


    这里会因为当前的 Fragment 没有处于 active 状态而退出 considerNotify() 方法,从而消息无法被传递出去。

    3、LiveData 的通知机制

    LiveData 的通知机制并不复杂,它的类主要包含在 livedata-core 包下面,总共也就 3 个类。LiveData 是一个抽象类,它有一个默认的实现就是 MutableLiveData.

    LiveData 主要依靠内部的变量 mObservers 来缓存订阅的对象和订阅信息。其定义如下,使用了一个哈希表进行缓存和映射,

    private SafeIterableMap, ObserverWrapper> mObservers = new SafeIterableMap<>();
    每当我们调用一次 observe() 方法的时候就会有一个映射关系被加入到哈希表中,

    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
    // 持有者当前处于被销毁状态,因此可以忽略此次观察
    return;
    }
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && !existing.isAttachedTo(owner)) {
    throw new IllegalArgumentException("Cannot add the same observer"
    + " with different lifecycles");
    }
    if (existing != null) {
    return;
    }
    owner.getLifecycle().addObserver(wrapper);
    }


    从上面的代码我们可以看出,添加到映射关系中的类会先被包装成 LifecycleBoundObserver 对象。然后使用该对象对 owner 的生命周期进行监听。

    这的 LifecycleBoundObserver 和 ObserverWrapper 两个类的定义如下,

    class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
    @NonNull final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer observer) {
    super(observer);
    mOwner = owner;
    }

    @Override
    boolean shouldBeActive() {
    return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    @Override
    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
    if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
    removeObserver(mObserver);
    return;
    }
    activeStateChanged(shouldBeActive());
    }

    @Override
    boolean isAttachedTo(LifecycleOwner owner) {
    return mOwner == owner;
    }

    @Override
    void detachObserver() {
    mOwner.getLifecycle().removeObserver(this);
    }
    }

    private abstract class ObserverWrapper {
    final Observer mObserver;
    boolean mActive;
    int mLastVersion = START_VERSION;

    ObserverWrapper(Observer observer) {
    mObserver = observer;
    }

    abstract boolean shouldBeActive();

    boolean isAttachedTo(LifecycleOwner owner) {
    return false;
    }

    void detachObserver() {}

    void activeStateChanged(boolean newActive) {
    if (newActive == mActive) {
    return;
    }
    mActive = newActive;
    boolean wasInactive = LiveData.this.mActiveCount == 0;
    LiveData.this.mActiveCount += mActive ? 1 : -1;
    if (wasInactive && mActive) {
    onActive();
    }
    if (LiveData.this.mActiveCount == 0 && !mActive) {
    onInactive();
    }
    if (mActive) {
    dispatchingValue(this);
    }
    }
    }


    上面的类中我们先来关注 LifecycleBoundObserver 中的 onStateChanged() 方法。该方法继承自 LifecycleObserver. 这里的 Lifecycle.Event 是一个枚举类型,定义了一些与生命周期相关的枚举值。所以,当 Activity 或者 Fragment 的生命周期发生变化的时候会回调这个方法。从上面我们也可以看出,该方法内部又调用了基类的 activeStateChanged() 方法,该方法主要用来更新当前的 Observer 是否处于 Active 的状态。我们上面无法通知也是因为在这个方法中 mActive 被置为 false 造成的。

    继续看 activeStateChanged() 方法,我们可以看出在最后的几行中,它调用了 dispatchingValue(this) 方法。所以,当 Fragment 从处于后台切换到前台之后,会将当前缓存的值通知给观察者。

    那么值是如何缓存的,以及缓存了多少值呢?回到之前的 setValue() 和 dispatchingValue() 方法中,我们发现值是以一个单独的变量进行缓存的,

    private volatile Object mData = NOT_SET;
    因此,在我们的示例中,当页面从后台切换到前台的时候,只能将最后一次缓存的结果通知给观察者就真相大白了。

    总结

    从上面的分析中,我们对 LiveData 总结如下,

    当调用 observe() 方法的时候,我们的观察者将会和 LifecycleOwner (Fragment 或者 Activity) 一起被包装到一个类中,并使用哈希表建立映射关系。同时,还会对 Fragment 或者 Activity 的生命周期方法进行监听,以此来达到监听观察者是否处于 active 状态的目的。

    当 Fragment 或者 Activity 处于后台的时候,其内部的观察者将处于非 active 状态,此时使用 setValue() 设置的值会缓存到 LiveData 中。但是这种缓存只能缓存一个值,新的值会替换旧的值。因此,当页面从后台恢复到前台的时候只有最后设置的一个值会被传递给观察者。

    在 2 中的当 Fragment 或者 Activity 从后台恢复的时候进行通知也是通过监听其生命周期方法实现的。

    调用了 observe() 之后,Fragment 或者 Activity 被缓存了起来,不会造成内存泄漏吗?答案是不会的。因为 LiveData 可以对其生命周期进行监听,当其处于销毁状态的时候,该映射关系将被从缓存中移除。

    通俗地讲,

    LiveData 里面保存了一个值,当 Fragment 处于后台更新这个值的时候,会直接把这个值改掉,但是不会通知到观察者,直到 Fragment 从后台回来的时候才通知给观察者。而这里 LiveData 感知 Fragment 处于后台还是前台是依靠 LifecycleObserver 的通知机制来完成的。

    收起阅读 »

    揭开 ViewModel 的生命周期控制的神秘面纱

    1、从一个 Bug 说起想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。起因是这样的,昨天在...
    继续阅读 »

    1、从一个 Bug 说起

    想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。

    起因是这样的,昨天在修复程序中的 Bug 的时候遇到了一个异常,是从 ViewModel 中获取存储的数据的时候报了空指针。我启用了开发者模式的 “不保留活动” 之后很容易地重现了这个异常。出现错误的原因也很简单,相关的代码如下:

    private ReceiptViewerViewModel viewModel;

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
    viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1
    handleIntent(savedInstanceState);
    // ...
    }

    private void handleIntent(Bundle savedInstanceState) {
    LoadingStatus loadingStatus;
    if (savedInstanceState == null) {
    loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
    }
    viewModel.setLoadingStatus(loadingStatus);
    }


    在方法 doCreateView() 中我获取了 viewModel 实例,然后在 handleIntent() 方法中从 Intent 中取出传入的参数。当然,还要使用 viewModel 的 getter 方法从其中取出 loadingStatus 并使用。在使用的时候抛了空指针。

    显然,一般情况下是不会出现问题的,但是如果 Activity 在后台被销毁了,那么再重建的时候就会出现空指针异常。

    解决方法也比较简单,在 onSaveInstanceState() 方法中将数据缓存起来即可,即:

    private void handleIntent(Bundle savedInstanceState) {
    LoadingStatus loadingStatus;
    if (savedInstanceState == null) {
    loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);
    } else {
    loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);
    }
    viewModel.setLoadingStatus(loadingStatus);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());
    }


    现在的问题是 ViewModel 的生命周期问题,有人说在 doCreateView() 方法的 1 处得到的不是之前的 ViewModel 吗,数据不是之前已经设置过了吗?所以,这牵扯 ViewModel 是在什么时候被销毁和重建的问题。

    2、ViewModel 的生命周期

    有的人希望使用 ViewModel 缓存 Activity 的信息,然后在 doCreateView() 方法的 1 处得到之前的 ViewModel 实例,这样 ViewModel 的数据就是 Activity 销毁之前的数据,这可行吗?我们从源码角度来看下这个问题。

    首先,每次获取 viewmodel 实例的时候都会调用下面的方法来获取 ViewModel 实例。从下面的 get() 方法中可以看出,实例化过的 ViewModel 是从 mViewModelStore 中获取的。如果由 ViewModelStores.of(activity) 方法得到的 mViewModelStore 不是同一个,那么得到的 ViewModel 也不是同一个。

    下面方法中的 get() 方法中后续的逻辑是如果之前没有缓存过 ViewModel,那么就构建一个新的实例并将其放进 mViewModelStore 中。这部分代码逻辑比较简单,我们不继续分析了。

    // ViewModelProviders#of()
    public static ViewModelProvider of(@NonNull FragmentActivity activity) {
    ViewModelProvider.AndroidViewModelFactory factory =
    ViewModelProvider.AndroidViewModelFactory.getInstance(activity);
    return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1
    }

    // ViewModelProvider#get()
    public T get(@NonNull String key, @NonNull Class modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
    return (T) viewModel;
    }

    viewModel = mFactory.create(modelClass);
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
    }


    我们回到上述 of() 方法的 1 处,来看下 ViewModelStores.of() 方法,其定义如下:

    // ViewModelStores#of()
    public static ViewModelStore of(@NonNull FragmentActivity activity) {
    if (activity instanceof ViewModelStoreOwner) {
    return ((ViewModelStoreOwner) activity).getViewModelStore();
    }
    return holderFragmentFor(activity).getViewModelStore();
    }

    // HolderFragment#holderFragmentFor()
    public static HolderFragment holderFragmentFor(FragmentActivity activity) {
    return sHolderFragmentManager.holderFragmentFor(activity);
    }


    这里会从 holderFragmentFor() 方法中获取一个 HolderFragment 实例,它是一个 Fragment 的实现类。然后从该实例中获取 ViewModelStore 的实例。所以,ViewModel 对生命周期的管理与 Glide 和 RxPermission 等框架的处理方式一致,就是使用一个空的 Fragment 来进行生命周期管理。

    对于 HolderFragment,其定义如下。从下面的代码我们可以看出,上述用到的 ViewModelStore 实例就是 HolderFragment 的一个局部变量。所以,ViewModel 使用空的 Fragment 管理生命周期实锤了。

    public class HolderFragment extends Fragment implements ViewModelStoreOwner {
    private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
    private ViewModelStore mViewModelStore = new ViewModelStore();

    public HolderFragment() {
    setRetainInstance(true);
    }

    // ...
    }


    此外,我们注意到上面的 HolderFragment 的构造方法中还调用了 setRetainInstance(true) 这一行代码。我们进入该方法看它的注释:

    Control whether a fragment instance is retained across Activityre-creation (such as from a configuration change). This can onlybe used with fragments not in the back stack. If set, the fragmentlifecycle will be slightly different when an activity is recreated:

    就是说,当 Activity 被重建的时候该 Fragment 会被保留,然后传递给新创建的 Activity. 但是,这只适用于不处于后台的 Fragment. 所以,如果 Activity 处于后台的时候,Fragment 不会保留,那么它得到的 ViewModelStore 实例就不同了。

    所以,总结下来,准确地讲:当 Activity 处于前台的时候被销毁了,那么得到的 ViewModel 是之前实例过的 ViewModel;如果 Activity 处于后台时被销毁了,那么得到的 ViewModel 不是同一个。举例说,如果 Activity 因为配置发生变化而被重建了,那么当重建的时候,ViewModel 是之前的实例;如果因为长期处于后台而被销毁了,那么重建的时候,ViewModel 就不是之前的实例了。


    回到之前的 holderFragmentFor() 方法,我们看下这里具体做了什么,其定义如下。

    // HolderFragmentManager#holderFragmentFor()
    HolderFragment holderFragmentFor(FragmentActivity activity) {
    // 使用 FragmentManager 获取 HolderFragment
    FragmentManager fm = activity.getSupportFragmentManager();
    HolderFragment holder = findHolderFragment(fm);
    if (holder != null) {
    return holder;
    }
    // 从哈希表中获取 HolderFragment
    holder = mNotCommittedActivityHolders.get(activity);
    if (holder != null) {
    return holder;
    }

    if (!mActivityCallbacksIsAdded) {
    mActivityCallbacksIsAdded = true;
    activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
    }
    holder = createHolderFragment(fm);
    // 将新的实例放进哈希表中
    mNotCommittedActivityHolders.put(activity, holder);
    return holder;
    }


    首先,尝试使用 FragmentManager 来获取 HolderFragment,如果获取不到就从 mNotCommittedActivityHolders 中进行获取。这里的 mNotCommittedActivityHolders 是一个哈希表,每次实例化的新的 HolderFragment 会被添加到哈希表中。

    另外,上面的方法中还使用了 ActivityLifecycleCallbacks 对 Activity 的生命周期进行监听。其定义如下,

    private ActivityLifecycleCallbacks mActivityCallbacks =
    new EmptyActivityLifecycleCallbacks() {
    @Override
    public void onActivityDestroyed(Activity activity) {
    HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);
    }
    };


    当 Activity 被销毁的时候会从哈希表中移除映射关系。所以,每次 Activity 被销毁的时候哈希表中的映射关系都不存在了。而之所以 ViewModel 能够实现在 Activity 配置发生变化的时候获取之前的 ViewModel 是通过上面的 setRetainInstance(true) 和 findHolderFragment(fm) 来实现的。

    总结
    以上就是 ViewModel 的生命周期的总结。我们只是通过对主流程的分析研究了它的生命周期的流程,实际上内部还有许多小细节,逻辑也比较简单,我们就不一一说明了。

    viewmodel-lifecycle
    这里使用了 Activity rotated,也就是 Activity 处于前台的时候配置发生变化的情况,而不是处于后台,不知道你之前有没有注意这一点呢?

    原文链接:揭开 ViewModel 的生命周期控制的神秘面纱

    收起阅读 »

    一文说透 Android 应用架构 MVC、MVP、MVVM 和 组件化

    MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和...
    继续阅读 »

    MVC、MVP 和 MVVM 是常见的三种架构设计模式,当前 MVP 和 MVVM 的使用相对比较广泛,当然 MVC 也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发,每个模块又可以编译成独立的APP进行开发。理论上讲,组件化和前面三种架构设计不是一个层次的。它们之间的关系是,组件化的各个组件可以使用前面三种架构设计。我们只有了解了这些架构设计的特点之后,才能在进行开发的时候选择适合自己项目的架构模式,这也是本文的目的。

    1、MVC

    MVC (Model-View-Controller, 模型-视图-控制器),标准的 MVC 是这个样子的:

    模型层 (Model):业务逻辑对应的数据模型,无 View 无关,而与业务相关;

    视图层 (View):一般使用 XML 或者 Java 对界面进行描述;

    控制层 (Controllor):在 Android 中通常指 Activity 和 Fragment,或者由其控制的业务类。

    Activity 并非标准的 Controller,它一方面用来控制了布局,另一方面还要在 Activity 中写业务代码,造成了 Activity 既像 View 又像Controller。

    在 Android 开发中,就是指直接使用 Activity 并在其中写业务逻辑的开发方式。显然,一方面 Activity 本身就是一个视图,另一方面又要负责处理业务逻辑,因此逻辑会比较混乱。

    这种开发方式不太适合 Android 开发。

    2、MVP

    2.1 概念梳理

    MVP (Model-View-Presenter) 是 MVC 的演化版本,几个主要部分如下:

    模型层 (Model):主要提供数据存取功能。

    视图层 (View):处理用户事件和视图。在 Android 中,可能是指 Activity、Fragment 或者 View。

    展示层 (Presenter):负责通过 Model 存取数据,连接 View 和 Model,从 Model 中取出数据交给 View。

    所以,对于 MVP 的架构设计,我们有以下几点需要说明:

    这里的 Model 是用来存取数据的,也就是用来从指定的数据源中获取数据,不要将其理解成 MVC 中的 Model。在 MVC 中 Model 是数据模型,在 MVP 中,我们用 Bean 来表示数据模型。

    Model 和 View 不会直接发生关系,它们需要通过 Presenter 来进行交互。在实际的开发中,我们可以用接口来定义一些规范,然后让我们的 View 和 Model 实现它们,并借助 Presenter 进行交互即可。

    为了说明 MVP 设计模式,我们给出一个示例程序。你可以在 Android-references 中获取到它的源代码。

    2.2 示例程序

    在该示例中,我们使用了:

    开眼视频的 API 作为数据源;

    Retrofit 进行数据访问;

    使用 ARouter 进行路由;

    使用 MVP 设计模式作为程序架构。



    这里我们首先定义了 MVP 模式中的最顶层的 View 和 Presenter,在这里分别是 BaseView 和 BasePresenter,它们在该项目中是两个空的接口,在一些项目中,我们可以根据自己的需求在这两个接口中添加自己需要的方法。

    然后,我们定义了 HomeContract。它是一个抽象的接口,相当于一层协议,用来规定指定的功能的 View 和 Presenter 分别应该具有哪些方法。通常,对于不同的功能,我们需要分别实现一个 MVP,每个 MVP 都会有一个对应的 Contract。笔者认为它的好处在于,将指定的 View 和 Presenter 的接口定义在一个接口中,更加集中。它们各自需要实现的方法也一目了然地展现在了我们面前。

    这里根据我们的业务场景,该接口的定义如下:

    public interface HomeContract {

    interface IView extends BaseView {
    void setFirstPage(List itemLists);
    void setNextPage(List itemLists);
    void onError(String msg);
    }

    interface IPresenter extends BasePresenter {
    void requestFirstPage();
    void requestNextPage();
    }
    }


    HomeContract 用来规定 View 和 Presenter 应该具有的操作,在这里它用来指定主页的 View 和 Presenter 的方法。从上面我们也可以看出,这里的 IView 和 IPresenter 分别实现了 BaseView 和 BasePresenter。

    上面,我们定义了 V 和 P 的规范,MVP 中还有一项 Model,它用来从网络中获取数据。这里我们省去网络相关的具体的代码,你只需要知道 APIRetrofit.getEyepetizerService() 是用来获取 Retrofit 对应的 Service,而 getMoreHomeData() 和 getFirstHomeData() 是用来从指定的接口中获取数据就行。下面是 HomeModel 的定义:

    public class HomeModel {

    public Observable getFirstHomeData() {
    return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());
    }

    public Observable getMoreHomeData(String url) {
    return APIRetrofit.getEyepetizerService().getMoreHomeData(url);
    }
    }


    OK,上面我们已经完成了 Model 的定义和 View 及 Presenter 的规范的定义。下面,我们就需要具体去实现 View 和 Presenter。

    首先是 Presenter,下面是我们的 HomePresenter 的定义。在下面的代码中,为了更加清晰地展示其中的逻辑,我删减了一部分无关代码:

    public class HomePresenter implements HomeContract.IPresenter {

    private HomeContract.IView view;

    private HomeModel homeModel;

    private String nextPageUrl;

    // 传入View并实例化Model
    public HomePresenter(HomeContract.IView view) {
    this.view = view;
    homeModel = new HomeModel();
    }



    // 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调

    @Override
    public void requestFirstPage() {
    Disposable disposable = homeModel.getFirstHomeData()
    // ....
    .subscribe(itemLists -> { view.setFirstPage(itemLists); },
    throwable -> { view.onError(throwable.toString()); });
    }



    // 使用Model请求数据,并在得到请求结果的时候调用View的方法进行回调

    @Override
    public void requestNextPage() {
    Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)
    // ....
    .subscribe(itemLists -> { view.setFirstPage(itemLists); },
    throwable -> { view.onError(throwable.toString()); });
    }
    }


    从上面我们可以看出,在 Presenter 需要将 View 和 Model 建立联系。我们需要在初始化的时候传入 View,并实例化一个 Model。Presenter 通过 Mode l获取数据,并在拿到数据的时候,通过 View 的方法通知给 View 层。

    然后,就是我们的 View 层的代码,同样,我对代码做了删减:

    @Route(path = BaseConstants.EYEPETIZER_MENU)
    public class HomeActivity extends CommonActivity implements HomeContract.IView {

    // 实例化Presenter
    private HomeContract.IPresenter presenter;
    {
    presenter = new HomePresenter(this);
    }

    @Override
    protected int getLayoutResId() {
    return R.layout.activity_eyepetizer_menu;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
    // ...
    // 使用Presenter请求数据
    presenter.requestFirstPage();
    loading = true;
    }

    private void configList() {
    // ...
    getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    // 请求下一页的数据
    presenter.requestNextPage();
    }
    }
    });
    }



    // 当请求到结果的时候在页面上做处理,展示到页面上

    @Override
    public void setFirstPage(List itemLists) {
    loading = false;
    homeAdapter.addData(itemLists);
    }



    // 当请求到结果的时候在页面上做处理,展示到页面上

    @Override
    public void setNextPage(List itemLists) {
    loading = false;
    homeAdapter.addData(itemLists);
    }

    @Override
    public void onError(String msg) {
    ToastUtils.makeToast(msg);
    }

    // ...
    }


    从上面的代码中我们可以看出实际在 View 中也要维护一个 Presenter 的实例。当需要请求数据的时候会使用该实例的方法来请求数据,所以,在开发的时候,我们需要根据请求数据的情况,在 Presenter 中定义接口方法。

    实际上,MVP 的原理就是 View 通过 Presenter 获取数据,获取到数据之后再回调 View 的方法来展示数据。

    另外一个值得注意的地方就是,在实际的使用过程中,尤其是进行异步请求的时候,为了给 Model 和 View 之间解耦,我们会在 Presenter 中使用 Handler 收发消息来建立两者之间的桥梁。

    2.3 MVC 和 MVP 的区别

    MVC 中是允许 Model 和 View 进行交互的,而MVP中,Model 与 View 之间的交互由Presenter完成;

    MVP 模式就是将 P 定义成一个接口,然后在每个触发的事件中调用接口的方法来处理,也就是将逻辑放进了 P 中,需要执行某些操作的时候调用 P 的方法就行了。

    2.4 MVP的优缺点

    优点:

    降低耦合度,实现了 Model 和 View 真正的完全分离,可以修改 View 而不影响 Modle;

    模块职责划分明显,层次清晰;

    隐藏数据;

    Presenter 可以复用,一个 Presenter 可以用于多个 View,而不需要更改 Presenter 的逻辑;

    利于测试驱动开发,以前的 Android 开发是难以进行单元测试的;

    View 可以进行组件化,在 MVP 当中,View 不依赖 Model。

    缺点:

    Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->View 的手动同步逻辑,造成 Presenter 比较笨重,维护起来会比较困难;

    由于对视图的渲染放在了 Presenter 中,所以视图和 Presenter 的交互会过于频繁;

    如果 Presenter 过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密,一旦视图需要变更,那么 Presenter 也需要变更了。

    3、MVVM (分手大师)

    3.1 基础概念

    MVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

    模型层 (Model):负责从各种数据源中获取数据;

    视图层 (View):在 Android 中对应于 Activity 和 Fragment,用于展示给用户和处理用户交互,会驱动 ViewModel 从 Model 中获取数据;

    ViewModel 层:用于将 Model 和 View 进行关联,我们可以在 View 中通过 ViewModel 从 Model 中获取数据;当获取到了数据之后,会通过自动绑定,比如 DataBinding,来将结果自动刷新到界面上。

    使用 Google 官方的 Android Architecture Components ,我们可以很容易地将 MVVM 应用到我们的应用中。下面,我们就使用它来展示一下 MVVM 的实际的应用。你可以在 Android-references 中获取到它的源代码。

    3.2 示例程序

    在该项目中,我们使用了:

    果壳网的 API 作为数据源;

    使用 Retrofit 进行网络数据访问;

    使用 ViewMdeol 作为整体的架构设计。

    mvvm
    这里的 model.data 下面的类是对应于网络的数据实体的,由 JSON 自动生成,这里我们不进行详细描述。这里的 model.repository 下面的两个类是用来从网络中获取数据信息的,我们也忽略它的定义。

    上面就是我们的 Model 的定义,并没有太多的内容,基本与 MVP 一致。

    下面的是 ViewModel 的代码,我们选择了其中的一个方法来进行说明。当我们定义 ViewModel 的时候,需要继承 ViewModel 类。

    public class GuokrViewModel extends ViewModel {

    public LiveData> getGuokrNews(int offset, int limit) {
    MutableLiveData> result = new MutableLiveData<>();
    GuokrRetrofit.getGuokrService().getNews(offset, limit)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer() {
    @Override
    public void onError(Throwable e) {
    result.setValue(Resource.error(e.getMessage(), null));
    }

    @Override
    public void onComplete() { }

    @Override
    public void onSubscribe(Disposable d) { }

    @Override
    public void onNext(GuokrNews guokrNews) {
    result.setValue(Resource.success(guokrNews));
    }
    });
    return result;
    }
    }


    这里的 ViewModel 来自 android.arch.lifecycle.ViewModel,所以,为了使用它,我们还需要加入下面的依赖:

    api "android.arch.lifecycle:runtime:$archVersion"
    api "android.arch.lifecycle:extensions:$archVersion"
    annotationProcessor "android.arch.lifecycle:compiler:$archVersion"


    在 ViewModel 的定义中,我们直接使用 Retrofit 来从网络中获取数据。然后当获取到数据的时候,我们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层,我们只需要调用该方法,并对返回的 LiveData 进行"监听"即可。这里,我们将错误信息和返回的数据信息进行了封装,并且封装了一个代表当前状态的枚举信息,你可以参考源代码来详细了解下这些内容。

    上面我们定义完了 Model 和 ViewModel,下面我们看下 View 层的定义,以及在 View 层中该如何使用 ViewModel。

    @Route(path = BaseConstants.GUOKR_NEWS_LIST)
    public class NewsListFragment extends CommonFragment {

    private GuokrViewModel guokrViewModel;

    private int offset = 0;

    private final int limit = 20;

    private GuokrNewsAdapter adapter;

    @Override
    protected int getLayoutResId() {
    return R.layout.fragment_news_list;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
    // ...

    guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);

    fetchNews();
    }

    private void fetchNews() {
    guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {
    if (guokrNewsResource == null) {
    return;
    }
    switch (guokrNewsResource.status) {
    case FAILED:
    ToastUtils.makeToast(guokrNewsResource.message);
    break;
    case SUCCESS:
    adapter.addData(guokrNewsResource.data.getResult());
    adapter.notifyDataSetChanged();
    break;
    }
    });
    }
    }


    以上就是我们的 View 层的定义,这里我们先使用了

    这里的view.fragment包下面的类对应于实际的页面,这里我们 ViewModelProviders 的方法来获取我们需要使用的 ViewModel,然后,我们直接使用该 ViewModel 的方法获取数据,并对返回的结果进行“监听”即可。

    以上就是 MVVM 的基本使用,当然,这里我们并没有使用 DataBinding 直接与返回的列表信息进行绑定,它被更多的用在了整个 Fragment 的布局中。

    3.3 MVVM 的优点和缺点

    MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

    低耦合:视图(View)可以独立于Model变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。

    可重用性:你可以把一些视图逻辑放在一个 ViewModel 里面,让很多 view 重用这段视图逻辑。

    独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

    可测试:界面素来是比较难于测试的,而现在测试可以针对 ViewModel 来写。

    4、组件化

    4.1 基础概念

    所谓的组件化,通俗理解就是将一个工程分成各个模块,各个模块之间相互解耦,可以独立开发并编译成一个独立的 APP 进行调试,然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协作、同步开发;被分割出来的模块又可以在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤其适用于比较大型的项目。

    简单了解了组件化之后,让我们来看一下如何实现组件化开发。你可能之前听说过组件化开发,或者被其高大上的称谓吓到了,但它实际应用起来并不复杂,至少借助了现成的框架之后并不复杂。这里我们先梳理一下,在应用组件化的时候需要解决哪些问题:

    如何分成各个模块?我们可以根据业务来进行拆分,对于比较大的功能模块可以作为应用的一个模块来使用,但是也应该注意,划分出来的模块不要过多,否则可能会降低编译的速度并且增加维护的难度。

    各个模块之间如何进行数据共享和数据通信?我们可以把需要共享的数据划分成一个单独的模块来放置公共数据。各个模块之间的数据通信,我们可以使用阿里的 ARouter 进行页面的跳转,使用封装之后的 RxJava 作为 EventBus 进行全局的数据通信。

    如何将各个模块打包成一个独立的 APP 进行调试?首先这个要建立在2的基础上,然后,我们可以在各个模块的 gradle 文件里面配置需要加载的 AndroidManifest.xml 文件,并可以为每个应用配置一个独立的 Application 和启动类。

    如何防止资源名冲突问题?遵守命名规约就能规避资源名冲突问题。

    如何解决 library 重复依赖以及 sdk 和依赖的第三方版本号控制问题?可以将各个模块公用的依赖的版本配置到 settings.gradle 里面,并且可以建立一个公共的模块来配置所需要的各种依赖。

    Talk is cheap,下面让我们动手实践来应用组件化进行开发。你可以在Github中获取到它的源代码。

    4.2 组件化实践

    1. 包结构

    首先,我们先来看整个应用的包的结构。如下图所示,该模块的划分是根据各个模块的功能来决定的。图的右侧白色的部分是各个模块的文件路径,我推荐使用这种方式,而不是将各个模块放置在 app 下面,因为这样看起来更加的清晰。为了达到这个目的,你只需要按照下面的方式在 settings.gralde 里面配置一下各个模块的路径即可。注意在实际应用的时候模块的路径的关系,不要搞错了。

    组件化
    然后,我们介绍一下这里的 commons 模块。它用来存放公共的资源和一些依赖,这里我们将两者放在了一个模块中以减少模块的数量。下面是它的 gradle 的部分配置。这里我们使用了 api 来引入各个依赖,以便在其他的模块中也能使用这些依赖。

    dependencies {
    api fileTree(include: ['*.jar'], dir: 'libs')
    // ...
    // router
    api 'com.alibaba:arouter-api:1.3.1'
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
    // walle
    api 'com.meituan.android.walle:library:1.1.6'
    // umeng
    api 'com.umeng.sdk:common:1.5.3'
    api 'com.umeng.sdk:analytics:7.5.3'
    api files('libs/pldroid-player-1.5.0.jar')
    }


    2. 路由

    接着,我们来看一下路由框架的配置。这里,我们使用阿里的 ARouter 来进行页面之间的跳转,你可以在 Github 上面了解该框架的配置和使用方式。这里我们只讲解一下在组件化开发的时候需要注意的地方。注意到 ARouter 是通过注解来进行页面配置的,并且它的注解是在编译的时候进行处理的。所以,我们需要引入 arouter-compiler 来使用它的编译时处理功能。需要注意的地方是,我们只要在公共的模块中加入 arouter-api 就可以使用 ARouter 的 API 了,但是需要在每个模块中引入 arouter-compiler 才能使用编译时注解。也就是说,我们需要在每个模块中都加入 arouter-compiler 依赖。

    ARouter 的实现原理使用了 Java 中的注解处理,你可以通过阅读我之前写的一篇文章来了解 《Java 开发者核心技能之 Java 注解及其典型的使用方法》。另外,我们也会在后续的文章中分析 ARouter 的实现原理,欢迎关注我的公众号「Code Brick」。

    3. 模块独立

    为了能够将各个模块编译成一个独立的 APP,我们需要在 Gradle 里面做一些配置。

    首先,我们需要在 gradle.properties 定义一些布尔类型的变量用来判断各个模块是作为一个 library 还是 application 进行编译。这里我的配置如下面的代码所示。也就是,我为每个模块都定义了这么一个布尔类型的变量,当然,你也可以只定义一个变量,然后在各个模块中使用同一个变量来进行判断。
    isGuokrModuleApp=false
    isLiveModuleApp=false
    isLayoutModuleApp=false
    isLibraryModuleApp=false
    isEyepetizerModuleApp=false


    然后,我们来看一下各个模块中的 gradle 该如何配置,这里我们以开眼视频的功能模块作为例子来进行讲解。首先,一个模块作为 library 还是 application 是根据引用的 plugin 来决定的,所以,我们要根据之前定义的布尔变量来决定使用的 plugin:

    if (isEyepetizerModuleApp.toBoolean()) {
    apply plugin: 'com.android.application'
    } else {
    apply plugin: 'com.android.library'
    }


    假如我们要将某个模块作为一个独立的 APP,那么启动类你肯定需要配置。这就意味着你需要两个 AndroidManifest.xml 文件,一个用于 library 状态,一个用于 application 状态。所以,我们可以在 main 目录下面再定义一个 AndroidManifest.xml,然后,我们在该配置文件中不只指定启动类,还使用我们定义的 Application。指定 Application 有时候是必须的,比如你需要在各个模块里面初始化 ARouter 等等。这部分代码就不给出了,可以参考源码,这里我们给出一下在 Gradle 里面指定 AndroidManifest.xml 的方式。

    如下所示,我们可以根据之前定义的布尔值来决定使用哪一个配置文件:

    sourceSets {
    main {
    jniLibs.srcDirs = ['libs']
    if (isEyepetizerModuleApp.toBoolean()) {
    manifest.srcFile "src/main/debug/AndroidManifest.xml"
    } else {
    manifest.srcFile "src/main/AndroidManifest.xml"
    }
    }
    }


    此外,还需要注意的是,如果我们希望在每个模块中都能应用 DataBinding 和 Java 8 的一些特性,那么你需要在每个模块里面都加入下面的配置:

    // use data binding
    dataBinding {
    enabled = true
    }
    // use java 8 language
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    }


    对于编译时注解之类的配置,我们也需要在每个模块里面都进行声明。

    完成了以上的配置,我们只要根据需要编译的类型,修改之前定义的布尔值,来决定是将该模块编译成 APP 还是作为类库来使用即可。

    以上就是组件化在 Android 开发当中的应用。

    总结
    MVC、MVP 和 MVVM 各有各自的特点,可以根据应用开发的需要选择适合自己的架构模式。组件化的目的就在于保持各个模块之间的独立从而便于分工协作。它们之间的关系就是,你可以在组件化的各个模块中应用前面三种架构模式的一种或者几种。
    原文链接:https://mp.weixin.qq.com/s?__biz=MzA3MzgzMzgyNw==&mid=2247483866&idx=1&sn=5b3746c7b82e779882cc5c670a30b217&chksm=9f084dd6a87fc4c0453eeb10f1f8442bdae225fb1f6cad7b2655a20383507d5709d9e3aa749f&token=1361179115&lang=zh_CN#rd

    收起阅读 »

    今日头条屏幕适配方案终极版正式发布!

    前言 我在前面两篇文章中详细介绍了 今日头条适配方案 和 SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定...
    继续阅读 »

    前言


    我在前面两篇文章中详细介绍了 今日头条适配方案SmallestWidth 限定符适配方案 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 Android 屏幕适配方案,而且它们都已经拥有了一定的用户基数


    但是对于一些才接触这两个方案的朋友,肯定或多或少还是不知道如何选择这两个方案,我虽然在之前的文章中给出了它们各自的优缺点,但是并没有用统一的标准对它们进行更细致的对比,所以也就没办法更形象的体现它们的优劣,那下面我就用统一的标准对它们进行对比,看看它们的对比情况


    方案对比


    我始终坚定地认为在这两个方案中,并不能以单个标准就能评判出谁一定比谁好,因为它们都有各自的优缺点,都不是完美的,从更客观的角度来看,它们谁都不能成为最好的那个,只有可能明确了它们各自的优缺点,知道在它们的优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,从而选择出一个最适合自己项目的屏幕适配方案


    单纯的争论谁是最好的 Android 屏幕适配方案没有任何意义,每个人的需求不一样,站的角度不一样,评判标准也不一样,你能接受的东西他不一定能接受,你觉得不可接受的东西他却觉得可以接受,你有你的理由,他有他的理由,想让一个观点让所有人都能接受太难了!所以我在这里只是列出它们的对比项和对比结果,尽可能的做到客观,最后的选择结果请自行决定,如果还有什么遗漏的对比项,请补充!


































































    对比项目 对比对象 A 对比结果 对比对象 B
    适配效果(越高越好) 今日头条适配方案 SW 限定符适配方案(在未覆盖的机型上会存在一定的误差)
    稳定性(越高越好) 今日头条适配方案 < SW 限定符适配方案
    灵活性(越高越好) 今日头条适配方案 > SW 限定符适配方案
    扩展性(越高越好) 今日头条适配方案 > SW 限定符适配方案
    侵入性(越低越好) 今日头条适配方案 < SW 限定符适配方案
    使用成本(越低越好) 今日头条适配方案 < SW 限定符适配方案
    维护成本(越低越好) 今日头条适配方案 < SW 限定符适配方案
    性能损耗 今日头条适配方案没有性能损耗 = SW 限定符适配方案没有性能损耗
    副作用 今日头条适配方案会影响一些三方库和系统控件 SW 限定符适配方案会影响 App 的体积

    可以看到 SmallestWidth 限定符适配方案今日头条适配方案 的适配效果其实都是差不多的,我在前面的文章中也通过公式计算过它们的精确度,SmallestWidth 限定符适配方案 运行在未覆盖的机型上虽然也可以适配,但是却会出现一定的误差,所以 今日头条适配方案 的适配精确度确实要比 SmallestWidth 限定符适配方案 略高的,不过只要 SmallestWidth 限定符适配方案 合理的分配资源文件,适配效果的差距应该也不大


    SmallestWidth 限定符适配方案 主打的是稳定性,在运行过程中极少会出现安全隐患,适配范围也可控,不会产生其他未知的影响,而 今日头条适配方案 主打的是降低开发成本、提高开发效率,使用上更灵活,也能满足更多的扩展需求,简单一句话概括就是,这两兄弟,一个求稳,一个求快,好了,我就介绍这么多了,自己选择吧!



    由来


    下面就开始介绍我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize,大家千万不要认为,我推出的屏幕适配框架 AndroidAutoSize 是根据 今日头条屏幕适配方案 优化的,我本人就一定支持 今日头条屏幕适配方案 是最好的 Android 屏幕适配方案这个观点,它确实很优秀,但同样也有很多不足,我最真实的观点在上面就已经表述咯,至于我为什么要根据 今日头条屏幕适配方案 再封装一个屏幕适配框架,无外乎就以下几点原因:



    • SmallestWidth 限定符适配方案 已经有多个优秀的开源解决方案了,它们已经能满足我们日常开发中的所有需求


    • 今日头条 官方技术团队只公布了 今日头条屏幕适配方案文章 以及核心代码,但并没有在 Github 上创建公开的仓库,一个新的方案必定要有一个成长迭代的过程,在此期间,一定需要一个可以把所有使用者聚集起来的公共社区,可以让所有使用该方案的使用者在上面交流,大家一起总结、一起填坑,这样才能让该方案更成熟稳定,这就是开源的力量


    • 今日头条 官方技术团队公布的核心代码并不能满足我的所有需求,已经开源的其他基于 今日头条屏幕适配方案 的开源项目以及解决方案也不能满足我的所有需求,而我有更好的实现想法


    • MVPArms 需要一个适配效果还不错并且切换维护成本也比较低的屏幕适配框架,以帮助使用者用较低的成本、工作量将已经停止维护的 AndroidAutoLayout 快速替换掉



    我建议大家都可以去实际体验一下 今日头条屏幕适配方案SmallestWidth 限定符适配方案,感受下它们的异同,我给的建议是,可以在项目中先使用 今日头条屏幕适配方案,感受下它的使用方式以及适配效果,今日头条屏幕适配方案 的侵入性非常低,如果在使用过程中遇到什么不能解决的问题,马上可以切换为其他的屏幕适配方案,在切换的过程中也花费不了多少工作量,试错成本非常低


    但如果你在项目中先使用 SmallestWidth 限定符适配方案,之后在使用的过程中再遇到什么不能解决的问题,这时想切换为其他的屏幕适配方案,这工作量可就大了,每个 Layout 文件都含有大量的 dimens 引用,改起来这工作量得有多大,想想都觉得后怕,这就是侵入性太高导致的最致命的问题


    与今日头条屏幕适配方案的关系


    AndroidAutoSize今日头条屏幕适配方案 的关系,相当于汽车和发动机的关系,今日头条屏幕适配方案 官方公布的代码,只实现了修改系统 density 的相关逻辑,这的确在屏幕适配中起到了最关键的作用,但这还远远还不够


    要想让使用者能够更傻瓜式的使用该方案,并且能够应对日常开发中的所有复杂需求,那在架构框架时,还需要考虑 API 的易用性以及合理性、框架的扩展性以及灵活性、功能的全面性、注释和文档的易读性等多个方面的问题


    于是我带着我的这些标准在网上搜寻了很久,发现并没有任何一个开源框架或解决方案能够达到我的所有标准,它们大多数还只是停留在将 今日头条屏幕适配方案 封装成工具类来引入项目的阶段,这样在功能的扩展上有限制,并且对用户的使用体验也不好,而我想做的是一个全面性的产品级屏幕适配框架,这离我最初的构想,差距还非常大,于是我只好自己动手,将我的所有思想实现,这才有了 AndroidAutoSize


    写完 AndroidAutoSize 框架后,因为对 今日头条屏幕适配方案 有了更加深入的理解,所以才写了 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案,以帮助大家更清晰的理解 今日头条屏幕适配方案


    与 AndroidAutoLayout 的关系


    AndroidAutoSize 因为名字和 鸿神AndroidAutoLayout 非常相似,并且在填写设计图尺寸的方式上也极为相似,再加上我写的屏幕适配系列的文章也发布在了 鸿神 的公众号上,所以很多人以为 AndroidAutoSize鸿神 写的 AndroidAutoLayout 的升级版,这里我哭笑不得 😂,我只好在这里说一句,大家好,我叫 JessYan,的确可以理解为 AndroidAutoSizeAndroidAutoLayout 的升级版,但是它是我写的,关注一波呗


    AndroidAutoSizeAndroidAutoLayout 的原理,却天差地别,比如 AndroidAutoLayout 只能使用 px 作为布局单位,而 AndroidAutoSize 恰好相反,在布局中 dp、sp、pt、in、mm 所有的单位都能支持,唯独不支持 px,但这也意味着 AndroidAutoSizeAndroidAutoLayout 在项目中可以共存,互不影响,所以使用 AndroidAutoLayout 的老项目也可以放心的引入 AndroidAutoSize,慢慢的完成屏幕适配框架的切换


    之所以将框架取名为 AndroidAutoSize,第一,是想致敬 AndroidAutoLayoutAndroid 屏幕适配领域的贡献,第二,也想成为在 Android 屏幕适配领域有重要影响力的框架


    结构


    我在上面就已经说了很多开源框架以及解决方案,只是把 今日头条屏幕适配方案 简单的封装成一个工具类然后引入项目,这时很多人就会说了 今日头条屏幕适配方案 官方公布的全部代码都只有 30 行不到,你不把它封装成工具类,那封装成什么?该怎么封装?下面就来看看 AndroidAutoSize 的整体结构

    ├── external
    │ ├── ExternalAdaptInfo.java
    │ ├── ExternalAdaptManager.java
    │── internal
    │ ├── CancelAdapt.java
    │ ├── CustomAdapt.java
    │── unit
    │ ├── Subunits.java
    │ ├── UnitsManager.java
    │── utils
    │ ├── AutoSizeUtils.java
    │ ├── LogUtils.java
    │ ├── Preconditions.java
    │ ├── ScreenUtils.java
    ├── ActivityLifecycleCallbacksImpl.java
    ├── AutoAdaptStrategy.java
    ├── AutoSize.java
    ├── AutoSizeConfig.java
    ├── DefaultAutoAdaptStrategy.java
    ├── DisplayMetricsInfo.java
    ├── FragmentLifecycleCallbacksImpl.java
    ├── InitProvider.java

    AndroidAutoSize 根据 今日头条屏幕适配方案 官方公布的 30 行不到的代码,经过不断的优化和扩展,发展成了现在拥有 18 个类文件,上千行代码的全面性屏幕适配框架,在迭代的过程中完善和优化了很多功能,相比 今日头条屏幕适配方案 官方公布的原始代码,AndroidAutoSize 更加稳定、更加易用、更加强大,欢迎阅读源码,注释非常详细哦!


    功能介绍


    AndroidAutoSize 在使用上非常简单,只需要填写设计图尺寸这一步即可接入项目,但需要注意的是,AndroidAutoSize 有两种类型的布局单位可以选择,一个是 主单位 (dp、sp),一个是 副单位 (pt、in、mm),两种单位面向的应用场景都有不同,也都有各自的优缺点



    • 主单位: 使用 dp、sp 为单位进行布局,侵入性最低,会影响其他三方库页面、三方库控件以及系统控件的布局效果,但 AndroidAutoSize 也通过这个特性,使用 ExternalAdaptManager 实现了在不修改三方库源码的情况下适配三方库的功能


    • 副单位: 使用 pt、in、mm 为单位进行布局,侵入性高,对老项目的支持比较好,不会影响其他三方库页面、三方库控件以及系统控件的布局效果,可以彻底的屏蔽修改 density 所造成的所有未知和已知问题,但这样 AndroidAutoSize 也就无法对三方库进行适配



    大家可以根据自己的应用场景在 主单位副单位 中选择一个作为布局单位,建议想引入老项目并且注重稳定性的人群使用 副单位,只是想试试本框架,随时可能切换为其他屏幕适配方案的人群使用 主单位


    其实 AndroidAutoSize 可以同时支持 主单位副单位,但 AndroidAutoSize 可以同时支持 主单位副单位 的目的,只是为了让使用者可以在 主单位副单位 之间灵活切换,因为切换单位的工作量可能非常巨大,不能立即完成,但领导又要求马上打包上线,这时就可以起到一个很好的过渡作用


    主单位


    主单位Demodemo


    基本使用


    AndroidAutoSize 引入项目后,只要在 appAndroidManifest.xml 中填写上设计图尺寸,无需其他过多配置 (如果你没有其他自定义需求的话),AndroidAutoSize 即可自动运行,像下面这样👇

                    





    在使用主单位时,design_width_in_dpdesign_height_in_dp 的单位必须是 dp,如果设计师给你的设计图,只标注了 px 尺寸 (现在已经有很多 UI 工具可以自动标注 dp 尺寸了),那请自行根据公式 dp = px / (DPI / 160)px 尺寸转换为 dp 尺寸,如果你不知道 DPI 是多少?那请以自己测试机的 DPI 为准,如果连怎么得到设备的 DPI 都不知道?百度吧好伐,如果你实在找不到设备的 DPI 那就直接将 px 尺寸除以 3 或者 2 也是可以的


    如果你只是想使用 AndroidAutoSize 的基础功能,AndroidAutoSize 的使用方法在这里就结束了,只需要上面这一步,即可帮助你以最简单的方式接入 AndroidAutoSize,但是作为一个全面性的屏幕适配框架,在保证基础功能的简易性的同时,也必须保证复杂的需求也能在框架内被解决,从而达到一个小闭环,所以下面介绍的内容全是前人踩坑踩出来的一些必备功能,如果你没这个需求,或者觉得麻烦,可以按需查看或者跳过,下面的内容建议和 Demo 配合起来阅读,效果更佳


    注意事项



    • 你在 AndroidManifest.xml 中怎么把设计图的 px 尺寸转换为 dp 尺寸,那在布局时,每个控件的大小也需要以同样的方式将设计图上标注的 px 尺寸转换为 dp 尺寸,千万不要在 AndroidManifest.xml 中填写的是 dp 尺寸,却在布局中继续填写设计图上标注的 px 尺寸




    • design_width_in_dpdesign_height_in_dp 虽然都需要填写,但是 AndroidAutoSize 只会将高度和宽度其中的一个作为基准进行适配,一方作为基准,另一方就会变为备用,默认以宽度为基准进行适配,可以通过 AutoSizeConfig#setBaseOnWidth(Boolean) 不停的切换,这意味着最后运行到设备上的布局效果,在高度和宽度中只有一方可以和设计图上一模一样,另外一方会和设计图出现偏差,为什么不像 AndroidAutoLayout 一样,高和宽都以设计图的效果等比例完美呈现呢?这也很简单,你无法保证所有设备的高宽比例都和你设计图上的高宽比例一致,特别是在现在全面屏全面推出的情况下,如果这里不这样做的话,当你的项目运行在与设计图高宽比例不一致的设备上时,布局会出现严重的变形,这个几率非常大,详情请看 这里


    自动运行是如何做到的?


    很多人有疑惑,为什么使用者只需要在 AndroidManifest.xml 中填写一下 meta-data 标签,其他什么都不做,AndroidAutoSize 就能自动运行,并在 App 启动时自动解析 AndroidManifest.xml 中填写的设计图尺寸,这里很多人不敢相信,问我真的只需要填写下设计图尺寸框架就可以正常运行吗?难道使用了什么 黑科技?


    其实这里并没有用到什么 黑科技,原理反而非常简单,只需要声明一个 ContentProvider,在它的 onCreate 方法中启动框架即可,在 App 启动时,系统会在 App 的主进程中自动实例化你声明的这个 ContentProvider,并调用它的 onCreate 方法,执行时机比 Application#onCreate 还靠前,可以做一些初始化的工作,get 到了吗?


    这里需要注意的是,如果你的项目拥有多进程,系统只会在主进程中实例化一个你声明的 ContentProvider,并不会在其他非主进程中实例化 ContentProvider,如果在当前进程中 ContentProvider 没有被实例化,那 ContentProvider#onCreate 就不会被调用,你的初始化代码在当前进程中也就不会执行,这时就需要在 Application#onCreate 中调用下 ContentProvider#query 执行一下查询操作,这时 ContentProvider 就会在当前进程中实例化 (每个进程中只会保证有一个实例),所以应用到框架中就是,如果你需要在多个进程中都进行屏幕适配,那就需要在 Application#onCreate 中调用 AutoSize#initCompatMultiProcess 方法


    进阶使用


    虽然 AndroidAutoSize 不需要其他过多的配置,只需要在 AndroidManifest.xml 中填写下设计图尺寸就能正常运行,但 AndroidAutoSize 还是为大家准备了很多可配置选项,尽最大可能满足大家日常开发中的所有扩展需求


    所有的全局配置选项在 Demo 中都有介绍,每个 API 中也都有详细的注释,在这里就不过多介绍了


    自定义 Activity


    AndroidManifest.xml 中填写的设计图尺寸,是整个项目的全局设计图尺寸,但是如果某些 Activity 页面由于某些原因,设计师单独出图,这个页面的设计图尺寸和在 AndroidManifest.xml 中填写的设计图尺寸不一样该怎么办呢?不要急,AndroidAutoSize 已经为你考虑好了,让这个页面的 Activity 实现 CustomAdapt 接口即可实现你的需求,CustomAdapt 接口的第一个方法可以修改当前页面的设计图尺寸,第二个方法可以切换当前页面的适配基准,下面的注释都解释的很清楚

    public class CustomAdaptActivity extends AppCompatActivity implements CustomAdapt {

    /**
    * 是否按照宽度进行等比例适配 (为了保证在高宽比不同的屏幕上也能正常适配, 所以只能在宽度和高度之中选择一个作为基准进行适配)
    *
    *
    @return {@code true} 为按照宽度进行适配, {@code false} 为按照高度进行适配
    */

    @Override
    public boolean isBaseOnWidth() {
    return false;
    }

    /**
    * 这里使用 iPhone 的设计图, iPhone 的设计图尺寸为 750px * 1334px, 高换算成 dp 为 667 (1334px / 2 = 667dp)
    *


    * 返回设计图上的设计尺寸, 单位 dp
    * {
    @link #getSizeInDp} 须配合 {@link #isBaseOnWidth()} 使用, 规则如下:
    * 如果 {
    @link #isBaseOnWidth()} 返回 {@code true}, {@link #getSizeInDp} 则应该返回设计图的总宽度
    * 如果 {
    @link #isBaseOnWidth()} 返回 {@code false}, {@link #getSizeInDp} 则应该返回设计图的总高度
    * 如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸, {
    @link #getSizeInDp} 则返回 {@code 0}
    *
    *
    @return 设计图上的设计尺寸, 单位 dp
    */

    @Override
    public float getSizeInDp() {
    return 667;
    }
    }


    如果某个 Activity 想放弃适配,让这个 Activity 实现 CancelAdapt 接口即可,比如修改 density 影响到了老项目中的某些 Activity 页面的布局效果,这时就可以让这个 Activity 实现 CancelAdapt 接口

    public class CancelAdaptActivity extends AppCompatActivity implements CancelAdapt {

    }

    自定义 Fragment


    Fragment 的自定义方式和 Activity 是一样的,只不过在使用前需要先在 App 初始化时开启对 Fragment 的支持

    AutoSizeConfig.getInstance().setCustomFragment(true);

    实现 CustomAdapt

    public class CustomAdaptFragment extends Fragment implements CustomAdapt {

    @Override
    public boolean isBaseOnWidth() {
    return false;
    }

    @Override
    public float getSizeInDp() {
    return 667;
    }
    }

    实现 CancelAdapt

    public class CancelAdaptFragment extends Fragment implements CancelAdapt {

    }

    适配三方库页面


    在使用主单位时可以使用 ExternalAdaptManager 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (Activity、Fragment)


    由于 AndroidAutoSize 要求需要自定义适配参数或取消适配的页面必须实现 CustomAdaptCancelAdapt,这时问题就来了,三方库是通过远程依赖的,我们无法修改它的源码,这时我们怎么让三方库的页面也能实现自定义适配参数或取消适配呢?别急,这个需求 AndroidAutoSize 也已经为你考虑好了,当然不会让你将三方库下载到本地然后改源码!



    • 通过 ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo) 将需要自定义的类和自定义适配参数添加进方法即可替代实现 CustomAdapt 的方式,这里 展示了使用方式,以及详细的注释


    • 通过 ExternalAdaptManager#addCancelAdaptOfActivity(Class) 将需要取消适配的类添加进方法即可替代实现 CancelAdapt 的方式,这里 也展示了使用方式,以及详细的注释



    需要注意的是 ExternalAdaptManager 的方法虽然可以添加任何类,但是只能支持 Activity、Fragment,并且 ExternalAdaptManager 是支持链式调用的,以便于持续添加多个页面


    当然 ExternalAdaptManager 不仅可以对三方库的页面使用,也可以让自己项目中的 Activity、Fragment 不用实现 CustomAdaptCancelAdapt 即可达到自定义适配参数和取消适配的功能


    副单位


    前面已经介绍了 副单位 的应用场景,这里就直接介绍 副单位 如何使用,副单位Demodemo-subunits


    基本使用


    首先和 主单位 一样也需要先在 appAndroidManifest.xml 中填写上设计图尺寸,但和 主单位 不一样的是,当在使用 副单位design_width_in_dpdesign_height_in_dp 的单位不需要一定是 dp,可以直接填写设计图的 px 尺寸,在布局文件中每个控件的大小也可以直接填写设计图上标注的 px 尺寸,无需再将 px 转换为 dp,这是 副单位的 特性之一,可以帮助大家提高开发效率








    由于 AndroidAutoSize 提供了 pt、in、mm 三种类型的 副单位 供使用者选择,所以在使用 副单位 时,还需要在 APP 初始化时,通过 UnitsManager#setSupportSubunits(Subunits) 方法选择一个你喜欢的副单位,然后在布局文件中使用这个副单位进行布局,三种类型的副单位,其实效果都是一样,大家按喜欢的名字选择即可


    由于使用副单位是为了彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,所以在使用副单位时建议调用 UnitsManager#setSupportDP(false)UnitsManager#setSupportSP(false),关闭 AndroidAutoSizedpsp 的支持,AndroidAutoSize 为什么不在使用 副单位 时默认关闭对 dpsp 的支持?因为允许同时支持 主单位副单位 可以帮助使用者在 主单位副单位 之间切换时更好的过渡,这点在前面就已经提到过


    UnitsManager 的详细使用方法,在 demo-subunits 中都有展示,注释也十分详细


    自定义 ActivityFragment


    在使用 副单位 时自定义 ActivityFragment 的方式是和 主单位 是一样的,这里就不再过多介绍了


    适配三方库页面


    如果你的项目在使用 副单位 并且关闭了对 主单位 (dp、sp) 的支持,这时 ExternalAdaptManager 对三方库的页面是不起作用的,只对自己项目中的页面起作用,除非三方库的页面也使用了副单位 (pt、in、mm) 进行布局


    其实 副单位 之所以能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,就是因为三方库页面、三方库控件以及系统控件基本上使用的都是 dp、sp 进行布局,所以只要 AndroidAutoSize 关闭了对 dp、sp 的支持,转而使用 副单位 进行布局,就能彻底屏蔽修改 density 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响


    但这也同样意味着使用 副单位 就不能适配三方库的页面了,ExternalAdaptManager 也就对三方库的页面不起作用了


    布局实时预览


    在开发阶段布局时的实时预览是一个很重要的环节,很多情况下 Android Studio 提供的默认预览设备并不能完全展示我们的设计图,所以我们就需要自己创建模拟设备,dp、pt、in、mm 这四种单位的模拟设备创建方法请看 这里


    总结


    AndroidAutoSize 在经历了 240+ commit60+ issues6 个版本 的洗礼后,逐渐的稳定了下来,已经在上个星期发布了首个正式版,在这里要感谢将 AndroidAutoSize 接入到自己项目中的上千个使用者,感谢他们的信赖,AndroidAutoSize 创建的初衷就是为了让所有使用 今日头条屏幕适配方案 的使用者能有一个可以一起交流、沟通的聚集地,所以后面也会持续的收集并解决 今日头条屏幕适配方案的常见问题,让 今日头条屏幕适配方案 变得更加成熟、稳定


    至此本系列的第三篇文章也就完结了,这也预示着这个系列连载的终结,这篇文章建议结合系列的第一篇文章 骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案 一起看,这样可以对 今日头条屏幕适配方案 有一个更深入的理解,如果你能将整个系列的文章都全部认真看完,那你对 Android 屏幕适配领域的相关知识绝对会有一个飞速的提升!


    当你的项目需要切换某个框架时,你会怎么去考察、分析、对比现有的开源方案,并有足够的理由去选择或优化一个最适合自己项目的方案呢?其实整个系列文章可以看作是我怎么去选择同类型开源方案的过程,你以后当遇到同样的选择也可以参照我的思维方式去处理,当然如果以后面试官问到你屏幕适配相关的问题,你能将我如何选择、分析、对比已有方案的过程以及文章中的核心知识点告诉给面试官,那肯定比你直接说一句我使用的是某某开源库有价值得多



    作者:JessYan
    链接:https://www.jianshu.com/p/4aa23d69d481
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    骚年你的屏幕适配方式该升级了!-SmallestWidth 限定符适配方案

    前言 ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案 了解我的朋友一定知道,MVPArms 一直使用的是 鸿神 的 AndroidA...
    继续阅读 »

    前言


    ok,根据上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 smallestWidth 限定符屏幕适配方案


    了解我的朋友一定知道,MVPArms 一直使用的是 鸿神AndroidAutoLayout 屏幕适配方案,得益于 AndroidAutoLayout 的便捷,所以我对屏幕适配领域研究的不是很多,AndroidAutoLayout 停止维护后,我也一直在找寻着替代方案,直到 今日头条屏幕适配方案 刷屏,后来又无意间看到了 smallestWidth 限定符屏幕适配方案,这才慢慢的将研究方向转向了屏幕适配领域


    最近一个月才开始慢慢恶补 Android 屏幕适配的相关知识,对这两个方案也进行了更深入的研究,可以说从一个小白慢慢成长而来,所以我明白小白的痛,因此在上一篇文章 骚年你的屏幕适配方式该升级了!-今日头条适配方案 中,把 今日头条屏幕适配方案 讲得非常的细,尽量把每一个知识点都描述清晰,深怕小白漏掉每一个细节,这篇文章我也会延续上一篇文章的优良传统,将 smallestWidth 限定符屏幕适配方案 的每一个知识点都描述清晰


    顺便说一句,感谢大家对 AndroidAutoSize 的支持,我只是在上一篇文章中提了一嘴我刚发布的屏幕适配框架 AndroidAutoSize,还没给出详细的介绍和原理剖析 (原计划在本系列的第三篇文章中发布),AndroidAutoSize 就被大家推上了 Github Trending,一个多星期就拿了 2k+ stars,随着关注度的增加,我在这段时间里也累坏了,issues 就没断过,不到半个月就提交了 200 多次 commit,但累并快乐着,在这里要再次感谢大家对 AndroidAutoSize 的认可




    大家要注意了!这些观点其实针对的是所有以百分比缩放布局的库,而不只是今日头条屏幕适配方案,所以这些观点也同样适用于 smallestWidth 限定符屏幕适配方案,这点有很多人存在误解,所以一定要注意!





    Android系统碎片化机型以及屏幕尺寸碎片化屏幕分辨率碎片化 有多严重大家可以通过 友盟指数 了解一下,有些时候在某些事情的决断标准上,并不能按照事情的对错来决断,大多数情况还是要分析成本,收益等多种因素,通过利弊来决断,每个人的利弊标准又都不一样,所以每个人的观点也都会有差别,但也都应该得到尊重,所以我只是说说自己的观点,也不否认任何人的观点


    方案是死的人是活的,在某些大屏手机或平板电脑上,您也可以采用其他适配方案和百分比库结合使用,比如针对某个屏幕区间的设备单独出一套设计图以显示比小屏幕手机更多更精细的内容,来达到与百分比库互补的效果,没有一个方案可以说自己是完美的,但我们能清晰的认识到不同方案的优缺点,将它们的优点相结合,才能应付更复杂的开发需求,产出最好的产品


    友情提示: 下面要介绍的 smallestWidth 限定符屏幕适配方案,原理也同样是按照百分比缩放布局,理论上也会存在上面所说的 大屏手机和小屏手机显示的内容相同 的问题,选择与否请仔细斟酌


    简介 smallestWidth 限定符适配方案


    这个方案的的使用方式和我们平时在布局中引用 dimens 无异,核心点在于生成 dimens.xml 文件,但是已经有大神帮我们做了这 一步


    ├── src/main
    │ ├── res
    │ ├── ├──values
    │ ├── ├──values-800x480
    │ ├── ├──values-860x540
    │ ├── ├──values-1024x600
    │ ├── ├──values-1024x768
    │ ├── ├──...
    │ ├── ├──values-2560x1440

    如果有人还记得上面这种 宽高限定符屏幕适配方案 的话,就可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案 长这样👇


    ├── src/main
    │ ├── res
    │ ├── ├──values
    │ ├── ├──values-sw320dp
    │ ├── ├──values-sw360dp
    │ ├── ├──values-sw400dp
    │ ├── ├──values-sw411dp
    │ ├── ├──values-sw480dp
    │ ├── ├──...
    │ ├── ├──values-sw600dp
    │ ├── ├──values-sw640dp

    原理


    其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-swdp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-swdp 文件夹,而对应的 values-swdp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备


    如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-swdp 文件夹,则会去寻找与之 最小宽度 (smallestWidth) 相近的 values-swdp 文件夹,系统只会寻找小于或等于当前设备 最小宽度 (smallestWidth)values-swdp,这就是优于 宽高限定符屏幕适配方案 的容错率,并且也可以少生成很多 values-swdp 文件夹,减轻 App 的体积


    什么是 smallestWidth


    smallestWidth 翻译为中文的意思就是 最小宽度,那这个 最小宽度 是什么意思呢?


    系统会根据当前设备屏幕的 最小宽度 来匹配 values-swdp,为什么不是根据 宽度 来匹配,而要加上 最小 这两个字呢?


    这就要说到,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 最小 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 最小宽度,这个 最小宽度 是根据屏幕来定的,是固定不变的,意思是不管您怎么旋转屏幕,只要这个屏幕的高度大于宽度,那系统就只会认定宽度的值为 最小宽度,反之如果屏幕的宽度大于高度,那系统就会认定屏幕的高度的值为 最小宽度


    如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?可以再根据 values-wdp (去掉 sw 中的 s) 生成一套资源文件


    如果想区分屏幕的方向来做适配该怎么办呢?那就只有再根据 屏幕方向限定符 生成一套资源文件咯,后缀加上 -land-port 即可,像这样,values-sw400dp-land (最小宽度 400 dp 横向)values-sw400dp-port (最小宽度 400 dp 纵向)


    smallestWidth 的值是怎么算的


    要先算出当前设备的 smallestWidth 值我们才能知道当前设备该匹配哪个 values-swdp 文件夹


    ok,还是按照上一篇文章的叙述方式,现在来举栗说明,帮助大家更好理解


    我们假设设备的屏幕信息是 1920 * 1080480 dpi


    根据上面的规则我们要在屏幕的高度和宽度中选择值最小的一方作为最小宽度,1080 < 1920,明显 1080 px 就是我们要找的 最小宽度 的值,但 最小宽度 的单位是 dp,所以我们要把 px 转换为 dp


    帮助大家再巩固下基础,下面的公式一定不能再忘了!


    px / density = dpDPI / 160 = density,所以最终的公式是 px / (DPI / 160) = dp


    所以我们得到的 最小宽度 的值是 360 dp (1080 / (480 / 160) = 360)


    现在我们已经算出了当前设备的最小宽度是 360 dp,我们晓得系统会根据这个 最小宽度 帮助我们匹配到 values-sw360dp 文件夹下的 dimens.xml 文件,如果项目中没有 values-sw360dp 这个文件夹,系统才会去匹配相近的 values-swdp 文件夹


    dimens.xml 文件是整个方案的核心所在,所以接下来我们再来看看 values-sw360dp 文件夹中的这个 dimens.xml 是根据什么原理生成的


    dimens.xml 生成原理


    因为我们在项目布局中引用的 dimens 的实际值,来源于根据当前设备屏幕的 最小宽度 所匹配的 values-swdp 文件夹中的 dimens.xml,所以搞清楚 dimens.xml 的生成原理,有助于我们理解 smallestWidth 限定符屏幕适配方案


    说到 dimens.xml 的生成,就要涉及到两个因数,第一个因素是 最小宽度基准值,第二个因素就是您的项目需要适配哪些 最小宽度,通俗理解就是需要生成多少个 values-swdp 文件夹


    第一个因素


    最小宽度基准值 是什么意思呢?简单理解就是您需要把设备的屏幕宽度分为多少份,假设我们现在把项目的 最小宽度基准值 定为 360,那这个方案就会理解为您想把所有设备的屏幕宽度都分为 360 份,方案会帮您在 dimens.xml 文件中生成 1360dimens 引用,比如 values-sw360dp 中的 dimens.xml 是长这样的

    <?xml version="1.0" encoding="UTF-8"?>
    <resources>
    <dimen name="dp_1">1dp</dimen>
    <dimen name="dp_2">2dp</dimen>
    <dimen name="dp_3">3dp</dimen>
    <dimen name="dp_4">4dp</dimen>
    <dimen name="dp_5">5dp</dimen>
    <dimen name="dp_6">6dp</dimen>
    <dimen name="dp_7">7dp</dimen>
    <dimen name="dp_8">8dp</dimen>
    <dimen name="dp_9">9dp</dimen>
    <dimen name="dp_10">10dp</dimen>
    ...
    <dimen name="dp_356">356dp</dimen>
    <dimen name="dp_357">357dp</dimen>
    <dimen name="dp_358">358dp</dimen>
    <dimen name="dp_359">359dp</dimen>
    <dimen name="dp_360">360dp</dimen>
    </resources>



    values-sw360dp 指的是当前设备屏幕的 最小宽度360dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 360dp),把屏幕宽度分为 360 份,刚好每份等于 1dp,所以每个引用都递增 1dp,值最大的 dimens 引用 dp_360 值也是 360dp,刚好覆盖屏幕宽度


    下面再来看看将 最小宽度基准值 定为 360 时,values-sw400dp 中的 dimens.xml 长什么样

    <?xml version="1.0" encoding="UTF-8"?>
    <resources>
    <dimen name="dp_1">1.1111dp</dimen>
    <dimen name="dp_2">2.2222dp</dimen>
    <dimen name="dp_3">3.3333dp</dimen>
    <dimen name="dp_4">4.4444dp</dimen>
    <dimen name="dp_5">5.5556dp</dimen>
    <dimen name="dp_6">6.6667dp</dimen>
    <dimen name="dp_7">7.7778dp</dimen>
    <dimen name="dp_8">8.8889dp</dimen>
    <dimen name="dp_9">10.0000dp</dimen>
    <dimen name="dp_10">11.1111dp</dimen>
    ...
    <dimen name="dp_355">394.4444dp</dimen>
    <dimen name="dp_356">395.5556dp</dimen>
    <dimen name="dp_357">396.6667dp</dimen>
    <dimen name="dp_358">397.7778dp</dimen>
    <dimen name="dp_359">398.8889dp</dimen>
    <dimen name="dp_360">400.0000dp</dimen>
    </resources>



    values-sw400dp 指的是当前设备屏幕的 最小宽度400dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 400dp),把屏幕宽度同样分为 360份,这时每份就等于 1.1111dp 了,每个引用都递增 1.1111dp,值最大的 dimens 引用 dp_360 同样刚好覆盖屏幕宽度,为 400dp


    通过两个 dimens.xml 文件的比较,dimens.xml 的生成原理一目了然,方案会先确定 最小宽度基准值,然后将每个 values-swdp 中的 dimens.xml 文件都分配与 最小宽度基准值 相同的份数,再根据公式 屏幕最小宽度 / 份数 (最小宽度基准值) 求出每份占多少 dp,保证不管在哪个 values-swdp 中,份数 (最小宽度基准值) * 每份占的 dp 值 的结果都是刚好覆盖屏幕宽度,所以在 份数 不变的情况下,只需要根据屏幕的宽度在不同的设备上动态调整 每份占的 dp 值,就能完成适配


    这样就能保证不管将项目运行到哪个设备上,只要当前设备能匹配到对应的 values-swdp 文件夹,那布局中的 dimens 引用就能根据当前屏幕的情况进行缩放,保证能完美适配,如果没有匹配到对应的 values-swdp 文件夹,也没关系,它会去寻找与之相近的 values-swdp 文件夹,虽然在这种情况下,布局中的 dimens 引用的值可能有些许误差,但是也能保证最大程度的完成适配


    说到这里,那大家就应该就会明白我为什么会说 smallestWidth 限定符屏幕适配方案 的原理也同样是按百分比进行布局,如果在布局中,一个 View 的宽度引用 dp_100,那不管运行到哪个设备上,这个 View 的宽度都是当前设备屏幕总宽度的 360分之100,前提是项目提供有当前设备屏幕对应的 values-swdp,如果没有对应的 values-swdp,就会去寻找相近的 values-swdp,这时就会存在误差了,至于误差是大是小,这就要看您的第二个因数怎么分配了


    其实 smallestWidth 限定符屏幕适配方案 的原理和 今日头条屏幕适配方案 挺像的,今日头条屏幕适配方案 是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),而 smallestWidth 限定符屏幕适配方案 同样是根据屏幕的宽度动态调整每个设备 每份占的 dp 值


    第二个因素


    第二个因数是需要适配哪些 最小宽度?比如您想适配的 最小宽度320dp360dp400dp411dp480dp,那方案就会为您的项目生成 values-sw320dpvalues-sw360dpvalues-sw400dpvalues-sw411dpvalues-sw480dp 这几个资源文件夹,像这样👇


    ├── src/main
    │ ├── res
    │ ├── ├──values
    │ ├── ├──values-sw320dp
    │ ├── ├──values-sw360dp
    │ ├── ├──values-sw400dp
    │ ├── ├──values-sw411dp
    │ ├── ├──values-sw480dp

    方案会为您需要适配的 最小宽度,在项目中生成一系列对应的 values-swdp,在前面也说了,如果某个设备没有为它提供对应的 values-swdp,那它就会去寻找相近的 values-swdp,但如果这个相近的 values-swdp 与期望的 values-swdp 差距太大,那适配效果也就会大打折扣


    那是不是 values-swdp 文件夹生成的越多,覆盖越多市面上的设备,就越好呢?


    也不是,因为每个 values-swdp 文件夹其实都会占用一定的 App 体积,values-swdp 文件夹越多,App 的体积也就会越大


    所以一定要合理分配 values-swdp,以越少的 values-swdp 文件夹,覆盖越多的机型


    验证方案可行性


    原理讲完了,我们还是按照老规矩,来验证一下这个方案是否可行?


    假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133)


    在使用 smallestWidth 限定符屏幕适配方案 时,需要提供 最小宽度基准值 和需要适配哪些 最小宽度,我们就把 最小宽度基准值 设置为 375 (和 设计图 一致),这时方案就会为我们需要适配的 最小宽度 生成对应的 values-swdp 文件夹,文件夹中的 dimens.xml 文件是由从 1375 组成的 dimens 引用,把所有设备的屏幕宽度都分为 375 份,所以在布局文件中我们应该把这个 View 的高宽都引用 dp_50


    下面就来验证下在使用 smallestWidth 限定符屏幕适配方案 的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致


    验证设备 1


    设备 1 的屏幕总宽度为 1080 px,屏幕总高度为 1920 pxDPI480


    设备 1 的屏幕高度大于屏幕宽度,所以 设备 1最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 1最小宽度 的值为 360 dp (1080 / (480 / 160) = 360)


    根据 设备 1最小宽度 应该匹配的是 values-sw360dp 这个文件夹,假设 values-sw360dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值375 生成的,360 / 375 = 0.96,所以每份占的 dp 值为 0.96dimens.xml 里面的内容是长下面这样的

    <?xml version="1.0" encoding="UTF-8"?>
    <resources>
    <dimen name="dp_1">0.96dp</dimen>
    <dimen name="dp_2">1.92dp</dimen>
    <dimen name="dp_3">2.88dp</dimen>
    <dimen name="dp_4">3.84dp</dimen>
    <dimen name="dp_5">4.8dp</dimen>
    ...
    <dimen name="dp_50">48dp</dimen>
    ...
    <dimen name="dp_371">356.16dp</dimen>
    <dimen name="dp_372">357.12dp</dimen>
    <dimen name="dp_373">358.08dp</dimen>
    <dimen name="dp_374">359.04dp</dimen>
    <dimen name="dp_375">360dp</dimen>
    </resources>



    可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw360dp 中定格在了 48 dp,所以这个 View设备 1 上的高宽都为 48 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 144 px (48 * (480 / 160) = 144)


    144 / 1080 = 0.133View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


    某些设备的高宽是和 设备 1 相同的,但是 DPI 可能不同,而由于 smallestWidth 限定符屏幕适配方案 并没有像 今日头条屏幕适配方案 一样去自行修改 density,所以系统就会使用默认的公式 DPI / 160 求出 densitydensity 又会影响到 dppx 的换算,因此 DPI 的变化,是有可能会影响到 smallestWidth 限定符屏幕适配方案


    所以我们再来试试在这种特殊情况下 smallestWidth 限定符屏幕适配方案 是否也能完成适配


    验证设备 2


    设备 2 的屏幕总宽度为 1080 px,屏幕总高度为 1920 pxDPI420


    设备 2 的屏幕高度大于屏幕宽度,所以 设备 2最小宽度 为屏幕宽度,再根据公式 px / (DPI / 160) = dp,求出 设备 2最小宽度 的值为 411.429 dp (1080 / (420 / 160) = 411.429)


    根据 设备 2最小宽度 应该匹配的是 values-sw411dp 这个文件夹,假设 values-sw411dp 文件夹及里面的 dimens.xml 已经生成,且是按 最小宽度基准值375 生成的,411 / 375 = 1.096,所以每份占的 dp 值为 1.096dimens.xml 里面的内容是长下面这样的👇

    <?xml version="1.0" encoding="UTF-8"?>
    <resources>
    <dimen name="dp_1">1.096dp</dimen>
    <dimen name="dp_2">2.192dp</dimen>
    <dimen name="dp_3">3.288dp</dimen>
    <dimen name="dp_4">4.384dp</dimen>
    <dimen name="dp_5">5.48dp</dimen>
    ...
    <dimen name="dp_50">54.8dp</dimen>
    ...
    <dimen name="dp_371">406.616dp</dimen>
    <dimen name="dp_372">407.712dp</dimen>
    <dimen name="dp_373">408.808dp</dimen>
    <dimen name="dp_374">409.904dp</dimen>
    <dimen name="dp_375">411dp</dimen>
    </resources>



    可以看到这个 View 在布局中引用的 dp_50,最终在 values-sw411dp 中定格在了 54.8dp,所以这个 View设备 2 上的高宽都为 54.8 dp,系统最后会将高宽都换算成 px,根据公式 dp * (DPI / 160) = px,所以这个 View 的高宽换算为 px 后等于 143.85 px (54.8 * (420 / 160) = 143.85)


    143.85 / 1080 = 0.133View 的实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


    虽然 View设备 2 上的高宽是 143.85 px,比 设备 1144 px 少了 0.15 px,但是误差非常小,整体的比例并没有发生太大的变化,是完全可以接受的


    这个误差是怎么引起的呢,因为 设备 2最小宽度 的实际值是 411.429 dp,但是匹配的 values-sw411dp 舍去了小数点后面的位数 (切记!系统会去寻找小于或等于 411.429 dp 的 values-swdp,所以 values-sw412dp 这个文件夹,设备 2 是匹配不了的),所以才存在了一定的误差,因此上面介绍的第二个因数是非常重要的,这直接决定误差是大还是小


    可以看到即使在高宽一样但 DPI 不一样的设备上,smallestWidth 限定符屏幕适配方案 也能完成等比例适配,证明这个方案是可行的,如果大家还心存疑虑,也可以再试试其他分辨率的设备,其实到最后得出的比例都是在 0.133 左右,唯一的变数就是第二个因数,如果您生成的 values-swdp 与设备实际的 最小宽度 差别不大,那误差也就在能接受的范围内,如果差别很大,那就直接 GG


    优点



    1. 非常稳定,极低概率出现意外


    2. 不会有任何性能的损耗


    3. 适配范围可自由控制,不会影响其他三方库


    4. 在插件的配合下,学习成本低



    缺点



    1. 在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦


    2. 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂


    3. 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择


    4. 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积


    5. 不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积


    6. 不能以高度为基准进行适配,考虑到这个方案的名字本身就叫 最小宽度限定符适配方案,所以在使用这个方案之前就应该要知道这个方案只能以宽度为基准进行适配,为什么现在的屏幕适配方案只能以高度或宽度其中的一个作为基准进行适配,请看 这里



    使用中的问题


    这时有人就会问了,设计师给的设计图只标注了 px,使用这个方案时,那不是还要先将 px 换算成 dp


    其实也可以不用换算的,那这是什么骚操作呢?


    很简单,你把设计图的 px 总宽度设置成 最小宽度基准值 就可以了,还是以前面验证可行性的例子


    我们在前面验证可行性时把 最小宽度基准值 设置成了 375,为什么是 375 呢?因为设计图的总宽度为 375 dp,如果换算成 px,总宽度就是 750 px,我们这时把 最小宽度基准值 设置成 750,然后看看 values-sw360dp 中的 dimens.xml 长什么样👇<?xml version="1.0" encoding="UTF-8"?>

    <resources>
    <dimen name="px_1">0.48dp</dimen>
    <dimen name="px_2">0.96dp</dimen>
    <dimen name="px_3">1.44dp</dimen>
    <dimen name="px_4">1.92dp</dimen>
    <dimen name="px_5">2.4dp</dimen>
    ...
    <dimen name="px_50">24dp</dimen>
    ...
    <dimen name="px_100">48dp</dimen>
    ...
    <dimen name="px_746">358.08dp</dimen>
    <dimen name="px_747">358.56dp</dimen>
    <dimen name="px_748">359.04dp</dimen>
    <dimen name="px_749">359.52dp</dimen>
    <dimen name="px_750">360dp</dimen>
    </resources>



    360 dp 被分成了 750 份,相比之前的 375 份,现在 每份占的 dp 值 正好减少了一半,还记得在验证可行性的例子中那个 View 的尺寸是多少吗?50dp * 50dp,如果设计图只标注 px,那这个 View 在设计图上的的尺寸应该是 100px * 100px,那我们直接根据设计图上标注的 px,想都不用想直接在布局中引用 px_100 就可以了,因为在 375 份时的 dp_50 刚好等于 750 份时的 px_100 (值都是 48 dp),所以这时的适配效果和之前验证可行性时的适配效果没有任何区别


    看懂了吗?直接将 最小宽度基准值 和布局中的引用都以 px 作为单位就可以直接填写设计图上标注的 px


    总结


    关于文中所列出的优缺点,列出的缺点数量确实比列出的优点数量多,但 缺点 3缺点 4缺点 5 其实都可以归纳于 占用 App 体积 这一个缺点,因为他们都可以通过增加资源文件来解决问题,而 缺点 6 则是这个方案的特色,只能以宽度为基准进行适配,这个从这个方案的名字就能看出


    请大家千万不要曲解文章的意思,不要只是单纯的对比优缺点的数量,缺点的数量大于优点的数量就一定是这个方案不行?没有一个方案是完美的,每个人的需求也都不一样,作为一篇科普类文章我只可能把这个方案描述得尽可能的全面


    这个方案能给你带来什么,不能给你带来什么,我必须客观的描述清楚,这样才有助你做出决定,你应该注重的是在这些优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,而不只是单纯的去看数量,这样毫无意义,有些人就是觉得稳定性最重要,其他的都可以做出妥协,那其他缺点对于他来说都是无所谓的


    好了,这个系列的第二篇文章讲完了,这篇文章也是按照上篇文章的优良传统,写的非常详细,哪怕是新手我相信也应该能看懂,为什么这么多人都不知道自己该选择什么样的方案,就是因为自己都没搞懂这些方案的原理,懂了原理过后才知道这些方案是否是自己想要的


    接下来的第三篇文章会详细讲解两个方案的深入对比以及该如何选择,并剖析我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize 的原理,敬请期待


    如果大家想使用 smallestWidth 限定符屏幕适配方案,可以参考 这篇文章,里面提供有自动生成资源文件的插件和 Demo,由于我并没有在项目中使用 smallestWidth 限定符屏幕适配方案,所以如果在文章中有遗漏的知识点请谅解以及补充,感谢!



    作者:JessYan
    链接:https://www.jianshu.com/p/2aded8bb6ede
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    骚年你的屏幕适配方式该升级了!-今日头条适配方案

    前言 这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案 上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方...
    继续阅读 »

    前言


    这个月在 Android 技术圈中 屏幕适配 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案


    上上个星期 Blankj 老师发表了一篇力挺今日头条屏幕适配方案的 文章,提出了很多优化的方案,并开源了相关源码


    上个星期 拉丁吴 老师在 鸿神 的公众号上发布了一篇 文章,详细描述了市面上主流的几种屏幕适配方案,并发布了他的 smallestWidth 限定符适配方案和相关源码 (其实早就发布了),文章写的很好,建议大家去看看


    其实大家最关注的不是市面上有多少种屏幕适配方案,而是自己的项目该选择哪种屏幕适配方案,可以看出两位老师最终选择的屏幕适配方案都是不同的


    我下面就来分析分析,我作为一个才接触这两个屏幕适配方案的吃瓜群众,我是怎么来验证这两种屏幕适配方案是否可行,以及怎样根据它们的优缺点来选择一个最适合自己项目的屏幕适配方案


    浅谈适配方案


    拉丁吴 老师的文章中谈到了两个比较经典的屏幕适配方案,在我印象中十分深刻,我想大多数兄弟都用过,在我的开发生涯里也是有很长一段时间都在用这两种屏幕适配方案


    第一种就是宽高限定符适配,什么是宽高限定符适配呢

    ├── src/main

    │   ├── res
    │ ├── ├──values
    │ ├── ├──values-800x480
    │ ├── ├──values-860x540
    │ ├── ├──values-1024x600
    │ ├── ├──values-1024x768
    │ ├── ├──...
    │ ├── ├──values-2560x1440

    就是这种,在资源文件下生成不同分辨率的资源文件,然后在布局文件中引用对应的 dimens,大家一定还有印象


    第二种就是 鸿神AndroidAutoLayout


    这两种方案都已经逐渐退出了历史的舞台,为什么想必大家都知道,不知道的建议看看 拉丁吴 老师的文章,所以这两种方案我在文章中就不在阐述了,主要讲讲现在最主流的两种屏幕适配方案,今日头条适配方案smallestWidth 限定符适配方案


    建议大家不清楚这两个方案的先看看这两篇文章,才清楚我在讲什么,后面我要讲解它们的原理,以及验证这两种方案是否真的可行,最后对他们进行深入对比,对于他们的一些缺点给予对应的解决方案,绝对干货


    今日头条屏幕适配方案


    原理


    上面已经告知,不了解这两个方案的先看看上面的两篇文章,所以这里我就假设大家已经看了上面的文章或者之前就了解过这两个方案,所以在本文中我就不再阐述 DPIDensity 以及一些比较基础的知识点,上面的文章已经阐述的够清楚了


    今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density


    当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density


    density 的意思就是 1 dp 占当前设备多少像素


    为什么要算出 density,这和屏幕适配有什么关系呢?


    public static float applyDimension(int unit, float value,
    DisplayMetrics metrics
    )

    {
    switch (unit) {
    case COMPLEX_UNIT_PX:
    return value;
    case COMPLEX_UNIT_DIP:
    return value * metrics.density;
    case COMPLEX_UNIT_SP:
    return value * metrics.scaledDensity;
    case COMPLEX_UNIT_PT:
    return value * metrics.xdpi * (1.0f/72);
    case COMPLEX_UNIT_IN:
    return value * metrics.xdpi;
    case COMPLEX_UNIT_MM:
    return value * metrics.xdpi * (1.0f/25.4f);
    }
    return 0;
    }

    大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 px


    所以我们常用的 pxdp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步


    要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 AndroidAutoLayout 一样,高以高为基准,宽以宽为基准,同时进行适配呢


    这就引出了一个现在比较棘手的问题,大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致


    这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题


    明白这个后,我再来说说 densitydensity 在每个设备上都是固定的,DPI / 160 = density屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度



    • 设备 1,屏幕宽度为 1080px480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360dp


    • 设备 2,屏幕宽度为 1440560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411dp



    可以看到屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的


    这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8% (100 / 360 = 0.278)


    但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3% (100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差


    这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的


    这时我们该怎么做呢?改变每个 Viewdp 值?不现实,在每个设备上都要通过代码动态计算 Viewdp 值,工作量太大


    如果每个 Viewdp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp


    屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度


    在这个公式中我们要保证 屏幕的总 dp 宽度设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了


    当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density


    这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配


    验证方案可行性


    上面已经把原理分析的很清楚了,很多文章只是一笔带过这个公式,公式虽然很简单但我们还是想晓得这是怎么来的,所以我就反向推理了一遍,如果还是看不懂,那我只能说我尽力了,原理讲完了,那我们再来现场验证一下这个方案是否可行?


    假设设计图总宽度为 375 dp,一个 View 在这个设计图上的尺寸是 50dp * 50dp,这个 View 的宽度占整个设计图宽度的 13.3% (50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 View 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致


    验证设备 1


    屏幕总宽度为 1080 px,根据今日头条的的公式求出 density1080 / 375 = 2.88 (density)


    这个 50dp * 50dpView,系统最后会将高宽都换算成 px50dp * 2.88 = 144 px (根据公式 dp * density = px)


    144 / 1080 = 0.133View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放


    某些设备总宽度为 1080 px,但是 DPI 可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 DPI 求出 density,是根据自己的公式求出的 density,所以这对今日头条的方案没有影响


    上面只能确定在所有屏幕总宽度为 1080 px 的设备上能完成等比例适配,那我们再来试试其他分辨率的设备


    验证设备 2


    屏幕总宽度为 1440 px,根据今日头条的的公式求出 density1440 / 375 = 3.84 (density)


    这个 50dp * 50dpView,系统最后会将高宽都换算成 px50dp * 3.84 = 192 px (根据公式 dp * density = px)


    192 / 1440 = 0.133View 实际宽度与 屏幕总宽度 的比例和 View 在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放


    两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 0.133


    优点



    1. 使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案


    2. 侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0


    3. 可适配三方库的控件和系统的控件(不止是 ActivityFragmentDialogToast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益


    4. 不会有任何性能的损耗



    缺点


    暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的


    只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的


    这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样


    当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重


    举个栗子


    假设一个三方库的 View,作者在设计时,把它设计为 100dp * 100dp,设计图的最大宽度为 1000dp,这个 View 在设计图中的比例是 100 / 1000 = 0.1,意思是这个 View 的宽度在设计图中占整个宽度的 10%,如果我们要完成等比例适配,那这个三方库 View 在所有的设备上与屏幕的总宽度的比例,都必须保持在 10%


    这时在一个使用今日头条屏幕适配方案的项目上,设置的设计图最大宽度如果是 1000dp,那这个三方库 View,与项目自身都可以完美的适配,但当我们项目自身的设计图最大宽度不是 1000dp,是 500dp 时,100 / 500 = 0.2,可以看到,比例发生了较大的变化,从 10% 上升为 20%,明显这个三方库 View 高于作者的预期,比之前更大了


    这就是两个设计图尺寸不一致导致的非常严重的问题,当两个设计图尺寸差距越大,那适配的效果也就天差万别了


    解决方案


    方案 1


    调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配


    这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变


    但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity 为单位修改设计图尺寸,相当于每个 Activity 都可以自定义设计图尺寸,因为有些 Activity 不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity 都有控制权的话,这也是最灵活的


    但这也有个问题,当一个 Activity 使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题


    方案 2


    第二个方案是最简单的,也是按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案


    使用中的问题


    有些文章中提到了今日头条屏幕适配方案可以将设计图尺寸填写成以 px 为单位的宽度和高度,这样我们在布局文件中,也就能直接填写设计图上标注的 px 值,省掉了将 px 换算为 dp 的时间 (大部分公司的设计图都只标注 px 值),而且照样能完美适配


    但是我建议大家千万不要这样做,还是老老实实的以 dp 为单位填写 dp 值,为什么呢?


    直接填写 px 虽然刚开始布局的时候很爽,但是这个坑就已经埋上了,会让你后面很爽,有哪些坑?


    第一个坑


    这样无疑于使项目强耦合于这个方案,当你遇到无法解决的问题想切换为其他屏幕适配方案的时候,layout 文件里曾经填写的 px 值都会作为 dp


    比如你的设计图实际宽度为 1080px,你不换算为 360dp (1080 / 3 = 360),却直接将 1080px 作为这个方案的设计图尺寸,那你在 layout 文件中,填写的也都是设计图上标注的 px 值,但是单位却是 dp


    一个在设计图上 300px * 300pxView,你可以直接在 layout 文件中填写为 300dp,而由于这个方案可以动态改变 density 的原因还是可以做到等比例适配,非常爽!


    但你不要忘了,这样你就强耦合于这个方案了,因为当你不使用这个方案时,density 是不可变的!


    举个栗子


    使用这个方案时,在屏幕宽度为 1080px 的设备上,将设计图宽度直接填写为 1080,根据今日头条公式


    当前设备屏幕总宽度 / 设计图总宽度 = density


    这时得出 density 为 1 (1080 / 1080 = 1),所以你在 layout 文件中你填写的 300dp 最后转换为 px 也是 300px (300dp * 1 = 300px 根据公式 dp * density = px)


    在这个方案的帮助下非常完美,和设计图一模一样完成了适配


    但当你不使用这个方案时,density 的换算公式就变为官方的 DPI / 160 = density

    在这个屏幕宽度为 1080px480dpi 的设备上,density 就固定为 3 (480 / 160 = 3)


    这时再来看看你之前在 layout 文件中填写的 dp,换算成 px900 px (300dp * 3 = 900px 根据公式 dp * density = px)


    原本在在设计图上为 300pxView,这时却达到了惊人的 900px,3倍的差距,恭喜你,你已经强耦合于这个方案了,你要不所有 layout 文件都改一遍,要不继续使用这个方案


    第二个坑


    第二个坑其实就是刚刚在上面说的今日头条适配方案的缺点,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重


    你如果直接填写以 px 为设计图的尺寸,这不用想,肯定和所有的三方库以及系统控件的设计图尺寸都不一样,而且差距都非常之大,至少两三倍的差距,这时你在当前页面弹个 Toast 就可以明显看到,比之前小很多,可以说是天差万别,用其他三方库 View,也是一样的,会小很多


    因为你以 px 为单位填写设计图尺寸,人家却用的 dp,差距能不大吗,你如果老老实实用 dp,哪怕三方库的设计图尺寸和你项目自身的设计图尺寸不一样,那也差距不大,小到一定程度,基本都不用调整,可以忽略不计,而且很多三方库的设计图尺寸其实也都是那几个大众尺寸,很大可能和你项目自身的设计图尺寸一样


    总结


    可以看到我讲的非常详细,可以说比今日头条官方以及任何博客写的都清楚,从原理到优缺点再到解决方案一应俱全,因为篇幅有限,如果我还想把 smallestWidth 限定符适配方案写的这么详细,那估计这篇文章得有一万字了


    所以我把这次的屏幕适配文章归位一个系列,一共分为三篇,第一篇详细的讲 今日头条屏幕适配方案,第二篇详细的讲 smallestWidth 限定符适配方案,第三篇详细讲两个方案的深入对比以及如何选择,并发布我根据 今日头条屏幕适配方案 优化的屏幕适配框架 AndroidAutoSize


    今日头条屏幕适配方案 官方公布的核心源码只有 30 行不到,但我这个框架的源码有 1500 行以上,在保留原有特性的情况下增加了不少功能和特性,功能增加了不少,但是使用上却变简单了









    只要这一步填写了设计图的高宽以 dp 为单位,你什么都不做,框架就开始适配了


    大家可以提前看看我是怎么封装和优化的,我后面的第三篇文章会给出这个框架的原理分析,敬请期待




    关于大家的评论以及关注的问题,我在这里统一回复一下:


    感谢,大家的关注和回复,我介绍这个今日头条的屏幕适配方案并不是说他有多么完美,只是他确实有效而且能帮我们减少很多开发成本


    对于很多人说的 DPI 的存在,不就是为了让大屏能显示更多的内容,如果一个大屏手机和小屏手机,显示的内容都相同,那用户买大屏手机又有什么意义呢,我觉得大家对 DPI 的理解是对的,这个观点我也是认同的,Google 设计 DPI 时可能也是这么想的,但是有一点大家没考虑到,Android 的碎片化太严重了


    为什么 Android 诞生这么多年,Android 的百分比库层出不穷,按理说他们都违背了上面说的这个理念,但为什么还有这么多人去研究百分比库通过各种方式去实现百分比布局 (谷歌官方也曾出过百分比库)?为什么?很简单,因为需求啊!为什么需求,因为开发成本低啊!为什么今日头条的这个屏幕适配方案现在能这么火,因为他的开发成本是目前所有屏幕适配方案中最低的啊!


    DPI 的意义谁又不懂呢?难道就你懂,今日头条这种大公司的程序员不懂这个道理吗?今日头条这么大的公司难道不想把每个机型每个版本的设备都适配完美,让自己的 App 体验更好,哪怕付出更大的成本,有些时候想象是美好的,但是这个投入的成本,谁又能承担呢,连今日头条这么大的公司,这么雄厚的资本都没选择投入更大的成本,对每个机型进行更精细化的适配,难道市面上的中小型公司又有这个能力投入这么大的成本吗?


    鱼和熊掌不可兼得,DPI 的意义在 Google 的设计理念中是完全正确的,但不是所有公司都能承受这个成本,想必今日头条的程序员,也是因为今日头条 App 的用户量足够多,机型分布足够广,也是被屏幕适配这个问题折磨的不要不要的,才想出这么个不这么完美但是却很有效的方案



    作者:JessYan
    链接:https://www.jianshu.com/p/55e0fca23b4f
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Android 优雅的为RecyclerView添加HeaderView和FooterView

    1、概述RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求。如果你对RecyclerView不陌生的话,你一定遇到过这样的情况,我想给RecyclerView加个headerView或...
    继续阅读 »

    1、概述

    RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求。

    如果你对RecyclerView不陌生的话,你一定遇到过这样的情况,我想给RecyclerView加个headerView或者footerView,当你敲出.addHeaderView,你会发现并没有添加头部或者底部View的相关API。

    那么本文主要的内容很明显了,完成以下工作:

    如何为RecyclerView添加HeaderView(支持多个)
    如何为RecyclerView添加FooterView(支持多个)
    如何让HeaderView或者FooterView适配各种LayoutManager

    那我只能考虑自己换种思路来解决这个问题,并且提供尽可能多的功能了~

    2 、思路

    (1)原理

    对于添加headerView或者footerView的思路

    其实HeaderView实际上也是Item的一种,只不过显示在顶部的位置,那么我们完全可以通过为其设置ItemType来完成。

    有了思路以后,我们心里就妥了,最起码我们的内心中想想是可以实现的,接下来考虑一些细节。

    (2)一些细节

    假设我们现在已经完成了RecyclerView的编写,忽然有个需求,需要在列表上加个HeaderView,此时我们该怎么办呢?

    打开我们的Adapter,然后按照我们上述的原理,添加特殊的ViewType,然后修改代码完成。

    这是比较常规的做法了,但是有个问题是,如果需要添加viewType,那么可能我们的Adapter需要修改的幅度就比较大了,比如getItemType、getItemCount、onBindViewHolder、onCreateViewHolder等,几乎所有的方法都要进行改变。

    这样来看,出错率是非常高的。

    况且一个项目中可能多个RecyclerView都需要在其列表中添加headerView。

    这么来看,直接改Adapter的代码是非常不划算的,最好能够设计一个类,可以无缝的为原有的Adapter添加headerView和footerView。

    本文的思路是通过类似装饰者模式,去设计一个类,增强原有Adapter的功能,使其支持addHeaderView和addFooterView。这样我们就可以不去改动我们之前已经完成的代码,灵活的去扩展功能了。

    我希望的用法是这样的:

    mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
    t1.setText("Header 1");
    TextView t2 = new TextView(this);
    mHeaderAndFooterWrapper.addHeaderView(t2);


    在不改变原有的Adapter基础上去增强其功能。

    3、初步的实现

    (1) 基本代码



    首先我们编写一个Adapter的子类,我们叫做HeaderAndFooterWrapper,然后再其内部添加了addHeaderView,addFooterView等一些辅助方法。

    这里你可以看到,对于多个HeaderView,讲道理我们首先想到的应该是使用List,而这里我们为什么要使用SparseArrayCompat呢?

    public class HeaderAndFooterWrapper extends RecyclerView.Adapter
    {
    private static final int BASE_ITEM_TYPE_HEADER = 100000;
    private static final int BASE_ITEM_TYPE_FOOTER = 200000;

    private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
    private SparseArrayCompat mFootViews = new SparseArrayCompat<>();

    private RecyclerView.Adapter mInnerAdapter;

    public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
    {
    mInnerAdapter = adapter;
    }

    private boolean isHeaderViewPos(int position)
    {
    return position < getHeadersCount();
    }

    private boolean isFooterViewPos(int position)
    {
    return position >= getHeadersCount() + getRealItemCount();
    }


    public void addHeaderView(View view)
    {
    mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
    }

    public void addFootView(View view)
    {
    mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
    }

    public int getHeadersCount()
    {
    return mHeaderViews.size();
    }

    public int getFootersCount()
    {
    return mFootViews.size();
    }
    }


    SparseArrayCompat有什么特点呢?它类似于Map,只不过在某些情况下比Map的性能要好,并且只能存储key为int的情况。

    并且可以看到我们对每个HeaderView,都有一个特定的key与其对应,第一个headerView对应的是BASE_ITEM_TYPE_HEADER,第二个对应的是BASE_ITEM_TYPE_HEADER+1;

    为什么要这么做呢?

    这两个问题都需要到复写onCreateViewHolder的时候来说明。

    (2)复写相关方法

    public class HeaderAndFooterWrapper extends RecyclerView.Adapter
    {

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
    if (mHeaderViews.get(viewType) != null)
    {

    ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
    return holder;

    } else if (mFootViews.get(viewType) != null)
    {
    ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
    return holder;
    }
    return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public int getItemViewType(int position)
    {
    if (isHeaderViewPos(position))
    {
    return mHeaderViews.keyAt(position);
    } else if (isFooterViewPos(position))
    {
    return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
    }
    return mInnerAdapter.getItemViewType(position - getHeadersCount());
    }

    private int getRealItemCount()
    {
    return mInnerAdapter.getItemCount();
    }


    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    {
    if (isHeaderViewPos(position))
    {
    return;
    }
    if (isFooterViewPos(position))
    {
    return;
    }
    mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
    }

    @Override
    public int getItemCount()
    {
    return getHeadersCount() + getFootersCount() + getRealItemCount();
    }
    }



    getItemViewType
    由于我们增加了headerView和footerView首先需要复写的就是getItemCount和getItemViewType。

    getItemCount很好理解;

    对于getItemType,可以看到我们的返回值是mHeaderViews.keyAt(position),这个值其实就是我们addHeaderView时的key,footerView是一样的处理方式,这里可以看出我们为每一个headerView创建了一个itemType。

    onCreateViewHolder
    可以看到,我们分别判断viewType,如果是headview或者是footerview,我们则为其单独创建ViewHolder,这里的ViewHolder是我之前写的一个通用的库里面的类,文末有链接。当然,你也可以自己写一个ViewHolder的实现类,只需要将对应的headerView作为itemView传入ViewHolder的构造即可。

    这个方法中,我们就可以解答之前的问题了:

    为什么我要用SparseArrayCompat而不是List?
    为什么我要让每个headerView对应一个itemType,而不是固定的一个?
    对于headerView假设我们有多个,那么onCreateViewHolder返回的ViewHolder中的itemView应该对应不同的headerView,如果是List,那么不同的headerView应该对应着:list.get(0),list.get(1)等。

    但是问题来了,该方法并没有position参数,只有itemType参数,如果itemType还是固定的一个值,那么你是没有办法根据参数得到不同的headerView的。

    所以,我利用SparseArrayCompat,将其key作为itemType,value为我们的headerView,在onCreateViewHolder中,直接通过itemType,即可获得我们的headerView,然后构造ViewHolder对象。而且我们的取值是从100000开始的,正常的itemType是从0开始取值的,所以正常情况下,是不可能发生冲突的。

    需要说明的是,这里的意思并非是一定不能用List,通过一些特殊的处理,List也能达到上述我描述的效果。

    onBindViewHolder
    onBindViewHolder比较简单,发现是HeaderView或者FooterView直接return即可,因为对于头部和底部我们仅仅做展示即可,对于事件应该是在addHeaderView等方法前设置。

    这样就初步完成了我们的装饰类,我们分别添加两个headerView和footerView:

    大家都知道RecyclerView比较强大,可以设置不同的LayoutManager,那么我们换成GridLayoutMananger再看看效果。



    好像发现了不对的地方,我们的headerView果真被当成普通的Item处理了,不过由于我们的编写方式,出现上述情况是可以理解的。

    那么我们该如何处理呢?让每个headerView独立的占据一行?

    4、进一步的完善

    好在RecyclerView里面为我们提供了一些方法。

    (1)针对GridLayoutManager

    在我们的HeaderAndFooterWrapper中复写onAttachedToRecyclerView方法,如下:

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView)
    {
    innerAdapter.onAttachedToRecyclerView(recyclerView);

    RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
    if (layoutManager instanceof GridLayoutManager)
    {
    final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
    final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

    gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
    {
    @Override
    public int getSpanSize(int position)
    {
    int viewType = getItemViewType(position);
    if (mHeaderViews.get(viewType) != null)
    {
    return layoutManager.getSpanCount();
    } else if (mFootViews.get(viewType) != null)
    {
    return layoutManager.getSpanCount();
    }
    if (oldLookup != null)
    return oldLookup.getSpanSize(position);
    return 1;
    }
    });
    gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
    }
    }



    当发现layoutManager为GridLayoutManager时,通过设置SpanSizeLookup,对其getSpanSize方法,返回值设置为layoutManager.getSpanCount();


    (2)对于StaggeredGridLayoutManager

    在刚才的代码中我们好像没有发现StaggeredGridLayoutManager的身影,StaggeredGridLayoutManager并没有setSpanSizeLookup这样的方法,那么该如何处理呢?

    依然不复杂,重写onViewAttachedToWindow方法,如下:

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
    {
    mInnerAdapter.onViewAttachedToWindow(holder);
    int position = holder.getLayoutPosition();
    if (isHeaderViewPos(position) || isFooterViewPos(position))
    {
    ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

    if (lp != null
    && lp instanceof StaggeredGridLayoutManager.LayoutParams)
    {

    StaggeredGridLayoutManager.LayoutParams p =
    (StaggeredGridLayoutManager.LayoutParams) lp;

    p.setFullSpan(true);
    }
    }
    }



    这样就完成了对StaggeredGridLayoutManager的处理,效果图就不贴了。

    到此,我们就完成了整个HeaderAndFooterWrapper的编写,可以在不改变原Adapter代码的情况下,为其添加一个或者多个headerView或者footerView,以及完成了如何让HeaderView或者FooterView适配各种LayoutManager。

    源码地址:https://github.com/hongyangAndroid/baseAdapter
    ————————————————
    版权声明:本文为CSDN博主「鸿洋_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:
    https://blog.csdn.net/lmj623565791/article/details/51854533

    收起阅读 »

    小谈 Kotlin 的空处理

    近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin 与 Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。 ...
    继续阅读 »

    近来关于 Kotlin 的文章着实不少,Google 官方的支持让越来越多的开发者开始关注 Kotlin。不久前加入的项目用的是 Kotlin Java 混合开发的模式,纸上得来终觉浅,终于可以实践一把新语言。本文就来小谈一下 Kotlin 中的空处理。



    一、上手的确容易



    先扯一扯 Kotlin 学习本身。

    之前各种听人说上手容易,但真要切换到另一门语言,难免还是会踌躇是否有这个必要。现在因为工作关系直接上手 Kotlin,感受是 真香(上手的确容易)

    首先在代码阅读层面,对于有 Java 基础的程序员来说阅读 Kotlin 代码基本无障碍,除去一些操作符、一些顺序上的变化,整体上可以直接阅读。

    其次在代码编写层面,仅需要改变一些编码习惯。主要是:语句不要写分号、变量需要用 var val 声明、类型写在变量之后、实例化一个对象时不用 "new" …… 习惯层面的改变只需要多写代码,自然而然就适应了。

    最后在学习方式层面,由于 Kotlin 最终都会被编译成字节码跑在 JVM
    上,所以初入手时完全可以用 Java 作为对比。比如你可能不知道
    Kotlin
    companion object 是什么意思,但你知道既然
    Kotlin
    最终会转成 jvm 可以跑的字节码,那 Java 里必然可以找到与之对应的东西。

    Android Studio 也提供了很方便的工具。选择菜单 Tools -> Kotlin -> Show
    Kotlin Bytecode
    即可看到 Kotlin 编译成的字节码,点击窗口上方的 "Decompile" 即可看到这份字节码对应的 Java 代码。
    —— 这个工具特别重要,假如一段
    Kotlin
    代码让你看得云里雾里,看一下它对应的 Java 代码你就能知道它的含义。


    当然这里仅仅是说上手或入门(仅入门的话可以忽略诸如协程等高级特性),真正熟练应用乃至完全掌握肯定需要一定时间。



    二、针对 NPE 的强规则



    有些文章说 Kotlin 帮开发者解决了 NPENullPointerException),这个说法是不对的。在我看来,Kotlin 没有帮开发者解决了 NPE Kotlin: 臣妾真的做不到啊),而是通过在语言层面增加各种强规则,强制开发者去自己处理可能的空指针问题,达到尽量减少(只能减少而无法完全避免)出现 NPE 的目的。

    那么 Kotlin 具体是怎么做的呢?别着急,我们可以先回顾一下在 Java 中我们是怎么处理空指针问题的。

    Java 中对于空指针的处理总体来说可以分为防御式编程契约式编程两种方案。

    防御式编程大家应该不陌生,核心思想是不信任任何外部输入 —— 不管是真实的用户输入还是其他模块传入的实参,具体点就是各种判空。创建一个方法需要判空,创建一个逻辑块需要判空,甚至自己的代码内部也需要判空(防止对象的回收之类的)。示例如下:

       public void showToast(Activity activity) {

           if (activity == null) {

               return;

           }

           

           ......

       }

    另一种是契约式编程,各个模块之间约定好一种规则,大家按照规则来办事,出了问题找没有遵守规则的人负责,这样可以避免大量的判空逻辑。Android 提供了相关的注解以及最基础的检查来协助开发者,示例如下:

       public void showToast(@NonNull Activity activity) {

           ......

       }

    在示例中我们给 Activity 增加了 @NonNull 的注解,就是向所有调用这个方法的人声明了一个约定,调用方应该保证传入的 activity 非空。当然聪明的你应该知道,这是一个很弱的限制,调用方没注意或者不理会这个注解的话,程序就依然还有 NPE 导致的 crash 的风险。

    回过头来,对于 Kotlin,我觉得就是一种把契约式编程和防御式编程相结合且提升到语言层面的处理方式。(听起来似乎比 Java 中各种判空或注解更麻烦?继续看下去,你会发现的确是更麻烦……

    Kotlin 中,有以下几方面约束:

    1.  
    在声明阶段,变量需要决定自己是否可为空,比如
    var time: Long? 可接受 null,而 var time: Long 则不能接受 null

    2.  
    在变量传递阶段,必须保持可空性一致,比如形参声明是不为空的,那么实参必须本身是非空或者转为非空才能正常传递。示例如下:

       fun main() {

           ......

           //  test(isOpen) 直接这样调用,编译不通过


           // 可以是在空检查之内传递,证明自己非空


           isOpen?.apply { 

               test(this)

           }

           // 也可以是强制转成非空类型


           test(isOpen!!)

       }





       private fun test(open: Boolean) {

           ......

       }

    3.  
    在使用阶段,需要严格判空:

       var time: Long? = 1000

         //尽管你才赋值了非空的值,但在使用过程中,你无法这样:


         //time.toInt()

         //必须判空


         time?.toInt()

    总的来说 Kotlin 为了解决 NPE 做了大量语言层级的强限制,的确可以做到减少 NPE 的发生。但这种既契约式(判空)又防御式(声明空与非空)的方案会让开发者做更多的工作,会更麻烦一点。

    当然,Kotlin 为了减少麻烦,用 "?" 简化了判空逻辑 —— "?" 的实质还是判空,我们可以通过工具查看 time?.toInt() Java 等价代码是:

         if (time != null) {

            int var10000 = (int)time;

         }

    这种简化在数据层级很深需要写大量判空语句时会特别方便,这也是为什么虽然逻辑上 Kotlin 让开发者做了更多工作,但写代码过程中却并没有感觉到更麻烦。



    三、强规则之下的 NPE 问题



    Kotlin 这么严密的防御之下,NPE 问题是否已经被终结了呢?答案当然是否定的。 在实践过程中我们发现主要有以下几种容易导致 NPE 的场景:

    1. data class(含义对应
    Java
    中的 model)声明了非空

    例如从后端拿 json 数据的场景,后端的哪个字段可能会传空是客户端无法控制的,这种情况下我们的预期必须是每个字段都可能为空,这样转成 json object 时才不会有问题:

    data class User(

           var id: Long?,

           var gender: Long?,

           var avatar: String?)

    假如有一个字段忘了加上"?",后端没传该值就会抛出空指针异常。

    2. 过分依赖 Kotlin 的空值检查

    private lateinit var mUser: User



    ...



    private fun initView() {

       mUser = intent.getParcelableExtra<User>("key_user")

    }

    Kotlin 的体系中久了会过分依赖于
    Android Studio
    的空值检查,在代码提示中
    Intent
    getParcelableExtra 方法返回的是非空,因此这里你直接用方法结果赋值不会有任何警告。但点击进 getParcelableExtra 方法内部你会发现它的实现是这样的:

       public <T extends Parcelable> T getParcelableExtra(String name) {

           return mExtras == null ? null : mExtras.<T>getParcelable(name);

       }

    内部的其他代码不展开了,总之它是可能会返回 null 的,直接赋值显然会有问题。

    我理解这是 Kotlin 编译工具对 Java 代码检查的不足之处,它无法准确判断 Java 方法是否会返回空就选择无条件信任,即便方法本身可能还声明了 @Nullable

    3. 变量或形参声明为非空

    这点与第一、第二点都很类似,主要是使用过程中一定要进一步思考传递过来的值是否真的非空。

    有人可能会说,那我全部都声明为可空类型不就得了么 —— 这样做会让你在使用该变量的所有地方都需要判空,Kotlin 本身的便利性就荡然无存了。

    我的观点是不要因噎废食,使用时多注意点就可以避免大部分问题。

    4. !! 强行转为非空

    当将可空类型赋值给非空类型时,需要有对空类型的判断,确保非空才能赋值(Kotlin 的约束)。

    我们使用!! 可以很方便得将可空转为非空但可空变量值为 null,则会 crash

    因此使用上建议在确保非空时才用 !!:

       param!!

    否则还是尽量放在判空代码块里:

       param?.let {

          doSomething(it)

       }


    四、实践中碰到的问题



    Java 的空处理转到 Kotlin 的空处理,我们可能会下意识去寻找对标 Java 的判空写法:

       if (n != null) {

          //非空如何 

       } else {

          //为空又如何


       }

    Kotlin 中类似的写法的确有,那就是结合高阶函数 letapplyrun …… 来处理判空,比如上述 Java 代码就可以写成:

       n?.let {

          //非空如何


       } ?: let {

          //为空又如何


       }

    但这里有几个小坑。

    1. 两个代码块不是互斥关系

    假如是 Java 的写法,那么不管 n 的值怎样,两个代码块都是互斥的,也就是非黑即白。但 Kotlin 的这种写法不是(不确定这种写法是否是最佳实践,假如有更好的方案可以留言指出)。

    ?: 这个操作符可以理解为 if (a != null) a else b,也就是它之前的值非空返回之前的值,否则返回之后的值。

    假如用的是 let, 注意看它的返回值是指定 return 或函数里最后一行,那么碰到以下情况:

       val n = 1

       var a = 0

       n?.let {

          a++

          ...

          null  //最后一行为 null


       } ?: let {

          a++

       }

    你会很神奇地发现 a 的值是 2,也就是既执行了前一个代码块,也执行了后一个代码块

    上面这种写法你可能不以为然,因为很明显地提醒了诸位需要注意最后一行,但假如是之前没注意这个细节或者是下面这种写法呢?

       n?.let {

          ...

          anMap.put(key, value) // anMap 是一个 HashMap


       } ?: let {

          ...

       }

    应该很少人会注意到 Map put 方法是有返回值的,且可能会返回 null。那么这种情况下很容易踩坑。

    2. 两个代码块的对象不同

    let 为例,在 let 代码块里可以用 it 指代该对象(其他高阶函数可能用
    this
    ,类似的),那么我们在写如下代码时可能会顺手这样写:

       activity {

          n?.let {

             it.hashCode() // it n


          } ?: let {

             it.hashCode() // it activity


          }  

       }

    结果自然会发现值不一样。前一个代码块 it 指代的是 n,而后一个代码块里 it 指代的是整个代码块指向的
    this

    原因是 ?: let 之间是没有 . 的,也就是说后一个代码块调用 let 的对象并不是被判空的对象,而是 this。(不过这种场景会出错的概率不大,因为在后一个代码块里很多对象 n 的方法用不了,就会注意到问题了)



    后记



    总的来说切换到 Kotlin 还是比预期顺利和舒服,写惯了
    Kotlin
    后再回去写 Java 反倒有点不习惯。今天先写这点,后面有其他需要总结的再分享。


    收起阅读 »

    Activity启动流程

    Activity启动流程很多文章都已经说过了,这里说一下自己的理解。Activity启动流程分两种,一种是启动正在运行的app的Activity,即启动子Activity。如无特殊声明默认和启动该activity的activity处于同一进程。如果有声明在一个...
    继续阅读 »

    Activity启动流程很多文章都已经说过了,这里说一下自己的理解。

    Activity启动流程分两种,一种是启动正在运行的appActivity,即启动子Activity如无特殊声明默认和启动该activityactivity处于同一进程。如果有声明在一个新的进程中,则处于两个进程。另一种是打开新的app,即为Launcher启动新的Activity后边启动Activity的流程是一样的,区别是前边判断进程是否存在的那部分。

    Activity启动的前提是已经开机,各项进程和AMS等服务已经初始化完成,在这里也提一下那些内容。

    Activity启动之前

    init进程init是所有linux程序的起点,是Zygote的父进程。解析init.rc孵化出Zygote进程。

    Zygote进程Zygote是所有Java进程的父进程,所有的App进程都是由Zygote进程fork生成的。

    SystemServer进程System
    Server
    Zygote孵化的第一个进程。SystemServer负责启动和管理整个Java framework,包含AMSPMS等服务。

    LauncherZygote进程孵化的第一个App进程是Launcher

    1.init进程是什么?

    Android是基于linux系统的,手机开机之后,linux内核进行加载。加载完成之后会启动init进程。

    init
    进程会启动ServiceManager,孵化一些守护进程,并解析init.rc孵化Zygote进程。

    2.Zygote进程是什么?

    所有的App进程都是由Zygote进程fork生成的,包括SystemServer进程。Zygote初始化后,会注册一个等待接受消息的socketOS层会采用socket进行IPC通信。

    3.为什么是Zygote来孵化进程,而不是新建进程呢?

    每个应用程序都是运行在各自的Dalvik虚拟机中,应用程序每次运行都要重新初始化和启动虚拟机,这个过程会耗费很长时间。Zygote会把已经运行的虚拟机的代码和内存信息共享,起到一个预加载资源和类的作用,从而缩短启动时间。

    Activity启动阶段

    涉及到的概念

    进程Android系统为每个APP分配至少一个进程

    IPC:跨进程通信,Android中采用Binder机制。

    涉及到的类

    ActivityStackActivityAMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。

    ActivitySupervisor:管理 activity 任务栈

    ActivityThreadActivityThread
    运行在UI线程(主线程),App的真正入口。

    ApplicationThread:用来实现AMSActivityThread之间的交互。

    ApplicationThreadProxyApplicationThread
    在服务端的代理。AMS就是通过该代理与ActivityThread进行通信的。

    IActivityManager:继承与IInterface接口,抽象出跨进程通信需要实现的功能

    AMN:运行在server端(SystemServer进程)。实现了Binder类,具体功能由子类AMS实现。

    AMSAMN的子类,负责管理四大组件和进程,包括生命周期和状态切换。AMS因为要和ui交互,所以极其复杂,涉及window

    AMPAMSclient端代理(app进程)。了解Binder知识可以比较容易理解server端的stubclient端的proxyAMPAMS通过Binder通信。

    Instrumentation:仪表盘,负责调用ActivityApplication生命周期。测试用到这个类比较多。

    流程图

    这个图来源自网上,之前也看过很多类似讲流程的文章,但是大都是片段的。这个图是目前看到的最全的,自己去画一下也应该不会比这个全了,所以在这里直接引用一下,可以去浏览器上放大看。


    涉及到的进程

    ·      
    Launcher所在的进程

    ·      
    AMS所在的SystemServer进程

    ·      
    要启动的Activity所在的app进程

    如果是启动根Activity,就涉及上述三个进程。

    如果是启动子Activity,那么就只涉及AMS进程和app所在进程。

    具体流程

    1. LauncherLauncher通知AMS要启动activity

    ·      
    startActivitySafely->startActivity->Instrumentation.execStartActivity()(AMP.startActivity)->AMS.startActivity

    2. AMS:PMSresoveIntent验证要启动activity是否匹配。如果匹配,通过ApplicationThread发消息给Launcher所在的主线程,暂停当前Activity(Launcher)

    3. 暂停完,在该activity还不可见时,通知AMS,根据要启动的Activity配置ActivityStack。然后判断要启动的Activity进程是否存在?

    ·      
    存在:发送消息LAUNCH_ACTIVITY给需要启动的Activity主线程,执行handleLaunchActivity

    ·      
    不存在:通过socketzygote请求创建进程。进程启动后,ActivityThread.attach

    4. 判断Application是否存在,若不存在,通过LoadApk.makeApplication创建一个。在主线程中通过thread.attach方法来关联ApplicationThread

    5. 在通过ActivityStackSupervisor来获取当前需要显示的ActivityStack

    6. 继续通过ApplicationThread来发送消息给主线程的Handler来启动ActivityhandleLaunchActivity)。

    7. handleLauchActivity:调用了performLauchActivity,里边Instrumentation生成了新的activity对象,继续调用activity生命周期。

    IPC过程:

    双方都是通过对方的代理对象来进行通信。

    1.app
    AMS通信:app通过本进程的AMPAMS进行Binder通信

    2.AMS
    和新app通信:通过ApplicationThreadProxy来通信,并不直接和ActivityThread通信

    参考函数流程

    Activity启动流程(从Launcher开始):

    第一阶段: Launcher通知AMS要启动新的Activity(在Launcher所在的进程执行)

    ·      
    Launcher.startActivitySafely //首先Launcher发起启动Activity的请求

    ·      
    Activity.startActivity

    ·      
    Activity.startActivityForResult

    ·      
    Instrumentation.execStartActivity //交由Instrumentation代为发起请求

    ·      
    ActivityManager.getService().startActivity
    //
    通过IActivityManagerSingleton.get()得到一个AMP代理对象

    ·      
    ActivityManagerProxy.startActivity
    //
    通过AMP代理通知AMS启动activity

    第二阶段:AMS先校验一下Activity的正确性,如果正确的话,会暂存一下Activity的信息。然后,AMS会通知Launcher程序pause Activity(在AMS所在进程执行)

    ·      
    ActivityManagerService.startActivity

    ·      
    ActivityManagerService.startActivityAsUser

    ·      
    ActivityStackSupervisor.startActivityMayWait

    ·      
    ActivityStackSupervisor.startActivityLocked
    :检查有没有在AndroidManifest中注册

    ·      
    ActivityStackSupervisor.startActivityUncheckedLocked

    ·      
    ActivityStack.startActivityLocked :判断是否需要创建一个新的任务来启动Activity

    ·      
    ActivityStack.resumeTopActivityLocked :获取栈顶的activity,并通知Launcher应该pause掉这个Activity以便启动新的activity

    ·      
    ActivityStack.startPausingLocked

    ·      
    ApplicationThreadProxy.schedulePauseActivity

    第三阶段: pause LauncherActivity,并通知AMS已经paused(在Launcher所在进程执行)

    ·      
    ApplicationThread.schedulePauseActivity

    ·      
    ActivityThread.queueOrSendMessage

    ·      
    H.handleMessage

    ·      
    ActivityThread.handlePauseActivity

    ·      
    ActivityManagerProxy.activityPaused

    第四阶段:检查activity所在进程是否存在,如果存在,就直接通知这个进程,在该进程中启动Activity;不存在的话,会调用Process.start创建一个新进程(执行在AMS进程)

    ·      
    ActivityManagerService.activityPaused

    ·      
    ActivityStack.activityPaused

    ·      
    ActivityStack.completePauseLocked

    ·      
    ActivityStack.resumeTopActivityLocked

    ·      
    ActivityStack.startSpecificActivityLocked

    ·      
    ActivityManagerService.startProcessLocked

    ·      
    Process.start //在这里创建了新进程,新的进程会导入ActivityThread类,并执行它的main函数

    第五阶段: 创建ActivityThread实例,执行一些初始化操作,并绑定Application。如果Application不存在,会调用LoadedApk.makeApplication创建一个新的Application对象。之后进入Loop循环。(执行在新创建的app进程)

    ·      
    ActivityThread.main

    ·      
    ActivityThread.attach(false) //声明不是系统进程

    ·      
    ActivityManagerProxy.attachApplication

    第六阶段:处理新的应用进程发出的创建进程完成的通信请求,并通知新应用程序进程启动目标Activity组件(执行在AMS进程)

    ·      
    ActivityManagerService.attachApplication //AMS绑定本地ApplicationThread对象,后续通过ApplicationThreadProxy来通信。

    ·      
    ActivityManagerService.attachApplicationLocked

    ·      
    ActivityStack.realStartActivityLocked //真正要启动Activity了!

    ·      
    ApplicationThreadProxy.scheduleLaunchActivity
    //AMS
    通过ATP通知app进程启动Activity

    第七阶段: 加载MainActivity类,调用onCreate声明周期方法(执行在新启动的app进程)

    ·      
    ApplicationThread.scheduleLaunchActivity
    //ApplicationThread
    发消息给AT

    ·      
    ActivityThread.queueOrSendMessage

    ·      
    H.handleMessage //ATHandler来处理接收到的LAUNCH_ACTIVITY的消息

    ·      
    ActivityThread.handleLaunchActivity

    ·      
    ActivityThread.performLaunchActivity

    ·      
    Instrumentation.newActivity //调用Instrumentation类来新建一个Activity对象

    ·      
    Instrumentation.callActivityOnCreate

    ·      
    MainActivity.onCreate

    ·      
    ActivityThread.handleResumeActivity

    ·      
    AMP.activityResumed

    ·      
    AMS.activityResumed(AMS进程)

    参考文章

    http://gityuan.com/2016/03/12/start-activity/

    https://blog.csdn.net/luoshengyang/article/details/6689748

    收起阅读 »

    iOS 实例对象,类对象,元类对象

    OC对象的分类OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)实例对象:实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据...
    继续阅读 »

    OC对象的分类

    OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)

    • 实例对象:

      实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:
     NSObject *objc1 = [[NSObject alloc]init];
    NSObject *objc2 = [[NSObject alloc]init];

    NSLog(@"instance----%p %p",objc1,objc2);
    输出结果:



    instance实例对象存储的信息:
    1.isa指针

    2.其他成员变量


    我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址

    类对象

    类对象的获取
            Class Classobjc1 = [objc1 class];
    Class Classobjc2 = [objc2 class];
    Class Classobjc3 = object_getClass(objc1);
    Class Classobjc4 = object_getClass(objc2);
    Class Classobjc5 = [NSObject class];
    NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

    打印结果

    2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
    从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个

    类对象的作用

    类对象存储的信息:
    1.isa指针
    2.superclass指针
    3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)



    元类对象

    1.元类对象的获取

            Class metaObjc1 = object_getClass([NSObject class]);
    Class metaObjc2 = object_getClass(Classobjc1);
    Class metaObjc3 = object_getClass(Classobjc3);
    Class metaObjc4 = object_getClass(Classobjc5);

    打印指针地址

    NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
    2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
    2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
    2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

    获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个

    特别注意一点:

    Class objc = [[NSObject class] class];
    Class objcL = [[[NSObject class] class] class];

    无论class几次,它返回的始终是类对象

    2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140

    元类存储结构:
    元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:
    1.isa指针
    2.superclass指针
    3.类方法(即加号方法)



    从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空

    是否为元类 class_isMetaClass(objcL);

    收起阅读 »

    iOS开发 - 编译&链接

    对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
    继续阅读 »
    对于平常的应用程序开发,我们很少需要关注编译链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译链接的过程一步完成,通常将这种编译链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

    正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余。

    编译流程分析

    现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

    #include <stdio.h>
    int main(){
    printf("Hello World");
    return 0;
    }

    在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

    $ gcc hello.c
    $ ./a.out
    Hello World
    复制代码

    其实上述过程可以分解为四步:

    • 预处理(Prepressing)
    • 编译(Compilation)
    • 汇编(Assembly)
    • 链接(Linking)



    预编译

    首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

    $ gcc –E hello.c –o hello.i
    复制代码

    还可以下面的表达

    $ cpp hello.c > hello.i
    复制代码

    预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

    • 将所有的#define删除,并展开所有的宏定义
    • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
    • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
    • 删除所有的注释///**/
    • 添加行号和文件名标识,比如#2 “hello.c” 2。
    • 保留所有的#pragma编译器指令

    截图个大家看看效果

    经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

    编译(compliation)

    编译过程就是把预处理完的文件进行一系列的:词法分析语法分析语义分析优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

    $ gcc –S hello.i –o hello.s



    通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

    汇编(assembly)

    汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

    as hello.s –o hello.o
    复制代码

    或者

    gcc –c hello.s –o hello.o
    复制代码

    或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

    gcc –c hello.c –o hello.o
    复制代码

    链接(linking)

      链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

    注意默认情况没有gcc / 记得 :
    $ brew install gcc
    复制代码

    链接相应的库




    下面在贴出我们的写出的源代码是如何变成目标代码的流程图:




    主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

    到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看



    iOS的编译器

    iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

    LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。




    编译器前端的任务是进行:

    • 语法分析
    • 语义分析
    • 生成中间代码(intermediate representation )

    在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


    以上图解内容所做的是事情和gcc编译一模模一样样!

    iOS程序-详细编译过程

    • 1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
    • 2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
    • 3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
    • 4.链接文件:将项目中的多个可执行文件合并成一个文件;
    • 5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
    • 6.编译 storyboard 文件:storyboard 文件也是会被编译的;
    • 7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
    • 8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage
    • 9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
    • 10.生成 .app 包
    • 11.将 Swift 标准库拷贝到包中
    • 12.对包进行签名
    • 13.完成打包
    编译过程的确是个比较复杂的过程,还有链接!并不是说难就不需要掌握,我个人建议每一个进阶路上iOS开发人员,都是要了解一下的。不需要你多么牛逼,但是你能在平时的交流讨论,面试中能点出一个两个相应的点,我相信绝对是逼格满满!


    作者:Cooci
    原贴链接:https://juejin.cn/post/6844903841842872327


    收起阅读 »

    iOS开发 Fastlane 自动打包技术

    Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布AppGithub官网文档我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们...
    继续阅读 »

    Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App

    Github

    官网

    文档

    我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:

    • 一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。

    • 避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。

    • 通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect。

    • 证书的同步更新,管理,在新电脑能够迅速具备项目打包环境

    如果你也有上述需求,那我相信Fastlane是一个好的选择。

    那既然说Fastlane是一套工具的集合,那认识并了解其中的工具的作用是必不可少的环节。按照功能属性Fastlane对工具进行了如下分类(链接至官网详细介绍):

    Testing 测试相关

    Building 打包

    Screenshots 截图

    Project 项目配置

    Code Signing 代码签名

    Documentation 文档

    Beta 内测相关

    Push 推送

    Releasing your app 发布

    Source Control Git工作流

    Notifications 通知相关

    Misc 其他的杂七杂八

    分类下对应的就是具体的每一个工具的介绍,在这里每一个工具Fastlane叫做action,下文我们也统一叫action。这里我会列举一些我认为常用的action,其他的大家可以去官网看下

    gym:是fastlane提供的打包工具

    snapshot: 生成多个设备的截图文件

    frameit :对截图加一层物理边框

    increment_build_number:自增build number 然后与之对应的get_build_number。Version number同理。

    cert:创建一个新的代码签名证书

    sigh:生成一个provisioning profile并保存打当前文件

    pem:确保当前的推送证书是活跃的,如果没有会帮你生成一个新的

    match:在团队中同步证书和描述文件。(这是一种管理证书的全新方式,需要重点关注下)

    testflight:上传ipa到testflight

    deliver:上传ipa到AppStore

    当然官网里面其实是有很多可以划等号的Action,大家在看的时候注意下。Actions官网关于Action的介绍

    多说无益,开始上手

    当前最新版本是2.8.4

    一、安装xcode命令行工具

    xcode-select --install,如果没有安装,会弹出对话框,点击安装。如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装

    二、安装Fastlane

    sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的

    安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。

    三、初始化Fastlane

    cd到你的项目目录执行 

    fastlane init


    这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。

    如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。



    接着会提示你输入开发者账号和密码。

    [20:48:55]: Please enter your Apple ID developer credentials

    [20:48:55]: Apple ID Username:

    登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

    如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。


    四、文件系统

    初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。


    其中metadata和screenshots分别对应App元数据和商店应用截图。

    Appfile主要存放App的apple_id team_id app_identifier等信息

    Deliverfile中为发布的配置信息,一般情况用不到。

    Fastfile是我们最应该关注的文件,也是我们的工作文件。

    Fastfile



    之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。

    打包到蒲公英

    这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。

    蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。

    打开终端输入fastlane add_plugin pgyer


    新建一个lane


    desc "打包到pgy"


    lane :test do |options|
    gym(
    clean:true, #打包前clean项目
    export_method: "ad-hoc", #导出方式
    scheme:"shangshaban", #scheme
    configuration: "Debug",#环境
    output_directory:"./app",#ipa的存放目录
    output_name:get_build_number()#输出ipa的文件名为当前的build号
    )
    #蒲公英的配置 替换为自己的api_key和user_key
    pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
    end

    这样一个打包到蒲公英的lane就完成了。

    option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台

    执行

    在工作目录的终端执行

    fastlane test desc:测试打包



    然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置

    其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解。

    match

    开头已经说了,match是一种全新的证书同步管理机制,也是我认为在fastlane中相对重要的一环,介于篇幅这篇就不细说了,有兴趣的可以去官网看下,有机会我也会更新一篇关于match的文章。match

    其他的一些小提示

    1. 可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来

    2. 如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。

    3. 如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。

    4. 如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据skip_screenshots:true,#不上传屏幕截图

    关于fastlane的一些想法

    其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!



    作者:Cooci
    原贴链接:https://www.jianshu.com/p/59725c52e0fa





    收起阅读 »