Chromium 基础库使用说明

Stella981
• 阅读 1237

Chromium 基础库使用说明

chromium base

作者: 易旭昕 (@roger2yi)


Chromium 提供了一个类似 WTF 的基础库,甚至包含了更多的内容。这个基础库在 Blink 之外被广泛使用(Blink 里面仍然使用的是 WTF),了解它的使用对我们实际的代码编写是十分重要的。本文主要介绍 Chromium 基础库包括的主要内容,并详细说明一些重要类型的使用方式。如果需要了解某个特定目录或者文件的内容概要,学辉的这篇文档可以提供一个不错的全面索引,另外 Chromium 为所有的基础库类型都提供了完整的单元测试,通过阅读单元测试代码了解这些类型的使用也是很好的方式。

Chromium 基础库概览

Chromium 基础库包括的内容十分繁杂,我把其中的主要部分大致分为以下几类:

  • 容器类型

Chromium 的代码主要使用 STL 容器类型,比如 std::vector,std::list,另外 GCC 和 MSVC 提供的 STL 扩展容器类型 hash_map 和 hash_set 也在 Chromium 中使用,不过统一放在 base 名字空间里面,通过 base::hash_mapbase_hash_set 使用。

在 STL 外,Chromium 基础库还提供了一些额外的容器类型比如 base::LinkedList,base::MRUCache 等。

容器类型代码位于 containers 子目录下。

  • 智能指针

Chromium 提供了一篇官方的文档 Smart Pointer Guidelines 讲解了在 Chromium 里面常见的几种智能指针,最常见的包括 base::scoped_ptr,base::ScopedVector,base::WeakPtr 和  base::scoped_refptr。

智能指针代码位于 memory 子目录下。

  • 回调函数

Chromium 基础库提供了 base::Bind 机制,可以将全局函数和成员函数,跟它的调用上下文绑定在一起,构成一个回调函数对象。这个回调函数对象可以被传递,被保存,被当做消息发送到线程的消息 循环里面,最后我们可以通过这个回调函数对象的 Run 方法调用跟它关联的函数。

回调函数代码位于基础库的根目录下。

  • 线程相关

Chromium 基础库提供了大量跟线程相关的设施,包括平台线程的封装类型 base::Thread,线程本地存储 base::ThreadLocalPointer,消息循环 base::MessageLoop,线程同步设施 base::Lock,base::WaitableEvent 等等,还有原子操作和内存屏障的支持。

线程相关的代码位于 threadingmessage_loopsynchronization 子目录下,原子操作和内存屏障位于根目录的 atomicops.h。

  • 字串处理

Chromium 使用 std::string 作为字串容器,官方文档 Chromium String usage 提供了在 Chromium 里面字串使用的一些说明。另外 strings 子目录下提供了一些针对字串的辅助操作设施。

  • 文件操作

Chromium 基础库的 base::File 提供了文件相关的操作,相关的代码位于根目录files 子目录下;

  • 计时器

Chromium 基础库的 base::Timer 提供了计时器相关的操作,相关的代码位于 timer 子目录下;

  • 日志和调试

Chromium 基础库提供了通用的日志输出和各种调试辅助等机制,相关的代码位于根目录debugprofile 子目录下;

  • 系统监控

包括系统状态监控电池状态监控内存监控,分别位于 system_monitorpower_monitor,和 memory 子目录下;

  • Android 相关

基础库的 android 子目录下是 Android 平台相关的代码,除了包括其它基础类型的 Android 适配代码外,还有一些 Android 平台特有的类型,像一些用于 JNI 支持的辅助类型。

除了上面列举的部分外,基础库还包括的一些设施有进程内存分配器国际化支持随机数生成Base64编码Sha1编码等等,还有一些难以归类的工具类型。

容器类型

LinkedList

base::LinkedList 是 std::list 的一个替代品,优点是当你拥有一个节点对象时,要删除这个节点只需要 O(1) 的复杂度,并且插入节点不需要新增分配内存。能够做到这一点是因为 LinkedList 要求节点类型必须以 LinkNode 作为基类,而 LinkNode 本身已经包含了指向前/后节点的指针。下面的代码演示了 LinkedList 的常见用法:

  1. class MyNodeType : public LinkNode<MyNodeType> {

  2. ...

  3. };

  4. LinkedList<MyNodeType> list;

  5. LinkNode<MyNodeType>* n1 = ...;

  6. LinkNode<MyNodeType>* n2 = ...;

  7. LinkNode<MyNodeType>* n3 = ...;

  8. list.Append(n1);

  9. list.Append(n3);

  10. n2->InsertBefore(n3);

  11. for (LinkNode<MyNodeType>* node = list.head();

  12. node != list.end();

  13. node = node->next()) {

  14. MyNodeType* value = node->value();

  15. ...

  16. }

MRUCache

MRU 是 most recently used 的缩写,MRUCache 提供了一个类似 Map 的容器类型,主要的区别是可以设定容器的最大容纳个数,如果超过则自动移除最久不被使用的那个对象

MRUCache 实际上还存在几种不同的变种:

  1. MRUCache 是最常用的,它假设自身不拥有对象,当移除对象时不执行删除操作;

  2. OwningMRUCache 假设自己拥有对象,并要求存储对象是使用指针类型,在移除对象时会执行删除操作;

  3. HashingMRUCache 跟 MRUCache 的区别是,它内部使用 base::hash_map 而不是 std::map 存储对象,所以也要求键值对象支持 hash 操作;

智能指针

按照官方文档的说明,什么时候我们应该使用什么类型的智能指针:

  • 拥有对象的时候

使用 scoped_ptr 或者 ScopedVector,它们可以使用来管理所拥有的非引用计数的堆分配对象。

  • 不拥有对象的时候

使用 raw pointer 或者 WeakPtr。如果其它代码拥有对象,但是你需要知道这个对象是否已经被销毁,就使用 WeakPtr,当所关联的对象被销毁的时候 WeakPtr 会自动被置空。你可以通过 WeakPtr.get 方法获得关联对象的指针,如果返回值为空则说明对象已经被销毁。

  • 使用引用计数对象的时候

