深入理解 Node.js 的 Buffer

发表于 3年以前  | 总阅读数:323 次

前言:Buffer 模块是 Node.js 非常重要的模块,很多模块都依赖它,本文介绍一下 Buffer 模块底层的原理,包括 Buffer 的核心实现和 V8 堆外内存等内容。

1 Buffer 的实现

1.1 Buffer 的 JS 层实现

Buffer 模块的实现虽然非常复杂,代码也非常多,但是很多都是编码解码以及内存分配管理的逻辑,我们从常用的使用方式 Buffer.from 来看看 Buffer 的核心实现。

Buffer.from = function from(value, encodingOrOffset, length) {
  return fromString(value, encodingOrOffset);

};


function fromString(string, encoding) {
  return fromStringFast(string, ops);

}


function fromStringFast(string, ops) {
  const length = ops.byteLength(string);
  // 长度太长,从 C++ 层分配
  if (length >= (Buffer.poolSize >>> 1))
    return createFromString(string, ops.encodingVal);
  // 剩下的不够了,扩容 
  if (length > (poolSize - poolOffset))
    createPool();
  // 从 allocPool (ArrayBuffer)中分配内存
  let b = new FastBuffer(allocPool, poolOffset, length);
  const actual = ops.write(b, string, 0, length);
  poolOffset += actual;
  alignPool();
  return b;

}

from 的逻辑如下:1. 如果长度大于 Node.js 设置的阈值,则调用 createFromString 通过 C++ 层直接分配内存。2. 否则判断之前剩下的内存是否足够,足够则直接分配。Node.js 初始化时会首先分配一大块内存由 JS 管理,每次从这块内存了切分一部分给使用方,如果不够则扩容。我们看看 createPool。

// 分配一个内存池
function createPool() {
  poolSize = Buffer.poolSize;
  // 拿到底层的 ArrayBuffer
  allocPool = createUnsafeBuffer(poolSize).buffer;
  poolOffset = 0;

}


function createUnsafeBuffer(size) {
  zeroFill[0] = 0;
  try {
    return new FastBuffer(size);
  } finally {
    zeroFill[0] = 1;
  }

}


class FastBuffer extends Uint8Array {}

我们看到最终调用 Uint8Array 实现了内存分配。3. 通过 new FastBuffer(allocPool, poolOffset, length) 从内存池中分配一块内存。如下图所示。

1.2 Buffer 的 C++ 层实现

分析 C++ 层之前我们先看一下 V8 里下面几个对象的关系图。

接着来看看通过 createFromString 直接从 C++ 申请内存的实现。

void CreateFromString(const FunctionCallbackInfo<Value>& args) {
  enum encoding enc = static_cast<enum encoding>(args[1].As<Int32>()->Value());
  Local<Object> buf;
  if (New(args.GetIsolate(), args[0].As<String>(), enc).ToLocal(&buf))
    args.GetReturnValue().Set(buf);

}


MaybeLocal<Object> New(Isolate* isolate,
                       Local<String> string,
                       enum encoding enc) {
  EscapableHandleScope scope(isolate);

  size_t length;
  // 计算长度
  if (!StringBytes::Size(isolate, string, enc).To(&length))
    return Local<Object>();
  size_t actual = 0;
  char* data = nullptr;
  // 直接通过 realloc 在进程堆上申请一块内存
  data = UncheckedMalloc(length);
  // 按照编码转换数据
  actual = StringBytes::Write(isolate, data, length, string, enc);
  return scope.EscapeMaybe(New(isolate, data, actual));

}


MaybeLocal<Object> New(Isolate* isolate, char* data, size_t length) {
  EscapableHandleScope handle_scope(isolate);
  Environment* env = Environment::GetCurrent(isolate);
  Local<Object> obj;
  if (Buffer::New(env, data, length).ToLocal(&obj))
    return handle_scope.Escape(obj);
  return Local<Object>();

}


MaybeLocal<Object> New(Environment* env,
                       char* data,
                       size_t length) {
  // JS 层变量释放后使得这块内存没人用了,GC 时在回调里释放这块内存               
  auto free_callback = [](char* data, void* hint) { free(data); };
  return New(env, data, length, free_callback, nullptr);

}


