Android 匿名內存深入分析

Android 匿名內存解析

有了binder機制為什么還需要匿名內存來實現IPC呢?我覺得很大的原因就是binder傳輸是有大小限制的,不說應用層的限制。在驅動中binder的傳輸大小被限制在了4M,分享一張圖片可能就超過了這個限制。匿名內存的主要解決思路就是通過binder傳輸文件描述符,使得兩個進程都能訪問同一個地址來實現共享。

MemoryFile使用

在平常開發中android提供了MemoryFile來實現匿名內存。看下最簡單的實現。

Service端

?
const val GET_ASH_MEMORY = 1000
class MyService : Service() {val ashData = "AshDemo".toByteArray()override fun onBind(intent: Intent): IBinder {return object : Binder() {override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {when(code){GET_ASH_MEMORY->{//收到客戶端請求的時候會煩val descriptor = createMemoryFile()reply?.writeParcelable(descriptor, 0)reply?.writeInt(ashData.size)return true}else->{return super.onTransact(code, data, reply, flags)}}}}}private fun createMemoryFile(): ParcelFileDescriptor? {val file = MemoryFile("AshFile", 1024)//創建MemoryFileval descriptorMethod = file.javaClass.getDeclaredMethod("getFileDescriptor")val fd=descriptorMethod.invoke(file)//反射拿到fdfile.writeBytes(ashData, 0, 0,ashData.size)//寫入字符串return ParcelFileDescriptor.dup(fd as FileDescriptor?)//返回一個封裝的fd}
}

?Server的功能很簡單收到GET_ASH_MEMORY請求的時候創建一個MemoryFile,往里寫入一個字符串的byte數組,然后將fd和字符長度寫入reply中返回給客戶端。

?

Client端

?
class MainActivity : AppCompatActivity() {val connect = object :ServiceConnection{override fun onServiceConnected(name: ComponentName?, service: IBinder?) {val reply = Parcel.obtain()val sendData = Parcel.obtain()service?.transact(GET_ASH_MEMORY, sendData, reply, 0)//傳輸信號GET_ASH_MEMORYval pfd = reply.readParcelable<ParcelFileDescriptor>(javaClass.classLoader)val descriptor = pfd?.fileDescriptor//拿到fdval size = reply.readInt()//拿到長度val input = FileInputStream(descriptor)val bytes = input.readBytes()val message = String(bytes, 0, size, Charsets.UTF_8)//生成stringToast.makeText(this@MainActivity,message,Toast.LENGTH_SHORT).show()}
?override fun onServiceDisconnected(name: ComponentName?) {}
?}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)findViewById<TextView>(R.id.intent).setOnClickListener {//啟動服務bindService(Intent(this,MyService::class.java),connect, Context.BIND_AUTO_CREATE)}}
}

客戶端也很簡單,啟動服務,發送一個獲取MemoryFile的請求,然后通過reply拿到fd和長度,用FileInputStream讀取fd中的內容,最后通過toast可以驗證這個message已經拿到了。

AshMemory 創建原理

    public MemoryFile(String name, int length) throws IOException {try {mSharedMemory = SharedMemory.create(name, length);mMapping = mSharedMemory.mapReadWrite();} catch (ErrnoException ex) {ex.rethrowAsIOException();}}

MemoryFile就是對SharedMemory的一層封裝,具體的工能都是SharedMemory實現的。看SharedMemory的實現。

