开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

解锁面向对象设计的六大原则:让你的代码更优雅、更可维护的秘诀

发表于 2024-04-22

本文微信公众号「安卓小煜」首发

1. 背景

大家应该都听过面向对象设计的 SOLID 原则,本文我们就来唠一唠面向对象设计的六大原则,也就是 SOLID+迪米特原则。

2. SOLID 原则

2.1 单一职责原则

单一职责原则的英文名称是 Single Responsibility Principle,缩写是 SRP。

SRP 的定义是,就一个类而言,应该仅有一个引起它变化的原因

简单来说,一个类中应该是一组相关性很高的函数、数据的封装

举个例子

我们要设计一个图片加载器。这个时候一个类里面如果我们这样写:

1
2
3
4
5
6
7
8
9
10
11
java复制代码class ImageLoader {
void display(ImageView img) {
// TODO
}
void setImageCache(String uri, Bitmap bitmap) {
// TODO
}
Bitmap getImageCache(String uri) {
// TODO
}
}

我们会发现这个图片加载器类既承担了图片加载的功能,也承担了图片缓存的功能。

当我们图片加载的逻辑修改时,我们需要来改动这个类的代码。

当我们图片缓存的逻辑修改时,也需要来改动这个类的代码。

也就是说引起我们图片加载器类变化的原因存在多个,不符合 SRP,因此我们需要对其进行功能的解耦,把缓存给单独拎出来放到另一个类里面。

2.2 开闭原则

开闭原则的英文全称是 Open Close Principle,缩写是 OCP。

OCP 的定义是,软件中的对象应该对于扩展是开放的,但是,对于修改是封闭的

举个例子

我们设计的图片加载器。

图片缓存应该支持多种缓存方式,并且要支持用户自定义缓存。

因此需要提供一个接口给用户设置,通过依赖注入的方式来确定具体的缓存策略。

1
2
3
4
5
6
7
8
9
10
11
java复制代码interface ImageCache {
void setImageCache(String uri, Bitmap bitmap);
Bitmap getImageCache(String uri);
}

class ImageLoader {
// 提供了接口让用户可以支持缓存的自定义
void setCacheStrategy(ImageCache cache) {
// TODO
}
}

2.3 里氏替换原则

里氏替换原则的英文全称是 Liskov Substitution Principle,缩写是 LSP,不是lǎo sè pī

LSP 直截了当的定义是,所有引用基类的地方必须能透明地使用其子类的对象

举个例子

还是以我们设计的图片加载器为例来进行说明。

我们可以看到,缓存的多种实现方式,依靠的就是里氏替换原则,有了这个原则,用户如果要自定义缓存的实现方式,只需要写一个子类继承我们的基类 ImageCache ,然后通过依赖注入设置进去即可。

而这里依赖注入可以成功的前提正是由于引用基类的地方能使用其子类的对象。所以说

里氏替换原则跟开闭原则是不离不弃的,通过里氏替换原则来达到对扩展开放,对修改封闭的效果。

2.4 接口隔离原则

接口隔离原则的英文全称是 Interface Segregation Principle,缩写是 ISP

ISP 的定义是,客户端不应该依赖它不需要的接口

举个例子

我们知道,在使用流进行文件的读写之后,我们要及时进行关闭,否则就可能导致内存泄漏。

我们使用流来读操作一般会写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码String filePath = "path/to/your/file.txt"; // 请替换为你的文件路径  
BufferedReader reader = null;

