Watchdog 源码分析
来源:互联网 发布:网络文凭如何取得 编辑:程序博客网 时间:2024/05/16 05:59
public class Watchdog extends Thread { static final String TAG = "Watchdog"; // Set this to true to use debug default values. static final boolean DB = false; // Set this to true to have the watchdog record kernel thread stacks when it fires static final boolean RECORD_KERNEL_THREADS = true; static final long DEFAULT_TIMEOUT = DB ? 10*1000 : 60*1000; static final long CHECK_INTERVAL = DEFAULT_TIMEOUT / 2; // These are temporally ordered: larger values as lateness increases static final int COMPLETED = 0; static final int WAITING = 1; static final int WAITED_HALF = 2; static final int OVERDUE = 3; static final int TIME_SF_WAIT = 20000; // Which native processes to dump into dropbox's stack traces public static final String[] NATIVE_STACKS_OF_INTEREST = new String[] { "/system/bin/mediaserver", "/system/bin/sdcard", "/system/bin/surfaceflinger" }; static Watchdog sWatchdog; ExceptionLog exceptionHWT; private ServerSocket mServerSocket = null; private static final int PROT = 23233; private Socket mSocket = null; /* This handler will be used to post message back onto the main thread */ final ArrayList<HandlerChecker> mHandlerCheckers = new ArrayList<>(); final HandlerChecker mMonitorChecker; ContentResolver mResolver; ActivityManagerService mActivity; int mPhonePid; IActivityController mController; boolean mAllowRestart = true; public long GetSFStatus() { if (exceptionHWT != null) { return exceptionHWT.SFMatterJava(0, 0); } else { return 0; } } public static int GetSFReboot() { return SystemProperties.getInt("service.sf.reboot", 0); } public static void SetSFReboot() { int OldTime = SystemProperties.getInt("service.sf.reboot", 0); OldTime = OldTime + 1; if (OldTime > 9) OldTime = 9; SystemProperties.set("service.sf.reboot", String.valueOf(OldTime)); } /** * Used for checking status of handle threads and scheduling monitor callbacks. */ public final class HandlerChecker implements Runnable { private final Handler mHandler; public final String mName; private final long mWaitMax; public final ArrayList<Monitor> mMonitors = new ArrayList<Monitor>(); private boolean mCompleted; private Monitor mCurrentMonitor; private long mStartTime; HandlerChecker(Handler handler, String name, long waitMaxMillis) { mHandler = handler; mName = name; mWaitMax = waitMaxMillis; mCompleted = true; } public void addMonitor(Monitor monitor) { Log.d("hecheng_watchdog", "addMonitor"); mMonitors.add(monitor); } //检测本 handlerChecker 线程是否ok public void scheduleCheckLocked() { if (mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling()) { // If the target looper has recently been polling, then // there is no reason to enqueue our checker on it since that // is as good as it not being deadlocked. This avoid having // to do a context switch to check the thread. Note that we // only do this if mCheckReboot is false and we have no // monitors, since those would need to be executed at this point. mCompleted = true; return; } //如果其一次检测没有完成返回。 if (!mCompleted) { // we already have a check in flight, so no need return; } mCompleted = false; mCurrentMonitor = null; //给被监控的进程发送消息 //监控线程受到消息后会调用 HandlerChecker的run 方法。 //mStartTime 记录本handlerChecker 开始检测的时间 mStartTime = SystemClock.uptimeMillis(); mHandler.postAtFrontOfQueue(this); } //判断当前handlerChcker是否超时 public boolean isOverdueLocked() { return (!mCompleted) && (SystemClock.uptimeMillis() > mStartTime + mWaitMax); } //如果没有COMPLETED,获取其超时时间,并返回对应的值 //超时时间小于指定时间的一般(一般为30s),返回WAITING, 系统会继续等待不做处理 //超时时间大于 30s 小于指定时间,返回WAITED_HALF, 系统会dump出运行所有进程堆栈,然后继续等待 //超过超时时间返回OVEROUE,系统会dump堆栈,并重启 public int getCompletionStateLocked() { if (mCompleted) { return COMPLETED; } else { long latency = SystemClock.uptimeMillis() - mStartTime; if (latency < mWaitMax/2) { return WAITING; } else if (latency < mWaitMax) { return WAITED_HALF; } } return OVERDUE; } public Thread getThread() { return mHandler.getLooper().getThread(); } public String getName() { return mName; } public String describeBlockedStateLocked() { if (mCurrentMonitor == null) { return "Blocked in handler on " + mName + " (" + getThread().getName() + ")"; } else { return "Blocked in monitor " + mCurrentMonitor.getClass().getName() + " on " + mName + " (" + getThread().getName() + ")"; } } //调用到此处说明,此线程的handler 是通畅的可以正常接受消息 @Override public void run() { //监控 此线程内添加的monitor 对象,是否存在死锁情况, final int size = mMonitors.size(); for (int i = 0 ; i < size ; i++) { synchronized (Watchdog.this) { mCurrentMonitor = mMonitors.get(i); } mCurrentMonitor.monitor(); } //此线程内的monitor 不存在死锁情况,handlerChecker基本上就通过了。 Log.d("hecheng_watchdog", "Thread is " + mName + ", size is " + size); synchronized (Watchdog.this) { mCompleted = true; mCurrentMonitor = null; } } } final class RebootRequestReceiver extends BroadcastReceiver { @Override public void onReceive(Context c, Intent intent) { if (intent.getIntExtra("nowait", 0) != 0) { rebootSystem("Received ACTION_REBOOT broadcast"); return; } if (intent.getIntExtra("hangdump", 0) != 0) { Slog.i(TAG, "Receiver hangdump start!\n"); runHangdump(); return; } Slog.w(TAG, "Unsupported ACTION_REBOOT broadcast: " + intent); } } /** Monitor for checking the availability of binder threads. The monitor will block until * there is a binder thread available to process in coming IPCs to make sure other processes * can still communicate with the service. */ private static final class BinderThreadMonitor implements Watchdog.Monitor { @Override public void monitor() { Binder.blockUntilThreadAvailable(); } } public interface Monitor { void monitor(); } public static Watchdog getInstance() { if (sWatchdog == null) { sWatchdog = new Watchdog(); } return sWatchdog; } private Watchdog() { super("watchdog"); // Initialize handler checkers for each common thread we want to check. Note // that we are not currently checking the background thread, since it can // potentially hold longer running operations with no guarantees about the timeliness // of operations there. // The shared foreground thread is the main checker. It is where we // will also dispatch monitor checks and do other work. //watchdog 检测多个线程,超时时间是 60秒, //HandlerChecker 是通过检测handler 来检测线程是否堵塞 //TODO mMonitorChecker = new HandlerChecker(FgThread.getHandler(), "foreground thread", DEFAULT_TIMEOUT); mHandlerCheckers.add(mMonitorChecker); // Add checker for main thread. We only do a quick check since there // can be UI running on the thread. mHandlerCheckers.add(new HandlerChecker(new Handler(Looper.getMainLooper()), "main thread", DEFAULT_TIMEOUT)); // Add checker for shared UI thread. mHandlerCheckers.add(new HandlerChecker(UiThread.getHandler(), "ui thread", DEFAULT_TIMEOUT)); // And also check IO thread. mHandlerCheckers.add(new HandlerChecker(IoThread.getHandler(), "i/o thread", DEFAULT_TIMEOUT)); mHandlerCheckers.add(new HandlerChecker(DisplayThread.getHandler(), "display thread", DEFAULT_TIMEOUT)); if (SystemProperties.get("ro.have_aee_feature").equals("1")) { exceptionHWT = new ExceptionLog(); runHangdumpServer(); } } public void setLight() { Slog.i(TAG, "runHangdump-server-start setLight!\n"); Light tNotificationLight = null; LightsManager tLights = LocalServices.getService(LightsManager.class); tNotificationLight = tLights.getLight(LightsManager.LIGHT_ID_NOTIFICATIONS); tNotificationLight.setFlashing(0xFF0000FF, Light.LIGHT_FLASH_NONE, 1000, 1000); Slog.i(TAG, "runHangdump-server-start setLight end!\n"); } public void runHangdump() { Slog.i(TAG, "runHangdump-server-start thread!\n"); new Thread(new Runnable() { @Override public void run() { Slog.i(TAG, "runHangdump-server hangdump thread start!\n"); final ArrayList<HandlerChecker> blockedCheckers; String subject; exceptionHWT.WDTMatterJava(52325); setLight(); // trace and wait another half. if (exceptionHWT != null) exceptionHWT.WDTMatterJava(60); dumpAllBackTraces(true); Slog.i(TAG, "runHangdump-server thread dumpAllBackTraces first end!\n"); SystemClock.sleep(30000); Slog.i(TAG, "runHangdump-server thread get subject start!\n"); blockedCheckers = getBlockedCheckersLocked(); subject = describeCheckersLocked(blockedCheckers); EventLog.writeEvent(EventLogTags.WATCHDOG, subject); Slog.i(TAG, "runHangdump-server thread get subject end!\n"); if (exceptionHWT != null) exceptionHWT.WDTMatterJava(60); dumpAllBackTraces(true); Slog.i(TAG, "runHangdump-server thread dumpAllBackTraces second end!\n"); SystemClock.sleep(2000); Slog.i(TAG, "runHangdump-server thread get dumpKernelStackTraces start!\n"); dumpKernelStackTraces(); // Trigger the kernel to dump all blocked threads, and backtraces on all CPUs to the kernel log doSysRq('w'); doSysRq('l'); Slog.i(TAG, "runHangdump-server thread get dumpKernelStackTraces end!\n"); Slog.i(TAG, "runHangdump-server thread start addErrorToDropbox!\n"); mActivity.addErrorToDropBox("watchdog", null, "system_server", null, null, subject, null, null, null); Slog.i(TAG, "runHangdump-server thread addErrorToDropBox end!subject:" + "subject!" + "\n"); SystemClock.sleep(2000); if (exceptionHWT != null) { Slog.i(TAG, "runHangdump-server exceptionHWT start!\n"); exceptionHWT.WDTMatterJava(52323); } } }).start(); } public void runHangdumpServer() { Slog.i(TAG, "runHangdumpServer start!\n"); new Thread(new Runnable() { @Override public void run() { Slog.i(TAG, "runHangdumpServer thread!\n"); try{ if (mServerSocket == null) { Slog.i(TAG, "runHangdumpServer ServerSocket(prot):" + PROT + "!\n"); mServerSocket= new ServerSocket(PROT); while(true){ Slog.i(TAG, "runHangdumpServer accept!\n"); mSocket = mServerSocket.accept(); InputStream inputStream = mSocket.getInputStream(); OutputStream outputStream = mSocket.getOutputStream(); DataInputStream mDataInputStream = new DataInputStream(new BufferedInputStream(inputStream)); byte[] data = new byte[16]; int len = mDataInputStream.read(data); String msg = new String(data, "UTF-8"); Slog.i(TAG, "runHangdumpServer thread msg: " + msg + "\n"); while(msg!=null){ Slog.i(TAG, "runHangdumpServer write!\n"); outputStream.write("hello runHangdumpServer ok!\n".getBytes()); outputStream.flush(); if (msg.equals("start_hangdump_j")) { Slog.i(TAG, "runHangdumpServer thread_j start!\n"); runHangdump(); } else if (msg.equals("start_hangdump_c")) { Slog.i(TAG, "runHangdumpServer thread_c start!\n"); runHangdump(); } break; } } } } catch(Exception e){ Slog.w(TAG, "mServerSocket exception", e); } finally { Slog.i(TAG, "mServerSocket finally!\n"); try { mSocket.close(); mSocket = null; mServerSocket.close(); mServerSocket = null; } catch (IOException e) { Slog.w(TAG, "mServerSocket IOException", e); } } } }).start(); } public void init(Context context, ActivityManagerService activity) { mResolver = context.getContentResolver(); mActivity = activity; context.registerReceiver(new RebootRequestReceiver(), new IntentFilter(Intent.ACTION_REBOOT), android.Manifest.permission.REBOOT, null); if(exceptionHWT!= null){ exceptionHWT.WDTMatterJava(0); } } public void processStarted(String name, int pid) { synchronized (this) { if ("com.android.phone".equals(name)) { mPhonePid = pid; } } } public void setActivityController(IActivityController controller) { synchronized (this) { mController = controller; } } public void setAllowRestart(boolean allowRestart) { synchronized (this) { mAllowRestart = allowRestart; } } //addMonitor 添加 fg线程内的对象monitor,在检测fg线程时,并检测其添加的对象,是否存在死锁。 //检测顺序为,先检测fg线程的handler是否可以正常接受信息,然后检测其添加的monitor 是否存在死锁 public void addMonitor(Monitor monitor) { synchronized (this) { if (isAlive()) { throw new RuntimeException("Monitors can't be added once the Watchdog is running"); } mMonitorChecker.addMonitor(monitor); } } //addThread 添加要检测的 线程,默认时间为60s,可以指定时间时间 public void addThread(Handler thread) { addThread(thread, DEFAULT_TIMEOUT); } public void addThread(Handler thread, long timeoutMillis) { synchronized (this) { if (isAlive()) { throw new RuntimeException("Threads can't be added once the Watchdog is running"); } final String name = thread.getLooper().getThread().getName(); mHandlerCheckers.add(new HandlerChecker(thread, name, timeoutMillis)); } } /** * Perform a full reboot of the system. */ void rebootSystem(String reason) { Slog.i(TAG, "Rebooting system because: " + reason); IPowerManager pms = (IPowerManager)ServiceManager.getService(Context.POWER_SERVICE); try { pms.reboot(false, reason, false); } catch (RemoteException ex) { } } //检测所有的handlerChecker,看其中是否存在超时的现象。 private int evaluateCheckerCompletionLocked() { int state = COMPLETED; //遍历检测handlerChecker 看是否存在超时现象。 for (int i=0; i<mHandlerCheckers.size(); i++) { HandlerChecker hc = mHandlerCheckers.get(i); state = Math.max(state, hc.getCompletionStateLocked());//getCompletionStateLocked 检测对应的线程的handlerChecker是否超时 } return state; } private ArrayList<HandlerChecker> getBlockedCheckersLocked() { ArrayList<HandlerChecker> checkers = new ArrayList<HandlerChecker>(); for (int i=0; i<mHandlerCheckers.size(); i++) { HandlerChecker hc = mHandlerCheckers.get(i); if (hc.isOverdueLocked()) { checkers.add(hc); } } return checkers; } private String describeCheckersLocked(ArrayList<HandlerChecker> checkers) { StringBuilder builder = new StringBuilder(128); for (int i=0; i<checkers.size(); i++) { if (builder.length() > 0) { builder.append(", "); } builder.append(checkers.get(i).describeBlockedStateLocked()); } return builder.toString(); }/* private void CputimeEnable(String bootevent){ try { FileOutputStream fcputime = new FileOutputStream("/proc/mtprof/cputime"); fcputime.write(bootevent.getBytes()); fcputime.close(); } catch (FileNotFoundException e) { Slog.e(TAG, "cputime entry can not found!", e); } catch (java.io.IOException e) { Slog.e(TAG, "cputime entry open fail", e); } }*/ //watchdog 监控开始 @Override public void run() { boolean waitedHalf = false; boolean mNeedDump = false; boolean mSFHang = false; while (true) { final ArrayList<HandlerChecker> blockedCheckers; String subject; mSFHang = false; if (exceptionHWT != null) { exceptionHWT.WDTMatterJava(60); } if (mNeedDump) { // We've waited half the deadlock-detection interval. Pull a stack // trace and wait another half. if (exceptionHWT != null) exceptionHWT.WDTMatterJava(60); dumpAllBackTraces(true); mNeedDump = false; } final boolean allowRestart; int debuggerWasConnected = 0; Slog.w(TAG, "SWT Watchdog before synchronized:" + SystemClock.uptimeMillis()); synchronized (this) { Slog.w(TAG, "SWT Watchdog after synchronized:" + SystemClock.uptimeMillis()); long timeout = CHECK_INTERVAL; long SFHangTime; // Make sure we (re)spin the checkers that have become idle within // this wait-and-check interval //TODO 遍历所有的handlerChecker, 向他们发送消息看线程是否ok。 //如果handlerChecker的前一次检测没有完成,那么此handlerchecker 线程跳过本次检测。 for (int i=0; i<mHandlerCheckers.size(); i++) { HandlerChecker hc = mHandlerCheckers.get(i); Log.d("hecheng_watchdog", "hc is " + hc.mName + ", mMonitors size is " + hc.mMonitors.size()); hc.scheduleCheckLocked(); } if (debuggerWasConnected > 0) { debuggerWasConnected--; } // NOTE: We use uptimeMillis() here because we do not want to increment the time we // wait while asleep. If the device is asleep then the thing that we are waiting // to timeout on is asleep as well and won't have a chance to run, causing a false // positive on when to kill things. //CputimeEnable(new String("1")); long start = SystemClock.uptimeMillis(); //设置此循环是为了防止,被异常唤醒,确保wait 时间为 30s while (timeout > 0) { if (Debug.isDebuggerConnected()) { debuggerWasConnected = 2; } try { Slog.w(TAG, "SWT Watchdog before wait timeout:" + timeout); Slog.w(TAG, "SWT Watchdog before wait current time:" + SystemClock.uptimeMillis()); Slog.w(TAG, "SWT Watchdog before wait start:" + start); Slog.w(TAG, "SWT Watchdog before wait CHECK_INTERVAL:" + CHECK_INTERVAL); wait(timeout); Slog.w(TAG, "SWT Watchdog after wait current time:" + SystemClock.uptimeMillis()); } catch (InterruptedException e) { Log.wtf(TAG, e); } if (Debug.isDebuggerConnected()) { debuggerWasConnected = 2; } //CHECK_INTERVAL 30s, 那么相当于watchdog 30 秒检测一次 timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start); } //MTK enhance SFHangTime = GetSFStatus(); Slog.w(TAG, "**Get SF Time **" + SFHangTime); if (SFHangTime > TIME_SF_WAIT * 2) { Slog.v(TAG, "**SF hang Time **" + SFHangTime); mSFHang = true; } //@@ else { //检测是否存在超时的线程 final int waitState = evaluateCheckerCompletionLocked(); //COMPLETED 无超时进程 //WAITING 有等待进程,0< 等待时间< 超时时间/2 //WAITED_HALF 有等待进程,超时时间/2 < 等待时间 < 超时时间 if (waitState == COMPLETED) { Slog.v(TAG, "**COMPLETED **"); // The monitors have returned; reset waitedHalf = false; //CputimeEnable(new String("0")); continue; } else if (waitState == WAITING) { Slog.v(TAG, "**WAITING **"); // still waiting but within their configured intervals; back off and recheck // CputimeEnable(new String("0")); continue; } else if (waitState == WAITED_HALF) { Slog.v(TAG, "**WAITED_HALF **"); if (!waitedHalf) { Slog.v(TAG, "**waitedHalf **"); // We've waited half the deadlock-detection interval. Pull a stack // trace and wait another half. mNeedDump = true; waitedHalf = true; } continue; } } //handlerChecker 超时 //something is overdue! blockedCheckers = getBlockedCheckersLocked();//获取所有超时的handlerChecker subject = describeCheckersLocked(blockedCheckers);// 获取String ,看是 handler time out, 还是 monitor time out. allowRestart = mAllowRestart; } //CputimeEnable(new String("0")); // If we got here, that means that the system is most likely hung. // First collect stack traces from all threads of the system process. // Then kill this process so that the system will restart. Slog.e(TAG, "**SWT happen **" + subject); if (mSFHang && subject.isEmpty()) { subject = "surfaceflinger hang."; } EventLog.writeEvent(EventLogTags.WATCHDOG, subject); if (exceptionHWT != null) exceptionHWT.WDTMatterJava(60); dumpAllBackTraces(false); // Give some extra time to make sure the stack traces get written. // The system's been hanging for a minute, another second or two won't hurt much. SystemClock.sleep(2000); // Pull our own kernel thread stacks as well if we're configured for that if (RECORD_KERNEL_THREADS) { dumpKernelStackTraces(); } // Trigger the kernel to dump all blocked threads, and backtraces on all CPUs to the kernel log doSysRq('w'); doSysRq('l'); /// M: WDT debug enhancement /// need to wait the AEE dumps all info, then kill system server @{ /* // Try to add the error to the dropbox, but assuming that the ActivityManager // itself may be deadlocked. (which has happened, causing this statement to // deadlock and the watchdog as a whole to be ineffective) Thread dropboxThread = new Thread("watchdogWriteToDropbox") { public void run() { mActivity.addErrorToDropBox( "watchdog", null, "system_server", null, null, name, null, stack, null); } }; dropboxThread.start(); try { dropboxThread.join(2000); // wait up to 2 seconds for it to return. } catch (InterruptedException ignored) {} */ Slog.v(TAG, "** save all info before killnig system server **"); mActivity.addErrorToDropBox("watchdog", null, "system_server", null, null, subject, null, null, null); IActivityController controller; synchronized (this) { controller = mController; } if ((mSFHang == false) && (controller != null)) { Slog.i(TAG, "Reporting stuck state to activity controller"); try { Binder.setDumpDisabled("Service dumps disabled due to hung system process."); Slog.i(TAG, "Binder.setDumpDisabled"); // 1 = keep waiting, -1 = kill system int res = controller.systemNotResponding(subject); if (res >= 0) { Slog.i(TAG, "Activity controller requested to coninue to wait"); waitedHalf = false; continue; } Slog.i(TAG, "Activity controller requested to reboot"); } catch (RemoteException e) { } } // Only kill the process if the debugger is not attached. if (Debug.isDebuggerConnected()) { debuggerWasConnected = 2; } if (debuggerWasConnected >= 2) { Slog.w(TAG, "Debugger connected: Watchdog is *not* killing the system process"); } else if (debuggerWasConnected > 0) { Slog.w(TAG, "Debugger was connected: Watchdog is *not* killing the system process"); } else if (!allowRestart) { Slog.w(TAG, "Restart not allowed: Watchdog is *not* killing the system process"); } else { Slog.w(TAG, "*** WATCHDOG KILLING SYSTEM PROCESS: " + subject); for (int i=0; i<blockedCheckers.size(); i++) { Slog.w(TAG, blockedCheckers.get(i).getName() + " stack trace:"); StackTraceElement[] stackTrace = blockedCheckers.get(i).getThread().getStackTrace(); for (StackTraceElement element: stackTrace) { Slog.w(TAG, " at " + element); } } SystemClock.sleep(25000); /// @} Slog.w(TAG, "*** GOODBYE!"); // MTK enhance if (mSFHang) { Slog.w(TAG, "SF hang!"); if (GetSFReboot() > 3) { Slog.w(TAG, "SF hang reboot time larger than 3 time, reboot device!"); rebootSystem("Maybe SF driver hang,reboot device."); } else { SetSFReboot(); } } //@ Process.killProcess(Process.myPid()); System.exit(10); } waitedHalf = false; } } private void doSysRq(char c) { try { FileWriter sysrq_trigger = new FileWriter("/proc/sysrq-trigger"); sysrq_trigger.write(c); sysrq_trigger.close(); } catch (IOException e) { Slog.w(TAG, "Failed to write to /proc/sysrq-trigger", e); } } private File dumpKernelStackTraces() { String tracesPath = SystemProperties.get("dalvik.vm.stack-trace-file", null); if (tracesPath == null || tracesPath.length() == 0) { return null; } native_dumpKernelStacks(tracesPath); return new File(tracesPath); } private native void native_dumpKernelStacks(String tracesPath); /** * M: WDT debug enhancement */ private File dumpAllBackTraces(boolean clearTraces) { /*debug flag for dump all thread backtrace for ANR*/ ArrayList<Integer> pids = new ArrayList<Integer>(); /// M: WDT debug enhancement /// it's better to dump all running processes backtraces /// and integrate with AEE @{ // pids.add(Process.myPid()); //if (mPhonePid > 0) pids.add(mPhonePid); // Pass !waitedHalf so that just in case we somehow wind up here without having mActivity.getRunningProcessPids(pids); File stack = ActivityManagerService.dumpStackTraces(true, pids, null, null, NATIVE_STACKS_OF_INTEREST); return stack; }}
0 0
- Watchdog 源码分析
- framework watchdog源码分析
- Android软Watchdog源码分析
- Android软Watchdog源码分析
- Android软Watchdog源码分析
- Android软Watchdog源码分析
- WatchDog 源码
- watchdog 分析
- kdb中的watchdog分析
- Android WatchDog分析
- android -- WatchDog看门狗分析
- SW watchdog 分析
- Android watchdog分析
- android -- WatchDog看门狗分析
- android -- WatchDog看门狗分析
- SW watchdog 分析
- linux watchdog 分析
- Android WatchDog分析
- Access2016学习7
- 文章标题
- Linux 下的zip,rar
- 技术点详解---SSL VPN
- Android笔记:EditText自定义背景
- Watchdog 源码分析
- 开源软件修改的必要性
- 28款GitHub最流行的开源机器学习项目
- 设计模式之 工厂模式(Factory)
- 【c++】c++初识--基本知识梳理(1)
- java synchronized详解
- Android新的menu实现——ActionMode
- struts2的核心和工作原理
- 多字段模糊查询,前一个字段无搜索结果返回null时不影响后一个字段模糊查询