    public static @NonNull SharedMemory create(@Nullable String name, int size)throws ErrnoException {if (size <= 0) {throw new IllegalArgumentException("Size must be greater than zero");}return new SharedMemory(nCreate(name, size));}private static native FileDescriptor nCreate(String name, int size) throws ErrnoException;

通過一個JNI獲得fd,從這里可以推斷出java層也只是一個封裝,拿到的已經是創建好的fd。

//frameworks/base/core/jni/android_os_SharedMemory.cpp
jobject SharedMemory_nCreate(JNIEnv* env, jobject, jstring jname, jint size) {const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;int fd = ashmem_create_region(name, size);//創建匿名內存塊int err = fd < 0 ? errno : 0;if (name) {env->ReleaseStringUTFChars(jname, name);}if (fd < 0) {jniThrowErrnoException(env, "SharedMemory_create", err);return nullptr;}jobject jifd = jniCreateFileDescriptor(env, fd);//創建java fd返回if (jifd == nullptr) {close(fd);}return jifd;
}

通過cutils中的ashmem_create_region函數實現的創建

//system/core/libcutils/ashmem-dev.cpp
int ashmem_create_region(const char *name, size_t size)
{int ret, save_errno;
?if (has_memfd_support()) {//老版本兼容用return memfd_create_region(name ? name : "none", size);}
?int fd = __ashmem_open();//打開Ashmem驅動if (fd < 0) {return fd;}if (name) {char buf[ASHMEM_NAME_LEN] = {0};strlcpy(buf, name, sizeof(buf));ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));//通過ioctl設置名字if (ret < 0) {goto error;}}ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));//通過ioctl設置大小if (ret < 0) {goto error;}return fd;
error:save_errno = errno;close(fd);errno = save_errno;return ret;
}
?

標準的驅動交互操作

1.open打開驅動

2.通過ioctl與驅動進行交互

下面看下open的流程

static int __ashmem_open()
{int fd;
?pthread_mutex_lock(&__ashmem_lock);fd = __ashmem_open_locked();pthread_mutex_unlock(&__ashmem_lock);
?return fd;
}
?
/* logistics of getting file descriptor for ashmem */
static int __ashmem_open_locked()
{static const std::string ashmem_device_path = get_ashmem_device_path();//拿到Ashmem驅動路徑if (ashmem_device_path.empty()) {return -1;}int fd = TEMP_FAILURE_RETRY(open(ashmem_device_path.c_str(), O_RDWR | O_CLOEXEC));return fd;
}

回到MemoryFile的構造函數中,拿到了驅動的fd之后調用了mapReadWrite

    public @NonNull ByteBuffer mapReadWrite() throws ErrnoException {return map(OsConstants.PROT_READ | OsConstants.PROT_WRITE, 0, mSize);}public @NonNull ByteBuffer map(int prot, int offset, int length) throws ErrnoException {checkOpen();validateProt(prot);if (offset < 0) {throw new IllegalArgumentException("Offset must be >= 0");}if (length <= 0) {throw new IllegalArgumentException("Length must be > 0");}if (offset + length > mSize) {throw new IllegalArgumentException("offset + length must not exceed getSize()");}long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED, mFileDescriptor, offset);//調用了系統的mmapboolean readOnly = (prot & OsConstants.PROT_WRITE) == 0;Runnable unmapper = new Unmapper(address, length, mMemoryRegistration.acquire());return new DirectByteBuffer(length, address, mFileDescriptor, unmapper, readOnly);}
?

到這里就有一個疑問,Linux就有共享內存,android為什么要自己搞一套,只能看下Ashmemory驅動的實現了。

驅動第一步看init和file_operations

static int __init ashmem_init(void)
{int ret = -ENOMEM;
?ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",sizeof(struct ashmem_area),0, 0, NULL);//創建if (!ashmem_area_cachep) {pr_err("failed to create slab cache\n");goto out;}
?ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",sizeof(struct ashmem_range),0, SLAB_RECLAIM_ACCOUNT, NULL);//創建if (!ashmem_range_cachep) {pr_err("failed to create slab cache\n");goto out_free1;}
?ret = misc_register(&ashmem_misc);//注冊為了一個misc設備........return ret;
}

創建了兩個內存分配器ashmem_area_cachep和ashmem_range_cachep用于分配ashmem_area和ashmem_range

//common/drivers/staging/android/ashmem.c
static const struct file_operations ashmem_fops = {.owner = THIS_MODULE,.open = ashmem_open,.release = ashmem_release,.read_iter = ashmem_read_iter,.llseek = ashmem_llseek,.mmap = ashmem_mmap,.unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT.compat_ioctl = compat_ashmem_ioctl,
#endif
#ifdef CONFIG_PROC_FS.show_fdinfo = ashmem_show_fdinfo,
#endif
};
?

open調用的就是ashmem_open

static int ashmem_open(struct inode *inode, struct file *file)
{struct ashmem_area *asma;int ret;
?ret = generic_file_open(inode, file);if (ret)return ret;
?asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);//分配一個ashmem_areaif (!asma)return -ENOMEM;
?INIT_LIST_HEAD(&asma->unpinned_list);//初始化unpinned_listmemcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);//初始化一個名字asma->prot_mask = PROT_MASK;file->private_data = asma;return 0;
}

ioctl設置名字和長度調用的就是ashmem_ioctl

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{struct ashmem_area *asma = file->private_data;long ret = -ENOTTY;
?switch (cmd) {case ASHMEM_SET_NAME:ret = set_name(asma, (void __user *)arg);break;case ASHMEM_SET_SIZE:ret = -EINVAL;mutex_lock(&ashmem_mutex);if (!asma->file) {ret = 0;asma->size = (size_t)arg;}mutex_unlock(&ashmem_mutex);break;}........}

實現也都很簡單就是改變了一下asma里的值。接下來就是重點mmap了,具體是怎么分配內存的。

?static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{static struct file_operations vmfile_fops;struct ashmem_area *asma = file->private_data;int ret = 0;
?mutex_lock(&ashmem_mutex);
?/* user needs to SET_SIZE before mapping */if (!asma->size) {//判斷設置了sizeret = -EINVAL;goto out;}
?/* requested mapping size larger than object size */if (vma->vm_end - vma->vm_start > PAGE_ALIGN(asma->size)) {//判斷大小是否超過了虛擬內存ret = -EINVAL;goto out;}
?/* requested protection bits must match our allowed protection mask */if ((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask, 0)) &calc_vm_prot_bits(PROT_MASK, 0)) {//權限判斷ret = -EPERM;goto out;}vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
?if (!asma->file) {//是否創建過臨時文件,沒創建過進入char *name = ASHMEM_NAME_DEF;struct file *vmfile;struct inode *inode;
?if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')name = asma->name;
?/* ... and allocate the backing shmem file */vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);//調用linux函數在tmpfs中創建臨時文件if (IS_ERR(vmfile)) {ret = PTR_ERR(vmfile);goto out;}vmfile->f_mode |= FMODE_LSEEK;inode = file_inode(vmfile);lockdep_set_class(&inode->i_rwsem, &backing_shmem_inode_class);asma->file = vmfile;/** override mmap operation of the vmfile so that it can't be* remapped which would lead to creation of a new vma with no* asma permission checks. Have to override get_unmapped_area* as well to prevent VM_BUG_ON check for f_ops modification.*/if (!vmfile_fops.mmap) {//設置了臨時文件的文件操作,防止有其他程序mmap這個臨時文件vmfile_fops = *vmfile->f_op;vmfile_fops.mmap = ashmem_vmfile_mmap;vmfile_fops.get_unmapped_area =ashmem_vmfile_get_unmapped_area;}vmfile->f_op = &vmfile_fops;}get_file(asma->file);
?/** XXX - Reworked to use shmem_zero_setup() instead of* shmem_set_file while we're in staging. -jstultz*/if (vma->vm_flags & VM_SHARED) {//這塊內存是不是需要跨進程ret = shmem_zero_setup(vma);//設置文件if (ret) {fput(asma->file);goto out;}} else {/**實現就是把vm_ops設置為NULLstatic inline void vma_set_anonymous(struct vm_area_struct *vma){vma->vm_ops = NULL;}*/vma_set_anonymous(vma);}
?vma_set_file(vma, asma->file);/* XXX: merge this with the get_file() above if possible */fput(asma->file);
?
out:mutex_unlock(&ashmem_mutex);return ret;
}

函數很長,但是思路還是很清晰的。創建臨時文件,設置文件操作。其中調用的都是linux的系統函數了,看真正設置的shmem_zero_setup函數

int shmem_zero_setup(struct vm_area_struct *vma)
{struct file *file;loff_t size = vma->vm_end - vma->vm_start;
?/** Cloning a new file under mmap_lock leads to a lock ordering conflict* between XFS directory reading and selinux: since this file is only* accessible to the user through its mapping, use S_PRIVATE flag to* bypass file security, in the same way as shmem_kernel_file_setup().*/file = shmem_kernel_file_setup("dev/zero", size, vma->vm_flags);if (IS_ERR(file))return PTR_ERR(file);
?if (vma->vm_file)fput(vma->vm_file);vma->vm_file = file;vma->vm_ops = &shmem_vm_ops;//很重要的操作將這塊虛擬內存的vm_ops設置為shmem_vm_ops
?if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) &&((vma->vm_start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK) <(vma->vm_end & HPAGE_PMD_MASK)) {khugepaged_enter(vma, vma->vm_flags);}
?return 0;
}
static const struct vm_operations_struct shmem_vm_ops = {.fault      = shmem_fault,//Linux的共享內存實現的基礎.map_pages  = filemap_map_pages,
#ifdef CONFIG_NUMA.set_policy     = shmem_set_policy,.get_policy     = shmem_get_policy,
#endif
};

到這里共享內存的初始化就結束了。

AshMemory 讀寫

?//frameworks/base/core/java/android/os/MemoryFile.java
public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)throws IOException {beginAccess();try {mMapping.position(destOffset);mMapping.put(buffer, srcOffset, count);} finally {endAccess();}}private void beginAccess() throws IOException {checkActive();if (mAllowPurging) {if (native_pin(mSharedMemory.getFileDescriptor(), true)) {throw new IOException("MemoryFile has been purged");}}}
?private void endAccess() throws IOException {if (mAllowPurging) {native_pin(mSharedMemory.getFileDescriptor(), false);}}

其中beginAccess和endAccess是對應的。調用的都是native_pin是一個native函數,一個參數是true一個是false。pin的作用就是鎖住這塊內存不被系統回收,當不使用的時候就解鎖。

static jboolean android_os_MemoryFile_pin(JNIEnv* env, jobject clazz, jobject fileDescriptor,jboolean pin) {int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);int result = (pin ? ashmem_pin_region(fd, 0, 0) : ashmem_unpin_region(fd, 0, 0));if (result < 0) {jniThrowException(env, "java/io/IOException", NULL);}return result == ASHMEM_WAS_PURGED;
}

調用的ashmem_pin_region和ashmem_unpin_region來實現解鎖和解鎖。實現還是在ashmem-dev.cpp

//system/core/libcutils/ashmem-dev.cpp
int ashmem_pin_region(int fd, size_t offset, size_t len)
{.......ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };return __ashmem_check_failure(fd, TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin)));
}

通過的也是ioclt通知的驅動。加鎖的細節就不展開了。具體的寫入就是利用linux的共享內存機制實現的共享。

Linux共享機制簡介

共享簡單的實現方式就是通過mmap同一個文件來實現。但是真實文件的讀寫速度實在是太慢了,所以利用tmpfs這個虛擬文件系統,創建了一個虛擬文件來讀寫。同時這塊虛擬內存在上面也寫到重寫了vm_ops。當有進程操作這個虛擬內存的時候會觸發缺頁錯誤,接著會去查找Page緩存,由于是第一次所以沒有緩存,讀取物理內存,同時加入Page緩存,當第二個進程進來的時也觸發缺頁錯誤時就能找到Page緩存了,那么他們操作的就是同一塊物理內存了。

總結

看完之后發現AshMemory是基于Linux的共享內存實現的。做了幾點改造