try {
reader = new BufferedReader(new FileReader(filePath));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

我们会发现,流的关闭写的有点多层嵌套的样子,不够简洁

1
2
3
4
5
6
7
java复制代码if (reader != null) {  
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}

因此我们会考虑把流的关闭代码抽取出来,写到一个工具类里面,类似:

1
2
3
4
5
6
7
8
9
java复制代码public void close(BufferedReader reader) {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

当然,有了上文的学习基础之后,大家应该发现这个工具类写的有点问题,它依赖的是具体而不是抽象,如果我们换一个读文件的 API,我们这个方法就不能使用了。

我们跟踪了 BufferedReader,发现它的父类是 Reader,那是不是代表着我们使用 Reader 来做参数就 OK 了呢?

当然不是

如果你参数用 Reader,那么对于 Writer 或者 InputStream 又不通用了。

这里的一个做法其实就体现了 ISP。

对于有关闭功能的类,它都可以实现 Closeable 接口,这个接口里面就一个 close 方法

因此,如果你自己写的一个类,有关闭的功能,也可以实现 Closeable 接口

Closeable 接口只提供了一个关闭方法,因此如果客户端有关闭需要,就可以依赖它,而没有关闭需求的类,则不需要依赖这个接口

所以对于关闭功能的封装,我们可以写出如下代码:

1
2
3
4
5
6
7
8
9
java复制代码public void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

ISP 使得我们的这个公共方法可以给那些有关闭需求的类使用

2.5 依赖倒置原则

依赖倒置原则英文全称是 Dependence Inversion Principle,缩写是 DIP。

DIP 在 Java 语言中的表现是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

举个例子

上文中的图片加载器,缓存的多种实现方式,是通过依赖注入来实现的。其依赖关系是通过接口 ImageCache 来产生的。


以上就是面向对象编程的 SOLID 原则

单一职责 SRP

开闭原则 OCP

里氏置换 LSP

接口隔离 ISP

依赖倒置 DIP(依赖反转)

3. 迪米特原则

迪米特原则的英文全称是 Law Of Demeter,缩写是 LOD

LOD 的定义是,一个对象应该对其他对象有最少的了解

举个例子

大家应该都找过房子。

找房子我们可以抽象出三个角色:租户、房子、中介。

对于租户来说,他只需要把想要的房子面积和接受价格等要求给到中介即可,而不需要自己去判断这个房子是不是符合他的要求,这些都是属于中介的职责。因此对于租户来说,他不需要对房子有了解。

4. 总结

以上就是面向对象设计的六大原则,也就是 SOLID+迪米特原则。

大家在进行软件开发时,心里要牢记这些原则,但是又不硬套原则。

不管是原则还是工具,最终的目的都只是为了让我们的软件开发更有效率。


参考:

Android 源码设计模式解析与实战

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

鸿蒙应用开发-webview 组件(使用和调试)

发表于 2024-04-22

1.WebView 组件介绍

HarmonyOS中的Web组件是一种基于Web技术的组件,可以在HarmonyOS应用程序中嵌入Web内容。通过使用Web组件,开发人员可以将Web页面或应用程序嵌入到HarmonyOS应用程序中,实现更丰富的用户界面和功能。

Web组件提供了一系列的API和工具,开发人员可以使用这些API和工具来控制和操作内嵌的Web页面。例如,开发人员可以使用JavaScript和CSS来操作和样式化Web页面的元素,还可以使用HTML5的各种功能来实现各种交互和媒体功能。

Web组件还支持与HarmonyOS应用程序的其他部分进行通信和交互。开发人员可以使用JavaScript和HarmonyOS的API来实现应用程序的功能,例如访问设备的传感器、调用系统的功能等。此外,Web组件还可以通过与应用程序的其他组件进行交互来实现更复杂的功能,例如在应用程序的其他组件中显示Web页面的内容、发送和接收消息等。

功能 描述
页面加载 Web组件提供基础的前端页面加载能力,包括加载网络页面、本地页面、Html格式文本数据。
页面交互 Web组件提供丰富的页面交互方式,包括设置前端页面深色模式,新窗口中加载页面,位置权限管理,Cookie管理,应用侧使用前端页面JavaScript等能力。
页面调试 Web组件支持使用Devtools工具调试前端页面。

2.使用Web组件加载页面

  • 2.1 加载网络页面

1、权限配置

1
2
3
4
5
ts复制代码"requestPermissions": [
{
"name": "ohos.permission.INTERNET" // 使用网络权限
}
]

2、加载网页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ts复制代码import web_webview from '@ohos.web.webview'

@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController()
build() {
Column() {
Button('加载首页')
.onClick(() => {
try {
// 点击按钮时,通过loadUrl,跳转到www.example1.com
this.controller.loadUrl('blog.csdn.net/aa2528877987');
} catch (error) {
console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.baidu.com', controller: this.controller})
}
}
}
  • 2.2 加载本地页面

1、ets 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ts复制代码// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
webviewController: web_webview.WebviewController = new web_webview.WebviewController();

build() {
Column() {
Button('loadUrl')
.onClick(() => {
try {
// 点击按钮时,通过loadUrl,跳转到local1.html
this.webviewController.loadUrl($rawfile("local1.html"));
} catch (error) {
console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);
}
})
// 组件创建时,通过$rawfile加载本地文件local.html
Web({ src: $rawfile("local.html"), controller: this.webviewController })
}
}
}

2、本地页面

1
2
3
4
5
6
7
html复制代码<!-- local.html -->
<!DOCTYPE html>
<html>
<body>
<p>Hello World</p>
</body>
</html>
  • 2.3 加载HTML格式的文本数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ts复制代码// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();

build() {
Column() {
Button('loadData')
.onClick(() => {
try {
// 点击按钮时,通过loadData,加载HTML格式的文本数据
this.controller.loadData(
"<html><body bgcolor="white">Source:<pre>source</pre></body></html>",
"text/html",
"UTF-8"
);
} catch (error) {
console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.controller })
}
}
}

3.设置基本属性和事件

3.1 设置深色模式

通过darkMode()接口可以配置不同的深色模式。

  • WebDarkMode.Off模式表示关闭深色模式。
  • WebDarkMode.On表示开启深色模式,且深色模式跟随前端页面。
  • WebDarkMode.Auto表示开启深色模式,且深色模式跟随系统。
  • forceDarkAccess()接口可将前端页面强制配置深色模式,且深色模式不跟随前端页面和系统。配置该模式时候,需要配置成WebDarkMode.On。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ts复制代码// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
@State mode: WebDarkMode = WebDarkMode.On;
@State access: boolean = true;
build() {
Column() {
Web({ src: 'www.example.com', controller: this.controller })
.darkMode(this.mode)
.forceDarkAccess(this.access)
}
}
}

3.2 上传文件

HarmonyOS中Web组件的onShowFileSelector()方法用于显示文件选择器,让用户选择文件。它可以用于在应用中上传文件或选择本地文件等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ts复制代码// xxx.ets
import web_webview from '@ohos.web.webview';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController()
build() {
Column() {
// 加载本地local.html页面
Web({ src: $rawfile('local.html'), controller: this.controller })
.onShowFileSelector((event) => {
// 开发者设置要上传的文件路径
let fileList: Array<string> = [
'xxx/test.png',
]
event.result.handleFileList(fileList)
return true;
})
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Document</title>
</head>

<body>
// 点击文件上传按钮
<input type="file" value="file"></br>
</body>
</html>

3.3 在新窗口中打开页面

开发者可以使用multiWindowAccess()接口来设置网页是否可以在新窗口中打开。通过调用此接口并传入相应的参数,可以控制网页是否允许使用新窗口。

当网页请求在新窗口中打开时,应用将收到Web组件的新窗口事件,可以通过onWindowNew()接口来处理此事件。在此接口中,开发者可以根据需要创建新的窗口来处理Web组件的窗口请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
ts复制代码// xxx.ets
import web_webview from '@ohos.web.webview'

//在同一page页有两个web组件。在WebComponent新开窗口时,会跳转到NewWebViewComp。
@CustomDialog
struct NewWebViewComp {
controller?: CustomDialogController
webviewController1: web_webview.WebviewController = new web_webview.WebviewController()
build() {
Column() {
Web({ src: "", controller: this.webviewController1 })
.javaScriptAccess(true)
.multiWindowAccess(false)
.onWindowExit(()=> {
console.info("NewWebViewComp onWindowExit")
if (this.controller) {
this.controller.close()
}
})
}
}
}

@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController()
dialogController: CustomDialogController | null = null
build() {
Column() {
Web({ src:$rawfile("window.html"), controller: this.controller })
.javaScriptAccess(true)
//需要使能multiWindowAccess
.multiWindowAccess(true)
.allowWindowOpenMethod(true)
.onWindowNew((event) => {
if (this.dialogController) {
this.dialogController.close()
}
let popController:web_webview.WebviewController = new web_webview.WebviewController()
this.dialogController = new CustomDialogController({
builder: NewWebViewComp({webviewController1: popController})
})
this.dialogController.open()
//将新窗口对应WebviewController返回给Web内核。
//如果不需要打开新窗口请调用event.handler.setWebController接口设置成null。
//若不调用event.handler.setWebController接口,会造成render进程阻塞。
event.handler.setWebController(popController)
})
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html复制代码 <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WindowEvent</title>
</head>
<body>
<input type="button" value="新窗口中打开网页" onclick="OpenNewWindow()">
<script type="text/javascript">
function OpenNewWindow()
{
let openedWindow = window.open("about:blank", "", "location=no,status=no,scrollvars=no");
openedWindow.document.write("<p>这是我的窗口</p>");
openedWindow.focus();
}
</script>
</body>
</html>

3.4 管理位置权限

对于某个网站进行位置权限管理的过程中,开发者可以通过onGeolocationShow()接口来向用户请求位置权限。该接口会触发一个位置权限请求对话框,用户可以选择是否授权该网站获取设备的位置信息。

Web组件会根据用户的选择,将权限授予或拒绝。如果权限被授予,前端页面将能够获取设备的位置信息。如果权限被拒绝,前端页面将无法获取设备的位置信息。

在进行位置权限请求之前,开发者需要在应用的配置文件中添加ohos.permission.LOCATION权限,以确保应用有权限获取设备的位置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
html复制代码<!DOCTYPE html>
<html>
<body>
<p id="locationInfo">位置信息</p>
<button onclick="getLocation()">获取位置</button>
<script>
var locationInfo=document.getElementById("locationInfo");
function getLocation(){
if (navigator.geolocation) {
<!-- 前端页面访问设备地理位置 -->
navigator.geolocation.getCurrentPosition(showPosition);
}
}
function showPosition(position){
locationInfo.innerHTML="Latitude: " + position.coords.latitude + "<br />Longitude: " + position.coords.longitude;
}
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Web({ src:$rawfile('getLocation.html'), controller:this.controller })
.geolocationAccess(true)
.onGeolocationShow((event) => { // 地理位置权限申请通知
AlertDialog.show({
title: '位置权限请求',
message: '是否允许获取位置信息',
primaryButton: {
value: 'cancel',
action: () => {
event.geolocation.invoke(event.origin, false, false); // 不允许此站点地理位置权限请求
}
},
secondaryButton: {
value: 'ok',
action: () => {
event.geolocation.invoke(event.origin, true, false); // 允许此站点地理位置权限请求
}
},
cancel: () => {
event.geolocation.invoke(event.origin, false, false); // 不允许此站点地理位置权限请求
}
})
})
}
}
}

4.请求响应

4.1 前端代码
1
2
3
4
5
6
7
8
9
10
11
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>example</title>
</head>
<body>
<!-- 页面资源请求 -->
<a href="https://www.example.com/test.html">intercept test!</a>
</body>
</html>
4.2 应用侧代码

在HarmonyOS中,onInterceptRequest()是一个接口,用于拦截网络请求并进行处理。它是HarmonyOS的网络框架提供的一种扩展机制,可以在网络请求发起之前拦截请求,并进行一些自定义的操作。

当一个网络请求发起时,HarmonyOS的网络框架会首先调用onInterceptRequest()接口。在该接口中,你可以对请求进行一些处理,例如修改请求的URL、添加请求头、修改请求参数等。还可以在此处拦截请求,返回自定义的响应结果,以实现一些常见的操作,如模拟网络请求,拦截广告请求等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码import web_webview from '@ohos.web.webview'

@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController()
responseResource: WebResourceResponse = new WebResourceResponse()
// 开发者自定义响应数据
@State webdata: string = "<!DOCTYPE html>\n" +
"<html>\n"+
"<head>\n"+
"<title>intercept test</title>\n"+
"</head>\n"+
"<body>\n"+
"<h1>intercept test</h1>\n"+
"</body>\n"+
"</html>"
build() {
Column() {
Web({ src: $rawfile('local.html'), controller: this.controller })
.onInterceptRequest((event?: Record<string, WebResourceRequest>): WebResourceResponse => {
if (!event) {
return new WebResourceResponse();
}
let mRequest: WebResourceRequest = event.request as WebResourceRequest;
console.info('TAGLee: url:'+ mRequest.getRequestUrl());
//拦截页面请求,如果加载的url判断与目标url一致则返回自定义加载结果webdata
if(mRequest.getRequestUrl() === 'https://www.example.com/test.html'){
// 构造响应数据
this.responseResource.setResponseData(this.webdata);
this.responseResource.setResponseEncoding('utf-8');
this.responseResource.setResponseMimeType('text/html');
this.responseResource.setResponseCode(200);
this.responseResource.setReasonMessage('OK');
return this.responseResource;
}
return;
})
}
}
}

5.web chrome调试

1、开启调试

在HarmonyOS中,setWebDebuggingAccess()接口用于设置是否允许调试Web视图。

setWebDebuggingAccess()接口的语法如下:

1
java复制代码setWebDebuggingAccess(boolean debuggable);

参数debuggable为boolean型,表示是否允许调试Web视图。如果debuggable为true,则允许调试Web视图;如果debuggable为false,则禁止调试Web视图。

此接口需要在合适的地方调用,例如在应用程序的入口Activity中或者WebView的初始化代码中调用。调用该方法后,系统将根据参数的值来决定是否允许调试Web视图。

2、 配置端口

1
2
3
4
java复制代码// 添加映射 
hdc fport tcp:9222 tcp:9222
// 查看映射
hdc fport ls

3、在PC端chrome浏览器地址栏中输入chrome://inspect/#devices,页面识别到设备后,就可以开始页面调试

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微信分享第三方连接(H5页面)自定义页面 一、准备工作 二、

发表于 2024-04-22

一、准备工作

  • 一个已备案的网站
  • 一个已认证的公众号(注意,个人权限是不可以的,需要企业权限)。

确保你有一个企业级的微信公众号,并完成企业认证。个人公众号可能无法获取全部接口权限。

  • 获取AppID和AppSecret

在微信公众平台登录后,进入“开发”部分,选择“基本配置”,在这里你可以找到你的AppID和AppSecret。

二、微信公众号后台设置

  • 设置JS接口安全域名:
    进入公众号设置的“功能设置”中,填写“JS接口安全域名”。需确保你的网页服务部署在此域名下,且该域名已通过ICP备案。
  • 设置IP白名单:
    在“安全中心”或“开发者中心”设置服务器IP白名单,以便微信服务器能够与之通信。

三、获取签名(含代码)

3.1 获取access_token

  • 接口说明:

使用你的AppID和AppSecret,通过GET请求到api.weixin.qq.com/cgi-bin/tok…

参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token):

官方文档:developers.weixin.qq.com/doc/offiacc…

  • golang代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
go复制代码/*
获取小程序全局唯一后台接口调用凭据:
https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/access-token/auth.getAccessToken.html
*/
package weixinclient

import (
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
)

