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.mzph.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…