chromium 常用函数
0, 各种对外的delegate,client:
MouseWheelEventQueue 的 client_ 是 InputRouterImpl
InputRouterImpl 的 client_ 是 RenderWidgetHostImpl
RenderWidgetHostImpl 的 delegate_ 是 WebContentsImpl
WebContentsImpl 的 delegate_ 是 Browser (src\chrome\browser\ui\browser.cc)
1,从ResourceId类获取整型的id值
auto myindex = quad->resource_id().GetUnsafeValue();
默认的页面style:
third_party\blink\renderer\core\html\resources\下
forced_colors.css
html.css
由 third_party\blink\public\blink_resources.grd 引用。最终由 third_party\blink\renderer\core\css\css_default_style_sheets.cc 中用。
2,blink中父类子类来回转换 IsA To DynamicTo
- DCHECK(IsA<HTMLVideoElement>(media_element));
- To<HTMLVideoElement>(media_element) 动态强制转向子类
- 转向具体类,子类。判断是否成功:
if (auto* anchor = DynamicTo<HTMLAnchorElement>(element)) return anchor->VisitedLinkHash(); //--------------------------------------------------------------------- const WebInputElement input_element = element.DynamicTo<WebInputElement>(); if (input_element.IsNull() && !form_util::IsTextAreaElement(element)) return; Image* HitTestResult::GetImage(const Node* node) { if (!node) return nullptr; LayoutObject* layout_object = node->GetLayoutObject(); if (layout_object && layout_object->IsImage()) { auto* image = To<LayoutImage>(layout_object); if (image->CachedImage() && !image->CachedImage()->ErrorOccurred()) return image->CachedImage()->GetImage(); } return nullptr; }
判断是否是form的text输入框:
form_util::IsTextAreaElementOrTextInput
void PasswordAutofillAgent::FocusedNodeHasChanged(const blink::WebNode& node) {if (!node.IsElementNode()) {focus_state_notifier_.FocusedInputChanged(FieldRendererId(),FocusedFieldType::kUnknown);return;}auto element = node.To<WebElement>();if (element.IsFormControlElement() &&form_util::IsTextAreaElement(element.To<WebFormControlElement>())) {FieldRendererId textarea_id(element.To<WebFormControlElement>().UniqueRendererFormControlId());focus_state_notifier_.FocusedInputChanged(textarea_id, FocusedFieldType::kFillableTextArea);return;}WebInputElement input_element = element.DynamicTo<WebInputElement>();if (input_element.IsNull()) {focus_state_notifier_.FocusedInputChanged(FieldRendererId(), FocusedFieldType::kUnfillableElement);return;}auto focused_field_type = FocusedFieldType::kUnfillableElement;if (input_element.IsTextField() && IsElementEditable(input_element)) {focused_input_element_ = input_element;WebString type = input_element.GetAttribute("type");if (!type.IsNull() && type == "search")focused_field_type = FocusedFieldType::kFillableSearchField;else if (input_element.IsPasswordFieldForAutofill())focused_field_type = FocusedFieldType::kFillablePasswordField;else if (base::Contains(web_input_to_password_info_, input_element))focused_field_type = FocusedFieldType::kFillableUsernameField; 确认是个密码框对应的用户名文本框elsefocused_field_type = FocusedFieldType::kFillableNonSearchField;}const FieldRendererId input_id(input_element.UniqueRendererFormControlId());focus_state_notifier_.FocusedInputChanged(input_id, focused_field_type);field_data_manager_->UpdateFieldDataMapWithNullValue(input_id, FieldPropertiesFlags::kHadFocus);}
是否url访问会放到历史记录
bool LocalFrame::NavigationShouldReplaceCurrentHistoryEntry
3. skia的使用
3.1 api各种示例,可以直接在网页编写调试。
网址:https://fiddle.skia.org/named/
wasm或者skia层的调试时,可以用skDebug输出来查看日志 别忘了字串加上最后的 换行符,否则看不到输出!!!!
SkDebugf("matrix %c= nearlyEqual\n", matrix == nearlyEqual ? '=' : '!');
类的dump函数会打印出对象信息:
SkMatrix nearlyEqual; nearlyEqual.setAll(0.7071f, -0.7071f, 0, 0.7071f, 0.7071f, 0, 0, 0, 1); nearlyEqual.dump();
输出:[ 0.7071 -0.7071 0.0000][ 0.7071 0.7071 0.0000][ 0.0000 0.0000 1.0000]
4. log输出:
#include "base/logging.h"
LOG(INFO)<<"HELLO";
DLOG(ERROR) << __func__ << ": {code=" << error->code();
-----------------------------------------------------------
调试输出字串 内核中是ToString , skia的dump
LayerImpl::ToString()
gfx::transform::ToString()
raster_source->GetSize().ToString();
request.GetResourceRequest().Url().GetString().Utf8()
---------------------------------------------------------
拼接string concat splice
方法a:
std::string dump_name = base::StringPrintf("cc/resource_memory/provider_%d/resource_%u", tracing_id_, resource_entry.first.GetUnsafeValue());
对类加个tostring()输出:
std::string SoftwareImageDecodeCacheUtils::CacheKey::ToString() const { std::ostringstream str; str << "frame_key[" << frame_key_.ToString() << "]\ntype["; switch (type_) { case kOriginal: str << "Original"; break; case kSubrectOriginal: str << "SubrectOriginal"; break; case kSubrectAndScale: str << "SubrectAndScale"; break; } str << "]\nis_nearest_neightbor[" << is_nearest_neighbor_ << "]\nsrc_rect[" << src_rect_.ToString() << "]\ntarget_size[" << target_size_.ToString() << "]\ntarget_color_params[" << target_color_params_.ToString() << "]\nhash[" << hash_ << "]"; return str.str(); }
json: base::Value
对象是一个通用的数据容器,可以包含多种类型的数据,如字典、列表、字符串、整数等。要打印输出 base::Value
对象的内容,可以将其转换为 JSON 字符串,然后打印该字符串。
// Returns parameters associated with the start of a HTTP stream job. base::Value NetLogHttpStreamJobParams(const NetLogSource& source, const GURL& original_url, const GURL& url, bool expect_spdy, bool using_quic, HttpStreamFactory::JobType job_type, RequestPriority priority) { base::Value::Dict dict; if (source.IsValid()) source.AddToEventParameters(dict); dict.Set("original_url", original_url.DeprecatedGetOriginAsURL().spec()); dict.Set("url", url.DeprecatedGetOriginAsURL().spec()); dict.Set("expect_spdy", expect_spdy); dict.Set("using_quic", using_quic); dict.Set("priority", RequestPriorityToString(priority)); dict.Set("type", NetLogHttpStreamJobType(job_type)); return base::Value(std::move(dict)); } { std::string json_string; base::JSONWriter::Write(base::Value xxx, &json_string); std::cout << json_string << std::endl; }
5. gfx等库的值想看的话用 ToString
std::cout<<LayerImpl::ToString() gfx::transform::ToString() raster_source->GetSize().ToString();
gfx 与 skia的转化:ui/gfx/geometry/skia_conversions.h
SkM44 TransformToSkM44(const Transform& tranform); Transform SkMatrixToTransform(const SkMatrix& matrix); SkIRect RectToSkIRect(const Rect& rect); SkIRectToRect SizeToSkISize
6. 字符集转换
1 2 3 4 5 6 | base::string16 utf16_output; //对应未知编码,先转成utf16,在转成utf8输出。字符集charset没有设置,就用系统当前默认的。windows为GBK。windows10可以自己把默认字符集改为UTF8。 if (!referrer_charset.empty() && ConvertToUTF16(encoded_word, referrer_charset.c_str(), &utf16_output)) { *output = base::UTF16ToUTF8(utf16_output); |
WebString FilePathToWebString(const base::FilePath& path) { if (path.empty()) return WebString(); #if BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA) return WebString::FromUTF8(path.value()); #else return WebString::FromUTF16(path.AsUTF16Unsafe()); #endif }
7. 用net库输出二进制内容
chromium打印二进制函数: #include "net/base/hex_utils.h" auto sss = base::StringPiece(info.content_disposition); LOG(INFO) << net::HexDump(sss);
// Print binary data to stdout as hex. void print_data(const SkData* data, const char* name) { if (data) { SkDebugf("\nxxd -r -p > %s << EOF", name); size_t s = data->size(); const uint8_t* d = data->bytes(); for (size_t i = 0; i < s; ++i) { if (i % 40 == 0) { SkDebugf("\n"); } SkDebugf("%02x", d[i]); } SkDebugf("\nEOF\n\n"); } }
8. 单例示例,需要c++17
sk_sp<SkFontMgr> SkFontMgr::RefDefault() { static SkOnce once; static sk_sp<SkFontMgr> singleton; once([]{ sk_sp<SkFontMgr> fm = gSkFontMgr_DefaultFactory ? gSkFontMgr_DefaultFactory() : SkFontMgr::Factory(); singleton = fm ? std::move(fm) : sk_make_sp<SkEmptyFontMgr>(); }); return singleton; }
SkOnce的实现:
查看代码
/* * Copyright 2013 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #ifndef SkOnce_DEFINED #define SkOnce_DEFINED #include "include/private/SkThreadAnnotations.h" #include <atomic> #include <utility> // SkOnce provides call-once guarantees for Skia, much like std::once_flag/std::call_once(). // // There should be no particularly error-prone gotcha use cases when using SkOnce. // It works correctly as a class member, a local, a global, a function-scoped static, whatever. class SkOnce { public: constexpr SkOnce() = default; template <typename Fn, typename... Args> void operator()(Fn&& fn, Args&&... args) { auto state = fState.load(std::memory_order_acquire); if (state == Done) { return; } // If it looks like no one has started calling fn(), try to claim that job. if (state == NotStarted && fState.compare_exchange_strong(state, Claimed, std::memory_order_relaxed, std::memory_order_relaxed)) { // Great! We'll run fn() then notify the other threads by releasing Done into fState. fn(std::forward<Args>(args)...); return fState.store(Done, std::memory_order_release); } // Some other thread is calling fn(). // We'll just spin here acquiring until it releases Done into fState. SK_POTENTIALLY_BLOCKING_REGION_BEGIN; while (fState.load(std::memory_order_acquire) != Done) { /*spin*/ } SK_POTENTIALLY_BLOCKING_REGION_END; } private: enum State : uint8_t { NotStarted, Claimed, Done}; std::atomic<uint8_t> fState{NotStarted}; }; #endif // SkOnce_DEFINED
// static BookmarkModel* BookmarkModelFactory::GetForBrowserContext( content::BrowserContext* context) { return static_cast<BookmarkModel*>( GetInstance()->GetServiceForBrowserContext(context, true)); } // static BookmarkModelFactory* BookmarkModelFactory::GetInstance() { return base::Singleton<BookmarkModelFactory>::get(); }
内核中单例
封在文件内部 namespace { using RenderFrameDevToolsMap = std::map<FrameTreeNode*, RenderFrameDevToolsAgentHost*>; base::LazyInstance<RenderFrameDevToolsMap>::Leaky g_agent_host_instances = LAZY_INSTANCE_INITIALIZER; RenderFrameDevToolsAgentHost* FindAgentHost(FrameTreeNode* frame_tree_node) { if (!g_agent_host_instances.IsCreated()) return nullptr; auto it = g_agent_host_instances.Get().find(frame_tree_node); return it == g_agent_host_instances.Get().end() ? nullptr : it->second; } } 使用: g_agent_host_instances.Get().erase(frame_tree_node_); g_agent_host_instances.Get()[frame_tree_node] = this;
9. wtf::String 到 std::string 转换
static inline std::string fromWTFString(const WTF::String& wtf_string) {
//if (wtf_string.IsNull())
// return std::string();
return std::string(wtf_string.Utf8().data(), wtf_string.Utf8().size());
}
E:\dev\chromium104\src\third_party\blink\renderer\platform\wtf\text\wtf_string.h 使用 header
std::string to wtf::String: explicit String(const std::string& s) : String(s.c_str(), s.length()) {}
10,计算耗时:
10.1 用 base 库:
base::TimeDelta g_fallback_delay = base::Seconds
(3);
//#include "base/time/time.h" #include "base/timer/elapsed_timer.h" //metrics_time_delta_ = base::TimeDelta(); base::ElapsedTimer timer; Decode(index); LOG(INFO)<<timer.Elapsed(); //输出: 20.8946 s //metrics_time_delta_ += timer.Elapsed();
const base::TimeTicks start = base::TimeTicks::Now(); 。。。。。。。。 const int64_t elapsed_us = (base::TimeTicks::Now() - start).InMicroseconds(); printf("%" PRIu64 " MB/s,\telapsed = %" PRIu64 " source=%d dest=%d\n", static_cast<uint64_t>(elapsed_us == 0 ? 0 : num_bytes / elapsed_us), static_cast<uint64_t>(elapsed_us), GetBitmapSize(&source), GetBitmapSize(&dest)); return true; }
// For other cases, only update at |kFlushDelay| intervals. This // throttles how frequently we update |m_image| and how frequently we // inform the clients which causes an invalidation of this image. In other // words, we only invalidate this image every |kFlushDelay| seconds // while loading. if (!is_pending_flushing_) { scoped_refptr<base::SingleThreadTaskRunner> task_runner = Loader()->GetLoadingTaskRunner(); base::TimeTicks now = base::TimeTicks::Now(); if (last_flush_time_.is_null()) last_flush_time_ = now; DCHECK_LE(last_flush_time_, now); base::TimeDelta flush_delay = std::max(base::TimeDelta(), last_flush_time_ - now + kFlushDelay); task_runner->PostDelayedTask(FROM_HERE, WTF::Bind(&ImageResource::FlushImageIfNeeded, WrapWeakPersistent(this)), flush_delay); is_pending_flushing_ = true;
#include "base/time/time.h" base::Time start_time_ = base::Time::Now(); base::TimeDelta delta = base::TimeTicks::Now() - start_time_; base::Time plusone = base::Time::Now() + base::Days(1); double real_wall_time = base::Time::Now().ToDoubleT(); double delta.InSecondsF() int64_t (base::Time::Now() - base::Time::UnixEpoch()).InMilliseconds(); base::TimeDelta duration int InDays() const; int InDaysFloored() const; constexpr int InHours() const; constexpr int InMinutes() const; constexpr double InSecondsF() const; constexpr int64_t InSeconds() const; double InMillisecondsF() const; int64_t InMilliseconds() const; int64_t InMillisecondsRoundedUp() const; constexpr int64_t InMicroseconds() const { return delta_; } double InMicrosecondsF() const; constexpr int64_t InNanoseconds() const; base::TimeDelta kSlowDuration = base::Seconds(1); base::TimeDelta kFastDuration = base::Milliseconds(1);
class SimpleURLLoaderImpl{ // The timer that triggers a timeout when a request takes too long. base::OneShotTimer timeout_timer_; // How long |timeout_timer_| should wait before timing out a request. A value // of zero means do not set a timeout. base::TimeDelta timeout_duration_ = base::TimeDelta(); } bool NavigationURLLoaderImpl::SetNavigationTimeout(base::TimeDelta timeout) { // If the timer has already been started, don't change it. if (timeout_timer_.IsRunning()) return false; // Fail the navigation with error code ERR_TIMED_OUT if the timer triggers // before the navigation commits. timeout_timer_.Start( FROM_HERE, timeout, base::BindOnce(&NavigationURLLoaderImpl::NotifyRequestFailed, base::Unretained(this), network::URLLoaderCompletionStatus(net::ERR_TIMED_OUT))); return true; }
10.2 std库计时
#include <chrono> class Timer { public: void start() { fStart = std::chrono::high_resolution_clock::now(); } void stop() { auto end = std::chrono::high_resolution_clock::now(); fElapsedSeconds += end - fStart; } double elapsedSeconds() { return fElapsedSeconds.count(); } private: decltype(std::chrono::high_resolution_clock::now()) fStart; std::chrono::duration<double> fElapsedSeconds{0.0}; }; Timer drawTime; drawTime.start(); 。。。。。。。。。 drawTime.stop(); fprintf(stderr, "%s use GPU %s elapsed time %8.6f s\n", gSkpName.c_str(), gUseGpu ? "true" : "false", drawTime.elapsedSeconds());
12 文件输出
内核base
#include "base/files/file_util.h" WriteFile(base::FilePath(FILE_PATH_LITERAL("c:/myskp/select-auto.html")), data, size);
文件权限判断
// Determine if the certain permissions have been granted to a file. bool HasPermissionsForFile(const base::FilePath& file, int permissions) { #if BUILDFLAG(IS_ANDROID) if (file.IsContentUri()) return HasPermissionsForContentUri(file, permissions); #endif if (!permissions || file.empty() || !file.IsAbsolute()) return false; base::FilePath current_path = file.StripTrailingSeparators(); base::FilePath last_path; int skip = 0; while (current_path != last_path) { base::FilePath base_name = current_path.BaseName(); if (base_name.value() == base::FilePath::kParentDirectory) { ++skip; } else if (skip > 0) { if (base_name.value() != base::FilePath::kCurrentDirectory) --skip; } else { FileMap::const_iterator it = file_permissions_.find(current_path); if (it != file_permissions_.end()) return (it->second & permissions) == permissions; } last_path = current_path; current_path = current_path.DirName(); } return false; }
12.1 root layer 到 picture 到 bitmap 到 png 编码到 文件输出
const cc::Layer* InspectorLayerTreeAgent::RootLayer() { return inspected_frames_->Root()->View()->RootCcLayer(); } 获取picture后输出: { auto* rLayer = MainFrame().View()->RootCcLayer(); for (const auto& layer : rLayer->children()) { if (!layer->update_rect().IsEmpty()) { if (!layer->draws_content()) return ; //获取 picture。因为sk_sp<const SkPicture>转成 sk_sp<SkPicture>,传入 MakeFromPicture。 //所以复制了一份picture sk_sp<const SkPicture> picture0 =layer->GetPicture(); sk_sp<SkData> skp = picture0->serialize(); sk_sp<SkPicture> picture = SkPicture::MakeFromData(skp.get()); //sk_sp<SkPicture>& picture = const_cast<sk_sp<SkPicture>&>(picture0); auto image=SkImage::MakeFromPicture(picture, SkISize::Make(layer->bounds().width(), layer->bounds().height()), nullptr, nullptr, SkImage::BitDepth::kU8, SkColorSpace::MakeSRGB()); sk_sp<SkData> data = image->encodeToData();//SkEncodedImageFormat::kPNG, 100 base::FilePath path = base::FilePath(FILE_PATH_LITERAL("c:\\mySkp")); char filename[128]; base::Time::Exploded exploded; base::Time::NowFromSystemTime().LocalExplode(&exploded); std::snprintf(filename, 128, "%03d-id-%03d-%02d%02d%02d%03d-%s.png", 111, 111, exploded.hour, exploded.minute, exploded.second, exploded.millisecond, ""); path = path.Append(base::FilePath::FromUTF8Unsafe(filename)); SkFILEWStream out(path.AsUTF8Unsafe().c_str()); out.write(data->data(), data->size()); } } }
skia库 文件,关于创建目录
文件,关于创建目录,文件访问权限的:
放着base那块儿 #include "base/files/file_path.h" #include "base/files/file_util.h" 放到那块儿 #include "third_party/skia/include/core/SkStream.h" base::FilePath dirpath = base::FilePath::FromUTF8Unsafe("e:\\tmp"); if (!base::CreateDirectory(dirpath) || !base::PathIsWritable(dirpath)) { std::string msg("Path is not writable: "); msg.append(dirpath.MaybeAsASCII()); LOG(ERROR) << "=======LayerTreeHost SKPicture create path error or no privillege:"<<msg; return; } std::string filename = "layer_" + base::NumberToString(layer_id_++) + ".skp"; std::string filepath = dirpath.AppendASCII(filename).MaybeAsASCII(); DCHECK(!filepath.empty()); SkFILEWStream file(filepath.c_str()); DCHECK(file.isValid()); auto data = picture->serialize(); file.write(data->data(), data->size()); file.fsync(); static void PrintDocumentTofile(v8::Isolate* isolate, const std::string& filename, sk_sp<SkDocument> (*make_doc)(SkWStream*), RenderFrameImpl* render_frame) { GpuBenchmarkingContext context(render_frame); base::FilePath path = base::FilePath::FromUTF8Unsafe(filename); if (!base::PathIsWritable(path.DirName())) { } SkFILEWStream wStream(path.MaybeAsASCII().c_str());
void write_png(const std::string& filename, const SkBitmap& bitmap) { auto data = SkEncodeBitmap(bitmap, SkEncodedImageFormat::kPNG, 0); SkFILEWStream w{filename.c_str()}; w.write(data->data(), data->size()); w.fsync(); }
编码图片,直接
#include "include/core/SkBitmap.h" #include "include/core/SkEncodedImageFormat.h" #include "include/core/SkImageEncoder.h" #include "base/files/file.h" #include "base/files/file_path.h" base::FilePath path = base::FilePath(FILE_PATH_LITERAL("c:\\mySkp")); char filename[128]; base::Time::Exploded exploded; base::Time::NowFromSystemTime().LocalExplode(&exploded); std::snprintf(filename, 128, "%03d-id-%03d-%02d%02d%02d%03d-%s", 111, 111, exploded.hour, exploded.minute, exploded.second, exploded.millisecond, ".png" ); path = path.Append(base::FilePath::FromUTF8Unsafe(filename)); SkFILEWStream out(path.AsUTF8Unsafe().c_str()); SkEncodeImage(&out, fRasterized, SkEncodedImageFormat::kPNG, 100);
目录处理:
scoped_refptr<SharedBuffer> ReadFile(const char* dir, const char* file_name) { StringBuilder file_path; if (strncmp(dir, "web_tests/", 10) == 0) { file_path.Append(test::BlinkWebTestsDir()); file_path.Append('/'); file_path.Append(dir + 10); } else { file_path.Append(test::BlinkRootDir()); file_path.Append('/'); file_path.Append(dir); } file_path.Append('/'); file_path.Append(file_name); return test::ReadFromFile(file_path.ToString()); } scoped_refptr<SharedBuffer> ReadFromFile(const String& path) { base::FilePath file_path = blink::WebStringToFilePath(path); std::string buffer; base::ReadFileToString(file_path, &buffer); return SharedBuffer::Create(buffer.data(), buffer.size()); }
base::ScopedTempDir temp_dir_; ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); history_dir_ = temp_dir_.GetPath().AppendASCII("VisitedLinkTest"); ASSERT_TRUE(base::CreateDirectory(history_dir_)); visited_file_ = history_dir_.Append(FILE_PATH_LITERAL("VisitedLinks")); ASSERT_TRUE(temp_dir_.Delete());
jpg解码只允许按照8的倍数缩放????????
// we'll only allow downscaling an image if both dimensions fit a
// whole number of MCUs or if decoding to the original size would
// cause us to exceed memory limits. The latter case is detected by
// checking the |max_numerator| returned by DesiredScaleNumerator():
// this method will return either |g_scale_denominator| if decoding to
// the original size won't exceed the memory limit (see
// |max_decoded_bytes_| in ImageDecoder) or something less than
// |g_scale_denominator| otherwise to ensure the image is downscaled.
// static unsigned JPEGImageDecoder::DesiredScaleNumerator(wtf_size_t max_decoded_bytes, wtf_size_t original_bytes, unsigned scale_denominator) { if (original_bytes <= max_decoded_bytes) return scale_denominator;//无限大,就返回 8 // Downsample according to the maximum decoded size. return static_cast<unsigned>(floor(sqrt( // MSVC needs explicit parameter type for sqrt(). static_cast<float>(max_decoded_bytes) / original_bytes * scale_denominator * scale_denominator))); // max_decoded_bytes/original_bytes * 8 * 8 }
13. 全局变量
13.1
static bool g_should_use_external_popup_menus = false; void WebView::SetUseExternalPopupMenus(bool use_external_popup_menus) { g_should_use_external_popup_menus = use_external_popup_menus; } bool WebViewImpl::UseExternalPopupMenus() { return g_should_use_external_popup_menus; }
13.2
#include "base/no_destructor.h" // static Clipboard::ClipboardMap* Clipboard::ClipboardMapPtr() { static base::NoDestructor<ClipboardMap> clipboard_map; return clipboard_map.get(); }
使用:
// static
void Clipboard::SetClipboardForCurrentThread(
std::unique_ptr<Clipboard> platform_clipboard) {
base::AutoLock lock(ClipboardMapLock());
base::PlatformThreadId id = Clipboard::GetAndValidateThreadID();
ClipboardMap* clipboard_map = ClipboardMapPtr();
// This shouldn't happen. The clipboard should not already exist.
DCHECK(!base::Contains(*clipboard_map, id));
clipboard_map
->insert({id, std::move(platform_clipboard)});
}
14, 文件保存。从blink调用了skia的库.
PaintRecord to SkPicture
#include <string> #include "base/files/file_path.h" #include "third_party/skia/include/core/SkStream.h" #include "third_party/skia/include/core/SkPicture.h" { sk_sp<cc::PaintRecord> record = canvas2d_bridge_->getLastRecord(); cc::PlaybackParams::CustomDataRasterCallback callback; auto bound = SkRect::MakeXYWH(0, 0, 800, 800); sk_sp<const SkPicture> picture = cc::ToSkPicture(record, bound, nullptr, callback); if (picture) { sk_sp<SkData> data = picture->serialize(); base::FilePath path = base::FilePath(FILE_PATH_LITERAL("D:\\mySkp")); char filename[128]; base::Time::Exploded exploded; base::Time::NowFromSystemTime().LocalExplode(&exploded); std::snprintf(filename, 128, "%03d-id-%03d-%02d%02d%02d%03d-%s", 111, 111, exploded.hour, exploded.minute, exploded.second, exploded.millisecond, ""); path = path.Append(base::FilePath::FromUTF8Unsafe(filename)); SkFILEWStream out(path.AsUTF8Unsafe().c_str()); out.write(data->data(), data->size()); } }
SkPicture to Skimage
SkImage::MakeFromPicture(picture, SkISize::Make(width, height),
nullptr, nullptr, SkImage::BitDepth::kU8,
SkColorSpace::MakeSRGB());
或者
SkPicture保存成图片png
查看代码
Vector<uint8_t> PictureSnapshot::Replay(unsigned from_step, unsigned to_step, double scale) const { const SkIRect bounds = picture_->cullRect().roundOut(); int width = ceil(scale * bounds.width()); int height = ceil(scale * bounds.height()); // TODO(fmalita): convert this to SkSurface/SkImage, drop the intermediate // SkBitmap. SkBitmap bitmap; bitmap.allocPixels(SkImageInfo::MakeN32Premul(width, height)); bitmap.eraseARGB(0, 0, 0, 0); { ReplayingCanvas canvas(bitmap, from_step, to_step); // Disable LCD text preemptively, because the picture opacity is unknown. // The canonical API involves SkSurface props, but since we're not // SkSurface-based at this point (see TODO above) we (ab)use saveLayer for // this purpose. SkAutoCanvasRestore auto_restore(&canvas, false); canvas.saveLayer(nullptr, nullptr); canvas.scale(scale, scale); canvas.ResetStepCount(); picture_->playback(&canvas, &canvas); } Vector<uint8_t> encoded_image; SkPixmap src; bool peekResult = bitmap.peekPixels(&src); DCHECK(peekResult); SkPngEncoder::Options options; options.fFilterFlags = SkPngEncoder::FilterFlag::kSub; options.fZLibLevel = 3; if (!ImageEncoder::Encode(&encoded_image, src, options)) return Vector<uint8_t>(); return encoded_image; }
picture编码成image,用 surface
auto pic = SkPicture::MakeFromData(picData, &procs); auto cullRect = pic->cullRect(); auto r = cullRect.round(); auto s = SkSurface::MakeRasterN32Premul(r.width(), r.height()); auto c = s->getCanvas(); c->drawPicture(pic); auto i = s->makeImageSnapshot(); auto data = i->encodeToData(); SkFILEWStream f(outFilename.c_str()); f.write(data->data(), data->size());
15. 生成 DisplayItemList,layer_tree_host
cc\trees\layer_tree_host_unittest_capture_content.cc
scoped_refptr<DisplayItemList> PaintContentsToDisplayList() override { auto display_list = base::MakeRefCounted<DisplayItemList>(); for (auto& holder : holders_) { display_list->StartPaint(); display_list->push<DrawTextBlobOp>( SkTextBlob::MakeFromString(holder.text().data(), SkFont()), static_cast<float>(holder.rect().x()), static_cast<float>(holder.rect().y()), holder.node_id(), PaintFlags()); display_list->EndPaintOfUnpaired(holder.rect()); } display_list->Finalize(); return display_list; } void SetupRootPictureLayer(const gfx::Size& size) { scoped_refptr<Layer> root = Layer::Create(); root->SetBounds(size); client_.set_bounds(size); root_picture_layer_ = FakePictureLayer::Create(&client_); root_picture_layer_->SetBounds(size); root->AddChild(root_picture_layer_); layer_tree_host()->SetRootLayer(root); layer_tree_host()->SetVisualDeviceViewportIntersectionRect( gfx::Rect(device_bounds_)); }
16. id序列生成大法
#include "base/atomic_sequence_num.h" namespace cc { namespace { // Tracing ID sequence for use in CacheEntry. base::AtomicSequenceNumber g_next_tracing_id_; uint64_t tracing_id_= g_next_tracing_id_.GetNext();
17, 互相访问,互转 localFrame 换到 WebLocalFrameImpl 到 WebLocalFrameClient
-
LocalFrame 到 WebLocalFrameImpl
LocalFrame* frame
WebLocalFrameImpl* web_frame = WebLocalFrameImpl::FromFrame(frame);
web_frame->Client()->ScriptedPrint();
-
WebLocalFrameImpl利用client取到 RenderFrameImpl.
core中 LocalDOMWindow 这些html往外接口是 chromeClient,调研client的具体方法时走到实现类chromeClientImpl
-
webLocalFrame 到 localFrame
WebLocalFrameImpl* popup = FocusedWebLocalFrameInWidget();
LocalFrame* popup_frame= popup->GetFrame();
blink::WebLocalFrame* web_frame;
web_frame->GetDocument().GetFrame()
返回就是WebDocument,LocalFrame* XXX 返回是 WebLocalFrame
WebLocalFrame* WebDocument::GetFrame() const { return WebLocalFrameImpl::FromFrame(ConstUnwrap<Document>()->GetFrame()); }
Document* LocalFrame::GetDocument()通过lf可以返回Document*
这个强制将dom_window转成localDomWindow:
const LocalDOMWindow* LocalFrame::DomWindow() const {
return To<LocalDOMWindow>(dom_window_.Get());
}
WebDocument存放着Document: WebDocument(Document* elem)
获取缩放: WebLocalFrameImpl::FromFrame(local_frame_)
->LocalRootFrameWidget
()
->GetEmulatorScale();
LocalFrame 到 page 到 chromeClient
LocalFrame* frame = owner_element.GetDocument().GetFrame();
const Page* page = frame ? frame->GetPage() : nullptr;
dpr = page->GetChromeClient().GetScreenInfo(*frame).device_scale_factor;
LocalFrame* opener_frame = client->OwnerElement().GetDocument().GetFrame();
owner_document.GetStyleEngine().GetFontSelector() src\third_party\blink\renderer\core\css\style_engine.h
-
RenderFrameHost 访问 localframe
// Holder of Mojo connection with the LocalFrame in Blink.
mojo::AssociatedRemote<blink::mojom::LocalFrame> local_frame_;
-
RenderFrameImpl 和RenderFrameHostImpl 互调
RenderFrameImpl.GetFrameHost()获取、 RenderFrameHostImpl.GetMojomFrameInRenderer()
-
renderFrame 到 localFrame
blink::WebLocalFrame* myWebLocalFrame = render_frame()->GetWebFrame();
blink::WebLocalFrameImpl* frame_impl = DynamicTo<blink::WebLocalFrameImpl>(myWebLocalFrame);
blink::LayoutView* layout_view = frame_impl->GetFrame()->GetDocument()->GetLayoutView();
blink::LocalFrame* local_frame = frame_impl ->GetFrame();
localFrame获取远程的localFrameHost
local_frame->GetLocalFrameHostRemote()
RenderFrameHostImpl 获取 RenderWidgetHostImpl
RenderFrameHostImpl* host_;
RenderWidgetHostImpl* widget_host =
host_ ? host_->GetRenderWidgetHost() : nullptr;
从 RenderWidgetHostView 获取到 RenderWidgetHostImpl
RenderWidgetHostImpl* render_widget_host_impl = widget_host_view->host();
Frame to localFrame
auto* local_frame = DynamicTo(frame);
Frame* FocusController::FocusedOrMainFrame() const { if (LocalFrame* frame = FocusedFrame()) return frame; // TODO(dcheng, alexmos): https://crbug.com/820786: This is a temporary hack // to ensure that we return a LocalFrame, even when the mainFrame is remote. // FocusController needs to be refactored to deal with RemoteFrames // cross-process focus transfers. for (Frame* frame = &page_->MainFrame()->Tree().Top(); frame; frame = frame->Tree().TraverseNext()) { auto* local_frame = DynamicTo<LocalFrame>(frame); if (local_frame) return frame; } return page_->MainFrame(); }
17.1 从子renderWidget找到父
std::unique_ptr<RenderWidgetHostIterator>
RenderWidgetHostImpl::GetEmbeddedRenderWidgetHosts() {
// This iterates over all RenderWidgetHosts and returns those whose Views
// are children of this host's View.
auto hosts = std::make_unique<RenderWidgetHostIteratorImpl>();
auto* parent_view = static_cast<RenderWidgetHostViewBase*>(GetView());
for (auto& it : g_routing_id_widget_map.Get()) {
RenderWidgetHost* widget = it.second;auto* view = static_cast<RenderWidgetHostViewBase*>(widget->GetView());
if (view && view->IsRenderWidgetHostViewChildFrame() &&
static_cast<RenderWidgetHostViewChildFrame*>(view)->GetParentView() ==
parent_view) {
hosts->Add(widget);
}
}return std::move(hosts);
}
18. sharedbuffer 转成 string
scoped_refptr<SharedBuffer> data = SharedBuffer::Create(); popup_client_->WriteDocument(data.get()); const char* myhtml = data->Data(); size_t size = data->size(); LOG(ERROR)<<" ==== MYHTML SELECTION:"<< base::StringPiece(myhtml, size); // Create a StringPiece to view the UTF-8 encoded data without copying base::StringPiece stringPiece(bufferData, bufferSize); // Convert the StringPiece to a std::string by copying the data std::string utf8String(stringPiece.data(), stringPiece.size()); // Now utf8String contains the UTF-8 encoded data as a C++ string // Print out the UTF-8 string std::cout << "UTF-8 String: " << utf8String << std::endl;
19, css 序列化成字符串
ItemIterationContext context(*owner_element_->GetComputedStyle(), data.get()); context.SerializeBaseStyle();
参考
void InternalPopupMenu::Update(bool force_update) { scoped_refptr<SharedBuffer> data = SharedBuffer::Create(); PagePopupClient::AddString("window.updateData = {\n", data.get()); PagePopupClient::AddString("type: \"update\",\n", data.get()); ItemIterationContext context(*owner_element_->GetComputedStyle(), data.get()); context.SerializeBaseStyle(); PagePopupClient::AddString("children: [", data.get()); const HeapVector<Member<HTMLElement>>& items = owner_element_->GetListItems(); for (; context.list_index_ < items.size(); ++context.list_index_) { Element& child = *items[context.list_index_]; if (!IsA<HTMLOptGroupElement>(child.parentNode())) context.FinishGroupIfNecessary(); if (auto* option = DynamicTo<HTMLOptionElement>(child)) AddOption(context, *option); else if (auto* optgroup = DynamicTo<HTMLOptGroupElement>(child)) AddOptGroup(context, *optgroup); else if (auto* hr = DynamicTo<HTMLHRElement>(child)) AddSeparator(context, *hr); } context.FinishGroupIfNecessary(); PagePopupClient::AddString("],\n", data.get()); gfx::Rect anchor_rect_in_screen = chrome_client_->LocalRootToScreenDIPs( owner_element_->VisibleBoundsInLocalRoot(), OwnerElement().GetDocument().View()); AddProperty("anchorRectInScreen", anchor_rect_in_screen, data.get()); PagePopupClient::AddString("}\n", data.get()); #if 1//zhibin:select log html update LOG(INFO) << " SLECT UPDATE HTML: " << String::FromUTF8(data->Data(), data->size()); #endif popup_->PostMessageToPopup(String::FromUTF8(data->Data(), data->size())); }
- 字符串从base的wtf库到平台比如win字符 | 从文件后缀取到mime type
#include "net/base/platform_mime_util.h" #include <string> #include "base/strings/utf_string_conversions.h" #include "base/win/registry.h" #include <windows.h> namespace net { bool MimeUtil::GetMimeTypeFromFile(const base::FilePath& file_path, string* result) const { base::FilePath::StringType file_name_str = file_path.Extension(); if (file_name_str.empty()) return false; return GetMimeTypeFromExtension(file_name_str.substr(1), result); } bool PlatformMimeUtil::GetPlatformMimeTypeFromExtension( const base::FilePath::StringType& ext, std::string* result) const { // check windows registry for file extension's mime type (registry key // names are not case-sensitive). base::FilePath::StringType value, key = FILE_PATH_LITERAL(".") + ext; base::win::RegKey(HKEY_CLASSES_ROOT, key.c_str(), KEY_READ) .ReadValue(L"Content Type", &value); if (!value.empty()) { *result = base::WideToUTF8(value); return true; } return false; }
20. 内核访问content层
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
访问到glue层。再通过
21. 调用进入 WaitForLoad,即正常执行被终止,延迟2s后,重新返回入口函数。流程再来一遍,不满足条件时继续延迟。直到通过,继续执行。
WaitForLoad
void PrintRenderFrameHelper::RequestPrintPreview(PrintPreviewRequestType type, bool already_notified_frame) { 。。。。。。 is_scripted_preview_delayed_ = true; if (is_loading_) { // Wait for DidStopLoading, for two reasons: // * To give the document time to finish loading any pending resources /// that are desired for printing. // * Plugins may not know the correct|is_modifiable| value until they // are fully loaded, which occurs when DidStopLoading() is called. // Defer showing the preview until then. WaitForLoad(type); return;//这里正常执行被返回。后续由WaitForLoad里面的postTask任务继续接力。 } void PrintRenderFrameHelper::WaitForLoad(PrintPreviewRequestType type) { static constexpr base::TimeDelta kLoadEventTimeout = base::Seconds(2); on_stop_loading_closure_ = base::BindOnce(&PrintRenderFrameHelper::RequestPrintPreview, weak_ptr_factory_.GetWeakPtr(), type, true); base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( FROM_HERE, base::BindOnce(&PrintRenderFrameHelper::DidFinishLoadForPrinting, weak_ptr_factory_.GetWeakPtr()), kLoadEventTimeout);//接力棒是2秒后执行 DidFinishLoadForPrinting } void PrintRenderFrameHelper::DidFinishLoad/DidFinishLoadForPrinting() { is_loading_ = false; if (!on_stop_loading_closure_.is_null()) std::move(on_stop_loading_closure_).Run(); } 运行后Run :RequestPrintPreview 再次执行WaitForLoad流程。即不停循环,直到满足。
创建共享内存
// Initializes the shared memory structure. The salt should already be filled // in so that it can be written to the shared memory bool VisitedLinkWriter::CreateURLTable(int32_t num_entries) { base::MappedReadOnlyRegion table_memory; if (CreateApartURLTable(num_entries, salt_, &table_memory)) { mapped_table_memory_ = std::move(table_memory); hash_table_ = GetHashTableFromMapping(mapped_table_memory_.mapping); table_length_ = num_entries; used_items_ = 0; return true; } // static bool VisitedLinkWriter::CreateApartURLTable( int32_t num_entries, const uint8_t salt[LINK_SALT_LENGTH], base::MappedReadOnlyRegion* memory) { DCHECK(salt); DCHECK(memory); // The table is the size of the table followed by the entries. uint32_t alloc_size = num_entries * sizeof(Fingerprint) + sizeof(SharedHeader); // Create the shared memory object. *memory = base::ReadOnlySharedMemoryRegion::Create(alloc_size); if (!memory->IsValid()) return false; memset(memory->mapping.memory(), 0, alloc_size); // Save the header for other processes to read. SharedHeader* header = static_cast<SharedHeader*>(memory->mapping.memory()); header->length = num_entries; memcpy(header->salt, salt, LINK_SALT_LENGTH); return true; }
22, 异步调用时回调中对象可能被释放问题
1,利用 独立指针;另外由于 chromium的base::BindOnce里面的lamba只能是最简的,才能转成函数指针,不能带状态。可以利用bind参数,将状态传入。
base::RunLoo run_loop; //没用。只是说明按引用传递,在模板中。
std::unique_ptr<network::SimpleURLLoader> url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kRBELogUploaderTrafficAnnotation);url_loader->AttachStringForUpload(upload_data, MimeContentType());
auto* loader_ptr = url_loader.get();
loader_ptr->DownloadToString(url_load_factory.get(),
base::BindOnce(
[](base::RunLoop& run_loop, std::unique_ptr<network::SimpleURLLoader> loader, std::unique_ptr<std::string> response_body) {
LOG(ERROR) << "==== OnSimpleLoaderComplete";
//run_loop.Quit(); // 退出消息循环
loader.reset(); },std::ref(run_loop), std::move(url_loader) //前面的lamda绑定了两个参数. 第三个参数response_body,才是真正的DownloadToString第二个参数的原型:base::OnceCallback<void(std::unique_ptr<std::string> response_body)>
), //bindOnce结束,将一个三个参数的lamda,绑定了前两个参数。只剩一个参数。与DownloadToString要求的第二个参数一致。
10000);
23, 启动参数
"out\Default\chrome1.exe" --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --process-per-site --disable-dev-shm-usage --flag-switches-begin --enable-features=BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/3000/should_ignore_blocklists/true,BackForwardCacheNoTimeEviction,CacheControlNoStoreEnterBackForwardCache:level/store-and-evict,TimeoutTcpConnectAttempt:TimeoutTcpConnectAttemptMin/3s/TimeoutTcpConnectAttemptMax/5s/TimeoutTcpConnectAttemptRTTMultiplier/2,NetworkQualityEstimator --flag-switches-end --no-sandbox --log-net-log="C:\mylogs\netlog.json" --enable-logging --v=0 --vmodule=*surface_layer*=2,render_frame_host_impl=0 --flag-switches-begin --flag-switches-end http://192.168.11.1/
24 element bound 到 screen space 转换
void ChromeAutofillClient::ShowAutofillPopup(const autofill::AutofillClient::PopupOpenArgs& open_args,base::WeakPtr<AutofillPopupDelegate> delegate) {// Convert element_bounds to be in screen space.gfx::Rect client_area = web_contents()->GetContainerBounds();gfx::RectF element_bounds_in_screen_space =open_args.element_bounds + client_area.OffsetFromOrigin();
25,win上的实用,hook:src\base\win\win_util.cc
void __cdecl ForceCrashOnSigAbort(int) {
*((volatile int*)nullptr) = 0x1337;
}
26 实现第一个类的打印,obj.ToString 类似
利用json
#include "components/password_manager/core/browser/password_form.h" #include <algorithm> #include <ostream> #include <sstream> #include <string> #include "base/json/json_writer.h" #include "base/json/values_util.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" std::string ToString(PasswordForm::Scheme scheme) { switch (scheme) { case PasswordForm::Scheme::kHtml: return "HTML"; case PasswordForm::Scheme::kBasic: return "Basic"; case PasswordForm::Scheme::kDigest: return "Digest"; case PasswordForm::Scheme::kOther: return "Other"; case PasswordForm::Scheme::kUsernameOnly: return "UsernameOnly"; } NOTREACHED(); return std::string(); } // Utility function that creates a std::string from an object supporting the // ostream operator<<. template <typename T> std::string ToString(const T& obj) { std::ostringstream ostream; ostream << obj; return ostream.str(); } std::u16string ValueElementVectorToString( const ValueElementVector& value_element_pairs) { std::vector<std::u16string> pairs(value_element_pairs.size()); std::transform( value_element_pairs.begin(), value_element_pairs.end(), pairs.begin(), [](const ValueElementPair& p) { return p.first + u"+" + p.second; }); return base::JoinString(pairs, u", "); } // Serializes a PasswordForm to a JSON object. Used only for logging in tests. void PasswordFormToJSON(const PasswordForm& form, base::Value::Dict& target) { target.Set("primary_key", form.primary_key.has_value() ? base::NumberToString(form.primary_key.value().value()) : "PRIMARY KEY IS MISSING"); target.Set("scheme", ToString(form.scheme)); target.Set("signon_realm", form.signon_realm); target.Set("is_public_suffix_match", form.is_public_suffix_match); target.Set("is_affiliation_based_match", form.is_affiliation_based_match); target.Set("url", form.url.possibly_invalid_spec()); target.Set("action", form.action.possibly_invalid_spec()); target.Set("submit_element", form.submit_element); target.Set("username_element", form.username_element); target.Set("username_element_renderer_id", base::NumberToString(form.username_element_renderer_id.value())); target.Set("username_value", form.username_value); target.Set("password_element", form.password_element); target.Set("password_value", form.password_value); target.Set("password_element_renderer_id", base::NumberToString(form.password_element_renderer_id.value())); ............ target.Set("in_store", ToString(form.in_store)); std::vector<std::string> hashes; hashes.reserve(form.moving_blocked_for_list.size()); for (const auto& gaia_id_hash : form.moving_blocked_for_list) { hashes.push_back(gaia_id_hash.ToBase64()); } target.Set("moving_blocked_for_list", base::JoinString(hashes, ", ")); base::Value::List password_issues; password_issues.reserve(form.password_issues.size()); for (const auto& issue : form.password_issues) { base::Value::Dict issue_value; issue_value.Set("insecurity_type", ToString(issue.first)); issue_value.Set("create_time", base::TimeToValue(issue.second.create_time)); issue_value.Set("is_muted", static_cast<bool>(issue.second.is_muted)); password_issues.Append(std::move(issue_value)); } target.Set("password_issues ", std::move(password_issues)); base::Value::List password_notes; password_notes.reserve(form.notes.size()); for (const auto& note : form.notes) { base::Value::Dict note_dict; note_dict.Set("unique_display_name", note.unique_display_name); note_dict.Set("value", note.value); note_dict.Set("date_created", base::TimeToValue(note.date_created)); note_dict.Set("hide_by_default", note.hide_by_default); password_notes.Append(std::move(note_dict)); } target.Set("notes", std::move(password_notes)); target.Set("previously_associated_sync_account_email", form.previously_associated_sync_account_email); } absl::optional<std::u16string> PasswordForm::GetNoteWithEmptyUniqueDisplayName() const { const auto& note_itr = base::ranges::find_if( notes, &std::u16string::empty, &PasswordNote::unique_display_name); return note_itr != notes.end() ? absl::make_optional(note_itr->value) : absl::nullopt; } std::ostream& operator<<(std::ostream& os, PasswordForm::Scheme scheme) { return os << ToString(scheme);利用 ToString模板转换 } 中间实现方法 std::ostream& operator<<(std::ostream& os, const PasswordForm& form) { base::Value::Dict form_json; PasswordFormToJSON(form, form_json);生成一个具体的对象json。 // Serialize the default PasswordForm, and remove values from the result that // are equal to this to make the results more concise. base::Value::Dict default_form_json; PasswordFormToJSON(PasswordForm(), default_form_json);生成一个默认值的对象json。 for (auto it_default_key_values : default_form_json) { 比较具体对象值与默认值的差异,捡出具体对象的东西。为了观看显著的,不看默认值,没意义。 const base::Value* actual_value = form_json.Find(it_default_key_values.first); if (actual_value != nullptr && it_default_key_values.second == *actual_value) { form_json.Remove(it_default_key_values.first); } } std::string form_as_string; base::JSONWriter::WriteWithOptions( form_json, base::JSONWriter::OPTIONS_PRETTY_PRINT, &form_as_string); base::TrimWhitespaceASCII(form_as_string, base::TRIM_ALL, &form_as_string); return os << "PasswordForm(" << form_as_string << ")"; } 打印一个 PasswordForm 时调用 std::ostream& operator<<(std::ostream& os, PasswordForm* form) { return os << "&" << *form; } 头文件里面: // For testing. #if defined(UNIT_TEST) std::ostream& operator<<(std::ostream& os, PasswordForm::Scheme scheme); std::ostream& operator<<(std::ostream& os, const PasswordForm& form); std::ostream& operator<<(std::ostream& os, PasswordForm* form); #endif
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2020-07-23 chromium 处理 addEventListener 事件
2019-07-23 Elasticsearch SQL用法详解
2019-07-23 MySQL重要知识点
2018-07-23 如何刻录cd音乐