注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android gradle迁移至kts

背景 在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持...
继续阅读 »

背景


在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持kotlin好久了,但是由于编译速度或者转换成本的原因,真正实现kts转换的项目很少。在笔者的mac m1 中使用最新版的AS去编译build.gradle.kts,速度已经是和用groovy写的gradle脚本不相上下了,所以就准备写了这篇文章,希望做一个记录与分享。



















groovykotlin
好处:构建速度较快,运用广泛,动态灵活好处:编译时完成所有,语法简洁,android项目中可用一套语言开发构建脚本与app编写
坏处:语法糖的作用下,很难理解gradle运行的全貌,作用单一,维护成本较高坏处:编译略慢于groovy,学习资料较少

虽然主流的gradle脚本编写依旧是groovy,但是android开发者官网也在推荐迁移到kotlin


编译前准备


这里推荐看看这篇文章,里面也涵盖了很多干货,


全局替换‘’为“”


在kotlin中,表示一个字符串用“”,不同于groovy的‘ ’,所以我们需要全局替换。可以通过快捷方式command/control+shift+R 全局替换,选中匹配正则表达式并设定file mask 为 *.gradle:

正则表达式
'(.*?[^\\])'
作用范围为
"$1"

image.png


全局替换方法调用


在groovy中,方法是可以隐藏(),举个例子

apply plugin: "com.android.application"

这里实际上是调用apply方法,然后命名参数是plugin,内容围为"com.android.application",然而在kotlin语法中,我们需要以()或者invoke的方式才能调用一个方法,所以我们要给所有的groovy函数调用添加()

正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)

image.png
很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为

task(sourcesJar(type: Jar) {
from(android.sourceSets.main.java.srcDirs)
classifier = "sources"
})

gradle kt化


接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。


开始前准备


以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如


image.png
对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用


ext


我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!

val myNewProperty by extra("initial value")

但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!


task



  • 新建task
groovy版本

task clean(type: Delete) {
delete rootProject.buildDir
}

比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称

task<Delete>("clean",{
delete(rootProject.buildDir)
})

当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过

 ./gradlew help --task task名

去查看相应的类型



  • 已有task修改
    对于有些是已经在gradle编译时存在的函数任务,比如
groovy版本

wrapper{
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}

这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到

tasks {
named<Wrapper>("wrapper") {
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN

}
}

这种方式,去找到一个我们想要的task,并配置其内容



  • 生命周期函数
    我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast
tasks.create("greeting") {
doLast { println("Hello, World!") }
}

再比如dependOn

task<Jar>("javadocJar", {
dependsOn(tasks.findByName("javadoc"))
})

动态函数


sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员

sourceSets{
val main by getting{
jniLibs.srcDirs("src/main/libs")
jni.srcDirs()
}

}

也可以通过getByName方式去获取

sourceSets.getByName("main")

plugins


在比较旧的版本中,我们AS默认创建引入一个plugin的方式是

apply plugin: 'com.android.application'

其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成

plugins {
id("com.android.application")
}

就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。


说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!


Plugins解析


我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看


DefaultScriptPluginFactory

            final ScriptTarget initialPassScriptTarget = initialPassTarget(target);

ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);

// 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
CompileOperation<?> initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
Class<? extends BasicScript> scriptType = initialPassScriptTarget.getScriptClass();
ScriptRunner<? extends BasicScript, ?> initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
initialRunner.run(target, services);

PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

// 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
final ScriptTarget scriptTarget = secondPassTarget(target);
scriptType = scriptTarget.getScriptClass();

CompileOperation<BuildScriptData> operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);

final ScriptRunner<? extends BasicScript, BuildScriptData> runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
scriptTarget.attachScript(runner.getScript());
}
if (!runner.getRunDoesSomething()) {
return;
}

Runnable buildScriptRunner = () -> runner.run(target, services);

boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
}




可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。


image.png


那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码


重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段

public class DefaultCompileOperationFactory implements CompileOperationFactory {
private static final StringInterner INTERNER = new StringInterner();
private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
private static final String BODY_COMPILE_STAGE = "BODY";

private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
private final DocumentationRegistry documentationRegistry;

public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
this.documentationRegistry = documentationRegistry;
}

public CompileOperation<?> getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
}

public CompileOperation<BuildScriptData> getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
String operationId = scriptTarget.getId();
return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
}
}

getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行


@Override
public Statement transform(SourceUnit sourceUnit, Statement statement) {
...

if (scriptBlock.getName().equals(PLUGINS)) {
return transformPluginsBlock(scriptBlock, sourceUnit, statement);
}
...


总结


文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!


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

Android的线程和线程池

从用途上来说Android的线程主要分为主线程和子线程两类,主线程主要处理和界面相关的工作,子线程主要处理耗时操作。除Thread之外,Android中还有其他扮演线程的角色如AsyncTask、IntentService、HandleThread,其中Asy...
继续阅读 »

从用途上来说Android的线程主要分为主线程和子线程两类,主线程主要处理和界面相关的工作,子线程主要处理耗时操作。除Thread之外,Android中还有其他扮演线程的角色如AsyncTask、IntentService、HandleThread,其中AsyncTask的底层用到了线程池,IntentService和HandleThread的底层直接使用了线程。


AsyncTask内部封装了线程池和Handler主要是为了方便开发者在在线程中更新UI;HandlerThread是一个具有消息循环的线程,它的内部可以使用Handler;IntentService是一个服务,系统对其进行了封装使其可以更方便的执行后台任务,IntentService内部采用HandleThread来执行任务,当任务执行完毕后IntentService会自动退出。IntentService是一个服务但是它不容易被系统杀死因此它可以尽量的保证任务的执行。


1.主线程和子线程


主线程是指进程所拥有的的线程,在Java中默认情况下一个进程只能有一个线程,这个线程就是主线程。主线程主要处理界面交互的相关逻辑,因为界面随时都有可能更新因此在主线程不能做耗时操作,否则界面就会出现卡顿的现象。主线程之外的线程都是子线程,也叫做工作线程。


Android沿用了Java的线程模型,也有主线程和子线程之分,主线程主要工作是运行四大组件及处理他们和用户的交互,子线程的主要工作就是处理耗时任务,例如网络请求,I/O操作等。Android3.0开始系统要求网络访问必须在子线程中进行否则就会报错,NetWorkOnMainThreadException


2.Android中的线程形态


2.1 AsyncTask


AsyncTask是一个轻量级的异步任务类,它可以在线程池中执行异步任务然后把执行进度和执行结果传递给主线程并在主线程更新UI。从实现上来说AsyncTask封装了Thread和Handler,通过AsyncTask可以很方便的执行后台任务以及主线程中访问UI,但是AsyncTask不适合处理耗时任务,耗时任务还是要交给线程池执行。


AsyncTask的四个核心类如下:





    • onPreExecute():主要用于做一些准备工作,在主线程中执行异步任务执行之前

    • doInBackground(Params ... params):在线程池执行,此方法用于执行异步任务,params表示输入的参数,在此方法中可以通过publishProgress方法来更新任务进度,publishProgress会调用onProgressUpdate

    • onProgressUpdate(Progress .. value):在主线程执行,当任务执行进度发生改变时会调用这个方法

    • onPostExecute(Result result):在主线程执行,异步任务之后执行这个方法,result参数是返回值,即doInBackground的返回值。




2.2 AsyncTask的工作原理


2.3 HandleThread


HandleThread继承自Thread,它是一种可以使用Handler的Thread,它的实现在run方法中调用Looper.prepare()来创建消息队列然后通过Looper.loop()来开启消息循环,这样在实际使用中就可以在HandleThread中创建Handler了。

@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

HandleThread和Thread的区别是什么?





    • Thread的run方法中主要是用来执行一个耗时任务;

    • HandleThread在内部创建了一个消息队列需要通过Handler的消息方式来通知HandleThread执行一个具体的任务,HandlerThread的run方法是一个无限循环因此在不使用是调用quit或者quitSafely方法终止线程的执行。HandleTread的具体使用场景是IntentService。




2.4 IntentService


IntentService继承自Service并且是一个抽象的类因此使用它时就必须创建它的子类,IntentService可用于执行后台耗时的任务,当任务执行完毕后就会自动停止。IntentService是一个服务因此它的优先级要比线程高并且不容易被系统杀死,因此可以利用这个特点执行一些高优先级的后台任务,它的实现主要是HandlerThread和Handler,这点可以从onCreate方法中了解。

//IntentService#onCreate
@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

当IntentService第一次被启动时回调用onCreate方法,在onCreate方法中会创建HandlerThread,然后使用它的Looper创建一个Handler对象ServiceHandler,这样通过mServiceHandler把消息发送到HandlerThread中执行。每次启动IntentService都会调用onStartCommand,IntentService在onStartCommand中会处理每个后台任务的Intent。

//IntentService#onStartCommand
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

//IntentService#onStart
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

onStartCommand是如何处理外界的Intent的?


在onStartCommand方法中进入了onStart方法,在这个方法中IntentService通过mserviceHandler发送了一条消息,然后这个消息会在HandlerThread中被处理。mServiceHandler接收到消息后会把intent传递给onHandlerIntent(),这个intent跟启动IntentService时的startService中的intent是一样的,因此可以通过这个intent解析出启动IntentService传递的参数是什么然后通过这些参数就可以区分具体的后台任务,这样onHandleIntent就可以对不同的后台任务做处理了。当onHandleIntent方法执行结束后IntentService就会通过stopSelf(int startId)方法来尝试停止服务,这里不用stopSelf()的原因是因为这个方法被调用之后会立即停止服务但是这个时候可能还有其他消息未处理完毕,而采用stopSelf(int startId)方法则会等待所有消息都处理完毕后才会终止服务。调用stopSelf(int startId)终止服务时会根据startId判断最近启动的服务的startId是否相等,相等则立即终止服务否则不终止服务。


每执行一个后台任务就会启动一次intentService,而IntentService内部则通过消息的方式向HandlerThread请求执行任务,Handler中的Looper是顺序处理消息的,这就意味着IntentService也是顺序执行后台任务的,当有多个后台任务同时存在时这些后台任务会按照外界发起的顺序排队执行。


3.Android中的线程池


线程池的优点:





    • 线程池中的线程可重复使用,避免因为线程的创建和销毁带来的性能开销;

    • 能有效控制线程池中的最大并发数避免大量的线程之间因互相抢占系统资源导致的阻塞现象;

    • 能够对线程进行简单的管理并提供定时执行以及指定间隔循环执行等功能。




Android的线程池的概念来自于Java中的Executor,Executor是一个接口,真正的线程的实现是ThreadPoolExecutor,它提供了一些列参数来配置线程池,通过不同的参数可以创建不同的线程池。


3.1 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;
}

ThreadPoolExecutor是线程池的真正实现,它的构造函数中提供了一系列参数,先看一下每个参数的含义:





    • corePoolSize:线程池的核心线程数,默认情况下核心线程会在线程池中一直存活即使他们处于闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeOut置为true那么闲置的核心线程在等待新的任务到来时会有超时策略,超时时间由keepAliveTime指定,当等待时间超过keepAliveTime设置的时间后核心线程就会被终止。

    • maxinumPoolSize:线程池中所能容纳的最大线程数,当活动线程达到做大数量时后续的新任务就会被阻塞。

    • keepAliveTime:非核心线程闲置时的超时时长,超过这个时长非核心线程就会被回收。

    • unit:用于指定超时时间的单位,常用单位有毫秒、秒、分钟等。

    • workQueue:线程池中的任务队列,通过线程池中的execute方法提交的Runnable对象会存储在这个参数中。

    • threadFactory:线程工厂,为线程池提供创建新的线程的功能。

    • handler:这个参数不常用,当线程池无法执行新的任务时,这可能是由于任务队列已满或者无法成功执行任务,这个时候ThreadPoolExecutor会调用handler的rejectExecution方法来通知调用者。




ThreadPoolExecutor执行任务时大致遵循如下规则:





    1. 如果线程池中的线程数量没有达到核心线程的数量那么会直接启动一个核心线程来执行任务;

    2. 如果线程池中线程数量已经达到或者超过核心线程的数量那么会把后续的任务插入到队列中等待执行;

    3. 如果任务队列也无法插入那么在基本可以确定是队列已满这时如果线程池中的线程数量没有达到最大值就会立刻创建非核心线程来执行任务;

    4. 如果非核心线程的创建已经达到或者超过线程池的最大数量那么就拒绝执行此任务,同时ThreadPoolExecutor会通过RejectedExecutionHandler抛出异常rejectedExecution。




3.2线程池的分类



  • FixedThreadPool:它是一种数量固定的线程池,当线程处于空闲状态时也不会被回收,除非线程池被关闭。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有空闲线程出来。FixedThreadPool只有核心线程并且不会被回收因此它可以更加快速的响应外界的请求。

  • CacheThreadPool:它是一种线程数量不定的线程池且只有非核心线程,线程的最大数量是Integer.MAX_VALUE,当线程池中的线程都处于活动状态时如果有新的任务进来就会创建一个新的线程去执行任务,同时它还有超时机制,当一个线程闲置超过60秒时就会被回收。

  • ScheduleThreadPool:它是一种拥有固定数量的核心线程和不固定数量的非核心线程的线程池,当非核心线程闲置时会立即被回收。

  • SignleThreadExecutor:它是一种只有一个核心线程的线程池,所有任务都在同一个线程中按顺序执行。

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

我从 Android 官方 App 中学到了什么?

最近 Android 官方开源了一个新的 App: Now in Android ,这个 App 主要展示了其他 App 可能没有的一些最佳实践、架构设计、以及完整的线上 App (后面会发布到 Google Play 商店中)解决方案,其次是帮助开发者及时了...
继续阅读 »

最近 Android 官方开源了一个新的 App: Now in Android ,这个 App 主要展示了其他 App 可能没有的一些最佳实践、架构设计、以及完整的线上 App (后面会发布到 Google Play 商店中)解决方案,其次是帮助开发者及时了解到自己感兴趣的 Android 开发领域。现在已经在 GitHub 中开源。


通过这篇文章你可以了解到 Now in Android 的应用架构:分层、关键类以及他们之间的交互。


目标&要求


App 的架构目标有以下几点:



  • 尽可能遵循 官方架构指南

  • 易于开发人员理解,没有什么太实验性的特性。

  • 支持多个开发人员在同一个代码库上工作。

  • 在开发人员的机器上和使用持续集成 (CI) 促进本地和仪器测试。

  • 最小化构建时间。


架构概述


App 目前包括 Data layerUI layerDomain layer 正在开发中。


Diagram showing overall app architecture


该架构遵循单向数据流的响应式编程方式。Data Layer 位于底层,主要包括:



  • UI Layer 需对 Data Layer 的变化做出反应。

  • 事件应向下流动。

  • 数据/状态应向上流动。


数据流是采用 Kotlin Flows 来实现的。


示例:在 For you 页面展示新闻信息


App 首次运行的时候,会尝试从云端加载新闻列表(选择 stagingrelease 构建变体时,debug 构建将使用本地数据)。加载后,这些内容会根据用户选择的兴趣显示给用户。


下图详细展示了事件以及数据是流转的。


Diagram showing how news resources are displayed on the For You screen


下面是每一步的详细过程。 Code 列中的内容是对应的代码,可以下载项目后在 Android Studio 查看。






























































步骤描述Code
1App 启动的时候,WorkManager 的同步任务会把所有的 Repository 添加到任务队列中。SyncInitializer.create
2初始状态会设置为 Loading,这样会在 UI 页面上展示一个旋转的动画。ForYouFeedState.Loading
3WorkManager 开始执行 OfflineFirstNewsRepository 中的同步任务,开始同步远程的数据源。SyncWorker.doWork
4OfflineFirstNewsRepository 开始调用 RetrofitNiaNetwork 开始使用 Retrofit 进行真正的网络请求。OfflineFirstNewsRepository.syncWith
5RetrofitNiaNetwork 调用云端接口。RetrofitNiaNetwork.getNewsResources
6RetrofitNiaNetwork 接收到远程服务器返回的数据。RetrofitNiaNetwork.getNewsResources
7OfflineFirstNewsRepository 通过 NewsResourceDao 将远程数据更新(增删改查)到本地的 Room 数据库中。OfflineFirstNewsRepository.syncWith
8NewsResourceDao 中的数据发生变化的时候,其会被更新到新闻的数据流(Flow)中。NewsResourceDao.getNewsResourcesStream
9OfflineFirstNewsRepository 扮演数据流中的 中间操作符, 将 PopulatedNewsResource (数据层内部数据库的一个实体类) 转换成公开的 NewsResource 实体类供其他层使用。OfflineFirstNewsRepository.getNewsResourcesStream
10ForYouViewModel 接收到 Success 成功, ForYouScreen 会使用新的 State 来渲染页面。页面将会展示最新的新闻内容。ForYouFeedState.Success

Data Layer


数据层包含 App 数据以及业务逻辑,会优先提供本地离线数据,它是 App 中所有数据的唯一信源。


Diagram showing the data layer architecture


每个 Repository 中都有自己的实体类(model/entity)。如,TopicsRepository 包含 Topic 实体类, NewsRepository 包含 NewsResource 实体类。


Repository 是其他层的公共的 API,提供了访问 App 数据的唯一途径。Repository 通常提供一种或多种数据读取和写入的方法。


读取数据


数据通过数据流提供。这意味着 Repository 的调用者都必须准备好对数据的变化做出响应。数据不会作为快照(例如 getModel )提供,因为无法确保它在使用时仍然有效。


Repository 以本地存储数据作为单一信源,因此从实例读取时不会出现错误。但是,当尝试将本地存储中的数据与云端数据进行合并时,可能会发生错误。有关错误的更多信息,请查看下面的数据同步部分。


示例:读取作者信息


可以用过订阅 AuthorsRepository::getAuthorsStream 发出的流来获得 List<Authors> 信息。每当作者列表更改时(例如,添加新作者时),更新后的 List<Author> 的内容都会发送到数据流中。如下:

class OfflineFirstTopicsRepository @Inject constructor(  
private val topicDao: TopicDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences,
) : TopicsRepository {

// 监听 Room 数据的变化,当数据发生变化的时候,调用者就会收到对应的数据
override fun getTopicsStream(): Flow<List<Topic>> = topicDao.getTopicEntitiesStream().map {
it.map(TopicEntity::asExternalModel)
}

// ...
}

写入数据


为了写入数据,Repository 库提供了 suspend 函数。由调用者来确保它们在合适的 scope 中被执行。


示例: 关注 Topic


调用 TopicsRepository.setFollowedTopicId 将用户想要关注的 topic id 传入即可。


OfflineFirstTopicsRepository 中定义:

interface TopicsRepository : Syncable {

suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)

}

ForYouViewModel 中定义:

class ForYouViewModel @Inject constructor(
private val topicsRepository: TopicsRepository,
// ...
) : ViewModel() {
// ...

fun saveFollowedInterests() {
// ...
viewModelScope.launch {
topicsRepository.setFollowedTopicIds(inProgressTopicSelection)
// ...
}
}
}

数据源(Data Sources)


Repository 可能依赖于一个或多个 DataSource。例如,OfflineFirstTopicsRepository 依赖以下数据源:



























名称使用目的
TopicsDaoRoom/SQLite持久化和 Topics 相关的关系型数据。
NiaPreferencesProto DataStore持久化和用户相关的非结构化偏好数据,主要是用户感兴趣的 Topics 内容。这里使用的是 .proto 文件。
NiANetworkRetrofit云端以 JSON 形式提供对应的 Topics 数据。

数据同步


Repository 的职责之一就是整合本地数据与云端数据。一旦从云端返回数据就会立即将其写入本地数据中。更新后的数据将会从本地数据(Room)中发送到相关的数据流中,调用者便可以监听到对应的变化。


这种方法可确保应用程序的读取和写入关注点是分开的,不会相互干扰。


在数据同步过程中出现错误的情况下,应采用对应的回退策略。App 中是经由 SyncWorker 代理给 WorkManager 的。 SyncWorkerSynchronizer 的实现类。


可以通过 OfflineFirstNewsRepository.syncWith 来查看数据同步的示例,如下:

class OfflineFirstNewsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao,
private val episodeDao: EpisodeDao,
private val authorDao: AuthorDao,
private val topicDao: TopicDao,
private val network: NiANetwork,
) : NewsRepository {

override suspend fun syncWith(synchronizer: Synchronizer) =
synchronizer.changeListSync(
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
// ...
}
)
}

UI Layer


UI Layer 包含:



ViewModelRepository 接收数据流并将其转换为 UI State。UI 元素根据 UI State 进行渲染,并为用户提供了与 App 交互的方式。这些交互作为事件(UI Event)传递到对应的 ViewModel 中。


Diagram showing the UI layer architecture


构建 UI State


UI State 一般是通过接口和 data class 来组装的密封类。State 对象只能通过数据流的转换发出。这种方法可确保:



  • UI State 始终代表底层应用程序数据 - App 中的单一信源。

  • UI 元素处理所有可能的 UI State


示例:For You 页面的新闻列表


For You 页面的新闻列表数据源是 ForYouFeedState ,他是一个 sealed interface 类,包含 LoadingSuccess 两种状态:



  • Loading 表示数据正在加载。

  • Success 表示数据加载成功。Success 状态包含新闻资源列表。
sealed interface ForYouFeedState {
object Loading : ForYouFeedState
data class Success(val feed: List<SaveableNewsResource>) : ForYouFeedState
}

ForYouScreen 中会处理 feedState 的这两种状态,如下:

private fun LazyListScope.Feed(
feedState: ForYouFeedState,
//...
) {
when (feedState) {
ForYouFeedState.Loading -> {
// show loading
}
is ForYouFeedState.Success -> {
// show feed
}
}
}

将数据流转换为 UI State


ViewModel 从一个或者多个 Repository 中接收数据流当做冷 。将他们一起 组合 成单一的 UI State。然后使用 stateIn 将冷流转换成热流。转换的状态流使 UI 元素可以读取到数据流中最后的状态。


示例: 展示已关注的话题及作者


InterestsViewModel 暴露 StateFlow<FollowingUiState> 类型的 uiState 。通过组合 4 个数据流来创建热流:



  • 作者列表

  • 已关注的作者 ID 列表

  • Topics 列表

  • 已关注 Topics 列表的 IDs


Author 转换为 FollowableAuthorFollowableAuthor 是对 Author 的包装类, 添加了当前用户是否已经关注了作者。对 Topic 也做了相同转换。 如下:

    val uiState: StateFlow<InterestsUiState> = combine(
authorsRepository.getAuthorsStream(),
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getTopicsStream(),
topicsRepository.getFollowedTopicIdsStream(),
) { availableAuthors, followedAuthorIdsState, availableTopics, followedTopicIdsState ->

InterestsUiState.Interests(
// 将 Author 转换为 FollowableAuthor,FollowableAuthor 是对 Author 的包装类,
// 添加了当前用户是否已经关注了作者
authors = availableAuthors
.map { author ->
FollowableAuthor(
author = author,
isFollowed = author.id in followedAuthorIdsState
)
}
.sortedBy { it.author.name },
// 将 Topic 转换为 FollowableTopic,同 Author
topics = availableTopics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in followedTopicIdsState
)
}
.sortedBy { it.topic.name }
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)

两个新的列表创建了新的 FollowingUiState.Interests UiState 暴露给 UI 层。


处理用户交互


用户对 UI 元素的操作通过常规的函数调用传递给 ViewModel ,这些方法作为 lambda 表达式传递给 UI 元素。


示例:关注话题


InterestsScreen 通过 followTopic lambda 表达式传递事件,然后会调用到 InterestsViewModel.followTopic 函数。当用户点击关注话题的时候,函数将会被调用。然后 ViewModel 就会通过通知 TopicsRepository 处理对应的用户操作。


如下在 InterestsRoute 中关联 InterestsScreenInterestsViewModel

@Composable  
fun InterestsRoute(
modifier: Modifier = Modifier,
navigateToAuthor: (String) -> Unit,
navigateToTopic: (String) -> Unit,
viewModel: InterestsViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val tabState by viewModel.tabState.collectAsState()

InterestsScreen(
uiState = uiState,
tabState = tabState,
followTopic = viewModel::followTopic,
// ...
)
}

@Composable
fun InterestsScreen(
uiState: InterestsUiState,
tabState: InterestsTabState,
followTopic: (String, Boolean) -> Unit,
// ...
) {
//...
}


扩展阅读


本文主要是根据 Now in Android 中的 Architecture Learning Journey 整理而得,感兴趣的可以进一步阅读原文。除此之外,还可以进一步学习 Android 官方相关的资料:



关于架构指南部分,我之前也整理了部分对应的解读部分,大家可以移步查看



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

程序员能有什么好出路?

关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。 如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的...
继续阅读 »

关于职场的焦虑无处不在,而这些文章也加重了我们的焦虑。就我个人而言,我也仔细想过这个问题,其实从本质上来说,只是个“竞争力”的问题。


如果你觉得自己没有竞争力了,那么你就会焦虑,而你又将焦虑的原因归结于一个你没办法改变的问题,那就是“年龄”。于是一个逻辑自洽的描述出来了:

我30岁了,没啥竞争力,未来何去何从?

出路耶


我从事这个行业,其实是个人挺喜欢编程的,觉得编程是一件挺舒心的事情,所以没有考虑过换行。周围其实有一些同事,离开了这个行当,有一些赚了更多的钱,也有一些日子过的更不舒心,这里不予置评。


我简单的叙述一些可能的出路,这些出路没什么对错的区别,只是在我们人生抉择中,希望你能看到更多的选项。


技术深造


如果你在技术上有优势,这是一条可以走通的路子,未来的方向大致是“架构师”、“技术顾问”等等。这需要你有一些大型项目的经验,所以一些在大型公司就业的程序员,天然的拥有更多的机会。


通常技术深造主要是两部分:



  1. 技术视野,你需要一定的知识广度,对常用技术有深刻的理解,对部分不常用技术也要熟悉。

  2. 技术能力,有的时候,亲自动手能力、解决问题能力会很重要。


项目管理


很多程序员转行做了项目管理,其实在我们的日常工作中,项目管理一直伴随着我们,时长日久,我们对项目管理会变的更熟悉一些。这也造成了一些错觉,让我们觉得项目管理没那么难,“我去我也行”。


但是,项目管理从来不是一项普通的工作,相对于程序员,项目管理人员面临的环境会更加复杂。



  1. 面对客户。有时候,会遇见一些喜欢刁难我们的客户的。

  2. 面对团队。团队也可能不和谐。

  3. 计划乱了、工期排期、风险控制、质量管理、干系人管理等等专业知识。


自由职业


依赖于自己过硬的技术,可以承接一些外包的项目,成为一名自由的外包人员。



  1. 你的人际关系会很重要。周围有一些能打单的朋友,会让你工作的很舒服。

  2. 把事情做好,赢得信赖。

  3. 来自第三方平台的外包项目还是比较坑的,尽量做熟人生意。


跑单


当然,你在行业内可能会认识不少的朋友,他们的手里可能有些业务需要外包人员进行开发,那么拿下这些合同,找到自己朋友里面有时间做私活的人,然后我完成它。



  1. 你的人际关系更为重要。通常,这会给你带来财富。

  2. 做好自己的品牌,赢得认可,那么就有赢得钞票的机会。


插件独立开发者


一个人开发一个应用,然后上架,成功率是很低的。所以依托于平台,做一些平台内的插件,然后依托于平台推广,那么成功的几率会大一些。



  1. 你的技术能力很重要,毕竟没有专门的测试人员进行测试。

  2. 你选择的平台很重要,比如跨境电商、钉钉、微信、谷歌浏览器等等。

  3. 更加重要的是,你要对这个方向感兴趣。


独立开发者


如果你财富自由了,又喜欢编程,可以成为一名伟大的独立开发者,你脑海中的任何想法,都可以通过双手变为现实。



  1. 因为热爱,所以你会有更多的可能。

  2. 能力足够,可以参与开源的基金会,参与一些开源项目。

  3. 如果财富没自由,那也不影响我们在闲暇时间里追逐我们的梦想。


团购


IT行业是一个挺特殊的团体,他们的某些消费习惯趋于雷同,针对这些消费习惯和爱好,做一些团购,相信会赚到不少钱。



  1. 还是人际关系。

  2. 你喜欢做这些事情,从免费到收费循序渐进。

  3. 记住,双赢才能长久,IT行的聪明人是比较多的。


大公司养老团


找个大的,稳定的公司养老,但是也要留好退路,居安思危。


其他


比如炒股、搞理财的、做导游的、创业的……


每个人都会有自己的选择,有的人做好了准备,有的人还懵懵懂懂,2023年的行情如何还未可知,希望能长风破浪吧


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

JS中的高阶函数

web
JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。 具体来说,高阶函数可以...
继续阅读 »

JavaScript中的高阶函数是指可以接受其他函数作为参数或者返回一个函数作为结果的函数。这种函数在函数式编程范式中特别常见,允许用一种更抽象、更灵活的方式处理代码。在JavaScript中,函数可以像其他数据类型一样被传递和操作。



具体来说,高阶函数可以有以下几种形式:



  1. 接受函数作为参数的高阶函数


function map(array, fn) {
let result = [];
for (let i = 0; i < array.length; i++) {
result.push(fn(array[i]));
}
return result;
}

let numbers = [1, 2, 3, 4, 5];
let squaredNumbers = map(numbers, function(x) {
return x * x;
});
console.log(squaredNumbers); // [1, 4, 9, 16, 25]

在上面的例子中,map函数接受一个数组和一个函数作为参数,然后使用该函数对数组中的每个元素进行转换,并返回转换后的结果。




  1. 返回函数的高阶函数


function multiplyBy(n) {
return function(x) {
return x * n;
};
}

let double = multiplyBy(2);
let triple = multiplyBy(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30

在上面的例子中,multiplyBy函数返回一个函数,该函数可以将传入的参数乘以n。我们可以使用multiplyBy函数创建一个新的函数,然后使用该函数对不同的值进行乘法运算。




  1. 同时接受和返回函数的高阶函数


function compose(f, g) {
return function(x) {
return f(g(x));
};
}

function square(x) {
return x * x;
}

function addOne(x) {
return x + 1;
}

let addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(3)); // 16

在上面的例子中,compose函数接受两个函数作为参数,然后返回一个新的函数,该函数首先对输入值应用g函数,然后将结果传递给f函数,并返回f(g(x))的结果。我们可以使用compose函数创建一个新的函数,该函数可以将其他两个函数的功能组合在一起,以实现更复杂的操作。



其实,即使是业务代码中也会有很多用到高阶函数的地方,比如数组的迭代方法(map、filter、reduce等)、定时器(setTimeout和setInterval),还有比较典型的函数柯理化、函数组合(compose)、偏函数等,通过使用高阶函数,我们可以将常见的操作抽象出来,并将它们作为可重用的函数进行封装,从而使代码更加简洁、灵活和易于维护。





在使用高阶函数时,有时候需要注意回调函数中的上下文问题。如果回调函数中的this关键字不是指向我们期望的对象,就会导致程序出现错误。为了解决这个问题,可以使用bindapplycall等方法来明确指定回调函数的上下文。


let obj = {
value: 0,
increment: function() {
this.value++;
}
};

let arr = [1, 2, 3, 4, 5];

arr.forEach(obj.increment.bind(obj));
console.log(obj.value); // 5

在上面的例子中,obj.increment.bind(obj)会返回一个新函数,该函数会将this关键字绑定到obj对象上。我们可以使用这个新函数来作为forEach方法的回调函数,以确保increment方法的上下文指向obj对象。



其余还有诸如函数副作用问题、内存占用问题和性能问题等。为了解决这些问题,可以使用一些优化技巧,比如明确指定回调函数的上下文、使用纯函数、使用函数柯里化或函数组合等。这些技巧可以帮助我们更加灵活地使用高阶函数,并提高代

作者:施主来了
来源:juejin.cn/post/7232838211030302777
码的性能和可维护性。

收起阅读 »

函数实现单例模式

