java 设计模式:工厂方法模式

前言简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。优点:用户只需要知道具...
继续阅读 »

前言

简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
  • 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
  • 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。

缺点:

  • 类的个数容易过多,增加复杂度
  • 增加了系统的抽象性和理解难度
  • 抽象产品只能生产一种产品,此弊端可使用抽象工厂模式解决。

应用场景:

  • 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
  • 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
  • 客户不关心创建产品的细节,只关心产品的品牌

代码实现

工厂方法模式的主要角色如下。

  1. 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  2. 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  3. 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
  4. 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。


    在这里插入图片描述
    在这里插入图片描述

    kotlin代码实现

//抽象产品:提供了产品的接口
interface IProduct{
fun setPingPai(string: String)
fun showName() :String
}
//具体产品1:实现抽象产品中的抽象方法
class Dog : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}

override fun showName() = "dog"

}
//具体产品2:实现抽象产品中的抽象方法
class Cat : IProduct{
var pinPai:String? = null
override fun setPingPai(string: String) {
this.pinPai = string
}
override fun showName() = "cat"
}
//抽象工厂:提供了厂品的生成方法
interface IFactory{
fun getPinPai():String
fun createProduct(type:Int):IProduct
}
//具体工厂1:实现了厂品的生成方法
class ABCFactory():IFactory{
override fun getPinPai() = "ABC"

override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}

}
//具体工厂2:实现了厂品的生成方法
class CBDFactory():IFactory{
override fun getPinPai() = "CBD"
override fun createProduct(type: Int): IProduct {
return when(type){
1-> Dog().apply { setPingPai(getPinPai()) }
2-> Cat().apply { setPingPai(getPinPai()) }
else -> throw NullPointerException()
}
}

}

Android源码分析

ThreadFactory

在这里插入图片描述
在这里插入图片描述
//抽象产品
public interface Runnable {
public abstract void run();
}

//抽象工厂
public interface ThreadFactory {
Thread newThread(Runnable r);
}

具体的实现

//实现1 TaskThreadFactory
class TaskThreadFactory(var name: String) : ThreadFactory {
private val mThreadNumber = AtomicInteger(1)
override fun newThread(r: Runnable): Thread {
return Thread(r, name + "#" + mThreadNumber.getAndIncrement())
}
}
//实现2 DiskLruCacheThreadFactory
private static final class DiskLruCacheThreadFactory implements ThreadFactory {
@Override
public synchronized Thread newThread(Runnable runnable) {
Thread result = new Thread(runnable, "glide-disk-lru-cache-thread");
result.setPriority(Thread.MIN_PRIORITY);
return result;
}
}

解释:

参数Runnable r,我们可以创建很多此类线程的产品类,我们还可以创建工厂来创造某类专用线程

收起阅读 »

iOS-异步绘制原理

在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 c...
继续阅读 »

在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 contents 属性,完成显示。

这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。

那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?

答案是可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础。

UIView 调用 setNeedsDisplay 方法其实是调用其 layer 属性的同名方法,这时 layer 并不会立刻调用 display 方法,而是要等到当前 runloop 即将结束的时候调用 display,进入到绘制流程。在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并没有实现 displayLayer: 方法,所以进入系统的绘制流程,我们可以通过实现 displayLayer: 方法来进行异步绘制。

有了上面的异步绘制原理流程图,我们可以得到一个实现异步绘制的初步思路:
在“异步绘制入口”去开辟子线程,然后在子线程中实现和系统类似的绘制流程。

二、系统绘制流程

要实现异步绘制,我们首先要了解系统的绘制流程,看下面一张流程图:


三、异步绘制流程

我们看一幅时序图


#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>

@implementation AsyncDrawLabel

- (void)setText:(NSString *)text {
_text = text;
[self.layer setNeedsDisplay];
}

- (void)setFont:(UIFont *)font {
_font = font;
[self.layer setNeedsDisplay];
}

- (void)displayLayer:(CALayer *)layer {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
});
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)image.CGImage;
});
});
}

- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));

NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}

@end

AsyncDrawLabel 是一个继承 UIView 的类,其 Label 的文本绘制功能需要我们自己实现。

我们在 - (void)displayLayer:(CALayer *)layer 方法中异步在全局队列中创建上下文环境然后使用 - (void)draw:(CGContextRef)context size:(CGSize)size 方法进行文本的简单绘制,再回到主线程为 self.layer.contents 赋值。从而完成了一个简单的异步绘制。

当然这样的绘制的问题是,如果绘制数量较多,绘制频繁,会阻塞全局队列,因为全局队列中还有一些系统提交的任务需要执行,可能会对其造成影响。

YYAsyncLayer

我们需要更加优化的方式去管理异步绘制的线程和执行流程,使用 YYAsyncLayer 可以让我们把注意力放在具体的绘制(需要我们做的是上面代码中 - draw: size: 做的事情),而不需要考虑线程的管理,绘制的时机等,大大提高绘制的效率以及我们编程的速度。

YYAsyncLayer 的主要流程如下

在主线程的 RunLoop 中注册一个 observer,它的优先级要比系统的 CATransaction 低,保证系统先做完必须的工作。

把需要异步绘制的操作集中起来。比如设置字体、颜色、背景色等,不是设置一个就绘制一个,而是把它们集中起来,RunLoop 会在 observer 需要的时机通知统一处理。

处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给 layer.contents。

流程图如下:


使用 YYAsyncLayer 的代码:

#import "AsyncDrawLabel.h"
#import <YYAsyncLayer.h>
#import <CoreText/CoreText.h>

@interface AsyncDrawLabel ()<YYAsyncLayerDelegate>

@end

@implementation AsyncDrawLabel

+ (Class)layerClass {
return YYAsyncLayer.class;
}

- (void)setText:(NSString *)text {
_text = text.copy;
[self commitTransaction];
}

- (void)setFont:(UIFont *)font {
_font = font;
[self commitTransaction];
}

- (void)layoutSubviews {
[super layoutSubviews];
[self commitTransaction];
}

- (void)contentsNeedUpdated {
[self.layer setNeedsDisplay];
}

- (void)commitTransaction {
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

// 在这里创建异步绘制的任务
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer * _Nonnull layer) {

};
task.display = ^(CGContextRef _Nonnull context, CGSize size, BOOL (^ _Nonnull isCancelled)(void)) {
if (isCancelled() || self.text.length == 0) {
return;
}
// 在这里进行异步绘制
[self draw:context size:size];
};
task.didDisplay = ^(CALayer * _Nonnull layer, BOOL finished) {
if (finished) {

} else {

}
};
return task;
}

- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));

NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end

原文链接:https://blog.csdn.net/wywinstonwy/article/details/105660643

收起阅读 »

iOS-视图&图像相关

Auto Layout 原理Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。所谓约束,通常是定义了两个视图之...
继续阅读 »

Auto Layout 原理

Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。

所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果

UIView之drawRect: & layoutSubviews的作用和机制

drawRect 调用机制

1、调用时机:loadView ->ViewDidload ->drawRect:

2、如果在UIView初始化时没有设置rect大小,将直接导致drawRect:不被自动调用。

3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:

4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是:view当前的rect不能为nil

5、该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。

这里简单说一下sizeToFit和sizeThatFit:
sizeToFit:会计算出最优的 size 而且会改变自己的size
sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size

注意事项
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取到一个invalidate的ref保存下来,在drawRect中并不能用于画图。等到在这里调用时,可能当前上下文环境已经变化。
2、若使用CALayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕。
4、UIImageView继承自UIView,但是UIImageView能不重写drawRect方法用于实现自定义绘图。具体原因如下:
Apple在文档中指出:UIImageView是专门为显示图片做的控件,用了最优显示技术,是不让调用darwrect方法, 要调用这个方法,只能从uiview里重写。

layoutSubviews

这个方法是用来对subviews重新布局,默认没有做任何事情,需要子类进行重写。
当我们在某个类的内部调整子视图位置时,需要调用。

反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。

①、- (void)layoutSubviews;
对subview重新布局
②、- (void)setNeedsLayout;
将视图标记为需要重新布局, 这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
③、- (void)layoutIfNeeded;
如果有需要刷新的标记立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]

这里有必要描述下三者之间的关系:
在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。
setNeedLayout就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。
layoutIfNeed就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
layoutSubviews则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed,告诉系统是否立即执行重新布局的操作。

layoutSubviews调用时机

结论是经过搜索得到的,基于此笔者进行了验证,并得到了些结果:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。(当然这里frame为0,是不会调用的,同上面的drawrect:一样)
3、设置view的Frame会触发layoutSubviews,(当然前提是frame的值设置前后发生了变化。)
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转屏幕会触发父UIView上的layoutSubviews事件。(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview中做相应的操作)
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。(Apple是不建议这么做的)
这里需要补充一点:layoutSubview是布局相关,而drawRect则是负责绘制。因此从调用时序上来讲,layoutSubviews要早于drawRect:函数。

摘自链接:https://www.jianshu.com/p/2ab322e1c7d4
收起阅读 »

iOS底层系列:Category

前言Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。我们在Category中,可以直接添...
继续阅读 »

前言

Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。
我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。
我们在Category中,可以直接添加方法,而且我们也都知道,添加的方法会合并到本类当中,同时我们也可以声明属性,但是此时的属性没有功能,也就是不能存值,这就类似于Swift中的计算属性,如果我们想让这个属性可以储存值,就要用runtime的方式,动态的添加。

探究

1. Category为什么能添加方法不能添加成员变量
首先我们先创建一个Person类,然后创建一个Person+Run的Category,并在Person+Run中实现-run方法。
我们可以使用命令行对Person+Run.m进行编译

xcrun -sdk iphonesimulator clang -rewrite-objc Person+Run.m

得到一个Person+Run.cpp文件,在文件的底部,可以找到这样一个结构体

struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};

这些字段几乎都是见名知意了。
每一个Category都会编译然后存储在一个_category_t类型的变量中

static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
0,
0,
0,
};

因为我们的Person+Run里面只有一个实例方法,从上述代码中来看,也只有对应的位置传值了。
通过这个_category_t的结构结构我们也可以看出,属性存储在_prop_list_t,这里并没有类中的objc_ivar_list结构体,所以Category的_category_t结构体中根本没有储存ivar的地方,所以不能添加成员变量。
如果我们在分类中手动为成员变量添加了set和get方法之后,也可以调用,但实际上是没有内存来储值的,这就好像Swift中的计算属性,只起到了计算的作用,就相当于是两个方法(set和get),但是并不能拥有真用的内存来存储值。
举个例子

@property (copy, nonatomic) NSString * name;

下面这个声明如果实在类中,系统会默认帮我们声明一个成员变量_name, 在.h中声明setName和name两个方法,并提供setName和name方法的默认实现。
如果是在Category中,只相当于声明setName和name两个方法,没有实现也没有_name。

2. Category的方法是何时合并到类中的
大家都知道Category分类肯定是我们的应用启动是,通过运行时特性加载的,但是这个加载过程具体的细节就要结合runtime的源码来分析了。
runtime源码太多了,我们先通过大概浏览代码来定位实现功能的相关位置。
我从objc-runtime-new.mm中找到了下面这个方法。

void attachLists(List* const * addedLists, uint32_t addedCount)

而且他的注释一些的很清楚,修复类的方法,协议和变量列表,关联还未关联的分类。
然后我们继续找,就找到了我们需要的这个方法。

void attachLists(List* const * addedLists, uint32_t addedCount)

我们从其中摘出一段代码来分析就可以解决我们的问题了。

// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));

在调用此方法之前我们所有的分类会被方法一个list里面(每一个分类都是一个元素),然后再调用attachLists方法,我们可以看到,在realloc的时候传进一个newCount,这是因为要增加分类中的方法,所以需要对之前的数组扩容,在扩容结束后先调用了memmove方法,在调用memcopy,大家可以上网查一下这两个方法具体的区别,这里简单一说,其实完成的效果都是把后面的内存的内容拷贝到前面内存中去,但是memmove可以处理内存重叠的问题。
其实也就是首先将原来数组中的每个元素先往后移动(我们要添加几个元素,就移动几位),因为移动后的位置,其实也是数组自己的内存空间,所以存在重叠问题,直接移动会导致元素丢失的问题,所以用memmove(会检测是否有内存重叠)。
移动完之后,把我们储存分类中方法的list中的元素移动到数组前面位置。
过程就是这样子了,其实我们第三个问题就顺便解决完了。

3. Category方法和类中方法的执行顺序
上面其实说到了,类中原来的方法是要往后面移动的,分类的方法添加到前面的位置,而且调用方法的时候是在list中遍历查找,所以我们调用方法的时候,肯定会先调用到Category中的方法,但是这并不是覆盖,因为我们的原方法还在,只是这中机制保证了如果分类中有重写类的方法,会被优先查找到。

4. +load和+initialize的区别
对于这个问题我们从两个角度出发分析,调用方式调用时刻

+load
简单的举一个例子,我们创建一个Person类,然后重写+load方法,然后为Person新建两个Category,都分别实现+load。

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

@implementation Person (Test1)
+ (void)load {
NSLog(@"Person Test1 - load");
}
@end

@implementation Person (Test2)
+ (void)load {
NSLog(@"Person Test2 - load");
}
@end

当我们进行项目的时候,会得到下面的打印结果。

2020-09-14 09:34:41.900161+0800 Category[4533:53426] Person - load
2020-09-14 09:34:41.900629+0800 Category[4533:53426] Person Test1 - load
2020-09-14 09:34:41.900700+0800 Category[4533:53426] Person Test2 - load

我们并没有使用这个Person类和他的Category,所以应该是项目运行后,runtime在加载类和分类的时候,就会调用+load方法。
我们从源码中找到下面这个方法
void load_images(const char *path __unused, const struct mach_header *mh)
方法中的最后一行调用了call_load_methods(),这个call_load_methods()中就是实现了+load的调用方式。
下面是call_load_methods()函数的实现 ,大家简单浏览一遍

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;
}

从源码中很清楚的可以看到, 先调用call_class_loads(), 再调用call_category_loads(),这就说明了在调用所有的+load方法时,实现调用了所有类的+load方法,再去调用分类中的+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, @selector(load));
}

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

从中间的循环中可以看出,是取到了每个类的+load函数的指针,直接通过指针调用了这个函数。 call_category_loads()函数中体现出来的Category的+load方法的调用,也是同理。
同时这也解答了我们的另一个疑惑,那就是为什么总是先调用类的+load,在调用Category的+load。
思考:如果存在继承的情况,+load又会是怎样的调用顺序呢?
从上面call_class_loads()函数中可以看到有一个list:loadable_classes,我们猜测这里面应该就是存放着我们所有的类,因为下面的循环是从0开始循环,所以我们要研究所有的类的+load方法的执行顺序,就要看这个list中的类的顺序是怎么样的。
我们从个源码中可以找到这样一个方法,prepare_load_methods,在其实现中调用了schedule_class_load方法,我们看一下schedule_class_load的源码

static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize

if (cls->data()->flags & RW_LOADED) return;

// Ensure superclass-first ordering
schedule_class_load(cls->superclass);

add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}

从源码中schedule_class_load(cls->superclass);这一句中可以看出,递归调用自己本身,并且传入自己的父类,结果递归之后,才调用add_class_to_loadable_list,这就说明父类总是在子类前面加入到list当中,所有在调用一个类的+load方法之前,肯定要先调用其父类的+load方法。
那如果是其他没有继承关系的类呢,这就跟编译顺序有关系了,大家可以自己尝试验证一下。
小结:

  • +load方法会在runtime加载类和分类时调用

  • 每个类和分类的+load方法之后调用一次

  • 调用顺序:先调用类的+load

+initialize
+initialize的调用是不同的,如果某一个类我们没有使用过,他的+initialize方法是不会调用的,到我们使用这个类(调用了类的某个方法)的时候,才会触发+initialize方法的调用。

@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end

@implementation Person (Test1)
+ (void)initialize {
NSLog(@"Person Test1 - initialize");
}
@end

@implementation Person (Test2)
+ (void)initialize {
NSLog(@"Person Test2 - initialize");
}
@end

当我们执行[Person alloc];的时候,才会走+initialize方法,而且执行的Category中的+initialize:

2020-09-14 10:40:23.579623+0800 Category[9134:94173] Person Test2 - initialize

这个我们之前已经说过了,Category的方法会添加list的前面,所以会先被找到并且执行,所以我们猜测+initialize的执行是走的正常的消息机制,objc_msgSend。

由于objc_msgSend实现并没有完全开源,都是汇编代码,所以我们需要换一个思路来研究源码。

objc_msgSend本质是什么?以调用实例方法为例,其实就是通过isa指针找到该类,然后寻找方法,找到之后调用。如果没有找到则通过superClass找到父类,继续查找方法。上面的例子中,我们仅仅是调用了一个alloc方法,但是也执行了+initialize方法,所以我们猜测+initialize会在查找方法的时候调用到。通过这个思路,我们定位到了class_getInstanceMethod()函数(class_getInstanceMethod函数就是在类中查找某个sel时候调用的),在这个函数中,又调用了IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

在该函数中我们可以找到下面这段代码

if ((behavior & LOOKUP_INITIALIZE)  &&  !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}

可以看出如果类还没有执行+initialize 就会先执行,我们再看一下if语句中的initializeNonMetaClass函数,他会先拿到superClass,执行superClass的+initialize

supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}

这就是存在继承的情况,为什么会先执行父类的+initialize。

大总结

  1. 调用方式:
    load是根据函数地址直接调用
    initialize是通过消息机制objc_msgSend调用

  2. 调用时刻:

    load是在runtime加载类和分类时调用(只会调用一次)
    initialize是在类第一次收到消息时调用个,默认没有继承的情况下每个类只会initialize一次(父类的initialize可能会被执行多次)

  3. 调用顺序

    load
    先调用类的load:先编译的类先调用,子类调用之前,先调用父类的
    在调用Category的load:先编译的先调用

    initialize
    先初始化父类
    在初始化子类(初始化子类可能调用父类的initialize)

补充
上面总结的时候说到父类的initialize会被执行多次,什么情况下会被执行多次,为什么?举个例子:

@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end

@implementation Student
@end

Student类继承Person类,并且只有父类Person中实现了+initialize,Student类中并没有实现
此时我们调用[Student alloc];, 会得到如下的打印。

2020-09-14 11:31:55.377569+0800 Category[11483:125034] Person - initialize
2020-09-14 11:31:55.377659+0800 Category[11483:125034] Person - initialize

Person的+initialize被执行了两次,但这两个意义是不同的,第一次执行是因为在调用子类的+initialize方法之前必须先执行父类的了+initialize,所以会打印一次。当要执行子类的+initialize时,通过消息机制,student类中并没有找到实现的+initialize的实现,所以要通过superClass指针去到父类中继续查找,因为父类中实现了+initialize,所以才会有了第二次的打印。

结尾
本文的篇幅略长,笔者按照自己的思路和想法写完了此文,陈述过程不一定那么调理和完善,大家在阅读过程中发现问题,可以留言交流。
感谢阅读。

转自:https://www.jianshu.com/p/141b04e376d4

收起阅读 »

复杂场景下的h5与小程序通信

复杂场景下的h5与小程序通信一、背景在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。但在套壳小程序中,h5与...
继续阅读 »

复杂场景下的h5与小程序通信

一、背景

在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
  • 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
  • 小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。
  • 一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。

二、在业务内的实践

  • 因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:

