注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

膜拜!用最少的代码却实现了最牛逼的滚动动画!

今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~ 在聊ScrollTrigger插件之前我们先简单了解下GSAP。 GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、Reac...
继续阅读 »

今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~



在聊ScrollTrigger插件之前我们先简单了解下GSAP



GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、React、画布、通用对象等)动画化,并解决不同浏览器上存在的兼容问题,而且比 jQuery快 20 倍。大约1000万个网站和许多主要品牌都在使用GSAP。



接下来老鱼带领大家一起学习ScrollTrigger插件的使用。


插件简介


ScrollTrigger是基于GSAP实现的一款高性能页面滚动触发HTML元素动画的插件。


通过ScrollTrigger使用最少的代码创建令人叹为观止的滚动动画。我们需要知道ScrollTrigger是基于GSAP实现的插件,ScrollTrigger是处理滚动事件的,而真正处理动画是GSAP,二者组合使用才能实现滚动动画~


插件特点



  • 将任何动画链接到特定元素,以便它仅在视图中显示该元素时才执行该动画。

  • 可以在进入/离开定义的区域或将其直接链接到滚动栏时在动画上执行操作(播放、暂停、恢复、重新启动、反转、完成、重置)。

  • 延迟动画和滚动条之间的同步。

  • 根据速度捕捉动画中的进度值。

  • 嵌入滚动直接触发到任何 GSAP 动画(包括时间线)或创建独立实例,并利用丰富的回调系统做任何您想做的事。

  • 高级固定功能可以在某些滚动位置之间锁定一个元素。

  • 灵活定义滚动位置。

  • 支持垂直或水平滚动。

  • 丰富的回调系统。

  • 当窗口调整大小时,自动重新计算位置。

  • 在开发过程中启用视觉标记,以准确查看开始/结束/触发点的位置。

  • 在滚动记录器处于活动状态时,如将active类添加到触发元素中:toggleClass: "active"

  • 使用 matchMedia() 标准媒体查询为各种屏幕尺寸创建不同的设置。

  • 自定义滚动触发器容器,可以定义一个 div 而不一定是浏览器视口。

  • 高度优化以实现最大性能。

  • 插件大约只有6.5kb大小。


安装/引用


CDN


<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js"></script>

ES Modules


import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

UMD/CommonJS


import { gsap } from "gsap/dist/gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);


简单示例


gsap.to(".box", {
scrollTrigger: ".box", // start the animation when ".box" enters the viewport (once)
x: 500
});

高级示例


let tl = gsap.timeline({
  // 添加到整个时间线
  scrollTrigger: {
    trigger: ".container",
    pin: true,   // 在执行时固定触发器元素
    start: "top top", // 当触发器的顶部碰到视口的顶部时
    end: "+=500", // 在滚动 500 px后结束
    scrub: 1, // 触发器1秒后跟上滚动条
    snap: {
      snapTo: "labels", // 捕捉时间线中最近的标签
      duration: {min: 0.2, max: 3}, // 捕捉动画应至少为 0.2 秒,但不超过 3 秒(由速度决定)
      delay: 0.2, // 从上次滚动事件开始等待 0.2 秒,然后再进行捕捉
      ease: "power1.inOut" // 捕捉动画的过度时间(默认为“power3”)
    }
  }
});

// 向时间线添加动画和标签
tl.addLabel("start")
.from(".box p", {scale: 0.3, rotation:45, autoAlpha: 0})
.addLabel("color")
.from(".box", {backgroundColor: "#28a92b"})
.addLabel("spin")
.to(".box", {rotation: 360})
.addLabel("end");

自定义示例


ScrollTrigger.create({
trigger: "#id",
start: "top top",
endTrigger: "#otherID",
end: "bottom 50%+=100px",
onToggle: self => console.log("toggled, isActive:", self.isActive),
onUpdate: self => {
  console.log("progress:", self.progress.toFixed(3), "direction:", self.direction, "velocity", self.getVelocity());
}
});

接下来,我们一起来看使用ScrollTrigger可以实现怎样的效果吧。


利用ScrollTrigger可以实现很多炫酷的效果,还有更多示例及源代码,快去公众号后台回复aaa滚动获取学习吧!也欢迎同学们和老鱼讨论哦~


作者:大前端实验室
链接:https://juejin.cn/post/7038378577028448293

收起阅读 »

领导:小伙子,咱们这个页面出来太慢了!赶紧给我优化一下。

性能优化 这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点? 接下来让我们一起来探索前端...
继续阅读 »

性能优化


这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点?


接下来让我们一起来探索前端性能优化(emo~


如何量化网站是否需要做性能优化?


首先现在工具碎片化的时代,各种工具满天飞,如何找到一个方便又能直击痛点的工具,是重中之重的首要任务。



下面使用的就是Chrome自带的插件工具进行分析



可以使用chrome自带的lightHouse工具进行分析。得出的分数会列举出三个档次。然后再根据提出不同建议进行优化。


例如:打开掘金的页面,然后点开开发者工具中的Lighthouse插件


1.png


我们可以看到几项指标:



  • First Contentful Paint 首屏加载时间(FCP)

  • Time to interactive 可互动的时间(TTI) 衡量一个页面多长时间才能完全交互

  • Speed Index 内容明显填充的速度(SI) 分数越低越好

  • Total Blocking Time 总阻塞时间(TBT) 主线程运行超过50ms的任务叫做Long Task,Total Blocking Time (TBT) 是 Long Tasks(所有超过 50ms 的任务)阻塞主线程并影响页面可用性的时间量,比如异步任务过长就会导致阻塞主线程渲染,这时就需要处理这部分任务

  • Largest Contentful Paint 最大视觉元素加载的时间(LCP) 对于SEO来说最重要的指标,用户如果打开页面很久都不能看清楚完整页面,那么SEO就会很低。(对于Google来说)

  • Cumulative Layout Shift 累计布局偏移(CLS) 衡量页面点击某些内容位置发生偏移后对页面对影响 eg:当图片宽高不确定时会时该指标更高,还比如异步或者dom动态加载到现有内容上的情况也会造成CLS升高


以上的6个指标就能很好的量化我们网页的性能。得出类似以下结论,并采取措施。



下面的图片是分析自己的项目得出的图表



2.png


3.png



  • 比如打包体积 (webpack优化,tree-sharking和按需加载插件,以及css合并)

  • 图片加载大小优化(使用可压缩图片,搭配上懒加载和预加载)

  • http1.0替换为http2.0后可使用二进制标头和多路复用。(某些图片使用cdn请求时使用了http1.0)

  • 图片没有加上width和heigth(或者说没有父容器限制),当页面重绘重排时容易造成页面排版混乱的情况

  • 避免巨大的网络负载,比如图片的同时请求和减少同时请求的数量

  • 静态资源缓存

  • 减少未使用的 JavaScript 并推迟加载脚本(defer和async)



千遍万遍,不如自己行动一遍。dev your project!然后再对比服用,效果更好哦!



如何做性能优化


Vue-cli已经做了的优化:



  • 使用cache-loader默认为Vue/Babel/TypeScript编译开启,文件会缓存在node_modules/.cache里

  • 图片小于4k的会转为base64储存在js文件中

  • 生产环境会将css提取成单独的文件

  • 提取公共代码

  • 代码压缩

  • 给所有的js文件和css文件加上preload


我们需要做的优化:(下面做出的优化都是根据分析工具得出后,对应自己的项目进行细化而来)

  1. 首先代码层面:

    1. 多图片的页面需要做图片懒加载+预加载+cdn请求以及压缩。后期会推出一篇关于图片优化的文章...
    2. 组件按需加载
    3. 对于迫不得已的dom操作,尽量一次性操作。避免多次操作dom造成页面重绘重排
    4. 公共组件的提取
    5. ajax的请求尽量能够减少多个,如果ajax请求比较慢,但是又必须得请求。那么可以考虑使用 Web Worker
  2. 打包项目。

    1. 使用webpack插件 例如 tree-sharking进行剔除无关的依赖加载。使用terser进行代码压缩,给执行时间长的loader加 cache-loader,可以使得下次打包就会使用 node_modules/.cache 里的
    2. 静态资源使用缓存或者cdn加载,部分动态文件设置缓存过期时间

作者:Tzyito
链接:https://juejin.cn/post/7008422231403397134

收起阅读 »

知道这个,再也不用写一堆el-table-column了

前言 最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。 下面就来分享一下! 进入正题 上面就是table...
继续阅读 »

前言


最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。


下面就来分享一下!


进入正题


image.png
上面就是table中的全部项,去除第一个复选框,最后一个操作的插槽,一共七项,也就是说el-table-column一共要写9对。这简直不能忍!


image.png



这个图只作举一个例子用,跟上面不产生对应关系。



其中就有5个el-form-item,就这么一大堆。


所以,我当时就想,可不可以用v-for去渲染el-table-column这个标签呢?保留复选框和最后的操作插槽,我们只需要渲染中间的那几项就行。


经过我的实验,确实是可以实现的。



这么写之后就开始质疑之前的我为什么没有这个想法? 要不就能少写一堆💩啦



实现代码如下(标签部分):



            v-for="item in columns"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:formatter="item.formatter"
:width="item.width">



思路是这样,把标签需要显示的定义在一个数组中,遍历数组来达到我们想要的效果,formatter是我们完成提交的数据和页面显示数据的一个转换所用到的。具体写法在下面js部分有写。


定义数组的写法是vue3 composition api的写法,这个思路的话,用Vue2的写法也能实现的,重要的毕竟是思想(啊,我之前还是想不到这种思路)。



再吐槽一下下,这种写法每写一个函数或者变量就要return回去,也挺麻烦的感觉,hhhhh



实现代码如下(JS部分):


const columns = reactive([
{
label:'用户ID',
prop:'userId'
},
{
label:'用户名',
prop:'userName'
},
{
label:'用户邮箱',
prop:'userEmail'
},
{
label:'用户角色',
prop:'role',
formatter(row,column,value){
return {
0:"管理员",
1:"普通用户"
}[value]
}
},
{
label:'用户状态',
prop:'state',
formatter(row,column,value){
return {
1:"在职",
2:"离职",
3:"试用期"
}[value]
}
},
{
label:'注册时间',
prop:'createTime'
},
{
label:'最后登陆时间',
prop:'lastLoginTime'
}
])

作者:Ned
链接:https://juejin.cn/post/7025921628684943396

收起阅读 »

浏览器为什么能唤起App的页面

疑问的开端 大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。 这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面? 说到跨app的页面调用,大家是不是能够想到一个机制:Activity的...
继续阅读 »

疑问的开端


大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。


image.png


这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面?


说到跨app的页面调用,大家是不是能够想到一个机制:Activity的隐式调用?


一、隐式启动原理


当我们有需要调起其他app的页面时,使用的API就是隐式调用。


比如我们有一个app声明了这样的Activity:


<activity android:name=".OtherActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="mdove"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

其他App想启动上边这个Activity如下的调用就好:


val intent = Intent()
intent.action = "mdove"
startActivity(intent)

我们没有主动声明Activity的class,那么系统是怎么为我们找到对应的Activity的呢?其实这里和正常的Activity启动流程是一样的,无非是if / else的实现不同而已。


接下来咱们就回顾一下Activity的启动流程,为了避免陷入细节,这里只展开和大家相对“耳熟能详”的类和调用栈,以串流程为主。


1.1、跨进程


首先我们必须明确一点:无论是隐式启动还是显示启动;无论是启动App内Activity还是启动App外的Activity都是跨进程的。比如我们上述的例子,一个App想要启动另一个App的页面。



注意没有root的手机,是看不到系统孵化出来的进程的。也就是我们常见的为什么有些代码打不上断点。



image.png


追过startActivity()的同学,应该很熟悉下边这个调用流程,跟进几个方法之后就发现进到了一个叫做ActivityTread的类里边。



ActivityTread这个类有什么特点?有main函数,就是我们的主线程。



很快我们能看到一个比较常见类的调用:Instrumentation


// Activity.java
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
// 省略
}

注意mInstrumentation#execStartActivity()有一个标黄的入参,它是ActivityThread中的内部类ApplicationThread



ApplicationThread这个类有什么特点,它实现了IApplicationThread.Stub,也就是aidl的“跨进程调用的客户端回调”。



此外mInstrumentation#execStartActivity()中又会看到一个大名鼎鼎的调用:


public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
// 省略...
ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
return null;
}

我们点击去getService()会看到一个标红的IActivityManager的类。



它并不是一个.java文件,而是aidl文件。



所以ActivityManager.``getService``()本质返回的是“进程的服务端”接口实例,也就是:


1.2、ActivityManagerService



public class ActivityManagerService extends IActivityManager.Stub



所以执行到这就转到了系统进程(system_process进程)。省略一下代码细节,看一下调用栈:


image.png


从过上述debug截图,看一看到此时已经拿到了我们的目标Activitiy的相关信息。


这里简化一些获取目标类的源码,直接引入结论:


1.3、PackageManagerService


这里类相当于解析手机内的所有apk,将其信息构造到内存之中,比如下图这样:



image.png



小tips:手机目录中/data/system/packages.xml,可以看到所有apk的path、进程名、权限等信息。



1.4、启动新进程


打开目标Activity的前提是:目标Activity的进程启动了。所以第一次想要打开目标Activity,就意味着要启动进程。


启动进程的代码就在启动Activity的方法中:


resumeTopActivityInnerLocked->startProcessLocked


image.png


这里便引入了另一个另一个大名鼎鼎的类:ZygoteInit。这里简单来说会通过ZygoteInit来进行App进程启动的。


1.5、ApplicationThread


进程启动后,继续回到目标Activity的启动流程。这里依旧是一系列的system_process进行的转来转去,然后IApplicationThread进入目标进程。



注意看,在这里再次通过IApplicationThread回调到ActivityThread


class H extends Handler {
// 省略
public void handleMessage(Message msg) {
switch (msg.what) {
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
// 省略
break;
case RELAUNCH_ACTIVITY:
handleRelaunchActivityLocally((IBinder) msg.obj);
break;
}
// 省略...
}
}

// 执行Callback
public void execute(ClientTransaction transaction) {
final IBinder token = transaction.getActivityToken();
executeCallbacks(transaction);
}

这里所谓的CallBack的实现是LaunchActivityItem#execute(),对应的实现:


public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client);
client.handleLaunchActivity(r, pendingActions, null);
}

此时就转到了ActivityThread#handleLaunchActivity(),也就转到了咱们日常的生命周期里边,调用栈如下:



上述截图的调用链中暗含了Activity实例化的过程(反射):


public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

return (Activity) cl.loadClass(className).newInstance();

}
复制代码

二、浏览器启动原理


Helo站内的回流页就是一个标准的,浏览器唤起另一个App的实例。


2.1、交互流程


html标签有一个属性href,比如:<a href="...">


我们常见的一种用法:<a href="``https://www.baidu.com``">。也就是点击之后跳转到百度。


因为这个是前端的标签,依托于浏览器及其内核的实现,跳转到一个网页似乎很“顺其自然”(不然叫什么浏览器)。


当然这里和android交互的流程基本一致:用隐式调用的方式,声明需要启动的Activity;然后<a href="">传入对应的协议(scheme)即可。比如:


前端页面:


<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<a href="mdove1://haha"> 启动OtherActivity </a>
</body>

android声明:


<activity
android:name=".OtherActivity"
android:screenOrientation="portrait">
<intent-filter>
<data
android:host="haha"
android:scheme="mdove1" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

2.2、推理实现


浏览器能够加载scheme,可以理解为是浏览器内核做了封装。那么想要让android也能支持对scheme的解析,难道是由浏览器内核做处理吗?


很明显不可能,做了一套移动端的操作系统,然后让浏览器过来实现,是不是有点杀人诛心。


所以大概率能猜测出来,应该是手机中的浏览器app做的处理。我们就基于这个猜想去看一看浏览器.apk的实现。


2.3、浏览器实现


基于上边说的/data/system/packages.xml文件,我们可以pull出来浏览器的.apk。



然后jadx反编译一下Browser.apk中WebView相关的源码:




我们可以发现对href的处理来自于隐式跳转,所以一切就和上边的流程串了起来。


作者:咸鱼正翻身
链接:https://juejin.cn/post/7033751175551942692

收起阅读 »

实现穿梭栈帧的魔法--协程

1. 协程-穿梭栈帧的魔法 协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。 2...
继续阅读 »

1. 协程-穿梭栈帧的魔法


协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。


2. 如何实现协程


前提:本文仅探讨kotlin协程实现


其实在反编译suspend函数反编译后就能知道协程的实现原理(以下)


github.com/yujinyan/ko…


//协程代码
//suspend fun foo() :Any{
// delay(3000L)
// val value =getCurrentTime()
// Log.e("TAG", "result is $value")
//}
//等价代码
@suspend fun foo() {
foo(object : Continuation<Any> {
override fun resumeWith(result: Result<Any>) {
val value = result.getOrThrow()
Log.e("TAG", "result is $value")
}
})
}

@suspend fun foo(continuation: Continuation<Any>): Any {
class FooContinuation : Continuation<Any> {
var label: Int = 0

override fun resumeWith(result: Result<Any>) {
val outcome = invokeSuspend()
if (outcome === COROUTINE_SUSPENDED) return
continuation.resume(result.getOrThrow())
}

fun invokeSuspend(): Any {
return foo(this)
}
}

val cont = (continuation as? FooContinuation) ?: FooContinuation()
return when (cont.label) {
0 -> {
cont.label++
//异步延时任务
AppExecutors.newInstance().otherIO.execute {
Thread.sleep(3000L)
val value = getCurrentTime()
cont.resume(value)
}
COROUTINE_SUSPENDED
}
1 -> 1 // return 1
else -> error("shouldn't happen")
}
}

核心就是函数内匿名内部类的巧用,真的很妙



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

构建Java IO框架体系

IO框架 Java IO的学习是一件非常艰巨的任务。 它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这...
继续阅读 »

IO框架


	Java IO的学习是一件非常艰巨的任务。

它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这些情况综合起来就给我们带来了大量的学习任务,大量的类需要学习。


我们要学会所有的这些java 的IO是很难的,因为我们没有构建一个关于IO的体系,要构建这个体系又需要深入理解IO库的演进过程,所以,我们如果缺乏历史的眼光,很快我们会对什么时候应该使用IO中的哪些类,以及什么时候不该使用它们而困惑。


所以,在开发者的眼中,IO很乱,很多类,很多方法,很迷茫。


IO简介


数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。


流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种: **1) 字节流:**数据流中最小的数据单元是字节 **2) 字符流:**数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。


Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。掌握了这些就掌握了Java I/O的精髓了。


Java I/O主要包括如下3层次:


  1. 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等

  2. 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类

  3. 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFYWs0jZ-1638951173815)(F:\001_优秀课题\29_Java IO\IO图谱.png)]


IO详细介绍


在Android 平台,从应用的角度出发,我们最需要关注和研究的就是 字节流(Stream)字符流(Reader/Writer)和 File/ RandomAccessFile。当我们需要的时候再深入研究也未尝不是一件好事。关于字符和字节,例如文本文件,XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。面对如此复杂的类关系,有一个点是我们必须要首先掌握的,那就是设计模式中的修饰模式,学会并理解修饰模式是搞懂流必备的前提条件哦。


字节流的学习


在具体的学习流之前,我们必须要学的一个设计模式是装饰模式。因为从流的整个发展历史,出现的各种类之间的关系看,都是沿用了修饰模式,都是一个类的功能可以用来修饰其他类,然后组合成为一个比较复杂的流。比如说:


     	DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(file)));

从上面的代码块中大家不难看出这些类的关系:为了向文件中写入数据,首先需要创建一个FileOutputStream,然后为了提升访问的效率,所以将它发送给具备缓存功能的BufferedOutput-Stream,而为了实现与机器类型无关的java基本类型数据的输出,所以,我们将缓存的流传递给了DataOutputStream。从上面的关系,我们可以看到,其根本目的都是为outputSteam添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。因此,学习流,必须要学好装饰模式。


下面的图是一个关于字节流的图谱,这张图谱比较全面的概况了我们字节流中间的各个类以及他们之间的关系。


输入输出流.jpg


字节流的学习过程


为什么要按照一个学习路线来呢?原因是他们的功能决定的。


OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream


相应的学习InputStream方法就好了。


从学习的角度来,我们应该先掌握FilterOutputStream, 以及FileOutputStream,这两个类是基本的类,从继承关系可以不难发现他们都是对 abstract 类 OutputStream的拓展,是它的子类。然而,伴随着 对 Stream流的功能的拓展,所以就出现了 DataOutputStream,(将java中的基础数据类型写入数据字节输出流中、保存在存储介质中、然后可以用DataOutputStream从存储介质中读取到程序中还原成java基础类型)。这里多提一句、DataOutputStream、FilterOutputStream三个类的关系的这种设计既使用了装饰器模式 避免了类的爆炸式增长。


为了提升Stream的执行效率,所以出现了bufferedOutputStream。bufferedOutputStream就是将本地添加了一个缓存的数组。在使用bufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘中读多少个byte的数据,而出现bufferedOutputSteam之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问的次数以达到提升性能的目的。


另外一方面,我们知道了outputStream(输出流)的发展历史后,我们便可以知道如何使用outpuSteam了,同样的方法,我们可以运用到inputStream中来,这样对称的解释就出现到了inputStream相关的中来了,于是,我们对整个字节流就有了全方位的理解,所以这样子我们就不会感觉到流的复杂了。这个时候对于其他的一些字节流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的学习就自需要在使用的时候看看API即可。


字符流的学习


下图则是一个关于字符流的图谱,这张图谱比较全面的概况了我们字符流中间的各个类以及他们之间的关系。


字符输入输出流.jpg


字符流的学习和字节流的学习是一样的,它和字节流有着同样的发展过程,只是,字节流面向的是我们未知或者即使知道了他们的编码格式也意义不大的文件(png,exe, zip)的时候是采用字节,而面对一些我们知道文件构造我们就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。reader 和 Stream最大的区别我认为是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为自有字符才具备行的概念,相反字节流中的行也就是一个字节符号。


字符流的学习历程:


Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其他


同时类比着学习Reader相关的类。


FilterWriter/FilterReader

字符过滤输出流、与FilterOutputStream功能一样、只是简单重写了父类的方法、目的是为所有装饰类提供标准和基本的方法、要求子类必须实现核心方法、和拥有自己的特色。这里FilterWriter没有子类、可能其意义只是提供一个接口、留着以后的扩展。。。本身是一个抽象类。


BufferedWriter/BufferedReader

BufferedWriter是 Writer类的一个子类。他的功能是为传入的底层字符输出流提供缓存功能、同样当使用底层字符输出流向目的地中写入字符或者字符数组时、每写入一次就要打开一次到目的地的连接、这样频繁的访问不断效率底下、也有可能会对存储介质造成一定的破坏、比如当我们向磁盘中不断的写入字节时、夸张一点、将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、没写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedWriter将底层字符输出流、比如FileReader包装一下之后、我们可以在程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数量时、一次性写入FileReader流中、此时、FileReader就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量!


OutputStreamWriter/InputStreamReader

输入字符转换流、是输入字节流转向输入字符流的桥梁、用于将输入字节流转换成输入字符流、通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中、与OutputStreamWriter一样、本质也是使用其内部的一个类来完成所有工作:StreamDecoder、使用默认或者指定的编码将字节转换成字符;OutputStreamWriter/ InputStreamReader只是对StreamDecoder进行了封装、isr内部所有方法核心都是调用StreamDecoder来完成的、InputStreamReader只是对StreamDecoder进行了封装、使得我们可以直接使用读取方法、而不用关心内部实现。


	OutputStreamWriter、InputStreamReader分别为InputStream、OutputStream的低级输入输出流提供将字节转换成字符的桥梁、他们只是外边的一个门面、真正的核心:

OutputStreamWriter中的StreamEncoder:


         1、使用指定的或者默认的编码集将字符转码为字节        

2、调用StreamEncoder自身实现的写入方法将转码后的字节写入到底层字节输出流中。

InputStreamReader中的StreamDecoder:


        1、使用指定的或者默认的编码集将字节解码为字符         

2、调用StreamDecoder自身实现的读取方法将解码后的字符读取到程序中。

在理解这两个流的时候要注意:java——io中只有将字节转换成字符的类、没有将字符转换成字节的类、原因很简单——字符流的存在本来就像对字节流进行了装饰、加工处理以便更方便的去使用、在使用这两个流的时候要注意:由于这两个流要频繁的对读取或者写入的字节或者字符进行转码、解码和与底层流的源和目的地进行交互、所以使用的时候要使用BufferedWriter、BufferedReader进行包装、以达到最高效率、和保护存储介质。


FileReader/FileWriter

FileReader和FileWriter 继承于InputStreamReader/OutputStreamWriter。


从源码可以发现FileWriter 文件字符输出流、主要用于将字符写入到指定的打开的文件中、其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream、然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果使用这个类的话、最好使用BufferedWriter包装一下、高端大气上档次、低调奢华有内涵!


FileReader 文件字符输入流、用于将文件内容以字符形式读取出来、一般用于读取字符形式的文件内容、也可以读取字节形式、但是因为FileReader内部也是通过传入的参数构造InputStreamReader、并且只能使用默认编码、所以我们无法控制编码问题、这样的话就很容易造成乱码。所以读取字节形式的文件还是使用字节流来操作的好、同样在使用此流的时候用BufferedReader包装一下、就算冲着BufferedReader的readLine()方法去的也要使用这个包装类、不说他还能提高效率、保护存储介质。


字节流与字符流的关系


那么字节输入流和字符输入流之间的关系是怎样的呢?请看下图


字节与字符输入流.jpg


同样的字节与字符输出流字节的关系也如下图所示


字节与字符输出流.jpg


字节流与字符流的区别


字节流和字符流使用是非常相似的,那么除了操作代码的不同之外,还有哪些不同呢?


  字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容


  那开发中究竟用字节流好还是用字符流好呢?

  在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。


  如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。


字节流与字符流的转换


虽然Java支持字节流和字符流,但有时需要在字节流和字符流两者之间转换。InputStreamReader和OutputStreamWriter,这两个为类是字节流和字符流之间相互转换的类。


  InputSreamReader用于将一个字节流中的字节解码成字符:

  有两个构造方法: 


   InputStreamReader(InputStream in);

  功能:用默认字符集创建一个InputStreamReader对象


   InputStreamReader(InputStream in,String CharsetName);

  功能:接收已指定字符集名的字符串,并用该字符创建对象


  OutputStream用于将写入的字符编码成字节后写入一个字节流。

  同样有两个构造方法


  OutputStreamWriter(OutputStream out);

  功能:用默认字符集创建一个OutputStreamWriter对象;   


  OutputStreamWriter(OutputStream out,String  CharSetName);

  功能:接收已指定字符集名的字符串,并用该字符集创建OutputStreamWrite对象


为了避免频繁的转换字节流和字符流,对以上两个类进行了封装。


  BufferedWriter类封装了OutputStreamWriter类;


  BufferedReader类封装了InputStreamReader类;


  封装格式


  BufferedWriter out=new BufferedWriter(new OutputStreamWriter(System.out));
BufferedReader in= new BufferedReader(new InputStreamReader(System.in);

  利用下面的语句,可以从控制台读取一行字符串:


  BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
String line=in.readLine();


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

kotlin 与java 互操作

简介 大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠 互操作性与可空性 Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不...
继续阅读 »

简介


大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠


互操作性与可空性


Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不能想当然地认为它的返回值就能符合kotlin关于空值的规定


kotlin


fun main() {
val my = MyClass()
val value = my.getCanNullValue()
println(value?.capitalize())
}

java


public class MyClass {
public String value;

public String getCanNullValue(){
return value;
}
}

类型映射


代码运行时,所有的映射类型都会重新映射回对应的java类型


fun main() {
val my = MyClass()
my.value = "a123"
val value = my.getCanNullValue()
println(value.javaClass)
}

结果为:class java.lang.String


属性访问


不需要调用相关setter方法,你可以使用赋值语法来设置一个java字段值了


val my = MyClass()
my.value = "a123"

@JvmName


这个注解可以改变字节码中生成的类名或方法名称,如果作用在顶级作用域(文件中),则会改变生成对应Java类的名称。如果作用在方法上,则会改变生成对应Java方法的名称。


kotlin


@file:JvmName("FooKt")
@JvmName("foo1")
fun foo() {
println("Hello, Jvm...")
}

java


// 相当于下面的Java代码
public final class FooKt {
public static final void foo1() {
String var0 = "Hello, Jvm...";
System.out.println(var0);
}
}

第一个注解@file:JvmName("FooKt")的作用是使生成的类名变为FooKt,第二个注解的作用是使生成的方法名称变为foo1


@JvmField


Kotlin编译器默认会将类中声明的成员变量编译成私有变量,Java语言要访问该变量必须通过其生成的getter方法。而使用上面的注解可以向Java暴露该变量,即使其访问变为公开(修饰符变为public)。


Kotlin


class JavaToKotlin {
@JvmField
val info = "Hello"
}

@JvmOverloads


由于Kotlin语言支持方法参数默认值,而实现类似功能Java需要使用方法重载来实现,这个注解就是为解决这个问题而生的,添加这个注解会自动生成重载方法


Kotlin


@JvmOverloads
fun prinltInfo(name: String, age: Int = 1) {
println("$name $age")
}

java


 public static void main(String[] args) {
MyKotlin.prinltInfo("arrom");
MyKotlin.prinltInfo("arrom", 20);
}

@JvmStatic


@JvmStatic注解的作用类似于@JvmField,可以直接调用伴生对象里的函数


class JavaToKotlin {
@JvmField
val info = "Hello"

companion object {
@JvmField
val max: Int = 200

@JvmStatic
fun loadConfig(): String {
return "loading config"
}
}
}

@Throws


由于Kotlin语言不支持CE(Checked Exception),所谓CE,即方法可能抛出的异常是已知的。Java语言通过throws关键字在方法上声明CE。为了兼容这种写法,Kotlin语言新增了@Throws注解,该注解的接收一个可变参数,参数类型是多个异常的KClass实例。Kotlin编译器通过读取注解参数,在生成的字节码中自动添加CE声明。


Kotlin


@Throws(IllegalArgumentException::class)
fun div(x: Int, y: Int): Float {
return x.toFloat() / y
}

Java


// 生成的代码相当于下面这段Java代码
public static final float div(int x, int y) throws IllegalArgumentException {
return (float)x / (float)y;
}

添加了@Throws(IllegalArgumentException::class)注解后,在生成的方法签名上自动添加了可能抛出的异常声明(throws IllegalArgumentException),即CE。


@Synchronized


用于产生同步方法。Kotlin语言不支持synchronized关键字,处理类似Java语言的并发问题,Kotlin语言建议使用同步方法进行处理


Kotlin


@Synchronized
fun start() {
println("Start do something...")
}

java


// 生成的代码相当于下面这段Java代码
public static final synchronized void start() {
String var0 = "Start do something...";
System.out.println(var0);
}

函数类型操作


Java中没有函数类型,所以,在Java里,kotlin函数类型使用FunctionN这样的名字的接口来表示,N代表入参的个数,一共有24个这样的接口,从Function0到Function23,每个接口都包含一个invoke函数,调用匿名函数需要调用invoke


kotlin:


val funcp:(String) -> String = {
it.capitalize()
}

java:


Function1 funcp = ArromKt.getFuncp();
funcp.invoke("arrom");

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

LiveData学习记

LiveData 使用 var liveData: MutableLiveData<String>? = null //初始化 liveData = MutableLiveData() // 设置 observe liveData?.observe...
继续阅读 »

LiveData 使用


var liveData: MutableLiveData<String>? = null
//初始化
liveData = MutableLiveData()
// 设置 observe
liveData?.observe(this, {
Log.e("Main2", "2界面接收数据 = $it")
Toast.makeText(this, "2界面接收数据 = $it", Toast.LENGTH_LONG).show()
})
// 发送值
liveData?.value = "2界面发送数据 $indexValue"

LiveData 是针对同一个界面数据相互传递, 配合 MVVM 使用


如果想跨界面使用 比如 Activity1 想传值 给 Activity2 可以把LiveData 下沉(二次封装)


package com.one_hour.test_livedata
import androidx.lifecycle.MutableLiveData
object LiveDataBusBeta{
//创建一个Map 管理 LiveData
private val liveDataMap: MutableMap<String, MutableLiveData<Any>> = HashMap()
// 设置一个 key
fun <T> getLiveData(key: String) : MutableLiveData<T>? {
if (!liveDataMap.containsKey(key)) {
liveDataMap.put(key, MutableLiveData<Any>())
}
return liveDataMap[key] as MutableLiveData<T>
}
fun removeMapLiveData(key : String) {
liveDataMap.remove(key)
}
}

像这样下沉后会出现 Bug, 如场景:当界面Activity1 向未创建的Activity2 发送消息时,会在Activity2 创建时 出现从界面1传过来的数据,这是我们不需要的。(现象出现叫 消息粘性)


什么是粘性事件

即发射的事件如果早于注册,那么注册之后依然可以接收到的事件称为粘性事件


消息粘性 咋个形成的 ?
先创建 new MutableLiveData -> setValue -> observe(绑定监听)


LiveData 绑定(observe)源码

    @MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}


1.


owner.getLifecycle() 获取的是 Lifecycle 监听Activity 生命周期变化的流程
androidx.appcompat.app.AppCompatActivity (继承)-> androidx.fragment.app.FragmentActivity (继承)-> androidx.activity.ComponentActivity (继承)->androidx.core.app.ComponentActivity( 实现 LifecycleOwner) -> 现在 实例化 private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);


androidx.core.app.ComponentActivity( 实现 LifecycleOwner)


@CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
//添加一个 mLifecycleRegistry 状态管理
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
super.onSaveInstanceState(outState);
}

androidx.activity.ComponentActivity( 实现 LifecycleOwner)


    @CallSuper
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
Lifecycle lifecycle = getLifecycle();
//设置 lifecycle 当前状态
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
}
super.onSaveInstanceState(outState);
mSavedStateRegistryController.performSave(outState);
}

androidx.fragment.app.FragmentActivity


final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);
//开始绑定什么周期 调用 handleLifecycleEvent 绑定状态
mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.xxx);

2.


** owner.getLifecycle().addObserver(wrapper); 中 addObserver 调用了 androidx.lifecycle.LifecycleRegistry的 addObserver,而LifecycleRegistry是在FragmentActivity类中实例化获取**


    @Override
public void addObserver(@NonNull LifecycleObserver observer) {
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

if (previous != null) {
return;
}
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}
、、、、、省略代码
}

statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); 在循环中一直调用


    static class ObserverWithState {
State mState;
LifecycleEventObserver mLifecycleObserver;

ObserverWithState(LifecycleObserver observer, State initialState) {
mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
mState = initialState;
}

void dispatchEvent(LifecycleOwner owner, Event event) {
State newState = getStateAfter(event);
mState = min(mState, newState);
mLifecycleObserver.onStateChanged(owner, event);
mState = newState;
}
}

LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); 监听状态改变 并且
在ObserverWithState 中调用了 mLifecycleObserver.onStateChanged(owner, event); -》mLifecycleObserver 指的就是 LifecycleBoundObserver


class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver


        @Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
// 这里是如果状态 是可见的 那么就发送消息
// 就调用 class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法
//shouldBeActive() 获取 mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); 状态 是否可见
activeStateChanged(shouldBeActive());
}

class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法 并分发 dispatchingValue 值


        void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
// immediately set active state, so we'd never dispatch anything to inactive
// owner
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
// 调用 dispatchingValue 回到 abstract class LiveData<T> 类里面的 dispatchingValue 方法
dispatchingValue(this);
}
}
}

dispatchingValue 都调用了相同的函数 considerNotify


    void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
do {
mDispatchInvalidated = false;
if (initiator != null) {
considerNotify(initiator);
initiator = null;
} else {
for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
break;
}
}
}
} while (mDispatchInvalidated);
mDispatchingValue = false;
}

considerNotify 中 observer.mObserver.onChanged 回调数据


    private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

解决粘性代码


  • 方法1


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer


class BaseLiveData<T> : MutableLiveData<T>() {
private var isSticky: Boolean = false
private var mStickyData: T? = null
private var mVersion = 0

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (isSticky) {
super.observe(owner, observer)
} else {
super.observe(owner, CustomObserver<T>(this, observer))
}

}

/**
* 发送非粘性数据
*/
override fun setValue(value: T) {
mVersion++
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
mVersion++
isSticky = false
super.postValue(value)
}

/**
* 发送粘性数据
*/
fun setStickyData(data: T?) {
mStickyData = data
isSticky = true
setValue(data!!)
}

fun postStickyData(mStickyData: T?) {
this.mStickyData = mStickyData
isSticky = true
super.postValue(mStickyData!!)
}

inner class CustomObserver<T>(val mLiveData: BaseLiveData<T>, var mObserver: Observer<in T>?,
var isSticky: Boolean = false) : Observer<T> {

private var mLastVersion = mLiveData.mVersion

override fun onChanged(t: T) {
if (mLastVersion >= mLiveData.mVersion) {
if (isSticky && mLiveData.mStickyData != null) {
mObserver?.onChanged(mLiveData.mStickyData)
}
return
}
mLastVersion = mLiveData.mVersion
mObserver?.onChanged(t)

}

}
}


  • 方法2


利用反射 修改 observer.mLastVersion 值
observer.mLastVersion 的 获取值的调用链 :
observer.mLastVersion -》considerNotify (iterator.next().getValue()) -> mObservers (SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>()) -> ObserverWrapper(int mLastVersion = START_VERSION;) (子类LifecycleBoundObserver, 但是只有父类 ObserverWrapper 才有 mLastVersion, 所以获取父类的 mLastVersion 进行修改)


import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.lang.reflect.Field
import java.lang.reflect.Method


class BaseUnStickyLiveData<T> : MutableLiveData<T>() {

private var isSticky = false

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, observer)
if (!isSticky) {
hookClass(observer)
}
}

override fun setValue(value: T) {
isSticky = false
super.setValue(value)
}

override fun postValue(value: T) {
isSticky = false
super.postValue(value)
}

fun setStickyValue(value: T) {
isSticky = true
super.setValue(value)
}

fun setStickyPostValue(value: T) {
isSticky = true
super.postValue(value)
}

