android 非root app 捕捉系統廣播_APP的生死之道

這篇文章主要介紹APP在安卓系統中是怎么被殺死的,按照怎樣的一個策略去釋放進程;同時介紹一些延長應用存活時間的方案,雖然這個在現在安卓系統上越來越難實現了,但是也是可以稍微了解下,主要也是通過這些hack的方案更好的了解安卓系統對進程的管理。

進程是怎么被殺死的?

我們知道,安卓系統里的所有APP都是被系統所托管的,也就是說,安卓系統負責APP進程的創建和回收。

進程創建很簡單,就是在APP啟動的時候,由Zygote進程fork出一個新的進程出來(注:系統服務system server也是由Zygote所創建),這個不是我們這邊文章的重點,后面會有專門的文章來敘述。

進程的回收發生在如下幾種情況:

  • 進程Crash掉了

  • 用戶主動的退出(殺進程,不殺進程的app還是在系統中的,這樣是為了能快速的再次啟動~)

  • 內存緊張,并且進程已經不在可見進程了

前面2種是用戶行為或APP本身問題導致的進程回收,而第3種是系統行為,也是我們做APP保活可以做的地方。在內存緊張時,由Low Memory Killer根據進程的優先級來按順序的進行回收。

Low Memory Killer(LMK)

Low Memory Killer是基于Linux的Out Of Memory Killer(OOMKiller)優化的一種內存回收機制,相對與OOMKiller,它發生的時機要稍微早一點。

為什么要有這樣的改進?

一般的內存分配的邏輯是:程序向OS申請一段連續的內存空間,然后返回這段空間的起始地址。如果沒有足夠的空間可分配,則返回null回來,表示系統沒有可分配的內存。

Linux的內存分配則更加的積極:它假設應用申請了內存空間后并不會立即去使用它,所以允許超剩余內存的申請,當應用真的需要使用它的時候,操作系統可能已經通過回收了其他應用的內存空間而變得有能力去滿足這個應用的需求,簡單的說,就是允許應用申請比實際可分配空間(包括物理內存和Swap)更多的內存,這個特性稱為OverCommit。

如果申請的內存在需要使用的時候還沒有被釋放掉,那么就會觸發OOM Killer,直接干掉這個進程,這個可能不是用戶想要得到的結果。而LMK則將內存回收的時間提前,選擇殺死那么優先級最低的進程來釋放內存,同時設置了不同的內存大小觸發時機,這樣更加的靈活。

LMK的執行原理

安卓內核會每隔一段時間會檢查當前系統的空閑內存是否低于某個預置,如果是,則按照oom_adj的值按照從大到小的順序殺死進程,直到釋放的內存足夠。

所以這里有2個最直接相關的值:

  • 內存閾值

  • oom_adj值

1、LMK之內存閾值

LMK是個多層次的內存回收器,它會根據內存的不同的閾值進行內存的回收,而具體的內存的的閾值是寫在系統文件里的,位置在/sys/module/lowmemorykiller/parameters/minfree(必須在root下查看):74421c74-111d-eb11-8da9-e4434bdf6706.png上面數字的單位是頁(page),1page=4KB。內存閾值在不同的手機上時不一樣的,那么這個值是怎么來確定的呢?這個可以看下ProcessList的代碼:

private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
// Scale buckets from avail memory: at 300MB we use the lowest values to
// 700MB or more for the top values.
float scaleMem = ((float)(mTotalMemMb-350))/(700-350);

// Scale buckets from screen size.
int minSize = 480*800; // 384000
int maxSize = 1280*800; // 1024000 230400 870400 .264
float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
if (false) {
Slog.i("XXXXXX", "scaleMem=" + scaleMem);
Slog.i("XXXXXX", "scaleDisp=" + scaleDisp + " dw=" + displayWidth
+ " dh=" + displayHeight);
}

float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
if (scale < 0) scale = 0;
else if (scale > 1) scale = 1;
int minfree_adj = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
int minfree_abs = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);
if (false) {
Slog.i("XXXXXX", "minfree_adj=" + minfree_adj + " minfree_abs=" + minfree_abs);
}

