ContentProvider简介

(一) 基础知识

Content Provider属于Android四大组件之一,相比较而言,它更侧重于共享数据。Android的数据存储方式有以下几种:Shared Preferences、网络存储、文件存储、数据库。但是一般情况下这些数据都是在单独一个应用中使用,数据和文件在不同应用程序之间的共享也比较复杂,而Content Provider则正好比较擅长这个,如果需要共享给其他应用,那么我们就可以通过Content Provider来实现了。

Content Provider和组件Activity、Service一样,需要在AndroidManifest.xml文件中配置之后才能使用。系统在安装应用程序的时候,会检查其声明的Content Provider,并且把这些Content Provider的描述信息保存起来,其中最重要的就是Content Provider的Authority信息。安装应用程序的时候,系统并不会把这些Content Provider加载到内存中来,而是采取懒加载的机制,等到第一次使用Content Provider的时候,才会把它加载到内存中来,这样以后再使用这个Content Provider的时候,就可以直接使用第一次使用创建的对象了。系统使用Content Provider进行共享数据的架构如下图所示:

Content Provider对外共享数据时,模仿数据库的操作,统一定义了数据的CRUD(增、查、改、删)操作,统一了数据的访问方式,使用上就方便多了。其具有如下特点:

  1. Content Provider为存储和获取数据提供了统一的接口,使用表的形式来组织数据。
  2. 不用关心数据存储的细节,并不要求访问的数据一定要是数据库,数据类型没有限制。
  3. Content Provider可以在不同的应用程序之间共享数据。
  4. 具有权限控制机制,可以保护数据访问的安全性,有很高安全性。
  5. Android为常见的一些数据提供了默认的Content Provider(音频、视频、图片和通讯录等),可以直接访问,很方便获取这些数据。
  6. 具有监听机制,当数据发生改变的时候可以将当前URI 数据发生改变的事件通知到监听器,实现实时的监听效果。

(二) 基本实现

1.自定义Content Provider

除了访问系统提供的Content Provider之外,在实际开发中,很多情况下是需要把数据共享给其他应用或者进程使用的,这时候就需要当前进程里自定义实现Content Provider来共享数据,自定义Content Provider的实现也比较简单,如下几步即可:

  1. 首先需要确定authority,标识当前自定义Content Provider,可以配置多个。以QQ音乐为例,假如实现一个ContentProvider,对应实现类TestContentProvider,那么authority可以是”com.tencent.qqmusic.xxx.TestContentProvider”。
  2. 定义CONTENT_URI,实现UriMatcher,确定下来当前自定义Content Provider需要处理的URI及映射关系。 对应TestContentProvider,可以设置”content://com.tencent.qqmusic.xxx.TestContentProvider/folder”对应歌单数据, “content://com.tencent.qqmusic.xxx.TestContentProvider/foldersong”对应歌单歌曲数据。
  3. 自定义一个Content Provider类,来实现抽象类ContentProvider。
  4. 实现Content Provider的方法(insert、query、update、delete、getType、onCreate等)。
  5. 通过UriMatcher确定要操作的URI数据类型,ContentUris可用于操作Uri路径后面的ID部分,使用Uri.getQueryParameter可获取URI中的参数。
  6. 实现getType方法,返回MIME类型。 如果操作的数据属于集合类型,那么MIME类型字符串应该以”vnd.android.cursor.dir/“开头; 如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以”vnd.android.cursor.item/“开头。
  7. 在AndroidMinifest.xml中进行声明此自定义Content Provider,设置访问权限等。
  8. 对于数据库操作,需要注意防止SQL 注入。推荐使用问号做替换符的方式,多个参数就用多个问号代替,按照顺序对应选择参数数组中的各个值。执行此操作时,用户输入直接受查询约束,而不解释为 SQL 语句的一部分。由于用户输入未作为 SQL 处理,因此无法注入恶意 SQL。

2.访问Content Provider

在访问的时候,需要使用到Content Resolver,每个content都有接口getContentResolver()可以获取Content Resolver,通过Content Resolver即可访问对应的Content Resolver。需要通过以下几步来实现访问Content Resolver:

  1. 为应用程序添加被访问Content Provider的访问权限。
  2. 通过getContentResolver()方法得到Content Resolver对象。
  3. 调用ContentResolver类的query()方法查询数据,该方法会返回一个Cursor对象。其他更新、插入等操作类似,此处按照查询举例。
  4. 对得到的Cursor对象进行分析,得到需要的数据。
  5. 查询结束,需要调用Cursor类的close()方法将Cursor对象关闭。
  6. 可以调用Content Resolver其他方法来完成对Content Provider的访问。