private fun hookClass(observer: Observer<in T>) {
val liveDataClass = LiveData::class.java
try {
//获取field private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers
val mObservers: Field = liveDataClass.getDeclaredField("mObservers")
mObservers.setAccessible(true)

//获取SafeIterableMap集合mObservers
val observers: Any = mObservers.get(this)

//获取SafeIterableMap的get(Object obj)方法
val observersClass: Class<*> = observers.javaClass
val methodGet: Method = observersClass.getDeclaredMethod("get", Any::class.java)
methodGet.setAccessible(true)

//获取到observer在集合中对应的ObserverWrapper对象
val objectWrapperEntry: Any = methodGet.invoke(observers, observer)
var objectWrapper: Any? = null
if (objectWrapperEntry is Map.Entry<*, *>) {
objectWrapper = objectWrapperEntry.value
}
if (objectWrapper == null) {
//throw NullPointerException("ObserverWrapper can not be null")
return
}

// 获取ListData的mVersion
val mVersion: Field = liveDataClass.getDeclaredField("mVersion")
mVersion.setAccessible(true)
val mVersionValue: Any = mVersion.get(this)

//获取ObserverWrapper的Class对象 LifecycleBoundObserver extends ObserverWrapper
val wrapperClass: Class<*> = objectWrapper.javaClass.superclass

//获取ObserverWrapper的field mLastVersion
val mLastVersion: Field = wrapperClass.getDeclaredField("mLastVersion")
mLastVersion.setAccessible(true)

//把当前ListData的mVersion赋值给 ObserverWrapper的field mLastVersion
mLastVersion.set(objectWrapper, mVersionValue)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
throw RuntimeException(e)
} else {
e.printStackTrace()
}
}
}
}

配合二次 封装的 LiveDataBusBeta 使用


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

看一遍就理解:动态规划详解

前言 我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~ 什么是动态规划? 动态规划的核心思想 一个例...
继续阅读 »

前言


我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~



  • 什么是动态规划?

  • 动态规划的核心思想

  • 一个例子走进动态规划

  • 动态规划的解题套路

  • leetcode案例分析



公众号:捡田螺的小男孩


什么是动态规划?


动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。



dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.



以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。



一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。



动态规划核心思想


动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算


动态规划在于记住过往


我们来看下,网上比较流行的一个例子:




  • A : "1+1+1+1+1+1+1+1 =?"

  • A : "上面等式的值是多少"

  • B : 计算 "8"

  • A : 在上面等式的左边写上 "1+" 呢?

  • A : "此时等式的值为多少"

  • B : 很快得出答案 "9"

  • A : "你怎么这么快就知道答案了"

  • A : "只要在8的基础上加1就行了"

  • A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"



一个例子带你走进动态规划 -- 青蛙跳阶问题


暴力递归



leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。



有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:




  • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。

  • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。

  • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。



假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:


f(10) = f(9)+f(8)
f (9) = f(8) + f(7)
f (8) = f(7) + f(6)
...
f(3) = f(2) + f(1)

即通用公式为: f(n) = f(n-1) + f(n-2)

那f(2) 或者 f(1) 等于多少呢?



  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当只有1级台阶时,只有一种跳法,即f(1)= 1;


因此可以用递归去解决这个问题:


class Solution {
public int numWays(int n) {
if(n == 1){
return 1;
}
if(n == 2){
return 2;
}
return numWays(n-1) + numWays(n-2);
}
}

去leetcode提交一下,发现有问题,超出时间限制了



为什么超时了呢?递归耗时在哪里呢?先画出递归树看看:




  • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)

  • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。

  • 一直到 f(2) 和 f(1),递归树才终止。


我们先来看看这个递归的时间复杂度吧:


递归时间复杂度 = 解决一个子问题时间*子问题个数


  • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 O(1);

  • 问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)。


因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果n比较大的话,超时很正常的了。


回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如f(8)被计算了两次,f(7)被重复计算了3次...所以这个递归算法低效的原因,就是存在大量的重复计算


既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。


带备忘录的递归解法(自顶向下)


一般使用一个数组或者一个哈希map充当这个备忘录



  • 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:




  • 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~



第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。



所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:



备忘录的递归算法,子问题个数=树节点数=n,解决一个子问题还是O(1),所以带备忘录的递归算法的时间复杂度是O(n)。接下来呢,我们用带备忘录的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码如下:


public class Solution {
//使用哈希map,充当备忘录的作用
Map<Integer, Integer> tempMap = new HashMap();
public int numWays(int n) {
// n = 0 也算1种
if (n == 0) {
return 1;
}
if (n <= 2) {
return n;
}
//先判断有没计算过,即看看备忘录有没有
if (tempMap.containsKey(n)) {
//备忘录有,即计算过,直接返回
return tempMap.get(n);
} else {
// 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
return tempMap.get(n);
}
}
}

去leetcode提交一下,如图,稳了:



其实,还可以用动态规划解决这道题。


自底向上的动态规划


动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:



  • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

  • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。


动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:



  • f(n-1)和f(n-2) 称为 f(n) 的最优子结构

  • f(n)= f(n-1)+f(n-2)就称为状态转移方程

  • f(1) = 1, f(2) = 2 就是边界啦

  • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。


我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:



带备忘录的递归解法,空间复杂度是O(n),但是呢,仔细观察上图,可以发现,f(n)只依赖前面两个数,所以只需要两个变量a和b来存储,就可以满足需求了,因此空间复杂度是O(1)就可以啦



动态规划实现代码如下:


public class Solution {
public int numWays(int n) {
if (n<= 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for (int i = 3; i <= n; i++) {
temp = (a + b)% 1000000007;
a = b;
b = temp;
}
return temp;
}
}

动态规划的解题套路


什么样的问题可以考虑使用动态规划解决呢?



如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。



比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。


动态规划的解题思路


动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:



  • 穷举分析

  • 确定边界

  • 找出规律,确定最优子结构

  • 写出状态转移方程


1. 穷举分析



  • 当台阶数是1的时候,有一种跳法,f(1) =1

  • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

  • 当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3

  • 当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5

  • 当台阶是5级时......


自底向上的动态规划


2. 确定边界


通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2,当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。


3. 找规律,确定最优子结构


n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子结构?有这么一个解释:



一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质



4, 写出状态转移方程


通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:



5. 代码实现


我们实现代码的时候,一般注意从底往上遍历哈,然后关注下边界情况,空间复杂度,也就差不多啦。动态规划有个框架的,大家实现的时候,可以考虑适当参考一下:


dp[0][0][...] = 边界值
for(状态1 :所有状态1的值){
for(状态2 :所有状态2的值){
for(...){
//状态转移方程
dp[状态1][状态2][...] = 求最值
}
}
}

leetcode案例分析


我们一起来分析一道经典leetcode题目吧



给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。



示例 1:


输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:


输入:nums = [0,1,0,3,2,3]
输出:4

我们按照以上动态规划的解题思路,



  • 穷举分析

  • 确定边界

  • 找规律,确定最优子结构

  • 状态转移方程


1.穷举分析


因为动态规划,核心思想包括拆分子问题,记住过往,减少重复计算。 所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?


自顶向上的穷举

这里观察规律,显然是有关系的,我们还是遵循动态规划自底向上的原则,基于示例1的数据,从数组只有一个元素开始分析。



  • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.

  • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。

  • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。

  • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。

  • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。

  • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。

  • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。

  • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。

  • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.


分析找规律,拆分子问题

通过上面分析,我们可以发现一个规律


如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。


原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列

是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:



nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:



其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦




  • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]5小,所以就是[2]+[5]啦,即dp[4]=2

  • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]3小,所以就是[2]+[3]啦,即dp[4]=2

  • nums[5]=7,以7结尾的最长子序列就是[2,5,7][2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1



很显然有这个规律:一个以nums[i]结尾的数组nums



  • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有,dp(i) =max(dp(j))+1,


最简单的边界情况


当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1,
因此边界就是dp(1)=1。


确定最优子结构


从穷举分析,我们可以得出,以下的最优结构:


dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

max(dp(j)) 就是最优子结构。


状态转移方程


通过前面分析,我们就可以得出状态转移方程啦:



所以数组num[i]的最长递增子序列就是:


最长递增子序列 =max(dp[i])

代码实现


class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
//初始化就是边界情况
dp[0] = 1;
int maxans = 1;
//自底向上遍历
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
//从下标0到i遍历
for (int j = 0; j < i; j++) {
//找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
if (nums[j] < nums[i]) {
//因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
//求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}

参考与感谢



  • leetcode官网

  • 《labuladong算法小抄》

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

如何进一步提高flutter内存表现

前言 性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。 本文就内存优化过程中一些实践经验跟大家做一个分享。 Flutter 上线之后 闲鱼使用一套混合栈管理的方案将Flutte...
继续阅读 »

前言


性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。


本文就内存优化过程中一些实践经验跟大家做一个分享。


Flutter 上线之后


闲鱼使用一套混合栈管理的方案将Flutter嵌入到现有的App中。在产品体验上我们取得了优于Native的体验。主要得益于Flutter的在跨平台渲染方面的优势,部分原因则是因为我们用Dart语言重新实现的页面抛弃了很多历史的包袱轻装上阵。


上线之后各方面技术指标,都达到甚至超出了部分预期。而我们最为担心的一些稳定性指标,比如crash也在稳定的范围之内。但是在一段时间后我们发现由于内存过高而被系统杀死的abort率数据有比较明显的异常。性能稳定性问题是非常关键的,于是我们火速开展了问题排查。


问题定位与排查


显然问题出在了过大的内存消耗上。内存消耗在App中构成比较复杂,如何在复杂的业务中去定位到罪魁祸首呢?稍加观察,我们确定Flutter问题相对比价明显。工欲善其事必先利其器,需要更好地定位内存的问题,善用已经的工具是非常有帮助的。好在我们在Native层和Dart层都有足够多的性能分析工具进行使用。


工具分析


这里简单介绍我们如何使用的工具去观察手机数据以便于分析问题。需要注意的是,本文的重点不是工具的使用方法介绍,所以只是简单列举部分使用到的常见工具。


Xcode Instruments


Instruments是iOS内存排查的利器,可以比较便捷地观察实时内存使用情况,自然不必多说。


Xcode MemGraph + VMMap


XCode 8之后推出的MEMGraph是Xcode的内存调试利器,可以看到实时的可视化的内存。更为方便的是,你可以将MemGraph导出,配合命令行工具更好的得到结构化的信息。


Dart Observatory


这是Dart语言官方的调试工具,里面也包含了类似于Xcode的Instruments的工具。在Debug模式下Dart VM启动以后会在特定的端口接受调试请求。官方文档


观察结果


在整个过程中我进行了大量的观察,这里分享一部分典型的数据表现。


通过Xcode Instruments排查的话,我们观察到CG Raster Data这个数据有些高。这个Raster Data呢其实是图片光栅化的时候的内存消耗。


我们将App内存异常的场景的MemGraph导出来,对其执行VMMap指令得出的结果:


vmmap --summary Runner[40957].memgraph

vmmap Runner[40957].memgraph | grep 'IOKit'

vmmap Summary


vmmap address


我们主要关注resident和dirty的内存。发现IOKit占用了大量的内存。


结合Xcode Raster Data还有IOKit的大量内存消耗,我们开始怀疑问题是图内存泄漏导致的。经过进一步通过Dart Observatory观察Dart Image对象的内存情况。

Dart image instance

观察结果显示,在内存较高的场景下在Dart层的确同时存在了较多Image(如图中270)的对象。现在基本可以确定内存问题跟Dart层的图片有很大的关系。


这个结果,我估计很多人都已经想到了,App有明显的内存问题很有可能就是跟多媒体资源有关系。通过工具得出的准确数据线索,我们得到一个大致的方向去深入研究。


诡异的Dart图片数量爆炸


图片对象泄漏?


前面我们用工具观察到Dart层的Image对象数量过多直接导致了非常大的内存压力,我们起初怀疑存在图片的内存泄漏。但是我们在经过进一步确认以后发现图片其实并没有真正的泄漏。


Dart语言采用垃圾回收机制(Garbage Collection 下面开始简称GC)来管理分配的内存,VM层面的垃圾回收应该大多数情况下是可信的。但是从实际观察来看,图片数量的爆炸造成的较大的内存峰值直观感觉上GC来得有些不及时。在Debug模式下我们使用Dart Observatory手动触发GC,最终这些图片对象在没有引用的情况下最终还是会被回收。


至此,我们基本可以确认,图片对象不存在泄漏。那是什么导致了GC的反应迟钝呢,难道是Dart语言本身的问题吗?


Garbage Collection 不及时?


为此我需要了解一下Dart内存管理机制垃圾回收的实现,关于详细的内存问题我团队的 @匠修 同学已经发过一篇相关文章可以参考:内存文章


我这里不详细讨论Dart垃圾回收实现细节,只聊一聊Flutter与Dart相关的一些内容。


关于Flutter我需要首先明确几个概念:




  1. Framework(Dart)(跟iOS平台连接的库Flutter.framework要区别开)特指由Dart编写的Flutter相关代码。




  2. Dart VM执行Dart代码的Dart语言相关库,它是以C实现的Dart SDk形式提供的。对外主要暴露了C接口Dart Api。里面主要包含了Dart的编译器,运行时等等。




  3. FLutter Engine C++实现的Flutter驱动引擎。他主要负责跨平台的绘制实现,包含Skia渲染引擎的接入;Dart语言的集成;以及跟Native层的适配和Embeder相关的一些代码。简单理解,iOS平台上面Flutter.framework, Android平台上的Flutter.jar便是引擎代码构建后的产物。




在Dart代码里面对于GC是没有感知的。


对于Dart SDK也就是Dart语言我们可以做的很有限,因为Dart语言本身是一种标准,如果Dart真的有问题我们需要和Dart维护团队协作推进问题的解决。Dart语言设计的时候初衷也是希望GC对于使用者是透明的,我们不应该依赖GC实现的具体算法和策略。不过我们还是需要通过Dart SDK的源码去理解GC的大致情况。


既然我们前面已经确认并非内存泄漏,所以我们在对GC延迟的问题的调查主要放在Flutter Engine以及Dart CG入口上。


Flutter与Dart Garbage Collection


既然感觉GC不及时,先撇开消耗,我们至少可以尝试多触发几次GC来减轻内存峰值压力。但是我在仔细查阅dart_api.h(/src/third_party/dart/runtime/include/dart_api.h )接口文件后,但是并没有找到显式提供触发GC的接口。


但是找到了如下这个方法Dart_NotifyIdle


/**
* Notifies the VM that the embedder expects to be idle until |deadline|. The VM
* may use this time to perform garbage collection or other tasks to avoid
* delays during execution of Dart code in the future.
*
* |deadline| is measured in microseconds against the system's monotonic time.
* This clock can be accessed via Dart_TimelineGetMicros().
*
* Requires there to be a current isolate.
*/

DART_EXPORT void Dart_NotifyIdle(int64_t deadline);

这个接口意思是我们可以在空闲的时候显式地通知Dart,你接下来可以利用这些时间(dealine之前)去做GC。注意,这里的GC不保证会马上执行,可以理解我们请求Dart去做GC,具体做不做还是取决于Dart本身的策略。


另外,我还找到一个方法叫做Dart_NotifyLowMemory:


/**
* Notifies the VM that the system is running low on memory.
*
* Does not require a current isolate. Only valid after calling Dart_Initialize.
*/

DART_EXPORT void Dart_NotifyLowMemory();

不过这个Dart_NotifyLowMemory方法其实跟GC没有太大关系,它其实是在低内存的情况下把多余的isolate去终止掉。你可以简单理解,把一些不是必须的线程给清理掉。


在研究Flutter Engine代码后你会发现,Flutter Engine其实就是通过Dart_NotifyIdle去跟Dart层进行GC方面的协作的。我们可以在Flutter Engine源码animator.cc看到以下代码:


  
//Animator负责刷新和通知帧的绘制
if (!frame_scheduled_) {
// We don't have another frame pending, so we're waiting on user input
// or I/O. Allow the Dart VM 100 ms.
delegate_.OnAnimatorNotifyIdle(*this, dart_frame_deadline_ + 100000);
}


//delegate 最终会调用到这里
bool RuntimeController::NotifyIdle(int64_t deadline) {
if (!root_isolate_) {
return false;
}

tonic::DartState::Scope scope(root_isolate_.get());
//Dart api接口
Dart_NotifyIdle(deadline);
return true;
}

这里的逻辑比较直观:如果当前没有帧渲染的任务时候就通过NotifyIdle告诉Dart层可以进行GC操作了。注意,这里并不是说只有在这种情况下Dart才回去做GC,Flutter只是通过这种方式尽可能利用空闲去做GC,配合Dart以更合理的时间去做GC。


看到这里,我们有足够的理由去尝试一下这个接口,于是我们在一些内存压力比较大的场景进行了手动请求GC的操作。线上的Abort虽然有明显好转,但是内存峰值并没有因此得到改善。我们需要进一步找到根本原因。


图片数量爆炸的真相


为了确定图片大量囤积释放不及时的问题,我们需要跟踪Flutter图片从初始化到销毁的整个流程。


我们从Dart层开始去追寻Image对象的生命周期,我们可以看到Flutter里面所以的图片都是经过ImageProvider来获取的,ImageProvider在获取图片的时候会调用一个Resolve接口,而这个接口会首先查询ImageCache去读取图片,如果不存在缓存就new Image的实例出来。


关键代码:


  ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = new ImageStream();
T obtainedKey;
obtainKey(configuration).then((T key) {
obtainedKey = key;
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
}).catchError(
(dynamic exception, StackTrace stack) async {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while resolving an image',
silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.writeln('Image configuration: $configuration');
if (obtainedKey != null)
information.writeln('Image key: $obtainedKey');
}
));
return null;
}
);
return stream;
}

大致的逻辑



  1. Resolve 请求获取图片.

  2. 查询是否存在于ImageCache.Yes->3 NO->4

  3. 返回已经存在的图片对象

  4. 生成新的Image对象并开始加载
    看起来没有特别复杂的逻辑,不过这里我要提一下Flutter ImageCache的实现。


Flutter ImageCache


Flutter ImageCache最初的版本其实非常简单,用Map实现的基于LRU算法缓存。这个算法和实现没有什么问题,但是要注意的是ImageCache缓存的是ImageStream对象,也就是缓存的是一个异步加载的图片的对象。而且缓存没有对占用内存总量做限制,而是采用默认最大限制1000个对象(Flutter在0.5.6 beta中加入了对内存大小限制的逻辑)。缓存异步加载对象的一个问题是,在图片加载解码完成之前,无法知道到底将要消耗多少内存,至少在Flutter这个Cache实现中没有处理这个问题。具体的实现感兴趣的朋友可以阅读ImageCache.dart源码。


其实Flutter本身提供了定制化Cache的能力,所以优化ImageCache的第一步就是要根据机型的物理内存去做缓存大小的适配,设置ImageCache的合理限制。关于ImageCache的问题,可以参考官方文档和这个issue,我这里不展开去聊了。


Flutter Image生命周期


回到我们的Image对象跟踪,很明显,在缓存没有命中的情况下会有新的Image产生。继续深入代码会发现Image对象是由这段代码产生的:



Future instantiateImageCodec(Uint8List list) {
return _futurize(
(_Callback callback) => _instantiateImageCodec(list, callback, null)
);
}

String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo)
native 'instantiateImageCodec';

这里有个native关键字,这是Dart调用C代码的能力,我们查看具体的源码可以发现这个最终初始化的是一个C++的codec对象。具体的代码在Flutter Engine codec.cc。它大致的过程就是先在IO线程中启动了一个解码任务,在IO完成之后再把最终的图片对象发回UI线程。关于Flutter线程的详细介绍,我在另外一篇文章中已经有介绍,这里附上链接给有兴趣的朋友。深入理解Flutter Engine线程模型。经过来这些代码和线程分析,我们得到大致的流程图:


图片爆炸流程图


也就是说,解码任务在IO线程进行,IO任务队列里面都是C++ lambda表达式,持有了实际的解码对象,也就持有了内存资源。当IO线程任务过多的时候,会有很多IO任务在等待执行,这些内存资源也被闭包所持有而等待释放。这就是为什么直观上会有内存释放不及时而造成内存峰值的问题。这也解释了为什么之前拿到的vmmap虚拟内存数据里面IOKit是大头。


这样我们找到了关键的线索,在缓存不命中的情况下,大量初始化Image对象,导致IO线程任务繁重,而IO又持有大量的图片解码所用的内存资源。带这个推论,我在Flutter Engine的Task Runner加入了任务数量和C++ image对象的监控代码,证实了的确存在IO任务线程过载的情况,峰值在极端情况下瞬时达到了100+IO操作。


IO Runner监控


到这里问题似乎越来越明了了,但是为什么会有这么IO任务触发呢?上述逻辑虽然可能会有IO线程过载的情况下占用大量内存的情况。上层要求生成新的图片对象,这种请求是没有错误的,设计就是如此。就好比主线程阻塞大量的任务,必然会导致界面卡顿,但者却不是主线程本身的问题。我们需要从源头找到导致新对象创建暴涨真正导致IO线程过载的原因。


大量请求的根源


在前面的线索之下,我们继续寻找问题的根源。我们在实际App操作的过程当中发现,页面Push的越多,图片生成的速度越来越快。也就是说页面越多请求越快,看起来没有什么大问题。但是可见的图片其实总是在一定数量范围之内的,不应该随着页面增多而加快对象创建的频率。我们下意识的开始怀疑是否存在不可见的Image Widget也在不断请求图片的情况。最终导致了Cache无法命中而大量生成新的图片的场景。


我开始调查每个页面的图片加载请求,我们知道Flutter里面万物皆Widget,页面都是是Widget,由Navigator管理。我在Widget的生命周期方法(详细见Flutter官方文档)中加入监控代码,如我所料,在Navigator栈底下不可见的页面也还在不停的Resolve Image,直接导致了image对象暴涨而导致IO线程过载,导致了内存峰值。


看起来,我们终于找到了根本原因。解决方案并不难。在页面不可见的时候没必要发出多余的图片加载请求,峰值也就随之降下来了。再经过一番代码优化和测试以后问题得到了根本上的解决。优化上线以后,我们看到了数据发生了质的好转。
有朋友可能想问,为什么不可见的Widget也会被调用到相关的生命周期方法。这里我推荐阅读Flutter官方文档关于Widget相关的介绍,篇幅有限我这里不展开介绍了。widgets


至此,我们已经解决了一个较为严重的内存问题。内存优化情况复杂,可以点也比较多,接下来我继续简要分享在其它一些方面的优化方案。


截图缓存优化


文件缓存+预加载策略


我们是采用嵌入式Flutter并使用一套混合栈模式管理Native和Flutter页面相互跳转的逻辑。由于FlutterView在App中是单例形式存在的,我们为了更好的用户体验,在页面切换的过程中使用的截图的方式来进行过渡。


大家都知道,图片是非常占用内存的对象,我们如何在不降低用户体验的同时获得最小的内存消耗呢?假如我们每push一个页面都保存一张截图,那么内存是以线性复杂度增长的,这显然不够好。


内存和空间在大多数情况下是一个互相转换的关系,优化很多时候其实是找一个合理的折中点。
最终我采用了预加载+缓存的策略,在页面最多只在内存中同时存在两个截图,其它的存文件,在需要的时候提前进行预加载。
简要流程图:


简要流程图


这样的话就做到了不影响用户体验的前提下,将空间复杂度从O(n)降低到了O(1)。
这个优化进一步节省了不必要的内存开销。


截图额外的优化



  • 针对当前设备的内存情况,自适应调整截图的分辨率,争取最小的内存消耗。

  • 在极端的内存情况下,把所有截图都从内存中移除存(存文件可恢复),采用PlaceHolder的形式。极端情况下避免被杀,保证可用性的体验降级策略。


页面兜底策略


对于电商类App存在一个普遍的问题,用户会不断的push页面到栈里面,我们不能阻止用户这种行为。我们当然可以把老页面干掉,每次回退的时候重新加载,但是这种用户体验跟Web页一样,是用户不可接受的。我们要维持页面的状态以保证用户体验。这必然会导致内存的线性增长,最终肯定难免要被杀。我们优化的目的是提高用户能够push的极限页面数量。


对于Flutter页面优化,除了在优化每一个页面消耗的内存之外,我们做了降级兜底策略去保证App的可用性:在极端情况下将老页面进行销毁,在需要的时候重新创建。这的确降低了用户体验,在极端情况下,降级体验还是比Crash要好一些。



FlutterViewController 单例析构


另外我想讨论的一个话题是关于FlutterViewController的。目前Flutter的设计是按照单例模式去运行的,这对于完全用Flutterc重新开发的App没有太大的问题。但是对于混合型App,多出来的常驻内存确实是一个问题。


实际上,Flutter Engine底层实现是考虑到了析构这个问题,有相关的接口。但是在Embeder这一层(具体FlutterViewController Message Channels这一层),在实现过程中存在一些循环引用,导致在Native层就算没有引用FlutterViewController的时候也无法释放.


FlutterViewController引用图


我在经过一段时间的尝试后,算是把循环引用解除了。这些循环引用主要集中在FlutterChannel这一块。在解除之后我顺利的释放了FlutterViewController,可以明显看到常驻内存得到了释放。但是我发现释放FlutterViewController的时候会导致一部分Skia Image对象泄漏,因为Skia Objects必须在它创建的线程进行释放(详情请参考skia_gpu_object.cc源码),线程同步的问题。关于这个问题我在GitHub上面有一个issue大家可以参考。FlutterViewController释放issue


目前,这个优化我们已经反馈给Flutter团队,期待他们官方支持。希望大家可以一起探索研究。


进一步探讨


除此之外,Flutter内存方面其实还有比较多方面可以去研究。我这里列举几个目前观察到的问题。




  1. 我在内存分析的时候发现Flutter底层使用的boring ssl库有可以确定的内存泄漏。虽然这个泄漏比较缓慢,但是对于App长期运行还是有影响的。我在GitHub上面提了个issue跟进,目前已有相关的人员进行跟进。SSL leak issue




  2. 关于图片渲染,目前Flutter还是有优化空间的,特别是图片的按需剪裁。大多数情况下是没有不要将整一个bitmap解压到内存中的,我们可以针对显示的区域大小和屏幕的分辨率对图片进行合理的缩放以取得最好的性能消耗。




  3. 在分析Flutter内存的MemGraph的时候,我发现Skia引擎当中对于TextLayout消耗了大量的内存.目前我没有找到具体的原因,可能存在优化的空间。




结语


在这篇文章里,我简要的聊了一下目前团队在Flutter应用内存方面做出的尝试和探索。短短一篇文章无法包含所有内容,只能推出了几个典型的案例来作分析,希望可以跟大家一起探讨研究。欢迎感兴趣的朋友一起研究,如有更好的想法方案,我非常乐意看到你的分享。


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

Flutter动画实现粒子漂浮效果

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。 本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。 效果图 (这里为了方便录制gif,动画设置的较快;如果将动画的Duration设...
继续阅读 »

要问2019年最火的移动端框架,肯定非Google的Flutter莫属。

image

本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。




效果图


image


(这里为了方便录制gif,动画设置的较快;如果将动画的Duration设置成20s,看起来就是浮动的效果了)
粒子碰撞的效果参考了张风捷特列 大佬的Flutter动画之粒子精讲


1. Flutter的动画原理



在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。



简而言之,就是逐帧绘制,只要屏幕刷新的足够快,我们就会觉得这是个连续的动画。
设想一个小球从屏幕顶端移动到底端的动画,为了完成这个动画,我们需要哪些数据呢?



  • 小球的运动轨迹,即起始点s、终点e和中间任意一点p

  • 动画持续时长t


只有这两个参数够吗?明显是不够的,因为小球按照给定的轨迹运动,可能是匀速、先快后慢、先慢后快、甚至是一会儿快一会慢的交替地运动,只要在时间t内完成,都是可能的。所以我们应该再指定一个参数c来控制动画的速度。


1.1 vsync探究


废话不多说,我们看看Flutter中是动画部分的代码:


AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});
复制代码

简要分析一下,AnimationController,顾名思义,控制器,用来控制动画的播放。传入的参数中,duration我们知道是前面提到过的动画持续时长t,那这个vsync是啥参数呢?打过游戏的同学可能对这个单词有印象,vsync 就是 垂直同步 。那什么是垂直同步呢?



垂直同步又称场同步(Vertical Hold),从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约。



简而言之就是,显卡在完成渲染后,将画面数据送往显存中,而显示器从显存中一行一行从上到下取出画面数据,进行显示。但是屏幕的刷新率和显卡渲染数据的速度很多时候是不匹配的,试想一下,显示器刚扫描显示完屏幕上半部分的画面,正准备从显存取下面的画面数据时,显卡送来了下一帧的图像数据覆盖了原来的显存,这个时候显示器取出的下面部分的图像就和上面的不匹配,造成画面撕裂。


为了避免这种情况,我们引入垂直同步信号,只有在显示器完整的扫描显示完一帧的画面后,显卡收到垂直同步信号才能刷新显存。
可是这个物理信号跟我们flutter动画有啥关系呢?vsync对应的参数是this,我们继续分析一下this对应的下面的类。


class _RunBallState extends State<RunBall> with TickerProviderStateMixin 
复制代码

with关键字是使用该类的方法而不继承该类,Mixin是类似于Java的接口,区别在于Mixin中的方法不是抽象方法而是已经实现了的方法。



这个TickerProviderStateMixin到底是干啥的呢???经过哥们儿Google的帮助,在网上找到了



关于动画的驱动,在此简单的说一下,Ticker是被SchedulerBinding所驱动。SchedulerBinding则是监听着Window.onBeginFrame回调。
Window.onBeginFrame的作用是什么呢,是告诉应用该提供一个scene了,它是被硬件的VSync信号所驱动的。



于是我们终于发现了,绕了一圈,归根到底还是真正的硬件产生的垂直同步信号在驱动着Flutter的动画的进行。


..addListener(() {
//_renderBezier();
print(controllerG.value);
print('这是第${++count}次回调');
});

复制代码

注意到之前的代码中存在一个动画控制器的监听器,动画在执行时间内,函数回调controller.value会生成一个从0到1的double类型的数值。我们在控制台打印出结果如下:

image


image


经过观察,两次试验,在2s的动画执行时间内,该回调函数分别被执行了50次,53次,并不是一个固定值。也就是说硬件(模拟器)的屏幕刷新率大概维持在(25~26.5帧/s)。


结论:硬件决定动画刷新率


1.2 动画动起来


搞懂了动画的原理之后,我们接下来就是逐帧的绘制了。关于Flutter的自定义View,跟android原生比较像。


image


继承CustomPainter类,重写paint和shouldRepaint方法,具体实现可以看代码.


class Ball {
double aX;
double aY;
double vX;
double vY;
double x;
double y;
double r;
Color color;}

复制代码

小球Ball具有圆心坐标、半径、颜色、速度、加速度等属性,通过数学表达式计算速度和加速度的关系,就可以实现匀加速的效果。


//运动学公式,看起来少了个时间t;实际上这些函数在动画过程中逐帧回调,把每帧刷新周期当成单位时间,相当于t=1
_ball.x += _ball.vX;//位移=速度*时间
_ball.y += _ball.vY;
_ball.vX += _ball.aX;//速度=加速度*时间
_ball.vY += _ball.aY;

复制代码

控制器使得函数不断回调,在回调函数函数里改变小球的相关参数,并且调用setState()函数,使得UI重新绘制;小球的轨迹坐标不断地变化,逐帧绘制的小球看起来就在运动了。你甚至可以在添加效果使得小球在撞到边界时变色或者半径变小(参考文章开头的粒子碰撞效果图)。


2. 小球随机浮动的思考


问题来了,我想要一个漂浮的效果呢?最好是随机的轨迹,就像气泡在空中飘乎不定,于是引起了我的思考;匀速然后方向随机?感觉不够优雅,于是去网上搜了一下,发现了思路!



首先随机生成一条贝塞尔曲线作为轨迹,等小球运动到终点,再生成新的贝塞尔曲线轨迹



生成二阶贝塞尔曲线的公式如下:


//二次贝塞尔曲线轨迹坐标,根据参数t返回坐标;起始点p0,控制点p1,终点p2
Offset quadBezierTrack(double t, Offset p0, Offset p1, Offset p2) {
var bx = (1 - t) * (1 - t) * p0.dx + 2 * t * (1 - t) * p1.dx + t * t * p2.dx;
var by = (1 - t) * (1 - t) * p0.dy + 2 * t * (1 - t) * p1.dy + t * t * p2.dy;

return Offset(bx, by);
}
复制代码

很巧的是,这里需要传入一个0~1之间double类型的参数t,恰好前面我们提过,animationController会在给定的时间内,生成一个0~1的value;这太巧了。


起始点的坐标不用说,接下来就剩解决控制点p1和p2,当然是随机生成这两点,但是如果同时有多个小球呢?比如5个小球同时进行漂浮,每个小球都对应一组三个坐标的信息,给小球Ball添加三个坐标的属性?不,这个时候,我们可以巧妙地利用带种子参数的随机数。



我们知道随机数在生成的时候,如果种子相同的话,每次生成的随机数也是相同的。



每个小球对象在创建的时候自增地赋予一个整形的id,作为随机种子;比如5个小球,我们起始的id为:2,4,6,8,10;


    Offset p0 = ball.p0;//起点坐标
Offset p1 = _randPosition(ball.id);
Offset p2 = _randPosition(ball.id + 1);
复制代码

rand(2),rand(2+1)为第一个小球的p1和p2坐标;当所有小球到达终点时,此时原来的终点p2为新一轮贝塞尔曲线的起点;此时相应的id也应增加,为了防止重复,id应增加小球数量5 *2,即第二轮运动开始时,5个小球的id为:12,14,16,18,20。
这样就保证了每轮贝塞尔曲线运动的时候,对于每个小球而言,p0,p1,p2是确定的;新一轮的运动所需要的随机的三个坐标点,只需要改变id的值就好了。


Path quadBezierPath(Offset p0, Offset p1, Offset p2) {
Path path = new Path();
path.moveTo(p0.dx, p0.dy);
path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
return path;
}
复制代码

这个时候,我们还可以利用Flutter自带的api画出二次贝塞尔曲线的轨迹,看看小球的运动是否落在轨迹上。


image


2.1 一些细节


animation = CurvedAnimation(parent: controllerG, curve: Curves.bounceOut);
复制代码

这里的Curve就是前面提到的,控制动画过程的参数,flutter自带了挺多效果,我最喜欢这个bounceOut(弹出效果)

image


 animation.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
// TODO: Handle this case.
break;
case AnimationStatus.forward:
// TODO: Handle this case.
break;
case AnimationStatus.reverse:
// TODO: Handle this case.
break;
case AnimationStatus.completed:
// TODO: Handle this case.
controllerG.reset();
controllerG.forward();
break;
}
});

复制代码

监听动画过程的状态,当一轮动画结束时,status状态为AnimationStatus.completed;此时,我们将控制器reset重置,再forward重新启动,此时就会开始新一轮的动画效果;如果我们选的是reverse,则动画会反向播放。




GestureDetector(
child: Container(
width: double.infinity,
height: 200,
child: CustomPaint(
painter: FloatBallView(_ballsF, _areaF),
),
),
onTap: () {
controllerG.forward();
},
onDoubleTap: () {
controllerG.stop();
},
),
复制代码

为了方便控制,我还加了个手势监听器,单击控制动画运行,双击暂停动画。


3 完结


水平有限,文中如有错误还请各位指出,我是梦龙Dragon


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

浅探Google V8引擎

探析它之前,我们先抛出以下几个疑问:为什么需要 V8 引擎呢?V8 引擎到底是个啥?它可以做些什么呢?了解它能有什么收获呢?接下来就针对以上几个问题进行详细描述。由来我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命...
继续阅读 »

探析它之前,我们先抛出以下几个疑问:

  • 为什么需要 V8 引擎呢?

  • V8 引擎到底是个啥?

  • 它可以做些什么呢?

  • 了解它能有什么收获呢?

接下来就针对以上几个问题进行详细描述。

由来

我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命名一个变量时不需要声明变量的类型)、弱类型、基于原型的语言,内置支持类型。而一般 JS 都是在前端执行(直接影响界面),需要能够快速响应用户,那么就要求语言本身可以被快速地解析和执行,JS 引擎就为此而问世。

这里提到了解释型语言和静态语言(编译型语言),先简单介绍一下二者:

  • 解释型语言(JS)

    • 每次运行时需要解释器按句依次解释源代码执行,将它们翻译成机器认识的机器代码再执行

  • 编译型语言(Java)

    • 运行时可经过编译器翻译成可执行文件,再由机器运行该可执行文件即可

从上面的描述中可以看到 JS 运行时每次都要根据源文件进行解释然后执行,而编译型的只需要编译一次,下次可直接运行其可执行文件,但是这样就会导致跨平台的兼容性很差,因此各有优劣。

而众多 JS 引擎(V8、JavaScriptCore、SpiderMonkey、Chakra等)中 V8 是最为出色的,加上它也是应用于当前最流行的谷歌浏览器,所以我们非常有必要去认识和了解一下,这样对于开发者也就更清楚 JS 在浏览器中到底是如何运行的了。

认识

定义

  • 使用 C++ 开发

  • 谷歌开源

  • 编译成原生机器码(支持IA-32, x86-64, ARM, or MIPS CPUs)

  • 使用了如内联缓存(inline caching)等方法来提高性能

  • 运行速度快,可媲美二进制程序

  • 支持众多操作系统,如 windows、linux、android 等

  • 支持其他硬件架构,如 IA32,X64,ARM 等

  • 具有很好的可移植和跨平台特性

运行

先来一张官方流程图:

img

准备

JS 文件加载(不归 V8 管):可能来自于网络请求、本地的cache或者是也可以是来自service worker,这是 V8 运行的前提(有源文件才有要解释执行的)。 3种加载方式 & V8的优化

  • Cold load: 首次加载脚本文件时,没有任何数据缓存

  • Warm load:V8 分析到如果使用了相同的脚本文件,会将编译后的代码与脚本文件一起缓存到磁盘缓存中

  • Hot load: 当第三次加载相同的脚本文件时,V8 可以从磁盘缓存中载入脚本,并且还能拿到上次加载时编译后的代码,这样可以避免完全从头开始解析和编译脚本

而在 V8 6.6 版本的时候进一步改进代码缓存策略,简单讲就是从缓存代码依赖编译过程的模式,改变成两个过程解耦,并增加了可缓存的代码量,从而提升了解析和编译的时间,大大提升了性能,具体细节见V8 6.6 进一步改进缓存性能

