android SystemUI 流程分析
2013-10-18 14:00
1191 查看
android4 SystemUI 流程分析
什么是SystemUI?
对于Phone来说SystemUI指的是:StatusBar(状态栏)、NavigationBar(导航栏)。而对于Tablet或者是TV来说SystemUI指的是:
CombinedBar(包括了StatusBar和NavigationBar)。
启动后Phone界面上的信号,蓝牙标志,Wifi标志等等这些状态显示标志都会在StatusBar上显示。当我们的设备开机后,首先
需要给用户呈现的就是各种界面同时也包括了我们的SystemUI,因此对于整个Android系统来说,SystemUI都有举足轻重的作用。
现在就从代码开始一步步的分析
1、启动流程
代码路径:fameworks/base/packages/SystemUI
建立工程导入到eclipse中代码具体图示:
先从 AndroidManifest.xml 看看有哪些东东,以前说过android中有四大组件,这里就有如下的三大部分:
系统服务 Service :
SystemUIService
TakeScreenshotService
LoadAverageService
广播*** BroadcastReceive:
BootReceiver
Activity 应用:
USB的挺多哟...
UsbStorageActivity
UsbConfirmActivity
UsbPermissionActivity
UsbStorageActivity
UsbAccessoryUriActivity
NetworkOverLimitActivity
<!-- started from ... somewhere -->
Nyandroid
具体定义请看 AndroidManifest.xml 文件,上面只是简单的列一下
先看第一个Activity -- Nyandroid 这里做了什么呢?
就是网上传说中的 好多安卓机器人飞过去。。。。其中代码很简单,简单说一下动画效果的代码:
定义在 frameworks\base\packages\SystemUI\res\drawable\nyandroid_anim.xml
相关图片在: frameworks\base\packages\SystemUI\res\drawable-nodpi 如图示:
然后再看最重要的服务:SystemUIService
一般来说,Service启动一般由开机广播或者StartService/BindService这几种方式来启动。既然这个Service是一个系统
服务,应该是由系统这边启动,那么看下 SystemServer.java ,果然发现如下启动代码:
对于Android启动流程请看如下系统文章:
/article/1341930.html android启动--深入理解init进程
/article/1341929.html android启动--深入理解zygote
/article/1341928.html android启动--深入理解zygote (II)
/article/1341927.html android启动--深入理解启动HOME
那么就继续跟踪 SystemUIService 中代码:
在这代码中:
通过AIDL获取WindowManager对象并调用 wm.canStatusBarHide() 这个代码在哪里呢?
查看: frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
从以上代码来看,shortSizeDp小于600dp时,则系统会认为该设备是Phone反之则认为是Tablet。
根据mStatusBarCanHide的值,设定StatusBar或者SystemBar(CombinedBar)的高度,以及是否显示NavigationBar。
2、StatusBar(状态栏)及NavigationBar(导航栏)
如果是 StatusBar 则 SERVICES[0] 存放 com.android.systemui.statusbar.phone.PhoneStatusBar 否则存放
com.android.systemui.statusbar.tablet.TabletStatusBar
SERVICES[1] 存放 com.android.systemui.power.PowerUI.class
从我的机器上打印来看,
E/SystemServer( 1299): Starting service: Intent { cmp=com.android.systemui/.SystemUIService }
D/SystemUIService( 1382): running: com.android.systemui.statusbar.tablet.TabletStatusBar@415b8b20
D/SystemUIService( 1382): running: com.android.systemui.power.PowerUI@416b5ae8
I/PowerUI ( 1382): start
然后调用 mServices[i].start();那么就分析 TabletStatusBar 中的start方法吧
调用到 frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBar.java
在这里,完成了SystemUI的整个初始化以及设置过程,并最终呈现到界面上。
启动过程中完成如下操作:
1、获取icon list,addIcon(iconList.getSlot(i), i, viewIndex, icon);
2、获取notification,addNotification(notificationKeys.get(i), notifications.get(i));
3、显示StatusBar,WindowManagerImpl.getDefault().addView(sb, lp);
显示NavigationBar,WindowManagerImpl.getDefault().addView(
mNavigationBarView, getNavigationBarLayoutParams());
时序图如下:
3、最近任务缩略图显示
长按home键,列出最近启动过的任务缩略图,重要的两个类
// Recent apps
private RecentsPanelView mRecentsPanel;
private RecentTasksLoader mRecentTasksLoader;
SystemUI 获取按键事件,获取缩略图并将其显示出来,最后响应view上按键响应相应事件:
对于我们来说,关注点主要有如下几个:
1、缩略图如何获取
这里利用 ActivityManager 中的方法:getRecentTasks 获取当前任务的列表,然后再利用 getTaskThumbnails 获取
按键View 就是几个按键相应的View
相应的应用缩略图,调用序列图如下:
2、显示缩略图
public void show(boolean show, boolean animate,
ArrayList<TaskDescription> recentTaskDescriptions) {
if (show) {
// Need to update list of recent apps before we set visibility so this view's
// content description is updated before it gets focus for TalkBack mode
refreshRecentTasksList(recentTaskDescriptions);
// if there are no apps, either bring up a "No recent apps" message, or just
// quit early
boolean noApps = (mRecentTaskDescriptions.size() == 0);
if (mRecentsNoApps != null) { // doesn't exist on large devices
mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE);
} else {
if (noApps) {
if (DEBUG) Log.v(TAG, "Nothing to show");
return;
}
}
}else {
mRecentTasksLoader.cancelLoadingThumbnails();
mRecentTasksDirty = true;
}
...
}
如果 mRecentsNoApps 为空则表示没有任务,显示 "No recent apps" 否则显示应用列表
否则则显示任务的缩略图。时序图如下:
3、点击某个缩略图执行
这里分为点击某个缩略图执行程序及长按缩略图执行程序
这里直接继承了 View.OnItemClickListener 所以可以直接执行子项按键事件
处理点击事件方法:
注意代码:context.startActivity(intent); 这里就是执行对应的 Activity
处理长按键点击事件方法:
这里弹出一个PopupMenu,分别是 A:"Remove from list" 及 B:"App Info"
其中A项表示将此任务移除出列表,执行 mRecentsContainer.removeViewInLayout(selectedView);
另外B是启动另外一个Acitivty列出应用信息:
总结:
这里详细的对SystemUI 的两个最重要的 StatusBar NavigationBar(SystemUIService) 及缩略图代码流程分析。
因此各家厂商根据自家的需求,需要定制SystemUI或者美化SystemUI,不同的平台也会有不同的修改,但大体框架是没有变的,
无非是在原有基础上的修修改改或者增加一些自己的类等等。
什么是SystemUI?
对于Phone来说SystemUI指的是:StatusBar(状态栏)、NavigationBar(导航栏)。而对于Tablet或者是TV来说SystemUI指的是:
CombinedBar(包括了StatusBar和NavigationBar)。
启动后Phone界面上的信号,蓝牙标志,Wifi标志等等这些状态显示标志都会在StatusBar上显示。当我们的设备开机后,首先
需要给用户呈现的就是各种界面同时也包括了我们的SystemUI,因此对于整个Android系统来说,SystemUI都有举足轻重的作用。
现在就从代码开始一步步的分析
1、启动流程
代码路径:fameworks/base/packages/SystemUI
建立工程导入到eclipse中代码具体图示:
先从 AndroidManifest.xml 看看有哪些东东,以前说过android中有四大组件,这里就有如下的三大部分:
系统服务 Service :
SystemUIService
TakeScreenshotService
LoadAverageService
广播*** BroadcastReceive:
BootReceiver
Activity 应用:
USB的挺多哟...
UsbStorageActivity
UsbConfirmActivity
UsbPermissionActivity
UsbStorageActivity
UsbAccessoryUriActivity
NetworkOverLimitActivity
<!-- started from ... somewhere -->
Nyandroid
具体定义请看 AndroidManifest.xml 文件,上面只是简单的列一下
先看第一个Activity -- Nyandroid 这里做了什么呢?
就是网上传说中的 好多安卓机器人飞过去。。。。其中代码很简单,简单说一下动画效果的代码:
public class FlyingCat extends ImageView { public FlyingCat(Context context, AttributeSet as) { super(context, as); setImageResource(R.drawable.nyandroid_anim); // @@@ if (DEBUG) setBackgroundColor(0x80FF0000); } ... }
定义在 frameworks\base\packages\SystemUI\res\drawable\nyandroid_anim.xml
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@drawable/nyandroid00" android:duration="80" /> <item android:drawable="@drawable/nyandroid01" android:duration="80" /> <item android:drawable="@drawable/nyandroid02" android:duration="80" /> <item android:drawable="@drawable/nyandroid03" android:duration="80" /> <item android:drawable="@drawable/nyandroid04" android:duration="80" /> <item android:drawable="@drawable/nyandroid05" android:duration="80" /> <item android:drawable="@drawable/nyandroid06" android:duration="80" /> <item android:drawable="@drawable/nyandroid07" android:duration="80" /> <item android:drawable="@drawable/nyandroid08" android:duration="80" /> <item android:drawable="@drawable/nyandroid09" android:duration="80" /> <item android:drawable="@drawable/nyandroid10" android:duration="80" /> <item android:drawable="@drawable/nyandroid11" android:duration="80" /> </animation-list>
相关图片在: frameworks\base\packages\SystemUI\res\drawable-nodpi 如图示:
然后再看最重要的服务:SystemUIService
一般来说,Service启动一般由开机广播或者StartService/BindService这几种方式来启动。既然这个Service是一个系统
服务,应该是由系统这边启动,那么看下 SystemServer.java ,果然发现如下启动代码:
startSystemUi(contextF); static final void startSystemUi(Context context) { Intent intent = new Intent(); intent.setComponent(new ComponentName("com.android.systemui", "com.android.systemui.SystemUIService")); Slog.d(TAG, "Starting service: " + intent); context.startService(intent); }
对于Android启动流程请看如下系统文章:
/article/1341930.html android启动--深入理解init进程
/article/1341929.html android启动--深入理解zygote
/article/1341928.html android启动--深入理解zygote (II)
/article/1341927.html android启动--深入理解启动HOME
那么就继续跟踪 SystemUIService 中代码:
/** * The class names of the stuff to start. */ final Object[] SERVICES = new Object[] { 0, // system bar or status bar, filled in below. com.android.systemui.power.PowerUI.class, }; @Override public void onCreate() { // Pick status bar or system bar. IWindowManager wm = IWindowManager.Stub.asInterface( ServiceManager.getService(Context.WINDOW_SERVICE)); try { SERVICES[0] = wm.canStatusBarHide() ? R.string.config_statusBarComponent : R.string.config_systemBarComponent; } catch (RemoteException e) { Slog.w(TAG, "Failing checking whether status bar can hide", e); } final int N = SERVICES.length; mServices = new SystemUI ; for (int i=0; i<N; i++) { Class cl = chooseClass(SERVICES[i]); Slog.d(TAG, "loading: " + cl); try { mServices[i] = (SystemUI)cl.newInstance(); } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } catch (InstantiationException ex) { throw new RuntimeException(ex); } mServices[i].mContext = this; Slog.d(TAG, "running: " + mServices[i]); mServices[i].start(); } }
在这代码中:
SERVICES[0] = wm.canStatusBarHide() ? R.string.config_statusBarComponent : R.string.config_systemBarComponent;
通过AIDL获取WindowManager对象并调用 wm.canStatusBarHide() 这个代码在哪里呢?
查看: frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
public boolean canStatusBarHide() { return mStatusBarCanHide; } public void setInitialDisplaySize(int width, int height) { ... // Determine whether the status bar can hide based on the size // of the screen. We assume sizes > 600dp are tablets where we // will use the system bar. int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / DisplayMetrics.DENSITY_DEVICE; mStatusBarCanHide = shortSizeDp < 600; }
从以上代码来看,shortSizeDp小于600dp时,则系统会认为该设备是Phone反之则认为是Tablet。
根据mStatusBarCanHide的值,设定StatusBar或者SystemBar(CombinedBar)的高度,以及是否显示NavigationBar。
2、StatusBar(状态栏)及NavigationBar(导航栏)
如果是 StatusBar 则 SERVICES[0] 存放 com.android.systemui.statusbar.phone.PhoneStatusBar 否则存放
com.android.systemui.statusbar.tablet.TabletStatusBar
SERVICES[1] 存放 com.android.systemui.power.PowerUI.class
从我的机器上打印来看,
E/SystemServer( 1299): Starting service: Intent { cmp=com.android.systemui/.SystemUIService }
D/SystemUIService( 1382): running: com.android.systemui.statusbar.tablet.TabletStatusBar@415b8b20
D/SystemUIService( 1382): running: com.android.systemui.power.PowerUI@416b5ae8
I/PowerUI ( 1382): start
然后调用 mServices[i].start();那么就分析 TabletStatusBar 中的start方法吧
@Override public void start() { super.start(); // will add the main bar view }
调用到 frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/StatusBar.java
public void start() { // First set up our views and stuff. View sb = makeStatusBarView(); // Connect in to the status bar manager service StatusBarIconList iconList = new StatusBarIconList(); ArrayList<IBinder> notificationKeys = new ArrayList<IBinder>(); ArrayList<StatusBarNotification> notifications = new ArrayList<StatusBarNotification>(); mCommandQueue = new CommandQueue(this, iconList); mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); int[] switches = new int[7]; ArrayList<IBinder> binders = new ArrayList<IBinder>(); try { mBarService.registerStatusBar(mCommandQueue, iconList, notificationKeys, notifications, switches, binders); } catch (RemoteException ex) { // If the system process isn't there we're doomed anyway. } disable(switches[0]); setSystemUiVisibility(switches[1]); topAppWindowChanged(switches[2] != 0); // StatusBarManagerService has a back up of IME token and it's restored here. setImeWindowStatus(binders.get(0), switches[3], switches[4]); setHardKeyboardStatus(switches[5] != 0, switches[6] != 0); // Set up the initial icon state int N = iconList.size(); int viewIndex = 0; for (int i=0; i<N; i++) { StatusBarIcon icon = iconList.getIcon(i); if (icon != null) { addIcon(iconList.getSlot(i), i, viewIndex, icon); viewIndex++; } } // Set up the initial notification state N = notificationKeys.size(); if (N == notifications.size()) { for (int i=0; i<N; i++) { addNotification(notificationKeys.get(i), notifications.get(i)); } } else { Log.wtf(TAG, "Notification list length mismatch: keys=" + N + " notifications=" + notifications.size()); } // Put up the view final int height = getStatusBarHeight(); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height, WindowManager.LayoutParams.TYPE_STATUS_BAR, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, PixelFormat.OPAQUE); // the status bar should be in an overlay if possible final Display defaultDisplay = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); if (ActivityManager.isHighEndGfx(defaultDisplay)) { lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } lp.gravity = getStatusBarGravity(); lp.setTitle("StatusBar"); lp.packageName = mContext.getPackageName(); lp.windowAnimations = R.style.Animation_StatusBar; WindowManagerImpl.getDefault().addView(sb, lp); if (SPEW) { Slog.d(TAG, "Added status bar view: gravity=0x" + Integer.toHexString(lp.gravity) + " icons=" + iconList.size() + " disabled=0x" + Integer.toHexString(switches[0]) + " lights=" + switches[1] + " menu=" + switches[2] + " imeButton=" + switches[3] ); } mDoNotDisturb = new DoNotDisturb(mContext); }
在这里,完成了SystemUI的整个初始化以及设置过程,并最终呈现到界面上。
启动过程中完成如下操作:
1、获取icon list,addIcon(iconList.getSlot(i), i, viewIndex, icon);
2、获取notification,addNotification(notificationKeys.get(i), notifications.get(i));
3、显示StatusBar,WindowManagerImpl.getDefault().addView(sb, lp);
显示NavigationBar,WindowManagerImpl.getDefault().addView(
mNavigationBarView, getNavigationBarLayoutParams());
时序图如下:
3、最近任务缩略图显示
长按home键,列出最近启动过的任务缩略图,重要的两个类
// Recent apps
private RecentsPanelView mRecentsPanel;
private RecentTasksLoader mRecentTasksLoader;
SystemUI 获取按键事件,获取缩略图并将其显示出来,最后响应view上按键响应相应事件:
对于我们来说,关注点主要有如下几个:
1、缩略图如何获取
RecentsPanelView.java 中 refreshRecentTasksList(recentTaskDescriptions); --> mRecentTaskDescriptions = mRecentTasksLoader.getRecentTasks(); --> RecentTasksLoader.java 中 // return a snapshot of the current list of recent apps ArrayList<TaskDescription> getRecentTasks() { cancelLoadingThumbnails(); ArrayList<TaskDescription> tasks = new ArrayList<TaskDescription>(); final PackageManager pm = mContext.getPackageManager(); final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UN***AILABLE); ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) .resolveActivityInfo(pm, 0); HashSet<Integer> recentTasksToKeepInCache = new HashSet<Integer>(); int numTasks = recentTasks.size(); // skip the first task - assume it's either the home screen or the current activity. final int first = 1; recentTasksToKeepInCache.add(recentTasks.get(0).persistentId); for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) { final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i); TaskDescription item = createTaskDescription(recentInfo.id, recentInfo.persistentId, recentInfo.baseIntent, recentInfo.origActivity, recentInfo.description, homeInfo); if (item != null) { tasks.add(item); ++index; } } // when we're not using the TaskDescription cache, we load the thumbnails in the // background loadThumbnailsInBackground(new ArrayList<TaskDescription>(tasks)); return tasks; }
这里利用 ActivityManager 中的方法:getRecentTasks 获取当前任务的列表,然后再利用 getTaskThumbnails 获取
按键View 就是几个按键相应的View
public View getRecentsButton() { return mCurrentView.findViewById(R.id.recent_apps); } public View getMenuButton() { return mCurrentView.findViewById(R.id.menu); } public View getBackButton() { return mCurrentView.findViewById(R.id.back); } public View getHomeButton() { return mCurrentView.findViewById(R.id.home); }
相应的应用缩略图,调用序列图如下:
2、显示缩略图
public void show(boolean show, boolean animate,
ArrayList<TaskDescription> recentTaskDescriptions) {
if (show) {
// Need to update list of recent apps before we set visibility so this view's
// content description is updated before it gets focus for TalkBack mode
refreshRecentTasksList(recentTaskDescriptions);
// if there are no apps, either bring up a "No recent apps" message, or just
// quit early
boolean noApps = (mRecentTaskDescriptions.size() == 0);
if (mRecentsNoApps != null) { // doesn't exist on large devices
mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE);
} else {
if (noApps) {
if (DEBUG) Log.v(TAG, "Nothing to show");
return;
}
}
}else {
mRecentTasksLoader.cancelLoadingThumbnails();
mRecentTasksDirty = true;
}
...
}
如果 mRecentsNoApps 为空则表示没有任务,显示 "No recent apps" 否则显示应用列表
否则则显示任务的缩略图。时序图如下:
3、点击某个缩略图执行
这里分为点击某个缩略图执行程序及长按缩略图执行程序
这里直接继承了 View.OnItemClickListener 所以可以直接执行子项按键事件
public class RecentsPanelView extends RelativeLayout implements OnItemClickListener, RecentsCallback, StatusBarPanel, Animator.AnimatorListener, View.OnTouchListener
处理点击事件方法:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { handleOnClick(view); } public void handleOnClick(View view) { TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription; final Context context = view.getContext(); final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (ad.taskId >= 0) { // This is an active task; it should just go to the foreground. am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME); } else { Intent intent = ad.intent; intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY | Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK); if (DEBUG) Log.v(TAG, "Starting activity " + intent); context.startActivity(intent); } hide(true); }
注意代码:context.startActivity(intent); 这里就是执行对应的 Activity
处理长按键点击事件方法:
public void handleLongPress( final View selectedView, final View anchorView, final View thumbnailView) { thumbnailView.setSelected(true); PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.recent_remove_item) { mRecentsContainer.removeViewInLayout(selectedView); } else if (item.getItemId() == R.id.recent_inspect_item) { ViewHolder viewHolder = (ViewHolder) selectedView.getTag(); if (viewHolder != null) { final TaskDescription ad = viewHolder.taskDescription; startApplicationDetailsActivity(ad.packageName); mBar.animateCollapse(); } else { throw new IllegalStateException("Oops, no tag on view " + selectedView); } } else { return false; } return true; } }); popup.setOnDismissListener(new PopupMenu.OnDismissListener() { public void onDismiss(PopupMenu menu) { thumbnailView.setSelected(false); } }); popup.show(); }
这里弹出一个PopupMenu,分别是 A:"Remove from list" 及 B:"App Info"
其中A项表示将此任务移除出列表,执行 mRecentsContainer.removeViewInLayout(selectedView);
另外B是启动另外一个Acitivty列出应用信息:
private void startApplicationDetailsActivity(String packageName) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(intent); }
总结:
这里详细的对SystemUI 的两个最重要的 StatusBar NavigationBar(SystemUIService) 及缩略图代码流程分析。
因此各家厂商根据自家的需求,需要定制SystemUI或者美化SystemUI,不同的平台也会有不同的修改,但大体框架是没有变的,
无非是在原有基础上的修修改改或者增加一些自己的类等等。
相关文章推荐
- JetBrains WebStorm 7.0 Build 131.202 Win/Mac/Liniux
- 关于master..spt_values的应用帖子
- getRequestDispatcher()与sendRedirect()区别
- EasyUi_学习之路_04
- PHP中include与require的用法区别
- asterisk-gui nautilus-gksu_2.0.2-5ubuntu2_i386.deb
- Ext.ComponentQuery用法
- ligerUi Grid 自动换行
- The 1st tip of DB Query Analyzer
- The 1st tip of DB Query Analyzer
- UITextField详解
- UVa 10049 Self-describing Sequence (自描述序列&二分递推)
- LeetCode:N-Queens
- LeetCode:N-Queens II
- no such partition grub rescue的解决方案
- 关于UIView的autoresizingMask属性的研究
- iPhone开发--uitableview
- Android 实战之UI线程和Worker线程交互
- easyui datagrid formatter 无效解决办法
- uIP的udp实现