3.URI

为了便于管理和访问,每个Content Provider必须有唯一标示,用URI表示,这就类似广播里的Action。URI类似HTTP URL,构成如下:

content://authority/path

所有Content Provider的URI必须以content://开头,这是Android规定的;另外#匹配一个数字字符串,*匹配一个文本字符串。Uri 和 Uri.Builder 类包含根据字符串构建格式规范的 URI 对象的便利方法, ContentUris 包含一些对 ID 值操作的方法。

authority:符号名称(其权限),是个字符串,由开发者来定义,做为此Content Provider的唯一标识,系统会根据这个标识查找Content Provider。

path:路径,也是字符串,表示要操作的数据。可根据自己的实现逻辑来指定。 比如对于URI是”content://contacts/people”,则此URI的authority是” contacts “,对应手机联系人信息;对应的path为” people “,表示此URI数据对应于联系人信息里的people表。

(三) 声明Content Provider

每个应用自定义的所有Content Provider都必须在manifest文件中通过元素进行声明;否则系统就不知道它的存在,更不会运行它。只有是本应用实现的Content Provider才能在本应用声明,使用其他应用的Content Provider不应该在本应用定义。Android系统根据authority串储存一个到Content Provider的引用。声明Content Provider组件常见的参数如下所示:

<provider android:authorities="list"
android:enabled=["true" |"false"]
android:exported=["true" |"false"]
android:grantUriPermissions=["true" | "false"]
android:icon="drawable resource"
android:initOrder="integer"
android:label="string resource"
android:multiprocess=["true" | "false"]
android:name="string"
android:permission="string"
android:process="string"
android:readPermission="string"
android:syncable=["true" |"false"]
android:writePermission="string" >
    ...
</provider>

各个属性意义如下:

  1. android:authorities 数据URI的授权Authority列表,有多个Authority时,要用分号来分离每个Authority。为了避免冲突,Authority名应该使用Java样式的命名规则,最典型的做法就是用Content Provider子类名称来设定这个属性。这个属性没有默认值,必须至少指定一个。
  2. android:enabled 是否启用Content Provider。如果启用则为true,否则为false,默认为true。 元素有它自身的应用到所有组件的enabled属性,包括Content Provider。要让这个Content Provider启用,和的enabled属性都必须为true。如果有一个为false,这个provider就被disabled,那么它就不能实例化。
  3. android:exported 是否Content Provider对其它应用开放,是否让其他应用使用。 true表示这个provider对其它应用可用。任何应用都能使用这个provider指定的permission来访问它。 false表示这个provider对其它应用不可用。这样只有同一个用户ID(UID)的应用才能访问它,其他应用将不可访问。 对将android:minSdkVersion或android:targetSdkVersion设置为16或以下的应用这个默认值为true。对于将这些属性设置为17或更高的应用这个默认值为false。
  4. android:grantUriPermissions 原来没有权限访问Content Provider数据的组件,是否允许临时性地使用readPermission、writePermission和permission属性来忽略这个权限限制,并授权其使用这个provider。如果权限能授予则为true,否则为false。如果为true,那么权限能被授予任何Content Provider的数据。如果为false,如果有的话,那么权限只能被授予在 子元素中列出的数据子集。默认是false。 授权是指给应用组件一次访问由权限保护的数据的方式。例如,当电子邮件包含附件,即使这个视图没有查看所有Content Providers属性的一般权限,这个电子邮件应用也可以调用对应的视图打开它。在这种情况下,权限由启动组件的intent对象中的FLAG_GRANT_READ_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSION标记来授权。例如,电邮应用可以将FLAG_GRANT_READ_URI_PERMISSION放入intent对象中传递给Context.startActivity。在这个intent中这个对这个URI的权限就被指定。 如果你通过将这个属性设置为true或定义子元素开启了这个功能,那么当URI从provider删除时,你必须调用Context.revokeUriPermission()。
  5. android:icon 这个Content Provider呈现的图标。这个属性必须设置为包含图片的drawable资源的引用。如果没有设置,由application指定的图标就会代替它(参见 元素的icon属性)。
  6. android:initOrder 相对于同一个进程中的其它Content Provider来说,这个 Content Provider初始化的顺序。当在Content Providers之间有依赖关系时,为它们当中的每一个设置这个属性可以确保它们按照那些依赖需要的顺序被创建。这个值是整数,数字越大越优先。
  7. android:label 用户可读的Content Provider的标签。如果这个属性没有设置,那么这个application的标签就会代替它(参见 元素的label属性)。 这个标签应该设置为string资源的引用,以便它能像其它string资源一样在用户接口中本地化。然而,为了方便起见,当你开发应用时,它也能设置为raw串。
  8. android:multiprocess 多进程模式是否打开,是否这个Content Provider的实例能在每个客户端中被创建。如果这个实例能在多个进程中创建则为true,否则为false,默认为false。 一般来说,Content Provider只能在定义它的应用中初始化。然而,如果这个标记设为true,那么系统就能在每个客户端需要和它交互的进程中创建一个实例,这样可以省掉进程之间的通信开销。
  9. android:name 实现这个Content Provider类的名称,也就是Content Provider的子类。这应该是完整的类名。然而,作为一种简写的方式,如果这个名称的第一个字母是“.”,那么就会默认追加上manifest中定义的包名。 这个属性没有默认值,必须指定。
  10. android:permission 客户端读、写Content Provider数据所需的权限的名称。这个属性同时设置读权限和写权限的比较方便的方法。然而,readPermission和writePermission属性优先于这个属性。如果readPermission属性也设置,那么它就控制对Content Provider的查询。如果writePermission属性也设置,那么它就控制对Content Provider的修改。
  11. android:process 这个Content Provider运行的进程的名称。一般来说,所有组件都应当运行由它所在应用创建的默认进程中,和应用的包名一样。 元素的process属性可以对所有应用组件设置同一个默认值。然而每个组件自身可以通过process属性重写这个默认值,进而允许你的应用跨进程通信。 如果这个分配的名称以冒号(:)开始,那么当它需要运行时,会创建一个新的、私有的进程来运行这个Content Provider。如果这个进程名称以小写字母开始,那么这个Content Provider将运行在全局进程中,这就允许这个进程在不同的应用中共享,进而降低对资源的消耗。
  12. android:readPermission 客户端读取Content Provider数据所需的权限。
  13. android:syncable 是否Content Provider控制的属于要和服务数据进行同步,若同步则为true,否则为false。
  14. android:writePermission 客户端修改由Content Provider控制的数据时所需的权限。