web
单例模式 一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。 class Foo { static instance; static init() { if (!this.instance) t...
继续阅读 »

wallhaven-gpqye7.jpg


单例模式


一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。


class Foo {
static instance;
static init() {
if (!this.instance) this.instance = new Foo();
return this.instance;
}
constructor() {}
}

// 将单例实例化 并暴露出去
export default Foo.init()


如此,我们就实现了简单的单例模式,并且在其他文件引入的时候已经是实例化过一次的了,或者交由用户者自行调用 init 也是可以的



函数实现


而在函数的实现上,其实本身类就是函数的某种抽象,如果去掉这个 new 的话,单纯用函数又是怎么做的呢?


let ipcMainInstance;
export default () => {
const init = () => {
return {
name: "phy",
hobby: "play games"
};
};

return () => {
if (!ipcMainInstance) {
ipcMainInstance = init();
}
return ipcMainInstance;
};
};

使用


const ipcInit = createIpc();
ipcInit();


因为我们使用的是二阶函数进行 init,所以写法上是二次调用才是 init,每个人的设计写法不一样。



然而这种写法上,每次都要写一个 init 方法进行单例实例化的包裹,这明显是一个重复工作,我们是否可以将 init 方法独立成一个函子,让他帮我们自动将我们传进去的函数进行处理,返回来的就是一个单例模式的函数呢?


抽象单例模式函子


// 非void返回值
type NonVoidReturn<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R extends void
? never
: T
: any;

/**
* 创建单例模式的函子
* @param {function} fn
* @returns {any} fn调用的返回值 必须得有return 可推断
*/

const createSgp = <T extends (...args: any) => any>(fn: NonVoidReturn<T>) => {
let _instance: undefined | ReturnType<T>;

return () => {
if (!_instance) {
_instance = fn();
}
return _instance;
};
};

export default createSgp;


使用上



import createSgp from "./createSgp";

const useAuto = () => {
let count = 0;

const setCount = (num: number) => {
count = num;
};

const getCount = () => count

return {
getCount,
setCount
};
};

// 将其处理成单例模式 并且暴露出去
export default createSgp(useAuto);


如此我们就完成了单例模式的包裹处理,并且是一个单例模式的函数。



对于hooks使用单例模式函数的问题


其实上面的操作看起来很酷,实际上很少会用到,因为你得考虑到,我用单例模式的意义是什么,如果这个函数只需要调用一次,那么就有必要用单例模式,但是hooks一般用到的时候,都属于操作性逻辑,尽量不应在hooks里面去做hooks初始化时有函数自执行调用,这个调用应该交由用户去做,我是这么理解hooks的,而这也就导致,hooks不应该用单例了,而且hooks用单例会有bug,请看下面的代码:


  let count = 0;
const useCount = {
count,
add(num){
count += num
}
}

这里我就一次简化useCount的return出来的东西,那么我们思考下,如果说,这个add在外部调用了,那么这个count会变吗?答案是不会,为什么呢?



因为当前add操作的count,是外部的count,并不是return对象的count,这句话可能很绕,但是仔细思考,一开始useCount(),他return的count是长什么样,此时,他其实就是数字0,那么,add改的count真的是这个return对象的count吗?相信说到这里,你就懂为什么了。



那我如果真的要联动到这个count,怎么做呢?


  const useCount = {
count: 0,
add(num){
this.count += num
}
}


答案是,用到this,此时这个add操作的count就是此时return 对象的count了,而这也跟类一个原理了,因为类更改的成员属性,都是实例对象本身的,而不是外部的,所以,他能更新上。这个问题,也是后面我发现的,所以以此记录一下。



作者:phy_lei
来源:juejin.cn/post/7232499216529834039
收起阅读 »

小程序轮播图的高度如何与图片高度保持一致

web
一、存在现象 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例: 这是在iPhone Xr上的显示效果...
继续阅读 »

一、存在现象




  • 在原生小程序中,我们从服务器获取轮播图的数据,这些图片的数据都是有一定宽高的,我们需要去适配这些图片在不同手机上显示时的宽高,不然的话,在不同的设备上就会不同的效果,也就出现了所谓的bug,如下案例:




  • 这是在iPhone Xr上的显示效果:轮播图的指示点显示正常
    image.png




  • 这是在iPhone 5上的显示效果:轮播图的指示点就到图片下方去了
    image.png




二、解决方法


思路



  • 在图片加载完成后,获取到图片的高度,获取到之后进行赋值。这样的话,我们需要使用image标签的bindload属性,当图片加载完成时触发


image.png



  • 获取图片高度,可以当做获取这个轮播图组件的高度,这组件是小程序界面上的一个节点,可以使用获取界面上的节点信息APIwx.createSelectorQuery()来获取


const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect()
query.selectViewport().scrollOffset()
query.exec(function(res){
res[0].top // #the-id节点的上边界坐标
res[1].scrollTop // 显示区域的竖直滚动位置
})



  • 节点信息查询 API 可以用于获取节点属性、样式、在界面上的位置等信息。最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。如下图所示,里面有我们所需要的height,我们将这个height赋值给swiper组件,再令image标签mode="widthFix",即可自动适应轮播图高度和图片的高度保持一致



    • widthFix:缩放模式,宽度不变,高度自动变化,保持原图宽高比不变

    • HeightFix:缩放模式,高度不变,宽度自动变化,保持原图宽高比不变




  • 这是iPhone Xr上的数据,height:152.4375
    image.png




  • 这是iPhone 5上的数据,height:118.21875
    image.png




实现



  • wxml:轮播图


<swiper class="swiper" autoplay indicator-dots circular interval="{{4000}}" style="height: {{swiperHeight}}px;">
<block wx:for="{{banners}}" wx:key="bannerId">
<swiper-item class="swiper-item">
<image class="swiper-image" src="{{item.pic}}" mode="widthFix" bindload="getSwiperImageLoaded"></image>
</swiper-item>
</block>
</swiper>


  • js:只展示获取图片高度的代码,像获取轮播图数据代码已省略


Page({
data: {
swiperHeight: 0, // 轮播图组件初始高度
},

// 图片加载完成
getSwiperImageLoaded() {
// 获取图片高度
const query = wx.createSelectorQuery();
query.select(".swiper-image").boundingClientRect();
query.exec((res) => {
this.setData({ swiperHeight: rect.height });
});
},
})


  • 在上述代码中getSwiperImageLoaded方法也可以进行抽离到utils中成为一个工具函数,并用Promise进行返回,方便其他地方需要使用到


export default function (selector) {
return new Promise((resolve) => {
const query = wx.createSelectorQuery();
query.select(selector).boundingClientRect();
query.exec(resolve)
});
}


  • 所以在上述的实现代码中getSwiperImageLoaded方法可以进行如下的优化:


getSwiperImageLoaded() {
// 优化
queryRect(".swiper-image").then((res) => {
const rect = res[0];
this.setData({ swiperHeight: rect.height });
});
},


  • 如此一来,在iPhone 5上的轮播图组件展示也正常


image.png



  • 最后,因为获取的是轮播图,那么获取的数据就不止一条,按以上代码逻辑,获取到多少条数据就会执行多少遍setData赋值操作,所以可以考虑使用防抖或者节流进行进一步优化。


作者:晚风予星
来源:juejin.cn/post/7232625387296129080
收起阅读 »

CSS小技巧之圆形虚线边框

web
虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码: border-style: dotted dashed solid double; 这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的...
继续阅读 »

虚线相信大家日常都用的比较多,常见的用法就是使用 border-style 控制不同的样式,比如设置如下边框代码:


border-style: dotted dashed solid double;

这将设置顶部的边框样式为点状,右边的边框样式为虚线,底部的边框样式为实线,左边的边框样式为双线。如下图所示:



border-style 除了上面所支持的样式还有 groove ridge inset outset 3D相关的样式设置,关于 border-style 的相关使用本文并不过多介绍,有兴趣的可以看官方文档。本文主要介绍使用CSS渐变实现更自定义化的虚线边框,以满足需求中的特殊场景使用。如封面图所示的6种情况足以体现足够自定义的边框样式,接下来看实现方式。


功能分析


基于封面图分析实现这类虚线边框应该满足一下几个功能配置:



  • 虚线的点数量

  • 虚线的颜色,可以纯色,多个颜色,渐变色

  • 虚线的粗细程度

  • 虚线点之间的间隔宽度


由于我们是自定义的虚线边框,所以尽可能不增加额外的元素,所以虚线的内容使用伪元素实现,然后使用定位覆盖在元素内容的上方,那么你肯定有疑问了,既然是覆盖在元素的上方,那不上遮挡了元素本身吗?



来到本文自定义圆形虚线边框的关键部分,这里我们使用CSS mask 实现,并配合使用 -webkit-mask-composite: source-in 显示元素本身的内容。



-webkit-mask-composite: 属性指定了将应用于一个元素的多个蒙版图像合成显示。当一个元素存在多重 mask 时,我们就可以运用 -webkit-mask-composite 进行效果叠加。



代码实现


首先基于上面分析的几个功能配置进行变量定义,方便后续更改变量值即可调整边框样式。


--n:20;   /* 控制虚线数量 */
--d:8deg; /* 控制虚线之间的距离 */
--t:5px; /* 控制虚线的粗细 */
--c:red; /* 控制虚线的颜色 */

对应不同的元素传入不同的值:


<div class="box" style="--n:3;--t:8px;--d:10deg;--c:linear-gradient(45deg,red,blue)">3</div>
<div class="
box" style="--n:6;--t:12px;--d:20deg;--c:green">6</div>

然后给伪元素设置基础的样式,定位,背景色,圆角等。


.box::after {
content: "";
position: absolute;
border-radius: 50%;
background: var(--c);
}

按不同的元素传入不同的背景色,最终的效果是这样的。



继续设置在mask中设置一个重复的锥形渐变 repeating-conic-gradient,代码如下:


repeating-conic-gradient(
from calc(var(--d)/2),
#000 0 calc(360deg/var(--n) - var(--d)),
#0000 0 calc(360deg/var(--n))
)



  • from calc(var(--d)/2) 定义了渐变的起点,以虚线之间的距离除以2可以让最终有对称的效果




  • #000 0 calc(360deg/var(--n) - var(--d)):定义了第一个颜色为黑色(#000),起点位置为0,终止位置为360deg/var(--n) - var(--d)度,基于虚线之间的距离和虚线的个数计算出每段虚线的渐变终止位置




  • #0000 0 calc(360deg/var(--n)):定义了第二个颜色为透明色,起点位置为0,终止位置为基于虚线的个数计算,这样与上一个颜色的差即是 --d 的距离,也就是我们控制虚线之间的距离。




基于上述代码现在的界面是如下效果:



上面有提到 -webkit-mask-composite 是应用于一个元素的多个蒙版图像合成显示,所以我们这里需要在mask中再增加一个蒙板进行合成最终的效果。


增加以下代码到mask中:


linear-gradient(#0000 0 0) content-box

注意这里使用了content-box作为背景盒模型,这意味着背景颜色只会应用到元素的内容区域,这段代码将创建一个只在元素内容区域的水平线性渐变背景,且是完全透明的背景色。


为什么是内容区域,因为这里和padding有关联,我们将定义的控制虚线的粗细 --t:5px; 应用到了伪元素的 padding 中。


padding: var(--t);

这样刚刚新增的透明背景就只会应用到下图的蓝色内容区域,再结合 -webkit-mask-composite,即``只剩下 padding 部分的内容,也就是我们的自定义边框部分。



增加以下代码:


-webkit-mask-composite: source-in;

即是最终的效果,因为这里增加的mask背景是透明色,这里 -webkit-mask-composite 的属性不限制使用 source-in, 其他的好几个都是一样的效果,有兴趣的可以了解了解。



都已经到这一步了,是不是应该再增加一些效果呢,给这个圆形的边框增加动起来的效果看看,增加一个简单的旋转动画 animation: rotate 5s linear infinite;,这样看着是不是更有感觉,适用的场景就多了。



码上掘金在线预览:



最后


到此整体代码实现就结束了,看完是不是感觉挺简单的,基于伪元素设置锥形渐变 repeating-conic-gradient并配合-webkit-mask-composite实现自定义圆形虚线边框的效果。这里是设置了 border-radius:50%; 圆角最终呈现的是圆形,有兴趣的可以更改CSS代码试试其他的形状颜色间距等。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


参考



codepen.io/t_afif/pen/…



作者:南城FE
来源:juejin.cn/post/7233052510553522213
收起阅读 »

我竟然完美地用js实现默认的文本框粘贴事件

web
前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状 废话连篇 默认情况对一个文本框粘贴,应该会有这样的功能: 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后 将选中的文字替换成粘贴的文本 但是由于需求,我们需要拦截粘...
继续阅读 »

前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状



废话连篇


默认情况对一个文本框粘贴,应该会有这样的功能:



  1. 粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后

  2. 将选中的文字替换成粘贴的文本


但是由于需求,我们需要拦截粘贴的事件,对剪贴板的文字进行过滤,这时候粘贴的功能都得自己实现了,而一旦自己实现,上面2个功能就不见了,我们就需要还原它。


面对这样的需求,我们肯定要控制移动光标,可是现在的网上环境真的是惨,千篇一律的没用代码...于是我就发表了这篇文章。


先上代码


    <textarea id="text" style="width: 996px; height: 423px;"></textarea>
<script>
// 监听输入框粘贴事件
document.getElementById('text').addEventListener('paste', function (e) {
e.preventDefault();
let clipboardData = e.clipboardData.getData('text');
// 这里写你对剪贴板的私货
let tc = document.querySelector("#text");
tc.focus();
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
if(tc.selectionStart != tc.selectionEnd){
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionEnd)
}else{
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
}

// 重新设置光标位置
tc.selectionEnd =tc.selectionStart = start
});
</script>


怎么理解上述两个功能?
第一个解释:
比如说现在文本框有:



染念真的很生气



如果我们现在在真的后面粘贴不要,变成



染念真的不要很生气|



拦截后的光标是在生气后面,但是我们经常使用发现,光标应该出现在不要的后面吧!
就像这样:



染念真的不要|很生气



第2个解释:



染念真的不要很生气



我们全选真的的同时粘贴求你,拦截后会变成



染念真的求你不要很生气|



但默认应该是:



染念求你|不要很生气



代码分析


针对第2个问题,我们应该先要获取默认的光标位置在何处,tc.selectionStart是获取光标开始位置,tc.selectionEnd是获取光标结束位置。
为什么这里我写了一个判断呢?因为默认时候,我们没有选中一块区域,就是把光标人为移动到某个位置(读到这里,光标在位置后面,现在人为移动到就是前面,这个例子可以理解不?),这个时候两个值是相等的。



233|333


^--- ^


1-- - 4


tc.selectionEnd=4,tc.selectionStart = 4



如果相等,说明就是简单的定位,tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart); ,tc.value.substring(0,tc.selectionStart)获取光标前的内容,tc.value.substring(tc.selectionStart)是光标后的内容。
如果不相等,说明我们选中了一个区域(光标选中一块区域说明我们选中了一个区域),代码只需要在最后获取光标后的内容这的索引改成tc.selectionEnd



|233333|


^----- ^


1----- 7


tc.selectionEnd=7,tc.selectionStart = 1



在获取光标位置之前,我们应该先使用tc.focus();聚焦,使得光标回到文本框的默认位置(最后),这样才能获得位置。
针对第1个问题,我们就要把光标移动到粘贴的文本之后,我们需要计算位置。
获得这个位置,一定要在tc.value重新赋值之前,因为这样的索引都没有改动。
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;这个代码和上面解释重复,很简单,我就不解释了。
最后处理完了,重新设置光标位置,tc.selectionEnd =tc.selectionStart = start,一定让selectionEnd和selectionStart相同,不然选中一个区域了。


如果我们在value重新赋值之后获取(tc.value.substr(0,tc.selectionStart)+clipboardData).length,大家注意到没,我们操作的是tc.value,value已经变了,这里的重新定位光标开始已经没有任何意义了!



载于我的博客

收起阅读 »

CSS新特性:让你的网页变得更加酷炫

随着互联网的发展,网页设计越来越重要。在过去,人们主要关注网站内容,而不太关注样式和布局。但是现代网页设计已经超出了这些基础层面,它们需要吸引用户、提高用户体验并增强用户对产品或服务的信心。为了实现这些目标,CSS(层叠样式表)的新特性已经变得越来越重要。在本...
继续阅读 »

随着互联网的发展,网页设计越来越重要。在过去,人们主要关注网站内容,而不太关注样式和布局。但是现代网页设计已经超出了这些基础层面,它们需要吸引用户、提高用户体验并增强用户对产品或服务的信心。为了实现这些目标,CSS(层叠样式表)的新特性已经变得越来越重要。在本文中,我们将介绍一些新的CSS特性,它们可以让你的网页变得更加酷炫。


1. 自定义属性


自定义属性是CSS最新的功能之一。使用这个功能,你可以定义你自己的CSS属性,并在整个代码库中重复使用。你只需定义一次属性,然后通过var()函数在整个代码库中使用。例如:


:root {
--primary-color: #FF0000;
}

.button {
background-color: var(--primary-color);
}

在上面的例子中,我们定义了一个名为--primary-color的CSS变量,并将其设置为红色。然后,我们将这个变量用于按钮的背景颜色。当你需要改变主色调时,只需改变--primary-color变量的值即可。


2. 网格布局


CSS网格布局是CSS3的新特性之一。使用网格布局,你可以轻松地创建复杂的网页布局。它基于行和列,而不是像传统布局那样基于盒子模型。例如:


.wrapper {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
grid-template-rows: repeat(3, 100px);
gap: 20px;
}

.box {
background-color: #000;
color: #FFF;
padding: 20px;
}

在上面的例子中,我们定义了一个名为wrapper的容器,并将其设置为网格布局。我们制定了三个列,第一列占整个空间的1/4,第二列占整个空间的一半,第三列占整个空间的1/4。我们还定义了三个行,每行高度为100像素。我们还给每个box加入了一些样式,以展示我们的布局。


3. 媒体查询


媒体查询是CSS用于响应式设计的技术。这意味着当用户改变浏览器窗口大小时,网页的布局会自动适应。例如:


@media screen and (max-width: 768px) {
.menu {
display: none;
}
.menu-toggle {
display: block;
}
}

在上面的例子中,我们定义了一个媒体查询,当浏览器窗口小于768像素时,将隐藏菜单并显示菜单切换按钮。这样,在移动设备上,网页的导航栏就会变得更加友好。


4. 动画


CSS动画是一种让你的网页更加生动和酷炫的技术。使用CSS动画,你可以使各种元素在网页上动起来,例如按钮、菜单等等。例如:


.button {
background-color: #FF0000;
color: #FFF;
padding: 20px;
border-radius: 10px;
animation: pulse 2s infinite;
}

@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}

在上面的例子中,我们定义了一个名为pulse的CSS动画,并将其应用于按钮。这个动画可以让按钮缓慢地放大和缩小,使它看起来更加醒目和有趣。


5. 变换


变换是CSS中的一个很棒的特性,可以让你改变元素的位置、大小、旋转角度等属性。使用变换,你可以创建一些非常惊人的效果,例如立体旋转、翻转等等。例如:


.box {
background-color: #FF0000;
width: 100px;
height: 100px;
transform: rotate(45deg);
}

在上面的例子中,我们定义了一个名为box的盒子,并将其旋转了45度。这可以让盒子看起来更加有趣和吸引人。


总结:


本文介绍了一些最新的CSS特性,包括自定义属性、网格布局、媒体查询、动画和变换。使用这些特性,你可以轻松地创建一个现代化、酷炫的网页设计,提高用户体验并增强用户对产品或服务的信心。当然,在实际开发中,我们还需要根据实际情况选择合适的技术和工具,以达到最佳的效果。

作者:饺子不放糖
来源:juejin.cn/post/7233057834287366203

收起阅读 »

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题


可以关注 程序员三时公众

作者:程序员三时
来源:juejin.cn/post/7232120266805526584
号 进行技术交流讨论

收起阅读 »

你管这破玩意叫缓存穿透?还是缓存击穿?

大家好,我是哪吒。 一、缓存预热 Redis缓存预热是指在服务器启动或应用程序启动之前,将一些数据先存储到Redis中,以提高Redis的性能和数据一致性。这可以减少服务器在启动或应用程序启动时的数据传输量和延迟,从而提高应用程序的性能和可靠性。 1、缓存预热...
继续阅读 »

大家好,我是哪吒。


一、缓存预热


Redis缓存预热是指在服务器启动或应用程序启动之前,将一些数据先存储到Redis中,以提高Redis的性能和数据一致性。这可以减少服务器在启动或应用程序启动时的数据传输量和延迟,从而提高应用程序的性能和可靠性。


1、缓存预热常见步骤


(1)数据准备


在应用程序启动或服务器启动之前,准备一些数据,这些数据可以是静态数据、缓存数据或其他需要预热的数据。


(2)数据存储


将数据存储到Redis中,可以使用Redis的列表(List)数据类型或集合(Set)数据类型。


(3)数据预热


在服务器启动或应用程序启动之前,将数据存储到Redis中。可以使用Redis的客户端工具或命令行工具来执行此操作。


(4)数据清洗


在服务器启动或应用程序启动之后,可能会对存储在Redis中的数据进行清洗和处理。例如,可以删除过期的数据、修改错误的数据等。


需要注意的是,Redis缓存预热可能会增加服务器的开销,因此应该在必要时进行。同时,为了减少预热的次数,可以考虑使用Redis的其他数据类型,如哈希表(Hash)或有序集合(Sorted Set)。此外,为了提高数据一致性和性能,可以使用Redis的持久化功能,将数据存储到Redis中,并在服务器重启后自动恢复数据。


2、代码实现


@Component
@Slf4j
public class BloomFilterInit
{
@Resource
private RedisTemplate redisTemplate;

//初始化白名单数据
@PostConstruct
public void init() {
//1 白名单客户加载到布隆过滤器
String key = "customer:1";
//2 计算hashValue,由于存在计算出来负数的可能,我们取绝对值
int hashValue = Math.abs(key.hashCode());
//3 通过hashValue和2的32次方后取余,获得对应的下标坑位
long index = (long)(hashValue % Math.pow(2,32));
log.info(key+" 对应的坑位index:{}",index);
//4 设置redis里面的bitmap对应类型白名单:whitelistCustomer的坑位,将该值设置为1
redisTemplate.opsForValue().setBit("whitelistCustomer",index,true);

}
}

二、缓存雪崩


Redis缓存雪崩是指在缓存系统中,由于某些原因,缓存的数据突然大量地被删除或修改,导致缓存系统的性能下降,甚至无法正常工作。


1、什么情况会发生缓存雪崩?


(1)误删除


由于误操作或故障,缓存系统可能会误删除一些正常的数据。这种情况通常会在数据库中发生。


(2)误修改


由于误操作或故障,缓存系统可能会误修改一些正常的数据。这种情况通常会在数据库中发生。


(3)负载波动


缓存系统通常会承受一定的负载波动,例如,在高峰期间,数据量可能会大幅增加,从而导致缓存系统的性能下降。


(4)数据变化频繁


如果缓存系统中的数据变化频繁,例如,每秒钟都会有大量的数据插入或删除,那么缓存系统可能会因为响应过慢而导致雪崩。


2、Redis缓存集群实现高可用


(1)主从 + 哨兵


(2)Redis集群


(3)开启Redis持久化机制aof/rdb,尽快恢复缓存集群。


3、如何避免Redis缓存雪崩?


(1)数据备份


定期备份数据,以防止误删除或误修改。


(2)数据同步


定期同步数据,以防止数据不一致。


(3)负载均衡


使用负载均衡器将请求分配到多个Redis实例上,以减轻单个实例的负载。


(4)数据优化


优化数据库结构,减少数据变化频繁的情况。


(5)监控与告警


监控Redis实例的性能指标,及时发现缓存系统的异常,并发出告警。


三、缓存穿透


Redis缓存穿透是指在Redis缓存系统中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。


1、什么情况会发生缓存穿透?


(1)数据量过大


当缓存中存储的数据量过大时,缓存的数据量可能会超过Redis的数据存储限制,从而导致缓存失去了它的作用。


(2)数据更新频繁


当缓存中存储的数据更新频繁时,缓存的数据可能会出现异步的变化,导致缓存无法被正常访问。


(3)数据过期


当缓存中存储的数据过期时,缓存的数据可能会失去它的作用,因为Redis会在一定时间后自动将过期的数据删除。


(4)数据权限限制


当缓存中存储的数据受到权限限制时,只有拥有足够权限的用户才能访问和处理这些数据,从而导致缓存失去了它的作用。


(5)Redis性能瓶颈


当Redis服务器的性能达到极限时,Redis缓存可能会因为响应过慢而导致穿透。


2、如何避免Redis缓存穿透?


(1)设置合理的缓存大小


根据实际需求设置合理的缓存大小,以避免缓存穿透。


(2)优化数据结构


根据实际需求优化数据结构,以减少数据的大小和更新频率。


(3)设置合理的过期时间


设置合理的过期时间,以避免缓存失去它的作用。


(4)增加Redis的并发处理能力


通过增加Redis的并发处理能力,以提高缓存的处理能力和响应速度。


(5)优化Redis服务器的硬件和软件配置


通过优化Redis服务器的硬件和软件配置,以提高Redis的性能和处理能力。


Redis缓存穿透


四、通过空对象缓存解决缓存穿透


如果发生了缓存穿透,可以针对要查询的数据,在Redis中插入一条数据,添加一个约定好的默认值,比如defaultNull。


比如你想通过某个id查询某某订单,Redis中没有,MySQL中也没有,此时,就可以在Redis中插入一条,存为defaultNull,下次再查询就有了,因为是提前约定好的,前端也明白是啥意思,一切OK,岁月静好。


这种方式只能解决key相同的情况,如果key都不同,则完蛋。


五、Google布隆过滤器Guava解决缓存穿透



1、引入pom


<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

2、创建布隆过滤器


BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100);

(3)布隆过滤器中添加元素


bloomFilter.mightContain(1)

(4)判断布隆过滤器中是否存在


bloomFilter.mightContain(1)

3、fpp误判率


@Service
@Slf4j
public class GuavaBloomFilterService {
public static final int SIZE = 1000000;

//误判率
public static double fpp = 0.01;

//创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);

public void guavaBloomFilter() {
for (int i = 1; i <= SIZE; i++) {
bloomFilter.put(i);
}
ArrayList<Integer> list = new ArrayList<>(10000);

for (int i = SIZE + 1; i <= SIZE + (10000); i++) {
if (bloomFilter.mightContain(i)) {
log.info("被误判了:{}", i);
list.add(i);
}
}
log.info("误判总数量:{}", list.size());
}
}

六、Redis缓存击穿


Redis缓存击穿是指在Redis缓存系统中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。


1、什么情况会发生缓存击穿?


根本原因:热点Key失效


(1)数据量过大


当缓存中存储的数据量过大时,缓存的数据量可能会超过Redis的数据存储限制,从而导致缓存失去了它的作用。


(2)数据更新频繁


当缓存中存储的数据更新频繁时,缓存的数据可能会出现异步的变化,导致缓存无法被正常访问。


(3)数据过期


当缓存中存储的数据过期时,缓存的数据可能会失去它的作用,因为Redis会在一定时间后自动将过期的数据删除。


(4)数据权限限制


当缓存中存储的数据受到权限限制时,只有拥有足够权限的用户才能访问和处理这些数据,从而导致缓存失去了它的作用。


(5)Redis性能瓶颈


当Redis服务器的性能达到极限时,Redis缓存可能会因为响应过慢而导致击穿。


2、如何避免Redis缓存击穿?


(1)设置合理的缓存大小


根据实际需求设置合理的缓存大小,以避免缓存穿透。


(2)优化数据结构


根据实际需求优化数据结构,以减少数据的大小和更新频率。


(3)设置合理的过期时间


设置合理的过期时间,以避免缓存失去它的作用。


(4)增加Redis的并发处理能力


通过增加Redis的并发处理能力,以提高缓存的处理能力和响应速度。


(5)优化Redis服务器的硬件和软件配置


通过优化Redis服务器的硬件和软件配置,以提高Redis的性能和处理能力。


七、Redis缓存击穿解决方案


1、互斥更新


通过双检加锁机制。


2、差异失效时间



先更新从缓存B,再更新主缓存A,而且让从缓存B的缓存失效时间长于A,保证A失效时,B还在。


作者:哪吒编程
来源:juejin.cn/post/7233052510553636901
收起阅读 »

聊一聊Kotlin协程"低级"api

聊一聊kotlin协程“低级”api Kotlin协程已经出来很久了,相信大家都有不同程度的用上了,由于最近处理的需求有遇到协程相关,因此今天来聊一Kotlin协程的“低级”api,首先低级api并不是它真的很“低级”,而是kotlin协程库中的基础api,我...
继续阅读 »

聊一聊kotlin协程“低级”api


Kotlin协程已经出来很久了,相信大家都有不同程度的用上了,由于最近处理的需求有遇到协程相关,因此今天来聊一Kotlin协程的“低级”api,首先低级api并不是它真的很“低级”,而是kotlin协程库中的基础api,我们一般开发用的,其实都是通过低级api进行封装的高级函数,本章会通过低级api的组合,实现一个自定义的async await 函数(下文也会介绍kotlin 高级api的async await),涉及的低级api有startCoroutineContinuationInterceptor


startCoroutine


我们知道,一个suspend关键字修饰的函数,只能在协程体中执行,伴随着suspend 关键字,kotlin coroutine common库(平台无关)也提供出来一个api,用于直接通过suspend 修饰的函数直接启动一个协程,它就是startCoroutine@SinceKotlin("1.3")

@Suppress("UNCHECKED_CAST")
public fun <R, T> (suspend R.() -> T).startCoroutine(
作为Receiver
receiver: R,
当前协程结束时的回调
completion: Continuation<T>
) {
createCoroutineUnintercepted(receiver, completion).intercepted().resume(Unit)
}

可以看到,它的Receiver是(suspend R.() -> T),即是一个suspend修饰的函数,那么这个有什么作用呢?我们知道,在普通函数中无法调起suspend函数(因为普通函数没有隐含的Continuation对象,这里我们不在这章讲,可以参考kotlin协程的资料)


image.png
但是普通函数是可以调起一个以suspend函数作为Receiver的函数(本质也是一个普通函数)


image.png
其中startCoroutine就是其中一个,本质就是我们直接从外部提供了一个Continuation,同时调用了resume方法,去进入到了协程的世界

startCoroutine实现

createCoroutineUnintercepted(completion).intercepted().resume(Unit)

这个原理我们就不细讲下去原理,之前也有写过相关的文章。通过这种调用,我们其实就可以实现在普通的函数环境,开启一个协程环境(即带有了Continuation),进而调用其他的suspend函数。


ContinuationInterceptor


我们都知道拦截器的概念,那么kotlin协程也有,就是ContinuationInterceptor,它提供以AOP的方式,让外部在resume(协程恢复)前后进行自定义的拦截操作,比如高级api中的Diapatcher就是。当然什么是resume协程恢复呢,可能读者有点懵,我们还是以上图中出现的mySuspendFunc举例子mySuspendFunc是一个suspned函数

::mySuspendFunc.startCoroutine(object : Continuation<Unit> {
override val context: CoroutineContext
get() = EmptyCoroutineContext

override fun resumeWith(result: Result<Unit>) {

}

})

它其实等价于val continuation = ::mySuspendFunc.createCoroutine(object :Continuation<Unit>{

    override val context: CoroutineContext
get() = EmptyCoroutineContext

override fun resumeWith(result: Result<Unit>) {
Log.e("hello","当前协程执行完成的回调")
}

})
continuation.resume(Unit)

startCoroutine方法就相当于创建了一个Continuation对象,并调用了resume。创建Continuation可通过createCoroutine方法,返回一个Continuation,如果我们不调用resume方法,那么它其实什么也不会执行,只有调用了resume等执行方法之后,才会执行到后续的协程体(这个也是协程内部实现,感兴趣可以看看之前文章)


而我们的拦截器,就相当于在continuation.resume前后,可以添加自己的逻辑。我们可以通过继承ContinuationInterceptor,实现自己的拦截器逻辑,其中需要复写的方法是interceptContinuation方法,用于返回一个自己定义的Continuation对象,而我们可以在这个Continuation的resumeWith方法里面(当调用了resume之后,会执行到resumeWith方法),进行前后打印/其他自定义操作(比如切换线程)



class ClassInterceptor() :ContinuationInterceptor { override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =MyContinuation(continuation)

}
class MyContinuation<T>(private val continuation: Continuation<T>):Continuation<T> by continuation{
override fun resumeWith(result: Result<T>) {
Log.e("hello","MyContinuation start ${result.getOrThrow()}")
continuation.resumeWith(result)

Log.e("hello","MyContinuation end ")
}
}

其中的key是ContinuationInterceptor,协程内部会在每次协程恢复的时候,通过coroutineContext取出key为ContinuationInterceptor的拦截器,进行拦截调用,当然这也是kotlin协程内部实现,这里简单提一下。


实战


kotlin协程api中的 async await


我们来看一下kotlin Coroutine 的高级api async await用法

CoroutineScope(Dispatchers.Main).launch {
val block = async(Dispatchers.IO) {
// 阻塞的事项

}
// 处理其他主线程的事务

// 此时必须需要async的结果时,则可通过await()进行获取
val result = block.await()
}

我们可以通过async方法,在其他线程中处理其他阻塞事务,当主线程必须要用async的结果的时候,就可以通过await等待,这里如果结果返回了,则直接获取值,否则就等待async执行完成。这是Coroutine提供给我们的高级api,能够将任务简单分层而不需要过多的回调处理。


通过startCoroutine与ContinuationInterceptor实现自定义的 async await


我们可以参考其他语言的async,或者Dart的异步方法调用,都有类似这种方式进行线程调用

async {
val result = await {
suspend 函数
}
消费result
}

await在async作用域里面,同时获取到result后再进行消费,async可以直接在普通函数调用,而不需要在协程体内,下面我们来实现一下这个做法。


首先我们想要限定await函数只能在async的作用域才能使用,那么首先我们就要定义出来一个Receiver,我们可以在Receiver里面定义出自己想要暴露的方法

interface AsyncScope {
fun myFunc(){

}

}
fun async(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

注意这个类,resumeWith 只会跟startCoroutine的这个协程绑定关系,跟await的协程没有关系
class AsyncStub(override val context: CoroutineContext = EmptyCoroutineContext) :
Continuation<Unit>, AsyncScope {
override fun resumeWith(result: Result<Unit>) {

// 这个是干嘛的 == > 完成的回调
Log.e("hello","AsyncStub resumeWith ${Thread.currentThread().id} ${result.getOrThrow()}")
}
}

上面我们定义出来一个async函数,同时定义出来了一个AsyncStub的类,它有两个用处,第一个是为了充当Receiver,用于规范后续的await函数只能在这个Receiver作用域中调用,第二个作用是startCoroutine函数必须要传入一个参数completion,是为了收到当前协程结束的回调resumeWith中可以得到当前协程体结束回调的信息

await方法里面

suspend fun<T> AsyncScope.await(block:() -> T) = suspendCoroutine<T> {
// 自定义的Receiver函数
myFunc()

Thread{
切换线程执行await中的方法
it.resumeWith(Result.success(block()))
}.start()
}

在await中,其实是一个扩展函数,我们可以调用任何在AsyncScope中定义的方法,同时这里我们模拟了一下线程切换的操作(Dispatcher的实现,这里不采用Dispatcher就是想让大家知道其实Dispatcher.IO也是这样实现的),在子线程中调用it.resumeWith(Result.success(block())),用于返回所需要的信息


通过上面定的方法,我们可以实现

async {
val result = await {
suspend 函数
}
消费result
}

这种调用方式,但是这里引来了一个问题,因为我们在await函数中实际将操作切换到了子线程,我们想要将消费result的动作切换至主线程怎么办呢?又或者是加入我们希望获取结果前做一些调整怎么办呢?别急,我们这里预留了一个CoroutineContext函数,我们可以在外部传入一个CoroutineContext

public interface ContinuationInterceptor : CoroutineContext.Element
而CoroutineContext.Element又是继承于CoroutineContext
CoroutineContext.Element:CoroutineContext

而我们的拦截器,正是CoroutineContext的子类,我们把上文的ClassInterceptor修改一下


class ClassInterceptor() : ContinuationInterceptor {
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MyContinuation(continuation)

}

class MyContinuation<T>(private val continuation: Continuation<T>) :
Continuation<T> by continuation {
private val handler = Handler(Looper.getMainLooper())
override fun resumeWith(result: Result<T>) {
Log.e("hello", "MyContinuation start ${result.getOrThrow()}")

handler.post {
continuation.resumeWith(Result.success(自定义内容))
}
Log.e("hello", "MyContinuation end ")
}
}

同时把async默认参数CoroutineContext实现一下即可

fun async(
context: CoroutineContext = ClassInterceptor(),
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

此后我们就可以直接通过,完美实现了一个类js协程的调用,同时具备了自动切换线程的能力

async {
val result = await {
test()
}
Log.e("hello", "result is $result ${Looper.myLooper() == Looper.getMainLooper()}")
}

结果

  E  start 
E MyContinuation start kotlin.Unit
E MyContinuation end
E end
E 执行阻塞函数 test 1923
E MyContinuation start 自定义内容数值
E MyContinuation end
E result is 自定义内容的数值 true
E AsyncStub resumeWith 2 kotlin.Unit

最后,这里需要注意的是,为什么拦截器回调了两次,因为我们async的时候开启了一个协程,同时await的时候也开启了一个,因此是两个。AsyncStub只回调了一次,是因为AsyncStub被当作complete参数传入了async开启的协程block.startCoroutine,因此只是async中的协程结束才会被回调。


image.png


本章代码


class ClassInterceptor() : ContinuationInterceptor {
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MyContinuation(continuation)

}

class MyContinuation<T>(private val continuation: Continuation<T>) :
Continuation<T> by continuation {
private val handler = Handler(Looper.getMainLooper())
override fun resumeWith(result: Result<T>) {
Log.e("hello", "MyContinuation start ${result.getOrThrow()}")

handler.post {
continuation.resumeWith(Result.success(6 as T))
}
Log.e("hello", "MyContinuation end ")
}
}
interface AsyncScope {
fun myFunc(){

}

}
fun async(
context: CoroutineContext = ClassInterceptor(),
block: suspend AsyncScope.() -> Unit
) {
// 这个有两个作用 1.充当receiver 2.completion,接收回调
val completion = AsyncStub(context)
block.startCoroutine(completion, completion)
}

class AsyncStub(override val context: CoroutineContext = EmptyCoroutineContext) :
Continuation<Unit>, AsyncScope {
override fun resumeWith(result: Result<Unit>) {

// 这个是干嘛的 == > 完成的回调
Log.e("hello","AsyncStub resumeWith ${Thread.currentThread().id} ${result.getOrThrow()}")
}
}


suspend fun<T> AsyncScope.await(block:() -> T) = suspendCoroutine<T> {
myFunc()

Thread{
it.resumeWith(Result.success(block()))
}.start()
}
模拟阻塞
fun test(): Int {
Thread.sleep(5000)
Log.e("hello", "执行阻塞函数 test ${Thread.currentThread().id}")
return 5
}
async {
val result = await {
test()
}
Log.e("hello", "result is $result ${Looper.myLooper() == Looper.getMainLooper()}")
}

最后


我们通过协程的低级api,实现了一个与官方库不同版本的async await,同时也希望通过对低级api的设计,也能对Coroutine官方库的高级api的实现有一定的了解。



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

程序员IT行业,外行眼里高收入人群,内行人里的卷王

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。...
继续阅读 »

程序员 一词,在我眼里其实是贬义词。因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员·


他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道什么时候开始网络上也去渲染这些,把程序员和这些联想在一起了。


回到正题,我们来聊聊,我们光鲜靓丽背后高工资。


是的作为一名程序员,在许多人的眼中,IT行业收入可能相对较高。这是不可否认的。但是,在这个职业领域里,我们所面对的困难和挑战也是非常的多。


持续的学习能力



程序员需要持续地学习,不断地掌握新技能。



随着技术的不断发展,我们需要不断地学习新的编程语言、开发框架、工具以及平台等等,这是非常耗费精力和时间的。每次技术更新都需要我们拿出宝贵的时间,去研究、学习和应用。


尤其在公司用项目中,用到新技术需要你在一定时间熟悉并使用时候,那个时候你自己只有硬着头皮,一边工作一边学习,如果你敢和老板说不会,那,,,我是没那个胆量


高强度抗压力



ICU,猝死,996说的就是我们



我们需要经常探索和应对极具挑战性的编程问题。解决一个困难的问题可能需要我们数小时,甚至数天的时间,这需要我们付出大量的勤奋和耐心。有时候,我们会出现程序崩溃或运行缓慢的情况,当然,这种情况下我们也需要更多的时间去诊断和解决问题,


还要保持高效率工作,同时保证项目的质量。有时候,团队需要在紧张的时间内完成特别复杂的任务,这就需要我们花费更多的时间和精力来完成工作。


枯燥乏味生活


由于高强度工作,和加班,我们的业余生活可能不够丰富,社交能力也会不足


高额经济支出


程序员IT软件行业,一般都是在一线城市工作,或者新一线,二线城市,所以面临的经济支持也会比较大,


最难的就是房租支持,生活开销。


一线城市工作,钱也只能在一线城市花,有时候也是真的存不了什么钱,明明自己什么也没有额外支持干些什么,可是每月剩下的存款也没有多少


短暂职业生涯


“背负黑匣子”:程序员的工作虽然看似高薪,但在实际工作中,我们承担了处理复杂技术问题的重任。


“独自快乐?”:程序员在工作中经常需要在长时间内独立思考和解决问题,缺乏团队合作可能会导致孤独和焦虑。


“冰山一角的技能”:程序员需要不断学习和更新技能,以适应快速变化的技术需求,这需要不断的自我修炼和付出时间。


“猝不及防的技术变革”:程序员在处理技术问题时需要时刻保持警惕,技术日新月异,无法预测的技术变革可能会对工作带来极大的压力。


“难以理解的需求”:客户和管理层的需求往往复杂而难以理解,程序员需要积极与他们沟通,但这也会给他们带来额外的挑战和压力。


“不请自来的漏洞”:安全漏洞是程序员必须不断面对和解决的问题,这种不确认的风险可能会让程序员时刻处于焦虑状态。


“高度聚焦的任务”:程序员在处理技术问题时需要集中精力和关注度,这通常需要长时间的高度聚焦,导致他们缺乏生活平衡。


“时刻警觉”:程序员在工作中必须时刻提醒自己,保持警觉和冷静,以便快速识别和解决问题。


“枯燥重复的任务”:与那些高度专业的技术任务相比,程序员还需要完成一些枯燥重复的工作,这让他们感到无聊和疲惫。


“被误解的天才”:程序员通常被视为是天才,但是他们经常被误解、被怀疑,这可能给他们的职业带来一定的负担。


程序员IT,也是吃年轻饭的,不是说你年龄越大,就代表你资历越深。 职业焦虑30岁年龄危机 越来越年轻化


要么转行,要么深造,


Yo,这是程序员的故事

高薪却伴随着堆积如山的代码

代码缺陷层出不穷,拯救业务成了千里马

深夜里加班的钟声不停响起

与bug展开了无尽的搏斗,时间与生命的角逐

接口返回的200,可前端却丝毫未见变化

HTTP媒体类型不支持,世界一团糟

Java Spring框架调试繁琐,无尽加班真让人绝望

可哪怕压力再大,我们还是核心开发者的倡导者

应用业务需要承载,才能取得胜利的喝彩

程序员的苦工是世界最稀缺的产业

我们不妥协,用技术创意为行业注入新生命

我们坚持高质量代码的规范

纵使压力山大,我们仍能跨过这些阻碍

这是程序员的故事。

大家有什么想法和故事吗,在工作中是否也遇到了和我一样的问题?

作者:程序员三时
来源:juejin.cn/post/7232120266805526584

收起阅读 »

Kotlin | 理解泛型使用

泛型类 & 泛型方法 泛型,指的是具体的类型泛化,多用在集合中(如List、Map),编码时使用符号代替,在使用时再确定具体类型。 泛型通常用于类和方法中,称为泛型类、泛型方法,使用示例:/** * 泛型类 */ abstract class Ba...
继续阅读 »

泛型类 & 泛型方法


泛型,指的是具体的类型泛化,多用在集合中(如ListMap),编码时使用符号代替,在使用时再确定具体类型。


泛型通常用于类和方法中,称为泛型类、泛型方法,使用示例:

/**
* 泛型类
*/
abstract class BaseBook<T> {
private var books: ArrayList<T> = ArrayList()

/**
* 泛型方法
*/
fun <E : T> add(item: E) {
books.add(item)
println("list:$books, size:${books.size}")
}
}

/**
* 子类继承BaseBook并传入泛型参数MathBook
*/
class BookImpl : BaseBook<MathBook>()

fun main() {
BookImpl().apply {
add(MathBook("数学"))
}
}

执行main()方法,输出:

list:[MathBook(math=数学)], size: 1

Java泛型通配符


? extends E 定义上界


Java中的泛型是不型变的,举个例子:IntegerObject的子类,但是List<Integer>并不是List<Object>的子类,因为List是不型变的。如:
错误
如果想让List<Integer>成为List<Object>的子类,可以通过上界操作符 ? extends E 来操作。


? extends E 表示此方法接受 E 或者 E 的 一些子类型对象的集合,而不只是 E 自身extends操作符可以限定上界通配符类型,使得通配符类型是协变的。注意,经过协变之后,数据是可读不可写的。示例:

//继承关系Child -> Parent 
class Parent{
protected String name = "Parent";
}

class Child extends Parent {
protected String name = "Child";
}

定义实体类,继承关系:Child -> Parent

class CList<E> {
//通过<? extends E>来定义上界
public void addAll(List<? extends E> list) {
//...
}
}

/**
* <? extends E>来定义上界,可以保证协变性
*/
public void GExtends() {
//1、Child是Parent的子类
Parent parent = new Child();

//2、协变,泛型参数是Parent
CList<Parent> objs = new CList<>();
List<Child> strs = new ArrayList<>(); //声明字符串List
strs.add(new Child());
objs.addAll(strs); //addAll()方法中的入参必须为List<? extends E>,从而保证了List<Child>是List<Parent>的子类。
}

addAll()方法中的入参必须为List<? extends E>,从而保证了List<Child>List<Parent>的子类。如果addAll()中的入参改为List<E>,则编译器会直接报错,因为List<Child>并不是List<Parent>的子类,如下:


错误


? super E 定义下界


? super E 可以看作一个E或者E的父类的“未知类型”,这里的父类包括直接和间接父类。super定义泛型的下界,使得通配符类型是逆变的。经过逆变之后,数据是可写不可读的,如: List<? super Child>List<Parent> 的一个超类。示例:

class CList<E> {
//通过<? super E>来定义下界
public void popAll(List<? super E> dest) {
//...
}

/**
* 逆变性
*/
public void GSuper(){
CList<Child> objs = new CList<>();
List<Parent> parents = new ArrayList<>(); //声明字符串List
parents.add(new Parent());
objs.popAll(parents); //逆变
}

可以看到popAll()的入参必须声明为List<? super E>,如果改为List<E>,编译器会直接报错:


错误


Kotlin泛型


Java 一样,Kolin 泛型本身也是不能型变的。



  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends T

  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super T


声明处型变


协变< out T>
interface GenericsP<T> {
fun get(): T //读取并返回T,可以认为只能读取T的对象是生产者
}

如上声明了GenericsP< T>接口,如果其内部只能读取并返回T,可以认为GenericsP实例对象为生产者(返回T)。

open class Book(val name: String)
data class EnglishBook(val english: String) : Book(english)
data class MathBook(val math: String) : Book(math)

已知EnglishBook、MathBookBook的子类,但是如果将Book、EnglishBook当成泛型放入GenericsP,他们之间的关系还成立吗?即:


error信息
可以看到编译器直接报错,因为虽然EnglishBookBook的子类,但是GenericsP<EnglishBook>并不是GenericsP<Book>的子类,如果想让这个关系也成立,Kotlin提供了out修饰符,out修饰符能够确保:



1、T只能用于函数返回中,不能用于参数输入中;
2、GenericsP< EnglishBook>可以安全的作为GenericsP< Book>的子类



示例如下:

interface GenericsP<out T> {
fun get(): T //读取并返回T,可以认为只能读取T的对象是生产者
// fun put(item: T) //错误,不允许在输入参数中使用
}

经过如上的改动后,可以看到GenericsP<EnglishBook>可以正确赋值给GenericsP<Book>了:
正确赋值


逆变< in T>
interface GenericsC<T> {
fun put(item: T) //写入T,可以认为只能写入T的对象是消费者
}

如上声明了GenericsC<T>接口,如果其内部只能写入T,可以认为GenericsC实例对象为消费者(消费T)。为了保证T只能出现在参数输入位置,而不能出现在函数返回位置上,Kotlin可以使用in进行控制:

interface GenericsC<in T> {
fun put(item: T) //写入T,可以认为只能写入T的对象是消费者
//fun get(): T //错误,不允许在返回中使用
}

继续编写如下函数:

    /**
* 称为GenericsC在Book上是逆变的。
* 跟系统源码中的Comparable类似
*/
private fun consume(to: GenericsC<Book>) {
//GenericsC<Book>实例赋值给了GenericsC<EnglishBook>
val target: GenericsC<EnglishBook> = to
target.put(EnglishBook("英语"))
}

可以看到GenericsC中的泛型参数声明为in后,GenericsC<Book>实例可以直接赋值给了GenericsC<EnglishBook>,称为GenericsCBook上是逆变的。在系统源码中我们经常使用的一个例子就是Comparable:

//Comparable.kt
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}

使用处型变(类型投影)


上一节中in、out都是写在类的声明处,从而控制泛型参数的使用场景,但是如果泛型参数既可能出现在函数入参中,又可能出现在函数返回中,典型的类就是Array:

class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}

这时候就不能在声明处做任何协变/逆变的操作了,如下函数中使用Array

 fun copy(from: Array<Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}

调用方:

val strs: Array<String> = arrayOf("1", "2")
val any = Array<Any>(2) {}
copy(strs, any) //编译器报错 strs其类型为 Array<String> 但此处期望 Array<Any>

错误原因就是因为Array<String>并不是Array<Any>的子类,即不是协变的,这里是为了保证数据的安全性。如果可以保证Array< String>传入copy()函数之后不能被写入,那么就保证了安全性,既然我们在声明Array时不能限制泛型参数,那么完全可以在使用处进行限制,如下:

 fun copy(from: Array<out Any>, to: Array<Any>) {
if (from.size != to.size) return
for (i in from.indices)
to[i] = from[i]
}

可以看到对from添加了out限制,这种被称为使用处型变。即不允许from进行写入操作,那么就可以保证了from的安全性,再进行上面的调用时,copy(strs, any)就可以正确的执行了。


星投影< *>


当不使用协变、逆变时,某些场景下可以使用<*>来实现泛型,如:



  • 对于GenericsP<out T: Book>GenericsP< *>相当于GenericsP<out Book>,当T未知时,可以安全的从GenericsP<*>中读取Book值;

  • 对于GenericsC<in T>GenericsC<*>相当于 GenericsC<in Nothing>,当T未知时,没有安全方式写入GenericsC<*>

  • 对于Generics<T: Book>T为有上界Book的不型变参数,当Generics<*>读取时等价于Generics<out Book>;写入时等价于Generics<in Nothing>


泛型擦除


泛型参数会在编译期间存在,在运行期间会被擦除,例如:Generics<EnglishBook>Generics<MathBook> 的实例都会被擦除为 Generics<*>。运行时期检测一个泛型类型的实例无法通过is关键字进行判断,另外运行期间具体的泛型类型判断也无法判断,如: books as List<Book>,只会对非泛型部分进行检测,形如:books as List<*>


如果想具体化泛型参数,可以通过inline + reified的方式:

 /**
* inline + reified 使得类型参数被实化 reified:实体化的
* 注:带reified类型参数的内联函数,Java是无法直接调用的
*/
inline fun <reified T> isAny(value: Any): Boolean {
return value is T
}

参考


【1】https://www.kotlincn.net/docs/reference/generics.html
【2】https://mp.weixin.qq.com/s/vSwx7fgROJcrQwEOW7Ws8A
【3】https://juejin.cn/post/7042606952311947278


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

怎么做登录(单点登录)功能?

简单上个图(有水印。因为穷所以没开会员) 先分析下登陆要做啥 首先,搞清楚要做什么。 登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。 怎么落实? 怎么实现它?用什么实现? ...
继续阅读 »

简单上个图(有水印。因为穷所以没开会员)


怎么做登陆(单点登陆)?.png


先分析下登陆要做啥



首先,搞清楚要做什么。


登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。



怎么落实?



怎么实现它?用什么实现?




我们的项目是Springboot + Vue前后端分离类型的。


选择用token + redis 实现,权限的话用SpringSecurity来做。




前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。


单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。



上代码



概念这个东西越说越玄。咱们直接上代码吧。



接口:
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   //用户名、密码、验证码、uuid
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                     loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
}