  • 首先把一整塊內存變成了一個個region,這樣在不用的時候可以解鎖來讓系統回收。
  • 將Linux共享內存的整數標記共享內存,而AshMemory是用的fd,讓它可以利用binder機制的fd傳輸。
  • 讀寫設置都做了加鎖的處理,減少了用戶使用的難度。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/164559.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/164559.shtml
英文地址,請注明出處:http://en.pswp.cn/news/164559.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

黑馬點評-10實現用戶點贊和點贊排行榜功能

用戶點贊功能 如果用戶只要點贊一次就對數據庫中blog表中的liked字段的值加1就會導致一個用戶無限點贊 PutMapping("/like/{id}") public Result likeBlog(PathVariable("id") Long id) {// 修改點贊數量,update tb_blog set liked liked 1 where id …

編譯器核心技術概覽

編譯技術是一門龐大的學科&#xff0c;我們無法對其做完善的講解。但不同用途的編譯器或編譯技術的難度可能相差很大&#xff0c;對知識的掌握要求也會相差很多。如果你要實現諸如 C、JavaScript 這類通用用途語言&#xff08;general purpose language&#xff09;&#xff0c…

buck降壓電路

一、Buck電路的拓撲結構 Buck是直流轉直流的降壓電路,下面是拓撲結構,作為硬件工程師,這個最好是能夠記下來,了然于胸。 為啥要記下來,自然是因為這個電路太基礎了,并且誰都會用到,更重要的一點,面試可能會考。。。 上圖是個異步buck,同步buck就是將里面的二極管換成M…

3D火山圖繪制教程

一邊學習&#xff0c;一邊總結&#xff0c;一邊分享&#xff01; 本期教程內容 **注&#xff1a;**本教程詳細內容 Volcano3D繪制3D火山圖 一、前言 火山圖是做差異分析中最常用到的圖形&#xff0c;在前面的推文中&#xff0c;我們也推出了好幾期火山圖的繪制教程&#xff0…

Android——資源IDnonFinalResIds和“Attribute value must be constant”錯誤

一、異常描述 通過資源ID引用資源提示錯誤 Attribute value must be constant 二、解決方案 在根目錄下的文件 gradle.properties 中添加如下配置&#xff0c;然后Sync Project android.nonFinalResIdsfalse 三、問題原因 android.nonFinalResIds 是Android開發中一個用于解…

此處不允許使用特性namespace

1.DOCTYPE 后面改成 mapper 2.PUBLIC一行中的Config改為Mapper 3.將下一行config變為小寫的mapper <?xml version"1.0" encoding"UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.or…

交叉編譯安裝時報錯 ./install.sh: 15: ./install.sh: Bad substitution

報錯信息截圖如下&#xff1a; 解決方法 vim install.sh #!/bin/sh -e 修改為 !/bin/bash -e重新執行 sudo ./install.sh 成功運行

【Java并發】聊聊線程池原理以及實際應用

線程其實對于操作系統來說是寶貴的資源&#xff0c;java層面的線程其實本質還是依賴于操作系統內核的線程進行處理任務&#xff0c;如果頻繁的創建、使用、銷毀線程&#xff0c;那么勢必會非常浪費資源以及性能不高&#xff0c;所以池化技術&#xff08;數據庫連接池、線程池&a…

暢談Linux在小型微型企業中的應用

在這篇文章里我們討論和暢談一下linux系統在小微型企業中的應用&#xff0c;為什么會寫這篇文章呢&#xff1f;因為在平時的工作中&#xff0c;認識的一些做小微型企業的朋友&#xff0c;他們經常找我咨詢或是去解決一些平時工作中的IT相關的問題&#xff0c;那么小微型企業中的…

相同結構體不同類型轉換

緣由&#xff1a; 最近開發上遇到一個問題&#xff0c;通過grpcgateway 處理后的int64&uint64類型數據均轉換成了字符串類型&#xff0c;本身服務于前端&#xff0c;沒有任何問題。但是 項目部署現場后&#xff0c;發現需要兩套環境&#xff0c;那么就出現一個問題&#x…

2022 年十大 JavaScript 框架

2022 年十大 Web 應用開發 JavaScript 框架。 React.js jQuery Express Angular Vue.js Angular.js Svelte Next.js Ember.js Meteor React.js React.js 于 2013 年由 Meta(Facebook 前身) 推出&#xff0c;是一款開源的、免費的 JavaScript 庫。React.js 被用于開…

C++中的map和set的使用

C中的map詳解 關聯式容器鍵值對樹形結構的關聯式容器set的使用1. set的模板參數列表2. set的構造3. set的迭代器4. set的容量5. set修改操作6. set的使用舉例 map1. map的簡介2. map的模板參數說明3. map的構造4. map的迭代器5. map的容量與元素訪問6. map的元素修改 multimap和…

Linux vim操作教程(vim 基操、vim替換和查找、 vim改變文本顏色、判斷和循環語句)

vim 基操 vim 是一個強大的文本編輯器,常用于在終端環境下編輯文件。下面是一些常用的 vim 操作: 打開文件:在終端中輸入 vim 文件名 來打開一個文件,如果文件不存在,則會創建一個新文件。 模式切換: 按下 i 進入插入模式,在該模式下可以輸入和編輯文本。按下 Esc 鍵返…

python單例模式

單例模式是一種創建型設計模式&#xff0c;它保證一個類僅有一個實例&#xff0c;并提供一個全局訪問點。 在 Python 中&#xff0c;可以使用以下幾種方式來創建單例模式&#xff1a; 使用 __new__ 方法 在 Python 中&#xff0c; __new__ 方法是一個類方法&#xff0c;它在…

msvcp120.dll丟失是什么意思,哪個修復方法最簡單

在計算機使用過程中&#xff0c;我們經常會遇到一些錯誤提示&#xff0c;其中之一就是“找不到msvcp120.dll”。這個錯誤通常發生在運行某些程序或游戲時&#xff0c;它會導致程序無法正常啟動或運行。那么&#xff0c;這個錯誤提示到底是什么意思呢&#xff1f;為了解決這個問…

深入了解Java8新特性-日期時間API_LocalDate類

閱讀建議 嗨&#xff0c;伙計&#xff01;刷到這篇文章咱們就是有緣人&#xff0c;在閱讀這篇文章前我有一些建議&#xff1a; 本篇文章大概12000多字&#xff0c;預計閱讀時間長需要10分鐘。本篇文章的實戰性、理論性較強&#xff0c;是一篇質量分數較高的技術干貨文章&…

【iOS】數據持久化(一)之Plist文件、Preference(NSUserDefaults類)

目錄 什么是Plist文件&#xff1f;plist可以存儲哪些數據類型plist文件數據的讀取與存儲 Perference&#xff08;NSUserDefaults&#xff09;使用方法registerDefaults: 方法的使用 什么是Plist文件&#xff1f; Plist文件&#xff08;屬性列表&#xff09;是將某些特定的類&a…

python運行hhblits二進制命令的包裝器類

hhblits 是 HMM-HMM&#xff08;Hidden Markov Model to Hidden Markov Model&#xff09;比對方法的一部分&#xff0c;也是 HMMER 軟件套件中的工具之一。與 hhsearch 類似&#xff0c;hhblits 也用于進行高效的蛋白質序列比對&#xff0c;特別擅長于檢測遠緣同源性。 hh-su…

筑牢思想防線——建行駐江門市分行紀檢組舉辦2023年清廉合規大講堂

為推動廉潔教育打通“最后一公里”&#xff0c;近日&#xff0c;建行駐江門市分行紀檢組舉辦江門市分行2023年清廉合規大講堂。 本次大講堂檢察官結合一線辦案經歷&#xff0c;從防范化解金融風險、預防金融從業人員犯罪等方面對全轄員工進行了深入淺出地的講解&#xff0c;引導…

代碼隨想錄算法訓練營第五十二天|1143.最長公共子序列 1035.不相交的線 53. 最大子序和

文檔講解&#xff1a;代碼隨想錄 視頻講解&#xff1a;代碼隨想錄B站賬號 狀態&#xff1a;看了視頻題解和文章解析后做出來了 1143.最長公共子序列 class Solution:def longestCommonSubsequence(self, text1: str, text2: str) -> int:dp [[0] * (len(text2) 1) for _ i…