注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Flutter 入门路线图

本文是为那些渴望开始学习 flutter 的人们而准备的,这是一个适合初学者从所有必要资源中逐步学习的路线图。 什么是 flutter Flutter 是 Google 的 UI 工具包,可通过单个代码库为移动设备,web 和桌面系统构建漂亮的,本机编译的应用...
继续阅读 »

本文是为那些渴望开始学习 flutter 的人们而准备的,这是一个适合初学者从所有必要资源中逐步学习的路线图。


什么是 flutter


Flutter 是 Google 的 UI 工具包,可通过单个代码库为移动设备,web 和桌面系统构建漂亮的,本机编译的应用程序。


下面两个视频很好地介绍了 flutter;


Introducing Flutter1


What's new in Flutter 20192



[1] https://youtu.be/fq4N0hgOWzU


[2] https://youtu.be/5VbAwhBBHsg


为什么是 flutter?


我们已经知道有很多框架可以提供跨平台功能,那么在这场激烈竞争中,是什么让 flutter 显得特别呢?


快速开发


Flutter 的热加载功能可帮助您快速轻松地进行实验,构建用户界面,添加功能并更快地修复错误。在 iOS 和 Android 的模拟器和硬件上体验亚秒级的重新加载时间,而不会丢失状态。


富有表现力的精美用户界面


Flutter 内置的精美 Material Design 和 Cupertino(iOS-flavor)小部件,丰富的运动 API,流畅的自然滚动以及对平台的了解,可为您的用户带来更多惊喜。


native 级别的性能


Flutter 的小部件结合了所有重要的平台差异,例如滚动,导航,图标和字体,以在 iOS 和 Android 上提供完整的 native 性能。


查看 Flutter 的功能


以下是全球开发人员构建的 Flutter 应用程序的展示。


Apps take flight with Flutter3


An open list of apps built with Flutter4


Flutter Awesome5


Start Flutter | Forever free, open source, and easy to use.6



[3]https://flutter.dev/showcase


[4]https://itsallwidgets.com/


[5]https://flutterawesome.com/


[6]https://startflutter.com/


首先要做什么?


Flutter 既快速又容易,如果您熟悉 Java 或任何面向对象的语言,那么很不错,但是我强烈建议您具备 Dart 的基本知识。


以下是一些可能对您有所帮助的视频。


Dart Programming for Flutter7


Dart Programming Tutorial - Full Course8


Introduction to Dart for Beginners9


Dart: Basics of Dart Part - 1/2 | Flutter10



[7]https://youtu.be/5rtujDjt50I?list=PLlxmoA0rQ-LyHW9voBdNo4gEEIh0SjG-q


[8]https://youtu.be/Ej_Pcr4uC2Q


[9]https://youtu.be/8F2uemqLwvE?list=PLJbE2Yu2zumDjfrfu8kisK9lQVcpMDDzZ


[10]https://youtu.be/DFRl4UyS7c8?list=PLR2qQy0Zxs_W4a6P70VYtzna7jwl3-lxI


对于那些不喜欢看视频的人,可以查看以下站点


Tutorials11


Dart Programming Tutorial12


Learn Dart In A Week With These Free Resources13



[11]https://dart.dev/tutorials


[12]https://www.tutorialspoint.com/dart_programming/index.htm


[13]https://hackernoon.com/learn-dart-in-a-week-with-these-free-resources-b892e5265220


是什么使 Dart 如此典型,为什么 flutter 会使用它?


为什么 Flutter 使用 Dart?


可以查看以下文章和视频


Why Flutter Uses Dart?14


视频:Why Flutter Uses Dart?15



[14]https://hackernoon.com/why-flutter-uses-dart-dd635a054ebf


[15]https://youtu.be/5F-6n_2XWR8


Flutter 底层是如何工作的?


由于 iOS 不允许动态编译,因此您的 Dart 代码会使用 AOT 直接编译为本地代码。


要了解更多信息,请在下面查看这些资源:


Technical overview16


How to Dart and Flutter Work Together?17


What's Revolutionary about Flutter18


How Flutter reners Widgets19


How is Flutter different for app development20



[16]https://flutter.dev/docs/resources/technical-overview


[17]https://youtu.be/iVYpeEd3Jes


[18]https://hackernoon.com/whats-revolutionary-about-flutter-946915b09514


[19]https://youtu.be/996ZgFRENMs


[20]https://youtu.be/l-YO9CmaSUM


Flutter快速且易于使用,现在让我们看看如何安装它。


如何安装Flutter?


这是开发人员文档的链接,您可以在其中找到在现有的操作系统中安装Flutter。


Install21



[21]https://flutter.dev/docs/get-started/install


解决安装过程中的问题


如果您在安装 flutter 时遇到任何问题,并且 flutter 无法正常工作,那么这就是出现了一些问题。


设置 flutter 路径时遇到麻烦-找不到flutter命令22


Flutter Doctor无法识别Android Studio flutter和dart插件,但已安装插件23


Flutter和Dart插件未在Flutter Doctor中安装警告24


安装 flutter 时的一些常见问题。25



[22]https://stackoverflow.com/questions/49268297/having-trouble-setting-flutter-path-flutter-commands-not-found


[23]https://github.com/flutter/flutter/issues/21881


[24]https://github.com/flutter/flutter/issues/11940


[25]https://github.com/flutter/flutter/wiki/Workarounds-for-common-issues


设置Flutter的编辑器


Set up an editor26



[26]https://flutter.dev/docs/get-started/editor


创建您的Flutter项目


通过以下命令创建 flutter 项目


flutter create <project-name>

或者您可以使用IDE(Intellij,Android Studio等)


项目概况


当您创建 flutter 应用程序时,您会看到这些文件和文件夹,大多数代码是用 dart 编写在 lib 文件夹中,native 代码放在 android 和 ios 目录下。


Jay Tillu的一篇文章解释了该项目的结构。


Flutter Project Structure27



[27]https://dev.to/jay_tillu/flutter-project-structure-1lhe


运行你的第一个 App


Test drive28


或者你可以使用以下命令来运行您的第一个应用程序


flutter run

当您启动第一个应用程序时,一定会感到很兴奋(从技术上说,这不是您的应用程序,代码已经在那里😜)。 我也很兴奋🎉。


创建flutter应用程序时,您会看到计数器应用程序已经有代码了。


运行代码时,您将看到此信息。这是一个简单的计数器应用程序,其中有一个FAB(FloatingActionButton)和 Text 来指示已按下 FAB 多少次。



[28]https://flutter.dev/docs/get-started/test-drive


flutter 中的 widget


如果看到代码,您将看到 StatefulWidget 和 StatelessWidget。在深入探讨之前,我们先来了解一下什么是 Widget。


Introduction to widget29


基本上,在 flutter 应用程序中看到的所有内容都是一个小部件。


我发现 What is a Widget in Flutter30 一文中的解释非常准确


Flutter小组还提供了一个YouTube播放列表(Widget of the week31),该列表仅讨论flutter中的Widget。



[29]https://flutter.dev/docs/development/ui/widgets-intro


[30]https://stackoverflow.com/questions/50958238/what-is-a-widget-in-flutter


[31]https://youtu.be/b_sQ9bMltGU?list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG


什么是有状态和无状态小部件?


在 Stateless Widget 中,其所有属性都是不可变的,这意味着 StatelessWidget 永远不会自行重建(但可以从外部事件重建),而 StatefulWidget 可以。


Intro to Flutter - Stateful and Stateless Widgets, Widget Tree - Part One32


Flutter: Stateful vs Stateless Widget33


How to Create Stateless Widgets - Flutter Widgets 101 Ep. 134


How Stateful Widgets Are Used Best - Flutter Widgets 101 Ep. 235


Google's Flutter Tutorials | 6 - Stateless & Stateful Widgets | Android & iOS | Dart36



[32]https://youtu.be/-QRQIKtPTlI


[33]https://medium.com/flutter-community/flutter-stateful-vs-stateless-db325309deae


[34]https://www.youtube.com/watch?v=wE7khGHVkYY&feature=emb_title


[35]https://www.youtube.com/watch?v=AqCMFXEmf3w&feature=emb_title


[36]https://www.youtube.com/watch?list=PLR2qQy0Zxs_UdqAcaipPR3CG1Ly57UlhV&v=VnWHOogtDk8&feature=emb_title


让我们创建第一个Flutter应用


Google已经提供了一个 Codelab,您可以从那里开始学习如何构建自己的第一个 Flutter 应用程序。


Write Your First Flutter App, part 137


Write Your First Flutter App, part 238


Flutter Tutorial Part 1: Build a Flutter app from scratch39


1.3 Flutter Hello World Tutorial: Create First Flutter Application: Flutter Dart Tutorial40


1.4 First Flutter Application using Dart: PART-2 Flutter Tutorial for Beginners using Dart41



[37]https://codelabs.developers.google.com/codelabs/first-flutter-app-pt1/#4


[38]https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2/#0


[39]https://medium.com/aviabird/flutter-tutorial-how-to-build-an-app-from-scratch-b88d4e0e10d7


[40]https://www.youtube.com/watch?list=PLlxmoA0rQ-Lw6tAs2fGFuXGP13-dWdKsB&v=dsyucuytW2k


[41]https://www.youtube.com/watch?list=PLlxmoA0rQ-Lw6tAs2fGFuXGP13-dWdKsB&v=ycHX8QtV08c


如何在 Flutter 中创建 UI?


为了使 UI 更加流畅,您需要基本了解布局以及如何使用它们。


Layouts in Flutter42


Flutter layout Cheat Sheet43



[42]https://flutter.dev/docs/development/ui/layout


[43]https://medium.com/flutter-community/flutter-layout-cheat-sheet-5363348d037e


如何在您的应用中添加交互?


在 flutter 中,您不能只是分配一个值并留下它


例如


String value="Hello";
------------------------------
Text(value);
---SOMEWHERE IN THE CODE------
onTap(){
value="How are you?";
}

如果您认为文本将要更改,那么您错了🙅‍♂️,您将不得不使用 setState()。


onTap(){
setState({
value="How are you?";
});
}

添加 setState() 将重建小部件并显示更改。


Adding interactivity to your Flutter app44


我建议您跟进有关开发的 Flutter 官方文档


Development45


flutter 中的所有内容都是小部件,您可以自行创建任何自定义小部件,但是已经有通过 flutter 定义的小部件。


Widget catalog46



[44]https://flutter.dev/docs/development/ui/interactive


[45]https://flutter.dev/docs/development


[46]https://flutter.dev/docs/development/ui/widgets


Flutter 中的 JSON 解析


JSON and serialization47


Parsing JSON in Flutter48


Parsing complex JSON in Flutter49


Working with APIs in Flutter50


Handling Network Calls like a Pro in Flutter51


Flutter - Build An App To Fetch Data Online Using HTTP GET | Android & iOS52


Testing, JSON serialization, and immutables (The Boring Flutter Development Show, Ep. 2)53



[47]https://flutter.dev/docs/development/data-and-backend/json


[48]https://medium.com/flutterdevs/parsing-complex-json-in-flutter-b7f991611d3e


[49]https://medium.com/flutter-community/parsing-complex-json-in-flutter-747c46655f51


[50]https://medium.com/flutter-community/working-with-apis-in-flutter-8745968103e9


[51]https://medium.com/flutter-community/handling-network-calls-like-a-pro-in-flutter-31bd30c86be1


[52]https://www.youtube.com/watch?list=PLR2qQy0Zxs_UdqAcaipPR3CG1Ly57UlhV&v=aIJU68Phi1w


[53]https://www.youtube.com/watch?v=TiCA0CEePyE


在 Flutter 中使用数据库


SQLite


Persist data with SQLite54


Data Persistence with SQLite | Flutter55


4.1 Flutter SQFLite Database Tutorial: Implement SQLite database with example: Section Overview56


Moor (Room for Flutter) #1 – Tables & Queries – Fluent SQLite Database57



[54]https://flutter.dev/docs/cookbook/persistence/sqlite


[55]https://medium.com/flutterdevs/data-persistence-with-sqlite-flutter-47a6f67b973f


[56]https://www.youtube.com/watch?list=PLDQl6gZtjvFu5l20K5KTEBLCjfRjHowLj&v=1BwjNEKD8g8


[57]https://www.youtube.com/watch?v=zpWsedYMczM


SharedPreferences


Shared preferences plugin58


Using SharedPreferences in Flutter59


Store key-value data on disk60


Making use of Shared Preferences, Flex Widgets and Dismissibles with Dart's Flutter framework61



[58]https://pub.dev/packages/shared_preferences


[59]https://medium.com/flutterdevs/using-sharedpreferences-in-flutter-251755f07127


[60]https://flutter.dev/docs/cookbook/persistence/key-value


[61]https://www.youtube.com/watch?v=IvrAAMQnj4k


使用Firebase


将 Firebase 添加到您的 Flutter 应用62


Firebase for Flutter63


Flutter - Firestore introduction64



[62]https://firebase.google.com/docs/flutter/setup


[63]https://codelabs.developers.google.com/codelabs/flutter-firebase/#0


[64]https://www.youtube.com/watch?list=PLgGjX33Qsw-Ha_8ks9im86sLIihimuYrr&v=LzEbpALmRlc


其他学习 Flutter 的资源


以下是其他开发人员和Flutter团队提供的一些资源:


Technical overview65


Resources to learn Flutter66


Free resources to learn and advance in Flutter67


Flutter Community68


My Favourite List of Flutter Resources69


awesome-flutter70


londonappbrewery/Flutter-Course-Resources71


A Searchable List of Flutter Resources72


FlutterDevs73



[65]https://flutter.dev/docs/resources/technical-overview


[66]https://medium.com/flutter-community/resources-to-learn-flutter-2ade7aa73305


[67]https://medium.com/flutter-community/free-resources-to-learn-and-advance-in-flutter-e07875ffc825


[68]https://medium.com/flutter-community


[69]https://medium.com/coding-with-flutter/my-favourite-list-of-flutter-resources-523adc611cbe


[70]https://github.com/Solido/awesome-flutter


[71]https://github.com/londonappbrewery/Flutter-Course-Resources


[72]https://flutterx.com/


[73]https://medium.com/flutterdevs


关于 Flutter 的问题


FAQ74


Answering Questions on Flutter App Development75


Flutter Vs. React Native: FAQs for Every Developer76



[74]https://flutter.dev/docs/resources/faq


[75]https://medium.com/@dev.n/answering-questions-on-flutter-app-development-6d50eb7223f3


[76]https://hackernoon.com/flutter-vs-react-native-faqs-for-every-developer-yjp329z


本文仅适用于初学者。


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

Kotlin + Flow 实现的 Android 应用初始化任务启动库

特性 Kotlin + Flow 实现的 Android 应用初始化任务启动库。 支持模块化,按模块加载任务 可指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行,默认值all 可指定任务仅在工作线程执行 可指定任务仅在调试模式执行 ...
继续阅读 »

特性


Kotlin + Flow 实现的 Android 应用初始化任务启动库。



  • 支持模块化,按模块加载任务

  • 可指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行,默认值all

  • 可指定任务仅在工作线程执行

  • 可指定任务仅在调试模式执行

  • 可指定任务在满足合规条件后执行

  • 可指定任务优先级,决定同模块内无依赖同步任务的执行顺序

  • 可指定依赖任务列表,能检测循环依赖

  • 使用 Flow 调度任务

  • 仅200多行代码,简单明了

  • 有耗时统计


引入依赖


项目地址:github.com/czy1121/ini…


repositories { 
maven { url "https://gitee.com/ezy/repo/raw/android_public/"}
}
dependencies {
implementation "me.reezy.init:init:0.9.0"
kapt "me.reezy.init:init-compiler:0.9.0"

// 使用 init-startup 代替 init 可以利用 Jetpack Startup 库自动初始化
// 无需在 Application.onCreate 调用 InitManager.init()
implementation "me.reezy.init:init-startup:0.9.0"
}

使用


AndroidManifest.xml<application> 里添加模块


<meta-data android:name="modules" android:value="app" />

通过注解 @InitInitTask 接口定义一个任务


@Init
class OneInit : InitTask {
override fun execute(app: Application) {
Log.e(TAG, "this is ${javaClass.simpleName} in ${Thread.currentThread().name}")
}
}

通过注解 @Init 的参数配置任务信息


@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Init(
val process: String = "all", // 指定工作进程名称,main 表示仅在主进程运行,all 表示在所有进程运行
val background: Boolean = false, // 是否在工作线程执行任务
val debugOnly: Boolean = false, // 是否仅在 DEBUG 模式执行任务
val compliance: Boolean = false, // 是否需要合规执行
val depends: Array<String> = [], // 依赖的任务列表
val priority: Short = 0 //
)

APT会按模块收集任务信息并生成任务加载器(InitLoader_$moduleName),任务加载器用于添加任务到TaskList


class Task(
val name: String, // APT收集的任务名称格式为 "$moduleName:${clazz.simpleName}"
val background: Boolean = false, // 是否在工作线程执行任务
val priority: Int = 0, // 进程运行的优先级,值小的先执行
val depends: Set<String> = setOf(), // 依赖的任务列表,同模块只需指定"${clazz.simpleName}",跨模块需要指定 "$moduleName:${clazz.simpleName}"
val block: () -> Unit = {}, // 待执行的任务
) {
val children: MutableSet<Task> = mutableSetOf() // 子任务列表
}

核心类



  • TaskList 负责持有和添加任务

  • TaskManager 负责调度任务,支持添加开关任务(没有业务仅作为开关,可手动触发完成,并偿试执行其子任务)

    • 无依赖的异步任务,在子线程并行执行

    • 无依赖的同步任务,在主线程顺序执行

    • 有依赖的任务,确保无循环依赖,且被依赖的任务先执行



  • InitManager 负责找到各模块的任务加载器并开始启动初始化,它使用了一个合规开关来使相关任务在确定合规后执行


可以不使用 InitManager 收集任务


val taskList = TaskList(app).apply {
add("task1") {
}
add("task2", depends = setOf("t1")) {
}
add("task3", depends = setOf("task1")) {
}
}

val manager = TaskManager(taskList, setOf("t1"))
manager.start()

// ...

// 完成开关任务t1
manager.trigger("t1")

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

实现一套自己的WebKit

速度快:相比系统webview的网页打开速度有30+%的提升;省流量:使用云端优化技术使流量节省20+%;更安全:安全问题可以在24小时内修复;更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;。。。打包chromium,生成官方的apk安装打开ch...
继续阅读 »

大家应该都听过腾讯的X5浏览器,据官方介绍其有以下优势:

  • 速度快:相比系统webview的网页打开速度有30+%的提升;

  • 省流量:使用云端优化技术使流量节省20+%;

  • 更安全:安全问题可以在24小时内修复;

  • 更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;

  • 。。。