用户信息验证交给SpringSecurity

/**
* 登录验证
*/
public String login(String username, String password, String code, String uuid)
{
   // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
   boolean captchaEnabled = configService.selectCaptchaEnabled();
   if (captchaEnabled)
  {
       //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
       validateCaptcha(username, code, uuid);
  }
   // 用户验证 -- SpringSecurity
   Authentication authentication = null;
   try
  {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
       //
       authentication = authenticationManager.authenticate(authenticationToken);
  }
   catch (Exception e)
  {
       if (e instanceof BadCredentialsException)
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
           throw new UserPasswordNotMatchException();
      }
       else
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
           throw new ServiceException(e.getMessage());
      }
  }
   finally
  {
       AuthenticationContextHolder.clearContext();
  }
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}

把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)
/**
* 校验验证码
*/
public void validateCaptcha(String username, String code, String uuid)
{
   //uuid是验证码的redis key
   String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
   String captcha = redisCache.getCacheObject(verifyKey);
   redisCache.deleteObject(verifyKey);
   if (captcha == null)
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
       throw new CaptchaExpireException();
  }
   if (!code.equalsIgnoreCase(captcha))
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
       throw new CaptchaException();
  }
}

token生成部分


这里,token

/**
* 创建令牌
*/
public String createToken(LoginUser loginUser)
{
   String token = IdUtils.fastUUID();
   loginUser.setToken(token);
   setUserAgent(loginUser);
   refreshToken(loginUser);

   Map<String, Object> claims = new HashMap<>();
   claims.put(Constants.LOGIN_USER_KEY, token);
   return createToken(claims);
}

刷新token
/**
* 刷新令牌
*/
public void refreshToken(LoginUser loginUser)
{
   loginUser.setLoginTime(System.currentTimeMillis());
   loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
   // 根据uuid将loginUser缓存
   String userKey = getTokenKey(loginUser.getToken());
   redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

验证token
/**
* 验证令牌
*/
public void verifyToken(LoginUser loginUser)
{
   long expireTime = loginUser.getExpireTime();
   long currentTime = System.currentTimeMillis();
   if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
  {
       refreshToken(loginUser);
  }
}


注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。


另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
   //...无关的代码删了
   httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
}
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
   @Autowired
   private TokenService tokenService;

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
       throws ServletException, IOException
  {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
      {
           //刷新token有效期
           tokenService.verifyToken(loginUser);
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
           authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
           SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
       chain.doFilter(request, response);
  }
}


这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。


这里JWT只是起到个加密的作用,无它。


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

装了我这 10 个 IDEA 神级插件后,同事也开始情不自禁的嘚瑟了

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。 Vuesion Theme 颜值是生产...
继续阅读 »

昨天,有读者私信发我掘金上的一篇文章,说里面提到的 Intellij IDEA 插件真心不错,基本上可以一站式开发了,希望能分享给更多的小伙伴,我在本地装了体验了一下,觉得确实值得推荐,希望小伙伴们有时间也可以尝试一下。


Vuesion Theme


颜值是生产力的第一要素,IDE 整好看了,每天对着它也是神清气爽,有木有?就 Intellij IDEA 提供的暗黑和亮白主色,虽然说已经非常清爽了,但时间久了总觉得需要再来点新鲜感?


Vuesion Theme 这个主题装上后,你会感觉整个 Intellij IDEA 更高级了。



安装完插件就立马生效了,瞧这该死的漂亮,整个代码着色,以及文件的图标,都更炫酷了:



当然了,主题这事,萝卜白菜各有所爱,就像玩 dota,我就喜欢露娜。


lombok


可能提到 lombok,多多少少有些争议,但不得不说,这玩意的确是很能省代码,并且很多开源的第三方 jar 包,以及 Intellij IDEA 2020.3 以后的版本也都默认加了 lombok。



这么多注解可以选择,在写 VO、DO、DTO 的时候是真的省心省力。



如果没有 lombok 的帮助,那整个代码就要炸了呀。对比一下,是不是感受还挺明显的?



当然了,要使用 lombok,你得在 pom.xml 文件中引入 lombok 的依赖包。


xml
复制代码
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

File Expander


这个插件不仅可以反编译,还可以打开 tar.gz,zip 等压缩文件,



如果有小伙伴反驳说自己不装插件也可以打开 jar 包里的代码,那是因为你的 jar 在 classpath。如果单独打开一个 jar 包,不装插件是看不了的。



GitToolBox


如果你经常使用 Git 提交代码的话,这款插件就非常的爽。



它能直接提示你远程版本库里有多少文件更新,你有多少文件没有提交到版本库,甚至可以显示上一次提交的时间和版本更新者。



Maven Helper


这插件几乎人手一个了吧,Java 后端开发必备啊。



依赖可视化的神器,可以很清楚地知道依赖的关系图谱,假如有冲突的话,也是一目了然。



Translation


对于英文能力差的同学来说,这个翻译插件简直神了,它支持 Google 翻译、有道翻译、百度翻译、Alibaba 翻译。



刚好写这篇内容的时候,发现最新的版本是 3.3.5,趁机升级一波。有了这款翻译插件,看源码绝对是爽歪歪。以前遇到不认识的单词,真的是好烦,还要切到翻译软件那里查,现在可好,单词翻译、文档翻译、注释翻译,都有了。



arthas idea


Arthas 应该大家都很熟悉了,阿里开源的一款强大的 java 在线诊断工具。


但如果每次都要你输入一长串命令的话,相信你也会很崩溃,尤其是很多时候我还记忆模糊,很多记不住。这款插件刚好解决了我这个烦恼,极大地提高了生产力



使用起来也非常方便,直接进入你要诊断的方法和类,右键选择对应的命令,就会自动帮你生成了。



Free Mybatis plugin


Mybatis 基本上是目前最主流的 ORM 框架了,相比于 hibernate 更加灵活,性能也更好。所以我们一般在 Spring Boot 项目中都会写对应的 mapper.java 和 mapper.xml。


那有了这款插件之后,两者就可以轻松关联起来。



比如,我这里要查看 ArticleMapper 的 xml,那么编辑器的行号右侧就会有一个向右的→,直接点击就跳转过去了。



想跳转回来的话,也是同样的道理,所以有了这款产检,mapper 和 xml 之间就可以自由切换了,丝滑。


VisualGC


这里给大家推荐一个 JVM 堆栈可视化工具,可以和 Intellij IDEA 深度集成——VisualGC。



当我们需要监控一个进程的时候,直接打开 VisualGC面板,就可以查看到堆栈和垃圾收集情况,可以说是一目了然。



CheckStyle-IDEA


如果你比较追求代码规范的话,可以安装这个插件,它会提醒你注意无用导入、注释、语法错误❎、代码冗余等等。



在 CheckStyle 面板中,你可以选择 Google 代码规范或者 sun 的代码规范,跑一遍检查,就可以看到所有的修改建议了。



最后


以上这 10 款 Intellij IDEA 插件也是我平常开发中经常用到的,如果大家有更好更效率的插件,也可以评论里留言。


参考链接:juejin.cn/post/702802…


没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。


本文已收录到 GitHub 上星标 4k+ 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发(Git、Nginx、Maven、Intellij IDEA、Spring、Spring Boot、Redis、MySql 等等)、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路😄。


Github 仓库:github.com/itwanger/to…


star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。



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

为什么选择FastAPI?

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。 一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数...
继续阅读 »

说起FastAPI,我们一开始是不太想尝试的,毕竟是个没尝试过的开发框架,怕踩坑,怕影响项目开发周期。



一直以来我们的主要开发框架是 Django,Django 是个非常方便的 Python 后端框架,一条命令即可生成规范的项目结构,命令创建子应用,健壮的数据库 ORM 支持,功能完善、要素齐全:自带大量常用工具和框架(比如分页,auth,权限管理), 适合快速开发企业级网站。完善的文档:经过多年的发展和完善,Django 有广泛的实践案例和完善的在线文档,可以让开发人员有好的借鉴途径。并且使用众多,能够借鉴的例子也很多。目前 NetDevOps 的平台后端,大多数都是基于 Python 的 Django 来开发的,原因是:python 的确上手快,django 支持完善,能够迅速的建立和设备的联系并生成自带的管理页面。


但是Django太全了太重了,随着开发周期越往后,开发模块越来越多,整个程序就显得异常臃肿,代码逻辑相互耦合十分严重,整个系统需要保持极高的稳定性。所以,我们希望能够有个小而精的框架,可以逐步的将Django项目中的模块慢慢做拆分,做解耦,做程序的可插拔。FastAPI这时候进入到我们的视野。



FastAPI 顾名思义,一个字,就是快。基于(并完全兼容)OPenAPI 的相关开放标准。



  • 高性能:FastAPI 采用异步编程模式,基于 Starlette 框架和 pydantic 库进行开发。其性能相比于 Flask 和 Django 均有很大提升。

  • 简单易用:FastAPI 提供了自动生成 API 文档的功能,丰富的文档可以让开发人员更快速地了解 API 的使用方法。

  • 规范化:FastAPI 默认支持 OpenAPI(前身为 Swagger)和 JSON Schema,从而规范化 API 的设计和发布。

  • 类型检查:FastAPI 强制使用类型注解,使得代码更加严谨,同时可以使用 mypy 等类型检查工具来保证代码的质量。

  • 整合多种数据库支持:FastAPI 可以无缝进行整合多种数据库的使用,比如 SQLAlchemy、Tortoise ORM 等。


使用 FastAPI 可以提升性能、简化开发、规范 API 设计、增加代码可读性和可维护性,从而促进开发效率的提升。 FastAPI 站在以下巨人的肩膀之上:


Starlette 负责 web 部分。 Pydantic 负责数据部分。


Starlette


Starlette 是一个轻量级的 ASGI 框架和工具包,特别适合用来构建高性能的 asyncio 服务.


Starlette 的主要特性:



  1. 性能表现优异

  2. WebSocket 支持.

  3. GraphQL 支持.

  4. 进程内的后台任务执行

  5. 启动和关闭服务的事件触发

  6. 测试客户端构建于 requests.

  7. 支持 CORS, GZip, Static Files, Streaming 响应.

  8. 支持会话和 Cookie

  9. 100% 测试覆盖率

  10. 100% 类型注解

  11. 无依赖


Pydantic


pydantic 库是 python 中用于数据接口定义检查与设置管理的库。


pydantic 在运行时强制执行类型提示,并在数据无效时提供友好的错误。


它具有如下优点:



  1. 与 IDE/linter 完美搭配,不需要学习新的模式,只是使用类型注解定义类的实例

  2. 多用途,BaseSettings 既可以验证请求数据,也可以从环境变量中读取系统设置

  3. 快速

  4. 可以验证复杂结构

  5. 可扩展,可以使用 validator 装饰器装饰的模型上的方法来扩展验证

  6. 数据类集成,除了 BaseModel,pydantic 还提供了一个 dataclass 装饰器,它创建带有输入数据解析和验证的普通 Python 数据类。


FastAPI 推荐使用 uvicorn 启动服务



Uvicorn 是基于 uvloop 和 httptools 构建的非常快速的 ASGI 服务器。


uvicorn 是一个基于 asyncio 开发的一个轻量级高效的 web 服务器框架


uvicorn 设计的初衷是想要实现两个目标:


使用 uvloop 和 httptools 实现一个极速的 asyncio 服务器


实现一个基于 ASGI(异步服务器网关接口)的最小应用程序接口。


它目前支持 http, websockets, Pub/Sub 广播,并且可以扩展到其他协议和消息类型。


uvloop 用于替换标准库 asyncio 中的事件循环,使用 Cython 实现,它非常快,可以使 asyncio 的速度提高 2-4 倍。asyncio 不用我介绍吧,写异步代码离不开它。


httptools 是 nodejs HTTP 解析器的 Python 实现。


综述


综上所述,我们推荐使用 FastAPI 来进行快速开发和程序构建,尤其是功能较为简单的应用,它也支持数据结构校验和异步。提高代码执行效率和开发效率。同时具备丰富的数据支持,基于 OpenAPI 的自助交互式文档,让开发过程中的调试也十分方便。快速敏捷开发是我们的追求。



  • 快速开发:提供基础框架和默认设置,使开发人员可以快速构建 API,并自动生成文档和测试样例。

  • 高性能:基于 Python 3.6+、ASGI 和 asyncio 等技术,提供高性能特性,因此在高流量和低延迟的应用场景下比其他框架有更好的响应速度和性能。

  • 文档自动生成:内置文档生成器,自动解析函数参数和返回值来生成规范化的文档,并支持 OpenAPI 和 JSON Schema 的标准格式。

  • 强类型支持:采用类型注解,提高代码的质量和可读性,减少出现运行时错误的可能性。

  • 多种数据库支持:支持多种数据库,包括 SQLAlchemy、Tortoise ORM 等,可以方便地处理数据库操作。


FastAPI 是一款快速、高性能、安全、易用、规范化的 Web 应用框架,能够极大地提高开发效率,保证应用的高性能和可靠性。


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

Socket学习网络基础

1.OSI七层网络模型浅析 当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了! OSI七层网络模型(从下往上) : 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传...
继续阅读 »

1.OSI七层网络模型浅析


当然,我们不是专业搞网络工程的,只要知道有哪些层,大概是拿来干嘛的就可以了!


OSI七层网络模型(从下往上)




  • 物理层(Physical) :设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等!在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,这一层的单位是:bit比特

  • 数据链路层(Datalink) :可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等!另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两过程!这种建立起来的数据收发关系~该层的设备有:网卡,网桥,网路交换机,另外该层的单位为:

  • 网络层(Network) :主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了把任意两台终端设备数据链接起来的问题!简单点说就是:建立网络连接和为上层提供服务!该层的设备有:路由!该层的单位为:数据包,另外IP协议就在这一层!

  • 传输层(Transport) :向上面的应用层提供通信服务,面向通信部分的最高层,同时也是用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络层,并且保证这些数据段有效的到达对端!所以这层的单位是:数据段;而这层有两个很重要的协议就是:TCP传输控制协议UDP用户数据报协议,这也是本章节核心讲解的部分!

  • 会话层(Session) :负责在网络中的两节点之间建立、维持和终止通信。建立通信链接,保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!

  • 表示层(Presentation) :对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、数据格式转换和加密解密,压缩解压缩"等

  • 应用层(Application) :OSI参考模型的最高层,为用户的应用程序提供网络服务。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。



好的上面我们浅述了OSI七层网络模型,下面总结下:



OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊的网络功能,从网络功能角度观察:



  • 下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主

  • 第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;

  • 上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。


简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。




TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层(对应OSI的第四层)。TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。这4层分别为:



  • 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。

  • 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。

  • 网络互连层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。

  • 主机到网络层:对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。


3.TCP/UDP区别讲解


好吧,前两点侃侃而谈,只是给大家普及下OSI七层模型和TCP/IP四层模型的概念,接下来要讲的是和我们Socket开发相关的一些概念名词了!


1)IP地址



2)端口



1. 用于区分不同的应用程序


2. 端口号的范围为0-65535,其中0-1023未系统的保留端口,我们的程序尽可能别使用这些端口!


3. IP地址和端口号组成了我们的Socket,Socket是网络运行程序间双向通信链路的终结点,是TCP和UDP的基础!


4. 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23




3)TCP协议与UDP协议的比较:


TCP协议流程详解:


首先TCP/IP是一个协议簇,里面包括很多协议的。UDP只是其中的一个。之所以命名为TCP/IP协议,因为TCP,IP协议是两个很重要的协议,就用他两命名了。


下面我们来讲解TCP协议和UDP协议的区别:


TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,即在收发数据钱,都需要与对面建立可靠的链接,这也是面试经常会问到的TCP的三次握手以及TCP的四次挥手三次握手:建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立,在Socket编程中,这一过程由客户端执行connect来触发,具体流程图如下:




  • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。


四次挥手:终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在Socket编程中,这一过程由客户端或服务端任一方执行close来触发,具体流程图如下:




  • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态

  • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。另外也可能是同时发起主动关闭的情况:



另外还可能有一个常见的问题就是:为什么建立连接是三次握手,而关闭连接却是四次挥手呢?答:因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。


UDP协议详解


UDP(User Datagram Protocol)用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。相比TCP就是无需建立链接,结构简单,无法保证正确性,容易丢包。


4.Java中对于网络提供的几个关键类


针对不同的网络通信层次,Java给我们提供的网络功能有四大类:



  • InetAddress:用于标识网络上的硬件资源

  • URL:统一资源定位符,通过URL可以直接读取或者写入网络上的数据

  • Socket和ServerSocket:使用TCP协议实现网络通信的Socket相关的类

  • Datagram:使用UDP协议,将数据保存在数据报中,通过网络进行通信


本节我们只介绍前两个类,Socket与Datagram到TCP和UDP的章节再讲解!


~InetAddress的使用例子