MaybeLocal<Object> New(Environment* env,
                       char* data,
                       size_t length,
                       FreeCallback callback,
                       void* hint) {
  EscapableHandleScope scope(env->isolate());
  // 创建一个 ArrayBuffer
  Local<ArrayBuffer> ab =
      CallbackInfo::CreateTrackedArrayBuffer(env, data, length, callback, hint);
  /* 
    创建一个 Uint8Array 
    Buffer::New => Local<Uint8Array> ui = Uint8Array::New(ab, byte_offset, length)
  */
  MaybeLocal<Uint8Array> maybe_ui = Buffer::New(env, ab, 0, length);

  Local<Uint8Array> ui;
  if (!maybe_ui.ToLocal(&ui))
    return MaybeLocal<Object>();

  return scope.Escape(ui);

}

通过一系列的调用,最后通过 CreateTrackedArrayBuffer 创建了一个 ArrayBuffer,再通过 ArrayBuffer 创建了一个 Uint8Array。接着看一下 CreateTrackedArrayBuffer 的实现。

Local<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
    Environment* env,
    char* data,
    size_t length,
    FreeCallback callback,
    void* hint) {
  // 管理回调
  CallbackInfo* self = new CallbackInfo(env, callback, data, hint);
  // 用自己申请的内存创建一个 BackingStore,并设置 GC 回调
  std::unique_ptr<BackingStore> bs =
      ArrayBuffer::NewBackingStore(data, length, [](void*, size_t, void* arg) {
        static_cast<CallbackInfo*>(arg)->OnBackingStoreFree();
      }, self);
  // 通过 BackingStore 创建 ArrayBuffer
  Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(bs));
  return ab;

}

看一下 NewBackingStore 的实现。

std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
    void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter,
    void* deleter_data) {
  std::unique_ptr<i::BackingStoreBase> backing_store = i::BackingStore::WrapAllocation(data, byte_length, deleter, deleter_data,
                                      i::SharedFlag::kNotShared);
  return std::unique_ptr<v8::BackingStore>(
      static_cast<v8::BackingStore*>(backing_store.release()));

}


std::unique_ptr<BackingStore> BackingStore::WrapAllocation(
    void* allocation_base, size_t allocation_length,
    v8::BackingStore::DeleterCallback deleter, void* deleter_data,
    SharedFlag shared) {
  bool is_empty_deleter = (deleter == v8::BackingStore::EmptyDeleter);
  // 新建一个 BackingStore 
  auto result = new BackingStore(allocation_base,    // start
                                 allocation_length,  // length
                                 allocation_length,  // capacity
                                 shared,             // shared
                                 false,              // is_wasm_memory
                                 true,               // free_on_destruct
                                 false,              // has_guard_regions
                                 // 说明释放内存由调用方执行
                                 true,               // custom_deleter
                                 is_empty_deleter);  // empty_deleter
  // 保存回调需要的信息                               
  result->type_specific_data_.deleter = {deleter, deleter_data};
  return std::unique_ptr<BackingStore>(result);

}

NewBackingStore 最终是创建了一个 BackingStore 对象。我们再看一下 GC 时 BackingStore 的析构函数里都做了什么。

BackingStore::~BackingStore() {
  if (custom_deleter_) {
    type_specific_data_.deleter.callback(buffer_start_, byte_length_,
                                         type_specific_data_.deleter.data);
    Clear();
    return;
  }

}

析构的时候会执行创建 BackingStore 时保存的回调。我们看一下管理回调的 CallbackInfo 的实现。

CallbackInfo::CallbackInfo(Environment* env,
                           FreeCallback callback,
                           char* data,
                           void* hint)
    : callback_(callback),
      data_(data),
      hint_(hint),
      env_(env) {
  env->AddCleanupHook(CleanupHook, this);
  env->isolate()->AdjustAmountOfExternalAllocatedMemory(sizeof(*this));

}

CallbackInfo 的实现很简单,主要的地方是 AdjustAmountOfExternalAllocatedMemory。该函数告诉 V8 堆外内存增加了多少个字节,V8 会根据内存的数据做适当的 GC。CallbackInfo 主要是保存了回调和内存地址。接着在 GC 的时候会回调 CallbackInfo 的 OnBackingStoreFree。