final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;

for (int i=0; iint low = mOomMinFreeLow[i];
int high = mOomMinFreeHigh[i];
if (is64bit) {
// Increase the high min-free levels for cached processes for 64-bit
if (i == 4) high = (high*3)/2;
else if (i == 5) high = (high*7)/4;
}
mOomMinFree[i] = (int)(low + ((high-low)*scale));
}

if (minfree_abs >= 0) {
for (int i=0; i mOomMinFree[i] = (int)((float)minfree_abs * mOomMinFree[i]
/ mOomMinFree[mOomAdj.length - 1]);
}
}

if (minfree_adj != 0) {
for (int i=0; i mOomMinFree[i] += (int)((float)minfree_adj * mOomMinFree[i]
/ mOomMinFree[mOomAdj.length - 1]);
if (mOomMinFree[i] < 0) {
mOomMinFree[i] = 0;
}
}
}

// The maximum size we will restore a process from cached to background, when under
// memory duress, is 1/3 the size we have reserved for kernel caches and other overhead
// before killing background processes.
mCachedRestoreLevel = (getMemLevel(ProcessList.CACHED_APP_MAX_ADJ)/1024) / 3;

// Ask the kernel to try to keep enough memory free to allocate 3 full
// screen 32bpp buffers without entering direct reclaim.
int reserve = displayWidth * displayHeight * 4 * 3 / 1024;
int reserve_adj = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAdjust);
int reserve_abs = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAbsolute);

if (reserve_abs >= 0) {
reserve = reserve_abs;
}

if (reserve_adj != 0) {
reserve += reserve_adj;
if (reserve < 0) {
reserve = 0;
}
}

if (write) {
ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
buf.putInt(LMK_TARGET);
for (int i=0; i buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);
buf.putInt(mOomAdj[i]);
}

writeLmkd(buf);
SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
}
// GB: 2048,3072,4096,6144,7168,8192
// HC: 8192,10240,12288,14336,16384,20480
}

上面的代碼主要做了如下幾件事情:

  1. 根據手機的內存值,計算一個內存的比例值scaleMem(具體意義不明白~~)

  2. 根據傳入的window的displayWidth/displayHeight,計算一個視圖的比例值scaleDisp

  3. 在scaleMem和scaleDisp中選擇最大的一個

  4. 計算內存閾值,這里會經過3個步驟的計算(具體略)

  5. 傳給LMKD守護進程(進程間通信采用了socket方式),并且寫入到minfree文件里

這個方法是在什么時候觸發的呢?

updateOomLevels()是在AMS的updateConfiguration()方法里調用的,也就是說在設備配置變化的時候就會觸發。

為什么是6個值?

這個就是安卓系統做的分層次回收,它定義了6個層級的回收閾值,分別對應到了不同的進程狀態。

具體的定義:

private final int[] mOomAdj = new int[] {
FOREGROUND_APP_ADJ,
VISIBLE_APP_ADJ,
PERCEPTIBLE_APP_ADJ,
BACKUP_APP_ADJ,
CACHED_APP_MIN_ADJ,
CACHED_APP_MAX_ADJ
};
  • FOREGROUND_APP_ADJ:前臺進程

    • 該進程擁有一個正在和用戶交互的activity

    • 該進程擁有一個service,該service與用戶正在交互的Activity綁定

    • 該進程擁有一個service,該service通過startForeground()稱為了前臺服務

    • 某個進程擁有一個BroadcastReceiver,并且該BroadcastReceiver正在執行其onReceive()方法

  • VISIBLE_APP_ADJ:可見進程

    • 該進程擁有不在前臺但是用戶任可見的activity(比如支付時拉起的第三方支付浮窗)

    • 該進程擁有綁定到可見或前臺activity的service

  • PERCEPTIBLE_APP_ADJ:可感知進程

    • 播放音樂進程

    • 計步進程

  • BACKUP_APP_ADJ:備份進程

    • 做備份操作的進程

  • CACHED_APP_MIN_ADJ:不可見進程最小adj值

  • CACHED_APP_MAX_ADJ:不可見進程最大adj值