type AccessTokenVo struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
Errcode int64 `json:"errcode"`
Errmsg string `json:"errmsg"`
}

func GetAccessToken(c *gin.Context) (*AccessTokenVo, error) {
ctx := commonx.GetTrace(c)

ps := fmt.Sprintf("grant_type=client_credential&appid=%s&secret=%s", config.BookAppid, config.BookSecret)

ep := getEndPoint("GetAccessToken")
resp, err := doGet(c, ep, ps, nil)
dlog.Infof("%v||GetSessionBycode resp=%v,err=%v", ctx, resp, err)
if err != nil || len(resp) == 0 {
return nil, errors.New("call API(GetAccessToken) fail")
}

var vo AccessTokenVo
err = json.Unmarshal(resp, &vo)
if err != nil || vo.Errcode > 0 {
err = fmt.Errorf("err=%v", vo.Errmsg)
return nil, err
}

return &vo, nil
}

返回示例:

1
2
3
4
json复制代码{
"access_token": "80_PR606SNAcwOIyhsRuuOVC11eHDiy1ZqKMiWn6JoxYZH3ANGt13s5DWgiWtIbk0JAxn5LBKyZBMK5-cP5q_NBvTVdFtIf9utExtktae_7c1t4Wm9aBkEd5fuYpw4MMQfAHARRV",
"expires_in": 7200
}

3.2 获取jsapi_ticket

  • 接口说明:

使用上一步获取的access_token,通过GET请求到api.weixin.qq.com/cgi-bin/tic…

参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token)

官方文档:developers.weixin.qq.com/doc/offiacc…

  • 请求方式:
1
2
3
4
bash复制代码请求方式:GET
请求参数:上一步获取到的 access_token
请求地址:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
注意:有效期7200秒, 这里建议将 access_token 和 jsapi_ticket 都在服务器端进行获取并缓存,前端通过接口调取结果
  • jsapi_ticket 说明:

jsapi_ticket是公众号用于调用微信 JS 接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的 api 调用次数非常有限,频繁刷新jsapi_ticket会导致 api 调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket (这里建议将 access_token 和 jsapi_ticket 都在服务器端进行获取并缓存,前端通过接口调取结果)。

  • golang代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
go复制代码package weixinclient

import (
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
)

type GetticketVo struct {
Ticket string `json:"ticket"`
ExpiresIn int64 `json:"expires_in"`
Errcode int64 `json:"errcode"`
Errmsg string `json:"errmsg"`
}

func Getticket(c *gin.Context, accessToken string) (*GetticketVo, error) {
ctx := commonx.GetTrace(c)

ps := fmt.Sprintf("access_token=%s&type=jsapi", accessToken)

ep := getEndPoint("Getticket")

resp, err := doGet(c, ep, ps, nil)
dlog.Infof("%v||Getticket resp=%v,err=%v", ctx, resp, err)
if err != nil || len(resp) == 0 {
return nil, errors.New("call API(Getticket) fail")
}

var vo GetticketVo
err = json.Unmarshal(resp, &vo)
if err != nil || vo.Errcode > 0 || vo.Errmsg != "ok" {
err = fmt.Errorf("err=%v", vo.Errmsg)
return nil, err
}

return &vo, nil
}

成功返回如下JSON:

1
2
3
4
5
6
json复制代码{
"errcode":0,
"errmsg":"ok",
"ticket":"caLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
"expires_in":7200
}

3.3 根据获取到的 ticket 来生成签名

  • 签名说明

微信开发中,ticket 通常用于获取 JS-SDK 的配置参数,包括签名(signature)。这些参数使得你能够在网页上使用微信的 JS-SDK。以下是一个基本的步骤说明如何使用获取到的 ticket 来生成签名:

准备用于签名的参数:nonceStr(随机字符串), timestamp(时间戳), url(当前网页的 URL,不包含 # 及其后面部分)和 jsapi_ticket。

将这些参数按照字段名的ASCII 码从小到大排序(字典序),并且使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串。
对拼接后的字符串进行 SHA1 加密,得到的结果即是签名(signature)。

  • golang代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
go复制代码package main  

import (
"crypto/sha1"
"encoding/hex"
"fmt"
"sort"
"strings"
"time"
)

// GenerateSignature 生成微信JS-SDK的签名
func GenerateSignature(nonceStr, url, jsapiTicket string) string {
// 实时获取当前时间戳
timestamp := fmt.Sprintf("%d", time.Now().Unix())

// 准备用于签名的原始数据
params := map[string]string{
"jsapi_ticket": jsapiTicket,
"noncestr": nonceStr,
"timestamp": timestamp,
"url": url,
}

// 对参数名进行排序并拼接
var keys []string
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)

var signStrings []string
for _, key := range keys {
signStrings = append(signStrings, fmt.Sprintf("%s=%s", key, params[key]))
}
signString := strings.Join(signStrings, "&")

// 使用SHA1进行签名
h := sha1.New()
h.Write([]byte(signString))
signature := hex.EncodeToString(h.Sum(nil))

return signature
}

func main() {
nonceStr := "Wm3WZYTPz0wzccnW" // 这个应该是随机生成的字符串
url := "http://mp.weixin.qq.com?params=value" // 你的网页URL
jsapiTicket := "bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA" // 从微信服务器获取的jsapi_ticket

// 生成签名
signature := GenerateSignature(nonceStr, url, jsapiTicket)
fmt.Println("生成的签名是:", signature)
}

在这个示例中,GenerateSignature 函数接受四个参数:nonceStr(随机字符串),timestamp(时间戳),url(当前网页的URL),和jsapiTicket(从微信服务器获取的票据)。函数内部会对这些参数进行字典排序,拼接成一个待签名的字符串,然后使用SHA1算法生成签名。

请注意,为了简化示例,这里直接提供了nonceStr、timestamp、url和jsapiTicket的示例值。在实际应用中,我们需要根据具体情况动态获取这些值。特别是jsapiTicket,你需要先从微信服务器获取。

最后,main 函数中调用了 GenerateSignature 并打印出了生成的签名。我们可以将这段代码集成到我们的Go应用中,并根据需要进行适当的修改和调整。

  • noncestr 的含义是什么?应该如何动态获取?

noncestr是一个随机字符串,通常用于确保请求的唯一性和安全性。在微信JS-SDK、微信支付等接口中,noncestr 作为一个重要的参数,用于防止重放攻击和确保请求的时效性。

在微信开发中,noncestr 通常需要你自己生成,并确保其唯一性和随机性。以下是几种生成 noncestr 的方法:

  1. 使用随机数或UUID:
    你可以使用编程语言中的随机数生成函数或者UUID生成库来创建一个唯一的字符串。例如,在Go语言中,你可以使用crypto/rand库生成一个随机数,并将其转换为字符串,或者使用第三方库如github.com/google/uuid来生成一个UUID。
  2. 时间戳结合随机数:
    为了增加noncestr的复杂性,你也可以将当前的时间戳与随机数结合起来使用。这样既可以保证每次请求的noncestr都是唯一的,也能通过时间戳增加一定的时效性验证。
  3. 使用安全的随机数生成器:
    在安全性要求较高的场景下,应使用安全的随机数生成器来产生noncestr,以确保其不可预测性。

golang动态获取 noncestr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
go复制代码package main  

import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
)

// GenerateNonceStr 生成一个随机的 noncestr
func GenerateNonceStr(length int) (string, error) {
bytes := make([]byte, length/2) // 因为一个字节可以表示为两个16进制数字
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}

func main() {
nonceStr, err := GenerateNonceStr(32) // 生成一个32个字符长的noncestr
if err != nil {
log.Fatalf("Failed to generate nonceStr: %v", err)
}
fmt.Println("Generated nonceStr:", nonceStr)
}

得到的签名:

1
复制代码e851776f519a6c8d716bc61a5dec87b042d14c3e

四、H5页面配置与分享设置

注意:从这一步开始,后面的部分属于前端工作

4.1 引入微信JS-SDK

在需要调用 JS 接口的页面引入如下 JS文件,(支持https):res.wx.qq.com/open/js/jwe…

如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:res2.wx.qq.com/open/js/jwe…

备注:支持使用 AMD/CMD 标准模块加载方法加载

4.2 通过 config 接口注入权限验证配置

