博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android之ContentProvider源码解析
阅读量:7049 次
发布时间:2019-06-28

本文共 25305 字,大约阅读时间需要 84 分钟。

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;    }}复制代码

 仅仅一个自定义类还不够,ContentProvideractivityservice一样,需要在AndroidManifest.xml文件中进行配置。

复制代码

 配置参数还是蛮多的,但是我们只需要关注multiprocessprocessexported这三个参数即可(其他参数可以参考这篇文章)。exported为true则表示允许其他应用访问应用中的ContentProvider(跨应用访问),默认为false。process表示ContentProvider所在的进程。multiprocess为true表示每个调用者进程都会创建一个ContentProvider实例,默认为false。当multiprocessprocess这两个参数结合起来就有点意思,会产生以下几种情况。

  • android:process=":remote"、android:multiprocess="true"ContentProvider不会随应用的启动而加载,当调用ContentProvider的时候才会加载,并且ContentProvider是在调用者的进程中初始化。这时候可能定义ContentProviderremote进程还没有启动。 -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随应用启动而初始化的工作流程

 这篇文章说了Application实例是在ActivityThreadhandleBindApplication方法中创建。在讲解这个方法时疏漏了一点,那就是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 {            ...        }        // 预加载字体资源        ...    }复制代码

 上面简化了大量代码,但重要部分还在。可以看到installContentProvidersApplicationonCreate之前调用,所以可以得出结论:ContentProvideronCreateApplicationonCreate之前调用。  下面来看installContentProviders方法的实现。

private void installContentProviders(            Context context, List
providers) { final ArrayList
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为例,来看ContentResolverinsert方法。  

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的实例,下面就来看ActivityThreadacquireProvider方法的实现。

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进程已存在则调用ActivityThreadscheduleInstallProvider方法。

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方法,具体流程在前面也讲解过,这里就不在过多叙述。  再次回到ActivityThreadacquireProvider方法,当通过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会随着应用的启动而初始化,此时ContentProvideronCreate方法会在ApplicationonCreate之前调用。当设置时,ContentProvider会在第一次使用时初始化。
  • 当设置android:multiprocess="true"时,会在每个调用者进程创建一个ContentProvide实例。其设置的android:process=":remote"属性也就无效了
  • 如果ContentProvider在应用主进程创建则crud也在主线程中进程,因为并没有开启子线程,在ContentProvider创建时。

【参考资料】 《Android艺术探索》

转载地址:http://srpol.baihongyu.com/

你可能感兴趣的文章
IT行业¬——Linux
查看>>
linkerd ab部署测试
查看>>
#日常杂记#Informatica 910 常见问题及可能的解决方法
查看>>
Spring Cloud Gateway 之 Only one connection receive subscriber allowed
查看>>
VoltDB 简介
查看>>
编译日志
查看>>
FieldType in Lucene
查看>>
为面试准备的知识点
查看>>
使用 CXF 做 webservice 简单例子
查看>>
Spring MVC之@RequestMapping 详解
查看>>
IE9开始支持SVG格式(VML终结)
查看>>
【转】PHP中的Hash算法
查看>>
SqlLite的工具类SQLiteOpenHelper
查看>>
chgrp chown chmod
查看>>
nodejs中安装express
查看>>
2014软件表
查看>>
Struts2教程3:struts.xml常用配置解析
查看>>
ruby的并发和并行
查看>>
SSIndicatorLabel
查看>>
ASFBPostController
查看>>