export function injectMiniAppScript() {
if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
const s = document.createElement('script');

s.src = 'https://appx/web-view.min.js';
s.onload = () => {
// 加载完成时触发自定义事件
const customEvent = new CustomEvent('myLoad', { detail:'' });
document.dispatchEvent(customEvent);
};

s.onerror = (e) => {
// 加载失败时上传日志
uploadLog({
tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
});
};

document.body.insertBefore(s, document.body.firstChild);
}
}

加载脚本完成后,我们就可以调用my.postMessagemy.onMessage进行通信(统一约定h5发送消息给小程序时,必须带action,小程序根据action处理业务逻辑,同时小程序处理完成的结果必须带type,h5在不同的业务场景下通过my.onMessage处理不同type的响应),比如典型的,h5调用小程序签到:
h5部分代码如下:

// 处理扫脸签到逻辑
const faceVerify = (): Promise => {

return new Promise((resolve) => {
const handle = () => {
window.my.onMessage = (result: AlipaySignResult) => {
if (result.type === 'FACE_VERIFY_TIMEOUT' ||
result.type === 'DO_SIGN' ||
result.type === 'FACE_VERIFY' ||
result.type === 'LOCATION' ||
result.type === 'LOCATION_UNBELIEVABLE' ||
result.type === 'NOT_IN_ALIPAY') {
resolve(result);
}
};

window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
};

if (window.my) {
handle();
} else {
// 先记录错误日志
sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
// 监听load事件
document.addEventListener('myLoad', handle);
}
});
};

实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus,先来看看怎么用,还是上面的场景

// 处理扫脸签到逻辑
const faceVerify = (): Promise => {
miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
return miniAppBus.subscribeAsync([
'FACE_VERIFY_TIMEOUT',
'DO_SIGN',
'FACE_VERIFY',
'LOCATION',
'LOCATION_UNBELIEVABLE',
'NOT_IN_ALIPAY',
])
};
  • 可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。

三、实现及背后的思考

  • 为了满足不同场景和使用的方便,公开暴露的interface如下:

interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}

subscribe:函数接收两个参数,
type:需要订阅的type,可以是字符串,也可以是数组。
callback:回调函数。
subscribeAsync:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。
unsubscribe:取消订阅。
postMessage:postMessage替代,无需关注环境变量。

完整代码:

import { injectMiniAppScript } from './tools';

/**
* @description 小程序返回结果
* @export
* @interface MiniAppMessage
*/

interface MiniAppMessageBase {
type: string;
}

type MiniAppMessage = MiniAppMessageBase & {
[P in keyof T]: T[P]
}
/**
* @description 小程序接收消息
* @export
* @interface MessageToMiniApp
*/
export interface MessageToMiniApp {
action: string;
[x: string]: unknown
}

interface MiniAppMessageSubscriber {
(params: MiniAppMessage): void
}
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}
class MiniAppEventBus implements MiniAppEventBus{

/**
* @description: 监听函数
* @type {Map}
* @memberof MiniAppEventBus
*/
listeners: Map;
constructor() {
this.listeners = new Map>>();
this.init();
}

/**
* @description 初始化
* @private
* @memberof MiniAppEventBus
*/
private init() {
if (!window.my) {
// 引入脚本
injectMiniAppScript();
}

this.startListen();
}

/**
* @description 保证my变量存在的时候执行函数func
* @private
* @param {Function} func
* @returns
* @memberof MiniAppEventBus
*/
private async ensureEnv(func: Function) {
return new Promise((resolve) => {
const promiseResolve = () => {
resolve(func.call(this));
};

// 全局变量
if (window.my) {
promiseResolve();
}

document.addEventListener('myLoad', promiseResolve);
});
}

/**
* @description 监听小程序消息
* @private
* @memberof MiniAppEventBus
*/
private listen() {
window.my.onMessage = (msg: MiniAppMessage) => {
this.dispatch(msg.type, msg);
};
}

private async startListen() {
return this.ensureEnv(this.listen);
}

/**
* @description 发送消息,必须包含action
* @param {MessageToMiniApp} msg
* @returns
* @memberof MiniAppEventBus
*/
public postMessage(msg: MessageToMiniApp) {
return new Promise((resolve) => {
const realPost = () => {
resolve(window.my.postMessage(msg));
};

resolve(this.ensureEnv(realPost));
});
}

/**
* @description 订阅消息,支持单个或多个
* @template T
* @param {(string|string[])} type
* @param {MiniAppMessageSubscriber} callback
* @returns
* @memberof MiniAppEventBus
*/
public subscribe(type: string | string[], callback: MiniAppMessageSubscriber) {
const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber) => {
let listeners = this.listeners.get(type) || [];

listeners.push(cb);
this.listeners.set(type, listeners);
};

this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
}

private forEach(type:string | string[],cb:(type:string)=>void){
if (typeof type === 'string') {
return cb(type);
}

for (const key in type) {
if (Object.prototype.hasOwnProperty.call(type, key)) {
const element = type[key];

cb(element);
}
}
}

/**
* @description 异步订阅
* @template T
* @param {(string|string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
public async subscribeAsync(type: string | string[]): Promise> {
return new Promise((resolve, _reject) => {
this.subscribe(type, resolve);
});
}

/**
* @description 触发事件
* @param {string} type
* @param {MiniAppMessage} msg
* @memberof MiniAppEventBus
*/
public async dispatch(type: string, msg: MiniAppMessage) {
let listeners = this.listeners.get(type) || [];

listeners.map(i => {
if (typeof i === 'function') {
i(msg);
}
});
}

public async unSubscribe(type:string | string[]){
const unsubscribeSingle = (type: string) => {
this.listeners.set(type, []);
};

this.forEach(type,(type:string)=>unsubscribeSingle(type));
}
}

export default new MiniAppEventBus();
  • class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。

四、小程序内部的处理

  • 定义action handle,通过策略模式解耦:

const actionHandles = {
async FACE_VERIFY(){},
async GET_STEP(){},
async UPLOAD_HASH(){},
async GET_AUTH_CODE(){},
...// 其他action
}
....
// 在webview的消息监听函数中
async startProcess(e) {
const data = e.detail;
// 根据不同的action调用不同的handle处理
const handle = actionHandles[data.action];
if (handle) {

return actionHandles[data.action](this, data)
}
return uploadLogsExtend({
tip: STRING_CONTANT.UNKNOWN_ACTIONS,
data
})
}
  • 使用起来也是得心顺畅,舒服。

其他

类型完备,使用时智能提示,方便快捷。

原文链接:https://segmentfault.com/a/1190000023360940

收起阅读 »

java 设计模式:简单工厂

工厂模式的定义定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。简单工厂如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。简单工厂通常为静态方法,因此...
继续阅读 »

工厂模式的定义

定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。

简单工厂

如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。

简单工厂通常为静态方法,因此又叫静态工厂方法模式

优点:

  • 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
  • 客户端无需知道所创建具体产品的类名,只需知道参数即可。
    也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。

缺点:

  • 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则。
  • 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
  • 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
  • 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。

应用场景

对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。

代码实现

简单工厂模式的主要角色如下:

  • 简单工厂:是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
  • 抽象产品:是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
  • 具体产品:是简单工厂模式的创建目标。

其结构图如下图所示。


kotlin代码实现

interface IProduct{
fun showName() :String
}

class Dog : IProduct{
override fun showName() = "dog"

}

class Cat : IProduct{
override fun showName() = "cat"
}

object AnimalFactory{
fun createAnimal(type:Int):IProduct{
return when(type){
1-> Dog()
2-> Cat()
else -> throw NullPointerException()
}
}
}

简单工厂模式在Android中的实际应用

fragment 的构建

有时候,为了简化简单工厂模式,我们可以将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中。Fragment的创建使用简单工厂方法没有抽象产品类,所以工厂类放到了实现产品类中。

class ListWorkFragment : BMvpFragment<ListWorkView ,ListWorkPresenter>(),ListWorkView,ISubjectView{
companion object {
@JvmStatic
fun newInstance(recommendTypeId: Int,
termCode: String = "") =
ListWorkFragment().apply {
arguments = Bundle().apply {
putInt("type", recommendTypeId)
putString("code", termCode)
}
}
}

优点

  1. 在创建Fragment的时候,可以不需要管内部参数,而从外部输入
  2. Fragment推荐使用setArguments来传递参数,避免在横竖屏切换的时候Fragment自动调用自己的无参构造函数,导致数据丢失。

Bitmap源码分析

@UnsupportedAppUsage(maxTargetSdk = 28)
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets) {
this(nativeBitmap, width, height, density, requestPremultiplied, ninePatchChunk,
ninePatchInsets, true);
}

// called from JNI and Bitmap_Delegate.
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc) {
...
}

看构造函数可知,无法new出bitmap,那么怎么创建bitmap对象呢?

 BitmapFactory.decodeFile("")

内部源码

public static Bitmap decodeFile(String pathName) {
return decodeFile(pathName, null);
}

public static Bitmap decodeFile(String pathName, Options opts) {
Bitmap bm = null;
InputStream stream = null;
try {
stream = new FileInputStream(pathName);
bm = decodeStream(stream, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
*/

Log.e("BitmapFactory", "Unable to decode stream: " + e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// do nothing here
}
}
}
return bm;
}

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
// we don't throw in this case, thus allowing the caller to only check
// the cache, and not force the image to be decoded.
if (is == null) {
return null;
}

Bitmap bm = null;

Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}

if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}

setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}

return bm;
}

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);

/**
* Set the newly decoded bitmap's density based on the Options.
*/

private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;

final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}

byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}

看下BitmapFactory的注释我们可以看到,这个工厂支持从不同的资源创建Bitmap对象,包括files, streams, 和byte-arrays,但是调用关系都大同小异。

收起阅读 »

小程序自动化测试

背景近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小...
继续阅读 »

背景

近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。


上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。

自动化 SDK

如何将操作路径还原这个问题,首选官方提供的 SDK: miniprogram-automator

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:

  • 控制小程序跳转到指定页面
  • 获取小程序页面数据
  • 获取小程序页面元素状态
  • 触发小程序元素绑定事件
  • 往 AppService 注入代码片段
  • 调用 wx 对象上任意接口
  • ...

上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档,当然如果之前用过 puppeteer ,也可以快速上手,api 基本一致。下面简单介绍下 SDK 的使用方式。

// 引入sdk
const automator = require('miniprogram-automator')

// 启动微信开发者工具
automator.launch({
// 微信开发者工具安装路径下的 cli 工具
// Windows下为安装路径下的 cli.bat
// MacOS下为安装路径下的 cli
cliPath: 'path/to/cli',
// 项目地址,即要运行的小程序的路径
projectPath: 'path/to/project',
}).then(async miniProgram => { // miniProgram 为 IDE 启动后的实例
// 启动小程序里的 index 页面
const page = await miniProgram.reLaunch('/page/index/index')
// 等待 500 ms
await page.waitFor(500)
// 获取页面元素
const element = await page.$('.main-btn')
// 点击元素
await element.tap()
// 关闭 IDE
await miniProgram.close()
})

有个地方需要提醒一下:使用 SDK 之前需要开启开发者工具的服务端口,要不然会启动失败。




捕获用户行为

有了还原操作路径的办法,接下来就要解决记录操作路径的难题了。

在小程序中,并不能像 web 中通过事件冒泡的方式在 window 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 Page 、Component 方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 event 对象,以此来捕获所有的事件。

// 暂存原生方法
const originPage = Page
const originComponent = Component

// 改写 Page
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
originPage(params)
}
// 改写 Component
Component = (params) => {
if (params.methods) {
const { methods } = params
const names = Object.keys(methods)
for (const name of names) {
// 进行方法拦截
if (typeof methods[name] === 'function') {
methods[name] = hookMethod(name, methods[name], true)
}
}
}
originComponent(params)
}

const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (evt && evt.target && evt.type) {
// 记录用户行为
}
return method.apply(this, args)
}
}

这里的代码只是代理了所有的事件方法,并不能用来还原用户的行为,要还原用户行为还必须知道该事件类型是否是需要的,比如点击、长按、输入。

const evtTypes = [
'tap', // 点击
'input', // 输入
'confirm', // 回车
'longpress' // 长按
]
const hookMethod = (name, method) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
// 记录用户行为
}
return method.apply(this, args)
}
}

确定事件类型之后,还需要明确点击的元素到底是哪个,但是小程序里面比较坑的地方就是,event 对象的 target 属性中,并没有元素的类名,但是可以获取元素的 dataset。


为了准确的获取元素,我们需要在构建中增加一个步骤,修改 wxml 文件,将所有元素的 class 属性复制一份到 

<!-- 构建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn"></view>
<view class="{{mainClassName}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"{{mainClassName}}"></view>

但是获取到 class 之后,又会有另一个坑,小程序的自动化测试工具并不能直接获取页面里自定义组件中的元素,必须先获取自定义组件。

<!-- Page -->
<toast text="loading" show="{{showToast}}" />
<!-- Component -->
<view class="toast" wx:if="{{show}}">
<text class="toast-text">{{text}}</text>
<view class="toast-close" />
</view>
// 如果直接查找 .toast-close 会得到 null
const element = await page.$('.toast-close')
element.tap() // Error!

// 必须先通过自定义组件的 tagName 找到自定义组件
// 再从自定义组件中通过 className 查找对应元素
const element = await page.$('toast .toast-close')
element.tap()

所以我们在构建操作的时候,还需要为元素插入 tagName。

<!-- 构建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"view" />
<toast text="loading" show="{{showToast}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"toast" />

现在我们可以继续愉快的记录用户行为了。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
actions.push({
time: Date.now(),
type,
query,
value
})
}

// 代理事件方法
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
const { type, target, detail } = evt
const { id, dataset = {} } = target
const { className = '' } = dataset
const { value = '' } = detail // input事件触发时,输入框的值
// 记录用户行为
let query = ''
if (isComponent) {
// 如果是组件内的方法,需要获取当前组件的 tagName
query = `${this.dataset.tagName} `
}
if (id) {
// id 存在,则直接通过 id 查找元素
query += id
} else {
// id 不存在,才通过 className 查找元素
query += className
}
addAction(type, query, value)
}
return method.apply(this, args)
}
}

到这里已经记录了用户所有的点击、输入、回车相关的操作。但是还有滚动屏幕的操作没有记录,我们可以直接代理 Page 的 onPageScroll 方法。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
if (type === 'scroll' || type === 'input') {
// 如果上一次行为也是滚动或输入,则重置 value 即可
const last = this.actions[this.actions.length - 1]
if (last && last.type === type) {
last.value = value
last.time = Date.now()
return
}
}
actions.push({
time: Date.now(),
type,
query,
value
})
}

Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
const { onPageScroll } = params
// 拦截滚动事件
params.onPageScroll = function (...args) {
const [evt] = args
const { scrollTop } = evt
addAction('scroll', '', scrollTop)
onPageScroll.apply(this, args)
}
originPage(params)
}

这里有个优化点,就是滚动操作记录的时候,可以判断一下上次操作是否也为滚动操作,如果是同一个操作,则只需要修改一下滚动距离即可,因为两次滚动可以一步到位。同理,输入事件也是,输入的值也可以一步到位。

还原用户行为

用户操作完毕后,可以在控制台输出用户行为的 json 文本,把 json 文本复制出来后,就可以通过自动化工具运行了。

// 引入sdk
const automator = require('miniprogram-automator')

// 用户操作行为
const actions = [
{ type: 'tap', query: 'goods .title', value: '', time: 1596965650000 },
{ type: 'scroll', query: '', value: 560, time: 1596965710680 },
{ type: 'tap', query: 'gotoTop', value: '', time: 1596965770000 }
]

// 启动微信开发者工具
automator.launch({
projectPath: 'path/to/project',
}).then(async miniProgram => {
let page = await miniProgram.reLaunch('/page/index/index')

let prevTime
for (const action of actions) {
const { type, query, value, time } = action
if (prevTime) {
// 计算两次操作之间的等待时间
await page.waitFor(time - prevTime)
}
// 重置上次操作时间
prevTime = time

// 获取当前页面实例
page = await miniProgram.currentPage()
switch (type) {
case 'tap':
const element = await page.$(query)
await element.tap()
break;
case 'input':
const element = await page.$(query)
await element.input(value)
break;
case 'confirm':
const element = await page.$(query)
await element.trigger('confirm', { value });
break;
case 'scroll':
await miniProgram.pageScrollTo(value)
break;
}
// 每次操作结束后,等待 5s,防止页面跳转过程中,后面的操作找不到页面
await page.waitFor(5000)
}

// 关闭 IDE
await miniProgram.close()
})

这里只是简单的还原了用户的操作行为,实际运行过程中,还会涉及到网络请求和 localstorage 的 mock,这里不再展开讲述。同时,我们还可以接入 jest 工具,更加方便用例的编写。

总结

看似很难的需求,只要用心去发掘,总能找到对应的解决办法。另外微信小程序的自动化工具真的有很多坑,遇到问题可以先到小程序社区去找找,大部分坑都有前人踩过,还有一些一时无法解决的问题只能想其他办法来规避。最后祝愿天下无 bug。

原文链接:https://segmentfault.com/a/1190000023555693


收起阅读 »

键盘设置如何优化小程序使用体验?

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。Input 组件的 type 属性从小程序的 1.0 版本开始,...
继续阅读 »

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。

在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。

Input 组件的 type 属性


从小程序的 1.0 版本开始,就支持为 input 组件设置 type,不同的 type 会显示不同的手机键盘。默认情况下,显示的是 text 文本输入键盘,这个键盘的特点是显示所有的内容,可以适用于所有的场景。

但,适用于所有场景也就意味着不适用于所有场景,总会在每一个场景中有着种种不便,因此,在实际的开发中,为了获得更佳的体验,你可以通过设置不同的 Type 来控制实际的键盘显示情况。


除了默认的 text 类以外,你还可以使用 number(数字输入键盘)、idcard 身份证输入键盘和 digit 带小数点的数字键盘。


你可以根据自己的实际使用场景来设置不同的类型,比如说

  • 如果你的小程序的验证码都是数字的,那么你给出一个 text 类型的键盘,显然不如给一个 number 类型的键盘更合适。
  • 如果你的小程序中涉及到了手机号的输入,那么这种情况下你就可以选择使用 number 类型的键盘,来优化用户输入时的体验。

这里的思路是类似的,当你预期用户输入的内容只有数字,就可以考虑 numberdigitidcard 等类型,来优化你的小程序的实际使用体验。


## 总结

input 组件默认提供的 四种 type ,可以通过选择不同的类型,从而获得不同的体验效果,从而对于你的小程序体验进行优化和推进。

原文链接:https://segmentfault.com/a/1190000025160488

收起阅读 »

小程序canvas实现图片压缩

我们需要在选择图片后对图片做一次安全校验启用云开发现在我们需要一个 后端接口 来实现图片的 安全校验 功能这时候临时搭个Node服务好像不太现实又不是什么正经项目于是就想到了微信的云开发功能用起来真实方便快捷至于图片的校...
继续阅读 »




我们需要在选择图片后

对图片做一次安全校验

启用云开发

现在我们需要一个 后端接口 来实现图片的 安全校验 功能

这时候临时搭个Node服务好像不太现实

又不是什么正经项目

于是就想到了微信的云开发功能

用起来真实方便快捷

至于图片的校验方法

直接用云函数调用 security.imgSecCheck 接口就好了

流程