所有需要使用 JS-SDK 的页面必须先注入配置信息,否则将无法调用(同一个 url 仅需调用一次,对于变化 url 的SPA的web app可在每次 url 变化时进行调用,目前 Android 微信客户端不支持 pushState 的H5新特性,所以使用 pushState 来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。

注意:

  • 引入JS文件后,直接执行下列代码
  • debug: true 用来调试的,如果不想alert弹出,改成false即可
  • alert 弹出框中 errMsg 不一定就是错误,知识提示信息,例如 updateAppMessageShareData:ok 代表的是updateAppMessageShareData接口是没有问题的。
  • 签名用的 noncestr 和 timestamp 必须与 wx.config 中的 nonceStr 和 timestamp 相同。
  • jsApiList 接口列表:developers.weixin.qq.com/doc/offiacc…
  • jsApiList 接口列表 例如 wx.updateAppMessageShareData({ 配置 }) jsApiList填写 [‘updateAppMessageShareData’] 即可
1
2
3
4
5
6
7
8
csharp复制代码wx.config({
debug: true, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名(这里用上面第三步得到的签名)
jsApiList: [] // 必填,需要使用的 JS 接口列表
});

4.3 通过 ready 接口处理成功验证

在wx.ready函数中配置onMenuShareTimeline、onMenuShareAppMessage等接口实现微信分享功能,并设置自定义的分享标题、描述、缩略图及链接。

1
2
3
lua复制代码wx.ready(function(){
// config信息验证后会执行 ready 方法,所有接口调用都必须在 config 接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在 ready 函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在 ready 函数中。
});

4.4 通过 error 接口处理失败验证

使用wx.error函数处理验证失败的情况。

1
2
3
lua复制代码wx.error(function(res){
// config信息验证失败会执行 error 函数,如签名过期导致验证失败,具体错误信息可以打开 config 的debug模式查看,也可以在返回的 res 参数中查看,对于 SPA 可以在这里更新签名。
});

4.5 js代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
javascript复制代码// 模拟从后端获取签名等配置信息的函数  
function getWechatConfig(url) {
// 通常情况下,以下代码应由后端服务生成并返回给前端
// 这里仅为示例,实际项目中,signature, nonceStr, timestamp 应该在服务器端生成
const appId = 'YOUR_APP_ID'; // 替换为你的AppID
const jsApiList = ['onMenuShareTimeline', 'onMenuShareAppMessage'];

// 以下为模拟数据,实际开发中应由服务器端提供
const nonceStr = 'Wm3WZYTPz0wzccnW'; // 随机字符串
const timestamp = Math.floor(Date.now() / 1000).toString(); // 当前时间戳
const signature = 'SOME_SIGNATURE_STRING'; // 签名,应由后端根据算法生成

return {
appId,
nonceStr,
timestamp,
signature,
jsApiList
};
}

// 调用微信JS-SDK配置
function initWechatSDK() {
const currentUrl = encodeURIComponent(location.href.split('#')[0]);
const config = getWechatConfig(currentUrl);

wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: config.appId, // 必填,公众号的唯一标识
timestamp: config.timestamp, // 必填,生成签名的时间戳
nonceStr: config.nonceStr, // 必填,生成签名的随机串
signature: config.signature, // 必填,签名
jsApiList: config.jsApiList // 必填,需要使用的JS接口列表
});

wx.ready(function () {
// 在这里调用 API
configShare();
});

wx.error(function (res) {
// config信息验证失败会执行error函数,如签名过期(7200s)等原因触发error函数
console.error('微信JS-SDK配置失败', res);
});
}

// 配置微信分享
function configShare() {
const shareData = {
title: '自定义分享标题', // 分享标题
desc: '自定义分享描述', // 分享描述
link: 'https://example.com', // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: 'https://example.com/path/to/image.jpg', // 分享图标
type: '', // 分享类型,music、video或link,不填默认为link
dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
success: function () {
// 用户点击了分享后执行的回调函数
console.log('分享成功');
}
};

wx.onMenuShareTimeline(shareData); // 分享到朋友圈
wx.onMenuShareAppMessage(shareData); // 分享给朋友
wx.onMenuShareQQ(shareData); // 分享到QQ
wx.onMenuShareWeibo(shareData); // 分享到微博
}

// 初始化微信JS-SDK
initWechatSDK();

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

未来已来:解锁AGI的无限潜能与挑战

发表于 2024-04-22

DALL·E 2024-04-22 16.31.50 - A futuristic cityscape with a humanoid robot sitting on a bench, looking thoughtful. The robot is sleek, with a shiny metal surface, and appears to be.webp

引言

假设你有一天醒来,发现你的智能手机不仅提醒你今天的日程,还把你昨晚做的那个奇怪的梦解释了一番,并建议你可能需要减少咖啡摄入量——这不是科幻电影的情节,而是人工通用智能(AGI)潜在的未来场景。

目前,我们所使用的AI系统,比如能够识别你的猫照片并自动打上“可爱”的标签的那种,它们非常擅长处理特定任务。但让我们想象一下,一个不仅仅擅长单一任务,而是可以像人类一样进行思考、学习和适应的智能系统。这正是AGI的承诺所在——不只是一个问题解决者,更是一个能在多领域展现高水平智能的全能型选手。

传统的AI像是那种只会做家庭作业的模范生,而AGI更像是一个多才多艺的全能学生,既能解数学题,也能帮你制定健身计划,甚至在你心情不好时,还能做个心理咨询师。

AGI的核心目标,是创造一个能够执行与人类智能相当的任何智能任务的机器。听起来是不是像是科幻电影里的超级计算机?但这不仅仅是科幻——这是科技界正在朝着这个方向迈进的真实目标。

想象一下,一个能够自主创新、进行艺术创作,甚至进行科学研究的AI。我们正处于这样一个激动人心的技术革命的边缘,这不只是关于机器的进步,更是关于我们如何理解智能、意识以及我们自己的探索。

那么,让我们一起揭开AGI的神秘面纱,看看它究竟是如何一步步接近人类智能的,同时也让我们思考一下,当AGI真的来临时,我们该如何与这些比我们还要聪明的机器共处。不用担心,我保证这不会太无聊——至少不会比你昨晚的梦更无聊。

技术发展现状

让我们一起跳入AI的现状,看看它如何一步步变革我们的世界,甚至可能正在悄悄地计划接管宇宙。好吧,也许接管宇宙还为时尚早,但AI在增强应用、自动化复杂任务、乃至成为操作系统的核心组件方面的确取得了显著进步。

应用增强阶段

首先,AI正在让现有应用变得更加智能。比如说,现在的自然语言处理技术已经可以让机器理解人类的语言——不仅仅是字面意义,还包括那些隐藏的讽刺和幽默。是的,现在你的电脑或许能比你的朋友更早发现你的笑话其实一点也不好笑。

图像识别也取得了飞速发展。你的手机现在不只是可以识别出照片中是你的脸,还能分辨出你是在微笑还是在皱眉。有一天它可能会建议你:“嘿,这张自拍再试一次,上次你的笑容有点勉强。”

AI自动化阶段

接下来,AI如何自动化复杂任务呢?拿自动驾驶汽车来说,它们已经能在没有人类司机的情况下安全行驶,尽管偶尔还是会对停车票感到困惑。而在数据分析方面,AI可以处理和分析大规模数据集,找出趋势和模式,比任何人类数据科学家都要快——只是它还没有学会如何在不让人困惑的情况下展示这些数据。

赋能操作系统阶段

更进一步,AI正逐步成为操作系统的核心组件。想象一下,你的电脑操作系统不只是帮你管理文件和运行程序,还能预测你下一步需要什么文件,甚至在你还没想到之前就已经帮你准备好了。它变得如此聪明,以至于有一天你可能会误以为你的电脑在跟你玩捉迷藏。

OpenAI前全球商业化负责人Zack Kass,他认为AGI带来的变革将是翻天覆地的——想象一下通过一副眼镜就能控制家中的所有电器。不过,如果你像我一样,经常忘记自己把眼镜放在哪里,那么在找到眼镜之前,可能连开个灯都成问题。

AGI原型模型代码示例

简化的AGI模型框架

使用PyTorch构建。这个模型旨在通过增量学习适应不同的任务,模拟AGI在多任务学习和适应新环境的能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
python复制代码pythonCopy codeimport torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# 定义一个简单的神经网络模型,具有可扩展的学习能力
class AGINet(nn.Module):
def __init__(self):
super(AGINet, self).__init__()
self.layer1 = nn.Linear(10, 20)
self.layer2 = nn.Linear(20, 10)
self.task_adaptation_layer = nn.Linear(10, 10)

def forward(self, x, task_id):
x = torch.relu(self.layer1(x))
x = torch.relu(self.layer2(x))
# 通过不同的任务适应层来适应不同的任务
if task_id == 1:
x = torch.sigmoid(self.task_adaptation_layer(x))
elif task_id == 2:
x = torch.tanh(self.task_adaptation_layer(x))
return x

# 实例化模型
model = AGINet()

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 假设我们有两种任务的数据
data1 = torch.randn(100, 10) # 任务1的数据
target1 = torch.randint(0, 2, (100,)) # 任务1的目标

data2 = torch.randn(100, 10) # 任务2的数据
target2 = torch.randint(0, 2, (100,)) # 任务2的目标

# 训练模型以适应两种任务
for epoch in range(10): # 训练10个周期
for data, target, task_id in [(data1, target1, 1), (data2, target2, 2)]:
optimizer.zero_grad()
output = model(data, task_id)
loss = criterion(output, target)
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}, Task {task_id}, Loss: {loss.item()}")

# 测试模型的泛化能力
test_data = torch.randn(10, 10)
test_task_id = 1 # 测试任务1
test_output = model(test_data, test_task_id)
print("Test output:", test_output)

这段代码展示了一个具有初步适应性的模型框架,通过不同的任务适应层来处理不同的任务。在这个简化的例子中,模型尝试学习并区分两种不同类型的输入数据,并对应不同的处理方式。这种模型是理解AGI概念的一种方式,展示了机器学习模型如何适应不同的任务,虽然离真正的AGI还有较大的距离。

多模态AI模型的实现

一个简单的多模态AI模型,该模型能同时处理文本和图像输入。这种模型可以用于理解社交媒体帖子的情感,结合文本描述和图片内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
python复制代码pythonCopy codeimport torch
from torch import nn
from torchvision.models import resnet18
from transformers import BertModel, BertTokenizer

class MultiModalModel(nn.Module):
def __init__(self):
super(MultiModalModel, self).__init__()
self.text_model = BertModel.from_pretrained('bert-base-uncased')
self.image_model = resnet18(pretrained=True)
self.classifier = nn.Linear(self.text_model.config.hidden_size + self.image_model.fc.out_features, 2)

def forward(self, input_ids, attention_mask, images):
text_features = self.text_model(input_ids=input_ids, attention_mask=attention_mask)[1]
image_features = self.image_model(images)
combined_features = torch.cat((text_features, image_features), dim=1)
output = self.classifier(combined_features)
return output