分析

此过程是将上面环节得到的 JS 代码转换为 AST(抽象语法树)。

词法分析

从左往右逐个字符地扫描源代码,通过分析,产生一个不同的标记,这里的标记称为 token,代表着源代码的最小单位,通俗讲就是将一段代码拆分成最小的不可再拆分的单元,这个过程称为词法标记,该过程的产物供下面的语法分析环节使用。

这里罗列一下词法分析器常用的 token 标记种类:

  • 常数(整数、小数、字符、字符串等)

  • 操作符(算术操作符、比较操作符、逻辑操作符)

  • 分隔符(逗号、分号、括号等)

  • 保留字

  • 标识符(变量名、函数名、类名等)

TOKEN-TYPE TOKEN-VALUE\
-----------------------------------------------\
T_IF                 if\
T_WHILE              while\
T_ASSIGN             =\
T_GREATTHAN          >\
T_GREATEQUAL         >=\
T_IDENTIFIER name    / numTickets / ...\
T_INTEGERCONSTANT    100 / 1 / 12 / ....\
T_STRINGCONSTANT     "This is a string" / "hello" / ...

上面提到会逐个从左至右扫描代码然后分析,那么很明显就会想到两种方案,扫描完再分析(非流式处理)和边扫描边分析(流式处理),简单画一下他们的时序图就能发现流式处理效率要高得多,同时分析完也会释放分析过程中占用的内存,也能大大提高内存使用效率,可见该优化的细节处理。

语法分析

语法分析是指根据某种给定的形式文法对由单词序列构成的输入文本(例如上个阶段的词法分析产物-tokens stream),进行分析并确定其语法结构的过程,最后产出其 AST(抽象语法树)。

V8 会将语法分析的过程分为两个阶段来执行:

  • Pre-parser

    • 跳过还未使用的代码

    • 不会生成对应的 AST,会产生不带有变量的引用和声明的 scopes 信息

    • 解析速度会是 Full-parser 的 2 倍

    • 根据 JS 的语法规则仅抛出一些特定的错误信息

  • Full-parser

    • 解析那些使用的代码

    • 生成对应的 AST

    • 产生具体的 scopes 信息,带有变量引用和声明等信息

    • 抛出所有的 JS 语法错误

为什么要做两次解析?

如果仅有一次,那只能是 Full-parser,但这样的话,大量未使用的代码会消耗非常多的解析时间,结合实例来看下:通过 Coverage 录制的方式可以分析页面哪些代码没有用到,如下图可以看到最高有 75% 的没有被执行。

img

但是预解析并不是万能的,得失是并存的,很明显的一个场景:该文件中的代码全都执行了,那其实就是没必要的,当然这种情况其实还是占比远不如上面的例子,所以这里其实也是一种权衡,需要照顾大多数来达到综合性能的提升。

下面给出一个示例:

function add(x, y) {
   if (typeof x === "number") {
       return x + y;
  } else {
       return x + 'tadm';
  }
}

复制上面的代码到 web1web2 可以很直观的看到他们的 tokens 和 AST 结构(也可自行写一些代码体验)。

img

  • tokens

[
  {
       "type": "Keyword",
       "value": "function"
  },
  {
       "type": "Identifier",
       "value": "add"
  },
  {
       "type": "Punctuator",
       "value": "("
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": ","
  },
  {
       "type": "Identifier",
       "value": "y"
  },
  {
       "type": "Punctuator",
       "value": ")"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "if"
  },
  {
       "type": "Punctuator",
       "value": "("
  },
  {
       "type": "Keyword",
       "value": "typeof"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "==="
  },
  {
       "type": "String",
       "value": "\"number\""
  },
  {
       "type": "Punctuator",
       "value": ")"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "return"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "+"
  },
  {
       "type": "Identifier",
       "value": "y"
  },
  {
       "type": "Punctuator",
       "value": ";"
  },
  {
       "type": "Punctuator",
       "value": "}"
  },
  {
       "type": "Keyword",
       "value": "else"
  },
  {
       "type": "Punctuator",
       "value": "{"
  },
  {
       "type": "Keyword",
       "value": "return"
  },
  {
       "type": "Identifier",
       "value": "x"
  },
  {
       "type": "Punctuator",
       "value": "+"
  },
  {
       "type": "String",
       "value": "'tadm'"
  },
  {
       "type": "Punctuator",
       "value": ";"
  },
  {
       "type": "Punctuator",
       "value": "}"
  },
  {
       "type": "Punctuator",
       "value": "}"
  }
]
  • AST

{
 "type": "Program",
 "body": [
  {
     "type": "FunctionDeclaration",
     "id": {
       "type": "Identifier",
       "name": "add"
    },
     "params": [
      {
         "type": "Identifier",
         "name": "x"
      },
      {
         "type": "Identifier",
         "name": "y"
      }
    ],
     "body": {
       "type": "BlockStatement",
       "body": [
        {
           "type": "IfStatement",
           "test": {
             "type": "BinaryExpression",
             "operator": "===",
             "left": {
               "type": "UnaryExpression",
               "operator": "typeof",
               "argument": {
                 "type": "Identifier",
                 "name": "x"
              },
               "prefix": true
            },
             "right": {
               "type": "Literal",
               "value": "number",
               "raw": "\"number\""
            }
          },
           "consequent": {
             "type": "BlockStatement",
             "body": [
              {
                 "type": "ReturnStatement",
                 "argument": {
                   "type": "BinaryExpression",
                   "operator": "+",
                   "left": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "right": {
                     "type": "Identifier",
                     "name": "y"
                  }
                }
              }
            ]
          },
           "alternate": {
             "type": "BlockStatement",
             "body": [
              {
                 "type": "ReturnStatement",
                 "argument": {
                   "type": "BinaryExpression",
                   "operator": "+",
                   "left": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "right": {
                     "type": "Literal",
                     "value": "tadm",
                     "raw": "'tadm'"
                  }
                }
              }
            ]
          }
        }
      ]
    },
     "generator": false,
     "expression": false,
     "async": false
  }
],
 "sourceType": "script"
}

解释

该阶段就是将上面产生的 AST 转换成字节码。

这里增加字节码(中间产物)的好处是,并不是将 AST 直接翻译成机器码,因为对应的 cpu 系统会不一致,翻译成机器码时要结合每种 cpu 底层的指令集,这样实现起来代码复杂度会非常高;还有个就是内存占用的问题,因为机器码会存储在内存中,而退出进程后又会存储在磁盘上,加上转换后的机器码多出来很多信息,会比源文件大很多,导致了严重的内存占用问题。

V8 在执行字节码的过程中,使用到了通用寄存器累加寄存器,函数参数和局部变量保存在通用寄存器里面,累加器中保存中间计算结果,在执行指令的过程中,如果直接由 cpu 从内存中读取数据的话,比较影响程序执行的性能,使用寄存器存储中间数据的设计,可以大大提升 cpu 执行的速度。

编译

这个过程主要是 V8 的 TurboFan编译器 将字节码翻译成机器码的过程。

字节码配合解释器和编译器这一技术设计,可以称为JIT(即时编译技术),Java 虚拟机也是类似的技术,解释器在解释执行字节码时,会收集代码信息,标记一些热点代码(就是一段代码被重复执行多次),TurboFan 会将热点代码直接编译成机器码,缓存起来,下次调用直接运行对应的二进制的机器码,加快执行速度。

在 TurboFan 将字节码编译成机器码的过程中,还进行了简化处理:常量合并、强制折减、代数重新组合。

比如:3 + 4 --> 7,x + 1 + 2 --> x + 3 ......

执行

到这里我们就开始执行上一阶段产出的机器码。

而在 JS 的执行过程中,经常遇到的就是对象属性的访问。作为一种动态的语言,一个简单的属性访问可能包含着复杂的语义,比如Object.xxx的形式,可能是属性的直接访问,也可能去调用的对象的Getter方法,还有可能是要通过原型链往上层对象中查找。这种不确定性而且动态判断的情况,会浪费很多查找时间,所以 V8 会把第一次分析的结果放在缓存中,当再次访问相同的属性时,会优先从缓存中去取,调用 GetProperty(Object, "xxx", feedback_cache) 的方法获取缓存,如果有缓存结果,就会跳过查找过程,又大大提升了运行性能。

除了上面针对读取对象属性的结果缓存的优化,V8 还引入了 Object Shapes(隐藏类)的概念,这里面会记录一些对象的基本信息(比如对象拥有的所有属性、每个属性对于这个对象的偏移量等),这样我们去访问属性时就可以直接通过属性名和偏移量直接定位到他的内存地址,读取即可,大大提升访问效率。

既然 V8 提出了隐藏类(两个形状相同的对象会去复用同一个隐藏类,何为形状相同的对象?两个对象满足有相同个数的相同属性名称和相同的属性顺序),那么我们开发者也可以很好的去利用它:

  • 尽量创建形状相同的对象

  • 创建完对象后尽量不要再去操作属性,即不增加或者删除属性,也就不会破环对象的形状

完成

到此 V8 已经完成了一份 JS 代码的读取、分析、解释、编译、执行。

总结

以上就是从 JS 代码下载到最终在 V8 引擎执行的过程分析,可以发现 V8 其实有很多实现的技术点,有着很巧妙的设计思想,比如流式处理、缓存中间产物、垃圾回收等,这里面又会涉及到很多细节,很值得继续深入研究。

作者:Tadm
来源:https://juejin.cn/post/7032278688192430117

收起阅读 »

手写清除console的loader

前言删除console方式介绍通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅 因此下面需要介绍几种优雅的清除方式该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在...
继续阅读 »




前言

作为一个前端,对于console.log的调试可谓是相当熟悉,话不多说就是好用!帮助我们解决了很多bug^_^
但是!有因必有果(虽然不知道为什么说这句但是很顺口),如果把console发到生产环境也是很头疼的,尤其是如果打印的信息很私密的话,可能要凉凉TT

删除console方式介绍

对于在生产环境必须要清除的console语句,如果手动一个个删除,听上去就很辛苦,因此这篇文章本着看到了就要学,学到了就要用的精神我打算介绍一下手写loader的方式清除代码中的console语句,在此之前也介绍一下其他可以清除console语句的方式吧哈哈

1. 方式一:暴力清除

通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅
因此下面需要介绍几种优雅的清除方式

2. 方式二 :uglifyjs-webpack-plugin

该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在webpack的optimization下,即可使用,需要注意的是:此配置只在production环境下生效

安装
npm i uglifyjs-webpack-plugin

其中drop_console和pure_funcs的区别是:

  • drop_console的配置值为boolean,也就是说如果为true,那么代码中所有带console前缀的调试方式都会被清除,包括console.log,console.warn等

  • pure_funcs的配置值是一个数组,也就是可以配置清除那些带console前缀的语句,截图中配的是['console.log'],因此生产环境上只会清除console.log,如果代码中包含其他带console的前缀,如console.warn则保留

但是需要注意的是,该方法只对ES5语法有效,如果你的代码中涉及ES6就会报错

3. 方式三:terser-webpack-plugin

webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

安装
npm i terser-webpack-plugin@4

terser-webpack-plugin对于清楚console的配置可谓是跟uglifyjs-webpack-plugin一点没差,但是他们最大的差别就是TerserWebpackPlugin支持ES6的语法

4. 方式四:手写loader删除console

终于进入了主题了,朋友们

  1. 什么是loader

众所周知,webpack只能理解js,json等文件,那么除了js,json之外的文件就需要通过loader去顺利加载,因此loader在其中担任的就是翻译工作。loader可以看作一个node模块,实际上就是一个函数,但他不能是一个箭头函数,因为它需要继承webpack的this,可以在loader中使用webpack的方法。

  • 单一原则,一个loader只做一件事

  • 调用方式,loader是从右向左调用,遵循链式调用

  • 统一原则,输入输出都是字符串或者二进制数据

根据第三点,下面的代码就会报错,因为输出的是数字而不是字符串或二进制数据

module.exports = function(source) {
  return 111
}

  1. 新建清除console语句的loader

首先新建一个dropConsole.js文件

// source:表示当前要处理的内容
const reg = /(console.log\()(.*)(\))/g;
module.exports = function(source) {
  // 通过正则表达式将当前处理内容中的console替换为空字符串
  source = source.replace(reg, "")
  // 再把处理好的内容return出去,坚守输入输出都是字符串的原则,并可达到链式调用的目的供下一个loader处理
  return source
}
  1. 在webpack的配置文件中引入

module: {
  rules:[
      {
          test: /\.js/,
          use: [
              {
              loader: path.resolve(__dirname, "./dropConsole.js"),
              options: {
                name: "前端"
              }
              }
          ]
      },
    {
  ]
}

在webpack的配置中,loader的导入需要绝对路径,否则导入失效,如果想要像第三方loader一样引入,就需要配置resolveLoader 中的modules属性,告诉webpack,当node_modules中找不到时,去别的目录下找

module: {
  rules:[
      {
          test: /\.js/,
          use: [
              {
              loader: 'dropConsole',
              options: {
                name: "前端"
              }
              }
          ]
      },
    {
  ]
}
resolveLoader:{
  modules:["./node_modules","./build"] //此时我的loader写在build目录下
},

正常运行后,调试台将不会打印console信息

  1. 最后介绍几种在loader中常用的webpack api

  • this.query:返回webpack的参数即options的对象

  • this.callback:同步模式,可以把自定义处理好的数据传递给webpack

const reg = /(console.log\()(.*)(\))/g;
module.exports = function(source) {
  source = source.replace(reg, "");
  this.callback(null,source);
  // return的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
  return    
}
  • this.async():异步模式,可以大致的认为是this.callback的异步版本,因为最终返回的也是this.callback

const  path = require('path')
const util = require('util')
const babel = require('@babel/core')


const transform = util.promisify(babel.transform)

module.exports = function(source,map,meta) {
var callback = this.async();

transform(source).then(({code,map})=> {
    callback(null, code,map)
}).catch(err=> {
    callback(err)
})
};

最后的最后,webpack博大精深,值得我们好好学习,深入研究!

作者:我也想一夜暴富
来源:https://juejin.cn/post/7038413043084034062

收起阅读 »

uniapp热更新

热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦...
继续阅读 »



为什么要热更新

热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦,所以这个时候热更新就显得很重要了。

首先你需要在manifest.json 中修改版本号

如果之前是1.0.0那么修改之后比如是1.0.1或者1.1.0这样

然后你需要在HBuilderX中打一个wgt包

在顶部>发行>原生App-制作移动App资源升级包

包的位置会在控制台里面输出

你需要和后端约定一下接口,传递参数

然后你就可以在app.vue的onLaunch里面编写热更新的代码了,如果你有其他需求,你可以在其他页面的onLoad里面编写。

// #ifdef APP-PLUS  //APP上面才会执行
plus.runtime.getProperty(plus.runtime.appid,
function(widgetInfo) {
uni.request({
url: '请求url写你自己的',
method: "POST",
data: {
version: widgetInfo.version,
//app版本号
name: widgetInfo.name //app名称
},
success: (result) = >{
console.log(result) //请求成功的数据
var data = result.data.data
if (data.update && data.wgtUrl) {
var uploadTask = uni.downloadFile({ //下载
url: data.wgtUrl,
//后端传的wgt文件
success: (downloadResult) = >{ //下载成功执行
if (downloadResult.statusCode === 200) {
plus.runtime.install(downloadResult.tempFilePath, {
force: flase
},
function() {
plus.runtime.restart();
},
function(e) {});
}
},
}) uploadTask.onProgressUpdate((res) = >{
// 测试条件,取消上传任务。
if (res.progress == 100) { //res.progress 上传进度
uploadTask.abort();
}
});
}
}
});
});
// #endif

不支持的情况

  • SDK 部分有调整,比如新增了 Maps 模块等,不可通过此方式升级,必须通过整包的方式升级。

  • 原生插件的增改,同样不能使用此方式。
    对于老的非自定义组件编译模式,这种模式已经被淘汰下线。但以防万一也需要说明下,老的非自定义组件编译模式,如果之前工程没有 nvue 文件,但更新中新增了 nvue 文件,不能使用此方式。因为非自定义组件编译模式如果没有nvue文件是不会打包weex引擎进去的,原生引擎无法动态添加。自定义组件模式默认就含着weex引擎,不管工程下有没有nvue文件。

注意事项

  • 条件编译,仅在 App 平台执行此升级逻辑。

  • appid 以及版本信息等,在 HBuilderX 真机运行开发期间,均为 HBuilder 这个应用的信息,因此需要打包自定义基座或正式包测试升级功能。

  • plus.runtime.version 或者 uni.getSystemInfo() 读取到的是 apk/ipa 包的版本号,而非 manifest.json 资源中的版本信息,所以这里用 plus.runtime.getProperty() 来获取相关信息。

  • 安装 wgt 资源包成功后,必须执行 plus.runtime.restart(),否则新的内容并不会生效。

  • 如果App的原生引擎不升级,只升级wgt包时需要注意测试wgt资源和原生基座的兼容性。平台默认会对不匹配的版本进行提醒,如果自测没问题,可以在manifest中配置忽略提示,详见ask.dcloud.net.cn/article/356…

  • http://www.example.com 是一个仅用做示例说明的地址,实际应用中应该是真实的 IP 或有效域名,请勿直接复制粘贴使用。

关于热更新是否影响应用上架

应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对热更新大多持排斥态度。

但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成三方黑客可篡改其他App的数据。

使用热更新需要注意:

  • 上架审核期间不要弹出热更新提示

  • 热更新内容使用https下载,避免被三方网络劫持

  • 不要更新违法内容、不要通过热更新破坏应用市场的利益,比如iOS的虚拟支付要老老实实给Apple分钱

如果你的应用没有犯这些错误,应用市场是不会管的。

作者:是一个秃头
来源:https://juejin.cn/post/7039273141901721608

收起阅读 »

GC回收机制与分代回收策略

GC回收机制一、前言垃圾回收:Garbage Collection,简写 GC。JVM 中的垃圾回收器会自动回收无用的对象。但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与...
继续阅读 »



GC回收机制

一、前言

垃圾回收Garbage Collection,简写 GCJVM 中的垃圾回收器会自动回收无用的对象。

但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与调节。

在虚拟机中,程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而执行着出栈和入栈操作。所以这几个区域不需要考虑回收的问题。

而在 堆和方法区 中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。这部分的只有在程序运行期间才会知道需要创建哪些对象,这部分的内存的创建和回收是动态的,也是垃圾回收器重点关注的地方。

二、什么是垃圾

垃圾 就是 内存中已经没有用的对象。既然是 垃圾回收,就必须知道哪些对象是垃圾。

Java 虚拟机中使用了一种叫做 可达性分析 的算法 来决定对象是否可以被回收

GCRoot示意图

上图中 A、B、C、D、E 与 GCRoot 直接或间接产生引用链,所以 GC 扫描到这些对象时,并不会执行回收操作;J、K、M虽然之间有引用链,但是并没有与 GCRoot 存在引用链,所以当 GC 扫描到他们时会将他们回收。

注意的是,上图中所有的对象,包括 GCRoot,都是内存中的引用。

作为 GCRoot 的几种对象
  1. Java虚拟机栈(局部变量表)中的引用的对象;

  2. 方法区中静态引用指向的对象;

  3. 仍处于存活状态中的线程对象;

  4. Native方法中 JNI 引用的对象;

三、什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但一般情况下都会存在下面两种情况:

  1. Allocation Failure:在堆内存分配中,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

  2. System.gc():在应用层,Java开发工程师可以主动调用此API来请求一次 GC。

四、验证GCRoot的几种情况

在验证之前,先了解Java命令时的参数。

-Xms:初始分配 JVM 运行时的内存大小,如果不指定则默认为物理内存的 1/64

举个小例子

// 表示从物理内存中分配出 200M 空间给 JVM 内存
java -Xms200m HelloWorld
1.验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GCRoot
// 验证代码
public class GCRootLocalVariable {

  private int _10MB = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10MB];

  public static void main(String[] args) {
      System.out.println("开始时:");
      printMemory();
      method();
      System.gc();
      System.out.println("第二次GC完成");
      printMemory();
  }

  public static void method() {
      GCRootLocalVariable gc = new GCRootLocalVariable();
      System.gc();
      System.out.println("第一次GC完成");
      printMemory();
  }

  // 打印出当前JVM剩余空间和总的空间大小
  public static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共空间:" + totalMemory / 1024 / 1024 + "M");
  }
}
// 打印日志:
开始时:
剩余空间:119M
总共空间:123M
第一次GC完成
剩余空间:40M
总共空间:123M
第二次GC完成
剩余空间:120M
总共空间:123M

从上述代码中可以看到:

第一次打印内存信息,分别为 119M 和 123M;

第二次打印内存信息,分别为 40M 和 123M;剩余空间小了 80M,是因为在 method() 方法中创建了局部变量 gc(位于栈帧中的局部变量),并且这个 gc 对象会被作为 GCRoot。虽然创建的对象未被使用并且调用了 System.gc(),但是因为该方法未结束,所以创建的对象不能被回收。

第三次打印内存信息,分别为 120M 和 123M;method() 方法已经结束,创建的对象 gc 也随方法消失,不再有引用类型指向该 80M 对象。

【值得注意的是】

private int _10MB = 10 * 1024 * 1024;
private byte[] memory = new byte[8 * _10MB];

上面 2 行代码是必须的,如果去掉,那么 3 次打印结果将会一致,idea 也会出现Instantiation of utility class 警告信息,说这个类只存在静态方法,没必要创建这个对象。

这也就是说为什么创建 GCRootLocalVariable() 会需要 80M 的大小,是因为 GCRootLocalVariable 在创建时就会为其内部变量 memory 确定 80M 的大小。

2.验证方法区中的静态变量引用的对象作为 GCRoot
public class GCRootStaticVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private static GCRootStaticVariable staticVariable;

  public GCRootStaticVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootStaticVariable g = new GCRootStaticVariable(2 * _10M);
      g.staticVariable = new GCRootStaticVariable(4 * _10M);
      // 将g设置为null,调用GC时可以回收此对象内存
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  // 打印JVM剩余空间和总空间
  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余空间" + freeMemory/1024/1024 + "M");
      System.out.println("总共空间" + totalMemory/1024/1024 + "M");
  }
}

打印结果:
程序开始:
剩余空间119M
总共空间123M
GC完成
剩余空间81M
总共空间123M

通过上述打印结果可知:

  1. 程序刚开始时打印结果为 119M;

  2. 当创建 g 对象时分配 20M 内存,又为静态变量 staticVariable 分配 40M 内存;

  3. 当调用 gc 回收时,非静态变量 memory 分配的 20M 内存被回收;

  4. 但是作为 GCRoot 的静态变量 staticVariable 不会被回收,所以最终打印结果少了 40M 内存。

3.验证活跃线程作为GCRoot
public class GCRootThread {

  private int _10M = 10 * 1024 * 1024;
  private byte[] memory = new byte[8 * _10M];

  public static void main(String[] args) throws InterruptedException {
      System.out.println("程序开始:");
      printMemory();
      AsyncTask asyncTask = new AsyncTask(new GCRootThread());
      Thread thread = new Thread(asyncTask);
      thread.start();
      System.gc();
      System.out.println("main方法执行完成,执行gc");
      printMemory();
      thread.join();
      asyncTask = null;
      System.gc();
      System.out.println("线程代码执行完成,执行gc");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }

  private static class AsyncTask implements Runnable {

      private GCRootThread gcRootThread;

      public AsyncTask(GCRootThread gcRootThread) {
          this.gcRootThread = gcRootThread;
      }

      @Override
      public void run() {
          try {
              Thread.sleep(500);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
main方法执行完成,执行gc
剩余内存:41M
总共内存:123M
线程代码执行完成,执行gc
剩余内存:120M
总共内存:123M

通过上述打印结果可知:

  1. 程序刚开始时可用内存为 119M;

  2. 第一次调用 gc 时,线程并没有执行结束,并且它作为 GCRoot ,所以它所引用的 80M 内存不会被 GC 回收掉;

  3. thread.join() 保证线程结束后再调用后续代码,所以当第二次调用 GC 时,线程已经执行完毕并被置为 null;

  4. 这时线程已经销毁,所以该线程所引用的 80M 内存被 GC 回收掉。

4.测试成员变量是否可作为GCRoot
public class GCRootClassVariable {
  private static int _10M = 10 * 1024 * 1024;
  private byte[] memory;
  private GCRootClassVariable gcRootClassVariable;

  public GCRootClassVariable(int size) {
      memory = new byte[size];
  }

  public static void main(String[] args) {
      System.out.println("程序开始:");
      printMemory();
      GCRootClassVariable g = new GCRootClassVariable(2 * _10M);
      g.gcRootClassVariable = new GCRootClassVariable(4 * _10M);
      g = null;
      System.gc();
      System.out.println("GC完成");
      printMemory();
  }

  private static void printMemory() {
      long freeMemory = Runtime.getRuntime().freeMemory();
      long totalMemory = Runtime.getRuntime().totalMemory();
      System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
      System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
  }
}
打印结果:
程序开始:
剩余内存:119M
总共内存:123M
GC完成
剩余内存:121M
总共内存:123M

上述打印结果可知:

  1. 第一次打印结果与第二次打印结果一致:全局变量 gcRootClassVariable 随着 g=null 后被销毁。

  2. 所以全局变量并不能作为 GCRoot。

五、如何回收垃圾(常见的几种垃圾回收算法)

1.标记清除算法(Mark and Sweep GC)

从 “GCRoots” 集合开始,将内存整个遍历一次,保留所有可以被 GCRoots 直接或间接引用到的对象,而剩下的对象都当做垃圾对待并回收。

上述整个过程分为两步:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)。

  2. Sweep清楚阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清楚。

标记清除算法示意图

标记清除算法优缺点

【优点】

实现简单,不需要将对象进行移动。

【缺点】

需要中断进程内其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率。

2.复制算法(Copying)
  1. 将现有的内存空间分为两块,每次只使用其中一块;

  2. 在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中;

  3. 之后清除正在使用的内存块中的所有对象;

  4. 交换两个内存的角色,完成垃圾回收(目前使用A,B是空闲,算法完成后A为空闲,设置B为使用状态)。

复制算法复制前示意图

复制算法复制后示意图

复制算法优缺点

【优点】

按顺序分配内存即可;实现简单、运行高效,不用考虑内存碎片问题。

【缺点】

可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3.标记压缩算法(Mark-Compact)
  1. 需要先从根节点开始对所有可达对象做一次标记;

  2. 之后并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端;

  3. 最后清理边界外所有的空间。

所有,标记压缩也分为两步完成:

  1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)

  2. Compact压缩阶段:将剩余存活对象按顺序压缩到内存的某一端

标记压缩算法示意图

标记压缩算法优缺点

【优点】

避免了碎片产生,又不需要两块相同的内存空间,性价比较高。

【缺点】

所谓压缩操作,仍需要进行局部对象移动,一定程度上还是降低了效率。

分代回收策略

Java 虚拟机根据对象存活的周期不同,把堆内存划分为 新生代老年代,这就是 JVM 的内存分代策略。

注意:在 HotSpot 中除了 新生代老年代,还有 永久代

分代回收的中心思想:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短,如果经过多次回收仍然存活下来,则将它们转移到老年代中。

一、年轻代

新生成的对象优先存放在新生代中,存活率很低

新生代中,常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间,回收效率很高。所以一般采用的 GC 回收算法是 复制算法

新生代也可细分3部分:Eden、Survivor0(简称 S0)、Survivor1(简称 S1),这 3 部分按照 8:1:1 的比例来划分新生代。

新生代老年代示意图

新生成的对象会存放在 Eden 区。

新生代老年代示意图

当 Eden 区满时,会触发垃圾回收,回收掉垃圾之后,将剩下存活的对象存放到 S0 区。当下一次 Eden 区满时,再次触发垃圾回收,这时会将 Eden 区 和 S0 区存活的对象全部复制到 S1 区,并清空 Eden 区和 S0 区。

新生代老年代示意图

上述步骤重复 15 次之后,依然存活下来的对象存放到 老年区

二、老年代

一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。

老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小。

因为老年代对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

【注意的是】

有这么一种情况,老年代中的对象会引用新生代中的对象,这时如果要执行新生代的 GC,则可能要查询整个老年代引用新生代的情况,这种效率是极低的。所以老年代中维护了一个 512byte 的 table,所有老年代对象引用新生代对象的引用都记录在这里。这样新生代 GC 时只需要查询这个表即可。

三、GC log分析

为了让上层应用开发人员更加方便调试 Java 程序,JVM 提供了相应的 GC 日志,在 GC 执行垃圾回收事件中,会有各种相应的 log 被打印出来。

新生代和老年代打印的日志是有区别的:

【新生代GC:轻GC】这一区域的 GC 叫做 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度比较快。

【老年代GC:重GC】发生在这一区域的 GC 叫做 Major GC 或者 Full GC,当出现 Major GC,经常会伴随至少一次 Minor GC

Major GCFull GC 在有些虚拟机中还是有区别的:前者是仅回收老年代中的垃圾对象,后者是回收整个堆中的垃圾对象。

常用的 GC 命令参数
命令参数功能描述
-verbose:gc显示 GC 的操作内容
-Xms20M初始化堆大小为 20M
-Xmx20M设置堆最大分配内存 20M
-Xmn10M设置新生代的内存大小为 10M
-XX:+PrintGCDetails打印GC的详细log日志
-XX:SurvivorRatio=8新生代中 Eden 区域与 Survivor 区域的大小比值为 8:1:1

添加 VM Options 参数:分配堆内存 20M,10M给新生代,10M给老年代

// VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
public class MinorGCTest {

  private static final int _1M = 1024 * 1024;

  public static void main(String[] args) {
      byte[] a, b, c, d;
      a = new byte[2 * _1M];
      b = new byte[2 * _1M];
      c = new byte[2 * _1M];
      d = new byte[_1M];
  }
}
打印结果:(这里测试是第二次修改后的运行效果)
[GC (Allocation Failure) [PSYoungGen: 7820K->840K(9216K)] 7820K->6992K(19456K), 0.0072302 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 840K->0K(9216K)] [ParOldGen: 6152K->6759K(10240K)] 6992K->6759K(19456K), [Metaspace: 3198K->3198K(1056768K)], 0.0087734 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen     total 9216K, used 1190K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff7298d8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen       total 10240K, used 6759K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 66% used [0x00000000fec00000,0x00000000ff299cd0,0x00000000ff600000)
Metaspace       used 3205K, capacity 4496K, committed 4864K, reserved 1056768K
class space   used 351K, capacity 388K, committed 512K, reserved 1048576K

上述字段意思代表如下:

字段代表含义
PSYoungGen新生代
eden新生代中的 Eden 区
from新生代中的 S0 区
to新生代中的 S1 区
ParOldGen老年代
  1. 第一次运行效果后,因为 Eden 区 8M,S0 和 S1 各 1M。所以 a、b、c、d 共有 7M 空间都会在 Eden 区。

  2. 修改 d = new byte[2 * _1M],再次运行;

  3. JVM 会将 a/b/c 存放到 Eden 区,Eden 占有 6M 空间,无法再分配 2M 空间给 d;

  4. 因此会执行一次轻 GC,并尝试将 a/b/c 复制到 S1 区;

  5. 但是因为 S1 区只有 1M 空间,所以没办法存储 a/b/c 三者任一对象。

  6. 这种情况下,JVM 将 a/b/c 转移到老年代,将 d 保存在 Eden 区。

【最终结果】

Eden区 占用 2M 空间(d),老年代占用 6M 空间(a,b,c)

四、引用

通过 GC Roots 的引用可达性来判断对象是否存活,JVM 中的引入关系有以下四种:

引用英文名GC回收机制使用示例
强引用Strong Reference如果一个对象具有强引用,那么垃圾回收期绝不会回收它Object obj = new Object();
软引用Soft Reference在内存实在不足时,会对软引用进行回收SoftReference softObj = new SoftReference();
弱引用Weak Reference第一次GC回收时,如果垃圾回收器遍历到此弱引用,则将其回收WeakReference weakObj = new WeakReference();
虚引用Phantom Reference一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例不会使用
软引用的用法
public class SoftReferenceNormal {

  static class SoftObject {
      byte[] data = new byte[120 * 1024 * 1024]; // 120M
  }

  public static void main(String[] args) {
      SoftReference<SoftObject> softObj = new SoftReference<>(new SoftObject());
      System.out.println("第一次GC前,软引用:" + softObj.get());
      System.gc();
      System.out.println("第一次GC后,软引用:" + softObj.get());
      SoftObject obj = new SoftObject();
      System.out.println("分配100M强引用,软引用:" + softObj.get());
  }
}

添加 VM Option 参数:-Xmx200M 给堆内存分配最大200M内存

第一次 GC 前,软引用:SoftReferenceNormal$SoftObject@1b6d3586
第一次 GC 后,软引用:SoftReferenceNormal$SoftObject@1b6d3586
分配 100M 强引用,软引用:null
  1. 添加参数后位堆内存分配最大 200M 空间,分配给 softObj 对象 120M。

  2. 第一次 GC 后,因为剩余内存任然够,所以软引用并没有被回收。

  3. 当分配 100M 强引用后,堆内存空间不够,会触发GC回收,回收掉软引用。

软引用隐藏的问题

【注意】

被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj));
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END");
  }
}

添加 VM Option 参数:-Xms4m -Xmx4m -Xmn2m

// 打印结果:
cache size is 1
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at SoftReferenceTest$SoftObject.<init>(SoftReferenceTest.java:8)
at SoftReferenceTest.main(SoftReferenceTest.java:17)

程序崩溃,崩溃的原因并不是堆内存溢出,而是超出了 GC 开销限制。

这里错误的原因是:JVM 不停的回收软引用中的对象,回收次数过快,回收内存较小,占用资源过高了。

【解决方案】注册一个引用队列,将这个对象从 Set 中移除掉。

public class SoftReferenceTest {

  static class SoftObject {
      byte[] data = new byte[1024]; // 占用1k空间
  }

  private static final int _100K = 100 * 1024;
  // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
  private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);
  // 解决方案:注册一个引用队列,将要移除的对象从中删除
  private static ReferenceQueue<SoftObject> queue = new ReferenceQueue<>();
  // 记录清空次数
  private static int removeReferenceIndex = 0;

  public static void main(String[] args) {
      for (int i = 0; i < _100K; i++) {
          SoftObject obj = new SoftObject();
          cache.add(new SoftReference(obj, queue));
          // 清除掉软引用
          removeSoft();
          if (i * 10000 == 0) {
              System.out.println("cache size is " + cache.size());
          }
      }
      System.out.println("END removeReferenceIndex: " + removeReferenceIndex);
  }

  private static void removeSoft() {
      Reference<? extends SoftObject> poll = queue.poll();
      while (poll != null) {
          if (cache.remove(poll)) {
              removeReferenceIndex++;
          }
          poll = queue.poll();
      }
  }
}
// 打印结果:
cache size is 1
END removeReferenceIndex: 101745

作者:沅兮
来源:https://juejin.cn/post/7037330678731505672


收起阅读 »

swift 键盘收起

iOS
直接调用就能收起键盘,无需调用其他方法        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), t...
继续阅读 »







直接调用就能收起键盘,无需调用其他方法    

    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

收起阅读 »

iOS 底层原理探索 之 结构体内存对齐

iOS
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 目录如下:iOS 底层原理探索之 alloc以上内容的总结专栏iOS 底层原理探索 之 阶段总结准备Objective-C...
继续阅读 »


写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索之 alloc

以上内容的总结专栏


准备

Objective-C ,通常写作ObjC或OC,是扩充C的面向对象编程语言。它主要适用于Mac OS X 和 GNUstep者两个使用OpenStep标准的系统,而在NeXTSTEP和OpenStep中它更是基本语言。

GCC和Clang含Objective-C的编译器,Objective-C可以在GCC以及Clang运作的系统上编译。

我们平时开发用的Objective-C语言,编译后最终会转化成C/C++语言。

为什么要研究结构体的内存对齐呢? 因为作为一名iOS开发人员,随着对于底层的不断深入探究,我们都知道,所有的对象在底层中都是一个结构体。那么结构体的内存空间又会被系统分配多少空间,这个问题,值得我们一探究竟。

首先,从大神Cooci那里盗取了一张各数据类型占用的空间大小图片,作为今天探究结构体内存对齐原理的依据。

image.png

当我们创建一个对象的时候,我们并不需要过多的在意属性的顺序,因为系统会帮我们做优化处理。但是,在创建结构体的时候,就需要我们去分析了,因为这个时候系统并不会帮助我们做优化。

接下来,我们看下面两个结构体:

struct Struct1 {    
double a;
char b;
int c;
short d;
char e;
}struct1;

struct Struct2 {
double a;
int b;
char c;
short d;
char e;
}struct2;


两个结构体拥有的数据类型是相同的,按照图片中double 是8字节, char 是1字节, int 是4字节,short 是2字节, 那么 两个结构体应该是占 16字节的内存空间,也就是分配16字节空间即可,然而,我们看下面的结果:

    printf("%lu--%lu", sizeof(struct1), sizeof(struct2));
------------
24--16

那么,这就是有问题的了,两个拥有相同数据类型的结构体,被系统分配到的内存空间是不一样的,这是为什么呢?今天的重点就是这里,结构体的

内存对齐原则:

1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置
要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是
数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址
开始存储。

2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,
b里有char,int ,double等元素,那b应该从8的整数倍开始存储)

3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员
的整数倍,不足的要补⻬。

那么,我们按照以上内存对齐原则再来分析下 struct1 和 struct2 :


struct Struct1 { /// 18 --> 24
double a; //8 [0 1 2 3 4 5 6 7]
char b; //1 [8 ]
int c; //4 [9 [12 13 14 15]
short d; //2 [16 17]
char e; //1 [18]
}struct1;


struct Struct2 { /// 16 --> 16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
char e; // 1 [16]
}struct2;


接着,我们看下下面的结构体

struct Struct3 {    
double a;
int b;
char c;
short d;
int e;
struct Struct1 str;
}struct3;


打印输出结果为 48 ,分析如下:

    double a;           //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [ 14 15 ]
int e; //4 [ 16 17 18 19]
struct Struct1 str; //24 [24 ... 47]

所以,struct3 大小为48。


猜想:内存对齐的收尾工作中的内部最大成员指的是什么的大小呢?

接下来我们来一一验证一下

struct LGStruct4 {          /// 40 --> 48 
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [12]
short d; //2 [14 15]
int e; //4 [16 17 18 19]
struct Struct2 str; //16 [24 ... 39]
}struct4;

