内存管理
在编写 gtk-rs 程序时,内存管理可能有点棘手。 让我们来看看为什么会出现这种情况以及如何处理。
在第一个示例中,我们的窗口只有一个按钮。 每点击一次按钮,一个整数变量 number
就会自增一次。
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement0";
// DOES NOT COMPILE!
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(application: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// A mutable integer
let mut number = 0;
// Connect callbacks
// When a button is clicked, `number` should be changed
button_increase.connect_clicked(|_| number += 1);
// Create a window
let window = ApplicationWindow::builder()
.application(application)
.title("My GTK App")
.child(&button_increase)
.build();
// Present the window
window.present();
}
Rust 编译器拒绝编译这个程序,同时还吐出了多条错误信息。 让我们逐一查看。
error[E0373]: closure may outlive the current function, but it borrows `number`, which is owned by the current function
|
32 | button_increase.connect_clicked(|_| number += 1);
| ^^^ ------ `number` is borrowed here
| |
| may outlive borrowed value `number`
|
note: function requires argument type to outlive `'static`
|
32 | button_increase.connect_clicked(|_| number += 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `number` (and any other referenced variables), use the `move` keyword
|
32 | button_increase.connect_clicked(move |_| number += 1);
|
我们的闭包只借用了 number
. GTK 中的信号处理器要求其引用具有静态( `static
)生命周期,因此我们不能借用一个只在 build_ui
函数作用域中存在的变量。 编译器也给出了解决方法。 在闭包前面添加 move
关键字,number
就会被移入闭包。
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement0";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(application: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// DOES NOT COMPILE!
// A mutable integer
let mut number = 0;
// Connect callbacks
// When a button is clicked, `number` should be changed
button_increase.connect_clicked(move |_| number += 1);
// Create a window
let window = ApplicationWindow::builder()
.application(application)
.title("My GTK App")
.child(&button_increase)
.build();
// Present the window
window.present();
}
但这样仍然会出现以下错误信息:
error[E0594]: cannot assign to `number`, as it is a captured variable in a `Fn` closure
|
32 | button_increase.connect_clicked(move |_| number += 1);
| ^^^^^^^^^^^ cannot assign
为了理解该错误信息,我们必须了解 FnOnce
、FnMut
和 Fn
这三种闭包(closure) trait 之间的区别。 使用实现 FnOnce
特性的闭包的 API 给 API 消费者提供了最大的自由度。 闭包只被调用一次,因此它甚至可以使用自己的状态。 信号处理器可以被多次调用,所以它们不能接受 FnOnce
。
限制性更强的 FnMut
trait 不允许闭包消耗其状态,但它们仍可以对其进行修改。 信号处理器也不允许这样做,因为它们可以从自身内部调用。 这将导致多个可变引用,而借用检查器根本不喜欢这种情况。
这样就只剩下 Fn
. 状态可以不可变地借用,但我们如何修改 number
呢? 我们需要一种具有内部可变性的数据类型,比如 std::cell::Cell
.
Cell
只适用于实现了Copy
trait 的对象。对于其他对象,则应使用RefCell
. 有关内部可变性的更多信息,请参见《Rust Atomics and Locks》一书中的这一部分。
文件名:listings/g_object_memory_management/1/main.rs
use gtk::prelude::*;
use gtk::{glib, Application, ApplicationWindow, Button};
use std::cell::Cell;
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement1";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(application: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// A mutable integer
let number = Cell::new(0);
// Connect callbacks
// When a button is clicked, `number` should be changed
button_increase.connect_clicked(move |_| number.set(number.get() + 1));
// Create a window
let window = ApplicationWindow::builder()
.application(application)
.title("My GTK App")
.child(&button_increase)
.build();
// Present the window
window.present();
}
现在编译结果符合预期。
让我们举一个稍微复杂一点的例子:两个按钮都修改了同一个数字number
。 为此,我们需要一种方法,让两个闭包都拥有同一个值的所有权?
这正是 std::rc::Rc
类型的作用。
Rc
会计算通过 Clone::clone
创建和通过 Drop::drop
释放的强引用的数量,只有当这个数量降为零时,才会释放这个值。
文件名:listings/g_object_memory_management/2/main.rs
use std::cell::Cell;
use std::rc::Rc;
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// Reference-counted object with inner-mutability
let number = Rc::new(Cell::new(0));
// Connect callbacks, when a button is clicked `number` will be changed
let number_copy = number.clone();
button_increase.connect_clicked(move |_| number_copy.set(number_copy.get() + 1));
button_decrease.connect_clicked(move |_| number.set(number.get() - 1));
// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(>k_box)
.build();
// Present the window
window.present();
}
不过,用临时变量(如 number_copy)来填充作用域并不是一件好事。
我们可以使用 glib::clone!
宏来改善这种情况。
文件名:listings/g_object_memory_management/3/main.rs
use std::cell::Cell;
use std::rc::Rc;
use glib::clone;
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement3";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// Reference-counted object with inner mutability
let number = Rc::new(Cell::new(0));
// Connect callbacks
// When a button is clicked, `number` will be changed
button_increase.connect_clicked(clone!(
#[strong]
number,
move |_| {
number.set(number.get() + 1);
}
));
button_decrease.connect_clicked(move |_| {
number.set(number.get() - 1);
});
// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(>k_box)
.build();
// Present the window
window.present();
}
就像 Rc<Cell<T>>
一样,GObject 也是引用计数和可变的。 因此,我们可以像传递number
一样将按钮传递给闭包。
文件名:listings/g_object_memory_management/4/main.rs
use std::cell::Cell;
use std::rc::Rc;
use glib::clone;
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement4";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let number = Rc::new(Cell::new(0));
// Connect callbacks
// When a button is clicked, `number` and label of the other button will be changed
button_increase.connect_clicked(clone!(
#[weak]
number,
#[strong]
button_decrease,
move |_| {
number.set(number.get() + 1);
button_decrease.set_label(&number.get().to_string());
}
));
button_decrease.connect_clicked(clone!(
#[strong]
button_increase,
move |_| {
number.set(number.get() - 1);
button_increase.set_label(&number.get().to_string());
}
));
// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(>k_box)
.build();
// Present the window
window.present();
}
如果我们现在点击其中一个按钮,另一个按钮的标签就会改变。
但是,哎呀! 我们是不是忘了引用计数系统的一个恼人之处?是的:循环引用.。button_increase
持有 button_decrease
的强引用,反之亦然。 强引用可以防止引用的值被释放。 如果这个链条导致一个循环,那么这个循环中的所有值都不会被释放。 使用弱引用可以打破这种循环,因为弱引用不会使其值存活,而是提供了一种方法,可以在该值仍然存活时检索强引用。 由于我们希望应用程序释放不需要的内存,因此我们应该在按钮上使用弱引用。
文件名:listings/g_object_memory_management/5/main.rs
use std::cell::Cell;
use std::rc::Rc;
use glib::clone;
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement5";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// Reference-counted object with inner mutability
let number = Rc::new(Cell::new(0));
// Connect callbacks
// When a button is clicked, `number` and label of the other button will be changed
button_increase.connect_clicked(clone!(
#[weak]
number,
#[weak]
button_decrease,
move |_| {
number.set(number.get() + 1);
button_decrease.set_label(&number.get().to_string());
}
));
button_decrease.connect_clicked(clone!(
#[weak]
button_increase,
move |_| {
number.set(number.get() - 1);
button_increase.set_label(&number.get().to_string());
}
));
// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(>k_box)
.build();
// Present the window
window.present();
}
现在循环引用被破坏了。
每次点击按钮时,glib::clone
都会尝试升级弱引用。 例如,如果我们现在点击了一个按钮,而另一个按钮已不存在,回调将被跳过。
默认情况下,它会立即从闭包返回,返回值为 ()
。 如果闭包期望不同的返回值,可以指定 @default-return
。
请注意,我们在第二个闭包中移动了 number
。 如果我们在两个闭包中都移动了弱引用,那么没有任何东西能让 number
继续存活,闭包也不会被调用。 考虑到这一点,button_increase
和 button_decrease
也会在 build_ui
的作用域结束时被丢弃。 那么谁来保持按钮的存活呢?
文件名:listings/g_object_memory_management/5/main.rs
use std::cell::Cell;
use std::rc::Rc;
use glib::clone;
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement5";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// Reference-counted object with inner mutability
let number = Rc::new(Cell::new(0));
// Connect callbacks
// When a button is clicked, `number` and label of the other button will be changed
button_increase.connect_clicked(clone!(
#[weak]
number,
#[weak]
button_decrease,
move |_| {
number.set(number.get() + 1);
button_decrease.set_label(&number.get().to_string());
}
));
button_decrease.connect_clicked(clone!(
#[weak]
button_increase,
move |_| {
number.set(number.get() - 1);
button_increase.set_label(&number.get().to_string());
}
));
// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(>k_box)
.build();
// Present the window
window.present();
}
当我们将按钮附加到 gtk_box
时,gtk_box
会保留对按钮的强引用。
文件名:listings/g_object_memory_management/5/main.rs
use std::cell::Cell;
use std::rc::Rc;
use glib::clone;
use gtk::prelude::*;
use gtk::{self, glib, Application, ApplicationWindow, Button, Orientation};
const APP_ID: &str = "org.gtk_rs.GObjectMemoryManagement5";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create two buttons
let button_increase = Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// Reference-counted object with inner mutability
let number = Rc::new(Cell::new(0));
// Connect callbacks
// When a button is clicked, `number` and label of the other button will be changed
button_increase.connect_clicked(clone!(
#[weak]
number,
#[weak]
button_decrease,
move |_| {
number.set(number.get() + 1);
button_decrease.set_label(&number.get().to_string());
}
));
button_decrease.connect_clicked(clone!(
#[weak]
button_increase,
move |_| {
number.set(number.get() - 1);
button_increase.set_label(&number.get().to_string());
}
));
// Add buttons to `gtk_box`
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(>k_box)
.build();
// Present the window
window.present();
}
当我们将 gtk_box
设置为 window
的子窗口时,window
会保持对它的强引用。
直到我们关闭窗口,它都会保持 gtk_box
以及按钮的存活。
由于我们的应用程序只有一个窗口,关闭窗口也就意味着退出应用程序。
只要尽可能使用弱引用,您就会发现完全可以避免应用程序中的循环引用。 在没有循环引用的情况下,您可以依靠 GTK 来正确管理您传递给它的 GObject 的内存。