編輯:關於Android編程
1 概述
在Android中,我們常常使用PendingIntent來表達一種“留待日後處理”的意思。從這個角度來說,PendingIntent可以被理解為一種特殊的異步處理機制。不過,單就命名而言,PendingIntent其實具有一定誤導性,因為它既不繼承於Intent,也不包含Intent,它的核心可以粗略地匯總成四個字——“異步激發”。 很明顯,這種異步激發常常是要跨進程執行的。比如說A進程作為發起端,它可以從系統“獲取”一個PendingIntent,然後A進程可以將PendingIntent對象通過binder機制“傳遞”給B進程,再由B進程在未來某個合適時機,“回調”PendingIntent對象的send()動作,完成激發。 在Android系統中,最適合做集中性管理的組件就是AMS(Activity Manager Service)啦,所以它義不容辭地承擔起管理所有PendingIntent的職責。這樣我們就可以畫出如下示意圖: ![ 如圖](http://img.blog.csdn.net/20160906143509312) 注意其中的第4步“遞送相應的intent”。這一步遞送的intent是從何而來的呢?簡單地說,當發起端獲取PendingIntent時,其實是需要同時提供若干intent的。這些intent和PendingIntent只是配套的關系,而不是聚合的關系,它們會被緩存在AMS中。日後,一旦處理端將PendingIntent的“激發”語義傳遞到AMS,AMS就會嘗試找到與這個PendingIntent對應的若干intent,並遞送出去。 當然,以上說的只是大概情況,實際的技術細節會更復雜一點兒。下面我們就來談談細節。
2 PendingIntent的技術細節
2.1 發起端獲取PendingIntent
我們先要理解,所謂的“發起端獲取PendingIntent”到底指的是什麼。難道只是簡單new一個PendingIntent對象嗎?當然不是。此處的“獲取”動作其實還含有向AMS“注冊”intent的語義。 在PendingIntent.java文件中,我們可以看到有如下幾個比較常見的靜態函數:
public static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getService(Context context, int requestCode, Intent intent, int flags)
public static PendingIntent getActivities(Context context, int requestCode, Intent[] intents, int flags)
public static PendingIntent getActivities(Context context, int requestCode, Intent[] intents, int flags, Bundle options)
它們就是我們常用的獲取PendingIntent的動作了。
坦白說,這幾個函數的命名可真不怎麼樣,所以我們簡單解釋一下。上面的getActivity()的意思其實是,獲取一個PendingIntent對象,而且該對象日後激發時所做的事情是啟動一個新activity。也就是說,當它異步激發時,會執行類似Context.startActivity()那樣的動作。相應地,getBroadcast()和getService()所獲取的PendingIntent對象在激發時,會分別執行類似Context..sendBroadcast()和Context.startService()這樣的動作。至於最後兩個getActivities(),用得比較少,激發時可以啟動幾個activity。
我們以getActivity()的代碼來說明問題:
public static PendingIntent getActivity(Context context, int requestCode,
Intent intent, int flags, Bundle options)
{
String packageName = context.getPackageName();
String resolvedType = intent != null ?
intent.resolveTypeIfNeeded(context.getContentResolver()) : null;
try
{
intent.setAllowFds(false);
IIntentSender target = ActivityManagerNative.getDefault().getIntentSender(
ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
null, null, requestCode, new Intent[] { intent },
resolvedType != null ? new String[] { resolvedType } : null,
flags, options);
return target != null ? new PendingIntent(target) : null;
}
catch (RemoteException e)
{}
return null;
}
其中那句new PendingIntent(target)創建了PendingIntent對象,其重要性自不待言。然而,這個對象的內部核心其實是由上面那個getIntentSender()函數得來的。而這個IIntentSender核心才是我們真正需要關心的東西。說穿了,此處的IIntentSender對象是個binder代理,它對應的binder實體是AMS中的PendingIntentRecord對象。PendingIntent對象構造之時,IIntentSender代理作為參數傳進來,並記錄在PendingIntent的mTarget域。日後,當PendingIntent執行異步激發時,其內部就是靠這個mTarget域向AMS傳遞語義的。 我們前文說過,PendingIntent常常會經由binder機制,傳遞到另一個進程去。而binder機制可以保證,目標進程得到的PendingIntent的mTarget域也是合法的IIntentSender代理,而且和發起端的IIntentSender代理對應著同一個PendingIntentRecord實體。示意圖如下:
2.2 AMS裡的PendingIntentRecord
那麼PendingIntentRecord裡又有什麼信息呢?它的定義截選如下:
class PendingIntentRecord extends IIntentSender.Stub
{
final ActivityManagerService owner;
final Key key; // 最關鍵的key域
final int uid;
final WeakReference ref;
boolean sent = false;
boolean canceled = false;
String stringName;
. . . . . .
}
請注意其中那個key域。這裡的Key是個PendingIntentRecord的內嵌類,其定義截選如下:
final static class Key
{
final int type;
final String packageName;
final ActivityRecord activity;
final String who;
final int requestCode;
final Intent requestIntent; // 注意!
final String requestResolvedType;
final Bundle options;
Intent[] allIntents; // 注意!記錄了當初獲取PendingIntent時,用戶所指定的所有intent
String[] allResolvedTypes;
final int flags;
final int hashCode;
. . . . . .
. . . . . .
}
請注意其中的allIntents[]數組域以及requestIntent域。前者記錄了當初獲取PendingIntent時,用戶所指定的所有intent(雖然一般情況下只會指定一個intent,但類似getActivities()這樣的函數還是可以指定多個intent的),而後者可以粗淺地理解為用戶所指定的那個intent數組中的最後一個intent。現在大家應該清楚異步激發時用到的intent都存在哪裡了吧。
Key的構造函數截選如下:
Key(int _t, String _p, ActivityRecord _a, String _w,
int _r, Intent[] _i, String[] _it, int _f, Bundle _o)
{
type = _t;
packageName = _p;
activity = _a;
who = _w;
requestCode = _r;
requestIntent = _i != null ? _i[_i.length-1] : null; // intent數組中的最後一個
requestResolvedType = _it != null ? _it[_it.length-1] : null;
allIntents = _i; // 所有intent
allResolvedTypes = _it;
flags = _f;
options = _o;
. . . . . .
}
Key不光承擔著記錄信息的作用,它還承擔“鍵值”的作用。
2.3 AMS中的PendingIntentRecord總表
在AMS中,管理著系統中所有的PendingIntentRecord節點,所以需要把這些節點組織成一張表:
final HashMap> mIntentSenderRecords
這張哈希映射表的鍵值類型就是剛才所說的PendingIntentRecord.Key。
以後每當我們要獲取PendingIntent對象時,PendingIntent裡的mTarget是這樣得到的:AMS會先查mIntentSenderRecords表,如果能找到符合的PendingIntentRecord節點,則返回之。如果找不到,就創建一個新的PendingIntentRecord節點。因為PendingIntentRecord是個binder實體,所以經過binder機制傳遞後,客戶進程拿到的就是個合法的binder代理。如此一來,前文的示意圖可以進一步修改成下圖:
2.4 AMS裡的getIntentSender()函數
現在,我們回過頭繼續說前文的getActivity(),以及其調用的getIntentSender()。我們先列一遍getActivity()的原型:
public static PendingIntent getActivity(Context context, int requestCode,
Intent intent, int flags, Bundle options)
context參數是調用方的上下文。
requestCode是個簡單的整數,起區分作用。
intent是異步激發時將發出的intent。
flags可以包含一些既有的標識,比如FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT等等。不少同學對這個域不是很清楚,我們後文會細說。
options可以攜帶一些額外的數據。
getActivity()的代碼很簡單,其參數基本上都傳給了getIntentSender()。
IIntentSender target = ActivityManagerNative.getDefault().getIntentSender(. . . . . .)
getIntentSender()的原型大體是這樣的:
public IIntentSender getIntentSender(int type,
String packageName, IBinder token, String resultWho,
int requestCode, Intent[] intents, String[] resolvedTypes,
int flags, Bundle options) throws RemoteException;
其參數比getActivity()要多一些,我們逐個說明。
type參數表明PendingIntent的類型。getActivity()和getActivities()動作裡指定的類型值是INTENT_SENDER_ACTIVITY,getBroadcast()和getService()和動作裡指定的類型值分別是INTENT_SENDER_BROADCAST和INTENT_SENDER_SERVICE。另外,在Activity.java文件中,我們還看到一個createPendingResult()函數,這個函數表達了發起方的activity日後希望得到result回饋的意思,所以其內部調用getIntentSender()時指定的類型值為INTENT_SENDER_ACTIVITY_RESULT。
packageName參數表示發起端所屬的包名。
token參數是個指代回饋目標方的代理。這是什麼意思呢?我們常用的getActivity()、getBroadcast()和getService()中,只是把這個參數簡單地指定為null,表示這個PendingIntent激發時,是不需要發回什麼回饋的。不過當我們希望獲取類型為INTENT_SENDER_ACTIVITY_RESULT的PendingIntent時,就需要指定token參數了。具體可參考createPendingResult()的代碼:
public PendingIntent createPendingResult(int requestCode, Intent data, int flags)
{
String packageName = getPackageName();
try
{
data.setAllowFds(false);
IIntentSender target = ActivityManagerNative.getDefault().getIntentSender(
ActivityManager.INTENT_SENDER_ACTIVITY_RESULT,
packageName,
mParent == null ? mToken : mParent.mToken,
mEmbeddedID, requestCode, new Intent[] { data },
null, flags, null);
return target != null ? new PendingIntent(target) : null;
} catch (RemoteException e) {
// Empty
}
return null;
}
看到了嗎?傳入的token為Activity的mToken或者其mParent.mToken。說得簡單點兒,AMS內部可以根據這個token找到其對應的ActivityRecord,日後當PendingIntent激發時,AMS可以根據這個ActivityRecord確定出該向哪個目標進程的哪個Activity發出result語義。
resultWho參數和token參數息息相關,一般也是null啦。在createPendingResult()中,其值為Activity的mEmbeddedID字符串。
requestCode參數是個簡單的整數,可以在獲取PendingIntent時由用戶指定,它可以起區分的作用。
intents數組參數是異步激發時希望發出的intent。對於getActivity()、getBroadcast()和getService()來說,都只會指定一個intent而已。只有getActivities()會嘗試一次傳入若干intent。
resolvedTypes參數基本上和intent是相關的。一般是這樣得到的:
String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
context.getContentResolver()) : null;
這個值常常和intent內部的mData URI有關系,比如最終的值可能是URI對應的MIME類型。
flags參數可以指定PendingIntent的一些行為特點。它的取值是一些既有的比特標識的組合。目前可用的標識有:FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT、FLAG_UPDATE_CURRENT等等。有時候,flags中還可以附帶若干FILL_IN_XXX標識。我們把常見的標識定義列舉如下:
【PendingIntent中】
public static final int FLAG_ONE_SHOT = 1<<30;
public static final int FLAG_NO_CREATE = 1<<29;
public static final int FLAG_CANCEL_CURRENT = 1<<28;
public static final int FLAG_UPDATE_CURRENT = 1<<27;
【Intent中】
public static final int FILL_IN_ACTION = 1<<0;
public static final int FILL_IN_DATA = 1<<1;
public static final int FILL_IN_CATEGORIES = 1<<2;
public static final int FILL_IN_COMPONENT = 1<<3;
public static final int FILL_IN_PACKAGE = 1<<4;
public static final int FILL_IN_SOURCE_BOUNDS = 1<<5;
public static final int FILL_IN_SELECTOR = 1<<6;
public static final int FILL_IN_CLIP_DATA = 1<<7;
這些以FILL_IN_打頭的標志位,主要是在intent對象的fillIn()函數裡起作用:
public int fillIn(Intent other, int flags)
我們以FILL_IN_ACTION為例來說明,當我們執行類似srcIntent.fillIn(otherIntent, …)的句子時,如果otherIntent的mAction域不是null值,那麼fillIn()會在以下兩種情況下,用otherIntent的mAction域值為srcIntent的mAction域賦值:
1) 當srcIntent的mAction域值為null時;
2) 如果fillIn的flags參數裡攜帶了FILL_IN_ACTION標志位,那麼即便srcIntent的mAction已經有值了,此時也會用otherIntent的mAction域值強行替換掉srcIntent的mAction域值。
其他FILL_IN_標志位和FILL_IN_ACTION的處理方式類似,我們不再贅述。
options參數可以攜帶一些額外數據。
2.4.1 getIntentSender()函數
getIntentSender()函數摘錄如下:
public IIntentSender getIntentSender(int type, String packageName,
IBinder token, String resultWho,
int requestCode, Intent[] intents,
String[] resolvedTypes,
int flags, Bundle options)
{
. . . . . .
// 先判斷intents數組,可以用偽代碼checkIntents(intents)來表示
// checkIntents(intents);
. . . . . .
int callingUid = Binder.getCallingUid();
. . . . . .
if (callingUid != 0 && callingUid != Process.SYSTEM_UID)
{
int uid = AppGlobals.getPackageManager().getPackageUid(packageName,
UserId.getUserId(callingUid));
if (!UserId.isSameApp(callingUid, uid))
{
. . . . . .
throw new SecurityException(msg);
}
}
. . . . . .
return getIntentSenderLocked(type, packageName, Binder.getOrigCallingUid(),
token, resultWho, requestCode, intents, resolvedTypes, flags, options);
. . . . . .
}
getIntentSender()函數中有一段逐條判斷intents[]的代碼,我用偽代碼checkIntents(intents)來表示,這部分對應的實際代碼如下:
for (int i=0; i這段代碼說明在獲取PendingIntent對象時,intent中是不能攜帶文件描述符的。而且如果這個PendingIntent是那種要發出廣播的PendingIntent,那麼intent中也不能攜帶FLAG_RECEIVER_BOOT_UPGRADE標識符。“BOOT_UPGRADE”應該是“啟動並升級”的意思,它不能使用PendingIntent。 getIntentSender()中最核心的一句應該是調用getIntentSenderLocked()的那句。
2.4.2 getIntentSenderLocked()函數 getIntentSenderLocked()的代碼截選如下:
【frameworks/base/services/java/com/android/server/am/ActivityManagerService.java】IIntentSender getIntentSenderLocked(int type, String packageName, int callingUid,
IBinder token, String resultWho,
int requestCode, Intent[] intents,
String[] resolvedTypes, int flags,
Bundle options)
{
. . . . . .
// 如果是INTENT_SENDER_ACTIVITY_RESULT類型,那麼要判斷token所
// 代表的activity是否還在activity棧中
. . . . . .
// 整理flags中的信息
. . . . . .
PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName,
activity, resultWho,
requestCode, intents,
resolvedTypes, flags, options);
// 盡力從哈希映射表中查找key對應的PendingIntentRecord,如果找不到就創建一個新的節點。
WeakReference ref;
ref = mIntentSenderRecords.get(key);
PendingIntentRecord rec = ref != null ? ref.get() : null;
if (rec != null)
{
// 找到了匹配的PendingIntent,現在考慮要不要更新它,或者取消它。
if (!cancelCurrent)
{
if (updateCurrent)
{
// 如果明確指定了FLAG_UPDATE_CURRENT,那麼更新找到的節點
if (rec.key.requestIntent != null) {
rec.key.requestIntent.replaceExtras(intents != null ?
intents[intents.length - 1] : null);
}
if (intents != null) {
intents[intents.length-1] = rec.key.requestIntent;
rec.key.allIntents = intents;
rec.key.allResolvedTypes = resolvedTypes;
} else {
rec.key.allIntents = null;
rec.key.allResolvedTypes = null;
}
}
// 凡是能找到對應的節點,而且又不取消該節點的,那麼就return這個節點
return rec;
}
// 如果PendingIntent的標志中帶有FLAG_CANCEL_CURRENT,則從哈希映射表中刪除之
rec.canceled = true;
mIntentSenderRecords.remove(key);
}
if (noCreate)
{
// 如果明確表示了不創建新節點,也就是說標志中帶有FLAG_NO_CREATE,
// 那麼不管是不是Cancel了PendingIntent,此時一概直接返回。
return rec;
}
// 從哈希映射表中找不到,而且又沒有寫明FLAG_NO_CREATE,此時創建一個新節點
rec = new PendingIntentRecord(this, key, callingUid);
mIntentSenderRecords.put(key, rec.ref);
if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT)
{
// 如果intent需要返回結果,那麼修改token對應的ActivityRecord
// 的pendingResults域。
if (activity.pendingResults == null)
{
activity.pendingResults = new HashSet>();
}
activity.pendingResults.add(rec.ref);
}
return rec;
} 上面這段代碼主要做的事情有:
1) 將傳進來的多個參數信息整理成一個PendingIntentRecord.Key對象(key);2) 嘗試從mIntentSenderRecords總表中查找和key相符的PendingIntentRecord節點;3) 根據flags參數所含有的意義,對得到的PendingIntentRecord進行加工。有時候修改之,有時候刪除之。4) 如果在總表中沒有找到對應的PendingIntentRecord節點,或者根據flags的語義刪除了剛找到的節點,那麼此時的默認行為是創建一個新的PendingIntentRecord節點,並插入總表。除非flags中明確指定了FLAG_NO_CREATE,此時不會創建新節點。2.4.3 說說flags 從getIntentSenderLocked()的代碼中,我們終於搞明白了flags中那些特定比特值的意義了。我們現在總結一下。
應該說這些flags比特值基本上都是在圍繞著mIntentSenderRecords總表說事的。其中,FLAG_CANCEL_CURRENT的意思是,當我們獲取PendingIntent時,如果可以從總表中查到一個相符的已存在的PendingIntentRecord節點的話,那麼需要把這個節點從總表中清理出去。而在沒有指定FLAG_CANCEL_CURRENT的大前提下,如果用戶指定了FLAG_UPDATE_CURRENT標識,那麼會用新的intents參數替掉剛查到的PendingIntentRecord中的舊intents。
而不管是剛清理了已存在的PendingIntentRecord,還是壓根兒就沒有找到符合的PendingIntentRecord,只要用戶沒有明確指定FLAG_NO_CREATE標識,系統就會盡力創建一個新的PendingIntentRecord節點,並插入總表。
至於FLAG_ONE_SHOT標識嘛,它並沒有在getIntentSenderLocked()中露臉兒。它的名字是“FLAG_ONE_SHOT”,也就是“只打一槍”的意思,那麼很明顯,這個標識起作用的地方應該是在“激發”函數裡。在最終的激發函數(sendInner())裡,我們可以看到下面的代碼:
【frameworks/base/services/java/com/android/server/am/PendingIntentRecord.java】int sendInner(int code, Intent intent, String resolvedType,
IIntentReceiver finishedReceiver, String requiredPermission,
IBinder resultTo, String resultWho, int requestCode,
int flagsMask, int flagsValues, Bundle options)
{
synchronized(owner) {
if (!canceled)
{
sent = true;
if ((key.flags & PendingIntent.FLAG_ONE_SHOT) != 0) {
owner.cancelIntentSenderLocked(this, true);
canceled = true;
}
. . . . . .
. . . . . .
}
}
return ActivityManager.START_CANCELED;
}意思很簡單,一進行激發就把相應的PendingIntentRecord節點從總表中清理出去,而且把PendingIntentRecord的canceled域設為true。這樣,以後即便外界再調用send()動作都沒用了,因為再也無法進入if (!canceled)判斷了。2.4.4 將PendingIntentRecord節點插入總表 接下來getIntentSenderLocked()函數new了一個PendingIntentRecord節點,並將之插入mIntentSenderRecords總表中。
2.5 PendingIntent的激發動作 下面我們來看PendingIntent的激發動作。在前文我們已經說過,當需要激發PendingIntent之時,主要是通過調用PendingIntent的send()函數來完成激發動作的。PendingIntent提供了多個形式的send()函數,然而這些函數的內部其實調用的是同一個send(),其函數原型如下:
public void send(Context context, int code, Intent intent,
OnFinished onFinished, Handler handler, String requiredPermission)
throws CanceledException該函數內部最關鍵的一句是:int res = mTarget.send(code, intent, resolvedType,
onFinished != null ? new FinishedDispatcher(this, onFinished, handler) : null,
requiredPermission);我們前文已經介紹過這個mTarget域了,它對應著AMS中的某個PendingIntentRecord。 所以我們要看一下PendingIntentRecord一側的send()函數,其代碼如下:
public int send(int code, Intent intent, String resolvedType,
IIntentReceiver finishedReceiver, String requiredPermission)
{
return sendInner(code, intent, resolvedType, finishedReceiver,
requiredPermission, null, null, 0, 0, 0, null);
}其中sendInner()才是真正做激發動作的函數。 sendInner()完成的主要邏輯動作有:
1) 如果當前PendingIntentRecord節點已經處於canceled域為true的狀態,那麼說明這個節點已經被取消掉了,此時sendInner()不會做任何實質上的激發動作,只是簡單地return ActivityManager.START_CANCELED而已。2) 如果當初在創建這個節點時,使用者已經指定了FLAG_ONE_SHOT標志位的話,那麼此時sendInner()會把這個PendingIntentRecord節點從AMS中的總表中摘除,並且把canceled域設為true。而後的操作和普通激發時的動作是一致的,也就是說也會走下面的第3)步。3) 關於普通激發時應執行的邏輯動作是,根據當初創建PendingIntentRecord節點時,用戶指定的type類型,進行不同的處理。這個type其實就是我們前文所說的INTENT_SENDER_ACTIVITY、INTENT_SENDER_BROADCAST、INTENT_SENDER_SERVICE等類型啦,大家如有興趣,可自己參考本文一開始所說的getActivity()、getBroadcast()、getService()等函數的實現代碼。 現在還有一個問題是,既然我們在當初獲取PendingIntent時,已經指定了日後激發時需要遞送的intent(或intent數組),那麼為什麼send()動作裡還有一個intent參數呢?它們的關系又是什麼呢?我猜想,PendingIntent機制的設計者是希望給激發端一個修改“待激發的intent”的機會。比如當初我們獲取PendingIntent對象時,如果在flags裡設置了FILL_IN_ACTION標志位,那麼就說明我們允許日後在某個激發點,用新的intent的mAction域值,替換掉我們最初給的intent的mAction域值。如果一開始沒有設置FILL_IN_ACTION標志位,而且在最初的intent裡已經有了非空的mAction域值的話,那麼即使在激發端又傳入了新intent,它也不可能修改用新intent的mAction域值替換舊intent的mAction域值。
細心的讀者一定記得,當初獲取PendingIntent對象時,我們可是向AMS端傳遞了一個intent數組噢,雖然一般情況下這個數組裡只有一個intent元素,但有時候我們也是有可能一次性傳遞多個intent的。比如getActivities()函數就可以一次傳遞多個intent。可是現在激發動作send()卻只能傳遞一個intent參數,這該如何處理呢?答案很簡單,所傳入的intent只能影響已有的intent數組的最後一個intent元素。大家可以看看sendInner裡allIntents[allIntents.length-1] = finalIntent;一句。
Ok,intent說完了,下面就該做具體的激發了。我們以簡單的INTENT_SENDER_BROADCAST型PendingIntentRecord來說明,此時的激發動作就是發送一個廣播:
owner.broadcastIntentInPackage(key.packageName, uid, finalIntent, resolvedType,
finishedReceiver, code, null, null,
requiredPermission, (finishedReceiver != null),
false, UserId.getUserId(uid));至於其他類型的PendingIntentRecord的激發動作,大家可以自行查閱代碼,它們的基本代碼格局都是差不多的。3 小結 本文是基於我早先的一點兒筆記整理而成的。當時為了搞清楚PendingIntent的機理,也查閱了一些網上的相關文章,只是都不大滿足我的要求,後來只好自己看代碼,終於得了些自己的淺見。現在把我過去的一點兒認識整理出來,希望能對學習PendingIntent的同學有點兒幫助。
照例先看效果圖自定義代碼示例public class BezierView extends View { Paint paint;//畫筆 Path path;//路徑
Eclipse是老牌的開發工具,相信早期開發android程序每一個碼農都使用過這個軟件,添加ADT插件之後就能開發android程序了。因為是開源的,所以開發起項目來還
JSON數據是一種輕量級的數據交換格式,在Android中通常應用於客戶端與服務器交互之間的數據傳輸。像現在在網上有很多解析JSON數據的jar包,但是歸根到底用的都是A
在軟件開發的過程中,為了讓軟件在不同的場景下都可以使用,所以機型適配是不可或缺並且非常重要耗時的一個環節。一:機型適配需要考慮的幾個方面:1,Android的版本2.手機
由於android系統中應用程序之間不能共享內存。因此,在不同應用程序之