(四) 初始化

Content Provider和应用程序组件Activity、Service一样,需要在AndroidManifest.xml文件中配置之后才能使用。系统在安装包含Content Provider的应用程序的时候,会把这些Content Provider的描述信息保存起来,其中最重要的就是Content Provider的Authority信息。注意,安装应用程序的时候,并不会把相应的Content Provider加载到内存中来,系统采取的是懒加载的机制,等到第一次要使用这个Content Provider的时候,系统才会把它加载到内存中来,下次再要使用这个Content Provider的时候,就可以直接返回了。

(一) Content Provider所在进程主动启动时候的加载过程

加载过程如上图所示,具体步骤如下:

  1. 应用启动,创建并启动Content Provider所在进程。
  2. 在Android系统中,每一个应用程序进程都加载了一个ActivityThread实例,进程启动时候会调用ActivityThread的main函数。
  3. ActivityThread实例通过ApplicationThread调用ActivityManagerService的attach,将当前进程和ActivityManagerService关联起来。在这个ActivityThread实例里面,有一个成员变量mAppThread,它是一个Binder对象,类型为ApplicationThread,实现了IApplicationThread接口,它是专门用来和ActivityManagerService服务进行通信的。
  4. ActivityManagerService的attachApplication方法会进行此进程的关联操作,并通过ApplicationThread将Provider信息给ActivityThread。
  5. ApplicationThread调用installProvider来在本地安装每一个Content Proivder的信息,并且为每一个Content Provider创建一个ContentProviderHolder对象来保存相关的信息。ContentProviderHolder对象是一个Binder对象,是用来把Content Provider的信息传递给ActivityManagerService服务的。
  6. 当这些Content Provider都处理好了以后,ApplicationThread还要调用ActivityManagerService服务的publishContentProviders函数来通知ActivityManagerService服务,这个进程中所要加载的Content Provider,都已经准备完毕了。