void CallbackInfo::OnBackingStoreFree() {
  std::unique_ptr<CallbackInfo> self { this };
  Mutex::ScopedLock lock(mutex_);
  // check 阶段执行 CallAndResetCallback
  env_->SetImmediateThreadsafe([self = std::move(self)](Environment* env) {
    self->CallAndResetCallback();
  });}void CallbackInfo::CallAndResetCallback() {
  FreeCallback callback;
  {
    Mutex::ScopedLock lock(mutex_);
    callback = callback_;
    callback_ = nullptr;
  }
  if (callback != nullptr) {
    // 堆外内存减少了这么多个字节
    int64_t change_in_bytes = -static_cast<int64_t>(sizeof(*this));
    env_->isolate()->AdjustAmountOfExternalAllocatedMemory(change_in_bytes);
    // 执行回调,通常是释放内存
    callback(data_, hint_);
  }

}

1.3 Buffer C++ 层的另一种实现

刚才介绍的 C++ 实现中内存是由自己分配并释放的,下面介绍另一种内存的分配和释放由 V8 管理的场景。以 Buffer 的提供的 EncodeUtf8String 函数为例,该函数实现字符串的编码。

static void EncodeUtf8String(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();
  // 被编码的字符串
  Local<String> str = args[0].As<String>();
  size_t length = str->Utf8Length(isolate);
  // 分配内存
  AllocatedBuffer buf = AllocatedBuffer::AllocateManaged(env, length);
  // 编码
  str->WriteUtf8(isolate,
                 buf.data(),
                 -1,  // We are certain that `data` is sufficiently large
                 nullptr,
                 String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8);
  // 基于上面申请的 buf 内存新建一个 Uint8Array              
  auto array = Uint8Array::New(buf.ToArrayBuffer(), 0, length);
  args.GetReturnValue().Set(array);

}

我们重点分析 AllocatedBuffer::AllocateManaged。

AllocatedBuffer AllocatedBuffer::AllocateManaged(
    Environment* env,
    size_t size) {
  NoArrayBufferZeroFillScope no_zero_fill_scope(env->isolate_data());
  std::unique_ptr<v8::BackingStore> bs = v8::ArrayBuffer::NewBackingStore(env->isolate(), size);
  return AllocatedBuffer(env, std::move(bs));

}

AllocateManaged 调用 NewBackingStore 申请了内存。

std::unique_ptr<v8::BackingStore> v8::ArrayBuffer::NewBackingStore(
    Isolate* isolate, size_t byte_length) {

  i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
  std::unique_ptr<i::BackingStoreBase> backing_store =
      i::BackingStore::Allocate(i_isolate, byte_length,
                                i::SharedFlag::kNotShared,
                                i::InitializedFlag::kZeroInitialized);

  return std::unique_ptr<v8::BackingStore>(
      static_cast<v8::BackingStore*>(backing_store.release()));

}

继续看 BackingStore::Allocate。

std::unique_ptr<BackingStore> BackingStore::Allocate(
    Isolate* isolate, size_t byte_length, SharedFlag shared,
    InitializedFlag initialized) {
  void* buffer_start = nullptr;
  // ArrayBuffer 内存分配器,可以自定义,V8 默认提供的是使用平台相关的堆内存分析函数,比如 malloc
  auto allocator = isolate->array_buffer_allocator();
  if (byte_length != 0) {
    auto allocate_buffer = [allocator, initialized](size_t byte_length) {
      // 分配内存
      void* buffer_start = allocator->Allocate(byte_length);
      return buffer_start;
    };
    // 同步执行 allocate_buffer 分配内存
    buffer_start = isolate->heap()->AllocateExternalBackingStore(allocate_buffer, byte_length);
  }
  // 新建 BackingStore 管理内存
  auto result = new BackingStore(buffer_start,  // start
                                 byte_length,   // length
                                 byte_length,   // capacity
                                 shared,        // shared
                                 false,         // is_wasm_memory
                                 true,          // free_on_destruct
                                 false,         // has_guard_regions
                                 false,         // custom_deleter
                                 false);        // empty_deleter

  return std::unique_ptr<BackingStore>(result);

}