示例代码

    public static void main(String[] args) throws Exception{
//获取本机InetAddress的实例:
InetAddress address = InetAddress.getLocalHost();
System.out.println("本机名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
byte[] bytes = address.getAddress();
System.out.println("字节数组形式的IP地址:" + Arrays.toString(bytes));
System.out.println("直接输出InetAddress对象:" + address);
}

运行结果图



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

从进入内核态看内存管理

知乎上搜到一个比较有意思的话题:如何理解「进入内核态」,要回答好这个问题需要对内存管理及程序的运行机制有比较深刻的了解,比如你需要了解内存的分段,分页,中断等机制,信息量比较大,本文将会 Intel CPU 的发展历史讲起,循序渐近地帮助大家彻底掌握这一概念,...
继续阅读 »

知乎上搜到一个比较有意思的话题:如何理解「进入内核态」,要回答好这个问题需要对内存管理及程序的运行机制有比较深刻的了解,比如你需要了解内存的分段,分页,中断等机制,信息量比较大,本文将会 Intel CPU 的发展历史讲起,循序渐近地帮助大家彻底掌握这一概念,相信大家看了肯定有帮助,本文目录如下



  • CPU 运行机制

  • Intel CPU 历史发展史

    • 分段

    • 保护模式



  • 特权级

    • 系统调用

    • 中断



  • 分段内存的优缺点

  • 内存分页

  • 总结


CPU 运行机制


我们先简单地回顾一下 CPU 的工作机制,重新温习一下一些基本概念,因为我在查阅资料的过程发现一些网友对寻址,CPU 是几位的概念理解得有些模糊,理解了这些概念再去看 CPU 的发展史就不会再困惑


CPU 是如何工作的呢?它是根据一条条的机器指令来执行的,而机器指令= 操作码+操作数,操作数主要有三类:寄存器地址、内存地址或立即数(即常量)。


我们所熟悉的程序就是一堆指令和数据的集合,当打开程序时,装载器把程序中的指令和数据加载到内存中,然后 CPU 到内存中一条条地取指令,然后再译码,执行。


在内存中是以字节为基本单位来读写数据的,我们可以把内存看作是一个个的小格子(一般我们称其为内存单元),而每个小格子是一个字节,那么对于 B8 0123H 这条指令来说,它在内存中占三字节,如下,CPU 该怎么找到这些格子呢,我们需要给这些格子编号,这些编号也就是我们说的内存地址,根据内存地址就是可以定位指令所在位置,从而取出里面的数据



如图示:内存被分成了一个个的格子,每个格子一个字节,20000~20002 分别为对应格子的编号(即内存地址)


CPU 执行指令主要分为以下几个步骤




  1. 取指令,CPU 怎么知道要去取哪条指令呢,它里面有一个 IP 寄存器指向了对应要取的指令的内存地址, 然后这个内存地址会通过地址总线找到对应的格子,我们把这个过程称为寻址,不难发现寻址能力决定于地址总线的位宽,假设地址总线位数为 20 位,那么内存的可寻址空间为 2^20 * 1Byte = 1M,将格子(内存单元)里面的数据(指令)取出来后,再通过数据总线发往 CPU 中的指令缓存区(指令寄存器),那么一次能传多少数据呢,取决于数据总线的位宽,如果数据总线为 16 位,那么一次可以传 16 bit 也就是两个字节。




  2. 译码:指令缓冲区中的指令经过译码以确定该进行什么操作




  3. 执行:译码后会由控制单元向运算器发送控制指令进行操作(比如执行加减乘除等),执行是由运算器操纵数据也就是操作数进行计算,而操作数保存在存储单元(即片内的缓存和寄存器组)中,由于操作数有可能是内存地址,所以执行中可能需要到内存中获取数据(这个过程称为访存),执行后的结果保存在寄存器或写回内存中



    以指令 mov ax, 0123H 为例,它表示将数据 0123H 存到寄存器 AX 中,在此例中 AX 为 16 位寄存器,一次可以操作 16 位也就是 2 Byte 的数据,所以我们将其称为 16 位 CPU,CPU 是多少位取决于它一次执行指令的数据带宽,而数据带宽又取决于通用寄存器的位宽




  4. 更新 IP:执行完一条指令后,更新 IP 中的值,将其指向下一条指令的起始地址,然后重复步骤 1




由以上总结可知寻址能力与寄存器位数有关


接下来我们以执行四条指令为例再来仔细看下 CPU 是如何执行指令的,动图如下:



看到上面这个动图,细心地你可能会发现两个问题



  1. 前文说指令地址是根据 IP 来获取的吗,但上图显示指令地址却是由「CS 左移四位 + IP」计算而来的,与我们所阐述的指令保存在 IP 寄存器中似乎有些出入,这是怎么回事呢?

  2. 动图显示的地址是真实物理地址,这样进程之间可以互相访问/改写对方的物理地址,显然是不安全的,那如何才能做到安全访问或者说进程间内存的隔离呢


以上两点其实只要我们了解一下 CPU 的发展历史就明白解决方案了,有了以上的铺垫,在明白了寻址16/32/64 位 CPU 等术语的含义后,再去了解 CPU 的发展故事会更容易得多,话不多说,发车


Intel CPU 历史发展史


1971 年世界上第一块 4 位 CPU-4004 微处理器横空出世,1974 年 Intel 研发成功了 8 位 CPU-8080,这两款 CPU 都是使用的绝对物理地址来寻址的,指令地址只存在于 IP 寄存器中(即只使用 IP 寄存器即可确定内存地址)。由于是使用绝对物理地址寻址,也就意味着进程之间的内存数据可能会互相覆盖,很不安全,所以这两者只支持单进程


分段


1978 年英特尔又研究成功了第一款 16 位 CPU - 8086,这款 CPU 可以说是 x86 系列的鼻祖了,设计了 16 位的寄存器和 20 位的地址总线,所以内存地址可以达到 2^20 Byte 即 1M,极大地扩展了地址空间,但是问题来了,由于寄存器只有 16 位,那么 16 位的 IP 寄存器如何能寻址 20 位的地址呢,首先 Intel 工程师设计了一种分段的方法:1M 内存可以分为 16 个大小为 64 K 的段,那么内存地址就可以由「段的起始地址(也叫段基址) + 段内偏移(IP 寄存器中的值)」组成,对于进程说只需要关心 4 个段 ,代码段 数据段 堆栈段 附加段,这几个段的段基址分别保存在 CS,DS,SS,ES 这四个寄存器中



这四个寄存器也是 16 位,那怎么访问 20 位的内存地址呢,实现也很简单,将每个寄存器的值左移四位,然后再加上段内偏移即为寻址地址,CPU 都是取代码段 中的指令来执行的,我们以代码段内的寻址为例来计算内存地址,指令的地址 = CS << 4 + IP ,这种方式做到了 20 位的寻址,只要改变 CS,IP 的值,即可实现在 0 到最大地址 0xFFFFF 全部 20 位地址的寻址


举个例子:假设 CS 存的数据为 0x2000,IP 为 0x0003,那么对应的指令地址为



图示为真实的物理地址计算方式,从中可知, CS 其实保存的是真实物理地址的高 16 位


分段的初衷是为了解决寻址问题,但本质上CS:IP 计算得到的还是真实物理地址,所以它也无法支持多进程,因为使用绝对物理地址寻址意味着进程可以随意修改 CS:IP,将其指向任意地址,很可能会覆盖正在运行的其他进程的内存,造成灾难性后果。


我们把这种使用真实物理地址且未加任何限制的寻址方式称为实模式(real mode,即实际地址模式)


保护模式


实模式上的物理地址由 段寄存器中的段基址:IP 计算而来,而段基址可由用户随意指定,显然非常不安全,于是 Intel 在之后推出了 80286 中启用了保护模式,这个保护是怎么做的呢


首先段寄存器保存的不再是段基址了,而是段选择子(Selector),其结构如下



其中第 3 到 15 位保存的是描述符索引,此索引会根据 TI 的值是 0 还是 1 来选择是到 GDT(全局描述符表,一般也称为段表)还是 LDT 来找段描述符,段描述符保存的是段基址和段长度,找到段基址后再加上保存在 IP 寄存器中的段偏移量即为物理地址,段描述符的长度统一为 8 个字节,而 GDT/LDT 表的基地址保存在 gdtr/ldtr 寄存器中,以 GDT (此时 TI 值为 0)为例来看看此时 CPU 是如何寻址的



可以看到程序中的地址是由段选择子:段内偏移量组成的,也叫逻辑地址,在只有分段内存管理的情况下它也被称为虚拟内存


GDT 及段描述符的分配都是由操作系统管理的,进程也无法更新 CS 等寄存器中值,这样就避免了直接操作其他进程以及自身的物理地址,达到了保护内存的效果,从而为多进程运行提供了可能,我们把这种寻址方式称为保护模式


那么保护模式是如何实现的呢,细心的你可能发现了上图中在段选择子和段描述符中里出现了 RPLDPL 这两个新名词,这两个表示啥意思呢?这就涉及到一个概念:特权级


特权级


我们知道 CPU 是根据机器指令来执行的,但这些指令有些是非常危险的,比如清内存置时钟分配系统资源等,这些指令显然不能让普通的进程随意执行,应该始终控制在操作系统中执行,所以要把操作系统和普通的用户进程区分开来


我们把一个进程的虚拟地址划分为两个空间,用户空间内核空间,用户空间即普通进程所处空间,内核空间即操作系统所处空间



当 CPU 运行于用户空间(执行用户空间的指令)时,它处于用户态,只能执行普通的 CPU 指令 ,当 CPU 运行于内核空间(执行内核空间的指令)时,它处于内核态,可以执行清内存,置时钟,读写文件等特权指令,那怎么区分 CPU 是在用户态还是内核态呢,CPU 定义了四个特权等级,如下,从 0 到 3,特权等级依次递减,当特权级为 0 时,CPU 处于内核态,可以执行任何指令,当特权级为 3 时,CPU 处于用户态,在 Linux 中只用了 Ring 0,Ring 3 两个特权等级



那么问题来了,怎么知道 CPU 处于哪一个特权等级呢,还记得上文中我们提到的段选择子吗



其中的 RPL 表示请求特权((Requested privilege level))我们把当前保存于 CS 段寄存器的段选择子中的 RPL 称为 CPL(current priviledge level),即当前特权等级,可以看到 RPL 有两位,刚好对应着 0,1,2,3 四个特权级,而上文提到的 DPL 表示段描述符中的特权等级(Descriptor privilege level)知道了这两个概念也就知道保护模式的实现原理了,CPU 会在两个关键点上对内存进行保护




  1. 目标段选择子被加载时




  2. 当通过线性地址(在只有段式内存情况下,线性地址为物理地址)访问一个内存页时。由此可见,保护也反映在内存地址转换的过程之中,既包括分段又包括分页(后文分提到分页)




CPU 是怎么保护内存的呢,它会对 CPL,RPL,DPL 进行如下检查



只有 CPL <= DPL 且 RPL <= DPL(申请特权等级待以及当前特权等级必须比调用的目标代码段的特权级更高,以防普通程序直接调用目标代码段)时,才会加载目标代码段执行,否则会报一般保护异常 (General-protection exception)


那么特权等级(也就是 CPL)是怎么变化的呢,我们之前说了 CPU 运行于用户空间时,处于用户态,特权等级为 3,运行于内核空间时,处于内核态,特权等级为 0,所以也可以换个问法 CPU 是如何从用户空间切换到内核空间或者从内核空间切换到用户空间的,这就涉及到一个概念:系统调用


系统调用


我们知道用户进程虽然不能执行特权指令,但有时候也需要执行一些读写文件,发送网络包等操作,而这些操作又只能让操作系统来执行,那该怎么办呢,可以让操作系统提供接口,让用户进程来调用即可,我们把这种方式叫做系统调用,系统调用可以直接由应用程序调用,或者通过调用一些公用函数库或 shell(这些函数库或 shell 都封装了系统调用接口)等也可以达到间接调用系统调用的目的。通过系统调用,应用程序实现了陷入(trap)内核态的目的,这样就从用户态切换到了内核态中,如下


应用程序通过系统调用陷入内核态


那么系统调用又是怎么实现的呢,主要是靠中断实现的,接下来我们就来了解一下什么是中断


中断


陷入内核态的系统调用主要是通过一种 trap gate(陷阱门)来实现的,它其实是软件中断的一种,由 CPU 主动触发给自己一个中断向量号,然后 CPU 根据此中断向量号就可以去中断向量表找到对应的门描述符,门描述符与 GDT 中的段描述符相似,也是 8 个字节,门描述符中包含段选择子,段内偏移,DPL 等字段 ,然后再根据段选择子去 GDT(或者 LDT,下图以 GDT 为例) 中查找对应的段描述符,再找到段基地址,然后根据中断描述符表的段内偏移即可找到中断处理例程的入口点,整个中断处理流程如下



画外音:上图中门描述符和段描述符只画出了关键的几个字段,省略了其它次要字段


当然了,不是随便发一个中断向量都能被执行,只有满足一定条件的中断才允许被普通的应用程序调用,从发出软件中断再到执行中断对应的代码段会做如下的检查



一般应用程序发出软件中断对应的向量号是大家熟悉的 int 0x80(int 代表 interrupt),它的门描述符中的 DPL 为 3,所以能被所有的用户程序调用,而它对应的目标代码段描述符中的 DPL 为 0,所以当通过中断门检查后(即 CPL <= 门描述符中的 DPL 成立),CPU 就会将 CS 寄存器中的 RPL(3) 替换为目标代码段描述符的 DPL(0),替换后的 CPL 也就变成了 0,通过这种方式完成了从用户态到内核态的替换,当中断代码执行后执行 iret 指令又会切换回用户态


另外当执行中断程序时,还需要首先把当前用户进程中对应的堆栈,返回地址等信息,以便切回到用户态时能恢复现场


可以看到 int 80h 这种软件中断的执行又是检查特权级,又是从用户态切换到内核态,又是保存寄存器的值,可谓是非常的耗时,光看一下以下图示就知道像 int 0x80 这样的软件中断开销是有多大了


系统调用


所以后来又开发出了 SYSENTER/SYSCALL 这样快速系统调用的指令,它们取消了权限检查,也不需要在中断描述表(Interrupt Descriptor Table、IDT)中查找系统调用对应的执行过程,也不需要保存堆栈和返回地址等信息,而是直接进入CPL 0,并将新值加载到与代码和堆栈有关的寄存器当中(cs,eip,ss 和 esp),所以极大地提升了性能


分段内存的优缺点


使用了保护模式后,程序员就可以在代码中使用了段选择子:段偏移量的方式来寻址,这不仅让多进程运行成为了可能,而且也解放了程序员的生产力,我们完全可以认为程序拥有所有的内存空间(虚拟空间),因为段选择子是由操作系统分配的,只要操作系统保证不同进程的段的虚拟空间映射到不同的物理空间上,不要重叠即可,也就是说虽然各个程序的虚拟空间是一样的,但由于它们映射的物理地址是不同且不重叠的,所以是能正常工作的,但是为了方便映射,一般要求在物理空间中分配的段是连续的(这样只要维护映射关系的起始地址和对应的空间大小即可)


段式内存管理-虚拟空间与实际物理内存的映射


但段式内存管理缺点也很明显:内存碎片可能很大,举个例子



如上图示,连续加载了三个程序到内存中,如果把 Chrome 关闭了,此时内存中有两段 128 M的空闲内存,但如果此时要加载一个 192 M 的程序 X 却有心无力了 ,因为段式内存需要划分出一块连续的内存空间,此时你可以选择把占 256 M 的 Python 程序先 swap 到磁盘中,然后紧跟着 512 M 内存的后面划分出 256 M 内存,再给 Python 程序 swap 到这块物理内存中,这样就腾出了连续的 256 M 内存,从而可以加载程序 X 了,但这种频繁地将几十上百兆内存与硬盘进行 swap 显然会对性能造成严重的影响,毕竟谁都知道内存和硬盘的读写速度可是一个天上一个地上,如果一定要交换,能否每次 swap 得能少一点,比如只有几 K,这样就能满足我们的需求,分页内存管理就诞生了


内存分页


1985 年 intel 推出了 32 位处理器 80386,也是首款支持分页内存的 CPU


和分段这样连续分配一整段的空间给程序相比,分页是把整个物理空间切成一段段固定尺寸的大小,当然为了映射,虚拟地址也需要切成一段段固定尺寸的大小,这种固定尺寸的大小我们一般称其为页,在 LInux 中一般每页的大小为 4KB,这样虚拟地址和物理地址就通过页来映射起来了



当然了这种映射关系是需要一个映射表来记录的,这样才能把虚拟地址映射到物理内存中,给定一个虚拟地址,它最终肯定在某个物理页内,所以虚拟地址一般由「页号+页内偏移」组成,而映射表项需要包含物理内存的页号,这样只要将页号对应起来,再加上页内偏移,即可获取最终的物理内存



于是问题来了,映射表(也称页表)该怎么设计呢,我们以 32 位虚拟地址位置来看看,假设页大小为 4K(2^12),那么至少需要 2^20 也就是 100 多万个页表项才能完全覆盖所有的虚拟地址,假设每一个页表项 4 个字节,那就意味着为一个进程的虚拟地址就需要准备 2^20 * 4 B = 4 M 的页表大小,如果有 100 个进程,就意味着光是页表就要占用 400M 的空间了,这显然是非常巨大的开销,那该怎么解决这个页表空间占用巨大的问题呢


我们注意到现在的做法是一次性为进程分配了占用其所有虚拟空间的页表项,但实际上一个进程根本用不到这么巨大的虚拟空间,所以这种分配方式无疑导致很多分配的页表白白浪费了,那该怎么办,答案是分级管理,等真正需要分配物理空间的时候再分配,其实大家可以想想我们熟悉的 windows 是怎么分配的,是不是一开始只分配了 C 盘,D盘,E盘,等要存储的时候,先确定是哪个盘,再在这个盘下分配目录,然后再把文件存到这个目录下,并不会一开始就把所有盘的空间给分配完的



同样的道理,以 32 位虚拟地址为例,我们也可以对页表进行分级管理, 页表项 2^20 = 2^10 * 2^10 = 1024 * 1024,我们把一个页表分成两级页表,第一级页表 1024 项,每一项都指向一个包含有 1024 个页表项的二级页表


图片来自《图解系统》


这样只有在一级页表中的页表项被分配的时候才会分配二级页表,极大的节省了空间,我们简单算下,假设 4G 的虚拟空间进程只用了 20%(已经很大了,大部分用不到这么多),那么由于一级页表空间为 1024 *4 = 4K,总的页表空间为 4K+ 0.2 * 4M = 0.804M,相比于原来的 4M 是个巨大的提升!


那么对于分页保护模式又是如何起作用的呢,同样以 32 位为例,它的二级页表项(也称 page table entry)其实是以下结构



注意第三位(也就是 2 对应的位置)有个 U/S,它其实就是代表特权级,表示的是用户/超级用户标志。为 1 时,允许所有特权级别的程序访问;为 0 时,仅允许特权级为0、1、2(Linux 中没有 1,2)的程序(也就是内核)访问。页目录中的这个位对其所映射的所有页面起作用


既然分页这么好,那么分段是不是可以去掉了呢,理论上确实可以,但 Intel 的 CPU 严格执行了 backward compatibility(回溯兼容),也就是说最新的 CPU 永远可以运行针对早期 CPU 开发的程序,否则早期的程序就得针对新 CPU 架构重新开发了(早期程序针对的是 CPU 的段式管理进行开发),这无论对用户还是开发者都是不能接受的(别忘了安腾死亡的一大原因就是由于不兼容之前版本的指令),兼容性虽然意味着每款新的 CPU 都得兼容老的指令,所背的历史包袱越来越重,但对程序来说能运行肯定比重新开发好,所以既然早期的 CPU 支持段,那么自从 80386 开始的所有 CPU 也都得支持段,而分页反而是可选的,也就意味着这些 CPU 的内存管理都是段页式管理,逻辑地址要先经过段式管理单元转成线性地址(也称虚拟地址),然后再经过页式管理单元转成物理内存,如下


分页是可选项


在 Linux 中,虽然也是段页式内存管理,但它统一把 CS,DS,SS,ES 的段基址设置为了 0,段界限也设置为了整个虚拟内存的长度,所有段都分布在同一个地址空间,这种内存模式也叫平坦内存模型(flat memory model)


平坦内存模型


我们知道逻辑地址由段选择子:段内偏移地址组成,既然段选择子指向的段基地址为 0,那也就意味着段内偏移地址即为即为线性地址(也就是虚拟地址),由此可知 Linux 中所有程序的代码都使用了虚拟地址,通过这种方式巧妙地绕开了分段管理,分段只起到了访问控制和权限的作用(别忘了各种权限检查依赖 DPL,RPL 等特权字段,特权极转移也依赖于段选择子中的 DPL 来切换的)


总结


看完本文相信大家对实模式,保护模式,特权级转换,分段,分页等概念应该有了比较清晰的认识。


我们简单总结一下,CPU 诞生之间,使用的绝对物理内存来寻址(也就是实模式),随后随着 8086 的诞生,由于工艺的原因,虽然地址总线是 20 位,但寄存器却只有 16 位,一个难题出现了,16 位的寄存器该怎么寻址 20 位的内存地址呢,于是段的概念被提出了,段的出现虽然解决了寻址问题,但本质上 CS << 4 + IP 的寻址方式依然还是绝对物理地址,这样的话由于地址会互相覆盖,显然无法做到多进程运行,于是保护模式被提出了,保护就是为了物理内存免受非法访问,于是用户空间,内核空间,特权级也被提出来了,段寄存器里保存的不再是段基址,而是段选择子,由操作系统分配,用户也无法随意修改段选择子,必须通过中断的形式才能从用户态陷入内核态,中断执行的过程也需要经历特权级的检查,检查通过之后特权级从 3 切换到了 0,于是就可以放心合法的执行特权指令了。可以看到,通过操作系统分配段选择子+中断的方式内存得到了有效保护,但是分段可能造成内存碎片过大以倒频繁 swap 会影响性能的问题,于是分页出现了,保护模式+分页终于可以让多进程,高效调度成为了可能


参考



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

计算机网络 传输层

计算机网络 传输层 传输层服务 传输层在网络层的基础上,实现了进程到进程的服务。 传输层通过引入端口号来区分进程,在 UDP/TCP 的首部中,一个端口占用 2B, 即 16 bit。 复用与分用 一台主机上的多个进程,利用一个传输层实体,将数据发送出去,这...
继续阅读 »

计算机网络 传输层


传输层服务


传输层在网络层的基础上,实现了进程到进程的服务。


传输层通过引入端口号来区分进程,在 UDP/TCP 的首部中,一个端口占用 2B, 即 16 bit。



复用与分用


一台主机上的多个进程,利用一个传输层实体,将数据发送出去,这就叫复用。而远端主机的传输层实体,收到了很多数据,然后通过识别端口号,将数据交给具体的进程,这就叫分用(或解复用)。


UDP(无连接传输协议 / 用户数据报协议)


面向报文协议,即一个 UDP 数据报就是应用层交下来一个完整报文。


是一种尽力而为的,非有序的、无连接的一种协议,通常用于流媒体应用、事务性应用,如 DNS 等。


UDP 的报文结构



UDP 会使用校验和对数据做差错控制编码。


将数据按每16位一组相加,最高位相加产生的进位,回卷到最低位去,如此,将最后的和取反码就是最终的校验和,在接收端,按同样的方法求校验和,与接收端的校验和相加,如果结果不是 16 个 1,则肯定出错,如果是 16 个 1 ,说明大概率没有出错


可靠数据传输的基本原理


可靠的传输通常需要实现数据的不丢失、不乱序、无差错等核心功能。


我所看过的传输层相关的书,都是通过递进式的讲解实现可靠的网络传输,这里也已这种方式来记录。但我这里 rdt1.0、rdt2.0、rdt3.0跟书本上的可能不一致,纯粹是按自己理解的来,我觉得这里能够理解其逐渐完善可靠性的过程就OK了。


rdt1.0 停等协议


先假设传输层以下的实现都是可靠的,不会出现数据差错的情况,**那如何实现不乱序呢?**很简单,我们只要保证按顺序传就可以,发送端先发一个,等接收端收到了再发下一个,如此往复直到数据全部发送完。这样的协议叫做停等协议。



rdt2.1 ACK 和 NAK


这里看上去确实实现了按序到达,但这是建立在底层不会出现差错的情况下,一旦出现了一些异常情况,比如**分组在传输过程中出现了比特反转(0被识别为1,1识别为0)**该如何处理?


那需要接收端给发送端一个答复,接收端是否收到了合法的分组。如果收到了正确的分组,接收端回复一个确定(ACK),发送端识别到时ACK,就可以继续发送下一个分组,若收到了错误的分组,则回复一个否定(NAK),那接收端就需要重发当前分组。



如图这里主要有两种异常情况:




  1. 分组在传输过程中出错:


    接收端检测到错误之后,回复 NAK , 发送端收到 NAK 后,需要重传上一个分组。




  2. ACK 或 NAK 在传输过程中出错:


    发送端不能识别这到底是 ACK 还是 NAK,于是就统一按照这是 NAK 来处理,于是就重发上一个分组。而这里重发后,会导致接收端收到重复的分组,接收端需要丢弃这个分组,然后回复 ACK。




当然,如果过程中没有出错,那就一步一步的正常的将数据发送给接收端就OK了。


rdt2.2 序号


在rdt2.2中,我们开始使用序号,给分组进行编号。并且使用上一个分组的编号来替代 NAK ,什么意思呢?如果用户在收到一个分组 P3 时,发现数据出错了,那这时它就不再回复 NAK 了,而是回复 ACK = P2,表示接收端才收到 P2,于是发送端需要重传 P3,这样实现了跟 NAK 一样的效果。这么做为后面的流水协议奠定了基础。



可以看到,这里用分组的序号替代单纯的 ACK/NAK,异常情况跟 rdt 2.1 非常相似。


rdt3.0 超时重传机制


如果分组在传输过程中丢失了,接收端等待下一个分组,发送端等待 ACK,双方如果都这么默默的等待下去,那不就出现死锁了?


我们可以让发送端发送一个分组后,设置一个超时定时器,如果在一段时间后还没有收到 ACK, 触发超时重传机制,重发上一个分组,这里不论是分组丢失还是 ACK 丢失,都可以重发。如果是 ACK 丢失,接收端会收到重复的分组,跟 rdt2.0 的方案类似,将重复的分组丢弃就可以了,并且对当前的分组发送 ACK 。



由于定时器的设置,不可能百分百的确定分组或 ACK 丢失,可能在超时重传后好一段时间,对前面某个分组的 ACK 慢悠悠的过来了,这是发送端可以什么都不做,无视它就可以了,因为前面已经收到过对这个分组的确认。


超时计时器的时间需要根据RTT的时间来动态设置,如果超时时间设置得过短,会引起不必要的重传,如果设置得过长,会使重传效率变低。


到目前为止,停等协议已经实现了可靠传输,但是它的信道利用率很低


滑动窗口协议(slide window)


停等协议的信道利用率很低,是因为它一次只发一个分组,确认发送完成后才能发另一个,这中间会有很长的等待时间,如何提高信道利用率呢?显然,我们在发完一个分组后,不等 ACK 就在此下一个,看上去就能提高效率。因此,这种连续发送多个未经确认的分组的协议叫做流水线协议


在发送端会有一段缓冲区,可以存储已经发送出去但还没有收到 ack 的分组,用于后面检错重发、超时重发等。而在接收端,也有一段缓存区,主要是为了适应发送端发送速率和接收端接收速率不一致的情况,当发送端速度过快时,可以用缓冲区暂存一下。


发送端缓冲区的发送窗口长度叫做 sw , 接收端缓冲区接收窗口长度叫做 rw 。窗口是缓冲区的一个子集。



  • 当 sw = 1,rw = 1 时,这就是停等协议

  • 当 sw > 1,rw = 1 时,这就是 GBN 协议

  • 当 sw > 1,rw > 1时,这就是 SR (selective repeat)协议



通常发送窗口的后沿指针指向低位的首个已发送但没有确认的分组,前沿指针指向高位第一个已发送的分组,如果发送的分组数量比最大发送窗口长度小,那表明发送端可以继续发送分组,也就是前沿指针可以向前移动。如果低位的未确认的分组收到了确认,则后沿指针可以向前移动,移动到首个未确认的分组。这时,发送窗口被使用的长度就变小了,发送端就可以继续发送分组。


而接收端收到的分组在接收窗口范围内时,可以接收并对其作出确认,若没有比它序号更低的分组等待被确认,则接收窗口的后沿指针可以向前移动,移动到首个未确认的分组。由于窗口指针的移动,接收端又有了更多的空间来接收后续的分组。如果接收的分组超出接收窗口范围,则将其丢弃,因为接收窗口暂时没有足够空间去保存更多的分组。


GBN

GBN 协议特点:rw = 1, 顺序接收,累积确认。


双方正常的流程:发送方可以按序发送多个分组,这些分组有序的到达接收端,接收端对其一一作出确认,接收端每接收一个分组,窗口就向前移动一位,然后接收到下一个分组,再对下一个分组进行确认,然后再往前移动。


双方的异常流程:



  1. 如果接收端收到乱序的分组,比如在等待 P1 的过程中,结果收到一个 P2,那接收端会将其丢弃,并发送其上一个分组,也就是 P0 的确认。发送端收到 P0 的 ACK 后,就会将 P0 之后的所有的分组重发一遍,这也就是 go back N 的意义。总结起来就是,如果低位的分组出错(失序或超时)了,需要从低位开始重传后续所有的分组



SR

SR 协议特点:rw > 1, 可以乱序接收非累积确认,或者叫单独确认,收到哪个分组,就给哪个确认。窗口向前滑动时,滑到从低位开始第一个未确认的分组


双方的正常流程:发送方依次发送多个分组,每个分组要设置一个独立的超时计时器,由于 SR 的接收窗口长度大于 1,因此只要是在窗口范围内的分组,它都可以接收,并单独对其作出确认。发送方收到确认后,取消超时计时器,如果没有比当前更小的需要被确认的分组,那后沿指针就向前移动,发送端继续发送后续的分组。


双方的异常情况:




  1. 发送方的某个分组丢失了,那接收方就无法对其作出确认,接收窗口的后沿指针无法向前移动,一段时间后,发送端的这个分组的超时计时器会触发重传,然后重新设置超时计时器。接收方收到这个分组后,对其作出确认,然后接收窗口的前沿指针得以向前移动,从而可以接收更多的分组。而发送方收到确认后,发送窗口的后沿指针也会向前移动,发送端就可以发送后续的分组。




  2. 接收端的某个分组的ACK 丢失了,这里发送端对应的分组超时计时器会进行重发,重发后接收端收到了重复的分组,而接收端发现这个分组已经被确认过,直接将其忽略即可。如果接收端的某个分组的 ACK 是因为网络拥塞导致超时之后才到达发送端,那发送端发现自己已经重发了这个分组,正在等待确认中,那发送端直接忽略这个ACK就可以了,同理,如果某个分组已经通过超时重传后被确认了,而首次发送的 ACK 才缓缓到达,发送端同样可以忽略这次的确认,因为这个分组已经被确认过了。




GBN & SR 的差别

GBN 使用起来简单,适合在低差错率的网络中使用,使用累积确认(比如,对某个分组进行确认,则表明这个分组及以前所有的分组,都被正确接收了,而之后的分组没有被收到),一旦出错,需要重发之后所有已经发送过的分组,会有比较大的代价。


SR 实现起来复杂些,因为每个分组都有单独的超时计时器,只需要对出现差错的分组单独重传,适合在差错率较高的网络中使用。使用非累积确认。


TCP(面向连接传输协议)


在可靠传输原理中,我们为了方便,是给分组设置了编号,但实际在 TCP 中是对字节流进行编号。


由于我们到了具体的 TCP 协议,通常需要将分组描述为报文段


TCP 采用了前面可靠传输原理中的流水线协议、累积确认、单超时计时器等特点,是 SR 和 GBN 的混合,同时还支持流量控制,拥塞控制。


段结构



TCP 在载荷部分前,会加上自己的控制信息,也就是 TCP 的首部。如果不包含选项部分,首部的长度为 20个字节。


介绍几个主要的字段。


序号:由发送端设置,表示发送端发送报文段的首字节编号。


确认号:由接收端设置,当 ACK = 1时,确认号才有效,表示接收端期望收到字节编号。同时,在累积确认中表示这个编号之前的分组已经收到。


SYN:表示要建立连接。


FIN:表示要拆除连接。


可靠数据传输


TCP 的可靠传输原理大致可以理解为是 SR + 累积确认 + 快速重传。TCP 的发送端不会为每个发出去的报文段设置超时计时器,而是在发送窗口滑动到首个未确认的报文段时启动一个超时计时器。


TCP 对于乱序到达的分组,没有规定该存储还是丢弃,可以自由实现。


快速重传


低位的分组,在超时计时器触发超时之前,如果收到了连续 3 次重复的对当前报文段的确认,可以认为当前报文段已经出现了差错,发送端就可以提前重发这个报文段,而不用等到超时计时器生效。


TCP 的实际实现中,在收到某个分组后,会短暂的等待一会,等收到下一个分组后,再发送确认,这样就可以只发送 1 次确认,这也是利用了累积确认的优点。当然,它不能等待太久,并且等待的分组只能有 1 个,如果下一个分组到来,立刻发送确认。


流量控制


流量控制时为了解决发送端的发送速率和接收端的读取数据不一致的问题,通常是发送端发送的太快,超出了接收端的缓冲区,超出的这些报文段就会被丢弃,那发送端发这么多也就没有了意义。


接收方在发送确认时,可以将自己可用的缓冲区大小放在 TCP 首部的接收窗口字段中,发送端收到这个 ACK 后,就知道是否还能够继续发送数据,可以根据接收端的窗口,调节自己的窗口大小。


连接管理


TCP 是面向连接的,在正式传送数据前,需要做一些准备工作,如:协商起始序号,设置发送窗口、接收窗口等。



连接建立被称为“三次握手”,TCP 的连接建立过程如下:


连接建立前,客户端处于连接关闭状态,服务端处于监听状态。



  1. 客户端发送连接建立请求,SYN=1,同时设置一个随机的初始字节序号x,即 seq=x。

  2. 服务端收到了连接建立请求,设置 ACK=1,表示同意建立请求,同时设置请求号 ack=x+1,TCP 规定 SYN 报文段不能携带数据,但需要消耗 1 个序号,因此这里的 ack=x+1。这部分操作表示服务端同意建立请求。同时,第二次握手还有服务端作为发送方希望与客户端建立连接的请求,所以,这里有跟第一次握手相似的数据,SYN=1, seq=y,y 是服务端随机生成的序号。

  3. 客户端收到了服务端发来的 ack,那客户端作为发送方与服务端的连接就已经建立,这时客户端就已经可以向服务端发送数据了。同时也收到了服务端发来的连接建立请求,因而这次客户端需要回复 ACK=1,ack=y+1, 表示同意建立请求,然后这次不论是否传送数据 seq都是 x+1。


经过上面三次握手,客户端与服务端的双向的连接就建立起来了。


当双方通信完毕后,TCP 需要拆除连接。连接拆除的过程通常叫做“四次挥手”,过程如下:




  1. 客户端发送 FIN=1,表示希望拆除连接。

  2. 服务端收到后发送 ACK=1。(客户端收到ACK后,客户端到服务端的连接就拆除了,但服务端到客户端的连接仍然可用,服务端仍然可以向客户端发送数据)

  3. 服务端发送 FIN=1,表示希望拆除连接。

  4. 客户端收到后发送 ACK=1。服务端收到后确认后,连接即拆除。但站在客户端的角度,作为最后一个发送消息的角色,它还不知道自己发送的确认有没有准确的到达服务器,所以在发送完 ACK 后,会等待一段时间,如果服务端没有重传FIN,那客户端作为接收方到服务端的接收连接也就断开了,那到目前为止,所有的连接就都拆除了。


上面第四步客户端会有一个等待时间来防止 ack 出现差错,但这个方案也不是完美的,如果服务端发现这个ack 有差错,重发了一个连接拆除的请求,但这个请求慢悠悠过了好一会才到达客户端(时间大于客户端的等待时间),可是客户端已经关闭了连接,那服务端短时间内就没法收到拆除连接的 ack 了。


因此,作为拆除连接的最后一环,第四次挥手无论怎样都不会完美,只能尽可能的降低出现问题的概率。


为什么要设置 SequenceNum?


防止滞留在网络中的上一次连接中的一些段,被识别位本次连接中的段。SequenceNum 的长度是 32 位,可以编址 4G 的字节,这么大的范围在两次连接中很难出现相近的序号段,一旦序号段差别很大,这个“野“报文段也就很难出现接收窗口中,就会被接收方丢弃。


为什么2次握手不行?



  1. TCP 是全双工的连接,建立连接时,既要有客户端到服务端的连接,也要有服务端到客户端的连接。建立一个方向的连接需要 2 次握手,两个方向就是 4 次,但是在服务端向客户端确认连接时,可以捎带上服务端对客户端的连接建立请求,这样就合并了一次握手,所以有三次。

  2. 如果只有 2 次握手,且服务端的确认出现差错的情况下,客户端就收不到建立连接的确认,这个方向的连接就会失败。而服务端不知道自己的连接确认丢失了,导致服务端到客户端的连接建立了,这样就出现了“半连接”的情况。如果服务端有大量的这种“半连接”,会极大的消耗服务端的性能。如果上一个连接滞留在网络中的报文段到达了接收方,还有可能阴差阳错的被服务端接收。



TCP的拥塞控制


拥塞时指过多的分组被注入到网络中,超出了网络的处理能力,导致大量的分组 ”拥挤“ 在网络中间设备队列中等待转发,网络性能显著下降的现象。


拥塞的直接后果:



  1. 数据分组通过网络的延时显著增加;

  2. 由于中间网络设备的队列满导致大量的分组被丢弃。



拥塞控制就是通过合理调度、规范、调整向网络中发送数据的主机数量、发送速率或数据量,以避免拥塞或尽快消除已发生的拥塞。拥塞控制可以在不同层实现,比较典型的是在网络层和传输层进行拥塞控制。


拥塞控制的基本思想是 AIMD。拥塞窗口的调整主要分为慢启动阶段和拥塞避免阶段。在慢启动阶段,每收到 1 个确认段,拥塞窗口增加 1 个 MSS,每经过 1 个 RTT,拥塞窗口增长 1 倍;在拥塞避免阶段,每经过 1 个 RTT,拥塞窗口才增长 1 个 MSS。



如何检测网络中是否发生了拥塞?



  1. 分组发生了超时

  2. 连续收到了多个对同一分组的重复确认。

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

毕业三年,我活成了理想中的样子?

又到了毕业季,不知不觉毕业已经三年了 🤔 在这三年期间对生活有了很多感悟,俗话说“字节一年,人间三年”,感觉自己又读了两个大学 hh 对这三年做个总结吧,同时也分享下自己在杭州、在大厂的所见所得所感,也希望对你有所帮助~ 一、关于我 第一次以这种形式与大家见...
继续阅读 »

又到了毕业季,不知不觉毕业已经三年了 🤔


在这三年期间对生活有了很多感悟,俗话说“字节一年,人间三年”,感觉自己又读了两个大学 hh


对这三年做个总结吧,同时也分享下自己在杭州、在大厂的所见所得所感,也希望对你有所帮助~


一、关于我



第一次以这种形式与大家见面,还是自我介绍下吧


希望尽量用简短的文字让你了解我,同时磨平一些行文的信息差


那我们从一个小游戏开始吧



1.1 “生命年轮”游戏


2.game.jpg



  • 以七年为一个节点,写下七年中,每年对自己最重要的事情

  • 并分享给朋友,谈谈对这些事件的看法和对自己的影响,从而让彼此更加亲近


1.2 年轮线


成都人,95后,从 2015 年大一到 2022 毕业三年的 7 年


1.3 核心事件



  • 2016 年:坚持计算机行业。“软件工程”作为高考最后一个志愿,大一结束时本想转专业,但最后还是坚持下来

  • 2017 年:担任街舞协会 会长,举办《HipHop 之夜》晚会,邀请了各大高校协会参与

  • 2018 年:大三确认技术方向,开始发力,自学前端

  • 2019 年:大四实习,应届去了杭州字节跳动

  • 2020 年:重新定义了学习,面向方法论成长

  • 2021 年:遵从内心,学会勇敢,想清楚自己真正喜欢和擅长什么

  • 2022 年:再见杭州,你好魔都。去上海寻找无限可能


1.4 个人感受


性格成就现在的我


我是那种“玩和学”能够分开的人,学的时候认真学,玩的时候也会认真玩。所以大一、大二严格来说是“玩”过去的,我过的很开心。大三决定不考研后,开始自学专业课,应届能进大厂


方法论很重要


不得不说公司 2 年多的成长能够抵过大学 4 年的学习(没说不重要)。身边结识了一群优秀的人,才慢慢明白,自己学生生涯的思维是不全面的,学习方式是不高效的,由此才开始慢慢调整


选择大于努力


人生面临各种选择,无论是学生时代还是涉足社会。如果大方向是错的,再怎么努力也不会看到好结果


二、城里、城外


那么开始我的分享


3.city.jpg



苏小姐道:“法国也有这么一句话。不过,不说是鸟笼,说是被围困的城堡,城外的人想冲进去,城里的人想逃出来。鸿渐,是不是?”——《围城》



2.1 小故事


应届拿到几个大厂 offer,拥有不错的年薪,这也确实是我大学时代的梦,骄傲地跟父母报完喜讯后,马上订了台心仪已久的 Mac Pro。来到杭州报道,同事、环境等都让我格外欣喜和满足。我终于 进入 了字节


工作一年多,萌生了 国留学的想法,想再深造深造。同事D 再三劝阻我重视“沉默成本”,同时目前也是快升职之际,这种时间节点不常,有应该把握,另外出国的话以后稳定了随时都可以出去。句句在理,我被说服了,还是选择 下来搏一搏


想着暂时不出去的话,那就先 外企吧。随后去报了雅思课程,无论是之后留学还是外企,总归没有坏处。却因为疫情原因,兜兜转转花了半年终于考过了雅思。有了英语能力加成,加上本身技术底子不错,准备了一阵,拿到了苏州微软的 offer,最后却因为各种原因 拒绝


一个饭后,阳光明媚,约着同事C 和同事D 下去逛逛,吐槽 了下当下种种。C 告知了他准备 离开 的想法,我感到稍许失落,但也表示理解。很快,我们就相聚在散伙饭桌上了


2.2 感悟



关于“城里城外”故事还有很多,相信在你的生活中也萦绕着它们



站在城里:理性看待 对当下的不满


不满足于当下,本身是中性的,它既能变成追求美好生活的 动力,也可能成为负能量的源泉,让我们迷失在报怨中。应该要正确看待欲望



  • 看清改变的迫切性和必要性

  • 树立清晰的目标,制定计划

  • 按照计划推进、根据状况调整


不盲目憧憬城外:按照自己的节奏走


城外风光似乎无限美好,为什么他一年就能考上研、升职加薪、出国留学……,总是被别人牵着走,和别人比较,自己永远都不会快乐


想起一个故事,高中时听班主任讲他带过的一个学姐,她想去国外读本科,所以高中就自学雅思,2013年左右,国内考点很少,无奈只能抢国外的场次,于是她一个人在新加坡考完了雅思。而我完成雅思,是在工作 2 年之后,与高中生涯相隔近 10 年,但这是属于我的 人生节奏


朋友,认真规划未来,按照自己的轨迹前行,想去的地方总会到达的。同时也能够真心为好友们的成功而祝福


跨越城墙:构筑自己的“储蓄池”


天地悠悠,过客匆匆,潮起又潮落。人生总会面临改变,无论是主动还是被动的。要学会构建自己的认知“储蓄池”


站在《系统论》角度,系统的发展,伴随着正要素和负要素的推动,此消彼长,对应着人生的起伏。我们靠什么去扛过一次次重大改变,仅靠遇事时候的打鸡血、调整心态 是远远不够的,靠的是我们在精神和认知上的 未雨绸缪。即


有计划的提升自我,建立具有缓冲能力的储蓄池,以从容应对每次的变更


4.pool.png


三、幸存者偏差是把双刃剑


5.sword.jpg



幸存者偏差效应:指只能看到经过某种筛选而产生的结果,而没有意识到筛选的过程,因此忽略了筛选掉的关键信息



3.1 小故事


毕业三年,现在年薪也来到了 n十万,但细想身边同事好友 谁又不是呢,自己是个正常水平吧,没有什么优越;学历呢,字节背靠杭州,面向浙大招聘,同事学历也基本至一本起步,自己也没有什么异常。同事W 说如果你学历本科,月入过万,有车有房,帅气阳光,你就已经是百里挑一了。我陷入了沉思


前同事P 来到杭州阿里,相约一起喝酒。我吐槽说其实大厂也就那样,面试造火箭,进去拧螺丝,业务需求 没啥难度,写写界面,封封组件……他说:“你也不能这样说,还是有很多人想进都进不了大厂的,我觉得进大厂可以证明自己”。“证明自己”,我一惊,忽然想起这个被抛在“大明湖畔”的词。回想快毕业时,当时天天想着要进大厂。但身处大厂久了,反而被“幸存者偏差”折磨的不成样


3.2 感悟



不可否认,幸存者偏差是刻在潜意识深处的东西



身处幸存者偏差,让我们与环境拉齐


身处大厂,周围同事都很优秀,我在有意识地吸收他们的工作经验和学习方法。同时也看到自己能力上的不足,并积极改进,慢慢总结出自己的一套方法论 无论什么环境,潜意识本能会 向均线拉齐,即所谓耳濡目染:大学寝室中 5 个室友都在打游戏,你会本能想加入,想着大家不都这样;同样身处于优秀的人之间,你会本能向他们靠齐,努力克服惰性


跳出舒适圈


幸存者偏差的缺点就是长此以往,自己会对当下变得麻木,失去对整体局势变化趋势的感知力 没有什么“大家都这样,那我也就这样了”的借口,当直觉层面感到不对时,要 引起重视。及时跳出舒适圈,去感受更多的可能性,慢慢从这些可能性中筛选出适宜变化的、高效的、自己够得着的生活方式


四、所谓理想


6.dream.jpeg



兄弟B 在英国留学,兄弟L 在云南做生意,我在杭州当码农,我们都有光明的前途。——《新华字典》(玩个梗 hh)



4.1 三十而立


90 后是“悲催”的一代



  • 1982 年,计划生育政策正式写入宪法,90后一代基本都是独生子女

  • 2000 年,互联网起步,70后、80后陆续下海创业,垄断资源

  • 2010 年前后,通货膨胀,物价飞涨

  • 2014 年,房价大涨,高处不胜寒,90后初入社会

  • 2015 年,二胎政策全面开放,90后成为生育主力军,承受高额房贷

  • 2020 年,互联网增量饱和,整个行业开始内卷

  • 2030 年,中国逐步进入老龄化社会,90后延迟退休


以上列举的时间线肯定不全或说准确,但我想说的是 90后,同 80后、70后一样,也是背负沉重压力的一代,每个代有着自己的低谷和红利。90后在享受着“第三次工业革命”——互联网时代 的便利的同时,也在 承担 着意想不到的压力,很多 90后开始学会摆烂,佛系的生活渐渐流行开来,不婚、晚婚、晚育是常态


2021 年我国结婚登记数据为 763.6 万对,当我妈催我谈恋爱结婚时,我却只能说:“你看,90后 不都这样?”


在奔三途中的 90后们,我认为首先要“立”的还是“立己”。一次聚餐时,询问老会长Z,我是考研还是工作好,他脱口而出,“去考研吧,提升自己是最没有风险的投资”,这句话我会记一辈子


人们普遍惰于思考,在俗成的时间点随大流,没错,你大概率不会输的很惨。平安喜乐,踞一方天地,亦岂不快哉?所以,更多的是你与自己的博弈


4.2 精神上的富裕是最难满足的


古有“饱暖思淫欲”,当代有《马斯洛需求层次理论》金字塔。人有基本的生理、安全和归属需求,能生存下去后,开始思考价值认可和 理想抱负。感谢祖国的强大让我们处于和平的年代,能有机会去追求形而上的东西


7.tri.png


每次层需求的满足都是奔赴下一境界的激励源,相对的,需求层次越高,也 更难得到满足,取决于个人的潜力点和你的预期高度。还有一点就是,人的欲望是无穷无尽的,每个阶段有每个阶段的执念,是安于现状还是继续奋勇前行,也是你说的算。但我认为,对于精神层面的追求,你至少应该有那么一次是为自己而活的


4.3 理想的样子


“理想”本来就是一个很虚的概念,它可以杂糅进很多虚幻的东西,但“目标”不是,想清楚自己到底想要什么,勇敢地朝着这个它前进,慢慢部署自己的硬实力和软实力,直到逐渐达到那个高度


对于我来说,为未来 奋斗 过程中的自己,本就是我 理想 中的样子!


五、回归生活


在祖国广袤的土地上,在鳞次栉比的大厦里,我用一把键盘 养活 了自己


如果你问我理想的生活是什么,我会告诉你,我拼尽全力,只是为了能够 平凡 地度过这一生。正如万青唱的那样:傍晚6点下班……


8.song.JPG


感谢你的阅读


By Liam


2022.05.19 于杭州


收起阅读 »

不一样的深拷贝

web
对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛逼的深拷贝, 1.思考 众所周知普通的数据类型是值存储,而复杂类型是通过开辟内存空间来存储数据的,我们通过...
继续阅读 »

对于深拷贝这个概念在面试中时常被提起,面试官可能让你实现深拷贝需要考虑那些因素,或者直接让你手写封装一个深拷贝,那么今天就和大家探讨一下一个让面试官感到牛逼的深拷贝,


1.思考


众所周知普通的数据类型是值存储,而复杂类型是通过开辟内存空间来存储数据的,我们通过内存地址从而查找数据,为了可以完全得到一个与原对象一模一样但又没有内存地址关联的深拷贝,我们需要考虑的因素其实有很多,
1.Object.create()创造的对象 Object.create()详细介绍


  let obj = Object.create(null)
obj.name = '张三'
obj.age = 22

这个对象是一个没有原型的对象,大部分对象都有自己的原型,可以使用公共的方法,但这个却不行,我们是不是应该把它考虑进去?


2.symbol作为属性名的情况 Symbol详细介绍 以及
for in 详细介绍


let obj = {
name: 'aa',
age: 22,
[Symbol('a')]: '独一无二的'
}

对于带有symbol的属性,在 for in 的迭代中是不可枚举的,我们是不是需要考虑如何解决?


3.对于修改对象的属性描述 Object.defineProperty()


let obj = { name: 'ayu', age: 22, sex: '男' }
Object.defineProperty(obj, 'age', {
enumerable: true,
configurable: true,
value: 22,
writable: false
})

这里我们改写了原对象的属性描述,age变得无法枚举,for in 也失去效果,并且很多默认的属性描述信息,我们是不是在拷贝后也应该和原对象保持一致?


4.对象的循环引用


let obj = { name: 'ayu', age: 22, sex: '男' }
obj.e = e

obj对象中有个e的属性指向obj,造成相互引用,当我们在封装深拷贝时,主要是通过递归来逐层查找属性值的情况,然后对其进行操作,如果出现这个情况,就会死循环递归造成栈内存溢出,这种情况难道也不值得考虑嘛?


5.一些特殊的对象
都说万物皆对象,对象其实有很多类型,正则,日期(Date),等都需要特殊处理
而函数和数组就比较简单


6.深拷贝的多数要点
也就是当一个对象里面嵌套了多层对象,这个大家应该都知道,我们通常一般使用递归去处理,再结合上面分析的因素就可以封装函数了


const isComplexDataType = (obj) => (typeof obj === 'object' || typeof obj === 'function') && obj !== null
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp) return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] =
isComplexDataType(obj[key]) && typeof obj[key] !== 'function' ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}