使用 scoped_refptr,不过 Chromium 不鼓励使用引用计数对象,特别是在多线程场景下,引用计数对象会使对象的拥有权难以确定和对象销毁的顺序和时机难以确定。

scoped_ptr

base::scoped_ptr 是 Chromium 里面最常用的智能指针,一些常见的用法:

  1. // We put a pointer into a smart pointer at construction time.

  2. scoped_ptr<base::Value> value(base::JSONReader::Read(data));

  3. scoped_ptr<Foo> foo_ptr(new Foo(...));

  4. // ...Or by using reset().

  5. scoped_ptr<Bar> bar_ptr; // Like "Bar* bar_ptr = NULL;".

  6. bar_ptr.reset(new Bar(...)); // Now |bar_ptr| is non-NULL and owns the object.

  7. // We can test the smart pointer directly or use get() to see the raw pointer underneath.

  8. if (!value)

  9. return false;

  10. Foo* raw_ptr = foo_ptr.get();

  11. // We can call through the smart pointer as if it were a pointer.

  12. DictionaryValue* dict = NULL;

  13. if (!value->GetAsDictionary(&dict))

  14. return false;

当 scoped_ptr 作为函数参数使用时,这意味着函数的代码会获得参数对象的所有权,函数的调用者如果不是使用一个临时的 scoped_ptr 的话,它需要使用 Pass() 方法来放弃自己的 scoped_ptr 对对象的所有权,例程如下:

  1. // Foo() takes ownership of |bar|.

  2. void Foo(scoped_ptr<Bar> bar);

  3. ...

  4. scoped_ptr<Bar> bar_ptr(new Bar());

  5. Foo(bar_ptr.Pass()); // Pass() makes |bar_ptr| NULL.

  6. Foo(scoped_ptr<Bar>(new Bar())); // No need to use Pass() on temporaries.

如果函数返回一个 scoped_ptr,这意味着函数的调用者获得返回对象的所有权,例程如下:

// Foo takes ownership of |bar|, and the caller takes ownership of the returned// object.scoped_ptr<Bar> Foo(scoped_ptr<Bar> bar) {  if (cond) {    return bar.Pass();                 // Transfers ownership of |bar| back to                                       // the caller.  }  return scoped_ptr<Bar>(new Bar()));  // No Pass() necessary on temporaries.  // Note that on this codepath, |bar| gets deleted here.}

最后需要注意的是不应该在函数的参数和返回值中使用 scoped_ptr 的指针或者引用形式(scoped_ptr<>* scoped_ptr<>&),它会模糊所有权的转移,使最终谁拥有对象的所有权难以理解。

ScopedVector

在 STL 容器里面存储 scoped_ptr, 类似 std::vector<scoped_ptr<T> > 这样的用法可能会有问题,比如下面的代码:

  1. std::vector<scoped_ptr<T> > vec;

  2. ...

  3. // 对象的所有权会从 vec 转移到 scoped_ptr p,并随着 p 被销毁而销毁!!!

  4. scoped_ptr<T> p = vec[0];

因为上述代码的危险性,所以 Chromium 不支持通过 STL 容器存储 scoped_ptr,它提供了 base::ScopedVector 来满足大部分这种需求,ScopedVector 拥有存储在它内部的对象,并在移除对象的时候负责销毁对象,如果 ScopedVector 本身被销毁,它会销毁它所存储的所有对象。因为 ScopedVector 内部存储的是 raw pointer,就不存在像 std::vector<scoped_ptr<T> > 这样容易误用的危险性。

  1. base::ScopedVector<T> vec;

  2. ...

  3. // 通过 raw pointer p 使用对象,不会有所有权的转移

  4. T* p = vec[0];

如果需要在其它 STL 容器里面使用智能指针,希望在容器被销毁或者移除元素时自动销毁容器存储的对象,可以考虑使用 linked_ptr。

WeakPtr

base::WeakPtr 是所谓的弱指针,Chromium 鼓励更多使用 WeakPtr 而不是滥用需要引用计数的 scoped_refptr,因为 WeakPtr 明确不会拥有对象的所有权,也不会影响对象的销毁顺序。

base::WeakPtr 需要通过 base::WeakPtrFactory 创建,一般情况下它们使用的方式是这样的:

  1. class Controller {

  2. public:

  3. void SpawnWorker() { Worker::StartNew(weak_factory_.GetWeakPtr()); }

  4. void WorkComplete(const Result& result) { ... }

  5. private:

  6. // Member variables should appear before the WeakPtrFactory, to ensure

  7. // that any WeakPtrs to Controller are invalidated before its members

  8. // variable's destructors are executed, rendering them invalid.

  9. WeakPtrFactory<Controller> weak_factory_;

  10. };

  11. class Worker {

  12. public:

  13. static void StartNew(const WeakPtr<Controller>& controller) {

  14. Worker* worker = new Worker(controller);

  15. // Kick off asynchronous processing...

  16. }

  17. private:

  18. Worker(const WeakPtr<Controller>& controller)

  19. : controller_(controller) {}

  20. void DidCompleteAsynchronousProcessing(const Result& result) {

  21. if (controller_)

  22. controller_->WorkComplete(result);

  23. }

  24. WeakPtr<Controller> controller_;

  25. };

  26. 需要支持 WeakPtr 的类型 Controller 拥有一个 WeakPtrFactory 的成员变量,外部获取的 WeakPtr 都是通过这个 WeakPtrFactory 创建的;

  27. 当 Controller 对象被销毁时,它的 WeakPtrFactory 成员变量也会同时被销毁,WeakPtrFactory 被销毁的同时会将所有通过它创建的 WeakPtr 置空

  28. Controller 的 WeakPtrFactory 的成员变量一般放在最后面,这样它就是第一个被销毁的成员变量,似乎没有太大意义,不过 Chromium 习惯使用这样的方式;

