注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

SpringBoot 三大开发工具,你都用过么?

1、SpringBoot Dedevtools 他是一个让SpringBoot支持热部署的工具,下面是引用的方法 要么在创建项目的时候直接勾选下面的配置: 要么给springBoot项目添加下面的依赖: <dependency> <...
继续阅读 »

1、SpringBoot Dedevtools


他是一个让SpringBoot支持热部署的工具,下面是引用的方法


要么在创建项目的时候直接勾选下面的配置:


图片


要么给springBoot项目添加下面的依赖:


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>



  • idea修改完代码后再按下 ctrl + f9 使其重新编译一下,即完成了热部署功能




  • eclipse是按ctrl + s保存 即可自动编译




如果你想一修改代码就自动重新编译,无需按ctrl+f9。只需要下面的操作:


1.在idea的setting中把下面的勾都打上


图片


2.进入pom.xml,在build的反标签后给个光标,然后按Alt+Shift+ctrl+/


图片


3.然后勾选下面的东西,接着重启idea即可


图片


2、Lombok


Lombok是简化JavaBean开发的工具,让开发者省去构造器,getter,setter的书写。


在项目初始化时勾选下面的配置,即可使用Lombok


图片


或者在项目中导入下面的依赖:


<dependency>    
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

使用时,idea还需要下载下面的插件:


图片


下面的使用的例子


@AllArgsConstructor
//全参构造器
@NoArgsConstructor
//无参构造器
@Data//getter + setterpublic
class User {
private Long id;
private String name;
private Integer age;
private String email;
}

3、Spring Configuration Processor


该工具是给实体类的属性注入开启提示,自我感觉该工具意义不是特别大!


因为SpringBoot存在属性注入,比如下面的实体类:


@Component
@ConfigurationProperties(prefix = "mypet")
public class Pet {
private String nickName;
private String strain;

public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getStrain() {
return strain;
}
public void setStrain(String strain) {
this.strain = strain;
}
@Override public String toString() {
return "Pet [nickName=" + nickName + ", strain=" + strain + "]";
}
}

想要在application.properties和application.yml中给mypet注入属性,却没有任何的提示,为了解决这一问题,我们在创建SpringBoot的时候勾选下面的场景:


图片


或者直接在项目中添加下面的依赖:


<dependency>     
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

并在build的标签中排除对该工具的打包:(减少打成jar包的大小)


<build>    
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins></build>


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

过年想要红包?年前你先把咱们的红包系统上线了呗!

红包分类 产品需求设计分为两类红包,个人红包,群红包。群红包又分为专属、均分、群手气三种。分别适应不同的场景。如下图所示: 红包实现 发红包流程: 1、用户进入发红包界面发起请求; 2、服务端接受到请求后,对用户的红包金额进行冻结(前提用户事先开通余额账户)...
继续阅读 »

红包分类


产品需求设计分为两类红包,个人红包,群红包。群红包又分为专属、均分、群手气三种。分别适应不同的场景。如下图所示:
image.png


红包实现


发红包流程:


1、用户进入发红包界面发起请求;
2、服务端接受到请求后,对用户的红包金额进行冻结(前提用户事先开通余额账户)。
3、是否余额充足(兜底教研),如果充足发红包成功,并且生成红包记录,如果不充足提示错误信息。
4、推送最终结果给用户,如果发成功了会推两条消息,一个是发送人告诉用户红包发成功了,且推送群/个人立即领取。
5、如果红包发成功后,发一个延迟1天的 MQ 消息,做一个超期未退款处理。把冻结账户的钱返还给用户。
image.png


领红包流程:


当用户收到红包的推送过后,用户就可以通过该消息,进行红包的领取。
1、参数状态判断,判断红包是否过期,红包是否领完,红包是否重复领取等逻辑;
2、生成红包领取记录;
3、生成红包入账记录,对领取者上账。生成余额流水,并且增加余额流水。
4、减少发红包者的冻结余额,完成红包领取流程。
未命名文件.png


红包高并发


高并发设计


对于群手气红包肯定会存在并发问题,比如微信群红包领取的时候。一个 200 人的群同时来领取1个 200 元,10 人可以领取的红包,从用户的发起可能到领取基本是在 1-2 秒内领完。
怎么样保证既能高效的领取,又能保证金额都能正确,且不会记错账。我们采用的方案就是“一快一慢”的方案。


1、对于群红包的场景,我们首先会将红包金额提前计算。然后存储到 Redis 中 Key : redpacket:amount_list:#{id}存储的结构是一个 List 集合。
2、 每次领取的时候我们会做一个 rpop操作,获取到红包的金额。由于这块是redis 操作,是非常高效的。
image.png
3、为了保证数据的持久性、可靠性。我们会生成一个领取记录到 mysql 数据库中持久化。
4、然后发送红包领取成功的消息出去,在记账服务中进行订阅,异步入账。


具体的流程如下:
image.png


幂等性保证


为了保证领取过程用户有序的领取,且保证一个用户只能领取成功一次,如果第二次来领取,咱们就提示已经领取过了,不能重复领取。这是我们在高并发场景下必须严保证的问题。当时我们选择的是通过分布式锁的方式来解决的,锁的 key 设计如下:
redpacket:amount_list:#{id}_#{user_id}
这样设计的好处就是能够保证一当前分布式系统中,当前只能一个有效的请求进入正真的处理逻辑中。
兜底保障:
1、在领取记录表中增加 user_id, redpacket_id 为唯一索引。
2、对红包的剩余金额做乐观锁更新(可以使用 tk.mapper 的 @Version)。


可拓展性设计


为了保证可拓展性的设计,我们当时采用的是 策略 + 模板方法的设计模型进行低耦合设计。


手气红包金额计算


我们采用的是中位数随机算法(大致的逻辑就是控制一个中位数的值最大金额,最小金额的区间不能超过中位数的浮动水位线),更多的随机算法,大家可以参阅:为啥春节抢红包总不是手气最佳?看完微信抢红包算法你就明白了!


金额随机代码


public class RedPacketUtils {

private static final Random random = new Random();


/**
* 根据总数分割个数及限定区间进行数据随机处理
* 数列浮动阀值为0.95
*
* @param totalMoney - 被分割的总数
* @param splitNum - 分割的个数
* @param min - 单个数字下限
* @param max - 单个数字上限
* @return - 返回符合要求的数字列表
*/
public static List<BigDecimal> genRandomList(BigDecimal totalMoney, Integer splitNum, BigDecimal min, BigDecimal max) {
totalMoney = totalMoney.multiply(new BigDecimal(100));
min = min.multiply(new BigDecimal(100));
max = max.multiply(new BigDecimal(100));
List<Integer> li = genRandList(totalMoney.intValue(), splitNum, min.intValue(), max.intValue(), 0.95f);
List<BigDecimal> randomList = new CopyOnWriteArrayList<>();
for (Integer v : li) {
BigDecimal randomVlue = new BigDecimal(v).divide(new BigDecimal(100));
randomList.add(randomVlue);
}

randomList = randomArrayList(randomList);
return randomList;
}

/**
* 根据总数分割个数及限定区间进行数据随机处理
*
* @param total - 被分割的总数
* @param splitNum - 分割的个数
* @param min - 单个数字下限
* @param max - 单个数字上限
* @param thresh - 数列浮动阀值[0.0, 1.0]
*/
public static List<Integer> genRandList(int total, int splitNum, int min, int max, float thresh) {
assert total >= splitNum * min && total <= splitNum * max : "请校验红包参数设置的合理性";
assert thresh >= 0.0f && thresh <= 1.0f;
// 平均分配
int average = total / splitNum;
List<Integer> list = new ArrayList<>(splitNum);
int rest = total - average * splitNum;
for (int i = 0; i < splitNum; i++) {
if (i < rest) {
list.add(average + 1);
} else {
list.add(average);
}
}
// 如果浮动阀值为0则不进行数据随机处理
if (thresh == 0) {
return list;
}
// 根据阀值进行数据随机处理
int randOfRange = 0;
int randRom = 0;
int nextIndex = 0;
int nextValue = 0;
int surplus = 0;//多余
int lack = 0;//缺少
for (int i = 0; i < splitNum - 1; i++) {
nextIndex = i + 1;
int itemThis = list.get(i);
int itemNext = list.get(nextIndex);
boolean isLt = itemThis < itemNext;
int rangeThis = isLt ? max - itemThis : itemThis - min;
int rangeNext = isLt ? itemNext - min : max - itemNext;
int rangeFinal = (int) Math.ceil(thresh * (Math.min(rangeThis, rangeNext) + 100));
randOfRange = random.nextInt(rangeFinal);
randRom = isLt ? 1 : -1;
int iValue = list.get(i) + randRom * randOfRange;
nextValue = list.get(nextIndex) + randRom * randOfRange * -1;
if (iValue > max) {
surplus += (iValue - max);
list.set(i, max);
} else if (iValue < min) {
list.set(i, min);
lack += (min - iValue);
} else {
list.set(i, iValue);
}
list.set(nextIndex, nextValue);
}
if (nextValue > max) {
surplus += (nextValue - max);
list.set(nextIndex, max);
}
if (nextValue < min) {
lack += (min - nextValue);
list.set(nextIndex, min);
}
if (surplus - lack > 0) {//钱发少了 给低于max的凑到max
for (int i = 0; i < list.size(); i++) {
int value = list.get(i);
if (value < max) {
int tmp = max - value;
if (surplus >= tmp) {
surplus -= tmp;
list.set(i, max);
} else {
list.set(i, value + surplus);
return list;
}
}
}
} else if (lack - surplus > 0) {//钱发多了 给超过高于min的人凑到min
for (int i = 0; i < list.size(); i++) {
int value = list.get(i);
if (value > min) {
int tmp = value - min;
if (lack >= tmp) {
lack -= tmp;
list.set(i, min);
} else {
list.set(i, min + tmp - lack);
return list;
}
}
}
}
return list;
}

/**
* 打乱ArrayList
*/
public static List<BigDecimal> randomArrayList(List<BigDecimal> sourceList) {
if (sourceList == null || sourceList.isEmpty()) {
return sourceList;
}
List<BigDecimal> randomList = new CopyOnWriteArrayList<>();
do {
int randomIndex = Math.abs(new Random().nextInt(sourceList.size()));
randomList.add(sourceList.remove(randomIndex));
} while (sourceList.size() > 0);

return randomList;
}


public static void main(String[] args) {
Long startTi = System.currentTimeMillis();
List<BigDecimal> li = genRandomList(new BigDecimal(100000), 26000, new BigDecimal(2), new BigDecimal(30));
li = randomArrayList(li);
BigDecimal total = BigDecimal.ZERO;
System.out.println("======total=========total:" + total);
System.out.println("======size=========size:" + li.size());
Long endTi = System.currentTimeMillis();
System.out.println("======耗时=========:" + (endTi - startTi) / 1000 + "秒");
}
}

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

SpringBoot:如何优雅地进行参数传递、响应数据封装、异常处理?

在项目开发中,接口与接口之间、前后端之间的数据传输都使用 JSON 格式。 1 fastjson使用 阿里巴巴的 fastjson是目前应用最广泛的JSON解析框架。本文也将使用fastjson。 1.1 引入依赖 <dependency> &nb...
继续阅读 »

在项目开发中,接口与接口之间、前后端之间的数据传输都使用 JSON 格式。


1 fastjson使用


阿里巴巴的 fastjson是目前应用最广泛的JSON解析框架。本文也将使用fastjson。


1.1 引入依赖


<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.35</version>
</dependency>

2 统一封装返回数据


在web项目中,接口返回数据一般要包含状态码、信息、数据等,例如下面的接口示例:


import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/8/21 14:55
 * @description
 * @modify
 */
@RestController
@RequestMapping(value = "/test", method = RequestMethod.GET)
public class TestController {
    @RequestMapping("/json")
    public JSONObject test() {
        JSONObject result = new JSONObject();
        try {
            // 业务逻辑代码
            result.put("code", 0);
            result.put("msg", "操作成功!");
            result.put("data", "测试数据");
        } catch (Exception e) {
            result.put("code", 500);
            result.put("msg", "系统异常,请联系管理员!");
        }
        return result;
    }
}

这样的话,每个接口都这样处理,非常麻烦,需要一种更优雅的实现方式。


2.1 定义统一的JSON结构


统一的 JSON 结构中属性包括数据、状态码、提示信息,其他项可以自己根据需要添加。一般来说,应该有默认的返回结构,也应该有用户指定的返回结构。由于返回数据类型无法确定,需要使用泛型,代码如下:


public class ResponseInfo<T> {
    /**
     * 状态码
     */
    protected String code;
    /**
     * 响应信息
     */
    protected String msg;
    /**
     * 返回数据
     */
    private T data;

    /**
     * 若没有数据返回,默认状态码为 0,提示信息为“操作成功!”
     */
    public ResponseInfo() {
        this.code = 0;
        this.msg = "操作成功!";
    }

    /**
     * 若没有数据返回,可以人为指定状态码和提示信息
     * @param code
     * @param msg
     */
    public ResponseInfo(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 有数据返回时,状态码为 0,默认提示信息为“操作成功!”
     * @param data
     */
    public ResponseInfo(T data) {
        this.data = data;
        this.code = 0;
        this.msg = "操作成功!";
    }

    /**
     * 有数据返回,状态码为 0,人为指定提示信息
     * @param data
     * @param msg
     */
    public ResponseInfo(T data, String msg) {
        this.data = data;
        this.code = 0;
        this.msg = msg;
    }
    // 省略 get 和 set 方法
}

2.2 使用统一的JSON结构


我们封装了统一的返回数据结构后,在接口中就可以直接使用了。如下:


import com.example.demo.model.ResponseInfo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/8/21 14:55
 * @description
 * @modify
 */
@RestController
@RequestMapping(value = "/test", method = RequestMethod.GET)
public class TestController {
    @RequestMapping("/json")
    public ResponseInfo test() {
        try {
            // 模拟异常业务代码
            int num = 1 / 0;
            return new ResponseInfo("测试数据");
        } catch (Exception e) {
            return new ResponseInfo(500, "系统异常,请联系管理员!");
        }
    }
}

如上,接口的返回数据处理便优雅了许多。针对上面接口做个测试,启动项目,通过浏览器访问:localhost:8096/test/json,得到响应结果:


{"code":500,"msg":"系统异常,请联系管理员!","data":null}

3 全局异常处理


3.1 系统定义异常处理


新建一个 ExceptionHandlerAdvice 全局异常处理类,然后加上 @RestControllerAdvice 注解即可拦截项目中抛出的异常,如下代码中包含了几个异常处理,如参数格式异常、参数缺失、系统异常等,见下例:


@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {

    // 参数格式异常处理
    @ExceptionHandler({IllegalArgumentException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo badRequestException(IllegalArgumentException exception) {
     log.error("参数格式不合法:" + e.getMessage());
        return new ResponseInfo(HttpStatus.BAD_REQUEST.value() + "", "参数格式不符!");
    }

 // 权限不足异常处理
    @ExceptionHandler({AccessDeniedException.class})
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ResponseInfo badRequestException(AccessDeniedException exception) {
        return new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", exception.getMessage());
    }

 // 参数缺失异常处理
    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo badRequestException(Exception exception) {
        return new ResponseInfo(HttpStatus.BAD_REQUEST.value() + "", "缺少必填参数!");
    }

    // 空指针异常
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo handleTypeMismatchException(NullPointerException ex) {
        log.error("空指针异常,{}", ex.getMessage());
        return new JsonResult("500", "空指针异常");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUnexpectedServer(Exception ex) {
        log.error("系统异常:", ex);
        return new JsonResult("500", "系统发生异常,请联系管理员");
    }
    
    // 系统异常处理
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo exception(Throwable throwable) {
        log.error("系统异常", throwable);
        return new ResponseInfo(HttpStatus.INTERNAL_SERVER_ERROR.value() + "系统异常,请联系管理员!");
    }
}


  1. @RestControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。

  2. @RestControllerAdvice 注解包含了 @ResponseBody 注解,为了异常处理完之后给调用方输出一个 JSON 格式的封装数据。

  3. @RestControllerAdvice 注解还有个 basePackages 属性,该属性用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。

  4. 在方法上通过 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,最后将结果通过统一的 JSON 结构体返回给调用者。

  5. 但在项目中,我们一般都会比较详细地去拦截一些常见异常,拦截 Exception 虽然可以一劳永逸,但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下面,如果都没有找到,最后再拦截一下 Exception 异常,保证输出信息友好。


下面我们通过一个接口来进行测试:


@RestController
@RequestMapping(value = "/test", method = RequestMethod.POST)
public class TestController {
    @RequestMapping("/json")
    public ResponseInfo test(@RequestParam String userName, @RequestParam String password) {
        try {
            String data = "登录用户:" + userName + ",密码:" + password;
            return new ResponseInfo("0", "操作成功!", data);
        } catch (Exception e) {
            return new ResponseInfo("500", "系统异常,请联系管理员!");
        }
    }
}

接口调用,password这项故意空缺:


MarkerHub


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

建议收藏 | SpringBoot 元数据配置原来可以这么拓展!

一、背景 最近在调试reactive-steams源码的时候看到spring-boot源码包里面的 spring-configuration-metadata.json additional-spring-configuration-metadata....
继续阅读 »

一、背景


最近在调试reactive-steams源码的时候看到spring-boot源码包里面的




  • spring-configuration-metadata.json




  • additional-spring-configuration-metadata.json





说实话主要是metadata吸引了我,因为最近在调整引擎元数据管理确实折腾了很久。


查了官方的资料发现这里也是 SpringBoot 提供的元数据配置拓展,但是这里的元数据不是只在 Spring bean 管理的元数据类似。


▐ 官方解释




访问地址:docs.spring.io/spring-boot…



简单点可以理解为这类元数据的配置时为了让我们在使用 IDEA 开发的过程中,使用application.properties或者 application.yml配置的时候更有注释说明,更方便我们开发使用。


▐ 官方案例


以我们常用的 logging 配置为例




  • 元数据配置







  • 定义配置



二、应用实例


▐ 插件工厂配置定义


配置元数据文件位于 jar 下面。META-INF/spring-configuration-metadata.json它们使用简单的 JSON 格式,其中的项目分类在“groups”或“properties”下



{
"properties": [
{
"name": "plugin-cache.basePackage",
"type": "java.lang.String",
"description": "文档扫描包路径。"
},
{
"name": "plugin-cache.title",
"type": "java.lang.String",
"description": "Plugin Cache 插件工厂"
},
{
"name": "plugin-cache.description",
"type": "java.lang.String",
"description": "插件工厂描述"
},
{
"name": "plugin-cache.version",
"type": "java.lang.String",
"defaultValue": "V1.0",
"description": "版本。"
}
]
}

大部分元数据文件是在编译时通过处理所有带注释的项目自动生成的


@ConfigurationProperties 可以查看先前的文章


@EnableConfigurationProperties 的工作原理


参考下面 properties 表格进行配置上的理解。



deprecation 每个 properties 元素的属性中包含的 JSON 对象可以包含以下属性:



▐ 插件工厂配置注入


@Data
@Component
@ConfigurationProperties(PluginCacheProperties.PREFIX)
class PluginCacheProperties {

public static final String PREFIX = "plugin-cache";

/**
* 文档扫描包路径
*/
private String basePackage = "";

/**
* Plugin Cache 插件工厂
*/
private String title = "Plugin Cache 插件工厂";

/**
* 服务文件介绍
*/
private String description = "插件缓存说明";

/**
* 版本
*/
private String version = "V1.0";

/**
* 默认编码
*/
private String charset="UTF-8";

}

▐ 配置应用




三、总结


对于元数据配置,理解起来不难!主要为了组件库为了让使用者更加优化使用提供的一套 IDEA 提示说明。借此我们在开放私有组件或者插件的时候在对于配置项可对外提供开放能力,可以根据元数据配置来完善 IDEA 提示说明。这样其他人用起来的时候能很快知道对应的参数的配置类型以及相关的配置属性说明。总结本篇文章希望对从事相关工作的同学能够有所帮助或者启发


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

KTV歌词解析, 音准评分组件

iOS
KTV歌词解析, 音准评分组件介绍支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分欢迎各位大佬提交PR, 有问题提issue, 我会不定时fixGithub使用方法初始化    private...
继续阅读 »

KTV歌词解析, 音准评分组件

KTV歌词解析, 音准评分组件

介绍

支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分

欢迎各位大佬提交PR, 有问题提issue, 我会不定时fix

Github

使用方法

初始化

    private lazy var lrcScoreView: AgoraLrcScoreView = {
       let lrcScoreView = AgoraLrcScoreView(delegate: self)
       lrcScoreView.config.scoreConfig.scoreViewHeight = 100
       lrcScoreView.config.scoreConfig.emitterColors = [.systemPink]
       lrcScoreView.config.lrcConfig.lrcFontSize = .systemFont(ofSize: 15)
       return lrcScoreView
   }()

配置属性

组件base配置
    /// 评分组件配置
   public var scoreConfig: AgoraScoreItemConfigModel = .init()
   /// 歌词组件配置
   public var lrcConfig: AgoraLrcConfigModel = .init()
   /// 是否隐藏评分组件
   public var isHiddenScoreView: Bool = false
   /// 背景图
   public var backgroundImageView: UIImageView?
   /// 评分组件和歌词组件之间的间距 默认: 0
   public var spacing: CGFloat = 0
歌词配置
    /// 无歌词提示文案
   public var tipsString: String = "纯音乐,无歌词"
   /// 提示文字颜色
   public var tipsColor: UIColor = .black
   /// 提示文字大小
   public var tipsFont: UIFont = .systemFont(ofSize: 17)
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .lightGray
   /// 是否隐藏分割线
   public var isHiddenSeparator: Bool = false
   /// 默认歌词背景色
   public var lrcNormalColor: UIColor = .gray
   /// 高亮歌词背景色
   public var lrcHighlightColor: UIColor = .white
   /// 实时绘制的歌词颜色
   public var lrcDrawingColor: UIColor = .orange
   /// 歌词文字大小 默认: 15
   public var lrcFontSize: UIFont = .systemFont(ofSize: 15)
   /// 歌词高亮文字缩放大小 默认: 1.1
   public var lrcHighlightScaleSize: Double = 1.1
   /// 歌词左右两边间距
   public var lrcLeftAndRightMargin: CGFloat = 15
   /// 等待开始圆点背景色 默认: 灰色
   public var waitingViewBgColor: UIColor? = .gray
   /// 等待开始圆点大小 默认: 10
   public var waitingViewSize: CGFloat = 10
   /// 是否可以拖动歌词 默认: true
   public var isDrag: Bool = true
评分配置
    /// 评分视图高度 默认:100
   public var scoreViewHeight: CGFloat = 100
   /// 圆的起始位置: 默认: 100
   public var innerMargin: CGFloat = 100
   /// 线的高度 默认:10
   public var lineHeight: CGFloat = 10
   /// 线的宽度 默认: 120
   public var lineWidht: CGFloat = 120
   /// 默认线的背景色
   public var normalColor: UIColor = .gray
   /// 匹配后线的背景色
   public var highlightColor: UIColor = .orange
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .systemPink
   /// 是否隐藏垂直分割线
   public var isHiddenVerticalSeparatorLine: Bool = false
   /// 是否隐藏上下分割线
   public var isHiddenSeparatorLine: Bool = false
   /// 游标背景色
   public var cursorColor: UIColor = .systemPink
   /// 游标的宽
   public var cursorWidth: CGFloat = 20
   /// 游标的高
   public var cursorHeight: CGFloat = 20
   /// 是否隐藏粒子动画效果
   public var isHiddenEmitterView: Bool = false
   /// 使用图片创建粒子动画
   public var emitterImages: [UIImage]?
   /// emitterImages为空默认使用颜色创建粒子动画
   public var emitterColors: [UIColor] = [.red]
   /// 尾部动画图片
   public var tailAnimateImage: UIImage?
   /// 尾部动画颜色
   public var tailAnimateColor: UIColor? = .yellow
   /// 评分默认分数: 50
   public var defaultScore: Double = 50

事件回调

歌词Delegate
weak var delegate: AgoraLrcViewDelegate?

protocol AgoraLrcViewDelegate {
   /// 当前播放器的时间 单位: 秒
   func getPlayerCurrentTime() -> TimeInterval
   /// 获取歌曲总时长
   func getTotalTime() -> TimeInterval

   /// 设置播放器时间
   @objc
   optional func seekToTime(time: TimeInterval)
   /// 当前正在播放的歌词和进度
   @objc
   optional func currentPlayerLrc(lrc: String, progress: CGFloat)
}
歌词下载Delegate
weak var downloadDelegate: AgoraLrcDownloadDelegate?

protocol AgoraLrcDownloadDelegate {
   /// 开始下载
   @objc
   optional func beginDownloadLrc(url: String)
   /// 下载完成
   @objc
   optional func downloadLrcFinished(url: String)
   /// 下载进度
   @objc
   optional func downloadLrcProgress(url: String, progress: Double)
   /// 下载失败
   @objc
   optional func downloadLrcError(url: String, error: Error?)
   /// 下载取消
   @objc
   optional func downloadLrcCanceld(url: String)
   /// 开始解析歌词
   @objc
   optional func beginParseLrc()
   /// 解析歌词结束
   @objc
   optional func parseLrcFinished()
}
评分Delegate
weak var scoreDelegate: AgoraKaraokeScoreDelegate?

protocol AgoraKaraokeScoreDelegate {
   /// 分数实时回调
   /// score: 每次增加的分数
   /// cumulativeScore: 累加分数
   /// totalScore: 总分
   @objc optional func agoraKaraokeScore(score: Double, cumulativeScore: Double, totalScore: Double)
}

集成方式

本地pod引入,暂时使用本地pod 待后续发布cocoapods

把 'AgoraLrcScoreView' 复制到根目录, 执行pod

pod 'AgoraLrcScore', :path => "AgoraLrcScoreView"

作者:莫烦恼
来源:https://juejin.cn/post/7054443857928257550

收起阅读 »

支付宝集五福手画福字功能(含撤销操作)用Flutter如何实现?

今早一觉醒来发现支付宝一年一度的集五福活动又开始了,其中包含了一个功能就是手写福字,还包括撤销一笔,清除重写,保存相册等,那么使用Flutter应该如何实现这些功能呢?需求包含需求的具体有:界面随着用户手指的滑动显示走过轨迹,也就是对应的笔画。点击清空按钮可以...
继续阅读 »

支付宝集五福手画福字功能(含撤销操作)用Flutter如何实现?

今早一觉醒来发现支付宝一年一度的集五福活动又开始了,其中包含了一个功能就是手写福字,还包括撤销一笔,清除重写,保存相册等,那么使用Flutter应该如何实现这些功能呢?

需求

包含需求的具体有:

  • 界面随着用户手指的滑动显示走过轨迹,也就是对应的笔画。

  • 点击清空按钮可以清除所有的笔画。

  • 点击撤销按钮可以清除上一步画过的笔画。

  • 保存所写的文字样式到相册。

实现思路

显示笔画轨迹

使用Listener组件对用户手指落下、滑动和收起的动作进行监听,在onPointerDown,onPointerMove,onPointerUp3个监听方法中返回的PointerMoveEvent对象包含了手指所在的位置坐标偏移量localPosition,用户每次滑动时都会记录下轨迹经过的坐标点,这些坐标点连接起来就是一条线。其次,再配合使用CustomPainter进行画布自绘,将所有划过的点的连接成线使用画笔绘制在界面上即可。

搜集坐标点:

Listener(
child: Container(
  alignment: Alignment.center,
  color: Colors.transparent,
  width: double.infinity,
  height: MediaQuery.of(context).size.height,
),
onPointerDown: (PointerDownEvent event) {
  setState(() {
     
  });
},
onPointerMove: (PointerMoveEvent event) {
  setState(() {
     
  });
},
onPointerUp: (PointerUpEvent event) {
  setState(() {
     
  });
},
),

绘制:

@override
void paint(Canvas canvas, Size size) {
myPaint.strokeCap = StrokeCap.round;
myPaint.strokeWidth = 15.0;
if (lines.isEmpty) {
  canvas.drawPoints(PointMode.polygon, [Offset.zero, Offset.zero], myPaint);
} else {
  for (int k = 0; k < lines.length; k++) {
    for (int i = 0; i < lines[k].length - 1; i++) {
      if (lines[k][i] != Offset.zero && lines[k][i + 1] != Offset.zero) {
        canvas.drawLine(lines[k][i], lines[k][i + 1], myPaint);
      }
    }
  }
}
}

撤销与清空

图片

看到上面的代码有的人可能会比较疑惑,绘制时为什么这么复杂,还出现了双重循环。这就和撤销功能有关了,先假设不需要撤销功能,其实我们就可以直接把所有笔画的点连接到一起进行绘制就可以了,但是一旦引入了撤销功能,就要记录每一笔笔画,福字笔画是13画,那么理论上是需要记录13个笔画的,才能保证每次撤销时都能正常退回上一次画过的笔迹,所以第一反应就是使用集合将每一次笔画记录下来。而上面也说了每一个笔画其实也是多个坐标点的集合,所以所有笔画就是一个坐标点集合的集合,即:

/// 所有笔画划线集合
List<List<Offset>> _lines = [];

另外,也不难想到,我们可以轻易通过手指按下和手指手指的方法回调来区分笔画开始和结束。在两个方法中进行笔画的add和更新。

onPointerDown: (PointerDownEvent event) {
setState(() {
  _event = event;
  _points.add(_event?.localPosition ?? Offset.zero);
  _lines.add(_points);
});
},
onPointerMove: (PointerMoveEvent event) {
setState(() {
  _event = event;
  _points.add(_event?.localPosition ?? Offset.zero);
  _lines.last = _points;
});
},
onPointerUp: (PointerUpEvent event) {
setState(() {
  _event = event;
  _points.add(Offset.zero);
  _lines.last = _points;
});
_points = [];
},

而前面说的双重遍历这时也比较好理解了:

  • 第一层循环是遍历所有的笔画,遍历次数就是福字的笔画数。

  • 第二层循环是每一个笔画包括的好多个坐标点,遍历出来使用drawLine方法绘制到界面上形成一条线。

这样在进行撤销操作时,调用list的removeLast方法移除最后一项再刷新界面就能实现退回一笔的效果了,清空就是清空笔画集合。

保存到相册

保存相册主要是引入了两个插件库:permission_handlerimage_gallery_saver,一个用来获取存储权限,一个用来保存到相册。 使用RepaintBoundary组件将画布包裹起来,并指定key,在点击保存时按顺序调用如下方法先获取截图后保存即可:

RenderRepaintBoundary boundary =
  key.currentContext!.findRenderObject() as RenderRepaintBoundary;
var image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
_postBytes = byteData?.buffer.asUint8List();
var result = await ImageGallerySaver.saveImage(_postBytes!);

完整代码与demo下载

github地址

安卓手机扫码下载

作者:单总不会亏待你
来源:https://juejin.cn/post/7054833357267402788

收起阅读 »

35岁奥地利最帅总理辞职,超360万年薪加入硅谷当「码农」

1、 除了北妈,说实话你们🍋不?有意思的是,这位世界上最年轻的政付首脑,虽曾就读于在维也纳大学的法学院,但中途就辍学了。又是一个辍学的学渣逆袭的故事,为什么外国人都喜欢辍学啊,我气啊。在过去的四年里,这位35岁的年轻人一直是奥地利政治的主人。但一桩腐...
继续阅读 »
1、


先看个图,客观的说,这人帅不帅?气质可以不?

此人就是刚在年底辞职的奥地利总理。

此人名叫Sebastian Kurz,他是世界最年轻的郭嘉总理,27岁就任外交部长,31岁当选总理。

35岁辞职总理职务,转行去硅谷科技公司做打工人!

刺激不?励志不?

现在互联网这么卷了吗?35岁奥地利总理辞职转行去硅谷当码农?我们看看咋回事。

但你不得不服,有些人一出生就站在罗马城外了。

Kurz 在2021年12月辞职总理,但不到一个月的时间,他就在美国硅谷找到了薪酬最高的经理职位。
 
据估计,Kurz的年薪将超过50万欧元(约360万元),远远高于他担任总理时31.2万欧元(约226万元)的收入。不过总理200多万年薪,也算是很高了啊。

果然,资本主义太邪恶了,连人民公仆都给这么高收入,腐蚀人啊。

在政府工作了十年的Kurz,依然是世界上最年轻的外交部长和最年轻的总理保持者。

除了北妈,说实话你们🍋不?



有意思的是,这位世界上最年轻的政付首脑,虽曾就读于在维也纳大学的法学院,但中途就辍学了。

又是一个辍学的学渣逆袭的故事,为什么外国人都喜欢辍学啊,我气啊。

在过去的四年里,这位35岁的年轻人一直是奥地利政治的主人。但一桩腐败丑闻,让他的第二个总理职位戛然而止。
 
警方突袭了维也纳的各个部门,揭开了奥地利国家检察官对政府核心部门腐败的调查。
 
虽然没有提出正式指控,但调查的一部分正是关于Kurz在2013年至2017年担任外交部长期间是否使用国家资金并开具假发票以购买有利的媒体报道。
 
12月2日,星期四,Sebastian Kurz 在新闻发布会上,他宣布自己将辞去所有职务。
 
这个决定对他来说很不容易,但尽管如此,他说「我并不感到任何忧郁。因为我非常感谢在过去十年中我所经历的一切」。

瞧瞧,政客真是说话的艺术家。

2、
随后,在不到一个月的时间里,他转头去找了老朋友,一位在美国硅谷的大资本家 Thiel资本的创始人Peter Thiel。

就是那个和如今的世界首富 马斯克年轻时候一起创造 第一个移动支付paypal的家伙。他们俩也是靠paypal捞到了第一桶金。

如今 Thiel 身家91.3亿美元,排名世界富豪279名。

对于Kurz来说,算是老熟人了,他在当总理期间就市场去硅谷访问考察,对科技有浓烈兴趣。 然后结交了各种硅谷CEO和各种人脉,这是要提前布局啊这是。


看来这帅哥一直都有个互联网梦,也难怪从政坛辞职,立马加入硅谷公司。

目前前总理 Kurz 加入 Thiel的资本公司,这是一家风险对冲基金资本公司,严格意义来说算是一家科技金融公司。

用技术来指导商业投资理财和商业战略等业务。

而Kurz的职位严格意义来说 是战略执行官,负责欧洲奥地利地缘政治和欧洲战略指导判断,并不是具体写业务的码农。

看来这位最年轻的总理帅哥,还可能干老本行。而且收入更高,身份更自由,完美实现了,破圈副业,妥妥的人生赢家。

说实话,我已经吃了一筐柠檬了。酸🍋🍋

3、
那么给我们的启示是什么呢?我认为有三点。

一是无论身份职位差异多么大,未来都是不可确定的。你贵为总理也要未雨绸缪,说下台就下台了,更别说我们普通小人物。

二是人脉和保持好奇心的重要性,我们要时常维护职场关系,甚至利用职场和工作便利 要懂得权衡利弊为自己拓展相关人脉,日后你绝对用得上。

刘邦之所以草根得天下,我觉得他比那朱元璋还难,他利用的很重要的一点是什么?很大程度就是人脉的力量。

而如果日后,个人创业,很大程度也是整合资源,利用人脉。

因为web2.0时代,技术已经完全不是壁垒了,资源和生产资料才是。而web3.0又要来了呢。

而人脉资源属于生产资料的一种重要资料。

35岁其实大家不用害怕,每年互联网圈都会裁人,而在5年前,都在传说30岁码农就找不到工作了,如今变成了35岁危机。

我想过不了几年,40岁的码农还依然是很多的。因为从业者其实是在减少的,总人口趋势也不容乐观。

每年都在谈 中年危机,但真正的危机并不是大环境带给你的危机, 而是你自己在年轻时候意识不到你也会老。

你没有人无远虑必有近忧的觉悟。

大家主张躺平,而你真的信了!躺不了几年,你就真的危机了。

三是,成年人追求利益和money没有错。错的是你怎么追求,君子爱财取之有道,才是正常的社会想象。

今天北妈又在贩卖焦虑?但焦虑不是北妈写出来你才焦虑,而是它一直在你内心,而你不愿意面对它罢了,一起成长吧。

作者:北妈
来源:https://mp.weixin.qq.com/s/kHyPm1ww8msBGn-TMtn58w
收起阅读 »

字节面:什么是伪共享?

CPU 如何读写数据的?先来认识 CPU 的架构,只有理解了 CPU 的 架构,才能更好地理解 CPU 是如何读写数据的,对于现代 CPU 的架构图如下: 可以看到,一个 CPU 里通常会有多个 CPU 核心,比如上图中的 1 号和 2 号 CPU 核心,并且...
继续阅读 »

CPU 如何读写数据的?

先来认识 CPU 的架构,只有理解了 CPU 的 架构,才能更好地理解 CPU 是如何读写数据的,对于现代 CPU 的架构图如下:


pic_dab4651e.png

可以看到,一个 CPU 里通常会有多个 CPU 核心,比如上图中的 1 号和 2 号 CPU 核心,并且每个 CPU 核心都有自己的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分为 dCache(数据缓存) 和 iCache(指令缓存),L3 Cache 则是多个核心共享的,这就是 CPU 典型的缓存层次。


上面提到的都是 CPU 内部的 Cache,放眼外部的话,还会有内存和硬盘,这些存储设备共同构成了金字塔存储层次。如下图所示:


pic_f0854715.png

从上图也可以看到,从上往下,存储设备的容量会越大,而访问速度会越慢。至于每个存储设备的访问延时,你可以看下图的表格:


pic_6b362bd7.png

你可以看到, CPU 访问 L1 Cache 速度比访问内存快 100 倍,这就是为什么 CPU 里会有 L1~L3 Cache 的原因,目的就是把 Cache 作为 CPU 与内存之间的缓存层,以减少对内存的访问频率。


CPU 从内存中读取数据到 Cache 的时候,并不是一个字节一个字节读取,而是一块一块的方式来读取数据的,这一块一块的数据被称为 CPU Line(缓存行),所以 CPU Line 是 CPU 从内存读取数据到 Cache 的单位。


至于 CPU Line 大小,在 Linux 系统可以用下面的方式查看到,你可以看我服务器的 L1 Cache Line 大小是 64 字节,也就意味着 L1 Cache 一次载入数据的大小是 64 字节。


pic_4c8081a7.png

那么对数组的加载, CPU 就会加载数组里面连续的多个数据到 Cache 里,因此我们应该按照物理内存地址分布的顺序去访问元素,这样访问数组元素的时候,Cache 命中率就会很高,于是就能减少从内存读取数据的频率, 从而可提高程序的性能。


但是,在我们不使用数组,而是使用单独的变量的时候,则会有 Cache 伪共享的问题,Cache 伪共享问题上是一个性能杀手,我们应该要规避它。


接下来,就来看看 Cache 伪共享是什么?又如何避免这个问题?


现在假设有一个双核心的 CPU,这两个 CPU 核心并行运行着两个不同的线程,它们同时从内存中读取两个不同的数据,分别是类型为 long 的变量 A 和 B,这个两个数据的地址在物理内存上是连续的,如果 Cahce Line 的大小是 64 字节,并且变量 A 在 Cahce Line 的开头位置,那么这两个数据是位于同一个 Cache Line 中,又因为 CPU Line 是 CPU 从内存读取数据到 Cache 的单位,所以这两个数据会被同时读入到了两个 CPU 核心中各自 Cache 中。


pic_54538b4a.png

我们来思考一个问题,如果这两个不同核心的线程分别修改不同的数据,比如 1 号 CPU 核心的线程只修改了 变量 A,或 2 号 CPU 核心的线程的线程只修改了变量 B,会发生什么呢?


分析伪共享的问题

现在我们结合保证多核缓存一致的 MESI 协议,来说明这一整个的过程,如果你还不知道 MESI 协议,你可以看我这篇文章「[10 张图打开 CPU 缓存一致性的大门][10 _ CPU]」。


①. 最开始变量 A 和 B 都还不在 Cache 里面,假设 1 号核心绑定了线程 A,2 号核心绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B。


pic_20cb02eb.png

②. 1 号核心读取变量 A,由于 CPU 从内存读取数据到 Cache 的单位是 Cache Line,也正好变量 A 和 变量 B 的数据归属于同一个 Cache Line,所以 A 和 B 的数据都会被加载到 Cache,并将此 Cache Line 标记为「独占」状态。


pic_31e05dfd.png

③. 接着,2 号核心开始从内存里读取变量 B,同样的也是读取 Cache Line 大小的数据到 Cache 中,此 Cache Line 中的数据也包含了变量 A 和 变量 B,此时 1 号和 2 号核心的 Cache Line 状态变为「共享」状态。


pic_611082d7.png

④. 1 号核心需要修改变量 A,发现此 Cache Line 的状态是「共享」状态,所以先需要通过总线发送消息给 2 号核心,通知 2 号核心把 Cache 中对应的 Cache Line 标记为「已失效」状态,然后 1 号核心对应的 Cache Line 状态变成「已修改」状态,并且修改变量 A。


pic_c6c22e2c.png

⑤. 之后,2 号核心需要修改变量 B,此时 2 号核心的 Cache 中对应的 Cache Line 是已失效状态,另外由于 1 号核心的 Cache 也有此相同的数据,且状态为「已修改」状态,所以要先把 1 号核心的 Cache 对应的 Cache Line 写回到内存,然后 2 号核心再从内存读取 Cache Line 大小的数据到 Cache 中,最后把变量 B 修改到 2 号核心的 Cache 中,并将状态标记为「已修改」状态。


pic_33e76a58.png

所以,可以发现如果 1 号和 2 号 CPU 核心这样持续交替的分别修改变量 A 和 B,就会重复 ④ 和 ⑤ 这两个步骤,Cache 并没有起到缓存的效果,虽然变量 A 和 B 之间其实并没有任何的关系,但是因为同时归属于一个 Cache Line ,这个 Cache Line 中的任意数据被修改后,都会相互影响,从而出现 ④ 和 ⑤ 这两个步骤。


因此,这种因为多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)。


避免伪共享的方法

因此,对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,否则就会出现为伪共享的问题。


接下来,看看在实际项目中是用什么方式来避免伪共享的问题的。


在 Linux 内核中存在 cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。


pic_2ad0ad52.png

从上面的宏定义,我们可以看到:



  • 如果在多核(MP)系统里,该宏定义是 cacheline_aligned,也就是 Cache Line 的大小;
  • 而如果在单核系统里,该宏定义是空的;

因此,针对在同一个 Cache Line 中的共享的数据,如果在多核之间竞争比较严重,为了防止伪共享现象的发生,可以采用上面的宏定义使得变量在 Cache Line 里是对齐的。


举个例子,有下面这个结构体:


pic_46aebc19.png

结构体里的两个成员变量 a 和 b 在物理内存地址上是连续的,于是它们可能会位于同一个 Cache Line 中,如下图:


pic_56ff524f.png

所以,为了防止前面提到的 Cache 伪共享问题,我们可以使用上面介绍的宏定义,将 b 的地址设置为 Cache Line 对齐地址,如下:


pic_1eeffcc3.png

这样 a 和 b 变量就不会在同一个 Cache Line 中了,如下图:


pic_e35118bc.png

所以,避免 Cache 伪共享实际上是用空间换时间的思想,浪费一部分 Cache 空间,从而换来性能的提升。


我们再来看一个应用层面的规避方案,有一个 Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式,来避免伪共享的问题。


Disruptor 中有一个 RingBuffer 类会经常被多个线程使用,代码如下:


pic_ce4a0542.png

你可能会觉得 RingBufferPad 类里 7 个 long 类型的名字很奇怪,但事实上,它们虽然看起来毫无作用,但却对性能的提升起到了至关重要的作用。


我们都知道,CPU Cache 从内存读取数据的单位是 CPU Line,一般 64 位 CPU 的 CPU Line 的大小是 64 个字节,一个 long 类型的数据是 8 个字节,所以 CPU 一下会加载 8 个 long 类型的数据。


根据 JVM 对象继承关系中父类成员和子类成员,内存地址是连续排列布局的,因此 RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何实际用途,更不会对它们进行读写操作。


pic_4fc4c5eb.png

另外,RingBufferFelds 里面定义的这些变量都是 final 修饰的,意味着第一次加载之后不会再修改, 又由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,于是只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生伪共享的问题。


作者:小林
来源:https://mp.weixin.qq.com/s/zeGxBx77TFGtVeMRBVR-Lg

收起阅读 »

元宇宙的“42条共识”

罗马不是一天建成的,元宇宙也一样。元宇宙融合了信息技术(5G/6G)、互联网时代(web3.0)、人工智能、云算力、大数据、区块链以及VR、AR、MR,游戏引擎在内的虚拟现实技术的成果。 它将引发基础数学(算法)、信息学(编程、信息熵)、生命科学(脑机接入)、...
继续阅读 »

01元宇宙不是一天建成的

罗马不是一天建成的,元宇宙也一样。

人类从未像今天这样,可以自己成为“创世主”

元宇宙融合了信息技术(5G/6G)、互联网时代(web3.0)、人工智能云算力大数据区块链以及VR、AR、MR,游戏引擎在内的虚拟现实技术的成果。



pic_85b46dac.png

它将引发基础数学(算法)、信息学(编程、信息熵)、生命科学(脑机接入)、区块链(加密金融)、量子计算(算力)等学科的深入研究和交叉互动。

还会推动未来学、哲学、逻辑学、伦理学、科幻等人文科学体系的全新突破。

但这一切需要时间,不能急功近利的思考去变现。

02元宇宙不是“虚拟世界”

《头号玩家》不是元宇宙,《黑客帝国》也不是元宇宙。

元宇宙不是简单的虚拟世界,它与平行世界也不是相互割裂,而是交汇融合

线上+线下是元宇宙未来的存在模式

线下的场景会成为元宇宙的一个重要组成部分,元宇宙也会为线下的沉浸式娱乐带来更多可能。



pic_eda6ce07.png

元宇宙与现实世界一开始存在边界,但两者的边界会变得越来越模糊,最终变成一个硬币的两面,相互依存。

元宇宙是虚实共生,不是镜像孪生。

03算力即权力

在元宇宙世界,算力是同水、电、油、气一样的基础设施。

没有算力,元宇宙将停止运转

谁拥有了算力,谁就拥有了财富

谁拥有了控制算力的权限,谁就拥有控制世界的权力。



pic_6b068420.png

未来世界的算力分为:

中心化算力去中心化算力

如果中心化算力占据上峰,那这个世界将更加不平等。

如果去中心化算力占据上身,那这个世界相对来说更加公平。

人类未来最大的矛盾,是日益增长的数据处理有限算力之间的矛盾!

04元宇宙不是游戏

游戏是元宇宙入口之一,但元宇宙不是游戏。

将元宇宙视为一个超级大型3D虚拟游戏是片面浅薄的。

元宇宙是整合多种新技术而产生的新型虚实相融的数字文明。



pic_52445957.png

涉及主权财富、生态建设、经济体系、价值设定等多重要素,是自然人的高维度拓展。

05元宇宙不属于

任何一家科技巨头

元宇宙的核心是“去中心化”,不会被某一家科技巨头公司控制。

被科技巨头的控制的元宇宙,不是元宇宙。

任何一家科技巨头,也无法真正建成完整的元宇宙。


pic_8baff518.png

真正的元宇宙最终需要实现跨链互通、身份互认、价值共享,它不属于任何一家科技巨头,而是属于每一个人。

06元宇宙与星辰大海不是对立关系

太空歌剧和赛博朋克相互融合,不是非此即彼。

元宇宙与星辰大海不是竞争对立关系,前者向内拓展,后者向外延伸,最终殊途同归,共同发展。

pic_f8f972c9.png

更好的数字虚拟技术和更发达的太空技术,其实是相辅相成的。

07概念股不是“元宇宙”

股市中的“元宇宙”,都是打着概念炒作的韭菜盘。

元宇宙不是单纯的游戏,现阶段核心是个人数字身份和个人主权财富

pic_abda03ed.png

概念股大部分是“伪元宇宙”,在意的是短期的套利炒作。

所以,了解元宇宙的深层逻辑,认识什么好的项目,别成为韭菜。

08Web3.0是元宇宙

不可或缺的一部分

没有CryptoWeb3.0不是Web3.0,没有Web3.0的元宇宙不是元宇宙。

pic_03810ba6.png

实现Web3.0所需的技术路径中,跨链、分布式存储隐私计算是Web3.0生态目前发展阶段的核心技术栈,这些,与元宇宙中的去中心化身份、信用体系建设都高度吻合。

09元宇宙是“元叙事”,是人类大目标

元宇宙是跟“全球化”一样的元叙事,它是关于“永恒真理”“人类解救”的故事,是一种对未来进程有始有终的构想形式,具有五大特征

预测未来世界、追寻终极目标、产业利益趋同、统一前进方向、构建多重世界。pic_a8d7095a.png

10元宇宙是一个熵+系统

薛定谔在《生命是什么》中提到“生命是负熵”这一概念。

我们所处的现实世界是一个不断熵增的封闭系统,按照热力学第二定律,现实世界一定会有热寂的一天。pic_a109e725.png

元宇宙通过各类技术集成,可以最大程度减少元宇宙系统的熵增无序

在某一个时刻,元宇宙是一个熵-系统

但它没有办法摆脱母世界的影响,最终仍然是一个熵+系统

11元宇宙:数学>人性

元宇宙的底层逻辑,遵循“数学契约论”

在现实世界中,社会契约论主宰着现代文明

在元宇宙中,数学契约代替社会契约,用数学约束人性,用合约前置来消除人类基因中的自私自利

pic_60c26f31.png

图片数学契约自动检测订约人在订约后的行为,在元宇宙的任何行为都会被数个数学契约智能监督。

12元宇宙不是“文明内卷”

很多人用刘慈欣在2018年的演讲反驳“元宇宙”,称元宇宙是“文明内卷”

但刘慈欣讲的这段演讲时目标对象是“虚拟世界”,当时没有元宇宙的概念。

元宇宙是虚拟世界现实世界的融合,也是多种技术发展到一定阶段后的融合结晶pic_3be0e5a1.png

元宇宙不会带来文明内卷,最终实现文明的跃迁

13区块链是元宇宙的补天石

区块链让元宇宙有了现实驱动,而不再只是精神上的虚无飘缈。

区块链带来了数字ID所承载独一无二的身份,也带来了经济体系的安全稳定运行。pic_3a7bed60.png

甚至还包括私钥即一切,最终带来上层建筑的变化:

公正与自由、去中心化、数据私有化、反垄断……

物理硬件只是外在的肉身,区块链才是鲜活的灵魂。

14元宇宙“大爆炸奇点”不可知

就像宇宙诞生源于一个奇点,元宇宙的最终爆发也将由一个“奇点”点燃。

但目前这一“奇点”是未知的。大多数人只盯着已经成形的项目或者大公司,但风起于青萍之末,元宇宙带来的是一个全新时代,只怕没有那么容易猜到结局。pic_44e3ab15.png

一个伟大的元宇宙的形成,也许一个故事,一个钱包,一个插件,一套NFT图像,一个合约都可能会成为它的爆炸奇点。

15数字身份是元宇宙的最终入口

在现实世界中,我们拥有自己的身份证,其承载我们在现实世界价值

在元宇宙中,数字身份会是最终入口,其背后负载的是数字世界的社交关系资产pic_d1c7dd30.png

这个数字身份根据自己的价值观、元宇宙观、个体定位等因素,通过你在元宇宙中的种种行为选择进行确认赋权。它不仅仅是一个头像,而是一个真实存在且影响未来的数字ID

16元宇宙是新思想的“源泉”

可以明确的是,元宇宙将是新思想的源泉。

技术新思想:

5G+AI+XR云计算,区块链,高度沉浸社交,引擎技术、脑机接口,数字人,边缘计算,数学算法,3D操作系统等都会出现新技术。

金融新思想:

Web3.0、区块链、DAO、DeFi、GameFi、NFT、DEX、AMM、以太坊、USDC。

同时也是人类反思自我的核心场景,通过自我创世从而思考“创世主本意”pic_041c483f.png

从这个层次上来讲,元宇宙将产生新哲学。

17元宇宙是非线性时空

现实世界,我们遵循时间走向,以单箭头形式前行。

pic_d7a179cf.png

而元宇宙却可以是时间尺度上完全不同的平行和非线性。利用的数字身份多重人格,可以进行类**平行宇宙式的体验,在同一时间尺度上,可以完成不同的事情,实现小说中才可能出现的“非线性叙事”。

18DAO是元宇宙

核心治理模式

DAO就是未来。

DAO保障了规则制定权在社区手中,形成元宇宙的治理基石。

在DAO的治理模式下,元宇宙完全是去中心化的,其特征是开源、资产自由流动、人员自由贡献、社区投票表决、治理结果执行不受干扰。pic_182881c4.png

没有人可以发号施令,管控是分散存在而不是按等级划分。它允许每个人参与讨论,也鼓励团队合作

基于DAO治理,元宇宙将治理权交给所有参与者,建立可信规则的社区自治。

19元宇宙:互联网与

区块链的结合

元宇宙主要分为两派:互联网派和区块链派。

两派之争,分化出完全不同的元宇宙进化路线

无论是互联网派,还是区块链派,在实现元宇宙的过程中都有自己的烦恼和担忧

互联网最大的问题在于“安全性”,区块链最大的问题在于“效率性”,但同时两派又都有自己的优势pic_b84d136f.png

通过将互联网技术区块链原理结合起来,是实现元宇宙的最好路径。

20元宇宙的“大一统理论”

自然世界的科学家一直在寻找“大一统理论”

依照量子理论的观点:

物质是一份份的信息。

最前沿的弦理论更认为宇宙就是一个大提琴家奏出的乐章

pic_b6092c7f.png

自然世界的本质难道是信息比特

元宇宙的大一统理论是什么?

我们可以看得非常清楚:

元宇宙的源头是原子世界

原子世界是创世主(第一推动力),他们构建的01就是“大一统理论”

21元宇宙经济学:人即货币

元宇宙下的“元经济学”遵循“人即货币”理念。

每个人都有自己映射的Token,这些Token可以量化自己的价值。pic_706bda4d.png

在“人即货币”理念下,人从出生起就是天生的点对点的信任机器,人本身成了衡量一切的价值标的。

“人即货币”有三大定律

1、每个人都有发行货币的自由;

2、个人价值=个人币值;

3、人币同在。

“人即货币”是元宇宙共识时代的高版本,通过自律来换取更大的自由和信用,让自发行的货币更有价值。

22元宇宙是技术的“结晶点”

元宇宙为什么受大企业青睐,因为它是技术“结晶点”。

谁能成为这个“结晶点”,谁就可能成为未来最强大的企业。

20世纪初的汽车,20世纪末的互联网

21世纪初的移动互联网(手机),未来的元宇宙(技术载体未定)。 pic_cf4af863.png

它不是某一项技术,而是一系列“连点成线”技术的集合。 其中包括芯片技术、通信技术、区块链技术、交互技术、虚拟引擎技术、AI技术、网络及软硬件编程等各种数字技术之大成。

23元宇宙是一个开源世界

在元宇宙中,除了个体掌控的私有数据外,所有一切都是开源的。

代码是开源的,你可以随意查看那些开源代码;

技术是开源的,所有元宇宙的技术底层逻辑你都可以学习;

公共数据是开源的,所有人皆可查看和使用,以此规避中心化平台的垄断;

内容是开源的,只要你有创意,你就能创造元宇宙的内容;

智能合约是开源的,你可以查看所有在链上的智能合约内容;

……

pic_f7b0283b.png

24“主权财富”是最大公约数

在元宇宙这个话题上,大家短期内是没办法达成共识的,每个人只看到自己所认可的元宇宙。pic_86807320.png

但获得自由、可自我分配的“主权财富”是所有人的共识……

25NFT是元宇宙中的“生命体”

NFT是数字世界中一种不能被复制、更换、切分的,用于检验特定数字资产真实性或权利的唯一数据表示。pic_3cecd2aa.png

在元宇宙中生成的原生NFT,是元宇宙独一无二的“生命体”。它不是一种简单的数字模拟信号,最终都可以形成一个自己的、超出任何产品的世界。

26元宇宙原创主权:内容至上

元宇宙是一个包容万象的新世界,由所有参与者共同创建。

未来的元宇宙,它的内容全部来源于参与者。相比与传统互联网,在元宇宙中,内容的重要性要远远大于平台的重要性。pic_b2796bb7.png

依托开源的方式,让所有人都能够参与到内容创造中,享受共同创建元宇宙的乐趣。谁拥有了创意,谁拥有了优质内容,谁自己就能在元宇宙中建立新平台。

27元宇宙:游戏即劳动

元宇宙不是游戏世界,但游戏却是元宇宙的一大组成部分。

游戏是元宇宙的最佳突破口,完整的元宇宙需要具有博弈策略的游戏来完成行为创造。pic_7c4b4d00.png

在元宇宙中,游戏即生活游戏即劳动,它将物理世界的劳动和虚拟世界的游戏光滑连接,将游戏和劳动结合起来。

28元宇宙不能让你逃避现实

元宇宙不是一个让你逃避世界的新去处。

pic_a044c385.png

堕落者进入元宇宙只会更加堕落,而那些优秀的人则会创造更多价值财富

29元宇宙是全生态进化

元宇宙是一个非常宏大复杂的结构,这样一个系统不是像某个游戏那样可以一起打包升级,整个系统的变化和升级是非常复杂的,它又同属于一个宇宙,所以它的升级和改造是全生态的进化

pic_33f038e8.png

这个全生态的进化一共可分为七层

自然层、物理层、交互层、数据层、协议层、合约层、应用层。

一个不断生长壮大的元宇宙,它的系统架构最终会向生命体学习,它的进化会向自然进化学习。

30超现实治理

元宇宙是超现实世界,以下几点都是超现实治理要素。

去中心化治理:

去中心化的社会组织里,管控分散存在而不是按等级划分

代码即法律:

由代码构成的智能代码合约形成了“自规则”“法律前置”降低了法律执行成本,有《少数派报告》的味道。

共算主义:

每个人都有获得算力的权利,每个人也有贡献算力的义务

数据私有:

用户持有私钥掌控个人数据,用户拥有完全自主管理个人数据的权力。

分布式金融:

金融体系以分布式为主,任何第三方不能逆转任何一笔交易。

pic_78e41459.png

31没有共识,就没有数字土地

没有形成共识的元宇宙大地,所谓数字土地就是泡沫。

宇宙最初三秒钟是在高温和碰撞中创造“物体规律”,当宇宙世界的规律形成后,才开始慢慢形成星球星系

pic_4521e2fc.png

现在元宇宙还没有达到星球自成、达成共识、进行数字土地财富分配的阶段。
数字土地,只是一场泡沫。
可以作为数字实验,切莫高价入局

32比特世界的“互操作性”

张飞杀岳飞,杀得满天飞。这样的事情在原子世界不可能发生。

是梅西厉害还是马拉多纳厉害?跨时空也很难比较。
但在元宇宙的“比特世界”,底层数字协议一旦形成共识,“复仇者联盟”分分钟钟就可以组队打BOSS。

pic_3f035c72.png
这就是元宇宙的“互操作性”,数字底层协议保证了算法形成的比特数字可跨宇宙穿越。你穿着“2140元宇宙”的数字盔甲,在Axie Infinity城堡卖掉盖亚蓝戒,换得了Decentraland上的一块火星大陆……
所有在元宇宙世界伟大的数字IP,都可以正面交锋。

33数字人进化:AI数字人将成为大多数

数字人将由以下几种人构成:

虚拟的假人:

产生了虚拟数据身份

意识上传的真人:

以人为模板,以人的意识为主体,他们是从真实世界移居到元宇宙的移民,不是原住民。

AI数字人:

一开始就是程序设计而成,没有真实的碳基或硅基身体,不受到任何束缚,原则上可以在元宇宙中遨游,它们是元宇宙的原住民。这些人将成为大多数。

pic_9c9a5fcb.png

AI数字人摆脱了肉身束缚,只受到数学规律限制,而数学的世界更抽象,这意味着AI数字人的能力将以数学为边界,而不以物理为边界,它们的未来将发展到什么程度?暂时无法评估,但一旦形成进化态,那将完全颠覆元宇宙

34乌合之众的极乐狂欢:一九定律

技术潮流不可阻挡,人类娱乐化亦不可阻挡。
元宇宙会整合现有世界各项技术,让生产力得到迅速提升。
与此同时,刺激丰富,体验极致,在元宇宙中能保持清醒的人会越来越少;元宇宙沉浸程度比现实社会游戏更深,时间一久,偏理性的人和偏感性的人的分布,就会从二八定律变成一九定律

pic_e1cf3762.png

元宇宙的技术将实现感官享乐主义,各种小宇宙就是寺庙和教堂,乌合之众是它的狂热粉丝和朝拜者。

那么,元宇宙的牧师和神会是谁?

35加密朋克2.0的“暗宇宙”

顶尖程序员、自由主义者、无政府主义者、科技金融玩家等元宇宙高级玩家,他们发现元宇宙被乌合之众占领,无法实现当初建设元宇宙的梦想——一个人人自由、共享开放的“美丽新世界”

pic_5a7c8969.png

这些人在元宇宙的隐秘节点,通过多层链接,建立暗宇宙,只有少数人能够通过自己去发现、审查、注册进入暗宇宙。

这些少数派成了暗宇宙背后的操控人,类似于现在的tor世界(暗网)。
基于元宇宙的“去中心化”的特质,“暗宇宙”会比现在“暗网”影响力要大。

36程序员的“个人屠龙”时代

一开始,大公司垄断元宇宙会被唾弃。
但少数人最终会成为主宰,程序员权力更大。

顶尖玩家操控世界,因为非线性时空以及“一九定律”关系,聚集的粉丝信徒可在数小时内到达数十万,基于个人信仰的组织会越变得越强,而基于血脉、原始宗教、疆域的组织会被淡化。

pic_1e762f25.png

一个基于去中心化理念形成的元宇宙,最终会形成中心化的个人帝国
这是元宇宙最难破解的“二元悖论”之一。

传统宗教必须浴火重生,否则新的科哲宗教将诞生。
屠龙者,最终成为巨龙。

37元宇宙是一台可自主学习的计算机

原子宇宙,从某种程度上来看,它的矩阵架构是通过自主学习演化而来。
它貌似宏观的算法精准,又是从最微不足道的布朗运动开始。

pic_b649959d.png

就算是伟大的相对论,在138亿年前的操作影响与138亿年后并不一样。热力学第二定律未知,这意味着“物理学”是图灵完备的。

现在元宇宙最有影响力的以太坊协议,也是一个图灵完备协议,以它为基石的元宇宙更有可能形成一台可自主学习的计算机。

38高维宇宙:虚实二相性

现实世界,微粒的波粒二象性很难理解。

德布罗意提出任何实物粒子都具有波粒二象性,波动与微粒之间的关系:E=hν,p=h/λ。

pic_090e097c.png

但只有从更高维的角度去解释:

两者是统一的。

元宇宙也是一样,如果完全进入到虚实共生的元宇宙世界,当最初创造元宇宙的原子人彻底消失后,拥有实体和虚拟身份的数字人同样也存在同样的疑问:为什么存在虚实二相性?

39意识形态之争,比特与原子的对抗

选择“比特(BIT)”,还是选择“原子(ATOM)”,可能是未来元宇宙世界最大的意识形态争论。

基于这一矛盾,元宇宙可能会分化出两种不同形态人种

选择现实世界原子人、选择数字世界比特人。

这两个人种沦为两个势力,最终各自为战。

pic_3b66c9e0.png

有人会拒绝元宇宙,坚守在现实世界;

有人会维护元宇宙,不允许其他异类存在。

比特与原子的对抗,未来元宇宙的矛盾冲突焦点

所以,选择比特,还是选择原子

40人格分裂症的春天到了

数字身份不遵循牛顿力学,而遵循MWI(多重世界)理论。
人可以选择不同身份体验人生,开启“多重人格社交”,在宇宙1中当一个植物学家,在宇宙2中做王国的领袖,元宇宙3中变成一只小动物……

pic_b6d0c286.png

多重身份连接到同一经济系统中,身份越多,利益越大。这是一个“人格分裂者”的乐园。
越是人格分裂的人,越是在元宇宙如鱼得水

41无法预知元宇宙尽头

目前的元宇宙,依旧在起步阶段,我们暂时无法预知元宇宙的未来发展。

微观世界,海森堡提出了不确定性原理**ΔxΔp≥h/4π**。

元宇宙世界,刚刚出生就饱受争议,未来之路同样艰险。

就像在全球化被提出时,没有人能预想到它的发展竟会如此曲折

元宇宙像一个新生儿,我们不能确定,它最终会成长至什么模样。

pic_b6084c3e.png

但唯一能够确定的是,我们每一个人都有机会去影响元宇宙尽头的结局。

因为在元宇宙尽头下,每个人都是创世的一份子

42元宇宙最大的共识,是没有共识

最大共识是没有共识,又一个二元悖论

前面提到的所有共识,是对元宇宙的短暂的认知和总结。

元宇宙三大定律:

非定域实在性,多世界诠释,虚实二象相(互补原理)。

都充满着矛盾辩证

元宇宙处在起步阶段,基础设施、底层逻辑、价值观等都还没有完全建立。

今天的共识,可能在明天就被推翻。

元宇宙不断变化的,又是去中心化的,它的共识会一直迭代

元宇宙的第42条共识,正是没有共识

作者:罗金海
来源:https://mp.weixin.qq.com/s/6ovJ8KcjXFYHFCxMV8CUbQ


收起阅读 »

API安全接口安全设计

如何保证外网开放接口的安全性。使用加签名方式,防止数据篡改信息加密与密钥管理搭建OAuth2.0认证授权使用令牌方式搭建网关实现黑名单和白名单一、令牌方式搭建搭建API开放平台方案设计:1.第三方机构申请一个appId,通过appId去获取accessToke...
继续阅读 »

如何保证外网开放接口的安全性。

  • 使用加签名方式,防止数据篡改

  • 信息加密与密钥管理

  • 搭建OAuth2.0认证授权

  • 使用令牌方式

  • 搭建网关实现黑名单和白名单

一、令牌方式搭建搭建API开放平台


方案设计:

1.第三方机构申请一个appId,通过appId去获取accessToken,每次请求获取accessToken都要把老的accessToken删掉

2.第三方机构请求数据需要加上accessToken参数,每次业务处理中心执行业务前,先去dba持久层查看accessToken是否存在(可以把accessToken放到redis中,这样有个过期时间的效果),存在就说明这个机构是合法,无需要登录就可以请求业务数据。不存在说明这个机构是非法的,不返回业务数据。

3.好处:无状态设计,每次请求保证都是在我们持久层保存的机构的请求,如果有人盗用我们accessToken,可以重新申请一个新的taken.

二、基于OAuth2.0协议方式

原理

第三方授权,原理和1的令牌方式一样

1.假设我是服务提供者A,我有开发接口,外部机构B请求A的接口必须申请自己的appid(B机构id)

2.当B要调用A接口查某个用户信息的时候,需要对应用户授权,告诉A,我愿同意把我的信息告诉B,A生产一个授权token给B。

3.B使用token获取某个用户的信息。

联合微信登录总体处理流程

  1. 用户同意授权,获取code

  2. 通过code换取网页授权access_token

  3. 通过access_token获取用户openId

  4. 通过openId获取用户信息

三、信息加密与密钥管理

  • 单向散列加密

  • 对称加密

  • 非对称加密

  • 安全密钥管理

1.单向散列加密

散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。

散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。

单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:

  • MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;

SHA-1与MD5的比较

因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:

  • 对强行供给的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2128数量级的操作,而对SHA-1则是2160数量级的操作。这样,SHA-1对强行攻击有更大的强度。

  • 对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。

  • 速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

1、特征:雪崩效应、定长输出和不可逆。

2、作用是:确保数据的完整性。

3、加密算法:md5(标准密钥长度128位)、sha1(标准密钥长度160位)、md4、CRC-32

4、加密工具:md5sum、sha1sum、openssl dgst。

5、计算某个文件的hash值,例如:md5sum/shalsum FileName,openssl dgst –md5/-sha

2.对称加密

秘钥:加密解密使用同一个密钥、数据的机密性双向保证、加密效率高、适合加密于大数据大文件、加密强度不高(相对于非对称加密)

对称加密优缺点

  • 优点:与公钥加密相比运算速度快。

  • 缺点:不能作为身份验证,密钥发放困难


DES是一种对称加密算法,加密和解密过程中,密钥长度都必须是8的倍数

public class DES {
public DES() {
}

// 测试
public static void main(String args[]) throws Exception {
// 待加密内容
String str = "123456";
// 密码,长度要是8的倍数 密钥随意定
String password = "12345678";
byte[] encrypt = encrypt(str.getBytes(), password);
System.out.println("加密前:" +str);
System.out.println("加密后:" + new String(encrypt));
// 解密
byte[] decrypt = decrypt(encrypt, password);
System.out.println("解密后:" + new String(decrypt));
}

/**
* 加密
*
* @param datasource
*           byte[]
* @param password
*           String
* @return byte[]
*/
public static byte[] encrypt(byte[] datasource, String password) {
try {
  SecureRandom random = new SecureRandom();
  DESKeySpec desKey = new DESKeySpec(password.getBytes());
  // 创建一个密匙工厂,然后用它把DESKeySpec转换成
  SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
  SecretKey securekey = keyFactory.generateSecret(desKey);
  // Cipher对象实际完成加密操作
  Cipher cipher = Cipher.getInstance("DES");
  // 用密匙初始化Cipher对象,ENCRYPT_MODE用于将 Cipher 初始化为加密模式的常量
  cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
  // 现在,获取数据并加密
  // 正式执行加密操作
  return cipher.doFinal(datasource); // 按单部分操作加密或解密数据,或者结束一个多部分操作
} catch (Throwable e) {
  e.printStackTrace();
}
return null;
}

/**
* 解密
*
* @param src
*           byte[]
* @param password
*           String
* @return byte[]
* @throws Exception
*/
public static byte[] decrypt(byte[] src, String password) throws Exception {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(password.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");// 返回实现指定转换的
                  // Cipher
                  // 对象
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 真正开始解密操作
return cipher.doFinal(src);
}
}

输出

加密前:123456
加密后:>p.72|
解密后:123456

3.非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。

公钥与私钥是一对

  • 公钥对数据进行加密,只有用对应的私钥才能解密

  • 私钥对数据进行加密,只有用对应的公钥才能解密

过程:

  • 甲方生成一对密钥,并将公钥公开,乙方使用该甲方的公钥对机密信息进行加密后再发送给甲方;

  • 甲方用自己私钥对加密后的信息进行解密。

  • 甲方想要回复乙方时,使用乙方的公钥对数据进行加密

  • 乙方使用自己的私钥来进行解密。

  • 甲方只能用其私钥解密由其公钥加密后的任何信息。

特点:

  • 算法强度复杂

  • 保密性比较好

  • 加密解密速度没有对称加密解密的速度快。

  • 对称密码体制中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥。所以保证其安全性就是保证密钥的安全,而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多

  • 适用于:金融,支付领域

RSA加密是一种非对称加密

import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import org.apache.commons.codec.binary.Base64;


/**
* RSA加解密工具类
*
*
*/
public class RSAUtil {

public static String publicKey; // 公钥
public static String privateKey; // 私钥

/**
* 生成公钥和私钥
*/
public static void generateKey() {
// 1.初始化秘钥
KeyPairGenerator keyPairGenerator;
try {
  keyPairGenerator = KeyPairGenerator.getInstance("RSA");
  SecureRandom sr = new SecureRandom(); // 随机数生成器
  keyPairGenerator.initialize(512, sr); // 设置512位长的秘钥
  KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 开始创建
  RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
  RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
  // 进行转码
  publicKey = Base64.encodeBase64String(rsaPublicKey.getEncoded());
  // 进行转码
  privateKey = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
}

/**
* 私钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByprivateKey(String content, String privateKeyStr, int opmode) {
// 私钥要用PKCS8进行处理
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyStr));
KeyFactory keyFactory;
PrivateKey privateKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, privateKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }

} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

/**
* 公钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByPublicKey(String content, String publicKeyStr, int opmode) {
// 公钥要用X509进行处理
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyStr));
KeyFactory keyFactory;
PublicKey publicKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, publicKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }
} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

// 测试方法
public static void main(String[] args) {
/**
  * 注意: 私钥加密必须公钥解密 公钥加密必须私钥解密
  * // 正常在开发中的时候,后端开发人员生成好密钥对,服务器端保存私钥 客户端保存公钥
  */
System.out.println("-------------生成两对秘钥,分别发送方和接收方保管-------------");
RSAUtil.generateKey();
System.out.println("公钥:" + RSAUtil.publicKey);
System.out.println("私钥:" + RSAUtil.privateKey);

System.out.println("-------------私钥加密公钥解密-------------");
  String textsr = "11111111";
  // 私钥加密
  String cipherText = RSAUtil.encryptByprivateKey(textsr,
  RSAUtil.privateKey, Cipher.ENCRYPT_MODE);
  System.out.println("私钥加密后:" + cipherText);
  // 公钥解密
  String text = RSAUtil.encryptByPublicKey(cipherText,
  RSAUtil.publicKey, Cipher.DECRYPT_MODE);
  System.out.println("公钥解密后:" + text);

System.out.println("-------------公钥加密私钥解密-------------");
// 公钥加密
String textsr2 = "222222";

String cipherText2 = RSAUtil.encryptByPublicKey(textsr2, RSAUtil.publicKey, Cipher.ENCRYPT_MODE);
System.out.println("公钥加密后:" + cipherText2);
// 私钥解密
String text2 = RSAUtil.encryptByprivateKey(cipherText2, RSAUtil.privateKey, Cipher.DECRYPT_MODE);
System.out.print("私钥解密后:" + text2 );
}

}


四、使用加签名方式,防止数据篡改

客户端:请求的数据分为2部分(业务参数,签名参数),签名参数=md5(业务参数)

服务端:验证md5(业务参数)是否与签名参数相同
————————————————
作者:单身贵族男
来源:https://blog.csdn.net/zhou920786312/article/details/95536556

收起阅读 »

一个向上帝买了挂的男人!

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。 在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不...
继续阅读 »

pic_8ae2a872.png

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。

在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不知道他是谁。 约翰·冯·诺依曼是20世纪最有影响力的人物之一。他可能比过去150年中任何一位伟大的思想更直接地影响了你的生活,他的研究涉及从量子力学到气候科学的一切。 pic_1f8dc88d.png 冯·诺依曼最大的贡献是现代计算机,他采用了图灵奠定的卓越理论框架,并实际提出了为几乎所有数字计算机提供动力的架构:冯·诺依曼架构。 更有争议的是,冯·诺依曼在二战期间对曼哈顿计划做出了重大贡献,包括完善原子弹本身的设计和对其功能至关重要的机制。 pic_7ec06076.png 与曼哈顿计划的其他一些老兵不同,冯·诺依曼从未对自己在该计划中的角色表示遗憾,甚至在冷战期间推动了「同归于尽」的政策。 至少可以说,约翰·冯·诺依曼是一个复杂的人物,但他在20世纪几乎无人能及,可以说他对现代世界的责任比他同时代的任何人都大。

神童降世

约翰·冯·诺依曼于1903年12月28日出生在匈牙利首都布达佩斯。 冯·诺依曼的父亲是银行家,母亲是奥匈贵族的女儿,他的父母富有且受人尊敬。
pic_34745df7.png
1913年,奥匈帝国皇帝弗朗茨·约瑟夫授予冯·诺依曼的父亲贵族地位,并给了这个家族一个世袭的头衔「马吉塔」,即现在的罗马尼亚马吉塔。 这个头衔纯粹是尊称,因为这个家庭与这个地方没有任何联系,但这是冯·诺依曼一生都会坚持的。 年轻的冯·诺依曼在同龄人中,被认为是真正的神童,尤其是在数学方面。人们认为他有摄影一般的记忆力,这帮助他从很小的时候就吸收了大量的知识。 pic_9118426a.png 冯·诺伊曼11岁时与表妹 Katalin Alcsuti在一起 六岁时,他就开始在头脑中进行两个八位数的除法,八岁时,他已经掌握了微积分。 pic_1875c648.png 他的父亲认为,他所有的孩子都需要说除母语匈牙利语以外的欧洲主要语言,所以冯·诺依曼学习了英语、法语、意大利语和德语。 小时候,他对历史也有很深的兴趣,并阅读了德国历史学家威廉·昂肯的整部46卷专著《通史》。
pic_b93227b9.png
德国历史学家威廉·昂肯 在老师的鼓励下,冯·诺依曼在学习上取得了优异的成绩,但他的父亲不相信数学家的职业会带来经济上的利益。 相反,冯·诺依曼和他的父亲达成共识,同意冯·诺依曼从事化学工程,他17岁去柏林学习,后来在苏黎世学习。 化学似乎是冯·诺依曼少数几个不感兴趣的领域之一,尽管他确实获得了苏黎世化学工程文凭,同时还获得了数学博士学位。
pic_c0043b57.png

职业生涯早期

约翰·冯·诺依曼很早就发表了论文,从20岁开始,他写了一篇定义序数的论文,这仍然是我们今天使用的定义。 他写了关于集合论的博士论文,并在一生中对该领域做出了若干贡献。
到1927年,冯·诺依曼已经发表了12篇著名的数学论文。到1929年,他已经出版了32部作品,以大约一个月一篇学术论文的速度写出了很多重要的工作。 pic_1cb6f516.png 1928年,他成为柏林大学的一名私 人教师,也是柏林大学历史上获得该职位最年轻的人。 这个职位使他能够在大学里讲课,直到1929年他成为汉堡大学的一名私人教师。 冯·诺依曼在父亲于1929年去世后,皈依了天主教。1930年元旦,约翰·冯·诺依曼与布达佩斯大学经济学学生玛丽埃塔·科维西结婚,并于1935年与她生下了他唯一的孩子玛丽娜。 虽然冯·诺依曼似乎注定要在德国科学院从事一个有前途的职业,但在1929年10月,他获得了新泽西州普林斯顿大学的一个职位,他最终接受了这个职位,并于1930年与妻子一起前往美国。

移民美国

到1933年,约翰·冯·诺依曼成为新成立的普林斯顿高等研究院最初的六名数学教授之一,他也将在这个岗位上度过余生。 当他搬到新泽西时,像他之前的许多美国移民一样,冯·诺依曼将他的匈牙利名字英语化了(由玛吉塔伊·诺依曼·亚诺斯变成约翰·冯·诺依曼,使用德国式的世袭尊称)。 1937年,他和妻子离婚,第二年冯·诺依曼再婚,这次是和克拉拉·丹,他在第二次世界大战前最后一次访问匈牙利时,在布达佩斯第一次见到了克拉拉·丹。 pic_75b34244.png 1937年,冯·诺依曼成为美国公民,1939年,他的母亲、兄弟姐妹和姻亲也都移民到了美国(他的父亲早些时候去世了)。

战争年代与「曼哈顿计划」

约翰·冯·诺依曼对历史最重要的贡献之一是他在第二次世界大战期间对曼哈顿计划的研究。 一如既往,冯·诺依曼不能让数学挑战得不到解决,更困难的问题之一是如何模拟爆炸的影响。 冯·诺依曼在20世纪30年代投身于这些问题的研究,并成为该领域的专家。如果他有特长的话,那应该是研究聚能装药(Shaped Charges,用于爆破)领域的数学问题,聚能装药是用来控制和引导爆炸能量的。 pic_434c04b8.png 这使他与美国军方,特别是美国海军进行了相当多的定期磋商。当曼哈顿计划在20世纪40年代初开始工作时,冯·诺依曼因其专业知识而被招募。 1943年,冯·诺依曼对曼哈顿计划产生了最重大、最持久的影响。 pic_dd9691a8.png 曼哈顿计划成员 当时,设计原子弹的洛斯阿拉莫斯实验室发现,钚-239(该项目使用的裂变材料之一)与实验室的工作炸弹设计不兼容。 实验室的物理学家塞斯·尼德迈尔一直在研究一种独立的内爆型炸弹设计,这种设计很有希望,但许多人认为它不可行。
pic_bbad35a1.png
核弹内爆机制的动画 要引爆核爆炸,需要在炸弹的反应物中引发失控的裂变链式反应。链式反应的速度是指数级的,因此控制爆炸足够长的时间,使足够的裂变材料进行所需的反应,是一项重大挑战。 内爆型炸弹需要更复杂的控制来产生反应,但它也不需要像洛斯阿拉莫斯开发的枪型炸弹设计那样耗费那么多的材料。 内爆型装置使用一系列受控的常规爆炸来压缩其核心中的裂变反应物。 在这种压力下,裂变材料迅速开始核裂变链式反应,通过内爆的力量保持在原位,并允许更多的裂变材料释放其能量。
pic_32a52999.png
控制这些爆炸以产生精确的内爆力来产生期望的反应是一项很大的挑战,而冯·诺依曼以极大的热情接受了它。 他认为,使用较少的球形材料并通过内爆力适当压缩,可以产生更有效的爆炸,尽可能多地使用现有的裂变材料。 他经常是少数几个主张内爆方法的人之一,并最终计算出了一个数学公式,表明如果内爆能以至少95%的精度保持球形的几何形状,该方法就是可以实现的。 冯·诺依曼还计算出,如果爆炸在目标上方一定距离引爆,而不是在击中地面时引爆,爆炸的有效性将会提高。 这大大增加了原子弹的杀伤力,也减少了爆炸产生的尘埃量。 之后,冯·诺依曼被选为科学顾问团队的一员,他们就炸弹的可能目标咨询军方。 冯·诺依曼建议将目标定为日本京都,因为京都作为文化之都,其毁灭可能足以迫使战争迅速结束。 提出这一建议的不止他一个人,但战争部长亨利·史汀生否决了将目标对准京都这个提议,因为那里有许多历史建筑和重要的宗教圣地,所以最后选择了广岛和长崎。 1945年7月16日的三位一体试验中,冯·诺依曼在场,当时第一枚原子武器被引爆。广岛和长崎被炸后,日本投降,第二次世界大战结束。 pic_87622a6c.png 三位一体试验 与曼哈顿计划中同时代的一些人不同,冯·诺依曼似乎没有任何反思的痛苦、遗憾,甚至对他在原子弹方面的工作也没有一丝怀疑。 事实上,冯·诺依曼是核武器发展和相互保证毁灭(MAD)理论的最有力支持者之一,认为这是防止另一场灾难性世界大战的唯一方法。

核武器的「轻量化」思想

像战后初期的许多美国人一样,冯·诺依曼担心美国在核武器发展方面落后于苏联。 到上世纪40 年代末到50 年代初,用战略轰炸机向敌人投掷更多原子弹的理念,逐渐被新的火箭技术所取代。
冯·诺依曼认为,导弹是核武器的未来,由于他与参与苏联武器研制的德国科学家有过接触,他知道苏联对此问题的看法与他是一样的。 军备竞赛开始了,美苏两国把氢弹越做越小,可以装入洲际弹道导弹的弹头,冯·诺依曼积极为美国效力,努力缩小与苏联的「导弹差距」。 战后,冯·诺依曼在原子能委员会任职,为政府和军方提供核技术开发和战略方面的建议,并被广泛认为是「确保相互毁灭」理论(MAD)的设计者,而 MAD 在冷战期间确实被政府采纳,成为事实上的美国国策。

建造第一台真正的计算机

pic_75fe701f.png 冯·诺依曼在上世纪30年代初遇到了艾伦·图灵,当时图灵正在普林斯顿攻读博士学位。1937年,图灵发表了具有里程碑意义的论文《论可计算数》,奠定了现代计算的理论基础。
pic_3623b10a.png 冯·诺依曼很快认识到了图灵这一发现的重要性,并在30年代推动了计算机科学的发展。在普林斯顿大学,他和图灵围绕人工智能的思想曾进行了长时间的讨论。 作为一名数学家,冯·诺依曼从更抽象的角度研究计算机科学,另一个原因也是因为在30年代时,并没有真正可以工作的计算机。 在二战结束后,这种情况很快就改变了。 pic_bf84e043.png 冯·诺依曼深入参与了第一台可编程电子计算机「ENIAC」的开发,这台计算机能够识别和决定其他数据操作规则集,而不是最初使用的规则集。是冯诺依曼将 ENIAC 修改为作为存储程序机器运行。 后者让使我们今天理解的现代程序成为可能。冯·诺依曼本人编写了几个在 ENIAC 上运行的首批程序,并用这些程序模拟原子能委员会的部分核武器研究。 毫无疑问,冯·诺依曼对计算机科学领域最持久的贡献是在当今运行的每台计算机中使用的两个基本概念:冯·诺依曼体系架构和存储程序概念。 pic_e701e538.png 冯·诺依曼架构涉及构成计算机的物理电子电路的组织方式。按照这种方式构建的计算机被称为「冯·诺依曼机」。该架构由算术和逻辑单元 (ALU)、控制单元和临时存储器寄存器组成,它们共同构成了中央处理器 (CPU)。 CPU 连接到内存单元,该内存单元包含将要由CPU处理和操作的所有数据。CPU还连接到输入和输出设备,以根据需要更改数据,并检索运行程序的结果。 自 1945 年冯诺依曼提出这一架构以来,直到今天,它基本上仍是当今大多数通用计算机的运行方式,几乎没有改变。 另一项重大创新与冯·诺依曼架构有关,即存储程序概念,也就是说,被操作或处理的数据,以及描述如何操纵和处理该数据的程序,都存储在计算机的内存中。 这两项相互交织的创新实现了图灵机的理论框架,实际上将它们变成了可以用来计算工资、火炮轨迹、游戏、互联网等几乎所有一切数据的机器。

对其他领域的杰出贡献

除了数学和计算机科学之外,冯·诺依曼一生都对其他几个领域也做出了重大贡献。
在早期的职业生涯中,冯·诺依曼为新兴的量子力学领域做出了重大贡献。 pic_e52acca3.png 1932年,他和保罗·狄拉克在《量子力学的数学基础》一书中发表了狄拉克-冯·诺依曼公理,这是该领域的第一个完整的数学框架。在这本书中,他还提出了量子逻辑的形式系统,也是同类体系中的首创。 冯·诺依曼还将博弈论确立为一门严谨的数学学科,这无疑影响了他后来关于MAD理论的地缘政治战略工作。 pic_a9fc558f.png 冯·诺依曼的博弈论中包含这样一个观点,即在广泛的博弈类别中,总是有可能找到一个平衡,任何参与者都不应单方面偏离这个平衡。 在生命科学领域,冯·诺依曼对元胞自动机的自我复制进行了彻底的数学分析,主要是构造函数、正在构建的事物以及构造函数构建所讨论事物所遵循的蓝图之间的关系。该分析描述了一种自我复制的机器,它是在40年代设计的,没有使用计算机。 冯·诺依曼的数学造诣也惠及气候科学。1950年,他编写了第一个气候建模程序,并使用 ENIAC 使用数值数据进行了世界上第一个气象预测。 冯·诺依曼预计,全球变暖是人类活动的结果,他在1955年写道: 「工业燃烧煤和石油释放到大气中的二氧化碳,可能已经充分改变了大气的成分,导致全球普遍变暖约1华氏度。」 冯·诺依曼也被认为是第一个描述「技术奇点」的人。冯·诺依曼的朋友斯坦·乌拉姆 (Stan Ulam) 后来描述了与他的一次对话,这次对话在今天听起来非常有先见之明。 pic_ccf72d4b.png 斯坦·乌拉姆、理查德·费曼和冯·诺依曼在一起 乌拉姆回忆说:「有一次谈话集中在不断加速的技术进步和人类生活方式的变化上,这让我们看到了人类历史上一些本质上的奇点。一旦超越了这些奇点,我们所熟知的人类事务就将无法继续下去了。」

冯·诺依曼的去世,和他的光辉遗产

1955 年,冯·诺依曼在看医生时发现他的锁骨上长了一块肉,他被诊断患有癌症,但他并没有充分接受这个事实。 众所周知,冯·诺依曼对即将到来的结局感到恐惧。他的一生好友尤金·维格纳 (Eugene Wigner) 写道:
pic_4c140e80.png 「当冯·诺依曼意识到自己病入膏肓时,他的逻辑迫使他意识到,自己即将不复存在,因此也不再有思想……亲眼目睹这一过程是令人心碎的,所有的希望都消失了,即将到来的命运尽管难以接受,但已经不可避免。」 冯·诺依曼的病情在1956年持续恶化,最终被送进了华盛顿特区的沃尔特里德陆军医疗中心。为防止泄密,军方对他实施了特殊的安全措施。 冯·诺依曼邀请一位天主教神父在他临终前商量,并接受了他的临终仪式安排。不过这位神父本人表示,冯诺依曼看上去似乎并没有从仪式中得到安慰。 1957年2月8日,冯·诺依曼因癌症逝世,享年53岁,他被安葬在新泽西州的普林斯顿公墓。 关于冯·诺依曼的癌症是否与他在「曼哈顿计划」期间遭受辐射有关,人们一直存在争议,但毫无争议的是,人类过早地失去了当代最伟大的科学巨人之一。 pic_1628bda4.png 冯·诺依曼的助手 P.R. Halmos 在1973年写道: 「人类的英雄有两种:一种和我们所有人一样,但更加相似,另一种显然具有一些「超人」的特质。 我们都可以跑步,我们中的一些人可以在不到4分钟的时间内跑完一英里。但有些事,我们大多数人一辈子都无法做到。冯·诺依曼的伟大贡献是惠及全人类的。在某些时候,我们或多或少都能清晰地思考,但冯·诺依曼的清晰思维始终比我们大多数人高出好几个数量级。」 冯·诺依曼的才华是毋庸置疑的,尽管他留下的遗产,尤其是核武器方面的贡献,比他的朋友和崇拜者愿意承认的要复杂得多。 无论我们最终如何看待冯·诺依曼和他的成就,我们都可以肯定地说,在未来一代人甚至几代人的时间里,都不太可能出现像他一样,对人类历史产生如此重大影响的人了。

转自:新智元 | David 小咸鱼
原文链接:https://interestingengineering.com/john-von-neumann-human-the-computer-behind-project-manhattan

收起阅读 »

【小声团队】 - 我们为什么选择了Flutter Desktop

本文由小声团队出品,小声团队是一个专注于音频&音乐技术的初创团队,深度使用Flutter构建跨平台应用,希望与大家一起共同探索Flutter在桌面端&移动端的可能性。 背景 我们计划研发一款全功能跨平台的音乐制作平台(DAW),从立项之初我们...
继续阅读 »

本文由小声团队出品,小声团队是一个专注于音频&音乐技术的初创团队,深度使用Flutter构建跨平台应用,希望与大家一起共同探索Flutter在桌面端&移动端的可能性。


背景


小声团队使用Flutter Desktop研发的DAW截屏


我们计划研发一款全功能跨平台的音乐制作平台(DAW),从立项之初我们就已经明确了全平台的支持计划(即Windows / MacOS / Linux / iOS / Android ) ,也因此我们也是以这个为目标来寻找技术解决方案,经过一段时间的研究与学习,大致确定了几个可选项,内部的调研结果如下(本结果仅代表团队内部认知,如有差异还请包涵):

技术方案性能研发效率跨平台兼容性扩展能力原生代码交互能力
HTML5
QT极低
React Native
Flutter



为什么不使用基于HTML5打造的技术栈?


HTML5是众所周知的最易上手的跨平台UI解决方案,并且产业成熟,有众多可选的框架与开源组件可直接使用。但是DAW作为一款专业生产力工具并不适合完全在浏览器环境中运行,比如第三方插件系统浏览器则无法支撑,另外在内存资源上的使用也不是很便捷,通常一个音乐工程可能需要占据数G内存,运行时需要维护数万个对象,这对于Javascript来说还是浏览器来说都是很严重的负担。
从另一个方面来看,就算我们需要以一种阉割的形式支持Web,那么WASM技术则是我们更佳的选择。
因此,我们不考虑基于HTML5的技术方案。


为什么不选择QT & GTK 等老牌原生高性能框架?


在传统技术上来看,QT是最符合我们需求的技术方案,很多老牌工具厂商背后也都是基于QT技术栈完成。QT在运行效率上而言无疑是最佳的选择,我们的主要顾虑在对于CPP的掌控能力与研发效率,UI开发与引擎开发有一个很大的根本区别在于引擎开发通常使用单元测试来完成逻辑验证,而UI则很难使用单元测试来验证UI效果,也很少看到有团队真的依赖单元测试的方式来进行UI开发,而QT没有像Webpack类似的hot reload技术,UI的验证效率会非常的低下,甚至于不是我们一个小团队可以承受得起的。
而CPP也是入门门槛极高的编程语言,我们对于QT方案也存疑,但是没有完全放弃。


Flutter 的什么特性吸引了我们



  • Flutter使用基于Skia绘图引擎直接构建组件,操作系统只需要提供像素级的绘图能力即可,因此也就保证了跨平台的UI一致性(像素级一致),而对React Native的兼容性吐槽一直充斥着社区。

  • Dart对于UI开发也是非常舒服的。

    • 对象默认引用传递。

    • 支持HOT Reload。这为开发效率带来本质的提升,使得Flutter研发效率不弱于HTML5

    • AOT支持,生产级代码运行效率飞升,不逊色于原生应用的表现。

    • FFI 支持 。 可以直接与原生C & Cpp代码进行交互而几乎没有任何性能损失。

    • Web 支持。 Flutter 即可直接编译到Web运行,这也为我们提供Web服务打下了可能性。




Flutter的这些特性都是直击我们需求的,所以我们决定尝试使用Flutter来构建我们的平台。


结论


如果你也在寻找一个技术技术方案兼顾研发效率与运行时效率,那么Flutter应该是一个很不错的选择。


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

10分钟用Flutter写出摸鱼桌面版App

起因摸鱼是不可能摸鱼的,这辈子都不会摸鱼。缘起是 郭佬 分享了一则微博:share.api.weibo.cn/share/26900…顿时这个念头划过了我的脑海:好东西,但是我用的是 MacBook,不能用这个应用。但是貌似我可以自己写一个...
继续阅读 »

起因

摸鱼是不可能摸鱼的,这辈子都不会摸鱼。

缘起是 郭佬 分享了一则微博:share.api.weibo.cn/share/26900…

image-20211217002031951

顿时这个念头划过了我的脑海:好东西,但是我用的是 MacBook,不能用这个应用。但是貌似我可以自己写一个?

准备工作

年轻最需要的就是行动力,想到就干,尽管我此刻正在理顺 DevFest 的讲稿,但丝毫不妨碍我用 10 分钟写一个 App。于是我打出了一套组合拳:

  • flutter config --enable-macos-desktop
  • flutter create --platforms=macos touch_fish_on_macos

一个支持 macOS 的 Flutter 项目就创建好了。(此时大约过去了 1 分钟)

开始敲代码

找到资源

我们首先需要一张高清无码的  图片,这里你可以在网上进行搜寻,有一点需要注意的是,使用 LOGO 要注意使用场景带来的版权问题。找到图片后,丢到 assets/apple-logo.png,并在 pubspec.yaml 中加上资源引用:

flutter:
use-material-design: true
+ assets:
+ - assets/apple-logo.png

思考布局

我们来观察一下 macOS 的启动画面,有几个要点:

image-20211217003055127

  • LOGO 在屏幕中间,固定大小约为 100dp;
  • LOGO 与进度条间隔约 100 dp;
  • 进度条高度约 5dp,宽度约 200dp,圆角几乎完全覆盖高度,值部分为白色,背景部分为填充色+浅灰色边框。

(别问我为什么这些东西能观察出来,问就是天天教 UI 改 UI。)

确认了大概的布局模式,接下来我们开始搭布局。(此时大约过去了 2 分钟)

实现布局

首先将 LOGO 居中、着色、设定宽度为 100,上下间隔 100:

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 100),
child: Image.asset(
'assets/apple-logo.png',
color: CupertinoColors.white, // 使用 Cupertino 系列的白色着色
width: 100,
),
),
const Spacer(),
],
),
);

然后在下方放一个相对靠上的进度条:

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 100),
child: Image.asset(
'assets/apple-logo.png',
color: CupertinoColors.white, // 使用 Cupertino 系列的白色
width: 100,
),
),
Expanded(
child: Container(
width: 200,
alignment: Alignment.topCenter, // 相对靠上中部对齐
child: DecoratedBox(
border: Border.all(color: CupertinoColors.systemGrey), // 设置边框
borderRadius: BorderRadius.circular(10), // 这里的值比高大就行
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10), // 需要进行圆角裁剪
child: LinearProgressIndicator(
value: 0.3, // 当前的进度值
backgroundColor: CupertinoColors.lightBackgroundGray.withOpacity(.3),
color: CupertinoColors.white,
minHeight: 5, // 设置进度条的高度
),
),
),
),
],
),
);

到这里你可以直接 run,一个静态的界面已经做好了。(此时大约过去了 4 分钟)

打开 App,你已经可以放在一旁挂机了,老板走到你的身边,可能会跟你闲聊更新的内容。但是,更新界面不会动,能称之为更新界面? 当老板一而再再而三地从你身边经过,发现还是这个进度的时候,也许就已经把你的工资划掉了,或者第二天你因为进办公室在椅子上坐下而被辞退。

那么下一步我们就要思考如何让它动起来。

思考动画

来看看启动动画大概是怎么样的:

Kapture 2021-12-17 at 00.51.40

  • 开始是没有进度条的;
  • 进度条会逐级移动、速度不一定相等。

基于以上两个条件,我设计了一种动画处理方式:

  • 构造分段的时长 (Duration),可以自由组合由多个时长;
  • 动画通过时长的数量决定每个时长最终的进度;
  • 每段时长控制起始值到结束值的间隔。

只有三个条件,简单到起飞,开动!(此时大约过去了 5 分钟)

实现动画

开局一个 AnimationController

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
/// 巧用 late 初始化,节省代码量
late final AnimationController _controller = AnimationController(vsync: this);

/// 启动后等待的时长
Duration get _waitingDuration => const Duration(seconds: 5);

/// 分段的动画时长
List<Duration> get _periodDurations {
return <Duration>[
const Duration(seconds: 5),
const Duration(seconds: 10),
const Duration(seconds: 4),
];
}

/// 当前进行到哪一个分段
final ValueNotifier<int> _currentPeriod = ValueNotifier<int>(1);

接下来实现动画方法,采用了递归调用的方式,减少调用链的控制:

@override
void initState() {
super.initState();
// 等待对应秒数后,开始进度条动画
Future.delayed(_waitingDuration).then((_) => _callAnimation());
}

Future<void> _callAnimation() async {
// 取当前分段
final Duration _currentDuration = _periodDurations[currentPeriod];
// 准备下一分段
currentPeriod++;
// 如果到了最后一个分段,取空
final Duration? _nextDuration = currentPeriod < _periodDurations.length ? _periodDurations.last : null;
// 计算当前分段动画的结束值
final double target = currentPeriod / _periodDurations.length;
// 执行动画
await _controller.animateTo(target, duration: _currentDuration);
// 如果下一分段为空,即执行到了最后一个分段,重设当前分段,动画结束
if (_nextDuration == null) {
currentPeriod = 0;
return;
}
// 否则调用下一分段的动画
await _callAnimation();
}

以上短短几行代码,就完美的实现了进度条的动画操作。(此时大约过去了 8 分钟)

最后一步,将动画、分段二者与进度条绑定,在没进入分段前不展示进度条,在动画开始后展示对应的进度:

ValueListenableBuilder<int>(
valueListenable: _currentPeriod,
builder: (_, int period, __) {
// 分段为0时,不展示
if (period == 0) {
return const SizedBox.shrink();
}
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: CupertinoColors.systemGrey),
borderRadius: BorderRadius.circular(10),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: AnimatedBuilder( // 使用 AnimatedBuilder,在动画进行时触发更新
animation: _controller,
builder: (_, __) => LinearProgressIndicator(
value: _controller.value, // 将 controller 的值绑定给进度
backgroundColor: CupertinoColors.lightBackgroundGray.withOpacity(.3),
color: CupertinoColors.white,
minHeight: 5,
),
),
),
);
},

大功告成,总共用时 10 分钟,让我们跑起来看看效果。(下图 22.1 M)

Kapture 2021-12-17 at 01.15.08

这还原度,谁看了不迷糊呢?🤩从此开启上班摸鱼之路...

打包发布

发布正式版的 macOS 应用较为复杂,但我们可以打包给自己使用,只需要一行命令即可:flutter build macos

成功后,产物将会输出在 build/macos/Build/Products/Release/touch_fish_on_macos.app,双击即可使用。

结语

至此,一个简单的能在 macOS 上运行的摸鱼 Flutter App 就这么开发完成了。完整的 demo 可以访问我的仓库:github.com/AlexV525/fl… 。你可以给它提一些需求,我觉得这样一款软件还是挺有意思的。

可能大多数人都没有想到,编写一个 Flutter 应用,跑在 macOS 上,能有这么简单。当然,看似短暂的 10 分钟并没有包括安装环境、搜索素材、提交到 git 的时间,但在这个时间范围内,完成相关的事情也是绰绰有余。

希望这篇文章能激起你学习 Flutter 的兴趣,或是在桌面端尝试 Flutter 的兴趣,又或是挑战自己编程速度的兴趣(别说 10 分钟不可能,实际上我用的时间还要更少)。


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

收起阅读 »

Flutter(一)Hello, Flutter!

1. 移动端开发演变过程 1.1 原生开发 原生应用程序是指某一个移动平台(比如iOS或安卓)所特有的应用,使用相应平台支持的开发工具和语言,直接调用系统SDK API。 Android原生应用:使用Java或Kotlin直接调用Android SDK开发...
继续阅读 »

1. 移动端开发演变过程


1.1 原生开发


原生应用程序是指某一个移动平台(比如iOS或安卓)所特有的应用,使用相应平台支持的开发工具和语言,直接调用系统SDK API。




  • Android原生应用:使用Java或Kotlin直接调用Android SDK开发的应用程序;




  • iOS原生应用:通过Objective-C或Swift语言直接调用iOS SDK开发的应用程序。




主要优势:




  • 可直接无障碍的访问平台全部功能;




  • 速度快、性能高、可以实现复杂动画及绘制,整体用户体验好;




主要缺点:




  • 开发成本高;不同平台必须维护不同代码,人力成本、测试成本大;




  • 内容固定,动态化弱,大多数情况下,有新功能更新时只能发版,但应用上架、审核是需要周期的,这对高速变化的互联网时代来说是很难接受的;




针对动态化和开发成本两个问题,诞生了一些跨平台的动态化框架。👇👇👇👇👇👇


1.2 跨平台技术简介


这里的跨平台指Android和iOS两个平台。根据其原理,主要分为三类:




  • H5+原生(Cordova、Ionic、微信小程序)=> Hybrid/混合开发





  • 原理:APP需要动态变化的内容通过H5来实现,进一步通过原生的网页加载控件WebView (Android)或WKWebView(iOS)来加载。






  • WebView实质上是一个浏览器内核,其JavaScript依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。所以,对于H5不能实现的功能,都需要原生去做。






  • 核心:混合框架会在原生代码中预先实现一些访问系统能力的API, 暴露给WebView以供JavaScript调用,让WebView成为JavaScript与原生API之间通信的桥梁,主要负责JavaScript与原生之间传递调用消息 => JsBridge(WebView JavaScript Bridge)





  • JavaScript开发+原生渲染 (React Native、Weex、快应用)




这里主要介绍下 React Native 特点




  • 和 React 原理相似,支持响应式编程,开发者只需关注状态转移,不需要关注UI绘制;




  • React 和 React Native 主要的区别在于虚拟DOM映射的对象是什么:





  • React中虚拟DOM最终会映射为浏览器DOM树;






  • RN中虚拟DOM会通过 JavaScriptCore 映射为原生控件树。(两步)







  • 第一步:布局消息传递; 将虚拟DOM布局信息传递给原生;








  • 第二步:原生根据布局信息通过对应的原生控件渲染控件树;






  • 优点:





  • 采用Web开发技术栈,社区庞大、上手快、开发成本相对较低。






  • 原生渲染,性能相比H5提高很多。






  • 动态化较好,支持热更新。





  • 不足:





  • 渲染时需要JavaScript和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿;






  • JavaScript为脚本语言,执行时需要JIT(Just In Time),执行效率和AOT(Ahead Of Time)代码仍有差距;






  • 由于渲染依赖原生控件,不同平台的控件需要单独维护。





RN架构升级,进行中,解决频繁和原生通信的瓶颈问题。)




  • 自绘UI+原生(QT for mobile、Flutter)





  • QT for mobile(是移动端开发跨平台自绘引擎的先驱,也是烈士。)






  • Flutter 👇👇👇👇👇👇





2. Flutter 介绍


Flutter 是 Google 推出并开源的移动应用框架,主打跨平台、高保真、高性能。


2.1 跨平台自绘引擎


Flutter 与用于构建应用程序的其他框架不同,是因为 Flutter 既不使用 WebView 也不使用操作系统的原生控件。相反,Flutter 自己实现了自绘引擎。这样不仅能保证一套代码可以同时运行在 IOS 和 Android 平台上,还保证了 Android 和 IOS 上 UI 的一致性,而且也可以避免对原生控件依赖带来的限制和高昂的成本维护,也不用和native层做过多的通信,大大提高性能。


Flutter 使用 Skia 作为其2D渲染引擎,Skia 是一个 Google 的2D图形处理函数,包含字符、坐标转换,以及点阵图都有高效能且简介的表现,Skia 是跨平台的,并提供了非常友好的 API ,目前 Google Chrome 浏览器和 Android 均采用 Skia 作为其绘图引擎。


目前 Flutter 默认支持 Android、IOS、Fuchsia(Google新的自研操作系统)、鸿蒙四个移动平台,也支持 Web 开发(Flutter for web)、 PC 、小程序的开发。


2.2 采用Dart语言


这是一个很有意思也很有争议的问题,Flutter为什么选择Dart语言?


开发效率高。Dart运行时和编译器支持Flutter两个关键特性的组合:


基于JIT的快读开发周期:Flutter在开发阶段采用JIT模式。这样就避免了每次改动都要进行编译,极大节省了开发时间;并且在iOS和Android模拟器或真机上可以实现毫秒级热重载,并且不会丢失状态。


基于AOT的发布包:Flutter 在发布时间可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力(虽然可以通过打包工具实现)。



目前,程序主要有两种运行方式:静态编译与动态解释。

静态编译:在执行前全部被翻译为机器码

👉 AOT(Ahead of time)即“提前编译”; AOT程序的典型代表是用C/C++开发的应用,他们必须在执行前编译成机器码; 动态解释(解释执行)则是一句一句边翻译边运行

👉 JIT(Just-in-time)即“即时编译”。 JIT的代表则非常多,如JavaScript、Python等。

事实上,所有脚本语言都支持JIT模式。但需要注意的是,JIT和AOT 指的是程序运行方式,和编译语言并非强相关,有些语言既可以以JIT方式运行也可以以AOT方式运行,如Java、Python,他们可以在第一次执行时编译成中间字节码,然后在之后执行时可以直接执行字节码,也许有人会说,中间字节码并非机器码,在程序执行时仍需动态将字节码转换成机器码,是的,这没有错,不过通常我们区分是否为AOT的标准就是看代码在执行前是否需要编译,只要需要编译,无论其编译产物是字节码还是机器码,都属于AOT(不必纠结于概念,概念是为了传达精神而发明的,理解原理即可)。



高性能。Flutter提供流畅、高保真的UI体验,为了实现这一点,Flutter中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现丢帧的周期性暂停的问题,而Dart支持AOT,在这一点上可以比JavaScript做的更好。


类型安全。由于Dart 是类型安全的语言,支持静态类型检查,所以可以在编译前发现一些类型错误,并排除潜在问题。这一点对于前端开发者极具吸引力,为了解决JavaScript弱类型的缺点,前端社区出现了很多给JavaScript代码添加静态类型检测的扩展语言和工具,如:微软的TypeScript、Facebook的Flow。而 Dart 本身就支持静态类型,这是他的一个重要优势。


快速内存分配。Flutter框架使用函数式流,这使得它在很大程度上依赖底层内存分配器。因此,拥有一个能够有效的处理琐碎任务的内存分配器将显得十分重要。但其实在内存分配上Dart并没有超越JavaScript,只是Flutter需要,Dart 恰好满足。


Dart 团队就在 Flutter身边。Dart 语言也是谷歌推出的,由于有Dart团队的积极投入,Flutter团队可以获得更多、更方便的支持。



例如:Dart 最初并没有提供原生二进制文件的工具链(这对于实现可预测的高性能有很大帮助),但是现在它实现了,因为 Dart 团队专门为 Flutter 构建了它。 Dart VM 之前已经针对吞吐量进行了优化,但团队现在正忙于优化VM的延迟,这对于Flutter的工作负载更为重要。



2.3 高性能


Flutter 高性能主要是靠以上刚刚介绍的两点来保证的:


采用 Dart 语言开发。Dart 在 JIT(即时编译)模式下,速度与 JavaScript 基本持平,但是 Dart 支持 AOT,当以 AOT 模式运行时,JavaScript 就远远追不上了。速度的提升对高帧率下的数据计算很有帮助。


使用自己的渲染引擎来绘制 UI,布局数据由 Dart 语言直接控制,在布局过程中不需要像 RN 那样要在 JavaScript 和 Native 之间通信,这在一些滑动拖动场景下有明显优势,因为在滑动和拖动过程中往往都会引起布局发生变化,所以 JavaScript 需要在 Native 之间不停的同步布局信息,这和在浏览器中要 JavaScript 频繁操作 DOM 所带来的问题是相同的,都会带来比较大的性能开销。


2.4 响应式框架


借鉴 React 响应式的 UI 框架设计思想。中心思想是用 widget 构建你的 UI。 Widget 描述了他们的视图在给定其当前配置和状态时应该看起来像什么。当 widget 的状态发生变化时,widget 会重新构建 UI,Flutter 会对比前后变化的不同, 来确定底层渲染树从一个状态转换到下一个状态所需的最小更改(类似于 React/Vue 中虚拟 DOM 的 diff 算法)。


2.5 多端编译


2.5.1 移动端




  • 打包 Android 并发布手机商店 👉 VSCode





  • 打包命令



    flutter build apk





  • 发布:







  • 注册各大应用市场开发者账号








  • 创建要发布的app信息








  • 按照流程上传






  • 打包 IOS 并发布 Apple Store 👉 XCode





  • 打包命令



    flutter build ios





  • 发布:







  • 注册 Apple 开发者








  • 创建要发布的app信息








  • 配置钥匙串








  • 在xcode配置apple stoer的证书和bundle id








  • xcode>Product>Archive>Distribute App







2.5.2 Web端




  • Flutter 2.x




  • 检查是否支持web开发 $ flutter devices







  • 打包命令


    flutter build web --web-renderer html # 打开速度最快,兼容性好
    flutter build web # 打开速度一般,兼容性好
    flutter build web --web-renderer canvaskit # 打开速度最慢,兼容性好




  • 部署:直接部署到服务器上即可




3. Flutter 框架结构


3.1 移动架构



3.1.1 Framework 框架层


框架层。这是一个纯dart实现的响应式框架,它实现了一套基础库。




  • 底下两层(Foundation、Animation、Painting、Gestures)在Google的一些视频中被合并为一个Dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。




  • Rendering 层,是一个抽象层,它依赖于 Dart UI ,这一层会构建一个UI树,当UI有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟dom。Rendering层可以说是Flutter UI 框架的核心,它除了确定每个UI元素的位置、大小之外,还要调用底层dart:ui进行坐标变化和绘制。




  • Widgets层是Flutter提供的一套基础组件




  • Material 和 Cupertino 是 Flutter 提供的两种视觉风格的组件库,Material 安卓风格,Cupertino 是IOS风格。




实际开发过程中,主要都是和最上面的Widgets、Material/Cupertino两层打交道。


3.1.2 Engine 引擎层


纯C++实现的SDK,为Flutter的核心,提供了Flutter核心API的底层实现。其中包括了Dart运行时、Skia引擎、文字排版引擎等,是连接框架和系统(Android/IOS)的桥梁。在代码调用dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。


3.1.3 Embedder 嵌入层


嵌入层基本是由平台对应的语言来实现的。例如:在Android上,是由Java和C++ 来实现的,IOS是由Object-C和Object-C++来实现的。


嵌入层为Flutter提供了一个入口,Flutter系统是通过该入口访问底层系统提供的服务,例如输入法、绘制surface等。


3.2 Web端架构



3.3 移动端开发方式架构比较



4. 移动端开发对比


4.1 方案对比



4.2 性能对比



感兴趣 这里 有详细的性能对比介绍,不赘述。


5. Flutter 周边——调试工具


Flutter日志 和 print(); 是比较常用的检查数据的调试方法,但是不支持图形化界面、布局等的检查。👇👇👇👇👇👇


5.1 inspector 插件


可视化和浏览Flutter Widget树的工具。查看渲染树,了解布局信息。


5.2 Dev Tools


5.3 UME


字节 Flutter Infra 团队推出的开源 应用内 调试工具。



Pub.dev 发布地址

pub.flutter-io.cn/packages/fl…

GitHub 开源地址

github.com/bytedance/f…



功能 => UI 检查、代码查看、日志查看、性能工具 ...


服务于 => 设计师、产品经理、研发工程师或质量工程师 ...



  • UI 检查插件包。点选 widget 获取 widget 基本信息、代码所在目录、widget 层级结构、RenderObject 的 build 链与描述的能力,颜色拾取与对齐标尺在视觉验收环节提供有力帮助。







  • 代码查看插件。允许用户输入关键字,进行模糊匹配,对任意代码内容的查看能力。




  • 日志查看。日志信息可通过统一面板提供筛选、导出等。




  • 性能检测包。提供了当前 VM 对象实例数量与内存占用大小等信息。




6. Flutter 周边——学习路线


6.1 学习社区




  • Flutter 中文网 👉 入门文档




  • Flutter 中文社区 👉 入门文档




  • 咸鱼Flutter 👉 可借鉴的架构方案




  • 掘进Flutter 👉 专业性强的博客




  • StackOverflow 👉 问答社区,我遇到的flutter的问题,这里的答案最靠谱




  • 源码及注释 👉 🙋‍♂️🙋‍♂️🙋‍♂️🙋‍♂️🙋‍♂️🙋‍♂️ 最方便开发时查找




6.2 学习路线


1、准备期



  • 目标:




  1. 通过学习 Dart 基本语法,了解 Flutter 中的 Dart 的基本使用;




  2. 学习 Flutter 提供的基础布局和基本内容组件,完成基本页面展示。





2、入门期



3、进阶期



6.3 学习开源项目


UI组件集项目 FlutterUnit


7. Flutter 周边——JS2Flutter



JS 转 Dart => JS2Flutter


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

一兜糖 APP :用 Flutter 链接关于家的一切

一兜糖家居,成立于2009年,2015年获得腾讯系战略投资,截至2020年平台已累积3000万装修用户、5万设计师和10万经销商。自成立以来,我们通过「找灵感——学知识——做决策」全路径的优质内容输出,帮助用户提高消费决策的效率;通过门店线上化、口碑线上化和获...
继续阅读 »

一兜糖家居,成立于2009年,2015年获得腾讯系战略投资,截至2020年平台已累积3000万装修用户、5万设计师和10万经销商。自成立以来,我们通过「找灵感——学知识——做决策」全路径的优质内容输出,帮助用户提高消费决策的效率;通过门店线上化、口碑线上化和获客线上化,帮助品牌全面种草;通过内容、社群及用户资源,全面释放设计师价值,帮助设计师提高选品及获客效率。通过链接关于家的一切,我们希望让家变得有温度。



说重点


一兜糖 APP 在 2020 年开始尝试使用 Flutter 重构 iOS / Android 应用,使用 4 个月时间逐步迁移至 Flutter 工作流上。然后又使用一个月的时间,将 H5 以及微信小程序迁移到 MPFlutter 上。\


在重构完成后,一兜糖仅使用一套代码,便完成了在 iOS / Android / H5 / 微信小程序四端的部署,提升近四倍开发效率。


本文将与您分享一兜糖在重构过程中遇到的问题和解决方案。


一兜糖 APP 重构前的问题


2015 年,一兜糖家居开始发行 Android / iOS 两个平台的 APP,经过五年多的打磨,APP 内容日渐丰富,风格也越来越贴近用户使用习惯。直至 2020 年 4 月份,一兜糖家居 APP 均采用原生(Swift / Java)开发的方式进行,随着业务的快速增长以及五年多的技术迭代,面临的问题也越来越多。



  • Android / iOS 两个平台开发进度不统一,实现效果差异性较大,随着业务需求的累积,差异越来越大。

  • 项目日常维护开发所需人力越来越多,开发成本越来越高,Android / iOS 需要开发人员分别进行开发,同一个功能需要双倍的工作量来完成,如果把 H5 / 小程序涵括在内,同一个功能甚至需要四倍的工作量。

  • 将近五年的技术变更,冗余代码越来越多,导致项目的编译时长越来越长,往往一个小小的变动,都要经过漫长的等待时间后才能看到改动后的效果。


为了解决以上问题,2020 年 4 月,一兜糖 APP 开发团队决定对 APP 进行重构,希望重构能够实现快速开发,减少两个平台的差异等问题。而此时,市面上 APP 开发方案除了原生开发,跨平台方案也已经相对成熟,使用哪种技术重构 APP 成为一兜糖移动团队需要面临的第一个抉择。


原生 VS 跨平台


原生开发的方式,两个平台的开发人员分别进行开发,可以以一种很高效的方式调度系统资源,性能更加优秀。缺点也很明显:



  • 同一个业务却需要对两个平台分别进行开发,增加开发成本,业务代码无法在两个平台上复用。

  • 重构时间只有短短的两个月,如果要以原生的方式对 APP 进行整体重构,不现实,人力不足也会增加重构失败的风险。

  • 两个平台的差异性大,无法给不同平台的用户提供一致的使用体验。

  • 原生开发的方式,需要分别开发相应的需求,后续的测试流程,也需要测试人员分别对相应平台进行测试,增加测试成本。


而跨平台方案则完美解决了以上问题,一兜糖 APP 开发团队尝试使用 Flutter 跨平台方案重构 APP。


为什么选择 Flutter


市面上跨平方技术方案除 Flutter 之外,还有多种其他技术,为何最终定型 Flutter 呢?团队主要从以下几点考虑。


节省人力


跨平台方案能够有效减少适配多个平台所带来的工作量,提高业务代码的复用率,原本需要两个平台开发人员一起开发的工作,现在只需要投入一半的人力即可完成需求。同样是跨平台方案,React Native 则需要投入更多的精力用于 UI 兼容等琐碎的事项。


性能上限


相比其他跨平台方案,Flutter 通过 Skia 引擎渲染 UI 视图,这种渲染方式其性能上限非常接近原生。也因为 Flutter 的自绘引擎设计,跨平台 UI 一致性得以保证,因 iOS / Android 系统升级而导致的适配问题更少,可维护性更高,开发人员也可以更专注于业务开发而无需担心平台兼容问题。


声明式 UI


有别于命令式编程,Flutter 使用声明式 UI 设计。使用声明式 UI,开发人员只需要描述当前的 UI 状态,无须关注不同 UI 状态之间的转换,状态转换的相关逻辑则交由 Flutter 框架处理。这无疑提高了开发人员的效率,开发人员可以更加专注于页面的结构。


热重载


在调试模式下,Flutter 支持运行时编译即热重载功能。热重载模块首先扫描工程中发生改动的文件,接着将发生变化的 Dart 代码编译转化为 Dart Kernel 文件,然后将增量的 Dart Kernel 文件发送给 Dart VM,Dart VM 则将接收到的增量文件与原本的 Dart Kernel 文件合并,重新加载合并后的 Dart Kernel 文件,最后在确认 Dart VM 资源加载成功后,重置 UI 线程,完成 Widget 的重建工作。


热重载对于页面的改动,无需重新启动 APP 即可呈现。可以保存页面的视图状态,对于一些在页面栈很深情况,无需为恢复视图栈而重复大量调试步骤,节省调试复杂交互界面的时间。


Flutter 生态


Flutter 开发社区生态活跃,开发中遇到的问题基本都可以在开发社区上找到解决方案。


开工大吉


我们按照以下步骤重构应用



  1. 统一中心思想,做好全员培训工作。

  2. 设计应用架构,以渐进式的方式过渡到 Flutter 工作流。

  3. 逐步重构,有计划地重构。

  4. 拆除旧有代码。


全员培训工作


在重构工作开始时,除 TL 外,团队成员均无 Flutter / Dart 开发经验。


重构工作的关键在于人,我们通过数次重要的会议,告知团队成员目前面临的困境,以及使用  Flutter 重构能为团队带来的收益,并鼓励团队成员尽快学习 Flutter / Dart,让成员感知该技术的便利性。


通过2~3周的学习,团队成员均已上手 Flutter,具备重构条件。


渐进式地过渡到 Flutter


一兜糖家居 APP 已经迭代了五年多,使用 Flutter 重构 APP 的时候,会面临另一个抉择:是以原生为主,Flutter 为辅还是以 Flutter 为主,原生为辅?


市面上大部分 APP 使用 Flutter 的过程中,都是小范围使用 Flutter,绝大部分功能以原生的方式实现,新功能则尝试使用 Flutter 进行开发。这样对 APP 的改动及影响会保持在一个很小的范围,然而这对于开发人员来说却不够友好,调试的时候,需要在原生模块和 Flutter 模块来回切换,比较麻烦;原生项目唤起 Flutter 页面时,也需要大量的资源支持,这同样不利于提升 APP 的性能。因此,一兜糖移动团队决定以 Flutter 为主,原生为辅 的方式重构 APP。


而对于 Android/iOS 这两个已经迭代了五年的原生项目,一下子舍弃原有的代码显然不太现实,我们先创建一个新的 Flutter 项目(ydt_flutter),将原本的两个原生项目(iOS / Android)迁移到新建的 ydt_flutter 项目目录下。


合并仓库 合并构建流程


完成这一步后,即可开始将功能从原生往 Flutter 的方向迁移了。但如果直接在 lib 目录下开发的话,会有以下几个问题:


开发阶段编译项目的时候,会将原生冗余的代码也编译进去,造成第一次编译项目时需要经过漫长的等待。


不利于项目的迁移,原本高度耦合的项目代码会对重构中的 Flutter 项目造成干扰,也导致调试链的增长。


因此,在将两个原生项目迁移到 ydt_flutter 下后,在该目录下又创建了一个 common 模块(纯 Flutter 项目),后续重构的工作全部放在该模块下进行。这样做的好处是可以区分出 Flutter 重构后的项目和原本的原生项目,还可以简化调试工作,降低了两个项目的耦合度。后续相应的业务模块完成后,返回原生项目将相关的业务代码移除即可,一步步的将项目迁移到 Flutter 上。



逐步重构,有计划地重构。


接下来,是开发团队使用 4 个月的时间,逐步以模块为单位,重构应用。\


我们的应用是内容平台应用,在重构的过程中也同时顺带地配合设计组的同事重新整理了 APP 的 UI 风格,统一整理出 ListItemWidget(列表类型的组件) & WaterfallItem(瀑布流类型的组件)。该 APP 最核心的场景是各类列表与内容详情页,我们可以通过 CustomScrollView 的方式,以搭积木的方法,拼接出所有页面。



CustomScrollView + ListItem + WaterfallItem


通过这种方式,配合 GraphQL 下发的数据,我们拼接出了一个完整的内容应用。在这个过程中,Flutter 的一切皆 Widget 的思想,为重构提供了非常大的便利。


拆除旧有代码


在完成重构以后,便是将原有的原生代码删除,直至拆除上面提到的渐进式架构。


收益


重构对开发团队来说是一件高风险高收益的事,重构过程需要对旧业务进行整理,不仅要填上遗留的暗坑,同时还要避免新坑出现。好处也显而易见,可以解决绝大部分原本令人头痛的问题。


借助 Flutter 的诸多优秀特性,四个月时间,一兜糖家居 APP 重构成功。经过将近一年的线上运营,APP 总体使用和用户反馈良好。借助 CustomScrollView + ListItem + Waterfall,有一些小需求甚至无需发版即可将新改动呈现在用户面前。


重构后的 APP 中 Flutter 覆盖场景超过 90%。Dart 语法对比原生开发所使用的开发语言,语法更加简洁,如 await & async 可以有效的避免原生开发中处处可见的回调地狱,代码逻辑更加清晰。这些新特性,帮助开发人员减少 40% 的日常业务开发时间,所需的人力成本也降为原来的一半,开发效率提升 50%。


因为 Flutter 跨平台的优越性,不仅可以缩短开发周期,后续的测试周期也可以相应缩短,只需编写一次 Flutter 的自动化测试用例,而无需分别对 Android/iOS 编写测试用例,测试效率提升的同时降低测试成本。不仅如此,APP 交付给 UI 设计师阶段,也可以大大节省设计师 UI 走查的时间,走查效率提升 50%。


使用 Flutter 开发后,各开发相关的流程所需成本都显著降低,流程也同步简化了不少。接下来,一兜糖移动开发团队将会致力于将 Flutter 覆盖 APP 中 95% 以上的场景。


额外收获


对于 H5 和 微信小程序,一兜糖团队也尝试使用 MPFlutter 进行重构、迁移。


MPFlutter 是一个跨平台的 Flutter 开发框架,致力于推广 Flutter 至官方未能涉足的领域,如微信小程序。


借助 MPFlutter,一兜糖只用了一个月时间,简单修改一些关键的节点代码,便将应用部署到 H5 和微信小程序中。


体验一兜糖应用


如果您对一兜糖的成果感兴趣,不妨现在就通过以下方式体验。



  • 在 AppStore 或各大安卓应用商店,搜索『一兜糖』下载应用。

  • 在微信搜索『一兜糖』体验小程序。

  • 点击 h5.yidoutang.com/v6/ ,体验一兜糖 H5 网站。


如果您正考虑如何装修新房,或者想知道屋主们的生活状态,欢迎持续使用一兜糖 APP。


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

Google 推荐使用 MVI 架构?卷起来了~

前言 前段时间写了一些介绍MVI架构的文章,不过软件开发上没有最好的架构,只有最合适的架构,同时众所周知,Google推荐的是MVVM架构。相信很多人都会有疑问,我为什么不使用官方推荐的MVVM,而要用你说的这个什么MVI架构呢? 不过我这几天查看Androi...
继续阅读 »

前言


前段时间写了一些介绍MVI架构的文章,不过软件开发上没有最好的架构,只有最合适的架构,同时众所周知,Google推荐的是MVVM架构。相信很多人都会有疑问,我为什么不使用官方推荐的MVVM,而要用你说的这个什么MVI架构呢?

不过我这几天查看Android应用架构指南,发现谷歌推荐的最佳实践已经变成了单向数据流动 + 状态集中管理,这不就是MVI架构吗?
看起来Google已经开始推荐使用MVI架构了,大家也有必要开始了解一下Android应用架构指南的最新版本了~


本文主要基于Android应用架构指南,感兴趣的也可以直接查看原文


总体架构


两个架构原则


Android的架构设计原则主要有两个


分离关注点


要遵循的最重要的原则是分离关注点。一种常见的错误是在一个 ActivityFragment 中编写所有代码。这些基于界面的类应仅包含处理界面和操作系统交互的逻辑。
总得来说,ActivityFragment中的代码应该尽量精简,尽量将业务逻辑迁移到其它层


通过数据驱动界面


另一个重要原则是您应该通过数据驱动界面(最好是持久性模型)。数据模型独立于应用中的界面元素和其他组件。

这意味着它们与界面和应用组件的生命周期没有关联,但仍会在操作系统决定从内存中移除应用的进程时被销毁。

数据模型与界面元素,生命周期解耦,因此方便复用,同时便于测试,更加稳定可靠。


推荐的应用架构


基于上一部分提到的常见架构原则,每个应用应至少有两个层:



  • 界面层 - 在屏幕上显示应用数据。

  • 数据层 - 提供所需要的应用数据。


您可以额外添加一个名为“网域层”的架构层,以简化和复用使用界面层与数据层之间的交互

p1.png


如上所示,各层之间的依赖关系是单向依赖的,网域层,数据层不依赖于界面层


界面层


界面的作用是在屏幕上显示应用数据,并响应用户的点击。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。

不过,从数据层获取的应用数据的格式通常不同于UI需要展示的数据的格式,因此我们需要将数据层数据转化为页面的状态

因此界面层一般分为两部分,即UI层与State HolderState Holder的角色一般由ViewModel承担

p2.png


数据层的作用是存储和管理应用数据,以及提供对应用数据的访问权限,因此界面层必须执行以下步骤:



  1. 获取应用数据,并将其转换为UI可以轻松呈现的UI State

  2. 订阅UI State,当页面状态发生改变时刷新UI

  3. 接收用户的输入事件,并根据相应的事件进行处理,从而刷新UI State

  4. 根据需要重复第 1-3 步。


主要是一个单向数据流动,如下图所示:


p3.png


因此界面层主要需要做以下工作:



  1. 如何定义UI State

  2. 如何使用单向数据流 (UDF),作为提供和管理UI State的方式。

  3. 如何暴露与更新UI State

  4. 如何订阅UI State


如何定义UI State


如果我们要实现一个新闻列表界面,我们该怎么定义UI State呢?我们将界面需要的所有状态都封装在一个data class中。

与之前的MVVM模式的主要区别之一也在这里,即之前通常是一个State对应一个LiveData,而MVI架构则强调对UI State的集中管理


data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf(),
val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)

以上示例中的UI State定义是不可变的。这样的主要好处是,不可变对象可保证即时提供应用的状态。这样一来,UI便可专注于发挥单一作用:读取UI State并相应地更新其UI元素。因此,切勿直接在UI中修改UI State。违反这个原则会导致同一条信息有多个可信来源,从而导致数据不一致的问题。


例如,如上中来自UI StateNewsItemUiState对象中的bookmarked标记在Activity类中已更新,那么该标记会与数据层展开竞争,从而产生多数据源的问题。


UI State集中管理的优缺点


MVVM中我们通常是多个数据流,即一个State对应一个LiveData,而MVI中则是单个数据流。两者各有什么优缺点?

单个数据流的优点主要在于方便,减少模板代码,添加一个状态只需要给data class添加一个属性即可,可以有效地降低ViewModelView的通信成本

同时UI State集中管理可以轻松地实现类似MediatorLiveData的效果,比如可能只有在用户已登录并且是付费新闻服务订阅者时,您才需要显示书签按钮。您可以按如下方式定义UI State


data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf()
){
val canBookmarkNews: Boolean get() = isSignedIn && isPremium
}

如上所示,书签的可见性是其它两个属性的派生属性,其它两个属性发生变化时,canBookmarkNews也会自动变化,当我们需要实现书签的可见与隐藏逻辑,只需要订阅canBookmarkNews即可,这样可以轻松实现类似MediatorLiveData的效果,但是远比MediatorLiveData要简单


当然,UI State集中管理也会有一些问题:



  • 不相关的数据类型:UI所需的某些状态可能是完全相互独立的。在此类情况下,将这些不同的状态捆绑在一起的代价可能会超过其优势,尤其是当其中某个状态的更新频率高于其他状态的更新频率时。

  • UiState diffingUiState 对象中的字段越多,数据流就越有可能因为其中一个字段被更新而发出。由于视图没有 diffing 机制来了解连续发出的数据流是否相同,因此每次发出都会导致视图更新。当然,我们可以对 LiveDataFlow使用 distinctUntilChanged() 等方法来实现局部刷新,从而解决这个问题


使用单向数据流管理UI State


上文提到,为了保证UI中不能修改状态,UI State中的元素都是不可变的,那么如何更新UI State呢?

我们一般使用ViewModel作为UI State的容器,因此响应用户输入更新UI State主要分为以下几步:



  1. ViewModel 会存储并公开UI StateUI State是经过ViewModel转换的应用数据。

  2. UI层会向ViewModel发送用户事件通知。

  3. ViewModel会处理用户操作并更新UI State

  4. 更新后的状态将反馈给UI以进行呈现。

  5. 系统会对导致状态更改的所有事件重复上述操作。


举个例子,如果用户需要给新闻列表加个书签,那么就需要将事件传递给ViewModel,然后ViewModel更新UI State(中间可能有数据层的更新),UI层订阅UI State订响应刷新,从而完成页面刷新,如下图所示:


p4.png


为什么使用单向数据流动?


单向数据流动可以实现关注点分离原则,它可以将状态变化来源位置、转换位置以及最终使用位置进行分离。

这种分离可让UI只发挥其名称所表明的作用:通过观察UI State变化来显示页面信息,并将用户输入传递给ViewModel以实现状态刷新。


换句话说,单向数据流动有助于实现以下几点:



  1. 数据一致性。界面只有一个可信来源。

  2. 可测试性。状态来源是独立的,因此可独立于界面进行测试。

  3. 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。


暴露与更新UI State


定义好UI State并确定如何管理相应状态后,下一步是将提供的状态发送给界面。我们可以使用LiveData或者StateFlowUI State转化为数据流并暴露给UI

为了保证不能在UI中修改状态,我们应该定义一个可变的StateFlow与一个不可变的StateFlow,如下所示:


class NewsViewModel(...) : ViewModel() {

private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

...

}

这样一来,UI层可以订阅状态,而ViewModel也可以修改状态,以需要执行异步操作的情况为例,可以使用viewModelScope启动协程,并且可以在操作完成时更新状态。


class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {

private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

private var fetchJob: Job? = null

fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
}

在上面的示例中,NewsViewModel 类会尝试进行网络请求,然后更新UI State,然后UI层可以对其做出适当反应


订阅UI State


订阅UI State很简单,只需要在UI层观察并刷新UI即可


class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}

UI State实现局部刷新


因为MVI架构下实现了UI State的集中管理,因此更新一个属性就会导致UI State的更新,那么在这种情况下怎么实现局部刷新呢?

我们可以利用distinctUntilChanged实现,distinctUntilChanged只有在值发生变化了之后才会回调刷新,相当于对属性做了一个防抖,因此我们可以实现局部刷新,使用方式如下所示


class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Bind the visibility of the progressBar to the state
// of isFetchingArticles.
viewModel.uiState
.map { it.isFetchingArticles }
.distinctUntilChanged()
.collect { progressBar.isVisible = it }
}
}
}
}

当然我们也可以对其进行一定的封装,给Flow或者LiveData添加一个扩展函数,令其支持监听属性即可,使用方式如下所示


class MainActivity : AppCompatActivity() {
private fun initViewModel() {
viewModel.viewStates.run {
//监听newsList
observeState(this@MainActivity, MainViewState::newsList) {
newsRvAdapter.submitList(it)
}
//监听网络状态
observeState(this@MainActivity, MainViewState::fetchStatus) {
//..
}
}
}
}

关于MVI架构下支持属性监听,更加详细地内容可见:MVI 架构更佳实践:支持 LiveData 属性监听


网域层


网域层是位于界面层和数据层之间的可选层。


p5.png
网域层负责封装复杂的业务逻辑,或者由多个ViewModel重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。因此,您应仅在需要时使用该层。

网域层具有以下优势:



  1. 避免代码重复。

  2. 改善使用网域层类的类的可读性。

  3. 改善应用的可测试性。

  4. 让您能够划分好职责,从而避免出现大型类。


我感觉对于常见的APP,网域层似乎并没有必要,对于ViewModel重复的逻辑,使用util来说一般就已足够

或许网域层适用于特别大型的项目吧,各位可根据自己的需求选用,关于网域层的详细信息可见:developer.android.com/jetpack/gui…


数据层


数据层主要负责获取与处理数据的逻辑,数据层由多个Repository组成,其中每个Repository可包含零到多个Data Source。您应该为应用处理的每种不同类型的数据创建一个Repository类。例如,您可以为与电影相关的数据创建 MoviesRepository 类,或者为与付款相关的数据创建 PaymentsRepository 类。当然为了方便,针对只有一个数据源的Repository,也可以将数据源的代码也写在Repository,后续有多个数据源时再做拆分


p6.png
数据层跟之前的MVVM架构下的数据层并没用什么区别,这里就不多介绍了,关于数据层的详细信息可见:developer.android.com/jetpack/gui…


总结


相比老版的架构指南,新版主要是增加了网域层并修改了界面层,其中网域层是可选的,各位各根据自己的项目需求使用。

而界面层则从MVVM架构变成了MVI架构,强调了数据的单向数据流动状态的集中管理。相比MVVM架构,MVI架构主要有以下优点



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯,在数据一致性,可测试性,可维护性上都有一定优势

  2. 强调对UI State的集中管理,只需要订阅一个ViewState便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. 添加状态只需要添加一个属性,降低了ViewModelView层的通信成本,将业务逻辑集中在ViewModel中,View层只需要订阅状态然后刷新即可


当然在软件开发中没有最好的架构,只有最合适的架构,各位可根据情况选用适合项目的架构,实际上在我看来Google在指南中推荐使用MVI而不再是MVVM,很可能是为了统一AndroidCompose的架构。因为在Compose中并没有双向数据绑定,只有单向数据流动,因此MVI是最适合Compose的架构。


当然如果你的项目中没有使用DataBinding,或许也可以开始尝试一下使用MVI,不使用DataBindingMVVM架构切换为MVI成本不高,切换起来也比较简单,在易用性,数据一致性,可测试性,可维护性等方面都有一定优势,后续也可以无缝切换到Compose


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

使用 Nginx 构建前端日志统计服务

之前的几篇文章都是关于低代码平台的。这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。先放一下整体流程图吧:日志收集在常见的埋点方案中,...
继续阅读 »

背景

之前的几篇文章都是关于低代码平台的。


这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。

今天就来说一下其中的统计服务:目的主要是为了实现 H5 页面的分渠道统计(其实不仅仅是分渠道统计,核心是想做一个自定义事件统计服务,只是目前有分渠道统计的需求),查看每个渠道具体的 PV 情况。(具体会在 url 上面体现,会带上页面名称、id、渠道类型等)

先放一下整体流程图吧:


日志收集

常见的日志收集方式有手动埋点和自动埋点,这里我们不关注于如何收集日志,而是如何将收集的日志的发送到服务器。

在常见的埋点方案中,通过图片来发送埋点请求是一种经常被采纳的,它有很多优势:

  • 没有跨域

  • 体积小

  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)

  • 执行过程无阻塞

这里的方案就是在 nginx 上放一张 1px * 1px 的静态图片,然后通过访问该图片(http://xxxx.png?env=xx&event=xxx),并将埋点数据放在query参数上,以此将埋点数据落到nginx日志中。

iOS 上会限制 get 请求的 url 长度,但我们这里真实场景发送的数据不会太多,所以目前暂时采用这种方案

这里简单阐述一下为什么图片地址的query key 要这么设计,如果单纯是为了统计渠道和作品,很有可能会把key设计为channelworkId这种,但上面也说到了,我们是想做一个自定义事件统计服务,那么就要考虑字段的可扩展性,字段应更有通用语义。所以参考了很多统计服务的设计,这里采用的字段为:

  • env

  • event

  • key

  • value

之后每次访问页面,nginx就会自动记录日志到access_log中。

有了日志,下面我们来看下如何来对其进行拆分。

日志拆分

为何要拆分日志

access.log日志默认不会拆分,会越积累越多,系统磁盘的空间会被消耗得越来越多,将来可能面临着日志写入失败、服务异常的问题。

日志文件内容过多,对于后续的问题排查和分析也会变得很困难。

所以日志的拆分是有必要也是必须的。

如何拆分日志

我们这里拆分日志的核心思路是:将当前的access.log复制一份重命名为新的日志文件,之后清空老的日志文件。

视流量情况(流量越大日志文件积累的越快),按天、小时、分钟来拆分。可以把access.log按天拆分到某个文件夹中。

log_by_day/2021-12-19.log
log_by_day/2021-12-20.log
log_by_day/2021-12-21.log

但上面的复制 -> 清空操作肯定是要自动处理的,这里就需要启动定时任务,在每天固定的时间(我这里是在每天凌晨 00:00)来处理。

定时任务

其实定时任务不仅在日志拆分的时候会用到,在后面的日志分析和日志清除都会用到,这里先简单介绍一下,最终会整合拆分、分析和清除。

linux中内置的cron进程就是来处理定时任务的。在node中我们一般会用node-schedulecron来处理定时任务。

这里使用的是cron

/**
   cron 定时规则 https://www.npmjs.com/package/cron
   *    *    *    *    *    *
           
           
            day of week (0 - 6) (Sun-Sat)
          └───── month (1 - 12)
        └────────── day of month (1 - 31)
      └─────────────── hour (0 - 23)
    └──────────────────── minute (0 - 59)
  └───────────────────────── second (0 - 59)
*/

具体使用方式就不展开说明了。

编码

有了上面这些储备,下面我就来写一下这块代码,首先梳理下逻辑:

1️⃣ 读取源文件 access.log

2️⃣ 创建拆分后的文件夹(不存在时需自动创建)

3️⃣ 创建日志文件(天维度,不存在时需自动创建)

4️⃣ 拷贝源日志至新文件

5️⃣ 清空 access.log

/**
* 拆分日志文件
*
* @param {*} accessLogPath
*/
function splitLogFile(accessLogPath) {
 const accessLogFile = path.join(accessLogPath, "access.log");

 const distFolder = path.join(accessLogPath, DIST_FOLDER_NAME);
 fse.ensureDirSync(distFolder);

 const distFile = path.join(distFolder, genYesterdayLogFileName());
 fse.ensureFileSync(distFile);
 fse.outputFileSync(distFile, ""); // 防止重复,先清空

 fse.copySync(accessLogFile, distFile);

 fse.outputFileSync(accessLogFile, "");
}

日志分析

日志分析就是读取上一步拆分好的文件,然后按照一定规则去处理、落库。这里有一个很重要的点要提一下:node在处理大文件或者未知内存文件大小的时候千万不要使用readFile,会突破 V8 内存限制。正是考虑到这种情况,所以这里读取日志文件的方式应该是:createReadStream创建一个可读流交给 readline 逐行读取处理

readline

readline 模块提供了用于从可读流每次一行地读取数据的接口。 可以使用以下方式访问它:

const readline = require("readline");

readline 的使用也非常简单:创建一个接口实例,传入对应的参数:

const readStream = fs.createReadStream(logFile);
const rl = readline.createInterface({
 input: readStream,
});

然后监听对应事件即可:

rl.on("line", (line) => {
 if (!line) return;

 // 获取 url query
 const query = getQueryFromLogLine(line);
 if (_.isEmpty(query)) return;

 // 累加逻辑
 // ...
});
rl.on("close", () => {
 // 逐行读取结束,存入数据库
 const result = eventData.getResult();
 resolve(result);
});

这里用到了lineclose事件:

  • line事件:每当 input 流接收到行尾输入(\n、\r 或 \r\n)时,则会触发 'line' 事件

  • close事件:一般在传输结束时会触发该事件

逐行分析日志结果

了解了readline 的使用,下面让我们来逐行对日志结果进行分析吧。

首先来看下access.log中日志的格式:

我们取其中一行来分析:

127.0.0.1 - - [19/Feb/2021:15:22:06 +0800] "GET /event.png?env=h5&event=pv&key=24&value=2 HTTP/1.1" 200 5233 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" "-"

我们要拿到的就是urlquery部分,也就是我们在h5中自定义的数据。

通过正则匹配即可:

const reg = /GET\s\/event.png\?(.+?)\s/;
const matchResult = line.match(reg);
console.log("matchResult", matchResult);

const queryStr = matchResult[1];
console.log("queryStr", queryStr);

打印结果为:

queryStr可通过node中的querystring.parse()来处理:

const query = querystring.parse(queryStr);

console.log('query', query)
{
env: 'h5',
event: 'pv',
key: '24',
value: '2'
}

剩下的就是对数据做累加处理了。

但如何去做累加,我们要想一下,最开始也说了是要去做分渠道统计,那么最终的结果应该可以清晰的看到两个数据:

  • 所有渠道的数据

  • 每个渠道单独的数据

只有这样的数据对于运营才是有价值的,数据的好坏也直接决定了后面在每个渠道投放的力度。

这里我参考了 Google Analytics中的多渠道漏斗的概念,由上到下分维度记录每个维度的数据,这样就可以清晰的知道每个渠道的情况了。

具体实现也不麻烦,我们先来看下刚刚从一条链接中得到的有用数据:

{
 env: 'h5',
 event: 'pv',
 key: '24',
 value: '2'
}

这里的env代表环境,这里统计的都是来源于h5页面,所以envh5,但是为了扩展,所以设置了这个字段。

event表示事件名称,这里主要是统计访问量,所以为pv

key是作品 id。

value是渠道 code,目前主要有:1-微信、2-小红书、3-抖音。

再来看下最终统计得到的结果吧:

{
 date: '2021-12-21',
 key: 'h5',
 value: { num: 1276}
}
{
 date: '2021-12-21',
 key: 'h5.pv',
 value: { num: 1000}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12',
 value: { num: 200}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.1',
 value: { num: 56}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.2',
 value: { num: 84}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.3',
 value: { num: 60}
}

这是截取了2021-12-21当天的数据,我给大家分析一波:

1️⃣ h5:当天 h5 页面的自定义事件上报总数为 1276

2️⃣ h5.pv:其中 所有 pv(也就是 h5.pv)为 1000

3️⃣ h5.pv.12:作品 id 为 12 的 pv 一共有 200

4️⃣ h5.pv.12.1:作品 id 为 12 的在微信渠道的 pv 为 56

5️⃣ h5.pv.12.2:作品 id 为 12 的在小红书渠道的 pv 为 84

6️⃣ h5.pv.12.2:作品 id 为 12 的在抖音渠道的 pv 为 60

这样就能清楚的得到某一天某个作品在某条渠道的访问情况了,后续再以这些数据为支撑做成可视化报表,效果就一目了然了。

统计结果入库

目前这部分数据是放在了mongoDB中,关于node中使用mongoDB就不展开说了,不熟悉的可以参考我另外一篇文章Koa2+MongoDB+JWT 实战--Restful API 最佳实践

这里贴下model吧:

/**
* @description event 数据模型
*/
const mongoose = require("../db/mongoose");

const schema = mongoose.Schema(
{
   date: Date,
   key: String,
   value: {
     num: Number,
  },
},
{
   timestamps: true,
}
);

const EventModel = mongoose.model("event_analytics_data", schema);

module.exports = EventModel;

日志删除

随着页面的持续访问,日志文件会快速增加,超过一定时间的日志文件存在的价值也不是很大,所以我们要定期清除日志文件。

这个其实比较简单,遍历文件,因为文件名都是以日期命名的(格式:2021-12-14.log),所以只要判断时间间隔大于 90 天就删除日志文件。

贴一下核心实现:

// 读取日志文件
const fileNames = fse.readdirSync(distFolder);
fileNames.forEach((fileName) => {
 try {
   // fileName 格式 '2021-09-14.log'
   const dateStr = fileName.split(".")[0];
   const d = new Date(dateStr);
   const t = Date.now() - d.getTime();
   if (t / 1000 / 60 / 60 / 24 > 90) {
     // 时间间隔,大于 90 天,则删除日志文件
     const filePath = path.join(distFolder, fileName);
     fse.removeSync(filePath);
  }
} catch (error) {
   console.error(`日志文件格式错误 ${fileName}`, error);
}
});

定时任务整合

到这里,日志的拆分、分析和清除都说完了,现在要用cron来对他们做整合了。

首先来创建定时任务:

function schedule(cronTime, onTick) {
 if (!cronTime) return;
 if (typeof onTick !== "function") return;

 // 创建定时任务
 const c = new CronJob(
   cronTime,
   onTick,
   null, // onComplete 何时停止任务
   true, // 初始化之后立刻执行
   "Asia/Shanghai" // 时区
);

 // 进程结束时,停止定时任务
 process.on("exit", () => c.stop());
}

然后每一阶段都在不同的时间阶段去处理(定时拆分 -> 定时分析 -> 定时删除)

定时拆分

function splitLogFileTiming() {
 const cronTime = "0 0 0 * * *"; // 每天的 00:00:00
 schedule(cronTime, () => splitLogFile(accessLogPath));
 console.log("定时拆分日志文件", cronTime);
}

定时分析并入库

function analysisLogsTiming() {
 const cronTime = "0 0 3 * * *"; // 每天的 3:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => analysisLogsAndWriteDB(accessLogPath));
 console.log("定时分析日志并入库", cronTime);
}

定时删除

function rmLogsTiming() {
 const cronTime = "0 0 4 * * *"; // 每天的 4:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => rmLogs(accessLogPath));
 console.log("定时删除过期日志文件", cronTime);
}

然后在应用入口按序调用即可:

// 定时拆分日志文件
splitLogFileTiming();
// 定时分析日志并入库
analysisLogsTiming();
// 定时删除过期日志文件
rmLogsTiming();

总结

ok,到这里,一个简易的统计服务就完成了。

作者:前端森林
来源:https://segmentfault.com/a/1190000041184489

收起阅读 »

淘特 Flutter 流畅度优化实践

不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在Flutter流畅度方面的诸多优化实践,这些优化不涉及Engine改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带...
继续阅读 »

不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在Flutter流畅度方面的诸多优化实践,这些优化不涉及Engine改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带来显著提升,值得Flutter开发者将其应用在产品的每一个像素。

背景

淘特具备鲜明的三大特征:

  1. 业务特征:淘特拥有业界最复杂的淘系电商链路

  2. 用户特征:淘特用户中有大量的中老年用户,大量的用户手机系统版本较低,大量的用户使用中低端机

  3. 技术特征:淘特大规模采用Flutter跨平台渲染技术

综上所述:

最复杂业务链路+最低性能用户群体+最新的跨平台技术==>核心问题之一:页面流畅度受到严峻挑战

Flutter核心链路20S快速滚动帧率卡顿率(每秒卡顿率)
直播Tab277.04%
我的41.36667.63%
详情26.715.58%

注:相关数据以vivo Y67,淘特

3.32.999.10 (103) 测得

目标

流畅度是用户体验的关键一环,大家都不希望手机用起来像看电影/刷PPT,尤其是现在高刷屏(90/120hz)的普及,更是极大强化了用户对流畅度的感知,但流畅度也跟产品复杂度强相关,也是一次繁与简的取舍,淘特流畅度一期优化目标:

Flutter核心链路页面达到高流畅度(平均帧率:低端机45FPS、中端机50FPS、高端50FPS)

一期优化后的状态

事项平均帧率卡顿率提升效果
1.直播Tab推荐、分类栏目46.00.35%帧率提高19帧、卡顿率降低6.7%
2.我的页面46.00%帧率提高4.6帧,卡顿率降低7.6%
3.详情45.02%帧率提高18.3桢,卡顿率降低13.58%

旧版3.32如视频左,新版3.37如视频右。因uiautomator工具会触发无障碍性能ISSUE,此版本对比为人工测试。

视频请见:淘特 Flutter 流畅度优化实践

除了数据上的明显提升,体感上,旧版快滑卡顿明显,画面突变明显,新版则基本消除明显的卡顿,画面连续平稳。

问题

回到技术本身,Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因:

  1. UI线程慢了-->渲染指令出的慢

  2. GPU线程慢了-->光栅化慢、图层合成慢、像素上屏慢

那么,怎么解上述的 2 个问题是咱们所关心的重点。既然知道某块有问题,我们自然要有工具系统化的度量问题水平,以及系统化的理论支撑实践,且看以下2节,过程中穿插相关策略在淘特的实践, 理论与实践结合理解更透。

怎么解

解法 - 案例

降低setState的触发节点

大家都知道Flutter的刷新机制,在越高的Widget树层级触发setState标脏Element,“脏树越大”,在越低层级越局部的Widget触发状态更新,“脏树越小”,被标记为脏树后将触发Element.Rebuild,遍历组件树。原理请看下图“Flutter页面刷新机制源码解析”:




“Element.updateChild源码分析”请见下文优化二。

实际应用淘特为例。直播Tab的视频预览功能为例,最初直播Tab的视频播放index通过状态层层传递给子组件,一旦状态变更,顶层setState触发播放index更新, 造成整个页面刷新。但实际整个页面需要更新状态的只有“需要暂停的原VideoWidget”和“待播放的VideoWidget”, 我们改为监听机制,页面中的所有VideoWidget注册监听,顶层用EventBus统一分发播放index至各VideoWidget,局部Widget Check后改变自身状态。

再比如详情页,由于使用了“上一个页面借图”的功能,监听到滚动后隐藏借的图,但setState的调用节点放在了详情顶层Widget,造成了全局刷新。实际该监听刷新逻辑可下放至“借图组件”,降低“脏树”的大小。



缓存不变的Widget

缓存不变的Widget有2大好处。1.被缓存的Widget将无需重复创建, 虽然Flutter官方认为Widget是一种非常轻量级的对象,在实际业务中,Build耗时过高仍是一种常见现象。2.返回相同引用的Widget将使Flutter停止该子树后续遍历, 即Flutter认为该子树无变化无需更新。原理请看下图“Element.updateChild源码分析”


应用场景以淘特实际页面为例。详情页部分组件使用了DXWidget,理论上组件内容一经创建后当次页面生命周期不会再有变化,此种情况即可缓存不变的Widget,避免重复动态渲染DX,停止子树遍历。

Feed流的Item组件,布局复杂,创建成本较高,理论上创建一次后内容也不会再变化,但item可能被删除,此时应该用Objectkey唯一标识组件,防止状态错位。



减少不必要的build(setState)

直播Tab用到一个埋点曝光组件,经过DevTools检查,发现其在每一次进度回调中重新创建itemWidget,虽然这不会造成业务异常,但理论上itemWidget只需被创建一次,这块经排查是使用组件时误传了builder函数,而不是直接传itemWidget实例。

详情页的逻辑非常复杂,AppBar根据滚动距离实时计算透明度,这会导致高频的setState,实际上透明度变化前后应该满足一个差值后才应刷新一次状态, 为了性能考量,透明度应该只有少数几种值变更。



多变图层与不变图层分离

在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。以淘特为例。

直播Feed中的Gif图是不断高频跳动,这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。

同理, 秒杀倒计时也是电商常见场景, 该组件也适用于RepaintBoundary场景。



避免频繁的triggerGC

因为AliFlutter的关系,我们得以主动触发DartGC,但GC同样也是有消耗的,高频的GC更是如此。淘特之前因为iOS的内存压力,在列表滚动停止时ScrollEndNotification则会触发GC,ScrollEndNotification在每一次手Down->up事件后都会触发一次,如果用户多次触摸,则会较为频繁的触发GC,实测影响Y67 4帧左右的性能,这块增加页面不可见时GC 和在Y67等android低端机关闭滑动GC,提高滑动性能。


大JSON解析子线程化

Flutter的isolate默认是单线程模型,而所有的UI操作又都是在UI线程进行的,想应用多线程的并发优势需新开isolate 或compute。无论如何await,scheduleTask 都只是延后任务的调用时机,仍然会占用“UI线程”, 所以在大Json解析或大量的channel调用时,一定要观测对UI线程的消耗情况。在淘特中,我们在低端机开启json解析compute化,不阻塞UI线程。

尽量减少或降级Clip、Opacity等组件的使用

Flutter中,Clip主要用于裁剪,裁矩形、圆角矩形、圆形。一旦调用,后续所有的绘图指令都会受其Clip影响。有些ClipRRect可以用ShapeDecoration代替,Opacitiy改用AnimatedOpacity, 针对图片的Clip裁切,可以走定制图片库Transform实现。


降级CustomScrollView预渲染区域为合理值

默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,即如双列瀑布流,当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(加载更多时),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,淘特在低端机降级该区域距离为0或较小值。


高频埋点 Channel批量化操作

在组件曝光时上报埋点是很常见的行为,但在快速滚动的场景下, 瞬间10+ item的略过,20+ channel的调用同样会占用一定的UI线程资源和Native UI线程资源。这里淘特针对部分场景做了批量、定时上传, 维护一个埋点队列,默认定时3S或50条,业务不可见时上报,合并20+channel调用为单次。业务也可在合适时机点强制flush队列上报, 同时在Native侧,将埋点行为切换至子线程进行。

其他有效优化措施

部分业务特效,业务繁忙度在低端机上都是可以适度降级的,如淘特将Feed视频预览播放延迟时间从500ms降为1.5S,Feed流预加载阈值距离从2000+降为500,图片圆角降直角等降级措施的核心思路都是先保证最低端的用户也能用的顺畅,再美化细节锦上添花。

Flutter在无障碍开启情况下,快速滚动场景存在性能问题,如确定业务无需无障碍或用户误触发无障碍,可添加ExcludeSemantics Widget屏蔽无障碍。

通过DevTools检测,发现high_available高可用帧率检测在老版本存在性能问题,这块可升级插件版本或低端机屏蔽该检测。

解法 - 优化案例总结

上述十条优化实践,抛开细节看原理,大致分为以下几类, 融会贯通,实践出真知。

如何提高UI线程性能:

  • 如何提高build性能

    • 降低遍历出发点,降低setState的触发节

    • 停止树的遍历,不变的内容,返回同样的组件实例、Flutter将停止遍历该树(SlideTransition)

    • 减少非必要的build(setState)

  • 如何提高layout性能

    • layout暂时不太容易出问题

  • 如何提高paint性能

    • RepaintBoundary分离多变和不变的图层,如Gif、动画, 但多图层的合成也是有开销的

  • 其他

    • 耗时方法如大JSON解析用compute子线程化

    • 减少不必要的channel调用或批量合并

    • 减少动画

    • 减少Release时的log

    • 提高UI线程在Android/iOS的优先级

    • 列表组件支持局部build

    • 较小的cacheExtent值,减少渲染范围

如何提高GPU线程性能:

  1. 谨慎saveLayer

  2. 尽量少ClipPath、一旦调用,后续所有绘图指令需与Path做相交。(ClipRect、ClipRRect等)

  3. 减少毛玻璃BackdropFilter、阴影boxShadow

  4. 减少Opacity使用,必要时用AnimatedOpacity

解法 - 测量工具

工欲善其事,必先利其器。工具主要分为以下两块。

  1. 流畅度检测:无需侵入代码的流畅度检测方案有几种, 既可以通过adb取surfaceflinger数据, 也可以基于VirtualDisplay做图像对比,或者使用官方DevTools。第三方比较成熟的如PerfDog

  2. 卡顿排查:DevTools是官方的开发配套工具,非常实用

    1. Performance检测单帧CPU耗时(build、layout、paint)、GPU耗时、Widget Build次数

    2. CPUProfiler 检测方法耗时

    3. Flutter Inspector观察不合理布局

    4. Memory 监控Dart内存情况

DevTools

Flutter分为三种编译模式,Debug/Release大家都很熟悉,Debug最大特性为HotReload可调试,Release为最高性能,Profile模式则取其中间,专用于性能分析,其产物以AOT模式无限接近Release性能运行,又保留了丰富的性能分析途径。

如何以Profile模式运行flutter?

如果是混合工程,android为例,在app/build.gradle添加profile{init with debug}即可, 部分应用资源区分debug/profile,也可Copy一份profile。当然,更hack更彻底的方式,可直接修改$flutterRoot/packages/flutter_tools/gradle/flutter.gradle文件中buildModeFor方法,默认返回想要的Profile/Release模式。

如何在Profile模式下打开DevTools?

推荐使用IDE的flutter attach 或者 命令行采用flutter pub global run devtools,填入observatory的地址,即可开始使用DevTools。

Flutter Performance&Inspector

以AS为例,右侧会出现Flutter Performance和Inspector2个功能区。Performance功能区如下图:


Overlay效果如下图。可以看到有2排柱状图,上方为GPU帧耗时,下方为CPU耗时,实时显示最近300帧情况,当当前帧耗时超过16ms时,绿色扫描线会变红色, 此图常用于观察动态过程中的“瞬时卡顿点”。


Inspector较为简单,可观看Widget树结构和实际的Render Tree结构,包含基本的布局信息,DevTools中Inspector包含更详细信息。



DevTools&Flutter Inspector


DevTools&Performance



Performance功能是性能优化的核心工具,这里可以分析出大部分UI线程、GPU线程卡顿的原因。为方便分析,此图用Debug模式得来,实际性能分析以Profile模式为准。

如上图1所示,Build函数耗时明显过长,且连续数十帧如此,必然是Build的逻辑有严重问题。理论上Widget创建一次后状态未改变时无需重建。由前文淘特案例可以发现,这里实际是业务错误的在滚动进度回调中重复创建Widget所致。实际的Build应只在瀑布流Layout逻辑中创建执行2次。

Paint函数详情可在debug模式通过debugProfilePaintsEnabled=true开启。当多变的元素与不变的元素混在同一图层时可造成图层整体的过度重复绘制, 如元素内容无变化,Paint函数中也不应出现多余元素的绘制耗时。通过前面提及的Repain RainBow开关或debugRepaintRainbowEnabled=true, 可实时观察重绘情况,如下图所示。

每一个图层都有对应的不同颜色框体。只有发生Repaint的图层颜色会发生变化,多余的图层变色,我们就要排查是否正常。


GPU耗时过多一般源于重量级组件的过度使用如Clip、Opacity、阴影, 这块发现耗时过多可参考前文解法进行优化或降级, 关于GPU更多的优化可参考liyuqian的高性能图形引擎分享。

在图1最下方的CPU Profile即代表当帧的CPU耗时情况,BottomUp方便查找最耗时的方法。

DevTools&CPU Profiler


在Performance的隔壁是CPU Profiler,这里用于统计一段时间内CPU的耗时情况,一般根据方法名结合经验判断是业务异常还是正常耗时,根据visitChilddren-->getScrollRenderObject方法名搜索,发现高可用帧率监控存在性能问题。

Devtools还有内存、Debugger、网络、日志等功能模块,这块流畅度优化中使用不多,后续有更好的经验再和大家分享。

DebugFlags&Build


上图是一张针对build阶段常见的debug功能表, debugPrintRebuildDirtyWidgets开关将在控制台打印什么树当前正在被重建,debugProfileBuildsEnabled作用同Performance的Track Widget Builds,监控Build函数详情。前3个字段在debug模式使用,最后一个可在Profile模式使用。

DebugFlag&Paint

上图是一张针对Paint阶段常见的debug功能表。debugDumpLayerTree()函数可用于打印layer树,debugPaintLayerBordersEnabled可在每一个图层周围形成边界(框),debugRepaintRainbowEnabled作用同Inspector中的RainBow Enable, 图层重绘时边框颜色将变化。debugProfilePaintsEnabled前文已提到,方便分析paint函数详情。


展望

以上便是淘特Flutter流畅度优化第一期实践,也是体感优化最明显的的一期优化。但距离极致的用户体验目标仍有不小的差距。集团同学提供了很多秀实践学习。如UC Hummer的Engine流畅度优化, 闲鱼的局部刷新复用列表组件PowerScrollView、线上线下的高精准多维度检测卡顿,及如何防止流畅度优化不恶化的方案, 淘特也在不断学习成长挑战极限,在二期实践中,为了最极致的体验,淘特将结合Hummer引擎,深度优化高性能图片库、高性能流式容器、建立全面的线下线上数据监控体系,做一个”让用户爽的淘特App“。

参考资料

收起阅读 »

偷偷看了同事的代码找到了优雅代码的秘密

真正的大师永远怀着一颗学徒的心引言对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程...
继续阅读 »

真正的大师永远怀着一颗学徒的心

引言

对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程序猿在这个工程中写好代码的初始热情都没了。相反,如果clone下的代码结构清晰,代码优雅易懂,那么你在写代码的时候都不好意思写烂代码。这其中的差别相信工作过的同学都深有体会,那么我们看了那么多代码之后,到底什么样的代码才是好代码呢?它们有没有一些共同的特征或者原则?本文通过阐述优雅代码的设计原则来和大家聊聊怎么写好代码。

代码设计原则

好代码是设计出来的,也是重构出来的,更是不断迭代出来的。在我们接到需求,经过概要设计过后就要着手进行编码了。但是在实际编码之前,我们还需要进行领域分层设计以及代码结构设计。那么怎么样才能设计出来比较优雅的代码结构呢?有一些大神们总结出来的优雅代码的设计原则,我们分别来看下。

SRP

所谓SRP(Single Responsibility Principle)原则就是职责单一原则,从字面意思上面好像很好理解,一看就知道什么意思。但是看的会不一定就代表我们就会用,有的时候我们以为我们自己会了,但是在实际应用的时候又会遇到这样或者那样的问题。原因就是实际我们没有把问题想透,没有进行深度思考,知识还只是知识,并没有转化为我们的能力。就比如这里所说的职责单一原则指的是谁的单一职责,是类还是模块还是域呢?域可能包含多个模块,模块也可以包含多个类,这些都是问题。

为了方便进行说明,这里以类来进行职责单一设计原则的说明。对于一个类来说,如果它只负责完成一个职责或者功能,那么我们可以说这个类时符合单一职责原则。请大家回想一下,其实我们在实际的编码过程中,已经有意无意的在使用单一职责设计原则了。因为实际它是符合我们人思考问题的方式的。为什么这么说呢?想想我们在整理衣柜的时候,为了方便拿衣服我们会把夏天的衣服放在一个柜子中,冬天的衣服放在一个柜子。这样季节变化的时候,我们只要到对应的柜子直接拿衣服就可以了。否则如果冬天和夏天的衣服都放在一个柜子中,我们找衣服的时候可就费劲了。放到软件代码设计中,我们也需要采用这样的分类思维。在进行类设计的时候,要设计粒度小、功能单一的类,而不是大而全的类。

举个栗子,在学生管理系统中,如果一个类中既有学生信息的操作比如创建或者删除动作,又有关于课程的创建以及修改动作,那么我们可以认为这个类时不满足单一职责的设计原则的,因为它将两个不同业务域的业务混杂在了一起,所以我们需要进行拆分,将这个大而全的类拆分为学生以及课程两个业务域,这样粒度更细,更加内聚。

笔者根据自身的经验,总结了需要考虑进行单一职责拆分的几个场,希望对大家判断是否需要进行拆分有个简单的判断的标准: 1、不同的业务域需要进行拆分,就像上面的例子,另外如果与其他类的依赖过多,也需要考虑是不是应该进行拆分; 2、如果我们在类中编写代码的时候发现私有方法具有一定的通用性,比如判断ip是不是合法,解析xml等,那我们可以考虑将这些方法抽出来形成公共的工具类,这样其他类也可以方便的进行使用。 另外单一职责的设计思想不止在代码设计中使用,我们在进行微服务拆分的时候也会一定程度的遵循这个原则。

OCP

OCP(Open Closed Principle)即对修改关闭,对扩展开放原则,个人觉得这是设计原则中最难的原则。不仅理解起来有一定的门槛,在实际编码过程中也是不容易做到的。 首先我们得先搞清楚这里的所说的修改以及扩展的区别在什么地方,说实话一开始看到这个原则的时候,我总觉得修改和开放说的不是一个意思嘛?想来想去都觉得有点迷糊。后来在不断的项目实践中,对这个设计原则的理解逐渐加深了。

设计原则中所说的修改指的是对原有代码的修改,而扩展指的是在原有代码基础上的能力的扩展,并不修改原先已有的代码。这是修改与扩展的最大的区别,一个需要修改原来的代码逻辑,另一个不修改。因此才叫对修改关闭但是对扩展开放。弄清楚修改和扩展的区别之后,我们再想一想为什么要对修改关闭,而要对扩展开放呢? 我们都知道软件平台都是不断进行更新迭代的,因此我们需要不断在原先的代码中进行开发。那么就会涉及到一个问题如果我们的代码设计的不好,扩展性不强,那么每次进行功能迭代的时候都会修改原先已有的代码,有修改就有可能引入bug,造成系统平台的不稳定。因此我们为了平台的稳定性,需要对修改关闭。但是我们要添加新的功能怎么办呢?那就是通过扩展的方式来进行,因此需要实现对扩展开放。

这里我们以一个例子来进行说明,否则可能还是有点抽象。在一个监控平台中,我们需要对服务所占用CPU、内存等运行信息进行监控,第一版代码如下。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

代码逻辑很简单,就是根据对应的告警规则中的阈值判断是否达到触发告警通知的条件。如果此时来了个需求,需要增加判断的条件,就是根据服务对应的状态,判断需不需要进行告警通知。我们来先看下比较low的修改方法。我们在checkServiceStatus方法中增加了服务状态的参数,同事在方法中增加了判断状态的逻辑。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

很显然这种修改方法非常的不友好,为什么这么说呢?首先修改了方法参数,那么调用该方法的地方可能也需要修改,另外如果改方法有单元测试方法的话,单元测试用例必定也需要修改,在原有测试过的代码中添加新的逻辑,也增加了bug引入的风险。因此这种修改的方式我们需要进行避免。那么怎么修改才能够体现对修改关闭以及对扩展开放呢? 首先我们可以先将关于服务状态的属性抽象为一个ServiceStatus 实体,在对应的检查方法中以ServiceStatus 为入参,这样以后如果还有服务状态的属性增加的话,只需要在ServiceStatus 中添加即可,并不需要修改方法中的参数以及调用方法的地方,同样单元测试的方法也不用修改。

@Data
public class ServiceStatus {
   String serviecName;
   int cpu;
   int memory;
   int status;

}

另外在检测方法中,我们怎么修改才能体现可扩展呢?而不是在检测方法中添加处理逻辑。一个比较好的实现方式就是通过抽象检测方法,具体的实现在各个实现类中。这样即使新增检测逻辑,只需要扩展检测实现方法就可,不需要在修改原先代码的逻辑,实现代码的可扩展。

LSP

LSP(Liskov Substitution Principle)里氏替换原则,这个设计原则我觉得相较于前面的两个设计原则来说要简单些。它的内容为子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

我们怎么判断有没有违背LSP呢?我觉得有两个关键点可以作为判断的依据,一个是子类有没有改变父类申明需要实现的业务功能,另一个是否违反父类关于输入、输出以及异常抛出的规定。

ISP

ISP(Interface Segregation Principle)接口隔离原则,简单理解就是只给调用方需要的接口,它不需要的就不要硬塞给他了。这里我们举个栗子,以下是关于产品的接口,其中包含了创建产品、删除产品、根据ID获取产品以及更新产品的接口。如果此时我们需要对外提供一个根据产品的类别获取产品的接口,我们应该怎么办?很多同学会说,这还不简单,我们直接在这个接口里面添加根据类别查询产品的接口就OK了啊。大家想想这个方案有没有什么问题。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public class UserServiceImpl implements UserService { //...}

这个方案看上去没什么问题,但是再往深处想一想,外部系统只需要一个根据产品类别查询商品的功能,,但是实际我们提供的接口中还包含了删除、更新商品的接口。如果这些接口被其他系统误调了可能会导致产品信息的删除或者误更新。因此我们可以将这些第三方调用的接口都隔离出来,这样就不存在误调用以及接口能力被无序扩散的情况了。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public interface ThirdSystemProductService{
   List<Product> getProductByType(int type);
}

public class UserServiceImpl implements UserService { //...}

LOD

LOD(Law of Demeter)即迪米特法则,这是我们要介绍的最后一个代码设计法则了,光从名字上面上看,有点不明觉厉的感觉,看不出来到底到底表达个什么意思。我们可以来看下原文是怎么描述这个设计原则的。 Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers. 按照我自己的理解,这迪米特设计原则的最核心思想或者说最想达到的目的就是尽最大能力减小代码修改带来的对原有的系统的影响。所以需要实现类、模块或者服务能够实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。打个比方这就像抗战时期的的地下组织一样,相关联的聚合到一起,但是与外部保持尽可能少的联系,也就是低耦合。

总结

本文总结了软件代码设计中的五大原则,按照我自己的理解,这五大原则就是程序猿代码设计的内功,而二十三种设计模式实际就是内功催生出来的编程招式,因此深入理解五大设计原则是我们用好设计模式的基础,也是我们在平时设计代码结构的时候需要遵循的一些常见规范。只有不断的在设计代码-》遵循规范-》编写代码-》重构这个循环中磨砺,我们才能编写出优雅的代码。

作者:慕枫技术笔记
来源:https://juejin.cn/post/7046404022143549447

收起阅读 »

二维码扫码登录是什么原理

前几天看了极客时间一个二维码的视频,写的不错,这里总结下在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码...
继续阅读 »

前几天看了极客时间一个二维码的视频,写的不错,这里总结下

在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码,防止上当受骗。

二维码,大家再熟悉不过了

购物扫个码,吃饭扫个码,坐公交也扫个码

在扫码的过程中,大家可能会有疑问:这二维码安全吗?会不会泄漏我的个人信息?更深度的用户还会考虑:我的系统是不是也可以搞一个二维码来推广呢?

这时候就需要了解一下二维码背后的技术和逻辑了!

二维码最常用的场景之一就是通过手机端应用扫描PC或者WEB端的二维码,来登录同一个系统。 比如手机微信扫码登录PC端微信,手机淘宝扫码登录PC端淘宝。 那么就让我们来看一下,二维码登录是怎么操作的!

二维码登录的本质

二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!

  1. 告诉系统我是谁

  2. 向系统证明我是谁

比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;

那么扫码登录是怎么做到这两件事情的呢?我们一起来考虑一下

手机端应用扫PC端二维码,手机端确认后,账号就在PC端登录成功了!这里,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。

所以,第一件事情,告诉系统我是谁,是比较清楚的!

通过扫描二维码,把手机端的账号信息传递到PC端,至于是怎么传的,我们后面再说

第二件事情,向系统证明我是谁。扫码登录过程中,用户并没有去输入密码,也没有输入验证码,或者其他什么码。那是怎么证明的呢?

有些同学会想到,是不是扫码过程中,把密码传到了PC端呢? 但这是不可能的。因为那样太不安全的,客户端也根本不会去存储密码。我们仔细想一下,其实手机端APP它是已经登录过的,就是说手机端是已经通过登录认证。所说只要扫码确认是这个手机且是这个账号操作的,其实就能间接证明我谁。

认识二维码

那么如何做确认呢?我们后面会详细说明,在这之前我们需要先认识一下二维码! 在认识二维码之前我们先看一下一维码!

所谓一维码,也就是条形码,超市里的条形码--这个相信大家都非常熟悉,条形码实际上就是一串数字,它上面存储了商品的序列号。

二维码其实与条形码类似,只不过它存储的不一定是数字,还可以是任何的字符串,你可以认为,它就是字符串的另外一种表现形式,

在搜索引擎中搜索二维码,你可以找到很多在线生成二维码的工具网站,这些网站可以提供字符串与二维码之间相互转换的功能,比如 草料二维码网站

在左边的输入框就可以输入你的内容,它可以是文本、网址,文件........。然后就可以生成代表它们的二维码

你也可以把二维码上传,进行”解码“,然后就可以解析出二维码代表的含义

系统认证机制

认识了二维码,我们了解一下移动互联网下的系统认证机制。

前面我们说过,为了安全,手机端它是不会存储你的登录密码的。 但是在日常使用过程中,我们应该会注意到,只有在你的应用下载下来后,第一次登录的时候,才需要进行一个账号密码的登录, 那之后呢 即使这个应用进程被杀掉,或者手机重启,都是不需要再次输入账号密码的,它可以自动登录。

其实这背后就是一套基于token的认证机制,我们来看一下这套机制是怎么运行的,

  1. 账号密码登录时,客户端会将设备信息一起传递给服务端,

  2. 如果账号密码校验通过,服务端会把账号与设备进行一个绑定,存在一个数据结构中,这个数据结构中包含了账号ID,设备ID,设备类型等等

const token = {
 acountid:'账号ID',
 deviceid:'登录的设备ID',
 deviceType:'设备类型,如 iso,android,pc......',
}

然后服务端会生成一个token,用它来映射数据结构,这个token其实就是一串有着特殊意义的字符串,它的意义就在于,通过它可以找到对应的账号与设备信息,

  1. 客户端得到这个token后,需要进行一个本地保存,每次访问系统API都携带上token与设备信息。

  2. 服务端就可以通过token找到与它绑定的账号与设备信息,然后把绑定的设备信息与客户端每次传来的设备信息进行比较, 如果相同,那么校验通过,返回AP接口响应数据, 如果不同,那就是校验不通过拒绝访问

从前面这个流程,我们可以看到,客户端不会也没必要保存你的密码,相反,它是保存了token。可能有些同学会想,这个token这么重要,万一被别人知道了怎么办。实际上,知道了也没有影响, 因为设备信息是唯一的,只要你的设备信息别人不知道, 别人拿其他设备来访问,验证也是不通过的。

可以说,客户端登录的目的,就是获得属于自己的token。

那么在扫码登录过程中,PC端是怎么获得属于自己的token呢?不可能手机端直接把自己的token给PC端用!token只能属于某个客户端私有,其他人或者是其他客户端是用不了的。在分析这个问题之前,我们有必要先梳理一下,扫描二维码登录的一般步骤是什么样的。这可以帮助我们梳理清楚整个过程,

扫描二维码登录的一般步骤

大概流程

  1. 扫码前,手机端应用是已登录状态,PC端显示一个二维码,等待扫描

  2. 手机端打开应用,扫描PC端的二维码,扫描后,会提示"已扫描,请在手机端点击确认"

  3. 用户在手机端点击确认,确认后PC端登录就成功了

可以看到,二维码在中间有三个状态, 待扫描,已扫描待确认,已确认。 那么可以想象

  1. 二维码的背后它一定存在一个唯一性的ID,当二维码生成时,这个ID也一起生成,并且绑定了PC端的设备信息

  2. 手机去扫描这个二维码

  3. 二维码切换为 已扫描待确认状态, 此时就会将账号信息与这个ID绑定

  4. 当手机端确认登录时,它就会生成PC端用于登录的token,并返回给PC端

好了,到这里,基本思路就已经清晰了,接下来我们把整个过程再具体化一下

二维码准备

按二维码不同状态来看, 首先是等待扫描状态,用户打开PC端,切换到二维码登录界面时。

  1. PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端

  2. 服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定

  3. 然后把二维码ID返回给PC端

  4. PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)

  5. 为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息

二维码已经准好了,接下来就是扫描状态

扫描状态切换

  1. 用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID

  2. 再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端

  3. 服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端

  4. 因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描

那么为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。

在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的,

状态确认

最后就是状态的确认了。

  1. 手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认

  2. 服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户PC端登录的token

  3. 这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到用户登录的token

  4. 到这里,登录就成功了,后端PC端就可以用token去访问服务端的资源了

扫码动作的基础流程都讲完了,有些细节还没有深入介绍,

比如二维码的内容是什么?

  • 可以是二维码ID

  • 可以是包含二维码ID的一个url地址

在扫码确认这一步,用户取消了怎么处理? 这些细节都留给大家思考

总结

我们从登陆的本质触发,探索二维码扫码登录是如何做到的

  1. 告诉系统我是谁

  2. 向系统证明我谁

在这个过程中,我们先简单讲了两个前提知识,

  • 一个是二维码原理,

  • 一个是基于token的认证机制。

然后我们以二维码状态为轴,分析了这背后的逻辑: 通过token认证机制与二维码状态变化来实现扫码登录.

需要指出的是,前面的讲的登录流程,它适用于同一个系统的PC端,WEB端,移动端。

平时我们还有另外一种场景也比较常见,那就是通过第三方应用来扫码登录,比如极客时间/掘金 都可以选择微信/QQ等扫码登录,那么这种通过第三方应用扫码登录又是什么原理呢?

感兴趣的同学可以思考研究一下,欢迎在评论区留下你的见解。


作者:望道同学
来源:https://juejin.cn/post/6940976355097985032

收起阅读 »

Nginx 配置在线一键生成“神器”

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。网址:https://nginxconfig.io/操...
继续阅读 »

Nginx作为一个轻量级的HTTP服务器,相比Apache优势也是比较明显的,在性能上它占用资源少,能支持更高更多的并发连接,从而达到提高访问效率;在功能上它是一款非常优秀的代理服务器与负载均衡服务器;在安装配置上它安装,配置都比较简单。

但在实际的生产配置环境中,肯定会经常遇到需要修改、或者重新增加Nginx配置的问题,有的时候需求更是多种多样,修修改改经常会出现这样、那样的一些错误,特别的烦索。

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。

网址:https://nginxconfig.io/

NGINX Config 支持 HTTP、HTTPS、PHP、Python、Node.js、WordPress、Drupal、缓存、逆向代理、日志等各种配置选项。在线生成 Web 服务器 Nginx 配置文件。

操作配置也非常简单,你需要做的只需要2步:

  • 打开官方网站

  • 按需求配置相关参数

系统就会自动生成特定的配置文件。虽然界面是英文的,但是功能的页面做的非常直观,生成的Nginx格式规范。

案例展示

配置域名:mingongge.com 实现用户访问*.mingongge.com 域名时会自动跳转到 mingongge.com 此配置,并且开启http强制跳转到https的配置。

这时,Nginx的配置就会实时自动生成在下面,我把生成的配置复制过来,如下:

/etc/nginx/sites-available/mingongge.com.conf#文件名都给你按规则配置好了
server {
listen 443 ssl http2;

server_name mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

# security
include nginxconfig.io/security.conf;

# additional config
include nginxconfig.io/general.conf;
}

# subdomains redirect
server {
listen 443 ssl http2;

server_name *.mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

return 301 https://mingongge.com$request_uri;
}

# HTTP redirect
server {
listen 80;

server_name .mingongge.com;

include nginxconfig.io/letsencrypt.conf;

location / {
return 301 https://mingongge.com$request_uri;
}
}

非常的方便与快速。

官方还提供一些Nginx的基础优化配置,如下:

/etc/nginx/nginx.conf
# Generated by nginxconfig.io

user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
multi_accept on;
worker_connections 65535;
}

http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
client_max_body_size 16M;

# MIME
include mime.types;
default_type application/octet-stream;

# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

# load configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

还有基于安全的配置,如下:

/etc/nginx/nginxconfig.io/security.conf
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;

# . files
location ~ /\.(?!well-known) {
deny all;
}

都相当于是提供一些基础的模板配置,可以根据自己的实际需求去修改。

有了这个神器在手,再也不用为配置Nginx的各类配置而烦恼了!!

作者:民工哥

来源:https://www.bbsmax.com/A/kjdw09OwzN/

收起阅读 »

如何搭建一套前端团队的组件系统

使用第三方组件库优缺点快速开发系统管理或中台产品B端产品比较适合,用户群体比较小众,重点在与功能和业务逻辑上手简单,学习成本低体积大,用户访问时间过长,对于C端产品,时间就是金钱,除非部署在高性能服务器或者使用cdn弥补,需要更轻量级组件永恒不变的风格,产品没...
继续阅读 »

伴着公司业务发展,开源的组件库已无法满业务需要,搭建一套更适合公司业务的UI组件库,势在必行,目前市面上有很多功能强大且完善的组件库,比如基于react的开源组件库antDesign,vue的开源组件库elementUI等。

使用第三方组件库优缺点

优点

  • 快速开发系统管理或中台产品

  • B端产品比较适合,用户群体比较小众,重点在与功能和业务逻辑

  • 上手简单,学习成本低

缺点

  • 体积大,用户访问时间过长,对于C端产品,时间就是金钱,除非部署在高性能服务器或者使用cdn弥补,需要更轻量级组件

  • 永恒不变的风格,产品没有差异性

自己搭建组件库相比第三方的优点

  • 打包体积小,更轻量,更贴近业务使用场景

  • 采用内部组件库安全性更高,防止嵌入攻击还有防止类似antDesign圣诞节彩蛋的suprise

  • 构建和开发更灵活,且组合性更高

搭建流程

  • 搭建打包组件库脚手架

  • 组件系统设计思路和模式

  • 组件库的划分

  • 组件库文档生成

  • 将组件库部署到github并发布到npm

搭建打包组件库脚手架

打包组件库工具有很多

  • rollup,打包js利器,非常轻量,集成tree-shaking

  • create-react-app/vue-cli3,可快速改造一个组件库的脚手架

  • webpack自行封装

  • umi/father,基于rollup和babel组件打包功能,集成docz的文档,支持TypeScript等

组件系统设计思路和模式

可以看到基础UI组件是原子组件,作为各种复杂组件的重要组成部分,只有组件的颗粒度足够细,才能满足业务组件使用,区块组件是我们把相同的业务结合基础UI组件进行封装。

这样一套完整的组件化系统就完成了,其中各个组件之间关系是单向的,业务组件只能包含基础UI组件,不能包含区块组件,区块组件里由基础UI组件和业务组件组成。

组件库的划分

我们的基础UI组件库可以参考目前非常流行的UI组件库antd,划分为:通用、布局、导航、数据录入、数据展示、反馈、其他

具体如下:

组件库文档生成

StoryBook

StoryBookReactVueAngular最受欢迎的UI组件开发工具。它可以在隔离的环境中开发和设计应用程序;也可以那个使用它来快速构建UI组件的文档

安装

yarn add @storybook/react

// package.json设置scripts
"scripts": {  
   "storybook": "start-storybook -p 8000"
}

创建文件例如:Button.stories.js

import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'

import Button from './button'
import '../../styles/index.scss'

const defaultButton = () => (
 <Button onClick={action('clicked')}> default button </Button>
)

const buttonWithSize = () => (
 <>
   <Button size="lg"> large button </Button>
   <Button size="sm"> small button </Button>
 </>
)

const buttonWithType = () => (
 <>
   <Button btnType="primary"> primary button </Button>
   <Button btnType="danger"> danger button </Button>
   <Button btnType="link" href="https://www.baidu.com"> link button </Button>
 </>
)
storiesOf('Button Component', module)
.add('Button', defaultButton)
.add('不同尺寸的 Button', buttonWithSize)
.add('不同类型的 Button', buttonWithType)

基于umi/father脚手架

集成了docz文档功能,一个开箱即用的组件库打包工具,省去了很多配置工作。docz文档

将组件库部署到github并发布到npm上

package.json配置github地址

"repository": { 
   "type": "git",
   "url": "https://github.com:riyue/zhixing.git"
}

首先在npm官网注册账号,然后执行如下命令,也可发布到自己团队私服上

// 输入用户名和密码
npm adduser

// 发布
npm publish

结束

至此整个组件系统设计思路介绍完毕,在开发中一些细节没有展开叙述,例如:整个组件系统全局主题色配置、单元测试、代码规范检查等,需要大家在实践中去发现问题并解决问题。

希望本文能帮助到你或者给正在搭建组件系统的你有所启发。

作者:日月之行_
来源:https://juejin.cn/post/6999987294534893599

收起阅读 »

2021年互联网公司“死亡”名单,看看有没有你的老东家

几年前我就知道IT桔子一直在统计互联网公司的死亡清单,没想到他们一直坚持在做,目前统计了2014年至2022年,随时更新,上榜公司表示:简直是社死现场!找ofo小黄车 、学霸君、环球易购、叮咚快买、菜鸟团、巨人教育、买卖宝、DaDa英语等因为是公墓嘛,所有还有...
继续阅读 »

几年前我就知道IT桔子一直在统计互联网公司的死亡清单,没想到他们一直坚持在做,目前统计了2014年至2022年,随时更新,上榜公司表示:简直是社死现场!

这里面包括也很多的知名公司,如:

找ofo小黄车 、学霸君、环球易购、叮咚快买、菜鸟团、巨人教育、买卖宝、DaDa英语等

因为是公墓嘛,所有还有一个让人哭笑不得的功能,就是“上香“,你要不要为你已经倒闭的前公司上个香呢?

在这份名单中,清晰的记录了公司名称,存活时长、倒闭时长、所属行业、公司地点、成立时间、获投状态,我们根据行业来看看,2021年总计倒闭公司821家,都是什么情况。

2021年《教育类》公司死亡清单

2021年《金融类》公司死亡清单

2021年《游戏类》公司死亡清单

2021年《区块链类》公司死亡清单

总计分为23个大类,详细的可以去IT桔子的网站查看。

针对这些行业公司的死亡原因,该网站也进行了统计。

如:手机游戏类公司 公司死亡795家,死亡原因70%都是因为资金问题。

如:交友社区类公司 公司死亡464家,死亡原因40%都是市场定位有问题,30%的原因是产品问题,也有10%的原因是资金问题。

针对除了行业的大类分析,每个公司的死亡原因也进行了详细说明。

如买卖宝,存活时间有14年2个月,曾经获得过红杉资本、腾讯、京东等的融资。估值97.5亿元。

死亡原因:竞争不足。

如杰睿教育,存活时间有10年9个月,曾经获得过赛领资本、千帆资本、真格基金等的融资。估值26.6亿元。

死亡原因:政策监管。

数据说明

死亡公司数据库网页声明:

一、本网页基于IT桔子投资数据库而打造的“死亡公司数据库”,致力于展现中国新经济领域近些年倒闭的创新创业公司;
二、“死亡公司数据库”的公司关闭时间是依据公开媒体报道及部分估算,可能会存在些许误差,但我们着力确保更高的可靠性;
三、IT桔子对所收录公司运营状况的判定来源如下:
1、公开媒体报道公司关闭、破产清算的;
2、公司自身在微信、微博等渠道宣布关闭、破产清算的;
3、公司明显经营异常:公司被注销;公司产品比如APP或微信持续6个月及以上没更新;公司因为监管被抓、无法经营……交叉比对后确认没有持续经营。

这几天也看了不少博主对2022年的进行了预测,但是,我们始终要相信:世界上唯一不变的是一直在变。特别是作为一名在互联网觅食的程序员,一定要提升自己的技术和管理能力,投资自己是最正确的选择。

对于那些想创业的朋友,一定要三思而后行,别一时想不开就出来创业!

守住自己的钱袋子,别乱投资!猥琐发育,只要还有本金,没下赌桌,就还有翻身的机会,不要梭哈!

2022年,我们一起加油!

作者:机器人的秘密探索
来源:https://www.sohu.com/a/515405099_121124372

收起阅读 »

求解“微信群覆盖”的三种方法:暴力,染色,链表,并查集

(1) 题目简介;(3) 思路二:染色法;(5) 思路四:并查集法;(1) 每个微信群由一个唯一的gid标识;(3) 一个用户可以加入多个群;g1{u1, u2, u3}可以看到,用户u1加入了g1与g2两个群。gid和uid都是uint64;(1) ...
继续阅读 »



这是一篇聊算法的文章,从一个小面试题开始,扩展到一系列基础算法,包含几个部分:

(1) 题目简介;

(2) 思路一:暴力法;

(3) 思路二:染色法;

(4) 思路三:链表法;

(5) 思路四:并查集法;

除了聊方案,重点分享思考过程。文章较长,可提前收藏。


第一部分:题目简介


问题提出:求微信群覆盖


微信有很多群,现进行如下抽象:

(1) 每个微信群由一个唯一的gid标识;

(2) 微信群内每个用户由一个唯一的uid标识;

(3) 一个用户可以加入多个群;

(4) 群可以抽象成一个由不重复uid组成的集合,例如:

g1{u1, u2, u3}

g2{u1, u4, u5}

可以看到,用户u1加入了g1与g2两个群。

画外音:

gid和uid都是uint64;

集合内没有重复元素;


假设微信有M个群(M为亿级别),每个群内平均有N个用户(N为十级别).


现在要进行如下操作:

(1) 如果两个微信群中有相同的用户则将两个微信群合并,并生成一个新微信群;

例如,上面的g1和g2就会合并成新的群:

g3{u1, u2, u3, u4, u5};

画外音:集合g1中包含u1,集合g2中包含u1,合并后的微信群g3也只包含一个u1。

(2) 不断的进行上述操作,直到剩下所有的微信群都不含相同的用户为止

将上述操作称:求群的覆盖。


设计算法,求群的覆盖,并说明算法时间与空间复杂度。

画外音:你遇到过类似的面试题吗?


对于一个复杂的问题,思路肯定是“先解决,再优化”,大部分人不是神,很难一步到位。先用一种比较“笨”的方法解决,再看“笨方法”有什么痛点,优化各个痛点,不断升级方案。


第二部分:暴力法


拿到这个问题,很容易想到的思路是:

(1) 先初始化M个集合,用集合来表示微信群gid与用户uid的关系;

(2) 找到哪两个(哪些)集合需要合并;

(3) 接着,进行集合的合并;

(4) 迭代步骤二和步骤三,直至所有集合都没有相同元素,算法结束;


第一步,如何初始化集合?

set这种数据结构,大家用得很多,来表示集合:

(1) 新建M个set来表示M个微信群gid;

(2) 每个set插入N个元素来表示微信群中的用户uid;


set有两种最常见的实现方式,一种是树型set,一种是哈希型set


假设有集合:

s={7, 2, 0, 14, 4, 12}


树型set的实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(lg(n));

(2) 能实现有序查找;

(3) 省空间;


哈希型set实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(1);

(2) 不能实现有序查找;

画外音:求群覆盖,哈希型实现的初始化更快,复杂度是O(M*N)。


第二步,如何判断两个(多个)集合要不要合并?

集合对set(i)和set(j),判断里面有没有重复元素,如果有,就需要合并,判重的伪代码是:

// 对set(i)和set(j)进行元素判断并合并

(1)    foreach (element in set(i))

(2)    if (element in set(j))

         merge(set(i), set(j));


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)判断element是否在第二个集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第三步,如何合并集合?

集合对set(i)和set(j)如果需要合并,只要把一个集合中的元素插入到另一个集合中即可:

// 对set(i)和set(j)进行集合合并

merge(set(i), set(j)){

(1)    foreach (element in set(i))

(2)    set(j).insert(element);

}


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)把element插入到集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第四步:迭代第二步与第三步,直至结束

对于M个集合,暴力针对所有集合对,进行重复元素判断并合并,用两个for循环可以暴力解决:

(1)for(i = 1 to M)

(2)    for(j= i+1 to M)

         //对set(i)和set(j)进行元素判断并合并

         foreach (element in set(i))

         if (element in set(j))

         merge(set(i), set(j));


递归调用,两个for循环,复杂度是O(M*M)。


综上,如果这么解决群覆盖的问题,时间复杂度至少是:

O(M*N) // 集合初始化的过程

+

O(M*M) // 两重for循环递归

*

O(N) // 判重

*

O(N) // 合并

画外音:实际复杂度要高于这个,随着集合的合并,集合元素会越来越多,判重和合并的成本会越来越高。


第三部分:染色法


总的来说,暴力法效率非常低,集合都是一个一个合并的,同一个元素在合并的过程中要遍历很多次。很容易想到一个优化点,能不能一次合并多个集合?


暴力法中,判断两个集合set和set是否需要合并,思路是:遍历set中的所有element,看在set中是否存在,如果存在,说明存在交集,则需要合并。


哪些集合能够一次性合并?

当某些集合中包含同一个元素时,可以一次性合并。


怎么一次性发现,哪些集合包含同一个元素,并合并去重呢?


回顾一下工作中的类似需求:

M个文件,每个文件包含N个用户名,或者N个手机号,如何合并去重?

最常见的玩法是:

cat file_1 file_2 … file_M | sort | uniq > result


这里的思路是什么?

(1) 把M*N个用户名/手机号输出;

(2) sort排序,排序之后相同的元素会相邻

(3) uniq去重,相邻元素如果相同只保留一个;


排序之后相同的元素会相邻”,就是一次性找出所有可合并集合的关键,这是染色法的核心。


举一个栗子

假设有6个微信群,每个微信群有若干个用户:

s1={1,0,5} s2={3,1} s3={2,9}

s4={4,6} s5={4,7} s6={1,8}

假设使用树形set来表示集合。

首先,给同一个集合中的所有元素染上相同的颜色,表示来自同一个集合。

然后,对所有的元素进行排序,会发现:

(1) 相同的元素一定相邻,并且一定来自不同的集合;

(2) 同一个颜色的元素被打散了;

这些相邻且相同的元素,来自哪一个集合,这些集合就是需要合并的,如上图:

(1) 粉色的1来自集合s1,紫色的1来自集合s2,黄色的1来自集合s6,所以s1s2s6需要合并;

(2) 蓝色的4来自集合s4,青色的4来自集合s5,所以s4s5需要合并;


不用像暴力法遍历所有的集合对,而是一个排序动作,就能找到所有需要合并的集合。

画外音:暴力法一次处理2个集合,染色法一次可以合并N个集合。

集合合并的过程,可以想象为,相同相邻元素所在集合,染成第一个元素的颜色

(1) 紫色和黄色,染成粉色;

(2) 青色,染成蓝色;


最终,剩余三种颜色,也就是三个集合:

s1={0,1,3,5,8}

s3={2,9}

s4={4,6,7}


神奇不神奇!!!


染色法有意思么?但仍有两个遗留问题

(1) 粉色1,紫色1,黄色1,三个元素如何找到这三个元素所在的集合s1s2s6呢?

(2) s1s2s6三个集合如何快速合并

画外音:假设总元素个数n=M*N,如果使用树形set,合并的复杂度为O(n*lg(n)),即O(M*N*lg(M*N))。


我们继续往下看。


第四部分:链表法


染色法遗留了两个问题:

步骤(2)中,如何通过元素快速定位集合

步骤(3)中,如何快速合并集合

我们继续聊聊这两个问题的优化思路。


问题一:如何由元素快速定位集合?

普通的集合,只能由集合根(root)定位元素,不能由元素逆向定位root,如何支持元素逆向定位root呢?

很容易想到,每个节点增加一个父指针即可。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* parent;    // 指向父节点

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向它的上级,而只有root->parent=NULL。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         element* temp=x;

         while(temp->parent != NULL){

                   temp= temp->parent;

         }

         return temp;

}


很容易发现,由元素找集合根的时间复杂度是树的高度,即O(lg(n))


有没有更快的方法呢?

进一步思考,为什么每个节点要指向父节点,直接指向根节点是不是也可以。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* root;         // 指向集合根

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向集合的根。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         return x->root;

}


很容易发现,升级后,由元素找集合根的时间复杂度是O(1)

画外音:不能更快了吧。


另外,这种方式,能在O(1)的时间内,判断两个元素是否在同一个集合内

bool in_the_same_set(element* a, element* b){

         return (a->root == b->root);

}

甚为方便。

画外音:两个元素的根相同,就在同一个集合内。


问题二:如何快速进行集合合并? 

暴力法中提到过,集合合并的伪代码为:

merge(set(i), set(j)){

         foreach(element in set(i))

                   set(j).insert(element);

}

把一个集合中的元素插入到另一个集合中即可。


假设set(i)的元素个数为n1,set(j)的元素个数为n2,其时间复杂度为O(n1*lg(n2))。


在“微信群覆盖”这个业务场景下,随着集合的不断合并,集合高度越来越高,合并会越来越慢,有没有更快的集合合并方式呢?


仔细回顾一下:

(1) 树形set的优点是,支持有序查找,省空间;

(2) 哈希型set的优点是,快速插入与查找;


而“微信群覆盖”场景对集合的频繁操作是:

(1) 由元素找集合根;

(2) 集合合并;


那么,为什么要用树形结构或者哈希型结构来表示集合呢?

画外音:优点完全没有利用上嘛。


让我们来看看,这个场景中,如果用链表来表示集合会怎么样,合并会不会更快?

s1={7,3,1,4}

s2={1,6}

如上图,分别用链表来表示这两个集合。可以看到,为了满足“快速由元素定位集合根”的需求,每个元素仍然会指向根。


s1和s2如果要合并,需要做两件事:

(1) 集合1的尾巴,链向集合2的头(蓝线1);

(2) 集合2的所有元素,指向集合1的根(蓝线2,3);


合并完的效果是:

变成了一个更大的集合。


假设set(1)的元素个数为n1,set(2)的元素个数为n2,整个合并的过程的时间复杂度是O(n2)。

画外音:时间耗在set(2)中的元素变化。


咦,我们发现:

(1) 将短的链表,接到长的链表上;

(2) 将长的链表,接到短的链表上;

所使用的时间是不一样的。


为了让时间更快,一律使用更快的方式:“元素少的链表”主动接入到“元素多的链表”的尾巴后面。这样,改变的元素个数能更少一些,这个优化被称作“加权合并”。


对于M个微信群,平均每个微信群N个用户的场景,用链表的方式表示集合,按照“加权合并”的方式合并集合,最坏的情况下,时间复杂度是O(M*N)。

画外音:假设所有的集合都要合并,共M次,每次都要改变N个元素的根指向,故为O(M*N)。


于是,对于“M个群,每个群N个用户,微信群求覆盖”问题,使用“染色法”加上“链表法”,核心思路三步骤:

(1) 全部元素全局排序

(2) 全局排序后,不同集合中的相同元素,一定是相邻的,通过相同相邻的元素,一次性找到所有需要合并的集合

(3) 合并这些集合,算法完成;


其中:

步骤(1),全局排序,时间复杂度O(M*N);

步骤(2),染色思路,能够迅猛定位哪些集合需要合并,每个元素增加一个属性指向集合根,实现O(1)级别的元素定位集合;

步骤(3),使用链表表示集合,使用加权合并的方式来合并集合,合并的时间复杂度也是O(M*N);


总时间复杂度是:

O(M*N)    //排序

+

O(1)        //由元素找到需要合并的集合

*

O(M*N)    //集合合并


神奇不神奇!


神奇不止一种,还有其他方法吗?我们接着往下看。


第五部分:并查集法


分离集合(disjoint set)是一种经典的数据结构,它有三类操作:

Make-set(a):生成一个只有一个元素a的集合;

Union(X, Y):合并两个集合X和Y;

Find-set(a):查找元素a所在集合,即通过元素找集合;


这种数据结构特别适合用来解决这类集合合并与查找的问题,又称为并查集


能不能利用并查集来解决求“微信群覆盖”问题呢?


一、并查集的链表实现


链表法里基本聊过,为了保证知识的系统性,这里再稍微回顾一下。

如上图,并查集可以用链表来实现。


链表实现的并查集,Find-set(a)的时间复杂度是多少?

集合里的每个元素,都指向“集合的句柄”,这样可以使得“查找元素a所在集合S”,即Find-set(a)操作在O(1)的时间内完成


链表实现的并查集,Union(X, Y)的时间复杂度是多少?

假设有集合:

S1={7,3,1,4}

S2={1,6}


合并S1和S2两个集合,需要做两件事情:

(1) 第一个集合的尾元素,链向第二个集合的头元素(蓝线1);

(2) 第二个集合的所有元素,指向第一个集合的句柄(蓝线2,3);


合并完的效果是:

变成了一个更大的集合S1。


集合合并时,将短的链表,往长的链表上接,这样变动的元素更少,这个优化叫做“加权合并”。

画外音:实现的过程中,集合句柄要存储元素个数,头元素,尾元素等属性,以方便上述操作进行。


假设每个集合的平均元素个数是nUnion(X, Y)操作的时间复杂度是O(n)


能不能Find-set(a)与Union(X, Y)都在O(1)的时间内完成呢?

可以,这就引发了并查集的第二种实现方法。


二、并查集的有根树实现


什么是有根树,和普通的树有什么不同?

常用的set,就是用普通的二叉树实现的,其元素的数据结构是:

element{

         int data;

         element* left;

         element* right;

}

通过左指针与右指针,父亲节点指向儿子节点。


而有根树,其元素的数据结构是:

element{

         int data;

         element* parent;

}

通过儿子节点,指向父亲节点。


假设有集合:

S1={7,3,1,4}

S2={1,6}


通过如果通过有根树表示,可能是这样的:

所有的元素,都通过parent指针指向集合句柄,所有元素的Find-set(a)的时间复杂度也是O(1)。

画外音:假设集合的首个元素,代表集合句柄。


有根树实现的并查集,Union(X, Y)的过程如何?时间复杂度是多少?

通过有根树实现并查集,集合合并时,直接将一个集合句柄,指向另一个集合即可。

如上图所示,S2的句柄,指向S1的句柄,集合合并完成:S2消亡,S1变为了更大的集合。


容易知道,集合合并的时间复杂度为O(1)


会发现,集合合并之后,有根树的高度变高了,与“加权合并”的优化思路类似,总是把节点数少的有根树,指向节点数多的有根树(更确切的说,是高度矮的树,指向高度高的树),这个优化叫做“按秩合并”。


新的问题来了,集合合并之后,不是所有元素的Find-set(a)操作都是O(1)了,怎么办?

如图S1与S2合并后的新S1,首次“通过元素6来找新S1的句柄”,不能在O(1)的时间内完成了,需要两次操作。


但为了让未来“通过元素6来找新S1的句柄”的操作能够在O(1)的时间内完成,在首次进行Find-set(“6”)时,就要将元素6“寻根”路径上的所有元素,都指向集合句柄,如下图。

某个元素如果不直接指向集合句柄,首次Find-set(a)操作的过程中,会将该路径上的所有元素都直接指向句柄,这个优化叫做“路径压缩”。

画外音:路径上的元素第二次执行Find-set(a)时,时间复杂度就是O(1)了。


实施“路径压缩”优化之后,Find-set的平均时间复杂度仍是O(1)


稍微总结一下。


通过链表实现并查集:

(1) Find-set的时间复杂度,是O(1)常数时间;

(2) Union的时间复杂度,是集合平均元素个数,即线性时间;

画外音:别忘了“加权合并”优化。


通过有根树实现并查集:

(1) Union的时间复杂度,是O(1)常数时间;

(2) Find-set的时间复杂度,通过“按秩合并”与“路径压缩”优化后,平均时间复杂度也是O(1);


即,使用并查集,非常适合解决“微信群覆盖”问题。


知其然,知其所以然,思路往往比结果更重要

算法,其实还是挺有意思的。

作者:58沈剑
来源:https://mp.weixin.qq.com/s/2MNL4vDpXQR94KGts4JUhA

收起阅读 »

一张图看懂开源许可协议,开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别 以下是上述协议的简单介绍:BSD开源协议BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由...
继续阅读 »

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别



以下是上述协议的简单介绍:
BSD开源协议
BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。

但”为所欲为”的前提当你发布使用了BSD协议的代码,或则以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件:

    如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。
    如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。
    不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD 代码鼓励代码共享,但需要尊重代码作者的著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。

Apache Licence 2.0
Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似:

    需要给代码的用户一份Apache Licence
    如果你修改了代码,需要再被修改的文件中说明。
    在延伸的代码中(修改和有源代码衍生的代码中)需要带有原来代码中的协议,商标,专利声明和其他原来作者规定需要包含的说明。
    如果再发布的产品中包含一个Notice文件,则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可,但不可以表现为对Apache Licence构成更改。

Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。
GPL

我们很熟悉的Linux就是采用了GPL。GPL协议和BSD, Apache Licence等鼓励代码重用的许可很不一样。GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。这也就是为什么我们能用免费的各种linux,包括商业公司的linux和linux上各种各样的由个人,组织,以及商业软件公司开发的免费软件了。

GPL协议的主要内容是只要在一个软件中使用(”使用”指类库引用,修改后的代码或者衍生代码)GPL 协议的产品,则该软件产品必须也采用GPL协议,既必须也是开源和免费。这就是所谓的”传染性”。GPL协议的产品作为一个单独的产品使用没有任何问题,还可以享受免费的优势。

由于GPL严格要求使用了GPL类库的软件产品必须使用GPL协议,对于使用GPL协议的开源代码,商业软件或者对代码有保密要求的部门就不适合集成/采用作为类库和二次开发的基础。

其它细节如再发布的时候需要伴随GPL协议等和BSD/Apache等类似。

LGPL
LGPL是GPL的一个为主要为类库使用设计的开源协议。和GPL要求任何使用/修改/衍生之GPL类库的的软件必须采用GPL协议不同。LGPL 允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。

但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。

GPL/LGPL都保障原作者的知识产权,避免有人利用开源代码复制并开发类似的产品

MIT
MIT是和BSD一样宽范的许可协议,作者只想保留版权,而无任何其他了限制.也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的.

MPL
MPL是The Mozilla Public License的简写,是1998年初Netscape的 Mozilla小组为其开源软件项目设计的软件许可证。MPL许可证出现的最重要原因就是,Netscape公司认为GPL许可证没有很好地平衡开发者对源代码的需求和他们利用源代码获得的利益。同著名的GPL许可证和BSD许可证相比,MPL在许多权利与义务的约定方面与它们相同(因为都是符合OSIA 认定的开源软件许可证)。但是,相比而言MPL还有以下几个显著的不同之处:

◆ MPL虽然要求对于经MPL许可证发布的源代码的修改也要以MPL许可证的方式再许可出来,以保证其他人可以在MPL的条款下共享源代码。但是,在MPL 许可证中对“发布”的定义是“以源代码方式发布的文件”,这就意味着MPL允许一个企业在自己已有的源代码库上加一个接口,除了接口程序的源代码以MPL 许可证的形式对外许可外,源代码库中的源代码就可以不用MPL许可证的方式强制对外许可。这些,就为借鉴别人的源代码用做自己商业软件开发的行为留了一个豁口。
◆ MPL许可证第三条第7款中允许被许可人将经过MPL许可证获得的源代码同自己其他类型的代码混合得到自己的软件程序。
◆ 对软件专利的态度,MPL许可证不像GPL许可证那样明确表示反对软件专利,但是却明确要求源代码的提供者不能提供已经受专利保护的源代码(除非他本人是专利权人,并书面向公众免费许可这些源代码),也不能在将这些源代码以开放源代码许可证形式许可后再去申请与这些源代码有关的专利。
◆ 对源代码的定义
而在MPL(1.1版本)许可证中,对源代码的定义是:“源代码指的是对作品进行修改最优先择取的形式,它包括:所有模块的所有源程序,加上有关的接口的定义,加上控制可执行作品的安装和编译的‘原本’(原文为‘Script’),或者不是与初始源代码显著不同的源代码就是被源代码贡献者选择的从公共领域可以得到的程序代码。”
◆ MPL许可证第3条有专门的一款是关于对源代码修改进行描述的规定,就是要求所有再发布者都得有一个专门的文件就对源代码程序修改的时间和修改的方式有描述。

作者:微wx笑
翻译:https://blog.csdn.net/testcs_dn/article/details/38496107
原文:http://www.mozilla.org/MPL/MPL-1.1.html

收起阅读 »

拒绝白嫖,开源项目作者删库跑路,数千个应用程序无限输出乱码

「我删我自己的开源项目代码,需要经过别人允许吗?」几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「co...
继续阅读 »



「我删我自己的开源项目代码,需要经过别人允许吗?」

几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。

更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「colors.js」的作者 Marak Squires 本人。

一夜之间,Marak Squires 主动删除了「faker.js」和「colors.js」项目仓库的所有代码,让正在使用这两个开源项目的数千位开发者直接崩溃。

「faker.js」和「colors.js」

faker.js 在 npm 上的每周下载量接近 250 万,color.js 每周的下载量约为 2240 万,本次删库的影响是极其严重的,使用这两个项目开发的工具包括 AWS CDK 等。

如果在构建和测试应用时,真实的数据量远远不够,那么 Faker 类工具将帮助开发者生成伪数据。faker.js 就是可为多个领域生成伪数据的 Node.js 库,包括地址、商业、公司、日期、财务、图像、随机数、名称等。

faker.js 支持生成英文、中文等多语种信息,包含丰富的 API,此前版本通常一个月迭代更新一次。faker.js 不仅可以使用在服务器端的 JavaScript,还可以应用在浏览器端的 JavaScript。

现在,faker.js 项目的所有 commit 信息都被改为「endgame」,在 README 中,作者写下这样一句话:「What really happened with Aaron Swartz?」

Swartz 是一位杰出的开发人员,帮助建立了 Creative Commons、RSS 和 Reddit。2011 年,Swartz 被指控从学术数据库 JSTOR 中窃取文件,目的是免费访问这些文件。Swartz 在 2013 年自杀,Squires 提到 Swartz 可能意指围绕这一死亡疑云。

Marak Squires 向 colors.js 提交了恶意代码,添加了一个「a new American flag module」,然后将其发布到了 GitHub 和 npm。

随后他在 GitHub 和 npm 发布了 faker.js 6.6.6,这两个动作引发了同样的破坏性事件。破坏后的版本导致应用程序无限输出奇怪的字母和符号,从三行写着「LIBERTY LIBERTY LIBERTY」的文本开始,后面跟着一系列非 ASCII 字符:

目前,color.js 已经更新了一个可以使用的版本。faker.js 项目尚未恢复,开发者只能通过降级到此前的 5.5.3 版本来解决问题。

为了解决问题,Squires 在 GitHub 上还发布了更新以解决「zalgo 问题」,该问题是指损坏文件产生的故障文本。

「我们注意到在 v1.4.44-liberty-2 版本的 colors 中有一个 zalgo 错误,」Squires 以一种讽刺的语气写道。「我们现在正在努力解决这个问题,很快就会有解决方案。」

在将更新推送到 faker.js 两天后,Squires 发了一条推文,表示自己存储了数百个项目的 GitHub 账户已经被封。Squires 在 1 月 4 日发布了 faker.js 的最新 commit,在 1 月 6 日被封,直到 1 月 7 日推送了 colors.js 的「liberty」版本。然而,从 faker.js 和 colors.js 的更新日志来看,他的账户似乎被解封过。目前尚不清楚 Squires 的帐户是否再次被封。

至此,故事并没有就此结束。Squires 2020 年 11 月发在 GitHub 上的一篇帖子被挖出来,在帖子中他写道自己不再想做免费的工作了。「恕我直言,我不想再用我的免费工作来支持财富 500 强(和其他小型公司),以此为契机,向我发送一份六位数的年度合同,或者 fork 项目并让其他人参与其中。」

Squires 的大胆举动引起了人们对开源开发者的道德和财务困境的关注,这可能是 Marak Squires 行动的目标。大量网站、软件和应用程序依赖开源开发人员来创建基本工具和组件,而所有这些都是免费的,无偿开发人员经常不知疲倦地工作,努力修复其开源软件中的安全问题。

开发者们怎么看

软件工程师 Sergio Gómez 表示:「从 GitHub 删除自己的代码违反了他们的服务条款?WTF?这是绑架。我们需要开始分散托管免费软件源代码。」

「不知道发生了什么,但我将我所有的项目都托管在 GitLab 私有 instance 上,永远不要相信任何互联网服务提供商。」

有网友认为 faker.js 团队的反应有些夸张了,并说道:「没有人会用一个只生成一些虚假数据的包赚大钱。faker.js 的确为开发者生成伪数据节省了一些时间,但我们也可以让实习生编写类似程序来生成数据。这对企业来说并没有那么重要。」

甚至有人认为 Marak 这么做是一种冲动行为,不够理性,并和他之前「卖掉房子购买 NFT」的传闻联系起来,认为 Marak 需要学会控制自己的情绪:

这种说法很快带偏部分网友的看法,有人原本同情开源项目被「白嫖」,但现在已转向认为 Marak 是恶意删库,并指出:「停止维护他的项目或完全删除都是他的权利,但故意提交有害代码是不对的。」

当然,也有人为开源软件(FOSS)开发者的待遇鸣不平:「希望有相关的基金会位 FOSS 开发人员提供资金支持」,而软件的可靠性和稳定性也是至关重要的

有人表示:一些大公司确实不尊重开源项目的版权,滥用开源项目对于 FOSS 开发者来说是绝对不公平的。但 Marak 对 faker.js 的做法并不可取,不是正面例子,存在 Marak 的个人负面原因。

对此,你有什么看法?

作者:机器之心Pro

来源:https://www.163.com/dy/article/GTC8PE5M0511AQHO.html

收起阅读 »

崩溃的一天,西安一码通崩溃背后的技术问题

12月20号,算得上西安崩溃的一天。西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。足足瘫痪超过 15 个多小时!到了下午,新闻甚至提示:这是解决问题的方法吗?今天,我们就试着分析一下这个业务、以及对应的技术问题。西安一码通其它业务我们暂且不分析...
继续阅读 »

1.崩溃的一天

12月20号,算得上西安崩溃的一天。

12月19号新增病例21个,20号新增病例42个,并且有部分病例已经在社区内传播.

西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。

在这样严峻的情况下,作为防控最核心的系统:西安一码通竟然崩溃了,并且崩溃得是那么的彻底。

足足瘫痪超过 15 个多小时!

整整一天的时间呀,多少上班族被堵在地铁口,多少旅客被冻在半路上,进退不能...

到了下午,新闻甚至提示:

为了减轻系统压力,建议广大市民非必要不展码、亮码,在出现系统卡顿时,请耐心等待,尽量避免反复刷新,也感谢广大市民朋友们的理解配合。

这是解决问题的方法吗?

如果真的需要限流来防止系统崩溃,用技术手段来限流是不是会更简单一些,甚至前面加一个 nginx 就能解决的问题。

今天,我们就试着分析一下这个业务、以及对应的技术问题。

2.产品分析

西安一码通其它业务我们暂且不分析,那并不是重点,并且当天也没有完全崩溃,崩溃的仅有扫码功能。

其实这是一个非常典型的大量查询、少数更新的业务,闭着眼睛分析一下,可以说, 90% 以上的流量都是查询。

我们先来看看第一版的产品形态,扫码之后展示个人部分姓名和身份政信息,同时下面展示绿、黄、红码。

这是西安一码通最开始的样子,业务流程仅仅只需要一个请求,甚至一个查询的 SQL 就可以搞定。

到了后来,这个界面做了2次比较大的改版。

第一次改版新增了疫苗接种信息,加了一个边框;第二次改版新增了核酸检测信息,在最下方展示核酸检测时间、结果。

整个页面增加了2个查询业务,如果系统背后使用的是关系数据库,可能会多增加至少2个查询SQL。

基本上就是这样的一个需求,据统计西安有1300万人口,按照最大10%的市民同时扫码(我怀疑不会有这么多),也就是百万的并发量。

这样一个并发量的业务,在互联网公司很常见,甚至比这个复杂的场景也多了去了。

那怎么就崩了呢?

3.技术分析

在当天晚上的官方回复中,我们看到有这样一句话:

12月20日早7:40分左右,西安“一码通”用户访问量激增,每秒访问量达到以往峰值的10倍以上,造成网络拥塞,致使包括“一码通”在内的部分应用系统无法正常使用。“

一码通”后台监控第一时间报警,各24小时驻场通信、网络、政务云、安全和运维团队立即开展排查,平台应用系统和数据库运行正常,判断问题出现在网络接口侧。

根据上面的信息,数据库和平台系统都正常,是网络出现了问题。

我之前在文章《一次dns缓存引发的惨案》画过一张访问示意图,用这个图来和大家分析一下,网络出现问题的情况。

一般用户的请求,会先从域名开始,经过DNS服务器解析后拿到外网IP地址,经过外网IP访问防火墙和负载之后打到服务器,最后服务器响应后将结果返回到浏览器。

如果真的是网络出现问题,一般最常见的问题就是 DNS 解析错误,或者外网的宽带被打满了。

DNS解析错误一定不是本次的问题,不然可能不只是这一个功能出错了;外网的宽带被打满,直接增加带宽就行,不至于一天都没搞定。

如果真的是网络侧出现问题,一般也不需要改动业务,但实际上系统恢复的时候,大家都发现界面回到文章开头提到了第一个版本了。

也就是说系统“回滚”了。

界面少了接种信息和核酸检测信息的内容,并且在一码通的首页位置,新增加了一个核酸查询的页面。

所以,仅仅是网络接口侧出现问题吗?我这里有一点点的疑问。

4.个人分析

根据我以往的经验,这是一个很典型的系统过载现象,也就是说短期内请求量超过服务器响应。

说人话就是,外部请求量超过了系统的最大处理能力。

当然了,系统最大处理能力和系统架构息息相关,同样的服务器不同的架构,系统负载量差异极大。

应对这样的问题,解决起来无非有两个方案,一个是限流,另外一个就是扩容了。

限流就是把用户挡在外面,先处理能处理的请求;扩容就是加服务器、增加数据库承载能力。

上面提到官方让大家没事别刷一码通,也算是人工限流的一种方式;不过在技术体系上基本上不会这样做。

技术上的限流方案有很多,但最简单的就是前面挂一个 Nginx 配置一下就能用;复杂一点就是接入层自己写算法。

当然了限流不能真正的解决问题,只是负责把一部分请求挡在外面;真正解决问题还是需要扩容,满足所有用户。

但实际上,根据解决问题的处理和产品回滚的情况来看,一码通并没有第一时间做扩容,而是选择了回滚。

这说明,在系统架构设计上,没有充分考虑扩容的情况,所以并不能支持第一时间选择这个方案。

5.理想的方案?

上面说那么多也仅仅是个人推测,实际上可能他们会面临更多现实问题,比如工期紧张、老板控制预算等等...

话说回来,如果你是负责一码通公司的架构师,你会怎么设计整个技术方案呢?欢迎大家留言,这里说说我的想法。

第一步,读写分离、缓存。

至少把系统分为2大块,满足日常使用的读业务单独抽取出来,用于承接外部的最大流量。

单独抽出一个子系统负责业务的更新,比如接种信息的更新、核酸信息的变化、或者根据业务定时变更码的颜色。

同时针对用户大量的单查询,上缓存系统,优先读取缓存系统的信息,防止压垮后面的数据库。

第二步,分库分表、服务拆分。

其实用户和用户之间的单个查询是没有关系的,完全可以根据用户的属性做分库分表。

比如就用用户ID取模分64个表,甚至可以分成64个子系统来查询,在接口最前端将流量分发掉,减轻单个表或者服务压力。

上面分析没有及时扩容,可能就是没有做服务拆分,如果都是单个的业务子服务的话,遇到过载的问题很容易做扩容。

当然,如果条件合适的话,上微服务架构就更好了,有一套解决方案来处理类似的问题。

第三步,大数据系统、容灾。

如果在一个页面中展示很多信息,还有一个技术方案,就是通过异步的数据清洗,整合到 nosql 的一张大表中。

用户扫描查询等相关业务,直接走 nosql 数据库即可。

这样处理的好处是,哪怕更新业务完全挂了,也不会影响用户扫码查询,因为两套系统、数据库都是完全分开的。

使用异地双机房等形式部署服务,同时做好整体的容灾、备灾方案,避免出现极端情况,比如机房光缆挖断等。

还有很多细节上的优化,这里就不一一说明了,这里也只是我的一些想法,欢迎大家留言补充。

6.最后

不管怎么分析,这肯定是人祸而不是天灾。

系统在没有经过严格测试之下,就直接投入到生产,在强度稍微大一点的环境中就崩溃了。

比西安大的城市很多,比西安现在疫情还要严重的情况,其它城市也遇到过,怎么没有出现类似的问题?

西安做为一个科技大省,出现这样的问题真的不应该,特别是我看了这个小程序背后使用的域名地址之后。

有一种无力吐槽的感觉,虽然说这和程序使用没有关系,但是从细节真的可以看出一个技术团队的实力。

希望这次能够吸取教训,避免再次出现类似的问题!

作者:纯洁的微笑
出处:https://www.cnblogs.com/ityouknow/p/15719395.html

收起阅读 »

一个命名引发的性能问题

故事背景我最近主要在定位、解决当前项目中的一些性能相关问题。在反馈的问题中,比较严重的问题之一是在户型预览编辑过程,电脑的 CPU 占用率高,及时什么都不做的情况下,CPU 占用也非常的高。同样的,利用 Chrome 提供的 Performance 录制 ⏺ ...
继续阅读 »



故事背景

我最近主要在定位、解决当前项目中的一些性能相关问题。

在反馈的问题中,比较严重的问题之一是在户型预览编辑过程,电脑的 CPU 占用率高,及时什么都不做的情况下,CPU 占用也非常的高。

同样的,利用 Chrome 提供的 Performance 录制 ⏺ 了无任何操作的 JavaScript 调用火焰图,发现 Pixi 内部会利用浏览器的requestAnimationFrame接口,执行自身的 render 方法进行 2D 场景的绘制渲染。

CPU占用率居高不下

发现问题

初步定位,CPU 占用只可能与 2D 场景中 PIXI 的 render() 有关系,使用 Performance 分析事件调用过程中发现,每次在调用需要使用 9ms 的时间

每次Render使用9ms

这个时间是否属于正常的时间范畴呢?

在了解了 PIXI 及绝大部分图形框架后得知,图形框架内部在调用 Render 的过程中其实正常的不会任何过多的计算内容,所有使用到的需要计算生成 Graphics 的地方,都只会生成一遍。

在之后的调用过程中,都会拿到之前生成好的 Buffer 直接进入 Renderer 进行下一帧的渲染,render() 大部分情况下都是在执行脏检查,有任意 Graphics 需要更新时,Graphicsdirty就会被更新,然后重新生成渲染 Buffer

所以,了解了render()方法的作用后,可以确定在静止不动时render()只是执行一些递归判断,不会耗费9ms这么长的时间,这其中定有蹊跷!

在查看到最终调用到的方法部分发现,在 PIXI 内部的render()方法中竟然会调用到 vue 的方法。

这样不正常的方法调用让我立刻想起来 vue 中,对data()返回的对象数据做原型链的改写。以及之前看到过的一篇文章:《一个 Vue 引发的性能问题》

之前在看到这篇文章时,只是觉得是一个非常有意思的案例,虽然结局办法并不见得是最优方法。但发现问题的过程非常有价值,原本打算拿到组里来进行分享。没想到报应不爽,这么快就发现我们的项目也存着这一个这样的问题,且影响程度远远大于这篇文章!

所以立马去检查了,2D 与 3D 场景实例化过程中对场景的定义,发现一个并没有对命名做_$的规范处理,这样会导致 scene2D 中的所有对象,都将会被 vue 处理为 vue 的可观察对象,也就是会在原型链中带入 getter/setter。

export default {
 data() {
   return {
     scene2d: null
  };
},
 created() {
   this.initScene2D();
},
 methods: {
   initScene2D() {
     this.scene2d = Scene2D.getInstance();
  }
}
};

解决办法

查看到 vue 的官方文档解释:

vue官方解释

所以,为了解决这个问题,需要对data()中不希望 vue 挂载原型链实现数据响应的对象做好命名规范处理。

export default {
 data() {
   return {
     $_scene2d: null
  };
},
 created() {
   this.initScene2D();
},
 methods: {
   initScene2D() {
     this.$_scene2d = Scene2D.getInstance();
  }
}
};

对,就这么简单!

在修改了Scene2D在 Vue 组件的命名后,从整体体验感受来讲“轻快”了许多,再次查看 CPU 及内存占用率都降了许多,render()时间的调用降低到0.94ms,对比如图:

Before

After

作者:Yee Wang
来源:https://yeee.wang/posts/42c7.html

收起阅读 »

如何接"地气"的接入微前端

前言微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。“微前端就是…xx 框架,xx 技...
继续阅读 »



前言

微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。

但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。

“微前端就是…xx 框架,xx 技术”

这种话就有点把这种杰出的思路说的局限了,我只能认为他是外行人,来蹭这个词的热度。

在我所负责的项目和团队中,已经有非常大的存量技术栈和页面已经在线上运行,任何迭代升级都必须要保证小心翼翼,万无一失。

可以说,从一定程度来讲,微前端所带来的这些好处是从用户体验和技术维护方面的,对业务的价值并不能量化体现,落地这项技术秉着既要也要还要的指导方针。

我们对存量技术栈一定需要保持敬畏,隔离,影响范围可控的几个基本要素,然后再考虑落地实施微前端方案。

所以,在这个基本要素和指导方针下。要落地这项新的技术时,一定充分充分了解,当前改造站点所存在的技术方案、占比 以及 当前成熟微前端框架已提供的能力差异,切勿生搬硬套。

背景

我所在团队维护的项目都是些 PC 操作后台(Workstation),这些工作台会存在不同的国家,不同时区,不同合作方等等问题。

如果需要开发一个新的页面需求,很可能投入进来的开发人员都来自不同团队,此时我们要在完成现有需求的同时还需要保证多个管理页面的风格统一,设计规范统一,组件统一,交互行为统一这非常困难。

当该业务需要迁移到另外一个工作台时,虽然需要保持逻辑一致,但导航栏、主题等却不同。

当前存量的方案都是采用 Java 直接进行 Template 渲染出 HTML,经过前面几代前辈的迭代,不同系统中已经存在几种不同技术栈产出的页面。

虽然都是 React 来实现的,但是前辈们都非常能折腾,没有一个是按照常规 React 组件形式开发出来的。

比如:

  1. 大部分页面是通过一份 JSON 配置,消费组件生成的页面。

  2. 部分页面是通过另外一个团队定义的 JSON 配置消费组件生成的,与上面 JSON 完全不一样。

  3. 还有一部分页面,是通过一套页面发布平台提供的 JS Bundle 加载出来的。

面对这样的技术背景下,除了微笑的喊 MMP,含泪说着自己听不懂的话(存在即合理,不难要你干吗?),还得接地气出这样一个落地方案。

方案 & 流程图

首先,需要明确的分析出站点所有页面,所需要加载的通用特性:

上述是精简过后的一些通用功能特性,这里简单做下介绍:

  • Layout Loader 用于加载不同工作台的导航

  • DADA Loader 用于加载 JSON 配置的页面

  • Source Code Loader 用于加载 JS Bundle

  • Micro Loader 用于处理微前端加载

  • Log Report 用于日志埋点

  • Time Zone 用于切换时区

  • i18n 用于切换多语言

  • Guider 用于统一管控用户引导

除此以外可能还会存在以下这些页面扩展能力:

  • 安全监控

  • 流量管控

  • 弹窗管控

  • 问卷调查

  • 答疑机器人

粗略统一归类后来看,页面的大体加载流程应该是这样:

实现细则

基于上述一个加载思路,首先需要做的是页面加载路径收口,需要保证所有页面的加载入口是在一个统一的 Loader 下,然后才可以较为系统的处理所有页面的加载生命周期。

在收敛的同时,同样需要保持开放,对核心加载路径要保持插件化开放,随时支持不同的扩展能力,渲染技术栈接入。

插件机制

所以,在主路径上,通过 Loader 加载配置进行处理,这份配置在主路径中提供上下文,然后交由插件进行消费,如图所示:

举个例子,拿一个独立的 JS Bundle 类型的子应用来说:

<div id="root"></div>
<script src="https://cdn.address/schema-resolver/index.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/layout.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/source-code.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/micro-loader.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/i18n.js"></script>

<script>
 SchemaResolver.render(
  {
     micro: true,
     host: "dev.address",
     hfType: "layout1",
     externals: ["//{HOST}/theme1/index.css"],
     // host is cdn prefix, the resource maybe in different env & country
     resource: {
       js: "/index.js",
       css: "/index.css",
    },
  },
  { container: document.querySelector("#root") }
);
</script>

通过上述的 Plugin 引入,即可开启和消费不同的配置。

这里引入了 Layout Plugin,该插件会消费 hfType 字段然后去加载对于的 Layout 资源提供 Container 交给下一个环节。

按照配置,当前页面开启了微前端,那么 Micro Loader 将会消费提供下来的 Container,然后建立沙箱(这里基于 qiankun),再提供 Container 出来。

最后交由 SourceCode Plugin 进行 Bundle 加载运行和渲染。如果这里是另外一种渲染协议或者技术栈,则可以根据不同配置交由不同插件消费 Container。

这个过程中,每个环节的插件是不依赖的,可插拔的

比如:

如果不加载 Layout Plugin 将不会消费 hfType 字段,也就不会将Layout插件逻辑注入到getContainer方法中,那么将直接得到由最外层下穿的Container进行渲染,也就不会有菜单相关透出。

如果不加载 Micro Plugin 同样不会有微前端的逻辑注入,也就不会建立沙箱,那么页面渲染流程将会按照常规模式继续运行。

当前SchemaResolver已经支持的插件有以下几种,详情参考 SchemaResolver

  • MicroLoader – Base on qiankun using this plug-in can make your content loaded through a micro application so that your content can use all the features of the Micro-Front-End.

  • DadaLoader – Use this plugin can make your app render content by Dada.

  • SourceCodeLoader – Use this plugin can load your js\css bundle to render content, our bundle standard is same as qiankun. You can quick start developing your own page through our toolkit lzd-toolkit-asc.

  • LayoutLoader – Use this plugin can make your page load layout(menu), you can use different hfType configuration to switch different layouts.

  • i18n – Use this plugin can make your page have multi-lang. schema.locale will be the mapping of multilingual keys in MCMS. The plugin will inject and register the language automatically.

  • APlus – Use this plugin can make your page have the feature of APlus .Statistics page interaction events, such as pv\uv. With DadaLoader you can even see every module data(click pv, exposed pv) in pages.

  • WalkThrough – Use this plugin can make your page have the feature of Walk Through. One-stop page features guide.

SchemaResolver的插件能力采用plugin-decorator,如要了解更多插件设计思路可以参考:为你的JavaScript库提供插件能力

SchemaResolver plugin feature is base on plugin-decorator. It’s very easy to develop a new plugin.

More information about plugin can read this article Provide plugin capabilities for your JavaScript library

安全迁移

对于我所在团队负责的项目来说,万万做不得一刀切的方案,所以针对现有存量页面,需要完整分析当前存量技术栈:

针对上述存量页面来说,需要从左到右分批进行页面级别控制上线部署,对于左侧部分页面甚至需要做些项目改造后才可部署接入上线。

这类迁移测试需要处理出一套 自动化e2e测试 流程,通过分批迁移同时梳理出 微前端注册表

有了这两项流程保证及范围控制,当前方案所上线内容完全可控,剩下要处理的大部分就是较为重复的体力活了,覆盖率也可量化。

微前端形态

按照上述方案迁移,那么预期的微前端形态将会是:

  1. 每个开启微前端的页面都可成为主应用

  2. 微前端是插件可选项,如果因为微前端导致的业务异常可随时关闭

  3. 同为微前端的页面路由相互之间切换可实现局部刷新形态,而跳转至非微前端注册表中的页面则会直接页面跳转。随着微前端页面覆盖率提高,局部刷新的覆盖率也会逐渐提高

  4. 可通过不同扩展插件,加载不同技术栈类型的存量页面,转换为对应子应用

在SchemaResolver中的注册和调用路径如下:

总结

透过技术看本质,微前端所代表的杰出思维,才是真正解决具体问题关键所在,只有解决了具体的业务问题,这项技术才有价值转换。

不要为了微前端做微前端,不要为了小程序做小程序。

当前,通过 SchemaResolver,可以针对不同角色提供不同的开放能力:

  • 针对平台管理员,提供插件能力开放全局扩展能力

  • 针对页面开发者,提供标准化接入方案路径,提供多种技术栈接入能力,并无感知提供微前端,多语言,埋点,菜单,主题加载等能力。解耦了不同系统公共能力,同时,这种方式可以让页面开发者快速将具体业务逻辑迁移到其他平台。

  • 针对第三方接入者,不需要关心了解系统菜单、主题接入方式,提供统一的接入口径,通过微前端隔离技术栈、隔离子应用样式。最后通过统一的页面系统管控,轻松入住对应平台,同时可以全局看到站点页面情况。

作者:Yee Wang
来源:https://yeee.wang/posts/3469.html

收起阅读 »

为你的JavaScript库提供插件能力

前言最近在做一个中台框架的设计开发,在做了主框架的基础能力后,思考在框架落实真实业务需求过程中,需要对主框架功能有非常多的定制化内容存在。如果在主体框架中做了哪怕一点业务改动,都可能会对后面的拓展性及灵活性有所限制。所以为了让主体框架做的更加灵活、扩展性更搞,...
继续阅读 »



前言

最近在做一个中台框架的设计开发,在做了主框架的基础能力后,思考在框架落实真实业务需求过程中,需要对主框架功能有非常多的定制化内容存在。如果在主体框架中做了哪怕一点业务改动,都可能会对后面的拓展性及灵活性有所限制。

所以为了让主体框架做的更加灵活、扩展性更搞,在主框架有了基础能力后,就不再对主框架做任何非主框架能力的业务功能开发。

要为主框架不断的”开槽”

其实在很多前端库中都有类似的设计,才能够让更多的开发者参与进来,完成各种各样的社区驱动开发。比如:WebpackBabelHexoVuePress等。

那么如何为自己的项目开槽,做插件呢?

调研

在了解了很多插件的项目源码后,发现实现大多大同小异,主要分为以下几个步骤:

  1. 为框架开发安装插件能力,插件容器

  2. 暴露主要生命运行周期节点方法(开槽)

  3. 编写注入业务插件代码

这些框架都是在自己实现一套这样的插件工具,几乎和业务强相关,不好拆离。或者做了一个改写方法的工具类。总体比较离散,不好直接拿来即用。

另外在实现方式上,大部分的插件实现是直接改写某方法,为他在运行时多加一个Wrap,来依次运行外部插件的逻辑代码。

// main.js
const main = {
 loadData:()=>{},
 render:()=>{}
}

// plugin1.js
const plugin1 = {
 render:()=>{}
}

// install.js
const install = (main, plugin) => {
 main.render = ()=>{
   plugin1.render()
   main.render()
}
}

在上述代码中的插件有几个明显的问题:

  • plugin1 无法控制 render() 的顺序

  • main 中无法得知什么函数可能会被插件改写,什么函数不会被插件改写

  • 如果按照模块文件拆分,团队成员中根本不知道 main.js 中的函数修改会存在风险,因为压根看不到 install.js 中的代码

那么后来,为了解决这些问题,可能会变成这样:

const component = {
 hooks:{
   componentWillMounted(){},
   componentDidMounted(){}
},
 mounte(){
   this.hooks.componentWillMounted();
   //... main code
   this.hooks.componentDidMounted();
}
}

const plugin = {
 componentWillMounted(){
   //...
},
 componentDidMounted(){
   //...
}
}

// install.js
const install = (main, plugin) => {
 // 忽略实现细节。
 main.hooks.componentWillMounted = ()=>{
   plugin1.componentWillMounted()
   main.hook.componentWillMounted()
}
 main.hooks.componentDidMounted = ()=>{
   plugin1.componentDidMounted()
   main.hook.componentDidMounted()
}
}

另外,还有一种解法,会给插件中给 next 方法,如下:

// main.js
const main = {
 loadData:()=>{},
 render:()=>{}
}

// plugin1.js
const plugin1 = {
 render:next=>{
   // run some thing before
   next();
   // run some thing after
}
}

// install.js
const install = (main, plugin) => {
 main.render = ()=>{
   plugin1.render(main.render)
}
}

如上,从调研结构来看,虽然都实现了对应功能,但是从实现过程来看,有几个比较明显的问题:

  • 对原函数侵入性修改过多

  • 对方法rewrite操作过多,太hack

  • 对TypeScript不友好

  • 多成员协作不友好

  • 对原函数操作不够灵活,不能修改原函数的入参出参

开搞

在调研了很多框架的实现方案的后,我希望以后我自己的插件库可以使用一个装饰器完成开槽,在插件类中通过一个装饰器完成注入,可以像这样使用和开发:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public method1() {
   console.log('origin method');
}
}

class DemoPlugin extends Plugin {
 @Inject
 public method1(next) {
   next();
   console.log('plugin method');
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());
demoTarget.method1();

// => origin method
// => plugin method

Decorator

并且可以支持对原函数的入参出参做装饰修改,如下:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public method1(name:string) {
   return `origin method ${name}`;
}
}

class DemoPlugin extends Plugin {
 @Inject
 public method1(next, name) {
   return `plugin ${next(name)}`;
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());

console.log(demoTarget.method1('cool'));

// => plugin origin method cool

Promise

当然,如果原函数是一个Promise的函数,那插件也应该支持Promise了!如下:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public methodPromise() {
   return new Promise(resolve => {
     setTimeout(() => resolve('origin method'), 1000);
  });
}
}

class DemoPlugin extends Plugin {
 @Inject
 public async methodPromise(next) {
   return `plugin ${await next()}`;
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());

demoTarget.methodPromise().then(console.log);

// => Promise<plugin origin method>

Duang!

最终,我完成了这个库的开发:plugin-decorator

GitHub: 地址

没错,我就知道你会点Star,毕竟你这么帅气、高大、威猛、酷炫、大佬!

总结

在该项目中,另外值得提的一点是,该项目是我在开发自己的一套中台框架中临时抽出来的一个工具库。

在工具库中采用了:

  • TypeScript

  • Ava Unit Test

  • Nyc

  • Typedoc

整体开发过程是先写测试用例,然后再按照测试用例进行开发,也就是传说中的 TDD(Test Drive Development)。

感觉这种方式,至少在我做库的抽离过程中,非常棒,整体开发流程非常高效,目的清晰。

在库的编译搭建中使用了 typescript-starter 这个库,为我节省了不少搭建项目的时间!

作者:Yee Wang
来源:https://yeee.wang/posts/dfa4.html

收起阅读 »

轻松生成小程序分享海报

小程序海报组件github.com/jasondu/wxa…需求小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小...
继续阅读 »



小程序海报组件

github.com/jasondu/wxa…

需求

小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小的压力;所以使用小程序的canvas是一个不错的选择,但由于canvas水比较深,坑比较多,还有不同海报需要重现写渲染流程,导致代码冗余难以维护,加上不同设备版本的情况不一样,因此小程序海报生成组件的需求十分迫切。

在实际开发中,我发现海报中的元素无非一下几种,只要实现这几种,就可以通过一份配置文件生成各种各样的海报了。

海报中的元素分类

要解决的问题

  • 单位问题

  • canvas隐藏问题

  • 圆角矩形、圆角图片

  • 多段文字

  • 超长文字和多行文字缩略问题

  • 矩形包含文字

  • 多个元素间的层级问题

  • 图片尺寸和渲染尺寸不一致问题

  • canvas转图片

  • IOS 6.6.7 clip问题

  • 关于获取canvas实例

单位问题

canvas绘制使用的是px单位,但不同设备的px是需要换算的,所以在组件中统一使用rpx单位,这里就涉及到单位怎么换算问题。

通过wx.getSystemInfoSync获取设备屏幕尺寸,从而得到比例,进而做转换,代码如下:

const sysInfo = wx.getSystemInfoSync();
const screenWidth = sysInfo.screenWidth;
this.factor = screenWidth / 750; // 获取比例
function toPx(rpx) { // rpx转px
return rpx * this.factor;
}
function toRpx(px) { // px转rpx
return px / this.factor;
},

canvas隐藏问题

在绘制海报过程时,我们不想让用户看到canvas,所以我们必须把canvas隐藏起来,一开始想到的是使用display:none; 但这样在转化成图片时会空白,所以这个是行不通的,所以只能控制canvas的绝对定位,将其移出可视界面,代码如下:

.canvas.pro {
  position: absolute;
  bottom: 0;
  left: -9999rpx;
}

圆角矩形、圆角图片

由于canvas没有提供现成的圆角api,所以我们只能手工画啦,实际上圆角矩形就是由4条线(黄色)和4个圆弧(红色)组成的,如下:

圆弧可以使用canvasContext.arcTo这个api实现,这个api的入参由两个控制点一个半径组成,对应上图的示例

canvasContext.arcTo(x1, y1, x2, y2, r)

接下来我们就可以非常轻松的写出生成圆角矩形的函数啦

/**
* 画圆角矩形
*/
_drawRadiusRect(x, y, w, h, r) {
  const br = r / 2;
  this.ctx.beginPath();
  this.ctx.moveTo(this.toPx(x + br), this.toPx(y));   // 移动到左上角的点
  this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y)); // 画上边的线
  this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br)); // 画右上角的弧
  this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br)); // 画右边的线
  this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br)); // 画右下角的弧
  this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h)); // 画下边的线
  this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br)); // 画左下角的弧
  this.ctx.lineTo(this.toPx(x), this.toPx(y + br)); // 画左边的线
  this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br)); // 画左上角的弧
}

如果是画线框就使用this.ctx.stroke();

如果是画色块就使用this.ctx.fill();

如果是圆角图片就使用

this.ctx.clip();
this.ctx.drawImage(***);

clip() 方法从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。

多段文字

如果是连续多段不同格式的文字,如果让用户每段文字都指定坐标是不现实的,因为上一段文字的长度是不固定的,这里的解决方案是使用ctx.measureText (基础库 1.9.90 开始支持)Api来计算一段文字的宽度,记住这里返回宽度的单位是px(),从而知道下一段文字的坐标。

超长文字和多行文字缩略问题

设置文字的宽度,通过ctx.measureText知道文字的宽度,如果超出设定的宽度,超出部分使用“...”代替;对于多行文字,经测试发现字体的高度大约等于字体大小,并提供lineHeight参数让用户可以自定义行高,这样我们就可以知道下一行的y轴坐标了。

矩形包含文字

这个同样使用ctx.measureText接口,从而控制矩形的宽度,当然这里用户还可以设置paddingLeft和paddingRight字段;

文字的垂直居中问题可以设置文字的基线对齐方式为middle(this.ctx.setTextBaseline('middle');),设置文字的坐标为矩形的中线就可以了;水平居中this.ctx.setTextAlign('center');;

多个元素间的层级问题

由于canvas没有Api可以设置绘制元素的层级,只能是根据后绘制层级高于前面绘制的方式,所以需要用户传入zIndex字段,利用数组排序(Array.prototype.sort)后再根据顺序绘制。

图片尺寸和渲染尺寸不一致问题

绘制图片我们使用ctx.drawImage()API;

如果使用drawImage(dx, dy, dWidth, dHeight),图片会压缩尺寸以适应绘制的尺寸,图片会变形,如下图:

在基础库1.9.0起支持drawImage(sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight),sx和sy是源图像的矩形选择框左上角的坐标,sWidth和sHeight是源图像的矩形选择框的宽度和高度,如下图:

如果绘制尺寸比源图尺寸宽,那么绘制尺寸的宽度就等于源图宽度;反之,绘制尺寸比源图尺寸高,那么绘制尺寸的高度等于源图高度;

我们可以通过wx.getImageInfoApi获取源图的尺寸;

canvas转图片

在canvas绘制完成后调用wx.canvasToTempFilePathApi将canvas转为图片输出,这样需要注意,wx.canvasToTempFilePath需要写在this.ctx.draw的回调中,并且在组件中使用这个接口需要在第二个入参传入this(),如下

this.ctx.draw(false, () => {
  wx.canvasToTempFilePath({
      canvasId: 'canvasid',
      success: (res) => {
          wx.hideLoading();
          this.triggerEvent('success', res.tempFilePath);
      },
      fail: (err) => {
          wx.hideLoading();
          this.triggerEvent('fail', err);
      }
  }, this);
});

IOS 6.6.7 clip问题

在IOS 6.6.7版本中clip方法连续裁剪图片时,只有第一张有效,这是微信的bug,官方也证实了(developers.weixin.qq.com/community/d…

关于获取canvas实例

我们可以使用wx.createCanvasContext获取小程序实例,但在组件中使用切记第二个参数需要带上this,如下

this.ctx = wx.createCanvasContext('canvasid', this);

如何使用组件

github.com/jasondu/wxa…

作者:jasondu41833
来源:https://juejin.cn/post/6844903663840788493

收起阅读 »

利用好 git bisect 这把利器,帮助你快速定位疑难 bug

Git
使用git bisect二分法定位问题的基本步骤:git bisect start [最近的出错的commitid] [较远的正确的commitid]测试相应的功能git bisect good 标记正确直到出现问题则 标记错误 git bisect bad提...
继续阅读 »

使用git bisect二分法定位问题的基本步骤:

  1. git bisect start [最近的出错的commitid] [较远的正确的commitid]

  2. 测试相应的功能

  3. git bisect good 标记正确

  4. 直到出现问题则 标记错误 git bisect bad

  5. 提示的commitid就是导致问题的那次提交

问题描述

我们以Vue DevUI组件库的一个bug举例子🌰

5d14c34b这一次commit,执行yarn build报错,报错信息如下:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

我可以确定的是上一次发版本(d577ce4)是可以build成功的。

git bisect 简介

git bisect命令使用二分搜索算法来查找提交历史中的哪一次提交引入了错误。它几乎能让你闭着眼睛快速定位任何源码导致的问题,非常实用。

你只需要告诉这个命令一个包含该bug的坏commit ID和一个引入该bug之前的好commit ID,这个命令会用二分法在这两个提交之间选择一个中间的commit ID,切换到那个commit ID的代码,然后询问你这是好的commit ID还是坏的commit ID,你告诉它是好还是坏,然后它会不断缩小范围,直到找到那次引入bug的凶手commit ID

这样我们就只需要分析那一次提交的代码,就能快速定位和解决这个bug(具体定位的时间取决于该次提交的代码量和你的经验),所以我们提交代码时一定要养成小批量提交的习惯,每次只提交一个小的独立功能,这样出问题了,定位起来会非常快。

接下来我就以Vue DevUI之前出现过的一个bug为例,详细介绍下如何使用git bisect这把利器。

定位过程

git bisect start 5d14c34b d577ce4
or
git bisect start HEAD d577ce4

其中5d14c34b这次是最近出现的有bug的提交,d577ce4这个是上一次发版本没问题的提交。

执行完启动bisect之后,马上就切到中间的一次提交啦,以下是打印结果:

kagol:vue-devui kagol$ git bisect start 5d14c34b d577ce4
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[1cfafaaa58e03850e0c9ddc4246ae40d18b03d71] fix: read-tip icon样式泄露 (#54)

可以看到已经切到以下提交:

[1cfafaaa] fix: read-tip icon样式泄露 (#54)

执行命令:

yarn build

构建成功,所以标记下good

git bisect good
kagol:vue-devui kagol$ git bisect good
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0] feat(drawer): add service model (#27)

标记万good,马上又通过二分法,切到了一次新的提交:

[c0c4cc1a] feat(drawer): add service model (#27)

再次执行build命令:

yarn build

build失败了,出现了我们最早遇到的报错:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

标记下bad,再一次切到中间的提交:

kagol:vue-devui kagol$ git bisect bad
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[86634fd8efd2b808811835e7cb7ca80bc2904795] feat: add scss preprocessor in docs && fix:(Toast) single lifeMode bug in Toast

以此类推,不断地验证、标记、验证、标记...最终会提示我们那一次提交导致了这次的bug,提交者、提交时间、提交message等信息。

kagol:vue-devui kagol$ git bisect good
c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0 is the first bad commit
commit c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0
Author: nif <lnzhangsong@163.com>
Date: Sun Dec 26 21:37:05 2021 +0800

feat(drawer): add service model (#27)

* feat(drawer): add service model

* docs(drawer): add service model demo

* fix(drawer): remove 'console.log()'

packages/devui-vue/devui/drawer/index.ts | 7 +++--
.../devui-vue/devui/drawer/src/drawer-service.ts | 33 ++++++++++++++++++++++
packages/devui-vue/devui/drawer/src/drawer.tsx | 3 ++
packages/devui-vue/docs/components/drawer/index.md | 29 +++++++++++++++++++
4 files changed, 69 insertions(+), 3 deletions(-)
create mode 100644 packages/devui-vue/devui/drawer/src/drawer-service.ts

最终定位到出问题的commit:

c0c4cc1a is the first bad commit

github.com/DevCloudFE/…

整个定位过程几乎是机械的操作,不需要了解项目源码,不需要了解最近谁提交了什么内容,只需要无脑地:验证、标记、验证、标记,最后git会告诉我们那一次提交出错。

这么香的工具,赶紧来试试吧!

问题分析

直到哪个commit出问题了,定位起来范围就小了很多。

如果平时提交代码又能很好地遵循小颗粒提交的话,bug呼之欲出。

这里必须表扬下我们DevUI的田主(Contributor)们,他们都养成了小颗粒提交的习惯,这次导致bug的提交c0c4cc1a,只提交了4个文件,涉及70多行代码。

我们在其中搜索下document关键字,发现了两处,都在drawer-service.ts整个文件中:

一处是12行的:

static $body: HTMLElement | null = document.body

另一处是17行的:

this.$div = document.createElement('div')

最终发现罪魁祸首就是12行的代码!

破案!

作者:DevUI团队
来源:https://juejin.cn/post/7046409685561245733

收起阅读 »

前端开发的积木理论——像搭积木一样做前端开发

1 概述用户界面是由一系列组件组合而成,组件将数据和交互封装在内,仅保留必要的接口与其他组件进行通信。在前端开发中,组件就像一个一个的小积木块,我们用这些积木块拼出一个一个页面,这些页面组成了一个完整的为用户提供价值的业务。相信大部分前端工程师都使用过组件库,...
继续阅读 »



1 概述

用户界面是由一系列组件组合而成,组件将数据和交互封装在内,仅保留必要的接口与其他组件进行通信。

在前端开发中,组件就像一个一个的小积木块,我们用这些积木块拼出一个一个页面,这些页面组成了一个完整的为用户提供价值的业务。

相信大部分前端工程师都使用过组件库,比如Ant DesignElementUI,都是我们非常熟悉的组件库,以及我们团队做的DevUI组件库。

组件库就像一个工具箱,里面包含了各式各样奇形怪状、功能各异的组件,我们直接拿这些组件小积木来拼页面,非常方便。

2 界面的基本元素

从抽象的角度来看,任何用户界面都是由组件数据交互组成的。

2.1 组件

组件是一个具有一定功能的独立单元,不同的组件可以组合在一起,构成功能更强大的组件.

组件是网页的器官。

组件的概念要和HTML标签的概念区分开来,HTML标签是网页的基本单元,而组件是基于HTML标签的、包含特定功能的独立单元,可以简单理解为组件是HTML标签的一个超集。

组件内部封装的是数据和交互,对外暴露必要的接口,以与其他组件进行通信。

2.2 数据

用户界面中包含很多数据,有不变的静态数据,也有随时间和交互改变的动态数据,它们大多来自于后台数据库。

组件内部包含数据,组件之间传递的也是数据。

数据是网页的核心。

在前端开发中,数据主要以JSON格式进行存储和传递。

2.3 交互

交互是用户的操作,主要通过鼠标和键盘等计算机外设触发,点击一次按钮、在文本框中输入一些字符、按下回车键等都是交互。

在网页中,所有的交互都是通过事件的方式进行响应的。

交互是网页的灵魂,不能进行交互的网页就像干涸的河流,了无生气。

3 组件的特性

一个设计良好的组件应该包含以下特性:

  • 复用性(Reuseability)

  • 组合性(Composability)

  • 扩展性(Scalability)

3.1 复用性

组件作为一个独立的单元,除了自身的特定功能之外,不应该包含任何业务相关的数据。

组件应该能够复用到任何业务中,只要该业务需要用到组件所包含的功能。

组件应该是资源独立的,以增强组件的复用能力。

3.2 组合性

组件与组件之间可以像积木一样进行组合,组合之后的组件拥有子组件的所有功能,变得更强大。

组合的方式可以是包裹别的组件,也可以是作为参数传入别的组件中。

3.3 扩展性

可以基于现有的组件进行扩展,开发功能更加定制化的组件,以满足业务需求。

组件的可扩展能力依赖于接口的设计,接口要尽可能的灵活,以应对不断变化的需求。

4 组件间通信

4.1 从外向内通信

通过props将数据传递到组件内部,以供组件自身或其子组件使用。

props是不可变的:

  • 这意味着我们无法在组件内部修改props的原始值

  • 也意味着只有外部传入了props,才能在组件内部获取和使用它

4.2 从内向外通信

可以通过两种方式将组件内部的数据传递到组件外:

  • 通过ref属性(组件实例的引用),通过组件的ref属性,可以获取到组件实例的引用,进而获取组件内部的数据和方法

  • 通过事件回调(比如:click),通过事件回调的方式,可以通过props将一个回调函数传递到组件内部,并将组件内部的数据通过回调传递到外部

4.3 双向通信

可以通过全局context的方式进行双向通信。

父组件声明context对象,那么其下所有的子组件,不管嵌套多深,都可以使用该对象的数据,并且可以通过回调函数的方式将子组件的数据传递出来供父组件使用。

4.4 多端通信

通过事件订阅的方式可以实现多个组件之间互相通信。

通过自定义事件(Custom Event),任何组件都可以与其他组件进行通讯,采用的是发布/订阅模式,通过向事件对象上添加监听和触发事件来实现组件间通信。

5 案例

接下来通过广告详情页面的开发来演示如何用积木理论来构建网页。

设计图如下:

5.1 第一步:拆积木

将设计图拆分成有层次的积木结构。

5.1.1 顶层积木

最顶层拆分成四个大积木:

  • Header(头部组件)

  • ChartBlock(图表块组件)

  • DetailBlock(详情块组件)

  • TableBlock(表格块组件)

5.1.2 中间层积木

  • 每个大积木又可以拆分成若干小积木

  • 中间层有些是不可拆分的原子积木,比如:ButtonCheckbox

  • 有些是由原子积木组合而成的复合积木,比如:DateRangePickerTable

层次结构如下:

  • Header

    • Breadcrumb

    • Button

    • DateRangePicker

  • ChartBlock

    • Tabs

    • LineChart

    • BarChart

  • DetailBlock

    • Button

    • List

  • TableBlock

    • Checkbox

    • Select

    • Button

    • Table

5.1.3 底层积木

最底层的积木都是不可拆分的原子积木。

5.2 第二步:造积木

已经将积木的层次结构设计出来,接下来就要考虑每个层次的积木怎么制造的问题。

这一块后面会专门写一个系列文章给大家分享如何自己制造组件。

5.3 第三步:搭积木

5.3.1 顶层积木

对应的代码:

<div class="ad-detail-container">
   <Header />
   <div class="content">
       <div class="chart-detail-block">
           <ChartBlock />
           <DetailBlock />
       </div>
       <TableBlock />
   </div>
</div>

其中的<div>标签是为了布局方便加入的。

5.3.2 中间层积木

Header对应的代码:

<div class="header">
 <div class="breadcrumb-area">
   <div class="breadcrumb-current">gary audio</div>
     <div class="breadcrumb-from">
      From:
       <d-breadcrumb class="breadcrumb" separator="">
         <d-breadcrumb-item href="http://www.qq.com">Campaign List</d-breadcrumb-item>
         <span class="breadcrumb-seprator">> </span>
         <d-breadcrumb-item href="http://www.qq.com">gary audio test</d-breadcrumb-item>
       </d-breadcrumb>
     </div>
   </div>
 </div>
 <div class="operation-area">
   <d-button icon="mail" class="flat" (click)="sendReportEmail()">Send Report Email</d-button>
   <d-date-range-picker (change)="changeDate()" />
 </div>
</div>

需要注意的是:为了方便阐述积木理论的核心思想,这里的原子组件大多都是已经造好的(使用DevUI组件库),也可以选择自己制造,后面会专门写一个系列文章给大家分享如何自己制造组件。

ChartBlock对应的代码:

<div class="chart-block">
   <d-tabs [defaultActiveKey]="1">
       <d-tab-item tab="Ad performance" [key]="1">
           <d-line-chart></d-line-chart>
       </d-tab-item>
       <d-tab-item tab="Audience" [key]="2">
           <d-bar-chart></d-bar-chart>
       </d-tab-item>
   </d-tabs>
</div>

DetailBlock对应的代码:

<div class="detail-block">
   <div class="detail-header">
       <div class="detail-title">Ad detail</div>
       <div class="detail-operation">
           <d-button icon="edit" class="flat" (click)="edit()">Edit</d-button>
           <d-button icon="delete" class="flat" (click)="delete()">Delete</d-button>
           <d-button icon="eye" class="flat" (click)="preview">Preview</d-button>
       </div>
   </div>
   <d-list [data]="adDetail" [config]="detailConfig"></d-list>
</div>

TableBlock对应的代码:

<div class="table-block">
   <div class="table-operation-bar">
       <d-checkbox (change)="changeDelivery()">Has delivery</Checkbox>
       <d-select class="select-table-column" [defaultValue]="1"
           (change)="select()">
           <d-select-option value="1">Performance</d-select-option>
           <d-select-option value="2">Customize</d-select-option>
       </Select>
       <d-button icon="export" (click)="exportData()">Export Data</d-button>
   </div>
   <d-table [dataSource]="adsList" [columns]="columns"></d-table>
</div>

由于篇幅原因,这个案例并没有包含交互的部分,不过基本能够阐述清楚积木理论的核心思想。

作者:DevUI团队
来源:https://juejin.cn/post/7047503485054484516

收起阅读 »

深入理解 redux 数据流和异步过程管理

前端框架的数据流前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。一般来说,除了某部分状态数据...
继续阅读 »


前端框架的数据流

前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。

数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。

一般来说,除了某部分状态数据是只有某个组件关心的,我们会把状态数据放在组件内以外,业务数据、多个组件关心的状态数据都会放在 store 里面。组件从 store 中取数据,当交互的时候去通知 store 改变对应的数据。

这个 store 不一定是 redux、mobox 这些第三方库,其实 react 内置的 context 也可以作为 store。但是 context 做为 store 有一个问题,任何组件都能从 context 中取出数据来修改,那么当排查问题的时候就特别困难,因为并不知道是哪个组件把数据改坏的,也就是数据流不清晰。

正是因为这个原因,我们几乎见不到用 context 作为 store,基本都是搭配一个 redux。

所以为什么 redux 好呢?第一个原因就是数据流清晰,改变数据有统一的入口。

组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。

这样数据流动是单向的,清晰的,很容易管理。

这就像为什么我们在公司里想要什么权限都要走审批流,而不是直接找某人,一样的道理。集中管理流程比较清晰,而且还可以追溯。

异步过程的管理

很多情况下改变 store 数据都是一个异步的过程,比如等待网络请求返回数据、定时改变数据、等待某个事件来改变数据等,那这些异步过程的代码放在哪里呢?

组件?

放在组件里是可以,但是异步过程怎么跨组件复用?多个异步过程之间怎么做串行、并行等控制?

所以当异步过程比较多,而且异步过程与异步过程之间也不独立,有串行、并行、甚至更复杂的关系的时候,直接把异步逻辑放组件内不行。

不放组件内,那放哪呢?

redux 提供的中间件机制是不是可以用来放这些异步过程呢?

redux 中间件

先看下什么是 redux 中间件:

redux 的流程很简单,就是 dispatch 一个 action 到 store, reducer 来处理 action。那么如果想在到达 store 之前多做一些处理呢?在哪里加?

改造 dispatch!中间件的原理就是层层包装 dispatch。

下面是 applyMiddleware 的源码,可以看到 applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。

function applyMiddleware(middlewares) {
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return { ...store, dispatch}
}

所以说中间件最终返回的函数就是处理 action 的 dispatch:

function middlewareXxx(store) {
return function (next) {
return function (action) {
// xx
};
};
};
}

中间件会包装 dispatch,而 dispatch 就是把 action 传给 store 的,所以中间件自然可以拿到 action、拿到 store,还有被包装的 dispatch,也就是 next。

比如 redux-thunk 中间件的实现:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();

它判断了如果 action 是一个函数,就执行该函数,并且把 store.dispath 和 store.getState 传进去,否则传给内层的 dispatch。

通过 redux-thunk 中间件,我们可以把异步过程通过函数的形式放在 dispatch 的参数里:

const login = (userName) => (dispatch) => {
dispatch({ type: 'loginStart' })
request.post('/api/login', { data: userName }, () => {
dispatch({ type: 'loginSuccess', payload: userName })
})
}
store.dispatch(login('guang'))

但是这样解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的问题了么?

没有,这段逻辑依然是在组件里写,只不过移到了 dispatch 里,也没有提供多个异步过程的管理机制。

解决这个问题,需要用 redux-saga 或 redux-observable 中间件。

redux-saga

redux-saga 并没有改变 action,它会把 action 透传给 store,只是多加了一条异步过程的处理。

redux-saga 中间件是这样启用的:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducer'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)

要调用 run 把 saga 的 watcher saga 跑起来:

watcher saga 里面监听了一些 action,然后调用 worker saga 来处理:

import { all, takeLatest } from 'redux-saga/effects'

function* rootSaga() {
yield all([
takeLatest('login', login),
takeLatest('logout', logout)
])
}
export default rootSaga

redux-saga 会先把 action 透传给 store,然后判断下该 action 是否是被 taker 监听的:

function sagaMiddleware({ getState, dispatch }) {
return function (next) {
return function (action) {
const result = next(action);// 把 action 透传给 store

channel.put(action); //触发 saga 的 action 监听流程

return result;
}
}
}

当发现该 action 是被监听的,那么就执行相应的 taker,调用 worker saga 来处理:

function* login(action) {
try {
const loginInfo = yield call(loginService, action.account)
yield put({ type: 'loginSuccess', loginInfo })
} catch (error) {
yield put({ type: 'loginError', error })
}
}

function* logout() {
yield put({ type: 'logoutSuccess'})
}

比如 login 和 logout 会有不同的 worker saga。

login 会请求 login 接口,然后触发 loginSuccess 或者 loginError 的 action。

logout 会触发 logoutSuccess 的 action。

redux saga 的异步过程管理就是这样的:先把 action 透传给 store,然后判断 action 是否是被 taker 监听的,如果是,则调用对应的 worker saga 进行处理。

redux saga 在 redux 的 action 流程之外,加了一条监听 action 的异步处理的流程。

其实整个流程还是比较容易理解的。理解成本高一点的就是 generator 的写法了:

比如下面这段代码:

function* xxxSaga() {
while(true) {
yield take('xxx_action');
//...
}
}

它就是对每一个监听到的 xxx_action 做同样的处理的意思,相当于 takeEvery:

function* xxxSaga() {
yield takeEvery('xxx_action');
//...
}

但是因为有一个 while(true),很多同学就不理解了,这不是死循环了么?

不是的。generator 执行后返回的是一个 iterator,需要另外一个程序调用 next 方法才会继续执行。所以怎么执行、是否继续执行都是由另一个程序控制的。

在 redux-saga 里面,控制 worker saga 执行的程序叫做 task。worker saga 只是告诉了 task 应该做什么处理,通过 call、fork、put 这些命令(这些命令叫做 effect)。

然后 task 会调用不同的实现函数来执行该 worker saga。

为什么要这样设计呢?直接执行不就行了,为啥要拆成 worker saga 和 task 两部分,这样理解成本不就高了么?

确实,设计成 generator 的形式会增加理解成本,但是换来的是可测试性。因为各种副作用,比如网络请求、dispatch action 到 store 等等,都变成了 call、put 等 effect,由 task 部分控制执行。那么具体怎么执行的就可以随意的切换了,这样测试的时候只需要模拟传入对应的数据,就可以测试 worker saga 了。

redux saga 设计成 generator 的形式是一种学习成本和可测试性的权衡。

还记得 redux-thunk 有啥问题么?多个异步过程之间的并行、串行的复杂关系没法处理。那 redux-saga 是怎么解决的呢?

redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系:

比如 takeEvery 会对多个 action 的每一个做同样的处理,takeLatest 会对多个 action 的最后一个做处理,race 会只返回最快的那个异步过程的结果,等等。

这些控制多个异步过程之间关系的 effect 正是 redux-thunk 所没有的,也是复杂异步过程的管理必不可少的部分。

所以 redux-saga 可以做复杂异步过程的管理,而且具有很好的可测试性。

其实异步过程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 实现的,它也是一种复杂异步过程管理的方案。

redux-observable

redux-observable 用起来和 redux-saga 特别像,比如启用插件的部分:

const epicMiddleware = createEpicMiddleware();

const store = createStore(
rootReducer,
applyMiddleware(epicMiddleware)
);

epicMiddleware.run(rootEpic);

和 redux saga 的启动流程是一样的,只是不叫 saga 而叫 epic。

但是对异步过程的处理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:

import { ajax } from 'rxjs/ajax';

const fetchUserEpic = (action$, state$) => action$.pipe(
ofType('FETCH_USER'),
mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
)
);

通过 ofType 来指定监听的 action,处理结束返回 action 传递给 store。

相比 redux-saga 来说,redux-observable 支持的异步过程的处理更丰富,直接对接了 operator 的生态,是开放的,而 redux-saga 则只是提供了内置的几个 effect 来处理。

所以做特别复杂的异步流程处理的时候,redux-observable 能够利用 rxjs 的操作符的优势会更明显。

但是 redux-saga 的优点还有基于 generator 的良好的可测试性,而且大多数场景下,redux-saga 提供的异步过程的处理能力就足够了,所以相对来说,redux-saga 用的更多一些。

总结

前端框架实现了数据到视图的绑定,我们只需要关心数据流就可以了。

相比 context 的混乱的数据流,redux 的 view -> action -> store -> view 的单向数据流更清晰且容易管理。

前端代码中有很多异步过程,这些异步过程之间可能有串行、并行甚至更复杂的关系,放在组件里并不好管理,可以放在 redux 的中间件里。

redux 的中间件就是对 dispatch 的层层包装,比如 redux-thunk 就是判断了下 action 是 function 就执行下,否则就是继续 dispatch。

redux-thunk 并没有提供多个异步过程管理的机制,复杂异步过程的管理还是得用 redux-saga 或者 redux-observable。

redux-saga 透传了 action 到 store,并且监听 action 执行相应的异步过程。异步过程的描述使用 generator 的形式,好处是可测试性。比如通过 take、takeEvery、takeLatest 来监听 action,然后执行 worker saga。worker saga 可以用 put、call、fork 等 effect 来描述不同的副作用,由 task 负责执行。

redux-observable 同样监听了 action 执行相应的异步过程,但是是基于 rxjs 的 operator,相比 saga 来说,异步过程的管理功能更强大。

不管是 redux-saga 通过 generator 来组织异步过程,通过内置 effect 来处理多个异步过程之间的关系,还是 redux-observable 通过 rxjs 的 operator 来组织异步过程和多个异步过程之间的关系。它们都解决了复杂异步过程的处理的问题,可以根据场景的复杂度灵活选用。

原文:https://juejin.cn/post/7011835078594527263


收起阅读 »

重构B端 ? 表单篇

随着业务的庞大。B端的业务越来越重,导致后面的需求越来越难满足,人在工位坐,锅从天上来,一个小前端就地开启了重构之旅 1. 梳理待重构的B端 上面是待重构 B端 的结构图,由 PHP 编写,利用约定的字段上传 JSON 文件,让 Controller 读取文...
继续阅读 »

随着业务的庞大。B端的业务越来越重,导致后面的需求越来越难满足,人在工位坐,锅从天上来,一个小前端就地开启了重构之旅


1. 梳理待重构的B端



上面是待重构 B端 的结构图,由 PHP 编写,利用约定的字段上传 JSON 文件,让 Controller 读取文件配置 在 DB 生成一个表,再由 Controller 直出到 View 层;在应对那些比较简单的逻辑或者功能性单一的业务时可以起到非常大的作用。但是需求只会越来越多。越来越复杂,尤其在处理复杂业务的时,整个 Controller 作为数据中枢,如果夹杂了太多的冗余逻辑,渐渐的就跟蜘蛛网一样难以维护。


2. 设计重构方案


了解完整个大概的数据走向以及业务背景之后,接下来就是设计整个重构方案了。



  • 继承之前的业务逻辑、通过 JSON 文件 渲染整个页面

  • 整体 UI 升级 ,因为用 React 重构,UI 这里选择 Antd

  • 前后端分离,本地开发先 Mock ,后期用 node 去代理或者用其他更灵活的方式交互数据


3. 配置文件改造



脚手架搭建,这里用 Antd Pro 脚手架 ( umi.js + antd ) , Router 配置 以及 React-Redux 等工具 umi 都帮我们内置了 ,基础搭建就不展开描述了。



{
"字段名称": {
"name_cfg": {
"label": "label",
"type": "select",
"tips": "tips",
"required": "required",
"max_length": 0,
"val_arr": {
"-1": "请选择",
"1": "字段 A",
"2": "字段 B",
"default": "-1"
},
"tabhide": {
"1": "A 字段, B 字段",
"2": "B 字段, C 字段",
"3": "C 字段, D 字段"
}
"relation_select": {
"url": "其他业务的接口请求",
"value": "Option id",
"name": "Option name",
"relation_field": "relation_field1, relation_field2"
}
},
"sql_cfg": {
"type": "int(11)",
"length": 11,
"primary": 0,
"default": -1,
"auto_increment": 0
}
},
...
}

这个 JSON 文件共有几十个字段,这里拿了一个比较有代表性的字段。这是一个 Select 类型的表单,val_arr 是这个 Select 的值,relation_select 请求其他业务的接口填进去这个 val_arr 供给给 Select , tabhide 表示的是,当值为 Key 时 隐藏表单的 Value 字段,多字段时以逗号分割。


此时需要一个过度文件将这个 JSON 文件处理成咋们方便处理的格式


// transfrom.js 
let Arr = [];
let Data = json.field_cfg;
for (let i in Data) {
let obj = {};
let val_Array = [];
let tab_hide = Data[i]['name_cfg']['tabhide']
for (let index in Data[i]['name_cfg']['val_arr']) {
let obj = {};
if (index !== 'default') {
obj['value'] = index;
obj['label'] = Data[i]['name_cfg']['val_arr'][index];
if(tab_hide && tab_hide[index]){
obj['tab_hide'] = tab_hide[index].split(',');
}
val_Array.push(obj);
} else {
val_Array.map((item) => {
if (item.value === Data[i]['name_cfg']['val_arr'][index]) {
item['default'] = true;
}
});
}
}
obj['id'] = i;
obj['name'] = Data[i]['name_cfg']['label'];
obj['type'] = Data[i]['name_cfg']['type'];
obj['required'] = Data[i]['name_cfg']['required'];
obj['tips'] = Data[i]['name_cfg']['tips'];
obj['multiple'] = Data[i]['name_cfg']['multiple'];
obj['val_arr'] = val_Array;
obj['sql_cfg_type'] = Data[i]['sql_cfg']['type'];
obj['sql_default'] = Data[i]['sql_cfg']['default'];
Arr.push(obj);
}

// config.js
const config = [
{
id: '字段名称',
name: 'label -> Name',
type: 'select',
required: 'required',
tips: 'tips',
val_arr: [
{ value: '-1', label: '请选择', default: true }
{ value: '1', label: 'Option A', tab_hide: ['字段A', '字段B'] }
{ value: '2', label: 'Option B', tab_hide: ['字段B', '字段C'] }
],
sql_cfg_type: 'int(11)',
sql_default: -1,
},
...
]

4. 表单渲染


Antd 的表单组件的功能非常丰富。拿到 Config 按照文档一把唆就完事了。

React 的 函数组件 和 Hooks 让代码更加简洁了,可太棒了!


  const renderForm: () => (JSX.Element | undefined)[] = () => {
// renderSelect(); renderText(); renderNumber(); renderDatePicker();
}
const renderSelect: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderText: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderNumber: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderDatePicker: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

5. 默认字段 & 隐藏字段


componentDidMount 的时候处理这些字段,用 Hooks 可以这么表达 React.useEffect(()=>{...},[])

默认字段 : 给 Form 表单 设置上配置表的 sql_default 即可。

隐藏字段 : 这里涉及到字段会重叠,之前用 JQuery 单纯操作 DOM 节点去 show() 和 hide()。在操作 Dom 这方面,JQ 确实是有他的优势。

现在的解决方案如下图所示:


  const [hideListMap, setHideListMap] = useState<any>(new Map());
configObj.forEach((item: { val_arr: { tab_hide: any; value: any; }[]; sql_default: any; id: any; }) => {
item.val_arr.forEach((val_arr_item: { tab_hide: any; value: any; }) => {
if (val_arr_item.tab_hide && val_arr_item.value === item.sql_default) {
hideListMap.set(item.id, val_arr_item.tab_hide);
}
});
});
setHideListMap(hideListMap)

new Map() 用字段名作为 key ,value 的值为 tab_hide 数组, 之前用 Array 创建一个动态的 key 、value ,发现操作起来没有 Map 好使。


  const [hideList, setHideList] = useState<string[]>();

React.useEffect(() => {
let arr: string[] = []
hideListMap.forEach((item: any, key: any) => {
arr.push(...item);
});
setHideList(arr);
}, [hideListMap])

const selctChange = React.useCallback((value: SelectValue, configItem: ConfigListData) => {
hideListMap.forEach((item: any, key: string) => {
if (key === configItem.id) {
configItem.val_arr.forEach((val_arr_item: { value: SelectValue; tab_hide: any; }) => {
if (val_arr_item.value === value) {
hideListMap.set(configItem.id, val_arr_item.tab_hide);
setHideListMap(new Map(hideListMap))
}
})
}
});
}, [hideListMap]);

React.useEffect 不仅可以当 componentDidMount ,还可以当 componentDidUpdate 使用,只需要在第二个参数加上监听的值 React.useEffect(()=>{...},[n]), 这里监听了 hideListMap 如果 Select 框触发了 改变了 hideListMap 会自动帮我个更新 hideList,只要拿这个 hideList 作为条件判断是否渲染。就满足了多字段隐藏的需求了。


  const renderForm: () => (JSX.Element | undefined)[] = () => {
return configObj.map((item: ConfigListData) => {
if (hideList && hideList.includes(item.id)) {
return undefined
}
if (item.type === 'text') {
if (item.sql_cfg_type.indexOf('int') !== -1) {
return renderNumber(item)
} else {
return renderText(item)
}
}
if (item.type === 'select') {
return renderSelect(item)
}
if (item.type === 'datetime') {
return renderDatePicker(item);
}
return undefined;
})
}

6. Select 动态渲染值


// transfrom.js
let service = [];
let Data = json.field_cfg;
for (let i in Data) {
let relation_select = Data[i]['name_cfg']['relation_select'];
if (relation_select && relation_select['relation_field']) {
relation_select['relation_field'] = relation_select['relation_field'].split(',');
service.push(relation_select);
}
}

// config.ts
const SERVICE = [
{
url: "其他业务的接口请求",
value: "Option id",
name: "Option name",
relation_field: ["relation_field1", "relation_field2" ],
method: 'get'
}
...
]
// utils.ts
import request from 'umi-request'; // 请求库
export const getSelectApi = async (url: string, method: string) => {
return request(url, {
method,
})
}

之前接口请求与 Config 是耦合在一起的,Config 的字段如果增加到一定的数量时就会变得难以维护。最后决定把 表单Config 和 Service 层解耦,目的是为了更为直观的区别 Config 和 Service,方便以后维护。



数据接口参数格式以及字段都要有所约束。这里需要起一个 node 层,主要是处理第三方接口跨域处理,以及参数统一等。



import { CONFIG_OBJ, SERVICE } from './config';
const Form: React.FC = () => {
const [configObj, setConfigObj] = React.useState(CONFIG_OBJ);
React.useEffect(() => {
(async () => {
for (let item of SERVICE) {
for (let fieldItem of item.relation_field) { // 多字段支持
insertObj[fieldItem] = await getSelectApi(item.url, item.method)
}
}
for (let key in insertObj) {
if (key === item.id) {
item.val_arr.push(...insertObj[key].list)
}
}
setConfigObj([...configObj]);
})()
})
...
}
export default Form

将解耦出来的接口配置融合在 form 表单。useState 检测到数据指向变动就会重新渲染。

到这里 Form 表单组件基本搭建完成了。


6. 最后


再进一步思考,这里还有些优化的点,我们可以把这些配置文件也融进去这个B端里,将 Config 和 Service 都写进 DB,脱离用文件上传这种方式。


B端产品的服务群体是企业内人员。B端的用户体验也一样重要,一个优秀的B端也是提高效率、节省成本的途径之一。



本文到此结束,希望对各位看官有帮助 (‾◡◝)


作者:Coffee_C
链接:https://juejin.cn/post/6922286595290693646

收起阅读 »

聊下与 hash 有关的加密

聊下与 hash 有关的加密hash 的简单概述奥.自己百度去吧 百度比我说的详细.咱们在这介绍一下 hash 的特点注意看啊 重点来了1.算法是公开的(怎么算的不重要,腻友不是数学家)2.对相同数据运算,得到的结果是一样的3.对不同数据运算,如MD5得到的结...
继续阅读 »

聊下与 hash 有关的加密

hash 的简单概述

奥.自己百度去吧 百度比我说的详细.

咱们在这介绍一下 hash 的特点

注意看啊 重点来了

1.算法是公开的(怎么算的不重要,腻友不是数学家)

2.对相同数据运算,得到的结果是一样的

3.对不同数据运算,如MD5得到的结果默认是128位,32个字符(16进制标识)

4.逆运算的可能约等于0

5.可称之为信息指纹,是用来做数据识别

hash (MD5)加密的用途

1.用户密码的加密

比如客户端用户密码设置为 123456  ,通过 MD5加密的结果是e10adc3949ba59abbe56e057f20f883e

然后把这个e10adc3949ba59abbe56e057f20f883e发送给服务器,服务器储存的就是e10adc3949ba59abbe56e057f20f883e .  这个时候就算服务器被攻击,数据库泄露,别人拿到的也是一段加密后的结果.是不是这样就安全了

觉得安全了的同学罚站半小时. 看特点 ,上述第二条, 对相同数据运算,得到的结果是一样的!

这个意思就是说.  通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB! 这些都是 MD5加密前和加密后的数据!



看吧.  一些常用或者常规通过 MD5加密已经可以通过第三方直接查询的到了

既然 MD5这么不安全,为什么这种加密还如此风靡 . 咱们接着往下看

这个时候有经验的同学应该知道怎么办了.有一种 md5的保护.叫加盐

什么是加盐呢 .举个栗子   用户的密码是123456吧. 这在程序里是个字符串吧.字符串拼接大家都会吧 比如用户密码后边都拼接一个 handsomezuyu. 哎? 试试看

123456handsomezuyu  md5 后的结果是9a9a83b884f51deb88944461075dc538

咱们在用这个结果去解密试试



这个handsomezuyu就是盐了. 当然盐也可以是coolzuyu  也可以是excellentzuyu 也可以是 goodzuyu. 都可以都可以. 

查不到了吧. 厉害了吧. 可以了吧.这招还行吧. 既简单又实用.都学会了吧.

(注意一下这个方法的缺点.参与这个项目的人都知道这个盐的值吧.那么如果有一天某位参与这个项目开发的同事因为在聚餐的时候放了个屁被开除了.额.... 是吧.那他如果想搞事... 好了.当然也有避免这种情况发生的措施,咱们这篇闲聊中先不讲.)


2.搜索引擎

嗯?这个时候可能有些小可爱就要问了.搜索引擎?哈希?

咱们就简单说一下搜索引擎与哈希的关系

比如你百度搜 "帅逼 zuyu" 和" zuyu 帅逼" 这两个关键词

出现的结果如果没有"帅逼 zuyu"直接关联的内容.那么搜索过后呈现的内容是一样的

因为搜索时引擎会拆词 ,关键字会被拆成" 帅逼 "和"zuyu"两个词 .然后得到两个词的哈希值.然后在对位相加.   无论顺序是怎么样的.对位相加后的结果都一样吧!所以就会出现了我上边说的内容一样的现象. 当然这只是一小部分关系,里边还有很多更复杂的算法和逻辑.这儿咱们就不一一标出了(呵呵,我也不知道)

3.版权/云盘

唉?版权?有点懵?版权还能和hash有关系?

咱们就先说说版权.比如腾讯视频上. 你拍了一段视频然后上传  ,紧接着上传成功 .这个时候腾讯视频就记录下了这个视频的 hash值.并且认定这个版权是你的. 其他人在上传相同的视频之后就会提示该视频已存在等类似的提示信息(如果你通过下载或者修改格式等各方便因素影响.即使视频内容完全一样, hash 值也有可能不同)

那云盘呢.云盘和 hash 有啥 关系呢.

举个栗子奥.百度云大家应该都用过对吧,而且你不仅用过,你用来干嘛我也知道.嗯,看懂这句话的都是同道中人. 好.咱们说回原题. 你们上传一些学习资料的时候有没有遇到过一种现象呢?

不是和谐啊! 不是和谐! 不是和谐!!!

秒传! 对吧  .一个挺大的文件  就秒传上去了.这就是和 hash的关系了.   百度云盘在你上传某国学习资料的时候,会先去拿到你这个文件对应的 hash 值.  如果云盘服务器就会做一个 hash 值的对比 .如果 hash 只能匹配的到,那就是服务器有相同的数据.  然后服务器只不过就是在你的账号里加了一条数据而已   .并不是真的把这个学习资料传上去.    当你下载一些学习资料的时候或者上传一些学习资料的时候  . 也遇到过提示你这个学习资料因为某种原因不可以下载了吧.  是吧.也是这个道理.

所以阿.这个时候知道原理了.那是不是就是可以避免一些不开心的事情了(压缩会改变 hash 值.修改文件名字不会改变 hash 值.翻录或者剪辑视频会改变 hash 值) 好 ,刹车.

来,回想一下, 我上边说特点. 第五条  可称之为信息指纹,是用来做数据识别. 理解了吧

至于特点的第一条. 算法是公开的 .咱们了解就好

至于特点的第三条.对不同数据运算,如MD5得到的结果默认是128位,32个字符(16进制标识)到更像是一种规则

至于特点的第四条.逆运算的可能约等于0 .简单解释一下就是说目前人们所熟知的知识范畴内 hash 就是不可逆运算



收起阅读 »

Taro的http封装

 当我们使用Taro的时候,经常会用到http请求,那么又怎么封装呢?serve.ts import { request, getStorageSync } from '@tarojs/taro' class Server { protecte...
继续阅读 »

当我们使用Taro的时候,经常会用到http请求,那么又怎么封装呢?

serve.ts


import { request, getStorageSync } from '@tarojs/taro'

class Server {
protected ajax({
url,
data,
method = 'GET',
...restParams
}: Taro.RequestParams
) {
// 用户token
const Authorization: string = getStorageSync('token') || ''
// 判断请求类型
let contentType: string
// GET请求
if (method === 'GET') {
contentType = 'application/json'
// POST 请求
} else if (method === 'POST') {
contentType = 'application/x-www-form-urlencoded'
}
return new Promise<Taro.request.SuccessCallbackResult>(
(resolve, reject) => {
request({
url,
data,
method,
header: {
'content-type': contentType,
Authorization,
},
...restParams,
// 成功回调
success(res: Taro.request.SuccessCallbackResult): void {
resolve(res)
},
// 失败回调
fail(err: Taro.General.CallbackResult): void {
reject(err)
},
})
}
)
}
}

export default Server

引用时


import Server from './serve'

let BASEURL: string
if (process.env.TARO_ENV === 'h5') {
BASEURL = '/api'
} else {
BASEURL = 'http://localhost:8000/api'
}

type Result = Taro.request.SuccessCallbackResult<any> | undefined

interface Err {
code: number
message: string
}

interface APILayout<T> {
status: number,
code: number
data: T
}
interface APIMessage<T> {
code: number
message: T
}

// 异常处理
function errMessage(error: Error | any, result: Result): Err | null {
console.log(error);

// H5
if (H5 && !error) {
return null
} else if (result?.statusCode === 200) {
return null
}
const code = error?.status || result?.statusCode
if (code === 401) {
// 清空token
}
const errInfo = {
401: '请先登录',
404: '服务器未响应',
500: '服务器繁忙',
}
return {
code,
message: errInfo[code] ? errInfo[code] : error.info,
}
}
export async function testAPI() {
let [error, result] = await to(
this.ajax({
url: BASEURL + '/test.php'
})
)
const err = errMessage(error, result)
const res: APILayout<Ip> = result?.data

return { err, res }
}

ok 直接用就行了













收起阅读 »

Kotlin invoke约定,让Kotlin代码更简洁

前言 最近看到DSL这个东西,不由的觉得里面可以利用Kotlin的一些特性能简化代码,所以具体来看看它是如何实现的。 正文 首先一上来就说原理或许对于不熟悉Kotlin的来说会感觉有点突兀,所以我准备从头梳理一下。 约定 Kotlin的约定我们在平时开发中肯定...
继续阅读 »

前言


最近看到DSL这个东西,不由的觉得里面可以利用Kotlin的一些特性能简化代码,所以具体来看看它是如何实现的。


正文


首先一上来就说原理或许对于不熟悉Kotlin的来说会感觉有点突兀,所以我准备从头梳理一下。


约定


Kotlin的约定我们在平时开发中肯定用到过,不过我们没有仔细去注意这个名词而已。约定的概念就是:使用与常规方法调用语法不同的、更简洁的符号,调用着有着特殊命名的函数。


这里提取2个关键点,一个是更简洁的符号调用,一个是特殊命名的函数。说白了就是让函数调用更加简洁。


比如我们最熟悉的集和调用 [index] 来 替代 get(index),我们自己也来定义个类,来实现一下这个约定:


data class TestBean(val name: String,val age: Int){
//定义非常简单 使用operator重载运算符get方法
operator fun get(index : Int): Any{
return when(index) {
0 -> name
1 -> age
else -> name
}
}

}

然后我们在使用时:


//这里就可以使用 [] 来替换 get来简化调用方法了
val testBean = TestBean("zyh",20)
testBean.get(0)
testBean[0]

invoke约定


和上面的get约定一样,[] 就是调用 get 方法的更简洁的方式,这里有个invoke约定,它的作用就是让对象像函数一样调用方法,下面直接来个例子:



data class TestBean(val name: String,val age: Int){

//重载定义invoke方法
operator fun invoke() : String{
return "$name - $age"
}

}

定义完上面代码后,我们来进行使用:


val testBean = TestBean("zyh",20)
//正常调用
testBean.invoke()
//约定后的简化调用
testBean()

这里会发现testBean对象可以调用invoke方法是正常调用,但是也可以testBean()直接来调用invoke方法,这就是invoke约定的作用,让调用invoke方法更简单。


invoke约定和函数式类型


既然了解了invoke约定,我们来和lambda结合起来。


对于lambda有点疑惑的可以查看文章:


# Kotlin lambda,有你想了解的一切


我们知道函数类型其实就是实现了FunctionN接口的类,然后当函数类型是函数类型时,这时传递给它一个lambda,lambda就会被编译成FunctionN的匿名内部类(当然是非内联的),然后调用lambda就变成了一次FunctionN接口的invoke调用。


还是看个例子代码:


//定义代码
class TestInvoke {
//高阶函数类型变量
private var mSingleListener: ((Int) -> Unit)? = null
//设置变量
public fun setSingleListener(listener:((Int) -> Unit)?){
this.mSingleListener = listener
}
//
fun testRun() {
//调用invoke函数
mSingleListener?.invoke(100)
//使用invoke约定,省去invoke
if (mSingleListener != null){
mSingleListener!!(100)
}
}

}

定义完上面回调变量后,我们来使用这个回调,由于我们知道高阶函数其实是实现了FunctionN接口的类,也就是实现了:


//注意,这里接口的方法就是invoke
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}

那我也就可以直接使用下面代码来传递参数:


val function1 = object: Function1<Int,Unit> {
override fun invoke(p1: Int) {
Logger.d("$p1")
}
}
testInvoke.setSingleListener(function1)

这里看起来合情合理,因为在testRun函数中我们调用了invoke函数,把100当做参数,然后这个100会被回调到function1中,但是我们传递lambda时呢:


val testInvoke  = TestInvoke()
testInvoke.setSingleListener { returnInt ->
Logger.d("$returnInt")
}

上面代码传递lambda和传递一个类的实例效果是一样的,只不过这里只是一段代码块,没有显示的调用invoke啥的,所以这就是一个特性,当lambda被用作参数被函数调用时,也就可以看成是一次invoke的自动调用


invoke在DSL中的实践:Gradle依赖


这里我们为什么要说这个invoke依赖呢,很大的原因就是它在一些DSL中有很好的用法,这里我们就来看个Gradle依赖的使用。


我们很常见下面代码:


dependencies {

implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
//...
}

这里我们都很习以为常,感觉这里很像配置项,而不像是代码,其实这个也是一段代码,只不过是这种风格。那这种风格如何实现呢,我们来简单实现一下:


class DependencyHandler{
//编译库
fun compile(libString: String){
Logger.d("add $libString")
}
//定义invoke方法
operator fun invoke(body: DependencyHandler.() -> Unit){
body()
}
}

上面代码写完后,我们便可以有下面3种调用方式:


val dependency = DependencyHandler()
//调用invoke
dependency.invoke {
compile("androidx.core:core-ktx:1.6.0")
}
//直接调用
dependency.compile("androidx.core:core-ktx:1.6.0")
//带接受者lambda方式
dependency{
compile("androidx.core:core-ktx:1.6.0")
}

由此可见,上面代码第三种方式便是我们在Gradle配置文件中常见的一种,这里其实就2个关键点,一个是定义invoke函数,一个是定义带接受者的lambda,调用时省去this即可。


总结


其实关于invoke约定和带接受者lambda的写法现在越来越流行了,比如之前的anko库,现在的compose库都是这种声明式的写法,看完原理后,就会发现其实还是很方便的。


后续开始研究compose的时候,再来补充一波。


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

android充电架构的分析

前言 目前android设备越来越多,对于快速充电和长时间待机的需求就不言而喻。对应于此的就是各大手机厂商不断突破大功率充电新闻频繁的出现。在个人目前遇到的快充方案中,基本上在大的架构下属于同一种类型。故分析记录下来。 基本原理 充电简单粗暴点来说就是把电流灌...
继续阅读 »

前言


目前android设备越来越多,对于快速充电和长时间待机的需求就不言而喻。对应于此的就是各大手机厂商不断突破大功率充电新闻频繁的出现。在个人目前遇到的快充方案中,基本上在大的架构下属于同一种类型。故分析记录下来。


基本原理


充电简单粗暴点来说就是把电流灌到电池里面去。那么最简单的方法就是直接拿一个电源接在电池的正负极。只要电源电压高于电池电压就可以把电流灌进去。就如同直接打开水龙头开关接水一样。


但是这样会存在很多问题。例如:电池此时的电压很小,电源电压很高,一怼上电池上的电流就会变得非常大,很可能烧坏电池。所以需要根据电池的电压来调节输入电源的电压。这样又会出现充一会后就调一下电压,太麻烦了。因此可以使用计算机来完成这些动作,通过一颗锂电池充放电芯片来管理充放电的过程。


使用锂电池充电芯片,整个充电过程大致分成了三个阶段:分别是预充、恒流、恒压。

image

上图是各个阶段电池电流,电压的状态。

但是该方案存在转化效率较低的问题,特别是在大功率的情况下,损耗太大。因此在手机里不常采用这个方案,不过无论怎样,基本的充电曲线还是和上图保持一致的。


基本硬件架构


现在手机充电的基本架构如下图所示

image


电源输入


首先是电源的输入。目前手机上输入电源普遍支持有线和无线两种方式。在该架构下并不是简单让电源输入一个固定电压完成充电。而在由ap在不同阶段调节输入电源的功率。因此电源的提供需要支持调压的过程。现在在无线中常使用qi协议,而有线中常使用pd协议。


充电模块


充电模块现在主要由main charger(充电芯片)和charger pump(电荷泵芯片)构成,因为charger pump在大电流的情况下效率很高。整个充电过程依旧和上面的充电曲线基本一致。只不过此时的cc阶段和前半段的cv阶段由charger pump来完成,其余的由main charger来完成


main charger芯片在自身集成了数字逻辑,所以可以自动的切换各个阶段。而charger pump则不是这样,它只是一个模拟器件,我们可以把它理解成一个开关(经过它以后电流升一倍,电压降一半。譬如输入10v,5a输出就是5v,10a


故在使用过程中通过ap的程序来模拟充电阶段。比如我们规定在cc阶段下的电流是6a,而此时ap采集到的电流是4a。那我们就通过协议去增大电源的输入;采集到的电流大于6a则降低电源的输入。


电量计模块


电量计模块主要是获取电池的电量信息。然后上层可以获得电池剩余电量百分比等。


软件架构


android手机内核采用的是linux内核,android有很多层。单单对于充电功能来说可以把它分为2层,一层是kernel里的驱动和逻辑实现,另一层是上层。android界面上显示和充电相关的信息就需要从kernel这一层拿。


在内核中一切皆文件,那对于充电这部分来说也不例外。上层获取的信息都是从/sys/class/power supply路径获取的。这个路径是由代码决定的(源码路径:/system/core/healthd/BatteryMonitor.cpp)

image

在充电信息发生改变的时候kernel就会调用uevent,上层就会接收到,然后执行下面的函数更新状态。


bool BatteryMonitor::update(void) {
bool logthis;

initBatteryProperties(&props);

if (!mHealthdConfig->batteryPresentPath.isEmpty())
props.batteryPresent = getBooleanField(mHealthdConfig->batteryPresentPath);
else
props.batteryPresent = mBatteryDevicePresent;

props.batteryLevel = mBatteryFixedCapacity ?
mBatteryFixedCapacity :
getIntField(mHealthdConfig->batteryCapacityPath);
props.batteryVoltage = getIntField(mHealthdConfig->batteryVoltagePath) / 1000;

if (!mHealthdConfig->batteryCurrentNowPath.isEmpty())
props.batteryCurrent = getIntField(mHealthdConfig->batteryCurrentNowPath) / 1000;

if (!mHealthdConfig->batteryFullChargePath.isEmpty())
props.batteryFullCharge = getIntField(mHealthdConfig->batteryFullChargePath);

if (!mHealthdConfig->batteryCycleCountPath.isEmpty())
props.batteryCycleCount = getIntField(mHealthdConfig->batteryCycleCountPath);

if (!mHealthdConfig->batteryChargeCounterPath.isEmpty())
props.batteryChargeCounter = getIntField(mHealthdConfig->batteryChargeCounterPath);

props.batteryTemperature = mBatteryFixedTemperature ?
mBatteryFixedTemperature :
getIntField(mHealthdConfig->batteryTemperaturePath);

std::string buf;

if (readFromFile(mHealthdConfig->batteryStatusPath, &buf) > 0)
props.batteryStatus = getBatteryStatus(buf.c_str());

if (readFromFile(mHealthdConfig->batteryHealthPath, &buf) > 0)
props.batteryHealth = getBatteryHealth(buf.c_str());

if (readFromFile(mHealthdConfig->batteryTechnologyPath, &buf) > 0)
props.batteryTechnology = String8(buf.c_str());

unsigned int i;
double MaxPower = 0;

for (i = 0; i < mChargerNames.size(); i++) {
String8 path;
path.appendFormat("%s/%s/online", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
if (getIntField(path)) {
path.clear();
path.appendFormat("%s/%s/type", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
switch(readPowerSupplyType(path)) {
case ANDROID_POWER_SUPPLY_TYPE_AC:
props.chargerAcOnline = true;
break;
case ANDROID_POWER_SUPPLY_TYPE_USB:
props.chargerUsbOnline = true;
break;
case ANDROID_POWER_SUPPLY_TYPE_WIRELESS:
props.chargerWirelessOnline = true;
break;
default:
KLOG_WARNING(LOG_TAG, "%s: Unknown power supply type\n",
mChargerNames[i].string());
}
path.clear();
path.appendFormat("%s/%s/current_max", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());
int ChargingCurrent =
(access(path.string(), R_OK) == 0) ? getIntField(path) : 0;

path.clear();
path.appendFormat("%s/%s/voltage_max", POWER_SUPPLY_SYSFS_PATH,
mChargerNames[i].string());

int ChargingVoltage =
(access(path.string(), R_OK) == 0) ? getIntField(path) :
DEFAULT_VBUS_VOLTAGE;

double power = ((double)ChargingCurrent / MILLION) *
((double)ChargingVoltage / MILLION);
if (MaxPower < power) {
props.maxChargingCurrent = ChargingCurrent;
props.maxChargingVoltage = ChargingVoltage;
MaxPower = power;
}
}
}

logthis = !healthd_board_battery_update(&props);

if (logthis) {
char dmesgline[256];
size_t len;
if (props.batteryPresent) {
snprintf(dmesgline, sizeof(dmesgline),
"battery l=%d v=%d t=%s%d.%d h=%d st=%d",
props.batteryLevel, props.batteryVoltage,
props.batteryTemperature < 0 ? "-" : "",
abs(props.batteryTemperature / 10),
abs(props.batteryTemperature % 10), props.batteryHealth,
props.batteryStatus);

len = strlen(dmesgline);
if (!mHealthdConfig->batteryCurrentNowPath.isEmpty()) {
len += snprintf(dmesgline + len, sizeof(dmesgline) - len,
" c=%d", props.batteryCurrent);
}

if (!mHealthdConfig->batteryFullChargePath.isEmpty()) {
len += snprintf(dmesgline + len, sizeof(dmesgline) - len,
" fc=%d", props.batteryFullCharge);
}

if (!mHealthdConfig->batteryCycleCountPath.isEmpty()) {
len += snprintf(dmesgline + len, sizeof(dmesgline) - len,
" cc=%d", props.batteryCycleCount);
}
} else {
len = snprintf(dmesgline, sizeof(dmesgline),
"battery none");
}

snprintf(dmesgline + len, sizeof(dmesgline) - len, " chg=%s%s%s",
props.chargerAcOnline ? "a" : "",
props.chargerUsbOnline ? "u" : "",
props.chargerWirelessOnline ? "w" : "");

KLOG_WARNING(LOG_TAG, "%s\n", dmesgline);
}

healthd_mode_ops->battery_update(&props);
return props.chargerAcOnline | props.chargerUsbOnline |
props.chargerWirelessOnline;
}


很显然可以从code看出,通过该路径下文件夹里的type文件的值,来获得不同的信息。
例如
image

此时我进入到test_usb节点下,type为usb。然后online节点则表示usb连接是否在线,此时为1,则表示usb连接,充电图标会亮起来。

image

然后通过命令echo 0 > online ,强制将该值写为0。充电图标消失。

image


故在手机端充电相关的开发基本围绕着power supply架构展开的。下面依次从kernel到上层应用进行分析。



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

手把手带你配置MySQL主备环境

为了保障生产数据的安全性,我们往往需要对数据库做备份,而通过MySQL主备配置,则是一种MySQL数据库备份的好的实现方案。本文将一步步带你搭建MySQL主备环境。MySQL主备搭建现有两台虚拟机,192.168.56.11(主)和192.168.56.12(...
继续阅读 »



为了保障生产数据的安全性,我们往往需要对数据库做备份,而通过MySQL主备配置,则是一种MySQL数据库备份的好的实现方案。本文将一步步带你搭建MySQL主备环境。

MySQL主备搭建

现有两台虚拟机,192.168.56.11(主)和192.168.56.12(备)

1 MySQL安装

离线安装解压版MySQL

1 上传压缩包
  • 以root用户上传mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz到/root/Downloads/下

2 解压
  • cd /root/Downloads

  • tar -zxvf mysql-5.7.29-linux-glibc2.12-x86_64.tar.gz

  • mv mysql-5.7.29-linux-glibc2.12-x86_64 mysql5.7

  • mkdir /usr/database

  • mv mysql5.7 /usr/database

3 创建系统用户组和用户
  • groupadd mysql

  • useradd -r -g mysql mysql

  • id mysql

4 创建mysql data目录
  • cd /usr/database/mysql5.7

  • mkdir data

4 设置data目录权限
  • chown -R mysql:mysql /usr/database/mysql5.7/

  • ll /usr/database

5 修改my.cnf文件

删除并重新创建/etc/my.cnf:

rm -rf /etc/my.cnf
vim /etc/my.cnf
[client]
port = 3306
socket = /tmp/mysql.sock

[mysqld]
init-connect='SET NAMES utf8'
basedir=/usr/database/mysql5.7     #根据自己的安装目录填写
datadir=/usr/database/mysql5.7/data #根据自己的mysql数据目录填写
socket=/tmp/mysql.sock
max_connections=200                 # 允许最大连接数
character-set-server=utf8           # 服务端使用的字符集默认为8比特编码的latin1字符集
default-storage-engine=INNODB       # 创建新表时将使用的默认存储引擎

最大连接数

  • max_connections<=16384

  • 管理员(SUPER)登录的连接,不计其中

  • mysql会为每个连接提供连接缓冲区,连接越多内存开销越大

  • 查询:show variables like '%_connections';show status like '%_connections';

  • max_used_connections / max_connections * 100% (理想值≈ 85%)

  • 数值过小会经常出现ERROR 1040: Too many connections错误

  • 修改方法:

    • 永久:在配置文件my.cnf中设置max_connections的值

    • 临时:以root登录mysql:set GLOBAL max_connections=xxx;->flush privileges;

mysql存储引擎

  • InnoDB:5.5及之后版本的默认引擎

    • 支持事务

    • 聚集索引,文件存放在主键索引的叶子节点上,必须要有主键。通过主键索引效率很高,但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。

    • 不支持全文类型索引

    • 支持外键

    • 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。

    • 最小锁粒度是行锁

    • 适用场景:支持事物、较多写操作、系统崩溃后相对易恢复

  • MyISAM:5.5版本之前的默认引擎

    • 不支持事务

    • 非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。

    • 保存了整个表的行数,执行select count(*) 时只需要读出该变量即可,速度很快;

    • 支持全文类型索引

    • 不支持外键

    • 最小锁粒度是表锁,一个更新语句会锁住整张表,导致其他查询和更新都被阻塞,因此并发访问受限

    • 表以文件形式保存,跨平台使用较方便

    • 适用场景:非事物型、读操作、小型应用

6 mysql初始化
  • /usr/database/mysql5.7/bin/mysqld --initialize-insecure --user=mysql --basedir=/usr/database/mysql5.7 --datadir=/usr/database/mysql5.7/data

#注意:mysqld --initialize-insecure初始化后的mysql是没有密码的

  • chown -R root:root /usr/database/mysql5.7/ #把安装目录的目录的权限所有者改为root

  • chown -R mysql:mysql /usr/database/mysql5.7/data/ #把data目录的权限所有者改为mysql

7 启动mysql
/usr/database/mysql5.7/bin/mysqld_safe --user=mysql &
8 修改root密码
  • cd /usr/database/mysql5.7/bin

  • ./mysql -u root -p # 默认没有密码,直接回车就行

  • use mysql;

  • update user set authentication_string=password('``rootpasswd``') where user='root';

  • flush privileges;

  • exit;

9 登录测试
  • /usr/database/mysql5.7/bin/mysql mysql -u root -p

  • (输入密码)

  • show databases;

  • exit;

10 启动设置
  • cp /usr/database/mysql5.7/support-files/mysql.server /etc/init.d/mysql

  • chkconfig --add mysql # 添加服务

  • chkconfig --list # 查看服务列表

  • chkconfig --level 345 mysql on # 设置开机启动

11 测试服务命令是否可用
  • systemctl status mysql

  • systemctl start mysql

  • systemctl stop mysql

12 设置远程访问
  • 登录数据库:mysql -uroot -p[password]

  • use mysql;

  • select host,user from user;

  • update user set host='%' where user='root';

  • flush privileges;

  • 如果还是无法访问,检查防火墙

13 创建新用户
  • 创建用户app:create user 'app'@'%' identified by 'password';

  • 用户赋权(具有数据库appdb的所有权限,并可远程不限ip访问):grant all on appdb.* to 'app'@'%';

  • flush privilegesl;

14 其他问题
  • -bash: mysql: command not found

    • 临时方案,重新登录后失效:alias mysql=/usr/database/mysql5.7/bin/mysql

    • 永久方案,将命令路径添加到PATH中:

      • vim /etc/profile

      • PATH="$PATH:/usr/database/mysql5.7/bin"

      • source /etc/profile

2 主备配置

1 生产环境为什么需要MySQL集群
  • 高可用性,在主节点失效时自动切换,不需要技术人员紧急处理

  • 高吞吐,可以多个节点同时提供读取数据服务,降低主节点负载,实现高吞吐

  • 可扩展性强,支持在线扩容

  • 无影响备份,在备节点进行备份操作不会对业务产生影响

MySQL集群的缺点:

  • 架构复杂,在部署、管理方面对技术人员要求高

  • 备节点拉取主节点日志时会对主节点服务器性能有一定影响

  • 如果配置了半同步复制,会对事物提交有一定影响

2 修改my.cnf
  • 主备服务器均创建日志文件路径:mkdir /data/mysql_log

  • 修改master下的/etc/my.cnf:

[client]
port = 3306
default-character-set=utf8mb4
socket = /usr/database/mysql5.7/mysql.sock

[mysqld]
basedir = /usr/database/mysql5.7
datadir = /usr/database/mysql5.7/data
tmpdir = /tmp
socket = /tmp/mysql.sock
# pid-file = /data/mysql_db/mysql_seg_3306/mysql.pid
skip-external-locking = 1
skip-name-resolve = 1
port = 3306
server-id = 113306

default-storage-engine = InnoDB
character-set-server = utf8mb4
default_password_lifetime=0

#### log ####
log_timestamps=system
log_bin = /data/mysql_log/mysql-bin
log_bin_index = /data/mysql_log/mysql-bin.index
binlog_format = row
relay_log_recovery=ON
relay_log=/data/mysql_log/mysql-relay-bin
relay_log_index=/data/mysql_log/mysql-relay-bin.index
log_error = /data/mysql_log/mysql-error.log

#### replication ####
replicate_wild_ignore_table = information_schema.%,performance_schema.%,sys.%

#### semi sync replication settings #####
plugin_dir=/usr/database/mysql5.7/lib/plugin
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
loose_rpl_semi_sync_master_enabled = 1
loose_rpl_semi_sync_slave_enabled = 1
loose_rpl_semi_sync_master_timeout = 5000
  • 修改slave下的/etc/my.cnf:

[client]
port = 3306
default-character-set=utf8mb4
socket = /usr/database/mysql5.7/mysql.sock

[mysqld]
basedir = /usr/database/mysql5.7
datadir = /usr/database/mysql5.7/data
tmpdir = /tmp
socket = /tmp/mysql.sock
# pid-file = /data/mysql_db/mysql_seg_3306/mysql.pid
skip-external-locking = 1
skip-name-resolve = 1
port = 3306
server-id = 123306
read-only=1

default-storage-engine = InnoDB
character-set-server = utf8mb4
default_password_lifetime=0

#### log ####
log_timestamps=system
log_bin = /data/mysql_log/mysql-bin
log_bin_index = /data/mysql_log/mysql-bin.index
binlog_format = row
relay_log_recovery=ON
relay_log=/data/mysql_log/mysql-relay-bin
relay_log_index=/data/mysql_log/mysql-relay-bin.index
log_error = /data/mysql_log/mysql-error.log

#### replication ####
replicate_wild_ignore_table = information_schema.%,performance_schema.%,sys.%

#### semi sync replication settings #####
plugin_dir=/usr/database/mysql5.7/lib/plugin
plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"
loose_rpl_semi_sync_master_enabled = 1
loose_rpl_semi_sync_slave_enabled = 1
loose_rpl_semi_sync_master_timeout = 5000
  • utf8:最大unicode字符是0xffff,仅支持Unicode 中的基本多文种平面(BMP),任何不在基本多文本平面的 Unicode字符,都无法使用 Mysql 的 utf8 字符集存储。

  • utf8mb4(most bytes 4)专门用来兼容四字节的unicode,是utf8的超集,包含很多不常用的汉字、Emoji表情,以及任何新增的 Unicode 字符等等

3 master上创建同步用户
  • mysql -uroot -proot

  • use mysql;

  • create user 'repl'@'%' identified by 'repl';

  • grant replication slave on . to 'repl'@'%';

  • flush privileges;

4 备份master数据库
/usr/database/mysql5.7/bin/mysqldump -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -A > mysql_backup_full.sql

上述命令报错,1045,增加登录信息:

/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -A > mysql_backup_full.sql

导出指定数据库(仅表结构):

/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 -d testdb1 testdb2 > mysql_backup_test.sql

导出指定数据库(表结构+数据):

**/usr/database/mysql5.7/bin/mysqldump -uroot -proot -S /tmp/mysql.sock -F --opt -R --single-transaction --master-data=2 --default-character-set=utf8 testdb1 testdb2 > mysql_backup_testdatas.sql**
5 slave上恢复master数据库

master推送sql:rsync -avzP mysql_backup_test.sql 192.168.56.12:/root/Downloads/

slave导入sql:/usr/database/mysql5.7/bin/mysqldump -S /tmp/mysql.sock < mysql_backup_test.sql

上述命令不成功,需先创建数据库,改为下述操作:

**mysql -uroot -proot**
**use test;**
**source /root/Downloads/**``**mysql_backup_test.sql**
6 开启同步
  • mysql命令行中查看master status中的File和Position参数:show master status;

    • 查看进程:show processlist\G

  • slave的mysql命令行执行:

mysql> CHANGE MASTER TO
   -> MASTER_HOST='192.168.41.83',
   -> MASTER_PORT=3306,
   -> MASTER_USER='repl',
   -> MASTER_PASSWORD='repl',
   -> MASTER_LOG_FILE='mysql-bin.000004',
   -> MASTER_LOG_POS=154;
mysql> start slave;
mysql> show slave status\G

状态中注意这几项:

Slave_IO_Running:取 Master 日志的线程, Yes 为正在运行

Slave_SQL_Running:从日志恢复数据的线程, Yes 为正在运行

Seconds_Behind_Master:当前数据库相对于主库的数据延迟, 这个值是根据二进制日志的时间戳计算得到的(秒)

7 同步测试

master数据库插入一条数据,可用看到slave同步更新了

主备有了,要是能够故障自动切换就完美了,这正是下一篇内容。

引用请注明出处!

收起阅读 »

动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?

90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。今天的面试题是:如何实现动态代理?JDK Proxy 和 CGLib 有什...
继续阅读 »



90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。

今天的面试题是:如何实现动态代理?JDK Proxy 和 CGLib 有什么区别?

典型回答

动态代理的常用实现方式是反射。反射机制 是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。

但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。

JDK Proxy 和 CGLib 的区别主要体现在以下几个方面:

  • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;

  • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;

  • JDK Proxy 是通过拦截器加反射的方式实现的;

  • JDK Proxy 只能代理继承接口的类;

  • JDK Proxy 实现和调用起来比较简单;

  • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;

  • CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。

考点分析

此面试题考察的是你对反射、动态代理及 CGLib 的了解,很多人经常会把反射和动态代理划为等号,但从严格意义上来说,这种想法是不正确的,真正能搞懂它们之间的关系,也体现了你扎实 Java 的基本功。和这个问题相关的知识点,还有以下几个:

  • 你对 JDK Proxy 和 CGLib 的掌握程度。

  • Lombok 是通过反射实现的吗?

  • 动态代理和静态代理有什么区别?

  • 动态代理的使用场景有哪些?

  • Spring 中的动态代理是通过什么方式实现的?

知识扩展

1.JDK Proxy 和 CGLib 的使用及代码分析

JDK Proxy 动态代理实现

JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* JDK Proxy 相关示例
*/
public class ProxyExample {
  static interface Car {
      void running();
  }

  static class Bus implements Car {
      @Override
      public void running() {
          System.out.println("The bus is running.");
      }
  }

  static class Taxi implements Car {
      @Override
      public void running() {
          System.out.println("The taxi is running.");
      }
  }

  /**
    * JDK Proxy
    */
  static class JDKProxy implements InvocationHandler {
      private Object target; // 代理对象

      // 获取到代理对象
      public Object getInstance(Object target) {
          this.target = target;
          // 取得代理对象
          return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                  target.getClass().getInterfaces(), this);
      }

      /**
        * 执行代理方法
        * @param proxy 代理对象
        * @param method 代理方法
        * @param args   方法的参数
        * @return
        * @throws InvocationTargetException
        * @throws IllegalAccessException
        */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
              throws InvocationTargetException, IllegalAccessException {
          System.out.println("动态代理之前的业务处理.");
          Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理)
          return result;
      }
  }

  public static void main(String[] args) {
      // 执行 JDK Proxy
      JDKProxy jdkProxy = new JDKProxy();
      Car carInstance = (Car) jdkProxy.getInstance(new Taxi());
      carInstance.running();
}

以上程序的执行结果是:

动态代理之前的业务处理.
The taxi is running.

可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下:

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。

CGLib 的实现

在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置:

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

CGLib 实现代码如下:

package com.lagou.interview;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLibExample {

  static class Car {
      public void running() {
          System.out.println("The car is running.");
      }
  }

  /**
    * CGLib 代理类
    */
  static class CGLibProxy implements MethodInterceptor {
      private Object target; // 代理对象

      public Object getInstance(Object target) {
          this.target = target;
          Enhancer enhancer = new Enhancer();
          // 设置父类为实例类
          enhancer.setSuperclass(this.target.getClass());
          // 回调方法
          enhancer.setCallback(this);
          // 创建代理对象
          return enhancer.create();
      }

      @Override
      public Object intercept(Object o, Method method,
                              Object[] objects, MethodProxy methodProxy) throws Throwable {
          System.out.println("方法调用前业务处理.");
          Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
          return result;
      }
  }

  // 执行 CGLib 的方法调用
  public static void main(String[] args) {
      // 创建 CGLib 代理类
      CGLibProxy proxy = new CGLibProxy();
      // 初始化代理对象
      Car car = (Car) proxy.getInstance(new Car());
      // 执行方法
      car.running();
}

以上程序的执行结果是:

方法调用前业务处理.
The car is running.

可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。

2.Lombok 原理分析

在开始讲 Lombok 的原理之前,我们先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效的解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,向这种方法都可以使用 Lombok 注解来完成。

例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的:

public class Person {
  private Integer id;
  private String name;
  public Integer getId() {
      return id;
  }
  public void setId(Integer id) {
      this.id = id;
  }
  public String getName() {
      return name;
  }
  public void setName(String name) {
      this.name = name;
  }
}

在使用 Lombok 之后,代码是这样的:

@Data
public class Person {
  private Integer id;
  private String name;
}

可以看出 Lombok 让代码简单和优雅了很多。

小贴士:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,我们需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示:

接下来讲讲 Lombok 的原理。

Lombok 的实现和反射没有任何关系,前面我们说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢?

回到我们刚才 Setter/Getter 的方法,当我们打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码竟然是这样的:

可以看出 Lombok 是在编译期就为我们生成了对应的字节码。

其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下:

从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。

3.动态代理知识点扩充

当面试官问动态代理的时候,经常会问到它和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。

动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。

Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-/> 即可。

小结

今天我们介绍了 JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。

除了 JDK Proxy 和 CGLib 之外,我们还讲了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式,希望本文可以帮助到你。

作者:Java面试真题解析
来源:https://mp.weixin.qq.com/s/UIMeWRerV9FhtEotV8CF9w

收起阅读 »

面试官:this和super有什么区别?this能调用到父类吗?

this 和 super 都是 Java 中常见的关键字,虽然二者在很多情况下都可以被省略,但它们在 Java 中所起的作用是不可磨灭的。它们都是用来起指代作用的,每个类在实例化的时候之所以能调用到 Object 类(Object 类是所有类的父类),全是二者...
继续阅读 »

this 和 super 都是 Java 中常见的关键字,虽然二者在很多情况下都可以被省略,但它们在 Java 中所起的作用是不可磨灭的。它们都是用来起指代作用的,每个类在实例化的时候之所以能调用到 Object 类(Object 类是所有类的父类),全是二者的“功劳”。

1.super 关键字

super 是用来访问父类实例属性和方法的。

1.1 super 方法使用

每个实例类如果没有显示的指定构造方法,那么它会生成一个隐藏的无参构造方法。对于 super() 方法也是类似,如果没有显示指定 super() 方法,那么子类会生成一个隐藏的 super() 方法,用来调用父类的无参构造方法,这就是咱们开篇所说的“每个类在实例化的时候之所以能调用到 Object 类,就是默认 super 方法起作用了”,接下来我们通过实例来验证一下这个说法。

PS:所谓的“显示”,是指在程序中主动的调用,也就是在程序中添加相应的执行代码。

public class SuperExample {
   // 测试方法
   public static void main(String[] args) {
       Son son = new Son();
  }
}

/**
* 父类
*/
class Father {
   public Father() {
       System.out.println("执行父类的构造方法");
  }
}

/**
* 子类
*/
class Son extends Father {
}

在以上代码中,子类 Son 并没有显示指定 super() 方法,我们运行以上程序,执行的结果如下: 从上述的打印结果可以看出,子类 Son 在没有显示指定 super() 方法的情况下,竟然调用了父类的无参构造方法,这样从侧面验证了,如果子类没有显示指定 super() 方法,那么它也会生成一个隐藏的 super() 方法。这一点我们也可以从此类生成的字节码文件中得到证实,如下图所示:

super 方法注意事项

如果显示使用 super() 方法,那么 super() 方法必须放在构造方法的首行,否则编译器会报错,如下代码所示: 如上图看到的那样,如果 super() 方法没有放在首行,那么编译器就会报错:提示 super() 方法必须放到构造方法的首行。 为什么要把 super() 方法放在首行呢? 这是因为,只要将 super() 方法放在首行,那么在实例化子类时才能确保父类已经被先初始化了。

1.2 super 属性使用

使用 super 还可以调用父类的属性,比如以下代码可以通过子类 Son 调用父类中的 age 属性,实现代码如下:

public class SuperExample {
   // 测试方法
   public static void main(String[] args) {
       Son son = new Son();
  }
}

/**
* 父类
*/
class Father {
   // 定义一个 age 属性
   public int age = 30;

   public Father() {
       super();
       System.out.println("执行父类的构造方法");
  }
}

/**
* 子类
*/
class Son extends Father {
   public Son() {
       System.out.println("父类 age:" + super.age);
  }
}

以上程序的执行结果如下图所示,在子类中成功地获取到了父类中的 age 属性:

2.this 关键字

this 是用来访问本类实例属性和方法的,它会先从本类中找,如果本类中找不到则在父类中找。

2.1 this 属性使用

this 最常见的用法是用来赋值本类属性的,比如常见的 setter 方法,如下代码所示: 上述代码中 this.name 表示 Person 类的 name 属性,此处的 this 关键字不能省略,如果省略就相当于给当前的局部变量 name 赋值 name,自己给自己赋值了。我们可以尝试一下,将 this 关键字取消掉,实现代码如下:

class Person {
   private String name;
   public void setName(String name) {
       this.name = name;
  }
   public String getName() {
       return name;
  }
}
public class ThisExample {
   public static void main(String[] args) {
       Person p = new Person();
       p.setName("磊哥");
       System.out.println(p.getName());
  }
}

以上程序的执行结果如下图所示: 从上述结果可以看出,将 this 关键字去掉之后,赋值失败,Person 对象中的 name 属性就为 null 了。

2.2 this 方法使用

我们可以使用 this() 方法来调用本类中的构造方法,具体实现代码如下:

public class ThisExample {
   // 测试方法
   public static void main(String[] args) {
       Son p = new Son("Java");
  }
}

/**
* 父类
*/
class Father {
   public Father() {
       System.out.println("执行父类的构造方法");
  }
}

/**
* 子类
*/
class Son extends Father {
   public Son() {
       System.out.println("子类中的无参构造方法");
  }
   public Son(String name) {
       // 使用 this 调用本类中无参的构造方法
       this();
       System.out.println("子类有参构造方法,name:" + name);
  }
}

以上程序的执行结果如下图所示: 从上述结果中可以看出,通过 this() 方法成功调用到了本类中的无参构造方法。

注意:this() 方法和 super() 方法的使用规则一样,如果显示的调用,只能放在方法的首行。

2.3 this 访问父类方法

接下来,我们尝试使用 this 访问父类方法,具体实现代码如下:

public class ThisExample {
   public static void main(String[] args) {
       Son son = new Son();
       son.sm();
  }
}

/**
* 父类
*/
class Father {
   public void fm() {
       System.out.println("调用了父类中的 fm() 方法");
  }
}

/**
* 子类
*/
class Son extends Father {
   public void sm() {
       System.out.println("调用子类的 sm() 方法访问父类方法");
       // 调用父类中的方法
       this.fm();
  }
}

以上程序的执行结果如下: 从上述结果可以看出,使用 this 是可以访问到父类中的方法的,this 会先从本类中找,如果找不到则会去父类中找。

3.this 和 super 的区别

1.指代的对象不同

super 指代的是父类,是用来访问父类的;而 this 指代的是当前类。

2.查找范围不同

super 只能查找父类,而 this 会先从本类中找,如果找不到则会去父类中找。

3.本类属性赋值不同

this 可以用来为本类的实例属性赋值,而 super 则不能实现此功能。

4.this 可用于 synchronized

因为 this 表示当前对象,所以this 可用于 synchronized(this){....} 加锁,而 super 则不能实现此功能。

总结

this 和 super 都是 Java 中的关键字,都起指代作用,当显示使用它们时,都需要将它们放在方法的首行(否则编译器会报错)。this 表示当前对象,super 用来指代父类对象,它们有四点不同:指代对象、查找访问、本类属性赋值和 synchronized 的使用不同。

作者:Java中文社群
来源:https://juejin.cn/post/7046994591253266440

收起阅读 »

觉得前端不需要懂算法?那来看下这个真实的例子

算法是问题的解决步骤,同一个问题可以有多种解决思路,也就会有多种算法,但是算法之间是有好坏之分的,区分标志就是复杂度。通过复杂度可以估算出耗时/内存占用等性能的好坏,所以我们用复杂度来评价算法。(不了解复杂度可以看这篇:性能分析不一定得用 Profiler,复...
继续阅读 »

算法是问题的解决步骤,同一个问题可以有多种解决思路,也就会有多种算法,但是算法之间是有好坏之分的,区分标志就是复杂度。

通过复杂度可以估算出耗时/内存占用等性能的好坏,所以我们用复杂度来评价算法。

(不了解复杂度可以看这篇:性能分析不一定得用 Profiler,复杂度分析也行

开发的时候,大多数场景下我们用最朴素的思路,也就是复杂度比较高的算法也没啥问题,好像也用不到各种高大上的算法,算法这个东西似乎可学可不学。

其实不是的,那是因为你没有遇到一些数据量大的场景。

下面我给你举一个我之前公司的具体场景的例子:

体现算法威力的例子

这是我前公司高德真实的例子。

我们会做全源码的依赖分析,会有几万个模块,一个模块依赖另一个模块叫做正向依赖,一个模块被另一个模块依赖叫做反向依赖。我们会先分析一遍正向依赖,然后再分析一遍反向依赖。

分析反向依赖的时候,之前的思路是这样的,对于每一个依赖,都遍历一边所有的模块,找到依赖它的模块,这就是它的反向依赖。

这个思路是很朴素的,容易想到的思路,但是这个思路有没有问题呢?

这个算法的复杂度是 O(n^2),如果 n 达到了十几万,那性能会很差的,从复杂度我们就可以估算出来。

事实上也确实是这样,后来我们跑一遍全源码依赖需要用 10 几个小时,甚至一晚上都跑不出来。

如果让你去优化,你会怎么优化性能呢?

有的同学可能会说,能不能拆成多进程/多个工作线程,把依赖分析的任务拆成几部分来做,这样能得到几倍的性能提升。

是,几倍的提升很大了。

但是如果说我们后来做了一个改动,性能直接提升了几万倍你信么?

我们的改动方式是这样的:

之前是在分析反向依赖的时候每一个依赖都要遍历一遍所有的正向依赖。但其实正向依赖反过来不就是反向依赖么?

所以我们直接改成了分析正向依赖的时候同时记录反向依赖。

这样根本就不需要单独分析反向依赖了,算法复杂度从 O(n^2)降到了 O(n)。

O(n^2) 到 O(n) 的变化在有几万个模块的时候,就相当于几万倍的性能提升。

这体现在时间上就是我们之前要跑一个晚上的代码,现在十几分钟就跑完了。这优化力度,你觉得光靠多线程/进程来跑能做到么?

这就是算法的威力,当你想到了一个复杂度更低的算法,那就意味着性能有了大幅的提升。

为什么我们整天说 diff 算法,因为它把 O(n^2) 的朴素算法复杂度降低到了 O(n),这就意味着 dom 节点有几千个的时候,就会有几千倍的性能提升。

所以,感受到算法的威力了么?

总结

多线程、缓存等手段最多提升几倍的性能,而算法的优化是直接提升数量级的性能,当数据量大了以后,就是几千几万倍的性能提升。

那为什么我们平时觉得算法没用呢?那是因为你处理的数据量太小了,处理几百个数据,你用 O(n^2) O(n^3) 和 O(n) 的算法,都差不了多少。

你处理的场景数据量越大,那算法的重要性越高,因为好的算法和差的算法的差别不是几倍几十倍那么简单,可能是几万倍的差别。

所以,你会见到各大公司都在考算法,没用么?不是的,是太过重要了,直接决定着写出的代码的性能。

原文:https://juejin.cn/post/7023024399376711694

收起阅读 »

前端监控系统设计

前言: 创建一个可随意插拔的插件式前端监控系统 一、数据采集 1.异常数据 1.1 静态资源异常 使用window.addEventListener('error',cb) 由于这个方法会捕获到很多error,所以我们要从中筛选出静态资源文件加载错误情况,这里...
继续阅读 »

前言: 创建一个可随意插拔的插件式前端监控系统


一、数据采集


1.异常数据


1.1 静态资源异常


使用window.addEventListener('error',cb)


由于这个方法会捕获到很多error,所以我们要从中筛选出静态资源文件加载错误情况,这里只监控了js、css、img


// 捕获静态资源加载失败错误 js css img
window.addEventListener('error', e => {
const target = e.targetl
if (!target) return
const typeName = e.target.localName;
let sourceUrl = "";
if (typeName === "link") {
sourceUrl = e.target.href;
} else if (typeName === "script" || typeName === "img") {
sourceUrl = e.target.src;
}

if (sourceUrl) {
lazyReportCache({
url: sourceUrl,
type: 'error',
subType: 'resource',
startTime: e.timeStamp,
html: target.outerHTML,
resourceType: target.tagName,
paths: e.path.map(item => item.tagName).filter(Boolean),
pageURL: getPageURL(),
})
}
}, true)


1.2 js错误


通过 window.onerror 获取错误发生时的行、列号,以及错误堆栈


生产环境需要上传打包后生成的map文件,利用source-map 对压缩后的代码文件和行列号得出未压缩前的报错行列数和源码文件


// parseErrorMsg.js
const fs = require('fs');
const path = require('path');
const sourceMap = require('source-map');

export default async function parseErrorMsg(error) {
const mapObj = JSON.parse(getMapFileContent(error.url))
const consumer = await new sourceMap.SourceMapConsumer(mapObj)
// 将 webpack://source-map-demo/./src/index.js 文件中的 ./ 去掉
const sources = mapObj.sources.map(item => format(item))
// 根据压缩后的报错信息得出未压缩前的报错行列数和源码文件
const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
// sourcesContent 中包含了各个文件的未压缩前的源码,根据文件名找出对应的源码
const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
return {
file: originalInfo.source,
content: originalFileContent,
line: originalInfo.line,
column: originalInfo.column,
msg: error.msg,
error: error.error
}
}

function format(item) {
return item.replace(/(\.\/)*/g, '')
}

function getMapFileContent(url) {
return fs.readFileSync(path.resolve(__dirname, `./dist/${url.split('/').pop()}.map`), 'utf-8')
}

1.3 自定义异常


通过console.error打印出来的,我们将其认为是自定义错误


使用 window.console.error 上报自定义异常信息


1.4 接口异常



  1. 当状态码异常时,上报异常

  2. 重写 onloadend 方法,当其 response 对象中 code 值不为 '000000' 时上报异常 

  3. 重写 onerror 方法,当网络中断时无法触发 onload(end) 事件,会触发 onerror, 此时上报异常


1.5 监听未处理的promise错误


当Promise 被reject 且没有reject 处理器的时候,就会触发 unhandledrejection 事件


使用 window.addEventListener('unhandledrejection',cb)


2.性能数据


2.1 FP/FCP/LCP/CLS


chrome 开发团队提出了一系列用于检测网页性能的指标:



  • FP(first-paint),从页面加载开始到第一个像素绘制到屏幕上的时间

  • FCP(first-contentful-paint),从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间

  • LCP(largest-contentful-paint),从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间

  • CLS(layout-shift),从页面加载开始和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数




其中,前三个性能指标都可以直接通过 PerformanceObserver (PerformanceObserver 是一个性能监测对象,用于监测性能度量事件 )来获取。而CLS 则需要通过一些计算。


在了解一下计算方式之前,我们先了解一下会话窗口的概念:一个或多个布局偏移间,它们之间有少于1秒的时间间隔,并且第一个和最后一个布局偏移时间间隔上限为5秒,超过5秒的布局偏移将被划分到新的会话窗口。


Chrome 速度指标团队在完成大规模分析后,将所有会话窗口中的偏移累加最大值用来反映页面布局最差的情况(即CLS)。


如下图:会话窗口2只有一个微小的布局偏移,则会话窗口2会被忽略,CLS只计算会话窗口1中布局偏移的总和。


拉低平均值的小布局偏移示例


2.2 DOMContentLoaded事件 和  onload 事件



  • DOMContentLoaded: HTML文档被加载和解析完成。在文档中没有脚本的情况下,浏览器解析完文档便能触发DOMContentLoaded;当文档中有脚本时,脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。但是在任何情况下,DOMContentLoaded 都不需要等图片等其他资源的解析。

  • onload: 需要等页面中图片、视频、音频等其他所有资源都加载后才会触发。


为什么我们在开发时强调把css放在头部,js放在尾部?


image.png


首先文件放置顺序决定下载的优先级,而浏览器为了避免样式变化导致页面重排or重绘,会阻塞内容的呈现,等所有css加载并解析完成后才一次性呈现页面内容,在此期间就会出现“白屏”。


而现代浏览器为了优化用户体验,无需等到所有HTML文档都解析完成才开始构建布局渲染树,也就是说浏览器能够渲染不完整的DOM tree和cssom,尽快减少白屏时间。


假设我们把js放在头部,js会阻塞解析dom,导致FP(First Paint)延后,所以我们将js放在尾部,以减少FP的时间,但不会减少 DOMContentLoaded 被触发的时间。


2.3 资源加载耗时及是否命中缓存情况


通过 PerformanceObserver 收集,当浏览器不支持 PerformanceObserver,还可以通过 performance.getEntriesByType(entryType) 来进行降级处理,其中:



  • Navigation Timing 收集了HTML文档的性能指标

  • Resource Timing 收集了文档依赖的资源的性能指标,如:css,js,图片等等


这里不统计以下资源类型:



  • beacon: 用于上报数据,不统计

  • xmlhttprequest:单独统计


我们能够获取到资源对象的如下信息:


image.png


使用performance.now()精确计算程序执行时间:



  • performance.now()  与 Date.now()  不同的是,返回了以微秒(百万分之一秒)为单位的时间,更加精准。并且与 Date.now()  会受系统程序执行阻塞的影响不同,performance.now()  的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)。

  • Date.now()  输出的是 UNIX 时间,即距离 1970 的时间,而 performance.now()  输出的是相对于 performance.timing.navigationStart(页面初始化) 的时间。

  • 使用 Date.now()  的差值并非绝对精确,因为计算时间时受系统限制(可能阻塞)。但使用 performance.now()  的差值,并不影响我们计算程序执行的精确时间。


判断该资源是否命中缓存:

在这些资源对象中有一个 transferSize 字段,它表示获取资源的大小,包括响应头字段和响应数据的大小。如果这个值为 0,说明是从缓存中直接读取的(强制缓存)。如果这个值不为 0,但是 encodedBodySize 字段为 0,说明它走的是协商缓存(encodedBodySize 表示请求响应数据 body 的大小)。不符合以上条件的,说明未命中缓存。


2.4 接口请求耗时以及接口调用成败情况


对XMLHttpRequest 原型链上的send 以及open方法进行改写


import { originalOpen, originalSend, originalProto } from '../utils/xhr'
import { lazyReportCache } from '../utils/report'

function overwriteOpenAndSend() {
originalProto.open = function newOpen(...args) {
this.url = args[1]
this.method = args[0]
originalOpen.apply(this, args)
}

originalProto.send = function newSend(...args) {
this.startTime = Date.now()

const onLoadend = () => {
this.endTime = Date.now()
this.duration = this.endTime - this.startTime

const { duration, startTime, endTime, url, method } = this
const { readyState, status, statusText, response, responseUrl, responseText } = this
console.log(this)
const reportData = {
status,
duration,
startTime,
endTime,
url,
method: (method || 'GET').toUpperCase(),
success: status >= 200 && status < 300,
subType: 'xhr',
type: 'performance',
}

lazyReportCache(reportData)

this.removeEventListener('loadend', onLoadend, true)
}

this.addEventListener('loadend', onLoadend, true)
originalSend.apply(this, args)
}
}

export default function xhr() {
overwriteOpenAndSend()
}

二、数据上报


1. 上报方法


采用sendBeacon 和 XMLHttpRequest 相结合的方式


为什么要使用sendBeacon?



统计和诊断代码通常要在 unload 或者 beforeunload (en-US) 事件处理器中发起一个同步 XMLHttpRequest 来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力。

navigator.sendBeacon()  方法可用于通过HTTP将少量数据异步传输到Web服务器,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。



2. 上报时机



  1. 先缓存上报数据,缓存到一定数量后,利用 requestIdleCallback/setTimeout 延时上报。

  2. 在即将离开当前页面(刷新或关闭)时上报 (onBeforeUnload )/ 在页面不可见时上报(onVisibilitychange,判断document.visibilityState/ document.hidden 状态)

作者:spider集控团队
链接:https://juejin.cn/post/7046697922255126558

收起阅读 »

拒绝!封装el-table,请别再用JSON数组来配置列了

阅读本文📖你将:明白通过JSON 来配置el-table的列可能并不是那么美。(作者主观意见)学会一点关于VNode操作的实例。(一点点)辩证地思考一下当我们在团队内对组件进行二次封装时,哪些东西是我们需要取舍的。前言大家好,我是春哥。我热爱&nbs...
继续阅读 »

阅读本文📖

你将:

  1. 明白通过JSON 来配置el-table的列可能并不是那么美。(作者主观意见)
  2. 学会一点关于VNode操作的实例。(一点点)
  3. 辩证地思考一下当我们在团队内对组件进行二次封装时,哪些东西是我们需要取舍的。

前言

大家好,我是春哥
我热爱 vue.js , ElementUI , Element Plus 相关技术栈,我的目标是给大家分享最实用、最有用的知识点,希望大家都可以早早下班,并可以飞速完成工作,淡定摸鱼🐟。

相信使用 vue 的同学大部分都用过 Element 系列框架,并且绝大部分都用过其中的 el-table 组件。并且几乎所有人都会把表格和分页进行一层封装。

不过,很多人在封装时,总是习惯性地把 el-table 官方推荐的 "插槽写法" 改成 "JSON 数组" 写法。

就像这样:

<template>
<my-el-table :columns="columns" :data="tableData">
</my-el-table>
</template>
<script setup>
const columns = [
{
prop: 'date',
label: 'Date',
width: '180'
},
{
prop: 'name',
label: 'Name'
}
]
// ...其他略
</script>

但经过我多年踩坑的惨痛经历,我必须要大声说出那句话:
快住手!有更好的封装技巧

尔康式拒绝

JSON 式封装哪些缺点?

缺点一:学习成本增高

以下两种场景,如果是你今天刚刚入职,你更愿意在业务代码里看到哪种组件呢?

你更愿意使用哪种组件?

我反正是更偏向于 1 和 4
第 1 种意味着它有丰富的社区支持,有准确而清晰的文档和 demo 可以借鉴。
第 4 种意味着你依然可以靠官方文档横行,并且可以使用一些同事根据业务进行的"增强能力"。

那么 2 和 3 呢?
也许我的同事真的可以做出很好的封装,但如果你在小厂、在初创公司、甚至在外包公司,更大的可能是你的同事并不靠谱。
他的某些封装只是为了满足单一的业务场景, 但你为了了解他的功能,却不得不去面对全新的 api,甚至是通过看他的源码才能了解具体有什么 api 和能力。

在这种场景下,我选择面对熟悉的,官方的 api。 

缺点二:自定义内容需要写 h 函数

不会真的有人喜欢在业务里写 h 函数吧?

当简单的 JSON 配置无法满足产品经理那天马行空的想象力时,你可能需要对 el-table-column 里的内容进行更多的自定义。
此时,也许你就会怀念"插槽式"的便捷了。
假设你的产品经理要求你写一个 带色彩的状态列 。 h和插槽

以上两种写法你会选择哪种呢?
而且,当业务变得更加复杂的时候,h 函数写法的可读性是指数式下跌的,你怕不怕?

当然,用JSX写法来简化 h 函数写法是个不错的思路。

JSON 式封装有哪些优点?

优点一:能简化写法?(并不)

有人说:“ JSON 式封装,能够简化代码写法。”
听到这样的话,我的内心其实是充满困惑的:它究竟能简化什么?
写法上的对比

看出来了吗?
这种常见的所谓 封装,只不过是做了做简单形式化的转换:你并没有少写哪怕一个属性,只不过把它们从这里挪到了那里。

甚至于极端场景,你还需要多写代码:

// 从:
<el-table-column show-overflow-tooltip />
// 变成:
{
'show-overflow-tooltip': true
}

优点二:只有 JSON 化才能实现动态列?(并不)

我在《我对 Element 的 Table 做了封装》 里讨论时, JackLiR8 同学提出了一个疑问: 
简化一下就是:

怎样封装才能在保持插槽写法的情况下,实现 动态列呢 ?

其实这个问题并不难,前提是需要你理解 vNode 是什么以及怎么操作它们。

我做了一个简单的例子,核心代码如下:

// vue3 函数式组件写法
const FunctionalTable = (props, context) => {
// 获得插槽里的 vNodes
const vNodes = context.slots.default()
// 过滤 vNodes
const filteredVNodes = props.visibleKeys == null ? vNodes : vNodes.filter(node => props.visibleKeys.includes(node?.props?.prop))
// 把属性透传给el-table
return <el-table {...context.attrs}>
{ filteredVNodes }
</el-table>
}
// vue3 函数式组件定义 props
FunctionalTable.props = {
visibleKeys: {
type: Array,
default: () => null
}
}

这就能实现 动态列 了?
是的。
下面正是使用时的代码:

<template>
<el-button @click="onClick">给我变!</el-button>
<FunctionalTable :data="tableData" :visibleKeys="visibleKeys">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="address" label="Address" />
</FunctionalTable>
</template>
<script setup>
// 其他略
const visibleKeys = ref(['date', 'name'])
const onClick = () => {
visibleKeys.value = ['date', 'name', 'address']
}
// 其他略
// ...

效果如下: 插槽写法的动态列

毫无疑问,当遇到复杂场景,以及列里需要渲染各种奇形怪状的东西如 tagpopover 或者你需要进行更加复杂的定义的时候,插槽写法是更为优秀的。
这是上述demo的源代码 => github源码

优点三:JSON 配置能存数据库?(我劝你慎重)

"如果我把列的 JSON 配置存到数据库里,那我就不用写代码了!"

好家伙,我直呼好家伙!

除非你已经封装了非常成熟的可视化配置方案,否则! 当业务上需要新增一列时,不还是你写?服务端和运维可不会帮你写代码。

只不过你存储代码的地点,从 git 变成了 数据库

碰上懒一点的服务端,你还需要安装数据库链接软件,增加一项写 sql update 语句的活儿。

更让人感到害怕的是,你丢失了对代码版本跟踪的能力。

"为啥生产库/测试库/开发库存的数据不一样,到底应该用谁,我也不知道这字段是哪个版本、因为什么被谁合入的呀...."

那一刻,你可能会无比怀念 git commit-msg 那。

我期望的封装是什么样的?

如果你想设计一款基于"element UI"或"element Plus",能解决一些迫在眉睫的问题,能优化一些写法,能规范一些格式,能让团队小伙伴们乐于使用到组件库。
我想,你可能得充分考虑以下内容:

  • 它的API是否简单易学(甚至大部分就和 element 一模一样 )?
  • 它是否确确实实简化了业务上的写法?
    比如把 表格 和 分页器 合并,比如提供 请求方法 作为 prop 等都是能极大降低业务复杂性的封装。
  • 它是否扩展性强,易维护?api 设计是否和项目保持风格上的一致?
    在同一个项目里,render/jsx/template 混用很可能会让一些新人感到吃力。
  • 它是否是增强和渐进的?
    我可不希望当我试图使用 elementUI 某个特性时,我猛然发现我同事封装的组件居然不支持!

震惊!

免喷声明

以上所有配图和文本都是我的个人观点。

如果你认为它们是错的,那它们就是错的吧。

关于我反对把 Element 表格列 JSON化 最初的初心,我确实厌倦了在不同团队不同公司总是要一遍又一遍去看前同事们蹩脚的封装,去理解他们做了哪些东西,拼写有没有改编,有没有丢失特性,去再学习一遍完全没有学习价值的API

希望大家写代码时,都能获得良好的体验。

封装组件时,都能封装出"能用","好用", "大家愿意用"的组件!


原文:https://juejin.cn/post/7043578962026430471

收起阅读 »

Python服务端快速调用环信MQTT REST接口下发消息

本文介绍Python服务端通过调用环信MQTT REST API接口快速实现消息下发,使用时可参阅REST发送消息接口介绍1. 前提条件1.1 获取服务器信息调用环信MQTT REST API接口前,需要获取四个环信MQTT服务器信息,包括:应用clientI...
继续阅读 »


本文介绍Python服务端通过调用环信MQTT REST API接口快速实现消息下发,使用时可参阅
REST发送消息接口介绍


1. 前提条件

1.1 获取服务器信息
调用环信MQTT REST API接口前,需要获取四个环信MQTT服务器信息,包括:应用clientID应用clientSecretREST API地址及应用ID
1、应用clientID:从环信console【应用概览】->【应用详情】->【开发者ID】下 "client ID"获取;
2、应用clientSecret:从环信console【应用概览】->【应用详情】->【开发者ID】下"clientSecret"获取; 
3、RSET API地址:从环信console【MQTT】->【服务概览】->【服务配置】下"REST API地址"获取; 
4、应用ID:从环信console【MQTT】->【服务概览】->【服务配置】下"AppId"获取;



2. 实现流程 
注:本代码对消息体内容进行GBK转码,可支持语音播报(适用于扬声器播放中文内容),如不需要此场景使用,可根据需求设置转码格式。

import requests
import time
import json
import base64


# 填写服务参数
# 1、app_client_id:应用clientID
# 2、app_client_secret:应用clientSecret
# 3、api_url_base:RSET API地址
# 4、app_id:应用ID
app_client_id = ' XXXXX'

app_client_secret = 'XXXX'

api_url_base = 'XXXXX'

app_id = 'XXXXXX'


# 播报文字
speak_text = '欢迎使用环信mqtt'


# 获取应用token
api_url_app_token = api_url_base + '/openapi/rm/app/token'
def get_app_token():
data = {
'appClientId':app_client_id,
'appClientSecret':app_client_secret
}

header = {'Content-Type': 'application/json'}

re = requests.post(api_url_app_token, headers=header, data=json.dumps(data))
return (json.loads(re.text)['body']['access_token'])


# 发送mqtt消息
api_url_publish = api_url_base + '/openapi/v1/rm/chat/publish'
def send_msg(app_token, txt):

# 智能音箱的 msgid 每次都不一样才会播报声音
# 这里用毫秒时间戳当作 msgid
time_millis = int(round(time.time() * 1000))

dat ={
'type':'tts_dynamic',
'msgid': time_millis,
'txt':txt ,
}

json_text = json.dumps(dat, ensure_ascii=False)
json_h = json_text.encode(encoding="gbk")
base64_bytes = base64.b64encode(json_h)
base64_utf8 = str(base64_bytes,'utf-8')

#topics,要发送的主题
#clientid,当前客户端ID,格式为“xxxx@appid”
data = {
'topics':['861714050059769'],
'clientid':'12@ff6sc0',
'payload':base64_utf8,
"encoding":'base64',
'qos':1,
'retain':0,
'expire':86400
}

header = {
'Content-Type': 'application/json',
'Authorization': app_token
}

re = requests.post(api_url_publish, headers=header, data=json.dumps(data))
return (json.loads(re.text))


print('正在获取应用token...')
app_token = get_app_token()
print('获取应用token成功')

print(send_msg(app_token, speak_text))
print('发送消息成功')



 三、更多信息

* 如果您在使用MQTT服务中,有任何疑问和建议,欢迎您联系我们

收起阅读 »

PHP服务端快速调用环信MQTT REST接口下发消息

本文介绍PHP服务端通过调用环信MQTT REST API接口快速实现消息下发,使用时可参阅REST发送消息接口介绍1. 前提条件1.1 获取服务器信息调用环信MQTT REST API接口前,需要获取四个环信MQTT服务器信息,包括:应用clientID、应...
继续阅读 »


本文介绍PHP服务端通过调用环信MQTT REST API接口快速实现消息下发,使用时可参阅
REST发送消息接口介绍


1. 前提条件

1.1 获取服务器信息
调用环信MQTT REST API接口前,需要获取四个环信MQTT服务器信息,包括:应用clientID应用clientSecretREST API地址及应用ID
1、应用clientID:从环信console【应用概览】->【应用详情】->【开发者ID】下 "client ID"获取;
2、应用clientSecret:从环信console【应用概览】->【应用详情】->【开发者ID】下"clientSecret"获取; 
3、RSET API地址:从环信console【MQTT】->【服务概览】->【服务配置】下"REST API地址"获取; 
4、应用ID:从环信console【MQTT】->【服务概览】->【服务配置】下"AppId"获取;



2. 实现流程 
注:本代码对消息体内容进行GBK转码,可支持语音播报(适用于扬声器播放中文内容),如不需要此场景使用,可根据需求设置转码格式。

// 填写服务参数
// 1、client_id:应用clientID
// 2、client_secret:应用clientSecret
// 3、rest_uri:RSET API地址
// 4、app_id:应用ID
$config = [
'rest_uri' => 'XXXXXX',
'client_id' => 'XXXXXX',
'client_secret' => 'XXXXXX',
'app_id' => 'XXXXXX',
];

// 实时 token
$accessToken = get_access_token();

// 固定值,有有效期
//$accessToken = 'YWMtiftbBF7sEeyeASnTGg_ZZCGtXR4YNTAxtZpP1MjdlZbv64ppqWZOEI663pDy48tKAgMAAAF9xoOlvAWP1ADm__IWx_b4TLJvCb9axcY6cNImjMXJcx1ty7UK-Ked2w';

// 发送消息
$message = [
'type' => 'tts_dynamic',
'msgid' => 'c1b5d5f46092d4c01a5f422ae2b9ad41188',
'txt' => '测试测试'
];
var_dump(send(['861714050059769'], $message));

/**
* @description: 获取 Token
* @return {String}
*/
function get_access_token()
{
global $config;
$uri = $config['rest_uri'] . '/openapi/rm/app/token';
$body = [
'appClientId' => $config['client_id'],
'appClientSecret' => $config['client_secret'],
];
$headers = [
'Content-Type' => 'application/json',
];
$ret = json_decode(curl_request($uri, $body, $headers), true);
return isset($ret['code']) && $ret['code'] == 200 ? $ret['body']['access_token'] : $ret;
}

/**
* @description: 发送消息
* @param {array} $topics 要发消息的主题数组
* @param {mixed} $message 要发送的消息内容
* @param {String} $deviceID deviceID 用户自定义
* @return {array}
*/
function send($topics, $message, $deviceID = '12')
{
global $config, $accessToken;
$uri = $config['rest_uri'] . '/openapi/v1/rm/chat/publish';
$body = [
'topics' => $topics,
'clientid' => "{$deviceID}@{$config['app_id']}",
'payload' => base64_encode(iconv("UTF-8", "GBK", json_encode($message, JSON_UNESCAPED_UNICODE))),
'encoding' => 'base64',
];

$headers = [
'Content-Type' => 'application/json',
'Authorization' => $accessToken,
];
$ret = json_decode(curl_request($uri, $body, $headers), true);
return $ret;
}

/**
* @description: 查看消息
* @param {String} $messageId 指定的消息ID
* @return {array}
*/
function show($messageId)
{
global $config, $accessToken;
$uri = $config['rest_uri'] . '/openapi/rm/message/message?messageId=' . $messageId;
$headers = [
'Content-Type' => 'application/json',
'Authorization' => $accessToken,
];
$ret = json_decode(curl_request($uri, null, $headers), true);
if (isset($ret['code']) && $ret['code'] == 200) {
$ret['body']['message'] = json_decode(iconv('GBK', 'UTF-8', base64_decode($ret['body']['message'])), true);
return $ret['body'];
}
return $ret;
}

function curl_request($url, $data = null, $headers = null)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
// CURLOPT_HEADER => true, // 将头文件的信息作为数据流输出
// CURLOPT_NOBODY => false, // true 时将不输出 BODY 部分。同时 Mehtod 变成了 HEAD。修改为 false 时不会变成 GET。
// CURLOPT_CUSTOMREQUEST => $request->method, // 请求方法
if(!empty($data)){
curl_setopt($ch, CURLOPT_POST, 1);
if (is_array($data)) {
$data = json_encode($data);
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
if(!empty($headers)){
curl_setopt($ch, CURLOPT_HTTPHEADER, buildHeaders($headers));
}
$output = curl_exec($ch);
// $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
return $output;
}

function buildHeaders($headers)
{{{:playground:message:微信交流群.jpeg?200|}}
$headersArr = array();
foreach ($headers as $key => $value) {
array_push($headersArr, "{$key}: {$value}");
}
return $headersArr;
}


 三、更多信息

* 如果您在使用MQTT服务中,有任何疑问和建议,欢迎您联系我们

收起阅读 »

Android编译插桩操作字节码

1. 概念 什么是编译插桩 顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。我们项目中的 Dagger、ButterKnife或者kotlin都用到了编译插桩技术。 要理解编译插桩,我们要先知道在Android中.java 文件是怎么编...
继续阅读 »

1. 概念


什么是编译插桩


顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。我们项目中的 Dagger、ButterKnife或者kotlin都用到了编译插桩技术。


要理解编译插桩,我们要先知道在Android中.java 文件是怎么编译的。


WechatIMG106.png


如上图所示,demo.java通过javac命令编译成demo.class文件,然后通过字节码文件编译器将class文件打包成.dex。


我们今天要说的插桩,就是在class文件转为.dex之前修改或者添加代码。


2. 场景


我们什么时候会用到它呢?



  • 日志埋点

  • 性能监控

  • 权限控制

  • 代码替换

  • 代码调试

  • 等等...


3. 插桩工具介绍




  • AspectJ




AspectJ 是老牌 AOP(Aspect-Oriented Programming)框架。其主要优势是成熟稳定,使用者也不需要对字节码文件有深入的理解。




  • ASM




ASM 最初起源于一个博士的研究项目,在 2002 年开源,并从 5.x 版本便开始支持 Java 8。并且,ASM 是诸多 JVM 语言钦定的字节码生成库,它在效率与性能方面的优势要远超其它的字节码操作库如 javassist、AspectJ。其主要优势是内存占用很小,运行速度快,操作灵活。但是上手难度大,需要对 Java 字节码有比较充分的了解。


本文使用 ASM 来实现简单的编译插桩效果,接下来我们是想一个小需求,


4. 实践


1. 创建AsmDemo项目,其中只有一个MainActivity


QQ20211224-135648@2x.png


2.创建自定义gradle插件


QQ20211224-135825@2x.png
删除module中main文件夹下所有目录,新建groovy跟java目录。


2222.png
gradle插件是用groovy编写的,所以groovy文件存放.groovy文件,java目录中存放asm相关类。
清空build.gradle文件内容,改为如下内容:


plugins {
id 'groovy'
id 'maven'
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.4'
}
group = "demo.asm.plugin"
version = "1.0.0"

uploadArchives {
repositories {
mavenDeployer {
repository(url: uri("../asm_lifecycle_repo"))
}
}
}

3.创建LifeCyclePlugin文件


package demo.asm.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin {

@Override
void apply(Project target) {
println("hello this is my plugin")
}
}

LifeCyclePlugin实现了Plugin接口,但我们在app中使用此插件的时候,LifeCyclePlugin的apply插件会被调用。


接着创建properties文件:
首先在main下面创建resources/META-INF/gradle-plugins目录,然后在gradle-plugins中创建demo.asm.lifecycle.properties,并填入如下内容:


implementation-class=demo.asm.plugin.LifeCyclePlugin

其中文件名demo.asm.lifecycle就是我们插件的名称,后续我们需要在app的build.gradle文件中引用此插件。
好了,现在我们的插件已经写完了,我们把他部署到本地仓库中来测试一下。发布地址在上述build.grale文件中repository属性配置。我将其配置在asm_lifecycle_repo目录中。


我们在 Android Studio 的右边栏找到 Gradle 中点击 uploadArchives,执行 plugin 的部署任务,构建成功后,本地会出现一个repo目录,就是我们自定义的插件。


333.png


我们测试一下demo.asm.lifecycle。


首先在项目根目录的build.gradle文件中添加


buildscript {
ext.kotlin_version = '1.4.32'
repositories {
google()
mavenCentral()
maven { url 'asm_lifecycle_repo' } //需要添加的内容
}
dependencies {
classpath "com.android.tools.build:gradle:3.5.4"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
classpath 'demo.asm.plugin:asm_lifecycle_plugin:1.0.0' //需要添加的内容

}
}

然后在app的build.gradle中添加


id 'demo.asm.lifecycle'

然后我们执行命令./gradlew clean assembleDebug,可以看到hello this is my plugin 正确输出,说明我们自定义的gradle插件可以使用。


444.png


然后我们来自定义transform,来遍历.class文件
这部分功能主要依赖 Transform API。


4.自定义transform


什么是 Transform ?


Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。
创建LifeCycleTransfrom文件,内容如下:


package demo.asm.plugin

import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import joptsimple.internal.Classes

/**
* Transform 主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义 Transform 设置一些遍历规则,
*/

public class LifeCycleTransform extends Transform {

/**
* 设置我们自定义的 Transform 对应的 Task 名称。Gradle 在编译的时候,会将这个名称显示在控制台上。
* 比如:Task :app:transformClassesWithXXXForDebug。
* @return
*/

@Override
String getName() {
return "LifeCycleTransform"
}
/**
* 在项目中会有各种各样格式的文件,通过 getInputType 可以设置 LifeCycleTransform 接收的文件类型,
* 此方法返回的类型是 Set
集合。
* 此方法有俩种取值
* 1.CLASSES:代表只检索 .class 文件;
* 2.RESOURCES:代表检索 java 标准资源文件。
* @return
*/

@Override
Set getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 这个方法规定自定义 Transform 检索的范围,具体有以下几种取值:
* EXTERNAL LIBRARIES 只有外部库
* PROJECT 只有项目内容
* PROJECT LOCAL DEPS 只有项目的本地依赖(本地jar )
* PROVIDED ONLY 只提供本地或远程依赖项
* SUB PROJECTS 只有子项目。
* SUB PROJECTS LOCAL DEPS 只有子项目的本地依赖项(本地jar)。
* TESTED CODE 由当前变量(包括依赖项)测试的代码
* @return
*/

@Override
Set getScopes() {
return TransformManager.PROJECT_ONLY
}
/**
* isIncremental() 表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。
* @return
*/

@Override
boolean isIncremental() {
return false
}
/**
* 最重要的方法,在这个方法中,可以获取到俩个数据的流向
* inputs:inputs 中是传过来的输入流,其中有两种格式,一种是 jar 包格式,一种是 directory(目录格式)。
* outputProvider:outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错。
*
* @param transformInvocation
* @throws TransformException* @throws InterruptedException* @throws IOException
*/

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection tis = transformInvocation.inputs
tis.forEach(ti -> {
ti.directoryInputs.each {
File file = it.file
if (file)
{
file.traverse {
println("find class:" + it.name)
}
}
}
})

}
}

然后将我们将自定义的transform注册到我们定义好的plugin中,LifeCyclePlugin代码修改如下:


package demo.asm.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin {

@Override
void apply(Project target) {
println("hello this is my plugin")
def android = target.extensions.getByType(AppExtension)
println "======register transform ========"
LifeCycleTransform transform = new LifeCycleTransform()
android.registerTransform(transform)

}
}

然后再次执行./gradlew clean assembleDebug,可以看到项目中所有的.class文件都被输出了


555.png


5.使用 ASM,插入字节码到 Activity 文件


ASM 是一套开源框架,其中几个常用的 API 如下:


ClassReader:负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter。


ClassVisitor:负责访问 .class 文件中各个元素,还记得上一课时我们介绍的 .class 文件结构吗?ClassVisitor 就是用来解析这些文件结构的,当解析到某些特定结构时(比如类变量、方法),它会自动调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者修改 .class 文件内容。


ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组。


添加 ASM 依赖
在asm_demo_plugin的build.gradle中添加asm依赖


dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.4'
implementation 'org.ow2.asm:asm:8.0.1'//需要添加的依赖
implementation 'org.ow2.asm:asm-commons:8.0.1'//需要添加的依赖
}

在main/java下面创建包 demo/asm/asm目录并添加如下代码:


import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;


/**
* Created by zhangzhenrui
*/


public class LifeCycleClassVisitor extends ClassVisitor {

private String className = "";
private String superName = "";

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
}


public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("classVisitor methodName" + name + ",supername" + superName);
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (superName.equals("android/support/v7/app/AppCompatActivity")) {
if (name.equals("onCreate")) {
return new LifeCycleMethodVisitor(className, name, mv);
}
}
return mv;
}


public void visitEnd() {
super.visitEnd();
}

public LifeCycleClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
}


import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
* Created by zhangzhenrui
*/


class LifeCycleMethodVisitor extends MethodVisitor {
private String className;
private String methodName;

public LifeCycleMethodVisitor(String className, String methodName, MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
this.className = className;
this.methodName = methodName;
}

public void visitCode() {
super.visitCode();
System.out.println("methodVistor visitorCode");
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(className + "------>" + methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);

}

}

然后修改LifeCycleTransformtransform函数如下:


@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection transformInputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
transformInputs.each { TransformInput transformInput ->
transformInput.directoryInputs.each { DirectoryInput directoryInput ->
File file = directoryInput.file
if (file)
{
file.traverse(type: FileType.FILES, namefilter: ~/.*.class/) { File item ->
ClassReader classReader = new ClassReader(item.bytes)
if (classReader.itemCount != 0) {
System.out.println("find class:" + item.name + "classReader.length:" + classReader.getItemCount())
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new LifeCycleClassVisitor(classWriter)
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
byte[] bytes = classWriter.toByteArray()
FileOutputStream outputStream = new FileOutputStream(item.path)
outputStream.write(bytes)
outputStream.close()
}
}
}
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}

}
}

重新部署我们的插件后,重新运行主项目,可以看到:


MainActivity------>onCreate

但是我们没有在MainActivity中写一行代码,这样就实现了动态注入日志的功能


5.总结


本篇文章主要讲述了在Android中使用asm动态操作字节码的流程,其中涉及到的技术点有



  • 自定义gradle插件

  • transform的使用

  • asm的使用

收起阅读 »

swift 苹果登录

iOS
- 苹果登录的前期工作: - 1.开发者账号中增加苹果登录的选项- 2.xcode中配置苹果登录 //swift版本的代码逻辑 //头文件 import AuthenticationServices //按钮加载 苹果登录 对于按钮有一定的要求,具体查看...
继续阅读 »
苹果登录
项目中继承第三方登录时,需增加上苹果登录即可上架
苹果登录需要iOS系统 13以上支持
详细的内容阅读苹果官方的网址
url:https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple
- 苹果登录的前期工作:
- 1.开发者账号中增加苹果登录的选项


1.1  可能会造成证书无法使用,重新编辑一下保存下载即可!


- 2.xcode中配置苹果登录


前期的配置基本上完成
剩下的就是代码逻辑
- 3.代码中增加苹果登录的逻辑
//swift版本的代码逻辑
//头文件
import AuthenticationServices

//按钮加载 苹果登录 对于按钮有一定的要求,具体查看上方的连接
// 此处使用了一个临时的
if #available(iOS 13.0, *) {
let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.frame = CGRect(x: (KScreenWidth - 300) / 2, y: kScreenHeight - 50, width: 300, height: 30)
authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
self.view.addSubview(authorizationButton)
} else {
// Fallback on earlier versions
}


//MARK: 点击苹果登陆按钮
@objc
func handleAuthorizationAppleIDButtonPress() {

if #available(iOS 13.0, *) {
/**
- 点击 苹果登录的按钮跳出苹果登录的界面
- 跳转出系统界面
*/

let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]

let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self as? ASAuthorizationControllerPresentationContextProviding
authorizationController.performRequests()

} else {
// Fallback on earlier versions
}

}

//MARK: - 授权成功
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if #available(iOS 13.0, *) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
/**
- 首次注册 能够那去到的参数分别是:
1. user
2.state
3.authorizedScopes
4.authorizationCode
5.identityToken
6.email
7.fullName
8.realUserStatus
*/

// Create an account in your system.
let userIdentifier = appleIDCredential.user
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
let code = appleIDCredential.authorizationCode
// For the purpose of this demo app, store the `userIdentifier` in the keychain.
self.saveUserInKeychain(userIdentifier)

// For the purpose of this demo app, show the Apple ID credential information in the `ResultViewController`.
self.showResultViewController(userIdentifier: userIdentifier, fullName: fullName, email: email)
BPLog.lmhInfo("userID:\(userIdentifier),fullName:\(fullName),userEmail:\(email),code:\(code)")
case let passwordCredential as ASPasswordCredential:

// Sign in using an existing iCloud Keychain credential.
let username = passwordCredential.user
let password = passwordCredential.password

// For the purpose of this demo app, show the password credential as an alert.
DispatchQueue.main.async {
self.showPasswordCredentialAlert(username: username, password: password)
}

default:
break
}
} else {
// Fallback on earlier versions
}
}
收起阅读 »

快速掌握 Performance 性能分析:一个真实的优化案例

这么强大的工具肯定是要好好掌握的,今天我们就来做一个性能优化的案例来快速上手 Performance 吧。首先,我们准备这样一段代码:<html lang="en"><head>    <meta charse...
继续阅读 »

Chrome Devtools 的 Performance 工具是性能分析和优化的利器,因为它可以记录每一段代码的耗时,进而分析出性能瓶颈,然后做针对性的优化。

这么强大的工具肯定是要好好掌握的,今天我们就来做一个性能优化的案例来快速上手 Performance 吧。

性能分析

首先,我们准备这样一段代码:


<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>worker performance optimizationtitle>
head>
<body>
   <script>
       function a() {
          b();
      }
       function b() {
           let total = 0;
           for(let i = 0; i< 10*10000*10000; i++) {
               total += i;
          }
           console.log('b:', total);
      }

       a();
   script>
   <script>
       function c() {
           d();
      }
       function d() {
           let total = 0;
           for(let i = 0; i< 1*10000*10000; i++) {
               total += i;
          }
           console.log('c:', total);
      }
       c();
   script>
body>
html>

很明显,两个 script 标签是两个宏任务,第一个宏任务的调用栈是 a、b,第二个宏任务的调用栈是 c、d。

我们用 Performance 来看一下是不是这样:

首先用无痕模式打开 chrome,无痕模式下没有插件,分析性能不会受插件影响。

打开 chrome devtools 的 Performance 面板,点击 reload 按钮,会重新加载页面并开始记录耗时:

过几秒点击结束。

这时候界面就会展示出记录的信息:

图中标出的 Main 就是主线程。

主线程是不断执行 Event Loop 的,可以看到有两个 Task(宏任务),调用栈分别是 a、b 和 c、d,和我们分析的对上了。(当然,还有一些浏览器内部的函数,比如 parseHtml、evaluateScript 等,这些可以忽略)

Performance 工具最重要的是分析主线程的 Event Loop,分析每个 Task 的耗时、调用栈等信息。

当你点击某个宏任务的时候,在下面的面板会显示调用栈的详情(选择 bottom-up 是列表展示, call tree 是树形展示)

每个函数的耗时也都显示在左侧,右侧有源码地址,点击就可以跳到 Sources 对应的代码。

直接展示了每行代码的耗时,太方便了!

工具介绍完了,我们来分析下代码哪里有性能问题。

很明显, b 和 d 两个函数的循环累加耗时太高了。

在 Performance 中也可以看到 Task 被标红了,下面的 summary 面板也显示了 long task 的警告。

有同学可能会问:为什么要优化 long task 呢?

因为渲染和 JS 执行都在主线程,在一个 Event Loop 中,会相互阻塞,如果 JS 有长时间执行的 Task,就会阻塞渲染,导致页面卡顿。所以,性能分析主要的目的是找到 long task,之后消除它。

可能很多同学都不知道,其实网页的渲染也是一个宏任务,所以才会和 JS 执行互相阻塞。关于这一点的证明可以看我前面一篇文章:

通过 Performance 证明,网页的渲染是一个宏任务

找到了要优化的代码,也知道了优化的目标(消除 long task),那么就开始优化吧。

性能优化

我们优化的目标是把两个 long task 中的耗时逻辑(循环累加)给去掉或者拆分成多个 task。

关于拆分 task 这点,可以参考 React 从递归渲染 vdom 转为链表的可打断的渲染 vdom 的优化,也就是 fiber 的架构,它的目的也是为了拆分 long task。

但明显我们这里的逻辑没啥好拆分的,它就是一个大循环。

那么能不能不放在主线程跑,放到其他线程跑呢?浏览器的 web worker 好像就是做耗时计算的性能优化的。

我们来试一下:

封装这样一个函数,传入 url 和数字,函数会创建一个 worker 线程,通过 postMessage 传递 num 过去,并且监听 message 事件来接收返回的数据。

function runWorker(url, num) {
   return new Promise((resolve, reject) => {
       const worker = new Worker(url);
       worker.postMessage(num);
       worker.addEventListener('message', function (evt) {
           resolve(evt.data);
      });
       worker.onerror = reject;
  });
};

然后 b 和 c 函数就可以改成这样了:

function b() {
   runWorker('./worker.js', 10*10000*10000).then(res => {
       console.log('b:', res);
  });
}

耗时逻辑移到了 worker 线程:

addEventListener('message', function(evt) {
   let total = 0;
   let num = evt.data;
   for(let i = 0; i< num; i++) {
       total += i;
  }
   postMessage(total);
});

完美。我们再跑一下试试:

哇,long task 一个都没有了!

然后你还会发现 Main 线程下面多了两个 Worker 线程:

虽然 Worker 还有 long task,但是不重要,毕竟计算量在那,只要主线程没有 long task 就行。

这样,我们通过把计算量拆分到 worker 线程,充分利用了多核 cpu 的能力,解决了主线程的 long task 问题,界面交互会很流畅。

我们再看下 Sources 面板:

对比下之前的:

这优化力度,肉眼可见!

就这样,我们一起完成了一次网页的性能优化,通过 Peformance 分析出 long task,定位到耗时代码,然后通过 worker 拆分计算量进行优化,成功消除了主线程的 long task。

代码传到了 github,感兴趣的可以拉下来用 Performance 工具分析下:

github.com/QuarkGluonP…

总结

Chrome Devtools 的 Performance 工具是网页性能分析的利器,它可以记录一段时间内的代码执行情况,比如 Main 线程的 Event Loop、每个 Event loop 的 Task,每个 Task 的调用栈,每个函数的耗时等,还可以定位到 Sources 中的源码位置。

性能优化的目标就是找到 Task 中的 long task,然后消除它。因为网页的渲染是一个宏任务,和 JS 的宏任务在同一个 Event Loop 中,是相互阻塞的。

我们做了一个真实的优化案例,通过 Performance 分析出了代码中的耗时部分,发现是计算量大导致的,所以我们把计算逻辑拆分到了 worker 线程以充分利用多核 cpu 的并行处理能力,消除了主线程的 long task。

做完这个性能优化的案例之后,是不是觉得 Peformance 工具用起来也不难呢?

其实会分析主线程的 Event Loop,会分析 Task 和 Task 的调用栈,找出 long task,并能定位到耗时的代码,Performance 工具就算是掌握了大部分了,常用的功能也就是这些。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7046805217668497445

收起阅读 »