按照我对于内存对齐原则中收尾工作的理解, 最终的大小 应该是 Struct2 的 大小 16 的整数倍 也就是 48 才对。然而, 结果却是:

    NSLog(@"%lu", sizeof(struct4));
--------
SMObjcBuild[8076:213800] 40

对,是40你没有看错,这样的话,很显然,我理解的就是错误的, 结构体内部最大成员应该指的是这里的 double,那么我们接下来验证一下: 1、

struct Struct2 {    ///16
double a; //8 [0 1 2 3 4 5 6 7]
int b; //4 [8 9 10 11]
char c; //1 [ 12 ]
short d; //2 [14 15]
}struct2;

struct LGStruct4 { /// 24

short d; //2 [0 1]

struct Struct2 str; // 16 [8 ... 23]

}struct4;

结果是 :24


因为,结构体内部最大成员是 double也就是8;并不是按照 LGStruct4中的str长度为16的整数倍来计算,所以最后的结果是24。

总结

结构体内部最大成员指的是结构体内部的数据类型,所以,结构体内包含结构体的时候,并不是按照内部的结构体长度的整数倍来计算的哦。


收起阅读 »

iOS 底层原理探索 之 alloc

iOS
iOS 底层原理探索 之 alloc写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 内容的总结专栏iOS 底层原理探索 之 阶段总结序作为一名iOS开发人员,在平时开发工...
继续阅读 »

iOS 底层原理探索 之 alloc

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。


内容的总结专栏


作为一名iOS开发人员,在平时开发工作中,所有的对象我们使用最多的是alloc来创建。那么alloc底层做了哪些操作呢?接下来我会一步一步探究alloc方法的底层实现。

初探

我们先来看下面的代码

    SMPerson *p1 = [SMPerson alloc];
SMPerson *p2 = [p1 init];
SMPerson *p3 = [p1 init];

NSLog(@"%@-%p-%p", p1, p1, &p1);
NSLog(@"%@-%p-%p", p2, p2, &p2);
NSLog(@"%@-%p-%p", p3, p3, &p3);

打印内容:

    <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15088
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15080
<SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15078

可见,在 SMPerson 使用 alloc 方法从系统中申请开辟内存空间后 init方法并没有对内存空间做任何的处理,地址指针的创建来自于 alloc方法。如下所示:

地址.001.jpeg

注:细心的你一定注意到了,p1、p2、p3都是相差了8个字节。 这是因为,指针占内存空间大小为8字节,p1、p2、p3 都是从栈内存空间上申请的,且栈内存空间是连续的。同时,他们都指向了同一个内存地址。

那么, alloc 是如何开辟内存空间的呢?

首先,第一反应是,我们要Jump to Definition,

2241622899100_.pic_hd.jpg

结果,Xcode中并不能直接跳转后显示其底层实现,所以 并不是我们想要的。

2251622899278_.pic_hd.jpg

WX20210605-214250@2x.png

中探

接下来,我们通过三种方法来一探究竟:

方法1

既然不可以直接跳转到API文档来查看alloc的内部实现,那么我们还可以通过下 符号断点 来探寻 其实现原理。

WX20210605-212725@2x.png

接下来我们就来到此处

WX20210605-213213@2x.png

一个名为 libobjc.A.dylib 的库,至此,我们就应该要去找苹果开源的库,以寻找我们想要的答案。

点击查看苹果开源源码汇总

方法2

我们也可以直接在alloc那一行打一个断点,代码运行到此处后,按住control键 点击 step into, 接下来,就来到里这里

WX20210605-214413@2x.png 我们可以看到一个 objc_alloc 的函数方法到调用,此时,我们再下一个符号断点,同样的,我们还是找到了 libobjc.A.dylib 这个库。

WX20210605-215027@2x.png

方法3

此外,我们还是可以通过汇编来调试和查找相应的实现内容,断点依然是在alloc那一行。

Debug > Debug Workflow > Always Show Disassembly

WX20210605-215336@2x.png

找到 callq 方法调用那一行, WX20210605-215715@2x.png

接着, step into 进去, 我们找到了 objc_alloc 的调用, 之后的操作和 方法2的后续步骤一样,最终,可以找到 libobjc.A.dylib 这个库。 WX20210605-215732@2x.png

深探

下载源码 objc4-818.2

接下来对源码进行分析,

alloc方法会调用到此处

WX20210605-231454@2x.png

接着是 调用 _objc_rootAlloc

WX20210605-231517@2x.png

之后调用 到 callAlloc

WX20210605-231545@2x.png

跟着断点会来到 _objc_rootAllocWithZone

WX20210605-231647@2x.png

之后是 _class_createInstanceFromZone

此方法是重点

WX20210605-231758@2x.png

_class_createInstanceFromZone 方法中,该方法就是一个类初始化所走的流程,重点的地方有三处

第一处是:
    // 计算出开辟内存空间大小
size = cls->instanceSize(extraBytes);

内部实现如下: WX20210605-231838@2x.png 其中在计算内存空间大小时,会调用 cache.fastInstanceSize(extraBytes) 方法,

最终会调用 align16(size + extra - FAST_CACHE_ALLOC_DELTA16) 方法。 align16 的实现如下:

static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}

可见, 系统会进行 16字节 的对齐操作,也就是说,一个对象所占用的内存大小至少是16字节。

在这里 我们举个例子: size_t x = 8; 那么 align16操作后的大小计算过程如下:

    (8 + 15) & ~15;

0000 0000 0000 1000 8
0000 0000 0000 1111 15

= 0000 0000 0001 0111 23
1111 1111 1111 0000 ~15

= 0000 0000 0001 0000 16


第二处是:
    ///向系统申请开辟内存空间,返回地址指针;
obj = (id)calloc(1, size);

第三处是:
    /// 将类和指针做绑定
obj->initInstanceIsa(cls, hasCxxDtor);

总结:

所以,最后我们总结一下, alloc的底层调用流程如下:

alloc流程.001.jpeg

就是这样一个流程,系统就帮我们创建出来一个类对象。

补充

image.png

  • lldb 如何打印实力对象中成员为 double 类型的数值: e -f f -- <值>
收起阅读 »

String还有长度限制?是多少?

前言 话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。 String 首先要知道String的长度限制我们就需要知道String是怎么存储字符串...
继续阅读 »

前言


话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。


String


首先要知道String的长度限制我们就需要知道String是怎么存储字符串的,String其实是使用的一个char类型的数组来存储字符串中的字符的。



那么String既然是数组存储那数组会有长度的限制吗?是的有限制,但是是在有先提条件下的,我们看看String中返回length的方法。



由此我们看到返回值类型是int类型,Java中定义数组是可以给数组指定长度的,当然不指定的话默认会根据数组元素来指定:


int[] arr1 = new int[10]; // 定义一个长度为10的数组
int[] arr2 = {1,2,3,4,5}; // 那么此时数组的长度为5
复制代码

整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^31 -1,那么说明了数组的长度是0~2^31-1,那么计算一下就是(2^31-1 = 2147483647 = 4GB)



看到这我们尝试通过编码来验证一下上述观点。



以上是我通过定义字面量的形式构造的10万个字符的字符串,编译之后虚拟机提示报错,说我们的字符串长度过长,不是说好了可以存21亿个吗?为什么才10万个就报错了呢?


其实这里涉及到了JVM编译规范的限制了,其实JVM在编译时,如果我们将字符串定义成了字面量的形式,编译时JVM是会将其存放在常量池中,这时候JVM对这个常量池存储String类型做出了限制,接下来我们先看下手册是如何说的。



常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 由tag 的类型所决定。



我们可以看到 String类型的表示是 CONSTANT_String ,我们来看下CONSTANT_String具体是如何定义的。



这里定义的 u2 string_index 表示的是常量池的有效索引,其类型是CONSTANT_Utf8_info 结构体表示的,这里我们需要注意的是其中定义的length我们看下面这张图。



在class文件中u2表示的是无符号数占2个字节单位,我们知道1个字节占8位,2个字节就是16位 ,那么2个字节能表示的范围就是2^16- 1 = 65535 。范中class文件格式对u1、u2的定义的解释做了一下摘要:


#这里对java虚拟机规摘要部分


##1、class文件中文件内容类型解释


定义一组私有数据类型来表示 Class 文件的内容,它们包括 u1,u2 和 u4,分别代 表了 1、2 和 4 个字节的无符号数。


每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。


##2、程序异常处理的有效范围解释


start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围。


start_pc 必须是对当前 code[]数组中某一指令的操作码的有效索引,end_pc 要 么是对当前 code[]数组中某一指令的操作码的有效索引,要么等于 code_length 的值,即当前 code[]数组的长度。start_pc 的值必须比 end_pc 小。


当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设 x 为 异常句柄的有效范围内的值,x 满足:start_pc ≤ x < end_pc


实际上,end_pc 值本身不属于异常处理器的有效范围这点属于 Java 虚拟机历史上 的一个设计缺陷:如果 Java 虚拟机中的一个方法的 code 属性的长度刚好是 65535 个字节,并且以一个 1 个字节长度的指令结束,那么这条指令将不能被异常处理器 所处理。


不过编译器可以通过限制任何方法、实例初始化方法或类初始化方法的code[]数组最大长度为 65534,这样可以间接弥补这个 BUG。



注意:这里对个人认为比较重要的点做了标记,首先第一个加粗说白了就是说数组有效范围就是【0-65565】但是第二个加粗的地方又解释了,因为虚拟机还需要1个字节的指令作为结束,所以其实真正的有效范围是【0-65564】,这里要注意这里的范围仅限编译时期,如果你是运行时拼接的字符串是可以超出这个范围的。



接下来我们通过一个小实验来测试一下我们构建一个长度为65534的字符串,看看是否就能编译通过。0期阶段汇总


首先通过一个for循环构建65534长度的字符串,在控制台打印后,我们通过自己度娘的一个在线字符统计工具计算了一下确实是65534个字符,如下:




然后我们将字符复制后以定义字面量的形式赋值给字符串,可以看到我们选择这些字符右下角显示的确实是65534,于是乎运行了一波,果然成功了。




#看到这里我们来总结一下:


##字符串有长度限制吗?是多少?


首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。


但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。


其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。


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

ASM字节码插桩

ASM字节码插桩 一、什么是插桩 QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Ro...
继续阅读 »

ASM字节码插桩


一、什么是插桩


QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。


插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。


插桩前.png


插桩后.png


我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。1个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。


二、字节码操作框架


上面我们提到QQ空间使用了Javaassist来进行字节码插桩,除了Javaassist之外还有一个应用更为广泛的ASM框架同样也是字节码操作框架,Instant Run包括AspectJ就是借助ASM来实现各自的功能。


我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。



字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。


三、ASM的使用


由于ASM具有相对于Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在Java程序中完成对字节码的修改测试。


3.1、在AS中引入ASM


ASM可以直接从jcenter()仓库中引入,所以我们可以进入:bintray.com/进行搜索



点击图中标注的工件进入,可以看到最新的正式版本为:7.1。



因此,我们可以在AS中加入:


引入ASM.png


同时,需要注意的是:我们使用testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。



AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。



3.2、准备待插桩Class


test/java下面创建一个Java类:


public class InjectTest {

public static void main(String[] args) {

}
}
</pre>

由于我们操作的是字节码插桩,所以可以进入test/java下面使用javac对这个类进行编译生成对应的class文件。


javac InjectTest.java

3.3、执行插桩


因为main方法中没有任何输出代码,我们输入命令:java InjectTest执行这个Class不会有任何输出。那么我们接下来利用ASM,向main方法中插入一开始图中的记录函数执行时间的日志输出。


在单元测试中写入测试方法


<pre spellcheck="false" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> /**
* 1、准备待分析的class
*/
FileInputStream fis = new FileInputStream
("xxxxx/test/java/InjectTest.class");

/**
* 2、执行分析与插桩
*/
//class字节码的读取与分析引擎
ClassReader cr = new ClassReader(fis);
// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);


/**
* 3、获得结果并输出
*/
byte[] newClassBytes = cw.toByteArray();
File file = new File("xxx/test/java2/");
file.mkdirs();

FileOutputStream fos = new FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);

fos.close();</pre>

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到test/java2目录下。其中关键点就在于第2步中,如何进行插桩。


把class数据交给ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数ClassAdapterVisitor


<pre spellcheck="false" lang="java" cid="n41" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ClassAdapterVisitor extends ClassVisitor {

public ClassAdapterVisitor(ClassVisitor cv) {
super(Opcodes.ASM7, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
System.out.println("方法:" + name + " 签名:" + desc);

MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
return new MethodAdapterVisitor(api,mv, access, name, desc);
}
}</pre>

分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此ClassReader会将调用ClassAdapterVisitor中对应的visitMethodvisitAnnotationvisitField这些visitXX方法。


我们的目的是进行函数插桩,因此重写visitMethod方法,在这个方法中我们返回一个MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。


<pre spellcheck="false" lang="java" cid="n45" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.enjoy.asminject.example;

import com.enjoy.asminject.ASMTest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

/**
* AdviceAdapter: 子类
* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
*/
public class MethodAdapterVisitor extends AdviceAdapter {

private boolean inject;

protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}


/**
* 分析方法上面的注解
* 在这里干嘛???
* <p>
* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
* 没使用,就不管了。
*
* @param desc
* @param visible
* @return
*/
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (Type.getDescriptor(ASMTest.class).equals(desc)) {
System.out.println(desc);
inject = true;
}
return super.visitAnnotation(desc, visible);
}

private int start;

@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}

@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (inject){
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);

getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
"/PrintStream;"));

//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));


visitLdcInsn("execute:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));

//减法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);


invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));

}
}
}</pre>

MethodAdapterVisitor继承自AdviceAdapter,其实就是MethodVisitor 的子类,AdviceAdapter封装了指令插入方法,更为直观与简单。


上述代码中onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入long s = System.currentTimeMillis();。在onMethodExit中即方法最后插入输出代码。


<pre spellcheck="false" lang="java" cid="n48" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//执行完了怎么办? 记录到本地变量中
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));

start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
//记录 方法执行结果给创建的本地变量
storeLocal(start);
}
}</pre>

这里面的代码怎么写?其实就是long s = System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码


<pre spellcheck="false" lang="java" cid="n50" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">void test(){
//插入的代码
long s = System.currentTimeMillis();
/**
* 方法实现代码....
*/
//插入的代码
long e = System.currentTimeMillis();
System.out.println("execute:"+(e-s)+" ms.");
}</pre>

然后使用javac编译成Class再使用javap -c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。


插件安装.png


安装完成之后,可以在需要插桩的类源码中点击右键:


查看字节码.png


点击ASM Bytecode Viewer之后会弹出


字节码.png


所以第20行代码:long s = System.currentTimeMillis();会包含两个指令:INVOKESTATICLSTORE


再回到onMethodEnter方法中


<pre spellcheck="false" lang="java" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (inject) {
//invokeStatic指令,调用静态方法
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
//创建本地 LONG类型变量
start = newLocal(Type.LONG_TYPE);
//store指令 将方法执行结果从操作数栈存储到局部变量
storeLocal(start);
}
}</pre>

而`onMethodExit`也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

四、Android中的实现


在Android中实现,我们需要考虑的第一个问题是如何获得所有的Class文件来判断是否需要插桩。Transform就是干这件事情的。


相关视频


Android项目实战 微信Matrix卡顿监控方案,函数自动埋点实践


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

Flutter 单例的实现

和谐学习!不急不躁!!我是你们的老朋友小青龙~ 前言 回顾iOS,单例的写法如下: static JXWaitingView *shared; +(JXWaitingView*)sharedInstance{ static dispatch_once_t...
继续阅读 »

和谐学习!不急不躁!!我是你们的老朋友小青龙~


前言


回顾iOS,单例的写法如下:


static JXWaitingView *shared;

+(JXWaitingView*)sharedInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shared=[[JXWaitingView alloc]initWithTitle:nil];
});
return shared;
}

其目的是通过dispatch_once来控制【初始化方法】只会执行一次,然后用static修饰的对象来接收并返回它。所以核心是只会执行一次初始化


创建单例


创建单例的案例


class Student {
String? name;
int? age;
//构造方法
Student({this.name, this.age});

// 单例方法
static Student? _dioInstance;
static Student instanceSingleStudent() {
if (_dioInstance == null) {
_dioInstance = Student();
}
return _dioInstance!;
}
}

测试单例效果


测试一


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


测试二


import 'package:flutter_async_programming/Student.dart';

void main() {
Student studentA = Student.instanceSingleStudent();
studentA.name = "张三";
Student studentB = Student.instanceSingleStudent();
studentB.name = "李四";
print('studentA姓名是${studentA.name}');
print('studentB姓名是${studentB.name}');
}

运行效果


image.png


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

拒绝编译等待 - 动态研发模式 ARK

iOS
背景 pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更...
继续阅读 »



背景

iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

  • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。

  • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。

  • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

如何处理这些问题?

究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

Show Case

动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。
演示基于字节跳动本地研发工具 MBox[2]

  1. 仓库下载

ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

  1. 开发组件

CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

  1. pod install

传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

  1. 开发组件编译&调试

和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

  1. 查看全源码

ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

pod doc 优点:

  • 支持异步和同步,执行过程中不影响本地开发。

  • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。

  • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。

  • 仅复用 pod installer 中的资源下载、缓存模块。

  • 支持仓库统一鉴权,自动跳过无权限组件仓库。

收益

体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

写在最后

ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

相关技术文章将陆续分享,敬请期待。

扩展阅读

[1] ARK: https://github.com/kuperxu/KwaiTechnologyCommunication/blob/master/5.WWDC-ARK.pdf
[2] MBox: https://mp.weixin.qq.com/s/5_IlQPWnCug_f3SDrnImCw

作者:字节跳动终端技术——徐纪光
来源:https://blog.csdn.net/YZcoder/article/details/121374743


收起阅读 »

手把手带你,优化一个滚动时流畅的TableView

iOS
手把手带你,优化一个滚动时流畅的TableView这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战我的专栏iOS 底层原理探索iOS 底层原理探索 之 阶段总结意识到我的问题平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是...
继续阅读 »

手把手带你,优化一个滚动时流畅的TableView

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战


我的专栏

  1. iOS 底层原理探索
  2. iOS 底层原理探索 之 阶段总结

意识到我的问题

平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是很好的,TableView的Cell滚动的时候不会去加载显示图片内容,当一次滑动结束之后,Cell上的新闻图片便开始逐个的加载显示出来,所以整个滑动的过程是很流畅的。这中体验也是相当nice的。

我最开始的做法

开发中TableView的使用是非常值频繁的,当TableViewCell上需要加载图片的时候,是一件比较头疼的事。因为,用户一边滑动TableView,TableView需要一边从网络获取图片。之前的操作都是放在 cellForRowAtIndexPath 中来处理,这就导致用户在滑动TableView的时候,会特别的卡(尤其是滑动特别快时),而且,手机的CPU使用率也会飙的非常的高。对于用户来说,这显然是一个十分糟糕的体验。

糟糕的图片显示 代码

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.index = indexPath;

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];

NSString *url = [info objectForKey: @"img" ];
NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
cell.img.image = [UIImage imageWithData:iData];
cell.typeL.text = [NSString stringWithFormat:@"%ld-%ld", cell.index.section, cell.index.row];

return cell;
}

糟糕的手机CPU飙升率

未命名.gif

糟糕的用户滑动体验

未命名1.gif

不只是用户,对于开发这来讲,这也是不可以接受的体验。

平时接触并使用的app也非常的多,发现他们多处理方式就是,当用户滑动列表的时候,不再加载图片,等用户的滑动结束之后,会开始逐一的加载图片。这是非常好的优化思路,减轻了CPU的负担,也不会基本不会让用户感觉到页面滚动时候的卡顿。这也就是最开始我描述的我看新闻app的使用体验。

收到这个思路的启发,我们开始着手将上面糟糕的体验作一下优化吧。

总结思路开启优化之路

那么,带着这个优化思路,我开始了对于这个TableView 的优化。

  • 首先,我们只加载当前用户可以看到的cell上的图片。
  • 其次,我们一次只加载一张图片。

要完成以上两点,图片的加载显示就不能在cellForRowAtIndexPath中完成,我们要定义并实现一个图片的加载显示方法,以便在合适的时机,调用刷新内容显示。

loadSeeImage 加载图片的优化

#pragma mark load Images
- (void)loadSeeImage {

//记录本次加载的几张图片
NSInteger loadC = 0;

// 用户可以看见的cells
NSArray *cells = [self.imageTableView visibleCells];

// 调度组
dispatch_group_t group = dispatch_group_create();

for (int i = 0; i < cells.count; i++) {

ImageTableViewCell *cell = [cells objectAtIndex:i];

NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];
NSString *url = [info objectForKey: @"img" ];

NSString *data = [info objectForKey:@"data"];

if ([data isKindOfClass:[NSData class]]) {


}else {

// 添加调度则到我们的串行队列中去
dispatch_group_async(group, self.loadQueue, ^{

NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
NSLog(@" load image %ld-%ld ", cell.index.section, cell.index.row);
if (iData) {
// 缓存
[info setValue:@"1" forKey:@"isload"];
[info setValue:iData forKey:@"data"];
}
NSString *isload = [info objectForKey:@"isload"];

if ([isload isEqualToString:@"0"]) {

dispatch_async(dispatch_get_main_queue(), ^{

cell.img.image = [UIImage imageNamed:@""];
}); }else {

if (iData) {

dispatch_async(dispatch_get_main_queue(), ^{
//显示加载后的图片
cell.img.image = [UIImage imageWithData:iData];
});
}
}

});

if (i == cells.count - 1) {

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 全部加载完毕的通知
NSLog(@"load finished");
});
}

loadC += 1;
}
}

NSLog(@"本次加载了 %ld 张图片", loadC);
}

其次就是 loadSeeImage 调用时机的处理,我们要做到用户在滑动列表之后加载,就是在下面两处加载:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView   {  

[self loadSeeImage];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {

if (scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking) {
return;
}
[self loadSeeImage];
}

当然,首次进入页面,列表数据加载完毕后,我们也要加载一次图片的哦。 好的下面看下优化后的结果:

优化xcode.gif

优化phone.gif

CPU占用率比之前最高的时候降低了一半多,app在滑动的时候也没有明显卡顿的地方。 完美。

收起阅读 »

面向 JavaScript 开发人员的 5 大物联网库

最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为...
继续阅读 »


最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为重要的连接基础,是元宇宙场景得以实现的关键。元宇宙将汇集游戏引擎、AR可穿戴设备、VR、现实世界数据集和不断发展的物联网。

物联网(英语:InternetofThings,简称 IoT)是一种计算设备、机器、数码机器之间相互联系的系统,它拥有一种统一的统一识别代码(UID),并且能够在网络上传送数据,不需要人与人、或人与设备之间的交互。

作为一个前端工程师(JavaScript工程师),似乎觉得这一切有点模式,其实不然,现代 JavaScript 的可以使用的场景越来越多,包括物联网,在本文中,将介绍可以在 JavaScript 代码中用于连接设备的 5 个脚本库。

1. Cylon.js

官方网站: https://cylonjs.com/

Cylon.js 是用于机器人、物理计算和物联网 (IoT) 的流行 JavaScript 框架之一。不仅仅是一个“物联网”库,它还是一个完整的机器人框架,支持超过 43 个不同的平台,这是与机器连接的 43 种不同的地方或方式,目前支持的机器人和物理计算系统及软件平台有Arduino、Beaglebone Black、BLE、Disispark、Intel Galileo and Edison、Intel IoT Analytics、OpenCV、Octobl、Raspberry Pi、Salesforce等。

可以使用 Cylon.js 连接到关键字并侦听它或 Arduino 板发送的事件,或者提供一个 HTTP API 接口并通过那里获取数据(它们也支持 socket.ioMQTT)。想通过 JavaScript 控制无人机吗?这并非不可以,首先需要安装:

npm install cylon cylon-firmata cylon-gpio cylon-i2c

然后运行一个这样的小脚本, 参考文章:

npm install cylon cylon-ardrone

然后运行脚本:

const Cylon = require("cylon");

Cylon.robot({
  connections: {
      ardrone: { adaptor: "ardrone", port: "192.168.1.1" },
  },

  devices: {
      drone: { driver: "ardrone" },
  },

  work: function (my) {
      my.drone.takeoff();
      after((10).seconds(), function () {
          my.drone.land();
      });
      after((15).seconds(), function () {
          my.drone.stop();
      });
  },
}).start();

如果有设备可以试试。 Cylon.js 的工作方式是允许其他人通过插件的方式提供连接器,这意味着这个库提供的功能没有限制。最重要的是,文档本身非常详细,写得很好,完整的代码示例。

2. IoT.js

官方网站: https://iotjs.net/

IoT.js 是一个用 JavaScript 编写的物联网 (IoT) 框架。它旨在基于网络技术在物联网世界中提供一个可互操作的服务平台。

如果希望在一个连接的设备中执行一些物联网(而不是在一个强大的、充满资源的服务器中的接收端),那么可能需要针对该环境进行优化。这个 IoT 框架运行在 JerryScript 引擎之上, JerryScript 引擎是一个针对小型设备优化的 JavaScript 运行时。这意味着,虽然无法使用最先进的 JS 的全部功能,但确实可以使用:

  • 完全支持 ECMAScript 5.1 语法。

  • 低内存消耗优化

  • 能够将 JS 代码预编译为字节码

但是,兼容平台的数量没有 Cylon.js 多,而 IoT.js 只兼容:

关于他们的文档,这应该是衡量一个库有多好的标准之一。他们有一些基本的例子和入门指南。但可能就是这样了。考虑到 IoT.js 是一个底层的硬件接口,现在看起来它希望开发人员已经有使用其他产品的经验,而不是针对JS开发人员寻求进入物联网。

3. Johnny-Five

官方网站: http://johnny-five.io/

Johnny Five 是流行的 JavaScript 机器人和物联网平台之一。由 Bocoup 于 2012 年开发的 Johnny Five 一个开源的、基于 Firmata 协议的物联网和机器人编程框架,是 JavaScript 开发人员可用的最古老的机器人和物联网平台之一,从那时起,它的功能和兼容性都在不断增长。

Johnny Five 支持 Arduino(所有型号)、Electric Imp、Beagle Bone、Intel Galileo & Edison、Raspberry Pi 等。该平台可轻松与流行的应用程序库(如 Express.js 和 Socket.io)以及物联网框架(如 Octoblu)结合使用。

他们的文档非常详细,充满了关于硬件连接的示例和图表,这是一个很好的学习资源。

4. NodeRed

官方网站: https://nodered.org/

NodeRed 是建立在 Node.js 之上,是一个基于流的编程工具,最初由 IBM 的新兴技术服务团队开发,现在是 JS 基金会的一部分。该平台允许在部署之前从浏览器以图形方式设置数据流和工作流。在理想的情况下,不需要编写任何代码,也许设置一些平台凭据。 NodeRed 还充当和其他人共享他们创建的流程的中心化平台,这是防止每次都重新创建轮子的好方法,即使没有真正编写代码。

5. Zetta

官方网站: https://www.zettajs.org/

ZettaJS 是一个基于 Node.js 构建的开源平台,用于创建跨地理分布式计算机和云运行的物联网服务器。是另一种通过 JavaScript 与远程设备交互的方式。这里的主要区别在于 ZettaJS 的目标是将每个设备都变成一个 API,这是将 IoT 泛化为一个通用概念的好方法。如今,设备及其接口的数量正在爆炸增长,但没有对其进行规范控制。 ZettaJS 正试图在这方面进行改进,通过非常直观的编码方式,可以简单地为设备安装驱动程序,并在其中启用公共接口,并通过代码与它们交互。

6. 总结

通过上面介绍,JavaScript 不仅限于浏览器,甚至不限于基于 API 的后端开发,还可以随心所欲地从设备中提取数据或从设备中提取数据,并使用几乎完全相同的语言来控制它。

作者:天行无忌
来源:https://blog.51cto.com/devpoint/4762760

收起阅读 »

给团队做个分享,用30张图带你快速了解TypeScript

正文30张脑图常见的基本类型我们知道TS是JS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:特殊类型除了一些在JS中常见的类型,也还有一些TS所特有的类型类型断言和类型守卫如何在运行时需要保证和检测来自其他地方的数据也符...
继续阅读 »

正文

30张脑图

常见的基本类型

我们知道TSJS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:

1常见的基本类型.png

特殊类型

除了一些在JS中常见的类型,也还有一些TS所特有的类型

2特殊类型.png

类型断言和类型守卫

如何在运行时需要保证和检测来自其他地方的数据也符合我们的要求,这就需要用到断言,而断言需要类型守卫

3类型断言.png

接口

接口本身只是一种规范,里头定义了一些必须有的属性或者方法,接口可以用于规范functionclass或者constructor,只是规则有点区别

4TS中的接口.png

类和修饰符

JS一样,类class出现的目的,其实就是把一些相关的东西放在一起,方便管理

TS主要也是通过class关键字来定义一个类,并且它还提供了3个修饰符

5类和修饰符.png

类的继承和抽象类

TS中的继承ES6中的类的继承极其相识,子类可以通过extends关键字继承一个类

但是它还有抽象类的概念,而且抽象类作为基类,不能new

6.0类的继承和抽象类.png

泛型

将泛型理解为宽泛的类型,它通常用于类和函数

但不管是用于类还是用于函数,核心思想都是:把类型当一种特殊的参数传入进去

7泛型.png

类型推断

TS中是有类型推论的,即在有些没有明确指出类型的地方,类型推论会帮助提供类型

8类型推断.png

函数类型

为了让我们更容易使用,TS为函数添加了类型等

9函数.png

数字枚举和字符串枚举

枚举的好处是,我们可以定义一些带名字的常量,而且可以清晰地表达意图或创建一组有区别的用例

TS支持数字的和基于字符串的枚举

10枚举.png

类型兼容性

TS里的类型兼容性是基于结构子类型的 11类型兼容性.png

联合类型和交叉类型

补充两个TS的类型:联合类型和交叉类型

12联合类型和交叉类型.png

for..of和for..in

TS也支持for..offor..in,但你知道他们两个主要的区别吗

13forin和forof.png

模块

TS的模块化沿用了JS模块的概念,模块是在自身的作用域中执行,在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们

14模块.png

命名空间的使用

使用命名空间的方式,其实非常简单,格式如下: namespace X {}

15命名空间的使用.png

解决单个命名空间过大的问题

16解决单个命名空间过大的问题.png

简化命名空间

要简化命名空间,核心就是给常用的对象起一个短的名字

TS中使用import为指定的符号创建一个别名,格式大概是:import q = x.y.z

17简化命名空间.png

规避2个TS中命名空间和模块的陷阱

18陷阱.png

模块解析流程

模块解析是指编译器在查找导入模块内容时所遵循的流程

流程大致如下:

image.png

相对和非相对模块导入

相对和非相对模块导入主要有以下两点不同

image.png

Classic模块解析策略

TS的模块解析策略,其中的一种就叫Classic

21Classic模块解析策略.png

Node.js模块解析过程

为什么要说Node.js模块解析过程,其实是为了讲TS的另一种模块解析策略做铺垫---Node模块解析策略。

因为Node模块解析策略就是一种试图在运行时模仿Node.js模块解析的策略

22Node.js的模块解析过程.png

Node模块解析策略

Node模块解析策略模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件的模块解析的策略,但是跟Node.js会有点区别

23Node模块解析策略.png

声明合并之接口合并

声明合并指的就是编译器会针对同名的声明合并为一个声明

声明合并包括接口合并,接口的合并需要区分接口里面的成员有函数成员和非函数成员,两者有差异

24接口合并.png

合并命名空间

命名空间的合并需要分两种情况:一是同名的命名空间之间的合并,二是命名空间和其他类型的合并

25合并命名空间.png

JSX模式

TS具有三种JSX模式:preservereactreact-native

26JSX.png

三斜线指令

三斜线指令其实上面有讲过,像/// <reference>

它的格式就是三条斜线后面跟一个标签

27三斜线指令.png


作者:LBJ
链接:https://juejin.cn/post/7036266588227502093

收起阅读 »

我去!爬虫遇到字体反爬,哭了

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的: 还有这样的: 可以看到这些字体已经被加密(反爬) 竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类) 01 网页分析在开始分析反爬之前,先简单的介绍一下背景(爬取的网页) 辰...
继续阅读 »

今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的:


img

还有这样的:


img

可以看到这些字体已经被加密反爬


竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类


01 网页分析

在开始分析反爬之前,先简单的介绍一下背景(爬取的网页)


img

辰哥爬取的某某点评的店铺信息。一开始查看网页源码是这样的


img

这种什么也看不到,咱们换另一种方式:通过程序直接把整个网页源代码保存下来


img

获取到的网页源码如下:


img

比如这里看到评论数(4位数)都有对应着一个编号(相同的数字编号相同),应该是对应着网站的字体库


下一步,我们需要找到这个网站的字体库。


02 获取字体库

这里的字体库建议在目标网站里面去获取,因为不同的网站的字体库是不一样,导致解码还原的字体也会不一样。


1、抓包获取字体库


img

在浏览器network里面可以看到一共有三种字体库。(三种字体库各有不同的妙用,后面会有解释


img

把字体库链接复制在浏览器里面打开,就可以把字体库下载到本地。


2、查看字体库


这里使用FontCreator的工具查看字体库。


下载地址:


https://www.high-logic.com/font-editor/fontcreator/download

这里需要注册,邮箱验证才能下载,不过辰哥已经下载了,可以在公众号回复:FC,获取安装包。


安装之后,把刚刚下载的字体库在FontCreator中打开


img

可以看到字体的内容以及对应的编号


比如数字7对应F399数字8对应F572 ,咱们在原网页和源码对比,是否如此???


img

可以看到,真是一模一样对应着解码就可以还原字体。


3、为什么会有三个字体库


img

在查看加密字体的CSS样式时,方式有css内容是这样的


img

字体库1:d35c3812.woff 对应解码class为 shopNum


字体库2:084c9fff.woff 对应解码class为 reviewTag和address


字体库3:73f5e6f3.woff 对应解码class为 tagName


也就是说,字体所属的不同class标签,对应的解密字体库是不一样的,辰哥这里不得不说一句:太鸡贼了


img

咱们这里获取的评论数,clas为shopNum,需要用到字体库d35c3812.woff


03 代码实现解密

1、加载字体库


既然我们已经知道了字体反爬的原理,那么我们就可以开始编程实现解密还原。


加载字体库的Python库包是:fontTools ,安装命令如下:


pip install fontTools

img

将字体库的内容对应关系保存为xml格式


img

code和name是一一对应关系


img

img

可以看到网页源码中的编号后四位对应着字体库的编号。


因此我们可以建立应该字体对应集合


img

建立好映射关系好,到网页源码中去进行替换


img

img

这样我们就成功的将字体反爬处理完毕。后面提取内容大家基本都没问题。


2、完整代码


img

输出结果:


img

可以看到加密的数字全部都还原了。


04 小结

辰哥在本文中主要讲解了如此处理字体反爬问题,并以某某点评为例去实战演示分析。辰哥在文中处理的数字类型,大家可以尝试去试试中文如何解决。


作者:Python研究者
来源:https://juejin.cn/post/6970933428145356831

收起阅读 »

js实现放大镜

借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。JS // 获取小图和遮罩、大图、大盒子    var small ...
继续阅读 »



先看效果图

实现原理

借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。

HTML和CSS

 <div class="wrap">
   
   <div id="small">
     <img src="img/1.jpg" alt="" >
     <div id="mark">div>
   div>
   
   <div id="big">
     <img src="img/2.jpg" alt="" id="bigimg">
   div>
 div>
* {
    margin: 0;
    padding: 0;
  }
  .wrap {
    width: 1500px;
    margin: 100px auto;
  }

  #small {
    width: 432px;
    height: 768px;
    float: left;
    position: relative;
  }

  #big {
    /* background-color: seagreen; */
    width: 768px;
    height: 768px;
    float: left;
    /* 超出取景框的部分隐藏 */
    overflow: hidden;
    margin-left: 20px;
    position: relative;
    display: none;
  }

  #bigimg {
    /* width: 864px; */
    position: absolute;
    left: 0;
    top: 0;
  }

  #mark {
    width: 220px;
    height: 220px;
    background-color: #fff;
    opacity: .5;
    position: absolute;
    left: 0;
    top: 0;
    /* 鼠标箭头样式 */
    cursor: move;
    display: none;
  }

JS

 // 获取小图和遮罩、大图、大盒子
   var small = document.getElementById("small")
   var mark = document.getElementById("mark")
   var big = document.getElementById("big")
   var bigimg = document.getElementById("bigimg")
   // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动
   small.onmousemove = function (e) {
     // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半)
     var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft
     var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop
     // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值)
     var max_left = small.offsetWidth - mark.offsetWidth;
     var max_top = small.offsetHeight - mark.offsetHeight;
     // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离)
     var n = big.offsetWidth / mark.offsetWidth
     // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top)
     // 判断水平边界
     if (s_left < 0) {
       s_left = 0
    } else if (s_left > max_left) {
       s_left = max_left
    }
     //判断垂直边界
     if (s_top < 0) {
       s_top = 0
    } else if (s_top > max_top) {
       s_top = max_top
    }
     // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来!
     mark.style.left = s_left + "px";
     mark.style.top = s_top + "px";
     // 计算大图移动的距离
     var levelx = -n * s_left;
     var verticaly = -n * s_top;
     // 让图片动起来
     bigimg.style.left = levelx + "px";
     bigimg.style.top = verticaly + "px";
  }
   // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失
   small.onmouseenter = function () {
     mark.style.display = "block"
     big.style.display= "block"
  }
   small.onmouseleave = function () {
     mark.style.display = "none"
     big.style.display= "none"
  }

总结

  • 鼠标焦点一旦动起来,它的偏移量就是动态的;父元素和子元素加上定位后,通过动态改变某个元素的lefttop值来实现“动”的效果。

  • 大图/小图=放大镜(遮罩)/取景框

  • 两张图片一定要等比例缩放

作者:Onion韩
来源:https://juejin.cn/post/7030963292818374670

收起阅读 »

从谷歌一行代码学到的姿势

网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()...
继续阅读 »

网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框