BackingStore::Allocate 分配一块内存并新建 BackingStore 对象管理这块内存,内存分配器是在初始化 V8 的时候设置的。这里我们再看一下 AllocateExternalBackingStore 函数的逻辑。

void* Heap::AllocateExternalBackingStore(
    const std::function<void*(size_t)>& allocate, size_t byte_length) {
   // 可能需要触发 GC
   if (!always_allocate()) {
    size_t new_space_backing_store_bytes =
        new_space()->ExternalBackingStoreBytes();
    if (new_space_backing_store_bytes >= 2 * kMaxSemiSpaceSize &&
        new_space_backing_store_bytes >= byte_length) {
      CollectGarbage(NEW_SPACE,
                     GarbageCollectionReason::kExternalMemoryPressure);
    }
  }
  // 分配内存
  void* result = allocate(byte_length);
  // 成功则返回
  if (result) return result;
  // 失败则进行 GC
  if (!always_allocate()) {
    for (int i = 0; i < 2; i++) {
      CollectGarbage(OLD_SPACE,
                     GarbageCollectionReason::kExternalMemoryPressure);
      result = allocate(byte_length);
      if (result) return result;
    }
    isolate()->counters()->gc_last_resort_from_handles()->Increment();
    CollectAllAvailableGarbage(
        GarbageCollectionReason::kExternalMemoryPressure);
  }
  // 再次分配,失败则返回失败
  return allocate(byte_length);

}

我们看到通过 BackingStore 申请内存失败时会触发 GC 来腾出更多的可用内存。分配完内存后,最终以 BackingStore 对象为参数,返回一个 AllocatedBuffer 对象。

AllocatedBuffer::AllocatedBuffer(
    Environment* env, std::unique_ptr<v8::BackingStore> bs)
    : env_(env), backing_store_(std::move(bs)) {}

接着把 AllocatedBuffer 对象转成 ArrayBuffer 对象。

v8::Local<v8::ArrayBuffer> AllocatedBuffer::ToArrayBuffer() {
  return v8::ArrayBuffer::New(env_->isolate(), std::move(backing_store_));

}

最后把 ArrayBuffer 对象传入 Uint8Array 返回一个 Uint8Array 对象返回给调用方。

2 Uint8Array 的使用和实现

从前面的实现中可以看到 C++ 层的实现中,内存都是从进程的堆中分配的,那么 JS 层通过 Uint8Array 申请的内存是否也是在进程堆中申请的呢?下面我们看看 V8 中 Uint8Array 的实现。Uint8Array 有多种创建方式,我们只看 new Uint8Array(length) 的实现。

transitioning macro ConstructByLength(implicit context: Context)(
    map: Map, lengthObj: JSAny,
    elementsInfo: typed_array::TypedArrayElementsInfo): JSTypedArray {
  try {
    // 申请的内存大小
    const length: uintptr = ToIndex(lengthObj);
    // 拿到创建 ArrayBuffer 的函数
    const defaultConstructor: Constructor = GetArrayBufferFunction();
    const initialize: constexpr bool = true;
    return TypedArrayInitialize(
        initialize, map, length, elementsInfo, defaultConstructor)
        otherwise RangeError;
  }

}


transitioning macro TypedArrayInitialize(implicit context: Context)(
    initialize: constexpr bool, map: Map, length: uintptr,
    elementsInfo: typed_array::TypedArrayElementsInfo,
    bufferConstructor: JSReceiver): JSTypedArray labels IfRangeError {

  const byteLength = elementsInfo.CalculateByteLength(length);
  const byteLengthNum = Convert<Number>(byteLength);
  const defaultConstructor = GetArrayBufferFunction();
  const byteOffset: uintptr = 0;

  try {
    // 创建 JSArrayBuffer
    const buffer = AllocateEmptyOnHeapBuffer(byteLength);
    const isOnHeap: constexpr bool = true;
    // 通过 buffer 创建 TypedArray
    const typedArray = AllocateTypedArray(
        isOnHeap, map, buffer, byteOffset, byteLength, length);
    // 内存置 0
    if constexpr (initialize) {
      const backingStore = typedArray.data_ptr;
      typed_array::CallCMemset(backingStore, 0, byteLength);
    }

    return typedArray;
  }

}