chooseImage() {
/// 用户选择图片
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async res => {
if (res.errMsg === 'chooseImage:ok') {
wx.showLoading({ title: '图片加载中' })
// 获取图片临时地址
const path = res.tempFilePaths[0]
// 将图片地址实例化为图片
const image = await loadImage(path, this.canvas)
// 压缩图片
const filePath = await compress.call(this, image, 'canvas_compress')
// 校验图片合法性
const imgValid = await checkImage(filePath)
wx.hideLoading()
if (!imgValid) return
// 图片安全检测通过,执行后续操作
...
}
})
}


所以在图片上传前要先对超出尺寸的图片进行压缩处理
基本逻辑就是

超出尺寸的图片等比例缩小就好了

我们先要有一个canvas元素

用来处理需要压缩的图片

<template>
<view class="menu-background">
<view class="item replace" bindtap="chooseImage">
<i class="iconfont icon-image"></i>
<text class="title">图片</text>
<text class="sub-title">图片仅供本地使用</text>
</view>
//
// canvas
//
<canvas
type="2d"
id="canvas_compress"
class="canvas-compress"
style="width:
{{canvasCompress.width}}px; height: {{canvasCompress.height}}px"
/>

</view>
</template>

将canvas移到视野不可见到位置

.canvas-compress
position absolute
left 0
top 1000px

图片进行压缩处理

/**
* 压缩图片
* 将尺寸超过规范的图片最小限度压缩
* @param {Image} image 需要压缩的图片实例
* @param {String} canvasId 用来处理压缩图片的canvas对应的canvasId
* @param {Object} config 压缩的图片规范 -> { maxWidth 最大宽度, maxHeight 最小宽度 }
* @return {Promise} promise返回 压缩后的 图片路径
*/
export default function (image, canvasId, config = { maxWidth: 750, maxHeight: 1334 }) {
// 引用的组件传入的this作用域
const _this = this
return new Promise((resolve, reject) => {
// 获取图片原始宽高
let width = image.width
let height = image.height
// 宽度 > 最大限宽 -> 重置尺寸
if (width > config.maxWidth) {
const ratio = width / config.maxWidth
width = config.maxWidth
height = height / ratio
}
// 高度 > 最大限高度 -> 重置尺寸
if (height > config.maxHeight) {
const ratio = height / config.maxHeight
height = config.maxHeight
width = width / ratio
}
// 设置canvas的css宽高
_this.canvasCompress.width = width
_this.canvasCompress.height = height
const query = this.createSelectorQuery()
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec(async res => {
// 获取 canvas 实例
const canvas = res[0].node
// 获取 canvas 绘图上下文
const ctx = canvas.getContext('2d')
// 根据设备dpr处理尺寸
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 将图片绘制到 canvas
ctx.drawImage(image, 0, 0, width, height)
// 将canvas图片上传到微信临时文件
wx.canvasToTempFilePath({
canvas,
x: 0,
y: 0,
destWidth: width,
destHeight: height,
complete (res) {
if (res.errMsg === 'canvasToTempFilePath:ok') {
// 返回临时文件路径
resolve(res.tempFilePath)
}
},
fail(err) {
reject(err)
}
})
})
})
}

图片安全校验

云函数 checkImage.js

const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
/**
* 校验图片合法性
* @param {*} event.fileID 微信云存储的图片ID
* @return {Number} 0:校验失败;1:校验通过
*/
exports.main = async (event, context) => {
const contentType = 'image/png'
const fileID = event.fileID
try {
// 根据fileID下载图片
const file = await cloud.downloadFile({
fileID
})
const value = file.fileContent
// 调用 imgSecCheck 借口,校验不通过接口会抛错
// 必要参数 media { contentType, value }
const result = await cloud.openapi.security.imgSecCheck({
media: {
contentType,
value
}
})
return 1
} catch (err) {
return 0
}
}

组件调用云函数封装

/**
* 校验图片是否存在敏感信息
* @param { String } filePath
* @return { Promise } promise返回校验结果
*/
export default function (filePath) {
return new Promise((resolve, reject) => {
// 先将图片上传到云开发存储
wx.cloud.uploadFile({
cloudPath: `${new Date().getTime()}.png`,
filePath,
success (res) {
// 调用云函数-checkImage
wx.cloud.callFunction({
name: 'checkImage',
data: {
fileID: res.fileID
},
success (res) {
// res.result -> 0:存在敏感信息;1:校验通过
resolve(res.result)
if (!res.result) {
wx.showToast({
title: '图片可能含有敏感信息, 请重新选择',
icon: 'none'
})
}
},
fail (err) {
reject(err)
}
})
},
fail (err) {
reject(err)
}
})
})
}

原文链接:https://segmentfault.com/a/1190000038685508


收起阅读 »

小程序的「获取URL Scheme」能力

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。什么是 URL Scheme微信提供了一个接口,可以生成如 weixin://dl/business/?t=...
继续阅读 »

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。

什么是 URL Scheme

微信提供了一个接口,可以生成如 weixin://dl/business/?t= *TICKET* 的 URL Scheme。你可以在系统自带的浏览器,比如 Safari 中访问这个地址,自动跳转到你自己的微信小程序中。

URL Scheme 能实现什么?

URL Scheme 的用途最大自然是各种营销用途,比如短信营销。不过,如果我们发散思维,就可以知道,URL Scheme 可以有更多的用途。

URL Scheme 在 iOS 系统应用中是比较多的,不少 iOS 的 Power User 都会借助 URL Scheme 来自定义自己的手机中的一些操作,实现特别的操作。我们可以参考 iOS 的 Power User 的用法,理解微信的 URL Scheme 的用法

  • 通过快捷指令来打开特定的 App
  • 在浏览器中嵌入 URL Scheme 来打开应用的特定页面。

如果我们将这些能力迁移到微信生态中,就可以发现,这里我们同样可以实现:

  • 在公众号网页中嵌入 URL Scheme ,从而实现公众号内网页与小程序无缝链接
  • 在短信中嵌入 URL Scheme ,从而实现短信营销,轻松的与自己的产品整合
  • 根据 URL Scheme ,生成一些特殊的二维码,嵌入在图片中

不仅如此,因为目前微信的安装率远高于普通 App,因此,你在进行营销的时候,就再也无需担心用户没有安装自己的 App,大可以先让用户进入到小程序,成为用户后,再引导用户下载 App,提升产品体验

URL Scheme 的劣势

虽然很好,不过 URL Scheme 目前还有一些问题,比如只限于国内非个人主体小程序,对于个人开发者来说就无法使用了。

总结

URL Scheme 的开放,对于微信生态来说,是一个很有力的工具,开发者可以借助与 URL Scheme 来完成自己在微信生态中的推广。在未来,我们可以看到,越来越多的开发者借助于 URL Scheme ,来实现一些很有意思的营销方式。

让我们拭目以待。

原文链接:https://segmentfault.com/a/1190000038919562


收起阅读 »

Kotlin Vocabulary | 数据类

一只小奶狗会有名字、品种以及一堆可爱的特点作为其属性。如果将其建模为一个类,并且只用来保存这些属性数据,那么您应当使用数据类。在使用数据类时,编译器会为您自动生成 toString()、equals() 与 hashCode() 函数,并提供开箱即用的 解构 ...
继续阅读 »

一只小奶狗会有名字、品种以及一堆可爱的特点作为其属性。如果将其建模为一个类,并且只用来保存这些属性数据,那么您应当使用数据类。在使用数据类时,编译器会为您自动生成 toString()equals()hashCode() 函数,并提供开箱即用的 解构 与拷贝功能,从而帮您简化工作,使您可以专注于那些需要展示的数据。接下来本文将会带您了解数据类的其他好处、限制以及其实现的内部原理。


用法概览


声明一个数据类,需要使用 data 修饰符并在其构造函数中以 val 或 var 参数的形式指定其属性。您可以为数据类的构造函数提供默认参数,就像其他函数与构造函数一样;您也可以直接访问和修改属性,以及在类中定义函数。


但相比于普通类,您可以获得以下几个好处:



  • Kotlin 编译器已为您默认实现了 toString()equals()hashCode() 函数 ,从而避免了一系列人工操作可能造成的小错误,例如: 忘记在每次新增或更新属性后更新这些函数、实现 hashCode 时出现逻辑错误,或是在实现 equals 后忘记实现 hashCode 等;

  • 解构;

  • 通过 copy() 函数轻松进行拷贝。


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class Puppy(
val name: String,
val breed: String,
var cuteness: Int = 11
)

// 创建新的实例
val tofuPuppy = Puppy(name = "Tofu", breed = "Corgi", cuteness = Int.MAX_VALUE)
val tacoPuppy = Puppy(name = "Taco", breed = "Cockapoo")

// 访问和修改属性
val breed = tofuPuppy.breed
tofuPuppy.cuteness++

// 解构
val (name, breed, cuteness) = tofuPuppy
println(name) // prints: "Tofu"

// 拷贝:使用与 tofuPuppy 相同的品种和可爱度创建一个小狗,但名字不同
val tacoPuppy = tofuPuppy.copy(name = "Taco")

限制


数据类有着一系列的限制。


构造函数参数


数据类是作为数据持有者被创建的。为了强制执行这一角色,您必须至少传入一个参数到它的主构造函数,而且参数必须是 val 或 var 属性。尝试添加不带 val 或 var 的参数将会导致编译错误。


作为最佳实践,请考虑使用 val 而不是 var,来提升不可变性,否则可能会出现一些细微的问题。如使用数据类作为 HashMap 对象的键时,容器可能会因为其 var 值的改变而获取出无效的结果。


同样,尝试在主构造函数中添加 vararg 参数也会导致编译错误:


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class Puppy constructor(
val name: String,
val breed: String,
var cuteness: Int = 11,
// 错误:数据类的的主构造函数中只能包含属性 (val 或 var) 参数
playful: Boolean,
// 错误:数据类型的主构造函数已禁用 vararg 参数
vararg friends: Puppy
)

vararg 不被允许是由于 JVM 中数组和集合的 equals() 的实现方法不同。Andrey Breslav 的解释是:



集合的 equals() 进行的是结构化比较,而数组不是,数组使用 equals() 等效于判断其引用是否相等: this === other。


*阅读更多: blog.jetbrains.com/kotlin/2015…



继承


数据类可以继承于接口、抽象类或者普通类,但是不能继承其他数据类。数据类也不能被标记为 open。添加 open 修饰符会导致错误: Modifier ‘open’ is incompatible with ‘data’ (‘open’ 修饰符不兼容 ‘data’)


内部实现


为了理解这些功能为何能够实现,我们来检查下 Kotlin 究竟生成了什么。为了做到这点,我们需要查看反编译后的 Java 代码: Tools -> Kotlin -> Show Kotlin Bytecode,然后点击 Decompile 按钮。


属性


就像普通的类一样,Puppy 是一个公共 final 类,包含了我们定义的属性以及它们的 getter 和 setter:


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

public final class Puppy {
@NotNull
private final String name;
@NotNull
private final String breed;
private int cuteness;

@NotNull
public final String getName() {
return this.name;
}

@NotNull
public final String getBreed() {
return this.breed;
}

public final int getCuteness() {
return this.cuteness;
}

public final void setCuteness(int var1) {
this.cuteness = var1;
}
...
}
复制代码

构造函数


我们定义的构造函数是由编译器生成的。由于我们在构造函数中使用了默认参数,所以我们也得到了第二个合成构造函数。


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

public Puppy(@NotNull String name, @NotNull String breed, int cuteness) {
...
this.name = name;
this.breed = breed;
this.cuteness = cuteness;
}

// $FF: synthetic method
public Puppy(String var1, String var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 4) != 0) {
var3 = 11;
}

this(var1, var2, var3);
}
...
}
复制代码

toString()、hashCode() 和 equals()


Kotlin 会为您生成 toString()hashCode()equals() 方法。当您修改了数据类或更新了属性之后,也能自动为您更新为正确的实现。就像下面这样,hashCode()equals() 总是需要同步。在 Puppy 类中它们如下所示:


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

...
@NotNull
public String toString() {
return "Puppy(name=" + this.name + ", breed=" + this.breed + ", cuteness=" + this.cuteness + ")";
}

public int hashCode() {
String var10000 = this.name;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
String var10001 = this.breed;
return (var1 + (var10001 != null ? var10001.hashCode() : 0)) * 31 + this.cuteness;
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Puppy) {
Puppy var2 = (Puppy)var1;
if (Intrinsics.areEqual(this.name, var2.name) && Intrinsics.areEqual(this.breed, var2.breed) && this.cuteness == var2.cuteness) {
return true;
}
}

return false;
} else {
return true;
}
}
...


toStringhashCode 函数的实现很直接,跟一般您所实现的类似,而 equals 使用了 Intrinsics.areEqual 以实现结构化比较:


public static boolean areEqual(Object first, Object second) {
return first == null ? second == null : first.equals(second);
}
复制代码

通过使用方法调用而不是直接实现,Kotlin 语言的开发者可以获得更多的灵活性。如果有需要,他们可以在未来的语言版本中修改 areEqual 函数的实现。


Component


为了实现解构,数据类生成了一系列只返回一个字段的 componentN() 方法。component 的数量取决于构造函数参数的数量:


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public final String component1() {
return this.name;
}

@NotNull
public final String component2() {
return this.breed;
}

public final int component3() {
return this.cuteness;
}
...
您可以通过阅读我们之前的 Kotlin Vocabulary 文章 来了解更多有关解构的内容。

拷贝


数据类会生成一个用于创建新对象实例的 copy() 方法,它可以保持任意数量的原对象属性值。您可以认为 copy() 是个含有所有数据对象字段作为参数的函数,它同时用原对象的字段值作为方法参数的默认值。知道了这一点,您就可以理解 Kotlin 为什么会创建两个 copy() 函数: copycopy$default。后者是一个合成方法,用来保证参数没有传值时,可以正确地使用原对象的值:


/* Copyright 2020 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */
...
@NotNull
public final Puppy copy(@NotNull String name, @NotNull String breed, int cuteness) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(breed, "breed");
return new Puppy(name, breed, cuteness);
}

// $FF: synthetic method
public static Puppy copy$default(Puppy var0, String var1, String var2, int var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.name;
}

if ((var4 & 2) != 0) {
var2 = var0.breed;
}

if ((var4 & 4) != 0) {
var3 = var0.cuteness;
}

return var0.copy(var1, var2, var3);
}
...总结

数据类是 Kotlin 中最常用的功能之一,原因也很简单 —— 它减少了您需要编写的模板代码、提供了诸如解构和拷贝对象这样的功能,从而让您可以专注于重要的事: 您的应用。


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

Rxjava 线程切换原理

前言 rxjava 可以很方便的进行线程切换, 那么rxjava是如何进行线程切换的呢?阅读本文可以了解下rxjava 是如何进行线程切换的及线程切换的影响点。 一个简单的代码: Observable.create(new ObservableOnSubsc...
继续阅读 »

前言


rxjava 可以很方便的进行线程切换, 那么rxjava是如何进行线程切换的呢?阅读本文可以了解下rxjava 是如何进行线程切换的及线程切换的影响点。




一个简单的代码:


Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(ObservableEmitter<String> e) throws Exception {
Log.d("WanRxjava ", "subscrib td ==" + Thread.currentThread().getName());
e.onNext("我在发送next");
e.onComplete();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {
Log.d("WanRxjava ", "onSubscribe td ==" + Thread.currentThread().getName());
}

@Override
public void onNext(String value) {
Log.d("WanRxjava ", "onNext td ==" + Thread.currentThread().getName());
}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {
Log.d("WanRxjava ", "onComplete td ==" + Thread.currentThread().getName());
}
});

如上代码,实现了线程切换和观察者被观察者绑定的逻辑。我们分四部分看上述代码逻辑create、subscribeOn、observeOn、subscribe


1.create

create 顾名思议是 创建被观察者,这里有一个参数是 ObservableOnSubscribe,这是个接口类,我们看下create 的源码:


@SchedulerSupport(SchedulerSupport.NONE)
public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
ObjectHelper.requireNonNull(source, "source is null");
return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source));
}



将ObservableOnSubscribe 传入后 又调用了 new ObservableCreate(source)


public final class ObservableCreate<T> extends Observable<T> {
final ObservableOnSubscribe<T> source;

public ObservableCreate(ObservableOnSubscribe<T> source) {
this.source = source;
}
}


ObservableCreate 有一个变量是 source,这里只是将传入的ObservableOnSubscribe 赋值给source,也就是做了一层包装,然后返回。


2.subscribeOn

调用完create后返回了 ObservableCreate(Observable),然后继续调用subscribeOn,传入了一个变量 Schedulers.io()


@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> subscribeOn(Scheduler scheduler) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
}


我们看到调用了new ObservableSubscribeOn(this, scheduler) 将自身和 scheduler 传入


public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;

public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {
super(source);
this.scheduler = scheduler;
}
}


ObservableSubscribeOn 将scheduler 和 create 返回的对象又包装了一层 返回ObservableSubscribeOn


3.observeOn

有一个参数是 Scheduler


@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> observeOn(Scheduler scheduler) {
return observeOn(scheduler, false, bufferSize());
}
@SchedulerSupport(SchedulerSupport.CUSTOM)
public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
ObjectHelper.verifyPositive(bufferSize, "bufferSize");
return RxJavaPlugins.onAssembly(new ObservableObserveOn<T>(this, scheduler, delayError, bufferSize));
}


ObservableSubscribeOn(observable)又调用了observeOn,然后调用了new ObservableObserveOn(this, scheduler, delayError, bufferSize)


public final class ObservableObserveOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;
final boolean delayError;
final int bufferSize;
public ObservableObserveOn(ObservableSource<T> source, Scheduler scheduler, boolean delayError, int bufferSize) {
super(source);
this.scheduler = scheduler;
this.delayError = delayError;
this.bufferSize = bufferSize;
}
}


又是一个包装,将ObservableSubscribeOn 和 scheduler 包装成 ObservableObserveOn


4.subscribe

上述最后一步即调用ObservableObserveOn.subscribe,传入参数是一个 observer


//ObservableObserveOn.java
@SchedulerSupport(SchedulerSupport.NONE)
@Override
public final void subscribe(Observer<? super T> observer) {
ObjectHelper.requireNonNull(observer, "observer is null");
try {
observer = RxJavaPlugins.onSubscribe(this, observer);

ObjectHelper.requireNonNull(observer, "Plugin returned null Observer");

subscribeActual(observer);
} catch (NullPointerException e) { // NOPMD
throw e;
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
// can't call onError because no way to know if a Disposable has been set or not
// can't call onSubscribe because the call might have set a Subscription already
RxJavaPlugins.onError(e);

NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
npe.initCause(e);
throw npe;
}
}


可以看到调用subscribe 后调用了subscribeActual(observer);将observer 传入


我们看下 subscribeActual(observer)


//ObservableObserveOn.java
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();

source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
}
}


上面的if 先不管,主要看下下面的逻辑,调用了 scheduler.createWorker(),这个scheduler 是 observeOn 传入的,然后调用


new ObserveOnObserver(observer, w, delayError, bufferSize);将worker /observer 又做了一次包装。