就不一一列举了,详细见(x5.tencent.com/docs/index.…
既然自定义webkit有能力做到这些,那我们为什么不自己试着整一个?

打包chromium,生成官方的apk

具体实现见Chromium网络请求

安装打开chrome_public_apk,它其实就是chrome浏览器app,那么我们怎么实现一套webkit呢?

webkit解构

webkit的包结构(从快手这看,容易理解)

这是快手app解开后webkit下的内容,整体上看,其实就是拷贝一套系统的webkit的api
上图1 mProvider就是webview的实现类,2 则为系统实现,既然是自定义webkit为什么要有2呢

主要应该是两个原因

  • 是chromium内核包太大了

  • 实现快速修复问题的目的,这可能就是X5提到的安全这点能在24小时之内修复

所以为了处理以上这两个问题,chromium内核一般都是插件化的实现,因此在插件还没有加载到的时候,我们只能去展示系统的webview,这里回到第一张图片,webkit adapter包下的就包含了所有webview参数和返回对象的包装,意在实现两套webkit对应的转换,比如

这里我们使用的地方调用的是com.kuaishou.webkit.CookieManager.getInstance(),在自己的内核没有加载完成的时候,所有调用都中转到了系统的android.webkit.CookieManager

难点攻坚

  • 内核模块调整,我们需要调整内核代码,把所有指向系统webkit包名全部改成我们自定义webkit的包名,为了使内核编译能通过,我们需要拷贝我们自定义的webkit这个模块到内核里去,在编译的时候把其剔除,这里我们需要去稍微了解一下gn构建配置相关的内容。

  • 插件化的实现,插件化现在应该都烂大街了,省略。

// 这里稍微提一下,加载内核apk的Classloader的实现
public static class DexClassLoaderOptimize extends DexClassLoader {
  @Override // java.lang.ClassLoader
  public Class loadClass(String str, boolean z) throws ClassNotFoundException {
      // 所有系统相关的类,或者我们的webkit层全部在我们app层找

      if (str.startsWith("java.") || ((str.startsWith("android.")
&& !str.startsWith("android.support.")) ||
str.startsWith("com.kuaishou.webkit"))) {

          return super.loadClass(str, z);
      }
      // 否则直接在插件中找
      try {
          return findClass(str);
      } catch (Exception unused) {
          return super.loadClass(str, z);
      }
  }
}
  • 整个webkit层,可以参照着快手的实现。

  • 在内核的加载过程中,主要涉及两个动态库一个是webview.so,另一个是webview_plat_support.so,其中webview_plat_support.so是Android系统内部的一个so,内核渲染的一个支持模块,我们需要使用到这个模块去做渲染的事情。(实际上Android10版本以上其实并不依赖该so去实现渲染能力)

webview_plat_support问题攻坚

webview_plat_support.so(以下简称plat_support)

一开始想过直接拷贝一份对应的so到我们的内核里,但是plat_support依赖了一些系统的动态库,api23以后ndk的动态库(见public),有些我们是引用不到的, 另外不同版本可能不同机型的plat_support的实现也有些差异,内核里需要维护太多的plat_support,增加复杂度

采用反射回调系统实现(所有内核里的native方法转到系统webview的实现),一开始在android10上测试ok,以为绕过了该问题,然而当我们在Android10以下的版本上,同时开启系统WebView和自定义webview就出现问题了,运气好的话要么系统WebView黑屏,要么自定义的WebView黑屏,多数情况下会崩溃,主要是plat_support其中一个方法nativeSetChromiumAwDrawGLFunction,保存chromium 内部渲染出口对象是一个单例实现,所以会出现两边同时都调用到这个方法,要么系统WebView黑屏,要么系统自定义Webview黑屏,所以我们需要自己实现一个plat_support,一开始参考快手的实现,因为从第三个图中,我们看到它有对应的plat_support

同时还看到了,它对系统库libskia.so的调用

实现上主要是GraphicsUtils对应的两个native方法比较复杂涉及到GraphicBuffer类 GraphicBuffer这个类位于libui.so,然而快手的plat_support中并没有找到libui.so的调用,既然不引用libui.so,我们就要自己去创建对应的对象,这里我们可以把它改成结构体

匹配所有的成员变量,就可以强转成目标对象(这和java完全不一样,一般没有关联的类的实例不能强转成目标类,不能调用的目标类的方法),只是我们需要该类原本实现的方法在本地重新实现一次,然而目标类的方法里又有别的类的调用,延展开来,最终处理的内容就很多了,还有一大堆版本适配问题,到这里暂时hold。

好了那么我们再来看看X5的实现,

好家伙,这直接反回0,然后我立马测试了一下,通过~~~
也就是说前面海量的代码实现,其实是可以删除的,chromium内部有自己默认的实现(这里我们还是可以反射转回到系统webview的实现,包括nativeGetFunctionTable)。

剩下的我们只需处理以下几个native方法即可 这个几个方法处理就非常简单了,注意kAwDrawGLInfoVersion的版本号适配就Ok了。

结语

当我们整完这一套,就可以开始我们的内核定制了,包括小程序领域所谓的同城渲染能力,JS ServerWorker的定制等等。

作者:北纬34点8度
来源:https: //juejin.cn/post/7037807249103798285

收起阅读 »

先睹为快即将到来的HTML6

HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。 尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核...
继续阅读 »
HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。

尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。

支持原生模式

该元素<dialog> 将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。

这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。

<dialog>
<form method="dialog">
  <input type="submit" value="确定" />
  <input type="submit" value="取消" />
</form>
</dialog>

在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。

可以在 <dialog> 其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>

总的来说,这个标签在用户交互和改进的界面中变得有益。

可以通过更改 <dialog> 标签的 open 属性以控制打开和关闭。

<dialog open>
<p>组件内容</p>
</dialog>

没有 JavaScript 的单页应用程序

FutureClaw 杂志主编 Bobby Mozumder 建议:

将锚元素链接到 JSON/XML、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将 DOM 元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。

据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。

自由调整图像大小

HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。

每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce 标签 img 在处理这个问题时不是很有效。

这个问题可以通过一个新标签 <srcset> 来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。

专用库

将可用库引入 HTML6 绝对是提高开发效率的重要一步。

微格式

很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。

自定义菜单

尽管标签<ul><ol>非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。

这就是创建标签 <menu> 的驱动力,它可以处理按钮驱动的列表元素。

<menu type="toolbar">
<li><button>个人信息</button></li>
<li><button>系统设置</button></li>
<li><button>账号注销</button></li>
</menu>

因此 <menu>,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。

增强身份验证

虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。

集成摄像头

HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。

总结

没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。

作者:天行无忌
来源:https://juejin.cn/post/7032874253573685261

收起阅读 »

Redis分布式锁

需求分布式应⽤进⾏逻辑处理时经常会遇到并发问题。互斥访问某个网络上的资源,需要有一个存在于网络上的锁服务器,负责锁的申请与回收。Redis 可以充当锁服务器的角色。首先,Redis 是单进程单线程的工作模式,所有前来申请锁资源的请求都被排队处理,能保证锁资源的...
继续阅读 »



需求

分布式应⽤进⾏逻辑处理时经常会遇到并发问题。

互斥访问某个网络上的资源,需要有一个存在于网络上的锁服务器,负责锁的申请与回收。Redis 可以充当锁服务器的角色。首先,Redis 是单进程单线程的工作模式,所有前来申请锁资源的请求都被排队处理,能保证锁资源的同步访问。

适用原因:

  • Redis 可以被多个客户端共享访问,是·共享存储系统,可以用来保存分布 式锁

  • Redis 的读写性能高,可以应对高并发的锁操作场景。

实现

在分布式场景下,锁变量需要由一个共享存储系统来维护,这样,多个客户端可以通过访问共享存储系统来访问锁变量。

简单实现

模仿单机上的锁,使用锁变量即可在Redis上实现分布式锁。

我们可以在 Redis 服务器设置一个键值对,用以表示一把互斥锁,当申请锁的时候,要求申请方设置(SET)这个键值对,当释放锁的时候,要求释放方删除(DEL)这个键值对。

但最基本需要保证加锁解锁操作的原子性。同时为了保证锁在异常情况下能被释放,必须设置超时时间。

Redis 2.8 版本中作者加⼊了 set 指令的扩展参数,使得 setnx 和 expire 指令可以⼀起执⾏

加锁原子操作

加锁包含了三个操作(读取锁变量、判断锁变量值以及把锁变量值设置为 1),而这 三个操作在执行时需要保证原子性。

首先是 SETNX 命令,它用于设置键值对的值。具体来说,就是这个命令在执行时会判断键 值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻 辑执⾏的太长,超出了锁的超时限制,就无法保证互斥。

解决方案

最简单的就是避免Redis 分布式锁⽤于较⻓时间的任务。如果真的偶尔出现了,数据出现的⼩波错乱可能需要⼈⼯介⼊解决。

判断拥有者

为了防止锁变量被拥有者之外的客户端进程删除,需要能区分来自不同客户端的锁操作

set 指令的 value 参数设置为⼀个 随机数,释放锁时先匹配随机数是否⼀致,然后再删除 key,这是为 了确保当前线程占有的锁不会被其它线程释放,除⾮这个锁是过期了被服务器⾃动释放的。

Redis 给 SET 命令提供 了类似的选项 NX,用来实现“不存在即设置”。如果使用了 NX 选项,SET 命令只有在键 值对不存在时,才会进行设置,否则不做赋值操作。此外,SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。

可重入问题

可重⼊性是指线程在持有锁的情况下再次请求加锁,如果⼀个锁⽀持 同⼀个线程的多次加锁,那么这个锁就是可重⼊的。Redis 分布式锁如果要⽀持 可重⼊,需要对客户端的 set ⽅法进⾏包装,使⽤线程的 Threadlocal 变量存储当前持有锁的计数。

分布式拓展

单Redis实例并不能满足我们的高可用要求,一旦实例崩溃,就无法对外分布式锁服务。

但在集群环境下,这种只对主Redis实例使用上述方案是有缺陷 的,它不是绝对安全的。

一旦主节点挂掉,但锁变量没有及时同步,就会导致互斥被破坏。

Redlock

为了解决这个问题,Antirez 发明了 Redlock 算法

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户 端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布 式锁了,否则加锁失败。

执行步骤:

  • 获取当前时间

  • 客户端按顺序依次向 N 个 Redis 实例执行加锁操作

  • 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过 程的总耗时。客户端只有在满足下面的这两个条件时,才能认为是加锁成功

    • 客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;

    • 客户端获取锁的总耗时没有超过锁的有效时间。


作者:不是二向箔
来源:https://juejin.cn/post/7038155529566289957

收起阅读 »

是时候封装一个DOM库了

增首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步而这里,只需要一步就能完成dom.create('hi') 它可以直接创建多标签的嵌套,如create('你好') 为什么能这样写? 因为我们用inn...
继续阅读 »

由于原始的DOM提供的API过长,不方便记忆,
于是我采用对象风格的形式封装了一个DOM库->源代码链接
这里对新封装的API进行总结
同样,用增删改查进行划分,我们先提供一个全局的window.dom对象

创建节点

create (string){
       const container = document.createElement("template")//template可以容纳任意元素
       container.innerHTML = string.trim();//去除字符串两边空格
       return container.content.firstChild;//用template,里面的元素必须这样获取  
}

首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步

  1. document.createElement('div')

  2. div.innerText = 'hi'

而这里,只需要一步就能完成dom.create('

hi
')
它可以直接创建多标签的嵌套,如create('
你好
')

为什么能这样写?
因为我们用innerHTML直接把字符串写进了HTML里,字符串直接变成了HTML里面的内容
为什么使用template?
因为template可以容纳任意元素,如果使用div,div不能直接容纳标签,但template就可以

新增哥哥

before(node,node2){
   node.parentNode.insertBefore(node2,node);
}

这个比较简单,找到爸爸节点,然后使用```JavaScript,新增一个node2即可

新增弟弟

after(node,node2){
   node.parentNode.insertBefore(node2,node.nextSibling); //把node2插到node下一个节点的前面,即使node的下一个节点为空,也能插入  
}

由于原始的DOM只有insertBefore,并没有insertAfter,所以要实现这个功能我们需要一个曲线救国的方法:
node.nextSibling 表示node节点的下一个节点,
而想在node的后面插入一个节点,就等于说在node的下一个节点前插入一个新节点node2即可
如上代码就是实现了这个操作,而即使node的下一个节点为空,也能成功插入

新增儿子

 append(parent,node){
       parent.appendChild(node)
}

找到爸爸节点,用appendChild即可

新增爸爸

 wrap(node,parent){
       dom.before(node,parent)
       dom.append(parent,node)
}

思路如图:

image.png

分为两步走:

  1. 先把新增的爸爸节点,放到老节点的前面

  2. 再把老节点放入新增的爸爸节点的里面

这样就可以使新的爸爸节点包裹住老节点

使用示例:

const newDiv = dom.create('
'
)

dom.wrap(test, newDiv)

删节点

remove(node){
      node.parentNode.removeChild(node)
      return node
}

找到爸爸节点,removeChild即可

删除所有子节点

empty(node){
   const array = []
   let x = node.firstChild
   while (x) {
       array.push(dom.remove(node.firstChild))
       x = node.firstChild//x指向下一个节点
  }
   return array
}

其实一开始的思路,是用for循环

for(let i = 0;i<childNodes.length;i++){
   dom.remove(childNodes[i])
}

但这样的思路有一个问题:childNodes.length是会随着删除而变化的
所以我们需要改变思路,用while循环:

  1. 先找到该节点的第一个儿子赋值为x

  2. 当x是存在的,我们就把它移除,并放入数组里面(用于获取删除的节点的引用)

  3. 再把x赋值给它的下一个节点(当第一个儿子被删除后,下一个儿子就变成了第一个儿子)

  4. 反复操作,直到所有子节点被删完

读写属性

 attr(node,name,value){
       if(arguments.length === 3){
           node.setAttribute(name,value)
      }else if(arguments.length === 2){
           return node.getAttribute(name)
      }
}

这里运用重载,实现两种不同的功能:

  1. 当输入的参数是3个时,就写属性

  2. 当如数的参数是2个时,读属性

使用示例:

//写:
//给
test
添加属性

dom.attr(test,'title','Hi,I am Wang')
//添加之后:
test

//读:
const title = dom.attr(test,'title')
console.log(`title:${title}`)
//打印出:title:Hi,Hi,I am Wang

读写文本内容

text(node,string){
   if(arguments.length === 2){
       if('innerText' in node){
           node.innerText = string
      }else{
           node.textContent = string
      }
  }else if(arguments.length === 1){
       if('innerText' in node){
           return node.innerText
      }else{
           return node.textContent
      }
  }
}

为什么这里需要适配,innerText与textContent?
因为虽然现在绝大多数浏览器都支持两种,但还是有非常旧的IE只支持innerText,所以这里是为了适配所有浏览器

同时与读写属性思路相同:

  1. 当输入的参数是2个时,就在节点里写文本

  2. 当输入的参数是1个时,就读文本内容

读写HTML的内容

html(node,string){
   if(arguments.length === 2){
       node.innerHTML = string
  }else if(arguments.length === 1){
           return node.innerHTML
  }
}

同样,2参数写内容,1参数读内容

修改Style

style(node,name,value){
       if(arguments.length === 3){
           //dom.style(div,'color','red')
           node.style[name] = value
      }else if(arguments.length === 2){
           if(typeof name === 'string'){
            //dom.style(div,'color')
           return node.style[name]    
          }else if(name instanceof Object){
               //dom.style(div,{color:'red'})
               const Object = name
               for(let key in Object){
                   //key:border/color
                   //node.style.border = ...
                   //node.style.color = ...
                   node.style[key] = Object[key]
              }
          }
      }
}

思路:

  1. 首先判断输入的参数,如果为3个如:dom.style(div,'color','red')

  2. 就更改它的style

  3. 如果输入参数为2个时,先判断输入name的值的类型

  4. 如果是字符串,如dom.style(div,'color'),就返回style的属性

  5. 如果是对象,如dom.style(div,{border:'1px solid red',color:'blue'}),就更改它的style

增删查class

class:{
   add(node,className){
       node.classList.add(className)    
  },
   remove(node,className){
       node.classList.remove(className)
  },
   has(node,className){
       return node.classList.contains(className)
  }
}

注:查找一个元素的classList里是否有某一个class, 用的是contains

添加事件监听

on(node,eventName,fn){
      node.addEventListener(eventName,fn)
}

使用示例:

const fn = ()=>{
   console.log('点击了')
}
dom.on(test,'click',fn)

这样当点击id为test的div时,就会打印出'点击了'

删除事件监听

off(node,eventName,fn){
   node.removeEventListener(eventName,fn)
}

获取单个或多个标签

find(selector,scope){
   return (scope || document).querySelectorAll(selector)
}

可以在指定区域或者全局的document里找

使用示例:
在document中查询:

const testDiv = dom.find('#test')[0]
console.log(testDiv)

在指定范围内查询:

 <div>
       <div id="test"><span>test1span>
       <p class="red">段落标签p>
       div>
       <div id="test2">
           <p class="red">段落标签p>
       div>
div>

我只想找test2里面的red,应该怎么做

const test2 = dom.find('#test2')[0]
console.log(dom.find('.red',test2)[0])

注意:末尾的[0]别忘记写

获取父元素

parent(node){
   return node.parentNode
}

获取子元素

children(node){
   return node.children
}

获取兄弟姐妹元素

siblings(node){
   return Array.from(node.parentNode.children).filter(n=>n!==node) //伪数组变数组再过滤本身
}

找到爸爸节点,然后过滤掉自己本身

获取弟弟

next(node){
   let x = node.nextSibling
   while(x && x.nodeType === 3){
       x = x.nextSibling
  }
   return x
}

为什么这里需要while(x && x.nodeType === 3)
因为我们不想获取文本节点(空格回车等)
所以当读到文本节点时,自动再去读取下一个节点,直到读到的内容不是文本节点为止

获取哥哥

previous(node){
       let x = node.previousSibling
       while(x && x.nodeType === 3){
           x = x.previousSibling
      }
       return x
}

与上面思路相同

遍历所有节点

each(nodeList,fn){
       for(let i=0;i<nodeList.length;i++){
           fn.call(null,nodeList[i])
      }
}

注:null用于填充this的位置
使用示例:
利用fn可以更改所有节点的style

const t = dom.find('#travel')[0]
dom.each(dom.children(t),(n)=>dom.style(n,'color','red'))

遍历每个节点,把每个节点的style都更改

用于获取排行老几

index(node){
   const list = dom.children(node.parentNode)
   let i;
   for(i=0;i<list.length;i++){
       if(list[i]===node){
            break
      }
  }
   return i
}

思路:

  1. 获取爸爸节点的所有儿子

  2. 设置一个变量i

  3. 如果i等于想要查询的node

  4. 退出循环,返回i值

作者:PrayWang
来源:https://juejin.cn/post/7038171258617331719

收起阅读 »

这一次,彻底搞懂 async...await

执行 async 函数,返回的都是 Promise 对象Promise.then() 对应 awaitPromise.catch() 对应 try...catch先看下面两个函数:async function test1() {  return 1;...
继续阅读 »

先上结论:

  • 执行 async 函数,返回的都是 Promise 对象

  • Promise.then() 对应 await

  • Promise.catch() 对应 try...catch

执行 async 函数,返回的都是 Promise 对象

先看下面两个函数:

async function test1() {
 return 1;
}
async function test2() {
 return Promise.resolve(2);
}
const res1 = test1();
const res2 = test2();
console.log('res1', res1);
console.log('res1', res2);

test1 和 test2 两个函数前面都加了 async,说明这两个都是异步函数,并且如果一个函数前加了 async,那么这个函数的返回值就是一个 Promise(不论这个函数返回的是什么,都会被 JS 引擎包装成 Promise 对象)。

输出结果如下图:

Promise.then() 对应 await

1.直接在一个 await 后面加 promise 对象

看下面的代码:

async function test3() {
 const p3 = Promise.resolve(3);
 p3.then(data => {
   console.log('data', data);
});

 const data = await p3;
 console.log('data', data);
}
test3();

输出结果如下图:

可以看到输出是相同的,这就说明了 Promise 的 then() 方法对应 await。

2.直接在一个 await 后面加一个基本数据类型的值

看下面的例子:

async function test4() {
 const data4 = await 4; // await Promise.resolve(4)
 console.log('data4', data4);
}
test4();

输出结果如下图:

可以看到输出的是 4,上面的 await 4 就相当于 await Promise.resolve(4),又因为 await 相当于 then(),所以输出的就是 4。

3.直接在一个 await 后面加一个异步函数

看下面的例子:

async function test1() {
 return 1;
}
async function test5() {
 const data5 = await test1();
 console.log('data5', data5);
}
test5();

输出结果如下图:

可以看到输出的是 1,首先 test5() 执行,然后执行 test1(),test1 返回数字 1,相当于返回 Promise.resolve(1),await 又相当于 then(),所以输出 1。

:::tip 提示 开发中最常用的就是第三种,await 后面跟一个异步函数,所以一定要掌握! :::

Promise.catch() 对应 try...catch

看下面的例子:

async function test6() {
 const p6 = Promise.reject(6);
 const data6 = await p6;
 console.log('data6', data6);
}
test6();

输出结果如下图:

可以看到没有捕获到错误,那应该怎么做呢?没错,可以使用 try...catch。 看下面的例子:

async function test6() {
 const p6 = Promise.reject(6);
 try {
   const data6 = await p6;
   console.log('data6', data6);
} catch (e) {
   console.log('e', e); // 顺利捕获错误
}
}
test6();

输出结果如下图:

可以看到已经成功捕获到错误了!

作者:ShiYan_Chen
来源:https://juejin.cn/post/7038152028664627230

收起阅读 »

女友半夜加班发自拍 python男友用30行代码发现惊天秘密

事情是这样的接到女朋友今晚要加班的电话如下 ↓ ↓ ↓敏感的小哥哥心生疑窦,难道会有原谅帽然后python撸了一段代码 分析照片小哥哥崩溃之余 大呼上当小哥哥将发给自己的照片原图下载下来并使用python写了一个脚本读取到了照片拍摄的详细的地址详细到了具体的街...
继续阅读 »



事情是这样的

正准备下班的python开发小哥哥

接到女朋友今晚要加班的电话

并给他发来一张背景模糊的自拍照

如下 ↓ ↓ ↓

敏感的小哥哥心生疑窦,难道会有原谅帽
然后python撸了一段代码 分析照片

分析下来 emmm
拍摄地址居然在 XXX酒店

小哥哥崩溃之余 大呼上当

python分析照片

小哥哥将发给自己的照片原图下载下来
并使用python写了一个脚本
读取到了照片拍摄的详细的地址
详细到了具体的街道和酒店名称

引入exifread模块

首先安装python的exifread模块,用于照片分析
pip install exifread 安装exfriead模块

PS C:\WINDOWS\system32> pip install exifread
Collecting exifread
Downloading ExifRead-2.3.2-py3-none-any.whl (38 kB)
Installing collected packages: exifread
Successfully installed exifread-2.3.2
PS C:\WINDOWS\system32> pip install json

GPS经纬度信息

其实我们平时拍摄的照片里,隐藏了大量的私密信息
包括 拍摄时间、极其精确 具体的GPS信息。
下面是通过exifread模块,来读取照片内的经纬度信息。

#读取照片的GPS经纬度信息
def find_GPS_image(pic_path):
GPS = {}
date = ''
with open(pic_path, 'rb') as f:
tags = exifread.process_file(f)
for tag, value in tags.items():
#纬度
if re.match('GPS GPSLatitudeRef', tag):
GPS['GPSLatitudeRef'] = str(value)
#经度
elif re.match('GPS GPSLongitudeRef', tag):
GPS['GPSLongitudeRef'] = str(value)
#海拔
elif re.match('GPS GPSAltitudeRef', tag):
GPS['GPSAltitudeRef'] = str(value)
elif re.match('GPS GPSLatitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLatitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLatitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSLongitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLongitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLongitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSAltitude', tag):
GPS['GPSAltitude'] = str(value)
elif re.match('.*Date.*', tag):
date = str(value)
return {'GPS_information': GPS, 'date_information': date}

百度API将GPS转地址

这里需要使用调用百度API,将GPS经纬度信息转换为具体的地址信息。

这里,你需要一个调用百度API的ak值,这个可以注册一个百度开发者获得,当然,你也可以使用博主的这个ak

调用之后,就可以将拍摄时间、拍摄详细地址都解析出来。

def find_address_from_GPS(GPS):
secret_key = 'zbLsuDDL4CS2U0M4KezOZZbGUY9iWtVf'
if not GPS['GPS_information']:
return '该照片无GPS信息'
#经纬度信息
lat, lng = GPS['GPS_information']['GPSLatitude'], GPS['GPS_information']['GPSLongitude']
baidu_map_api = "http://api.map.baidu.com/geocoder/v2/?ak={0}&callback=renderReverse&location={1},{2}s&output=json&pois=0".format(
secret_key, lat, lng)
response = requests.get(baidu_map_api)
#百度API转换成具体的地址
content = response.text.replace("renderReverse&&renderReverse(", "")[:-1]
print(content)
baidu_map_address = json.loads(content)
#将返回的json信息解析整理出来
formatted_address = baidu_map_address["result"]["formatted_address"]
province = baidu_map_address["result"]["addressComponent"]["province"]
city = baidu_map_address["result"]["addressComponent"]["city"]
district = baidu_map_address["result"]["addressComponent"]["district"]
location = baidu_map_address["result"]["sematic_description"]
return formatted_address,province,city,district,location

if __name__ == '__main__':
GPS_info = find_GPS_image(pic_path='C:/女友自拍.jpg')
address = find_address_from_GPS(GPS=GPS_info)
print("拍摄时间:" + GPS_info.get("date_information"))
print('照片拍摄地址:' + str(address))

老王得到的结果是这样的

照片拍摄地址:('云南省红河哈尼族彝族自治州弥勒县', '云南省', '红河哈尼族彝族自治州', '弥勒县', '湖泉酒店-A座东南128米')

云南弥勒湖泉酒店,这明显不是老王女友工作的地方,老王搜索了一下,这是一家温泉度假酒店。

顿时就明白了

{"status":0,"result":{"location":{"lng":103.41424699999998,"lat":24.410461020097278},
"formatted_address":"云南省红河哈尼族彝族自治州弥勒县",
"business":"",
"addressComponent":{"country":"中国",
"country_code":0,
"country_code_iso":"CHN",
"country_code_iso2":"CN",
"province":"云南省",
"city":"红河哈尼族彝族自治州",
"city_level":2,"district":"弥勒县",
"town":"","town_code":"","adcode":"532526",
"street_number":"",
"direction":"","distance":""},
"sematic_description":"湖泉酒店-A座东南128米",
"cityCode":107}}

拍摄时间:2021:5:03 20:05:32
照片拍摄地址:('云南省红河哈尼族彝族自治州弥勒县', '云南省', '红河哈尼族彝族自治州', '弥勒县', '湖泉酒店-A座东南128米')

完整代码如下

import exifread
import re
import json
import requests
import os

#转换经纬度格式
def latitude_and_longitude_convert_to_decimal_system(*arg):
"""
经纬度转为小数, param arg:
:return: 十进制小数
"""
return float(arg[0]) + ((float(arg[1]) + (float(arg[2].split('/')[0]) / float(arg[2].split('/')[-1]) / 60)) / 60)

#读取照片的GPS经纬度信息
def find_GPS_image(pic_path):
GPS = {}
date = ''
with open(pic_path, 'rb') as f:
tags = exifread.process_file(f)
for tag, value in tags.items():
#纬度
if re.match('GPS GPSLatitudeRef', tag):
GPS['GPSLatitudeRef'] = str(value)
#经度
elif re.match('GPS GPSLongitudeRef', tag):
GPS['GPSLongitudeRef'] = str(value)
#海拔
elif re.match('GPS GPSAltitudeRef', tag):
GPS['GPSAltitudeRef'] = str(value)
elif re.match('GPS GPSLatitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLatitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLatitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSLongitude', tag):
try:
match_result = re.match('\[(\w*),(\w*),(\w.*)/(\w.*)\]', str(value)).groups()
GPS['GPSLongitude'] = int(match_result[0]), int(match_result[1]), int(match_result[2])
except:
deg, min, sec = [x.replace(' ', '') for x in str(value)[1:-1].split(',')]
GPS['GPSLongitude'] = latitude_and_longitude_convert_to_decimal_system(deg, min, sec)
elif re.match('GPS GPSAltitude', tag):
GPS['GPSAltitude'] = str(value)
elif re.match('.*Date.*', tag):
date = str(value)
return {'GPS_information': GPS, 'date_information': date}

#通过baidu Map的API将GPS信息转换成地址。
def find_address_from_GPS(GPS):
"""
使用Geocoding API把经纬度坐标转换为结构化地址。
:param GPS:
:return:
"""
secret_key = 'zbLsuDDL4CS2U0M4KezOZZbGUY9iWtVf'
if not GPS['GPS_information']:
return '该照片无GPS信息'
lat, lng = GPS['GPS_information']['GPSLatitude'], GPS['GPS_information']['GPSLongitude']
baidu_map_api = "http://api.map.baidu.com/geocoder/v2/?ak={0}&callback=renderReverse&location={1},{2}s&output=json&pois=0".format(
secret_key, lat, lng)
response = requests.get(baidu_map_api)
content = response.text.replace("renderReverse&&renderReverse(", "")[:-1]
print(content)
baidu_map_address = json.loads(content)
formatted_address = baidu_map_address["result"]["formatted_address"]
province = baidu_map_address["result"]["addressComponent"]["province"]
city = baidu_map_address["result"]["addressComponent"]["city"]
district = baidu_map_address["result"]["addressComponent"]["district"]
location = baidu_map_address["result"]["sematic_description"]
return formatted_address,province,city,district,location
if __name__ == '__main__':
GPS_info = find_GPS_image(pic_path='C:/Users/pacer/desktop/img/5.jpg')
address = find_address_from_GPS(GPS=GPS_info)
print("拍摄时间:" + GPS_info.get("date_information"))
print('照片拍摄地址:' + str(address))

作者:LexSaints
来源:https://juejin.cn/post/6967563349609414692

收起阅读 »

会话过期后token刷新,重新请求接口(订阅发布模式)

需求响应拦截拦截到302后,我们进入到刷新token逻辑我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,...
继续阅读 »

前言


❝ 最近,我们老大让小白搞一下登录模块,登录模块说简单也简单,复杂也复杂。本章主要讲一下,会话过期后,token 刷新的一系列的事。 ❞

需求

在一个页面内,当请求失败并且返回 302 后,判断是接口过期还是登录过期,如果是接口过期,则去请求新的token,然后拿新的token去再次发起请求.

思路


  • 当初,想了一个黑科技(为了偷懒),就是拿到新的token后,直接强制刷新页面,这样一个页面内的接口就自动刷新啦~(方便是方便,用户体验却不好)

  • 目前,想到了重新请求接口时,可以配合订阅发布模式来提高用户体验

响应拦截

首先我们发起一个请求 axios({url:'/test',data:xxx}).then(res=>{})

拦截到302后,我们进入到刷新token逻辑

响应拦截代码

axios.interceptors.response.use(
   function (response) {
       if (response.status == 200) {
           return response;
      }
  },
  (err) => {
       //刷新token
       let res = err.response || {};
       if (res.data.meta?.statusCode == 302) {
           return refeshToken(res);
      } else {  
           return err;
      }
  }
);

我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken方法~

刷新token方法

//避免其他接口同时请求(只请求一次token接口)
let isRefreshToken = false;
const refeshToken = (response) => {
  if (!isRefreshToken) {
           isRefreshToken = true;
           axios({
               //获取新token接口
               url: `/api/refreshToken`,
          })
              .then((res) => {
                   const { data = '', meta = {} } = res.data;
                   if (meta.statusCode === 200) {
                       isRefreshToken = false;
                       //发布 消息
                       retryOldRequest.trigger(data);
                  } else {
                       history.push('/user/login');
                  }
              })
              .catch((err) => {
                   history.push('/user/login');
              });
      }
       //收集订阅者 并把成功后的数据返回原接口
       return retryOldRequest.listen(response);
};

看到这,有的小伙伴就有点奇怪retryOldRequest这个又是什么?没错,这就是我们男二 订阅发布模式队列。

订阅发布模式


大家如果还不了解订阅发布模式,可以点击看一下,里面有大神写的通俗易懂的例子(觉得学到的话,可以顺便帮点赞哦~)。

把失败的接口当订阅者,成功拿到新的token后再发布(重新请求接口)。

以下便是订阅发布模式代码

const retryOldRequest = {
   //维护失败请求的response
   requestQuery: [],

   //添加订阅者
   listen(response) {
       return new Promise((resolve) => {
           this.requestQuery.push((newToken) => {
               let config = response.config || {};
               //Authorization是传给后台的身份令牌
               config.headers['Authorization'] = newToken;
               resolve(axios(config));
          });
      });
  },

   //发布消息
   trigger(newToken) {
       this.requestQuery.forEach((fn) => {
           fn(newToken);
      });
       this.requestQuery = [];
  },
};

大家可以先不用关注订阅者的逻辑,只需要知道订阅者是每次请求失败后的接口(reponse)就好了。

每次进入refeshToken方法,我们失败的接口都会触发retryOldRequest.listen去订阅,而我们的requestQuery则是保存这些订阅者的队列。

注意:我们订阅者队列requestQuery是保存待发布的方法。而在成功获取新token后,retryOldRequest.trigger就会去发布这些消息(新token)给订阅者(触发订阅队列的方法)。

而订阅者(response)里面有config配置,我们拿到新的token后(发布后),修改config里面的请求头Autorzation.而借助Promise我们可以更好的拿到新token请求回来的接口数据,一旦请求到数据,我们可以原封不动的返回给原来的接口/test了(因为我们在响应拦截那里返回的是refreshToken,而refreshToken又返回的是订阅者retryOldRequest.listen返回的数据,而Listiner又返回Promise的数据,Promise又在成功请求后resolve出去)。

看到这,小伙伴们是不是觉得有点绕了~

而在真实开发中,我们的逻辑还含有登录过期(与请求过期区分开来)。我们是根据当前时间 - 过去时间 < expiresTime(epiresTime:登录后返回的有效时间)来判断是请求过期还是登录过期的。 以下是完整逻辑
以下是完整代码

const retryOldRequest = {
   //维护失败请求的response
   requestQuery: [],

   //添加订阅者
   listen(response) {
       return new Promise((resolve) => {
           this.requestQuery.push((newToken) => {
               let config = response.config || {};
               config.headers['Authorization'] = newToken;
               resolve(axios(config));
          });
      });
  },

   //发布消息
   trigger(newToken) {
       this.requestQuery.forEach((fn) => {
           fn(newToken);
      });
       this.requestQuery = [];
  },
};
/**
* sessionExpiredTips
* 会话过期:
* 刷新token失败,得重新登录
* 用户未授权,页面跳转到登录页面
* 接口过期 => 刷新token
* 登录过期 => 重新登录
* expiresTime => 在本业务中返回18000ms == 5h
* ****/

//避免其他接口同时请求
let isRefreshToken = false;
let timer = null;
const refeshToken = (response) => {
   //登录后拿到的有效期
   let userExpir = localStorage.getItem('expiresTime');
   //当前时间
   let nowTime = Math.floor(new Date().getTime() / 1000);
   //最后请求的时间
   let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
   let token = localStorage.getItem('token');

   if (token && nowTime - lastResTime < userExpir) {
       if (!isRefreshToken) {
           isRefreshToken = true;
           axios({
               url: `/api/refreshToken`,
          })
              .then((res) => {
                   const { data = '', meta = {} } = res.data;
                   isRefreshToken = false;
                   if (meta.statusCode === 200) {
                       localStorage.getItem('token', data);
                       localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
                      );
                       //发布 消息
                       retryOldRequest.trigger(data);
                  } else {
                      //去登录
                  }
              })
              .catch((err) => {
                   isRefreshToken = false;
                  //去登录
              });
      }
       //收集订阅者 并把成功后的数据返回原接口
       return retryOldRequest.listen(response);
  } else {
       //节流:避免重复运行
      //去登录
  }
};

// http response 响应拦截
axios.interceptors.response.use(
   function (response) {
       if (response.status == 200) {
           //记录最后操作时间
          localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
           return response;
      }
  },
  (err) => {
       let res = err.response || {};
       if (res.data.meta?.statusCode == 302) {
           return refeshToken(res);
      } else {
           //302 报的错误;
           return err;
      }
  }
);

以上便是我们这边的业务,如果写的不好请大佬多担待~~

如果有好方案的小伙伴也可以在评论区内互相讨论~


作者:用户3797421129853
来源:https://juejin.cn/post/7037787299202990093

收起阅读 »

通过 Performance 证明,网页的渲染是一个宏任务

别着急反驳,后面我会给出证据。调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。但是性能分析的调试工具却不能这样做...
继续阅读 »

网页的渲染是一个宏任务。 这是我下的一个结论。

别着急反驳,后面我会给出证据。

我们先来聊下什么是调试:

调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。

我们最常用的调试工具是 JS Debugger,它支持断点,可以在某处断住,查看当前上下文的变量、调用栈等,这对于理清逻辑很有帮助。

但是性能分析的调试工具却不能这样做,不能用断住的方式实时查看,因为会影响数据的真实性。所以这类工具都是通过录制一段时间的数据,然后作事后的统计和分析的方式,常用的是 Chrome Devtools 里的 Performance 工具。(甚至为了避免浏览器插件的影响,还要用无痕模式来运行网页)点击录制按钮 record 开始录制(如果想录制从页面加载开始的数据,就点击 reload 按钮),Performance 会记录下录制时间内各方面的数据。

有哪些数据呢?

网页的运行是有多个线程的,主线程负责通过 Event Loop 的方式来不断的执行 JS 和渲染,也有一些别的线程,比如合成渲染图层的线程,Web Worker 的线程等,渲染的每一帧会绘制到界面上。

网页是这样运行的,那记录的自然也都是这些数据:Performance 会记录网页的每个线程的数据,其中最重要的是主线程,也就是图中的 Main,这部分记录着 Event Loop 的执行过程,记录着 JS 执行的调用栈和页面渲染的流程。看到图中标出的一个个小灰块了么,那就是一个个 Task,也就是宏任务。Event Loop 就是循环执行宏任务。每个 Task 都有自己的调用栈,可以看到函数的执行路径,耗时等信息。图中宽度代表了耗时,可以直观的通过块的宽窄来分析性能。

执行完宏任务会执行所有的微任务,在图中也可以清晰的看到:点击每一个块可以看到代码的位置,可以定位到对应代码,这样就可以分析出哪块代码性能不好。这些是 Main 线程的执行逻辑,也就是通过 Event Loop 来不断执行 JS 和渲染。

当然,还有其他线程,比如光栅化线程,也就是负责把渲染出的图层合并成一帧的线程:总之,就像 Debugger 面前,JS 的执行过程没有秘密一样,在 Performance 面前,网页各线程的执行过程也没有秘密。

说了这么多,就是为了讲清楚调试工具和 Performance 都是干啥的,它记录了哪些信息。

我们想知道渲染是不是一个宏任务,自然可以通过 Performance 来轻易的分析出来。

我们继续看 Main 线程的 Event Loop 执行过程:你会看到一个个很小的灰块,也就是一个个 Task,每隔一段时间都会执行,点击它,就会看到其实他做的就是渲染,包括计算布局,更新渲染树,合并图层、渲染等。

这说明了什么,不就说明了渲染是一个宏任务么。

所以,我们得到了结论:渲染是一个宏任务,通过 Event Loop 来做一帧帧的渲染。

通过 Performance 调试工具,我们可以看到 Main 线程 Event Loop 的细节,看到 JS 执行和渲染的详细过程。

有时你可能会看到有的 Task 部分被标红了,还警告说这是 Long Task。因为渲染和 JS 执行都是在同一个 Event Loop 内做的,那如果有执行时间过长的 Task,自然会导致渲染被延后,也就是掉帧,用户感受到的就是页面的卡顿。

避免 Long Task,这是网页性能优化的一个重点。这也是为什么 React 使用了 Fiber 架构的可打断的组件树渲染,替代掉了之前的递归渲染整个组件树的方式,就是为了不产生 Long Task。

总结

本文目的为了证明渲染是不是一个宏任务,但其实更重要的是想讲清楚调试工具的意义。

调试工具可以分析程序运行过程中某一刻或某一段时间的各方面的数据,有两种方式:一种是 Debugger 那种断点的方式,可以看到上下文变量的值、调用栈,可以帮助理清逻辑、定位问题。而性能分析工具则是另一种方式,通过录制一段时间内的各种数据,做事后的分析和统计,这样能保证数据的真实性。

网页的性能分析工具 Performance 可以记录网页执行过程中的各个线程的执行情况,主要是主线程的 Event Loop 的执行过程,包括 JS 执行、渲染等。

通过 Performance,我们可以轻易的得出“渲染是一个宏任务”的结论。

就像在 Debugger 面前,JS 执行过程没有秘密一样。在 Performance 面前,网页的执行过程也同样没有秘密。

作者:zxg_神说要有光
来源:https://juejin.cn/post/7037839989018722340

收起阅读 »

短信跳小程序

方案:使用微信提供的url link方法 生成短链接 发送给用户 用户点击短链接会跳转到微信提供默认的默认页面 进而打开小程序场景假设:经理人发布一条运输任务 司机收到短信点击打开小程序接单经理人发布时点击发布按钮 h5调用服务端接口 传参服务端要跳转的小程序...
继续阅读 »

方案:使用微信提供的url link方法 生成短链接 发送给用户 用户点击短链接会跳转到微信提供默认的默认页面 进而打开小程序
场景假设:经理人发布一条运输任务 司机收到短信点击打开小程序接单

实现:

  • 经理人发布时点击发布按钮 h5调用服务端接口 传参服务端要跳转的小程序页面 所需要参数

  • 服务端拿access_token和前端传的参数 加链接失效时间 调用微信api

api.weixin.qq.com/wxa/generat… 得到链接 如wxaurl.cn/ow7ctZP4n8v 将此链接发送短信给司机

  • 司机点击此链接 效果如下图所示:打开小程序 h5写逻辑跳转指定页面

自己调postman调微信api post方式 接口: api.weixin.qq.com/wxa/generat…

传参

{ 
    "path": "pages/index/index",\
    "query": "?fromType=4&transportBulkLineId=111&isLinkUrlCome=1&SCANFROMTYPE=143&lineAssignRelId=111",\
     "env_version": "trial",\
     "is_expire": true,\
    "expire_time": "1638855772"\
}

返参

{
     "errcode": 0,
    "errmsg": "ok",
    "url_link": "https://wxaurl.cn/GAxGcil2Bbp"
}

url link说明文档: developers.weixin.qq.com/miniprogram…
url link方法需要服务端调用 调用接口方式参考:
developers.weixin.qq.com/miniprogram…

作者:懿小诺
来源:https://juejin.cn/post/7037356611031007239

收起阅读 »

技术选型,Vue和React的对比

1. MVVM和MVCVue是MVVM,React是MVC。MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它...
继续阅读 »

1. MVVM和MVC

Vue是MVVM,React是MVC。

MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。

也就是说MVVM实现的是业务逻辑组件的重用,使开发更高效,结构更清晰,增加代码的复用性。

可以理解为MVVM是MVC的升级版。

虽然React不算一个完整的MVC框架,可以认为是MVC中的V(View),但是Vue的MVVM还是更面向未来一些。

2. 数据绑定

vue是双向绑定,react是单向绑定。

单向绑定的优点是相应的可以带来单向数据流,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于项目的可维护性。

但是Vue虽然是双向绑定,但是也是单向数据流,它的双向绑定只是一个语法糖,想看正经的双向绑定可以去看下Dva。

单向绑定的缺点则是代码量会相应的上升,数据的流转过程变长,从而出现很多类似的重复代码。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得冗余。

双向绑定可以在表单交互较多的场景下,会简化大量业务无关的代码。

我认为Vue的设计方案好一些,全局性数据流使用单向,局部性数据流使用双向。

3. 数据更新

3.1 React 更新流程

React 推崇 Immutable(不可变),通过重新render去发现和更新自身。

3.2 Vue 更新流程

Vue通过收集数据依赖去发现更新。

Vue很吸引人的就是它的响应式更新,Vue首次渲染触发data的getter,从而触发依赖收集,为对应的数据创建watcher,当数据发生更改的时候,setter被触发,然后通知各个watcher在下个tick的时候更新数据。

所以说,如果data中某些数据没有在模板template 中使用的话,更新这些数据的时候,是不会触发更新的。这样的设计非常好,没有在模版上用到的变量,当它的值发生变化时,不更新视图,相当于内置了React的shouldComponentUpdate。

3.3 更新比较

  • 获取数据更新的手段和更新的粒度不一样

Vue通过依赖收集,当数据更新时 ,Vue明确知道是哪些数据更新了,每个组件都有自己的渲渲染watcher,掌管当前组件的视图更新,所以可以精确地更新对应的组件,所以更新的粒度是组件级别的。

React会递归地把所有的子组件重新render一下,不管是不是更新的数据,此时,都是新的。然后通过 diff 算法 来决定更新哪部分的视图。所以,React 的更新粒度是一个整体。

  • 对更新数据是否需要渲染页面的处理不一样

  • 只有依赖收集的数据发生更新,Vue 才会去重新渲染页面

  • 只要数据有更新(setState,useState 等手段触发更新),都会去重新渲染页面 (可以使用shouldComponentUpdate/ PureComponent 改善)

Vue的文档里有一描述说,Vue是细粒度数据响应机制,所以说数据更新这一块,我认为Vue的设计方案好一些。

4. 性能对比

借用尤大大的一段话:

模板在性能这块吊打 tsx,在 IDE 支持抹平了的前提下用 tsx 本质上是在为了开发者的偏好牺牲用户体验的性能(性能没遇到瓶颈就无所谓) 这边自己不维护框架的人吐槽吐槽我也能理解,毕竟作为使用者只需要考虑自己爽不爽。作为维护者,Vue 的已有的用户习惯、生态和历史包袱摆在那里,能激进的程度是有限的,Vue 3 的大部分设计都是戴着镣铐跳舞,需要做很多折衷。如果真要激进还不如开个新项目,或者没人用的玩票项目,想怎么设计都可以。 组件泛型的问题也有不少人提出了,这个目前确实不行,但不表示以后不会有。 最后实话实说,所有前端里面像这个问题下面的类型体操运动员们毕竟是少数,绝大部分有 intellisense + 类型校验就满足需求了。真的对类型特别特别较真的用 React 也没什么不好,无非就是性能差点。

为什么模板性能吊打TSX?

tsx和vue template其实都是一样的模版语言,tsx最终也会被编译成createElement,模板被编译成render函数,所以本质上两者都有compile-time和runtime,但tsx的特殊性在于它本身是在ts语义下的,过于灵活导致优化无从下手。但是vue的模板得益于自身本来就是DSL,有自己的文法和语义,所以vue在模板的compile-time做了巨多的优化,比如提升不变的vnode,以及blocktree配合patchflag靶向更新,这些优化在最终的runtime上会把性能拉开不少。

DSL: 一种为特定领域设计的,具有受限表达性编程语言。

所以说Vue的性能是优于React的。

5. React Hooks和Vue Hooks

其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:

  1. 不要在循环,条件或嵌套函数中调用 Hook

  2. 确保总是在你的 React 函数的最顶层调用他们。

  3. 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

而 Vue 带来的不同在于:

  1. 与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup函数仅被调用一次,这在性能上比较占优。

  2. 对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。

  3. 不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。

  4. React Hook 有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。

  5. 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。

我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。

--- 来自ssh

Vue的组合式API刚出来的时候确实一看好像React Hooks,我也对它的.value进行了吐槽,

但是总体来说还是更偏向于Vue Hooks。

6. 写法

React的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等,所以说React的写法感觉相对自由一些,逻辑正确老子想怎么写怎么写,对于我来说,我确实更偏向于React的写法。

Vue则是把html,css,js组合到一起,就像 Web 开发多年的传统开发方式一样, vue-loader会解析文件,提取每个语言块用各自的处理方式,vue有单文件组件(SFC),可以把html、css、js写到一个文件中,html提供了模板引擎来处理。Vue感觉是给你搭了一个框架,告诉你什么地方该写什么东西,你只要按照他的要求向里面填内容就可以了,没有React那么自由,但是上手难度简单了许多。而且因为SFC,一个组件的代码会看起来很长,维护起来很头痛。

7. 理念及设计

Vue 和 React 的核心差异,以及核心差异对后续设计产生的“不可逆”影响。

Vue 和 React 在 API 设计风格和哲学理念(甚至作者个人魅力)上的不同。

Vue 和 React 在工程化预编译构建阶段,AOT 和 JIT 优化的本质差异和设计。

这个层次的比较确实对我难度确实大,我也懒得去copy,下面是Lucas大佬的分析,可以去看一下,时空隧道

作者:黑色的枫
来源:https://juejin.cn/post/7037365650251055134

收起阅读 »

微前端-从了解到动手搭建

前言微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署。与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和...
继续阅读 »

前言

微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署

为什么出现?

与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。

微前端 vs 巨石应用


微前端巨石应用
可维护性拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。
开发效率结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。
代码复用所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。

架构方案

基座模式是当前比较常见的微前端架构设计。

首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。

应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。

目前可以实现微前端架构的方案有如下:

HTTP后端路由转发(nginx)

  • ✅ 简单高效快速,同时不需要前端做额外的工作。

  • ❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新

iframe

  • ✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小

  • ❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等

各个业务独立打到npm包中

  • ✅ 门槛低,易上手

  • ❌ 模块修改后需要重新部署发布,太麻烦。

组合式应用路由分发(基座模式)

  • ✅ 纯前端改造,体验良好,各个业务相互独立

  • ❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题

web component

  • ✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态

  • ❌ 比较新,兼容性较差

微前端页面形态

微前端基座框架需要解决的问题

路由分发

作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:

  1. 远程拉取子应用内容

  2. 将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域

  3. 当子应用切换走时,同步卸载这些内容

对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:

  1. 当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。

  2. 最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个子应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给子应用的路由,子应用可以是手动监听hashchange或者popstate事件接收,或者采用react-router接管路由,后面的逻辑就由子应用自己控制。

应用隔离

应用隔离问题主要分为主应用和子应用,子应用和子应用之间的JavaScript执行环境隔离,CSS样式隔离,

CSS

  • 当主应用和子应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。

  • 而对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。

JavaScript隔离

  • 每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。

  • 沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象,需要结合 with 关键字和window.Proxy对象来实现浏览器端的沙箱。

消息通信

应用间通信有很多种方式,当然,要让多个分离的子应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个子应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。

当然,如果基座和子应用采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。

搭一个看看?

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。也是支付宝内部广泛使用的微前端框架。

那么我们就使用 qiankun 从头搭一个demo出来体验一下

基座

  • 基座我们使用react,自行使用 create-react-app 创建一个react项目即可。

  • npm install qiankun -s

  • 在基座中需要调用 registerMicroApps 注册子应用,然后调用start启动

因此在 index.js 中插入如下代码

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
{
   name: 'vueApp',
   entry: '//localhost:8080',
   container: '#container',
   activeRule: '/app-vue',
},
]);

// 启动 qiankun
start();
  • 修改App.js

    • 加入一些 antd 元素,让demo像样一些

    • 同时,由于qiankun根据路由来加载不同微应用,我们也安装 react-router-dom

    • npm install react-router-dom

    • 安装完之后修改 App.js 如下:

import { useState } from 'react';
import { Layout, Menu } from 'antd';
import { PieChartOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom'
import './App.css';

const { Header, Content, Footer, Sider } = Layout;

const App = () => {
 const [collapsed, setCollapsed] = useState(false);
 
 const onCollapse = collapsed => {
   setCollapsed(collapsed);
};

 return (
   <Layout style={{ minHeight: '100vh' }}>
     <Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
       <div className="logo" />
       <Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
         <Menu.Item key="1" icon={<PieChartOutlined />}>
           <Link to="/app-vue">Vue应用</Link>
         </Menu.Item>
       </Menu>
     </Sider>
     <Layout className="site-layout">
       <Header className="site-layout-background" style={{ padding: 0 }} />
       <Content style={{ margin: '16px' }}>
         <div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
       </Content>
       <Footer style={{ textAlign: 'center' }}>This Project ©2021 Created by DiDi</Footer>
     </Layout>
   </Layout>
);
}

export default App;
  • 记得修改 index.js,把 App 组件用 react-router-dom 的 BrowserRouter 包一层,让 BrowserRouter 作为顶层组件才可以跳转

  • 至此,基座搭好了

子页面

尝试使用vue作为子页面,来体现微前端的技术隔离性。

  • 使用vue-cli创建vue2.x项目

  • 修改main.js如下:

import Vue from "vue/dist/vue.js";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

// window.__POWERED_BY_QIANKUN__ 为true 说明在 qiankun 架构中
// 修改webpack的publicPath,将子应用资源加载的公共基础路径设为 qiankun 包装后的路径
// 这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的实际地址是子应用的服务器地址,子应用的应用资源都在他本身的实际服务器上
if (window.__POWERED_BY_QIANKUN__) {
 // eslint-disable-next-line no-undef
 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

let instance = null;
function render(props = {}) {
 const { container } = props;
 instance = new Vue({
   router,
   render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}

// 独立运行时 直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
 render();
}

// 应用需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
export async function bootstrap() {
 console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
 console.log("[vue] props from main framework", props);
 render(props);
}

export async function unmount() {
 instance.$destroy();
 instance.$el.innerHTML = "";
 instance = null;
}
  • router.js配置如下:

import Vue from "vue/dist/vue.js";
import VueRouter from "vue-router";

Vue.use(VueRouter);

const routes = [
{
   path: "/test",
   name: "Test",
   component: () => import("./components/Test.vue"),
},
{
   path: "/hello",
   name: "Hello",
   component: () => import("./components/Hello.vue"),
},
];

const router = new VueRouter({
 base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : "/",
 mode: "history",
 routes,
});

export default router;
  • 根目录下新建vue.config.js 用来配置webpack,内容如下:

const { name } = require("./package");
module.exports = {
 devServer: {
   // 跨域
   headers: {
     "Access-Control-Allow-Origin": "*",
  },
},
 configureWebpack: {
   output: {
     library: `${name}-[name]`,
     // 把微应用打包成 umd 库格式
     libraryTarget: "umd",
     jsonpFunction: `webpackJsonp_${name}`,
  },
},
};

启动

基座和子应用分别启动,可以看到,子应用已经加载到了主应用中:


作者:visa
来源:https://juejin.cn/post/7037386845751083021

收起阅读 »

实现一个逐步递增的数字动画

背景 可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。 先上一个思维导图: 一、实现类似滚轮的效果,容器固定,数字向上滚动 先列举所有的可能的值形成一个...
继续阅读 »

背景


可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。


先上一个思维导图:


思维导图.png


一、实现类似滚轮的效果,容器固定,数字向上滚动


demo1.gif


先列举所有的可能的值形成一个纵向的列表,然后固定一个容器,匀速更改数字的偏移值。


下面来介绍一下这种方案的实现,元素值从0到9一共十个值,每个数字占纵向列表的10%,所以纵向偏移值依次为为0% —> -90%


实现:


<ul>
<li>
<span>0123456789</span>
</li>
</ul>

ul{
margin-top: 200px;
}
ul li{
margin:0 auto;
width: 20px;
height: 30px;
text-align: center;
border:2px solid rgba(221,221,221,1);
border-radius:4px;
}
ul li span{
position: absolute;
color: #fff;
top: 30%;
left: 50%;
transform: translate(-50%,0);
transition: transform 500ms ease-in-out;
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 17px;
}

let spanDom = document.querySelector('span')
let start = 0
setInterval(() =>{
start++
if(start>9){
start = 0
}
spanDom.style.transform = `translate(-50%,-${start*10}%)`
}, 1000)

上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。 但是由于设定了固定的过渡动画时间,就会出现一个向反方向滚动的情况,为了解决这个问题,可以参考无缝滚动的思路




  • 在9后面复制一份0,




  • 当纵向列表滚动到9的时候,继续滚动到复制的0




  • 滚动到复制的0的时候,把列表的偏移位置改为0,并且控制动画时间为0




<ul>
<li>
<span>01234567890</span>
</li>
</ul>

let spanDom = document.querySelector('span')
let start = 0
var timer = setInterval(fn, 1000);
function fn() {
start++
clearInterval(timer)
timer = setInterval(fn,start >10 ? 0 : 1000);
if(start>10){
spanDom.style.transition = `none`
start = 0
}else{
spanDom.style.transition = `transform 500ms ease-in-out`
}
spanDom.style.transform = `translate(-50%,-${start/11*100}%)`
}

demo2.gif


利用两个元素实现滚动


仔细看动图的效果,事实上在在视口只有两个元素,一个值之前的值,一个为当前的值,滚动偏移值只需设置translateY(-100%)


具体思路:




  • 声明两个变量,分别存放之前的值prev,以及变化后的值cur;声明一个变量play作为这两个值的滚动动画的开关




  • 使用useEffect监听监听传入的值:如果是有效的数字,那么把没有变化前的值赋值给prev,把当前传入的值赋值给cur,并且设置palytrue开启滚动动画




下面是调整后的代码结构:


 <div className={styles.slider}>
{[prev, cur].map((item, index) => (
<span key={index} className={`${styles['slider-text']} ${playing && styles['slider-ani']} ${(prev === 0 && cur === 0 && index ===0) && styles['slider-hide']}`}>
{item}
</span>
))}
</div>

const { value} = props
const [prev, setPrev] = useState(0)
const [cur, setCur] = useState(0)
const [playing, setPlaying] = useState(false)

const play = (pre, current) => {
setPrev(pre)
setCur(current)
setPlaying(false)
setTimeout(() => {
setPlaying(true)
}, 20)
}

useEffect(() => {
if (!Number.isNaN(value)) {
play(cur, value)
} else {
setPrev(value)
setCur(value)
}
}, [value])

.slider {
display: flex;
flex-direction: column;
height: 36px;
margin-top: 24%;
overflow: hidden;
text-align: left;
}

.slider-text {
display: block;
height: 100%;
transform: translateY(0%);
}

.slider-ani {
transform: translateY(-100%);
transition: transform 1s ease;
}
.slider-hide {
opacity: 0;
}


实现多个滚轮的向上滚动的数字组件


组件.gif


利用H5的requestAnimationFrame()API实现数字逐步递增的动画效果


实现一个数字的逐渐递增的滚动动画,并且要在指定时间内完成。要看到流畅的动画效果,就需要在更新元素状态时以一定的频率进行,JS动画都是通过在很短的时间内不停的渲染/绘制元素做到的,所以计时器一直都是Javascript动画的核心技术,关键就是刷新的间隔时间,刷新时间需要尽量短,这样动画效果才能显得更加流畅,不卡顿;同时刷新间隔又不能太短,需要确保浏览器有能力渲染动画


大多数电脑显示器的刷新频率是 60Hz,即每秒重绘 60次。因此平滑动画的最佳循环间隔是通常是 1000ms/60,约等于16.6ms


计时器对比




  • 与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要程序员自己设置时间间隔。setTimeout 和 setInterval 的问题是精确度低。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。




  • requestAnimationFrame 采用系统时间间隔,它会要求浏览器根据自己的频率进行一次重绘,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。




  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。




  • requestAnimationFrame 对于隐藏或不可见元素,将不会进行重绘或回流,就意味着使用更少的 CPU、GPU 和内存使用量。




  • requestAnimationFrame 是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。




requestAnimationFrame实现滚动动画思路



  • 动画开始,记录开始动画的时间 startTimeRef.current


const startTimeRef = useRef(Date.now());
const [t, setT] = useState(Date.now());


  • 之后每一帧动画,记录从开始动画经过了多长时间,计算出当前帧的所到达的数字应该是多少,即currentValue


useEffect(() => {
const rafFunc = () => {
const now = Date.now();
const t = now - startTimeRef.current;
if (t >= period) {
setT(period);
} else {
setT(t);
requestAnimationFrame(rafFunc);
}
};
let raf;
if (autoScroll) {
raf = requestAnimationFrame(rafFunc);
startTimeRef.current = Date.now();
} else {
raf && cancelAnimationFrame(raf);
}
return () => raf && cancelAnimationFrame(raf);
}, [period, autoScroll]);

const currentValue = useMemo(() => ((to - from) / period) * t + from, [t, period, from, to]);


  • 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画


成果展示


成果.gif


成果2.gif


作者:我就是胖虎
链接:https://juejin.cn/post/7025913017627836452

收起阅读 »

前端金额格式化处理

前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题 以此为例:12341234.246 => ¥ 12,341,234.25 方式一...
继续阅读 »

前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题



以此为例:12341234.246 => ¥ 12,341,234.25


方式一:采用浏览器自带的Number.prototype.toLocaleString()处理整数部分,小数部分直接用Number.prototype.toFixed()四舍五入处理


// v1.0
const formatMoney = (money, symbol = "", decimals = 2) => {
if (!(money && money > 0)) {
return 0.0;
}

let arr = money.toFixed(decimals).toString().split(".");
let first = parseInt(arr[0]).toLocaleString();
let result = [first, arr[1]].join(".");
return `${symbol} ${money.toFixed(decimals)}`;
};

formatMoney(12341234.246); // 12,341,234.25
formatMoney(12341234.246, "¥", 1); // ¥ 12,341,234.2

2021.11.9 更改记录 我之前写复杂了,经过评论区[黄景圣]的指点,优化如下:


// v2.0 简化函数
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol} ${parseFloat(money.toFixed(decimals)).toLocaleString()}`;

formatMoney(12341234.246, "¥", 2) // ¥ 12,341,234.25

// 或者只用toLocaleString()处理
const format = (money, decimals = 2) =>
money.toLocaleString("zh", {
style: "currency",
currency: "CNY",
maximumFractionDigits: decimals,
useGrouping: true, // false-没有千位分隔符;true-有千位分隔符
});
format(12341234.246); // ¥12,341,234.25

2021.11.10 更改记录 经过评论区[你摸摸我这料子]的提示,解决了 toFixed() 精度失效的问题,具体可查看前端小数展示精度处理


// 测试数据如下:
formatMoney(12.035); // 12.04 正常四舍五入
formatMoney(12.045); // 12.04 异常,应该为12.05,没有四舍五入

// v3.0 解决toFixed()问题
const formatToFixed = (money, decimals = 2) => {
return (
Math.round((parseFloat(money) + Number.EPSILON) * Math.pow(10, decimals)) /
Math.pow(10, decimals)
).toFixed(decimals);
};
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString()}`;

formatMoney(12341234.035, '¥'); // ¥12,341,234.04
formatMoney(12341234.045, '¥'); // ¥12,341,234.05

2021.11.17 更改记录 通过评论区[Ryan_zhang]的提醒,解决了保留四位小数显示的问题


// v4.0 只更改了formatMoney函数,其他的不变
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString(
"zh",
{
maximumFractionDigits: decimals,
useGrouping: true,
}
)}`;
formatMoney(12341234.12335, "¥", 4); // ¥12,341,234.1234
formatMoney(12341234.12345, "¥", 4); // ¥12,341,234.1235

方式二:使用正则表达式处理整数部分,小数部分同上所示。有个《JS 正则迷你书》介绍正则表达式挺好的,在 2.4.2 章就讲了“数字的千位分隔符表示法”,介绍的很详细,推荐看看。



  • \b:单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置

  • \B :\b 的反面的意思,非单词边界

  • (?=p):其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p


/**
* @params {Number} money 金额
* @params {Number} decimals 保留小数点后位数
* @params {String} symbol 前置符号
*/
const formatMoney = (money, symbol = "", decimals = 2) => {
let result = money
.toFixed(decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);
return result;
};

formatMoney(12341234.246, "$", 2); // $12,341,234.25

// v2.0 解决toFixed()问题
const formatMoneyNew = (money, symbol = "", decimals = 2) =>
formatToFixed(money, decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);

formatMoneyNew(12341234.035, "¥", 2); // ¥12,341,234.04
formatMoneyNew(12341234.045, "¥", 2); // ¥12,341,234.05

方式三:循环字符串,通过 slice 截取实现



  • substring(start, end):包含 start,不包含 end

  • substr(start, length):包含 start,长度为 length

  • slice(start, end):可操作数组和字符串;包含 start,不包含 end

  • splice(start, length, items):只能针对数组;增删改都可以


const formatMoney = (money, symbol = "", decimals = 2) => {
// 改造前
// let arr = money.toFixed(decimals).toString().split(".");
// 改造后
let arr = formatToFixed(money, decimals).toString().split(".");
let num = arr[0];
let first = "";
su;
while (num.length > 3) {
first = "," + num.slice(-3) + first;
num = num.slice(0, num.length - 3);
}
if (num) {
first = num + first;
}
return `${symbol} ${[first, arr[1]].join(".")}`;
};

formatMoney(12341234.246, "$", 2); // $ 12,341,234.25
formatMoney(12341234.035, "¥", 2); // ¥ 12,341,234.04
formatMoney(12341234.045, "¥", 2); // ¥ 12,341,234.05

2021.11.24 更改记录 通过评论区[SpriteBoy]和[maxxx]的提醒,采用Intl内置的NumberFormat试试


方式四:Intl.NumberFormat,用法和toLocaleString()挺相似的


const formatMoney = (money, decimals = 2) => {
return new Intl.NumberFormat("zh-CN", {
style: "currency", // 货币形式
currency: "CNY", // "CNY"是人民币
currencyDisplay: "symbol", // 默认“symbol”,中文中代表“¥”符号
// useGrouping: true, // 是否使用分组分隔符,如千位分隔符或千/万/亿分隔符,默认为true
// minimumIntegerDigits: 1, // 使用的整数数字的最小数目.可能的值是从1到21,默认值是1
// minimumFractionDigits: 2, // 使用的小数位数的最小数目.可能的值是从 0 到 20
maximumFractionDigits: decimals, // 使用的小数位数的最大数目。可能的值是从 0 到 20
}).format(money);
};

console.log(formatMoney(12341234.2, 2)); // ¥12,341,234.20
console.log(formatMoney(12341234.246, 1)); // ¥12,341,234.2
console.log(formatMoney(12341234.035, 2)); // ¥12,341,234.04
console.log(formatMoney(12341234.045, 2)); // ¥12,341,234.05
console.log(formatMoney(12341234.12335, 4)); // ¥12,341,234.1234
console.log(formatMoney(12341234.12345, 4)); // ¥12,341,234.1235

作者:时光足迹
链接:https://juejin.cn/post/7028086399601475591

收起阅读 »

清空数组的几个方式

1. 前言 前两天在工作当中遇到一个问题,在vue3中使用reactive生成的响应式数组如何清空,当然我一般清空都是这么写: let array = [1,2,3]; array = []; 不过这么用在reactive代理的方式中还是有点问题,比如...
继续阅读 »

1. 前言


前两天在工作当中遇到一个问题,在vue3中使用reactive生成的响应式数组如何清空,当然我一般清空都是这么写:


  let array = [1,2,3];
array = [];

不过这么用在reactive代理的方式中还是有点问题,比如这样:


    let array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
},)
array = reactive([]);