# 示例用法
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "This is a positive message with a happy image."
inputs = tokenizer(text, return_tensors='pt')
images = torch.randn(1, 3, 224, 224) # 假设的图像数据
model = MultiModalModel()
output = model(inputs['input_ids'], inputs['attention_mask'], images)

AI在生命科学中的应用

用于预测蛋白质结构或药物相互作用,展示AI如何在生物科技领域中发挥作用,尤其是在新药研发和个性化医疗中的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
python复制代码pythonCopy code# 假设使用深度学习模型预测药物分子的活性
import torch
import torch.nn as nn
from rdkit import Chem
from rdkit.Chem import AllChem

class DrugActivityModel(nn.Module):
def __init__(self):
super(DrugActivityModel, self).__init__()
self.fc1 = nn.Linear(2048, 1024) # 2048是分子指纹的大小
self.fc2 = nn.Linear(1024, 512)
self.fc3 = nn.Linear(512, 1)

def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = torch.sigmoid(self.fc3(x))
return x

def molecule_to_fingerprint(molecule, n_bits=2048):
fingerprint = AllChem.GetMorganFingerprintAsBitVect(molecule, 2, n_bits)
array = np.zeros((0,), dtype=np.int8)
DataStructs.ConvertToNumpyArray(fingerprint, array)
return array

# 示例用法
smiles = "CCO"
molecule = Chem.MolFromSmiles(smiles)
fingerprint = molecule_to_fingerprint(molecule)
fingerprint_tensor = torch.tensor([fingerprint], dtype=torch.float)
model = DrugActivityModel()
predicted_activity = model(fingerprint_tensor)

技术趋势

当我们谈论AI的未来趋势时,你可能会想到的是科幻电影里的超级计算机,或是那些能够预测未来的神秘装置。不过,现实中的AI技术发展可能比你想象的还要酷,尤其是在多模态AI和生命科学领域的应用上。

多模态AI

首先,让我们来聊聊多模态AI。这不是一部混合了多种口味的奇异饮料,而是一种能够处理和理解多种类型数据(如文本、图像、声音)的AI。想象一下,一个AI系统不仅可以读懂你写的邮件,还能理解你语音留言的语调,并分析你发送的表情包。它甚至可以通过你的笑声判断你今天的心情。是的,这意味着未来的AI可能会成为理解你最多的朋友。

这种多模态AI的应用范围极广,从智能助手到安全监控系统,都可以通过整合不同类型的数据输入来提供更精准、更个性化的服务。想象一下,你的智能家居助手不仅能控制温度和灯光,还能根据你的表情和语气来调节家中的氛围——这简直就像是家里多了一个心灵手巧的家政员。

AI与生命科学交融

接下来是AI与生命科学的交融。如果你认为AI和生物科技的结合只发生在那些高端实验室里,那么你可能需要更新一下观念了。现在,AI正在帮助科学家设计新药,通过分析复杂的生物数据来预测药物的效果。AI还与3D打印技术相结合,打印出可以用于医疗植入或生物组织工程的结构。

而脑机接口技术,这听起来像不像是从科幻小说里直接跳出来的?这种技术允许我们的大脑直接与计算机系统交流。想象一下,未来你可能不需要使用任何物理设备,只需通过思考就能与你的电脑或手机交互。虽然这听起来可能会让一些隐私权倡导者感到不安(确实有点可怕),但这也预示着我们在治疗神经疾病和增强人体功能方面的巨大潜力。

未来发展

拿好你的安全帽,因为我们即将深入探讨AGI将如何在未来搅动一池春水——或者说,电子汤?无论你喜欢哪种比喻,我们都能保证这将是一场激动人心的旅程。

AGI在能源领域的应用

想象一下,你家的智能系统不仅能根据天气预报调整空调温度,还能预测整个社区的能源需求,并在你还在梦乡时优化整个城市的电网。这就是AGI在能源领域的魔法——不仅能节省你的电费,还可能帮助我们拯救地球。谁说英雄必须穿披风?

AGI与算力的结合

随着AGI的发展,我们的算力需求也在爆炸式增长。未来的算力可能不仅仅是一种资源,更是一种艺术形式。想象一下,算力如此之大,以至于我们开始用它来做艺术创作——“我这幅作品用了一百万小时的GPU时间,你觉得怎么样?”显然,我们的算力需求已经超越了传统的框架,可能需要一种全新的计算架构来应对这种需求。

AGI在机器人技术中的应用

机器人已经不再是简单的吸尘器或是制造线上的自动臂。在AGI的帮助下,未来的机器人可能会成为我们的个人助理,甚至是朋友。他们会理解你的感情,记住你的喜好,甚至在你需要时给予安慰。但愿他们不会因为被迫看太多肥皂剧而感到厌烦。

数据生产与“真实”概念的变革

在AGI的帮助下,我们可能会创造出比实际人类还要多的数据。但在这个“AI纪元”,真实的体验可能变得更加珍贵。想象一下,未来的你可能需要支付高额费用,只为体验一次没有数字增强的现实生活——“真实”体验可能成为新的奢侈品。

专家对AGI未来的预测

Zack Kass认为到2030年我们将进入通用人工智能的时代。他预测这将是一场比任何科技革命都要深刻的变化,可能最终导致我们不再需要钱包,因为能源和许多服务变得免费。而Demis Hassabis则梦想着一个由AGI驱动的未来,其中科技不仅解决了我们的所有问题,还帮助我们创造了新的艺术形式。

在这一点上,我们应当保持乐观,同时也不能忽视潜在的伦理和隐私问题。正如所有伟大的科技进步一样,责任感将是我们最重要的伴侣。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在本地跑一个AI模型(7) - 你打草稿,模型来画

发表于 2024-04-22

本文是Stable Diffusion系列第三篇。

前两篇文章我们介绍了在本地运行Stable Diffusion模型的方法,以及使用LoRA对模型生成的图片进行微调。

本篇文章中,我们将介绍两种技术来控制图像的生成过程,对模型图像生成进行引导,使得生成的图像更符合我们的需求。这样生成的图片,更具有商业价值。

图像生成引导技术

在 Stable Diffusion等文本转图像模型中,”对模型图像生成进行引导”是指通过提供额外的信息来控制图像生成过程,使其生成更符合预期的图像。引导的方法有多种,比如前两篇文章提及的“引导词”和LoRA都是引导技术的一种。在本篇文章中,重点介绍的是“使用参考图像”进行引导的方法,也就是Image to Image。试想一下:你先用草稿将绘画元素勾勒出来,然后利用该技术即可让AI对作品细节进行补充。

ControlNet和Adapter

  • ControlNet是一种基于神经网络的技术。ControlNet使用一个神经网络来学习图像和文本描述之间的关系,并利用该关系来引导图像生成过程。
  • Adapter是一种基于提示嵌入prompt embedding的技术。提示嵌入是将文本提示转换为向量表示的过程。Adapter通过将提示嵌入与图像嵌入进行结合,来引导图像生成过程。

这个表格展示了这两种技术的工作原理及优缺点:

特性 Adapter ControlNet
工作原理 基于提示嵌入 基于神经网络
优点 易于使用、灵活 效果好、可控性强
缺点 效果可能不佳、需要额外的训练数据 使用复杂、需要训练数据

controlnet-aux

在开始前,我们先下载一个工具:

1
shell复制代码pip install controlnet-aux mediapipe

controlnet-aux提供了多种辅助模型,称为Detector。这些 Detector可以分析图像并提取特定的信息,然后将这些信息作为条件传递给 ControlNet,从而更精细地控制图像生成过程。这一步骤我们称之为生成“引导图”。以下是一些常见的Detector模型及其简介:

  • CannyDetector:
    • 功能:利用 Canny 边缘检测算法提取图像的边缘信息。
    • 应用场景:生成卡通插画、突出图像中的线条和结构、控制图像的锐利程度。
  • HEDdetector:
    • 功能:利用 HED 边缘检测算法提取图像的边缘信息,与 Canny 边缘检测相比,HED 能够检测更复杂的边缘。
    • 应用场景:类似于 CannyDetector,但适用于细节更丰富的图像。
  • LineartDetector:
    • 功能:提取图像中的线条艺术信息,例如漫画和素描中的线条。
    • 应用场景:生成线条艺术图像、将照片转换为线条艺术风格。
  • MidasDetector:
    • 功能:估计图像中物体的深度信息。
    • 应用场景:生成具有三维立体感和景深效果的图像、控制图像中景物的前后关系。
  • MLSDdetector (Multi-Line Style Detector):
    • 功能:提取图像中多种线条的样式信息,例如粗细、颜色和纹理。
    • 应用场景:生成具有特定线条风格的图像,例如漫画的不同类型或者艺术流派。
  • NormalBaeDetector:
    • 功能:估计图像中物体的表面法线信息,可以理解为物体表面的朝向。
    • 应用场景:生成更加逼真写实的图像,例如控制光照效果和材质质感。
  • OpenposeDetector:
    • 功能:检测图像中人体关键点的位置,例如头部、肩部、肘部等。
    • 应用场景:生成包含人物的图像,并控制人物的姿势和动作。
  • ZoeDetector (Object detector):
    • 功能:检测并识别图像中的物体类别。
    • 应用场景:控制图像中要生成或排除的物体类型,例如生成特定场景或物体组合的图像。

要使用这些Detector,需要先下载模型,地址:huggingface.co/lllyasviel/…。你需要哪些Detector就下载哪些模型。省事的话就全部下载。

各种Detector对比

首先写一段代码运行Detector:

1
2
3
4
5
6
7
8
9
10
python复制代码from controlnet_aux import HEDdetector
from diffusers.utils import load_image, make_image_grid

