内容简介:仅仅一个自定义类还不够,
ContentProvider
是Android四大组件之一,它的主要作用是进程间共享数据。Android中的数据存储方式主要有以下几种:网络存储、文件存储( SharedPreferences
属于文件的一种)、数据库。大多数情况下这些数据存储操作都是在同一进程中进行,但如果要数据和文件在不同进程间共享就比较复杂,而 ContentProvider
正好擅长这个,所以在多进程之间共享数据的最好方式就是通过 ContentProvider
来实现。
1、ContentProvider的使用
ContentProvider
是个抽象类,需要一个自定义类来实现其中的抽象方法,如下:
public class MyContentProvider extends ContentProvider { private static final String TAG = "MyContentProvider"; //ContentProvider中的抽象方法,需要在子类实现 @Override public boolean onCreate() { return false; } //ContentProvider通过反射创建对象成功后第一个调用的方法 @Override public void attachInfo(Context context, ProviderInfo info) { //在父类中调用了onCreate方法 super.attachInfo(context, info); } //数据查询操作,如果ContentProvider在主进程中创建则该操作在主线程中执行,非线程安全 @Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { return null; } //返回当前 Url所代表数据的MIME类型 @Nullable @Override public String getType(@NonNull Uri uri) { return null; } //数据插入操作,如果ContentProvider在主进程中创建则该操作在主线程中执行,非线程安全 @Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { return null; } //数据删除操作,如果ContentProvider在主进程中创建则该操作在主线程中执行,非线程安全 @Override public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; } //数据更新操作,如果ContentProvider在主进程中创建则该操作在主线程中执行,非线程安全 @Override public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { return 0; } } 复制代码
仅仅一个自定义类还不够, ContentProvider
与 activity
、 service
一样,需要在AndroidManifest.xml文件中进行配置。
<provider android:name=".MyContentProvider" android:authorities="com.example.content.provider" android:multiprocess="false" android:process=":remote" android:exported="true"/> 复制代码
配置参数还是蛮多的,但是我们只需要关注 multiprocess
、 process
及 exported
这三个参数即可(其他参数可以参考ContentProvider简介这篇文章)。 exported
为true则表示允许其他应用访问应用中的 ContentProvider
(跨应用访问),默认为false。 process
表示 ContentProvider
所在的进程。 multiprocess
为true表示每个调用者进程都会创建一个 ContentProvider
实例,默认为false。当 multiprocess
、 process
这两个参数结合起来就有点意思,会产生以下几种情况。
-
android:process=":remote"、android:multiprocess="true"
,
ContentProvider
不会随应用的启动而加载,当调用ContentProvider
的时候才会加载,并且ContentProvider
是在调用者的进程中初始化。这时候可能定义ContentProvider
的remote
进程还没有启动。 - android:process=":remote"、android:multiprocess="false"(默认情况) ,ContentProvider
不会随应用的启动而加载,当调用到ContentProvider
的时候才会加载,并且ContentProvider
是在“remote”进程中初始化。 -
android:multiprocess="true"
,
ContentProvider
会随着应用的启动而加载,并且ContentProvider
是在应用进程的主线程中初始化的。当被调用时会在调用者进程中实例化一个ContentProvider
对象。 -
android:multiprocess="false"(默认情况)
,
ContentProvider
会随着应用的启动而加载,并且ContentProvider
是在应用主进程的主线程中初始化的。这种ContentProvider
只有一个实例,运行在自己App的进程中。所有调用者共享该ContentProvider
实例,调用者与ContentProvider
实例位于两个不同的进程。
ContentProvider
创建成功后,使用起来还是比较简单,首先获得一个 ContentResolver
对象,再对该对象的crud操作即可。
//拿到访问的uri Uri uri_user = Uri.parse("content://com.example.content.provider"); ContentResolver resolver = getContentResolver(); //通过URI来插入数据 resolver.insert(uri_user, ...); //通过URI来查询数据 resolver.query(uri_user,...) //通过URI来更新数据 resolver.update(uri_user,...) //通过URI来删除数据 resolver.delete(uri_user,...) 复制代码
总体上来说, ContentProvider
的使用还是蛮简单的,主要在AndroidManifest.xml中对 ContentProvider
进行参数配置时要注意一些。
2、ContentProvider的工作流程
前面说不设置 process
时, ContentProvider
则会随着应用的启动而加载、初始化,反之则会在调用时进行加载、初始化,先来看一下 ContentProvider
随着应用的启动而加载、初始化的流程。
2.1、ContentProvider随应用启动而初始化的工作流程
Android源码分析之Activity启动流程
这篇文章说了 Application
实例是在 ActivityThread
的 handleBindApplication
方法中创建。在讲解这个方法时疏漏了一点,那就是 ContentProvider
会在这个方法中创建。
private void handleBindApplication(AppBindData data) { ... try { //通过反射创建Application实例 Application app = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; if (!data.restrictedBackupMode) { //如果有ContentProvider,则创建 if (!ArrayUtils.isEmpty(data.providers)) { //创建ContentProvider实例 installContentProviders(app, data.providers); } } try { //调用Instrumentation的onCreate方法 mInstrumentation.onCreate(data.instrumentationArgs); } catch (Exception e) { ... } try { //调用Application的onCreate方法 mInstrumentation.callApplicationOnCreate(app); } catch (Exception e) { ... } } finally { ... } // 预加载字体资源 ... } 复制代码
上面简化了大量代码,但重要部分还在。可以看到 installContentProviders
在 Application
的 onCreate
之前调用,所以可以得出结论:
ContentProvider
的 onCreate
在 Application
的 onCreate
之前调用
。
下面来看 installContentProviders
方法的实现。
private void installContentProviders( Context context, List<ProviderInfo> providers) { final ArrayList<ContentProviderHolder> results = new ArrayList<>(); //遍历所有需要随应用启动的ContentProvider for (ProviderInfo cpi : providers) { ... //创建ContentProvider实例 ContentProviderHolder cph = installProvider(context, null, cpi, false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/); if (cph != null) { cph.noReleaseNeeded = true; results.add(cph); } } try { //发布 ActivityManager.getService().publishContentProviders( getApplicationThread(), results); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } 复制代码
installContentProviders
方法主要是创建 ContentProvider
实例并在AMS中发布。在调用 installProvider
方法时传入的holder为null,所以就会在 installProvider
中创建 ContentProvider
实例并加入HashMap中进行缓存。
//创建ContentProvider实例 private ContentProviderHolder installProvider(Context context, ContentProviderHolder holder, ProviderInfo info, boolean noisy, boolean noReleaseNeeded, boolean stable) { ContentProvider localProvider = null; IContentProvider provider; if (holder == null || holder.provider == null) { ... Context c = null; ApplicationInfo ai = info.applicationInfo; if (context.getPackageName().equals(ai.packageName)) { //在应用主进程中创建ContentProvider实例 c = context; } else if (mInitialApplication != null && mInitialApplication.getPackageName().equals(ai.packageName)) { //在单独进程中创建ContentProvider实例, c = mInitialApplication; } else { ... } ... try { //拿到类加载器 final java.lang.ClassLoader cl = c.getClassLoader(); //通过反射创建ContentProvider实例 localProvider = (ContentProvider)cl. loadClass(info.name).newInstance(); //拿到ContentProvider对应的IContentProvider接口 provider = localProvider.getIContentProvider(); //ContentProvider实例创建失败 if (provider == null) { ... return null; } // 调用ContentProvider的attachInfo方法,在该方法里会调用ContentProvider的onCreate方法 localProvider.attachInfo(c, info); } catch (java.lang.Exception e) { ... return null; } } else { provider = holder.provider; ... } ContentProviderHolder retHolder; synchronized (mProviderMap) { IBinder jBinder = provider.asBinder(); if (localProvider != null) { ComponentName cname = new ComponentName(info.packageName, info.name); ProviderClientRecord pr = mLocalProvidersByName.get(cname); if (pr != null) { provider = pr.mProvider; } else { //创建ContentProviderHolder实例 holder = new ContentProviderHolder(info); holder.provider = provider; holder.noReleaseNeeded = true; //添加ContentProvider信息到mProviderMap pr = installProviderAuthoritiesLocked(provider, localProvider, holder); mLocalProviders.put(jBinder, pr); mLocalProvidersByName.put(cname, pr); } retHolder = pr.mHolder; } else { ... } } return retHolder; } 复制代码
installProviderAuthoritiesLocked
方法主要是创建一个 ProviderClientRecord
对象来记录 ContentProvider
信息并存入mProviderMap这个HashMap中以备下次获取,在后面会提到mProviderMap。
关于 ContentProvider
随着应用的启动而加载、初始化的流程到这里就结束了。下面就来看使用 ContentProvider
的工作流程。
2.2、ContentProvider在使用时初始化的工作流程
前面讲过如何使用 ContentProvider
,所以这里以 insert
为例,来看 ContentResolver
的 insert
方法。
public final @Nullable Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values) { IContentProvider provider = acquireProvider(url); if (provider == null) { throw new IllegalArgumentException("Unknown URL " + url); } try { ... //进行数据插入操作 Uri createdRow = provider.insert(mPackageName, url, values); ... return createdRow; } catch (RemoteException e) { return null; } finally { //释放引用 releaseProvider(provider); } } 复制代码
首先调用 acquireProvider
方法获取一个 IContentProvider
对象引用,而该方法是一个抽象方法,需要在子类实现,经查询,发现它的实现是在 ApplicationContentResolver
类中,该类是 ContextImpl
的一个静态内部类,来看这个类的实现。
private static final class ApplicationContentResolver extends ContentResolver { ... @Override protected IContentProvider acquireProvider(Context context, String auth) { return mMainThread.acquireProvider(context, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), true); } ... } 复制代码
经查询发现 mMainThread
就是 ActivityThread
的实例,下面就来看 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; } ContentProviderHolder holder = null; try { //当缓存中没有ContentProvider示例时,需要通过AMS来创建一个ContentProvider示例 holder = ActivityManager.getService().getContentProvider( getApplicationThread(), auth, userId, stable); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } if (holder == null) { //通过AMS创建ContentProvider对象失败 return null; } //由于这里的holder不为null,所以在这里调用该方法主要是为了增加或减少计数引用 holder = installProvider(c, holder, holder.info, true /*noisy*/, holder.noReleaseNeeded, stable); return holder.provider; } 复制代码
首先会从 acquireExistingProvider
中去查找 ContentProvider
对象,如果不存在才会调用AMS来创建。
public final IContentProvider acquireExistingProvider( Context c, String auth, int userId, boolean stable) { synchronized (mProviderMap) { //ProviderKey的equals与hashCode方法被被重新实现 final ProviderKey key = new ProviderKey(auth, userId); //从mProviderMap中获取ContentProvider信息 final ProviderClientRecord pr = mProviderMap.get(key); if (pr == null) { return null; } IContentProvider provider = pr.mProvider; IBinder jBinder = provider.asBinder(); if (!jBinder.isBinderAlive()) { //ContentProvider所在进程被系统杀死 handleUnstableProviderDiedLocked(jBinder, true); return null; } ProviderRefCount prc = mProviderRefCountMap.get(jBinder); if (prc != null) { //当stable为true时则增加计数引用 incProviderRefLocked(prc, stable); } return provider; } } 复制代码
前面讲解 installProvider
时说过 ContenProvider
实例创建成功后会将 ProviderClientRecord
信息保存在mProviderMap这个HashMap中,而这里就是直接从mProviderMap中获取 ContenProvider
信息。
回到 acquireProvider
方法。如果从 acquireExistingProvider
中获取的对象为null,那么就得通过AMS中的 getContentProvider
方法来创建,来看一下该方法的实现。
public final ContentProviderHolder getContentProvider( IApplicationThread caller, String name, int userId, boolean stable) { ... return getContentProviderImpl(caller, name, null, stable, userId); } //具体创建ContentProvider实例的方法 private ContentProviderHolder getContentProviderImpl(IApplicationThread caller, String name, IBinder token, boolean stable, int userId) { ContentProviderRecord cpr; ContentProviderConnection conn = null; ProviderInfo cpi = null; //分段锁 synchronized(this) { ProcessRecord r = null; ... // 首先检查该ContentProviders是否已经发布 cpr = mProviderMap.getProviderByName(name, userId); ... //判断ContentProvider是否在运行 boolean providerRunning = cpr != null && cpr.proc != null && !cpr.proc.killed; //ContentProvider已经在运行 if (providerRunning) { cpi = cpr.info; String msg; //权限检查 if ((msg = checkContentProviderPermissionLocked(cpi, r, userId, checkCrossUser)) != null) { //没有权限则报错 throw new SecurityException(msg); } if (r != null && cpr.canRunHere(r)) { //此 ContentProvider已发布或正在发布...但它也允许在调用者的进程中运行,因此不要建立连接,只是让调用者实例化自己的实例。 //创建一个ContentProviderHolder对象 ContentProviderHolder holder = cpr.newHolder(null); //不给调用者提供者对象,它需要自己创建, holder.provider = null; return holder; } ... //获取ContentProviderConnection对象,它继承与Binder,主要作用是连接客户端与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) { updateLruProcessLocked(cpr.proc, false, null); } } final int verifiedAdj = cpr.proc.verifiedAdj; //更新进程的adj值,该值非常重要,值越大越容易被系统回收,系统进程的adj值基本上都小于0 boolean success = updateOomAdjLocked(cpr.proc, true); //检车adj值是否更新成功,可能存在更新失败的可能 if (success && verifiedAdj != cpr.proc.setAdj && !isProcessAliveLocked(cpr.proc)) { success = false; } ... if (!success) { //ContentProvider所在进程已被杀死,做一些清理数据的操作 appDiedLocked(cpr.proc); if (!lastRef) { // This wasn't the last ref our process had on // the provider... we have now been killed, bail. return null; } providerRunning = false; conn = null; } else { cpr.proc.verifiedAdj = cpr.proc.setAdj; } ... } //ContentProvider没有运行运行或者未创建 if (!providerRunning) { ... if ((msg = checkContentProviderPermissionLocked(cpi, r, userId, !singleton)) != null) { //未获取权限 throw new SecurityException(msg); } //如果ContentProvider未在系统进程中运行,并且系统尚未准备好运行其他进程,则快速失败而不是挂起。 if (!mProcessesReady && !cpi.processName.equals("system")) { throw new IllegalArgumentException( "Attempt to launch content provider before system ready"); } //确保开启ContentProvider的应用再运行,否则返回null if (!mUserController.isUserRunningLocked(userId, 0)) { return null; } ComponentName comp = new ComponentName(cpi.packageName, cpi.name); //检查该ContentProviders是否已经发布 cpr = mProviderMap.getProviderByClass(comp, userId); final boolean firstClass = cpr == null; if (firstClass) { ... try { ... ai = getAppInfoForUser(ai, userId); //创建ContentProviderRecord对象 cpr = new ContentProviderRecord(this, cpi, ai, comp, singleton); } catch (RemoteException ex) { // pm is in same process, this will never happen. } finally { Binder.restoreCallingIdentity(ident); } } if (r != null && cpr.canRunHere(r)) { //如果这是一个多进程ContentProvider,那么只需返回其信息并允许调用者实例化它。 只有在ContentProvider与调用者进程的用户相同时才执行此操作,或者可以以root身份运行(因此可以在任何进程中运行)。 //当android:multiprocess="true"时会走这里 return cpr.newHolder(null); } //从待启动的ContentProvider查找要启动的ContentProvider final int N = mLaunchingProviders.size(); int i; for (i = 0; i < N; i++) { if (mLaunchingProviders.get(i) == cpr) { break; } } //如果ContentProvider尚未启动,则启动它。 if (i >= N) { try { //如果ContentProvider所在进程已存在则直接启动 //获取进程信息 ProcessRecord proc = getProcessRecordLocked( cpi.processName, cpr.appInfo.uid, false); if (proc != null && proc.thread != null && !proc.killed) { if (!proc.pubProviders.containsKey(cpi.name)) { proc.pubProviders.put(cpi.name, cpr); try { //通过ActivityThread启动ContentProvider proc.thread.scheduleInstallProvider(cpi); } catch (RemoteException e) { } } } else { //如果ContentProvider所属进程不存在则开启新的进程 proc = startProcessLocked(cpi.processName, cpr.appInfo, false, 0, "content provider", new ComponentName(cpi.applicationInfo.packageName, cpi.name), false, false, false); //进程创建失败 if (proc == null) { return null; } } cpr.launchingApp = proc; //添加到正在启动的集合中 mLaunchingProviders.add(cpr); } finally { Binder.restoreCallingIdentity(origId); } } if (firstClass) { //如果是第一次的话则需要存储信息,根据ComponentName来保存信息 mProviderMap.putProviderByClass(comp, cpr); } //保存ContentProvider信息,根据名称保存 mProviderMap.putProviderByName(name, cpr); conn = incProviderCountLocked(r, cpr, token, stable); if (conn != null) { //需要等待 conn.waiting = true; } } ... } // 等待ContentProvider的发布,如果未发布成功则会一直在这里阻塞 ... return cpr != null ? cpr.newHolder(conn) : null; } 复制代码
上面关于AMS如何创建 ContentProviderHolder
做了详细的介绍,主要分为 ContentProvider
是否正在运行这两种情况,如果在运行就会提高 ContentProvider
所在进程的优先级并创建一个 ContentProviderConnection
对象。如果未运行则又分为 ContentProvider
所在进程是否存在的两种情况。如果 ContentProvider
进程已存在则调用 ActivityThread
的 scheduleInstallProvider
方法。
public void scheduleInstallProvider(ProviderInfo provider) { sendMessage(H.INSTALL_PROVIDER, provider); } //handler里会调用下面的方法 public void handleInstallProvider(ProviderInfo info) { final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); try { installContentProviders(mInitialApplication, Lists.newArrayList(info)); } finally { StrictMode.setThreadPolicy(oldPolicy); } } 复制代码
可以发现在 handleInstallProvider
里也调用了 installContentProviders
这个方法,该方法在前面就有讲解,这里就不在讲解了。如果 ContentProvider
进程不存在则创建一个新的进程。创建新进程的流程跟应用的启动流程一样,会创建 Application
对象,调用 installContentProviders
方法,具体流程在前面也讲解过,这里就不在过多叙述。
再次回到 ActivityThread
中 acquireProvider
方法,当通过AMS获得 ContentProviderHolder
对象后就会调用 installProvider
方法,关于该方法,前面讲了一些,这里就主要讲剩下的一些东西。
private ContentProviderHolder installProvider(Context context, ContentProviderHolder holder, ProviderInfo info, boolean noisy, boolean noReleaseNeeded, boolean stable) { ContentProvider localProvider = null; IContentProvider provider; //传入的holder及holder.provider不会为null if (holder == null || holder.provider == null) { ... } else { //拿到创建的ContentProvider对象 provider = holder.provider; } ContentProviderHolder retHolder; synchronized (mProviderMap) { IBinder jBinder = provider.asBinder(); if (localProvider != null) { ... } else { //主要是增加或减少引用计数, ProviderRefCount prc = mProviderRefCountMap.get(jBinder); if (prc != null) { if (!noReleaseNeeded) { incProviderRefLocked(prc, stable); try { ActivityManager.getService().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; } 复制代码
主要是做了一个引用计数操作,当stable和unstable引用计数都为0时则移除connection信息。
2.3、inset操作的实现
前面基本上就把 ContentResolver
中的 acquireProvider
讲解完毕,最后该方法返回了一个 IContentProvider
对象,它的实现是 ContentProvider
中的 Transport
类。
class Transport extends ContentProviderNative { ... @Override public Cursor query(String callingPkg, Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable ICancellationSignal cancellationSignal) { ... try { return ContentProvider.this.query( uri, projection, queryArgs, CancellationSignal.fromTransport(cancellationSignal)); } finally { setCallingPackage(original); } } @Override public String getType(Uri uri) { ... return ContentProvider.this.getType(uri); } @Override public Uri insert(String callingPkg, Uri uri, ContentValues initialValues) { ... try { return maybeAddUserId(ContentProvider.this.insert(uri, initialValues), userId); } finally { setCallingPackage(original); } } ... @Override public int delete(String callingPkg, Uri uri, String selection, String[] selectionArgs) { ... try { return ContentProvider.this.delete(uri, selection, selectionArgs); } finally { setCallingPackage(original); } } @Override public int update(String callingPkg, Uri uri, ContentValues values, String selection, String[] selectionArgs) { ... try { return ContentProvider.this.update(uri, values, selection, selectionArgs); } finally { setCallingPackage(original); } } ... } 复制代码
可以发现 Transport
中的crud操作就是直接对 ContentProvider
进行crud操作,而 Transport
又能够通过Binder进行进程间通信。
到此就把 ContentProvider
的工作流程梳理完毕了。
3、总结
前面两节主要讲解了 ContentProvider
的使用、 ContentProvider
的创建及示例 insert
方法的具体实现。下面就总结以下几点。
-
当不设置
android:process=":remote"
时,ContentProvider
会随着应用的启动而初始化,此时ContentProvider
的onCreate
方法会在Application
的onCreate
之前调用。当设置时,ContentProvider
会在第一次使用时初始化。 -
当设置
android:multiprocess="true"
时,会在每个调用者进程创建一个ContentProvide
实例。其设置的android:process=":remote"
属性也就无效了 -
如果
ContentProvider
在应用主进程创建则crud也在主线程中进程,因为并没有开启子线程,在ContentProvider
创建时。
【参考资料】《Android艺术探索》 [深入理解Android卷二 全文-第七章]深入理解ContentProvider 从源码角度看ContentProvider Android ContentProvider 多进程multiprocess 详解 ContentProvider简介 Android:关于ContentProvider的知识都在这里了! ContentProvider 引发闪退之谜
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java技术手册(第6版)
Benjamin J Evans、David Flanagan / 安道 / 人民邮电出版社 / 2015-12-1 / 79.00
《Java技术手册 第6版》为《Java 技术手册》的升级版,涵盖全新的Java 7 和Java 8。第1部分介绍Java 编程语言和Java 平台,主要内容有Java 环境、Java 基本句法、Java 面向对象编程、Java 类型系统、Java的面向对象设计、Java 实现内存管理和并发编程的方式。第2部分通过大量示例来阐述如何在Java 环境中完成实际的编程任务,主要内容有编程和文档约定,使......一起来看看 《Java技术手册(第6版)》 这本书的介绍吧!