很显然,因为丢失了对原来响应式对象的引用,这样就直接失去了监听


2. 清空数据的几种方式


当然,作为一名十年代码经验常年摸鱼的我,立马就给出了几个解决方案。


2.1 使用ref()


使用ref,这是最简便的方法:


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},)

array.value = [];

image.png


2.2 使用slice


slice顾名思义,就是对数组进行切片,然后返回一个新数组,感觉和go语言的切片有点类似。当然用过react的小伙伴应该经常用slice,清空一个数组只需要这样写:


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},)

array.value = array.value.slice(0,0);

image.png
不过需要注意要使用ref


2.3 length赋值为0


个人比较喜欢这种,直接将length赋值为0


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},{
deep:true
})

array.value.length = 0;

而且,这种只会触发一次,但是需要注意watch要开启deep:


image.png


不过,这种方式,使用reactive会更加方便,也不用开启deep:


    const array = reactive([1,2,3]);

watch(()=>[...array],()=>{
console.log(array);
})

array.length = 0;

image.png


2.4 使用splice


副作用函数splice也是一种方案,这种情况同时也可以使用reactive:


    const array = reactive([1,2,3]);

watch(()=>[...array],()=>{
console.log(array);
},)

array.splice(0,array.length)

不过要注意,watch会触发多次:


1636352459(1).jpg


当然也可以使用ref,但是注意这种情况下,需要开启deep:


    const array = ref([1,2,3]);

watch(array,()=>{
console.log(array.value);
},{
deep:true
})

array.value.splice(0,array.value.length)

image.png


但是可以看到ref也和reactive一样,会触发多次。


3. 总结


以上是我个人工作中的对于清空数组的总结,但是可以看到splice还是有点特殊的,会触发多次,不过为什么会产生这种差异还有待研究。


v2-db16a663d4445bb2044d2635ab81f2a2_720w.jpg


作者:RadiumAg
链接:https://juejin.cn/post/7028086044285206564

收起阅读 »

Android | 彻底理解 View 的坐标

Android | 彻底理解 View 的坐标前言如果你是一位从事 Android 原生开发的工程师,那么肯定会对 View 的各种坐标感到迷惑,不理解他们的真正含义。因为曾经我也和你们一样,面对他们时感到陌生和害怕。现在我将这些知识点整理成文,希望可以给大家...
继续阅读 »

Android | 彻底理解 View 的坐标

前言

如果你是一位从事 Android 原生开发的工程师,那么肯定会对 View 的各种坐标感到迷惑,不理解他们的真正含义。因为曾经我也和你们一样,面对他们时感到陌生和害怕。现在我将这些知识点整理成文,希望可以给大家一些帮助。

View 的坐标分为四大类:位置坐标,内容滚动坐标,平移坐标,触摸坐标。 通过阅读本文,读者能够在理解各种 View 坐标的基础上,今后在面对动画和触摸事件的处理会更加的游刃有余。

预备知识

如果你对以下知识有过了解,阅读本文将会很轻松。

  1. 了解 View 的属性动画;View 触摸事件的分发;View 的测量、布局过程
  2. 了解 Kotlin 基础语法

环境

文中所有的代码和运行截图,基于以下开发环境。
Android Studio 4.1.1
Kotlin 1.4.20
程序运行系统 Android 5.1

View 的位置坐标

View 的位置坐标是我们日常开发中最常见的一类坐标,分别是左、上、右、下,获取他们的值也很简单。

getLeft()
getTop()
getRight()
getBottom()

如上所示,通过 View 的上面四个方法,就可以获取到 View 的位置坐标了。需要注意以下三点。

1. (left,top) 代表 View 的左上角坐标,(right,bottom) 代表 View 的右下角坐标
2. 位置坐标是一种相对于父容器的坐标,即坐标系原点是父容器的左上角
3. 位置坐标不会因为 View 的内容滚动、View 的平移而改变,他们在 View 的测量、布局过程结束后就确定了

View 的内容滚动坐标

通过 View 的下面两个方法,可以得到 View 内容滚动后的左上角坐标。

getScrollX()
getScrollY()

需要注意 View 的内容滚动坐标是相对于 View 自身的坐标,即坐标系原点是 View 自身的左上角,并不是父容器。 如下伪代码和运行截图所示,绿色区域是一个 TextView ,当我们点击绿色区域的时候,TextView 的内容会向右滚动 100px的距离,根据运行后的截图,可以得出如下结论。

1. View 的位置坐标并不会因为 View 的内容滚动后而发生改变,这在上面已经说明过
2. 当一个 View 的内容从左向右滚动时,getScrollX() 是负值,同理当一个 View 的内容从上往下滚动时,getScrollY()也是负值。反之,从右向左,从下往上就是正值

viewBinding.tvScroll.scrollTo(-100,0)

初始坐标.png

向右滚动100px.png

View 的平移坐标

读者在实际开发中,或多或少都接触过 View 的属性动画,大概是平移、旋转、缩放三种。而平移的运用场景在 Android 中实在是太多了,基本你能看到的 View 滑动效果,都是通过属性动画的平移来实现的。通过下面两个方法,可以得到 View 左上角的平移坐标。需要注意 View 的平移坐标同样是相对于 View 自身的坐标,即坐标系原点是 View 自身的左上角,并不是父容器

getTranslationX()
getTranslationY()

当然在你需要的时候,通过 View 的下面两个方法,仍然可以获得 View 相对于父容器的平移坐标。

getX()
getY()

他们两者之间的数学关系如下

getX() = getLeft() + getTranslationX()
getY() = getTop() + getTranslationY()

如下图伪代码和运行截图所示,绿色区域仍然是一个 TextView,当我们点击绿色区域的时候,使用属性动画,让 TextView 向右平移 100px,根据运行后的截图,可以得出如下结论。

1. View 的位置坐标不会随 View的平移而改变
2. 和 View 的内容滚动不一样,View 的平移是整个 View 都向右平移
3. 向右平移 getTranslationX() 是正值,同理向下平移 getTranslationY()也是正值。反之就是负值

val translationXAnim = ObjectAnimator.ofFloat(viewBinding.tvScroll,"translationX",0f,100f).setDuration(2*1000)
translationXAnim.start()

初始坐标.png

向右平移100px.png

这里读者思考一个问题,如果让你实现一个 View 的滑动效果时,选择内容滚动还是属性动画平移? 很显然,平移相对内容滚动有诸多优点,首先是平移坐标的正负值符合人们的直观感受,其次平移是整个 View 的平移,实际应用场景更多,没有明显的缺点。

View 的触摸坐标

View 的触摸坐标和触摸事件是相关联的,获取触摸坐标有如下两组方法。

第一组,获取 View 触摸点相对于 View 自身左上角的坐标

eventX = event.getX()
eventY = event.getY()

第二组,获取 View 触摸点相对于设备屏幕左上角的坐标

rawX = event.getRawX()
rawY = event.getRawY()

注意上面伪代码中 event 的类型是 MotionEvent,读者可以通过调用 View 的 setOnTouchListener 方法,或重写 View 的 onTouchEvent 方法,来使用这个对象。

如下运行截图所示,绿色区域仍然是一个 TextView,不过这里为它设置了 50px 的左边距和 50px 的上边距。点击绿色区域的任意一处,你都会看到,rawX 和 eventX 始终相差 50px,rawY 和 eventY 始终相差 75px。 根据上面的分析 rawY 和 eventY 也应该相差 50px 才对?其实多出来的 25px 是屏幕状态栏的高度,这证实了上面的结论。

1. event.getX() 和 event.getY() 是 View 触摸点相对于 View 自身左上角的坐标
2. event.getRawX() 和 event.getRawY() 是 View 触摸点相对于设备屏幕左上角的坐标

触摸坐标.png

通过下图,读者或许能够更好理解。

触摸坐标

写在最后

本文是对 View 坐标的一次实践与总结,希望本文能够给读者一点帮助。

原文链接:https://juejin.cn/post/7037320714935861284?utm_source=gold_browser_extension

收起阅读 »

Android asm加注解实现自动Log打印

Android asm加注解实现自动Log打印前言在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时...
继续阅读 »

Android asm加注解实现自动Log打印

前言

在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时候也需要获取对象里面的变量值等。

hanno