思路
从deepclone这个函数开始说起



  1. 1.如果对象的构造器是Date构造器,则我们使用Dte构造器再构造一个Date

  2. 如果对象的构造器是正则构造器再构造一个正则

  3. WeakMap我们先不提,allDesc是拿到原对象所有的属性(可枚举以及不可枚举)以及对应的属性描述信息

  4. cloneObj是我们根据第三步拷贝的一个新的对象的信息,不过是一个浅拷贝,而且我们考虑了原型不存在的情况 Object.assin与Object.create的区别

  5. 通过for of 循环 Reflect.ownKeys(obj) Reflect.ownKeys()用法 (Reflect.ownKeys()可以遍历对象自身所有的属性(symbol,不可枚举都可以),然后重新将obj的key以及对应的值赋值给cloneObj,并且对obj[key]的值做了讨论,当它是对象并且不是函数时,我们递归处理,否则里面为普通值,直接赋给ObjClone


对于deepClone的第二个参数WeakMap来讲, 请大家想想最开始我们提到的一个问题,我们有一个对象,然后我们填了了一个属性,属性为这个对象,这是在相互引用,如果我们处理这样的对象,也使用递归处理,那么就是死循环,因此我们需要一个数据结构来解决,每次我们递归处理的时候,都把obj,以及赋值的cloneobj对应存储,当遇到死循环的时候直接return这个对象即可
WeakMap详细介绍·


(本文用到大量ES5以后的API,推荐阅读阮一峰老师的ES6,这样才能理解的透彻)

作者:当然是黑猫警长啦
来源:juejin.cn/post/7120893997718962213

收起阅读 »

简单理解Vue的data为啥只能是函数

web
前言 在学习vue的时候vue2只有在组件中严格要求data必须是一个函数,而在普通vue实例中,data可以是一个对象,但是在vue3出现后data必须一个函数,当时看着官方文档说的是好像是对象的引用问题,但是内部原理却不是很了解,今天通过一个简单的例子来说...
继续阅读 »

前言


在学习vue的时候vue2只有在组件中严格要求data必须是一个函数,而在普通vue实例中,data可以是一个对象,但是在vue3出现后data必须一个函数,当时看着官方文档说的是好像是对象的引用问题,但是内部原理却不是很了解,今天通过一个简单的例子来说明为啥data必须是一个函数


参考 (vue2data描述)


参考: (vue3data描述)


1.Vue3中的data


const { createApp } = Vue
const app = {
data: {
a: 1
},
template: `

{{a}}


`

}
createApp(app).mount('#app')

image.png
可以看到上来vue就给了警告说明data必须是一个函数 下面直接抛错


2.vue中的data


var app = new Vue({
el: '#app',
data: { a: 'hello world' }
})


这种写法是可以的,前面提过普通实例data可以是对象,但是在组件中必须是函数,
那么在vue2中难道普通实例就没有缺陷嘛?

答案:是有缺陷的,
比如这样


<div id="app1">{{ message }}div>
<div id="app2">{{ message }}div>


const data = { message: 'hello world' }
const vue1 = new Vue({
el: '#app1',
data
})

const vue2 = new Vue({
el: '#app2',
data
})


这样在页面中会显示2个内容为hello world的div标签
那么当我们通过实例去改变messag呢?


 vue1.message = 'hello Vue'

image.png


奇怪的事情发生了,我知识改变了vue1的实例中的数据,但是其他实例的数据也发生了改变,相信很简单就能看出来这应该是共用同一个对象的引用而导致的,这在开放中是非常不友好的,开发者很容易就产生连串的错误,vue2也知道这种缺陷只是没有在普通实例中去体现而已,只在组件中实现了对于data的约束


为了让大家更好的立即为啥data必须是一个函数,黑猫在此简单实现一个vue的实例然后来证明为啥data是一个函数,以及如果data不是一个函数,我们应该如何处理


3.证明data是函数以及原理实现


在实现简单原理之前,我们需要搞清楚Vue在创建实例之前,对于data到底做了什么事情简单来说就是:


vue 在创建实例的过程中调用data函数返回实例对象通过响应式包装后存储在实例的data上并且实例可以直接越过data上并且实例可以直接越过data访问属性


1.通过这句描述可以知道Vue是一个构造函数,并且传入的参数中有一个data的属性,我们可以$data去访问,也可以直接访问这个属性,并且我们需要对这个data做代理

那么简单实现如下


function Vue(options) {
this.$data = proxy(options.data())
}
function proxy(options) {
return new Proxy(options, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
}
const data = function () {
return {
a: 'hello world'
}
}
const vue1 = new Vue({
data
})
const vue2 = new Vue({
data
})
vue1.$data.a = 'hello Vue'
console.log(vue1.$data.a) // hello Vue
console.log(vue2.$data.a) // hello world

通过简单实现可与看出来,当我们的data是一个函数的时候,在Vue的构造函数中,只有有实例创建就有执行data函数,然后返回一个特别的对象,所以当我们修改其中一个实例的时候并不会对其他实例的数据产生变化

那么当data不是一个函数呢 ,我们简单改下代码,代码如下


function Vue(options) {
this.$data = proxy(options.data)
}
function proxy(options) {
return new Proxy(options, {
get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
}
})
}
const data = {
a: 'hello world'
}
const vue1 = new Vue({
data
})
const vue2 = new Vue({
data
})
vue1.$data.a = 'hello Vue'
console.log(vue1.$data.a) // hello Vue
console.log(vue2.$data.a) // hello Vue

可以看出,由于共用一个对象,当代理的时候也是对同一个对象进行代理,那么当我们通过一个实例去改变数据的时候,就会影响其他实例的状态


4.如果data必须是一个对象呢?


假如有人提出如果data是一个对象,那么我们应该如何处理呢,其实也非常简单,在代理的时候我们可以将传入的data对象通过深拷贝即可,这样我们就不会使用相同引用的对象啦。

[深拷贝牛逼封装参考我以前的文章](不一样的深拷贝)


作者:当然是黑猫警长啦
来源:juejin.cn/post/7154664015333949470
收起阅读 »

javascript实现动态分页

web
之前分页都是使用框架给出的分页类来实现分页,当然,体验可能不是那么好。 这次在写YII2.0框架的后台管理系统的小例子的时候,我这也尝试了一下前后分离,用ajax来实现分页跳转。 那么前端的页码绘制及跳页等其他的样式,都是由JavaScript根据后台返回的数...
继续阅读 »

之前分页都是使用框架给出的分页类来实现分页,当然,体验可能不是那么好。


这次在写YII2.0框架的后台管理系统的小例子的时候,我这也尝试了一下前后分离,用ajax来实现分页跳转。


那么前端的页码绘制及跳页等其他的样式,都是由JavaScript根据后台返回的数据拼接而成。我的分页效果如下图所示:






 


大概就是上面的样子。


Html代码如下:对照第一张图片


<ul> 
    <li><span>1<span data-id="1"></span></span></li>
    <li><a data-id="2">2</a></li>
    <li><a data-id="3">3</a></li>
    <li><a data-id="4">4</a></li>
    <li><a data-id="5">5</a></li>
    <li><a data-id="6">6</a></li>
    <li><a data-id="7">7</a></li>
    <li><a data-id="8">8</a></li>
    <li><a data-id="false"> ... </a></li>
    <li><a data-id="11"> 11 </a></li>
    <li><a data-id="next"> &gt;&gt; </a></li>
</ul>

JavaScript代码如下:


我这里使用的是纯JavaScript代码,没有使用jquery,这个是考虑到兼容性的问题。


/**
* @name 绘制分页
* @author camellia
* @date 20200703
* @param pageOptions 这是一个json对象
* @param pageTotal 总页数
* @param curPage 当前页数
* @param paginationId  显示分页代码的上层DOM的id
*/

 function dynamicPagingFunc(pageOptions)
 {
    // 总页数
    var pageTotal = pageOptions.pageTotal || 1;
    // 当前页
    var curPage = pageOptions.curPage || 1;
    // 获取页面DOM对象
    var paginationId = document.getElementById(''+pageOptions.paginationId+'') || document.getElementById('pagination');
    // 如果当前页 大于总页数  当前页为1
    if(curPage>pageTotal)
    {
       curPage =1;
    }
    var html = "<ul>  ";
    /*总页数小于5,全部显示*/
    if(pageTotal<=5)
    {
       html = appendItem(pageTotal,curPage,html);
       paginationId.innerHTML = html;
    }
    /*总页数大于5时,要分析当前页*/
    if(pageTotal>5)
    {
       if(curPage<=4)
       {
          html = appendItem(pageTotal,curPage,html);
          paginationId.innerHTML = html;
       }
       else if(curPage>4)
       {
          html = appendItem(pageTotal,curPage,html);
          paginationId.innerHTML = html;
       }
    }
    // 显示到页面上的html字符串
    // var html = "<ul>  ";
    // html = appendItem(pageTotal,curPage,html);
    html += "</ul>";
    // 显示至页面中
    paginationId.innerHTML = html;
 }
 
 /**
  * @name 绘制分页内部调用方法,根据不同页码来分析显示样式
* @author camellia
* @date 20200703
  * @param pageTotal 总页数
  * @param curPage 当前页
  * @param html 显示在页面上的html字符串
  */

 function appendItem(pageTotal,curPage,html)
 {
    // 显示页
    var showPage = 8;
    // 总页数大于XX页的时候,中间默认...
    var maxPage = 9;
    // 开始页
    var starPage = 0;
    // 结束页
    var endPage = 0;
    // 首先当前页不为1的时候显示上一页
    if(curPage != 1)
    {
       html += "<li><a data-id = 'prev' > << </a></li> ";
    }
    // 当总页数小于或等于最大显示页数时,首页是1,结束页是最大显示页
    if(pageTotal <= maxPage)
    {
       starPage = 1;
       endPage = pageTotal;
    }
    else if(pageTotal>maxPage && curPage<= showPage)
    {
       starPage = 1;
       endPage = showPage;
       if(curPage == showPage)
       {
          endPage = maxPage;
       }
    }
    else
    {
       if(pageTotal == curPage)
       {
          starPage = curPage - 3;
          endPage = curPage;
       }
       else
       {
          starPage = curPage - 2;
          endPage = Number(curPage) + 1;
       }
 
       html += "<li><a data-id = '1'> 1 </a></li> ";
       html += "<li><a data-id='false'> ... </a></li> ";
    }
    var i = 1;
    for(let i = starPage;i <= endPage;i++)
    {
       if(i==curPage)
       {
          html += "<li ><span>"+ i +"<span data-id="+i+"></span></span></li>";
       }
       else
       {
          html += "<li ><a data-id = "+ i +">"+i+"</a></li>";
       }
    }
 
 
    if(pageTotal<=maxPage)
    {
       if(pageTotal != curPage)
       {
          html += "<li><a data-id='next' > >> </a></li> ";
       }
    }
    else
    {
       if(curPage < pageTotal-2)
       {
          html += "<li><a data-id='false'> ... </a></li> ";
       }
       if(curPage <= pageTotal-2)
       {
          html += "<li><a data-id = "+pageTotal+" > "+pageTotal+" </a></li> ";
       }
       if(pageTotal != curPage)
       {
          html += "<li><a data-id = 'next' > >> </a></li> ";
       }
    }
    return html;
 }

 调用上边的分页代码:


// 绘制分页码
 var pageOptions = {'pageTotal':result.pageNumber,'curPage':result.page,paginationId:'pages'};
 dynamicPagingFunc(pageOptions);

我这里把分页的样式是引用的公共css中的文件,这里就不展示了,将你的分页html代码把我的代码替换掉就好。


参数的聚体解释以及函数中用到的参数,备注基本都已给出。


下面这部分是点击各个页码时,请求数据及重回页码的部分


/**
 * @name 分页点击方法,因为页面html是后生成的,所以需要使用ON方法进行绑定
* @author camellia
* @date 20200703
 */

 $(document).on('click''.next'function()
 {
     layer.load(0, {shadefalse});
     // 获取当前页码
     var obj = $(this).attr('data-id');
     // 获取前一页的页码,点击上一页以及下一页的时候使用
     var curpages = $("li .sr-only").attr('data-id');
     // 点击下一页的时候
     if(obj == 'next')
     {
         obj = Number(curpages) + 1;
     }
     else if(obj == 'prev')// 点击上一页的时候
     {
         obj = curpages - 1;
     }
     $.ajax({
         //几个参数需要注意一下
         type"POST",//方法类型
         dataType"json",//预期服务器返回的数据类型
         url"?r=xxx/xxx-xxx" ,//url
         data: {'page':obj},
         successfunction (result)
         {
             // 将列表部分的html清空
             document.getElementById('tbody').innerHTML = '';
             // 重新绘制数据列表
             drawPage(result.dbbacklist);
             // 绘制分页码
             var pageOptions = {'pageTotal':result.pageNumber,'curPage':result.page,paginationId:'pages'};
             dynamicPagingFunc(pageOptions);
             layer.closeAll();
         },
         error : function() {
             alert("异常!");
         }
     });
 });

有好的建议,请在下方输入你的评论。


欢迎访问个人博客:guanchao.site


欢迎访问我的小程序:打开微信->发现->小程序->搜索“时间里的”


作者:camellia
来源:juejin.cn/post/7111487878546341919
收起阅读 »

差两个像素让我很难受,这问题绝不允许留到明年!

web
2022年8月8日,linxiang07 同学给我们的 Vue DevUI 提了一个 Issue: #1199 Button/Search/Input/Select等支持设置size的组件标准不统一,并且认真梳理了现有支持size属性的组件列表和每个组件大中小...
继续阅读 »

2022年8月8日,linxiang07 同学给我们的 Vue DevUI 提了一个 Issue:
#1199 Button/Search/Input/Select等支持设置size的组件标准不统一,并且认真梳理了现有支持size属性的组件列表和每个组件大中小尺寸的现状,整理了一个表格(可以说是提 Issue 的典范,值得学习)。



不仅如此,linxiang 同学还提供了详细的修改建议:



  1. 建议xs、 sm 、md、lg使用标准的尺寸

  2. 建议这些将组件的尺寸使用公共的sass变量

  3. 建议参考社区主流的尺寸

  4. 考虑移除xs这个尺寸、或都都支持xs


作为一名对自己有要求的前端,差两个像素不能忍


如果业务只使用单个组件,可能看不太出问题,比如 Input 组件的尺寸如下:



  • sm 24px

  • md 26px

  • lg 44px



Select 组件的尺寸如下:



  • sm 22px

  • md 26px

  • lg 42px



当 Input 和 Select 组件单独使用时,可能看不出什么问题,但是一旦把他俩放一块儿,问题就出来了。



大家仔细一看,可以看出中间这个下拉框比两边输入框和按钮的高度都要小一点。


别跟我说你没看出来!作为一名资深的前端,像素眼应该早就该练就啦!


作为一名对自己严格要求的前端,必须 100% 还原设计稿,差两个像素怎么能忍!


vaebe: 表单 size 这个 已经很久了 争取不要留到23年


这时我们的 Maintainer 成员 vaebe 主动承担了该问题的修复工作(必须为 vaebe 同学点赞)。



看着只是一个 Issue,但其实这里面涉及的组件很多。


8月12日,vaebe 同学提了第一个修复该问题的 PR:


style(input): input组件的 size 大小


直到12月13日(今天)提交最后一个 PR:


cascader组件 props size 在表单内部时应该跟随表单变化


共持续5个月,累计提交34个PR,不仅完美地修复了这个组件尺寸不统一的问题,还完善了相关组件的单元测试,非常专业,必须再次给 vaebe 同学点赞。



关于 vaebe 同学


vaebe 同学是今年4月刚加入我们的开源社区的,一直有在社区持续作出贡献,修复了大量组件的缺陷,完善了组件文档,补充了单元测试,还为我们新增了 ButtonGroup 组件,是一位非常优秀和专业的开发者。



如果你也对开源感兴趣,欢迎加入我们的开源社区,添加小助手微信:opentiny-official,拉你进我们的技术交流群!


Vue DevUI:github.com/DevCloudFE/…(欢迎点亮 Star 🌟)


--- END ---


我是 Kagol,如果你喜欢我的文章,可以给我点个赞,关注我的掘金账号和公众号 Kagol,一起交流前端技术、一起做开源!


封面图来自B站UP主亿点点不一样的视频:吃毒蘑菇真的能见小人吗?耗时六个月拍下蘑菇的生长和繁殖


2.png


作者:Kagol
来源:juejin.cn/post/7176661549115768889
收起阅读 »

vue单页面应用部署配置

web
前端 Vue是一款非常流行的JavaScript框架,它提供了一套高效、灵活、易于使用的前端开发工具。在实际开发中,我们通常会使用Vue来构建单页面应用(SPA),并将其部署到服务器上以便用户访问。本篇博客将介绍如何进行Vue单页面应用的部署配置。 构建生产版...
继续阅读 »

前端


Vue是一款非常流行的JavaScript框架,它提供了一套高效、灵活、易于使用的前端开发工具。在实际开发中,我们通常会使用Vue来构建单页面应用(SPA),并将其部署到服务器上以便用户访问。本篇博客将介绍如何进行Vue单页面应用的部署配置。


构建生产版本


首先,我们需要将Vue应用程序构建为生产版本,这可以通过运行以下命令来完成:


npm run build

该命令将生成一个dist目录,其中包含了生产版本的所有必要文件,例如HTML、CSS、JavaScript等。在部署之前,我们需要将这些文件上传到服务器上,并将其存储在合适的位置。


配置Nginx服务器


接下来,我们需要将Vue应用程序与Nginx服务器结合起来,以便处理HTTP请求和响应。下面是一个简单的配置示例:


server {
listen 80;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}
}

在上面的示例中,我们定义了一个名为“example.com”的虚拟主机,并指定了根目录即Vue应用程序所在的dist目录。同时,我们还设置了默认的index.html文件,并通过location指令来处理所有的HTTP请求。


配置HTTPS加密连接


如果需要启用HTTPS加密连接,我们可以通过以下方式来进行配置:


server {
listen 443 ssl;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

location / {
try_files $uri $uri/ /index.html;
}
}

在上面的示例中,我们使用ssl指令来启用SSL/TLS支持,并设置了证书和私钥文件的路径。同时,我们还将所有HTTP请求重定向到HTTPS连接,以确保数据传输的安全性。


配置缓存和压缩


为了提高Vue应用程序的性能和响应速度,我们可以配置缓存和压缩。下面是一个简单的配置示例:


server {
listen 80;
server_name example.com;

root /var/www/vue-app/dist;
index index.html;

location / {
try_files $uri $uri/ /index.html;

expires 1d;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
}


在上面的示例中,我们使用expires指令来定义缓存时间,并使用gzip指令来启用Gzip压缩。同时,我们还设置了需要进行压缩的文件类型,例如文本、CSS、JavaScript等。


总结


以上就是Vue单页面应用的部署配置步骤。首先,我们需要构建生产版本,并将其上传到服务器上。然后,我们需要通过Nginx服务器来处理HTTP请求和响应,以及启用HTTPS加密连接、缓存和压缩等功能。了解这些配置信息,将有助于我们更好地部署和管理

作者:爱划水de鲸鱼哥
来源:juejin.cn/post/7222651312072802359
Vue单页面应用程序

收起阅读 »

css卡片悬停

web
前言 今天分享一个简单的卡片鼠标悬停动画,初始显示一张图片,当鼠标移至卡片上方时,显示文字,先来看看预览效果: 代码实现 页面布局 <div class="view view-first"> <img src="./images...
继续阅读 »

前言


今天分享一个简单的卡片鼠标悬停动画,初始显示一张图片,当鼠标移至卡片上方时,显示文字,先来看看预览效果:


1.gif

代码实现


页面布局


<div class="view view-first">  
<img src="./images/1.webp" />
<div class="mask">
<h2>Title</h2>
<p>Your Text</p>
<a href="#" class="info">Read More</a>
</div>

</div>

这段代码了一个用于展示图片的容器 <div> 元素,其中包含了一个图片 <img> 元素和一个用于显示图片标题、文字和链接的 <div> 元素。这个容器使用了类名为 viewview-first 的 CSS 类来进行样式控制。


页面样式


.view {
width: 1080px;
height: 1430px;
margin: 10px auto;
border: 10px solid red;
overflow: hidden;
position: relative;
text-align: center;
box-shadow: 1px 1px 2px #e6e6e6;
cursor: pointer;
}
.view .mask, .view .content {
width: 1080px;
height: 1430px;
position: absolute;
overflow: hidden;
top: 0;
left: 0
}
.view h2 {
text-transform: uppercase;
color: #fff;
text-align: center;
font-size: 180px;
padding: 10px;
background: rgba(0, 0, 0, 0.6);
margin: 220px 0 0 0
}
.view p {
font-family: Georgia, serif;
font-style: italic;
font-size: 120px;
color: #fff;
padding: 10px 20px 20px;
text-align: center
}
.view a.info {
display: inline-block;
text-decoration: none;
padding: 7px 14px;
font-size: 60px;
background: #000;
color: #fff;
text-transform: uppercase;
box-shadow: 0 0 1px #000
}
.view a.info:hover {
box-shadow: 0 0 5px #000
}


.view-first img {
transition: all 0.2s linear;
}
.view-first .mask {
opacity: 0;
background-color: rgba(219,127,8, 0.7);
transition: all 0.4s ease-in-out;
}
.view-first h2 {
transform: translateY(-100px);
opacity: 0;
transition: all 0.2s ease-in-out;
}
.view-first p {
transform: translateY(100px);
opacity: 0;
transition: all 0.2s linear;
}
.view-first a.info{
opacity: 0;
transition: all 0.2s ease-in-out;
}

.view-first:hover img {
transform: scale(1.2);
}
.view-first:hover .mask {
opacity: 0.8;
}
.view-first:hover h2,
.view-first:hover p,
.view-first:hover a.info {
opacity: 1;
transform: translateY(0px);
}
.view-first:hover p {
transition-delay: 0.1s;
}
.view-first:hover a.info {
transition-delay: 0.2s;
}

这段 CSS 代码定义了 .view.view-first 这两个类的样式属性。其中,.view 类定义了容器的基本样式,包括宽度、高度、边距、背景颜色、阴影等。.view-first 类定义了容器在鼠标悬停时的效果,包括图片放大、遮罩层透明度变化、标题、文字和链接的透明度和位置变化等。这段代码通过使用伪类 :hover 来控制在鼠标悬停时的效果。同时,这段 CSS 代码中包含了一些过渡效果(transition),通过设置不同的过渡时间和延迟时间,实现了在鼠标悬停时的平滑动画效果。同时,通过使用透明度(opacity)、位移(transform: translateY())和缩放(transform: scale())等属性,实现了图片和文字的渐现和渐变效果。接下来对各个样式进行详细解释:


.view {
width: 1080px;
height: 1430px;
margin: 10px auto;
border: 10px solid red;
overflow: hidden;
position: relative;
text-align: center;
box-shadow: 1px 1px 2px #e6e6e6;
cursor: pointer;
}

设置容器元素的宽度和高度,margin: 10px auto;设置容器元素的外边距,使其在水平方向上居中,上下边距为 10 像素,text-align: center;文本的水平对齐方式为居中,box-shadow: 1px 1px 2px #e6e6e6;设置容器元素的阴影效果,水平和垂直偏移都为 1 像素,模糊半径为 2 像素,阴影颜色为 #e6e6e6。cursor: pointer;设置鼠标悬停在容器元素上时的光标样式为手型。


.view .mask, .view .content {
width: 1080px;
height: 1430px;
position: absolute;
overflow: hidden;
top: 0;
left: 0
}

选中类名为 "mask" 和 "content" 的元素,采用绝对定位,设置topleft偏移量为0。


.view h2 {
text-transform: uppercase;
color: #fff;
text-align: center;
font-size: 180px;
padding: 10px;
background: rgba(0, 0, 0, 0.6);
margin: 220px 0 0 0
}

对字体颜色和大小进行设置,文字水平居中,设置背景色等,text-transform: uppercase;设置标题文本转换为大写。


.view p {
font-family: Georgia, serif;
font-style: italic;
font-size: 120px;
color: #fff;
padding: 10px 20px 20px;
text-align: center
}
.view a.info {
display: inline-block;
text-decoration: none;
padding: 7px 14px;
font-size: 60px;
background: #000;
color: #fff;
text-transform: uppercase;
box-shadow: 0 0 1px #000
}
.view a.info:hover {
box-shadow: 0 0 5px #000
}

对子元素p标签和指定a标签进行字体样式进行设置,text-decoration: none;去除下划线,a元素在鼠标悬停状态下的添加阴影。


.view-first img { 
transition: all 0.2s linear;
}
.view-first .mask {
opacity: 0;
background-color: rgba(219,127,8, 0.7);
transition: all 0.4s ease-in-out;
}
.view-first h2 {
transform: translateY(-100px);
opacity: 0;
transition: all 0.2s ease-in-out;
}
.view-first p {
transform: translateY(100px);
opacity: 0;
transition: all 0.2s linear;
}
.view-first a.info{
opacity: 0;
transition: all 0.2s ease-in-out;
}

.view-first:hover img {
transform: scale(1.2);
}
.view-first:hover .mask {
opacity: 0.8;
}
.view-first:hover h2,
.view-first:hover p,
.view-first:hover a.info {
opacity: 1;
transform: translateY(0px);
}
.view-first:hover p {
transition-delay: 0.1s;
}
.view-first:hover a.info {
transition-delay: 0.2s;
}

对各元素在鼠标悬停状态下的样式进行设置,并添加动画效果,主要动画元素transform: scale(1.2);图片在悬停状态下缩放1.2倍,transform: translateY(0px);在y轴上偏移量,transition-delay: 0.1s;动画延迟时间,ease-in-out缓入缓出。


结语


以上便是全部代码了,总体比较简单,只需要使用一些简单的动画属性即可,喜欢的小伙伴可以拿去看看,根据自己想要的效果进行修改。


作者:codePanda
来源:juejin.cn/post/7223742591372312636
收起阅读 »

裸辞半个月的程序猿在干什么?

序 8月1日,美好的一天,我果断裸辞,在公司1年半交接不过半天时间便匆匆结束了这在里的征程。心情由忧转喜再转忧再转喜,好比是坐山车似的来回起伏。 为什么选择裸辞? 裸辞并不是对自己的不负责任,也不是任性,对于我来说可能是一次重新的洗礼。 大家都明白现在的互...
继续阅读 »


8月1日,美好的一天,我果断裸辞,在公司1年半交接不过半天时间便匆匆结束了这在里的征程。心情由忧转喜再转忧再转喜,好比是坐山车似的来回起伏


image.png


为什么选择裸辞?


image.png


裸辞并不是对自己的不负责任,也不是任性,对于我来说可能是一次重新的洗礼。
大家都明白现在的互联网形势并不是很乐观,在我裸辞的前夕每日优鲜又爆雷了。在如此悲凉的环境之下为什么还是要义务反顾的选择裸辞?我自己裸辞主要有4个原因:


1.之前的工作确实让人心累且身体累。自己需要一段时间来恢复一下身心健康。因为身体是本钱,并不能因为自己是年轻人而肆意挥霍,提前透支。心情精神则更是重中之重,心情愉悦,才能百病不轻,才能有更加积极向上的态度。


2.自己没有大多生活的负担,如家庭,车贷房贷等。这可能也是年轻人这个阶段唯一的一次特权了,因为不久之后这些东西都会接踵而至,那个时候,裸辞这个词也许永远不可能出现在我的字典中。这一次自己还是要好好享受这人生的最后一次特权。


3.对于我这个阶段的程序员来说,好好规划自己的职业生涯显得尤为重要,选择远比努力来得重要。且这个阶段的程序员找工作我始终认为都是一个应该好好准备的过程,不仅仅是查漏补缺一些知识点,更是应该定下心来为自己的将来做一些规划。


4.大环境的不好对于我来说可能有些许影响,但是对自己足够自信的我相信这并不是挡住我裸辞的拦路虎。也许这个思想后面会有转变,但绝对不会是现在!


image.png


当然这些东西某种程度上来说也许都是借口,因为健身学习、查漏补缺、人生规划可以放在平时下班之后或者是双休日。并不一定需要一大段空闲时间来做这些事情。在没辞职之前我也是一直对自己这样说的,我确实也是这样做的,但是总是感觉效果不佳,或者说没有心思,也许这也是我自身意志不够的原因吧。但我相信不少人应该也是同我一样,心理知道但是却不能高效的做到。


裸辞真的好么?


裸辞真的好么?掘金上也有很多针对于年轻人裸辞好坏的讨论,但是对于我来说我觉得效果是不错的。


裸辞之后,我为自己制定了一份所谓的计划表,也确实都的做到了,因为这份计划表实在是太简单了,除了运动就是玩,没有任何学习的计划,持续2周。


1.5点半起床空腹晨跑,我坚持了一天最后被我无情删除了这个计划。(说实话真的难)最后被我改成了早晨起床后做一些室内的有氧运动。


2.上午就是玩手机看视频。(动漫补番,玩玩手游,看看电影电视剧)


3.下午2点-4点半健身房健身游泳。


4.晚上7-9点看书,看完夜跑5km,11点睡觉。


现在的我已经经过了2周的洗礼,体重减了3.4kg,整个人精神状态都好了不少,心情愉悦,一改之前的颓废。自己的改变自己才是最清楚的,文字真的很难描述清楚。有过这种感觉的人也许会明白。


裸辞之后我做了什么


image.png


上面讲到了我前2周的玩耍健身计划,那么后2周的学习健身计划也应该开始了,这篇文章就是计划的开始。运动健身看书的时间是不会做任何修改的,主要是把之前的玩乐的时间划3分之2给学习罢了,细节就不多说了。


我这个人缺点很多,尤其是不能长时间坚持,但是短时间的坚持我从来没有失败过。所以我给自己定制的计划时间都相对来说很短。毕竟裸辞,生活还是要继续的,再次找工作的时间区间也就1个多月时间罢了。



裸辞对于每个人来说意义效果都是不同的,对于我来说裸辞只是让我能够更好的开启我的下段征程。我分享自己的裸辞一是为了给自己一个交代与监督,二是为jym提供一些短浅的建议与经验而已。


在我学习健身计划结束的时候,我同样会分享一篇文章来对自己的第二阶段做一个总结。


非常感谢能看到这里的jym!


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

Android 开发中必须了解的 Context

1. 什么是 context? 作为安卓开发工程师,Context是我们经常使用的一个重要概念。 从代码的角度来看,Context是一个抽象类,它代表着应用程序环境和运行时状态的信息。Context具有许多子类,包括Activity和Service等,每个子类...
继续阅读 »

1. 什么是 context?


作为安卓开发工程师,Context是我们经常使用的一个重要概念。


从代码的角度来看,Context是一个抽象类,它代表着应用程序环境和运行时状态的信息。Context具有许多子类,包括Activity和Service等,每个子类都代表着不同的应用程序环境和状态。


从设计的角度来看,Context是一个非常重要的概念,因为它允许我们在应用程序中访问系统资源,例如数据库,共享偏好设置和系统服务等。Context还允许我们在应用程序中创建新的组件,例如Activity和Service等。


实际上,Context在安卓开发中几乎无处不在。例如,我们可以使用Context来启动一个新的Activity,获取应用程序的资源,读取和写入文件,以及访问系统服务和传感器等。Context还可以帮助我们管理应用程序的生命周期,例如在应用程序销毁时释放资源。


总之,Context是安卓开发中不可或缺的概念,它允许我们访问系统资源,管理应用程序的生命周期,并与系统交互。理解Context的概念和使用方法对于成为一名优秀的安卓开发工程师至关重要。


2. context继承关系


Context
├── ContextImpl
├── ContextWrapper
│ ├── Application
│ ├── Service
│ ├── ContextThemeWrapper
│ │ ├── Activity
│ │ │ ├── FragmentActivity
│ │ │ └── ...
│ │ └── ...
│ └── ...
└── ...

Context是一个抽象类,它有多个直接或间接的子类。


ContextImpl是Context的一个实现类,真正实现了Context中的所有函数,所调用的各种Context类的方法,其实现均来自于该类。


ContextWrapper是一个包装类,它可以包装另一个Context对象,并在其基础上添加新的功能。内部包含一个真正的Context引用,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。


ContextThemeWrapper是一个特殊的包装类,它可以为应用程序的UI组件添加主题样式。主题就是指Activity元素指定的主题。只有Activity需要主题,所以Activity继承自ContextThemeWrapper,而Application和Service直接继承自ContextWrapper。


总之,Context的继承关系非常复杂,但是理解这些关系对于在安卓开发中正确地使用Context非常重要。通过继承关系,我们可以了解每个Context子类的作用和用途,并且可以选择合适的Context对象来访问应用程序的资源和系统服务。


3.Context如何创建


在安卓应用程序中,Activity是通过调用startActivity()方法来启动的。当我们启动一个Activity时,系统会通过调用Activity的生命周期方法来创建、启动和销毁Activity对象。
而其中创建Activity的方法最终是走到ActivityThread.performLaunchActivity()方法。将其中无关方法删除后:
、、、
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
//创建 ContextImpl对象
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;


    //创建Activity对象
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
//Activity初始化
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken, r.shareableActivityToken);
//Theme设置
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}

return activity;
}

、、、
可以看到Activity的创建过程十分清楚:



  1. 创建ContextImpl对象,方法最终走到静态方法ContextImpl.createActivityContext()创建。

  2. 创建Activity对象,最终instantiateActivity()通过调用Class的newInstance()方法,反射创建出来,方法注解到This method is only intended to provide a hook for instantiation. It does not provide earlier access to the Activity object. The returned object will not be initialized as a Context yet and should not be used to interact with other android APIs.,方法只创建了Activity的早期对象,并没有对它做Context的初始化,所以不能调用安卓相关api。简单来说,Activity本身继承自ContextWrapper,这个方法并没有具体实现任何Context的方法,只是将所有方法代理给了内部的baseContext,所以反射创建后,调用任何的系统的方法都是无效的。

  3. Activity初始化,调用Activity.attch(),这个方法对Activity做各种所需的初始化,Context、Thread、parent、Window、Token等等,而Context的初始化就是调用ContextWrapper.attachBaseContext()把第一步创建的ContextImpl设置到baseContext。

  4. Theme设置,前面说到Activity实现的是ContextThemeWrapper,对ContextWrapper扩展并支持了Theme的替换,调用ContextThemeWrapper.setTheme()完成Theme的初始化。


4.一些思考




  1. ContextThemeWrapper作为ContextWrapper一个扩展,它是重写了ContextImpl中的一些关于Theme的实现,也就是说ContextImpl本身也是有Theme的实现,它提供的Theme是整个APP的Theme,而这里扩展了之后,支持了Theme的替换之后,在不同的页面支持了不同的Theme设置。




  2. Context作为应用程序环境和运行时状态的信息,设计初衷上它应该是固定的,在创建成功之后就禁止改变,所以在ContextWrapper.attachBaseContext()中设置了拦截,只允许设置一次baseContext,重新设置会抛出异常。但是在一些特殊的场景中,比如跨页面使用View,或者提前创建View的时候,其实会有场景涉及替换Context。另一个坑是ContextWrapper限制baseContext只允许系统调用。不过在SDK31中,官方提供了一个特殊版本的ContextWrapper,也就是MutableContextWrapper,支持了替换baseContext。




  3. Context设计是很典型的装饰器模式,Context抽象定义了具体的接口;ContextImpl具体实现了Context定义的所有方法;ContextWrapper继承了Context接口,并包装了具体实现ContextImpl;ContextThemeWrapper继承了ContextWrapper并扩展了替换Theme的功能。