在多线程环境下使用 WeakPtr 和 WeakPtrFactory 需要注意,它们只支持这样的方式:

  1. WeakPtrFactory 和 WeakPtr 属于创建它们的线程,也只能在创建它们的线程将 WeakPtr 置空,检查一个 WeakPtr 是否为空,和访问 WeakPtr 指向的对象

  2. 属于线程 A 的 WeakPtr 可以传递给 线程 B,线程 B 不能直接使用这个 WeakPtr,这不是线程安全的但是它可以使用这个 WeakPtr 往线程 A 发送任务(PostTask),因为任务是在线程 A 执行的,所以任务执行代码本身可以使用这个 WeakPtr;

scoped_refptr

用于支持引用计数对象的智能指针,要求对象类型继承至 RefCounted 或者 RefCountedThreadSafe,后者是线程安全的。Chromium 因为历史遗留的缘故,当前的代码中使用 scoped_refptr 的地方还比较多,但是目前官方已经不鼓励 scoped_refptr 的使用,认为它会导致对象的所有权,和销毁的顺序和时机难以确定,并认为绝大部分情况下 scoped_refptr 都可以使用 scoped_ptr 和 WeakPtr 来取代,设计本身也不应该过多依赖多个线程共享对象这种方式。

下面是一些简单的使用例程:

  1. class MyFoo : public RefCounted<MyFoo> {

  2. ...

  3. };

  4. void some_function() {

  5. scoped_refptr<MyFoo> foo = new MyFoo();

  6. foo->Method(param);

  7. // |foo| is released when this function returns

  8. }

  9. void some_other_function() {

  10. scoped_refptr<MyFoo> foo = new MyFoo();

  11. ...

  12. foo = NULL; // explicitly releases |foo|

  13. ...

  14. if (foo)

  15. foo->Method(param);

  16. }

  17. {

  18. scoped_refptr<MyFoo> a = new MyFoo();

  19. scoped_refptr<MyFoo> b;

  20. b.swap(a);

  21. // now, |b| references the MyFoo object, and |a| references NULL.

  22. }

  23. {

  24. scoped_refptr<MyFoo> a = new MyFoo();

  25. scoped_refptr<MyFoo> b;

  26. b = a;

  27. // now, |a| and |b| each own a reference to the same MyFoo object.

  28. }

linked_ptr

linked_ptr 行为上有些类似 scoped_refptr,但是不需要对象本身支持引用计数,它是通过将所有指向同一个对象的 linked_ptr 链接成一条链来实现引用计数的,当一个 linked_ptr 从另外一个 linked_ptr 拷贝时,它会把自身加入这条链,而这个 linked_ptr 被销毁时,它会把自身从这条链移除,如果它是最后一个,则同时销毁指向的对象。

linked_ptr 实际上有可能比 scoped_refptr 更危险,它使得对象的持有者和销毁时机变得更不明确,同时也不是线程安全的。所以 linked_ptr 一般只是用在 STL 容器上面,容器持有这些对象,并且在容器本身被销毁时销毁对象,这样就不会产生太多混乱

base::SupportsUserData 的实现里面使用了 linked_ptr,用来在一个 std::map 里面存储 User Data。

  1. typedef std::map<const void*, linked_ptr<Data> > DataMap;

  2. // Externally-defined data accessible by key.

  3. DataMap user_data_;

  4. SupportsUserData::Data* SupportsUserData::GetUserData(const void* key) const {

  5. DataMap::const_iterator found = user_data_.find(key);

  6. if (found != user_data_.end())

  7. return found->second.get();

  8. return NULL;

  9. }

  10. void SupportsUserData::SetUserData(const void* key, Data* data) {

  11. user_data_[key] = linked_ptr<Data>(data);

  12. }

回调函数

Chromium 提供了 base::Bind 和模版类型 base::Callback 对函数回调提供了支持,下面是一个简单的使用例程,将一个全局函数绑定到一个 Callback 对象,并通过 Callback.Run 调用这个函数:

   int Return5() { return 5; }   base::Callback<int(void)> func_cb = base::Bind(&Return5);   LOG(INFO) << func_cb.Run();  // Prints 5.

如果要绑定一个类的成员函数,我们需要为 Bind 方法提供这个类的一个实例对象,把它跟 Callback 对象绑定,为了保证这个对象在 Callback 对象被执行时仍然存活,或者 Callback 对象能够知道这个对象已经被销毁,我们需要提供一个 scoped_refptr 或者 WeakPtr,通过 base::Unretained(ptr) 用 raw pointer 也可以,不过后果自负... 早期 Chromium 的代码使用 scoped_refptr 比较多,现在 Chromium 更倾向于使用 WeakPtr,当然使用 WeakPtr 时我们要注意这个 Callback 只能在 WeakPtr 所属的线程中被调用,因为它是非线程安全的,下面是一个使用 scoped_refptr 的例子:

   class Ref : public base::RefCountedThreadSafe<Ref> {    public:     int Foo() { return 3; }     void PrintBye() { LOG(INFO) << "bye."; }   };   scoped_refptr<Ref> ref = new Ref();   base::Callback<void(void)> ref_cb = base::Bind(&Ref::Foo, ref);   LOG(INFO) << ref_cb.Run();  // Prints out 3.

如果绑定的函数需要参数,我们可以事先绑定所有参数对象到 Callback 里面,也可以事先不绑定参数,甚至可以事先只绑定一部分参数,事先绑定所有参数的 Callback 在 Chromium 里面称为闭包 Closure:

  1. void MyFunc(int i, const std::string& str) {}

  2. base::Callback<void(int, const std::string&)> cb = base::Bind(&MyFunc);

  3. cb.Run(23, "hello, world");

  4. void MyFunc(int i, const std::string& str) {}

  5. base::Callback<void(void)> cb = base::Bind(&MyFunc, 23, "hello world");

  6. cb.Run();

  7. base::Closure cb = base::Bind(&MyClass::MyFunc, this, 23, "hello world");

