了解ActivityThread
2015-11-15 12:45
603 查看
Activity里面有一个成员变量ActivityThread,也就是Activity的UI主线程。
这样ActivityThread作用大概就是用于控制与管理一个应用进程的主线程的操作,包括管理与处理activity manager发送过来的关于activities、广播以及其他的操作请求。
activityThread的主要成员变量如下:
从上往下慢慢看一些重要的成员变量,通过他们的作用来看ActivityThread的作用。
Context 是一个抽象类,activity也最终继承了这个类,这样子就可以提供了一个全局的环境,这也就是成为上下文的意思。
![](http://img.blog.csdn.net/20151115130341122?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
ContextImpl的作用主要有:
1.初始化时注册了一系列的服务:
其实ContextImp含有一个map来存储这些注册了的服务,只要通过service名字就可以拿到ServiceFetcher :
当想要在上下文调用 getSystemService查找获取系统服务的时候,就会在这个map里面查找:
ServiceFetcher 是用于修改与查找sercive记录的类,其实最终存储service是一个 final ArrayList<Object> mServiceCache = new ArrayList<Object>();的成员变量,就是下面的红色部分。可以知道,通过键值对的形式就可以根据service的名字获得到对应的ServiceFetcher ,这个ServiceFetcher 会在mServiceCache 中查找并返回相应的service。
2.剩下的基本就是Context类的实现,也就是说在activity中Context的方法的实现是在ContextImp里面的,这也体现了面向接口编程的思想。对应有什么方法以及作用可以参考Context类。
![](http://img.blog.csdn.net/20151115154544717?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
ApplicationThreadProxy是IApplicationThread的代理,也就是说ApplicationThreadNative实现了IApplicationThread,最终方法实现是在ApplicationThreadProxy里面,ApplicationThreadNative继承了Binder,复写了onTransact,根据相应的code调用IApplicationThread的方法,最终调用的是ApplicationThreadProxy的实现。
以以下这个为例子:
最终调用的是ApplicationThreadProxy的schedulePauseActivity:
![](http://img.blog.csdn.net/20151115170943412?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
我们以schedulePauseActivity方法为例子看:
sendMessage方法如下:
看到了,最终调用到mH,mH是一个hander对象:
最终调用到:
可以看到整个调用流程后大概就可以知道了,通过调用ApplicationThread的方法可以控制activity的生命周期。
那么ApplicationThread到底有谁调用,就是说有谁去控制activity的生命周期。
在activityThread里面找到了attach的方法:
每个activity应该对应了一个ActivityClientRecord。
说是这个类用于监控系统与应用程序直接的交互,称为仪器。这个类是蛮复杂的,下次再看咯。
/** * This manages the execution of the main thread in an * application process, scheduling and executing activities, * broadcasts, and other operations on it as the activity * manager requests. * * {@hide} */ public final class ActivityThread {}
这样ActivityThread作用大概就是用于控制与管理一个应用进程的主线程的操作,包括管理与处理activity manager发送过来的关于activities、广播以及其他的操作请求。
activityThread的主要成员变量如下:
private ContextImpl mSystemContext; static IPackageManager sPackageManager; final ApplicationThread mAppThread = new ApplicationThread(); final Looper mLooper = Looper.myLooper(); final H mH = new H(); final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<IBinder, ActivityClientRecord>(); // List of new activities (via ActivityRecord.nextIdle) that should // be reported when next we idle. ActivityClientRecord mNewActivities = null; // Number of activities that are currently visible on-screen. int mNumVisibleActivities = 0; final ArrayMap<IBinder, Service> mServices = new ArrayMap<IBinder, Service>(); AppBindData mBoundApplication; Profiler mProfiler; int mCurDefaultDisplayDpi; boolean mDensityCompatMode; Configuration mConfiguration; Configuration mCompatConfiguration; Application mInitialApplication; final ArrayList<Application> mAllApplications = new ArrayList<Application>(); // set of instantiated backup agents, keyed by package name final ArrayMap<String, BackupAgent> mBackupAgents = new ArrayMap<String, BackupAgent>(); /** Reference to singleton {@link ActivityThread} */ private static ActivityThread sCurrentActivityThread; Instrumentation mInstrumentation; String mInstrumentationAppDir = null; String mInstrumentationAppLibraryDir = null; String mInstrumentationAppPackage = null; String mInstrumentedAppDir = null; String mInstrumentedAppLibraryDir = null; boolean mSystemThread = false; boolean mJitEnabled = false; // These can be accessed by multiple threads; mPackages is the lock. // XXX For now we keep around information about all packages we have // seen, not removing entries from this map. // NOTE: The activity and window managers need to call in to // ActivityThread to do things like update resource configurations, // which means this lock gets held while the activity and window managers // holds their own lock. Thus you MUST NEVER call back into the activity manager // or window manager or anything that depends on them while holding this lock. final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<String, WeakReference<LoadedApk>>(); final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages = new ArrayMap<String, WeakReference<LoadedApk>>(); final ArrayList<ActivityClientRecord> mRelaunchingActivities = new ArrayList<ActivityClientRecord>(); Configuration mPendingConfiguration = null; private final ResourcesManager mResourcesManager;
从上往下慢慢看一些重要的成员变量,通过他们的作用来看ActivityThread的作用。
1.ContextImpl
class ContextImpl extends Context {
/** * Interface to global information about an application environment. This is * an abstract class whose implementation is provided by * the Android system. It * allows access to application-specific resources and classes, as well as * up-calls for application-level operations such as launching activities, * broadcasting and receiving intents, etc. */ public abstract class Context {
Context 是一个抽象类,activity也最终继承了这个类,这样子就可以提供了一个全局的环境,这也就是成为上下文的意思。
ContextImpl的作用主要有:
1.初始化时注册了一系列的服务:
static { registerService(ACCESSIBILITY_SERVICE, new ServiceFetcher() { public Object getService(ContextImpl ctx) { return AccessibilityManager.getInstance(ctx); }}); registerService(CAPTIONING_SERVICE, new ServiceFetcher() { public Object getService(ContextImpl ctx) { return new CaptioningManager(ctx); }}); 。。。。省略许多 registerService(USER_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(USER_SERVICE); IUserManager service = IUserManager.Stub.asInterface(b); return new UserManager(ctx, service); }}); registerService(APP_OPS_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder b = ServiceManager.getService(APP_OPS_SERVICE); IAppOpsService service = IAppOpsService.Stub.asInterface(b); return new AppOpsManager(ctx, service); }}); registerService(CAMERA_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { return new CameraManager(ctx); } }); registerService(PRINT_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder iBinder = ServiceManager.getService(Context.PRINT_SERVICE); IPrintManager service = IPrintManager.Stub.asInterface(iBinder); return new PrintManager(ctx.getOuterContext(), service, UserHandle.myUserId(), UserHandle.getAppId(Process.myUid())); }}); registerService(CONSUMER_IR_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { return new ConsumerIrManager(ctx); }}); }
其实ContextImp含有一个map来存储这些注册了的服务,只要通过service名字就可以拿到ServiceFetcher :
private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP = new HashMap<String, ServiceFetcher>(); private static int sNextPerContextServiceCacheIndex = 0; private static void registerService(String serviceName, ServiceFetcher fetcher) { if (!(fetcher instanceof StaticServiceFetcher)) { fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++; } SYSTEM_SERVICE_MAP.put(serviceName, fetcher); }
当想要在上下文调用 getSystemService查找获取系统服务的时候,就会在这个map里面查找:
@Override public Object getSystemService(String name) { ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name); return fetcher == null ? null : fetcher.getService(this); }
ServiceFetcher 是用于修改与查找sercive记录的类,其实最终存储service是一个 final ArrayList<Object> mServiceCache = new ArrayList<Object>();的成员变量,就是下面的红色部分。可以知道,通过键值对的形式就可以根据service的名字获得到对应的ServiceFetcher ,这个ServiceFetcher 会在mServiceCache 中查找并返回相应的service。
static class ServiceFetcher { int mContextCacheIndex = -1; /** * Main entrypoint; only override if you don't need caching. */ public Object getService(ContextImpl ctx) { ArrayList<Object> cache =<span style="color:#ff0000;"> ctx.mServiceCache</span>; Object service; synchronized (cache) { if (cache.size() == 0) { // Initialize the cache vector on first access. // At this point sNextPerContextServiceCacheIndex // is the number of potential services that are // cached per-Context. for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) { cache.add(null); } } else { service = cache.get(mContextCacheIndex); if (service != null) { return service; } } service = createService(ctx); cache.set(mContextCacheIndex, service); return service; } } /** * Override this to create a new per-Context instance of the * service. getService() will handle locking and caching. */ public Object createService(ContextImpl ctx) { throw new RuntimeException("Not implemented"); } }
2.剩下的基本就是Context类的实现,也就是说在activity中Context的方法的实现是在ContextImp里面的,这也体现了面向接口编程的思想。对应有什么方法以及作用可以参考Context类。
2.IPackageManager
这个IPackageManager发现打不开,在ActivityThread发现只有在一处地方调用到。public static IPackageManager getPackageManager() { if (sPackageManager != null) { //Slog.v("PackageManager", "returning cur default = " + sPackageManager); return sPackageManager; } IBinder b = ServiceManager.getService("package"); //Slog.v("PackageManager", "default service binder = " + b); sPackageManager = IPackageManager.Stub.asInterface(b); //Slog.v("PackageManager", "default service = " + sPackageManager); return sPackageManager; }这来直接就调用到了ServiceManager.getService("package");,所以可以猜测系统服务之一的包管理服务,这个IPackageManager应该是与包管理服务对应的提供相同方法的接口类,作用当然是为了时本地调用与远程调用保持一致,这样的好处就是对于使用者来说调用远程服务看不出来与与调用本地的区别。
3.ApplicationThread
ApplicationThread其实是一个binder对象,关系如下:ApplicationThreadProxy是IApplicationThread的代理,也就是说ApplicationThreadNative实现了IApplicationThread,最终方法实现是在ApplicationThreadProxy里面,ApplicationThreadNative继承了Binder,复写了onTransact,根据相应的code调用IApplicationThread的方法,最终调用的是ApplicationThreadProxy的实现。
public abstract class ApplicationThreadNative extends Binder implements IApplicationThread { /** * Cast a Binder object into an application thread interface, generating * a proxy if needed. */ static public IApplicationThread asInterface(IBinder obj) { if (obj == null) { return null; } IApplicationThread in = (IApplicationThread)obj.queryLocalInterface(descriptor); if (in != null) { return in; } return new ApplicationThreadProxy(obj); } public ApplicationThreadNative() { attachInterface(this, descriptor); } @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { switch (code) { case SCHEDULE_PAUSE_ACTIVITY_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); IBinder b = data.readStrongBinder(); boolean finished = data.readInt() != 0; boolean userLeaving = data.readInt() != 0; int configChanges = data.readInt(); schedulePauseActivity(b, finished, userLeaving, configChanges); return true; } 省略很多。。。 case SCHEDULE_STOP_ACTIVITY_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); IBinder b = data.readStrongBinder(); boolean show = data.readInt() != 0; int configChanges = data.readInt(); scheduleStopActivity(b, show, configChanges); return true; } case SCHEDULE_STOP_SERVICE_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); IBinder token = data.readStrongBinder(); scheduleStopService(token); return true; }
以以下这个为例子:
case SCHEDULE_PAUSE_ACTIVITY_TRANSACTION: { data.enforceInterface(IApplicationThread.descriptor); IBinder b = data.readStrongBinder(); boolean finished = data.readInt() != 0; boolean userLeaving = data.readInt() != 0; int configChanges = data.readInt(); schedulePauseActivity(b, finished, userLeaving, configChanges); return true; }
最终调用的是ApplicationThreadProxy的schedulePauseActivity:
public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); data.writeInt(finished ? 1 : 0); data.writeInt(userLeaving ? 1 :0); data.writeInt(configChanges); mRemote.transact(SCHEDULE_PAUSE_ACTIVITY_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); }ApplicationThread主要的作用是控制activity的生命周期,看一下列出来的部分的方法:
我们以schedulePauseActivity方法为例子看:
public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges) { sendMessage( finished ? <span style="color:#ff0000;">H.PAUSE_ACTIVITY_FINISHING</span> : H.PAUSE_ACTIVITY, token, (userLeaving ? 1 : 0), configChanges); }
sendMessage方法如下:
private void sendMessage(int what, Object obj, int arg1, int arg2) { sendMessage(what, obj, arg1, arg2, false); } private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) { if (DEBUG_MESSAGES) Slog.v( TAG, "SCHEDULE " + what + " " + mH.codeToString(what) + ": " + arg1 + " / " + obj); Message msg = Message.obtain(); msg.what = what; msg.obj = obj; msg.arg1 = arg1; msg.arg2 = arg2; if (async) { msg.setAsynchronous(true); } mH.sendMessage(msg); }
看到了,最终调用到mH,mH是一个hander对象:
</pre></p><pre name="code" class="java">private class H extends Handler { public static final int LAUNCH_ACTIVITY = 100; public static final int PAUSE_ACTIVITY = 101; public static final int PAUSE_ACTIVITY_FINISHING= 102; 省略若干、、、、 public static final int TRIM_MEMORY = 140; public static final int DUMP_PROVIDER = 141; public static final int UNSTABLE_PROVIDER_DIED = 142; public static final int REQUEST_ASSIST_CONTEXT_EXTRAS = 143; public static final int TRANSLUCENT_CONVERSION_COMPLETE = 144; public static final int INSTALL_PROVIDER = 145; 、、、 public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { case LAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); ActivityClientRecord r = (ActivityClientRecord)msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; case RELAUNCH_ACTIVITY: { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart"); ActivityClientRecord r = (ActivityClientRecord)msg.obj; handleRelaunchActivity(r); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; <span style="color:#ff0000;">case PAUSE_ACTIVITY: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause"); handlePauseActivity((IBinder)msg.obj, false, msg.arg1 != 0, msg.arg2); maybeSnapshot(); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break;</span> 、、、、 case INSTALL_PROVIDER: handleInstallProvider((ProviderInfo) msg.obj); break; } if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what)); }
private void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges) { ActivityClientRecord r = mActivities.get(token); if (r != null) { //Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r); if (userLeaving) { performUserLeavingActivity(r); } r.activity.mConfigChangeFlags |= configChanges; <span style="color:#ff0000;"> performPauseActivity(token, finished, r.isPreHoneycomb());</span> // Make sure any pending writes are now committed. if (r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } // Tell the activity manager we have paused. try { ActivityManagerNative.getDefault().activityPaused(token); } catch (RemoteException ex) { } } }
最终调用到:
final Bundle performPauseActivity(ActivityClientRecord r, boolean finished, boolean saveState) { if (r.paused) { if (r.activity.mFinished) { // If we are finishing, we won't call onResume() in certain cases. // So here we likewise don't want to call onPause() if the activity // isn't resumed. return null; } RuntimeException e = new RuntimeException( "Performing pause of activity that is not resumed: " + r.intent.getComponent().toShortString()); Slog.e(TAG, e.getMessage(), e); } Bundle state = null; if (finished) { r.activity.mFinished = true; } try { // Next have the activity save its current state and managed dialogs... if (!r.activity.mFinished && saveState) { state = new Bundle(); state.setAllowFds(false); mInstrumentation.callActivityOnSaveInstanceState(r.activity, state); r.state = state; } // Now we are idle. r.activity.mCalled = false; <span style="background-color: rgb(255, 102, 102);"> mInstrumentation.callActivityOnPause(r.activity);//回调onPause()方法</span> EventLog.writeEvent(LOG_ON_PAUSE_CALLED, UserHandle.myUserId(), r.activity.getComponentName().getClassName()); if (!r.activity.mCalled) { throw new SuperNotCalledException( "Activity " + r.intent.getComponent().toShortString() + " did not call through to super.onPause()"); } } catch (SuperNotCalledException e) { throw e; } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to pause activity " + r.intent.getComponent().toShortString() + ": " + e.toString(), e); } } r.paused = true; // Notify any outstanding on paused listeners ArrayList<OnActivityPausedListener> listeners; synchronized (mOnPauseListeners) { listeners = mOnPauseListeners.remove(r.activity); } int size = (listeners != null ? listeners.size() : 0); for (int i = 0; i < size; i++) { listeners.get(i).onPaused(r.activity); } return state; }
可以看到整个调用流程后大概就可以知道了,通过调用ApplicationThread的方法可以控制activity的生命周期。
那么ApplicationThread到底有谁调用,就是说有谁去控制activity的生命周期。
在activityThread里面找到了attach的方法:
private void attach(boolean system) { sCurrentActivityThread = this; mSystemThread = system; if (!system) { ViewRootImpl.addFirstDrawHandler(new Runnable() { @Override public void run() { ensureJitEnabled(); } }); android.ddm.DdmHandleAppName.setAppName("<pre-initialized>", UserHandle.myUserId()); RuntimeInit.setApplicationObject(mAppThread.asBinder()); IActivityManager mgr = ActivityManagerNative.getDefault(); try { <span style="color:#cc0000;"> mgr.attachApplication(mAppThread);</span> } catch (RemoteException ex) { // Ignore } } else { // Don't set application object here -- if the system crashes, // we can't display an alert, we just want to die die die. android.ddm.DdmHandleAppName.setAppName("system_process", UserHandle.myUserId()); try { mInstrumentation = new Instrumentation(); ContextImpl context = ContextImpl.createAppContext( this, getSystemContext().mPackageInfo); Application app = Instrumentation.newApplication(Application.class, context); mAllApplications.add(app); mInitialApplication = app; app.onCreate(); } catch (Exception e) { throw new RuntimeException( "Unable to instantiate Application():" + e.toString(), e); } } // add dropbox logging to libcore DropBox.setReporter(new DropBoxReporter()); ViewRootImpl.addConfigCallback(new ComponentCallbacks2() { @Override public void onConfigurationChanged(Configuration newConfig) { synchronized (mResourcesManager) { // We need to apply this change to the resources // immediately, because upon returning the view // hierarchy will be informed about it. if (mResourcesManager.applyConfigurationToResourcesLocked(newConfig, null)) { // This actually changed the resources! Tell // everyone about it. if (mPendingConfiguration == null || mPendingConfiguration.isOtherSeqNewer(newConfig)) { mPendingConfiguration = newConfig; sendMessage(H.CONFIGURATION_CHANGED, newConfig); } } } } @Override public void onLowMemory() { } @Override public void onTrimMemory(int level) { } }); }大概明白了,其实是有activitymanager联系到ApplicationThread,也就是说有它去控制activity的生命周期的。
4. H mH
这个其实就是个hander对象,用于处理applicationTread对应的方法,上面已经说过了。5. ArrayMap<IBinder, ActivityClientRecord> mActivities
这是用于存放所有activity的记录的集合。ActivityClientRecord类如下:static final class ActivityClientRecord { IBinder token; int ident; Intent intent; Bundle state; <span style="color:#ff0000;">Activity activity;</span> Window window; Activity parent; String embeddedID; Activity.NonConfigurationInstances lastNonConfigurationInstances; boolean paused; boolean stopped; boolean hideForNow; Configuration newConfig; Configuration createdConfig; ActivityClientRecord nextIdle; String profileFile; ParcelFileDescriptor profileFd; boolean autoStopProfiler; ActivityInfo activityInfo; CompatibilityInfo compatInfo; LoadedApk packageInfo; List<ResultInfo> pendingResults; List<Intent> pendingIntents; boolean startsNotResumed; boolean isForward; int pendingConfigChanges; boolean onlyLocalRequest; View mPendingRemoveWindow; WindowManager mPendingRemoveWindowManager; ActivityClientRecord() { parent = null; embeddedID = null; paused = false; stopped = false; hideForNow = false; nextIdle = null; } public boolean isPreHoneycomb() { if (activity != null) { return activity.getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB; } return false; } public String toString() { ComponentName componentName = intent != null ? intent.getComponent() : null; return "ActivityRecord{" + Integer.toHexString(System.identityHashCode(this)) + " token=" + token + " " + (componentName == null ? "no component name" : componentName.toShortString()) + "}"; } }
每个activity应该对应了一个ActivityClientRecord。
6.Instrumentation
/** * Base class for implementing application instrumentation code. When running * with instrumentation turned on, this class will be instantiated for you * before any of the application code, allowing you to monitor all of the * interaction the system has with the application. An Instrumentation * implementation is described to the system through an AndroidManifest.xml's * <instrumentation> tag. */ public class Instrumentation
说是这个类用于监控系统与应用程序直接的交互,称为仪器。这个类是蛮复杂的,下次再看咯。
相关文章推荐
- Puppet之exec资源管理
- SSL编程(3).NET实现SSL服务端
- Spring笔记
- 游戏性能对比:Steam OS相比Windows仍有显著差距
- LeetCode(172)Factorial Trailing Zeroes
- javascript 中的 delete
- LeetCode(172)Factorial Trailing Zeroes
- C语言中内联函数的作用
- Android 混淆机制
- Spark RDD Persistence
- 摆脱恐惧(老俞演讲稿)
- 51CTO的无闻的Go基础教程中并发相关的概念有错误
- POJ 3295 *** Tautology
- 欢迎使用CSDN-markdown编辑器
- centos7 boot空间不够用的解决方案
- 基于语义的物体检测笔记
- 九度OJ 1339:ACM (排序)
- (ZT)C语言运算符号优先级图表
- 动手动脑之多态与异常处理
- 九度OJ 1339:ACM (排序)