[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

运行效果如下图:

这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。

我的理解其中主要包含如下4个知识点:

1. [].forEach.call
2. $$("*")
3. a.style.outline
4. (~~(Math.random()*(1<<24))).toString(16)

1 [].forEach.call

1.1 [].forEach

forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:

let arr = [3, 5, 8];
arr.forEach((item) => {
console.log(item);
})
// 控制台输出:
// 3
// 5
// 8

那么下面的写法:

[].forEach

只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。

得到 forEach 这个方法后,就可以通过 call 发起调用。

1.2 call

call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。

常规调用函数的姿势:

let object1 = {
id: 1,
printId() {
console.log(this.id)
}
}
object1.printId();
// 控制台输出:
// 1

因为是正常调用,方法内的this指向object1对象,所以上例输出1。

使用call调用printId方法,并传入另外一个对象object2:

let object2 = {
id: 2
}
object1.printId.call(object2);
// 控制台输出:
// 2

这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。

1.3 综合分析

综合来看:

[].forEach.call( $$("*"), function(a){} )

这行代码的意思就是遍历如下对象:

$$("*") 

然后用如下方法处理每个元素:

function(a){}

其中,a就是遍历的的每一个元素。

那么

$$("*") 

指什么呢?我们接着往后看。

2 $$("*")

这个写法用来获取页面所有元素,相当于

document.querySelectorAll('*')

只是

$$("*") 

只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。

3 a.style.outline

设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。

<style type="text/css">
#swiper {
width: 100px;
height: 100px;
outline: 10px solid;
}
style>

<div id="swiper">div>

运行效果:

div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。

外边框设置的最大作用就是:

可以设置元素边框效果,但是不影响页面布局。

4 (~~(Math.random()*(1<<24))).toString(16)

这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?

16进制的颜色值:81f262

4.1 Math.random()

这个容易理解,就是随机 [0, 1) 的小数。

4.2 1<<24

这个表示1左移24位,二进制表示如下所示:

1 0000 0000 0000 0000 0000 0000  

十进制就是表示:

2^24

那么

Math.random() * (1<<24)

就会得到如下范围的一个随机浮点数:

[0, 2^24) 

4.3 两次按位取反

因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。

所以

(~~(Math.random()*(1<<24)))

就会得到如下范围的一个随机整数:

[0, 2^24) 

4.4 转成字符串toString(16)

最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。

对象.toString(2); // 转成2进制
对象.toString(8); // 转成8进制
对象.toString(10); // 转成10进制
对象.toString(16); // 转成16进制

上面的得到的随机整数用二进制表示就是:

0000 0000 0000 0000 0000 0000  

1111 1111 1111 1111 1111 1111

那么2进制转成16进制,是不是就是每4位转一个?

最终是不是就得到一个6个长度的16进制数了?

这个字符串加上#是不是就是16进制的颜色值了?

形如:

#ac83ce
#b74384
等等...

实务应用

虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。

主要原因是两个:

1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
2. 选中所有元素再遍历,性能低。

如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。

参考如下示例:

// 实例化 TreeWalker 对象
let walker = document.createTreeWalker(
document.documentElement,
NodeFilter.SHOW_ELEMENT
);
// 遍历
let node = walker.nextNode();
while (node !== null) {
node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
node = walker.nextNode();
}

虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。

如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。

参考资料

JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…

querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…

作者:晴空闲云
来源:https://juejin.cn/post/7034777643014684703

收起阅读 »

现在实现倒计时都这么卷了吗?

但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版 为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时 旧版的功能实现代码 const totalDuration = 10...
继续阅读 »

但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版


为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时


旧版的功能实现代码


const totalDuration = 10 * 1000;
let requestRef = null;
let startTime;
let prevEndTime;
let prevTime;
let currentCount = totalDuration;
let endTime;
let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
let interval = 1000;
let nextTime = interval;

setInterval(() => {
let n = 0;
while (n++ < 1000000000);
}, 0);

const animate = (timestamp) => {
if (prevTime !== undefined) {
const deltaTime = timestamp - prevTime;
if (deltaTime >= nextTime) {
prevTime = timestamp;
prevEndTime = endTime;
endTime = new Date().getTime();
currentCount = currentCount - 1000;
console.log("currentCount: ", currentCount / 1000);
timeDifferance = endTime - startTime - (totalDuration - currentCount);
console.log(timeDifferance);
nextTime = interval - timeDifferance;
// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}
console.log(`执行下一次渲染的时间是:${nextTime}ms`);
if (currentCount <= 0) {
currentCount = 0;
cancelAnimationFrame(requestRef);
console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
return;
}
}
} else {
startTime = new Date().getTime();
prevTime = timestamp;
endTime = new Date().getTime();
}
requestRef = requestAnimationFrame(animate);
};

requestRef = requestAnimationFrame(animate);


然后有个细小的问题在于这段代码


// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}

问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s,我这里设置下一个循环是0s,然后现在倒计时当前15s,就会看到快速倒计时到12s,产品同学说你这倒计时还怎么加速了呀


这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下


其实很简单,就是把这个临近值0设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验


例如以前追赶2s3s~4s内立刻追赶上,那么波动是很明显的,但是如果把2s的落后秒数,平躺到接下来要倒计时的1min里,每次大概追赶30ms,那是看不出来滴


// 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
if (nextTime < 900) {
nextTime = 900;
}

这里我设置落后太多时,每秒追赶100ms,假如落后2s20s后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!


虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦


结语


以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!


作者:一只凤梨
链接:https://juejin.cn/post/7026735190634414087

收起阅读 »

中高级前端不一定了解的setTimeout | 网易实践小总结

setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
继续阅读 »

setTimeout的创建和执行


我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


然后我们看下具体例子:


setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')

以上例子执行是这样:



  • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

  • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

  • 3.执行console.log('martincai')代码

  • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


循环源码:


void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);

// 执行延迟队列中的任务
ProcessDelayTask()

if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}

删除延迟任务


clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


setTimeout的几个注意点:



  1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


  function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()

这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



  1. setTimeout嵌套下会有4ms的延迟


Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



  1. 未激活的页面的setTimeout更改为至少1000ms


当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



  1. 延迟时间有最大值


目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


  setTimeout(() => {
console.log(1)
}, 2 ** 31)

以上代码会立即执行


作者:我在曾经眺望彼岸
链接:https://juejin.cn/post/7032091028609990692

收起阅读 »

Android 图形处理 —— Matrix 原理剖析

Matrix 简介 Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换 它的结构大概是这样的 其中每个位置的数值作用和其名称所代表的的含义是一一对应的 MSCALE_X、M...
继续阅读 »

Matrix 简介


Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换


它的结构大概是这样的


matrix


其中每个位置的数值作用和其名称所代表的的含义是一一对应的



  • MSCALE_X、MSCALE_Y:控制缩放

  • MTRANS_X、MTRANS_Y:控制平移

  • MSKEW_X、MSKEW_X:控制错切

  • MSCALE_X、MSCALE_Y、MSKEW_X、MSKEW_X:控制旋转

  • MPERSP_0、MPERSP_1、MPERSP_2:控制透视


matrix_1


在 Android 中,我们直接实例化一个 Matrix,内部的矩阵长这样:


matrix_3


是一个左上到右下为 1,其余为 0 的矩阵,也叫单位矩阵,一般数学上表示为 I


Matrix 坐标变换原理


前面说到 Matirx 主要的作用就是处理坐标的变换,而坐标的基本变换有:平移、缩放、旋转和错切



这里所说的基本变换,也称仿射变换 ,透视不属于仿射变化,关于透视相关的内容不在本文的范围内



当矩阵的最后一行是 0,0,1 代表该矩阵是仿射矩阵,下文中所有的矩阵默认都是仿射矩阵


线性代数中的矩阵乘法


在正式介绍 Matrix 是如何控制坐标变换的原理之前,我们先简单复习一下线性代数中的矩阵乘法,详细的讲解可参见维基百科或者翻翻大学的《线性代数》,这里只做最简单的介绍




  • 两个矩阵相乘,前提是第一个矩阵的列数等于第二个矩阵的行数




  • 若 A 为 m × n 的矩阵,B 为 n × p 的矩阵,则他们的乘积 AB 会是一个 m × p 的矩阵,表达可以写为





  • 由定义计算,AB 中任意一点(a,b)的值为 A 中第 a 行的数和 B 中第 b 列的数的乘积的和







了解矩阵乘法的基本方法之后,我们还需要记住几个性质,对后续的分析有用



  • 满足结合律,即 A(BC)=(AB)C

  • 满足分配律,即 A(B + C) = AB + AC (A + B)C = AC + BC

  • 不满足交换律,即 AB != BA

  • 单位矩阵 I 与任意矩阵相乘,等于矩阵本身,即 IA = ABI = B


缩放(Scale)


我们先想想,让我们实现把一个点 (x0, y0) 的 x 轴和 y 轴分别缩放 k1 和 k2 倍,我们会怎么做,很简单


val x = k1 * x0
val y = k2 * y0

那如果用矩阵怎么实现呢,前面我们讲到 Matrix 中 MSCALE_XMSCALE_Y 是用来控制缩放的,我们在这里填分别设置为 k1 和 k2,看起来是这样的


image-20211109103257621

而点 (x0, y0) 用矩阵表示是这样的


image-20211109103824496

有些人会疑问,最后一行这里不是还有一个 1 吗,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的 (x, y),两者看起来一样,计算机无法区分,为了让计算机也可以区分它们,增加了一个标志位,即


(x, y, 1) -> 点
(x, y, 0) -> 向量

现在 Matrix 和点都可以用矩阵表示了,接下来我们看看怎么通过这两个矩阵得到一个缩放之后的点 (x, y). 前面我们已经介绍过矩阵的乘法,让我们看看把上面两个矩阵相乘会得到什么结果


image-20211109104922576

可以看到,矩阵相乘得到了一个(k1x0, k2y0,1)的矩阵,上面说过,计算机中,这个矩阵就代表点 (k1x0, k2y0), 而这个点刚好就是我们要的缩放之后的点


以上所有过程用代码来实现,看起来就是像下面这样


val xy = FloatArray(x0, y0)
Matrix().apply {
setScale(k1, k2)
mapPoints(xy)
}

平移(Translate)


平移和缩放也是类似的,实现平移,我们一般可写为


val x = x0 + deltaX
val y = y0 + deltaY

而用矩阵来实现则是


val xy = FloatArray(x0, y0)
Matrix().apply {
setTranslate(k1, k2)
mapPoints(xy)
}

换成数学表示


translate


根据矩阵乘法


x = 1 × x0 + 0 × y0 + deltaX × 1 = x0 + deltaX
y = 0 × x0 + 1 × y0 + deltaY × 1 = y0 + deltaY

可得和一开始的实现也是效果一致的


错切(Skew)


错切相对于平移和缩放,可能大部分人对这个名词比较陌生,直接看三张图大家可能会比较直观


水平错切


x = x0 + ky0
y = y0

矩阵表示



水平错切


垂直错切


x = x0
y = kx0 + y0

矩阵表示




复合错切


x = x0 + k1y0
y = k2x0 + y0

矩阵表示




旋转(Rotate)


旋转相对以上三种变化又有一点复杂,这里涉及一些三角函数的计算,忘记的可以去维基百科 先复习下



image-20211108215739508

同样我们先自己实现一下旋转,假设一个点 A(x0, y0), 距离原点的距离为 r,与水平夹角为 α,现绕原点顺时针旋转 θ 度,旋转之后的点为 B(x, y)



用矩阵表示




Matrix 复合操作原理


前面介绍了四种基本变换,如果我们需要同时应用上多种变化,比如先绕原点顺时针旋转 90° 再 x 轴平移 100,y 轴平移 100, 最后 x、y 轴缩放0.5 倍,那么就需要用到复合操作


还是先用自己的实现来实现一下


x = ((x0 · cosθ - y0 · sinθ) + 100) · 0.5
y = ((y0 · cosθ + x0 · sinθ) + 100) · 0.5

矩阵表示


image-20211206155715836


按照前面的方式逐个推导,最终也能得到和上述一样的结果


到此,我们可以对 Matrix 做出一个基本的认识:Matrix 基于矩阵计算的原理,解决了计算机中坐标映射和变化的问题


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

Glide线程池

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标 弄清楚Glide是如何做线程池配置的 Glide如何进行优先级加载 Glide用来...
继续阅读 »

hello:大家好我是 小小小小小鹿,一枚菜鸡Android程序猿。最近正在阅读Glide源码,今天我们要研究的部分是Glide 线程池的配置。 本次代码阅读主要有两个目标



  1. 弄清楚Glide是如何做线程池配置的

  2. Glide如何进行优先级加载


Glide用来进行图片加载,我们知道当页面暂停的时候,glide可以根据页面的生命周期,来暂停当前页面的请求,但是如果当前页面通过滑动加载大量图片,那么Glide是怎么进行图片加载的呢?是先调用的加载在前还是后调用的加载在前面呢?如果某个页面的部分图片需要优先被加载,那么Glide又该如何处理呢?


Glide线程池的使用


Glide DecodeJob 的工作过程我们知道Glide在进行一次完成的数据加载会经历 ResourceCacheGenerator --> DataCacheGenerator --> SourceGenerator 的三个过程变化。而在这个过程变化中会涉及到两个线程池的使用。




  1. EngineJob#start开始本次请求


    public synchronized void start(DecodeJob<R> decodeJob) {
     this.decodeJob = decodeJob;
       //如果是从 缓存中获取图片使用 diskCacheExecutor
     GlideExecutor executor = decodeJob.willDecodeFromCache()
         ? diskCacheExecutor
        : getActiveSourceExecutor();
     executor.execute(decodeJob);
    }

    private GlideExecutor getActiveSourceExecutor() {
        //如果useUnlimitedSourceGeneratorPool 为true 使用无限制的线程池
        //如果useAnimationPool 为true且如果useUnlimitedSourceGeneratorPool为false 使用动画线程池 否则使用sourceExecutor
       return useUnlimitedSourceGeneratorPool
           ? sourceUnlimitedExecutor : (useAnimationPool ? animationExecutor : sourceExecutor);
    }



  2. EngineJob#reschedule重新进行调度


    @Override
    public void reschedule(DecodeJob<?> job) {
     //此时线程池的使用逻辑和EngineJob#start不在文件中加载数据一致
     getActiveSourceExecutor().execute(job);
    }



Glide线程池的配置


Glide Excutor参数初始化来自于GlideBuilder#build 而这些在不额外设置的情况下都来自于GlideExecutor。而GlideExecutor的所有线程池都是通过配置ThreadPoolExecutor来完成的。


初识ThreadPoolExecutor


ExecutorService是最初的线程池接口,ThreadPoolExecutor类是对线程池的具体实现,它通过构造方法来配置线程池的参数。


public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler) {
       if (corePoolSize < 0 ||
           maximumPoolSize <= 0 ||
           maximumPoolSize < corePoolSize ||
           keepAliveTime < 0)
           throw new IllegalArgumentException();
       if (workQueue == null || threadFactory == null || handler == null)
           throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
  }

参数解释:


corePoolSize,线程池中核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。 maximumPoolSize,线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。 keepAliveTime,非核心线程的超时时长,当闲置时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。 unit,枚举时间单位,TimeUnit。 workQueue,线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上。 线程池的分配遵循这样的规则:


当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务; 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行; 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务; 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。


threadFactory:线程工厂,为线程池提供创建新线程的能力。


DiskCacheExecutor的配置过程


GlideExecutor提供了三个创建DiskCacheExecutor的方法,最终都会调用到有三个参数那个


public static GlideExecutor newDiskCacheExecutor(
   int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
 return new GlideExecutor(
     new ThreadPoolExecutor(
         threadCount /* corePoolSize */,
         threadCount /* maximumPoolSize */,
         0 /* keepAliveTime */,
         TimeUnit.MILLISECONDS,
         new PriorityBlockingQueue<Runnable>(),
         new DefaultThreadFactory(name, uncaughtThrowableStrategy, true)));
}

在默认创建的时候,调用的是无参数的那个,threadCount 值为1 即DiskCacheExecutor是一个核心线程数为1,没有非核心线程的线程池,所有任务在线程池中串行执行,Runnable的存储对象是PriorityBlockingQueue。


SourceExecutor的配置过程


public static GlideExecutor newSourceExecutor() {
 return newSourceExecutor(
     calculateBestThreadCount(),
     DEFAULT_SOURCE_EXECUTOR_NAME,
     UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newSourceExecutor(
     int threadCount, String name, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
   return new GlideExecutor(
       new ThreadPoolExecutor(
           threadCount /* corePoolSize */,
           threadCount /* maximumPoolSize */,
           0 /* keepAliveTime */,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(name, uncaughtThrowableStrategy, false)));
}

可以看到SourceExecutor的构建过程和基本一致,不同的地方在于核心线程的数量是通过calculateBestThreadCount来动态计算的。


if (bestThreadCount == 0) {
   //如果cpu核心数超过4则核心线程数为4 如果Cpu核心数小于4那么使用Cpu核心数作为核心线程数量
 bestThreadCount =
     Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());
}
return bestThreadCount;

UnlimitedSourceExecutor无限制的线程池


public static GlideExecutor newUnlimitedSourceExecutor() {
 return new GlideExecutor(new ThreadPoolExecutor(
     0,
     Integer.MAX_VALUE,
     KEEP_ALIVE_TIME_MS,
     TimeUnit.MILLISECONDS,
     new SynchronousQueue<Runnable>(),
     new DefaultThreadFactory(
         SOURCE_UNLIMITED_EXECUTOR_NAME,
         UncaughtThrowableStrategy.DEFAULT,
         false)));
}

UnlimitedSourceExecutor没有核心线程,非核心线程数量无限大。


AnimationExecutor


public static GlideExecutor newAnimationExecutor() {
 int bestThreadCount = calculateBestThreadCount();
 int maximumPoolSize = bestThreadCount >= 4 ? 2 : 1;
 return newAnimationExecutor(maximumPoolSize, UncaughtThrowableStrategy.DEFAULT);
}

public static GlideExecutor newAnimationExecutor(
     int threadCount, UncaughtThrowableStrategy uncaughtThrowableStrategy) {
    return new GlideExecutor(
       new ThreadPoolExecutor(
           0 /* corePoolSize */,
           threadCount,
           KEEP_ALIVE_TIME_MS,
           TimeUnit.MILLISECONDS,
           new PriorityBlockingQueue<Runnable>(),
           new DefaultThreadFactory(
               ANIMATION_EXECUTOR_NAME,
               uncaughtThrowableStrategy,
               true)));
}

AnimationExecutor没有核心线程,非核心线程数量根据Cpu核心数来决定,当Cpu核心数大于等4时 非核心线程数为2,否则为1。


Glide线程池总结


DiskCacheExecutor和SourceExecutor 采用固定核心线程数固定,适用于处理CPU密集型的任务,但是没有非核心线程。确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。


UnlimitedSourceExecutor采用无核心线程,非核心线程无限大适用于并发执行大量短期的小任务。在空闲的时候消耗资源非常少。


AnimationExecutor没有核心线程,非核心线程有限,同UnlimitedSourceExecutor的区别就是核心线程数量和工作队列不一致。第一次看到这么用。


Glide如何实现加载优先级


除了UnlimitedSourceExecutor其余的都是使用的PriorityBlockingQueue。PriorityBlockingQueue是一个具有优先级的无界阻塞队列。也就是说优先级越高越先执行。


我们知道图片的加载是在线程池中执行的DecodeJob,DecodeJob实现了Runnable和Comparable接口。当DecodeJob被提交到线程池的时候,如果需要加入工作队列会通过compareTo比较Decodejob优先级


@Override
public int compareTo(@NonNull DecodeJob<?> other) {
 //先比较 Priority  
 int result = getPriority() - other.getPriority();
 //如果 Priority优先级一致 ,比较order order是一个自增的int 每一次初始化DecodeJob 都会执行++ 因此后初始化的DecodeJob比先初始化的优先级高。
 if (result == 0) {
   result = order - other.order;
}
 return result;
}
作者:小小小小小鹿
链接:https://juejin.cn/post/7038795986482757669
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin开发中的一些Tips

作用域函数选择 目前有let、run、with、apply 和 also五个作用域函数。 官方文档有张表来说明它们之间的区别:   总结一下有几点区别: 1、apply和also返回上下文对象。 2、let、run 和with返回lambda 结果。 3、l...
继续阅读 »

作用域函数选择


目前有letrunwithapply 和 also五个作用域函数。


官方文档有张表来说明它们之间的区别: 



 总结一下有几点区别:


1、applyalso返回上下文对象。


2、letrun 和with返回lambda 结果。


3、letalso引用对象是it ,其余是this


1.letrun是我日常使用最多的两个,它们之间很类似。


private var textView: TextView? = null

textView?.let {
it.text = "Kotlin"
it.textSize = 14f
}

textView?.run {
text = "Kotlin"
textSize = 14f
}

相比较来说使用run显得比较简洁,但let的优势在于可以将it重命名,提高代码的可读性,也可以避免作用域函数嵌套时导致混淆上下文对象的情况。


2.对于可空对象,使用let比较方便。对于非空对象可以使用with


3.applyalso也非常相似,文档给出的建议是如果是对象配置操作使用apply,额外的处理使用also。例如:


val numberList = mutableListOf()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

简单说就是符合单词的含义使用,提高代码可读性。


总的来说,这几种函数有许多重叠的部分,因此可以根据开发中的具体情况来使用。以上仅做参考。


Sequence


我们经常会使用到kotlin的集合操作符,比如 map 和 filter 等。


list.map {
it * 2
}.filter {
it % 3 == 0
}

老规矩,看一下反编译后的代码: 



就干了这么点事情,创建了两个集合,循环了两遍。效率太低,这还不如自己写个for循环,一个循环就处理完了。看一下map的源码:


public inline fun  Iterable.map(transform: (T) -> R): List {
return mapTo(ArrayList(collectionSizeOrDefault(10)), transform)
}

public inline fun > Iterable.mapTo(destination: C, transform: (T) -> R): C {
for (item in this)
destination.add(transform(item))
return destination
}

内部实现确实如此,难道这些操作符不香了?


其实这时就可以使用Sequences(序列),用法很简单,只需要在集合后添加一个asSeqence() 方法。


list.asSequence().map {
it * 2
}.filter {
it % 3 == 0
}

反编译:


SequencesKt.filter(SequencesKt.map(CollectionsKt.asSequence((Iterable)list), (Function1)null.INSTANCE), (Function1)null.INSTANCE);

有两个Function1,其实就是lambda表达式,这是因为Sequence没有使用内联导致的。我们先看看SequencesKt.map源码:


public fun  Sequence.map(transform: (T) -> R): Sequence {
return TransformingSequence(this, transform)
}

internal class TransformingSequence
constructor(private val sequence: Sequence, private val transformer: (T) -> R) : Sequence {
override fun iterator(): Iterator = object : Iterator {
val iterator = sequence.iterator()
override fun next(): R {
return transformer(iterator.next())
}

override fun hasNext(): Boolean {
return iterator.hasNext()
}
}

internal fun flatten(iterator: (R) -> Iterator): Sequence {
return FlatteningSequence(sequence, transformer, iterator)
}
}

可以看到没有创建中间集合去循环,只是创建了一个Sequence对象,里面实现了迭代器。SequencesKt.filter方法也是类似。细心的话你会发现,这都只是创建Sequence对象,所以要想真正拿到处理后的集合,需要添加toList()这种末端操作。


map 和 filter 这类属于中间操作,返回的是一个新Sequence,里面有数据迭代时的实际处理。而 toList和first这类属于末端操作用来返回结果。


所以Sequence是延迟执行的,这也就是它为何不会出现我们一开始提到的问题,一次循环就处理完成了。


总结一下Sequence的使用场景:


1、有多个集合操作符时,建议使用Sequence。


2、数据量大的时候,这样可以避免重复创建中间集合。这个数据量大,怎么也是万以上的级别了。


所以对于一般Android开发中来说,不使用Sequence其实差别不大。。。哈哈。。


协程


有些人会错误理解kotlin的协程,觉得它的性能更高,是一种“轻量级”的线程,类似go语言的协程。但是如果你细想一下,这是不太可能的,最终它都是要在JVM上运行,java都没有的东西,你就实现了,你这不是打java的脸嘛。


所以对于JVM平台,kotlin的协程只能是对Thread API的封装,和我们用的Executor类似。所以对于协程的性能,我个人也认为差别不大。只能说kotlin借助语言简洁的优势,让操作线程变的更加简单。


之所以上面说JVM,是因为kotlin还有js和native平台。对于它们来说,或许可以实现真正的协程。


推荐扔物线大佬关于协程的文章,帮你更好的理解kotlin的协程:到底什么是「非阻塞式」挂起?协程真的更轻量级吗?


Checked Exception


这对熟悉Java的同学并不陌生,Checked Exception 是处理异常的一种机制,如果你的方法中声明了它可能会抛出的异常,编译器就会强制开发者对异常进行处理,否则编译不会通过。我们需要使用 try catch 捕获异常或者使用 throws 抛出异常处理它。


但是Kotlin中并不支持这个机制,也就是说不会强制你去处理抛出的异常。至于Checked Exception 好不好,争议也不少。这里就不讨论各自的优缺点了。


既然Kotlin中没有这个机制已经是既成事实,那么我们在使用中就需要考虑它带来的影响。比如我们开发中在调用一些方法时,要注意看一下源码中是否有指定异常抛出,然后做相应处理,避免不必要的崩溃。


例如常用的json解析:


private fun test() {
val jsonObject = JSONObject("{...}")
jsonObject.getString("id")
...
}

在java中我们需要处理JSONException,kotlin中因为没有Checked Exception,如果我们像上面这样直接使用,虽然程序可以运行,可是一但解析出现异常,程序就会崩溃。


Intrinsics检查


如果你经常观察反编译后的java代码,会发现有许多类似Intrinsics.checkXXX这样的代码。


fun test(str: String) {
println(str)
}

反编译: 



 比如图中的checkParameterIsNotNull就是用了检查参数是否为空。虽然我们的参数是不可控的,但是考虑到方法会被Java调用,Kotlin会默认的增加checkParameterIsNotNull校验。如果kotlin方法是私有的,也就不会有此行检查。


checkParameterIsNotNull并不会有性能问题,相反这种提前判断参数是否正确,可以避免程序向后执行导致不必要的资源消耗。


当然如果你想去除它,可以添加下面的配置到你的gradle文件,这样就会在编译时去除它。


kotlinOptions {
freeCompilerArgs = [
'-Xno-param-assertions',
'-Xno-call-assertions',
'-Xno-receiver-assertions'
]
}

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

一图秒懂CDN原理

CDN
前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。 先了说明下现状: 图片保存在阿里OSS中 国内使用了阿里云CDN 国外使用Akamai(全球CDN厂商) 按理说,CDN都有...
继续阅读 »

前些天,线上灰度了一个功能,下午接到一些业务上报国外用户访问时图片无法显示,但是国内访问都是正常,所以怀疑是国外CDN问题导致。


先了说明下现状:



  1. 图片保存在阿里OSS中

  2. 国内使用了阿里云CDN

  3. 国外使用Akamai(全球CDN厂商)



按理说,CDN都有,图片不应该访问不到。于是,在脑子中根据CDN的原理,先思考下可能的问题



CDN原理


CDN全称是Content Delivery Network,即内容分发网络,也称为内容传送网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。


cdn.jpg


如上图CDN的逻辑主要分为两步:DNS解析请求边缘节点



用dig看下DNS解析结果:



$ dig juejin.cn

; <<>> DiG 9.10.6 <<>> juejin.cn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63296
;; flags: qr rd ra; QUERY: 1, ANSWER: 9, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;juejin.cn. IN A

;; ANSWER SECTION:
juejin.cn. 412 IN CNAME juejin.cn.w.cdngslb.com.
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.229
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.227
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.231
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.224
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.225
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.230
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.226
juejin.cn.w.cdngslb.com. 60 IN A 112.85.251.228

;; Query time: 9 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Sat May 15 14:26:26 CST 2021
;; MSG SIZE rcvd: 203

在ANSWER SECTION列表可以看出



  1. juejin.cn为cname记录指向juejin.cn.w.cdngslb.com

  2. juejin.cn.w.cdngslb.com返回了7条A记录,这7个ip 信息是江苏 徐州 联通,我所在地是上海,联通,可以看出返回的都是就近节点。实际上CDN是有非常多的边缘节点。



用tcpdump来监控下DNS的UDP数据包




  1. 在一个窗口输入sudo tcpdump -n -s 1500 udp and port 53

  2. 在另一个窗口输入ping juejin.cn


监控到的UDP数据包如下:


21:49:13.960212 IP 192.168.3.201.52647 > 192.168.3.1.53: 37581+ A? juejin.cn. (27)
21:49:13.975290 IP 192.168.3.1.53 > 192.168.3.201.52647: 37581 9/0/0 CNAME juejin.cn.w.cdngslb.com., A 112.85.251.229, A 112.85.251.230, A 112.85.251.226, A 112.85.251.228, A 112.85.251.224, A 112.85.251.231, A 112.85.251.225, A 112.85.251.227 (192)

其中,192.168.3.1为路由器IP。也就是本机向路由器询问DNS解析,如果路由器已经缓存了,就会直接返回。


复现问题


我们回到问题中,如果CDN返回的边缘节点如果不出问题,图片应该是可以很快访问到的,CDN厂商不至于出现这个问题。那么问题在那里呢?


在公司环境无法复现问题,就要找一个最接近客户场景的环境来测试,于是想办法搞到一台香港window系统的测试机,远程上去一看,还果真有问题。



图片在界面中不显示,但是直接在浏览器访问是正常的,开发者模式下发现访问图片时出现跨域错误



一张正常显示的图片请求返回的http头是这样的:


Response Headers:
accept-ranges: bytes
access-control-allow-origin: *
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

1. access-control-allow-origin


通配符 * 表示允许被任何网站引用。如果想让资源只被指定域名访问,只需把*改为域名就行了,如下:


access-control-allow-origin: `https://juejin.cn`

2. etag


etag是http协议缓存逻辑中的一个属性。CDN的目的就是减少网络访问,因为缓存是必须要用的功能。


而无法显示的图片,返回的请求头是这样的:


Response Headers:
accept-ranges: bytes
etag: "A31F477F3232DA431D3B77543C3EBF92"
last-modified: Thu, 25 Jul 2019 01:37:03 GMT
...省略

没有access-control-allow-origin这一项,导致页面中无法加载。


浏览器边缘节点请求图片命中缓存,返回图片响应头中没有CORS属性抛出CORS异常,图片不渲染浏览器边缘节点


解决办法很简单,在CDN后台配置返回access-control-allow-origin信息即可


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

Flutter: 完成一个图片APP

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如...
继续阅读 »

自从 Flutter 推出之后, 一直是备受关注, 有看好的也有不看好的, 作为移动开发人员自然是要尝试一下的(但是它的嵌套写法真的难受), 本着学一个东西, 就一定要动手的态度, 平时又喜欢看一些猫狗的图片, 就想着做一个加载猫狗图片你的 APP, 界面图如下(界面不是很好看).






主要模块



NetWork

api.dart文件中, 分别定义了DogApi, CatApi两个类, 一个用于处理获取猫的图片的类, 一个用于处理狗的图片的类.


http_request.dart文件封装了Http请求, 用于发送和接收数据.


url.dart文件封装了需要用到的Api接口, 主要是为了方便和统一管理而编写.


Models文件夹下分别定义不同API接口返回数据的模型.


图片页

瀑布流使用的flutter_staggered_grid_view库, 作者自定义了Delegate计算布局, 使用起来非常简单.


Widget scene = new StaggeredGridView.countBuilder(
physics: BouncingScrollPhysics(),
itemCount: this.breedImgs != null ? this.breedImgs.urls.length : 0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
crossAxisCount: 3,
itemBuilder: (context, index) {
return new GestureDetector(
onTapUp: (TapUpDetails detail) {
// 展示该品种的相关信息
dynamic breed = this.breeds[this.selectedIdx].description;
// TODO: 取出当前点击的然后所有往后的
List<String> unreadImgs = new List<String>();
for (int i = index; i < this.breedImgs.urls.length; i++) {
unreadImgs.add(this.breedImgs.urls[i]);
}
AnimalImagesPage photoPage = new AnimalImagesPage(
listImages: unreadImgs,
breed: this.breeds[this.selectedIdx].name,
imgType: "Cat",
petInfo: this.breeds[this.selectedIdx],
);
Navigator.of(context)
.push(new MaterialPageRoute(builder: (context) {
return photoPage;
}));
},
child: new Container(
width: 100,
height: 100,
color: Color(0xFF2FC77D), //Colors.blueAccent,
child: new CachedNetworkImage(
imageUrl: this.breedImgs.urls[index],
fit: BoxFit.fill,
placeholder: (context, index) {
return new Center(child: new CupertinoActivityIndicator());
},
),
),
);
},
// 该属性可以控制当前 Cell 占用的空间大小, 用来实现瀑布的感觉
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(1, index.isEven ? 1.5 : 1),
);

  • 组装PickerView


系统默认的 PickerView 在每一次切换都会回调, 而且没有确定和取消事件,
如果直接使用会造成频繁的网络请求, 内存消耗也太快, 所以组装了一下, 增加确定和取消才去执行网络请求, 这样就解决了这个问题.


    Widget column = Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
width: MediaQuery.of(context).size.width,
height: 40,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Padding(
padding: EdgeInsets.only(left: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
// 回调操作
this.submit(this.selectedIndex);
},
child: new Text(
"确定",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
),
new Padding(
padding: EdgeInsets.only(right: 10.0),
child: new GestureDetector(
onTapUp: (detail) {
// 点击了确定按钮, 退出当前页面
Navigator.of(context).pop();
},
child: new Text(
"取消",
style: TextStyle(
decoration: TextDecoration.none,
color: Colors.white,
fontSize: 18),
),
),
)
],
),
),
],
),
new Container(
height: 1,
color: Colors.white,
),
// Picker
new Expanded(
child: new CupertinoPicker.builder(
backgroundColor: Colors.transparent,
itemExtent: 44,
childCount: this.names.length,
onSelectedItemChanged: (int selected) {
this.selectedIndex = selected;
this.onSelected(selected);
},
itemBuilder: (context, index) {
return new Container(
width: 160,
height: 44,
alignment: Alignment.center,
child: new Text(
this.names[index],
textAlign: TextAlign.right,
style: new TextStyle(
color: Colors.white,
fontSize: 16,
decoration: TextDecoration.none),
),
);
}),
)
],
);
详情页


  • Column 包含 ListView


详情页中, 上方是一个图片, 下方是关于品种的相关信息, 下方是通过 API获取到的属性进行一个展示, 需要注意一点是, 如果Column封装了MainAxis相同方向的滚动控件, 必须设置Width/Height, 同理, Row也是需要注意这一点的.


我在这里的做法是通过一个Container包裹 ListView.


new Container(
margin: EdgeInsets.only(bottom: 10, top: 10),
height: MediaQuery.of(context).size.height - MediaQuery.of(context).size.width / 1.2 - 80,
width: MediaQuery.of(context).size.width,
child: listView,
),

  • 图片动画


这一部分稍微复杂一些, 首先需要监听滑动的距离, 来对图片进行变换, 最后根据是否达到阈值来进行切换动画, 这里我没有实现在最后一张和第一张图片进行切换以至于可以无限循环滚动, 我在边界阈值上只是阻止了下一步动画.


动画我都是通过Matrix4来设置不同位置的属性, 它也能模拟出 3D 效果,


动画的变换都是Tween来管理.


  void _initAnimation() {
// 透明度动画
this.opacityAnimation = new Tween(begin: 1.0, end: 0.0).animate(
new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.decelerate))
..addListener(() {
this.setState(() {
// 通知 Fluter Engine 重绘
});
});
// 翻转动画
// 第三个值是角度
var startTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
var endTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..rotateX(3.1415927);
this.transformAnimation = new Tween(begin: startTrans, end: endTrans)
.animate(new CurvedAnimation(
parent: this._nextAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
// 缩放
var saveStartTrans = Matrix4.identity()..setEntry(3, 2, 0.006);
// 平移且缩放
var saveEndTrans = Matrix4.identity()
..setEntry(3, 2, 0.006)
..scale(0.1, 0.1)
..translate(-20.0, 20.0); // MediaQuery.of(context).size.height
this.saveToPhotos = new Tween(begin: saveStartTrans, end: saveEndTrans)
.animate(new CurvedAnimation(
parent: this._saveAnimationController, curve: Curves.easeIn))
..addListener(() {
this.setState(() {});
});
}

Widget引用这个属性来执行动画.


Widget pet = new GestureDetector(
onVerticalDragUpdate: nextUpdate,
onVerticalDragStart: nextStart,
onVerticalDragEnd: next,
child: new Transform(
transform: this.dragUpdateTransform,
child: Container(
child: new Transform(
alignment: Alignment.bottomLeft,
transform: transform,
child: new Opacity(
opacity: opacity,
child: Container(
width: MediaQuery.of(context).size.width / 1.2,
height: MediaQuery.of(context).size.width / 1.5 - 30,
child: new Padding(
padding: EdgeInsets.all(0),
child: new CachedNetworkImage(
imageUrl: this.widget.listImages[item],
fit: BoxFit.fill,
placeholder: (context, content) {
return new Container(
width: MediaQuery.of(context).size.width / 2.0 - 40,
height: MediaQuery.of(context).size.width / 2.0 - 60,
color: Color(0xFF2FC77D),
child: new Center(
child: new CupertinoActivityIndicator(),
),
);
},
),
),
),
),
),
),
),
);
Firebase_admob

注意: 这里需要去 firebase 官网注册 APP, 然后分别下载 iOS, Android 的配置文件放到指定的位置, 否则程序启动的时候会闪退.


iOS info.plist: GADApplicationIdentifier也需要配置, 虽然在 Dart 中会启动的时候就注册ID, 但是这里也别忘了配置.


Android Manifst.xml 也需要配置


<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value=""/>

这里说一下我因为个人编码导致的问题, 我尝试自己来控制广告展示, 加了一个读秒跳过按钮(想强制观看一段时间), 点击跳过设置setState, 但是在 build 方法中又请求了广告, 导致了一个死循环, 最后由于请求次数过多还没有设置自己的设备为测试设备也不是使用的测试ID, 账号被暂停了, 所以大家使用的时候要避免这个问题, 尽量还是将自己的设备添加到测试设备中.


使用的话比较简单(官方的演示代码直接复制也可以用).


class AdPage {
MobileAdTargetingInfo targetingInfo;

InterstitialAd interstitial;

BannerAd banner;

void initAttributes() {
if (this.targetingInfo == null) {
this.targetingInfo = MobileAdTargetingInfo(
keywords: ["some keyword for your app"],
// 防止被Google 认为是无效点击和展示.
testDevices: ["Your Phone", "Simulator"]);

bool android = Platform.isAndroid;

this.interstitial = InterstitialAd(
adUnitId: InterstitialAd.testAdUnitId,
targetingInfo: this.targetingInfo,
listener: (MobileAdEvent event) {
if (event == MobileAdEvent.closed) {
// 点击关闭
print("InterstitialAd Closed");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.clicked) {
// 关闭
print("InterstitialAd Clicked");
this.interstitial.dispose();
this.interstitial = null;
} else if (event == MobileAdEvent.loaded) {
// 加载
print("InterstitialAd Loaded");
}
print("InterstitialAd event is $event");
},
);

// this.banner = BannerAd(
// targetingInfo: this.targetingInfo,
// size: AdSize.smartBanner,
// listener: (MobileAdEvent event) {
// if (event == MobileAdEvent.closed) {
// // 点击关闭
// print("InterstitialAd Closed");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.clicked) {
// // 关闭
// print("InterstitialAd Clicked");
// this.interstitial.dispose();
// this.interstitial = null;
// } else if (event == MobileAdEvent.loaded) {
// // 加载
// print("InterstitialAd Loaded");
// }
// print("InterstitialAd event is $event");
// });
}
}

@override
void show() {
// 初始化数据
this.initAttributes();
// 然后控制跳转
if (this.interstitial != null) {
this.interstitial.load();
this.interstitial.show(
anchorType: AnchorType.bottom,
anchorOffset: 0.0,
);
}
}
}

项目比较简单, 但是编写的过程中也遇到了许多问题, 慢慢解决的过程也学到了挺多.


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

Flutter页面传值的几种方式

今天来聊聊Flutter页面传值的几种方式: InheritWidget Notification Eventbus (当前Flutter版本:2.0.4) InheritWidget 如果看过Provider的源码的同学都知道,Provider跨组件传值...
继续阅读 »

今天来聊聊Flutter页面传值的几种方式:



  1. InheritWidget

  2. Notification

  3. Eventbus


(当前Flutter版本:2.0.4)


InheritWidget


如果看过Provider的源码的同学都知道,Provider跨组件传值的原理就是根据系统提供的InheritWidget实现的,让我们来看一下这个组件。
InheritWidget是一个抽象类,我们写一个保存用户信息的类UserInfoInheritWidget继承于InheritWidget:


class UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
UserInfoInheritWidget({Key key, this.userInfoBean, Widget child}) : super (child: child);

static UserInfoWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<UserInfoWidget>();
}