//ObservableObserveOn 内部类
static final class ObserveOnObserver<T> extends BasicIntQueueDisposable<T>
implements Observer<T>, Runnable {

private static final long serialVersionUID = 6576896619930983584L;
final Observer<? super T> actual;
final Scheduler.Worker worker;
final boolean delayError;
final int bufferSize;

SimpleQueue<T> queue;

Disposable s;

Throwable error;
volatile boolean done;

volatile boolean cancelled;

int sourceMode;

boolean outputFused;

ObserveOnObserver(Observer<? super T> actual, Scheduler.Worker worker, boolean delayError, int bufferSize) {
this.actual = actual;
this.worker = worker;
this.delayError = delayError;
this.bufferSize = bufferSize;
}

@Override
public void onSubscribe(Disposable s) {
if (DisposableHelper.validate(this.s, s)) {
this.s = s;
if (s instanceof QueueDisposable) {
@SuppressWarnings("unchecked")
QueueDisposable<T> qd = (QueueDisposable<T>) s;

int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY);

if (m == QueueDisposable.SYNC) {
sourceMode = m;
queue = qd;
done = true;
actual.onSubscribe(this);
schedule();
return;
}
if (m == QueueDisposable.ASYNC) {
sourceMode = m;
queue = qd;
actual.onSubscribe(this);
return;
}
}

queue = new SpscLinkedArrayQueue<T>(bufferSize);

actual.onSubscribe(this);
}
}

@Override
public void onNext(T t) {
if (done) {
return;
}

if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
schedule();
}

@Override
public void onError(Throwable t) {
if (done) {
RxJavaPlugins.onError(t);
return;
}
error = t;
done = true;
schedule();
}

@Override
public void onComplete() {
if (done) {
return;
}
done = true;
schedule();
}

@Override
public void dispose() {
if (!cancelled) {
cancelled = true;
s.dispose();
worker.dispose();
if (getAndIncrement() == 0) {
queue.clear();
}
}
}

@Override
public boolean isDisposed() {
return cancelled;
}

void schedule() {
if (getAndIncrement() == 0) {
worker.schedule(this);
}
}

void drainNormal() {
int missed = 1;

final SimpleQueue<T> q = queue;
final Observer<? super T> a = actual;

for (;;) {
if (checkTerminated(done, q.isEmpty(), a)) {
return;
}

for (;;) {
boolean d = done;
T v;

try {
v = q.poll();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
s.dispose();
q.clear();
a.onError(ex);
return;
}
boolean empty = v == null;

if (checkTerminated(d, empty, a)) {
return;
}

if (empty) {
break;
}

a.onNext(v);
}

missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}

void drainFused() {
int missed = 1;

for (;;) {
if (cancelled) {
return;
}

boolean d = done;
Throwable ex = error;

if (!delayError && d && ex != null) {
actual.onError(error);
worker.dispose();
return;
}

actual.onNext(null);

if (d) {
ex = error;
if (ex != null) {
actual.onError(ex);
} else {
actual.onComplete();
}
worker.dispose();
return;
}

missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}

@Override
public void run() {
if (outputFused) {
drainFused();
} else {
drainNormal();
}
}

boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) {
if (cancelled) {
queue.clear();
return true;
}
if (d) {
Throwable e = error;
if (delayError) {
if (empty) {
if (e != null) {
a.onError(e);
} else {
a.onComplete();
}
worker.dispose();
return true;
}
} else {
if (e != null) {
queue.clear();
a.onError(e);
worker.dispose();
return true;
} else
if (empty) {
a.onComplete();
worker.dispose();
return true;
}
}
}
return false;
}

@Override
public int requestFusion(int mode) {
if ((mode & ASYNC) != 0) {
outputFused = true;
return ASYNC;
}
return NONE;
}

@Override
public T poll() throws Exception {
return queue.poll();
}

@Override
public void clear() {
queue.clear();
}

@Override
public boolean isEmpty() {
return queue.isEmpty();
}
}


包装完ObserveOnObserver后,调用了source.subscribe 这里的source 即ObservableSubscribeOn.subscribe,进而调用ObservableSubscribeOn.subscribeActual


//ObservableSubscribeOn.java
@Override
public void subscribeActual(final Observer<? super T> s) {
final SubscribeOnObserver<T> parent = new scheduler<T>(s);

s.onSubscribe(parent);

parent.setDisposable(scheduler.scheduleDirect(new Runnable() {
@Override
public void run() {
source.subscribe(parent);
}
}));
}

static final class SubscribeOnObserver<T> extends AtomicReference<Disposable> implements Observer<T>, Disposable {

private static final long serialVersionUID = 8094547886072529208L;
final Observer<? super T> actual;

final AtomicReference<Disposable> s;

SubscribeOnObserver(Observer<? super T> actual) {
this.actual = actual;
this.s = new AtomicReference<Disposable>();
}

@Override
public void onSubscribe(Disposable s) {
DisposableHelper.setOnce(this.s, s);
}

@Override
public void onNext(T t) {
actual.onNext(t);
}

@Override
public void onError(Throwable t) {
actual.onError(t);
}

@Override
public void onComplete() {
actual.onComplete();
}

@Override
public void dispose() {
DisposableHelper.dispose(s);
DisposableHelper.dispose(this);
}

@Override
public boolean isDisposed() {
return DisposableHelper.isDisposed(get());
}

void setDisposable(Disposable d) {
DisposableHelper.setOnce(this, d);
}
}


ObservableSubscribeOn.subscribeActual


首先将传入的观察者封装成 SubscribeOnObserver

然后触发了 onSubscribe,接着调用 scheduler.scheduleDirect(new Runnable() 这里的scheduler 是 subscribeOn 传入的

最后调用了 scheduler.setsetDisposable方法。

我们看到 run 的方法体即source.subscribe(parent);这里的source 即 ObservableCreate(ObservableOnSubscribe),传入了observer,然后调用 observer的OnNext 和 OnComplete 方法。


5.小结:

a. 调用Observer.OnSubscribe 方法是 不受线程调度影响的

b.subscribeOn 影响的是发送事件的线程

c.observerOn 影响的是观察者处理接受数据的线程,如果没有调用observeOn 则不会进行包装成 ObserveOnObserver,也就是说不会执行观察者的线程切换,和 发送者的线程一致

d.多次调用subscribeOn切换线程,每次都会new ObservableSubscribeOn,触发事件发送时会往上调用,也就是第一次调用的subscribeOn传入的线程 会执行发送事件,后面的线程切换无效

e.Observer.OnSubscribe 只会执行一次,因为调用DisposableHelper.setOnce(this.s, s)

f.处理完onComplete 或者onError 后就不会再发出事件,因为被观察者发送完这两个事件后 就会调用disposed


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

收起阅读 »

聊聊 Bitmap 的一些知识点

Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Andro...
继续阅读 »

Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Android 2.3 及更高版本的默认设置),将单张照片加载到内存大约需要 48MB 内存(4048 * 3036 * 4 字节),如此庞大的内存需求可能会立即耗尽应用的所有可用内存


本篇文章就来讲下 Bitmap 一些比较有用的知识点,希望对你有所帮助 😇😇


全文可以概括为以下几个问题:



  1. Bitmap 所占内存大小的计算公式?

  2. Bitmap 所占内存大小和所在的 drawable 文件夹的关系?

  3. Bitmap 所占内存大小和 ImageView 的宽高的关系?

  4. Bitmap 如何减少内存大小?


1、预备知识


在开始讲关于 Bitmap 的知识点前,需要先阐述一些基础概念作为预备知识


我们知道,在不同手机屏幕上 1dp 所对应的 px 值可能是会有很大差异的。例如,在小屏幕手机上 1dp 可能对应 1px,在大屏幕手机上对应的可能是 3px,这也是我们的应用实现屏幕适配的原理基础之一


想要知道在特定一台手机上 1dp 对应多少 px,或者是想要知道屏幕宽高大小,这些信息都可以通过 DisplayMetrics 来获取


val displayMetrics = applicationContext.resources.displayMetrics


打印出本文所使用的模拟器的 DisplayMetrics 信息:


DisplayMetrics{density=3.0, width=1080, height=1920, scaledDensity=3.0, xdpi=480.0, ydpi=480.0}


从中就可以提取出几点信息:



  1. density 等于 3,说明在该模拟器上 1dp 等于 3px

  2. 屏幕宽高大小为 1920 x 1080 px,即 640 x 360 dp

  3. 屏幕像素密度为 480dpi


dpi 是一个很重要的值,指的是在系统软件上指定的单位尺寸的像素数量,往往是写在系统出厂配置文件的一个固定值。Android 系统定义的屏幕像素密度基准值是 160dpi,该基准值下 1dp 就等于 1px,依此类推 320dpi 下 1dp 就等于 2px


dpi 决定了应用在显示 drawable 时是选择哪一个文件夹内的切图。每个 drawable 文件夹都对应不同的 dpi 大小,Android 系统会自动根据当前手机的实际 dpi 大小从合适的 drawable 文件夹内选取图片,不同的后缀名对应的 dpi 大小就如以下表格所示。如果 drawable 文件夹名不带后缀,那么该文件夹就对应 160dpi


对于本文所使用的模拟器来说,应用在选择图片时就会优先从 drawable-xxhdpi 文件夹拿,如果该文件夹内没找到图片,就会依照 xxxhdpi -> xhdpi -> hdpi -> mdpi -> ldpi 的顺序进行查找,优先使用高密度版本的图片资源


2、内存大小的计算公式


先将一张大小为 1920 x 1080 px 的图片保存到 drawable-xxhdpi 文件夹内,然后将其显示在一个宽高均为 180dp 的 ImageView 上,该 Bitmap 所占用的内存就通过 bitmap.byteCount来获取


    val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("imageView width: " + imageView.width)
log("imageView height: " + imageView.height)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)


BitmapMainActivity: imageView width: 540
BitmapMainActivity: imageView height: 540
BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 8294400



  • 由于模拟器的 density 等于 3,所以 ImageView 的宽高都是 540 px

  • Bitmap 的宽高还是保持其原有大小,即1920 x 1080 px

  • ARGB_8888 代表的是该 Bitmap 的编码格式,该格式下一个像素点需要占用 4 byte

  • inDensity 代表的是系统最终选择的 drawable 文件夹类型,等于 480 说明取的是 drawable-xxhdpi文件夹下的图片

  • inTargetDensity 代表的是当前设备的 dpi

  • 8294400 就是 Bitmap 所占用的内存大小,单位是 byte


从最终结果可以很容易地就逆推出 Bitmap 所占内存大小的计算公式:bitmapWidth * bitmapHeight * 单位像素点所占用的字节数,即 1920 * 1080 * 4 = 8294400


此外,在 Android 2.3 版本之前,Bitmap 像素存储需要的内存是在 native 上分配的,并且生命周期不太可控,可能需要用户自己回收。2.3 - 7.1 之间,Bitmap 的像素存储在 Dalvik 的 Java 堆上,当然,4.4 之前的甚至能在匿名共享内存上分配(Fresco采用),而 8.0 之后的像素内存又重新回到 native 上去分配,不需要用户主动回收,8.0 之后图像资源的管理更加优秀,极大降低了 OOM


3、和 drawable 文件夹的关系


上面之所以很容易就逆推出了 Bitmap 所占内存大小的计算公式,是因为所有条件都被我故意设定为最优情况了,才使得计算过程这么简单。而实际上 Bitmap 所占内存大小和其所在的 drawable 文件夹是有很大关系的,虽然计算公式没变


现在的大部分应用为了达到最优的显示效果,会为应用准备多套切图放在不同的 drawable 文件夹下,而BitmapFactory.decodeResource 方法在解码 Bitmap 的时候,就会自动根据当前设备的 dpi 和 drawable 文件夹类型来判断是否需要对图片进行缩放显示


将图片从 drawable-xxhdpi迁移到 drawable-xhdpi文件夹,然后再打印日志信息


BitmapMainActivity: imageView width: 540
BitmapMainActivity: imageView height: 540
BitmapMainActivity: bitmap width: 2880
BitmapMainActivity: bitmap height: 1620
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 320
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 18662400

可以看到,Bitmap 的宽高都发生了变化,inDensity 等于 320 也说明了选取的是drawable-xhdpi文件夹内的图片,Bitmap 所占内存居然增加了一倍多


模拟器的 dpi 是 480,拿到了 dpi 为 320 的drawable-xhdpi文件夹下的图片,在系统的理解中该文件夹存放的都是小图标,是为小屏幕手机准备的,现在要在大屏幕手机上展示的话就需要对其进行放大,放大的比例就是 480 / 320 = 1.5 倍,因此 Bitmap 的宽就会变为 1920 * 1.5 = 2880 px,高就会变为 1080 * 1.5 = 1620 px,最终占用的内存空间大小就是 2880 * 1620 * 4 = 18662400


所以说,对于同一台手机,Bitmap 在不同 drawable 文件夹下对其最终占用的内存大小是有很大关系的,虽然计算公式没变,但是由于系统会进行自动缩放,Bitmap 的宽高都变为了原先的 1.5 倍,导致最终 Bitmap 的内存大小就变为了 8294400 * 1.5 * 1.5 = 18662400


同理,对于同个 drawable 文件夹下的同一张图片,在不同的手机屏幕上也可能会占用不同的内存空间,因为不同的手机的 dpi 大小可能是不一样的,BitmapFactory 进行缩放的比例也就不一样


4、和 ImageView 的宽高的关系


在上一个例子里,Bitmap 的宽高是 2880 * 1620 px,ImageView 的宽高是 540 * 540 px,该 Bitmap 肯定是会显示不全的,读者可以试着自己改变 ImageView 的宽高大小来验证是否会对 Bitmap 的大小产生影响


这里就不贴代码了,直接来说结论,答案是没有关系。原因也很简单,毕竟上述例子是先将 Bitmap 加载到内存中后再设置给 ImageView 的,ImageView 自然不会影响到 Bitmap 的加载过程,该 Bitmap 的大小只受其所在的 drawable 文件夹类型以及手机的 dpi 大小这两个因素的影响。但这个结论是需要考虑测试方式的,如果你是使用 Glide 来加载图片,Glide 内部实现了按需加载的机制,避免由于 Bitmap 过大而 ImageView 显示不全导致内存浪费的情况,这种情况下 ImageView 的宽高就会影响到 Bitmap 的内存大小了


5、BitmapFactory


BitmapFactory 提供了很多个方法用于加载 Bitmap 对象:decodeFile、decodeResourceStream、decodeResource、decodeByteArray、decodeStream 等多个,但只有 decodeResourceStreamdecodeResource 这两个方法才会根据 dpi 进行自动缩放


decodeResource 方法也会调用到decodeResourceStream方法,decodeResourceStream方法如果判断到inDensityinTargetDensity 两个属性外部没有主动赋值的话,就会根据实际情况进行赋值。如果是从磁盘或者 assert 目录加载图片的话是不会进行自动缩放的,毕竟这些来源也不具备 dpi 信息,Bitmap 的分辨率也只能保持其原有大小


	@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//如果 density 没有赋值的话(等于0),那么就使用基准值 160 dpi
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
//在这里进行赋值,density 就等于 drawable 对应的 dpi
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//如果没有主动设置 inTargetDensity 的话,inTargetDensity 就等于设备的 dpi
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}

6、Bitmap.Config


Bitmap.Config 定义了四种常见的编码格式,分别是:



  • ALPHA_8。每个像素点需要一个字节的内存,只存储位图的透明度,没有颜色信息

  • ARGB_4444。A(Alpha)、R(Red)、G(Green)、B(Blue)各占四位精度,共计十六位的精度,折合两个字节,也就是说一个像素点占两个字节的内存,会存储位图的透明度和颜色信息

  • ARGB_8888。ARGB 各占八个位的精度,折合四个字节,会存储位图的透明度和颜色信息

  • RGB_565。R占五位精度,G占六位精度,B占五位精度,一共是十六位精度,折合两个字节,只存储颜色信息,没有透明度信息


7、优化 Bitmap


根据 Bitmap 所占内存大小的计算公式:bitmapWidth * bitmapHeight * 单位像素点所占用的字节数,想要尽量减少 Bitmap 占用的内存大小的话就要从降低图片分辨率降低单位像素需要的字节数这两方面来考虑了


在一开始的情况下加载到的 Bitmap 的宽高是 1920 * 1080,占用的内存空间是 1920 * 1080 * 4 = 8294400,约 7.9 MB,这是优化前的状态


    val options = BitmapFactory.Options()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)

BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 8294400

1、inSampleSize


由于 ImageView 的宽高只有 540 * 540 px,此时 Bitmap 也只能在 ImageView 上显示为一个像素缩略图,如果进行原图加载的话其实会造成很大的内存浪费,此时我们就可以通过 inSampleSize 属性来压缩图片尺寸


例如,将 inSampleSize 设置为 2 后,Bitmap 的宽高就都会缩减为原先的一半,占用的内存空间就变成了原先的四分之一, 960 * 540 * 4 = 2073600,约 1.9 MB


    val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)

BitmapMainActivity: bitmap width: 960
BitmapMainActivity: bitmap height: 540
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 2073600

可以看到,inSampleSize 属性应该设置多少是需要根据 Bitmap 的实际宽高ImageView 的实际宽高这两个条件来一起决定的。我们在正式加载 Bitmap 前要先获取到 Bitmap 的实际宽高大小,这可以通过 inJustDecodeBounds 属性来实现。设置 inJustDecodeBounds 为 true 后 decodeResource方法只会去读取 Bitmap 的宽高属性而不会去进行实际加载,这个操作是比较轻量级的。然后通过每次循环对半折减,计算出 inSampleSize 需要设置为多少才能尽量接近到 ImageView 的实际宽高,之后将 inJustDecodeBounds 设置为 false 去实际加载 Bitmap


    val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
val inSampleSize = calculateInSampleSize(options, imageView.width, imageView.height)
options.inSampleSize = inSampleSize
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}

需要注意的是,inSampleSize 使用的最终值将是向下舍入为最接近的 2 的幂,BitmapFactory 内部会自动会该值进行校验修正


2、inTargetDensity


如果我们不主动设置 inTargetDensity 的话,decodeResource 方法会自动根据当前设备的 dpi 来对 Bitmap 进行缩放处理,我们可以通过主动设置 inTargetDensity 来控制缩放比例,从而控制 Bitmap 的最终宽高。最终宽高的生成规则: 180 / 480 * 1920 = 720,180 / 480 * 1080 = 405,占用的内存空间是 720 * 405 * 4 = 1166400,约 1.1 MB


    val options = BitmapFactory.Options()
options.inTargetDensity = 180
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)

BitmapMainActivity: bitmap width: 720
BitmapMainActivity: bitmap height: 405
BitmapMainActivity: bitmap config: ARGB_8888
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 1166400

3、Bitmap.Config


BitmapFactory 默认使用的编码图片格式是 ARGB_8888,每个像素点占用四个字节,我们可以按需改变要采用的图片格式。例如,如果要加载的 Bitmap 不包含透明通道的,我们可以使用 RGB_565,该格式每个像素点占用两个字节,占用的内存空间是 1920 * 1080 * 2 = 4147200,约 3.9 MB


    val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.RGB_565
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options)
imageView.setImageBitmap(bitmap)
log("bitmap width: " + bitmap.width)
log("bitmap height: " + bitmap.height)
log("bitmap config: " + bitmap.config)
log("inDensity: " + options.inDensity)
log("inTargetDensity: " + options.inTargetDensity)
log("bitmap byteCount: " + bitmap.byteCount)

BitmapMainActivity: bitmap width: 1920
BitmapMainActivity: bitmap height: 1080
BitmapMainActivity: bitmap config: RGB_565
BitmapMainActivity: inDensity: 480
BitmapMainActivity: inTargetDensity: 480
BitmapMainActivity: bitmap byteCount: 4147200

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

kotlin 扩展函数之Boolean扩展

Kotlin 扩展函数Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式,这样一来,可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。我们在Andr...
继续阅读 »

kotlin 扩展函数之Boolean扩展

Kotlin 扩展函数

Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式,这样一来,可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。我们在Android 开发中,对于Android 经常使用的API 都可以结合业务做扩展处理,处理之后即可达到某部分业务相关的API逻辑全盘使用。

