android Widget添加过程和android添加widget不更新的问题分析解决
第一部分: android widget 添加过程分析
Android中的AppWidget与google widget和中移动的widget并不是一个概念,这里的AppWidget只是把一个进程的控件嵌入到别外一个进程的窗口里的一种方法。View在另 外一个进程里显示,但事件的处理方法还是在原来的进程里。这有点像 X Window中的嵌入式窗口。
首先,我们 需要了解RemoteViews, AppWidgetHost, AppWidgetHostView等概念
RemoteViews:并不是一个真正的View,它没有实现View的接口,而只是一个用于描述View的实体。比如:创建View需要的资源ID和各个控件的事件响应方法。RemoteViews会通过进程间通信机制传递给AppWidgetHost。
AppWidgetHost
AppWidgetHost是真正容纳AppWidget的地方,它的主要功能有两个:
1 . 监听来自AppWidgetService的事件:
class Callbacks extends IAppWidgetHost.Stub{
public void updateAppWidget(int appWidgetId,RemoteViews views) { Message
msg = mHandler.obtainMessage(HANDLE_UPDATE);
msg.arg1 = appWidgetId;
msg.obj = views; msg.sendToTarget();
} //处理update事件,更新widget
public void providerChanged(int appWidgetId,AppWidgetProviderInfo info) {
Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED);
msg.arg1 = appWidgetId;
msg.obj = info;
msg.sendToTarget();
}//处理providerChanged事件,更新widget
}
public UpdateHandler(Looper looper) { super(looper); }
public void handleMessage(Message msg) {
switch (msg.what) {
case HANDLE_UPDATE{
updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj);
break;
}
case HANDLE_PROVIDER_CHANGED{
onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj);
break;
}
}
}
}
2 . 另外一个功能就是创建AppWidgetHostView。
前面我们说过RemoteViews不是真正的View,只是View的描述,而 AppWidgetHostView才是真正的View。这里先创建AppWidgetHostView,然后通过AppWidgetService查询 appWidgetId对应的RemoteViews,最后把RemoteViews传递给AppWidgetHostView去 updateAppWidget。
public final AppWidgetHostView createView(Context context, int appWidgetId, AppWidgetProviderInfo appWidget) {AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
view.setAppWidget(appWidgetId, appWidget);
synchronized (mViews) { mViews.put(appWidgetId, view); }
RemoteViews views = null;
try {
views = sService.getAppWidgetViews(appWidgetId);
} catch(RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
view.updateAppWidget(views);
return view;
}
AppWidgetHost其实是一个容器,在这个容器中可以放置widget,我们平常熟悉的Lanuch 就可以放置widget,所以lanuch应该是继承AppWidgetHost的
public LauncherAppWidgetHost(Context context, int hostId) {
super(context, hostId);
}
@Override
protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
return new LauncherAppWidgetHostView(context);
}
}
AppWidgetHostView
AppWidgetHostView是真正的View,但它只是一个容器,用来容纳实际的AppWidget的View。这个AppWidget的View是根据RemoteViews的描述来创建。这是在updateAppWidget里做的:
public void updateAppWidget(RemoteViews remoteViews){...
if (content == null && layoutId ==mLayoutId) {
try {
remoteViews.reapply(mContext, mView);
content = mView;
recycled = true;
if(LOGD) Log.d(TAG, "was able to recycled existing layout");
} catch (RuntimeException e) {
exception= e;
}
} // Try normal RemoteView inflation
if (content == null) {
try {
content =remoteViews.apply(mContext, this);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch(RuntimeException e) { exception = e; }
}
...
if (!recycled) {
prepareView(content);
addView(content);
}
if (mView != content) {
removeView(mView);
mView = content;
}
...
}
remoteViews.apply创建了实际的View,下面代码可以看出:
public View apply(Context context, ViewGroup parent) {View result = null;
Context c =prepareContext(context);
Resources r = c.getResources();
LayoutInflater inflater =(LayoutInflater) c .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater =inflater.cloneInContext(c);
inflater.setFilter(this);
result = inflater.inflate(mLayoutId,parent, false);
performApply(result);
return result;
}
Host的实现者
AppWidgetHost和AppWidgetHostView是在框架中定义的两个基类。应用程序可以利用这两个类来实现自己的Host。Launcher是缺省的桌面,它是一个Host的实现者。
LauncherAppWidgetHostView扩展了AppWidgetHostView,实现了对长按事件的处理。
LauncherAppWidgetHost扩展了AppWidgetHost,这里只是重载了onCreateView,创建LauncherAppWidgetHostView的实例。
LauncherAppWidgetHostView: 扩展了AppWidgetHostView,实现了对长按事件的处理
LauncherAppWidgetHost: 扩展了AppWidgetHost,这里只是重载了onCreateView,创建LauncherAppWidgetHostView的实例
24 /** 25 * Specific {@link AppWidgetHost} that creates our {@link LauncherAppWidgetHostView} 26 * which correctly captures all long-press events. This ensures that users can 27 * always pick up and move widgets. 28 */ 29 public class LauncherAppWidgetHost extends AppWidgetHost { 30 public LauncherAppWidgetHost(Context context, int hostId) { 31 super(context, hostId); 32 } 33 34 @Override 35 protected AppWidgetHostView onCreateView(Context context, int appWidgetId, 36 AppWidgetProviderInfo appWidget) { 37 return new LauncherAppWidgetHostView(context); 38 } 39 }
首先在Launcher.java中定义了如下两个变量
174 private AppWidgetManager mAppWidgetManager; 175 private LauncherAppWidgetHost mAppWidgetHost;在onCreate函数中初始化,
224 mAppWidgetManager = AppWidgetManager.getInstance(this); 225 mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID); 226 mAppWidgetHost.startListening(); 上述代码,获取mAppWidgetManager的实例,并创建LauncherAppWidgetHost,以及监听 AppWidgetManager只是应用程序与底层Service之间的一个桥梁,是Android中标准的aidl实现方式 应用程序通过AppWidgetManager调用Service中的方法 frameworks/base / core / java / android / appwidget / AppWidgetManager.java 35 /** 36 * Updates AppWidget state; gets information about installed AppWidget providers and other 37 * AppWidget related state. 38 */ 39 public class AppWidgetManager { 197 static WeakHashMap以上代码是设计模式中标准的单例模式
frameworks/base/ core / java / android / appwidget / AppWidgetHost.java
90 public AppWidgetHost(Context context, int hostId) { 91 mContext = context; 92 mHostId = hostId; 93 mHandler = new UpdateHandler(context.getMainLooper()); 94 synchronized (sServiceLock) { 95 if (sService == null) { 96 IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE); 97 sService = IAppWidgetService.Stub.asInterface(b); 98 } 99 } 100 }可以看到AppWidgetHost有自己的HostId,Handler,和sService
93 mHandler = new UpdateHandler(context.getMainLooper());
这是啥用法呢?
参数为Looper,即消息处理放到此Looper的MessageQueue中,有哪些消息呢?
40 static final int HANDLE_UPDATE = 1; 41 static final int HANDLE_PROVIDER_CHANGED = 2; 48 49 class Callbacks extends IAppWidgetHost.Stub { 50 public void updateAppWidget(int appWidgetId, RemoteViews views) { 51 Message msg = mHandler.obtainMessage(HANDLE_UPDATE); 52 msg.arg1 = appWidgetId; 53 msg.obj = views; 54 msg.sendToTarget(); 55 } 56 57 public void providerChanged(int appWidgetId, AppWidgetProviderInfo info) { 58 Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED); 59 msg.arg1 = appWidgetId; 60 msg.obj = info; 61 msg.sendToTarget(); 62 } 63 } 64 65 class UpdateHandler extends Handler { 66 public UpdateHandler(Looper looper) { 67 super(looper); 68 } 69 70 public void handleMessage(Message msg) { 71 switch (msg.what) { 72 case HANDLE_UPDATE: { 73 updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj); 74 break; 75 } 76 case HANDLE_PROVIDER_CHANGED: { 77 onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj); 78 break; 79 } 80 } 81 } 82 }通过以上可以看到主要有两中类型的消息,HANDLE_UPDATE和HANDLE_PROVIDER_CHANGED
处理即通过自身定义的方法
231 /** 232 * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk. 233 */ 234 protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) { 235 AppWidgetHostView v; 236 synchronized (mViews) { 237 v = mViews.get(appWidgetId); 238 } 239 if (v != null) { 240 v.updateAppWidget(null, AppWidgetHostView.UPDATE_FLAGS_RESET); 241 } 242 } 243 244 void updateAppWidgetView(int appWidgetId, RemoteViews views) { 245 AppWidgetHostView v; 246 synchronized (mViews) { 247 v = mViews.get(appWidgetId); 248 } 249 if (v != null) { 250 v.updateAppWidget(views, 0); 251 } 252 }
那么此消息是何时由谁发送的呢?
从以上的代码中看到AppWidgetHost定义了内部类Callback,扩展了类IAppWidgetHost.Stub,类Callback中负责发送以上消息
Launcher中会调用本类中的如下方法,
102 /** 103 * Start receiving onAppWidgetChanged calls for your AppWidgets. Call this when your activity 104 * becomes visible, i.e. from onStart() in your Activity. 105 */ 106 public void startListening() { 107 int[] updatedIds; 108 ArrayList同时发送Intent,其中保存有刚刚分配的appWidgetId,AppWidgetManager.EXTRA_APPWIDGET_ID
139 /** 140 * Get a appWidgetId for a host in the calling process. 141 * 142 * @return a appWidgetId 143 */ 144 public int allocateAppWidgetId() { 145 try { 146 if (mPackageName == null) { 147 mPackageName = mContext.getPackageName(); 148 } 149 return sService.allocateAppWidgetId(mPackageName, mHostId); 150 } 151 catch (RemoteException e) { 152 throw new RuntimeException("system server dead?", e); 153 } 154 } 2016 Intent pickIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK); 2017 pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 2018 // start the pick activity 2019 startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET); 这段代码之后,代码将会怎么执行呢,根据Log信息,可以看到代码将会执行到Setting应用中 packages/apps/Settings/ src / com / android / settings / AppWidgetPickActivity.java 此类将会通过AppWidgetService获取到当前系统已经安装的Widget,并显示出来 78 /** 79 * Create list entries for any custom widgets requested through 80 * {@link AppWidgetManager#EXTRA_CUSTOM_INFO}. 81 */ 82 void putCustomAppWidgets(ListThis may be sent in response to a new instance for this AppWidget provider having 139 * been instantiated, the requested {@link AppWidgetProviderInfo#updatePeriodMillis update interval} 140 * having lapsed, or the system booting. 141 * 142 *
143 * The intent will contain the following extras: 144 *
{@link #EXTRA_APPWIDGET_IDS} | 147 *The appWidgetIds to update. This may be all of the AppWidgets created for this 148 * provider, or just a subset. The system tries to send updates for as few AppWidget 149 * instances as possible. | 150 *
待用户选择完要添加的widget之后,将会回到Launcher.java中的函数onActivityResult中
538 case REQUEST_PICK_APPWIDGET: 539 addAppWidget(data); 540 break;上述addAppWidget中做了哪些事情呢?
1174 void addAppWidget(Intent data) { 1175 // TODO: catch bad widget exception when sent 1176 int appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); 1177 AppWidgetProviderInfo appWidget = mAppWidgetManager.getAppWidgetInfo(appWidgetId); 1178 1179 if (appWidget.configure != null) { 1180 // Launch over to configure widget, if needed 1181 Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); 1182 intent.setComponent(appWidget.configure); 1183 intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 1184 1185 startActivityForResultSafely(intent, REQUEST_CREATE_APPWIDGET); 1186 } else { 1187 // Otherwise just add it 1188 onActivityResult(REQUEST_CREATE_APPWIDGET, Activity.RESULT_OK, data); 1189 } 1190 }首 先获取appWidgetId,再通过AppWidgetManager获取AppWidgetProviderInfo,最后判断此Widget是否存 在ConfigActivity,如果存在则启动ConfigActivity,否则直接调用函数onActivityResult
541 case REQUEST_CREATE_APPWIDGET: 542 completeAddAppWidget(data, mAddItemCellInfo); 543 break;通过函数completeAddAppWidget把此widget的信息插入到数据库中,并添加到桌面上
873 /** 874 * Add a widget to the workspace. 875 * 876 * @param data The intent describing the appWidgetId. 877 * @param cellInfo The position on screen where to create the widget. 878 */ 879 private void completeAddAppWidget(Intent data, CellLayout.CellInfo cellInfo) { 880 Bundle extras = data.getExtras(); 881 int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1); 882 883 if (LOGD) Log.d(TAG, "dumping extras content=" + extras.toString()); 884 885 AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); 886 887 // Calculate the grid spans needed to fit this widget 888 CellLayout layout = (CellLayout) mWorkspace.getChildAt(cellInfo.screen); 889 int[] spans = layout.rectToCell(appWidgetInfo.minWidth, appWidgetInfo.minHeight); 890 891 // Try finding open space on Launcher screen 892 final int[] xy = mCellCoordinates; 893 if (!findSlot(cellInfo, xy, spans[0], spans[1])) { 894 if (appWidgetId != -1) mAppWidgetHost.deleteAppWidgetId(appWidgetId); 895 return; 896 } 897 898 // Build Launcher-specific widget info and save to database 899 LauncherAppWidgetInfo launcherInfo = new LauncherAppWidgetInfo(appWidgetId); 900 launcherInfo.spanX = spans[0]; 901 launcherInfo.spanY = spans[1]; 902 903 LauncherModel.addItemToDatabase(this, launcherInfo, 904 LauncherSettings.Favorites.CONTAINER_DESKTOP, 905 mWorkspace.getCurrentScreen(), xy[0], xy[1], false); 906 907 if (!mRestoring) { 908 mDesktopItems.add(launcherInfo); 909 910 // Perform actual inflation because we're live 911 launcherInfo.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); 912 913 launcherInfo.hostView.setAppWidget(appWidgetId, appWidgetInfo); 914 launcherInfo.hostView.setTag(launcherInfo); 915 916 mWorkspace.addInCurrentScreen(launcherInfo.hostView, xy[0], xy[1], 917 launcherInfo.spanX, launcherInfo.spanY, isWorkspaceLocked()); 918 } 919 }
Launcher中删除widget
长按一个widget,并拖入到DeleteZone中可实现删除
具体代码在DeleteZone中
92 public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, 93 DragView dragView, Object dragInfo) { 94 final ItemInfo item = (ItemInfo) dragInfo; 95 96 if (item.container == -1) return; 97 98 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 99 if (item instanceof LauncherAppWidgetInfo) { 100 mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); 101 } 102 } else { 103 if (source instanceof UserFolder) { 104 final UserFolder userFolder = (UserFolder) source; 105 final UserFolderInfo userFolderInfo = (UserFolderInfo) userFolder.getInfo(); 106 // Item must be a ShortcutInfo otherwise it couldn't have been in the folder 107 // in the first place. 108 userFolderInfo.remove((ShortcutInfo)item); 109 } 110 } 111 if (item instanceof UserFolderInfo) { 112 final UserFolderInfo userFolderInfo = (UserFolderInfo)item; 113 LauncherModel.deleteUserFolderContentsFromDatabase(mLauncher, userFolderInfo); 114 mLauncher.removeFolder(userFolderInfo); 115 } else if (item instanceof LauncherAppWidgetInfo) { 116 final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; 117 final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); 118 if (appWidgetHost != null) { 119 appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); 120 } 121 } 122 LauncherModel.deleteItemFromDatabase(mLauncher, item); 123 }删除时,判断删除的类型是否是AppWidget,如果是的话,要通过AppWidgetHost,删除AppWidetId,并最终从数据库中删除。
第二部分:分析问题:
问题描述:在烧上新版本以及做完factory reset之后,概率性出现添加到桌面上的widget不更新的问题,如电量管理没有图片,但是功能不受影响,天气和时钟都不更新。
问题再现条件:手机插有sim卡 && 第一次开机或为恢复出产设置重启
问题再现概率:50%
原因:
由于手机第一次开机的时候,会有一个下面的启动流程:
Launcher启动->检测到sim卡->重启Launcher
上一个启动的Launcher的activity还没有退出的时候,新启的Launcher的activity已经起来了。
由于统一package的activity只能注册一个listener,新启动的activity在要注册listener时,
因为上一个启动的Launcher还没有完全退出,系统发现此listener已经存在,
所以直接就把前一个listener的引用传了出来。但那个已经存在的listener对新的activity来说只是昙花一现,
即将被上一个Launcher销毁掉,新的activity中的listener变为null,导致以后所有的widget更新消息都得不到 处 理。
对应方法:
只需要修改一行代码就能搞定。我这边测试了5次,基本确认没有问题。
请参考下面的diff结果修改Launcher2的AndroidManifest.xml文件:
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3706661..c92f502 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -75,7 +75,8 @@
android:stateNotNeeded="true"
android:theme="@style/Theme"
android:screenOrientation="nosensor"
- android:windowSoftInputMode="stateUnspecified|adjustPan">
+ android:windowSoftInputMode="stateUnspecified|adjustPan"
+ android:configChanges="mcc|mnc|keyboard|keyboardHidden|navigation|orientation|uiMode">
android:configChanges 如果配置了这个属性,当我们横竖屏切换的时候会直接调用onCreate方法中的onConfigurationChanged方法,而不会重新执行onCreate方法,那当然如果不配置这个属性的话就会重新调用onCreate方法了
转下这个属性的使用方式
通过 设置 这个属性可以使Activity捕捉设备状态变化,以下是可以被识别的内容:
CONFIG_FONT_SCALE
CONFIG_MCC
CONFIG_MNC
CONFIG_LOCALE
CONFIG_TOUCHSCREEN
CONFIG_KEYBOARD
CONFIG_NAVIGATION
CONFIG_ORIENTATION
设置方法:将下列字段用“|”符号分隔开,例如:“ locale|navigation|orientation
”
Value | Description |
“mcc“ | The IMSI mobile country code (MCC) has changed — that is, a SIM hasbeen detected and updated the MCC.移动国家号码,由三位数字组成,每个国家都有自己独立的MCC,可以识别手机用户所属国家。 |
“mnc“ | The IMSI mobile network code (MNC) has changed — that is, a SIM hasbeen detected and updated the MNC.移动网号,在一个国家或者地区中,用于区分手机用户的服务商。 |
“locale“ | The locale has changed — for example, the user has selected a new language that text should be displayed in.用户所在地区发生变化。 |
“touchscreen“ | The touchscreen has changed. (This should never normally happen.) |
“keyboard“ | The keyboard type has changed — for example, the user has plugged in an external keyboard.键盘模式发生变化,例如:用户接入外部键盘输入。 |
“keyboardHidden“ | The keyboard accessibility has changed — for example, the user has slid the keyboard out to expose it.用户打开手机硬件键盘 |
“navigation“ | The navigation type has changed. (This should never normally happen.) |
“orientation“ | The screen orientation has changed — that is, the user has rotated the device.设备旋转,横向显示和竖向显示模式切换。 |
“fontScale“ | The font scaling factor has changed — that is, the user has selected a new global font size.全局字体大小缩放发生改变 |
? View Code XML
package="com.androidres.ConfigChangedTesting" android:versionCode="1" android:versionName="1.0.0"> android:icon="@drawable/icon"android:label="@string/app_name"> android:name=".ConfigChangedTesting" android:label="@string/app_name" android:configChanges="keyboardHidden|orientation"> android:name="android.intent.action.MAIN" /> > > > > |
在Activity中添加了 android:configChanges属性,目的是当所指定属性(Configuration Changes)发生改变时,通知 程序 调用 onConfigurationChanged()函数。 创建一个Layout UI:
? View Code XML
android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > android:id="@+id/pick" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Pick" /> android:id="@+id/view" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="View" /> > |
这个简单的UI包含两个按钮,其中一个是通过Contact列表选择一个 联系人 ,另外一个是查看当前选择联系人的详细内容。
项目的Java源代码:
01.import android.app.Activity;
02.import android.content.Intent;
03.import android.content.res.Configuration;
04.import android.net.Uri;
05.import android.os.Bundle;
06.import android.provider.Contacts.People;
07.import android.view.View;
08.import android.widget.Button;
09.
10.public class ConfigChangedTesting extends Activity {
11. /** Called when the activity is first created. */
12. static final int PICK_REQUEST = 1337;
13. Button viewButton=null;
14. Uri contact = null;
15. @Override
16. public void onCreate(Bundle savedInstanceState) {
17. super.onCreate(savedInstanceState);
18. //setContentView(R.layout.main);
19.
20. setupViews();
21. }
22.
23. public void onConfigurationChanged(Configuration newConfig) {
24. super.onConfigurationChanged(newConfig);
25.
26. setupViews();
27. }
28.
29. /* (non-Javadoc)
30. * @see android.app.Activity#onActivityResult(int, int, android.content.Intent)
31. */
32. @Override
33. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
34. // TODO Auto-generated method stub
35. //super.onActivityResult(requestCode, resultCode, data);
36.
37. if(requestCode == PICK_REQUEST){
38.
39. if(resultCode==RESULT_OK){
40.
41. contact = data.getData();
42. viewButton.setEnabled(true);
43. }
44.
45. }
46.
47. }
48.
49. private void setupViews(){
50.
51. setContentView(R.layout.main);
52.
53. Button pickBtn = (Button)findViewById(R.id.pick);
54.
55. pickBtn.setOnClickListener(new View.OnClickListener(){
56.
57. public void onClick(View v) {
58. // TODO Auto-generated method stub
59.
60. Intent i=new Intent(Intent.ACTION_PICK,People.CONTENT_URI);
61. startActivityForResult(i,PICK_REQUEST);
62. }
63. });
64.
65. viewButton =(Button)findViewById(R.id.view);
66.
67. viewButton.setOnClickListener(new View.OnClickListener() {
68. public void onClick(View view) {
69. startActivity(new Intent(Intent.ACTION_VIEW, contact));
70. }
71. });
72.
73. viewButton.setEnabled(contact!=null);
74. }
75.}
转载自:
http://blog.163.com/dengjingniurou@126/blog/static/53989196201201910154151/
更多相关文章
- Android中通过Messenger与Service实现进程间双向通信
- android 自定义AlertDialog 与Activity相互传递数据
- Android如何完全调试framework层代码
- 浅入浅出 Android(安卓)安全:第三章 Android(安卓)本地用户空间层
- android service
- android 开发提速
- Android高性能编程
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用