注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android 流畅性三板斧之帧率监控

前言 Android 流畅性监控的三板斧,这里所指是【帧率的监控】,【卡顿监控】和【ANR的监控】。之所以讲这三者放在一起是他们的联系比较密切。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会产生ANR。 严谨的讲,帧率下降不一定会有卡顿(这里对卡顿是从技术角...
继续阅读 »

前言


Android 流畅性监控的三板斧,这里所指是【帧率的监控】,【卡顿监控】和【ANR的监控】。之所以讲这三者放在一起是他们的联系比较密切。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会产生ANR。


严谨的讲,帧率下降不一定会有卡顿(这里对卡顿是从技术角度定义在主线程执行了耗时任务),卡顿产生的原因还有其他因素导致,比如系统负载、CPU繁忙等。关于卡顿的详细内容放在流畅性三板斧的第二篇。


【过分的卡顿】也不一定产生ANR,卡顿但未触发ANR产生的条件就不会产生ANR。关于ANR的详细内容我们放在三板斧系列文章的第三篇。


该篇我们从应用开发者的角度,探索在应用层监控帧率的四种方式。


温馨提示,本文涉及的实现的代码以上传至github github.com/drummor/Get…,结合代码食用更佳


1 什么是帧率



帧率(Frame rate)是以帧称为单位的位图图像连续出现在显示器上的频率(速率)。



2 Android 中帧率的监控


线下开发我们可以使用开发者选项的帧率监控或者 adb shell dumpsys gfxinfo packagename进行监控针对性优化。这些方案不能带到线上。


惯常我们在Android里线下对帧率的监控主要依托Choreographer,关于Choreographer不再赘述在其他的文章有比较全面的介绍可以看这两篇文章



3 简单监控帧率方案


利用Choreographer的postcallback方法接口轮询方式,能够对帧率进行统计。


image.png


choreographer.postCallback()内部是挂载了一个CALLBACK_ANIMATION类型的callback。轮训方式往choreographer内添加callback,相邻两个callback执行时间间隔即能粗略统计单帧的耗时。严谨的讲这不是单帧的耗时而是两个【半帧】拼凑的耗时。


代码示例如下。


class PoorFrameTracker {
   private var mLastFrameTime = -1L
   private var mFrameCount: Int = 0
   val calRate = 200 //ms
   fun startTrack() {
       mLastFrameTime = 0L
       mFrameCount = 0
       Choreographer.getInstance().postFrameCallback(object : FrameCallback {
           override fun doFrame(frameTimeNanos: Long) {
               if (mLastFrameTime == -1L) {
                   mLastFrameTime = frameTimeNanos
              }
               val diff = (frameTimeNanos - mLastFrameTime) / 1_000_000.0f
               if (diff > calRate) {
                   var fps = mFrameCount / diff * 1000
                   if (fps > 60) {fps = 60.0f}
                   //todo :统计
                   mFrameCount = 0
                   mLastFrameTime = -1
              } else {
mFrameCount++
              }
               Choreographer.getInstance().postFrameCallback(this);
          }
      })
  }
}

优点



  • 简单快捷,无黑科技


缺点



  • 无活动时,也会监控,无效信息会把帧率较低时给平均掉。

  • 对应用带来不必要的负担。


4 帧率监控进化之一 hook Choreographer


针对章节三的方案,首先我们有两个主要的优化方向希望在主线程不活动的时候不进行帧率的检测


我们调用公开api Choreographer.postCallback()时会触发垂直同步(这部分可以参考另一篇文章)。


 # choreographer
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
       private long mTimestampNanos;
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
                VsyncEventData vsyncEventData)
{
          ...
                mTimestampNanos = timestampNanos;
                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, mLastVsyncEventData);
        }
    }


  • 【采集每帧的开始】利用Looper中Printer采集Message的开始和结束。上段代码是Choreographer中的一段代码。当收到底层垂直同步信号的时,利用Handler机制post的一个Runable,执行该帧的动作doFrame()。依次我们可以采集到每帧的开始和结束。


# Choreographer
private final CallbackQueue[] mCallbackQueues;

image.png



  • 【过滤出每帧的执行动作】我们知道主线程中不单单执行每帧的动作,还会执行其他动作。如何过滤出执行的是每帧的动作。反射往Choreographer往里添加callback不触发垂直同步,同时在同步信号回调时,会调用我们传入的callback,如果执行了传入的callbacl就可以标识该次执行动作是帧的执行动作。

  • 【采集真实的垂直同步到达时间】反射拿到mTimestampNanos

  • 结合以上,我们能够采集到每帧执行耗时,依次可以计算出准确的帧率。且比我们第一种方案要优雅很多。


  void doFrame(long frameTimeNanos, int frame, DisplayEventReceiver.VsyncEventData vsyncEventData) {
      ...
       final long frameIntervalNanos = vsyncEventData.frameInterval;
       doCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_COMMIT, frameData, frameIntervalNanos);
      ...
  }


  • 同时我们还可以通过反射的方式给Chorographer 里 mCallbackQueues添加不同的类型动作,采集不同类型动作的耗时。


补充


image.png



  • 严格意义上,该方案统计的也不是真实的帧率,而是一帧所有耗时中在UI Thread执行部分的耗时,上图doFrame部分。其他线程和进程还会执行其他动作最终才能完成一帧的绘制。但对于我们应用层来说更关注监控doFrame,我们在应用开发层面大部分能够干预的也在doFrame这部分。


(方案思路Matrix)


关于这个方案可查看: github.com/drummor/Get…


5 帧率监控进化之二 滑动帧率


#View
   protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  ...
       final AttachInfo ai = mAttachInfo;
       if (ai != null) {
           ai.mViewScrollChanged = true;
      }
...
  }


  • View里如果有滑动行为产生最终都会调用到onScrollChanged(),当该方法调用的时候,会将mAttachInfo的mViewScrollChanged值设为true


#ViewRootImpl
   private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
...
       if (mAttachInfo.mViewScrollChanged) {
           mAttachInfo.mViewScrollChanged = false;
           mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
      }
  }



  • 如上代码ViewRootImpl的draw方法会如果check到mAttachInfo.mViewScrollChanged值为true就会就会调用ViewTreeObserverdispatchOnScrollChanged()方法,只要我们在viewTreeObserver设置监听,就能获取到界面是否正在滑动这一重要事件。


image.png




  • 整个过程的如上图所示,我们收到滑动回调这一事件的时候,其实是choreographer的doFrame()调用而来。




  • 结合上面我们就可以在收到【滑动事件】的时候使用Choreographer的postCallback开始统计帧率。




  • 什么时候结束呢?在没有【滑动信息】生成出来的时候看下面代码


       private var isScroll = false
       init {
           window.decorView.viewTreeObserver.addOnScrollChangedListener {
            //标识正在滑动
               isScroll = true
    //开始统计帧率        
               Choreographer.getInstance().postFrameCallback(FrameCallback())
          }
      }

      private inner class FrameCallback : Choreographer.FrameCallback {
           override fun doFrame(frameTimeNanos: Long) {
               if (isScroll) {
                   isScroll = false //重置滑动状态
                   if (lastFrameTime != 0L) {
                       val dropFrame =
                          (((frameTimeNanos - lastFrameTime) / 1000000f / 16.6667f) + 1f).toInt()
                       notifyListener(dropFrame)
                  }
                   lastFrameTime = frameTimeNanos
              } else {
                   lastFrameTime = 0
              }
          }
      }

    这样我们就实现了一个监控滑动帧率的方案,代码实现放在了 github.com/drummor/Get…




(方案来自淘宝技术团队)


6 帧率监控进化 之三 官方方案


官方出手,官方在Android N 以上新增了Window.OnFrameMetricsAvailableListener可以监听每帧的执行状态。包含总耗时,绘制耗时,布局耗时,动画耗时,测量耗时。依次我们可以计算出帧率。


  private val metricsAvailableListener =
       Window.OnFrameMetricsAvailableListener { window, frameMetrics, dropCountSinceLastInvocation ->
           val intent = frameMetrics?.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP) ?: 0
           val vsync = frameMetrics?.getMetric(FrameMetrics.VSYNC_TIMESTAMP) ?: 0
           val animation = frameMetrics?.getMetric(FrameMetrics.ANIMATION_DURATION) ?: 0
           val vsyncTotal = frameMetrics?.getMetric(FrameMetrics.TOTAL_DURATION) ?: 0
           val measureCost = frameMetrics?.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) ?: 0    
           //计算帧率
      }

this.window.addOnFrameMetricsAvailableListener(//向window注册监听
                metricsAvailableListener,
  Handler(handlerThread.looper)

同时配合Jetpack的FrameMetricsAggregator的可以统计出帧耗时情况。


 private val frameMetricsAggregator = FrameMetricsAggregator()
frameMetricsAggregator.add(this@FrameActivity)
frameMetricsAggregator.metrics?.let {
               it[FrameMetricsAggregator.TOTAL_INDEX] //总耗时概况
               it[FrameMetricsAggregator.INPUT_INDEX] //输入事件耗时
               it[FrameMetricsAggregator.DRAW_INDEX]  //绘制事件耗时概况
          }

FrameMetricsAggregator内部存储比较有意思,是有一个SparseIntArray数组SparseIntArray[] mMetrics = new SparseIntArray[LAST_INDEX + 1],存储各个阶段的耗时SparseIntArray的key为耗时,value为该耗时的个数。


mMetrics[TOTAL_INDEX]:
{3=8, 4=13, 5=2, 6=44, 7=4, 15=1, 196=1, 198=1, 204=1, 209=1, 210=1, 233=1, 265=1}

如上这是每帧总耗时的分布,耗时3ms的有8个,耗时4ms的有8个


我们可以制定自己的标准,诸如单帧耗时<30ms为优秀,单帧耗时>30ms 且<60ms为正常,单帧耗时>60ms且<200ms为过高,单帧>200为严重。


7 数据统计


首先有一个大的原则,帧耗时统计是在有渲染动作发生时统计,空闲状态不统计。


帧率的统计就是,渲染帧的数量除以有帧渲染发生动作时间得到。


另,每帧的耗时不尽相同,希望抓住主线,针对性的统计慢帧冻帧的数量以及占比。或者切割的更为精细,如Matrix里默认的把帧的耗时表现分为四个等级。



  • 正常帧,<3*16ms

  • 中间帧,<9*16ms

  • 慢帧,<24*16ms

  • 冻帧,<42*16ms


再有就是,如通过adb shell dumpsys gfxinfo packagename命令或者FrameMetricsAggregator里的统计方式,把相同耗时的帧进行合并。


帧的统计往往以page(Activity)为维度,作为一个数据大盘数据。


8 其他



  • 帧率真实一个笼统的指标,会存在单帧耗时很高,还是帧率平均下来很优秀,从数据上看问题不大,但是用户的感知会比较强烈。我们更需要做的找到那个隐藏着的【耗时高】的单帧;我们需要全面的对主线程里的执行任务进行全面的监控,也就是卡顿监控的范畴。

  • 帧率只是统计【页面绘制】的概况,不能够全面反映主线程的耗时情况。主线程如果存在耗时动作,比如一个主线程的Handler的执行了一个>100ms的任务,如果此时并没有绘制任务需要执行,此时的不一定帧率就会降低。

  • 【warning!!】最后,已经困扰好几天,实际测试中发现,使用Window.OnFrameMetricsAvailableListener与hook choreograoher方案对比,Window.OnFrameMetricsAvailableListener有漏报的情况产生。这需要看framework源码进一步追查,有对这方面有研究的同学欢迎留言讨论。

  • 本文涉及的实现的代码以上传至github github.com/drummor/Get…


关注点赞鼓励,流畅性三板斧系列剩下的两篇,卡顿监控和ANR监控也会陆续放出。



作者:Drummor
来源:juejin.cn/post/7217801491188809789
收起阅读 »

这一次,我还是想选择自由

辞职回老家有一周多了。 这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。 找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术...
继续阅读 »

辞职回老家有一周多了。


这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。



找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术文章。


自由职业的话收入不稳定,赚多赚少都要靠自己。但可以住在小县城的家里,有妈妈做的好吃的菜,有可爱粘人的猫猫,有我新买的投影仪可以和妈妈一起看电视,可以和美好的一切在一起。我不喜欢旅游之类的,宅在家里就已经是我最幸福的状态了。




而且具体做啥可以自己来决定,我有挺多想研究的东西的。


这两天也在面试了,还是那些八股文,卷来卷去的,没啥意思。可能如果真的去了字节,我会更不适应。要不还是不继续面了。


我去年也自由职业过,现在和那时候的区别是我粉丝更多了,技术积累也更多了,而且给我妈新买了个房子,可以在这里继续我的神光实验室。



上图是神光实验室 1.0,之前在老家附近租的一个出租屋。


神光实验室 2.0 是这样的,在新家里:




上次结束自由职业是因为我爸的要求,他说还是希望我有个正当工作。


现在我爸没了,没有人会阻止我了。


我没有负债,还有一定的积蓄,而且我现在啥也不干也有能养活自己的收入。




要不就再任性一次,在家里继续自己的技术梦想,继续搞神光实验室?🤔


就这么愉快的决定了!


这一次,我还是想遵循自己的内心,选择自由,选择和喜欢的一切在一起。


以后公众号会保持日更,其余时间写小册和准备出版的书。


努力一点的话,各方面应该还是可以的。



作者:zxg_神说要有光
来源:juejin.cn/post/7217810344697577528
收起阅读 »

如何使用 ChatGPT 3.5 API 创建自己的智能应用?

前言 OPEN AI 的开放 API 可以说是前端开发者的福利,我们只需要调用 api,就可以创建一个智能应用, 在上一篇文章中,我们介绍了《基于 ChatGPT API 的划词翻译浏览器脚本实现》,使用的模型是 text-davinci-003 也就是文本补...
继续阅读 »

前言


OPEN AI 的开放 API 可以说是前端开发者的福利,我们只需要调用 api,就可以创建一个智能应用,
在上一篇文章中,我们介绍了《基于 ChatGPT API 的划词翻译浏览器脚本实现》,使用的模型是 text-davinci-003 也就是文本补全模型,今天我们将使用 gpt-3.5-turbo 模型来实现一个场景化的智能应用。


OPEN AI API 介绍


自动完成 API


POST https://api.openai.com/v1/completions


以下是自动完成 API,有了 OPENAI_API_KEY 之后,我们只需要传入 prompt


const OPENAI_API_KEY = "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";

const prompt = `Translate this into Chinese:
hello world`
;
const res = await fetch("https://api.openai.com/v1/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "text-davinci-003",
prompt,
max_tokens: 1000,
temperature: 0,
}),
});
const response = await res.json();

const result = response.choices[0].text;

对话 API


POST https://api.openai.com/v1/chat/completions


由于自动补全 API 只能传入一个参数 prompt,AI 不能够理解上下文的场景,因此 gpt-3.5+ API 是为了让 AI 能够支持基于一组对话来返回数据。


在 Node.js 中可以使用以下代码来实现。


const OPENAI_API_KEY = "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";

const prompt = [...];
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages,
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
max_tokens: 500
}),
});
const response = await res.json();

const result = response.choices[0].message

以下是官网给出 messages 例子


const messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]


  • 每一个 message 由 rolecontent 组成。

  • role 只能是 3 个值, systemuserassistant

  • systemassistant 是可选的,user 是必须的。


官方提供了 playground 帮助我们创建 messages 信息。


openai playground


assistant 也就是其中一次返回的数据信息。
发送的 messages 如下:


const messages=[
{
"role": "system",
"content": "你是一名精通 typescript 的前端工程师,不需要解释"
},
{
"role": "user",
"content": "Convert the following JSON to typescript interface without explanation\n\n{\n \"name\": \"Allen\",\n \"age\": 18\n}"
}
]

比如使用上面的 messages 信息,我们就可以根据它,来创建一个 Tailwind css 代码生成器。


openai playground 拷贝 fetch


通过右键可以直接拷贝为 Node.js fetch 代码。


再来实现一个 JSON 转 Typescript 的例子


openai playground JSON 转 Typescript


那么我们通过以上截图的 messages,就可以创建一个 JSON 转 Typescript 生成器。


在 Next.js 使用


接下来,我们就在 Next.js 中创建一个全栈应用。


那为什么选择使用 Next.JS 呢?



  1. 它是一个全栈框架,既可以写接口也可以使用 react 写前端;

  2. 可以很轻松部署到 verel, 让我们可以直接访问 OPENAI 的接口,摆脱网络限制。


这里我选择使用大圣老师的email-helper模板


创建github仓库


点击 GitHub 选择 Use this Template, 创建一个自己的仓库


目录结构


这个项目很简单,在 pages 目录下 api/generate.ts 用于代理请求接口。


index.tsx 也就是我们的主界面,一个按钮,一个请求,没有其他复杂逻辑。


接下来我们就根据它来创建一个智能的Tailwind CSS 代码生成器


1、首先将 messages 改成以上截图中的 message


2、然后将需要生成的变量存到 state 中,我们就可以实现如下界面


Tailwind CSS 代码生成器界面


点击生成代码就可以 让 ai 帮我们写代码了。


这个界面,有些单调,可以在这个页面上列一些常用的组件,那么也可以直接使用 chatGPT 来生成。


chatGPT 生成组件


将 GPT 回答直接转换成 JSON 数据


chatGPT 转 JSON 数据


将数据渲染到页面中,就可以生成快捷标签了


实现效果


接下来,再将 Tailwind css 的颜色,作为我们的变量,同样使用 GPT 来生成数据


生成 Tailwind 颜色


用同样的方式,转化成 JSON,拷贝到我们的代码中。


Tailwind CSS 代码生成器效果


最后一步,我们需要实现一个预览效果,这样的话,就可以所见即所得,根据效果,直接拷贝想要的代码。



小结


本文介绍了 openai 的 api 使用方法,以及如何使用 openai 的 playground 生成需要的 messages 信息。并且通过一个 Next.js 实战例子,结合 ChatGPT 开发了一个 Tailwind CSS 代码生成器。


最后


贴一下文本的代码仓库和预览地址


代码仓库:github.com/maqi1520/op…


预览地址:openai.maqib.cn/


如果对你有帮助,记得给个三连,感谢你的阅读。





作者:狂奔滴小马
来源:juejin.cn/post/7217820487203192892
收起阅读 »

看了antfu大佬的v-lazy-show,我学会了怎么编译模板指令

web
前言 一开始关注到 antfu 是他的一头长发,毕竟留长发的肯定是技术大佬。果不其然,antfu 是个很高产、很 creative 的大佬,我也很喜欢他写的工具,无论是@antfu/eslint-config、unocss、还是vitest等等。 而这篇文章故...
继续阅读 »

前言


一开始关注到 antfu 是他的一头长发,毕竟留长发的肯定是技术大佬。果不其然,antfu 是个很高产、很 creative 的大佬,我也很喜欢他写的工具,无论是@antfu/eslint-configunocss、还是vitest等等。


而这篇文章故事的起源是,我今天中午逛 github 的时候发现大佬又又又又开了一个新的 repo(这是家常便饭的事),v-lazy-show


image.png


看了下是两天前的,所以好奇点进去看看是什么东东。


介绍是:A compile-time directive to lazy initialize v-show for Vue. It makes components mount after first truthy value (v-if), and the DOM keep alive when toggling (v-show).


简单的说,v-lazy-show 是一个编译时指令,就是对 v-show 的一种优化,因为我们知道,v-show 的原理只是基于简单的切换 display none,false则为none,true则移除


bite-me-i-dare-you.gif


但即使在第一次条件为 falsy 的时候,其依然会渲染对应的组件,那如果该组件很大,就会带来额外的渲染开销,比如我们有个 Tabs,默认初始显示第一个 tab,但后面的 tab 也都渲染了,只是没有显示罢了(实际上没有必要,因为可能你点都不会点开)。


那基于此种情况下,我们可以优化一下,即第一次条件为 falsy 的情况下,不渲染对应的组件,直到条件为 truthy 才渲染该组件。


将原本的 v-show 改为 v-lazy-show 或者 v-show.lazy


<script setup lang="ts">
import { ref } from 'vue'
import ExpansiveComponent from './ExpansiveComponent.vue'

const enabled = ref(false)
</script>

<template>
<button @click="enabled = !enabled">
Toggle
</button>

<div class="hello-word-wrapper">
<ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
<ExpansiveComponent v-show.lazy="enabled" msg="v-lazy.show" />

<ExpansiveComponent v-show="enabled" msg="v-show" />

<ExpansiveComponent v-if="enabled" msg="v-if" />
</div>
</template>

<!-- ExpansiveComponent.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'

const props = defineProps({
msg: {
type: String,
required: true,
},
})

onMounted(() => {
console.log(`${props.msg} mounted`)
})
</script>

<template>
<div>
<div v-for="i in 1000" :key="i">
Hello {{ msg }}
</div>
</div>
</template>

2023-04-03 15.55.15.gif



ExpansiveComponent 渲染了 1000 行 div,在条件 enabled 初始为 false 的情况下,对应 v-show 来说,其依然会渲染,而对于 v-lazy-show 或 v-show.lazy 来说,只有第一次 enabled 为 true 才渲染,避免了不必要的初始渲染开销



如何使用?


国际惯例,先装下依赖,这里强烈推荐 antfu 大佬的 ni


npm install v-lazy-show -D
yarn add v-lazy-show -D
pnpm add v-lazy-show -D
ni v-lazy-show -D

既然是个编译时指令,且是处理 vue template 的,那么就应该在对应的构建工具中配置,如下:


如果你用的是 vite,那么配置如下


// vite.config.ts
import { defineConfig } from 'vite'
import { transformLazyShow } from 'v-lazy-show'

export default defineConfig({
plugins: [
Vue({
template: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加在这里
],
},
},
}),
]
})

如果你用的是 Nuxt,那么应该这样配置:


// nuxt.config.ts
import { transformLazyShow } from 'v-lazy-show'

export default defineNuxtConfig({
vue: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加上这行
],
},
},
})

那么,该指令是如何起作用的?


上面的指令作用很好理解,那么其是如何实现的呢?我们看下大佬是怎么做的。具体可见源码


源码不多,我这里直接贴出来,再一步步看如何实现(这里快速过一下即可,后面会一步步分析):


import {
CREATE_COMMENT,
FRAGMENT,
createCallExpression,
createCompoundExpression,
createConditionalExpression,
createSequenceExpression,
createSimpleExpression,
createStructuralDirectiveTransform,
createVNodeCall,
traverseNode,
} from '@vue/compiler-core'

const indexMap = new WeakMap()

// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L28
const NodeTypes = {
SIMPLE_EXPRESSION: 4,
}

// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L62
const ElementTypes = {
TEMPLATE: 3,
}

// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/shared/src/patchFlags.ts#L19
const PatchFlags = {
STABLE_FRAGMENT: 64,
}

export const transformLazyShow = createStructuralDirectiveTransform(
/^(lazy-show|show)$/,
(node, dir, context) => {
// forward normal `v-show` as-is
if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}

const directiveName = dir.name === 'show'
? 'v-show.lazy'
: 'v-lazy-show'

if (node.tagType === ElementTypes.TEMPLATE || node.tag === 'template')
throw new Error(`${directiveName} can not be used on <template>`)

if (context.ssr || context.inSSR) {
// rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
node.props.push({
...dir,
exp: dir.exp
? createSimpleExpression(dir.exp.loc.source)
: undefined,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'if',
})
return
}

const { helper } = context
const keyIndex = (indexMap.get(context.root) || 0) + 1
indexMap.set(context.root, keyIndex)

const key = `_lazyshow${keyIndex}`

const body = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
[node],
PatchFlags.STABLE_FRAGMENT.toString(),
undefined,
undefined,
true,
false,
false /* isComponent */,
node.loc,
)

const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
) as any

context.replaceNode(wrapNode)

return () => {
if (!node.codegenNode)
traverseNode(node, context)

// rename `v-lazy-show` to `v-show` and let Vue handles it
node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})
}
},
)

createStructuralDirectiveTransform


因为是处理运行时的指令,那么自然用到了 createStructuralDirectiveTransform 这个函数,我们先简单看下其作用:


createStructuralDirectiveTransform 是一个工厂函数,用于创建一个自定义的 transform 函数,用于在编译过程中处理特定的结构性指令(例如 v-for, v-if, v-else-if, v-else 等)。


该函数有两个参数:




  • nameMatcher:一个正则表达式或字符串,用于匹配需要被处理的指令名称。




  • fn:一个函数,用于处理结构性指令。该函数有三个参数:



    • node:当前节点对象。

    • dir:当前节点上的指令对象。

    • context:编译上下文对象,包含编译期间的各种配置和数据。




createStructuralDirectiveTransform 函数会返回一个函数,该函数接收一个节点对象和编译上下文对象,用于根据指定的 nameMatcher 匹配到对应的指令后,调用用户自定义的 fn 函数进行处理。


在编译过程中,当遇到符合 nameMatcher 的结构性指令时,就会调用返回的处理函数进行处理,例如在本例中,当遇到 v-show 或 v-lazy-show 时,就会调用 transformLazyShow 处理函数进行处理。


不处理 v-show


if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}

因为 v-show.lazy 是可以生效的,所以 v-show 会进入该方法,但如果仅仅只是 v-show,而没有 lazy 修饰符,那么实际上不用处理


这里有个细节,为何要将指令对象 push 进 props,不 push 行不行?


原先的表现是 v-show 条件为 false 时 display 为 none,渲染了节点,只是不显示:


image.png


而注释node.props.push(dir)后,看看页面表现咋样:


image.png


v-show 的功能没了,也就是说指令的功能会添加到 props 上,所以这里要特别注意,不是单纯的返回 node 即可。后来还有几处node.props.push,原理跟这里一样。


服务端渲染目前是转为 v-if


if (context.ssr || context.inSSR) {
// rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
node.props.push({
...dir,
exp: dir.exp
? createSimpleExpression(dir.exp.loc.source)
: undefined,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'if',
})
return
}

将 v-lazy-show 改名为 v-if,且过滤掉修饰符


createVNodeCall 给原先节点包一层 template


顾名思义,createVNodeCall 是 用来创建一个 vnode 节点的函数:


const body = createVNodeCall(
/** 当前的上下文 (context) 对象,即 CodegenContext */
context,
/** helper 函数是 Vue 内部使用的帮助函数。FRAGMENT 表示创建 Fragment 节点的 helper 函数 */
helper(FRAGMENT),
/** 组件的 props */
undefined,
/** 当前节点的子节点数组,即包含有指令的节点本身 */
[node],
/** 表示该节点的 PatchFlag,指明了该节点是否稳定、是否具有一些特定的更新行为等。STABLE_FRAGMENT 表示该 Fragment 节点是一个稳定的节点,即其子节点不会发生改变 */
PatchFlags.STABLE_FRAGMENT.toString(),
/** 该节点的动态 keys */
undefined,
/** 该节点的模板引用 (ref) */
undefined,
/** 表示该节点是否需要开启 Block (块) 模式,即是否需要对其子节点进行优化 */
true,
/** 表示该节点是否是一个 Portal 节点 */
false,
/** 表示该节点是否是一个组件 */
false /* isComponent */,
/** 该节点在模板中的位置信息 */
node.loc,
)

参数含义如下,简单了解即可(反正看了就忘)


也就是说,其会生成如下模板:


<template>
<ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
</template>

关键代码(重点)


接下来这部分是主要原理,请打起十二分精神。


先在全局维护一个 map,代码中叫 indexMap,是一个 WeakMap(不知道 WeakMap 的可以去了解下)。然后为每一个带有 v-lazy-show 指令的生成一个唯一 key,这里叫做_lazyshow${keyIndex},也就是第一个就是_lazyshow1,第二个是_lazyshow2...


  const keyIndex = (indexMap.get(context.root) || 0) + 1
indexMap.set(context.root, keyIndex)

const key = `_lazyshow${keyIndex}`

然后将生成的key放到渲染函数的_cache上(渲染函数的第二个参数,function render(_ctx, _cache)),即通过_cache.${key}作为辅助变量。之后会根据 createConditionalExpression 创建一个条件表达式


const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
// 生成一个注释节点 `<!--v-show-if-->`
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
)

也就是说, v-lazy-show 初始传入的条件为 false 时,那么会为你创建一个注释节点,用来占位:


createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
])

image.png



这个跟 v-if 一样



直到第一次条件为真时,将 _cache.${key} 置为 true,那么以后的行为就跟 v-show 一致了,上面的 dir.exp 即指令中的条件,如


<div v-show="enabled"/>

enabled 即 exp,表达式的意思。


readme给出的转换如下:


<template>
<div v-lazy-show="foo">
Hello
</div>
</template>

会转换为:


import { Fragment as _Fragment, createCommentVNode as _createCommentVNode, createElementBlock as _createElementBlock, createElementVNode as _createElementVNode, openBlock as _openBlock, vShow as _vShow, withDirectives as _withDirectives } from 'vue'

export function render(_ctx, _cache) {
return (_cache._lazyshow1 || _ctx.foo)
? (_cache._lazyshow1 = true, (_openBlock(),
_withDirectives(_createElementVNode('div', null, ' Hello ', 512 /* NEED_PATCH */), [
[_vShow, _ctx.foo]
])))
: _createCommentVNode('v-show-if', true)
}

你可以简单理解为会将<ExpansiveComponent msg="v-lazy-show" v-lazy-show=""enabled"/>转为下面:


<template v-if="_cache._lazyshow1 || enabled">
<!-- 为true时会把_cache._lazyshow1置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-lazy-show" v-lazy-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>

<template v-if="_cache._lazyshow2 || enabled">
<!-- 为true时会把_cache._lazyshow2置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show.lazy="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>

然后将原先节点替换为处理后的 wrapperNode 即可


context.replaceNode(wrapNode)

最后将 v-lazy-show | v-shouw.lazy 处理为 v-show


因为 vue 本身是没有 v-lazy-show 的,v-show 也没有 lazy 的的修饰符,那么要让指令生效,就要做到两个:



  1. 将原先的 show-lazy 改名为 show

  2. 过滤掉 lazy 的修饰符


node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})

也就变成这样啦:


<template v-if="_cache._lazyshow1 || enabled">
<!-- 为true时会把_cache._lazyshow1置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>


<template v-if="_cache._lazyshow2 || enabled">
<!-- 为true时会把_cache._lazyshow2置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-show.lazy" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>

小结一下:




  1. 为每一个使用 v-lazy-show 分配唯一的 key,放到渲染函数内部的_cache上,即借助辅助变量_cache.${key}



    • 当初始条件为 falsy 时不渲染节点,只渲染注释节点 <!--v-show-if-->

    • 直到条件为真时将其置为 true,之后的表现就跟 v-show 一致了





  1. 由于 vue 不认识 v-lazy-show,v-show.lazy,使用要将指令改回 v-show,且过滤掉 lazy 修饰符(如果使用 v-show.lazy 的话)


最后


以上就是我对该运行时编译插件的认识了,可以将 repo 拉下来,上面有个 playground,可以自己调试调试,说不定有新的认识。


好了,文章到此为止,你今天学废了吗?


image.png



作者:暴走老七
来源:juejin.cn/post/7217836890119995450
收起阅读 »

程序员“摸鱼”神器,GitHub Copilot“凭本事”完全免费!!

上周Microsoft 365 Copilot的发布会上几段演示视频让朋友圈沸腾了一整天,颠覆,失业,工业革命刷屏,普遍的焦虑中有工作中重度依赖office的朋友表示如果功能都是真的,那么确实可以节约出很多时间摸鱼,但是立马就有人提醒他或许老板觉得可以节约掉一...
继续阅读 »

上周Microsoft 365 Copilot的发布会上几段演示视频让朋友圈沸腾了一整天,颠覆,失业,工业革命刷屏,普遍的焦虑中有工作中重度依赖office的朋友表示如果功能都是真的,那么确实可以节约出很多时间摸鱼,但是立马就有人提醒他或许老板觉得可以节约掉一半的人省下成本换跑车。


各类信息流中也夹杂着对文心一言的冷嘲热讽,熊熊烈火,车水马龙的梗证实了文心一言确实支持多模态,对中文也有更深的理解...但是不多(纯调侃,本人看好文心的未来)。


图片


图片


我也看了微软发布会的录播和文心一言发布会的直播,发布会上二者的演示内容的形式都是播放视频(为什么只有百度因为这点被骂)。从产品的一系列命名可以看出,微软还是希望以人为中心产出内容,AI只是一个副机长,协助主角更高效地产出,从而让机长把时间留给更有价值,更需要脑力的事情上。 李彦宏在发布的全过程中其实也没有自吹自擂,甚至反复承认目前的效果一般,内部体验的过程中还有不少瑕疵,同时也花了很多口舌强调文心一言 【未来】在中文场景中表现会好于ChatGPT。


对比下来,同样的PPT发布,二者都宣称自己不是简单的GPT套壳,而是结合了自身的优势,借力GPT深度整合以及增强了自家产品。


我看到Copilot这个词,还是下意识地先想起了一些跟程序员有关的事情。


其实早在2021年6月份的时候,GitHub(已被微软收购)就发布了GitHub Copilot,对,也叫Copilot,但是一直没有引起很广泛的关注,原因我猜测有以下几点




  • 程序员圈子的事情,没有出圈




  • 彼时LLM,GPT等概念还没有火,没有获得广泛关注




  • 普遍觉得AI真正在编程这件事上取代人还言之过早




那么站在发布接近2年之后的今天,GitHub Copilot可以做些什么呢?


从功能性的角度出发,其实官网最显眼的位置已经概括得非常准确了


图片


Github Copilot基于openAI预训练的模型,在几十亿行的公开代码的基础上进行了训练,可以将自然语言描述的逻辑转换为代码建议,目前已经支持几十种编程语言。通过这个“助手”,全世界的开发者都可以从样版代码,重复工作等dirtywork中解放出来,把精力聚焦在更重要的事情上——构建伟大的软件!!


GO语言的效果如下:


图片


TS的效果如下:


图片


在用户的主观投票上,可以看出大家比较认可Github Copilot带来的生产力提升,但是有一说一,这个是主观的评价,并无法知道从客观的角度上,老板主观上是不是觉得你提效了(当然你可以只为了自己开心),嘻嘻。


图片


Github Copilot带来的另一个重大的意义——有了这个副机长的帮助,在面对自己不熟悉的语言或者平台编程的时候,你会更有信心。不会在一些低级错误上面拉扯很久,减少在基础的问题上的自信受挫。


在当前主流的IDE或者编辑器上都可以找到插件,目前每一个GitHub的普通用户都可以获得60天的试用期。


图片


具体的使用步骤,这里以VSCode为例:


step1: 在应用商店搜索Github Copilot,点击安装


图片


step2: 安装完成之后,会弹出提示框跳转到Github登录


图片


step3: 登录完成之后,会叫你充钱。😊


图片


60天试用的标题非常醒目!!


最底下还有两行文字说明了可以免费试用的人群,这里我给大家放大看看:


图片


如果是你GitHub上面最流行的那部分开源项目的贡献者的话或者是认证的过的学生(有苹果教育优惠内味儿了!!),可以免费使用Github Copilot。至于“最流行的开源项目”包含哪些?我也没有找到这样的一个名单...不过如果你符合要求的话,点开订阅页面的时候,直接就可以看到免费订阅的操作俺妞。


你看看,我标题是不是没有乱取?!!是不是真有人可以完全免费?!!!(逃)


我很有B数,乖乖准备充钱了。支持信用卡或者贝宝,...反正我最后没充钱成功,有谁成功了留言告诉我哦。😊


图片


如果在VSCode的应用商店中搜索Github Copilot Labs插件的话,你就会发现一款插件的插件,本质上是将一些常用的Prompts封装成了按钮和可视化的操作,比如:


解析代码(帮你看懂一块屎山代码到底做了啥)


图片


实现语言转换


图片


代码刷子功能


可以增强代码可读性,添加类型,智能修复可能的bug,比code Runner更优秀的即时代码调试,优化冗余代码.....


代码刷子在日常编码中应该是非常实用了,可以有效提升代码质量,建议大家充钱试试。


图片


测试用例生成


这个功能可以说是非常非常非常实用了,平时写单元测试其实非常耗时,而且有不少样版代码,这块工作有人代劳的话,真的是可以省出很多时间(摸鱼)!!**

**


图片


你可能会问了,介绍了这么多功能,怎么不点按钮让大伙儿看看效果?!


肯定不是缺钱,而是真的充钱失败了,可能我的visa卡有问题!


好用归好用,如果你既没有免费的资格,又没有充钱,且还安装了插件的话,插件会非常烦人地弹窗提醒你没权限,连不上(催你充钱)!有点讨厌。


图片


图片


最后,不得不提的是这个工具的局限性,ChatGPT(基于3.5)告诉了我以下几点帮大家避雷。


图片


祝福大家充完钱之后立马变强!!


如果没有充钱,也变强了,请留言告诉我!!


如果没有充钱,也没有变强,请留言告诉我!!


如果充钱了,也没有变强,请留言告诉我!!


如果没有充钱,也没有变强,请留言告诉我!!


图片


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

GPT-4风口来临!20个你可以起飞的姿势

聊天 GPT 正在席卷网络世界,它让人们赚了很多钱。 在本文中,列出了20 个最佳聊天 GPT 商业理念。 我们将介绍您可以使用聊天 GPT 创建的一些 AL工县示例,以及您可以作为个体创业者销售的不同服务。 全文目录: 1.聊天 GPT Saa...
继续阅读 »

聊天 GPT 正在席卷网络世界,它让人们赚了很多钱。


在本文中,列出了20 个最佳聊天 GPT 商业理念。


我们将介绍您可以使用聊天 GPT 创建的一些 AL工县示例,以及您可以作为个体创业者销售的不同服务。


全文目录:




  • 1.聊天 GPT SaaS 业务




  • 2. API 即服务




  • 3.AI自动优酷频道




  • 4. 社交媒体营销机构




  • 5. 使用聊天GPT创建课程




  • 6.开始按需打印商店




  • 7.AI 个人助理




  • 8.AI 自媒体助手




  • 9. 客户服务聊天机器人




  • 10. 财务规划应用程序




  • 11. 健康与保健应用程序




  • 12. 娱乐应用




  • 13. 数字副本




  • 14. 转录应用程序




  • 15.AI 旅游应用




  • 16. 新闻和信息应用程序




  • 17.AI 流媒体服务




  • 18.AI 网红营销




  • 19. 文案服务




  • 20. 编码服务




  • GPT商业理念-最后的话




1.聊天 GPT SaaS 业务


第一个也可能是最有利可图的聊天 GPT 商业理念是开展 SaaS 业务。


去GPT 并创建一个简单的单词计数器。您可以键入如下提示:


“编写工具网站的完整代码,计算文本区域中的字数。使用 HTML、CSS 和 JS”


图片


获得代码后,您可以打开Visual Studio代码,创建三个文件 JavaScript, CSS和HTML,然后简 单地粘贴代码。


我们有一个简单的单词计数器工具。


这就是您可以使用聊天 GPT 创建软件的方式。


您还可以告诉它以使界面更加有趣和用户友好。


你也可以在Code Canyon等网站上出售迷你JavaScript文件。


我知道很多人每个月都在Code Canyon上销售JS文件赚数千美元。


我玩过聊天 GPT,并在不到 1 小时的时间内创建了五个以上的工具。


但现在最大的问题是如何经受住竞争。我们有很多工具。那么如何竞争呢?


只需遵循这 4 条规则:




  1. 创建独特的界面。告诉乍得 GPT 并使用它,直到它创建最佳用户界面。




  2. 在一个网站中组合多个工具。




  3. 在Medium,Reddit和社交媒体上推广您的工具。




  4. 尝试找到低竞争的工具创意。




💡 专业提示


注意:请注意,即使使用 Chat GPT,构建软件也需要一些编码技能。如果您在这些领域没有经验,则可以将其外包给Fiverr等网站,人们已经使用聊天GPT提供网络编程服务。这是一种无需任何编码即可启动 SaaS 业务的简单方法。


2. API 即服务


下一个聊天 GPT 商业理念是创建一个 API 并将其出售为每月定期会员资格。这种方法的好处是无需投资即可开始。


但首先,什么是 API?


API 只是开发人员用来通过代码访问自己操作的数据的东西。


一个简单的例子。如果你现在想在Facebook上发布帖子,你只需要去你的手机或网络浏览器,打开Facebook并使用浏览器或Facebook界面发布。


如果您是开发人员,并且想使用自己的代码在应用程序中访问Facebook,该怎么办?您将需要使用Facebook API。所以简单地说,API就像一个经纪人。


这有点像你、开发人员和某个服务(如Facebook或其他任何东西)之间的中间人。现在,你知道什么是 API,让我们使用 Charge GPT 在几秒钟内创建一个。


在 Charge GPT 中,让我们测试创建一个生成代码的 API,以便任何人都可以在自己的应用程序中使用它。所以这是代码。


图片


您可以复制它,打开 Visual Studio,创建控制器,粘贴代码,API将在不到1分钟的时间内启动并运行。


现在,您需要发布此 APl。为此,您还可以询问聊天 GPT 我们可以在哪里免费发布此内容。在


这里,我们有多种选择,例如Microsoft Azure,AwS Lambda或Google Cloud。


图片


好的,现在,在哪里出售它?


让我们问问聊天 GPT。在这里,我们有很多市场,我们可以在其中发布我们的 API 并像 Rapid API 一样出售它等等。


图片


您甚至可以告诉聊天 GPT 用您想要的任何编程语言(如 Node、JS 或 Python)重写代码,任何您想要的内容。


棒。好吧,看起来很简单。是的。但主要问题在于 API 的想法。从哪里获得人们会购买或开发人员会购买的想法。


可以通过查看顶级API市场来获得一些想法:


● RapidAPI 是一个平台,使开发人员能够查找并连接到数千个 API。它提供各种类别的 API,包括金融、业务健康和媒体。


● ProgrammableWeb 是一个 API 目录,提供有关 API 和提供它们的公司的信息。它允许开发人员按类别搜索 API,并为 API 提供测试和集成工具。


● 邮递员 API 网络是来自领先公司的 API 集合,可用于测试和集成。包括来自PayPal,微软和谷歌等公司的API。


以下是您可以开发和销售的一些 API 即服务理念:


● 提供实时天气数据(包括当前状况、预报和历史数据)的天气 API。企业、开发人员和应用程序创建者可以使用它来向其用户提供与天气相关的信息。


● 一种图像识别API,可以对图片中的对象、场景和活动进行识别和分类。企业和开发人员使用它来创建可以自动识别和组织图像的应用程序,或训练机器学习模型。


● 一种社交媒体API,允许开发人员访问来自各种社交媒体平台(如Facebook,Twitter和Instagram)的数据。企业和开发人员可以使用它来分析社交媒体活动、跟踪指标或构建社交媒体营销工具。


● 一种基于位置的 API,允许开发人员访问与地理位置相关的数据,例如地图、兴趣点和交通信息。


● 一种支付 API,允许开发人员轻松地将各种支付方式(如信用卡、PayPal和 Apple Pay)集成到他们的 app 和网站中。


● 一种语音和语音识别 API,允许开发人员向其应用和设备添加语音控制功能。


● 一种自然语言处理 API,允许开发人员向其应用和设备添加自然语言理解。


3.AI自动优酷频道


看看现在在几乎任何平台上创建 AI 社交媒体形象变得多么容易。


使用聊天 GPT,您可以创建自己的全自动自媒体频道。


您只需转到聊天 GPT 并键入“给我写 1500 字的视频脚本......”。


目前这里唯一的限制是它还无法为您编辑视频。


但是一个真正强大的工具是Pictory AI,它是一个AI视频生成器,可以创建视频而无需制作任何实际镜头或录制您的声音


只需将您的脚本复制到 Pictory AI 中并将脚本推送到视频,即可将此脚本转换为带有 b-roll 的完整视频。


图片


以这种疯狂的创建内容的速度,您每天都可以创建数千条内容。


如果你是一个创作者,并且你将来不会使用人工智能来实际创建你的内容,从长远来看,你会失败。


4. 社交媒体营销机构


随着社交媒体平台逐年增长,越来越多的企业需要社交媒体的存在,因此对社交媒体管理服务的需求不断增加。


如果您选择这条路线,您可以直接向企业提供服务,并创办SMMA代理机构或在Fiverr或Upwork等自由职业平台上创建演出。


ChatGPT 可以通过多种方式为您提供帮助。它可以帮助您制定内容策略,并帮助您撰写引人入胜的社交媒体帖子和标题。


但是,重要的是要记住,社交媒体管理还涉及诸如选择和编辑图像以及回复评论之类的任务,这些任务仍然需要您的个人风格。


5. 使用聊天GPT创建课程


所以我们不得不谈论教育的转型。人们正在从专家那里在线购买课程和辅导计划,这些课程和辅导计划在某种程度上取代了对大学的需求,并不完全是大学的需求,而是很多人并不完全知道如何对待自己的生活。


对于真正小众的话题,例如学习钢琴或音频工程或博客或人们可以购买 1000 美元课程的这些独特事物,这一点变得越来越真实。


假设我们正在制作一个关于如何制作黄油的课程。您可以生成制作黄油的所有步骤的完整列表,并通过在此处和那里进行一些更改来添加您的专业知识,以获得整个课程的完美记录。


图片


如果你想为课程添加一些特定事物的图片,你可以从MidJourney获得不受版权保护的图像,并要求它制作一根扔进锅里的黄油。


图片


就这样,你会在平底锅里看到一张漂亮的黄油图片。


通过这样做,您将拥有可以在 Kartra 或 点击漏斗 等平台上销售的整个课程。


有成千上万的人愿意为信息付费,而不是自己学习。


关键是睁开眼睛看看这个世界,意识到人们每月通过销售课程赚取数百万元。


因此,实际上,最终流量加上后端销售漏斗相当于一百万元的业务。


6.开始按需打印商店


这种商业模式的前提是围绕人们真正感兴趣的特定主题在 Etsy 上创建利基商店。


在这个商业模式中,你的工作是在旅途中创造令人敬畏的艺术,这是一种人工智能艺术生成工具,然后将其放在一些T恤或海报上,并将其放在Etsy上的利基商店中。然后使用按需打印服务来运送和创建这些产品。


要开始,您首先需要找到一个利基市场。Etsy的一个很好的关键字研究工具称为eRank,可让您在Etsy上找到竞争较低的关键字。


图片


一旦找到一个利基市场并找到关键字,您就可以前往 MidJourney 网站,这是一个人工智能艺术生成平台。


您需要做的就是使用提供的提示开始创建自己的艺术。


图片


既然您已经拥有了一件您认为您的利基市场中的人会喜欢的好艺术品,您现在需要创建一个 Etsy 帐户,然后将该 Etsy 帐户连接到按需打印服务,例如 Printful.


Printful 是一款很棒的按需印刷品,可让您将图像放在杯子、海报 T 恤和一大堆其他商品上。


图片


Printful 有一个集成,可让您直接连接到您的 Etsy 商店。


然后,您可以开始创建不同的物品并在Etsy上出售它们。


这是一个如此有利可图且可扩展的商业模式的原因是,Etsy为您带来了所有流量,而不必推销您的任何东西。


您需要做的就是点击这些正确的关键字,然后您的东西将被营销给正在寻找这种商品的合适人。


7.AI 个人助理


现在,下一个可能是最自然的用例,目前每个人都有一个AI通用助手,无论是Alexa还是Siri,甚至是Bixby。诀窍将是进入一个真正薄片的市场。


我的意思是,你真的不希望Siri给你健身建议。您需要为该利基市场训练专门的语言模型,并提供干净的用户界面供客户使用。您可以训练您的 AI 模型以专注于健身旅行或金融并提供指导。


潜在的目标受众将是寻求数字助理来帮助他们管理日常任务的个人和企业。


8.AI 自媒体助手


正如我们刚刚了解到的,我们可以训练人工智能模型来实际模拟一个真实的人,就像我们在上一个想法中对格雷厄姆·斯特凡(Graham Stefan)建模一样。现在,您为AI模型提供的输入越多,它就会变得越准确,越智能。


考虑到这一点内容创作者要回应他们的粉丝是一项艰巨的任务。通常,如果他们足够大,他们只有两到三秒钟的时间来对每条评论说谢谢。但是,使用由AI模型提供支持的简单Chrome插件,该插件可以学习您的语气,节奏,喜欢说什么以及您通常如何回应其中一条评论。


图片


它可以帮助您生成真正有意义和深思熟虑的回复,您稍后会来编辑或拒绝,甚至只是批准并将它们公开发布到网站上。最好的部分是,它会在你的声音中。


这个应用程序也很棒,你通过批准、拒绝或编辑评论给它的反馈越多,人工智能就会学习,每一条评论都会变得越来越好。


9. 客户服务聊天机器人


客户服务聊天机器人是一种人工智能驱动的聊天机器人,旨在帮助客户完成诸如回答常见问题、提供有关产品或服务的信息以及解决问题等任务。


要使用聊天 GPT 构建用于客户服务的聊天机器人,您需要在客户查询和响应的数据集上训练模型。这将使聊天机器人能够了解客户可能遇到的问题和问题类型,并提供适当的响应。


用于客户服务的聊天机器人的潜在目标受众是希望改善客户服务的各种行业的企业。这可能包括电子商务企业、基于服务的企业和 B2B 公司。


10. 财务规划应用程序


下一个想法是创建一个应用程序,旨在帮助完成预算、储蓄和投资等任务。


例如,您可以通过输入 Graham Stefan 的整个视频和课程目录来训练 ChatGPT 模型,了解他的投资和预算哲学。


然后,人工智能将能够监控您的支出模式、银行账户、投资,并为您提供反馈,说明您应该做什么才能实现您的财务目标。


如果您需要此类应用程序的灵感,可以查看 fylehq.com。


图片


它是一种费用管理软件,它使用 AI 从费用收据中准确提取和编码数据。


11. 健康与保健应用程序


这个 ChatGPT 商业理念就是创建一个健康应用程序,旨在帮助完成跟踪健康指标、提供健康和保健提示以及将用户与医疗保健提供者联系起来等任务。


cass.ai 这种应用程序的一个很好的例子是。


图片


要为这种类型的聊天机器人训练模型,您需要在患者病历、医院记录和体检结果等大数据源上训练它。


12. 娱乐应用


下一个想法是创建一个应用程序,提供有关电影、电视节目、音乐和其他娱乐形式的推荐和信息。


若要为此类应用训练模型,需要大型娱乐信息数据集,例如电影和电视节目评论、音乐推荐以及有关即将发生的事件的信息。


例如,数据集可以包含诸如“年度最佳电影是什么”、“公告牌百强单曲榜上排名前 10 的歌曲是什么”和“百老汇必看的节目是什么”等短语。


13. 数字副本


现在,把最好的想法留给爱,有一个名为 character.ai 的网站,你可以尝试训练一个语言模型来代表世界上的任何人,无论是像埃隆马斯克这样的活着的人,甚至是像本杰明富兰克林这样的死者。


你可以用我实际与之交互的AI模型将它们变成3D增强现实体验。


托尼·罗宾斯(Tony Robbins)一直在幕后工作,这是他毕生工作的巨作。


图片


一种人工智能语言模型,它知道他所知道的一切,并且拥有技术和能力,可以扩展到他可以以托尼罗宾斯私人教练的形式为世界各地的每个人提供他的教学。


14. 转录应用程序


有些网站像 scribie.com,从字面上支付从视频或注册中转录内容的费用,因此需求就在那里。


事实上,如果你浏览Scibie的网站,你会看到许多服务。例如转录会议、演讲、论文等。


图片


因此,您可以考虑创建一个应用程序并使用开放 AI 中的 API 在您的应用程序中使用 whisper 服务。Whisper是OpenAI的另一项服务。


这里涉及更多的工作,但我认为了解如何将 API 集成到应用程序中以及如何构建前端应用程序确实值得了解,因为这个 API 可以提供很多东西。


15.AI 旅游应用


有一些 AI 旅行应用程序可以帮助您完成预订航班和酒店、查找旅行优惠以及为目的地的活动和景点提供建议等任务。


其中一个应用程序是 Emma.ai 它会自动在您的日历中直接为您提供旅行时间和旅程信息,用于客户约会和会议。


图片


该应用程序可让您使用缓冲时间功能和约会预订功能来管理日历。


他们在大型旅行信息数据集上训练模型,例如航班和酒店预订、旅行优惠以及目的地活动和景点的建议。


16. 新闻和信息应用程序


要创建基于 ChatGPT 的新闻和信息应用程序,您需要收集大量新闻文章和其他相关信息的数据集,这些信息涉及各种主题,例如来自信誉良好的新闻来源的文章,以及来自其他来源的信息,例如政府网站、行业报告和专家分析,具体取决于您的利基。


您还需要在此数据集上训练 ChatGPT 模型,对其进行微调,以理解和响应与新闻和信息相关的自然语言查询和命令。


您的模型应该能够理解和回答诸如“印度冠状病毒的最新消息”、“今天股市发生了什么”和“中东当前局势如何”等问题。


为此,我建议从一个较小的利基市场开始,而不是创建一个试图涵盖所有内容的新闻应用程序。


17.AI 流媒体服务


如果你最近一直在关注直播,你可能听说过一个名为Neurosama的AI直播主播,她本质上是一个玩Minecraft和OSU等视频游戏的动漫女孩。


图片


同时,她与聊天互动,破解笑话,并从她使用机器学习技术处理的信息中学习。这令人印象深刻。


现在,事情是这样的。如果你想创建一个类似的AI主播,这可能是非常具有挑战性的。我不知道有任何可供公众使用的特定工具。如果存在这样的工具,我相信每个人都会使用它。


但是,如果你能找到一种方法来编程这样的东西或创建一个类似的工具,你就有可能将这些人工智能流媒体作为一项业务出售。


您可以提供完整的编码来创建整个AI流,在您的服务器上运行它,并以月费将其出售给公司。


对于没有编码知识并希望进入流媒体领域的公司来说,这可能非常诱人。或者,您可以运行 10 到 20 个 AI 流光,或者找出其他方法来制作 AI 内容。


18.AI 网红营销


接下来,您甚至可以创建AI Instagram影响者。事实上,Pacsun背后的面孔是一个甚至不存在的数字影响者,但她在Instagram上拥有近3万粉丝。


图片


想象一下,对于希望聘请模特或影响者来推广其页面的公司来说,使用人工智能影响者是一个双赢的局面。他们不会衰老,不会努力工作,也不会被起诉。


如果你能创造一些这样的人工智能影响者,并将它们推销给公司,你就可以赚到一个沉重的包。


19. 文案服务


销售文案服务是最明显的 Chat GPT 商业理念,但同时也是最具竞争力的理念,因为每个人都可以做到这一点。但它可能仍然有效,因为它适用于全球许多自由职业者,正如您稍后看到的那样。


以下是您可以出售的一些文案服务:


● 博客写作 


● 产品描述 


● 网站文案 


● 销售文案 


● 校对


所有这些服务都是高利润的,人们愿意为你付费。


人类的大脑有局限性,但现在你没有任何局限性,你有一个真正的竞争优势,这要归功于这种人工智能。


您可以在自由职业者网站上一次提供多种不同的服务。


图片


因此,让我们以Fiverr上的博客写作为例。


因此,如您所见,这些人已经通过销售这项服务赚了很多钱。


但同样,关键是要找到一个特定的利基市场,因为它几乎与所有这些聊天 GPT 商业理念一样。


20. 编码服务


正如您刚刚看到的那样,ChatGPT 为应用程序或谷歌浏览器扩展程序创建代码,以便它可以创建代码并更正它。


编码的需求量非常高,企业和企业家愿意向任何能够提供这项服务并做好这项工作的人支付巨额资金。


现在你有了另一个不公平的优势,你有ChatGPT为你做所有的工作。


这只


是关于利用自由职业者网站并在那里宣传自己作为专业编码员。


给它一些时间将一些客户聚集在一起,使用聊天 GPT 制作一些高质量的工作,然后从那里进行扩展和构建。


GPT商业理念-最后的话


这些是您可以尝试的最佳聊天GPT商业理念。


人工智能驱动的企业正在激增,企业家利用这项技术的机会是巨大的。


ChatGPT 有潜力彻底改变现有行业,创造新行业,并为消费者提供高度个性化的服务。


如果您想创业,请考虑将 ChatGPT 纳入您的业务模式并以创新的方式应用它。随着技术的不断发展,人工智能在不同行业和市场的潜在应用也将如此。


ONE MORE THING


咪豆AI圈(Meedo)针对当前人工智能领域行业入门成本较高、碎片化信息严重、资源链接不足等痛点问题,致力于打造人工智能领域的全资源、深内容、广链接三位一体的在线科研社区平台,提供AI导航网、AI版知乎,AI知识树和AI圈子等服务,欢迎AI未来儿一起来探索(http://www.meedo.top/)


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

有些东西你要是早点懂,也不至于走那么多冤枉路

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。 在大学二年级下学期之前,我是处于一种“无头苍蝇”的...
继续阅读 »

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。


在大学二年级下学期之前,我是处于一种“无头苍蝇”的状态,并不是说自己自甘堕落,破罐子破摔,不是的,相反,我是渴求改变自己的,想学东西的,但是,对于我这种普通本科的学生,虽然学校图书馆有你看不完的书,但是,你总得知道你该看什么书,什么样的对你有帮助,知识的海洋是没有边际的,但是一个人的精力是有限的,当把过多的时间花费在一些对自己没有成长,但是自己却在自嗨的事情上的时候是很可怕的。


就拿读书这件事来说,那时候因为我是“无头苍蝇”,所以就“病急乱投医”,总觉得要去看一点书来充实自己,于是我就看了一些历史,三皇五帝,春秋战国,秦汉三国南北朝都去看了,后面又去看了王阳明,曾国藩,后面是越看越觉得不行,我是学软件工程的,怎么研究起历史来了,然后又去寻求内心的安慰,又去听电台,我记得那时候我最爱听的就是《饮鸩不止渴》和《十点读书》,然后里面的一些鸡汤就把自己灌饱了,就觉得“未来可期”,其实后来才发现是“未来可欺”,不过其实对于像我这样的人,整个社会太多太多,他们想改变自己,想未来有一份不错的事业,他们有梦想,有激情,但是,他们却不知道怎么做,他们的父母不懂这个专业,他们的身边也没啥人懂这个专业,他们从小没见过大的世界,所以导致他们“浪费”了很多时光。


当然,读书是一件十分好的事,听电台也很好,但是,在生命的每一个阶段,你自己应该把时间主要花费在什么上面,这是一个很有智慧的问题,读历史,读人物传记能够让我们有更多的思考空间,有宽广的胸怀,让人遇事从容淡定,因为随着时间的推移,枭雄豪杰不过是一堆白骨,但是眼下我们依旧在生活着,是避不开生计,避不开七情六欲的,所以在头顶星辰大海的同时也要看好眼前的路,在自己没有方向的时候,就看别人怎么做的,如果没有目标参考系,那就做自己专业该做的,然后极力去了解相关资讯和技术,这样即使种不出南瓜,但是也绝对不会少了豆子,最主要的是,这个过程它会去锻炼人,提高自己的思考能力。


不过在自己有了目标以后,怎么去实现这个目标更是一个问题,如果没有条理,没有规划,没有结合社会情况,那么,努力就会显得无力苍白,学生时代的时候,说白了,对于普通本科的学校,大家都差不多,自然没有人有更高的论调,所以都沉迷于表面的浮华,而不去关注其核心原理,所以在学习技术的时候,也会显得毛毛躁躁,不去认真专研,而是沉迷于“多”,“炫”上面,其实着是不对的,不论是计算机专业还是其他专业,知识框架是很多的,但是大多数人少了一种刨根问底的精神,当然,刨根问底也并不一定是一种值得称赞的精神,但是在学生时代如果有刨根问底的精神,那么事必成,因为学生时代正是种子发芽的时候,但是进入社会以后,刨根问底未必就是好事了,它和我们的职业规划,个人性格等有很大的关联,比如你觉得你不可能成为技术专家的条件,你的优势也不在哪里,你还去刨根问底技术,那么这不是明智的选择,但是如果暂时还在一个过渡期,很多东西还不确定,那么是有必要去刨根问底的,而且刨得越深越好。


个人定位无论在那个阶段都是很重要的,它是一种判断力,更是一种智慧,你让韩信去管钱粮他肯定不如萧何,你让萧何去管兵马打仗,他肯定不如韩信,所以,没有那个方向是做好的,没有那个岗位是最值得深耕的,这完全要根据自己的情况来,如果你是一个技术控,对技术有无尽的热爱,加上脑子转得也快,那么,从事技术发展肯定是一个很不错的选择,但是如果对技术不敏感,别人一遍就会,而我十遍都不会,那么可能我真的不适合,但是我对商业有洞察力,对客户有一套打法,那么就没必要去技术哪里死磕,虽然你能花11个小时弄懂,但是硬生生花11小时去弄,因为人最宝贵的就是时间,这样就显得不理智,因为其他它从某种意义上已经证明你确实不适合干这个。


主要就说这些了,虽然都是能懂的道理,但是还是要时刻记录下来,给自己看的同时也希望对别人有启示,往往很多时候是环境和认知限制了我们,所以这时候思考和学习就是最好的解法,在资源不充足的情况下,一定会踩很多坑,滩很多浑水,所以寻求资源是一件十分有必要的事情,多向优秀的人请教,学习,多了解这个社会的运转,不要被关在狭小的信息茧房里。


今天的分享就到这里,感谢你的观看,我们下期见。


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

你是先就业还是先择业?

就业的”就“不是让你将就   是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。   其实某方面来说他们并没有说错,我们...
继续阅读 »

就业的”就“不是让你将就


  是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。


  其实某方面来说他们并没有说错,我们已经成年了,需要独立自主。在漂亮国,到了18岁好像都要分家了吧。不过我们在中国,中国的国情肯定和漂亮国不一样。除此之外中国家庭从小的哭穷式教育,估计让许多孩子都想自己经济独立吧。这个现象导致了大家认为有工作就行了,我管他什么工作呢。


  但是从自身职业发展来讲,这是对自己极其不负责的表现,往往许多人的第一份工作就决定了人生轨迹,不论是以后决定发展的城市,还是以后工作的方向,其实已经早已埋下种子。你说你可以换工作啊,可以跳槽啊,现实往往会打醒你,你以为你没了应届生身份,凭着你那不到一年的工作经验,人家企业看中你什么。所以我们要就业要为自己,同时也要为自己的未来负责,我们要慎之又慎。所以我们要就业不过的选择自己合适的就业不能盲目就业,家长的思想大部分过时了,停留在了上了大学就有好工作的时代。我们只能参照而不能按部就班,对于老师,大部分是为了提高学校就业率完成指标而已,不必要太大理会,当然和你关系好的老师除外,但是相反如果和你关系好他一定会不会让你草草就业的。


u=1343747016,2016950934&fm=253&fmt=auto&app=138&f=JPEG.webp


择业的”择“也别太择



钱多事少离家近,位高权重责任轻。睡觉睡到自然醒,数钱数到手抽筋。



  说完就业再谈谈择业,相信上面这句话大家都听过,这简直就是梦中情职,所以择业在我看来无非四种:离家近的、工资高的、自己感兴趣的、清闲的。这四种涵盖了大部分职业了吧。所以我们怎么择业,选择一个适合自己的职位对于未来发展是事半功倍的。


  大家选择职业的时候不知道是从哪方面来选择的,首先离家近,相信很多女生都是考虑这个优先吧,感觉男生就是喜欢仗剑走天涯那种🤣。然后考虑清闲的,想想你二十几岁的年龄你还要工作四五十年可能,选个清闲一点的职业不过分吧,最好就是考一个公务员事业编了,实在不行就去央企国企了,当然这种工作大家都想去,虽然工资不高但是福利好啊。再者就是兴趣了,把自己的兴趣培养成自己的职业也是可以的,大学就是很好的时间,选那种课比较少的专业,这里不得不再次吐槽大学课程的无用性。然后自己选一个自己喜欢的职业,比如摄影、博主什么的。不过当喜欢的事变成职业很多人也就不喜欢了,比如电竞职业选手他们天天十几个小时训练打游戏,他们下班还会想打游戏嘛🤣。就是坚持很重要。再再者,有的人说自己天生无感对什么都没兴趣,那么恭喜你和我一样🤣,就是什么的不是很感兴趣,也不讨厌,那么我建议搞钱,选个高薪的职业搞到足够的钱就退休了,当初就是看程序员薪资高入行了,对钱总感兴趣了吧。总而言之择业择业选择自己合适的再就业。


  鱼和熊掌不可兼得。选择离家近的就得忍受小镇的慢节奏,没有快速的地铁,没有好玩的游乐场,有的只是街坊邻居的互相寒暄,没有夜晚的灯红酒绿,只有晚上八九点就安静的大街。选择清闲的公务员,那么就要懂的人情世故,还有每个月几千块钱的工资。选择自己感兴趣的,那么就得忍受孤独,经得起自我怀疑要有坚定的勇气。高工资的不用多说了吧,996,007时间就是金钱,加班是常态,通宵也是偶尔。所以没有哪份职业好坏,选择自己合适的,加油奋斗吧!


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

使用 Kotlin 委托,拆分比较复杂的 ViewModel

需求背景 在实际的开发场景中,一个页面的数据,可能是由多个业务的数据来组成的。 使用 MVVM 架构进行实现,在 ViewModel 中存放和处理多个业务的数据,通知 View 层刷新 UI。 传统实现 比如上面的例子,页面由3 个模块数据构成。 我们可...
继续阅读 »

需求背景




  1. 在实际的开发场景中,一个页面的数据,可能是由多个业务的数据来组成的。

  2. 使用 MVVM 架构进行实现,在 ViewModel 中存放和处理多个业务的数据,通知 View 层刷新 UI。


传统实现


比如上面的例子,页面由3 个模块数据构成。


我们可以创建一个 ViewModel ,以及 3个 LiveData 来驱动刷新对应的 UI 。


    class HomeViewModel() : ViewModel() {

private val _newsViewState = MutableLiveData<String>()
val newsViewState: LiveData<String>
get() = _newsViewState

private val _weatherState = MutableLiveData<String>()
val weatherState: LiveData<String>
get() = _weatherState

private val _imageOfTheDayState = MutableLiveData<String>()
val imageOfTheDayState: LiveData<String>
get() = _imageOfTheDayState

fun getNews(){}
fun getWeather(){}
fun getImage(){}

}

这样的实现会有个缺点,就是随着业务的迭代,页面的逻辑变得复杂,这里的 ViewModel 类代码会变复杂,变得臃肿。


这个时候,就可能需要考虑进行拆分 ViewModel


一种实现方法,就是直接简单地拆分为3个 ViewModel,每个 ViewModel 处理对应的业务。但是这样会带来其他问题,就是在 View 层使用的时候,要判断当前是什么业务,然后再去获取对应的ViewModel,使用起来会比较麻烦。


优化实现


目标:



  • 将 ViewModel 拆分成多个子 ViewModel,每个子 ViewModel 只关注处理自身的业务逻辑

  • 尽量考虑代码的可维护性、可扩展性


Kotlin 委托



  • 委托(Delegate)是 Kotlin 的一种语言特性,用于更加优雅地实现代理模式

  • 本质上就是使用了 by 语法后,编译器会帮忙生成相关代码。

  • 类委托: 一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。

  • 基础类和被委托类都实现同一个接口,编译时生成的字节码中,继承自 Base 接口的方法都会委托给BaseImpl 处理。


// 基础接口
interface Base {
fun print()
}

// 基础对象
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

// 被委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 最终调用了 Base#print()
}

具体实现


定义子 ViewModel 的接口,以及对应的实现类


    interface NewsViewModel {
companion object {
fun create(): NewsViewModel = NewsViewModelImpl()
}

val newsViewState: LiveData<String>

fun getNews()
}

interface WeatherViewModel {
companion object {
fun create(): WeatherViewModel = WeatherViewModelImpl()
}

val weatherState: LiveData<String>

fun getWeather()
}

interface ImageOfTheDayStateViewModel {
companion object {
fun create(): ImageOfTheDayStateViewModel = ImageOfTheDayStateImpl()
}

val imageState: LiveData<String>

fun getImage()
}

class NewsViewModelImpl : NewsViewModel, ViewModel() {
override val newsViewState = MutableLiveData<String>()

override fun getNews() {
newsViewState.postValue("测试")
}
}

class WeatherViewModelImpl : WeatherViewModel, ViewModel() {
override val weatherState = MutableLiveData<String>()

override fun getWeather() {
weatherState.postValue("测试")
}
}

class ImageOfTheDayStateImpl : ImageOfTheDayStateViewModel, ViewModel() {
override val imageState = MutableLiveData<String>()

override fun getImage() {
imageState.postValue("测试")
}
}


  • 把一个大模块,划分成若干个小的业务模块,由对应的 ViewModel 来进行处理,彼此之间尽量保持独立。

  • 定义接口类,提供需要对外暴漏的字段和方法

  • 定义接口实现类,内部负责实现 ViewModel 的业务细节,修改对应字段值,实现相应方法。

  • 这种实现方式,就不需要像上面的例子一样,每次都要多声明一个带划线的私有变量。并且可以对外隐藏更多 ViewModel 的实现细节,封装性更好


组合 ViewModel


image.png


    interface HomeViewModel : NewsViewModel, WeatherViewModel, ImageOfTheDayStateViewModel {
companion object {
fun create(activity: FragmentActivity): HomeViewModel {
return ViewModelProviders.of(activity, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass == HomeViewModelImpl::class.java) {
@Suppress("UNCHECKED_CAST")

val newsViewModel = NewsViewModel.create()
val weatherViewModel = WeatherViewModel.create()
val imageOfTheDayStateImpl = ImageOfTheDayStateViewModel.create()

HomeViewModelImpl(
newsViewModel,
weatherViewModel,
imageOfTheDayStateImpl
) as T
} else {
modelClass.newInstance()
}

}
}).get(HomeViewModelImpl::class.java)
}
}
}

class HomeViewModelImpl(
private val newsViewModel: NewsViewModel,
private val weatherViewModel: WeatherViewModel,
private val imageOfTheDayState: ImageOfTheDayStateViewModel
) : ViewModel(),
HomeViewModel,
NewsViewModel by newsViewModel,
WeatherViewModel by weatherViewModel,
ImageOfTheDayStateViewModel by imageOfTheDayState {

val subViewModels = listOf(newsViewModel, weatherViewModel, imageOfTheDayState)

override fun onCleared() {
subViewModels.filterIsInstance(BaseViewModel::class.java)
.forEach { it.onCleared() }
super.onCleared()
}
}


  • 定义接口类 HomeViewModel,继承了多个子 ViewModel 的接口

  • 定义实现类 HomeViewModelImpl,组合多个子 ViewModel,并通过 Kotlin 类委托的形式,把对应的接口交给相应的实现类来处理

  • 通过这种方式,可以把对应模块的业务逻辑,拆分到对应的子 ViewModel 中进行处理

  • 如果后续需要新增一个新业务数据,只需新增相应的子模块对应的 ViewModel,而无需修改其他子模块对应的 ViewModel。

  • 自定义 ViewModelFactory,提供 create 的静态方法,用于外部获取和创建 HomeViewModel。


使用方式



  • 对于 View 层来说,只需要获取 HomeViewModel 就行了。

  • 调用暴露的方法,最后会委托给对应子 ViewModel 实现类进行处理。


        val viewModel = HomeViewModel.create(this)

viewModel.getNews()
viewModel.getWeather()
viewModel.getImage()

viewModel.newsViewState.observe(this) {

}
viewModel.weatherState.observe(this) {

}
viewModel.imageState.observe(this) {

}

扩展



  • 上面的例子,HomeViewModel 下面,可以由若干个子 ViewMdeol 构成。

  • 随着业务拓展,NewsViewModel、WeatherViewModel、ImageOfTheDayStateViewModel,也可能是分别由若干个子 ViewModel 构成。那也可以参照上面的方式,进行实现,最后会形成一棵”ViewModel 树“,各个节点的 ViewModel 负责处理对应的业务逻辑。


image.png


总结


这里只是提供一种拆分 ViewModel 的思路,在项目中进行应用的话,可以根据需要进行改造。


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

【自定义 View】一个易用且好看的阴影控件

前言 在 Android 的界面绘制中,控件的阴影是我们经常会处理的一种界面元素,尤其会出现在按钮 Button 这种需要吸引用户关注点的控件上。Android 原生提供了控件的 Z 轴属性即 elevetion 供阴影效果,但是这个效果嘛,但凡是有一点想法的...
继续阅读 »

前言


Android 的界面绘制中,控件的阴影是我们经常会处理的一种界面元素,尤其会出现在按钮 Button 这种需要吸引用户关注点的控件上。Android 原生提供了控件的 Z 轴属性即 elevetion 供阴影效果,但是这个效果嘛,但凡是有一点想法的 UI 都不会满意的,比如我司的,就坚决不接受。


常见的问题比如不支持特定的阴影形状或大小,或不允许完全自定义阴影的颜色或透明度,切图是一种方式,但是自定义 View 绘制的效果会更好,毕竟切图会实实在在的造成 apk 包体积的增大,而且屏幕适配也会是一个潜藏的问题隐患。



结合我的经验,简单封装了一下,分享我目前使用的 ShadowView


使用


圆角矩形阴影




  1. 普通阴影


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.randalldev.shadowview.ShadowView
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="@id/btn_target"
    app:layout_constraintEnd_toEndOf="@id/btn_target"
    app:layout_constraintStart_toStartOf="@id/btn_target"
    app:layout_constraintTop_toTopOf="@id/btn_target"
    app:shadowBottomHeight="16dp"
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowLeftHeight="16dp"
    app:shadowRadius="16dp"
    app:shadowRightHeight="16dp"
    app:shadowRound="8dp"
    app:shadowTopHeight="16dp" />

    <Button
    android:id="@+id/btn_target"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent"
    android:paddingStart="40dp"
    android:paddingEnd="40dp"
    android:paddingTop="20dp"
    android:paddingBottom="20dp"
    android:text="target button"
    android:textColor="@color/purple_700"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png


    抛开配色不谈,这个效果还可以吧




  2. 普通阴影 + 偏移


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowLeftHeight="16dp"
    app:shadowOffsetX="8dp"
    app:shadowOffsetY="4dp"
    app:shadowRadius="16dp"
    ···
    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png




圆形阴影


圆形阴影也可以认为是一种特殊的圆角矩形阴影,可以继续沿用圆角矩形的方式,或者添加 shadowShape 属性。


如果要使用圆角矩形的方式,需要事先确定目标控件的尺寸,这可能会遇到屏幕适配问题,所以我这里就直接演示使用 shadowShape 属性的方式




  1. 普通阴影


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowRadius="16dp"
    app:shadowShape="1" />

    <Button
    android:id="@+id/btn_target"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:background="@android:color/transparent"
    android:padding="20dp"
    android:text="target button"
    android:textColor="@color/purple_700"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintDimensionRatio="1:1"
    ···

    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png


    很简单吧,相比圆角矩形的配置,多了一个 shadowShape 但是少了很多尺寸的设置,只需要设置一个 shadowRaduis 即可。


    需要注意的是,我这里使用了 ConstrainLayoutratio 属性设置为 1:1 来实现一个正方形的目标控件,因为在绘制圆形时,是以控件的中心作为圆心来绘制的,如果不是正方形就可能出现问题了。




  2. 普通阴影 + 偏移


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowRadius="16dp"
    app:shadowOffsetX="4dp"
    app:shadowOffsetY="4dp"
    app:shadowShape="1" />

    ···
    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png





这个使用起来还是比较方便的吧,只需要目标控件设置 padding 留出足够的空间绘制阴影效果即可。


并且不需要再写 drawable 文件设置控件的背景了。


当然也不是没有缺陷,目前还是只能兼容圆角矩形和圆形。异形的暂时没用到,可能也不会去做支持。



实现


什么是阴影


首先,阴影是什么?


在真实世界中,阴影是物体遮挡住光源的光路出现的现象;在 Android View 体系中则是 Z 轴高度,Z 轴高度越高,阴影范围越大,颜色越深。


但是仅仅通过 elevetion 属性设置 Z 轴高度实现的阴影视效上往往只能说满足有无的问题,毕竟国内谁按照 MD 风格去设计界面啊。


image.png


那么,阴影是什么?


当我们自定义 View 去绘制阴影的时候,其实也可以是一圈从边缘向四周放射式扩散的渐变色层,从而造成一种视觉的阴影效果。


那偏移又是什么?


偏移其实就是表达光源的位置,偏移为 0,即光源在正中心光线直射,阴影效果是从边缘均匀的向四周逐渐变淡。


X 偏移为正,则光源在中心偏右,Y 偏移为正,则光源在中心偏下。 若为负数则相反。视觉上则会出现某一或两轴方向上的阴影区域偏少。


上代码


初始化


这段很简单,就是读取 attrs 属性,设置硬件加速


init {
initView(context, attrs)
//设置软件渲染类型,跟绘制阴影相关,后边会说
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}

绘制阴影


这里创建了一个画笔 Paint 的实例,画笔的颜色是目标控件的背景色;绘制模式设置的是 FILL 表示填充模式,还有 STROKE 描边模式,FILL_AND_STROKE 描边加填充模式;AntiAlias 设置为 true 标识开启抗锯齿。


这里就是使用 PaintsetShadowLayer() 方法创建阴影效果,其中:



  • radius:阴影半径,值越大阴影越模糊,值为0时阴影消失。

  • dx:阴影在水平方向的偏移量,正值表示向右偏移,负值表示向左偏移。

  • dy:阴影在垂直方向的偏移量,正值表示向下偏移,负值表示向上偏移。

  • shadowColor:阴影颜色。


Canvas 可以理解为画布,基于 shadowShape 属性在画布上对应的绘制圆角矩形和圆形两种不同形状。



  • drawRoundRect() 用于在 Canvas 上绘制一个圆角矩形。该方法需要传递四个参数,分别是矩形左上角的 X 坐标,矩形左上角的 Y 坐标,矩形右下角的 X 坐标和矩形右下角的 Y 坐标。此外还需要提供两个额外参数,分别是圆角的 X 半径和 Y 半径。

  • canvas.drawCircle() 用于在 Canvas 上绘制一个圆形。该方法需要传递三个参数,分别是圆心的 X 坐标,圆心的 Y 坐标以及圆的半径。


创建一个 RectF,也就是一个矩形对象,表示一个浮点数精度的矩形。在绘制操作,比如指定绘制区域、裁剪画布等经常会用到。其构造函数包含4个浮点型成员变量:left、top、right、bottom,分别表示矩形左边界、上边界、右边界和下边界的坐标值。


override fun dispatchDraw(canvas: Canvas) {
// 配置画笔
val shadowPaint = Paint()
shadowPaint.color = shadowCardColor
shadowPaint.style = Paint.Style.FILL
shadowPaint.isAntiAlias = true
val left = shadowLeftHeight.toFloat()
val top = shadowTopHeight.toFloat()
val right = (width - shadowRightHeight).toFloat()
val bottom = (height - shadowBottomHeight).toFloat()
// 配置阴影的范围,偏移,颜色
shadowPaint.setShadowLayer(shadowRadius.toFloat(), shadowOffsetX.toFloat(), shadowOffsetY.toFloat(), shadowColor)
if (shadowShape == 0) {
// 如果绘制圆角矩形的阴影,用 drawRoundRect
val rectF = RectF(left, top, right, bottom)
canvas.drawRoundRect(rectF, shadowRound.toFloat(), shadowRound.toFloat(), shadowPaint)
} else {
// 如果绘制圆形的阴影,用 drawCircle
val radius = measuredHeight.toFloat() / 2 - shadowRadius
canvas.drawCircle(measuredHeight.toFloat() / 2, measuredHeight.toFloat() / 2, radius, shadowPaint)
}
shadowPaint.utilReset()
canvas.save()
}

总结


Android 界面绘制中,阴影是常见的 UI 元素之一,而 Android 原生提供的 elevation 属性虽然可以实现阴影效果,但往往不能满足 UI 设计的要求。因此,自定义 View 绘制阴影的方式更为灵活和实用。本文介绍了 ShadowView,它可以方便地绘制圆角矩形和圆形的阴影,且支持颜色、透明度和阴影形状的自定义。此外,本文还提供了使用 ShadowView 绘制阴影的示例代码,可供读者参考和使用。通过使用 ShadowView,可以更加方便地实现复杂、美观的阴影效果,提高 Android 应用的用户体验。


参考文章


Android进阶:快速实现自定义阴影效果


ShadowView


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

写给 Android 开发者的系统基础知识科普

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。 另外广东这两天好冷啊,大家注意保暖~ 虚拟机与运行时 对象的概念 假设 g...
继续阅读 »

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。


另外广东这两天好冷啊,大家注意保暖~



虚拟机与运行时


对象的概念


假设 getObjectAddress(Object) 是一个获取对象内存地址的方法。


第一题:


考虑如下代码:


public static void main(String[] args) {
Object o = new Object();
long address1 = getObjectAddress(o);
// .......
long address2 = getObjectAddress(o);
}

main 方法中,创建了一个 Object 对象,随后两次调用 getObjectAddress 获取该对象的地址。两次获取到的对象地址是否有可能不同?换句话说,对象的地址是否有可能变更?










答:有可能。JVM 中存在 GC 即“垃圾回收”机制,会回收不再使用的对象以腾出内存空间。GC 可能会移动对象。


第二题:


考虑如下代码:


private static long allocate() {
Object o = new Object();
return getObjectAddress(o);
}

public static void main(String[] args) {
long address1 = allocate();
// ......
long address2 = allocate();
}

allocate() 创建了一个 Object 对象,然后获取它的对象地址。
main 方法中调用两次 allocate(),这两个对象的内存地址是否有可能相同?










答:有可能。在 allocate() 方法中创建的对象在该方法返回后便失去所有引用成为“不再需要的对象”,如果两次方法调用之间,第一次方法调用中产生的临时对象被上文中提到的 GC 机制回收,对应的内存空间就变得“空闲”,可以被其他对象占用。


第三题:


哎呀,既然上面说同一个对象的内存地址可能不相同,两个不同对象也有可能有相同的内存地址,而java 里的 == 又是判断对象的内存地址,那么


Object o = new Object();
if (o != o)

还有


Object o1 = new Object();
Object o2 = new Object();
if (o1 == o2)

这里的两个 if 不是都有可能成立?










答:不可能。== 操作符比较的确实是对象地址没错,但是这里其实还隐含了两个条件:



  1. 这个操作符比较的是 “那一刻” 两个对象的地址。

  2. 比较的两个对象都位于同一个进程内。


上述提到的两种情况都不满足“同一时间”这一条件,因此这两条 if 永远不会成立。


类与方法


第四题:


假设 Framework 是 Android Framework 里的一个类,App 是某个 Android App 的一个类:


public class Framework {
public static int api() {
return 0;
}
}

public class App {
public static void main(String[] args) {
Framework.api();
}
}

编译 App,然后将 Frameworkapi 方法的返回值类型从 int 改为 long,编译 Framework 但不重新编译 App,App 是否可以正常调用 Framework 的 api 方法?










答:不能。Java 类内存储的被调用方法的信息里包含返回值类型,如果返回值类型不对在运行时就找不到对应方法。将方法改为成员变量然后修改该变量的类型也同理。


第五题:


考虑如下代码:


class Parent {
public void call() {
privateMethod();
}
private void privateMethod() {
System.out.println("Parent method called");
}
}

class Child extends Parent {
private void privateMethod() {
System.out.println("Child method called");
}
}

new Child().call();

Child 里的 privateMethod 是否重写了 Parent 里的?call 中调用的 privateMethod() 会调用到 Parent 里的还是 Child 里的?










答:不构成方法重写,还是会调用到 Parent 里的 privateMethod。private 方法是 direct 方法,direct 方法无法被重写。


操作系统基础


多进程与虚拟内存


假设有进程 A 和进程 B。


第六题:


进程 A 里的对象 a 和进程 B 里的对象 b 拥有相同的内存地址,它们是同一个对象吗?










答:当然不是,上面才说过“对象相等”这个概念在同一个进程里才有意义,不认真听课思考是会被打屁屁的~


第七题:


进程 A 内有一个对象 a 并将这个对象的内存地址传递给了 B,B 是否可以直接访问(读取、写入等操作)这个对象?










答:不能,大概率会触发段错误,小概率会修改到自己内存空间里某个冤种对象的数据,无论如何都不会影响到进程 A。作为在用户空间运行的进程,它们拿到的所谓内存地址全部都是虚拟地址,进程访问这些地址的时候会先经过一个转换过程转化为物理地址再操作。如果转换出错(人家根本不认识你给的这个地址,或者对应内存的权限不让你执行对应操作),就会触发段错误。


第八题:


还是我们可爱的进程 A 和 B,但是这次 B 是 A 的子进程,即 A 调用 fork 产生了 B 这个新的进程:


void a() {
int* p = malloc(sizeof(int));
*p = 1;
if (fork() > 0) {
// 进程 A 也即父进程
// 巴拉巴拉巴拉一堆操作
} else {
// 进程 B 也即子进程
*p = 2;
}
}

(fork 是 Posix 内创建进程的 API,调用完成后如果仍然在父进程则返回子进程的 pid 永远大于 0,在子进程则返回 0)


(还是理解不了就把 A 想象为 Zygote 进程,B 想象为任意 App 进程)


这一段代码分配了一段内存,调用 fork 产生了一个子进程,然后在子进程里将预先分配好的那段内存里的值更改为 2。
问:进程 B 做出的更改是否对进程 A 可见?










答:不可见,进程 A 看见的那一段内存的值依然是 1。Linux 内核有一个叫做“写时复制”(Copy On Write)的技术,在进程 B 尝试写入这一段内存的时候会偷偷把真实的内存给复制一份,最后写入的是这份拷贝里的值,而进程 A 看见的还是原来的值。


跨进程大数据传递


已知进程 A 和进程 B,进程 A 暴露出一个 AIDL 接口,现在进程 B 要从 A 获取 10M 的数据(远远超出 binder 数据大小限制),且禁止传递文件路径,只允许调用这个 AIDL 接口一次,请问如何实现?










答:可以传递文件描述符(File Descriptor)。别以为这个玩意只能表示文件!举个例子,作为应用层开发者我们可以使用共享内存的方法,这样编写 AIDL 实现类把数据传递出去:


@Override public SharedMemory getData() throws RemoteException {
int size = 10 * 1024 * 1024;
try {
SharedMemory sharedMemory = SharedMemory.create("shared memory", size);
ByteBuffer buffer = sharedMemory.mapReadWrite();
for (int i = 0;i < 10;i++) {
// 模拟产生一堆数据
buffer.put(i * 1024 * 1024, (byte) 114);
buffer.put(i * 1024 * 1024 + 1, (byte) 51);
buffer.put(i * 1024 * 1024 + 2, (byte) 4);
buffer.put(i * 1024 * 1024 + 3, (byte) 191);
buffer.put(i * 1024 * 1024 + 4, (byte) 98);
buffer.put(i * 1024 * 1024 + 5, (byte) 108);
buffer.put(i * 1024 * 1024 + 6, (byte) 93);
}
SharedMemory.unmap(buffer);
sharedMemory.setProtect(OsConstants.PROT_READ);
return sharedMemory;
} catch (ErrnoException e) {
throw new RemoteException("remote create shared memory failed: " + e.getMessage());
}
}

然后在进程 B 里这样拿:


IRemoteService service = IRemoteService.Stub.asInterface(binder);
try {
SharedMemory sharedMemory = service.getData();
ByteBuffer buffer = sharedMemory.mapReadOnly();

// 模拟处理数据
int[] temp = new int[10];
for (int i = 0;i < 10;i++) {
for (int j = 0;j < 10;j++) {
temp[j] = buffer.get(i * 1024 * 1024 + j);
}
Log.e(TAG, "Large buffer[" + i + "]=" + Arrays.toString(temp));
}
SharedMemory.unmap(buffer);
sharedMemory.close();
} catch (Exception e) {
throw new RuntimeException(e);
}

这里使用的 SharedMemory 从 Android 8.1 开始可用,在 8.1 之前的系统里也有一个叫做 MemoryFile 的 API 可以用。
打开 SharedMemory 里的源码,你会发现其实它内部就是创建了一块 ashmem (匿名共享内存),然后将对应的文件描述符传递给 binder。内核会负责将一个可用的文件描述符传递给目标进程。
你可以将它理解为可以跨进程传递的 File Stream(只要能通过权限检查),合理利用这个小玩意有奇效哦 :)


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

一个简单的自定义输入框

Hello啊各位老铁,今天还是一篇关于自定义View相关的,带来一个大众的,常见的一个输入框,很多的场合下都能遇到,比如验证码,密码框等等,配置了很多常见的属性,可以满足不同场合下的需求,矩形框,圆角框,下划线等等均可满足,长度设置,光标选择,背景选择,均可控...
继续阅读 »

Hello啊各位老铁,今天还是一篇关于自定义View相关的,带来一个大众的,常见的一个输入框,很多的场合下都能遇到,比如验证码,密码框等等,配置了很多常见的属性,可以满足不同场合下的需求,矩形框,圆角框,下划线等等均可满足,长度设置,光标选择,背景选择,均可控制,废话不多数,我们直接进入正题。


今天的内容大致如下:


1、效果及代码具体调用。


2、具体实现过程。


3、开源地址。


4、总结及注意事项。


一、效果及代码具体调用。


效果展示


边框黑圆圈展示



边框文字展示



纯色背景文字展示



纯色背景黑圆圈展示



纯色背景星号展示



下划线文字展示



下划线黑圆圈展示



纯色背景横向光标展示



能实现的效果还有很多,大家可以根据属性来动态配置即可。


关于使用方式,大家可以下载源码,直接复制即可,毕竟只有一个类,如果懒得下载源码,使用我给大家准备的远程Maven也是可以的,也是非常的方便,远程Maven具体使用如下。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。


allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


dependencies {
implementation 'com.vip:edit:1.0.3'
}

代码使用


   <com.vip.edit.InputBoxView
android:id="@+id/ib_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:layout_marginRight="10dp"
app:input_background="#f5f5f5"
app:input_canvas_type="rect"
app:input_length="6"
app:input_text_size="16sp"
app:input_text_type="round"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />


属性介绍











































































































属性类型概述
input_canvas_typeenum绘制类型,目前有三种,线(line),矩形(rect),圆角(round),圆角需要结合属性input_radius使用
input_canvas_styleenum绘制画笔类型,空心还是实心,空心(stroke),实心(fill),实心和空心(fill_and_stroke)
input_backgroundcolor输入框的背景
input_select_backgroundcolor输入框的选择背景
input_radiusdimension输入框圆角度数
input_line_heightdimension输入框下划线的高度
input_lengthinteger输入框的长度
input_spacingdimension输入框的间距
input_text_colorcolor输入框的内容颜色
input_text_sizedimension输入框的文字大小
input_text_typeenum输入框的文字类型,普通文字(text),星号(asterisk),黑圆圈(round)
input_is_cursorboolean输入框是否有光标,默认展示光标
input_cursor_directionboolean输入框光标方向
input_cursor_widthdimension输入框光标宽度
input_cursor_colorcolor输入框光标颜色
input_cursor_spacingcolor输入框光标间距
input_cursor_is_twinkleboolean输入框的光标是否闪烁
input_is_android_keyboardboolean输入框是否弹起原生的软件盘,默认谈起,可以调用自定义的键盘
input_cursor_margin_bottomdimension横向的光标距离底部的距离

方法介绍










































方法参数概述
clearContent无参清空内容
setContentString设置内容
hideInputMethod无参隐藏软键盘,使用系统软键盘时
showKeyBoard回调函数需要弹起自己的软键盘时可以调用
inputChangeContent回调函数获取连续的输入结果
inputEndResult回调函数获取最终的输入内容,当等于你设置的length时进行回调

二、具体实现过程。


实现的过程也是非常的简单,大致可以分为五步走,1、绘制方格或下划线,2、绘制内容,3、绘制光标,4、实现光标闪动,5、软键盘控制,来,我们一步一步的来实现。


1、绘制方格或下划线


绘制方格或下划线,如下草图所示,需要根据传递的数量来进行绘制,首先要计算出每一格的宽度,也就是屏幕的宽度-格子之间的边距/格子的数量。



  //每个输入框的宽度=屏幕的宽-左右的边距-输入框直接的间距/输入框的个数
mRectWidth = (width - mSpacing * (mLength - 1)) / mLength

得到了每一格的宽度之后,就可以根据数量来进行动态的绘制了,无非就是遍历,根据属性input_canvas_type来绘制不同的效果,mSelectBackGroundColor变量为属性input_select_background设置的值,用来标记选择的输入框颜色,如下图所示:



如果,你想要改变选中的格子边框颜色,就可以进行设置颜色值,同样的需要搭配画笔的绘制样式,如下代码所示:


 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制输入框
*/
private fun canvasInputBox(canvas: Canvas?) {
mPaint!!.apply {
color = mBackGroundColor//设置背景颜色
strokeCap = Paint.Cap.ROUND//圆角线
}


for (a in 0 until mLength) {

val textLength = text.toString().length//当前输入的长度

if (mSelectBackGroundColor != 0) {
var paintStyle = Paint.Style.STROKE
when (mInputCanvasStyle) {
0 -> {
paintStyle = Paint.Style.STROKE
}
1 -> {
paintStyle = Paint.Style.FILL
}
2 -> {
paintStyle = Paint.Style.FILL_AND_STROKE
}
}
if (a == textLength) {
mPaint!!.apply {
style = paintStyle
color = mSelectBackGroundColor//设置选中背景颜色
}
} else {
mPaint!!.apply {
style = paintStyle
color = mBackGroundColor//设置背景颜色
}

}
}

val left = a * mRectWidth + a * mSpacing
val top = 0f
val right = (a + 1) * mRectWidth + a * mSpacing
val bottom = height.toFloat()

when (mInputCanvasType) {
0 -> {
//绘制下划线
canvas?.drawRoundRect(
left,
bottom - mLineHeight,
right,
bottom,
mRadius,
mRadius,
mPaint!!
)
}
1 -> {
//绘制矩形
canvas?.drawRect(left, top, right, bottom, mPaint!!)
}
2 -> {
//绘制圆角矩形
canvas?.drawRoundRect(left, top, right, bottom, mRadius, mRadius, mPaint!!)
}
}
}
}

绘制格子,最重要的就是计算每个格子的位置,其实只需要考虑X的坐标即可,Y可以直接充满View的高度。


2、绘制内容


绘制输入的内容,和绘制格子一样,重要的就是计算位置,有了格子的位置之后,计算内容就比较的简单了,只需要获取格子的中间坐标即可,计算如下:首先,拿到每个格子的右边X坐标点,再减去格子宽度的一半,就得到的中间的X坐标,但是,文字的绘制,还需要减去文字宽度的一半,这个一定要注意,否则,文字就是从中间点往右进行绘制的,就偏移了中间点。


文字的X轴计算如下:


  val textX = ((a + 1) * mRectWidth) + a * mSpacing - mRectWidth / 2 - w / 2

同理,Y的计算方式类似,全部代码如下,有一点需要注意下,就是星号,星号和文字以及圆圈还是有不一样的地方,那就比较小,那么就需要特殊的处理一下,都是基础的代码,没什么好说的。


/**
* AUTHOR:AbnerMing
* INTRODUCE:绘制内容
*/
private fun drawText(canvas: Canvas?) {
mPaint!!.apply {
style = Paint.Style.FILL
color = mTextColor//设置内容颜色
textSize = mTextSize
}

if (!TextUtils.isEmpty(text)) {
for (a in text!!.indices) {
val content = text!![a].toString()
var endContent = content

if (mTextType == 1) {
endContent = "*"
} else if (mTextType == 2) {
endContent = "●"
}

val rect = Rect()
mPaint!!.getTextBounds(endContent, 0, content.length, rect)
val w = mPaint!!.measureText(endContent)//获取文字的宽
//获取文字的X坐标
val textX = ((a + 1) * mRectWidth) + a * mSpacing - mRectWidth / 2 - w / 2
val h = rect.height()
//获取文字的Y坐标
var textY = (height + h) / 2.0f
//针对星号做特殊处理
if (mTextType == 1) {
textY += mTextSize / 3
}

canvas?.drawText(endContent, textX, textY, mPaint!!)
}
}
}

3、绘制光标


绘制光标就比较简单了,无非就是纵向还是横向,也是根据设置的属性来控制的,纵向计算出X坐标即可,横向就计算出Y的坐标即可。需要注意的是,距离左右或者上下的间距控制,代码如下:


 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制光标
*/
private fun drawCursor(canvas: Canvas?) {
mCursorPaint!!.apply {
strokeWidth = mCursorWidth
isAntiAlias = true
}
//需要根据当前输入的位置,计算光标的绘制位置
val len = text?.length
if (len!! < mLength) {
if (mCursorDirection) {
//纵向光标
val rectWidth = ((len + 1) * mRectWidth) + len * mSpacing - mRectWidth / 2
canvas?.drawLine(
rectWidth,
mCursorSpacing,
rectWidth,
height - mCursorSpacing,
mCursorPaint!!
)
} else {
val endX = ((len + 1) * mRectWidth) + len * mSpacing
val startX = endX - mRectWidth
//横向光标
canvas?.drawLine(
startX + mCursorSpacing,
height.toFloat() - mCursorMarginBottom,//减去距离底部的边距
endX - mCursorSpacing,
height.toFloat() - mCursorMarginBottom,
mCursorPaint!!
)
}
}

}

4、实现光标闪动


光标闪动,使用了一个属性动画,设置无限循环,然后控制画笔的颜色即可。


    private val cursorAnim: ValueAnimator = ValueAnimator.ofInt(0, 2).apply {
duration = 1000
repeatCount = ValueAnimator.INFINITE//无线循环
repeatMode = ValueAnimator.RESTART//正序
}

在onAttachedToWindow方法里做启动动画操作:mCursorTwinkle为是否需要光标,需要再启动,是通过属性input_cursor_is_twinkle来控制的。


if (mCursorTwinkle) {
//不在运行,开启动画
if (!cursorAnim.isRunning) {
cursorAnim.start()
}
cursorAnim.addUpdateListener {
val v = it.animatedValue as Int
if (v == 0) {
mCursorPaint?.color = Color.TRANSPARENT
} else {
mCursorPaint?.color = mCursorColor
}
postInvalidate()
}
}

同样的,当onDetachedFromWindow方法时,就需要结束。


if (mCursorTwinkle) {
if (cursorAnim.isRunning || cursorAnim.isStarted) {
cursorAnim.end()
}
cursorAnim.removeAllUpdateListeners()
}

判断在文字的输入时进行闪烁,这个是很重要的,也就是闪烁的位置,一定是当前的输入位置,未输入就是第一格闪烁,依次类推,输入完成,就结束闪烁。


override fun onTextChanged(
text: CharSequence?,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
if (!mIsAttachedToWindows) return

//输入框的光标是否闪烁
if (mCursorTwinkle) {
if ((text?.length ?: 0) >= mLength) {
cursorAnim.takeIf { it.isStarted || it.isRunning }?.end()
} else if (!cursorAnim.isRunning) {
cursorAnim.start()
}
}

val endContent = text.toString()
if (endContent.length == mLength) {
//一样的话,进行回调
mEndContentResult?.invoke(endContent)
}

mChangeContent?.invoke(endContent)

}

5、软键盘控制


软件盘控制,有两种方式,一种是弹出系统的软键盘,一种是弹出自定义的软键盘,这个控制也是由传递的属性input_is_android_keyboard来操作的,默认为true,弹出系统的,否则就弹出自定义的,针对自定义的弹出,需要暴露出实现的方法,由使用者进行实现。


 /**
* AUTHOR:AbnerMing
* INTRODUCE:弹起软件盘
*/
private fun showKeyboard() {
if (mIsAndroidKeyBoard) {
isFocusable = true
isFocusableInTouchMode = true
requestFocus()
val im =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
?: return
im.showSoftInput(this, InputMethodManager.SHOW_FORCED)
} else {
//启用自定义的软键盘
if (mKeyBoard != null) {
mKeyBoard?.invoke()
}
}
}

mKeyBoard为弹出自定义软键盘回调函数,代码如下:


    /**
* AUTHOR:AbnerMing
* INTRODUCE:显示自己定义的软件盘
*/
private var mKeyBoard: (() -> Unit?)? = null
fun showKeyBoard(block: () -> Unit) {
mKeyBoard = block
}

隐藏软键盘操作,可以在页面隐藏时进行触发,目前在自定义View中onDetachedFromWindow方法里进行了调用,当然,你可以自己选择性调用。


    /**
* AUTHOR:AbnerMing
* INTRODUCE:隐藏软件盘
*/
fun hideInputMethod() {
if (mIsAndroidKeyBoard) {
val imm: InputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0) //强制隐藏
}
}

三、开源地址。


目前项目已经开源,需要的朋友可以查看:github.com/AbnerMing88…


四、总结及注意事项。


1、触摸输入框,默认是弹出系统自带的软键盘的,如果想使用自定义的,设置属性input_is_android_keyboard为false即可,并调用showKeyBoard回调函数,在showKeyBoard方法里进行书写弹起自定义软键盘即可。


2、如果绘制类型属性input_canvas_type为round,也就是圆角时,需要结合input_radius这个属性,来实现圆角的角度大小。


3、光标的方向属性input_cursor_direction是一个boolean类型的值,默认是纵向的,false是为横向的。


4、当输入框的选择背景input_select_background不为空时,画笔属性input_canvas_style才会生效。


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

职场上有什么谎言?

努力干活就能赚多点钱 职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的...
继续阅读 »

努力干活就能赚多点钱


职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的报酬,而是更倾向于平衡员工的工作与生活,提高员工的幸福感和满意度。因此,新进职场人士应该认识到,在职场中坚持适度的工作量、良好的工作习惯和优秀的职业素养才是取得成功的重要因素。


我都是为你好


“我都是为你好”可能是一种常见的谎言,在不同情境下被使用。在某些情况下,这可能真诚地表达出对他人的关心和照顾,但在其他情况下,这也可能成为掩盖自己私人动机或者行为错误的借口。因此,在职场和日常生活中,我们需要学会审视这句话所蕴含的背后意图,并判断其是否真实可信。同时,我们也应该秉持着开放、坦诚、尊重和理解的态度,与他人进行良好的沟通和相处,以建立健康、和谐的人际关系。


他做得比你好,向他好好学习


“他做得比你好,向他好好学习”是一句非常有益的建议,可以让人们从成功的经验中汲取营养,不断提高自己的能力和水平。在职场中,人们面对不同的工作任务和挑战,而且每个人的工作方式、思维模式和经验都不同,因此,我们应该善于借鉴他人的优点和长处,吸取别人的经验和教训,不断完善自己的职业素养和技能。然而,这并不意味着要完全依赖和模仿别人,而是应该在合适的时机,根据自身实际情况和需要,加以改进和创新,开拓自己的专业视野和发展空间。


在职场中,有些人可能会通过拍马屁、拉关系等不正当手段来获取自己的利益或者提高自己的地位。然而,这种做法可能会导致负面后果和损失,例如破坏工作团队的合作氛围、损害自己的职业形象和信誉等。因此,我们应该始终保持清醒和冷静的头脑,不受拍马屁等诱惑,专注于自己的工作和职责,努力提高自己的专业水平和职业素养。同时,我们也应该与他人建立良好的人际关系,以合理、公正、透明的方式展示自己的才华和成果,赢得别人的尊重和信任,并在适当的时刻借助他人的力量来实现共同的目标。


公司不怎么赚钱,理解一下,行情好了加工资


如果公司在过去设定了一些目标和承诺,但无法兑现或者没有达到预期的结果,那么这就是一种失信行为。画饼充当推销手段,可能会对员工、客户和利益相关方造成误导和不良影响,并破坏公司的商誉和形象。因此,公司应该根据市场实际情况和自身能力水平,制定合理、可行的计划和策略,避免过于浮夸和虚幻的承诺,注重落实和执行,加强与员工、客户和社会各方的沟通和互动,建立坦诚、透明的企业文化和价值观念。同时,员工也应该保持客观、谨慎、理性的态度,不盲目追求高回报或者虚假宣传,始终以个人职业道德和职责为先,为公司和自

作者:象骑士
来源:juejin.cn/post/7213636024102469693
己的未来发展负责任。

收起阅读 »

写给迷茫的 1-3 年前端人的思考

写给迷茫的 1-3 年前端人的思考 前言 今年由于大环境差,加之大家都会遇到的职场困惑期,让很多人觉得很无助,不知道要向何处去努力。 我有两三位朋友正面临类似的困境,向我寻求意见。虽然我不是什么大厂的架构师,也没有多牛,但也有一些自己的思考和实践,希望能帮助到...
继续阅读 »

写给迷茫的 1-3 年前端人的思考


前言


今年由于大环境差,加之大家都会遇到的职场困惑期,让很多人觉得很无助,不知道要向何处去努力。


我有两三位朋友正面临类似的困境,向我寻求意见。虽然我不是什么大厂的架构师,也没有多牛,但也有一些自己的思考和实践,希望能帮助到他们,也希望与大家一起探讨这个话题。


思考


学好英语


原因



  • 获得更广泛的就业机会

  • 拥有更高的技术天花板


获得更广泛的就业机会

1680090655136.png


如果你了解过外国的就业机会,就会发现除了母语为英语的国家,像德国、西班牙、瑞典、荷兰这样的英语为第二语言的国家,只要英语过关,找工作方面也是完全没问题的。


当你学好英语时,就不仅局限于国内这个环境,而是在全世界范围内找工作。此外,国外远程工作机会也不少,在英语不错的情况下,完全可以拿着美元在国内享福。


image.png


更高的技术天花板

我们不得不承认英语在技术领域处于绝对统治地位。如果你的英语水平不够,那么想学习更加新和深入的技术都将面临巨大的困难。我相信每个人都能明白这点。


如何学好英语?


学习英语的资料汗牛充栋,方法也不胜枚举,但知易行难,我个人也在努力中,所以就交给大家自行探索。


找到并深入特定领域


除了日常的页面和组件开发,与前端相关的技术还有许多特定领域,例如:AI、低代码、图形学、数据大屏、serverless、构建、错误监控、行为分析、Web3、编辑器、微前端、服务端、云原生、游戏、组件库、ab测试等等。我个人建议至少找到一个领域深入研究。


原因



  • 形成职业亮点

  • 增加职业壁垒


形成职业亮点

在面试和简历筛选中,面试官最看重的是是否具有亮点。没有亮点的人基本上很难达到高级别,顶多只能达到中级别。而亮点可以从许多方面切入,其中之一就是在某个特定领域有自己的见解和产出,能够为人所不能。


增加职业壁垒

如果你拥有某个领域的知识,那么你不仅是一个切图仔,即使老板想要裁员,他也会掂量是否能够找到更合适的人才。


如何找到自己的领域?


工作

大多数人的专业领域都是从工作中找到的。每个人在职业生涯中都会遇到一些技术难点,此时你可以分析业界各种解决方案的优缺点和实现原理,并思考是否有更好的解决方案或者在社区方案的基础上能否进行更优化、更便捷化,或者更加适合国内市场的特殊需求。


许多人不愿意在工作之外的时间思考与工作相关的问题,但对于自己有利的事情,我认为可以适当花时间深入研究或有意识地增加这部分时间的分配。


主动寻找

如果日常工作就是纯纯的切图仔,那我们就需要主动出击,找到一个自己感兴趣的点并进行深入研究。


例如,如果你想找一份 Web3 相关的工作,那么你现在就应该开始学习 Web3 知识,并将你的学习过程记录下来并发布到各个文章平台,扩大你的影响力。


面向工作和薪资学习


如果你现在的工作只是切图,下班后也没有什么事做,不知道该学习什么,那么这个问题就很简单了。


你可以看一下 BOSS 直聘上前端工程师的工作描述中都需要什么技能以及哪些行业薪资高,什么技能、行业薪资高,你就学什么就对了。当然最好的方法是主动面试尝试毒打。


例如 1:如果你想找远程工作,发现国外许多人使用 tailwindcss,工作描述中也常常要求掌握这个技能,那么你应该毫不犹豫地开始学习 tailwindcss。


例如 2:如果你发现 Web3 相关工作的薪资比普通前端工作高很多,而你又想要高薪,那么你应该立即开始学习 Web3 知识。


关注前端趋势


了解当前前端技术的现状,关注前端技术未来的发展趋势。如果其他几项知易行难,我建议先从这项内容开始,每天花费时间不需要超过 20 分钟即可完成。


如何做到了解目前前端发展的趋势呢?



  • GitHub Trending:每天早晨到公司先打开 GitHub Trending 看看社区中哪些项目正在受到关注(10 分钟)

  • MDH 前端周刊:umijs 作者开设的前端周刊,关注最新的前端发展

  • 两个油管博主

    • t3dotgg:拥有 11 万 YouTube 频道订阅者,基本上当社区一项技术开始火时就会跟进讲解

    • Web Dev Simplified:拥有 120 万 YouTube 频道订阅者,除了讲解前端基础知识外,还会跟进前端的最新发展




当你看到某个东西变得流行起来,你需要思考它为什么会火,之前的技术方案有哪些问题,新的方案怎么解决之前的问题的等等,这样的思考会带领你走向更深的前端知识区。


做开源项目


原因



  • 满足自我实现需求

  • 提高技术水平上限

  • 更好地找工作

  • 更多可能性


满足自我实现需求

当你的开源项目受到关注和讨论时,相当于你被需要、被认可,这是一种很好的正向反馈,会使你身心愉悦。


提高技术水平上限

当你开发出一个新的东西时,代表着你对这个事物有着自己的思考和见解,人与人之间的差异在于思想上。如果社区内有新的反馈,会促使你不断完善和学习相关知识,无形中提升了你的个人能力。


更好地找工作

一个好的开源项目是一个亮点,面试一般都会询问相关问题,此时的主动权是掌握在你手里的。


更多可能性

如果你做过一个成功的开源项目,你就会发现有人开始找你写小册、出课程甚至出版社会找你写书。此外你还可以在文档中有自己的赞助、广告等收入,虽然不多,但是这都代表了一种不同于上班的可能性。


如何开始自己的开源项目?


做开源有两大难点,首先是如何开始一个开源项目,其次是如何让它变得受欢迎。


如何开始一个开源项目,已经在以前的文章里阐述了 juejin.cn/post/719877… ,这里不再赘述。


至于如何让项目受欢迎,所涉及的东西就比较多了,这里就先挖个坑,看是否有人感兴趣,如果有的话,可以再写一篇文章。


基础知识


前端是离端很近的岗位,它所依附的知识变化也很快,昨天还是 jQuery,今天变成了 React、Vue、Webpack、Jest,明天就是 Vite、TurboPack、vitest, SolidJS、Qwik、Astro、Svelte、Remix,后天呢?。


虽然上层的知识变化很快,但是仔细观察就会发现底层的知识却保持不变,比如编程思想、编译原理、算法等。


在这里,我推荐以下三本书作为入门:



  • 《重构——改善既有的代码设计》

  • 《程序员修炼之道》

  • 《黑客与画家》


软技能


除了编程技能外,软技能也很重要。例如这篇 文章 中列举的 8 个能力:



  • 人际交往能力

  • 结构化思维能力

  • 沟通能力

  • 写作能力

  • 自我营销能力

  • 演讲能力

  • 协同/领导能力

  • 抗压能力


如果你可以逐步提高所有这些方面,那当然最好。如果不能全面发展,其中我认为 写作能力 可以先发育,推荐以下内容:



当然关键还是多写多练,毕竟纸上得来终觉浅。


人生方向


编程行业仍然是普通人最好的出路。毕竟没有哪个专业刚毕业的大学生(没背景)就能拿到 7、8 千,甚至脉脉上刚毕业就能拿到 2、30k。


但如果你真的对技术不感兴趣或者做的很痛苦,可以思考是否要继续从事这个行业,是否有其他出路。这是留给你自己的思考题。


后记


实际上,本文已经写完并放置了几周了。随着人工智能井喷式发展、国内经济实行的下行以及各种裁员消息,让我觉得上面的这些努力似乎已没有意义,普通人的命运是否会像《人类简史》中描述的那样,成为无用阶层。


但我又想起一句话:“悲观者正确,乐观者成功”。因此,我还是决定与大家分享这篇文章。


当然,知易行难。如果你在一家 996 公司,建议你骑驴找马,背背八股文,并且在上班前花 10 分钟看一下 GitHub Trending 就行了;


如果是在 965 公司,除了看看 GitHub Trending 建议再至少学一样其他东西。


最后,无论往什么方向努力,希望最重要,希望大家都能怀揣希望,继续前行。


作者:超杰_
来源:juejin.cn/post/7215908160019824698
收起阅读 »

00后的我,大四即将毕业了

前言:   我写这篇文章的初衷主要是记录我自己作为00后面临毕业的总结,算是对大学四年的总结吧,这是第一次写文章总结,也算是00后从小学到大学的学生生涯的总结吧!主要就是写的一名从小听家长的话,做一个乖孩子;听老师的话做一个好学生从小学到大学毕业的感触吧!差不...
继续阅读 »

前言:


  我写这篇文章的初衷主要是记录我自己作为00后面临毕业的总结,算是对大学四年的总结吧,这是第一次写文章总结,也算是00后从小学到大学的学生生涯的总结吧!主要就是写的一名从小听家长的话,做一个乖孩子;听老师的话做一个好学生从小学到大学毕业的感触吧!差不多就是讲的我自己了,哈哈哈哈,我知道自己是这么想的,但我还是想知道大部分00后面临毕业是怎么样的心情🤣


road-3396764__480.jpg


00后还年轻吗?


  00后,听起来很年轻吧,我记得当年上学的时候,国际上有一个词语“千禧一代”,我记得好像当时知道这个词语还是TFBoy火起来了,记得都是未成年然后刚刚出道的时候,以前都还不清楚“千禧一代”一词代表着什么,现在我们00的作为最后一代千禧,也已经面临大学毕业了,我不知道其他地方,我这普遍还是00的同学,大学毕业意味着
进入社会,所以00后其实已经不年轻了。



“千禧一代”是指出生于20世纪时未成年,在跨入21世纪(即2000年)以后达到成年年龄的一代人,这代人的成长时期几乎同时和互联网/计算机科学的形成与高速发展时期相吻合。



大学四年


  大学之大不在大楼,在于大师,对啊大学就是该是大学云集的地方,但是作为大学过来的人,我经历到的大学绝非家长口中的大学,也绝非高中老师口中的大学,这个我不说大家懂得都懂,可能就是这种类似“潜规则”样的教育,我不知道怎么去描述,写着写着又跑题了,我不知道是故意的还是不小心的 🤣


大一


  大一学校安排还是挺多,晚上晚自习,早上晨读晨跑,似乎高三的感觉又回来了,但是那种日子还是比较充实的,因为不需要你思考自己的道路,只需要依照着安排完成感觉就很满足了,大一下学期记得是疫情爆发了,也不能返校在家上网课。在家学习效率其实挺低的,即使我每天还会准时上课但是总是觉得学不进去,后面侥幸没有挂科,相信好多同学的因为在家上网课挂科了吧,第一次报的英语四级因此还被延期了,不过也是裸考,后面侥幸过了😁


大二


  下学期疫情没有那么严峻了,开始返校上课了,我感觉隔了半个学期像是第一次新生来这学校一样,不过已经是大二的老油条了,后面也是自己每个学期选看起来更有趣的课,就像矮个子里挑高个子,我学的软件工程专业,其实所有课都是又难又枯燥的,不仅仅是老师那落后十年的ppt还有那十几年没有改版的课本,考试大家都是临时抱佛脚了,快说你也是🤣


大三


  大三在我印象中是没有什么印象的,因为除了上课还是上课,课超级多,晚饭都没时间不知不觉大三就过了大四的时候在学校的安排下去培训三个月,不知道其他学校有没有这种,交了大四一年的学费(1W),软件工程专业不知道学费贵在哪里。。


  但是从大三结束就没去过学校了,暑假在和学校合作的培训公司培训了3个月,后面直接秋招,这个我觉得是最气人的,我们选择就业的被送去深圳培训公司培训,那些考研考公的就在学校,学校里的校招根本与我们出来的无缘,不管是路途远还是学校把我们宿舍都直接撤了。大三就让把东西寄回家啊,我还以为是读的三年专科(这里不是看不起专科的意思),这不是明显给考研和考公的更多机会了,我们出来就业的只能在网上各种海投
BOSS直拒、前程堪忧、失联招聘、58不成、裂聘……


现在


  我算是运气还好,面试虽然寥寥无几,最后也是拿到两个offer,选择一个相比之下好一点的就签三方了至于班里其他人就只能等了,等到年末,刺骨的不是天气的寒冷,而是就业的寒潮,班里三分之二的人只能空手回家过年,我虽然拿到offer但也决定回家休整几个月,毕竟当时疫情还没有结束呢,毕竟签了三方
就决定明年再去了。


2023我即将毕业了


university-student-1872810__480.jpg
  希望大家能够在大学早点找到学习的意义,不管是为了找工作还是为了充实自己,看到过一个视频内容说的是年龄其实都是虚无的最重要的还是自己的经历,换句话说我们人的最终都是各种经历形成的,和你多大年龄多大岁数是没有关系的,所以说我们需要增加自己的经历,不管是好的还是不好的,都是需要去经历的,最后祝福大家前程似锦!


作者:过了三分之二年河东
来源:juejin.cn/post/7215886869200306237
收起阅读 »

提高你工作效率的10条建议

最近看到一个关于工作效率的问题,这里系统整理下自己总结的一些经验。 有一个跟工作效率有点像的词汇:生产效率。 生产效率指的是单位时间内的有效产出,想要生产效率高,要么做事的“质”和“量”更高,要么缩短所花费的时间。 工作效率和生产效率比较类似,很多都可以借鉴。...
继续阅读 »

最近看到一个关于工作效率的问题,这里系统整理下自己总结的一些经验。


有一个跟工作效率有点像的词汇:生产效率。


生产效率指的是单位时间内的有效产出,想要生产效率高,要么做事的“质”和“量”更高,要么缩短所花费的时间。


工作效率和生产效率比较类似,很多都可以借鉴。


有些工作效率高的,三年经验可以顶别人五年工作经验。


找到自己精力最旺盛的时间段


有人喜欢早自习时候睡觉,有人喜欢晨读。每个人的作息规律不同,可以在自己正常运行一段时间后,找到最佳节奏。在这个精力最旺盛的时间段,更容易进入心流,可以集中精力处理优先级比较高的事情。另外,大段时间尽量不要被大段。


掌握通用技能


掌握基础的电脑办公软件技能、沟通能力、时间管理能力、快速学习一项技能的能力等等。能够使用软件等解决日常工作中遇到的问题,提高工作效率。


如何提高解决问题的能力?


掌握工作必备的基础技能


基础知识扎实的话,就可以避免在一些低级错误上花费很多时间。如果基础不好,而工作任务又比较重,就类似于每天都在考试,但是却没有时间学习新知识,这样学习成绩也无法提升。


单位时间内不断给自己施压


一小时干别人两三个小时干的活,同样的任务,第二次、第三次做的时候就有意识地提高效率。



像训练肌肉一样的训练自己的大脑。同等时间内,从明天起,让自己思考学习双倍的工作量。注意我加粗加重的关键词,不许增加时间,一个小时还是那一个小时,时间不变,内容翻番。


一开始一定有些疲劳感,但只要不生病,那说明你的大脑就能够适应。坚持半个月,习惯它。再加倍。再坚持半个月,习惯它。


一直加倍加倍坚持到你感觉要生病了为止,把速度降下来,降低20%。把这个效率维持终身。当训练成为习惯的时候,你会越来越轻松,越来越惬意。全力以赴的思考也是一样的。


任何一个人,只要你肯,你都能这么去训练自己的工作效率。而当你的效率提升到别人的4倍,8倍。你会发现生活很惬意,不是因为压力变小了,而是因为你习惯了。


——记忆承载《韦小宝绝境》



定期复盘


每周大致回顾下自己本周做了什么,有什么需求改进的。可以自己给自己写周报。


充分利用碎片化时间


可以在上班大致想下今天要做的内容,在下班路上回顾下今天都做了些什么,哪些做得好,哪些还有待改进。


多出妙招不如减少失误


尽量少出岔子,可以避免因为失误而带来的对已有时间的占用。


做最重要的事情


领会领导意图,抓住重点。细枝末节可以在大的事情基本上确认无误的时候再做。


不会就搜


总有些问题是自己措手不及的,不会就搜,不行就换一个搜索引擎,或者换一个关键词。


适当摸鱼


该休息休息会,劳逸结合。休息时间可以整理下文档,换换思路也行,有时候现在百思不得其解的问题,出去溜达一圈回来就豁然开朗了。

作者:江湖人称向前兄
来源:juejin.cn/post/7216671329188937787

收起阅读 »

众人围剿,GPT-5招惹了谁

GPT-4 火爆全球,引发了人工智能大浪潮。过去的一个月,OpenAI、微软、谷歌加上百度不断释放王炸,所有人都相信,AI 的就是未来的生产力。俗话说,人红是非多,树未大已招风,这不,反对 AI 继续前进的声音就来了。 千人呼吁暂停AI训练 3月29日,马斯克...
继续阅读 »



GPT-4 火爆全球,引发了人工智能大浪潮。过去的一个月,OpenAI、微软、谷歌加上百度不断释放王炸,所有人都相信,AI 的就是未来的生产力。俗话说,人红是非多,树未大已招风,这不,反对 AI 继续前进的声音就来了。


千人呼吁暂停AI训练


3月29日,马斯克、苹果联合创始人 Steve Wozniak、Stability AI创始人 Emad Mostaque 等上千名科技大佬和AI专家签署公开信,呼吁暂停训练比 GPT-4 更强大的 AI 系统,为期6个月。


image.png
根据公开信的表示,在这 6 个月内,全社会需要完成这些事:



  • 所有 AI 实验室和独立学者都应该合力开发一套共享安全协议,用于高级 AI 的设计和开发

  • 协议完成后,应该由独立的外部专家进行严格的审计和监督

  • 这些协议必须确保这些 AI 系统毋庸置疑的安全

  • 如果不能迅速暂停,就应该让政府介入。


所有的人工智能研究和开发,都应该重新聚焦于这一点——让当如今最强大的 SOTA 模型更加准确、安全、可解释、透明、稳健、对齐,值得人类信赖,对人类忠诚。


代表人物分析


这次呼吁大佬众多,最具代表性的无疑是马斯克和 Stability AI 创始人 Emad Mostaque。


马斯克是 OpenAI 公司的联合创始人之一,可谓是原始股东,但他在2018年离开了 OpenAI 的董事会。马斯克一直对微软和比尔盖茨持批评意见,对于OpenAI也是如此,此前曾表示“ OpenAI 最初是作为一家开源的非营利性公司而创建的,为了抗衡谷歌,但现在它已经成一家闭源的营利性公司,由微软有效控制……这完全不是我的本意。”


言外之意,OpenAI 不应该成为一个赚钱的公司,应该开源,让所有人看到核心代码和核心算法。如果是这样,应该建议特斯拉免费开源所有自动驾驶技术源码,马斯克对这个问题的回复是“如果其他汽车制造商想要获得授权并在他们的汽车上使用特斯拉的自动驾驶技术,这将是非常酷的一件事情,但是考虑到该系统开发成本极高,特斯拉将会收取一定的费用。”


划重点就是自动驾驶技术成本高,所以要收费。ChatGPT 的训练成本高,惨遭无视。


另一位代表人物 Emad Mostaque 是 AIGC 独角兽企业 Stability AI 的创始人,号称“要让10亿人用上开源大模型”。Stability AI 最牛的项目是人工智能文本转图像模型 Stable Diffusion ,如今,这个项目深陷侵权旋涡。在今年一月份,全球知名图片提供商华盖创意(Getty Images)和艺术家萨拉·安德森(Sarah Andersen), 凯利·麦克南(Kelly McKernan)和卡拉·奥尔蒂斯(Karla Ortiz)起诉了Stability AI,认为Stability AI在未经许可或考虑的情况下,使用他人的知识产权,为自己的经济利益服务。


下面这幅图中,左边是知名油画家Erin Hanson的作品 "Crystalline Maples",右边是CNN记者通过 Stable Diffusion 生成的结果。


image.png


以我们受过九年义务教育的眼光来看,这两幅图风格、色彩,线条几乎一样,说是出自同一人之手也不为过。


在自己公司严重侵犯他人知识产权的情况下,去说另一家公司影响了人类安全和社会稳定,不过是五十步笑百步了。


反对原因分析


信息安全


信息安全是过去三个月最容易攻击ChatGPT的理由,联名信提出了一条质询,我们是否应该让机器用宣传和谎言充斥我们的信息渠道?综合起来的观点是,不良行为者可能会故意创建带有事实错误的内容,作为战略影响力活动的一部分,传播错误信息、虚假信息和彻头彻尾的谎言,这可能会对社会和个人造成危害。将这个观点强加于ChatGPT上,是避重就轻之举。


虚假信息,有什么比搜索引擎更多吗?虚假广告,违禁视频,歧视言论等数不胜数,上当受骗的人同样数不胜数。相较而言,ChatGPT的表现已经非常遵守道德和法律了。


错误信息,对于企业而言,文本信息会经过员工的二次编辑和确认,才会发布;错误的代码会经过程序员的修改和验证,才会用于产品中。只要责任制明确,风险是可控的。


人身安全


这次事件,被大家提起最多一条理由就是比GPT-4更先进AI系统将威胁人类安全,AI将杀死人类。若说威胁安全,智能驾驶和机器人更具有天然不安全属性。GPT-5终究是活在互联网世界中,任他搅的天翻地覆,也不会直接对人类进行物理攻击。智能驾驶如果失去控制,将导致车毁人亡,交通瘫痪。未来给机器人装上武器,就是最强特种兵。


不可否认的是,AI未来确实存在风险,但我们不能饮鸩止渴,因为未来的风险而停止新技术的前进。人工智能炒作了这么多年,直到ChatGPT才真正点燃了火炬,我们不应该在刚刚见到光明时,就亲手熄灭了它,技术推进和安全协议制定完全可以同步进行。


失业


根据高盛研究报告,全球预计将有3亿个工作岗位被生成式AI取代。目前欧美约有三分之二的工作岗位都在某种程度上受到AI自动化趋势的影响,而多达四分之一的当前岗位有可能最终被完全取代。该研究计算出美国63%的工作暴露在“AI影响范围”中,其中7%的工作有一半以上的流程可以由AI自动化完成,这使他们很容易马上被人工智能取代。


对于国内来说,目前感觉还好,可能主要在图像创作领域感受到寒意比较强,上周看到有博主表示,公司一次性裁了三个原画师。


b2f5b5034fac9cc366bff4dcc1815a32.jpeg
当新技术出现时,初期给社会带来的冲击会让很多人感到不适应,因为不适应,所以会本能的去排斥它。比如曾经的克隆,刚出现时引起了大家恐慌,认为会制造另一个自己,同时带来繁衍上的伦理问题。再比如前几年新能源起步时,大家纷纷嘲讽新能源车,认为它是来收智商税的,时至今日,新能源车已经是大势所趋。


现在的失业主要是国际经济形势带来的,而不是刚刚发展的AI系统带来的。ChatGPT只是一个工具,若说替代,机器人替换下来的劳动人口更多,但没有千名大佬站出来说要暂停机器人技术的发展。


利益


世上没有无缘无故的爱,也没有无缘无故的恨,天下熙熙,皆为利来,天下攘攘,皆为利往,所谓者,都是为了自身利益。呼吁暂停训练比 GPT-4 更强大的 AI 系统,目前只有OpenAI有能力训练比GPT-4更强大的系统GPT-5。根据预测,作为过渡的 GPT-4.5 模型将在 2023 年 9 月或 10 月推出,刚好就是联名信提出的暂停6个月。因此,所谓的的暂停,完全就是针对OpenAI的GPT-5。


OpenAI和微软在三月份的一系列进展让其他的公司产生了深深的危机感,这次的专家有的是自己拥有AI公司,有的是自己在AI领域深耕多年,通常来说,大多数人已经成为了利益团体的代言人。既生瑜何生亮,我没有的你也不能有,我有了,但你一枝独秀,那就枪打出头鸟。只有减缓OpenAI的发展速度,才能给自己追赶的机会。


正如前谷歌大脑成员吴恩达所说,我们该做的,应该是在AI创造的巨大价值与现实风险之间,取得一个平衡。把“让AI取得超越GPT-4的进展”暂停6个月,这个想法很糟糕。


总结


AI不是洪水猛兽,暂停GPT-5训练的做法解决不了安全问题,只有技术演进和安全协议制定同步进行,才能实现科技繁荣。6个月后的GPT-4.5依然只是一个工具,不存在威胁人类安全的可能,之后需要更多训练时间的GPT-5同样只是一个工具,这段时间,足够制定联名信期望的安全协议了。


所以,当务之急不是暂停训练比 GPT-4 更强大的 AI 系统,而是立即推动安全协议条款的研究。


作者:柒号华仔
来源:juejin.cn/post/7216412604800450621
收起阅读 »

Android TextView中那些冷门好用的用法

介绍 TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。 自定义字体 默认情况下,TextView 使用系统字体...
继续阅读 »

介绍


TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。


自定义字体


默认情况下,TextView 使用系统字体显示文本。但其实我们也可以导入我们自己的字体文件在 TextView 中使用自定义字体。这可以通过将字体文件添加到资源文件夹(res/font 或者 assets)并在 TextView 上以编程方式设置来实现。


要使用自定义字体,我们需要下载字体文件(或者自己生成)并将其添加到资源文件夹中。然后,我们可以使用setTypeface()方法在TextView上以编程方式设置字体。我们还可以在XML中使用android:fontFamily属性设置字体。需要注意的是,fontFamily方式只能使用系统预设的字体并且仅对英文字符有效,如果TextView的文本内容是中文的话这个属性设置后将不会有任何效果。


以下是 Android TextView 自定义字体的代码示例:



  1. 将字体文件添加到 assets 或 res/font 文件夹中。

  2. 通过以下代码设置字体:


// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);

// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

在上面的示例中,我们首先从 assets 文件夹中创建了一个新的 Typeface 对象。然后,我们使用 setTypeface() 方法将该对象设置为 TextView 的字体。


在上面的示例中,我们将字体文件命名为 “myfont.ttf”。我们可以将其替换为要使用的任何字体文件的名称。


自定义字体是 TextView 的强大功能之一,它可以帮助我们创建具有独特外观和感觉的应用程序。另外,我们也可以通过这种方法实现自定义图标的绘制。


AutoLink


AutoLink 是一种功能,它自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL ,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。


您可以通过将 autoLink 属性设置为 emailphoneweball 来在 TextView 上启用 AutoLink 。您还可以使用 Linkify 类设置自定义链接模式。


AutoLink 是一个功能,它自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。


要在 TextView 上启用 AutoLink,您需要将autoLink属性设置为emailphoneweball。您还可以使用Linkify类设置自定义链接模式。


以下是一个Android TextView AutoLink代码使用示例:


<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:textColorLink="@android:color/holo_red_dark"
android:text="这是我的个人博客地址: http://www.geektang.cn" />


在上面的示例中,我们将 autoLink 属性设置为 web ,这意味着 TextView 将自动检测文本中的 URL 并将其转换为可点击的链接。我们还将 text 属性将文本设置为 这是我的个人博客地址: http://www.geektang.cn 。当用户单击链接时,它们将被带到 http://www.geektang.cn 网站。另外,我们也可以通过 textColorLink 属性将 Link 颜色为我们喜欢的颜色。


AutoLink是一个非常有用的功能,它可以帮助您更轻松地创建可交互的文本。


对齐模式


对齐模式是一种功能,允许您通过在单词之间添加空格将文本对齐到左右边距。这使得文本更易读且视觉上更具吸引力。您可以将对齐模式属性设置为 inter_wordinter_character


要使用对齐模式功能,您需要在 TextView 上设置 justificationMode 属性。但是,此功能仅适用于运行 Android 8.0(API 级别 26)或更高版本的设备。


以下是对齐模式功能的代码示例:


<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is some sample text that will be justified."
android:justificationMode="inter_word"/>

在上面的示例中,我们将 justificationMode 属性设置为 inter_word 。这意味着 TextView 将在单词之间添加空格,以便将文本对齐到左右边距。


以下是对齐模式功能的显示效果示例:


image.png
同样一段文本,上面的设置 justificationMode 为 inter_word ,是不是看起来会比下面的好看一些呢?这个属性一般用于多行英文文本,如果只有一行文本或者文本内容是纯中文字符的话,不会有任何效果。


作者:GeekTR
来源:juejin.cn/post/7217082232937283645
收起阅读 »

连续加班一个多月后,反思一下为啥国内程序员加班这么多

连续加班一个多月后,反思一下为啥国内程序员加班这么多防杠指南:本文不适用于资深大佬,若喷必回今年过完年之后一直在加班,关注我的粉丝应该也能看出来,2 月份和 3 月份写的笔记确实比较少,最近才开始恢复加班完毕是得好好思考一下,毕竟咱这班也不能白加了对吧,我得好...
继续阅读 »

连续加班一个多月后,反思一下为啥国内程序员加班这么多

防杠指南:本文不适用于资深大佬,若喷必回

今年过完年之后一直在加班,关注我的粉丝应该也能看出来,2 月份和 3 月份写的笔记确实比较少,最近才开始恢复

加班完毕是得好好思考一下,毕竟咱这班也不能白加了对吧,我得好好想一想到底是为什么会导致我加班,我细数了一下平时导致我加班几个主要原因,大家看看有没有共鸣

业务需求倒排期,改的随意

互联网公司的业务迭代是非常快的,尤其是电商、营销相关的业务,基本上随时都在出需求,需求顺排倒还好,无非就是给了排期之后顺着做就行了

但是有一个非常蛋疼的点,如果这个需求业务方要的非常急,比如说 15 号出的需求 PRD ,月底就得上线,必须得倒排,那么就是说上线的时间定了,测试的时间占用一段,联调的时间再占用一段,留给开发的时间真的不多了

时间不够怎么办?要么加人要么加班,加人还有个问题,有的功能并不是很好拆分,而且人多了管理成本也在增加,1+1 并不是一定能等于 2 ,所以到最后就只能全员加班来肝需求

关于业务需求,还有一个可能导致加班的点是改的随意。

之前我在字节跳动打工的时候,每次需求评审会一堆年轻的 PM ,跟唱戏似的,你方唱罢我方上,哭爹喊娘的说自己的需求是多么多么的重要,常用的话术是:我这个需求是 xx 级别的老板看重的、我这个需求可以为公司创造 xx 的收入等等

一个个的 PRD 写的怎么样不重要,最重要的是抢占研发资源,最好可以把程序员固定在自己手里

等到需求开始做了,发现其实 PRD 里面有很多东西没想明白,这个时候就开始改 PRD ,改了 PRD 但是研发排期却不变,那这咋办呢?程序员加班呗

所以国内经常流行一个调侃的对联:

上联是:这个需求很简单

下联是:怎么实现我不管

横批是:明天上线

虽然这个对联是调侃的,但也暗示了很多公司在研发流程的不规范、管理混乱,这也是大部分程序员加班的重要原因

会议太多,占用时间

会议太多这个事情可能是大公司的通病,有时候屁大点事情就拉个会议,我细数了一下我一个月参加的会议:

  1. 需求评审会
  2. 技术方案评审会
  3. 需求复盘会
  4. 细节对齐会
  5. xx 项目启动会议
  6. xx 横向项目
  7. 技术分享会
  8. 周会
  9. 测试用例评审
  10. OKR 会议
  11. CodeReview 会议
  12. 等等......

其实这里面的会议真的太多了,有的团队还有早晨的站会等等,进一步压缩了写代码的时间

那能不能提升效率呢?我觉得可以

就说这个需求评审会吧,如果说每个人会前都能仔细的过一遍 PRD ,记录好疑点,那评审会完全可以开成答疑会,解答完疑问就差不多了,这样子可以节约很多时间,不至于一个需求评审会就开一两个小时

还有技术分享会,很多 leader 为了提升团队的技术氛围会要求组员进行技术分享,但是有的时候,分享的东西别人不一定感兴趣,深度把握的不好的话组员也会只把它当做任务去完成,这就是纯粹的浪费时间了

总之会议这部分,我觉得是一个存在很大提效空间的地方,一个事情是否需要拉会、是否要拉那么多人,是值得思考的

技术需求,各种丐版轮子

关于技术需求这个问题,我不知道是不是国内程序员的特色哈,就是纯做 PM 提的业务需求是很难得到好绩效和晋升的,因为这些事情是你工作职责范围内的事情,你很难说清楚这些事情带来的收益是 PM 的功劳还是研发的功劳

要想得到好绩效、超出预期,那就必须得做一些纯技术的事情,也就是所谓的“技术需求”,而且必须自己挤时间做,不会为这部分工作量专门划时间

常见的技术需求,比如说这两年特别流行的 LowCode 平台,据我所知很多大公司都在搞这种,并且是投入了很多研发的精力在里面的,美其名曰 LowCode 平台可以提高效率,所以在很多需求开发中强行推,要求研发必须使用 LowCode 平台来完成研发,但是在使用的过程中并没有提升效率,反而让研发增加了很多兼容成本和额外的工作量,不管能不能提供效率,先卷了再说

甚至有时候,多个团队之间在卷同样的技术轮子,一个大公司内部至少有 3 个 LowCode 平台、5 个组件库、3 个部署平台、4 个项目管理平台等等,大家都在加班卷技术项目,卷自己团队的存在感和好绩效

到最后,这个技术项目会出现在晋升答辩的 PPT 和汇报材料上,包装后的数字和成果都很亮眼,技术项目的发起者拿到了好绩效、晋升成功,等到晋升成功之后,这个技术项目的使命也就完成了,从此刻开始它就走上了烂尾的道路,历史项目也就留下了一堆烂摊子代码

老老实实做业务需求的人得不到晋升,做各种丐版技术轮子并且强推的人最后得到了晋升,这个问题在国内大公司非常普遍,这也是造成很多研发被卷着加班的重要原因

杂七杂八的事情,耗费精力

程序员还有一些杂事儿,也是相当的耗费精力了,我举几个例子

首先说线上 oncall ,这个事情其实也算是研发的正常工作范围内的事情了,但是如果一天出一个比较麻烦的线上 bug ,那今天肯定其他的事情就没空做了,或者只能加班去做

更不用说,如果所在的部门是基础架构部门的话,要处理技术之外的一些使用答疑事项,这部分事情毫无技术含量,和客服无异

还有就是非常强调技术要去深入业务,好嘛没问题,但是深入业务也是需要耗费时间的,这就意味着你除了读 PRD 以外还得去看 MRD ,可能你需要去和业务部门、市场部门的同事开会旁听 ta 门关心的事情,除过技术相关的东西以外还需要去关注业务指标

这又给自己增加了工作量,leader 不会说专门给这部分工作量去给你增加时间,只能自己挤时间了,这无形中又增加了加班

总结

我总结的这几个原因是我结合自身加班情况分析而来,可能国外的程序员也存在同样的问题,也可能有的人看法不一样,欢迎交流。

作者:程序员耳东
来源:www.v2ex.com/t/927862
收起阅读 »

关于Android相册实现的一些经验

一、序 我之前发布了个图片加载框架,在JCenter关闭后,“闭关修炼”,想着改好了出个2.0版本。 后来觉得仅增加功能和改进实现不够,得补充一下用例。 相册列表的加载就是很好的用例,然后在Github找了一圈,没有找到满意的,有的甚至好几年没维护了,于是就自...
继续阅读 »

一、序


我之前发布了个图片加载框架,在JCenter关闭后,“闭关修炼”,想着改好了出个2.0版本。

后来觉得仅增加功能和改进实现不够,得补充一下用例。

相册列表的加载就是很好的用例,然后在Github找了一圈,没有找到满意的,有的甚至好几年没维护了,于是就自己写了一个。

代码链接:github.com/BillyWei01/…


相比于图片加载,相册加载在Github上要多很多。

其原因大概是图片加载的input/output比较规范,不涉及UI布局;
而相册则不然,几乎每个APP都会有自己独特的需求,有自己的UI风格。

因此,相册库很难做到通用于大部分APP。

我所实现的这个也一样,并非以实现通用的相册组件为目的,而是作为一个样例,以供参考。


二、 需求描述


网上不少相册的开源库,都是照微信相册来搭的界面,我也是跟着这么做吧,要是说涉及侵权什么的,那些前辈应该先比我收到通知……

主要是自己也不会UI设计,不找个参照对象怕实现的太难看。

话说回来,要是真的涉及侵权,请联系我处理。


相册所要实现的功能,概括来说,就是显示相册列表,点击缩略图选中,点击完成结束选择,返回选择结果。


需求细节,包括但不限于以下列表:



  • 实现目录列表,相册列表,预览页面;

  • 支持单选/多选;

  • 支持显示选择顺序和限定选择数量;

  • 支持自定义筛选条件;

  • 支持自定义目录排序;

  • 支持“原图”选项;

  • 支持再次进入相册时传入已经选中的图片/视频;

  • 支持切换出APP外拍照或删除照片后,回到相册时自动刷新;


效果如图:


easy_album_cn.jpg


三、API设计


由于不同的页面可能需求不一样,所以可以将需求参数封装到”Request“中;

对于通用的选项,以及相册组件的全局配置,可以更封装到“Config"中。

而Request/Config最好是用链式API去设置参数,链式API尤其适合参数是“可选项”的场景。


3.1 全局设置


EasyAlbum.config()
.setImageLoader(GlideImageLoader)
.setDefaultFolderComparator { o1, o2 -> o1.name.compareTo(o2.name)}

GlideImageLoader是相册组件定义的ImageLoader接口的实现类。


public interface ImageLoader {
void loadPreview(MediaData data, ImageView imageView, boolean asBitmap);

void loadThumbnail(MediaData data, ImageView imageView, boolean asBitmap);
}

不同的APP使用的图片加载框架不一样,所以相册组件最好不要强依赖图片加载框架,而是暴露接口给调用者。

当然,对于整个APP而言,不建议定义这样的ImageLoader类,因为APP使用图片加载的地方很多,

定义这样的类,要么需要重载很多方法,要么就是参数列表很长,也就丧失链式API的优点了。


关于目录排序,EasyAlbum中定义的默认排序是按照更新时间(取最新的图片的更新时间)排序。

上面代码举例的是按目录名排序。

如果需要某个目录排在列表前面,可以这样定义(以“Camera”为例):


private val priorityFolderComparator = Comparator<Folder> { o1, o2 ->
val priorityFolder = "Camera"
if (o1.name == priorityFolder) -1
else if (o2.name == priorityFolder) 1
else o1.name.compareTo(o2.name)
}

出个思考题:

如果需要“优先排序”的不只一个目录,比如希望“Camera"第一优先,"Screenshots"第二优先,“Pictures"第三优先……

改如何定义Comparator?


3.2 启动相册


EasyAlbum启动相册以from起头,以start结束。


EasyAlbum.from(this)
.setFilter(TestMediaFilter(option))
.setSelectedLimit(selectLimit)
.setOverLimitCallback(overLimitCallback)
.setSelectedList(mediaAdapter?.getData())
.setAllString(option.text)
.enableOriginal()
.start { result ->
mediaAdapter?.setData(result.selectedList)
}

具体到实现,就是from返回 Request, Request的start方法启动相册页(AlbumActivity)。


public class EasyAlbum {
public static AlbumRequest from(@NonNull Context context) {
return new AlbumRequest(context);
}
}

public final class AlbumRequest {
private WeakReference<Context> contextRef;

AlbumRequest(Context context) {
this.contextRef = new WeakReference<>(context);
}

// ...其他参数..

public void start(ResultCallback callback) {
Session.init(this, callback, selectedList);
if (contextRef != null) {
Context context = contextRef.get();
if (context != null) {
context.startActivity(new Intent(context, AlbumActivity.class));
}
contextRef = null;
}
}
}

启动AlbumActivity,就涉及传参和结果返回。

有两种思路:



  1. 通过intent传参数到AlbumActivity, 用startActivityForResult启动,通过onActivityResult接收。

  2. 通过静态变量传递参数,通过Callback回调结果。


第一种方法,需要所有的参数都能放入Intent, 基础数据可以传,自定义数据类可以实现Parcelable,

但那对于接口的实现,就没办法放 intent 了,到头来还是要走静态变量。

因此,干脆就都走静态变量传递好了。

这个方案可行的前提是, AlbumActivity是封闭的,不会在跳转其他Activity。

在这个前提下,App不会同一个时刻打开多个AlbumActivity,不需要担心共享变量相互干扰的情况。

然后就是,在Activity结束时,做好清理工作。

可以将“启动相册-选择图片-结束相册”抽象为一次“Session”, 在相册结束时,执行一下clear操作。


final class Session {
static AlbumRequest request;
static AlbumResult result;
private static ResultCallback resultCallback;

static void init(AlbumRequest req, ResultCallback callback, List<MediaData> selectedList) {
request = req;
resultCallback = callback;
result = new AlbumResult();
if (selectedList != null) {
result.selectedList.addAll(selectedList);
}
}

static void clear() {
if (request != null) {
request.clear();
request = null;
resultCallback = null;
result = null;
}
}
}

四、媒体文件加载


媒体文件加载似乎很简单,就调ContentResolver query一下的事,但要做到尽量完备,需要考虑的细节还是不少的。


4.1 MediaStore API


查询媒体数据库,需走ContentResolver的qurey方法:


public final Cursor query( 
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder,
CancellationSignal cancellationSignal)
{
}

媒体数据库记录了各种媒体类型,要过滤其中的“图片”和“视频”,有两种方法:


1、用SDK定义好的MediaStore.Video和MediaStore.Images的Uri。


MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI

2、直接读取"content://external", 通过MEDIA_TYPE字段过滤。


private static final Uri CONTENT_URI = MediaStore.Files.getContentUri("external");

private static final String TYPE_SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
+ " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
+ ")";

如果需要同时读取图片和视频,第2种方法更省事一些。


至于查询的字段,视需求而定。

以下是比较常见的字段:


private static final String[] PROJECTIONS = new String[]{
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.Video.Media.DURATION,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.Images.Media.ORIENTATION
};

DURATION, SIZE, WIDTH, HEIGHT,ORIENTATION等字段有可能是无效的(0或者null),

如果是无效的,可以去从文件本身获取,但读文件比较耗时,

所以可以先尝试从MediaStore读取,毕竟是都访问到这条记录了,从空间局部原理来说,读取这些字段是顺便的事情,代价要比另外读文件本身低很多。

当然,如果确实不需要这些信息,可以直接不读取。


4.2 数据包装


数据查询出来,需要定义Entity来包装数据。


public final class MediaData implements Comparable<MediaData> {
private static final String BASE_VIDEO_URI = "content://media/external/video/media/";
private static final String BASE_IMAGE_URI = "content://media/external/images/media/";

static final byte ROTATE_UNKNOWN = -1;
static final byte ROTATE_NO = 0;
static final byte ROTATE_YES = 1;

public final boolean isVideo;
public final int mediaId;
public final String parent;
public final String name;
public final long modifiedTime; // in seconds
public String mime;

long fileSize;
int duration;
int width;
int height;
byte rotate = ROTATE_UNKNOWN;

public String getPath() {
return parent + name;
}

public Uri getUri() {
String baseUri = isVideo ? BASE_VIDEO_URI : BASE_IMAGE_URI;
return Uri.parse(baseUri + mediaId);
}

public int getRealWidth() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? width : height;
}

public int getRealHeight() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? height : width;
}

// ......
}

4.2.1 数据共享


字段的定义中,没有直接定义path字段,而是定义了parent和name,因为图片/视频文件可能有成千上万个,但是目录大概率不会超过3位数,所以,我们可以通过复用parent来节约内存。

同理,mime也可以复用。


截取部分查询的代码:


int count = cursor.getCount();
List<MediaData> list = new ArrayList<>(count);
while (cursor.moveToNext()) {
String path = cursor.getString(IDX_DATA);
String parent = parentPool.getOrAdd(Utils.getParentPath(path));
String name = Utils.getFileName(path);
String mime = mimePool.getOrAdd(cursor.getString(IDX_MIME_TYPE));
// ......
}

复用字符串,可以用HashMap来做,我这边是仿照HashMap写了一个专用的类来实现。

getOrAdd方法:传入一个字符串,如果容器中已经有这个字符串,返回容器保存的字符串,
否则,保存当前字符串并返回。

如此,所有的MediaData共用相同parent和mime字符串对象。


4.2.2 处理无效数据


前面提到,从MediaStore读取的数据,有部分是无效的。

这些可能无效的字段不要直接public, 而是提供get方法,并在返回之前检查数据的有效性,如果数据无效则读文件获取数据。

当然,读文件是耗时操作,虽然一般情况下时间是可控的,但是最好还是放IO线程去访问比较保险。


也有比较折中的做法:



  1. 数据只是用作参考,有的话更好,没有也没关系。

    如果是这样的话,提供不做检查直接返回数据的方法:


    public int getWidth() {
return rotate != ROTATE_YES ? width : height;
}

public int getHeight() {
return rotate != ROTATE_YES ? height : width;
}


  1. 数据比较重要,但也不至于没有就不行。

    这种case,当数据无效时,可以先尝试读取,但是加个timeout, 在规定时间内没有完成读取则直接返回。


    public int getDuration() {
if (isVideo && duration == 0) {
checkData();
}
return duration;
}

void checkData() {
if (!hadFillData) {
FutureTask<Boolean> future = new FutureTask<>(this::fillData);
try {
// Limit the time for filling extra info, in case of ANR.
AlbumConfig.getExecutor().execute(future);
future.get(300, TimeUnit.MILLISECONDS);
} catch (Throwable ignore) {
}
}
}

4.3 数据加载


数据加载部分是最影响相册体验的因素之一。

等待时间、数据刷新,数据有效性等都会影响相册的交互。


4.3.1 缓存MediaData


媒体库查询是一个综合IO读取和CPU密集计算的操作,文件少的时候还好,一旦文件比较多,耗时几秒钟也是有的。

如果用户每次打开相册都要等几秒钟才刷出数据,那体验就太糟糕了。

加个MediaData的缓存,再次进入相册时,就不需要再次读所有字段了,

只需读取MediaStore的ID字段,然后结合缓存,做下Diff, 已删除的移除出缓存,新增的根据ID检索其记录,创建MediaData添加到缓存。

再次进入相册,即使有增删也不会太多。


缓存MediaData的好处不仅仅是加速再次查询MediaStore,还可以减少对象的创建,不需要每次查询都重新创建MediaData对象;

另外,前面也提到,MediaData部分字段有的是无效的,在无效时需要读取原文件获取,缓存MediaData可免去再次读文件获取数据的时间(如果对象是读取MediaStore重新创建的,就又回到无效的状态了)。


还有就是,有缓存的话,就可以做预加载了。

当然这个得看APP是否有这个需求,如果APP是媒体相关的,大概率要访问相册的,可以考虑预加载。


做缓存的代价就是要占用些内存,这也是前面MediaData为什么复用parent和mime的原因。

缓存是空间换时间,复用对象是时间换空间,总体而言这个对冲是赚的,因为读取IO更耗时。

另外,如果有必要,可以提供clearCache接口,在适当的时机清空缓存。


4.3.2 组装结果


相册的UI层所需要的是: 根据Request的查询条件过滤后的MediaData,以目录为分组,按更新时间降序排列的数据。
缓存的MediaData并非查询的终点,但却提供了一个好的起点。

在有缓存好的MediaData列表的前提下,可直接根据MediaData列表做过滤,排序和分组,

而不需要每次都将过滤条件拼接SQL到数据库中查询,而且相比于拼接SQL,在上层直接根据MediaData过滤要更加灵活。


下面是EasyAlbum基于MediaData缓存的查询:


private static List<Folder> makeResult(AlbumRequest request) {
AlbumRequest.MediaFilter filter = request.filter;
ArrayList<MediaData> totalList = new ArrayList<>(mediaCache.size());

if (filter == null) {
totalList.addAll(mediaCache.values());
} else {
// 根据filter过滤MediaData
for (MediaData item : mediaCache.values()) {
if (filter.accept(item)) {
totalList.add(item);
}
}
}

// 先对所有MediaData排序,后面分组后就不需要继续在分组内排序了
// 因为分组时是按顺序放到分组列表的。
Collections.sort(totalList);

Map<String, ArrayList<MediaData>> groupMap = new HashMap<>();
for (MediaData item : totalList) {
String parent = item.parent;
ArrayList<MediaData> subList = groupMap.get(parent);
if (subList == null) {
subList = new ArrayList<>();
groupMap.put(parent, subList);
}
subList.add(item);
}

final List<Folder> result = new ArrayList<>(groupMap.size() + 1);
for (Map.Entry<String, ArrayList<MediaData>> entry : groupMap.entrySet()) {
String folderName = Utils.getFileName(entry.getKey());
result.add(new Folder(folderName, entry.getValue()));
}

// 对目录排序
Collections.sort(result, request.folderComparator);

// 最后,总列表放在最前
result.add(0, new Folder(request.getAllString(), totalList));
return result;
}

MediaFilter的定义如下:


public interface MediaFilter {
boolean accept(MediaData media);

// To identify the filter
String tag();
}

基于MediaData缓存列表的查询虽然比基于数据库的查询快不少,但是当文件很多时,也还是要花一些时间的。
所以我们可以再加一个缓存:缓存最终结果。

再加一个结果缓存,只是增加了些容器,容器指向的对象(MediaData)是之前MediaData缓存列表所引用的对象,所以代价还好。

再次进入相册时,可以先直接取结果显示,然后再去检查MediaStore相对于缓存有没有变更,有则刷新缓存和UI,否则直接返回。

APP可能有多个地方需要相册,不同地方查询条件可能不一样,所以MediaFilter定义了tag接口,用来区分不同的查询。


4.3.3 加载流程


流程图如下:

注意,下图的“结果”是提供给相册页面显示的数据,并非相册返回给调用者的“已选中的媒体”。



做了两层缓存,加载流程是复杂一些。

但好处也是显而易见的,增加了结果缓存之后,再次启动相册就基本是“秒开”了。

查询过程是在后台线程中执行的,结果通过handler发送给AlbumActivity。


图中还有一些小处理没画出来。

比如,首次加载,在发送结果给相册界面之后,还会继续执行一个“检查文件是否已删除”的操作。

针对的是这么一种情况:MediaStore中的记录,DATA字段所对应的文件不存在。

我自己的设备上是没有出现过这种case, 我也是听前辈讲的,或许他们遇到过。

如果确实有设备存在这样的情况,的确应该检查一下,否则相册滑动到这些“文件不存在”的记录时,会只看到一片黑,稍微影响体验。

但由于我自己没有具体考证,所以在EasyAblum的全局配置中留了option, 可以设置不执行。

关于这点大家按具体情况自行评估。


加载流程一般在进入相册页时启动。

考虑到用户在浏览相册时,有时候可能会切换出去拍照或者删除照片,可在onResume的时候也启动一下加载流程,检查是否有媒体文件增删。


五、相册列表


5.1 媒体缩略图


Android系统对相册文件提供了获取缩略图的API,通过该API获取图片要比直接读取媒体文件本身要快很多。
一些图片加载框架中有实现相关逻辑,比如Glide的实现了MediaStoreImageThumbLoader和MediaStoreVideoThumbLoader,但是所用API比较旧,在我的设备(Android 10)上已经不生效了。

如果使用Glide的朋友可以自行实现ModelLoader和ResourceDecoder来处理。

EasyAlbum的Demo中有实现,感兴趣的朋友可以参考一下。


5.2 列表布局


相册列表通常是方格布局,如果RecycleView布局,最好能让每一列都等宽。

下面这个ItemDecoration的实现是其中一种方法:


public class GridItemDecoration extends RecyclerView.ItemDecoration {
private final int n; // 列的数量
private final int space; // 列与列之间的间隔
private final int part; // 每一列应该分摊多少间隔

public GridItemDecoration(int n, int space) {
this.n = n;
this.space = space;
// 总间隔:space * (n - 1) ,等分n份
part = space * (n - 1) / n;
}

@Override
public void getItemOffsets(
@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state)
{
int position = parent.getChildLayoutPosition(view);
int i = position % n;
// 第i列(0开始)的左边部分的间隔的计算公式:space * i / n
outRect.left = Math.round(part * i / (float) (n - 1));
outRect.right = part - outRect.left;
outRect.top = 0;
outRect.bottom = space;
}
}

其原理就是将所有space加起来,等分为n份,每个item分摊1份。

其中第i列(index从0开始)的左边部分的间隔的计算公式为:space * i / n 。

比方说colomn = 4, 那么就有3个space; 如果每个space=4px, 则每个item分摊4 * (4-1)/ 4 = 3px。

第1个item, left=0px, right = 3px;

第2个item, left=1px, right = 2px;

第3个item, left=2px, right =1px;

第4个item, left=3px, right =0px。

于是,每个间隔看起来都是4px, 且每个item的left+right都是相等的,所以留给view的宽度是相等的。

效果如下图:



有的地方是这么去分配left和right的:


        outRect.left = column == 0 ? 0 : space / 2;
outRect.right = column == (n - 1) ? 0 : space / 2;

这样能让每个间隔的大小相等,但是view本身的宽度就不相等了。

效果如下图:



左右两个item分别比中间的item多了2px。

这2px看上去不多,但是可能会导致列表变更(增删)时,图片框架的缓存失效。

例如:

如果删除了最接近的一张照片,原第2-4列会移动到1-3列,原第1列会移动到第4列。

于是第2列的宽度从266变为288,第4列的宽度从288变为266,

而图片加载框架的target宽高是缓存key的计算要素之一,宽度变了,就不能命中之前的缓存了。


六、后序


相册的实现可简单可复杂,我见过的最简单的实现是直接在主线程查询媒体数据库的……

本文从各个方面分享了一些相册实现的经验,尤其是相册加载部分。

目前这个时代,手机存几千上万张图片是很常见的,优化好相册的加载,能提升不少用户体验。


项目已发布到Github和Maven Central:


Githun地址:
github.com/BillyWei01/…


下载方式:


implementation 'io.github.billywei01:easyalbum:1.0.6'

作者:呼啸长风
来源:juejin.cn/post/7215163152907092024
收起阅读 »

2022,这一年,我三十,而未立。

子曰:“吾十有五,而志于学,三十而立”。 一、我的背景 1. 大学之路 我大学时学的是电气工程及其自动化专业,和编程相关的学科有 plc 和单片机,但他们都不是 web 编程。后来机缘巧合,想做个网站,自学起了 web 编程,因为什么也不懂,也没人咨询,盲选了...
继续阅读 »

子曰:“吾十有五,而志于学,三十而立”。


一、我的背景


1. 大学之路


我大学时学的是电气工程及其自动化专业,和编程相关的学科有 plc 和单片机,但他们都不是 web 编程。后来机缘巧合,想做个网站,自学起了 web 编程,因为什么也不懂,也没人咨询,盲选了 PHP,再后来又学了些前端知识,算是入门了吧。


2. 初入职场


2016年,我毕业了。电气工程的工作是真不好找,好在我的 web 编程基础还算扎实,就想着去做个码农吧。我的第一份工作,找的很随性,那时连招聘软件都不了解,网上随意的搜到了家南京的软件小公司,看到官网的电话,打了过去。他招人,我找工作,就这么成了。

面试那天,雨特别大,老板后来和我说只有不是特别菜,冒这么大雨来,就要了。公司很小,一共4个人,老板和我简单聊了聊,看了看我带去的作品,就这么成了。于是我就有了第一份工作,成为了一名 phper

年底时,我跑路了,薪资太低,活不下去了,(公司竟然连五险都没给我交,不过这是后话了)。年后,换了一家继续做 phper,主要是微信相关的开发,虽然还有好几个开发者同事,但微信这块,只有我一个人,所以算是个“全干工程师”吧。


3. 不破不立


18年中,在这家干了一年半的我再次提桶,这一次我又是裸辞。既有自身能力原因,也有大环境因素,一个月时间,我没能找到合适的 php 工作。眼看身无分文,痛定思痛,我离开了南京,到了苏州这个竞争压力小些的地方。不仅是地理上离开了舒适区,工作上我也不得不从 phper 转向前端了。曾经学习与使用多年的 php,简化成了简历上的一句 “熟悉 php 语言” 几个文字。


4. 渐入佳境


靠着网友们的鼓励,我在苏州找到了一份不错的前端工作,公司在调整期,我成了前端接盘者,幸运的是没过多久项目就开始了重构。公司的技术栈是 Vue 和微信小程序。好在这些我之前有所涉及,在跌跌撞撞中也还算应付的不错。之后几年,每次有新的项目,我都会总结之前项目的问题,将学到的知识与经验应用其中,这期间技能得到了不小的成长。后来有了更多小伙伴的加入,我也开始负责了些前端的管理工作。


4. 愈感迷茫


如今,我已在这家公司练习时长四年半。并非现在的公司环境有多舒适或者福利待遇优渥,同一批共事的同事,现在也只剩下了一位。期间也有过多次离开的想法,但近几年的大环境,一直不是很好,生活上的琐事也不得不花费不少精力去处理。

可能我的能力,停留在了2021年初了吧,近几年的前端新技术,都没有触及。我仍然守着我的 Vue 2 与微信小程序这点地方,努力耕耘着。像 Vue 3viteTS 等,还停留在听说过的阶段。我有许多想做和需要做的事情,但时间,总是不够用,年已三十,孑然一身。


二、回顾 2022


这一年感觉经历了很多事情,不管是生活还是工作。但细想,又说不出是哪些。我想从迷茫中走出来。


1. 身份的转变


年初伊始,部门因为人员多了,boss 希望我们的部门经理把权力下放,不要事事躬亲。在 boss 的多次劝说下,经理索性当甩手掌柜,前端组完全交给了我去负责。经过一年的时间,我并未感觉到有多少管理能力的提升,领导经常贩卖焦虑,公司也在业务转型,事情比之前多了,原来他分配各每个人的活大多还能适量,现在都堆积到我这边,平均到每个人可能是1.5个量了,带来无止境的加班。我感觉我没做好。


2. 重拾旧代码


年初,我又折腾起了搁置了挺久的影视介绍网站项目,之前断断续续,以学习为目的折腾了4、5年了。今年终于陆续的折腾完,并开源了出来。因为项目较久,很多代码写的很是拙劣,所以我按照目前的能力,尽可能的做了重构。


今年的晚上下班后与周末的时间,尽数花在这个上了。
下面截取一张项目的更新图吧:


image.png

image.png



周末我一般不提交代码,所以没有提交记录。



3. 开启新篇章


当你看到这篇文章时,我已经成为了掘金的一名技术分享者。选择掘金社区,是因为从掘金中,我学到了挺多的知识。喝水不忘挖井人,之前我是个知识的获取者,现在我希望能将这几年学到的一些技术与经验,分享给大家。


三、展望 2023


1. 不忘初心


2023,是18天后。大环境会变成什么样子,还是个未知数。我无法预测环境,但可以规划自己,我会坚持将自己的知识与大家分享。


2. 重新开始


四年多的前端工作经验,后期能力停滞不前。而我最需要的,就是打破这种状态,“三十而已”嘛。工作上:我在考虑换个工作环境,离开目前的舒适区。职业上:会去了解、学习、应用 Vue3TS微服务LowCode等等不算新的新事物。


结语


相聚掘金,与君共勉。


作者:冰糖雪梨同学
来源:juejin.cn/post/7176262850011693116
收起阅读 »

怎么去选择一个公司?

一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。 那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。 企业文化和价值观 行业势头 工资待遇 公司规模 人才水平 企业文化和价值观 无...
继续阅读 »

一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。


那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。



  • 企业文化和价值观

  • 行业势头

  • 工资待遇

  • 公司规模

  • 人才水平


企业文化和价值观


无法适应企业文化和价值观的员工,注定会被边缘化,获取不到资源,直到被淘汰。而适应企业文化和价值观的员工,在公司做事情则更能够得心应手。


如何选择适合自己的企业文化和价值观


如果你打算在一个公司长期发展,可以试着找找里面的熟人,聊聊公司内部的做事风格,比如晋升、奖金、淘汰、组内合作、跨部门合作以及如何处理各种意外情况等,这样就能实际感受到企业的文化和价值观了,然后再根据自己的标准,判断是否适合自己。


行业势头


行业一般会有风口期、黄金发展期和下降期三个阶段。



  • 处于下降趋势的行业要慎重考虑。

  • 处于风口期的行业发展趋势还不是很明显,如果你之前从事的行业和新的风口相关,那么不妨试试;如果你对这些风口背后的行业不是很熟悉,那不妨等风口的势头明朗了,再做打算。

  • 处于黄金发展期的行业发展已经稳定,有成熟的盈利模式,在这样的行业中积累经验,会在行业的发展上升期变得越来越值钱。如果你对这些行业感兴趣,不妨考虑相关的公司。


工资待遇


工资待遇不仅仅包括固定工资,还有一次性收入、奖金、股票以及各种福利等。


很多新入职的员工会有一些的奖金,例如签字费、安家费等,这些是一次性的,有时还会附加”规定时间内不能离职”等约束条件。这部分钱的性价比比较低,但一般金额还不错。


奖金主要看公司,操作空间很大,它和公司的经营状况关联紧密,谈Offer时约定的数额到后面不一定能够兑现,尤其是这两年整个互联网行业都不景气,很多公司的奖金都“打骨折”甚至直接取消了。


其他福利一般包括商业医疗保险、年假、体检、补贴等,它和公司所在行业有关联,具有公司特色。


股票也是待遇中很重要的一部分,很多公司在签Offer时会约定一定数量的股票,但是会分四年左右结清,这需要考虑你能坚持四年吗?四年之后没有股票要怎么办?


公司规模


如果待遇和岗位差不多,建议优先选择头部大公司,这样你可以学到更多的经验,接触更有挑战的业务场景,让自己成长的更快。


如果你看好一个行业,那么需要努力进入这个行业的头部公司。


人才水平


一个公司的人才水平,决定了公司对人才的态度和公司内部合作与管理的风格。


举个例子,如果一个公司里程序员的水平都很一般,那么这个公司就更倾向于不相信员工的技术能力,并制定非常细致和严格的管理规范和流程,避免员工犯错。如果你的水平高,就会被各种管理规范和流程束缚住。同时,如果你发现与你合作的人的水平都很“感人”,你也需要调整自己的风格,让自己的工作成果能够适应公司普遍的水平。




此文章为极客时间3月份Day26学习笔记,内容来自《职场生存

手册》课程。

收起阅读 »

掌控情绪,成为自己的主宰

最近看了一本《蛤蟆先生去看心理医生》,这是一本很薄的书,四五个小时就能看完,但看完觉得收获非常大,建议大家都去看看。 其中,对于三种人格状态、人生坐标及其自证预言的让我耳目一新,获益良多。童年时期对世界的态度和看法会让我们树立起人生坐标,在其后的成长阶段影响我...
继续阅读 »

最近看了一本《蛤蟆先生去看心理医生》,这是一本很薄的书,四五个小时就能看完,但看完觉得收获非常大,建议大家都去看看。


其中,对于三种人格状态、人生坐标及其自证预言的让我耳目一新,获益良多。童年时期对世界的态度和看法会让我们树立起人生坐标,在其后的成长阶段影响我们对待事物时所站的角度,或自怨自艾陷入自责螺旋、或怨天尤人指责他人、或理智冷静分析问题并解决问题,从而实现童年时期建立的人生坐标的自证预言。


此外,关于情绪的产生,究竟是目的产生情绪,还是情绪产生目的,文中也提出了一些与以往截然不同的观点。


比如有的挑剔型父母,不会讲道理或者懒得讲道理,把愤怒当成更便捷、更省事的手段,震慑住自己的孩子,进而使他听自己的话。这个例子里,可以说情绪是被被捏造出来的一种可放可收的控制他人的工具。


下文一起探讨一下一些书中的观点。


一、三种人格状态


对于每个人来说,会同时有儿童、父母以及成年人的状态。每一种自我状态都包括完整的思想、情感和行为方式,人与人之间的交往就是各自的三种不同人格状态之间的交往。


当面临训斥或批评等情境时,会触发切换到不同的人格状态。比如被老师、领导、权威、家长训斥,这时会切换到适应性儿童状态,感到无助、自责。此时,有的人会出现缺少自尊的行为,比如讨好或自我贬低,把自己放在弱者的地位,希望得到别人的同情,而此时对方则处在控制型父母状态。


三种人格状态


1.1 儿童自我状态


儿童自我状态源于童年时期的经历,是个体最先诞生的人格状态,是一个人从脆弱、幼小、无助,任何事都要依赖别人的阶段形成的个性部分。


适应性儿童自我状态


适应性儿童自我状态源于童年时期安全感的缺乏,发展出依赖性和迎合性的个性特征。这种状态下,会表现出顺从、听话、讨好等行为,内心常常充满自责、担心、焦虑。


在被批评或者自我批评的时候,常常会进入适应性儿童自我状态,感到无助和沮丧。


自由型儿童自我状态


自由型儿童自我状态则因为在童年时期得到了足够的支持和鼓励,孩子们的个性得到了充分的发展。


在这种状态下,人们表现出冲动、天真、撒泼、贪玩、冒险等行为,像以自我为中心的儿童一样追求快感并能充分表达自我的感情。当我们大哭、大笑的时候就处于这种状态。最典型的表达方式是“我要”或“我不要”。


1.2 父母自我状态


控制型父母自我状态


控制型父母自我状态与人交往表现出教育、批评、教训、控制的一面,他们会用言行重复从父母那里学来的观念和价值观,并试图证明给别人看,让别人接受他们的观念和价值观。


他们会动不动就指责你,还用不可能达到的标准来评判别人。指责你时会假装成营养型父母自我状态,说一些“我比你更心痛”,“我是为了你好”之类的话。


处于这个状态的人总希望扮演法官的角色,不停地控诉别人,给别人定罪,然后顺理成章地惩罚他们。有时甚至会将审判的矛头指向自己,进行毫不留情的自我批判。


他们从来不会抑郁,因为愤怒能够非常有效地抵抗抑郁。愤怒的人从不觉得内疚,因为他们总在怪罪别人。他们自卫的方式,是把自己内在的恐惧对外投射到别人身上,这样就能把对自己的怒火转向别人。


营养型父母自我状态


营养型父母自我状态与人交往表现出温暖、关怀、安慰、鼓励,就像母亲一样温柔体贴地对待身边的人。


1.3 成人自我状态


成人自我状态与人交往表现出理性、冷静、沉稳,而且善于思考利弊,用理性而不是情绪化的方式来行事,能理性地应对正在发生的现实状况。


进入儿童状态和父母状态是被迫还是主动?


在某些场景下,经验会告诉你,现在应该愤怒了,因此你就条件反射产生愤怒。比如父母不经过你的授权就把你的玩具送给了别人,老师冤枉了没有偷东西的你,莫名其妙被路人大妈骂了一顿,在这些状态下,我们很难保持理智,经常会做出情感化的反应。


此时我们会觉得愤怒是别人引发的,是别人为你选择的,因而别人控制了你的情绪。


但除非强迫,没有人能让我们产生什么感受,说到底,是我们选择了自己的感受。是自己选择了愤怒,也自己选择了悲伤。


只有成人自我状态才能理性思考


当处于儿童状态时,你会体验到童年的感受,比如无助、自责、冲动、愤怒,再次体验到过去的情绪,但学不到任何新的东西了。


当处在控制型父母状态时,基本上你不是在挑剔就是在教育别人。旧的思想主宰着你,这就是为什么单靠争论不能改变一个人的想法,只会让人更固执己见。


我们在这两种状态时,像父母或儿童一样行事,几乎不需要去思考,因为我们知道要做什么、说什么,就好像出演一个我们最喜欢、最了解的角色,台词和动作都烂熟于心。


比方说,有个角色叫生气鬼(当然没有比善于打压式教育的中国家长更适合扮演生气鬼这个角色了)。


生气鬼很懂该怎么表达愤怒。遇到适合他演的剧目,他能一字不差地说出台词,而且他经常遇到这样的场景,是不是很奇怪?他能不假思索地切换到愤怒的语调和音高,自动筛选出合适的用词,他的整个姿态都在表达愤怒。总之,他演的生气鬼接近完美,而关键在于,甚至都不用动脑子!就好像为了这场演出他排练了一辈子,而频繁地出演这个角色也使得他每一次表演都更传神。


只有在成人自我状态里,才能学到关于自我的新知识,因为只有在那个时候,才能理智思考当下的事情,评估自己的行为,或者倾听别人对你的看法而不马上驳斥。只有在这个时候,我们所有的知识和技能都能为己所用,而不再被脑子里父母过去的声音所驱使,也不会被童年的情绪所困扰。


二、人生坐标


每一个生命一定都得经历开始、中间和结束这三个阶段,而开始的阶段会显著地影响后来的阶段。因此你对世界的看法是在人生的最初阶段里形成的。


比如在你童年时,大约四到五岁左右,你会试图回答两个问题。




  1. 第一个问题是:我是怎么看自己的?我好吗?




  2. 第二个问题是:我是怎么看别人的?他们好吗?




人生坐标


一旦我们在童年决定用哪种态度和观点,这些态度和观点会变成我们心理的底层架构,在随后的人生里就会始终坚持自己的选择。从那以后,我们便建构出一个世界,不断确认和支持这些信念和预期。换一个词来说,我们把自己的人生变成了一个自证预言


所谓自证预言就是,我们会控制事件的发生,确保自己的世界和预期的一样,从而保证预言会成真。


2.1 我好,你不好


这类人认为我比别人好,表现出自负、偏执,对应于控制型父母自我状态,压制别人,证明自己的优越。


表现为:



  1. 以自我为中心,自以为是;

  2. 喜欢把失败的责任归咎他人;

  3. 固执己见,唯我独尊。


2.2 我不好,你好


这类人认为自己很差劲,别人都比他好,这种想法来源于童年时期的无助感,表现出自卑、依赖、讨好型人格,对应于适应型儿童自我状态。有些低自尊的人认为自己是生活的受害者,爱玩那些受害者游戏,但却善待别人。


遇到问题希望依赖他人解决问题,希望有一个父母、老师这样角色的人直接给出答案。


表现为:



  1. 自卑,易放弃自我或顺从他人;

  2. 喜欢加倍努力去赢得他人赞赏;

  3. 喜欢与父母意识重的人为友。


2.3 我不好,你不好


这类人表现出反社会模式,极端孤独和退缩,常常看不起自己,也看不起别人。


2.4 我好,你好


这种心理模式的人通常非常阳光和健康,以成熟和健康的方式与人交往。


表现为:



  1. 相信他人,能够接纳自己和他人。

  2. 善于发现彼此优点与长处。

  3. 保持积极、乐观、进取的心理状态。


三、心理游戏


人生坐标是一种处世态度,心理游戏是处世行为,当选择了什么样的人生坐标,就会导致你玩什么样的心理游戏。而根据心理游戏,又会导致对应的人生终点。


3.1 受害者游戏


处于悲伤的儿童状态时,会玩一些受害者游戏,把自己置于受虐者的位置。


我真不幸


玩这个游戏的人确信他们是不幸的,会随时报出一长串遭遇过的不幸事件。同时,这些人会竭尽所能地选择记住那些悲伤和不快乐的事件,而忘记或忽略美好的时光,从而让自己的人生更加贴近预想中的人生,让自己更好地扮演一个不幸的角色。


比如有的人会觉得自己非常糟糕,即使真爱来临,也会觉得自己不配,从而主动拒绝美好人生。


可怜弱小的我(PLOM)


可怜弱小的我(PLOM, Poor Littlle Old Me),这种人生活中喜欢用自怜猛烈地攻击自己,总感觉自己能力差,长的丑,事事不如人,处处低人一等。相信自己又弱小,又可怜,简直一无是处。


生活中喜欢做小透明,碰到机遇不会去接,反而第一时间躲开,确保实现自己可怜又弱小这个预言。


当面对对方处于控制型父母状态对自己横加指责和训斥,甚至会偷偷地或无意识地配合对方,来给自己制造不快,从而让在 PLOM 游戏里成为赢家,虽然现实世界里自己是受虐者。


不论做什么都要爱我


相信大家都遇到过那些喜欢挑战人性的人,这其实是不自信的表现。


有的人(男性或女性)会首先预设立场对方不爱我,然后不断突破底线去试探对方,就是想看看别人能宽容他们到什么程度,什么时候会排斥他们。这个过程中慢慢耗尽了爱和热情,直到最后对方忍无可忍离开自己,接着他们就会说:我早说过你会这样对我,证明我是真的很差劲。


这就像先预设杯子会摔碎,然后放到 20 厘米高的地方放下,如果杯子没有被打碎,就提高到 50 厘米、1 米,直到杯子终于被摔碎,然后得意的指着一地碎渣说:你看,我说了杯子会被摔碎吧。


在完成这种逼着爱的人离开自己的自我毁灭行为后,却因为对方的表现证实了自己的预言,这些人甚至有一些得意洋洋,或者说超脱的快感。


3.2 施虐者游戏


这些施虐者游戏中的人利用任何时机来制造一些能让他们审判别人的情境。是他们内心的施虐者让他们这么做,可内心的施虐者是谁呢,这是个值得思考的问题。


玩这类游戏的人常常会寻找弱势群体或者那些容易受到伤害的人,来满足自己的控制欲和优越感,或者至少能让他们占据道德和权威的制高点对别人评头论足。


我抓到你了,你这个坏蛋(NIGYYSOB)


我抓到你了,你这个坏蛋(NIGYYSOB, now I ‘ve got you. you son of bitch) 这种游戏能让愤怒的人找到看似正当的理由来发火,证明别人即无能又缺少道德,从而借此证实“我好,你不好”的人生坐标,接下来,他们就可以理所当然的进行训斥和惩罚。


工作场合不免有人会犯错,这种情况很常见,上司发现之后把犯错的下属叫进来好一顿训斥,小题大做,对下属大声咆哮。这种情况下,占据支配地位的人(如领导、老师、高年级的学生)很容易把自己想象成严厉的父母,或把员工当成顽劣的孩子来惩罚,或体罚所谓不听话的学生,或霸凌低年级的同学。


你为什么总让我失望/你怎么敢


还有些占有支配地位的人会说你竟敢忤逆我!或者你为什么总让我失望,玩这些游戏的人处于挑剔型父母状态,使得别人自卑、自责,从而加强他们的道德优越感,证实自己高人一等,他人一无是处。


猜猜我在想什么


有时候课堂上的老师会对学生玩一个猜猜我在想什么(Guess the word in my head)的游戏,学生因猜不出来自然觉得自己愚蠢,老师赢了无知的学生,从而获得优越感。


还有个典型场景是恋爱中的情侣,想必大家应该很熟悉了吧 😅。


玩这种游戏的人应该直接说出自己的需求,而不是然后要求别人理解自己,还是说,他们根本只是希望利用情绪这个工具直接惩罚别人呢。


四、活得真实


有人说,除了疾病带来的痛苦,所有的痛苦和悲伤都是源于自己的价值观。过去的经历往往会束缚自己的思想,不自觉进入适应型儿童或控制型父母状态,让自己或他人痛苦。


活得真实,就是真诚地面对自己的价值观和需求,打破从童年延续而来的因果循环,让真实的自我摆脱过去经历的束缚,在自由中成为真正的自己。


4.1 摆脱因处于儿童状态而没有主见


有时候,在面临决策时,我们会不自觉地向他人征求建议,放弃自己的主见,把选择的自由交出去。这是因为潜意识里不想承担选择的责任,逃避自由。这样失败发生的时候,我们可以理直气壮的责怪他人。


责怪是人处在儿童自我状态里做的事情,如果你处在成人自我状态,会认识到你对自己是有自主权的,你有力量改变你自己。决策的时候,你应该广开言路,但要允许用自己的方式去尝试,哪怕错了也没关系,从依赖逐步对抗依赖,最终走入了独立的状态。


不要总期望依赖一个智者(老师、家长等)来告诉自己一切的答案,而要逐渐引导自己进入成人状态,当然训练自己进入成人自我状态需要艰辛的努力和刻意的思考。


4.2 成为一个高情商的人


情商真正的意思是理解你内心的情感世界,并且还能掌控它。


高情商的人都拥有强大的自我意识,了解并理解自己的情感。如果你否认自己的情绪,不论是用无视还是压抑的方式,在某种程度上成了一个残缺的人。(虽然很多父母从小就会压抑你显露弱小的一面,比如:不许哭)


他们能管理情绪,能从悲伤和不幸中重新振作。但也许最重要的是,他们能控制冲动,也懂得延迟满足,从而避免轻率的决定和不妥的行为。


最后,把人生的坐标设置为“我好,你也好”,真诚的对待自己,拥抱并接纳自己的情绪,自信的决策并勇敢承担责任。也可以与他人共情,欣赏他人的优点,诚挚的合作,他人取得进步的时候也就可以发自内心的赞赏。


共勉!




PS:本文收录在在下的博客 Github - SHERlocked93/blog 系列文章中,欢迎大家关注我的公众号 前端下午茶,直接搜索即可添加或者点这里添加,持续为大家推送前端以及前端周边相关优质技术文,共同进步,一起加油~



推介阅读:



  1. 蛤蟆先生去看心理医生



另外可以加入「前端下午茶交流qun」微信qun,微信搜索 sherlocked_93 加我好友,备注 1

作者:SHERlocked93
来源:juejin.cn/post/7215185077444886589
>,我拉你入qun~

收起阅读 »

8 款AI 绘画生成器:从文本创建 AI 艺术图像

人工智能正在影响各行各业,近年来它对创意产业的影响越来越大。由于AI绘画生成器的可操作性,许多人有机会用自己的想法进行艺术创作——即使他们没有接受过系统的专业艺术教育。 最先进的人工智能绘画生成器可能会改变我们未来创作艺术的方式。使用 AI 绘画生成软件,您可...
继续阅读 »

人工智能正在影响各行各业,近年来它对创意产业的影响越来越大。由于AI绘画生成器的可操作性,许多人有机会用自己的想法进行艺术创作——即使他们没有接受过系统的专业艺术教育。


最先进的人工智能绘画生成器可能会改变我们未来创作艺术的方式。使用 AI 绘画生成软件,您可以生成肖像、风景和抽象艺术。您甚至可以模仿著名艺术家的风格。


简单说,您可以使用在线 AI 绘画生成器。通过使用在线AI图像生成器,输入文本,就可获得根据您描述而来的逼真的样式图像。


市场上出现了一系列AI绘画生成器,可以尝试一下。本文是对市场上推荐的一些流行的AI绘画生成器的全面回顾。请继续阅读。


1. 福托尔(Fotor)


Fotor,一站式多合一在线照片编辑器,最近发布了一个 精湛的AI图像生成器 。你只需要把你的想法输入到生成器中,然后你可以看到它在几秒钟内变成一个图像。Fotor有多种图像样式供您选择,例如随机,3D,动漫等。


Fotor的AI文本到艺术生成器最显着的特点是它非常适合初学者使用,只需填写文本并选择要生成图像的效果即可。如果您对照片不满意,可以多次重复生成,以确保获得最满意的结果。每个帐户每天都有一个积分可供您免费使用高质量的 AI 艺术


主要特点:



  • 每天生成 10 张免费图片。

  • 9种灯光效果供您选择。

  • 9种不同的转换风格供您选择。

  • 文本到图像和图像到图像的转换模式。

  • 6种作品可供选择。


Fotor AI 绘画生成器


2. 达尔-E 2(DALL-E 2)


AI绘画生成器达勒2


公众已知的最受欢迎的AI绘画生成器是 Dall-E-2图像生成器 ,由OpenAI开发的AI图像生成器。只需几分钟,您就可以使用 AI 技术创建高度逼真的图像。该工具可用于创建插图、设计产品和为业务产生新想法。Dall-E-2 是一个易于使用的界面,任何人都可以使用 AI 创建高质量的图像。DALL-E 2 支持向生成的图像添加详细信息或对其进行其他修改。


主要特点:



  • 高度逼真的图像。

  • 创建插图。

  • 设计产品。

  • 可定制的多层图像。

  • 编辑和修饰功能。

  • 免费试用(尽管您必须通过等候名单获得邀请)。


3. 火锅(Hotpot ai)--支持api


火锅AI绘画生成器


火锅 AI 可帮助您创建令人惊叹的图形、图像和文本。它激发创造力并自动化工作,而易于编辑的模板使任何人都可以创建设备模型、社交媒体帖子、营销图像、应用程序图标和其他工作图形。


火锅AI的文本到图像AI绘画生成器使任何人都可以创建有吸引力的绘画,插图和图像。描述你想要什么,并观看火锅将其变为现实。


付费创作在 3-10 秒内完成。免费请求需要 1-15 分钟,具体取决于流量。付费用户可以获得更快的服务器、更好的图像、商业用途,并避免每日限制。该系统为不太富裕的人免费提供补贴。您还可以免费申请积分以减少等待时间。


主要特点:



  • 无需代码即可创建 API/批量。

  • 快速照片生成(付费)。

  • 每日免费照片生成积分可用。


4. 夜间咖啡厅(NightCafe)


爱画生成器夜咖啡厅


夜咖啡馆是著名的人工智能艺术生成器之一。它以比其他 AI 绘画生成器具有更多的算法和选项而闻名,并且新手很容易上手。您需要做的就是前往他们的网站并根据您的想象力输入文本提示。然后,您需要等待最多 30 秒,一件艺术品才会出现在您面前。Nightcafe有自己的一套积分系统,您可以通过参加各种活动来获得积分,然后拥有可以免费生成图像的次数。此外,您还可以购买积分。


主要特点:



  • 信用赚取系统。

  • 视频生成工具。

  • 有用的社交功能。

  • 获得您生成的艺术品的所有权。

  • 比其他生成器更多的算法


5. 深度人工智能(DeepAI)


深度AI绘画生成器


自 2016 年以来,DeepAI 是首批通过开源软件生成人工智能图像的 AI 绘画生成器之一。


DeepAI允许您创建任意数量的图像,并且每个图像都是唯一的。它是高度可定制的,允许您更改细节、颜色、纹理等的数量。如果您输入插图,DeepAI 可以立即生成与分辨率无关的矢量图像。


这是一个免费的在线AI图像生成器,这意味着您无需下载或进行其他设置。DeepAI还有一个API,开发人员可以使用它来连接到另一个软件项目。


主要特点:



  • 始终免费使用。

  • 为每个提示生成四个输出图像。

  • 开源软件。

  • 更改每个图像的各种细节。

  • 使用卡通 GAN 创建卡通


6. 深度梦境生成器(Deep Dream Generator)


深梦生成器


深度梦境生成器 是另一种流行的 AI 绘画生成器,支持在线人工智能来创建逼真的图像。Deep Dream依赖于用数百万张图像训练的神经网络。它易于使用,只需要您上传图像,然后根据原始图像自动生成新图像,您还可以选择不同地方或时期的绘画风格。


该工具允许您选择一个类别,例如动物或风景,然后基于它创建逼真的图像。最重要的是,Deep Dream允许您从三种样式中进行选择。深风格,薄风格或深梦。选择样式后,可以预览图像。


主要特点:



  • 训练神经网络的数百万张图像。

  • 不同的绘画风格。

  • 图像的分类。

  • 文本到图像,图像到图像。


7. 克雷永(Craiyon)


克雷永爱绘画生成器


Craiyon,以前称为DALL-E mini,是一种人工智能模型,可以从任何文本提示中绘制图像。只需输入文本描述,它将根据您输入的文本生成 9 个不同的图像。该模型需要大量计算,因此Craiyon依靠广告和捐赠来支付其服务器的费用。只要您尊重使用条款,您就可以随意使用它们供个人使用,无论您是想与朋友分享还是将它们打印在 T 恤上。
主要特点:



  • 易于使用。

  • 无需注册或注册。

  • 免费生成无限的AI图像。

  • 一次生成 9 张图像,以有趣和创造性的方式


8. 星空人工智能(StarryAI)


星空AI绘画生成器


星空 AI 是一个 AI 绘画生成器,专注于将您的想法转化为 NFT 艺术。与大多数其他AI艺术生成器类似,Starry AI赋予您生成图像的所有权。这意味着您可以在任何地方使用图像,用于个人或商业用途。


Starry AI最好的部分是它是完全免费的。它是最好的免费 AI NFT 艺术生成器之一。它不需要用户的任何输入。它可以使用机器学习算法处理图像。该技术在不断改进,但已经有令人难以置信的使用该应用程序创作的艺术示例。


主要特点:



  • 自动图像生成器。

  • 无需用户输入。

  • 免费的 NFT 生成器。

  • 文本到图像


结论


这是人工智能的时期。使用 AI 图像生成器的强大功能自己创作令人惊叹的艺术品。自动将您的想象力变成绘画。AI图像生成器是未来。


在本文中,我们简要介绍了市场上顶级的AI绘画生成器,并推荐了八种最好的AI绘画生成器供您尝试。希望本指南对您有所帮助,谢谢!





作者:非优秀程序员
来源:juejin.cn/post/7214164344290951205
收起阅读 »

SpringBoot 项目使用 Sa-Token 完成登录认证

一、设计思路 对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验: 如果校验通过,则:正常返回数据。 如果校验未通过,则:抛出异常,告知其需要先进行登录。 那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录...
继续阅读 »

一、设计思路


对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:



  • 如果校验通过,则:正常返回数据。

  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。


那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:



  1. 用户提交 name + password 参数,调用登录接口。

  2. 登录成功,返回这个用户的 Token 会话凭证。

  3. 用户后续的每次请求,都携带上这个 Token。

  4. 服务器根据 Token 判断此会话是否登录成功。


所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。


动态图演示:


登录认证


接下来,我们将介绍在 SpringBoot 中如何使用 Sa-Token 完成登录认证操作。



Sa-Token 是一个 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:gitee.com/dromara/sa-…



首先在项目中引入 Sa-Token 依赖:


<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。


二、登录与注销


根据以上思路,我们需要一个会话登录的函数:


// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:



  1. 检查此账号是否之前已有登录

  2. 为账号生成 Token 凭证与 Session 会话

  3. 通知全局侦听器,xx 账号登录成功

  4. Token 注入到请求上下文

  5. 等等其它工作……


你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端


所以一般情况下,我们的登录接口代码,会大致类似如下:


// 会话登录接口 
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 第一步:比对前端提交的账号名称、密码
if("zhang".equals(name) && "123456".equals(pwd)) {
// 第二步:根据账号id,进行登录
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}

如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。
是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。


如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:



  • Cookie 可以从后端控制往浏览器中写入 Token 值。

  • Cookie 会在前端每次发起请求时自动提交 Token 值。


因此,在 Cookie 功能的加持下,我们可以仅靠 StpUtil.login(id) 一句代码就完成登录认证。


除了登录方法,我们还需要:


// 当前会话注销登录
StpUtil.logout();

// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();

// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();

异常 NotLoginException 代表当前会话暂未登录,可能的原因有很多:
前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 已经过期 …… 等等。


Sa-Token 未登录场景值参照表:


场景值对应常量含义说明
-1NotLoginException.NOT_TOKEN未能从请求中读取到 Token
-2NotLoginException.INVALID_TOKEN已读取到 Token,但是 Token无效
-3NotLoginException.TOKEN_TIMEOUT已读取到 Token,但是 Token已经过期
-4NotLoginException.BE_REPLACED已读取到 Token,但是 Token 已被顶下线
-5NotLoginException.KICK_OUT已读取到 Token,但是 Token 已被踢下线

那么,如何获取场景值呢?废话少说直接上代码:


// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle)
throws Exception {

// 打印堆栈,以供调试
nle.printStackTrace();

// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未提供token";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token已被踢下线";
}
else {
message = "当前会话未登录";
}

// 返回给前端
return SaResult.error(message);
}



注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理

三、会话查询


// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();

// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型

// ---------- 指定未登录情形下返回的默认值 ----------

// 获取当前会话账号id, 如果未登录,则返回null
StpUtil.getLoginIdDefaultNull();

// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);

四、Token 查询


// 获取当前会话的token值
StpUtil.getTokenValue();

// 获取当前`StpLogic`的token名称
StpUtil.getTokenName();

// 获取指定token对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);

// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();

// 获取当前会话的token信息参数
StpUtil.getTokenInfo();

TokenInfo 是 Token 信息 Model,用来描述一个 Token 的常用参数:


{
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginType": "login", // 账号类型标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)
"loginDevice": "default-device" // 登录设备类型
}

五、来个小测试,加深一下理解


新建 LoginAuthController,复制以下代码


package com.pj.cases.use;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

/**
* Sa-Token 登录认证示例
*
* @author kong
* @since 2022-10-13
*/

@RestController
@RequestMapping("/acc/")
public class LoginAuthController {

// 会话登录接口 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {

// 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(name) && "123456".equals(pwd)) {

// 第二步:根据账号id,进行登录
// 此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象
StpUtil.login(10001);

// SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述
return SaResult.ok("登录成功");
}

return SaResult.error("登录失败");
}

// 查询当前登录状态 ---- http://localhost:8081/acc/isLogin
@RequestMapping("isLogin")
public SaResult isLogin() {
// StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false
boolean isLogin = StpUtil.isLogin();
return SaResult.ok("当前客户端是否登录:" + isLogin);
}

// 校验当前登录状态 ---- http://localhost:8081/acc/checkLogin
@RequestMapping("checkLogin")
public SaResult checkLogin() {
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();

// 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息
return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息");
}

// 获取当前登录的账号是谁 ---- http://localhost:8081/acc/getLoginId
@RequestMapping("getLoginId")
public SaResult getLoginId() {
// 需要注意的是,StpUtil.getLoginId() 自带登录校验效果
// 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的
Object userId = StpUtil.getLoginId();
System.out.println("当前登录的账号id是:" + userId);

// 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值
// 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id
Object userId2 = StpUtil.getLoginId(0);
System.out.println("当前登录的账号id是:" + userId2);

// 或者使其在未登录的时候返回 null
Object userId3 = StpUtil.getLoginIdDefaultNull();
System.out.println("当前登录的账号id是:" + userId3);

// 类型转换:
// StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型
int userId4 = StpUtil.getLoginIdAsInt(); // 将返回值转换为 int 类型
long userId5 = StpUtil.getLoginIdAsLong(); // 将返回值转换为 long 类型
String userId6 = StpUtil.getLoginIdAsString(); // 将返回值转换为 String 类型

// 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换?
// 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId
System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6);

// 返回给前端
return SaResult.ok("当前客户端登录的账号id是:" + userId);
}

// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
// TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称
String tokenName = StpUtil.getTokenName();
System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName);

// 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值
// 框架默认前端可以从以下三个途径中提交 Token:
// Cookie (浏览器自动提交)
// Header头 (代码手动提交)
// Query 参数 (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx
// 读取顺序为: Query 参数 --> Header头 -- > Cookie
// 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token
String tokenValue = StpUtil.getTokenValue();
System.out.println("前端提交的Token值为:" + tokenValue);

// TokenInfo 包含了此 Token 的大多数信息
SaTokenInfo info = StpUtil.getTokenInfo();
System.out.println("Token 名称:" + info.getTokenName());
System.out.println("Token 值:" + info.getTokenValue());
System.out.println("当前是否登录:" + info.getIsLogin());
System.out.println("当前登录的账号id:" + info.getLoginId());
System.out.println("当前登录账号的类型:" + info.getLoginType());
System.out.println("当前登录客户端的设备类型:" + info.getLoginDevice());
System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
System.out.println("当前 Token 的剩余临时有效期:" + info.getTokenActivityTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
System.out.println("当前 User-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在

// 返回给前端
return SaResult.data(StpUtil.getTokenInfo());
}

// 会话注销 ---- http://localhost:8081/acc/logout
@RequestMapping("logout")
public SaResult logout() {
// 退出登录会清除三个地方的数据:
// 1、Redis中保存的 Token 信息
// 2、当前请求上下文中保存的 Token 信息
// 3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除)
StpUtil.logout();

// StpUtil.logout() 在未登录时也是可以调用成功的,
// 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态
System.out.println("当前是否处于登录状态:" + StpUtil.isLogin());

// 返回给前端
return SaResult.ok("退出登录成功");
}

}

代码注释已针对每一步操作做出详细解释,大家可根据可参照注释中的访问链接进行逐步测试。


本示例代码已上传至 Gitee,可参考:
Sa-Token 登录认证示例




参考资料



作者:省长
来源:juejin.cn/post/7215971680349569061
收起阅读 »

给轮播图做一个自适应的高度。

web
不知道大家有没有遇到这样的需求或者说看到类似的效果,就是列表进去详情看轮播图的时候,当手指滚动轮播图时轮播的高度容器会自适应,这样下面的内容就向上挤,滑动的过程会计算高度,释放的时候也会滚到下一张,也会计算对应图片的高度,然后做一个缓动的动画效果。就像下面这张...
继续阅读 »

不知道大家有没有遇到这样的需求或者说看到类似的效果,就是列表进去详情看轮播图的时候,当手指滚动轮播图时轮播的高度容器会自适应,这样下面的内容就向上挤,滑动的过程会计算高度,释放的时候也会滚到下一张,也会计算对应图片的高度,然后做一个缓动的动画效果。就像下面这张图的样子。


1.gif


可以看到上面的图片内容文字,随着轮播的滑动高度也在变化。费话不多说直接上代码。


实现方法


可以通过监听鼠标mounse 或者手指的滑动 touch 事件来控制图片,这里本文只说一下轮播的功能实现思路,重点说的是怎么实现高度的自适应。


直接开始正文,先看 html 代码结构。


html 结构


<div class="container">
 <div class="wrapper">
   <div class="swiper">
     <div class="item">
       <img src="https://ci.xiaohongshu.com/776d1cc7-ff36-5881-ad8f-12a5cd1c3ab3?imageView2/2/w/1080/format/jpg" alt="">
     </div>
     <div class="item">
       <img src="https://ci.xiaohongshu.com/b8e16620-66a0-79a5-8a4b-5bfee1028554?imageView2/2/w/1080/format/jpg" alt="">
     </div>
     <div class="item">
       <img src="https://ci.xiaohongshu.com/e12013c2-3c46-a2cc-7fda-1e0b20b36f3d?imageView2/2/w/1080/format/jpg" alt="">
     </div>
   </div>
 </div>
 <div class="content">这是一段内容</div>
</div>

css 样式


.container {
 width: 100%;
 overflow: hidden;
}
.wrapper {
 width: 100%;
}
.swiper {
 font-size: 0;
 white-space: nowrap;
}
.item {
 display: inline-block;
 width: 100%;
 vertical-align: top; // 一定要使用顶部对齐,不然会出现错位的情况
}
.item img {
 width: 100%;
 height: auto;
 display: block;
}
.content {
 position: relative;
 z-index: 9;
 font-size: 14px;
 text-align: center;
 padding-top: 20px;
 background-color: #fff;
 height: 200px;
}

值得注意的地方有几点;



  1. 在使用父级 white-space 时,子集元素设置 display: inline-block 会出现高度不同的排列错位,解决办法就是加上一句 vertical-align: top ,具体什么原因我也不细讲了。

  2. 另外父级还要设置 font-size: 0 ,如果没加上的话,就会出现两个子集有空隙出现,加上之后空隙就会去掉。

  3. img 图片最好设置成高度自适应,宽度100% 还要加上 display: block ,没有的话底部就会出现间隙。


写好上面的 html容器部分和 样式,下面就看一下 js 上是怎么处理的。


Js 实现


开始之前我们先思考一下去怎么实现这个轮播以及高度的自适应问题,分为几步操作;



  1. 鼠标按下时,需要记录当前的位置和一些其他初始化的信息,并且给当前的父元素添加相应的鼠标事件。

  2. 鼠标移动时,需要通过当前实时移动时点位和按下时点位的相减,得到移动的距离位置,然后再赋值给父元素设置其样式 transform 位置,中间还做其他的边界处理,当然还有高度的变化。

  3. 鼠标释放是,通过移动时记录的距离信息判断是左滑还是右滑,拿到其对应的索引,通过索引就可以计算到滚动下一张的距离,释放之后设置 transition 过渡动画即可。


按照我们试想的思路,开始正文;


初始化数据


const data = {
 ele: null,
 width: 0,
 len: 0,
 proportion: .3,
 type: false,
 heights: [500, 250, 375],
 currentIndex: 0,
 startOffset: 0,
 clientX: 0,
 distanceX: 0,
 duration: 30,
 touching: false
}

const wrapper = data.ele = document.querySelector('.wrapper')
const items = document.querySelectorAll('.item')
data.width = wrapper.offsetWidth
data.len = items.length - 1
wrapper.addEventListener('touchstart', onStart)
wrapper.addEventListener('mousedown', onStart)

注意,这里在做高度之前,我们需要等图片加载完成之后才能拿到每一个元素的高度,我这里为了省懒就没写具体代码,上面的 heights 对应的是每个图片在渲染之后的高度,一般情况下最好让后端传回来带宽高,这样就不需要用 onload 再去处理这个。


鼠标按下时


function onStart(event) {
 if (event.type === 'mousedown' && event.which !== 1) return
 if (event.type === 'touchstart' && event.touches.length > 1) return
 data.type = event.type === 'touchstart'
 const events = data.type ? event.touches[0] || event : event

 data.touching = true
 data.clientX = events.clientX
 data.startOffset = data.currentIndex * -data.width

 data.ele.style.transition = `none`
 window.addEventListener(data.type ? 'touchmove' : 'mousemove', onMove, { passive: false })
 window.addEventListener(data.type ? 'touchend' : 'mouseup', onEnd, false)
}

上面的代码里面我做了PC和移动端的兼容,跟计划的一样,保存一下 clientX 坐标和一个初始的坐标 startOffset 这个由当前索引和父级宽度计算得到,场景是当从第二张图片滚动到第三张图片时,会把之前的第一张图片的距离也要加上去,不然就计算错误,看下面滑动时的代码。


另外在做监听移动的时候加上了 passive: false 是为了在移动端兼容处理。


鼠标移动时


function onMove(event) {
 event.preventDefault()
 if (!data.touching) return
 const events = data.type ? event.touches[0] || event : event

 data.distanceX = events.clientX - data.clientX

 let translatex = data.startOffset + data.distanceX
 if (translatex > 0) {
   translatex = translatex > 30 ? 30 : translatex
} else {
   const d = -(data.len * data.width + 30)
   translatex = translatex < d ? d : translatex
}

 data.ele.style.transform = `translate3d(${translatex}px, 0, 0)`
 data.ele.style.webkitTransform = `translate3d(${translatex}px, 0, 0)`
}

做了一个边界处理的,超了 30 的距离就不让继续滑动了,加上之前保存的 startOffset 的值,得到的就是具体移动的距离了。


鼠标释放时


function onEnd() {
 if (!data.touching) return
 data.touching = false

 // 通过计算 proportion 滑动的阈值拿到释放后的索引
 if (Math.abs(data.distanceX) > data.width * data.proportion) {
   data.currentIndex -= data.distanceX / Math.abs(data.distanceX)
}
 if (data.currentIndex < 0) {
   data.currentIndex = 0
} else if (data.currentIndex > data.len) {
   data.currentIndex = data.len
}
 const translatex = data.currentIndex * -data.width

 data.ele.style.transition = 'all .3s ease'
 data.ele.style.transform = `translate3d(${translatex}px, 0, 0)`
 data.ele.style.webkitTransform = `translate3d(${translatex}px, 0, 0)`

 window.removeEventListener(data.type ? 'touchmove' : 'mousemove', onMove, { passive: false })
 window.removeEventListener(data.type ? 'touchend' : 'mouseup', onEnd, false)
}

通过计算 proportion 滑动的阈值拿到释放后的索引,也就是超过父级宽度的三分之一时释放就会滚动到下一张,拿到索引之后就可以设置需要移动的最终距离,记得加上 transition 做一个缓动效果,最后也别忘记移除事件的监听。


至此上面的简单的轮播效果就大功告成了,但是还缺少一点东西,就是本篇需要讲的自适应高度,为了方便理解就单独拿出来说一下。


高度自适应


在移动时就可以在里面做相关的代码整理了, onMove 函数里加上以下代码,来获取实时的高度。


const index = data.currentIndex
const currentHeight = data.heights[index]
   
// 判断手指滑动的方向拿到下一张图片的高度
let nextHeight = data.distanceX > 0 ? data.heights[index - 1] : data.heights[index + 1]
let diffHeight = Math.abs((nextHeight - currentHeight) * (data.distanceX / data.width))
let realHeight = currentHeight + (nextHeight - currentHeight > 0 ? diffHeight : -diffHeight)

data.ele.style.height = `${realHeight}px`

这里是移动时的高度变化,另外还需要在释放时也要处理, onEnd 函数里加上以下代码。


// ... 因为上面已经拿到了下一张的索引 currentIndex
const currentHeight = data.heights[data.currentIndex]

data.ele.style.height = `${currentHeight}px`

因为上面已经拿到了下一张的索引 currentIndex 所以再滚动到下一张是就直接通过数据获取就可以了。


可以在线预览一下效果。


作者:ZHOUYUANN
来源:juejin.cn/post/7213654163317162045
收起阅读 »

抓包神器 Charles 使用教程(含破解)支持mac ios Android

本文以Mac 系统为例进行讲解 配置手机代理: 手机和 Mac 连接到同一个 WiFi 网络 1.1 Android 系统:「以华为 P20 手机为例」 设置 -> 无线和网络 -> WLAN 长按当前 WiFi -> 修改网络 勾选显...
继续阅读 »

本文以Mac 系统为例进行讲解



  • 配置手机代理:


手机和 Mac 连接到同一个 WiFi 网络


1.1 Android 系统:「以华为 P20 手机为例」



  • 设置 -> 无线和网络 -> WLAN

  • 长按当前 WiFi -> 修改网络

  • 勾选显示高级选项

  • 代理 -> 手动

  • 服务器主机名 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 服务器端口 -> 8888

  • 保存


1.2 IOS 系统:「以 iPhone Xs Max 手机为例」



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

  • 最底部 HTTP 代理 -> 配置代理 -> 勾选手动

  • 服务器 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 端口 -> 8888

  • 存储


核心功能


一、  抓包「以 iPhone Xs Max 为例」



  1. Charles 设置



  • Proxy -> Proxy Settings -> Port -> 8888

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




  1. 手机设置代理如上「配置手机代理」步骤




  2. 打开手机上任意联网的应用,Charles 会弹出请求连接的确认菜单,点击“Allow“即可完成设置




二、  过滤网络请求




  1. 左侧底部 Filter 栏 -> 过滤关键字




  2. 在 Charles 的菜单栏选择




Proxy -> Recording Settings -> Include -> add「依次填入协议+主机名+端口号,即可只抓取目标网站的包」



  1. 切换到 Sequence,在想过滤的网络请求上右击,选择“Focus“,在 Filter 栏勾选上 Focused


三、  分析 HTTPS 



  1. Mac 安装证书:


Help -> SSL Proxying -> Install Charles Root Certificate -> 输入系统的帐号密码,即可在钥匙串中看到添加好的证书


image.png


如果遇到证书不被信任的问题,解决办法:


Mac本顶栏 前往 -> 实用工具 -> 打开钥匙串访问 -> 找到该证书 -> 双击或右键「显示简介」-> 点开「信任」-> 选择「始终信任」




  1. Charles 设置请求允许 SSL proxying




  2. Charles 默认并不抓取 HTTPS 网络通讯的数据,若想拦截所有 HTTPS 网络请求,需要进行设置:在请求上右击选择 Enable SSL proxying




image.png
2. Charles -> Proxy -> SSL Proxying Settings -> SSL Proxying「添加对应的域名和端口号,为方便也可端口号直接添加通配符*」



  1. 移动端安装证书


a. Charles 选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser


b. 确保手机连上代理的情况下,在手机浏览器栏输入:chls.pro/ssl,下载证书,完成安装。


Android tips


1.1. 用自带浏览器下载证书,自带浏览器下载的证书提示文件格式不对,无法安装,可以尝试用uc浏览器下载后更改后缀为.crt后直接打开安装.(如果提示type the password for credenttial storage,需要给手机设置开机密码重启后再安装)


1.2. 若不能直接安装,需要下载下来,到手机设置 -> 安全 -> 从设备存储空间安装 -> 找到下载的证书 .pem 结尾的 -> 点击安装即可


IOS tips


IOS 需要设置手机信任证书,详见 官方文档。若不能直接安装,需在手机「设置」-> 通用 -> 描述文件与设备管理安装下载的证书,完成安装后 -> 找到关于本机 -> 证书信任设置,打开刚安装的证书的开关。


charles 安装&破解:


Charles的安装
官网最新的版本:http://www.charlesproxy.com/download/
Charles的注册
1.找到这个注册官网 :http://www.zzzmode.com/mytools/cha…
2.自定义"RegisterName",点击生成,复制key值
3.Charles->help->Registered.. 填写RegisterName值和复制的key值即可


抓包内容遇到乱码,解决如下:



  • Proxy -> SSL Proxy Settings -> Add

  • Host:*「代表所有网站都拦截」

  • Port:443

  • 保存后,在抓包数据就会显示正常


四、  模拟弱网




  1. 选择 Proxy -> Throttle Settings -> 勾选 Enable Throttling -> 选择 Throttle Preset 类型
    image.png
    五、  Mock 数据




  2. 以 map local 为例,修改返回值




选择目标请求,右键选择 Save All保存请求的 response 内容到本地文件



  1. 配置 Charles Map Local,Tool -> Map Local -> 勾选 Enable Map Local -> Add 「添加目标请求及需要替换的response 文件地址」-> OK


image.png



  1. 用文本编辑器打开保存的 json 文件,修改内容,进行替换。打开客户端应用重新请求该接口,返回的数据就是本地的文件数据。




作者:CodeCiCi
来源:juejin.cn/post/7215105725387374650
收起阅读 »

简析无感知刷新Token

web
在前后端分离的应用中,使用Token进行认证是一种较为常见的方式。但是,由于Token的有效期限制,需要不断刷新Token,否则会导致用户认证失败。为了解决这个问题,可以实现无感知刷新Token的功能,本文将介绍如何实现无感知刷新Token。 Token认证的...
继续阅读 »

在前后端分离的应用中,使用Token进行认证是一种较为常见的方式。但是,由于Token的有效期限制,需要不断刷新Token,否则会导致用户认证失败。为了解决这个问题,可以实现无感知刷新Token的功能,本文将介绍如何实现无感知刷新Token。


Token认证的原理


在Web应用中,常见的Token认证方式有基于Cookie和基于Token的认证。基于Cookie的认证方式是将认证信息保存在Cookie中,每次请求时将Cookie发送给服务器进行认证;而基于Token的认证方式是将认证信息保存在Token中,每次请求时将Token发送给服务器进行认证。


在基于Token的认证方式中,客户端将认证信息保存在Token中,而不是保存在Cookie中。在认证成功后,服务器将生成一个Access Token和一个Refresh Token,并将它们返回给客户端。Access Token用于访问受保护的API,Refresh Token用于获取新的Access Token。


什么是无感知刷新Token


无感知刷新Token是指,在Token过期之前,系统自动使用Refresh Token获取新的Access Token,从而实现Token的无感知刷新,用户可以无缝继续使用应用。


在实现无感知刷新Token的过程中,需要考虑以下几个方面:



  • 如何判断Token是否过期?

  • 如何在Token过期时自动使用Refresh Token获取新的Access Token?

  • 如何处理Refresh Token的安全问题?


下面将介绍如何实现无感知刷新Token的具体步骤。


实现步骤


步骤一:获取Access Token和Refresh Token


在认证成功后,需要将Access Token和Refresh Token发送给客户端。Access Token用于访问受保护的API,Refresh Token用于获取新的Access Token。可以使用JWT(JSON Web Token)或OAuth2(开放授权)等方式实现认证。


在JWT中,可以使用如下代码生成Access Token和Refresh Token:


const accessToken = jwt.sign({userId: '123'}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
const refreshToken = jwt.sign({userId: '123'}, 'REFRESH_TOKEN_SECRET', {expiresIn: '7d'});

步骤二:在请求中携带Access Token


在每个需要认证的API请求中,需要在请求头中携带Access Token,如下所示:


GET /api/user HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

在前端中,可以使用Axios等库设置请求头:


axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

步骤三:拦截401 Unauthorized响应


在服务器返回401 Unauthorized响应时,说明Access Token已经过期,需要使用Refresh Token获取新的Access Token。可以使用Axios拦截器或Fetch API的中间件实现拦截。


在Axios中,可以使用如下代码实现拦截器:


axios.interceptors.response.use(response => {
return response;
}, error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; //防止无限调用
return axios.post('/api/refresh_token', {refreshToken})
.then(response => {
const { access_token, refresh_token } = response.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return axios(originalRequest);
});
}
return Promise.reject(error);
});

在Fetch中,可以使用如下代码实现中间件:


function authMiddleware(request) {
const access_token = localStorage.getItem('access_token');
if (access_token) {
request.headers.set('Authorization', `Bearer ${access_token}`);
}
return request;
}

function tokenRefreshMiddleware(response) {
if (response.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
return fetch('/api/refresh_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
}).then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Refresh Token failed');
}).then(data => {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
return Promise.resolve('refreshed');
}).catch(error => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
return Promise.reject(error);
});
}
return Promise.resolve('ok');
}

fetch('/api/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
middleware: [authMiddleware, tokenRefreshMiddleware]
}).then(response => {
console.log(response);
}).catch(error => {
console.error(error);
});

在上述代码中,使用Axios或Fetch拦截器拦截401 Unauthorized响应,如果发现Access Token已经过期,则发送Refresh Token请求获取新的Access Token,并将新的Access Token设置到请求头中,重新发送请求。


步骤四:服务器处理Refresh Token请求


在服务器端,需要编写API处理Refresh Token请求,生成新的Access Token,并返回给客户端。


JWT中,可以使用如下代码生成新的Access Token:


const accessToken = jwt.sign({userId: '123'}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});

在刷新Token时,需要验证Refresh Token的合法性,可以使用如下代码验证Refresh Token:


try {
const payload = jwt.verify(refreshToken, 'REFRESH_TOKEN_SECRET');
const accessToken = jwt.sign({userId: payload.userId}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
const refreshToken = jwt.sign({userId: payload.userId}, 'REFRESH_TOKEN_SECRET', {expiresIn: '7d'});
res.json({access_token: accessToken, refresh_token: refreshToken});
} catch (err) {
res.sendStatus(401);
}

在上述代码中,使用JWT的verify方法验证Refresh Token的合法性,如果验证成功,则生成新的Access Token和Refresh Token,并返回给客户端。


步骤五:设置定时刷新Token


为了避免Access Token过期时间太长,可以设置定时刷新Token的功能。可以使用定时器或Web Workers等方式实现定时刷新Token。在每次刷新Token时,需要重新获取新的Access Token和Refresh Token,并保存到客户端。


function refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
axios.post('/api/refresh_token', {refreshToken})
.then(response => {
const { access_token, refresh_token } = response.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
})
.catch(error => {
console.error(error);
});
}

setInterval(refreshToken, 14 * 60 * 1000); // 每14分钟刷新Token

在上述代码中,使用定时器每14分钟刷新Token。在刷新Token成功后,将新的Access Token和Refresh Token保存到客户端,并将新的Access Token设置到请求头中。


安全性考虑


在实现无感知刷新Token的过程中,需要考虑到Refresh Token的安全性问题。因为Refresh Token具有长期的有效期限,一旦Refresh Token被泄露,攻击者就可以使用Refresh Token获取新的Access Token,从而绕过认证机制,访问受保护的API。


为了增加Refresh Token的安全性,可以考虑以下几种措施:



  • 将Refresh Token保存在HttpOnly Cookie中,可以避免在客户端被JavaScript获取;

  • 对Refresh Token进行加密或签名,可以增加其安
    作者:XinD
    来源:juejin.cn/post/7215569601161150522
    全性。

收起阅读 »

震惊!这个基于GPT-4的代码编辑器让我感到恐慌!

一 首先,我不是标题党。我确确实实受到了震撼。 其次,我今天要写的也不是在chatGPT里面叫AI写什么冒泡排序,鸡兔同笼等网上都已有大量代码示例的问题。 我知道chatGPT已经火出圈了,本人也试验过叫AI写一些简单的程序,太简单的基本上都能写对,稍微复杂点...
继续阅读 »


首先,我不是标题党。我确确实实受到了震撼。


其次,我今天要写的也不是在chatGPT里面叫AI写什么冒泡排序,鸡兔同笼等网上都已有大量代码示例的问题。


我知道chatGPT已经火出圈了,本人也试验过叫AI写一些简单的程序,太简单的基本上都能写对,稍微复杂点的也能介绍个大致思路,代码也能给出,但是很多都无法正常跑起来,也有一些逻辑性的错误。最多也只能用来参考下。


虽然我觉得目前AI能理解一些人类的意图,能给出大致的实现代码,但是还无法代替程序员去写一些稍微复杂点的算法程序。


直到我今天在网上看到这样一款AI写程序的软件:Cursor


官网长这样:


1.png



查了下该软件的特点以及背后的公司,问了new bing:


2.png


好家伙,原来是openAI这个公司出的。


但我又一想,不就是接入了chatGPT的API么,包装成一个IDE的样子。关键是chatGPT的代码功力我领教过,其实问题挺多的,有时候会有很基础的逻辑错误问题,完全不能拿来直接跑。


但是看在是openAI公司出品的份上,我还是下了这个软件,其实我并不报希望。


软件界面长这样,很像一个IDE:


3.png


简单问了一些猜数字,快速排序的问题。全对,直接复制到IDEA里就能直接跑。比如上图就是我问的一个给出猜数字游戏代码的问题。


其实这种程度chatGPT也能做到。但是很明显我的直观感受是cursor给出的代码的速度比chatGPT快太多了,基本是一秒十几行的速度。


我决定上点强度。



我于是不再问一些网上已有大量示例的经典问题,提了一个swing的需求,要求他帮我写一个swing界面,具体描述如下:



用swing写一个秒表程序,请在界面上画一个圆形的红色的秒表图形,图形上有2根针,一根是分针,一根是秒针,分针比秒针要短,初始都指向0分0秒。在秒表下方还有2个按钮,一个是开始,一个是暂停,当点击开始按钮的时候,秒表时钟开始走动,当点击暂停时,秒表停止走动。暂停后再点击开始,会继续走动。



输入进去,然后AI几乎没思考就开始写了:


4.gif


几秒钟就写完了,好像乍看之下还挺像那么回事,因为我看到他定义了颜色,画了线。我复制到IDEA里面一运行,竟然真的可以运行起来,效果如下:


5.gif


这个有点出乎我的意料,整体除了按钮位置有点不对以外,其他功能和我描述完全正确。


接着加大难度,我给他出了一道在swing界面文件对比的题:



请用swing写一个程序,图形界面顶部上有3个按钮,其中2个分别支持上传2个TXT文件,还有一个比较按钮,点击按钮,则会去比较这2个文件中内容的不同之处,如果完全一致,则弹出一个提示框表明2个文件内容一致。如果不一样,则在下方图形界面(和按钮不在同一行)分别显示这2个文件的内容,在文件内容里面用黄色下标箭头在内容不一致的地方打上标记



想解释下,为什么我一直给他出swing的题,因为swing有界面,好验证啊。


依然是秒出代码,大家看动图:


6.gif


程序明显比之前长很多,中间我输入了2次继续。总体挺丝滑的。复制程序到IDEA里面运行:


7.gif


这下彻底震惊到我了,卧槽,核心功能算是全部实现了。但也有瑕疵,我要求的是用黄色箭头把不一样的地方作标记,他则是把不一样的内容用文本的形式列了出来。


GPT-4写程序难道那么厉害了么,只要描述一小段话,就能写出一个小demo程序来。而且还可以直接运行。


我于是把相同的描述贴给了chatGPT,虽然chatGPT也给出了代码,但是运行出来是完全不对的。


这就说明,cursor不仅仅是个套壳软件。它是真正基于代码的方式进行训练的。



除了swing,普通的java多线程并发业务程序能写么,我于是又问了一个常见的业务问题:电商秒杀模拟程序。描述如下:



写一段程序,模拟下以下业务:
举办一个秒杀活动,总共有2个商品,商品A和商品B,各有50件。需要定义出商品的类。用线程模拟1w个人同时进来抢购,1w个人分别用ID1,ID2,ID3,以此类推来表示。
每个人每个商品只能最多抢2件。2个商品均没抢到的顾客信息不用打印,只打印出抢到了商品的顾客信息,格式举例如下:
顾客[ID1]抢到了[商品A]2件,[商品B]1件



我相信我描述的已经挺清楚了,也说明了要进行多线程,顾客ID命名给了一个推论的形式描述,以及打印信息只给了一个范例描述,看看AI能否学样去打印出符合我的结果


操作过程和上面一样,我就不贴动图了。贴一个图片看看:


8.png


运行出来的打印结果为:


9.png


这下我又要卧槽了,结果是100%完全正确的!我又仔细看了AI写多线程并发,发现也是完全正确的。



其实我测试到这里的时候,我心里已经开始焦虑了,没错,目前cursor也只能写一些单一算法的程序,但是正确率和理解力已经让我吃惊了,从chatGPT横空出世到GPT-4这才几个月啊,就已经这么强了。是不是再过几年,我们就要失业了,是不是就再也不需要程序员了?产品经理只要把详细的描述贴给AI,AI半小时吊打一个技术团队一个月的工作量。


细思极恐。


最后我把cursor的官网地址贴一下,大家可以去下载体验:


https://www.cursor.so/

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

白嫖谷歌搭建个人AI绘画(stable-diffusion),A卡救星

💡 最近的AI绘画大火,满心欢喜的准备体验一下,奈何网上大多数网站都是要收费,想着本地搭建一波,结果发现自己是AMD,但是多数开源的都不支持AMD,幸好在B站找到了大佬白嫖教程,这里就小记一下自己白嫖谷歌计算资源,自己生成AI绘图的教程 前置条件:可以访问谷歌...
继续阅读 »

💡 最近的AI绘画大火,满心欢喜的准备体验一下,奈何网上大多数网站都是要收费,想着本地搭建一波,结果发现自己是AMD,但是多数开源的都不支持AMD,幸好在B站找到了大佬白嫖教程,这里就小记一下自己白嫖谷歌计算资源,自己生成AI绘图的教程


前置条件:可以访问谷歌,有谷歌账号,Github



操作步骤:



1.打开Github项目




  1. 项目地址:github.com/camenduru/s…

  2. 分支选择drive





2.项目安装到谷歌的云端硬盘


2.1 按住ctrl点击一号位置,新窗口打开第一个链接,出现一个新的页面 2.2 第一步:复制到云端硬盘,第二步点击运行,第三步出现这个说明成功,点击期间会出现谷歌的弹窗,直接确定就可以了。 谷歌的云端硬盘,每个用户有15G的免费空间,这个项目大概12G,剩下的空间可以装一写model


3.运行stable-diffusion-webui


3.1 回到Github页面,继续按住ctrl点击二号位置,会打开一个新的连接,和第一次一样,保存-运行 3.2 运行需要一段时间,过一会儿,我们就会看见给出了两个连接,选择最后一个 3.3 打开连接,可以看到AI绘画熟悉的页面,默认是有个model的,不过它生成的图不怎么样,可以去换个model


4.更换model


4.1 model网址:civitai.com/ 4.2 这个复制model链接 4.3 添加model,回到Github页面,继续按住ctrl点击三号位置,将刚才复制的链接放到第一行,第二行是model的名字。复制完后,直接运行,等待就可以了


5.查看model,下载完成后model存放位置




6.生成成果


适合大家随意的玩一玩儿,祝大家玩儿的愉快。


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

H5开屏从龟速到闪电,企微是如何做到的

导读|H5开屏龟速常是令开发者头疼的问题。腾讯企业微信团队对该现象进行分析优化,最终H5开屏耗时130ms,达到秒开效果!企微前端开发工程师陈智仁将分享可用可扩展的Hybird H5秒开方案。该团队使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找...
继续阅读 »

导读|H5开屏龟速常是令开发者头疼的问题。腾讯企业微信团队对该现象进行分析优化,最终H5开屏耗时130ms,达到秒开效果!企微前端开发工程师陈智仁将分享可用可扩展的Hybird H5秒开方案。该团队使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找到瓶颈环节,进一步采用“预热”进行优化提速以解决了WebView初始化、数据预拉取、js执行(app初始化)耗时的问题。希望这些通用方法对你有帮助。


图片


背景


服务端渲染(SSR)是Web主流的性能优化手段。SSR直出相比传统的SPA应用加载渲染规避了首屏拉取数据和资源请求的网络往返耗时。团队针对Web开发也已经支持了SSR能力。近期出于动态化运营的考虑,我们选择了Web开发,同时我们也接到了提升体验的诉求。


以企业微信要开发的页面为例:采用SSR方案,从用户点击到首屏渲染的耗时均值约600ms,白屏时间的存在是可以感知到的。为了尽可能消除白屏达到秒开效果,我们尝试做更多探索。


图片


方案思路


1) 方案选型


如何实现页面秒开呢?从最直观的渲染链路来入手分析。下图列出了从用户点击到看到首屏渲染可交互,一个SPA应用主要环节的加载流程。我们调研了业内相关方案,从渲染链路的视角来看下常见方案的优化思路。


图片



  • 传统离线包


在加载渲染过程中,网络IO是很明显的一个耗时瓶颈。传统的离线包方案思路很直接,如果网络耗时那就将资源离线,很好地解决了资源请求的耗时。用Service Worker也能达到离线包的效果,同时也是Web标准。首次渲染优化一般需要结合客户端配置预启动脚本来达到缓存资源的效果。



  • SSR


SSR则从另外的角度出发,在请求页面的时候就进行服务端数据拉取和页面直出,首屏得以在一个网络往返就可以展示,有效地规避了后续需要等待css/js资源加载、数据拉取的时间。性能体验有比较大的提升,在BFF普及的情况下开发模式简单,很受欢迎。



  • 公司内相关工作


考虑到WebView的初始化(冷启动/ 二次启动)、页面网络请求、首屏数据接口的耗时,白屏时间还是可感知地存在的。以我们要开发的页面为例采用SSR首屏耗时均值600ms,可交互时间均值1100ms。如何进一步消除白屏?这里为各位介绍公司内外针对h5首屏性能优化的优秀方案。


手Q团队的VasSonic是集大成者,主要思路是采用WebView和数据预拉取并行的方式。这套方案需要客户端和服务端采用指定协议改造接入,开发时也有一定的改造工作。


微信游戏团队主要思路是利用jsCore做客户端预渲染,用户点击后直接上屏。这个方法也达到了很好的效果,首屏FCP时间从1664ms降低到了411ms。


我们做了一个简要的方案对比,可以看到每个方案都针对渲染链路的某个或多个环节做了优化,其中VasSonic的效果比较显著。不过结合企业微信业务实际情况,我们列出了如下几点考虑:


首先,接入对客户端和服务端有一定的改造成本,业务开发也有一定的改造工作。其次,我们已经有一套的统一发布平台,希望能复用这套发布能力。最后,性能上有没有进一步优化的空间呢?业务需求对体验上的要求是希望达到更好的性能效果或者说尽可能完全地消除白屏。


基于以上考虑,我们在上述方案的基础上做了进一步的实践探索,以期望达到更好的性能效果。



离线包SSRVasSonicCSR
资源加载
图片


图片

图片
数据拉取

图片

图片

图片
JS执行



WebView启动优化


图片

首屏FCP

图片

图片

图片
可交互(取决于JS执行)




2)方案架构


为了达到尽可能完全消除白屏,我们还是从初始问题出发,结合渲染链路进行分析,思路上针对每个环节采取对应的优化方法。


每个环节的优化在具体落地时会存在着方案的利弊取舍。比如预拉取数据一般的思路是交给客户端来做,但是存在着客户端请求和h5请求两套机制(鉴权、请求通道等方面)如何协调的问题。在渲染链路分析时,如果业务的js执行也贡献了不少耗时,有没有可能从通用基础方案的角度来解决这个问题,同时也能减少业务对性能优化的关注?这是个值得各位思考探索的问题。具体的内容会在后面展开来说。


如图展示了方案的优化思路和主流程。方案使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找到瓶颈环节,进一步采用预热的思路进行优化提速,解决了WebView初始化、数据预拉取、js执行(app初始化)耗时的问题,最终达到了理想的性能体验。


图片


图1 上屏流程


图片


图2 方案架构


下面我们具体介绍下方案,包括:离线包技术、预热提速和进一步的优化工作。


图片


离线包加速


为了规避资源请求耗时,我们使用了离线包技术。离线包技术是比较成熟的方案,相关打包、发布拉取的方案这里不多说了,主要说下方案中一些设计上的考量。


1)加载流程


图片


我们通过offid作为离线包应用的标识,fallback机制保证离线资源不可达时用户也可以正常访问页面,通过离线包预拉取和异步检测更新机制提高了离线包命中率,尽可能消除了网络资源加载的耗时。



2)fallback机制


因为用户网络状况的不确定性,离线包加载可能存在失败的情况。**为了保证可用性,我们确定了离线包加载不阻塞渲染的思路。**当用户点击入口url,对应offid离线包在本地不存在时,会fallback请求现网页面,同时异步加载离线包。所以我们针对离线包的打包结构,按照现网URL path来组织资源路径。这样客户端请求拦截处理也会比较方便,不需要理解映射规则。当发现离线包不匹配资源时,放过请求透到现网即可。如图展示了我们的离线包结构示例。


图片



3. 离线包生命周期


为了提高离线包命中率,我们会配置一些时机(e.g.入口曝光)来预拉取离线包。


离线包的更新机制:客户端加载时根据offid检测到本地离线包的存在,则直接使用拉起,同时启动异步版本检测和更新。如果新包版本号大于本地版本号则更新缓存,同时发布平台也支持区分测试环境、正式环境以及按条件灰度。


上了离线包后,可以看到页面的首屏耗时均值从基准无优化的1340ms降到了963ms,离线包的预拉取和更新策略则使离线包命中率达到了95%。首屏耗时得到了一定的降低,但也还有比较大的优化空间,需要更一步的分析优化。


图片


预热提速


通过离线包的加速,我们解决了资源请求耗时的问题,不过从整个渲染链路来看还有很大的优化空间,我们做了具体的耗时分析,找出耗时瓶颈,针对耗时环节做了进一步的优化提速


1)耗时分析


离线包技术规避了资源请求耗时,但是从整个渲染链路来看还有很大的优化空间,我们做了耗时分析如下。


Hybird应用中,WebView初始化是比较耗时的环节,这里我们针对iOS WebView做了测试。



首次冷启动/ms二次打开/ms
iOS(WKWebView)480ms90ms

数据拉取方面,不同入口页面的耗时不一,某些入口页面比较重的接口耗时超过了1s。


图片


图片


此外,我们发现js执行也贡献了不少耗时。以某入口页面为例,框架初始化时间~10ms,app初始化时间~440ms。


图片



2)渲染链路预热提速




  • 预热流程




我们的目标是消除白屏,这里理想的方案是找到一种和业务无关的通用解法。方案的主要思路是预热,把能提前做的都做了。预热是不是就是把WebView提前创建出来就好了呢?不是的,这里的预热涉及到多个渲染环节的优化组合。如图展示了预热的整体流程,下面一个个来解。


图片



2)WebView预创建


为了消除WebView的耗时,我们采取了全局的预创建WebView,时机为配置入口曝光。不过全局复用预热WebView不可避免地会引入可能的业务内存泄露问题,下文会介绍对应的规避方案。





  • 数据预拉取




数据拉取是页面渲染的一个耗时环节。为了消除数据预拉取耗时,在预创建WebView阶段我们同时进行了数据预拉取。


数据预拉取常见的思路是交给客户端来做,但是存在着客户端请求和h5请求两套机制如何协调的问题,以请求鉴权为例,存在以下的问题:


第一,Web团队自身有一层node BFF,实现了相应的数据拉取业务逻辑,而客户端则走的私有协议通道请求C++后台,二者是不同的鉴权机制。


第二,如果交给客户端来做,可以接入HTTP请求这套机制,改造成本比较大,如果复用原有通道,则一份数据业务逻辑需要两套实现。


如何设计一套通用可扩展的方案?我们希望做到客户端只关注容器的能力(预热、资源拦截等),屏蔽掉更深入的对Web的感知,这样的解耦可以有效控制方案的复杂度。因此,这里我们针对离线包配置项增加了preUrl字段,使客户端维护更通用的能力,数据预拉取交给业务团队来做,具体如下:


第一,客户端:拉取某个离线包配置项时会读取该字段,同时针对当前曝光的入口url可能存在多个有着不同的数据需求,这里会进行收集,将曝光url中的业务key参数拼接到preUrl来初始化WebView,这些作为通用能力。


第二,业务:preUrl页面在加载时会拉取相应的业务数据存到localStorage,实际的数据预拉取请求放到业务方发起,也可以很好地兼容已有的技术栈。





  • JS预执行




很接近目标了,最后js执行的耗时能不能消除呢?首先来看下440ms的耗时具体在哪里,通过分析看到,框架初始化仅需要不到10ms的时间,而真正的大头在业务代码的执行,其中代码编译耗时~80ms,其余的都是业务app初始化执行时间,这个是业务本身复杂度造成的。


我们首先考虑了创建两个WebView的方案,一个负责加载preUrl预拉取数据,另一个负责loadUrl上屏,这样设计上比较简洁健壮,不过实践下来发现效果不理想,如图展示了该方案的效果,渲染不稳定可以感知到白屏的存在。在已经有了预拉取数据和离线资源的情况下,理论上用户点击后需要等待的就只有渲染这块的耗时,实际我们发现在复杂应用初始化时存在js执行耗时较大的问题。


图片


最终我们做了一个预执行的解法。结合SPA的特点,将preUrl作为SPA的一个子页面,不需要UI展示,只负责预拉取数据,这样子页面加载完成的同时也完成了app提前初始化。而相应的不同入口切换页面时,不同于复用预热WebView重新reload页面,为了保留app初始化的效果,我们采取了一套Native通知Web SDK,页面切换交给WebView控制的方案。其中,Native通知则以调用SDK全局方法的方式。通过这种方式,入口页面间切换其实只是hashchange触发的子页面渲染,达到了不错的效果。流程图即预热方案的上屏部分。


图片


该方案执行后我们达到了预期目标效果,最大限度地消除了白屏接近Native体验。需求上线后通过监控数据可以看到在命中预热和离线包逻辑的情况下,从用户点击到页面上屏可交互耗时均值约130ms。


图片


图片


进一步优化



1)离线包安全


在离线包安全方面,为了防止包篡改,每我们次打包发布时都会生成包签名和文件md5。客户端在使用解析离线包时会校验完整性,在返回离线资源时会校验文件完整性。


2)稳定性


整体方案在性能上已经达到目标了,保证稳定性对产品体验也很重要。**我们为了消除js执行的耗时,采取了Native通知Web SDK控制页面切换的方式。虽然比较灵活但是也带来了稳定性的问题。**具体来说,如果SDK在做页面切换时异常,之后用户打开每个入口url都会看到相同的页面。入口页面的业务在用户使用过程中如果跳转了非SPA的链接同时没有注入SDK,之后的页面切换也会失效。


如何保证预热容器的可用性呢?我们设计了一套通知机制确保客户端感知到预热容器的可用状态,并在不可用时得以恢复,如图。预热容器会维护isInit和isInvokedSuc两个状态。只有当preUrl成功加载和SDK执行成功上屏时,两个状态才会置true,此时的预热WebView才是可用的,否则会回退到普通容器模式进行load url来加载页面。


图片


此外,在每次入口url曝光时,已有的预热容器也会销毁重建,也有效保证了容器的稳定性。



3)内存泄露


使用全局的预创建WebView,不可避免的会引入可能的业务内存泄露问题。在测试过程中,我们也发现了这种例子。可以看到当点开使用了预热容器的页面后放置一段时间,整个内存在不断上涨,最终会导致PC端页面的白屏或者移动端的Crash,这个状况最终归因是业务逻辑的实现存在缺陷。


图片


不过在基础技术的角度而言,开发者也需要采取措施来尽可能规避内存泄露的情况。主要思路是减少同一个预热容器的常驻,也就是对存活的容器设置有效期,在适当的时机检查并清理过期容器,我们选择的时机是App前后台切换时


4)解决副作用


出于性能考虑,我们选择了通过Web SDK控制页面的方案,同时使用了全局的预创建WebView。这带来了副作用——当页面对容器做了全局的设置,可能会影响到下一个页面的表现。比如:设置document.title、通过私有JSAPI设置了WebView导航栏的表......


当执行这些操作时,在下一个页面也复用预热容器的情况下,全局设置没有得到清理重置或者覆盖,用户会看到上个页面的表现。


为了解决上述问题,业务可以在每个页面主动声明需要的表现来覆盖上个页面的设置,理想的方法还是基础技术来规避这个问题来保证业务开发的一致性。我们在SDK控制切换页面时,进行了一系列的重置操作。


此外,在Windows和Mac端,我们也设计了双预热WebView的方案来完全解决这个问题。每次使用时同时创建新容器,得以保证每次打开入口页面都是使用新创建的容器。当然,方案的另一面则是会带来App内存的上涨。


图片


图片


总结


我们从渲染链路入手,针对每个环节进行分析优化,最终沉淀了一套可用可扩展的Hybird H5秒开方案。从渲染链路的角度来看,方案通过离线包和预热一系列优化,将用户从点击到可交互的时间缩短到了一个SPA路由切换上屏步骤的耗时。


图片


上线后我们监控发现,命中了预热离线逻辑的页面首屏耗时~130ms,相比于离线包、SSR都有优势,同时预热离线容器命中率也达到了97%,达到了理想的体验效果。希望本篇对你有帮助。



图片


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

巧妙利用枚举来替代if语句

前言 亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句 能实现功能的代码千篇一律,但优雅的代码万里挑一 业务背景 在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。 我就简答举个栗子哈💬 根据 不同的c...
继续阅读 »

前言


亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句


能实现功能的代码千篇一律,但优雅的代码万里挑一


业务背景


在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。


我就简答举个栗子哈💬



根据 不同的code,返回不同的对象
传1 返回 一个对象,包含属性:name、age ; 传2,返回一个对象,包含属性name ; 传3,返回一个对象,包含属性sex ....
字段值默认为 test



思路


摇头版


public class TestEnum {
public static void main(String[] args) {
Integer code = 1;//这里为了简单,直接这么写的,实际情况一般是根据参数获取
JSONObject jsonObject = new JSONObject();
if(Objects.equals(0,code)){
jsonObject.fluentPut("name", "test").fluentPut("age", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(1, code)){
jsonObject.fluentPut("name", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(2,code)){
jsonObject.fluentPut("sex", "test");
System.out.println("jsonObject = " + jsonObject);
}
}
}

上面的代码在功能上是没有问题滴,但是要扩展的话就💘,比如 当code为4时,ba la ba la,我们只有再去写一遍if语句,随着code的增加,if语句也会随之增加,后面的人接手你的代码时 💔


image-20230327234216250


优雅版


我们首先定义一个枚举类,维护对应Code需要返回的字段


@Getter
@AllArgsConstructor
public enum DataEnum {
/**
* 枚举类
*/
CODE1(1,new ArrayList<>(Arrays.asList("name","age"))),
CODE2(2,new ArrayList<>(Arrays.asList("name"))),
CODE3(3,new ArrayList<>(Arrays.asList("sex")))
;
private Integer code;
private List<String> fields;
//传入code 即可获取对应的 fields
public static List<String> getFieldsByCode(Integer code){
DataEnum[] values = DataEnum.values();
for (DataEnum value : values) {
if(Objects.equals(code, value.getCode())) {
return value.getFields();
}
}
return null;
}
}

客户端代码


public class TestEnum {
public static void main(String[] args) {
//优雅版
JSONObject jsonObject = new JSONObject();
//传入code,获取fields
List<String> fieldsByCode = DataEnum.getFieldsByCode(1);
assert fieldsByCode != null;
fieldsByCode.forEach(x->{
jsonObject.put(x,"test");
});
System.out.println(jsonObject);
}
}

实现的功能和上面的一样,但是我们发现TestEnum代码里面一条if语句都没有也,这时,即使code增加了,我们也只需要维护枚举类里面的代码,压根不用在TestEnum里面添加if语句,是不是很优雅😎


image-20230327235125257


小总结


【Tips】我们在写代码时,一定要考虑代码的通用性


上面的案例中,第一个版本仅仅只是能实现功能,但是当发生变化时难以维护,代码里面有大量的if语句,看着也比较臃肿,后面的人来维护时,也只能不断的添加if语句,而第二个版本巧用枚举类的方法,用一个通用的获取fields的方法,我们的TestEnum代码就变得相当优雅了😎


结语


谢谢你的阅读,由于作者水平有限,难免有不足之处,若读者发现问题,还请批评,在留言区留言或者私信告知,我一定会尽快修改的。若各位大佬有什么好的解法,或者有意义的解法都可以在评论区展示额,万分谢谢。
写作不易,望各位老板点点赞,加个关注!😘😘😘


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

代码优化一下,用线程池管理那些随意创建出来的线程

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码 很简单的一段代码,创建了一个Threa...
继续阅读 »

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码


image.png

很简单的一段代码,创建了一个Thread,然后把耗时工作放在里面进行就好了,如果项目当中只有一两处出现这样的代码,倒也影响不大,但是现在的项目当中,耗时的操作一大堆,比如文件读取,数据库的读取,sp操作,或者需要频繁从某个服务器获取数据显示在屏幕上,比如k线图等,像这些操作如果我们都去通过创建新的线程去执行它们,那么对性能以及内存的开销是很大的,所以我们在平时开发过程当中应该养成习惯,不要去创建新的线程而是通过使用线程池去执行自己的任务


线程池


为什么要使用线程池呢?线程池总结一下有以下几点优势



  • 降低资源消耗:通过复用之前创建过的线程资源,降低线程创建与销毁带来的性能与内存的开销

  • 提高响应速度:无需等待线程创建,直接可以执行任务

  • 提高线程可管理性:使用线程池可以对线程资源统一调优,分配,管理

  • 使用更多扩展功能:使用线程池可以进行一些延迟或者周期性工作


而我们创建线程池的方式有以下几种



  • Executors.newFixedThreadPool:创建一个固定大小的线程池

  • Executors.newCachedThreadPool:创建一个可缓存的线程池

  • Executors.newSingleThreadExecutor:创建单个线程数的线程池

  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池

  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池

  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池

  • ThreadPoolExecutor:最原始的创建方式,以上六种方式的内部也是通过这个创建的线程池


虽然我们提倡使用线程池,但是有这么多的创建方式,我们如果不在项目当中做一下管理的话,那么各种各样的线程池都有可能被使用到,由于每种创建方式对于线程的管理方式都不一样,如果不合理创建的话,很可能会出现问题,所以我们需要有一个统一创建线程池的地方


统一管理线程


image.png

首先我们先创建一个线程池,使用Executors.newCachedThreadPool()去创建一个
ExecutorService,至于为什么选择newCachedThreadPool(),我们看下它的源码


image.png

从上面一大段英文注释中我们能知道,这是一个可缓存的线程池,并且corePoolSize为0说明这个线程池没有始终存活的线程,如果线程池中没有可用线程,会重新创建新线程,而线程池中如果有可用线程,那么这个线程会被再利用,一个线程如果60秒内没有被使用,那么将会从队列中移除并销毁,所以个人感觉对于并发要求不是特别高的移动端,从性能角度来讲使用这样的一个线程池是比较合适的,当然具体设计方案以业务性质来决定,现在我们可以将项目当中的线程放在我们的线程池里面运行了,再增加一个执行线程的函数


image.png

通过这个函数就可以有效的避免项目当中随意创建线程的现象发生,让项目当中的线程可以井然有序的运行,但是这还没完事,我们知道Runnable在任务执行完成之后是没有返回结果的,因为Runnable接口中的run方法的返回类型是个void,但实际开发当中,我们的确有需求,在执行一些比如查询数据库,读取文件之类的操作中,需要获取任务的执行结果,之前都是通过在线程当中手动添加一个handler将需要的数据传递出来,再专业一点使用RxJava或者Flow,但不管什么方式,这些都会造成代码耦合,我们还有更简单的方式


Callable和Future


这两个类是在java1.5之后推出来的,目的就是解决线程执行完成之后没有返回结果的问题,我们先来对比下Runnable与Callable


image.png

相比较于Runnable,Callable接口里面也有一个call的方法,这个方法是有返回值的,并且可以抛出异常,所以以后当我们需要获取任务的执行结果的时候,我们还可以使用Callable代替Runnable,那么如何使用并获取返回值呢?当然是使用我们已经创建好的ExecutorService,它里面提供了一个函数去执行Callable


image.png

使用submit函数就可以执行我们的Callable,返回值是一个Future,而如何去获取真正的返回结果,就在Future里面,我们看下


image.png

使用get方法就可以获取线程的执行结果,我们现在就来试试Callable和Future,在PoolManager里面再增加一个函数,用来执行Callable


image.png

我们这里有个简单的任务,就是返回一段文字,然后将这段文字显示在界面上,那么第一步,先在布局文件里面添加一个按钮


image.png

然后点击这个按钮,将任务里面返回的文字显示在按钮上,代码如下


image.png

得到的效果如下


aa2.gif


在这边特地把执行结果放在界面上而不是用日志打印出来的原因可能大家已经发现了,Callable在返回执行结果的同时,也帮我们把线程切回到了主线程,所以我们不用在特地去切换线程更新ui界面了


周期性任务


普通的单个任务我们讲完了,但是在项目当中往往会存在一些比较特殊的任务,可能需要你去周期性的去执行,举个常见的例子,在证券类的app里绘制k线图的时候,并不需要将服务器吐出来的数据统统拿出来绘制ui,这样对性能的开销是很大的,我们正确的做法是将数据都先存放在一个buffer里面,然后定时的去buffer里面拿最新数据就好,那这样一个定时刷新的功能如何在我们的线程池里面去实现呢,这里就要用到刚刚说到的另一种创建线程池的方式


image.png

这个函数创建的是一个ScheduledExecutorService对象,可周期性的执行任务,入参的corepoolSize表示可并发的线程数,现在我们在PoolManager里面添加上这个ScheduledExecutorService


image.png

而如何去执行任务,我们使用ScheduledExecutorService里面的scheduleAtFixedRate函数,我们先看下这个函数都有哪些入参


image.png

不用去看注释我们就能知道怎么使用这个函数,command就是执行的任务,第二,第三个参数分别表示延迟执行的时间以及任务执行的周期时间,第四个参数是时间的单位,在看返回值是一个ScheduleFuture,既然也是个Future,那是不是也可以通过它去获取任务执行的结果呢?答案是拿不到的,一个原因是command是一个Runnable而不是Callable,不会返回任务的执行结果,另外我们从注释上就能了解,这个ScheduleFuture只是用来当周期任务中有任务被取消了,或者被异常终止的时候,抛出异常用的,那ScheduledExecutorService一定有入参是Callable的函数的吧,找了找发现并没有,那只有一个办法了,我们在command里面去执行一个Callable任务,再将任务的执行结果回调出来就好了,代码设计如下


image.png

我们创建了一个函数叫executeScheduledJob,也有四个入参,job是一个Callable,用来执行我们的任务,callback是一个回调,用来将任务执行结果回调到上层去处理,后面两个刚刚已经介绍过了,这里设置了默认值,可自定义,现在我们就来实现一个简单的读秒功能,点击刚刚那个按钮,按钮初始值是1,然后每秒钟加一,代码实现如下


image.png

这边创建了一个CounterViewModel用来执行计数器的逻辑,dataState是一个StateFlow并且设置了初始值1,在onCallback里面接收到了任务执行结果并发送至上层展示,上层的代码逻辑如下


image.png

现在这个计时器功能完成了,我们来执行下代码看看效果如何


aa3.gif


我们这边使用StateFlow发送数据还有个好处,当接收的数据中有些数据需要过滤掉的时候,我们还可以使用StateFlow提供的操作符实现,比如这边我们只想展示奇数,那么代码可以改成如下所示


image.png

使用filter操作符将偶数的值过滤掉了,我们再看看效果


aa4.gif


总结


我们的这个线程管理工具到这里已经完成了,不是很复杂,但是项目当中存不存在这样一个工具明显会对整体开发效率,代码的可读性,维护成本,以及一个app的性能角度来讲都会有个很大的提升与改善,后面如果还做了其他优化工作,也会拿出来分享。


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

Moshi 真正意义上的完美解决Gson在kotlin中默认值空的问题

Moshi Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com) 依赖 implementation("com.square...
继续阅读 »

Moshi


Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com)


依赖


implementation("com.squareup.moshi:moshi:1.8.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")

使用场景


基于kotlin-reflection反射需要额外添加 com.squareup.moshi:moshi-kotlin:1.13.0 依赖


// generateAdapter = true 表示使用codegen生成这个类的JsonAdapter
@JsonClass(generateAdapter = true)
// @Json 标识json中字段名
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
   val moshi: Moshi = Moshi.Builder()
       // KotlinJsonAdapterFactory基于kotlin-reflection反射创建自定义类型的JsonAdapter
      .addLast(KotlinJsonAdapterFactory())
      .build()
   val json = """{"_name": "xxx", "age": 20}"""
   val person = moshi.adapter(Person::class.java).fromJson(json)
   println(person)
}


  • KotlinJsonAdapterFactory用于反射生成数据类的JsonAdapter,如果不使用codegen,那么这个配置是必要的;如果有多个factory,一般将KotlinJsonAdapterFactory添加到最后,因为创建Adapter时是顺序遍历factory进行创建的,应该把反射创建作为最后的手段




  • @JsonClass(generateAdapter = true)标识此类,让codegen在编译期生成此类的JsonAdapter,codegen需要数据类和它的properties可见性都是internal/public




  • moshi不允许需要序列化的类不是存粹的Java/Kotlin类,比如说Java继承Kotlin或者Kotlin继承Java


存在的问题


所有的字段都有默认值的情况


@JsonClass(generateAdapter = true)
data class DefaultAll(
  val name: String = "me",
  val age: Int = 17
)

这种情况下,gson 和 moshi都可以正常解析 “{}” json字符


部分字段有默认值


@JsonClass(generateAdapter = true)
data class DefaultPart(
  val name: String = "me",
  val gender: String = "male",
  val age: Int
)

// 针对以下json gson忽略name,gender属性的默认值,而moshi可以正常解析
val json = """{"age": 17}"""


产生的原因


Gson反序列化对象时优先获取无参构造函数,由于DefaultPart age属性没有默认值,在生成字节码文件后,该类没有无参构造函数,所有Gson最后调用了Unsafe.newInstance函数,该函数不会调用构造函数,执行对象初始化代码,导致name,gender对象是null。


Moshi 通过adpter的方式匹配类的构造函数,使用函数签名最相近的构造函数构造对象,可以是的默认值不丢失,但在官方的例程中,某些情况下依然会出现我们不希望出现的问题。


Moshi的特殊Json场景


1、属性缺失


针对以下类


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int
)

若json = """ {"name":"John","age":18}""" Moshi可以正常解析,但如果Json=""" {"name":"John"}"""Moshi会抛出Required value age missing at $ 的异常,


2、属性=null


若Json = """{"name":"John","age":null} ”“”Moshi会抛出Non-null value age was null at $ 的异常


很多时候后台返回的Json数据并不是完全的统一,会存在以上情况,我们可以通过对age属性如gender属性一般设置默认值的方式处理,但可不可以更偷懒一点,可以不用写默认值,系统也能给一个默认值出来。


完善Moshi


分析官方库KotlinJsonAdapterFactory类,发现,以上两个逻辑的判断代码在这里


internal class KotlinJsonAdapter<T>(
 val constructor: KFunction<T>,
   // 所有属性的bindingAdpter
 val allBindings: List<Binding<T, Any?>?>,
   // 忽略反序列化的属性
 val nonIgnoredBindings: List<Binding<T, Any?>>,
   // 反射类得来的属性列表
 val options: JsonReader.Options
) : JsonAdapter<T>() {

 override fun fromJson(reader: JsonReader): T {
   val constructorSize = constructor.parameters.size

   // Read each value into its slot in the array.
   val values = Array<Any?>(allBindings.size) { ABSENT_VALUE }
   reader.beginObject()
   while (reader.hasNext()) {
       //通过reader获取到Json 属性对应的类属性的索引
     val index = reader.selectName(options)
     if (index == -1) {
       reader.skipName()
       reader.skipValue()
       continue
    }
       //拿到该属性的binding
     val binding = nonIgnoredBindings[index]
// 拿到属性值的索引
     val propertyIndex = binding.propertyIndex
     if (values[propertyIndex] !== ABSENT_VALUE) {
       throw JsonDataException(
         "Multiple values for '${binding.property.name}' at ${reader.path}"
      )
    }
// 递归的方式,初始化属性值
     values[propertyIndex] = binding.adapter.fromJson(reader)

       // 关键的地方1
       // 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
     if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
         // 抛出Non-null value age was null at $ 异常
       throw Util.unexpectedNull(
         binding.property.name,
         binding.jsonName,
         reader
      )
    }
  }
   reader.endObject()

   // 关键的地方2
    // 初始化剩下json中没有的属性
   // Confirm all parameters are present, optional, or nullable.
     // 是否调用全属性构造函数标志
   var isFullInitialized = allBindings.size == constructorSize
   for (i in 0 until constructorSize) {
     if (values[i] === ABSENT_VALUE) {
         // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
         constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
         constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
         else -> throw Util.missingProperty(
           constructor.parameters[i].name,
           allBindings[i]?.jsonName,
           reader
        )
      }
    }
  }

   // Call the constructor using a Map so that absent optionals get defaults.
   val result = if (isFullInitialized) {
     constructor.call(*values)
  } else {
     constructor.callBy(IndexedParameterMap(constructor.parameters, values))
  }

   // Set remaining properties.
   for (i in constructorSize until allBindings.size) {
     val binding = allBindings[i]!!
     val value = values[i]
     binding.set(result, value)
  }

   return result
}

 override fun toJson(writer: JsonWriter, value: T?) {
   if (value == null) throw NullPointerException("value == null")

   writer.beginObject()
   for (binding in allBindings) {
     if (binding == null) continue // Skip constructor parameters that aren't properties.

     writer.name(binding.jsonName)
     binding.adapter.toJson(writer, binding.get(value))
  }
   writer.endObject()
}


通过代码的分析,是不是可以在两个关键的逻辑点做以下修改



// 关键的地方1
// 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
   // 抛出Non-null value age was null at $ 异常
   //throw Util.unexpectedNull(
   //   binding.property.name,
   //   binding.jsonName,
   //   reader
   //)
   // age:null 重置为ABSENT_VALUE值,交由最后初始化剩下json中没有的属性的时候去初始化
values[propertyIndex] = ABSENT_VALUE
}

// 关键的地方2
// 初始化剩下json中没有的属性
// Confirm all parameters are present, optional, or nullable.
// 是否调用全属性构造函数标志
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
   if (values[i] === ABSENT_VALUE) {
       // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
           constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
           constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
           else ->{
               //throw Util.missingProperty(
                   //constructor.parameters[i].name,
                   //allBindings[i]?.jsonName,
                   //reader
          //)
               // 填充默认
               val index = options.strings().indexOf(constructor.parameters[i].name)
               val binding = nonIgnoredBindings[index]
               val propertyIndex = binding.propertyIndex
// 为该属性初始化默认值
               values[propertyIndex] = fullDefault(binding)

          }
      }
  }
}



private fun fullDefault(binding: Binding<T, Any?>): Any? {
       return when (binding.property.returnType.classifier) {
           Int::class -> 0
           String::class -> ""
           Boolean::class -> false
           Byte::class -> 0.toByte()
           Char::class -> Char.MIN_VALUE
           Double::class -> 0.0
           Float::class -> 0f
           Long::class -> 0L
           Short::class -> 0.toShort()
           // 过滤递归类初始化,这种会导致死循环
           constructor.returnType.classifier -> {
               val message =
                   "Unsolvable as for: ${binding.property.returnType.classifier}(value:${binding.property.returnType.classifier})"
               throw JsonDataException(message)
          }
           is Any -> {
               // 如果是集合就初始化[],否则就是{}对象
               if (Collection::class.java.isAssignableFrom(binding.property.returnType.javaType.rawType)) {
                   binding.adapter.fromJson("[]")
              } else {
                   binding.adapter.fromJson("{}")
              }
          }
           else -> {}
      }
  }

最终效果


"""{"name":"John","age":null} ”“” age会被初始化成0,


"""{"name":"John"} ”“” age依然会是0,即使我们在类中没有定义age的默认值


甚至是对象


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int,
   val action:Action
)
class Action(val ac:String)

最终Action也会产生一个Action(ac:"")的值


data class RestResponse<T>(
val code: Int,
val msg: String="",
val data: T?
) {
fun isSuccess() = code == 1

fun checkData() = data != null

fun successRestData() = isSuccess() && checkData()

fun requsetData() = data!!
}
class TestD(val a:Int,val b:String,val c:Boolean,val d:List<Test> ) {
}

class Test(val a:Int,val b:String,val c:Boolean=true)



val s = """
{
"code":200,
"msg":"ok",
"data":[{"a":0,"c":false,"d":[{"b":null}]}]}
""".trimIndent()

val a :RestResponse<List<TestD>>? = s.fromJson()



最终a为 {"code":200,"msg":"ok","data":[{"a":0,"b":"","c":false,"d":[{"a":0,"b":"","c":true}]}]}


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

抓包神器 Charles 使用教程支持mac ios Android

本文以Mac 系统为例进行讲解 配置手机代理: 手机和 Mac 连接到同一个 WiFi 网络 1.1 Android 系统:「以华为 P20 手机为例」 设置 -> 无线和网络 -> WLAN 长按当前 WiFi -> 修改网络 勾选显...
继续阅读 »

本文以Mac 系统为例进行讲解



  • 配置手机代理:


手机和 Mac 连接到同一个 WiFi 网络


1.1 Android 系统:「以华为 P20 手机为例」



  • 设置 -> 无线和网络 -> WLAN

  • 长按当前 WiFi -> 修改网络

  • 勾选显示高级选项

  • 代理 -> 手动

  • 服务器主机名 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 服务器端口 -> 8888

  • 保存


1.2 IOS 系统:「以 iPhone Xs Max 手机为例」



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

  • 最底部 HTTP 代理 -> 配置代理 -> 勾选手动

  • 服务器 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 端口 -> 8888

  • 存储


核心功能


一、  抓包「以 iPhone Xs Max 为例」



  1. Charles 设置



  • Proxy -> Proxy Settings -> Port -> 8888

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




  1. 手机设置代理如上「配置手机代理」步骤




  2. 打开手机上任意联网的应用,Charles 会弹出请求连接的确认菜单,点击“Allow“即可完成设置




二、  过滤网络请求



  1. 左侧底部 Filter 栏 -> 过滤关键字




  1. 在 Charles 的菜单栏选择


Proxy -> Recording Settings -> Include -> add「依次填入协议+主机名+端口号,即可只抓取目标网站的包」



  1. 切换到 Sequence,在想过滤的网络请求上右击,选择“Focus“,在 Filter 栏勾选上 Focused


三、  分析 HTTPS 



  1. Mac 安装证书:


Help -> SSL Proxying -> Install Charles Root Certificate -> 输入系统的帐号密码,即可在钥匙串中看到添加好的证书


image.png


如果遇到证书不被信任的问题,解决办法:


Mac本顶栏 前往 -> 实用工具 -> 打开钥匙串访问 -> 找到该证书 -> 双击或右键「显示简介」-> 点开「信任」-> 选择「始终信任」




  1. Charles 设置请求允许 SSL proxying




  2. Charles 默认并不抓取 HTTPS 网络通讯的数据,若想拦截所有 HTTPS 网络请求,需要进行设置:在请求上右击选择 Enable SSL proxying




image.png
2. Charles -> Proxy -> SSL Proxying Settings -> SSL Proxying「添加对应的域名和端口号,为方便也可端口号直接添加通配符*」



  1. 移动端安装证书


a. Charles 选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser


b. 确保手机连上代理的情况下,在手机浏览器栏输入:chls.pro/ssl,下载证书,完成安装。


Android tips


1.1. 小米机型请注意,如果是 MIUI 9 以上的版本,请不要用自带浏览器下载证书,自带浏览器下载的证书文件格式不对,无法安装,uc 浏览器下载没有问题。


1.2. 若不能直接安装,需要下载下来,到手机设置 -> 安全 -> 从设备存储空间安装 -> 找到下载的证书 .pem 结尾的 -> 点击安装即可


IOS tips


IOS 需要设置手机信任证书,详见 官方文档。若不能直接安装,需在手机「设置」-> 通用 -> 描述文件与设备管理安装下载的证书,完成安装后 -> 找到关于本机 -> 证书信任设置,打开刚安装的证书的开关。


抓包内容遇到乱码,解决如下:



  • Proxy -> SSL Proxy Settings -> Add

  • Host:*「代表所有网站都拦截」

  • Port:443

  • 保存后,在抓包数据就会显示正常


四、  模拟弱网




  1. 选择 Proxy -> Throttle Settings -> 勾选 Enable Throttling -> 选择 Throttle Preset 类型
    image.png
    五、  Mock 数据




  2. 以 map local 为例,修改返回值




选择目标请求,右键选择 Save All保存请求的 response 内容到本地文件



  1. 配置 Charles Map Local,Tool -> Map Local -> 勾选 Enable Map Local -> Add 「添加目标请求及需要替换的response 文件地址」-> OK


image.png



  1. 用文本编辑器打开保存的 json 文件,修改内容,进行替换。打开客户端应用重新请求该接口,返回的数据就是本地的文件数据。



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

Android打造专有hook,让不规范的代码扼杀在萌芽之中

俗话说,无规矩不成方圆,同样的放在代码里也是十分的贴切,所谓在代码里的规矩,指的就是规范,在一定规范约束下的项目,无论是参与开发还是后期维护,都是非常的直观与便捷,不能说赏心悦目,也可以用健壮可维护来表示;毕竟协同开发的项目,每个人都有自己的一套开发标准,你没...
继续阅读 »

俗话说,无规矩不成方圆,同样的放在代码里也是十分的贴切,所谓在代码里的规矩,指的就是规范,在一定规范约束下的项目,无论是参与开发还是后期维护,都是非常的直观与便捷,不能说赏心悦目,也可以用健壮可维护来表示;毕竟协同开发的项目,每个人都有自己的一套开发标准,你没有一套规范,或者是规范没有落地执行,想想,长此以往,会发生什么?代码堆积如山?维护成本翻倍增加?新人接手困难?等等,所谓的问题会扑面而来。


正所谓规范是一个项目的基石,也是衡量一个项目,是否健壮,稳定,可维护的标准,可谓是相当重要的。我相信,大部分的公司都有自己的一套规范标准,我也相信,很多可能就是一个摆设,毕竟人员的众多,无法做到一一的约束,如果采取人工的检查,无形当中就会投入大量的时间和人力成本,基于此,所谓的规范,也很难执行下去。


介于人工和时间的投入,我在以往的研究与探索中,开发出了一个可视化的代码检查工具,之前进行过分享,《一个便捷操作的Android可视化规范检查》,想了解的老铁可以看一看,本篇文章不做过多介绍,当时只介绍了工具的使用,没有介绍相关功能的开发过程,后续,有时间了,我会整理开源出来,一直忙于开发,老铁们,多谅解。这个可视化的检查工具,虽然大大提高了检查效率,也节省了人力和时间,但有一个潜在的弊端,就是,只能检查提交之后的代码是否符合规范,对于提交之前没有进行检查,也就说,在提交之前,规范也好,不规范也罢,都能提交上来,用工具检查后,进行修改,更改不规范的地方后然后再提交,只能采取这样的一个模式检查。


这样的一个模式,比较符合,最后的代码检查,适用于项目负责人,开发Leader,对组员提交上来的代码进行规范的审阅,其实并不适用于开发人员,不适用不代表着不可用,只不过相对流程上稍微复杂了几步;应对这样的一个因素,如何适用于开发人员,方便在提交代码之前进行规范检查,便整体提上了研发日程,经过几日的研究与编写,一个简单便捷的Android端Git提交专有hook,便应运而生了。


说它简单,是因为不需要编写任何的代码逻辑,只需要寥寥几步命令,便安装完毕,通过配置文件,便可灵活定制属于自己的检查范围。


为了更好的阐述功能及讲述实现过程,便于大家定制自己的开发规范,再加上篇幅的约束,我总结了四篇文章来进行系统的梳理,还请大家,保持关注,今天这篇,主要讲述最终的开发成果,也就是规范工具如何使用,规范这个东西,其实大差不差,大家完全可以使用我自己已经开发好的这套。


这个工具的开发,利用的是git 钩子(hook),当然也是借助的是Node.js来实现的相关功能,下篇文章会详细介绍,我们先来安装程序,来目睹一下实际的效果,安装程序,只需要执行几步命令即可,无需代码介入,在实际的开发中需要开发人员,分别进行安装。


安装流程


1、安装 Node.js,如果已经安装,可直接第2步:


Node.js中允许使用 JavaScript 开发服务端以及命令行程序,我们可以去官网nodejs.org
下载最新版本的安装程序,然后一步一步进行安装就可以了,这个没什么好说的,都是开发人员。


2、安装android_standard


android_standard是最终的工具,里面包含着拦截代码判断的各种逻辑 在项目根目录下执行如下命令:


npm install android_standard --save-dev

执行完命令后,你会发现,你的项目下已经多了一个目录,还有两个json文件,如下图所示:


image.png


node_modules,用来存放下载安装的包文件夹,里面有我们要使用到的功能,其实和Android中lib目录很类似,都是一些提供功能的库。


package.json文件,是配置文件,比如应用的名字,作者,介绍,还有相关的依赖等,和Android中的build.gradle文件类似。


3、创建git配置文件,执行如下命令


node node_modules/android_standard/gitCommitConfig

命令执行成功会返回如下信息:


image.png


此命令执行完后,会在项目根目录下创建gitCommitConfig文件,这个文件很重要,是我们执行相关命令的配置文件,内容如下,大家可以根据自己实际项目需要进行更改。


项目下生成gitCommitConfig.android文件,.android是我自己定义的,至于什么格式,等你自己开发的时候,完全可以自定义,是个文件就行。


image.png


打开后,文件内容如下,此文件是比较重要的,后续所有的规范检查,都要根据这个文件里的参数来执行,大家在使用的时候,就可以通过这个文件来操作具体的规范检查。


image.png


4、更改执行文件,执行如下命令


执行文件,就是需要在上边生成的package.json文件,添加运行程序,使其在git提交时进行hook拦截。


node node_modules/android_standard/package

5、添加git过滤


因为执行完上述命令后,会产生几个文件,而这几个文件是不需要我们上传到远程仓库的,所以我们需要在.gitignore文件里添加忽略,直接复制即可。


/node_modules
package.json
package-lock.json
gitCommitConfig.android

6、后续如果有更新,可命令进行操作:


注:此命令在更新时执行


npm update android_standard --save-dev

7、删除操作


注:后续不想使用了,便可执行如下命令:


npm uninstall android_standard --save-dev

具体使用


通过上述的安装流程,短短几个命令,我们的规范检查便安装完毕,后续只需要通过gitCommitConfig.android文件,来动态的更改参数即可,是不是非常的方便,接下来,我们来实际的操作一番。


关于配置文件的相关参数,也都有注释,一看便知,这里简单针对最后的参数,做一个说明,也就是gitCommand这个参数,true为工具,false为命令方式;true也好,false也好,在主要的功能验证上,没有区别,唯一的区别就是,命令行的方式提交,会有颜色区分,后面有效果。


image.png


我们先来看下命令行下的执行效果,当配置文件开关gitCommitSwitch已开,并且gitCommand为false,其他的配置参数,大家可以根据需要进行改动,在代码提交的时候如下效果:


image.png


在Android studio中提交代码执行效果


image.png


TortoiseGit提交代码执行效果:


image.png


目前呢,针对Android端的规范检查,无论是java还是Kotlin,还是资源文件,都做了一定的适配,经过多方测试,一切正常,如果大家的公司也需要这样的一个hook工具,欢迎使用,也欢迎继续关注接下来的相关实现逻辑文章。


好了各位老铁,这篇文章先到这里,下篇文章会讲述,具体的实现过程,哦,忘记了,上篇结尾的遗留组件化还未更新,那就更新完组件化,接着更新这个,哈哈,敬请期待!


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

做个清醒的程序员之成为少数派

阅读时长约10分钟,统计2604个字。 这是一篇插队的文章。 本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。 周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,...
继续阅读 »

阅读时长约10分钟,统计2604个字。


这是一篇插队的文章。


本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。


周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,最近更新比较频繁,加了很多人。这位老兄一上来先是肯定了我的文章,随后指出了文中的错误。坦率地讲,自从复活博客之后,这还是第一位指出我错误的朋友,一下子我就来了兴趣。


在本系列文集的《序》中,我原文是这样写的:



我一直奉行一句话:“有道无术,尚可求也;有术无道,则止于术”。这句话出自老子的《道德经》,而且很好理解。



他指出《道德经》里其实没有这句话。但是呢,本着对读者负责的态度,我在写文章的时候确实去查了一下。程序员这个职业大家都懂,比较较真,至少我是这样的。于是我就找到了一些依据,来证明我说的是对的。但很快便发现事实其实不是这样,这位老兄所言非虚,我引的这句话确实并不出自《道德经》。所以,我要在这里向所有读过我上篇文章的朋友道个歉。澄清一下:“有道无术,尚可求也;有术无道,则止于术”,尽管这句话有几分道理,但真的不是《道德经》原文。


好了,故事就到这里结束了。说到这,大家应该也能理解我为什么要把这篇文章拿来插队。一方面趁热打铁,有错误及时声明,另一方面这个故事对我有新的启发。


这位老兄,名为张鸿羽。稍加细聊后,我得知鸿羽兄是有背过原文的,而我没有。我只是看到大部分都这样说,便信以为真,然后也跟着这样说。显然,我成为了大多数人中的一份子。而鸿羽兄是少数派中的一份子。有时候,真理真的掌握在少数人手中。


回想过去几年的工作历程,特别是刚开始工作的那几年,我做的很多工作都是“探索型”的。所谓“探索型”,就是对新技术,或者说是公司的研发部门未曾使用过的技术进行尝试摸索。当然,尝试新技术之前,要能发现新技术。而一项新技术的诞生,总会伴随着官方的宣传,以及一些支持它、拥护它的人高声叫好。但只有真正尝试过,特别是用新技术来实现较为复杂系统的时候,才会知道这项新技术到底优势在哪,劣势又在哪。


诚然,如果让我来总结我尝试新技术、新框架的经验,我会说:大部分新技术或是框架确实弥补了已有框架的不足,但其弥补的程度往往并不是质变的,只是小步优化。甚至有些新兴技术在弥补的同时,还引入了其它的问题。这对于使用它的开发者来说,的确是个坏消息。


但话说回来,没尝试用过,又怎能知道答案呢?技术的发展本就是这样一步一个坎,有时候走一步还退两步的呀。


这或许就是我等软件开发者的宿命,对于现存的技术框架,总是有这样或那样的不满意,觉得用着不顺手。期盼着某一天,某个技术大佬,或者团体,发明了一种新的框架,甚至是新的编程语言。或是直接起义,自己创造一款新的技术框架,能真正地解决那些令我们不满的问题,让软件开发编程成为真正的享受。


但现实是,很多新的技术框架的诞生,都伴随着类似的口号。也总会有勇敢的开发者尝鲜,也总会经历被坑,然后不断填坑的过程。而这些敢于尝鲜的开发者,就是那些最终会成为“少数派”的人。他们知道在各种美好的宣传背后,隐藏着多深的坑。对于这些坑,又该用什么方法去填。


“少数派”或许才是那些头脑最清醒的那一小撮人群。


但是,成为“少数派”不仅意味着失败的尝试,还有大多数人的不理解。甚至更严重一些,就是诋毁,百口莫辩。这需要一颗强大的内心,和与时间做朋友的勇气以及态度。


不过,我为什么鼓励程序员要做“少数派”,而不是成为“大多数”呢?还有另外一个原因,那就是由行业特征决定的。我相信程序员大多都活跃在互联网行业,这个行业是赢家通吃的指数型结构。有点类似财富分配,大部分的财富掌握在少数人的手里。而且无论如何数学建模,或是提高那些穷人的初始资金,最终推演的结局依然如此。


如今,在中国,乃至全世界,所谓“互联网大厂”无非就是那几家,而剩下的呢?数字上远远超过我们熟知的那些大厂,但拥有的财富值却位于指数图表中的长尾之中。这就是指数型的行业的特征,也是程序员这个群体的特征。


如果大家有查相关的数据,可以发现优秀程序员的工作效率往往是普通程序员的好几倍,尽管薪水上的差距不是这样。而大多数都是普通程序员,优秀程序员只属于“少数派”。优秀程序员,拿到需求,会做足够的分析,到了动手的时候,则像个流水线的工人;普通程序员,拿到需求就想赶快动手,面临的有可能是回炉重造。优秀程序员,会充分考虑到使用场景,采用防御式编程来规避可能带来的缺陷;普通程序员,想的只是实现需求,把程序健壮性扔给测试人员。优秀程序员,会考虑代码的可读性,为代码添加合适的注释、每个方法或函数的功能单一、清晰;普通程序员,急于求成,不注重代码规范,导致日后维护困难……


但是,追求效率和追求质量,大多数公司都会选择前者。但做多和做好,结果往往相差甚远。


大部分人倾向于做多、扩张、追求规模化。但殊不知做大的后果往往是成本的上升,利润却不一定变高。但做好却不一样,它追求的是平衡收支,而不是盲目追求利润。更好的做法其实是在做好之前,不要做大。要相信好产品,自然会带来口碑。过分追求大规模,反倒会使高利润远去。而把事情做好的心态,看似发展得慢,实则是条捷径。


回顾我创作的历程,之前的我总想着多写,多写就是扩张,意味着规模。但这种心态往往做不出好书,因为这是效率当先,质量次之的做法。但我身边也有的人,创作很用心,不着急让书早日面试,很认真地创作,比我的速度慢一些。这便是把事情做好的心态。你猜结果如何?人家一年十几万的稿酬,我却只有可怜的几万块。


所以,上面那套理论并不是我胡乱写的,或是从哪本书里看到,就抄过来的。而是真的付出了血和泪,总结出的道理。在此,我劝你做个“清醒”的人。追求效率没错,一旦做得过火,则会适得其反。


另一方面,如果只想成为大多数,可不可以呢?当然也可以,只不过互联网行业或许不再适合。那些符合正态分布的行业才是想成为大多数的那类人的理想去处。


比如,餐饮行业。现在,大家可以想一想,有没有那家餐馆,或是哪个餐饮品牌,能做到赢家通吃?似乎没有,如果也去查这方面的数据,就会发现餐饮行业其实并不是指数分布,而是呈正态分布的。只要能做到普通中位数的水平,就OK了。


真正的高手一般都是“少数派”。他们不仅能力拔群,思考问题时的方法、对世界的认知和一般人都有区别。若要成为软件开发工程师中的“高手”,必须成为“少数派”

作者:萧文翰
来源:juejin.cn/post/7214855127625302053
,成为战场上的传说。

收起阅读 »

22年回家,治好了我的精神焦虑,终于睡了一周的好觉

2023-1-29 脑海的片段:看灿姐打麻将,拍全家福的情景,在奶奶家吃饭,奶奶给我钱,看外婆,去老街,爬山去龙泉寺,妈妈的眼睛,妈妈打呼噜,电梯外的妈妈,大双给奶奶夹菜,小双给豪儿倒洗脚水,妈妈玩抖音,给妈妈染头发 过年回家,在高铁站等了3个小时,没有带孩子...
继续阅读 »

2023-1-29


脑海的片段:看灿姐打麻将,拍全家福的情景,在奶奶家吃饭,奶奶给我钱,看外婆,去老街,爬山去龙泉寺,妈妈的眼睛,妈妈打呼噜,电梯外的妈妈,大双给奶奶夹菜,小双给豪儿倒洗脚水,妈妈玩抖音,给妈妈染头发


过年回家,在高铁站等了3个小时,没有带孩子,两个人在哪里应该都过得挺好,带着孩子,还得管他吃喝,睡觉,妈耶,真累。坐上火车,第一次坐软卧,有门,两层,床比较大,有靠背,说是以前有拖鞋。但是也不够我和儿子睡的。在上铺又总感觉要掉下来。接着好几天没睡好,半夜醒来,心慌,涌出好多口水,想吐。有点吓着了,从来没这样过,真怕自己的心脏有毛病。而且老贺把40度左右的水给我泡酸辣粉,我不想他难受,吃了半碗。好像是到湖北的某个站,上来两个重庆人,很大声,后来那个中年男人还问晨晨几岁了,我才反应过来,这是重庆人,声大,但是每个人都还是热心肠的,看着两个人好像吵架一样,其实他们在聊天,哈哈哈


到了重庆,中午了,我实在是不想洗漱了,带孩子去哪里,都累。下车去了观音桥,看着外面好像不咋的,进去人挤人,全是好吃的,要不是等会要吃美娃鱼,我觉得我可以把每个都吃个遍。看见了红姐一家,聊天下来,我发现红姐也真的是为人母了,围着孩子转,会为了家里的琐事妥协,以前吐槽的老公,在她那也是很爱的,看得出来。可可很可爱,我能感受到一点有女儿的感觉,哈哈哈


2d87fb7c74cd7cd0443ae32725b3331.jpg


1f427cb7d2a0ac9ac3a0c0da1ee11ac.jpg


e4c382de28bf282462671199173e461.jpg
永川下车,打车回家就是8点多了,回家,一路上就别寻思钱就对了,哪哪都是钱。爸爸妈妈说是等我们吃饭,两年没回家了,每次回去,还是觉得就在昨天,周围一切在改变,又好像没变。到家,老妈就开始拍抖音,这是我没有想到的。不过她有自己的世界,乐趣,也挺好的。我说要吃夹沙肉,妈妈爸爸自己做的红豆馅的馅,应该花了不少时间,很好吃,就是不敢多吃了。家里很漂亮,妈妈开始像我展示各种东西,她买的好货,哈哈哈,儿子玩上了,外婆买的垃圾车。当然两个弟弟还是玩手机,看看啥时候开始有改变


晚上睡得很香,3个人都睡得很香,好几个月没有这种感觉了,睡完觉起来,世界都那么清晰,上了厕所就转进了妈妈的被窝。听妈妈说,听妈妈抱怨。第一天,我还是可以挺住的。妈妈更唠叨了,哈哈哈。在天津,是儿子一天800遍的叫妈妈,现在又多了800遍“莎莎”。事实证明,妈妈老了,需要有人关注了。


21号,30,过年,小插曲,对于我来说,不在意,都不算事儿,我只记得菜很好吃。特别是牛肉,下午吃到塞牙,扣了好久。上午买了烟花,买的时候,两只说不感兴趣,结果晚上玩的好嗨,明年一定要多买点


22号,初一,赶集,一家人去赶集,老爸先带去买气球了,一个可爱的草莓气球,贺小狗自己选的。赶集的人不多,没有以前热闹了,卖烟花的,开始卖纸钱,火炮了。走了一圈去火锅店吃牛肉火锅。本来没抱多大期待,结果好好吃,吃了一盘子牛肉。下午跟妈妈去田间摘了好多青棉花,奈何肚子天天不消化,不然还可以吃青团,下午出去走走,也是开心的,听妈妈吐槽爸爸,带儿子玩泥巴。妈妈还说拿钱给我还贷款,我很感动,我不要她的钱,来的很辛苦,自己留着点,以后养老不用愁。


d13c91f66201c308d98bce12f715a01.jpg


背砍很多青棉花


23号,初二,本来每年初二去舅舅家,今年改成了初三。晚起的早晨,睡得依旧的香。今天给妈妈染头发,爸爸做完饭,吃完打麻将去了,我给妈妈染头发,洗头发,还算成功,妈妈开心,我就开心。我们下午决定去走走,小米跟着我们,他要是知道要走8公里,肯定不会跟我们走了,去田间走走,好舒服。路过观音菩萨,土地公公,走了很多坟堆的山里,可惜没有转角走向龙泉寺。明年补上。我给观音菩萨求妈妈睡得好,希望有用。

2fbfd1d965b24ef0da89d0df4f6b330.jpg


去龙泉寺的路


94bd173a9f6501855b10341346dc7c4.jpg


老公说瓦房就是,天快黑了,没继续下去了


24号,初三,早起了,因为爸爸说早上8点出发,结果好像就我当真了,早起,洗头,我还做了一个瑜伽。我们坐车,爸爸妈妈骑车,社恐的我,喜欢热闹,又不知道怎么与人 交流。上坟,好像是我每年比较期待的事,大家聚在一起去看望已故的老人,热闹之余,想念外婆,从她走了起,我每次去她那,都会给她磕三个头,不是秀,是真想她。灿姐喜欢声张,我不在乎别人说什么,我只在乎我心里有她,磕不磕头,是我心里想做的,不是要别人看的。我以为这么久,我会忘记,好像越来越想她了。《寻梦环游记》里说,生的人,没有人记得你,已故的老人就会消失,所以我的外婆应该还没有,因为我还记得她。去了一直害怕的老宅子看,梦里那么真实,那么黑,和老公去看的时候,仿佛在逛博物馆,不害怕了,多了几分怀念。外面的大门没了,被杜老师占了修了栋房子,里面关着鸡,进门两边的棺材没了,因为老人都走了。天井小时候感觉蛮大,中间的台,要走好多步,现在几步就跨过去了。两边的台子没有了,左边的台子是用来洗衣服的,右边的台子是坐着旁边的老人,她天天会在那里叹息,撑唤(一身疼,叫唤),骂人,使唤她老头子。右边的大门锁上了,人家搬出去了,我还记得那户人家的男的姓曾,女的姓贾(重庆话就是真假)。桃屋(饭厅)也小了,就剩下被舅舅烧纸烧黑的外公的遗照,还有边上慈祥的外婆,一个香炉,几根没烧完的香。睡觉的屋,没有了床,都空了,墙被凿了一个大洞,屋里亮了,不吓人了。下面的大屋,中间的灯绳子还在,小时候跑进去,大概知道绳子在哪里,摸一把没摸着就要跑出去,太黑了,太害怕了。天井旁边的窗户还在,以前我和外婆在那砍红薯,熬猪食。厨房都没了,没了。路过一个空屋子就到了现在舅舅住的楼房,大坝子放了几张桌子,是往年的热闹场景。吃饭前,和老公去了小时候上学走的路,还是那个小河,竹林,再也不用在田坎上跑着去上学,却有点怀念跑着去上学的时候。路走不通了,上面成了一片橘子地。坐在坝子里吃饭,真香,踏实。她们打麻将去了,我和老公去了三姨对面的路,去老街,杜老师说对面可以走去老街。对面路不太熟悉,过河的桥还是玉子板。路修成了大道,唯一不好的是有好多野狗,不敢走,老奶奶说带路,不好意思让她带那么远,我们走了老奶奶说的另一条田间小路,看见了田里的老牛,也是惬意。我们拿着两根棍子走了一道。老街的小学,旁边修了新的,老的还在,是以前一样的,不知道那个像保剑锋,去他家排练,他把衣服内衣裤子扔门后面的语文老师还在那个学校不,哈哈哈哈。老街的东西,老建筑都垮了,转转去了新街,好近,以前好像要走很久,现在不到10分钟就到。我们吃了葱油饼和凉面,凉面是那个味,酸甜辣,4块钱一碗,太便宜了。

e6d9a0829d22c82e9555feeb58236eb.jpg


橘子地


e2b35aada3b72a52092e9e324643865.jpg


玉子板的桥,30多年了吧


微信图片_20230201133535.jpg


耕地的牛


微信图片_20230201133517.jpg


去老街路上


微信图片_20230201133549.jpg


中间的灯绳,小时候的噩梦


5bd221108042713193a315bede266f8.jpg


天井旁边的窗户


微信图片_20230201133556.jpg


有大洞的屋,不吓人了


微信图片_20230201133559.jpg


老街的折叠门,最早的折叠门了吧


25号,初四,今天我们家请客,来了好多人,爸爸总是能一个人做很多菜出来,还挺好吃,肚子不争气,吃不了多少,去帮妈妈修了戒指,没想到我的手指比她粗那么多,还是大了。回来看她们打麻将,大家一起聊了些有的没的,比我想的亲切多了,特别是灿姐,好像姐妹亲更多了些,还有就是小双,他甘愿给豪儿倒洗脚水,豪儿说热了,他赶紧给加凉水。我有点不解,我在想,是不是每个人都有自己的圈子,就是学数学的圈子,交集,并集那种圈子,我们在他的并集区域里,不在他的交集区域,有点意思。他应该是算我们家最任性的人,孩子这样挺好的,长大之前至少任性过,这才是小孩子。

26号,初五,定了快3年的全家福,终于要拍上了。早起的我洗头,敷脸。热饭,跟三姨聊了会,她总是能让人觉得她可怜,或许她就是有点可怜吧,但是她又是那么自私,喜欢占便宜。说我去年初一走,聪明,省了200块钱的事,翻篇了,我不计较了。我生怕爸妈弟弟有一点不配合,急眼,这个全家福拍不上。回家一直没有太照顾老公,我感觉他懂我,也确实很理解我,没陪他,他一切都安排的很好,这点我很开心。还好妈妈没有晕车,下车,我们爬了一会山,到了山腰的影楼。有点简陋,一个区里,也还行啦。没想到给我画那么久,我的初衷是让爸妈好看点,仔细点,毕竟下次再让他们来拍,估计很难。妈妈穿婚纱真美,但是我却好想哭,不知道为什么。妈妈是老了,眼睛开始耷拉下来了,生完一场大病,右眼掉的厉害,我让化妆师再贴一个双眼皮贴,对称一下。拍照的地方好冷,可别给我妈妈感冒了,对了,忘记说,中午他们吃泡面的时候,还蛮温馨的,哈哈哈。特别感谢我老公,在我化妆的时候,给他们拍的照片,很好,其实这就算是我的生日礼物了,真的就想拍全家福。两个弟弟真是长高了,小的胖的可爱,大的帅气,爸爸年轻,妈妈美丽。一路下来,挺开心的。爸爸应该也是开心的,我看出来他尴尬了,笑的有点不自然,逗一逗就笑的开心了。他还拿手机照了一些,也是开心的,一家人都笑着拍完一组,除了不配合的儿子,不强求他们拍第二组了。这样开心的收尾挺好的。妈妈担心没拍完,我说拍多了,他们照片多了加照片要多收钱,她就放心了。跟着老公这些年,我慢慢学会了,解释,沟通,交流,站在对方的角度,再有点耐心,其实妈妈就懂了,像个孩子。只是我们身上的琐事太多,耐心在消耗,跟家人,反而多了些任性。下山大家有说有笑,挺快就到了公交点,爸妈希望去店里看看,两个弟弟肯定是不想走了,开心收尾挺好的,我们回家了。到了爷爷家,由于大伯一家没有来,没人做饭,妈妈生气,爸爸就是踏实干活。锅很黑,煤气罐没有气了,拿着大锅上外面炒的。大双烧火,爸爸炒菜,我和妈妈端菜,奶奶找盘子,爷爷站屋里用有些黑的水洗碗,儿子拿着竹子块在坝子里玩,老公时而拍照录像,时而站在那看爷爷弯腰洗碗,他好奇87岁的老人,弯腰低头洗碗,居然头不晕,这就是劳动人民的腻害之处。别说,虽然到处是鸡屎,炒出来的菜,真的蛮香的。我们坐在浓浓鸡屎味,有些霉臭,旁边是洗完碗装着污水的盆,满桌子的菜,大双吃的很香,奶奶来了,他让他涮涮有葱的碗,给她夹了够不着的豆干,他是个善良的孩子,抛开玩手机不说,其实也是个懂事的孩子。老公吃了好多,喝了蜂蜜酒,好像是他爷爷,哈哈哈。我吃了半碗饭,豆干,豆角,和鱼里的菜,其实挺好吃的。别的不重要,爷爷笑咪了眼睛。我和他们的回忆不太好,没有什么深厚的感情,但是长大后,每次去他们那,会觉得温暖,淳朴。他们也希望我去看他们。那晚,妈妈很开心,睡得很香,我们一起睡得,她摆了一会龙门阵,说这么晚了,快睡觉了,明天要早起。开始打呼噜了。希望你每晚都睡那么香,多好。


28215f5ad2d11c633a1d2206471fe55.jpg


爷爷坝子里做饭


微信图片_20230201134322.jpg


准备拍全家福啦


微信图片_20230201134326.jpg


先偷拍一点


微信图片_20230201134329.jpg


双胞胎,一点不像的弟弟


微信图片_20230201134332.jpg


双胞胎,一点不像的哥哥


27号,初六,早上的票很早,以后得长教训,早点订票了。本以为这次中午的票,可以消停的走,但是我一点不着急,感觉肯定有办法,或许内心不想走吧,不然早就着急了。吃了3碗面,真香,可惜没吃上妈妈做的肥肠鱼,好多菜,看五一吧,能不能回去一趟。大概率不行。回去他们也不放假,在管子里也算不上陪伴了。妈妈每次走的时候,都会说,回来都是晚上,走的都是早上,烧白没吃上,肥肠没吃上,土鸭子鸭杂专门给你留的,也没吃上,冰箱里的笋子也没吃上。。。。即使我在家待一个月,也会那么说,就是舍不得我走,巴不得把家里能带的都给我装下,能装的我都装上了,那是妈妈的爱。晚走一小时,好像也没晚多少,妈妈送到电梯那,我知道她为什么不下去,不是懒,是她要哭。爸爸送我们到车站,走的时候,我把贺小狗做好了,赶紧摆了摆手,生怕车快了,爸爸看不到我了,他看着呢,一直盯着呢,没舍得走,直到车走远。。。。又要在车站等3个小时,带着这只狗,咋熬,冰淇淋只撑了十几分钟。老公问我想要什么生日礼物,我开玩笑的说我想回家,真的只是玩笑,不知道为什么留下了泪水,可能家在内心深处吧。

22年回家,治好了我的精神焦虑,终于睡了一周的好觉


作者:用户5176977956251
来源:juejin.cn/post/7194623390991253541
收起阅读 »

从0搭建nestjs项目并部署到本地docker

web
开发目标:快速搭建nestjs项目本地环境,并测试本地打包方便后期部署到服务器。 项目准备:node环境、npm依赖、docker 创建项目并启动 使用typeorm连接mysql 使用class-validate校验入参 使用全局filter处理异常,使用...
继续阅读 »

开发目标:快速搭建nestjs项目本地环境,并测试本地打包方便后期部署到服务器。


项目准备:node环境、npm依赖、docker



  1. 创建项目并启动

  2. 使用typeorm连接mysql

  3. 使用class-validate校验入参

  4. 使用全局filter处理异常,使用全局interceptor处理成功信息

  5. 使用ioredis连接redis

  6. 使用swaager文档

  7. 使用docker-compose打包并运行

  8. 总结


一、创建项目并启动


1、全局安装nestjs并创建项目

npm i -g @nestjs/cli
nest new nest-demo

2、使用热更新模式运行项目

npm run start:dev

此时访问 http://localhost:3000就可以看到 Hello World!


3、使用cli一键生成一个user模块

nest g resource system/user

选择REST API和自动生成CURD


4、设置全局api前缀

src/main.ts


async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // 设置全局api前缀
await app.listen(3000);
}
bootstrap();

更多nestjs入门教程查看:# 跟随官网学nestjs之入门


二、使用typeorm连接并操作mysq


1、安装依赖

npm i @nestjs/typeorm typeorm mysql @nestjs/config -S

2、在src下创建 config/env.ts 用来判断当前环境,抛出配置文件地址

src/config/env.ts


import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV == 'prod';

function parseEnv() {
const localEnv = path.resolve('.env');
const prodEnv = path.resolve('.env.prod');

if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
throw new Error('缺少环境配置文件');
}

const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
return { path: filePath };
}
export default parseEnv();

3、在src下创建.env配置文件

src/.env


# default
PORT=9000

# database
DB_HOST=localhost
DB_PORT=3306
DB_USER=demo_user
DB_PASSWD=123456
DB_DATABASE=demo_db


4、在app.module内挂载全局配置和mysql

src/app.module.ts


import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import envConfig from './config/env';
import { AppService } from './app.service';
import { UserModule } from './system/user/user.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 设置为全局
envFilePath: [envConfig.path],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST', 'localhost'), // 主机,默认为localhost
port: configService.get<number>('DB_PORT', 3306), // 端口号
username: configService.get('DB_USER', 'root'), // 用户名
password: configService.get('DB_PASSWORD', '123456'), // 密码
database: configService.get('DB_DATABASE', 'test_db'), //数据库名
entities: ['dist/**/*.entity{.ts,.js}'],
timezone: '+08:00', //服务器上配置的时区
synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭
autoLoadEntities: true,
}),
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

5、定义userEntity实体

src/system/user/entities/user.entity.ts


import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user_tb')
export class UserEntity {
@PrimaryGeneratedColumn()
s_id: string;

@Column({ type: 'varchar', length: 20, default: '', comment: '名称' })
s_name: string;

@Column({ type: 'int', default: 0, comment: '年龄' })
s_age: number;
}

6、user.module内引入entity实体

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
// 引入typeorm和Enetiy实例
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [UserService],
}
)

export class UserModule {}

7、在控制器user.controller修改api地址

@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}

地址拼接为:全局前缀api+模块user+自定义create = localhost:3000/api/user/crtate


image.png


image.png


三、使用class-validato校验入参


1、安装依赖

npm i class-validator class-transformer -S

2、配置校验规则

src/system/user/dto/create-user.dto.ts


import { IsNotEmpty } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty({ message: '名称不能为空' })
readonly s_name: string;
}

image.png


更多校验规则查看:git文档


四、使用filter全局错误过滤、interceptor全局成功过滤


1、使用cli自动生成过滤器


nest g filter common/http-exception
nest g interceptor common/transform

2、编写过滤器


src/common/http-exception/http-exception.filter.ts


import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码

let resultMessage = exception.message;

// 拦截class-validate错误信息
try {
const exceptionResponse = exception.getResponse() as any;
if (Object.hasOwnProperty.call(exceptionResponse, 'message')) {
resultMessage = exceptionResponse.message;
}
} catch (e) {}

const errorResponse = {
data: null,
message: resultMessage,
code: '9999',
};

// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}

src/common/transform/transform.interceptor.ts


import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: '0000',
msg: '请求成功',
};
}),
);
}
}

3、在main.ts里挂载


import { HttpExceptionFilter } from './common/http-exception/http-exception.filter';
import { TransformInterceptor } from './common/transform/transform.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter()); // 全局注册错误的过滤器(错误异常)
app.useGlobalInterceptors(new TransformInterceptor()); // 全局注册成功过滤器
await app.listen(3000);
}
bootstrap();

手动抛出异常错误只需在service的方法里


throw new HttpException('message', HttpStatus.BAD_REQUEST)


五、使用idredis连接redis


1、安装依赖

npm i ioredis -S

2、在.env文件添加reids配置

# redis
REDIS_HOST=localhost
REIDS_PORT=6379
REIDS_PASSWD=
REIDS_DB=3

3、在common目录下创建cache模块,连接redis

nest g mo cache common && nest g s cache common

src/common/cache/cache.service.ts


import { Injectable, Logger } from '@nestjs/common';
import { Redis } from 'ioredis';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class CacheService {
public client;
constructor(private readonly configService: ConfigService) {
this.getClient();
}

async getClient() {
const client = new Redis({
host: this.configService.get('REDIS_HOST', 'localhost'), // 主机,默认为localhost
port: this.configService.get<number>('REIDS_PORT', 6379), // 端口号
password: this.configService.get('REIDS_PASSWD', ''), // 密码
db: this.configService.get<number>('REIDS_DB', 3),
});
// 连接成功提示
client.on('connect', () =>
Logger.log(
`redis连接成功,端口${this.configService.get<number>(
'REIDS_PORT',
3306,
)}
`
,
),
);
client.on('error', (err) => Logger.error('Redis Error', err));

this.client = client;
}

public async set(key: string, val: string, second?: number) {
const res = await this.client.set(key, val, 'EX', second);
return res === 'OK';
}

public async get(key: string) {
const res = await this.client.get(key);
return res;
}
}

在cache.module内抛出service
src/common/cache/cache.module.ts


@Module({
providers: [CacheService],
exports: [CacheService],
})

4、在user.module内引入cacheModule并在user.service内使用

src/system/user/user.module.ts


import { CacheModule } from 'src/common/cache/cache.module';
@Module({
imports: [CacheModule],
controllers: [UserController],
providers: [UserService],
})

export class UserModule {}

src/system/user/user.service.ts


import { CacheService } from '@src/common/cache/cache.service';

@Injectable()
export class UserService {
constructor(
private readonly cacheService: CacheService,
) {}

async create(createUserDto: CreateUserDto) {
const redisTest = await this.cacheService.get('redisTest');

Logger.log(redisTest, 'redisTest');
if (!redisTest) {
await this.setRedis();
return this.create(createUserDto);
}

...
}
async setRedis() {
const res = await this.cacheService.set(
'redisTest',
'test_val',
12 * 60 * 60,
);
if (!res) {
Logger.log('redis保存失败');
} else {
Logger.log('redis保存成功');
}
}
}

image.png


image.png


六、使用swagger生成文档


1、安装依赖

npm i @nestjs/swagger swagger-ui-express -S

2、在main.ts引入并配置

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 设置swaager
const options = new DocumentBuilder()
.setTitle('nest-demo example')
.setDescription('The nest demo API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('swagger', app, document);

...
}
bootstrap();

此时访问http://wwww.localhost:9000/swagge就可以看到文档


image.png


3、在控制器为业务模块和api打上标签

src/system/user/user.controller.ts


import { ApiTags, ApiOperation } from '@nestjs/swagger';

@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@ApiOperation({
summary: '创建用户',
})
@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}

4、在dto内为字段设置名称

src/system/user/dto/create-user.dto.ts


import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
@ApiProperty({ type: 'string', example: '用户名称' })
@IsNotEmpty({ message: '名称不能为空' })
readonly s_name: string;

@ApiProperty({ type: 'number', example: '用户年龄' })
readonly s_age: number;
}

这时刷新浏览器,就能看到文档更新了


image.png


更多swaager配置查看:官方文档


七、使用docker-compose自动部署到本地docker


1、在根目录下创建docker-compose.yml

version: "3.0"

services:
# docker容器启动的redis默认是没有redis.conf的配置文件,所以用docker启动redis之前,需要先去官网下载redis.conf的配置文件
redis_demo: # 服务名称
container_name: redis_demo # 容器名称
image: daocloud.io/library/redis:6.0.3-alpine3.11 # 使用官方镜像
# 配置redis.conf方式启动
# command: redis-server /usr/local/etc/redis/redis.conf --requirepass 123456 --appendonly yes # 设置redis登录密码 123456、--appendonly yes:这个命令是用于开启redis数据持久化
# 无需配置文件方式启动
command: redis-server --appendonly yes # 开启redis数据持久化
ports:
- 6379:6379 # 本机端口:容器端口
restart: on-failure # 自动重启
volumes:
- ./deploy/redis/db:/data # 把持久化数据挂载到宿主机
- ./deploy/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf # 把redis的配置文件挂载到宿主机
- ./deploy/redis/logs:/logs # 用来存放日志
environment:
- TZ=Asia/Shanghai # 解决容器 时区的问题
networks:
- my-server_demo

mysql_demo:
container_name: mysql_demo
image: daocloud.io/library/mysql:8.0.20 # 使用官方镜像
ports:
- 3306:3306 # 本机端口:容器端口
restart: on-failure
environment:
MYSQL_DATABASE: demo_db
MYSQL_ROOT_PASSWORD: 123456
MYSQL_USER: demo_user
MYSQL_PASSWORD: 123456
MYSQL_ROOT_HOST: '%'
volumes:
- ./deploy/mysql/db:/var/lib/mysql # 用来存放了数据库表文件
- ./deploy/mysql/conf/my.cnf:/etc/my.cnf # 存放自定义的配置文件
# 我们在启动MySQL容器时自动创建我们需要的数据库和表
# mysql官方镜像中提供了容器启动时自动docker-entrypoint-initdb.d下的脚本的功能
- ./deploy/mysql/init:/docker-entrypoint-initdb.d/ # 存放初始化的脚本
networks:
- my-server_demo

server_demo: # nestjs服务
container_name: server_demo
build: # 根据Dockerfile构建镜像
context: .
dockerfile: Dockerfile
ports:
- 9003:9003
restart: on-failure # 设置自动重启,这一步必须设置,主要是存在mysql还没有启动完成就启动了node服务
networks:
- my-server_demo
depends_on: # node服务依赖于mysql和redis
- redis_demo
- mysql_demo

# 声明一下网桥 my-server。
# 重要:将所有服务都挂载在同一网桥即可通过容器名来互相通信了
# 如nestjs连接mysql和redis,可以通过容器名来互相通信
networks:
my-server_demo:

2、在根目录创建Dockerfile文件

FROM daocloud.io/library/node:14.7.0

# 设置时区
ENV TZ=Asia/Shanghai \
DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

# 创建工作目录
RUN mkdir -p /app

# 指定工作目录
WORKDIR /app

# 复制当前代码到/app工作目录
COPY . ./

# npm 源,选用国内镜像源以提高下载速度
RUN npm config set registry https://registry.npm.taobao.org/

# npm 安装依赖
COPY package.json /app/package.json
RUN rm -rf /app/package-lock.json
RUN cd /app && rm -rf /app/node_modules && npm install

# 打包
RUN cd /app && rm -rf /app/dist && npm run build

# 启动服务
# "start:prod": "cross-env NODE_ENV=production node ./dist/src/main.js",
CMD npm run start:prod

EXPOSE 9003

3、修改.env.prod正式环境配置

# default
PORT=9003
HOST=localhost

# database
DB_HOST=mysql_demo #使用容器名称连接
DB_PORT=3306
DB_USER=demo_user
DB_PASSWD=123456
DB_DATABASE=demo_db

# redis
REDIS_HOST=redis_demo #使用容器名称连接
REIDS_PORT=6379
REIDS_PASSWD=
REIDS_DB=3

4、修改main.ts启动端口

import { ConfigService } from '@nestjs/config';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService); // 获取全局配置
const PORT = configService.get<number>('PORT', 9000);
const HOST = configService.get('HOST', 'localhost');
await app.listen(PORT, () => {
Logger.log(`服务已经启动,接口请访问:http://wwww.${HOST}:${PORT}`);
});
}
bootstrap();

5、前台运行打包

docker-compose up


运行完成后大概率会报错,因为我们使用的mysql账号没有权限,所以需要进行设置


image.png


// 进入mysql容器命令
docker ecex -it mysql_demo /bin/bash
// 登录mysql
mysql -uroot -p123456
// 查询数据库后进入mysql查询数据表
show databases;
use mysql;
show tables;
// 查看user表中的数据
select User,Host from user;
// 刚创建的用户表没有我们设置连接的用户和host,所以需要创建
CREATE USER 'demo_user'@'%' IDENTIFIED BY '123456';
// 给创建的用户赋予权限
GRANT ALL ON *.* TO 'demo_user'@'%';
// 刷新权限
flush privileges;

如果还报错修改下密码即可
Pasted Graphic 1.png


ALTER USER 'demo_user'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

此时项目应该能正常启动并成功访问


image.png


image.png


6、切换后台运行

// Ctrl+C 终止程序后执行后台运行命令
docker-compose up -d

八、总结


docker-compose up正常用来测试本地打包,和第一次构建redismysql容器,后续需要在本地运行开发模式只需保证redismysql容器正常运行即可,如需再次打包,删除server容器和镜像再次执行即可


docker ps -a // 查询docker容器
docker rm server_demo // 删除server容器
docker images // 查询镜像
docker rmi nest-demo_server_demo // 删除server镜像, server镜像名称:项目名称_容器名称
docker-compose up -d // 重新打包

本地开发模式只需关闭server容器,然后在项目内只需 start:dev即可


docker stop server_demo
npm run start:dev

作者:jjggddb
来源:juejin.cn/post/7215844385614528549
收起阅读 »

keepAlive模式下切换页面时缓存页面中el-select已展开的选项框无法自动关闭解决方案

web
问题描述 如下图,在keepAlive缓存的页面中使用element中的select选择器,打开弹出框后不手动关闭,直接切换页面,会出现弹出框仍然展示在页面上的现象。 问题原因 select选择器提供一个属性 popper-append-to-body 为...
继续阅读 »

问题描述


如下图,在keepAlive缓存的页面中使用element中的select选择器,打开弹出框后不手动关闭,直接切换页面,会出现弹出框仍然展示在页面上的现象。


select-bug.gif


问题原因



  1. select选择器提供一个属性 popper-append-to-body 为false时,弹出框是放置在select选择器所在层级中,为true时,允许将弹出框插入至body元素中。


image.png


image.png



  1. 本页面被keepAlive缓存后 再切出本页面时不会触发select选择器组件的blur事件


所以当弹出框被插入至body元素中时,切出缓存页面 无法触发select选择器组件的blur事件,弹出框在body中无法隐藏


解决方法1


设置属性 popper-append-to-body为false,弹出框不会直接插入至body元素中,页面切换后弹出框也会被隐藏


局限性:
某些场景需要设置select选择器上级元素超出隐藏,弹出框如果超出上级元素的范围则无法完全展示


image.png


解决方法2


elementselect选择器源码中弹出框开启关闭由变量visible控制,将elselect组件包装一下,在deactivated生命周期钩子里设置弹出框关闭,注册组件时


image.png


// SelectWrapper 组件
<script lang="ts">
import { Mixins, Component, Watch } from 'vue-property-decorator';
import { Select } from 'element-ui';

@Component({
name: 'ElSelect',
})
export default class ElSelect extends Mixins(Select) {
visible: boolean | undefined;

deactivated() {
this.visible = false;
}
}
</script>

入口文件全局注册新的SelectWrapper组件,替换掉elementselect选择器,这样可以做到在业务组件中无感使用


 app.component('el-select', SelectWrapper);

作者:Eden的前端笔记
来源:juejin.cn/post/7215855138812461115
收起阅读 »

iframe之间的通信

web
前言 iframe 想必大家都挺熟悉的了,就不多说了👍👍。写这篇文章的初衷主要是丰富自己的知识和解决遇到的问题。因为我基本上没接触过 iframe ,所以对它的通信方式不是很了解。前几天,跟我的一个朋友(在下杨公子)聊天时,他提到了 iframe 的通信方式,...
继续阅读 »

前言


iframe 想必大家都挺熟悉的了,就不多说了👍👍。写这篇文章的初衷主要是丰富自己的知识和解决遇到的问题。因为我基本上没接触过 iframe ,所以对它的通信方式不是很了解。前几天,跟我的一个朋友(在下杨公子)聊天时,他提到了 iframe 的通信方式,我觉得很有意思,就开始了解和学习。在这篇文章中,我将分享我所学到的内容,希望对大家有所帮助🤪🤪。


接下来我们就一起来学习一下关于 iframe 通信的相关知识吧😁


iframe通信的几种方式😶‍🌫️😶‍🌫️



  1. URL 传参:父窗口可以通过在 iframe 的 src 属性后添加参数来向子窗口传递数据,子窗口可以通过 location.searchlocation.hash 来获取参数✨✨。



  • 使用 ? 拼接参数,子页面使用 location.search 接收参数


// parent.html
<iframe id="iframe1" src="./child1.html?name=来自parent的消息" frameborder="0"></iframe>

// child1.html
<script>
console.log(window.decodeURIComponent(location.search)) // ?name=来自parent的消息
</script>



  • 使用 # 拼接参数,子页面使用 location.hash 接收参数,同时还可以使用 window.onhashchange 来监听参数的变化。


// parent.html
<iframe id="iframe1" src="./child1.html#name=来自parent的消息" frameborder="0"></iframe>
<script>
const iframe1 = document.getElementById('iframe1')
// 在2s后更改hash
setTimeout(() => {
iframe1.src = './child1.html#age=12'
}, 2000)
</script>


// child1.html
<script>
console.log('hash', window.decodeURIComponent(location.hash)) // #name=来自parent的消息
window.onhashchange = () => {
console.log('hashchange', window.location.hash) // #age=12
}
</script>



⚡⚡需要注意的是通过 URL 传参 的时候,传输携带中文的话,记得使用 decodeURIComponent 进行解码。




  1. window.postMessage:安全、可靠且支持跨域的 iframe 通信方式,它可以在两个窗口之间异步传递消息✨✨✨✨✨。



  • 在发送方中,使用 window.postMessage() 方法向另一个窗口发送消息。该方法接收两个参数:要发送的消息和目标窗口的源(例如,"http://127.0.0.1:5500/child.html" 或 "*")。


window.postMessage('Hello world!', 'http://127.0.0.1:5500/child.html')


  • 在接收方中,使用 window.addEventListener() 方法监听 message 事件。该事件对象包含三个属性:data 表示接收到的数据,origin 表示发送方的源,source 表示发送方窗口的引用。


window.addEventListener('message', function(event) {
// 判断消息是否来自可信任的源
if (event.origin === 'http://127.0.0.1:5500/child.html') {
console.log('message: ' + event.data)
}
})

兼容性,来自 window.postMessage | MDN


image.png



  1. window.name:可以使用一个隐藏的iframe和window.name属性在不同的窗口之间共享数据✨✨。



  • 在子页面中,将要传递给父页面的数据保存在 window.name 属性中。


例如


window.name = 'Hello Parent!';


  • 在父页面中,创建一个隐藏的 iframe 元素,并且将其源设置为子页面的 URL


const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://127.0.0.1:5500/child1.html';
document.body.appendChild(iframe);


  • 在父页面中,等待 iframe 加载完成后,通过访问 iframe.contentWindow.name 属性来获取子页面中保存的数据。


iframe.onload = function() {
const childData = iframe.contentWindow.name;
onsole.log('message:', childData); // 输出:message: Hello Parent!
};


⚡⚡注意:使用 window.name 进行跨域 iframe 通信存在安全性问题,因为所有具有相同名称的窗口都可以访问和修改 window.name




  1. 服务器端转发:可以将消息从一个iframe发送到服务器,然后再由服务器将其转发到另一个iframe。✨✨✨



博客主要记录一些学习的文章,如有不足,望大家指出,谢谢。



作者:树深遇鹿
来源:juejin.cn/post/7215854856731934781
收起阅读 »

女朋友想学webGL修图,安排!

web
前言 看完小白可以用webgl实现修图功能!我们平常生活中都使用过adobe photoshop修图,各种各样的滤镜以及特效眼花缭乱,实现高斯模糊,雕刻,曝光等这些特效看起来似乎很难,那么今天我们来手敲一个简单实现。 之前讲了简单的webgl的原理与点的绘制、...
继续阅读 »

前言


看完小白可以用webgl实现修图功能!我们平常生活中都使用过adobe photoshop修图,各种各样的滤镜以及特效眼花缭乱,实现高斯模糊,雕刻,曝光等这些特效看起来似乎很难,那么今天我们来手敲一个简单实现。


之前讲了简单的webgl的原理与点的绘制、以及webgl在vscode需要注意的点,本文将接着介绍如何做个简单的修图功能,由于篇幅有限,只讲基本的语法、多边形绘制、缓冲区、帧缓存、纹理uv等。


预览


chrome-capture-2023-2-30.gif


canvas也可以更简单的实现,getImageData可以得到点的集合,然后putImageData绘制就行了。但是一些复杂的算法,例如高斯模糊、雕刻效果,貌似就没有webgl灵活了。


createBuffer 缓冲区


缓冲区你可以理解canvas的save()保存状态,但是这里我们一般是点的集合,这里我不会讲具体的api细节,但是知道具体的代码流程就行,就是创建buffer及数据 -> 绑定数据 -> 如何加载


let bufferOrigin = gl.createBuffer()
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, bufferOrigin);

gl.enableVertexAttribArray(positionAttributeLocation); // 告诉缓冲区怎么加载
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);

Program 对象


const canvas = document.querySelector("#canvas");
image.width = 540
image.height = 720

canvas.style.width = 540 + 'px'
canvas.style.height = 720 + 'px'

const gl = canvas.getContext("webgl");
if (!gl) {
return;
}
const program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-2d", "fragment-shader-2d"]);

image加载的dom对象,设置宽高,webglUtils是封装的方法,其实就是之前的初始化的着色器,返回program程序对象。


shader 着色器


先看看着色器源码


<script id="vertex-shader-2d" type="x-shader/x-vertex">
attribute vec2 a_position; // attribute在顶点着色器处理
attribute vec2 a_texCoord; // 纹理参数
uniform vec2 u_resolution; // 页面的坐标
attribute vec4 a_composeColor; // 纹理增强的向量
varying vec4 v_composeColor;

void main() {
// 屏幕坐标 -> 裁剪坐标
vec2 zeroToOne = a_position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;

vec2 clipSpace = zeroToTwo - 1.0;

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_composeColor = a_composeColor;
v_texCoord = a_texCoord;
}
</script>

attribute类型用于顶点着色器的属性,一般可以在后期动态添加一些控制,但是uniform是只能静态编译的时候就决定了,所以一般用于控制材质、光照等确定的值。为了能控制到片元着色器,那么一定要使用varying这个类型,一般通过变量传递给片元着色器做动态的渲染,所以一般会配合attriubute + varying


<script id="fragment-shader-2d" type="x-shader/x-fragment">
precision mediump float;

uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];
uniform float u_kernelWeight;
varying vec2 v_texCoord;
varying vec4 v_composeColor;

void main() {
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
// 卷积内核的前置处理,u_kernel我们传递的核心数据
vec4 colorSum =
texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
//.....
// 计算最终的颜色结果
gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1) * v_composeColor;
}
</script>


这里返回的结果gl_FragColor就是最终绘制的颜色。注意颜色范围是0到1需要做个转换,这里的最核心的也就是卷积的算法


卷积


卷积就是一个 3×3 的矩阵, 矩阵中的每一项代表当前处理的像素和周围8个像素的乘法因子, 相乘后将结果加起来除以内核权重(内核中所有值的和或 1.0 ,取二者中较大者)


image.png


像素矩阵 * 修改矩阵 = 赋值于内核也就是中心位置


这也就是我们能处理模糊、锐化等特效的原理, 下面是简单的计算


// 将周围八个点相加用于平均数相除
function computeKernelWeight(kernel) {
const weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}

 gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1) * v_composeColor;

滤镜


 const kernelsFilter = {
sharpness: {
name: '锐度',
data: [
0, -1, 0,
-1, 5, -1,
0, -1, 0
],
},
gaussianBlur: {
name: '高斯模糊',
data: [
0, 1, 0,
1, 1, 1,
0, 1, 0
],
},
edgeDetect2: {
name: '反相',
data: [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
],
},
emboss: {
name: '浮雕效果',
data: [
-2, -1, 0,
-1, 1, 1,
0, 1, 2
],
},
};

// 向量乘积的滤镜
const composeFilter = {
light: {
name: '曝光',
data: new Float32Array([1.2, 1.2, 1.2, 1])
},
langmanmeigui: {
name: '浪漫玫瑰',
data: new Float32Array([1.1, 1, 1, 1])
},
// ....
}

将上面的参数传入对上面的着色器,然后通过卷积赋值于gl_FragColor,这样简单的修图工具就大功告成了。


texcoord 纹理


const texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
// ...
gl.vertexAttribPointer(texcoordLocation, size2, type2, normalize2, stride2, offsetVal2);

这里用缓冲区处理,本质图片也就是4个点的矩形,因为每个点其实对应像素和位置, 下面是创建纹理的标准代码,将image传入到textImage2D,然后将缓冲区绑定到这样我们就可以绘制纹理了


 // webgl创建纹理,并设置基本纹理参数,载入image图片
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

//定义纹理处理能力
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);


帧缓冲


如何给图片施加多种状态的叠加效果,也就是图片 -> 纹理一 -> 纹理一 + 纹理二 -> 画布,那么我们需要用到帧缓冲,其实就是通过不断的bindTexture来覆盖之前的状态。


// 绘制帧缓冲
function drawFrames () {
const originTexture = createAndSetupTexture(gl)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
let textures = []
let frameBuffers = []
const kernelsFilterList = ['gaussianBlur', 'emboss', 'boxBlur',
'gaussianBlur', 'boxBlur', 'gaussianBlur', 'boxBlur', 'gaussianBlur'] //叠加效果的数组

for (let i = 0; i < kernelsFilterList.length; i++) {
let texture = createAndSetupTexture(gl)
textures.push(texture)

gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, null);

var fBuffer = gl.createFramebuffer()
frameBuffers.push(fBuffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer);
// 绑定纹理到帧缓冲
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}
gl.bindTexture(gl.TEXTURE_2D, originTexture);

for (var i = 0; i < kernelsFilterList.length; i++) {
setFramebuffer(frameBuffers[i], image.width, image.height);
drawWithKernel(kernelsFilterList[i]);
// 叠加
gl.bindTexture(gl.TEXTURE_2D, textures[i]);
}

// 绘制
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer (fbo, width, height) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // 绑定帧缓存
gl.uniform2f(resolutionLocation, width, height); // 设置到裁剪坐标
gl.viewport(0, 0, width, height); // 将裁剪坐标自适应到屏幕坐标
}
}


总结


通过基本的语法、纹理使用、帧缓存等,我们对webgl的基本的2d图形处理有了一定的认知,正常在绘制三角形,四边形,圆形,我们都可以使用缓存区,最后drawArrays绘制,在一些图形的渲染需要保存之前的状态的时候,我们可以使用帧缓存处理。关于当前页面的优化,当前的修图页面应该将各种调色分到不同的glsl文件,同样我们也可以做裁剪,上传图片编辑并下载。



如果觉得文章对你有帮助,不要忘了一键三连 👍



附录



  1. 内卷年代,是该学学WebGL了 - 掘金 (juejin.cn)

  2. 为什么我的WebGL开发这么丝滑 🌊 - 掘金 (juejin.cn)


作者:谦宇
来源:juejin.cn/post/7215977393696522299
收起阅读 »

屡次wx.getLocation申请被拒,让ChatGPT帮我编写申请理由后立马通过

微信小程序完成某些需求功能需要获取精准经纬度,需要微信小程序后台-开发管理-接口设置,申请“获取当前的地理位置、速度(wx.getLocation)”,一直审核不通过,有的人甚至提交了10多次都没有通过,如果你没有掌握方法,及时申请几个月还是一样不给通过。 包...
继续阅读 »

微信小程序完成某些需求功能需要获取精准经纬度,需要微信小程序后台-开发管理-接口设置,申请“获取当前的地理位置、速度(wx.getLocation)”,一直审核不通过,有的人甚至提交了10多次都没有通过,如果你没有掌握方法,及时申请几个月还是一样不给通过。


包括本人也是申请数次未通过,思考是否是理由不够充分,借助ChatGPT帮助我丰富申请理由,我用ChatGPT的回答来申请,立马审核通过。以下是模拟对ChatGPT的提问和回复。
1679989283893.jpg


提问


这是一个社区团购的微信小程序,由于团购服务点功能,需要精准定位到最近的服务点,因此需要wx.getLocation接口申请。请你基于我的需求,编写一个充分的申请原因,并包含操作步骤说明。


ChatGP回答


申请原因:


该社区团购微信小程序需要使用wx.getLocation接口获取用户的地理位置信息,以便精准定位用户所在地区,并显示最近的服务点。服务点根据用户所在地区的需求,提供更加贴近实际的商品和服务,确保用户的用户体验。该接口可以通过用户授权的方式获得用户所在位置信息,不会泄露用户的隐私信息。


操作步骤:


1. 首先在微信小程序中调用wx.getLocation接口进行定位,用户将会收到授权提示,需同意授权以便应用获取用户的地理位置信息。

2.定位成功后,小程序将通过得到的经纬度信息,计算用户位置与服务点之间的距离,并返回最近的服务点信息给用户。

3. 如果用户不同意授权,将无法获取用户地理位置信息,定位功能将无法使用,导致无法提供精准的服务点查找。

4.在使用wx.getLocation接口时,需要注意用户个人隐私问题,小程序应在提供提醒并遵守相关规定的前提下,妥善保护用户隐私信息。


接口申请总结


ChatGPT的回答固然帮助我们丰富了申请原因,但是申请该接口的基本要点还是要掌握。



  1. 确保小程序的服务类目可申请wx.getLocation,最好是主营类目,包含两个类目。

  2. 原因表达充分,主要以用户体验角度,及用户隐私。

  3. 提交的图片、视频,符合申请的步骤。


最后


ChatGPT是自然语言处理工具,需要充分的表达清楚,才可能接近输出我们需要的答案,且我们能够甄别验证它的输出。


其他
# 经验分享:快速通过“获取当前的地理位置、速度(wx.getLocation)接口”审核


作者:ZTrainWilliams
来源:juejin.cn/post/7215465880884674619
收起阅读 »

往往排查很久的问题,最后发现都非常简单。。。

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。 大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommi...
继续阅读 »

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。


大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommitException,并且集中在灰度机器上。


E20:21:59.770 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR [Consumer clientId=xx-xx.4-0, groupId=xx-xx-consumer_[gray]] Offset commit with offsets {xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918, leaderEpoch=4, metadata=''}, xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228, leaderEpoch=5, metadata=''}} failed org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: Failed to send request after 30000 ms.


排查


检查了这个 Topic 的流量流入、流出情况,发现并不是很高,至少和 QA 环境的压测流量对比,连零头都没有达到。


但是从发生异常的这个 Topic 的历史流量来看的话,发生问题的那几个时间点的流量又确实比平时高出了很多。



同时我们检查 Broker 集群的负载情况,发现那几个时间点的 CPU 负载也比平时也高出很多(也只是比平时高,整体并不算高)。



对Broker集群的日志排查,也没发现什么特殊的地方。


然后我们对这个应用在QA上进行了模拟,尝试复现,遗憾的是,尽管我们在QA上把生产流量放大到很多倍并尝试了多次,问题还是没能出现。


此时,我们把问题归于当时的网络环境,这个结论在当时其实是站不住脚的,如果那个时刻网络环境发生了抖动的话,其它应用为什么没有这类异常?


可能其它的服务实例网络情况是好的,只是发生问题的这个灰实例网络发生了问题。


那问题又来了,为什么这个实例的其它 Topic 没有报出异常,偏偏问题只出现在这个 Topic 呢?。。。。。。。。。


至此,陷入了僵局,无从下手的感觉。


从这个客户端的开发、测试到压测,如果有 bug 的话,不可能躲过前面那么多环节,偏偏爆发在了生产环境。


没办法了,我们再次进行了一次灰度发布,如果过了一夜没有事情发生,我们就把问题划分到环境问题,如果再次出现问题的话,那就只能把问题划分到我们实现的 Kafka 客户端的问题了。


果不其然,发布后的第二天凌晨1点多,又出现了大量的 RetriableCommitFailedException,只是这次换了个 Topic,并且异常的原因又多出了其它Caused by 。


org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR 
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.

分析


这次出现的异常与之前异常的不同之处在于:



  1. 1. Topic 变了

  2. 2. 异常Cause变了


而与之前异常又有相同之处:



  1. 1. 只发生在灰度消费者组

  2. 2. 都是RetriableCommitFailedException


RetriableCommitFailedException 意思很明确了,可以重试提交的异常,网上搜了一圈后仅发现StackOverFlow上有一问题描述和我们的现象相似度很高,遗憾的是没人回复这个问题:StackOverFlow。


我们看下 RetriableCommitFailedException 这个异常和产生这个异常的调用层级关系。



除了产生异常的具体 Cause 不同,剩下的都是让我们再 retry,You should retry Commiting the lastest consumed offsets。



从调用层级上来看,我们可以得到几个关键的信息,commit 、 async。


再结合异常发生的实例,我们可以得到有用关键信息: 灰度、commit 、async。


在灰度消息的实现上,我们确实存在着管理位移和手动提交的实现。



看代码的第 62 行,如果当前批次消息经过 filter 的过滤后一条消息都不符合当前实例消费,那么我们就把当前批次进行手动异步提交位移。结合我们在生产的实际情况,在灰度实例上我们确实会把所有的消息都过滤掉,并异步提交位移。


为什么我们封装的客户端提交就会报大量的报错,而使用 spring-kafka 的没有呢?


我们看下Spring对提交位移这块的核心实现逻辑。



可以同步,也可以异步提交,具体那种提交方式就要看 this.containerProperties.isSyncCommits() 这个属性的配置了,然而我们一般也不会去配置这个东西,大部分都是在使用默认配置。



人家默认使用的是同步提交方式,而我们使用的是异步方式。


同步提交和异步提交有什么区别么?


先看下同步提交的实现:



只要遇到了不是不可恢复的异常外,在 timer 参数过期时间范围内重试到成功(这个方法的描述感觉不是很严谨的样子)。



我们在看下异步提交方式的核心实现:



我们不要被第 645 行的 RequestFuture future = sendOffsetCommitRequest(offsets) 所迷惑,它其实并不是发送位移提交的请求,它内部只是把当前请求包装好,放到 private final UnsentRequests unsent = new UnsentRequests(); 这个属性中,同时唤醒真正的发送线程来发送的。



这里不是重点,重点是如果我们的异步提交发生了异常,它只是简单的使用 RetriableCommitFailedException 给我们包装了一层。


重试呢?为什么异步发送产生了可重试异常它不给我们自动重试?


如果我们对多个异步提交进行重试的话,很大可能会导致位移覆盖,从而引发重复消费的问题。


正好,我们遇到的所有异常都是 RetriableCommitException 类型的,也就是说,我们把灰度位移提交的方式修改成同步可重试的提交方式,就可以解决我们遇到的问题了。


作者:艾小仙
来源:juejin.cn/post/7214398563023274021
收起阅读 »

既当产品又当研发,字节大哥手把手带我追求极致

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做...
继续阅读 »

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做的东西,上面说的特点至少有一个不具备,甚至通通不具备。


而我在字节实习的过程中,所经手的恰恰就是这么一个需求不明确、解决方案不明确、最终产品效果不明确的项目。整个过程中有过焦头烂额毫无进展的时刻也有过欲哭无泪的时刻,还好有我的mentor带着我一路披荆斩棘、过关斩将。


首先和大家讲一下项目背景,当时我在的组是视频会议移动端,经历了近三年大流感的洗礼,相信大家对于视频会议中可能遇到的各种问题如数家珍,包括但不限于没声了、没音了、没画面了、画面卡顿、画面不清晰、画面和语音不同步、同步屏幕时闪退等等等等。作为一个服务企业级的B端产品,出现以上问题时就可能会投诉,然后经过客户成功部门转手到运营再转手到研发这里,研发就需要判断分析一下究竟是我们产品的原因、还是客户本身设备的问题、或者是第三方环境的因素,当用户的量级上来后,这样的客诉就会很多,会严重占用oncall的研发人员的时间以及精力。


我的mentor,一个专注于解决问题、避免重复劳动的人,一个字节范我觉得有E+的人,一个虽然身处移动端但是前后端甚至网络也都会的人,觉得这样很不OK,应该有个工具,能够自动的分析出来客户究竟遇到了什么问题,分析不出来的再找研发进行排查。没有这个工具也不影响业务开发的进展,所以整个项目并不存在时间上的紧迫性,但是呢,有这个工具做出来后肯定会大大降低研发的开发时间,所以项目的必要性还是有的。于是,我作为刚入职的实习新人,这个项目就交给我来做了。


而我,一个还没有从校园中完全出来的新兵蛋子,说实话面对这样的场面是一脸懵逼的,对于要做啥、要怎么做可以说是一无所知,我的mentor在我入职后,让我先了解了解背景,第一周就带着我oncall了,让我知道都可能有样的客诉,手把手给我演示他们正常的排查问题的方式。先了解客户反馈的情况,然后捞出来客户对应时间的设备信息以及设备日志。


说实话,作为一个新人,或者说我本身对于项目有一种畏难心理,碰到一点难题就总是想着往后拖,或者摆烂先不管然后就搁置在一边不想再问津了,但是我的mentor是一个有着坚定信念的人,差不多就是见山开山,见水架桥这种,遇到问题会主动找到相关人员一起解决,可以说就是有那种主人翁,项目owner的意识。于是,我就跟在他的后面,和整个团队的不同角色沟通他们遇到问题时排查的思路,试图总结出来一种通用的流程。在过程中,难免有许多困难,我的第一反应是退缩,但是导师的第一反应是拉会拉上相关人员一起讨论,看看用什么方式可以解决。比如在如何确定设备日志和故障表现的映射关系时,先后调研了多种方式看看相关团队有没有类似的做法以及他们最后实现的效果,包括大数据机器学习、代码状态流转图、自定义规则引擎等多种方式,最后调研后决定采用自定义规则引擎的方式。在实现需求的时候,需要其他团队协作时,他总是直接向前提出自己的需求,而我向一个陌生人发消息之前总要做一些心理建设,总是在担心些什么、害怕些什么,而事实上大家都是打工人,谁也不比谁厉害多少,对方不配合那就拉+1进群一起看看,解决不了就向上暴露问题。


于是,导师披荆斩棘在前,我在后面跟着实现他的设想。我们很快就做出来了第一个版本。通过Python自动化下载设备日志,然后正则匹配筛选出含有特定标记的日志,并对他们的出现频率次数做出判断。因为Python是解释型的语言,所以可以把规则直接写成python语言,用eval或者exec函数进行执行。第一个版本做出来后,导师又积极的带着我去给其他人宣传我们的这个工具。然后根据他们的反馈继续进行相关改进,最后我离职前实现的效果就是@ 一个群里的机器人,告诉他出现问题的ID,他就能自动化的拉下来日志进行排查,然后告诉你他分析的结果,整个交互非常的方便。


一个成功的项目必须要有一个负责的owner,我的导师就向我展示了一个优秀的owner是如何一步步解决问题、排除项目中的难关,如今我也正式成为一名打工人,希望我也能早日如他一般自如的面对工作。


我是日暮与星辰之间,出道两年半的Java选手,相信时间的力量,

作者:日暮与星辰之间
来源:juejin.cn/post/7211801284709138493
一起成为更好的自己!

收起阅读 »

硬盘坏了,一气之下用 js 写了个恢复程序

web
硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
继续阅读 »

硬盘坏了,一气之下写了个恢复程序


师傅拯救无望


硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


2023-03-24-14-15-16.png


再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


2023-03-24-14-18-50.png


2023-03-24-14-19-30.png


2023-03-24-14-20-05.png


那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


自救之路


在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


经过调研,数据恢复方法通常有:



  • 硬件损坏,对坏的盘进行修复

  • 误删或逻辑错误等,文件扫描修复

  • git 重置恢复


很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


{
// 配置版本
"version": 1,
// 原来文件所在位置
"resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
// 文件历史
"entries": [
{
// 历史文件存储的名称
"id": "YFRn.mjs",
"source": "工作区编辑",
// 修改的时间
"timestamp": 1656583915880
},
{
"id": "Vfen.mjs",
"timestamp": 1656585664751
},
]
}

通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


气死我了,一气之下就自己写个!


恢复程序开发步骤


毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


首先考虑需求:



  • 我要实现一个自动扫描 vscode 数据目录

  • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

  • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

  • 扫描出来有N个项目时,我可以指定只还原某此项目

  • 我可以搜索文件、目录名或文件内容进行还原

  • 为了方便,我还要一个看起来不太丑的操作界面


大概就上面这些吧。


然后考虑实现:


我要实现一个自动扫描 vscode 数据目录


要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


参考: stackoverflow.com/a/72610691


  - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
- win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
- /home/USER/.config/VSCodium/User/History/
- C:\Users\USER\AppData\Roaming\VSCodium\User\History

大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


  let { historyPath, toDir } = req.body
const homeDir = os.userInfo().homedir
const pathList = [
historyPath,
`${homeDir}/AppData/Roaming/Code/User/History/`,
`${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
`${homeDir}/AppData/Roaming/VSCodium/User/History`,
`${homeDir}/.config/VSCodium/User/History/`,
]
historyPath = (() => {
return pathList.find((path) => path && fs.existsSync(path))
})()
toDir = toDir || normalize(`${process.cwd()}/re-store/`)

然后以原始的目录结构还原出来……


这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


function scan({ historyPath, toDir } = {}) {
const gitRoot = `${historyPath}/**/entries.json`

fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
const globbyList = globby.sync([gitRoot], {})

let fileList = globbyList.map((file) => {
const data = require(file)
const dir = path.parse(file).dir
// entries.json 地址
data.from = file
data.fromDir = dir
// 原文件地址
data.resource = decodeURIComponent(data.resource).replace(
/.*?\/\/\/(.*$)/,
`$1`
)
// 原文件存储目录
data.resourceDir = path.parse(data.resource).dir
// 恢复后的完整地址
data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
// 恢复后的目录
data.rresourceDir = `${toDir}/${path
.parse(data.resource)
.dir.replace(/:\//g, `/`)}
`

const newItem = [...data.entries].pop()
// 创建文件所在目录
fs.mkdirSync(data.rresourceDir, { recursive: true })
const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
encoding: `binary`,
})
fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
return data
})

const tree = pathToTree(fileList, { key: `resource` })
return tree
}

为了方便,我还要一个看起来不太丑的操作界面


我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


image.png


如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


实际上,这个需求得加钱。


2023-03-24-15-09-25.png


由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


  const opener = require(`opener`)
const { portNumbers, default: getPort } = await import(`get-port`)
const port = await getPort({ port: portNumbers(3000, 3100) })
const server = express()
server.listen(port, `0.0.0.0`, () => {
const link = `http://127.0.0.1:${port}`
opener(link)
})

封装成工具,我为人人


理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


npx vscode-file-recovery

这就是恢复后的文件在硬盘里的样子啦:


2023-03-24-15-22-23.png


所有代码位于:



建议收藏,以备不时之需。/手动狗头


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7213994684262826040
收起阅读 »

Android开发小技巧-屏幕常亮与高亮的管理

前言 在使用我们国民应用微信和支付宝的时候,打开付款码给别人扫码的时候,那个页面简直亮瞎我的眼,做为一个Android开发者,我就想这个功能是怎么实现的呢? 问题:如何实现屏幕的常量与亮度控制呢?又有哪些方式来实现呢? 一、WakeLock机制 说起应用程序A...
继续阅读 »

前言


在使用我们国民应用微信和支付宝的时候,打开付款码给别人扫码的时候,那个页面简直亮瞎我的眼,做为一个Android开发者,我就想这个功能是怎么实现的呢?


问题:如何实现屏幕的常量与亮度控制呢?又有哪些方式来实现呢?


一、WakeLock机制


说起应用程序App的耗电,其本质就是硬件的消耗电量,硬件耗电单元分为CPU、基带、GPU、WIFI、BT、GPS、LCD/OLED等等。


耗电量的层级 基带非通话时间的能耗基本上在 5mA 左右, 而CPU只要处于非休眠状态,能耗至少在 50mA 以上,GPU执行图形运算时会更高, 另外 LCD/OLED, GPS等硬件又更高。


一般手机待机时,CPU、LCD、WIFI均进入休眠状态,这时 Android 中应用程序的代码也会停止执行,只会有基带处理器的耗电。 这也就是为什么微信比短信相比更加的耗电,答案就是短信使用基带耗电小, 而微信使用CUP耗电大。


Android 为了确保应用程序中关键代码的正确执行,提供了 WakeLock 的API,使得应用程序有权限通过代码阻止CPU进入休眠状态。


WakeLock 阻止应用处理器 CPU 的挂起,确保关键代码的运行,通过中断唤起应用处理器 CPU,可以阻止屏幕变暗。所有的 WakeLock 被释放后,系统会挂起。


例如以下音乐播放器,我们申请了 WakeLock 的情况下,就算按下电源键锁屏了,我们的音乐还是会播放。CPU不会休眠照样会处理我们的应用。


但是如果我们不释放 WakeLock,或者滥用 WakeLock 就会导致电池续航尿崩,用户查看电池消耗查看你的 App 耗电量高就会卸载了。


除了阻止 CPU 休眠,WakeLock 还可以让屏幕常亮,通过设置对应的 levelAndFlags 即可实现,常用的几个levelAndFlags:



  • PARTIAL_WAKE_LOCK:保持CPU 运转,屏幕和键盘灯有可能是关闭的。

  • SCREEN_DIM_WAKE_LOCK:保持CPU 运转,允许保持屏幕显示但有可能是灰的,允许关闭键盘灯

  • SCREEN_BRIGHT_WAKE_LOCK:保持CPU 运转,保持屏幕高亮显示,允许关闭键盘灯

  • FULL_WAKE_LOCK:保持CPU 运转,保持屏幕高亮显示,键盘灯也保持亮度

  • ACQUIRE_CAUSES_WAKEUP:不会唤醒设备,强制屏幕马上高亮显示,键盘灯开启。有一个例外,如果有notification弹出的话,会唤醒设备。

  • ON_AFTER_RELEASE:WakeLock 被释放后,维持屏幕亮度一小段时间,减少 WakeLock 循环时的闪烁情况


怎么使用呢?先声明权限


    <uses-permission android:name="android.permission.WAKE_LOCK"/>

然后直接使用:


    override fun init() {
val powerManager = commContext().getSystemService(Service.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "My Lock")

//是否需计算锁的数量
wakeLock.setReferenceCounted(false)

}

override fun onResume() {
super.onResume()
wakeLock.acquire()

}

override fun onStop() {
super.onStop()
wakeLock.release();
}

这样就可以实现一个简单的屏幕常亮的控制了。


虽然是可以实现逻辑,但是按照上面说的,谷歌可能也是怕我们滥用,导致手机续航尿崩,然后甩锅到Android系列上,说Android系列辣椒续航不行什么什么的,谷歌老早就标记过时了,并提供了新的 Api 实现此功能 SCREEN_ON。


二、KEEP_SCREEN_ON


有两种方式设置,一种是xml内部设置,另一种是通过Activity的window添加flag来设置


    <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:keepScreenOn="true"
android:orientation="vertical"/>



或者oncreate方法中添加flags


    override fun init() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

两种方法都能实现,并且是和Activity生命周期绑定的,当我们退出这个页面就可以退出常量的状态,使用起来也是非常的方便。


那如果是这样一样场景,比如我们使用单Activity+多Fragment的方式,我们需要为其中一个Fragment设置为常亮,切换Fragment的时候动态的切换亮度,那怎么办?


我们需要动态的开关这个flag


window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


又或者找到跟视图动态设置keepScreenOn属性 setKeepScreenOn 也是可行的,下面我们会以window的方式做一个工具封装类。


二、最大亮度的设置


最大亮度可以通过当前 Activity 的 window 对象设置 windowLayoutParams ,设置对应的 screenBrightness 值即可实现。


我们可以实现一个工具类来控制常亮的开关和最大亮度的开关,特别适用于单Activity+多Fragment的使用场景。


public class ScreenUtils {

/**
* 获取屏幕宽度
*/

public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
return width;
}


/**
* 获取屏幕高度
*/

public static int getScreenHeith(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int height = wm.getDefaultDisplay().getHeight();
return height;
}


/**
* 是否使屏幕常亮
*
* @param activity 当前的页面的Activity
*/

public static void keepScreenLongLight(Activity activity, boolean isOpenLight, boolean maxBrightness) {

Window window = activity.getWindow();
if (isOpenLight) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

WindowManager.LayoutParams windowLayoutParams = window.getAttributes();
windowLayoutParams.screenBrightness = maxBrightness ?
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL : WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
window.setAttributes(windowLayoutParams);
}
}

使用的时候在页面显示的时候常亮且最大亮度即可,然后我们可以在任意地方关闭这些设置。


    override fun onResume() {
super.onResume()

ScreenUtils.keepScreenLongLight(this, true, true)
}


override fun onStop() {
super.onStop()

ScreenUtils.keepScreenLongLight(this, false, false)
}

最大亮度打开页面简直亮瞎我的眼!



OK,完结!



作者:newki
来源:juejin.cn/post/7130424225147387935
收起阅读 »

十六进制常量还有这种玩法

前言 上一篇文章中 juejin.cn/post/715437… ,在源码解析阶段,那些判断16进制的地方,很有意思,加上我以前也写过一篇关于这个的文章 http://www.jianshu.com/p/bff2b84ca… ,所以想在这里做个分享。 状态变量...
继续阅读 »

前言


上一篇文章中 juejin.cn/post/715437… ,在源码解析阶段,那些判断16进制的地方,很有意思,加上我以前也写过一篇关于这个的文章 http://www.jianshu.com/p/bff2b84ca… ,所以想在这里做个分享。


状态变量的一般写法


可能有些朋友平时在开发时定义常量状态是这样定义的:


public static final int SEX_BOY = 0; // 男生
public static final int SEX_GIRL = 1; // 女生

然后看了某篇文章之后,某个经验丰富的说,定义常量时最好使用16进制,再去看了看Android某些类的源码,嗯,好像里面定义常量确实是使用了16进制,于是之后写代码就开始


public static final int SEX_BOY = 0x00; // 男生
public static final int SEX_GIRL = 0x01; // 女生
public static final int SEX_OTHER = 0x02; // 其它

比如说有很多个状态,就从0x01、0x02......0xff 这样列举下去。


这样写对吗?我随便找个源码来举例下,随便从View.java扣出一段代码


e60a26626531e0f7231f1858adc1518.png


为什么是这样定义呢,为什么不是像我们那种写法?


叠加状态的定义方式


其实这个直接说不好解释,跟着我去操作,就理解为什么要这么定义了。


假设我们定义状态,定义成这样


public static final int TYPE1 = 0x01;
public static final int TYPE2 = 0x02;
public static final int TYPE3 = 0x04;
public static final int TYPE4 = 0x08;
public static final int TYPE5 = 0x10;
public static final int TYPE6 = 0x20;
public static final int TYPE7 = 0x40;
public static final int TYPE8 = 0x80;

为什么这么写呢?

我们将16进制转成2进行,上面就对应成


0d39ccd1271903a3cc6f461d2b03217.png


有意思的就在这里,我先说的我想的过程中错误的一个思路 (我觉得挺有意思的,所以可以说下,因为是一个错误的思路,如果不想看可以直接跳看下面的这样定义的原因)


二进制从右往左来说

(1)我用第一位表示性别 000:女 001:男

(2)我用第二位表示角色 000:学生 010:老师

(3)我用第三位表示班级 000:A班 100:B班

那么 “A班的女老师” 我可以表示成 010 = 2

“A班的男老师” 可以表示成 011=3

“B班的女学生”可以表示成 100 .....

这样可以组成8个状态而不会冲突,但是这样的做法是只能用3个状态组合进行比较,而且单个状态下有000表示了3种,而且这种做法同一位上只能表示两种状态,假如我加个C班,那就没辙了。


然后换了一种思考的方法,假如我这样表示状态


public static final int TYPE1 = 0x01;  // 女
public static final int TYPE2 = 0x02; // 男
public static final int TYPE3 = 0x04; // 学生
public static final int TYPE4 = 0x08; // 老师
public static final int TYPE5 = 0x10; // 主任
public static final int TYPE6 = 0x20; // A班
public static final int TYPE7 = 0x40; // B班
public static final int TYPE8 = 0x80; // C班

那么 使用二进制的或运算:

“A班的女老师” 我可以表示成 TYPE6|TYPE1|TYPE4 = 0010 1001 = 41

“A班的男老师” 可以表示成 TYPE6|TYPE2|TYPE4 = 0010 1010 = 42

“B班的女主任”可以表示成 TYPE7|TYPE1|TYPE5 = 0101 0001 = 81

这样也能把多个状态组成一个状态,而且组合状态也能和单个状态进行同等级判断,并且这种做法不会产生重复的状态。


举个例子就是说你平时写


if(性别==女 && 角色 == 老师 && 班级 == A班){
......
}else if(版本 == C班){
......
}

如果用我这种方法定义状态的话,你只用写


if(type == 0x29){
......
}else if(type == 0x80){
......
}

可能有些人说就仅仅为了这样?那我写&&还好过,写成16进制转换转的我脑壳疼。我还不如多写几个&&,而且这样也更容易让别人看懂。
但这个写法不仅仅有这种好处,再举个例子,假如在很多个组合的状态中你需要去判断这个状态是“男”还是“女”等等,多状态下判断单状态多了,也不是说乱,但会写很多代码,但是现在可以直接这样写


public void switchSex(type){
if(type & 0x03 == 0x01){
// 是女生
}else{
// 是男生
}
}

就可以直接这样用二进制的与运算来实现判断。

我也仅仅是举了两个例子,我的意思是这样去定义十六进制常量,方便二进制做运算,二进制还有其他的运算呢,我仅仅举了“或”和“与”,还有什么异或啊,移位啊之类的,而且就算作用不大,按装逼来说,我直接做二进制的运算肯定比你那些乱七八糟的运算来得快吧。


总结


当然这只是我领悟的一种思路,而且我想很多人也知道这种做法,或者用16进制来定义常量不仅仅有这个好处,只是我觉得很有意思,所以想分享一下。


作者:流浪汉kylin
来源:juejin.cn/post/7155474762053992485
收起阅读 »

“勇敢的去走自己的夜路”——走出我的“浪浪山”

引子 2022年,经历过太多太多的故事,也发生了太多太多的事故。 这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是...
继续阅读 »

引子


2022年,经历过太多太多的故事,也发生了太多太多的事故。


这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是遇到了一堆可爱的掘友,利用掘金的资源也找到了一个工作。


这一年,我走了很远的路,吃了很多的苦,踩了很多的坑,才将这份年终总结交付与星球大伙。也曾有幸与掘友一起分享只属于我们的“情书”


第一节:对你,同样对自己说


今天是 2023年1月1日,这一年,半分努力,半分浑噩,忽隐忽现的理想,支撑着自己踽踽独行。几年前,他应征入伍,算不上什么好选择,也或许并没有选择的权利。北方干冷的空气,窗前停驻的麻雀,以及战友豪迈的言语曾一度让我觉得,南京或许会是我最终的归宿。


在南京的第二个年头,这一年我21周岁,报国的赤心和热血似乎都正热时,我做出了人生的第一个计划,“退役复学”。


感恩军旅生活,让我真正的热爱祖国与持续学习,在二零年上旬,新冠疫情爆发了,一个八十八线小城市的我,除了紧张的气氛外,到也没受到多大的影响,在家依旧忙碌,直到2022年2月10日,我记得非常清楚,写了一天前端(三件套的弱鸡)代码的我,结束了当天的笔记小结,打开了B站,悄然间随机看见了关注了好久的鱼皮居然真的开了学习圈子(编程导航),这让一个对编程说不上爱的萌新,从此爱上了coder与share,(一个利他的博主谁又能不爱呢?),曾经把编程视为作业的我,我发现我能用他code出一个全新的世界,我便一发不可收拾爱上了它(这里的它指的是编程)。


(一)身体是革命的本钱


but 「熬夜 + 不规律的作息 + 不健康的饮食」+ 「年轻」= 无事发生


“年轻人”,似乎总是有一种得天独厚的优势,有精力,有体力。而这对于我这个退役选手更是easy了,这些不太好的习惯也似乎在年轻一代的大水潭中泛不出多少涟漪,凭借着这份“本钱” ,自然能更加心安理得,反正:我还年轻,我还可以熬。


(2) 继续战斗,也请先照顾好自己


疫情消耗掉了大半年的时间,大学断断续续的锻炼,把熬夜换成了早起,开始按时吃早餐,解封后的日子,趁着南方冬天来的很晚,与几个战友开始了跑步的活动,这一阶段体能上确实有了很大的提升,我很享受跑步后,被风吹过的感觉(皮一下:我也曾吹过未来女朋友曾吹过的风)。


说来也很神奇,每次当我没什么精神,只要去跑步,回来冲个澡就会精神百倍,所以我一般傍晚的时候有空就会去跑跑,然后就可以再晚上全身心的写代码,整理笔记(当然最后就是发到星球上面,感受大家阅读后的“指责与指导,哈哈哈”)。


运动本不应被当做一种应该做的任务,而应被看作一种休闲的方式,没必要与别人比较强度,组别,只有自己舒服就是最好的标准。


所以,不管是真的热爱也罢,苦于生计也罢,即使继续战斗也请先照顾好自己


(二) 随波逐流只会让你接近平均值


普通人的危机感总来自他人,而想要成为一个优秀的人,危机感必须来自自身,随波逐流只会让你靠近平均值,总有一种恍惚感,懂得越多,越觉得自己像这个世界的孤儿,与同龄人格格不入,总是自负的认为他人幼稚,就像鲁迅先生说过:“人类的悲欢并不相通......”。听着他们谈论着我 “早就走过的路”,“早就见过的风光”,我也只觉得他们吵闹。


可惜,我在某些时候,总是小气的,心中惰于学习,更不愿将自己的 “财富” 与他人分享,总忧虑别人以己为石,跳向远方,患得患失的一种矛盾,让自己无奈又颓靡。
后来我遇到了鱼皮,我发现分享的乐趣后我便不再随波逐流,持续性努力,以下是我在星球这一年输出的笔记



(ps:请大佬过目,记得留赞)如下:



大数据笔记:wx.zsxq.com/dweb2/index…


运维Devops笔记:wx.zsxq.com/dweb2/index…


低代码Lowcode笔记:wx.zsxq.com/dweb2/index…


yarn的学习:wx.zsxq.com/dweb2/index…


软件设计师:wx.zsxq.com/dweb2/index…


NodeJS笔记:wx.zsxq.com/dweb2/index…


机器学习方面:wx.zsxq.com/dweb2/index…


Vue+pinpa笔记:wx.zsxq.com/dweb2/index…


MySQL笔记:wx.zsxq.com/dweb2/index…


华为鸿蒙认证:wx.zsxq.com/dweb2/index…


软件工程笔记:wx.zsxq.com/dweb2/index…


力扣刷题攻略:wx.zsxq.com/dweb2/index…


ES6模块暴露笔记:wx.zsxq.com/dweb2/index…


ACM算法思维导图:wx.zsxq.com/dweb2/index…


Bootstrap笔记:wx.zsxq.com/dweb2/index…


网络安全资源贴:wx.zsxq.com/dweb2/index…


(三) 不被枯井遮住双眼,保持谦虚及自尊


目光短浅带来的问题是致命的,当你有一天觉得自己好像还不错,好像已经登到峰顶了。那就需要反思一下自己或许已经陷入了“枯井”中,你会这样想,那大概率是被枯井遮住了双眼,你看不到枯井之外的世界,为了一点点成就就沾沾自喜,虽然阶段性的成功也很值得高兴,但千万不要走进这份舒适区中,温水煮青蛙的例子也不少见,走出枯井后,你就会发现外面的世界还是在一个枯井中,你要做的就是不断的往上爬。


永远不要看不起任何人,即使一位在你眼中普普通通的人,他的技术或许逊色你不少,但是他在思想和创造性上总能给你意想不到的惊喜。即使我的学校很普通,但是我的身边仍然有着一批充满韧劲的朋友,希望能通过考研,亦或者对于技术的钻研,弥补自己高考的遗憾,我记得大二那年,我常常在凌晨一点半两点收到微信弹窗大家一起交流一些问题。备战比赛的三点一线生活,学技术的通宵达旦,为了目标不断努力,这样的人仍然值得我尊重与学习,我认为他们拥有了一名大学生应该有的“灵魂”


除此之外,请千万保持自尊,自尊并非别人给的,而是自己给的,如果遇到比自己弱的人就有“自尊”,遇到比自己强的人就畏畏惧惧,没有“自尊”,那么这种自尊就没有任何意义了,闻道有先后,术业有专攻,应当尊重任何在某个方向的前辈,但是也没必要过于拘束,见贤思齐,见不贤而内自省即可。


(四) 远离总是给你负面情绪的人


但是如果你遇到了一些人,总时时刻刻,在学习以及生活上给你一些负面的观点,这种人会严重影响你坚定往枯井上爬的信念,不管你们是什么关系,我给你一个建议——赶快跑(这里现代化的叫法喊:润),有多快,跑多快,如果你们不幸要发生必要的交互,请将这段关系限制在最小范围内,切勿投入感情


(五) 传道授业:若要学知识,必得为人师


这一年我很喜欢读一本书,那就是《软技能:代码之外的生存指南》(下面我会提到)其中有一个章节给我印象很深,即第33章,传道授业:若要学知识,必得为人师,下面我摘了一段:



在你传道授业的时候都会发生什么 当我们初次接触某个课题的时候,我们对于自己对此了解多少往往都会高估。我们很容易自欺欺人,以为已经对某样东西 了如指掌,直到我们试着去教会别人的时候,才能发现事实并非如此。你有没有被别人问过一个非常简单的问题,却震惊地发现自己不能清晰地解答。你刚开始会说:“这个,很明显……”,接下来只有“哦……”。这种情况在我身上屡屡发生。我们自认为已经透彻理解了这个话题,实际上我们只是掌握了表面知识。这就是传道授业的价值。在你的知识集合里面,总有一部分知识你并没有理解透彻到可以向别人解释,而“教”的过程能够迫使你面对这一部分。作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。这种肤浅的理解力无碍于我们完成工作,因而不易被察觉。然而一旦我们试着向别人解释某件事情的运作原理或背后的原因的时候,我们在认知上的漏洞就会暴露出来。不过这并非坏事。我们需要知道自己的弱点,然后才能对症下药。在教别人的时候,你迫使自己面对课题中的难点,深入 探索,从只知皮毛变成完全理解。学习是暂时的,而理解是永久性的。我可以背诵九九乘法表,但是一旦理解了乘法的运算原理,即使突然记性不好,我也可以重做一张乘法表。



我已经记不清很多年前我初中亦还是我高中的一位任课老师曾经说过这么一句话:能教会别人,自己也就没问题了。可惜那个时候的自己压根没提起学习的欲望,当然了,也或许与我自己根本不喜欢枯燥的应试教育有点关系。我也没理解这句话的意思。大学这几年,我很喜欢与朋友交流技术方面的事情,每个人都有很多我意想不到的理解与想法。还有更多时候我更加喜欢帮助朋友解决一些问题,当你什么时候可以将别人的一个问题,用通俗的解释说明 + 简洁却又富有代表意义的实例 + 补充一些自己的理解与看法,说给别人听得时候,最起码,我认为你对于这块内容就真的入门了。当你能够滔滔不绝的讲解给别人一块内容,能合理的安排讲解的引子与顺序的时候,这说明这一块的知识已经在你脑海中有了一条清晰的体系。同时你通过与别人交流的时候,再根据别人对你提供的一些方向好好反思斟酌一下,不断的修改。相信我,当你成功与他人讲解/交流你的知识后,你会爱上这种感觉的。


但是老板和老师可不会等你,很多时候我们都不得不 “填鸭子” 式的学习一些内容,例如根据老师的要求,强制使用一些指定的框架或者技术,或者根据业务/项目组长的需要和安排,你需要快速的学习一些你并不熟悉的内容,凭借我们多年 “应试” 的本事,大家总能很快的就找到这种套路,例如怎么快速搭建环境,怎么配置,如何快速的用起来。但是千万别止步于此,不然终究只是一个CRUD工程师,这也不一定是坏事,当你熟悉如何用一款框架或技术后,再去看一些源码,或许会事半功倍。



作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。



(六) 别让情绪扼杀你状态


(1) 所谓迷茫,都不过是想的太多


总在独处时,开始怀疑自己,我是谁,我在干什么,以后该怎么办......在我理智的那两天,我都会把这种状态归咎于闲的蛋疼。但是确确实实在那种状态下,什么事情都没法下手,最严重的的一种状态,就是会有一种深深的无力感,感觉距离目标实在太远了。这种无力感,会瞬间摧毁你的勇气,让你不敢下手去做些什么。就像是一场噩梦,你明知道应该醒来,却无法挣脱。



鼠鼠我啊是家里唯一的大学生,大学入了党,工作也没有让家里操心,家里人都认为我有出息了,只有鼠鼠觉得鼠鼠是个废物,鼠鼠以前也会想着让妈妈为自己骄傲,让家里人可以开心的生活,可是浪浪山不如鼠鼠所愿,鼠鼠在浪浪山清楚的认识到了阶层的差距,身边的人正活着曾经难以想象的生活,鼠鼠也才知道人生可以那么精彩,它就在我眼前,又好像远在天边。鼠鼠在家里是最强天赋,在浪浪山却是擦锅布,我好像永远走不出浪浪山了,鼠鼠想回下水道,鼠鼠下辈子不想做鼠鼠。



这种状态,都不过是因为想的太多,我们总是在刚起步的时候,就想着终点在哪里;总是在刚学习一项技能的时候,就想着攻克技术难题;总是在与人初次见面之后,就想推心置腹;总是在今天都没有过好的时候,就想着明天该怎么办。我就是这样一个人,常常纠结于各种各样的学习路线上,每次在学习不同的技术的选择上,进行纠结,但其实这两者我明明是有足够的时间兼学的,还有时候明明知道基础要一步一步走扎实,但是却想到后面还有各种各样的新式技术,高级技术等着我,就会又开始所谓的迷茫。


其实这种所谓的迷茫,很多时候都是源自于我们想的太多了,路要一步一步走,饭要一口一口吃,想的太多,就会感到迷茫和焦虑。最好的办法就是,立足当下,安于寂寞,不要太着急看到极大的成果,放平心态,只有你的心里想通了,你的状态就会迅速回归,重振旗鼓


(2) 你总需要一个人走一段路


孤独伴随着,几年前来到几百公里外上学的我,亦或是年后即将开始找实习,找工作的我。


我想我总会有一段时间感觉到莫名的孤独,想找个人聊聊天,却又不想去找,自己戴着耳机,漫无目的的走在路上。以及每次晚上或者凌晨写完东西,躺在床上有一种说不出的感觉,特别的是,我并不感到忧伤,只是感觉空落落的,也不想认识新的朋友,也不想联系家人,却也不知道有些话该和谁说。


即使你人缘很好,常常有三五好友一起相伴,但是总会有一些空隙感到孤独,这源自于你的内心还是不够强大,有的人独行却乐在其中,有的人三无成伴却又内心孤独,因为孤独的人心中并无足乐者,灵魂还是被空虚填满。


所以,请充实自己的生活,多出去走走,多与人交往,给自己多找点自己感兴趣的事情去做,即使感到孤独,也没必要太过沮丧,只需要告诉自己,沮丧,孤独,都是正常的,我们要在自己走的这段路上,让自己成为一个更加闪亮的人。走过自己的一段夜路,终将会有柳暗花明又一村的“闹市”。


(七) 恋爱的本质是「撞」而不是「寻」


(1) 你真的想要谈恋爱吗?


有时候总会想,谈恋爱是「一定要」还是「可以要」亦或者 「没必要」。


总有那么几天,好似陷入了爱情的怪圈。让你平淡无奇的生活荡起了阵阵涟漪,打破了你安稳的生活轨迹。


大部分时候,或许只是你想要摆脱这种“孤零零”的状态,又或者看着别人的“幸福”与“快乐”,激起了你的那份欲望,而欲望总会在你的忍耐中冲昏你的头脑,让你开始憧憬爱情,并且费力的去「寻」去「找」,试图去接触不同的人,试图找出哪一个是适合自己的,或许你会觉得,主动去寻找自己的幸福是一件很美好的事情,不过于我而言,这并不是爱情,我只能把它叫做权衡利弊后的一个选择。


或许有的时候,你只是想找一个人陪你,那也或许并不是爱情。你结束了一天的忙碌,合上了笔记本电脑,关掉了手机,疲惫的倚靠在椅子上,狭小的房间中,只有那盏台灯在一片死寂中发出微弱的光。连点一只烟的动作都觉得多余,他只想一个人安静的待一会,也不知道在想什么。但如果无由头的想起了一些事情,一些人,这个引火线,就会瞬间将情绪点燃,无尽的孤独涌上心头,这个时候,你渴望有一个人陪在你身边,陪你说说话,哪怕陪着你坐一坐,起码让你知道你并不是一个人。自此以后,你开始标榜自己「需要人陪」,看似高尚的理由,其实只是你害怕寂寞的一种借口,就算你真的找到了一个陪着你的人,那你真的爱她吗,可能你只是在你漆黑的房间中又添置了一盏台灯,这样能让你的眼中看起来更加明亮。


(2) 三观一致真的很重要!


这几年也接触过一些异性,或许也有动心过,但是你会发现,不同人看待,处理事物的方式会有截然不同的结果,你认为简直不可理解的事情,在其眼中似乎也稀松平常,或许你不懂她,也许她不懂你,三观这个词的定义实在太模糊,最简单的方法就是看你们在一起的感觉,给你的感觉如果是很舒服的,那么可以进一步了解一下,害,没什么好说的了, 希望你可以找一位能符合你心中期望的另一半。


(3) 顺着人生轨迹走吧,别为了一个人停下来


千万不要陷入单恋的漩涡中,这是致命的,对的人是不需要主动找的,你只需要顺着人生轨迹走,在合适的年纪做合适的 “正事” ,自然而然就会遇到那个人了,如果等到七老八十,也没有遇到,或许这也就是命。或许说的太悲观了,但我仍认为,与其让自己为了追求一个不确定,也或许没有回应的爱情,不如自己欣赏自己孤岛中的美丽。但话也不能太绝对,或许有一天我就会因为所谓的爱情,陷入盲目。爱情这东西,谁说的好呢。但我只要不断告诉自己,一定不要停下来


第二节 这一年我都做了些什么


(一) 学习 + 技术输出


(1) 行百里者半九十


按照原来的计划,从 Java --> JavaWeb --> SSM --> SpringBoot 这个主线就算结束了,其中夹杂着 SQL,Linux,Web,小程序,设计模式等等的一些支线。不过,根据自己的情况和具体需要吗,其实我已经做出了一些重点的调整,我会在后面的目标中去提到。


(2) 一年和球友一起 输出了一百多万字的笔记



先放地址【Java全栈方向】:http://www.yuque.com/icu0/wevy7f


欢迎大佬们关注一下小弟。



一年中,一边学习,一边做总结,做整理,陆陆续续一年也写了200来篇笔记(也可能是文章或者感悟)(不一定纯后端/前端,还有 Linux ,计网等等)记得某个大佬说过写博客和笔记不一定能做到对别人有帮助,但起码对自己有帮助。但是我一直通过大白话概括 + 做图 + 简单示例 + 官方说明的方式写文章,也在努力希望能对别人也有帮助。



(二) 超爱买书的购物狂


这一年买了不少书(买了 == 看了),还有好多想买的都在我的购物车里吃灰,再买我真怕自己变成一个光收藏的 “读书人” 了,来盘点盘点这一年我看了比较有感觉的书(没感觉的和没怎么读的就不提了,如果给我多一点时间,我争取出一篇介绍自己读的书籍的文章)




一件恐怖的事情:我利用一年时间看过了这些书



第三节 明年今日,记得要回头看看



We already walked too far, down to we had forgotten why embarked. ——纪伯伦《先知》


译文:我们已经走得太远,以至于忘记了为什么而出发。



2022年度回顾



2023年新的目标


技术上:


只有写1-2月的,所以我放一个链接,欢迎大家监督我学习



http://www.yuque.com/icu0/qeowns… 《Cool的三两事》



生活上:


  1. 孝敬父母

  2. 勤运动

  3. 照顾好自己的身体

  4. 不要熬夜

  5. 与人交谈,沉稳思虑而后动

  6. 多读书,多出去走走,善待他人


学业上:


  1. 英语四级

  2. 拉取开源项目,为开源项目提PR

  3. 持续输出技术型文章

  4. 专升本上岸


总而言之,2022喜忧参半,有“春风得意马蹄疾,一日看尽长安花”的喜悦,也有“停杯投箸不能食,拔剑四顾心茫然”的忧愁,但我希望我的2023能有“长风破浪会有时,直挂云帆济沧海”。



个人独白:


以上内容皆是一名专科生的自白,感谢自己在大专三年没有一天是“浑浑噩噩式”学习,也没有一天因为当前的荣誉而骄傲满足,同时感谢部队两年的栽培,让我站在低谷依旧能仰望天空,扎根大地,心有猛虎,细嗅蔷薇。


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

让ChatGPT帮我写一篇博客

前言 昨天还是前天在公众号看到美国的一个设计师兼作家的Jackson,在用ChatGPT创业,他让ChatGPT做“老板”,自己充当一个人工联络员,这样的一个故事让他火了,不仅推特粉丝大涨,公司也做得有模有样。这就给我一些启发,今天早上突然想到为什么不能让Ch...
继续阅读 »

前言


昨天还是前天在公众号看到美国的一个设计师兼作家的Jackson,在用ChatGPT创业,他让ChatGPT做“老板”,自己充当一个人工联络员,这样的一个故事让他火了,不仅推特粉丝大涨,公司也做得有模有样。这就给我一些启发,今天早上突然想到为什么不能让ChatGPT也帮我干活呢,所以我让他写了一篇我感兴趣的博客。


什么是交互艺术?



  • 交互艺术的定义和历史


交互艺术(Interactive Art)是指通过观众与作品互动来创造艺术作品的一种艺术形式。观众可以通过各种方式来参与艺术作品的创作过程,例如通过触摸、声音、身体动作、传感器等方式与作品互动,从而影响作品的展示效果和呈现形式。


交互艺术的历史可以追溯到20世纪60年代,当时的科技进步促使艺术家开始尝试使用电子设备和计算机来创建艺术作品。随着计算机技术的不断发展,交互艺术逐渐成为一个独立的艺术领域,许多艺术家开始探索如何将新兴技术与传统艺术形式结合起来,创造出更具互动性、多媒体性和参与性的艺术作品。


在当代艺术中,交互艺术已经成为一个重要的艺术形式,不断涌现出许多富有创意和思想性的作品。交互艺术的发展也受到了数字技术和科技的快速进步的影响,艺术家们可以使用越来越先进的技术来创造更加复杂、多样化的交互艺术作品。



  • 交互艺术与传统艺术形式的区别和相似之处


交互艺术与传统艺术形式在表现形式、创作方式和观赏方式上存在一些区别和相似之处。


区别:



  1. 表现形式不同:传统艺术形式通常是静态的,比如绘画、雕塑等,而交互艺术则常常是动态的,利用科技和互动方式与观众进行互动。

  2. 创作方式不同:传统艺术形式往往是由一个或几个艺术家单独创作完成,而交互艺术则通常需要一个团队来完成,包括设计师、程序员、工程师等多个专业人员。

  3. 观众参与度不同:传统艺术形式通常是观众被动地欣赏,而交互艺术则需要观众的积极参与,观众不再是简单的观看者,而是成为作品的一部分,与作品进行互动。


相似之处:



  1. 创作目的相似:传统艺术和交互艺术都有一个共同的目的,就是通过艺术表达和传递情感、思想和价值观念等。

  2. 创作需要创意:无论是传统艺术还是交互艺术,都需要创意,才能产生具有艺术价值的作品。

  3. 艺术的审美标准类似:无论是传统艺术还是交互艺术,都需要具有一定的审美标准,才能得到观众的认可。


总的来说,交互艺术是一种创新的艺术形式,与传统艺术形式相比具有很多不同的地方,但它们都是为了表达和传递情感、思想和价值观念等而存在的。


交互艺术的表现形式



  • 交互装置


互动装置是一种创新的展示形式,是通过各种技术手段实现人机交互,让观众能够积极参与其中,与艺术品互动,甚至改变艺术品的形态。例如,光线与声音互动的装置、投影与运动互动的装置、使用虚拟现实技术的装置等。



  • 互动性媒体


数字媒体是指以计算机技术为基础,通过数字技术手段,将文本、音频、视频等多种媒体形式进行集成和处理的一种新兴的艺术形式。数字媒体广泛应用于网络、游戏、互动展览、艺术品创作等领域。数字媒体可以通过人机交互来实现观众参与。



  • 跨媒介艺术


跨媒介艺术是指将不同的媒介进行融合,从而创造出全新的艺术形式。在交互艺术中,跨媒介艺术得以大量运用,以实现更为复杂和多样化的表现。主要包括:融合音乐,舞蹈,戏剧,美术,文字等等。


交互艺术的设计过程



  • 设计理念和目标


交互艺术的设计理念和目标可以根据具体项目的不同而有所不同,比如以互动性为主:交互艺术的设计目标是与观众进行互动,让观众成为艺术作品的一部分。互动可以是双向的,也可以是多向的,观众和作品之间可以有各种形式的交流和反馈。以参与性为主:交互艺术作品的设计目的是让观众成为作品的参与者,观众不仅是作品的被动观看者,还可以通过各种方式主动参与到作品中,体验艺术的过程。以创新性为主:交互艺术通常借助科技手段来实现艺术形式的创新,例如虚拟现实、增强现实、人工智能等技术,让观众体验到新颖的艺术形式和感官体验。以实验性为主:交互艺术通常具有实验性质,设计者会尝试各种不同的技术和形式,不断探索和发掘新的艺术表现方式和可能性。以社交性为主:交互艺术作品通常可以带来社交体验,让多个观众之间产生互动和交流,增加观众之间的沟通和共同体验。以可持续性为主:交互艺术的设计也需要考虑作品的可持续性,包括对环境的影响、对观众的健康和安全等方面的考虑。同时还需要考虑作品的维护和管理,确保作品的长期运行和展示。



  • 技术实现和选择


交互艺术的技术实现有多种选择,以下是一些常见的技术实现:



  1. 传感器技术:通过感应器获取观众的运动、声音、触摸等行为,以此来激发或控制艺术作品的变化。

  2. 虚拟现实技术:使用计算机技术和虚拟现实设备(如头戴式显示器、手套式控制器等)创造虚拟空间,使观众可以沉浸在其中与作品进行交互。

  3. 增强现实技术:使用手机、平板电脑等设备,将虚拟图像叠加在现实场景中,使观众可以在真实场景中进行虚拟的交互体验。

  4. 数据可视化技术:使用数据可视化软件和技术将数据转化为图形、动画、声音等形式,让观众可以与数据进行交互并得到更深入的理解。

  5. 互动音乐技术:使用计算机技术和音乐软件,将观众的声音、运动等行为转化为音乐,并与音乐作品进行互动。

  6. 智能机器人技术:使用机器人技术和人工智能技术,创造能够与观众进行交互的智能机器人艺术作品。


除此之外,还有许多其他的技术可以被应用于交互艺术的实现,这取决于艺术家的创造力和技术能力。



  • 用户参与和反馈


用户参与和反馈在交互艺术中起着至关重要的作用,这是因为交互艺术强调观众参与、互动和沟通,与传统艺术形式相比,用户的参与和反馈更能够影响交互艺术的展现和效果。用户参与和反馈可以创造更丰富、更具有互动性的艺术体验。通过参与和反馈,用户可以主动探索和发现艺术作品中的细节,与艺术家进行更深入的互动和交流。用户参与和反馈可以增加用户对交互艺术的参与度,使观众更加融入艺术作品之中,感受到艺术作品所传达的情感和信息。用户反馈可以帮助艺术家改善艺术作品的表现,及时发现并解决问题,让作品更加完善和符合观众的期望。通过参与和反馈,用户可以更好地理解和体验艺术作品,从而对作品产生更深刻的印象和理解,提高作品的艺术价值和影响力。


交互艺术的影响和意义



  • 对艺术和文化的影响


首先,交互艺术提供了新的观看方式和体验方式,通过参与和互动,观众成为了作品的一部分,与作品发生了联系和互动,这种体验方式比传统艺术观看更加身临其境,更能够引起观众的共鸣和情感共鸣。


其次,交互艺术扩展了艺术形式和创作方式的范围,使得艺术家可以使用更多的媒介和技术手段来表现自己的创意和思想,创作出更加复杂、多样化的作品。同时,交互艺术还促进了跨学科的合作和交流,让不同领域的人们汇聚在一起,共同探索艺术的新领域和可能性。


最后,交互艺术也在一定程度上挑战了传统艺术的观念和价值体系,它更加强调观众的参与和互动,追求创意和表达的多样性和自由性,让艺术更加民主化和开放化,更加贴近生活和人们的需求。



  • 对科技和创新的影响


交互艺术与科技、创新密切相关,因为交互艺术往往需要运用先进的科技和技术手段来实现。因此,交互艺术对科技和创新的影响主要表现在以下几个方面:



  1. 推动科技进步和应用:交互艺术在探索人机交互的过程中,往往需要运用先进的科技和技术手段,例如虚拟现实、增强现实、智能算法、传感器技术等等。这些技术的研究和应用,可以推动科技的进步和应用,也可以为其他领域的技术创新提供借鉴和参考。

  2. 催生新兴产业:随着交互艺术的不断发展和普及,一些新兴产业也应运而生,例如虚拟现实、增强现实、智能穿戴等等。这些产业的发展,也为科技和创新提供了新的发展机遇。

  3. 拓展创新思维:交互艺术强调观众参与和互动,鼓励观众从不同的角度去思考和理解作品。这种互动式的艺术形式,不仅可以拓展观众的视野和想象力,也可以激发人们的创新思维,从而为科技和创新带来新的灵感和方向。

  4. 促进科技与文化的融合:交互艺术将科技和文化相结合,探索科技与文化之间的互动和融合。这种融合不仅可以为文化艺术注入新的活力和创新,也可以促进科技和文化之间的相互理解和交流,为科技和创新带来新的思路和方向。



  • 对社会和人类的影响


首先,交互艺术的出现丰富了人们的文化生活,为人们带来了全新的艺术体验。交互艺术将观众从被动的接受者转变为积极的参与者和创造者,让人们更深入地体验艺术,对个人的审美和文化素养的提高有积极的推动作用。


其次,交互艺术对科技和创新的发展也有很大的促进作用。在交互艺术的设计中,常常会运用到各种前沿的科技手段,如人工智能、虚拟现实、增强现实等,这些技术的应用不仅提高了艺术表现的多样性和创新性,也促进了科技的发展和推广。


另外,交互艺术也推动了跨学科的合作与交流。交互艺术的创作需要艺术家、设计师、工程师等多个领域的专业人才进行合作,这种跨学科的合作有助于促进不同领域之间的交流与合作,进一步推动科技和艺术的发展。


最后,交互艺术也对社会产生了深远的影响。交互艺术作为一种探索艺术与科技、人与自然等关系的艺术形式,常常会引起人们对社会和人类的思考和反思。同时,交互艺术还可以作为一种公共艺术形式,为城市文化建设和社区文化发展做出贡献。


一些优秀的交互艺术作品的介绍和分析


《雨林声音之旅》(The Rainforest):这是一个由音乐家、工程师和艺术家合作制作的多媒体艺术作品,旨在通过视听交互体验向人们展示热带雨林生态系统的美丽和脆弱。在这个作品中,观众通过穿戴智能耳机,能够听到热带雨林中各种生物的声音,并随着观众的行动而改变。这个作品既展示了交互艺术对生态保护的关注,也通过技术手段提供了一个沉浸式的交互体验。


《万花筒之舞》(Kaleidoscope Dance):这个作品由艺术家和编程人员合作创作,是一个通过跟随舞者动作变幻图形的互动舞蹈。在这个作品中,观众通过观看跳舞者的身影,看到身影在投影上变幻出不同的几何图形,并随着舞者的动作而变化。这个作品的互动性和美学效果非常出色,展示了交互艺术的创造性和能力。


《未来自然》(Future Natural):这个作品由美国纽约市的艺术家托尼·瑞戈(tony oursler)制作,展示了未来科技与自然环境的融合。这个作品是一个互动装置,观众通过操纵屏幕上的自然元素,比如云、火、风和水,来创造出自己的自然景观。这个作品展示了交互艺术的潜力,让观众在艺术作品中自由探索和创造。


《印象派的视觉音乐》(Visual Music of Impressionism):这是一个通过数字技术还原19世纪法国印象派画家的绘画作品的互动展览。这个展览展示了通过数字技术将视觉艺术与音乐相结合的能力,让观众可以通过触摸屏幕、移动手势和声音互动等方式来探索印象派绘画作品的美学和音乐性。


未来展望:交互艺术的发展和趋势


交互艺术从20世纪末期开始兴起,并在21世纪初逐渐得到了广泛的关注和发展。最初的交互艺术作品主要依赖计算机技术,随着移动设备、传感器、物联网等技术的发展,交互艺术的形式也越来越多样化和复杂化。


近年来,交互艺术已经从传统的展览空间向公共空间和虚拟空间延伸,例如城市中的互动艺术装置和游戏、虚拟现实艺术作品等。同时,交互艺术也更多地关注社会和环境问题,例如气候变化、人类生活和工作的影响等。


未来的趋势将继续突破传统的艺术形式,更多地与科技、社会、文化等领域交叉融合,例如增强现实、人工智能、机器人技术等。同时,交互艺术也将更加强调观众的参与和反馈,更加注重体验和互动的情感性和反思性。


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