比如对于Fragment中ViewMdoel 对象上下文转换的扩展

inline fun <reified T : ViewModel> Fragment.viewModel(
factory: ViewModelProvider.Factory,
body: T.() -> Unit
): T {
val vm = ViewModelProviders.of(this, factory)[T::class.java]
vm.body()
return vm
}
复制代码

今天对我们常用的基本数据类型Boolean进行一个扩展

目的: 能让我们在使用的过程中更加符合阅读的逻辑思维,更加简便,不使用if else(明文)表达式, 先上代码和测试用例:

代码:BooleanEtx.kt

package com.kpa.component.ui.extension

/**
* @author: kpa
* @time: 2021/4/17
* @email: billkp@yeah.net
**/


/**
* 数据
*/

sealed class BooleanExt<out T>

object Otherwise : BooleanExt<Nothing>()
class WithData<T>(val data: T) : BooleanExt<T>()

/**
* 判断条件为true 时执行block
*/

inline fun <T : Any> Boolean.yes(block: () -> T) =
when {
this -> {
WithData(block())
}
else -> {
Otherwise
}
}

/**
* 判断条件为false 时执行block
*
*/

inline fun <T> Boolean.no(block: () -> T) = when {
this -> Otherwise
else -> {
WithData(block())
}
}

/**
* 与判断条件互斥时执行block
*/

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T =
when (this) {
is Otherwise -> block()
is WithData -> this.data
}
复制代码

测试用例:

@Test
fun addition_isCorrect() {
true.yes {
// doSomething
}
false.no{
// doSomething
}
// 有返回值(条件为true)
val otherwise = getBoolean().yes {
2
}.otherwise {
1
}
assertEquals(otherwise, 2)
// 有返回值(条件为false)
val otherwise1 = false.no {
2
}.otherwise {
3
}
assertEquals(otherwise1, 2)
}


fun getBoolean() = true
复制代码

总结:

这样就能在工作中直接根据业务去写对应的逻辑了,并且使用了inline函数,所以在字节码层面我们还是if else 的,所以不需担心安全问题,简化了代码,唯一添加的开销就是创建数据返回类WithData,当然在我们开发中是可以忽略不计的。

收起阅读 »

Kotlin 单例模式的常用写法

饿汉式 object Singleton 复制代码 线程安全的懒汉式 class Singleton private constructor() { companion object { private var instance: S...
继续阅读 »

饿汉式


object Singleton
复制代码

线程安全的懒汉式


class Singleton private constructor() {

companion object {
private var instance: Singleton? = null
get() {
if (field == null) field = Singleton()
return field
}

@Synchronized
fun instance(): Singleton {
return instance!!
}
}
}
复制代码

双重校验锁式


class KtSingleton3 private constructor() {
companion object {
val instance by lazy { KtSingleton3() }
}
}
复制代码

Lazy 是接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托。第一次调用 get() 会执行已传递给 lazy()lambda 表达式并记录结果,后续调用 get() 只是返回记录的结果。Lazy 默认的线程模式就是 LazyThreadSafetyMode.SYNCHRONIZED 内部默认双重校验锁


Lazy内部实现


public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
复制代码

Lazy接口


public interface Lazy<out T> {
//当前实例化对象,一旦实例化后,该对象不会再改变
public val value: T
//返回true表示,已经延迟实例化过了,false 表示,没有被实例化,
//一旦方法返回true,该方法会一直返回true,且不会再继续实例化
public fun isInitialized(): Boolean
}
复制代码

SynchronizedLazyImpl


private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this

override val value: T
get() {
val _v1 = _value
//判断是否已经初始化过,如果初始化过直接返回,不在调用高级函数内部逻辑
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}

return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
}
else {
//调用高级函数获取其返回值
val typedValue = initializer!!()
//将返回值赋值给_value,用于下次判断时,直接返回高级函数的返回值
_value = typedValue
initializer = null
typedValue
}
}
}
//省略部分代码
}
复制代码

静态内部类式


class Singleton private constructor() {
companion object {
val instance = SingletonHolder.holder
}

private object SingletonHolder {
val holder = Singleton()
}
}
复制代码

枚举式


enum class Singleton {
INSTANCE;
}

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

iOS --常见崩溃和防护(二)

接上一章。。。。。。。iOS9之前会crash,iOS9之后苹果系统已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。四、NSTimer Crash 防护产生的原因:NSTimer会 强引用 tar...
继续阅读 »

接上一章。。。。。。。

三、NSNotification Crash

产生的原因:
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。

iOS9之前会crash,iOS9之后苹果系统已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。

解决方案:
NSNotification Crash的防护原理很简单, 利用method swizzling hook NSObject的dealloc函数,再对象真正dealloc之前先调用一下:[[NSNotificationCenter defaultCenter] removeObserver:self],即可。
#import 

/**
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。

iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了
*/

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (NSNotificationCrash)

+ (void)xz_enableNotificationProtector;

@end

NS_ASSUME_NONNULL_END
#import "NSObject+NSNotificationCrash.h"
#import "NSObject+XZSwizzle.h"
#import


static const char *isNSNotification = "isNSNotification";

@implementation NSObject (NSNotificationCrash)


+ (void)xz_enableNotificationProtector {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *objc = [[NSObject alloc] init];

[objc xz_instanceSwizzleMethod:@selector(addObserver:selector:name:object:) replaceMethod:@selector(xz_addObserver:selector:name:object:)];

// 在ARC环境下不能显示的@selector dealloc。
[objc xz_instanceSwizzleMethod:NSSelectorFromString(@"dealloc") replaceMethod:NSSelectorFromString(@"xz_dealloc")];
});
}

- (void)xz_addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {

// 添加标志位,在delloc中只有isNSNotification是YES,才会移除通知
[observer setIsNSNotification:YES];
[self xz_addObserver:observer selector:aSelector name:aName object:anObject];
}


- (void)setIsNSNotification:(BOOL)yesOrNo {
objc_setAssociatedObject(self, isNSNotification, @(yesOrNo), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)isNSNotification {
NSNumber *number = objc_getAssociatedObject(self, isNSNotification);;
return [number boolValue];
}

/**
如果一个对象从来没有添加过通知,那就不要remove操作
*/
- (void)xz_dealloc
{
if ([self isNSNotification]) {
NSLog(@"CrashProtector: %@ is dealloc,but NSNotificationCenter Also exsit",self);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

[self xz_dealloc];
}

@end

四、NSTimer Crash 防护

产生的原因:
NSTimer会 强引用 target实例,所以需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。所以,很有必要设计出一种方案,可以有效的防护NSTimer的滥用问题。

解决方案:
定义一个抽象类,NSTimer实例强引用抽象类,而在抽象类中,弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。

具体方式:
1、定义一个抽象类,抽象类中弱引用target。

#import 

NS_ASSUME_NONNULL_BEGIN

@interface XZProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END
#import "XZProxy.h"


@interface XZProxy ()

/// 消息转发的对象
@property (nonatomic, weak) id target;

@end

@implementation XZProxy

+ (instancetype)proxyWithTarget:(id)target {
// NSProxy没有init方法, 只需要调用alloc创建对象即可
XZProxy *proxy = [XZProxy alloc];
proxy.target = target;
return proxy;
}

- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

@end

2、创建category,交换系统方法,实现NSTimer强引用抽象类。
ps:也可以不使用分类,不用交换方法,直接在创建timer实例的时候,将原本的target指向抽象类即可

#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (NSTimerCrash)

+ (void)xz_enableTimerProtector;

@end

NS_ASSUME_NONNULL_END
#import "NSObject+NSTimerCrash.h"
#import "NSObject+XZSwizzle.h"
#import "XZProxy.h"

@implementation NSObject (NSTimerCrash)


+ (void)xz_enableTimerProtector {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 创建一个timer并把它指定到一个默认的runloop模式中,并且在 TimeInterval时间后 启动定时器
[NSTimer xz_classSwizzleMethod:@selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:) replaceMethod:@selector(xz_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)];

// 创建一个定时器,但是么有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法。
[NSTimer xz_classSwizzleMethod:@selector(timerWithTimeInterval:target:selector:userInfo:repeats:) replaceMethod:@selector(xz_timerWithTimeInterval:target:selector:userInfo:repeats:)];
});
}


+ (NSTimer *)xz_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {

return [self xz_scheduledTimerWithTimeInterval:timeInterval target:[XZProxy proxyWithTarget:target] selector:selector userInfo:userInfo repeats:repeats];
}

+ (NSTimer *)xz_timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {

return [self xz_timerWithTimeInterval:timeInterval target:[XZProxy proxyWithTarget:target] selector:aSelector userInfo:userInfo repeats:yesOrNo];
}

@end


摘自链接:https://www.jianshu.com/p/3324786893a1

收起阅读 »

java 设计模式:组合模式

1、概念将对象以树形结构组织起来,以达成“部分-整体”的层次机构,使得客户端对单个对象和组合对象的使用具有一致性。是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象...
继续阅读 »

1、概念

将对象以树形结构组织起来,以达成“部分-整体”的层次机构,使得客户端对单个对象和组合对象的使用具有一致性。

是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。

2、使用场景

部分、整体场景,如树形菜单,文件、文件夹的管理。

  1. 需要表示一个对象整体或部分层次
  2. 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

3、如何使用

树枝和叶子实现统一接口,树枝内部组合该接口。

4、UML结构图分析

5、实际代码分析

例:文件与文件夹的关系

先进行普通的实现方式

//文件类
public class File {
public String name;

public File(String name) {
this.name = name;
}

/**
* 操作方法
* @return
*/
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//文件夹类
public class Folder{
public String name;

private List<File> mFileList;
public Folder(String name) {
mFileList = new ArrayList<>();
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}

public void add(File file) {
mFileList.add(file);
}

public void remove(File file) {
mFileList.remove(file);
}

public File getChild(int pos) {
return mFileList.get(pos);
}
}
//运行
File file = new File("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();

文件和文件夹作为两个类来进行操作,将文件类进行添加文件,但是呢?如果文件夹下添加文件夹该咋办呢?就需要再创建一个list来存放文件夹,这样大家都是节点,为啥搞得这么复杂呢?既然存在上下级节点的问题,咱们就抽象为一个抽象类,用抽象类作为节点,子类就是文件夹和文件。

//将文件与文件夹统一看作是一类节点,做一个抽象类来定义这种节点,然后以其实现类来区分文件与目录,在实现类中分别定义各自的具体实现内容,把组合方法写到这个节点类中
public abstract class File {

public String name;

public File(String name) {
this.name = name;
}

/**
* 操作方法
* @return
*/
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public abstract void watch();

/**
* 组合方法
* @param file
*/
public void add(File file){
throw new UnsupportedOperationException();
}

public void remove(File file){
throw new UnsupportedOperationException();
}

public File getChild(int pos){
throw new UnsupportedOperationException();
}

}
public class Folder extends File{

private List<File> mFileList;
public Folder(String name) {
super(name);
mFileList = new ArrayList<>();
}

@Override
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}

@Override
public void add(File file) {
mFileList.add(file);
}

@Override
public void remove(File file) {
mFileList.remove(file);
}

@Override
public File getChild(int pos) {
return mFileList.get(pos);
}
}
public class TestFile extends File {
public TestFile(String name) {
super(name);
}

@Override
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//运行
TestFile file = new TestFile("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
folder.getChild(0).watch();

这种组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多及目录呈现等树形结构数据的操作。

安全组合模式(简化)如下code

//将文件与文件夹统一看作是一类节点,做一个抽象类来定义这种节点,然后以其实现类来区分文件与目录,在实现类中分别定义各自的具体实现内容,把组合方法写到这个节点类中
public abstract class File {

public String name;

public File(String name) {
this.name = name;
}

/**
* 操作方法
* @return
*/
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public abstract void watch();


}
public class Folder extends File{

private List<File> mFileList;
public Folder(String name) {
super(name);
mFileList = new ArrayList<>();
}

@Override
public void watch() {
StringBuilder sb = new StringBuilder();
for(File f:mFileList){
sb.append(f.name);
}
System.out.println("文件名"+getName()+"文件数目"+ mFileList.size()+"名字"+sb.toString());
}

public void add(File file) {
mFileList.add(file);
}

public void remove(File file) {
mFileList.remove(file);
}

public File getChild(int pos) {
return mFileList.get(pos);
}
}
public class TestFile extends File {
public TestFile(String name) {
super(name);
}

@Override
public void watch() {
System.out.println("文件名"+getName()+"文件数目");
}
}
//运行
TestFile file = new TestFile("hehe");
Folder folder = new Folder("heere");
folder.add(file);
folder.watch();
folder.getChild(0).watch();

安全组合模式分工就很明确了。它还有一个好处就是当我们add/remove的时候,我们能知道具体的类是什么了,而透明组合模式就得在运行时去判断,比较麻烦。

优点:

  1. 高层模块调用简单
  2. 节点自由增加

缺点:

  1. 在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
  2. 叶子类型不能控制。比如我想控制ViewGroup添加的View必须为TextView的时候,约束起来就很麻烦。特别是类型多的时候。
收起阅读 »

java 设计模式:外观设计模式

1、概念外观设计模式的主要目的在于让外部减少与子系统内部多个模块的交互,从而让外部能够更简单的使用子系统。他负责把客户端的请求转发给子系统内部的各个模块进行处理。2、使用场景当你要为一个复杂子系统提供一个简单接口时客户程序与抽象类的实现部分之间存在很大的依赖性...
继续阅读 »

1、概念

外观设计模式的主要目的在于让外部减少与子系统内部多个模块的交互,从而让外部能够更简单的使用子系统。他负责把客户端的请求转发给子系统内部的各个模块进行处理。

2、使用场景

  1. 当你要为一个复杂子系统提供一个简单接口时
  2. 客户程序与抽象类的实现部分之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  3. 当你需要构建一个层次结构的子系统时; 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。

3、UML结构图分析

4、实际代码分析


/**
* 模块A
*/
public class SubSystemA {

public void testFunA(){
System.out.println("testFunA");
}

}
/**
* 模块B
*/
public class SubSystemB {
public void testFunB(){
System.out.println("testFunB");
}
}
/**
* 模块C
*/
public class SubSystemC {
public void testFunC(){
System.out.println("testFunC");
}
}
/**
* Facade
*/
public class Facade {

private SubSystemA subSystemA;
private SubSystemB subSystemB;
private SubSystemC subSystemC;
private Facade(){
subSystemA = new SubSystemA();
subSystemB = new SubSystemB();
subSystemC = new SubSystemC();
}

private static Facade instance;

public static Facade getInstance(){
if(instance==null){
instance = new Facade();
}
return instance;
}

public void tastOperation(){
subSystemA.testFunA();
subSystemB.testFunB();
subSystemC.testFunC();
}
}

//运行
Facade.getInstance().tastOperation();

由于外观类维持了对多个子系统类的引用,外观对象在系统运行时将占用较多的系统资源,因此需要对外观对象的数量进行限制,避免系统资源的浪费。可以结合单例模式对外观类进行改进,将外观类设计为一个单例类。通过对外观模式单例化,可以确保系统中只有唯一一个访问子系统的入口,降低系统资源的消耗。

我在项目中的实践:

在项目中经常会出现,网络请求,缓存本地,本地有缓存用本地缓存,而且网络请求经常会在多个地方调用,如果不采用外观模式设计,则会出现客户端的代码异常复杂,而且不利于维护。于是我就进行了如下改变,建立中间仓库类来进行数据切换,客户端只需要进行对仓库数据进行调用,不用关心仓库里数据怎样生成的。

/**
* 建立仓库接口类
* TestApiDataSource
*/
public interface TestApiDataSource {


/**
* 登陆接口
* @param params
* @return
*/
Observable<GetLoginResponse> getLogin(GetLoginParams params);
}
/**
* 建立本地数据源(主要是为了方便客户端调用)
* TestApiLocalDataSource
*/
public class TestApiLocalDataSource extends BaseLocalDataSource implements TestApiDataSource {


@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {

Observable<GetLoginResponse> observable = Observable.create(new ObservableOnSubscribe<GetLoginResponse>() {
@Override
public void subscribe(ObservableEmitter<GetLoginResponse> subscriber) throws Exception {
subscriber.onComplete();

}
});
return observable;
}

}

/**
* 建立网络数据源
* TestApiRemoteDataSource
*/
public class TestApiRemoteDataSource extends BaseRemoteDataSource implements TestApiDataSource {

/**
*
* 请求网络
* @param params
* @return
*/
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
return ApiSource.getApiService(AppHuanJingFactory.getAppModel().getApi()).getApi2Service().getLogin(params);
}

}

/**
* 建立单例仓库类
* TestApiRepository
*/
public class TestApiRepository extends BaseRepository<TestApiLocalDataSource,TestApiRemoteDataSource> implements TestApiDataSource {

public static volatile TestApiRepository instance;


public static TestApiRepository getInstance(){
if(instance==null){
synchronized (TestApiRepository.class){
if(instance==null){
instance = new TestApiRepository(new TestApiLocalDataSource(),new TestApiRemoteDataSource());
}
}
}
return instance;
}

protected TestApiRepository(TestApiLocalDataSource localDataSource, TestApiRemoteDataSource remoteDataSource) {
super(localDataSource, remoteDataSource);
}


/**
* 数据源切换
* #getLogin#
* @param params
* @return
*/
@Override
public Observable<GetLoginResponse> getLogin(GetLoginParams params) {
Observable<GetLoginResponse> observable = Observable.
concat(localDataSource.getLogin(params),
remoteDataSource.getLogin(params).
doOnNext(new Consumer<GetLoginResponse>() {
@Override
public void accept(GetLoginResponse response) throws Exception {
/**
* cache
*/
}
})).compose(RxTransformerHelper.<GetLoginResponse>ioToUI()).firstOrError().toObservable();
return observable;
}


}
//客户端执行,不需要考虑具体实现
TestApiRepository.getInstance().getLogin(new GetLoginParams()).subscribe(new BaseRxNetworkResponseObserver<GetLoginResponse>() {
@Override
public void onResponse(GetLoginResponse getLoginResponse) {

}

@Override
public void onResponseFail(Exception e) {

}

@Override
protected void onBeforeResponseOperation() {

}

@Override
public void onSubscribe(Disposable d) {
add(d);
}
});


优点:

  1. 由于Facade类封装了各个模块交互过程,如果今后内部模块调用关系发生了变化,只需要修改facade实现就可以了
  2. facade实现是可以被多个客户端调用的
  3. 使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟Facade类交互即可。
收起阅读 »

java 设计模式:装饰者模式

简单详解:1、概念动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。2、使用场景在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。和继承类似添加相应的职责。当不能采用生成...
继续阅读 »

简单详解:

1、概念

动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。

2、使用场景

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。和继承类似添加相应的职责。
  2. 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

3、UML结构图分析

