一、自己的理解

对于content provide的启动我是这样认为的,要用ContentResolver去获得一个contentProvider,在这的获得的过程中,

1、如果本应用之前有contentProvider的引用,则直接返回。

2、如果没用,则向AMS(ActivityManagerService)去申请,然后AMS返回一个ContentProvideHolder对象,这时又分两种情况:

2.1如果此contentprovider需要在本应用进程中创建,则返回一个Holder对象,然后在本进程中创建一个contentprovider,并保存一系列引用。

2.2 如果此contentprovider在其他进程中,这时又要分情况:

2.2.1如果此contentprovider已经存在,并且可以在运行在本应用进程中,则直接返回;如果不可以,则需要设置一个ContentProviderConnection对象 conn,然后将contentProvider添加到conn中,即为contentProvider添加了一个异地引用。

2.2.2如果此contentProvider不存在,则需要去创建一个,如果这个contentProvider所在的进程已经被创建,则直接在此进程中加载,如果没有被创建,则先启动这个应用进程,然后加载。在这个加载过程中会有一个while循环,直到contentprovider被加载完毕。

最后,会返回一个带有contentProvider的Holder对象。

ps:其实返回的ContentProvider对象并不真正是一个ContentProvider对象,因为这是两个进程之间的通信,他们不能直接获得彼此进程中的一些引用,而客户进程得到的ContentProvider对象引用,本质上是一个Binder对象。因为Android中只有BInder才能在两个进程之间通信。在这里,客户端得到的其实是Transport对象,通过它来操作服务端的ContentProvider

二、源码浅析

在分析源码前,先说明几个变量:

1、AMS的mProviderMap,它保存了整个系统中所有的ContentProviderRecord对象,而这个记录里面包含了contentProvider,mProviderMap有两种获得ContentProviderRecord的方法,一是通过类名获得,另一个是通过contentProvider的授权URI获得。

2、ProcessRecord的pubProviders和conProviders,他们是被存放在每个应用各自的进程中,以键值对的方式记录着ContentProviderRecord,pubProviders保存着进程中所有创建的ContentProvider,conProviders保存着进程中所有使用的ContentProvider。

3、ContentProviderHolder中保存了ContentProvider的相关信息,它含有ProviderInfo(包含了contentProvider的授权URI,读写权限等信息),ContentProvider的引用,IBInder(这其实是一个服务端的ContentProviderConnection对象,其继承了BInder,用来作为客户端与服务端的链接)

下面我们来看一下源码中使怎么写的:

1、ContentResolver的query

在query()中,会首先获得contentProvider对象,然后用此对象去进行查询。要获得contentProvider对象会调用acquireUnstableProvider(uri)或acquireProvider(uri)。其实这两个方法的最终调用都是一样的,现在,我们选择acquireUnstableProvider(uri)来分析。

2、ContentResolver的acquireUnstableProvider()

要分析此方法,首先要知道resolver对象是如何获得的。resolver对象是通过context对象的getContentResolver()获得的。其实是ContextImpl实现了Context,然后由ContextWrapper对其进行了修饰,即有ContextImpl对象的一个引用——mBase,然后Service,Activity又继承了ContextWrapper ,由此就可以使用上下文的资源。在ContextImpl中,有一个ApplicationContentResolver类继承了ContentResolver,并在ContextImplement初始化时,对其进行实例化,保存为mContentResolver引用。在getContentResolver()中会使用mBase的getContentResolver(),得到的就是这个mContentResolver,然后会在它的acquireUnstableProvider()去调用mMainThread(ActivityThread)的acquireProvider().

3、ActivityThread的acquireProvider()方法