這6個adj值被寫入到了/sys/module/lowmemorykiller/parameters/adj里(root手機可以查看),android 7.0之前這些值都是各位數的,android 7.0之后,這些值都被重新賦值了,adj值越大優先級越低。一般以0作為系統進程和應用進程的分界線,小于0的是系統進程,LMK一般不會回收。

2、oom_adj值

每個應用進程的adj值是怎么計算和存儲的呢?

核心方法(都在AMS里):

  • updateOomAdjLocked():觸發更新adj

  • computeOomAdjLocked():計算當前進程的adj值

  • applyOomAdjLocked():應用adj值

更為詳細的關于何時觸發更新?怎么計算的?請參考文章Android進程調度之adj算法。

總結下來,可以用一張流程圖表示:76421c74-111d-eb11-8da9-e4434bdf6706.png

我們看下oom_adj值和oom_score_adj值。oom_adj是指示出當前進程是屬于哪一類的進程,這個值在computeOomAdjLocked()里會計算出來;oom_score_adj這個是當前進程的所打的一個分數(具體怎么打分的不清楚,但是不同進程同一oom_adj值,oom_score_adj是一樣的)

他們也是寫入到文件里的,具體在/proc//下:

  • /proc//oom_adj

  • /proc//oom_score_adj

如何找到一個APP進程的pid值呢?

adb shell ps | grep 

79421c74-111d-eb11-8da9-e4434bdf6706.png

第二個數字23826就是當前進程的pid值。

3、總結&示例說明

安卓系統每隔一段時間(具體不清楚~)會檢查下當前內存的空閑情況,看看是否存在低于minfree列表中的某個閾值。

我們假設有這么個表格:

oom_adj0100200300900906
oom_score_adj0691171905291000
minfree122881843224576368644300849152

如果剩余內存少于24576(約98M),那么安卓系統就會找出當前在/proc下oom_adj的值大于等于2進程,按照oom_adj的值大小逆序排列;然后LMK會從最大的oom_adj的值的進程開始kill,釋放他們的內存,直到內存的閾值超過了24576;如果有oom_adj的值相等的進程,則優先kill占用內存大的進程。

整個過程描述:

7b421c74-111d-eb11-8da9-e4434bdf6706.png

進程與進程優先級

在Android中,應用進程劃分5級:

  • 前臺進程(Foreground process)

  • 可見進程(Visible process)

  • 服務進程(Service process)

  • 后臺進程(Background process)

  • 空進程(Empty process)

    • 不含任何活動應用組件的進程。保留這種進程的的唯一目的是用作緩存,以縮短下次在其中運行組件所需的啟動時間,這就是所謂熱啟動。為了使系統資源在進程緩存和底層內核緩存之間保持平衡,系統往往會終止這些進程。

根據進程中當前活動組件的重要程度,Android會對進程的優先級進行評定,這個可以在ProcessList.java里可以看到,具體為(基于android 8.0):

adjadj值解釋
UNKNOWN_ADJ1001無法確定的進程,一般發生在緩存的時候
CACHED_APP_MAX_ADJ906空進程,最大adj值
CACHED_APP_MIN_ADJ900空進程,最小adj值
SERVICE_B_ADJ800不活躍進程
PREVIOUS_APP_ADJ700上一次交互的進程,比如對個應用切換
HOME_APP_ADJ600Launcher進程,盡量不要kill它
SERVICE_ADJJ500程序里的service進程
HEAVY_WEIGHT_APP_ADJ400重型進程,一般運行在后臺,盡量不要kill它
BACKUP_APP_ADJ300備份進程
PERCEPTIBLE_APP_ADJ200可感知進程
VISIBLE_APP_ADJ100可見進程
FOREGROUND_APP_ADJ0前臺進程
PERSISTENT_SERVICE_ADJ-700系統進程,或持續存在進程附著的服務進程
PERSISTENT_PROC_ADJ-800系統持續性進程,如電話
SYSTEM_ADJ-900system進程
NATIVE_ADJ-1000native進程,不屬于JVM內存處理范圍