  • 抽象构件(Component)角色:给出一个抽象接口,已规范准备接收附加责任的对象。
  • 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类
  • 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
  • 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。

4、实际代码分析

/**
* 装饰类Component,所有类的父类
*/
public interface Component {

void sampleOperation();
}

/**
* 实现抽象部件,具体装饰过程还是交给子类实现
*/
public class Decorator implements Component {

private Component component;
public Decorator(Component component){
this.component = component;
}

@Override
public void sampleOperation() {
component.sampleOperation();
}

}

/**
* 需要装扮的类
*/
public class ConcreteComponent implements Component{
@Override
public void sampleOperation() {

}
}

/**
* 具体实现
*/
public class ConcreateDecoratorA extends Decorator{
public ConcreateDecoratorA(Component component) {
super(component);
}

@Override
public void sampleOperation() {
super.sampleOperation();
addPingShengm();
}

/**
* 新增业务方法
*/
private void addPingShengm() {
System.out.println("添加绘彩1");
}
}

/**
* 具体实现
*/
public class ConcreateDecoratorB extends Decorator{
public ConcreateDecoratorB(Component component) {
super(component);
}


@Override
public void sampleOperation() {
super.sampleOperation();
addPingShengm();
}


/**
* 新增业务方法
*/
private void addPingShengm() {
System.out.println("添加绘彩2");
}
}

举一个实际例子:

工厂需要产生多种水杯,有瓶身绘彩,有不锈钢盖被子,也有不锈钢盖和瓶身绘彩的杯子。(等各种需求)

假如说采用继承子类的方式。如下code:

/**
* 创建水杯的接口包含四个方法,底座,盖子,瓶身,一个实现功能product
*/
public interface IShuiBei {

void dizuo();

void gaizi();

void pingsheng();

void product();

}
/**
* 水晶杯实现类
*/
public class ShuiJInBei implements IShuiBei,Component {
@Override
public void dizuo() {
System.out.println("水晶底座");
}

@Override
public void gaizi() {
System.out.println("水晶盖子");
}

@Override
public void pingsheng() {
System.out.println("水晶瓶身");
}

@Override
public void product() {
dizuo();
gaizi();
pingsheng();
}
}

/**
* 添加绘彩的水晶杯
*/
public class HuiCaiShuiJinBei extends ShuiJInBei{
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
/**
* 不锈钢杯子盖的水晶杯
*/
public class HuiJinGangGaiBei extends ShuiJInBei{
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
}

/**
* 不锈钢杯子盖的水晶杯带彩绘
*/
public class HuiCaiShuiJinGangGaiBei extends ShuiJInBei{
@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}

//运行
HuiCaiShuiJinBei huiCaiShuiJinBei = new HuiCaiShuiJinBei();
HuiCaiShuiJinGangGaiBei huiCaiShuiJinGangGaiBei = new HuiCaiShuiJinGangGaiBei();
ShuiJInBei shuiJInBei = new ShuiJInBei();
huiCaiShuiJinBei.product();
huiCaiShuiJinGangGaiBei.product();
shuiJInBei.product();

一共创建三个子类,一个父类,当然如果需求更多的话,子类会不断的增加。

装饰类实现如上功能code:

/**
* 实现抽象部件
*/
public class ShuijinbeiDecorator implements IShuiBei{
IShuiBei iShuiBei;
public ShuijinbeiDecorator(IShuiBei iShuiBei){
this.iShuiBei = iShuiBei;
}

@Override
public void dizuo() {
iShuiBei.dizuo();
}

@Override
public void gaizi() {
iShuiBei.gaizi();
}

@Override
public void pingsheng() {
iShuiBei.pingsheng();
}

@Override
public void product() {
dizuo();
gaizi();
pingsheng();
}
}
/**
* 钢盖实现类
*/
public class GangGaiDecorator extends ShuijinbeiDecorator{
public GangGaiDecorator(IShuiBei iShuiBei) {
super(iShuiBei);
}

@Override
public void gaizi() {
System.out.println("不锈钢杯盖");
}
}

/**
* 彩绘实现类
*/
public class CaihuiDecorator extends ShuijinbeiDecorator{
public CaihuiDecorator(IShuiBei iShuiBei) {
super(iShuiBei);
}

@Override
public void pingsheng() {
super.pingsheng();
System.out.println("添加绘彩");
}
}
//运行
IShuiBei iShuiBei = new ShuiJInBei();
iShuiBei.product();
iShuiBei = new CaihuiDecorator(iShuiBei);
iShuiBei.product();
iShuiBei = new GangGaiDecorator(iShuiBei);
iShuiBei.product();
iShuiBei = new ShuiJInBei();
iShuiBei = new GangGaiDecorator(iShuiBei);
iShuiBei.product();

看到如上代码你大概会恍然大悟,装饰模式如果在你的子类特别多,用装饰模式很好,但是比较容易出错哦。

装饰模式的优点

  1. 装饰模式与继承关系的目的都是要拓展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  2. 装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者“除掉”一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
  3. 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合。

装饰模式的缺点

由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,在另外一方面,使用装饰模式会产生比使用继承关系所产生的更多的对象。而更多的对象会使得查找错误更为困难,特别是这些对象在看上去极为相似的时候。

装饰模式在Android中的实际应用

context类簇

收起阅读 »

java 设计模式:适配器模式

adapter定义:将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。类适配器定义:是把适配的类的api转化成为目标类的api。adapter是为了让adaptee与Target发生关系建立的a...
继续阅读 »

adapter定义:

将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

类适配器

定义:

是把适配的类的api转化成为目标类的api。

adapter是为了让adaptee与Target发生关系建立的

adapter 实现Target接口,来继承Adaptee,实现需要实现的方法

代码:

//适配接口
public interface Target {
void request();
}

//需要适配的对象
public class Adaptee {

public void SpecialRequest(){
System.out.println("SpecialRequest");
}
}
//适配器
public class Adapter extends Adaptee implements Target {

@Override
public void request() {
specialRequest();
}
}

//运行代码
Target target = new Adapter();
target.request();
  • 类适配器是继承的方式,方法通过静态定义的
  • 对于类适配器,可以重新定义Adaptee的部分行为。
  • 对于类适配器,仅仅引入一个对象,并不需要额外的引用来间接得到Adaptee。
  • 对于类适配器,由于适配器直接继承adaptee,使得适配器不能和adaptee的子类一起工作。

对象适配器模式

定义:与类的适配器模式一样,对象的适配器模式把被适配的类的api转化为目标类的api,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是通过委派关系连接到adaptee类。

//适配接口
public interface Target {
void request();
}

//需要适配的对象
public class Adaptee {

public void SpecialRequest(){
System.out.println("SpecialRequest");
}
}
//适配器
public class Adapter implements Target {

Adaptee adaptee;

public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}

@Override
public void request() {
adaptee.specialRequest();
}
}

//运行代码
Target target = new Adapter(new Adaptee());
target.request();

如上两个对比,就能看出类适配器和对象适配器的区别。

对象适配器:持有一个对象来实现适配器模式

类适配器:通过继承来实现适配器模式。

  • 对象适配器使用对象组合的方式,是动态组合的方式
  • 对象适配器,一个适配器可以把多种不同的源适配器适配到同一个目标
  • 对于对象适配器,要重定义adapee的行为比较困难
  • 对象适配器需要额外的引用来间接得到adapter

adaper在Android中的运用

listview

收起阅读 »

java 设计模式:建造者模式

概念:建造者模式是较为复杂的创建型模式,将组件和组件的组件过程分开,然后一步一步建造一个复杂的对象。所以建造者模式又叫生成器模式。它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解...
继续阅读 »

概念:

建造者模式是较为复杂的创建型模式,将组件和组件的组件过程分开,然后一步一步建造一个复杂的对象。所以建造者模式又叫生成器模式。它允许用户在不知道内部构建细节的情况下,非常精细地控制对象构建流程。该模式是为了将构建过程非常复杂的对象进行拆分,让它与它的部件解耦,提升代码的可读性以及扩展性。

使用场景:

构造一个对象需要很多参数的时候,并且参数的个数或者类型不固定的时候

UML结构图

例:

//创建复杂对象Product
public class Product {

private String partA;
private String partB;
private String partC;

public String getPartA() {
return partA;
}

public void setPartA(String partA) {
this.partA = partA;
}

public String getPartB() {
return partB;
}

public void setPartB(String partB) {
this.partB = partB;
}

public String getPartC() {
return partC;
}

public void setPartC(String partC) {
this.partC = partC;
}
}

//创建抽象类Builder
public abstract class Builder {

protected Product product = new Product();

public abstract void builderPartA();
public abstract void builderPartB();
public abstract void builderPartC();

public Product getResult() {
return product;
}
}

//创建实现类ConcreateBuilder
public class ConcreateBuilder extends Builder {
@Override
public void builderPartA() {

}

@Override
public void builderPartB() {

}

@Override
public void builderPartC() {

}
}

//创建组装对象Director
public class Director {

private Builder builder;

public Director(Builder builder){
this.builder = builder;
}

public void setBuilder(Builder builder) {
this.builder = builder;
}

public Product constract(){
builder.builderPartA();
builder.builderPartB();
builder.builderPartC();
return builder.getResult();
}
}
//运行
Builder builder = new ConcreateBuilder();
Director director = new Director(builder);
Product product = director.constract();
  • Builder:他为创建一个创建Product对象的各个部件指定抽象接口
  • ConcreateBuilder:它实现了builder接口,实现各个部件的具体构造和装配方法。
  • Product:他是被构建的复杂对象,包好多个组成部件。
  • Director:指挥者又称为导演类,负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系。

优点:

  • 松散耦合:生成其模式可以用同一个构造算法构建出表现上完全不同的产品,实现产品构建和产品表现上的分离。
  • 很容易改变产品内部表示。
  • 更好的复用性:生成器模式很好的实现了构建算法和具体产品实现的分离。

缺点:

  • 会产生多余的Builder对象,Director对象,消耗内存。
  • 对象构建过程暴露。

builder模式在Android中的实际运用

1.AlertDialog

2.Glide/okhttp

收起阅读 »

iOS --常见崩溃和防护(一)

iOS 的崩溃我们常见的crash有哪些呢?1.unrecognized selector crash (没找到对应的函数)2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 ...
继续阅读 »

iOS 的崩溃

我们常见的crash有哪些呢?

1.unrecognized selector crash (没找到对应的函数)

2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 )

3.NSNotification crash:(当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification)

4.NSTimer类型crash:(需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash)

5.Container类型crash:(数组,字典,常见的越界,插入,nil)

6.野指针类型的crash

7.非主线程刷UI类型:(在非主线程刷UI将会导致app运行crash)……

如何防护crash

一、unrecognized selector crash

unrecognized selector类型的crash,通常是因为一个对象调用了一个不属于它方法的方法导致的。而我们可以从方法调用的过程中,寻找到避免程序崩溃的突破口。

方法调用的过程是哪样的呢?

方法调用的过程--调用实例方法
1.在对象的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.对象的<缓存方法列表> 里没找到,就去<类的方法列表>里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
4.如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。

方法调用的过程--调用类方法
1.在类的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.类的<缓存方法列表> 里没找到,就去里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去meta类的父类里执行1、2。
4.如果找到了根meta类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。

从上面的方法调用过程可以看出,在找不到调用的方法程序崩溃之前,我们可以通过重写NSObject方法进行拦截调用,阻止程序的crash。这里面就用到了消息的转发机制:

runtime提供了3种方式去补救:

1:调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2:调用forwardingTargetForSelector让别的对象去执行这个函数
3:调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。
如果都不行,系统才会调用doesNotRecognizeSelector抛出异常。

既然可以补救,我们完全也可以利用消息转发机制来做文章,但是我们选择哪一步比较合适呢?
1:resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
2:forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的 NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写
3:forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写

对于NSObject方法的重写,我们可以分为以下几步:
第一步:为类动态的创建一个消息接受类。
第二步:为类动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
第三步:将消息直接转发到这个消息接受类类对象上。

解决方法:

1、创建一个消息接受类。(继承至NSObject)

当调用方法的消息转发给该类后,该类也没有这个方法,回调用resolveInstanceMethod:方法,在消息接受类中重写方法,返回YES,表明该消息已经处理,这样就不会崩溃了。
重写的resolveInstanceMethod:方法中一定要有动态添加方法的处理,不然会继续走消息转发的流程,从而造成死循环。

#import 


@interface XZUnrecognizedSelectorSolveObject : NSObject

@property (nonatomic, weak) NSObject *objc;

@end
#Import "XZUnrecognizedSelectorSolveObject.h"
#import

@interface XZUnrecognizedSelectorSolveObject ()

@end

@implementation XZUnrecognizedSelectorSolveObject

+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果没有动态添加方法的话,还会调用forwardingTargetForSelector:方法,从而造成死循环
class_addMethod([self class], sel, (IMP)addMethod, "v@:@");
return YES;
}

id addMethod(id self, SEL _cmd) {
NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return 0;
}

@end
2、为NSObject添加分类,拦截NSObject的forwardingTargetForSelector:方法。

实现原理:在分类中自定义一个xz_forwardingTargetForSelector:方法,然后替换掉系统的forwardingTargetForSelector:方法

#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (SelectorCrash)

+ (void)xz_enableSelectorProtector;

@end

NS_ASSUME_NONNULL_END
#import "NSObject+SelectorCrash.h"
#import
#import "NSObject+XZSwizzle.h"
#Import "XZUnrecognizedSelectorSolveObject.h"

@implementation NSObject (SelectorCrash)

+ (void)xz_enableSelectorProtector {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *object = [[NSObject alloc] init];
[object xz_instanceSwizzleMethod:@selector(forwardingTargetForSelector:) replaceMethod:@selector(xz_forwardingTargetForSelector:)];
});
}

- (id)xz_forwardingTargetForSelector:(SEL)aSelector {
// 判断某个类是否有某个实例方法,有则返回YES,否则返回NO
if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
// 有forwardInvocation实例方法
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));

if (imp != impOfNSObject) {
return nil;
}
}

// 新建桩类转发消息
XZUnrecognizedSelectorSolveObject *solveObject = [XZUnrecognizedSelectorSolveObject new];
solveObject.objc = self;
return solveObject;
}

@end

交换方法代码 如下:

#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (XZSwizzle)

/**
对类方法进行拦截并替换

@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;

/**
对类方法进行拦截并替换

@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;



/**
对实例方法进行拦截并替换

@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;

/**
对实例方法进行拦截并替换

@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;

@end

NS_ASSUME_NONNULL_END

#import "NSObject+XZSwizzle.h"
#import

@implementation NSObject (XZSwizzle)


/**
对类方法进行拦截并替换

@param originalSelector 类原有方法
@param replaceSelector 自定义替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];

[self xz_classSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}

/**
对类方法进行拦截并替换

@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {

// Method中包含IMP函数指针,通过替换IMP,使SEL调用不同函数实现
Method originalMethod = class_getClassMethod(kClass, originalSelector);
Method replaceMethod = class_getClassMethod(kClass, replaceSelector);

// 获取MetaClass (交换、添加等类方法需要用metaClass)
Class metaClass = objc_getMetaClass(NSStringFromClass(kClass).UTF8String);

// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(metaClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));

if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(metaClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}


/**
对实例方法进行拦截并替换

@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];

[self xz_instanceSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}

/**
对实例方法进行拦截并替换

@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {

Method originalMethod = class_getInstanceMethod(kClass, originalSelector);
Method replaceMethod = class_getInstanceMethod(kClass, replaceSelector);

// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));

if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(kClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}

@end
调用:
在AppDelegate调用[NSObject xz_enableSelectorProtector]; 就可以了

二、KVO Crash

KVO Crash,通常是KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者引起的。
一个被观察的对象上有若干个观察者,每个观察者又有若干条keypath。如果观察者和keypathx的数量一多,很容易不清楚被观察的对象整个KVO关系,导致被观察者在dealloc的时候,仍然残存着一些关系没有被注销,同时还会导致KVO注册者和移除观察者不匹配的情况发生。尤其是多线程的情况下,导致KVO重复添加观察者或者移除观察者的情况,这种类似的情况通常发生的比较隐蔽,很难从代码的层面上排查。

解决方法:

可以让观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张MAP表来维护KVO的整个关系,这样做的好处有2个:

1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。

2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash

具体方式:
1、自定义一个继承自NSObject的代理类,并通过Catagory将这个代理类作为NSObject的属性进行关联
#import 
#import "XZKVOProxy.h"

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (KVOCrash)

@property (nonatomic, strong) XZKVOProxy * _Nullable KVOProxy; // 自定义的kvo关系的代理

@end

NS_ASSUME_NONNULL_END
#import "NSObject+KVOCrash.h"
#import "XZKVOProxy.h"
#import


#pragma mark - NSObject + KVOCrash

static void *NSObjectKVOProxyKey = &NSObjectKVOProxyKey;

@implementation NSObject (KVOCrash)

- (XZKVOProxy *)KVOProxy {
id proxy = objc_getAssociatedObject(self, NSObjectKVOProxyKey);

if (nil == proxy) {
proxy = [XZKVOProxy kvoProxyWithObserver:self];
self.KVOProxy = proxy;
}

return proxy;
}

- (void)setKVOProxy:(XZKVOProxy *)proxy
{
objc_setAssociatedObject(self, NSObjectKVOProxyKey, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

2、在自定义代理类中建立一个map来维护KVO整个关系

#import 


typedef void (^XZKVONitificationBlock)(id _Nullable observer, id _Nullable object, NSDictionary * _Nullable change);

/**
KVO配置类
用于存储KVO里面的相关设置参数
*/
@interface XZKVOInfo : NSObject

//- (instancetype _Nullable)initWithObserver:(id _Nonnull)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock _Nonnull )block;

@end


NS_ASSUME_NONNULL_BEGIN
/**
KVO管理类
用于管理object添加和移除的消息,(通过Map进行KVO之间的关系)(字典应该也可以)
*/
@interface XZKVOProxy : NSObject

@property (nullable, nonatomic, weak, readonly) id observer;


+ (instancetype)kvoProxyWithObserver:(nullable id)observer;

- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block;

- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath;
- (void)xz_unobserver:(id _Nullable)object;

- (void)xz_unobserverAll;

@end

NS_ASSUME_NONNULL_END

#import "XZKVOProxy.h"
#import


@interface XZKVOInfo ()
{
@public
__weak id _object; // 观察对象
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
XZKVONitificationBlock _block;
}
@end

@implementation XZKVOInfo

- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context {
return [self initWithObserver:object keyPath:keyPath options:options block:NULL action:NULL context:context];
}

- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context
block:(XZKVONitificationBlock)block {

return [self initWithObserver:object keyPath:keyPath options:options block:block action:NULL context:context];
}

- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
block:(_Nullable XZKVONitificationBlock)block
action:(_Nullable SEL)action
context:(void * _Nullable)context {
if (self = [super init]) {
_object = object;
_block = block;
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}

@end



/**
此类用来管理混乱的KVO关系
让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系

好处:
不会crash如果出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以 1.直接阻止这些非正常的操作。

crash 2.被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。

👇:
重复添加观察者不会crash,即不会走@catch
多次添加对同一个属性观察的观察者,系统方法内部会强应用这个观察者,同理即可remove该观察者同样次数。

*/
@interface XZKVOProxy ()
{
pthread_mutex_t _mutex;
NSMapTable *> *_objectInfoMap;///< map来维护KVO整个关系
}
@end

@implementation XZKVOProxy

+ (instancetype)kvoProxyWithObserver:(nullable id)observer {
return [[self alloc] initWithObserver:observer];
}

- (instancetype)initWithObserver:(nullable id)observer {
if (self = [super init]) {
_observer = observer;
_objectInfoMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality capacity:0];
}
return self;
}

/**
加锁、解锁
*/
- (void)lock {
pthread_mutex_lock(&_mutex);
}

- (void)unlock {
pthread_mutex_unlock(&_mutex);
}


/**
添加、删除 观察者
*/
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block {

// 断言
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:options context:context block:block];

if (info) {
// 将info以key-value的形式存储到map中。key是被观察对象;value是观察信息的集合。
// 加锁
[self lock];

NSMutableSet *infos = [_objectInfoMap objectForKey:object];


BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
break;
}
}