_    _
| | | |
| |__| | __ _ _ __ _ __ ___
| __ |/ _` | '_ \| '_ \ / _ \
| | | | (_| | | | | | | | (_) |
|_| |_|\__,_|_| |_|_| |_|\___/
复制代码

通过字节码插件实现注解打印log,注解可以加在类上面,也可以加在方法上面,当加在类上面时会打印全部方法的log,当加在方法上面时打印当前方法的log

使用方法

1、类中全部方法打印log

@HannoLog
class MainActivity : AppCompatActivity() {
// ...
}
复制代码

只要在类上面加上@HannoLog注解就可以在编译的时候给这个类中所有的方法插入log,运行时输出log。

2、给类中的某些方法加log

class MainActivity : AppCompatActivity() {
@HannoLog(level = Log.INFO, enableTime = false,watchField=true)
private fun test(a: Int = 3, b: String = "good"): Int {
return a + 1
}
}
复制代码

通过在方法上面添加注解可以在当前方法中插入log。 3、打印的log

//D/MainActivity: ┌───────────────────────────────────------───────────────────────────────────------
//D/MainActivity: │ method: onCreate(android.os.Bundle)
//D/MainActivity: │ params: [{name='savedInstanceState', value=null}]
//D/MainActivity: │ time: 22ms
//D/MainActivity: │ fields: {name='a', value=3}{name='b', value=false}{name='c', value=ccc}
//D/MainActivity: │ thread: main
//D/MainActivity: └───────────────────────────────────------───────────────────────────────────------
复制代码

其中method是当前方法名,params是方法的参数名和值,time方法的执行时间,fields是当前对象的fields值,thread当前方法执行的线程。

HannoLog参数解释

可以通过level来设置log的级别,level的设置可以调用Log里面的INFO,DEBUG,ERROR等。enableTime用来设置是否打印方法执行的时间,默认是false,如果要打印设置enableTime=true. tagName用于设置log的名称,默认是当前类名,也可以通过这个方法进行设置。

1、level控制log打印的等级,默认是log.d,可以通过@HannoLog(level = Log.INFO)来设置等级,支持Log.DEBUG,Log.ERROR等。

2、enableTime控制是否输出方法的执行时间,默认是false,如果要打印可以通过@HannoLog(enableTime=true)来设置。

3、tagName设置tag的名称,默认是当前类名,也可以通过 @HannoLog(tagName = "test")来设置。

4、watchField用于观察对象中的field值,通过@HannoLog(watchField = true)设置,由于静态方法中不能调用非静态的field所以这个参数在静态方法上统一不生效。

重要的类

1、HannoLog HannoLog是注解类,里面提供了控制参数。对应上面的HannoLog参数解释

/**
*
*
*
* create by 胡汉君
* date 2021/11/10 17:38
* 定义一个注解,用于标注当前方法需要打印log
*/

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface HannoLog {
//定义一下log的级别,默认是3,debug级别
int level() default Log.DEBUG;
/**
* @return 打印方法的运行时间
*/

boolean enableTime() default false;

/**
* @return tag的名称,默认是类名,也可以设置
*/

String tagName() default "";

/**
* @return 是否观察field的值,如果观察就会就拿到对象里面全部的field值
*/

boolean watchField() default false;
}
复制代码

2、HannoExtension

public class HannoExtension {
//控制是否使用Hanno
boolean enable;
//控制是否打印log
boolean openLog = true;

public boolean isEnableModule() {
return enableModule;
}

public void setEnableModule(boolean enableModule) {
this.enableModule = enableModule;
}

//设置这个值为true可以给整个module的方法增加log
boolean enableModule = false;

public boolean isEnable() {
return enable;
}

public boolean isOpenLog() {
return openLog;
}

public void setOpenLog(boolean openLog) {
this.openLog = openLog;
}

public void setEnable(boolean enable) {
this.enable = enable;
}
}
复制代码

HannoExtension提供gradle.build文件是否开启plugin 和打印执行plugin的log 默认情况下添加HannoLog之后会进行asm插装,也可以通过在module的build.gradle文件中添加以下配置使在编译时不执行字节码插装提高编译速度

apply plugin: 'com.hanking.hanno'
hannoExtension{
enable=false
openLog=false
}
复制代码

实现原理

hanno是通过asm字节码插桩方式来实现的。Android项目的编译过程如下图: 在这里插入图片描述 java编译器会将.java类编译生成.class类,asm可以用来修改.class类,通过对.class类的修改就可以达到往已有的类中加入代码的目的。一个.java文件经过Java编译器(javac)编译之后会生成一个.class文件。 在.class文件中,存储的是字节码(ByteCode)数据,如下图所示。 在这里插入图片描述 ASM所的操作对象是是字节码(ByteCode)的类库。ASM处理字节码(ByteCode)数据的流程是这样的:

第一步,将.class文件拆分成多个部分;

第二步,对某一个部分的信息进行修改;

第三步,将多个部分重新组织成一个新的.class文件。

ClassFile

.class文件中,存储的是ByteCode数据。但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构。

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码

字节码的类库和ClassFile之间关系 在这里插入图片描述

asm的组成

从组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API。

  • 其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;
  • 其中,Tree API包括asm-tree.jar和asm-analysis.jar。

在这里插入图片描述

asm中重要的类

  • ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。
  • ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。
  • ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。

在这里插入图片描述

.class文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件
复制代码

ClassVisitor类

ClassVisitor是一个抽象类,实现类有ClassWriter类(Core API)和ClassNode类(Tree API)。

public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
}
复制代码
  • api字段:int类型的数据,指出了当前使用的ASM API版本。
  • cv字段:ClassVisitor类型的数据,它的作用是将多个ClassVisitor串连起来

在这里插入图片描述

classVisitor的方法

visit()、visitField()、visitMethod()和visitEnd()。

visitXxx()方法与ClassFile ClassVisitor的visitXxx()方法与ClassFile之间存在对应关系。在ClassVisitor中定义的visitXxx()方法,并不是凭空产生的,这些方法存在的目的就是为了生成一个合法的.class文件,而这个.class文件要符合ClassFile的结构,所以这些visitXxx()方法与ClassFile的结构密切相关。 1、visit()方法 用于生成类或者接口的定义,如下生成一个为printField的类,因为如果类默认继承的父类是Object类,所以superName是” java/lang/Object “。

cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/hank/test/PrintField", null, "java/lang/Object", null);
复制代码

2、visitField()方法 对应classFile中的field_info,用于生成对象里面的属性值。通过visitField生成一个属性,如下:

FieldVisitor fv;
{
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "a", "I", null, new Integer(2));
fv.visitEnd();
}
复制代码

3、visitMethod()方法 用于生成一个方法,对应classFile中的method_info

ClassWriter类

ClassWriter的父类是ClassVisitor,因此ClassWriter类继承了visit()、visitField()、visitMethod()和visitEnd()等方法。 toByteArray方法 在ClassWriter类当中,提供了一个toByteArray()方法。这个方法的作用是将对visitXxx()的调用转换成byte[],而这些byte[]的内容就遵循ClassFile结构。 在toByteArray()方法的代码当中,通过三个步骤来得到byte[]:

  • 第一步,计算size大小。这个size就是表示byte[]的最终的长度是多少。
  • 第二步,将数据填充到byte[]当中。
  • 第三步,将byte[]数据返回。

3、使用ClassWriter类 使用ClassWriter生成一个Class文件,可以大致分成三个步骤:

  • 第一步,创建ClassWriter对象。
  • 第二步,调用ClassWriter对象的visitXxx()方法。
  • 第三步,调用ClassWriter对象的toByteArray()方法。
import org.objectweb.asm.ClassWriter;

import static org.objectweb.asm.Opcodes.*;

public class GenerateCore {
public static byte[] dump () throws Exception {
// (1) 创建ClassWriter对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用visitXxx()方法
cw.visit();
cw.visitField();
cw.visitMethod();
cw.visitEnd(); // 注意,最后要调用visitEnd()方法

// (3) 调用toByteArray()方法
byte[] bytes = cw.toByteArray();
return bytes;
}
}
复制代码

Hanno源码分析

上面已经先回顾一下asm相关的基础知识,下面对hanno源码进行分析。主要针对三个方面:

1、如何在方法中插入Log语句。

2、如何获取对象中的field值。

3、如何获取到方法的参数


原文链接:https://juejin.cn/post/7037369790100406309?utm_source=gold_browser_extension

收起阅读 »

Android - 依赖统一管理

#前言 前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结 目前Android依赖统一管理的方式有以下几种方式,接下来我们一起慢慢分析一下各...
继续阅读 »

#前言


前段时间自己在搭建组件化框架时候遇到了多人协作 Moudle 版本依赖冲突以及重复导包和同一个包导入不同版本的情况,针对这个问题对依赖统一这块做了一次比较详细的学习和总结


目前Android依赖统一管理的方式有以下几种方式,接下来我们一起慢慢分析一下各种方式优缺点



  1. groovy ext扩展函数(也有称之为:"循环优化")

  2. kotlin+buildSrc

  3. composing builds

  4. catalog

  5. 自定义插件+includeBuild


Groovy ext扩展函数


这种方式可能是大家最开始或者说是比较常见的一种依赖配置方式:
iShot2021-12-02 15.17.09.png


示例代码


然后在项目根build.gradle(即root路径下)


apply from:"config.gradle"


引入的方式有两种一种是循环遍历:


iShot2021-12-03 10.26.55.png


iShot2021-12-03 10.32.12.png


总结:


优点:


1:后续添加依赖不需要改动build.gradle,直接在config.gradle


2:精简了build.gradle的长度


缺点:


1:不支持代码提醒


2:不支持点击跳转


3:多moudle 开发时,不同module的依赖需要ctrl+c/v 导致开发的效率降低


kotlin+buildSrc


buildSrc


The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc directory, which has to sit in the root project directory. buildSrc should be preferred over script plugins as it is easier to maintain, refactor and test the code.


这是来自gradle官方文档对buildSrc的解释:


当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑,与脚本插件相比,buildSrc 应该是首选,因为它更易于维护、重构和测试代码


通过上面的介绍,大家或许对buildsrc 有一定理解了,那么我们就看下他怎么和kotlin一起使用达到项目统一依赖管理的


iShot2021-12-03 13.44.50.png


如上图所示我们首先创建一个名为buildSrc的module,gradle 构建的时候会先检查工程中是否有buildSrc命名的目录然后会自动编译和测试这段代码并写入到构建脚本的类路径中,所以无需在setting.gradle 做任何配置有关buildSrc的配置信息


官方的配置信息


iShot2021-12-03 14.19.27.png


iShot2021-12-03 14.21.33.png


这是我的项目中配置信息


这种方式管理依赖优点和缺点如下:


优点:


1:但这种方式支持IDE,输入代码会有提示,会自动完成,所以非常推荐使用这种方式来管理项目中的依赖包


2:支持 AndroidStudio 单击跳转


缺点:


来自gradle文档


A change in buildSrc causes the whole project to become out-of-date. Thus, when making small incremental changes, the --no-rebuild command-line option is often helpful to get faster feedback. Remember to run a full build regularly or at least when you’re done, though.


更改buildSrc会导致整个项目过时。因此,在进行小的增量更改时,--no-rebuild命令行选项通常有助于获得更快的反馈。不过,请记住定期或至少在完成后运行完整构建。


从官网的解释我们可以得出结论:


buildSrc 是对全局的所有 module 的配置依赖更新会重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。


阅读到这里我们可能会思考那么有没有一种方式是在部分module 需要修改依赖版本的时候而不会重新构建整个项目的方式呢,探索极致是每一个研发人员毕生所追求的,那么***"includeBuild"***这种方式应运而生


composing builds


那么我们开始一步一步实现这种方式:


1:首先创建一个library 的module <对于使用kotlin 或者 java>就要看自己的比较中意哪种语言喽


iShot2021-12-03 14.46.44.png


2:就是在library 配置插件等信息


apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'

buildscript {
repositories {
// https://developer.aliyun.com/mvn/guide
//todo error:"Using insecure protocols with repositories, without explicit opt-in,"
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

dependencies {
// 因为使用的 Kotlin 需要需要添加 Kotlin 插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
}
}




repositories {
// 需要添加 jcenter 否则会提示找不到 gradlePlugin
repositories {
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}

}

dependencies {
implementation gradleApi()
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}


compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

gradlePlugin{
plugins {
version{
// 在 app 模块需要通过 id 引用这个插件
id = 'com.bimromatic.version.plugin'
// 实现这个插件的类的路径
implementationClass = 'com.bimromatic.plugin.VersionPlugin'
}
}
}
复制代码

3:在项目路径下建立一个在.gradle 配置的类名实现Plugin 这个接口


/*
* Copyright 2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.api;

/**
* <p>A <code>Plugin</code> represents an extension to Gradle. A plugin applies some configuration to a target object.
* Usually, this target object is a {@link org.gradle.api.Project}, but plugins can be applied to any type of
* objects.</p>
*
* @param <T> The type of object which this plugin can configure.
*/
public interface Plugin<T> {
/**
* Apply this plugin to the given target object.
*
* @param target The target object
*/
void apply(T target);
}
复制代码

4:在settings.gradle添加


iShot2021-12-03 15.01.56.png


5:在需要用的地方添加插件名


iShot2021-12-03 15.03.33.png


详细配置请移步我们的项目查看


因为时间的原因,这次项目管理依赖就讲到这里,后续会把google在孵化器期 Catalog统一配置依赖版本 讲解一下,然后我们再把各种依赖管理方式用在编辑器跑一下试试看看那种方式构建速度最快。


如果你们觉得写得不错的随手给我点个关注,后期会持续做移动端技术文章的分享,或者给我的github 点个start 后期会上传一些干货。


对了如果文章中有讲的什么不对的地方咱们评论区见,或者提上你们宝贵的issue

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

收起阅读 »

Android实战——RecyclerView条目曝光埋点

一、概要 100行代码实现recyclerview条目曝光埋点设计 二、设计思路 条目露出来一半以上视为该条目曝光。 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。 滚动状态...
继续阅读 »

一、概要


100行代码实现recyclerview条目曝光埋点设计


二、设计思路



  1. 条目露出来一半以上视为该条目曝光。

  2. 在rv滚动过程中或者数据变更回调OnGlobalLayoutListener时,将符合条件1的条目记录在曝光列表、上传埋点集合里。

  3. 滚动状态变更和OnGlobalLayoutListener回调时,且列表状态为idle状态,触发上报埋点。


三、容错性



  1. 滑动过快时,视为未曝光

  2. 数据变更时,重新检测曝光

  3. 曝光过的条目,不会重复曝光


四、接入影响



  1. 对业务代码零侵入

  2. 对列表滑动体验无影响


五、代码实现


import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import java.util.*

class RVItemExposureListener(
private val mRecyclerView: RecyclerView,
private val mExposureListener: IOnExposureListener?
) {
interface IOnExposureListener {
fun onExposure(position: Int)
fun onUpload(exposureList: List<Int>?): Boolean
}

private val mExposureList: MutableList<Int> = ArrayList()
private val mUploadList: MutableList<Int> = ArrayList()
private var mScrollState = 0

var isEnableExposure = true
private var mCheckChildViewExposure = true

private val mViewVisible = Rect()
fun checkChildExposeStatus() {
if (!isEnableExposure) {
return
}
val length = mRecyclerView.childCount
if (length != 0) {
var view: View?
for (i in 0 until length) {
view = mRecyclerView.getChildAt(i)
if (view != null) {
view.getLocalVisibleRect(mViewVisible)
if (mViewVisible.height() > view.height / 2 && mViewVisible.top < mRecyclerView.bottom) {
checkExposure(view)
}
}
}
}
}

private fun checkExposure(childView: View): Boolean {
val position = mRecyclerView.getChildAdapterPosition(childView)
if (position < 0 || mExposureList.contains(position)) {
return false
}
mExposureList.add(position)
mUploadList.add(position)
mExposureListener?.onExposure(position)
return true
}

private fun uploadList() {
if (mScrollState == RecyclerView.SCROLL_STATE_IDLE && mUploadList.size > 0 && mExposureListener != null) {
val success = mExposureListener.onUpload(mUploadList)
if (success) {
mUploadList.clear()
}
}
}

init {
mRecyclerView.viewTreeObserver.addOnGlobalLayoutListener {
if (mRecyclerView.childCount == 0 || !mCheckChildViewExposure) {
return@addOnGlobalLayoutListener
}
checkChildExposeStatus()
uploadList()
mCheckChildViewExposure = false
}
mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
super.onScrollStateChanged(recyclerView, newState)
mScrollState = newState
uploadList()
}

override fun onScrolled(
recyclerView: RecyclerView,
dx: Int,
dy: Int
) {
super.onScrolled(recyclerView, dx, dy)
if (!isEnableExposure) {
return
}

// 大于50视为滑动过快
if (mScrollState == RecyclerView.SCROLL_STATE_SETTLING && Math.abs(dy) > 50) {
return
}
checkChildExposeStatus()
}
})
}
}


六、使用


RVItemExposureListener(yourRecyclerView, object : RVItemExposureListener.IOnExposureListener {
override fun onExposure(position: Int) {
// 滑动过程中出现的条目
Log.d("exposure-curPosition:", position.toString())
}

override fun onUpload(exposureList: List<Int>?): Boolean {
Log.d("exposure-positionList", exposureList.toString())
// 上报成功后返回true
return true
}

})

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

使用 Python 程序实现摩斯密码翻译器

算法加密解密执行摩斯密码对照表输出:.--- ..- . .--- .. -. -....- .... .- .. -.-- --- -. --.JUEJIN-HAIYONG.. .-.. --- ...- . -.-- --- ..-I LOVE YOU作...
继续阅读 »

摩斯密码是一种将文本信息作为一系列通断的音调、灯光或咔嗒声传输的方法,无需特殊设备,熟记的小伙伴即可直接翻译。它以电报发明者Samuel F. B. Morse的名字命名。

算法

算法非常简单。英语中的每个字符都被一系列“点”和“破折号”代替,或者有时只是单数的“点”或“破折号”,反之亦然。

加密

  1. 在加密的情况下,我们一次一个地从单词中提取每个字符(如果不是空格),并将其与存储在我们选择的任何数据结构中的相应摩斯密码匹配(如果您使用 python 编码,字典可以变成在这种情况下非常有用)

  2. 将摩斯密码存储在一个变量中,该变量将包含我们编码的字符串,然后我们在包含结果的字符串中添加一个空格。

  3. 在用摩斯密码编码时,我们需要在每个字符之间添加 1 个空格,在每个单词之间添加 2 个连续空格。

  4. 如果字符是空格,则向包含结果的变量添加另一个空格。我们重复这个过程,直到我们遍历整个字符串

解密

  1. 在解密的情况下,我们首先在要解码的字符串末尾添加一个空格(这将在后面解释)。

  2. 现在我们继续从字符串中提取字符,直到我们没有任何空间。

  3. 一旦我们得到一个空格,我们就会在提取的字符序列(或我们的莫尔斯电码)中查找相应的英语字符,并将其添加到将存储结果的变量中。

  4. 请记住,跟踪空间是此解密过程中最重要的部分。一旦我们得到 2 个连续的空格,我们就会向包含解码字符串的变量添加另一个空格。

  5. 字符串末尾的最后一个空格将帮助我们识别莫尔斯电码字符的最后一个序列(因为空格充当提取字符并开始解码它们的检查)。

执行

Python 提供了一种称为字典的数据结构,它以键值对的形式存储信息,这对于实现诸如摩尔斯电码之类的密码非常方便。我们可以将摩斯密码表保存在字典中,其中 (键值对)=>(英文字符-莫尔斯电码) 。明文(英文字符)代替密钥,密文(摩斯密码)形成相应密钥的值。键的值可以从字典中访问,就像我们通过索引访问数组的值一样,反之亦然。

摩斯密码对照表

# 实现摩斯密码翻译器的 Python 程序

'''
VARIABLE KEY
'cipher' -> '存储英文字符串的摩斯翻译形式'
'decipher' -> '存储摩斯字符串的英文翻译形式'
'citext' -> '存储单个字符的摩斯密码'
'i' -> '计算摩斯字符之间的空格'
'message' -> '存储要编码或解码的字符串
'''

# 表示摩斯密码图的字典
MORSE_CODE_DICT = { 'A':'.-', 'B':'-...',
'C':'-.-.', 'D':'-..', 'E':'.',
'F':'..-.', 'G':'--.', 'H':'....',
'I':'..', 'J':'.---', 'K':'-.-',
'L':'.-..', 'M':'--', 'N':'-.',
'O':'---', 'P':'.--.', 'Q':'--.-',
'R':'.-.', 'S':'...', 'T':'-',
'U':'..-', 'V':'...-', 'W':'.--',
'X':'-..-', 'Y':'-.--', 'Z':'--..',
'1':'.----', '2':'..---', '3':'...--',
'4':'....-', '5':'.....', '6':'-....',
'7':'--...', '8':'---..', '9':'----.',
'0':'-----', ', ':'--..--', '.':'.-.-.-',
'?':'..--..', '/':'-..-.', '-':'-....-',
'(':'-.--.', ')':'-.--.-'}

# 根据摩斯密码图对字符串进行加密的函数
def encrypt(message):
cipher = ''
for letter in message:
if letter != ' ':

# 查字典并添加对应的摩斯密码
# 用空格分隔不同字符的摩斯密码
cipher += MORSE_CODE_DICT[letter] + ' '
else:
# 1个空格表示不同的字符
# 2表示不同的词
cipher += ' '

return cipher

# 将字符串从摩斯解密为英文的函数
def decrypt(message):

# 在末尾添加额外空间以访问最后一个摩斯密码
message += ' '

decipher = ''
citext = ''
for letter in message:

# 检查空间
if (letter != ' '):

# 计数器来跟踪空间
i = 0

# 在空格的情况下
citext += letter

# 在空间的情况下
else:
# 如果 i = 1 表示一个新字符
i += 1

# 如果 i = 2 表示一个新词
if i == 2 :

# 添加空格来分隔单词
decipher += ' '
else:

# 使用它们的值访问密钥(加密的反向)
decipher += list(MORSE_CODE_DICT.keys())[list(MORSE_CODE_DICT
.values()).index(citext)]
citext = ''

return decipher

# 硬编码驱动函数来运行程序
def main():
message = "JUEJIN-HAIYONG"
result = encrypt(message.upper())
print (result)

message = ".--- ..- . .--- .. -. -....- .... .- .. -.-- --- -. --."
result = decrypt(message)
print (result)

message = "I LOVE YOU"
result = encrypt(message.upper())
print (result)

message = ".. .-.. --- ...- . -.-- --- ..-"
result = decrypt(message)
print (result)

# 执行主函数
if __name__ == '__main__':
main()

输出:

.--- ..- . .--- .. -. -....- .... .- .. -.-- --- -. --.
JUEJIN-HAIYONG
.. .-.. --- ...- . -.-- --- ..-
I LOVE YOU

作者:海拥
来源:https://juejin.cn/post/6990223674758397960

收起阅读 »

手写一个 ts-node 来深入理解它的原理

当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。 有没有觉得很神奇,ts-node 怎么做到的直接跑...
继续阅读 »

当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。


有没有觉得很神奇,ts-node 怎么做到的直接跑 ts 代码的?


其实原理并不难,今天我们来实现一个 ts-node 吧。


相关基础

实现 ts-node 需要 3 方面的基础知识:



  • require hook
  • repl 模块、vm 模块
  • ts compiler api

我们先学下这些基础


require hook

Node.js 当 require 一个 js 模块的时候,内部会分别调用 Module.load、 Module._extensions[‘.js’],Module._compile 这三个方法,然后才是执行。


img

同理,ts 模块、json 模块等也是一样的流程,那么我们只需要修改 Module._extensions[扩展名] 的方法,就能达到 hook 的目的:


require.extensions['.ts'] = function(module, filename) {
// 修改代码
module._compile(修改后的代码, filename);
}

比如上面我们注册了 ts 的处理函数,这样当处理 ts 模块时就会调用这个方法,所以我们在这里面做编译就可以了,这就是 ts-node 能够直接执行 ts 的原理。


repl 模块

Node.js 提供了 repl 模块可以创建 Read、Evaluate、Print、Loop 的命令行交互环境,就是那种一问一答的方式。ts-node 也支持 repl 的模式,可以直接写 ts 代码然后执行,原理就是基于 repl 模块做的扩展。


repl 的 api 是这样的: 通过 start 方法来创建一个 repl 的交互,可以指定提示符 prompt,可以自己实现 eval 的处理逻辑:


const repl = require('repl');

const r = repl.start({
prompt: '- . - > ',
eval: myEval
});

function myEval(cmd, context, filename, callback) {
// 对输入的命令做处理
callback(null, 处理后的内容);
}

repl 的执行时有一个上下文的,在这里就是 r.context,我们在这个上下文里执行代码要使用 vm 模块:


const vm = require('vm');

const res = vm.runInContext(要执行的代码, r.context);

这两个模块结合,就可以实现一问一答的命令行交互,而且 ts 的编译也可以放在 eval 的时候做,这样就实现了直接执行 ts 代码。


ts compiler api

ts 的编译我们主要是使用 tsc 的命令行工具,但其实它同样也提供了编译的 api,叫做 ts compiler api。我们做工具的时候就需要直接调用 compiler api 来做编译。


转换 ts 代码为 js 代码的 api 是这个:


const { outputText } = ts.transpileModule(ts代码, {
compilerOptions: {
strict: false,
sourceMap: false,
// 其他编译选项
}
});

当然,ts 也提供了类型检查的 api,因为参数比较多,我们后面一篇文章再做展开,这里只了解 transpileModule 的 api 就够了。


了解了 require hook、repl 和 vm、ts compiler api 这三方面的知识之后,ts-node 的实现原理就呼之欲出了,接下来我们就来实现一下。


实现 ts-node

直接执行的模式

我们可以使用 ts-node + 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts'] 来实现的。


在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。


所以我们重写 Module._extensions['.ts'] 方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。


这样,我们就可以直接执行 ts 模块了,具体的模块路径是通过命令行参数执行的,可以用 process.argv 来取。


const path = require('path');
const ts = require('typescript');
const fs = require('fs');

const filePath = process.argv[2];

require.extensions['.ts'] = function(module, filename) {
const fileFullPath = path.resolve(__dirname, filename);
const content = fs.readFileSync(fileFullPath, 'utf-8');

const { outputText } = ts.transpileModule(content, {
compilerOptions: require('./tsconfig.json')
});

module._compile(outputText, filename);
}

require(filePath);

我们准备一个这样的 ts 文件 test.ts:


const a = 1;
const b = 2;

function add(a: number, b: number): number {
return a + b;
}

console.log(add(a, b));

然后用这个工具 hook.js 来跑:


img

可以看到,成功的执行了 ts,这就是 ts-node 的原理。


当然,细节的逻辑还有很多,但是最主要的原理就是 require hook + ts compiler api。


repl 模式

ts-node 支持启动一个 repl 的环境,交互式的输入 ts 代码然后执行,它的原理就是基于 Node.js 提供的 repl 模块做的扩展,在自定义的 eval 函数里面做了 ts 的编译,然后使用 vm.runInContext 的 api 在 repl 的上下文中执行 js 代码。


我们也启动一个 repl 的环境,设置提示符和自定义的 eval 实现。


const repl = require('repl');

const r = repl.start({
prompt: '- . - > ',
eval: myEval
});

function myEval(cmd, context, filename, callback) {

}

eval 的实现就是编译 ts 代码为 js,然后用 vm.runInContext 来执行编译后的 js 代码,执行的 context 指定为 repl 的 context:


function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}

同时,我们还可以对 repl 的 context 做一些扩展,比如注入一个 who 的环境变量:


Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});

我们来测试下效果:


img

可以看到,执行后启动了一个 repl 环境,提示符修改成了 -.- >,可以直接执行 ts 代码,还可以访问全局变量 who。


这就是 ts-node 的 repl 模式的大概原理: repl + vm + ts compiler api。


全部代码如下:


const repl = require('repl');
const ts = require('typescript');
const vm = require('vm');

const r = repl.start({
prompt: '- . - > ',
eval: myEval
});

Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});

function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}

总结

ts-node 可以直接执行 ts 代码,不需要手动编译,为了深入理解它,我们我们实现了一个简易 ts-node,支持了直接执行和 repl 模式。


直接执行的原理是通过 require hook,也就是 Module._extensions[ext] 里通过 ts compiler api 对代码做转换,之后再执行,这样的效果就是可以直接执行 ts 代码。


repl 的原理是基于 Node.js 的 repl 模块做的扩展,可以定制提示符、上下文、eval 逻辑等,我们在 eval 里用 ts compiler api 做了编译,然后通过 vm.runInContext 在 repl 的 context 中执行编译后的 js。这样的效果就是可以在 repl 里直接执行 ts 代码。


当然,完整的 ts-node 还有很多细节,但是大概的原理我们已经懂了,而且还学到了 require hook、repl 和 vm 模块、 ts compiler api 等知识。


题外话

其实 ts-node 的原理是应一个同学的要求写的,大家有想读的 nodejs 工具的源码也可以告诉我呀(可以加我微信),无偿提供源码带读 + 简易实现的服务,不过会做一些筛选。


img
作者:zxg_神说要有光
来源:https://juejin.cn/post/7036688014206042143

收起阅读 »

前端自动化部署:借助Gitlab CI/CD实现

🛫 前端自动化部署:借助Gitlab CI/CD实现🌏 概论传统的前端部署往往都要经历:本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段,这些都是机械重复的步骤。对于这一过程我们往往可以通过CI/...
继续阅读 »

🛫 前端自动化部署:借助Gitlab CI/CD实现

🌏 概论

传统的前端部署往往都要经历:本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段,这些都是机械重复的步骤。对于这一过程我们往往可以通过CI/CD方法进行优化。所谓CI/CD,即持续集成/持续部署,以上我们所说的步骤便可以看作是持续部署的一种形态,其更详细的解释大家可以自行了解。


JenkinsTravis CI这些都是可以完成持续部署的工具。除此之外,Gitlab CI/CD也能很好的完成这一需求。下面就来详细介绍下。


🌏 核心工具

GitLab Runner

GitLab Runner是配合GitLab CI/CD完成工作的核心程序,出于性能考虑,GitLab Runner应该与Gitlab部署在不同的服务器上(Gitlab在单独的仓库服务器上,GitLab Runner在部署web应用的服务器上)。GitLab Runner在与GitLab关联后,可以在服务器上完成诸如项目拉取、文件打包、资源复制等各种命令操作。


Git

web服务器上需要安装Git来进行远程仓库的获取工作。


Node

用于在web服务器上完成打包工作。


NPM or Yarn or pnpm

用于在web服务器上完成依赖下载等工作(用yarn,pnpm亦可)。


web服务器上的所需程序

🌏 流程

这里我自己用的是centOS环境:


1. 在web服务器上安装所需工具

(1)安装Node


# 下载node包
wget https://nodejs.org/dist/v16.13.0/node-v16.13.0-linux-x64.tar.xz

# 解压Node包
tar -xf node-v16.13.0-linux-x64.tar.xz

# 在配置文件(位置多在/etc/profile)末尾添加:
export PATH=$PATH:/root/node-v16.13.0-linux-x64/bin

# 刷新shell环境:
source /etc/profile

# 查看版本(输出版本号则安装成功):
node -v

#后续安装操作,都可通过-v或者--version来查看是否成功

npm已内置在node中,如要使用yarn或,则可通过npm进行全局安装,命令与我们本地环境下的使用命令是一样的:


npm i yarn -g
#or
npm i pnpm -g

(2)安装Git


# 利用yum安装git
yum -y install git

# 查看git版本
git --version

(3)安装Gitlab Runner


# 安装程序
wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64

# 等待下载完成后分配权限
chmod +x /usr/local/bin/gitlab-runner

# 创建runner用户
useradd --comment 'test' --create-home gitlab-runner --shell /bin/bash

# 安装程序
gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

# 启动程序
gitlab-runner start

# 安装完成后可使用gitlab-runner --version查看是否成功

2. 配置Runner及CI/CD

基本的安装操作完成后,就是最核心的阶段:Runner与CI/CD的配置。


(1)配置Gitlab Runner


首先打开待添加自动部署功能的gitlab仓库,在其中设置 > CI/CD > Runner中找到runner配置信息备用:


image.png

在web服务器中配置runner:


gitlab-runner register

>> Enter the GitLab instance URL (for example, https://gitlab.com/):
# 输入刚才获取到的gitlab仓库地址
>> Enter the registration token:
# 输入刚才获取到的token
>> Enter a description for the runner:
# 自定runner描述
>> Enter tags for the runner (comma-separated):
# 自定runner标签
>> Enter an executor: docker-ssh, docker+machine, docker-ssh+machine, docker, parallels, shell, ssh, virtualbox, kubernetes, custom:
# 选择执行器,此处我们输入shell

完整示例:image.png

(2)配置.gitlab-ci.yml


.gitlab-ci.yml文件是流水线执行的流程文件,Runner会据此完成规定的一系列流程。


我们在项目根目录中创建.gitlab-ci.yml文件,然后在其中编写内容:


# 阶段
stages:
- install
- build
- deploy

cache:
paths:
- node_modules/

# 安装依赖
install:
stage: install
# 此处的tags必须填入之前注册时自定的tag
tags:
- deploy
# 规定仅在package.json提交时才触发此阶段
only:
changes:
- package.json
# 执行脚本
script:
yarn

# 打包项目
build:
stage: build
tags:
- deploy
script:
- yarn build
# 将此阶段产物传递至下一阶段
artifacts:
paths:
- dist/

# 部署项目
deploy:
stage: deploy
tags:
- deploy
script:
# 清空网站根目录,目录请根据服务器实际情况填写
- rm -rf /www/wwwroot/stjerne/salary/*
# 复制打包后的文件至网站根目录,目录请根据服务器实际情况填写
- cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/stjerne/salary/

保存并推送至gitlab后即可自动开始构建部署。


构建中可在gitlab CI/CD面板查看构建进程:


image.png

待流水线JOB完成后可前往页面查看🛫🛫🛫🛫🛫


作者:星始流年
来源:https://juejin.cn/post/7037022688493338661

收起阅读 »

聊一聊线程池和Kotlin协程

目前很多开发组都用上协程来处理异步任务了,但是有的地方协程提供的原生API还是不足以应付,比方说一些SDK提供了传入Executor的接口(以便复用调用者的线程池来执行异步任务),这时候可以用JDK提供的线程池,或者封装一下协程也可以满足需求。 协程提供了Di...
继续阅读 »

目前很多开发组都用上协程来处理异步任务了,但是有的地方协程提供的原生API还是不足以应付,比方说一些SDK提供了传入Executor的接口(以便复用调用者的线程池来执行异步任务),这时候可以用JDK提供的线程池,或者封装一下协程也可以满足需求。


协程提供了Dispatchers.DefaultDispatchers.IO 分别用于 计算密集型 任务和 IO密集型 任务,类似于RxJava的 Schedulers.computation()Schedulers.io()

但两者有所差异,比如RxJava的 Schedulers.io() 不做并发限制,而 Dispatchers.io() 做了并发限制:



It defaults to the limit of 64 threads or the number of cores (whichever is larger)



考虑到当前移动设备的CPU核心数都不超过64,所以可以认为协程的 Dispatchers.IO 的最大并发为64。

Dispatchers.Default 的并发限制为:



By default, the maximal level of parallelism used by this dispatcher is equal to the number of CPU cores, but is at least two



考虑到目前Android设备核心数都在2个以上,所以可以认为 Dispatchers.Default 的最大并发为 CPU cores。

Dispatchers.DefaultDispatchers.IO 是共享协程自己的线程池的,二者可以复用线程。

不过目前这两个Dispatchers 并未完全满足项目中的需求,有时我们需要一些自定义的并发限制,其中最常见的是串行。


RxJava有Schedulers.single() ,但这个Schedulers.single()和AsyncTask的SERAIL_EXECOTOR一样,是全局串行,不同的任务处在同一个串行队列,会相互堵塞,因而可能会引发问题。


或许也是因为这个原因,kotlin协程没有定义“Dispatchers.Single"。

对于需要串行的场景,可以这样实现:


val coroutineContext: CoroutineContext =
Executors.newSingleThreadExecutor().asCoroutineDispatcher()

这样可以实现局部的串行,但和协程的线程池是相互独立的,不能复用线程。

线程池的好处:



  1. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  2. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。


然彼此独立创建线程池的话,会大打折扣。

如何既复用协程的线程池,又自主控制并发呢?

一个办法就是套队列来控制并发,然后还是任务还是执行在线程池之上。

AsyncTask 就是这样实现的:


private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}

protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}


用SerialExecutor的execute的任务会先进入队列,当mActive为空时从队列获取任务赋值给mActive然后通过线程池 THREAD_POOL_EXECUTOR执行。

当然AsyncTask 的SerialExecutor是全局唯一的,所以会有上面提到的各种任务相互堵塞的问题。可以通过创建不同是的SerialExecutor实例来达到各业务各自串行。


在Kotlin环境下,我们可以利用协程和Channel来实现:


fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
send(0)
CoroutineScope(Dispatchers.IO).launch {
block()
receive()
}
}
}

// 使用方法
private val serialChannel = Channel<Any>(1)
serialChannel.runBlock {
// do somthing
}


添加Log编写测试如下:


private val a = AtomicInteger(0)
private val b = AtomicInteger(0)
fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
Log.d("MyTag", "before send " + a.getAndIncrement() + getTime())
send(0)
Log.i("MyTag", "after send " + b.getAndIncrement() + getTime())
CoroutineScope(Dispatchers.Default).launch {
block()
receive()
}
}
}

private fun test() {
// 并发限制为1,串行执行任务
val channel = Channel<Any>(1)
val t1 = System.currentTimeMillis()
repeat(4) { x ->
channel.runBlock {
Thread.sleep(1000L)
Log.w("MyTag", "$x done job" + getTime())
}
}

CoroutineScope(Dispatchers.Default).launch {
while (!channel.isEmpty) {
delay(200)
}
val t2 = System.currentTimeMillis()
Log.d("MyTag", "Jobs all done, use time:" + (t2 - t1))
}
}


执行结果:



第一个任务可以顺利通过send(), 而随后的任务被suspend, 直到前面的任务执行完(执行block),调用recevie(), 然后下一个任务通过send() ……依此类推。

最终,消耗4s完成任务。


如果Channel的参数改成2,则能有两个任务可以通过send() :



最终,消耗2s完成任务。


关于参数可以参考Channel的构造函数:


public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =
when (capacity) {
RENDEZVOUS -> RendezvousChannel()
UNLIMITED -> LinkedListChannel()
CONFLATED -> ConflatedChannel()
BUFFERED -> ArrayChannel(CHANNEL_DEFAULT_CAPACITY)
else -> ArrayChannel(capacity)
}

在前面的实现中, 我们关注UNLIMITED, BUFFERED 以及 capacity > 0 的情况即可:



  • UNLIMITED: 不做限制;

  • BUFFERED: 并发数由 kotlin "kotlinx.coroutines.channels.defaultBuffer"决定,目前测试得到8;

  • capacity > 0, 则并发数由 capacity 决定;

  • 特别地,当capacity = 1,为串行调度。


不过,[Dispatchers.IO] 本身有并发限制(目前版本是64),

所有对于 Channel.UNLIMITED 和 capacity > 64 的情况,和capacity=64的情况是相同的。

我们可以为不同的业务创建不同的Channel实例,从而各自控制并发且最终在协程的线程池上执行任务。

简要示意图如下:



为了简化,我们假设Dispatchers的并发限制为4。



  • 不同Channel有各自的buffer, 当任务小于capacity时进入buffer, 大于capacity时新任务被suspend。

  • Dispatchers 不断地执行任务然后调用receive(), 上面的实现中,receive并非要取什么信息,仅仅是让channel空出buffer, 好让被suspend的任务可以通过send()然后进入Dispatchers的调度。

  • 极端情况下(进入Disptachers的任务大于并发限制时),任务进入Dispatchers也不会被立即执行,这个设定可以避免开启的线程太多而陷于线程上下文频繁切换的困境。


通过Channel可以实现并发的控制,但是日常开发中有的地方并不是简单地执行个任务,而是需要一个ExecutorService或者Executor。


为此,我们可以实现一个ExecutorService。

当然了,不是直接implement ExecutorService, 而是像ThreadPoolExecutor一样继承AbstractExecutorService, 这样只需要实现几个方法即可。



最终完整代码如下:


fun Channel<Any>.runBlock(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(Dispatchers.Unconfined).launch {
send(0)
CoroutineScope(Dispatchers.IO).launch {
block()
receive()
}
}
}

class ChannelExecutorService(capacity: Int) : AbstractExecutorService() {
private val channel = Channel<Any>(capacity)

override fun execute(command: Runnable) {
channel.runBlock {
command.run()
}
}

fun isEmpty(): Boolean {
return channel.isEmpty || channel.isClosedForReceive
}

override fun shutdown() {
channel.close()
}

override fun shutdownNow(): MutableList<Runnable> {
shutdown()
return mutableListOf()
}

@ExperimentalCoroutinesApi
override fun isShutdown(): Boolean {
return channel.isClosedForSend
}

@ExperimentalCoroutinesApi
override fun isTerminated(): Boolean {
return channel.isClosedForReceive
}

override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
var millis = unit.toMillis(timeout)
while (!isTerminated && millis > 0) {
try {
Thread.sleep(200L)
millis -= 200L
} catch (ignore: Exception) {
}
}
return isTerminated
}
}

需要简单地控制并发的地方,直接定义Channel然后调用runBlock即可;


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

Android 编译速度提升黑科技 - RocketX

怎么做编译优化,当时说了个方案,就是编译时将所有的模块依赖修改为 aar,然后每次编译将变动的模块改成源码依赖,同时编译完成再将修改模块上传为 aar,这样可以始终做到仅有最少的模块参与源码编译,从而提升编译速度。 当然说起来轻松,做起来没有那么容易,终于有位...
继续阅读 »

怎么做编译优化,当时说了个方案,就是编译时将所有的模块依赖修改为 aar,然后每次编译将变动的模块改成源码依赖,同时编译完成再将修改模块上传为 aar,这样可以始终做到仅有最少的模块参与源码编译,从而提升编译速度。


当然说起来轻松,做起来没有那么容易,终于有位小伙伴将上述描述开发成一个开源方案了,非常值得大家学习和借鉴。


1.背景描述


在项目体量越来越大的情况下,编译速度也随着增长,有时候一个修改需要等待长达好几分钟的编译时间。


基于这种普遍的情况,推出了 RocketX ,通过在编译流程动态修改项目依赖关系, 动态 替换 module 为 aar,做到只编译改动模块,其他模块不参与编译,无需改动原有项目任何代码,提高全量编译的速度。


2.效果展示


2.1、测试项目介绍

目标项目一共 3W+ 个类与资源文件,全量编译 4min 左右(测试使用 18 年 mbp 8代i7 16g)。


通过 RocketX 全量增速之后的效果(每一个操作取 3 次平均值)。


image.png


项目依赖关系如下图,app 依赖 bm 业务模块,bm 业务模块依赖顶层 base/comm 模块。


image.png


依赖关系


• 当 base/comm 模块改动,底部的所有模块都必须参与编译。因为 app/bmxxx 模块可能使用了 base 模块中的接口或变量等,并且不知道是否有改动到。(那么速度就非常慢)


• 当 bmDiscover 做了改动,只需要 app 模块和 bmDiscover 两个模块参与编译。(速度较快)


• rx(RocketX) 在无论哪一个模块的编译速度基本都是在控制在 30s 左右,因为只编译 app 和 改动的模块,其他模块是 aar 包不参与编译。


顶层模块速度提升 300%+


3.思路问题分析与模块搭建


3.1、思路问题分析

需要通过 gradle plugin 的形式动态修改没有改动过的 module 依赖为 相对应的 aar 依赖,如果 module 改动,退化成 project 工程依赖,这样每次只有改动的 module 和 app 两个模块编译。


需要把 implement/api moduleB,修改为implement/api aarB。


需要构建 local maven 存储未被修改的 module 对应的 aar。(也可以通过 flatDir 代替速度更快)


编译流程启动,需要找到哪一个 module 做了修改。


需要遍历每一个 module 的依赖关系进行置换, module 依赖怎么获取?一次性能获取到所有模块依赖,还是分模块各自回调?修改其中一个模块依赖关系会阻断后面模块依赖回调?


每一个 module 换变成 aar 之后,自身依赖的 child 依赖 (网络依赖,aar),给到 parent module (如何找到所有 parent module) ? 还是直接给 app module ? 有没有 app 到 module 依赖断掉的风险?这里需要出一个技术方案。


需要hook 编译流程,完成后置换 loacal maven 中被修改的 aar。


提供 AS 状态栏 button, 实现开启关闭功能,加速编译还是让开发者使用已经习惯性的三角形 run 按钮。


3.2、模块搭建

依照上面的分析,虽然问题很多,但是大致可以把整个项目分成以下几块:


image.png


4.问题解决与实现


4.1、implement 源码实现入口在 DynamicAddDependencyMethods 中的 tryInvokeMethod 方法。他是一个动态语言的 methodMissing 功能。

tryInvokeMethod 代码分析:


 public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {       //省略部分代码 ...       return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null)); }
复制代码

dependencyAdder 实现是一个 DirectDependencyAdder。


private class DirectDependencyAdder implements DependencyAdder<Dependency> {    private DirectDependencyAdder() {    }    public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) {        return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction);    }}
复制代码

最后是在 DefaultDependencyHandler.this.doAdd 进行添加进去,而 DefaultDependencyHandler 在 project可以获取。


  DependencyHandler getDependencies(); 
复制代码

通过以上的分析,添加相对应的 aar/jar 可以通过以下代码实现。


fun addAarDependencyToProject(aarName: String, configName: String, project: Project) {    //添加 aar 依赖 以下代码等同于 api/implementation/xxx (name: 'libaccount-2.0.0', ext: 'aar'),源码使用 linkedMap    if (!File(FileUtil.getLocalMavenCacheDir() + aarName + ".aar").exists()) return    val map = linkedMapOf<String, String>()    map.put("name", aarName)    map.put("ext", "aar")    // TODO: 2021/11/5 改变依赖 这里后面需要修改成    //project.dependencies.add(configName, "com.${project.name}:${project.name}:1.0")    project.dependencies.add(configName, map)}
复制代码

4.2、localMave 优先使用 flatDir 实现通过指定一个缓存目录把生成 aar/jar 包丢进去,依赖修改时候通过找寻进行替换。

fun flatDirs() {    val map = mutableMapOf<String, File>()    map.put("dirs", File(getLocalMavenCacheDir()))    appProject.rootProject.allprojects {        it.repositories.flatDir(map)    }}
复制代码

4.3、编译流程启动,需要找到哪一个 module做了修改。

使用遍历整个项目的文件的 lastModifyTime 去做实现。


以每一个 module 为一个粒度,递归遍历当前 module 的文件,把每个文件的 lastModifyTime 整合计算得出一个唯一标识 countTime。


通过 countTime 与上一次的作对比,相同说明没改动,不同则改动. 并需要同步计算后的 countTime 到本地缓存中。


整体 3W 个文件耗时 1.2s 可以接受。


4.4、 module 依赖关系获取。

通过以下代码可以找到生成整个项目的依赖关系图时机,并在此处生成依赖图解析器。


 project.gradle.addListener(DependencyResolutionListener listener)
复制代码

4.5、 module 依赖关系 project 替换成 aar 技术方案

每一个 module 依赖关系替换的遍历顺序是无序的,所以技术方案需要支持无序的替换。


目前使用的方案是:如果当前模块 A 未改动,需要把 A 通过 localMaven 置换成 A.aar,并把 A.aar 以及 A 的 child 依赖,给到第一层的 parent module 即可。(可能会质疑如果 parent module 也是 aar 怎么办,其实这块也是没有问题的,这里就不展开说了,篇幅太长)


为什么要给到 parent 不能直接给到 app ,下图一个简单的示例如果 B.aar 不给 A 模块的话,A 使用 B 模块的接口不见了,会导致编译不过。


image.png


给出整体项目替换的技术方案演示:


image.png


4.5、hook 编译流程,完成后置换 loacal maven 中被修改的 aar。

点击三角形 run,执行的命令是 app:assembleDebug , 需要在 assembleDebug 后面补一个 uploadLocalMavenTask, 通过 finalizedBy 把我们的 task 运行起来去同步修改后的 aar


4.6、提供 AS 状态栏 button,小火箭按钮一个喷火一个没有喷火,代表 enable/disable , 一个 扫把clean rockectx 的缓存。

image.png


5一天一个小惊喜


5.1、发现点击 run 按钮 ,执行的命令是 app:assembleDebug ,各个子 module 在 output 并没有打包出 aar。

解决:通过研究 gradle 源码发现打包是由 bundleFlavor{Flavor}{BuildType}Aar 这个task执行出来,那么只需要将各个模块对应的 task 找到并注入到 app:assembleDebug 之后运行即可。


5.2、发现运行起来后存在多个 jar 包重复问题。

解决:implementation fileTree(dir: "libs", include: ["*.jar"]) jar 依赖不能交到 parent module,jar 包会打进 aar 中的lib 可直接剔除。通过以下代码可以判断:


// 这里的依赖是以下两种: 无需添加在 parent ,因为 jar 包直接进入 自身的 aar 中的libs 文件夹//    implementation rootProject.files("libs/xxx.jar")//    implementation fileTree(dir: "libs", include: ["*.jar"])childDepency.files is DefaultConfigurableFileCollection || childDepency.files is DefaultConfigurableFileTree
复制代码

5.3、发现 aar/jar 存在多种依赖方式。

implementation (name: 'libXXX', ext: 'aar')


implementation files("libXXX.aar")


解决:使用第一种,第二种会合并进aar,导致类重复问题.


5.4、发现 aar 新姿势依赖。

configurations.maybeCreate("default")artifacts.add("default", file('lib-xx.aar'))
复制代码

上面代码把 aar 做了一个单独的 module 给到其他 module 依赖,default config 其实是 module 最终输出 aar 的持有者,default config 可以持有一个 列表的aar ,所以把 aar 手动添加到 default config,也相当于当前 module 打包出来的产物。


解决:通过 childProject.configurations.maybeCreate("default").artifacts 找到所有添加进来的 aar ,单独发布 localmaven。


5.5、发现 android module 打包出来可以是 jar。

解决:通过找到名字叫做 jar 的task,并且在 jar task 后面注入 uploadLocalMaven task。


5.6、发现 arouter 有 bug,transform 没有通过 outputProvider.deleteAll() 清理旧的缓存。

解决:详情查看 issue,结果arouter 问题是解决了,代码也是合并了。但并没有发布新的插件版本到 mavenCentral,于是先自行帮 arouter 解决一下。


github.com/alibaba/ARo…



关注我,每天分享知识干货!

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

Android CameraX结合LibYUV和GPUImage自定义相机滤镜

前言 之前使用Camera实现了一个自定义相机滤镜(Android自定义相机滤镜 ),但是运行起来有点卡顿,这次用Camerax来实现一样的效果发现很流畅,在此记录一下,也希望能帮到有需要的同学。 实现效果 实现步骤 1.引入依赖库 这里我引入的依赖库有Ca...
继续阅读 »

前言


之前使用Camera实现了一个自定义相机滤镜(Android自定义相机滤镜 ),但是运行起来有点卡顿,这次用Camerax来实现一样的效果发现很流畅,在此记录一下,也希望能帮到有需要的同学。


实现效果



实现步骤


1.引入依赖库

这里我引入的依赖库有CameraXGPUImage(滤镜库)、Utilcodex(一款好用的工具类)


// CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-camera2:1.0.1"
// CameraX Lifecycle Library
    implementation "androidx.camera:camera-lifecycle:1.0.1"
// CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha27"

    implementation'jp.co.cyberagent.android.gpuimage:gpuimage-library:1.4.1'
    implementation 'com.blankj:utilcodex:1.30.6'

2.引入libyuv

这里我用的是这个案例(github.com/theeasiestw…



3.编写CameraX预览代码

布局代码如下


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="0dp"
        android:layout_height="0dp" />
</FrameLayout>

Activity中开启相机预览代码如下,基本都是Google官方提供的案例代码


class MainActivity : AppCompatActivity() {
    private lateinit var cameraExecutor: ExecutorService
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        cameraExecutor = Executors.newSingleThreadExecutor()
        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(Runnable {
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(viewFinder.surfaceProvider)
                }
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    companion object {
        private const val TAG = "CameraXBasic"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

    }
}

到这里就可以实现相机预览了



4.增加相机数据回调

我们要增加滤镜效果就必须对相机的数据进行操作,这里我们通过获取相机数据回调来获取可修改的数据


val imageAnalyzer = ImageAnalysis.Builder()
                //设置回调数据的比例为16:9
                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor,this@MainActivity)
                }

这里我们还需要进行绑定



除此之外我们还需要在Activity中实现ImageAnalysis.Analyzer接口,数据的获取就在此接口的回调方法中获取,如下所示,其中ImageProxy就包含了图像数据


override fun analyze(image: ImageProxy) {

}

5.对回调数据进行处理

我们在相机数据回调的方法中对图像进行处理并添加滤镜,当然在此之前我们还需要创建GPUImage对象并设置滤镜类型


private var bitmap:Bitmap? = null
private var gpuImage:GPUImage? = null
//创建GPUImage对象并设置滤镜类型,这里我使用的是素描滤镜
private fun initFilter() {
        gpuImage = GPUImage(this)
        gpuImage!!.setFilter(GPUImageSketchFilter())
    }
@SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        //将Android的YUV数据转为libYuv的数据
        var yuvFrame = yuvUtils.convertToI420(image.image!!)
        //对图像进行旋转(由于回调的相机数据是横着的因此需要旋转90度)
        yuvFrame = yuvUtils.rotate(yuvFrame, 90)
        //根据图像大小创建Bitmap
        bitmap = Bitmap.createBitmap(yuvFrame.width, yuvFrame.height, Bitmap.Config.ARGB_8888)
        //将图像转为Argb格式的并填充到Bitmap上
        yuvUtils.yuv420ToArgb(yuvFrame,bitmap!!)
        //利用GpuImage给图像添加滤镜
        bitmap = gpuImage!!.getBitmapWithFilterApplied(bitmap)
        //由于这不是UI线程因此需要在UI线程更新UI
        img.post {
            img.setImageBitmap(bitmap)
            //关闭ImageProxy,才会回调下一次的数据
            image.close()
        }

    }

6.拍摄照片

这里我们加一个拍照的按钮


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <ImageView
        android:id="@+id/img"
        android:scaleType="centerCrop"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <Button
        android:id="@+id/bt_takepicture"
        android:layout_gravity="center_horizontal|bottom"
        android:layout_marginBottom="100dp"
        android:text="拍照"
        android:layout_width="70dp"
        android:layout_height="70dp"/>
</FrameLayout>

然后我们在Activity中添加拍照的逻辑,其实就是将Bitmap转为图片保存到SD卡,这里我们使用了之前引入的Utilcodex工具,当我们点击按钮的时候isTakePhoto 会变为true,然后在相机的回调中就会进行保存图片的处理


bt_takepicture.setOnClickListener {
            isTakePhoto = true
        }

并且我们加入变量控制,在拍照的时候不处理回调数据


@SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        if(!isTakePhoto){
            //将Android的YUV数据转为libYuv的数据
            var yuvFrame = yuvUtils.convertToI420(image.image!!)
            //对图像进行旋转(由于回调的相机数据是横着的因此需要旋转90度)
            yuvFrame = yuvUtils.rotate(yuvFrame, 90)
            //根据图像大小创建Bitmap
            bitmap = Bitmap.createBitmap(yuvFrame.width, yuvFrame.height, Bitmap.Config.ARGB_8888)
            //将图像转为Argb格式的并填充到Bitmap上
            yuvUtils.yuv420ToArgb(yuvFrame,bitmap!!)
            //利用GpuImage给图像添加滤镜
            bitmap = gpuImage!!.getBitmapWithFilterApplied(bitmap)
            //由于这不是UI线程因此需要在UI线程更新UI
            img.post {
                img.setImageBitmap(bitmap)
                if(isTakePhoto){
                    takePhoto()
                }
                //关闭ImageProxy,才会回调下一次的数据
                image.close()
            }
        }else{
            image.close()
        }
    }
 /**
     * 拍照
     */
    private fun takePhoto() {
        Thread{
            val filePath = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"${System.currentTimeMillis()}save.png")
            ImageUtils.save(bitmap,filePath.absolutePath,Bitmap.CompressFormat.PNG)
            ToastUtils.showShort("拍摄成功")
            isTakePhoto = false
        }.start()
    }

效果如下



保存的图片在如下目录



保存的图片如下



只有不断的学习进步,才能不被时代淘汰。关注我,每天分享知识干货!


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

探究Android属性动画执行过程

1.引言属性动画作为Android动画功能的一个重要组成部分,可以实现很多有趣的动画效果,理解属性动画的执行过程有助于我们更好地使用属性动画去实现需求。本文将从源码的角度去探索属性动画的实现过程,加深大家对其的认知和理解。2.属性动画相关的类2.1 Value...
继续阅读 »

1.引言

属性动画作为Android动画功能的一个重要组成部分,可以实现很多有趣的动画效果,理解属性动画的执行过程有助于我们更好地使用属性动画去实现需求。本文将从源码的角度去探索属性动画的实现过程,加深大家对其的认知和理解。

2.属性动画相关的类

2.1 ValueAnimator

这个类是实现属性动画的一个重要的类,通过ValueAnimator.ofFloat()、ValueAnimator.ofInt()、ValueAnimator.ofObject()、ValueAnimator.ofArgb()、ValueAnimator.ofPropertyValuesHolder()等方法可以获得ValueAnimator的对象,然后可以通过对这个对象的操作去实现动画。使用ValueAnimator实现属性动画,需要实现ValueAnimator.AnimatorUpdateListener()接口,并在onAnimationUpdate()方法内为要添加动画的对象设置属性值。

2.2 ObjectAnimator

ObjectAnimator是ValueAnimator的子类,可以操作目标对象的动画属性,这个类的构造函数支持采用参数的形式传入要使用动画的目标对象和属性名。

3.属性动画的实现过程

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(iv, "alpha", 1.0f, 0f);
objectAnimator.setDuration(3000);
objectAnimator.start();

这是一段简单的代码,它使用属性动画实现了一张图片的透明度渐变的效果,我们从这一段代码入手,去分析属性动画的实现过程。

3.1 创建属性动画

/**
* target:添加动画效果的目标对象
* propertyName:动画效果的属性名
* values:动画将会在这个时间之间执行的数值集合
*/
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values){
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
}

这个方法返回了一个属性动画对象,第一个参数是产生动画效果的目标对象,第二个参数是属性名,目标对象的属性名应该有与之对应的set()方法,例如我们传入属性名"alpha",那么这个目标对象也应该有setAlpha()方法。参数values传一个值的时候,这个值是动画的结束值,传两个数值的时候,第一个值是开始值,第二个值是结束值,多于两个值的时候,第一个值是开始值,最后一个值是结束值。

private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}

这个是属性动画的构造函数,里面执行了两个方法setTarget(target)和setPropertyName(propertyName)。

@Override
public void setTarget(@Nullable Object target) {
final Object oldTarget = getTarget();
if (oldTarget != target) {
if (isStarted()) {
cancel();
}
mTarget = target == null ? null : new WeakReference<Object>(target);
// New target should cause re-initialization prior to starting
mInitialized = false;
}
}
public void setPropertyName(@NonNull String propertyName) {
// mValues could be null if this is being constructed piecemeal. Just record the
// propertyName to be used later when setValues() is called if so.
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
mPropertyName = propertyName;
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}

mValues是一个PropertyValuesHolder数组,PropertyValuesHolder持有动画的属性名和属性值信息,mValuesMap是一个hashmap数组,用来管理PropertyValuesHolder对象,在调用getAnimatedValue(String)方法的时候,这个map通过属性名去查找动画执行的数值。当mValues不为空的时候,将属性名信息放入mValuesMap。

//ObjectAnimator
@Override
public void setFloatValues(float... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
super.setFloatValues(values);
}
}

mValues为null或者数组元素个数为0的时候,调用其父类ValueAnimator的setValues()方法,在setValues()内执行了初始化mValues和mValuesMap的操作,并将PropertyValuesHolder放入mValuesMap。当mValues不为null且元素个数不为0的时候,调用其父类ValueAnimator的setFloatValues()方法,在setFloatValues()方法内满足条件又会调用到PropertyValuesHolder的setFloatValues()方法。

//PropertyValuesHolder
public void setFloatValues(float... values) {
mValueType = float.class;
mKeyframes = KeyframeSet.ofFloat(values);
}

这里的mValueType指的是提供的值的类型,mKeyframes是定义这个动画的关键帧集合。

//KeyframeSet
public static KeyframeSet ofFloat(float... values) {
boolean badValue = false;
int numKeyframes = values.length;
FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
if (Float.isNaN(values[0])) {
badValue = true;
}
} else {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
if (Float.isNaN(values[i])) {
badValue = true;
}
}
}
if (badValue) {
Log.w("Animator", "Bad value (NaN) in float animator");
}
return new FloatKeyframeSet(keyframes);
}

在这个方法内新建了一个FloatKeyframe数组,数组的元素至少为2个,FloatKeyframe是Keyframe的内部子类,持有这个动画的时间值对,Keyframe类被ValueAnimator用来定义整个动画过程中动画目标的数值,当时间从一帧到另一帧,目标对象的值也会从上一帧的值运动到下一帧的值。

/**
* fraction:取值范围0到1之间,表示全部动画时长中已经执行的时间部分
* value:关键帧中与时间相对应的数值
*/
public static Keyframe ofFloat(float fraction, float value) {
return new FloatKeyframe(fraction, value);
}

此方法使用给定的时间和数值创建一个关键帧对象,到这里,属性动画的创建过程基本完成。

3.2 属性动画执行过程

//ObjectAnimator
@Override
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}

在代码中调用objectAnimator.start()的时候动画开始执行,内部调用了其父类ValueAnimator的start()方法。

//ValueAnimator
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
// Calculate the fraction of the current iteration.
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
addAnimationCallback(0);

if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
startAnimation();
if (mSeekFraction == -1) {
// No seek, start at play time 0. Note that the reason we are not using fraction 0
// is because for animations with 0 duration, we want to be consistent with pre-N
// behavior: skip to the final value immediately.
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}

在这个方法内进行了一些赋值操作,addAnimationCallback(0)和startAnimation()是比较重要的操作。

//ValueAnimator
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}

这个方法内执行了AnimationHandler的addAnimationFrameCallback()方法注册回调,我们继续看看addAnimationFrameCallback()方法。

//AnimationHandler
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}

if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}

这个方法添加了一个AnimationFrameCallback回调,AnimationFrameCallback是AnimationHandler的一个内部接口,其中有两个重要的方法doAnimationFrame()和commitAnimationFrame()。

//AnimationHandler
interface AnimationFrameCallback {
boolean doAnimationFrame(long frameTime);

void commitAnimationFrame(long frameTime);
}

AnimationFrameCallback是可以收到动画执行时间和帧提交时间通知的回调,内有两个方法,doAnimationFrame()和commitAnimationFrame()。

//AnimationHandler
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

final Choreographer mChoreographer = Choreographer.getInstance();

@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}

@Override
public void postCommitCallback(Runnable runnable) {
mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
}

@Override
public long getFrameTime() {
return mChoreographer.getFrameTime();
}

@Override
public long getFrameDelay() {
return Choreographer.getFrameDelay();
}

@Override
public void setFrameDelay(long delay) {
Choreographer.setFrameDelay(delay);
}
}

前面的getProvider()方法获得了MyFrameCallbackProvider的一个实例,MyFrameCallbackProvider是AnimationHandler的一个内部类,实现了AnimationFrameCallbackProvider接口,使用Choreographer作为计时脉冲的提供者,去发送帧回调。Choreographer从显示器子系统获得时间脉冲,postFrameCallback()方法发送帧回调。

//AnimationHandler
public interface AnimationFrameCallbackProvider {
void postFrameCallback(Choreographer.FrameCallback callback);
void postCommitCallback(Runnable runnable);
long getFrameTime();
long getFrameDelay();
void setFrameDelay(long delay);
}
//AnimationHandler
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};

在这个回调内执行了doAnimationFrame()方法,如果mAnimationCallbacks的个数大于0,AnimationFrameCallbackProvider就继续发送帧回调,继续重复执行doAnimationFrame()。

//AnimationHandler   
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}
cleanUpList();
}

在这个方法内开启了一个循环,里面执行了callback.doAnimationFrame(),这个操作会触发ValueAnimator类中的doAnimationFrame()。

//ValueAnimator
private void startAnimation() {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}

mAnimationEndRequested = false;
initAnimation();
mRunning = true;
if (mSeekFraction >= 0) {
mOverallFraction = mSeekFraction;
} else {
mOverallFraction = 0f;
}
if (mListeners != null) {
notifyStartListeners();
}
}

startAnimation()方法内调用了initAnimation()初始化动画。

//ValueAnimator
public final boolean doAnimationFrame(long frameTime) {
//省略部分代码
...
final long currentTime = Math.max(frameTime, mStartTime);
boolean finished = animateBasedOnTime(currentTime);

if (finished) {
endAnimation();
}
return finished;
}

这个方法在执行动画的过程中会被多次调用,其中重要的操作是animateBasedOnTime(currentTime)。

//ValueAnimator
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}

animateBasedOnTime()方法计算了已经执行的动画时长和动画分数,并调用animateValue()方法计算动画值。

//ValueAnimator
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}

ValueAnimator的animateValue()方法内部首先根据动画分数得到插值分数,再根据插值分数计算动画值,并调用了AnimatorUpdateListener的onAnimationUpdate()方法通知更新。

//ObjectAnimator
@Override
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}

super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}

ObjectAnimator的animateValue()方法不仅调用了父类的animateValue()方法,还在循环内调用了PropertyValuesHolder的setAnimatedValue()方法,传入的参数是产生动画效果的目标对象。

//PropertyValuesHolder
@Override
void setAnimatedValue(Object target) {
if (mFloatProperty != null) {
mFloatProperty.setValue(target, mFloatAnimatedValue);
return;
}
if (mProperty != null) {
mProperty.set(target, mFloatAnimatedValue);
return;
}
if (mJniSetter != 0) {
nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
return;
}
if (mSetter != null) {
try {
mTmpValueArray[0] = mFloatAnimatedValue;
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}

在PropertyValuesHolder的setAnimatedValue()方法内部,先通过JNI去修改目标对象的属性值,如果通过JNI找不到对应的方法,则通过使用反射机制修改目标对象的属性值。

4.总结

属性动画的功能相当强大,可以为视图对象和非视图对象添加动画效果,属性动画是通过改变要添加动画的目标对象的属性值实现的,ValueAnimator基于动画时长和已经执行的时长计算得出动画分数,然后根据设置的时间插值器TimeInterpolator计算得出动画的插值分数,再调用对应的估值器TypeEvaluator根据插值分数、起始值和结束值计算得出对象的属性值,ObjectAnimator类在计算出动画的新值后自动地更新对象的属性值,ValueAnimator类则需要手动地去设置对象的属性值。


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

收起阅读 »

为什么我不用 Typescript

前言 我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescrip...
继续阅读 »

前言


我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescript。我顿时感觉我似乎落后了,于是鼓起劲,开始学起了 Typescript。


但是我学了下,再用了下,发现它没有像被吹的那么神。虽说是 Javascript 的超集,也的确有些地方挺好的,但是还是不足够改变我使用 Javascript 编程。就像虽然有 Deno,但是我还是用 Node.js 一样。


所以,我就写这篇文章,说下我个人感觉 Typescript 的缺点、为何它的优点无法打动我用它替代 Javascript,以及跟推荐我使用 Typescript 的大家讲一下我不用 Typescript 的逻辑。


各位想骂我心里骂骂就好了,我今天过个生日也不容易。


缺陷


1. 语法丑陋,代码臃肿


我写两段相同的代码,大家感受下:


// js
const multiply = (i, j) => i * j

// ts
function multiply(i: number, j: number) {
return i + j
}

你看这类型注释,把好好的一段代码弄得这么乱……反正我看这样的 Typescript,花的反应时间一定比看上面的 Javascript 代码长。——不过也有可能是我比较熟悉 Javascript 吧。


复杂一点的东西也是一个道理(Apollo GraphQL 的代码):


// js
import React from 'react';
import ApolloClient from 'apollo-client';

let apolloContext;

export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext({});
}
return apolloContext;
}

export function resetApolloContext() {
apolloContext = React.createContext({});
}

铁定要比这个好得多:


// ts
import React from 'react';
import ApolloClient from 'apollo-client';

export interface ApolloContextValue {
client?: ApolloClient<object>;
renderPromises?: Record<any, any>;
}

let apolloContext: React.Context<ApolloContextValue>;

export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext<ApolloContextValue>({});
}
return apolloContext;
}

export function resetApolloContext() {
apolloContext = React.createContext<ApolloContextValue>({});
}

甚至有人提了个 issue 就是抱怨 Type 让它变得难用。


这么看,实在是为了这个假静态类型语言牺牲太多了,毕竟代码可读性还是很重要的。——之所以说它是假静态语言,是因为在真正的静态类型语言中,如 C 和 C++,不同的变量类型在内存中的存储方式不同,而在 Typescript 中不是这样。


比如,缺了这个可读性,debug 会变得更难。你是不是没有注意到我上面 multiply 的 Typescript 代码其实有 bug——应该是 * 而不是 +


2. 麻烦


浏览器不能直接执行 Typescript,所以 Typescript 必须要被编译成 Javascript 才能执行,要花一段时间;项目越大,花的时间越长,所以 Deno 才要停用它。并且,使用 Typescript 要安装新的依赖,虽然的确不麻烦,但是不用 Typescript,就不用再多装一个依赖了是不是。


其实还有一点,但是放不上台面来讲,因为这是我自己的问题。


我一直不大喜欢给添花样的东西,比如 pugtypescriptdeno 等;虽然 scss 啥的我觉得还是不错的——没有它我就写不出 @knowscount/vue-lib 这个仓库。


3. 文件体积会变大


随随便便就能猜到,我写那么多额外的类型注释、代码变得那么臃肿肯定会让 Typescript 文件比用 Javascript 编写的文件更大。作为一个用 “tab 会让文件体积更小” 作为论据的 tab 党,我当然讨厌 Typescript 啦哈哈哈哈。


我理解在编译过后都是一样的,但是反正……我还是不爽。而且正是由于 TypeScript 会被编译到JavaScript 中,所以才会出现无论你的类型设计得多么仔细,还是有可能让不同的值类型潜入 JavaScript 变量中的问题。这是不可避免的,因为 JavaScript 仍然是没有类型的。


4. 报错使我老花


单纯吐槽一句,为什么它的报错那么丑,我就拼错了一个单词他给我又臭又长报一大段,还没有颜色。。


为何无法打动我


在讲为什么 Typescript 的优点无法打动我之前,我先来讲一讲 Typescript 有哪些优点吧:



  1. 大厂的产品

  2. 大厂在用

  3. 可以用未来的特性

  4. 降低出 bug 的可能性

  5. 面对对象编程(OOP)


对于它是微软的产品,我不能多说啥,毕竟我用 Visual Studio Code 用得很香;但是大厂在用这个论点,就不一样了。


有个逻辑谬误叫做「reductio ad absurdum」,也就是「归谬法」。什么意思呢:



大厂用 Typescript,所以我要用 Typescript。

大厂几百万改个 logo,我就借几百万改个 logo,因为大厂是大厂,肯定做得对。



这就很荒谬。


的确,大公司会采用 Typescript,必定有他的道理。但是,同样的论证思路也可以用于 FlowAngularVueEmberjQueryBootstrap 等等等等,几乎所有流行的库都是如此,那么它们一定都适合你吗?


关于它可以让你提前接触到未来的特性……大哥,babel 不香吗?


最后就是 OOP 以及降低出 bug 的可能性(Typesafe)。OOP 是 Typescript 的核心部分,而现在 OOP 已经不吃香了……例如 Ilya Suzdalnitski 就说过它是「万亿美元的灾难」。


v2-9cc25cb80ddc2df7ab06f0184e433790_1440w.jpg


至于为什么这么说,无非就两点——面向对象代码难以重构,也难以进行单元测试。重构时的抓狂不提,单元测试的重要性,大家都清楚吧。


而在 Javascript 这种非 OOP 语言里头,函数可以独立于对象存在。不用为了包含这些函数而去发明一些奇怪的概念真是一种解脱。


总之,TypeScript 的所谓优点(更好的错误处理、类型推理)都不是最佳方案。你还是得写测试,还是得好好命名你的函数和变量。个人觉得单单像 Typescript 一样添加一个接口或类型不能解决任何这些问题。


正好一千五百字。


作者:TurpinHero
链接:https://juejin.cn/post/6961012856573657095

收起阅读 »

我是如何把vue项目启动时间从70s优化到7秒的

可怕的启动时间 公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右 启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧! 考虑到升级webpack版本的风险还是比较大的,出...
继续阅读 »

可怕的启动时间


公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右


image.png


启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧!


考虑到升级webpack版本的风险还是比较大的,出了一点问题都得找我,想想还是先别冒险,稳妥为主,所以我选择了通过插件来优化构建时间。


通过查阅资料,提升webpack的构建时间有以下几个方向:



  • 多进程处理文件,同一时间处理多个文件

  • 预编译资源模块,比如把长时间不变的库提取出来做预编译,构建的时候直接取编译结果就好

  • 缓存,未修改的模块直接拿到处理结果,不必编译

  • 减少构建搜索和处理的文件数量


针对以上几种优化方向,给出以下几种优化方案。


多进程构建


happypack


happypack 的作用就是将文件解析任务分解成多个子进程并发执行。


子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度。


查看happypack的github,发现作者已经不再维护该插件,并且作者推荐使用webpack官方的多进程插件thread-loader,所以我放弃了happypacy,选择了thread-loader。


thread-loader


thread-loader是官方维护的多进程loader,功能类似于happypack,也是通过开启子任务来并行解析文件,从而提高构建速度。


把这个loader放在其他loader前面。不过该loader是有限制的。示例:



  • loader无法发出文件。

  • loader不能使用自定义加载器API。

  • loader无法访问网页包选项。


每个worker都是一个单独的node.js进程,其开销约为600毫秒。还有进程间通信的开销。在小型项目中使用thread-loader可能并不能优化项目的构建速度,反而会拖慢构建速度,所以使用该loader时需要明确项目构建构成中真正耗时的过程。


我的项目中我主要是用该loader用来解析vue和js文件,作用于vue-loaderbabel-loader,如下代码:


const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}

module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}


配置了thread-loader后,重新构建试试,如下图所示,大概缩短了10秒的构建时间,还不错。


image.png


利用缓存提升二次构建的速度


虽然使用了多进程构建项目使构建时间缩短了10秒,但是一分钟的构建时间依然让人无法接受,这种挤牙膏似的优化方式多少让人有点不爽,有没有比较爽的方法来进一步缩短构建时间呢?


答案是有的,使用缓存。


缓存,不难理解就是第一次构建的时候将构建的结果缓存起来,当第二构建时,查看对应缓存是否修改,如果没有修改,直接使用缓存,由此,我们可以想象,当项目的变化不大时,大部分缓存都是可复用的,拿构建的速度岂不是会有质的飞跃。


cache-loader


说到缓存,当然百度一查,最先出现的就是cache-loader,github搜索下官方文档,得到如下结果:


该loader会缓存其他loader的处理结果,把该loader放到其他loader的前面,同时该loader保存和读取缓存文件也会有开销,所以建议在开销较大的loader前使用该loader。


文档很简单,考虑到项目中的vue-loaderbabel-loadercss-loader会有比较大的开销,所以为这些loader加上缓存,那么接下来就把cache-loader加到项目中吧:


const cacheLoader = {
loader: 'cache-loader'
}

const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}

module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
cacheLoader,
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
cacheLoader,
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}


util.js文件中,该文件主要是生成css相关的webpack配置,找到generateLoaders函数,修改如下:


  const cacheLoader = {
loader: 'cache-loader'
}

function generateLoaders(loader, loaderOptions) {
// 在css-loader前增加cache-loader
const loaders = options.usePostCSS ? [cacheLoader, cssLoader, postcssLoader] : [cacheLoader, cssLoader]

if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}

// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
// 添加这句配置解决element-ui的图标路径问题
publicPath: '../../'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}

如上配置完成后,再次启动项目,可以发现,现在的启动时间没什么变化,然后我们二次启动项目,可以发现现在的启动时间来到了30s左右,前面我们已经说过了,cache-loader缓存只有在二次启动的时候才会生效。


image.png


虽然项目启动时间优化了一半还多,但是我们的欲望是无限大的,30秒的时间离我们的预期还是有点差距的,继续优化!


hard-source-webpack-plugin


HardSourceWebpackPlugin是一个webpack插件,为模块提供中间缓存步骤。为了查看结果,您需要使用此插件运行webpack两次:第一次构建将花费正常的时间。第二次建设将大大加快。


话不多说,直接配置到项目中:


const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
//...
plugins: [
new HardSourceWebpackPlugin()
]
}

image.png


二次构建时,我们会发现构建时间来到了个位数,只有短短的7秒钟。


在二次构建中,我发现了一个现象,构建的进度会从10% 一下跳到 80%,甚至是一瞬间就完成了中间构建过程。这正验证了该插件为模块提供中间缓存的说法。


为模块提供中间缓存,我的理解是cache-loader缓存的是对应loader的处理结果 ,而这个插件甚至可以缓存整个项目全部的处理结果,直接引用最终输出的缓存文件,从而大大提高构建速度。


其他优化方法


babel-loader开启缓存


babel-loader自带缓存功能,开启cacheDirectory配置项即可,官网的说法是,开启缓存会提高大约两倍的转换时间。


module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
...
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启缓存
}
}
]
}
]
}
}

uglifyjs-webpack-plugin开启多进程压缩


uglifyjs-webpack-plugin或是其他的代码压缩工具都提供了多进程压缩代码的功能,开启可加速代码压缩。


动态polyfill


建议查看该篇文章


一文搞清楚前端 polyfill


总结


至此,我们完成了项目构建时间从70s到7s的优化过程,文中主要使用:





























一步步的将项目优化到几乎立马启动,哎,看来这下摸鱼的时间又少了,加油干吧,打工人!


作者:进击的小超人
链接:https://juejin.cn/post/6979879230297341989

收起阅读 »

桌面上的Flutter:Electron又多了个对手

从本质上看,Flutter 是一个独立的二进制可执行文件。它不仅改变了移动设备的玩法,在桌面设备上也同样不可小觑。一次编写,可在 Android、iOS、Windows、Mac 和 Linux 上进行原生部署,并通过 AngularDart 将所有的业务逻辑共...
继续阅读 »

从本质上看,Flutter 是一个独立的二进制可执行文件。它不仅改变了移动设备的玩法,在桌面设备上也同样不可小觑。一次编写,可在 Android、iOS、Windows、Mac 和 Linux 上进行原生部署,并通过 AngularDart 将所有的业务逻辑共享到 Web 上,这也是它的一大特点。


原生桌面客户端加速移动开发

在进入真实的原生桌面应用程序之前,先让我们看看在桌面上运行的 Flutter 可以为开发移动设备的人们带来哪些好处。


 启动时间


首先是启动 Android 模拟器和运行 Gradle。


下面的动图记录了模拟器冷启动并运行默认的 Flutter 应用程序。我只截取了其中的 2 分 40 秒,可以看出来在那段时间内可以发生很多事情。



但如果我告诉你,你可以在不到 10 秒的时间内启动并运行应用程序,你会怎么想?


运行原生应用程序可以省去启动 Android 模拟器和运行 Gradle 的全部开销。


看看这个:



请注意,你不必离开 IntelliJ。我们开发了将 Flutter 作为原生应用程序所需的工具,它适用于所有的 Flutter IDE。


 在运行时调整大小

与其他应用程序一样,你需要测试不同大小的布局,那么你需要做些什么?


你要求你的朋友使用不同的手机或者创建一组模拟器,以确保你的布局在每台设备上都是正常的。


这对我来说是个麻烦事。我们能更简单一点吗?


可以!



 使用 PC 上的资源

在开发和测试需要与手机上的资源发生交互的应用程序时,首先需要将所有测试文件移动到模拟器或设备上,这样可能会非常烦人。


如果只需要使用原生文件选择器来选择你想要的文件会不会更好?



 热重载和调试

热重载和调试功能是每个高效率工程师所必须的。



 内存占用

对于使用笔记本电脑或配置不太好的电脑的人来说,内存是非常重要的。


Android 模拟器占用大约 1GB 的内存。现在想象一下,为了测试一个聊天应用程序或类似的程序,需要启动 IntelliJ 和狂吃内存的 Chrome。



因为嵌入器是以原生的方式运行,所以不需要 Android 模拟器。这使它的内存占用变得更小。



原生桌面应用

只是在桌面上运行一个 Flutter 应用程序对于可立即发布的成熟桌面应用程序来说是远远不够的。这样做感觉上就像在桌面上运行移动应用程序。


少了什么东西?很多!


悬停、光标变化、滚轮交互,等等。


我们设法在不改变任何平台代码的情况下实现这些功能——它是一个独立的软件包,可以被包含在任何普通的 Flutter 应用程序中。但是,当与桌面嵌入器一起使用时,奇迹就发生了!



这是在 Android 模拟器运行完全相同的代码的结果。



同时开发 Android 和桌面应用程序。



桌面小部件展示

悬停:



光标:



开发一个真正的跨平台应用——包括桌面
 小部件

你创建的大多数小部件都是普遍可用的,如按钮、加载指标器等。


那些需要根据平台呈现不同外观的小部件可以通过 TargetPlatform 属性进行封装,非常容易。


像 CursorWidget 这样的小部件也可以被包含在 Android 版本中。


 页面

根据平台和屏幕尺寸的不同,页面也会有很大差异。不过它们大多只是布局不同,而不是功能差异。


使用 PageLayoutWidget 可以轻松地为每个平台创建准确的布局。



默认情况下对平板电脑也提供了很好的支持。


 插件

使用同时支持桌面嵌入器的插件时,不需要修改 Flutter 代码。


 代码什么时候发布?

很快。不过这个项目仍然处于测试阶段,在不久的将来很可能会发生一些变化。


我们的目标是在不久的将来发布易于安装、设置和使用的产品。


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

高效开发:分享 `extension` 有趣的用法

前言 extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。 举个栗子🌰,对 int 类型扩展 小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串 ...
继续阅读 »

前言


extension 可以在不更改类或创建子类的情况下,向类添加扩展功能的一种方式。灵活使用 extension 对基础类进行扩展,对开发效率有显著提升。


举个栗子🌰,对 int 类型扩展


小轰在开发项目中碰到需求:将单位为分的数值转换成单位为元的字符串


/// 通常的写法,封装转换方法

///封装方法:金额转字符串 保留两位小数
String convertPointToUnit(int num){
return (num.toDouble() / 100).toStringAsFixed(2);
}

///使用
void main(){
int num = 100;
var result = convertPointToUnit(num);
print(result); //打印结果为 1.00
}

同样的功能,使用 extension 进行开发,会更加简洁,如下:


/// 使用 extension 对 int 类进行扩展,添加方法 moneyString
extension ExInt on int {
/// 金额转字符串 保留两位小数
/// 100 => 1.00
String get moneyString => (this.toDouble() / 100).toStringAsFixed(2);
}

import ../ExInt.dart;
///使用
void main(){
int num = 100;
print(num.moneyString);
}

扩展后,直接作为该类型的成员方法来被使用。extension 就像是基因赋值,直接将能力(方法)对宿主进行赠与。


各种场景的扩展演示



  • 对枚举进行扩展实现


enum FruitEnum { apple, banana }

extension ExFruitEnum on FruitEnum {
String get name {
switch (this) {
case FruitEnum.apple:
return "apple";
case FruitEnum.banana:
return "banana";
}
}
}

///字符串匹配枚举
FruitEnum generateFruit (String fruitType){
if(fruitType == FruitEnum.apple.name){
return FruitEnum.apple;
} else if(fruitType == FruitEnum.banana.name){
return FruitEnum.banana;
}
}


  • 扩展作用于泛型:


//扩展list的方法
extension ExList<T> on List<T> {
//扩展操作符
List<T> operator -() => reversed.toList();
//一个链表分割成两个
List<List<T>> split(int at) => <List<T>>[sublist(0, at), sublist(at)];
}


  • 扩展在 Widget 控件中的应用


我们会有类似的控件


Column(
children: <Widget>[
Container(
paddint: const EdgeInsets.all(10)
child: AWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: BWidget(),
),
Container(
paddint: const EdgeInsets.all(10)
child: CWidget(),
),
]
)

代码中有很多的冗余对吧?我们用 extension 进行扩展一下:


extension ExWidget on Widget {
Widget paddingAll(double padding) {
return Container(
paddint: const EdgeInsets.all(padding)
child: this,
);
}
}

之后我们就可以改成:


Column(
children: <Widget>[
AWidget().paddingAll(10),
BWidget().paddingAll(10),
CWidget().paddingAll(10),
]
)

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

Android程序员如何从设计角度思考HTTPS

typora-root-url: img typora-copy-images-to: img 从设计角度思考HTTPS 我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HT...
继续阅读 »
typora-root-url: img
typora-copy-images-to: img

从设计角度思考HTTPS


我们了解了HTTP协议的内容后,明白HTTP存在很多安全隐患,所以后来推出了安全协议-HTTPS,我们不妨站在设计角度来设计一个安全的HTTP连接协议,看看HTTP存在哪些问题,我们该如何设计能保证安全性,从而了解HTTPS的安全协议是如何保障的HTTP安全

首先我们需要考虑一下,实现HTTPS所谓的安全,我们需要保证那些地方的安全:


1.首先我们需要保证服务端和客户端之间发送的消息是安全的


2.其次我们要保证服务端和客户端之间的连接是安全的


3.最后我们还要保证服务端不会被其他的伪造客户端连接,并且通过此方式破解加密方式


服务端/客户端信息交互的安全


首先我们先来考虑一下,有什么方法可以保证客户端发送的消息给服务端并且服务端返回结果,这个过程是安全的,大概的过程如下:


image.png


这个时候我们最先想到的方案--加密,我们使用加密算法给数据加密了不就行了吗,那么该选择什么加密算法呢?开发过程中最常见的加密算法如:MD5、SHA1这样的摘要算法或者aes、des这样的对称加密算法,又或者rsa这样的非对称算法,看起来每一种都可以实现数据加密传输,但是我们不要忘记了,第三点中我们希望能保证其他客户端连接不会破解此种加密方式,要知道在互联网中,不是仅仅一台客户端和一个服务端交互,可以有无数台客户端同时与服务端交互,这个时候我们如果要防止别的客户端破解加密,看起来摘要算法这种不可逆的算法刚好合适,但是我们不要忘记了,客户端和服务端需要完成交互的,那么也就是说这个加密不能为不可逆算法,否则客户端也无法对服务端数据进行处理,服务端也无法处理客户端数据,那么只能是对称加密算法或者非对称加密算法能满足了,我们继续思考,如果是多台客户端同时连接服务端,如下图:


image.png


那么似乎哪一种加密都能满足,那么我们不禁有个问题,万一有黑客(恶意机器)拦截了我们的请求,并且充当了中间的传输者,我们的这两种加密算法还安全吗?如下图:


image.png


可以看到,我们的客户端和服务端中间被未知的恶意机器拦截转发了请求,那么我们之前的加密方式如果是直接传递的加密方式和密钥,如果是对称加密那么结局可想而知,对于中间机器来说,依然可以解密出客户端和服务端的消息,对于黑客来说依然是透明的,安全性仅仅比不加密强上一点点,完全不可以称之为可信任的安全协议,那么使用非对称加密呢?我们都知道非对称加密是一堆密钥,每一端持有自己的私钥,对外公开公钥,而公钥加密仅仅使用私钥才可以解密,这样即使有中间机器拦截,也仅仅能拿到客户端和服务端的公钥,但是我们不要忘记了,客户端应该是持有服务端的公钥,用公钥加密传输给服务端,服务端私钥解密,响应的过程即是客户端的私钥解密服务端持有的客户端公钥,中间机器即使拦截了双方的公钥,也无法解密双方公钥自身加密的信息,这样的话,客户端和服务端数据传输安全的问题似乎完美解决了


新隐患-公钥传输方式


刚刚我们经过对比,确定了使用公私钥方式的非对称加密来作为客户端-服务端传输的加密方式,看起来应该高枕无忧了,那么事实真的如此吗?其实和对称加密一样,非对称加密这样直接传输加密,也仅仅是提高了一点点安全性而已,如果遇到的黑客在拦截到客户端的请求后,将自身的公钥传递给服务端以及客户端,而将客户端/服务端的公钥持有会如何?是的,细极思恐,那样中间机器将拥有解密双端消息的能力!为什么会这样?试想一下,客户端使用所谓服务端的公钥加密消息,发送,被中间机器拦截后,这所谓的服务端公钥是中间机器的,那么私钥岂不是可以解密拿到明文信息?然后再伪装使用拦截到的真实的客户端的公钥加密,转发给服务端,同理,服务端的所谓客户端公钥加密对于中间机器完全形同虚设,那么这种问题如何解决呢?我们可不可以更换一种公钥传输方式,尽量绕开中间机器的拦截,保证安全性呢?


我们可以想下,大概有如下两种方法传输公钥:


1.服务端把公钥发送给每一个连接进来的客户端


2.将公钥放到一个地方(比如独立的服务器,或者文件系统),客户端需要获取公钥的时候,访问这个地方的公钥来和服务端进行匹配


而第一个方案,即我们刚刚推翻的方案,很明显会存在被拦截调包的可能,那么似乎我们只能使用第二个方案来传输公钥?那么我们不禁有个问题,即客户端是如何知道存放公钥的远程服务器地址以及认证加密方式,而且每次建立连接都要来获取一次,对服务器的抗压能力也有一定的考验?还有如何保证黑客等恶意访问的用户不能通过此种方式拿到公钥,所以安全也是个比较麻烦的问题


引入第三方CA机构


由于上述提到的问题,所以对于个人而言,如果在开发网站的同时,还要再花费大量金钱和精力在开发公钥服务上,是很不合理的,那么有木有专门做这个的公司,我们托管给这个公司帮我们完成,只需要付出金钱的代价就能体验到服务不可以吗?于是,专门负责证书认证的第三方CA机构出现了,我们只需要提前申请好对应的服务端信息,并且提交对应资料,付出报酬,CA就会给我们提供对应的服务端认证服务,大大减少我们的操作和复杂度,但是这个时候我们不禁又有个问题,CA机构能保证只有客户端拿到认证的证书,并且认证通过,拦截对应的非正常客户端吗?如果不能的话,那岂不是黑客也可以拿到认证?现在的问题开始朝着如何认证用户真伪方向发展了


验证证书有效性


其实想要解决认证的问题,我们可以从生活中寻找一些灵感,我们每个人都有一个唯一的id,证明身份,这样可以保证识别出id和对应的人,也能识别不法分子,那么,既然计算机来源于生活,设计出来的东西也应该遵循正常的逻辑,我们何不给每个证书设置类似id的唯一编号呢?当然计算机是死的,没办法简单的将机器和证书编号进行绑定,那么就需要设计一个符合逻辑的证书验证过程。我们不妨思考下,平时开发的软件为了识别被人篡改的软件,我们是如何做的,相信大脑里有个词会一闪而过,MD5/SHA1(签名)?没错,那么我们证书的认证可否按照这个思路设计?


现在我们假设,客户端拿到证书后,能够从证书上拿到公钥信息、证书签名hash和有效期等信息,也就是说证书内置了计算整个证书的签名hash值,如果此时我们根据客户端的签名算法进行一次加签计算,和证书默认计算好的hash比较,发现不一致,那么就说明证书被修改了,肯定不是第三方发布的正式证书,如果一致,说明证书是真实的,没有被篡改,我们可以尝试与服务端连接了,因为证书拿到了,也有了公钥,后续的就是加密通信的过程了


至此,似乎一个安全的加密https简陋的设计出来了,也似乎解决了这些安全问题,但是不得不提的一点是,我们上面有个很重要的一点,即存放证书的服务器一定要保证安全性,第三方机构算不算绝对安全呢?答案是否定的,因为在https至今的历史上,发生过第三方机构被黑客攻击成功,黑客使用的也是正版的证书的事件,只能说计算机的世界不存在绝对安全,而是相对来说,安全系数提高了太多


HTTPS认证过程


前面我们设计了简陋版的HTTPS,那么,我们接下来看看,正版的HTTPS大体认证过程是如何的,首先我们从申请证书开始:


image.png


可以看到,申请证书的时候,需要提供很多内容,其中域名、签名hash算法、加密算法是最重要的,通过这三项计算生成证书以及确定加密认证算法,并且在这个过程中还需要提供服务端自己的公钥,用来生成证书,CA机构使用自己的私钥加密证书,生成证书传递给服务端


2.证书申请拿到以后,客户端tcp三次握手连接(会携带一个随机数client-random),这个时候服务端将证书信息(包含过期时间、签名算法、当前证书的hash签名、服务端公钥、颁发证书机构等信息)传递给客户端,并且传递一个random随机数


3.客户端收到证书后,使用浏览器内置的CA认证,对证书的颁发机构逐个/逐层校验,确定证书来源正常,并且校验证书过期时间,确定是否可用,最后根据证书的签名算法,计算出对应的签名hash,和证书内置的签名hash比较,确定是否是未篡改的证书,完全认证通过后,证书认证环节结束


4.客户端生成随机对称密钥( pre-master ),将双端随机数组合通过证书的公钥(服务端的公钥)加密后,发送给服务端,服务端收到后,根据双方生成的随机数组合验证击进行http通信


以上就是HTTPS认证的大体流程,另外需要注意的是,HTTPS使用了签名算法(MD5/SHA256等)、对称加密以及非对称加密完成了整个交互过程,在认证过程中仅仅使用了签名算法和非对称加密保证建立通道的安全稳定,在通道建立过程中,维持了一个sessionid,用来防止频繁创建通道大量消耗资源,尽可能保证通道长期连接复用,并且我们需要知道一点,非对称加密虽然安全,但是相比较对称加密,加密解密步骤复杂导致时间会更久,所以HTTPS在建立通道以后,会选择双端协议使用对称加密来完成后续的数据交互,而上述提到的双方的随机对称密钥组合是用来在建立连接后的第一次交互的过程中,二次确认握手过程是否被篡改(客户端把Client.random + sever.random + pre-master组合后使用公钥加密,并且把握手消息根据证书的签名算法计算hash,发送给服务端确认握手过程是否被窜改),完成校验后,确定当前是安全连接后,双端之间就会使用约定好的对称加密算法进行数据加密解密传输,至此一个完整的HTTPS协议完成


相关视频


Android程序员中高级进阶学习/OkHttp原理分析


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

做一个短链接系统需要考虑这么多

什么是短链接短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:mp.weixin.qq.com/s?biz=MzU5M…又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者...
继续阅读 »



什么是短链接

短链接顾名思义,就是一个比较短的链接(我好像说了个废话),我们平时看到的链接可能长这样:

mp.weixin.qq.com/s?biz=MzU5M…

又臭又长有没有(没错,这是我的WX公众号链接,可以关注一下),那如果我们需要将某个链接发在某个文章或者推广给别人的时候,这么长看着也太不爽了,而短链接的出现就是用一个很短的URL来替代这个很长的家伙,当用户访问短链接的时候,会重定向到原来的链接。比如长下面这样:

sourl.cn/CsTkky

你如果平时有注意的话,各种商业短信上的链接也是会转成特别短的:

img

这个特别短的URL就是短链接。

为什么需要URL短链接

URL短链接用于为长URL创建较短的别名,我们称这些缩短的别名为“短链接”;当用户点击这些短链接时,会被重定向到原始URL;短链接在显示、打印、发送消息时可节省大量空间。

例如,如果我们通过sourl缩短以下URL:

juejin.cn/user/211951…

我们可以得到一个短链接:

sourl.cn/R99fbj

缩短的URL几乎是实际URL大小的三分之一。

URL缩写经常用于优化设备之间的链接,跟踪单个链接以分析受众,衡量广告活动的表现,或隐藏关联的原始URL。

如果你以前没有使用过sourl,可以尝试创建一个新的URL短链接,并花一些时间浏览一下他们的服务提供的各种选项。可以让你更好的理解这篇文章。

系统的要求和目标

在完成一个功能或者开发一个系统时,先确定系统的定位和要达到的目标是一个好的习惯,这可以让你在设计和开发过程中有更清晰的思路。

我们的短链接系统应满足以下要求:

功能要求:

  • 给定一个URL,我们的服务应该为其生成一个较短且唯一的别名,这叫做短链接,此链接应足够短,以便于复制和粘贴到应用程序中;

  • 当用户访问短链接时,我们的服务应该将他们重定向到原始链接;

  • 用户应该能够选择性地为他们的URL选择一个自定义的短链接;

  • 链接可以在指定时间跨度之后过期,用户应该能够指定过期时间。

非功能要求:

  • 系统必须高度可用。如果我们的服务关闭,所有URL重定向都将开始失败。

  • URL重定向的延迟情况应该足够小;

  • 短链接应该是不可猜测的。

扩展要求:

  • 支持分析和统计,例如短链接的访问次数;

  • 其他服务也应该可以通过RESTAPI访问我们的服务。

容量要求和限制

我们的系统将会有很大的访问量。会有对短链接的读取请求和创建短链接的写入请求。假设读写比例为100:1。

访问量预估:

假设我们每个月有5亿个新增短链接,读写比为100:1,我们可以预计在同一时间内有500亿重定向:

100 * 5亿 => 500亿

我们系统的QPS(每秒查询数量)是多少?每秒的新短链接为:

5亿/ (30天 * 24小时 * 3600 秒) ≈ 200 URLs/s

考虑到100:1读写比,每秒URL重定向将为:

100 * 200 URLs/s = 20000/s

存储预估:

假设我们将每个URL缩短请求(以及相关的缩短链接)存储5年。由于我们预计每个月将有5亿个新URL,因此我们预计存储的对象总数将为300亿:

5亿 * 5 年 * 12 月 = 300亿 

假设每个存储的对象大约有500个字节(这只是一个估算值)。我们将需要15TB的总存储:

300亿*500bytes≈15TB

带宽预估:

对于写请求,由于我们预计每秒有200个新的短链接创建,因此我们服务的总传入数据为每秒100KB:

200*500bytes≈100KB/s

对于读请求,预计每秒约有20,000个URL重定向,因此我们服务的总传出数据将为每秒10MB:

20000 * 500 bytes ≈10 MB/s

内存预估:

对于一些热门访问的URL为了提高访问速率,我们需要进行缓存,需要多少内存来存储它们?如果我们遵循二八原则,即20%的URL产生80%的流量,我们希望缓存这20%的热门URL。

由于我们每秒有20,000个请求,因此我们每天将收到17亿个请求:

20000 * 24 * 3600 ≈ 17亿

要缓存这些请求中的20%,我们需要170 GB的内存:

17亿 * 0.2 * 500bytes ≈ 170GB

这里需要注意的一件事是,由于将会有许多来自相同URL的重复请求,因此我们的实际内存使用量可能达不到170 GB。

整体来说,假设每月新增5亿个URL,读写比为100:1,我们的预估数据大概是下面这样:

类型预估数值
新增短链接200/s
短链接重定向20000/s
传入数据100KB/s
传出数据10 MB/s
存储5年容量15 TB
内存缓存容量170 GB

系统API设计

一旦我们最终确定了需求,就可以定义系统的API了,这里则是要明确定义我们的系统能提供什么服务。

我们可以使用REST API来公开我们服务的功能。以下是用于创建和删除URL的API的定义:

创建短链接接口

String createURL(api_dev_key, original_url, custom_alias=None, user_name=None, expire_date=None)
复制代码

参数列表:

api_dev_key:分配给注册用户的开发者密钥,可以根据该值对用户的创建短链接数量进行限制;

original_url:需要生成短链接的原始URL;

custom_alias :用户对于URL自定义的名称;

user_name :可以用在编码中的用户名;

expire_date :短链接的过期时间;

返回值:

成功生成短链接将返回短链接URL;否则,将返回错误代码。

删除短链接接口

String deleteURL(api_dev_key, url_key)
复制代码

其中url_key是表示要删除的短链接字符串;成功删除将返回delete success

如何发现和防止短链接被滥用?

恶意用户可以通过使用当前设计中的所有URL密钥来对我们进行攻击。为了防止滥用,我们可以通过用户的api_dev_key来限制用户。每个api_dev_key可以限制为每段时间创建一定数量的URL和重定向(可以根据开发者密钥设置不同的持续时间)。

数据模型设计

在开发之前完成数据模型的设计将有助于理解各个组件之间的数据流。

在我们短链接服务系统中的数据,存在以下特点:

  • 需要存储十亿条数据记录;

  • 存储的每个对象都很小(小于1K);

  • 除了存储哪个用户创建了URL之外,记录之间没有任何关系;

  • 我们的服务会有大量的读取请求。

我们需要创建两张表,一张用于存储短链接数据,一张用于存储用户数据;

img

应该使用怎样的数据库?

因为我们预计要存储数十亿行,并且不需要使用对象之间的关系-所以像mongoDB、Cassandra这样的NoSQL存储是更好的选择。选择NoSQL也更容易扩展。

基本系统设计与算法

现在需要解决的问题是如何为给定的URL生成一个简短且唯一的密钥。主要有两种解决方案:

  • 对原URL进行编码

  • 提前离线生成秘钥

对原URL编码

可以计算给定URL的唯一HASH值(例如,MD5或SHA256等)。然后可以对HASH进行编码以供显示。该编码可以是base36([a-z,0-9])base62([A-Z,a-z,0-9]),如果我们加上+/,就可以使用Base64编码。需要考虑的一个问题是短链接的长度应该是多少?6个、8个或10个字符?

使用Base64编码,6个字母的长密钥将产生64^6≈687亿个可能的字符串; 使用Base64编码,8个字母长的密钥将产生64^8≈281万亿个可能的字符串。

按照我们预估的数据,687亿对于我们来说足够了,所以可以选择6个字母。

如果我们使用MD5算法作为我们的HASH函数,它将产生一个128位的HASH值。在Base64编码之后,我们将得到一个超过21个字符的字符串(因为每个Base64字符编码6位HASH值)。

现在我们每个短链接只有6(或8)个字符的空间,那么我们将如何选择我们的密钥呢?

我们可以取前6(或8)个字母作为密钥,但是这样导致链接重复;要解决这个问题,我们可以从编码字符串中选择一些其他字符或交换一些字符。

我们的解决方案有以下问题:

解决办法:

我们可以将递增的序列号附加到每个输入URL以使其唯一,然后生成其散列。不过,我们不需要将此序列号存储在数据库中。此方法可能存在的问题是序列号不断增加会导致溢出。添加递增的序列号也会影响服务的性能。

另一种解决方案可以是将用户ID附加到输入URL。但是,如果用户尚未登录,我们将不得不要求用户选择一个唯一的key。即使这样也有可能有冲突,需要不断生成直到得到唯一的密钥。

离线生成秘钥

可以有一个独立的密钥生成服务,我们就叫它KGS(Key Generation Service),它预先生成随机的六个字母的字符串,并将它们存储在数据库中。每当我们想要生成短链接时,都去KGS获取一个已经生成的密钥并使用。这种方法更简单快捷。我们不仅不需要对URL进行编码,而且也不必担心重复或冲突。KGS将确保插入到数据库中的所有密钥都是唯一的。

会存在并发问题吗?

密钥一旦使用,就应该在数据库中进行标记,以确保不会再次使用。如果有多个服务器同时读取密钥,我们可能会遇到两个或多个服务器尝试从数据库读取相同密钥的情况。如何解决这个并发问题呢?

KGS可以使用两个表来存储密钥:一个用于尚未使用的密钥,一个用于所有已使用的密钥。

一旦KGS将密钥提供给其中一个服务器,它就可以将它们移动到已使用的秘钥表中;可以始终在内存中保留一些密钥,以便在服务器需要时快速提供它们。

为简单起见,一旦KGS将一些密钥加载到内存中,它就可以将它们移动到Used Key表中。这可确保每台服务器都获得唯一的密钥。

如果在将所有加载的密钥分配给某个服务器之前KGS重启或死亡,我们将浪费这些密钥,考虑到我们拥有的秘钥很多,这种情况也可以接受。

还必须确保KGS不将相同的密钥提供给多个服务器,因此,KGS将秘钥加载到内存和将秘钥移动到已使用表的动作需要时同步的,或者加锁,然后才能将秘钥提供给服务器。

KGS是否存在单点故障?

要解决KGS单点故障问题,我们可以使用KGS的备用副本。当主服务器死机时,备用服务器可以接管以生成和提供密钥。

每个应用服务器是否可以换成一些Key?

可以,这样可以减少对KGS的访问,不过,在这种情况下,如果应用服务器在使用所有密钥之前死亡,我们最终将丢失这些密钥。但是因为我们的秘钥数量很多,这点可以接受。

如何完成秘钥查找?

我们可以在数据库中查找密钥以获得完整的URL。如果它存在于数据库中,则向浏览器发回一个“HTTP302 Redirect”状态,将存储的URL传递到请求的Location字段中。如果密钥不在我们系统中,则发出HTTP 404 Not Found状态或将用户重定向回主页。

数据分区和复制

因为我们要存储十亿个URL数据,那么一个数据库节点在存储上可能不满足要求,并且单节点也不能支撑我们读取的要求。

因此,我们需要开发一种分区方案,将数据划分并存储到不同的数据库服务中。

基于范围分区:

我们可以根据短链接的第一个字母将URL存储在不同的分区中。因此,我们将所有以字母'A/a'开头的URL保存在一个分区中,将以字母‘B/b’开头的URL保存在另一个分区中,以此类推。这种方法称为基于范围的分区。我们甚至可以将某些不太频繁出现的字母合并到一个数据库分区中。

基于hash值分区:

在此方案中,我们对要存储的对象进行Hash计算。然后,我们根据Hash结果计算使用哪个分区。在我们的例子中,我们可以使用短链接的Hash值来确定存储数据对象的分区。

Hash函数会将URL随机分配到不同的分区中(例如,Hash函数总是可以将任何‘键’映射到[1…256]之间的一个数字,这个数字将表示我们在其中存储对象的分区。

这种方式有可能导致有些分区数据超载,可以使用一致性哈希算法解决。

缓存

对于频繁访问的热点URL我们可以进行缓存。缓存的方案可以使用现成的解决方案,比如使用memcached,Redis等,因此,应用服务器在查找数据库之前可以快速检查高速缓存是否具有所需的URL。

如果确定缓存容量?

可以从每天20%的流量开始,并根据客户端的使用模式调整所需的缓存服务器数量。如上所述,我们需要170 GB内存来缓存20%的日常流量。可以使用几个较小的服务器来存储所有这些热门URL。

选择哪种淘汰策略?

淘汰策略是指当缓存已满时,如果我们想用更热点的URL替换链接,我们该如何选择?

对于我们的系统来说,最近最少使用(LRU)是一个合理的策略。在此策略下,我们首先丢弃最近最少使用的URL;我们可以使用一个短链接或短链接的HASH值作为key的Hash Map或类似的数据结构来存储URL和访问次数。

如何更新缓存?

每当出现缓存未命中时,我们的服务器都会命中后端数据库。每次发生这种情况,我们都可以更新缓存并将新条目传递给所有缓存副本。每个副本都可以通过添加新条目来更新其缓存。如果副本已经有该条目,它可以简单地忽略它。

负载均衡

可以在系统中的三个位置添加负载均衡层:

  • 在客户端和应用程序服务器之间;

  • 在应用程序服务器和数据库服务器之间;

  • 在应用程序服务器和缓存服务器之间。

可以使用简单的循环调度方法,在后端服务器之间平均分配传入的请求。这种负载均衡方式实现起来很简单,并且不会带来任何开销。此方法的另一个好处是,如果服务器死机,负载均衡可以让其退出轮换,并停止向其发送任何流量。

循环调度的一个问题是没有考虑服务器过载情况。因此,如果服务器过载或速度慢,不会停止向该服务器发送新请求。要处理此问题,可以放置一个更智能的解决方案,定期查询后端服务器的负载并基于此调整流量。

数据清除策略

数据应该永远保留,还是应该被清除?如果达到用户指定的过期时间,短链接应该如何处理?

  • 持续扫描数据库,清除过期数据。

  • 懒惰删除策略

如果我们选择持续查询过期链接来删除,将会给数据库带来很大的压力;可以慢慢删除过期的链接,并进行懒惰的方式清理。服务确保只有过期的链接将被删除,尽管一些过期的链接可以在数据库保存更长时间,但永远不会返回给用户。

  • 每当用户尝试访问过期链接时,我们都可以删除该链接并向用户返回错误;

  • 单独的清理服务可以定期运行,从存储和缓存中删除过期的链接;

  • 此服务应该非常轻量级,并计划仅在预期用户流量较低时运行;

  • 我们可以为每个短链接设置默认的到期时间(例如两年);

  • 删除过期链接后,我们可以将密钥放回KGS的数据库中重复使用。

结语

以上就是开发一个短链接服务系统要做的方方面面,可能还存在一些小黑没有考虑到的地方,欢迎留言区交流!如果对你有一点点帮助,点个赞鼓励一下。

作者:小黑说Java
来源:https://juejin.cn/post/7034325565431611406

收起阅读 »

Python编程需要遵循的一些规则v2

Python编程需要遵循的一些规则v2使用 pylintpylint 是一个在 Python 源代码中查找 bug 的工具. 对于 C 和 C++ 这样的强类型静态语言来说, 这些 bug 通常由编译器来捕获. 由于 Python 的动态特性, 有些警告可能不...
继续阅读 »



Python编程需要遵循的一些规则v2

使用 pylint

pylint 是一个在 Python 源代码中查找 bug 的工具. 对于 C 和 C++ 这样的强类型静态语言来说, 这些 bug 通常由编译器来捕获. 由于 Python 的动态特性, 有些警告可能不对. 不过虚报的情况应该比较少. 确保对你的代码运行 pylint. 在 CI 流程中加入 pylint 检查的步骤. 抑制不准确的警告, 以便其他正确的警告可以暴露出来。

自底向上编程

自底向上编程(bottom up): 从最底层,依赖最少的地方开始设计结构及编写代码, 再编写调用这些代码的逻辑, 自底向上构造程序.

  • 采取自底向上的设计方式会让代码更少以及开发过程更加敏捷.

  • 自底向上的设计更容易产生符合单一责任原则(SRP) 的代码.

  • 组件之间的调用关系清晰, 组件更易复用, 更易编写单元测试案例.

如:需要编写调用外部系统 API 获取数据来完成业务逻辑的代码.

  • 应该先编写一个独立的模块将调用外部系统 API 获取数据的接口封装在一些函数中, 然后再编写如何调用这些函数 来完成业务逻辑.

  • 不可以先写业务逻辑, 然后在需要调用外部 API 时再去实现相关代码, 这会产生调用 API 的代码直 接耦合在业务逻辑中的代码.

防御式编程

使用 assert 语句确保程序处于的正确状态 不要过度使用 assert, 应该只用于确保核心的部分.

注意 assert 不能代替运行时的异常, 不要忘记 assert 语句可能会被解析器忽略.

assert 语句通常可用于以下场景:

  • 确保公共类或者函数被正确地调用 例如一个公共函数可以处理 list 或 dict 类型参数, 在函数开头使用 assert isinstance(param, (list, dict))确保函数接受的参数是 list 或 dict

  • assert 用于确保不变量. 防止需求改变时引起代码行为的改变

if target == x:
  run_x_code()
elif target == y:
  run_y_code()
else:
  run_z_code()

假设该代码上线时是正确的, target 只会是 x, y, z 三种情况, 但是稍后如果需求改变了, target 允许 w 的 情况出现. 当 target 为 w 时该代码就会错误地调用 run_z_code, 这通常会引起糟糕的后果.

  • 使用 assert 来确保不变量

assert target in (x, y, z)
if target == x:
  run_x_code()
elif target == y:
  run_y_code()
else:
  assert target == z
  run_z_code()

不使用 assert 的场景:

  • 不使用 assert 在校验用户输入的数据, 需要校验的情况下应该抛出异常

  • 不将 assert 用于允许正常失败的情况, 将 assert 用于检查不允许失败的情况.

  • 用户不应该直接看到 AssertionError, 如果用户可以看到, 将这种情况视为一个 BUG

避免使用 magic number

赋予特殊的常量一个名字, 避免重复地直接使用它们的字面值. 合适的时候使用枚举值 Enum.

使用常量在重构时只需要修改一个地方, 如果直接使用字面值在重构时将修改所有使用到的地方.

  • 建议

GRAVITATIONAL_CONSTANT = 9.81

def get_potential_energy(mass, height):
  return mass * height * GRAVITATIONAL_CONSTANT

class ConfigStatus:
  ENABLED = 1
  DISABLED = 0

Config.objects.filter(enabled=ConfigStatus.ENABLED)
  • 不建议

def get_potential_energy(mass, height):
  return mass * height * 9.81

# Django ORM
Config.objects.filter(enabled=1)

处理字典 key 不存在时的默认值

使用 dict.setdefault 或者 defaultdict

# group words by frequency
words = [(1, 'apple'), (2, 'banana'), (1, 'cat')]
frequency = {}

dict.setdefault

  • 建议

for freq, word in words:
  frequency.setdefault(freq, []).append(word)

或者使用 defaultdict

from collections import defaultdict

frequency = defaultdict(list)

for freq, word in words:
  frequency[freq].append(word)
  • 不建议

for freq, word in words:
  if freq not in frequency:
      frequency[freq] = []
  frequency[freq].append(word)

注意在 Python 3 中 map filter 返回的是生成器而不是列表, 在隋性计算方面有所区别

禁止使用 import *

原则上禁止避免使用 import *, 应该显式地列出每一个需要导入的模块

使用 import * 会污染当前命名空间的变量, 无法找到变量的定义是来哪个模块, 在被 import 的模块上的改动可 能会在预期外地影响到其它模块, 可能会引起难以排查的问题.

在某些必须需要使用或者是惯用法 from foo import * 的场景下, 应该在模块 foo 的末尾使用 all 控制被导出的变量.

# foo.py
CONST_VALUE = 1
class Apple:
  ...

__all__ = ("CONST_VALUE", "Apple")

# bar.py
# noinspection PyUnresolvedReferences
from foo import *

作者:未来现相
来源:https://mp.weixin.qq.com/s/QinR-bHolVlr0z8IyhCqfg

收起阅读 »

从零到一编写 IOC 容器

前言本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScri...
继续阅读 »




前言

本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

一、TS 装饰器

1、类装饰器

(1)类型声明

type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
  • 参数:

    target: 类的构造器。

  • 返回:

如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T) {
 // 新构造器继承原有的构造器,并且返回
 return class extends BaseClass {  
   // 新增属性 school
   public school = 'qinghua'
   // 重写方法 toString
   toString() {
     return JSON.stringify(this);
  }
};
}

@School
class Student {
 public name = 'tom';
 public age = 14;
}

console.log(new Student().toString())
// {"name":"tom","age":14,"school":"qinghua"}

但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}


@School
class Student{
 getSchool() {
   return this.school; // Property 'school' does not exist on type 'Student'
}
}

new Student().school  // Property 'school' does not exist on type 'Student'

这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示

type Consturctor = { new (...args: any[]): any };

function School<T extends Consturctor>(BaseClass: T){
 return class extends BaseClass {
   // 新增属性 school
   public school = 'qinghua'
};
}

// 新增一个类用于提供类型信息
class Base {
 school: string;
}

@School
class Student extends Base{
 getSchool() {
   return this.school;
}
}

new Student().school)

2、属性装饰器

(1)类型声明

type PropertyDecorator = (
target: Object,
 propertyKey: string | symbol
) => void;
复制代码
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称。

  • 返回:

返回的结果将被忽略。

我们可以通过属性装饰器给属性添加对应的验证判断,如下所示

function NameObserve(target: Object, property: string): void {
 console.log('target:', target)
 console.log('property:', property)
 let _property = Symbol(property)
 Object.defineProperty(target, property, {
   set(val){
     if(val.length > 4){
       throw new Error('名称不能超过4位!')
    }
     this[_property] = val;
  },
   get: function() {
     return this[_property];
}
})
}

class Student {
 @NameObserve
 public name: string;  // target: Student {}   key: 'name'
}

const stu = new Student();
stu.name = 'jack'
console.log(stu.name); // jack
// stu.name = 'jack1'; // Error: 名称不能超过4位!

export default Student;

3、方法装饰器

(1)类型声明:

type MethodDecorator = <T>(
 target: Object,
 propertyKey: string | symbol,
 descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;

    2. propertyKey: 属性的名称;

    3. descriptor: 属性的描述器;

  • 返回: 如果返回了值,它会被用于替代属性的描述器。

方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力

function logger(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const origin = descriptor.value;
 console.log(descriptor)
 descriptor.value = function(...args: number[]){
   console.log('params:', ...args)
   const result = origin.call(this, ...args);
   console.log('result:', result);
   return result;
}
}

class Person {
 @logger
 add(x: number, y: number){
   return x + y;
}
}

const person = new Person();
const result = person.add(1, 2);
console.log('查看 result:', result) // 3

4、访问器装饰器

访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:

  • value

  • writable

  • enumerable

  • configurable

访问器装饰器的描述器的key为:

  • get

  • set

  • enumerable

  • configurable

例如,我们可以对访问器进行统一更改:

function descDecorator(target: Object, property: string, 
   descriptor: PropertyDescriptor): PropertyDescriptor | void {
 const originalSet = descriptor.set;
 const originalGet = descriptor.get;
 descriptor.set = function(value: any){
   return originalSet.call(this, value)
}
 descriptor.get = function(): string{
   return 'name:' + originalGet.call(this)
}
}

class Person {
 private _name = 'tom';

 @descDecorator
 set name(value: string){
   this._name = value;
}

 get name(){
   return this._name;
}
}

const person = new Person();
person.name = ('tom');
console.log('查看:', person.name) // name:'tom'

5、参数装饰器

类型声明:

type ParameterDecorator = (
 target: Object,
 propertyKey: string | symbol,
 parameterIndex: number
) => void;
  • 参数:

    1. target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。

    2. propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。

    3. parameterIndex: 参数在方法中所处的位置的下标。

  • 返回:

返回的值将会被忽略。

单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。

function ParamDecorator(target: Object, property: string, 
   paramIndex: number): void {
 console.log(property);
 console.log(paramIndex);
}

class Person {
 private name: string;

 public setNmae(@ParamDecorator school: string, name: string){  // setNmae 0
   this.name = school + '_' + name
}
}

6、执行时机

装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。

function f(C) {
 console.log('apply decorator')
 return C
}

@f
class A {}

// output: apply decorator

7、执行顺序

不同类型的装饰器的执行顺序是明确定义的:

  • 实例成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 静态成员:参数装饰器 -> 方法/访问器/属性 装饰器

  • 构造器:参数装饰器

  • 类装饰器

示例如下所示

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

@f("Class Decorator")
class C {
 @f("Static Property")
 static prop?: number;

 @f("Static Method")
 static method(@f("Static Method Parameter") foo:any) {}

 constructor(@f("Constructor Parameter") foo:any) {}

 @f("Instance Method")
 method(@f("Instance Method Parameter") foo:any) {}

 @f("Instance Property")
 prop?: number;
}

/* 输出顺序如下
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
*/

我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。

function f(key: string): any {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 method(
   @f("Parameter Foo") foo,
   @f("Parameter Bar") bar
) {}
}

/* 输出顺序如下
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
*/

8、多个装饰器组合

我们可以对同一目标应用多个装饰器。它们的组合顺序为:

  • 求值外层装饰器

  • 求值内层装饰器

  • 调用内层装饰器

  • 调用外层装饰器

如下示例所示

function f(key: string) {
 console.log("evaluate: ", key);
 return function () {
   console.log("call: ", key);
};
}

class C {
 @f("Outer Method")
 @f("Inner Method")
 method() {}
}

/* 输出顺序如下
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
*/

二、Reflect Metadata

1、背景

在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:

  • 其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)

  • 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。

  • 为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;

  • 元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;

  • 对开发人员来说,定义新的元数据生成装饰器应该简洁易用;

2、使用

TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示

  • npm i reflect-metadata --save

  • 在 tsconfig.json 里配置选项 emitDecoratorMetadata: true

关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

import "reflect-metadata";

@Reflect.metadata('classMetaData', 'A')
class SomeClass {
 @Reflect.metadata('methodMetaData', 'B')
 public someMethod(): string {
   return 'hello someMethod';
}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B

当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的

import "reflect-metadata";

function classDecorator(): ClassDecorator {
 return target => {
   // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
   Reflect.defineMetadata('classMetaData', 'A', target);
};
}

function methodDecorator(): MethodDecorator {
 return (target, key, descriptor) => {
   // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
   Reflect.defineMetadata('methodMetaData', 'B', target, key);
};
}

@classDecorator()
class SomeClass {
 @methodDecorator()
 someMethod() {}
}

console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'

3、design:类型元数据

在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据

  • design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;

  • design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;

  • design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;

示例如下所示

import "reflect-metadata";

@Reflect.metadata('type', 'class')
class A {  
 constructor(
   public name: string,
   public age: number
) { }  

 @Reflect.metadata(undefined, undefined)  
 method(name: string, age: number):boolean {    
   return true  
}
}

 const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
 const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
 const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
 
 console.log(t1)  // [Function: Function]
 console.log(...t2) // [Function: String] [Function: Number]
 console.log(t3) // [Function: Boolean]

三、IOC 容器实现

1、源码解读

我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:

  • AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;

  • MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;

  • RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;

packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:

  • @provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;

  • @inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。

2、简单实现

2.1、装饰器 Provider

实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。

import 'reflect-metadata'
import * as camelcase from 'camelcase'
import { class_key } from './constant'

// Provider 装饰的类,表示要注册到 IOC 容器中
export function Provider (identifier?: string, args?: Array<any>) {
 return function (target: any) {
   // 类注册的唯一标识符
   identifier = identifier ?? camelcase(target.name)

   Reflect.defineMetadata(class_key, {
     id: identifier,  // 唯一标识符
     args: args || [] // 实例化所需参数
  }, target)
   return target
}
}

2.2、装饰器 Inject

实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。

import 'reflect-metadata'
import { props_key } from './constant'

export function Inject () {
 return function (target: any, targetKey: string) {
   // 注入对象
   const annotationTarget = target.constructor
   let props = {}
   // 同一个类,多个属性注入类
   if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
     props = Reflect.getMetadata(props_key, annotationTarget)
  }

   //@ts-ignore
   props[targetKey] = {
     value: targetKey
  }

   Reflect.defineMetadata(props_key, props, annotationTarget)
}
}

2.3、管理容器 Container

管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。

import 'reflect-metadata'
import { props_key } from './constant'

export class Container {
 bindMap = new Map()

 // 绑定类信息
 bind(identifier: string, registerClass: any, constructorArgs: any[]) {
   this.bindMap.set(identifier, {registerClass, constructorArgs})
}

 // 获取实例,将实例绑定到需要注入的对象上
 get<T>(identifier: string): T {
   const target = this.bindMap.get(identifier)
   if (target) {
     const { registerClass, constructorArgs } = target
     // 等价于 const instance = new registerClass([...constructorArgs])
     const instance = Reflect.construct(registerClass, constructorArgs)

     const props = Reflect.getMetadata(props_key, registerClass)
     for (let prop in props) {
       const identifier = props[prop].value
       // 递归进行实例化获取 injected object
       instance[prop] = this.get(identifier)
    }
     return instance
  }
}
}

2.4、加载类文件 load

启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。

import * as fs from 'fs'
import { resolve } from 'path'
import { class_key } from './constant'

// 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
export function load(container: any, path: string) {
 const list = fs.readdirSync(path)
 for (const file of list) {
   if (/\.ts$/.test(file)) {
     const exports = require(resolve(path, file))

     for (const m in exports) {
       const module = exports[m]
       if (typeof module === 'function') {
         const metadata = Reflect.getMetadata(class_key, module)
         // register
         if (metadata) {
           container.bind(metadata.id, module, metadata.args)
        }
      }
    }
  }
}
}

2.5、示例类

三个示例类如下所示

// class A
import { Provider } from "../provide";
import { Inject } from "../inject";
import B from './classB'
import C from './classC'

@Provider('a')
export default class A {
 @Inject()
 private b: B

 @Inject()
 c: C

 print () {
   this.c.print()
}
}

// class B
import { Provider } from '../provide'

@Provider('b', [10])
export default class B {
 n: number
 constructor (n: number) {
   this.n = n
}
}

// class C
import { Provider } from '../provide'

@Provider()
export default class C {
 print () {
   console.log('hello')
}
}

2.6、初始化

我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。

import { Container } from './container'
import { load } from './load'
import { class_path } from './constant'

const init =  function () {

 const container = new Container()
 // 通过加载,会先执行装饰器(设置元数据),
 // 再由 container 统一管理元数据中,供后续使用
 load(container, class_path)
 const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
 console.log(a);
 a.c.print() // hello
}

init()

总结

本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。

作者:我是你的超级英雄
来源:https://juejin.cn/post/7036895697865555982

收起阅读 »

300行代码实现循环滚动控件

序言在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能...
继续阅读 »

序言

在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。 由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能,

使用

使用起来很简单。把需要显示的控件放置在其中就行。就和普通的HorizontalScrollView用法一样。 不过子控件必须要LoopLinearLayout 在这里插入图片描述

效果

  • 1.支持左右循环滚动
  • 2.支持自动滚动
  • 3.支持点击事件
  • 4.触摸暂停
  • 5.支持惯性滚动
  • 6.一共不到300行代码,逻辑简单易于扩展

在这里插入图片描述

原理

通过继承自HorizontalScrollView实现,重新onOverScrolled 和 scrollTo 方法在调用supper方法之前,对是否到达边界进行判断,如果到达就调用LoopLinearLayout.changeItemsToRight() 方法对内容重新摆放。

摆放使用的是 child.layout() 的方法,没有性能问题。摆放完成以后,对scrollX进行重新赋值。

需要注意的是在HorizontalScrollView中有一个负责惯性滚动的OverScroller 在这里插入图片描述 但是在调用其fling方法之前会设置maxX这导致无法滚动到控件内容之外。所以使用反射修改了这个类。拦截了fling方法 在这里插入图片描述

而动画的时长设置的是滚动一个LoopScrollView宽度需要的时间。还有就是无限循环的动画需要在 onDetachedFromWindow中移除,避免内存泄漏

源码

LoopLinearLayout

package com.example.myapplication;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.annotation.Nullable;

import java.util.List;

/**
* Created by zhuguohui
* Date: 2021/11/30
* Time: 10:46
* Desc:
*/
public class LoopLinearLayout extends LinearLayout {
public LoopLinearLayout(Context context) {
this(context, null);
}

public LoopLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}


public void changeItemsToRight(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() - offset, childAt.getTop(), childAt.getRight() - offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()+offset2,view.getTop(),view.getRight()+offset2,view.getBottom());
}
}
public void changeItemsToLeft(List<View> moveItems, int offset) {

int offset2 = 0;
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
if (!moveItems.contains(childAt)) {
MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
childAt.layout(childAt.getLeft() + offset, childAt.getTop(), childAt.getRight() + offset, childAt.getBottom());
}
}
for(View view:moveItems){
view.layout(view.getLeft()-offset2,view.getTop(),view.getRight()-offset2,view.getBottom());
}
}


}

LoopScrollView

package com.example.myapplication;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.HorizontalScrollView;
import android.widget.OverScroller;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class LoopScrollView extends HorizontalScrollView {

private LoopScroller loopScroller;
private ValueAnimator animator;

public LoopScrollView(Context context) {
this(context, null);
}

public LoopScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
setOverScrollMode(OVER_SCROLL_ALWAYS);
try {
@SuppressLint("DiscouragedPrivateApi")
Field field =HorizontalScrollView.class.getDeclaredField("mScroller");
field.setAccessible(true);
loopScroller = new LoopScroller(getContext());
field.set(this, loopScroller);

} catch (Exception e) {
e.printStackTrace();
}

}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(changed||animator==null){
buildAnimation();
}
}

private void buildAnimation() {
if(animator!=null){
animator.cancel();
animator=null;
}
animator = ValueAnimator.ofInt(getWidth() - getPaddingRight() - getPaddingLeft());
animator.setDuration(5*1000);
animator.setRepeatCount(-1);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value= (int) animation.getAnimatedValue();
int scrollByX=value-lastValue;
// Log.i("zzz","scroll by x="+scrollByX);
scrollByX=Math.max(0,scrollByX);
if(userUp) {
scrollBy(scrollByX, 0);
}
lastValue=value;
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}

@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}

});
animator.start();
}

static class LoopScroller extends OverScroller{
public LoopScroller(Context context) {
super(context);
}

@Override
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
super.fling(startX, startY, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, minY, maxY, 0, overY);
}
}




@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if(animator!=null){
animator.cancel();
animator.removeAllListeners();
animator = null;
}
}

@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (userUp) {
//scroller再滚动
scrollX=loopScroller.getCurrX();
int detailX = scrollX - lastScrollX;
lastScrollX = scrollX;
if(detailX==0){
return;
}
scrollX = detailX + getScrollX();

}
int moveTo = moveItem(scrollX,clampedX);

super.onOverScrolled(moveTo, scrollY, false, clampedY);
}

boolean userUp = true;
int lastScrollX = 0;

@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
userUp = true;
lastScrollX = getScrollX();
} else {
userUp = false;
}
return super.onTouchEvent(ev);
}
@Override
public void scrollTo(int x, int y) {
int scrollTo = moveItem(x, false);
super.scrollTo(scrollTo, y);
}


private int moveItem(int scrollX, boolean clampedX) {

int toScrollX = scrollX;

if (getChildCount() > 0) {
if (!canScroll(scrollX,clampedX)) {
boolean toLeft=scrollX<=0;
int mWidth=getWidth()-getPaddingLeft()-getPaddingRight();
//无法向右滚动了,将屏幕外的item,移动到后面
List<View> needRemoveViewList = new ArrayList<>();
LoopLinearLayout group = (LoopLinearLayout) getChildAt(0);
int removeItemsWidth = 0;
boolean needRemove = false;
for (int i = group.getChildCount() - 1; i >= 0; i--) {
View itemView = group.getChildAt(i);
MarginLayoutParams params = (MarginLayoutParams) itemView.getLayoutParams();
if(toLeft){
int itemLeft = itemView.getLeft() - params.leftMargin;
if (itemLeft >= mWidth) {
//表示之后的控件都需要移除
needRemove = true;
}
}else{
int itemRight = itemView.getRight() + params.rightMargin;
if (itemRight <= scrollX) {
//表示之后的控件都需要移除
needRemove = true;
}
}

if (needRemove) {
int itemWidth = itemView.getWidth() + params.rightMargin + params.leftMargin;
removeItemsWidth += itemWidth;
needRemoveViewList.add(0,itemView);
}
needRemove=false;
}
if(!toLeft){
group.changeItemsToRight(needRemoveViewList,removeItemsWidth);
toScrollX -=removeItemsWidth;
}else{
group.changeItemsToLeft(needRemoveViewList,removeItemsWidth);
toScrollX +=removeItemsWidth;
}

}

}
return Math.max(0, toScrollX);
}

private boolean canScroll(int scrollX, boolean clampedX) {
if(scrollX<0){
return false;
}
if(scrollX==0&&clampedX){
//表示向左划不动了
return false;
}
View child = getChildAt(0);
if (child != null) {
int childWidth = child.getWidth();
return getWidth() + scrollX < childWidth + getPaddingLeft() + getPaddingRight();
}
return false;
}
}

最后所有的功能只依赖上述两个类,关于动画的时长写死在类中的,没有抽成方法。有需要的自己去改吧。


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

收起阅读 »

synchronized 的实现原理

synchronized 的使用 锁代码块(锁对象可指定,可为this、XXX.class、全局变量) 锁普通方法(锁对象是this,即该类实例本身) 锁静态方法(锁对象是该类,即XXX.class) 锁代码块 public class Sync { ...
继续阅读 »

synchronized 的使用



  • 锁代码块(锁对象可指定,可为this、XXX.class、全局变量)

  • 锁普通方法(锁对象是this,即该类实例本身)

  • 锁静态方法(锁对象是该类,即XXX.class)


锁代码块


public class Sync {

private int a = 0;

public void add(){
synchronized (this){
System.out.println("a values "+ ++a);
}
}

}

反编译之后的


public add()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
ALOAD 0
DUP
ASTORE 1
MONITORENTER
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "a values "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
DUP
GETFIELD com/arrom/webview/Sync.a : I
ICONST_1
IADD
DUP_X1
PUTFIELD com/arrom/webview/Sync.a : I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
ALOAD 1
MONITOREXIT
L1
GOTO L4
L2
ASTORE 2
ALOAD 1
MONITOREXIT
L3
ALOAD 2
ATHROW
L4
RETURN
MAXSTACK = 5
MAXLOCALS = 3
}

由反编译结果可以看出:synchronized代码块主要是靠MONITORENTERMONITOREXIT这两个原语来实现同步的。当线程进入MONITORENTER获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行MONITOREXIT释放锁后,其他线程才可以竞争获取锁。


MONITORENTER

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权.



  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。


第2点就涉及到了可重入锁,意思就是说当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的synchronized语句块嵌套在一起。在进入时,monitor的进入数+1;退出时就-1,直到为0的时候才可以被其他线程竞争获取。


MONITOREXIT

执行MONITOREXIT的线程必须是objectref所对应的monitor的所有者。


指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。


锁普通方法


public class Sync {

private int a = 0;

public synchronized void add(){
System.out.println("a values "+ ++a);
}

}

反编译之后并没有monitorenter和monitorexit,但是常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:


当方法调用时会检查方法的ACC_SYNCHRONIZED之后才能执行方法体,方法执行完后再释放monitor。


在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。


MONITORENTER和ACC_SYNCHRONIZED只是起标志作用,并无实质操作。


锁静态方法



private static int a = 0;

public synchronized static void add(){
System.out.println("a values "+ ++a);
}

常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。


锁的竞争过程


image.png



  • (1)、多个线程请求锁,首先进入Contention List,它可以接纳所有请求线程,而且是一个后进先出(LIFO)的虚拟队列,通过结点Node和next指针构造。

  • (2)(3)、ContentionList会被线程并发访问,EntryList为了降低线程对ContentionList队尾的争用而构造出来。当Owner释放锁时,会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head结点)为Ready Thread,也就是说某个时刻最多只有一个线程正在竞争锁。

  • (4)、Owner并不是直接把锁交给OnDeck线程,而是将竞争锁的权利交给OnDeck(将锁释放了),然后让OnDeck自己去竞争。竞争成功后,OnDeck线程就变成Owner;否则继续留在EntryList的队头。

  • (5)(6)、当线程调用wait方法被阻塞时,进入WaitSet;当其他线程调用notifyAll()(notify())方法后,阻塞队列的(某个)线程就会进入EntryList中。


      处于ContetionList、EntryList、WaitSet的线程均处于阻塞状态。而线程被阻塞涉及到用户态与内核态的切换(Liunx),系统切换严重影响锁的性能。解决这个问题的办法就是自旋。自旋就是线程不断进行内部循环,即for循环什么也不做,防止线程wait()阻塞,在自旋过程中不断尝试获取锁,如果自旋期间,Owner刚好释放锁,此时自旋线程就可以去竞争锁。如果自旋了一段时间还没获取到锁,那没办法,只能调用wait()阻塞了。 

为什么自旋了一段时间后又调用wait()方法呢?因为自旋是要消耗CPU的,而且还有线程上下文切换,因为CPU还可以调度线程,只不过执行的是空的for循环罢了。 

对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。 

所以,synchronized是什么时候进行自旋的?答案是在进入ContetionList之前,因为它自旋一定时间后还没获取锁,最后它只好在ContetionList中阻塞等待了。


对象头


对象头(Object Header)包括两部分信息。


一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。


对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。


image.png


另外一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。


为了减少锁释放带来的消耗,锁有一个升级的机制,从轻到重依次是:无锁状态 ——> 偏向锁 ——> 轻量级锁 ——>重量级锁。


偏向锁


在无其它线程与它竞争的情况下,持有偏向锁的线程永远也不需要同步。


它的加锁过程很简单:线程访问同步代码块时检查偏向锁中线程ID是否指向自己,如果是表明该线程已获得锁;否则,检测偏向锁标记是否为1,不是的话则CAS竞争锁,如果是就将对象头中线程ID指向自己。


当存在线程竞争锁时,偏向锁才会撤销,转而升级为轻量级锁。而这个撤销过程则需要有一个全局安全点(即这个时间点上没有正在执行的字节码)


image.png


在撤销锁的时候,栈中对象头的Mark Word要么偏向于其他线程,要么恢复到无锁或者轻量级锁。



  • 优点:加锁和解锁无需额外消耗

  • 缺点:锁进化时会带来额外锁撤销的消耗

  • 适用场景:只有一个线程访问同步代码块


轻量级锁


image.png



  • 优点:竞争的线程不阻塞,也就是不涉及到用户态与内核态的切换(Liunx),减少系统切换锁带来的开销

  • 缺点:如果长时间竞争不到锁,自旋会消耗CPU

  • 适用场景:追求响应时间、同步块执行速度非常快


重量级锁


它是传统意义上的锁,通过互斥量来实现同步,线程阻塞,等待Owner释放锁唤醒。




  • 优点:线程竞争不自旋,不消耗CPU




  • 缺点:线程阻塞,响应时间慢




  • 适用场景:追求吞吐量、同步块执行时间较长


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

Jetpack-Lifecycle

1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在Com...
继续阅读 »

图片.png


1.AndroidX 的ComponentActivity 实现了LifecycleOwner接口,ComponentActivity 的子类会重写LifecycleOwner的接口方法,以便得到我们使用的lifecycle对象,lifecycle 是在ComponentActivity 中创建的lifecycleRegistry对象;


2.使用的时候,通过lifecycleRegistry对象addObserver的方式注册LifecycleObserver,当生命周期变化的时候,会回调LifecycleObserver的onStateChanged, 里面的Lifecycle.Event能监听到当前Activity不同生命周期的变化


原理:


1.lifecycle注册的时候,会初始化observer的状态State,初始状态是 initial或者destroy, 将observer和state封装成一个类作为map的value值, observer作为key;


2.addObserver还有一个状态对齐,等会讲


3.当宿主activity或者fragment生命周期发生变化的时候,会分发当前的生命周期事件,转换成对应的mState,


3.1 和当前map中最早添加的observer的state进行比较,如果mState小于 state的值, 说明当前执行的是OnResume->onPause->onStop->onDestroy中的某一个环节, 遍历当前的map,将map的state向下更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.2 和当前map中最后添加的observer的state进行比较,如果mState大于 state的值, 说明当前执行的是onCreate->onStart->onResume中的某一个环节, 遍历当前的map,将map的state向上更新为当前的生命周期状态,并调用observer的onStateChange方法;


3.3 状态对齐, 比如:当我们在生命周期的onStop方法中去addObserver时,此时添加到map中的state是inital状态, 实际上当前的生命周期是onStop,对应的是Created状态, 此时需要将map中小于Created的状态更新成Created状态,因为是upEvent, 所以回调的event会有onCreate


小结: 1.创建一个state保存到map; 2.等生命周期变化时,更新state的值,回调onStateChanged方法,达到监控生命周期的作用


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

Glide数据输入输出

基础概念 在正式开始之前先明确一些概念 Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。 我们把load的不同类型称为不同的输入。 Glide输出: Glide RequestManager提供了许多...
继续阅读 »

基础概念


在正式开始之前先明确一些概念


Glide输入: 我们日常在使用Glide的时候,通过load可以加载不同的资源类型例如文件,字符串等待。


requestManager多个load重载.png


我们把load的不同类型称为不同的输入。


Glide输出: Glide RequestManager提供了许多的as重载方法,


GlideAs方法.png


通过不同的as我们可以指定不同的输出类型。


ModelLoader: 是一个泛型接口,最直观的翻译是模型加载器。ModelLoader标记了它能够加载什么类型的数据,以及加载后返回什么样的数据类型。注意这里说说的返回的数据类型并不是我们想要的输出。ModelLoader定义如下


public interface ModelLoader<Model, Data> {
 class LoadData<Data> {
     //数据加载的key
   public final Key sourceKey;
   public final List<Key> alternateKeys;
     //获取数据的接口,对应获取不同类型的数据实现
   public final DataFetcher<Data> fetcher;

   public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
     this(sourceKey, Collections.<Key>emptyList(), fetcher);
  }

   public LoadData(@NonNull Key sourceKey, @NonNull List<Key> alternateKeys,
       @NonNull DataFetcher<Data> fetcher) {
     this.sourceKey = Preconditions.checkNotNull(sourceKey);
     this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
     this.fetcher = Preconditions.checkNotNull(fetcher);
  }
}

   //创建LoadData 对象
 @Nullable
 LoadData<Data> buildLoadData(@NonNull Model model, int width, int height,
     @NonNull Options options);
   //判断当前的ModelLoader是否能够处理这个model
 boolean handles(@NonNull Model model);
}

DataFetcher: 用于进行数据加载,不同的类型有不同的DataFetcher


SourceGenerator远程数据加载过程


@Override
public boolean startNext() {
 //...
 boolean started = false;
 while (!started && hasNextModelLoader()) {
   loadData = helper.getLoadData().get(loadDataListIndex++);
   if (loadData != null
       && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
       || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
     started = true;
     loadData.fetcher.loadData(helper.getPriority(), this);
  }
}
 return started;
}

代码流程:


通过DecodeHelper获取LoadData,遍历每一个LoadData;


查看当前LoadData加载出来的数据能不能,转换成我们想要的输出数据,如果可以的话就是用当前loadData进行数据加载。


DecodeHelpe#getLoadData()


List<LoadData<?>> getLoadData() {
 if (!isLoadDataSet) {
   isLoadDataSet = true;
   loadData.clear();
     //此处的model就是我们 通过调用load传递进来的参数 即输入
   List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
   //noinspection ForLoopReplaceableByForEach to improve perf
   for (int i = 0, size = modelLoaders.size(); i < size; i++) {
     ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
     //通过modelLoader 构建loadData
     LoadData<?> current =
         modelLoader.buildLoadData(model, width, height, options);
     if (current != null) {
       loadData.add(current);
    }
  }
}
 return loadData;
}

ModelLoaderRegistry#getModelLoaders


getModelLoaders()实现的位置在ModelLoaderRegistry#getModelLoaders


public <A> List<ModelLoader<A, ?>> getModelLoaders(@NonNull A model) {
   //获取对应的modelLoader
 List<ModelLoader<A, ?>> modelLoaders = getModelLoadersForClass(getClass(model));
 int size = modelLoaders.size();
 boolean isEmpty = true;
 List<ModelLoader<A, ?>> filteredLoaders = Collections.emptyList();
 //noinspection ForLoopReplaceableByForEach to improve perf
 for (int i = 0; i < size; i++) {
   ModelLoader<A, ?> loader = modelLoaders.get(i);
     //判断对应的modelLoader是否有能力处理对应的model
   if (loader.handles(model)) {
     if (isEmpty) {
       filteredLoaders = new ArrayList<>(size - i);
       isEmpty = false;
    }
     filteredLoaders.add(loader);
  }
}
 return filteredLoaders;
}

getModelLoadersForClass主要是通过MultiModelLoaderFactory#build。然后MultiModelLoaderFactory会遍历所有已经注册的ModelLoader,只要当前的model是已经注册model的子类或者对应的实现,那么就会把对应的ModelLoader添加到待返回的集合中。


DecodeHelper#hasLoadPath


boolean hasLoadPath(Class<?> dataClass) {
   return getLoadPath(dataClass) != null;
}

<Data> LoadPath<Data, ?, Transcode> getLoadPath(Class<Data> dataClass) {
 return glideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}

可以看到hasLoadPath代码其实非常简单,就是获取一个LoadPath集合。获取的时候传递了三个参数 DataFetcher加载出来的数据类型dataClass,resourceClass ,transcodeClass


getLoadPath参数


对于resourceClass ,transcodeClass在DecodeHelper定义如下:


private Class<?> resourceClass;
private Class<Transcode> transcodeClass;

他们在init方法中进行初始化,经过层层代码的流转我们发现最终的参数初始化来自于RequestBuilder#obtainRequest


private Request obtainRequest(
     Target<TranscodeType> target,
     RequestListener<TranscodeType> targetListener,
     BaseRequestOptions<?> requestOptions,
     RequestCoordinator requestCoordinator,
     TransitionOptions<?, ? super TranscodeType> transitionOptions,
     Priority priority,
     int overrideWidth,
     int overrideHeight,
     Executor callbackExecutor) {
   return SingleRequest.obtain(
       context,
       glideContext,
       model,
       //该参数会在调用as系列方法后初始化,指向的是我们想要的输出类型。
       transcodeClass,
       //指向的是RequestBuilder 自身
       requestOptions,
       overrideWidth,
       overrideHeight,
       priority,
       target,
       targetListener,
       requestListeners,
       requestCoordinator,
       glideContext.getEngine(),
       transitionOptions.getTransitionFactory(),
       callbackExecutor);
}

而RequestOptions#getResourceClass返回的resourceClass默认情况下返回的是Object,而在asBitmap和asGifDrawable会做其它的转换。


private static final RequestOptions DECODE_TYPE_BITMAP = decodeTypeOf(Bitmap.class).lock();
private static final RequestOptions DECODE_TYPE_GIF = decodeTypeOf(GifDrawable.class).lock();

@NonNull
 @CheckResult
 public RequestBuilder<Bitmap> asBitmap() {
   return as(Bitmap.class).apply(DECODE_TYPE_BITMAP);
}
 public RequestBuilder<GifDrawable> asGif() {
   return as(GifDrawable.class).apply(DECODE_TYPE_GIF);
}

getLoadPath执行过程


getLoadPath最终会调用Registry#getLoadPath


@Nullable
public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
   @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
   @NonNull Class<Transcode> transcodeClass) {
   //先获取DecodePath  
 List<DecodePath<Data, TResource, Transcode>> decodePaths =
       getDecodePaths(dataClass, resourceClass, transcodeClass);
   if (decodePaths.isEmpty()) {
     result = null;
  } else {
     result =
         new LoadPath<>(
             dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
  }
   loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
 return result;
}

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>> getDecodePaths(
     @NonNull Class<Data> dataClass, @NonNull Class<TResource> resourceClass,
     @NonNull Class<Transcode> transcodeClass) {
   List<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();
   //遍历所有资源解码器,获取能够解析当前输入dataClass的解码器
   List<Class<TResource>> registeredResourceClasses =
       decoderRegistry.getResourceClasses(dataClass, resourceClass);
   for (Class<TResource> registeredResourceClass : registeredResourceClasses) {
       //获取能够解析当前输入dataClass且将数据转变成我们想要的transcodeClass类型的转换类
     List<Class<Transcode>> registeredTranscodeClasses =
         transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass);

     for (Class<Transcode> registeredTranscodeClass : registeredTranscodeClasses) {
//获取对应的所有解码器
       List<ResourceDecoder<Data, TResource>> decoders =
           decoderRegistry.getDecoders(dataClass, registeredResourceClass);
       //转换类
       ResourceTranscoder<TResource, Transcode> transcoder =
           transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
       @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
       DecodePath<Data, TResource, Transcode> path =
           new DecodePath<>(dataClass, registeredResourceClass, registeredTranscodeClass,
               decoders, transcoder, throwableListPool);
       decodePaths.add(path);
    }
  }
   return decodePaths;
}

整个过程涉及到两个关键类LoadPath DecodePath。


LoadPath 由数据类型datacalss 和 DecodePath组成


DecodePath 由数据类型dataclass 解码器 ResourceDecoder 集合 和资源转换 ResourceTranscoder 构成。总体上而言 一个LoadPath的存在代表着可能存在一条路径能够将ModelLoader加载出来的data解码转换成我们指定的数据类型。


DocodeJob数据解码的过程


Glide DecodeJob 的工作过程我们知道SourceGenerator在数据加载完成之后如果允许缓存原始数据会再次执行SourceGenerator#startNext将加载的数据进行缓存,然后通过DataCacheGenerator从缓存文件中获取。最终获取数据成功后会调用DocodeJob#onDataFetcherReady


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

大厂面试Kafka,一定会问到的幂等性

01 幂等性如此重要 Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。 02 哪些因素影响幂等性...
继续阅读 »

01 幂等性如此重要


Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。

02 哪些因素影响幂等性


使用Kafka时,需要保证exactly-once语义。要知道在分布式系统中,出现网络分区是不可避免的,如果kafka broker 在回复ack时,出现网络故障或者是full gc导致ack timeout,producer将会重发,如何保证producer重试时不造成重复or乱序?又或者producer 挂了,新的producer并没有old producer的状态数据,这个时候如何保证幂等?即使Kafka 发送消息满足了幂等,consumer拉取到消息后,把消息交给线程池workers,workers线程对message的处理可能包含异步操作,又会出现以下情况:


  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失




  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行




  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失




本文将针对以上问题作出讨论


03 Kafka保证发送幂等性


       针对以上的问题,kafka在0.11版新增了幂等型producer和事务型producer。前者解决了单会话幂等性等问题,后者解决了多会话幂等性。


单会话幂等性


为解决producer重试引起的乱序和重复。Kafka增加了pid和seq。Producer中每个RecordBatch都有一个单调递增的seq; Broker上每个tp也会维护pid-seq的映射,并且每Commit都会更新lastSeq。这样recordBatch到来时,broker会先检查RecordBatch再保存数据:如果batch中 baseSeq(第一条消息的seq)比Broker维护的序号(lastSeq)大1,则保存数据,否则不保存(inSequence方法)。

ProducerStateManager.scala


    private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {

    validationType match {

    case ValidationType.None =>

    case ValidationType.EpochOnly =>

    checkProducerEpoch(producerEpoch, offset)

    case ValidationType.Full =>

    checkProducerEpoch(producerEpoch, offset)

    checkSequence(producerEpoch, firstSeq, offset)

    }

    }

    private def checkSequence(producerEpoch: Short, appendFirstSeq: Int, offset: Long): Unit = {

    if (producerEpoch != updatedEntry.producerEpoch) {

    if (appendFirstSeq != 0) {

    if (updatedEntry.producerEpoch != RecordBatch.NO_PRODUCER_EPOCH) {

    throw new OutOfOrderSequenceException(s"Invalid sequence number for new epoch at offset $offset in " +

    s"partition $topicPartition: $producerEpoch (request epoch), $appendFirstSeq (seq. number)")

    } else {

    throw new UnknownProducerIdException(s"Found no record of producerId=$producerId on the broker at offset $offset" +

    s"in partition $topicPartition. It is possible that the last message with the producerId=$producerId has " +

    "been removed due to hitting the retention limit.")

    }

    }

    } else {

    val currentLastSeq = if (!updatedEntry.isEmpty)

    updatedEntry.lastSeq

    else if (producerEpoch == currentEntry.producerEpoch)

    currentEntry.lastSeq

    else

    RecordBatch.NO_SEQUENCE

    if (currentLastSeq == RecordBatch.NO_SEQUENCE && appendFirstSeq != 0) {

ne throw mew UnknownProducerIdException(s"Local producer state matches expected epoch $producerEpoch " +

    s"for producerId=$producerId at offset $offset in partition $topicPartition, but the next expected " +

    "sequence number is not known.")

    } else if (!inSequence(currentLastSeq, appendFirstSeq)) {

    throw new OutOfOrderSequenceException(s"Out of order sequence number for producerId $producerId at " +

    s"offset $offset in partition $topicPartition: $appendFirstSeq (incoming seq. number), " +

    s"$currentLastSeq (current end sequence number)")

    }

    }

    }

    private def inSequence(lastSeq: Int, nextSeq: Int): Boolean = {

    nextSeq == lastSeq + 1L || (nextSeq == 0 && lastSeq == Int.MaxValue)

    }



引申:Kafka producer 对有序性做了哪些处理


假设我们有5个请求,batch1、batch2、batch3、batch4、batch5;如果只有batch2 ack failed,3、4、5都保存了,那2将会随下次batch重发而造成重复。我们可以设置max.in.flight.requests.per.connection=1(客户端在单个连接上能够发送的未响应请求的个数)来解决乱序,但降低了系统吞吐。
新版本kafka设置enable.idempotence=true后能够动态调整max-in-flight-request。正常情况下max.in.flight.requests.per.connection 大于1。当重试请求到来且时,batch 会根据 seq重新添加到队列的合适位置,并把max.in.flight.requests.per.connection设为1, 这样它 前面的 batch序号都比它小,只有前面的都发完了,它才能发。

    private void insertInSequenceOrder(Deque<ProducerBatch> deque, ProducerBatch batch) {

    // When we are requeing and have enabled idempotence, the reenqueued batch must always have a sequence.

    if (batch.baseSequence() == RecordBatch.NO_SEQUENCE)

    throw new IllegalStateException("Trying to re-enqueue a batch which doesn't have a sequence even " +

    "though idempotency is enabled.");

    if (transactionManager.nextBatchBySequence(batch.topicPartition) == null)

    throw new IllegalStateException("We are re-enqueueing a batch which is not tracked as part of the in flight " +

    "requests. batch.topicPartition: " + batch.topicPartition + "; batch.baseSequence: " + batch.baseSequence());

    ProducerBatch firstBatchInQueue = deque.peekFirst();

    if (firstBatchInQueue != null && firstBatchInQueue.hasSequence() && firstBatchInQueue.baseSequence() < batch.baseSequence()) {

    List<ProducerBatch> orderedBatches = new ArrayList<>();

    while (deque.peekFirst() != null && deque.peekFirst().hasSequence() && deque.peekFirst().baseSequence() < batch.baseSequence())

    orderedBatches.add(deque.pollFirst());

    log.debug("Reordered incoming batch with sequence {} for partition {}. It was placed in the queue at " +

    "position {}", batch.baseSequence(), batch.topicPartition, orderedBatches.size())

    deque.addFirst(batch);

    // Now we have to re insert the previously queued batches in the right order.

    for (int i = orderedBatches.size() - 1; i >= 0; --i) {

    deque.addFirst(orderedBatches.get(i));

    }

    // At this point, the incoming batch has been queued in the correct place according to its sequence.

    } else {

    deque.addFirst(batch);

    }

    }


多会话幂等性


在单会话幂等性中介绍,kafka通过引入pid和seq来实现单会话幂等性,但正是引入了pid,当应用重启时,新的producer并没有old producer的状态数据。可能重复保存。

Kafka事务通过隔离机制来实现多会话幂等性


kafka事务引入了transactionId 和Epoch,设置transactional.id后,一个transactionId只对应一个pid, 且Server 端会记录最新的 Epoch 值。这样有新的producer初始化时,会向TransactionCoordinator发送InitPIDRequest请求, TransactionCoordinator 已经有了这个 transactionId对应的 meta,会返回之前分配的 PID,并把 Epoch 自增 1 返回,这样当old
producer恢复过来请求操作时,将被认为是无效producer抛出异常。     如果没有开启事务,TransactionCoordinator会为新的producer返回new pid,这样就起不到隔离效果,因此无法实现多会话幂等。

    private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {

    validationType match {

    case ValidationType.None =>

    case ValidationType.EpochOnly =>

    checkProducerEpoch(producerEpoch, offset)

    case ValidationType.Full => //开始事务,执行这个判断

    checkProducerEpoch(producerEpoch, offset)

    checkSequence(producerEpoch, firstSeq, offset)

    }

    }

    private def checkProducerEpoch(producerEpoch: Short, offset: Long): Unit = {

    if (producerEpoch < updatedEntry.producerEpoch) {

    throw new ProducerFencedException(s"Producer's epoch at offset $offset is no longer valid in " +

    s"partition $topicPartition: $producerEpoch (request epoch), ${updatedEntry.producerEpoch} (current epoch)")

    }

    }


04 Consumer端幂等性


如上所述,consumer拉取到消息后,把消息交给线程池workers,workers对message的handle可能包含异步操作,又会出现以下情况:


  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失




  • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行




  • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失




对此我们常用的方法时,works取到消息后先执行如下code:


    if(cache.contain(msgId)){

    // cache中包含msgId,已经处理过

    continue;

    }else {

    lock.lock();

    cache.put(msgId,timeout);

    commitSync();

    lock.unLock();

    }

    // 后续完成所有操作后,删除cache中的msgId,只要msgId存在cache中,就认为已经处理过。Note:需要给cache设置有消息


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

Toast必须在UI(主)线程使用?

背景 依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。 非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。 直至前两天我的朋友 “林小海” 同学说toast不能在子线程中...
继续阅读 »

背景


依稀记得,从最开始干Android这一行就经常听到有人说:toast(吐司)不能在子线程调用显示,只能在UI(主)线程调用展示。


非常惭愧的是,我之前也这么认为,并且这个问题也一直没有深究。


直至前两天我的朋友 “林小海” 同学说toast不能在子线程中显示,这句话使我突然想起了点什么。


我觉得我有必要证明、并且纠正一下。


toast不能在子线程调用展示的结论真的是谬论~


疑点


前两天在说到这个toast的时候一瞬间对于只能在UI线程中调用展示的说法产生了两个疑点:




  1. 在子线程更新UI一般都会有以下报错提示:


    Only the original thread that created a view hierarchy can touch its views.


    但是,我们在子线程直接toast的话,报错的提示如下:


    Can't toast on a thread that has not called Looper.prepare()


    明显,两个报错信息是不一样的,从toast这条来看的话是指不能在一个没有调用Looper.prepare()的线程里面进行toast,字面意思上有说是不能在子线程更新UI吗?No,没有!这也就有了下面第2点的写法。




  2. 曾见过一种在子线程使用toast的用法如下(正是那时候没去深究这个问题):




        new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(MainActivity.this.getApplicationContext(),"SHOW",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();

关于Looper这个东西,我想大家都很熟悉了,我就不多说looper这块了,下面主要分析一下为什么这样的写法就可以在子线程进行toast了呢?


并且Looper.loop()这个函数调用后是会阻塞轮循的,这种写法是会导致线程没有及时销毁,在toast完之后我特意给大家用如下代码展示一下这个线程的状态:


    Log.d("Wepon", "isAlive:"+t[0].isAlive());
Log.d("Wepon", "state:" + t[0].getState());

D/Wepon: isAlive:true
D/Wepon: state:RUNNABLE

可以看到,线程还活着,没有销毁掉。当然,这种代码里面如果想要退出looper的循环以达到线程可以正常销毁的话是可以使用looper.quit相关的函数的,但是这个调用quit的时机却是不好把握的。


下面将通过Toast相关的源码来分析一下为什么会出现上面的情况?


源码分析


Read the fuck source code.


1.分析Toast.makeText()方法


首先看我们的调用Toast.makeText,makeText这个函数的源码:


    // 这里一般是我们外部调用Toast.makeText(this, "xxxxx", Toast.LENGTH_SHORT)会进入的方法。
// 然后会调用下面的函数。
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}

/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used. // 1. 注意这一句话
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {

// 2. 构造toast实例,有传入looper,此处looper为null
Toast result = new Toast(context, looper);

LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);

result.mNextView = v;
result.mDuration = duration;

return result;
}

从上面的源码中看第1点注释,looper为null的时候会调用Looper.myLooper(),这个方法的作用是取我们线程里面的looper对象,这个调用是在Toast的构造函数里面发生的,看我们的Toast构造函数:


2.分析Toast构造函数


    /**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
// 1.此处创建一个TN的实例,传入looper,接下来主要分析一下这个TN类
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}

TN的构造函数如下,删除了一部分不重要代码:


TN(String packageName, @Nullable Looper looper) {
// .....
// ..... 省略部分源码,这
// .....

// 重点
// 2.判断looper == null,这里我们从上面传入的时候就是null,所以会进到里面去。
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
// 3.然后会调用Looper.myLooper这个函数,也就是会从ThreadLocal<Looper> sThreadLocal 去获取当前线程的looper。
// 如果ThreadLocal这个不太清楚的可以先去看看handler源码分析相关的内容了解一下。
looper = Looper.myLooper();
if (looper == null) {
// 4.这就是报错信息的根源点了!!
// 没有获取到当前线程的looper的话,就会抛出这个异常。
// 所以分析到这里,就可以明白为什么在子线程直接toast会抛出这个异常
// 而在子线程中创建了looper就不会抛出异常了。
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
// 5.这里不重点讲toast是如何展示出来的源码了,主要都在TN这个类里面,
// Toast与TN中间有涉及aidl跨进程的调用,这些可以看看源码。
// 大致就是:我们的show方法实际是会往这个looper里面放入message的,
// looper.loop()会阻塞、轮循,
// 当looper里面有Message的时候会将message取出来,
// 然后会通过handler的handleMessage来处理。
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
// .... 省略代码
case SHOW: // 显示,与WindowManager有关,这部分源码不做说明了,可以自己看看,就在TN类里面。
case HIDE: // 隐藏
case CANCEL: // 取消
}
}
};
}

总结


从第1点可以看到会创建TN的实例,并传入looper,此时的looper还是null。


进入TN的构造函数可以看到会有looper是否为null的判断,并且在当looper为null时,会从当前线程去获取looper(第3点,Looper.myLooper()),如果还是获取不到,刚会抛出我们开头说的这个异常信息:Can't toast on a thread that has not called Looper.prepare()。


而有同学会误会只能在UI线程toast的原因是:UI(主)线程在刚创建的时候就有创建looper的实例了,在主线程toast的话,会通过Looper.myLooper()获取到对应的looper,所以不会抛出异常信息。


而不能直接在子线程程中toast的原因是:子线程中没有创建looper的话,去通过Looper.myLooper()获取到的为null,就会throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");


另外,两个点说明一下:



  1. Looper.prepare() 是创建一个looper并通过ThreadLocal跟当前线程关联上,也就是通过sThreadLocal.set(new Looper(quitAllowed));

  2. Looper.loop()是开启轮循,有消息就会处理,没有的话就会阻塞。


综上,“Toast必须在UI(主)线程使用”这个说法是不对滴!,以后千万不要再说toast只能在UI线程显示啦.....


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

手把手教你用Flutter搭建属于自己的个人博客

Flutter 2.0以来已经稳定支持web的开发,现在来教大家使用Flutter搭建一个个人的博客网站,使用Github提供的Actions、gh-pages服务,毕竟一时白票一时爽,一直白嫖一直爽。 1. 使用AndoridStuido创建一个Flutte...
继续阅读 »

Flutter 2.0以来已经稳定支持web的开发,现在来教大家使用Flutter搭建一个个人的博客网站,使用Github提供的Actions、gh-pages服务,毕竟一时白票一时爽,一直白嫖一直爽。


1. 使用AndoridStuido创建一个Flutter项目


Dingtalk_20210512195651.jpg


2. Github注册一个账号,并且创建一个Repository


Dingtalk_20210512195915.jpg


3. 上传创建的Flutter项目到这个Repository的master分支中


4. 获取Github的access token


新建一个access token
Dingtalk_20210512200242.jpg
保存token,等下要用


Dingtalk_20210512200516.jpg


5. 配置Actions secrets,name随便填写,value填入刚刚获取的token


Dingtalk_20210512200640.jpg


6.配置Actions


Dingtalk_20210512200843.jpg


Dingtalk_20210512201013.jpg


需要填写的规则


name: Flutter Web
on:
push:
branches:
- master
jobs:
build:
name: Build Web
env:
my_secret: ${{secrets.commit_secret}}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: subosito/flutter-action@v1
with:
channel: 'dev'
- run: flutter pub get
- run: flutter build web --release
- run: |
cd build/web
git init
git config --global user.email aaa
git config --global user.name bbb
git status
git remote add origin https://${{secrets.commit_secret}}@github.com/xxx/yyy.git
git checkout -b gh-pages
git add --all
git commit -m "update"
git push origin gh-pages -f

aaa-你的邮箱 bbb替-你的名称 xxx-你的git名字 yyy为-Repository名字


然后我们每次提交修改到master上时,Actions都会自动帮我们打包web到gh-pages分支上,完成Actions后,我们可以查看flutter构建的博客网站,一般网址为https://你的git名字.github.io/Repository名字/。


这里记得注意的是需要修改web目录下index.html中


<base href="/">
修改为Repository的名字
<base href="/flutter_blog/">

不然在打开网页的时候会找不到资源。


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

Python对象的浅拷贝与深拷贝

在讲我们深浅拷贝之前,我们需要先区分一下拷贝和赋值的概念。看下面的例子a = [1,2,3]赋值:b = a拷贝:b = a.copy()上面的两行代码究竟有什么不同呢?带着这个问题,继续 看了上面这张图,相信大家已经对直接赋值和拷贝有了一个比较清楚的认识...
继续阅读 »

在讲我们深浅拷贝之前,我们需要先区分一下拷贝和赋值的概念。看下面的例子

a = [1,2,3]

赋值:

b = a

拷贝:

b = a.copy()

上面的两行代码究竟有什么不同呢?带着这个问题,继续

Python对象的浅拷贝与深拷贝_递归



看了上面这张图,相信大家已经对直接赋值和拷贝有了一个比较清楚的认识。

直接赋值:复制一个对象的引用给新变量
拷贝:复制一个对象到新的内存地址空间,并且将新变量引用到复制后的对象

我们的深浅拷贝只是对于可变对象来讨论的。 不熟悉的朋友需要自己去了解可变对象与不可变对象哦。

1 对象的嵌套引用

a = { "list": [1,2,3] }

上面的代码,在内存中是什么样子的呢?请看下图:

Python对象的浅拷贝与深拷贝_递归_02



原来,在我们的嵌套对象中,子对象也是一个引用。

2 浅拷贝

Python对象的浅拷贝与深拷贝_python_03



如上图所示,我们就可以很好的理解什么叫做浅拷贝了。

浅拷贝:只拷贝父对象,不会拷贝对象的内部的子对象。内部的子对象指向的还是同一个引用

上面 的 a 和 c 是一个独立的对象,但他们的子对象还是指向统一对象

2.1 浅拷贝的方法

  • .copy()

a = {"list": [1,2,3] }
b = a.copy()
  • copy模块

import copy
a = {"list": [1,2,3] }
b = copy.copy(a)
  • 列表切片[:]

a = [1,2,3,[1,2,3]]
b = a[1:]
  • for循环

a = [1,2,3,[1,2,3]]
b = []
for i in a:
  b.append(i)

2.2 浅拷贝的影响

a = {"list":[1,2,3]}
b = a.copy()
a["list"].append(4)

print(a)
# {'list': [1, 2, 3, 4]}

print(b)
# {'list': [1, 2, 3, 4]}

在上面的例子中,我们明明只改变 a 的子对象,却发现 b 的子对象也跟着改变了。这样在我们的程序中也许会引发很多的BUG。

3 深拷贝

上面我们知道了什么是浅拷贝,那我们的深拷贝就更好理解了。

Python对象的浅拷贝与深拷贝_python_04



深拷贝:完全拷贝了父对象及其子对象,两者已经完成没有任何关联,是完全独立的。

import copy
a = {"list":[1,2,3]}
b = copy.deepcopy(a)
a["list"].append(4)

print(a)
# {'list': [1, 2, 3, 4]}

print(b)
# {'list': [1, 2, 3,]}

上面的例子中,我们再次修改 a 的子对象对 b 已经没有任何影响

4 手动实现一个深拷贝

主要采用递归的方法解决问题。判断拷贝的每一项子对象是否为引用对象。如果是就采用递归的方式将子对象进行复制。

def deepcopy(instance):
  if isinstance(instance, dict):
      return {k:deepcopy(v) for k,v in instance.items() }
   
  elif isinstance(instance, list):
      return [deepcopy(x) for x in instance]
   
  else:
      return instance

a = {"list": [1,2,3]}
b = deepcopy(a)

print(a)
# {'list': [1, 2, 3]}

print(b)
# {'list': [1, 2, 3]}

a["list"].append(4)
print(a)
# {'list': [1, 2, 3, 4]}

print(b)
# {'list': [1, 2, 3]}

创作不易,且读且珍惜。如有错漏还请海涵并联系作者修改,内容有参考,如有侵权,请联系作者删除。如果文章对您有帮助,还请动动小手,您的支持是我最大的动力。

作者:趣玩Python
来源:https://blog.51cto.com/u_14666251/4716452 收起阅读 »

手写迷你版Vue

手写迷你版Vue参考代码:github.com/57code/vue-…Vue响应式设计思路Vue响应式主要包含:数据响应式监听数据变化,并在视图中更新Vue2使用Object.defineProperty实现数据劫持Vu3使用Proxy实现数据劫持模板引擎提...
继续阅读 »




手写迷你版Vue

参考代码:github.com/57code/vue-…

Vue响应式设计思路

Vue响应式主要包含:

  • 数据响应式

  • 监听数据变化,并在视图中更新

  • Vue2使用Object.defineProperty实现数据劫持

  • Vu3使用Proxy实现数据劫持

  • 模板引擎

  • 提供描述视图的模板语法

  • 插值表达式{{}}

  • 指令 v-bind, v-on, v-model, v-for,v-if

  • 渲染

  • 将模板转换为html

  • 解析模板,生成vdom,把vdom渲染为普通dom

数据响应式原理

image.png

数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty实现数据变化的检测

原理解析

  • new Vue()⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer

  • 同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在

Compile

  • 同时定义⼀个更新函数和Watcher实例,将来对应数据变化时,Watcher会调⽤更新函数

  • 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个

Watcher

  • 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数

image.png

一些关键类说明

CVue:自定义Vue类 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher:执⾏更新函数(更新dom) Dep:管理多个Watcher实例,批量更新

涉及关键方法说明

observe: 遍历vm.data的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例进行真正响应式处理

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.0">
<title>cvue</title>
<script src="./cvue.js"></script>
</head>
<body>
<div id="app">
  <p>{{ count }}</p>
</div>

<script>
  const app = new CVue({
    el: '#app',
    data: {
      count: 0
    }
  })
  setInterval(() => {
    app.count +=1
  }, 1000);
</script>
</body>
</html>

CVue

  • 创建基本CVue构造函数:

  • 执⾏初始化,对data执⾏响应化处理

// 自定义Vue类
class CVue {
constructor(options) {
  this.$options = options
  this.$data = options.data

  // 响应化处理
  observe(this.$data)
}
}

// 数据响应式, 修改对象的getter,setter
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
Object.defineProperty(obj, key, {
  get() {
    return val
  },
  set(newVal) {
    if(val !== newVal) {
      console.log(`set ${key}:${newVal}, old is ${val}`)

      val = newVal
      // 继续进行响应式处理,处理newVal是对象情况
      observe(val)
    }
  }
})
}

// 遍历obj,对其所有属性做响应式
function observe(obj) {
// 只处理对象类型的
if(typeof obj !== 'object' || obj == null) {
  return
}
// 实例化Observe实例
new Observe(obj)
}

// 根据传入value的类型做相应的响应式处理
class Observe {
constructor(obj) {
  if(Array.isArray(obj)) {
    // TODO
  } else {
    // 对象
    this.walk(obj)
  }
}
walk(obj) {
  // 遍历obj所有属性,调用defineReactive进行响应化
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}

为vm.$data做代理

方便实例上设置和获取数据

例如

原本应该是

vm.$data.count
vm.$data.count = 233

代理之后后,可以使用如下方式

vm.count
vm.count = 233

给vm.$data做代理

class CVue {
constructor(options) {
  // 省略
  // 响应化处理
  observe(this.$data)

  // 代理data上属性到实例上
  proxy(this)
}
}

// 把CVue实例上data对象的属性到代理到实例上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
  Object.defineProperty(vm, key, {
    get() {
      // 实现 vm.count 取值
      return vm.$data[key]
    },
    set(newVal) {
      // 实现 vm.count = 123赋值
      vm.$data[key] = newVal
    }
  })
})
}

编译

image.png

初始化视图

根据节点类型进行编译
class CVue {
constructor(options) {
  // 省略。。
  // 2 代理data上属性到实例上
  proxy(this)

  // 3 编译
  new Compile(this, this.$options.el)
}
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
  this.$vm = vm
  this.$el = document.querySelector(el)

  if(this.$el) {
    this.complie(this.$el)
  }
}
// 编译
complie(el) {
  // 取出所有子节点
  const childNodes = el.childNodes
  // 遍历节点,进行初始化视图
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      // TODO
      console.log(`编译元素 ${node.nodeName}`)
    } else if(this.isInterpolation(node)) {
      console.log(`编译插值文本 ${node.nodeName}`)
    }
    // 递归编译,处理嵌套情况
    if(node.childNodes) {
      this.complie(node)
    }
  })
}
// 是元素节点
isElement(node) {
  return node.nodeType === 1
}
// 是插值表达式
isInterpolation(node) {
  return node.nodeType === 3
    && /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值表达式
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      console.log(`编译元素 ${node.nodeName}`)
    } else if(this.isInterpolation(node)) {
      // console.log(`编译插值文本 ${node.textContent}`)
      this.complieText(node)
    }
    // 省略
  })
}
// 是插值表达式
isInterpolation(node) {
  return node.nodeType === 3
    && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
complieText(node) {
  // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
  // 相等于{{ count }}中的count
  const exp = String(RegExp.$1).trim()
  node.textContent = this.$vm[exp]
}
}
编译元素节点和指令

需要取出指令和指令绑定值 使用数据更新视图

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      console.log(`编译元素 ${node.nodeName}`)
      this.complieElement(node)
    }
    // 省略
  })
}
// 是元素节点
isElement(node) {
  return node.nodeType === 1
}
// 编译元素
complieElement(node) {
  // 取出元素上属性
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    // c-text="count"中c-text是attr.name,count是attr.value
    const { name: attrName, value: exp } = attr
    if(this.isDirective(attrName)) {
      // 取出指令
      const dir = attrName.substring(2)
      this[dir] && this[dir](node, exp)
    }
  })
}
// 是指令
isDirective(attrName) {
  return attrName.startsWith('')
}
// 处理c-text文本指令
text(node, exp) {
  node.textContent = this.$vm[exp]
}
// 处理c-html指令
html(node, exp) {
  node.innerHTML = this.$vm[exp]
}
}

以上完成初次渲染,但是数据变化后,不会触发页面更新

依赖收集

视图中会⽤到data中某key,这称为依赖。 同⼀个key可能出现多次,每次出现都需要收集(⽤⼀个Watcher来维护维护他们的关系),此过程称为依赖收集。 多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。

image.png

  • data中的key和dep是一对一关系

  • 视图中key出现和Watcher关系,key出现一次就对应一个Watcher

  • dep和Watcher是一对多关系

实现思路

  • defineReactive中为每个key定义一个Dep实例

  • 编译阶段,初始化视图时读取key, 会创建Watcher实例

  • 由于读取过程中会触发key的getter方法,便可以把Watcher实例存储到key对应的Dep实例

  • 当key更新时,触发setter方法,取出对应的Dep实例Dep实例调用notiy方法通知所有Watcher更新

定义Watcher类

监听器,数据变化更新对应节点视图

// 创建Watcher监听器,负责更新视图
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
  this.$vm = vm
  this.$key = key
  this.$updateFn = updateFn
}
update() {
  // 调用更新函数,获取最新值传递进去
  this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
// 省略。。。
// 编译插值
complieText(node) {
  // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
  // 相等于{{ count }}中的count
  const exp = String(RegExp.$1).trim()
  // node.textContent = this.$vm[exp]
  this.update(node, exp, 'text')
}
// 处理c-text文本指令
text(node, exp) {
  // node.textContent = this.$vm[exp]
  this.update(node, exp, 'text')
}
// 处理c-html指令
html(node, exp) {
  // node.innerHTML = this.$vm[exp]
  this.update(node, exp, 'html')
}
// 更新函数
update(node, exp, dir) {
  const fn = this[`${dir}Updater`]
  fn && fn(node, this.$vm[exp])

  // 创建监听器
  new Watcher(this.$vm, exp, function(newVal) {
    fn && fn(node, newVal)
  })
}
// 文本更新器
textUpdater(node, value) {
  node.textContent = value
}
// html更新器
htmlUpdater(node, value) {
  node.innerHTML = value
}
}
定义Dep类
  • data的一个属性对应一个Dep实例

  • 管理多个Watcher实例,通知所有Watcher实例更新

// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {
constructor() {
  this.deps = []
}
// 添加Watcher实例
addDep(dep) {
  this.deps.push(dep)
}
notify() {
  // 通知所有Wather更新视图
  this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
  // 省略
  // 把Wather实例临时挂载在Dep.target上
  Dep.target = this
  // 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
  this.$vm[key]
  // 添加后,重置Dep.target
  Dep.target = null
}
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)

const dep = new Dep()
Object.defineProperty(obj, key, {
  get() {
    Dep.target && dep.addDep(Dep.target)
    return val
  },
  set(newVal) {
    if(val !== newVal) {
      val = newVal
      // 继续进行响应式处理,处理newVal是对象情况
      observe(val)
      // 更新视图
      dep.notify()
    }
  }
})
}

监听事件指令@xxx

  • 在创建vue实例时,需要缓存methods到vue实例上

  • 编译阶段取出methods挂载到Compile实例上

  • 编译元素时

  • 识别出v-on指令时,进行事件的绑定

  • 识别出@属性时,进行事件绑定

  • 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用bind修改监听函数的this指向为组件实例

// 自定义Vue类
class CVue {
constructor(options) {
  this.$methods = options.methods
}
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
  this.$vm = vm
  this.$el = document.querySelector(el)
  this.$methods = vm.$methods
}

// 编译元素
complieElement(node) {
  // 取出元素上属性
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    // c-text="count"中c-text是attr.name,count是attr.value
    const { name: attrName, value: exp } = attr
    if(this.isDirective(attrName)) {
      // 省略。。。
      if(this.isEventListener(attrName)) {
        // v-on:click, subStr(5)即可截取到click
        const eventType = attrName.substring(5)
        this.bindEvent(eventType, node, exp)
      }
    } else if(this.isEventListener(attrName)) {
      // @click, subStr(1)即可截取到click
      const eventType = attrName.substring(1)
      this.bindEvent(eventType, node, exp)
    }
  })
}
// 是事件监听
isEventListener(attrName) {
  return attrName.startsWith('@') || attrName.startsWith('c-on')
}
// 绑定事件
bindEvent(eventType, node, exp) {
  // 取出表达式对应函数
  const method = this.$methods[exp]
  // 增加监听并修改this指向当前组件实例
  node.addEventListener(eventType, method.bind(this.$vm))
}
}

v-model双向绑定

实现v-model绑定input元素时的双向绑定功能

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
// 省略...
// 处理c-model指令
model(node, exp) {
  // 渲染视图
  this.update(node, exp, 'model')
  // 监听input变化
  node.addEventListener('input', (e) => {
    const { value } = e.target
    // 更新数据,相当于this.username = 'mio'
    this.$vm[exp] = value
  })
}
// model更新器
modelUpdater(node, value) {
  node.value = value
}
}

数组响应式

  • 获取数组原型

  • 数组原型创建对象作为数组拦截器

  • 重写数组的7个方法

// 数组响应式
// 获取数组原型, 后面修改7个方法
const originProto = Array.prototype
// 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
const arrayProto = Object.create(originProto)
// 拦截数组方法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
// 在备份的原型上做修改
arrayProto[method] = function() {
  // 调用原始操作
  originProto[method].apply(this, arguments)
  // 发出变更通知
  console.log(`method:${method} value:${Array.from(arguments)}`)
}
})

class Observe {
constructor(obj) {
  if(Array.isArray(obj)) {
    // 修改数组原型为自定义的
    obj.__proto__ = arrayProto
    this.observeArray(obj)
  } else {
    // 对象
    this.walk(obj)
  }
}
observeArray(items) {
  // 如果数组内部元素时对象,继续做响应化处理
  items.forEach(item => observe(item))
}
}

作者:LastStarDust
来源:https://juejin.cn/post/7036291383153393701

收起阅读 »

LRU缓存-keep-alive实现原理

相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive 这个内置组件。 keep-alive 是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不...
继续阅读 »



前言

相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive 这个内置组件。

那么什么是 keep-alive 呢?

keep-alive 是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。简单的说,keep-alive用于保存组件的渲染状态,避免组件反复创建和渲染,有效提升系统性能。 keep-alivemax 属性,用于限制可以缓存多少组件实例,一旦这个数字达到了上限,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉,而这里所运用到的缓存机制就是 LRU 算法

LRU 缓存淘汰算法

LRU( least recently used)根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰现阶段最久未被访问的数据

LRU的主体思想在于:如果数据最近被访问过,那么将来被访问的几率也更高

fifo对比lru原理

  1. 新数据插入到链表尾部;

  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表尾部

  3. 当链表满的时候,将链表头部的数据丢弃。

实现LRU的数据结构

经典的 LRU 一般都使用 hashMap + 双向链表。考虑可能需要频繁删除一个元素,并将这个元素的前一个节点指向下一个节点,所以使用双链接最合适。并且它是按照结点最近被使用的时间顺序来存储的。 如果一个结点被访问了, 我们有理由相信它在接下来的一段时间被访问的概率要大于其它结点。

map.keys()

不过既然已经在 js 里都已经使用 Map 了,何不直接取用现成的迭代器获取下一个结点的 key 值(keys().next( )

// ./LRU.ts
export class LRUCache {
capacity: number; // 容量
cache: Map; // 缓存
constructor(capacity: number) {
  this.capacity = capacity;
  this.cache = new Map();
}
get(key: number): number {
  if (this.cache.has(key)) {
    let temp = this.cache.get(key) as number;
    //访问到的 key 若在缓存中,将其提前
    this.cache.delete(key);
    this.cache.set(key, temp);
    return temp;
  }
  return -1;
}
put(key: number, value: number): void {
  if (this.cache.has(key)) {
    this.cache.delete(key);
    //存在则删除,if 结束再提前
  } else if (this.cache.size >= this.capacity) {
    // 超过缓存长度,淘汰最近没使用的
    this.cache.delete(this.cache.keys().next().value);
    console.log(`refresh: key:${key} , value:${value}`)
  }
  this.cache.set(key, value);
}
toString(){
  console.log('capacity',this.capacity)
  console.table(this.cache)
}
}
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2)   // 2,剩余容量3
list.put(3,3)   // 3,剩余容量2
list.put(4,4)   // 4,剩余容量1
list.put(5,5)   // 5,已满   从头至尾         2-3-4-5
list.put(4,4)   // 入4,已存在 ——> 置队尾         2-3-5-4
list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3)     // 获取3,刷新3——> 置队尾         5-4-1-3
list.toString()
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)

list.put(2,2)   // 2,剩余容量3
list.put(3,3)   // 3,剩余容量2
list.put(4,4)   // 4,剩余容量1
list.put(5,5)   // 5,已满   从头至尾 2-3-4-5
list.put(4,4)   // 入4,已存在 ——> 置队尾 2-3-5-4
list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3)     // 获取3,刷新3——> 置队尾 5-4-1-3
list.toString()

结果如下: lru打印结果.jpg

vue 中 Keep-Alive

原理

  1. 使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量

  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例

  3. 根据组件 ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)

  4. 获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称

  5. 获取 keep-alive 包裹着的第一个子组件对象及其组件名

源码分析

初始化 keepAlive 组件
const KeepAliveImpl: ComponentOptions = {
 name: `KeepAlive`,
 props: {
   include: [String, RegExp, Array],
   exclude: [String, RegExp, Array],
   max: [String, Number],
},
 setup(props: KeepAliveProps, { slots }: SetupContext) {
   // 初始化数据
   const cache: Cache = new Map();
   const keys: Keys = new Set();
   let current: VNode | null = null;
   // 当 props 上的 include 或者 exclude 变化时移除缓存
   watch(
    () => [props.include, props.exclude],
    ([include, exclude]) => {
     include && pruneCache((name) => matches(include, name));
     exclude && pruneCache((name) => !matches(exclude, name));
    },
    { flush: "post", deep: true }
  );
   // 缓存组件的子树 subTree
   let pendingCacheKey: CacheKey | null = null;
   const cacheSubtree = () => {
     // fix #1621, the pendingCacheKey could be 0
     if (pendingCacheKey != null) {
       cache.set(pendingCacheKey, getInnerChild(instance.subTree));
    }
  };
   // KeepAlive 组件的设计,本质上就是空间换时间。
   // 在 KeepAlive 组件内部,
   // 当组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
   onMounted(cacheSubtree);
   onUpdated(cacheSubtree);
   onBeforeUnmount(() => {
   // 卸载缓存表里的所有组件和其中的子树...
  }
   return ()=>{
     // 返回 keepAlive 实例
  }
}
}

return ()=>{
 // 省略部分代码,以下是缓存逻辑
 pendingCacheKey = null
 const children = slots.default()
 let vnode = children[0]
 const comp = vnode.type as Component
 const name = getName(comp)
 const { include, exclude, max } = props
 // key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
 const key = vnode.key == null ? comp : vnode.key
 // 通过 key 值获取缓存节点
 const cachedVNode = cache.get(key)
 if (cachedVNode) {
   // 缓存存在,则使用缓存装载数据
   vnode.el = cachedVNode.el
   vnode.component = cachedVNode.component
   if (vnode.transition) {
     // 递归更新子树上的 transition hooks
     setTransitionHooks(vnode, vnode.transition!)
  }
     // 阻止 vNode 节点作为新节点被挂载
     vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
     // 刷新key的优先级
     keys.delete(key)
     keys.add(key)
} else {
     keys.add(key)
     // 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
     if (max && keys.size > parseInt(max as string, 10)) {
       pruneCacheEntry(keys.values().next().value)
    }
  }
   // 避免 vNode 被卸载
   vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
   current = vnode
   return vnode;
}
将组件移出缓存表
// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
 cache.forEach((vnode, key) => {
   const name = getComponentName(vnode.type as ConcreteComponent);
   if (name && (!filter || !filter(name))) {
     // !filter(name) 即 name 在 includes 或不在 excludes 中
     pruneCacheEntry(key);
  }
});
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
 const cached = cache.get(key) as VNode;
 if (!current || cached.type !== current.type) {
   /* 当前没有处在 activated 状态的组件
    * 或者当前处在 activated 组件不是要删除的 key 时
    * 卸载这个组件
  */
   unmount(cached); // unmount方法里同样包含了 resetShapeFlag
} else if (current) {
   // 当前组件在未来应该不再被 keepAlive 缓存
   // 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
   resetShapeFlag(current);
   // resetShapeFlag
}
 cache.delete(key);
 keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
 let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
  // ... 清除组件的 shapeFlag
}

keep-alive案例

本部分将使用 vue 3.x 的新特性来模拟 keep-alive 的具体应用场景

在 index.vue 里我们引入了 CountUp 、timer 和 ColorRandom 三个带有状态的组件 在容量为 2 的 中包裹了一个动态组件

// index.vue
<script setup>
import { ref } from "vue"
import CountUp from '../components/CountUp.vue'
import ColorRandom from '../components/ColorRandom.vue'
import Timer from '../components/Timer.vue'
const tabs = ref([    // 组件列表
{
   title: "ColorPicker",
   comp: ColorRandom,
},
{
   title: "timer1",
   comp: Timer,
},
{
   title: "timer2",
   comp: Timer,
},
{
   title: "CountUp",
   comp: CountUp,
},
])
const currentTab = ref(tabs.value[0]) // tab 默认展示第一个组件
const tabSwitch = (tab) => {
 currentTab.value = tab
}
script>
<template>
 <div id="main-page">keep-alive demo belowdiv>
 <div class="tab-group">
   <button
   v-for="tab in tabs"
   :key="tab"
   :class="['tab-button', { active: currentTab === tab }]"
   @click="tabSwitch(tab)"
 >
   {{ tab.title }}
 button>
 div>
 <keep-alive max="2">
   
   <component
     v-if="currentTab"
     :is="currentTab.comp"
     :key="currentTab.title"
     :name="currentTab.title"
   />
 keep-alive>
template>

缓存状态

缓存流程如下:

缓存流程图

可以看到被包裹在 keep-alive 的动态组件缓存了前一个组件的状态。

通过观察 vue devtools 里节点的变化,可以看到此时 keepAlive 中包含了 ColorRandomTimer 两个组件,当前展示的组件会处在 activated 的状态,而其他被缓存的组件则处在 inactivated 的状态

如果我们注释了两个 keep-alive 会发现不管怎么切换组件,都只会重新渲染,并不会保留前次的状态
keepAlive-cache.gif

移除组件

移除流程如下:

移除流程图

为了验证组件是否在切换tab时能被成功卸载,在每个组件的 onUnmounted 中加上了 log

onUnmounted(()=>{
 console.log(`${props.name} 组件被卸载`)
})
  • 当缓存数据长度小于等于 max ,切换组件并不会卸载其他组件,就像上面在 vue devtools 里展示的一样,只会触发组件的 activateddeactivated 两个生命周期

  • 若此时缓存数据长度大于 max ,则会从缓存列表中删除优先级较低的,优先被淘汰的组件,对应的可以看到该组件 umounted 生命周期触发。

性能优化

使用 KeepAlive 后,被 KeepAlive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。

总结

Vue 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件( include 与 exclude )的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。

具体缓存过程如下:

  1. 声明有序集合 keys 作为缓存容器,存入组件的唯一 key 值

  2. 在缓存容器 keys 中,越靠前的 key 值意味着被访问的越少也越优先被淘汰

  3. 渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,刷新该 key 的优先级

  4. 未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据

  5. 当触发 beforeMount/update 生命周期,缓存当前 activated 组件的子树的数据


参考

作者:政采云前端团队
来源:https://juejin.cn/post/7036483610920091656

收起阅读 »

Android 关键字高亮

前言项目中经常会遇到需要对关键字加特殊色值显示,不管是搜索内容还是列表关键字展示,对于特殊文字或者词组高亮是一种很常见的需求,Android 没有自带这样的工具或者组件提供,但是我们可以自己实现一个这样的工具类,用到的地方直接调用就好了。文字高亮所谓文字高亮,...
继续阅读 »

前言

项目中经常会遇到需要对关键字加特殊色值显示,不管是搜索内容还是列表关键字展示,对于特殊文字或者词组高亮是一种很常见的需求,Android 没有自带这样的工具或者组件提供,但是我们可以自己实现一个这样的工具类,用到的地方直接调用就好了。

文字高亮

所谓文字高亮,其实就是针对某个字符做特殊颜色显示,下面列举几种常见的实现方式

一、通过加载Html标签,显示高亮

Android 的TextView 可以加载带Html标签的段落,方法:

textView.setText(Html.fromHtml("<font color='red' size='24'>Hello World</font>"));

那么要高亮显示关键字,就可以这样实现,把需要高亮显示的关键字,通过这样的方式,组合起来就好了,例如:

textView.setText(“这是我的第一个安卓项目” + Html.fromHtml("<font color='red' size='24'>Hello World</font>"));

二、通过SpannableString来实现文本高亮

先来简单了解下SpannableString

SpannableString的基本使用代码示例:

//设置Url地址连接
private void addUrlSpan() {
SpannableString spanString = new SpannableString("超链接");
URLSpan span = new URLSpan("tel:0123456789");
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}

//设置字体背景的颜色
private void addBackColorSpan() {
SpannableString spanString = new SpannableString("文字背景颜色");
BackgroundColorSpan span = new BackgroundColorSpan(Color.YELLOW);
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}
//设置字体的颜色
private void addForeColorSpan() {
SpannableString spanString = new SpannableString("文字前景颜色");
ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);
spanString.setSpan(span, 0, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}
//设置字体的大小
private void addFontSpan() {
SpannableString spanString = new SpannableString("36号字体");
AbsoluteSizeSpan span = new AbsoluteSizeSpan(36);
spanString.setSpan(span, 0, 5, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.append(spanString);
}

以上是比较常用的,还有其他例如设置字体加粗,下划线,删除线等,都可以实现

我们这里主要用到给字体设置背景色,通过正则表达式匹配关键字,设置段落中匹配到的关键字高亮

/***
* 指定关键字高亮 字符串整体高亮
* @param originString 原字符串
* @param keyWords 高亮字符串
* @param highLightColor 高亮色值
* @return 高亮后的字符串
*/
public static SpannableString getHighLightWord(String originString, String keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (!TextUtils.isEmpty(keyWords)) {
Pattern pattern = Pattern.compile(keyWords);
Matcher matcher = pattern.matcher(originSpannableString);
while (matcher.find()) {
int startIndex = matcher.start();
int endIndex = matcher.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return originSpannableString;
}

在扩展一下,可以支持关键字,关键词拆分显示

类似:测试1234测1234试(测试为高亮字,实现测试/测/试分别高亮)

/***
* 指定关键字高亮 支持分段高亮
* @param originString
* @param keyWords
* @param highLightColor
* @return
*/
public static SpannableString getHighLightWords(String originString, String keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (!TextUtils.isEmpty(keyWords)) {
for (int i = 0; i < keyWords.length(); i++) {
Pattern p = Pattern.compile(String.valueOf(keyWords.charAt(i)));
Matcher m = p.matcher(originSpannableString);
while (m.find()) {
int start = m.start();
int end = m.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return originSpannableString;
}

字符可以,那么数组呢,是不是也可以实现了?

/***
* 指定关键字数组高亮
* @param originString 原字符串
* @param keyWords 高亮字符串数组
* @param highLightColor 高亮色值
* @return 高亮后的字符串
*/
public static SpannableString getHighLightWordsArray(String originString, String[] keyWords, int highLightColor) {
SpannableString originSpannableString = new SpannableString(originString);
if (keyWords != null && keyWords.length > 0) {
for (int i = 0; i < keyWords.length; i++) {
Pattern p = Pattern.compile(keyWords[i]);
Matcher m = p.matcher(originSpannableString);
while (m.find()) {
int start = m.start();
int end = m.end();
originSpannableString.setSpan(new ForegroundColorSpan(highLightColor), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return originSpannableString;
}

总结

这样不管来什么需求,是不是都可以满足了,随便产品经理提,要什么给什么

收起阅读 »

聊一聊Android开发利器之adb

学无止境,有一技旁身,至少不至于孤陋寡闻。adb的全称为Android Debug Bridge,通过adb可以方便我们调试Android程序。作为移动端开发相关的同学,掌握所需要的adb操作命令是非常必须的,就把平时工作中用得相对比较多的adb命令做个梳理。...
继续阅读 »

学无止境,有一技旁身,至少不至于孤陋寡闻。

adb的全称为Android Debug Bridge,通过adb可以方便我们调试Android程序。作为移动端开发相关的同学,掌握所需要的adb操作命令是非常必须的,就把平时工作中用得相对比较多的adb命令做个梳理。(日常中把adb操作命令搭配shell alias使用起来更方便)

ADB常用命令

1.启动/停止adb server命令

adb start-server  //启动命令
adb kill-server //停止命令

2. 通过adb查看设备相关信息

  1. 查询已连接设备/模拟器
    adb devices
  2. 查看手机型号
    adb shell getprop ro.product.model
  3. 查看电池状况
    adb shell dumpsys battery
  4. 查看屏幕分辨率
    adb shell wm size
  5. 查看屏幕密度
    adb shell wm density
  6. 查看显示屏参数
    adb shell dumpsys window displays
  7. 查看Android系统版本
    adb shell getprop ro.build.version.release
  8. 查看CPU信息
    adb shell cat /proc/cpuinfo
  9. 查看手机CPU架构
    adb shell getprop ro.product.cpu.abi
  10. 查看内存信息
    adb shell cat /proc/meminfo

3. 通过adb连接设备命令

adb [-d|-e|-s ]
如果只有一个设备/模拟器连接时,可以省略掉 [-d|-e|-s ] 这一部分,直接使用 adb即可 。 如果有多个设备/模拟器连接,则需要为命令指定目标设备。

参数含义
-d指定当前唯一通过 USB 连接的 Android 设备为命令目标
-e指定当前唯一运行的模拟器为命令目标
-s <serialNumber>指定相应 serialNumber 号的设备/模拟器为命令目标
在多个设备/模拟器连接的情况下较常用的是-s参数,serialNumber 可以通过adb devices命令获取。如:
$ adb devices
List of devices attached
cfxxxxxx device
emulator-5554 device
10.xxx.xxx.x:5555 device

输出里的 cfxxxxxxemulator-5554 和 10.xxx.xxx.x:5555 即为 serialNumber。 比如这时想指定 cfxxxxxx 这个设备来运行 adb 命令 获取屏幕分辨率:

adb -s cfxxxxxx shell wm size

安装应用:

adb -s cfxxxxxx install hello.apk

遇到多设备/模拟器的情况均使用这几个参数为命令指定目标设备。

4. 通过adb在设备上操作应用相关

  1. 安装 APK

    adb install [-rtsdg] <apk_path>

    参数:
    adb install 后面可以跟一些可选参数来控制安装 APK 的行为,可用参数及含义如下:

    参数含义
    -r允许覆盖安装
    -t允许安装 AndroidManifest.xml 里 application 指定 android:testOnly="true" 的应用
    -s将应用安装到 sdcard
    -d允许降级覆盖安装
    -g授予所有运行时权限
  2. 卸载应用

    adb uninstall [-k] <packagename>

    <packagename> 表示应用的包名,-k 参数可选,表示卸载应用但保留数据和缓存目录。

    adb uninstall com.vic.dynamicview
  3. 强制停止应用

    adb shell am force-stop <packagename>

    命令示例:

    adb shell am force-stop com.vic.dynamicview
  4. 调起对应的Activity

    adb shell am start [options] <INTENT>

    例如:

    adb shell am start -n com.vic.dynamicview/.MainActivity --es "params" "hello, world"

    表示调起 com.vic.dynamicview/.MainActivity 并传给它 string 数据键值对 params - hello, world。

  5. 查看前台 Activity

    adb shell dumpsys activity activities | grep ResumedActivity

    查看activity堆栈信息: adb shell dumpsys activity

    ACTIVITY MANAGER PENDING INTENTS (adb shell dumpsys activity intents)
    ...
    ACTIVITY MANAGER BROADCAST STATE (adb shell dumpsys activity broadcasts)
    ...
    ACTIVITY MANAGER CONTENT PROVIDERS (adb shell dumpsys activity providers)
    ...
    ACTIVITY MANAGER SERVICES (adb shell dumpsys activity services)
    ...
    ACTIVITY MANAGER ACTIVITIES (adb shell dumpsys activity activities)
    ...
    ACTIVITY MANAGER RUNNING PROCESSES (adb shell dumpsys activity processes)
    ...
  6. 打开系统设置:
    adb shell am start -n com.android.settings/com.android.settings.Settings

  7. 打开开发者选项:
    adb shell am start -a com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS

  8. 进入WiFi设置
    adb shell am start -a android.settings.WIRELESS_SETTINGS

  9. 重启系统
    adb reboot

5. 通过adb操作日志相关

  1. logcathelp帮助信息
    adb logcat --help 可以查看logcat帮助信息
    adb logcat 命令格式: adb logcat [选项] [过滤项], 其中 选项 和 过滤项 在 中括号 [] 中, 说明这是可选的;

  2. 输出日志信息到文件:
    ">"输出 :
    ">" 后面跟着要输出的日志文件, 可以将 logcat 日志输出到文件中, 使用 adb logcat > log 命令, 使用 more log 命令查看日志信息;
    如:adb logcat > ~/logdebug.log

  3. 输出指定标签内容:
    "-s"选项 : 设置默认的过滤器, 如 我们想要输出 "System.out" 标签的信息, 就可以使用 adb logcat -s System.out 命令;

  4. 清空日志缓存信息:
    使用 adb logcat -c 命令, 可以将之前的日志信息清空, 重新开始输出日志信息;

  5. 输出缓存日志:
    使用 adb logcat -d 命令, 输出命令, 之后退出命令, 不会进行阻塞;

  6. 输出最近的日志:
    使用 adb logcat -t 5 命令, 可以输出最近的5行日志, 并且不会阻塞;

  7. 日志过滤:
    注意:在windows上不能使用grep关键字,可以用findstr代替grep.

    • 过滤固定字符串:
      adb logcat | grep logtag
      adb logcat | grep -i logtag #忽略大小写。
      adb logcat | grep logtag > ~/result.log #将过滤后的日志输出到文件
      adb logcat | grep --color=auto -i logtag #设置匹配字符串颜色。

    • 使用正则表达式匹配
      adb logcat | grep "^..Activity"

ADB其他命令

1. 清除应用数据与缓存

adb shell pm clear <packagename>

<packagename> 表示应用名包,这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」。

adb shell pm clear com.xxx.xxx

2. 与应用交互操作

主要是使用 am <command> 命令,常用的 <command> 如下:

command用途
start [options] <INTENT>启动 <INTENT> 指定的 Activity
startservice [options] <INTENT>启动 <INTENT> 指定的 Service
broadcast [options] <INTENT>发送 <INTENT> 指定的广播
force-stop <packagename>停止 <packagename> 相关的进程

<INTENT> 参数很灵活,和写 Android 程序时代码里的 Intent 相对应。

用于决定 intent 对象的选项如下:

参数含义
-a <ACTION>指定 action,比如 android.intent.action.VIEW
-c <CATEGORY>指定 category,比如 android.intent.category.APP_CONTACTS
-n <COMPONENT>指定完整 component 名,用于明确指定启动哪个 Activity,如 com.example.app/.ExampleActivity

<INTENT> 里还能带数据,就像写代码时的 Bundle 一样:

参数含义
--esn <EXTRA_KEY>null 值(只有 key 名)
-e--es <EXTRA_KEY> <EXTRA_STRING_VALUE>`
--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE>boolean 值
--ei <EXTRA_KEY> <EXTRA_INT_VALUE>integer 值
--el <EXTRA_KEY> <EXTRA_LONG_VALUE>long 值
--ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE>float 值
--eu <EXTRA_KEY> <EXTRA_URI_VALUE>URI
--ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE>component name
--eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE...]integer 数组
--ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE...]long 数组
  1. 调起Activity

    adb shell am start [options] <INTENT>

    例如:

    adb shell am start -n com.cc.test/.MainActivity --es "params" "hello, world"

    表示调起 com.cc.test/.MainActivity 并传给它 string 数据键值对 params - hello, world。

  2. 调起Service

    adb shell am startservice [options] <INTENT>

    例如:

    adb shell am startservice -n com.tencent.mm/.plugin.accountsync.model.AccountAuthenticatorService
  3. 发送广播

    adb shell am broadcast [options] <INTENT>

    可以向所有组件广播,也可以只向指定组件广播。 例如,向所有组件广播 BOOT_COMPLETED:

    adb shell am broadcast -a android.intent.action.BOOT_COMPLETED

    又例如,只向 com.cc.test/.BootCompletedReceiver 广播 BOOT_COMPLETED:

    adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n com.cc.test/.BootCompletedReceiver
  4. 撤销应用程序的权限

    1. 向应用授予权限。只能授予应用程序声明的可选权限
    adb shell pm grant <packagename> <PACKAGE_PERMISSION>

    例如:adb -d shell pm grant packageName android.permission.BATTERY_STATS

    1. 取消应用授权
    adb shell pm revoke <packagename> <PACKAGE_PERMISSION>