@override
bool updateShouldNotify(UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

我们在这里面定义了一个静态方法:of,并且传入了一个context,根据context获取当前类,拿到当前类中的UserInfoBean,其实获取主题数据也是根据InheritWidget这种方式获取Theme.of(context),关于of方法后面重点讲一下,updateShouldNotify是刷新机制,什么时候刷新数据


还有一个用户信息的实体:


class UserInfoBean {
String name;
String address;
UserInfoBean({this.name, this.address});
}

我们做两个页面,第一个页面显示用户信息,还有一个按钮,点击按钮跳转到第二个页面,同样也是显示用户信息:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Column(
children: [
Text(UserInfoWidget.of(context)!.userInfoBean.name),
Text(UserInfoWidget.of(context)!.userInfoBean.address),
SizedBox(height: 40),
TextButton(
child: Text('点击跳转'),
onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (context){
return DetailPage();
}));
},
)
],
),
),
);
}
}

class DetailPage extends StatefulWidget {
@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Detail'),
),
body: DefaultTextStyle(
style: TextStyle(fontSize: 30, color: Colors.black),
child: Center(
child: Column(
children: [
Text(UserInfoWidget.of(context).userInfoBean.name),
Text(UserInfoWidget.of(context).userInfoBean.address),
TextButton(
onPressed: () {
setState(() {
UserInfoWidget.of(context)!.updateBean('wf123','address123');
});
},
child: Text('点击修改'))
],
),
),
)
);
}
}

由于我们这里是跨组件传值,需要把UserInfoWidget放在MaterialApp的上层,并给UserInfoBean一个初始值:


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return UserInfoWidget(
userInfoBean: UserInfoBean(name: 'wf', address: 'address'),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}

这样就实现了一个跨组件传值,但是还有个问题,我们给UserInfoWidget赋值的时候是在最顶层,在真实业务场景中,如果我们把UserInfo的赋值放在MaterialApp上面,这时候我们还没拿到用户数据呢,所以就要有一个可以更新UserInfo的方法,并且修改后立即刷新,我们可以借助setState,把我们上面定义的UserInfoWidget改个名字然后封装在StatefulWidget 中:


class _UserInfoInheritWidget extends InheritedWidget {

UserInfoBean userInfoBean;
Function update;
_UserInfoInheritWidget({Key key, this.userInfoBean, this.update, Widget child}) : super (child: child);

updateBean(String name, String address){
update(name, address);
}

@override
bool updateShouldNotify(_UserInfoInheritWidget oldWidget) {
return oldWidget.userInfoBean != userInfoBean;
}
}

class UserInfoWidget extends StatefulWidget {
UserInfoBean userInfoBean;
Widget child;
UserInfoWidget({Key key, this.userInfoBean, this.child}) : super (key: key);

static _UserInfoInheritWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<_UserInfoInheritWidget>();
}
@override
State<StatefulWidget> createState() => _UserInfoState();
}

class _UserInfoState extends State <UserInfoWidget> {

_update(String name, String address){
UserInfoBean bean = UserInfoBean(name: name, address: address);
widget.userInfoBean = bean;
setState(() {});
}
@override
Widget build(BuildContext context) {
return _UserInfoInheritWidget(
child: widget.child,
userInfoBean: widget.userInfoBean,
update: _update,
);
}
}

上面把继承自InheritWidget的类改了一个名字:_UserInfoInheritWidget,对外只暴露用StatefulWidget封装过的UserInfoWidget,向_UserInfoInheritWidget传入了包含setState的更新数据方法,更新数据的时候通过UserInfoWidget.of(context)获取到继承于InheritWidget_UserInfoInheritWidget类,调用updateBean方法实际上就调用了包含setState的方法,所以做到了数据更新和页面刷新


1.gif


下面重点说一下UserInfoWidget.of(context)是如何获取到继承于InheritWidget类的对象的,通过查看类似的方法:Theme.of(context)发现是根据dependOnInheritedWidgetOfExactType,于是我们也照着它的样子获取到了_UserInfoInheritWidget,点到dependOnInheritedWidgetOfExactType源码中看一下,发现跳转到了BuildContext中定义了这个方法:


  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
复制代码

了解WidgetElementRenderObject三只之间关系的同学都知道,其实contextElement的一个实例,BuildContext的注释也提到了这一点:


image.png
我们可以在Element中找到这个方法的实现:


@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

_inheritedWidgets是从哪来的,我们搜索一下在Element中发现


void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}

再看一下_updateInheritance方法是什么时候调用的


@mustCallSuper
void mount(Element? parent, dynamic newSlot) {
...
...省略无关代码
_parent = parent;
_slot = newSlot;
_lifecycleState = _ElementLifecycle.active;
_depth = _parent != null ? _parent!.depth + 1 : 1;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
final Key? key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();//这里调用了一次
}

还有:


@mustCallSuper
void activate() {
...
...已省略无关代码
final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
_lifecycleState = _ElementLifecycle.active;
_dependencies?.clear();
_hadUnsatisfiedDependencies = false;
_updateInheritance();//这里又调用了一次
if (_dirty)
owner!.scheduleBuildFor(this);
if (hadDependencies)
didChangeDependencies();
}

从上面代码我们可以看到每个页面的Element都会通过_parent向下级传递父级信息,而我们的UserInfoWidget就保存在_parent中的_inheritedWidgets集合中:
Map<Type, InheritedElement>? _inheritedWidgets;,当_inheritedWidgets在页面树中向下传递的时候,如果当前WidgetInheritWidget,在当前Widget对应的Element中先看_parent传过来的_inheritedWidgets是否为空,如果为空就新建一个集合,把自己存到这个集合中,以当前的类型作为key(这也是为什么调用of方法中的context.dependOnInheritedWidgetOfExactType方法为什么要传当前类型的原因),从_inheritedWidgets集合中去取值;如果不为空直接把自己存进去,这就是of的原理了。


Notification


上面讲的InheritWidget一般是根部组建向子级组件传值,Notification是从子级组件向父级组件传值,下面我们来看一下它的用法


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: NotificationListener<MyNotification>(
onNotification: (MyNotification data) {
userInfoBean = data.userInfoBean;
setState(() {});
///这里需要返回一个bool值,true表示阻止事件继续向上传递,false表示事件可以继续向上传递到父级组件
return true;
},
child: Builder(
///这里用了一个Builder包装了一下,为的是能取到
///NotificationListener的context
builder: (context) {
return Column(
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
Container(
child: FlatButton(
child: Text('点击传值'),
onPressed: () {
MyNotification(userInfoBean: UserInfoBean(name: 'wf123', address: 'address123')).dispatch(context);
},
),
)
],
);
},
),
),
),
);
}
}

///Notification是一个抽象类,
///使用Notification需要自定义一个class继承Notification
class MyNotification extends Notification {
UserInfoBean userInfoBean;
MyNotification({this.userInfoBean}) : super();
}

我们到源码中看一下这个dispatch方法:


void dispatch(BuildContext target) {
// The `target` may be null if the subtree the notification is supposed to be
// dispatched in is in the process of being disposed.
target?.visitAncestorElements(visitAncestor);
}

target就是我们传进来的context,也就是调用了BuildContextvisitAncestorElements方法,并且把visitAncestor方法作为一个参数传过去,visitAncestor方法返回一个bool值:


  @protected
@mustCallSuper
bool visitAncestor(Element element) {
if (element is StatelessElement) {
final StatelessWidget widget = element.widget;
if (widget is NotificationListener<Notification>) {
if (widget._dispatch(this, element)) // that function checks the type dynamically
return false;
}
}
return true;
}

我们进入Element内部看一下visitAncestorElements方法的实现:


@override
void visitAncestorElements(bool visitor(Element element)) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor))
ancestor = ancestor._parent;
}

当有父级节点,并且visitor方法返回true的时候执行while循环,visitorNotification类传进来的方法,回过头再看visitor方法的实现,当Elementvisitor方法传递的ancestorNotificationListener类的情况下,再判断widget._dispatch方法,而widget._dispatch方法:


final NotificationListenerCallback<T>? onNotification;

bool _dispatch(Notification notification, Element element) {
if (onNotification != null && notification is T) {
final bool result = onNotification!(notification);
return result == true; // so that null and false have the same effect
}
return false;
}

就是我们在外面写的onNotification方法的实现,我们在外面实现的onNotification方法返回true(即阻止事件继续向上传递),上面的while循环主要是为了执行我们onNotification里面的方法.


总结一下:MyNotification执行dispatch方法,传递context,根据当前context向父级查找对应NotificationListener,并且执行NotificationListener里面的onNotification方法,返回true,则事件不再向上级传递,如果返回false则事件继续向上一个NotificationListener传递,并执行里面对应的方法。Notification主要用在同一个页面中,子级向父级传值,比较轻量级,不过如果我们用了Provider可能就就直接借助Provider传值了。


Eventbus


Eventbus用于两个不同的页面,可以跨多级页面传值,用法也比较简单,我创建了一个EventBusUtil来创建一个单例


import 'package:event_bus/event_bus.dart';
class EventBusUtil {
static EventBus ? _instance;
static EventBus getInstance(){
if (_instance == null) {
_instance = EventBus();
}
return _instance!;
}
}

在第一个页面监听:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
EventBusUtil.getInstance().on<UserInfoBean>().listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//不用的时候记得关闭
EventBusUtil.getInstance().destroy();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return EventBusDetailPage();
}));
}, child: Text('点击跳转'))

],
),
),
);
}
}

在第二个页面发送事件:


class EventBusDetailPage extends StatefulWidget {
@override
_EventBusDetailPageState createState() => _EventBusDetailPageState();
}

class _EventBusDetailPageState extends State<EventBusDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('EventBusDetail'),
),
body: Center(
child: TextButton(onPressed: (){
EventBusUtil.getInstance().fire(UserInfoBean(name: 'name EventBus', address: 'address EventBus'));
}, child: Text('点击传值')),
),
);
}
}

我们看一下EventBus的源码,发现只有几十行代码,他的内部是创建了一个StreamController,通过StreamController来实现跨组件传值,我们也可以直接使用一下这个StreamController实现页面传值:


class Page19PassByValue extends StatefulWidget {
@override
_Page19PassByValueState createState() => _Page19PassByValueState();
}

StreamController controller = StreamController();

class _Page19PassByValueState extends State<Page19PassByValue> {

//设置一个初始值
UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
@override
void initState() {
super.initState();
controller.stream.listen((event) {
setState(() {
userInfoBean = event;
});
});
}

@override
void dispose() {
super.dispose();
//页面销毁的时候记得关闭
controller.close();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PassByValue'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(userInfoBean.name),
Text(userInfoBean.address),
TextButton(onPressed: (){
Navigator.of(context).push(CupertinoPageRoute(builder: (_){
return MyStreamControllerDetail();
}));
}, child: Text('点击跳转'))
],
),
)
);
}
}

class MyStreamControllerDetail extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyStreamControllerDetailState();
}
}
class _MyStreamControllerDetailState extends State <MyStreamControllerDetail> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StreamController'),
),
body: Center(
child: TextButton(onPressed: (){
//返回上个页面,会发现页面的数据已经变了
controller.sink.add(UserInfoBean(name: 'StreamController pass name: 123', address: 'StreamController pass address 123'));
}, child: Text('点击传值'),),
),
);
}
}

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

Swift 指针的应用

iOS
Swift与指针由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。但是,“慎用”不代表“不能用”,更不代表“没用”。相反,...
继续阅读 »

Swift与指针

由于Swift本身是一门较为现代的语言,支持很多高级特性,所以对于程序员来说,大部分时候不需要用到指针这种更“底层”的特性。而Swift语言的设计者也在尽可能希望开发者能尽量少的使用指针。

但是,“慎用”不代表“不能用”,更不代表“没用”。相反,指针非常有用,在某些场景下还是必不可少的特性。尤其是开发工作和系统底层特性、内存处理、高性能需求息息相关时。

所以,Swift通过在施加某种限制的前提下为开发者暴露了指针的使用接口,本篇文章重点介绍Swift使用指针的相关类型、函数,以及在实践应用中灵活使用指针解决问题的技巧。

类型限定的指针 UnsafePointer

Swift通过UnsafePointer<T>来指向一个类型为T的指针,该指针的内容是 只读 的,对于一个UnsafePointer<T>变量来说,通过pointee成员即可获得T的值。

func call(_ p: UnsafePointer<Int>) {
print("\(p.pointee)")
}
var a = 1234
call(&a) // 打印:1234

以上例子中函数call接收一个UnsafePointer<Int>类型作为参数,变量a通过在变量名前面加上&将其地址传给call。函数call直接打印指针的pointee成员,该成员就是a的值,所以最终打印结果为1234

注1:&aswift提供的语法特性,用于传递指针,但它有严格的适用场景限制。

注2:注意示例中对于变量a使用了var声明,而事实上UnsafePointer是“常量指针”,并不会修改a的内容,即使是这样a还是必须用var声明,如果用let会报错Cannot pass immutable value as inout argument: 'a' is a 'let' constant。这是因为swift规定UnsafePointer作为参数只能接收inout修饰的类型,而inout修饰的类型必然是可写的,所以使用var在所难免。

内容可写的类型限定指针 UnsafeMutablePointer

既然有 内容只读 指针,必须也得有 内容可读写 指针搭配才行,在Swift中,内容可读写的类型限定指针为UnsafeMutablePointer<T>类型,就和名字描述的那样,它和UnsafePointer最大的区别就是它指向的内容是可更改的,并且更改后指向的“数据源”也会被改动。

func modify(_ p: UnsafeMutablePointer<Int>) {
p.pointee = 5678
}
var a = 1234
modify(&a)
print("\(a)") // 打印:5678

在以上的例子中,指针p指向的值被重新赋值为5678,这也使得指针的“源”,即变量a的值发生变化,最终打印a的结果可以看出a被修改为5678

指针的辅助函数 withUnsafePointer

通过函数withUnsafePointer,获得指定类型的对应指针。该函数原型如下:

func withUnsafePointer<T, Result>(to value: inout T, _ body: (UnsafePointer<T>) throws -> Result) rethrows -> Result

这个函数原型看似很复杂很长,其实只要理解它所需要的信息只有两个:

  1. 指针指向类型是什么。
  2. 想要返回的指针地址是什么。
var a = 1234
let p = withUnsafePointer(to: &a) { $0 }
print("\(p.pointee)") // 打印:1234

以上例子是withUnsafePointer最精简的调用例子,我们定义了一个整形a,而p就是指向a的整形指针,事实上它的类型会被自动转换为UnsafePointer<Int>,第二个参数被简化为了{ $0 },它传入了一个代码块,代码块接收一个UnsafePointer<Int>参数,该参数即是a的地址,直接通过$0将它返回,即得到了a的指针,最终它被传给了p

对于第二个参数,或许有人会产生疑问,它似乎是没有意义的参数,大部分时候我们不是直接返回a的地址吗,为什么要多此一举通过代码块返回一次?这个疑问是合理的,绝大多数时候确实第二个参数显得有些多余,当然了,有时候可以通过第二个参数提供指针偏移的灵活性,如下例子可以提供一个案例。

var a = [1234, 5678]
let p = withUnsafePointer(to: &a[0]) { $0 + 1 }
print("\(p.pointee)") // 打印:5678

以上例子中,通过在第二个参数中对地址施加偏移,可以原来指向数组首个元素的地址偏移到第二个地址中。

另外,由于withUnsafePointer带着两个泛型参数,这意味着第二个参数可以是不同的类型。

var a = 1234
let p = withUnsafePointer(to: &a) { $0.debugDescription }
print("\(p)")

以上例子中,withUnsafePointer返回的并不是UnsafePointer<Int>类型,甚至不是指针,而是一个字符串,字符串保存着a对应指针的debug信息。

注1:同样的,和withUnsafePointer相对应的,还有withUnsafeMutablePointer,一样是只读和可读写的区别。读者可以自行测试用法。 注2:基本上Swift指针操作的with系列函数都提供了第二个参数用来灵活的提供函数的返回类型。

获取指针并进行字节级操作 withUnsafeBytes

有时候,我们需要对某块内存进行字节级编程。比如我们用一个32位整形来表示一个32位的内存块,对内存中的每个字节进行读写操作。

通过withUnsafeBytes,可以得到某个类型的数据的字节指针,从而可以对它们进行字节级编程。

var a: UInt32 = 0x12345678
let p = withUnsafeBytes(of: &a) { $0 }
var log = ""
for item in p {
let hex = NSString(format: "%x", item)
log += "\(hex)"
}
print("\(p.count)") // 打印:4
print("\(log)") // 对于小端机器会打印:78563412

在以上例子中,withUnsafeBytes返回了一个类型UnsafeRawBufferPointer,该类型代表着一个字节级的内存块,并提供了等价于数组操作,所以你可以通过下标索引、for循环的方式来处理返回的对象。

例子中的a是一个32位整形,所以p指针的count返回的是4,单位为字节。 在本例中,对内存块p从低到高逐字节的打印每个字节的16进制值。 具体打印出来的结果因运行的机器而异,在大端机器上,打印的结果是12345678,而在小端机器上打印结果则是78563412

注:大端和小端决定了一个基础数据单元在内存中是如何按序存放的,例如小端机器会将基本数据单元的低位放在内存的低位,由低到高排列,而大端机器则相反。具体相关知识可查阅维基百科。大部分情况下,同一台机器采用的字节序列是一致的,某些CPU可以配置大小端的切换。

指向连续内存的指针 UnsafeBufferPointer

Swift的数组提供了函数withUnsafeBufferPointer,通过它我们可以方便的用指针来处理数组。如下例子:

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
print("\(p.count)") // 打印:6
print("\(p[3])") // 打印:-2

在该例子中,通过withUnsafeBufferPointer,可以获得变量pp的类型为UnsafeBufferPointer<Int32>,它代表着一整块的连续内存,我们可以像看待数组一样看待它,并且它也支持大部分数组操作。

指针的类型转换

介绍了那么多Swift中的指针类型,每一种都有各自的用途,但是在实际开发中,很可能我们需要将一个指针类型转换为特定的指针类型。

以下例子提供了几个类型指针之间的转换

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p2: UnsafePointer<UInt32>
let p2 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

以上例子中,我们获得了以下三个指针类型

  1. UnsafeBufferPointer<Int32>类型的指针p
  2. UnsafePointer<UInt32>类型的指针p2
  3. UnsafeBufferPointer<UInt32>类型的指针p3

该例子有部分细节必须讲明,首先是baseAddress,通过该成员得到UnsafeBufferPointer基地址,获得的数据类型是UnsafePointer<>

由于a指向的元数据类型是Int32,所以其baseAddress类型即是UnsafePointer<Int32>

在本例中,我们将元数据类型由Int32改为UInt32,这里用到了UnsafePointer的成员函数withMemoryRebound,通过它将UnsafePointer<Int32>转换为UnsafePointer<UInt32>

最后一部分,我们创建了一个新的指针UnsafeBufferPointer,通过其构造函数,我们让该指针的起始位置设定为p2,元素个数设定为p的元素个数,这样就成功得到了一个UnsafeBufferPointer<UInt32>类型。

接下来的打印语句,我们可以看到p3类型的count成员依然是6,而p3[3]打印的结果却是4294967294,而不是数组a对应元素的-2,这是因为从p3的角度来看,它是用UInt32类型来“看待”原先的Int32数据元素。

回调函数的实用性

前面讨论withUnsafePointer时我曾经提过第二个回调参数似乎略显鸡肋,事实上它非常有用,通过回调函数,我们可以对上一段代码进行“优化”。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
// 类型 p: UnsafeBufferPointer<Int32>
let p = a.withUnsafeBufferPointer { $0 }
// 类型 p3: UnsafeBufferPointer<UInt32>
let p3 = p.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: p.count) {
UnsafeBufferPointer(start: $0, count: p.count)
}
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

可以看到利用回调函数,我们把原先的p2p3代码合并了,这样可以让withMemoryRebound立刻返回UnsafeBufferPointer<UInt32>类型。

注:事实上该回调还可以不断“套娃”,也就是说可以直接把p3部分的代码和p也进行合并,但是出于可读性考虑,开发者应自己根据需要选择性进行嵌套。

Swift中的空指针:UnsafeRawPointer

就像C语言有void*(即空指针)一样,Swift也有自己的空指针,它通过类型UnsafeRawPointer来获得,我们知道,空指针没有指向特定的类型,又“可以”指向任何类型,灵活性极高,也需要程序员自己能够理解和处理好对应的细节。

同样是将UnsafeBufferPointer<Int32>转换为UnsafeBufferPointer<UInt32>,以下代码通过UnsafeRawPointer来实现。

let a: [Int32] = [1, 2, -1, -2, 5, 6]
let p = a.withUnsafeBufferPointer { $0 }
let p2 = UnsafeRawPointer(p.baseAddress!).assumingMemoryBound(to: UInt32.self)
let p3 = UnsafeBufferPointer(start: p2, count: p.count)
print("\(p3.count)") // 打印:6
print("\(p3[3])") // 打印:4294967294

在该例子中我们通过空指针完成了如下操作:

  1. UnsafeRawPointer通过构造函数接收了p的“基地址”构造了一个空指针类型。
  2. 由于构造的是空指针类型,我们需要对它进行类型转换,通过assumingMemoryBound把它转换成新的数据类型UnsafePointer<UInt32>
  3. 通过UnsafeBufferPointer构造函数重新构造了一个新的指针UnsafeBufferPointer<UInt32>

通过指针动态创建、销毁内存

有时候我们需要动态开辟和管理一块内存,最后释放它,Swift提供了UnsafeMutablePointer的成员函数allocate来处理该工作。

let p = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
p.initialize(to: 0) // 初始化
p.pointee = 32
print("\(p.pointee)") // 打印:32
p.deinitialize(count: 1) // 反初始化
p.deallocate()

以上例子中我们提供了一个存放32位整形的内存块,容量为1(即其容量为1个32位整形,实际就是 4 个字节)。 接下来的代码示例较为简单,即开辟内存,初始化、赋值、反初始化,释放内存的流程。

Swift指针类型和C指针类型的对应关系

Swift的指针类型看似繁多,事实上只是对C指针类型进行了封装和类别整理,并增加了一定程度上的安全性。

下表提供了SwiftC部分指针类型和函数的大致等价关系。

SwiftC描述
UnsafeMutableRawPointervoid*空指针
UnsafeMutablePointerT*类型指针
UnsafeRawPointerconst void*常量空指针
UnsafePointerconst T*常量类型指针
UnsafeMutablePointer.allocate(int32_t*)malloc分配内存

可以看出Swift的指针并不神秘,它只是映射了C语言指针的对应操作(只是乍看一下更复杂)。

进阶实践:C标准库函数的映射调用

Swift提供了大量的C标准库的桥接调用,也就是说,我们可以像调用C语言库函数一样调用Swift函数。这其中包括很多有用的函数,如memcpystrcpy等。

下面通过一段示例程序来展现这类函数的调用。

var n = 10086
// malloc
let p = malloc(MemoryLayout<Int32>.size)!
// memcpy
memcpy(p, &n, MemoryLayout<Int32>.size)
let p2 = p.assumingMemoryBound(to: Int32.self)
print("\(p2.pointee)") // 打印:10086
// strcpy
let str = "abc".cString(using: .ascii)!
if str.count != MemoryLayout<Int32>.size {
return
}
let pstr = p.assumingMemoryBound(to: CChar.self)
strcpy(pstr, str)
print("\(String(cString: pstr))") // 打印:abc
// strlen
print("\(strlen(pstr))") // 打印: 3
// memset
memset(p, 0, MemoryLayout<Int32>.size)
print("\(p2.pointee)") // 打印:0
// strcat
strcat(pstr, "h".cString(using: .ascii)!)
strcat(pstr, "i".cString(using: .ascii)!)
print("\(String(cString: pstr))") // 打印:hi
// strstr
let s = strstr(pstr, "i")!
print("\(String(cString: s))") // 打印:i
// strcmp
print("\(strcmp(pstr, "hi".cString(using: .ascii)!))") // 打印:0
// free
free(p)

以上demo提供了如memsetstrcpyC库函数原型的调用方式。通过该例子可以看出指针操作的灵活性,对于开辟的一块4个字节的内存,我们既可以把它看做一个32位整形,又可以把它看做4个ascii字符,当把它看做4个字符时,我们可以用它存放abc三个字符,并在最后一个字节用\0作为终止符。

总结

指针可以让我们用更底层的视角来看待程序和数据,在某些场景下,通过指针我们有机会开发出更高性能的代码。但同时指针的使用有时也是极复杂易出错的。如何使用好这把双刃剑,全看开发者自身的能力和态度。本文仅仅是抛砖引玉的提供了Swift指针的基本框架和使用技巧,大量细节因为篇幅原因并未提及,还需要读者自行不断研究和学习。

本文的样例代码已上传至我的github,请参见地址:github.com/FengHaiTong… 。


作者:风海铜锣
链接:https://juejin.cn/post/7030789069915291661
来源:稀土掘金

收起阅读 »

Swift热更新(1)- 免费版接入

iOS
SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言...
继续阅读 »

SOT学习和使用的成本主要集中在前期,主要涉及编译流程的修改。之前介绍了纯OC项目如何接入「 OC接入例子 」。本文介绍如何给纯Swift项目接入SOT,包括免费版和网站版。

本文以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotdemo 」,Debug模式下接入了免费版,Release模式接入了网站版,读者也可以直接用该分支测试。

现在开始从头讲解,clone原本的工程后,命令行cd进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...

我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):

......

点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...

Step1: 配置编译环境

参考「 免费版 」的step1到step3,step3拷贝的sotconfig.sh放到项目的Demo的目录下:...

用文本编辑器打开sotconfig.sh,修改EnableSot=1:...

Step2: 修改编译选项

添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:

  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...

  2. Other Linker Flags添加-sotmodule $(PRODUCT_NAME) /Users/sotsdk-1.0/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotconfig.sh,每个选项的意义如下:

    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字;
    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁;
    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作,用$(SRCROOT)引用到
    • /Users/sotsdk-1.0/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机
  3. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...

  4. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Target的Enable Bitcode设为No...


Step3: 增加拷贝补丁脚本

SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:

...

脚本内容为:sh /Users/sotsdk-1.0/project-script/sot_package.sh "$SOURCE_ROOT/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo...

把Based on dependency analysis的勾去掉


Step4: 链接C++库

SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...

点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step5: 调用SDK API

需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,可以直接添加到Demo工程中。点击Xcode软件的File按钮,接着点击Add Files to "Demo",如下图所示:...

选择到SDK目录swift-call-objc中,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的Demo target,如下图所示:...

点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header:...

然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...

打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...

打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


测试热更

Step1: 热更注入

按上面配置完之后,确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...

然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......

项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。


Step2: 生成补丁

上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:

  1. 首先启动SOT生成补丁模式,修改sotconfig.sh为EnableSot=1GenerateSotShip=1
  2. ...
  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“:......
  4. 生成补丁跟OC项目不一样,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,然后再展开Link Demo(x86_64)的编译日志:...可以看到此时的Link是用来生成补丁的,日志里也显示了函数demoBasics被修改了:...
  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/Debug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。

Step3: 加载补丁

启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...

如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。

顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。

如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现怎么改代码都不会生效了。


作者:忒修斯科技
链接:https://juejin.cn/post/7026197659006287903
来源:稀土掘金

收起阅读 »

Swift开发规范

iOS
Swift开发规范前言开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。命名规约代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的...
继续阅读 »

Swift开发规范

前言

开发规范的目的是保证统一项目成员的编码风格,并使代码美观,每个公司对于代码的规范也不尽相同,希望该份规范能给大家起到借鉴作用。本文为原创,如需转载请说明原文地址链接。

命名规约

  • 代码中的命名严禁使用拼音及英文混合的方式,更不允许直接出现中文的方式,最好也不要使用下划线或者美元符号开头;
  • 文件名、class、struct、enum、protocol 命名统一使用 UpperCamelCase 风格;
  • 方法名、参数名、成员变量、局部变量、枚举成员统一使用 lowerCamelCase 风格
  • 全局常量命名使用 k 前缀 + UpperCamelCase 命名;
  • 扩展文件,用“原始类型名+扩展名”作为扩展文件名,其中原始类型名及扩展名也使用 UpperCamelCase 风格,如UIView+Frame.swift
  • 工程中文件夹或者 Group 统一使用 UpperCamelCase 风格,一律使用单数形式;
  • 命名中出现缩略词时,缩略词要么全部大写,要么全部小写,以首字母大小写为准,通用缩略词包括 JSON、URL 等;如class IDUtil {}func idToString() { }
  • 不要使用不规范的缩写,如 AbstractClass“缩写”命名成 AbsClass 等,不怕名称长,就怕名称不明确。
  • 文件名如果有复数含义,文件名应使用复数形式,如一些工具类;

修饰规约

  • 能用 let 修饰的时候,不要使用 var;
  • 修饰符顺序按照 注解、访问限制、static、final 顺序;
  • 尽可能利用访问限制修饰符控制类、方法等的访问限制;
  • 写方法时,要考虑这个方法是否会被重载。如果不会,标记为 final,final 会缩短编译时间;
  • 在编写库的时候需要注意修饰符的选用,遵循开闭原则;

格式规约

  • 类、函数左大括号不另起一行,与名称之间留有空格
  • 禁止使用无用分号
  • 代码中的空格出现地点
    • 注释符号与注释内容之间有空格
    • 类继承, 参数名和类型之间等, 冒号前面不加空格, 但后面跟空格
    • 任何运算符前后有空格
    • 表示返回值的 -> 两边
    • 参数列表、数组、tuple、字典里的逗号后面有一个空格
  • 方法之间空一行
  • 重载的声明放在一起,按照参数的多少从少到多向下排列
  • 每一行只声明一个变量
  • 如果是一个很长的数字时,建议使用下划线按照语言习惯三位或者四位一组分割连接。
  • 表示单例的静态属性,一般命名为 shared 或者 default
  • 如果是空的 block,直接声明{ },括号之间不需换行
  • 解包时推荐使用原有名字,前提是解包后的名字与解包前的名字在作用域上不会形成冲突
  • if 后面的 else\else if, 跟着上一个 if\else if 的右括号
  • switch 中, case 跟 switch 左对齐
  • 每行代码长度应小于 100 个字符,或者阅读时候不应该需要滚动屏幕,在正常范围内可以看到完整代码
  • 实现每个协议时, 在单独的 extension 里来实现

简略规约

  • Swift 会被结构体按照自身的成员自动生成一个非 public 的初始化方法,如果这个初始化方法刚好适合,不要自己再声明
  • 类及结构体初始化方法不要直接调用.init,直接直接省略,使用()
  • 如果只有一个 get 的计算属性,忽略 get
  • 数据定义时,尽量使用字面量形式进行自动推断,如果上下文不足以推断字面量类型时,需要声明赋值类型
  • 省略默认的访问权限(internal)
  • 过滤, 转换等, 优先使用 filter, map 等高阶函数简化代码,并尽量使用最简写
  • 使用闭包时,尽量使用最简写
  • 使用枚举属性时尽量使用自动推断,进行缩写
  • 无用的代码及时删除
  • 尽量使用各种语法糖
  • 访问实例成员或方法时尽量不要使用 self.,特殊场景除外,如构造函数时
  • 当方法无返回值时,不需添加 void

注释规约

  • 文档注释使用单行注释,即///,不使用多行注释,即/***/。 多行注释用于对某一代码段或者设计进行描述
  • 对于公开的类、方法以及属性等必须加上文档注释,方法需要加上对应的Parameter(s)ReturnsThrows 标签,强烈建议使用⌥ ⌘ /自动生成文档模板
  • 在代码中灵活的使用一些地标注释,如MARKFIXMETODO,当同一文件中存在多种类型定义或者多种逻辑时,可以使用Mark进行分组注释
  • 尽量将注释另起一行,而不是放在代码后

其他

  • 不要使用魔法值(即未经定义的常量);
  • 函数参数最多不得超过 8 个;寄存器数目问题,超过 8 个会影响效率;
  • 图形化的字面量,#colorLiteral(...)#imageLiteral(...)只能用在 playground 当做自我练习使用,禁止在项目工程中使用
  • 避免强制解包以及强制类型映射,尽量使用if let 或 guard let进行解包,禁止try!形式处理异常,避免使用隐式解包
  • 避免判断语句嵌套层次太深,使用 guard 提前返回
  • 如果 for 循环在函数体中只有一个 if 判断,使用 for where 进行替换
  • 实现每个协议时, 尽量在单独的 extension 里来实现;但需要考虑到协议的方法是否有 override 的可能,定义在 extension 的方法无法被 override,除非加上@objc 方法修改其派发方式
  • 优先创建函数而不是自定义操作符
  • 尽可能少的使用全局命名空间,如常量、变量、方法等
  • 赋值数组、字典时每个元素分别占用一行时,最后一个选项后面也添加逗号;这样未来如果有元素加入会更加方便
  • 布尔类型属性使用 is 作为属性名前缀,返回值为布尔型类型的方法名使用 is 作为方法名作为前缀
  • 类似注解的修饰词单独占一行,如@objc,@discardableResult 等
  • extension 上不用加任何修饰符,修饰符加在 extension 内的变量或方法上
  • 使用 guard 来提前结束条件,避免形成判断嵌套;
  • 善用字典去减少判断,可将条件与结果分别当做 key 及 value 存入字典中;
  • 封装时善用 assert,方便问题排查;
  • 在闭包中使用 self 时使用捕获列表[weak self]避免循环引用,闭包开始判断 self 的有效性
  • 使用委托和协议时,避免循环引用,定义属性的时候使用 weak 修饰

工具

SwiftLint 工具 提示格式错误

SwiftFormat 工具 提示并修复格式错误

两者大部分格式规范都是一致的,少许规范不一致,两个工具之间使用不冲突,可以在项目中共存。我们通过配置文件可以控制启用或者关闭相应的规则,具体使用规则参照对应仓库的 REAMME.md 文件。

相关规范

Swift 官方 API 设计指南

google 发布的 Swift 编码规范


有一个技术的圈子与一群同道众人非常重要,来我的技术公众号及博客,这里只聊技术干货。


链接:https://juejin.cn/post/6976282985695969294
收起阅读 »

? 我的独立开发的故事

iOS
🐻 我的独立开发的故事我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。我做过直播、相机、社交类APP。个人独立app 《imi》《今日计划》2020年,我...
继续阅读 »

🐻 我的独立开发的故事

我是独立开发者熊大,最近一年尝试了独立开发的滋味,也想和大家聊一聊独立开发的心历路程。 如果你也有开发一款app的想法,那你可以看一看我的独立开发的故事。

  • 我做过直播、相机、社交类APP。
  • 个人独立app 《imi》《今日计划》
  • 2020年,我想要尝试一下独立开发的方向。

第一款app的开发周期

做第一款软件《今日计划》时,周一到周六工作,大小周,晚上会有一些开发时间。

总体如下:

  • 每天1小时写app代码 * 60 = 60小时
  • 每周周日有4个小时 * 8 = 32小时
  • 清明节三天 (按照8小时/天tian计算):3*8 = 24小时

一共约120个小时:完成了设计到上线。

我也买了阿里云的ECS,用vapor搭建了后台,维护成本有点高,果断放弃了。

当我开心的把它分享给朋友时,朋友们都说他很丑,于是被贴上一系列标签『丑』、『直男审美』、『搭配有问题』、『太简单了吧』····,总而言之,没什么好的形容词。

(PS:T M D 我自己都感觉有点坑)

 报着期望,又紧急改版一次,更换了icon,改了一些设计。也就是现在的这一版。我在圈子里又推广了一波,登顶效率榜Top20(其实是各位兄弟给面子)。

后来陆陆续续也有一些下载,但由于工作紧张,没能持续更新迭代。

离职风波