if (_isExisting == YES) {
// 解锁
[self unlock];
return;
}
// // check for info existence
// XZKVOInfo *existingInfo = [infos member:info];
// if (nil != existingInfo) {
// // observation info already exists; do not observe it again
//
// // 解锁
// [self unlock];
// return;
// }

// 不存在
if (infos == nil) {
// 创建set,并将set添加进Map里
infos = [NSMutableSet set];
[_objectInfoMap setObject:infos forKey:object];
}
// 将要添加的KVOInfo添加进set里面
[infos addObject:info];

// 解锁
[self unlock];


// 将 kvoProxy 作为观察者;添加观察者
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:info->_context];
}
}

- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath {

// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:0 context:nil];

// 加锁
[self lock];

// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];

BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}

if (_isExisting == YES) {
// 存在
[infos removeObject:info];

// remove no longer used infos
if (0 == infos.count) {
[_objectInfoMap removeObjectForKey:object];
}

// 解锁
[self unlock];


// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
} else {
// 解锁
[self unlock];
}

// XZKVOInfo *registeredInfo = [infos member:info];
//
// if (nil != registeredInfo) {
// [infos removeObject:registeredInfo];
//
// // remove no longer used infos
// if (0 == infos.count) {
// [_objectInfoMap removeObjectForKey:object];
// }
//
// // 解锁
// [self unlock];
//
//
// // 移除观察者
// [object removeObserver:self forKeyPath:registeredInfo->_keyPath context:registeredInfo->_context];
// } else {
// // 解锁
// [self unlock];
// }
}