5. 附录


装饰器模式是一种结构型设计模式,它允许我们在运行时动态地为一个对象添加新的行为,而无需修改其源代码。装饰器模式通过将对象包装在一个装饰器对象中,来增加对象的功能。装饰器模式是一种非常灵活的模式,它可以在不改变原始对象的情况下,动态地添加新的行为和功能。


装饰器模式的核心思想是将对象包装在一个或多个装饰器对象中,这些装饰器对象具有与原始对象相同的接口,可以在不改变原始对象的情况下,为其添加新的行为。装饰器对象可以嵌套在一起,形成一个链式结构,从而实现更复杂的功能。


装饰器模式的结构由四个基本元素组成:




  1. 抽象组件(Component):定义了一个对象的基本接口,可以是一个抽象类或接口。




  2. 具体组件(ConcreteComponent):实现了抽象组件接口,是被装饰的对象。




  3. 抽象装饰器(Decorator):继承或实现了抽象组件接口,用于包装具体组件或其他装饰器。




  4. 具体装饰器(ConcreteDecorator):继承或实现了抽象装饰器接口,实现了具体的装饰逻辑。




装饰器模式的优点在于:




  1. 可以动态地为对象添加新的行为,无需修改其源代码。




  2. 可以嵌套多个装饰器对象,形成一个链式结构,从而实现更复杂的功能。




  3. 装饰器对象与原始对象具有相同的接口,可以完全替代原始对象。




装饰器模式的缺点在于:




  1. 可能会导致类的数量增加,增加代码的复杂度。




  2. 在装饰器链中,有些装饰器可能不被使用,但仍然需要创建和维护,浪费资源。


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

Android 官方项目是怎么做模块化的?快来学习下

概述 模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。 模块化的好处 模块化有以下好处: 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。 支持并行工...
继续阅读 »

概述


模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。


模块化的好处


模块化有以下好处:



  • 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。

  • 支持并行工作:模块化有助于减少代码冲突,为大型团队中的开发人员提供更高效的并行工作。

  • 所有权:一个模块可以有一个专门的 owner,负责维护代码和测试、修复错误和审查更改。

  • 封装:独立的代码更容易阅读、理解、测试和维护。

  • 减少构建时间:利用 Gradle 的并行和增量构建可以减少构建时间。

  • 动态交付:模块化是 Play 功能交付 的一项要求,它允许有条件地交付应用程序的某些功能或按需下载。

  • 可重用性:模块化为代码共享和构建多个应用程序、跨不同平台、从同一基础提供了机会。


模块化的误区


模块化也可能会被滥用,需要注意以下问题:



  • 太多模块:每个模块都有其成本,如 Gradle 配置的复杂性增加。这可能会导致 Gradle 同步及编译时间的增加,并产生持续的维护成本。此外,与单模块相比,添加更多模块会增加项目 Gradle 设置的复杂性。这可以通过使用约定插件来缓解,将可重用和可组合的构建配置提取到类型安全的 Kotlin 代码中。在 Now in Android 应用程序中,可以在 build-logic文件夹 中找到这些约定插件。

  • 没有足够的模块:相反,如果你的模块很少、很大并且紧密耦合,最终会产生另外的大模块。这将失去模块化的一些好处。如果您的模块臃肿且没有单一的、明确定义的职责,您应该考虑将其进一步拆分。

  • 太复杂了:模块化并没有灵丹妙药 -- 一种方案解决所有项目的模块化问题。事实上,模块化你的项目并不总是有意义的。这主要取决于代码库的大小和相对复杂性。如果您的项目预计不会超过某个阈值,则可扩展性和构建时间收益将不适用。


模块化策略


需要注意的是没有单一的模块化方案,可以确保其对所有项目都适用。但是,可以遵循一般准则,可以尽可能的享受其好处并规避其缺点。


这里提到的模块,是指 Android 项目中的 module,通常会包含 Gradle 构建脚本、源代码、资源等,模块可以独立构建和测试。如下:


一般来说,模块内的代码应该争取做到低耦合、高内聚。



  • 低耦合:模块应尽可能相互独立,以便对一个模块的更改对其他模块的影响为零或最小。他们不应该了解其他模块的内部工作原理。

  • 高内聚:一个模块应该包含一组充当系统的代码。它应该有明确的职责并保持在某些领域知识的范围内。例如,Now in Android 项目中的core-network模块负责发出网络请求、处理来自远程数据源的响应以及向其他模块提供数据。


Now in Android 项目中的模块类型



注:模块依赖图(如下)可以在模块化初期用于可视化各个模块之间的依赖关系。



modularization-graph.png


Now in Android 项目中有以下几种类型的模块:



  • app 模块: 包含绑定其余代码库的应用程序级和脚手架类,app例如和应用程序级受控导航。一个很好的例子是通过导航设置和底部导航栏设置。该模块依赖于所有模块和必需的模块。

  • feature- 模块: 功能特定的模块,其范围可以处理应用程序中的单一职责。这些模块可以在需要时被任何应用程序重用,包括测试或其他风格的应用程序,同时仍然保持分离和隔离。如果一个类只有一个feature模块需要,它应该保留在该模块中。如果不是,则应将其提取到适当的core模块中。一个feature模块不应依赖于其他功能模块。他们只依赖于core他们需要的模块。

  • core-模块:包含辅助代码和特定依赖项的公共库模块,需要在应用程序中的其他模块之间共享。这些模块可以依赖于其他核心模块,但它们不应依赖于功能模块或应用程序模块。

  • 其他模块 - 例如和模块syncbenchmark、 test以及 app-nia-catalog用于快速显示我们的设计系统的目录应用程序。


项目中的主要模块


基于以上模块化方案,Now in Android 应用程序包含以下模块:



































































模块名职责关键类及核心示例
app将应用程序正常运行所需的所有内容整合在一起。这包括 UI 脚手架和导航。NiaApp, MainActivity 应用级控制导航通过 NiaNavHost, NiaTopLevelNavigation
feature-1, feature-2 ...与特定功能或用户相关的功能。通常包含从其他模块读取数据的 UI 组件和 ViewModel。如:feature-author在 AuthorScreen 上显示有关作者的信息。feature-foryou它在“For You” tab 页显示用户的新闻提要和首次运行期间的入职。AuthorScreen AuthorViewModel
core-data保存多个特性模块中的数据。TopicsRepository AuthorsRepository
core-ui不同功能使用的 UI 组件、可组合项和资源,例如图标。NiaIcons NewsResourceCardExpanded
core-common模块之间共享的公共类。NiaDispatchers Result
core-network发出网络请求并处理对应的结果。RetrofitNiANetworkApi
core-testing测试依赖项、存储库和实用程序类。NiaTestRunner TestDispatcherRule
core-datastore使用 DataStore 存储持久数据。NiaPreferences UserPreferencesSerializer
core-database使用 Room 的本地数据库存储。NiADatabase DatabaseMigrations Dao classes
core-model整个应用程序中使用的模型类。Author Episode NewsResource
core-navigation导航依赖项和共享导航类。NiaNavigationDestination

Now in Android 的模块化


Now in Android 项目中的模块化方案是在综合考虑项目的 Roadmap、即将开展的工作和新功能的情况下定义的。Now in Android 项目的目标是提供一个接近生产环境的大型 App 的模块化方案,并且要让方案看起来并没有过度模块化,希望是在两者之间找到一种平衡。


这种方法与 Android 社区进行了讨论,并根据他们的反馈进行了改进。这里并没有一个绝对的正确答案。归根结底,模块化 App 有很多方法和方法,没有唯一的灵丹妙药。这就需要在模块化之前考虑清楚目标、要解决的问题已经对后续工作的影响,这些特定的情况会决定模块化的具体方案。可以绘制出模块依赖关系图,以便帮助更好地分析和规划。


这个项目就是一个示例,并不是一个需要固守不可改变固定结构,相反而是可以根据需求就行变化的。根据 Now in Android 这是我们发现最适合我们项目的一般准则,并提供了一个示例,可以在此基础上进一步修改、扩展和构建。如果您的数据层很小,则可以将其保存在单个模块中。但是一旦存储库和数据源的数量开始增长,可能值得考虑将它们拆分为单独的模块。


最后,官方对其他方式的模块化方案也是持开发态度,有更好的方案及建议也可以反馈出来。


总结


以上内容是根据 Modularization learning journey 翻译整理而得。整体上是提供了一个示例,对一些初学者有一个可以参考学习的工程,对社区中模块化开发起到的积极的作用。说实话,这部分技术在国内并不是什么新技术了。


下面讲一个我个人对这个模块化方案的理解,以下是个人观点,请批判性看待。


首先是好的点提供了通用的 Gradle 配置,简化了各个模块的配置步骤,各种方式预计会在之后的一些项目中流行开来。


不足的点就是没有明确模块化的整体策略,是应采取按照功能还是按照特性分,类似讨论还有我们平时的类文件是按照功能来分还是特性来分,如下是按照特性区分:


# DO,建议方式
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3

按照功能区分的方式大致如下:


# DO NOT,不建议方式
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data

我个人是倾向去按照特性的方式区分,而示例中看上去是偏后者,或者是一个混合体,比如有的模块是添加 feature 前缀的,但是 core-model 模块又是在统一的一个模块中集中管理。个人建议的方式应该是将各个模块中各自使用的模型放到自己的模块中,否则项目在后续进行组件化时将会遇到频繁发版的问题。当然,这种方式在模块化的阶段并没有什么大问题。


模块化之后就是组件化,组件化之后就是壳工程,每个技术阶段对应到团队发展的阶段,有机会的话后面可以展开聊聊。


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

程序员的坏习惯

前言 每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。 不遵循项目规范 每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、...
继续阅读 »

前言


每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。


图片.png


不遵循项目规范


每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、魔鬼数字、提交代码覆盖他人代码等问题经常发生,如果大家能够遵循相关规范,这些问题都可以避免。


用复杂SQL语句来解决问题


程序员在开发功能时,总想着是否能用一条SQL语句来完成这个功能,于是实现的SQL语句写的非常复杂,包含各种子查询嵌套,函数转换等。这样的SQL语句一旦出现了性能问题,很难进行相关优化。


缺少全局把控思维,只关注某一块业务


新增新功能只关注某一小块业务,不考虑系统整体的扩展性,其他模块已经有相关的实现了,却又重复实现,导致重复代码严重。修改功能不考虑对其他模块的影响。


函数复杂冗长,逻辑混乱


一个函数几百行,复杂函数不做拆分,导致代码变得越来月臃肿,最后谁也不敢动。函数还是要遵循设计模式的单一职责,一个函数只做一件事情。如果函数逻辑确实复杂,需要进行拆分,保证逻辑清晰。


缺乏主动思考,拿来主义


实现相关功能,先网上百度一下,拷贝相关的代码,能够运行成功认为万事大吉。到了生产却出现了各种各样的问题,因为网上的demo程序和实际项目的在场景使用上有区别,尤其是相关的参数配置,一定要弄清楚具体的含义,不同场景下,设置参数的值不同。


核心业务逻辑,缺少相关日志和注释


很多核心的业务逻辑实现,整个方法几乎没看到相关注释和日志打印,除了自己能看懂代码逻辑,其他人根本看不懂。一旦生产出了问题,找不到有效的日志输出,问题根本无法定位。


修改代码,缺少必要测试


很多人都会存在侥幸心里,认为只是改了一个变量或者只修改一行代码,不用自测了应该没有问题,殊不知就是因为改一行代码导致了严重的bug。所以修改代码一定要进行自测。


需求没理清,直接写代码


很多程序员在接到需求后,不怎么思考就开始写代码,写着写着发现自己的理解与实际的需求有偏差,造成无意义返工。所以需要多花些时间梳理需求,整理相关思路,能规避很多不合理的问题。


讨论问题,表达没有逻辑、没有重点


讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明。需要学会沟通和表达,才能进行有效的沟通和合作。


不能从错误中吸取教训


作为一位开发人员,你会犯很多错误,这不可避免也没什么大不了的。但如果你总是犯同样的错误,不能从中吸取教训,那态度就出现问题了。


总结


关于这些坏习惯,你是否中招了,大家应该尽早规避这些坏习惯,成为一名优秀的程序员。


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

在字节跳动实习后,程序员是这样写简历的

你在每一段经历中的收获,都会变成简历上的信息。 那么,字节跳动的技术实习生们,都收获了些什么呢? 我们要来了四位技术实习生的简历,上面写着他们在字节跳动实习究竟做了什么、学了什么、有哪些方面的成长。 今天,咱们假装自己是 HR,来看看几位技术实习生们究竟有怎样...
继续阅读 »

你在每一段经历中的收获,都会变成简历上的信息。


那么,字节跳动的技术实习生们,都收获了些什么呢?


我们要来了四位技术实习生的简历,上面写着他们在字节跳动实习究竟做了什么、学了什么、有哪些方面的成长。


今天,咱们假装自己是 HR,来看看几位技术实习生们究竟有怎样的履历吧。






在字节跳动的不同业务中,


技术实习生同学都在充分被信任的情况下,


做着不输正式员工的工作。


在 Leader 指引下进步,


在 mentor 带领下学习,


不断试错,不断创新,


不断创造更有价值的技术。


大胆投递简历,


你也可以和上面四位同学一样,


用真实战,练真本事。


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

【Android】书客编辑器安卓Java版

书客编辑器是一款基于Markdown标记语言的开源的富文本编辑器,它以简易的操作界面和强大的功能深受广大开发者的喜爱。正如官方所说:现在的版本不一定是最好的版本,却是最好的开源版本。官方地址:editor.ibooker.cc。 下面针对书客编辑器安卓Java...
继续阅读 »

书客创作


书客编辑器是一款基于Markdown标记语言的开源的富文本编辑器,它以简易的操作界面和强大的功能深受广大开发者的喜爱。正如官方所说:现在的版本不一定是最好的版本,却是最好的开源版本。官方地址:editor.ibooker.cc


下面针对书客编辑器安卓Java版,进行详解说明。


效果图


在进行讲解之前,首先看一下书客编辑器安卓版的效果图:


书客编辑器安卓版效果图


一、引入资源


引入书客编辑器安卓Java版的方式有很多,这里主要提供两种方式:


1、在build.gradle文件中添加以下代码:


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

dependencies {
compile 'com.github.zrunker:IbookerEditorAndroid:v1.0.1'
}

2、在maven文件中添加以下代码:


<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>

<dependency>
<groupId>com.github.zrunker</groupId>
<artifactId>IbookerEditorAndroid</artifactId>
<version>v1.0.1</version>
</dependency>

二、使用


书客编辑器安卓版简易所在就是只需要简单引入资源之后,可以直接进行使用。因为书客编辑器安卓版不仅仅提供了功能实现,还提供了界面。所以使用过程中,连界面绘制都不用了。


界面分析


书客编辑器安卓版界面大致分为三个部分,即编辑器顶部,内容区(编辑区+预览区)和底部(工具栏)。


书客编辑器安卓-布局轮廓图


首先在布局文件中引入书客编辑器安卓版控件,如布局文件为activity_main.xml,只需要在该文件内添加以下代码即可:


<?xml version="1.0" encoding="utf-8"?>
<cc.ibooker.ibookereditorlib.IbookerEditorView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ibookereditorview"
android:layout_width="match_parent"
android:layout_height="match_parent" />

实际上IbookerEditorView继承LinearLayout,所以它具备LinearLayout的一切功能。


三、功能介绍


根据轮廓图可以看出,书客编辑器安卓版布局只有三个部分,所以关于书客编辑器安卓版功能模块也就分三个部分对外提供使用,即修改哪一个布局模块就是对于哪一个功能模块。


顶部功能模块


书客编辑器安卓版顶部实际上是采用IbookerEditorTopView控件进行呈现,所以要实现顶部相关控件功能首先要获取该控件。


书客编辑器安卓版顶部


书客编辑器安卓版顶部界面图,从左到右分别对应返回(back),撤销(undo),重做(redo),编辑模式(edit),预览模式(preview),帮助(help),关于(about)。知道每个按钮对应的功能,所以就可以去修改或完善相关实现过程。


例如修改返回按钮一些属性,可以使用一下代码:


// 设置书客编辑器顶部布局相关属性
ibookerEditorView.getIbookerEditorTopView()
.setBackImgVisibility(View.VISIBLE)
.setBackImageResource(R.mipmap.ic_launcher);

当然也可以通过IbookerEditorTopView获取相关控件,然后针对该控件进行逐一处理:


ibookerEditorView.getIbookerEditorTopView()
.getBackImg()
.setVisibility(View.VISIBLE);

这里只是使用返回按钮进行举例说,其他按钮使用规则更返回按钮一样。


中间功能模块


书客编辑器安卓版中间区域又分为两个部分,分别是编辑部分和预览部分,所以要修改相关功能就要获取到相关部分的控件。其中编辑部分由IbookerEditorEditView控件进行呈现,预览部分由IbookerEditorPreView控件进行呈现。


例如修改编辑部分相关属性,可以使用如下代码:


// 设置书客编辑器中间布局相关属性
ibookerEditorView.getIbookerEditorVpView().getEditView()
.setIbookerEdHint("书客编辑器")
.setIbookerBackgroundColor(Color.parseColor("#DDDDDD"));

编辑部分并不是只有一个控件,所以也可以获取相关控件,然后针对特定控件进行逐一操作:


ibookerEditorView.getIbookerEditorVpView()
.getEditView()
.getIbookerEd()
.setText("书客编辑器");

// 执行预览功能
ibookerEditorView.getIbookerEditorVpView()
.getPreView()
.ibookerHtmlCompile("预览内容");

底部功能模块


书客编辑器安卓版,底部为工具栏,由IbookerEditorToolView进行呈现。


工具栏一共提供了30多种功能,每一个按钮对应一个功能。各个控件分别为:


boldIBtn, italicIBtn, strikeoutIBtn, underlineIBtn, capitalsIBtn, 
uppercaseIBtn, lowercaseIBtn, h1IBtn, h2IBtn,
h3IBtn, h4IBtn, h5IBtn, h6IBtn, linkIBtn, quoteIBtn,
codeIBtn, imguIBtn, olIBtn, ulIBtn, unselectedIBtn,
selectedIBtn, tableIBtn, htmlIBtn, hrIBtn, emojiIBtn;

所以要修改底部相关属性,首先要获取到IbookerEditorToolView控件,然后对该控件进行操作。


// 设置书客编辑器底部布局相关属性
ibookerEditorView.getIbookerEditorToolView()
.setEmojiIBtnVisibility(View.GONE);

当然底部一共有30多个控件,也可以直接获取到相关控件,然后该控件进行操作,如:


ibookerEditorView.getIbookerEditorToolView().getEmojiIBtn().setVisibility(View.GONE);

补充功能:按钮点击事件监听


这里的按钮点击事件监听主要是针对顶部布局按钮和底部布局按钮。


顶部部分按钮点击事件监听,需要实现IbookerEditorTopView.OnTopClickListener接口,而每个按钮点击通过对应Tag来判断,具体代码如下:


// 顶部按钮点击事件监听
@Override
public void onTopClick(Object tag) {
if (tag.equals(IMG_BACK)) {// 返回
} else if (tag.equals(IBTN_UNDO)) {// 撤销
} else if (tag.equals(IBTN_REDO)) {// 重做
} else if (tag.equals(IBTN_EDIT)) {// 编辑
} else if (tag.equals(IBTN_PREVIEW)) {// 预览
} else if (tag.equals(IBTN_HELP)) {// 帮助
} else if (tag.equals(IBTN_ABOUT)) {// 关于
}
}

其中IMG_BACK、IBTN_UNDO等变量是由IbookerEditorEnum枚举类提供。


底部部分按钮点击事件监听,需要实现IbookerEditorToolView.OnToolClickListener接口,而每个按钮点击通过对应Tag来判断,具体代码如下:


// 工具栏按钮点击事件监听
@Override
public void onToolClick(Object tag) {
if (tag.equals(IBTN_BOLD)) {// 加粗
} else if (tag.equals(IBTN_ITALIC)) {// 斜体
} else if (tag.equals(IBTN_STRIKEOUT)) {// 删除线
} else if (tag.equals(IBTN_UNDERLINE)) {// 下划线
} else if (tag.equals(IBTN_CAPITALS)) {// 单词首字母大写
} else if (tag.equals(IBTN_UPPERCASE)) {// 字母转大写
} else if (tag.equals(IBTN_LOWERCASE)) {// 字母转小写
} else if (tag.equals(IBTN_H1)) {// 一级标题
} else if (tag.equals(IBTN_H2)) {// 二级标题
} else if (tag.equals(IBTN_H3)) {// 三级标题
} else if (tag.equals(IBTN_H4)) {// 四级标题
} else if (tag.equals(IBTN_H5)) {// 五级标题
} else if (tag.equals(IBTN_H6)) {// 六级标题
} else if (tag.equals(IBTN_LINK)) {// 超链接
} else if (tag.equals(IBTN_QUOTE)) {// 引用
} else if (tag.equals(IBTN_CODE)) {// 代码
} else if (tag.equals(IBTN_IMG_U)) {// 图片
} else if (tag.equals(IBTN_OL)) {// 数字列表
} else if (tag.equals(IBTN_UL)) {// 普通列表
} else if (tag.equals(IBTN_UNSELECTED)) {// 复选框未选中
} else if (tag.equals(IBTN_SELECTED)) {// 复选框选中
} else if (tag.equals(IBTN_TABLE)) {// 表格
} else if (tag.equals(IBTN_HTML)) {// HTML
} else if (tag.equals(IBTN_HR)) {// 分割线
}
}

其中IBTN_BOLD、IBTN_ITALIC等变量是由IbookerEditorEnum枚举类提供。


Github地址
阅读原文




微信公众号:书客创作


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

常用到的几个Kotlin开发技巧,减少对业务层代码的入侵

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。 善用@get/@set: JvmName()注解并搭配setter/getter使用 假设当前存在下面三个类代码: #Opt1 public cl...
继续阅读 »

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。



善用@get/@set: JvmName()注解并搭配setter/getter使用


假设当前存在下面三个类代码:


#Opt1


public class Opt1 {

private String mContent;

public String getRealContent() {
return mContent;
}

public void setContent(String mContent) {
this.mContent = mContent;
}
}

#Opt2


public class Opt2 {

public void opt2(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

@Opt3


public class Opt3 {

public void opt3(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

这个时候我想将Opt1类重构成kotlin,我们先看下通过AS的命令Convert Java File to Kotlin File自动转换的结果:


image.png


可以看到为了兼容Opt2Opt3的调用,直接把我的属性名给改成了realContent,kotlin会自动生成getRealContent()setRealContent()方法,这样Opt2Opt3就不用进行任何调整了,kotlin这样就显得太过于智能了。


这样看起来没啥问题,但是java重构kotlin,直接把属性名给我改了,并隐式生成了属性的set和get方法,对于java而言不使用的方法会报灰提示或者只有当前类使用AS会警告可以声明成private,但是对于kotlin生成的set、get方法是隐式的,容易忽略。


所以大家在使用Convert Java File to Kotlin File命令将java重构kotlin的结果一定不能抱有百分之百的信任,即使它很智能,但还是一定要细细的看下转换后的代码逻辑,可能还有不少的优化空间。


这个地方就得需要我们手动进行修改了,比如不想对外暴露修改这个字段的set方法,调整如下:


class Opt1 {
var realContent: String? = null
private set
}

再比如保持原有的字段名mContent,不能被改为realContent,同时又要保证兼容Opt2Opt3类的调用不能报错,且尽量避免去修改里面的代码,我们就可以做如下调整:


class Opt1 {
@get: JvmName("getRealContent")
var mContent: String? = null
private set
}

善用默认参数+@JvmOverloads减少模板代码编写


假设当前Opt1有下面的方法:


public String getSqlCmd(String table) {
return "select * from " + table;
}

且被Opt2Opt3进行了调用,这个时候如果有另一个类Opt3想要调用这个函数并只想从数据库查询指定字段,如果用java实现有两种方式:



  1. 直接在getSqlCmd()方法中添加一个查询字段参数,如果传入的值为null,就查询所有的字段,否则就查询指定字段:


public String getSqlCmd(String table, String name) {
if (TextUtils.isEmpty(name)) {
return "select * from " + table;
}
return "select " + name + " from " + table;
}

这样一来,是不是原本Opt2Opt3getSqlCmd()方法调用是不是需要改动,多传一个参数给方法,而在日常的项目开发中,有可能这个getSqlCmd()被几十个地方调用,难道你一个个的改过去?不太现实且是一种非常糟糕的实现。



  1. 直接在Opt1中新增一个getSqlCmd()的重载方法,传入指定的字段去查询:


public String getSqlCmd(String table,String name) {
return "select " + name + " from " + table;
}

这样做的好处就是不用调整Opt2Opt3getSqlCmd(String table)方法调用逻辑,但是会编写很多模板代码,尤其是getSqlCmd()这个方法体可能七八十行的情况下。


如果Opt1类代码减少即200-400行且不负责的情况下,我们可以将其重构成kotlin,借助于默认参数来实现方法功能增加又不用编写模板代码的效果(如果你的Java类上千行又很复杂,请谨慎转换成kotlin使用下面这种方式)。


@JvmOverloads
fun getSqlCmd(table: String, name: String? = null): String {
return "select ${if (name.isNullOrEmpty()) "*" else name} from $table"
}

添加默认参数name时还要添加@JvmOverloads注解,这样是为了保证java只传一个table参数也能正常调用。


通过上面这种方式,我们就能保证实现了方法功能增加,又不用改动Opt2Opt3对于getSqlCmd()方法的调用逻辑,并且还不用编写额外的模板代码,一举多得。


总结


本篇文章主要介绍了在java重构成kotlin过程中比较常用到的两个技巧,最终实现效果是减少对业务逻辑代码的入侵,希望能对你有所帮助。


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

用力一瞥Android渲染机制-黄油计划

一. 渲染基本概念 对于渲染来说在开始前我们先了解几个概念: CPU主要负责包括 Measure,Layout,Record,Execute 的计算操作。 GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(...
继续阅读 »

一. 渲染基本概念


对于渲染来说在开始前我们先了解几个概念:


CPU主要负责包括 MeasureLayoutRecordExecute 的计算操作。


GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(像素)以用于显示设备输出的过程,简单来说就是将我们要显示的视图,转换成用像素来表示的格式。


帧率代表了GPU在一秒内绘制操作的帧数。


刷新率代表了屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ。


二. Android黄油计划


涉及到滑动流畅,Android在谷歌4.1版本引入了黄油计划。其中有三个重要的核心元素:VSYNC、缓存区和Choreographer:


2.1 VSYNC信号


在Android4.0的时候,CPU可能会因为在忙其他的事情,导致没来得及处理UI绘制。为了解决这个问题,设计成系统在收到VSYN信号后,才会开始下一帧的渲染。也就是收到VSYN通知,CPU和GPU才开始计算然后把数据写入buffer中。


VSYN信号是由屏幕产生的,并且以60fps的固定频率发送给Android系统,在Android系统中的SurfaceFlinger接收发送的Vsync信号。当屏幕从缓存区扫描完一帧到屏幕上之后,开始扫描下一帧之前,发出的一个同步信号,该信号用来切换前缓冲区和后缓冲区。


在引入了Vsyn信号之后,绘制就变成了:


image.png


可以看到渲染的时候从第0帧开始,CPU开始准备第一帧的图形处理,好了才交给GPU进行处理,再上一帧到来之后,CPU就会开始第二帧的处理,基本上跟Vsync的信号保持同步。


有了Vsync机制,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。


2.2 三重缓存


在采用双缓冲机制的时候,也意味着有两个缓存区,分别是让绘制和显示器拥有各自的buffer,GPU使用Back Buffer进行一帧图像数据写入,显示器则是用Frame Buffer,一般来说CPU和GPU处理数据的速度视乎都能在16ms内完成,而且还有时间空余。但是一旦界面比较复杂的情况,CPU/GPU的处理时间超过了16ms,双缓冲开始失效了:


image.png


在第二个时间段内,因为GPU还是处理B帧,数据没有及时交换,导致继续系那是之前A缓存区中的内容。


在B帧完成之后,又因为缺少了Vusnc信号,只能等待一段时间。


直到下一个Vsync信号出现的时候,CPU/GPU才开始马上执行,由于执行时间仍然超过了16ms,导致下一次应该执行的缓存区交换又被推迟了,反复这种情形,就会出越来越多的jank。


为了解决这个问题,Android 4.1才引入了三缓冲机制:在双缓冲机制的基础上增加了一个Graohic Buffer缓冲区,这样就可以最大限度的利用空闲的时间。


image.png


可以看到在第二个时间段里有了区别,在第一次Vsync发生之后,CPU不用再等待了,它会使用第三个bufferC来进行下一帧的准备工作。整个过程就开始的时候卡顿了一下,后面还是很流畅的。但是GPU需要跨越两个Vsync信号才能显示,这样就还是会有一个延迟的现象。


总的来说三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了lag。


2.3 Choreographer


在了解了Vsync机制后,上层又是如何接受这个Vsync信号的?


Google为上层设计了一个Choreographer类,翻译成中文是“编舞者”,是希望通过它来控制上层的绘制(舞蹈)节奏。


可以直接从其构造函数开始看起:


private Choreographer(Looper looper, int vsyncSource) {
//创建Looper对象
mLooper = looper;
//接受处理消息
mHandler = new FrameHandler(looper);
//用来接受垂直同步脉冲,也就是Vsync信号
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
//计算下一帧的时间,Androoid手机屏幕是60Hz的刷新频率
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//初始化CallbackQueue,将在下一帧开始渲染时回调
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}

主要来看下FrameHandlerFrameDisplayEventReceiver的数据结构:


private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//开始渲染下一帧的操作
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
//请求Vsync信号
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
//请求执行Callback
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}

FrameHandler可以看到对三种消息进行了处理,对其具体实现一会分析。


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
}

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {

......
mTimestampNanos = timestampNanos;
mFrame = frame;
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

可以看出来这个类主要是用来接收底层的VSync信号开始处理UI过程。而Vsync信号是由SurfaceFlinger实现并定时发送,接收到之后就会调用onVsync方法,在里面进行处理消息发送到主线程处理,另外在run()方法里面执行了doFrame(),这也是接下来要关注的重点方法。


2.3.1 Choreographer执行过程



ViewRootImpl 中调用 Choreographer 的 postCallback 方法请求 Vsync 并传递一个任务(事件类型是 Choreographer.CALLBACK_TRAVERSAL)



最开始执行的是postCallBack发起回调,这个FrameCallback将会在下一帧渲染时执行。而其内部又调用了postCallbackDelayed方法,在其中又调用了postCallbackDelayedInternal方法:


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
......
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

在这里执行了时间的计算,如果立即就会调用scheduleFrameLocked方法,不然就会延迟发送一个MSG_DO_SCHEDULE_CALLBACK消息,并且在这里使用msg.setAsynchronous(true)讲消息设置成异步。、


而所对应的mHandle也就是之前的FrameHandler,根据消息类型MSG_DO_SCHEDULE_CALLBACK,最终会调用到doScheduleCallback方法:


void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

到了这一步看到还是会调用到scheduleFrameLocked方法。


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//开启了Vsync
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}


if (isRunningOnLooperThreadLocked()) {
//申请Vsync信号
scheduleVsyncLocked();
} else {
//最终还是会调用到scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//如果没有直接使用Vsync的话,则直接通过该消息执行doFrame
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

在这里对是否使用Vsync信号进行处理,如果没有使用则直接通过消息执行doFrame。如果使用的就会先判断是否在当前Looper线程中运行,如果在的话就会请求Vsync信号,否则发送消息到 FrameHandler。直接来看下scheduleVsyncLocked方法:


 private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

可以看到调用了FrameDisplayEventReceiverscheduleVsync方法,通过查找在其父类DisplayEventReceiver中找到了scheduleVsync方法:


public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
//申请VSYNC信号,会回调onVsunc方法
nativeScheduleVsync(mReceiverPtr);
}
}

scheduleVsync()就是使用native方法nativeScheduleVsync()去申请VSYNC信号。等下一次信号接收后会调用dispatchVsync 方法:


private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) {
onVsync(timestampNanos, physicalDisplayId, frame);
}

这个onVsync方法最终实现也就是在FrameDisplayEventReceiver里。可以知道最终还是走到了doFrame方法里。


void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
......
//设置当前frame的Vsync信号到来时间
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//时间差大于一个时钟周期,认为跳frame
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//跳frame数大于默认值,打印警告信息,默认值为30
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
//计算实际开始当前frame与时钟信号的偏差值
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}

//修正偏差值,忽略偏差,为了后续更好地同步工作
frameTimeNanos = startNanos - lastFrameOffset;
}

//若时间回溯,则不进行任何工作,等待下一个时钟信号的到来
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
//请求下一次时钟信号
scheduleVsyncLocked();
return;
}

......

//记录当前frame信息
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
//记录上一次frame开始时间,修正后的
mLastFrameTimeNanos = frameTimeNanos;
}