如何讓APP活的久一點?

讓app活的久一點,可以從兩個方面來優化:

  1. 盡量保證不被系統殺死

  2. 能夠自我復活

如何保證不被殺?

核心就是提高adj的值,讓系統覺得不能殺。最厲害的就是將自己的應用弄成系統應用,但是這個不在技術討論的范疇。

1、進程拆分

內存的大小也是系統殺死進程的一個考量,所以通過進程拆分來來減少整個app的內存大小。如webview單進程,push模塊單進程。

2、onTrimeMemory的回調

OnTrimMemory()也是從內存的角度來保活的方案,通過對自我的瘦身來降低內存,降低被后臺殺死的風險。我們知道,OnTrimMemory()可以做到不同級別額裁剪,這個就給上層更合理的去做裁剪。

3、開啟前臺Service

前臺service可以提高應用的adj值,降低被系統回收的概率。

操作就是:在應用啟動的時候,啟動service,并通過使用startForeground()將當前service設置為前臺service來提高優先級。這里需要注意android系統的不同版本對于開啟前臺服務的控制:

  • API < 18:直接startForeground()即可

  • API >= 18:startForeground()必須給用戶一個可見的notification

  • API >= 26:在Android8.0之后, google對后臺啟動service進行了更加嚴格的限制,但是還是可以通過ContextCompat.startForegroundService()創建前臺服務

對于會出現可見notification的情況,可以專門開啟一個服務去關閉它。

示例:

public class CancelNoticeService extends Service {
private Handler handler;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
handler = new Handler();
Notification.Builder builder = new Notification.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
startForeground(ForegroundService.NOTICE_ID,builder.build());
handler.postDelayed(new Runnable() {
@Override
public void run() {
stopForeground(true);
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.cancel(ForegroundService.NOTICE_ID);
stopSelf();
}
}, 300);
}
}

4、1像素Activity

這個主要是在鎖屏后的保活,通過在鎖屏后(app已經被退到后臺)打開一個1像素的Activity。

public class SinglePixelActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window mWindow = getWindow();
mWindow.setGravity(Gravity.LEFT | Gravity.TOP);
WindowManager.LayoutParams params = mWindow.getAttributes();
params.x = 0;
params.y = 0;
params.height = 1;
params.width = 1;
mWindow.setAttributes(params);
SinglePixelManager.instance().setSingleActivity(this);
}
}

這里是不是1像素不重要,因為屏幕開啟后,會關閉這個Activity。并且不能一直讓這個1像素一直占住屏幕,因為會導致Launcher無法點擊。

5、循環播放無聲音頻

這個就是比較極端的一種方式了,可能會帶來耗電方面的損耗。同時,在某些手機上,用戶是知道你在播放的,如下圖

7f421c74-111d-eb11-8da9-e4434bdf6706.png

可以看到播放的波浪,這個效果很好,在某些手機上連一鍵清理都無法清理掉,但是在產品中使用還是得慎重。

如何復活?

防止app不會系統回收可以做的方案比較少,而且隨著安卓系統的升級,對這方面的控制越來越嚴格。

那么我們還可以從復活的角度來思考app存活的問題。隨著系統的升級,復活的可能性也是越來越低,下面大致說一下可以嘗試的方式(其實很多也沒啥卵用~):

1、START_STICKY

如果service進程被kill掉,保留service的狀態為開始狀態,但不保留啟動時候傳來的intent對象。隨后系統會嘗試重新創建service,由于服務狀態為開始狀態,所以創建服務后一定會調用onStartCommand(Intent,int,int)方法。如果在此期間沒有任何啟動命令被傳遞到service,那么參數Intent將為null。