如果想让 Callback 对象拥有跟它绑定的类对象或者参数对象,也可以使用 base::Owned 或者 base::Passed 方法,分别针对 raw pointer 和 scoped_ptr,如果是 scoped_ptr 类型参数的话,在调用时 Callback 就会将这个参数对象的所有权转移给被回调的函数,最后 Callback 对象被销毁时会自动销毁绑定的类对象和参数对象(如果还拥有这个参数对象的话):

  1. MyClass* myclass = new MyClass;

  2. base::Bind(&MyClass::Foo, base::Owned(myclass));

  3. void TakesOwnership(scoped_ptr<Foo> arg) {}

  4. scoped_ptr<Foo> f(new Foo);

  5. // f becomes null during the following call.

  6. base::Closure cb = base::Bind(&TakesOwnership, base::Passed(&f));

总而言之,在使用 Chromium 的回调函数机制时,一定要非常清楚跟 Callback 对象绑定的类对象和参数对象的所有权和生命周期,避免在 Callback 被调用时,访问到已经被销毁的对象。

线程相关

线程和消息循环

base::Thread 是 Chromium 提供的对平台线程的封装,并自带了消息循环 base::MessageLoop,如果需要一个不用消息循环的线程,可以考虑使用 base::SimpleThread。

一个继承 base::Thread 的自己的线程类,可能需要复写 Init 和 Cleanup 方法,它们在这个线程中被调用,分别位于消息循环启动和停止的时候。

class InProcessRendererThread : public base::Thread { public:  ... protected:  virtual void Init() override;  virtual void CleanUp() override;  ...};

我们可以通过 Thread.message_loop 或者 Thread.message_loop_proxy 方法获取这个线程的消息循环,后者返回的是 MessageLoopProxy,在 Chromium 里面使用 MessageLoopProxy 比直接使用 MessageLoop 要更普遍,并且通过 scoped_refptr 的方式使用 MessageLoopProxy 比通过 raw pointer 的方式使用 MessageLoop 也更安全,通过下面的两种方式可以获得当前运行线程的 MessageLoopProxy。

   MessageLoop::current()->message_loop_proxy()   MessageLoopProxy::current()

MessageLoopProxy 继承了接口 SequencedTaskRunner,后者又继承了接口 TaskRunner,所以 MessageLoopProxy 实现了一系列的 PostXXXTask 的方法。一个 Task 实际上就是一个 Closure,如前所述 Closure 就是一个预先绑定了所有参数对象的 Callback 对象。 通过 MessageLoopProxy PostTask 就相当于发送一个消息给这个 MessageLoopProxy 所属的线程,这个被发送的 Callback 对象将会在 MessageLoopProxy 所属的线程执行,跟 Callback 对象绑定的函数将会被调用。

PostXXXTask 有若干变种,包括延迟的时间,是否是 Non-Nestable。延迟时间比较容易理解,不需要延迟则为 0,而 Non-Nestable 的意思是 - 如 果 Task T1 在执行过程中 Post Task T2 到当前线程的 MessageLoop,并且 T1 接着直接调用 MessageLoop 的 Run,或者 RunLoop 的 Run 方法,相当于要求 MessageLoop 在当前消息循环中进入一个子循环,马上执行其它等待中的任务,在这种状况下 MessageLoop 进入了 Nested 状态,如果 T2 是 Non-Nestable,Chromium 将会保证 T2 在这种情况下绝对不会被执行,如果 T2 不是 Non-Nestable,就有可能在会被执行。

下面是一个简单使用例程:

  1. scoped_refptr<base::MessageLoopProxy> ui_loop_;

  2. base::WeakPtr<SharedRendererState> ui_thread_weak_ptr_;

  3. void SharedRendererState::PostExternalDrawConstraintsToChildCompositor(

  4. const ParentCompositorDrawConstraints& parent_draw_constraints) {

  5. if (UpdateDrawConstraints(parent_draw_constraints)) {

  6. // No need to hold the lock_ during the post task.

  7. ui_loop_->PostTask(

  8. FROM_HERE,

  9. base::Bind(&SharedRendererState::UpdateParentDrawConstraintsOnUIThread,

  10. ui_thread_weak_ptr_));

  11. }

  12. }

如果需要任务执行后原线程获得通知,可以使用 PostTaskAndReply 方法,参考下面的例程,task 执行后,reply 会在调用 PostTaskAndReplay 的原线程被调用,并且 task 和 reply 对象都保证在原线程被销毁,这样我们可以在 task 和 reply 上绑定必须要在原线程销毁的对象。另外一些需要注意的地方:

  • task 绑定的类对象会作为参数传递给 reply 的回调函数;

  • reply 绑定的类对象 DataLoder 不是线程安全的,它通过 WeakPtr 跟 reply 绑定,可以提前被销毁,reply 会被自动取消;

  1. bool PostTaskAndReply(const tracked_objects::Location& from_here,

  2. const Closure& task,

  3. const Closure& reply);

  4. class DataBuffer : public RefCountedThreadSafe<DataBuffer> {

  5. public:

  6. // Called to add data into a buffer.

  7. void AddData(void* buf, size_t length);

  8. ...

  9. };

  10. class DataLoader : public SupportsWeakPtr<DataLoader> {

  11. public:

  12. void GetData() {

  13. scoped_refptr<DataBuffer> buffer = new DataBuffer();

  14. target_thread_.message_loop_proxy()->PostTaskAndReply(

  15. FROM_HERE,

  16. base::Bind(&DataBuffer::AddData, buffer),

  17. base::Bind(&DataLoader::OnDataReceived, AsWeakPtr(), buffer));

  18. }

  19. private:

  20. void OnDataReceived(scoped_refptr<DataBuffer> buffer) {

  21. // Do something with buffer.

  22. }

  23. };

如果 PostTask 之后,我们又希望取消它,可以使用 base::CancelableTaskTracker 来 PostTask,CancelableTaskTracker 本身不是线程安全的,它的创建,销毁,PostTask,Cancel 都必须在同一个线程。下面是一个简单的使用例子:

  1. Thread worker_thread("worker thread");

  2. worker_thread.Start();

  3. CancelableTaskTracker::TaskId task_id =

  4. task_tracker_.PostTaskAndReply(worker_thread.message_loop_proxy().get(),

  5. FROM_HERE,

  6. Bind(&DoNothing),

  7. Bind(&DoNothing));

  8. task_tracker_.TryCancel(task_id);