3. 模拟按键/输入

Usage: input [<source>] <command> [<arg>...]

The sources are:
mouse
keyboard
joystick
touchnavigation
touchpad
trackball
stylus
dpad
gesture
touchscreen
gamepad

The commands and default sources are:
text <string> (Default: touchscreen)
keyevent [--longpress] <key code number or name> ... (Default: keyboard)
tap <x> <y> (Default: touchscreen)
swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
press (Default: trackball)
roll <dx> <dy> (Default: trackball)

比如模拟点击://在屏幕上点击坐标点x=50 y=250的位置。

adb shell input tap 50 250

结合shell alias使用adb

shell终端的别名只是命令的简写,有类似键盘快捷键的效果。如果你经常执行某个长长的命令,可以给它起一个简短的化名。使用alias命令列出所有定义的别名。
你可以在~/.bashrc(.zshrc)文件中直接定义别名如alias logRunActivity="adb shell dumpsys activity activities | grep 'Run*'",也可以新创建一个文件如.byterc, 然后在当前shell对应的文件中.bashrc或者.zshrc 中增加source ~/.byterc,重新source配置,使得配置生效,即可使别名全局生效。使用别名可以节省时间、提高工作效率。

如何添加别名alias

下面在MAC环境采用新建文件形式添加别名,步骤如下:

  1. 新建.byterc 文件
    • 如果已经新建,直接打开
      open ~/.byterc
    • 没有新建,则新建后打开
      新建: touch ~/.byterc
      打开:open ~/.byterc
  2. 在.zshrc中添加source ~/.byterc
  3. 在打开的.byterc文件中定义别名
    alias logRunActivity="adb shell dumpsys activity activities | grep 'Run*'"
    Android同学应该知道作用就是查看当前设备运行的Activity信息
  4. 重新source配置,使得配置生效
    $ source ~/.byterc
    如果不是新建文件,直接使用.bashrc或者.zshrc ,直接source对应的配置即可,如:$ source ~/.zshrc .
  5. 此时在命令行中直接执行logRunActivity 即可查看当前设备运行的Activity信息。