public final IContentProvider acquireProvider(Context c, String auth, int userId, boolean stable) {
 //查看是否有已经存在的ContentProvider,若存在,则将其返回final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);if (provider != null) {return provider;}// There is a possible race here.  Another thread may try to acquire// the same provider at the same time.  When this happens, we want to ensure// that the first one wins.// Note that we cannot hold the lock while acquiring and installing the// provider since it might take a long time to run and it could also potentially// be re-entrant in the case where the provider is in the same process.IActivityManager.ContentProviderHolder holder = null;try {
     //向AMS请求一个含有ContentProvider的Holderholder = ActivityManagerNative.getDefault().getContentProvider(getApplicationThread(), auth, userId, stable);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}if (holder == null) {Slog.e(TAG, "Failed to find provider info for " + auth);return null;}// Install provider will increment the reference count for us, and break// any ties in the race.
 //如注释所说,对ContentProvider添加引用holder = installProvider(c, holder, holder.info,true /*noisy*/, holder.noReleaseNeeded, stable);return holder.provider;}

关于此方法的作用,已在注释中写明,从这个方法中我们的值ContentProvider主要是从两个方面去获取的,一,从本地存在的COntentProvider集合中获取,即第4步;二,向AMS索取,即5,6,7步

4、ActivityThread的acquireExistingProvider()

public final IContentProvider acquireExistingProvider(Context c, String auth, int userId, boolean stable) {synchronized (mProviderMap) {final ProviderKey key = new ProviderKey(auth, userId);
     //查看本应用中是否存有要求的ContentProvider,没有则返回nullfinal ProviderClientRecord pr = mProviderMap.get(key);if (pr == null) {return null;}
        //查看provider是否可用,不可用返回nullIContentProvider provider = pr.mProvider;IBinder jBinder = provider.asBinder();if (!jBinder.isBinderAlive()) {// The hosting process of the provider has died; we can't// use this one.Log.i(TAG, "Acquiring provider " + auth + " for user " + userId+ ": existing object's process dead");handleUnstableProviderDiedLocked(jBinder, true);return null;}
        //至此,说明provider是可用的,然后对contentProvider的引用数量进行改变// Only increment the ref count if we have one.  If we don't then the// provider is not reference counted and never needs to be released.ProviderRefCount prc = mProviderRefCountMap.get(jBinder);if (prc != null) {incProviderRefLocked(prc, stable);}return provider;}}

关于incProviderRefLocked(),当请求一个新的ContentProvider时会调用此方法,它会将ContentProvider的引用计数加1,即将ContentProviderConnection对象进行更新,告诉它有某个应用需要使用新的ContentProvider,令其更新他的计数器。

5、AMS的getContentProvider()

调用此方法说明,在本应用没有已经存在的ContentProvider,需要向AMS申请一个。在此方法中会进行参数检查,最终去调用getContentProviderImpl()

5.1、AMS的getContentProviderImpl()

这个方法较长我们将它分为几个部分:

5.1.1若ContentProvider已经存在,则返回引用

5.1.2若其不存在,但是提供它的进程存在,则令此进程加载ContentProvider,然后返回新加载的对象

5.1.3若其不存,且提供它的进程也不存在,则开启此进程,并加载ContentProvider ,而后返回新加载的对象

好的下面来分析5.1.1:

先声明一些变量来保存数据ContentProviderRecord cpr;ContentProviderConnection conn = null;ProviderInfo cpi = null;

接下来是一个同步代码块,再此代码块中获得ContentProvider,并防止多个进程进行争抢,导致出错

            ProcessRecord r = null;if (caller != null) {r = getRecordForAppLocked(caller);if (r == null) {throw new SecurityException("Unable to find app for caller " + caller+ " (pid=" + Binder.getCallingPid()+ ") when getting content provider " + name);}}

获得客户端的进程,用来判断获得ContentProvider后,能否直接在客户端进程运行。

// First check if this content provider has been published...cpr = mProviderMap.getProviderByName(name, userId);// If that didn't work, check if it exists for user 0 and then// verify that it's a singleton provider before using it.if (cpr == null && userId != UserHandle.USER_SYSTEM) {cpr = mProviderMap.getProviderByName(name, UserHandle.USER_SYSTEM);if (cpr != null) {cpi = cpr.info;if (isSingleton(cpi.processName, cpi.applicationInfo,cpi.name, cpi.flags)&& isValidSingletonCall(r.uid, cpi.applicationInfo.uid)) {userId = UserHandle.USER_SYSTEM;checkCrossUser = false;} else {cpr = null;cpi = null;}}}

在此代码块中,首先查看,授权URI对应的进程中是否有ContentProvider,若没有,则去系统进程中查找,找到则证明此ContentProvider是单实例的,即系统中只有一个。

boolean providerRunning = cpr != null && cpr.proc != null && !cpr.proc.killed;

这一句是判断是否有存在且存活的ContentProvider,若有则为true,否则为false

 if (providerRunning) {cpi = cpr.info;......
        //查看是否可以在请求contentProvider的进程中运行,如果可以或者此contentProvider就是请求,提供的
                //则直接返回if (r != null && cpr.canRunHere(r)) {ContentProviderHolder holder = cpr.newHolder(null);holder.provider = null;return holder;}......// In this case the provider instance already exists, so we can// return it right away.
     //为contentProvider增加引用数conn = incProviderCountLocked(r, cpr, token, stable);if (conn != null && (conn.stableCount+conn.unstableCount) == 1) {if (cpr.proc != null && r.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ) {// 更新提供contentProvider进程的位置......updateLruProcessLocked(cpr.proc, false, null);}}
        //下面检查提供contentprovider的进程是否存活,若已死亡,等待新的进程启动//它是通过oom_adj的值来检查的final int verifiedAdj = cpr.proc.verifiedAdj;boolean success = updateOomAdjLocked(cpr.proc);if (success && verifiedAdj != cpr.proc.setAdj && !isProcessAliveLocked(cpr.proc)) {success = false;}......if (!success) {// 进程已为空,等待新进程的启动appDiedLocked(cpr.proc);......providerRunning = false;conn = null;} else {cpr.proc.verifiedAdj = cpr.proc.setAdj;}Binder.restoreCallingIdentity(origId);}

上面的代码是当有contentProvider时,该如何做。应在注释中写明

这两个方法的作用详见:

下面我们来分析contentProvider不存在的情况,这里我们把5.1.2和5.1.3合并到一起分析

          if (!providerRunning) {                        try{                                 //在此获得provider的信息            cpi = AppGlobals.getPackageManager().resolveContentProvider(name,STOCK_PM_FLAGS | PackageManager.GET_URI_PERMISSION_PATTERNS, userId);checkTime(startTime, "getContentProviderImpl: after resolveContentProvider");} catch (RemoteException ex) {}if (cpi == null) {return null;}// 参数检查          ......          ComponentName comp = new ComponentName(cpi.packageName, cpi.name);......          //以类名的来获得providercpr = mProviderMap.getProviderByClass(comp, userId);......final boolean firstClass = cpr == null;if (firstClass) {......try {checkTime(startTime, "getContentProviderImpl: before getApplicationInfo");ApplicationInfo ai =AppGlobals.getPackageManager().getApplicationInfo(cpi.applicationInfo.packageName,STOCK_PM_FLAGS, userId);......ai = getAppInfoForUser(ai, userId);              //因为我们需要provider,但是provider记录不存在,所以在此创建一个ContentProviderRecord来保存要获得providercpr = new ContentProviderRecord(this, cpi, ai, comp, singleton);} catch (RemoteException ex) {// pm is in same process, this will never happen.} finally {Binder.restoreCallingIdentity(ident);}}......          //到这里我们已经获得了contentProviderRecord对象cpr,不管它含不含有provider          if (r != null && cpr.canRunHere(r)) {            //条件成立,表示provider可以在请求者进程运行,或在请求者进程创建,则可以直接返回一个Holderreturn cpr.newHolder(null);}if (DEBUG_PROVIDER) Slog.w(TAG_PROVIDER, "LAUNCHING REMOTE PROVIDER (myuid "+ (r != null ? r.uid : null) + " pruid " + cpr.appInfo.uid + "): "+ cpr.info.name + " callers=" + Debug.getCallers(6));//查看是否有正在启动的provider,若有等待启动完成final int N = mLaunchingProviders.size();int i;for (i = 0; i < N; i++) {if (mLaunchingProviders.get(i) == cpr) {break;}}// 没有正在启动的provider,去启动它if (i >= N) {final long origId = Binder.clearCallingIdentity();try {// 加载provider所在的包try {......AppGlobals.getPackageManager().setPackageStoppedState(cpr.appInfo.packageName, false, userId);checkTime(startTime, "getContentProviderImpl: after set stopped state");} catch (RemoteException e) {} catch (IllegalArgumentException e) {Slog.w(TAG, "Failed trying to unstop package "+ cpr.appInfo.packageName + ": " + e);}// 获得provider所在的进程,因为我们要在此进程中启动providerProcessRecord proc = getProcessRecordLocked(cpi.processName, cpr.appInfo.uid, false);if (proc != null && proc.thread != null && !proc.killed) {                //条件成立,表示进程已经启动if (DEBUG_PROVIDER) Slog.d(TAG_PROVIDER,"Installing in existing process " + proc);if (!proc.pubProviders.containsKey(cpi.name)) {checkTime(startTime, "getContentProviderImpl: scheduling install");proc.pubProviders.put(cpi.name, cpr);try {                     //在此进程启动providerproc.thread.scheduleInstallProvider(cpi);} catch (RemoteException e) {}}} else {//条件不成立,则需要先启动一个进程,然后等待此进程加载providerproc = startProcessLocked(cpi.processName,cpr.appInfo, false, 0, "content provider",new ComponentName(cpi.applicationInfo.packageName,cpi.name), false, false, false);......}cpr.launchingApp = proc;mLaunchingProviders.add(cpr);} finally {Binder.restoreCallingIdentity(origId);}}//此处保存这个新provider的一些引用信息if (firstClass) {mProviderMap.putProviderByClass(comp, cpr);}mProviderMap.putProviderByName(name, cpr);conn = incProviderCountLocked(r, cpr, token, stable);if (conn != null) {conn.waiting = true;}}

完成上面的代码,表示provider已经存在,或正在启动,下面的代码用来检查

synchronized (cpr) {while (cpr.provider == null) {......try {......cpr.wait();} catch (InterruptedException ex) {} finally {if (conn != null) {conn.waiting = false;}}}}return cpr != null ? cpr.newHolder(conn) : null;

新的provider成功获得后,把它放在一个Holder中返回

下面我们来说一下,上面的两个进程中启动provider的方法startProcessLocked()和scheduleInstallProvider(),他们最终都会调用installProvider()。现在,我们以scheduleInstallProvider()来分析。首先,会调用ApplicationThread的scheduleInstallProvider(),而在这个方法中会给消息队列发送一个消息,然后,会转到ActivityThread中的Handler对象中去处理。继而调用handleInstallProvider().

ActivityThread的handleInstallProvider():

public void handleInstallProvider(ProviderInfo info) {final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();try {installContentProviders(mInitialApplication, Lists.newArrayList(info));} finally {StrictMode.setThreadPolicy(oldPolicy);}}

此方法会转而去调用installContentProviders()

ActivityThread的installContentProviders()

 private void installContentProviders(Context context, List<ProviderInfo> providers) {final ArrayList<IActivityManager.ContentProviderHolder> results =new ArrayList<IActivityManager.ContentProviderHolder>();for (ProviderInfo cpi : providers) {if (DEBUG_PROVIDER) {StringBuilder buf = new StringBuilder(128);buf.append("Pub ");buf.append(cpi.authority);buf.append(": ");buf.append(cpi.name);Log.i(TAG, buf.toString());}IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi,false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);if (cph != null) {cph.noReleaseNeeded = true;results.add(cph);}}try {ActivityManagerNative.getDefault().publishContentProviders(getApplicationThread(), results);} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}}

在这个方法中,首先会获得要加载的每一个provider的信息,因为我们这里只传入了一个,所以providers的长度为1.然后调用installProvider()去启动provider,启动完成之后会去告诉AMS,此provider已经启动完毕,让AMS去更新一些信息。installProvider()这个方法我们在前面请求者请求provider时见过,所以在这我们结合着两种不同的场景来分这方法的实现

ActivityThread的installProviders()

    private IActivityManager.ContentProviderHolder installProvider(Context context,                  I       ActivityManager.ContentProviderHolder holder, ProviderInfo inf         boolean noisy, boolean noReleaseNeeded, boolean stable) {               ContentProvider localProvider = null;               IContentProvider provider;     if (holder == null || holder.provider == null) {......
      //在此首先要获得与要加载provider相关的Context,因为provider是null,所以我们在后面要去实例化一个provider,这是当前情况下的处理做法
        Context c = null; ApplicationInfo ai = info.applicationInfo; if (context.getPackageName().equals(ai.packageName)) { c = context; } else if (mInitialApplication != null && mInitialApplication.getPackageName().equals(ai.packageName)) { c = mInitialApplication; } else { try { c = context.createPackageContext(ai.packageName, Context.CONTEXT_INCLUDE_CODE); } catch (PackageManager.NameNotFoundException e) { // Ignore  } } ......try { final java.lang.ClassLoader cl = c.getClassLoader(); localProvider = (ContentProvider)cl. loadClass(info.name).newInstance();                    //实例化一个provider,并获得可以在进程间通信的Transport对象 provider = localProvider.getIContentProvider(); ......        // 为新创建的provider配置一些信息,如读写权限之类的  localProvider.attachInfo(c, info); } catch (java.lang.Exception e) { ...... } } else {       //这是在请求者请求时的处理,传进来的是一个在其他进程已经启动好了的provider provider = holder.provider; ...... } IActivityManager.ContentProviderHolder retHolder; synchronized (mProviderMap) { ...... IBinder jBinder = provider.asBinder(); if (localProvider != null) {          //条件成立,表示此provider是新建的,需要保存一些引用 ComponentName cname = new ComponentName(info.packageName, info.name); ProviderClientRecord pr = mLocalProvidersByName.get(cname); if (pr != null) { //为null表示,在多个进程同时请求时,竞争失败,已经有其他进程先获得了provider,在此不需要在此保存 provider = pr.mProvider; } else {            //需要将provider保存在holder中 holder = new IActivityManager.ContentProviderHolder(info); holder.provider = provider; holder.noReleaseNeeded = true; pr = installProviderAuthoritiesLocked(provider, localProvider, holder); mLocalProviders.put(jBinder, pr); mLocalProvidersByName.put(cname, pr); } retHolder = pr.mHolder; } else {         //表示此provider是用其他进程传入的,要在此保存provider的引用数量,当Pro不为null时,是第一次传入,可以根据要求判断是否进行更新         //pro为null,则需要创建一个provider远程引用数,并进行保存 ProviderRefCount prc = mProviderRefCountMap.get(jBinder); if (prc != null) { ...... if (!noReleaseNeeded) { incProviderRefLocked(prc, stable); try { ActivityManagerNative.getDefault().removeContentProvider( holder.connection, stable); } catch (RemoteException e) { //do nothing content provider object is dead any way  } } } else { ProviderClientRecord client = installProviderAuthoritiesLocked( provider, localProvider, holder); if (noReleaseNeeded) { prc = new ProviderRefCount(holder, client, 1000, 1000); } else { prc = stable ? new ProviderRefCount(holder, client, 1, 0) : new ProviderRefCount(holder, client, 0, 1); } mProviderRefCountMap.put(jBinder, prc); } retHolder = prc.holder; } } return retHolder; }

然后,我们可以通知AMS去发布provider了

AMS的publishContentProviders()

public final void publishContentProviders(IApplicationThread caller,List<ContentProviderHolder> providers) {.....synchronized (this) {final ProcessRecord r = getRecordForAppLocked(caller);......final int N = providers.size();for (int i = 0; i < N; i++) {ContentProviderHolder src = providers.get(i);if (src == null || src.info == null || src.provider == null) {continue;}         //dst为之前在getContentProviderImpl中创建的provider记录ContentProviderRecord dst = r.pubProviders.get(src.info.name);if (dst != null) {ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name);mProviderMap.putProviderByClass(comp, dst);String names[] = dst.info.authority.split(";");for (int j = 0; j < names.length; j++) {mProviderMap.putProviderByName(names[j], dst);}int launchingCount = mLaunchingProviders.size();int j;boolean wasInLaunchingProviders = false;for (j = 0; j < launchingCount; j++) {if (mLaunchingProviders.get(j) == dst) {mLaunchingProviders.remove(j);wasInLaunchingProviders = true;j--;launchingCount--;}}//在此表示,请求的provider已经被启动,并向record中添加provider,然后打断前面的while循环。synchronized (dst) {dst.provider = src.provider;dst.proc = r;dst.notifyAll();}updateOomAdjLocked(r);maybeUpdateProviderUsageStatsLocked(r, src.info.packageName,src.info.authority);}}Binder.restoreCallingIdentity(origId);}}

传入的providers 是已经加载好的provider,会与加载的provider进行比较,若相同则说明已经启动,并将其从待启动队列中移除。并向provider记录中添加provider,这样前面的while循环就可以被打断,从而AMS就可以将provider返回,给请求者。

Ps:

关于Transport的几点说明:

1、Transport类型的对象是被用来代表自己所创建的ContentProvider的。它会将传递对provider所做的操作

2、它还是一个Binder对象,因此它就能实现进程间的通信,在上面的onTransact中,query()其实是Transport类型对象的,但是它会将这个查询转发给provider。

参考大神的博客:http://blog.csdn.net/luoshengyang/article/details/6963418,结合android5.0分析

至此contentProvider的启动就已经分析结束。
















												

Content Provider启动浅析相关推荐

  1. Android应用程序组件Content Provider的启动过程源代码分析(1)

             通过前面的学习,我们知道在Android系统中,Content Provider可以为不同的应用程序访问相同的数据提供统一的入口.Content Provider一般是运行在独立的进 ...

  2. 浅析调用android的content provider(一)

            在Android下,查询联系人.通话记录等,需要用到content provider.但是,调用content provider时,Android框架内部是如何做的呢?这一系列文章就是 ...

  3. Android应用程序组件Content Provider的启动过程源代码分析(6)

        Step 17. ActivityThread.installProvider         这个函数定义在frameworks/base/core/java/android/app/Act ...

  4. android 组件(activity,service,content provider,broadcast receiver,intent)详解

    Android应用程序由若干个不同类型的组件组合而成,每一个组件具有其特定的安全保护设计方式,它们的安全直接影响到应用程序的安全.Android应用程序组件的主要类型有:活动(Activity),服务 ...

  5. Android应用程序组件Content Provider的共享数据更新通知机制分析

    在Android系统中,应用程序组件Content Provider为不同的应用程序实现数据共享提供了基础设施,它主要通过Binder进程间通信机制和匿名共享内存机制来实现的.关于数据共享的另一个 话 ...

  6. Android应用程序组件Content Provider在应用程序之间共享数据的原理分析(1)

             在Android系统中,不同的应用程序是不能直接读写对方的数据文件的,如果它们想共享数据的话,只能通过Content Provider组件来实现.那么,Content Provide ...

  7. 在Content provider实现中使用SQLiteOpenHelper

    来自:http://www.apkbus.com/android-16353-1-1.html 在前面的编写最简单的Content Provider的示例是很粗糙的,目的是让读者尽快了解怎样编写和使用 ...

  8. 安卓基础巩固(二):四大组件:Activity、Service、Broadcast、Content Provider

    文章目录 Activity 生命周期 onCreate和onStart的区别 onPause和onStop的区别 生命周期的变化 Activity的启动 Intent Bundle Activity携 ...

  9. Content Provider (内容提供者)

    相关文章 Broadcast Receiver(广播接收者) 什么是Service(服务)? Android开发-Intent(意图) 为什么需要Content Provider(内容提供者)?简单来 ...

最新文章

  1. [AI开发]目标检测之素材标注
  2. php加载外部html,VUE页面加载外部HTML实例详解
  3. 微服务实践分享(9)文档中心
  4. 国内三款主流海淘产品APP竞品分析
  5. Python之路(第三十九篇)管道、进程间数据共享Manager
  6. c语言control表题目,CMFCControl 问题
  7. Linux中samba的权限详解,活用三种权限 理解Samba的权限控制
  8. 【待解答】文件目录可以利用foreach边遍历边删除操作,为什么?
  9. 商户分账交易汇总和商户交易汇总不一致
  10. 学习笔记:平衡树-splay
  11. Python之冒泡排序和选择排序的比较
  12. iOS-代码实现TableViewCell创建多个样式的Cell
  13. HDU 3683 模拟amp;搜索
  14. springboot+vue全栈开发_全栈的自我修养: 002使用@vue/cli进行vue环境搭建 (使用Vue,SpringBoot,Flask完成前后端分离)...
  15. json-smart 使用示例(推荐fastjson)
  16. Adminlte数据分页设置
  17. 《阿里铁军》的读后感范文3700字
  18. 基于 esp-idf 的 UART 应用例程解读
  19. 语音识别论文:Comparing the Benefit of Synthetic Training Data for Various Automatic Speech Recognition Arc
  20. 怎样选择ADC芯片?

热门文章

  1. Win10提示“无法创建新的分区也找不到现有的分区”
  2. OLE excel
  3. Java编程——杨辉三角(一)
  4. Git 如何生成SSH key
  5. 数据结构-栈(栈的C语言实现)
  6. 滤镜功能针的萌翻了!Snapchat为狗狗配戴眼镜
  7. pdf 电子签章 java_在pdf上加盖电子签章
  8. 如何在pdf中加入手写签名
  9. MySQL运动会管理系统_运动会管理系统(JAVA,JSP,SERVLET,SQLSERVER)
  10. Python 中的多进程(进程之间的通信)