《水印相机》这款app目前,摄影榜Top20,很荣幸是我从零带到百万日活的,深知好产品的指数爆发增长。我内心真的想去外边看看,想见识更多优秀的、有趣的人,于是世界那么大,我想出去看看,真的成为了我离职的最主要理由。

从上家公司收获的最大的便是经验,一份让我受用很多年的经验。

离职后,并不缺少内推的机会,但我还没想好该怎么走接下来的路,我在思考,是去大厂深造,还是开启自由职业呢?自己一直是个骄傲的人,毕业时我的薪资就是 xx k,不能为五斗米而折腰,干脆做个自由职业好了。于是把想法讲给周围的人,最后还是找了份工作,公司就在我家的旁边,上下班5分钟。

于是从7月份开始,我就几乎每天晚上有两个小时的时间为开启我的自由职业之路做准备,只要副业收入过万,就开始全职独立开发。

新app上线

2020.08 一个小伙伴,会飞的猪,加入了开发阵营。

2020.10 小满 加了开发阵营。

(由于特殊原因,名字保密)

2021年1月上线了新的免费App《imi-成就最好的自己》,这次的app,至少在UI上取得了程序员的好评,我们还没有正式推广,只是在小圈子里发了一下动态试试水。

我们小团队也开了个新的公·众·号:《独立开发者基地》,感兴趣额可以关注。

惭愧的是,由于新公司较忙,进行了几次通宵加班后,我严重的拖累了小团队的开发进度,本来应该是2020年底就应该上线的。

《imi》

这是一款风格可爱简单的规划、计划类软件,致敬自己,致敬青春。

imi寓意:我就是我,我们一定是不完美的,也许不成功,也许不漂亮,但这就是我,与众不同。

给张图看看:

这个idea是我想的,简单说就是一个计划类软件,里边有

  • 人生节点
  • 座右铭
  • 成就
  • 笔记
  • 喜欢的人
  • 倒计时
  • 指纹解锁
  • 云同步。

设计这款软件希望能让大家觉得有用,不知道软件的初衷是不是个伪命题。让时间见证吧。

独立开发者应该都知道霸榜很久的《时间规划局》,这次《imi》就是冲着它去的,她将作为我们的竞品之一,我想我们这么有情怀的app对标这样的工具类软件,是有点希望的(怕怕)。

希望大家下载: imi-成就 给予我们支持 ^_^

给独立开发者的福利

这个应该算是福利吧,我们小团队,整理出了app的加速库,《今日计划》《imi-成就》两款app都是基于这个加速库开发的。接下来的其他app也会基于这个加速库开发,意味着我们会持续完善、维护这个加速库。里边有很多实用的功能,欢迎star🌟。

加速库SpeedySwift仓库:https://github.com/Tliens/SpeedySwift

imi 中用到的第三方库:

  # Pods for App1125
pod 'HWPanModal', '~> 0.8.1'
pod 'RealmSwift', '~> 10.5.0'
pod 'ZLPhotoBrowser', '~> 4.1.2'
pod 'SwiftDate', '~> 6.3.1'
pod 'IceCream',:path =>'Dev-pods/IceCream' # 数据同步icloud
# pod 'FSPagerView' # 轮播图
# pod 'SwiftyStoreKit' # 内购组件
pod 'Schedule', '~> 2.1.0'
pod 'Hero', '~> 1.5.0'
pod 'BiometricAuthentication'
#依赖库
pod 'UMCCommon', '~> 2.1.4'
#统计 SDK
pod 'UMCAnalytics', '~> 6.1.0'


回顾2020

get的技能:

  • 有幸能主导组件化开发
  • 函数响应式编程
  • go服务端

展望2021

希望大家健康、开心

我们会继续维护,维护今日计划、imi。也会有新的app出现。

最后

天行健君子以自强不息,地势坤君子以厚德载物。

虽大部分努力都没有收获,但热爱诞生创造的婴孩。

与君共勉!!!

写于 2021.01.13 北京·安贞门
链接:https://juejin.cn/post/6917058456184684557
收起阅读 »

python协程(超详细)

1、迭代1.1 迭代的概念使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程# Python 中的迭代for value in [2, 3, 4]:    print(value)1.2 可迭代对象标准概念:在类...
继续阅读 »



1、迭代

1.1 迭代的概念

使用for循环遍历取值的过程叫做迭代,比如:使用for循环遍历列表获取值的过程

# Python 中的迭代
for value in [2, 3, 4]:
   print(value)

1.2 可迭代对象

标准概念:在类里面定义__iter__方法,并使用该类创建的对象就是可迭代对象

简单记忆:使用for循环遍历取值的对象叫做可迭代对象, 比如:列表、元组、字典、集合、range、字符串

1.3 判断对象是否是可迭代对象

# 元组,列表,字典,字符串,集合,range都是可迭代对象
from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 判断对象是否是指定类型
result = isinstance((3, 5), Iterable)
print("元组是否是可迭代对象:", result)

result = isinstance([3, 5], Iterable)
print("列表是否是可迭代对象:", result)

result = isinstance({"name": "张三"}, Iterable)
print("字典是否是可迭代对象:", result)

result = isinstance("hello", Iterable)
print("字符串是否是可迭代对象:", result)

result = isinstance({3, 5}, Iterable)
print("集合是否是可迭代对象:", result)

result = isinstance(range(5), Iterable)
print("range是否是可迭代对象:", result)

result = isinstance(5, Iterable)
print("整数是否是可迭代对象:", result)

# 提示: 以后还根据对象判断是否是其它类型,比如以后可以判断函数里面的参数是否是自己想要的类型
result = isinstance(5, int)
print("整数是否是int类型对象:", result)

class Student(object):
   pass

stu = Student()
result = isinstance(stu, Iterable)

print("stu是否是可迭代对象:", result)

result = isinstance(stu, Student)

print("stu是否是Student类型的对象:", result)

1.4 自定义可迭代对象

在类中实现__iter__方法

自定义可迭代类型代码

from collections import Iterable
# 如果解释器提示警告,就是用下面的导入方式
# from collections.abc import Iterable

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       pass

my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
   print(value)

执行结果:

Traceback (most recent call last):
True
 File "/Users/hbin/Desktop/untitled/aa.py", line 24, in <module>
   for value in my_list:
TypeError: iter() returned non-iterator of type 'NoneType'

通过执行结果可以看出来,遍历可迭代对象依次获取数据需要迭代器

总结

在类里面提供一个__iter__创建的对象是可迭代对象,可迭代对象是需要迭代器完成数据迭代的

2、迭代器

2.1 自定义迭代器对象

自定义迭代器对象: 在类里面定义__iter____next__方法创建的对象就是迭代器对象

from collections import Iterable
from collections import Iterator

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       my_iterator = MyIterator(self.my_list)
       return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
class MyIterator(object):

   def __init__(self, my_list):
       self.my_list = my_list

       # 记录当前获取数据的下标
       self.current_index = 0

       # 判断当前对象是否是迭代器
       result = isinstance(self, Iterator)
       print("MyIterator创建的对象是否是迭代器:", result)

   def __iter__(self):
       return self

   # 获取迭代器中下一个值
   def __next__(self):
       if self.current_index < len(self.my_list):
           self.current_index += 1
           return self.my_list[self.current_index - 1]
       else:
           # 数据取完了,需要抛出一个停止迭代的异常
           raise StopIteration


my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)
result = isinstance(my_list, Iterable)

print(result)

for value in my_list:
   print(value)

运行结果:

True
MyIterator创建的对象是否是迭代器: True
1
2

2.2 iter()函数与next()函数

  1. iter函数: 获取可迭代对象的迭代器,会调用可迭代对象身上的__iter__方法

  2. next函数: 获取迭代器中下一个值,会调用迭代器对象身上的__next__方法

# 自定义可迭代对象: 在类里面定义__iter__方法创建的对象就是可迭代对象
class MyList(object):

   def __init__(self):
       self.my_list = list()

   # 添加指定元素
   def append_item(self, item):
       self.my_list.append(item)

   def __iter__(self):
       # 可迭代对象的本质:遍历可迭代对象的时候其实获取的是可迭代对象的迭代器, 然后通过迭代器获取对象中的数据
       my_iterator = MyIterator(self.my_list)
       return my_iterator


# 自定义迭代器对象: 在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
# 迭代器是记录当前数据的位置以便获取下一个位置的值
class MyIterator(object):

   def __init__(self, my_list):
       self.my_list = my_list

       # 记录当前获取数据的下标
       self.current_index = 0

   def __iter__(self):
       return self

   # 获取迭代器中下一个值
   def __next__(self):
       if self.current_index < len(self.my_list):
           self.current_index += 1
           return self.my_list[self.current_index - 1]
       else:
           # 数据取完了,需要抛出一个停止迭代的异常
           raise StopIteration

# 创建了一个自定义的可迭代对象
my_list = MyList()
my_list.append_item(1)
my_list.append_item(2)

# 获取可迭代对象的迭代器
my_iterator = iter(my_list)
print(my_iterator)
# 获取迭代器中下一个值
# value = next(my_iterator)
# print(value)

# 循环通过迭代器获取数据
while True:
   try:
       value = next(my_iterator)
       print(value)
   except StopIteration as e:
       break

2.3 for循环的本质

遍历的是可迭代对象

  • for item in Iterable 循环的本质就是先通过iter()函数获取可迭代对象Iterable的迭代器,然后对获取到的迭代器不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

遍历的是迭代器

  • for item in Iterator 循环的迭代器,不断调用next()方法来获取下一个值并将其赋值给item,当遇到StopIteration的异常后循环结束。

2.4 迭代器的应用场景

我们发现迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。

举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

现在我们想要通过for...in...循环来遍历迭代斐波那契数列中的前n个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

class Fibonacci(object):

   def __init__(self, num):
       # num:表示生成多少fibonacci数字
       self.num = num
       # 记录fibonacci前两个值
       self.a = 0
       self.b = 1
       # 记录当前生成数字的索引
       self.current_index = 0

   def __iter__(self):
       return self

   def __next__(self):
       if self.current_index < self.num:
           result = self.a
           self.a, self.b = self.b, self.a + self.b
           self.current_index += 1
           return result
       else:
           raise StopIteration


fib = Fibonacci(5)
# value = next(fib)
# print(value)

for value in fib:
   print(value)

执行结果:

0
1
1
2
3

小结

迭代器的作用就是是记录当前数据的位置以便获取下一个位置的值

3、生成器

3.1 生成器的概念

生成器是一类特殊的迭代器,它不需要再像上面的类一样写__iter__()和__next__()方法了, 使用更加方便,它依然可以使用next函数和for循环取值

3.2 创建生成器方法1

  • 第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( )

my_list = [i * 2 for i in range(5)]
print(my_list)

# 创建生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)

# next获取生成器下一个值
# value = next(my_generator)
#
# print(value)
for value in my_generator:
   print(value)

执行结果:

[0, 2, 4, 6, 8]
<generator object <genexpr> at 0x101367048>
0
2
4
6
8

3.3 创建生成器方法2

在def函数里面看到有yield关键字那么就是生成器

def fibonacci(num):
   a = 0
   b = 1
   # 记录生成fibonacci数字的下标
   current_index = 0
   print("--11---")
   while current_index < num:
       result = a
       a, b = b, a + b
       current_index += 1
       print("--22---")
       # 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
       yield result
       print("--33---")


fib = fibonacci(5)
value = next(fib)
print(value)
value = next(fib)
print(value)

value = next(fib)
print(value)

# for value in fib:
#     print(value)

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。

简单来说:只要在def中有yield关键字的 就称为 生成器

3.4 生成器使用return关键字

def fibonacci(num):
a = 0
b = 1
# 记录生成fibonacci数字的下标
current_index = 0
print("--11---")
while current_index < num:
result = a
a, b = b, a + b
current_index += 1
print("--22---")
# 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
yield result
print("--33---")
return "嘻嘻"

fib = fibonacci(5)
value = next(fib)
print(value)
# 提示: 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

# return 和 yield的区别
# yield: 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值
# return: 只能返回一次值,代码执行到return语句就停止迭代

try:
value = next(fib)
print(value)
except StopIteration as e:
# 获取return的返回值
print(e.value)

提示:

  • 生成器里面使用return关键字语法上没有问题,但是代码执行到return语句会停止迭代,抛出停止迭代异常

3.5 yield和return的对比

  • 使用了yield关键字的函数不再是函数,而是生成器。(使用了yield的函数就是生成器)

  • 代码执行到yield会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行

  • 每次启动生成器都会返回一个值,多次启动可以返回多个值,也就是yield可以返回多个值

  • return只能返回一次值,代码执行到return语句就停止迭代,抛出停止迭代异常

3.6 使用send方法启动生成器并传参

send方法启动生成器的时候可以传参数

def gen():
   i = 0
   while i<5:
       temp = yield i
       print(temp)
       i+=1

执行结果:

In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:

**注意:如果第一次启动生成器使用send方法,那么参数只能传入None,一般第一次启动生成器使用next函数

小结

  • 生成器创建有两种方式,一般都使用yield关键字方法创建生成器

  • yield特点是代码执行到yield会暂停,把结果返回出去,再次启动生成器在暂停的位置继续往下执行

4、协程

4.1 协程的概念

协程,又称微线程,纤程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行 通俗理解只要在def里面只看到一个yield关键字表示就是协程

协程是也是实现多任务的一种方式

协程yield的代码实现

简单实现协程

import time

def work1():
   while True:
       print("----work1---")
       yield
       time.sleep(0.5)

def work2():
   while True:
       print("----work2---")
       yield
       time.sleep(0.5)

def main():
   w1 = work1()
   w2 = work2()
   while True:
       next(w1)
       next(w2)

if __name__ == "__main__":
   main()

运行结果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

小结

协程之间执行任务按照一定顺序交替执行

5、greenlet

5.1 greentlet的介绍

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

使用如下命令安装greenlet模块:

pip3 install greenlet

使用协程完成多任务

import time
import greenlet


# 任务1
def work1():
for i in range(5):
print("work1...")
time.sleep(0.2)
# 切换到协程2里面执行对应的任务
g2.switch()


# 任务2
def work2():
for i in range(5):
print("work2...")
time.sleep(0.2)
# 切换到第一个协程执行对应的任务
g1.switch()


if __name__ == '__main__':
# 创建协程指定对应的任务
g1 = greenlet.greenlet(work1)
g2 = greenlet.greenlet(work2)

# 切换到第一个协程执行对应的任务
g1.switch()

运行效果

work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...
work1...
work2...

6、gevent

6.1 gevent的介绍

greenlet已经实现了协程,但是这个还要人工切换,这里介绍一个比greenlet更强大而且能够自动切换任务的第三方库,那就是gevent。

gevent内部封装的greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

安装

pip3 install gevent

6.2 gevent的使用

import gevent

def work(n):
   for i in range(n):
       # 获取当前协程
       print(gevent.getcurrent(), i)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 0
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 0
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 0
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 1
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 1
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 1
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 2
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 2
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 2
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 3
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 3
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 3
<Greenlet "Greenlet-0" at 0x26d8c970488: work(5)> 4
<Greenlet "Greenlet-1" at 0x26d8c970598: work(5)> 4
<Greenlet "Greenlet-2" at 0x26d8c9706a8: work(5)> 4

可以看到,3个greenlet是依次运行而不是交替运行

6.3 gevent切换执行

import gevent

def work(n):
   for i in range(n):
       # 获取当前协程
       print(gevent.getcurrent(), i)
       #用来模拟一个耗时操作,注意不是time模块中的sleep
       gevent.sleep(1)

g1 = gevent.spawn(work, 5)
g2 = gevent.spawn(work, 5)
g3 = gevent.spawn(work, 5)
g1.join()
g2.join()
g3.join()

运行结果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

6.4 给程序打补丁

import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
   for i in range(num):
       print("work1....")
       time.sleep(0.2)
       # gevent.sleep(0.2)

# 任务1
def work2(num):
   for i in range(num):
       print("work2....")
       time.sleep(0.2)
       # gevent.sleep(0.2)



if __name__ == '__main__':
   # 创建协程指定对应的任务
   g1 = gevent.spawn(work1, 3)
   g2 = gevent.spawn(work2, 3)

   # 主线程等待协程执行完成以后程序再退出
   g1.join()
   g2.join()

运行结果

work1....
work2....
work1....
work2....
work1....
work2....

6.5 注意

  • 当前程序是一个死循环并且还能有耗时操作,就不需要加上join方法了,因为程序需要一直运行不会退出

示例代码

import gevent
import time
from gevent import monkey

# 打补丁,让gevent框架识别耗时操作,比如:time.sleep,网络请求延时
monkey.patch_all()


# 任务1
def work1(num):
   for i in range(num):
       print("work1....")
       time.sleep(0.2)
       # gevent.sleep(0.2)

# 任务1
def work2(num):
   for i in range(num):
       print("work2....")
       time.sleep(0.2)
       # gevent.sleep(0.2)



if __name__ == '__main__':
   # 创建协程指定对应的任务
   g1 = gevent.spawn(work1, 3)
   g2 = gevent.spawn(work2, 3)

   while True:
       print("主线程中执行")
       time.sleep(0.5)

执行结果:

主线程中执行work1....work2....work1....work2....work1....work2....主线程中执行主线程中执行主线程中执行..省略..
  • 如果使用的协程过多,如果想启动它们就需要一个一个的去使用join()方法去阻塞主线程,这样代码会过于冗余,可以使用gevent.joinall()方法启动需要使用的协程

    实例代码

 import time
import gevent

def work1():
   for i in range(5):
       print("work1工作了{}".format(i))
       gevent.sleep(1)

def work2():
   for i in range(5):
       print("work2工作了{}".format(i))
       gevent.sleep(1)


if __name__ == '__main__':
   w1 = gevent.spawn(work1)
   w2 = gevent.spawn(work2)
   gevent.joinall([w1, w2])  # 参数可以为list,set或者tuple

7、进程、线程、协程对比

7.1 进程、线程、协程之间的关系

  • 一个进程至少有一个线程,进程里面可以有多个线程

  • 一个线程里面可以有多个协程

关系图.png

7.2 进程、线程、线程的对比

  1. 进程是资源分配的单位

  2. 线程是操作系统调度的单位

  3. 进程切换需要的资源最大,效率很低

  4. 线程切换需要的资源一般,效率一般(当然了在不考虑GIL的情况下)

  5. 协程切换任务资源很小,效率高

  6. 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中 所以是并发

小结

  • 进程、线程、协程都是可以完成多任务的,可以根据自己实际开发的需要选择使用

  • 由于线程、协程需要的资源很少,所以使用线程和协程的几率最大

  • 开辟协程需要的资源最少

作者:y大壮
来源:https://juejin.cn/post/6971037591952949256

收起阅读 »

android媲美微信扫码库

之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美github:github.com/DyncKathlin…强烈推荐MIKit Barcode Scanning识别速度超快,基本上camer...
继续阅读 »



之前使用的是zxing封装的库,但是识别率和识别速度没法和微信比较,现在使用的Google开源识别库完全可以和微信媲美

github:github.com/DyncKathlin…

强烈推荐MIKit Barcode Scanning

识别速度超快,基本上camera抓取到二维码就能识别到其内容(这是重点)。
基于MIKit Barcode Scanning的识别库进行封装,操作简单。
支持识别多个二维码,条形码。
支持任意比例展示,可以1:2,1.5:2等,不会发生像拉伸变形。
使用camera,不是cameraX哦。

效果图


第一个是Google开源的,第二个是zxing开源的

使用方式

build.gradle引用

implementation 'com.github.dynckathline:barcode:2.5'

初始化和监听结果回调

        //构造出扫描管理器
      configViewFinderView(viewfinderView);
      mlKit = new MLKit(this, preview, graphicOverlay);
      //是否扫描成功后播放提示音和震动
      mlKit.setPlayBeepAndVibrate(true, true);
      //仅识别二维码
      BarcodeScannerOptions options =
              new BarcodeScannerOptions.Builder()
                      .setBarcodeFormats(
                              Barcode.FORMAT_QR_CODE,
                              Barcode.FORMAT_AZTEC)
                      .build();
      mlKit.setBarcodeFormats(null);
      mlKit.setOnScanListener(new MLKit.OnScanListener() {
          @Override
          public void onSuccess(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
              showScanResult(barcodes, graphicOverlay, image);
          }

          @Override
          public void onFail(int code, Exception e) {

          }
      });

展示结果

private void showScanResult(List<Barcode> barcodes, @NonNull GraphicOverlay graphicOverlay, InputImage image) {
      if (barcodes.isEmpty()) {
          return;
      }

      mlKit.setAnalyze(false);
      CustomDialog.Builder builder = new CustomDialog.Builder(context);
      CustomDialog dialog = builder
              .setContentView(R.layout.barcode_result_dialog)
              .setLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
              .setOnInitListener(new CustomDialog.Builder.OnInitListener() {
                  @Override
                  public void init(CustomDialog customDialog) {
                      Button btnDialogCancel = customDialog.findViewById(R.id.btnDialogCancel);
                      Button btnDialogOK = customDialog.findViewById(R.id.btnDialogOK);
                      TextView tvDialogContent = customDialog.findViewById(R.id.tvDialogContent);
                      ImageView ivDialogContent = customDialog.findViewById(R.id.ivDialogContent);

                      Bitmap bitmap = null;
                      ByteBuffer byteBuffer = image.getByteBuffer();
                      if (byteBuffer != null) {
                          FrameMetadata.Builder builder = new FrameMetadata.Builder();
                          builder.setWidth(image.getWidth())
                                  .setHeight(image.getHeight())
                                  .setRotation(image.getRotationDegrees());
                          bitmap = BitmapUtils.getBitmap(byteBuffer, builder.build());
                      } else {
                          bitmap = image.getBitmapInternal();
                      }
                      if (bitmap != null) {
                          graphicOverlay.add(new CameraImageGraphic(graphicOverlay, bitmap));
                      } else {
                          ivDialogContent.setVisibility(View.GONE);
                      }
                      SpanUtils spanUtils = SpanUtils.with(tvDialogContent);
                      for (int i = 0; i < barcodes.size(); ++i) {
                          Barcode barcode = barcodes.get(i);
                          BarcodeGraphic graphic = new BarcodeGraphic(graphicOverlay, barcode);
                          graphicOverlay.add(graphic);
                          Rect boundingBox = barcode.getBoundingBox();
                          spanUtils.append(String.format("(%d,%d)", boundingBox.left, boundingBox.top))
                                  .append(barcode.getRawValue())
                                  .setClickSpan(i % 2 == 0 ? getResources().getColor(R.color.colorPrimary) : getResources().getColor(R.color.colorAccent), false, new View.OnClickListener() {
                              @Override
                              public void onClick(View v) {
                                  Toast.makeText(getApplicationContext(), barcode.getRawValue(), Toast.LENGTH_SHORT).show();
                              }
                          })
                                  .setBackgroundColor(i % 2 == 0 ? getResources().getColor(R.color.colorAccent) : getResources().getColor(R.color.colorPrimary))
                                  .appendLine()
                                  .appendLine();
                      }
                      spanUtils.create();
                      Bitmap bitmapFromView = loadBitmapFromView(graphicOverlay);
                      ivDialogContent.setImageBitmap(bitmapFromView);

                      btnDialogCancel.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              finish();
                          }
                      });
                      btnDialogOK.setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View v) {
                              customDialog.dismiss();
                              mlKit.setAnalyze(true);
                          }
                      });
                  }
              })
              .build();
  }

作者:KathLine
来源:https://juejin.cn/post/6972476138203381790

收起阅读 »

Android:这是一个让你心动的日期&时间选择组件

预览引入添加 JitPack repositoryallprojects { repositories { ... maven { url "https://jitpack.io" } }}添加 Gradle依赖depe...
继续阅读 »



预览




imgimgimg

引入

添加 JitPack repository

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

添加 Gradle依赖

dependencies {
  ...
  implementation 'com.google.android.material:material:1.1.0' //为了防止不必要的依赖冲突,0.0.3开始需要自行依赖google material库
  implementation 'com.github.loperSeven:DateTimePicker:0.3.0'//此处不保证最新版本,最新版需前往文末github查看
}

开始使用

内置弹窗CardDatePickerDialog

最简单的使用方式

//kotlin
    CardDatePickerDialog.builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose {millisecond->
                 
              }.build().show()
//java
new CardDatePickerDialog.Builder(this)
              .setTitle("SET MAX DATE")
              .setOnChoose("确定", aLong -> {
                   //aLong = millisecond
                   return null;
              }).build().show();

所有可配置属性

  CardDatePickerDialog.builder(context)
              .setTitle("CARD DATE PICKER DIALOG")
              .setDisplayType(displayList)
              .setBackGroundModel(model)
              .showBackNow(true)
              .setPickerLayout(layout)
              .setDefaultTime(defaultDate)
              .setMaxTime(maxDate)
              .setMinTime(minDate)
              .setWrapSelectorWheel(false)
              .setThemeColor(color)
              .showDateLabel(true)
              .showFocusDateInfo(true)
              .setLabelText("年","月","日","时","分")
              .setOnChoose("选择"){millisecond->}
              .setOnCancel("关闭") {}
              .build().show()

可配置属性说明

  • 设置标题

fun setTitle(value: String)
  • 是否显示回到当前按钮

fun showBackNow(b: Boolean)
  • 是否显示选中日期信息

fun showFocusDateInfo(b: Boolean)
  • 设置自定义选择器

//自定义选择器Layout注意事详见 【定制 DateTimePicker】
fun setPickerLayout(@NotNull layoutResId: Int)
  • 显示模式

// model 分为:CardDatePickerDialog.CARD//卡片,CardDatePickerDialog.CUBE//方形,CardDatePickerDialog.STACK//顶部圆角
// model 允许直接传入drawable资源文件id作为弹窗的背景,如示例内custom
fun setBackGroundModel(model: Int)
  • 设置主题颜色

fun setThemeColor(@ColorInt themeColor: Int)
  • 设置显示值

fun setDisplayType(vararg types: Int)
fun setDisplayType(types: MutableList<Int>)
  • 设置默认时间

fun setDefaultTime(millisecond: Long)
  • 设置范围最小值

fun setMinTime(millisecond: Long)
  • 设置范围最大值

fun setMaxTime(millisecond: Long)
  • 是否显示单位标签

fun showDateLabel(b: Boolean)
  • 设置标签文字

/**
*示例
*setLabelText("年","月","日","时","分")
*setLabelText("年","月","日","时")
*setLabelText(month="月",hour="时")
*/
fun setLabelText(year:String=yearLabel,month:String=monthLabel,day:String=dayLabel,hour:String=hourLabel,min:String=minLabel)
  • 设置是否循环滚动

/**
*示例(默认为true)
*setWrapSelectorWheel(false)
*setWrapSelectorWheel(DateTimeConfig.YEAR,DateTimeConfig.MONTH,wrapSelector = false)
*setWrapSelectorWheel(arrayListOf(DateTimeConfig.YEAR,DateTimeConfig.MONTH),false)
*/
fun setWrapSelectorWheel()
  • 绑定选择监听

/**
*示例
*setOnChoose("确定")
*setOnChoose{millisecond->}
*setOnChoose("确定"){millisecond->}
*/
fun setOnChoose(text: String = "确定", listener: ((Long) -> Unit)? = null)
  • 绑定取消监听

/**
*示例
*setOnCancel("取消")
*setOnCancel{}
*setOnCancel("取消"){}
*/
fun setOnCancel(text: String = "取消", listener: (() -> Unit)? = null)

选择器 DateTimePicker

xml中

app:layout 为自定义选择器布局 可参考 定制 DateTimePicker

        <com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           app:showLabel="true"
           app:textSize="16sp"
           app:themeColor="#FF8080" />

代码中

  • 设置监听

    dateTimePicker.setOnDateTimeChangedListener { millisecond ->  }

更多设置

  • 设置自定义选择器布局(注意:需要在dateTimePicker其他方法之前调用,否则其他方法将会失效)

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId
  • 设置显示状态

DateTimePicker支持显示 年月日时分 五个选项的任意组合,显示顺序以此为年、月、日、时、分,setDisplayType中可无序设置。

     dateTimePicker.setDisplayType(intArrayOf(
           DateTimeConfig.YEAR,//显示年
           DateTimeConfig.MONTH,//显示月
           DateTimeConfig.DAY,//显示日
           DateTimeConfig.HOUR,//显示时
           DateTimeConfig.MIN))//显示分
  • 设置默认选中时间

 dateTimePicker.setDefaultMillisecond(defaultMillisecond)//defaultMillisecond 为毫秒时间戳
  • 设置允许选择的最小时间

  dateTimePicker.setMinMillisecond(minMillisecond)
  • 设置允许选择的最大时间

  dateTimePicker.setMaxMillisecond(maxMillisecond)
  • 是否显示label标签(选中栏 年月日时分汉字)

  dateTimePicker.showLabel(true)
  • 设置主题颜色

  dateTimePicker.setThemeColor(ContextCompat.getColor(context,R.color.colorPrimary))
  • 设置字体大小

设置的字体大小为选中栏的字体大小,预览字体会根据字体大小等比缩放

  dateTimePicker.setTextSize(15)//单位为sp
  • 设置标签文字

  //全部
 dateTimePicker.setLabelText(" Y"," M"," D"," Hr"," Min")
 //指定
 dateTimePicker.setLabelText(min = "M")

定制 DateTimePicker

说明

DateTimePicker 主要由至多6个 NumberPicker 组成,所以在自定义布局时,根据自己所需的样式摆放 NumberPicker 即可。以下为注意事项

开始定制

  • DateTimePicker 至多支持6个 NumberPicker ,你可以在xml中按需摆放1-6个 NumberPicker

  • 为了让 DateTimePicker 找到 NumberPicker ,需要在xml中为 NumberPicker 指定 idtag,规则如下

/**
* year:np_datetime_year
* month:np_datetime_month
* day:np_datetime_day
* hour:np_datetime_hour
* minute:np_datetime_minute
* second:np_datetime_second
*/
android:id="@+id/np_datetime_year"  or  android:tag="np_datetime_year"
  • 使用定制UI

CardDatePickerDialog 中使用

fun setPickerLayout(@NotNull layoutResId: Int)

DateTimePicker 中使用

<com.loper7.date_time_picker.DateTimePicker
           android:id="@+id/dateTimePicker"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:layout="@layout/layout_date_picker_segmentation"
           />

或者

 dateTimePicker.setLayout(R.layout.layout_date_picker_segmentation)//自定义layout resId

XML示例

示例图片

imgimg

更高的拓展性

如果以上自定义并不能满足你的需求,你还可以定制你自己的 DateTimePicker , 可参照 DateTimePicker.kt 定义你想要属性以及在代码内编写你的UI逻辑。选择器的各种逻辑约束抽离在 DateTimeController.kt ,你的 DateTimePicker 只需让 DateTimeController.kt 绑定 NumberPicker 即可。比如:

DateTimeController().bindPicker(YEAR, mYearSpinner)
          .bindPicker(MONTH, mMonthSpinner)
          .bindPicker(DAY, mDaySpinner).bindPicker(HOUR, mHourSpinner)
          .bindPicker(MIN, mMinuteSpinner).bindPicker(SECOND, mSecondSpinner).build()

作者:LOPER7
来源:https://juejin.cn/post/6917909994985750535

收起阅读 »

你可以永远相信debugger,但是不能永远相信console.log

总结放前面:console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的...
继续阅读 »

总结放前面:

console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的时候会获得最新的值导致展开和不展开的表现不一致。

不知道各位大佬有没有遇到过这样的情况,我在代码里面console.log()了一个数组,然后打开浏览器控制台,看着是空的就像这样[],结果我点展开它里面又有值了,但是在代码打印的位置使用length或者获取数组里面的值都是不行的,🤯 就像下面这样:

let arr = [];
const setFun = () => {
   return new Promise((reslove) => {
       let arr1 = [1, 2, 3];
       setTimeout(() => {
           reslove(arr1);
      }, 2000)
  });
}
const getFun = async () => {
   let result = await setFun();
   result.forEach((item) => {
       arr.push(item);
  })
}
getFun();
console.log(arr);


或者说我在某处代码console.log()一个对象,明明控制台打印对象的某一个key是1,但是我展开这个对象里面的key居然是2,我在代码里面获取的也是2,就像下面这样:
不知道各位大佬遇到这样的情况是怎么个想法,反正我第一次遇到的时候我还以为是我的谷歌浏览器出问题了,擦💦我甚至都想卸载重装一波。后来动了动🧠,觉得可能是代码执行顺序的原因,所以我就在代码里面打了断点看了一下,在执行console.log()的时候arr的确是一个空的对象,对arr数组的操作是在console.log()执行之后才进行的。

所以说这到底是为什么呐?
其实这个还是和js的引用数据类型还有console.log()的设计有关系。我们都知道引用数据类型大体上可以说是由两部分组成:指针和内容,指针保存的内容就是一个内存地址的指向,指针一般都是基本数据类型保存在栈内存,内容就包含着这个引用数据类型的实际值一般保存在堆内存。😍 而console.log呐打印的时候只是打印了这个引用数据类型的一个快照,快照中的指针和内容都是照相的时候的内容,在console.log()之后,修改了这个引用数据类型,或者说在这之前修改的操作在一个异步的内容里面,当我们去看打印的时候,这个引用数据类型的内容可能就被修改了,但是因为快照的原因我们看到的还是以前的值。
然后当我们展开的时候,浏览器会利用指针去内存重新读取内容,因为快找的指针是没有发生变化的,所以就看到了改变之后内存,这就是为什么我们展开和不展开看到的结果是不一样的原因了。当然造成这样的原因不一定都是因为我们代码在异步里面操作这个引用数据类型。
还有就是浏览器在进行I/O的时候异步会提升性能,所有这就是为什么有时候我们写的同步代码依然会出现不一致的情况,就像我第二个图一样。
下面就验证一下我上面的想法,当我把上面的代码修改一下,直接替换:

let arr = [];
const setFun = () => {
   return new Promise((reslove) => {
       let arr1 = [1, 2, 3];
       setTimeout(() => {
           reslove(arr1);
      }, 2000)
  });
}
const getFun = async () => {
   let result = await setFun();
   arr = result; // 修改部分
}
getFun();
console.log(arr);

那么我们看到的结果就和上面不一样了,这个展开的表现是和不展开是一样的。
相信各位大佬也知道是啥原因了,因为这次直接替换,修改的是指针的指向并没有修改之前引用数据饿类型的内存空间,所以当我们展开的时候快照中指针保存的地址还是空的,这样我们看到的和看之前的想法就对应上了。
注:该问题只存在于打印引用数据类型,基本数据类型不会出现。

作者:江湖不渡i
来源:https://juejin.cn/post/7032504319584780325

收起阅读 »

别被你的框架框住了

我短暂的职业生涯被 React 充斥着。还没毕业前我从 Vue 2.x 入手开始学习框架,在一个我当时觉得还行现在回看完全不行的状态进了公司。然后开启了跟 React 死磕的状态,从 class 组件到函数式组件,从 Redux 到 Recoil,从 Antd...
继续阅读 »

我短暂的职业生涯被 React 充斥着。

还没毕业前我从 Vue 2.x 入手开始学习框架,在一个我当时觉得还行现在回看完全不行的状态进了公司。然后开启了跟 React 死磕的状态,从 class 组件到函数式组件,从 Redux 到 Recoil,从 Antd 到 MUI...

不久前一个呆了2年多的项目成功结束,接下来要去一个新项目,新项目要用 Angular,于是我开始告别从毕业就开始用的 React,开始学习这个大家少有提及的框架。

回顾这几年,要说 React 带给我最多的是什么,我觉得可能是思想,是一种编程范式。为了理解 React 新的函数式组件,我去学习 FP,但我并不是一个原教旨主义者,所以我当然也不认同你想学 FP 就得去学 Lisp 的说法。

在这期间我发现小黄书的作者 Kyle Simpson 也写了一本专门为 JSer 介绍 FP 的,书中前言部分我深以为然:

The way I see it, functional programming is at its heart about using patterns in your code that are well-known, understandable, and proven to keep away the mistakes that make code harder to understand.

是的,编程范式的作用是为了让人们更好地组织和理解代码,编程范式应该去服务写代码的人,而不是人去事无巨细地遵循编程范式的每一个规则,理解每一个晦涩难懂的概念。

I believe that programming is fundamentally about humans, not about code. I believe that code is first and foremost a means of human communication, and only as a side effect (hear my self-referential chuckle) does it instruct the computer.

敏捷需要以人为本,写代码其实也一样。我们要做的应该是理解编程范式本身以及它背后的作用,或许在未来的某天你会突然发现,原来我用了这么久的这个玩意儿有一个这么有意思的名字,亦或者你可能永远也解释不清楚那个概念到底是什么:

A monad is just a monoid in the category of endofunctors.

一个单子不过是自函子范畴上的幺半群

那是不是搞不懂我就不能玩 FP 了?然后我就得站在鄙视链底端,被 Haskell、Lisp 玩家们指着鼻子嘲笑:你们看那家伙,其实啥也不懂,他那也叫 FP?

这个问题我没有答案,或许可以留给大家来讨论。但是到这里我至少明白了 React Hooks 为什么要叫 "hook";为什么有一个 hook 叫 "useEffect";我也理解了为什么大家都说不要用 hook 去实现 class 组件的生命周期。

除了写好 React 本身,我也尝试了纯函数、偏函数、柯里化、组合和 Point-free 风格的代码,确实得到了一些好处,也确实带来了一些不便。