线程本地存储

Chromium 提供了 ThreadLocalPointer,它是平台相关的线程本地存储机制的封装,可以存放一个 raw pointer,如果需要的是 bool 类型的变量,ThreadLocalBoolean 提供了更简单的使用方式。ThreadLocalPointer 的一个简单例程:

  1. // My class is logically attached to a single thread.  We cache a pointer

  2. // on the thread it was created on, so we can implement current().

  3. MyClass::MyClass() {

  4. DCHECK(Singleton<ThreadLocalPointer<MyClass> >::get()->Get() == NULL);

  5. Singleton<ThreadLocalPointer<MyClass> >::get()->Set(this);

  6. }

  7. MyClass::~MyClass() {

  8. DCHECK(Singleton<ThreadLocalPointer<MyClass> >::get()->Get() != NULL);

  9. Singleton<ThreadLocalPointer<MyClass> >::get()->Set(NULL);

  10. }

  11. // Return the current MyClass associated with the calling thread, can be

  12. // NULL if there isn't a MyClass associated.

  13. MyClass* MyClass::current() {

  14. return Singleton<ThreadLocalPointer<MyClass> >::get()->Get();

  15. }

线程同步

base::Lock 是平台相关锁的封装,base::AutoLock 提供了一个自动加锁/解锁的辅助类,这部分都比较容易理解。base::ConditionVariable 是平台相关的条件量的封装,跟其它库的 Condition 类型有些不同的是,它需要在构造时就指定对应的锁,而不是在 Wait 的时候才指定。

为了方便实现线程同步消息,Chromium 还提供了 base::WaitableEvent (如果是位于 cc 模块的代码,也可以使用 cc::CompletionEvent,它是 base::WaitableEvent 的封装),WaitableEvent 构造函数的第一个参数 manual_reset 的含义是,如果它为 false,一个已经 signaled 的 WaitableEvent 在被查询 IsSignaled 后会自动恢复到 unsignaled 的状态,所以一般没有特殊需要第一个参数都应该为 true。下面是一个简单的线程同步消息处理的例子:

  1. template <typename T>

  2. static void RunTaskWithResult(base::Callback<T(void)> task,

  3. T* result,

  4. base::WaitableEvent* completion) {

  5. *result = task.Run();

  6. completion->Signal();

  7. }

  8. base::WaitableEvent completion(true, false);

  9. bool result = false;

  10. QueueTask(

  11. base::Bind(&RunTaskWithResult<bool>, init_task, &result, &completion));

  12. completion.Wait();