這個主要是針對系統資源不足而導致的服務被關閉。其他情況下的app殺死是沒啥效果的。

2、JobScheduler

JobScheduler是Android 5.0引入的允許在將來的某個時刻在達到預先定義的條件的情況下執行指定的任務的API。通常情況下,即使APP被系統停止,預定的任務仍然會被執行。

JobScheduler工作原理:

首先在一個實現了JobService的子類的onStartJob方法中執行這項任務,使用JobInfo的Builder方法來設定條件并和實現了JobService的子類的組件名綁定,然后調用系統服務JobScheduler的schedule方法。這樣,即便在執行任務之前應用程序進程被殺,也不會導致任務不會執行,因為系統服務JobScheduler會使用bindServiceAsUser的方法把實現了JobService的子類服務啟動起來,并執行它的onStartJob方法。

JobScheduler只有在5.0以上才能使用,對于5.0以下的怎么辦呢?可以參考https://github.com/evant/JobSchedulerCompat?(這個項目作者已經很久不維護了,但是可以在它的基礎上去做完善和修改,同時也可以作為我們了解安卓CS架構的一個好的實例)

3、安卓賬號自同步

利用Android系統提供的賬號和同步機制實現。安卓會定期喚醒賬戶更新服務,我們可以自己設定同步的事件間隔,且發起更新的是系統,不會受到任何限制。

關于安卓賬號詳細介紹請移步:https://developer.android.com/training/sync-adapters/

該方法局限性還是很大的,用戶會發現莫名出現一個賬戶,并且同步是必須聯網的。

4、網絡連接保活

  • GCM

  • 公共的第三方push通道(信鴿等)

  • 自身跟服務器通過輪詢,或者長連接

更多的細節可以參考微信團隊分享的網絡保活的方案http://www.52im.net/thread-209-1-1.html。

5、系統廣播

這個意義也不太大了。

參考文獻

http://www.52im.net/thread-1135-1-1.html

http://www.52im.net/thread-210-1-1.html

https://github.com/Marswin/MarsDaemon

https://juejin.im/entry/5b22663df265da59bf79f25b/

https://juejin.im/post/58cf80abb123db3f6b45525d

作者簡介:payne, 天天P圖AND工程師


文章后記:

天天P圖是由騰訊公司開發的業內領先的圖像處理,相機美拍的APP。歡迎掃碼或搜索關注我們的微信公眾號:“天天P圖攻城獅”,那上面將陸續公開分享我們的技術實踐,期待一起交流學習!

? ??82421c74-111d-eb11-8da9-e4434bdf6706.jpeg

加入我們
天天P圖技術團隊長期招聘:

(1)?深度學習(圖像處理)研發工程師(上海)

工作職責

  • 開展圖像/視頻的深度學習相關領域研究和開發工作;

  • 負責圖像/視頻深度學習算法方案的設計與實現;

  • 支持社交平臺部產品前沿深度學習相關研究。

工作要求

  • 計算機等相關專業碩士及以上學歷,計算機視覺等方向優先;

  • 掌握主流計算機視覺和機器學習/深度學習等相關知識,有相關的研究經歷或開發經驗;

  • 具有較強的編程能力,熟悉C/C++、python;

  • 在人臉識別,背景分割,體態跟蹤等技術方向上有研究經歷者優先,熟悉主流和前沿的技術方案優先;

  • 寬泛的技術視野,創造性思維,富有想象力;

  • 思維活躍,能快速學習新知識,對技術研發富有激情。

(2) AND / iOS 開發工程師?

(3) 圖像處理算法工程師?

期待對我們感興趣或者有推薦的技術牛人加入我們(base 上海)!聯系方式:ttpic_dev@qq.com

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/259192.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/259192.shtml
英文地址,請注明出處:http://en.pswp.cn/news/259192.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++11系列學習之六-----for