try {
//执行相关callBack
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

doFrame方法对当前帧的运行时间进行了一系列判断和修正,最终顺序执行了五种事件回调。



  1. CALLBACK_INPUT:输入

  2. CALLBACK_ANIMATION:动画

  3. CALLBACK_INSETS_ANIMATION:插入更新的动画

  4. CALLBACK_TRAVERSAL:遍历,执行measure、layout、draw

  5. CALLBACK_COMMIT:遍历完成的提交操作,用来修正动画启动时间


接着就会执行doCallbacks方法:


void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
......
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
//迭代执行所有队列任务
for (CallbackRecord c = callbacks; c != null; c = c.next) {
.....
//调用CallbackRecord内的run方法
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

主要是去遍历CallbackRecrd,执行所有任务:


private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;

@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}

最终actionrun方法会被执行,这里的action也就是我们在前面调用psetCallback传进来的,也就是 ViewRootImpl 发起的绘制任务mTraversalRunnable了。


然后这里又一次调用了doFrame方法,在啥时候token会是FRAME_CALLBACK_TOKEN呢? 可以发现在我们调用postFrameCallback内部会调用postCallbackDelayedInternal进行赋值:


 public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

ChoreographerpostFrameCallback()通常用来计算丢帧情况。


知道了Choreographer是上层用来接收VSync的角色之后,我们需要进一步了解VSync信号是如何控制上层的绘制的。而绘制UI的起点是View的requestLayout或者是invalidate方法被调用触发,好了时间不早了,这些就放在下一篇Android的屏幕刷新机制里解释吧。(刷新流程和同步屏障)


三. 小结


Android在黄油计划中引入了三个核心元素:VSYNCTriple BufferChoreographer


VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。


三重缓存机制(Triple Buffer) 利用 CPU/GPU 的空闲等待时间提前准备好数据,有效的提升了渲染性能。


又介绍了 Choreographer ,它实现了协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。


参考


Android 显示刷新机制、VSYNC和三重缓存机制


Android图形显示系统(一)


Android屏幕刷新机制


Android Choreographer 源码分析


“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!


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

一次查找分子级Bug的经历,过程太酸爽了

作者:李亚飞 Debugging is like trying to find a needle in a haystack, except the needle is also made of hay. Debug调试就像是在大片的干草堆中找针一样,只不...
继续阅读 »

作者:李亚飞





Debugging is like trying to find a needle in a haystack, except the needle is also made of hay.


Debug调试就像是在大片的干草堆中找针一样,只不过针也是由干草制成的。



在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。


最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我们的这次“旅途”。



01 引子


我是 ShowMeBug 的 CEO 李亚飞,是一个古老的 Ruby 工程师。由于 2019 年招聘工程师的噩梦经历,我立志打造一个真实模拟工作场景的 IDE,用来终结八股文、算法横行的技术招聘时代。


这个云上的 IDE 引擎,我称之为轻协同 IDE 引擎——因为它不是为了繁杂重度的工作场景准备的,而是适应于大部分人的习惯、能快速上手熟悉、加载速度快、能协同(面试用)、低延迟感,让用户感受非常友好


图片


多环境启动与切换


为了达成秒级启动环境的性能要求,我们设计了一套精巧的分布式文件系统架构,其核心是一个可以瞬间复制大量小文件的写时复制 (COW) 技术。IO 吞吐能达到几万人同时在线,性能绝对是它的一大优势。


我们对此信心满满,然而没想到,很快就翻车了。


02 探险启程


2023 年 1 月,北方已经白雪皑皑,而深圳却仍难以感受到冬天的寒意。


我和我的团队在几次打开文件树的某个文件时,会显得有点慢——当时没有人在意,按照常规思路,“网速”背了这个锅。事后我们复盘才发现,这个看似微不足道的小问题,其实正是我们开始这次探险之旅的起点。


1 月底,南方的寒意缓缓侵入。这时候我们的轻协同 IDE 引擎已经开始陆续支持了 Vue2、Vue3、React、Django、Rails 等框架环境,一开始表现都很棒,加载和启动速度都很快。但是,跑了一段时间,我们开始察觉,线上环境就出现个别环境(Rails 环境)启动要 20-30s 才能完成


虽然其他环境仍然保持了极快的加载和启动速度,但敏锐的第六感告诉我,不行,这一定有什么猫腻,如果不立即行动,势必会对用户体验带来很不好的影响。于是,我开始安排团队排查眼前这个不起眼的问题,我们的探险之旅正式开始。


03 初露希望


湿冷的冬季,夜已深,我和我们的团队依旧坐在电脑前苦苦探索,瑟瑟发抖。


探险之旅的第一站,就是老大难的问题:定位Bug。目前只有某一个环境启动很慢,其他的环境都表现不错。大家想了很多办法都没有想明白为什么,甚至怀疑这个环境的模板是不是有问题——但把代码放在本地启动,最多就2秒。


哎,太诡异了。我们在这里卡了至少一周时间,不断追踪代码,分析日志文件,尝试各种方案,都没有弄清楚一个正常的程序启动为什么会慢。我们一度陷入了疲惫和焦虑的情绪中。



Debug 是种信仰,只有坚信自己能找到 Bug,才有可能找到 Bug。



软件开发界一直有一个低级 Bug 定律:所有诡异的问题都来自一个低级原因。在这“山重水复疑无路”之际,我们决定重新审视我们的探险路径:为什么只有 Rails 更慢,其他并不慢?会不会只是一个非常微小的原因而导致?


这时候,恰好有一个架构师朋友来访,向我们建议,可以用 perf 火焰图分析看看 Rails 的启动过程。


图片


perf火焰图实例


当我们用 perf 来分析时,惊讶地发现:原来 Rails 的启动要加载更多的文件! 紧接着,我们又重新用了一个文件读写监控的工具:fatrace,通过它,我们看到 Rails 每次启动需要读写至少 5000 个文件,但其他框架并不需要。


这才让我们突然意识到,会不会是文件系统读写速度不及预期,导致了启动变慢。


04 Bug现身


为了搞清楚是不是文件系统读写速度的问题,我急需一个测试 IO 抖动的脚本。我们初步估算一下,写好这个脚本需要好几个小时的时间。


夜已深,研发同学都陆续下班了。时间紧迫!我想起了火爆全球的 ChatGPT,心想,不如让它写一个试试。


图片


测试 IO 抖动的脚本


Cool,几乎不需要改动就能用,把代码扔在服务器开跑,一测,果然发现问题:每一次文件读写都需要 10-20ms 才能完成 。实际上,一个优秀的磁盘 IO 读写时延应该在亚毫级,但这里至少慢了 50 倍。

Bingo,如同“幽灵宝藏”一般的分子级 Bug 逐渐显现,问题的根因已经确认:过慢的磁盘 IO 读写引发了一系列操作变慢,进而导致启动时间变得非常慢


更庆幸的是,它还让我们发现了偶尔打开文件树变慢的根本原因,这也是整个系统并发能力下降的罪魁祸首


05 迷雾追因


看到这里,大家可能会问,这套分布式文件系统莫非一直这么慢,你们为什么在之前没有发现?


非也,早在项目开始的时候,这里的时延是比较良好的,大家没有特别注意这个 IOPS 性能指标,直到我们后面才留意到,系统运行超过一个月时,IO 读写时延很容易就进入到卡顿的状态,表现就是文件系统所在主机 CPU 忽高忽低,重启就会临时恢复。


此时,探险之旅还没结束。毕竟,这个“幽灵宝藏”周围依旧笼罩着一层迷雾。


我们继续用 fatrace(监控谁在读写哪个 IO)监控线上各个候选人答题目录的 IO读写情况,好家伙,我们发现了一个意外的情况:几乎每一秒都有一次全量的文件 stats 操作 (这是一个检测文件是否有属性变化的 IO 操作)!


也就是说,比如有 1000 个候选人正在各自的 IDE 中编码,每个候选人平均有 300 个文件,就会出现每秒 30 万的 IO 操作数!


我们赶紧去查资料,根据研究数据显示,一个普通的 SSD 盘的 IOPS 最高也就到 2-3 万 。于是,我们重新测试了自己分布式文件系统的 IOPS 能力,结果发现也是 2-3 万 。


那这肯定远远达不到我们理想中的能力级别。


这时,问题更加明确:某种未知的原因导致了大量的 IOPS 的需求,引发了 IO 读写时延变长,慢了大约几十倍


06 接近尾声


我和我的团队继续深究下去,问题已经变得非常明确了:


原来,早在去年 12 月,我们上线一个监听文件增删的变化来通知各端刷新的功能。


最开始我们采用事件监听 (fswatch event),因为跨了主机,所以存在 1-2s 的延迟。研发同学将其改为轮询实现的方案,进而引发了每秒扫描目录的 stats 行为。


当在百人以下访问时,IOPS 没有破万,还足够应对。但一旦访问量上千,便会引发 IO 变慢,进而导致系统出现各种异常:间歇导致某些关键接口 QPS 变低,进而引发系统抖动


随着“幽灵宝藏”显露真身,这次分子级 Bug 的探险之旅也已经接近尾声。团队大
呼:这过程实在太酸爽了!


07 技术无止境


每一个程序员在成长路上,都需要与 Bug 作充足的对抗,要么你勇于探索,深入代码的丛林,快速定位,挖到越来越丰富的“宝藏”,然后尽情汲取到顶级的知识,最终成为高手;或者被它打趴下, 花费大量时间都找不到问题的根源,成为芸芸众生中的一人。


当然,程序员的世界中,不单单是 Debug。


当我毕业 5 年之后,开始意识到技术的真正价值是解决真正的社会问题。前文中我提到,由于我发现技术招聘真是一个极其痛苦的事:特别花面试官的时间,却又无法有效分析出候选人的技术能力,所以创立 ShowMeBug 来解决这个问题:用模拟实战的编程环境,解决科学评估人才的难度


这个轻协同 IDE 技术从零开发,支持协同文件树、完全自定义的文件编辑器、协同的控制台 (Console) 与终端 (Shell),甚至直接支持 Ctrl+P 的文件树搜索,不仅易于使用,又强大有力。


但是这还不够。要知道,追求技术精进是我们技术人的毕生追求。对于这个轻协同IDE,我们追求三个零:零配置、零启动、零延迟。其中,零启动就是本文所追求的极限:以最快的速度启动环境和切换环境


因此,探险之旅结束后,我们进一步改进了此文件系统,设定 raid 的多磁盘冗余,采用高性能 SSD,同时重新制定了新磁盘架构参数,优化相关代码,最终大幅提升了分布式文件系统的稳定性与并发能力。


截止本文结尾,我们启动环境的平均速度为 1.3 秒,切换环境速度进入到亚秒级,仅需要 780ms。目前在全球范围的技术能力评估赛道 (TSA) 中,具备 1-2 年的领先性


08 后记


正当我打算结束本文时,我们内部的产品吐槽群信息闪烁,点开一看:嚯,我们又发现了新 Bug。


立夏已至,我们的探险之旅又即将开始。


作者:ShowMeBug技术团队
来源:juejin.cn/post/7231429790615240764
收起阅读 »

python-实现地铁延误告警

在深圳地铁延误、临停n次之后 终于让我不得不又new了一个py文件😭😭 这次主要记录的是一个延误告警的开发过程 一、实现逻辑 使用库:requests,time,zmail,re 实现逻辑: 1、抓取深圳地铁微博的文章 2、判断是否有延误相关的内容 3、判断时...
继续阅读 »

在深圳地铁延误、临停n次之后


终于让我不得不又new了一个py文件😭😭


这次主要记录的是一个延误告警的开发过程


一、实现逻辑


使用库:requests,time,zmail,re


实现逻辑:


1、抓取深圳地铁微博的文章


2、判断是否有延误相关的内容


3、判断时间是否是今天

4、通知方式:邮件


5、定时执行任务


二、抓取深圳地铁微博(一中1~3)



def goout_report():
url ="https://weibo.com/ajax/statuses/mymblog"
# url ="https://weibo.com/szmcservice/statuses/mymblog"
data = {"uid":2311331195,"page":1,"feature":0}
headers={
"accept":"application/json, text/plain, */*",
"accept-encoding":"gzip, deflate, br",
"accept-language":"zh-CN,zh;q=0.9",
"referer":"https://weibo.com/szmcservice?tabtype=feed",
"cookie":"SUB=_2AkMV8LtUf8NxqwJRmf8XzmLgaY9wywjEieKjrEqPJRMxHRl-yT92ql0ctRB6PnCVuU8iqV308mSwZuO-G9gDVwYDBUdc; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WFpwsXV4nqgkyH.bEVfx-Xw; login_sid_t=c6bbe5dc58bf01c49b0209c29fadc800; cross_origin_proto=SSL; _s_tentry=passport.weibo.com; Apache=4724569630281.133.1655452763512; SINAGLOBAL=4724569630281.133.1655452763512; ULV=1655452763517:1:1:1:4724569630281.133.1655452763512:; wb_view_log=1920*10801; XSRF-TOKEN=1YMvL3PsAm21Y3udZWs5LeX3; WBPSESS=xvhb-0KtQV-0lVspmRtycws5Su8i9HTZ6dAejg6GXKXDqr8m6IkGO6gdtA5nN5IMNb5JZ1up7qJoFXFyoP2RSQSYXHY1uLzykpOFENQ07VthB0G9WHKwRCMWdaof42zB4mOkdTEeX_N9-m1x6Cpm3pmPsC1YhmTwqH8RGwXmYkI=",
"referer":"https://weibo.com/szmcservice",
"x-requested-with": "XMLHttpRequest",
"x-xsrf-token":"1YMvL3PsAm21Y3udZWs5LeX3",
"sec-ch-ua":'Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102',
"sec-ch-ua-platform":"Windows",
"sec-fetch-dest": "empty",
}
text = requests.get(url,headers=headers,params=data,verify=False).json()['data']['list']
today_date = time.ctime()[:10]
for i in range(1,5):
time_post = text[i]['created_at'][:10]
content = str(text[i]).split("'text': '")[1].split(", 'textLength'")[0]
tp=""
if '延误' in content and time_post == today_date:
# mail(content)
text = re.findall(">(.*?)<|>(.*?)\\",content)
for i in text:
for j in i:
if j!="":


                       tp=tp+j

        mail(tp)
break
else:
continue



三、邮件通知,代码如下


def mail(content):
mail = {
'subject': '别墨迹了!地铁又双叒叕延误啦', #邮件标题
'content_text': content, # 邮件内容
}
server = zmail.server('自己的邮箱', '密码',smtp_host="smtp.qq.com",
smtp_port=465) #此处用的qq邮箱、授权码
server.send_mail('收件人邮箱', mail)

ps:需去QQ邮箱网页版-设置-账户-开启smtp服务、获取授权码


四、定时执行任务


1、Jenkins比较合适项目的一个定时执行,


可参考如下:


jenkins环境: jenkins环境部署踩坑记


git环境:Mac-git环境搭建


2、windows-计算机管理比较合适脚本的执行,具体步骤如下,




  • windows键+R输入compmgmt.msc可进入计算机管理界面


    图片




  • 点击上图“创建任务”后如图,


    “常规”界面上输入任务名称、选项二,


    这样锁屏也会自动执行脚本


    图片




  • 点击“触发器”-新建进入新建触发器界面


    这个界面可设置任务执行时间、执行频率、任务重复间隔、延迟时间等等


    图片




  • 点击“操作”-新建跳到如图-新建操作界面


    这个界面可在“程序或脚本”输入框设置脚本运行程序,比如python.exe


    在“添加参数”输入框设置需要运行脚本路径(包含脚本名)


    在“起始于”输入框设置脚本执行路径(一般可为脚本目录)


    图片




  • 其他选项卡也可以看看,


    全部填写完可以点击“创建任务”界面上的“确定”按钮,


    然后在列表中找到新建的任务点击可查看,


    图片




  • 实时执行测试的话可以点击上图“运行”按钮


    或者右击任务-运行即可


    任务执行结果如下:




图片


作者:WAF910
来源:juejin.cn/post/7231074060788613175
收起阅读 »

正则什么的,你让我写,我会难受,你让我用,真香!

web
哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。 但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 ...
继续阅读 »



哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。


image.png


但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 CV 过来直接用~


本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


千分位格式化


在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。



  1. 123456789 => 123,456,789

  2. 123456789.123 => 123,456,789.123


const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}

formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'

想想如果不是用正则,还可以用什么更优雅的方法实现它?


解析链接参数


你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:



// url

const name = getQueryByName('name') // fatfish
const age = getQueryByName('age') // 100

通过正则,简单就能实现 getQueryByName 函数:


const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// Generally, it will be decoded by decodeURIComponent
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}

const name = getQueryByName('name')
const age = getQueryByName('age')

console.log(name, age) // fatfish, 100

驼峰字符串


JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar

正则表达式分分钟教做人:


const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}

console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar

小写转大写


这个需求常见,无需多言,用就完事儿啦:


const capitalize = (string) => {
const capitalizeRegex = /(?:^|\s+)\w/g
return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
}

console.log(capitalize('hello world')) // Hello World
console.log(capitalize('hello WORLD')) // Hello World

实现 trim()


trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


const trim1 = (str) => {
return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
}

const string = ' hello medium '
const noSpaceString = 'hello medium'
const trimString = trim1(string)

console.log(string)
console.log(trimString, trimString === noSpaceString) // hello medium true
console.log(string)

trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;


HTML 转义


防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


正则处理如下:


const escape = (string) => {
const escapeMaps = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
"'": '#39'
}
// The effect here is the same as that of /[&<> "']/g
const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
}

console.log(escape(`

hello world



`
))
/*
<div>
<p>hello world</p>
</div>
*/


HTML 反转义


有了正向的转义,就有反向的逆转义,操作如下:


const unescape = (string) => {
const unescapeMaps = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
'#39': "'"
}
const unescapeRegexp = /&([^;]+);/g
return string.replace(unescapeRegexp, (match, unescapeKey) => {
return unescapeMaps[ unescapeKey ] || match
})
}

console.log(unescape(`
<div>
<p>hello world</p>
</div>
`
))
/*

hello world



*/


校验 24 小时制


处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
console.log(check24TimeRegexp.test('01:14')) // true
console.log(check24TimeRegexp.test('23:59')) // true
console.log(check24TimeRegexp.test('23:60')) // false
console.log(check24TimeRegexp.test('1:14')) // true
console.log(check24TimeRegexp.test('1:1')) // true

校验日期格式


常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

console.log(checkDateRegexp.test('2021-08-22')) // true
console.log(checkDateRegexp.test('2021/08/22')) // true
console.log(checkDateRegexp.test('2021.08.22')) // true
console.log(checkDateRegexp.test('2021.08/22')) // false
console.log(checkDateRegexp.test('2021/08-22')) // false

匹配颜色值


在字符串内匹配出 16 进制的颜色值:


const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

console.log(colorString.match(matchColorRegex))
// [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

判断 HTTPS/HTTP


这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


const checkProtocol = /^https?:/

console.log(checkProtocol.test('https://medium.com/')) // true
console.log(checkProtocol.test('http://medium.com/')) // true
console.log(checkProtocol.test('//medium.com/')) // false

校验版本号


版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


// x.y.z
const versionRegexp = /^(?:\d+\.){2}\d+$/

console.log(versionRegexp.test('1.1.1'))
console.log(versionRegexp.test('1.000.1'))
console.log(versionRegexp.test('1.000.1.1'))

获取网页 img 地址


这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


const matchImgs = (sHtml) => {
const imgUrlRegex = /]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
let matchImgUrls = []

sHtml.replace(imgUrlRegex, (match, $1) => {
$1 && matchImgUrls.push($1)
})
return matchImgUrls
}

console.log(matchImgs(document.body.innerHTML))

格式化电话号码


这个需求也是常见的一匹,用就完事了:


let mobile = '18379836654' 
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654

觉得不错的话,给个赞吧,以后继续补充~~


作者:掘金安东尼
来源:juejin.cn/post/7111857333113716750
收起阅读 »

css实现弧边选项卡

web
实现效果 实现方式 主要使用了 radial-gradient transform perspective rotateX transform-origin 等属性 思路 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变 ...
继续阅读 »

实现效果



image.png



实现方式



主要使用了



等属性



思路




  • 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变


image.png



  • 其实他是这样,如下图所示,我们只需要把黑色部分替换为透明即可,使用两个伪元素即可:


image.png



  • 通过超出隐藏和旋转得到想要的效果


image.png


image.png



  • 综上


在上述 outside-circle 的图形基础上:



  1. 设置一个适当的 perspective 值

  2. 设置一个恰当的旋转圆心 transform-origin

  3. 绕 X 轴进行旋转



  • 动图演示


3.gif



代码



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.g-container {
position: relative;
width: 300px;
height: 100px;
background: red;
border: 1px solid #277f9e;
border-radius: 10px;
overflow: hidden;
}
.g-inner {
position: absolute;
width: 150px;
height: 50px;
background: #fee6e0;
bottom: 0;
border-radius: 0 20px 0 20px;
transform: perspective(40px) scaleX(1.4) scaleY(1.5) rotateX(20deg) translate(-10px, 0);
transform-origin: 50% 100%;
}
.g-inner::before {
content: "";
position: absolute;
right: -10px;
width: 10px;
height: 10px;
top: 40px;
background: radial-gradient(circle at 100% 0, transparent, transparent 9.5px, #fee6e0 10px, #fee6e0);
}
.g-after {
position: absolute;
width: 150px;
height: 50px;
background: #6ecb15;
bottom: 49px;
right: 0;
border-radius: 20px 0 20px 0;
transform: perspective(40px) scaleX(1.4) scaleY(-1.5) rotateX(20deg) translate(14px, 0);
transform-origin: 53% 100%;
}
.g-after::before {
content: "";
position: absolute;
left: -10px;
top: 40px;
width: 10px;
height: 10px;
background: radial-gradient(circle at 0 0, transparent, transparent 9.5px, #6ecb15 10px, #6ecb15);
}
.g-inner-text,.g-after-text {
position: absolute;
width: 150px;
height: 50px;
line-height: 50px;
text-align: center;
}
.g-inner-text {
top: 50%;
left: 0;
}
.g-after-text {
top: 50%;
right: 0;
}
</style>
<body>
<div class="g-container">
<div class="g-inner"></div>
<div class="g-after"></div>
<div class="g-inner-text">选项卡1</div>
<div class="g-after-text">选项卡2</div>
</div>
</body>
</html>

参考文章:github.com/chokcoco/iC…


作者:Agony95z
来源:juejin.cn/post/7223580639710281787
收起阅读 »

极致舒适的Vue页面保活方案

web
为了让页面保活更加稳定,你们是怎么做的? 我用一行配置实现了 Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。 为什么需要页面保活? 页面保活可以提高用户...
继续阅读 »

为了让页面保活更加稳定,你们是怎么做的?


我用一行配置实现了


image.png



Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。



为什么需要页面保活?


页面保活可以提高用户的体验感。例如,当用户从一个带有分页的表格页面(【页面A】)跳转到数据详情页面(【页面B】),并查看了数据之后,当用户从【页面B】返回【页面A】时,如果没有页面保活,【页面A】会重新加载并跳转到第一页,这会让用户感到非常烦恼,因为他们需要重新选择页面和数据。因此,使用页面保活技术,当用户返回【页面A】时,可以恢复之前选择的页码和数据,让用户的体验更加流畅。


如何实现页面保活?


状态存储


这个方案最为直观,原理就是在离开【页面A】之前手动将需要保活的状态存储起来。可以将状态存储到LocalStoreSessionStoreIndexedDB。在【页面A】组件的onMounted钩子中,检测是否存在此前的状态,如果存在从外部存储中将状态恢复回来。


有什么问题?



  • 浪费心智(麻烦/操心)。这个方案存在的问题就是,需要在编写组件的时候就明确的知道跳转到某些页面时进行状态存储。

  • 无法解决子组件状态。在页面组件中还可以做到保存页面组件的状态,但是如何保存子组件呢。不可能所有的子组件状态都在页面组件中维护,因为这样的结构并不是合理。


组件缓存


利用Vue的内置组件<KeepAlive/>缓存包裹在其中的动态切换组件(也就是<Component/>组件)。<KeepAlive/>包裹动态组件时,会缓存不活跃的组件,而不是销毁它们。当一个组件在<KeepAlive/>中被切换时,activateddeactivated生命周期钩子会替换mountedunmounted钩子。最关键的是,<KeepAlive/>不仅适用于被包裹组件的根节点,也适用于其子孙节点。


<KeepAlive/>搭配vue-router即可实现页面的保活,实现代码如下:


<template>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

有什么问题?



  • 页面保活不准确。上面的方式虽然实现了页面保活,但是并不能满足生产要求,例如:【页面A】是应用首页,【页面B】是数据列表页,【页面C】是数据详情页。用户查看数据详情的动线是:【页面A】->【页面B】->【页面C】,在这条动线中【页面B】->【页面C】的时候需要缓存【页面B】,当从【页面C】->【页面B】的时候需要从换从中恢复【页面B】。但是【页面B】->【页面A】的时候又不需要缓存【页面B】,上面的这个方法并不能做到这样的配置。


最佳实践


最理想的保活方式是,不入侵组件代码的情况下,通过简单的配置实现按需的页面保活。


【不入侵组件代码】这条即可排除第一种方式的实现,第二种【组件缓存】的方式只是败在了【按需的页面保活】。那么改造第二种方式,通过在router的路由配置上进行按需保活的配置,再提供一种读取配置结合<KeepAlive/>include属性即可。


路由配置


src/router/index.ts


import useRoutersStore from '@/store/routers';

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/app',
name: 'App',
component: () => import('@/views/app/index.vue'),
},
{
path: '/data-list',
name: 'DataList',
component: () => import('@/views/data-list/index.vue'),
meta: {
// 离开【/data-list】前往【/data-detail】时缓存【/data-list】
leaveCaches: ['/data-detail'],
}
},
{
path: '/data-detail',
name: 'DataDetail',
component: () => import('@/views/data-detail/index.vue'),
}
]
}
];

router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
const { cacheRouter } = useRoutersStore();
cacheRouter(from, to);
next();
});

保活组件存储


src/stroe/router.ts


import { RouteLocationNormalized } from 'vue-router';

const useRouterStore = defineStore('router', {
state: () => ({
cacheComps: new Set<string>(),
}),
actions: {
cacheRouter(from: RouteLocationNormalized, to: RouteLocationNormalized) {
if(
Array.isArray(from.meta.leaveCaches) &&
from.meta.leaveCaches.inclued(to.path) &&
typeof from.name === 'string'
) {
this.cacheComps.add(form.name);
}
if(
Array.isArray(to.meta.leaveCaches) &&
!to.meta.leaveCaches.inclued(from.path) &&
typeof to.name === 'string'
) {
this.cacheComps.delete(to.name);
}
},
},
getters: {
keepAliveComps(state: State) {
return [...state.cacheComps];
},
},
});

页面缓存


src/layout/index.vue


<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="keepAliveComps">
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

<script lang='ts' setup>
import { storeToRefs } from 'pinia';
import useRouterStore from '@/store/router';

const { keepAliveComps } = storeToRefs(useRouterStore());
</script>

TypeScript提升配置体验


import 'vue-router';

export type LeaveCaches = string[];

declare module 'vue-router' {
interface RouteMeta {
leaveCaches?: LeaveCaches;
}
}

该方案的问题



  • 缺少通配符处理/*/**/index

  • 无法缓存/preview/:address这样的动态路由。

  • 组件名和路由名称必须保持一致。


总结


通过<RouterView v-slot="{ Component }">获取到当前路由对应的组件,在将该组件通过<component :is="Component" />渲染,渲染之前利用<KeepAlive :include="keepAliveComps">来过滤当前组件是否需要保活。
基于上述机制,通过简单的路由配置中的meta.leaveCaches = [...]来配置从当前路由出发到哪些路由时,需要缓存当前路由的内容。


如果大家有其他保活方案,欢迎留言交流哦!


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

vue 递归组件 作用域插槽

web
开头 这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。 代码 父组件 <template> <div> <Tree :data="data"> <templa...
继续阅读 »

开头


这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。


代码


父组件


<template>
<div>
<Tree :data="data">
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

理解步骤,始终知道 -> 递归就是把最里面的放到最外面来,你就当 A插槽最后会被 B 插槽替代

所以,父组件的 default 插槽用的是 B 插槽,因此 B 插槽就暴露出一个 title 给父组件使用。


删掉 A 的title :


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


由于可能只有一层,所以走不到 B 插槽,因此 A 插槽也需要暴露一个 title 给外面使用。


el-tree 的原理


父组件


<template>
<div>
<Tree :data="data">
<!-- C -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义22" }}
</div>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node,

},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if(!this.$parent.$scopedSlots.default) {
this.tree = this
}else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


这里可以看到,父组件的 C 和 子组件中的 B 都是使用到了 A 这个插槽。


这里我们只要能把 B 替换成父组件的 C 就完成了递归插槽。


子组件的代码转变


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<template #default="{ title }">
<node :title="title">
</node>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node: {
props: {
title: String,
},
render(h) {
const parent = this.$parent;
const tree = parent.tree
const title = this.title
return (tree.$scopedSlots.default({ title }))
}
}
},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if (!this.$parent.$scopedSlots.default) {
this.tree = this
} else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

这里搞了一个 node 的函数组件,node 函数组件拿到 子组件的 tree, tree也是一层层的保存着 $scopedSlots.default 其实就是 C 的那些编译节点。 然后把 title 传给了 C。


el-tree 源码贴图


image.png


tree


image.png


tree-node


image.png


image.png


image.png


image.png


总结


写的有点乱啊,这个只是辅助你理解 递归插槽,其实一开始都是懵逼了,多看下代码理解还是能看的懂的。


作者:晓欲望
来源:juejin.cn/post/7222931700438138937
收起阅读 »

不用刷新!用户无感升级,解决前端部署最后的问题

web
前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。 一、背景 网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。 二...
继续阅读 »

前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。


一、背景


网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。


二、问题分析


2.1 问题现象


网络控制台显示加载页面的资源显示404。


image.png


2.2 满足条件


发生这个现象,需要满足三个条件:



  1. 站点是SPA页面,并开启懒加载;

  2. 资源地址启用内容hash。(加载更快启用了强缓存,为了应对资源变更能及时更新内容,会对资源地址的文件名加上内容hash)。

  3. 覆盖式部署,新版本发布后旧的版本会被删除。


特别在容器部署的情况的SPA页面,很容易满足上诉三个条件。笔者在做公司的内部系统就踩过坑。


2.3 原因分析


浏览器打开页面后,会记录路由和资源路径的映射,服务器发版后,没有及时通知浏览器更新路由映射表。导致用户在发布前端打开的页面,在版本更新后,进入下一个路由加载上一个版本的资源失败,导致需要用户刷新才能正常使用。


image.png


三、解决方案


3.1 方案一:失败重试


3.1.1 思路整理:


既然加载失败了,就重试加载发版后的资源版本就行。增加一个manifest.json文件能够获取新版本对应的资源路径。


image.png


3.1.2 举例说明


以vue项目进行举例子说明:


第一步: 修改构建工具配置以生成manifest文件


使用vite构建的项目,可以在vite.config.ts增加配置build.manifest为true,用以生成manifest.json文件


export default defineConfig({
// 更多配置
build: {
//开启manifest
manifest: true,
cssCodeSplit: false //关闭单独生成css文件,方便demo演示
}
})

如果使用webpack构建的项目,可以使用webpack-manifest-plugin插件进行配置。


进行项目生产构建,生成manifest.json,内容如下:


 // 简单说明:文件内容是项目原始代码目录结构和构建生成的资源路径一一对应
{
"index.html": { // 页面入口
"dynamicImports": ["src/pages/page1.vue", "src/pages/page2.vue"],
"file": "assets/index-e170761c.js",
"isEntry": true,
"src": "index.html"
},
// page1对应单文件组件
"src/pages/page1.vue": {
"file": "assets/page1-515906ab1.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page1.vue"
},
// page2对应单文件组件
"src/pages/page2.vue": {
"file": "assets/page2-9785c68c.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page2.vue"
},
"style.css": {
"file": "assets/style-809e5baa.css",
"src": "style.css"
}
}

第二步,修改route文件,加上重试逻辑


在路由文件中,增加加载页面js失败的重试逻辑,通过新版的manifest.json来获取新版的页面js,再次加载。


import { createRouter, createWebHistory } from 'vue-router'


const router = createRouter({
history: createWebHistory('/'),
routes: [
{
path: '/page1',
// component: () => import(`../pages/page1.vue`), // 变更前
component: () => retryImport('page1'), // 变更后
},
{
path: '/page2',
// component: () => import(`../pages/page1.vue`),
component: () => retryImport('page2'),
},
]
})


async function retryImport(page) {
try {
// 加载页面资源
switch (page) {
case 'page1':
// 这里demo演示,没有使用dynamic-import-vars
return await import(`../pages/page1.vue`)
default:
return await import(`../pages/page2.vue`)
}
} catch (err: any) {
// 判断是否是资源加载错误,错误重试
if (err.toString().indexOf('Failed to fetch dynamically imported module') > -1) {
// 获取manifest资源清单
return fetch('/manifest.json').then(async (res) => {
const json = await res.json()
// 找到对应的最新版本的js
const errPage = `src/pages/${page}.vue`
// 加载新的js
return await import(`/${json[errPage].file}`)
})
}
throw err
}
}
export default router

3.1.3 总结


这个方案改造只涉及前端层,成本最低,但是无法做到多版本共存,只能适配部分发版变更,如果涉及删除页面的版本,最好增加一个容错页面。


3.2 方案二:增量部署


3.2.1 思路整理


生产环境发布改成增量发布,不再是覆盖式的发布,发版后旧版本依旧保留。


image.png


3.2.2 示例实践


需要改造构建配置,增加版本的概念,保证新旧版本不路径冲突


vite 构建工具示例:


// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const version = require('./package.json').version

// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
assetsDir: `./${versionName}`, // 版本号
}
})

webpack构建工具示例:


// webpack.config.js
const path = require('path');
// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
module.exports = {
//...
output: {
path: path.resolve(__dirname, `dist/${versionName}/assets`),
},
};

3.2.3 总结


需要CI/CD发版改造,由之前的容器部署改成静态部署(即文件上传对象存储的思路一样),这种增量发部署适配全部的场景,而且,支持多版本共存,能做到版本灰度放量。


四、总结


本文通过覆盖式部署在前端版本发布,导致用户不可使用的严重体验问题,分析问题发生根因,并给出两种解决思路。笔者结合公司的云设施,最后使用增量部署,并BFF层配合使用,多版本共存,线上启用通过配置指定启用哪个版本,再也不用赶着时间点去发版。


作者:azuo
来源:juejin.cn/post/7223196531143131194
收起阅读 »

VUE中常用的4种高级方法

web
1. provide/inject provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在...
继续阅读 »

1. provide/inject


provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在子孙组件中使用 inject 来注入这个数据。


使用 provide/inject 的好处是可以让我们在父组件和子孙组件之间传递数据,而无需手动进行繁琐的 props 传递。它可以让代码更加简洁和易于维护。但需要注意的是,provide/inject 的数据是非响应式的,这是因为provide/inject是一种更加底层的 API,它是基于依赖注入的方式来传递数据,而不是通过响应式系统来实现数据的更新和同步。


具体来说,provide方法提供的数据会被注入到子组件中的inject属性中,但是这些数据不会自动触发子组件的重新渲染,如果provide提供的数据发生了变化,子组件不会自动感知到这些变化并更新。


如果需要在子组件中使用provide/inject提供的数据,并且希望这些数据能够响应式地更新,可以考虑使用Vue的响应式数据来代替provide/inject。例如,可以将数据定义在父组件中,并通过props将其传递给子组件,子组件再通过$emit来向父组件发送数据更新的事件,从而实现响应式的数据更新。


下面是一个简单的例子,展示了如何在父组件中提供数据,并在子孙组件中注入这个数据:


<!-- 父组件 -->
<template>
<div>
<ChildComponent />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
provide: {
message: 'Hello from ParentComponent',
},
components: {
ChildComponent,
},
};
</script>

//上面provide还可以写成函数形式
export default {
provide(){
return {
message: this.message
}
}
}


<!-- 子组件 -->
<template>
<div>
<GrandchildComponent />
</div>
</template>

<script>
import GrandchildComponent from './GrandchildComponent.vue';

export default {
inject: ['message'],
components: {
GrandchildComponent,
},
};
</script>


<!-- 孙子组件 -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
inject: ['message'],
};
</script>


在上面的例子中,父组件中提供了一个名为 message 的数据,子孙组件中都可以使用 inject 来注入这个数据,并在模板中使用它。注意,子孙组件中的 inject 选项中使用了一个数组,数组中包含了需要注入的属性名。在这个例子中,我们只注入了一个 message 属性,所以数组中只有一个元素。


2. 自定义v-model


要使自定义的Vue组件支持v-model,需要实现一个名为value的prop和一个名为input的事件。在组件内部,将value prop 绑定到组件的内部状态,然后在对内部状态进行修改时触发input事件。


下面是一个简单的例子,展示如何创建一个自定义的输入框组件并支持v-model:


<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
name: 'MyInput',
props: {
value: String
}
};
</script>


在上面的组件中,我们定义了一个value prop,这是与v-model绑定的数据。我们还将内置的input事件转发为一个自定义的input事件,并在事件处理程序中更新内部状态。现在,我们可以在父组件中使用v-model来绑定这个自定义组件的值,就像使用普通的输入框一样:


<template>
<div>
<my-input v-model="message" />
<p>{{ message }}</p>
</div>
</template>

<script>
import MyInput from './MyInput.vue';

export default {
components: {
MyInput
},
data() {
return {
message: ''
};
}
};
</script>


在上面的代码中,我们通过使用v-model指令来双向绑定message数据和MyInput组件的值。当用户在输入框中输入文本时,MyInput组件会触发input事件,并将其更新的值发送给父组件,从而实现了双向绑定的效果。


3. 事件总线(EventBus)


Vue事件总线是一个事件处理机制,它可以让组件之间进行通信,以便在应用程序中共享信息。在Vue.js应用程序中,事件总线通常是一个全局实例,可以用来发送和接收事件。


以下是使用Vue事件总线的步骤:


3.1 创建一个全局Vue实例作为事件总线:


import Vue from 'vue';
export const eventBus = new Vue();

3.2 在需要发送事件的组件中,使用$emit方法触发事件并传递数据:


eventBus.$emit('eventName', data);

3.3 在需要接收事件的组件中,使用$on方法监听事件并处理数据:


eventBus.$on('eventName', (data) => {
// 处理数据
});

需要注意的是,事件总线是全局的,所以在不同的组件中,需要保证事件名称的唯一性。


另外,需要在组件销毁前使用$off方法取消事件监听:


eventBus.$off('eventName');

这样就可以在Vue.js应用程序中使用事件总线来实现组件之间的通信了。


4. render方法


Vue 的 render 方法是用来渲染组件的函数,它可以用来替代模板语法,通过代码的方式来生成 DOM 结构。相较于模板语法,render 方法具有更好的类型检查和代码提示。


下面详细介绍 Vue 的 render 方法的使用方法:


4.1 基本语法


render 方法的基本语法如下:


render: function (createElement) {
// 返回一个 VNode
}

其中 createElement 是一个函数,它用来创建 VNode(虚拟节点),并返回一个 VNode 对象。


4.2 创建 VNode


要创建 VNode,可以调用 createElement 函数,该函数接受三个参数:



  • 标签名或组件名

  • 可选的属性对象

  • 子节点数组


例如,下面的代码创建了一个包含文本节点的 div 元素:


render: function (createElement) {
return createElement('div', 'Hello, world!')
}

如果要创建一个带有子节点的元素,可以将子节点作为第三个参数传递给 createElement 函数。例如,下面的代码创建了一个包含两个子元素的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('h1', 'Hello'),
createElement('p', 'World')
])
}

如果要给元素添加属性,可以将属性对象作为第二个参数传递给 createElement 函数。例如,下面的代码创建了一个带有样式和事件处理程序的 button 元素:


render: function (createElement) {
return createElement('button', {
style: { backgroundColor: 'red' },
on: {
click: this.handleClick
}
}, 'Click me')
},
methods: {
handleClick: function () {
console.log('Button clicked')
}
}

4.3 动态数据


render 方法可以根据组件的状态动态生成内容。要在 render 方法中使用组件的数据,可以使用 this 关键字来访问组件实例的属性。例如,下面的代码根据组件的状态动态生成了一个带有计数器的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('p', 'Count: ' + this.count),
createElement('button', {
on: {
click: this.increment
}
}, 'Increment')
])
},
data: function () {
return {
count: 0
}
},
methods: {
increment: function () {
this.count++
}
}


4.4 JSX


在使用 Vue 的 render 方法时,也可以使用 JSX(JavaScript XML)语法,这样可以更方便地编写模板。要使用 JSX,需要在组件中导入 VuecreateElement 函数,并在 render 方法中使用 JSX 语法。例如,下面的代码使用了 JSX 语法来创建一个计数器组件:


import Vue from 'vue'

export default {
render() {
return (
<div>
<p>Count:{this.count}</p>
<button onClick={this.increment}>Increment</button>
</div>

)
},
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}


注意,在使用 JSX 时,需要使用 {} 包裹 JavaScript 表达式。


4.5 生成函数式组件


除了生成普通的组件,render 方法还可以生成函数式组件。函数式组件没有状态,只接收 props 作为输入,并返回一个 VNode。因为函数式组件没有状态,所以它们的性能比普通组件更高。


要生成函数式组件,可以在组件定义中将 functional 属性设置为 true。例如,下面的代码定义了一个函数式组件,用于显示列表项:


export default {
functional: true,
props: ['item'],
render: function (createElement, context) {
return createElement('li', context.props.item);
}
}

注意,在函数式组件中,props 作为第二个参数传递给 render<

作者:阿虎儿
来源:juejin.cn/post/7225921305597820985
/code> 方法。

收起阅读 »

代码重构和架构重构:你需要了解的区别

1 代码重构 定义 对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。 目的 增加可读性、增加可维护性、可扩展性 3 关键点 不影响输出 不修正错误 不增加新的功能性 代码重构时,发现有个功能实现逻辑不合理,可直接修改吗? 当然不可! 2 架构...
继续阅读 »

1 代码重构


定义


对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。


目的


增加可读性、增加可维护性、可扩展性


3 关键点



  • 不影响输出

  • 不修正错误

  • 不增加新的功能性


代码重构时,发现有个功能实现逻辑不合理,可直接修改吗?


当然不可!


2 架构重构


定义


通过整系统结构(4R)来修复系统质量问题而不影响整体系统能力。


目的


修复质量问题(性能、可用性、可扩展......)


关键点



  • 修复质量(架构,而非代码层面的质量)问题,提升架构质量

  • 不影响整体系统功能

  • 架构本质没有发生变化