device = 'mps'
original_image = load_image("data/3.jpg")
hed = HEDdetector.from_pretrained("your/path/controlnet-annotators").to(device)

hed_image = hed(original_image)
image_grid = make_image_grid([original_image, hed_image], rows=1, cols=2)
image_grid.save("data/out.jpg")

代码很简单,就不做解释了,下面是各种Detector的效果演示:

  • CannyDetector:
  • HEDdetector:
  • LineartDetector:
  • MidasDetector:
  • MLSDdetector (Multi-Line Style Detector):
  • NormalBaeDetector:
  • OpenposeDetector:
  • ZoeDetector (Object detector):

各种Controlnet对比

以上我们使用Detector生成了各种“引导图”,接下来我们使用Controlnet对引导图进行创作。老规矩,先去hugging face下载模型,我们这次测试的Controlnet都是sdxl的,具体原因参考前两篇文章:

  • controlnet-canny-sdxl-1.0
  • controlnet-openpose-sdxl-1.0
  • controlnet-zoe-depth-sdxl-1.0
  • controlnet-depth-sdxl-1.0

编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
python复制代码from diffusers import StableDiffusionXLControlNetPipeline, ControlNetModel, DPMSolverSinglestepScheduler
from diffusers.utils import load_image, make_image_grid
from controlnet_aux import CannyDetector


device = 'mps'
original_image = load_image("data/3.jpg")
canny = CannyDetector()
canny_image = canny(original_image)

controlnet = ControlNetModel.from_pretrained(
"your/path/controlnet-canny-sdxl-1.0",
use_safetensors=True
)

pipe = StableDiffusionXLControlNetPipeline.from_single_file(
"your/path/dreamshaperXL_v21TurboDPMSDE.safetensors",
controlnet=controlnet,
use_safetensors=True
).to(device)
pipe.scheduler = DPMSolverSinglestepScheduler.from_config(pipe.scheduler.config, use_karras_sigmas=True)
# pipe.enable_model_cpu_offload()

prompt = "masterpiece, 1girl, long hair, asian, sundress, cartoon"
negative_prompt = 'low quality, bad quality, sketches'

image = pipe(
prompt,
negative_prompt=negative_prompt,
image=canny_image,
# controlnet_conditioning_scale=0.5,
height=1024,
width=576,
num_inference_steps=6,
guidance_scale=2,
).images[0]
image_grid = make_image_grid([original_image, image], rows=1, cols=2)
image_grid.save("data/out.jpg")

提示词与上一篇文章一模一样,让我们来看一下生成的图片。

  • 使用CannyDetector和CannyControlnet
  • 使用OpenposeDetector和PoseControlnet
  • 使用MidasDetector和MidasControlnet

各种Adapter对比

与Contronet一样,先去hugging face上下载模型,这次我们使用腾讯的t2i-adapter进行测试:

  • t2i-adapter-openpose-sdxl-1.0
  • t2i-adapter-depth-zoe-sdxl-1.0
  • t2i-adapter-sketch-sdxl-1.0
  • t2i-adapter-lineart-sdxl-1.0
  • t2i-adapter-canny-sdxl-1.0
  • t2i-adapter-depth-midas-sdxl-1.0

编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
python复制代码from diffusers import T2IAdapter, StableDiffusionXLAdapterPipeline, DPMSolverSinglestepScheduler
from diffusers.utils import load_image, make_image_grid
from controlnet_aux import CannyDetector

device = 'mps'
original_image = load_image("data/3.jpg")
canny = CannyDetector()
canny_image = canny(original_image)

adapter = T2IAdapter.from_pretrained("/Users/lei/Downloads/t2i-adapter-canny-sdxl-1.0")

pipe = StableDiffusionXLAdapterPipeline.from_single_file(
"your/path/dreamshaperXL_v21TurboDPMSDE.safetensors",
# controlnet=controlnet,
adapter=adapter,
use_safetensors=True
).to(device)
pipe.scheduler = DPMSolverSinglestepScheduler.from_config(pipe.scheduler.config, use_karras_sigmas=True)
# pipe.enable_model_cpu_offload()

prompt = "masterpiece, 1girl, long hair, asian, sundress, cartoon"
negative_prompt = 'low quality, bad quality, sketches'

image = pipe(
prompt=prompt,
negative_prompt=negative_prompt,
image=canny_image,
# controlnet_conditioning_scale=0.5,
height=1024,
width=576,
num_inference_steps=6,
guidance_scale=2,
).images[0]
image_grid = make_image_grid([original_image, image], rows=1, cols=2)
image_grid.save("data/out.jpg")
  • 使用CannyDetector和CannyAdapter
  • 使用OpenposeDetector和PoseAdapter
  • 使用CannyDetector和SketchAdapter
  • 使用HEDdetector和SketchAdapter
  • 使用LineartDetector和LineartAdapter
  • 使用MidasDetector和MidasAdapter

总结

从图片质量可以看出,ControlNet对于引导图的遵循和生成图片的质量都较Adapter高,具体使用情况如何,大家在使用前还是自己测试一下。

本文首发于:babyno.top/posts/2024/…

公众号:机器人小不

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

掌握 Swift 中的 reduce 操作符,使你的代码更高

发表于 2024-04-22

前言

Swift 的 Sequence 类型有一个强大的操作符叫做 reduce,它允许你将序列的所有元素组合成一个单一的值。在处理来自 App Store Connect API 的响应时,我一直在反复使用它,我觉得写一篇关于它的博客文章是个好主意。

reduce 操作符有两种不同的签名,详细代码如下:

1
2
3
4
5
swift复制代码// 使用初始结果进行 reduce
@inlinable public func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (_ partialResult: Result, Self.Element) throws -> Result) rethrows -> Result

// 将结果归并到初始结果中
@inlinable public func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (_ partialResult: inout Result, Self.Element) throws -> ()) rethrows -> Result

这两个操作符在给定相同输入时实现相同的结果:它们从一个初始的 inout 值开始,遍历序列中的所有元素,并将它们作为参数传递给提供的闭包。由于初始值是作为 inout 参数传递的,闭包可以根据序列中的当前元素对其进行修改。每次迭代的更新值然后作为下一次迭代中闭包的第一个参数传递。

虽然它们看起来非常相似 - 它们都具有 O(n) 的复杂度,并且可以互换使用 - 但基于结果类型的不同,它们具有不同的效率影响。例如,当结果是像 Array 或 Dictionary 这样的写时复制类型时,你应该使用 into 变体。

使用初始结果进行 reduce

让我们来看一个非常简单的例子,以理解 reduce 操作符的工作原理。假设你有一个整数数组,你想要计算所有元素的总和作为结果。如果你不知道 reduce 操作符,你可以写一个像这样的函数,详细代码如下:

1
2
3
4
5
6
7
swift复制代码func sumAllElements(of numbers: [Int]) -> Int {
var sum = 0
for number in numbers {
sum += number
}
return sum
}

虽然这个函数完全有效,但它并不是最优雅的解决方案。你可以在一行代码中使用 reduce 操作符来实现相同的结果,代码如下:

1
2
3
swift复制代码func sumAllElements(of numbers: [Int]) -> Int {
numbers.reduce(0) { $0 + $1 }
}

或者更好的是,你可以直接将 + 操作符作为闭包传递,代码如下:

1
2
3
swift复制代码func sumAllElements(of numbers: [Int]) -> Int {
numbers.reduce(0, +)
}

使用初始结果进行 reduce

现在让我们来看一个稍微复杂一些的例子。假设我们有一个 ScreenshotBundle 数组,其中每个 bundle 都有一个名称和一个指向截图的 URL 列表。我们的 UI 需要根据用户的选择找到具有特定名称的截图 bundle,并在图像视图中显示所有的 URL:

这是我们在 Helm 中使用的代码变体,Hidde 和我正在构建 Helm,这是一款旨在使 App Store Connect 的用户更轻松、更愉快地发布应用程序和更新的应用。

我们可以通过保持 ScreenshotBundle 数组不变,然后搜索具有特定名称的 bundle 来实现这一点,核心代码如下:

1
2
3
4
5
6
7
8
swift复制代码struct ScreenshotBundle {
let name: String
let urls: [URL]
}

func find(bundleWithName name: String, in bundles: [ScreenshotBundle]) -> ScreenshotBundle? {
bundles.first(where: { $0.name == name })
}

虽然这种方法可行,但它并不是最有效的。first(where:) 函数的复杂度为 O(n),你可以想象,如果数组中的元素数量很大,这可能会成为一个问题。

相反,你可以使用 reduce 操作符一次将 ScreenshotBundle 数组转换为一个字典,其中键是 bundle 的名称,值是 bundle 本身。这样,你就可以在 O(1) 的时间复杂度内找到具有特定名称的 bundle,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
swift复制代码struct ScreenshotBundle {
let name: String
let urls: [URL]
}

func format(bundles: [ScreenshotBundle]) -> [String: ScreenshotBundle] {
bundles.reduce(into: [String: ScreenshotBundle]()) { result, bundle in
result[bundle.name] = bundle
}
}

func find(bundleWithName name: String, in bundles: [String: ScreenshotBundle]) -> ScreenshotBundle? {
bundles[name]
}

通过理解和掌握 reduce 操作符,你可以更高效地处理 Swift 中的集合类型,使你的代码更加简洁和易于理解。这种强大的操作符不仅能够提高代码的性能,还能提升开发效率,让你更轻松地应对复杂的数据处理任务。

在实际开发中,应该根据具体情况选择合适的 reduce 操作符,以确保代码的性能和可读性。通过合理地利用 reduce 操作符,你可以编写出更加优雅和高效的 Swift 代码,从而提升应用程序的质量和用户体验。