- (void)xz_unobserver:(id _Nullable)object {
// 加锁
[self lock];

// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];

[_objectInfoMap removeObjectForKey:object];
// 解锁
[self unlock];

// 批量移除观察者
for (XZKVOInfo *info in infos) {
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}

- (void)xz_unobserverAll {

if (_objectInfoMap) {
// 加锁
[self lock];

// copy一份map,防止删除数据异常冲突
NSMapTable *objectInfoMaps = [_objectInfoMap copy];

[_objectInfoMap removeAllObjects];

// 解锁
[self unlock];

// 移除全部观察者
for (id object in objectInfoMaps) {

NSSet *infos = [objectInfoMaps objectForKey:object];
if (!infos || infos.count == 0) {
continue;
}

for (XZKVOInfo *info in infos) {
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}

}
}



- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {

// NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);



NSLog(@"%@",keyPath);
NSLog(@"%@",object);
NSLog(@"%@",change);
NSLog(@"%@",context);


// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];

BOOL _isExisting = NO;
XZKVOInfo *info;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}

if (_isExisting == YES && info) {
XZKVOProxy *proxy = info->_object;
id observer = proxy.observer;

XZKVONitificationBlock block = info->_block;

if (block) {
block(observer, object, change);
}
}
}



- (void)dealloc {

// 移除所有观察者
[self xz_unobserverAll];

// 销毁mutex
pthread_mutex_destroy(&_mutex);
}

@end


摘自链接:https://www.jianshu.com/p/3324786893a1
收起阅读 »

iOS - 剖析性能优化相关

性能优化的几个点:1.卡顿优化在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。CPU(Central Processing Unit,中央处理器)对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core...
继续阅读 »

性能优化的几个点:

1.卡顿优化

在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。

CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。

GPU(Graphics Processing Unit,图形处理器)
纹理的渲染、


所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。

在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。

屏幕成像原理

我们所看到的动态的屏幕的成像其实和视频一样也是一帧一帧组成的。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(Horizonal Synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical Synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。

卡顿成因

前面我们知道,完成显示信息的过程是:CPU 计算数据 -> GPU 进行渲染 -> 屏幕发出 VSync 信号 -> 成像,假如屏幕已经发出了 VSync 但 GPU 还没有渲染完成,则只能将上一次的数据显示出来,以致于当前计算的帧数据丢失,这样就产生了卡顿,当前的帧数据计算好后只能等待下一个周期去渲染。

解决办法

解决卡顿现象的主要思路就是:尽可能减少 CPU 和 GPU 资源的消耗。
按照 60fps 的刷帧率,每隔 16ms 就会有一次 VSync 信号产生。那么针对 CPU 和 GPU 有以下优化方案:
CPU
  • 尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer;
  • 不要频繁地调用 UIView 的相关属性 如:frame、bounds、transform 等;
  • 尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改;
  • Autolayout 会比直接设置 frame 消耗更多的 CPU 资源;
  • 图片的 size 和 UIImageView 的 size 保持一致;
  • 控制线程的最大并发数量;
  • 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
  • GPU
    • 尽量避免短时间内大量图片显示;
    • GPU 能处理的最大纹理尺寸是 4096 * 4096,超过这个尺寸就会占用 CPU 资源,所以纹理不能超过这个尺寸;
    • 尽量减少透视图的数量和层次;
    • 减少透明的视图(alpha < 1),不透明的就设置 opaque 为 YES;
    • 尽量避免离屏渲染;
    离屏渲染

    在 OpenGL 中,GPU 有两种渲染方式:

    On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
    Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;

    离屏渲染消耗性能的原因:

    离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。

    哪些操作会触发离屏渲染?
  • 光栅化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圆角,同时设置 layer.masksToBounds = YESlayer.cornerRadius > 0
    • 可以用 CoreGraphics 绘制裁剪圆角
  • 阴影
    • 如果设置了 layer.shadowPath 不会产生离屏渲染

    卡顿检测

    这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。

    耗电优化

    耗电的主要来源为:

    1.CPU 处理;

    2.网络请求;

    3.定位;

    4.图像渲染;

    优化思路

    1.尽可能降低 CPU、GPU 功耗;

    2.少用定时器;

    3.优化 I/O 操作;

    尽量不要频繁写入小数据,最好一次性批量写入;
    读写大量重要数据时,可以用 dispatch_io,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;
    数据量大时,用数据库管理数据;

    4.网络优化;

  • 减少、压缩网络数据(JSON 比 XML 文件性能更高);
  • 若多次网络请求结果相同,尽量使用缓存;
  • 使用断点续传,否则网络不稳定时可能多次传输相同的内容;
  • 网络不可用时,不进行网络请求;
  • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
  • 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
  • 5.定位优化

  • 如果只是需要快速确定用户位置,用 CLLocationManager 的 requestLocation 方法定位,定位完成后,定位硬件会自动断电;
  • 若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务;
  • 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest
  • 需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically 为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;
  • 启动优化

    App 的启动分为两种:冷启动(Cold Launch) 和热启动(Warm Launch)
    前者表示从零开始启动 App,后者表示 App 已经存在内存中,在后台依然活着,再次点击图标启动 App。

    App 启动的优化主要是针对冷启动的优化,通过添加环境变量可以打印出 App 的启动时间分析:Edit Scheme -> Run -> Arguments -> Environment Variables 添加 DYLD_PRINT_STATISTICS 设置为 1。


    运行程序则会打印:


    这里打印的是在执行 main 函数之前的耗时信息,若想打印更详细的信息则添加环境变量为:
    DYLD_PRINT_STATISTICS_DETAILS 设置为 1。


    App 冷启动

    冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。

    第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。

    dyld

    dyld(Dynamic Link Editor),Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)


    启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。

    Runtime

    启动 App 时,调用 map_images 进行可执行文件的内容解析和处理,再 load_images 中调用 call_load_methods调用所有 Class 和 Category 的 load 方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor)) 修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。

    main

    在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions: 函数。

    启动优化思路

    针对不同的阶段,有不同的优化思路:
    dyld

    1.减少动态库、合并动态库,定期清理不必要的动态库;

    2.减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;

    3.减少 C++ 虚函数数量;

    4.Swift 开发尽量使用 struct;

    虚函数和 Java 中的抽象函数有点类似,但区别是,基类定义的虚函数,子类可以实现也可以不实现,而抽象函数子类一定要实现。

    Runtime

    用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))、C++ 静态构造器、以及 Objective-C 中的 load 方法;

    main
    将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中;

    安装包瘦身

    安装包(ipa)主要由可执行文件和资源文件组成,若不管理妥善则会造成安装包体积越来越大,所以针对资源优化我们可以将资源采取无损压缩,去除没用的资源。

    对于可执行文件的瘦身,我们可以:

    1.从编译器层面优化

    1.Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES
    2.去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加 -fno-exceptions;
    3.利用 AppCode,检测未使用代码检测:菜单栏 -> Code -> Inspect Code;
    4.编写 LLVM 插件检测重复代码、未调用代码;
    5.通过生成 LinkMap 文件检测;

    LinkMap

    Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES


    运行程序可看到:


    打开可看见各种信息:


    我们可根据这个信息针对某个类进行优化。

    摘自链接:https://www.jianshu.com/p/fe566ec32d28

    收起阅读 »

    从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


    收起阅读 »

    Android 快速跳转库

    事情起源activity 或者 fragment 每次跳转传值的时候,你是不是都很厌烦那种,参数传递。 那么如果数据极其多的情况下,你的代码将苦不堪言,即使在很好的设计下,也会很蛋疼。那么今天我给大家推荐一个工具 和咱原生跳转进行比较比较:1.跳转方式比较ba...
    继续阅读 »

    事情起源

    activity 或者 fragment 每次跳转传值的时候,你是不是都很厌烦那种,参数传递。 那么如果数据极其多的情况下,你的代码将苦不堪言,即使在很好的设计下,也会很蛋疼。那么今天我给大家推荐一个工具 和咱原生跳转进行比较

    比较:

    1.跳转方式比较

    bash Intenti=new Intent(this,MainActivity.class); 
    startActivity(i);

    vs

    ApMainActivity.newInstance().start(this)
    //发送 Intenti=new Intent(this,MainActivity.class);
    Bundle bundle = new Bundle();
    bundle.putInt("message", "123");
    i.putExtra("Bundle", bundle);
    startActivity(i);
    //接收
    String s=bundle.getString("message","");

    vs

    //发送 
    ApMainActivity.newInstance().apply { message = "123" } .start(this)
    //接收
    AutoJ.inject(this);

    实体发送 

    //发送 
    ApAllDataActivity.newInstance().apply { message = "123" myData = MyData("hfafas",true,21) } .start(this)
    //接收
    AutoJ.inject(this);

    目前 版本号 v1.0.7 更新内容:(专门为kotlin设计的快速跳转工具,如果你的项目只支持java语言请不要用该版本,建议用v1.0.2 地址 Version number v1.0.2) 

    1. 代码采用kotlin 语法糖 
    2. 支持默认值功能 
    3. 不再支持Serializable数据传输,改为性能更好的 Parcelable 大对象传输 
    4. 支持多进程activity 跳转 
    5. 降低内存占用,可回收内存提升
    AutoPage
    github地址 https://github.com/smartbackme/AutoPage 
    如果觉得不错 github 给个星 Android 容易的跳转工具
    注意事项:
    必须有如下两个要求
    androidx
    kotlin & java
    支持传输类型
    bundle 支持的基本类型都支持(除ShortArray) 以下类型都支持,如果类型不是如下类型,可能会报kapt错误

    :Parcelable
    String
    Long
    Int
    Boolean
    Char
    Byte
    Float
    Double
    Short
    CharSequence
    CharArray
    IntArray
    LongArray
    BooleanArray
    DoubleArray
    FloatArray
    ByteArray
    ArrayList
    ArrayList<:Parcelable>
    Array<:Parcelable>
    ###使用

    project : build.gradle 项目的gradle配置

       buildscript { repositories { maven { url 'https://www.jitpack.io' } } 

    在你的每个需要做容易跳转的模块添加如下配置 

    3. 你的项目必须要支持 kapt 

    4. kotlin kapt 

    5. 你的项目必须支持 @Parcelize 注解 也就是必须添加 

    apply plugin: 'kotlin-android-extensions'

    apply plugin: 'kotlin-android-extensions'
    apply plugin: 'kotlin-kapt'
    android { androidExtensions { experimental = true } }


    kapt com.github.smartbackme.AutoPage:autopage-processor:1.0.7
    implementation com.github.smartbackme.AutoPage:autopage:1.0.7

    重点

    1. @AutoPage 只能在字段或者类上标注
    1. Ap 作为前缀,为你快速跳转

    kotlin: 

    1. 字段必须标注 @JvmField 和 @AutoPage 

    2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)

    java: 

    1. 字段必须标注 @AutoPage 

    2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)

    ### Activity 中使用

    例1

    简单的跳转

    @AutoPage 
    class SimpleJump1Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_simple_jump1)
    }
    }

    之后调用

    ApSimpleJump1Activity.newInstance().start(this)

    例2

    简单的跳转并且带参数

    class MainActivity2 : AppCompatActivity() {


    @AutoPage
    @JvmField
    var message:String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main2)
    AutoJ.inject(this)
    findViewById(R.id.text).text = message
    }


     之后调用

    ApMainActivity2.newInstance().apply { message = "123" } .start(this)

    例3:

    跳转带有result

    @AutoPage class SimpleJumpResultActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {     super.onCreate(savedInstanceState)     setContentView(R.layout.activitysimplejump_result) }override fun onBackPressed() {
    var intent = Intent()
    intent.putExtra("message","123")
    setResult(RESULT_OK,intent)
    super.onBackPressed()
    }


    之后调用

    ApSimpleJumpResultActivity.newInstance().apply { requestCode = 1 }.start(this)


    例4:

    实体传输

    实体 


    @Parcelize

    data class MyData(var message:String,var hehehe: Boolean,var temp :Int):Parcelable


    class AllDataActivity : AppCompatActivity() {


    @AutoPage
    @JvmField
    var myData:MyData? = null
    @AutoPage
    @JvmField
    var message:String? = "this is default value"
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_all_data)
    AutoJ.inject(this)


    Toast.makeText(this,myData?.toString()+message,Toast.LENGTH_LONG).show()
    }


    之后调用

    ApAllDataActivity.newInstance().apply { message = "123" myData = MyData("hfafas",true,21)


    例5:

    默认值

    class DefaultValueActivity : AppCompatActivity() {


    @AutoPage
    @JvmField
    var message:String? = "this is default value"

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_default_value)
    AutoJ.inject(this)

        // var args = intent.getParcelableExtra("123")

        findViewById(R.id.button6).text = message

    }

    }


    之后调用

    ApDefaultValueActivity.newInstance().apply { } .start(this)


    # 在 fragment 中使用

    class FragmentSimpleFragment : Fragment() {


    @AutoPage
    @JvmField
    var message:String? = null

    companion object {
    fun newInstance() = FragmentSimpleFragment()
    }

    private lateinit var viewModel: SimpleViewModel

    override fun onCreateView(
    inflater:
    LayoutInflater, container: ViewGroup?,
    savedInstanceState:
    Bundle?
    )
    : View {
    return inflater.inflate(R.layout.simple_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    AutoJ.inject(this)
    viewModel = ViewModelProvider(this).get(SimpleViewModel::class.java)
    view?.findViewById(R.id.message)?.text = message

    }

    }


    之后调用

    ApFragmentSimpleFragment.newInstance().apply { message = "123" }.build()


    收起阅读 »

    线上直播 | 开门5件事:一个CTO的随想

    4月23日晚20:00邀您一起收看线上直播【科创人· 案例研习社】听环信CTO赵贵斌为您讲述【开门5件事:一个CTO的随想】

    423日晚20:00


    邀您一起收看线上直播科创人· 案例研习社


    听环信CTO赵贵斌为您讲述开门5件事:一个CTO的随想



    线上直播 | 海外应用市场生存法则详解

    当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合...
    继续阅读 »

    当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。

    除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合规难捉摸,缺少资源两眼一抹黑……这些都是老生常谈的出海难题了。


    当众多移动应用产品开始将出海作为“紧急避险”,那么这些问题就从“槽点”变成了必须尽快破解的燃眉之急。


    蝉学院大咖分享会本期课程

    蝉大师将联合环信的大咖

    一起来聊聊看国内移动应用该怎么做~

    WechatIMG6.jpegWechatIMG11.jpeg

    ____    『蝉学院2021』   ____


        2020年,蝉大师已面向全球移动应用开发者、中高端运营推广人员推出涵盖线上及线下的一系列公开课程。

        为千万用户提供了业内最新热点、用户运营、流量变现、产品推广等实战经验;并帮助相关人员了解当前互联网行业的趋势及增量技巧。

        

        2021年,蝉学院·大咖分享会系列将邀请众多行业一线大咖,快速捕捉热点、深度剖析观点,为大家提供一个交流分享、观点碰撞的全新平台。

        我们已整装待发,期待您与我们并肩同行!


    ____    『活动合集』   ____


    图片集锦.jpg


    ____    『主办方』   ____

    1蝉大师LOGOJPG图片.png

          蝉大师是App大数据分析与应用全球推广优化专家。作为Apple官方数据提供商,平台每日跟踪全球数百万款App以及各大海外信息平台实时动态,每日获取数据超过15T,为全球的上千万移动互联网应用从业者和推广者提供基础数据分析和支持。是国内首家提供全球155个国家与地区的榜单、关键词、热搜以及苹果搜索广告数据的公司,并在全国首家实现苹果ASM竞价搜索广告60个国家地区的数据查询。官网:chandashi.com



    568x186.jpg


          环信是国内领行的企业级软件服务提供商,荣膺“Gartner 2016 Cool Vendor”。旗下主要产品线包括即时通讯能力PaaS平台——环信即时通讯云,全场景音视频PaaS平台——环信实时音视频云,全媒体智能客服SaaS平台——环信客服云,以及企业级人工智能服务能力平台——环信机器人,是国内较早覆盖云通讯、云客服、智能机器人的一体化产品技术储备企服公司。

    收起阅读 »

    【14万现金奖不玩虚的】声网联合环信第三届RTE 2021创新编程挑战赛报名啦!

    第三届 RTE 2021 创新编程挑战赛开始报名啦! RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、...
    继续阅读 »

    第三届 RTE 2021 创新编程挑战赛开始报名啦!

    RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、在线教学、互动游戏、互动直播、IoT 等任何实时互动场景应用,竞争最终大奖。

    本届大赛将继续以“线上编程+线上提交+线上决赛的方式进行。不论你是高校学生、创业者、极客企业,还是个人开发者,只要你爱 Coding,都可以在这里挥洒创意,尽情创造。


    扫码加入交流群

    大赛日程安排

    官网报名——4月15日 - 5月28日

    组队开发——4月15日 - 5月28日

    作品提交——5月28日 - 6月2日

    线上决赛——6月12日


    今年的大赛有两个赛道,「应用创新挑战赛道」和「技术创新挑战赛道」,都是面向应用编程爱好者及团队的。

    赛道一:应用创新

    赛道一面向所有的应用开发者。作为大赛的传统赛道,开发者可以自由发挥想象,开发具备实时互动能力的应用。开发者可以使用包括视频/音频
    SDK、云信令 SDK、互动白板 SDK、录制 SDK、实时码流加速 SDK、云录制 SDK、环信 IM SDK
    等产品,实现创意应用,不限平台及开发语言。
    同时,今年我们还联合相芯科技、360 等合作伙伴,开放出他们的 AI 能力,开发者可以根据自己的需求进行结合,在应用中实现美颜、背景抠图等功能,给了赛道一更多的创新可能性。
    赛道一奖项设置


    一等奖:50000 元 x 1 支队伍
    二等奖:30000 元 x 1 支队伍
    三等奖:10000 元 x 1 支队伍
    环信专项奖:20000元 x 1 支队伍(详见官网说明)
    优秀奖 2000 若干
    所有获奖团队可加入声网Agora 招聘绿色通道
    所有获奖团队一年内享受声网创业支持计划的福利

    赛道一评奖规则


    评委会根据“完成度”、“创意度”、“潜在商业价值”等多个维度进行考量。点我了解详细作品要求和评奖规则。


    赛道二:技术创新

    赛道二仅面向 C++语言开发者。我们在声网音视频 SDK 的基础上,封装了两个插件接口。参赛团队可以将自己的产品或开源项目封装为插件,通过对接插件接口,让插件功能融入基于 Agora SDK 开发的各种实时互动场景中。同时利用该插件开发可运行演示的 Demo。
    目前已经有多个合作伙伴通过云市场插件接口,成功将视频美颜、滤镜、变声等音视频扩展能力融入了各类实时互动场景中。我们希望通过将该插件接口开放给社区,来激发开发者的更多创造力,拓展 RTC 技术能力边界。
    赛道二奖项设置


    技术创新专项奖:20000 元*1 支队伍
    优秀奖 2000 若干
    所有获奖团队可加入声网Agora 招聘绿色通道
    所有获奖团队一年内享受声网创业支持计划的福利

    赛道二评奖规则


    本赛题提交的作品插件及功能演示Demo需能够正常运行,方可入围参与后续的评审。评委会根据“代码完整度”、“文档完整度”、“稳定性”、“创意度”等多个维度进行考量评分。点此了解详细作品要求和评奖规则。

    大赛评委

    本届大赛邀请了来自多个技术社区、团队的技术负责人和资深工程师作为评委。他们将在最后评选阶段在线上根据作品的完成度、稳定性、创意性等维度进行打分。本届的评委包括:

    开赛线上培训

    不清楚有哪些 SDK 可以使用?还不知道能用 SDK 做什么场景?作品完成后,怎么让它成为热门开源项目?

    为了解答大家的这些疑问,我们还将在 4 月 27 日、28 日组织两场线上直播,分别邀请声网、环信的产品负责人、历届编程大赛冠军分享他们的经验。

    立即报名 


    收起阅读 »

    iOS Universal Link(点击链接跳转到APP)

    Universe Link跳转流程步骤1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到2. 用text  ...
    继续阅读 »

    Universe Link跳转流程


    步骤

    1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到


    2. 用text   创建  apple-app-site-association  文件     去掉后缀!!!!!


    3.打开xcode 工程 配置下图文件


    4.在appdelegate 里面 回调接收url  获取链接里面的参数


    5.最重要的一步来了!!!!!

    用txt 把创建好的  apple-app-site-association  给后台 开发人员  将此文件 放到服务器的根目录下面 例如 https://www.baidu.com/apple-app-site-association

    重点!!!!!!!!  必须用https  

    收起阅读 »

    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--图形图像渲染原理

    引言作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的...
    继续阅读 »

    引言

    作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:

    1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。

    2.GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。

    这时候可能会产生一个问题:CPU 难道不能代替 GPU 来进行图形渲染吗?答案当然是肯定的,不过在看了下面这个视频就明白为什么要用 GPU 来进行图形渲染了。

    GPU CPU 模拟绘图视频

    使用 GPU 渲染图形的根本原因就是:速度。GPU 的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示。

    那么像素是如何绘制在屏幕上的?计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为 渲染。渲染过程中最常用的技术就是 光栅化

    关于光栅化的概念,以下图为例,假如有一道绿光与存储在内存中的一堆三角形中的某一个在三维空间坐标中存在相交的关系。那么这些处于相交位置的像素都会被绘制到屏幕上。当然这些三角形在三维空间中的前后关系也会以遮挡或部分遮挡的形式在屏幕上呈现出来。一句话总结:

    光栅化就是将数据转化成可见像素的过程。


    GPU 则是执行转换过程的硬件部件。由于这个过程涉及到屏幕上的每一个像素,所以 GPU 被设计成了一个高度并行化的硬件部件。

    下面,我们来简单了解一下 GPU 的历史。

    GPU 历史

    GPU 还未出现前,PC 上的图形操作是由 视频图形阵列(VGA,Video Graphics Array) 控制器完成。VGA 控制器由连接到一定容量的DRAM上的存储控制器和显示产生器构成。

    1997 年,VGA 控制器开始具备一些 3D 加速功能,包括用于 三角形生成、光栅化、纹理贴图 和 阴影。

    2000 年,一个单片处图形处理器继承了传统高端工作站图形流水线的几乎每一个细节。因此诞生了一个新的术语 GPU 用来表示图形设备已经变成了一个处理器。

    随着时间的推移,GPU 的可编程能力愈发强大,其作为可编程处理器取代了固定功能的专用逻辑,同时保持了基本的 3D 图形流水线组织。

    近年来,GPU 增加了处理器指令和存储器硬件,以支持通用编程语言,并创立了一种编程环境,从而允许使用熟悉的语言(包括 C/C++)对 GPU 进行编程。

    如今,GPU 及其相关驱动实现了图形处理中的 OpenGL 和 DirectX 模型,从而允许开发者能够轻易地操作硬件。OpenGL 严格来说并不是常规意义上的 API,而是一个第三方标准(由 khronos 组织制定并维护),其严格定义了每个函数该如何执行,以及它们的输出值。至于每个函数内部具体是如何实现的,则由 OpenGL 库的开发者自行决定。实际 OpenGL 库的开发者通常是显卡的生产商。DirectX 则是由 Microsoft 提供一套第三方标准。

    GPU 图形渲染流水线


    GPU 图形渲染流水线的主要工作可以被划分为两个部分:

    把 3D 坐标转换为 2D 坐标

    把 2D 坐标转变为实际的有颜色的像素

    GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。

    顶点着色器(Vertex Shader)
    形状装配(Shape Assembly),又称 图元装配
    几何着色器(Geometry Shader)
    光栅化(Rasterization)
    片段着色器(Fragment Shader)
    测试与混合(Tests and Blending)


    第一阶段,顶点着色器。该阶段的输入是 顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。

    第二阶段,形状(图元)装配。该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。

    第三阶段,几何着色器。该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

    第四阶段,光栅化。该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。

    第五阶段,片段着色器。该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。

    第六阶段,测试与混合。该阶段会检测片段的对应的深度值(z 坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

    关于混合,GPU 采用如下公式进行计算,并得出最后的颜色。

    R = S + D * (1 - Sa)

    关于公式的含义,假设有两个像素 S(source) 和 D(destination),S 在 z 轴方向相对靠前(在上面),D 在 z 轴方向相对靠后(在下面),那么最终的颜色值就是 S(上面像素) 的颜色 + D(下面像素) 的颜色 * (1 - S(上面像素) 颜色的透明度)。

    上述流水线以绘制一个三角形为进行介绍,可以为每个顶点添加颜色来增加图形的细节,从而创建图像。但是,如果让图形看上去更加真实,需要足够多的顶点和颜色,相应也会产生更大的开销。为了提高生产效率和执行效率,开发者经常会使用 纹理(Texture) 来表现细节。纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)。纹理一般可以直接作为图形渲染流水线的第五阶段的输入。

    最后,我们还需要知道上述阶段中的着色器事实上是一些程序,它们运行在 GPU 中成千上万的小处理器核中。这些着色器允许开发者进行配置,从而可以高效地控制图形渲染流水线中的特定部分。由于它们运行在 GPU 中,因此可以降低 CPU 的负荷。着色器可以使用多种语言编写,OpenGL 提供了 GLSL(OpenGL Shading Language) 着色器语言。

    GPU 存储系统

    早期的 GPU,不同的着色器对应有着不同的硬件单元。如今,GPU 流水线则使用一个统一的硬件来运行所有的着色器。此外,nVidia 还提出了 CUDA(Compute Unified Device Architecture) 编程模型,可以允许开发者通过编写 C 代码来访问 GPU 中所有的处理器核,从而深度挖掘 GPU 的并行计算能力。

    下图所示为 GPU 内部的层级结构。最底层是计算机的系统内存,其次是 GPU 的内部存储,然后依次是两级 cache:L2 和 L1,每个 L1 cache 连接至一个 流处理器(SM,stream processor)。

    SM L1 Cache 的存储容量大约为 16 至 64KB。

    GPU L2 Cache 的存储容量大约为几百 KB。

    GPU 的内存最大为 12GB。

    GPU 上的各级存储系统与对应层级的计算机存储系统相比要小不少。

    此外,GPU 内存并不具有一致性,也就意味着并不支持并发读取和并发写入。


    GPU 流处理器

    下图所示为 GPU 中每个流处理器的内部结构示意图。每个流处理器集成了一个 L1 Cache。顶部是处理器核共享的寄存器堆。


    CPU-GPU 异构系统

    至此,我们大致了解了 GPU 的工作原理和内部结构,那么实际应用中 CPU 和 GPU 又是如何协同工作的呢?

    下图所示为两种常见的 CPU-GPU 异构架构。

    左图是分离式的结构,CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。

    右图是耦合式的结构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。

    注意,目前很多 SoC 都是集成了CPU 和 GPU,事实上这仅仅是在物理上进行了集成,并不意味着它们使用的就是耦合式结构,大多数采用的还是分离式结构。耦合式结构是在系统上进行了集成。

    在存储管理方面,分离式结构中 CPU 和 GPU 各自拥有独立的内存,两者共享一套虚拟地址空间,必要时会进行内存拷贝。对于耦合式结构,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理。

    图形应用程序调用 OpenGL 或 Direct3D API 功能,将 GPU 作为协处理器使用。API 通过面向特殊 GPU 优化的图形设备驱动向 GPU 发送命令、程序、数据。

    GPU 资源管理模型

    下图所示为分离式异构系统中 GPU 的资源管理模型示意图。


    MMIO(Memory-Mapped I/O)

    CPU 通过 MMIO 访问 GPU 的寄存器状态。
    通过 MMIO 传送数据块传输命令,支持 DMA 的硬件可以实现块数据传输。

    GPU Context

    上下文表示 GPU 的计算状态,在 GPU 中占据部分虚拟地址空间。多个活跃态下的上下文可以在 GPU 中并存。

    CPU Channel

    来自 CPU 操作 GPU 的命令存储在内存中,并提交至 GPU channel 硬件单元。
    每个 GPU 上下文可拥有多个 GPU Channel。每个 GPU 上下文都包含 GPU channel 描述符(GPU 内存中的内存对象)。
    每个 GPU Channel 描述符存储了channel 的配置,如:其所在的页表。
    每个 GPU Channel 都有一个专用的命令缓冲区,该缓冲区分配在 GPU 内存中,通过 MMIO 对 CPU 可见。

    GPU 页表

    GPU 上下文使用 GPU 页表进行分配,该表将虚拟地址空间与其他地址空间隔离开来。
    GPU 页表与 CPU 页表分离,其驻留在 GPU 内存中,物理地址位于 GPU 通道描述符中。
    通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行。
    GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址,还转换为主机物理地址。这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中,从而构成一个完成的虚拟地址空间。

    PFIFO Engine

    PFIFO 是一个提交 GPU 命令的特殊引擎。
    PFIFO 维护多个独立的命令队列,即 channel。
    命令队列是带有 put 和 get 指针的环形缓冲器。
    PFIFO 引擎会拦截多有对通道控制区域的访问以供执行。
    GPU 驱动使用一个通道描述符来存储关联通道的设置。

    BO

    缓冲对象(Buffer Object)。一块内存,可以用来存储纹理,渲染对象,着色器代码等等。

    CPU-GPU 工作流

    下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:

    1.将主存的处理数据复制到显存中

    2.CPU 指令驱动 GPU

    3.GPU 中的每个运算单元并行处理

    4.GPU 将显存结果传回主存


    屏幕图像显示原理

    介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。


    下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。


    最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,GPU 通常会引入两个缓冲区,即 双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。


    双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:


    为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

    摘自:http://chuquan.me/2018/08/26/graphics-rending-principle-gpu

    收起阅读 »

    快速搭建Android项目-QMUI_Android

    QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目...
    继续阅读 »

    QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。

    功能特性

    全局 UI 配置

    只需要修改一份配置表就可以调整 App 的全局样式,包括组件颜色、导航栏、对话框、列表等。一处修改,全局生效。

    丰富的 UI 控件

    提供丰富常用的 UI 控件,例如 BottomSheet、Tab、圆角 ImageView、下拉刷新等,使用方便灵活,并且支持自定义控件的样式。

    高效的工具方法

    提供高效的工具方法,包括设备信息、屏幕信息、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。

    开始使用

    qmui

    1. 引入库

    最新的库会上传到 JCenter 仓库上,请确保配置了 JCenter 仓库源,然后直接引用:

    implementation 'com.qmuiteam:qmui:2.0.0-alpha10'
    至此,QMUI 已被引入项目中。

    2. 配置主题

    把项目的 theme 的 parent 指向 QMUI.Compat,至此,QMUI 可以正常工作。

    3. 覆盖组件的默认表现

    你可以通过在项目中的 theme 中用 <item name="(name)">(value)</item> 的形式来覆盖 QMUI 组件的默认表现。具体可指定的属性名请参考 @style/QMUI.Compat 或 @style/QMUI 中的属性。

    arch

    1. 引入库

    最新的库会上传到 JCenter 仓库上,请确保配置了 JCenter 仓库源,然后直接引用:

    def qmui_arch_version = '2.0.0-alpha10'
    implementation "com.qmuiteam:arch:$qmui_arch_version"
    kapt "com.qmuiteam:arch-compiler:$qmui_arch_version" // use annotationProcessor if java

    2. 在 Application 里初始化

    override fun onCreate() {
    super.onCreate()
    QMUISwipeBackActivityManager.init(this)
    }

    然后就可以使用 arch 库提供的 QMUIFragment、QMUIFragmentActivity、QMUIActivity 来作为基础类构建自己的界面了。

    3. proguard

    -keep class **_FragmentFinder { *; }
    -keep class androidx.fragment.app.* { *; }

    -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMap { *; }
    -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMapImpl { *; }

    -keep class com.qmuiteam.qmui.arch.scheme.SchemeMap {*;}
    -keep class com.qmuiteam.qmui.arch.scheme.SchemeMapImpl {*;}

    代码下载:QMUI_Android-master.zip

    原文链接:https://github.com/Tencent/QMUI_Android


    收起阅读 »

    Web 安全 之 Clickjacking

    Clickjacking ( UI redressing )在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。什么是点击劫持点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了...
    继续阅读 »

    Clickjacking ( UI redressing )

    在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。

    什么是点击劫持

    点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。

    例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe 合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF 攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF 则是在用户不知情或者没有输入的情况下伪造整个请求。


    针对 CSRF 攻击的防御措施通常是使用 CSRF token(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token 缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token 也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe 中。

    如何构造一个基本的点击劫持攻击

    点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe 嵌入并隐藏。使用样式标签和参数的示例如下:

    <head>
    <style>
    #target_website {
    position:relative;
    width:128px;
    height:128px;
    opacity:0.00001;
    z-index:2;
    }
    #decoy_website {
    position:absolute;
    width:300px;
    height:400px;
    z-index:1;
    }
    </style>
    </head>
    ...
    <body>
    <div id="decoy_website">
    ...decoy web content here...
    </div>
    <iframe id="target_website" src="https://vulnerable-website.com">
    </iframe>
    </body>

    目标网站 iframe 被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index 决定了 iframe 和网站图层的堆叠顺序。透明度被设置为零,因此 iframe 内容对用户是透明的。浏览器可能会基于 iframe 透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。

    预填写输入表单

    一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。

    Frame 拦截脚本

    只要网站可以被 frame ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame 的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame 拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为:

    • 检查并强制当前窗口是主窗口或顶部窗口
    • 使所有 frame 可见。
    • 阻止点击可不见的 frame
    • 拦截并标记对用户的潜在点击劫持攻击。

    Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe 的 sandbox 属性去规避 frame 拦截。当 iframe 的 sandbox 设置为 allow-forms 或 allow-scripts,且 allow-top-navigation 被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口:

    <iframe id="victim_website" src="https://victim-website.com" sandbox="allow-forms"></iframe>

    当 iframe 的 allow-forms 和 allow-scripts 被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。

    结合使用点击劫持与 DOM XSS 攻击

    到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。

    多步骤点击劫持

    攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。

    如何防御点击劫持攻击

    我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。

    点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。

    X-Frame-Options

    X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站:

    X-Frame-Optionsdeny

    或者使用 sameorigin 限制为只有同源网站可以引用:

    X-Frame-Optionssameorigin

    或者使用 allow-from 指定白名单:

    X-Frame-Options: allow-from https://normal-website.com

    X-Frame-Options 在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from)。然而,作为多层防御策略中的一部分,其与 Content Security Policy 结合使用时,可以有效地防止点击劫持攻击。

    Content Security Policy

    Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为:

    Content-Security-Policypolicy

    其中的 policy 是一个由分号分隔的策略指令字符串。CSP 向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。

    有关点击劫持的防御,建议在 Content-Security-Policy 中增加 frame-ancestors 策略。

    • frame-ancestors 'none' 类似于 X-Frame-Options: deny ,表示拒绝所有 iframe 引用。
    • frame-ancestors 'self' 类似于 X-Frame-Options: sameorigin ,表示只允许同源引用。

    示例:

    Content-Security-Policyframe-ancestors 'self';

    或者指定网站白名单:

    Content-Security-Policyframe-ancestors normal-website.com;

    为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。

    原文链接:https://segmentfault.com/a/1190000039341244

    收起阅读 »

    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



    收起阅读 »

    Web 安全 之 Directory traversal

    Directory traversal - 目录遍历在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。什么是目录遍历?目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程...
    继续阅读 »

    Directory traversal - 目录遍历

    在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。


    什么是目录遍历?

    目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。

    通过目录遍历读取任意文件

    假设某个应用程序通过如下 HTML 加载图像:

    ![](/loadImage?filename=218.png)

    这个 loadImage URL 通过 filename 文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/ 的磁盘上。应用程序基于此基准路径与请求的 filename 文件名返回如下路径的图像:

    /var/www/images/218.png

    如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件:

    https://insecure-website.com/loadImage?filename=../../../etc/passwd

    这将导致如下路径的文件被返回:

    /var/www/images/../../../etc/passwd

    ../ 表示上级目录,因此这个文件其实就是:

    /etc/passwd

    在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。

    在 Windows 系统上,..\ 和 ../ 的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式:

    https://insecure-website.com/loadImage?filename=..\..\..\windows\win.ini

    利用文件路径遍历漏洞的常见障碍

    许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。

    如果应用程序从用户输入的 filename 中剥离或阻止 ..\ 目录遍历序列,那么也可以使用各种技巧绕过防御。

    你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd 这样直接引用文件而不使用任何 ..\ 形式的遍历序列。

    你也可以嵌套的遍历序列,例如 ....// 或者 ....\/ ,即使内联序列被剥离,其也可以恢复为简单的遍历序列。

    你还可以使用各种非标准编码,例如 ..%c0%af 或者 ..%252f 以绕过输入过滤器。

    如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images ,则可以使用后跟遍历序列的方式绕过,例如:

    filename=/var/www/images/../../../etc/passwd

    如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png ,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查:

    filename=../../../etc/passwd%00.png

    如何防御目录遍历攻击

    防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。

    如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施:

    • 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。
    • 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。

    下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径:

    File file = new File(BASE_DIRECTORY, userInput);
    if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
    // process file
    }

    原文链接:https://segmentfault.com/a/1190000039307155


    收起阅读 »

    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;
    }