(二) Content Provider被其他进程使用时,被动启动

  1. 某个进程需要访问其他进程提供的Content Provider时,需要先通过Context获取Content Resolver,使用如下方法: context.getContentResolver(); 而应用程序上下文Context是由ContextImpl类来实现的,ContextImpl类的init函数是在应用程序启动的时候调用的,生成的Content Resolver是ApplicationContentResolver类型的对象。
  2. 拿到ContentResolver后,会调用到ContentResolver.acqireProvider来获取需要访问的Content Provider。
  3. ContentResolver验证参数URI的scheme是否正确,即是否是以content://开头,然后取出它的authority部分,最后获取Content Provider。
  4. 这里最终会调用ActivityThread.acquireProvider获取Content Provider。
  5. 此时Content Provider可能尚未加载,所以ActivityThread这里会有一个检查逻辑,在这里这个函数首先会通过getExistingProvider函数来检查本地是否已经存在这个要获取的Content Provider接口,如果存在,就直接返回了。本地已经存在的Context Provider接口保存在ActivityThread类的mProviderMap成员变量中,以Content Provider对应的URI的authority为键值保存。
  6. 如果ActivityThread本地未找到此Content Provider,就会调用ActivityManagerService服务的getContentProvider接口来获取一个ContentProviderHolder对象,这个对象就包含了我们所要获取的Provider接口。
  7. 在ActivityManagerService中,有两个成员变量是用来保存系统中的Content Provider信息的,一个是mProvidersByName,另外一个是mProvidersByClass,前者是以Content Provider的authoriry值为键值来保存的,后者是以Content Provider的类名为键值来保存的。一个Content Provider可以有多个authority,而只有一个类来和它对应,因此,这里要用两个Map来保存,这里为了方便根据不同条件来快速查找而设计的。
  8. 如果ActivityManagerService里没有此Content Provider缓存信息,这里会根据当前Content Provider是否是开启了多进程模式,如果是多进程模式,并且调用方UID和Content Provider声明的UID相同,则此处会创建一个ContentProviderHolder返回,通知调用方在本进程实例化一个。
  9. 一般情况下都不开多进程模式,所以本文的流程图是按照单进程模式来的,图中的第5步就是去启动Content Provider进程。所以ActivityManagerService通过AppGlobals.getPackageManager函数来获得PackageManagerService服务接口,然后分别通过它的resolveContentProvider和getApplicationInfo函数来分别获取Provider应用程序的相关信息,这些信息都是在安装应用程序的过程中保存下来的。调用startProcessLocked函数来启动Content Provider声明所在的进程来加载这个Content Provider对应的类,启动过程同前文介绍的主动启动APP过程。
  10. 因为我们需要获取的Content Provider是在新的进程中加载的,而ActivityManagerService的getContentProviderImpl这个函数是在系统进程中执行的,它必须要等到要获取的Content Provider是在新的进程中加载完成后才能返回,这样就涉及到进程同步的问题了。这里使用的同步方法是不断地去检查变量provider域是否被设置了。
  11. 当要获取的Content Provider在新的进程加载完成之后,它会通过Binder进程间通信机制调用到系统进程中,把这个provider域设置为已经加载好的Content Provider接口,这时候,函数getContentProviderImpl就可以返回了。
  12. 返回给调用者ActivityThread之后,ActivityThread还会调用installProvider函数来把这个接口保存在本地中,以便下次要使用这个Content Provider接口时,直接就可以通过getExistingProvider函数获取了。同样是执行installProvider函数,与APP主动启动时候加载Provider不同,这里传进来的参数provider是不为null的,因此,它不需要执行在本地加载Content Provider的工作,只需要把从ActivityMangerService中获得的Content Provider接口保存在成员变量mProviderMap中就可以了。

(五) 多进程模式

多进程模式,就是在不同的进程创建不同的实例;并且必须是同一个用户ID的情况下才允许创建调用方在调用方的进程再创建一个Content Provider实例,此后就不用跨进程访问了。开启多进程模式的方法就是在manifest文件声明的地方,设置” android:multiprocess “属性为true即可。

系统源代码如下:

1. if (r != null && cpr.canRunHere(r)) {
2.     // If this is a multiprocess provider, then just return its
3.     // info and allow the caller to instantiate it.  Only do
4.     // this if the provider is the same user as the caller's
5.     // process, or can run as root (so can be in any process).
6.     return cpr;
7. }

1. public boolean canRunHere(ProcessRecord app) {
2.     return (info.multiprocess || info.processName.equals(app.processName))
3.         && uid == app.info.uid;
4. }