注意: 可使用$ alias查看当前有设置哪些别名操作。


收起阅读 »

Swift 中的 Self & Self.Type & self

iOS
Swift 中的 Self & Self.Type & self这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战你可能在写代码的时候已经用过很多次 self 这个关键词了,但是你有没有想过什么是 self 呢?今天我们...
继续阅读 »

Swift 中的 Self & Self.Type & self

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战


你可能在写代码的时候已经用过很多次 self 这个关键词了,但是你有没有想过什么是 self 呢?今天我们就来看看:

  • 什么是 self、Self 和 Self.Type?
  • 都在什么情况下使用?

self

这个大家用的比较多了,self 通常用于当你需要引用你当前所在范围内的对象时。所以,例如,如果在 Rocket 的实例方法中使用 self,在这种情况下,self 将是该 Rocket 的实例。这个很好理解~

struct Rocket {
    func launch() {
        print("10 秒内发射 \(self)")
    }
}

let rocket = Rocket()
rocket.launch() //10 秒内发射 Rocket()

但是,如果要在类方法或静态方法中使用 self,该怎么办?在这种情况下,self 不能作为对实例的引用,因为没有实例,而 self 具有当前类型的值。这是因为静态方法和类方法存在于类型本身而不是实例上。

class Dog {
    class func bark() {
        print("\(self) 汪汪汪!")
    }
}