可能这些思想就是学习 React 带给我最大的 side effect 吧(笑。

与 React 准备 all in FP 相反的是,与 Angular 短暂接触的我发现它全面拥抱 OOP。与当时 React 从 class 组件切换到函数式组件一样,首先你得把编程范式思想完全转变过来才能很好地理解 Angular。这又促使我不得不去复习许多被我丢弃很久的 OOP 思想。

到这我不禁想起一次公司内 TDD 训练营,作业完成后去找 coach 讲解,讲解过程中 coach 讲到了抽象能力、隔离层、防腐层。那时我才发现自己 OO 的抽象能力和一起的后端小伙伴一比实在是差到不行,只有大学时候的能力。反思过后像是被 React 给“惯”坏了,几乎已经丢掉了这部分能力。

老实说我接触 React class 组件时间并不长,第一个项目只有短短几个月。后面两个项目虽然去写 Java 了,但是第一个都是一些修修补补的工作,更像是在做 DevOps,后来的项目去写 Java BFF,毫无抽象可言,全是数据 mapping。然后又进到了一个将“追求技术卓越”贯彻执行的项目,成了那批最早吃函数式组件螃蟹的人。

于是我接触 class 组件的时间就只有作为毕业生的那短短几个月而已。

然后当我看到 Angular 文档中的依赖注入时,我脑子只能零星蹦出一些概念:SOLID、解耦。别说细节,我甚至不知道我蹦出来的这些东西是不是对的。于是我又只能凭着自己的记忆去邮件里搜相关的博客大赛的文章。

我好像已经丢掉了 OOP 了。

种下一棵树最好的时间是十年前,其次是现在。

跳出all in FP 的 React 我发现世界不是非黑即白的。说是全面拥抱 OOP,但其实你可以很轻易的在 Angular 中发现 FP 的影子 -- 用 pipe 来处理数据,用 Rx 来处理请求。

既然是以人为本,编程范式本就不应该对立,它们明明可以互补,在自己擅长的领域处理自己擅长的事情,哪怕是同一个项目。看惯了两个阵营吵架的场景,好像这样的场景才是我想要的。

于是我又回忆起某天在项目上和大家讨论的项目分包问题,最后的结论是 OOP 的以对象和 domain 分包的策略在大多数时候要优于单纯的 FP 的方式。它能让功能更集中,让大家更容易找到自己想要找的东西。

但是回过头来静静思考,我虽然会好好学习 OOP,但是我目前大概率不会去深入学习相关的建模方法。因为在目前我的工作环境下,我没看到有前端同学需要深刻理解建模方法的场景,大多数情况浅尝辄止即可。

以我自身的经历来看,DDD 我看过也参加过培训,也跟着项目后端小伙伴在搭建项目时从零到一实践过。但是在实践不多的情况下,整个过程逃脱不了学了忘忘了学的魔咒。大概唯一的用处就是当我被抓到后端去干活能看懂他们为什么要这么组织代码,至于建模的那个过程,被抓去干活的我是大概率不会参与的。(当然如果你有相关的经历还请喷醒我,比如你作为偏前端的小伙伴就是要熟练掌握建模方法,不然工作就做不下去了)

不要被技术栈限制住了自己,其实以前一直对这句话一知半解,虽然可能现在的理解也没有很强。可是当你从一个框里跳出来以后,去思考画框这个人的想法,你可能能够得到一些不一样的思考。对于 Thoughtworker 来说学习一个新框架,一门新语言可能不是什么问题,那我们是不是可以更进一步,想想那些看起来“虚无缥缈”的东西呢。

别被你的框架框住你了。


作者:Teobler
来源:https://juejin.cn/post/7032467133611294733

收起阅读 »

12 个救命的 CSS 技巧

✨12 个救命的 CSS 技巧✨ 1. 使用 Shape-outside 在浮动图像周围弯曲文本它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:.any-shape {  width: 300px...
继续阅读 »



✨12 个救命的 CSS 技巧✨

1. 使用 Shape-outside 在浮动图像周围弯曲文本

它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:

.any-shape {
 width: 300px;
 float: left;
 shape-outside: circle(50%);
}

2. 魔法组合

这个小组合实际上可以防止你在 HTML 中遇到的大多数布局错误的问题。我们确实不希望水平滑块或绝对定位的项目做他们想做的事情,也不希望到处都是随机的边距和填充。所以这是你们的魔法组合。

* {
padding: 0;
margin: 0;
max-width: 100%;
overflow-x: hidden;
position: relative;
display: block;
}

有时“display:block”没有用,但在大多数情况下,你会将 <a><span> 视为与其他块一样的块。所以,在大多数情况下,它实际上会帮助你!

3. 拆分 HTML 和 CSS

这更像是一种“工作流程”类型的技巧。我建议你在开发时创建不同的 CSS 文件,最后才合并它们。例如,一个用于桌面,一个用于移动等。最后,你必须合并它们,因为这将有助于最大限度地减少您网站的 HTTP 请求数量。

同样的原则也适用于 HTML。如果你不是在 Gatsby 等 SPA 环境中进行开发,那么 PHP 可用于包含 HTML 代码片段。例如,你希望在单独的文件中保留一个“/modules”文件夹,该文件夹将包含导航栏、页脚等。因此,如果需要进行任何更改,你不必在每个页面上都对其进行编辑。模块化越多,结果就越好。

4. ::首字母

它将样式应用于块级元素的第一个字母。因此,我们可以从印刷或纸质杂志中引入我们熟悉的效果。如果没有这个伪元素,我们将不得不创建许多跨度来实现这种效果。例如:

这是如何做到的?代码如下:

p.intro:first-letter {
 font-size: 100px;
 display: block;
 float: left;
 line-height: .5;
 margin: 15px 15px 10px 0 ;
}

5. 四大核心属性

CSS 动画提供了一种相对简单的方法来在大量属性之间平滑过渡。良好的动画界面依赖于流畅流畅的体验。为了在我们的动画时间线中保持良好的性能,我们必须将我们的动画属性限制为以下四个核心:

  • 缩放 - transform:scale(2)

  • 旋转 - transform:rotate(180deg)

  • 位置 – transform:translateX(50rem)

  • 不透明度 - opacity: 0.5

边框半径、高度/宽度或边距等动画属性会影响浏览器布局方法,而背景、颜色或框阴影的动画会影响浏览器绘制方法。所有这些都会大大降低您的 FPS (FramesPerSecond)。您可以使用这些属性来产生一些有趣的效果,但应谨慎使用它们以保持良好的性能。

6. 使用变量保持一致

保持一致性的一个好方法是使用 CSS 变量或预处理器变量来预定义动画时间。

:root{ timing-base: 1000;}

在不定义单元的情况下设置基线动画或过渡持续时间为我们提供了在 calc() 函数中调用此持续时间的灵活性。此持续时间可能与我们的基本 CSS 变量不同,但它始终是对该数字的简单修改,并将始终保持一致的体验。

7. 圆锥梯度

有没有想过是否可以只使用 CSS 创建饼图?好消息是,您实际上可以!这可以使用 conic-gradient 函数来完成。此函数创建一个由渐变组成的图像,其中设置的颜色过渡围绕中心点旋转。您可以使用以下代码行执行此操作:

.piechart {
 background: conic-gradient(rgb(255, 132, 45) 0% 25%, rgb(166, 195, 209) 25% 56%, #ffb50d  56% 100%);
 border-radius: 50%;
 width: 300px;
 height: 300px;
}

8. 更改文本选择颜色

要更改文本选择颜色,我们使用 ::selection。它是一个伪元素,在浏览器级别覆盖以使用您选择的颜色替换文本突出显示颜色。使用光标选择内容后即可看到效果。

::selection {
    background-color: #f3b70f;
}

9. 悬停效果

悬停效果通常用于按钮、文本链接、站点的块部分、图标等。如果您想在有人将鼠标悬停在其上时更改颜色,只需使用相同的 CSS,但要添加 :hover到它并更改样式。这是您的方法;

.m h2{ 
   font-size:36px;
   color:#000;
   font-weight:800;
}
.m h2:hover{
   color:#f00;
}

当有人将鼠标悬停在 h2 标签上时,这会将您的 h2 标签的颜色从黑色更改为红色。它非常有用,因为如果您不想更改它,则不必再次声明字体大小或粗细。它只会更改您指定的任何属性。

10.投影

添加此属性可为透明图像带来更好的阴影效果。您可以使用给定的代码行执行此操作。

.img-wrapper img{
         width: 100% ;
         height: 100% ;
         object-fit: cover ;
         filter: drop-shadow(30px 10px 4px #757575);
}

11. 使用放置项居中 Div

居中 div 元素是我们必须执行的最可怕的任务之一。但不要害怕我的朋友,你可以用几行 CSS 将任何 div 居中。只是不要忘记设置display:grid; 对于父元素,然后使用如下所示的 place-items 属性。

main{
width: 100% ;
height: 80vh ;
display: grid ;
place-items: center center;
}

12. 使用 Flexbox 居中 Div

我们已经使用地点项目将项目居中。但是现在我们解决了一个经典问题,使用 flexbox 将 div 居中。为此,让我们看一下下面的示例:

<div>
<div></div>
</div>
.center {
display: flex;
align-items: center;
justify-content: center;
}

.center div {
width: 100px;
height: 100px;
border-radius: 50%;
background: #b8b7cd;
}

首先,我们需要确保父容器持有圆,即 flex-container。在它里面,我们有一个简单的 div 来制作我们的圆圈。我们需要使用以下与 flexbox 相关的重要属性:

  • display: flex; 这确保父容器具有 flexbox 布局。

  • align-items: center; 这可确保 flex 子项与横轴的中心对齐。

  • justify-content: center; 这确保 flex 子项与主轴的中心对齐。

之后,我们就有了常用的圆形 CSS 代码。现在这个圆是垂直和水平居中的,试试吧!

作者:海拥
来源:https://juejin.cn/post/7024372412632268813

收起阅读 »

Android组件化第一步壳工程配置

传统项目开发中,我们都是通过集成化的方式来搭建项目的架构。什么叫做集成化,我的理解,就是整个project有一个module,根据功能的需要来创建不同的library库,通过gradle的方式来实现依赖。 什么叫做组件化,我的理解就是,一个project中,将...
继续阅读 »

传统项目开发中,我们都是通过集成化的方式来搭建项目的架构。什么叫做集成化,我的理解,就是整个project有一个module,根据功能的需要来创建不同的library库,通过gradle的方式来实现依赖。


什么叫做组件化,我的理解就是,一个project中,将会有多个module,并且这个module可以在需要的时候切换身份,变成library,作为主module的依赖,主 module 就是我们的壳工程。


为什么会想尝试一下组件化呢?有两个愿景:


1.在开发中,可以不用打包整个app。实现测试同学的测试包和开发人员的自测包分离


2.减少开发自测时的打包时间。


以上就是我对组件化目前的理解,对于路由那块,分篇讨论。为了解决上面愿景中的两个问题,我们可以做如下配置。


开始步骤1,2,3...

1.在项目的build.gradle中创建一个boolean变量,用来确定编译项目为集成化模式还是组件化模式。同时配置buildconfig。方便在代码中进行判断处理。


image2021-3-5_9-28-45.png


这儿说一下,gradle的引入机制,没有根据文档,只是主观推断。gradle会先从项目的build.gradle中进行读取,通过ext来定义整个工程的变量,通过apply from 来引入其他的gradle配置文件,在project中配置的功能和变量,将会在整个工程中都可以使用。


2.在要做成组件化的library中进行配置,主要是切换plugin是library还是module,以及是否在default中展示application Id,这儿有可能因为依赖的库太多,需要配置mutidex,来解决65535的问题。def用于定义子module内部的变量。


image2021-3-5_9-29-7.png


image2021-3-5_9-29-18.png


3.在依赖该library的地方,也就是主module地方,进行配置。如果是集成化的配置,也就是isRelease为true,才可以依赖,否则会在编译时产生依赖重复引入异常,无法编译通过。同时在defaultConfig里面配置buildconfig变量,方便代码中使用,进行功能切换


image2021-3-5_9-29-35.png


// 如果是集成化模式,做发布版本时。各个模块都不能独立运行了

if (isRelease) {

implementation project(':YoungWear')

}

4.配置两个AndroidManifest,作为module时候是有Application的,同时按照mutidex的配置方案配置module,接下来是一些核心的代码配置

// 配置资源路径,方便测试环境,打包不集成到正式环境

sourceSets {

main {

if (!isRelease) {

// 如果是组件化模式,需要单独运行时

manifest.srcFile 'src/main/debug/AndroidManifest.xml'

} else {

// 集成化模式,整个项目打包apk

manifest.srcFile 'src/main/AndroidManifest.xml'

java {

// release 时 debug 目录下文件不需要合并到主工程

exclude '**/debug/**'

}}}}

image2021-3-5_9-30-16.png


windows下,分别给project里的build.gradle赋值true和false,terminal中输入gradlew compileDebugSource --stacktrace -info ,查看是否可以编译成功,当作为module的方式,可以在AS中,看到如下图的图标正常,就证明配置成功了,直接安装apk到手机就可以了。


image2021-3-5_9-30-36.png


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

Android开发:实现滑动退出 Fragment + Activity 二合一

前言 能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment? 今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库 特性: 1、SwipeBackFra...
继续阅读 »

前言



能否在不包含侧滑菜单的时候,添加一个侧滑返回,边缘finish当前Fragment?



今天把这项工作完成了,做成了单独的SwipeBackFragment库以及Fragmentation-SwipeBack拓展库


特性:

1、SwipeBackFragment , SwipeBackActivity二合一:当Activity内的Fragment数大于1时,滑动finish的是Fragment,如果小于等于1时,finish的是Activity。


2、支持左、右、左&右滑动(未来可能会增加更多滑动区域)


3、支持Scroll中的滑动监听


4、帮你处理了app被系统强杀后引起的Fragment重叠的情况


效果



效果图


谈谈实现


拖拽部分大部分是靠ViewDragHelper来实现的,ViewDragHelper帮我们处理了大量Touch相关事件,以及对速度、释放后的一些逻辑监控,大大简化了我们对触摸事件的处理。(本篇不对ViewDragHelper做详细介绍,有不熟悉的小伙伴可以自行查阅相关文档)


对Fragment以及Activiy的滑动退出,原理是一样的,都是在Activity/Fragment的视图上,添加一个父View:SwipeBackLayout,该Layout里创建ViewDragHelper,控制Activity/Fragment视图的拖拽。


1、Activity的实现


对于Activity的SwipeBack实现,网上有大量分析,这里我简要介绍下原理,如下图:



我们只要保证SwipeBackLayout、DecorView和Window的背景是透明的,这样拖拽Activity的xml布局时,可以看到上个Activity的界面,把布局滑走时,再finish掉该Activity即可。


public void attachToActivity(FragmentActivity activity) {
...
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
decorChild.setBackgroundResource(background);
decor.removeView(decorChild); // 移除decorChild
addView(decorChild); // 添加decorChild到SwipeBackLayout(FrameLayout)
setContentView(decorChild);
decor.addView(this);} // 把SwipeBackLayout添加到DecorView下


2、Fragment的实现


重点来了,Fragment的实现!

在实现前,我先说明Fragment的几个相关知识点:


1、Fragment的视图部分其实就是在onCreateView返回的View;


2、同一个Activity里的多个通过add装载的Fragment,他们在视图层是叠加上去的:

hide()并不销毁视图,仅仅让视图不可见,即View.setVisibility(GONE);

show()让视图变为可见,即View.setVisibility(VISIBLE);



add+show/hide的情况


3、通过replace装载的Fragment,他们在视图层是替换的,replace()会销毁当前的Fragment视图,即回调onDestoryView,返回时,重新创建视图,即回调onCreateView;



replace的情况


4、不管add还是replace,Fragment对象都会被FragmentManager保存在内存中,即使app在后台因系统资源不足被强杀,FragmentManager也会为你保存Fragment,当重启app时,我们可以从FragmentManager中获取这些Fragment。


分析:


Fragment之间的启动无非下图中的2种:



而这个库我并没有考虑replace的情况,因为我们的SwipeBackFragment应该是在"流式"使用的场景(FragmentA -> FragmentB ->....),而这种场景下结合上面的2、3、4条,add+show(),hide()无疑更优于replace,性能更佳、响应更快、我们app的代码逻辑更简单。


add+hide的方式的实现


从第1条,我们可以知道onCreateView的View就是需要放入SwipeBackLayout的子View,我们给该子View一个背景色,然后SwipeBackLayout透明,这样在拖拽时,即可看到"上个Fragment"。


当我们拖拽时,上个Fragment A的View是GONE状态,所以我们要做的就是当判断拖拽发生时,Fragment A的View设置为VISIBLE状态,这样拖拽的时候,上个Fragment A就被完好的显示出来了。


核心代码:


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(...);
return attachToSwipeBack(view);
}

protected View attachToSwipeBack(View view) {
mSwipeBackLayout.addView(view);
mSwipeBackLayout.setFragment(this, view);
return mSwipeBackLayout;
}


但是相比Activity,上个Activity的视图状态是VISIBLE的,而我们的上个Fragment的视图状态是GONE的,所以我们需要FragmentA.getView().setVisibility(VISIBLE),但是时机是什么时候呢?


最好的方案是开始拖拽前的那一刻,我是在ViewDragHelper里的tryCaptureView方法处理的:


@Override
public boolean tryCaptureView(View child, int pointerId) {
boolean dragEnable = mHelper.isEdgeTouched(ViewDragHelper.EDGE_LEFT);
if (mPreFragment == null) {
if (dragEnable && mFragment != null) {
...省略获取上一个Fragment代码
mPreFragment = fragment;
mPreFragment.getView().setVisibility(VISIBLE);
break;
}
} else {
View preView = mPreFragment.getView();
if (preView != null && preView.getVisibility() != VISIBLE) {
preView.setVisibility(VISIBLE);
}
}
return dragEnable;
}


通过上面代码,我们拖拽当前Fragment前的一瞬间,PreFragment的视图会被VISIBLE,同时完全不会影响onHiddenChanged方法,完美。(到这之前可能有小伙伴想到,只通过add不hide上个Fragment的思路怎么样?很明显是不行的,因为这样的话onHiddenChanged方法不会被回调,而我们使用add的方式,主要通过onHiddenChanged来作为“生命周期”来实现我们的逻辑的)


还一种情况需要注意,当我已经开始拖拽FragmentB打算pop时,拖拽到一半我放弃了,这时FragmentA的视图已经是VISIBLE状态,我又从B进入到Fragment C,这是我们应该把A的视图GONE掉:


SwipeBackFragment里:
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (hidden && mSwipeBackLayout != null) {
mSwipeBackLayout.hiddenFragment();
}
}

SwipeBackLayout里:
public void hiddenFragment() {
if (mPreFragment != null && mPreFragment.getView() != null) {
mPreFragment.getView().setVisibility(GONE);
}
}


坑点


1、触摸事件冲突


当我们所拖拽的边缘区域中的子View,有其他Touch事件,比如Click事件,这时我们会发现我们的拖拽失效了,这是因为,如果子View不消耗事件,那么整个Touch流程直接走onTouchEvent,在onTouchEvent的DOWN的时候就确定了CaptureView。如果子View消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在这过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获;


并且你需要考虑当前拖拽的页面下是有2个SwipeBackLayout:当前Fragment的和Activity的,最后代码如下:


@Override
public int getViewHorizontalDragRange(View child) {
if (mFragment != null) {
return 1;
} else {
if (mActivity != null && mActivity.getSupportFragmentManager().getBackStackEntryCount() == 1) {
return 1;
}
}
return 0;
}


这样的话,一方面解决了事件冲突,一方面完成了Activity内Fragment数量大于1时,拖拽的是Fragment,等于1时拖拽的是Activity。


2、动画


我们需要在拖拽完成时,将Fragment/Activity移出屏幕,紧接着关闭,最重要的是要保证当前Fragment/Actiivty关闭和上一个Fragment/Activity进入时是无动画的!


对于Activity这项工作很简单:Activity.overridePendingTransition(0, 0)即可。


对于Fragment,如果本身在Fragment跳转时,就不为其设置转场动画,那就可以直接使用了;

如果你使用了setCustomAnimations(enter,exit)或者setCustomAnimations(enter,exit,popenter,popexit),你可以这样处理:


SwipeBackLayout里:
{
mPreFragment.mLocking = true;
mFragment.mLocking =true;
mFragment.getFragmentManager().popBackStackImmediate();
mFragment.mLocking = false;
mPreFragment.mLocking = false;
}

SwipeBackFragment里:
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if(mLocking){
return mNoAnim;
}
return super.onCreateAnimation(transit, enter, nextAnim);
}


3、启动新Fragment时,不要调用show()


getSupportFragmentManager().beginTransaction()
.setCustomAnimations(xxx)
.add(xx, B)
// .show(B)
.hide(A)
.commit();


请不要调用上述代码里的show(B)

一方面是新add的B本身就是可见状态,不管你是show还是不调用show,都不会回调B的onHiddenChanged方法;

另一方面,如果你调用了show,滑动返回会后出现异常行为,回到PreFragment时,PreFragment的视图会是GONE状态;如果你非要调用show的话,请按下面的方式处理:(没必要的话,还是不要调用show了,下面的代码可能会产生闪烁)


@Overridepublic void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden && getView().getVisibility() != View.VISIBLE) {
getView().post(new Runnable() {
@Override
public void run() {
getView().setVisibility(View.VISIBLE);
}
});
}
}


最后


我为什么把这个库做成2个,一个单独使用的SwipeBackFragment和一个Fragmentation-SwipeBack拓展库呢?


原因在于:

SwipeBackFragment库是一个仅实现Fragment&Activity拖拽返回的基础库,适合轻度使用Fragment的小伙伴(项目属于多Activity+多Fragment,Fragment之间没有复杂的逻辑),当然你也可以随意拓展。


Fragmentation主要是在项目结构为 单Activity+多Fragment,或者重度使用Fragment的多Activity+多Fragment结构时的一个Fragment帮助库,Fragment-SwipeBack是在其基础上拓展的一个库,用于实现滑动返回功能,可以用于各种项目结构。


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

HashMap源码解析

带着问题看HashMap源码(基于JDK8) HashMap由于涉及到多个数据结构,所以变成了面试题的常客,下面带着以下几个面试常见问题去阅读JDK8中HashMap的源码 HashMap底层数据结构 HashMap的put过程 HashMap的get过程...
继续阅读 »

带着问题看HashMap源码(基于JDK8)



  • HashMap由于涉及到多个数据结构,所以变成了面试题的常客,下面带着以下几个面试常见问题去阅读JDK8中HashMap的源码

    1. HashMap底层数据结构

    2. HashMap的put过程

    3. HashMap的get过程

    4. HashMap如何扩容,扩容为啥是之前的2倍

    5. HashMap在JDK8中为啥要改成尾插法




1、HashMap底层数据结构



  • HashMap的数据结构是数组 + 链表 + 红黑树

    • 默认是存储的Node节点的数组


    Node<K,V>[] table;

    static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 存储的key的hash值
    final K key; // key键
    V value; // value值
    Node<K,V> next; // 链表指向的下一个节点


    • 当Node节点中链表(next)长度超过8时会将链表转换为红黑树TreeNode(Node的子类)以提高查询效率


    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent; // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev; // needed to unlink next upon deletion
    boolean red;


  • Node[]数组的初始长度默认为16,并且必须为2^n的形式(具体原因下面会有解释)


/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量为16,并且必须为2的幂数
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


  • HashMap默认的阈值threshold = 负载因子loadFactor(默认为0.75)*容量capacity,即初始时默认为16 * 0.75 = 12

    • 表示当hashMap中存储的元素超过该阈值时,为了减少hash碰撞,会对hashMap的容量Capacity进行resize扩容,每次扩容都是之前的2倍,扩容后会重新计算hash值即重新计算在新的存放位置并插入


    /**
    * The load factor used when none specified in constructor.
    * 当没有在构造中指定loadFactor加载因子时,默认值为0.75
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;



2、HashMap的put过程


put & putIfAbsent


/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
* 将指定的值与此映射中的指定键相关联。如果映射以前包含键的映射,则旧的值被替换
*
* @param key key with which the specified value is to be associated key值
* @param value value to be associated with the specified key key对应的Value值
* @return the previous value associated with key, or null if there was no mapping for key
* 当hashmap中已有当前key覆盖更新并返回旧的Value,如果没有返回null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

// onlyIfAbsent参数为true,表示仅在不包含该key时会插入,已包含要插入的key时则不会覆盖更新
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}

hash方法计算key的hash值


// 通过key计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

putVal相关代码


/**
* Implements Map.put and related methods
*
* @param hash hash for key key的hash值,通过hash方法获取
* @param key the key 键
* @param value the value to put 值
* @param onlyIfAbsent if true, don't change existing value 当已有key时是否覆盖更新
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none 返回旧的值,如果没有相同的key返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1、第一次put时table为null,就会触发resize,将初始化工作延迟到第一次添加元素时,懒加载
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、将hash值与size-1进行&运算得出数组存放的位置;当此位置上还未存放Node时
// 直接初始化创建一个Node(hash,key,value,null)并放置在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;e
// 3、假如该位置已经有值,但存储的key完全相同时,直接将原来的值赋值给临时e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4、假如该位置有值,key值也不同,先判断该Node是不是一个TreeNode类型(红黑树,Node的子类)
// 就调用putTreeVal方法执行红黑树的插入操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5、假如该位置有值,key值也不同,Node也不是一个TreeNode红黑树类型,
// 便会对链表进行遍历并对链表长度进行计数,遍历到链表中有相同key的节点会跳出遍历
// 当链表长度计数的值超过8(包含数组本身上的Node)时
// 就会触发treeifyBin操作即将链表转化为红黑树
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 这里主要针对相同的key做处理,当onlyIfAbsent为true时就不覆盖,为false时覆盖更新
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 6、当hashMap存储的元素数量超过阈值就会触发resize扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize扩容相关代码


/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;、
// 针对后续扩容
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 这里针对构造器中自行设置了initialCapacity的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 针对第一次put时,Node数组相关参数初始化
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 链表数组初始化
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 扩容时将旧的Node移到新的数组操作
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断高位是1还是0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

put大致的流程总结



  1. 第一次put元素时会触发resize方法,其实是将hashMap的Node[]数组初始化工作进行了类似懒加载的处理

  2. 将hash值与capacity-1进行&运算计算出当前key要放置在数组中的位置;当该位置无值时就会直接初始化创建一个Node(hash,key,value,null)并放置在该位置,如果已有值就先判断存储和插入的key是否相等,相等的话通过onlyIfAbsent参数判定是否要覆盖更新并返回旧值

  3. 如果已有值并且与要存储的key不等,就先判定该Node是否是一个TreeNode(红黑树,Node的子类),是的话就调用putTreeVal方法执行红黑树的插入操作

  4. 如果已有值并且与要存储的key不等也不是一个红黑树节点TreeNode就会对Node链表进行遍历操作,遍历到链表中有相同key就跳出根据onlyIfAbsent参数判定是否要覆盖更新,如果没有便新建Node,放置在Node链表的Next位置;如果链表长度超过8时便会将链表转化为红黑树并重新插入

  5. 最后判断HashMap存储的元素是否超过了阈值,超过阈值便会执行resize扩容操作,并且每次扩容都是之前的2倍。扩容后重新进行hash&(capacity-1)计算元素的插入位置重新插入


image.png


3、HashMap的get过程


get方法执行



  • 实质上是调用的getNode方法


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法


/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 先判断Node数组是否为空或length为0或是否存储的值本身为null,如果是直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 当匹配到节点数组上的Node的hash和key都相同时直接返回该Node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断Node.next,如果为TreeNode红黑树类型就利用getTreeNode方法进行红黑树的查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 不是红黑树结构就是链表结构,进行链表遍历操作,直至找到链表中hash和key值都相等
// 的元素便返回该Node
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

get大致的流程总结



  1. get方法实质调用的是getNode方法

  2. 首先通过hash(key)方法计算出key的hash值,再通过hash&(capacity-1)计算出要查找的Node数组中的元素位置

  3. 假如Node数组为null或者数组length为0或者该位置本身存储的元素就是null就直接返回null

  4. 假如该位置存储的元素不为null,直接对该位置的Node的hash和key进行匹配,假如都相等便匹配成功返回该Node

  5. 假如该数组上的Node不匹配就获取该Node的next元素,首先判断该元素是否是一个TreeNode红黑树节点类型的Node,如果是就利用getTreeNode方法进行红黑树的查找,找到返回该节点,找不到返回null

  6. 如果next节点的Node不是TreeNode表明是一个链表结构,直接循环遍历该链表,直至找到该值,或最后一个链表元素仍然不匹配就跳出循环返回null


4、HashMap如何扩容,扩容为啥是之前的2倍



  • HashMap中当存储的元素数量超过阈值时就会触发扩容,每次扩容后容量会变成之前的2倍

  • 因为扩容为2倍时,capacity-1转换成2进制后每一位都为1,使得hash&(capacity-1)计算得出要存放的新位置要么是之前的位置要么是之前的位置+ 之前的capacity,使得在扩容时,不需要重新计算元素的hash了,只需要判断最高位是1还是0就好了(hash&oldCapacity),一方面降低了hash冲突,一方面提升了扩容后重新插入的效率


image.png


5、HashMap在JDK8中为啥要改成尾插法



  • 参考:juejin.cn/post/684490…

  • HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了

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

Swift-Router 自己写个路由吧,第三方总是太复杂

iOS
Swift-Router 自己写个路由吧,第三方总是太复杂先看看这个路由的使用吧如果是网络地址,会直接自动跳转到 OtherWKWebViewController如果是应用内部的手动调用跳转直接跳转视图控制器EPRouter.pushViewControlle...
继续阅读 »

Swift-Router 自己写个路由吧,第三方总是太复杂

先看看这个路由的使用吧
  1. 如果是网络地址,会直接自动跳转到 OtherWKWebViewController
  2. 如果是应用内部的手动调用跳转
  • 直接跳转视图控制器
    • EPRouter.pushViewController(EPSMSLoginViewController())
  • 先在 RouteDict 注册映射关系再跳转
    • EPRouter.pushAppURLPath("goods/detail?spellId=xxx&productId=xxx")
  1. 又服务器来控制跳转 也得在 RouteDict 注册映射关系,只不过多加了一个 scheme
    • EPRouter.pushURLPath("applicationScheme://goods/detail?spellId=xxx&productId=xxx")

**!!!支持Swift、OC、Storyboard的跳转方式,可以在 loadViewController 看到实现方式 **

EPRouter的全部代码
class EPRouter: NSObject {

    private static let RouteDict:[String:String] = [
        "order/list"            :"OrderListPageViewController",   // 订单列表 segmentIndex
        "order/detail"          :"OrderDetailViewController",     // 订单详情 orderId
        "goods/detail"          :"GoodsDetailViewController",     // 商品详情productId
        "goods/list"            :"GoodsCategoryViewController", // type brandId 跳转到某个分类;跳转到某个品牌
        "goods/search"          :"SearchListViewController", // 搜索商品 text
        "coupon/list"           :"CouponListViewController",      // 优惠券列表
        "cart/list"             :"CartViewController",        // 购物车列表
        "address/list"          :"AddressListViewController",     // 收货地址列表
    ]


// 返回首页,然后指定选中模块
public static func backToTabBarController(index: NSInteger, completion:(()->())?=nil) {

guard let vc = EPCtrlManager.getTopVC(), let nav = vc.navigationController, let tabBarCtrl = nav.tabBarController  else {
return
}

nav.popToRootViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.1) {
tabBarCtrl.selectedIndex = index
completion?()
}
}


// 销毁n个界面 不建议使用这个方法 可以在pushAppURLPath方法中设置destroyTime达到一样的效果,又可以避免用户侧滑返回
public static func popViewController(animated: Bool, time:NSInteger=1) {

guard let nav = EPCtrlManager.getTopVC()?.navigationController else {
return
}
let vcs = nav.viewControllers
let count = vcs.count
let index = (count - 1) - time
if index >= 0 {
let vc = vcs[index]
nav.popToViewController(vc, animated: true)
} else {
nav.popViewController(animated: true)
}
}


    /// 回到目标控制器
    public static func popViewController(targetVC: UIViewController.Type, animated: Bool, toRootVC: Bool=true) {

        popViewController(targetVCs: [targetVC], animated: animated, toRootVC: toRootVC)
    }

    

    /// 回到目标控制器[vc],从前到后 没有目标控制器是否回到根视图
    public static func popViewController(targetVCs: [UIViewController.Type], animated: Bool, toRootVC: Bool=true) {

        guard let nav = EPCtrlManager.getTopVC()?.navigationController else {
            return
        }
        let vcs = nav.viewControllers
        var canPop = false
        for vc in vcs {
            for tvc in targetVCs {
                if vc.isMember(of: tvc) {
                    canPop = true
                    nav.popToViewController(vc, animated: animated)
                    break
                }
            }
        }
        if !canPop && toRootVC {
            nav.popToRootViewController(animated: animated)
        }
    }

    /// push 一个vc --- destroyTime: push之前要销毁的几个压栈vc
    @objc public static func pushAppURLPath(_ path: String, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        var urlString = "applicationScheme://"+path
        if path.contains("http://") || path.contains("https://") {
            urlString = path
        }
        pushURLString(urlString, query: query, animated: animated, destroyTime: destroyTime)
    }


    @objc public static func pushURLString(_ urlString: String, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        guard let tvc = loadViewControllerWitURI(urlString, query: query) else {
            return
        }
        pushViewController(tvc, animated: animated, destroyTime: destroyTime)
    }


    @objc public static func pushViewController(_ tvc: UIViewController, query: [AnyHashable: Any]=[:], animated: Bool=true, destroyTime:NSInteger=0) {

        guard let vc = EPCtrlManager.getTopVC() else {
            return
        }

        if let _ = tvc.pushInfo {
            tvc.pushInfo?.merge(query, uniquingKeysWith: { (_, new) in new })
        }else {
            tvc.pushInfo = query
        }
        guard let nav = vc.navigationController else {
            vc.present(tvc, animated: true, completion: nil)
            return
        }
        tvc.hidesBottomBarWhenPushed = true

        if destroyTime > 0 {
            let vcs = nav.viewControllers
            let count = vcs.count
            var index = (count - 1) - destroyTime
            if index < 0 { // destroyTime 很多时,直接从根视图push
                index = 0
            }

            var reVCS = [UIViewController]()
            for vc in nav.viewControllers[0...index] {
                reVCS.append(vc)
            }
            reVCS.append(tvc)
            nav.setViewControllers(reVCS, animated: animated)
        }else {
            nav.pushViewController(tvc, animated: animated)
        }
    }

    public static func loadViewController(_ className: String, parameters: [AnyHashable: Any]? = nil) -> UIViewController? {

        var desVC: UIViewController?
        let spaceName = (Bundle.main.infoDictionary?["CFBundleExecutable"] as? String) ?? "ApplicationName"

        if let vc = storyboardClass(className) { // storyboard
            desVC = vc
        }else if let aClass = NSClassFromString("\(spaceName).\(className)") { // Swift
            if aClass is UIViewController.Type {
                let type = aClass as! UIViewController.Type
                desVC = type.init()
            }
        }else if let aClass = NSClassFromString("\(className)") { // OC
            if aClass is UIViewController.Type {
                let type = aClass as! UIViewController.Type
                desVC = type.init()
            }
        }

        desVC?.pushInfo = parameters
        return desVC
    }


    public static func loadViewController(_ viewController: UIViewController, parameters: [AnyHashable: Any]? = nil) -> UIViewController {

        viewController.pushInfo = parameters
        return viewController

    }

    public static func loadViewControllerWitURI(_ urlString: String, query: [AnyHashable: Any]? = nil) -> UIViewController? {

        

        // 先进行编码,防止有中文的带入, 不行进行二次编码
        var urlString = urlString
        if (URLComponents(string: urlString) == nil) {
            urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? urlString
        }

        guard let url = URLComponents(string: urlString), let scheme = url.scheme else {
            HGLog("无效的地址:\(urlString)")
            return nil
        }

        if scheme == "http" || scheme == "https" {

            let webVC = OtherWKWebViewController()
            webVC._urlStr = urlString
            return webVC

        } else if String(format: "%@://", scheme) == "appcationScheme://" {
            let path = (url.host ?? "") + url.path
            guard  var vcClassName = RouteDict[path] else {
                HGLog("没有配置视图控制器呢。。。:\(urlString)")
                return nil
            }

            var info: [AnyHashable: Any]?
            if query?.count ?? 0 > 0 {
                info = [AnyHashable: Any]()
                for (key, value) in query! {
                    info![key] = value
                }
            }

            if let queryItems = url.queryItems {
                if info == nil {
                    info = [AnyHashable: Any]()
                }
                for item in queryItems {
                    if let value = item.value {
                        info![item.name] = value
                    }
                }
            }
            return loadViewController(vcClassName, parameters: info)
        }

        HGLog("未知scheme:\(urlString)")
        return nil

    }

    

    private static func storyboardClass(_ className: String) -> UIViewController? {

        if className == "VIPWithdrawViewController" { // 提现
            let vc = UIStoryboard.init(name: "VIP", bundle: nil).instantiateViewController(withIdentifier: "withdrawTVC")
            return vc
        }else if className == "VIPRecordListViewController" { // 提现记录
            let vc = UIStoryboard.init(name: "VIP", bundle: nil).instantiateViewController(withIdentifier: "recordListVC")
            return vc
        }
        return nil
    }
}

用来跳转传递数据的扩展属性
extension UIViewController {

    private struct PushAssociatedKeys {
        static var pushInfo = "pushInfo"
    }

    @objc open var pushInfo: [AnyHashable: Any]? {
        get {
            return objc_getAssociatedObject(self, &PushAssociatedKeys.pushInfo) as? [AnyHashable : Any]
        }
        set(newValue) {
            objc_setAssociatedObject(self, &PushAssociatedKeys.pushInfo, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

可见视图控制器的获取
class EPCtrlManager: NSObject {

    public static let `default`: EPCtrlManager = {
        return EPCtrlManager()
    }()

    // MARK: **- 查找顶层控制器、**
    // 获取顶层控制器 根据window
    @objc public static func  getTopVC() -> UIViewController? {

        var window = UIApplication.shared.keyWindow
        //是否为当前显示的window
        if window?.windowLevel != UIWindow.Level.normal{
            let windows = UIApplication.shared.windows
            for  windowTemp in windows{
                if windowTemp.windowLevel == UIWindow.Level.normal{
                    window = windowTemp
                    break
                }
            }
        }
        let vc = window?.rootViewController
        return getTopVC(withCurrentVC: vc)
    }

    ///根据控制器获取 顶层控制器
    private static func  getTopVC(withCurrentVC VC :UIViewController?) -> UIViewController? {

        if VC == nil {
            print("🌶: 找不到顶层控制器")
            return nil
        }

        if let presentVC = VC?.presentedViewController {
            //modal出来的 控制器
            return getTopVC(withCurrentVC: presentVC)
        }else if let tabVC = VC as? UITabBarController {
            // tabBar 的跟控制器
            if let selectVC = tabVC.selectedViewController {
                return getTopVC(withCurrentVC: selectVC)
            }
            return nil
        } else if let naiVC = VC as? UINavigationController {
            // 控制器是 nav
            return getTopVC(withCurrentVC:naiVC.visibleViewController)
        } else {
            // 返回顶控制器
            return VC
        }
    }
}
收起阅读 »