注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android 可视化预览及编辑Json

项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。 开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效 ...
继续阅读 »

项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。


开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效





    1. 每次抓包修改配置





    1. 本地导入配置,从磁盘读取




但两种方式都有一定弊端



  • 首先测试时依赖电脑修改配置

  • 无法直观预览广告配置


考虑到开发时经常使用的Json格式化工具,既可以直观的预览Json, 还可以在线编辑


那么就考虑将Json格式化工具移植到项目测试模块中


web网页可以处理Json格式化,同理在Android webView 中同样可行, 只需要引入处理格式化的JS代码即可。


查找资料,发现一个很实用的文章可视化编辑json数据——json editor


开始处理


首先准备好WebView的壳子


    //初始化
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
binding.webView.settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportZoom(true)
useWideViewPort = true
builtInZoomControls = true
}
binding.webView.addJavascriptInterface(JsInterface(this@MainActivity), "json_parse")
}

//webView 与 Android 交互
inner class JsInterface(context: Context) {
private val mContext: Context

init {
mContext = context
}

@JavascriptInterface
fun configContentChanged() {
runOnUiThread {
contentChanged = true
}
}

@JavascriptInterface
fun toastJson(msg: String?) {
runOnUiThread { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show() }
}

@JavascriptInterface
fun saveConfig(jsonString: String?) {
runOnUiThread {
contentChanged = false
Toast.makeText(mContext, "verification succeed", Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun parseJsonException(e: String?) {
runOnUiThread {
e?.takeIf { it.isNotBlank() }?.let { alert(it) }
}
}
}

加载json并在WebView中展示



viewModel.jsonData.observe(this) { str ->
if (str?.isNotBlank() == true) {
binding.webView.loadUrl("javascript:showJson($str)")
}
}

WebView 加载预览页面


        binding.webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel.loadAdConfig(this@MainActivity)
}
}

binding.webView.loadUrl("file:///android_asset/preview_json.html")


Json 预览页, preview_json.html实现



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="jquery.json-viewer.css"
rel="stylesheet" type="text/css">
</head>
<style type="text/css">
#json-display {
margin: 2em 0;
padding: 8px 15px;
min-height: 300px;
background: #ffffff;
color: #ff0000;
font-size: 16px;
width: 100%;
border-color: #00000000;
border:none;
line-height: 1.8;
}
#json-btn {
display: flex;
align-items: center;
font-size: 18px;
width:100%;
padding: 10;

}
#format_btn {
width: 50%;
height: 36px;
}
#save_btn {
width: 50%;
height: 36px;
margin-left: 4em;
}

</style>
<body>
<div style="padding: 2px 2px 2px 2px;">
<div id="json-btn" class="json-btn">
<button type="button" id="format_btn" onclick="format_btn();">Format</button>
<button type="button" id="save_btn" onclick="save_btn();">Verification</button>

</div>
<div>
<pre id="json-display" contenteditable="true"></pre>
</div>
<br>
</div>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.json-viewer.js"></script>
<script>

document.getElementById("json-display").addEventListener("input", function(){
console.log("json-display input");
json_parse.configContentChanged();
}, false);
function showJson(jsonObj){
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}
function format_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var jsonObj = JSON.parse(jsonval); //parse string to json
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}


function save_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var saveFailed = false;
try {
var jsonObj = JSON.parse(jsonval); //parse
} catch (e) {
console.error(e.message);
saveFailed = true;
json_parse.parseJsonException(e.message); // throw exception
}
if(!saveFailed) {
json_parse.saveConfig(jsonval);
}
}

</script>
</body>
</html>


这其中有两个问题需注意





    1. 如果value的值是url, 格式化后缺少引号
      从json-viewer.js源码可以发现,源码中会判断value是否是url,如果是则直接输出




处理方式:在json 左右添加上双引号


    if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + '"' +json + '"' + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}




    1. 如果折叠后json-viewer会增加<a>标签,即使使用text()方法获取到纯文本数据,这里面也包含了“n items”的字符串,那么该如何去除掉这些字符串呢?




 var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();

总结


使用时只需将json文件读取,传入preview_json.html的showJson方法


编辑结束后, 点击Save 即可保存


示例代码 Android 可视化编辑json JsonPreviewer


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

线程池也会导致OOM的原因

OOM
1. 前言 我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again 相信很多人碰到过这个错误,很...
继续阅读 »

1. 前言


我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again


相信很多人碰到过这个错误,很容易从网上搜索到出现这个问题的原因是线程过多,那线程过多为什么会导致OOM?线程什么情况下会释放资源?你又能如何做到让它不释放资源?


有的人可能会想到,那既然创建线程过多会导致OOM,那我用线程池不就行了。但是有没有想过,线程池,也可能会造成OOM。其实这里有个很经典的场景,你使用OkHttp的时候不注意,每次请求都创建OkHttpClient,导致线程池过多出现OOM


2. 简单了解线程池


如何去了解线程池,看源码,直接去看是很难看得懂的,要先了解线程池的原理,对它的设计思想有个大概的掌握之后,再去看源码,就会轻松很多,当然这里只了解基础的原理还不够,还需要有一些多线程相关的基础知识。


本篇文章只从部分源码的角度去分析,线程池如何导致OOM的,而不会全部去看所有线程池的源码细节,因为太多了


首先,要了解线程池,首先需要从它的参数入手:



  • corePoolSize:核心线程数量

  • maximumPoolSize:最大线程数量

  • keepAliveTime,unit:非核心线程的存活时间和单位

  • workQueue:阻塞队列

  • ThreadFactory:线程工厂

  • RejectedExecutionHandler:饱和策略


然后你从网上任何一个地方搜都能知道它大致的工作流程是,当一个任务开始执行时,先判断当前线程池数量是否达到核心线程数,没达到则创建一个核心线程来执行任务,如果超过,放到阻塞队列中等待,如果阻塞队列满了,未达到最大线程数,创建一条非核心线程执行任务,如果达到最大线程数,执行饱和策略。在这个过程中,核心线程不会回收,非核心线程会根据keepAliveTime和unit进行回收。


**这里可以多提一嘴,这个过程用了工厂模式ThreadFactory和策略模式RejectedExecutionHandler,关于策略模式可以看我这篇文章 ** juejin.cn/post/719502…


其实从这里就可以看出为什么线程池也会导致OOM了:核心线程不会回收,非核心线程使用完之后会根据keepAliveTime和unit进行回收 ,那核心线程就会一直存活(我这不考虑shutdown()和shutdownNow()这些情况),一直存活就会占用内存,那你如果创建很多线程池,就会OOM。


所以我这篇文章要分析:核心线程不会释放资源的过程,它内部怎么做到的。 只从这部分的源码去进行分析,不会全部都详细讲。


先别急,为了照顾一些基础不太好的朋友,涉及一些基础知识感觉还是要多讲一下。上面提到的线程回收和shutdown方法这些是什么意思?线程执行完它内部的代码后会主动释放资源吗?


我们都知道开发中有个概念叫生命周期,当然线程池和线程也有生命周期(这很重要),在开发中,我们称之为lifecycle。


生命周期当然是设计这个东西的开发者所定义的,我们先看线程池的生命周期,在ThreadPoolExecutor的注释中有写:


*
* The runState provides the main lifecycle control, taking on values:
*
* RUNNING: Accept new tasks and process queued tasks
* SHUTDOWN: Don't accept new tasks, but process queued tasks
* STOP: Don't accept new tasks, don't process queued tasks,
* and interrupt in-progress tasks
* TIDYING: All tasks have terminated, workerCount is zero,
* the thread transitioning to state TIDYING
* will run the terminated() hook method
* TERMINATED: terminated() has completed
*

看得出它的生命周期有RUNNING,SHUTDOWN,STOP,TIDYING和TERMINATED。而shutdown()和shutdownNow()方法会改变生命周期,这里不是对线程池做全面解析,所以先有个大概了解就行,可以暂时理解成这篇文章的所有分析都是针对RUNNING状态下的。


看完线程池的,再看看线程的生命周期。线程的生命周期有:



  • NEW:创建,简单来说就是new出来没start

  • RUNNABLE:运行,简单来说就是start后执行run方法

  • TERMINATED:中止,简单来说就是执行完run方法或者进行中断操作之后会变成这个状态

  • BLOCKED:阻塞,就是加锁之后竞争锁会进入到这个状态

  • WAITING、TIMED_WAITING:休眠,比如sleep方法


这个很重要,需要了解,你要学会线程这块相关的知识点的话,这些生命周期要深刻理解 。比如BLOCKED和WAITING有什么不同?然后学这块又会涉及到锁那一块的知识。以后有时间可以单独写几篇这类的文章,这里先大概有个概念,只需要能先看懂后面的源码就行。


从生命周期的概念你就能知道线程执行完它内部的代码后会主动释放资源,因为它run执行完之后生命周期会到TERMINATED,那这又涉及到了一个知识点,为什么主线程(ActivityThread),执行完run的代码后不会生命周期变成TERMINATED,这又涉及到Looper,就得了解Handler机制,可以看我这篇文章 juejin.cn/post/715882…


扯远了,现在进入正题,先想想,如果是你,你怎么做让核心线程执行完run之后不释放资源,很明显,只要让它不执行到TERMINATED生命周期就行,如何让它不变成TERMINATED状态,只需要让它进入BLOCKED或者WAITING状态就行。所以我的想法是这样的,当这个核心线程执行完这个任务之后,我让它WAITING,等到有新的任务进来的时候我再唤醒它进入RUNNABLE状态。 这是我从理论这个角度去分析的做法,那看看实际ThreadPoolExecutor是怎么做的


3. 线程池部分源码分析


前面说了,不会全部都讲,这里涉及到文章相关内容的流程就是核心线程的任务执行过程,所以这里主要分析核心线程。


当我们使用线程池执行一个任务时,会调用ThreadPoolExecutor的execute方法


public void execute(Runnable command) {
......

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}

// 我们只看核心线程的流程,所以后面的代码不用管
......
}

这个ctl是一个状态相关的代码,可以先不用管,我后面会简单统一做个解释,这里不去管它会比较容易理解,我们现在主要是为了看核心线程的流程。从这里可以看出,当前线程的数量小于核心线程的话执行addWorker方法


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

这个addWorker分为上下两部分,我们分别来做解析


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

// 下半部分
......
}

这里主要是做了状态判断的一些操作,我说过状态相关的我们可以先不管,但是这里的写法我觉得要单独讲一下为什么会这么写。不然它内部很多代码是这样的,我怕劝退很多人。


首先retry: ...... break retry; 这个语法糖,平常我们开发很少用到,可以去了解一下,这里就是为了跳出循环。 其次,这里的compareAndIncrementWorkerCount内部的代码是AtomicInteger ctl.compareAndSet(expect, expect + 1) ,Atomic的compareAndSet操作搭配死循环,这叫自旋,所以说要看懂这个需要一定的java多线程相关的基础。自旋的目的是为了什么?这就又涉及到了锁的分类中有乐观锁,有悲观锁。不清楚的可以去学一下这些知识,你就知道为什么它要这么做了,这里就不一一解释。包括你看它的源码,能看到,它会很多地方用自旋,很多地方用ReentrantLock,但它就是不用synchronized ,这些都是多线程这块基础的知识,这里不多说了。


看看下半部分


private boolean addWorker(Runnable firstTask, boolean core) {

// 上半部分
......



boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
......
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
......
}
return workerStarted;
}

看到它先创建一个Worker对象,再调用Worker对象内部的线程的start方法,我们看看Worker


private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

private static final long serialVersionUID = 6138294804551838833L;

final Thread thread;
Runnable firstTask;

Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

public void run() {
runWorker(this);
}

// 其它方法
......
}

看到它内部主要有两个对象firstTask就是任务,thread就是执行这个任务的线程,而这个线程是通过getThreadFactory().newThread(this)创建出来的,这个就是我们创建ThreadPoolExecutor时传的“线程工厂”

外部调t.start();之后就会执行这里的run方法,因为newThread传了this进去,你可以先简单理解调这个线程start会执行到这个run,然后run中调用runWorker(this);


注意,你想想runWorker(this)方法,包括之后的流程,都是执行在哪个线程中?都是执行在子线程中,因为这个run方法中的代码,都是执行在这个线程中。你一定要理解这一步,不然你自己看源码会可能看懵。 因为有些人长期不接触多线程环境的情况下,你会习惯单线程的思维去看问题,那就很容易出现理解上的错误。


我们继续看看runWorker,时刻提醒你自己,之后的流程都是在子线程中进行,这条子线程的生命周期变为RUNNABLE


final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
w.lock();

// 中断相关的操作
......

try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
......
} finally {
afterExecute(task, thrown);
}
} finally {
......
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

先讲讲这里的一个开发技巧,task.run()就是执行任务,它前面的beforeExecute和afterExecute就是模板方法设计模式,方便扩展用。

执行完任务后,最后执行processWorkerExit方法


private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}

tryTerminate();

......
}

workers.remove(w)后执行tryTerminate方法尝试将线程池的生命周期变为TERMINATED


final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

先不用管状态的变化,一般一眼都能看得出这里是结束的操作了,我们追踪的核心线程正常在RUNNING状态下是不会执行到这里的。 那我们期望的没任务情况下让线程休眠的操作在哪里?

看回runWorker方法


final void runWorker(Worker w) {
......
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
......
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

看到它的while中有个getTask()方法,认真看runWorker方法其实能看出,核心线程执行完一个任务之后会getTask()拿下一个任务去执行,这就是当核心线程满的时候任务会放到阻塞队列中,核心线程执行完任务之后会从阻塞队列中拿下一个任务执行。 getTask()从抽象上来看,就是从队列中拿任务。


private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
......

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

先把timed当成正常情况下为false,然后会执行workQueue.take(),这个workQueue是阻塞队列BlockingQueue, 注意,这里又需要有点基础了。正常有点基础的人看到这里,已经知道这里就是当没有任务会让核心线程休眠的操作,看不懂的,可以先了解下什么是AQS,可以看看我这篇文章 juejin.cn/post/716801…


如果你说你懒得看,行吧,我随便拿个ArrayBlockingQueue给你举例


public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}

notEmpty是Condition,这里调用了Condition的await()方法,然后想想执行这步操作的是在哪条线程上?线程进入WAITING状态了吧,不会进入TERMINATED了吧。


然后当有任务添加之后会唤醒它,它继续在循环中去执行任务。


这就验证了我们的猜想,通过让核心线程进入WAITING状态以此来达到执行完run方法中的任务也不会主动TERMINATED而释放线程。所以核心线程一直占用资源,这里说的资源指的是空间,而cpu的时间片是会让出的。


4. 部分线程池的操作解读


为什么线程池也会导致OOM,上面已经通过源码告诉你,核心线程不会释放内存空间,导致线程池多的情况下也会导致OOM。这里为了方便新手阅读ThreadPoolExecutor相关的代码,还是觉得写一些它内部的设计思想,不然没点基础的话确实很难看懂。


首先就是状态,上面源码中都有关线程池的生命中周期状态(ctl字段),可以看看它怎么设计的


private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // Integer.SIZE是32
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

它这里用了两个设计思想,第一个就是用位来表示状态,关于这类型的设计,可以看我这2篇文章 juejin.cn/post/715547…juejin.cn/post/720550…


另外一个设计思想是:用一个变量的高位置表示状态,低位表示数量。 这里就是用高3位来表示生命周期,剩下的低位表示线程的数量。和这个类似的操作有view中的MeasureSpec,也是一个变量表示两个状态。


然后关于设计模式,可以看到它这里最经典的就是用了策略模式,如果你看饱和策略那块的源码,可以好好看看它是怎么设计的。其它的还有工厂、模板之类的,这些也不难,就是策略还是建议学下它怎么去设计的。


然后多线程相关的基础,这个还是比较重要的,这块的基础不好,看ThreadPoolExecutor的源码会相对吃力。比如我上面提过的,线程的生命周期,锁相关的知识,还有AQS等等。如果你熟悉这些,再看这个源码就会轻松很多。


对于总体的设计,你第一看会觉得它的源码很绕,为什么会这样?因为有中断操作+自旋锁+状态的设计 ,它的这种设计就基本可以说是优化代码到极致,比如说状态的设计,就比普通的能省内存,能更方便通过CAS操作。用自旋就是乐观锁,能节省资源等。有中断操作,能让整个系统更灵活。相对的缺点就是不安全,什么意思呢?已是就是这样写代码很容易出BUG,所以这里的让人觉得很绕的代码,就是很多的状态的判断,这些都是为了保证这个流程的安全。


5. 总结


从部分源码的角度去分析,得到的结论是线程池也可能会导致OOM


那再思考一个问题:不断的创建线程池,“一定”会导致OOM吗? 如果你对线程池已经有一定的了解,相信你也知道这个问题的答案。


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

当我把ChatGPT拉进群聊里,我的朋友都玩疯了

前言 近期ChatGPT可以说是太火了,问答、写论文、写诗、写代码,只要输入精确的prompt,他的表现总是让人惊喜。本着打不过就加入的原则。要是把ChatGPT拉入群聊中,会是怎样一番场景?说做就做,花了1个晚上捣鼓了一个小Demo【ChatGPT群聊助手】...
继续阅读 »

前言


近期ChatGPT可以说是太火了,问答、写论文、写诗、写代码,只要输入精确的prompt,他的表现总是让人惊喜。本着打不过就加入的原则。要是把ChatGPT拉入群聊中,会是怎样一番场景?说做就做,花了1个晚上捣鼓了一个小Demo【ChatGPT群聊助手】,凭借它的“聪明才智”,应该可以搞定我的网友、女朋友、老妈的提问...


温馨提示:如果你从没体验过ChatGPT,给大家准备了一个新手体验Demo,免注册!免登陆!免代理!!!!!!,拉到文末可以快速查看噢。


使用效果


效果可看下图


微信图片_20230308154658.png


应用前景


虽Demo仅在小范围的群聊中测试,但ChatGPT语义理解和交互能力确实强大,不仅能联系对话的上下文,还能及时纠正代码bug。不经让人想到,若能将ChatGPT应用于聊天机器人软件,完成回答问题、提供服务、甚至解决问题的任务,帮助人们解决重复性或大量的人工工作,代替传统聊天机器人应用于客服、电商、教育和金融等行业。


相对于传统聊天机器人,ChatGPT可根据用户的要求和特性,及时调整回答的策略以便更准确的回答问题,有更人性化的体验。现在被广泛使用的智能客服还不够智能,ChatGPT所具备的能力,正是客服领域所需要的。


1 准备工作


在国内无法注册ChatGPT账户,因此需要准备如下:


能接收短信的国外手机号: 只需花几块钱,使用国外虚拟号码在线接收短信。可以去一些第三方平台如:sms-activate.org/cn


国外IP: 可以通过一些工具来实现,如通过工具使用美国节点IP。


这里需要注意的是,sms-activate.org选取手机号码国家的时候,建议选择印度,如果选择印度尼西亚,会在openAI报如下错误:


You’ve made too many phone verification requests. Please try again later or contact us through our help center at help.openai.com

微信图片_20230308154704.png


以上是必须的前提工作,有了以上准备工作后,就可以去chat.openai.com/auth/login注册账号了。


2 实现思路


2.1 技术现状


chatGPT提供了基于Web版的交互界面,不便于编程式调用。于是,我们可以通过模拟浏览器去登录,然后把交互过程封装成API接口。


2.2 实现过程


ChatGPT作为一个机器人角色加入群聊,需要在PC端转发ChatGPT问答。因此,我们可以在PC电脑上完成ChatGPT接口的封装,并加入群聊。然后通过即构IM(群聊)将数据实时传输,实现群聊里面与ChatGPT聊天。


微信图片_20230308154709.png


3 PC端封装代码实现


3.1 封装chatGPT调用


我们使用chatgpt-api库来封装调用chatGPT,因此先要安装好依赖库:


npm install chatgpt

安装好chtgpt库后,使用起来就非常简单了:


var ChatGPT, ConversationId, ParentMessageId;
var API_KEY = ;//这里填写KEY
(async () => {
const { ChatGPTAPI } = await import('chatgpt');
ChatGPT = new ChatGPTAPI({ apiKey: API_KEY})
})();
//向ChatGPT发出提问
function chat(text, cb) {
console.log("正在向ChatGPT发送提问:",text)
ChatGPT.sendMessage(text, {
conversationId: ConversationId,
parentMessageId: ParentMessageId
}).then(
function (res) {
ConversationId = res.conversationId
ParentMessageId = res.id
cb && cb(true, res.text)
console.log(res)
}
).catch(function (err) {
cb && cb(false, err);
});
}

注意到,在第二行需要填写API_KEY,登录OpenAI后,打开链接platform.openai.com/account/api…即可获取,如下图所示


微信图片_20230308154713.png


3.2 收发群聊消息


关于即构IM,如果大家感兴趣可以进入官网doc-zh.zego.im了解更多。总所周知,在即时聊天和实时音视频方面,即构IM是个人开发者或者中小型企业首选。因为我们只关注一对一私聊或者群聊,因此,在官方提供的SDK的基础上,我们做了二次封装。具体的封装代码请看附件,这里只贴出封装后的使用代码:


const Zego = require('./zego/Zego.js');

var zim;
function onError(err) {
console.log("on error", err);
}
//发送消息
function sendZegoMsg(isToGroup, text, toID){
Zego.sendMsg(zim, isToGroup, text, toID, function (succ, err) {
if (!succ) {
console.log("回复即构消息发送失败:", msg, err);
}
})
}
//收到消息回调
function onRcvZegoMsg(isFromGroup, msg, fromUID) {
var rcvText = msg.message ;

}
function main() {
let zegoChatGPTUID = "chatgpt"
zim = Zego.initZego(onError, onRcvZegoMsg, zegoChatGPTUID);

}
main();

在收到消息时,判断是否有@chatgpt关键字,如果有的话提取消息内容,然后去调用chatGPT封装好的接口等待ChatGPT回复,并将回复的内容往聊天群里发送。


4 手机端加入群聊与ChatGPT聊天


有了PC端实现后,接下来在手机端只需通过即构IM SDK向群里面@chatgpt发送提问消息即可,当然了,也可以在一对一私聊的时候@chatgpt然后调用chatGPT接口。这些都是可以根据实际需求定制开发,篇幅原因,这里我们只将群聊。


同样的,我们只关注收发消息,因此对即构官方提供的SDK做了二次封装。如果想了解更多细节可以前往官方文档阅读。


对登录ZIM、创建Token等代码这里不详细描述,感兴趣读者可以查看代码附件,代码很简单容易看懂。


首先封装Msg对象,表示消息实体类:


public class Msg {
public String msg;
public long time;
public String toUID;
public String fromUID;
public MsgType type;

public enum MsgType {
P2P,
GROUP
}
}

发送消息二次封装,同一群聊和一对一聊天接口:


public static void sendMsg(ZIM zim, Msg msg, ZIMMessageSentCallback cb) {
// 发送“单聊”通信的信息

ZIMTextMessage zimMessage = new ZIMTextMessage();
zimMessage.message = msg.msg;

ZIMMessageSendConfig config = new ZIMMessageSendConfig();
// 消息优先级,取值为 低:1 默认,中:2,高:3
config.priority = ZIMMessagePriority.LOW;
// 设置消息的离线推送配置
ZIMPushConfig pushConfig = new ZIMPushConfig();
pushConfig.title = "离线推送的标题";
pushConfig.content = "离线推送的内容";
pushConfig.extendedData = "离线推送的扩展信息";
config.pushConfig = pushConfig;
if (msg.type == Msg.MsgType.P2P)
zim.sendPeerMessage(zimMessage, msg.toUID, config, cb);
else
zim.sendGroupMessage(zimMessage, msg.toUID, config, cb);
}

二次封装接收消息,统一通过onRcvMsg函数接收消息。


private void onRcvMsg(ArrayList<ZIMMessage> messageList) {
if (lsArr == null) return;
for (ZIMMessage zimMessage : messageList) {
if (zimMessage instanceof ZIMTextMessage) {
ZIMTextMessage zimTextMessage = (ZIMTextMessage) zimMessage;
if (zimMessage.getTimestamp() < this.startTime)
continue;
String fromUID = zimTextMessage.getSenderUserID();
ZIMConversationType ztype = zimTextMessage.getConversationType();
String toUID = zimTextMessage.getConversationID();
Msg.MsgType type = Msg.MsgType.P2P;
if (ztype == ZIMConversationType.PEER) type = Msg.MsgType.P2P;
else if (ztype == ZIMConversationType.GROUP) type = Msg.MsgType.GROUP;
String data = zimTextMessage.message;
Msg msg = new Msg(type, data, zimMessage.getTimestamp(), fromUID, toUID);
for (MsgCenterListener l : lsArr) l.onRcvMsg(msg);
}
}
}
private ZIMEventHandler handler = new ZIMEventHandler() {

@Override
public void onReceivePeerMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromUserID) {
onRcvMsg(messageList);
}



@Override
public void onReceiveGroupMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromGroupID) {
onRcvMsg(messageList);
}

@Override
public void onTokenWillExpire(ZIM zim, int second) {
onRenewToken();
}
};

需要注意的是,因为我们目前场景只需关注文本消息,因此没有图片、文件之类的消息做过多考虑。如果有类似需求的读者可以根据官方文档进一步封装。


另外,为了简化,避免每次用户主动拉chatgpt进入一个新群,我们先约好一个超大群ID:group_chatgpt。每次新用户登录就加入这个大群就好。如果有更加细粒度控制需求,可以根据不同用户来创建不同群,然后向chatgpt机器人发送群ID,在PC端开发对应的自动加入对应群功能就好。


对于加群逻辑,也做了二次封装:


public void joinGroup(String groupId) {
zim.joinGroup(groupId, new ZIMGroupJoinedCallback() {
@Override
public void onGroupJoined(ZIMGroupFullInfo groupInfo, ZIMError errorInfo) {
for (MsgCenterListener l : lsArr)
l.onJoinGroup(groupId);
}
});

至此,整个流程开发完成,尽情享受ChatGPT吧。


5 开发者福利


除ChatGPT之外,Demo中使用的开发者工具ZIM SDK也是提升工作效率的利器,ZIM SDK提供了全面的 IM 能力,满足文本、图片、语音等多种消息类型,在线人数无上限,支持亿量级消息并发。同时支持安全审核机制,确保消息安全合规。


ZIM SDK提供了快速集成、接口丰富、成熟的即时通讯解决方案。满足多种业务场景通讯需求,适用于打造大型直播、语聊房、客服系统等场景。即构即时通讯产品 IM 开春钜惠低至1折,限时折扣专业版1200元http://www.zego.im/activity/ze…,也可搭配元宇宙和直播间其他产品组合使用。感兴趣的开发者可到即构官网去注册体验doc-zh.zego.im/article/115…


6 完整代码



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

我竟然突然焦虑,并且迷茫了

【随想录】我尽然突然焦虑,并且迷茫了 「随想录」 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 最近是怎么了 最近几个朋友,突然询问我,现在应该...
继续阅读 »

【随想录】我尽然突然焦虑,并且迷茫了



「随想录」


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



最近是怎么了


最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。



所以说我真的没有一个准确的答案回复。但是我以为目前的眼光来看一份好工作必备的条件就是,我在这个领域学的越多,我的工资和个人发展瓶颈越高,这份工作是一个持续学习的过程,并且回报和提高是肉眼可见的!



回忆那个时候


其实说实话,这个疑惑我上大一就开始有,但是那个时候是从高考的失落中寻找升学的路径,开始无脑的刷那种考研短视频



(看过可能都知道真的一下子励志的心就有了,但是回到现实生活中,看到身边人的状态~~~没错人就是一个从众的种群,你可能会问你会不会因为大一没有那么努力学习而后悔,但是其实我不会,因为那一年我的经历也是我最开心大学生活,虽然也干了很多被室友做成梗的糗事,但是想一想那不就是青春嘛,要是从小就会很有尺度的为人处世,想一想活着也很累嘛,害,浅浅致敬一下充满快乐和遗憾的青春呀!)


个人看法


哈哈,跑题了。给大家点力量把!前面满满的焦虑。其实我感觉我们都应该感谢我们来到计算机类的专业,从事这方面的学习和研究。


因为计算机的扩展性,不得不说各行各业都开始越来越喜欢我们计算机毕业的大学生(就业方向更加广),我也因为自己会计算机,成功进入一个一本高校以上的教育类公司实习(同时也是这个时候知道了更多优秀学校的毕业年轻人,真正认识到学校的层次带给人的很多东西真正的有差距);



虽然我是二本的学生,但是在亲戚朋友眼里,虽然学校比不上他们的孩子,但是计算机专业也能获得浅浅的也是唯一一点可以骄傲的东西(活在别人嘴这种思考方式肯定不是对的,但是现实就是在父母那里,我们考上什么大学和进入了哪里工作真的是他们在外人的脸面,这种比较情况在大家族或者说农村尤为严重);



技术论打败学校论,计算机专业是在“广义”上为数不多能打破学校出身论的学科,在公司上只要你能干活,公司就愿意要你,这个时候肯定有人diss我,现在培训班出来的很多都找不到工作呀,我的回答只能是:的确,因为这个行业的红利期展示达到了瓶颈期,加上大环境的不理想,会受到一些影响,但是我还是相信会好的,一切都会好的。



做技术既然这样了


关于最近论坛上说“前段已死”“后端当牛做马”“公司磨刀霍霍向测试”......



这个东西怎么说,我想大部分人看到这个都会被这个方向劝退,我从两个角度分析一下,上面说了,真滴卷,简历真滴多,存在过饱和;第二点,希望这个领域新人就不要来了,就是直接劝退,被让人来卷,狭义上少卷一些......



现在就是导致我也不敢给朋友做建议了,因为当他看到这些的时候,和进入工作环境真的不好,我真的怕被喷死


包括现在我的实习,大家看我的朋友圈看出工作环境不错很好,但是和工作的另一面,是不能发的呀,有时候我都笑称自己是“产业工人”(这个词是一个朋友调侃我的)


不行了,在传播焦虑思想,我该被喷死了,现在我给建议都变得很含蓄,因为时代红利期真的看不透,我也不敢说能维持多少年,而且我工作也一般,我不敢耽误大家(哈哈哈,突然想起一句话,一生清贫怎敢入繁华,二袖清风怎敢误佳人,又是emo小文案,都给我开E)


个人总结


本文就是调侃一下现在的环境啊,下面才是重点,只有干活和真话放在后面(印证一个道理:看到最后的才是真朋友才敢给真建议,我也不怕被骂)



心态方面:我们这个年纪就是迷茫的年纪,迷茫是一种正常的状态,因为作为一名成年人你真正在思考你的个人发展的状态,所以请把心放大,放轻松,你迷茫了已经比身边的人强太多了,如果真正焦虑的不能去学习了,去找个朋友聊一聊,实在不行,drink个两三瓶,好好睡一觉,第二天继续干,这摸想,这些都算个啥,没事你还有我,实在不行微我聊一聊,我永远都在,我的朋友!



工作方面:俗话说:女怕入错行,男怕娶错人!(突然发现引用没什么用,哈哈)我们可以多去实践,没错就是去实习,比如你想做前端的工作,你就可以直接去所在的城市(推荐省会去找实习)但是朋友其实实习很难,作为过来人,我能理解你,一个人在陌生的城市而且薪资很可怜,面对大城市的租房和吃饭有很多大坑,你要一一面对,但是在外面我们真要学会保护自己,而且实习生活中经济方面肯定要父母支持,所以一定要和父母好好沟通,其实你会发现我们越长大,和父母相处的时光越短。(我今年小年和十五都没在家过,害,那种心理苦的滋味很不好受)



升学方面:不是每一个都适合考研,不要盲从考研。但是这句话又是矛盾的,在我的实习生涯中,学历问题是一个很重要的问题,我们的工作类型真的不同,还是那句话,学历只是一个门槛,只要你迈入以后看的是你的个人能力。说一句悄悄话,我每天工作,最想的事情就是上学,心想老子考上研,不在干这活了,比你们都强。所以你要想考研,请此刻拿出你的笔,在纸上写下你要考研主要三个理由,你会更好的认识自己,更好选择。



好吧,今天的随想录就这摸多,只是对最近看文章有了灵感写下自己的看法,仅供参考哦!


回答问题


回应个问题:很多朋友问我为什么给这摸无私的建议,这是你经历了很多才得到的,要是分享出去,不是很亏?


(你要这摸问,的确你有卷到我的可能性,快给我爬。哈哈哈)可能是博客圈给的思想把,其实我说不上开源的思想,但是我遇到的人对我都是无私分享自己的经验和自己走过的坑,就是你懂吗,他们对我帮助都很大,他们在我眼里就是伟大的人,所以我也想要跟随他们,做追光的人!(上价值了哦,哈哈)



写在最后


最后一句话,迷茫这个东西,走着走着就清晰了,迷茫的时候,搞一点学习总是没错的。


作者:武师叔
来源:juejin.cn/post/7201752978259378232
收起阅读 »

25岁在培训班学java个人看法见解

前言 hello,大家好,我是小江。前段时间我发了一篇我在培训java感受的文章,大家都挺感兴趣的,也问了很多问题。这段时间我们刚好在写项目,昨天项目已经结束验收,现在有空在写一篇文章解答你们问的各种问题。首先说明,文章仅代表我个人观点,我个人的真实感受,而且...
继续阅读 »

前言


hello,大家好,我是小江。前段时间我发了一篇我在培训java感受的文章,大家都挺感兴趣的,也问了很多问题。这段时间我们刚好在写项目,昨天项目已经结束验收,现在有空在写一篇文章解答你们问的各种问题。首先说明,文章仅代表我个人观点,我个人的真实感受,而且我不会指明说出任何一个培训机构名,上篇文章大家要是看了应该知道,我全篇没有说任何一个培训机构名,我自己所在的也不会说。好了不多说,开始解答常见的问题吧。


1、行情不好了,还能入这行吗?


说实话,这个问题我也不知道怎么回答。确实现在行情越来越差了,要求越来越高了,学完找不到工作的肯定是有的,但我觉得吧,海投一直找不到工作的,要么就是技术太差,要么就是要求高了。可能前几年这技术和学历可以轻松找到1w的,现在技术要求高了点还只给6k,落差太大,就会不想去,然后发现其他的都是这样了,就抱怨起来了贩卖焦虑了。


为什么之前那些年好找工作,要求又低工资又高,只是躺在了时代的红利上。人都不是傻子,知道什么行业赚钱,必然会往这行拥入。人多了,那肯定是这样的结果。那到底现在能不能入这行呢?我个人觉得吧,你需要满足:


①学历要达到大专以上,最好是本科;

②年龄不要超过28岁,最好是26岁以下;

③自己能坚持,如果学技术要学好,能放下游戏等各种娱乐;


这是我个人看法,不知道大家有没有不同的意见,我认为一个24岁左右本科学历的刻苦学完编程,技术也可以,找个开发工作问题不大的。


2、入行是自学还是报班?


这个问题我不会提到任何一个培训机构名字,以免大家认为我是托。我来说说我的看法,我认为自制力强的、学习力OK的、时间充足的,完全可以自学的,我身边也有自学朋友。我入这行还是受我那朋友的启发和推荐的,我和我那朋友之前是在一个公司上班,不是it,干了大半年,他辞职了,说要搞编程,我没在意,觉得编程离我太远和我没有交集,又是将近半年过去了,我和他联系,他说他已经上班干开发了税后除去社保7K,我才开始和他聊这行,我也心动了。他说他自己就是自学的,大学专业搭点边,学了一点编程,但都忘了。他说他辞职后在家天天学习,除了吃饭就是学习,这点我是相信的,之前和他一个公司他也确实做事很认真。我后来不是也准备入行编程,我也想着学他自学,真正开始后,才知道人和人是有差距的,我心静不下,学一个小时就看不下去,所以我后来报了班。


这个问题,我的总结就是:

①自学可以也可行,关键看你有没有那毅力,每天能学8小时以上,并且是真正有效率的学了8小时,那你可以自学,网上资料都全,不会的可以查资料百度,或者花点小钱让别的大神帮你看看。


②自制力不行,又想入这行的,培训班也是一个选择,只是学费不便宜,成本大。培训班肯定是有用的,能学到东西,系统化都给你规划好了,资源齐全,讲的也细,也会讲很多企业实际开发需要注意的东西,练习题和项目都恰到好处的提升自己,不会不懂的可以随时问老师,最后模拟面试、指导简历什么都安排好了,确实能做到速成。不然也不会这么多培训出来的占满了这行业,使这行越来越卷。


③如果报班了,最好选个靠外那座位坐,不要坐里面,我就是坐里面靠墙,问老师问题都不好问,很坑。如果同桌是个技术很好的,那你就很幸运了,一个技术好的同桌太重要了。还有就是多问老师问题,你是花钱的,有问题就问,不要怕。


3、我在培训这两个多月后现在学的咋样?


我这一共四个阶段,我现在刚好学完第二个阶段,第一阶段基础javase,第二阶段javaweb,刚刚结束第二阶段做完项目,每个阶段结束都会有将近一个星期写项目。这次项目是小组一起开发,使用SVN工具小组一起自己分工一个写一个模块,最后整合,模拟企业开发。项目还可以,刚开始觉得很难,熟悉后就好了,难点不多,大都是crud,我感觉思路很重要,不要一上来就噼里啪啦开始写,先分析好,后面会越来越顺。等写完真的有成就感,项目确实很能锻炼自己,升华自己,查漏补缺。放几张照片看看我写的项目吧


页面显示效果截选:


image.png


后端代码截选:
image.png
前端代码截选:
image.png


结束语


我相信大家入行这个的,应该都和我一样,觉得工资还可以,工作看起来还体面。但入这行越久了解越深,就知道也有很多不容易,要付出很大的努力。如果刚踏入这行开始学的,一定要好好学,基础蛮重要,可能学着学着会迷茫,不要怀疑自己,多学多敲多思考,没有学不会的。最后送大家一句诗:路漫漫其修远兮,吾将上下而求索。


作者:学习编程的小江
来源:juejin.cn/post/7203294201312657469
收起阅读 »

改行后我在做什么?(2022-9-19日晚)

闲言碎语 今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。 但在我这个年纪,这个阶段,看似有很多选择...
继续阅读 »

闲言碎语



今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。




但在我这个年纪,这个阶段,看似有很多选择,但其实我没有选择。能做的也只是多挣点钱。




在这个信息爆炸的时代,我们知道更高的地方在哪里。但当你想要再往上走一步的时候,你发现你的上限,其实从出生或从你毕业的那一刻就已经注定了。可能少部分人通过自身的努力,的确能突破壁垒达到理想的高度。但这只是小概率事件罢了。在我看来整个社会的发展,其实早就已经陷入了一种怪圈。




在我,早些年刚刚进入社会的时候。那时的想法特别简单。就想着努力工作,努力提升自身的专业素养。被老板赏识,升职加薪成为一名管理者。如果,被淘汰了那应该是自己不够优秀,不够努力,专业技能不过硬,自己为人处事不够圆滑啥的。




内卷这个词语引爆网络的时候;当35岁被裁员成为常态的时候。再回头看我以前的那些想法那真的是一个笑话。(我觉得我可能是在为自己被淘汰找借口)



当前的状态



游戏工作室的项目,目前基本处于停滞的状态。我不敢加机器也不敢关机。有时候我都在想,是不是全中国那3-4亿的人都在搞这个?一个国外的游戏,金价直接拉成这个逼样。




汽配这边的话,只能说喝口稀饭。(我花了太多精力在游戏工作室上了)



梦想破灭咯



其实按照正常情况来说,游戏工作室最开始的阶段,我应该是能够稍微挣点钱的。我感觉我天时、地利、人和。我都占的。现在来看的话,其实我只占了人和。我自己可以编码,脚本还是从驱动层模拟键鼠,写的一套脚本。这样我都没赚钱,我擦勒。



接下来干嘛



接下来准备进厂打螺丝。(开玩笑的)
还是老老实实跟着我弟学着做生意吧。老老实实做汽配吧!在这个时代,好像有一技之长(尤其是IT)的人,好像并不能活得很好。除非,你这一技之长,特别特别长。(当下的中国不需要太多的这类专业技术人员吧。)



我感受到的大环境



我身边有蛮多的大牛。从他们的口中和我自己看到的。我感觉在IT这个领域,国内的环境太恶劣了。在前端,除开UI库,我用到的很多多的库全是老外的。为什么没有国人开源呢?因为,国人都忙着996了。我们可以在什么都不知道的情况下,通过复制粘贴,全局搜索解决大部分问题。 机械视觉、大数据分析、人工智能 等很多东西。这一切的基石很多年前就有了,为什么没人去研究他?为什么我们这波人,不断的在学习:这样、那样的框架。搭积木虽然很好玩。但创造一个积木,不应该也是一件更有挑战性的事情么?




在招聘网站还有一个特别奇怪的现象。看起来这家公司是在招人,但其实是培训机构。 看起来这家公司正儿八经是在招聘兼职,但其实只想骗你去办什么兼职卡。看起来是在招送快递,送外卖的,招聘司机的,但其实只是想套路你买车。我擦勒。这是怎样的一个恶劣的生存环境。这些个B人就不能干点,正经事?




卖菜的、拉车的、搞电商的、搞短视频、搞贷款的、卖保险的、这些个公司市值几百亿。很难看到一些靠创新,靠创造,靠产品质量,发展起来的公司。


作者:wjt
来源:juejin.cn/post/7144770465741946894

收起阅读 »

从framework角度看app保活问题

问题背景 最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论 不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发...
继续阅读 »

问题背景


最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论


保活


不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发者,根本无能为力,可真的是这样的吗?


保活方案


首先,我整理了从古到今,app开发者所使用过的以及当前还在使用的保活方式,主要思路有两个:保活和复活


保活的方案有:



  • 1像素惨案




  • 后台无声音乐




  • 前台service




  • 心跳机制




  • socket长连接




  • 无障碍服务




  • ......




复活的方案有:


  • 双进程守护(java层和native层)

  • JobScheduler定时任务

  • 推送/相互唤醒

  • ......


不难看出,app开发者为了能让自己的应用多存活一会儿,可谓是绞尽脑汁,但即使这样,随着Android系统升级,尤其是进入8.0之后,系统对应用的限制越来越高,传统的保活方式已经不生效,这让Android开发者手足无措,于是乎,出现了一种比较和谐的保活方式:



  • 引导用户开启手机白名单


这也是目前绝大多数应用所采用的的方式,相对于传统黑科技而言,此方式显得不那么流氓,比较容易被用户所接受。


但跟微信这样的国民级应用比起来,保活效果还是差了一大截,那么微信是怎么实现保活的呢?或者回到我们开头的问题,应用的生死真的只能靠系统调度吗?开发者能否干预控制呢?


进程调度原则


解开这个疑问之前,我们需要了解一下Android系统进程调度原则,主要介绍framework中承载四大组件的进程是如何根据组件状态而动态调节自身状态的。进程有两个比较重要的状态值:




  • oom_adj,定义在frameworks/base/services/core/java/com/android/server/am/ProcessList.java当中




  • procState,定义在frameworks/base/core/java/android/app/ActivityManager.java当中




OOM_ADJ

以Android10的源码为例,oom_adj划分为20级,取值范围[-10000,1001],Android6.0以前的取值范围是[-17,16]




  • oom_adj值越大,优先级越低




  • oom_adj<0的进程都是系统进程。




public final class ProcessList {
static final String TAG = TAG_WITH_CLASS_NAME ? "ProcessList" : TAG_AM;

// The minimum time we allow between crashes, for us to consider this
// application to be bad and stop and its services and reject broadcasts.
static final int MIN_CRASH_INTERVAL = 60 * 1000;

// OOM adjustments for processes in various states:

// Uninitialized value for any major or minor adj fields
static final int INVALID_ADJ = -10000;

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 1001;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;

// This is the oom_adj level that we allow to die first. This cannot be equal to
// CACHED_APP_MAX_ADJ unless processes are actively being assigned an oom_score_adj of
// CACHED_APP_MAX_ADJ.
static final int CACHED_APP_LMK_FIRST_ADJ = 950;

// Number of levels we have available for different service connection group importance
// levels.
static final int CACHED_APP_IMPORTANCE_LEVELS = 5;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 800;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app. This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 700;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 600;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 500;

// This is a process with a heavy-weight application. It is in the
// background, but we want to try to avoid killing it. Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 400;

// This is a process currently hosting a backup operation. Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 300;

// This is a process bound by the system (or other app) that's more important than services but
// not so perceptible that it affects the user immediately if killed.
static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 200;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 100;
static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

// This is a process that was recently TOP and moved to FGS. Continue to treat it almost
// like a foreground app for a while.
// @see TOP_TO_FGS_GRACE_PERIOD
static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

// This is the process running the current foreground app. We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -700;

// This is a system persistent process, such as telephony. Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -800;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -900;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -1000;

// Memory pages are 4K.
static final int PAGE_SIZE = 4 * 1024;

//省略部分代码
}

ADJ级别取值说明(可参考源码注释)
INVALID_ADJ-10000未初始化adj字段时的默认值
UNKNOWN_ADJ1001缓存进程,无法获取具体值
CACHED_APP_MAX_ADJ999不可见activity进程的最大值
CACHED_APP_MIN_ADJ900不可见activity进程的最小值
CACHED_APP_LMK_FIRST_ADJ950lowmemorykiller优先杀死的级别值
SERVICE_B_ADJ800旧的service的
PREVIOUS_APP_ADJ700上一个应用,常见于应用切换场景
HOME_APP_ADJ600home进程
SERVICE_ADJ500创建了service的进程
HEAVY_WEIGHT_APP_ADJ400后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ300备份进程
PERCEPTIBLE_LOW_APP_ADJ250受其他进程约束的进程
PERCEPTIBLE_APP_ADJ200可感知组件的进程,比如背景音乐播放
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ50最近运行的后台进程
FOREGROUND_APP_ADJ0前台进程,正在与用户交互
PERSISTENT_SERVICE_ADJ-700系统持久化进程已绑定的进程
PERSISTENT_PROC_ADJ-800系统持久化进程,比如telephony
SYSTEM_ADJ-900系统进程
NATIVE_ADJ-1000native进程,不受系统管理

可以通过cat /proc/进程id/oom_score_adj查看目标进程的oom_adj值,例如我们查看电话的adj


dialer_oom_adj


值为935,处于不可见进程的范围内,当我启动电话app,再次查看


dialer_oom_adj_open


此时adj值为0,也就是正在与用户交互的进程


ProcessState

process_state划分为23类,取值范围为[-1,21]


@SystemService(Context.ACTIVITY_SERVICE)
public class ActivityManager {
//省略部分代码
/** @hide Not a real process state. */
public static final int PROCESS_STATE_UNKNOWN = -1;

/** @hide Process is a persistent system process. */
public static final int PROCESS_STATE_PERSISTENT = 0;

/** @hide Process is a persistent system process and is doing UI. */
public static final int PROCESS_STATE_PERSISTENT_UI = 1;

/** @hide Process is hosting the current top activities. Note that this covers
* all activities that are visible to the user. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_TOP = 2;

/** @hide Process is hosting a foreground service with location type. */
public static final int PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3;

/** @hide Process is bound to a TOP app. This is ranked below SERVICE_LOCATION so that
* it doesn't get the capability of location access while-in-use. */

public static final int PROCESS_STATE_BOUND_TOP = 4;

/** @hide Process is hosting a foreground service. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 5;

/** @hide Process is hosting a foreground service due to a system binding. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 6;

/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 7;

/** @hide Process is important to the user, but not something they are aware of. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 8;

/** @hide Process is in the background transient so we will try to keep running. */
public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 9;

/** @hide Process is in the background running a backup/restore operation. */
public static final int PROCESS_STATE_BACKUP = 10;

/** @hide Process is in the background running a service. Unlike oom_adj, this level
* is used for both the normal running in background state and the executing
* operations state. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_SERVICE = 11;

/** @hide Process is in the background running a receiver. Note that from the
* perspective of oom_adj, receivers run at a higher foreground level, but for our
* prioritization here that is not necessary and putting them below services means
* many fewer changes in some process states as they receive broadcasts. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_RECEIVER = 12;

/** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
public static final int PROCESS_STATE_TOP_SLEEPING = 13;

/** @hide Process is in the background, but it can't restore its state so we want
* to try to avoid killing it. */

public static final int PROCESS_STATE_HEAVY_WEIGHT = 14;

/** @hide Process is in the background but hosts the home activity. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_HOME = 15;

/** @hide Process is in the background but hosts the last shown activity. */
public static final int PROCESS_STATE_LAST_ACTIVITY = 16;

/** @hide Process is being cached for later use and contains activities. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_CACHED_ACTIVITY = 17;

/** @hide Process is being cached for later use and is a client of another cached
* process that contains activities. */

public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 18;

/** @hide Process is being cached for later use and has an activity that corresponds
* to an existing recent task. */

public static final int PROCESS_STATE_CACHED_RECENT = 19;

/** @hide Process is being cached for later use and is empty. */
public static final int PROCESS_STATE_CACHED_EMPTY = 20;

/** @hide Process does not exist. */
public static final int PROCESS_STATE_NONEXISTENT = 21;
//省略部分代码
}

state级别取值说明(可参考源码注释)
PROCESS_STATE_UNKNOWN-1不是真正的进程状态
PROCESS_STATE_PERSISTENT0持久化的系统进程
PROCESS_STATE_PERSISTENT_UI1持久化的系统进程,并且正在操作UI
PROCESS_STATE_TOP2处于栈顶Activity的进程
PROCESS_STATE_FOREGROUND_SERVICE_LOCATION3运行前台位置服务的进程
PROCESS_STATE_BOUND_TOP4绑定到top应用的进程
PROCESS_STATE_FOREGROUND_SERVICE5运行前台服务的进程
PROCESS_STATE_BOUND_FOREGROUND_SERVICE6绑定前台服务的进程
PROCESS_STATE_IMPORTANT_FOREGROUND7对用户很重要的前台进程
PROCESS_STATE_IMPORTANT_BACKGROUND8对用户很重要的后台进程
PROCESS_STATE_TRANSIENT_BACKGROUND9临时处于后台运行的进程
PROCESS_STATE_BACKUP10备份进程
PROCESS_STATE_SERVICE11运行后台服务的进程
PROCESS_STATE_RECEIVER12运动广播的后台进程
PROCESS_STATE_TOP_SLEEPING13处于休眠状态的进程
PROCESS_STATE_HEAVY_WEIGHT14后台进程,但不能恢复自身状态
PROCESS_STATE_HOME15后台进程,在运行home activity
PROCESS_STATE_LAST_ACTIVITY16后台进程,在运行最后一次显示的activity
PROCESS_STATE_CACHED_ACTIVITY17缓存进程,包含activity
PROCESS_STATE_CACHED_ACTIVITY_CLIENT18缓存进程,且该进程是另一个包含activity进程的客户端
PROCESS_STATE_CACHED_RECENT19缓存进程,且有一个activity是最近任务里的activity
PROCESS_STATE_CACHED_EMPTY20空的缓存进程,备用
PROCESS_STATE_NONEXISTENT21不存在的进程

进程调度算法

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java中,有三个核心方法用于计算和更新进程的oom_adj值



  • updateOomAdjLocked():更新adj,当目标进程为空,或者被杀则返回false,否则返回true。

  • computeOomAdjLocked():计算adj,计算成功返回true,否则返回false。

  • applyOomAdjLocked():应用adj,当需要杀掉目标进程则返回false,否则返回true。


adj更新时机

也就是updateOomAdjLocked()被调用的时机。通俗的说,只要四大组件被创建或者状态发生变化,或者当前进程绑定了其他进程,都会触发adj更新,具体可在源码中查看此方法被调用的地方,比较多,这里就不列举了


adj的计算过程

computeOomAdjLocked()计算过程相当复杂,将近1000行代码,这里就不贴了,有兴趣可自行查看,总体思路就是根据当前进程的状态,设置对应的adj值,因为状态值很多,所以会有很多个if来判断每个状态是否符合,最终计算出当前进程属于哪种状态。


adj的应用

计算得出的adj值将发送给lowmemorykiller(简称lmk),由lmk来决定进程的生死,不同的厂商,lmk的算法略有不同,下面是源码中对lmk的介绍


/* drivers/misc/lowmemorykiller.c
*
* The lowmemorykiller driver lets user-space specify a set of memory thresholds
* where processes with a range of oom_score_adj values will get killed. Specify
* the minimum oom_score_adj values in
* /sys/module/lowmemorykiller/parameters/adj and the number of free pages in
* /sys/module/lowmemorykiller/parameters/minfree. Both files take a comma
* separated list of numbers in ascending order.
*
* For example, write "0,8" to /sys/module/lowmemorykiller/parameters/adj and
* "1024,4096" to /sys/module/lowmemorykiller/parameters/minfree to kill
* processes with a oom_score_adj value of 8 or higher when the free memory
* drops below 4096 pages and kill processes with a oom_score_adj value of 0 or
* higher when the free memory drops below 1024 pages.
*
* The driver considers memory used for caches to be free, but if a large
* percentage of the cached memory is locked this can be very inaccurate
* and processes may not get killed until the normal oom killer is triggered.
*
* Copyright (C) 2007-2008 Google, Inc.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/


保活核心思路


根据上面的Android进程调度原则得知,我们需要尽可能降低app进程的adj值,从而减少被lmk杀掉的可能性,而我们传统的保活方式最终目的也是降低adj值。而根据adj等级分类可以看出,通过应用层的方式最多能将adj降到100~200之间,我分别测试了微信、支付宝、酷狗音乐,启动后返回桌面并息屏,测试结果如下


微信测试结果:


weixin_oom_adj


微信创建了两个进程,查看这两个进程的adj值均为100,对应为adj等级表中的VISIBLE_APP_ADJ,此结果为测试机上微信未登录状态测试结果,当换成我的小米8测试后发现,登录状态下的微信有三个进程在运行


weixin_login_oom_adj


后查阅资料得知,进程名为com.tencent.soter.soterserver的进程是微信指纹支付,此进程的adj值居然为-800,上面我们说过,adj小于0的进程为系统进程,那么微信是如何做到创建一个系统进程的,我和我的小伙伴都惊呆了o.o,为此,我对比了一下支付宝的测试结果


支付宝测试结果:


alipay_oom_adj


支付宝创建了六个进程,查看这六个进程的adj值,除了一个为915,其余均为0,怎么肥事,0就意味着正在与用户交互的前台进程啊,我的世界要崩塌了,只有一种可能,支付宝通过未知的黑科技降低了adj值。


酷狗测试结果:


kugou_oom_adj.png


酷狗创建了两个进程,查看这两个进程的adj值分别为700、200,对应为adj等级表中的PREVIOUS_APP_ADJPERCEPTIBLE_APP_ADJ,还好,这个在意料之中。


测试思考


通过上面三个app的测试结果可以看出,微信和支付宝一定是使用了某种保活手段,让自身的adj降到最低,尤其是微信,居然可以创建系统进程,简直太逆天了,这是应用层绝对做不到的,一定是在native层完成的,但具体什么黑科技就不得而知了,毕竟反编译技术不是我的强项。


正当我郁郁寡欢之时,我想起了前两天看过的一篇文章《当 App 有了系统权限,真的可以为所欲为?》,文章讲述了第三方App如何利用CVE漏洞获取到系统权限,然后神不知鬼不觉的干一些匪夷所思的事儿,这让我茅塞顿开,或许这些大厂的app就是利用了系统漏洞来保活的,不然真的就说不通了,既然都能获取到系统权限了,那创建个系统进程不是分分钟的事儿吗,还需要啥厂商白名单。


总结


进程保活是一把双刃剑,增加app存活时间的同时牺牲的是用户手机的电量,内存,cpu等资源,甚至还有用户的忍耐度,作为开发者一定要合理取舍,不要为了保活而保活,即使需要保活,也尽量采用白色保活手段,别让用户手机变板砖,然后再来哭爹骂娘。


参考资料:


探讨Android6.0及以上系统APP常驻内存(保活)实现-争宠篇


探讨Android6.0及以上系统APP常驻内存(保活)实现-复活篇


探讨一种新型的双进程守护应用保活


史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术


当 App 有了系统权限,真的可以为所欲为?


「 深蓝洞察 」2022 年度最“不可赦”漏洞


作者:小迪vs同学
来源:juejin.cn/post/7210375037114138680
收起阅读 »

上千行代码的输入框的逻辑是什么?

web
需求 我们要做一个前端需求:需要一个输入框,支持 KQL 语法,支持智能匹配,前提条件纯前端实现。 该功能详见 kibana es7版本。有条件的可以去使用一下,感受一番。 需求分析 使用了一下该功能,感觉还是挺复杂的。不好实现啊,我,我,我。。。 不过因为...
继续阅读 »

需求


我们要做一个前端需求:需要一个输入框,支持 KQL 语法,支持智能匹配,前提条件纯前端实现。


该功能详见 kibana es7版本。有条件的可以去使用一下,感受一番。


image.png


需求分析


使用了一下该功能,感觉还是挺复杂的。不好实现啊,我,我,我。。。


不过因为 kibana 是开源的,我就去 github 上看了一下源码。



  • 首先人家是 React 版本,我的项目是 Vue 版本,我不能行使拿来主义。

  • 一个 input 框的核心代码写了一千多行,不包括一些工具函数,公共组件之类。


方案




  1. 我先研究源码,再把研究好的源码转成 vue 版本输出?


    该方案短时间内看不到效果,需要好好梳理其源码。是一个 0 或者 1 的问题,如果研究好了并实现转化出来,那就是 1,如果期间遇到问题阻塞了,那就是短时间看不到产出效果。不敢冒险。




  2. 创建一个 React 项目,把相关的这部分代码拆分出来,以微前端的方式内嵌到我的项目中?


    不知道在拆分代码和组装代码的过程中会遇到什么问题?未知,不敢冒险去耽误时间,也是一个 0 或者 1 的问题。




  3. 自己研究 KQL 语法,自己摸索规则,自己实现其逻辑?


    由于项目排期紧张,不敢太过冒险,就选择了自研。起码 ld 能看到进度。😁




image.png


image.png


image.png


我最后选择的是方案3:自研。但是如果有时间,我更倾向的想去尝试方案1 和方案2。


针对自研方案,我们就开干吧!撸起袖子加油干!😄


准备


首先,我们需要一些准备工作,我需要了解 KQL 语法是什么?然后使用它,研究其规则,梳理其逻辑。


Kibana 查询语言 (Kibana Query Language、简称 KQL) 是一种使用自由文本搜索或基于字段的搜索过滤 Elasticsearch 数据的简单语法。 KQL 仅用于过滤数据,并没有对数据进行排序或聚合的作用。


KQL 能够在您键入时建议字段名称、值和运算符。 建议的性能由 Kibana 设置控制。


KQL 能够查询嵌套字段和脚本字段。 KQL 不支持正则表达式或使用模糊术语进行搜索。


更为详细的可以看官方文档 Kibana Query Language



  • key method value 标准单个语句

  • key method value OR/AND key method value OR/AND key method value .... 标准多个语句

  • key OR/AND key OR/AND key OR/AND key method value .... 不标准多个语句

  • ......



Tips:key(字段名称)、method(运算符)、value(值)



实现


textarea



  • 由于用户可以输入多行文本信息,所以需要 textarea。type="textarea"

  • 为了用户能清楚看到输入内容,以及input 的美观,初始行数 :rows="2"

  • 因为我们能支持关键字和KQL两种情况,所以 placeholder="KQL/关键字"

  • 获取焦点需要打开下拉展示框 @focus="dropStatus = true"

  • 失去焦点且没有操作下拉选项则关闭下拉框 @blur="changeDrop"

  • 由于下拉框的位置需要跟着 textarea 高度变化,所以 v-resizeHeight="inputResizeHeight"


<el-input
v-resizeHeight="inputResizeHeight"
id="searchInputID"
ref="searchInputRef"
v-model="input"
:rows="2"
type="textarea"
placeholder="KQL/关键字"
class="searchInput"
@blur="changeDrop"
@focus="dropStatus = true"
>

</el-input>

changeDrop 需要判断用户是否正在操作下拉框内容,如果是,就不要关闭。这块你会怎么实现呢?可以先思考自己的实现方式,再看下边是我个人的实现方式。


其实理论就是给下拉框操作的时候增加标记,在失去焦点要关闭的时候,判断是否有这个标记,如果有,就不要关闭,否则就关闭。但这个标记又不能影响真正的失焦状态下关闭动作。


我想到的就是定时器,定时器能增加一个变量,同时还能自动销毁。具体的实现方式:


// 不关闭下拉框标记
noCloseInput() {
this.$refs.searchInputRef.focus()
if (this.timer) clearInterval(this.timer)
let time = 500
this.timer = setInterval(() => {
time -= 100
if (time === 0) {
clearInterval(this.timer)
this.timer = null
}
}, 100)
}

// 失焦操作
changeDrop() {
setTimeout(() => {
if (!this.timer) this.dropStatus = false
}, 200)
}

这么做需要有以下几点注意:



  • 失焦操作因为需要切换到下拉框有一定延迟需要定时器,而定时器的时间必须小于标记里边的定时器时间

  • 定时器 this.timer = setInterval() 中 this.timer 是定时器的 id

  • clearInterval(this.timer) 只会清除定时器,不会清空 this.timer


v-resizeHeight="inputResizeHeight" 这个是我写的一个自定义指令来检测元素的高度变化的,不知道你有什么好的方法吗?有的话请请共享一下,😍


const resizeHeight = {
// 绑定时调用
bind(el, binding) {
let height = ''
function isResize() {
// 可根据需求,调整内部代码,利用 binding.value 返回即可
const style = document.defaultView.getComputedStyle(el)
if (height !== style.height) {
// 此处关键代码,通过此处代码将数据进行返回,从而做到自适应
binding.value({ height: style.height })
}
height = style.height
}
// 设置调用函数的延时,间隔过短会消耗过多资源
el.__vueSetInterval__ = setInterval(isResize, 100)
},
unbind(el) {
clearInterval(el.__vueSetInterval__)
}
}

export default resizeHeight

下拉面板


image.png


下拉框是左右布局,右侧是检索语法说明的静态文案,可忽略。左侧是语句提示内容。


语句提示内容经过研究其实有四种:


key(字段名称)、method(运算符)、value(值)、connectionSymbol(连接符)


由于可能会有多个语句,其实我们是只对当前语句进行提示的,所以我们只分析当前语句的情况。


// 当前语句详情
{
cur_fields: '', // 当前 key
cur_methods: '', // 当前 method
cur_values: '', // 当前 value
cur_input: '' // 当前用户输入内容,可模糊匹配检索
}

有四部分,肯定就是需要在符合条件的情况下分别展示对应的 options 面板内容。


那判断条件就是如下图,其中后续需要注意的就是这几个判断条件的值赋值场景要准确。


image.png


语法分析器


想处理输入内容,做一个语法分析器,首先需要去监听用户的输入,那么就用 vue 提供的 watch。


watch: {
input: debounce(function(newValue, oldValue) {
if (newValue !== oldValue) this.dealInputShow(newValue)
}, 500)
}

基本大概思路如下:


KQL语法分析器.png


其中获取输入框的光标位置的方法如下:


const selectionStart = this.$refs.searchInputRef.$el.children[0].selectionStart

修改完了之后,光标会自动跑的最后,这样有点违反用户操作逻辑,所以需要设置一下光标位置:


if (this.endValue) {
this.$nextTick(() => {
const dom = this.$refs.searchInputRef.$el.children[0]
dom.setSelectionRange(this.input.length, this.input.length)
this.input += this.endValue
})
}

还有面板里边有四项内容,那每一项内容选择都可以通过鼠标点击选择,点击选择后,就需要按照规则处理一下,进行最终的字符串 this.input 拼接,得到最终结果。


// 当前 key 点击选择
curFieldClick(str) {},

// 当前 method 点击选择
curMethodClick(str) {},

// 当前 value 点击选择
curValueClick(str) {},

// 当前 链接符 点击选择
curConnectClick(str) {},

这部分需要注意的就是点击面板 input 会失去焦点,就加上前边说到的 noCloseInput() 不关闭下拉面板标记。


键盘快捷操作


必备的目前就 3 个事件 enter、up、down,其他算是锦上添花,由于排期紧张,暂时只做了必备的 3 个 事件:


<el-input
v-resizeHeight="inputResizeHeight"
id="searchInputID"
ref="searchInputRef"
v-model="input"
:rows="2"
type="textarea"
placeholder="KQL/关键字"
class="searchInput"
@blur="changeDrop"
@focus="dropStatus = true"
@keydown.enter.native.capture.prevent="getSearchEnter($event)"
@keydown.up.native.capture.prevent="getSearchUp($event)"
@keydown.down.native.capture.prevent="getSearchDown($event)"
>

</el-input>

那么,我们的这几个键盘事件都需要怎么处理呢??接下来就直接上代码简单分析一下:


// 键盘 enter 事件,有两种情况
// 一种就是 选择内容,第二种就是 相当于回车事件直接触发接口
getSearchEnter(event) {
event.preventDefault()

// 当前下拉面板的展示的 options
const suggestions = this.get_suggestions()

// 满足可以选的条件
if (this.dropStatus && this.dropIndex !== null && suggestions[this.dropIndex]) {
// 光标之后是否有内容,有就需要截取处理
// ......

// 当前项是否是手动输入的,需要做截取处理
// .......

// 拼接 enter 键选择的选项
this.input += suggestions[this.dropIndex] + ' '

// 光标之后是否有内容,就需要设置光标在当前操作位置,并拼接之前截取掉的光标后的内容
// .......

// 设置当前语法区域的各个当前项 cur_fields、cur_methods、cur_values
// ......

// 恢复键盘 up、down 选择初始值
this.dropIndex = 0
} else {
// 不满足选的条件,就关闭选择面板,并触发检索查询接口
this.dropStatus = false
this.$emit('getSearchData', 2)
}
},

// 键盘 up 事件
getSearchUp(event) {
event.preventDefault()

// 满足上移,就做 dropIndex 减法
if (this.dropStatus && this.dropIndex !== null) {
this.decrementIndex(this.dropIndex)
}
},

// 键盘 down 事件
getSearchDown(event) {
event.preventDefault()

// 满足下移,就做 dropIndex 加法
if (this.dropStatus && this.dropIndex !== null) {
this.incrementIndex(this.dropIndex)
}
},

// 加法,注意边界问题
incrementIndex(currentIndex) {
let nextIndex = currentIndex + 1
const suggestions = this.get_suggestions()
// 到最后边,重置到第一个,形成循环
if (currentIndex === null || nextIndex >= suggestions.length) {
nextIndex = 0
}
this.dropIndex = nextIndex

// 被选择的选项如果不在可视范围之内,需要滚动到可视区
this.$nextTick(() => this.scrollToOption())
},

// 减法,注意边界问题
decrementIndex(currentIndex) {
const previousIndex = currentIndex - 1
const suggestions = this.get_suggestions()
// 到最前边,重置到最后,形成循环
if (previousIndex < 0) {
this.dropIndex = suggestions.length - 1
} else {
this.dropIndex = previousIndex
}

// 被选择的选项如果不在可视范围之内,需要滚动到可视区
this.$nextTick(() => this.scrollToOption())
},

键盘事件的核心逻辑上述基本说清楚了,那么其中需要注意的一个点,那就是被选择的选项如果不在可视范围之内,需要滚动到可视区,这样可提高用户体验。那这块到底怎么做呢?其实实现起来还挺有意思的。


import scrollIntoView from './scroll-into-view'

// 滚动 optiosns 区域,保持在可视区域
scrollToOption() {
if (this.dropStatus === true) {
const target = document.getElementsByClassName('drop-active')[0]
const menu = document.getElementsByClassName('search-drop__left')[0]
scrollIntoView(menu, target)
}
},

scroll-into-view.js 内容如下:


export default function scrollIntoView(container, selected) {
// 如果当前激活 active 元素不存在
if (!selected) {
container.scrollTop = 0
return
}

const offsetParents = []
let pointer = selected.offsetParent
while (pointer && container !== pointer && container.contains(pointer)) {
offsetParents.push(pointer)
pointer = pointer.offsetParent
}

const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0)
const bottom = top + selected.offsetHeight
const viewRectTop = container.scrollTop
const viewRectBottom = viewRectTop + container.clientHeight

if (top < viewRectTop) {
container.scrollTop = top
} else if (bottom > viewRectBottom) {
container.scrollTop = bottom - container.clientHeight
}
}

针对上述内容几个技术点做出简单解释:


offsetParent:就是距离该子元素最近的进行过定位的父元素,如果其父元素中不存在定位则 offsetParent为:body元素。


offsetParent 根据定义分别存在以下几种情况:



  1. 元素自身有 fixed 定位,父元素不存在定位,则 offsetParent 的结果为 null(firefox 中为:body,其他浏览器返回为 null)

  2. 元素自身无 fixed 定位,且父元素也不存在定位,offsetParent 为 <body> 元素

  3. 元素自身无 fixed 定位,且父元素存在定位,offsetParent 为离自身最近且经过定位的父元素

  4. <body>元素的 offsetParent 是 null


offsetTop:元素到 offsetParent 顶部的距离


image.png


offsetHeight:是一个只读属性,它返回该元素的像素高度,高度包含内边距(padding)和边框(border),不包含外边距(margin),是一个整数,单位是像素 px。


通常,元素的 offsetHeight 是一种元素 CSS 高度的衡量标准,包括元素的边框、内边距和元素的水平滚动条(如果存在且渲染的话),不包含 :before或 :after 等伪类元素的高度。


image.png


scrollTop:可以获取或设置一个元素的内容垂直滚动的像素数。


一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0。


clientHeight:是一个只读属性,它返回该元素的像素高度,高度包含内边距(padding),不包含边框(border),外边距(margin)和滚动条,是一个整数,单位是像素 px。


clientHeight 可以通过 CSS height + CSS padding - 水平滚动条高度 (如果存在)来计算。


image.png


最全各个属性相关图如下:


image.png


效果


效果怎么说呢,也算顺利上线生产环境了,在此截图几张,给大家看看效果。


image.png


image.png


image.png


image.png


小结


做这个需求,最难的点是要求自己去研究 KQL 的语法规则,以及使用方式,然后总结规则,写出自己的词法分析器。


其中有什么技术难点吗?似乎并没有,都是各种判断条件,最简单的 if-else。


所以想告诉大家的是,不要一心只钻研技术,在做业务的时候也需要好好梳理业务,做一个懂业务的技术人。业务和技术互相成就!


最后,如果感到本文还可以,请给予支持!来个点赞、评论、收藏三连,万分感谢!😄🙏


作者:Bigger
来源:juejin.cn/post/7210593177820676154
收起阅读 »

ChatGPT API入门探索,制作AI孙子

最近ChatGPT的热度非常高,OPEN AI也是在不久前刚刚宣布开放ChatGPT商用API。 写这篇文章的契机在于前几天一位群友分享了一个日本程序员基于ChatGPT开发了一个AI佛:HOTOKE AI 例如当用户提问“我不知道生存的目的”,AI佛会做出如...
继续阅读 »

最近ChatGPT的热度非常高,OPEN AI也是在不久前刚刚宣布开放ChatGPT商用API。


写这篇文章的契机在于前几天一位群友分享了一个日本程序员基于ChatGPT开发了一个AI佛:HOTOKE AI


例如当用户提问“我不知道生存的目的”,AI佛会做出如下回答:


image.png


感觉还是挺有意思的,他会从佛教及心理学的角度去给你分析与解释。


但作为一名程序员,我们肯定对背后的原理更感兴趣,于是我又问了他和GPT有什么区别:


image.png


这个回答还是有点出乎我的意料的,于是我更感兴趣了,直接网上找起了教程,也研读了一下OPEN API的文档。所以这篇文章更像是一个学习笔记,也希望能吸引到一些同样感兴趣的小伙伴。


文章最后我们会开发一个基于ChatGPT的AI孙子(就是培养出高启强的那个孙子)


ChatGPT


相信大家能点进来的都知道ChatGPT是啥, 这part就过了吧。


OPEN AI API


Examples


OPEN AI API提供了很多种模型供用户选择,在官网上也提供了很多示例,包括各种各样的模型及其生成的结果。
这是官网示例的地址:platform.openai.com/examples


image.png


我们以Q&A为例:


image.png
可以看到它用的模型是text-davinci-003,然后他说"我是一个高度智能的问答机器人。如果你问的问题有确切答案,我就会给出答案,如果你问的问题是在搞我或者没有明确答案,我会回答不知道"。然后下面就是一些示例。


提示工程(Prompt Engineering)


我们还可以点右上角的Open in Playground来自己尝试下


image.png
可以看到右侧侧边栏有许多参数可以调节,根据调节的参数不同,生成的答案也会有所区别。参数就不细说了,感兴趣的同学可以自己去看,当你鼠标移到那个参数上时,会有对应的解释框弹出来解释这个参数的作用。


这里简单演示了如何去通过提示,引导AI之后回答出你所想要的答案,所以提示(Prompt)就变的尤为重要。事实上,现在提示工程师(Prompt Engineer)已经成为一个炙手可热的岗位,他们的主要任务就是引导AI去学习及训练。
官方文档也用了相当大的篇幅去介绍如何去提示:platform.openai.com/docs/guides…


这里就简单翻译一下开头:


概述

我们的模型可以做任何事情,从生成原创故事到执行复杂的文本分析。因为它们可以做这么多事情,所以你必须明确描述你想要什么。告诉他该怎么做,而不只是简单陈述,这就是一个好提示的秘籍。


创作提示有三个基本准则。




  • 展示和讲述。通过指示、例子或两者的结合,明确你想要什么。如果你想让模型按字母顺序排列一个项目清单,或按情绪对一个段落进行分类,就向它展示你想要的东西。




  • 提供高质量的数据。如果你试图建立一个分类器或让模型遵循一个模式,确保有足够的例子。确保校对你的例子 —— 模型足够聪明,它可以识别拼写错误,并且告诉你,但它也可能认为这是你故意写错的,这都会影响他的回答。




  • 检查你的设置。temperature和top_p参数控制了模型在产生响应方面的确定性。如果你要求它产生一个只有一个正确答案的响应,那么你就想把这些设置得低一些。如果你在寻找更多不同的反应,那么你可能想把它们设置得更高。人们使用这些设置的第一大错误是,假定它们是对"聪明"或"创造性"控制。




到这里大家应该也就知道了,那个AI佛就是通过程序员选择合适的模型,以及对他加以训练,比如告诉他当用户提问时,他需要从佛教及心理学的角度去回答用户的问题。


模型(Models)


模型可以说是非常重要的一部分,也是Open AI的核心。


image.png


ChatGPT用的就是GPT-3.5模型,他主要是面向大众,所以被训练的更加安全,不会输出一些出格的内容。


在GPT-3.5中又包含了几个子模型:


image.png


目前官方推荐的是gpt-3.5-turbo,称此模型能生成最好的结果,并且只需要text-davinci-003十分之一的算力。


价格


目前gpt-3.5-turbo的价格是每一千个token需要0.002🔪。所谓token可以理解为他处理的单词数,1000个token大概可以生成750个单词。
image.png


AI孙子


接下来我们尝试简单写一个AI孙子。


首先当然是创建一个项目,我用的就是简单的create-react-app去创建一个react应用:
npx create-react-app ai-sunzi


使用express写一个简单的服务端:


image.png


重写App.js,我们只需要一个textarea和一个提交按钮


image.png


访问http://localhost:3000 确保可以访问服务端,返回“Hello World!”:


image.png


接下来我们需要去Open AI官方文档中查看如何使用它的API,文档中有一个例子,如何在nodejs中引入包:


image.png


我们把他复制到我们代码中,有些地方还是需要改动一下,比如包的引入不能直接使用import。再将API Key复制到configuration中(API Key需要注册并登录OpenAI,可以免费领取):


image.png


然后我们再查看文档中Completion部分:


completion.png


将其复制到我们代码中并测试:


image.png


此处我将max_token修改为100,因为中文字符所占字节数长一些。随后将prompt参数改为“无论用户输入什么,只需回答这是一个测试”
再次访问localhost:3000可以看到确实可以返回“这是一个测试”。


说明我们的后端已经从OPEN AI得到了返回的数据。


再接下来就简单了,我们只需要把前端和后端连上,再修改prompt,让这个AI表现的像孙子:


image.png


将textarea中的message传入至prompt参数中,再从OPEN AI得到返回的答案,就大功告成了。
以下为一些测试:


image.png


image.png


image.png


Reference: http://www.youtube.com/watch?v=bB7…


作者:RyanGG
来源:juejin.cn/post/7209862607976644645
收起阅读 »

60分钟的文心一言发布会:我带你5分钟看完

李彦宏缓缓走入会场。 亮了,他亮了。 文心一言具有五大功能: 文学创作(写诗歌,写小说) 商业文案创作(给公司起名,写宣传语) 数理逻辑推算(做题) 中文理解(理解华夏文化) 多模态生成(文字、图片、音频、视频) 其中,前三个功能ChatGPT都有,我...
继续阅读 »

李彦宏缓缓走入会场。


live_capture_05_35.png


亮了,他亮了。


live_capture_05_59.png


文心一言具有五大功能:



  1. 文学创作(写诗歌,写小说)

  2. 商业文案创作(给公司起名,写宣传语)

  3. 数理逻辑推算(做题)

  4. 中文理解(理解华夏文化)

  5. 多模态生成(文字、图片、音频、视频)


其中,前三个功能ChatGPT都有,我感觉百度搞不过它。


比如:


live_capture_10_47.png


再比如:


live_capture_12_45.png


但是,后两个功能是ChatGPT不具备的。


首先说中文理解。


老李说文心一言的重点是用中文训练。AI模型能深入中国文化到什么程度呢?


拿洛阳纸贵举例子,他连着问了模型几个问题。


live_capture_15_31.png


尤其是第二个问题,洛阳纸贵,那么当时到底多少钱呢?


live_capture_14_03.png


模型可以结合唐朝时期的物价以及相关文献记载,给出来答案:当时洛阳的纸由八百文涨到两三千文。


甚至让它以“洛阳纸贵”写一篇藏头诗,模型也写了出来。虽然比不上我写的,但是起码它藏头。


我们再来看看ChatGPT对于中文的理解是什么效果。


问ChatGPT洛阳纸贵什么意思,然后再问洛阳的纸多少钱。


123.png


很明显,它对第二个问题回答的不是很好。它没有清楚的理解我的问题,反而还指出了我的错误。它没有说具体多少钱,它说这不是贵的问题,只是一种现象。


而对于藏头诗,ChatGPT也是无能为力。


456.png


它可能知道什么是诗,但并不知道什么是藏头诗。因为,这就有点深入中国文化了。


但是,我相信,你让它解释藏头诗,它凭借搜索引擎是能找到并打印出来的。但是,你让它写,很遗憾。


ChatGPT没有的,文心一言有的,第二个功能就是多模态。


多模态是AI的一个专业名词,比如文本是一种模态,图片是一种模态。多模态就是多种形式。


我们知道,ChatGPT是一个文本单模态的语言模型。


我李哥演示了一个从文字到图片到语音再到视频的例子。


live_capture_17_40.png


海报图片设计出来了。


live_capture_15_52.png


然后再用四川方言讲出来。


live_capture_16_28.png


最后生成视频。


live_capture_17_13.png


其实,后面的几种模态并非是首发,只是一种整合。


语音合成是老技术了,这不用说。


图文转视频的功能,去年开始,在各大自媒体平台也纷纷上线了。包括头条号在内,写完一篇文章,可以自动生成视频。


但是,大多都是素材库的标签拼凑。生成的效果较差,有时候百度自己生成的,自己都无法通过审核。


但是,让ChatGPT干这些,它除了文本模态之外,它都会回复你它只是一个语言模型。


789.png


讲完了五大功能。后面说了三大产业机会。


live_capture_26_30.png


中间休息,亮出了很多AIGC的案例。


live_capture_00_13.png


live_capture_00_22.png


live_capture_00_32.png


live_capture_00_42.png


休息完了,百度首席技术官,王海峰老师对文心一言的技术做了简单解读。


live_capture_01_24.png


其实主要还是说了利用飞桨开源平台实现的。


live_capture_01_57.png


live_capture_04_57.png


然后介绍了文心一言模型的功能组成。


live_capture_10_40.png


live_capture_12_47.png


live_capture_14_10.png


live_capture_19_05.png


live_capture_21_41.png


最后说,飞桨平台好!


live_capture_21_47.png


结束前,官方平台宣布,企业用户可以申请内测。


live_capture_24_06.png


方法在图片里面了,搜索关键词“百度智能云”填写表单就可以。


有个邀请体验说明是这样的:



我们期待与您尽快展开合作,但由于初期名额有限,目前暂不能满足所有人的申请,因此请您仔细填写需求,我们会结合业务场景、访问量等级等信息综合评估,并在您通过评估后尽快给您反馈,本次邀测仅面向企业用户,谢谢您的理解。



最后,我感觉李哥和王老师在讲的时候,声音有些发颤。可能理工男面对大型发布会都有怯场的情况,也可能他们对平台不自信。


整个发布会,都是在播放视频,并没有现场实际操作平台。这也是为了达到最好的发布效果。李哥说,实际操作的话会比较慢,大家需要等。


我感觉,抛去技术不谈,文心一言在中国肯定是有市场的。


因为,我在直播间发布了很多信息,都没有显示。但是,当我和大家万众一心时,我的评论赫然出现在字幕上。


2023-03-16_140740.png


这一点,ChatGPT无论如何是无法做到的!


我们不黑不吹,后续的情况,只能等待用户的反馈了。


作者:TF男孩
来源:juejin.cn/post/7211055301204705338
收起阅读 »

ChatGPT前端领域初探

什么是ChatGPT 官方解释:ChatGPT是一个智能聊天机器人,来自于OpenAI,它能够使用人工智能技术进行对话,并回答用户提出的问题和请求。它由GPT(Generative Pre-trained Transformer)算法支持,可以模拟人类对话和回...
继续阅读 »

什么是ChatGPT


官方解释:ChatGPT是一个智能聊天机器人,来自于OpenAI,它能够使用人工智能技术进行对话,并回答用户提出的问题和请求。它由GPT(Generative Pre-trained Transformer)算法支持,可以模拟人类对话和回答各种问题,包括日常生活、科技、娱乐、健康、财经等领域。ChatGPT可以通过各种渠道进行访问,如网站、社交媒体或移动应用程序。


ChatGPT优势


ChatGPT有以下几个优势:



  1. 可以模拟真人对话:ChatGPT使用GPT算法,可以生成自然语言,使得对话非常流畅和自然,就好像在与一个真人交流一样。

  2. 能够自我学习:ChatGPT使用机器学习技术,可以通过不断的学习来提高自己的答案和回复质量。

  3. 24小时在线:ChatGPT可以在任何时间回答用户的问题,不需要等待人类客服的接待时间。

  4. 处理大量请求:ChatGPT可以处理大量请求,在同一时间内可以同时与多个用户进行对话。

  5. 提高客户满意度:ChatGPT可以回答用户的问题并提供有用的信息,这可以提高用户的满意度和忠诚度。

  6. 提高效率:ChatGPT可以快速响应和解答用户问题,减少人工客服的工作量和时间。


接下来,我们来聊一聊它对前端开发产生了什么样的影响


体验流程


我们需要先拥有一个相应的账户才能体验,这里我直接放上体验流程的链接:sms-activate.org,按照本流程对于没有接触过ChatGPT的童鞋们可以体验一下,这里我就不展开详细解释了。tips:电脑需要科学上网哦~~


辅助开发


重点来了,我们需要先分析我们在日常开发中哪些方面可以用到它,根据开发的流程我们可以从以下几点分析:




  1. 需求阶段:我们做业务开发的前提是以需求为准,对于需求而言,实现的方式多种多样,我们应该分析一下,应该用什么技术去实现,具体对应到哪种框架、第三方依赖库等等。




  2. 编码阶段:这个阶段是业务逻辑的实现阶段,要完成需求中的功能。举个🌰:做登录注册模块,输入账号和密码时一般都需要校验格式(复杂情况),这时正则表达式不失为一个好的办法,此时我们的主角就该登场了~~ 话不多说,直接上图:


    image.png


    经验证:^[a-zA-Z0-9]{6,}$ 符合预期
      let reg = /^[a-zA-Z0-9]{6,}$/
    let str = '0203'
    let str2 = 'yk0203'
    console.log(reg.test(str), 'str') // false
    console.log(reg.test(str2), 'str2') // true



这个正则限制比较简单,来个复杂点的吧:


image.png


image.png tips:解释满分。


这里我就不做test了,更复杂的大家可以自己尝试~~



  1. 代码优化:我们在在编码阶段完成功能后,应该考虑代码优化之道,优化代码包括多个层面,提高代码的复用性就是其中一个方面,直接上demo说明一下:


image.png
在这个demo中,我们定义了两个函数,一个用于计算两个数的和,另一个用于计算两个数的差。通过调用这两个函数,我们可以实现复用性,避免在不同的地方重复编写相同的计算代码。此外,我们还可以通过将这些函数放入一个模块中,从而实现更高程度的复用性。将这两个函数放入一个名为"calculator.py"的模块中,然后在其他文件中引入这个模块,就可以在其他地方轻松地重用这些函数。然后我又向它发起提问:


image.png


image.png
我们使用JavaScript语言实现了与前面Python demo相同的功能。将这两个函数放入一个名为"Calculator.js"的模块中,然后在其他文件中引入这个模块,就可以在其他地方轻松地重用这些函数。其实这种思想在前端已经老生常谈了。



  1. 代码重构:一般来说,一个项目经过多个人先后开发并维护,每个人的开发习惯也不尽相同,最终导致代码中很多逻辑不是很清晰,尤其是那些很久之前的项目,再加上当时没有添加注释,维护效率不高,这个时候我们可以考虑下重构下部分代码,用图来说话:


image.png
这里只是一个简单的demo,项目中重构代码要比这个复杂(慎重考虑是否进行代码重构),说到这里,养成良好的编码习惯就显得特别重要啦,比如定义方法时语义化,适当给方法添加注释等,我们的主角也是也可以帮忙添加注释的,请看:


image.png


function factorial(n) { 
if (n === 0) return 1;
else return n * factorial(n-1);
}
//示例输出
console.log(factorial(5)); //120
console.log(factorial(10)); //3628800

image.png


// 定义递归函数,计算阶乘 
function factorial(n) {
if (n === 0) {
// 当 n 等于 0 时,阶乘结果为 1
return 1;
} else {
// 当 n 不等于 0 时,调用函数自身来计算阶乘
return n * factorial(n-1);
}
}
// 示例输出
console.log(factorial(5)); //120
console.log(factorial(10)); //3628800

然后我又追问:


image.png
综上,我们想要主角帮我们处理问题,就应该把条件罗列清晰,否则主角也给不了最完善的回答,它有时在处理基本数学问题时出错,有时无法全面理解逻辑,并且甚至会提供完全错误的事实来支持其答案。OpenAI 已经承认了这个问题,并表示“ChatGPT 有时会写出听起来合理但是不正确或无意义的答案”。ChatGPT 是一种辅助工具,能够理解复杂的问题。但最好也不要完全相信它所说的一切。


tips:提问和回答的截图均来自VScode的ChatGPT中文版扩展插件(站在了大佬的肩膀上),有兴趣的童鞋可以自行搜索体验一波~


ChatGPT的现状与未来


目前,OpenAI官方已经官宣了GPT-4,只不过还没完全开放,并且门槛较高,发展前景还是相当nice的。


总结


总体看来,未来可期~~,对于希望在工作中提升效率的开发人员来说是一柄利剑。然而,它的回答也不是100%准确的,因此在将其用于更高级的任务之前,需进行深究。到此,ChatGPT的初探到此结束,感谢各位看官。有问题欢迎评论区留言。


作者:青灬河
来源:juejin.cn/post/7210653822849548346
收起阅读 »

前端已死?铜三铁四?你收到offer了吗?

背景 今年是应届生就业最难的一年。说实话,每一届毕业生都这么说,而且每一届毕业生说的都没有问题。疫情刚开始那一年,王兴说过一句话 2019 年可能会是过去十年最差的一年,却是未来十年里最好的一年。因为难,所以很多人将目光放向了金三银四,无论是应届的,还是找新机...
继续阅读 »

背景


今年是应届生就业最难的一年。说实话,每一届毕业生都这么说,而且每一届毕业生说的都没有问题。疫情刚开始那一年,王兴说过一句话 2019 年可能会是过去十年最差的一年,却是未来十年里最好的一年。因为难,所以很多人将目光放向了金三银四,无论是应届的,还是找新机会的。


金三银四就是往年最好的求职时期。三四月份刚好又是春招的时期,过年后,每个企业又刚好制定好下一年的计划。刚发完年终奖,职场的小同学们又蠢蠢欲动,空出来不少位置。


现况


但是对于今年来说,金三银四就是纯骗局。即使你通过了很多简历初选,发现最后拿到手没有什么好offer,你发现好像同样的岗位,去年学长学姐,硬件指标不需要这么严格。辞职前聊的好好的offer,裸辞之后就被缩紧了。


我之前看过一段话,我一直觉得欧洲特别拉胯,今天搞游行,明天搞罢工,昨天嫌工作时间多了,今天嫌假期少了。直到有一天,我改变了看法,我毕业了。我们以为疫情过去,所有的一切都会好转,因为这是艰难的最主要来源。但是好像不是这样。因为疫情指很多深层问题的表面借口罢了。


原因


疫情只是一方面。国内资源开发过度,房地产集体不拿地,导致房企上下游的企业、广告公司、活动公司、印刷公司、工程公司对外招聘收紧。国际局势紧张,投资愈加谨慎,金融行业都卷得飞起,依赖投资的入不敷出的互联网公司,更无法支出如此大的人力成本。由此牵扯的行业计算机产品、自媒体运营、招聘收紧。教育本来就人走茶凉,牵扯出来的行业也多的不行。甚至俄乌战争能通过大量蝴蝶效应影响着我们。


那我们能怎么办呢?


混乱的背景,只会使富人更富,穷人更穷。没办法了吗?那我们能怎么办呢?


我们无法改变局势,我们只能在夹缝中求生存。但我们起码要成为在这个环境下人群中的最优解。求职也好,职业规划也好,面对现在困境,主要有两个生存的角度避坑主动措施


避坑


想要避免被坑,就得了解面试官和公司。他们到底怎么想的?


这两年最主要的坑分别是赛马机制表面繁荣白嫖逻辑


赛马机制


面试公司让你以为你很优秀,其实你已经掉入公司的陷阱了。赛马机制就是强势烘托起所有人的竞争欲,最后选出最优秀的一匹马。赛马本无问题,但是大面积的赛马是对求职者的极其不负责任。以前的公司经常会搞 PUA 这一套来实现控制员工的目的。现在公司用的方法更加高级 NLP 教练技巧。 NLP 教练技巧可以理解为正向PUA。


你以为面试官在夸你?你以为公司很器重你?


但你已经掉进公司糖衣炮弹的陷阱了。 NLP 本来是良性的引导,但在大面积的竞争驱使下,赛马机制应运而生。 NLP 带来的自我催眠会让你适应恶劣的竞争,像养蛊一样愿意吃掉所有的竞争者。


如何判断一个公司是否在赛马?


最简单的逻辑是



  1. 第一、看是否是需要通过试用期多人的竞争来确定留用。不用可惜这样子的机会,因为转正后一定会迎来更为内卷的赛马机制。

  2. 第二、 不是面试前,而是所有面试后还有一堆面试官加你微信的,大概率是想拉着你再赛马几次,或者我只能请你保护好自己的私生活。

  3. 第三、 画饼的公司必然在赛马。


就业难在哪里?因为人力资源充沛这件事既是红利也是诅咒。人力资源看起来充沛,就会带来第二个坑,


表面繁荣,


实际上企业现阶段根本没有什么需求,裁员都来不及呢。这和你们现在面对的很多情况是类似的。一个岗位有超多人竞聘,很多人认为这个岗位热门就是行业趋势了。其实并不是,其实只是只是企业可能需要到面率等一系列人力资源指标,为了让这个看起来很繁荣罢了。这也是为什么你们很多人通过简历关,但是最后都拿不到 offer 的原因


如何避免?


几乎无法避免,毕竟海头是你我们常干的事情。但是可以做好心理预设,就是招聘人数需求很大的岗位,不要对这种岗位抱有太高的希望,避免影响自己招聘热情和状态。


白嫖逻辑


当你跟面试官运筹帷幄,展示自己的专业性时,你已经失去了工作的机会。公司招不起人才,但想白嫖。人才的创意和专业知识,


怎么嫖?让人才参加面试?


我认识的一个大厂的面试官跟我聊天,说今年他们的招生名额少了很多,但是面试数量增加很多。我很好奇的问他,逻辑有点矛盾,他贱兮兮的跟我说,他们内部叫做给奖励,主要给那些很厉害的人奖励,但是并不会招他们。比如一些 35 岁以上很专业的人,比如有一些硬伤但真的有东西的人。这些人在求职市场处处碰壁,给的奖励就是面试的机会。他们刚好也可以从这些面试者那里了解到一些打破信息壁垒的专业知识或信息,顺便给一些刚入职的人或者项目经理培训,但是他们肯定是不会招的。说实话,我听到这里已经开始气了。应届生常见的类似的坑,就是拿一些内部案例作为面试题进行面试。本质就是白嫖,却堂而皇之的说是奖励,谁稀罕你的奖励。所以,如果你碰到公司,一拿非常具体的案例来去进行考核,让你演绎操作细节的。并且不断询问你对行业的认识,跟你讨论战略并显得他毫无理解的。或者面试官相较于你过于年轻的这一类公司,扭头就走就好了


相反的,如一个公司不断的给你输出他们的信息,并且真实的拿这些信息和你比对,这类公司大概率是比较靠谱的。


其实听你看完上面的,你基本能面对在金三银四的大粪坑了,还有一些很明显的坑,大部分你在网上也能搜索到,我就不扩展讲了。但是下面你要仔细看了,因为这是你在避坑之后能够有可能找到工作的重要手段。


主动措施


我们如果把找工作分成找工作前、找工作中和找工作后,我们分别要做的东西就是信息获取个人展示职业规划。我不断在文章里面强调信息的重要性。各个公司官网的 drop description 一定需要好好看看。如果可以用爬虫把很多公司的求职信息 down 下来,可以在他们的官网,也可以在智联招聘一类的网站,不用担心太难。你在 b 站或者其他平台搜索一下。爬虫找工作。现在已经有很多打包的工具或者软件,


金三银四


还有金三银四,不是三四月份开始,春节后就已经开启了。非要再说一些信息,新能源医药现在形势的确不错,按照之前教给你们的思维,他们的上下游求职都会比较顺利。应面试之前去企查查天眼查之类的,好好查一下公司的相关情况,什么情况,成立时间,公司规模,组织架构,法律风险是否存在信誉问题,股东信息是否正常,是否有财务纠纷。查完了,门清了你再冲,不然给你拐缅北去。再看一圈。脉脉拉钩,牛客网、看准网之类的,里面有大量吐槽到起飞的评价,可以让种草的你立马拔草。还有一些关于签署协议、劳动合同、三方协议等等一系列你需要了解,保护自己权益的相关信息,


个人展示


重要的大家理解的还是简历。简历其实针对不同的行业,需要针对定向的简历。因为你要展示的东西暴露问题是不一样的。如果你没有简历模板,或者简历做得很拉,可以使用木及简历,他是一款免费的简历模板网站。还可以使用Markdown编辑特别方便,不要用网上那些花里胡哨的剪辑模板,谁用谁死。往年我们在各大网站搜索找工作,都是各种干货方法。今年你在知乎搜一下 2023 找工作试试。只有一个统一的话题 2023 找工作好难。


你要准备无非是从找工作的环节入手。简历、笔试、面试、二面、终面。简历跟大家说了,笔试自己准备,唯一能准备的就是面试了。牛客网有不少念经,可以去看看,剩下的就是最后一个。


心态


最重要的心态随缘。我不是让大家摆烂。很多公司会组织无领导小组面试,很多公司会有压力面。如果让我给你讲分别,有什么技巧,我是能讲很多的,毕竟我也参加过招聘招聘。但是没必要随缘这个词很重要,因为它恰恰是器中了面试的核心表现状态。当你随缘的时候,无领导小组面试就没必要非要去争取leader,压力面也不会有什么压力,我不会。要知道 leader 通过率并不会比其他角色高。争取表现机会的行为其实并不会帮到什么。很多人总跟你讲放平心态,什么叫做放平心态?随缘就好。工作暂时找不到,并不会饿死并不是你一个人现在这么艰难。削弱焦虑还没到最后一刻


最后职业规划在这里。我只想跟应届生和已经工作的同学说两条。



  • 一、应届生转行并没有什么壁垒,去现在好一些的行业。

  • 二、别跳槽,市场太不景气了。不要觉得疫情过去,一切都好了。求职很重要,只是人生的一小步,并不会因为你应届的求职受挫而影响那么深远。从大厂离开的决定,那一刻,如果你也不想找工作,就就当你给自己放个长假。最重要的是要
    作者:zayyo
    来源:juejin.cn/post/7207314488243585081
    开心。

收起阅读 »

Android记一次JNI内存泄漏

记一次JNI内存泄漏 前景 在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。 分析 查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是a...
继续阅读 »

记一次JNI内存泄漏


前景


在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。


分析


查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是android11系统及以上的保存目录和android11以下不同,android11保存的目录在:


   /data/media/10/Download/leakcanary-包名/2023-03-14_17-19-45_115.hprof 

使用Memory Analyzer Tool(简称MAT) 工具进行分析,需要讲上面的hrof文件转换成mat需要的格式:


   hprof-conv -z 转换的文件 转换后的文件

hprof-conv -z 2023-03-14_17-19-45_115.hprof mat115.hprof

打开MAT,导入mat115文件,等待一段时间。


在预览界面打开Histogram,搜索需要检测的类,如:VideoActivity


screenshot-20230314-204413.png


搜索结果查看默认第一栏,如果没有泄漏,关闭VideoActivity之后,Objects数量一般是零,如果不为零,则可能存在泄漏。


右键Merge Shortest Paths to GC Roots/exclude all phantom/weak/soft etc,references/ 筛选出强引用的对象。


image.png


筛选出结果后,出现com.voyah.cockpit.video.ui.VideoActivity$1 @0x3232332 JIN Global 信息,且无法继续跟踪下去。


screenshot-20230314-205257.png


筛选出结果之后显示有六个VideoActivity对象没有释放,点击该对象也无法看到GC对象路径。(正常的java层内存泄漏能够看到泄漏的对象具体是哪一个)


正常的内存泄漏能够看到具体对象,如图:


image.png
这个MegaDataStorageConfig就是存在内存泄漏。


而我们现在的泄漏确实只知道VideoActivity$1 对象泄漏了,没有具体的对象,这样就没有办法跟踪下去了。


解决办法:


虽然无法继续跟踪,但泄漏的位置说明就是这个VideoActivity1,我们可以解压apk,在包内的class.dex中找到VideoActivity1 ,我们可以解压apk,在包内的class.dex中找到VideoActivity1这个Class类(class.dex可能有很多,一个个找),打开这个class,查看字节码(可以android studio中快捷打开build中的apk),根据【 .line 406 】等信息定位代码的位置,找到泄漏点。


screenshot-20230314-205442.png


screenshot-20230314-205600.png
screenshot-20230314-205523.png


根据方法名、代码行数、类名,直接定位到了存在泄漏的代码:


screenshot-20230314-205730.png


红框区内就是内存泄漏的代码,这个回调是一个三方sdk工具,我使用时进行了注册,在onDestory中反注册,但还是存在内存泄漏。(该对象未使用是我代码修改之后的)


修改方法


将这个回调移动到Application中去,然后进行事件或者回调的方式通知VideoActivity,在VideoActivity的onDestory中进行销毁回调。


修改完之后,多次进入VideoAcitivity然后在退出,导出hprof文件到mat中筛选查看,如图:


image.png


VideoActiviyty的对象已经变成了零,说明开始存在的内存泄漏已经修改好了,使用android proflier工具也能看到在退出videoactivity界面之后主动进行几次gc回收,内存使用量会回归到进入该界面之前。


总结:



  1. LeakCanary工具为辅助,MAT工具进行具体分析。因为LeakCanary工具的监听并不准确,如触发leakcanary泄漏警告时代码已经泄漏了很多次。

  2. 如果能够直接查看泄漏的对象,那是最好修改的,如果不能直接定位泄漏的对象,可以通过泄漏的Class对象在apk解压中找到改class,查看字节码定位具体的代码泄漏位置。

  3. 使用第三方的sdk时,最好使用Application Context,统一分发统一管理,减少内存泄漏。


作者:懵逼树上懵逼果
来源:juejin.cn/post/7210574525665771557
收起阅读 »

GPT-4 就要来了,你准备好了吗

GPT-4 即将面世,它到底是擎天柱还是威震天? 呆鸟说:“ChatGPT 是具备人机对话能力的人工智能,一经问世,就给世人带来了前所未有的新奇体验。短短几个月时间,它就占据了人们的视野。有人好奇,有人担忧,有人看到了致富的机会,也有人担心 AI 会夺走我们...
继续阅读 »

GPT-4 即将面世,它到底是擎天柱还是威震天?



呆鸟说:“ChatGPT 是具备人机对话能力的人工智能,一经问世,就给世人带来了前所未有的新奇体验。短短几个月时间,它就占据了人们的视野。有人好奇,有人担忧,有人看到了致富的机会,也有人担心 AI 会夺走我们的饭碗。ChatGPT 的升级版 GPT-4 也即将问世,面对更强大的 AI,我们应当何去何从呢”



GPT-3 是 ChatGPT 的核心,它是一个超大规模的大语言模型(large language models,LLMs)。GPT-3 的英文全称是生成式预训练转换器-3(generative pre-trained transformer 3),是基于 Transformer 架构开发的大型语言模型,拥有超过 1750 亿个参数。公众可以通过 OpenAI API 访问。ChatGPT 的 API 用户界面简单易用,只需“输入文本”,即可“输出文本”,无需用户具备任何专业技术知识。


据传,OpenAI 正在开发 GPT-4,该模型的参数可能高达 100 万亿,但 OpenAI 的首席执行官 Sam Altman 反驳了这一说法。


那么,GPT-4 与 GPT-3 到底有何不同,它是否会颠覆我们现有的认知?本文将一一对此进行说明。


预计 GPT-4 将在 2023 年发布


汽车人首领 ~ 擎天柱
汽车人首领 ~ 擎天柱


虽然尚未正式官宣,但 IT 界对 GPT-4 的传言数不胜数,预计 OpenAI 很有可能会在今年发布 GPT-4。尽管有传言称微软必应的聊天功能中使用了新版 GPT,但目前测试用户必须排队等待才能使用由 ChatGPT 支持的新必应,而且也无法确定新必应的聊天功能是否使用了新版本的 GPT。


在 ChatGPT 发布之前,即 2022 年底,OpenAI 只允许指定的合作伙伴、付费客户和学术机构使用 GPT-3。预计在GPT-4 发布之前,OpenAI 也会采取类似的方式限制 GPT-4 的使用人员。


GPT-4 可能不会比 GPT-3 训练更多的数据


有传言称,GPT-4 的参数规模将比 GPT-3 大 100 倍,即 17 万亿个参数。但 Altman 曾表示,GPT-4 的参数规模可能并不会比 GPT-3 大很多,因为改进的重点应该是利用现有数据的能力,而不是添加更多的数据。


与 GPT-3 竞争的 Megatron 3(注:Megatron 是变形金刚里霸天虎的首领,中文名为威震天)也是一种大语言模型,它训练的数据比 GPT-3 多,但在测试效果并没有超越 GPT-3,这表明在 AI 领域,规模更大并不一定意味着效果更好。改进算法将降低 GPT-4 和 ChatGPT 的运行成本,这将是 ChatGPT 想要取代谷歌成为最流行搜索引擎的重要因素。


霸天虎首领 ~ 威震天
霸天虎首领 ~ 威震天


GPT-4 将生成更好的编程语言代码


ChatGPT 令人印象最深刻的一点是它不仅可以生成人类语言,还可以生成 Javascript、Python、C++ 等编程语言的代码,可以说是软件开发、Web 开发和数据分析统统都能搞定。


有消息称,目前,OpenAI 正在积极招聘擅长使用人类语言描述代码功能的程序员,预计 GPT-4 将推动 AI 突破生成编程代码的新境界。这种趋势将进一步推动开发工具的革命,例如,微软旗下的 Github 推出的 Copilot 使用的就是经过微调的 GPT-3,提供了把人类自然语言转换为代码的能力。


GPT-4 将使 AI 生成编程代码的能力迈向新的境界。除了生成人类语言,ChatGPT 还能生成 JavaScript、Python、C++ 等编程语言的代码,在软件开发、Web开发和数据分析等领域都将有广泛的应用。


据称,OpenAI 正在招聘能以人类语言描述代码功能的程序员,这将进一步推动开发工具的革命。例如,微软旗下的 Github 推出的 Copilot 使用的就是经过微调的 GPT-3,能够把人类自然语言转换为代码。


总之一句话,就是让程序员开发干掉程序员的程序


GPT-4 不会添加图形功能


有人曾预测 GPT-4 将整合 GPT-3 的文本生成和 Dall-E 2(OpenAI 的另一款 AI 产品)的图像创作功能,如果 能提供数据可视化功能,ChatGPT 将更加完美。但 Altman 否认了这一点,并表示 GPT-4 仍只提供文本生成的功能。


Dall-E 生成的图像
Dall-E 生成的图像


有人会对 GPT-4 失望


虽然 GPT-3 的闪亮登场让整个世界都兴奋不已,但 GPT-4 的表现可能并不会让人再次惊艳。计算机第一次写诗的时候,你可能会觉得震撼,但几年后,即使能让 AI 把诗写得更优美,也不会再给人带来同样的震撼。


Altman 在今年一月的一次采访中曾说过,“有关 GPT-4 的传言很荒谬,都是空穴来风,只怕是希望越大,失望越大。”


那么,我亲爱的读者们,你们对此是怎么看的呢?


有兴趣写书的联系我


Nuxt3 小册推荐


作者:呆鸟
来源:juejin.cn/post/7205842390842114085
收起阅读 »

做一个文件拖动到文件夹的效果

web
在我的电脑中,回想一下我们想要把一个文件拖动到另一个文件夹是什么样子的呢 1:鼠标抓起文件 2:拖动文件到文件夹上方 3:文件夹高亮,表示到达指定位置 4:松开鼠标将文件夹放入文件 下面就来一步步实现它吧👇 一:让我们的元素可拖动 方式一: dragg...
继续阅读 »

在我的电脑中,回想一下我们想要把一个文件拖动到另一个文件夹是什么样子的呢



1:鼠标抓起文件

2:拖动文件到文件夹上方

3:文件夹高亮,表示到达指定位置

4:松开鼠标将文件夹放入文件



Kapture 2023-03-10 at 08.30.34.gif


下面就来一步步实现它吧👇


一:让我们的元素可拖动


方式一: draggable="true"


`<div draggable="true" class="dragdiv">拖动我</div>`

方式二:-webkit-user-drag: element;


  .dragdiv {

width: 100px;

height: 100px;

background-color: bisque;

-webkit-user-drag: element;

}


效果


Kapture 2023-03-10 at 08.55.25.gif


二:让文件夹有高亮效果


给文件夹添加伪类?


🙅如果你直接给文件夹设置伪类:hover,会发现当拖动元素时,文件夹的:hover是不会触发的


Kapture 2023-03-10 at 09.08.54.gif


🧘这是因为在拖拽元素时,拖拽操作和悬停操作是不同的事件类型,浏览器在处理拖拽操作时,会优先处理拖拽事件,而不会触发悬停事件。拖拽操作是通过鼠标点击和拖拽来触发的,而悬停事件是在鼠标指针停留在一个元素上时触发的。


所以我们就来对拖拽操作的事件类型做功课吧🫱



  • dragstart:拖拽开始

  • dragend:拖拽结束

  • dragover:拖拽的元素在可放置区域内移动时触发,即鼠标指针在可放置区域内移动时持续触发

  • dragenter:拖拽的元素首次进入可放置区域时触发

  • dragleave:拖拽的元素离开可放置区域时触发

  • drop:当在可放置区域时,松开鼠标放置元素时触发


什么是可放置元素?
当你给元素设置事件:dragover、dragenter、dragleave、drop的时候
它就变成了可放置元素,特点是移到上面有绿色的➕号

拖动高亮实现


1:我们给files文件夹添加两个响应事件:dragoverdragleave


ps: 这里用dragover事件而不用dragenter事件是为了后续能够成功触发drop事件

2:当拖动元素进入可放置区域时,动态的给files添加类,离开时则移除类


// 显示高亮类
.fileshover {
background-color: rgba(0, 255, 255, 0.979);
}
// 添加dragover事件处理程序,在可放置区域触发

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover');

});

// 添加dragleave事件处理程序,离开可放置区域触发

files.addEventListener('dragleave', (event) => {

event.target.classList.remove('fileshover');

});

🥳 恭喜你成功实现了移动到元素高亮的效果了


Kapture 2023-03-14 at 11.54.14.gif


三:文件信息传递


文件拖过去,是为了切换文件夹,在这里你可能会进行一些异步的操作,比如请求后端更换文件在数据库中的路径等。我们的需求多种多样,但是归根到底都是获取到文件的数据,并传递到文件夹中


DataTransfer对象


DragEvent.dataTransfer: 在拖放交互期间传输的数据


我们主要使用它的两个方法:



  • DataTransfer.setData(format, data):就是设置键值对,把我们要传的数据添加到drag object

  • DataTransfer.getData(format):根据键获取保存的数据


知道了这两个方法,相信你一定就有实现思路了 👊


拖拽开始 --> setData添加数据 --> 进入可放置区域 --> 放置时getData获取数据 --> 完成


1:给文件设置dragstart事件


// 开始拖拽事件

draggable.addEventListener('dragstart', (event) => {

const data = event.target.innerText;

event.dataTransfer.setData('name', data); //添加数据

})

2:在dragover事件中用event.preventDefault()阻止默认行为,允许拖拽元素放置到该元素上,否则无法触发drop事件


// 添加dragover事件处理程序

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover');

event.preventDefault(); //新增

});

3:给文件夹设置放置事件drop


// 添加drop事件处理程序

files.addEventListener('drop', (event) => {

const data = event.dataTransfer.getData('name'); // 获取文件的数据

const text = document.createTextNode(data);

files.appendChild(text);

event.target.classList.remove('fileshover'); // 记得放置后也要移除类

});

实现效果:


Kapture 2023-03-14 at 14.46.45.gif


四:完整代码:


<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

<style>

.dragdiv {

width: 100px;

height: 100px;

background-color: bisque;

-webkit-user-drag: element;

}

.files {

width: 200px;

height: 200px;

background-color: rgba(0, 255, 255, 0.376);

margin-top: 100px;

}

.fileshover {

background-color: rgba(0, 255, 255, 0.979);

}

</style>

</head>

<body>

<div draggable="true" class="dragdiv">我是文件1</div>

<div class="files">

<p>文件夹</p>

拖动文件名称:

</div>

<script>

const draggable = document.querySelector('.dragdiv');

const files = document.querySelector('.files');

// 开始拖拽事件

draggable.addEventListener('dragstart', (event) => {

const data = event.target.innerText;

event.dataTransfer.setData('name', data);

})

// 添加dragover事件处理程序

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover')

event.preventDefault()

});

// 添加dragleave事件处理程序

files.addEventListener('dragleave', (event) => {

event.target.classList.remove('fileshover')

});

// 添加drop事件处理程序

files.addEventListener('drop', (event) => {

const data = event.dataTransfer.getData('name')

const text = document.createTextNode(data)

files.appendChild(text);

event.target.classList.remove('fileshover')


});

</script>

</body>

</html>

总结:以上只是简单的熟悉拖拽事件的整个过程,你可以在此拓展更多自己想要功能,欢迎分享👏


作者:隐兮
来源:juejin.cn/post/7210256070299549755
收起阅读 »

Android 指纹识别(给应用添加指纹解锁)

使用指纹 说明 : 指纹解锁在23 的时候,官方就已经给出了api ,但是由于Android市场复杂,无法形成统一,硬件由不同的厂商开发,导致相同版本的软件系统,搭载的硬件千变万化,导致由的机型不支持指纹识别,但是,这也挡不住指纹识别在接下来的时间中进入An...
继续阅读 »

使用指纹



说明 : 指纹解锁在23 的时候,官方就已经给出了api ,但是由于Android市场复杂,无法形成统一,硬件由不同的厂商开发,导致相同版本的软件系统,搭载的硬件千变万化,导致由的机型不支持指纹识别,但是,这也挡不住指纹识别在接下来的时间中进入Android市场的趋势,因为它相比较输入密码或图案,它更加简单,相比较密码或者图案,它更炫酷 ,本文Demo 使用最新的28 支持的androidx 库中的API及最近火热的kotlin语言完成的



需要知道的



  • FingerprintManager : 指纹管理工具类

  • FingerprintManager.AuthenticationCallback :使用验证的时候传入该接口,通过该接口进行验证结果回调

  • FingerprintManager.CryptoObject: FingerprintManager 支持的分装加密对象的类



以上是28以下API 中使用的类 在Android 28版本中google 宣布使用Androidx 库代替Android库,所以在28版本中Android 推荐使用androidx库中的类 所以在本文中我 使用的是推荐是用的FingerprintManagerCompat 二者的使用的方式基本相似



如何使用指纹



  • 开始验证 ,系统默认的每段时间验证指纹次数为5次 次数用完之后自动关闭验证,并且30秒之内不允行在使用验证


验证的方法是authenticate()


/**
*
*@param crypto object associated with the call or null if none required.
* @param flags optional flags; should be 0
* @param cancel an object that can be used to cancel authentication
* @param callback an object to receive authentication events
* @param handler an optional handler for events
**/

@RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
public void authenticate(@Nullable CryptoObject crypto, int flags,
@Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
@Nullable Handler handler)
{
if (Build.VERSION.SDK_INT >= 23) {
final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
if (fp != null) {
android.os.CancellationSignal cancellationSignal = cancel != null
? (android.os.CancellationSignal) cancel.getCancellationSignalObject()
: null;
fp.authenticate(
wrapCryptoObject(crypto),
cancellationSignal,
flags,
wrapCallback(callback),
handler);
}
}
}



arg1: 用于通过指纹验证取出AndroidKeyStore中key的值
arg2: 系统建议为0




arg3: 取消指纹验证 手动关闭验证 可以调用该参数的cancel方法




arg4:返回验证结果




arg5: Handler fingerprint 中的
消息都是通过handler来传递的 如果不需要则传null 会自动默认创建一个主线程的handler来传递消息



使用指纹识别的条件



  • 添加权限(这个权限不需要在6.0中做处理)

  • 判断硬件是否支持

  • 是否已经设置了锁屏 并且已经有一个被录入的指纹

  • 判断是否至少存在一条指纹信息




通过零碎的知识完成一个Demo


这里写图片描述


指纹识别通过之后跳转到 指定页面


进入之后首先弹出对话框,进行指纹验证


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/fingerprint" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:text="验证指纹" />


<TextView
android:id="@+id/fingerprint_error_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:maxLines="1" />


<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:layout_marginRight="5dp"
android:background="#696969" />


<TextView
android:id="@+id/fingerprint_cancel_tv"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center"
android:text="取消"
android:textSize="16sp" />


</LinearLayout>


使用DialogFragment 完成对话框 新建一个DialogFragment 并且初始化相关的api


 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//获取fingerprintManagerCompat对象
fingerprintManagerCompat = FingerprintManagerCompat.from(context!!)
setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog)
}


在界面显示在前台的时候开始扫描


override fun onResume() {
super.onResume()
startListening()
}
@SuppressLint("MissingPermission")
private fun startListening() {
isSelfCancelled = false
mCancellationSignal = CancellationSignal()
fingerprintManagerCompat.authenticate(FingerprintManagerCompat.CryptoObject(mCipher), 0, mCancellationSignal, object : FingerprintManagerCompat.AuthenticationCallback() {
//验证错误
override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) {
if (!isSelfCancelled) {
errorMsg.text = errString
if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
Toast.makeText(mActivity, errString, Toast.LENGTH_SHORT).show()
dismiss()
mActivity.finish()
}
}
}
//成功
override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) {
MainActivity.startActivity(mActivity, true)
}
//错误时提示帮助,比如说指纹错误,我们将显示在界面上 让用户知道情况
override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) {
errorMsg.text = helpString
}
//验证失败
override fun onAuthenticationFailed() {
errorMsg.text = "指纹验证失败,请重试"
}
}, null)
}

在不可见的时候停止验证


if (null != mCancellationSignal) {
mCancellationSignal.cancel()
isSelfCancelled = true
}

在MainActivity 中首先判断是否验证成功 是 跳转到目标页 否则的话需要进行验证
在这个过程中我们需要做的就是判断是否支持,判断是否满足指纹验证的条件(条件在上面)


if (intent.getBooleanExtra("isSuccess", false)) {
WelcomeActivity.startActivity(this)
finish()
} else {
//判断是否支持该功能
if (supportFingerprint()) {
initKey() //生成一个对称加密的key
initCipher() //生成一个Cipher对象
}
}


验证条件


 if (Build.VERSION.SDK_INT < 23) {
Toast.makeText(this, "系统不支持指纹功能", Toast.LENGTH_SHORT).show()
return false
} else {
val keyguardManager = getSystemService(KeyguardManager::class.java)
val managerCompat = FingerprintManagerCompat.from(this)
if (!managerCompat.isHardwareDetected) {
Toast.makeText(this, "系统不支持指纹功能", Toast.LENGTH_SHORT).show()
return false
} else if (!keyguardManager.isKeyguardSecure) {
Toast.makeText(this, "屏幕未设置锁屏 请先设置锁屏并添加一个指纹", Toast.LENGTH_SHORT).show()
return false
} else if (!managerCompat.hasEnrolledFingerprints()) {
Toast.makeText(this, "至少在系统中添加一个指纹", Toast.LENGTH_SHORT).show()
return false
}
}

必须生成一个加密的key 和一个Cipher对象


//生成Cipher
private fun initCipher() {
val key = keyStore.getKey(DEFAULT_KEY_NAME, null) as SecretKey
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
cipher.init(Cipher.ENCRYPT_MODE, key)
showFingerPrintDialog(cipher)
}
//生成一个key
private fun initKey() {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(DEFAULT_KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}

Demo 是kotlin 写的
Demo地址


作者:狼窝山下的青年
来源:juejin.cn/post/7210220134601572410
收起阅读 »

面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯” 到 “觉悟” ,未来又如何落地

对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等,而其中不乏一些玩的很「花」的场景,例如: ChatPDF :使用 ChatPDF...
继续阅读 »

对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等,而其中不乏一些玩的很「花」的场景,例如:




  • ChatPDF :使用 ChatPDF 读取 PDF 之后,你可以和 PDF 文件进行「交谈」,就好像它是一个完全理解内容的「人」一样,通过它可以总结中心思想,解读专业论文,生成内容摘要,翻译外籍,并且还支持中文输出等





  • BiBiGPT : 一键总结视频内容,主要依赖字幕来做总结,绝对是「二创」作者的摸鱼利器。





所以把 ChatGPT 理解为「搜索引擎」其实并不正确,从上述介绍的两个落地实现上看, ChatGPT 不是单纯的统计模型,它的核心并不是完全依赖于它的「语料库」,更多来自于临场学习的能力「 in-context learning」,这就是 ChatGPT 不同于以往传统 NLP「一切都从语料的统计里学习」的原因



当然,我本身并非人工智能领域的开发者,而作为一个普通开发者,我更关心的是 ChatGPT 可以如何提升我的开(mo)发(yu)效率,只是没想到随手一试,我会被 ChatGPT 的 「 in-context learning」 给「逼疯」。



ChatGPT & UI


相信大家平时「面向」 ChatGPT 开发时,也是通过它来输出「算法」或者「 CURD」 等逻辑居多,因为这部分输出看起来相对会比较直观,而用 ChatGPT 来绘制前端 UI 的人应该不多,因为 UI 效果从代码上看并不直观 ,而且 ChatGPT 对与 UI 的理解目前还处于 「人工智障」的阶段。



但是我偏偏不信邪。。。。。



因为近期开发需求里恰好需要绘制一个具有动画效果的 ⭐️ 按键,面对这么「没有挑战性」的工作我决定尝试交给 ChatGPT 来完成,所以我向 ChatGPT 发起了第一个命令:



「用 Flutter 画一个黄色的五角星」




结果不负众望,关键部分如下代码所示,Flutter 很快就提供了完整的 Dart 代码,并且还针对代码提供了代码相关实现的讲解,不过运行之后可以看到,这时候的 ⭐️ 的样式并不满足我们的需求。



此时顶部的角也太「肥」了 。




所以我随着提出了调整,希望五角星的五个角能够一样大,只是没想到我的描述,开始让 ChatGPT 放飞自我



也许是我的描述并不准确?




在我满怀期待的 cv 代码并运行之后,猝不及防的「五角星」差点没让我喷出一口老血,虽然这也有五个角,但是你管这个叫 「五角星」 ???



这难道不是某个红白机游戏里的小飞机??




甚至于在看到后续 ChatGPT 关于代码的相关讲解时,我觉得它已经开始在「一本正经的胡说八道」,像极了今天早上刚给我提需求的产品经理



哪里可以看出五个角相同了???




接着我继续纠正我的需求,表示我要的是 「一个五个角一样大的黄色五角星」 ,我以为这样的描述应过比较贴切,须不知·····



如下代码所示,其实在看到代码输出 for 循环时我就觉得不对了,但是秉承着「一切以实物为准」的理念,在运行后不出意外的发生了意外,确实是五个角一样大,不过是一个等边五边形。



算一个发胖的 ⭐️ 能解(jiao)释(bian)过去不?




再看 ChatGPT 对于代码的描述,我发现我错了,原来它像的是「理解错需求还在嘴硬的我」,只是它在说「这是一个五角星」的时候眼皮都不会眨一下



AI:确实五个角一样大,五个角一样大的五边形为什么就不能是五角星?你这是歧视体型吗?




所以我继续要求:「我要的是五角星,不是五边形」,还好 ChatGPT 的临场学习能力不错,他又一次「重新定义五角星」,不过我此时我也不抱希望,就是单纯想看看它还能给出什么「惊喜」



不出意外,这个「离谱」的多边形让我心头一紧,就在我想着是否放弃的时候,身为人类无法驯服 AI 「既爱又恨」的复杂情绪,让我最终坚持一定要让 ChatGPT 给我画出一个 ⭐️。



不过心灰意冷之下,我选择让 ChatGPT 重新画一个黄色五角星,没想道这次却有了意外的惊喜,从下面的图片可以看到,此时的 ⭐️ 除了角度不对,形状已经完全满足需求。



所以一个问题我多问几遍,也许就能接近我要的答案?




事实上这也是目前 ChatGPT 的现状,因为「临场学力」能力等因素影响,同一个问题它可能会给出不同的答案,而有的答案其实和我们要的根本不沾边



那么,接下来只要让 ChatGPT 把 ⭐️ 旋转一下角度,应该就可以完成需求····了吧?所以我提出「帮我旋转 180度」的要求。



结果不出意外的还是发生了意外,可能 ChatGPT 理解的 180 度和我不大一样,如下图所示,确实旋转了,只是还是歪的,而基于前面的尝试,我觉得有必要再给它一次机会。



终于,在我换了以下描述之后,ChatGPT 似乎「开窍」了,终于让我如愿以偿的得到了我想要的 ⭐️ 代码。



可以看到,其实这段代码并不复杂,你自己写或者去搜索引擎扒一扒大概会比用 ChatGPT 更快,但是 ChatGPT 的魅力就在于:它也许会错,但是总有一次它会「蒙对」,如果现在还不对,那么未来也会有算对的时候,这就是我感受到的 AI 魅力所在



既然五角星都出来,那么接下来我们来完成最后一步的动画效果,我们希望的是点击 ⭐️ 的时候可以有一个爆炸粒子的动画,所以我提出了:



「希望加上一个动画效果,就是点击的时候星星爆炸成颗粒的动画效果」




结果还是「一口老血」,如下图所示,「星星确实爆炸了」,只是这个爆炸动画看起来是那么的「一言难尽」,感兴趣的可以 cv 下面的源代码自己体验下,不出意外接下来又是新的一轮人类和 AI 的「斗智斗勇」。




class YellowStar extends StatefulWidget {
@override
_YellowStarState createState() => _YellowStarState();
}

class _YellowStarState extends State<YellowStar> with TickerProviderStateMixin {
bool _isExploded = false;
late AnimationController _animationController;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

void _handleTapDown(TapDownDetails details) {
setState(() {
_isExploded = true;
_animationController.forward(from: 0.0);
});
}

void _resetStar() {
setState(() {
_isExploded = false;
_animationController.reset();
});
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTap: _resetStar,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: _isExploded
? BorderRadius.circular(50)
: BorderRadius.zero,
),
child: _isExploded
? _buildParticles()
: Container(
width: 100,
height: 100,
child: CustomPaint(
painter: YellowStarPainter(),
),
),
),
);
}

Widget _buildParticles() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: 100,
height: 100,
child: Stack(
children: List.generate(
50,
(index) {
double radius = _animation.value * 50;
double angle = 2 * pi * index / 50;
double x = 50 + cos(angle) * radius;
double y = 50 + sin(angle) * radius;
return Positioned(
left: x,
top: y,
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.yellow,
),
),
);
},
),
),
);
},
);
}
}



class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;

@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;

Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;

for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}

radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}

path.close();
canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

经过一轮「鏖战」如下图所示,面对同一个问题,ChatGPT 总能找到不同的实现方式来回答你,只是这些实现里有的无法运行的,有的是运行没有效果的,有的甚至是给你输出了一个 //TODO 爆炸动画 ,总之就是让你「血压飙升」。





当然,「胜利终将属于人类」,如下图所示,在经历了无数次「血压飙升」的场景之后,最终我还是得到了我想要的:点击 ⭐️,然后出现了💥的动画效果,尽管还是不完美,但是基本满足了我后续开发的需求。



这个代码完完全全由 ChatGPT 输出,我一句没改,完整代码贴在了后面。





class YellowStarPage extends StatefulWidget {
@override
_YellowStarPageState createState() => _YellowStarPageState();
}

class _YellowStarPageState extends State<YellowStarPage>
with SingleTickerProviderStateMixin
{
late AnimationController _animationController;
bool _isExploded = false;

@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

void _handleStarTap() {
if (!_isExploded) {
_isExploded = true;
_animationController.forward(from: 0);
Future.delayed(Duration(milliseconds: 500), () {
setState(() {
_isExploded = false;
});
});
}
setState(() {});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Yellow Star')),
body: Center(
child: GestureDetector(
onTap: _handleStarTap,
child: Container(
width: 300,
height: 300,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: YellowStarPainter(_animationController.value,
isExploded: _isExploded),
);
},
),
),
),
),
);
}
}

class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;

final double animationValue;
final bool isExploded;

YellowStarPainter(this.animationValue, {this.isExploded = false});

@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;

Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;

if (isExploded) {
double particleSize = starSize / 30;
paint.strokeWidth = 1;
paint.style = PaintingStyle.fill;
paint.color = Colors.yellow;
Random random = Random();

for (int i = 0; i < 30; i++) {
double dx = random.nextDouble() * starSize - starSize / 2;
double dy = random.nextDouble() * starSize - starSize / 2;
double x = centerX + dx * (1 + animationValue);
double y = centerY + dy * (1 + animationValue);

canvas.drawCircle(Offset(x, y), particleSize, paint);
}
} else {
for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}

radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}

path.close();
canvas.drawPath(path, paint);
}
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}


最后,给大家欣赏一下我让 ChatGPT 画一只米老鼠的「心路历程」,很明显这一次「人类一败涂地」,从目前的支持上看,让 ChatGPT 输出复杂图像内容并不理想,因为它不的笔画「不会拐弯」。




真的是又爱又恨。



最后


经过上面的一系列「折腾」,可以看到 ChatGPT 并没有我们想象中智能,如果面向 GPT 去开发,甚至可能并不靠谱,因为它并不对单一问题给出固定答案,甚至很多内容都是临场瞎编的,这也是因为大语言模型本身如何保证「正确」是一个复杂的问题,但是 ChatGPT 的魅力也来自于此:



它并不是完全基于语料来的统计来给答案



当然这也和 ChatGPT 本身的属性有关系, ChatGPT 目前的火爆有很大一部分属于「意外」,目前看它不是一个被精心产品化后的 2C 产品,反而 ChatPDFBiBiGPT 这种场景化的包装落地会是它未来的方向之一。


而现在 OpenAI 发布了多模态预训练大模型 CPT-4GPT-4 按照官方的说法是又得到了飞跃式提升:强大的识图能力;文字输入限制提升至 2.5 万字;回答准确性显著提高;能够生成歌词、创意文本,实现风格变化等等



所以我很期待 ChatGPT 可以用 Flutter 帮我画出一只米老鼠, 尽管 ChatGPT 现在可能会让你因为得到 1+1=3 这样的答案而「发疯”」,但是 AI 的魅力在于,它终有一天能得到准确的结果


作者:恋猫de小郭
来源:juejin.cn/post/7210605626501595195
收起阅读 »

如何写一个炫酷的大屏仿真页

web
前言 之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样: 看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿...
继续阅读 »

前言


之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样:


OPPO折叠屏


看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿真。


看效果:


11.gif


由于使用录屏,所以看着有点卡顿,实际效果非常流畅!


一、基础知识具备


仿生页里面用到很多自定义 View 的知识,比如:



  1. 贝塞尔曲线

  2. 熟悉 Canvas、Paint 和 Path 等常用的Api

  3. Matrix


具备这些知识以后,我们就可以看懂绝大部分的代码了。这一篇同样并不想和大家过多的介绍代码,具体的可以看一下代码。


二、双仿真和单仿真有什么不同


我写双仿真的时候,感觉和单仿真有两点不同:



  • 绘制的页数

  • 背部的贴图处理


首先,单仿真只要准备两页的数据:


QQ20230312-0.jpg


背部的内容也是第一页的内容,需要对第一页内容进行翻转再平移。


而双仿真需要准备六页的内容,拿左边来说:


QQ20230312-1.jpg


我们需要准备上层图片(柯基)、背部图片(阿拉斯加)和底部图片(吉娃娃,看不清),因为我们不知道用户会翻页哪侧,所以两侧一共需要准备六页的数据。


由于翻转机制的不一样,双仿真对于背部的内容只需要平移就行,但是需要新的一页内容,这里相对来说比单仿真简单。


三、我做了哪些优化


主要对翻页的思路进行了优化,


正常的思路是这样的,手指落下的点即页脚:


QQ20230312-2.jpg


这样写起来更加简单,但是对于用户来说,可操作的区域比较小,相对来说有点难用。


另外一种思路就是,手指落下的点即到底部同等距离的边:


QQ20230312-4.jpg


即手指落位的位置到当前页页脚距离 = 翻动的位置到当前页脚的距离


使用这种方式的好处就是用户可以操作的区域更大,翻书的感觉跟翻实体书的感觉更类似,也更加跟手。


总结


这篇文章就讲到这了,这个 Demo 其实是一个半成品,还有一些手势没处理,阴影的展示还有一些问题。


写仿真比较难的地方在于将一些场景转化成代码,有些地方确实很难去想。


talk is cheap, show me code:


仓库地址:github.com/mCyp/Double…


如果觉得本文不错,点赞是对本文最好的肯定,如果你还有任何问题,欢迎评论区讨论!


作者:九心
来源:juejin.cn/post/7209625823581978680
收起阅读 »

Android 可视化预览及编辑Json

Android 可视化编辑json JsonPreviewer 项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。 开发时单个广告可能需要多次修改配置来测试...
继续阅读 »


Android 可视化编辑json JsonPreviewer


项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。


开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效





    1. 每次抓包修改配置





    1. 本地导入配置,从磁盘读取




但两种方式都有一定弊端



  • 首先测试时依赖电脑修改配置

  • 无法直观预览广告配置


考虑到开发时经常使用的Json格式化工具,既可以直观的预览Json, 还可以在线编辑


那么就考虑将Json格式化工具移植到项目测试模块中


web网页可以处理Json格式化,同理在Android webView 中同样可行, 只需要引入处理格式化的JS代码即可。


查找资料,发现一个很实用的文章可视化编辑json数据——json editor


开始处理


首先准备好WebView的壳子


    //初始化
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
binding.webView.settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportZoom(true)
useWideViewPort = true
builtInZoomControls = true
}
binding.webView.addJavascriptInterface(JsInterface(this@MainActivity), "json_parse")
}

//webView 与 Android 交互
inner class JsInterface(context: Context) {
private val mContext: Context

init {
mContext = context
}

@JavascriptInterface
fun configContentChanged() {
runOnUiThread {
contentChanged = true
}
}

@JavascriptInterface
fun toastJson(msg: String?) {
runOnUiThread { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show() }
}

@JavascriptInterface
fun saveConfig(jsonString: String?) {
runOnUiThread {
contentChanged = false
Toast.makeText(mContext, "verification succeed", Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun parseJsonException(e: String?) {
runOnUiThread {
e?.takeIf { it.isNotBlank() }?.let { alert(it) }
}
}
}


加载json并在WebView中展示



viewModel.jsonData.observe(this) { str ->
if (str?.isNotBlank() == true) {
binding.webView.loadUrl("javascript:showJson($str)")
}
}


WebView 加载预览页面


        binding.webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel.loadAdConfig(this@MainActivity)
}
}

binding.webView.loadUrl("file:///android_asset/preview_json.html")



Json 预览页, preview_json.html实现



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="jquery.json-viewer.css"
rel="stylesheet" type="text/css">

</head>
<style type="text/css">
#json-display {
margin: 2em 0;
padding: 8px 15px;
min-height: 300px;
background: #ffffff;
color: #ff0000;
font-size: 16px;
width: 100%;
border-color: #00000000;
border:none;
line-height: 1.8;
}
#json-btn {
display: flex;
align-items: center;
font-size: 18px;
width:100%;
padding: 10;

}
#format_btn {
width: 50%;
height: 36px;
}
#save_btn {
width: 50%;
height: 36px;
margin-left: 4em;
}

</style>
<body>
<div style="padding: 2px 2px 2px 2px;">
<div id="json-btn" class="json-btn">
<button type="button" id="format_btn" onclick="format_btn();">Format</button>
<button type="button" id="save_btn" onclick="save_btn();">Verification</button>

</div>
<div>
<pre id="json-display" contenteditable="true"></pre>
</div>
<br>
</div>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.json-viewer.js"></script>
<script>

document.getElementById("json-display").addEventListener("input", function(){
console.log("json-display input");
json_parse.configContentChanged();
}, false);
function showJson(jsonObj){
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}
function format_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var jsonObj = JSON.parse(jsonval); //parse string to json
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}


function save_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var saveFailed = false;
try {
var jsonObj = JSON.parse(jsonval); //parse
} catch (e) {
console.error(e.message);
saveFailed = true;
json_parse.parseJsonException(e.message); // throw exception
}
if(!saveFailed) {
json_parse.saveConfig(jsonval);
}
}

</script>
</body>
</html>


这其中有两个问题需注意





    1. 如果value的值是url, 格式化后缺少引号
      从json-viewer.js源码可以发现,源码中会判断value是否是url,如果是则直接输出




处理方式:在json 左右添加上双引号


    if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + '"' +json + '"' + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '
\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}




    1. 如果折叠后json-viewer会增加<a>标签,即使使用text()方法获取到纯文本数据,这里面也包含了“n items”的字符串,那么该如何去除掉这些字符串呢?




 var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();

总结


使用时只需将json文件读取,传入preview_json.html的showJson方法


编辑结束后, 点击Save 即可保存


示例代码 Android 可视化编辑json JsonPreviewer




(可视化编辑json数据——json editor)[blog.51cto.com/u_56500

11/5…]

收起阅读 »

我本可以忍受黑暗,如果我未曾见过光明

随想录】我本可以忍受黑暗,如果我未曾见过光明 随想录 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 老文章? 这篇文章大体结构早已在我语雀...
继续阅读 »

随想录】我本可以忍受黑暗,如果我未曾见过光明



随想录


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)



视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/
复制代码

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



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

你没有必要完全辞去工作

我认为我们可以而且应该探索生活中的许多事情,我写这篇文章是为了展示成为一名创客和拥有一份全职工作不仅是可能的,而且使得你可用的机会多样化,这可以让你更加敏锐,务实与坚定。 在这篇文章中,我想解决三个关键概念。 首先是针对那些认为自己时间不够的人,以及为什么我觉...
继续阅读 »

我认为我们可以而且应该探索生活中的许多事情,我写这篇文章是为了展示成为一名创客和拥有一份全职工作不仅是可能的,而且使得你可用的机会多样化,这可以让你更加敏锐,务实与坚定。


在这篇文章中,我想解决三个关键概念。


首先是针对那些认为自己时间不够的人,以及为什么我觉得这种观念往往是错误的。


第二是强调坚持工作的好处,以及为什么成功人士最擅长的是降低风险,而不是最大化。


最后,第三部分将指出出一些我认为我们都可以在思维方式上做出的改进--超越单纯地创业和全职工作,这些概念希望能帮助你在一个更大的空间里进行优化,或者也许完全去除这个限制。


1.“我没有足够的时间”


美国人平均工作时长为 8.8 小时,这源于工业革命,并一直延续到 21 世纪,这处于一般的考量,而不是深思熟的考虑。罗伯特·欧文 (Robert Owen) 精心设计了“八小时劳动、八小时娱乐、八小时休息”的说法,努力让人们在合理的时间内工作,同时仍然能有效地运营工厂。


尽管世界和劳动力发生了翻天覆地的变化,但这种“工作时间”和“我的时间”的概念至今仍然存在。我在这里不是要质疑 40 小时模型(已经有太多资源了 - 谁还没有听说过 4 小时工作周?),而是质疑对“我的时间”的看法。


对许多人来说,长时间的工作意味着他们有权享受这种 "我的时间",并将 "我的时间 "设计得与 "工作时间 "尽可能地不同。对许多人来说,它看起来很像这样。Netflix and chill


但是,如果我们不再将“我的时间”想象成放松时间,而是完全按照它的标题:是时候专注于自己并与您的目标保持一致,那会怎样呢?如果你需要休息,那就休息吧。但如果你的目标是有朝一日成为一名企业家,那么应该投入大量的“我的时间”来实现这一目标,因为它不会自行发生。 “我的时间”不应该只是不累人的活动,而是任何可以帮助个人达到他们希望进入的未来状态的活动。


每天大约有 16 个小时分配给工作和睡眠,每个人大约有 8 个小时可以分配给“我的时间”,如果使用得当,每年将近 3000 个小时可以取得很多成就。


Sleep, commute, work, repeat. 睡觉、通勤、工作,如此重复。


Sleep, commute, work, repeat. 睡觉、通勤、工作,如此重复。



“Most people overestimate what they can do in a day, but underestimate what they can do in a year.”

“大多数人高估了他们一天能做的事情,却低估了他们一年能做的事情。”



还有一种误解是,为了建立一个可持续的业务,你需要花费大量的时间才能达到目的。虽然确实需要付出大量的努力,但最重要的是长期持续的努力。大多数人低估了复利的这个概念。


compound-interest.jpg


The power of compound interest. 复利的力量。


看看下面的等式:



  • 1.01³⁶⁵ = 37.8

  • 1.10³⁰ = 17.5


在一年内每天坚持改善你的业务(或生活)1%,比一个月内每天改善 10% 的效果要好一倍。坚持不懈加上复利的力量是强大的。



“如果一切都是最重要的,那么就没有什么是重要的。”



我认为,大多数人在生活中要么没有清楚地确定优先事项,要么将所有事情都考虑在内。虽然我相信雄心壮志,但成功的一个关键步骤是确定核心的优先事项,并消除在此之外的噪音。


最主要的优先事项是动态的,可以随着时间的推移而改变,但我认为,在一个特定的时间,你真的不能有超过 3 个核心重点。


设定这些重点之后,就是要改变行为,按照这些重点生活。再次,如果大多数人要客观地反思他们是如何花费时间的,他们会得到这样的结果。


1_nALgHXJAKmPcyqO10XvvCw.jpg


典型的一天。


对我来说,这就是我的个人优先事项随着时间的推移而发生的变化:



  • 2017 年:工作、旅行、人际关系

  • 2018 年:工作、学习编码、构建副业

  • 2019 年:工作、扩展副业、分享想法(写作、演讲)


为了在创作项目的同时维持一份全职工作,我不得不排除干扰。例如,我不看电视。我不通勤。我目前没有恋爱。这些都是主动的选择。


当然,其中一些东西将是暂时的(例如:人际关系),但我也注意到我在生活中重新引入的东西,以及它是否会促进、带走,或成为我的北极星之一。


我认为这个概念也可以被认为是分层的时间投资。对于你所做的任何事情,如果它有助于你的北极星,就把它看作是一级投资。对于那些对你的成长完全没有贡献的事情,也许可以把它标记为第四级。这并不意味着你不能跨层花费时间,但你花在每个层级上的时间应该反映出你对它们的关心程度。


示例(这对任何人来说都是一个独立的练习):



2. 坚持工作的好处


希望上一节有助于说服你,你有足够的时间全职工作,同时创建副业,或者说,如果你调整你的价值观→优先事项→行为,就可以在你的生活中融入更多的东西。在这一节中,我希望能表达为什么保持全职工作可以是一件美好的事情。


付费学习



“Some workplaces are definitely broken, but the entire workforce isn’t.”



我经常听到有人说 "我等不及要出去 "这样的话,指的是辞掉工作,最终自己当老板。在辞职之前,要考虑为什么要辞掉工作。通常情况下,这不是他们有全职工作的问题,而是他们所从事的特定工作,或许是他们所汇报的特定人员。


所有的人都应该努力找到一份能够赋予他们权力、激励他们并让他们在某些方面得到成长的工作。大公司实际上保证了这一点--你很少是公司里最聪明的人,你当然也不会是公司里每个方面都最有能力的人


在我的 "日常工作 "中,我可以不断向比我更聪明的人学习,并为此获得报酬。我还面临着我的副业项目根本不会遇到的挑战,我经常需要学习如何与他人一起解决这些挑战。我鼓励人们有意识地设计他们的职业道路,以掌握从硬到软的新技能。如果你最终决定在未来自立门户,那么这两者都会很重要。


随着劳动力变得更加活跃,在协同处理自己的项目的同时向他人学习的能力是许多人正在开发的。事实上,我在 Twitter 上对数百人进行了调查,发现相当多的人都在这样做。


保持新鲜的想法和清晰的头脑


在学习之外,保持一份 FT 工作还有其他实实在在的好处,可以帮助你建立一个更可持续的副业。


根据个人经验,我发现把我的工作和副业分开,使我仍能在两者中找到独立的乐趣。每当我从一个环境切换到另一个环境,特别是在创业方面,它仍然是 "有趣的"。


我认为这特别是因为在目前的状态下,创业不是我的生命线。我希望有一天它确实成为更有意义的东西,但就目前而言,我可以在不受立即赚钱需求影响的情况下就我的项目做出决定。


更重要的是,我可以专注于通过我真正关心的项目来表达自己,而不是专注于可能产生美元的东西,通过这个过程,我贴近我的价值观。换句话说,我可以专注于创造价值,而不是专门去获取价值,类似于 Gumroad 的创始人 Sahil Lavingia 如何转向做这件事,或者 Warby Parker 的创始人如何确保金钱不会战胜他们的价值观。



“开始之前我们是四个朋友,我们承诺公平对待彼此比成功更重要。” —— 亚当·格兰特



结合以上几点,当我意识到一个项目没有任何价值时,我可以放弃一个项目或理性思考,我也不需要拿 VC 的钱或倾向于我不相信的投资者。



“在一个领域拥有安全感让我们可以自由地在另一个领域独创。通过在财务上覆盖我们的基地,我们摆脱了出版半生不熟的书籍、出售劣质艺术品或开展未经考验的业务的压力。” —— 亚当·格兰特



最后,我可以在技能学习方面投入适当的时间。我把这比喻为这样一个概念:上市公司不太关注通过创新创造长期价值,而是关注下一个季度的收入数字。我是一只私人股票,可以专注于我自己和我的技能,目的是为了长期建设它们。


换句话说,我的表达和创意之间的明确区分与我的生命线分离,我认为这有助于做出更有效的决定。


进行大量试验,然后全力以赴



“企业家这个词,正如经济学家理查德·坎蒂隆创造的那样,字面意思是“风险承担者”。 —— 亚当·格兰特



有一个普遍的误解,认为企业家都是“冒险者”,你需要“全力以赴”才能成功。在亚当·格兰特的著作 Originals (中文名:《离经叛道:不按常理出牌的人如何改变世界》)中,这两者都被证明是错误的;企业家不一定是冒险者,而是更善于评估风险和对冲他们的赌注。



“当 Pierre Omidyar 创立 eBay 时,这只是一种爱好;在接下来的九个月里,他一直以程序员的身份工作,直到他的在线市场为他赚的钱比他的工作还多才离开。最好的企业家不是风险最大化者。 “他们在冒险中承担了风险。” —— 亚当·格兰特



Grant 还引用了 Joseph Raffiee 和 Jie Feng 的另一项研究,该研究从 1994 年到 2008 年对 5000 多名美国人提出了以下问题:“当人们开始创业时,他们最好是继续工作还是辞掉日常工作?”


结果呢?他们发现,那些离开工作岗位的人这样做不是出于经济需要,而是出于纯粹的自信。然而,那些更不确定的人比更喜欢冒险的人失败几率要低 33%。


另一项研究表明,那些在 Fast Company 最具创新力排行榜上名列前茅的企业家也倾向于坚持他们的日常工作,包括著名企业家 Phil Knight(耐克)、Steve Wozniak(苹果)以及谷歌创始人 Larry Page 和 Sergey Brin。


奈特当了 5 年的会计师,同时从他的后备箱里卖鞋,沃兹尼亚克继续在惠普工作,谷歌人继续在斯坦福大学攻读博士学位。这些只是书中的一些原作——Grant 还引用了类似的故事,包括 Brian May 在加入 Queen 之前研究天体物理学,John Legend 即使在发行他的第一张专辑后仍然担任管理顾问,Spanx 创始人 Sara Blakely 销售传真机,她的公司原型和规模最终成为世界上最年轻的白手起家的亿万富翁,著名作家斯蒂芬金在他的第一个故事发表后担任了 7 年的看门人、教师和加油站服务员。


我们都有多种激情,我认为生活就是在有意义的时候进行战略转型。无需立即从一个场景切换到另一个场景。人们可能认为冒险者很酷,但在另一边取得成功更酷。


3. 重构你的思维方式


无论你是否选择全职工作,同时探索副业项目,我认为我们都可以更有效地打开我们的思想,接受不同的思维方式。本节将涉及一些我认为我们可以停止限制自己和他人的方式。


世间安得两全法


人们喜欢把东西装进盒子里。你会听到人们总是使用名词或形容词作为明确的标签:



  • 技术还是非技术

  • 快乐或悲伤

  • 职员或企业家


看到我要去哪里了吗?尽管有这些标签,但我相信几乎所有东西都可以用某种曲线表示;特别是在技能习得方面。例如,你什么时候真正“成为”程序员?


1_MEZ99GbXHnSq27aDQaBxcQ.jpeg


真正的创造性思维者不再用二元思维,而是能够将这些曲线的概念内化。他们把事情看成一个斜坡、楼梯或维恩图,而不是一系列的盒子。当你消除二元对立时,你就能更清楚地看到其他选择,比如慢慢增加你对副业的时间投入,而不是立即辞职。


合理规划您的生活


我认为,如果有人认为自己的工作效率已经达到全球最高水平,那是非常天真的。事实是,我们都有改进的余地,不仅是在更快/更干净方面,而且是在做出更好地决定,删除那些首先不应该出现在我们盘子里的工作。


如果您选择从事多项工作,请确保您对所有这些都有独立的 KPI。人们倾向于在企业中这样做,但这个概念在我们的个人生活中却很少见。你能量化过去一年你在自己身上投入了多少时间吗?大多数人做不到。


如果两者都没有 KPI,那么没有明确 KPI 的那个自然会被搁置一旁,或者得不到应有的关注。


我还认为理解“元工作”的概念很重要。我对元工作的定义如下:“如果你连续一年做那个活动,你的生活会有什么不同吗?”


让我详细说明。


如果明年我每天都回复电子邮件,我的生活会不会发生重大变化?换句话说,我会从 A 搬到 B 吗?答案是不。


洗衣服、买杂货或做指甲等事情也是如此。哦,是的,Netflix 也一样。


还有第二种类型的任务,我将其标记为绝对任务。如果始终如一地完成,您可能会看到您的技能或生活发生重大变化。例如:如果你每天阅读一年,你的知识储备、创造力和阅读速度都可能会提高。如果你每天锻炼,你的健康无疑会有所改善。同样,如果您每天花 1 小时学习编码,到年底您将拥有全新的技能组合。


虽然元任务在生活中是不可避免的,但要确保你的生活目标不是元的,它们需要是绝对的。当你创建当天的待办事项清单时,确保至少有一件事是绝对的(记住:1.01³⁶⁵=37.8)。当然,当你可以时:尽可能多地将元任务自动化。元任务在很多方面都可以成为分心的代名词,除非它们给你的生活带来某种独立的快乐。


一夜成名的神话


最后,我想澄清最后一个误解:没有一夜成名这回事。这种误解源于媒体的运作方式。


TechCrunch 永远不会写 X 人如何用 Y 年时间引导一个可持续的非独角兽企业,遵守其价值观并尊重人们的隐私。离群索居者很耀眼,但他们仍然是离群索居者。


直到几年前,我才真正理解持续攀登的概念。我以为每一个成功的人都说要付出很多工作和努力,这只是在为他们的运气自我辩护。



"当我们惊叹于那些为创造力提供动力并推动世界变革的原创者时,我们往往会认为他们是由不同的布料剪成的。" -- 亚当-格兰特



现实情况是,构建任何有价值的东西都需要时间。当然,全职工作可能需要更长的时间来构建,但这没关系。


如果你目前有全职工作,不要把自己放在一个框框里,而是要开始为你觉得有趣的想法工作。完美的想法永远不会出现,,所以我鼓励每个人开始每周花 1 小时来研究他们认为有吸引力的想法,并逐渐增加,直到你处于一个可以让他们全职工作的地方。将你的生命线(你的工作)与你的项目分开,这种精神上的清醒可能是最健康和最周到的做法。


记住,你没有成为企业家的时刻,所以没有必要为了将自己定义为企业家而辞掉工作。


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

想退更多税?看完这篇就够了

前言 最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值...
继续阅读 »

前言


最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值变多。


image.png


专项附加扣除填报


首先打开个人所得税APP,点击下面的tabbar办税,然后点击专项附加扣除填报,就会出现如下图,通过填报上面的这些去进行增加专项附加扣除合计值。


0b957f5ecc7afa419053f890511d9aa.jpg


租房租金(和房贷只能二选一)



  • 要求:工作租房子了 (不需要合同也能填哦)

  • 扣除标准:省会或者直辖市 1500 / 月,二线城市 1100 /月,小城市 800 /月

  • 年总计:省会或者直辖市 18000,二线城市 13200,小城市 9600

  • 划重点: 多段租房经历可以多填几个租房,一定要填满!!!,一定要填,没租房子也可以填,随便填一个地址也行,这个不需要租房合同,也无法追查到的,但是和贷款只能二选一,具体选哪个请继续看。


image.png


住房贷款利息(和租房只能二选一)



  • 要求:首套房贷款

  • 扣除标准:1000/月

  • 年总计:12000

  • 划重点: 和租房二选一哦,具体选哪个看你所在的城市,哪个钱多选哪个~


image.png


子女教育



  • 要求:子女处于全日制学历教育阶段(幼儿园+小学+中学+大学),年龄>3岁。

  • 扣除标准:每人每月 1000 元 (两个孩子是一个月2000哦)

  • 年总计:12000(一个孩子的情况下)


image.png


继续教育



  • 要求:考证或者学历提升

  • 扣除标准:考证拿到证的当年一次性扣除:3600,学历提升 400/月

  • 年总计:考证:3600,学历提升:4800

  • 划重点: 这里的证必须是收录在 国家资格目录 的证书哦~


image.png


大病医疗


如果可以,我希望这个没有人申请,身体健康比什么都重要。



  • 要求:医保报销后个人花费15k+

  • 扣除标准:8w内根据你个人花费,花多少这个就是多少。

  • 年总计:根据实际。


image.png


赡养老人



  • 要求:父母 60 岁以上

  • 扣除标准:独生子女2000/月,非独生子女:和兄弟姐妹分摊 2000

  • 年总计:独生子女:24000,非独生子女: 0 ~ 12000


image.png


3岁以下婴幼儿照护



  • 要求:孩子 3 岁以下

  • 扣除标准:每月每人 1000 (两个孩子就是2000/月哦)

  • 年总计:12000(一个孩子的情况下)


如何能退更多的税


打开个人所得税APP → 我要查询 → 申报查询 → 已完成→ 选择当年的综合年度汇算→ 点击专项附加扣除合计,查看自己当年的专项附加扣除合计,可以简单计算下自己填报的专项附加扣除是否都已经填写。



PS:画重点!!!注意检查时间,我开始就是租房没填满,只写了一段租房经历,所以差点错过好几个亿,如果是2023年的填报,一定要注意你专项附加扣除是否涵盖整个2022年的日期。



113b48e415b80b8861261f3b3d961ab.jpg


有年终奖的情况


可能有些小伙伴公司会发年终奖 (如果和我一样没发年终奖,那这里可以跳过了,发年终奖的公司给我来一打),最好选择单独计税,不过都可以试一下,综合并入和单独合计,哪个退的多用哪个。


历史申报


以前的申报如果有没补充的,现在还是可以填哦,快去看看之前的申报,说不定会有惊喜哦。


最后


如果是需要补税的情况,如果少于400是不需要补的哦,当然如果大于400,一定要快点去缴纳哦,国家都留有档案,防止影响以后征信,哈哈哈,祝大家都能退几个亿~


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

保姆级JAVA对接ChatGPT教程,实现自己的AI对话助手

1.前言 大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API K...
继续阅读 »

1.前言


大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API Key的申请以及网络代理的搭建,那么事不宜迟,我们现在开始。


2.对接流程


2.1.API-Key的获取


首先第一步要获取OpenAI接口的API Key,该Key是你用来调用接口的token,主要用于接口鉴权。获取该key首先要注册OpenAi的账号,具体可以见我的另外一篇文章,ChatGPT保姆级注册教程



  1. 打开platform.openai.com/网站,点击view API Key,


image.png



  1. 点击创建key


image.png



  1. 弹窗显示生成的key,记得把key复制,不然等会就找不到这个key了,只能重新创建。


image.png


将API Key保存好以备用


2.2.API用量的查看


这里可以查看API的使用情况,新账号注册默认有5美元的试用额度,之前都是18美元,API成本降了之后试用额度也狠狠地砍了一刀啊,哈哈。


image.png


2.3.核心代码实现


2.3.1.pom依赖


http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.webtap
webtap
0.0.1
jar


org.springframework.boot
spring-boot-starter-parent
2.1.2.RELEASE




org.springframework.boot
spring-boot-starter-web


org.springframework.boot
spring-boot-starter-thymeleaf


nz.net.ultraq.thymeleaf
thymeleaf-layout-dialect


org.springframework.boot
spring-boot-starter-data-jpa


org.springframework.boot
spring-boot-devtools


org.springframework.boot
spring-boot-starter-test


org.springframework.boot
spring-boot-starter-mail



mysql
mysql-connector-java


org.apache.commons
commons-lang3
3.4


commons-codec
commons-codec


org.jsoup
jsoup
1.9.2



com.alibaba
fastjson
1.2.56


net.sourceforge.nekohtml
nekohtml
1.9.22


com.github.pagehelper
pagehelper-spring-boot-starter
1.4.1


org.projectlombok
lombok


org.apache.httpcomponents
httpasyncclient
4.0.2


org.apache.httpcomponents
httpcore-nio
4.3.2



org.apache.httpcomponents
httpclient
4.3.5


commons-codec
commons-codec




commons-httpclient
commons-httpclient
3.1


commons-codec
commons-codec




org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.1


com.github.ulisesbocchio
jasypt-spring-boot-starter
2.0.0







org.springframework.boot
spring-boot-maven-plugin






2.3.2.实体类ChatMessage.java


用于存放发送的消息信息,注解使用了lombok,如果没有使用lombok可以自动生成构造方法以及get和set方法


@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
//消息角色
String role;
//消息内容
String content;
}

2.3.3.实体类ChatCompletionRequest.java


用于发送的请求的参数实体类,参数释义如下:


model:选择使用的模型,如gpt-3.5-turbo


messages :发送的消息列表


temperature :温度,参数从0-2,越低表示越精准,越高表示越广发,回答的内容重复率越低


n :回复条数,一次对话回复的条数


stream :是否流式处理,就像ChatGPT一样的处理方式,会增量的发送信息。


max_tokens :生成的答案允许的最大token数


user :对话用户


@Data
@Builder
public class ChatCompletionRequest {

String model;

List messages;

Double temperature;

Integer n;

Boolean stream;

List stop;

Integer max_tokens;

String user;
}

2.3.4.实体类ExecuteRet .java


用于接收请求返回的信息以及执行结果



/**
* 调用返回
*/

public class ExecuteRet {

/**
* 操作是否成功
*/

private final boolean success;

/**
* 返回的内容
*/

private final String respStr;

/**
* 请求的地址
*/

private final HttpMethod method;

/**
* statusCode
*/

private final int statusCode;

public ExecuteRet(booleansuccess, StringrespStr, HttpMethodmethod, intstatusCode) {
this.success =success;
this.respStr =respStr;
this.method =method;
this.statusCode =statusCode;
}

@Override
public String toString()
{
return String.format("[success:%s,respStr:%s,statusCode:%s]", success, respStr, statusCode);
}

/**
*@returnthe isSuccess
*/

public boolean isSuccess() {
return success;
}

/**
*@returnthe !isSuccess
*/

public boolean isNotSuccess() {
return !success;
}

/**
*@returnthe respStr
*/

public String getRespStr() {
return respStr;
}

/**
*@returnthe statusCode
*/

public int getStatusCode() {
return statusCode;
}

/**
*@returnthe method
*/

public HttpMethod getMethod() {
return method;
}
}

2.3.5.实体类ChatCompletionChoice .java


用于接收ChatGPT返回的数据


@Data
public class ChatCompletionChoice {

Integer index;

ChatMessage message;

String finishReason;
}

2.3.6.接口调用核心类OpenAiApi .java


使用httpclient用于进行api接口的调用,支持post和get方法请求。


url为配置文件open.ai.url的值,表示调用api的地址:https://api.openai.com/ ,token为获取的api-key。
执行post或者get方法时增加头部信息headers.put("Authorization", "Bearer " + token); 用于通过接口鉴权。



@Slf4j
@Component
public class OpenAiApi {

@Value("${open.ai.url}")
private String url;
@Value("${open.ai.token}")
private String token;

private static final MultiThreadedHttpConnectionManagerCONNECTION_MANAGER= new MultiThreadedHttpConnectionManager();

static {
// 默认单个host最大链接数
CONNECTION_MANAGER.getParams().setDefaultMaxConnectionsPerHost(
Integer.valueOf(20));
// 最大总连接数,默认20
CONNECTION_MANAGER.getParams()
.setMaxTotalConnections(20);
// 连接超时时间
CONNECTION_MANAGER.getParams()
.setConnectionTimeout(60000);
// 读取超时时间
CONNECTION_MANAGER.getParams().setSoTimeout(60000);
}

public ExecuteRet get(Stringpath, Map headers) {
GetMethod method = new GetMethod(url +path);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
}

public ExecuteRet post(Stringpath, Stringjson, Map headers) {
try {
PostMethod method = new PostMethod(url +path);
//log.info("POST Url is {} ", url + path);
// 输出传入参数
log.info(String.format("POST JSON HttpMethod's Params = %s",json));
StringRequestEntity entity = new StringRequestEntity(json, "application/json", "UTF-8");
method.setRequestEntity(entity);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
} catch (UnsupportedEncodingExceptionex) {
log.error(ex.getMessage(),ex);
}
return new ExecuteRet(false, "", null, -1);
}

public ExecuteRet execute(HttpMethodmethod) {
HttpClient client = new HttpClient(CONNECTION_MANAGER);
int statusCode = -1;
String respStr = null;
boolean isSuccess = false;
try {
client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "UTF8");
statusCode = client.executeMethod(method);
method.getRequestHeaders();

// log.info("执行结果statusCode = " + statusCode);
InputStreamReader inputStreamReader = new InputStreamReader(method.getResponseBodyAsStream(), "UTF-8");
BufferedReader reader = new BufferedReader(inputStreamReader);
StringBuilder stringBuffer = new StringBuilder(100);
String str;
while ((str = reader.readLine()) != null) {
log.debug("逐行读取String = " + str);
stringBuffer.append(str.trim());
}
respStr = stringBuffer.toString();
if (respStr != null) {
log.info(String.format("执行结果String = %s, Length = %d", respStr, respStr.length()));
}
inputStreamReader.close();
reader.close();
// 返回200,接口调用成功
isSuccess = (statusCode == HttpStatus.SC_OK);
} catch (IOExceptionex) {
} finally {
method.releaseConnection();
}
return new ExecuteRet(isSuccess, respStr,method, statusCode);
}

}

2.3.7.定义接口常量类PathConstant.class


用于维护支持的api接口列表


public class PathConstant {
public static class MODEL {
//获取模型列表
public static String MODEL_LIST = "/v1/models";
}

public static class COMPLETIONS {
public static String CREATE_COMPLETION = "/v1/completions";
//创建对话
public static String CREATE_CHAT_COMPLETION = "/v1/chat/completions";

}
}

2.3.8.接口调用调试单元测试类OpenAiApplicationTests.class


核心代码都已经准备完毕,接下来写个单元测试测试下接口调用情况。



@SpringBootTest
@RunWith(SpringRunner.class)
public class OpenAiApplicationTests {

@Autowired
private OpenAiApi openAiApi;
@Test
public void createChatCompletion2() {
Scanner in = new Scanner(System.in);
String input = in.next();
ChatMessage systemMessage = new ChatMessage('user', input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
System.out.println(context.getContent());
}

}


  • 使用Scanner 用于控制台输入信息,如果单元测试时控制台不能输入,那么进入IDEA的安装目录,修改以下文件。增加最后一行增加-Deditable.java.test.console=true即可。


image.png
image.png




  • 创建ChatMessage对象,用于存放参数,role有user,system,assistant,一般接口返回的响应为assistant角色,我们一般使用user就好。




  • 定义请求参数ChatCompletionRequest,这里我们使用3.1日发布的最新模型gpt-3.5-turbo-0301。具体都有哪些模型大家可以调用v1/model接口查看支持的模型。




  • 之后调用openAiApi.post进行接口的请求,并将请求结果转为JSON对象。取其中的choices字段转为ChatCompletionChoice对象,该对象是存放api返回的具体信息。


    接口返回信息格式如下:


    {
    "id": "chatcmpl-6rNPw1hqm5xMVMsyf6PXClRHtNQAI",
    "object": "chat.completion",
    "created": 1678179420,
    "model": "gpt-3.5-turbo-0301",
    "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 339,
    "total_tokens": 355
    },
    "choices": [{
    "message": {
    "role": "assistant",
    "content": "\n\nI. 介绍数字孪生的概念和背景\n A. 数字孪生的定义和意义\n B. 数字孪生的发展历程\n C. 数字孪生在现代工业的应用\n\nII. 数字孪生的构建方法\n A. 数字孪生的数据采集和处理\n B. 数字孪生的建模和仿真\n C. 数字孪生的验证和测试\n\nIII. 数字孪生的应用领域和案例分析\n A. 制造业领域中的数字孪生应用\n B. 建筑和城市领域中的数字孪生应用\n C. 医疗和健康领域中的数字孪生应用\n\nIV. 数字孪生的挑战和发展趋势\n A. 数字孪生的技术挑战\n B. 数字孪生的实践难点\n C. 数字孪生的未来发展趋势\n\nV. 结论和展望\n A. 总结数字孪生的意义和价值\n B. 展望数字孪生的未来发展趋势和研究方向"
    },
    "finish_reason": "stop",
    "index": 0
    }]
    }



  • 输出对应的信息。




2.3.9.结果演示


image.png


2.4.连续对话实现


2.4.1连续对话的功能实现


基本接口调通之后,发现一次会话之后,没有返回完,输入继续又重新发起了新的会话。那么那么我们该如何实现联系上下文呢?其实只要做一些简单地改动,将每次对话的信息都保存到一个消息列表中,这样问答就支持上下文了,代码如下:


List messages = new ArrayList<>();
@Test
public void createChatCompletion() {
Scanner in = new Scanner(System.in);
String input = in.next();
while (!"exit".equals(input)) {
ChatMessage systemMessage = new ChatMessage(ChatMessageRole.USER.value(), input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
messages.add(context);
in = new Scanner(System.in);
input = in.next();
}
}

因为OpenAi的/v1/chat/completions接口消息参数是个list,这个是用来保存我们的上下文的,因此我们只要将每次对话的内容用list进行保存即可。


2.4.2结果如下:


image.png


image.png


4.常见问题


4.1.OpenAi接口调用不通


因为https://api.openai.com/地址也被限制了,但是接口没有对地区做校验,因此可以自己搭建一个香港代理,也可以走科学上网。


我采用的是香港代理的模式,一劳永逸,具体代理配置流程如下:



  1. 购买一台香港的虚拟机,反正以后都会用得到,作为开发者建议搞一个。搞活动的时候新人很便宜,基本3年的才200块钱。

  2. 访问nginx.org/download/ng… 下载最新版nginx

  3. 部署nginx并修改/nginx/config/nginx.conf文件,配置接口代理路径如下


server {
listen 19999;
server_name ai;

ssl_certificate /usr/local/nginx/ssl/server.crt;
ssl_certificate_key /usr/local/nginx/ssl/server.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

#charset koi8-r;


location /v1/ {
proxy_pass ;
}
}


  1. 启动nginx

  2. 将接口访问地址改为nginx的机器出口IP+端口即可


如果代理配置大家还不了解,可以留下评论我单独出一期教程。


4.2.接口返回401


检查请求方法是否增加token字段以及key是否正确


5.总结


至此JAVA对OpenAI对接就已经完成了,并且也支持连续对话,大家可以在此基础上不断地完善和桥接到web服务,定制自己的ChatGPT助手了。我自己也搭建了个平台,不断地在完善中,具体可见下图,后续会开源出来,想要体验的可以私信我获取地址和账号哈


image.png


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

Jetpack:Android新一代导航管理Navigation

前言 不知道小伙伴们是否注意到,用AS创建一个默认的新项目后,MainActivity已经有了很大的不同,最大的区别就是新增加了两个Fragment,同时我们注意到这两个Fragment之间跳转的时候并没有使用之前FragmentTransaction这种形式...
继续阅读 »

前言


不知道小伙伴们是否注意到,用AS创建一个默认的新项目后,MainActivity已经有了很大的不同,最大的区别就是新增加了两个Fragment,同时我们注意到这两个Fragment之间跳转的时候并没有使用之前FragmentTransaction这种形式,而是使用了NavController和NavHostFragment,这就是新一代导航管理————Navigation。


项目中依赖Navigation:


implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'

创建导航视图


新建一个Android Resource File,类型选择Navigation即可,输入名称后我们就创建了一个导航视图。


在导航试图中,我们可以通过添加activity/fragment等标签手动添加页面,也支持在Design页面中通过界面添加,如下:


B88C2B4A-4900-41DF-9BDC-7972F73190D2.png


注意:这样添加后手动修改一下label。如果我们将Navigation与ToolBar连接,会在标题栏这个label。


示例中添加了两个页面,添加后代码如下:


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

<fragment
android:id="@+id/FirstFragment"
android:name="com.xxx.xxx.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.xxx.xxx.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
</fragment>
</navigation>

除了添加Fragment和Activity,Google还提供了一个占位符placeholder,添加加完代码如下:


<fragment android:id="@+id/placeholder" />

用于暂时占位以便后面可以替换为Fragment和Activity


添加完页面后,我们还需要添加页面之间的导航,可以手动添加action标签,当然也可以通过拖拽来实现,如下:


ABE13B79-D136-4450-A454-B4C905733284.png


这样我们就添加了一个从FirstFragment导航到SecondFragment的动作,我们再添加一个逆向的动作,最终的代码如下:


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

<fragment
android:id="@+id/FirstFragment"
android:name="com.xxx.xxx.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">

<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.xxx.xxx.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">

<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>

注意占位符placeholder同样支持添加导航。


这样就实现了两个页面间的导航,最后还需要为这个navigation设置id和默认页面startDestination,如下:


<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">

这样导航视图就创建完成了。可以看到Google力图通过可视化工具来简化开发工作,这对我们开发者来说非常有用,可以省去大量编写同质化代码的时间。


添加NavHost


下一步我们需要向Activity中添加导航宿主,导航宿主是一个空页面,必须实现NavHost接口,我们使用Navigation提供的默认NavHost————NavHostFragment即可。如下:


<fragment
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />

在Activity的视图中添加一个fragment标签,android:name设置为实现类,即NavHostFragment;app:navGraph设置为刚才新建的导航视图。


注意app:defaultNavHost="true",设置为true后表示将这个NavHostFragment设置为默认导航宿主,这样就会拦截系统的返回按钮事件。同一布局中如果有多个导航宿主(比如双窗口)则必须制定一个为默认的导航宿主。


这时候我们运行应用,就可以发现Activity中已经可以展示FirstFragment了。


导航


我们还需要为两个fragment添加按钮,是其点击跳转到另外一个页面,代码如下:


binding.buttonFirst.setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}

示例中是FirstFragment中的一个按钮,点击时执行了id为action_FirstFragment_to_SecondFragment的动作,这个是我们之前在导航视图中配置好的,会导航到SecondFragment。


注意首先通过findNavController()来获取一个NavController对象,然后调用它的navigate函数即可,当然这个函数有多种重载,比如可以传递参数,如下:


public void navigate(@IdRes int resId, @Nullable Bundle args) {

这里不一一列举了,大家自行查看源码即可。


可以看到使用Navigation代码精简了很多,只需要一行代码执行一个函数即可。


findNavController


我们重点来看看findNavController(),它是一个扩展函数,如下:


fun Fragment.findNavController(): NavController =
NavHostFragment.findNavController(this)

实际上是NavHostFragment的一个静态函数findNavController:


@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
...
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}

// For DialogFragments, look at the dialog's decor view
Dialog dialog = fragment instanceof DialogFragment
? ((DialogFragment) fragment).getDialog()
: null;
if (dialog != null && dialog.getWindow() != null) {
return Navigation.findNavController(dialog.getWindow().getDecorView());
}

throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}

通过源码可以看到最终是执行了Navigation的findNavController函数,它的代码如下:


@NonNull
public static NavController findNavController(@NonNull View view) {
NavController navController = findViewNavController(view);
...
return navController;
}

这里是通过findViewNavController函数来获取NavController的,它的代码如下:


@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
if (controller != null) {
return controller;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}

这里可以看到通过view来获取NavController,如果没有则向上层查找(父view)直到找到或到根结点。getViewNavController代码如下:


@Nullable
private static NavController getViewNavController(@NonNull View view) {
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference<NavController>) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}

看到这里获取view中key为R.id.nav_controller_view_tag的tag,这个tag就是NavController,那么这个tag又从哪来的?


其实就是上面我们提到导航宿主————NavHostFragment,在他的onViewCreated中可以看到如下代码:


@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
Navigation.setViewNavController(view, mNavController);
// When added programmatically, we need to set the NavController on the parent - i.e.,
// the View that has the ID matching this NavHostFragment.
if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}

这里的mNavController是在NavHostFragment的onCreate中创建出来的,是一个NavHostController对象,它继承NavController,所以就是NavController。


可以看到onViewCreated中调用了Navigation的setViewNavController函数,它的代码如下:


public static void setViewNavController(@NonNull View view,
@Nullable NavController controller) {
view.setTag(R.id.nav_controller_view_tag, controller);
}

这样就将NavController加入tag中了,通过findNavController()就可以得到这个NavController来执行导航了。


注意在onViewCreated中不仅为Fragment的View添加了tag,同时还为其父View也添加了,这样做的目的是在Activity中也可以获取到NavController,这点下面就会遇到。


ToolBar


Google提供了Navigation与ToolBar连接的功能,代码如下:


val navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)

上面我们提到,如果Navigation与ToolBar连接,标题栏会自动显示在导航视图中设定好的label。


注意这里的findNavController是Activity的扩展函数,它最终一样会调用Navigation的对应函数,所以与Fragment的流程是一样的。而上面我们提到了,在NavHostFragment中给上层View也设置了tag,所以在这里才能获取到NavController。


除了这个,我们还可以发现当在切换页面的时候,标题栏的返回按钮也会自动显示和隐藏。当导航到第二个页面SecondFragment,返回按钮显示;当回退到首页时,返回按钮隐藏。


但是此时返回按钮点击无效,因为我们还需要重写一个函数:


override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}

这样当点击标题栏的返回按钮时,会执行NavController的navigateUp函数,就会退回到上一页面。


总结


可以看出通过Google推出的这个Navigation,可以让开发者更加优雅管理导航,同时也简化了这部分的开发工作,可视化功能可以让开发者更直观的进行管理。除此之外,Google还提供了Safe Args Gradle插件,该插件可以生成简单的对象和构建器类,这些类支持在目的地之间进行类型安全的导航和参数传递。关于这个大家可以参考官方文档developer.android.google.cn/guide/navig… 即可。


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

Android将so库封装到jar包中并加载其中的so库

说明 因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。 1.如何把so库放入jar包 so库放入jar参考...
继续阅读 »

说明


因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。


1.如何把so库放入jar包


so库放入jar参考此文章ANDROID将SO库封装到JAR包中并加载其中的SO库
放置路径
将so库改成.jet后缀,放置和加载so库的SoLoader类同一个目录下面。



2.如何使用groovy打包jar


打包jar
先把需要打包的class放置到同一个文件夹下面,然后打包即可,利用groovy的copy task完成这项工作非常简单。


3.如何加载jar包里面的so


3.1.首先判断当前jar里面是否存在so

InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");

如果inputStream不为空就表示存在。


3.2.拷贝

判断是否已经把so库拷贝到手机里面了,如果没有拷贝过就进行拷贝,这个代码逻辑很简单。


public class SoLoader {
private static final String TAG = "SoLoader";

/**
* so库释放位置
*/
public static String getPath() {
String path = GlobalCtx.getApp().getFilesDir().getAbsolutePath();
//String path = GlobalCtx.getApp().getExternalFilesDir(null).getAbsolutePath();
return path;
}

public static String get64SoFilePath() {
String path = SoLoader.getPath();
String v8a = path + File.separator + "jniLibs" + File.separator +
"arm64-v8a" + File.separator + "liblogan.so";
return v8a;
}

public static String get32SoFilePath() {
String path = SoLoader.getPath();
String v7a = path + File.separator + "jniLibs" + File.separator +
"armeabi-v7a" + File.separator + "liblogan.so";
return v7a;
}

/**
* 支持两种模式,如果InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
* 返回了空,表示可能此库是aar接入的,普通加载so库就行,不为空,需要拷贝so库,动态加载
*/
public static boolean jarMode() {
boolean jarMode = false;
InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
if (inputStream != null) {
jarMode = true;
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return jarMode;
}

/**
* 是否已经拷贝过so了
*/
public static boolean alreadyCopySo() {
String v8a = SoLoader.get64SoFilePath();
File file = new File(v8a);
if (file.exists()) {
String v7a = SoLoader.get32SoFilePath();
file = new File(v7a);
return file.exists();
}
return false;
}

/**
* 拷贝logan的so库
*/
public static boolean copyLoganJni() {
boolean load;
File dir = new File(getPath(), "jniLibs");
if (!dir.exists()) {
load = dir.mkdirs();
if (!load) {
return false;
}
}
File subdir = new File(dir, "arm64-v8a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
File dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/arm64-v8a/liblogan.so", dest);
load = copySo("/com/dianping/logan/arm64-v8a/liblogan.jet", dest);
if (load) {
subdir = new File(dir, "armeabi-v7a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/armeabi-v7a/liblogan.so", dest);
load = copySo("/com/dianping/logan/armeabi-v7a/liblogan.jet", dest);
}
return load;
}

public static boolean copySo(String name, File dest) {
InputStream inputStream = SoLoader.class.getResourceAsStream(name);
if (inputStream == null) {
Log.e(TAG, "inputStream == null");
return false;
}
boolean result = false;
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(dest);
int i;
byte[] buf = new byte[1024 * 4];
while ((i = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, i);
}
result = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

}

3.3.加载

首先判断当前应用是32位还是64位Process.is64Bit();。然后加载对应的32或者64位的so。


static {
try {
if (SoLoader.jarMode()) {
if (SoLoader.alreadyCopySo()) {
sIsCloganOk = loadLocalSo();
} else {
boolean copyLoganJni = SoLoader.copyLoganJni();
if (copyLoganJni) {
sIsCloganOk = loadLocalSo();
}
}
} else {
System.loadLibrary(LIBRARY_NAME);
sIsCloganOk = true;
}
} catch (Throwable e) {
e.printStackTrace();
sIsCloganOk = false;
}
}

static boolean loadLocalSo() {
boolean bit = Process.is64Bit();
if (bit) {
String v8a = SoLoader.get64SoFilePath();
try {
System.load(v8a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
} else {
String v7a = SoLoader.get32SoFilePath();
try {
System.load(v7a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
}

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

[崩溃] Android应用自动重启

背景 在App开发过程中,我们经常需要自动重启的功能。比如: 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。 crash的时候,可以捕获到异常,直接自动重启应用。 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才...
继续阅读 »

背景


在App开发过程中,我们经常需要自动重启的功能。比如:



  • 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。

  • crash的时候,可以捕获到异常,直接自动重启应用。

  • 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才能生效,此时可以用自动重启,方便测试。


那我们如何实现自动重启的功能呢?我们都知道如何杀掉进程,但是当我们的进程被杀掉之后,如何唤醒呢?


这篇文章就来和大家介绍一下,实现应用自动重启的几种方法。


方法1 AlarmManager


    private void setAlarmManager(){
Intent intent = new Intent();
intent.setClass(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis()+100, pendingIntent);
Process.killProcess(Process.myPid());
System.exit(0);
}

使用AlarmManager实现自动重启的核心思想:创建一个100ms之后的Alarm任务,等Alarm任务到执行时间了,会自动唤醒App。


缺点:



  • 在App被杀和拉起之间,会显示系统Launcher桌面,体验不好。

  • 在高版本不适用


方法2 直接启动Activity


private void restartApp(){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Process.killProcess(Process.myPid());
System.exit(0);
}

缺点:



  • MainActivity必须是Standard模式


方法3 ProcessPhoenix


JakeWharton大神开源了一个叫ProcessPhoenix的库,这个库可以实现无缝重启app。


实现原理其实很简单,我们先讲怎么使用,然后再来分析源码。


使用方法


首先引入ProcessPhoenix库,这个库不需要初始化,可以直接使用。


implementation 'com.jakewharton:process-phoenix:2.1.2'

使用1:如果想重启app后进入首页:


ProcessPhoenix.triggerRebirth(context);

使用2:如果想重启app后进入特定的页面,则需要构造具体页面的intent,当做参数传入:


Intent nextIntent = //...
ProcessPhoenix.triggerRebirth(context, nextIntent);

有一点需要特别注意。



  • 我们通常会在ApplicationonCreate方法中做一系列初始化的操作。

  • 如果使用Phoenix库,需要在onCreate方法中判断,如果当前进程是Phoenix进程,则直接return,跳过初始化的操作。


if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}

源码


ProcessPhoenix的原理:



  • 当调用triggerRebirth方法的时候,会启动一个透明的Activity,这个Activity运行在:phoenix进程

  • Activity启动后,杀掉主进程,然后用:phoenix进程拉起主进程的Activity

  • 关闭当前Activity,杀掉:phoenix进程


先来看看ManifestActivity的注册代码:


 <activity
android:name=".ProcessPhoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:process=":phoenix"
android:exported="false"
/>

可以看到这个Activity确实是在:phoenix进程启动的,且是Translucent透明的。


整个ProcessPhoenix的代码只有不到120行,非常简单。我们来看下triggerRebirth做了什么。


  public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

不带intenttriggerRebirth,最后也会调用到带intenttriggerRebirth方法。


getRestartIntent会获取主进程的Launch Activity


  private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if (defaultIntent != null) {
return defaultIntent;
}
}

所以要调用不带intenttriggerRebirth,必须在当前Appmanifest里,指定Launch Activity,否则会抛出异常。


接着来看看真正的triggerRebirth方法:


  public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// 第一个activity添加new_task标记,重新开启一个新的stack
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
// 这里是为了防止传入的context非Activity
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
// 将待启动的intent作为参数,intent是parcelable的
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
// 将主进程的pid作为参数
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
// 启动ProcessPhoenix Activity
context.startActivity(intent);
}

triggerRebirth方法,主要的功能是启动ProcessPhoenix Activity,相当于启动了:phoenix进程。同时,会将nextIntents和主进程的pid作为参数,传给新启动的ProcessPhoenix Activity


下面我们再来看看,ProcessPhoenix ActivityonCreate方法,看看新进程启动后做了什么。


  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 首先杀死主进程
Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
// 再启动主进程的intents
startActivities(intents.toArray(new Intent[intents.size()]));
// 关闭当前Activity,杀掉当前进程
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

:phoenix进程主要做了以下事情:



  • 杀死主进程

  • 用传入的Intent启动主进程的Activity(也可以是Service)

  • 关闭phoenix Activity,杀掉phoenix进程


总结


如果App有自动重启的需求,比较推荐使用ProcessPhoenix的方法。


原理其实非常简单:



  • 启动一个新的进程

  • 杀掉主进程

  • 用新的进程,重新拉起主进程

  • 杀掉新的进程


我们可以直接在工程里引入ProcessPhoenix开源库,也可以自己用代码实现这样的机制,总之都比较简单。


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

Android:我是如何优化APP体积的

前言 在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自...
继续阅读 »

前言



在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自研的一些APP可经不住这些考验,所以能压缩就压缩,能优化就尽量优化,以得到用户最好的体验,下面就来说说我在项目中是如何优化APP体积的。



1. 本地资源优化


这里主要是压缩一些图片和视频。项目中本地资源用到最多的应该就是图片,几乎每个页面都离不开图标,甚至一些页面采用大图片的形式。你可知道,正常不经压缩的图片大的可以上大几十兆,小则也是一兆起步。这里做了个实验,同一个文件分别采用svg、png、使用tiny压缩后的png、webp四种类型图片进行展示(顺序是从左到右,从上到下):


image.png


可以看到,加载出来的效果几乎没有什么区别,但体积却有很大的差别(其中webp是采取的默认75%转换):


image.png


所以,别再使用png格式图片,太浪费资源了,就算经过压缩还是不及svg和webp,这里的webp其实还可以加大转换力度,但个人还是比较喜欢svg。


至于音视频文件也是可以通过其他工具进行压缩再放入本地,如非必要,尽量还是使用网络资源。


2. lib优化


一些三方库会使用底层so文件,一般在配置的时候我们尽量选择一种cpu类型,这里选择armeabi-v7a,其实几乎都兼容


ndk {
//设置支持的SO库架构 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64
abiFilters 'armeabi-v7a'
}

可以看看APK体积分析,每种cpu占用体积都比较大,少配置一种就能省下不少空间。
image.png


3. 代码混淆、无用资源的删除


在bulid.gradle中配置minifyEnabled true开启代码混淆,还需要配置混淆规则,否则无法找到目标类。shrinkResources true则是打包时不会将无用资源打入包内,这里有个小坑。之前使用腾讯地图时,某些第三方的静态资源会因为这个操作不被打入包内,导致无法找到资源,所以根据具体情况使用。


 release {
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
// shrinkResources true 慎用,可能会导致第三方资源文件找不到
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

4. 代码复用,剔除无用代码


项目中由于多人协同开发会出现各写各的情况,需要抽出一些公共库之类的工具,方便代码复用。一些注释掉的代码该删除就删除。其实这一部分优化的体积相当少,但也得做,也是对代码质量的一种提升。


总结


其实只要做到了以上四步,APP体积优化已经得到了很大程度的提升了,其他再怎么优化效果也不是很明显了,最主要的就是本地资源和第三方so包体积占用较多。图片的使用我们尽量做到:小图标用svg,全屏类的大图可以考虑webp,最好不要使用png。ndk配置最好只配置一款cpu,几乎都可兼容,万不得已再加一个。


以上便是全部内容,希望对大家有所帮助。


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

【自定义 View】Android 实现物理碰撞效果的徽章墙

前言 在还没有疫情的年代,外出经常会选择高铁,等高铁的时候我就喜欢打开 掌上高铁 的成就,签到领个徽章,顺便玩一下那个类似碰撞小球的徽章墙,当时我就在想这东西怎么实现的,但是吧,实在太懒了/doge,这几年都没尝试去自己实现过。最近有时间倒逼自己做了一些学习和...
继续阅读 »

前言


在还没有疫情的年代,外出经常会选择高铁,等高铁的时候我就喜欢打开 掌上高铁 的成就,签到领个徽章,顺便玩一下那个类似碰撞小球的徽章墙,当时我就在想这东西怎么实现的,但是吧,实在太懒了/doge,这几年都没尝试去自己实现过。最近有时间倒逼自己做了一些学习和尝试,就分享一下这种功能的实现。



不过,当我为写这篇文章做准备的时候,据不完全考古发现,似乎摩拜的 app 更早就实现了这个需求,但有没有更早的我就不知道了/doge



x762f-m3kvm.gif


其实呢,我想起来做这个尝试是我在一个 Android 自定义 View 合集的库 里看到了一个叫 PhysicsLayout 的库,当时我就虎躯一震,我心心念念的徽章墙不就是这个效果嘛,于是也就有了这篇文章。这个 PhysicsLayout 其实是借助 JBox2D 来实现的,但不妨先借助 PhysicsLayout 实现徽章墙,然后再来探索 PhysicsLayout 的实现方式。


实现




  1. 添加依赖,sync


    implementation("com.jawnnypoo:physicslayout:3.0.1")



  2. 在布局文件中添加PhysicsLinearLayout,并添加一个 子 Viewrun 起来


    这里我给ImageView设置 3 个Physic的属性



    • layout_shape设置模拟物理形状为圆形

    • layout_circleRadius设置圆形的半径为25dp

    • layout_restitution设置物体弹性的系数,范围为 [0,1],0 表示完全不反弹,1 表示完全反弹




  3. 看上去好像效果还行,我们再多加几个试试 子 View 试试





  4. 有下坠效果了,但是还不能随手机转动自由转动,在我阅读了 PhysicsLayout 之后发现其并未提供随陀螺仪自由晃动的方法,那我们自己加一个,在 MainActivityPhysicsLayout 添加一个扩展方法


        /**
    * 随手机的转动,施加相应的矢量
    * @param x x 轴方向的分量
    * @param y y 轴方向的分量
    */

    fun PhysicsLinearLayout.onSensorChanged(x: Float, y: Float) {
    for (i in 0..this.childCount) {

    Log.d(this.javaClass.simpleName, "input vec2 value : x $x, y $y")

    val impulse = Vec2(x, y)
    val view: View? = this.getChildAt(i)
    val body = view?.getTag(com.jawnnypoo.physicslayout.R.id.physics_layout_body_tag) as? Body
    body?.applyLinearImpulse(impulse, body.position)
    }
    }



  5. MainActivityonCreate() 中获取陀螺仪数据,并将陀螺仪数据设置给我们为 PhysicsLayout 扩展的方法,run


        val physicsLayout = findViewById(R.id.physics_layout)

    val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

    gyroSensor?.also { sensor ->
    sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
    event?.also {
    if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {

    Log.d(this@MainActivity.javaClass.simpleName, "sensor value : x ${event.values[0]}, y ${event.values[1]}")
    physicsLayout.onSensorChanged(-event.values[0], event.values[1])
    }
    }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
    }, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    动了,但是好像和预期的效果不太符合呀,而且也不符合用户直觉。




  6. 那不知道这时候大家是怎么处理问题的,我是先去看看这个库的 issue,搜索一下和 sensor 相关的提问,第二个就是关于如何让子 view 根据加速度计的数值进行移动,作者给出的答复是使用重力传感器,并在AboutActivity中给出了示例代码。


    那我们这里就换用重力传感器来试一试。


        val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)

    gyroSensor?.also { sensor ->
    sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
    event?.also {
    if (event.sensor.type == Sensor.TYPE_GRAVITY) {
    Log.d(this@MainActivity.javaClass.simpleName, "sensor value : x ${event.values[0]}, y ${event.values[1]}")
    physicsLayout.physics.setGravity(-event.values[0], event.values[1])
    }
    }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
    }, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    这下碰撞效果就正常了,但是好像会卡住不动啊!





  7. 不急,回到 issue,看第一个提问:物理效果会在子 view 停止移动后结束 和这里遇到的问题一样,看一下互动,有人提出是由于物理模拟引擎在物体移动停止后将物体休眠了。给出的修改方式是设置 bodyDef.allowSleep = false


    这个属性,是由 子 View 持有,所有现在需要获取 子 View 的实例并设置对应的属性,这里我就演示修改其中一个的方式,其他类似。


        findViewById(R.id.iv_physics_a).apply {
    if (layoutParams is PhysicsLayoutParams) {
    (layoutParams as PhysicsLayoutParams).config.bodyDef.allowSleep = false
    }
    }

    ···



  8. 到这里,这个需求基本就算实现了。




原理


看完了徽章墙的实现方式,我们再来看看 PhysicsLayout 是如何实现这种物理模拟效果的。




  1. 初看一下代码结构,可以说非常简单


    image.png




  2. 那我们先看一下我上面使用到的 PhysicsLinearLayout


    class PhysicsLinearLayout : LinearLayout {

    lateinit var physics: Physics

    constructor(context: Context) : super(context) {
    init(null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    init(attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    init(attrs)
    }

    @TargetApi(21)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
    init(attrs)
    }

    private fun init(attrs: AttributeSet?) {
    setWillNotDraw(false)
    physics = Physics(this, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    physics.onSizeChanged(w, h)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    super.onLayout(changed, l, t, r, b)
    physics.onLayout(changed)
    }

    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    physics.onDraw(canvas)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    return physics.onInterceptTouchEvent(ev)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return physics.onTouchEvent(event)
    }

    override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
    return LayoutParams(context, attrs)
    }

    class LayoutParams(c: Context, attrs: AttributeSet?) : LinearLayout.LayoutParams(c, attrs), PhysicsLayoutParams {
    override var config: PhysicsConfig = PhysicsLayoutParamsProcessor.process(c, attrs)
    }
    }

    主要有下面几个重点



    1. 首先是在构造函数创建了 Physics 实例

    2. 然后把 View 的绘制,位置,变化,点击事件的处理统统交给了 physics 去处理

    3. 最后由 PhysicsLayoutParamsProcessor 创建 PhysicsConfig 的实例




  3. 那我们先来看一下简单一点的 PhysicsLayoutParamsProcessor


    object PhysicsLayoutParamsProcessor {

    /**
    * 处理子 view 的属性
    *
    * @param c context
    * @param attrs attributes
    * @return the PhysicsConfig
    */

    fun process(c: Context, attrs: AttributeSet?): PhysicsConfig {
    val config = PhysicsConfig()
    val array = c.obtainStyledAttributes(attrs, R.styleable.Physics_Layout)
    processCustom(array, config)
    processBodyDef(array, config)
    processFixtureDef(array, config)
    array.recycle()
    return config
    }

    /**
    * 处理子 view 的形状属性
    */

    private fun processCustom(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_shape)) {
    val shape = when (array.getInt(R.styleable.Physics_Layout_layout_shape, 0)) {
    1 -> Shape.CIRCLE
    else -> Shape.RECTANGLE
    }
    config.shape = shape
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_circleRadius)) {
    val radius = array.getDimensionPixelSize(R.styleable.Physics_Layout_layout_circleRadius, -1)
    config.radius = radius.toFloat()
    }
    }

    /**
    * 处理子 view 的刚体属性
    * 1. 刚体类型
    * 2. 刚体是否可以旋转
    */

    private fun processBodyDef(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_bodyType)) {
    val type = array.getInt(R.styleable.Physics_Layout_layout_bodyType, BodyType.DYNAMIC.ordinal)
    config.bodyDef.type = BodyType.values()[type]
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_fixedRotation)) {
    val fixedRotation = array.getBoolean(R.styleable.Physics_Layout_layout_fixedRotation, false)
    config.bodyDef.fixedRotation = fixedRotation
    }
    }

    /**
    * 处理子 view 的刚体描述
    * 1. 刚体的摩擦系数
    * 2. 刚体的补偿系数
    * 3. 刚体的密度
    */

    private fun processFixtureDef(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_friction)) {
    val friction = array.getFloat(R.styleable.Physics_Layout_layout_friction, -1f)
    config.fixtureDef.friction = friction
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_restitution)) {
    val restitution = array.getFloat(R.styleable.Physics_Layout_layout_restitution, -1f)
    config.fixtureDef.restitution = restitution
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_density)) {
    val density = array.getFloat(R.styleable.Physics_Layout_layout_density, -1f)
    config.fixtureDef.density = density
    }
    }
    }

    这个类比较简单,就是一个常规的读取设置并创建一个对应的 PhysicsConfig 的属性




  4. 现在我们来看最关键的 Physics,这个类代码相对比较长,我就不完全贴出来了,一段一段的来分析



    1. 首先定义了一些伴生对象,主要是预设了几种重力值,模拟世界的边界尺寸,渲染帧率
      companion object {
      private val TAG = Physics::class.java.simpleName
      const val NO_GRAVITY = 0.0f
      const val MOON_GRAVITY = 1.6f
      const val EARTH_GRAVITY = 9.8f
      const val JUPITER_GRAVITY = 24.8f

      // Size in DP of the bounds (world walls) of the view
      private const val BOUND_SIZE_DP = 20
      private const val FRAME_RATE = 1 / 60f

      /**
      * 在创建 view 对应的刚体时,设置配置参数
      * 当布局已经被渲染之后改变 view 的配置需要调用 ViewGroup.requestLayout,刚体才能使用新的配置创建
      */

      fun setPhysicsConfig(view: View, config: PhysicsConfig?) {
      view.setTag(R.id.physics_layout_config_tag, config)
      }
      }


    2. 然后定义了很多的成员变量,这里挑几个重要的说一说吧
      /**
      * 模拟世界每一步渲染的计算速度,默认是 8
      */

      var velocityIterations = 8

      /**
      * 模拟世界每一步渲染的迭代速度,默认是 3
      */

      var positionIterations = 3

      /**
      * 模拟世界每一米对应多少个像素,可以用来调整模拟世界的大小
      */

      var pixelsPerMeter = 0f

      /**
      * 当前控制着 view 的物理状态的模拟世界
      */

      var world: World? = null
      private set


    3. init 方法中主要是读取一些 Physics 配置,另外初始化了一个拖拽手势处理的实例
      init {
      viewDragHelper = TranslationViewDragHelper.create(viewGroup, 1.0f, viewDragHelperCallback)

      density = viewGroup.resources.displayMetrics.density
      if (attrs != null) {
      val a = viewGroup.context
      .obtainStyledAttributes(attrs, R.styleable.Physics)
      ···
      a.recycle()
      }
      }


    4. 然后提供了一些物理长度,角度的换算方法

    5. onLayout 中创建了模拟世界,根据边界设置决定是否启用边界,设置碰撞处理回调,根据子 view 创建刚体
      private fun createWorld() {
      // Null out all the bodies
      val oldBodiesArray = ArrayList()
      for (i in 0 until viewGroup.childCount) {
      val body = viewGroup.getChildAt(i).getTag(R.id.physics_layout_body_tag) as? Body
      oldBodiesArray.add(body)
      viewGroup.getChildAt(i).setTag(R.id.physics_layout_body_tag, null)
      }
      bounds.clear()
      if (debugLog) {
      Log.d(TAG, "createWorld")
      }
      world = World(Vec2(gravityX, gravityY))
      world?.setContactListener(contactListener)
      if (hasBounds) {
      enableBounds()
      }
      for (i in 0 until viewGroup.childCount) {
      val body = createBody(viewGroup.getChildAt(i), oldBodiesArray[i])
      onBodyCreatedListener?.onBodyCreated(viewGroup.getChildAt(i), body)
      }
      }


    6. onInterceptTouchEventonTouchEvent 中处理手势事件,如果没有开启滑动拖拽,时间继续传递,如果开启了,则由 viewDragHelper 来处理手势事件。
      fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
      if (!isFlingEnabled) {
      return false
      }
      val action = ev.actionMasked
      if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      viewDragHelper.cancel()
      return false
      }
      return viewDragHelper.shouldInterceptTouchEvent(ev)
      }

      fun onTouchEvent(ev: MotionEvent): Boolean {
      if (!isFlingEnabled) {
      return false
      }
      viewDragHelper.processTouchEvent(ev)
      return true
      }


    7. onDraw 中绘制 view 的物理效果


      1. 先设置世界的物理配置


        val world = world
        if (!isPhysicsEnabled || world == null) {
        return
        }
        world.step(FRAME_RATE, velocityIterations, positionIterations)



      2. 遍历 子 view 并获取此前在创建刚体时设置的刚体对象,对于正在被拖拽的 view 将其移动到对应的位置


        translateBodyToView(body, view)
        view.rotation = radiansToDegrees(body.angle) % 360f



      3. 否则的话,设置 view 的物理位置,这里的 debugDraw 一直是 false 所以并不会走这段逻辑,且由于是私有属性,外部无法修改,似乎永远不会走这里


         view.x = metersToPixels(body.position.x) - view.width / 2f
        view.y = metersToPixels(body.position.y) - view.height / 2f
        view.rotation = radiansToDegrees(body.angle) % 360f
        if (debugDraw) {
        val config = view.getTag(R.id.physics_layout_config_tag) as PhysicsConfig
        when (config.shape) {
        Shape.RECTANGLE -> {
        canvas.drawRect(
        metersToPixels(body.position.x) - view.width / 2,
        metersToPixels(body.position.y) - view.height / 2,
        metersToPixels(body.position.x) + view.width / 2,
        metersToPixels(body.position.y) + view.height / 2,
        debugPaint
        )
        }
        Shape.CIRCLE -> {
        canvas.drawCircle(
        metersToPixels(body.position.x),
        metersToPixels(body.position.y),
        config.radius,
        debugPaint
        )
        }
        }
        }



      4. 最后提供了一个接口便于我们在需要的时候修改 JBox2D 处理 view 对应的刚体的物理状态


        onPhysicsProcessedListeners.forEach { it.onPhysicsProcessed(this, world) }




    8. 还有一个测试物理碰撞效果的随机碰撞方法
      fun giveRandomImpulse() {
      var body: Body?
      var impulse: Vec2
      val random = Random()
      for (i in 0 until viewGroup.childCount) {
      impulse = Vec2((random.nextInt(1000) - 1000).toFloat(), (random.nextInt(1000) - 1000).toFloat())
      body = viewGroup.getChildAt(i).getTag(R.id.physics_layout_body_tag) as? Body
      body?.applyLinearImpulse(impulse, body.position)
      }
      }





Bonus




  1. 在上面分析代码的时候,多次提到手势拖拽,那怎么实现这个手势的效果,目前好像对手是没反应嘛~


    其实也很简单,将 physicsisFlingEnabled 属性设置为 true 即可。


    val physicsLayout = findViewById(R.id.physics_layout).apply {
    physics.isFlingEnabled = true
    }




  2. 在浏览 PhysicsLayout issue 的时候还意外的发现已经有国人实现了 Compose 版本的
    JetpackComposePhysicsLayout


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

临近研三毕业的一些思考

秋招 2023届秋招可谓是诸神之战,算法岗、开发岗、数据分析岗等都是岗位紧缺,但却有一堆985高校毕业生来竞争。我海投了接近150家公司,最终收获了3个offer:一个上海创业公司(年薪43万元)、一个半国企(年薪25万元)以及一个真正的国企(年薪15-18万...
继续阅读 »

秋招


2023届秋招可谓是诸神之战,算法岗、开发岗、数据分析岗等都是岗位紧缺,但却有一堆985高校毕业生来竞争。我海投了接近150家公司,最终收获了3个offer:一个上海创业公司(年薪43万元)、一个半国企(年薪25万元)以及一个真正的国企(年薪15-18万元)。


在找工作方面,实力占据30%,而运气和机遇则占据了70%。即便是同一家公司,不同的面试官也有着不同的面试风格,因此会影响不同的结果。虽然运气很重要,但是实力永远都是基础。没有实力,就没有后续的好运气和机遇。


秋招的准备流程大体上包括以下几个步骤:刷题、背诵八股、准备项目、深入挖掘项目难点、准备简历、投递简历、面试复盘、简历修订以及谈薪水。每一个过程都十分重要,缺一不可。


春招


我已经签了一个半国企的offer,因此就没有再关注其他公司的信息。我基本上可以称为放弃了春招。一方面,如果我拿到了大厂的offer,我担心自己会承受不住试用期,或者会面临毕业的问题。另一方面,如果我拿到了银行的offer,我也不喜欢银行死板的工作氛围、轮岗的机制以及勾心斗角的同事关系。我参加过一次国考,但是基本上是裸考,完全没有复习。虽然在国家体制内工作很好,但是对于经济条件不好的家庭来说,去体制内不是最优的选择。


毕业季


在毕业前,我开始思考自己的第一份工作。实际上,我更多的是感到焦虑,因为放弃了年薪43万元的offer需要很大的勇气。我一直在思考如何弥补自己失去这份工作的损失。因此,我萌生了一些想法:


想法一:接私活


接外包项目对于程序员来说可能相对较为简单,因为这类项目通常技术含量较低,但是与此同时,与客户的沟通和协商则显得比较繁琐。但是,这种方式往往容易让人陷入代码的搬运中,短期收益是可观的,可以轻松获得外快薪资,但从长远来看,这种做法得不偿失。


想法二:转型产品经理


也许是看多了脉脉,认为程序员35岁危机是必然存在的,但是各行各业都存在淘汰现象。比较幼稚的想法是觉得产品经理生命周期更长,更稳定,在咨询相关学长后才知道,我才发现,原来产品经理的竞争比技术岗还要激烈,失业后重新获得一份工作的几率要比技术岗更低。


想法三:技术积淀,做自媒体


有些人会认为,做自媒体的目的就是要从中牟利,尽管这种想法在大部分人中很普遍。但实际上,成功的自媒体人通常是因为一开始就热爱分享或热爱自己所从事的行业,最终才获得了第二桶金。虽然技术积淀很重要,但如果抱着功利性的心态去做自媒体,那么注定会经历种种落差、打击以及困难。


如何破局?


思来想去,无法得知后续的解决方法,于是想到了《刻意练习》这本书,当我遇到一个无法解决问题的时候,我应该把我的问题和不成熟解决方案向教练反馈,让有经验的教练来指导。我联系了实验室优秀的师兄,师兄给出了很有建设性的意见:


意见一:技术是生存之道


进入公司,要多看技术文档,多看源码,理解原理,刚开始要多写、多思考。只有思考,才能成长起来。互联网是有35危机,但是大部分行业都有35危机,你有技能,就算被毕业,也会找到一个不错的工作。


意见二:要有全局思维


在接到一个需求时,我们不能简单地完成它,而应该深入思考这个需求为什么被提出,采用什么方式解决会更好。一个高价值的程序员不应该以编写的代码量为标准来衡量,而应该根据他在团队中不可或缺的地位来衡量。从了解业务到产品,从产品到自己负责的模块,我们应该对整个公司的架构有粗粒度和细粒度的理解。在未来,我们也将独当一面,成为负责人。因此,我们要为自己工作,而不是做一个单纯的打工仔。公司的转正述职报告是一个很好的机会,我们可以在这个机会中展示我们所学的一切,不仅限于我们负责的模块。


意见三:真诚的职场态度


互联网有嫡系的说法,那么为甚么会存在嫡系呢???存在即合理,肯定有背后的原因。假如,你有一个能力强、有主见、会沟通的的下属,你会不会青睐他?让他升职为你创造更多的业绩呢。答案是肯定的。师兄谈到了怎么样成为嫡系的方法,总结来说就是真诚的职场态度。第一,交代的任务要有责任感,能完成的不能完成的,都要带着结果和理由汇报,这是工作,不是随便搞一搞,那么怎么才能算是认真的完成呢?要知道任务背后的目的,任务背后蕴含的底层实现或者原理,这块任务交给你,你对这个任务的理解应该比谁都要透彻;第二,要主动汇报,可以两周或者一周就和上级交流自己的工作任务情况自己的难点等等,和优秀的人交流你才能进步。程序员的通病也许就是太沉迷于自己的代码世界,而忘记了和上级和其它业务部门交流,导致你做的和老板想要的根本不是一回事;第三,多和其它部门的人沟通,不要占用人家的工作时间,可以茶余饭后聊聊天,了解一下人家在做什么,沟通也是一个重要技能。


总结


师兄给的建议非常perfect,让我醍醐灌顶。未来的道路还很长,我想避免焦虑的最好办法,就是每天进步一点点,每个阶段都能朝着规划前进一步。最后,希望

作者:Javan Lu
来源:juejin.cn/post/7208359036273098811
顺利毕业,未来可期。

收起阅读 »

中年程序员写给36岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长 _,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_ 回顾  忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长


_,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_


回顾 


忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己过去一年的得与失,去年是写给35岁的自己,今年该+1了,还是随笔的方式,想到哪写到哪。


2022年1月30日的时候 ,给自己过去的工作做一次简单的总结,主要还是写给自己,就像现在的时候可以回过头去看,也收获了许多朋友的关注,回去看一年前写的文章,以及大家的留言反馈,深有感触。


回看去年的flag,首先需要检讨一下,基本上都没有完成,但是自己也朝着这个目标在做,收获也是有的:



读书


每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。


写文章


一周完成一篇原创文章,不限类别


早睡早起


每天不晚于11:30休息



关于读书


上半年的时候,自己也是有点焦虑和迷茫,想成长想进步,所以焦虑心情自然就会出现,所以看了一些鸡汤书籍,什么《被讨厌的勇气》、《程序员的自我修养》、《情商》等等。实话说看完之后,确实能够缓解缓解内心的焦虑情绪,但是这些书籍能给到自己的,更多是一些方式和方法,对于内心的空洞和不充实带来的焦虑是没办法缓解的。


所以还需要对症下药,我自己所感受到的空洞和不充实,很多是来自自己对技术知识技能的缺乏和退步,说白了就是作为技术人,不能把技术给弄丢了 ,同样也想不断的跟上时代的步伐。


想要快速解决这种“焦虑”,我需要快速的制定一个短期、中期、长期的目标,围绕着目标去充实自己的知识体系。这里所说的目标还是比较容易制定的,毕竟是关乎自己的成长,也就是自己接下来想要成为什么样的人,什么样的知识体系能够让自己在当前以及未来几年的工作中都有帮助。从这个方面去想,首先未来还是想从事前端,所以我给自己制定的短期目标是算法成长 、中期目标是计算机图形学方面的知识掌握、长期目标是成为一名地图领域的技术专家(ps:说到这里先立个flag,我后面想写一个小册,专门关于地图领域相关的,我也是比较期待自己能写出来什么样的小册,不为别的,就是想把自己的知识沉淀下来)。


讲讲为什么要这么去规划目标,算法算是现在任何技术面试都会涉及的,但是我不是为了面试去看,而是为了提升自己在团队内部的技术影响力,《算法图解》这本书写的简单好理解,作者的思路非常清晰 ,看完之后给团队内部的同学分享,不仅能提升自己,还能带动团队一起学习,一举多得。计算机图形学知识是目前工作中会碰到的,比如渲染、大数据可视化、自动驾驶等等都会涉及,这一部分不建议大家先去看书,没有一本书能够说明白,推荐大家去搜《闫令琪》,非常厉害的大佬,上班路上每天花半个小时-1小时足够了,一个月基本上能够学完,之后再运用到工作中,融会贯通。


单独再讲讲长远目标,我之前并不是搞地图方向的,但是近期这份工作有机会接触到了这方面的工作,让我又重新燃起了工作中的那种欲望,很久没有工作中的那种成就感,这也许是10年前才会有的那种热情,所以我比较坚信未来几年自己希望能够深入投入这个方向,不一定是地图,但一定是和这个方向相关的领域,因为知识都是想通的。


关于写文章


写文章这件事情,我非常佩服一位前同事,也或许是因为我没有做到 ,但是别人坚持每天每日的在做 ,他连续两年每天都能产出一篇原创,关于各个方面的,这是值得我学习的地方,今年争取突破自己。


关于早睡


头发卡卡掉,感觉都是因为没有按时睡觉引起的,还是在能有条件的时候,尽量早睡。


工作


今年的工作可以用“黑暗”、“光明”两个词来概括。


黑暗


2022年经历疫情最严重的一年,大部分时间都是居家办公状态,这也导致和同事们的交流变得很少,很多想要推进的工作变得没那么顺利,徒增了不少压力。


2022年也是“财源滚滚”的一年,看着同事一个个离开,也有不少同事询问工作机会,也确实给自己内心带来不小的冲击,同时危机感也很明显。


在一个地方工作一段时间之后,多少都会遇到各种各样的问题,技术上是最省心的问题,解决就好。有江湖的地方就会有各种复杂到不敢想的关系网,谁是谁的小弟,谁是谁的心腹、谁是大老板招来的等等,遇到这种问题我更多的是做好自己,但我更多还是更愿意沉浸在技术的知识中,享受解决问题带来的快感。面对频繁换老板,技术人的通病,不善于抱大腿,当然我也不想在这方便再去做过多改变或者违背内心去做一些事情,保持好内心的底线,不突破我的底线则相安无事。


光明


呵护好内心的明灯


今年工作最大的动力是来自于自身能力的成长,规划的短中长目标基本上都在按照正确的方向在行进,这也是在排除各种各样的干扰后带来的好的结果,也是抱着一种积极向上的心态在努力,工作中最让人糟心的,无非就是背锅、背指标、裁员,最坏的情况也就这样了,守好内心的方向,做自己想做的事情就对了,自己左右不了的事情不去想太多,行业不景气的时候,我基本上是以这种心态在工作,人生并不是只有工作。


人情往来


工作中


今年在和外部部门的合作当中,收获了许多的认可,也建立了许多新的人脉关系,这也是人生中比较宝贵的资源。与合作方合作共赢一直都是我做事的指导方法 ,提前思考好双方的目标和边界,剩下的就是努力合作完成目标了。相信他人,他人也会给予你同样的信任。


生活中


生活中的关系会比工作中的关系更加的牢靠,当然工作中的关系发展的好的话,也可以沉淀到生活中,而不是换个工作全没了,今年工作中积累的关系,确实是可以有这方面的转换的,这也是一种收获。


技术成长


我一直都不太赞成技术人转纯管理这个方向,管好人其实可以很简单,丑话在前,用心对待,以诚相待,能做好这三点感觉都不会有太大问题,但技术丢了就很难再捡起来了,切记切记。


今年反尝试不直接带团队,更多的是以技术顾问、专家视角,甚至是一线coding的方式在工作,看似管人但又不管人,所以在技术上成长也是非常快的,少了很多其他的琐事,能够更加投入。


渲染


第一次接触这个词的时候是在2021年,公司专门配了一个渲染团队做这个事情,用前端白话讲,就是能把各种各样的图像画到canvas上,一个好的渲染引擎可以呈现任何想要呈现的物体。


为了学习渲染是做什么的,怎么做,当时把简单的数学知识重新学习了一下,看闫令琪大佬的课,看openGL、webGPU等等相关的知识,过程是比较辛苦的,但收获也是很多的。现在再看一些框架就能够理解为什么代码会这么写了,比如Threejs、deckgl等等,我们自己也用c++实现了一套底层的跨端渲染框架,虽然不全面,但内部够用同时也能提升自身技术水平。


架构


架构能力是随着工作中不断积累起来能力,当然这也需要在工作中不断的打磨和锻炼,如果一直是以完成任务的心态在工作那是很难练出来的。我所推崇的架构能力是以解决业务问题为主,提升产研的效率为辅。所以在工作中不会刻意去做架构,而是围绕着如何解决问题去架构,如何才能控制好不至于过度设计。


举个简单例子,假如我们已经有各种完善的点餐业务,需要做一个邀请大家一起喝奶茶的这么一个功能,从业务上我们先考虑两个核心逻辑:


1、用户点餐之后回到邀请页面,点完的所以人实时能看到其他人下单状态
2、队长确认所有人点完之后,下单付款,所有人的页面切换到送餐状态

如果是快速实现这个功能的话,其实是比较简单的,起一个轮询任务实时问服务端要数据,拿到数据后,根据状态决定下一步显示什么状态的页面


但是随着业务发展,会加入很多奇怪的逻辑,比如要支持修改、删除、踢人等等,这就会导致这个页面逻辑及其的复杂起来,如果不去思考的话,很容易就写出一堆面条代码,最后自己都不愿意去改。


所以针对这个功能  ,我自己会抽象成几部分去思考:


1、store该如何拆解,拆成几个,每个store对应哪个组件
2、store该如何去更新
3、与服务端如何通信,websocket、轮询都可以,看当下实际情况,保证稳定性即可
4、可以写几个js类去做这个事情,每个类的职责是什么

我觉得思考完这几个问题 ,大家对于这个页面该怎么去写应该能有一个很清晰的架构图在脑海中了吧,这里我就不过多展开了 ,有兴趣的话私聊,核心是要说架构其实也可以很简单。


总结


今年就不立flag了,目标能把去年的flag实现好,2023年是疫情结束的一年 ,我认为这是一个好的开始,努力工作是一方面,享受生活我认为也同样重要,今年更需要做好工作和生活的平衡,工作以外能有一些其他的成就。


写给36岁的自己,简单地回顾过去、总结现在、展望未来,希望当37岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给37岁的自己》。


附上去年总结《写给35岁的自己》

trong>

收起阅读 »

Android 插件化:插件内部跳转

在Android 插件化(加载插件)中,简单的用一个demo 讲了如何加载一个插件,并使用插件里的资源。 那如果我们的插件中有多个页面呢,要怎么办? 其实,也是很简单,还是通过外部 PluginActivity 的 startActivity来实现 一、Lif...
继续阅读 »

在Android 插件化(加载插件)中,简单的用一个demo 讲了如何加载一个插件,并使用插件里的资源。


那如果我们的插件中有多个页面呢,要怎么办?

其实,也是很简单,还是通过外部 PluginActivitystartActivity来实现


一、LifeActivitystartActivity


LifeActivity 这个插件类中定义一个 startActivity 方法,用宿主的 context 调用 startActivity 方法


public void startActivity(Intent intent) {
if (context != null) {
if (intent==null||intent.getComponent()==null)return;
Intent newIntent=new Intent();
String className=intent.getComponent().getClassName();
if (TextUtils.isEmpty(className))return;
Log.e("startActivity","className="+className);
newIntent.putExtra("className",intent.getComponent().getClassName());
context.startActivity(newIntent);
}
}

而对于第一个插件中的页面 TestActivity


image.png


可以看到,插件中的第一个页面 TestActivity 点击打开 插件页面 Test2Activity 时。写法跟我们在Android中的风格是一模一样的。其中的 findViewById 等,只要是用到上下文的,全部替换成宿主的,这里不多赘述了。


image.png


二、重写 PluginActivitystartActivity


注意:由于 Test2Activity 不是一个真正 Activity ,PluginActivitystartActivity 中,就不能打开这个页面,只能再重新打开一个PluginActivity,并将Test2Activity 类的信息再重新加载实例化一次,跟我们第一个加载TestActivity 是一样的。

override fun startActivity(intent: Intent?) {
val className = intent?.getStringExtra("className")
if (className.isNullOrBlank()) return
val newIntent = Intent(this, PluginActivity::class.java)
newIntent.putExtra("className", className)
super.startActivity(newIntent)
}

传入进去的 className 就是 Test2Activity ,在PluginActivity 走生命周期onCreate 时,loadClassActivity ,至此就完成了插件内部的跳转,是不是非常简单。


9b2ee0aed19ba5e77698cb1f9582b93d.gif


三、同理,有 Activity 就会有其他组件


我们可以在插件中自己实现 serviceContentProviderBroadcastReceiver
等等组件,并重写生命周期等方法。原理都非常简单,难的是思想,这些都是插件化中的冰山一脚,我自己的项目中的更加复杂。


由于这样的方式,需要手动创建生命周期管理,和后续Activity启动模式,入栈出栈的管理等等。其实可以使用ASM等字节码来转换,将四大组件转成普通类,这样,开发过程中容易调试,插件生成也相对简单。


四、动态加载


由于插件apk 是可以从外部sdk 等地方加载的,给我们带来很多便利。而且插件部分的资源都是动态的,可以做到热更新的效果,只要我修改了再重新打包下发就行了。后续可以自己实现一套插件管理,用于加载外部apk ,做到热插拔的作用。


作者:大强Dev
来源:juejin.cn/post/7209971268483825722
收起阅读 »

[崩溃] Android应用自动重启

背景 在App开发过程中,我们经常需要自动重启的功能。比如: 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。 crash的时候,可以捕获到异常,直接自动重启应用。 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才...
继续阅读 »

背景


在App开发过程中,我们经常需要自动重启的功能。比如:



  • 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。

  • crash的时候,可以捕获到异常,直接自动重启应用。

  • 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才能生效,此时可以用自动重启,方便测试。


那我们如何实现自动重启的功能呢?我们都知道如何杀掉进程,但是当我们的进程被杀掉之后,如何唤醒呢?


这篇文章就来和大家介绍一下,实现应用自动重启的几种方法。


方法1 AlarmManager


    private void setAlarmManager(){
Intent intent = new Intent();
intent.setClass(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis()+100, pendingIntent);
Process.killProcess(Process.myPid());
System.exit(0);
}

使用AlarmManager实现自动重启的核心思想:创建一个100ms之后的Alarm任务,等Alarm任务到执行时间了,会自动唤醒App。


缺点:



  • 在App被杀和拉起之间,会显示系统Launcher桌面,体验不好。

  • 在高版本不适用


方法2 直接启动Activity


private void restartApp(){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Process.killProcess(Process.myPid());
System.exit(0);
}

缺点:



  • MainActivity必须是Standard模式


方法3 ProcessPhoenix


JakeWharton大神开源了一个叫ProcessPhoenix的库,这个库可以实现无缝重启app。


实现原理其实很简单,我们先讲怎么使用,然后再来分析源码。


使用方法


首先引入ProcessPhoenix库,这个库不需要初始化,可以直接使用。


implementation 'com.jakewharton:process-phoenix:2.1.2'

使用1:如果想重启app后进入首页:


ProcessPhoenix.triggerRebirth(context);

使用2:如果想重启app后进入特定的页面,则需要构造具体页面的intent,当做参数传入:


Intent nextIntent = //...
ProcessPhoenix.triggerRebirth(context, nextIntent);

有一点需要特别注意。



  • 我们通常会在ApplicationonCreate方法中做一系列初始化的操作。

  • 如果使用Phoenix库,需要在onCreate方法中判断,如果当前进程是Phoenix进程,则直接return,跳过初始化的操作。


if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}

源码


ProcessPhoenix的原理:



  • 当调用triggerRebirth方法的时候,会启动一个透明的Activity,这个Activity运行在:phoenix进程

  • Activity启动后,杀掉主进程,然后用:phoenix进程拉起主进程的Activity

  • 关闭当前Activity,杀掉:phoenix进程


先来看看ManifestActivity的注册代码:


 <activity
android:name=".ProcessPhoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:process=":phoenix"
android:exported="false"
/>


可以看到这个Activity确实是在:phoenix进程启动的,且是Translucent透明的。


整个ProcessPhoenix的代码只有不到120行,非常简单。我们来看下triggerRebirth做了什么。


  public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

不带intenttriggerRebirth,最后也会调用到带intenttriggerRebirth方法。


getRestartIntent会获取主进程的Launch Activity


  private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if (defaultIntent != null) {
return defaultIntent;
}
}

所以要调用不带intenttriggerRebirth,必须在当前Appmanifest里,指定Launch Activity,否则会抛出异常。


接着来看看真正的triggerRebirth方法:


  public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// 第一个activity添加new_task标记,重新开启一个新的stack
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
// 这里是为了防止传入的context非Activity
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
// 将待启动的intent作为参数,intent是parcelable的
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
// 将主进程的pid作为参数
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
// 启动ProcessPhoenix Activity
context.startActivity(intent);
}

triggerRebirth方法,主要的功能是启动ProcessPhoenix Activity,相当于启动了:phoenix进程。同时,会将nextIntents和主进程的pid作为参数,传给新启动的ProcessPhoenix Activity


下面我们再来看看,ProcessPhoenix ActivityonCreate方法,看看新进程启动后做了什么。


  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 首先杀死主进程
Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
// 再启动主进程的intents
startActivities(intents.toArray(new Intent[intents.size()]));
// 关闭当前Activity,杀掉当前进程
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

:phoenix进程主要做了以下事情:



  • 杀死主进程

  • 用传入的Intent启动主进程的Activity(也可以是Service)

  • 关闭phoenix Activity,杀掉phoenix进程


总结


如果App有自动重启的需求,比较推荐使用ProcessPhoenix的方法。


原理其实非常简单:



  • 启动一个新的进程

  • 杀掉主进程

  • 用新的进程,重新拉起主进程

  • 杀掉新的进程


我们可以直接在工程里引入ProcessPhoenix开源库,也可以自己用代码实现这样的机

作者:尹学姐
来源:juejin.cn/post/7207743145999024165
制,总之都比较简单。

收起阅读 »

抛弃trycatch,用go的思想去处理js异常

web
errors 错误处理在编程中是不可避免的一部分,在程序开发过程中,不可必要的会出现各种的错误,是人为也可能是失误,任何不可预料的可能都会发生 为了更好的保证程序的健壮性和稳定性 我们必须要对错误处理有更好的认识 最近迷上了golang的错误处理哲学,希望由浅...
继续阅读 »

errors


错误处理在编程中是不可避免的一部分,在程序开发过程中,不可必要的会出现各种的错误,是人为也可能是失误,任何不可预料的可能都会发生


为了更好的保证程序的健壮性和稳定性


我们必须要对错误处理有更好的认识


最近迷上了golang的错误处理哲学,希望由浅入深的总结一下自己的思考和看得见的思考


👀 用 error 表示可能出现的错误,用throw强制抛出错误


通常情况下 错误处理的方式无非不过两种



  • 泛处理

  • 精处理


其实都很好理解


// 伪代码

try {
const file = await readFile('../file')

const content = filterContent(file)

} catch (e) {
alert('xxxx',e.msg)
}

泛处理指的是对所有可能的错误都使用相同的处理方式,比如在代码中使用相同统一的错误提示信息


这种方式适用于一些简单的,不太可能发生的错误,不如文件不存在,网络连接超时等。


对于更加复杂的错误,应该使用精处理,即根据具体情况对不同类型的错误进行特定的处理



const [readErr,readFile] = await readFile('../file')

if (readErr) {
// 处理读取错误
return
}

const [filterErr,filterContent] = filterContent(readFile)

if (filterErr) {
// 处理过滤错误
return
}

精处理可以让我们把控计划的每一步,问题也很显然暴露了出来,过于麻烦


在实际开发当中,我们需要根据实际情况选择适当的方式进行错误处理。


由于本人是精处理分子,基于此开发了一个js版本的errors


如何定义一个错误


import { Errors } from '@memo28/utils'

Errors.News('err')

如何给一个错误分类


import { Errors } from '@memo28/utils'

Errors.News('err', { classify: 1 })

如何判断两个错误是相同类型


import { Errors } from '@memo28/utils'

Errors.As(Errors.News('err', { classify: 1 }),
Errors.News('err2', { classify: 1 })) // true

Errors.As(Errors.News('err', { classify: 1 }),
Errors.News('err2', { classify: 2 })) // false

一个错误包含了哪些信息?


Errors.New('as').info() // { msg: 'as' , classify: undefined }

Errors.New('as').trace() // 打印调用栈

Errors.New('as').unWrap() // 'as'

最佳实践


import { AnomalousChain, panicProcessing } from '@memo28/utils'

class A extends AnomalousChain {
// 定义错误处理函数,
// 当每次执行的被 panicProcessing 装饰器包装过的函数都会检查 是否存在 this.errors 是否为 ErrorsNewResult 类型
// 如果为 ErrorsNewResult 类型则 调用 panicProcessing的onError回调 和 skip函数
skip(errors: ErrorsNewResult | null): this {
console.log(errors?.info().msg)
return this
}

@panicProcessing()
addOne(): this {
console.log('run one')
super.setErrors(Errors.New('run one errors'))
return this
}

@panicProcessing({
// 在 skip 前执行
onError(error) {
console.log(error.unWrap())
},
// onRecover 从错误中恢复, 当返回的是true时 继续执行addTwo内逻辑,反之
// onRecover(erros) {
// return true
// },
})
addTwo(): this {
console.log('run two')
return this
}
}
new A().addOne().addTwo()



// output
run one
run one errors // in onError
run one errors // in skip fn

复制代码
作者:我开心比苦恼多
来源:juejin.cn/post/7207707775774031930
>
收起阅读 »

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

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

先抛几个数据:


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


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


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


image.png


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


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


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


image.png


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


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


image.png


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


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


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


image.png


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


image.png


超级计算机模拟风洞实验


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


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


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


推荐阅读:


# 计算型存储/存算一体如何实现? - bonnie的回答


# 5分钟新知关注芯片算力|存算一体为什么是AI时代主流计算架构?



作者:掘金安东尼
来源:juejin.cn/post/7210028417617395772
收起阅读 »

为什么是14px?

web
字号与体验 肉眼到物体之间的距离,物体的高度以及这个三角形的角度,构成了一个三角函数的关系。 h=2d*tan(a/2) 而公式中的 h 的值和我们要解决的核心问题『主字号』有着很大的关系。 关于这个 a 的角度,有机构和团队做过研究,当大于 0.3 度时的...
继续阅读 »

字号与体验


1609983492683.png


肉眼到物体之间的距离,物体的高度以及这个三角形的角度,构成了一个三角函数的关系。


h=2d*tan(a/2)


而公式中的 h 的值和我们要解决的核心问题『主字号』有着很大的关系。


关于这个 a 的角度,有机构和团队做过研究,当大于 0.3 度时的阅读效率是最好的。


同时我们在操作电脑时,一般来说眼睛距离电脑屏幕的平均值大概会在 50 厘米左右。


然而,公式中的距离和高度的单位都是厘米,字体的单位是 pixel。


因此我们还需要将二者之间做一轮转换,完成转换所需的两个数值分别是 2.54(cm 到 inch)和 PPI(inch 到 pixel)。           


*PPI(Pixels Per Inch):像素密度,所表示的是每英寸所拥有的像素数量。


公式表达为_ppi_=√(x2+y2)/z(x:长度像素数;y:宽度像素数;z:屏幕大小)  


我们假定 PPI 为 120。通过计算便可以得出在显示器的 PPI 为 120 的情况下,理论上大于 12px 的字体能够满足用户的最佳阅读效率。基于这样的思路,确定主流 PPI 的范围,就很容易锁定主字体的大小了。


根据网络上的数据来源,我们发现只有大约 37.6% 的显示器 PPI 是小于 120 的,而 PPI 在 120-140 的显示器的占比大约为 40%。换句话说 12px 的字体只能满足 37.6% 用户的阅读体验,但如果我们将字体放大到 14px,就可以保证大约 77% 的显示器用户处于比较好的阅读体验。


作者:IDuxFE
来源:juejin.cn/post/7209967260899147834
收起阅读 »

从零开始构建用户模块:前端开发实践

web
场景 在大多数前端应用中都会有自己的用户模块,对于前端应用中的用户模块来说,需要从多个方面功能考虑,以掘金为例,可能需要下面这些功能: 多种登录方式,账号密码,手机验证码,第三方登录等 展示类信息,用户头像、用户名、个人介绍等 用户权限控制,可能需要附带角色...
继续阅读 »

场景


在大多数前端应用中都会有自己的用户模块,对于前端应用中的用户模块来说,需要从多个方面功能考虑,以掘金为例,可能需要下面这些功能:



  1. 多种登录方式,账号密码,手机验证码,第三方登录等

  2. 展示类信息,用户头像、用户名、个人介绍等

  3. 用户权限控制,可能需要附带角色信息等

  4. 发起请求可能还需要带上token等


接下来我们来一步步实现一个简单的用户模块


需求分析


用户模型


针对这些需求我们可以列出一个用户模型,包括下面这些参数


展示信息:



  • username 用户名

  • avatar 头像

  • introduction 个人介绍


角色信息:



  • role


鉴权:




  • token




这个user模型对于前端应用来说应该是全局唯一的,我们这里可以用singleton,标注为全局单例


import { singleton } from '@clean-js/presenter';

@singleton()
export class User {
username = '';
avatar = '';
introduction = '';

role = 'member';
token = '';

init(data: Partial<Omit<User, 'init'>>) {
Object.assign(this, data);
}
}

用户服务


接着可以针对我们的用户场景来构建用户服务类。


如下面这个UserService:



  • 注入了全局单例的User

  • loginWithMobile 提供了手机号验证码登录方法,这里我们用一个mock代码来模拟请求登录

  • updateUserInfo 用来获取用户信息,如头像,用户名之类的。从后端拉取信息之后我们会更新单例User


import { injectable } from '@clean-js/presenter';
import { User } from '../entity/user';


@injectable()
export class UserService {
constructor(private user: User) {}


/**
* 手机号验证码登录
*/

loginWithMobile(mobile: string, code: string) {
// mock 请求接口登录
return new Promise((resolve) => {
setTimeout(() => {
this.user.init({
token: 'abcdefg',
});


resolve(true);
}, 1000);
});
}


updateUserInfo() {
// mock 请求接口登录
return new Promise<User>((resolve) => {
setTimeout(() => {
this.user.init({
avatar:
'https://p3-passport.byteimg.com/img/user-avatar/2245576e2112372252f4fbd62c7c9014~180x180.awebp',
introduction: '欢乐堡什么都有,唯独没有欢乐',
username: '鱼露',
role: 'member',
});


resolve(this.user);
}, 1000);
});
}
}

界面状态


我们以登录界面和个人中心页面为例


登录界面


在登录界面需要这些页面状态和方法


View State:



  • loading: boolean; 页面loading

  • mobile: string; 输入手机号

  • code: string; 输入验证码


methods:



  • showLoading

  • hideLoading

  • login


import { history } from 'umi';
import { UserService } from '@/module/user/service/user';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { injectable, Presenter } from '@clean-js/presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { Button, Form, Input, message, Space } from 'antd';

interface IViewState {
loading: boolean;
mobile: string;
code: string;
}
@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userService: UserService) {
super();
this.state = {
loading: false,
mobile: '',
code: '',
};
}

_loadingCount = 0;

showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}

hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}

login = () => {
const { mobile, code } = this.state;
this.showLoading();
return this.userService
.loginWithMobile(mobile, code)
.then((res) => {
if (res) {
message.success('登录成功');
}
})
.finally(() => {
this.hideLoading();
});
};
}

export default function LoginPage() {
const { p } = usePresenter(PagePresenter);

return (
<div>
<Form
name="normal_login"
initialValues={{ email: 'admin@admin.com', password: 'admin' }}
onFinish={() => {
console.log(p, '==p');
p.login().then(() => {
setTimeout(() => {
history.push('/profile');
}, 1000);
});
}}
>
<Form.Item
name="email"
rules={[{ required: true, message: 'Please input your email!' }]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder="email"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your Password!' }]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Password"
/>
</Form.Item>

<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
>
Log in
</Button>
</Space>
</Form.Item>
</Form>
</div>
);
}

如上代码所示,一个登录页面就完成了,接下来我们实现一下个人中心页面


个人中心


import { UserService } from '@/module/user/service/user';
import { injectable, Presenter } from '@clean-js/presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { Image, Spin } from 'antd';
import { useEffect } from 'react';

interface IViewState {
loading: boolean;
username: string;
avatar: string;
introduction: string;
}

@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userS: UserService) {
super();
this.state = {
loading: false,
username: '',
avatar: '',
introduction: '',
};
}

_loadingCount = 0;

showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}

hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}

/**
* 拉取用户信息
*/

getUserInfo() {
this.showLoading();
this.userS
.updateUserInfo()
.then((u) => {
this.setState((s) => {
s.avatar = u.avatar;
s.username = u.username;
s.introduction = u.introduction;
});
})
.finally(() => {
this.hideLoading();
});
}
}
const ProfilePage = () => {
const { p } = usePresenter(PagePresenter);

useEffect(() => {
p.getUserInfo();
}, []);

return (
<Spin spinning={p.state.loading}>
<p>
avatar: <Image src={p.state.avatar} width={100} alt="avatar"></Image>
</p>
<p>username: {p.state.username}</p>
<p>introduction: {p.state.introduction}</p>
</Spin>

);
};

export default ProfilePage;

在这个ProfilePage中,我们初始化时会执行p.getUserInfo();


期间会切换loading的状态,并映射到页面的Spin组件中,执行完成后,更新页面的用户信息,用于展示


总结


至此,一个简单的用户模块就实现啦,整个用户模块以及页面的依赖关系可以查看下面这个UML图,



状态库仓库

仓库


各位大佬,记得一键三连,给个star,谢谢



作者:鱼露
来源:juejin.cn/post/7208818303673679933
收起阅读 »

Android必知必会-Stetho调试工具

一、背景 Stetho是 Facebook 出品的一个强大的 Android 调试工具,使用该工具你可以在 Chrome Developer Tools查看APP的布局, 网络请求(仅限使用Volle, okhttp的网络请求库), Sqlite, Pref...
继续阅读 »

一、背景



Stetho是 Facebook 出品的一个强大的 Android 调试工具,使用该工具你可以在 Chrome Developer Tools查看APP的布局, 网络请求(仅限使用Volle, okhttp的网络请求库), Sqlite, Preference, 一切都是可视化的操作,无须自己在去使用adb, 也不需要root你的设备



本人使用自己的Nubia Z9 Mini作为调试机,由于牵涉到Sqlite数据库,所以尝试了很多办法把它Root了,然而Root之后就无法正常升级系统。
今天得知一调试神器Stetho,无需Root就能查看数据库以及APP的布局(这一点没有Android Device Monitor使用方便,但是Android Device Monitor在Mac上总是莫名其妙出问题),使用起来很方便,大家可以尝试一下。


二、配置流程


1.引入主库


使用Gradle方式:


// Gradle dependency on Stetho 
dependencies {
compile 'com.facebook.stetho:stetho:1.3.1'
}

此外还支持Maven方式,这里不做介绍。


2.引入网络请求库


如果需要调试网络且你使用的网络请求库是Volle或者Okhttp,那么你才需要配置,否则跳过此步。
以下根据自己使用的网络请求库情况来导入相应的库:
1.使用okhttp 2.X


 dependencies { 
compile 'com.facebook.stetho:stetho-okhttp:1.3.1'
}

2.使用okhttp 3.X


dependencies { 
compile 'com.facebook.stetho:stetho-okhttp3:1.3.1'
}

3.使用HttpURLConnection


dependencies { 
compile 'com.facebook.stetho:stetho-urlconnection:1.3.1'
}

3.配置代码


配置Application


public class XXX extends Application {
public void onCreate() {
super.onCreate();
Stetho.initializeWithDefaults(this);
}
}

配置网络请求库:
OkHttp 2.2.x+ 或 3.x


//方案一
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

//方案二
new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();

如果使用的是HttpURLConnection,请查阅相关文档。


4.使用


运行重新编译后的APP程序,保持手机与电脑的连接,然后打开Chrome浏览器,在地址栏里输入:chrome://inspect然后选择自己的设备下运行的APP进程名下的Inspect链接 即可进行调试。


三、遇到的问题


1.okhttp版本问题:


可能你还在使用okhttp 2.x的版本,在引入网络库的时候,你需要去查看一下Stetho当前版本使用的okhttp版本,避免在项目中使用多个不同版本的okhttp


PSokhttp2.x和3.x的引入方式略有不同,不可以直接修改版本号来导入:


//2.x
compile 'com.squareup.okhttp:okhttp:2.x.x'
//3.x
compile 'com.squareup.okhttp3:okhttp:3.x.x'

2.配置okhttp代码方案一报错:


//方案一
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

//方案二
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();

我在使用方案一进行配置okhttp的时候,会报错:


 Caused by: java.lang.UnsupportedOperationException

不知道是不是兼容的问题,大家在使用的时候请注意。



Stetho官网


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

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

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

问题背景


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


交流1


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


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


交流2


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


chatgpt1.jpg


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


Demo验证


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


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


<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">

<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</activity>
</application>

</manifest>

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


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

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

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

运行,得到的日志如下


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

查看当前应用所有进程


查看进程1


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


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


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


<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">

<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</activity>
<activity
android:name=".TwoActivity"
android:process=":remote2" />

<activity
android:name=".ThreeActivity"
android:process=":remote3" />

<activity
android:name=".FourActivity"
android:process=":remote4" />

<activity
android:name=".FiveActivity"
android:process=":remote5" />

</application>

</manifest>

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


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

查看当前应用所有进程


查看进程2


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


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


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

查看当前应用所有进程


查看进程3


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


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


zygote进程


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



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

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


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


jpush进程


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


源码分析


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


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


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

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

//省略部分代码

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

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

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

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


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

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

Application app = null;

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

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

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

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

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


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

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

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

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

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

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


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


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


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


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

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

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

运行后得到如下日志


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

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



  • 单进程,创建1个application对象,执行一次onCreate()方法

  • 多进程(N),创建N个application对象,执行N次onCreate()方法


作者:小迪vs同学
来源:juejin.cn/post/7208345469658415159
收起阅读 »

回顾下小城市十年的经历:努力过,躺平过。

【回顾】 十年,目睹了时代的改变,也见证了技术的迭代。 【2011】大学毕业前的实习 这一年,大学即将毕业毕业,寒假回到了家乡的小县城,歪打正着的找了一份家乡县级市ZF的实习工作(合同工)。 技术:Ps Html Css 内容:内网新闻专题 薪资:150...
继续阅读 »

【回顾】



十年,目睹了时代的改变,也见证了技术的迭代。



【2011】大学毕业前的实习


这一年,大学即将毕业毕业,寒假回到了家乡的小县城,歪打正着的找了一份家乡县级市ZF的实习工作(合同工)。



  • 技术:Ps Html Css

  • 内容:内网新闻专题

  • 薪资:1500

  • 房价:3000/平


【2012】大学毕业了


这一年,大学毕业了,毕业那天喝了很多,并豪情壮志的和室友预定了四个五年计划。(一五:年薪10万。二五,年薪20万,三五,年薪30万,四五,财富自由。)


于是我放弃了ZF的安稳,只身来到了当地的十八线小城市,为了安身立命,于是随便找了一家只有2人的公司做网站~



  • 职位:美工

  • 技术:Ps Html Css Asp

  • 内容:企业网站

  • 薪资:1000

  • 房租:600

  • 房价:3500/平


这段时间,由于工资过低,每天只吃1-2顿饭,而且每天熬夜学习,体重从大学期间的100Kg降到了65Kg。


终于,在2012年的下半年找到了自己的第一份正式工作--所在城市日报集团。



  • 职位:美工

  • 技术:Ps Html Css JQ PHP

  • 门户网站、专题、企业网站

  • 薪资:3500

  • 房租:0(包吃住)

  • 房价:3500/平


【2014】开始学H5+C3


由于不知道多少线的小城市,技术需求极低,大部分工作内容还要考虑IE6的兼容性。技术部老大跟我说H5和C3咱们一时半会是用不到的。但我还是学了,我觉得早晚会用到。



  • 职位:美工

  • 技术:Ps Html Css JQ PHP

  • 门户网站、专题、企业网站

  • 薪资:3800

  • 房租:0(包吃住)

  • 房价:3500/平


【2015】辞职-新工作-公司解散-新工作


公司来了新领导,开始大刀阔斧的改革,并要求技术部承担经营任务,技术部老大一气之下带着我和另外一个程序W 离职了,大家决定自己干!


老大带我们组个个外包团队,包了一个公司的H5封装APP的活,签了3年的外包合同。我负责H5页面,W负责程序,老大负责服务器。我们仨找了个孵化器开始干了起来。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ

  • H5封装APP

  • 薪资:7000

  • 房租:500

  • 房价:5500/平


三个月后甲方撤资了~~ 突如其来的大家没了收入。老大还在努力的找新单子,我们也发现这不是长久之计,于是边接小单子边找新的出路。


新工作来的很意外,之前报社的老领导去了临市的大众网记者站当副站长,又被调回我们城市成立新的记者站。于是老领导找到了我,他们需要一个全栈员工。老领导于我也算是有知遇之恩,所以我决定去帮帮她。虽然给了我主任级别的待遇,但是工资依然不高。



  • 职位:全栈

  • 技术:Ps Html5 Css3 JQ 内部cms

  • 门户网站、专题、企业网站 微信H5

  • 薪资:4000

  • 房租:500

  • 房价:5500/平


【2016】 微信小程序来了


这一年,微信小程序的风吹进了我们这座小城市,开始有人问我会不会做。于是开始学习,并成功为一个经典做了一个卖票小程序,赚了3000.



  • 职位:全栈

  • 技术:Ps Html5 Css3 JQ 内部cms

  • 门户网站、专题、企业网站 微信H5

  • 薪资:4500

  • 房租:500

  • 房价:6500/平


【2018】结婚-辞职-新工作


这一年,和相恋5年的女朋友结婚了。忽然之间有了责任的压力,看看工资卡的工资,现在的工作显然是不行了,于是我决定辞职。


当我决定辞职之后我收到了两个橄榄枝,分别是老东家报社,和老同事W的。


老东家的一个同事已经升为主任,邀请我回去,工资开到了7500,在一个报社这样的单位我知道这是她的极限了。


W则是在我们三人组解散之后去了一家小公司,老板是个富二代,人也很好。现在业务增多想要叫我过去。


最终我选择了W的公司



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:13000

  • 房租:0(买房了,朋友的顶账房,没贷款欠朋友的,慢慢还。)

  • 房价:8000/平


【2019】vue来了我却躺平了


这一年,老婆的公司开始使用VUE2了,老婆让我跟她一起学,我却沉迷游戏躺平了。


现在公司的环境很好,老板有钱人也很好。技术部的电脑都是i9-11代的处理器+2060的显卡,这哪里是公司简直是网吧。


公司也是经常几周甚至一个月没有新的业务,只要保证手里的几个网站正常运行就可以了,于是大家开始沉沦,每天上班也几乎都是在看电影和打游戏,还有免费的零食和饮料。


大家每天也都是在讨论哪个3A大作发售了,哪个电影上映了,很少能听到关于技术的讨论了。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:15000

  • 房价:9000/平


【2020】疫情来了,小天使来了,危机也在慢慢靠近。


这一年,疫情开始蔓延,小宝宝也降生了。


公司还是一如既往,虽然我们知道业务在下滑,但是老板有钱,工资不降反升。


老婆打算辞掉工作专心把孩子带到3岁再工作。我考量了一下我的收入,觉得在这个不知道多少线的小城也算可以,就同意了。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:17000

  • 房价:10000/平


【2021】终于开始学VUE和React


这一年,终于开始学习VUE和React,虽然公司依然没有使用新框架的打算,主要是因为后端不想写接口。想继续使用现在的程序。好像大家都躺平了,也可能是小城市的惬意。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:18000

  • 房价:9000/平


【2022-上】听说大厂裁员,我们老板“毕业”了。


这一年,听说大厂都在裁员,小城市却依然风平浪静。大家日复一日的摸鱼。


某一天,噩耗传来,老板进去了,公司解散。WTF。


晴天霹雳,没想到我们还没毕业,老板却“毕业”了



  • 职位:无

  • 薪资:0

  • 房价:9000/平


【2022-中】现实的社会和迟到的技术


2022精彩的一年,被狠狠的从舒适圈里踢了出来。一脸懵逼。


开始重新找工作却发现,自己的技术早已落伍,不得已又要从头学起。


无奈重新学了Vue2和Vue3,学了node.js,学了webpack,又学了ES6和TypeScript。


终于补完了前两年欠下的学习,却发现这个城市的前端工资普遍在6K-9K


落差感又一次让我一脸懵逼


【2022-下】焦虑和心虚


最终我进入了一家实体企业,HR看着我的简历对我十年的工作经验还算满意,我却很心虚。


总觉得自己这十年的工作经验水分很大。


看着比我年轻的同事熟练的使用着各种框架,而我却还在查着各种API。


我陷入了深深的自我怀疑,我是不是蹉跎了我的青春。



  • 职位:前端

  • 技术:Html5 Css3 Vue2

  • 内部平台系统、微信小程序

  • 薪资:9000

  • 房价:9000/平


【总结】



小城市没有那么多的技术需求,也没有那么多的996,大家朝九晚五,周末双休过得很惬意。



刚毕业那会用着Table切图用着Asp做后台,大家只知道你是做网站的,公司招人也是招美工,要求设计、切图、程序都会一些。小城市就是要求你什么都会一点,但不用很精通,够用就行。


慢慢的随着技术的发展前端才被定义出来,但是很多公司招聘的时候写着需要使用Vue或者React,但事实上还是干这设计切图的活,前后端不分家。


小城市技术渗透的慢,但是依然在渗透,四年的惬意生活,让我慢慢的忘记自己最初的梦想,惊醒的时候却发现我已经掉队了。


【未来可期】



被踢出舒适圈,也被踢醒了



想想大学时期的四个五年计划一个也没实现,还剩最后一个,或许还能抢救一下,虽然很不切实际,但是总有个盼头。


重新规划自己的未来,躺也躺过了,现在想想躺平的日子也就那样。虽然自己已经30+但是还是可以继续搏一搏的。


重新定制一个三年计划吧



  • 继续学习、阅读源码,做1-2个长期维护的项目。

  • 研究下混合基金,适量投入。

  • 看一些游戏开发相关的书,研究下Unreal引擎,争取做出一个游戏Demo。(游戏剧本已经写好了2个)。


作者:不知君
来源:juejin.cn/post/7143404185738805262
收起阅读 »

想退更多税?看完这篇就够了

前言 最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值...
继续阅读 »

前言


最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值变多。


image.png


专项附加扣除填报


首先打开个人所得税APP,点击下面的tabbar办税,然后点击专项附加扣除填报,就会出现如下图,通过填报上面的这些去进行增加专项附加扣除合计值。


0b957f5ecc7afa419053f890511d9aa.jpg


租房租金(和房贷只能二选一)



  • 要求:工作租房子了 (不需要合同也能填哦)

  • 扣除标准:省会或者直辖市 1500 / 月,二线城市 1100 /月,小城市 800 /月

  • 年总计:省会或者直辖市 18000,二线城市 13200,小城市 9600

  • 划重点: 多段租房经历可以多填几个租房,一定要填满!!!,一定要填,没租房子也可以填,随便填一个地址也行,这个不需要租房合同,也无法追查到的,但是和贷款只能二选一,具体选哪个请继续看。


image.png


住房贷款利息(和租房只能二选一)



  • 要求:首套房贷款

  • 扣除标准:1000/月

  • 年总计:12000

  • 划重点: 和租房二选一哦,具体选哪个看你所在的城市,哪个钱多选哪个~


image.png


子女教育



  • 要求:子女处于全日制学历教育阶段(幼儿园+小学+中学+大学),年龄>3岁。

  • 扣除标准:每人每月 1000 元 (两个孩子是一个月2000哦)

  • 年总计:12000(一个孩子的情况下)


image.png


继续教育



  • 要求:考证或者学历提升

  • 扣除标准:考证拿到证的当年一次性扣除:3600,学历提升 400/月

  • 年总计:考证:3600,学历提升:4800

  • 划重点: 这里的证必须是收录在 国家资格目录 的证书哦~


image.png


大病医疗


如果可以,我希望这个没有人申请,身体健康比什么都重要。



  • 要求:医保报销后个人花费15k+

  • 扣除标准:8w内根据你个人花费,花多少这个就是多少。

  • 年总计:根据实际。


image.png


赡养老人



  • 要求:父母 60 岁以上

  • 扣除标准:独生子女2000/月,非独生子女:和兄弟姐妹分摊 2000

  • 年总计:独生子女:24000,非独生子女: 0 ~ 12000


image.png


3岁以下婴幼儿照护



  • 要求:孩子 3 岁以下

  • 扣除标准:每月每人 1000 (两个孩子就是2000/月哦)

  • 年总计:12000(一个孩子的情况下)


如何能退更多的税


打开个人所得税APP → 我要查询 → 申报查询 → 已完成→ 选择当年的综合年度汇算→ 点击专项附加扣除合计,查看自己当年的专项附加扣除合计,可以简单计算下自己填报的专项附加扣除是否都已经填写。



PS:画重点!!!注意检查时间,我开始就是租房没填满,只写了一段租房经历,所以差点错过好几个亿,如果是2023年的填报,一定要注意你专项附加扣除是否涵盖整个2022年的日期。



113b48e415b80b8861261f3b3d961ab.jpg


有年终奖的情况


可能有些小伙伴公司会发年终奖 (如果和我一样没发年终奖,那这里可以跳过了,发年终奖的公司给我来一打),最好选择单独计税,不过都可以试一下,综合并入和单独合计,哪个退的多用哪个。


历史申报


以前的申报如果有没补充的,现在还是可以填哦,快去看看之前的申报,说不定会有惊喜哦。


最后


如果是需要补税的情况,如果少于400是不需要补的哦,当然如果大于400,一定要快点去缴纳哦,国家都留有档案,防止影响以后征信,哈哈哈,祝大家都能退几个亿~



作者:我亚索贼六丶
来源:juejin.cn/post/7208793045479522364
收起阅读 »

从技术到管理:我是如何教导技术总监做事的?

很多朋友都知道我技术很强,但实际上除了技术之外我的管理能力同样很出众。 虽然我年纪不大,但做了很多年实际的技术管理相关的工作。 前段时间李哥向我付费取经,他已经 30 多岁了,工作了很多年,技术也很强。但今年却刚当上公司的技术总监。原来他是技术部三把手,升职的...
继续阅读 »

很多朋友都知道我技术很强,但实际上除了技术之外我的管理能力同样很出众。


虽然我年纪不大,但做了很多年实际的技术管理相关的工作。


前段时间李哥向我付费取经,他已经 30 多岁了,工作了很多年,技术也很强。但今年却刚当上公司的技术总监。原来他是技术部三把手,升职的原因是前任技术总监带着副总监一起创业去了。


因为吸收了教训,老板就找到李哥,允诺给李哥一段时间的考察期,让李哥管理整个技术团队。通过考察期后会给李哥一些股份,让李哥成为技术合伙人。李哥在这家公司干了好几年了,技术能力有目共睹,确实不错,但管理经验确实比较缺乏。老板选择他,一个是出于信任,另一个也是想培养李哥。


现在李哥虽然管着几十号人,可他并没有多少实际管理经验。很多事情总觉得做得很吃力,力不从心。


所以我就和他分享了一些自己从过去的实际操作中获得的经验。


授权


第一点就是,怎么授权?


很多刚做技术管理者的人并不喜欢放权,李哥就是这类人。


这么做有几个原因。


第一个原因是怕下属能力强,功高盖主抢风头。


第二是原因是贪权,掌控欲强,什么事都要过问。


第三个原因是怕下属能力差,怕下属做砸了。


这样就会导致一个现象,很多人拿着很高的工资,担任很高的职位,却担负不了多少责任。


李哥公司每周二、周四是发布日。发布会由李哥主持,每次发布完都要等两三个小时的线上人工验证。发布完后,李哥还要担惊受怕,生怕线上出问题。


这种事不应该让李哥具体负责,因为即使哪个服务出现故障,李哥又不懂具体的业务,也做不了什么。


李哥手底下副总监、研发经理、组长十几号人,这事应该分派下去。


所有的事情都集中到了企业主要的几个负责人身上,很容易出事。


而底下的人很容易习惯这种作风,事事找领导,没有主见和主动性。


甚至于有一次李哥不在,负责的研发经理取消了发布日当天的发布计划,因为他怕发布出问题但责任。


权利高度集中,人人不作为的公司,一定会逐渐走向衰败。


所以,权利收得太紧,就会死。


那该怎么放权?


其实很简单。


员工职责内的事情要放,员工职责外的事情不要放。


就像一个员工请半天假去医院做核酸,先汇报组长,组长通过后汇报到研发经理,再汇报到技术副总监,再报备到人事专员,同时抄送人力总监和技术总监。


这么长的流程有什么用呢?


可以我直接让李哥把流程改成员工汇报给组长,组长同步给人事专员。


出事了,直接找组长,因为人是组长带的,组长要负责。


流成越复杂,就越容易扯皮甩锅撇清责任。最后背锅的估计就是李哥这个大领导。


发布的事情也是这样,谁负责的项目,什么时候发,发布过程可能出现的问题,发布失败的后果。都应该由他承担。


这样就能做到责任清晰、效率高、权利透明。


但这样不是没有隐患,组长可能不负责任,乱批假、乱报销等等。


所以权利放得太松,就容易乱。


那如何找到合适的、对工作负责的人担任组长呢?这又是一个新的问题。我后面再讲。


激励


第二点是,怎么激励?


很多新晋管理人很容易忽略激励这一点,其实它非常重要。


团队人气的高低会直接影响工作效率和工作质量。


给大家激励,让大家朝着一个方向努力,实现共同的目标很有意义和价值。虽然它不是很明显的投入,但隐形利益真的非常巨大。


那该如何激励呢?很简单,给员工争取自己能力范围内最大的福利。


简单粗暴的说,就是奖钱。


项目提前结束,给公司节省资源。


客户满意,按时付款,没有拖欠款项。


客户非常满意,计划签订下一个合作项目的合同。


这些都是给公司赚钱。


而这些赚到的钱再拿出一部分奖励给员工,本来就合情合理。


同时还可以形成良性循环。


但也不是每个老板都愿意付出这一部分钱。


有些情况下,团队加班加点、辛苦付出完成了项目。但老板就是不愿意给大家一些奖励怎么办?


精神奖励,虽然画饼差不多,但有效。


就像学生的奖状、小红花、三好学生、优秀标兵。


这是一种荣誉。


组织部门举行一场表彰会,定制个优秀员工奖牌、奖状。也花不了多少钱。


被表扬的人当真就当真,不当真也没什么。


这也谈不上忽悠人。做得好,被表扬,被夸奖,本来就是正常的事。


但是这种激励有时也能有意想不到的效果。


奖罚分明


第三点是,怎么奖罚分明?


激励对应的就是惩罚。


哪些事该奖?哪些事不该奖?


哪些事该罚?哪些事不该罚?


分不清楚这些事就很容易出事。


很多年前刚做管理不太懂事,因为迟到处罚问题失去了一个非常优秀的下属。


技术部门的工作职责其实很明确,他是生产型的岗位,按时保质保量完成任务就可以,很多事情都没必要管理的那么死。


比如李哥公司的制度是九七五,但前技术总监给大家培养了一个潜规则。就是九九五,即使很多人早就完成了工作也不能回家,不然第二个月指定被扣绩效。


按照公司规定,加班超过三个小时才有加班费。但这多出来的两个小时就等于白干了。


而且很多人这两个小时并不是真的在干活,而是在摸鱼、刷短视频,或者楼下抽烟。


所以我让李哥和老板商量,公司改成弹性制度。


按工作产出计算绩效,而不是工时。


李哥公司有个运行多年的老系统,负责处理订单和支付的。但由于年久失修,没人愿意去改。因为稍有不慎就可能导致公司金钱受损。


所以我就告诉李哥,这事必须得有人干。而且只奖不罚。


搞砸了也不能罚,甚至还得发些安慰奖。


为什么这么做?


因为这是个烂摊子,出力不讨好的事。安排人去做,不奖就等于罚了。


干好了没奖励,干不好还得被罚。谁愿意干呢?


难用就难用些,出了问题用脚本修复下数据。


这也是这个系统这么些年一直勉勉强强运行,而无人敢修改的原因。


难的事,没人敢做的事,一定不能拖着。付出大代价也得完成。


另外,李哥公司里面工时最长的几个员工会得到劳模奖和全勤奖。


但这里面有人比较混,仗着家里公司近,下班不打卡,回家吃个饭,溜个弯,到十一点多再来公司打个下班卡。而且这货不经常这么干,按照公司 OA 系统的工时,正好把自己工时卡在前三名。


影响很不好,但又不好说他。


我让李哥直接让技术部门改变奖励方式,还是按照绩效和产出发奖,同时对 Bug 最多、产出最低的员工进行处罚,即使他工时最长。


因为正常上下班只是员工的分内之事。


这种事情不该奖励。


总结来说,就是分内的事、简单的事,不能奖、只能罚。分外的事、难的事,只能奖、不能罚。


篇幅有限,先记录以上三点。


希望对你有所帮助。


作者:代码与野兽
来源:juejin.cn/post/7209914417641996343

未完待续。


收起阅读 »

Android 获取IP和UA

最近接入了一个新的SDK,初始化接口需要传入当前设备的IP和UA作为参数。本文介绍如何获取设备的IP和UA。 获取IP 使用WIFI联网与不使用WIFI,获取到的IP地址不同。因此,需要先判断当前设备通过哪种方式联网,然后再获取对应的IP地址。 判断网络连接...
继续阅读 »

最近接入了一个新的SDK,初始化接口需要传入当前设备的IP和UA作为参数。本文介绍如何获取设备的IP和UA。


获取IP


使用WIFI联网与不使用WIFI,获取到的IP地址不同。因此,需要先判断当前设备通过哪种方式联网,然后再获取对应的IP地址。



  • 判断网络连接类型


通过ConnectivityManager判断网络连接类型,代码如下:


private fun checkCurrentNetworkType() {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.run {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
when (activeNetworkInfo?.type) {
ConnectivityManager.TYPE_MOBILE -> {
// 通过手机流量
}
ConnectivityManager.TYPE_WIFI -> {
// 通过WIFI
}
else -> {}
}
} else {
// Android M 以上建议使用getNetworkCapabilities API
activeNetwork?.let { network ->
getNetworkCapabilities(network)?.let { networkCapabilities ->
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
// 通过手机流量
}
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
// 通过WIFI
}
}
}
}
}
}
}
}


  • 获取手机卡联网 IP


通过NetworkInterface获取IPV4地址,代码如下:


NetworkInterface.getNetworkInterfaces().let {
loo@ for (networkInterface in Collections.list(it)) {
for (inetAddresses in Collections.list(networkInterface.inetAddresses)) {
if (!inetAddresses.isLoopbackAddress && !inetAddresses.isLinkLocalAddress) {
// IP地址
val mobileIp = inetAddresses.hostAddress
break@loo
}
}
}
}


  • 获取WIFI联网 IP


通过ConnectivityManagerWifiManager来获取IP地址,代码如下:


private fun getWIFIIp() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
// IP 地址
val wifiIp = Formatter.formatIpAddress(wifiManager.connectionInfo.ipAddress)
} else {
// Android Q 以上建议使用getNetworkCapabilities API
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.run {
activeNetwork?.let { network ->
(getNetworkCapabilities(network)?.transportInfo as? WifiInfo)?.let { wifiInfo ->
// IP 地址
val wifiIp = Formatter.formatIpAddress(wifiInfo.ipAddress)
}
}
}
}
}

获取UA


获取设备的UserAgent比较简单,代码如下:


// 系统 UA
System.getProperty("http.agent")

// WebView UA
WebSettings.getDefaultUserAgent(context)

示例


在示例Demo中添加了相关的演示代码。


ExampleDemo github


ExampleDemo gitee


效果如图:


device-2023-03-12-09 -original-original.gif
作者:ChenYhong
来源:juejin.cn/post/7209272192852148282
收起阅读 »

玩转Canvas——给坤坤变个颜色

web
Canvas可以绘制出强大的效果,让我们给坤坤换个色。 先看看效果图: 要怎么实现这样一个可以点击换色的效果呢? 话不多说,入正题。 第一步,创建基本元素,无须多言: <body> <canvas></canvas&...
继续阅读 »

Canvas可以绘制出强大的效果,让我们给坤坤换个色。


先看看效果图:





要怎么实现这样一个可以点击换色的效果呢?
话不多说,入正题。


第一步,创建基本元素,无须多言:


<body>
<canvas></canvas>
</body>

我们先加载坤坤的图片,然后给canvas添加基础事件:


const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true
});
//加载图片并绘制
const img = new Image();
img.src = './img/cxk.png';
img.onload = () => {
cvs.width = img.width;
cvs.height = img.height;
ctx.drawImage(img, 0, 0);
};

再给canvas注册一个点击事件:


//监听canvas点击
cvs.addEventListener('click', clickCb);

function clickCb(e) {
const x = e.offsetX;
const y = e.offsetY;
}

这样就拿到了点击的坐标,接下来的问题是,我们要如何拿到点击坐标的颜色值呢?


其实,canvas早就给我们提供了一个强大的api:getImageData


我们可以通过它获取整个canvas上每个像素点的颜色信息,一个像素点对应四个值,分别为rgba


ctx.getImageData返回数据结构如下:





所以我们便可以利用它拿到点击坐标的颜色值:


function clickCb(e) {
//省略之前代码
//...

const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
//获取点击坐标的rgba信息
const clickRgba = getColor(x, y, imgData.data);
}

//通过坐标获取rgba数组
function getColor(x, y, data) {
const i = getIndex(x, y);
return [data[i], data[i + 1], data[i + 2], data[i + 3]];
}

//通过坐标x,y获取对应imgData数组的索引
function getIndex(x, y) {
return (y * cvs.width + x) * 4;
}

接下来便是在点击处绘制我们的颜色值:


//为了方便,这里将变色值写死为原谅绿
const colorRgba = [0, 255, 0, 255];

function clickCb(e) {
//省略之前代码
//...

//坐标变色
function changeColor(x, y, imgData) {
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
}
changeColor(x, y, imgData);
}

此时如果点击坤坤的头发,会发现头发上仅仅带一点绿。要如何才能绿得彻底呢?


我们新增一个判断rgba值变化幅度的方法getDeff,当两者颜色相差过大,则视为不同区域。


//简单地根据绝对值之和判断是否为同颜色区域
function getDiff(rgba1, rgba2) {
return (
Math.abs(rgba2[0] - rgba1[0]) +
Math.abs(rgba2[1] - rgba1[1]) +
Math.abs(rgba2[2] - rgba1[2]) +
Math.abs(rgba2[3] - rgba1[3])
);
}

再新增一个判断坐标是否需要变色的方法:


function clickCb(e) {
//省略之前代码
//...

//判断该坐标是否无需变色
function stopChange(x, y, imgData) {
if (x < 0 || y < 0 || x > cvs.width || y > cvs.height) {
//超出canvas边界
return true
}
const rgba = getColor(x, y, imgData.data);
if (getDiff(rgba, clickRgba) >= 100) {
//色值差距过大
return true;
}
if (getDiff(rgba, colorRgba) === 0) {
//同颜色,不用改
return true;
}
}
}

我们更改changeColor方法,接下来便可以绿得彻底了:


function clickCb(e) {
//省略之前代码
//...

//坐标变色
function changeColor(x, y, imgData) {
if (stopChange(x, y, imgData)) {
return
}
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
//递归变色
changeColor(x - 1, y, imgData);
changeColor(x + 1, y, imgData);
changeColor(x, y + 1, imgData);
changeColor(x, y - 1, imgData);
}

//省略之前代码
//...
}

效果已然实现。但是上面通过递归调用函数去变色,如果变色区域过大,可能会导致栈溢出报错。


为了解决这个问题,我们得改用循环实现了。


这一步的实现需要一定的想象力。读者可以自己试试,看看能不能改用循环方式实现出来。


鉴于循环实现的代码略多,这里不再解释,直接上最终代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
.color-box {
margin-bottom: 20px;
}
.canvas-box {
text-align: center;
}
</style>
</head>
<body>
<div class="color-box">设置色值: <input type="color" /></div>
<div class="canvas-box">
<canvas></canvas>
</div>
<script>
let color = '#00ff00';
let colorRgba = getRGBAColor();
//hex转rgba数组
function getRGBAColor() {
const rgb = [color.slice(1, 3), color.slice(3, 5), color.slice(5)].map((item) =>
parseInt(item, 16)
);
return [...rgb, 255];
}

const input = document.querySelector('input[type=color]');
input.value = color;
input.addEventListener('change', function (e) {
color = e.target.value;
colorRgba = getRGBAColor();
});

const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true,
});
cvs.addEventListener('click', clickCb);

const img = new Image();
img.src = './img/cxk.png';
img.onload = () => {
cvs.width = 240;
cvs.height = (cvs.width * img.height) / img.width;
//图片缩放
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, cvs.width, cvs.height);
};

function clickCb(e) {
let x = e.offsetX;
let y = e.offsetY;
const pointMark = {};

const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
const clickRgba = getColor(x, y, imgData.data);
//坐标变色
function changeColor(x, y, imgData) {
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
markChange(x, y);
}

//判断该坐标是否无需变色
function stopChange(x, y, imgData) {
const rgba = getColor(x, y, imgData.data);
if (getDiff(rgba, clickRgba) >= 150) {
//色值差距过大
return true;
}
if (getDiff(rgba, colorRgba) === 0) {
//同颜色,不用改
return true;
}
if (hasChange(x, y)) {
//已变色
return true;
}
}
function hasChange(x, y) {
const pointKey = `${x}-${y}`;
return pointMark[pointKey];
}
function markChange(x, y) {
const pointKey = `${x}-${y}`;
pointMark[pointKey] = true;
}
//添加上下左右方向的点到等待变色的点数组
function addSurroundingPoint(x, y) {
if (y > 0) {
addWaitPoint(`${x}-${y - 1}`);
}
if (y < cvs.height - 1) {
addWaitPoint(`${x}-${y + 1}`);
}
if (x > 0) {
addWaitPoint(`${x - 1}-${y}`);
}
if (x < cvs.width - 1) {
addWaitPoint(`${x + 1}-${y}`);
}
}
function addWaitPoint(key) {
waitPoint[key] = true;
}
function deleteWaitPoint(key) {
delete waitPoint[key];
}
//本轮等待变色的点
const waitPoint = {
[`${x}-${y}`]: true,
};
while (Object.keys(waitPoint).length) {
const pointList = Object.keys(waitPoint);
for (let i = 0; i < pointList.length; i++) {
const key = pointList[i];
const list = key.split('-');
const x1 = +list[0];
const y1 = +list[1];

if (stopChange(x1, y1, imgData)) {
deleteWaitPoint(key);
continue;
}
changeColor(x1, y1, imgData);
deleteWaitPoint(key);
addSurroundingPoint(x1, y1);
}
}
}

//通过坐标x,y获取对应imgData数组的索引
function getIndex(x, y) {
return (y * cvs.width + x) * 4;
}

//通过坐标获取rgba数组
function getColor(x, y, data) {
const i = getIndex(x, y);
return [data[i], data[i + 1], data[i + 2], data[i + 3]];
}

////简单地根据绝对值之和判断是否为同颜色区域
function getDiff(rgba1, rgba2) {
return (
Math.abs(rgba2[0] - rgba1[0]) +
Math.abs(rgba2[1] - rgba1[1]) +
Math.abs(rgba2[2] - rgba1[2]) +
Math.abs(rgba2[3] - rgba1[3])
);
}
</script>
</body>
</html>

若有疑问,欢迎评论区讨论。


完整demo地址:canvas-change-color


作者:TRIS
来源:juejin.cn/post/7209226686372937789
收起阅读 »

自从学会这几个写代码的技巧后,摸鱼时间又长了!!!

web
嘿!👋 今天,我们将介绍 5 个 JavaScript 自定义的实用函数,它们可以在您的大多数项目中派上用场。 目录 01.console.log() 02.querySelector() 03.addEventListener() 04.random() ...
继续阅读 »

嘿!👋


今天,我们将介绍 5 个 JavaScript 自定义的实用函数,它们可以在您的大多数项目中派上用场。


目录



  • 01.console.log()

  • 02.querySelector()

  • 03.addEventListener()

  • 04.random()

  • 05.times()




01.console.log()


在项目的调试阶段,我们都很喜欢用console.log()来打印我们的值、来进行调试。那么,我们可不可以缩短它,以减少我们的拼写方式,并且节省一些时间呢?


//把`log`从`console.log`中解构出来
const { log } = console;

//log等同于console.log

log("Hello world!");
// 输出: Hello world!

// 等同于 //

console.log("Hello world!");
// 输出: Hello world!

说明:


我们使用解构赋值logconsole.log中解构出来




02.querySelector()


在使用 JavaScript 时,当我们对DOM进行操作时经常会使用querySelector()来获取DOM元素,原生的获取DOM的操作过程写起来又长阅读性又差,那我们能不能让他使用起来更加简单,代码看起来更加简洁呢?


//把获取DOM元素的操作封装成一个函数
const select = (selector, scope = document) => {
return scope.querySelector(selector);
};

//使用
const title = select("h1");
const className = select(".class");
const message = select("#message", formElem);

// 等同于 //

const title = document.querySelector("h1");
const className = document.querySelector(".class");
const message = formElem.querySelector("#message");

说明:


我们在select()函数需要接收 2 个参数:



  • 第一个:您要选择的DOM元素

  • 第二:您访问该元素的范围(默认 = document);




03.addEventListener()


对click、mousemove等事件的处理大多是通过addEventListener()方法实现的。原生实现的方法写起来又长阅读性又差,那我们能不能让他使用起来更加简单,代码看起来更加简洁呢?


const listen = (target, event, callback, ...options) => {
return target.addEventListener(event, callback, ...options);
};

//监听buttonElem元素点击事件,点击按钮打印Clicked!
listen(buttonElem, "click", () => console.log("Clicked!"));

//监听document的鼠标移上事件,当鼠标移动到document上时打印Mouse over!
listen(document, "mouseover", () => console.log("Mouse over!"));

//监听formElem上的submit事件,当提交时打印Form submitted!
listen(formElem, "submit", () => {
console.log("Form submitted!");
}, { once: true }
);

说明:


我们在listen()函数需要接收 4 个参数:



  • 第一个:你要定位的元素(例如“窗口”、“文档”或特定的 DOM 元素)

  • 第二:事件类型(例如“点击”、“提交”、“DOMContentLoaded”等)

  • 第三:回调函数

  • 第 4 个:剩余的可选选项(例如“捕获”、“一次”等)。此外,如有必要,我们使用传播语法来允许其他选项。否则,它可以像在addEventListener方法中一样被省略。




04. random()


你可能知道Math.random()是可以随机生成从 0 到 1 的函数。例如Math.random() * 10,它可以生成从 0 到 10 的随机数。但是,问题是尽管我们知道生成数字的范围,我们还是无法控制随机数的具体范围的。那我们要怎么样才能控制我们生成随机数的具体范围呢?


const random = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};

random(5, 10);
// 5/6/7/8/9/10

这个例子返回了一个在指定值之间的随机数。这个值大于于 min(有可能等于),并且小于(有可能等于)max




05. times()


有时,我们经常会有运行某个特定功能或者函数的需求。应对这种需求,我们可以使用setInterval()每间隔一段时间运行一次:


setInterval(() => {
randomFunction();
}, 5000); // runs every 5 seconds

但是问题是setInterval()是无限循环的,当我们有限定运行次数要求时,我们无法指定它要运行它的次数。所以,让我们来解决它!


const times = (func, n) => {
Array.from(Array(n)).forEach(() => {
func();
});
};

times(() => {
randomFunction();
}, 3); // runs 3 times

解释:



  • Array(n)- 创建一个长度为n的数组.


Array(5); // => [,,]


  • Array.from()- 从创建一个浅拷贝的Array(n)数组。它可以帮助对数组进行操作,并用“undefined”填充数组里面的值。您也可以使用Array.prototype.fill()方法获得相同的结果。


Array.from(Array(3)); // => [undefined,undefined,undefined]


注意: 在封装这个函数时,我发现到有些程序员更喜欢先传参数n,再传函数times(n, func)。但是,我觉得有点奇怪,所以我决定交换它们的位置,从而使语法更类似于函数setInterval()



setInterval(func, delay);

times(func, n);

此外,您还可以可以使用setTimes()来代替times()。来代替setInterval()``setTimeout()的功能




写在最后


伙伴们,如果你觉得我写的文章对你有帮助就给zayyo点一个赞👍或者关注➕都是对我最大的支持。当然你也可以加我微信:IsZhangjianhao,邀你进我的前端学习交流群,一起学习前端,成为更优秀的工程

作者:zayyo
来源:juejin.cn/post/7209861267715817509
师~


收起阅读 »

vue为什么v-for的优先级比v-if的高?

web
前言 有时候有些面试中经常会问到v-for与v-if谁的优先级高,这里就通过分析源码去解答一下这个问题。 下面的内容是在 当我们谈及v-model,我们在讨论什么?的基础上分析的,所以阅读下面内容之前可先看这篇文章。 继续从编译出发 以下面的例子出发分析: n...
继续阅读 »

前言


有时候有些面试中经常会问到v-forv-if谁的优先级高,这里就通过分析源码去解答一下这个问题。


下面的内容是在 当我们谈及v-model,我们在讨论什么?的基础上分析的,所以阅读下面内容之前可先看这篇文章。


继续从编译出发


以下面的例子出发分析:


new Vue({
el:'#app',
template:`
<ul>
<li v-for="(item,index) in items" v-if="index!==0">
{{item}}
</li>
</ul>
`

})

从上篇文章可以知道,编译有三个步骤



  • parse : 解析模板字符串生成 AST语法树

  • optimize : 优化语法树,主要时标记静态节点,提高更新页面的性能

  • codegen : 生成js代码,主要是render函数和staticRenderFns函数


我们再次顺着这三个步骤对上述例子进行分析。


parse


parse过程中,会对模板使用大量的正则表达式去进行解析。开头的例子会被解析成以下AST节点:


// 其实ast有很多属性,我这里只展示涉及到分析的属性
ast = {
'type': 1,
'tag': 'ul',
'attrsList': [],
attrsMap: {},
'children': [{
'type': 1,
'tag': 'li',
'attrsList': [],
'attrsMap': {
'v-for': '(item,index) in data',
'v-if': 'index!==0'
},
// v-if解析出来的属性
'if': 'index!==0',
'ifConditions': [{
'exp': 'index!==0',
'block': // 指向el自身
}],
// v-for解析出来的属性
'for': 'items',
'alias': 'item',
'iterator1': 'index',

'parent': // 指向其父节点
'children': [
'type': 2,
'expression': '_s(item)'
'text': '{{item}}',
'tokens': [
{'@binding':'item'},
]
]
}]
}

对于v-for指令,除了记录在attrsMapattrsList,还会新增for(对应要遍历的对象或数组),aliasiterator1,iterator2对应v-for指令绑定内容中的第一,第二,第三个参数,开头的例子没有第三个参数,因此没有iterator2属性。


对于v-if指令,把v-if指令中绑定的内容取出放在if中,与此同时初始化ifConditions属性为数组,然后往里面存放对象:{exp,block},其中exp存放v-if指令中绑定的内容,block指向el


optimize 过程在此不做分析,因为本例子没有静态节点。


codegen


上一篇文章从const code = generate(ast, options)开始分析过其生成代码的过程,generate内部会调用genElement用来解析el,也就是AST语法树。我们来看一下genElement的源码:


export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}

if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
// 其实从此处可以初步知道为什么v-for优先级比v-if高,
// 因为解析ast树生成渲染函数代码时,会先解析ast树中涉及到v-for的属性
// 然后再解析ast树中涉及到v-if的属性
// 而且genFor在会把el.forProcessed置为true,防止重复解析v-for相关属性
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)

} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}

const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}

接下来依次看看genForgenIf的函数源码:


export function genFor (el, state , altGen, altHelper) {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` + //递归调用genElement
'})'
}

在我们的例子里,当他处理liast树时,会先调用genElement,处理到for属性时,此时forProcessed为虚值,此时调用genFor处理li树中的v-for相关的属性。然后再调用genElement处理li树,此时因为forProcessedgenFor中已被标记为true。因此genFor不会被执行,继而执行genIf处理与v-if相关的属性。


export function genIf (el,state,altGen,altEmpty) {
el.ifProcessed = true // avoid recursion
// 调用genIfConditions主要处理el.ifConditions属性
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (conditions, state, altGen, altEmpty) {
if (!conditions.length) {
return altEmpty || '_e()' // _e用于生成空VNode
}

const condition = conditions.shift()
if (condition.exp) { //condition.exp即v-if绑定值,例子中则为'index!==0'
// 生成一段带三目运算符的js代码字符串
return `(${condition.exp})?${ genTernaryExp(condition.block) }:${ genIfConditions(conditions, state, altGen, altEmpty) }`
} else {
return `${genTernaryExp(condition.block)}`
}

// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}

参考 前端进阶面试题详细解答


最后,经过codegen生成的js代码如下:


function render() {
with(this) {
return _c('ul', _l((items), function (item, index) {
return (index !== 0) ? _c('li') : _e()
}), 0)
}
}

其中:



  1. _c: 调用 createElement 去创建 VNode

  2. _l: renderList函数,主要用来渲染列表

  3. _e: createEmptyVNode函数,主要用来创建空VNode


总结


为什么v-for的优先级比v-if的高?总结来说是编译有三个过程,parse->optimize->codegen。在codegen过程中,会先解析AST树中的与v-for相关的属性,再解析与v-if相关的属性。除此之外,也可以知道Vuev-forv-if

作者:bb_xiaxia1998
来源:juejin.cn/post/7209950095402582072
de>是怎么处理的。

收起阅读 »

你代码的异味是故意的还是不小心?是故意的!

一、代码竟会有“气味” 食物在腐烂之际,会散发出异味,提醒人食物已经坏掉了,需要处理。同样,如果代码中某处出现了问题,也会有一些症状。这些症状,被称之为“代码异味”(Code smell,也译作“代码味道”)。与食物腐败发出的味道不同的是,代码异味并非真正的气...
继续阅读 »

一、代码竟会有“气味”


食物在腐烂之际,会散发出异味,提醒人食物已经坏掉了,需要处理。同样,如果代码中某处出现了问题,也会有一些症状。这些症状,被称之为“代码异味”(Code smell,也译作“代码味道”)。与食物腐败发出的味道不同的是,代码异味并非真正的气味,而是一种“暗示”,暗示我们代码可能有问题,提示程序员需要对项目设计进行更进一步的查看。


代码异味一词最初是由Kent Beck在帮助Martin Fowler在编写《重构:改善既有代码的设计》一书时创造的。Martin Fowler对代码异味的定义是:代码异味是一种表象,它通常对应于系统中更深层次的问题。


代码异味的产生原因跟厨师的“清洗过程中故意保留”不一样,它更多地并非刻意为之,创造者也未必“品尝”过自己所写的代码,它更多地是由于设计缺陷或不良编码习惯而导致的不良代码症状。


这种异味也并非来自一种有据可查的标准,更多的是来自程序员的直觉。尤其是经验丰富和知识渊博的程序员,他们无需思考,只要通过查看代码或一段设计就可以立马对这个代码质量产生这种“感觉”,能对代码设计的优劣有一个大致的判断。这有点类似我们英语学到一定程度后,即便不能完全看懂文章,但凭借语感也能选出正确答案。


二、 代码异味的影响


对于代码异味的出现我们其实无需过度紧张,因为在整个程序中代码异味是无处不在的。


一般情况下,有“异味”的代码也依旧能运行得很好。只是倘若重视不够,没有适当地维护或改进代码,代码质量就会下降,系统也会开始变得难以维护和扩展,同时也会增加技术债务。这就像做出有异味的九转大肠的的小胖厨师,在前期准备中对评委的建议置若罔闻,一意孤行,做出来的菜连自己都难以下咽。


所以团队应尽可能地做有质量的代码,减少甚至避免这些问题,产生高效益的成果。


三、 如何辨别代码异味



代码是否存在代码异味,通常是靠程序员的主观判断,但由于语言、开发者、开发理论的不同,对代码异味的判断也会存在差异。


所以要想更精准地识别代码异味,获得更高的代码质量,程序员需要大量的实践和经验。不过,前辈们总结的经验也可以让我们少走一些弯路。Martin Fowler在《重构:改善既有代码的设计》一书中,列举了最常见的24种代码异味,可以帮助我们轻松识别,便于处理和改善它们:


1) 过大的类(Large Class)


一个类包含许多字段、方法或者代码行,并逐渐变得臃肿。


2) 数据泥团(Data Clumps)


代码的不同部分包含了相同的变量组,且这些数据总是绑在一起出现。


3) 过长参数列表(Long Parameter List)


指一个方法的参数超过了三个或四个。出现这种情况一般是将几种类型的算法合并到一个方法之后。


4) 基本类型偏执(Primitive Obsession)


创建一个原始字段比创建一个全新的类要容易得多,所以对于具有意义的业务概念如钱、坐标、范围等,很多程序员不愿意进行建模,而是使用基本数据类型进行表示,进而导致代码内聚性差、可读性差。


5) 神秘命名(Mysterious Name)


在编程中,命名是一件非常恼人的事情。一些可能只有自己看懂的命名,无疑加大了代码可读性的难度,有时甚至自己也会忘记这些命名的含义。


6) 重复代码(Duplicated Code)


这几乎是最常见的异味。当多个程序员同时处理同一程序的不同部分时,通常会发生这种情况。


7) 过长的函数(Long Function)


根据Martin Fowler的经验,通常活得最长、最好的程序,其中的函数都比较短。函数越长,就越难理解。


8) 全局数据(Global Data)


这是一个非常可怕且刺鼻的异味代码。因为从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底是哪段代码做出了修改。全局数据造成一次又一次的诡异Bug,让我们很难找出出错的代码。


9) 可变数据(Mutable Data)


如果可变数据的变量的作用域越大, 越容易出现问题。变量是可以更改的,但我们可能不知道是哪里改变了它。


10) 发散式变化(Divergent Change)


是指一个类受到多种变化的影响。


11) 霰弹式修改(Shotgun Surgery)


是指一种变化引发多个类相应修改。


12) 依恋情结(Feature Envy)


一个类使用另一个类的内部字段和方法的数据多于它自己的数据。


13) 重复的switch(Repeated Switch)


在不同的地方反复使用switch逻辑。这带来的问题就是当我们想要增加一个选择分支时,就必须找到所有的switch,并逐一更新。


14) 循环语句(Loops)


在编程语言中,循环一直是程序设计的核心要素。在《重构》中,Martin Fowler认为它是一种代码异味,因为他们觉得如今的循环已经有点过时了。他们提出“以管道取代循环”,这样可以帮助我们更快看清被处理的元素以及处理它们的动作。


15) 冗赘的元素(Lazy Element)


这是几乎无用的组件。我们在设计代码时有时为了未来的功能设计出“预备”代码,但实际上从未实现;又或者这个类本来有用但随着重构,越来越小,最后只剩下一个函数。无论哪种,它们都是冗赘无用的。


16) 推测的通用性(Speculative Generality)


是指为了“以防万一”,支持预期的未来功能,但这些功能并未被实现,这些类、方法、字段或参数也从未被使用,结果导致代码变得难以理解和支持。


17) 临时字段(Temporary Field)


创建临时字段以用于需要大量输入的算法。但这些字段仅在算法中使用,其余时间不使用。


18) 过长的消息链(Message Chains)


当客户端请求另一个对象,该对象又请求另一个对象,依此类推时,就会出现过长的消息链。这些链意味着客户端依赖于类结构的导航。一旦发生更改,客户端也要跟着修改。


19) 中间人(Middle Man)


指一个类只执行一个动作,但将工作委托给另一个类,这种委托属于过度委托。该类也可能只是一个空壳,只负责委托且只有一件事。


20) 内幕交易(Insider Trading)


指模块之间大量地交换数据,增加模块之间的耦合。


21) 异曲同工的类(Alternative Classes with Different Interface)


是指两个类执行了相同的功能但具有不同的方法名称。


22) 纯数据类(Data Class)


指包含字段和访问它们的粗略方法(getter 和 setter)的类。这些只是其他类使用的数据容器。这些类不包含任何附加功能,并且不能独立操作它们拥有的数据。


23) 被拒绝的遗赠(Refused Bequest)


指如果子类复用了超类的行为,但又不愿意支持超类的接口的情况。


24) 注释(Comments)


程序员将其作为一种“除臭剂”使用情况下的行为。比如:一段代码有着长长的注释,但这段长注释的存在是因为代码很糟糕。


四、 如何对代码“除臭”



1)重构


上述代码异味没有优先级一说,所以对于程序员而言,只能依靠直觉和经验去决定是否需要重构。


重构,一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码。是实现敏捷性的最重要的技术因素之一。是程序员根据已识别出的气味然后将代码分成更小的部分的过程,再决定要么删除它们,要么用更好的代码替换它们,如此循环重复这个过程,直到异味消失,这样可能会提高代码质量并让代码变得更具简单性、灵活性和可理解性。


2)使用代码检测工具



识别和消除代码异味是一个令人厌烦且不确定的过程,而且也不可能手动查找到和删除掉所有异味,尤其是面对一个有着上千行异味的代码的时候。所以使用一些代码检测工具可以辅助我们进行快速大量地审查,帮助我们节约时间来做更为重要的工作,比如能专注于代码高层面的设计原则问题。


好了,关于代码异味的知识,算是讲了个清楚,那么让我们相约下一次代码评审吧!



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

码农如何提高自己的品味

前言 软件研发工程师俗称程序员经常对业界外的人自谦作码农,一来给自己不菲的收入找个不错的说辞(像农民伯伯那样辛勤耕耘挣来的血汗钱),二来也是自嘲这个行业确实辛苦,辛苦得没时间捯饬,甚至没有驼背、脱发加持都说不过去。不过时间久了,行外人还真就相信了程序员就是一帮...
继续阅读 »

前言


软件研发工程师俗称程序员经常对业界外的人自谦作码农,一来给自己不菲的收入找个不错的说辞(像农民伯伯那样辛勤耕耘挣来的血汗钱),二来也是自嘲这个行业确实辛苦,辛苦得没时间捯饬,甚至没有驼背、脱发加持都说不过去。不过时间久了,行外人还真就相信了程序员就是一帮没品味,木讷的low货,大部分的文艺作品中也都是这么表现程序员的。可是我今天要说一下我的感受,编程是个艺术活,程序员是最聪明的一群人,我们的品味也可以像艺术家一样。


言归正转,你是不是以为我今天要教你穿搭?不不不,这依然是一篇技术文章,想学穿搭女士学陈舒婷(《狂飙》中的大嫂),男士找陈舒婷那样的女朋友就好了。笔者今天教你怎样有“品味”的写代码。



以下几点可提升“品味”


说明:以下是笔者的经验之谈具有部分主观性,不赞同的欢迎拍砖,要想体系化提升编码功底建议读《XX公司Java编码规范》、《Effective Java》、《代码整洁之道》。以下几点部分具有通用性,部分仅限于java语言,其它语言的同学绕过即可。


优雅防重


关于成体系的防重讲解,笔者之后打算写一篇文章介绍,今天只讲一种优雅的方式:


如果你的业务场景满足以下两个条件:


1 业务接口重复调用的概率不是很高


2 入参有明确业务主键如:订单ID,商品ID,文章ID,运单ID等


在这种场景下,非常适合乐观防重,思路就是代码处理不主动做防重,只在监测到重复提交后做相应处理。


如何监测到重复提交呢?MySQL唯一索引 + org.springframework.dao.DuplicateKeyException


代码如下:


public int createContent(ContentOverviewEntity contentEntity) {
try{
return contentOverviewRepository.createContent(contentEntity);
}catch (DuplicateKeyException dke){
log.warn("repeat content:{}",contentEntity.toString());
}
return 0;
}

用好lambda表达式


lambda表达式已经是一个老生常谈的话题了,笔者认为,初级程序员向中级进阶的必经之路就是攻克lambda表达式,lambda表达式和面向对象编程是两个编程理念,《架构整洁之道》里曾提到有三种编程范式,结构化编程(面向过程编程)、面向对象编程、函数式编程。初次接触lambda表达式肯定特别不适应,但如果熟悉以后你将打开一个编程方式的新思路。本文不讲lambda,只讲如下例子:


比如你想把一个二维表数据进行分组,可采用以下一行代码实现


List<ActionAggregation> actAggs = ....
Map<String, List<ActionAggregation>> collect =
actAggs.stream()
.collect(Collectors.groupingBy(ActionAggregation :: containWoNosStr,LinkedHashMap::new,Collectors.toList()));

用好卫语句


各个大场的JAVA编程规范里基本都有这条建议,但我见过的代码里,把它用好的不多,卫语句对提升代码的可维护性有着很大的作用,想像一下,在一个10层if 缩进的接口里找代码逻辑是一件多么痛苦的事情,有人说,哪有10层的缩进啊,别说,笔者还真的在一个微服务里的一个核心接口看到了这种代码,该接口被过多的人接手导致了这样的局面。系统接手人过多以后,代码腐化的速度超出你的想像。


下面举例说明:


没有用卫语句的代码,很多层缩进


if (title.equals(newTitle)){
if (...) {
if (...) {
if (...) {

}
}else{

}
}else{

}
}

使用了卫语句的代码,缩进很少


if (!title.equals(newTitle)) {
return xxx;
}
if (...) {
return xxx;
}else{
return yyy;
}
if (...) {
return zzz;
}

避免双重循环


简单说双重循环会将代码逻辑的时间复杂度扩大至O(n^2)


如果有按key匹配两个列表的场景建议使用以下方式:


1 将列表1 进行map化


2 循环列表2,从map中获取值


代码示例如下:


List<WorkOrderChain> allPre = ...
List<WorkOrderChain> chains = ...
Map<String, WorkOrderChain> preMap = allPre.stream().collect(Collectors.toMap(WorkOrderChain::getWoNext, item -> item,(v1, v2)->v1));
chains.forEach(item->{
WorkOrderChain preWo = preMap.get(item.getWoNo());
if (preWo!=null){
item.setIsHead(1);
}else{
item.setIsHead(0);
}
});

@see @link来设计RPC的API


程序员们还经常自嘲的几个词有:API工程师,中间件装配工等,既然咱平时写API写的比较多,那种就把它写到极致**@see @link**的作用是让使用方可以方便的链接到枚举类型的对象上,方便阅读


示例如下:


@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ContentProcessDto implements Serializable {
/**
* 内容ID
*/
private String contentId;
/**
* @see com.jd.jr.community.common.enums.ContentTypeEnum
*/
private Integer contentType;
/**
* @see com.jd.jr.community.common.enums.ContentQualityGradeEnum
*/
private Integer qualityGrade;
}

日志打印避免只打整个参数


研发经常为了省事,直接将入参这样打印


log.info("operateRelationParam:{}", JSONObject.toJSONString(request));

该日志进了日志系统后,研发在搜索日志的时候,很难根据业务主键排查问题


如果改进成以下方式,便可方便的进行日志搜索


log.info("operateRelationParam,id:{},req:{}", request.getId(),JSONObject.toJSONString(request));

如上:只需要全词匹配“operateRelationParam,id:111”,即可找到业务主键111的业务日志。


用异常捕获替代方法参数传递


我们经常面对的一种情况是:从子方法中获取返回的值来标识程序接下来的走向,这种方式笔者认为不够优雅。


举例:以下代码paramCheck和deleteContent方法,返回了这两个方法的执行结果,调用方通过返回结果判断程序走向


public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
log.info("deleteContentParam:{}", contentOptDto.toString());
try{
RpcResult<?> paramCheckRet = this.paramCheck(contentOptDto);
if (paramCheckRet.isSgmFail()){
return RpcResult.getSgmFail("非法参数:"+paramCheckRet.getMsg());
}
ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
RpcResult<?> delRet = contentEventHandleAbility.deleteContent(contentEntity);
if (delRet.isSgmFail()){
return RpcResult.getSgmFail("业务处理异常:"+delRet.getMsg());
}
}catch (Exception e){
log.error("deleteContent exception:",e);
return RpcResult.getSgmFail("内部处理错误");
}
return RpcResult.getSgmSuccess();
}



我们可以通过自定义异常的方式解决:子方法抛出不同的异常,调用方catch不同异常以便进行不同逻辑的处理,这样调用方特别清爽,不必做返回结果判断


代码示例如下:


public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
log.info("deleteContentParam:{}", contentOptDto.toString());
try{
this.paramCheck(contentOptDto);
ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
contentEventHandleAbility.deleteContent(contentEntity);
}catch(IllegalStateException pe){
log.error("deleteContentParam error:"+pe.getMessage(),pe);
return RpcResult.getSgmFail("非法参数:"+pe.getMessage());
}catch(BusinessException be){
log.error("deleteContentBusiness error:"+be.getMessage(),be);
return RpcResult.getSgmFail("业务处理异常:"+be.getMessage());
}catch (Exception e){
log.error("deleteContent exception:",e);
return RpcResult.getSgmFail("内部处理错误");
}
return RpcResult.getSgmSuccess();
}

自定义SpringBoot的Banner


别再让你的Spring Boot启动banner千篇一律,spring 支持自定义banner,该技能对业务功能实现没任何卵用,但会给枯燥的编程生活添加一点乐趣。


以下是官方文档的说明: docs.spring.io/spring-boot…


另外你还需要ASCII艺术字生成工具: tools.kalvinbg.cn/txt/ascii


效果如下:


   _ _                   _                     _                 _       
(_|_)_ __ __ _ __| | ___ _ __ __ _ | |__ ___ ___ | |_ ___
| | | '_ \ / _` | / _` |/ _ \| '_ \ / _` | | '_ \ / _ \ / _ \| __/ __|
| | | | | | (_| | | (_| | (_) | | | | (_| | | |_) | (_) | (_) | |_\__ \
_/ |_|_| |_|\__, | \__,_|\___/|_| |_|\__, | |_.__/ \___/ \___/ \__|___/
|__/ |___/ |___/

多用Java语法糖


编程语言中java的语法是相对繁琐的,用过golang的或scala的人感觉特别明显。java提供了10多种语法糖,写代码常使用语法糖,给人一种 “这哥们java用得通透” 的感觉。


举例:try-with-resource语法,当一个外部资源的句柄对象实现了AutoCloseable接口,JDK7中便可以利用try-with-resource语法更优雅的关闭资源,消除板式代码。


try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
System.out.println(inputStream.read());
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}

利用链式编程


链式编程,也叫级联式编程,调用对象的函数时返回一个this对象指向对象本身,达到链式效果,可以级联调用。链式编程的优点是:编程性强、可读性强、代码简洁。


举例:假如觉得官方提供的容器不够方便,可以自定义,代码如下,但更建议使用开源的经过验证的类库如guava包中的工具类


/**
链式map
*/
public class ChainMap<K,V> {
private Map<K,V> innerMap = new HashMap<>();
public V get(K key) {
return innerMap.get(key);
}

public ChainMap<K,V> chainPut(K key, V value) {
innerMap.put(key, value);
return this;
}

public static void main(String[] args) {
ChainMap<String,Object> chainMap = new ChainMap<>();
chainMap.chainPut("a","1")
.chainPut("b","2")
.chainPut("c","3");
}
}

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

ChatGPT组合拳-插件+自定义命令

前言 这几个月来ChatGPT已经融入了我的日常开发中,大大的提高了生产效率,比如写周报,可以提供一些关键字就能生成一段周报。在解决问题方面,除了一些疑难杂症,GPT大部分都很不错的解决,同时在没有思路的时候也能提供一点灵感。但是在使用过程中也会对有的答案的质...
继续阅读 »

前言


这几个月来ChatGPT已经融入了我的日常开发中,大大的提高了生产效率,比如写周报,可以提供一些关键字就能生成一段周报。在解决问题方面,除了一些疑难杂症,GPT大部分都很不错的解决,同时在没有思路的时候也能提供一点灵感。但是在使用过程中也会对有的答案的质量感到不满或者GPT并不能很好的理解我们意图的情况,这种情况就和Prompt有关系了,如何与GPT对话也是一门艺术,好的Prompt可以提高GPT的效率和准确性。


插件


开门见山,先介绍一个网站 prompts.chat


这个网站中提供了各式各样的Prompt参考,我们可以在其中查找需要的,或者自己根据需要改进。但仅仅如此还不够,因为我们每次重新开始对话都需要找Prompt再复制给GPT。


那有没有什么东西能使我们的体验再丝滑一点,这就不得不介绍一款插件了。


_}OD1$)DA$(K35ATYETLC_0.jpg


AIPRM for ChatGPT


这款插件大家在浏览器的插件商店可以直接搜索。安装好GPT界面会变成这样


image.png


上面的区域是其他人分享后社区精选的Prompt模板,下面的区域可以选择返回的语言、语调、文字风格。

image.png


当我们点击某个模板后,下面区域也会随之显示,接下来我们讲一下如何自己编写一个模板。

编写模板


我们点击Own Prompts


image.png


这里显示的就是我们自己编写的模板,简单添加一个。

image.png


选择模板后的样子


image.png


进行对话测试模板是否生效


image.png


接下来我们来看看这款插件的原理,其实就是把我们的输入嵌入到Prompt Template中,所以在编写Prompt Template时必须有[PROMPT],和字符串模板一样。


image.png


如此一来,我们每次打开对话只需要选择我们预先写好的模板就可以了,但仅仅如此还不够,因为我们每一次对话只能使用一个插件,如果我们场景比较多呢?


7$%H4%UN_7(FRGI0JOJ`$X1.gif


自定义命令


自定义命令,看着有点高级感,其实一点就通,与GPT的对话中我们有这么一种操作


image.png


image.png


image.png


再结合AIPRM for ChatGPT,you know?


image.png


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

从 GPU 到 ChatGPT

硬件 “没有硬件支持,你破解个屁” GPU 什么是 GPU? GPU 是 Graphics Processing Unit 的缩写,中文翻译为图形处理器。GPU 最初是为了提高电脑处理图形的速度而设计的,主要负责图像的计算和处理。GPU 通过并行计算的方式...
继续阅读 »

硬件


“没有硬件支持,你破解个屁”



GPU



什么是 GPU?


GPU 是 Graphics Processing Unit 的缩写,中文翻译为图形处理器。GPU 最初是为了提高电脑处理图形的速度而设计的,主要负责图像的计算和处理。GPU 通过并行计算的方式,可以同时执行多个任务,大大提高了图形和数据处理的速度和效率。


近年来,由于其并行计算的特性,GPU 也被应用于一些需要大量计算的领域,如机器学习、深度学习、数据挖掘、科学计算等。在这些领域中,GPU 可以加速训练模型、处理海量数据等计算密集型任务,显著提高了计算效率和速度。因此,GPU 已成为现代计算机的重要组成部分,被广泛应用于各种领域。


GPU 是如何工作的?


GPU 的工作原理和 CPU 类似,都是通过执行指令来完成计算任务的。不同的是,CPU 是通过串行执行指令的方式来完成计算任务的,而 GPU 是通过并行执行指令的方式来完成计算任务的。GPU 的并行计算方式可以同时执行多个任务,大大提高了计算效率和速度。


可以参考这个视频来了解 GPU 的工作原理:http://www.bilibili.com/video/BV1VW…


GPU 和 CPU 的区别


GPU 和 CPU 的区别主要体现在以下几个方面:




  1. 架构设计不同:CPU 的设计注重单线程处理能力,通常有少量的计算核心和更多的高速缓存。GPU 则是面向并行处理的设计,通常拥有大量的计算核心,但缓存较小。




  2. 计算方式不同:CPU 在处理任务时,主要通过执行指令流的方式进行计算。而 GPU 则是通过执行大量的线程,同时进行并行计算,以提高计算效率。GPU 的并行计算能力可以同时处理许多相似的任务,适用于大规模的计算密集型任务,例如图像处理、机器学习等。




  3. 用途不同:CPU 主要用于通用计算任务,例如文件处理、操作系统运行、编程等。GPU 则主要用于图形处理、游戏、计算密集型任务,例如机器学习、深度学习等。




总结来说,GPU 和 CPU 都有各自的优势和适用场景,它们通常是相互协作的。例如,在机器学习中,CPU 通常用于数据的预处理和模型的训练过程,而 GPU 则用于模型的计算推理过程。


我们常说的显卡就是 GPU 吗?


是的,我们通常所说的显卡(Graphics Card)就是安装了 GPU 的设备。显卡除了包含 GPU 之外,还包括显存、散热器、显卡 BIOS 等部件。显卡通过将 CPU 传输的数据转换为图像信号,控制显示器输出图像。


在一些需要大量图像处理或计算的应用场景中,GPU 可以比 CPU 更高效地完成任务。因此,现代的显卡也广泛应用于机器学习、深度学习等领域的加速计算,甚至被用于科学计算、天文学、地质学、气象学等领域。


关于显卡,你可能听说过“集成显卡”、“独立显卡”,其实,显卡的集成和独立通常是指显存的不同管理方式,它们有以下区别:




  1. 集成显卡:集成显卡通常是指将显存集成在主板芯片组或处理器内部的显卡。这种显卡通常性能较差,适用于一些简单的应用场景,例如日常办公、网页浏览等。




  2. 独立显卡:独立显卡通常是指显存独立于主板芯片组或处理器,有自己的显存和显存控制器。这种显卡性能更加强大,适用于游戏、图形处理、科学计算等需要大量显存和计算性能的应用场景。




  3. 共享显存:共享显存通常是指显存与系统内存共享使用,也就是一部分系统内存被划分为显存使用。这种方式适用于一些轻度图形处理的应用场景,例如电影播放、网页浏览等。




总的来说,集成显卡通常性能较差,适用于简单应用场景,独立显卡性能更加强大,适用于需要大量显存和计算性能的应用场景,而共享显存则是一种折中的方案,适用于一些轻度图形处理的应用场景。


GPU 厂商


海外头部 GPU 厂商:



  1. Nvidia:Nvidia 是目前全球最大的 GPU 制造商之一,Nvidia 主要生产针对游戏玩家、数据中心和专业用户等不同领域的 GPU 产品。

  2. AMD:全球知名的 GPU 制造商之一。AMD 主要生产用于个人电脑、工作站和服务器等不同领域的 GPU 产品。

  3. Intel:目前也开始进军 GPU 市场。Intel 主要生产用于个人电脑、工作站和服务器等不同领域的 GPU 产品。


国内 GPU 厂商:


海光信息、寒武纪、龙芯中科、景嘉微等。


芯片“卡脖子” 说的就是 GPU 吗?


是,但不全是。


"芯片卡脖子"是指全球半导体短缺现象,也称为"芯片荒"或"半导体荒",指的是 2020 年以来由新冠疫情和其他因素导致的全球半导体供应不足的局面。这种供应短缺已经影响了多个行业,包括汽车、电子产品、通信设备等。中国作为世界上最大的半导体市场之一,也受到了这种供应短缺的影响。


我国在半导体领域的自主研发和制造水平相对较低,依赖进口芯片来支撑其经济和工业发展。受全球芯片短缺影响,我国的一些关键行业,特别是汽车、电子和通信行业,出现了供应短缺和价格上涨等问题,对其经济造成了一定的影响。为了应对这种情况,政府加强了对半导体行业的支持,鼓励本土企业增加芯片研发和生产能力,以减轻对进口芯片的依赖。


具体与 GPU 相关的:2022 年 8 月 31 日,为符合美国政府要求,Nvidia 和 AMD 的高端 GPU 将在中国暂停销售,包括 Nvidia 的 A100、H100 以及 AMD 的 MI100 和 MI200 芯片


英伟达在 SEC 文件上官方确认此事,称是 8 月 26 日收到美国政府的通知。



SEC 文件是由上市公司、上市公司内部人士、券商提交给美国证券交易委员会(SEC) 的财务报表或者其他正式文件。




nvidia (英伟达)



根据 2021 年第四季度的市场研究报告,英伟达在全球离散显卡市场占有率为 51.2%,位列第一,超过了其竞争对手 AMD 的市场份额。而在全球 GPU 市场(包括离散显卡和集成显卡)中,英伟达的市场占有率为 18.8%,位列第二,仅次于 Intel 的市场份额。


nvidia 的产品矩阵



  1. GeForce 系列:主要面向消费者市场,包括桌面显卡和笔记本电脑显卡等,以高性能游戏和多媒体应用为主要应用场景。

  2. Quadro 系列:主要面向专业工作站市场,包括电影和电视制作、建筑设计、科学计算、医疗影像等领域,具有高性能、高稳定性和优秀的图形渲染能力。

  3. Tesla 系列:主要面向高性能计算市场,包括科学计算、深度学习、人工智能等领域,具有极高的计算性能和数据吞吐量,支持多 GPU 集群计算。

  4. Tegra 系列:主要面向移动和嵌入式市场,包括智能手机、平板电脑、汽车、无人机等领域,具有高性能、低功耗、小尺寸等特点。

  5. Jetson 系列:主要面向人工智能应用市场,包括机器人、自动驾驶、智能视频分析等领域,具有高性能、低功耗、小尺寸等特点。



可能你对上面这些产品系列、型号和名词不太了解,没有什么概念,那这样,咱们先建立个价格概念。我们以当下在人工智能领域广泛应用的 GPU A100 为例,看一下它的价格:



就是因为这个价格,所以 A100 也被称为“英伟达大金砖”.


为什么要单独说英伟达呢?因为算力是 人工智能的“力量源泉”,GPU 是算力的“主要供应商”。而英伟达是全球最大的 GPU 制造商,并且它的 GPU 算力是最强的,比如 A100 GPU 算力是 10.5 petaFLOPS,而 AMD 的 MI100 GPU 算力是 7.5 petaFLOPS。


不明白什么意思?Peta 是计量单位之一,它代表的是 10 的 15 次方。因此,1 petaFLOPS(PFLOPS)表示每秒可以完成 10 的 15 次浮点运算。所以,A100 GPU 算力为 10.5 petaFLOPS,意味着它可以每秒完成 10.5 万亿次浮点运算。


AI


什么是人工智能 (Artificial Intelligence-AI)?


人工智能是指一种计算机技术,它使得计算机系统可以通过学习、推理、自适应和自我修正等方法,模拟人类的智能行为,以实现类似于人类的智能水平的一系列任务。这些任务包括语音识别、自然语言处理、图像识别、机器翻译、自动驾驶、智能推荐和游戏等。
人工智能的核心是机器学习,它是通过使用大量数据和算法训练计算机系统,使其能够识别模式、做出预测和决策。人工智能还涉及到其他领域,如自然语言处理、计算机视觉、机器人技术、知识表示和推理等。
人工智能被广泛应用于各种领域,如医疗、金融、交通、制造业、媒体和游戏等,为这些领域带来了更高的效率和创新。


人工智能细分领域



人工智能领域有很多分支领域,以下列举一些比较常见的:



  1. 机器学习(Machine Learning):研究如何通过算法和模型让计算机从数据中学习和提取规律,以完成特定任务。

  2. 深度学习(Deep Learning):是机器学习的一种,使用多层神经网络来学习特征和模式,以实现对复杂任务的自动化处理。

  3. 自然语言处理(Natural Language Processing, NLP):研究如何让计算机理解、分析、处理人类语言的方法和技术。

  4. 计算机视觉(Computer Vision):研究如何让计算机“看懂”图像和视频,并从中提取有用的信息和特征。

  5. 机器人学(Robotics):研究如何设计、构建和控制机器人,让它们能够完成特定任务。

  6. 强化学习(Reinforcement Learning):是一种机器学习的方法,通过与环境的交互和反馈来学习最优行动策略。

  7. 知识图谱(Knowledge Graph):是一种将知识以图谱的形式进行组织、表示和推理的方法,用于实现智能搜索、推荐等应用。

  8. 语音识别(Speech Recognition):研究如何让计算机识别和理解人类语音,以实现语音输入、语音控制等功能。


当然以上这些分支领域互相也有交叉和相互影响,比如深度学习在计算机视觉、自然语言处理和语音识别等领域都有应用;计算机视觉和自然语言处理也经常结合在一起,比如在图像字幕生成和图像问答等任务中。此外,人工智能还与其他领域如控制工程、优化学、认知科学等存在交叉。


NLP


我们具体地来看一下自然语言处理(NLP)这个分支领域,它是人工智能的一个重要分支,也是人工智能技术在实际应用中最为广泛的应用之一。


NLP(Natural Language Processing,自然语言处理)旨在让计算机能够理解、解析、生成和操作人类语言。


NLP 技术可以用于文本分类、情感分析、机器翻译、问答系统、语音识别、自动摘要、信息抽取等多个方面。实现 NLP 技术通常需要使用一些基础的机器学习算法,例如文本预处理、词嵌入(word embedding)、分词、词性标注、命名实体识别等等。这些算法可以从大量的语料库中学习到语言的结构和规律,并通过统计分析和机器学习模型进行自然语言的处理和应用。


近年来,随着深度学习技术的发展,NLP 领域也出现了一些基于深度学习的新模型,例如 Transformer 模型和 BERT 模型等。这些模型通过使用大规模语料库进行预训练,可以在多个 NLP 任务中取得优秀的表现。同时,也涌现了一些新的应用领域,例如对话系统、智能客服、智能写作、智能问答等。


Transformer 是什么?


上文我们提到人工智能的分支领域之间会有交叉,Transformer 算是深度学习和 NLP 的交叉领域。



Transformer 模型是深度学习中的一种神经网络模型,该模型是由 Google 开源的。


Transformer 模型最初是在 2017 年发表的论文"Attention Is All You Need"中提出的,随后被加入到 TensorFlow 等深度学习框架中,方便了广大开发者使用和扩展。目前,Transformer 模型已经成为自然语言处理领域中最流行的模型之一。



TensorFlow 是一种用于实现神经网络模型的开源深度学习框架。因此,可以使用 TensorFlow 实现 Transformer 模型。实际上,TensorFlow 团队已经提供了一个名为“Tensor2Tensor”的库,其中包含了 Transformer 模型的实现。此外,许多研究人员和工程师也使用 TensorFlow 实现自己的 Transformer 模型,并将其用于各种 NLP 任务中。




Transformer 特别擅长处理序列数据,其中包括了 NLP 领域的自然语言文本数据。在 NLP 领域中,Transformer 模型被广泛应用于各种任务,例如机器翻译、文本摘要、文本分类、问答系统、语言模型等等。相比于传统的基于循环神经网络(RNN)的模型,Transformer 模型通过使用注意力机制(self-attention)和多头注意力机制(multi-head attention)来建模序列中的长程依赖性和关系,有效地缓解了 RNN 模型中梯度消失和梯度爆炸的问题,从而在 NLP 任务上取得了很好的表现。因此,可以说 Transformer 是 NLP 领域中的一种重要的深度学习模型,也是现代 NLP 技术的重要组成部分。


Transformer 模型的实现


Transformer 模型只是一个抽象的概念和算法框架,具体的实现还需要考虑许多细节和技巧。在实际应用中,需要根据具体的任务和数据集进行模型的设计、参数调整和训练等过程。此外,还需要使用特定的软件框架(如 TensorFlow、PyTorch 等)进行实现和优化,以提高模型的效率和准确性。


实现 Transformer 模型可以使用深度学习框架,如 TensorFlow、PyTorch 等。一般来说,实现 Transformer 模型的步骤如下:



  1. 数据准备:准备训练和测试数据,包括语料数据和标签数据等。
    模型架构设计:确定模型的结构,包括 Transformer 的编码器和解码器部分,以及注意力机制等。

  2. 模型训练:使用训练数据对模型进行训练,并对模型进行调优,以达到较好的预测效果。

  3. 模型评估:使用测试数据对模型进行评估,包括损失函数的计算、精度、召回率、F1 值等。

  4. 模型部署:将训练好的模型部署到生产环境中,进行实际的应用。


业界流行的实现方式是使用深度学习框架,如 TensorFlow 或 PyTorch,在现有的 Transformer 模型代码基础上进行二次开发,以满足自己的需求。同时,也有一些第三方的 Transformer 库,如 Hugging Face 的 Transformers 库,可供直接使用,方便快捷。


还有没有其他模型 ?


类似于 Transformer 的模型有许多,其中一些主要的模型包括:



  1. BERT(Bidirectional Encoder Representations from Transformers):BERT 是由 Google 在 2018 年推出的预训练语言模型,采用了 Transformer 模型的编码器部分,并使用双向的 Transformer 模型来对输入的文本进行建模。

  2. GPT(Generative Pre-trained Transformer):GPT 是由 OpenAI 在 2018 年推出的预训练语言模型,采用了 Transformer 模型的解码器部分,主要用于生成文本。

  3. XLNet:XLNet 是由 CMU、Google 和 Carnegie Mellon University 的研究人员在 2019 年提出的一种预训练语言模型,它使用了自回归 Transformer 模型和自回归 Transformer 模型的结合,具有更好的生成性能和语言理解能力。

  4. T5(Text-to-Text Transfer Transformer):T5 是由 Google 在 2019 年推出的一种基于 Transformer 的通用文本转换模型,可以处理各种 NLP 任务,如文本分类、问答、文本摘要等。

  5. RoBERTa(Robustly Optimized BERT Pretraining Approach):RoBERTa 是 Facebook 在 2019 年推出的预训练语言模型,它通过对 BERT 训练过程进行优化,提高了在多种 NLP 任务上的性能表现。


这些模型都基于 Transformer 架构,并通过不同的优化和改进来提高性能和应用范围。下面一张图是模型的家族树:



GPT 模型


2018 年 OpenAI 公司基于 Transformer 结构推出 GPT-1 (Generative Pre-training Transformers, 创造型预训练变换模型),参数量为 1.17 亿个,GPT-1 超越 Transformer 成为业内第一。2019 年至 2020 年,OpenAI 陆续发布 GPT-2、GPT-3,其参数量分别达 到 15 亿、1750 亿,其中,GPT-3 训练过程中直接以人类自然语言作为指令,显著提升了 LLM 在多种语言场景中的性能。


ChatGPT



ChatGPT 是美国 OpenAI 公司研发的对话 AI 模型,是由人工智能技术支持的自然语言处理(NLP,Natural Language Processing)工具,于 2022 年 11 月 30 日正式发布。它能够学习、理解人类语言,并结合对话上下文,与人类聊天互动,也可撰写稿件、翻译文字、编程、编写视频脚本等。截至 2023 年 1 月底,ChatGPT 月活用户已高达 1 亿,成为史上活跃用户规模增长最快的应用


与现存的其他同类产品相比,ChatGPT 的独特优势在于:



  1. 基于 GPT-3.5 架构,运用海量语料库训练模型,包括真实生活中的对话,使 ChatGPT 能做到接近与人类聊天

  2. 应用新技术 RLHF (Reinforcement Learning with Human Feedback,基于人类反馈的强化学习),从而能更准确地理解并遵循人类的思维、价值观与需求

  3. 可在同一阶段内完成模型训练

  4. 具有强大算力、自我学习能力和适应性,且预训练通用性较高

  5. 可进行连续多轮对话,提升用户体验

  6. 更具独立批判性思维,能质疑用户问题的合理性,也能承认自身知识的局限性,听取用户意见并改进答案。


GPT-3.5


ChatGPT 使用的 GPT-3.5 模型是在 GPT-3 的基础上加入 Reinforcement Learning from Human Feedback(RLHF,人类反馈强化学习)技术和近段策略优化算法,其目的是从真实性、无害性和有用性三个方面优化输出结果,降低预训练模型生成种族歧视、性别歧视等有害内容的风险。


ChatGPT 训练的过程主要有三个阶段。




  1. 第一步是训练监督策略,人类标注员对随机抽取的提示提供预期结果,用监督学习的形式微调 GPT-3.5,生成 Supervised Fine-Tuning(SFT)模型,使 GPT-3.5 初步理解指令,这一步与先前的 GPT-3 模型训练方式相同,类似于老师为学生提供标答的过程。

  2. 第二步是奖励模型,在 SFT 模型中随机抽取提示并生成数个结果,由人类标注员对结果的匹配程度进行排序,再将问题与结果配对成数据对输入奖励模型进行打分训练,这个步骤类似于学生模拟标答写出自己的答案,老师再对每个答案进行评分。

  3. 第三步是 Proximal Policy Optimization(PPO,近段策略优化),也是 ChatGPT 最突出的升级。模型通过第二步的打分机制,对 SFT 模型内数据进行训练,自动优化迭代,提高 ChatGPT 输出结果的质量,即是学生根据老师反馈的评分,对自己的作答进行修改,使答案更接近高分标准。


ChatGPT 的优势在于:



  1. 使用 1750 万亿参数的 GPT-3 为底层模型进行预训练,为全球最大的语言模型之一

  2. 算力上得到微软支持,使用上万片 NVIDIA A100 GPU 进行训练,模型的运行速度得到保障(从这里就看出硬件的重要性了,A100 “卡脖子”确实很难受,不过之前各厂都囤货了,短期应该能满足现状,而且作为 A00 的平替 A800 即将出货,训练效率快速提升,应该也能满足需求。)

  3. 算法上使用奖励模型和近端优化策略进行迭代优化, 将输出结果与人类预期答案对齐,减少有害性、歧视性答案,使 ChatGPT 更拟人化,让用户感觉沟通的过程更流畅。


GPT-4



据德国媒体 Heise 消息,当地时间 3 月 9 日一场人工智能相关活动上,四名微软德国员工在现场介绍了包括 GPT 系列在内的大语言模型(LLM),在活动中,微软德国首席技术官 Andreas Braun 表示 GPT-4 即将发布。


GPT-4 已经发展到基本上「适用于所有语言」:你可以用德语提问,然后用意大利语得到答案。借助多模态,微软和 OpenAI 将使「模型变得全面」。将提供完全不同的可能性,比如视频。


AIGC 模型


在人工智能内容生成领域,除了 OpenAI, 还有其他玩家,来看一下目前头部玩家的情况:



人工智能突破摩尔定律



摩尔定律是由英特尔公司创始人之一戈登·摩尔于 1965 年提出的一项预测。这项预测认为,在集成电路上可容纳的晶体管数量每隔 18 至 24 个月会翻一番,而成本不变或者成本减少。


简单来说,摩尔定律预测了随着时间的推移,计算机芯片上能集成的晶体管数量将以指数级别增长,而成本将持续降低。这意味着计算机性能将在同样的芯片面积上不断提高,同时计算机的成本也会不断降低。


摩尔定律在过去几十年的计算机工业中发挥了重要的作用,它是计算机发展的重要标志之一,但近年来随着摩尔定律趋于极限,一些人开始怀疑其可持续性。



摩尔定律的定义归纳起来,主要有以下三种版本:



  1. 集成电路上可容纳的晶体管数目,约每隔 18 个月便增加一倍。

  2. 微处理器的性能每隔 18 个月提高一倍,或价格下降一半。

  3. 相同价格所买的电脑,性能每隔 18 个月增加一倍。


随着模型的迭代,对算力的需求也越来越大了:


目前看人工智能对算力的需求已经突破了摩尔定律


未来


目前我已在编程、邮件书写、知识学习等多个场景开始使用 chatGPT,未来有计划开发 chatGPT的应用程序,让更多人能够体验到 chatGPT 的魅力。


未来已来,缺少的不是技术,而是想象力!


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

ChatGPT保姆级教程,一分钟学会使用ChatGPT!

最近ChatGPT大火!微软退出首款ChatGPT搜索引擎,阿里等国内巨头也纷纷爆出自家产品,一夜之间,全球最大的科技公司仿佛都回到了自己年轻时的样子! 然而,ChatGPT这么火,这么好玩的东西,国人都被卡在注册上了?! 今天老鱼给大家汇总了国内能使用Cha...
继续阅读 »

最近ChatGPT大火!微软退出首款ChatGPT搜索引擎,阿里等国内巨头也纷纷爆出自家产品,一夜之间,全球最大的科技公司仿佛都回到了自己年轻时的样子!


然而,ChatGPT这么火,这么好玩的东西,国人都被卡在注册上了?!


今天老鱼给大家汇总了国内能使用ChatGPT的方法,解锁更多ChatGPT玩法!


完整文档打开姿势: ChatGPT怎么玩)
在这里插入图片描述


一.准备工作


和注册美区 Apple ID 一样的流程:



  1. 挂个代理,伪装在日本、新加坡或者美国,建议新加坡;亲测香港是100%不可行的

  2. 准备一个国外手机号,GoogleVoice 虚拟号会被识别,亲测不行,使用接码平台

  3. Chrome 浏览器。


二. 注册接码平台


打开网站:sms-activate.org/ 使用邮箱注册
注意:邮件里链接点击后可能回404,多试几次就好了。
在这里插入图片描述
然后,选择充值 1 美元,支持使用支付宝。
在这里插入图片描述
充值完毕,在左侧搜索 OpenAI,从销量看印尼最高,亲测也可用。(印度也可以)
选择后系统会分配一个手机号,留着备用。
在这里插入图片描述


三. 注册 OpenAI


打开链接
beta.openai.com/signup
选择使用 Google 登录即可(建议)
在这里插入图片描述


在这里输入第二步中分配给你的手机号,然后点击【Send code via SMS】按钮。
在这里插入图片描述


回到刚刚的接码平台,就能看到收到的验证码了。
PS:注意购买后的短信有效期是20分钟,需要快速操作哦~
在这里插入图片描述


我们把验证码拷贝出来输入到OpenAI的注册界面即可
在这里插入图片描述


四. 体验ChatGPT


重新登录:chat.openai.com/auth/login


然后访问:chat.openai.com/chat


这时你就可以开始尽情和机器人聊天了
在这里插入图片描述
在这里插入图片描述
如果你没有上述条件去玩ChatGPT,那么看这里解锁更多简单玩法 ChatGPT怎么玩 )


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