Dog.bark() //Dog 汪汪汪!


struct Cat {
    static func meow() {
        print("\(self) 喵喵喵!")
    }
}

Cat.meow() // Cat 喵喵喵!


元类型

还有个需要注意的地方。所有的值都应该有一个类型,包括 self。就像上面提到的,静态和类方法存在于类型上,所以在这种情况下,self 就拥有了一种类型:Self.Type。比如:Dog.Type 就保存所有 Dog 的类型值。

包含其他类型的类型称为元类型

有点绕哈,简单来说,元类型 Dog.Type 不仅可以保存 Dog 类型的值,还可以保存它的所有子类的值。比如下面这个例子,其中 Labrador 是 Dog 的一个子类。

class Dog {
    class func bark() {
        print("\(self) 汪汪汪!")
    }
}

class Labrador: Dog {

}

Labrador.bark() //Labrador 汪汪汪!

如果你想将 type 本身当做一个属性,或者将其传递到函数中,那么你也可以将 type 本身作为值使用。这时候,就可以这样用:Type.self。

let dogType: Dog.Type = Labrador.self

func saySomething(dog: Dog.Type) {
    print("\(dog) 汪汪汪!")
}

saySomething(dog: dogType) // Labrador 汪汪汪!


Self

最后,就是大写 s 开头的 Self。在创建工厂方法或从协议方法返回具体类型时,非常的有用:

struct Rocket {
    func launch() {
        print("10 秒内发射 \(self)")
    }
}

extension Rocket {
    static func makeRocket() -> Self {
        return Rocket()
    }
}

protocol Factory {
    func make() -> Self
}

extension Rocket: Factory {
    func make() -> Rocket {
        return Rocket()
    }
}

收起阅读 »

iOS小技能:快速创建OEM项目app

iOS
iOS小技能:快速创建OEM项目app这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战。引言贴牌生产(英语:Original Equipment Manufacturer, OEM)因采购方可提供品牌和授权,允许制造方生产贴有该品牌的...
继续阅读 »

iOS小技能:快速创建OEM项目app

这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

引言

贴牌生产(英语:Original Equipment Manufacturer, OEM)

因采购方可提供品牌和授权,允许制造方生产贴有该品牌的产品,所以俗称“贴牌生产”。

需求背景: SAAS平台级应用系统为一个特大商户,提供专属OEM项目,在原有通用app的基础上进行定制化开发

例如去掉开屏广告,删除部分模块,保留核心模块。更换专属app icon以及主题色

I 上架资料

  1. 用户协议及隐私政策
  2. App版本、 审核测试账号信息
  3. icon、名称、套装 ID(bundle identifier)
  4. 关键词:
  5. app描述:
  6. 技术支持网址使用:

kunnan.blog.csdn.net/article/det…

II 开发小细节

  1. 更换基础配置信息,比如消息推送证书、第三方SDK的ApiKey、启动图、用户协议及隐私政策。
  2. 接口修改:比如登录接口新增SysId请求字段用于区分新旧版、修改域名(备案信息)
  3. 废弃开屏广告pod 'GDTMobSDK' ,'4.13.26'

1.1 更换高德定位SDK的apiKey

    NSString *AMapKey = @"";
[AMapServices sharedServices].apiKey = AMapKey;


1.2 更新消息推送证书和极光的appKey

  1. Mac 上的“钥匙串访问”创建证书签名请求 (CSR)

a. 启动位于 /Applications/Utilities 中的“钥匙串访问”。

b. 选取“钥匙串访问”>“证书助理”>“从证书颁发机构请求证书”。

c. 在“证书助理”对话框中,在“用户电子邮件地址”栏位中输入电子邮件地址。

d. 在“常用名称”栏位中,输入密钥的名称 (例如,Gita Kumar Dev Key)。

e. 将“CA 电子邮件地址”栏位留空。

f. 选取“存储到磁盘”,然后点按“继续”。

help.apple.com/developer-a…

在这里插入图片描述

  1. 从developer.apple.com 后台找到对应的Identifiers创建消息推送证书,并双击aps.cer安装到本地Mac,然后从钥匙串导出P12的正式上传到极光后台。

docs.jiguang.cn//jpush/clie…在这里插入图片描述

  1. 更换appKey(极光平台应用的唯一标识)
        [JPUSHService setupWithOption:launchOptions appKey:@""
channel:@"App Store"
apsForProduction:YES
advertisingIdentifier:nil];


http://www.jiguang.cn/accounts/lo…

1.3 更换Bugly的APPId

    [Bugly startWithAppId:@""];//异常上报


1.4 app启动的新版本提示

更换appid

    [self checkTheVersionWithappid:@""];


检查版本

在这里插入图片描述


- (void)checkTheVersionWithappid:(NSString*)appid{


[QCTNetworkHelper getWithUrl:[NSString stringWithFormat:@"http://itunes.apple.com/cn/lookup?id=%@",appid] params:nil successBlock:^(NSDictionary *result) {
if ([[result objectForKey:@"results"] isKindOfClass:[NSArray class]]) {
NSArray *tempArr = [result objectForKey:@"results"];
if (tempArr.count) {


NSString *versionStr =[[tempArr objectAtIndex:0] valueForKey:@"version"];
NSString *appStoreVersion = [versionStr stringByReplacingOccurrencesOfString:@"." withString:@""] ;
if (appStoreVersion.length==2) {
appStoreVersion = [appStoreVersion stringByAppendingString:@"0"];
}else if (appStoreVersion.length==1){
appStoreVersion = [appStoreVersion stringByAppendingString:@"00"];
}

NSDictionary *infoDic=[[NSBundle mainBundle] infoDictionary];
NSString* currentVersion = [[infoDic valueForKey:@"CFBundleShortVersionString"] stringByReplacingOccurrencesOfString:@"." withString:@""];

currentVersion = [currentVersion stringByReplacingOccurrencesOfString:@"." withString:@""];
if (currentVersion.length==2) {
currentVersion = [currentVersion stringByAppendingString:@"0"];
}else if (currentVersion.length==1){
currentVersion = [currentVersion stringByAppendingString:@"00"];
}



NSLog(@"currentVersion: %@",currentVersion);


if([self compareVesionWithServerVersion:versionStr]){



UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@%@",QCTLocal(@"Discover_a_new_version"),versionStr] message:QCTLocal(@"Whethertoupdate") preferredStyle:UIAlertControllerStyleAlert];
// "Illtalkaboutitlater"= "稍后再说";
// "Update now" = "立即去更新";
// "Unupdate"= "取消更新";

[alertController addAction:[UIAlertAction actionWithTitle:QCTLocal(@"Illtalkaboutitlater") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"取消更新");
}]];
[alertController addAction:[UIAlertAction actionWithTitle:QCTLocal(@"Updatenow") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"itms-apps://itunes.apple.com/app/id%@",appid]];
if (@available(iOS 10.0, *)) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
}];
} else {
// Fallback on earlier vesions
[[UIApplication sharedApplication] openURL:url];
}
}]];
[[QCT_Common getCurrentVC] presentViewController:alertController animated:YES completion:nil];
}
}
}
} failureBlock:^(NSError *error) {
NSLog(@"检查版本错误: %@",error);
}];
}


see also

更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

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

收起阅读 »

objc_msgsend(中)方法动态决议

iOS
引入在学习本文之前我们应该了解objc_msgsend消息快速查找(上) objc_msgsend(中)消息慢速查找 当快速消息查找和消息慢速查找都也找不到imp时,苹果系统后续是怎么处理的我们一起来学习! 方法动态决议主要做了哪些事情?准...
继续阅读 »


引入

在学习本文之前我们应该了解

当快速消息查找和消息慢速查找都也找不到imp时,苹果系统后续是怎么处理的我们一起来学习! 方法动态决议主要做了哪些事情?

准备工作

resolveMethod_locked动态方法决议

1.png

  • 赋值imp = forward_imp

  • 做了个单例判断动态控制执行流程根据behavior方法只执行一次。

2.png

对象方法的动态决议

3.png

类方法的动态决议

3.png

lookUpImpOrForwardTryCache

4.png

cache_getImp

5.png

  • 苹果给与一次动态方法决议的机会来挽救APP
  • 如果是类请用resolveInstanceMethod
  • 如果是元类请用resolveClassMethod

如果都没有处理那么imp = forward_imp ,const IMP forward_imp = (IMP)_objc_msgForward_impcache;

_objc_msgForward_impcache探究

6.png

  • __objc_forward_handler主要看这个函数处理

__objc_forward_handler

7.png

代码案例分析

   int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGTeacher *p = [LGTeacher alloc];
        [t sayHappy];
[LGTeacher saygood];
    }
    return 0;
}


崩溃信息

2021-11-28 22:36:39.223567+0800 KCObjcBuild[12626:762145] +[LGTeacher sayHappy]: unrecognized selector sent to class 0x100008310

2021-11-28 22:36:39.226012+0800 KCObjcBuild[12626:762145] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[LGTeacher sayHappy]: unrecognized selector sent to class 0x100008310'

复制代码

动态方法决议处理对象方法找不到

代码动态决议处理imp修复崩溃

@implementation LGTeacher

-(void)text{
    NSLog(@"%s", __func__ );
}

+(void)say777{

    NSLog(@"%s", __func__ );
}

// 对象方法动态决议

+(BOOL**)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(sayHappy)) {

        IMP imp =class_getMethodImplementation(self, @selector(text));
        Method m = class_getInstanceMethod(self, @selector(text));
        const char * type = method_getTypeEncoding(m);
        return** class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];

}

//类方法动态决议

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(saygood)) {
        IMP  imp7 = class_getMethodImplementation(objc_getMetaClass("LGTeacher"), @selector(say777));
        Method m  = class_getInstanceMethod(objc_getMetaClass("LGTeacher"), @selector(say777));
        const char type = method_getTypeEncoding(m);
        return class_addMethod(objc_getMetaClass("LGTeacher"), sel, imp7, type);
    }

    return [super resolveClassMethod:sel];

}

@end


运行打印信息

2021-11-29 16:30:46.403671+0800 KCObjcBuild[27071:213498] -[LGTeacher text]

2021-11-29 16:30:46.404186+0800 KCObjcBuild[27071:213498] +[LGTeacher say777]

  • 找不到imp我们动态添加一个imp ,但这样处理太麻烦了。
  • 实例方法方法查找流程 类->父类->NSObject->nil
  • 类方法查找流程 元类->父类->根元类-NsObject->nil

最终都会找到NSobject.我们可以在NSObject统一处理 所以我们可以给NSObject创建个分类

@implementation NSObject (Xu)
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (@selector(sayHello) == sel) {
NSLog(@"--进入%@--",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self , @selector(sayHello2));
Method meth = class_getInstanceMethod(self , @selector(sayHello2));
const char * type = method_getTypeEncoding(meth);
return class_addMethod(self ,sel, imp, type);;

}else if (@selector(test) == sel){
NSLog(@"--进入%@--",NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(object_getClass([self class]), @selector(newTest));
Method meth = class_getClassMethod(object_getClass([self class]) , @selector(newTest));
const char * type = method_getTypeEncoding(meth);
return class_addMethod(object_getClass([self class]) ,sel, imp, type);;
}
return NO;
}

- (void)sayHello2{
NSLog(@"--%s---",__func__);
}

+(void)newTest{
NSLog(@"--%s---",__func__);
}

@end


实例方法是类方法调用,系统都自动调用了resolveInstanceMethod方法,和上面探究的吻合。 动态方法决议优点

  • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器,或者跳转到首页
  • 如果项目中是不同的模块你可以根据命名不同,进行业务的区别
  • 这种方式叫切面编程熟成AOP

方法动态决议流程图

9.png

问题

  • resolveInstanceMethod为什么调用两次?
  • 统一处理方案怎么处理判断问题,可能是对象方法崩溃也可能是类方法崩溃,怎么处理?
  • 动态方法决议后苹果后续就没有处理了吗?

链接:https://juejin.cn/post/7035965819955707935
收起阅读 »