前言C11這次的更新帶來了令很多C程序員期待已久的for range循環&#xff0c;每次看到javascript&#xff0c; lua里的for range&#xff0c;心想要是C能有多好&#xff0c;心里別提多酸了。這次C11不負眾望&#xff0c;再也不用羨慕別家人的for range了。使用場景ex1&#xff1…

ArcGIS Engine 10開發環境的一些常見問題(轉載)

轉自&#xff1a;http://bbs.esrichina-bj.cn/ESRI/viewthread.php?tid107612&extra&page1 許多版友在剛剛使用ArcGIS 10做開發的時候&#xff0c;都會遇到這樣那樣的問題。在擔任實習版主的這一個多月里&#xff0c;看到了這么幾個與開發環境相關的問題&#xff0c;重…

@value 靜態變量_面試官:為什么靜態方法不能調用非靜態方法和變量?

這個可能很多人之前學習jvm的時候都會遇到&#xff0c;屬于一個小問題&#xff0c;寫這篇文章的原因是我在看java相關的面試題目中遇到的&#xff0c;因此順手總結一下&#xff1a;一、例子我們先看效果&#xff1a;我們在靜態方法main中調用非靜態變量或者是方法都會報錯。我們…

SpringMVC連接多數據源配置

在spring-config-datasource.xml中配置&#xff1a; <ds:ibatis-config><ds:sql-map-clientid"sqlMapClient2"datasource-ref"riskBasicDataSource2"config-location"classpath:sqlmap-config.xml"/> </ds:ibatis-config> <…

Memcached 工作原理

http://hzp.iteye.com/blog/1872664Memcached處理的原子是每一個&#xff08;key&#xff0c;value&#xff09;對&#xff08;以下簡稱kv對&#xff09;&#xff0c;key會通過一個hash算法轉化成hash-key&#xff0c;便于查找、對比以及做到盡可能的散列。同時&#xff0c;mem…

C++11系列學習之七---------初始化列表

一、前言C的學習中&#xff0c;我想每個人都被變量定義和申明折磨過&#xff0c;比如我在大學筆試過的幾家公司&#xff0c;都考察了const和變量&#xff0c;類型的不同排列組合&#xff0c;讓你區別有啥不同。反正在學習C過程中已經被折磨慣了&#xff0c;今天再來看看重溫下那…

c# streamReader轉XmlDocument讀取節點

http獲得web&#xff08;url&#xff09;請求&#xff0c;先是獲得數據流streamreader&#xff0c;之后將String數據流轉換為xmldocument&#xff0c;之后xmlnode讀取節點。 // get the responseWebResponse webResponse webRequest.GetResponse();if (webResponse null){ re…

ad中電容用什么封裝_用什么來降低噪聲?只要幾個電容器就可以,簡單有效!...

使用電容器降低噪聲噪聲分很多種&#xff0c;性質也是多種多樣的。所以&#xff0c;噪聲對策(即降低噪聲的方法)也多種多樣。在這里主要談開關電源相關的噪聲&#xff0c;因此&#xff0c;請理解為DC電壓中電壓電平較低、頻率較高的噪聲。另外&#xff0c;除電容外&#xff0c;…

C#委托的介紹(delegate、Action、Func、predicate)

委托是一個類&#xff0c;它定義了方法的類型&#xff0c;使得可以將方法當作另一個方法的參數來進行傳遞。事件是一種特殊的委托。 1.委托的聲明 (1). delegate delegate我們常用到的一種聲明 Delegate至少0個參數&#xff0c;至多32個參數&#xff0c;可以無返回值&#xff0…

版本1.8.1Go安裝以及語法高亮配置

注意點&#xff1a;普通用戶和root用戶高亮要設置兩遍①下載go安裝包 https://golang.org/doc/ 最新的版本&#xff1a;go1.8.1.linux-amd64.tar.gz ②進入主目錄&#xff1a;$:su ~賦給普通用戶root權限&#xff0c;以便執行tar命令&#xff1a;$:su root 把壓縮包解壓到/usr/…

求二叉樹中節點的最大距離