比如对于QQ音乐上述例子,TestContentProvider是对歌单、歌曲数据进行共享的,一般都是关闭多进程模式的。如果要打开此模式,那么由于QQ音乐本身是有多个进程的,在每一个进程中访问TestContentProvider的时候都会在本进程实例化一个TestContentProvider,这样就要求TestContentProvider里的增、删、改、查等操作必须是要支持在各个进程中正常运行。

(六) 数据共享

Content Provider在进行数据传递时,包括跨进程通信时,使用了SQLiteCursor对象,即SQLite数据库游标对象,此对象包含了一个成员变量mWindow,它的类型为CursorWindow,这个成员变量是通过SQLiteCursor的setWindow成员函数来设置的。最重要的是CursorWindow对象内部包含一块匿名共享内存,它实际上存储了匿名共享内存文件描述符,占用很少内存空间;并且在跨进程通信过程中,Binder驱动程序能自动确保两个进程中的匿名共享内存文件描述符指向同一块匿名内存。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的,所以节省了跨进程传输大量数据的开销,也大幅提升了效率。

此外,SQLiteCursor采用懒加载模式加载数据,最初只是预编译一个查询计划,需要执行的时候才去真正执行。这个SQLiteCursor对象的内部还有一个SQLite数据库查询对象mQuery,类型为SQLiteQuery,它继承自SQLiteProgram类。SQLiteProgram类代表一个数据库存查询计划,它的成员变量mCompiledSql包含了一个已经编译好的SQL查询语句,SQLiteCursor对象就是利用这个编译好的SQL查询语句来获得数据的,但是它并不是马上就去获取数据的,而是采用懒加载策略,等到需要时才去获取。当真正执行的时候,例如调用了SQLiteCursor对象的getCount、moveToFirst等成员函数时,SQLiteCursor对象通过调用成员变量mQuery的fillWindow成员函数来把从SQLite数据库中查询得到的数据保存其父类匿名共享内存mWindow中去。这是一种数据懒加载机制,需要的时候才去加载,这样就提高了数据传输过程中的效率。

需要注意的是Content Provider的call函数,这个函数比较特别,使用Bundle进行数据传递。如前所述,一般Content Provider交互都是通过Cursor传递数据的,比如使用query函数从Content Provider中获得数据,会将数据通过匿名共享内存来返回给调用者。当要传输的数据量比较大的时候,使用匿名共享内存来传输数据是比较好的,这样可以减少数据的拷贝,提高传输效率,节省内存占用。但是,当要传输的数据量小时,使用匿名共享内存来作为媒介就有点杀鸡用牛刀的味道,因为匿名共享内存并不是免费的午餐,系统创建和匿名共享内存也是有开销的。因此,Content Provider提供了call函数来让第三方应用程序来获取一些自定义数据,这些数据一般都比较小,例如,只是传输一个整数,这样就可以用较小的代价来达到相同的数据传输的目的。

(七) 数据监控

Content Provider中的数据更新通知机制和Android系统中的广播(Broadcast)通知机制有点相似。不过他们也有下面几个不同点:

  1. Content Provider是通过URI来把通知的发送者和接收者关联在一起的,而Broadcast是通过Intent来关联的。
  2. Content Provider的通知注册中心是由ContentService服务来扮演的,而Broadcast的是由ActivityManagerService服务来扮演的。
  3. Content Provider的监听器必须要继承ContentObserver类,而Broadcast的接收器要继承BroadcastReceiver类。

ContentService是在系统启动的时候就启动起来,以便后面启动起来的应用程序可以使用它。Android系统进程Zygote在启动的时候,在启动一个System进程来加载系统的一些关键服务,其中就包括ContentService服务。ContentService实例会被添加到ServiceManager中去,这样其它地方可以通过ServiceManager来访问此服务。如下所示:

系统进程Zygote启动。

  1. 启动SystemServer服务。
  2. SystemServer服务会启动ServerThread 线程。
  3. ServerThread线程里会调用ContentService的main方法启动ContentService服务。
  4. ContentService的main方法里会创建一个ContentService服务,并且通过ServiceManager.addService将此服务添加到ServiceManager中,这样就可以通过ServiceManager来访问ContentService了。

如果想要监听ContentProvider中的数据变化,可以使用ContentResolver的registerContentObserver注册一个监听器ContentObserver,其原理是在ContentResolver里先获取ContentService,然后调用ContentService的注册方法将此监听器ContentObserver注册上。

