注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

【Flutter基础】Dart中的并发Isolate

前言 说到 Flutter 中的异步,我想大家都不陌生。一般我们使用 Future、async-await 来进行网络请求、文件读取等异步加载,但要提到 Isolate ,大家就未必能够说的明白了,今天我就带大家了解下 Dart 中的并发 Isolate。 一...
继续阅读 »

前言


说到 Flutter 中的异步,我想大家都不陌生。一般我们使用 Futureasync-await 来进行网络请求、文件读取等异步加载,但要提到 Isolate ,大家就未必能够说的明白了,今天我就带大家了解下 Dart 中的并发 Isolate


一、Isolate的基本用法


1.1 Isolate的基本用法


对于 Isolate ,我们一般通过 Isolate.spawn() 来实现并发处理。


  const String downloadLink = '下载链接';
 final resultPort = ReceivePort();
 await Isolate.spawn(readAndParseJson, [resultPort.sendPort, downloadLink]);
 String fileContent = await resultPort.first as String;
 print('展示文件内容: $fileContent');

Isolate.spawn() 内部传递一个 entryPoint 初始化函数,用于执行异步操作。这里我们定义 readAndParseJson 函数,通过设置 延迟2秒 来模拟文件读取。


Future<void> readAndParseJson(List<dynamic> args) async {
 SendPort resultPort = args[0];
 String fileLink = args[1];

 print('获取下载链接: $fileLink');

 String fileContent = '文件内容';
 await Future.delayed(const Duration(seconds: 2));
 Isolate.exit(resultPort, fileContent);
}

运行结果:


WX20230314-232225@2x.png


1.2 Isolate的异常处理


由于 Isolate 开启的是一块新的隔离区,完全和启动的 Isolate 独立,自然是无法通过 try-catch 进行捕获。


好在 Isolate 提供了异常通知的能力,我们依旧可以通过 ReceivePort 来接收 Isolate 产生的异常,代码如下所示:


  const String downloadLink = '下载链接';
 final resultPort = ReceivePort();

 await Isolate.spawn(
   readAndParseJsonWithErrorHandle,
  [resultPort.sendPort, downloadLink],
   onError: resultPort.sendPort,
   onExit: resultPort.sendPort,
);

 // 获取结果
 final response = await resultPort.first;
 if (response == null) { // 没有消息
   print('没有消息');
} else if (response is List) { // 异常消息
   final errorAsString = response[0]; //异常
   final stackTraceAsString = response[1]; // 堆栈信息
   print('error: $errorAsString \nstackTrace: $stackTraceAsString');
} else { // 正常消息
   print(response);
}

readAndParseJsonWithErrorHandle 函数中,我们通过手动 throw Exception 来触发异常处理。


Future<void> readAndParseJsonWithErrorHandle(List<dynamic> args) async {
 SendPort resultPort = args[0];
 String fileLink = args[1];
 String newLink = '文件链接';

 await Future.delayed(const Duration(seconds: 2));
 throw Exception('下载失败');
 Isolate.exit(resultPort, newLink);
}

运行结果:


WX20230315-212149@2x.png


1.3 Isolate.run()


如果我们每次使用时都需要通过 ReceivePort 来实现 Isolate 的消息通信,这样会过于繁琐。好在官方也考虑到了这个问题,通过提供 Isolate.run() 来直接获取返回值:



注意:该方法需要在 Dart 2.19 以上的版本使用,对应 Flutter 3.7.0 以上。



 const String downloadLink = '下载链接';
String fileContent = await Isolate.run(() => handleReadAndParseJson(downloadLink));
print('展示文件内容: $fileContent');

/// 处理读取并解析文件内容
Future<String> handleReadAndParseJson(String fileLink) async {
 print('获取下载链接: $fileLink');
 String fileContent = '文件内容';
 await Future.delayed(const Duration(seconds: 2));
 return fileContent;
}

其原理就是内部通过对 Isolate.spawn() 进行封装,通过 Completer 来实现 Future 的异步回调。关键代码如下:


    var result = Completer<R>();
   var resultPort = RawReceivePort();
  ...
   try {
     Isolate.spawn(_RemoteRunner._remoteExecute,
             _RemoteRunner<R>(computation, resultPort.sendPort),
             onError: resultPort.sendPort,
             onExit: resultPort.sendPort,
             errorsAreFatal: true,
             debugName: debugName)
        .then<void>((_) {}, onError: (error, stack) {
       // Sending the computation failed asynchronously.
       // Do not expect a response, report the error asynchronously.
       resultPort.close();
       result.completeError(error, stack);
    });
  } on Object {
     // Sending the computation failed synchronously.
     // This is not expected to happen, but if it does,
     // the synchronous error is respected and rethrown synchronously.
     resultPort.close();
     rethrow;
  }


Tip:从官方的源码中我们可以学到,在调用 Isolate.spawn() 时,建议通过 tray-catch 捕获可能发生的异常,并且在最后需要关闭 ReceivePort 避免内存泄漏。



二、Flutter中的compute


除了上述在 Dart 中的用法外,我们还可以在 Flutter 中通过 compute() 来实现。并且这也是官方推荐的用法,因为 compute() 允许在非原生平台 Web 上运行。



官方原文:If you’re using Flutter, consider using Flutter’s compute() function instead of Isolate.run(). The compute function allows your code to work on both native and non-native platforms. Use Isolate.run() when targeting native platforms only for a more ergonomic API.



2.1 compute的使用


Isolate.run() 的使用方式类似,通过传入 callback 函数让 Isolate 执行:


  String content = await compute((link) async {
   print('开始下载: $link');
   await Future.delayed(const Duration(seconds: 2));
   return '下载的内容';
}, '下载链接');
 print('完成下载: $content');

运行结果:


WX20230315-223530@2x.png



Tip:在引入 Flutter 包之前,我们可以直接右键 run 'islate.dart' with Coverage 在Coverage 运行;在引入 Flutter 包之后,我们就需要在手机上运行,可以通过命令:open -a simulator 启动一个 iOS 模拟器运行。



2.2 compute的异常处理


查看 compute() 源码发现,内部使用的是 Isolate.run(),而 Isolate.run() 内部是通过 Completer 来完成异步回调的。因此,我们直接通过 try-catch 即可捕获异常:


  try {
   await compute((link) async {
     await Future.delayed(const Duration(seconds: 2));
     throw Exception('下载失败');
  }, '下载链接');
} catch (e) {
   print('error: $e');
}
 print('结束');

运行结果:


WX20230315-225157@2x.png


2.3 compute的源码分析


compute() 实际是对 isolates.compute 的实例化:


const ComputeImpl compute = isolates.compute;

Isolates 却是通过不同的平台来指定引入的类,这样也印证了为什么官方推荐在 Flutter 中使用 compute()


import '_isolates_io.dart'
 if (dart.library.js_util) '_isolates_web.dart' as isolates;

_isolates_io.dart 中是通过 Isolate.run() 来实现:


Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, {String? debugLabel}) async {
 debugLabel ??= kReleaseMode ? 'compute' : callback.toString();

 return Isolate.run<R>(() {
   return callback(message);
}, debugName: debugLabel);
}

_isolates_web.dart 中是通过 await null; 抽取单帧来执行函数:


Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
 // To avoid blocking the UI immediately for an expensive function call, we
 // pump a single frame to allow the framework to complete the current set
 // of work.
 await null;
 return callback(message);
}

2.4 compute小结



  1. 因为 compute() 需要引入 flutter/foundation.dart,所以只能在 Flutter 中运行。

  2. 在 Flutter 中推荐使用 compute() 来实现,因为兼容 Web 平台。

  3. 其内部实现:在平台侧通过 Isolate.run(),在 Web 侧通过 await null; 抽取单帧来执行函数。


三、Isolate 的工作原理


在 Dart 中,Isolate 是一种类似于线程的概念,可以独立于其他 Isolate 运行,并且具有自己的堆栈和内存空间。这使得 Isolate 可以并行执行代码,并且不会受到其他 Isolate 的影响。


3.1 Future 为何还是会导致卡顿?


有时候我们可能会困惑,为什么明明已经使用了 Future 来异步执行任务,还是会出现卡顿的现象。那是因为 Dart 是单线程的,如果在执行 Future 时遇到耗时的计算任务或者 I/O操作,这些操作会占用当前线程的资源,从而导致应用出现卡顿现象,影响用户体验。


相比之下,Isolate 可以实现多线程并发执行任务,可以利用多核 CPU,因此可以更有效地处理大规模的计算密集型任务、I/O 密集型任务以及处理需要大量计算的算法等。在 Isolate 中执行任务不会占用 UI 线程的资源,从而可以保证应用的流畅性和响应速度。


3.2 Isolate 的工作原理


Isolate 的工作原理是通过使用 Dart 的隔离机制来实现的。每个 Isolate 都运行在独立的隔离环境中,并且与其他 Isolate 共享代码的副本。这意味着Isolate之间不能直接共享数据,而必须使用消息传递机制来进行通信。


其实我们在执行 main() 时,就开始了主 Isolate 的运行,如下图所示:


basics-main-isolate.png


3.3 Isolate 的生命周期


Isolate的生命周期可以分为三个阶段:创建、运行和终止。



  1. 创建阶段:使用 Isolate.spawn() 方法可以创建一个新的 Isolate,并且将一个函数作为参数传递给这个方法。这个函数将作为新的 Isolate 的入口点,也就是 Isolate 启动时第一个执行的函数。创建 Isolate 时还可以指定其他参数,例如 Isolate 的名称、是否共享代码等等。

  2. 运行阶段:一旦创建了 Isolate,它就会开始执行入口点函数,并且进入事件循环。在事件循环中,Isolate 会不断地从消息队列中获取消息,并且根据消息的类型执行相应的代码。Isolate 可以同时执行多个任务,并且可以通过消息传递机制来协调这些任务的执行顺序。

  3. 终止阶段:当 Isolate 完成了它的任务,或者由于某些原因需要停止时,可以调用 Isolate.kill() 方法来终止 Isolate。此时,Isolate 会立即停止执行,并且 Isolate 对象和所有与它相关的资源都会被释放。


basics-isolate.png


3.4 Isolate 组


Dart 2.15 也就是 Flutter 2.8 版本之后,当一个 Isolate 调用了 Isolate.spawn(),两个 Isolate 将拥有同样的执行代码,并归入同一个 Isolate 组 中。Isolate 组会带来性能优化,例如新的 Isolate 会运行由 Isolate 组持有的代码,即共享代码调用。同时,Isolate.exit() 仅在对应的 Isolate 属于同一组时有效。


其原理是同一个 Isolate 组中的 Isolate 共享同一个堆,避免了对象的重复拷贝。这意味着生成一个新 Isolate 的速度提高了 100 倍,消耗的内存减少了 10-100 倍。



注意不要和前面的概念混淆,Isolate 仍然无法彼此共享内存,仍然需要消息传递。



四、使用场景


4.1 使用原则



  • 如果一段代码不会被中断,那么就直接使用正常的同步执行就行。

  • 如果代码段可以独立运行而不会影响应用程序的流畅性,建议使用 Future

  • 如果繁重的处理可能要花一些时间才能完成,而且会影响应用程序的流畅性,建议使用 Isolate


4.2 耗时衡量


通过原则来判断可能过于抽象,我们可以用耗时来衡量:



  • 对于耗时不超过 16ms 的操作推荐使用 Future

  • 对于耗时超过 16ms 以上的操作推荐使用 Isolate


至于为什么用 16ms 作为衡量呢,因为屏幕一帧的刷新间隔就是 16ms



compute API文档原文:


/// {@template flutter.foundation.compute.usecase} /// This is useful for operations that take longer than a few milliseconds, and /// which would therefore risk skipping frames. For tasks that will only take a /// few milliseconds, consider [SchedulerBinding.scheduleTask] instead. /// {@endtemplate}



五、结语


至此,我们完成了对 Isolate 概念和用法的认识。项目源码:Fitem/flutter_article


如果觉得这篇文章对你有所帮助的话,不要忘了一键三连哦,大家的点赞是我更新的动力🥰。最后祝大家周末愉快~



参考资料:


Isolate 的工作原理


Isolates in Flutter


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

Android应用被抓包?防护手段需知道

为了提高网络数据传输的安全性,业内采用HTTPS的方式取代原来的HTTP,Android的应用开发也不例外,我们似乎只需要修改一下域名就能完成http到https的切换,无需做其他额外的操作,那么这个HTTPS是如何实现的?是否真的就安全了?在不同的Andro...
继续阅读 »

为了提高网络数据传输的安全性,业内采用HTTPS的方式取代原来的HTTP,Android的应用开发也不例外,我们似乎只需要修改一下域名就能完成http到https的切换,无需做其他额外的操作,那么这个HTTPS是如何实现的?是否真的就安全了?在不同的Android版本上是否有差异?今天我们就来详细研究一下以上的问题。


Tips:本篇旨在讨论HTTPS传输的安全性,应用本地安全不在讨论范畴。


HTTPS原理



诞生背景



首先就是老生常谈的问题,什么是HTTPS,相信大家有有所了解,这里简单提一下:


由于HTTP协议(HyperText Transfer Protocol,超文本传输协议)中,传输的内容是明文的,请求一旦被劫持,内容就会完全暴露,劫持者可以对其进行窃取或篡改,因此这种数据的传输方式存在极大的安全隐患。


因此,在基于HTTP协议的基础上对传输内容进行加密的HTTPS协议(HyperText Transfer Protocol over Secure Socket Layer)便诞生了,这样即使传输的内容被劫持,由于数据是加密的,劫持者没有对应的密钥也很难对内容进行破解,从而提高的传输的安全性。



密钥协商



既然要对传输的内容进行加密,那就要约定好加密的方式与密钥管理。首先在加密方式的选择上,有对称加密非对称加密两种,这两种方式各有有缺。


对称加密:


加密和解密使用相同的密钥,这种效率比较高,但是存在密钥维护的问题。如果密钥通过请求动态下发,会有泄漏的风险。如果密钥存放到Client端,那么密钥变更时就要重新发版更新密钥,而且如果要请求多个服务器就要维护多个密钥,对于服务器端也是同理,这种密钥的维护成本极高。


非对称加密:


加密和解密使用不同的密钥,即公钥与私钥,私钥存放在Server端,不对外公开,公钥是对外公开的,这样无论是公钥打包进Client端还是由Server端动态下发,也无需担心泄漏的问题。但是这种加密方式效率较低。


HTTPS协议中,结合了对称加密和非对称加密两种方式,取其精华,弃其糟粕,发挥了两者各自的优势。


假设目前Server端有一对密钥,公钥A和私钥A,在Client端发起请求时,Server端下发公钥A给Client端,Client端生成一个会话密钥B,并使用公钥A对会话密钥B进行加密传到Server端,Server端使用私钥A进行解密得到会话密钥B,这时Client端和Server端完成了密钥协商工作,之后Client和和Server端交互的数据都使用会话密钥B进行对称加解密。在密钥协商过程中,就算被劫持,由于劫持者没有私钥A,也无法获取协商的会话密钥B,因此保证了数据传输的安全性。


密钥协商过程简图如下:


密钥协商简图.png



CA证书



上面的过程貌似解决了数据传输的安全问题,但依然有一个漏洞,就是如果劫持者篡改了Server端下发给Client端的公钥的情况。


中间人攻击(MITM攻击)简图如下:


中间人攻击简图.png


为了解决Client端对Server端下发公钥的信任问题,出现了一个被称作CA(Certificate Authority)的机构。


CA机构同样拥有采用非对称加密的公钥和私钥,公钥加上一些其他的信息(组织单位、颁发时间、过期时间等)信息被制作成一个cer/pem/crt等格式的文件,被称作证书,这些CA机构用来给其他组织单位签发证书的证书叫做根证书,根证书一般都会被预装在我们的设备中,被无条件信任


以Android设备为例,我们可以在设置 -> 安全 -> 更多安全设置 -> 加密与凭据 -> 信任的凭据中查看当前设备所有的预装的证书。


设备预装的证书.jpeg


如果Server端部署的证书是正规CA机构签发的证书(CA机构一般不会直接用根证书为企业签发域名证书,而是使用根证书生成的中间证书,一般情况下证书链是三级,根证书-中间证书-企业证书),那么我们在进行HTTPS请求的时候,不需要做其他额外操作,Client端获取到Server端下发的证书会自动与系统预装的证书进行校验,以确认证书是否被篡改。


如果Server端的证书是自签的,则需要在Client端自行处理证书校验规则,否则无法正常完成HTTPS请求。


这也是为什么,我们在Android开发网络请求时,无需做额外操作便能丝滑切换到HTTPS,但是这样真的就能保证网络请求的安全性了吗?


真的安全了吗?


经过上面的介绍我们可以了解到,如果Client端(手机、浏览器)中预装了大量正规CA机构的根证书,Server端如果是正规CA签发的证书,理论上是解决了HTTPS通信中双端的信任问题,但是还存在一个问题,就是这些Client端一般都会支持用户自行安装证书,这将会给Android端的网络安全带来哪些风险?接下来我们就继续来聊聊。


由于Android版本更新迭代较快,且不同版本之前差异较大,因此分析这个问题的时候一定要基于一个特定的系统版本,区别分析。Android 5.0(21)之前的版本太过古老,这里就不再进行分析,直接分析5.0之后的版本。


在一个只采用默认配置的的测试项目中进行HTTPS请求的抓包测试,发现在5.0(包括)到7.0(不包括)之间的版本,可以通过中间人或VPN的方式进行抓包,而7.0及以上版本则无法正常抓包,抓包情况如下



7.0以下手机代理抓包情况:



Android7.0以下.jpeg



7.0及以上手机代理抓包情况:



之所以7.0是个分水岭,是因为在Android7.0之前,系统除了对系统内置CA证书无条件信任外,对用户手动安装的CA证书也无条件信任了。


虽然说7.0及以上的设备不再信用用户自行添加的CA证书,安全性比之前的高很多,但是无门却无法阻止那些抓包的人使用7.0之下的手机,除非提高应用的最小支持版本,但这样就意味着要放弃一些用户,显然也不适用于所有情况。


那么如何在保证低版本兼容性的同时兼顾安全性呢,我们接下来继续探讨。


如何更安全


除了系统默认的安全校验之外,我们也可以通过如下手段来提高请求的安全性,让抓包变得更加困难。



禁用代理



该方式适用于所有Android版本。


在网络请求时,通过调用系统API获取当前网络是否设置了代理,如果设置了就终止请求,达到保护数据安全的目的。因为通过中间人的方式进行抓包,需要把网络请求转发到中间人的代理服务器,如果禁止了代理相当于从源头解决了问题。


优势:设置简单,系统API简单调用即可获取代理状态。


劣势:




  1. 会错杀一些因为其他场景而使用代理的用户,导致这样的用户无法正常使用




  2. 通过开启VPN在VPN上设置代理转发到中间人服务器的方式绕过




由于设置禁用代理的方式很容易被绕过且有可能影响正常开启VPN用户的使用,因此不推荐使用该方式。



数据加密



该方式适用于所有Android版本。


对请求传输的数据进行加密,然后再通过HTTPS协议传输。HTTPS本身在传输过程中会生成一个会话密钥,但是这个密钥可以被抓包获取,如果对传输的数据进行一次加密后再传输,即使被抓包也没法解析出真实的数据。


优势:安全性较高,只要密钥没有泄漏,数据被破获的风险较低


劣势:




  1. 修改同时修改Client端和Server端代码,增加加解密逻辑




  2. 加解密操作影响效率且有密钥维护的成本




在对数据安全性要求比较高的接口上,可以采用这种方式对传输内容进行增强保护。



证书单向认证



该方式适用于所有Android版本。


在默认情况下,HTTPS在握手时,Server端下证书等信息到Client端,Client端校验该证书是否为正规CA机构签发,如果是则通过校验。这里我们可以自定义校验规则,可以下载Server端的证书到打包到APK中,在请求时进行证书校验。


https单向认证.png


优势:安全性高。


劣势:证书容易过期,当前企业证书有效期只有1年,需要每年进行续签,Client需要维护证书更新的问题。



证书双向认证



该方式适用于所有Android版本。


在单向认证中,Client端会验证Server端是否安全,但是Server端并没有对Client进行校验,这里可以让Server端对Client也进行一次认证。这种认证需要在单向认证的基础上再额外创建一套证书B,存放在Client端,并在Client端完成对Server端的校验后,把Client端的公钥证书发送到Server端,由Server端进行校验,校验通过后开始密钥协商等后续步骤。


https双向认证.png


优势:安全性非常高!


劣势:




  1. Server端要存放Client端公钥证书,如果一个Server对应多个Client则需要维护多套




  2. 增加了校验成本,会降低相应速度





网络安全配置文件



该方案为google官方推荐的方案,也是一种证书的单向校验,不过在Android7.0及更高版本上,配置简单,只需要再清单文件的application节点下增加一个networkSecurityConfig项,并指向一个按固定的格式创建一个xml文件,即可完成网络安全的校验,体验相当丝滑,唯一美中不足的是该配置不支持7.0以下的版本。


在7.0及以上版本中,在xml文件夹下创建名为network_security_config_wanandroid的网络安全配置文件:


网络安全文件配置.jpeg


该文件只需要在清单文件application节点的networkSecurityConfig中引用该文件即可,如此就完成了对wanandroid.com域名及其所有次级域名的证书单向认证。


在7.0以下版本中:


由于networkSecurityConfig是7.0版本新增的,因此在所有7.0以下的设备上无法生效,所以针对7.0以下的设备只能通过代码进行认证。推荐使用OkHttp:


okHttp进行证书校验.png


需要注意的是,在通过代码配置指定域名的证书校验时,根域名和次级域名需要分别进行配置。


优势:安全性较高,代码改动少。


劣势:本质还是证书的单向认证。



选择要校验的证书



如果说采取了google推荐的方式进行安全校验,那校验证书链中的哪个证书比较合适呢?


理论上来说,当然是校验企业自己的证书最好,即证书链的第三层企业证书


image.png


但是该层证书的有效期比较短,一般每年都要进行重签,重签之后证书的Sha256就会发生变化,这时候就要及时更新Client端中信息,否则就无法正常完成校验。


为了规避证书频繁过期的问题,我们可以直接对根证书进行校验,一般来说,根证书的有效期是比较长的:


image.png


这样就不用担心证书频繁过期的问题了,但是如果再企业证书续签的时候更换了CA机构,那就必须要更新Client端中的根证书信息了,不过这就是另外的一个问题了。



只校验根证书会不会存在风险?



几乎不会,因为正规的CA机构在给一个企业颁发证书的时候,会有审核机制的,一般不会出现错误办法的状况,但在历史上确实出现过CA机构被骗,将证书颁发给了相应域名之外的人。下面截图来自Google官网:


列入黑名单.png


不过这是非常小概率的事件了,因此校验域名+根证书摘要算是即安全又避免了证书频繁过期的问题,再加上google官方的推荐,算的上是最佳解决方案了。


这篇文章就介绍到这里,感谢观看~~


上号.jpg


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

Android无需权限调起系统相机拍照

在进行一些小型APP的开发,或者是对拍照界面没有自定义要求时,我们可以用调起系统相机的方式快速完成拍照需求 和不需读写权限进行读写操作的方案一样,都是通过Intent启动系统的activity让用户进行操作,系统再将用户操作的结果告诉我们,因为过程对APP是完...
继续阅读 »

在进行一些小型APP的开发,或者是对拍照界面没有自定义要求时,我们可以用调起系统相机的方式快速完成拍照需求


和不需读写权限进行读写操作的方案一样,都是通过Intent启动系统的activity让用户进行操作,系统再将用户操作的结果告诉我们,因为过程对APP是完全透明的,所以不会侵犯用户隐私。


有两种方法可以调起系统相机拍照获取图片,我们先讲比较简单的一种


1、直接获取用户拍照结果

val launcher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) {bitmap->
bitmap ?: return@registerForActivityResult
vm.process(bitmap)
}

launcher.launch("image/*")

这个在旧版本的API中就等于


startActivityForResult(Intent(MediaStore.ACTION_IMAGE_CAPTURE),CODE)

等到用户完成拍照,返回我们的activity时,我们就可以得到一张经过压缩的bitmap。这个方法很简单,它的缺点就是获得的bitmap像素太低了,如果对图片像素有要求的话需要使用第二种方法


2、用户拍照之后指定相机将未压缩的图片存放到我们指定的目录

var uri: Uri? = null

val launcher =
registerForActivityResult(ActivityResultContracts.TakePicture()) {
if(it){
uri?.let { it1 -> vm.process(it1) }
}
}

val picture = File(externalCacheDir?.path, "picture")
picture.mkdirs()
uri = FileProvider.getUriForFile(
this,
"${BuildConfig.APPLICATION_ID}.fileprovider",
File(picture, "cache")
)
launcher.launch(uri)

这里我逐行进行解释:



  1. 首先,我们需要指定拍摄的照片要存到哪,所以我们先指定图片的存放路径为externalCacheDir.path/picture/cache 注意这张图片在文件系统中的名字就叫做cache了(没有文件后缀)。

  2. 然后我们通过FileProvider构建一个有授权的Uri给系统相机,相机程序拿到我们的临时授权,才有权限将文件存放到APP的私有目录。

  3. 系统相机拍照完成之后就会走到回调,如果resultCode为RESULT_OK才说明用户成功拍照并保存图片了。这样我们就能得到一张系统相机拍出来的原图的Uri,这样我们就可以用这张图片去处理业务了。


注意:使用方法二需要用到FileProvider,所以我们还要在AndroidManifest里声明


<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/provider_paths" />
</provider>

@xml/provider_paths是我们授权访问的文件路径,这里我写的是


<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>

关于这个"path.xml",其实还有一些可以补充说明的,后面有空会补上,这里我简单说明一下:


因为我们创建临时文件的时候,文件指定的目录是externalCacheDir?.path,对应的path就是external-cache-path,表示我们要临时授权的目录是externalCacheDir,如果文件目录指定的是其他路径,那path节点也需要改成代表对应文件夹的节点,这样其他应用才能访问到我们APP的私有目录


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

如何看待 AI 和 ChatGPT?

当下互联网和移动互联网的流量已经触顶,虽然偶尔还会出现一些爆款的产品,但是流量总量摆在那里,各个产品之间流量只是此消彼长而已。就互联网技术而言,现在大厂们出的互联网技术,基本是那种花很大力气但是收效并不大。在这种情况下,ChatGPT 或者 AI 会是下一个引...
继续阅读 »

当下互联网和移动互联网的流量已经触顶,虽然偶尔还会出现一些爆款的产品,但是流量总量摆在那里,各个产品之间流量只是此消彼长而已。就互联网技术而言,现在大厂们出的互联网技术,基本是那种花很大力气但是收效并不大。在这种情况下,ChatGPT 或者 AI 会是下一个引爆点吗?


1、ChatGPT


首先,让我们来了解下 ChatGPT。


ChatGPT 刚出来那会儿我就用了几次。当时我还调侃了一下,



客观来说,ChatGPT 的效果还是挺震撼的。尤其是用它来生成一些基本的算法和 Android 代码的时候,


IMG_0856.png


对于我来说,我可能会用它来帮助我做润色文本,修正英语翻译中的错误等一些基础性的工作。


ChatGPT 的问题是你无法保证它的结果总是正确,


IMG_0859.png


有时候还会给人一种说正确的废话的感觉。


尽管 ChatGPT 在回答问题方面已经很强大,但把 ChatGPT 和搜索引擎关联起来简直是误导。搜索引擎和 ChatGPT 的区别在于,搜索引擎给你提供的是参考,而 ChatGPT 直接告诉你了一个可能的答案。搜索引擎需要你自己去甄别,这在一定程度上可以保证你得到的答案的正确性。在搜索引擎的应用场景中,ChatGPT 只适合充当一个助手。另一个问题在于,在产品层面,很难将两者有机的结合在一起。因为两者提供的功能是存在重叠的。


ChatGPT 很火,在我看来起最重要的价值是,它是一种重要的人机交互能力。虽然,几年之前涌现出了大批不同品牌的智能音箱,但是那些智能音箱的“智能”还无法和 ChatGPT 媲美。ChatGPT 可以帮助机器更好地理解人类的语言。ChatGPT 的上下文关联的能力还可以通过对话修正 ChatGPT 的答案。想象一下 ChatGPT 和机器人结合在一起的场景。科幻片中的机器人指日可待。这也是我觉得 ChatGPT 这种人工智能的产品形态最大的潜力。


ChatGPT 只是 AI 应用的一个方面,加上之前的 AlphaGo 等,足以彰显 AI 的巨大潜力。


2、谁将被 AI 取代?


在未来,ChatGPT 以及未来的 AI 产品必将会取代一部分人工,甚至现在都已经有了这个苗头。这可不是危言耸听,
比如当下非常火的 AICG 技术。我们可以用 AI 来生成插画。这将取代一部分插画师的角色。


截屏2023-03-16 23.35.16.png


我们可以用 AI 生成模特效果图(现在已经有公司在研究这个),这可能取代一部分模特的角色。可以说,任何重复性的工作都可以被人工智能取代。


不过以上只是从使用价值来讨论 AI. 如果说从情感价值呢?


就比如说,一只猫和一个 ChatGPT 音箱,谁能给我们提供更多的情感价值呢?我觉得是猫。猫的价值在于不可预测性。它有自己的想法、行为。如果从输入信息来看,AI 输入的是二维平面的,但是猫输入的是三维的空间。所以,在这方面,当下的人工智能还无法和猫相比。


不过,假如有一天,AI 机器人已经足够强大,你会愿意和 AI 机器人谈恋爱吗?这可能是不久我们就要面对的问题。


3、AI 即未来


我之前的文章里也提到过互联网红利将尽。事实上有些头部互联网已经在寻找下一个风口。比如之前火过的 Web3 和元宇宙。


对于 web3,我之前提到过,它本质上并没有新的技术产生,是基于产品逻辑的创新。web3 的本质诉求是用户的数据归属和隐私安全。web3 是一次创新,但还不足以引领下一次变革。对于元宇宙,我个人无法理解为什么要放弃现实生活去追求虚拟的生活,还要求虚拟的世界尽可能接近现实。所以,元宇宙更像是大厂推动起来的一个伪风口。


但 AI 不一样!AI 很有可能会推动下一次产业的革新,而且是彻底的革新。生产方式都可能会由此发生翻天覆地的变化。 判断一个技术是否足以称得上变革,一个评价标准是它是否能够提升社会的生产效率。比如,石器时代是从赤手空拳进化到用石器而提升了效率,此外,还有蒸汽和电气带动的工业革命。从这个层面讲,AI 甚至比互联网能带来更大的变革。


But,AI 可能会带来一系列的社会问题。相比于失业而言,更严重的问题在于,但在一个不够公平的社会里,新的生产方式的变革可能会导致社会贫富的分化更加严重。因为,相对公平的社会里可以通过对 AI 企业征收更多的税,然后把税收用在公共福利上让普通人享受社会发展的红利。而不公平的社会里,普通人不仅无法享受到科技、社会进步带来的福利,还可能会因为 AI 失去工作。


4、参与 AI


最近我也有一个想法“是不是现在不学习 AI 就晚了”。AI 学习需要一定的数学门槛,此外,数据和计算资源也是一个门槛。比如,ChatGTP 用到的大模型就需要大量的数据进行训练,需要调几千个参数。这不是小作坊能够搞得起的。但不妨碍我们学习一些入门的 AI,做一些小的产品。大体上还是要了解一些 AI,不然很容易跟社会脱节。


今天我也看到了百度发布的文心一言。花费很短的时间仓促得地做出来,总给人一种趋之若鹜的感觉。


我也常常想一个问题,是不是任何行业都是外国人发现了一个行业,然后一帮中国人进去死命的卷。


科技的创新并不是靠商业判断规划出来的,只有靠兴趣的推动还需要一些冒险精神才能发现新大陆。另一方面也说明,靠中国式的内卷和低容错的生活节奏是很难孕育出创新的。当然我们可以通过看准了机遇之后快速推进。但,说到底,这种方式在科技进步的道路上只能做一个追随者而不是领导者。


总结


新的时代在呼唤着新的英雄,也许下一个乔布斯已经准备在车库里组装自己的产品了。AI 必将引领一次新的变革,让我们拭目以待。


作者:开发者如是说
来源:juejin.cn/post/7211116982513827900
收起阅读 »

我在字节的这两年

web
前言 作为脉脉和前端技术社区的活跃分子,我比较幸运的有了诸多面试机会并最终一路升级打怪如愿来到了这里。正式入职时间为2021年1月4日,也就是元旦后的第一个工作日。对于这一天,我印象深刻。踩着2020年的尾巴接到offer,属实是过了一个快乐的元旦。不知不觉已...
继续阅读 »

前言


作为脉脉和前端技术社区的活跃分子,我比较幸运的有了诸多面试机会并最终一路升级打怪如愿来到了这里。正式入职时间为2021年1月4日,也就是元旦后的第一个工作日。对于这一天,我印象深刻。踩着2020年的尾巴接到offer,属实是过了一个快乐的元旦。不知不觉已经两年多了,细细回想起来,更多的是岁月推移,并没有回头看看现在的自己和两年前的自己有什么差别。


决定写文章记录一下还要感谢那个离职前在飞书上和我告别的老哥,他说已经学到了想学的


那我呢?似乎还没有。


和优秀的人做有挑战的事不止是简单的一句话。


在字节停留时间越久,越是能感觉到身边人的优秀,也正是这份优秀推动着我不断前进。


本文将会从思维方式、问题排查、技术思考三个方面以回顾自我成长的视角展开叙述,欢迎阅读。


思维方式


思维方式指的是看待事物的角度、方式和方法。放到工作当中来看,我逐渐摸索出了几个具体的点。


工作优先级


曾很长一段时间里,我在工作上没有刻意区分优先级或者说有优先级但是区分度不是那么明显。这意味着只要不是恰好有紧急事情处理,基本上业务方提过来的合理需求我都会第一时间安排。不论需求大小,也不问紧急程度,都默认当作紧急处理。


诚然,在交付后得到业务方肯定的那一刻是有成就感的。但我逐渐意识到,这真的是有点本末倒置。由于我负责的这部分工作和底层数据相关,可能很多需求直接或间接的都会找到我。事实上,完成对齐过的工作才是我更应该高优做的事,剩下时间用来完成这些零散需求才更为合理。


起初我觉得有些小需求可能就是一两行代码的事,顺手一个分支就带上去了。但仔细想想,这好像引发了蝴蝶效应。一件事仅仅完成是不够的,该有的环节要有。 开发,测试,上线,周知业务方验收。这样一个小流程走下来耗费的时间可不仅仅是一两行代码占用的时间可比。更何况,可能还不止一个零散需求。时不时被打断,自然就会导致原有工作安排非预期delay。


在意识到这个问题后,来自业务方的需求我会主动问一下优先级。如果不是特别紧急的事情将不会安排在当前周期的工作计划里。此外,优先级判定上我会和业务方确认完使用场景后有自己的思考。对接次数多了,发现有些紧急并不是真的紧急,只是单纯的性子急。后来,对于这种零散需求,我会在项目管理平台写好描述和需求提出人,方便后续沟通。


这个记录还是很有意义的,深感好处明显。



  • 可以起到一个备忘录的作用,定期查看,提醒自己有todo要处理

  • 业务方(需求提出人)可能因业务场景变更或有了其他解决方案,不再需要后续支持

  • 原业务方(需求提出人)转岗或离职,不再需要后续支持


等到决定去做的时候,如果发现时间间隔较久,不要急着写代码,先和业务方二次确认这个需求是否有必要继续做。试想,如果耗时耗力做完,最后邀请业务方验收时候对方又反馈用不到了。什么心情?那肯定满脸黑人问号啊?实惨如我,曾有过这样的经历。深感前置确认真的很有必要,这样能有效避免打黑工的场景。


在有意识对工作优先级进行划分后,原定对齐的工作进展基本都可以得到保障。等到工作周期结束进行总结的时候,看到比较高的完成度,我觉得这份成就感更高。


ROI考量


ROI 全称为 Return On Investment,指的是投资回报率。我是在完成一个比较重要的功能模块迁移后才更加认识到这个东西的重要性。在做数据迁移的时候,我写脚本进行的全量迁移。为了兼容新旧平台的格式差异,我做了好几处的格式转换,过程中还遇到好几个bad case需要手动处理,总之并不是那么顺利。等到一切准备就绪,我开始拉群周知用户并以表格形式逐个进行使用情况的回访。结果很尴尬,实际使用的用户远低于历史存量用户。量少到我完全可以采用更快的手动迁移,省去做格式转换和写脚本的时间。


对于那些实际没人用的数据,我后来又进行了删除处理。这一波操作下来,真的投入产出比就不高了。算是吃一堑长一智吧,在对一个功能模块进行迁移的时候,前置工作除了搞清楚历史背景,实现原理,更应该确定实际使用人群。尤其是对于一个存在年头比我入职时间还久的功能,更应该花时间在这个点上好好调研下。确定目标人群才好"对症下药",这样才有可能是多人的狂欢而非仅仅是一个人单纯完成迁移工作的孤独玩耍。


有心和无意真的是两种不同的感觉。 实际上,在经历这个事情之前我对自己研发的模块也会有很多想法。有较长一段时间里,我脑海中冒出来的小想法会连同某个分支功能带上去,改动不大,但是可能要思考的点会比较多。现在回想起来,大多数属于ROI比较低的。而现在,不论是业务方提出的需求还是我自己的小想法我都会优先考虑ROI的问题。时间是很宝贵的,在有限时间内产生更高价值带来的成就感和自我认同感绝对是翻倍的。


技术与业务关联


在来字节前,我很喜欢花大把的时间去钻研一些自己喜欢但可能实际未必会用到或者说使用场景比较局限的东西。比如我曾跟着视频教程鼓捣过一段时间的Angular 1.x 。当时觉得ng-xx这个指令写起来倍感新奇,有种发现新大陆的小激动。也曾跟风学过一段时间的php,被其数量庞大的内置函数所震惊。等转回到业务上,发现花费大量时间研究的东西和业务根本不沾边或者说没必要为了尝试而去强切技术栈。如此一来,割裂就产生了。我曾好长一段时间困在这个技术和业务二选一的局面走不出来。


等入职字节并工作了一段时间后,我发现当业务形态开始变得复杂,对技术的考验也会随之而来善于运用技术恰到好处地解决业务痛点,远远比单纯研究技术有意义。 自嗨终究是自嗨,没有实际落地场景,过一段时间就会忘记。如果还没想清楚技术服务于业务这个关键点,那就会陷入【钻研技术->长久不用->遗忘->钻研技术】这个循环。保持技术热情是好事,但是对于一个几乎没有业务落地场景的技术,投入大把时间研究又有什么用呢?知识是检索的,当需要时自然会朝着这个方向靠近,有具体落地场景才能更好地巩固。


进一步让我体会到技术与业务是相辅相成的契机是对图数据库bytegraph的相关技术调研和最终的投入使用。业务场景需要,我这边会涉及不同类型数据之间关联关系的管理(CRUD操作)。这个关联有层级的概念,全部关联建立数据量已到千万级别。从设计角度和实践角度综合考量,已经不是MySQL擅长的场景。细想一下,层层关联铺开不就是一张图吗?自然是图数据库存储更为合适。


在我看完bytegraph相关文档并使用Gremlin图数据库语言写了几个符合自我预期的基础语句后,突然又找回了曾经独自钻研技术的快乐。在使用过程中,很自然的就和业务关联起来了。比如如何设计点和边?如何提高关联图查询速度?我曾写过一篇关于图数据库bytegraph介绍和基本使用的文档,有同学在看过后就着某个具体业务场景下点该如何设计这个话题和我进行了语音交流,最后我结合实际使用场景给出了有效结论,被肯定的瞬间同样是成就感满满。此外,在工作中对bytegraph的使用诉求,还推动了bytegraph NodeJS SDK 的诞生。有幸成为第一个吃螃蟹的人,真的很有纪念意义。


寻求长期方案


很多时候,解决问题的方案都不止一个。绝大多数情况下,选择临时解决方案是最快最省力的。当然,也不排除某些极限情况下足够的临时趋近于长久。但临时终归是临时,这意味着中后期规划可能会有变更,从而导致现有的方案不再适用,所以说寻求长期稳定的解决方案才是最终目的。尤其是当系统稳定性和切换成本冲突时,更应攻坚克难去破局。近期完成了权限平台相关接口的升级替换,由于历史包袱沉重,旧的权限接口越来越不稳定,已经影响平台侧权限的正常使用。在这种情况下,真的是不得不换。好处还是很明显的,虽然过程艰难,但稳定性上确实得到了保障。


相信字节内很多平台都是对权限系统强依赖的,这意味着一旦权限系统服务出了问题,其他的下游服务都会受牵连。这种权限问题感知相当明显,最简单的一个例子:为什么自己创建的东西在操作时提示没权限?


为了降低权限系统不可用对自身业务的影响,我用redis对所有涉及权限读数据的地方做了缓存(如用户权限列表)。每次刷新页面会在获取用户信息的同时查询最新的权限信息,当检测到返回结构非预期时,则不再更新,直接返回缓存数据。一般来说,读权限场景比写权限场景更多,有这样一层缓存来兜底,还是很有价值的。


此外,为了避免自己创建的东西在操作时提示没权限的尴尬局面,我进行了业务自身数据库优先权限系统接口查询的处理。这个很好理解,写到自己数据库再读取往往比写到权限系统数据库再读取来的方便,后者可能会有延迟。完成整体权限系统接口升级替换,再结合redis缓存,数据库优先权限系统接口读取这两个策略,在业务侧整体权限稳定性上可以看作是一个长期稳定的方案了。


直面问题


对于一个开发来说,出现问题在所难免。解决问题固然重要,但是摆正心态也同样重要。工作中基本都是多人协作开发,当收到线上报警消息时,如果能确定和自己的某些操作有关应及时和相关同学说明,避免其他人一同跟着排查。有句话听起来很矛盾,但是语境还挺合适的:"我知道你很慌,但是先别慌。" 出现问题,排查清楚后,及时修复就好,切莫讳疾忌医。


此外,有些问题隐藏比较深,复现链路较为隐晦,甚至可能除了开发自身,其他人几乎不会有感知。我曾遇到过一个这样的case,代码写完过了一年,也没有人反馈,最后还是我自己在某次调试时候发现并修复的。随着编码经验的积累,思维发散性也会更广,不同阶段考虑的点自然也有差异。没必要过多纠结当时为什么没有考虑到这个场景,更应该思量的是下次遇到类似情况如何避免。亡羊补牢,为时未晚。


问题排查


问题排查可以说是一个开发人员必备的能力。个人感觉保证开发永远不出bug的方式就是不去开发。当然,这并不现实。在字节这两年多的时间里,我踩过好多的坑,也出过事故,逐渐摸索出了一些问题排查的经验。


环境一致性校验


工作中我这边常用到的是本地环境、测试环境(boe),生产预览环境(ppe)和正式生产环境(prod)。每个阶段都有可能会引发问题,在开始排查问题前,需要先确定自己的调试环境与引发问题的环境一致。乍一看可能感觉这句话是废话,但是有过相关经验的人都知道这一条真的很重要。


说来惭愧,我有过本地调试半天发现死活不生效最后意识到看的是生产环境页面的尴尬经历,真的是又气又无奈。


优先保证这一点,能少走很多弯路。


格式一致性校验


格式一致性校验指的是确认原始数据在有意格式处理或漏处理后,是否和后续程序要接收的数据格式保持一致。


一般来说,编码粗心或者测试不够充分都有可能引发格式相关的问题。


有意处理的场景:


const list=[1,2,3]
// 有意处理
const formatList =list.map(d=>({
id:d
}))
// 省略一大段代码

// 此处错误传入了list,应使用formatList
getData(list)

function getData(list){
// do something...
return xxx
}

在前端操纵数据store也有可能存在类似的问题,原始数据格式在某个组件里被修改导致另一个组件无法预期解析。


漏处理的场景:


// sequelize findAll查询 限定只返回id属性
const ids = await modelA.findAll({
attributes: ['id'],
});

await modelB.findAll({
where: {
id: ids,//这里漏掉了对ids的处理
},
});

如图,使用了sequelize model方法中的findAll查询并限定只返回id属性,且变量命名为ids。


实际上,返回的结构是对象数组{id:number}[],而不是数字数组number[]。


请求响应一致性校验


服务里定义的路由地址和前端请求时的地址对不上,导致请求404。


可能是因为单词拼写错误:username or ursename? cornjob or cronjob? 或者cv后没有改全。


前置条件确认


这个偏向于涉及事件触发的场景,要先满足其前置条件。


下面列举几个有代表性的场景:



  1. 如果想在群里接收某个机器人推送的消息,需要先把机器人拉进群

  2. 如果想在eventbus消费生产者产生的数据,需要确保消费者是开启状态

  3. 如果想使用sdk正常解析hive数据,需要先申请表权限


分区间排查


这种方式适用于排查由程序代码引起但尚不确定具体代码位置的场景。


我将其划分为三段式:



  1. 给怀疑会出问题的代码圈定一个区间,非怀疑区间代码直接注释(前端更有效)或return掉(后端更有效)

  2. 添加相关打印并重新运行程序,观测输出和程序运行结果是否符合预期

  3. 收缩区间,重复1,2步骤,直至发现问题




这里举一个我在使用bytegraph过程中亲身遇到的一个cpu暴涨的例子。


最初bytegraph并不支持全图查询,所以在获取某个点所在的整张关联图谱时拆分成了以下三个步骤:



  1. 查询某个点在整张图上的关联点

  2. 遍历每个点,查询入边和出边

  3. 根据边的指向拼出完整的图谱


伪代码如下:


function getGraph(vertex:Vertex){
// 查询某个点在整张图上的关联点
const nodes=await getNodes(vertex);
console.log('get nodes')
// return 分割区间一,后续直接return
// 遍历每个点,查询入边和出边。
const edges=await getEdges(nodes)
console.log('get edges')
// return 分割区间二,后续直接return
// ... other
}

async function getEdges(vertexs: Vertex[]) {
let res: any = [];
for (let i = 0; i < vertexs.length; i++) {
const vertex = vertexs[i];
// 根据点查询入边和出边
const itemEdges=await findEdge(vertex);
res = [ ... res, ... itemEdges];
}
// return res 分割区间三,不执行uniqWith返回res
// 深度去重
return uniqWith(res, isEqual);
}

采用分区间排查问题的思路,在关键节点添加打印日志,触发调试。


查看打印信息,发现每次都是在获取所有边那里卡住。


此时可以进到getEdges里边查看,发现内部有一个去重操作。


试着去掉这个过程,再重试,问题未复现。ok,定位问题。




针对这个问题,我写了一个可复现的最小demo,感兴趣的可自行尝试。


结论是lodash的uniqWith和isEqual方法对大数据 重复率不高的数据进行深度去重会导致cpu暴涨。


const { uniqWith, isEqual } = require('lodash');
const http = require('http');
http
.createServer(async (req, res) => {
const arr = [];
for (let i = 0; i < 10000; i++) {
arr.push({
n: Math.random() * 20000,
m: Math.random() * 20000,
});
}
console.log(uniqWith(arr, isEqual));
res.end('hello world');
})
.listen(3000);

请求溯源


对于有提供Open API 给其他业务方使用或者说当前服务存在开放性接口(未设置权限)的情况下,都有可能存在非预期调用,其中最典型的是参数错误和session信息缺失。


我有过类似经历,某个已经线上稳定运行过一段时间的接口突然开始报错,从错误信息来看是参数错误。随后我仔细查找了代码里的调用点,只有可能在平台使用时触发。进一步查看,确认是开放性接口,没有权限管控。意识到应该是某个用户手动触发的,因为平台侧正常使用的请求参数符合预期。如果能定位到具体的人自然最好,如果找不到人就需要在代码层面做一个参数校验,如果传递过来的参数不符合预期,直接return掉。类似的,平台侧调用一定可以拿到session信息,但是接连几次报错都是拿不到session导致的,怀疑是非常规调用,直接return。


安全日志记录


我负责的工作中涉及很多底层数据,这些数据属性变更有可能会引发非预期的安全卡点。开启卡点的资产越多,类似问题感知就会越明显。内部定时任务,外部平台配置变更,扫描任务,人工变更都可以导致资产属性发生变化。因此,究竟是哪一环节发生的变更显得尤为重要,这能有效缩短问题排查链路。


通过在每个变更节点添加一条安全日志记录,可以有效辅助排查。此外,还可以作为业务方溯源的一个途径。比如解答某个资产卡点什么时候开启的?卡点开启同步自哪个部门?


审查数据库字段


在某些业务场景里会在数据库中存储JSON 字符串,此时需要对实际可能的JSON大小做一个预判,之后再设定与之匹配的字段类型和数据大小。否则当实际长度超过数据库设定字段长度时,JSON字符串就会被截断,导致最后的解析环节出错。


超时归因


开发中遇到网络超时问题太常见了,大多数情况下都可以通过添加重试机制,延长timeout的方式解决。这里我想说的是一个比较特别的场景,海外,国内跨机房通信。 绝大多数海外和国内的通信都是存在区域隔离的,调用不通表现上可能就是网络超时,这种情况下,重试也没用。解决途径也比较直观,要么直接避免这种情况,海外调海外,国内调国内,要么申请豁免。


善用工具


argos观测诊断平台


在问题排查上,观测诊断平台能起到有效的辅助作用。除了报错日志,还可以看到所在服务psm,集群,机房。这些都是缩短问题排查链路的有效信息,在服务实例比较多的情况下表现尤为明显。此外,还可以配置报警规则,命中后会有报警机器人进行推送,可及时感知线上问题的发生。


飞书机器人


真心觉得飞书机器人是一个很好用的小东西。用它可以干很多事,比如按时提醒该喝水了。在报警感知上,也可以通过机器人搞点事情。例如在某个装饰器里对核心接口请求地址(如包含/core/)进行识别,随后在catch代码块里捕获错误,最后将error message or error stack 推送到指定的飞书群里,这样团队其他成员也能及时感知。


飞书表格


个人精力有限,不可能时时刻刻盯着报警信息其他什么都不干。对于一些看起来影响不大,不用紧急修复的报警可以先通过飞书表格记录下来,等有时间后当成待办事项逐一解决。亲测,这种先收集后集中处理的方式比发现一个处理一个更省时间。


技术思考


规范


很长一段时间里我对技术的理解是运用掌握的知识完成开发,仅此而已。但事实上,开发流程不应仅局限于开发环节,还有其他很多有价值的事情需要关注,比如一些规范。团队协作和独立开发还是有明显区别的,没有规矩不成方圆。既然是协作,就要有达成一致的规范。


我曾写过一篇关于lint的文章并在小组内和其他同事对齐,共同商讨缩进风格,哪些规则要开启,哪些规则要禁用。项目编码风格统一的管控实现上依赖husky和lint-staged,在提交代码时进行lint检测,不符合检测规则无法提交,这样可以有效避免个人编码风格差异导致的格式change。


在代码提交上,由组内另一个同学制定了git工作流规范,共同约定了不同功能分支如何命名,分支间如何检出与合并,commit 应该如何编写。这种规范形成文档后作用明显,不论是日常开发还是线上部署,都有了更清晰的操作流程。此外,见名知意的commit message也更有助于查找具体功能点。试想一下,如果简写一个fix,或fix err ,等过段时间再看,哪里还记得到底fix了个什么?


类似的,小组内还有需求迭代,上线部署等相关规范,这些规范站在开发的全局视角来看,都是很有价值的。


质量


研发质量问题是一个非常值得重视的点,开发完成并不意味着整个研发环节就结束了,质量过关才是最后的收尾节点。简单来说,上线后功能平稳运行,无bug和性能问题,这样才算是合格。虽说百密一疏,但反复踩同样的坑或者踩不应该踩的坑就有些说不过去了。我印象比较深刻的踩坑点在于数据格式处理,这个在上文报警排查处有提到,不再赘述。还有一点,对于跨越大版本的sdk升级,一定要认真且足够详细的审查是否存在break change。有些break change是比较隐晦的,乍一看可能察觉不到玄机,切记想当然,在项目代码中搜索看看,总比自我回忆要可信的多。想要收获一批忠实用户,研发质量一定是排位比较靠前的。


稳定性


这里特指研发的系统稳定性,初期我这边涉及到的系统架构比较简单,所有功能模块共用一个服务。这样好处是很多代码可以复用,开发和上线也比较方便。祸福相依,但是一旦服务崩溃,除了影响自身业务正常使用,还会朝着下游其他业务辐射。具体表现上来看,一般是OEPN API不可用。为避免类似问题再发生,我和小组内其他同事一起完成了服务架构升级,将不同子模块拆分成不同的服务,接口层面根据重要等级和业务类型并借助负载均衡能力,分散至各自所在服务的不同集群。架构升级完成后,即使某个子模块出现问题,也不至于牵动整个服务崩盘。在此次架构升级中更深刻体会到了不同类型数据库在特定场景下的使用,Redis,MySQL,MongoDB,bytegraph都有涉及,收获颇多。


文档先行


对于一些偏复杂的模块,先找个文档梳理一下,逐步拆解清楚后再开始编码,属于磨刀不误砍柴工。以前我的习惯是想一个大概,然后投入开发,写着写着发现之前想错了,然后删掉代码,再写新的,这个过程可能会反复好几次。冷静下来好好想想,真不如先写清楚文档更省时省力。实测,让思维在文档上交锋,远比在编辑器里打架轻松的多。


沉淀总结


我始终觉得,有输入就应该有输出。不论是日常基础搬砖,还是攻坚克难了某个业务痛点,又或者加深了自己对某项技术的理解,都应该有所展现。并不是说非要落笔成文,但至少应该在一个属于自己的小天地里留些痕迹。如果实在懒得打字,不妨试试拍照式记忆。亲测,这个是科学中带有点玄学的方法。


先找到想要记住的画面,可以是控制台的数据打印,也可以是bug调试截图,又或者某段关键代码,然后想一个主题,与之进行关联,重复思考几次。好的,记住了。


还是那句话,有心和无意是不一样的。有心留意,这份记忆就会更为深刻。当下次遇到类似场景,近乎是条件反射的思维反应。比如我现在每次写删除语句一定会检查是否加上了where条件。这是有特殊意义的一段经历,不堪回首。


落地统计


辛辛苦苦搬砖究竟产生了怎样的价值呢?究竟有哪些人在用?这同样是一个比较关键的点。我曾梳理了一个关于OPEN API 业务落地情况的表格,里边记载了哪些业务方在用,什么场景下会用,对接人是谁。这样除了价值考量,还可以在接口变更或下线时及时联系使用方,避免造成非预期的影响。


总结


不知不觉,洋洋洒洒写了几千字,梦回毕业论文。曾觉得自己属于有所成长,但是成长算不上快那种。写完这篇文章后再回首,竟也方方面面很多点。不错,经过一番努力,终于从一棵小葱茁壮成长为一棵参天大葱了。


回到最初的问题上,时至今日,我仍然觉得还有很多东西要学。距离把想学的都学到,大概还有很长一段路要走。


好在这一路不算孤独,能和身边优秀的人一起做有挑战的事。


前方的路,仍然值得期待。


作者:冷月心
来源:juejin.cn/post/7211716002383429693

完结,撒花!

收起阅读 »

30 岁了!通过 AI 问答完成了这篇思考文章

大家好,我是 shixin。 岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。 说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危...
继续阅读 »

大家好,我是 shixin。


岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。


说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危机、生活压力。


无论怎样我终究还是和三十岁相遇了,既然逃不掉,那今天就和它聊一聊。


三十岁意味着什么


我拿着这个问题问了 ChatGPT,它根据我的上下文给的回答如下:




可以看到,它给的回答还是蛮好的,基本上道出了现在困扰我的一些点。


三十岁,工作和生活对我的要求更高了。


工作方面,现在需要考虑的比以前更多了一些。除了个人贡献还需要做团队贡献,为自己的小组和整个团队带来更大价值,把自己知道的技术和经验传播给更多小伙伴。


家庭方面,快到要小孩的时候了。理论上三十岁已经年纪不小应该响应国家号召,但无奈生娃养娃的成本太大,还得多奋斗几年才有底气。今年准备先把婚礼办了(疫情影响婚礼日期改了好几次,上帝保佑这次顺利),过两年再考虑要孩子吧。


至于工作生活的平衡,老实讲目前还没有足够的资本,还得在工作上投入大部分时间。如何解决这种情况呢?是个值得思考的问题。


三十岁前我的人生


三十岁前,我的人生里有很多意想不到


十岁的我,没有想到未来我会去包头,更没有想到会在高中遇到现在的老婆。那时的我在呼和浩特,有四五个很要好的朋友,搬家的时候心里有一万个不舍。


十五岁的我,没有想到我会去西安读书,学的是计算机。那时的我还在想方设法溜到网吧通宵打游戏。


二十岁的我,没有想到我会从事安卓开发,也没有想到会去上海工作。那时的我在盲目瞎学,手机上写 OJ,看小甲鱼和黑马程序员,图书馆借了几本很老的 MFC 和 HTML CSS 书,跟着例子敲出来一个 H5 打飞机游戏。


二十五岁的我,没有想到我会在上海定居。那时我想的是干几年去西安定居,在那里离老家近一点,买房压力也小一点。后来机缘巧合,在买房时和几个前辈朋友聊了聊,听了他们的劝导,改成在上海定居。




ChatGPT 的这段回答让我泪目。有时候打的字越多,越渴望得到认可的回复,这种感觉,它给到了。



三十岁的我,虽然没有 100% 达到五年前预想的目标,但好在完成了一些当时觉得很难的事,比如买房、写书、直播分享,这些事是我成长的见证,也让我沉淀下一些经验和教训。


希望自己可以继续保持的


我希望自己继续保持的第一个点:在损失可以接受的情况下,多尝试多探索。


之前打德扑的时候,我属于比较激进和浪的那种,这种性格的缺点是会浪费很多筹码,但优点是过程很有趣,也常常会博到一些额外的收益。


生活里也是类似,在大学做小生意的时候,我愿意多跑几家店看看有没有价格更合适的货,也愿意多推开一扇门去问一下是否有需求,虽然收到不少白眼、也没赚大钱,但这段经历让我意识到:反正被拒绝也没什么损失,多试一次就多一个机会。


第二个需要继续保持的点:多种善因。


过往人生的关键节点,让我深刻的感受到:当下的果,往往来自过去不经意间种下的因。


就拿今年的几件事来说:



  1. 二月有机会在社区里做分享,缘自去年国庆主动报名 GDE 项目,虽然没通过筛选,但好在建立了联系,有这种机会时人家才会想到我



  1. 上周组里做的 ReactNative 技术培训,缘自字节时做的 Diggo 项目,在其中提升了前端开发技术,以至于后面做 RN 很顺畅,从而走在团队前头


今年很多事都是之前种下的善因结出的果实,除了满足,还需要多想想:



  1. 怎样为以后种下更多善因



  1. 现在要做的事,从长期来看,重复多次后的收益是怎样的



第三个需要继续保持的点:每日、每周、每年必做计划。


每日预则立,不立则废。我是一个善忘的人,如果哪天没有定好计划,基本上就稀里糊涂的过去了。首次发现这个问题,是我写2016 年度总结的时候,回顾发现好多细节都不记得了,有的月份里可能只记得一两件事,剩下的日子都进了黑洞无影无踪。


从那以后我就经常做记录、做计划,既然内存不够用,那就用磁盘缓存。做好每天的计划后,即使被突发事情分了心,我也可以及时调整状态回归高优。在日积月累下,才渐渐地完成了一件件看似很难的事,比如一篇有价值的文章、一个高质量的开源库(github.com/shixinzhang…)。



希望自己可以避免的


除了需要继续保持的,我也有很多后悔的事,比如做错事、说错话、浪费时间。


总结原因后,大概有这几点需要避免:



  1. 避免思想上的懒惰,少说这样的话:没办法、算了、就这样吧;多说:我试试、或许这样做就可以



  1. 避免和他人比较,比别人优秀或者差都不重要,重要的是有没有持续前进



  1. 避免没有进展的时候硬逼自己,多思考方向、方法是不是有问题



  1. 避免花钱的时候只看价钱,不对比购买后的体验和长期区别



  1. 避免做计划的时候过于悲观,目标定高点才可能做的更好



  1. 避免追求完美而不愿意开始,做完比做好优先级更高



  1. 避免在累的时候不休息,贪图享乐而继续浑浑噩噩




  1. 避免骄傲自满、自我膨胀,骄傲一来羞耻就来了




大胆想象一下,三十五岁的我


借用亚马逊的逆向工作法,先想象一下我 35 岁的情况:



  1. 第一种可能:独立开发了某个产品,为细分领域的人提供了独特的价值,从而获得不错的收益,业务比较忙的时候雇佣了几个助手



  1. 第二种可能:继续打工,但因为技术较好、沟通表达能力不错、有商业思维,担任某个业务的技术负责人



  1. 第三种可能:因为工作经验和年纪薪资不匹配被裁,投简历基本没有回复,最后忍痛降薪 50% 接了个 offer


要达到第一种情况,需要具备技术广度,可以独立完成产品的需求调研、设计、全栈开发和运营,更重要的是,尽早捕捉到信息,挖掘出其中的信息不平衡点或者需求点。这种情况对人的要求更高、风险也更高。


要达到第二种情况,需要付出的努力比上面略微少一点,需要具备一定的技术深度和广度、提升对公司业务和行业趋势的了解,主导完成一些有价值的事,同时在公司内部有一定的影响力。这种情况比第一种更稳一点。


要避免第三种情况,需要经常了解市场相关岗位的要求,不断提升自己的技术和业务价值以匹配要求,最好有代表性的作品和影响力。


总结


这篇文章是我三十岁当天开始动笔写的,因为种种原因拖到今天才完成,实在不应该(捂脸哭。


总是听人讲“三十而立”,为了看自己到底立没立,我看了好些名人的视频,想从中寻找答案。



到现在我悟了,所谓的“立“就是建立、确定、稳固。人活着最重要的就是吃饱和开心,三十岁,能够有一技之长和自我融洽的三观,就算是立住了吧!



作者:张拭心
来源:juejin.cn/post/7210386831451357221
收起阅读 »

放弃熬夜,做清晨的霸主🔥

☀️ 前言 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)。 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。 在尝试早起将近一个月的时间后,我...
继续阅读 »

☀️ 前言



  • 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)

  • 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。

  • 在尝试早起将近一个月的时间后,我发现,我的效率确实是有了质的提升,接下来我会根据HeyMatt老师提到的方法和我一些实践来进行说明,感兴趣的小伙伴欢迎收藏慢慢看。


🕐 极致利用晚上时间的错觉



  • 会不会有很多小伙伴会有这种情况,每天辛勤劳作后,到了11点半大脑就会提示你:累了一天了,要不要放松一下呢?视频里说到,这种大脑暗示的放松大体分为三种:

    • 开始刷视频,打个游戏,借助浅层的刺激感来放松

    • 点个宵夜,搞个小烧烤吃吃,借助食物换取特定心境

    • 想一些过往能够牵动情绪的往事,沉浸在起伏连绵的情绪中



  • 绝了,以上三种我都尝试过,全中,但是作为程序员我还会有其他的几种:

    • 学习知识📖

    • 优化代码💼

    • 加快需求进度,赶需求🏃



  • 我经常会有这种想法,如果晚上11点半到1点半我可以把这些事情做完或者做多一点,那么我的时间就会被延长🕐。

  • 错❌,看了这个视频后我真的悟了,我花掉了N个晚上的两个小时,但是换不回来人生相应的发展,甚至很多质量很差的决策、代码都是在这个时间段产出的。

  • 可能你确实在这晚上获得了很多愉悦感,但是这个愉悦感是没有办法持续的第二天又赖床又想逃避,你会去想我白白浪费了晚上两个小时刷剧,过了一个晚上这个愉悦感在你早上醒来的时候会忽然转化为你的焦虑感

  • 确实是这样的,特别是在周末熬夜的时候,你会潜意识的特别晚睡,第二天让睡眠拉满,直接到中午才起床,但其实这样不是浪费了更多的时间吗?


🤔 三个风险



  • HeyMatt老师提到在熬夜的这些时间,面临了至少三个风险。


时间的消耗不可控



  • 就拿我来举例,我前段时间老是想着公司需求怎么做,需求的方案是不是不完整,是不是有可以优化的点,要修复的Bug怎么定位,怎么解决。

  • 我不自觉的就会想,噢我晚上把它给搞定,那么第二天就可以放下心去陪家人出去走走。

  • 可是事实呢?运气好一点或许可以在2个小时解决1点准时睡觉,但是运气不好时,时间会损耗越来越多,2个半小时,3个小时,4个小时,随着时间的消逝,问题没有解决就会越发焦虑,不禁查看时间已经凌晨3-4点了。

  • 就更不用说以前大学的时候玩游戏,想着赢一局就睡觉,结果一晚上都没赢过...😓


精神方面的损耗



  • 当我们消耗了晚上睡眠时间来工作、来学习、来游戏,那么代价就是你第二天会翻倍的疲惫。

  • 你会不自觉的想要睡久一点,因为这样才能弥补你精神的损耗,久而久之你就会养成晚睡晚起的习惯,试问一下自己有多久没有在周末看过清晨的阳光了?

  • 再说回我,当我前一个晚上没有解决问题带着焦虑躺在床上时,我脑子会不自觉全是需求、Bug,这真的不夸张,我真的睡着了都会梦到我在敲代码。这其实就是一种极度焦虑而缺乏休息的大脑能干出来的事情。

  • 我第二天闹钟响了想起我还有事情没做完,就会强迫自己起床,让自己跟**“想休息的大脑”**打架,久而久之这危害可想而知。


健康维度的损耗



  • 随着熬夜次数的增多,年龄的增长,很多可见或不可见的身体预警就会越来越多,具体有什么危害,去问AI吧,它是懂熬夜的。



🔥 做清晨的霸主



  • 那么怎么解决这些问题呢,其实很简单,把晚上11.30后熬夜的时间同等转化到早上即可,比如11.30-1.30,那么就转化到6.30-8.30,这时候就会有同学问了:哎呀小卢,你说的这么简单,就是起不来呀!!

  • 别急,我们都是程序员,最喜欢讲原理了,HeyMatt老师也将原理告诉了我们。


赖床原理



  • 其实我们赖床起不来的很大一部分原因是自己想太多了。

  • 闹钟一响,你会情不自禁去思考,“我真的要现在起床吗?” “我真的需要这一份需要早起的工作吗?” “我起床之后我需要干什么?” “这么起来会不会很累,要不还是再睡一会,反正今天不用上班?”

  • 这时候咱们大脑就处于一种**“睡眠”“清醒”**的重叠状态,就跟叠buffer一样,大脑没有明确的收到指令是要起床还是继续睡。

  • 当我们想得越多,意识就变得越模糊,但是大脑不愿意去思考,大脑无法清晰地识别并执行指令,导致我们又重新躺下了。


练就早起



  • 在一次采访中,美国作家 Jocko Willink 老师提出了一种早起方法::闹钟一响,你的大脑什么都不要想,也不需要去想,更不用去思考,让大脑一片空白,你只需执行动作即可。

  • 而这个动作其实特别简单,就是坐起来--->站起来--->去洗漱,什么都不用想,只用去做就好。

  • 抱着试一试的心态,我尝试了一下这种方法,并在第二天调整了闹钟到 6:30。第二天闹钟一响,直接走进卫生间刷个牙洗个脸,瞬间清醒了,而且我深刻的感觉到我的专注力精神力有着极大的提升,大脑天然的认为现在是正常起床,你是需要去工作和学习👍。

  • 绝了,这个方法真的很牛*,这种方法非常有效,让我觉得起床变得更容易了,推荐大家都去试试,你会回来点赞的。


克服痛苦



  • 是的没错,上面这种办法是会给人带来痛苦的,在起床的那一瞬间你会感觉仿佛整个房间的温度都骤降了下来,然后,你使劲从被窝里钻出来,脚底下着地的瞬间,你感到冰凉刺骨,就像是被一桶冰水泼醒一样。你感到全身的毛孔都瞬间闭合,肌肉僵硬,瑟瑟发抖,好像一股冰冷的气流刺痛着你的皮肤。

  • 但是这种痛苦是锐减的,在三分钟之后你的痛苦指数会从100%锐减到2%

  • 带着这种征服痛苦的快感,会更容易进入清晨的这两小时的写作和工作中。


✌️ 我得到了什么



  • 那么早起后,我收获了什么呢❓❓


更高效的工作时间



  • 早起可以让我在开始工作前有更多的时间来做自己想做的事情,比如锻炼、读书、学习新技能或者提升自己的专业知识等,这些事情可以提高我的效率专注力,让我在工作时间更加高效。

  • 早起可以让我更容易集中精力,因为此时还没有太多事情干扰我的注意力。这意味着我可以更快地完成任务,更少地分心更少地出错


更清晰的思维



  • 早上大脑比较清醒,思维更加清晰,这有助于我更好地思考解决问题,我不用担心我在早上写的需求方案是否模糊,也能更好的做一些决策

  • 此外,早起还可以让我避免上班前匆忙赶路的情况,减少心理上的紧张压力


更多可支配的时间



  • 早起了意味着早上两个最清醒的时间随便我来支配,我可以用半小时运动,再用10分钟喝个咖啡,然后可以做我喜欢做的事情。

  • 可以用来写代码,可以用来写文章,也可以用来运营个人账号

  • 可以让我有更多的时间规划安排工作,制定更好的工作计划时间管理策略,从而提高工作效率减少压力


更好的身体健康



  • 空腹运动对我来说是必须要坚持的一件事情,早起可以让我有更多的时间来锻炼身体,这对程序员来说非常重要,因为长时间的坐着工作容易导致身体不健康

  • 用来爬楼,用来跑步,用来健身环等等等等,随便我支配,根本不用担心下班完了后缺乏运动量。


👋 写在最后



  • 我相信,我坚持了一年后,我绝对可以成为清晨的霸主,你当然也可以。

  • 而且通过早起不思考这个方法,很多在生活有关于拖延的问题都可以用同样的方式解决,学会克服拖延直接去做,在之后就会庆幸自己做出了正确的决定

  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~


🌅 往期精彩


我被骂了,但我学会了如何构造高性能的树状结构🔥
466👍🏻


入职Apifox研发组三个月,我领悟了30个高效开发方法🔥
1218👍🏻


面试官:你觉得你最大的缺点是什么? 433👍🏻


几个一看就会的实用JavaScript优雅小技巧🌟
826👍🏻


作者:快跑啊小卢_
来源:juejin.cn/post/7210762743310417977
收起阅读 »

后端一次给你10万条数据,如何优雅展示,到底考察我什么?

web
前言 大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端) 前置工作 先把前置工作给做好,后...
继续阅读 »

前言


大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)


image.png


前置工作


先把前置工作给做好,后面才能进行测试


后端搭建


新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务



没有安装nodemon的同学可以先全局安装npm i nodemon -g



// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': '*',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
let list = []
let num = 0

// 生成10万条数据的list
for (let i = 0; i < 100000; i++) {
num++
list.push({
src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
text: `我是${num}号嘉宾林三心`,
tid: num
})
}
res.end(JSON.stringify(list));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})

前端页面


先新建一个index.html


// index.html

// 样式
<style>
* {
padding: 0;
margin: 0;
}
#container {
height: 100vh;
overflow: auto;
}
.sunshine {
display: flex;
padding: 10px;
}
img {
width: 150px;
height: 150px;
}
</style>

// html部分
<body>
<div id="container">
</div>
<script src="./index.js"></script>
</body>


然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据


// index.js

// 请求函数
const getList = () => {
return new Promise((resolve, reject) => {
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
resolve(JSON.parse(ajax.responseText))
}
}
})
}

// 获取container对象
const container = document.getElementById('container')

直接渲染


最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间


截屏2021-11-18 下午10.07.45.png


const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染


这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了


截屏2021-11-18 下午10.14.46.png


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
console.timeEnd('列表时间')
}

requestAnimationFrame


使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame


文档碎片的好处



  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
// 创建一个文档碎片
const fragment = document.createDocumentFragment()
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
// 先塞进文档碎片
fragment.appendChild(div)
}
// 一次性appendChild
container.appendChild(fragment)
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}

懒加载


为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着


其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。


至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性



IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例



截屏2021-11-18 下午10.41.01.png


<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
// 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
// 当前页数与最大页数的比较
if (page.value > maxPage.value) return
const clientHeight = container.value?.clientHeight
const blankTop = blank.value?.getBoundingClientRect().top
if (clientHeight === blankTop) {
// blank出现在视图,则当前页数加1
page.value++
}
}

onMounted(async () => {
const res = await getList()
list.value = res
})
</script>

<template>
<div id="container" @scroll="handleScroll" ref="container">
<div class="sunshine" v-for="(item) in showList" :key="item.tid">
<img :src="item.src" />
<span>{{ item.text }}</span>
</div>
<div ref="blank"></div>
</div>
</template>


虚拟列表


虚拟列表需要讲解的比较多,在这里我分享一下我的一篇虚拟列表的文章,哈哈我自认为讲的不错吧哈哈哈哈哈哈


结合“康熙选秀”,给大家讲讲“虚拟列表”


结语


如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。


如果你想一起学习前端或者摸鱼,那你可以加我,加入我的摸鱼学习群,点击这里 ---> 摸鱼沸点


如果你是有其他目的的,别加我,我不想跟你交朋友,我只想简简单单学习前端,不想搞一些有的没的!!!


作者:Sunshine_Lin
来源:juejin.cn/post/7031923575044964389
收起阅读 »

【Webpack Plugin】写了个插件跟喜欢的女生表白,结果......😭😭😭

web
👋 事情是这样的 作为一名母胎 solo 二十几年的我,平平无奇的一直活在别人的狗粮之下。渐渐的,我好像活成了一个随时见证别人爱情,也随时能为失恋的人排忧解难的角色。 直到前两天,公司新来了一个前端妹子。 相视的第一眼,我神迷了,我知道,终究是躲不过去了.....
继续阅读 »

👋 事情是这样的


作为一名母胎 solo 二十几年的我,平平无奇的一直活在别人的狗粮之下。渐渐的,我好像活成了一个随时见证别人爱情,也随时能为失恋的人排忧解难的角色。


image.png


直到前两天,公司新来了一个前端妹子。


相视的第一眼,我神迷了,我知道,终究是躲不过去了......


image.png


相逢却似曾相识,未曾相识已相思!


当晚,彻夜未眠...


6839f22e-2f0c-4117-b02b-5db21822e8f9.jpg


第二天早上,从同事的口中得知了女生的名字,我们暂且叫她小舒吧。


为了不暴露我的狼子野心(欲擒故纵拿捏的死死的),我决定出于同事的关心询问一下项目了解的怎么样了,有没有需要我帮忙的。


没想到小舒像抓到了救命稻草一样:“小哥,你来的正好,过来帮我看看项目怎么跑不起来??”


8f8a7944-af3a-41a3-9d46-0828aade3146.jpg


我回到座位上,很快的发现是由于项目中部分包的版本不兼容导致的,更新下版本就可以了。


正准备起身去找小舒时,一个奇怪的念头闪过......


我决定给我们的第一次交流一个惊喜:借着这次解决问题的机会,好好拉近一下我们之间的关系!!!


10145af4-9407-40a8-ba24-8b64bfebeaa8.jpg


想法一来便挡也挡不住。我决定在项目中运行一个插件:当启动项目时,直接在控制台中向小舒表达我的心意!!!😏😏😏


没办法,单身这么多年肯定是有原因的!一定是我不够主动!这次我可要好好把握这个机会!!!


f689092c-a552-4fda-8189-d6c7f59fecd3.jpg


🏂 说干就干


有了想法就开干,哥从来不是一个拖拖拉拉的人。


小舒的项目用的是 Webpack + React 技术栈,既然想要在项目启动的时候做事情,那肯定是得写个 Webpack 插件了。


先去官网了解一下 Webpack Plugin 的概念:



Webpack Plugin:向第三方开发者提供了 Webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 Webpack 构建流程中引入自定义的行为。创建插件比创建 loader 更加高级,因为你需要理解 Webpack 底层的特性来处理相应的钩子



867997b1-26f7-40fb-b4b2-c911306bdda4.jpg


通俗点说就是可以在构建流程中插入我们的自定义的行为,至于在哪个阶段插入或者做什么事情都可以通过 Webpack Plugin 来完成。


另外官网还提到,想要弄清楚 Webpack 插件 得先弄清楚这三个东西:tapablecompilercompilation对象,先快点花几分钟去了解一下,争取在中午吃饭前搞定!


27df2766-960f-4c9c-823c-46d62092bdd9.jpg


💁 tapable的使用姿势


tapable是一个类似于 Node.js 中的 EventEmitter 的库,但它更专注于自定义事件的触发和处理。通过 tapable 我们可以注册自定义事件,然后在适当的时机去触发执行。


e9bb13dc-a5b9-4e89-a258-8001c80d7558.jpg


举个例子🌰:类比到 VueReact 框架中的生命周期函数,它们就是到了固定的时间节点就执行对应的生命周期,tapable 做的事情就和这个差不多,可以先注册一系列的生命周期函数,然后在合适的时间点执行。


概念了解的差不多了,接下来去实操一下。初始化项目,安装依赖:


npm init //初始化项目
yarn add tapable -D //安装依赖

安装完项目依赖后,根据以下目录结构来添加对应的目录和文件:


├── dist # 打包输出目录
├── node_modules
├── package-lock.json
├── package.json
└── src # 源码目录
└── index.js # 入口文件

根据官方介绍,tapable 使用起来还是挺简单的,只需三步:



  1. 实例化钩子函数( tapable会暴露出各种各样的 hook,这里以同步钩子Synchook为例)

  2. 注册事件

  3. 触发事件


src/index.js


const { SyncHook } = require("tapable"); //这是一个同步钩子

//第一步:实例化钩子函数,可以在这里定义形参
const syncHook = new SyncHook(["author"]);

//第二步:注册事件1
syncHook.tap("监听器1", (name) => {
console.log("监听器1:", name);
});

//第二步:注册事件2
syncHook.tap("监听器2", (name) => {
console.log("监听器2", name);
});

//第三步:触发事件
syncHook.call("不要秃头啊");

运行 node ./src/index.js,拿到执行结果:


监听器1 不要秃头啊
监听器2 不要秃头啊

63c7e8b4-11bd-4cc5-a96d-be8bcc486365.jpg


从上面的例子中可以看出 tapable 采用的是发布订阅模式通过 tap 函数注册监听函数,然后通过 call 函数按顺序执行之前注册的函数


大致原理:


class SyncHook {
constructor() {
this.taps = [];
}

//注册监听函数,这里的name其实没啥用
tap(name, fn) {
this.taps.push({ name, fn });
}

//执行函数
call(...args) {
this.taps.forEach((tap) => tap.fn(...args));
}
}

另外,tapable 中不仅有 Synchook,还有其他类型的 hook:


image.png


image.png


这里详细说一下这几个类型的概念:



  • Basic(基本的):执行每一个事件函数,不关心函数的返回值

  • Waterfall(瀑布式的):如果前一个事件函数的结果 result !== undefined,则 result 会作为后一个事件函数的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)

  • Bail(保险的):执行每一个事件函数,遇到第一个结果 result !== undefined 则返回,不再继续执行(也就是只要其中一个有返回了,后面的就不执行了)

  • Loop(循环的):不停的循环执行事件函数,直到所有函数结果 result === undefined


大家也不用死记硬背,遇到相关的需求时查文档就好了。


在上面的例子中我们用的SyncHook,它就是一个同步的钩子。又因为并不关心返回值,所以也算是一个基本类型的 hook。


0564085f-3d25-4be0-aeed-6e24c6205762.jpg


👫 tabpable 和 Webpack 的关系


要说它们俩的关系,可真有点像男女朋友之间的难舍难分......


5be98b8a-f500-4f28-8240-a69e567e71b1.jpg


Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,比如



  • 在打包前需要处理用户传过来的参数,判断是采用单入口还是多入口打包,就是通过 EntryOptionPlugin 插件来做的

  • 在打包过程中,需要知道采用哪种读文件的方式就是通过 NodeEnvironmentPlugin 插件来做的

  • 在打包完成后,需要先清空 dist 文件夹,就是通过 CleanWebpackPlugin 插件来完成的

  • ......


而实现这一切的核心就是 tapable。Webpack 内部通过 tapable 会提前定义好一系列不同阶段的 hook ,然后在固定的时间点去执行(触发 call 函数)。而插件要做的就是通过 tap 函数注册自定义事件,从而让其控制在 Webapack 事件流上运行:


image.png


继续拿 Vue 和 React 举例,就好像框架内部定义了一系列的生命周期,而我们要做的就是在需要的时候定义好这些生命周期函数就好。


9fca05f9-1d48-436a-bafd-f45d808a3b49.jpg


🏊‍♀️ Compiler 和 Compilation 


在插件开发中还有两个很重要的资源:compilercompilation对象。理解它们是扩展 Webpack 引擎的第一步。



  • compiler 对象代表了完整的 webpack 生命周期。这个对象在启动 Webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin。当在 Webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 Webpack 的主环境。

  • compilation 对象代表了一次资源版本构建。当运行 Webpack 开发环境中间件( webpack-dev-server)时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。


5dd8339b-4ebd-4007-9e14-f1ca58c17d53.jpg


还是拿 React 框架举例子...... React:


590d1a42-2273-41c5-8ac2-d9e9ed90cf76.jpg


compiler比喻成 React 组件,在 React 组件中有一系列的生命周期函数(componentDidMount()render()componentDidUpdate()等等),这些钩子函数都可以在组件中被定义。


compilation比喻成 componentDidUpdate()componentDidUpdate()只是组件中的某一个钩子,它专门负责重复渲染的工作(compilation只是compiler中某一阶段的 hook ,主要负责对模块资源的处理,只不过它的工作更加细化,在它内部还有一些子生命周期函数)。


如果还是不理解,这里画个图帮助大家理解:


image.png


图上的 entryOptionafterPluginsbeforeRuncompilation 等均是构建过程中的生命周期,而 compilation 只是该过程中的其中一部分,它主要负责对模块资源的处理。在 compilation 内部也有自己的一系列生命周期,例如图中的 buildModulefinishModules 等。


cf8d3c96-74d9-434b-a27d-060dc5244311.jpg


至于为什么要这么处理,原因当然是为了解耦!!!


比如当我们启动 Webpack 的 watch模式,当文件模块发生变化时会重新进行编译,这个时候并不需要每次都重新创建 compiler 实例,只需要重新创建一个 compilation 来记录编译信息即可


另外,图中并没有将全部的 hook 展示出来,更多的hook可以自行查阅官网:compiler上挂载的 hookcompilation上挂载的 hook


ef377b09-4933-4828-9ce1-6bd2b2536e6f.jpg


🏃 如何编写插件


说了这么多,到底要怎么写一个 Webpack 插件?小舒还等着我呢!!!


bb9b6a98-7058-48fc-9ec5-bc84d7bcf8f2.jpg


刚才知道了在 Webpack 中的 compilercompilation 对象上挂载着一系列的生命周期 hook ,那接下来应该怎么在这些生命周期中注册自定义事件呢?


webpack 插件:


cb597c1b-a21a-4b8d-9fa8-b129380a3b9e.jpg


Webpack Plugin 其实就是一个普通的函数,在该函数中需要我们定制一个 apply 方法。当 Webpack 内部进行插件挂载时会执行 apply 函数。我们可以在 apply 方法中订阅各种生命周期钩子,当到达对应的时间点时就会执行。


bb740142-acbd-49da-898c-e0e765ec6552.jpg


这里可能有同学要问了,为什么非要定制一个apply方法?为什么不是其他的方法?


在这里我贴下官方源码:github.com/webpack/web…
大家一看便一目了然:


if (options.plugins && Array.isArray(options.plugins)) {
//这里的options.plugins就是webpack.config.js中的plugins
for (const plugin of options.plugins) {
plugin.apply(compiler); //执行插件的apply方法
}
}

这里官方写死了执行插件中的 apply 方法....,并没有什么很高深的原因.....


68456079-6083-46b1-9248-97f943d7d06d.jpg


那我们就按照规范写一个简易版的插件赶紧来练练手:在构建完成后打印日志。


首先我们需要知道构建完成后对应的的生命周期是哪个,通过 查阅文档得知是 complier 中的done 这个 hook :


image.png


接下来创建一个新项目验证我们的想法,时间不早了!小舒现在肯定很着急!!!


安装依赖:


npm init //初始化项目
yarn add webpack webpack-cli -D

安装完项目依赖后,根据以下目录结构来添加对应的目录和文件:


├── dist # 打包输出目录
├── plugins # 自定义插件文件夹
│ └── demo-plugin.js
├── node_modules
├── package-lock.json
├── package.json
├── src # 源码目录
│ └── index.js # 入口文件
└── webpack.config.js # webpack配置文件

demo-plugin.js


class DemoPlugin {
apply(compiler) {
//在done(构建完成后执行)这个hook上注册自定义事件
compiler.hooks.done.tap("DemoPlugin", () => {
console.log("DemoPlugin:编译结束了");
});
}
}

module.exports = DemoPlugin;

package.json


{
"name": "webpack-plugin",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"tapable": "^2.2.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
}
}

src/index.js


console.log("author:""不要秃头啊");

webpack.config.js


const DemoPlugin = require("./plugins/demo-plugin");

module.exports = {
mode: "development",
entry: "./src/index.js",
devtool: false,
plugins: [new DemoPlugin()],
};

运行 yarn build,运行结果:


yarn build
$ webpack
DemoPlugin:编译结束了
asset main.js 643 bytes [emitted] (name: main)
./src/index.js 476 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 71 ms
Done in 0.64s.

db4cafab-ece6-4bbb-8103-79e4589f0ebe.png


💘 开始我的表白之路....


好了,终于搞清楚怎么写插件了!!!


39d696c9-90ee-4544-9999-c056db959cfc.jpg


直接把刚才学的的demo插件改造一下:


class DonePlugin {
apply(compiler) {
//在done(构建完成后执行)这个hook上注册自定义事件
compiler.hooks.done.tap("DonePlugin", () => {
console.log(
"小姐姐,我知道此刻你很意外。但不知道怎么回事,我看见你的第一眼就沦陷了...可以给我一个多了解了解你的机会吗? ————来自一个热心帮你解决问题的人"
);
});
}
}
module.exports = DonePlugin;

正准备提交代码,思来想去,直接叫小姐姐好像不太好吧?是不是显得我很轻浮?


再说了,小舒怎么知道我在跟她说呢?


想了一会,不如直接用她的 git 账号名吧(当时要是脑子不抽风就好了......😭),于是改成动态获取git 用户名,为了显眼甚至还加了点颜色:


const chalk = require("chalk");//给日志加颜色插件
const execSync = require("child_process").execSync;

const error = chalk.bold.red; //红色日志
const warning = chalk.keyword("orange"); //橘色日志

class DonePlugin {
apply(compiler) {
compiler.hooks.done.tap("DonePlugin", () => {
//获取git账号信息的username
let name = execSync("git config user.name").toString().trim();

console.log(
error(`${name},`),
warning(
"我知道此刻你很意外。但不知道怎么回事,我看见你的第一眼就沦陷了...可以给我一个多了解了解你的机会吗? ————来自一个热心帮你解决问题的人"
)
);
});
}
}

module.exports = DonePlugin;

大致效果就是这样...


image.png


98936199-cfea-4dce-afd1-a25b2a8b1f58.jpg


😳 等待回应


把这一切都准备妥当后,剩下的就交给天意了。


结果是左等右等,到了下午四点迟迟没有等到小舒的回应......


072c64a4-ff38-4a44-83f0-40c754980149.jpg


难道是没看到吗?不应该啊,日志还加了颜色,很明显了!!!


莫非是女孩子太含蓄了,害羞了?


不行,我得主动出击!!


image.png


乘兴而去,败兴而归!!!还在同事圈里闹了个笑话!!!


但是为了下半生,豁出去了!!!


经过我的一番解释,小舒总算相信了我说的话,而我也赶紧去优化了一下代码......


自此以后,每天一句不重样的小情话,小舒甚至还和我互动了起来:


image.png


就这样,我们慢慢的发展成了无话不谈的男女朋友关系,直到前两天甚至还过了1000天纪念日,还给小舒送了点小礼物,虽然被骂直男...


image.png


接下来也该考虑结婚了!!!


“滴~~~,滴~~~,滴~~~,不要命了!等个红绿灯都能睡着?“


“喂,醒醒,醒醒。我的尿黄,让我去渍醒他!”


只听旁边有人说到......


原来只是黄粱一梦。


38d25691-8bdb-4d7b-9b5c-adb0b090c206.jpg


💔 最后的结局


最后,给大家一个忠告:追女孩子一定不要这样, 一定要舍得送花,一定要懂浪漫!!!没有哪个女孩子会因为你写个插件就跟你在一起的!!!


我决定勇敢的试一试:


image.png


卒。


作者:不要秃头啊
来源:juejin.cn/post/7160467329334607908
收起阅读 »

为什么大厂前端监控都在用GIF做埋点?

web
什么是前端监控? 它指的是通过一定的手段来获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,为产品优化指明方向,为用户提供更加精确、完善的服务。 前端监控 一般来讲一个成熟的产品,运营与产品团队需要关注用户在产品内的行为记录,通过用户的行为记录来...
继续阅读 »

什么是前端监控?


它指的是通过一定的手段来获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,为产品优化指明方向,为用户提供更加精确、完善的服务。


前端监控


一般来讲一个成熟的产品,运营与产品团队需要关注用户在产品内的行为记录,通过用户的行为记录来优化产品,研发与测试团队则需要关注产品的性能以及异常,确保产品的性能体验以及安全迭代。


所以前端监控一般也分为三大类:


数据监控(监控用户行为)



  • PV/UV: PV(page view):即页面浏览量或点击量;UV:指访问某个站点或点击某条新闻的不同 IP 地址的人数

  • 用户在每一个页面的停留时间

  • 用户通过什么入口来访问该网页

  • 用户在相应的页面中触发的行为,等...


统计这些数据是有意义的,比如我们知道了用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送等等。


性能监控(监控页面性能)



  • 不同用户,不同机型和不同系统下的首屏加载时间

  • 白屏时间

  • http 等请求的响应时间

  • 静态资源整体下载时间

  • 页面渲染时间

  • 页面交互动画完成时间,等...


这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,尽可能的提高用户体验。


异常监控(监控产品、系统异常)


及时的上报异常情况,可以避免线上故障的发上。虽然大部分异常可以通过 try catch 的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。常见的需要监控的异常包括:



  • Javascript 的异常监控

  • 样式丢失的异常监控


埋点上报


OK,上面我们说到了前端监控的三个分类,了解了一个产品需要监控哪些内容以及为什么需要监控这些内容,那么我们应该怎么实现前端监控呢?


实现前端监控,第一步肯定是将我们要监控的事项(数据)给收集起来,再提交给后台进行入库,最后再给数据分析组进行数据分析,最后处理好的数据再同步给运营或者是产品。数据收集的丰富性和准确性会直接影响到我们做前端监控的质量,因为我们会以此为基础,为产品的未来发展指引方向。


现在常见的埋点上报方法有三种:手动埋点、可视化埋点、无埋点


手动埋点


手动埋点,也叫代码埋点,即纯手动写代码,调用埋点 SDK 的函数,在需要埋点的业务逻辑功能位置调用接口,上报埋点数据,像**[友盟][百度统计]**等第三方数据统计服务商大都采用这种方案。手动埋点让使用者可以方便地设置自定义属性、自定义事件;所以当你需要深入下钻,并精细化自定义分析时,比较适合使用手动埋点。


手动埋点的缺陷就是,项目工程量大,需要埋点的位置太多,而且需要产品开发运营之间相互反复沟通,容易出现手动差错,如果错误,重新埋点的成本也很高。


可视化埋点


通过可视化交互的手段,代替上述的代码埋点。将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码。


可视化埋点的缺陷就是可以埋点的控件有限,不能手动定制。


无埋点


无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据。优点是前端只要一次加载埋点脚本,缺点是流量和采集的数据过于庞大,服务器性能压力山大。


为什么都用GIF来做埋点?


发现过程


首先说一下我是怎么发现的,前一段时间,产品提了个需求,说我们现在的书籍曝光上报规范并不是他们想要的数据,并且以后所有页面的书籍上报都统一成最新规范。


曝光规范:



  • 书籍出现在可视区并停留1秒,算作有效曝光

  • 书籍不能重复曝光,假如它一直在可视区滚动时只能上报一次

  • 当它移出可视区后再回到可视区,再按第一点进行曝光


OK,既然要所有页面统一,那就只能封装成通用库来使用了,这里实现逻辑就不贴了,想看的私聊我发你,主要的难点就是停留时长计算,以及曝光标记。


const exposeReportClass = new exposeReport({
scrollDom: "", // 滚动容器,建议指定一个滚动容器,不传默认为window
watchDom: ".bookitem", // 监听的dom,建议使用class类,标签也支持
time: 1000 // 停留有效时长ms
});
// 提供两个上报方法
exposeReportClass.didReport(()=>{
// 手动上报
//callback
})
exposeReportClass.scrollReport(()=>{
// 滚动动上报
//callback
})
//

具体业务逻辑之需要放在对应的callback里面,而上报逻辑开发者无需考虑,因为我底层已经统一处理好了。


然后我再测试的时候就发现,上报发的请求居然是通过图片发起的,并不是我们认为的接口上报。


report.png


然后我去查了下资料,发现很多大厂的上报都是这么干的!


使用GIF上报的原因


向服务器端上报数据,可以通过请求接口,请求普通文件,或者请求图片资源的方式进行。只要能上报数据,无论是请求GIF文件还是请求js文件或者是调用页面接口,服务器端其实并不关心具体的上报方式。那为什么所有系统都统一使用了请求GIF图片的方式上报数据呢?



  • 防止跨域


一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。而跨域请求很容易出现由于配置不当被浏览器拦截并报错,这是不能接受的。但图片的src属性并不会跨域,并且同样可以发起请求。(排除接口上报)



  • 防止阻塞页面加载,影响用户体验


通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。


但是图片请求例外。构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点,这是其他类型的资源请求所做不到的。(排除文件方式)



  • 相比PNG/JPG,GIF的体积最小


最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节。


同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。


并且大多采用的是1*1像素的透明GIF来上报


1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。


作者:前端南玖
来源:juejin.cn/post/7065123244881215518
收起阅读 »

我是埋点SDK,看我如何让甲方爸爸的页面卡顿10s+

web
背景音: Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK... 昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就...
继续阅读 »

背景音:



Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK...



昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就开始有用户反馈说网页卡卡地,走得比蜗牛还慢。


六点二十分,第一个用户提交了投诉工单,但这只是个开始。


今天早上九点十分,公司的运维团队已经接到了一大堆反馈工单,许多用户都遭受到了同样的问题。这是一个巨大的问题,一旦得不到解决,可能导致数万的用户受到影响。运维人员立即开始了排查工作,想要找出问题所在。


经过一个小时的紧急排查,他们终于想到了昨日的这名前端程序员,一经沟通发现是SDK版本更新引起的问题。在新的版本中,有一些不稳定的代码导致了性能问题。


然而,这不仅仅是个技术问题,因为接下来,他们要开始着手写事故报告,准备给上层领导交代。


接下来,进入正题:


一、问题排查定位


根据更新的版本体量,可以缩小和快速定位问题源于新引入埋点SDK



  1. 打开 开发者工具-性能分析,开始记录

  2. 刷新页面,重现问题

  3. 停止记录,排查分析性能问题


性能分析


如上图,按照耗时排序,可以快速定位找到对应的代码问题。


首先把编译压缩后的代码整理一下,接下来,深入代码一探究竟。


代码耗时.png


⏸️暂停一下,不妨猜猜看这里是为了干嘛?


🍵喝口茶,让我们沿着事件路径,反向继续摸清它的意图吧。


image.png


这里列举了231个字体名称,调用上文的 detect() 来分析。


⏸️暂停一下,那么这个操作为什么会耗时且阻塞页面渲染呢?


...


休息一下,让我们去看看这段代码的来龙去脉。


上面我们大概猜到代码是用于获取用户浏览器字体,那就简单检索一下 js get browser font


搜索结果.png


代码示例.png


证据确凿,错在对岸。


二、解决问题


相信大家也看出来了,我不是埋点SDK,我也不是甲方爸爸,我只能是一位前端开发。


联系反馈至SDK方,需要走工单,流程,而这一切要多少时间?


我唔知啊!领导也不接受啊!


👐没办法,只能自己缝补缝补了。


那么如何解决呢?



  1. 尝试修复 getFonts detect 字体检测逻辑,避免多次重绘。

  2. 缩短待检测字体目录。


人生苦短,我选方案3,直接修改返回值,跳过检测


getFonts () { return 'custom_font' }

那么让我们继续搬砖吧。



  1. 寻根


image.png


首先找到 SDK 加载对应 JS 的加载方式,看看能不能动点手脚。
这里可以看到,是采用很常见的 通过 appendScript loadJs 的方案,那么就可以复写拦截一下 appendChild 函数。



  1. 正源


通过拦截 appendChild,将SDK加载的JS改为加载修复后的JS文件。


核心代码如下:


var tempCAppend = document.head.appendChild
document.head.appendChild = function (t) {
if (t.tagName === 'SCRIPT' && t.src.includes('xxx.js')) {
t.src = 'custom_fix_xxx.js'
}
return tempCAppend.bind(this)(t)
}

三、后续


这件事情发生在21年底,今天为什么拿出来分享一下呢?


近期排查 qiankun 部分子应用路由加载异常的时候,定位到与 document.head.appendChild 被复写有关,于是去看SDK方是否修复,结果纹丝未动....


结合近期境遇,不得不感慨,业务能不能活下去,真的和代码、技术什么的毫无关系。


其他


❄️下雪了,简单看了几眼文心一言的发布会,更凉了。


作者:夏无凉风冬有雪
来源:juejin.cn/post/7211020974023868475
收起阅读 »

这几个群,程序员可千万不要进!

震惊!某摸鱼网站惊现肾结石俱乐部! (图源V2EX) 无关地域、无关性别,各位程序员们在肾结石这个病上面有着出奇一致的反应。诸如此类的各种职业病在我们的生活中更是十分常见。 也可能是到年纪了,在办公室里放眼望去,一群处于亚健康状态的同事们格外显眼:手上戴着护...
继续阅读 »

震惊!某摸鱼网站惊现肾结石俱乐部!
在这里插入图片描述


(图源V2EX)


无关地域、无关性别,各位程序员们在肾结石这个病上面有着出奇一致的反应。诸如此类的各种职业病在我们的生活中更是十分常见。


也可能是到年纪了,在办公室里放眼望去,一群处于亚健康状态的同事们格外显眼:手上戴着护腕的,脖子上贴着膏药的,闲着没事锤两下后背的,甚至还有站着办公时不时嘶哈两声的……


悄悄问了那位站着办公的同事,结果平白遭受了一个白眼:你见过花季少女割痔疮的吗?


没有,不过现在见到了。


不愧是万物生长的季节,那些困扰程序员们的各类职业病也开始逐渐冒头,像“肾结石俱乐部”这样的群,程序员们也许建了成百上千个,不信往下看看?


1、腱鞘炎相亲相爱一家人


在这里插入图片描述


专家建议


腱鞘炎对程序员群体来说已成为一种常见疾病,虽然不会对我们的生命构成威胁,但都或多或少地影响了我们的生活质量:


手部运动不灵活,手腕无力太软弱。
疼痛一来闹心窝,让人意乱难工作。


想知道自己有没有腱鞘炎,分享给大家一个小方法:
在这里插入图片描述


腱鞘炎保养可以从这几个方面入手:


休息:保证手腕足够的休息时间,比如每30分钟活动下手腕,避免过多接触冷水。
热敷:温热水泡手,或是热水袋热敷,每天2次,每次30~45分钟。
按摩:对侧拇指沿手指掌面按摩,按摩范围包括整个手掌指背以及手指的掌侧。


如果腱鞘炎比较严重,需要配合一些药物治疗、局部封闭注射治疗或手术治疗,具体需咨询医生。对于还没有腱鞘炎的小伙伴,以下几个手部操送给你,开启腱鞘炎预防之旅吧!


在这里插入图片描述


2、颈椎患者大本营


在这里插入图片描述


专家建议


其实随着与手机、电脑相处的时间越来越长,颈椎病的发病人群也逐渐年轻化。当我们在低头玩手机时,脊椎正在承受原本无法承受之重。


举个例子:一个人的头部重约5kg,当我们前倾看手机或低头时,颈部的负重就会大大增加。
在这里插入图片描述


看到这里,请大家立刻挺胸抬头并阅读、实践以下内容,减轻颈椎负担!
在这里插入图片描述


3、干眼症娱乐部


在这里插入图片描述


说起干眼症,大多数程序员都不陌生。引起干眼症的原因比较多,比如过劳致“干”、药物致“干”环境致“干”等等……干眼症很难根治,只能靠日常保养加以改善,从这个意义上讲,干眼症确实可以称为“绝症”。来听听专家的建议吧!


专家建议


1)调位置


电脑显示器的位置和字体大小,让眼睛距离屏幕50-60厘米,屏幕中心在视线水平或稍向下10-15厘米。
在这里插入图片描述


2)多眨眼


多眨眼可以放松睫状肌,滋润眼球。建议每分钟眨眼次数 >15-20次。


3)少看屏幕,多远眺


用电脑或看手机都要注意时间,避免长时间盯着屏幕。参考20原则:用眼20分钟可以盯着20米以外的地方,休息至少20秒。


4)其他小窍门



  • 使用蒸汽眼罩或热毛巾热敷,每天1-2次,每次5-10分钟;

  • 在医生的建议下使用人工泪液;

  • 佩戴隐形眼镜每天应控制在8小时以内,不超过12小时;

  • 使用空气加湿器,让空气湿度适宜;

  • 增加户外运动。


不说了,眼睛发干,需要摸会儿鱼休息一下(误)~


4、防脱治疗所


身边的程序员们说:一个程序员一生能写的程序行数是有限的,写一行代码就会掉一根头发,直到青丝落尽,就是这个程序员隐退江湖的时候。


当然,这只是玩笑话。引起脱发的原因可能是遗传、压力大、熬夜通宵、 频繁烫染或饮食不均衡等。怎么确定自己的史密斯夫妇还有约翰逊家族们安全,两个步骤测试一下:


在这里插入图片描述


保护我们的史密斯夫妇还有约翰逊家族们有这三个妙招:


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


这些我们眼中的“小病小灾”就像技术债务,短期内选择了忽视,累计到一定程度就会变成身体里的“定时炸弹”。所以,当我们开始长时间的久坐、低头盯手机,当我们压力变大,习惯于加班、熬夜、饮食不规律……更需要停下来想想。



在非洲有个部落
人如果连续赶路三天
就一定要停下来休息一天
因为人们害怕灵魂跟不上自己的脚步
……
——阮靖《风会记得你走过的路》



如果一直忙着赶路,也要记得停下休息,别让健康跟不上我们的脚步。


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

“ChatGPT 们” 所需算力真是“贵滴夸张”!

先抛几个数据: 当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复 如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算 而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要...
继续阅读 »

先抛几个数据:


当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复


如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算


而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要 7W+ RMB💴


image.png


意思就是:光计算这些有意义/无意义的问题,就要花费:21 亿+ RMB 💴


这还不算每天的电费/维护费等,众所周知,这种庞大计算类似矿机,很费电


一块 A100,功率 400W,30000 块 A100 ,就是 12_000_000 W,是 12 MW(12 个 100 万瓦)


image.png


目前,“GPT们”就像是井喷一样出现了各类产品,先不管算的结果怎么样,大家先支棱起来、先算起来


有预测:十年后,“GPT们” 一天所需算力的生产功率相当于半个核电站产生的功率,这是离谱且夸张的!全球算力几乎快要无法满足“GPT们”了


image.png


所以,以 ChatGPTPlus(每月20刀) 为代表的 “GPT们” 很贵,因为它本来算力消费就很贵,“用 GPT 编程比招一个普通程序员更贵”的段子并非玩笑。


所以,为什么算力如此重要?为什么微软要和韩国SK集团布局自建核电站?为什么咱们要强调西数东算、云计算等等?从这里也能窥见一斑。


不夸张的说,在未来,国力强弱一方面将通过算力强弱来体现。


image.png


2016年6月的不同国家之间的超级计算机500强的分布


image.png


超级计算机模拟风洞实验


小思考:在未来,算力的瓶颈将如何突破?


—— 目前芯片仍处在传统冯·诺伊曼架构之下,存储和计算区域是分离的,搬运数据“从存储到计算”花费巨大的功耗,如果能实现“存算一体”(就像大脑一样)将提升算力进入新的量级。


所以“存算一体”可能是个方向~


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

在国内如何不用梯子就使用ChatGPT

首先看效果图 如何国内访问 由于OpenAI官方服务器在国内被限制,所以想要在国内直接访问ChatGPT并不容易。但是,今天格子衫将介绍一种无需使用梯子的方法,就可以通过API接口访问ChatGPT。 要使用这种方法,您需要获取一个ApiKey。下面是如何...
继续阅读 »

首先看效果图


1678790956947.jpg


1678790977126.jpg


如何国内访问


由于OpenAI官方服务器在国内被限制,所以想要在国内直接访问ChatGPT并不容易。但是,今天格子衫将介绍一种无需使用梯子的方法,就可以通过API接口访问ChatGPT。


要使用这种方法,您需要获取一个ApiKey。下面是如何获取ApiKey的步骤:


获取 Api Key


首先,您需要注册一个OpenAI的账号(文末有其他方式)。



  1. 使用浏览器打开Account API Keys - OpenAI API,如果没有登录,会跳转到登录页面,按照页面登录即可。

  2. 登录完成以后,再次访问刚才的 Account API Keys - OpenAI API,就可以获取ApiKey了,按照下图操作即可获取ApiKey。


Create api key


点击 Create new secret key,会生成如下图所示的秘钥。


请点击右侧的“复制”按钮将ApiKey复制到剪贴板中,然后立即将它保存到一个安全的地方。这样,即使您的剪贴板内容被覆盖或丢失,您仍然可以访问到ApiKey。你应该跟保存密码一样保存它。


Create api key success


**注意:**ApiKey只会显示一次。如果您不小心丢失了它,您需要重新生成一个ApiKey。这可能需要您再次使用梯子访问OpenAI官方服务器,有点麻烦。


接下来,拿着这个 ApiKey,去访问国内版ChatGPT,把ApiKey输入到下图所示的指定位置;


image-20230312143953871


然后无需梯子就可以直接访问 ChatGPT了,直接在聊天窗口输入即可,可以和ChatGPT进行对话了!如下图所示:


image-20230312144941252


感兴趣的小伙伴们快行动起来吧!


地址是 chatv.fu520.top/


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

我为什么用TheRouter而不玩Arouter了呢

TheRouter 简介   TheRouter是货拉拉开源的路由框架,针对Android平台实现组件化、跨模块调用、动态化等功能的集成框架。 TheRouter 掘金:juejin.cn/post/713971… Github: github.com/Huo...
继续阅读 »

TheRouter 简介


  TheRouter是货拉拉开源的路由框架,针对Android平台实现组件化、跨模块调用、动态化等功能的集成框架。

TheRouter 掘金:juejin.cn/post/713971…

Github: github.com/HuolalaTech…

官网:therouter.cn/


为什么要用TheRouter


功能TheRouterARouterWMRouter
Fragment路由✔️✔️✔️
支持依赖注入✔️✔️✔️
加载路由表无运行时扫描无反射运行时扫描dex反射实例类性能损耗大运行时读文件反射实例类性能损耗中
注解正则表达式✔️✖️✔️
Activity指定拦截器✔️(四大拦截器可根据业务定制)✖️✔️
导出路由文档✔️(路由文档支持添加注释描述)✔️✖️
动态注册路由信息✔️✔️✖️
APT支持增量编译✔️✔️(开启文档生成则无法增量编译)✖️
plugin支持增量编译✔️✖️✖️
多 Path 对应同一页面(低成本实现双端path统一)✔️✖️✖️
远端路由表下发✔️✖️✖️
支持单模块独立初始化✔️✖️✖️
支持使用路由打开第三方库页面✔️✖️✖️


上图是从官方获取的介绍,结合自己使用,从以下几点介绍他的好处



  • 使用简易

  • 针对后台startActivity的兼容处理

  • 直接获取Intent对象

  • 动态下发路由表的骚操作

  • 和Arouter性能相比


- 使用简易


  针对刚入手的同学,很多同学其实需要的功能不多,主要就是跳转页面,原先使用过的Arouter,也能快速适应。想深入学习更多的同学可以再去官网或者官方掘金see see


// 简单的传参和路由跳转
TheRouter.build("test://webview/home")
.withString("url", "http://www.baidu.com")
.navigation(act)

// 如果要打开的是fragment,需要使用 .createFragment();

- 针对后台startActivity的兼容处理



Android 10 (级别 29) 及更高版本对后台应用可启动 Activity 的时间施加限制。这些限制有助于最大限度地减少对用户造成的中断(可以更省电),并且可以让用户更好地控制其屏幕上显示的内容。



  官方针对Android10以及以上版本调用startActivity的限制,也就是在后台调用该方法是无法跳转的,TheRoute提供兼容处理,先暂停跳转,等APP重新启动了再继续跳转。


// 以下代码只是功能介绍,大家按需自取
// 后台跳转页面
fun goBackActivity(activity: Activity) {
val navigator: Navigator =
TheRouter.build("test://webview/home")
.withString("paramStr", paramStr)
.withInt("paramInt", 0)
if (AndroidVersion.hasQ() && !AppUtils.isAppForeground()) {
// pending() 会标记存入集合LinkedList
navigator.pending().navigation(activity)
} else {
navigator.navigation(activity)
}
}

// Application监听
class ApplicationObserver : DefaultLifecycleObserver {

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
// 应用重进onResume,把原先pending()存入集合的navigation,重新取出全部执行
// sendPendingNavigator是Navigator的顶级函数
sendPendingNavigator()
}
}

- 直接获取Intent对象


  大部分的路由框架都不提供直接返回Intent,但是有时候我们又只需要对应其他模块的intent对象并不需要跳转,例如构建系统通知栏的点击需要传入PendingIntent,可能需要通过Intent对象构建。


// 获取intent
val intent = TheRouter.build("test://login/hello").createIntent(activity)

- 动态下发路由表的『骚操作』


   为什么我个人说这是『骚操作』呢?我们以前写一个页面,页面崩溃了,那么我们就想办法修复,让用户先忍忍。可是用你这个APP的人真的能忍受你的崩溃吗?答案是不能的,因此我们想尽各种办法例如热修复等等,但是还是有没有经过热修复的人崩溃了,还是大面积的崩溃。


  那动态下发路由表,让这个崩溃页面的路由改下其他页面,例如这个崩溃页面的路由跳转H5页面,告知用户正在紧急修复,这样的过渡会比直接崩溃使用感受好。


  当然不止这个场景,再举例一个我们最常见的,例如A页面忘了给相机权限,然后上线了跳转A页面的时候才发现少了这个权限,咋整?重发版呗。但是如果用TheRouter,可以在原工程预留一个过渡页面,我们动态下发路由表,跳到这个过渡页获取权限了再重新跳转原来A页面。


// 获取远端的路由map
/**
[
{
"path""test://home/webview/pre",
"action""test://action/start_activity?path=test://app/alert?type=-1&permissionJson={"android.permission.READ_PHONE_STATE": "需要获取您的手机网络状态"}",
"description""",
"className""com.test.main.PreWebViewV2Activity",
"params": {
}
}
]
// action说明: 可以写一个test://app/alter权限弹窗页面,传入permissionJson参数授权,授权成功之后再重新执行原来的路由地址
*/
fun getServiceRouterMap() {
try {
val gson = "注释提供的格式"
if (gson.isNullOrEmpty()) {
return
}
Kv.putEnv(LOCAL_ROUTER_MAP, gson) // 本地存储,方便下次取出
mRouteItem = GsonUtils.fromJson(gson, object : TypeToken?>() {}.type)
} catch (e: Exception) {
Log.d("updateRouterMap", e?.toString())
}
}

// 把远端路由遍历添加到本地
fun addMdapRouteMap() {
Log.d("HllMarsConfigTask", "asyncInitRouteMap")
var list = mRouteItem
if (list != null && list.size > 0) {
// 建议远端下发路由表差异部分,用远端包覆盖本地更合理
addRouteMap(list) // TheRouter库RouteMap.kt提供顶级函数
}
}

// 以下是TheRouter库RouteMap.kt类
// 具体实现:如何把远端路由添加到本地
@Synchronized
fun addRouteMap(routeItemArray: Collection?) {
if (routeItemArray != null && !routeItemArray.isEmpty()) {
for (entity in routeItemArray) {
addRouteItem(entity)
}
}
}

@Synchronized
fun addRouteItem(routeItem: RouteItem) {
var path = routeItem.path
if (path.endsWith("/")) {
path = path.substring(0, path.length - 1)
}
debug("addRouteItem", "add $path")
ROUTER_MAP[path] = routeItem
onRouteMapChangedListener?.onChanged(routeItem)
}

- 和Arouter性能相比


  以阿里的Arouter来说,它是有比较沉重的历史包袱,虽然后面有迭代几个版本来优化,但是更多的还是在历史基础上优化,TheRouter却没有历史包袱。下面是我们自己工程跑的对比效果


therouter和arouter性能对比.png


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

一个app到底会创建多少个Application对象

问题背景 最近跟群友讨论一个技术问题: 一个应用开启了多进程,最终到底会创建几个application对象,执行几次onCreate()方法? 有的群友根据自己的想法给出了猜想 甚至有的群友直接咨询起了ChatGPT 但至始至终都没有一个最终的结论。于是...
继续阅读 »

问题背景


最近跟群友讨论一个技术问题:


交流1


一个应用开启了多进程,最终到底会创建几个application对象,执行几次onCreate()方法?


有的群友根据自己的想法给出了猜想


交流2


甚至有的群友直接咨询起了ChatGPT


chatgpt1.jpg


但至始至终都没有一个最终的结论。于是乎,为了弄清这个问题,我决定先写个demo测试得出结论,然后从源码着手分析原因


Demo验证


首先创建了一个app项目,开启多进程


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">
<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>
</application>

</manifest>

然后在DemoApplication的onCreate()方法打印application对象的地址,当前进程名称


public class DemoApplication extends Application {
private static final String TAG = "jasonwan";

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Demo application onCreate: " + this + ", processName=" + getProcessName(this));
}

private String getProcessName(Application app) {
int myPid = Process.myPid();
ActivityManager am = (ActivityManager) app.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo runningAppProcess : runningAppProcesses) {
if (runningAppProcess.pid == myPid) {
return runningAppProcess.processName;
}
}
return "null";
}
}

运行,得到的日志如下


2023-03-07 11:15:27.785 19563-19563/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote

查看当前应用所有进程


查看进程1


说明此时app只有一个进程,且只有一个application对象,对象地址为@fb06c2d


现在我们将进程增加到多个,看看情况如何


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">
<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>
<activity
android:name=".TwoActivity"
android:process=":remote2" />
<activity
android:name=".ThreeActivity"
android:process=":remote3" />
<activity
android:name=".FourActivity"
android:process=":remote4" />
<activity
android:name=".FiveActivity"
android:process=":remote5" />
</application>

</manifest>

逻辑是点击MainActivity启动TwoActivity,点击TwoActivity启动ThreeActivity,以此类推。最后我们运行,启动所有Activity得到的日志如下


2023-03-07 11:25:35.433 19955-19955/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote
2023-03-07 11:25:43.795 20001-20001/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote2
2023-03-07 11:25:45.136 20046-20046/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote3
2023-03-07 11:25:45.993 20107-20107/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote4
2023-03-07 11:25:46.541 20148-20148/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote5

查看当前应用所有进程


查看进程2


此时app有5个进程,但application对象地址均为@fb06c2d,地址相同意味着它们是同一个对象。


那是不是就可以得出结论,无论启动多少个进程都只会创建一个application对象呢?并不能妄下此定论,我们将MainActivityprocess属性去掉再运行,得到的日志如下


2023-03-07 11:32:10.156 20318-20318/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@5d49e29, processName=com.jason.demo0307
2023-03-07 11:32:15.143 20375-20375/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote2
2023-03-07 11:32:16.477 20417-20417/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote3
2023-03-07 11:32:17.582 20463-20463/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote4
2023-03-07 11:32:18.882 20506-20506/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote5

查看当前应用所有进程


查看进程3


此时app有5个进程,但有2个application对象,对象地址为@5d49e29和@fb06c2d,且子进程的application对象都相同。


上述所有进程的父进程ID为678,而此进程正是zygote进程


zygote进程


根据上面的测试结果我们目前能得出的结论:



  • 结论1:单进程只创建一个Application对象,执行一次onCreate()方法;

  • 结论2:多进程至少创建2个Application对象,执行多次onCreate()方法,几个进程就执行几次;


结论2为什么说至少创建2个,因为我在集成了JPush的商业项目中测试发现,JPush创建的进程跟我自己创建的进程,Application地址是不同的。


jpush进程


这里三个进程,分别创建了三个Application对象,对象地址分别是@f31ba9d,@2c586f3,@fb06c2d


源码分析


这里需要先了解App的启动流程,具体可以参考《App启动流程》


Application的创建位于frameworks/base/core/java/android/app/ActivityThread.javahandleBindApplication()方法中


	@UnsupportedAppUsage
private void handleBindApplication(AppBindData data) {
long st_bindApp = SystemClock.uptimeMillis();
//省略部分代码

// Note when this process has started.
//设置进程启动时间
Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());

//省略部分代码

// send up app name; do this *before* waiting for debugger
//设置进程名称
Process.setArgV0(data.processName);
//省略部分代码

// Allow disk access during application and provider setup. This could
// block processing ordered broadcasts, but later processing would
// probably end up doing the same disk access.
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
//此处开始创建application对象,注意参数2为null
app = data.info.makeApplication(data.restrictedBackupMode, null);

//省略部分代码
try {
if ("com.jason.demo0307".equals(app.getPackageName())){
Log.d("jasonwan", "execute app onCreate(), app=:"+app+", processName="+getProcessName(app)+", pid="+Process.myPid());
}
//执行application的onCreate方法()
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
} finally {
// If the app targets < O-MR1, or doesn't change the thread policy
// during startup, clobber the policy to maintain behavior of b/36951662
if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
StrictMode.setThreadPolicy(savedPolicy);
}
}
//省略部分代码
}

实际创建过程在frameworks/base/core/java/android/app/LoadedApk.java中的makeApplication()方法中,LoadedApk顾名思义就是加载好的Apk文件,里面包含Apk所有信息,像包名、Application对象,app所在的目录等,这里直接看application的创建过程


	@UnsupportedAppUsage
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if ("com.jason.demo0307".equals(mApplicationInfo.packageName)) {
Log.d("jasonwan", "makeApplication: mApplication="+mApplication+", pid="+Process.myPid());
}
//如果已经创建过了就不再创建
if (mApplication != null) {
return mApplication;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");

Application app = null;

String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}

try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
//反射创建application对象
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
if ("com.jason.demo0307.DemoApplication".equals(appClass)){
Log.d("jasonwan", "create application, app="+app+", processName="+mActivityThread.getProcessName()+", pid="+Process.myPid());
}
appContext.setOuterContext(app);
} catch (Exception e) {
Log.d("jasonwan", "fail to create application, "+e.getMessage());
if (!mActivityThread.mInstrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
mApplication = app;

if (instrumentation != null) {
try {
//第一次启动创建时,instrumentation为null,不会执行onCreate()方法
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!instrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}

// 省略部分代码
return app;
}

为了看清application到底被创建了几次,我在关键地方埋下了log,TAG为jasonwan的log是我自己加的,编译验证,得到如下log


启动app,进入MainActivity
03-08 17:20:29.965 4069 4069 D jasonwan: makeApplication: mApplication=null, pid=4069
//创建application对象,地址为@c2f8311,当前进程id为4069
03-08 17:20:29.967 4069 4069 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:29.988 4069 4069 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:29.989 4069 4069 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:36.614 4069 4069 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4069

点击MainActivity,跳转到TwoActivity
03-08 17:20:39.686 4116 4116 D jasonwan: makeApplication: mApplication=null, pid=4116
//创建application对象,地址为@c2f8311,当前进程id为4116
03-08 17:20:39.687 4116 4116 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.688 4116 4116 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.688 4116 4116 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.733 4116 4116 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4116

点击TwoActivity,跳转到ThreeActivity
03-08 17:20:41.473 4147 4147 D jasonwan: makeApplication: mApplication=null, pid=4147
//创建application对象,地址为@c2f8311,当前进程id为4147
03-08 17:20:41.475 4147 4147 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.475 4147 4147 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.476 4147 4147 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.519 4147 4147 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4147

点击ThreeActivity,跳转到FourActivity
03-08 17:20:42.966 4174 4174 D jasonwan: makeApplication: mApplication=null, pid=4174
//创建application对象,地址为@c2f8311,当前进程id为4174
03-08 17:20:42.968 4174 4174 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:42.969 4174 4174 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:42.969 4174 4174 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:43.015 4174 4174 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4174

点击FourActivity,跳转到FiveActivity
03-08 17:20:44.426 4202 4202 D jasonwan: makeApplication: mApplication=null, pid=4202
//创建application对象,地址为@c2f8311,当前进程id为4202
03-08 17:20:44.428 4202 4202 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.429 4202 4202 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.430 4202 4202 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.473 4202 4202 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4202

结果很震惊,我们在5个进程中创建的application对象,地址均为@c2f8311,也就是至始至终创建的都是同一个Application对象,那么上面的结论2显然并不成立,只是测试的偶然性导致的。


可真的是这样子的吗,这也太颠覆我的三观了,为此我跟群友讨论了这个问题:


不同进程中的多个对象,内存地址相同,是否代表这些对象都是同一个对象?


群友的想法是,java中获取的都是虚拟内存地址,虚拟内存地址相同,不代表是同一个对象,必须物理内存地址相同,才表示是同一块内存空间,也就意味着是同一个对象,物理内存地址和虚拟内存地址存在一个映射关系,同时给出了java中获取物理内存地址的方法Android获取对象地址,主要是利用Unsafe这个类来操作,这个类有一个作用就是直接访问系统内存资源,具体描述见Java中的魔法类-Unsafe,因为这种操作是不安全的,所以被标为了私有,但我们可以通过反射去调用此API, 然后我又去请教了部门搞寄存器的大佬,大佬肯定了群友的想法,于是我添加代码,尝试获取对象的物理内存地址,看看是否相同


public class DemoApplication extends Application {
public static final String TAG = "jasonwan";

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "DemoApplication=" + this + ", address=" + addressOf(this) + ", pid=" + Process.myPid());
}

//获取对象的真实物理地址
public static long addressOf(Object o) {
Object[] array = new Object[]{o};
long objectAddress = -1;
try {
Class cls = Class.forName("sun.misc.Unsafe");
Field field = cls.getDeclaredField("theUnsafe");
field.setAccessible(true);
Object unsafe = field.get(null);
Class unsafeCls = unsafe.getClass();
Method arrayBaseOffset = unsafeCls.getMethod("arrayBaseOffset", Object.class.getClass());
int baseOffset = (int) arrayBaseOffset.invoke(unsafe, Object[].class);
Method size = unsafeCls.getMethod("addressSize");
int addressSize = (int) size.invoke(unsafe);
switch (addressSize) {
case 4:
Method getInt = unsafeCls.getMethod("getInt", Object.class, long.class);
objectAddress = (int) getInt.invoke(unsafe, array, baseOffset);
break;
case 8:
Method getLong = unsafeCls.getMethod("getLong", Object.class, long.class);
objectAddress = (long) getLong.invoke(unsafe, array, baseOffset);
break;
default:
throw new Error("unsupported address size: " + addressSize);
}
} catch (Exception e) {
e.printStackTrace();
}
return objectAddress;
}
}

运行后得到如下日志


2023-03-10 11:01:54.043 6535-6535/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@930d275, address=8050489105119022792, pid=6535
2023-03-10 11:02:22.610 6579-6579/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119027136, pid=6579
2023-03-10 11:02:36.369 6617-6617/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119029912, pid=6617
2023-03-10 11:02:39.244 6654-6654/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119032760, pid=6654
2023-03-10 11:02:40.841 6692-6692/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119036016, pid=6692
2023-03-10 11:02:52.429 6729-6729/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119038720, pid=6729

可以看到,虽然Application的虚拟内存地址相同,都是331b3b9,但它们的真实物理地址却不同,至此,我们可以得出最终结论



  • 单进程,创建1个application对象,执行一次onCreate()方法

  • 多进程(N),创建N个application对象,执行N次onCreate()方法

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

Android 可视化预览及编辑Json

项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。 开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效 ...
继续阅读 »

项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。


开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效





    1. 每次抓包修改配置





    1. 本地导入配置,从磁盘读取




但两种方式都有一定弊端



  • 首先测试时依赖电脑修改配置

  • 无法直观预览广告配置


考虑到开发时经常使用的Json格式化工具,既可以直观的预览Json, 还可以在线编辑


那么就考虑将Json格式化工具移植到项目测试模块中


web网页可以处理Json格式化,同理在Android webView 中同样可行, 只需要引入处理格式化的JS代码即可。


查找资料,发现一个很实用的文章可视化编辑json数据——json editor


开始处理


首先准备好WebView的壳子


    //初始化
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
binding.webView.settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportZoom(true)
useWideViewPort = true
builtInZoomControls = true
}
binding.webView.addJavascriptInterface(JsInterface(this@MainActivity), "json_parse")
}

//webView 与 Android 交互
inner class JsInterface(context: Context) {
private val mContext: Context

init {
mContext = context
}

@JavascriptInterface
fun configContentChanged() {
runOnUiThread {
contentChanged = true
}
}

@JavascriptInterface
fun toastJson(msg: String?) {
runOnUiThread { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show() }
}

@JavascriptInterface
fun saveConfig(jsonString: String?) {
runOnUiThread {
contentChanged = false
Toast.makeText(mContext, "verification succeed", Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun parseJsonException(e: String?) {
runOnUiThread {
e?.takeIf { it.isNotBlank() }?.let { alert(it) }
}
}
}

加载json并在WebView中展示



viewModel.jsonData.observe(this) { str ->
if (str?.isNotBlank() == true) {
binding.webView.loadUrl("javascript:showJson($str)")
}
}

WebView 加载预览页面


        binding.webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel.loadAdConfig(this@MainActivity)
}
}

binding.webView.loadUrl("file:///android_asset/preview_json.html")


Json 预览页, preview_json.html实现



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="jquery.json-viewer.css"
rel="stylesheet" type="text/css">
</head>
<style type="text/css">
#json-display {
margin: 2em 0;
padding: 8px 15px;
min-height: 300px;
background: #ffffff;
color: #ff0000;
font-size: 16px;
width: 100%;
border-color: #00000000;
border:none;
line-height: 1.8;
}
#json-btn {
display: flex;
align-items: center;
font-size: 18px;
width:100%;
padding: 10;

}
#format_btn {
width: 50%;
height: 36px;
}
#save_btn {
width: 50%;
height: 36px;
margin-left: 4em;
}

</style>
<body>
<div style="padding: 2px 2px 2px 2px;">
<div id="json-btn" class="json-btn">
<button type="button" id="format_btn" onclick="format_btn();">Format</button>
<button type="button" id="save_btn" onclick="save_btn();">Verification</button>

</div>
<div>
<pre id="json-display" contenteditable="true"></pre>
</div>
<br>
</div>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.json-viewer.js"></script>
<script>

document.getElementById("json-display").addEventListener("input", function(){
console.log("json-display input");
json_parse.configContentChanged();
}, false);
function showJson(jsonObj){
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}
function format_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var jsonObj = JSON.parse(jsonval); //parse string to json
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}


function save_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var saveFailed = false;
try {
var jsonObj = JSON.parse(jsonval); //parse
} catch (e) {
console.error(e.message);
saveFailed = true;
json_parse.parseJsonException(e.message); // throw exception
}
if(!saveFailed) {
json_parse.saveConfig(jsonval);
}
}

</script>
</body>
</html>


这其中有两个问题需注意





    1. 如果value的值是url, 格式化后缺少引号
      从json-viewer.js源码可以发现,源码中会判断value是否是url,如果是则直接输出




处理方式:在json 左右添加上双引号


    if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + '"' +json + '"' + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}




    1. 如果折叠后json-viewer会增加<a>标签,即使使用text()方法获取到纯文本数据,这里面也包含了“n items”的字符串,那么该如何去除掉这些字符串呢?




 var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();

总结


使用时只需将json文件读取,传入preview_json.html的showJson方法


编辑结束后, 点击Save 即可保存


示例代码 Android 可视化编辑json JsonPreviewer


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

线程池也会导致OOM的原因

OOM
1. 前言 我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again 相信很多人碰到过这个错误,很...
继续阅读 »

1. 前言


我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again


相信很多人碰到过这个错误,很容易从网上搜索到出现这个问题的原因是线程过多,那线程过多为什么会导致OOM?线程什么情况下会释放资源?你又能如何做到让它不释放资源?


有的人可能会想到,那既然创建线程过多会导致OOM,那我用线程池不就行了。但是有没有想过,线程池,也可能会造成OOM。其实这里有个很经典的场景,你使用OkHttp的时候不注意,每次请求都创建OkHttpClient,导致线程池过多出现OOM


2. 简单了解线程池


如何去了解线程池,看源码,直接去看是很难看得懂的,要先了解线程池的原理,对它的设计思想有个大概的掌握之后,再去看源码,就会轻松很多,当然这里只了解基础的原理还不够,还需要有一些多线程相关的基础知识。


本篇文章只从部分源码的角度去分析,线程池如何导致OOM的,而不会全部去看所有线程池的源码细节,因为太多了


首先,要了解线程池,首先需要从它的参数入手:



  • corePoolSize:核心线程数量

  • maximumPoolSize:最大线程数量

  • keepAliveTime,unit:非核心线程的存活时间和单位

  • workQueue:阻塞队列

  • ThreadFactory:线程工厂

  • RejectedExecutionHandler:饱和策略


然后你从网上任何一个地方搜都能知道它大致的工作流程是,当一个任务开始执行时,先判断当前线程池数量是否达到核心线程数,没达到则创建一个核心线程来执行任务,如果超过,放到阻塞队列中等待,如果阻塞队列满了,未达到最大线程数,创建一条非核心线程执行任务,如果达到最大线程数,执行饱和策略。在这个过程中,核心线程不会回收,非核心线程会根据keepAliveTime和unit进行回收。


**这里可以多提一嘴,这个过程用了工厂模式ThreadFactory和策略模式RejectedExecutionHandler,关于策略模式可以看我这篇文章 ** juejin.cn/post/719502…


其实从这里就可以看出为什么线程池也会导致OOM了:核心线程不会回收,非核心线程使用完之后会根据keepAliveTime和unit进行回收 ,那核心线程就会一直存活(我这不考虑shutdown()和shutdownNow()这些情况),一直存活就会占用内存,那你如果创建很多线程池,就会OOM。


所以我这篇文章要分析:核心线程不会释放资源的过程,它内部怎么做到的。 只从这部分的源码去进行分析,不会全部都详细讲。


先别急,为了照顾一些基础不太好的朋友,涉及一些基础知识感觉还是要多讲一下。上面提到的线程回收和shutdown方法这些是什么意思?线程执行完它内部的代码后会主动释放资源吗?


我们都知道开发中有个概念叫生命周期,当然线程池和线程也有生命周期(这很重要),在开发中,我们称之为lifecycle。


生命周期当然是设计这个东西的开发者所定义的,我们先看线程池的生命周期,在ThreadPoolExecutor的注释中有写:


*
* The runState provides the main lifecycle control, taking on values:
*
* RUNNING: Accept new tasks and process queued tasks
* SHUTDOWN: Don't accept new tasks, but process queued tasks
* STOP: Don't accept new tasks, don't process queued tasks,
* and interrupt in-progress tasks
* TIDYING: All tasks have terminated, workerCount is zero,
* the thread transitioning to state TIDYING
* will run the terminated() hook method
* TERMINATED: terminated() has completed
*

看得出它的生命周期有RUNNING,SHUTDOWN,STOP,TIDYING和TERMINATED。而shutdown()和shutdownNow()方法会改变生命周期,这里不是对线程池做全面解析,所以先有个大概了解就行,可以暂时理解成这篇文章的所有分析都是针对RUNNING状态下的。


看完线程池的,再看看线程的生命周期。线程的生命周期有:



  • NEW:创建,简单来说就是new出来没start

  • RUNNABLE:运行,简单来说就是start后执行run方法

  • TERMINATED:中止,简单来说就是执行完run方法或者进行中断操作之后会变成这个状态

  • BLOCKED:阻塞,就是加锁之后竞争锁会进入到这个状态

  • WAITING、TIMED_WAITING:休眠,比如sleep方法


这个很重要,需要了解,你要学会线程这块相关的知识点的话,这些生命周期要深刻理解 。比如BLOCKED和WAITING有什么不同?然后学这块又会涉及到锁那一块的知识。以后有时间可以单独写几篇这类的文章,这里先大概有个概念,只需要能先看懂后面的源码就行。


从生命周期的概念你就能知道线程执行完它内部的代码后会主动释放资源,因为它run执行完之后生命周期会到TERMINATED,那这又涉及到了一个知识点,为什么主线程(ActivityThread),执行完run的代码后不会生命周期变成TERMINATED,这又涉及到Looper,就得了解Handler机制,可以看我这篇文章 juejin.cn/post/715882…


扯远了,现在进入正题,先想想,如果是你,你怎么做让核心线程执行完run之后不释放资源,很明显,只要让它不执行到TERMINATED生命周期就行,如何让它不变成TERMINATED状态,只需要让它进入BLOCKED或者WAITING状态就行。所以我的想法是这样的,当这个核心线程执行完这个任务之后,我让它WAITING,等到有新的任务进来的时候我再唤醒它进入RUNNABLE状态。 这是我从理论这个角度去分析的做法,那看看实际ThreadPoolExecutor是怎么做的


3. 线程池部分源码分析


前面说了,不会全部都讲,这里涉及到文章相关内容的流程就是核心线程的任务执行过程,所以这里主要分析核心线程。


当我们使用线程池执行一个任务时,会调用ThreadPoolExecutor的execute方法


public void execute(Runnable command) {
......

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}

// 我们只看核心线程的流程,所以后面的代码不用管
......
}

这个ctl是一个状态相关的代码,可以先不用管,我后面会简单统一做个解释,这里不去管它会比较容易理解,我们现在主要是为了看核心线程的流程。从这里可以看出,当前线程的数量小于核心线程的话执行addWorker方法


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

这个addWorker分为上下两部分,我们分别来做解析


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

// 下半部分
......
}

这里主要是做了状态判断的一些操作,我说过状态相关的我们可以先不管,但是这里的写法我觉得要单独讲一下为什么会这么写。不然它内部很多代码是这样的,我怕劝退很多人。


首先retry: ...... break retry; 这个语法糖,平常我们开发很少用到,可以去了解一下,这里就是为了跳出循环。 其次,这里的compareAndIncrementWorkerCount内部的代码是AtomicInteger ctl.compareAndSet(expect, expect + 1) ,Atomic的compareAndSet操作搭配死循环,这叫自旋,所以说要看懂这个需要一定的java多线程相关的基础。自旋的目的是为了什么?这就又涉及到了锁的分类中有乐观锁,有悲观锁。不清楚的可以去学一下这些知识,你就知道为什么它要这么做了,这里就不一一解释。包括你看它的源码,能看到,它会很多地方用自旋,很多地方用ReentrantLock,但它就是不用synchronized ,这些都是多线程这块基础的知识,这里不多说了。


看看下半部分


private boolean addWorker(Runnable firstTask, boolean core) {

// 上半部分
......



boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
......
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
......
}
return workerStarted;
}

看到它先创建一个Worker对象,再调用Worker对象内部的线程的start方法,我们看看Worker


private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

private static final long serialVersionUID = 6138294804551838833L;

final Thread thread;
Runnable firstTask;

Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

public void run() {
runWorker(this);
}

// 其它方法
......
}

看到它内部主要有两个对象firstTask就是任务,thread就是执行这个任务的线程,而这个线程是通过getThreadFactory().newThread(this)创建出来的,这个就是我们创建ThreadPoolExecutor时传的“线程工厂”

外部调t.start();之后就会执行这里的run方法,因为newThread传了this进去,你可以先简单理解调这个线程start会执行到这个run,然后run中调用runWorker(this);


注意,你想想runWorker(this)方法,包括之后的流程,都是执行在哪个线程中?都是执行在子线程中,因为这个run方法中的代码,都是执行在这个线程中。你一定要理解这一步,不然你自己看源码会可能看懵。 因为有些人长期不接触多线程环境的情况下,你会习惯单线程的思维去看问题,那就很容易出现理解上的错误。


我们继续看看runWorker,时刻提醒你自己,之后的流程都是在子线程中进行,这条子线程的生命周期变为RUNNABLE


final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
w.lock();

// 中断相关的操作
......

try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
......
} finally {
afterExecute(task, thrown);
}
} finally {
......
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

先讲讲这里的一个开发技巧,task.run()就是执行任务,它前面的beforeExecute和afterExecute就是模板方法设计模式,方便扩展用。

执行完任务后,最后执行processWorkerExit方法


private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}

tryTerminate();

......
}

workers.remove(w)后执行tryTerminate方法尝试将线程池的生命周期变为TERMINATED


final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

先不用管状态的变化,一般一眼都能看得出这里是结束的操作了,我们追踪的核心线程正常在RUNNING状态下是不会执行到这里的。 那我们期望的没任务情况下让线程休眠的操作在哪里?

看回runWorker方法


final void runWorker(Worker w) {
......
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
......
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

看到它的while中有个getTask()方法,认真看runWorker方法其实能看出,核心线程执行完一个任务之后会getTask()拿下一个任务去执行,这就是当核心线程满的时候任务会放到阻塞队列中,核心线程执行完任务之后会从阻塞队列中拿下一个任务执行。 getTask()从抽象上来看,就是从队列中拿任务。


private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
......

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

先把timed当成正常情况下为false,然后会执行workQueue.take(),这个workQueue是阻塞队列BlockingQueue, 注意,这里又需要有点基础了。正常有点基础的人看到这里,已经知道这里就是当没有任务会让核心线程休眠的操作,看不懂的,可以先了解下什么是AQS,可以看看我这篇文章 juejin.cn/post/716801…


如果你说你懒得看,行吧,我随便拿个ArrayBlockingQueue给你举例


public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}

notEmpty是Condition,这里调用了Condition的await()方法,然后想想执行这步操作的是在哪条线程上?线程进入WAITING状态了吧,不会进入TERMINATED了吧。


然后当有任务添加之后会唤醒它,它继续在循环中去执行任务。


这就验证了我们的猜想,通过让核心线程进入WAITING状态以此来达到执行完run方法中的任务也不会主动TERMINATED而释放线程。所以核心线程一直占用资源,这里说的资源指的是空间,而cpu的时间片是会让出的。


4. 部分线程池的操作解读


为什么线程池也会导致OOM,上面已经通过源码告诉你,核心线程不会释放内存空间,导致线程池多的情况下也会导致OOM。这里为了方便新手阅读ThreadPoolExecutor相关的代码,还是觉得写一些它内部的设计思想,不然没点基础的话确实很难看懂。


首先就是状态,上面源码中都有关线程池的生命中周期状态(ctl字段),可以看看它怎么设计的


private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // Integer.SIZE是32
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

它这里用了两个设计思想,第一个就是用位来表示状态,关于这类型的设计,可以看我这2篇文章 juejin.cn/post/715547…juejin.cn/post/720550…


另外一个设计思想是:用一个变量的高位置表示状态,低位表示数量。 这里就是用高3位来表示生命周期,剩下的低位表示线程的数量。和这个类似的操作有view中的MeasureSpec,也是一个变量表示两个状态。


然后关于设计模式,可以看到它这里最经典的就是用了策略模式,如果你看饱和策略那块的源码,可以好好看看它是怎么设计的。其它的还有工厂、模板之类的,这些也不难,就是策略还是建议学下它怎么去设计的。


然后多线程相关的基础,这个还是比较重要的,这块的基础不好,看ThreadPoolExecutor的源码会相对吃力。比如我上面提过的,线程的生命周期,锁相关的知识,还有AQS等等。如果你熟悉这些,再看这个源码就会轻松很多。


对于总体的设计,你第一看会觉得它的源码很绕,为什么会这样?因为有中断操作+自旋锁+状态的设计 ,它的这种设计就基本可以说是优化代码到极致,比如说状态的设计,就比普通的能省内存,能更方便通过CAS操作。用自旋就是乐观锁,能节省资源等。有中断操作,能让整个系统更灵活。相对的缺点就是不安全,什么意思呢?已是就是这样写代码很容易出BUG,所以这里的让人觉得很绕的代码,就是很多的状态的判断,这些都是为了保证这个流程的安全。


5. 总结


从部分源码的角度去分析,得到的结论是线程池也可能会导致OOM


那再思考一个问题:不断的创建线程池,“一定”会导致OOM吗? 如果你对线程池已经有一定的了解,相信你也知道这个问题的答案。


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

当我把ChatGPT拉进群聊里,我的朋友都玩疯了

前言 近期ChatGPT可以说是太火了,问答、写论文、写诗、写代码,只要输入精确的prompt,他的表现总是让人惊喜。本着打不过就加入的原则。要是把ChatGPT拉入群聊中,会是怎样一番场景?说做就做,花了1个晚上捣鼓了一个小Demo【ChatGPT群聊助手】...
继续阅读 »

前言


近期ChatGPT可以说是太火了,问答、写论文、写诗、写代码,只要输入精确的prompt,他的表现总是让人惊喜。本着打不过就加入的原则。要是把ChatGPT拉入群聊中,会是怎样一番场景?说做就做,花了1个晚上捣鼓了一个小Demo【ChatGPT群聊助手】,凭借它的“聪明才智”,应该可以搞定我的网友、女朋友、老妈的提问...


温馨提示:如果你从没体验过ChatGPT,给大家准备了一个新手体验Demo,免注册!免登陆!免代理!!!!!!,拉到文末可以快速查看噢。


使用效果


效果可看下图


微信图片_20230308154658.png


应用前景


虽Demo仅在小范围的群聊中测试,但ChatGPT语义理解和交互能力确实强大,不仅能联系对话的上下文,还能及时纠正代码bug。不经让人想到,若能将ChatGPT应用于聊天机器人软件,完成回答问题、提供服务、甚至解决问题的任务,帮助人们解决重复性或大量的人工工作,代替传统聊天机器人应用于客服、电商、教育和金融等行业。


相对于传统聊天机器人,ChatGPT可根据用户的要求和特性,及时调整回答的策略以便更准确的回答问题,有更人性化的体验。现在被广泛使用的智能客服还不够智能,ChatGPT所具备的能力,正是客服领域所需要的。


1 准备工作


在国内无法注册ChatGPT账户,因此需要准备如下:


能接收短信的国外手机号: 只需花几块钱,使用国外虚拟号码在线接收短信。可以去一些第三方平台如:sms-activate.org/cn


国外IP: 可以通过一些工具来实现,如通过工具使用美国节点IP。


这里需要注意的是,sms-activate.org选取手机号码国家的时候,建议选择印度,如果选择印度尼西亚,会在openAI报如下错误:


You’ve made too many phone verification requests. Please try again later or contact us through our help center at help.openai.com

微信图片_20230308154704.png


以上是必须的前提工作,有了以上准备工作后,就可以去chat.openai.com/auth/login注册账号了。


2 实现思路


2.1 技术现状


chatGPT提供了基于Web版的交互界面,不便于编程式调用。于是,我们可以通过模拟浏览器去登录,然后把交互过程封装成API接口。


2.2 实现过程


ChatGPT作为一个机器人角色加入群聊,需要在PC端转发ChatGPT问答。因此,我们可以在PC电脑上完成ChatGPT接口的封装,并加入群聊。然后通过即构IM(群聊)将数据实时传输,实现群聊里面与ChatGPT聊天。


微信图片_20230308154709.png


3 PC端封装代码实现


3.1 封装chatGPT调用


我们使用chatgpt-api库来封装调用chatGPT,因此先要安装好依赖库:


npm install chatgpt

安装好chtgpt库后,使用起来就非常简单了:


var ChatGPT, ConversationId, ParentMessageId;
var API_KEY = ;//这里填写KEY
(async () => {
const { ChatGPTAPI } = await import('chatgpt');
ChatGPT = new ChatGPTAPI({ apiKey: API_KEY})
})();
//向ChatGPT发出提问
function chat(text, cb) {
console.log("正在向ChatGPT发送提问:",text)
ChatGPT.sendMessage(text, {
conversationId: ConversationId,
parentMessageId: ParentMessageId
}).then(
function (res) {
ConversationId = res.conversationId
ParentMessageId = res.id
cb && cb(true, res.text)
console.log(res)
}
).catch(function (err) {
cb && cb(false, err);
});
}

注意到,在第二行需要填写API_KEY,登录OpenAI后,打开链接platform.openai.com/account/api…即可获取,如下图所示


微信图片_20230308154713.png


3.2 收发群聊消息


关于即构IM,如果大家感兴趣可以进入官网doc-zh.zego.im了解更多。总所周知,在即时聊天和实时音视频方面,即构IM是个人开发者或者中小型企业首选。因为我们只关注一对一私聊或者群聊,因此,在官方提供的SDK的基础上,我们做了二次封装。具体的封装代码请看附件,这里只贴出封装后的使用代码:


const Zego = require('./zego/Zego.js');

var zim;
function onError(err) {
console.log("on error", err);
}
//发送消息
function sendZegoMsg(isToGroup, text, toID){
Zego.sendMsg(zim, isToGroup, text, toID, function (succ, err) {
if (!succ) {
console.log("回复即构消息发送失败:", msg, err);
}
})
}
//收到消息回调
function onRcvZegoMsg(isFromGroup, msg, fromUID) {
var rcvText = msg.message ;

}
function main() {
let zegoChatGPTUID = "chatgpt"
zim = Zego.initZego(onError, onRcvZegoMsg, zegoChatGPTUID);

}
main();

在收到消息时,判断是否有@chatgpt关键字,如果有的话提取消息内容,然后去调用chatGPT封装好的接口等待ChatGPT回复,并将回复的内容往聊天群里发送。


4 手机端加入群聊与ChatGPT聊天


有了PC端实现后,接下来在手机端只需通过即构IM SDK向群里面@chatgpt发送提问消息即可,当然了,也可以在一对一私聊的时候@chatgpt然后调用chatGPT接口。这些都是可以根据实际需求定制开发,篇幅原因,这里我们只将群聊。


同样的,我们只关注收发消息,因此对即构官方提供的SDK做了二次封装。如果想了解更多细节可以前往官方文档阅读。


对登录ZIM、创建Token等代码这里不详细描述,感兴趣读者可以查看代码附件,代码很简单容易看懂。


首先封装Msg对象,表示消息实体类:


public class Msg {
public String msg;
public long time;
public String toUID;
public String fromUID;
public MsgType type;

public enum MsgType {
P2P,
GROUP
}
}

发送消息二次封装,同一群聊和一对一聊天接口:


public static void sendMsg(ZIM zim, Msg msg, ZIMMessageSentCallback cb) {
// 发送“单聊”通信的信息

ZIMTextMessage zimMessage = new ZIMTextMessage();
zimMessage.message = msg.msg;

ZIMMessageSendConfig config = new ZIMMessageSendConfig();
// 消息优先级,取值为 低:1 默认,中:2,高:3
config.priority = ZIMMessagePriority.LOW;
// 设置消息的离线推送配置
ZIMPushConfig pushConfig = new ZIMPushConfig();
pushConfig.title = "离线推送的标题";
pushConfig.content = "离线推送的内容";
pushConfig.extendedData = "离线推送的扩展信息";
config.pushConfig = pushConfig;
if (msg.type == Msg.MsgType.P2P)
zim.sendPeerMessage(zimMessage, msg.toUID, config, cb);
else
zim.sendGroupMessage(zimMessage, msg.toUID, config, cb);
}

二次封装接收消息,统一通过onRcvMsg函数接收消息。


private void onRcvMsg(ArrayList<ZIMMessage> messageList) {
if (lsArr == null) return;
for (ZIMMessage zimMessage : messageList) {
if (zimMessage instanceof ZIMTextMessage) {
ZIMTextMessage zimTextMessage = (ZIMTextMessage) zimMessage;
if (zimMessage.getTimestamp() < this.startTime)
continue;
String fromUID = zimTextMessage.getSenderUserID();
ZIMConversationType ztype = zimTextMessage.getConversationType();
String toUID = zimTextMessage.getConversationID();
Msg.MsgType type = Msg.MsgType.P2P;
if (ztype == ZIMConversationType.PEER) type = Msg.MsgType.P2P;
else if (ztype == ZIMConversationType.GROUP) type = Msg.MsgType.GROUP;
String data = zimTextMessage.message;
Msg msg = new Msg(type, data, zimMessage.getTimestamp(), fromUID, toUID);
for (MsgCenterListener l : lsArr) l.onRcvMsg(msg);
}
}
}
private ZIMEventHandler handler = new ZIMEventHandler() {

@Override
public void onReceivePeerMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromUserID) {
onRcvMsg(messageList);
}



@Override
public void onReceiveGroupMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromGroupID) {
onRcvMsg(messageList);
}

@Override
public void onTokenWillExpire(ZIM zim, int second) {
onRenewToken();
}
};

需要注意的是,因为我们目前场景只需关注文本消息,因此没有图片、文件之类的消息做过多考虑。如果有类似需求的读者可以根据官方文档进一步封装。


另外,为了简化,避免每次用户主动拉chatgpt进入一个新群,我们先约好一个超大群ID:group_chatgpt。每次新用户登录就加入这个大群就好。如果有更加细粒度控制需求,可以根据不同用户来创建不同群,然后向chatgpt机器人发送群ID,在PC端开发对应的自动加入对应群功能就好。


对于加群逻辑,也做了二次封装:


public void joinGroup(String groupId) {
zim.joinGroup(groupId, new ZIMGroupJoinedCallback() {
@Override
public void onGroupJoined(ZIMGroupFullInfo groupInfo, ZIMError errorInfo) {
for (MsgCenterListener l : lsArr)
l.onJoinGroup(groupId);
}
});

至此,整个流程开发完成,尽情享受ChatGPT吧。


5 开发者福利


除ChatGPT之外,Demo中使用的开发者工具ZIM SDK也是提升工作效率的利器,ZIM SDK提供了全面的 IM 能力,满足文本、图片、语音等多种消息类型,在线人数无上限,支持亿量级消息并发。同时支持安全审核机制,确保消息安全合规。


ZIM SDK提供了快速集成、接口丰富、成熟的即时通讯解决方案。满足多种业务场景通讯需求,适用于打造大型直播、语聊房、客服系统等场景。即构即时通讯产品 IM 开春钜惠低至1折,限时折扣专业版1200元http://www.zego.im/activity/ze…,也可搭配元宇宙和直播间其他产品组合使用。感兴趣的开发者可到即构官网去注册体验doc-zh.zego.im/article/115…


6 完整代码



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

我竟然突然焦虑,并且迷茫了

【随想录】我尽然突然焦虑,并且迷茫了 「随想录」 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 最近是怎么了 最近几个朋友,突然询问我,现在应该...
继续阅读 »

【随想录】我尽然突然焦虑,并且迷茫了



「随想录」


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



最近是怎么了


最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。



所以说我真的没有一个准确的答案回复。但是我以为目前的眼光来看一份好工作必备的条件就是,我在这个领域学的越多,我的工资和个人发展瓶颈越高,这份工作是一个持续学习的过程,并且回报和提高是肉眼可见的!



回忆那个时候


其实说实话,这个疑惑我上大一就开始有,但是那个时候是从高考的失落中寻找升学的路径,开始无脑的刷那种考研短视频



(看过可能都知道真的一下子励志的心就有了,但是回到现实生活中,看到身边人的状态~~~没错人就是一个从众的种群,你可能会问你会不会因为大一没有那么努力学习而后悔,但是其实我不会,因为那一年我的经历也是我最开心大学生活,虽然也干了很多被室友做成梗的糗事,但是想一想那不就是青春嘛,要是从小就会很有尺度的为人处世,想一想活着也很累嘛,害,浅浅致敬一下充满快乐和遗憾的青春呀!)


个人看法


哈哈,跑题了。给大家点力量把!前面满满的焦虑。其实我感觉我们都应该感谢我们来到计算机类的专业,从事这方面的学习和研究。


因为计算机的扩展性,不得不说各行各业都开始越来越喜欢我们计算机毕业的大学生(就业方向更加广),我也因为自己会计算机,成功进入一个一本高校以上的教育类公司实习(同时也是这个时候知道了更多优秀学校的毕业年轻人,真正认识到学校的层次带给人的很多东西真正的有差距);



虽然我是二本的学生,但是在亲戚朋友眼里,虽然学校比不上他们的孩子,但是计算机专业也能获得浅浅的也是唯一一点可以骄傲的东西(活在别人嘴这种思考方式肯定不是对的,但是现实就是在父母那里,我们考上什么大学和进入了哪里工作真的是他们在外人的脸面,这种比较情况在大家族或者说农村尤为严重);



技术论打败学校论,计算机专业是在“广义”上为数不多能打破学校出身论的学科,在公司上只要你能干活,公司就愿意要你,这个时候肯定有人diss我,现在培训班出来的很多都找不到工作呀,我的回答只能是:的确,因为这个行业的红利期展示达到了瓶颈期,加上大环境的不理想,会受到一些影响,但是我还是相信会好的,一切都会好的。



做技术既然这样了


关于最近论坛上说“前段已死”“后端当牛做马”“公司磨刀霍霍向测试”......



这个东西怎么说,我想大部分人看到这个都会被这个方向劝退,我从两个角度分析一下,上面说了,真滴卷,简历真滴多,存在过饱和;第二点,希望这个领域新人就不要来了,就是直接劝退,被让人来卷,狭义上少卷一些......



现在就是导致我也不敢给朋友做建议了,因为当他看到这些的时候,和进入工作环境真的不好,我真的怕被喷死


包括现在我的实习,大家看我的朋友圈看出工作环境不错很好,但是和工作的另一面,是不能发的呀,有时候我都笑称自己是“产业工人”(这个词是一个朋友调侃我的)


不行了,在传播焦虑思想,我该被喷死了,现在我给建议都变得很含蓄,因为时代红利期真的看不透,我也不敢说能维持多少年,而且我工作也一般,我不敢耽误大家(哈哈哈,突然想起一句话,一生清贫怎敢入繁华,二袖清风怎敢误佳人,又是emo小文案,都给我开E)


个人总结


本文就是调侃一下现在的环境啊,下面才是重点,只有干活和真话放在后面(印证一个道理:看到最后的才是真朋友才敢给真建议,我也不怕被骂)



心态方面:我们这个年纪就是迷茫的年纪,迷茫是一种正常的状态,因为作为一名成年人你真正在思考你的个人发展的状态,所以请把心放大,放轻松,你迷茫了已经比身边的人强太多了,如果真正焦虑的不能去学习了,去找个朋友聊一聊,实在不行,drink个两三瓶,好好睡一觉,第二天继续干,这摸想,这些都算个啥,没事你还有我,实在不行微我聊一聊,我永远都在,我的朋友!



工作方面:俗话说:女怕入错行,男怕娶错人!(突然发现引用没什么用,哈哈)我们可以多去实践,没错就是去实习,比如你想做前端的工作,你就可以直接去所在的城市(推荐省会去找实习)但是朋友其实实习很难,作为过来人,我能理解你,一个人在陌生的城市而且薪资很可怜,面对大城市的租房和吃饭有很多大坑,你要一一面对,但是在外面我们真要学会保护自己,而且实习生活中经济方面肯定要父母支持,所以一定要和父母好好沟通,其实你会发现我们越长大,和父母相处的时光越短。(我今年小年和十五都没在家过,害,那种心理苦的滋味很不好受)



升学方面:不是每一个都适合考研,不要盲从考研。但是这句话又是矛盾的,在我的实习生涯中,学历问题是一个很重要的问题,我们的工作类型真的不同,还是那句话,学历只是一个门槛,只要你迈入以后看的是你的个人能力。说一句悄悄话,我每天工作,最想的事情就是上学,心想老子考上研,不在干这活了,比你们都强。所以你要想考研,请此刻拿出你的笔,在纸上写下你要考研主要三个理由,你会更好的认识自己,更好选择。



好吧,今天的随想录就这摸多,只是对最近看文章有了灵感写下自己的看法,仅供参考哦!


回答问题


回应个问题:很多朋友问我为什么给这摸无私的建议,这是你经历了很多才得到的,要是分享出去,不是很亏?


(你要这摸问,的确你有卷到我的可能性,快给我爬。哈哈哈)可能是博客圈给的思想把,其实我说不上开源的思想,但是我遇到的人对我都是无私分享自己的经验和自己走过的坑,就是你懂吗,他们对我帮助都很大,他们在我眼里就是伟大的人,所以我也想要跟随他们,做追光的人!(上价值了哦,哈哈)



写在最后


最后一句话,迷茫这个东西,走着走着就清晰了,迷茫的时候,搞一点学习总是没错的。


作者:武师叔
来源:juejin.cn/post/7201752978259378232
收起阅读 »

25岁在培训班学java个人看法见解

前言 hello,大家好,我是小江。前段时间我发了一篇我在培训java感受的文章,大家都挺感兴趣的,也问了很多问题。这段时间我们刚好在写项目,昨天项目已经结束验收,现在有空在写一篇文章解答你们问的各种问题。首先说明,文章仅代表我个人观点,我个人的真实感受,而且...
继续阅读 »

前言


hello,大家好,我是小江。前段时间我发了一篇我在培训java感受的文章,大家都挺感兴趣的,也问了很多问题。这段时间我们刚好在写项目,昨天项目已经结束验收,现在有空在写一篇文章解答你们问的各种问题。首先说明,文章仅代表我个人观点,我个人的真实感受,而且我不会指明说出任何一个培训机构名,上篇文章大家要是看了应该知道,我全篇没有说任何一个培训机构名,我自己所在的也不会说。好了不多说,开始解答常见的问题吧。


1、行情不好了,还能入这行吗?


说实话,这个问题我也不知道怎么回答。确实现在行情越来越差了,要求越来越高了,学完找不到工作的肯定是有的,但我觉得吧,海投一直找不到工作的,要么就是技术太差,要么就是要求高了。可能前几年这技术和学历可以轻松找到1w的,现在技术要求高了点还只给6k,落差太大,就会不想去,然后发现其他的都是这样了,就抱怨起来了贩卖焦虑了。


为什么之前那些年好找工作,要求又低工资又高,只是躺在了时代的红利上。人都不是傻子,知道什么行业赚钱,必然会往这行拥入。人多了,那肯定是这样的结果。那到底现在能不能入这行呢?我个人觉得吧,你需要满足:


①学历要达到大专以上,最好是本科;

②年龄不要超过28岁,最好是26岁以下;

③自己能坚持,如果学技术要学好,能放下游戏等各种娱乐;


这是我个人看法,不知道大家有没有不同的意见,我认为一个24岁左右本科学历的刻苦学完编程,技术也可以,找个开发工作问题不大的。


2、入行是自学还是报班?


这个问题我不会提到任何一个培训机构名字,以免大家认为我是托。我来说说我的看法,我认为自制力强的、学习力OK的、时间充足的,完全可以自学的,我身边也有自学朋友。我入这行还是受我那朋友的启发和推荐的,我和我那朋友之前是在一个公司上班,不是it,干了大半年,他辞职了,说要搞编程,我没在意,觉得编程离我太远和我没有交集,又是将近半年过去了,我和他联系,他说他已经上班干开发了税后除去社保7K,我才开始和他聊这行,我也心动了。他说他自己就是自学的,大学专业搭点边,学了一点编程,但都忘了。他说他辞职后在家天天学习,除了吃饭就是学习,这点我是相信的,之前和他一个公司他也确实做事很认真。我后来不是也准备入行编程,我也想着学他自学,真正开始后,才知道人和人是有差距的,我心静不下,学一个小时就看不下去,所以我后来报了班。


这个问题,我的总结就是:

①自学可以也可行,关键看你有没有那毅力,每天能学8小时以上,并且是真正有效率的学了8小时,那你可以自学,网上资料都全,不会的可以查资料百度,或者花点小钱让别的大神帮你看看。


②自制力不行,又想入这行的,培训班也是一个选择,只是学费不便宜,成本大。培训班肯定是有用的,能学到东西,系统化都给你规划好了,资源齐全,讲的也细,也会讲很多企业实际开发需要注意的东西,练习题和项目都恰到好处的提升自己,不会不懂的可以随时问老师,最后模拟面试、指导简历什么都安排好了,确实能做到速成。不然也不会这么多培训出来的占满了这行业,使这行越来越卷。


③如果报班了,最好选个靠外那座位坐,不要坐里面,我就是坐里面靠墙,问老师问题都不好问,很坑。如果同桌是个技术很好的,那你就很幸运了,一个技术好的同桌太重要了。还有就是多问老师问题,你是花钱的,有问题就问,不要怕。


3、我在培训这两个多月后现在学的咋样?


我这一共四个阶段,我现在刚好学完第二个阶段,第一阶段基础javase,第二阶段javaweb,刚刚结束第二阶段做完项目,每个阶段结束都会有将近一个星期写项目。这次项目是小组一起开发,使用SVN工具小组一起自己分工一个写一个模块,最后整合,模拟企业开发。项目还可以,刚开始觉得很难,熟悉后就好了,难点不多,大都是crud,我感觉思路很重要,不要一上来就噼里啪啦开始写,先分析好,后面会越来越顺。等写完真的有成就感,项目确实很能锻炼自己,升华自己,查漏补缺。放几张照片看看我写的项目吧


页面显示效果截选:


image.png


后端代码截选:
image.png
前端代码截选:
image.png


结束语


我相信大家入行这个的,应该都和我一样,觉得工资还可以,工作看起来还体面。但入这行越久了解越深,就知道也有很多不容易,要付出很大的努力。如果刚踏入这行开始学的,一定要好好学,基础蛮重要,可能学着学着会迷茫,不要怀疑自己,多学多敲多思考,没有学不会的。最后送大家一句诗:路漫漫其修远兮,吾将上下而求索。


作者:学习编程的小江
来源:juejin.cn/post/7203294201312657469
收起阅读 »

改行后我在做什么?(2022-9-19日晚)

闲言碎语 今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。 但在我这个年纪,这个阶段,看似有很多选择...
继续阅读 »

闲言碎语



今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。




但在我这个年纪,这个阶段,看似有很多选择,但其实我没有选择。能做的也只是多挣点钱。




在这个信息爆炸的时代,我们知道更高的地方在哪里。但当你想要再往上走一步的时候,你发现你的上限,其实从出生或从你毕业的那一刻就已经注定了。可能少部分人通过自身的努力,的确能突破壁垒达到理想的高度。但这只是小概率事件罢了。在我看来整个社会的发展,其实早就已经陷入了一种怪圈。




在我,早些年刚刚进入社会的时候。那时的想法特别简单。就想着努力工作,努力提升自身的专业素养。被老板赏识,升职加薪成为一名管理者。如果,被淘汰了那应该是自己不够优秀,不够努力,专业技能不过硬,自己为人处事不够圆滑啥的。




内卷这个词语引爆网络的时候;当35岁被裁员成为常态的时候。再回头看我以前的那些想法那真的是一个笑话。(我觉得我可能是在为自己被淘汰找借口)



当前的状态



游戏工作室的项目,目前基本处于停滞的状态。我不敢加机器也不敢关机。有时候我都在想,是不是全中国那3-4亿的人都在搞这个?一个国外的游戏,金价直接拉成这个逼样。




汽配这边的话,只能说喝口稀饭。(我花了太多精力在游戏工作室上了)



梦想破灭咯



其实按照正常情况来说,游戏工作室最开始的阶段,我应该是能够稍微挣点钱的。我感觉我天时、地利、人和。我都占的。现在来看的话,其实我只占了人和。我自己可以编码,脚本还是从驱动层模拟键鼠,写的一套脚本。这样我都没赚钱,我擦勒。



接下来干嘛



接下来准备进厂打螺丝。(开玩笑的)
还是老老实实跟着我弟学着做生意吧。老老实实做汽配吧!在这个时代,好像有一技之长(尤其是IT)的人,好像并不能活得很好。除非,你这一技之长,特别特别长。(当下的中国不需要太多的这类专业技术人员吧。)



我感受到的大环境



我身边有蛮多的大牛。从他们的口中和我自己看到的。我感觉在IT这个领域,国内的环境太恶劣了。在前端,除开UI库,我用到的很多多的库全是老外的。为什么没有国人开源呢?因为,国人都忙着996了。我们可以在什么都不知道的情况下,通过复制粘贴,全局搜索解决大部分问题。 机械视觉、大数据分析、人工智能 等很多东西。这一切的基石很多年前就有了,为什么没人去研究他?为什么我们这波人,不断的在学习:这样、那样的框架。搭积木虽然很好玩。但创造一个积木,不应该也是一件更有挑战性的事情么?




在招聘网站还有一个特别奇怪的现象。看起来这家公司是在招人,但其实是培训机构。 看起来这家公司正儿八经是在招聘兼职,但其实只想骗你去办什么兼职卡。看起来是在招送快递,送外卖的,招聘司机的,但其实只是想套路你买车。我擦勒。这是怎样的一个恶劣的生存环境。这些个B人就不能干点,正经事?




卖菜的、拉车的、搞电商的、搞短视频、搞贷款的、卖保险的、这些个公司市值几百亿。很难看到一些靠创新,靠创造,靠产品质量,发展起来的公司。


作者:wjt
来源:juejin.cn/post/7144770465741946894

收起阅读 »

从framework角度看app保活问题

问题背景 最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论 不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发...
继续阅读 »

问题背景


最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论


保活


不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发者,根本无能为力,可真的是这样的吗?


保活方案


首先,我整理了从古到今,app开发者所使用过的以及当前还在使用的保活方式,主要思路有两个:保活和复活


保活的方案有:



  • 1像素惨案




  • 后台无声音乐




  • 前台service




  • 心跳机制




  • socket长连接




  • 无障碍服务




  • ......




复活的方案有:


  • 双进程守护(java层和native层)

  • JobScheduler定时任务

  • 推送/相互唤醒

  • ......


不难看出,app开发者为了能让自己的应用多存活一会儿,可谓是绞尽脑汁,但即使这样,随着Android系统升级,尤其是进入8.0之后,系统对应用的限制越来越高,传统的保活方式已经不生效,这让Android开发者手足无措,于是乎,出现了一种比较和谐的保活方式:



  • 引导用户开启手机白名单


这也是目前绝大多数应用所采用的的方式,相对于传统黑科技而言,此方式显得不那么流氓,比较容易被用户所接受。


但跟微信这样的国民级应用比起来,保活效果还是差了一大截,那么微信是怎么实现保活的呢?或者回到我们开头的问题,应用的生死真的只能靠系统调度吗?开发者能否干预控制呢?


进程调度原则


解开这个疑问之前,我们需要了解一下Android系统进程调度原则,主要介绍framework中承载四大组件的进程是如何根据组件状态而动态调节自身状态的。进程有两个比较重要的状态值:




  • oom_adj,定义在frameworks/base/services/core/java/com/android/server/am/ProcessList.java当中




  • procState,定义在frameworks/base/core/java/android/app/ActivityManager.java当中




OOM_ADJ

以Android10的源码为例,oom_adj划分为20级,取值范围[-10000,1001],Android6.0以前的取值范围是[-17,16]




  • oom_adj值越大,优先级越低




  • oom_adj<0的进程都是系统进程。




public final class ProcessList {
static final String TAG = TAG_WITH_CLASS_NAME ? "ProcessList" : TAG_AM;

// The minimum time we allow between crashes, for us to consider this
// application to be bad and stop and its services and reject broadcasts.
static final int MIN_CRASH_INTERVAL = 60 * 1000;

// OOM adjustments for processes in various states:

// Uninitialized value for any major or minor adj fields
static final int INVALID_ADJ = -10000;

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 1001;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;

// This is the oom_adj level that we allow to die first. This cannot be equal to
// CACHED_APP_MAX_ADJ unless processes are actively being assigned an oom_score_adj of
// CACHED_APP_MAX_ADJ.
static final int CACHED_APP_LMK_FIRST_ADJ = 950;

// Number of levels we have available for different service connection group importance
// levels.
static final int CACHED_APP_IMPORTANCE_LEVELS = 5;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 800;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app. This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 700;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 600;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 500;

// This is a process with a heavy-weight application. It is in the
// background, but we want to try to avoid killing it. Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 400;

// This is a process currently hosting a backup operation. Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 300;

// This is a process bound by the system (or other app) that's more important than services but
// not so perceptible that it affects the user immediately if killed.
static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 200;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 100;
static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

// This is a process that was recently TOP and moved to FGS. Continue to treat it almost
// like a foreground app for a while.
// @see TOP_TO_FGS_GRACE_PERIOD
static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

// This is the process running the current foreground app. We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -700;

// This is a system persistent process, such as telephony. Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -800;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -900;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -1000;

// Memory pages are 4K.
static final int PAGE_SIZE = 4 * 1024;

//省略部分代码
}

ADJ级别取值说明(可参考源码注释)
INVALID_ADJ-10000未初始化adj字段时的默认值
UNKNOWN_ADJ1001缓存进程,无法获取具体值
CACHED_APP_MAX_ADJ999不可见activity进程的最大值
CACHED_APP_MIN_ADJ900不可见activity进程的最小值
CACHED_APP_LMK_FIRST_ADJ950lowmemorykiller优先杀死的级别值
SERVICE_B_ADJ800旧的service的
PREVIOUS_APP_ADJ700上一个应用,常见于应用切换场景
HOME_APP_ADJ600home进程
SERVICE_ADJ500创建了service的进程
HEAVY_WEIGHT_APP_ADJ400后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ300备份进程
PERCEPTIBLE_LOW_APP_ADJ250受其他进程约束的进程
PERCEPTIBLE_APP_ADJ200可感知组件的进程,比如背景音乐播放
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ50最近运行的后台进程
FOREGROUND_APP_ADJ0前台进程,正在与用户交互
PERSISTENT_SERVICE_ADJ-700系统持久化进程已绑定的进程
PERSISTENT_PROC_ADJ-800系统持久化进程,比如telephony
SYSTEM_ADJ-900系统进程
NATIVE_ADJ-1000native进程,不受系统管理

可以通过cat /proc/进程id/oom_score_adj查看目标进程的oom_adj值,例如我们查看电话的adj


dialer_oom_adj


值为935,处于不可见进程的范围内,当我启动电话app,再次查看


dialer_oom_adj_open


此时adj值为0,也就是正在与用户交互的进程


ProcessState

process_state划分为23类,取值范围为[-1,21]


@SystemService(Context.ACTIVITY_SERVICE)
public class ActivityManager {
//省略部分代码
/** @hide Not a real process state. */
public static final int PROCESS_STATE_UNKNOWN = -1;

/** @hide Process is a persistent system process. */
public static final int PROCESS_STATE_PERSISTENT = 0;

/** @hide Process is a persistent system process and is doing UI. */
public static final int PROCESS_STATE_PERSISTENT_UI = 1;

/** @hide Process is hosting the current top activities. Note that this covers
* all activities that are visible to the user. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_TOP = 2;

/** @hide Process is hosting a foreground service with location type. */
public static final int PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3;

/** @hide Process is bound to a TOP app. This is ranked below SERVICE_LOCATION so that
* it doesn't get the capability of location access while-in-use. */

public static final int PROCESS_STATE_BOUND_TOP = 4;

/** @hide Process is hosting a foreground service. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 5;

/** @hide Process is hosting a foreground service due to a system binding. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 6;

/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 7;

/** @hide Process is important to the user, but not something they are aware of. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 8;

/** @hide Process is in the background transient so we will try to keep running. */
public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 9;

/** @hide Process is in the background running a backup/restore operation. */
public static final int PROCESS_STATE_BACKUP = 10;

/** @hide Process is in the background running a service. Unlike oom_adj, this level
* is used for both the normal running in background state and the executing
* operations state. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_SERVICE = 11;

/** @hide Process is in the background running a receiver. Note that from the
* perspective of oom_adj, receivers run at a higher foreground level, but for our
* prioritization here that is not necessary and putting them below services means
* many fewer changes in some process states as they receive broadcasts. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_RECEIVER = 12;

/** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
public static final int PROCESS_STATE_TOP_SLEEPING = 13;

/** @hide Process is in the background, but it can't restore its state so we want
* to try to avoid killing it. */

public static final int PROCESS_STATE_HEAVY_WEIGHT = 14;

/** @hide Process is in the background but hosts the home activity. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_HOME = 15;

/** @hide Process is in the background but hosts the last shown activity. */
public static final int PROCESS_STATE_LAST_ACTIVITY = 16;

/** @hide Process is being cached for later use and contains activities. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_CACHED_ACTIVITY = 17;

/** @hide Process is being cached for later use and is a client of another cached
* process that contains activities. */

public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 18;

/** @hide Process is being cached for later use and has an activity that corresponds
* to an existing recent task. */

public static final int PROCESS_STATE_CACHED_RECENT = 19;

/** @hide Process is being cached for later use and is empty. */
public static final int PROCESS_STATE_CACHED_EMPTY = 20;

/** @hide Process does not exist. */
public static final int PROCESS_STATE_NONEXISTENT = 21;
//省略部分代码
}

state级别取值说明(可参考源码注释)
PROCESS_STATE_UNKNOWN-1不是真正的进程状态
PROCESS_STATE_PERSISTENT0持久化的系统进程
PROCESS_STATE_PERSISTENT_UI1持久化的系统进程,并且正在操作UI
PROCESS_STATE_TOP2处于栈顶Activity的进程
PROCESS_STATE_FOREGROUND_SERVICE_LOCATION3运行前台位置服务的进程
PROCESS_STATE_BOUND_TOP4绑定到top应用的进程
PROCESS_STATE_FOREGROUND_SERVICE5运行前台服务的进程
PROCESS_STATE_BOUND_FOREGROUND_SERVICE6绑定前台服务的进程
PROCESS_STATE_IMPORTANT_FOREGROUND7对用户很重要的前台进程
PROCESS_STATE_IMPORTANT_BACKGROUND8对用户很重要的后台进程
PROCESS_STATE_TRANSIENT_BACKGROUND9临时处于后台运行的进程
PROCESS_STATE_BACKUP10备份进程
PROCESS_STATE_SERVICE11运行后台服务的进程
PROCESS_STATE_RECEIVER12运动广播的后台进程
PROCESS_STATE_TOP_SLEEPING13处于休眠状态的进程
PROCESS_STATE_HEAVY_WEIGHT14后台进程,但不能恢复自身状态
PROCESS_STATE_HOME15后台进程,在运行home activity
PROCESS_STATE_LAST_ACTIVITY16后台进程,在运行最后一次显示的activity
PROCESS_STATE_CACHED_ACTIVITY17缓存进程,包含activity
PROCESS_STATE_CACHED_ACTIVITY_CLIENT18缓存进程,且该进程是另一个包含activity进程的客户端
PROCESS_STATE_CACHED_RECENT19缓存进程,且有一个activity是最近任务里的activity
PROCESS_STATE_CACHED_EMPTY20空的缓存进程,备用
PROCESS_STATE_NONEXISTENT21不存在的进程

进程调度算法

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java中,有三个核心方法用于计算和更新进程的oom_adj值



  • updateOomAdjLocked():更新adj,当目标进程为空,或者被杀则返回false,否则返回true。

  • computeOomAdjLocked():计算adj,计算成功返回true,否则返回false。

  • applyOomAdjLocked():应用adj,当需要杀掉目标进程则返回false,否则返回true。


adj更新时机

也就是updateOomAdjLocked()被调用的时机。通俗的说,只要四大组件被创建或者状态发生变化,或者当前进程绑定了其他进程,都会触发adj更新,具体可在源码中查看此方法被调用的地方,比较多,这里就不列举了


adj的计算过程

computeOomAdjLocked()计算过程相当复杂,将近1000行代码,这里就不贴了,有兴趣可自行查看,总体思路就是根据当前进程的状态,设置对应的adj值,因为状态值很多,所以会有很多个if来判断每个状态是否符合,最终计算出当前进程属于哪种状态。


adj的应用

计算得出的adj值将发送给lowmemorykiller(简称lmk),由lmk来决定进程的生死,不同的厂商,lmk的算法略有不同,下面是源码中对lmk的介绍


/* drivers/misc/lowmemorykiller.c
*
* The lowmemorykiller driver lets user-space specify a set of memory thresholds
* where processes with a range of oom_score_adj values will get killed. Specify
* the minimum oom_score_adj values in
* /sys/module/lowmemorykiller/parameters/adj and the number of free pages in
* /sys/module/lowmemorykiller/parameters/minfree. Both files take a comma
* separated list of numbers in ascending order.
*
* For example, write "0,8" to /sys/module/lowmemorykiller/parameters/adj and
* "1024,4096" to /sys/module/lowmemorykiller/parameters/minfree to kill
* processes with a oom_score_adj value of 8 or higher when the free memory
* drops below 4096 pages and kill processes with a oom_score_adj value of 0 or
* higher when the free memory drops below 1024 pages.
*
* The driver considers memory used for caches to be free, but if a large
* percentage of the cached memory is locked this can be very inaccurate
* and processes may not get killed until the normal oom killer is triggered.
*
* Copyright (C) 2007-2008 Google, Inc.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/


保活核心思路


根据上面的Android进程调度原则得知,我们需要尽可能降低app进程的adj值,从而减少被lmk杀掉的可能性,而我们传统的保活方式最终目的也是降低adj值。而根据adj等级分类可以看出,通过应用层的方式最多能将adj降到100~200之间,我分别测试了微信、支付宝、酷狗音乐,启动后返回桌面并息屏,测试结果如下


微信测试结果:


weixin_oom_adj


微信创建了两个进程,查看这两个进程的adj值均为100,对应为adj等级表中的VISIBLE_APP_ADJ,此结果为测试机上微信未登录状态测试结果,当换成我的小米8测试后发现,登录状态下的微信有三个进程在运行


weixin_login_oom_adj


后查阅资料得知,进程名为com.tencent.soter.soterserver的进程是微信指纹支付,此进程的adj值居然为-800,上面我们说过,adj小于0的进程为系统进程,那么微信是如何做到创建一个系统进程的,我和我的小伙伴都惊呆了o.o,为此,我对比了一下支付宝的测试结果


支付宝测试结果:


alipay_oom_adj


支付宝创建了六个进程,查看这六个进程的adj值,除了一个为915,其余均为0,怎么肥事,0就意味着正在与用户交互的前台进程啊,我的世界要崩塌了,只有一种可能,支付宝通过未知的黑科技降低了adj值。


酷狗测试结果:


kugou_oom_adj.png


酷狗创建了两个进程,查看这两个进程的adj值分别为700、200,对应为adj等级表中的PREVIOUS_APP_ADJPERCEPTIBLE_APP_ADJ,还好,这个在意料之中。


测试思考


通过上面三个app的测试结果可以看出,微信和支付宝一定是使用了某种保活手段,让自身的adj降到最低,尤其是微信,居然可以创建系统进程,简直太逆天了,这是应用层绝对做不到的,一定是在native层完成的,但具体什么黑科技就不得而知了,毕竟反编译技术不是我的强项。


正当我郁郁寡欢之时,我想起了前两天看过的一篇文章《当 App 有了系统权限,真的可以为所欲为?》,文章讲述了第三方App如何利用CVE漏洞获取到系统权限,然后神不知鬼不觉的干一些匪夷所思的事儿,这让我茅塞顿开,或许这些大厂的app就是利用了系统漏洞来保活的,不然真的就说不通了,既然都能获取到系统权限了,那创建个系统进程不是分分钟的事儿吗,还需要啥厂商白名单。


总结


进程保活是一把双刃剑,增加app存活时间的同时牺牲的是用户手机的电量,内存,cpu等资源,甚至还有用户的忍耐度,作为开发者一定要合理取舍,不要为了保活而保活,即使需要保活,也尽量采用白色保活手段,别让用户手机变板砖,然后再来哭爹骂娘。


参考资料:


探讨Android6.0及以上系统APP常驻内存(保活)实现-争宠篇


探讨Android6.0及以上系统APP常驻内存(保活)实现-复活篇


探讨一种新型的双进程守护应用保活


史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术


当 App 有了系统权限,真的可以为所欲为?


「 深蓝洞察 」2022 年度最“不可赦”漏洞


作者:小迪vs同学
来源:juejin.cn/post/7210375037114138680
收起阅读 »

上千行代码的输入框的逻辑是什么?

web
需求 我们要做一个前端需求:需要一个输入框,支持 KQL 语法,支持智能匹配,前提条件纯前端实现。 该功能详见 kibana es7版本。有条件的可以去使用一下,感受一番。 需求分析 使用了一下该功能,感觉还是挺复杂的。不好实现啊,我,我,我。。。 不过因为...
继续阅读 »

需求


我们要做一个前端需求:需要一个输入框,支持 KQL 语法,支持智能匹配,前提条件纯前端实现。


该功能详见 kibana es7版本。有条件的可以去使用一下,感受一番。


image.png


需求分析


使用了一下该功能,感觉还是挺复杂的。不好实现啊,我,我,我。。。


不过因为 kibana 是开源的,我就去 github 上看了一下源码。



  • 首先人家是 React 版本,我的项目是 Vue 版本,我不能行使拿来主义。

  • 一个 input 框的核心代码写了一千多行,不包括一些工具函数,公共组件之类。


方案




  1. 我先研究源码,再把研究好的源码转成 vue 版本输出?


    该方案短时间内看不到效果,需要好好梳理其源码。是一个 0 或者 1 的问题,如果研究好了并实现转化出来,那就是 1,如果期间遇到问题阻塞了,那就是短时间看不到产出效果。不敢冒险。




  2. 创建一个 React 项目,把相关的这部分代码拆分出来,以微前端的方式内嵌到我的项目中?


    不知道在拆分代码和组装代码的过程中会遇到什么问题?未知,不敢冒险去耽误时间,也是一个 0 或者 1 的问题。




  3. 自己研究 KQL 语法,自己摸索规则,自己实现其逻辑?


    由于项目排期紧张,不敢太过冒险,就选择了自研。起码 ld 能看到进度。😁




image.png


image.png


image.png


我最后选择的是方案3:自研。但是如果有时间,我更倾向的想去尝试方案1 和方案2。


针对自研方案,我们就开干吧!撸起袖子加油干!😄


准备


首先,我们需要一些准备工作,我需要了解 KQL 语法是什么?然后使用它,研究其规则,梳理其逻辑。


Kibana 查询语言 (Kibana Query Language、简称 KQL) 是一种使用自由文本搜索或基于字段的搜索过滤 Elasticsearch 数据的简单语法。 KQL 仅用于过滤数据,并没有对数据进行排序或聚合的作用。


KQL 能够在您键入时建议字段名称、值和运算符。 建议的性能由 Kibana 设置控制。


KQL 能够查询嵌套字段和脚本字段。 KQL 不支持正则表达式或使用模糊术语进行搜索。


更为详细的可以看官方文档 Kibana Query Language



  • key method value 标准单个语句

  • key method value OR/AND key method value OR/AND key method value .... 标准多个语句

  • key OR/AND key OR/AND key OR/AND key method value .... 不标准多个语句

  • ......



Tips:key(字段名称)、method(运算符)、value(值)



实现


textarea



  • 由于用户可以输入多行文本信息,所以需要 textarea。type="textarea"

  • 为了用户能清楚看到输入内容,以及input 的美观,初始行数 :rows="2"

  • 因为我们能支持关键字和KQL两种情况,所以 placeholder="KQL/关键字"

  • 获取焦点需要打开下拉展示框 @focus="dropStatus = true"

  • 失去焦点且没有操作下拉选项则关闭下拉框 @blur="changeDrop"

  • 由于下拉框的位置需要跟着 textarea 高度变化,所以 v-resizeHeight="inputResizeHeight"


<el-input
v-resizeHeight="inputResizeHeight"
id="searchInputID"
ref="searchInputRef"
v-model="input"
:rows="2"
type="textarea"
placeholder="KQL/关键字"
class="searchInput"
@blur="changeDrop"
@focus="dropStatus = true"
>

</el-input>

changeDrop 需要判断用户是否正在操作下拉框内容,如果是,就不要关闭。这块你会怎么实现呢?可以先思考自己的实现方式,再看下边是我个人的实现方式。


其实理论就是给下拉框操作的时候增加标记,在失去焦点要关闭的时候,判断是否有这个标记,如果有,就不要关闭,否则就关闭。但这个标记又不能影响真正的失焦状态下关闭动作。


我想到的就是定时器,定时器能增加一个变量,同时还能自动销毁。具体的实现方式:


// 不关闭下拉框标记
noCloseInput() {
this.$refs.searchInputRef.focus()
if (this.timer) clearInterval(this.timer)
let time = 500
this.timer = setInterval(() => {
time -= 100
if (time === 0) {
clearInterval(this.timer)
this.timer = null
}
}, 100)
}

// 失焦操作
changeDrop() {
setTimeout(() => {
if (!this.timer) this.dropStatus = false
}, 200)
}

这么做需要有以下几点注意:



  • 失焦操作因为需要切换到下拉框有一定延迟需要定时器,而定时器的时间必须小于标记里边的定时器时间

  • 定时器 this.timer = setInterval() 中 this.timer 是定时器的 id

  • clearInterval(this.timer) 只会清除定时器,不会清空 this.timer


v-resizeHeight="inputResizeHeight" 这个是我写的一个自定义指令来检测元素的高度变化的,不知道你有什么好的方法吗?有的话请请共享一下,😍


const resizeHeight = {
// 绑定时调用
bind(el, binding) {
let height = ''
function isResize() {
// 可根据需求,调整内部代码,利用 binding.value 返回即可
const style = document.defaultView.getComputedStyle(el)
if (height !== style.height) {
// 此处关键代码,通过此处代码将数据进行返回,从而做到自适应
binding.value({ height: style.height })
}
height = style.height
}
// 设置调用函数的延时,间隔过短会消耗过多资源
el.__vueSetInterval__ = setInterval(isResize, 100)
},
unbind(el) {
clearInterval(el.__vueSetInterval__)
}
}

export default resizeHeight

下拉面板


image.png


下拉框是左右布局,右侧是检索语法说明的静态文案,可忽略。左侧是语句提示内容。


语句提示内容经过研究其实有四种:


key(字段名称)、method(运算符)、value(值)、connectionSymbol(连接符)


由于可能会有多个语句,其实我们是只对当前语句进行提示的,所以我们只分析当前语句的情况。


// 当前语句详情
{
cur_fields: '', // 当前 key
cur_methods: '', // 当前 method
cur_values: '', // 当前 value
cur_input: '' // 当前用户输入内容,可模糊匹配检索
}

有四部分,肯定就是需要在符合条件的情况下分别展示对应的 options 面板内容。


那判断条件就是如下图,其中后续需要注意的就是这几个判断条件的值赋值场景要准确。


image.png


语法分析器


想处理输入内容,做一个语法分析器,首先需要去监听用户的输入,那么就用 vue 提供的 watch。


watch: {
input: debounce(function(newValue, oldValue) {
if (newValue !== oldValue) this.dealInputShow(newValue)
}, 500)
}

基本大概思路如下:


KQL语法分析器.png


其中获取输入框的光标位置的方法如下:


const selectionStart = this.$refs.searchInputRef.$el.children[0].selectionStart

修改完了之后,光标会自动跑的最后,这样有点违反用户操作逻辑,所以需要设置一下光标位置:


if (this.endValue) {
this.$nextTick(() => {
const dom = this.$refs.searchInputRef.$el.children[0]
dom.setSelectionRange(this.input.length, this.input.length)
this.input += this.endValue
})
}

还有面板里边有四项内容,那每一项内容选择都可以通过鼠标点击选择,点击选择后,就需要按照规则处理一下,进行最终的字符串 this.input 拼接,得到最终结果。


// 当前 key 点击选择
curFieldClick(str) {},

// 当前 method 点击选择
curMethodClick(str) {},

// 当前 value 点击选择
curValueClick(str) {},

// 当前 链接符 点击选择
curConnectClick(str) {},

这部分需要注意的就是点击面板 input 会失去焦点,就加上前边说到的 noCloseInput() 不关闭下拉面板标记。


键盘快捷操作


必备的目前就 3 个事件 enter、up、down,其他算是锦上添花,由于排期紧张,暂时只做了必备的 3 个 事件:


<el-input
v-resizeHeight="inputResizeHeight"
id="searchInputID"
ref="searchInputRef"
v-model="input"
:rows="2"
type="textarea"
placeholder="KQL/关键字"
class="searchInput"
@blur="changeDrop"
@focus="dropStatus = true"
@keydown.enter.native.capture.prevent="getSearchEnter($event)"
@keydown.up.native.capture.prevent="getSearchUp($event)"
@keydown.down.native.capture.prevent="getSearchDown($event)"
>

</el-input>

那么,我们的这几个键盘事件都需要怎么处理呢??接下来就直接上代码简单分析一下:


// 键盘 enter 事件,有两种情况
// 一种就是 选择内容,第二种就是 相当于回车事件直接触发接口
getSearchEnter(event) {
event.preventDefault()

// 当前下拉面板的展示的 options
const suggestions = this.get_suggestions()

// 满足可以选的条件
if (this.dropStatus && this.dropIndex !== null && suggestions[this.dropIndex]) {
// 光标之后是否有内容,有就需要截取处理
// ......

// 当前项是否是手动输入的,需要做截取处理
// .......

// 拼接 enter 键选择的选项
this.input += suggestions[this.dropIndex] + ' '

// 光标之后是否有内容,就需要设置光标在当前操作位置,并拼接之前截取掉的光标后的内容
// .......

// 设置当前语法区域的各个当前项 cur_fields、cur_methods、cur_values
// ......

// 恢复键盘 up、down 选择初始值
this.dropIndex = 0
} else {
// 不满足选的条件,就关闭选择面板,并触发检索查询接口
this.dropStatus = false
this.$emit('getSearchData', 2)
}
},

// 键盘 up 事件
getSearchUp(event) {
event.preventDefault()

// 满足上移,就做 dropIndex 减法
if (this.dropStatus && this.dropIndex !== null) {
this.decrementIndex(this.dropIndex)
}
},

// 键盘 down 事件
getSearchDown(event) {
event.preventDefault()

// 满足下移,就做 dropIndex 加法
if (this.dropStatus && this.dropIndex !== null) {
this.incrementIndex(this.dropIndex)
}
},

// 加法,注意边界问题
incrementIndex(currentIndex) {
let nextIndex = currentIndex + 1
const suggestions = this.get_suggestions()
// 到最后边,重置到第一个,形成循环
if (currentIndex === null || nextIndex >= suggestions.length) {
nextIndex = 0
}
this.dropIndex = nextIndex

// 被选择的选项如果不在可视范围之内,需要滚动到可视区
this.$nextTick(() => this.scrollToOption())
},

// 减法,注意边界问题
decrementIndex(currentIndex) {
const previousIndex = currentIndex - 1
const suggestions = this.get_suggestions()
// 到最前边,重置到最后,形成循环
if (previousIndex < 0) {
this.dropIndex = suggestions.length - 1
} else {
this.dropIndex = previousIndex
}

// 被选择的选项如果不在可视范围之内,需要滚动到可视区
this.$nextTick(() => this.scrollToOption())
},

键盘事件的核心逻辑上述基本说清楚了,那么其中需要注意的一个点,那就是被选择的选项如果不在可视范围之内,需要滚动到可视区,这样可提高用户体验。那这块到底怎么做呢?其实实现起来还挺有意思的。


import scrollIntoView from './scroll-into-view'

// 滚动 optiosns 区域,保持在可视区域
scrollToOption() {
if (this.dropStatus === true) {
const target = document.getElementsByClassName('drop-active')[0]
const menu = document.getElementsByClassName('search-drop__left')[0]
scrollIntoView(menu, target)
}
},

scroll-into-view.js 内容如下:


export default function scrollIntoView(container, selected) {
// 如果当前激活 active 元素不存在
if (!selected) {
container.scrollTop = 0
return
}

const offsetParents = []
let pointer = selected.offsetParent
while (pointer && container !== pointer && container.contains(pointer)) {
offsetParents.push(pointer)
pointer = pointer.offsetParent
}

const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0)
const bottom = top + selected.offsetHeight
const viewRectTop = container.scrollTop
const viewRectBottom = viewRectTop + container.clientHeight

if (top < viewRectTop) {
container.scrollTop = top
} else if (bottom > viewRectBottom) {
container.scrollTop = bottom - container.clientHeight
}
}

针对上述内容几个技术点做出简单解释:


offsetParent:就是距离该子元素最近的进行过定位的父元素,如果其父元素中不存在定位则 offsetParent为:body元素。


offsetParent 根据定义分别存在以下几种情况:



  1. 元素自身有 fixed 定位,父元素不存在定位,则 offsetParent 的结果为 null(firefox 中为:body,其他浏览器返回为 null)

  2. 元素自身无 fixed 定位,且父元素也不存在定位,offsetParent 为 <body> 元素

  3. 元素自身无 fixed 定位,且父元素存在定位,offsetParent 为离自身最近且经过定位的父元素

  4. <body>元素的 offsetParent 是 null


offsetTop:元素到 offsetParent 顶部的距离


image.png


offsetHeight:是一个只读属性,它返回该元素的像素高度,高度包含内边距(padding)和边框(border),不包含外边距(margin),是一个整数,单位是像素 px。


通常,元素的 offsetHeight 是一种元素 CSS 高度的衡量标准,包括元素的边框、内边距和元素的水平滚动条(如果存在且渲染的话),不包含 :before或 :after 等伪类元素的高度。


image.png


scrollTop:可以获取或设置一个元素的内容垂直滚动的像素数。


一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0。


clientHeight:是一个只读属性,它返回该元素的像素高度,高度包含内边距(padding),不包含边框(border),外边距(margin)和滚动条,是一个整数,单位是像素 px。


clientHeight 可以通过 CSS height + CSS padding - 水平滚动条高度 (如果存在)来计算。


image.png


最全各个属性相关图如下:


image.png


效果


效果怎么说呢,也算顺利上线生产环境了,在此截图几张,给大家看看效果。


image.png


image.png


image.png


image.png


小结


做这个需求,最难的点是要求自己去研究 KQL 的语法规则,以及使用方式,然后总结规则,写出自己的词法分析器。


其中有什么技术难点吗?似乎并没有,都是各种判断条件,最简单的 if-else。


所以想告诉大家的是,不要一心只钻研技术,在做业务的时候也需要好好梳理业务,做一个懂业务的技术人。业务和技术互相成就!


最后,如果感到本文还可以,请给予支持!来个点赞、评论、收藏三连,万分感谢!😄🙏


作者:Bigger
来源:juejin.cn/post/7210593177820676154
收起阅读 »

ChatGPT API入门探索,制作AI孙子

最近ChatGPT的热度非常高,OPEN AI也是在不久前刚刚宣布开放ChatGPT商用API。 写这篇文章的契机在于前几天一位群友分享了一个日本程序员基于ChatGPT开发了一个AI佛:HOTOKE AI 例如当用户提问“我不知道生存的目的”,AI佛会做出如...
继续阅读 »

最近ChatGPT的热度非常高,OPEN AI也是在不久前刚刚宣布开放ChatGPT商用API。


写这篇文章的契机在于前几天一位群友分享了一个日本程序员基于ChatGPT开发了一个AI佛:HOTOKE AI


例如当用户提问“我不知道生存的目的”,AI佛会做出如下回答:


image.png


感觉还是挺有意思的,他会从佛教及心理学的角度去给你分析与解释。


但作为一名程序员,我们肯定对背后的原理更感兴趣,于是我又问了他和GPT有什么区别:


image.png


这个回答还是有点出乎我的意料的,于是我更感兴趣了,直接网上找起了教程,也研读了一下OPEN API的文档。所以这篇文章更像是一个学习笔记,也希望能吸引到一些同样感兴趣的小伙伴。


文章最后我们会开发一个基于ChatGPT的AI孙子(就是培养出高启强的那个孙子)


ChatGPT


相信大家能点进来的都知道ChatGPT是啥, 这part就过了吧。


OPEN AI API


Examples


OPEN AI API提供了很多种模型供用户选择,在官网上也提供了很多示例,包括各种各样的模型及其生成的结果。
这是官网示例的地址:platform.openai.com/examples


image.png


我们以Q&A为例:


image.png
可以看到它用的模型是text-davinci-003,然后他说"我是一个高度智能的问答机器人。如果你问的问题有确切答案,我就会给出答案,如果你问的问题是在搞我或者没有明确答案,我会回答不知道"。然后下面就是一些示例。


提示工程(Prompt Engineering)


我们还可以点右上角的Open in Playground来自己尝试下


image.png
可以看到右侧侧边栏有许多参数可以调节,根据调节的参数不同,生成的答案也会有所区别。参数就不细说了,感兴趣的同学可以自己去看,当你鼠标移到那个参数上时,会有对应的解释框弹出来解释这个参数的作用。


这里简单演示了如何去通过提示,引导AI之后回答出你所想要的答案,所以提示(Prompt)就变的尤为重要。事实上,现在提示工程师(Prompt Engineer)已经成为一个炙手可热的岗位,他们的主要任务就是引导AI去学习及训练。
官方文档也用了相当大的篇幅去介绍如何去提示:platform.openai.com/docs/guides…


这里就简单翻译一下开头:


概述

我们的模型可以做任何事情,从生成原创故事到执行复杂的文本分析。因为它们可以做这么多事情,所以你必须明确描述你想要什么。告诉他该怎么做,而不只是简单陈述,这就是一个好提示的秘籍。


创作提示有三个基本准则。




  • 展示和讲述。通过指示、例子或两者的结合,明确你想要什么。如果你想让模型按字母顺序排列一个项目清单,或按情绪对一个段落进行分类,就向它展示你想要的东西。




  • 提供高质量的数据。如果你试图建立一个分类器或让模型遵循一个模式,确保有足够的例子。确保校对你的例子 —— 模型足够聪明,它可以识别拼写错误,并且告诉你,但它也可能认为这是你故意写错的,这都会影响他的回答。




  • 检查你的设置。temperature和top_p参数控制了模型在产生响应方面的确定性。如果你要求它产生一个只有一个正确答案的响应,那么你就想把这些设置得低一些。如果你在寻找更多不同的反应,那么你可能想把它们设置得更高。人们使用这些设置的第一大错误是,假定它们是对"聪明"或"创造性"控制。




到这里大家应该也就知道了,那个AI佛就是通过程序员选择合适的模型,以及对他加以训练,比如告诉他当用户提问时,他需要从佛教及心理学的角度去回答用户的问题。


模型(Models)


模型可以说是非常重要的一部分,也是Open AI的核心。


image.png


ChatGPT用的就是GPT-3.5模型,他主要是面向大众,所以被训练的更加安全,不会输出一些出格的内容。


在GPT-3.5中又包含了几个子模型:


image.png


目前官方推荐的是gpt-3.5-turbo,称此模型能生成最好的结果,并且只需要text-davinci-003十分之一的算力。


价格


目前gpt-3.5-turbo的价格是每一千个token需要0.002🔪。所谓token可以理解为他处理的单词数,1000个token大概可以生成750个单词。
image.png


AI孙子


接下来我们尝试简单写一个AI孙子。


首先当然是创建一个项目,我用的就是简单的create-react-app去创建一个react应用:
npx create-react-app ai-sunzi


使用express写一个简单的服务端:


image.png


重写App.js,我们只需要一个textarea和一个提交按钮


image.png


访问http://localhost:3000 确保可以访问服务端,返回“Hello World!”:


image.png


接下来我们需要去Open AI官方文档中查看如何使用它的API,文档中有一个例子,如何在nodejs中引入包:


image.png


我们把他复制到我们代码中,有些地方还是需要改动一下,比如包的引入不能直接使用import。再将API Key复制到configuration中(API Key需要注册并登录OpenAI,可以免费领取):


image.png


然后我们再查看文档中Completion部分:


completion.png


将其复制到我们代码中并测试:


image.png


此处我将max_token修改为100,因为中文字符所占字节数长一些。随后将prompt参数改为“无论用户输入什么,只需回答这是一个测试”
再次访问localhost:3000可以看到确实可以返回“这是一个测试”。


说明我们的后端已经从OPEN AI得到了返回的数据。


再接下来就简单了,我们只需要把前端和后端连上,再修改prompt,让这个AI表现的像孙子:


image.png


将textarea中的message传入至prompt参数中,再从OPEN AI得到返回的答案,就大功告成了。
以下为一些测试:


image.png


image.png


image.png


Reference: http://www.youtube.com/watch?v=bB7…


作者:RyanGG
来源:juejin.cn/post/7209862607976644645
收起阅读 »

60分钟的文心一言发布会:我带你5分钟看完

李彦宏缓缓走入会场。 亮了,他亮了。 文心一言具有五大功能: 文学创作(写诗歌,写小说) 商业文案创作(给公司起名,写宣传语) 数理逻辑推算(做题) 中文理解(理解华夏文化) 多模态生成(文字、图片、音频、视频) 其中,前三个功能ChatGPT都有,我...
继续阅读 »

李彦宏缓缓走入会场。


live_capture_05_35.png


亮了,他亮了。


live_capture_05_59.png


文心一言具有五大功能:



  1. 文学创作(写诗歌,写小说)

  2. 商业文案创作(给公司起名,写宣传语)

  3. 数理逻辑推算(做题)

  4. 中文理解(理解华夏文化)

  5. 多模态生成(文字、图片、音频、视频)


其中,前三个功能ChatGPT都有,我感觉百度搞不过它。


比如:


live_capture_10_47.png


再比如:


live_capture_12_45.png


但是,后两个功能是ChatGPT不具备的。


首先说中文理解。


老李说文心一言的重点是用中文训练。AI模型能深入中国文化到什么程度呢?


拿洛阳纸贵举例子,他连着问了模型几个问题。


live_capture_15_31.png


尤其是第二个问题,洛阳纸贵,那么当时到底多少钱呢?


live_capture_14_03.png


模型可以结合唐朝时期的物价以及相关文献记载,给出来答案:当时洛阳的纸由八百文涨到两三千文。


甚至让它以“洛阳纸贵”写一篇藏头诗,模型也写了出来。虽然比不上我写的,但是起码它藏头。


我们再来看看ChatGPT对于中文的理解是什么效果。


问ChatGPT洛阳纸贵什么意思,然后再问洛阳的纸多少钱。


123.png


很明显,它对第二个问题回答的不是很好。它没有清楚的理解我的问题,反而还指出了我的错误。它没有说具体多少钱,它说这不是贵的问题,只是一种现象。


而对于藏头诗,ChatGPT也是无能为力。


456.png


它可能知道什么是诗,但并不知道什么是藏头诗。因为,这就有点深入中国文化了。


但是,我相信,你让它解释藏头诗,它凭借搜索引擎是能找到并打印出来的。但是,你让它写,很遗憾。


ChatGPT没有的,文心一言有的,第二个功能就是多模态。


多模态是AI的一个专业名词,比如文本是一种模态,图片是一种模态。多模态就是多种形式。


我们知道,ChatGPT是一个文本单模态的语言模型。


我李哥演示了一个从文字到图片到语音再到视频的例子。


live_capture_17_40.png


海报图片设计出来了。


live_capture_15_52.png


然后再用四川方言讲出来。


live_capture_16_28.png


最后生成视频。


live_capture_17_13.png


其实,后面的几种模态并非是首发,只是一种整合。


语音合成是老技术了,这不用说。


图文转视频的功能,去年开始,在各大自媒体平台也纷纷上线了。包括头条号在内,写完一篇文章,可以自动生成视频。


但是,大多都是素材库的标签拼凑。生成的效果较差,有时候百度自己生成的,自己都无法通过审核。


但是,让ChatGPT干这些,它除了文本模态之外,它都会回复你它只是一个语言模型。


789.png


讲完了五大功能。后面说了三大产业机会。


live_capture_26_30.png


中间休息,亮出了很多AIGC的案例。


live_capture_00_13.png


live_capture_00_22.png


live_capture_00_32.png


live_capture_00_42.png


休息完了,百度首席技术官,王海峰老师对文心一言的技术做了简单解读。


live_capture_01_24.png


其实主要还是说了利用飞桨开源平台实现的。


live_capture_01_57.png


live_capture_04_57.png


然后介绍了文心一言模型的功能组成。


live_capture_10_40.png


live_capture_12_47.png


live_capture_14_10.png


live_capture_19_05.png


live_capture_21_41.png


最后说,飞桨平台好!


live_capture_21_47.png


结束前,官方平台宣布,企业用户可以申请内测。


live_capture_24_06.png


方法在图片里面了,搜索关键词“百度智能云”填写表单就可以。


有个邀请体验说明是这样的:



我们期待与您尽快展开合作,但由于初期名额有限,目前暂不能满足所有人的申请,因此请您仔细填写需求,我们会结合业务场景、访问量等级等信息综合评估,并在您通过评估后尽快给您反馈,本次邀测仅面向企业用户,谢谢您的理解。



最后,我感觉李哥和王老师在讲的时候,声音有些发颤。可能理工男面对大型发布会都有怯场的情况,也可能他们对平台不自信。


整个发布会,都是在播放视频,并没有现场实际操作平台。这也是为了达到最好的发布效果。李哥说,实际操作的话会比较慢,大家需要等。


我感觉,抛去技术不谈,文心一言在中国肯定是有市场的。


因为,我在直播间发布了很多信息,都没有显示。但是,当我和大家万众一心时,我的评论赫然出现在字幕上。


2023-03-16_140740.png


这一点,ChatGPT无论如何是无法做到的!


我们不黑不吹,后续的情况,只能等待用户的反馈了。


作者:TF男孩
来源:juejin.cn/post/7211055301204705338
收起阅读 »

ChatGPT前端领域初探

什么是ChatGPT 官方解释:ChatGPT是一个智能聊天机器人,来自于OpenAI,它能够使用人工智能技术进行对话,并回答用户提出的问题和请求。它由GPT(Generative Pre-trained Transformer)算法支持,可以模拟人类对话和回...
继续阅读 »

什么是ChatGPT


官方解释:ChatGPT是一个智能聊天机器人,来自于OpenAI,它能够使用人工智能技术进行对话,并回答用户提出的问题和请求。它由GPT(Generative Pre-trained Transformer)算法支持,可以模拟人类对话和回答各种问题,包括日常生活、科技、娱乐、健康、财经等领域。ChatGPT可以通过各种渠道进行访问,如网站、社交媒体或移动应用程序。


ChatGPT优势


ChatGPT有以下几个优势:



  1. 可以模拟真人对话:ChatGPT使用GPT算法,可以生成自然语言,使得对话非常流畅和自然,就好像在与一个真人交流一样。

  2. 能够自我学习:ChatGPT使用机器学习技术,可以通过不断的学习来提高自己的答案和回复质量。

  3. 24小时在线:ChatGPT可以在任何时间回答用户的问题,不需要等待人类客服的接待时间。

  4. 处理大量请求:ChatGPT可以处理大量请求,在同一时间内可以同时与多个用户进行对话。

  5. 提高客户满意度:ChatGPT可以回答用户的问题并提供有用的信息,这可以提高用户的满意度和忠诚度。

  6. 提高效率:ChatGPT可以快速响应和解答用户问题,减少人工客服的工作量和时间。


接下来,我们来聊一聊它对前端开发产生了什么样的影响


体验流程


我们需要先拥有一个相应的账户才能体验,这里我直接放上体验流程的链接:sms-activate.org,按照本流程对于没有接触过ChatGPT的童鞋们可以体验一下,这里我就不展开详细解释了。tips:电脑需要科学上网哦~~


辅助开发


重点来了,我们需要先分析我们在日常开发中哪些方面可以用到它,根据开发的流程我们可以从以下几点分析:




  1. 需求阶段:我们做业务开发的前提是以需求为准,对于需求而言,实现的方式多种多样,我们应该分析一下,应该用什么技术去实现,具体对应到哪种框架、第三方依赖库等等。




  2. 编码阶段:这个阶段是业务逻辑的实现阶段,要完成需求中的功能。举个🌰:做登录注册模块,输入账号和密码时一般都需要校验格式(复杂情况),这时正则表达式不失为一个好的办法,此时我们的主角就该登场了~~ 话不多说,直接上图:


    image.png


    经验证:^[a-zA-Z0-9]{6,}$ 符合预期
      let reg = /^[a-zA-Z0-9]{6,}$/
    let str = '0203'
    let str2 = 'yk0203'
    console.log(reg.test(str), 'str') // false
    console.log(reg.test(str2), 'str2') // true



这个正则限制比较简单,来个复杂点的吧:


image.png


image.png tips:解释满分。


这里我就不做test了,更复杂的大家可以自己尝试~~



  1. 代码优化:我们在在编码阶段完成功能后,应该考虑代码优化之道,优化代码包括多个层面,提高代码的复用性就是其中一个方面,直接上demo说明一下:


image.png
在这个demo中,我们定义了两个函数,一个用于计算两个数的和,另一个用于计算两个数的差。通过调用这两个函数,我们可以实现复用性,避免在不同的地方重复编写相同的计算代码。此外,我们还可以通过将这些函数放入一个模块中,从而实现更高程度的复用性。将这两个函数放入一个名为"calculator.py"的模块中,然后在其他文件中引入这个模块,就可以在其他地方轻松地重用这些函数。然后我又向它发起提问:


image.png


image.png
我们使用JavaScript语言实现了与前面Python demo相同的功能。将这两个函数放入一个名为"Calculator.js"的模块中,然后在其他文件中引入这个模块,就可以在其他地方轻松地重用这些函数。其实这种思想在前端已经老生常谈了。



  1. 代码重构:一般来说,一个项目经过多个人先后开发并维护,每个人的开发习惯也不尽相同,最终导致代码中很多逻辑不是很清晰,尤其是那些很久之前的项目,再加上当时没有添加注释,维护效率不高,这个时候我们可以考虑下重构下部分代码,用图来说话:


image.png
这里只是一个简单的demo,项目中重构代码要比这个复杂(慎重考虑是否进行代码重构),说到这里,养成良好的编码习惯就显得特别重要啦,比如定义方法时语义化,适当给方法添加注释等,我们的主角也是也可以帮忙添加注释的,请看:


image.png


function factorial(n) { 
if (n === 0) return 1;
else return n * factorial(n-1);
}
//示例输出
console.log(factorial(5)); //120
console.log(factorial(10)); //3628800

image.png


// 定义递归函数,计算阶乘 
function factorial(n) {
if (n === 0) {
// 当 n 等于 0 时,阶乘结果为 1
return 1;
} else {
// 当 n 不等于 0 时,调用函数自身来计算阶乘
return n * factorial(n-1);
}
}
// 示例输出
console.log(factorial(5)); //120
console.log(factorial(10)); //3628800

然后我又追问:


image.png
综上,我们想要主角帮我们处理问题,就应该把条件罗列清晰,否则主角也给不了最完善的回答,它有时在处理基本数学问题时出错,有时无法全面理解逻辑,并且甚至会提供完全错误的事实来支持其答案。OpenAI 已经承认了这个问题,并表示“ChatGPT 有时会写出听起来合理但是不正确或无意义的答案”。ChatGPT 是一种辅助工具,能够理解复杂的问题。但最好也不要完全相信它所说的一切。


tips:提问和回答的截图均来自VScode的ChatGPT中文版扩展插件(站在了大佬的肩膀上),有兴趣的童鞋可以自行搜索体验一波~


ChatGPT的现状与未来


目前,OpenAI官方已经官宣了GPT-4,只不过还没完全开放,并且门槛较高,发展前景还是相当nice的。


总结


总体看来,未来可期~~,对于希望在工作中提升效率的开发人员来说是一柄利剑。然而,它的回答也不是100%准确的,因此在将其用于更高级的任务之前,需进行深究。到此,ChatGPT的初探到此结束,感谢各位看官。有问题欢迎评论区留言。


作者:青灬河
来源:juejin.cn/post/7210653822849548346
收起阅读 »

前端已死?铜三铁四?你收到offer了吗?

背景 今年是应届生就业最难的一年。说实话,每一届毕业生都这么说,而且每一届毕业生说的都没有问题。疫情刚开始那一年,王兴说过一句话 2019 年可能会是过去十年最差的一年,却是未来十年里最好的一年。因为难,所以很多人将目光放向了金三银四,无论是应届的,还是找新机...
继续阅读 »

背景


今年是应届生就业最难的一年。说实话,每一届毕业生都这么说,而且每一届毕业生说的都没有问题。疫情刚开始那一年,王兴说过一句话 2019 年可能会是过去十年最差的一年,却是未来十年里最好的一年。因为难,所以很多人将目光放向了金三银四,无论是应届的,还是找新机会的。


金三银四就是往年最好的求职时期。三四月份刚好又是春招的时期,过年后,每个企业又刚好制定好下一年的计划。刚发完年终奖,职场的小同学们又蠢蠢欲动,空出来不少位置。


现况


但是对于今年来说,金三银四就是纯骗局。即使你通过了很多简历初选,发现最后拿到手没有什么好offer,你发现好像同样的岗位,去年学长学姐,硬件指标不需要这么严格。辞职前聊的好好的offer,裸辞之后就被缩紧了。


我之前看过一段话,我一直觉得欧洲特别拉胯,今天搞游行,明天搞罢工,昨天嫌工作时间多了,今天嫌假期少了。直到有一天,我改变了看法,我毕业了。我们以为疫情过去,所有的一切都会好转,因为这是艰难的最主要来源。但是好像不是这样。因为疫情指很多深层问题的表面借口罢了。


原因


疫情只是一方面。国内资源开发过度,房地产集体不拿地,导致房企上下游的企业、广告公司、活动公司、印刷公司、工程公司对外招聘收紧。国际局势紧张,投资愈加谨慎,金融行业都卷得飞起,依赖投资的入不敷出的互联网公司,更无法支出如此大的人力成本。由此牵扯的行业计算机产品、自媒体运营、招聘收紧。教育本来就人走茶凉,牵扯出来的行业也多的不行。甚至俄乌战争能通过大量蝴蝶效应影响着我们。


那我们能怎么办呢?


混乱的背景,只会使富人更富,穷人更穷。没办法了吗?那我们能怎么办呢?


我们无法改变局势,我们只能在夹缝中求生存。但我们起码要成为在这个环境下人群中的最优解。求职也好,职业规划也好,面对现在困境,主要有两个生存的角度避坑主动措施


避坑


想要避免被坑,就得了解面试官和公司。他们到底怎么想的?


这两年最主要的坑分别是赛马机制表面繁荣白嫖逻辑


赛马机制


面试公司让你以为你很优秀,其实你已经掉入公司的陷阱了。赛马机制就是强势烘托起所有人的竞争欲,最后选出最优秀的一匹马。赛马本无问题,但是大面积的赛马是对求职者的极其不负责任。以前的公司经常会搞 PUA 这一套来实现控制员工的目的。现在公司用的方法更加高级 NLP 教练技巧。 NLP 教练技巧可以理解为正向PUA。


你以为面试官在夸你?你以为公司很器重你?


但你已经掉进公司糖衣炮弹的陷阱了。 NLP 本来是良性的引导,但在大面积的竞争驱使下,赛马机制应运而生。 NLP 带来的自我催眠会让你适应恶劣的竞争,像养蛊一样愿意吃掉所有的竞争者。


如何判断一个公司是否在赛马?


最简单的逻辑是



  1. 第一、看是否是需要通过试用期多人的竞争来确定留用。不用可惜这样子的机会,因为转正后一定会迎来更为内卷的赛马机制。

  2. 第二、 不是面试前,而是所有面试后还有一堆面试官加你微信的,大概率是想拉着你再赛马几次,或者我只能请你保护好自己的私生活。

  3. 第三、 画饼的公司必然在赛马。


就业难在哪里?因为人力资源充沛这件事既是红利也是诅咒。人力资源看起来充沛,就会带来第二个坑,


表面繁荣,


实际上企业现阶段根本没有什么需求,裁员都来不及呢。这和你们现在面对的很多情况是类似的。一个岗位有超多人竞聘,很多人认为这个岗位热门就是行业趋势了。其实并不是,其实只是只是企业可能需要到面率等一系列人力资源指标,为了让这个看起来很繁荣罢了。这也是为什么你们很多人通过简历关,但是最后都拿不到 offer 的原因


如何避免?


几乎无法避免,毕竟海头是你我们常干的事情。但是可以做好心理预设,就是招聘人数需求很大的岗位,不要对这种岗位抱有太高的希望,避免影响自己招聘热情和状态。


白嫖逻辑


当你跟面试官运筹帷幄,展示自己的专业性时,你已经失去了工作的机会。公司招不起人才,但想白嫖。人才的创意和专业知识,


怎么嫖?让人才参加面试?


我认识的一个大厂的面试官跟我聊天,说今年他们的招生名额少了很多,但是面试数量增加很多。我很好奇的问他,逻辑有点矛盾,他贱兮兮的跟我说,他们内部叫做给奖励,主要给那些很厉害的人奖励,但是并不会招他们。比如一些 35 岁以上很专业的人,比如有一些硬伤但真的有东西的人。这些人在求职市场处处碰壁,给的奖励就是面试的机会。他们刚好也可以从这些面试者那里了解到一些打破信息壁垒的专业知识或信息,顺便给一些刚入职的人或者项目经理培训,但是他们肯定是不会招的。说实话,我听到这里已经开始气了。应届生常见的类似的坑,就是拿一些内部案例作为面试题进行面试。本质就是白嫖,却堂而皇之的说是奖励,谁稀罕你的奖励。所以,如果你碰到公司,一拿非常具体的案例来去进行考核,让你演绎操作细节的。并且不断询问你对行业的认识,跟你讨论战略并显得他毫无理解的。或者面试官相较于你过于年轻的这一类公司,扭头就走就好了


相反的,如一个公司不断的给你输出他们的信息,并且真实的拿这些信息和你比对,这类公司大概率是比较靠谱的。


其实听你看完上面的,你基本能面对在金三银四的大粪坑了,还有一些很明显的坑,大部分你在网上也能搜索到,我就不扩展讲了。但是下面你要仔细看了,因为这是你在避坑之后能够有可能找到工作的重要手段。


主动措施


我们如果把找工作分成找工作前、找工作中和找工作后,我们分别要做的东西就是信息获取个人展示职业规划。我不断在文章里面强调信息的重要性。各个公司官网的 drop description 一定需要好好看看。如果可以用爬虫把很多公司的求职信息 down 下来,可以在他们的官网,也可以在智联招聘一类的网站,不用担心太难。你在 b 站或者其他平台搜索一下。爬虫找工作。现在已经有很多打包的工具或者软件,


金三银四


还有金三银四,不是三四月份开始,春节后就已经开启了。非要再说一些信息,新能源医药现在形势的确不错,按照之前教给你们的思维,他们的上下游求职都会比较顺利。应面试之前去企查查天眼查之类的,好好查一下公司的相关情况,什么情况,成立时间,公司规模,组织架构,法律风险是否存在信誉问题,股东信息是否正常,是否有财务纠纷。查完了,门清了你再冲,不然给你拐缅北去。再看一圈。脉脉拉钩,牛客网、看准网之类的,里面有大量吐槽到起飞的评价,可以让种草的你立马拔草。还有一些关于签署协议、劳动合同、三方协议等等一系列你需要了解,保护自己权益的相关信息,


个人展示


重要的大家理解的还是简历。简历其实针对不同的行业,需要针对定向的简历。因为你要展示的东西暴露问题是不一样的。如果你没有简历模板,或者简历做得很拉,可以使用木及简历,他是一款免费的简历模板网站。还可以使用Markdown编辑特别方便,不要用网上那些花里胡哨的剪辑模板,谁用谁死。往年我们在各大网站搜索找工作,都是各种干货方法。今年你在知乎搜一下 2023 找工作试试。只有一个统一的话题 2023 找工作好难。


你要准备无非是从找工作的环节入手。简历、笔试、面试、二面、终面。简历跟大家说了,笔试自己准备,唯一能准备的就是面试了。牛客网有不少念经,可以去看看,剩下的就是最后一个。


心态


最重要的心态随缘。我不是让大家摆烂。很多公司会组织无领导小组面试,很多公司会有压力面。如果让我给你讲分别,有什么技巧,我是能讲很多的,毕竟我也参加过招聘招聘。但是没必要随缘这个词很重要,因为它恰恰是器中了面试的核心表现状态。当你随缘的时候,无领导小组面试就没必要非要去争取leader,压力面也不会有什么压力,我不会。要知道 leader 通过率并不会比其他角色高。争取表现机会的行为其实并不会帮到什么。很多人总跟你讲放平心态,什么叫做放平心态?随缘就好。工作暂时找不到,并不会饿死并不是你一个人现在这么艰难。削弱焦虑还没到最后一刻


最后职业规划在这里。我只想跟应届生和已经工作的同学说两条。



  • 一、应届生转行并没有什么壁垒,去现在好一些的行业。

  • 二、别跳槽,市场太不景气了。不要觉得疫情过去,一切都好了。求职很重要,只是人生的一小步,并不会因为你应届的求职受挫而影响那么深远。从大厂离开的决定,那一刻,如果你也不想找工作,就就当你给自己放个长假。最重要的是要
    作者:zayyo
    来源:juejin.cn/post/7207314488243585081
    开心。

收起阅读 »

Android记一次JNI内存泄漏

记一次JNI内存泄漏 前景 在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。 分析 查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是a...
继续阅读 »

记一次JNI内存泄漏


前景


在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。


分析


查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是android11系统及以上的保存目录和android11以下不同,android11保存的目录在:


   /data/media/10/Download/leakcanary-包名/2023-03-14_17-19-45_115.hprof 

使用Memory Analyzer Tool(简称MAT) 工具进行分析,需要讲上面的hrof文件转换成mat需要的格式:


   hprof-conv -z 转换的文件 转换后的文件

hprof-conv -z 2023-03-14_17-19-45_115.hprof mat115.hprof

打开MAT,导入mat115文件,等待一段时间。


在预览界面打开Histogram,搜索需要检测的类,如:VideoActivity


screenshot-20230314-204413.png


搜索结果查看默认第一栏,如果没有泄漏,关闭VideoActivity之后,Objects数量一般是零,如果不为零,则可能存在泄漏。


右键Merge Shortest Paths to GC Roots/exclude all phantom/weak/soft etc,references/ 筛选出强引用的对象。


image.png


筛选出结果后,出现com.voyah.cockpit.video.ui.VideoActivity$1 @0x3232332 JIN Global 信息,且无法继续跟踪下去。


screenshot-20230314-205257.png


筛选出结果之后显示有六个VideoActivity对象没有释放,点击该对象也无法看到GC对象路径。(正常的java层内存泄漏能够看到泄漏的对象具体是哪一个)


正常的内存泄漏能够看到具体对象,如图:


image.png
这个MegaDataStorageConfig就是存在内存泄漏。


而我们现在的泄漏确实只知道VideoActivity$1 对象泄漏了,没有具体的对象,这样就没有办法跟踪下去了。


解决办法:


虽然无法继续跟踪,但泄漏的位置说明就是这个VideoActivity1,我们可以解压apk,在包内的class.dex中找到VideoActivity1 ,我们可以解压apk,在包内的class.dex中找到VideoActivity1这个Class类(class.dex可能有很多,一个个找),打开这个class,查看字节码(可以android studio中快捷打开build中的apk),根据【 .line 406 】等信息定位代码的位置,找到泄漏点。


screenshot-20230314-205442.png


screenshot-20230314-205600.png
screenshot-20230314-205523.png


根据方法名、代码行数、类名,直接定位到了存在泄漏的代码:


screenshot-20230314-205730.png


红框区内就是内存泄漏的代码,这个回调是一个三方sdk工具,我使用时进行了注册,在onDestory中反注册,但还是存在内存泄漏。(该对象未使用是我代码修改之后的)


修改方法


将这个回调移动到Application中去,然后进行事件或者回调的方式通知VideoActivity,在VideoActivity的onDestory中进行销毁回调。


修改完之后,多次进入VideoAcitivity然后在退出,导出hprof文件到mat中筛选查看,如图:


image.png


VideoActiviyty的对象已经变成了零,说明开始存在的内存泄漏已经修改好了,使用android proflier工具也能看到在退出videoactivity界面之后主动进行几次gc回收,内存使用量会回归到进入该界面之前。


总结:



  1. LeakCanary工具为辅助,MAT工具进行具体分析。因为LeakCanary工具的监听并不准确,如触发leakcanary泄漏警告时代码已经泄漏了很多次。

  2. 如果能够直接查看泄漏的对象,那是最好修改的,如果不能直接定位泄漏的对象,可以通过泄漏的Class对象在apk解压中找到改class,查看字节码定位具体的代码泄漏位置。

  3. 使用第三方的sdk时,最好使用Application Context,统一分发统一管理,减少内存泄漏。


作者:懵逼树上懵逼果
来源:juejin.cn/post/7210574525665771557
收起阅读 »

GPT-4 就要来了,你准备好了吗

GPT-4 即将面世,它到底是擎天柱还是威震天? 呆鸟说:“ChatGPT 是具备人机对话能力的人工智能,一经问世,就给世人带来了前所未有的新奇体验。短短几个月时间,它就占据了人们的视野。有人好奇,有人担忧,有人看到了致富的机会,也有人担心 AI 会夺走我们...
继续阅读 »

GPT-4 即将面世,它到底是擎天柱还是威震天?



呆鸟说:“ChatGPT 是具备人机对话能力的人工智能,一经问世,就给世人带来了前所未有的新奇体验。短短几个月时间,它就占据了人们的视野。有人好奇,有人担忧,有人看到了致富的机会,也有人担心 AI 会夺走我们的饭碗。ChatGPT 的升级版 GPT-4 也即将问世,面对更强大的 AI,我们应当何去何从呢”



GPT-3 是 ChatGPT 的核心,它是一个超大规模的大语言模型(large language models,LLMs)。GPT-3 的英文全称是生成式预训练转换器-3(generative pre-trained transformer 3),是基于 Transformer 架构开发的大型语言模型,拥有超过 1750 亿个参数。公众可以通过 OpenAI API 访问。ChatGPT 的 API 用户界面简单易用,只需“输入文本”,即可“输出文本”,无需用户具备任何专业技术知识。


据传,OpenAI 正在开发 GPT-4,该模型的参数可能高达 100 万亿,但 OpenAI 的首席执行官 Sam Altman 反驳了这一说法。


那么,GPT-4 与 GPT-3 到底有何不同,它是否会颠覆我们现有的认知?本文将一一对此进行说明。


预计 GPT-4 将在 2023 年发布


汽车人首领 ~ 擎天柱
汽车人首领 ~ 擎天柱


虽然尚未正式官宣,但 IT 界对 GPT-4 的传言数不胜数,预计 OpenAI 很有可能会在今年发布 GPT-4。尽管有传言称微软必应的聊天功能中使用了新版 GPT,但目前测试用户必须排队等待才能使用由 ChatGPT 支持的新必应,而且也无法确定新必应的聊天功能是否使用了新版本的 GPT。


在 ChatGPT 发布之前,即 2022 年底,OpenAI 只允许指定的合作伙伴、付费客户和学术机构使用 GPT-3。预计在GPT-4 发布之前,OpenAI 也会采取类似的方式限制 GPT-4 的使用人员。


GPT-4 可能不会比 GPT-3 训练更多的数据


有传言称,GPT-4 的参数规模将比 GPT-3 大 100 倍,即 17 万亿个参数。但 Altman 曾表示,GPT-4 的参数规模可能并不会比 GPT-3 大很多,因为改进的重点应该是利用现有数据的能力,而不是添加更多的数据。


与 GPT-3 竞争的 Megatron 3(注:Megatron 是变形金刚里霸天虎的首领,中文名为威震天)也是一种大语言模型,它训练的数据比 GPT-3 多,但在测试效果并没有超越 GPT-3,这表明在 AI 领域,规模更大并不一定意味着效果更好。改进算法将降低 GPT-4 和 ChatGPT 的运行成本,这将是 ChatGPT 想要取代谷歌成为最流行搜索引擎的重要因素。


霸天虎首领 ~ 威震天
霸天虎首领 ~ 威震天


GPT-4 将生成更好的编程语言代码


ChatGPT 令人印象最深刻的一点是它不仅可以生成人类语言,还可以生成 Javascript、Python、C++ 等编程语言的代码,可以说是软件开发、Web 开发和数据分析统统都能搞定。


有消息称,目前,OpenAI 正在积极招聘擅长使用人类语言描述代码功能的程序员,预计 GPT-4 将推动 AI 突破生成编程代码的新境界。这种趋势将进一步推动开发工具的革命,例如,微软旗下的 Github 推出的 Copilot 使用的就是经过微调的 GPT-3,提供了把人类自然语言转换为代码的能力。


GPT-4 将使 AI 生成编程代码的能力迈向新的境界。除了生成人类语言,ChatGPT 还能生成 JavaScript、Python、C++ 等编程语言的代码,在软件开发、Web开发和数据分析等领域都将有广泛的应用。


据称,OpenAI 正在招聘能以人类语言描述代码功能的程序员,这将进一步推动开发工具的革命。例如,微软旗下的 Github 推出的 Copilot 使用的就是经过微调的 GPT-3,能够把人类自然语言转换为代码。


总之一句话,就是让程序员开发干掉程序员的程序


GPT-4 不会添加图形功能


有人曾预测 GPT-4 将整合 GPT-3 的文本生成和 Dall-E 2(OpenAI 的另一款 AI 产品)的图像创作功能,如果 能提供数据可视化功能,ChatGPT 将更加完美。但 Altman 否认了这一点,并表示 GPT-4 仍只提供文本生成的功能。


Dall-E 生成的图像
Dall-E 生成的图像


有人会对 GPT-4 失望


虽然 GPT-3 的闪亮登场让整个世界都兴奋不已,但 GPT-4 的表现可能并不会让人再次惊艳。计算机第一次写诗的时候,你可能会觉得震撼,但几年后,即使能让 AI 把诗写得更优美,也不会再给人带来同样的震撼。


Altman 在今年一月的一次采访中曾说过,“有关 GPT-4 的传言很荒谬,都是空穴来风,只怕是希望越大,失望越大。”


那么,我亲爱的读者们,你们对此是怎么看的呢?


有兴趣写书的联系我


Nuxt3 小册推荐


作者:呆鸟
来源:juejin.cn/post/7205842390842114085
收起阅读 »

做一个文件拖动到文件夹的效果

web
在我的电脑中,回想一下我们想要把一个文件拖动到另一个文件夹是什么样子的呢 1:鼠标抓起文件 2:拖动文件到文件夹上方 3:文件夹高亮,表示到达指定位置 4:松开鼠标将文件夹放入文件 下面就来一步步实现它吧👇 一:让我们的元素可拖动 方式一: dragg...
继续阅读 »

在我的电脑中,回想一下我们想要把一个文件拖动到另一个文件夹是什么样子的呢



1:鼠标抓起文件

2:拖动文件到文件夹上方

3:文件夹高亮,表示到达指定位置

4:松开鼠标将文件夹放入文件



Kapture 2023-03-10 at 08.30.34.gif


下面就来一步步实现它吧👇


一:让我们的元素可拖动


方式一: draggable="true"


`<div draggable="true" class="dragdiv">拖动我</div>`

方式二:-webkit-user-drag: element;


  .dragdiv {

width: 100px;

height: 100px;

background-color: bisque;

-webkit-user-drag: element;

}


效果


Kapture 2023-03-10 at 08.55.25.gif


二:让文件夹有高亮效果


给文件夹添加伪类?


🙅如果你直接给文件夹设置伪类:hover,会发现当拖动元素时,文件夹的:hover是不会触发的


Kapture 2023-03-10 at 09.08.54.gif


🧘这是因为在拖拽元素时,拖拽操作和悬停操作是不同的事件类型,浏览器在处理拖拽操作时,会优先处理拖拽事件,而不会触发悬停事件。拖拽操作是通过鼠标点击和拖拽来触发的,而悬停事件是在鼠标指针停留在一个元素上时触发的。


所以我们就来对拖拽操作的事件类型做功课吧🫱



  • dragstart:拖拽开始

  • dragend:拖拽结束

  • dragover:拖拽的元素在可放置区域内移动时触发,即鼠标指针在可放置区域内移动时持续触发

  • dragenter:拖拽的元素首次进入可放置区域时触发

  • dragleave:拖拽的元素离开可放置区域时触发

  • drop:当在可放置区域时,松开鼠标放置元素时触发


什么是可放置元素?
当你给元素设置事件:dragover、dragenter、dragleave、drop的时候
它就变成了可放置元素,特点是移到上面有绿色的➕号

拖动高亮实现


1:我们给files文件夹添加两个响应事件:dragoverdragleave


ps: 这里用dragover事件而不用dragenter事件是为了后续能够成功触发drop事件

2:当拖动元素进入可放置区域时,动态的给files添加类,离开时则移除类


// 显示高亮类
.fileshover {
background-color: rgba(0, 255, 255, 0.979);
}
// 添加dragover事件处理程序,在可放置区域触发

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover');

});

// 添加dragleave事件处理程序,离开可放置区域触发

files.addEventListener('dragleave', (event) => {

event.target.classList.remove('fileshover');

});

🥳 恭喜你成功实现了移动到元素高亮的效果了


Kapture 2023-03-14 at 11.54.14.gif


三:文件信息传递


文件拖过去,是为了切换文件夹,在这里你可能会进行一些异步的操作,比如请求后端更换文件在数据库中的路径等。我们的需求多种多样,但是归根到底都是获取到文件的数据,并传递到文件夹中


DataTransfer对象


DragEvent.dataTransfer: 在拖放交互期间传输的数据


我们主要使用它的两个方法:



  • DataTransfer.setData(format, data):就是设置键值对,把我们要传的数据添加到drag object

  • DataTransfer.getData(format):根据键获取保存的数据


知道了这两个方法,相信你一定就有实现思路了 👊


拖拽开始 --> setData添加数据 --> 进入可放置区域 --> 放置时getData获取数据 --> 完成


1:给文件设置dragstart事件


// 开始拖拽事件

draggable.addEventListener('dragstart', (event) => {

const data = event.target.innerText;

event.dataTransfer.setData('name', data); //添加数据

})

2:在dragover事件中用event.preventDefault()阻止默认行为,允许拖拽元素放置到该元素上,否则无法触发drop事件


// 添加dragover事件处理程序

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover');

event.preventDefault(); //新增

});

3:给文件夹设置放置事件drop


// 添加drop事件处理程序

files.addEventListener('drop', (event) => {

const data = event.dataTransfer.getData('name'); // 获取文件的数据

const text = document.createTextNode(data);

files.appendChild(text);

event.target.classList.remove('fileshover'); // 记得放置后也要移除类

});

实现效果:


Kapture 2023-03-14 at 14.46.45.gif


四:完整代码:


<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

<style>

.dragdiv {

width: 100px;

height: 100px;

background-color: bisque;

-webkit-user-drag: element;

}

.files {

width: 200px;

height: 200px;

background-color: rgba(0, 255, 255, 0.376);

margin-top: 100px;

}

.fileshover {

background-color: rgba(0, 255, 255, 0.979);

}

</style>

</head>

<body>

<div draggable="true" class="dragdiv">我是文件1</div>

<div class="files">

<p>文件夹</p>

拖动文件名称:

</div>

<script>

const draggable = document.querySelector('.dragdiv');

const files = document.querySelector('.files');

// 开始拖拽事件

draggable.addEventListener('dragstart', (event) => {

const data = event.target.innerText;

event.dataTransfer.setData('name', data);

})

// 添加dragover事件处理程序

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover')

event.preventDefault()

});

// 添加dragleave事件处理程序

files.addEventListener('dragleave', (event) => {

event.target.classList.remove('fileshover')

});

// 添加drop事件处理程序

files.addEventListener('drop', (event) => {

const data = event.dataTransfer.getData('name')

const text = document.createTextNode(data)

files.appendChild(text);

event.target.classList.remove('fileshover')


});

</script>

</body>

</html>

总结:以上只是简单的熟悉拖拽事件的整个过程,你可以在此拓展更多自己想要功能,欢迎分享👏


作者:隐兮
来源:juejin.cn/post/7210256070299549755
收起阅读 »

Android 指纹识别(给应用添加指纹解锁)

使用指纹 说明 : 指纹解锁在23 的时候,官方就已经给出了api ,但是由于Android市场复杂,无法形成统一,硬件由不同的厂商开发,导致相同版本的软件系统,搭载的硬件千变万化,导致由的机型不支持指纹识别,但是,这也挡不住指纹识别在接下来的时间中进入An...
继续阅读 »

使用指纹



说明 : 指纹解锁在23 的时候,官方就已经给出了api ,但是由于Android市场复杂,无法形成统一,硬件由不同的厂商开发,导致相同版本的软件系统,搭载的硬件千变万化,导致由的机型不支持指纹识别,但是,这也挡不住指纹识别在接下来的时间中进入Android市场的趋势,因为它相比较输入密码或图案,它更加简单,相比较密码或者图案,它更炫酷 ,本文Demo 使用最新的28 支持的androidx 库中的API及最近火热的kotlin语言完成的



需要知道的



  • FingerprintManager : 指纹管理工具类

  • FingerprintManager.AuthenticationCallback :使用验证的时候传入该接口,通过该接口进行验证结果回调

  • FingerprintManager.CryptoObject: FingerprintManager 支持的分装加密对象的类



以上是28以下API 中使用的类 在Android 28版本中google 宣布使用Androidx 库代替Android库,所以在28版本中Android 推荐使用androidx库中的类 所以在本文中我 使用的是推荐是用的FingerprintManagerCompat 二者的使用的方式基本相似



如何使用指纹



  • 开始验证 ,系统默认的每段时间验证指纹次数为5次 次数用完之后自动关闭验证,并且30秒之内不允行在使用验证


验证的方法是authenticate()


/**
*
*@param crypto object associated with the call or null if none required.
* @param flags optional flags; should be 0
* @param cancel an object that can be used to cancel authentication
* @param callback an object to receive authentication events
* @param handler an optional handler for events
**/

@RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
public void authenticate(@Nullable CryptoObject crypto, int flags,
@Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
@Nullable Handler handler)
{
if (Build.VERSION.SDK_INT >= 23) {
final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
if (fp != null) {
android.os.CancellationSignal cancellationSignal = cancel != null
? (android.os.CancellationSignal) cancel.getCancellationSignalObject()
: null;
fp.authenticate(
wrapCryptoObject(crypto),
cancellationSignal,
flags,
wrapCallback(callback),
handler);
}
}
}



arg1: 用于通过指纹验证取出AndroidKeyStore中key的值
arg2: 系统建议为0




arg3: 取消指纹验证 手动关闭验证 可以调用该参数的cancel方法




arg4:返回验证结果




arg5: Handler fingerprint 中的
消息都是通过handler来传递的 如果不需要则传null 会自动默认创建一个主线程的handler来传递消息



使用指纹识别的条件



  • 添加权限(这个权限不需要在6.0中做处理)

  • 判断硬件是否支持

  • 是否已经设置了锁屏 并且已经有一个被录入的指纹

  • 判断是否至少存在一条指纹信息




通过零碎的知识完成一个Demo


这里写图片描述


指纹识别通过之后跳转到 指定页面


进入之后首先弹出对话框,进行指纹验证


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/fingerprint" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:text="验证指纹" />


<TextView
android:id="@+id/fingerprint_error_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:maxLines="1" />


<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:layout_marginRight="5dp"
android:background="#696969" />


<TextView
android:id="@+id/fingerprint_cancel_tv"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center"
android:text="取消"
android:textSize="16sp" />


</LinearLayout>


使用DialogFragment 完成对话框 新建一个DialogFragment 并且初始化相关的api


 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//获取fingerprintManagerCompat对象
fingerprintManagerCompat = FingerprintManagerCompat.from(context!!)
setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog)
}


在界面显示在前台的时候开始扫描


override fun onResume() {
super.onResume()
startListening()
}
@SuppressLint("MissingPermission")
private fun startListening() {
isSelfCancelled = false
mCancellationSignal = CancellationSignal()
fingerprintManagerCompat.authenticate(FingerprintManagerCompat.CryptoObject(mCipher), 0, mCancellationSignal, object : FingerprintManagerCompat.AuthenticationCallback() {
//验证错误
override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) {
if (!isSelfCancelled) {
errorMsg.text = errString
if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
Toast.makeText(mActivity, errString, Toast.LENGTH_SHORT).show()
dismiss()
mActivity.finish()
}
}
}
//成功
override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) {
MainActivity.startActivity(mActivity, true)
}
//错误时提示帮助,比如说指纹错误,我们将显示在界面上 让用户知道情况
override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) {
errorMsg.text = helpString
}
//验证失败
override fun onAuthenticationFailed() {
errorMsg.text = "指纹验证失败,请重试"
}
}, null)
}

在不可见的时候停止验证


if (null != mCancellationSignal) {
mCancellationSignal.cancel()
isSelfCancelled = true
}

在MainActivity 中首先判断是否验证成功 是 跳转到目标页 否则的话需要进行验证
在这个过程中我们需要做的就是判断是否支持,判断是否满足指纹验证的条件(条件在上面)


if (intent.getBooleanExtra("isSuccess", false)) {
WelcomeActivity.startActivity(this)
finish()
} else {
//判断是否支持该功能
if (supportFingerprint()) {
initKey() //生成一个对称加密的key
initCipher() //生成一个Cipher对象
}
}


验证条件


 if (Build.VERSION.SDK_INT < 23) {
Toast.makeText(this, "系统不支持指纹功能", Toast.LENGTH_SHORT).show()
return false
} else {
val keyguardManager = getSystemService(KeyguardManager::class.java)
val managerCompat = FingerprintManagerCompat.from(this)
if (!managerCompat.isHardwareDetected) {
Toast.makeText(this, "系统不支持指纹功能", Toast.LENGTH_SHORT).show()
return false
} else if (!keyguardManager.isKeyguardSecure) {
Toast.makeText(this, "屏幕未设置锁屏 请先设置锁屏并添加一个指纹", Toast.LENGTH_SHORT).show()
return false
} else if (!managerCompat.hasEnrolledFingerprints()) {
Toast.makeText(this, "至少在系统中添加一个指纹", Toast.LENGTH_SHORT).show()
return false
}
}

必须生成一个加密的key 和一个Cipher对象


//生成Cipher
private fun initCipher() {
val key = keyStore.getKey(DEFAULT_KEY_NAME, null) as SecretKey
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
cipher.init(Cipher.ENCRYPT_MODE, key)
showFingerPrintDialog(cipher)
}
//生成一个key
private fun initKey() {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(DEFAULT_KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}

Demo 是kotlin 写的
Demo地址


作者:狼窝山下的青年
来源:juejin.cn/post/7210220134601572410
收起阅读 »

面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯” 到 “觉悟” ,未来又如何落地

对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等,而其中不乏一些玩的很「花」的场景,例如: ChatPDF :使用 ChatPDF...
继续阅读 »

对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等,而其中不乏一些玩的很「花」的场景,例如:




  • ChatPDF :使用 ChatPDF 读取 PDF 之后,你可以和 PDF 文件进行「交谈」,就好像它是一个完全理解内容的「人」一样,通过它可以总结中心思想,解读专业论文,生成内容摘要,翻译外籍,并且还支持中文输出等





  • BiBiGPT : 一键总结视频内容,主要依赖字幕来做总结,绝对是「二创」作者的摸鱼利器。





所以把 ChatGPT 理解为「搜索引擎」其实并不正确,从上述介绍的两个落地实现上看, ChatGPT 不是单纯的统计模型,它的核心并不是完全依赖于它的「语料库」,更多来自于临场学习的能力「 in-context learning」,这就是 ChatGPT 不同于以往传统 NLP「一切都从语料的统计里学习」的原因



当然,我本身并非人工智能领域的开发者,而作为一个普通开发者,我更关心的是 ChatGPT 可以如何提升我的开(mo)发(yu)效率,只是没想到随手一试,我会被 ChatGPT 的 「 in-context learning」 给「逼疯」。



ChatGPT & UI


相信大家平时「面向」 ChatGPT 开发时,也是通过它来输出「算法」或者「 CURD」 等逻辑居多,因为这部分输出看起来相对会比较直观,而用 ChatGPT 来绘制前端 UI 的人应该不多,因为 UI 效果从代码上看并不直观 ,而且 ChatGPT 对与 UI 的理解目前还处于 「人工智障」的阶段。



但是我偏偏不信邪。。。。。



因为近期开发需求里恰好需要绘制一个具有动画效果的 ⭐️ 按键,面对这么「没有挑战性」的工作我决定尝试交给 ChatGPT 来完成,所以我向 ChatGPT 发起了第一个命令:



「用 Flutter 画一个黄色的五角星」




结果不负众望,关键部分如下代码所示,Flutter 很快就提供了完整的 Dart 代码,并且还针对代码提供了代码相关实现的讲解,不过运行之后可以看到,这时候的 ⭐️ 的样式并不满足我们的需求。



此时顶部的角也太「肥」了 。




所以我随着提出了调整,希望五角星的五个角能够一样大,只是没想到我的描述,开始让 ChatGPT 放飞自我



也许是我的描述并不准确?




在我满怀期待的 cv 代码并运行之后,猝不及防的「五角星」差点没让我喷出一口老血,虽然这也有五个角,但是你管这个叫 「五角星」 ???



这难道不是某个红白机游戏里的小飞机??




甚至于在看到后续 ChatGPT 关于代码的相关讲解时,我觉得它已经开始在「一本正经的胡说八道」,像极了今天早上刚给我提需求的产品经理



哪里可以看出五个角相同了???




接着我继续纠正我的需求,表示我要的是 「一个五个角一样大的黄色五角星」 ,我以为这样的描述应过比较贴切,须不知·····



如下代码所示,其实在看到代码输出 for 循环时我就觉得不对了,但是秉承着「一切以实物为准」的理念,在运行后不出意外的发生了意外,确实是五个角一样大,不过是一个等边五边形。



算一个发胖的 ⭐️ 能解(jiao)释(bian)过去不?




再看 ChatGPT 对于代码的描述,我发现我错了,原来它像的是「理解错需求还在嘴硬的我」,只是它在说「这是一个五角星」的时候眼皮都不会眨一下



AI:确实五个角一样大,五个角一样大的五边形为什么就不能是五角星?你这是歧视体型吗?




所以我继续要求:「我要的是五角星,不是五边形」,还好 ChatGPT 的临场学习能力不错,他又一次「重新定义五角星」,不过我此时我也不抱希望,就是单纯想看看它还能给出什么「惊喜」



不出意外,这个「离谱」的多边形让我心头一紧,就在我想着是否放弃的时候,身为人类无法驯服 AI 「既爱又恨」的复杂情绪,让我最终坚持一定要让 ChatGPT 给我画出一个 ⭐️。



不过心灰意冷之下,我选择让 ChatGPT 重新画一个黄色五角星,没想道这次却有了意外的惊喜,从下面的图片可以看到,此时的 ⭐️ 除了角度不对,形状已经完全满足需求。



所以一个问题我多问几遍,也许就能接近我要的答案?




事实上这也是目前 ChatGPT 的现状,因为「临场学力」能力等因素影响,同一个问题它可能会给出不同的答案,而有的答案其实和我们要的根本不沾边



那么,接下来只要让 ChatGPT 把 ⭐️ 旋转一下角度,应该就可以完成需求····了吧?所以我提出「帮我旋转 180度」的要求。



结果不出意外的还是发生了意外,可能 ChatGPT 理解的 180 度和我不大一样,如下图所示,确实旋转了,只是还是歪的,而基于前面的尝试,我觉得有必要再给它一次机会。



终于,在我换了以下描述之后,ChatGPT 似乎「开窍」了,终于让我如愿以偿的得到了我想要的 ⭐️ 代码。



可以看到,其实这段代码并不复杂,你自己写或者去搜索引擎扒一扒大概会比用 ChatGPT 更快,但是 ChatGPT 的魅力就在于:它也许会错,但是总有一次它会「蒙对」,如果现在还不对,那么未来也会有算对的时候,这就是我感受到的 AI 魅力所在



既然五角星都出来,那么接下来我们来完成最后一步的动画效果,我们希望的是点击 ⭐️ 的时候可以有一个爆炸粒子的动画,所以我提出了:



「希望加上一个动画效果,就是点击的时候星星爆炸成颗粒的动画效果」




结果还是「一口老血」,如下图所示,「星星确实爆炸了」,只是这个爆炸动画看起来是那么的「一言难尽」,感兴趣的可以 cv 下面的源代码自己体验下,不出意外接下来又是新的一轮人类和 AI 的「斗智斗勇」。




class YellowStar extends StatefulWidget {
@override
_YellowStarState createState() => _YellowStarState();
}

class _YellowStarState extends State<YellowStar> with TickerProviderStateMixin {
bool _isExploded = false;
late AnimationController _animationController;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

void _handleTapDown(TapDownDetails details) {
setState(() {
_isExploded = true;
_animationController.forward(from: 0.0);
});
}

void _resetStar() {
setState(() {
_isExploded = false;
_animationController.reset();
});
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTap: _resetStar,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: _isExploded
? BorderRadius.circular(50)
: BorderRadius.zero,
),
child: _isExploded
? _buildParticles()
: Container(
width: 100,
height: 100,
child: CustomPaint(
painter: YellowStarPainter(),
),
),
),
);
}

Widget _buildParticles() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: 100,
height: 100,
child: Stack(
children: List.generate(
50,
(index) {
double radius = _animation.value * 50;
double angle = 2 * pi * index / 50;
double x = 50 + cos(angle) * radius;
double y = 50 + sin(angle) * radius;
return Positioned(
left: x,
top: y,
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.yellow,
),
),
);
},
),
),
);
},
);
}
}



class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;

@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;

Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;

for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}

radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}

path.close();
canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

经过一轮「鏖战」如下图所示,面对同一个问题,ChatGPT 总能找到不同的实现方式来回答你,只是这些实现里有的无法运行的,有的是运行没有效果的,有的甚至是给你输出了一个 //TODO 爆炸动画 ,总之就是让你「血压飙升」。





当然,「胜利终将属于人类」,如下图所示,在经历了无数次「血压飙升」的场景之后,最终我还是得到了我想要的:点击 ⭐️,然后出现了💥的动画效果,尽管还是不完美,但是基本满足了我后续开发的需求。



这个代码完完全全由 ChatGPT 输出,我一句没改,完整代码贴在了后面。





class YellowStarPage extends StatefulWidget {
@override
_YellowStarPageState createState() => _YellowStarPageState();
}

class _YellowStarPageState extends State<YellowStarPage>
with SingleTickerProviderStateMixin
{
late AnimationController _animationController;
bool _isExploded = false;

@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

void _handleStarTap() {
if (!_isExploded) {
_isExploded = true;
_animationController.forward(from: 0);
Future.delayed(Duration(milliseconds: 500), () {
setState(() {
_isExploded = false;
});
});
}
setState(() {});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Yellow Star')),
body: Center(
child: GestureDetector(
onTap: _handleStarTap,
child: Container(
width: 300,
height: 300,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: YellowStarPainter(_animationController.value,
isExploded: _isExploded),
);
},
),
),
),
),
);
}
}

class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;

final double animationValue;
final bool isExploded;

YellowStarPainter(this.animationValue, {this.isExploded = false});

@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;

Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;

if (isExploded) {
double particleSize = starSize / 30;
paint.strokeWidth = 1;
paint.style = PaintingStyle.fill;
paint.color = Colors.yellow;
Random random = Random();

for (int i = 0; i < 30; i++) {
double dx = random.nextDouble() * starSize - starSize / 2;
double dy = random.nextDouble() * starSize - starSize / 2;
double x = centerX + dx * (1 + animationValue);
double y = centerY + dy * (1 + animationValue);

canvas.drawCircle(Offset(x, y), particleSize, paint);
}
} else {
for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}

radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}

path.close();
canvas.drawPath(path, paint);
}
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}


最后,给大家欣赏一下我让 ChatGPT 画一只米老鼠的「心路历程」,很明显这一次「人类一败涂地」,从目前的支持上看,让 ChatGPT 输出复杂图像内容并不理想,因为它不的笔画「不会拐弯」。




真的是又爱又恨。



最后


经过上面的一系列「折腾」,可以看到 ChatGPT 并没有我们想象中智能,如果面向 GPT 去开发,甚至可能并不靠谱,因为它并不对单一问题给出固定答案,甚至很多内容都是临场瞎编的,这也是因为大语言模型本身如何保证「正确」是一个复杂的问题,但是 ChatGPT 的魅力也来自于此:



它并不是完全基于语料来的统计来给答案



当然这也和 ChatGPT 本身的属性有关系, ChatGPT 目前的火爆有很大一部分属于「意外」,目前看它不是一个被精心产品化后的 2C 产品,反而 ChatPDFBiBiGPT 这种场景化的包装落地会是它未来的方向之一。


而现在 OpenAI 发布了多模态预训练大模型 CPT-4GPT-4 按照官方的说法是又得到了飞跃式提升:强大的识图能力;文字输入限制提升至 2.5 万字;回答准确性显著提高;能够生成歌词、创意文本,实现风格变化等等



所以我很期待 ChatGPT 可以用 Flutter 帮我画出一只米老鼠, 尽管 ChatGPT 现在可能会让你因为得到 1+1=3 这样的答案而「发疯”」,但是 AI 的魅力在于,它终有一天能得到准确的结果


作者:恋猫de小郭
来源:juejin.cn/post/7210605626501595195
收起阅读 »

如何写一个炫酷的大屏仿真页

web
前言 之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样: 看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿...
继续阅读 »

前言


之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样:


OPPO折叠屏


看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿真。


看效果:


11.gif


由于使用录屏,所以看着有点卡顿,实际效果非常流畅!


一、基础知识具备


仿生页里面用到很多自定义 View 的知识,比如:



  1. 贝塞尔曲线

  2. 熟悉 Canvas、Paint 和 Path 等常用的Api

  3. Matrix


具备这些知识以后,我们就可以看懂绝大部分的代码了。这一篇同样并不想和大家过多的介绍代码,具体的可以看一下代码。


二、双仿真和单仿真有什么不同


我写双仿真的时候,感觉和单仿真有两点不同:



  • 绘制的页数

  • 背部的贴图处理


首先,单仿真只要准备两页的数据:


QQ20230312-0.jpg


背部的内容也是第一页的内容,需要对第一页内容进行翻转再平移。


而双仿真需要准备六页的内容,拿左边来说:


QQ20230312-1.jpg


我们需要准备上层图片(柯基)、背部图片(阿拉斯加)和底部图片(吉娃娃,看不清),因为我们不知道用户会翻页哪侧,所以两侧一共需要准备六页的数据。


由于翻转机制的不一样,双仿真对于背部的内容只需要平移就行,但是需要新的一页内容,这里相对来说比单仿真简单。


三、我做了哪些优化


主要对翻页的思路进行了优化,


正常的思路是这样的,手指落下的点即页脚:


QQ20230312-2.jpg


这样写起来更加简单,但是对于用户来说,可操作的区域比较小,相对来说有点难用。


另外一种思路就是,手指落下的点即到底部同等距离的边:


QQ20230312-4.jpg


即手指落位的位置到当前页页脚距离 = 翻动的位置到当前页脚的距离


使用这种方式的好处就是用户可以操作的区域更大,翻书的感觉跟翻实体书的感觉更类似,也更加跟手。


总结


这篇文章就讲到这了,这个 Demo 其实是一个半成品,还有一些手势没处理,阴影的展示还有一些问题。


写仿真比较难的地方在于将一些场景转化成代码,有些地方确实很难去想。


talk is cheap, show me code:


仓库地址:github.com/mCyp/Double…


如果觉得本文不错,点赞是对本文最好的肯定,如果你还有任何问题,欢迎评论区讨论!


作者:九心
来源:juejin.cn/post/7209625823581978680
收起阅读 »

Android 可视化预览及编辑Json

Android 可视化编辑json JsonPreviewer 项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。 开发时单个广告可能需要多次修改配置来测试...
继续阅读 »


Android 可视化编辑json JsonPreviewer


项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。


开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效





    1. 每次抓包修改配置





    1. 本地导入配置,从磁盘读取




但两种方式都有一定弊端



  • 首先测试时依赖电脑修改配置

  • 无法直观预览广告配置


考虑到开发时经常使用的Json格式化工具,既可以直观的预览Json, 还可以在线编辑


那么就考虑将Json格式化工具移植到项目测试模块中


web网页可以处理Json格式化,同理在Android webView 中同样可行, 只需要引入处理格式化的JS代码即可。


查找资料,发现一个很实用的文章可视化编辑json数据——json editor


开始处理


首先准备好WebView的壳子


    //初始化
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
binding.webView.settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportZoom(true)
useWideViewPort = true
builtInZoomControls = true
}
binding.webView.addJavascriptInterface(JsInterface(this@MainActivity), "json_parse")
}

//webView 与 Android 交互
inner class JsInterface(context: Context) {
private val mContext: Context

init {
mContext = context
}

@JavascriptInterface
fun configContentChanged() {
runOnUiThread {
contentChanged = true
}
}

@JavascriptInterface
fun toastJson(msg: String?) {
runOnUiThread { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show() }
}

@JavascriptInterface
fun saveConfig(jsonString: String?) {
runOnUiThread {
contentChanged = false
Toast.makeText(mContext, "verification succeed", Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun parseJsonException(e: String?) {
runOnUiThread {
e?.takeIf { it.isNotBlank() }?.let { alert(it) }
}
}
}


加载json并在WebView中展示



viewModel.jsonData.observe(this) { str ->
if (str?.isNotBlank() == true) {
binding.webView.loadUrl("javascript:showJson($str)")
}
}


WebView 加载预览页面


        binding.webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel.loadAdConfig(this@MainActivity)
}
}

binding.webView.loadUrl("file:///android_asset/preview_json.html")



Json 预览页, preview_json.html实现



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="jquery.json-viewer.css"
rel="stylesheet" type="text/css">

</head>
<style type="text/css">
#json-display {
margin: 2em 0;
padding: 8px 15px;
min-height: 300px;
background: #ffffff;
color: #ff0000;
font-size: 16px;
width: 100%;
border-color: #00000000;
border:none;
line-height: 1.8;
}
#json-btn {
display: flex;
align-items: center;
font-size: 18px;
width:100%;
padding: 10;

}
#format_btn {
width: 50%;
height: 36px;
}
#save_btn {
width: 50%;
height: 36px;
margin-left: 4em;
}

</style>
<body>
<div style="padding: 2px 2px 2px 2px;">
<div id="json-btn" class="json-btn">
<button type="button" id="format_btn" onclick="format_btn();">Format</button>
<button type="button" id="save_btn" onclick="save_btn();">Verification</button>

</div>
<div>
<pre id="json-display" contenteditable="true"></pre>
</div>
<br>
</div>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.json-viewer.js"></script>
<script>

document.getElementById("json-display").addEventListener("input", function(){
console.log("json-display input");
json_parse.configContentChanged();
}, false);
function showJson(jsonObj){
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}
function format_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var jsonObj = JSON.parse(jsonval); //parse string to json
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}


function save_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var saveFailed = false;
try {
var jsonObj = JSON.parse(jsonval); //parse
} catch (e) {
console.error(e.message);
saveFailed = true;
json_parse.parseJsonException(e.message); // throw exception
}
if(!saveFailed) {
json_parse.saveConfig(jsonval);
}
}

</script>
</body>
</html>


这其中有两个问题需注意





    1. 如果value的值是url, 格式化后缺少引号
      从json-viewer.js源码可以发现,源码中会判断value是否是url,如果是则直接输出




处理方式:在json 左右添加上双引号


    if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + '"' +json + '"' + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '
\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}




    1. 如果折叠后json-viewer会增加<a>标签,即使使用text()方法获取到纯文本数据,这里面也包含了“n items”的字符串,那么该如何去除掉这些字符串呢?




 var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();

总结


使用时只需将json文件读取,传入preview_json.html的showJson方法


编辑结束后, 点击Save 即可保存


示例代码 Android 可视化编辑json JsonPreviewer




(可视化编辑json数据——json editor)[blog.51cto.com/u_56500

11/5…]

收起阅读 »

我本可以忍受黑暗,如果我未曾见过光明

随想录】我本可以忍受黑暗,如果我未曾见过光明 随想录 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 老文章? 这篇文章大体结构早已在我语雀...
继续阅读 »

随想录】我本可以忍受黑暗,如果我未曾见过光明



随想录


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)



视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/
复制代码

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



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

你没有必要完全辞去工作

我认为我们可以而且应该探索生活中的许多事情,我写这篇文章是为了展示成为一名创客和拥有一份全职工作不仅是可能的,而且使得你可用的机会多样化,这可以让你更加敏锐,务实与坚定。 在这篇文章中,我想解决三个关键概念。 首先是针对那些认为自己时间不够的人,以及为什么我觉...
继续阅读 »

我认为我们可以而且应该探索生活中的许多事情,我写这篇文章是为了展示成为一名创客和拥有一份全职工作不仅是可能的,而且使得你可用的机会多样化,这可以让你更加敏锐,务实与坚定。


在这篇文章中,我想解决三个关键概念。


首先是针对那些认为自己时间不够的人,以及为什么我觉得这种观念往往是错误的。


第二是强调坚持工作的好处,以及为什么成功人士最擅长的是降低风险,而不是最大化。


最后,第三部分将指出出一些我认为我们都可以在思维方式上做出的改进--超越单纯地创业和全职工作,这些概念希望能帮助你在一个更大的空间里进行优化,或者也许完全去除这个限制。


1.“我没有足够的时间”


美国人平均工作时长为 8.8 小时,这源于工业革命,并一直延续到 21 世纪,这处于一般的考量,而不是深思熟的考虑。罗伯特·欧文 (Robert Owen) 精心设计了“八小时劳动、八小时娱乐、八小时休息”的说法,努力让人们在合理的时间内工作,同时仍然能有效地运营工厂。


尽管世界和劳动力发生了翻天覆地的变化,但这种“工作时间”和“我的时间”的概念至今仍然存在。我在这里不是要质疑 40 小时模型(已经有太多资源了 - 谁还没有听说过 4 小时工作周?),而是质疑对“我的时间”的看法。


对许多人来说,长时间的工作意味着他们有权享受这种 "我的时间",并将 "我的时间 "设计得与 "工作时间 "尽可能地不同。对许多人来说,它看起来很像这样。Netflix and chill


但是,如果我们不再将“我的时间”想象成放松时间,而是完全按照它的标题:是时候专注于自己并与您的目标保持一致,那会怎样呢?如果你需要休息,那就休息吧。但如果你的目标是有朝一日成为一名企业家,那么应该投入大量的“我的时间”来实现这一目标,因为它不会自行发生。 “我的时间”不应该只是不累人的活动,而是任何可以帮助个人达到他们希望进入的未来状态的活动。


每天大约有 16 个小时分配给工作和睡眠,每个人大约有 8 个小时可以分配给“我的时间”,如果使用得当,每年将近 3000 个小时可以取得很多成就。


Sleep, commute, work, repeat. 睡觉、通勤、工作,如此重复。


Sleep, commute, work, repeat. 睡觉、通勤、工作,如此重复。



“Most people overestimate what they can do in a day, but underestimate what they can do in a year.”

“大多数人高估了他们一天能做的事情,却低估了他们一年能做的事情。”



还有一种误解是,为了建立一个可持续的业务,你需要花费大量的时间才能达到目的。虽然确实需要付出大量的努力,但最重要的是长期持续的努力。大多数人低估了复利的这个概念。


compound-interest.jpg


The power of compound interest. 复利的力量。


看看下面的等式:



  • 1.01³⁶⁵ = 37.8

  • 1.10³⁰ = 17.5


在一年内每天坚持改善你的业务(或生活)1%,比一个月内每天改善 10% 的效果要好一倍。坚持不懈加上复利的力量是强大的。



“如果一切都是最重要的,那么就没有什么是重要的。”



我认为,大多数人在生活中要么没有清楚地确定优先事项,要么将所有事情都考虑在内。虽然我相信雄心壮志,但成功的一个关键步骤是确定核心的优先事项,并消除在此之外的噪音。


最主要的优先事项是动态的,可以随着时间的推移而改变,但我认为,在一个特定的时间,你真的不能有超过 3 个核心重点。


设定这些重点之后,就是要改变行为,按照这些重点生活。再次,如果大多数人要客观地反思他们是如何花费时间的,他们会得到这样的结果。


1_nALgHXJAKmPcyqO10XvvCw.jpg


典型的一天。


对我来说,这就是我的个人优先事项随着时间的推移而发生的变化:



  • 2017 年:工作、旅行、人际关系

  • 2018 年:工作、学习编码、构建副业

  • 2019 年:工作、扩展副业、分享想法(写作、演讲)


为了在创作项目的同时维持一份全职工作,我不得不排除干扰。例如,我不看电视。我不通勤。我目前没有恋爱。这些都是主动的选择。


当然,其中一些东西将是暂时的(例如:人际关系),但我也注意到我在生活中重新引入的东西,以及它是否会促进、带走,或成为我的北极星之一。


我认为这个概念也可以被认为是分层的时间投资。对于你所做的任何事情,如果它有助于你的北极星,就把它看作是一级投资。对于那些对你的成长完全没有贡献的事情,也许可以把它标记为第四级。这并不意味着你不能跨层花费时间,但你花在每个层级上的时间应该反映出你对它们的关心程度。


示例(这对任何人来说都是一个独立的练习):



2. 坚持工作的好处


希望上一节有助于说服你,你有足够的时间全职工作,同时创建副业,或者说,如果你调整你的价值观→优先事项→行为,就可以在你的生活中融入更多的东西。在这一节中,我希望能表达为什么保持全职工作可以是一件美好的事情。


付费学习



“Some workplaces are definitely broken, but the entire workforce isn’t.”



我经常听到有人说 "我等不及要出去 "这样的话,指的是辞掉工作,最终自己当老板。在辞职之前,要考虑为什么要辞掉工作。通常情况下,这不是他们有全职工作的问题,而是他们所从事的特定工作,或许是他们所汇报的特定人员。


所有的人都应该努力找到一份能够赋予他们权力、激励他们并让他们在某些方面得到成长的工作。大公司实际上保证了这一点--你很少是公司里最聪明的人,你当然也不会是公司里每个方面都最有能力的人


在我的 "日常工作 "中,我可以不断向比我更聪明的人学习,并为此获得报酬。我还面临着我的副业项目根本不会遇到的挑战,我经常需要学习如何与他人一起解决这些挑战。我鼓励人们有意识地设计他们的职业道路,以掌握从硬到软的新技能。如果你最终决定在未来自立门户,那么这两者都会很重要。


随着劳动力变得更加活跃,在协同处理自己的项目的同时向他人学习的能力是许多人正在开发的。事实上,我在 Twitter 上对数百人进行了调查,发现相当多的人都在这样做。


保持新鲜的想法和清晰的头脑


在学习之外,保持一份 FT 工作还有其他实实在在的好处,可以帮助你建立一个更可持续的副业。


根据个人经验,我发现把我的工作和副业分开,使我仍能在两者中找到独立的乐趣。每当我从一个环境切换到另一个环境,特别是在创业方面,它仍然是 "有趣的"。


我认为这特别是因为在目前的状态下,创业不是我的生命线。我希望有一天它确实成为更有意义的东西,但就目前而言,我可以在不受立即赚钱需求影响的情况下就我的项目做出决定。


更重要的是,我可以专注于通过我真正关心的项目来表达自己,而不是专注于可能产生美元的东西,通过这个过程,我贴近我的价值观。换句话说,我可以专注于创造价值,而不是专门去获取价值,类似于 Gumroad 的创始人 Sahil Lavingia 如何转向做这件事,或者 Warby Parker 的创始人如何确保金钱不会战胜他们的价值观。



“开始之前我们是四个朋友,我们承诺公平对待彼此比成功更重要。” —— 亚当·格兰特



结合以上几点,当我意识到一个项目没有任何价值时,我可以放弃一个项目或理性思考,我也不需要拿 VC 的钱或倾向于我不相信的投资者。



“在一个领域拥有安全感让我们可以自由地在另一个领域独创。通过在财务上覆盖我们的基地,我们摆脱了出版半生不熟的书籍、出售劣质艺术品或开展未经考验的业务的压力。” —— 亚当·格兰特



最后,我可以在技能学习方面投入适当的时间。我把这比喻为这样一个概念:上市公司不太关注通过创新创造长期价值,而是关注下一个季度的收入数字。我是一只私人股票,可以专注于我自己和我的技能,目的是为了长期建设它们。


换句话说,我的表达和创意之间的明确区分与我的生命线分离,我认为这有助于做出更有效的决定。


进行大量试验,然后全力以赴



“企业家这个词,正如经济学家理查德·坎蒂隆创造的那样,字面意思是“风险承担者”。 —— 亚当·格兰特



有一个普遍的误解,认为企业家都是“冒险者”,你需要“全力以赴”才能成功。在亚当·格兰特的著作 Originals (中文名:《离经叛道:不按常理出牌的人如何改变世界》)中,这两者都被证明是错误的;企业家不一定是冒险者,而是更善于评估风险和对冲他们的赌注。



“当 Pierre Omidyar 创立 eBay 时,这只是一种爱好;在接下来的九个月里,他一直以程序员的身份工作,直到他的在线市场为他赚的钱比他的工作还多才离开。最好的企业家不是风险最大化者。 “他们在冒险中承担了风险。” —— 亚当·格兰特



Grant 还引用了 Joseph Raffiee 和 Jie Feng 的另一项研究,该研究从 1994 年到 2008 年对 5000 多名美国人提出了以下问题:“当人们开始创业时,他们最好是继续工作还是辞掉日常工作?”


结果呢?他们发现,那些离开工作岗位的人这样做不是出于经济需要,而是出于纯粹的自信。然而,那些更不确定的人比更喜欢冒险的人失败几率要低 33%。


另一项研究表明,那些在 Fast Company 最具创新力排行榜上名列前茅的企业家也倾向于坚持他们的日常工作,包括著名企业家 Phil Knight(耐克)、Steve Wozniak(苹果)以及谷歌创始人 Larry Page 和 Sergey Brin。


奈特当了 5 年的会计师,同时从他的后备箱里卖鞋,沃兹尼亚克继续在惠普工作,谷歌人继续在斯坦福大学攻读博士学位。这些只是书中的一些原作——Grant 还引用了类似的故事,包括 Brian May 在加入 Queen 之前研究天体物理学,John Legend 即使在发行他的第一张专辑后仍然担任管理顾问,Spanx 创始人 Sara Blakely 销售传真机,她的公司原型和规模最终成为世界上最年轻的白手起家的亿万富翁,著名作家斯蒂芬金在他的第一个故事发表后担任了 7 年的看门人、教师和加油站服务员。


我们都有多种激情,我认为生活就是在有意义的时候进行战略转型。无需立即从一个场景切换到另一个场景。人们可能认为冒险者很酷,但在另一边取得成功更酷。


3. 重构你的思维方式


无论你是否选择全职工作,同时探索副业项目,我认为我们都可以更有效地打开我们的思想,接受不同的思维方式。本节将涉及一些我认为我们可以停止限制自己和他人的方式。


世间安得两全法


人们喜欢把东西装进盒子里。你会听到人们总是使用名词或形容词作为明确的标签:



  • 技术还是非技术

  • 快乐或悲伤

  • 职员或企业家


看到我要去哪里了吗?尽管有这些标签,但我相信几乎所有东西都可以用某种曲线表示;特别是在技能习得方面。例如,你什么时候真正“成为”程序员?


1_MEZ99GbXHnSq27aDQaBxcQ.jpeg


真正的创造性思维者不再用二元思维,而是能够将这些曲线的概念内化。他们把事情看成一个斜坡、楼梯或维恩图,而不是一系列的盒子。当你消除二元对立时,你就能更清楚地看到其他选择,比如慢慢增加你对副业的时间投入,而不是立即辞职。


合理规划您的生活


我认为,如果有人认为自己的工作效率已经达到全球最高水平,那是非常天真的。事实是,我们都有改进的余地,不仅是在更快/更干净方面,而且是在做出更好地决定,删除那些首先不应该出现在我们盘子里的工作。


如果您选择从事多项工作,请确保您对所有这些都有独立的 KPI。人们倾向于在企业中这样做,但这个概念在我们的个人生活中却很少见。你能量化过去一年你在自己身上投入了多少时间吗?大多数人做不到。


如果两者都没有 KPI,那么没有明确 KPI 的那个自然会被搁置一旁,或者得不到应有的关注。


我还认为理解“元工作”的概念很重要。我对元工作的定义如下:“如果你连续一年做那个活动,你的生活会有什么不同吗?”


让我详细说明。


如果明年我每天都回复电子邮件,我的生活会不会发生重大变化?换句话说,我会从 A 搬到 B 吗?答案是不。


洗衣服、买杂货或做指甲等事情也是如此。哦,是的,Netflix 也一样。


还有第二种类型的任务,我将其标记为绝对任务。如果始终如一地完成,您可能会看到您的技能或生活发生重大变化。例如:如果你每天阅读一年,你的知识储备、创造力和阅读速度都可能会提高。如果你每天锻炼,你的健康无疑会有所改善。同样,如果您每天花 1 小时学习编码,到年底您将拥有全新的技能组合。


虽然元任务在生活中是不可避免的,但要确保你的生活目标不是元的,它们需要是绝对的。当你创建当天的待办事项清单时,确保至少有一件事是绝对的(记住:1.01³⁶⁵=37.8)。当然,当你可以时:尽可能多地将元任务自动化。元任务在很多方面都可以成为分心的代名词,除非它们给你的生活带来某种独立的快乐。


一夜成名的神话


最后,我想澄清最后一个误解:没有一夜成名这回事。这种误解源于媒体的运作方式。


TechCrunch 永远不会写 X 人如何用 Y 年时间引导一个可持续的非独角兽企业,遵守其价值观并尊重人们的隐私。离群索居者很耀眼,但他们仍然是离群索居者。


直到几年前,我才真正理解持续攀登的概念。我以为每一个成功的人都说要付出很多工作和努力,这只是在为他们的运气自我辩护。



"当我们惊叹于那些为创造力提供动力并推动世界变革的原创者时,我们往往会认为他们是由不同的布料剪成的。" -- 亚当-格兰特



现实情况是,构建任何有价值的东西都需要时间。当然,全职工作可能需要更长的时间来构建,但这没关系。


如果你目前有全职工作,不要把自己放在一个框框里,而是要开始为你觉得有趣的想法工作。完美的想法永远不会出现,,所以我鼓励每个人开始每周花 1 小时来研究他们认为有吸引力的想法,并逐渐增加,直到你处于一个可以让他们全职工作的地方。将你的生命线(你的工作)与你的项目分开,这种精神上的清醒可能是最健康和最周到的做法。


记住,你没有成为企业家的时刻,所以没有必要为了将自己定义为企业家而辞掉工作。


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

想退更多税?看完这篇就够了

前言 最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值...
继续阅读 »

前言


最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值变多。


image.png


专项附加扣除填报


首先打开个人所得税APP,点击下面的tabbar办税,然后点击专项附加扣除填报,就会出现如下图,通过填报上面的这些去进行增加专项附加扣除合计值。


0b957f5ecc7afa419053f890511d9aa.jpg


租房租金(和房贷只能二选一)



  • 要求:工作租房子了 (不需要合同也能填哦)

  • 扣除标准:省会或者直辖市 1500 / 月,二线城市 1100 /月,小城市 800 /月

  • 年总计:省会或者直辖市 18000,二线城市 13200,小城市 9600

  • 划重点: 多段租房经历可以多填几个租房,一定要填满!!!,一定要填,没租房子也可以填,随便填一个地址也行,这个不需要租房合同,也无法追查到的,但是和贷款只能二选一,具体选哪个请继续看。


image.png


住房贷款利息(和租房只能二选一)



  • 要求:首套房贷款

  • 扣除标准:1000/月

  • 年总计:12000

  • 划重点: 和租房二选一哦,具体选哪个看你所在的城市,哪个钱多选哪个~


image.png


子女教育



  • 要求:子女处于全日制学历教育阶段(幼儿园+小学+中学+大学),年龄>3岁。

  • 扣除标准:每人每月 1000 元 (两个孩子是一个月2000哦)

  • 年总计:12000(一个孩子的情况下)


image.png


继续教育



  • 要求:考证或者学历提升

  • 扣除标准:考证拿到证的当年一次性扣除:3600,学历提升 400/月

  • 年总计:考证:3600,学历提升:4800

  • 划重点: 这里的证必须是收录在 国家资格目录 的证书哦~


image.png


大病医疗


如果可以,我希望这个没有人申请,身体健康比什么都重要。



  • 要求:医保报销后个人花费15k+

  • 扣除标准:8w内根据你个人花费,花多少这个就是多少。

  • 年总计:根据实际。


image.png


赡养老人



  • 要求:父母 60 岁以上

  • 扣除标准:独生子女2000/月,非独生子女:和兄弟姐妹分摊 2000

  • 年总计:独生子女:24000,非独生子女: 0 ~ 12000


image.png


3岁以下婴幼儿照护



  • 要求:孩子 3 岁以下

  • 扣除标准:每月每人 1000 (两个孩子就是2000/月哦)

  • 年总计:12000(一个孩子的情况下)


如何能退更多的税


打开个人所得税APP → 我要查询 → 申报查询 → 已完成→ 选择当年的综合年度汇算→ 点击专项附加扣除合计,查看自己当年的专项附加扣除合计,可以简单计算下自己填报的专项附加扣除是否都已经填写。



PS:画重点!!!注意检查时间,我开始就是租房没填满,只写了一段租房经历,所以差点错过好几个亿,如果是2023年的填报,一定要注意你专项附加扣除是否涵盖整个2022年的日期。



113b48e415b80b8861261f3b3d961ab.jpg


有年终奖的情况


可能有些小伙伴公司会发年终奖 (如果和我一样没发年终奖,那这里可以跳过了,发年终奖的公司给我来一打),最好选择单独计税,不过都可以试一下,综合并入和单独合计,哪个退的多用哪个。


历史申报


以前的申报如果有没补充的,现在还是可以填哦,快去看看之前的申报,说不定会有惊喜哦。


最后


如果是需要补税的情况,如果少于400是不需要补的哦,当然如果大于400,一定要快点去缴纳哦,国家都留有档案,防止影响以后征信,哈哈哈,祝大家都能退几个亿~


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

保姆级JAVA对接ChatGPT教程,实现自己的AI对话助手

1.前言 大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API K...
继续阅读 »

1.前言


大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API Key的申请以及网络代理的搭建,那么事不宜迟,我们现在开始。


2.对接流程


2.1.API-Key的获取


首先第一步要获取OpenAI接口的API Key,该Key是你用来调用接口的token,主要用于接口鉴权。获取该key首先要注册OpenAi的账号,具体可以见我的另外一篇文章,ChatGPT保姆级注册教程



  1. 打开platform.openai.com/网站,点击view API Key,


image.png



  1. 点击创建key


image.png



  1. 弹窗显示生成的key,记得把key复制,不然等会就找不到这个key了,只能重新创建。


image.png


将API Key保存好以备用


2.2.API用量的查看


这里可以查看API的使用情况,新账号注册默认有5美元的试用额度,之前都是18美元,API成本降了之后试用额度也狠狠地砍了一刀啊,哈哈。


image.png


2.3.核心代码实现


2.3.1.pom依赖


http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.webtap
webtap
0.0.1
jar


org.springframework.boot
spring-boot-starter-parent
2.1.2.RELEASE




org.springframework.boot
spring-boot-starter-web


org.springframework.boot
spring-boot-starter-thymeleaf


nz.net.ultraq.thymeleaf
thymeleaf-layout-dialect


org.springframework.boot
spring-boot-starter-data-jpa


org.springframework.boot
spring-boot-devtools


org.springframework.boot
spring-boot-starter-test


org.springframework.boot
spring-boot-starter-mail



mysql
mysql-connector-java


org.apache.commons
commons-lang3
3.4


commons-codec
commons-codec


org.jsoup
jsoup
1.9.2



com.alibaba
fastjson
1.2.56


net.sourceforge.nekohtml
nekohtml
1.9.22


com.github.pagehelper
pagehelper-spring-boot-starter
1.4.1


org.projectlombok
lombok


org.apache.httpcomponents
httpasyncclient
4.0.2


org.apache.httpcomponents
httpcore-nio
4.3.2



org.apache.httpcomponents
httpclient
4.3.5


commons-codec
commons-codec




commons-httpclient
commons-httpclient
3.1


commons-codec
commons-codec




org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.1


com.github.ulisesbocchio
jasypt-spring-boot-starter
2.0.0







org.springframework.boot
spring-boot-maven-plugin






2.3.2.实体类ChatMessage.java


用于存放发送的消息信息,注解使用了lombok,如果没有使用lombok可以自动生成构造方法以及get和set方法


@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
//消息角色
String role;
//消息内容
String content;
}

2.3.3.实体类ChatCompletionRequest.java


用于发送的请求的参数实体类,参数释义如下:


model:选择使用的模型,如gpt-3.5-turbo


messages :发送的消息列表


temperature :温度,参数从0-2,越低表示越精准,越高表示越广发,回答的内容重复率越低


n :回复条数,一次对话回复的条数


stream :是否流式处理,就像ChatGPT一样的处理方式,会增量的发送信息。


max_tokens :生成的答案允许的最大token数


user :对话用户


@Data
@Builder
public class ChatCompletionRequest {

String model;

List messages;

Double temperature;

Integer n;

Boolean stream;

List stop;

Integer max_tokens;

String user;
}

2.3.4.实体类ExecuteRet .java


用于接收请求返回的信息以及执行结果



/**
* 调用返回
*/

public class ExecuteRet {

/**
* 操作是否成功
*/

private final boolean success;

/**
* 返回的内容
*/

private final String respStr;

/**
* 请求的地址
*/

private final HttpMethod method;

/**
* statusCode
*/

private final int statusCode;

public ExecuteRet(booleansuccess, StringrespStr, HttpMethodmethod, intstatusCode) {
this.success =success;
this.respStr =respStr;
this.method =method;
this.statusCode =statusCode;
}

@Override
public String toString()
{
return String.format("[success:%s,respStr:%s,statusCode:%s]", success, respStr, statusCode);
}

/**
*@returnthe isSuccess
*/

public boolean isSuccess() {
return success;
}

/**
*@returnthe !isSuccess
*/

public boolean isNotSuccess() {
return !success;
}

/**
*@returnthe respStr
*/

public String getRespStr() {
return respStr;
}

/**
*@returnthe statusCode
*/

public int getStatusCode() {
return statusCode;
}

/**
*@returnthe method
*/

public HttpMethod getMethod() {
return method;
}
}

2.3.5.实体类ChatCompletionChoice .java


用于接收ChatGPT返回的数据


@Data
public class ChatCompletionChoice {

Integer index;

ChatMessage message;

String finishReason;
}

2.3.6.接口调用核心类OpenAiApi .java


使用httpclient用于进行api接口的调用,支持post和get方法请求。


url为配置文件open.ai.url的值,表示调用api的地址:https://api.openai.com/ ,token为获取的api-key。
执行post或者get方法时增加头部信息headers.put("Authorization", "Bearer " + token); 用于通过接口鉴权。



@Slf4j
@Component
public class OpenAiApi {

@Value("${open.ai.url}")
private String url;
@Value("${open.ai.token}")
private String token;

private static final MultiThreadedHttpConnectionManagerCONNECTION_MANAGER= new MultiThreadedHttpConnectionManager();

static {
// 默认单个host最大链接数
CONNECTION_MANAGER.getParams().setDefaultMaxConnectionsPerHost(
Integer.valueOf(20));
// 最大总连接数,默认20
CONNECTION_MANAGER.getParams()
.setMaxTotalConnections(20);
// 连接超时时间
CONNECTION_MANAGER.getParams()
.setConnectionTimeout(60000);
// 读取超时时间
CONNECTION_MANAGER.getParams().setSoTimeout(60000);
}

public ExecuteRet get(Stringpath, Map headers) {
GetMethod method = new GetMethod(url +path);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
}

public ExecuteRet post(Stringpath, Stringjson, Map headers) {
try {
PostMethod method = new PostMethod(url +path);
//log.info("POST Url is {} ", url + path);
// 输出传入参数
log.info(String.format("POST JSON HttpMethod's Params = %s",json));
StringRequestEntity entity = new StringRequestEntity(json, "application/json", "UTF-8");
method.setRequestEntity(entity);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
} catch (UnsupportedEncodingExceptionex) {
log.error(ex.getMessage(),ex);
}
return new ExecuteRet(false, "", null, -1);
}

public ExecuteRet execute(HttpMethodmethod) {
HttpClient client = new HttpClient(CONNECTION_MANAGER);
int statusCode = -1;
String respStr = null;
boolean isSuccess = false;
try {
client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "UTF8");
statusCode = client.executeMethod(method);
method.getRequestHeaders();

// log.info("执行结果statusCode = " + statusCode);
InputStreamReader inputStreamReader = new InputStreamReader(method.getResponseBodyAsStream(), "UTF-8");
BufferedReader reader = new BufferedReader(inputStreamReader);
StringBuilder stringBuffer = new StringBuilder(100);
String str;
while ((str = reader.readLine()) != null) {
log.debug("逐行读取String = " + str);
stringBuffer.append(str.trim());
}
respStr = stringBuffer.toString();
if (respStr != null) {
log.info(String.format("执行结果String = %s, Length = %d", respStr, respStr.length()));
}
inputStreamReader.close();
reader.close();
// 返回200,接口调用成功
isSuccess = (statusCode == HttpStatus.SC_OK);
} catch (IOExceptionex) {
} finally {
method.releaseConnection();
}
return new ExecuteRet(isSuccess, respStr,method, statusCode);
}

}

2.3.7.定义接口常量类PathConstant.class


用于维护支持的api接口列表


public class PathConstant {
public static class MODEL {
//获取模型列表
public static String MODEL_LIST = "/v1/models";
}

public static class COMPLETIONS {
public static String CREATE_COMPLETION = "/v1/completions";
//创建对话
public static String CREATE_CHAT_COMPLETION = "/v1/chat/completions";

}
}

2.3.8.接口调用调试单元测试类OpenAiApplicationTests.class


核心代码都已经准备完毕,接下来写个单元测试测试下接口调用情况。



@SpringBootTest
@RunWith(SpringRunner.class)
public class OpenAiApplicationTests {

@Autowired
private OpenAiApi openAiApi;
@Test
public void createChatCompletion2() {
Scanner in = new Scanner(System.in);
String input = in.next();
ChatMessage systemMessage = new ChatMessage('user', input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
System.out.println(context.getContent());
}

}


  • 使用Scanner 用于控制台输入信息,如果单元测试时控制台不能输入,那么进入IDEA的安装目录,修改以下文件。增加最后一行增加-Deditable.java.test.console=true即可。


image.png
image.png




  • 创建ChatMessage对象,用于存放参数,role有user,system,assistant,一般接口返回的响应为assistant角色,我们一般使用user就好。




  • 定义请求参数ChatCompletionRequest,这里我们使用3.1日发布的最新模型gpt-3.5-turbo-0301。具体都有哪些模型大家可以调用v1/model接口查看支持的模型。




  • 之后调用openAiApi.post进行接口的请求,并将请求结果转为JSON对象。取其中的choices字段转为ChatCompletionChoice对象,该对象是存放api返回的具体信息。


    接口返回信息格式如下:


    {
    "id": "chatcmpl-6rNPw1hqm5xMVMsyf6PXClRHtNQAI",
    "object": "chat.completion",
    "created": 1678179420,
    "model": "gpt-3.5-turbo-0301",
    "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 339,
    "total_tokens": 355
    },
    "choices": [{
    "message": {
    "role": "assistant",
    "content": "\n\nI. 介绍数字孪生的概念和背景\n A. 数字孪生的定义和意义\n B. 数字孪生的发展历程\n C. 数字孪生在现代工业的应用\n\nII. 数字孪生的构建方法\n A. 数字孪生的数据采集和处理\n B. 数字孪生的建模和仿真\n C. 数字孪生的验证和测试\n\nIII. 数字孪生的应用领域和案例分析\n A. 制造业领域中的数字孪生应用\n B. 建筑和城市领域中的数字孪生应用\n C. 医疗和健康领域中的数字孪生应用\n\nIV. 数字孪生的挑战和发展趋势\n A. 数字孪生的技术挑战\n B. 数字孪生的实践难点\n C. 数字孪生的未来发展趋势\n\nV. 结论和展望\n A. 总结数字孪生的意义和价值\n B. 展望数字孪生的未来发展趋势和研究方向"
    },
    "finish_reason": "stop",
    "index": 0
    }]
    }



  • 输出对应的信息。




2.3.9.结果演示


image.png


2.4.连续对话实现


2.4.1连续对话的功能实现


基本接口调通之后,发现一次会话之后,没有返回完,输入继续又重新发起了新的会话。那么那么我们该如何实现联系上下文呢?其实只要做一些简单地改动,将每次对话的信息都保存到一个消息列表中,这样问答就支持上下文了,代码如下:


List messages = new ArrayList<>();
@Test
public void createChatCompletion() {
Scanner in = new Scanner(System.in);
String input = in.next();
while (!"exit".equals(input)) {
ChatMessage systemMessage = new ChatMessage(ChatMessageRole.USER.value(), input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
messages.add(context);
in = new Scanner(System.in);
input = in.next();
}
}

因为OpenAi的/v1/chat/completions接口消息参数是个list,这个是用来保存我们的上下文的,因此我们只要将每次对话的内容用list进行保存即可。


2.4.2结果如下:


image.png


image.png


4.常见问题


4.1.OpenAi接口调用不通


因为https://api.openai.com/地址也被限制了,但是接口没有对地区做校验,因此可以自己搭建一个香港代理,也可以走科学上网。


我采用的是香港代理的模式,一劳永逸,具体代理配置流程如下:



  1. 购买一台香港的虚拟机,反正以后都会用得到,作为开发者建议搞一个。搞活动的时候新人很便宜,基本3年的才200块钱。

  2. 访问nginx.org/download/ng… 下载最新版nginx

  3. 部署nginx并修改/nginx/config/nginx.conf文件,配置接口代理路径如下


server {
listen 19999;
server_name ai;

ssl_certificate /usr/local/nginx/ssl/server.crt;
ssl_certificate_key /usr/local/nginx/ssl/server.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

#charset koi8-r;


location /v1/ {
proxy_pass ;
}
}


  1. 启动nginx

  2. 将接口访问地址改为nginx的机器出口IP+端口即可


如果代理配置大家还不了解,可以留下评论我单独出一期教程。


4.2.接口返回401


检查请求方法是否增加token字段以及key是否正确


5.总结


至此JAVA对OpenAI对接就已经完成了,并且也支持连续对话,大家可以在此基础上不断地完善和桥接到web服务,定制自己的ChatGPT助手了。我自己也搭建了个平台,不断地在完善中,具体可见下图,后续会开源出来,想要体验的可以私信我获取地址和账号哈


image.png


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

Jetpack:Android新一代导航管理Navigation

前言 不知道小伙伴们是否注意到,用AS创建一个默认的新项目后,MainActivity已经有了很大的不同,最大的区别就是新增加了两个Fragment,同时我们注意到这两个Fragment之间跳转的时候并没有使用之前FragmentTransaction这种形式...
继续阅读 »

前言


不知道小伙伴们是否注意到,用AS创建一个默认的新项目后,MainActivity已经有了很大的不同,最大的区别就是新增加了两个Fragment,同时我们注意到这两个Fragment之间跳转的时候并没有使用之前FragmentTransaction这种形式,而是使用了NavController和NavHostFragment,这就是新一代导航管理————Navigation。


项目中依赖Navigation:


implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'

创建导航视图


新建一个Android Resource File,类型选择Navigation即可,输入名称后我们就创建了一个导航视图。


在导航试图中,我们可以通过添加activity/fragment等标签手动添加页面,也支持在Design页面中通过界面添加,如下:


B88C2B4A-4900-41DF-9BDC-7972F73190D2.png


注意:这样添加后手动修改一下label。如果我们将Navigation与ToolBar连接,会在标题栏这个label。


示例中添加了两个页面,添加后代码如下:


<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<fragment
android:id="@+id/FirstFragment"
android:name="com.xxx.xxx.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.xxx.xxx.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
</fragment>
</navigation>

除了添加Fragment和Activity,Google还提供了一个占位符placeholder,添加加完代码如下:


<fragment android:id="@+id/placeholder" />

用于暂时占位以便后面可以替换为Fragment和Activity


添加完页面后,我们还需要添加页面之间的导航,可以手动添加action标签,当然也可以通过拖拽来实现,如下:


ABE13B79-D136-4450-A454-B4C905733284.png


这样我们就添加了一个从FirstFragment导航到SecondFragment的动作,我们再添加一个逆向的动作,最终的代码如下:


<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<fragment
android:id="@+id/FirstFragment"
android:name="com.xxx.xxx.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">

<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.xxx.xxx.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">

<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>

注意占位符placeholder同样支持添加导航。


这样就实现了两个页面间的导航,最后还需要为这个navigation设置id和默认页面startDestination,如下:


<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">

这样导航视图就创建完成了。可以看到Google力图通过可视化工具来简化开发工作,这对我们开发者来说非常有用,可以省去大量编写同质化代码的时间。


添加NavHost


下一步我们需要向Activity中添加导航宿主,导航宿主是一个空页面,必须实现NavHost接口,我们使用Navigation提供的默认NavHost————NavHostFragment即可。如下:


<fragment
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />

在Activity的视图中添加一个fragment标签,android:name设置为实现类,即NavHostFragment;app:navGraph设置为刚才新建的导航视图。


注意app:defaultNavHost="true",设置为true后表示将这个NavHostFragment设置为默认导航宿主,这样就会拦截系统的返回按钮事件。同一布局中如果有多个导航宿主(比如双窗口)则必须制定一个为默认的导航宿主。


这时候我们运行应用,就可以发现Activity中已经可以展示FirstFragment了。


导航


我们还需要为两个fragment添加按钮,是其点击跳转到另外一个页面,代码如下:


binding.buttonFirst.setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}

示例中是FirstFragment中的一个按钮,点击时执行了id为action_FirstFragment_to_SecondFragment的动作,这个是我们之前在导航视图中配置好的,会导航到SecondFragment。


注意首先通过findNavController()来获取一个NavController对象,然后调用它的navigate函数即可,当然这个函数有多种重载,比如可以传递参数,如下:


public void navigate(@IdRes int resId, @Nullable Bundle args) {

这里不一一列举了,大家自行查看源码即可。


可以看到使用Navigation代码精简了很多,只需要一行代码执行一个函数即可。


findNavController


我们重点来看看findNavController(),它是一个扩展函数,如下:


fun Fragment.findNavController(): NavController =
NavHostFragment.findNavController(this)

实际上是NavHostFragment的一个静态函数findNavController:


@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
...
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}

// For DialogFragments, look at the dialog's decor view
Dialog dialog = fragment instanceof DialogFragment
? ((DialogFragment) fragment).getDialog()
: null;
if (dialog != null && dialog.getWindow() != null) {
return Navigation.findNavController(dialog.getWindow().getDecorView());
}

throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}

通过源码可以看到最终是执行了Navigation的findNavController函数,它的代码如下:


@NonNull
public static NavController findNavController(@NonNull View view) {
NavController navController = findViewNavController(view);
...
return navController;
}

这里是通过findViewNavController函数来获取NavController的,它的代码如下:


@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
if (controller != null) {
return controller;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}

这里可以看到通过view来获取NavController,如果没有则向上层查找(父view)直到找到或到根结点。getViewNavController代码如下:


@Nullable
private static NavController getViewNavController(@NonNull View view) {
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference<NavController>) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}

看到这里获取view中key为R.id.nav_controller_view_tag的tag,这个tag就是NavController,那么这个tag又从哪来的?


其实就是上面我们提到导航宿主————NavHostFragment,在他的onViewCreated中可以看到如下代码:


@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
Navigation.setViewNavController(view, mNavController);
// When added programmatically, we need to set the NavController on the parent - i.e.,
// the View that has the ID matching this NavHostFragment.
if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}

这里的mNavController是在NavHostFragment的onCreate中创建出来的,是一个NavHostController对象,它继承NavController,所以就是NavController。


可以看到onViewCreated中调用了Navigation的setViewNavController函数,它的代码如下:


public static void setViewNavController(@NonNull View view,
@Nullable NavController controller) {
view.setTag(R.id.nav_controller_view_tag, controller);
}

这样就将NavController加入tag中了,通过findNavController()就可以得到这个NavController来执行导航了。


注意在onViewCreated中不仅为Fragment的View添加了tag,同时还为其父View也添加了,这样做的目的是在Activity中也可以获取到NavController,这点下面就会遇到。


ToolBar


Google提供了Navigation与ToolBar连接的功能,代码如下:


val navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)

上面我们提到,如果Navigation与ToolBar连接,标题栏会自动显示在导航视图中设定好的label。


注意这里的findNavController是Activity的扩展函数,它最终一样会调用Navigation的对应函数,所以与Fragment的流程是一样的。而上面我们提到了,在NavHostFragment中给上层View也设置了tag,所以在这里才能获取到NavController。


除了这个,我们还可以发现当在切换页面的时候,标题栏的返回按钮也会自动显示和隐藏。当导航到第二个页面SecondFragment,返回按钮显示;当回退到首页时,返回按钮隐藏。


但是此时返回按钮点击无效,因为我们还需要重写一个函数:


override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}

这样当点击标题栏的返回按钮时,会执行NavController的navigateUp函数,就会退回到上一页面。


总结


可以看出通过Google推出的这个Navigation,可以让开发者更加优雅管理导航,同时也简化了这部分的开发工作,可视化功能可以让开发者更直观的进行管理。除此之外,Google还提供了Safe Args Gradle插件,该插件可以生成简单的对象和构建器类,这些类支持在目的地之间进行类型安全的导航和参数传递。关于这个大家可以参考官方文档developer.android.google.cn/guide/navig… 即可。


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

Android将so库封装到jar包中并加载其中的so库

说明 因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。 1.如何把so库放入jar包 so库放入jar参考...
继续阅读 »

说明


因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。


1.如何把so库放入jar包


so库放入jar参考此文章ANDROID将SO库封装到JAR包中并加载其中的SO库
放置路径
将so库改成.jet后缀,放置和加载so库的SoLoader类同一个目录下面。



2.如何使用groovy打包jar


打包jar
先把需要打包的class放置到同一个文件夹下面,然后打包即可,利用groovy的copy task完成这项工作非常简单。


3.如何加载jar包里面的so


3.1.首先判断当前jar里面是否存在so

InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");

如果inputStream不为空就表示存在。


3.2.拷贝

判断是否已经把so库拷贝到手机里面了,如果没有拷贝过就进行拷贝,这个代码逻辑很简单。


public class SoLoader {
private static final String TAG = "SoLoader";

/**
* so库释放位置
*/
public static String getPath() {
String path = GlobalCtx.getApp().getFilesDir().getAbsolutePath();
//String path = GlobalCtx.getApp().getExternalFilesDir(null).getAbsolutePath();
return path;
}

public static String get64SoFilePath() {
String path = SoLoader.getPath();
String v8a = path + File.separator + "jniLibs" + File.separator +
"arm64-v8a" + File.separator + "liblogan.so";
return v8a;
}

public static String get32SoFilePath() {
String path = SoLoader.getPath();
String v7a = path + File.separator + "jniLibs" + File.separator +
"armeabi-v7a" + File.separator + "liblogan.so";
return v7a;
}

/**
* 支持两种模式,如果InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
* 返回了空,表示可能此库是aar接入的,普通加载so库就行,不为空,需要拷贝so库,动态加载
*/
public static boolean jarMode() {
boolean jarMode = false;
InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
if (inputStream != null) {
jarMode = true;
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return jarMode;
}

/**
* 是否已经拷贝过so了
*/
public static boolean alreadyCopySo() {
String v8a = SoLoader.get64SoFilePath();
File file = new File(v8a);
if (file.exists()) {
String v7a = SoLoader.get32SoFilePath();
file = new File(v7a);
return file.exists();
}
return false;
}

/**
* 拷贝logan的so库
*/
public static boolean copyLoganJni() {
boolean load;
File dir = new File(getPath(), "jniLibs");
if (!dir.exists()) {
load = dir.mkdirs();
if (!load) {
return false;
}
}
File subdir = new File(dir, "arm64-v8a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
File dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/arm64-v8a/liblogan.so", dest);
load = copySo("/com/dianping/logan/arm64-v8a/liblogan.jet", dest);
if (load) {
subdir = new File(dir, "armeabi-v7a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/armeabi-v7a/liblogan.so", dest);
load = copySo("/com/dianping/logan/armeabi-v7a/liblogan.jet", dest);
}
return load;
}

public static boolean copySo(String name, File dest) {
InputStream inputStream = SoLoader.class.getResourceAsStream(name);
if (inputStream == null) {
Log.e(TAG, "inputStream == null");
return false;
}
boolean result = false;
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(dest);
int i;
byte[] buf = new byte[1024 * 4];
while ((i = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, i);
}
result = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

}

3.3.加载

首先判断当前应用是32位还是64位Process.is64Bit();。然后加载对应的32或者64位的so。


static {
try {
if (SoLoader.jarMode()) {
if (SoLoader.alreadyCopySo()) {
sIsCloganOk = loadLocalSo();
} else {
boolean copyLoganJni = SoLoader.copyLoganJni();
if (copyLoganJni) {
sIsCloganOk = loadLocalSo();
}
}
} else {
System.loadLibrary(LIBRARY_NAME);
sIsCloganOk = true;
}
} catch (Throwable e) {
e.printStackTrace();
sIsCloganOk = false;
}
}

static boolean loadLocalSo() {
boolean bit = Process.is64Bit();
if (bit) {
String v8a = SoLoader.get64SoFilePath();
try {
System.load(v8a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
} else {
String v7a = SoLoader.get32SoFilePath();
try {
System.load(v7a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
}

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

[崩溃] Android应用自动重启

背景 在App开发过程中,我们经常需要自动重启的功能。比如: 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。 crash的时候,可以捕获到异常,直接自动重启应用。 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才...
继续阅读 »

背景


在App开发过程中,我们经常需要自动重启的功能。比如:



  • 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。

  • crash的时候,可以捕获到异常,直接自动重启应用。

  • 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才能生效,此时可以用自动重启,方便测试。


那我们如何实现自动重启的功能呢?我们都知道如何杀掉进程,但是当我们的进程被杀掉之后,如何唤醒呢?


这篇文章就来和大家介绍一下,实现应用自动重启的几种方法。


方法1 AlarmManager


    private void setAlarmManager(){
Intent intent = new Intent();
intent.setClass(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis()+100, pendingIntent);
Process.killProcess(Process.myPid());
System.exit(0);
}

使用AlarmManager实现自动重启的核心思想:创建一个100ms之后的Alarm任务,等Alarm任务到执行时间了,会自动唤醒App。


缺点:



  • 在App被杀和拉起之间,会显示系统Launcher桌面,体验不好。

  • 在高版本不适用


方法2 直接启动Activity


private void restartApp(){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Process.killProcess(Process.myPid());
System.exit(0);
}

缺点:



  • MainActivity必须是Standard模式


方法3 ProcessPhoenix


JakeWharton大神开源了一个叫ProcessPhoenix的库,这个库可以实现无缝重启app。


实现原理其实很简单,我们先讲怎么使用,然后再来分析源码。


使用方法


首先引入ProcessPhoenix库,这个库不需要初始化,可以直接使用。


implementation 'com.jakewharton:process-phoenix:2.1.2'

使用1:如果想重启app后进入首页:


ProcessPhoenix.triggerRebirth(context);

使用2:如果想重启app后进入特定的页面,则需要构造具体页面的intent,当做参数传入:


Intent nextIntent = //...
ProcessPhoenix.triggerRebirth(context, nextIntent);

有一点需要特别注意。



  • 我们通常会在ApplicationonCreate方法中做一系列初始化的操作。

  • 如果使用Phoenix库,需要在onCreate方法中判断,如果当前进程是Phoenix进程,则直接return,跳过初始化的操作。


if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}

源码


ProcessPhoenix的原理:



  • 当调用triggerRebirth方法的时候,会启动一个透明的Activity,这个Activity运行在:phoenix进程

  • Activity启动后,杀掉主进程,然后用:phoenix进程拉起主进程的Activity

  • 关闭当前Activity,杀掉:phoenix进程


先来看看ManifestActivity的注册代码:


 <activity
android:name=".ProcessPhoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:process=":phoenix"
android:exported="false"
/>

可以看到这个Activity确实是在:phoenix进程启动的,且是Translucent透明的。


整个ProcessPhoenix的代码只有不到120行,非常简单。我们来看下triggerRebirth做了什么。


  public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

不带intenttriggerRebirth,最后也会调用到带intenttriggerRebirth方法。


getRestartIntent会获取主进程的Launch Activity


  private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if (defaultIntent != null) {
return defaultIntent;
}
}

所以要调用不带intenttriggerRebirth,必须在当前Appmanifest里,指定Launch Activity,否则会抛出异常。


接着来看看真正的triggerRebirth方法:


  public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// 第一个activity添加new_task标记,重新开启一个新的stack
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
// 这里是为了防止传入的context非Activity
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
// 将待启动的intent作为参数,intent是parcelable的
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
// 将主进程的pid作为参数
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
// 启动ProcessPhoenix Activity
context.startActivity(intent);
}

triggerRebirth方法,主要的功能是启动ProcessPhoenix Activity,相当于启动了:phoenix进程。同时,会将nextIntents和主进程的pid作为参数,传给新启动的ProcessPhoenix Activity


下面我们再来看看,ProcessPhoenix ActivityonCreate方法,看看新进程启动后做了什么。


  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 首先杀死主进程
Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
// 再启动主进程的intents
startActivities(intents.toArray(new Intent[intents.size()]));
// 关闭当前Activity,杀掉当前进程
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

:phoenix进程主要做了以下事情:



  • 杀死主进程

  • 用传入的Intent启动主进程的Activity(也可以是Service)

  • 关闭phoenix Activity,杀掉phoenix进程


总结


如果App有自动重启的需求,比较推荐使用ProcessPhoenix的方法。


原理其实非常简单:



  • 启动一个新的进程

  • 杀掉主进程

  • 用新的进程,重新拉起主进程

  • 杀掉新的进程


我们可以直接在工程里引入ProcessPhoenix开源库,也可以自己用代码实现这样的机制,总之都比较简单。


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

Android:我是如何优化APP体积的

前言 在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自...
继续阅读 »

前言



在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自研的一些APP可经不住这些考验,所以能压缩就压缩,能优化就尽量优化,以得到用户最好的体验,下面就来说说我在项目中是如何优化APP体积的。



1. 本地资源优化


这里主要是压缩一些图片和视频。项目中本地资源用到最多的应该就是图片,几乎每个页面都离不开图标,甚至一些页面采用大图片的形式。你可知道,正常不经压缩的图片大的可以上大几十兆,小则也是一兆起步。这里做了个实验,同一个文件分别采用svg、png、使用tiny压缩后的png、webp四种类型图片进行展示(顺序是从左到右,从上到下):


image.png


可以看到,加载出来的效果几乎没有什么区别,但体积却有很大的差别(其中webp是采取的默认75%转换):


image.png


所以,别再使用png格式图片,太浪费资源了,就算经过压缩还是不及svg和webp,这里的webp其实还可以加大转换力度,但个人还是比较喜欢svg。


至于音视频文件也是可以通过其他工具进行压缩再放入本地,如非必要,尽量还是使用网络资源。


2. lib优化


一些三方库会使用底层so文件,一般在配置的时候我们尽量选择一种cpu类型,这里选择armeabi-v7a,其实几乎都兼容


ndk {
//设置支持的SO库架构 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64
abiFilters 'armeabi-v7a'
}

可以看看APK体积分析,每种cpu占用体积都比较大,少配置一种就能省下不少空间。
image.png


3. 代码混淆、无用资源的删除


在bulid.gradle中配置minifyEnabled true开启代码混淆,还需要配置混淆规则,否则无法找到目标类。shrinkResources true则是打包时不会将无用资源打入包内,这里有个小坑。之前使用腾讯地图时,某些第三方的静态资源会因为这个操作不被打入包内,导致无法找到资源,所以根据具体情况使用。


 release {
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
// shrinkResources true 慎用,可能会导致第三方资源文件找不到
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

4. 代码复用,剔除无用代码


项目中由于多人协同开发会出现各写各的情况,需要抽出一些公共库之类的工具,方便代码复用。一些注释掉的代码该删除就删除。其实这一部分优化的体积相当少,但也得做,也是对代码质量的一种提升。


总结


其实只要做到了以上四步,APP体积优化已经得到了很大程度的提升了,其他再怎么优化效果也不是很明显了,最主要的就是本地资源和第三方so包体积占用较多。图片的使用我们尽量做到:小图标用svg,全屏类的大图可以考虑webp,最好不要使用png。ndk配置最好只配置一款cpu,几乎都可兼容,万不得已再加一个。


以上便是全部内容,希望对大家有所帮助。


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

【自定义 View】Android 实现物理碰撞效果的徽章墙

前言 在还没有疫情的年代,外出经常会选择高铁,等高铁的时候我就喜欢打开 掌上高铁 的成就,签到领个徽章,顺便玩一下那个类似碰撞小球的徽章墙,当时我就在想这东西怎么实现的,但是吧,实在太懒了/doge,这几年都没尝试去自己实现过。最近有时间倒逼自己做了一些学习和...
继续阅读 »

前言


在还没有疫情的年代,外出经常会选择高铁,等高铁的时候我就喜欢打开 掌上高铁 的成就,签到领个徽章,顺便玩一下那个类似碰撞小球的徽章墙,当时我就在想这东西怎么实现的,但是吧,实在太懒了/doge,这几年都没尝试去自己实现过。最近有时间倒逼自己做了一些学习和尝试,就分享一下这种功能的实现。



不过,当我为写这篇文章做准备的时候,据不完全考古发现,似乎摩拜的 app 更早就实现了这个需求,但有没有更早的我就不知道了/doge



x762f-m3kvm.gif


其实呢,我想起来做这个尝试是我在一个 Android 自定义 View 合集的库 里看到了一个叫 PhysicsLayout 的库,当时我就虎躯一震,我心心念念的徽章墙不就是这个效果嘛,于是也就有了这篇文章。这个 PhysicsLayout 其实是借助 JBox2D 来实现的,但不妨先借助 PhysicsLayout 实现徽章墙,然后再来探索 PhysicsLayout 的实现方式。


实现




  1. 添加依赖,sync


    implementation("com.jawnnypoo:physicslayout:3.0.1")



  2. 在布局文件中添加PhysicsLinearLayout,并添加一个 子 Viewrun 起来


    这里我给ImageView设置 3 个Physic的属性



    • layout_shape设置模拟物理形状为圆形

    • layout_circleRadius设置圆形的半径为25dp

    • layout_restitution设置物体弹性的系数,范围为 [0,1],0 表示完全不反弹,1 表示完全反弹




  3. 看上去好像效果还行,我们再多加几个试试 子 View 试试





  4. 有下坠效果了,但是还不能随手机转动自由转动,在我阅读了 PhysicsLayout 之后发现其并未提供随陀螺仪自由晃动的方法,那我们自己加一个,在 MainActivityPhysicsLayout 添加一个扩展方法


        /**
    * 随手机的转动,施加相应的矢量
    * @param x x 轴方向的分量
    * @param y y 轴方向的分量
    */

    fun PhysicsLinearLayout.onSensorChanged(x: Float, y: Float) {
    for (i in 0..this.childCount) {

    Log.d(this.javaClass.simpleName, "input vec2 value : x $x, y $y")

    val impulse = Vec2(x, y)
    val view: View? = this.getChildAt(i)
    val body = view?.getTag(com.jawnnypoo.physicslayout.R.id.physics_layout_body_tag) as? Body
    body?.applyLinearImpulse(impulse, body.position)
    }
    }



  5. MainActivityonCreate() 中获取陀螺仪数据,并将陀螺仪数据设置给我们为 PhysicsLayout 扩展的方法,run


        val physicsLayout = findViewById(R.id.physics_layout)

    val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

    gyroSensor?.also { sensor ->
    sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
    event?.also {
    if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {

    Log.d(this@MainActivity.javaClass.simpleName, "sensor value : x ${event.values[0]}, y ${event.values[1]}")
    physicsLayout.onSensorChanged(-event.values[0], event.values[1])
    }
    }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
    }, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    动了,但是好像和预期的效果不太符合呀,而且也不符合用户直觉。




  6. 那不知道这时候大家是怎么处理问题的,我是先去看看这个库的 issue,搜索一下和 sensor 相关的提问,第二个就是关于如何让子 view 根据加速度计的数值进行移动,作者给出的答复是使用重力传感器,并在AboutActivity中给出了示例代码。


    那我们这里就换用重力传感器来试一试。


        val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)

    gyroSensor?.also { sensor ->
    sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
    event?.also {
    if (event.sensor.type == Sensor.TYPE_GRAVITY) {
    Log.d(this@MainActivity.javaClass.simpleName, "sensor value : x ${event.values[0]}, y ${event.values[1]}")
    physicsLayout.physics.setGravity(-event.values[0], event.values[1])
    }
    }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
    }, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    这下碰撞效果就正常了,但是好像会卡住不动啊!





  7. 不急,回到 issue,看第一个提问:物理效果会在子 view 停止移动后结束 和这里遇到的问题一样,看一下互动,有人提出是由于物理模拟引擎在物体移动停止后将物体休眠了。给出的修改方式是设置 bodyDef.allowSleep = false


    这个属性,是由 子 View 持有,所有现在需要获取 子 View 的实例并设置对应的属性,这里我就演示修改其中一个的方式,其他类似。


        findViewById(R.id.iv_physics_a).apply {
    if (layoutParams is PhysicsLayoutParams) {
    (layoutParams as PhysicsLayoutParams).config.bodyDef.allowSleep = false
    }
    }

    ···



  8. 到这里,这个需求基本就算实现了。




原理


看完了徽章墙的实现方式,我们再来看看 PhysicsLayout 是如何实现这种物理模拟效果的。




  1. 初看一下代码结构,可以说非常简单


    image.png




  2. 那我们先看一下我上面使用到的 PhysicsLinearLayout


    class PhysicsLinearLayout : LinearLayout {

    lateinit var physics: Physics

    constructor(context: Context) : super(context) {
    init(null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    init(attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    init(attrs)
    }

    @TargetApi(21)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
    init(attrs)
    }

    private fun init(attrs: AttributeSet?) {
    setWillNotDraw(false)
    physics = Physics(this, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    physics.onSizeChanged(w, h)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    super.onLayout(changed, l, t, r, b)
    physics.onLayout(changed)
    }

    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    physics.onDraw(canvas)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    return physics.onInterceptTouchEvent(ev)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return physics.onTouchEvent(event)
    }

    override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
    return LayoutParams(context, attrs)
    }

    class LayoutParams(c: Context, attrs: AttributeSet?) : LinearLayout.LayoutParams(c, attrs), PhysicsLayoutParams {
    override var config: PhysicsConfig = PhysicsLayoutParamsProcessor.process(c, attrs)
    }
    }

    主要有下面几个重点



    1. 首先是在构造函数创建了 Physics 实例

    2. 然后把 View 的绘制,位置,变化,点击事件的处理统统交给了 physics 去处理

    3. 最后由 PhysicsLayoutParamsProcessor 创建 PhysicsConfig 的实例




  3. 那我们先来看一下简单一点的 PhysicsLayoutParamsProcessor


    object PhysicsLayoutParamsProcessor {

    /**
    * 处理子 view 的属性
    *
    * @param c context
    * @param attrs attributes
    * @return the PhysicsConfig
    */

    fun process(c: Context, attrs: AttributeSet?): PhysicsConfig {
    val config = PhysicsConfig()
    val array = c.obtainStyledAttributes(attrs, R.styleable.Physics_Layout)
    processCustom(array, config)
    processBodyDef(array, config)
    processFixtureDef(array, config)
    array.recycle()
    return config
    }

    /**
    * 处理子 view 的形状属性
    */

    private fun processCustom(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_shape)) {
    val shape = when (array.getInt(R.styleable.Physics_Layout_layout_shape, 0)) {
    1 -> Shape.CIRCLE
    else -> Shape.RECTANGLE
    }
    config.shape = shape
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_circleRadius)) {
    val radius = array.getDimensionPixelSize(R.styleable.Physics_Layout_layout_circleRadius, -1)
    config.radius = radius.toFloat()
    }
    }

    /**
    * 处理子 view 的刚体属性
    * 1. 刚体类型
    * 2. 刚体是否可以旋转
    */

    private fun processBodyDef(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_bodyType)) {
    val type = array.getInt(R.styleable.Physics_Layout_layout_bodyType, BodyType.DYNAMIC.ordinal)
    config.bodyDef.type = BodyType.values()[type]
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_fixedRotation)) {
    val fixedRotation = array.getBoolean(R.styleable.Physics_Layout_layout_fixedRotation, false)
    config.bodyDef.fixedRotation = fixedRotation
    }
    }

    /**
    * 处理子 view 的刚体描述
    * 1. 刚体的摩擦系数
    * 2. 刚体的补偿系数
    * 3. 刚体的密度
    */

    private fun processFixtureDef(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_friction)) {
    val friction = array.getFloat(R.styleable.Physics_Layout_layout_friction, -1f)
    config.fixtureDef.friction = friction
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_restitution)) {
    val restitution = array.getFloat(R.styleable.Physics_Layout_layout_restitution, -1f)
    config.fixtureDef.restitution = restitution
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_density)) {
    val density = array.getFloat(R.styleable.Physics_Layout_layout_density, -1f)
    config.fixtureDef.density = density
    }
    }
    }

    这个类比较简单,就是一个常规的读取设置并创建一个对应的 PhysicsConfig 的属性




  4. 现在我们来看最关键的 Physics,这个类代码相对比较长,我就不完全贴出来了,一段一段的来分析



    1. 首先定义了一些伴生对象,主要是预设了几种重力值,模拟世界的边界尺寸,渲染帧率
      companion object {
      private val TAG = Physics::class.java.simpleName
      const val NO_GRAVITY = 0.0f
      const val MOON_GRAVITY = 1.6f
      const val EARTH_GRAVITY = 9.8f
      const val JUPITER_GRAVITY = 24.8f

      // Size in DP of the bounds (world walls) of the view
      private const val BOUND_SIZE_DP = 20
      private const val FRAME_RATE = 1 / 60f

      /**
      * 在创建 view 对应的刚体时,设置配置参数
      * 当布局已经被渲染之后改变 view 的配置需要调用 ViewGroup.requestLayout,刚体才能使用新的配置创建
      */

      fun setPhysicsConfig(view: View, config: PhysicsConfig?) {
      view.setTag(R.id.physics_layout_config_tag, config)
      }
      }


    2. 然后定义了很多的成员变量,这里挑几个重要的说一说吧
      /**
      * 模拟世界每一步渲染的计算速度,默认是 8
      */

      var velocityIterations = 8

      /**
      * 模拟世界每一步渲染的迭代速度,默认是 3
      */

      var positionIterations = 3

      /**
      * 模拟世界每一米对应多少个像素,可以用来调整模拟世界的大小
      */

      var pixelsPerMeter = 0f

      /**
      * 当前控制着 view 的物理状态的模拟世界
      */

      var world: World? = null
      private set


    3. init 方法中主要是读取一些 Physics 配置,另外初始化了一个拖拽手势处理的实例
      init {
      viewDragHelper = TranslationViewDragHelper.create(viewGroup, 1.0f, viewDragHelperCallback)

      density = viewGroup.resources.displayMetrics.density
      if (attrs != null) {
      val a = viewGroup.context
      .obtainStyledAttributes(attrs, R.styleable.Physics)
      ···
      a.recycle()
      }
      }


    4. 然后提供了一些物理长度,角度的换算方法

    5. onLayout 中创建了模拟世界,根据边界设置决定是否启用边界,设置碰撞处理回调,根据子 view 创建刚体
      private fun createWorld() {
      // Null out all the bodies
      val oldBodiesArray = ArrayList()
      for (i in 0 until viewGroup.childCount) {
      val body = viewGroup.getChildAt(i).getTag(R.id.physics_layout_body_tag) as? Body
      oldBodiesArray.add(body)
      viewGroup.getChildAt(i).setTag(R.id.physics_layout_body_tag, null)
      }
      bounds.clear()
      if (debugLog) {
      Log.d(TAG, "createWorld")
      }
      world = World(Vec2(gravityX, gravityY))
      world?.setContactListener(contactListener)
      if (hasBounds) {
      enableBounds()
      }
      for (i in 0 until viewGroup.childCount) {
      val body = createBody(viewGroup.getChildAt(i), oldBodiesArray[i])
      onBodyCreatedListener?.onBodyCreated(viewGroup.getChildAt(i), body)
      }
      }


    6. onInterceptTouchEventonTouchEvent 中处理手势事件,如果没有开启滑动拖拽,时间继续传递,如果开启了,则由 viewDragHelper 来处理手势事件。
      fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
      if (!isFlingEnabled) {
      return false
      }
      val action = ev.actionMasked
      if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      viewDragHelper.cancel()
      return false
      }
      return viewDragHelper.shouldInterceptTouchEvent(ev)
      }

      fun onTouchEvent(ev: MotionEvent): Boolean {
      if (!isFlingEnabled) {
      return false
      }
      viewDragHelper.processTouchEvent(ev)
      return true
      }


    7. onDraw 中绘制 view 的物理效果


      1. 先设置世界的物理配置


        val world = world
        if (!isPhysicsEnabled || world == null) {
        return
        }
        world.step(FRAME_RATE, velocityIterations, positionIterations)



      2. 遍历 子 view 并获取此前在创建刚体时设置的刚体对象,对于正在被拖拽的 view 将其移动到对应的位置


        translateBodyToView(body, view)
        view.rotation = radiansToDegrees(body.angle) % 360f



      3. 否则的话,设置 view 的物理位置,这里的 debugDraw 一直是 false 所以并不会走这段逻辑,且由于是私有属性,外部无法修改,似乎永远不会走这里


         view.x = metersToPixels(body.position.x) - view.width / 2f
        view.y = metersToPixels(body.position.y) - view.height / 2f
        view.rotation = radiansToDegrees(body.angle) % 360f
        if (debugDraw) {
        val config = view.getTag(R.id.physics_layout_config_tag) as PhysicsConfig
        when (config.shape) {
        Shape.RECTANGLE -> {
        canvas.drawRect(
        metersToPixels(body.position.x) - view.width / 2,
        metersToPixels(body.position.y) - view.height / 2,
        metersToPixels(body.position.x) + view.width / 2,
        metersToPixels(body.position.y) + view.height / 2,
        debugPaint
        )
        }
        Shape.CIRCLE -> {
        canvas.drawCircle(
        metersToPixels(body.position.x),
        metersToPixels(body.position.y),
        config.radius,
        debugPaint
        )
        }
        }
        }



      4. 最后提供了一个接口便于我们在需要的时候修改 JBox2D 处理 view 对应的刚体的物理状态


        onPhysicsProcessedListeners.forEach { it.onPhysicsProcessed(this, world) }




    8. 还有一个测试物理碰撞效果的随机碰撞方法
      fun giveRandomImpulse() {
      var body: Body?
      var impulse: Vec2
      val random = Random()
      for (i in 0 until viewGroup.childCount) {
      impulse = Vec2((random.nextInt(1000) - 1000).toFloat(), (random.nextInt(1000) - 1000).toFloat())
      body = viewGroup.getChildAt(i).getTag(R.id.physics_layout_body_tag) as? Body
      body?.applyLinearImpulse(impulse, body.position)
      }
      }





Bonus




  1. 在上面分析代码的时候,多次提到手势拖拽,那怎么实现这个手势的效果,目前好像对手是没反应嘛~


    其实也很简单,将 physicsisFlingEnabled 属性设置为 true 即可。


    val physicsLayout = findViewById(R.id.physics_layout).apply {
    physics.isFlingEnabled = true
    }




  2. 在浏览 PhysicsLayout issue 的时候还意外的发现已经有国人实现了 Compose 版本的
    JetpackComposePhysicsLayout


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

临近研三毕业的一些思考

秋招 2023届秋招可谓是诸神之战,算法岗、开发岗、数据分析岗等都是岗位紧缺,但却有一堆985高校毕业生来竞争。我海投了接近150家公司,最终收获了3个offer:一个上海创业公司(年薪43万元)、一个半国企(年薪25万元)以及一个真正的国企(年薪15-18万...
继续阅读 »

秋招


2023届秋招可谓是诸神之战,算法岗、开发岗、数据分析岗等都是岗位紧缺,但却有一堆985高校毕业生来竞争。我海投了接近150家公司,最终收获了3个offer:一个上海创业公司(年薪43万元)、一个半国企(年薪25万元)以及一个真正的国企(年薪15-18万元)。


在找工作方面,实力占据30%,而运气和机遇则占据了70%。即便是同一家公司,不同的面试官也有着不同的面试风格,因此会影响不同的结果。虽然运气很重要,但是实力永远都是基础。没有实力,就没有后续的好运气和机遇。


秋招的准备流程大体上包括以下几个步骤:刷题、背诵八股、准备项目、深入挖掘项目难点、准备简历、投递简历、面试复盘、简历修订以及谈薪水。每一个过程都十分重要,缺一不可。


春招


我已经签了一个半国企的offer,因此就没有再关注其他公司的信息。我基本上可以称为放弃了春招。一方面,如果我拿到了大厂的offer,我担心自己会承受不住试用期,或者会面临毕业的问题。另一方面,如果我拿到了银行的offer,我也不喜欢银行死板的工作氛围、轮岗的机制以及勾心斗角的同事关系。我参加过一次国考,但是基本上是裸考,完全没有复习。虽然在国家体制内工作很好,但是对于经济条件不好的家庭来说,去体制内不是最优的选择。


毕业季


在毕业前,我开始思考自己的第一份工作。实际上,我更多的是感到焦虑,因为放弃了年薪43万元的offer需要很大的勇气。我一直在思考如何弥补自己失去这份工作的损失。因此,我萌生了一些想法:


想法一:接私活


接外包项目对于程序员来说可能相对较为简单,因为这类项目通常技术含量较低,但是与此同时,与客户的沟通和协商则显得比较繁琐。但是,这种方式往往容易让人陷入代码的搬运中,短期收益是可观的,可以轻松获得外快薪资,但从长远来看,这种做法得不偿失。


想法二:转型产品经理


也许是看多了脉脉,认为程序员35岁危机是必然存在的,但是各行各业都存在淘汰现象。比较幼稚的想法是觉得产品经理生命周期更长,更稳定,在咨询相关学长后才知道,我才发现,原来产品经理的竞争比技术岗还要激烈,失业后重新获得一份工作的几率要比技术岗更低。


想法三:技术积淀,做自媒体


有些人会认为,做自媒体的目的就是要从中牟利,尽管这种想法在大部分人中很普遍。但实际上,成功的自媒体人通常是因为一开始就热爱分享或热爱自己所从事的行业,最终才获得了第二桶金。虽然技术积淀很重要,但如果抱着功利性的心态去做自媒体,那么注定会经历种种落差、打击以及困难。


如何破局?


思来想去,无法得知后续的解决方法,于是想到了《刻意练习》这本书,当我遇到一个无法解决问题的时候,我应该把我的问题和不成熟解决方案向教练反馈,让有经验的教练来指导。我联系了实验室优秀的师兄,师兄给出了很有建设性的意见:


意见一:技术是生存之道


进入公司,要多看技术文档,多看源码,理解原理,刚开始要多写、多思考。只有思考,才能成长起来。互联网是有35危机,但是大部分行业都有35危机,你有技能,就算被毕业,也会找到一个不错的工作。


意见二:要有全局思维


在接到一个需求时,我们不能简单地完成它,而应该深入思考这个需求为什么被提出,采用什么方式解决会更好。一个高价值的程序员不应该以编写的代码量为标准来衡量,而应该根据他在团队中不可或缺的地位来衡量。从了解业务到产品,从产品到自己负责的模块,我们应该对整个公司的架构有粗粒度和细粒度的理解。在未来,我们也将独当一面,成为负责人。因此,我们要为自己工作,而不是做一个单纯的打工仔。公司的转正述职报告是一个很好的机会,我们可以在这个机会中展示我们所学的一切,不仅限于我们负责的模块。


意见三:真诚的职场态度


互联网有嫡系的说法,那么为甚么会存在嫡系呢???存在即合理,肯定有背后的原因。假如,你有一个能力强、有主见、会沟通的的下属,你会不会青睐他?让他升职为你创造更多的业绩呢。答案是肯定的。师兄谈到了怎么样成为嫡系的方法,总结来说就是真诚的职场态度。第一,交代的任务要有责任感,能完成的不能完成的,都要带着结果和理由汇报,这是工作,不是随便搞一搞,那么怎么才能算是认真的完成呢?要知道任务背后的目的,任务背后蕴含的底层实现或者原理,这块任务交给你,你对这个任务的理解应该比谁都要透彻;第二,要主动汇报,可以两周或者一周就和上级交流自己的工作任务情况自己的难点等等,和优秀的人交流你才能进步。程序员的通病也许就是太沉迷于自己的代码世界,而忘记了和上级和其它业务部门交流,导致你做的和老板想要的根本不是一回事;第三,多和其它部门的人沟通,不要占用人家的工作时间,可以茶余饭后聊聊天,了解一下人家在做什么,沟通也是一个重要技能。


总结


师兄给的建议非常perfect,让我醍醐灌顶。未来的道路还很长,我想避免焦虑的最好办法,就是每天进步一点点,每个阶段都能朝着规划前进一步。最后,希望

作者:Javan Lu
来源:juejin.cn/post/7208359036273098811
顺利毕业,未来可期。

收起阅读 »

中年程序员写给36岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长 _,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_ 回顾  忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长


_,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_


回顾 


忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己过去一年的得与失,去年是写给35岁的自己,今年该+1了,还是随笔的方式,想到哪写到哪。


2022年1月30日的时候 ,给自己过去的工作做一次简单的总结,主要还是写给自己,就像现在的时候可以回过头去看,也收获了许多朋友的关注,回去看一年前写的文章,以及大家的留言反馈,深有感触。


回看去年的flag,首先需要检讨一下,基本上都没有完成,但是自己也朝着这个目标在做,收获也是有的:



读书


每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。


写文章


一周完成一篇原创文章,不限类别


早睡早起


每天不晚于11:30休息



关于读书


上半年的时候,自己也是有点焦虑和迷茫,想成长想进步,所以焦虑心情自然就会出现,所以看了一些鸡汤书籍,什么《被讨厌的勇气》、《程序员的自我修养》、《情商》等等。实话说看完之后,确实能够缓解缓解内心的焦虑情绪,但是这些书籍能给到自己的,更多是一些方式和方法,对于内心的空洞和不充实带来的焦虑是没办法缓解的。


所以还需要对症下药,我自己所感受到的空洞和不充实,很多是来自自己对技术知识技能的缺乏和退步,说白了就是作为技术人,不能把技术给弄丢了 ,同样也想不断的跟上时代的步伐。


想要快速解决这种“焦虑”,我需要快速的制定一个短期、中期、长期的目标,围绕着目标去充实自己的知识体系。这里所说的目标还是比较容易制定的,毕竟是关乎自己的成长,也就是自己接下来想要成为什么样的人,什么样的知识体系能够让自己在当前以及未来几年的工作中都有帮助。从这个方面去想,首先未来还是想从事前端,所以我给自己制定的短期目标是算法成长 、中期目标是计算机图形学方面的知识掌握、长期目标是成为一名地图领域的技术专家(ps:说到这里先立个flag,我后面想写一个小册,专门关于地图领域相关的,我也是比较期待自己能写出来什么样的小册,不为别的,就是想把自己的知识沉淀下来)。


讲讲为什么要这么去规划目标,算法算是现在任何技术面试都会涉及的,但是我不是为了面试去看,而是为了提升自己在团队内部的技术影响力,《算法图解》这本书写的简单好理解,作者的思路非常清晰 ,看完之后给团队内部的同学分享,不仅能提升自己,还能带动团队一起学习,一举多得。计算机图形学知识是目前工作中会碰到的,比如渲染、大数据可视化、自动驾驶等等都会涉及,这一部分不建议大家先去看书,没有一本书能够说明白,推荐大家去搜《闫令琪》,非常厉害的大佬,上班路上每天花半个小时-1小时足够了,一个月基本上能够学完,之后再运用到工作中,融会贯通。


单独再讲讲长远目标,我之前并不是搞地图方向的,但是近期这份工作有机会接触到了这方面的工作,让我又重新燃起了工作中的那种欲望,很久没有工作中的那种成就感,这也许是10年前才会有的那种热情,所以我比较坚信未来几年自己希望能够深入投入这个方向,不一定是地图,但一定是和这个方向相关的领域,因为知识都是想通的。


关于写文章


写文章这件事情,我非常佩服一位前同事,也或许是因为我没有做到 ,但是别人坚持每天每日的在做 ,他连续两年每天都能产出一篇原创,关于各个方面的,这是值得我学习的地方,今年争取突破自己。


关于早睡


头发卡卡掉,感觉都是因为没有按时睡觉引起的,还是在能有条件的时候,尽量早睡。


工作


今年的工作可以用“黑暗”、“光明”两个词来概括。


黑暗


2022年经历疫情最严重的一年,大部分时间都是居家办公状态,这也导致和同事们的交流变得很少,很多想要推进的工作变得没那么顺利,徒增了不少压力。


2022年也是“财源滚滚”的一年,看着同事一个个离开,也有不少同事询问工作机会,也确实给自己内心带来不小的冲击,同时危机感也很明显。


在一个地方工作一段时间之后,多少都会遇到各种各样的问题,技术上是最省心的问题,解决就好。有江湖的地方就会有各种复杂到不敢想的关系网,谁是谁的小弟,谁是谁的心腹、谁是大老板招来的等等,遇到这种问题我更多的是做好自己,但我更多还是更愿意沉浸在技术的知识中,享受解决问题带来的快感。面对频繁换老板,技术人的通病,不善于抱大腿,当然我也不想在这方便再去做过多改变或者违背内心去做一些事情,保持好内心的底线,不突破我的底线则相安无事。


光明


呵护好内心的明灯


今年工作最大的动力是来自于自身能力的成长,规划的短中长目标基本上都在按照正确的方向在行进,这也是在排除各种各样的干扰后带来的好的结果,也是抱着一种积极向上的心态在努力,工作中最让人糟心的,无非就是背锅、背指标、裁员,最坏的情况也就这样了,守好内心的方向,做自己想做的事情就对了,自己左右不了的事情不去想太多,行业不景气的时候,我基本上是以这种心态在工作,人生并不是只有工作。


人情往来


工作中


今年在和外部部门的合作当中,收获了许多的认可,也建立了许多新的人脉关系,这也是人生中比较宝贵的资源。与合作方合作共赢一直都是我做事的指导方法 ,提前思考好双方的目标和边界,剩下的就是努力合作完成目标了。相信他人,他人也会给予你同样的信任。


生活中


生活中的关系会比工作中的关系更加的牢靠,当然工作中的关系发展的好的话,也可以沉淀到生活中,而不是换个工作全没了,今年工作中积累的关系,确实是可以有这方面的转换的,这也是一种收获。


技术成长


我一直都不太赞成技术人转纯管理这个方向,管好人其实可以很简单,丑话在前,用心对待,以诚相待,能做好这三点感觉都不会有太大问题,但技术丢了就很难再捡起来了,切记切记。


今年反尝试不直接带团队,更多的是以技术顾问、专家视角,甚至是一线coding的方式在工作,看似管人但又不管人,所以在技术上成长也是非常快的,少了很多其他的琐事,能够更加投入。


渲染


第一次接触这个词的时候是在2021年,公司专门配了一个渲染团队做这个事情,用前端白话讲,就是能把各种各样的图像画到canvas上,一个好的渲染引擎可以呈现任何想要呈现的物体。


为了学习渲染是做什么的,怎么做,当时把简单的数学知识重新学习了一下,看闫令琪大佬的课,看openGL、webGPU等等相关的知识,过程是比较辛苦的,但收获也是很多的。现在再看一些框架就能够理解为什么代码会这么写了,比如Threejs、deckgl等等,我们自己也用c++实现了一套底层的跨端渲染框架,虽然不全面,但内部够用同时也能提升自身技术水平。


架构


架构能力是随着工作中不断积累起来能力,当然这也需要在工作中不断的打磨和锻炼,如果一直是以完成任务的心态在工作那是很难练出来的。我所推崇的架构能力是以解决业务问题为主,提升产研的效率为辅。所以在工作中不会刻意去做架构,而是围绕着如何解决问题去架构,如何才能控制好不至于过度设计。


举个简单例子,假如我们已经有各种完善的点餐业务,需要做一个邀请大家一起喝奶茶的这么一个功能,从业务上我们先考虑两个核心逻辑:


1、用户点餐之后回到邀请页面,点完的所以人实时能看到其他人下单状态
2、队长确认所有人点完之后,下单付款,所有人的页面切换到送餐状态

如果是快速实现这个功能的话,其实是比较简单的,起一个轮询任务实时问服务端要数据,拿到数据后,根据状态决定下一步显示什么状态的页面


但是随着业务发展,会加入很多奇怪的逻辑,比如要支持修改、删除、踢人等等,这就会导致这个页面逻辑及其的复杂起来,如果不去思考的话,很容易就写出一堆面条代码,最后自己都不愿意去改。


所以针对这个功能  ,我自己会抽象成几部分去思考:


1、store该如何拆解,拆成几个,每个store对应哪个组件
2、store该如何去更新
3、与服务端如何通信,websocket、轮询都可以,看当下实际情况,保证稳定性即可
4、可以写几个js类去做这个事情,每个类的职责是什么

我觉得思考完这几个问题 ,大家对于这个页面该怎么去写应该能有一个很清晰的架构图在脑海中了吧,这里我就不过多展开了 ,有兴趣的话私聊,核心是要说架构其实也可以很简单。


总结


今年就不立flag了,目标能把去年的flag实现好,2023年是疫情结束的一年 ,我认为这是一个好的开始,努力工作是一方面,享受生活我认为也同样重要,今年更需要做好工作和生活的平衡,工作以外能有一些其他的成就。


写给36岁的自己,简单地回顾过去、总结现在、展望未来,希望当37岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给37岁的自己》。


附上去年总结《写给35岁的自己》

trong>

收起阅读 »

Android 插件化:插件内部跳转

在Android 插件化(加载插件)中,简单的用一个demo 讲了如何加载一个插件,并使用插件里的资源。 那如果我们的插件中有多个页面呢,要怎么办? 其实,也是很简单,还是通过外部 PluginActivity 的 startActivity来实现 一、Lif...
继续阅读 »

在Android 插件化(加载插件)中,简单的用一个demo 讲了如何加载一个插件,并使用插件里的资源。


那如果我们的插件中有多个页面呢,要怎么办?

其实,也是很简单,还是通过外部 PluginActivitystartActivity来实现


一、LifeActivitystartActivity


LifeActivity 这个插件类中定义一个 startActivity 方法,用宿主的 context 调用 startActivity 方法


public void startActivity(Intent intent) {
if (context != null) {
if (intent==null||intent.getComponent()==null)return;
Intent newIntent=new Intent();
String className=intent.getComponent().getClassName();
if (TextUtils.isEmpty(className))return;
Log.e("startActivity","className="+className);
newIntent.putExtra("className",intent.getComponent().getClassName());
context.startActivity(newIntent);
}
}

而对于第一个插件中的页面 TestActivity


image.png


可以看到,插件中的第一个页面 TestActivity 点击打开 插件页面 Test2Activity 时。写法跟我们在Android中的风格是一模一样的。其中的 findViewById 等,只要是用到上下文的,全部替换成宿主的,这里不多赘述了。


image.png


二、重写 PluginActivitystartActivity


注意:由于 Test2Activity 不是一个真正 Activity ,PluginActivitystartActivity 中,就不能打开这个页面,只能再重新打开一个PluginActivity,并将Test2Activity 类的信息再重新加载实例化一次,跟我们第一个加载TestActivity 是一样的。

override fun startActivity(intent: Intent?) {
val className = intent?.getStringExtra("className")
if (className.isNullOrBlank()) return
val newIntent = Intent(this, PluginActivity::class.java)
newIntent.putExtra("className", className)
super.startActivity(newIntent)
}

传入进去的 className 就是 Test2Activity ,在PluginActivity 走生命周期onCreate 时,loadClassActivity ,至此就完成了插件内部的跳转,是不是非常简单。


9b2ee0aed19ba5e77698cb1f9582b93d.gif


三、同理,有 Activity 就会有其他组件


我们可以在插件中自己实现 serviceContentProviderBroadcastReceiver
等等组件,并重写生命周期等方法。原理都非常简单,难的是思想,这些都是插件化中的冰山一脚,我自己的项目中的更加复杂。


由于这样的方式,需要手动创建生命周期管理,和后续Activity启动模式,入栈出栈的管理等等。其实可以使用ASM等字节码来转换,将四大组件转成普通类,这样,开发过程中容易调试,插件生成也相对简单。


四、动态加载


由于插件apk 是可以从外部sdk 等地方加载的,给我们带来很多便利。而且插件部分的资源都是动态的,可以做到热更新的效果,只要我修改了再重新打包下发就行了。后续可以自己实现一套插件管理,用于加载外部apk ,做到热插拔的作用。


作者:大强Dev
来源:juejin.cn/post/7209971268483825722
收起阅读 »

[崩溃] Android应用自动重启

背景 在App开发过程中,我们经常需要自动重启的功能。比如: 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。 crash的时候,可以捕获到异常,直接自动重启应用。 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才...
继续阅读 »

背景


在App开发过程中,我们经常需要自动重启的功能。比如:



  • 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。

  • crash的时候,可以捕获到异常,直接自动重启应用。

  • 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才能生效,此时可以用自动重启,方便测试。


那我们如何实现自动重启的功能呢?我们都知道如何杀掉进程,但是当我们的进程被杀掉之后,如何唤醒呢?


这篇文章就来和大家介绍一下,实现应用自动重启的几种方法。


方法1 AlarmManager


    private void setAlarmManager(){
Intent intent = new Intent();
intent.setClass(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis()+100, pendingIntent);
Process.killProcess(Process.myPid());
System.exit(0);
}

使用AlarmManager实现自动重启的核心思想:创建一个100ms之后的Alarm任务,等Alarm任务到执行时间了,会自动唤醒App。


缺点:



  • 在App被杀和拉起之间,会显示系统Launcher桌面,体验不好。

  • 在高版本不适用


方法2 直接启动Activity


private void restartApp(){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Process.killProcess(Process.myPid());
System.exit(0);
}

缺点:



  • MainActivity必须是Standard模式


方法3 ProcessPhoenix


JakeWharton大神开源了一个叫ProcessPhoenix的库,这个库可以实现无缝重启app。


实现原理其实很简单,我们先讲怎么使用,然后再来分析源码。


使用方法


首先引入ProcessPhoenix库,这个库不需要初始化,可以直接使用。


implementation 'com.jakewharton:process-phoenix:2.1.2'

使用1:如果想重启app后进入首页:


ProcessPhoenix.triggerRebirth(context);

使用2:如果想重启app后进入特定的页面,则需要构造具体页面的intent,当做参数传入:


Intent nextIntent = //...
ProcessPhoenix.triggerRebirth(context, nextIntent);

有一点需要特别注意。



  • 我们通常会在ApplicationonCreate方法中做一系列初始化的操作。

  • 如果使用Phoenix库,需要在onCreate方法中判断,如果当前进程是Phoenix进程,则直接return,跳过初始化的操作。


if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}

源码


ProcessPhoenix的原理:



  • 当调用triggerRebirth方法的时候,会启动一个透明的Activity,这个Activity运行在:phoenix进程

  • Activity启动后,杀掉主进程,然后用:phoenix进程拉起主进程的Activity

  • 关闭当前Activity,杀掉:phoenix进程


先来看看ManifestActivity的注册代码:


 <activity
android:name=".ProcessPhoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:process=":phoenix"
android:exported="false"
/>


可以看到这个Activity确实是在:phoenix进程启动的,且是Translucent透明的。


整个ProcessPhoenix的代码只有不到120行,非常简单。我们来看下triggerRebirth做了什么。


  public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

不带intenttriggerRebirth,最后也会调用到带intenttriggerRebirth方法。


getRestartIntent会获取主进程的Launch Activity


  private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if (defaultIntent != null) {
return defaultIntent;
}
}

所以要调用不带intenttriggerRebirth,必须在当前Appmanifest里,指定Launch Activity,否则会抛出异常。


接着来看看真正的triggerRebirth方法:


  public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// 第一个activity添加new_task标记,重新开启一个新的stack
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
// 这里是为了防止传入的context非Activity
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
// 将待启动的intent作为参数,intent是parcelable的
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
// 将主进程的pid作为参数
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
// 启动ProcessPhoenix Activity
context.startActivity(intent);
}

triggerRebirth方法,主要的功能是启动ProcessPhoenix Activity,相当于启动了:phoenix进程。同时,会将nextIntents和主进程的pid作为参数,传给新启动的ProcessPhoenix Activity


下面我们再来看看,ProcessPhoenix ActivityonCreate方法,看看新进程启动后做了什么。


  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 首先杀死主进程
Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
// 再启动主进程的intents
startActivities(intents.toArray(new Intent[intents.size()]));
// 关闭当前Activity,杀掉当前进程
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

:phoenix进程主要做了以下事情:



  • 杀死主进程

  • 用传入的Intent启动主进程的Activity(也可以是Service)

  • 关闭phoenix Activity,杀掉phoenix进程


总结


如果App有自动重启的需求,比较推荐使用ProcessPhoenix的方法。


原理其实非常简单:



  • 启动一个新的进程

  • 杀掉主进程

  • 用新的进程,重新拉起主进程

  • 杀掉新的进程


我们可以直接在工程里引入ProcessPhoenix开源库,也可以自己用代码实现这样的机

作者:尹学姐
来源:juejin.cn/post/7207743145999024165
制,总之都比较简单。

收起阅读 »

抛弃trycatch,用go的思想去处理js异常

web
errors 错误处理在编程中是不可避免的一部分,在程序开发过程中,不可必要的会出现各种的错误,是人为也可能是失误,任何不可预料的可能都会发生 为了更好的保证程序的健壮性和稳定性 我们必须要对错误处理有更好的认识 最近迷上了golang的错误处理哲学,希望由浅...
继续阅读 »

errors


错误处理在编程中是不可避免的一部分,在程序开发过程中,不可必要的会出现各种的错误,是人为也可能是失误,任何不可预料的可能都会发生


为了更好的保证程序的健壮性和稳定性


我们必须要对错误处理有更好的认识


最近迷上了golang的错误处理哲学,希望由浅入深的总结一下自己的思考和看得见的思考


👀 用 error 表示可能出现的错误,用throw强制抛出错误


通常情况下 错误处理的方式无非不过两种



  • 泛处理

  • 精处理


其实都很好理解


// 伪代码

try {
const file = await readFile('../file')

const content = filterContent(file)

} catch (e) {
alert('xxxx',e.msg)
}

泛处理指的是对所有可能的错误都使用相同的处理方式,比如在代码中使用相同统一的错误提示信息


这种方式适用于一些简单的,不太可能发生的错误,不如文件不存在,网络连接超时等。


对于更加复杂的错误,应该使用精处理,即根据具体情况对不同类型的错误进行特定的处理



const [readErr,readFile] = await readFile('../file')

if (readErr) {
// 处理读取错误
return
}

const [filterErr,filterContent] = filterContent(readFile)

if (filterErr) {
// 处理过滤错误
return
}

精处理可以让我们把控计划的每一步,问题也很显然暴露了出来,过于麻烦


在实际开发当中,我们需要根据实际情况选择适当的方式进行错误处理。


由于本人是精处理分子,基于此开发了一个js版本的errors


如何定义一个错误


import { Errors } from '@memo28/utils'

Errors.News('err')

如何给一个错误分类


import { Errors } from '@memo28/utils'

Errors.News('err', { classify: 1 })

如何判断两个错误是相同类型


import { Errors } from '@memo28/utils'

Errors.As(Errors.News('err', { classify: 1 }),
Errors.News('err2', { classify: 1 })) // true

Errors.As(Errors.News('err', { classify: 1 }),
Errors.News('err2', { classify: 2 })) // false

一个错误包含了哪些信息?


Errors.New('as').info() // { msg: 'as' , classify: undefined }

Errors.New('as').trace() // 打印调用栈

Errors.New('as').unWrap() // 'as'

最佳实践


import { AnomalousChain, panicProcessing } from '@memo28/utils'

class A extends AnomalousChain {
// 定义错误处理函数,
// 当每次执行的被 panicProcessing 装饰器包装过的函数都会检查 是否存在 this.errors 是否为 ErrorsNewResult 类型
// 如果为 ErrorsNewResult 类型则 调用 panicProcessing的onError回调 和 skip函数
skip(errors: ErrorsNewResult | null): this {
console.log(errors?.info().msg)
return this
}

@panicProcessing()
addOne(): this {
console.log('run one')
super.setErrors(Errors.New('run one errors'))
return this
}

@panicProcessing({
// 在 skip 前执行
onError(error) {
console.log(error.unWrap())
},
// onRecover 从错误中恢复, 当返回的是true时 继续执行addTwo内逻辑,反之
// onRecover(erros) {
// return true
// },
})
addTwo(): this {
console.log('run two')
return this
}
}
new A().addOne().addTwo()



// output
run one
run one errors // in onError
run one errors // in skip fn

复制代码
作者:我开心比苦恼多
来源:juejin.cn/post/7207707775774031930
>
收起阅读 »

“ChatGPT 们” 所需算力真是“贵滴夸张”!

先抛几个数据: 当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复 如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算 而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要...
继续阅读 »

先抛几个数据:


当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复


如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算


而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要 7W+ RMB💴


image.png


意思就是:光计算这些有意义/无意义的问题,就要花费:21 亿+ RMB 💴


这还不算电费/维护费等,众所周知,这种庞大计算类似矿机,很费电


一块 A100,功率 400W,30000 块 A100 ,就是 12_000_000 W,是 12 MW(12 个 100 万瓦)


image.png


目前,“GPT们”就像是井喷一样出现了各类产品,先不管算的结果怎么样,大家先支棱起来、先算起来


有预测:十年后,“GPT们” 一天所需算力的生产功率相当于半个核电站产生的功率,这是离谱且夸张的!全球算力几乎快要无法满足“GPT们”了


image.png


所以,以 ChatGPTPlus(每月20刀) 为代表的 “GPT们” 很贵,因为它本来算力消费就很贵,“用 GPT 编程比招一个普通程序员更贵”的段子并非玩笑。


所以,为什么算力如此重要?为什么微软要和韩国SK集团布局自建核电站?为什么咱们要强调西数东算、云计算等等?从这里也能窥见一斑。


不夸张的说,在未来,国力强弱一方面将通过算力强弱来体现。


image.png


2016年6月的不同国家之间的超级计算机500强的分布


image.png


超级计算机模拟风洞实验


小思考:在未来,算力的瓶颈将如何突破?


—— 目前芯片仍处在传统冯·诺伊曼架构之下,存储和计算区域是分离的,搬运数据“从存储到计算”花费巨大的功耗,如果能实现“存算一体”(就像大脑一样)将提升算力进入新的量级。


所以“存算一体”可能是个方向~


推荐阅读:


# 计算型存储/存算一体如何实现? - bonnie的回答


# 5分钟新知关注芯片算力|存算一体为什么是AI时代主流计算架构?



作者:掘金安东尼
来源:juejin.cn/post/7210028417617395772
收起阅读 »