struct node{ Node Left; Node Right; int MaxLeft;//左子樹到該節點的最長距離 int MaxRight;//右子樹到該節點的最長距離 char chValue; }; void FindMaxLen(Node T) { int tmpMax 0; if (NULL T) { return; } if (NULL T->Left) { T->MaxLeft 0; } if (NULL T-&g…

flutter 自定義鍵盤_入門級機械鍵盤選購對比

個人覺得鍵盤這種東西&#xff0c;手感是最重要的&#xff0c;畢竟鍵盤是要拿用的&#xff0c;不是拿來供的。不管鍵盤再怎么好看、酷炫&#xff0c;只要你用起來不舒服、不習慣&#xff0c;那對你而言&#xff0c;就不會是一把好鍵盤。那么&#xff0c;影響手感的因素主要有哪…

騰訊2016校招試題----------格雷碼的實現

問題&#xff1a;產生n位元的所有格雷碼。格雷碼(Gray Code)是一個數列集合&#xff0c;每個數使用二進位來表示&#xff0c;假設使用n位元來表示每個數字&#xff0c;任兩個數之間只有一個位元值不同。例如以下為3位元的格雷碼&#xff1a; 000 001 011 010 110 111 101 100 。…

關于A/D方面的小結

&#xff08;轉載&#xff09;AD精度與分辨率 最近做了一塊板子&#xff0c;當然考慮到元器件的選型了&#xff0c;由于指標中要求精度比較高&#xff0c;所以對于AD的選型很慎重。 很多人對于精度和分辨率的概念不清楚&#xff0c;這里我做一下總結&#xff0c;希望大家不要…

常用表的字段

F:\study\表的設計 一&#xff1a;網站設置有哪些內容&#xff1a; 1>title 表題 2>logo 3>keyword 關鍵字 4>status 是否開啟 5>Internet 備案號 6>url 網址 7>tel 聯系電話 8>brief …

四個好看的CSS樣式表格

1. 單像素邊框CSS表格 這是一個非經常常使用的表格樣式。 源碼&#xff1a; <!-- CSS goes in the document HEAD or added to your external stylesheet --> <style type"text/css"> table.gridtable { font-family: verdana,arial,sans-serif; font-si…

C# COM ArcgisEngine 多線程相關

這段時間做ArcgisEngine&#xff0c;因為在做圖形交叉分析時&#xff0c;計算數據分多個線程分別計算不同的圖形&#xff0c;發現計算錯誤。后來初步了接了是由于所有的ArcObjects組件都被標記為單線程單元&#xff08;STA參考VS幫助文檔&#xff09;。每個STA都限制在一個線程…

loading initial ramdisk 卡住_驛站晨讀 | 一城市多家快遞“卡住了”!有快遞網點直接建議:換別家吧......

編輯&#xff1a;驛站老鬼 主播&#xff1a;若晨?▎美團回應“外賣小哥致電取餐被打成顱腦損傷”10月15日晚&#xff0c;成都溫江區某小區內發生一起顧客毆打外賣員事件&#xff0c;導致外賣員馮某東輕度顱腦損傷以及右膝外側半月板撕裂。據了解&#xff0c;事件起因是顧客要…

CVTE2016校招試題摘選

今年的題分兩部分&#xff0c;時間為晚上7:00-9:30,題目分不定項選擇與兩道編程題。 下面是我自己抄下來的一部分題&#xff0c;盡饗讀者。 1.堆排序屬于下面哪種排序方法&#xff1f; A、選擇排序 B、插入排序、C、交換排序 D、歸并排序 答案&#xff1a; A 2. 用RSA算法…

高手的經驗 硬件

一個硬件高手的設計經驗分享(ZT)大字體 樓主 一&#xff1a;成本節約現象一&#xff1a;這些拉高/拉低的電阻用多大的阻值關系不大&#xff0c;就選個整數5K吧點評&#xff1a;市場上不存在5K的阻值&#xff0c;最接近的是 4.99K&#xff08;精度1%&#xff09;&#xff0c;其…