主要逻辑分为两步,首先通过 AllocateEmptyOnHeapBuffer 申请一个 JSArrayBuffer,然后以 JSArrayBuffer 创建一个 TypedArray。我们先看一下 AllocateEmptyOnHeapBuffer。

TNode<JSArrayBuffer> TypedArrayBuiltinsAssembler::AllocateEmptyOnHeapBuffer(
    TNode<Context> context, TNode<UintPtrT> byte_length) {

  TNode<NativeContext> native_context = LoadNativeContext(context);
  TNode<Map> map = CAST(LoadContextElement(native_context, Context::ARRAY_BUFFER_MAP_INDEX));
  TNode<FixedArray> empty_fixed_array = EmptyFixedArrayConstant();
  // 申请一个 JSArrayBuffer 对象所需要的内存
  TNode<JSArrayBuffer> buffer = UncheckedCast<JSArrayBuffer>(Allocate(JSArrayBuffer::kSizeWithEmbedderFields));
  // 初始化对象的属性
  StoreMapNoWriteBarrier(buffer, map);
  StoreObjectFieldNoWriteBarrier(buffer, JSArray::kPropertiesOrHashOffset, empty_fixed_array);
  StoreObjectFieldNoWriteBarrier(buffer, JSArray::kElementsOffset, empty_fixed_array);
  int32_t bitfield_value = (1 << JSArrayBuffer::IsExternalBit::kShift) |
                           (1 << JSArrayBuffer::IsDetachableBit::kShift);
  StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kBitFieldOffset, Int32Constant(bitfield_value));
  StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kByteLengthOffset, byte_length);
  // 设置 buffer 为 nullptr                               
  StoreJSArrayBufferBackingStore(buffer, EncodeExternalPointer(ReinterpretCast<RawPtrT>(IntPtrConstant(0))));
  StoreObjectFieldNoWriteBarrier(buffer, JSArrayBuffer::kExtensionOffset, IntPtrConstant(0));
  for (int offset = JSArrayBuffer::kHeaderSize; offset < JSArrayBuffer::kSizeWithEmbedderFields; offset += kTaggedSize) {
    StoreObjectFieldNoWriteBarrier(buffer, offset, SmiConstant(0));
  }
  return buffer;

}

AllocateEmptyOnHeapBuffer 申请了一个空的 JSArrayBuffer 对象,空的意思是说没有存储数据的内存。接着看基于 JSArrayBuffer 对象 通过 AllocateTypedArray 创建一个 TypedArray。

transitioning macro AllocateTypedArray(implicit context: Context)(
    isOnHeap: constexpr bool, map: Map, buffer: JSArrayBuffer,
    byteOffset: uintptr, byteLength: uintptr, length: uintptr): JSTypedArray {
  // 从 V8 堆中申请存储数据的内存
  let elements: ByteArray = AllocateByteArray(byteLength);
  // 申请一个 JSTypedArray 对象
  const typedArray = UnsafeCast<JSTypedArray>(AllocateFastOrSlowJSObjectFromMap(map));
  // 初始化属性
  typedArray.elements = elements;
  typedArray.buffer = buffer;
  typedArray.byte_offset = byteOffset;
  typedArray.byte_length = byteLength;
  typedArray.length = length;
  typed_array::SetJSTypedArrayOnHeapDataPtr(typedArray, elements, byteOffset);
  SetupTypedArrayEmbedderFields(typedArray);
  return typedArray;

}

我们发现 Uint8Array 申请的内存是基于 V8 堆的,而不是 V8 的堆外内存,这难道和 C++ 层的实现不一样?Uint8Array 的内存的确是基于 V8 堆的,比如我像下面这样使用的时候。

const arr = new Uint8Array(1);
arr[0] = 65;

但是如果我们使用 arr.buffer 的时候,情况就不一样了。我们看看具体的实现。

BUILTIN(TypedArrayPrototypeBuffer) {
  HandleScope scope(isolate);
  CHECK_RECEIVER(JSTypedArray, typed_array,
                 "get %TypedArray%.prototype.buffer");
  return *typed_array->GetBuffer();

}

接着看 GetBuffer 的实现。