把某个子系统的实现方式从硬编码改为规则引擎,是代码重构还是架构重构?


属于架构重构,架构设计方案了,实现系统可扩展性。


3 代码重构 V.S 架构重构



4 架构重构技巧


4.0 手段



架构重构是否可以修改 4R 中的 Rank?


不能!修改 rank 就不是重构,而是演进了。拆微服务不属于改 rank。外部系统协作方式都得修改了。比如将淘宝的支付方式支付宝拆出来,成为支付宝公司了。


4.1 先局部优化后架构重构


局部优化


定义:对部分业务或者功能进行优化,不影响系统架构。


常见手段:



  • 数据库添加索引,优化索引

  • 某个数据缓存更新策略采用后台更新

  • 增加负载均衡服务数量

  • 优化代码里面并发的逻辑

  • 修改Innodb buffer pool 配置,分配更多内存

  • 服务间的某个接口增加1个参数


架构重构


定义:优化系统架构,整体提升质量,架构重构会影响架构的4R定义。


常见手段:



  • 引入消息队列(增加 Role )

  • 去掉 ZooKeeper,改为内置 Raft 算法实现(删除 Role)

  • 将 Memcached 改为 Redis( 改变 Role)

  • 按照稳定性拆分微服务( 拆分 Role )

  • 将粒度太细的微服务合并(合并 Role)

  • 将服务间的通信方式由 HTTP 改为 gRPC(修改 Relation )

  • SDK从读本地配置文件改为从管理系统读取配置(修改Rule )


4.2 有的放矢



案例




  • 开发效率很慢,P业务和M系统互相影响

  • 线上问题很多,尤其是数据类问题

  • M系统性能很低


有的放矢:



重构只解决第1个问题(开发效率很慢,P业务和M系统互相影响)。其他问题咋办,架构师你不解决了吗?架构重构后了,各个业务部门再解决各自的问题,如 P业务后台优化自己的问题,M 系统优化自己的性能问题,因为这些问题本身靠重构是解决不了的,而是要靠重构拆分之后,各自再继续优化。


4.3 合纵连横


合纵


说服业务方和老板




  1. 以数据说话


    把“可扩展性”转换为“版本开发速度很慢然后给出对应的项目数据(平时注意搜集数据)。




  2. 以案例说话(其实更有效,给人的冲击力更明显) 若没有数据,就举极端案例,如某个小功能,开发测试只要5天,但是等了1个月才上线。




连横


说服其它团队。



  1. 换位思考 思考对其它团队的好处,才能让人配合。

  2. 合作双赢 汇报和总结的时候,把其它团队也带上。


案例


合纵:告诉PM和项目经理极端案例,设计2周、开发2天、一个月才上线。


连横:P业务线上问题大大减少,P业务不会被其它业务影响


4.4 运筹帷幄


① 问题分类


将问题分类,一段时间集中处理类问题。 避免对照 Excel表格,一条条解决。


② 问题排序


分类后排序,按照优先级顺序来落地。


避免见缝插针式的安排重构任务,不要搭业务的顺风车重构:



  • 避免背锅

  • 效果不明显

  • 无法安排工作量大的重构


③ 逐一攻破


每类问题里面先易后难。


把容易的问题解决掉,增强信心。


④ 案例


Before:



  • 1个100多行的Excel问题表格,一个一个的解决

  • 专挑软柿子捏

  • 见缝插针


After:



  1. 分类:性能、组件、架构、代码

  2. 分阶段: 优化-> 架构重构 -> 架构演进

  3. 专项落地: 明确时间、目标、版本



5 架构重构FAQ


架构重构是否可以引入新技术?


可以,但尽量少,架构重构要求快准


业务不给时间重构怎么办 ?


会哭的孩了有奶吃。收集数据和案例,事实说话。


其它团队不配合怎么办 ?


学会利用上级力量。上级都不支持,说明你做的这个没意义,所以领导也不在乎。那就别做了。


业务进度很紧,人力不够怎么办 ?


收集需要重构的证据,技术汇报的时候有理有据



6 测试


6.1 判断



  1. 代码重构、架构重构、架构演进都不需要去修复问题 ×

  2. 微服务拆分既可以是架构重构的手段,也可以是架构演进的手段 √

  3. 架构重构应该搭业务版本的便车,可以避免对业务版本有影响 ×

  4. 架构重构是为修复问题,因此应该将系统遗留的问题都在架构重构的时候修复 ×

  5. 架构重构应该分门别类,按照优先级逐步落地 √


6.2 思考


架构重构的时候是否可以顺手将代码重构也做了 ? 因为反正都安排版本了。No!


局部优化不属于代码/架构重构。


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

Redis 使用zset做消息队列总结

1.zset为什么可以做消息队列 zset做消息队列的特性有: 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。 元素唯一性:zset的每个元素都是独一无二的...
继续阅读 »

1.zset为什么可以做消息队列


zset做消息队列的特性有:



  1. 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。

  2. 元素唯一性:zset的每个元素都是独一无二的,这对于实现某些消息需求(比如幂等性)是非常有帮助的。

  3. 成员和分数之间的映射关系:有序集合中的每个成员都有一个分数,这样就可以将相同的数据划分到不同的 queue 中,以及为每个 queue 设置不同的延时。

  4. 高效的添加删除操作:因为zset会自动维护元素之间的顺序,所以在添加或删除元素时无需进行手动排序,从而能提升操作速度。


Redis的zset天然支持按照时间顺序的消息队列,可以利用其成员唯一性的特性来保证消息不被重复消费,在实现高吞吐率等方面也有很大的优势。


2.zset实现消息队列的步骤


Redis的zset有序集合是可以用来实现消息队列的,一般是按照时间戳作为score的值,将消息内容作为value存入有序集合中。


以下是实现步骤:



  1. 客户端将消息推送到Redis的有序集合中。

  2. 有序集合中,每个成员都有一个分数(score)。在这里,我们可以设成消息的时间戳,也就是当时的时间。

  3. 当需要从消息队列中获取消息时,客户端获取有序集合前N个元素并进行操作。一般来说,N取一个适当的数值,比如10。


需要注意的是,Redis的zset是有序集合,它的元素是有序的,并且不能有重复元素。因此,如果需要处理有重复消息的情况,需要在消息体中加入某些唯一性标识来保证不会重复。


3.使用jedis实现消息队列示例


Java可以通过Redis的Java客户端包Jedis来使用Redis,Jedis提供了丰富的API来操作Redis,下面是一段实现用Redis的zset类型实现的消息队列的代码。


import redis.clients.jedis.Jedis;
import java.util.Set;

public class RedisMessageQueue {
  private Jedis jedis; //Redis连接对象
  private String queueName; //队列名字

  /**
    * 构造函数
    * @param host Redis主机地址
    * @param port Redis端口
    * @param password Redis密码
    * @param queueName 队列名字
    */
  public RedisMessageQueue(String host, int port, String password, String queueName){
      jedis = new Jedis(host, port);
      jedis.auth(password);
      this.queueName = queueName;
  }

  /**
    * 发送消息
    * @param message 消息内容
    */
  public void sendMessage(String message){
      //获取当前时间戳
      long timestamp = System.currentTimeMillis();
      //将消息添加到有序集合中
      jedis.zadd(queueName, timestamp, message);
  }

  /**
    * 接收消息
    * @param count 一次接收的消息数量
    * @return 返回接收到的消息
    */
  public String[] receiveMessage(int count){
      //设置最大轮询时间
      long timeout = 5000;
      //获取当前时间戳
      long start = System.currentTimeMillis();

      while (true) {
          //获取可用的消息数量
          long size = jedis.zcount(queueName, "-inf", "+inf");
          if (size == 0) {
              //如果无消息,休眠50ms后继续轮询
              try {
                  Thread.sleep(50);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } else {
              //计算需要获取的消息数量count与当前可用的消息数量size的最小值
              count = (int) Math.min(count, size);
              //获取消息
              Set<String> messages = jedis.zrange(queueName, 0, count - 1);
              String[] results = messages.toArray(new String[0]);
              //移除已处理的消息
              jedis.zremrangeByRank(queueName, 0, count - 1);
              return results;
          }

          //检查是否超时
          if (System.currentTimeMillis() - start > timeout) {
              return null; //超时返回空
          }
      }
  }

  /**
    * 销毁队列
    */
  public void destroy(){
      jedis.del(queueName);
      jedis.close();
  }
}


使用示例:


public static void main(String[] args) {
  //创建消息队列
  RedisMessageQueue messageQueue = new RedisMessageQueue("localhost", 6379, "password", "my_queue");

  //生产者发送消息
  messageQueue.sendMessage("message1");
  messageQueue.sendMessage("message2");

  //消费者接收消息
  String[] messages = messageQueue.receiveMessage(10);
  System.out.println(Arrays.toString(messages)); //输出:[message1, message2]

  //销毁队列
  messageQueue.destroy();
}


在实际应用中,可以结合线程池或者消息监听器等方式,将消息接收过程放置于独立的线程中,以提高消息队列的处理效率。


4.+inf与-inf


+inf 是 Redis 中用于表示正无穷大的一种特殊值,也就是无限大。在使用 Redis 的 zset 集合时,+inf 通常用作 ZREVRANGEBYSCORE 命令的上限值,表示查找 zset 集合中最大的分数值。+inf 后面的 -inf 表示 zset 中最小的分数值。这两个值一起可以用来获取 zset 集合中的所有元素或一个特定范围内的元素。例如:


# 获取 zset 集合中所有元素
ZREVRANGE queue +inf -inf WITHSCORES

# 获取 zset 集合中第1到第10个元素(分数从大到小排列)
ZREVRANGE queue +inf -inf WITHSCORES LIMIT 0 9

# 获取 zset 集合中分数在 1581095012 到当前时间之间的元素
ZREVRANGEBYSCORE queue +inf 1581095012 WITHSCORES

在这些命令中,+inf 代表了一个最大的分数值,-inf 代表了一个最小的分数值,用于确定查询的分数值范围。


5.redis使用list与zset做消息队列有什么区别


Redis 使用 List 和 ZSET 都可以实现消息队列,但是二者有以下不同之处:



  1. 数据结构不同:List 是一个有序的字符串列表,ZSET 则是一个有序集合,它们的底层实现机制不同。

  2. 存储方式不同:List 只能存储字符串类型的数据,而 ZSET 则可以存储带有权重的元素,即除了元素值外,还可以为每个元素指定一个分数。

  3. 功能不同: List 操作在元素添加、删除等方面比较方便,而 ZSET 在处理数据排序和范围查找等方面比 List 更加高效。

  4. 应用场景不同: 对于需要精细控制排序和分值的场景可以选用 ZSET,而对于只需要简单的队列操作,例如先进先出,可以直接采用 List。


综上所述,List 和 ZSET 都可以用于消息队列的实现,但如果需要更好的性能和更高级的排序功能,建议使用 ZSET。而如果只需要简单的队列操作,则 List 更加适合。


6.redis用zset做消息队列会出现大key的情况吗


在Redis中,使用zset作为消息队列,每个消息都是一个元素,元素中有一个分数代表了该消息的时间戳。如果系统中有大量消息需要入队或者大量的不同的队列,这个key的体积会越来越大,从而可能会出现大key的情况。


当Redis存储的某个键值对的大小超过实例的最大内存限制时,会触发Redis的内存回收机制,可以根据LRU算法等策略来选择需要回收的数据,并确保最热数据保持在内存中。如果内存不足,可以使用Redis的持久化机制,将数据写入磁盘。使用Redis集群,并且将数据分片到多个节点上,也是一种可以有效解决大key问题的方法。


针对大key的问题,可以考虑对消息进行切分,将一个队列切分成多个小队列,或者对消息队列集合进行分片,将消息分布到不同的Redis实例上,从而降低单个Redis实例的内存使用,并提高系统的可扩展性。


7.redis 用zset做消息队列如何处理消息积压



  1. 改变消费者的消费能力:


可以增加消费者的数量,或者优化消费者的消费能力,使其能够更快地处理消息。同时,可以根据消息队列中消息的数量,动态地调整消费者的数量、消费速率和优先级等参数。



  1. 对过期消息进行过滤:


将过期的消息移出消息队列,以减少队列的长度,从而使消费者能够及时地消费未过期的消息。可以使用Redis提供的zremrangebyscore()方法,对过期消息进行清理。



  1. 对消息进行分片:


将消息分片,分布到不同的消息队列中,使得不同的消费者可以并行地处理消息,以提高消息处理的效率。



  1. 对消息进行持久化:


使用Redis的持久化机制,将消息写入磁盘,以防止消息的丢失。同时,也可以使用多个Redis节点进行备份,以提高Redis系统的可靠性。


总的来说,在实际应用中,需要根据实际情况,综合考虑上述方法,选择适合自己的方案,以保证Redis的消息队列在处理消息积压时,能够保持高效和稳定。


8. redis使用zset做消息队列时,有多个消费者同时消费消息怎么处理


当使用 Redis 的 zset 作为消息队列时,可以通过以下方式来处理多个消费者同时消费消息:



  1. 利用Redis事务特性:zset中的元素的score会反映该元素的优先级,多个消费者可以使用Redis事务特性,采用原子性的操作将空闲的消息数据上锁,只有在被加锁的消费者消费完当前消息时,往消息队列中发送释放锁的指令,其它消费者才能够获得该消息并进行消费。

  2. 利用Redis分布式锁:使用 Redis 实现分布式锁来实现只有一个消费者消费一条消息,可以使用redis的SETNX命令(如果键已存在,则该命令不做任何事,如果密钥不存在,它将设置并返回1可以用作锁),将创建一个新的键来表示这一消息是否已经被锁定。

  3. 防止重复消费:为了防止多个消费者消费同一条消息,可以在消息队列中添加一个消息完成的标记,在消费者处理完一条消息之后,会将该消息的完成状态通知给消息队列,标记该消息已经被消费过,其它消费者再次尝试消费该消息时,发现已经被标记为完成,则不再消费该消息。


无论采用哪种方式,都需要保证消息队列的可靠性和高效性,否则会导致消息丢失或重复消费等问题。


9.redis使用zset做消息队列如何实现一个分组的功能


Redis 中的 Zset 可以用于实现一个有序集合,其中每个元素都会关联一个分数。在消息队列中,可以使用 Zset 来存储消息的优先级(即分数),并使用消息 ID 作为 Zset 中的成员,这样可以通过 Zset 的有序性来获取下一条要处理的消息。


为了实现一个分组的功能,可以使用 Redis 的命名空间来创建多个 Zset 集合。每个分组都有一个对应的 Zset 集合,消息都被添加到对应的集合中。然后,你可以从任何一个集合中获取下一条消息,这样就可以实现分组的功能。


例如,假设你的 Redis 实例有三个 Zset 集合,分别是 group1、group2 和 group3,你可以按照如下方式将消息添加到不同的分组中:


ZADD group1 1 message1
ZADD group2 2 message2
ZADD group3 3 message3

然后,你可以通过以下方式获取下一条要处理的消息:


ZRANGE group1 0 0 WITHSCORES
ZRANGE group2 0 0 WITHSCORES
ZRANGE group3 0 0 WITHSCORES

将返回结果中的第一个元素作为下一条要处理的消息。由于每个分组都是一个独立的 Zset 集合,因此它们之间是相互独立的,不会干扰彼此。


10. redis使用zset做消息队列有哪些注意事项


Redis 使用 ZSET 做消息队列时,需要注意以下几点:



  1. 消息的唯一性:使用 ZSET 作为消息队列存储的时候需要注意消息的唯一性,避免重复消息的情况出现。可以考虑使用消息 ID 或者时间戳来作为消息的唯一标识。

  2. 消息的顺序:使用 ZSET 作为消息队列存储可以保证消息的有序性,但消息的顺序可能不是按照消息 ID 或者时间戳的顺序。可以考虑在消息中增加时间戳等信息,然后在消费时根据这些信息对消息进行排序。

  3. 已消费的消息删除:在使用 ZSET 作为消息队列的时候需要注意如何删除已经消费的消息,可以使用 ZREMRANGEBYLEX 或者 ZREMRANGEBYSCORE 命令删除已经消费的消息。

  4. 消息堆积问题:ZSET 作为一种有序存储结构,有可能出现消息堆积的情况,如果消息队列里面的消息堆积过多,会影响消息队列的处理速度,甚至可能导致 Redis 宕机等问题。这个问题可以使用 Redis 定时器来解决,定期将过期的消息从队列中删除。

  5. 客户端的能力:在消费消息的时候需要考虑客户端的能力,可以考虑增加多个客户端同时消费消息,以提高消息队列的处理能力。

  6. Redis 节点的负载均衡:使用 ZSET 作为消息队列的存储结构,需要注意 Redis 节点的负载均衡,因为节点的并发连接数可能会受到限制。必要的时候可以增加 Redis 节点数量,或者采用 Redis 集群解决这个问题。


总之,使用 ZSET 作为消息队列存储需要特别注意消息的唯一性、消息的顺序、已消费消息删除、消息堆积问题、客户端的能力和节点的负载均衡等问题。


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

我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不?

问题 你好,我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不? 认真考虑一下,想到了以下几点,如有不对的地方欢迎批评指正。 第一,提升学历 从现在起为升本做好准备,做好规划,然后每天去认认真真的执行,如果可能,尽量考研。因为现在互联网行业...
继续阅读 »

问题


你好,我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不?


认真考虑一下,想到了以下几点,如有不对的地方欢迎批评指正。


第一,提升学历


从现在起为升本做好准备,做好规划,然后每天去认认真真的执行,如果可能,尽量考研。因为现在互联网行业确实很卷,等你大专毕业的时候应该会更卷。现在的招聘越来越看重学历,学历不行你连面试机会都没有。你可能听说过有大专也进大厂的,那是前几年,也是心存着偏差,不能去赌这个。现在的事实是专科面试的机会很少很少。作为一个普通人,我感觉去接受更高等的教育,未来才会有更多的选择,才能更好的去掌握自己的命运。


第二,学好基础知识


在学习上,一定要想法设法学好数据结构、数据库、操作系统、计算机网络、计算机组成原理、英语、数学等等。尽量每天都抽出点儿时间来学点。你可能会说,听别人说这些东西好像在工作中也不常用呀,对,一般的工作确实不常用,但正是这些决定了你未来能走多远。这些东西是你从事这个行业的根基,你这个根基越稳固,你的未来发展就会越好。出了新东西你才能更快的掌握。假设互联网真不行了,你有这些底子在,你可以很快的去切到其他行业一些软件儿上的开发,如果说你没有这个底子,想迅速切换想都别想。


第三,锻炼合作能力


在学校多去参加一些计算机类的比赛,比如说像一些算法相关的,锻炼自己与他人合作的能力。


第四,参与写作、开源


业余时间去写写博客儿,参加一些开源项目,这些对于你毕业后找工作都是有帮助的。不得不承认,现在的大学生也挺卷的,在学校就各种源码,算法各种卷。你需要制造点不一样,想想你毕业之后有一个不错的博客儿,有一个成百上千小星星的开源项目儿,肯定是加分。


第五,学习人情世故


学习之外适当的去兼顾一些人情往来之类的东西,注意是适当。比如宿舍的一些聚餐呀,学校举办的一些比赛,建议多参加唱歌、演讲、辩论类的比赛,锻炼自己的表达、表现力、还有心里素质。不要太在意别人的看法,没那么多人在意你,这个道理越早知道越好。你迟早是要步入社会的,不是说你技术多好,你就能混的多好,人情世故这个东西,很重要,早锻炼比晚锻炼强,真的,赶早不赶晚。


第六,锻炼身体


永远记住身体是革命的本钱,没有一个好身体啥都没用,要抽时间锻炼身体哈。


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

裁员、结婚、买房、赡养父母.....即将30岁,焦虑扑面而来

前言: 大家好,我是春风。 不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。 就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也...
继续阅读 »

前言:


大家好,我是春风。


不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。


就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也突然就不再属于你自己,我们会不自觉的扮演起家庭的依靠,而且还是唯一的依靠。这种压力完全是在自己还没准备的时候就突袭了你。


就像我这一周都是在这种压力和焦虑中度过...


192b2a6322f5c0559f59d567effb7bfb.jpeg


失眠


我不知道自己为什么会突然一下就想这么多,但年龄就像一个雷管,突然就炸开了赤裸裸的现实,或者是按下了一个倒计时。我不自觉的去想家庭,去想父母,去想我30岁40岁50|岁是什么样子。


这周的每天晚上我想着这些都失眠到三四点,当然如果这个时候你还像我一样去看下确切时间,你很大可能会失眠到五点。


尝试心理学


所以这几天上班也是一行代码都没敲,幸好需求不多。最后我迫切的觉得我应该找个办法解决一下,索性今天摸鱼一天,听了一天的心理学讲座的音频。


果然,心病还需心药医!!!


下面我给大家分享一下自己的治疗过程,希望也能对焦虑的你有所启发。
 


一、我为什么焦虑


解决焦虑的第一步就是先要弄清楚我们为什么焦虑?我们究竟在焦虑什么?可能很多人都是焦虑经济,焦虑结婚,焦虑生活中的各种琐事。


但我们也可以试着站在上帝视角,更深层次的解剖一下自己。


1. 焦虑多年努力没有换来想要的生活


比如我,我最大的焦虑也是钱,我从农村出来,没有任何背景,毕业到现在已经工作六年,20年在广州买房上车,但好巧不巧买的是恒大的房子,买完就暴雷,现在每个月有房贷,还要结婚。


所以我总是在想,这些年我算努力吗,为什么还是没有挣到钱。


三十而立近在眼前,可我这些年究竟立了什么呢?遥想刚毕业那会给自己定下的目标,虽然是天方夜谭,但对比现在,也太天方夜谭了吧。


不是说好的天道酬勤吗?不是说努力就会有收获吗?


所以我焦虑,我表面是焦虑钱,但何尝不是在焦虑自己这么多年的努力却没有得到我想要的结果呢?


2. 攀比带来的自我嫌弃


我们都知道攀比是不好的,尤其是在这个动辄年薪百万年薪的互联网世界,但也是这些网络信息的无孔不入,让我们不得不攀比,不得不怀疑自己是为什么会差这么多。


我承认自己是一个争强好胜的人,我会在读书时非常想要好的名次,因为我体验过那种胜利感一次之后,便会上瘾。所以现在工作,我也时常不自觉的攀比起来,因此,我也深深陷入了自我怀疑和自我嫌弃的枣泥。


为什么我努力学习,辛苦工作,一年下来却不如人家卖一个月炒饭,为什么那个做销售的同学两三个月就赚到了我两年的财富,为什么我工作六年攒下的钱,却还不及人家父母一个月的收租?


和我一样没背景的比我赚的多,有背景的赚的更多。这种怀疑病入膏肓的时候,我都会病态的想,那些富二代肯定都是花花公子,懒惰而不自知,毕竟电影里不都这样演吗?但现实是,别人会比你接受更好的家庭教育,环境教育。别人谈吐自如还努力学习。不仅赢在了起跑线,还比你努力。就是这种对比,越来越让我们自己嫌弃自己,厌恶自己。所以也就总是想要求自己必须去做更好的自己。


二、生命的意义


应该所有人都思考过这个问题吧,来这人间一趟,可都不想白来一趟。我们都想在这个世界留下点什么,就像战国时士人对君主,知道会被烹杀却勇于进言,只为留下一个劝谏名士的美名。人活一世,究竟为了什么呢?生前获利?死后留名?


但对于我们大多数的普通人呢?


待我们死去,我们的名字最多就被孙子辈的人知道,等到他们也故去,那这个世界还会有你来过的痕迹吗?


人生代代无穷已,江月年年望相似。


所以夜深人静的时候,我们总会在想,自己生命的意义?似乎一切都没有意义,我们注定就是会拥有一个低价值甚至无价值的人生


三、结婚的压力


我们九零后,比零零后环境是不是更好不确定,但对比八零后,肯定要差,八零后结婚,印象里还不太谈房子,车子,但我们结婚,确是一个必考题。


所以我们结婚率低,不仅有不婚族,还有现在的丁克族。


我自己来自农村,我们那里男女比例就严重失衡,村里的男孩子结婚的不超过一半。但是我爸着急,不知道你们是否有过这种催婚的经历,父母会反复的告诉你大龄剩男剩女有多丢人,你们的不婚不仅是你自己的问题,还会让家里人都抬不起头。是的,父母含辛茹苦养育了你们,现在因为你,让他们在别人面前抬不起头来,失去了自尊。


四、知道该做什么,但拖延没做后就会更加的自我嫌弃


我们擅长给自己定下很多目标,但有时候就是逃不过人性,孔子说,食色性也。我们在被创造的时候就是被设计为不断的追求多巴胺的动物。所以我们沉迷游戏,沉迷追剧。总是在周五的晚上选择放松自己。而不会因为定下了目标就去学习。


总之,我们的目标定的越美好,我们的行动往往越低效。最后,两者的差距越来越远。我们离自己期望中的那个自己判若两人。


我们又会厌恶自己,嫌弃自己。甚至痛骂自己的不自律。




以上是我分析的自己的焦虑点。相信很多也是屏幕前的你曾经或者当下也有的吧。接下来,就看看我是怎么在心理学上找到解决的办法的吧!


给自己的建议


关于攀比、努力没有想要的结果、不自律等等带来的自我嫌弃。我们或许应该这样看


1、承认自己的普通


有远大报负,有远大理想。追求自由和生命的绚丽是我们每个人都会有也应该有的念想。但当暂时还没有结果的时候。我们不应该及早否定自己。而是勇于承认自己的普通。我们都想成为这个世界上独一无二的人。事实上从某种意义上来说。我们也是独一无二的人。但从金钱,名望这些大家公认的层面来看。99.99%的人都是普通人。我们这一生很大可能就会这样平凡的过完一生。接受自己的普通,活在当下。这才是体验这趟生命之旅最应该有的态度。只要今天比昨天好。我们不就是在进步吗?


为什么一定要有个结果??


人生最大的悲哀就是跨越知道和做到的鸿沟,当一个人承认自己是个普通人的时候,他才是人格完整,真正成熟的时候


我们追求美好的事物,追求自己喜欢的东西,金钱也好,名望也罢,这都是无可厚非的。因为人就是需要被不断满足的,人因为有欲望才会想活下去。但是当暂时没有结果的时候。我们也不应该为此感到自责和焦虑。一旦我们队美好事物的追求变成了一种压力。我们就会陷入一种有负担的内缩状态,反而会跑不快


我们都害怕浪费生命,因为生命只有一次。我们想让自己的生命在这个世界留下来过的痕迹。所以我们追寻那些热爱的东西,但其实追求的过程才是最应该留下的痕迹,结果反而只是别人眼里的痕迹。


当然也有一种理解认为活在当下就是躺平。恰好现在网络上也是躺平之语频频入耳。我想说关于是努力追求理想还是躺平的一点观点。


在禅宗里有这样一句话说的非常好:身无所住而生其心


这里的住 代表的就是追求的一种执念。


身无所住而生其心,说的就是要避免有执和无执的两种极端状态。有执就是我们我都要要要。我要钱 我要名 我要豪车豪宅。无执就是觉得什么都没有意义。生命终会归于尘土。所以努力追求的再多,又有什么用呢?大多数人生命注定是无意义的。这也是很多人躺平的一部分原因吧!


但是就该这样躺平的度过一生吗?每天都陷入低价值的人生?


身无所住而生其心。我们的生命不应该陷入有执和无执这两种极端。花开了,虽然它终会化作春泥。但花开的此刻,它是真美啊!
 


2、关于结婚生子


 
关于结婚生子,为什么我要在所有人都结婚的年龄就结婚,为什么三十岁生孩子就是没出息。生育这个问题,其实是为了什么 我爸老说,你不生小孩或者很晚生小孩,到时候老了都没人照顾你,那养儿真的就是为了防老吗?其实这是一个伪命题,先还不说到时候,儿女孝不孝顺的问题,就说我爸,这么多年,他为了倾其所有,花我身上的钱不说几千万也有上百万了,如果真是要防老,那这个钱存银行,每年光吃利息就有几十万,几十万在一个农村来说晚年怎么都富足了,两三个人照顾你都够,而我到现在每年有给过我爸几十万吗?


再说养儿为了到时候不孤独,能享受天伦之乐,这算是感情上的需求吧。那既然这样,我在准备好的节奏里欣然的生育,不比我在年龄和周遭看法的压力下强行生育更加的好吗,当我想体验一下为人父的生命体验了,我顺其自然的要小孩儿,快快乐乐的养育他,而不是我已经三十岁了,别人小孩儿都打酱油了,大家都在说是不是我有问题,所以即使我现在经济,心理,精力上都没准备好,我也必须要一个小孩儿。


所以大人们说的并不是真正的理由,而人类或者动物,之所以热衷繁衍,最原始的动力是想把自己的基因流下去,是想在这个世界上留下一点记忆。


为别人而活。尤其是在农村,很多人一辈子就认识村里那些人,祖祖辈辈就只见过那些活法,在他们眼里,多少岁结婚,多少岁生孩子,这辈子就这么过去了。但是但凡有一点出格,那在其他人眼里就会抬不起头,因为,其他人出现意外的时候,自己也是这样看其他人的。所以大家都只为活在别人眼里而活,打个比方,我现在很想很想吃一个红薯,明明我吃完这个红薯,内心就会得到满足,但是我不会,因为别人会觉得我是不是穷,都只能吃红薯,这不单单是大家说的死要面子活受罪,其实是我们很多人骨子里的自卑,尤其是我们农村,经济条件都不好,没有什么值得炫耀的,所以我们就尽可能找大家能达成共识的去炫耀。很简单的一个例子。假如一个亿万富翁去到农村,他的身价已经足够自信了,即使他不结婚生子,其他人会看不起他吗?


结尾:


1、心理学是治愈,也是哲学上的思考。这种思考很多都能跳脱出现实而给到我们解决现实中问题的办法


2、再重复一遍:身无所住而生其心!


3、要爱具体的人,不要爱抽象的人,要爱生活,不要爱生活的意义。


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

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可


@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样


fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:


fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑


val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。


@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。


@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:


single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


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

Unit 为啥还能当函数参数?面向实用的 Kotlin Unit 详解

视频先行 这是一篇视频形式的分享,如果你方便看,可以直接去看视频: 哔哩哔哩:这里 抖音:这里 YouTube:这里 下面是视频内容的脚本文案原稿分享。 视频文案原稿 很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有...
继续阅读 »

视频先行


这是一篇视频形式的分享,如果你方便看,可以直接去看视频:




下面是视频内容的脚本文案原稿分享。



视频文案原稿


很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有沿用 Java 的 void 关键字,而要引入这个叫 Unit 的新东西?


// Java
public void sayHello() {
System.out.println("Hello!");
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

不过这个问题一般也不会维持很久,因为就算你不明白,好像……也不影响写代码。


直到这两年,大家发现 Compose 的官方示例代码里竟然有把 Unit 填到函数参数里的情况:


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

我们才觉得:「啊?还能这么写?」


Unit 的本质


大家好,我是扔物线朱凯。


今天来讲一讲 Unit 这个特殊的类型。


我们在刚学 Kotlin 的时候,就知道 Java 的 void 关键字在 Kotlin 里没有了,取而代之的是一个叫做 Unit 的东西:


// Java
public void sayHello() {
System.out.println("Hello!")
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

而这个 Unit,和 Java 的 void 其实是不一样的。比如 Unit 的返回值类型,我们是可以省略掉不写的:


// Kotlin
fun sayHello() {
println("Hello!")
}

不过省略只是语法上的便利,实际上 Kotlin 还是会把它理解成 Unit


Unit 和 Java 的 void 真正的区别在于,void 是真的表示什么都不返回,而 Kotlin 的 Unit 却是一个真实存在的类型:


public object Unit {
override fun toString() = "kotlin.Unit"
}

它是一个 object,也就是 Kotlin 里的单例类型或者说单例对象。当一个函数的返回值类型是 Unit 的时候,它是需要返回一个 Unit 类型的对象的:


// Kotlin
fun sayHello() {
println("Hello!")
return Unit
}

只不过因为它是个 object ,所以唯一能返回的值就是 Unit 本身。


另外,这一行 return 我们也可以省略不写:


// Kotlin
fun sayHello() {
println("Hello!")
}

因为就像返回值类型一样,这一行 return,Kotlin 也会帮我们自动加上:


// Kotlin
fun sayHello(): Unit {
println("Hello!")
return Unit
}

这两个 Unit 是不一样的,上面的是 Unit 这个类型,下面的是 Unit 这个单例对象,它俩长得一样但是是不同的东西。注意了,这个并不是 Kotlin 给 Unit 的特权,而是 object 本来就有的语法特性。你如果有需要,也可以用同样的格式来使用别的单例对象,是不会报错的:


object Rengwuxian

fun getRengwuxian(): Rengwuxian {
return Rengwuxian
}

包括你也可以这样写:


val unit: Unit = Unit

也是一样的道理,等号左边是类型,等号右边是对象——当然这么写没什么实际作用啊,单例你就直接用就行了。


所以在结构上,Unit 并没有任何的特别之处,它就只是一个 Kotlin 的 object 而已。除了对于函数返回值类型和返回值的自动补充之外,Kotlin 对它没有再施加任何的魔法了。它的特殊之处,更多的是在于语义和用途的角度:它是个由官方规定出来的、用于「什么也不返回」的场景的返回值类型。但这只是它被规定的用法而已,而本质上它真就是个实实在在的类型。也就是在 Kotlin 里,并不存在真正没有返回值的函数,所有「没有返回值」的函数实质上的返回值类型都是 Unit,而返回值也都是 Unit 这个单例对象,这是 Unit 和 Java 的 void 在本质上的不同。


Unit 的价值所在


那么接下来的问题就是:这么做的意义在哪?


意义就在于,Unit 去掉了无返回值的函数的特殊性,消除了有返回值和无返回值的函数的本质区别,这样很多事做起来就会更简单了。


例:有返回值的函数在重写时没有返回值


比如?


比如在 Java 里面,由于 void 并不是一种真正的类型,所以任何有返回值的方法在子类里的重写方法也都必须有返回值,而不能写成 void,不管你用不用泛型都是一样的:


public abstract class Maker {
public abstract Object make();
}

public class AppleMaker extends Maker {
// 合法
@Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker {
// 非法
@Override
public void make() {
world.refresh();
}
}


public abstract class Maker<T> {
public abstract T make();
}

public class AppleMaker extends Maker<Apple> {
// 合法
Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker<void> {
// 非法
Override
public void make() {
world.refresh();
}
}


你只能去写一行 return null 来手动实现接近于「什么都不返回」的效果:


public class NewWorldMaker extends Maker {
@Override
public Object make() {
world.refresh();
return null;
}
}


而且如果你用的是泛型,可能还需要用一个专门的虚假类型来让效果达到完美:


public class NewWorldMaker extends Maker<Void> {
@Override
public Void make() {
world.refresh();
return null;
}
}


而在 Kotlin 里,Unit 是一种真实存在的类型,所以直接写就行了:


abstract class Maker {
abstract fun make(): Any
}

class AppleMaker : Maker() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker() {
override fun make() {
world.refresh()
}
}

abstract class Maker<T> {
abstract fun make(): T
}

class AppleMaker : Maker<Apple>() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker<Unit>() {
override fun make() {
world.refresh()
}
}

这就是 Unit 的去特殊性——或者说通用性——所给我们带来的便利。


例:函数类型的函数参数


同样的,这种去特殊性对于 Kotlin 的函数式编程也提供了方便。一个函数的函数类型的参数,在函数调用的时候填入的实参,只要符合声明里面的返回值类型,它是可以有返回值,也可以没有返回值的:


fun runTask(task: () -> Any) {
when (val result = task()) {
Unit -> println("result is Unit")
String -> println("result is a String: $result")
else -> println("result is an unknown type")
}
}

...

runTask { } // () -> Unit
runTask { "完成!" } // () -> String
runTask { 1 } // () -> Int

Java 不支持把方法当做对象来传递,所以我们没法跟 Java 做对比;但如果 Kotlin 不是像现在这样用了 Unit,而是照抄了 Java 的 void 关键字,我们就肯定没办法这样写。


小结:去特殊化


这就是我刚才所说的,对于无返回值的函数的「去特殊化」,是 Unit 最核心的价值。它相当于是对 Java 的 void 进行了缺陷的修复,让本来有的问题现在没有了。而对于实际开发,它的作用是属于润物细无声的,你不需要懂我说的这一大堆东西,也不影响你享受 Unit 的这些好处。


…………


那我出这期视频干嘛?


——开个玩笑。了解各种魔法背后的实质,对于我们掌握和正确地使用一门语言是很有必要的。


延伸:当做纯粹的单例对象来使用


比如,知道 Unit 是什么之后,你就能理解为什么它能作为函数的参数去被使用。


Compose 里的协程函数 LaunchedEffect() 要求我们填入至少一个 key 参数,来让协程在界面状态变化时可以自动重启:


LaunchedEffect(key) {
xxxx
xxxxxx
xxx
}

而如果我们没有自动重启的需求,就可以在参数里填上一个 Unit


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

因为 Unit 是不变的,所以把它填进参数里,这个协程就不会自动重启了。这招用着非常方便,Compose 的官方示例里也有这样的代码。不过这个和 Unit 自身的定位已经无关了,而仅仅是在使用它「单例」的性质。实际上,你在括号里把它换成任何的常量,效果都是完全一样的,比如 true、比如 false、比如 1、比如 0、比如 你好,都是可以的。所以如果你什么时候想「随便拿个对象过来」,或者「随便拿个单例对象过来」,也可以使用 Unit,它和你自己创建一个 object 然后去使用,效果是一样的。


总结


好,这就是 Kotlin 的 Unit,希望这个视频可以帮助你更好地了解和使用它。下期我会讲 Kotlin 里另一个特殊的类型:Nothing。关注我,了解更多 Android 开发的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!


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