ContentObserver需要实现onChange方法来处理数据变化事件。这个监听器可以设置一Handler,如果设置了Handler,那么数据变化的时候,会在此Handler的线程中回调onChange。

对于Content Provider实现方,当数据变化的时候,想通知使用方的话,就需要调用调用getContentResolver().notifyChange来通知注册在此URI上的监听器,告诉监听器当前URI的数据发生了变化。其原理也是在ContentResolver里先获取ContentService,然后调用ContentService收集当前URI变化需要通知的ContentResolver,逐一调用它们的onChange方法来通知数据变化了。

(八) 数据超大问题

对于跨进程传输,安卓系统都有一个数据包的最大1M限制,而且是各个跨进程通信共同复用的,所以当单个跨进程数据传输过大的时候,很容易出现这个异常。

前面已经介绍过了Content Provider传递查询数据是通过虚拟共享内存的,这样可以极大减小发生跨进程数据超大异常的概率。但是并不是一定不会出现,因为这里仅仅是对返回数据使用了虚拟共享内存,但是接口调用参数依然是需要跨进程传输的,比如要批量插入很多数据,那么就会出现一个插入数据的数组,如果这个太大了,那么这个操作一定会出现数据超大异常

参数数据太大,超过了跨进程阈值1M的限制,会导致Content Provider所在进程根本收不到接口调用请求。解决办法还是不要使得接口参数太大,特别是批量操作接口,对应的参数数据不能太大,可以将批量操作拆成多个小的批量操作。

另外需要注意的call接口在跨进程中并没有使用虚拟共享内存,而是和普通AIDL一样使用了Binder框架,所以这个接口的使用一样存在普通AIDL的数据超大问题。

(九) 参考文章

  1. https://developer.android.com/reference/android/content/ContentProvider.html
  2. http://blog.csdn.net/luoshengyang/article/details/6946067
  3. http://www.tutorialspoint.com/android/android_content_providers.htm
  4. http://blog.csdn.net/winson_jason/article/details/8115846
  5. http://blog.csdn.net/think_soft/article/details/7582340
  6. http://blog.sina.com.cn/s/blog_49f62c350101hhhl.html
  7. http://www.2cto.com/kf/201404/296974.html

原文发布于微信公众号 - QQ音乐技术团队(gh_287053a877e6)

原文发表时间:2016-08-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏顶级程序员

Java Web前端到后台常用框架介绍

来源: 小宝鸽 - CSDN博客 链接: http://blog.csdn.net/u013142781/article/details/50922010 一...

5837
来自专栏闵开慧

tomcat6.0下找不到jasper-runtime.jar

今天有点需求,需要用jasper-runtime.jar包。但是我在我的\apache-tomcat-6.0.16\lib目录下,怎么也找不到这个jar包。结果...

3375
来自专栏北京马哥教育

ls 命令还能这么玩?看一下这 20 个实用范例

2034
来自专栏漏斗社区

工具| 手把手教你制作信息收集器之端口扫描

本期任务:使用python脚本实现端口扫描。 准备工具:选项分析器:optparse;网络库:socket 问题引入 1. 端口扫描器扫描效果如何? ...

3296
来自专栏北京马哥教育

29 条运维工程师必会实用 Linux 命令

虽然Linux发行版支持各种各样的饿GUI(graphical user interfaces),但在某些情况下,Linux的命令行接口(bash)仍然是简单...

3179
来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第三十一天 WebService学习【悟空教程】

简单的网络应用使用单一语言写成,它的唯一外部程序就是它所依赖的数据库。大家想想是不是这样呢?

2224
来自专栏Kevin-ZhangCG

[ Java面试题 ]JavaWeb篇

3638
来自专栏架构之路

socket在windows和Linux下的区别

1)头文件  windows下winsock.h/winsock2.h  linux下sys/socket.h    错误处理:errno.h  2)初始化 ...

3414
来自专栏aoho求索

Spring Cloud Bus中的事件的订阅与发布(二)

在之前的文章Spring Cloud Bus中的事件的订阅与发布(一)介绍了消息总线的相关事件。本文主要介绍消息总线的事件监听器以及消息的订阅与发布。 事件监听...

4617
来自专栏求索之路

Android数据层架构的实现 上篇

最近我们app的服务器吃不消了,所以我在为服务器增加缓存层之后,又想到在app端进行二级缓存以减少app对服务器的访问。我想很多app应该在项目的初期架构的时...

3198

扫码关注云+社区

领取腾讯云代金券