Handle<JSArrayBuffer> JSTypedArray::GetBuffer() {
  Isolate* isolate = GetIsolate();
  Handle<JSTypedArray> self(*this, isolate);
  // 拿到 TypeArray 对应的 JSArrayBuffer 对象
  Handle<JSArrayBuffer> array_buffer(JSArrayBuffer::cast(self->buffer()), isolate);
  // 分配过了直接返回
  if (!is_on_heap()) {
   return array_buffer;
  }
  size_t byte_length = self->byte_length();
  // 申请 byte_length 字节内存存储数据
  auto backing_store = BackingStore::Allocate(isolate, byte_length, SharedFlag::kNotShared, InitializedFlag::kUninitialized);
  // 关联 backing_store 到 array_buffer
  array_buffer->Setup(SharedFlag::kNotShared, std::move(backing_store));
  return array_buffer;

}

我们看到当使用 buffer 的时候,V8 会在 V8 堆外申请内存来替代初始化 Uint8Array 时在 V8 堆内分配的内存,并且把原来的数据复制过来。看一下下面的例子。

console.log(process.memoryUsage().arrayBuffers)
let a = new Uint8Array(10);
a[0] = 65;
console.log(process.memoryUsage().arrayBuffers)

我们会发现 arrayBuffers 的值是一样的,说明 Uint8Array 初始化时没有通过 arrayBuffers 申请堆外内存。接着再看下一个例子。

console.log(process.memoryUsage().arrayBuffers)
let a = new Uint8Array(1);
a[0] = 65;
a.buffer
console.log(process.memoryUsage().arrayBuffers)
console.log(new Uint8Array(a.buffer))

我们看到输出的内存增加了一个字节,输出的 a.buffer 是 [ 65 ](申请内存超 64 时会从堆外申请)。

3 堆外内存的管理

从之前的分析中我们看到,Node.js Buffer 是基于堆外内存实现的(自己申请进程堆内存或者使用 V8 默认的内存分配器),我们知道,平时使用的变量都是由 V8 负责管理内存的,那么 Buffer 所代表的堆外内存是怎么管理的呢?Buffer 的内存释放也是由 V8 跟踪的,不过释放的逻辑和堆内内存不太一样。我们通过一些例子来分析一下。

function forceGC() {
    new ArrayBuffer(1024 * 1024 * 1024);

}



setTimeout(() => {

    /*
        从 C++ 层调用 V8 对象创建内存
        let a = process.binding('buffer').createFromString("你好", 1);
    */ 
    /*
        直接使用 V8 内置对象
        let a = new ArrayBuffer(10);
    */
    // 从 C++ 层自己管理内存
    let a = process.binding('buffer').encodeUtf8String("你好");
    // 置空等待 GC
    a = null;
    // 分配一块大内存触发 GC
    process.nextTick(forceGC);

}, 1000);



const net = require('net');

net.createServer((socket) => {}).listen()

在 V8 的代码打断点,然后调试以上代码。

我们看到在超时回调里 V8 分配了一个 ArrayBufferExtension 对象并记录到 ArrayBufferSweeper 中。接着看一下触发 GC 时的逻辑。

V8 在 GC 中会调用

heap_->array_buffer_sweeper()->RequestSweepYoung() 回收堆外内存,另外 Node.js 本身似乎也使用线程去回收 堆外内存。我们再看一下自己管理内存的情况下回调的触发。

如果这样写是不会触发 BackingStore::~BackingStore 执行的,再次验证了 Uint8Array 初始化时没有使用 BackingStore。

setTimeout(() => {
   let a = new Uint8Array(1);
   // a.buffer;
   a = null;
   process.nextTick(forceGC);

});

但是如果把注释打开就可以。

4 总结

Buffer 平时用起来可能比较简单,但是如果深入研究它的实现就会发现涉及的内容不仅多,而且还复杂,不过深入理解了它的底层实现后,会有种豁然开朗的感觉,另外 Buffer 的内存是堆外内存,如果我们发现进程的内存不断增长但是 V8 堆快照大小变化不大,那可能是 Buffer 变量没有释放,理解实现能帮助我们更好地思考问题和解决问题。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/k1mYyMbsG5dC8zNzVZeA1Q

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录