base::WaitableEventWatcher 提供了 WaitableEvent 异步响应的使用方式,请看下面的例程,我们可以通过 WaitableEventWatcher 监控某个 WaitableEvent,并在它被 Signal 的时候触发事先设定的回调函数,实际内部实现是当 WaitableEvent 被 Signal 时,WaitableEventWatcher 事先设定的 Callback 对象会被发送到 StartWatching 的调用线程的消息循环里面

   class MyClass {    public:     void DoStuffWhenSignaled(WaitableEvent *waitable_event) {       watcher_.StartWatching(waitable_event,           base::Bind(&MyClass::OnWaitableEventSignaled, this);     }    private:     void OnWaitableEventSignaled(WaitableEvent* waitable_event) {       // OK, time to do stuff!     }     base::WaitableEventWatcher watcher_;   };

字串处理

Chromium 主要使用 std::string 作为字串类型,std::string 的一个主要问题是它本身不包含编码信息,所以 Chromium 约定 std::string 使用 UTF-8 编码,基础库里面还提供了 base::string16,string16 使用 UTF-16 编码

Chromium 另外还有一个 base::StringPiece 类型,它类似 WTF 里面的 CString,基本上就是 C 风格字串的一个简单封装,StringPiece 通常只是用来传递一块 string data 或者 raw data,它本身并不拥有这些数据,销毁时也不会释放数据

下面是一些使用时的注意事项:

  • 使用 string.empty() 做空串检查;

  • 字串常量使用 char[] 而不是 std::string,比如 const char kFoo[] = “foo”;

  • 在函数输入参数中使用 std::string,最好使用引用常量的方式避免拷贝;

  • 在循环的 inner loop 里面,一般应该避免临时 std::string 对象创建;

Chromium 提供的一些字串处理的辅助方法,比如字串格式化,分割,数值字串类型转换,比较,替换等等,位于 strings 子目录下,都比较简单,这里就不再详细说明了。

文件操作

PathService

base::PathService 提供了一种设定和获取一些预定义用途目录的机制,在 Android 上,我们需要的目录定义在 base_path_android.h 和 ui_base_path.h 里面,另外 PathService.java 提供了在 Java 端设定路径的功能。

  1. enum {

  2. PATH_ANDROID_START = 300,

  3. DIR_ANDROID_APP_DATA, // Directory where to put Android app's data.

  4. DIR_ANDROID_EXTERNAL_STORAGE, // Android external storage directory.

  5. PATH_ANDROID_END

  6. };

        PathService.override(PathService.DIR_MODULE, "/system/lib/");        final int DIR_RESOURCE_PAKS_ANDROID = 3003;        PathService.override(DIR_RESOURCE_PAKS_ANDROID,                "/system/framework/webview/paks");

File

base::File 提供了平台相关的文件对象的封装,可以通过它对文件和目录进行操作,包括创建,读写文件等等。base::FilePath 提供了一个文件或者目录路径的封装。

base::FileProxy 提供了一种异步文件操作的方法,你可以为 FileProxy 设置一个 TaskRunner,比如某个线程的 MessageLoopProxy,然后在 FileProxy 上执行的操作实际上都是由这个 TaskRunner 所属的线程异步执行,FileProxy 提供的方法跟 File 基本一致,一般后面会增加一个用于响应操作结果的 Callback 对象,这个 Callback 对象会在原调用线程执行。FileProxy 有一个限制是不能同时 Proxy 多个操作,只有完成一个操作后才能执行下一个操作。

下面是一个简单的例程,我们在另外一个 file_thread_ 线程创建或者打开一个文件,当文件创建或者打开后,原线程会执行 DidCreateOrOpen 函数处理操作结果:

  1. TaskRunner* file_task_runner() const {

  2. return file_thread_.message_loop_proxy().get();

  3. }

  4. void DidCreateOrOpen(File::Error error) {

  5. error_ = error;

  6. MessageLoop::current()->QuitWhenIdle();

  7. }

  8. FileProxy proxy(file_task_runner());

  9. proxy.CreateOrOpen(

  10. test_path(),

  11. File::FLAG_CREATE | File::FLAG_READ,

  12. Bind(&FileProxyTest::DidCreateOrOpen, weak_factory_.GetWeakPtr()));

  13. MessageLoop::current()->Run();

  14. EXPECT_EQ(File::FILE_OK, error_);

  15. EXPECT_TRUE(proxy.IsValid());

  16. EXPECT_TRUE(proxy.created());

  17. EXPECT_TRUE(PathExists(test_path()));

Chromium 还提供很多文件相关的辅助类:

  • base::FileEnumerator 提供了枚举某个 FilePath 下面的子文件的功能;

  • base::FilePathWatcher 提供了监控某个文件或者目录变化的功能;

  • base::ImportantFileWriter 提供了另外一种文件写入方式,避免应用崩溃导致文件写入一半,数据不完整的状况,原理是先写入一个临时文件,写完后再重命名;

  • base::MemoryMappedFile 提供了一种将只读文件全部或者部分映射到内存,读取文件相当于内存访问,加快读取的速度的机制;

  • file_util.h 里面提供大量文件操作的辅助方法,比如 CreateTemporaryFile,GetFileSize 等等;

计时器

base::Timer 实际上相当于 MessageLoop::PostDelayedTask 的封装,对外提供了一次性或者不断重复的计时器功能。Timer 的构造函数里面 retain_user_task 的含义是,当 Timer 被 Stop 的时候,关联的任务是否被保留,默认值为 true,也就是保留而不置空。跟 WTF 里面的 Timer 一样,base::Timer 是有线程归属性的,它属于调用 Start 或者 Reset 方法的线程,设置的任务也在这个线程里面执行。

    base::Timer timer(false, false);    EXPECT_FALSE(timer.IsRunning());    timer.Start(FROM_HERE, TimeDelta::FromDays(1),                base::Bind(&TimerTestCallback));    EXPECT_TRUE(timer.IsRunning());    timer.Stop();    EXPECT_FALSE(timer.IsRunning());    EXPECT_TRUE(timer.user_task().is_null());

base::ElapsedTimer 提供一个简单的方法给程序计算某些操作的耗时。

日志和调试

日志输出

Chromeium 提供了 LOG,DLOG,VLOG 几种输出日志的方式,类似下面这样的代码:

LOG(INFO) << "Found " << num_cookies << " cookies";LOG_IF(INFO, num_cookies > 10) << "Got lots of cookies";

INFO 是输出日志的级别,一共包括 INFO,WARNING,ERROR 和 FATAL 这四种,其中 FATAL 会在日志输出后自动引发一个崩溃。LOG_IF 提供了额外的条件判断,条件成立时才输出日志。DLOG 跟 LOG 的区别是 DLOG 只在 DEBUG 版本才生效,而 VLOG 跟 LOG 的区别是可以用 verbose 级别来控制是否生效,比如:

VLOG(1) << "I'm printed when you run the program with --v=1 or more";VLOG(2) << "I'm printed when you run the program with --v=2 or more";

当启动开关 --v=1 时 VLOG 1 以上的级别生效。

调用跟踪

Chromium 提供了强大的 Tracing 机制,在 Android 上也对接了 Android Systrace 机制,所以对我们来说,最简单的方式就打开 Chromium Tracing,然后通过 Android Systrace 捕捉跟踪的输出。

在 TestShell 里面,我们可以通过设置 BrowserActivity.ENABLE_ATRACE 开启 Chromium Tracing,或者通过菜单开启,然后调用 Android systrace 命令捕捉即可。

如果要增加跟踪的方法,最简单的方式是使用如下代码:

  TRACE_EVENT0("android_webview", "BrowserViewRenderer::OnDrawHardware");

更复杂的跟踪方式可以参考 trace_event.h 里面的说明文档。

调用堆栈

base::debug::StackTrace 提供了调用堆栈打印的功能,StackTrace 会在被构造的时候存储当前的调用堆栈数据,然后可以通过它直接打印到控制台或者获取相应的文本,在 Android 上是通过 logcat 输出 error 日志。一般来说 StackTrace 可以作为函数的临时变量输出当前函数的调用堆栈,也可以作为对象的成员变量记录对象创建时的调用堆栈。StackTrace 输出的是地址信息,还需要使用符号表和对应的工具翻译成可读的函数名字。

下面是一个简单的使用例程和输出的结果:

  1. void AwContents::Destroy(JNIEnv* env, jobject obj) {

  2. base::debug::StackTrace().Print();

  3. ...

  4. }

  5. #00 0x751c38a1 /data/app-lib/com.uc.webkit.test-1/libwebviewuc.so+0x001f08a1

  6. #01 0x4153f30f /system/lib/libdvm.so+0x0001d30f

系统监控

内存监控

Chromium 提供了 MemoryPressureListener 接口,在 Android 上实际对接了 ComponentCallbacks2 的 onTrimMemory 和 onLowMemory(MemoryPressureListener.java)。

使用方式如下:

  1. void OnMemoryPressure(MemoryPressureLevel memory_pressure_level) {

  2. ...

  3. }

  4. // Start listening.

  5. MemoryPressureListener* my_listener =

  6. new MemoryPressureListener(base::Bind(&OnMemoryPressure));

  7. ...

  8. // Stop listening.

  9. delete my_listener;

  • 创建 MemoryPressureListener 对象,并传入一个 Callback 对象,启动监听;

  • 销毁 MemoryPressureListener 停止监听;

  • Callback 对象会在创建 MemoryPressureListener 的线程被调用,调用是异步的,通过线程消息,即使这个线程就是 Android 的 UI 线程

  • MemoryPressureLevel 包括 MEMORY_PRESSURE_MODERATEMEMORY_PRESSURE_CRITICAL,跟 Android ComponentCallbacks2.onTrimMemory 和 onLowMemory 的对应关系如下面代码所示;

  1. // Modules are advised to free buffers that are cheap to re-allocate and not

  2. // immediately needed.

  3. DEFINE_MEMORY_PRESSURE_LEVEL(MEMORY_PRESSURE_MODERATE, 0)

  4. // At this level, modules are advised to free all possible memory.

  5. // The alternative is to be killed by the system, which means all memory will

  6. // have to be re-created, plus the cost of a cold start.

  7. DEFINE_MEMORY_PRESSURE_LEVEL(MEMORY_PRESSURE_CRITICAL, 2)

  8. public void onTrimMemory(int level) {

  9. maybeNotifyMemoryPresure(level);

  10. }

  11. public void onLowMemory() {

  12. nativeOnMemoryPressure(MemoryPressureLevelList.MEMORY_PRESSURE_CRITICAL);

  13. }

  14. public static void maybeNotifyMemoryPresure(int level) {

  15. if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {

  16. nativeOnMemoryPressure(MemoryPressureLevelList.MEMORY_PRESSURE_CRITICAL);

  17. } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND ||

  18. level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {

  19. // Don't notifiy on TRIM_MEMORY_UI_HIDDEN, since this class only

  20. // dispatches actionable memory pressure signals to native.

  21. nativeOnMemoryPressure(MemoryPressureLevelList.MEMORY_PRESSURE_MODERATE);

  22. }

  23. }

Android 相关

base::BuildInfo 基本上等同于 Android 的 android.os.Build 的 Native 版本,可以通过它获得一些 Build 相关的信息,比如系统版本号等。

path_utils.h 提供一些辅助函数,用于获取 Android 系统或者应用相关的特定目录,比如 GetDataDirectory 返回当前应用的 Data 目录。

jni_string.h 提供了一些跟字串相关的辅助函数,用于 Java String 和 Native String 之间的转换,比如 ConvertJavaStringToUTF8,ConvertUTF8ToJavaString 等。

jni_array.h 提供了一些跟数组相关的辅助函数,比如 ToJavaXXXArray 将一个 Native 数组转换成一个 Java 数组对象,转换过程中原始的数据会被拷贝

ScopedJavaLocalRef, ScopedJavaGlobalRef,JavaObjectWeakGlobalRef

base::android::ScopedJavaLocalRef 和 base::android::ScopedJavaGlobalRef 提供了在 Native 端持有一个 Java 对象,并在 Scoped 对象被销毁时自动解除该 Java 对象引用的机制,有点类似是针对 Java 对象的 scoped_refptr。

ScopedJavaLocalRef 对应 JNI 的 LocalRef,作为栈对象在函数内部使用,一般用于在函数结束时自动解除关联的 Java 对象的引用,或者作为函数的返回值传递 Java 对象的引用给它的调用者:

bool GetDatabaseDirectory(FilePath* result) {  JNIEnv* env = AttachCurrentThread();  ScopedJavaLocalRef<jstring> path =      Java_PathUtils_getDatabaseDirectory(env, GetApplicationContext());  FilePath data_path(ConvertJavaStringToUTF8(path));  *result = data_path;  return true;}

ScopedJavaGlobalRef 对应 JNI 的 GlobalRef,一般作为类的成员变量,或者在需要超过某个函数的调用生命周期去持有一个 Java 对象的状况下使用。

下面的例子演示了一个异步回调的处理,我们需要一个 ScopedJavaGlobalRef 保证这个关联的 Java 对象在回调函数被真正执行时任然存活而不会被销毁,base::Owened 将 j_callback 的拥有权转移给 base::Bind 创建的 Callback 对象。

  1. void GenerateMHTMLCallback(ScopedJavaGlobalRef<jobject>* callback,

  2. const base::FilePath& path, int64 size) {

  3. JNIEnv* env = AttachCurrentThread();

  4. // Android files are UTF8, so the path conversion below is safe.

  5. Java_AwContents_generateMHTMLCallback(

  6. env,

  7. ConvertUTF8ToJavaString(env, path.AsUTF8Unsafe()).obj(),

  8. size, callback->obj());

  9. }

  10. } // namespace

  11. void AwContents::GenerateMHTML(JNIEnv* env, jobject obj,

  12. jstring jpath, jobject callback) {

  13. DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  14. ScopedJavaGlobalRef<jobject>* j_callback = new ScopedJavaGlobalRef<jobject>();

  15. j_callback->Reset(env, callback);

  16. base::FilePath target_path(ConvertJavaStringToUTF8(env, jpath));

  17. web_contents_->GenerateMHTML(

  18. target_path,

  19. base::Bind(&GenerateMHTMLCallback, base::Owned(j_callback), target_path));

  20. }

base::JavaObjectWeakGlobalRef 用于持有一个 Java 对象的弱引用,对应 JNI 的 WeakGlobalRef,当需要使用这个 Java 对象时可以通过 JavaObjectWeakGlobalRef.get 返回一个 ScopedJavaLocalRef。

工具类型

这一节的内容包括一些比较零散,无法归类的工具类型。

SupportsUserData

base::SupportsUserData 是用来给一个对象增加 UserData 支持的辅助类,User Data,也叫 Client Data,一般使用 Key-Value 的方式存储,是这个对象的使用者将自己或者其它的使用者需要用到的一些数据附加在这个对象上面的一种机制,对象本身只是作为这些数据的一个载体。

需要承载 User Data 的类,需要继承 SupportsUserData,比如 content::WebContents,而需要作为 User Data 存储的类型,需要继承 SupportsUserData::Data,使用 void* 指针做 key。SupportsUserData 是非线程安全的,如果跨线程使用,需要使用者自己保证线程安全。

LazyInstance

LazyInstance 提供了一种延迟创建全局静态对象的方式,它的优点是:

  • 它预先在程序的静态内存区分配了对象的内存,当对象创建时就不需要在堆上分配内存,加快了对象创建的速度和减少堆内存碎片;

  • 它的对象创建是线程安全的,不用担心多线程竞争的状况;

  • 它延迟对象的创建到第一次使用的时候,避免在程序启动时创建,减少了启动的时间开销;

总的来说 LazyInstance 就像是函数内部的静态对象的线程安全版本,下面是使用的例程:

  1. static LazyInstance<MyClass> my_instance = LAZY_INSTANCE_INITIALIZER;

  2. void SomeMethod() {

  3. my_instance.Get().SomeMethod(); // MyClass::SomeMethod()

  4. MyClass* ptr = my_instance.Pointer();

  5. ptr->DoDoDo(); // MyClass::DoDoDo

  6. }

如果明确不需要销毁对象,不需要调用析构函数,可以使用 Leaky 类型定义(实际上在 CAW 上,用不用 Leaky 都一样,参看下面的 Singleton):

base::LazyInstance<GlobalTileManager>::Leaky g_tile_manager =    LAZY_INSTANCE_INITIALIZER;

Singleton

一般方便自己的类型实现单例模式的辅助类,使用的例程如下:

  1. // In your header:

  2. template <typename T> struct DefaultSingletonTraits;

  3. class FooClass {

  4. public:

  5. static FooClass* GetInstance();

  6. void Bar() { ... }

  7. private:

  8. FooClass() { ... }

  9. friend struct DefaultSingletonTraits<FooClass>;

  10. DISALLOW_COPY_AND_ASSIGN(FooClass);

  11. };

  12. // In your source file:

  13. FooClass* FooClass::GetInstance() {

  14. return Singleton<FooClass>::get();

  15. }

  16. // And to call methods on FooClass:

  17. FooClass::GetInstance()->Bar();

需要注意的是:

  • Singleton::get() 的调用方法必须命名为 GetInstance;

  • GetInstance 不能是 inline 的,也就是说它的实现不能放在头文件里面;

  • Singleton::get() 有一定的时间开销,避免在循环的 inner loop 里面每次都调用;

  • 对于 CAW 来说,使用 Singleton 的类型的析构函数是不会被自动调用的,对于 Chrome for Android 来说,在子进程退出时,使用 Singleton 的类型的析构函数在进程退出时被自动被调用,另外 LazyInstance 的状况也一样;

总的来说,Chromium 并不鼓励使用单例模式,所以能不用还是不用。

AutoReset

base::AutoReset 是一个很简单的辅助类,它一般作为栈对象使用,用途是构造时保存变量原有的值并设置新的值,当生命周期结束,析构的时候恢复变量原有的值。

  {    base::AutoReset<bool> frame_resetter(&viewport_clip_valid_for_dcheck_,                                         true);    layer_tree_host_->SetNeedsRedrawRect(clip_);    layer_tree_host_->Composite(gfx::FrameTime::Now());  }

ObserverList,ObserverListThreadSafe

base::ObserverList 是帮助实现观察者模式的一个辅助类,顾名思义,它提供了一个观察者列表容器。除此以外,使用 ObserverList 而不是直接使用 std::vector 或者 std::list 的原因还在于 ObserverList 提供了一个特定版本的迭代器实现,在迭代的过程中从容器中删除自己或者其它的 Observer 是安全的,迭代器的 GetNext 方法会自动检查容器是否被修改过,正确返回修改过后的容器的下一个元素。

一般的使用方式如下:

  1. class MyWidget {

  2. public:

  3. ...

  4. class Observer {

  5. public:

  6. virtual void OnFoo(MyWidget* w) = 0;

  7. virtual void OnBar(MyWidget* w, int x, int y) = 0;

  8. };

  9. void AddObserver(Observer* obs) {

  10. observer_list_.AddObserver(obs);

  11. }

  12. void RemoveObserver(Observer* obs) {

  13. observer_list_.RemoveObserver(obs);

  14. }

  15. void NotifyFoo() {

  16. FOR_EACH_OBSERVER(Observer, observer_list_, OnFoo(this));

  17. }

  18. void NotifyBar(int x, int y) {

  19. FOR_EACH_OBSERVER(Observer, observer_list_, OnBar(this, x, y));

  20. }

  21. private:

  22. ObserverList<Observer> observer_list_;

  23. };

base::ObserverListThreadSafe 相当于 base::ObserverList 的线程安全版本,通过 ObserverListThreadSafe.Notify 可以调用注册的 Observer 的某一个指定的方法,并且这个方法是在这个 Observer 所属的线程上被调用, 所谓 Observer 所属的线程就是指将 Observer 加入到 ObserverListThreadSafe 里面的那个调用线程。为了做到上述这一点,ObserverListThreadSafe 是通过 PostTask 到线程的消息循环来实现的,这也意味着跟 ObserverList 不同的是,Notify 和 Callback 被调用是异步的,而 ObserverList 是同步的,MemoryPressureListener 的内部实现就使用了 ObserverListThreadSafe。

  1. MemoryPressureListener::MemoryPressureListener(

  2. const MemoryPressureListener::MemoryPressureCallback& callback)

  3. : callback_(callback) {

  4. g_observers.Get().AddObserver(this);

  5. }

  6. MemoryPressureListener::~MemoryPressureListener() {

  7. g_observers.Get().RemoveObserver(this);

  8. }

  9. void MemoryPressureListener::Notify(MemoryPressureLevel memory_pressure_level) {

  10. callback_.Run(memory_pressure_level);

  11. }

  12. // static

  13. void MemoryPressureListener::NotifyMemoryPressure(

  14. MemoryPressureLevel memory_pressure_level) {

  15. TRACE_EVENT1("memory", "MemoryPressureListener::NotifyMemoryPressure",

  16. "level", memory_pressure_level);

  17. g_observers.Get().Notify(&MemoryPressureListener::Notify,

  18. memory_pressure_level);

  19. }

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这