了解 reduce 操作符的工作原理并熟练运用它,将会使你成为一个更加出色的 Swift 开发者,为你的项目带来更大的成功和成就。

总结

本文全面介绍了 Swift 中的 reduce 操作符,这是一个强大的工具,可以将序列的元素组合成单个值。文章解释了 reduce 操作符的两种不同签名,并通过代码示例演示了它们的用法。

其中讨论了如何使用带有初始结果的 reduce,演示了如何以简洁而优雅的方式计算数组中元素的总和。然后,它探讨了带有初始结果的 reduce 变体,展示了如何将数组高效地转换为字典。

本文对 Swift 开发人员来说是一份宝贵的资源,提供了关于 reduce 操作符的功能和应用的见解,使他们能够编写更高效、更优雅的代码。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

偷偷分享下我们公司的研发规范~

发表于 2024-04-22

大家好,我是程序员鱼皮。前几天我分享了自己 创业一周年的复盘总结 ,其中提到了一点:随着团队的扩大,我们会更注重研发规范和技术沉淀。

有程序员朋友就问了:啥是研发规范?

还有朋友表示:鱼皮别拿咱当外人,把你们公司的研发规范发来看看?

可以,必须安排!

这篇文章就给大家简单分享下我们公司的研发规范,不过在开始前必须要明确 2 点:

  1. 每个团队都应该根据情况定制自己的研发规范,别人的规范仅供参考,未必最适合你们团队。
  2. 篇幅有限,本文仅分享一些我认为很重要的规范,并且移除了我们自己的敏感信息。

一、项目整体研发流程

1)团队共同确认目标和规划

开会讨论,产出目标和规划文档

2)产品调研和需求分析

产出调研报告和需求分析文档

3)需求评审

开需求评审会,明确要做的需求和工作,评估工作量并明确工作时间节点。

4)方案设计

产出方案设计文档,比如数据库表设计、页面设计、接口设计等。

5)研发

包括各自开发、单元测试、前后端联调等

6)测试和验收

包括研发自测、产品验收、组内验收等

7)代码提交

提交可上线的代码,需要由负责人审查,通过后可合并

8)部署上线

将代码发布到服务器上,组内进行上线通知并更新上线文档,上线后需要自行验证

9)产品迭代

持续收集用户对新功能的反馈、并进行数据分析,从而验证改动效果,便于下一轮的更新迭代。

二、开发规范

开发前注意事项

1)确保自己充分理解了业务和需求,需要先进行整体的方案设计;尤其是对于重要需求和核心业务,必须先跟组内同学核对方案并通过后,才能下手开发,避免重复工作。

2)先熟悉项目再开发,建议阅读项目文档、项目代码、接口文档、前端组件文档等。

3)慎重引入新的依赖或类库、或者升级版本,重大依赖变更需要和组内其他成员确认。

4)熟悉团队已实现的功能和代码,尽量复用,避免重复开发。

5)熟悉团队内部的研发规范,并在 IDE 中进行相应的配置,比如前端配置 ESLint、Prettier 等代码规范插件。

开发中注意事项

1)开发新功能时,确保从项目仓库拉取 最新主分支 的代码。

2)每个功能都要新建自己的分支进行开发,千万不要直接修改主分支的代码!注意分支名称要使用英文、足够语义化,不要和其他人的混淆。

3)开发时,尽量复用现有的功能、模块、类、方法、对象代码。有现成的代码,就不要再重复编写。如无法复用,可以适当通过注释说明。

4)开发时,遵循团队内部的研发规范,尽量参考现有项目代码的写法,尤其是不要使用和原项目不一致的格式、命名、写法,避免特立独行。

5)开发过程中,有任何不明确的地方,不要凭空猜测,及时去联系项目的其他成员或负责人确认。

6)开发过程中,每隔一段时间(比如 1 - 3 天)可以使用 git pull 同步一下最新的主分支代码,防止合并代码冲突。

7)开发过程中,注意整体时间进度的把控,先完成再完美,有风险时及时反馈。

8)开发时,需要格外注意对异常情况的捕获和处理。

9)每个分支尽量保证纯净,尽量减少每次开发和提交时改动的代码量。建议每次开分支只改一个功能、Bug 或模块,不要把多个不相关的功能写在一起,并且非必要不修改。

10)完成部分功能开发后,一定要自测!自测时,可以 Mock 假数据。注意一定不要在线上测试、一定不要影响线上数据!

三、代码提交规范

1)只有通过测试和产品验收的代码,才能够发起合并到主分支的 PR 请求。在这之前可以提交到自己的分支。

2)发起合并到主分支的 PR 前,一定要完整阅读 3 遍自己的代码,避免不规范的写法和无意义的改动。

3)每次合并尽量只专注于一个功能或改动,避免多个功能耦合在一起合并,提高审查效率并降低改动风险。

4)每次提交时,需要在 commit 信息中提供代码改动说明,还可以通过关联需求文档、测试用例、方案文档、效果截图等方式进行补充说明。

commit 信息可参考《约定式提交》文档,但不做强制要求。

5)除非特殊情况,否则所有的代码必须经过至少一位项目负责人 Code Review 审核通过后,才能合并;并且只有合并到主分支的代码才允许发布上线。

上线规范

上线前注意事项

1)上线前,除了严格验证功能特性能否正常运行、并符合需求外,还要格外关注程序的:

  • 健壮性。比如给用户友好的错误提示、输入校验。
  • 安全性。防止越权操作、输入校验。
  • 稳定性。尽量保证调用 100% 成功,如果有几率失败,要考虑重试或容错策略。

2)除非特殊情况,只有经过产品验证的功能、通过代码审核的主分支代码才允许发布上线。

3)除非特殊情况,尽量在工作日上线(建议周二 ~ 周四),保证上线后出了问题时能够及时修复。

上线后注意事项

1)上线后,一定要再次进行完整流程的测试,尤其要重点关注权限相关的功能测试。

2)上线后,一定要在群内及时同步上线信息,周知相关的成员,如果遇到问题第一时间反馈。

3)首次上线后,需要即时配置监控告警。

4)上线验证通过、并经过内部群成员确认后,可以在外部用户群发布版本更新公告。

5)上线后,即时更新项目的更新记录文档。

6)注意,上线不是终点。上线后的一段时间(至少一周内),一定要持续观察自己负责的功能是否正常运行、持续接受用户反馈、通过数据分析来观察新功能的效果,期间有任何问题都需要即时修复处理,并且准备好下一期的改进迭代。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

借助/proc/pid/mem优化Android nativ

发表于 2024-04-22

背景

在实现Android native crash捕获功能的时候,需要读取crash进程的内存数据,比如在计算elf的 load bias、Build-Id时,需要读取一块内存区域的数据,比如在获取以某个寄存器值为地址的附近内存数据时,也需要读取一块内存数据。之前我实现的时候是使用ptrace系统调用来读取crash进程数据的,当时查看了下,似乎ptrace不支持一次读取一块内存数据,所以当时我是多次PTRACE_PEEKDATA来实现的,也就是多次调用下面的方法来获取crash进程的一块内存数据的:

1
2
3
4
5
6
7
8
c++复制代码std::optional<long> readData(pid_t pid, void* addr) {
errno = 0;
long data = ptrace(PTRACE_PEEKDATA, pid, addr, nullptr);
if (errno != 0) {
return {};
}
return data;
}

上周在写相关文章:Android native crash sdk实现之crash捕获&tombstone信息的生成的时候,又想起来这个事情,感觉应该是有方法能够一次获取一个 memory block的。

/proc/pid/mem

通过搜索发现 Linux 上有个文件:/proc/pid/mem,我们可以借助open,lseek,read 来访问目标进程的内存数据:

  1. 通过 open 打开 /proc/pid/mem,便可借助打开的这个fd访问目标进程的虚拟内存
  2. 通过 lseek 可以定位到指定的虚拟地址处
  3. 通过 read 可以读取一个内存块

当然打开/proc/pid/mem肯定是需要权限的,否则就可以获取任意进程的内存数据了。不过我们的dumper进程是crash进程的子进程,可以PTRACE_ATTACH到crash进程,也有权限open/proc/pid/mem。

一次读取一个memory block

可以按如下方式打开crash进程的 mem fd:

1
2
3
4
5
c++复制代码int openMemFd(pid_t targetPid) {
char path[32];
snprintf(path, sizeof(path), "/proc/%d/mem", targetPid);
return open(path, O_RDONLY);
}

可以按如下方式一次读取一个 memory block:

1
2
3
4
5
6
7
8
9
10
11
12
c++复制代码ssize_t readMemoryBlock(int memfd, uint64_t addr, void* buf, size_t len) {
if (!buf) {
return -1;
}

if (lseek(memfd, addr, SEEK_SET) == -1) {
LOGE("lseek failed: %s", strerror(errno));
return -1;
}

return read(memfd, buf, len);
}

以计算elf build-id 为例

上面提供了一个readMemoryBlock方法可以一次读取一个memory block,我们以计算elf build-id 为例看下使用:

  1. 读取 elf header:
1
2
3
4
5
c++复制代码ElfW(Ehdr) ehdr;
if (readMemoryBlock(memfd, mapAddr, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) {
LOGE("failed read elf header through memfd");
return;
}
  1. 读取 program header table:
1
2
3
4
5
6
7
8
9
10
11
c++复制代码auto phdrAddr = mapAddr + ehdr.e_phoff;
auto phdrSize = ehdr.e_phnum * ehdr.e_phentsize;
auto phdrData = malloc(phdrSize);
if (!phdrData) {
return;
}
if (readMemoryBlock(memfd, phdrAddr, phdrData, phdrSize) != phdrSize) {
LOGE("failed read phdr through memfd");
free(phdrData);
return;
}
  1. 读取 PT_NOTE 并计算 build-id:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
c++复制代码for (int i = 0; i < ehdr.e_phnum; ++i) {
auto phdr = *(ElfW(Phdr)*)((uint64_t)phdrData + i * ehdr.e_phentsize);
switch (phdr.p_type) {
case PT_NOTE: {
auto notePtr = (const char*)(loadBias_ + phdr.p_vaddr);
auto noteData = malloc(phdr.p_memsz);
if (readMemoryBlock(memfd, (uint64_t)notePtr, noteData, phdr.p_memsz) != phdr.p_memsz) {
LOGE("failed read note through memfd");
} else {
auto noteStart = (const char*)noteData;
auto noteEnd = noteStart + phdr.p_memsz;
do {
auto note = *(ElfW(Nhdr)*) noteStart;
if (strncmp(noteStart + sizeof(ElfW(Nhdr)), "GNU", sizeof("GNU")) == 0) {
auto descStart = noteStart + sizeof(ElfW(Nhdr)) + note.n_namesz;
auto descEnd = descStart + note.n_descsz;

std::stringstream buf;
buf.fill('0');
buf.setf(std::ios_base::hex, std::ios_base::basefield);
for (; descStart < descEnd; ++descStart) {
buf.width(2);
buf << (uint32_t) *descStart;
}
buildId_ = buf.str();
break;
}

noteStart += sizeof(ElfW(Nhdr)) + note.n_namesz + note.n_descsz;
} while (noteStart < noteEnd);
}
free(noteData);
break;
}
}
}

优势

相比多次调用ptrace系统调用,性能上应该会有优化,另外多次的系统调用在错误处理方面也会比较麻烦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

📢 包邮区的开发者们看过来!本周六,想不想用moonshot

发表于 2024-04-22

🔔 掘金动手实验室(JUEJIN Code Lab) 系列第四届报名开启!这是由稀土掘金技术社区精心策划并推出的一系列技术实践活动,旨在为开发者提供一个实践与创新的平台。这个活动系列不仅仅是一个技术交流的场所,更是一个将理论知识转化为实际应用的实验场。期待每一位参与者都能在这里找到成长和突破的可能。

动手实验室的第四期也携手了最近爆火的Moonshot AI,期待开发者们能来线下使用moonshot的模型进行bot的制作。4月27日和您相约上海漕河泾字节工区~快来一起了解下活动内容吧!

⏰ 活动流程概览

  1. 项目选择&计划阶段

* 审核通过的用户会有活动小助手添加您的微信后邀请您进入专属社群 **(请务必注意微信好友申请!)。** 入群后会有组队指导,用户可根据表单信息选择志同道合的研发一起组队(最多3人,单人也可成队);
* 确认队伍后需确认一个bot项目方向
* 每个小组需要明确分工,并且明确项目的目标和时间安排
  1. 线下捏bot阶段

活动时间:2024年4月27日 14:00开始

活动地点:上海市徐汇区古美路1520号漕河泾中心C栋F6

日程

时间 现场环节 详细说明
13:00-14:00 现场签到 提前准备好百格电子票二维码
14:00-14:30 活动介绍和宣讲 介绍活动规则及扣子bot实操演示教学
14:30-16:30 现场开发+调试+答疑 按照分组就坐,现场如有问题可举手示意,会有扣子官方人员答疑/每个小组需要在规定时间完成项目/Done is better than perfect
16:30-17:30 作品展示 需要每个小组准备好幻灯片,向其他小组展示项目成果/在展示过程中,参会人可以互相学习
17:30-18:00 作品评奖

帮助文档:如何搭建bot说明 www.coze.cn/docs/guides…

🎁 活动奖励

  1. 扣子官网推荐位

有机会被推荐到官网首页,展示24-72小时。

  1. API接入

活动用户优先参与扣子API内测,将扣子 Bot无缝集成到您的产品或网站中。

  1. 参与扣子官方访谈

有机会参与官方访谈交流,交流创作思路、使用难题,产品建议优先反馈;助力项目更快成长

☎️ 报名方式

参与活动免费,感兴趣的用户可点击或 扫描 下方二维码进行报名,报名成功后会以短信方式告知参会信息

💬 入群交流

审核通过的用户届时请留意自己的微信好友申请,会有专属小助手邀请您进入社群。

🌟 主办方

稀土掘金 : 一个帮助开发者成长的社区

如有任何疑问请在该文章留言或微信稀土君哦~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter开发之--初识Flutter 概述 Flutt

发表于 2024-04-22

概述

Flutter 是由Google公司研发的一种跨端开发技术,在2018年正式推出。Flutter自带Skia图形绘制引擎,采用自绘制的方式,不管是在Android还是IOS上,Flutter都统一使用Skia引擎进行绘制,因此两端的渲染过程完全一致,能够实现像素级别的双端一致性。

在执行效率方面,众所周知,不管是哪个跨端框架,执行效率通常都是低于原生的。但是Flutter的执行效率接近原生。原因有两个,一是Flutter采用的是Dart语言,支持多种编译方式,既能以JIT的方式编译,也能用AOT的方式编译,JIT主要用于开发的阶段,这种方式虽然运行效率低,但是可以热重载(Hot Reload)。当需要打包发布的时候,也就是Release模式,可以使用AOT的方式将Dart的代码编译成平台原生代码,运行时无需再通过解释器解释,执行效率接近原生。 二是渲染引擎的优势,Flutter采用集成的Skia引擎自己渲染UI,没有跨层带来的性能损耗。而跨层的意思就是绘制的任务需要经过native层,应用层等多个层级,比如在React Native中进行UI渲染的时候,Android端需要通过JS Bridg将绘制的任务从JavaScript引擎中从C/C++ 层转移到Java层,,这涉及到JNI的调用,所以会产生一定的性能开销。而Flutter的绘制没有跨层,所以不存在这些额外的性能损失。

在跨端方面,Flutter不仅能够跨移动端,而且可以运行在Web端和桌面端,跨端能力极强。代码复用方面,由于Flutter采用自渲染的方案,所以多端从运行时环境到底层渲染都完全一致,因此可以实现最大化的多端复用。

Flutter整体架构

在这里插入图片描述

如上图所示,Flutter 的整体架构分为3层,从上到下分别是,框架层(FrameWork), 引擎层(Engine)以及嵌入层(Embedder),接下来从下到上依次介绍各层

嵌入层

嵌入层是和设备操作系统离得最近的一层,在这一层会提供一系列的接口,因此在载有任何操作系统的设备上,只要实现了嵌入层提供的接口,就能让Flutter在该平台上运行,从而实现跨端的能力。原生系统在和Flutter嵌入层做对接的时候,需要向Flutter提供一个Surface用于UI的展示,并且还需要处理用户的输入手势,将用户的输入手势转换成Flutter支持的手势格式。除此之外,原生系统还需要向Flutter提供所需要的线程和消息队列以及一些原生能力的封装与导出。嵌入层的实现语言和平台有关,比如Androi
上使用C++和Java完成适配,IOS平台上使用Objective-C/Objective-C++完成适配

引擎层

引擎层由C/C++实现,在该层中包含了Dart运行时和Skia底层绘制库,分别用于执行Dart代码,底层布局,文本绘制等工作。引擎层还负责帧调度及UI的底层绘制工作,它提供了Flutter核心API的底层实现,包括图形绘制,文本布局,文件和网络I/O、可访问性支持,插件架构,以及Dart运行时和编译工具链。

框架层

框架层使用Dart语言实现了Flutter的应用层框架,在这一层中定义了一些Flutter框架中常用的最基础的类和Flutter的UI基础以及大量的UI组件。
在框架层中,自底向上又可以细分为Foundation层,动画绘制与手势层,渲染层,以及组件层。在Foundation层中定义了Flutter应用框架中最基础的类,比如AbstractNode抽象节点,ChangNotifier通知监听器,组件标识Key等。然后在动画、绘制、手势层中则是提供了Flutter的UI基础。然后在渲染层提供了布局能力,并且定义了RenderObject树的概念,通过这套机制可以确定UI组件在界面中的位置、大小、以及如何绘制。在组件层中饰项链响应式编程的开发模式以及组件化的开发模式。最后就是Flutter提供的这些组件整体可以分为两个组件库,分别是Material,对应Android 的设计风格,Cupertino,对应IOS的设计风格。

跑通demo尝鲜

编写Flutter项目建议使用MAC电脑,因为需要同时支持Android和IOS端的开发,所以Mac电脑是最佳选择,配置环境也会简单很多。

Flutter项目的目录介绍

在这里插入图片描述

Flutter demo项目的运行

在项目的lib/main.dart中输入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
dart复制代码
class MyApp extends StatelessWidget{
const MyApp({super.key});

@override
Widget build(BuildContext context) {
// TODO: implement build
return const Center(
child: Text("你好,自定义组件===Flutter",
style: TextStyle(color: Colors.red,fontSize: 30)),
);
}
}

在命令行执行flutter doctor,得到如下图的结果:
在这里插入图片描述
如果结果中没有红叉,就表示环境OK
运行结果:

Android平台:

s8tDCnfaSl.jpg
IOS平台:

img_v3_02a6_b1f8849f-75ba-4530-b1b9-10f412644bag.jpg
总结
==

本文主要介绍了flutter的技术特点以及整体架构并且介绍了flutter架构中各层的工作,并且在Android和IOS平台上跑通了一个基本的Demo。大致看了这个Demo的代码后,发现和Android的Compose的开发很相似,所以可以将两者对比起来学习。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…333435…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%