Android 代码优化点滴

MVC 模型:Mode,View,Controller与view相关的类:DragLayer (自定义的帧布局DragLayer 包含hotseat ,workspace ,和DragController , DragListener 等一起使得桌面上的shortcut具备拖动功能,和DropTarget一起具备删除功能  桌面拖动,删除的处理:将workspace 和DragLayer通过一些接口:DropTarget, DragSource, DragScroller配套完成,DragLayer 和DragController 操纵以上接口类,在workspace 去实现具体)àworkspace(自定义的类似pageview,自己可以snapToPage,即可在不同的cellLayout之间切换,同时实现拖动,触摸等接口,做具体的动作)à cellLayout(默认一个workspace 包含两个cellLayout,可以增加,删除 )à ShortcutAndWidgetContainer(在cellLayout 中,真正是它来确定桌面上的Shortcut 的位置,大小等)à (ShortcutInfo,FolderInfo)   与Mode 相关的类:LauncherProvider 提供和数据库直接操作相关的接口,LauncherModel 非UI线程(后台线程)加载或更新数据库。   与Controller相关的操作:Launcher 本身就是,1点击移动 , 删除 桌面上的Shortcut  DragController 同时,他提供DragListener,onDragStart ,onDragEnd 。workspace 实现接口对拖动导致的行为(删除应用,移动位置等)去通知LauncherModel 或者LauncherProvider 数据库的更新。LauncherApplication 作为一个Application类除了初始化保存一些全局变量外,创建LauncherModel,LauncherProvider等 数据操作并与他们绑定。这样在最关键的类Launcher中就可以通过LauncherApplication获得他们以便于操作数据。 入口点Launcher的onCreate() setupViews() 是创建桌面UI包括DragLayer,workspace等 建好布局之后去异步加载数据库,并且UI刷新。     mModel.startLoader();     mLoaderTask = new LoaderTask(mApp, isLaunching);     mLoaderTask.runBindSynchronousPage(synchronousBindPage);  同步加载    sWorker.post(mLoaderTask);异步加载loadWorkspaceOptimize();   à (loadWorkspaceScreens(mContext); 加载screen数据,à bindInfo(final ItemInfo itemInfo) 首先从launcher.db 中提取UI数据,在/data/data下在后面bind 这些数据到桌面UI上:à callbacks.bindItem(itemInfo);回调到执行launcher的bindItem à View shortcut = createShortcut(shortcutInfo);workspace.addInScreenworkspace 的addInScreen在视图上既负责向cellLayout中item ,cellLayout.addViewToCellLayout-> ShortcutAndWidgetContainer . addView,  同时又负责updateItemInDatabase 用户的对桌面的更改都会及时将相应的数据提交到launcher.db的favorites表 ,在桌面每次更新,旋转屏幕,重启等都会重新加载UI这就是绑定操作 这里Load和Bind的区别: 装载指的是将桌面UI相关数据添加到launcher.db的favorites表,或者从该表中读取分类,并将这些分类数据存储到一些字段中例如:sWorkspaceItems,mAllIdleAppsList。绑定操作就是将这些的数据在桌面上完成添加相应的shortcut ,appwidget 和folder   startBing 即在开始bind之前移除workspace所有视图workspace.removeAllViewsInLayout();  Bind 完成之后:重新刷新屏幕布局workspace.requestLayout(); 与一般的Handler 方便之处在于一般的handle人在主线程UI下运行,与这个同理的还有Service 相对应的IntentService  serviceHandler    private static final HandlerThread  sWorkerThread = new HandlerThread(“launcher-loader”);    static {        sWorkerThread.start();}将sWorker handler对象与HandlerThread  线程绑定处理耗时操作private static final Handler sWorker = new Handler(sWorkerThread.getLooper());    public void enqueuePackageUpdated(PackageUpdatedTask task) {        for (PackageUpdatedListener listener : mPackageUpdatedListeners) {            listener.onPreHandleUpdatedTask(task);        }        LogHelper.i(TAG + " enqueuePackageUpdated post task");        sWorker.post(task);    }PackageUpdatedTask 是实现了Runnable 所以上面post 会调用PackageUpdatedTask 的run 在此更新AllIdleAppsList ,完成之后,又启用一个线程调用callback. 例如:callbacks.bindAppsAdded  launcher 实现此接口LauncherModel.Callbacks,在bindAppsAdded 中,处理数据库更新。将当前的item信息添加到数据库。在onDestory时,一定要让线程退出。        mServiceHandler.removeCallbacksAndMessages(null);       mHandlerThread.quit();AsyncTask,      继承一个AsyncTask,然后实现        protected List doInBackground(Params… params)        protected void onPostExecute(Result result)      在onDestory时,要执行AsyncTask.cancel(); 思路  加载数据库是通过异步方式实现的,数据和桌面UI绑定是通过接口实现的。数据库改变到UI的刷新是通过监听实现。内容的改变到内容在数据库中的刷新或者是通过接收广播的形式,或者是通过异步线程操作的  (例如: launcher.db的favorites表被更新时,也会重新调用startLoader。)launcher.db 被破坏了怎么办?这个文件会被重新创建,launcher会成为默认的桌面。如何使显示推送的消息? LauncherModel.sBgScreenOrder 是所有screen id 集合LauncherModel.sBgWorkspaceScreens 是所有screen id 和 其对应的CellLayout 键值对集合MutilSelectedIcons setComponentEnabledSetting 可以禁用或者开启四大组件,可以用来隐藏应用图标,如果设置一个APP的mainactivity为COMPONENT_ENABLED_STATE_DISABLED将会在launcher中不显示应用图标。根据intent 中携带的boolean 数据 确定是否要changePreferredLauncher 更改默认启动的launcherRightTopMarkUpdaterManager   RightTopMarkUpdaterMissedCallContentObserver 继承UnreadContentObserver 实现RightTopMarkUpdater mMissedCallObserver = new MissedCallContentObserver(mContext, new Handler());                     mMissedCallObserver.registerUpdaterObserver(false);                    mListeners.put(CALL_COMPONENTNAME, mMissedCallObserver); public void updateIconsAfterLoadFinished() {        HashMap<ComponentName, RightTopMarkUpdater> updaters = new HashMap<ComponentName, RightTopMarkUpdater>();        synchronized (mLock) {            updaters.putAll(mListeners);        }        for (RightTopMarkUpdater updater : updaters.values()) {            updater.sendUpdateSignal();            if (updater.queryNotifyNum(mContext) <= 0) {//查询对应的数据URI                continue;            }            updater.updateIcon();        }        updaters.clear();    }   @Override    public void updateIcon() {        RightTopMarkUpdaterManager.updateCustomizeAppIcon(mContext, RightTopMarkUpdaterManager.CALL_COMPONENTNAME);    }UpdateCustomizeShortcuts 更新桌面iconlauncherModel 中有两个线程,一个是sWorkerThread,一个是主线程。下面的代码是要在sWorkerThread线程跑if (sWorkerThread.getThreadId() == Process.myTid()) {            r.run();        } else {            sWorker.post®;        }Class exthread extends Thread{Looper looper;public void run() {            Looper.prepare();//创建loop对象,包含消息队列。synchronized (this) {                mLooper = Looper.myLooper();                notifyAll();            }Looper.loop();//获得消息,从消息队列中分发对应的handle处理}}        public boolean quit() {//线程退出时的操作            Looper looper = getLooper();            if (looper != null) {                looper.quit();                return true;            }            return false;        }BubbleTextView shortcut,folder实际上都是BubbleTextView,if (child instanceof BubbleTextView) {                    String title = (String)((BubbleTextView)child).getText();mCountX mCountY 控件占用的网格数,一个屏幕的网格数例如256X256UI在水平,垂直方向上的跨度(占了多少个网格)if (lp.cellHSpan < 0) lp.cellHSpan = mCountX;            if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;CellLayout.LayoutParams lp =            new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); cellX 等都是按照网格数单位计算。一个屏幕的网格数例如256X256采用二维数组标记法判断该网格是否被占用。 if (null != mShortcutsAndWidgets) {//ShortcutAndWidgetContainer在cellLayout初始化时被加进来,它处理图标的添加,删除,位置,大小的确定                mShortcutsAndWidgets.addView(child, index, lp);            }PackageManager.resolveActivity 在启动activity之前判断是否存在于系统中。onStop 和onDestory可能不会执行,因为进程被中途 kill, 在onPause时,保存数据,是关键。所有Activity的状态 数据的修改,都应该此此时commitActivity只是一个数据的封装,并不是一个运行的实体,跟Thread没有亲缘关系。 ActivityThread在启动时,进程的主线程进入了一个Looper循环。由于ActvityThread才有大家熟悉 main函数,因此 ActivityThread持有这个进程始作俑者。 一个应用的所有Actvity,Service都是被ActivityThread持有并调度。因此同一个apk下,默认状态下 所有Activity,Service是在同一个主线程中执行。Activity和Service不仅共进程,而且是同一个线程, 因此他们之间自然是同步的。 一个普通的Handler只是利用Looper来完成事务的分片处理,并不会减轻主线程的工作压力。 如果在Activity中开一个线程,但是在onDestroy时,并不关闭这个Thread,其结果是此Thread将独立 Activity之外运行,不管Activity是否存在,这个Thread都将一直运行。如果这个Thread不幸还依 Acitivity,悲剧必然发生。 永远都不要让的你线程成为野线程!Android有进程的概念,也有任务的概率,但是我们完成一系列操作时,往往是需要多个Activity的配合。甚至多 apk完成一个任务,因此从操作结构来说,我们需要淡化进程的概率,而以任务的概率来看Android的处理机制。 Activity有四种加载模式:standard(默认), singleTop, singleTask和 singleInstance          standard: Activity的默认加载方法,即使某个Activity在Task栈中已经存在,另一个activity通过Intent跳转到该activity,同样会新创建一个实例压入栈中。例如:现在栈的情况为:A B C D,在D这个Activity中通过Intent跳转到D,那么现在的栈情况为: A B C D D ,此时按back键,会变成A B C D,还是显示D。  singleTop: 当该Activity位于栈顶的时候,再通过Intent跳转到本身这个Activity,则将不会创建一个新的实例压入栈中。例如:现在栈的情况为:A B C D。D的Launch mode设置成了singleTop,那么在D中启动Intent跳转到D,那么将不会新创建一个D的实例压入栈中,此时栈的情况依然为:A B C D。但是如果此时B的模式也是singleTop,D跳转到B,那么则会新建一个B的实例压入栈中,因为此时B不是位于栈顶,此时栈的情况就变成了:A B C D B。singleTask:         如果某个Activity是singleTask模式,那么Task栈中将会只有一个该Activity的实例。例如:现在栈的情况为:A B CD。B的Launch mode为singleTask,此时D通过Intent跳转到B,则栈的情况变成了:A B。而C和D被弹出销毁了,也 就是说位于B之上的实例都被销毁了。          singleTask模式的Activity不管是位于栈顶还是栈底,再次运行这个Activity时,都会destory掉它上面的Activity来保证整个栈中只有一个自己,这点是毋庸置疑的。singleInstance:        将Activity压入一个新建的任务栈中。例如:Task栈1的情况为:A B C。C通过Intent跳转到D,而D的Launch mode为singleInstance,则将会新建一个Task栈2。此时Task栈1的情况还是为:A B C。Task栈2的情况为:D。此时屏幕界面显示D的内容,如果这时D又通过Intent跳转到D,则Task栈2中也不会新建一个D的实例,所以两个栈的情况也不会变化。而如果D跳转到C,则栈1的情况变成了:A B C C,因为C的Launch mode为standard,此时如果再按返回键,则栈1变成:A B C。也就是说现在界面还显示C的内容,不是D。taskAffinity属性       Activity的归属,也就是Activity应该在哪个Task中,Activity与Task的吸附关系。我们知道,一般情况下在同一个应用中,启动的Activity都在同一个Task中,它们在该Task中度过自己的生命周期,这些Activity是从一而终的好榜样。

那么为什么我们创建的Activity会进入这个Task中?它们会转到其它的Task中吗?如果转到其它的Task中,它们会到什么样的Task中去?

解决这些问题的关键,在于每个Activity的taskAffinity属性。

每个Activity都有taskAffinity属性,这个属性指出了它希望进入的Task。如果一个Activity没有显式的指明该 Activity的taskAffinity,那么它的这个属性就等于Application指明的taskAffinity,如果 Application也没有指明,那么该taskAffinity的值就等于包名。而Task也有自己的affinity属性,它的值等于它的根 Activity的taskAffinity的值。

一开始,创建的Activity都会在创建它的Task中,并且大部分都在这里度过了它的整个生命。然而有一些情况,创建的Activity会被分配其它的Task中去,有的甚至,本来在一个Task中,之后出现了转移。allowTaskReparenting属性        用来标记Activity能否从启动的Task移动到有着affinity的Task(当这个Task进入到前台时),“true”,表示能移动,“false”,表示它必须呆在启动时呆在的那个Task里。 如果这个特性没有被设定,设定到元素上的allowTaskReparenting特性的值会应用到Activity上。默认值为“false”。 
        一般来说,当Activity启动后,它就与启动它的Task关联,并且在那里耗尽它的整个生命周期。当当前的Task不再显示时,你可以使用这个特性来强制Activity移动到有着affinity的Task中。典型用法是:把一个应用程序的Activity移到另一个应用程序的主Task中。  
      例如,如果 email中包含一个web页的链接,点击它就会启动一个Activity来显示这个页面。这个Activity是由Browser应用程序定义的,但是,现在它作为email Task的一部分。如果它重新宿主到Browser Task里,当Browser下一次进入到前台时,它就能被看见,并且,当email Task再次进入前台时,就看不到它了。 
       Actvity的affinity是由taskAffinity特性定义的。Task的affinity是通过读取根Activity的affinity决定。因此,根Activity总是位于相同affinity的Task里。由于启动模式为“singleTask”和“singleInstance”的Activity只能位于Task的底部,因此,重新宿主只能限于“standard”和“singleTop”模式。 Activity的启动Flag        LauncherMode是Activity自己具有的属性, 而Flag是第三方指定给Activity的属性,因此LauncherMode和Flag在某些时候会相同,也些时候会冲突  FLAG_ACTIVITY_NEW_TASK          例如现在栈1的情况是:A B C。C通过intent跳转到D,并且这个intent添加了FLAG_ACTIVITY_NEW_TASK 标记,如果D这个Activity在Manifest.xml中的声明中添加了Task affinity,并且和栈1的affinity不同,系统首先会查找有没有和D的Task affinity相同的task栈存在,如果有存在,将D压入那个栈,如果不存在则会新建一个D的affinity的栈将其压入。如果D的Task affinity默认没有设置,或者和栈1的affinity相同,则会把其压入栈1,变成:A B C D,这样就和不加FLAG_ACTIVITY_NEW_TASK 标记效果是一样的了。    注意如果试图从非activity的非正常途径启动一个activity,比如从一个service中启动一个activity,则intent比如要添加FLAG_ACTIVITY_NEW_TASK 标记。  FLAG_ACTIVITY_CLEAR_TOP例如现在的栈情况为:A B C D 。D此时通过intent跳转到B,如果这个intent添加FLAG_ACTIVITY_CLEAR_TOP 标记,则栈情况变为:A B。如果没有添加这个标记,则栈情况将会变成:A B C D B。也就是说,如果添加了FLAG_ACTIVITY_CLEAR_TOP 标记,并且目标Activity在栈中已经存在,则将会把位于该目标activity之上的activity从栈中弹出销毁。这跟上面把B的Launch mode设置成singleTask类似。  FLAG_ACTIVITY_SINGLE_TOP  类singTop
 FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET 类SingleTask  FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS  FLAG_ACTIVITY_REORDER_TO_FRONT   ABCD – > ACDB
         Context故名思议就一个应用的上下文环境,跟packageName强相关,layout id,String id, 能生效,都是限定 在特定的Context环境中。(Context接口导读)           通过Context的具体实现ContextImpl,          1. 我们可以获取apk运行的所有相关路径和打开文件          2. 我们可以获取啊apk的class、resource、ClassLoader,从而完成加载这个apk里面的字节码和资源。          3. 操作墙纸和获得系统提供的所有服务。          4. 可以在一个Context中,通过packageName,创建另外一个apk的Context,从而加载另外一个apk            的Resource资源和代码。          5. 可以获取PackageManager对象,从而完成更多全局性的操作。 《Intent-Filter》    这个实际是一个协议信息,表示这个Activity能够处理的所有任务类型和数据类型。        《Meta-data》            我们很少有人关注meta-data,这个是Activity用来标记这个Activity一些特殊信息的字段。             获取安装的Activity信息,指定PackageManager.GET_META_DATA定制Activity的专属信息             List resolveInfos = pm. queryIntentActivities(intent,                     PackageManager.GET_META_DATA);ResolveInfo          通过Context,可以获取PackageManager对象,有了 PackageManager对象,我们就能操作更多的东西,最熟悉的莫过于ResolveInfo。         public abstract List queryIntentActivities(Intent intent,    int flags);        public abstract List queryIntentServices(Intent intent,      int flags);        public abstract List queryBroadcastReceivers(Intent intent,   int flags);        public abstract List queryContentProviders(             String processName, int uid, int flags);    通过一个指定 的Intent,获取系统所有的     Activity,Service,Provider,    在结合meta-data,就可以完成一些列自     动扫描工作     比如系统自动识别当前安装的华为锁屏     系统自动识别MeWidget unit。 我们说过Activity在后台时,由于系统的内存不足,是很容易被杀死的,当横竖屏切换时,Activity有时也会先destory掉,然后再启动,再这个过程中,如果保存状态?以便Activity重启时,可以恢复?         方法一:重载onSaveInstanceState函数和onRestoreInstanceState函数,这个比较简单,通常会把一些简单数据类型放在onSaveInstanceState的Bundle参数,在onRestoreInstanceState时,从Bundle中取出信息。         方法二:重载onRetainNonConfigurationInstance 和使用getLastNonConfigurationInstance  
  public Object onRetainNonConfigurationInstance() {{    
       //这里需要保存的内容,在切换时不是bundle了,我们可以直接通过Object来代替
      return obj;
 }         在恢复窗口时,使用getLastNonConfigurationInstance 获取保存的对象,我们可以直接在onCreate中使用,比如           Object obj = getLastNonConfigurationInstance();     最终obj的内容就是上次切换时的内容。       方法二的好处:
  可以保存一些大数据,比如图片数据。           可以保存一些复杂的数据。          保存的数据是原封不动的返回的,并不是这个数据的拷贝,因此包含某些Connection Instance,从而保证可以在整个变化过程中保持连接状态。  l 在一切皆View中,我们已经分析过Dialog其实就是再次创建了一个View并添加到WindowManager中。          我们在Activity中使用Dialog时,有两种方式。          方式一:直接创建Dialog并显示。        方法二:调用Activity的showDialog(int id)        然后重载onCreateDialog    和重载onPrepareDialog(int id, Dialog dialog, Bundle args)     方法一的弊端是可能显示多个Dialog,且横竖屏      切换时Dialog的状态不ke恢复      方法二的好处,Dialog全部有Activity来管理,不会出现同一个Dialog显示两次问题。      Activty会管理Dialog的状态。Dialog可以通过 如果要实现一个Fragment,必须要有一个空构造器的构造函数。用于状态恢复时,系统构建整个Fragment       两个单独的Fragment之间是不应该进行通信的。应该使用他们所存在的Activity作为沟通的纽带。为了实现两个Fragment的交互,您可以在Fragment中定义一个接口,然后再这个接口中定义一个方法,在Fragment的onAttach()方法中调用这个接口中的方法。然后让Activity实现这个方法来完成Activity和Fragment之间的通信。       在Activity中使用有关Fragment的添加、删除、替换以及用它们执行其他响应用户交互行为的能力是一项伟大的功能。你提交给Activity的每组改变集合被叫做一个事务,并且你能使用FragmentTransaction中APIs来执行它。也能够把每个事务保存到被Activity管理的回退堆栈中,并允许用户通过Fragment改变来向后导航(类似同Activity的向后导航)。  你能够从FragmentManager对象中获取一个FragmentTransaction对象的实例,例如:FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();        每个事务是一组想要同时执行的改变,你能够使用诸如add()、remove()和replace()方法把想要在一个事务中执行的所有改变组合到一起,然后,调用commit()方法,把事务的执行结果反映到Activity中。但是,在调用commit()方法之前,为了把事务添加到Fragment事务的回退堆栈,你可能要调用addToBackStack()方法。这个回退堆栈被Activity管理,并且允许用户通过按返回按钮返回到先前的Fragment状态。PreferenceActivity直接继承自ListActivity,当绑定一个Preference信息时,会把Preference保存到一个List中去,然后在通过Adapter,把Preference的每一个Item绑定到ListView的Item上,因此通过getListViewI()来控制ListView的布局,达到控制PreferenceActivity的目的。 Preference是什么?这个是一个对普通View的封装。在《一切皆View》章节说过,我们看到的界面,都基本可以肯定是一个View。因此Preference最终现实的实际都是View。 Preference是一个数据的描述集合,描述了ListView的每一个Item的Title,sumary等信息。当要使用这个Preference时,直接inflater一个layout,然后把Preference的title,summary写到layout中。  if (0 == sModuleCount) {            if (mHandler.hasMessages(MSG_CHECK_APPSTATUS)) {                mHandler.removeMessages(MSG_CHECK_APPSTATUS);            }            mHandler.sendEmptyMessageDelayed(MSG_CHECK_APPSTATUS, 7000);—      我们在窗口上看到的任何东西,基本可以肯定是个View。因为目前我们接触的,只有View有draw方法和onDraw方法,也就是把自己绘制并显示出来(SurfaceView另说)。我们要在View上显示一个东西的话,最快速的方法,就是让View自己管理自己,自己显示自己。Activity最多只作为一个挂名的地方。           对于一个显示模块,最终的归属是WindowManager,而我们能控制的地方是View,对于显示这块做性能优化,我们就要从View着手,尽量减少View被动等待的时间。           当前只有Activity和Dialog会通过先创建一个Window,然后通过Window获取DecorView。其他可见的东西都是直接使用View并添加到WindowManager中          使用Window获取DecorView和不使用Window的区别,主要是获取焦点。  历史上JVM的单虚拟机并不适用于Android,自然地,Android考虑采用 :一个应用程序(APK),一个虚拟机实例。 好处显而易见:即使一个应用程序报错,一般也不会影响其他程序; 但问题也很明显:每打开一个应用程序,都要重新创建虚拟机实例(实际 上还有一些其他的事情要做),由此导致的性能问题–程序启动缓慢。 Ø ANR是Application Not Responding的缩写。 在Android系统中,系统服务WindowManagerService和ActivityManagerService会监视应用进程的响应性。如果一段时间没有响应,系统会弹出对话框提示用户是否选择关闭该程序或是继续等待。Ø 一般说来,ANR都是由用户输入来驱动的。举例:一个应用的UI线程正在进行一个耗时操作(如访问网络,进行一些缓慢的磁盘操作,执行没有优化过的SQL查询,大量写入数据到数据库等等),此时应用阻塞了I/O操作,使得系统无法处理用户的输入事件,当触发timeout条件时导致ANR。 Ø 1、WMS和AMS是如何监视应用响应时间的? Ø 答:KeyDispatchTimeout(原始5秒,后改为10秒):按键或触摸事件在特定时间内无响应,具体的超时时间的,Google原始设计中在framework的时间定义 Ø ActivityManagerService.java:Ø     // How long we wait until we timeout on key dispatching.
    static final int KEY_DISPATCHING_TIMEOUT = 51000;(原始5秒,DTS2012070901152 修改为10秒) Ø BroadcastTimeout(10 seconds) :BroadcastReceiver在特定时间内无法处理完成; Ø BroadcastTest.java:
    // How long we allow a receiver to run before giving up on it.
    static final int BROADCAST_TIMEOUT = 10
1000;Ø ServiceTimeout(20 seconds) :Service在特定的时间内无法处理完成 Ø ActiveServices.java:
    // How long we wait for a service to finish executing.
    static final int SERVICE_TIMEOUT = 201000; 尽量不要接收SCREEN_OFF的广播消息 原因分析:因为SCREEN_OFF时,系统即将休眠,如果在接收SCREEN_OFF消息的方法中处理事情,在某些情况下极易超时出现ANR(其概率大大超过接收其他广播)。 解决办法:1.在需要获得屏幕休眠时改用PowerManager的getScreenOn()函数来判断;         2.接收SCREEN_OFF消息后,将处理事件用Handler发消息来处理。(但是这也不是最好的方法,这种情况也有可能超时,只是降低了概率,因此还是建议不要使用SCREEN_OFF的广播消息) 例:  public void onReceive(Context context, Intent intent) {      if(“android.intent.action.SCREEN_OFF”.equals(intent.getAction())) {          //要处理的事件改用如下方式:          Message msg = new Message();          msg.what = MESSAGE;          mHandler.sendMessage(msg);                       } }                                private Handler mUIHandler = new Handler() {      @Override     public void handleMessage(Message msg) {          if(MESSAGE == msg.what){              //处理事件;         }     } } 发散思维:在回调函数等易超时的函数中不要做耗费CPU的操作,比如以on开头的函数:onReceive(), onKeyDown(), onConfigureChange()等等,遇到这种函数都改用Handler发消息来处理。【Android细节每日学习】第一期:尽量不要接收SCREEN_OFF的广播消息 原因分析:因为SCREEN_OFF时,系统即将休眠,如果在接收SCREEN_OFF消息的方法中处理事情,在某些情况下极易超时出现ANR(其概率大大超过接收其他广播)。 解决办法:1.在需要获得屏幕休眠时改用PowerManager的getScreenOn()函数来判断;         2.接收SCREEN_OFF消息后,将处理事件用Handler发消息来处理。(但是这也不是最好的方法,这种情况也有可能超时,只是降低了概率,因此还是建议不要使用SCREEN_OFF的广播消息) 例:  public void onReceive(Context context, Intent intent) {      if(“android.intent.action.SCREEN_OFF”.equals(intent.getAction())) {          //要处理的事件改用如下方式:          Message msg = new Message();          msg.what = MESSAGE;          mHandler.sendMessage(msg);                       } }                                private Handler mUIHandler = new Handler() {      @Override     public void handleMessage(Message msg) {          if(MESSAGE == msg.what){              //处理事件;         }     } } 发散思维:在回调函数等易超时的函数中不要做耗费CPU的操作,比如以on开头的函数:onReceive(), onKeyDown(), onConfigureChange()等等,遇到这种函数都改用Handler发消息来处理。 【Android细节每日学习】第二期:不要以为你的Activity在onResume状态时,你就可以为所欲为 【原因分析】:在锁屏状态时,你的Activity也是处于onResume状态;你的Acitivity有可能在高速运行,也一定在draw整个View。              如果正好处在锁屏界面,极易导致解锁界面冻屏。              你很意外,我也很意外,但是这就是事实,锁屏时你的Acitiviy是onRsume的,只是没有windowFocus。 【真实案例】:之前在Carmera界面锁屏时,手机解锁时极容易卡住,难以解锁。后来跟踪发现,在锁屏时,Carmera处于onResume,正在不停的取景,导致锁屏卡死。 【解决方法】:在锁屏状态下,你的Activity是没有Window Focus的,所以可以加上通过hasWindowFocus()这个方法来判断,从而避免在锁屏状态下做占用CPU高的操作。             同样在窗口焦点改变时,你也要判断当前是不是onResume。      private void startPreview();     private void stopPreview();    protected void onResume() {        super.onResume();        if(this.hasWindowFocus()){            //startPreview();        }    }     public void onWindowFocusChanged(boolean hasWindowFocus) {        // TODO Auto-generated method stub        super.onWindowFocusChanged(hasWindowFocus);        if(!hasWindowFocus){            //stopPreview();        }    } 【Android细节每日学习】第三期: onDetachedFromWindow与crash【背景说明】    onAttachedToWindow:是在View添加到Windows上时被调用。   一般在该方法中做初始化工作,比如注册一些广播等。     onDetachedFromWindow:在我们View从Windows上剥离时调用。当我们需要的View不再显示。 一般在该方法做一些收尾工作,比如取消广播注册等。     但是,如果在onAttachedToWindow注册广播接收器,在onDetachedFromWindow取消注册,总有一次,你会crash。 【原因分析】:这里面有一个大的系统bug,系统有时可能会调用一次onAttachedToWindow,而调用多次onDetachedFromWindow,从而可能导致你的清理操作出现异常。              【解决方法】:在注册广播接收时,设置一个boolean标记,取消注册时,先检查这个boolean标记。 例: protected void onAttachedToWindow() {        super.onAttachedToWindow();        / monitor time ticks, time changed, timezone */         if (null == mTimeChangeReceiver) {            mTimeChangeReceiver = new TimeChangeReceiver();        }        if (!mTimeChangeReceiver.mHasRegedit) {            registerReceiver();            mTimeChangeReceiver.mHasRegedit = true;        }    }    protected void onDetachedFromWindow() {        super.onDetachedFromWindow();         if (null != mTimeChangeReceiver && mTimeChangeReceiver.mHasRegedit) {            unRegisterReceiver();        }  【发散】:ViewFlipper中有重载的onAttachedToWindow()与onDetachedFromWindow(),它在onAttachedToWindow注册广播接收器,在onDetachedFromWindow取消注册,所以使用该类的时候也要小心,请重载这两个方法。  【Android细节每日学习】第四期: 警惕RemoteViews作为成员变量多次使用。 【背景】   Android中经常会使用在顶部状态条上显示一些通知。显示通知时一般都会用到RemoteViews,RemoteViews相当于一个协议,可以把一些界面上的元素封装到一个可以序列化的对象中,从而完成跨进程访问。   如果把RemoteViews作为成员变量多次使用,那么随着系统使用时间变长,极有可能发生ANR异常。【说明】   主题模块有可以下载在线主题,在下载主题时,需要发送通知显示当前的下载进度。但是测试过程中,发现多下载几个主题时,极高概率出现ANR。起初以为是下载多个主题是,CPU占用率太高导致。   经过分析RemoteViews原代码,发现RemoteViews中的所有数据在封装时,都是保存到一个ArrayList中去。如果多个主题下载通知共享一个RemoteViews,意味着这个ArrayList会越来越大,而ArrayList中的每一个对象,最后都会执行反射操作,因此出现ANR。因为后面的反射覆盖了前面的反射操作,因此我们在界面上没有发现显示异常。public void setBitmap(int viewId, String methodName, Bitmap value) {        addAction(new BitmapReflectionAction(viewId, methodName, value));} private void addAction(Action a) {        if (hasLandscapeAndPortraitLayouts()) {            throw new RuntimeException(“RemoteViews specifying separate landscape and portrait” +                    " layouts cannot be modified. Instead, fully configure the landscape and" +                    " portrait layouts individually before constructing the combined layout.");        }        if (mActions == null) {            mActions = new ArrayList();        }        mActions.add(a);         // update the memory usage stats        a.updateMemoryUsageEstimate(mMemoryUsageCounter);    } 【解决方法】RemoteViews永远都不要作为成员变量使用,只能作为临时变量,只使用一次。每次都重新构造。重复使用RemoteViews,会导致反射列表不断增加。  【Android细节每日学习】第五期:如何对一张Bitmap做软件切圆角【背景】对于需要圆角处理的图片,要UI一张一张的制作,麻不麻烦?        动态图片,如果也要达到切圆角的效果,又怎么做呢? 【方法】我们可以通过设置图片的透明度,把需要设圆角的部分设计成一个矩阵,通过改变矩阵的值来控制图片的透明度。(矩阵的透明度的值与UI制作图片的值保持一致,具体可参考附件)拿半径为4的圆角来举例:矩阵值为:new int[] {                        0, 64, 153, 230,                        64, 204, 255, 255,                        153, 255, 255, 255,                        255, 255, 255, 255                };(数值代表图片的透明度) 切图的具体算法如下:  public static Bitmap getRoundBitmap(Bitmap src, int radius) {        try {            Bitmap newBitMap = src.copy(Bitmap.Config.ARGB_8888, true);             int w = src.getWidth();            int h = src.getHeight();             int[] srcPixels = new int[w * h];            int[] maskPixels = new int[] {                        0, 64, 153, 230,                        64, 204, 255, 255,                        153, 255, 255, 255,                        255, 255, 255, 255                };             src.getPixels(srcPixels, 0, w, 0, 0, w, h);            clearRoundPixels(srcPixels, w, h, maskPixels, radius);            newBitMap.setPixels(srcPixels, 0, w, 0, 0, w, h);            return newBitMap;        } catch (Throwable ex) {            ex.printStackTrace();        }        return null;    }     private static void clearRoundPixels(int[] pixels, int w, int h, int[] maskPixels, int radius) {        for (int i = 0; i < radius; i++) {            int srcTopPos = i * w;            int srcBottomPos = (h - i - 1) * w;            int maskPos = i * radius;            for (int j = 0; j < radius; j++) {                int mask = maskPixels[maskPos + j];                if (mask == 0) {                    pixels[srcTopPos + j] = 0;                    pixels[srcTopPos - j + w - 1] = 0;                    pixels[srcBottomPos + j] = 0;                    pixels[srcBottomPos - j + w - 1] = 0;                } else if (mask < 255) {                    int alphaMask = (mask << 24) + 0xFFFFFF;                    pixels[srcTopPos + j] &= alphaMask;                    pixels[srcTopPos - j + w - 1] &= alphaMask;                    pixels[srcBottomPos + j] &= alphaMask;                    pixels[srcBottomPos - j + w - 1] &= alphaMask;                } else {                    break;                }            }        }    } 【发挥】对于别的图形,环形,圆形。。。别的形状,也可以通过修改矩阵的值来实现,要不试试?  【Android细节每日学习】第六期:小心MediaPlayer会超时【说明】MediaPlayer是Android系统中常用的媒体类播放器,一般用于播放视频,播放音乐铃声,如果我们在主线程中调用MediaPlayer的start,stop,reset等等函数,很 有可能对导致ANR超时。  【原因分析】MediaPlayer实际是对媒体播放器的一个封装,他所有的函数都会直接调用到底层的硬件,而媒体播放器的驱动在执行时,经常都会有几十毫秒的耗时,当硬件调试不是很好的,甚至出出现几秒钟的耗时,因此在这些情况下直接使用MediaPlayer,必然会出现ANR  【解决方法】1.直接AsyncPlayer类代替使用MediaPlayer,这些异步调用会减少MediaPlayer的超时。 2.直接实现一个类继承于MediaPlayer,然后仿照AsyncPlayer中的线程播放来调度MediaPlayer,总之,MediaPlayer的调度应该放到线程中完成。private final class Thread extends java.lang.Thread {        Thread() {            super(“AsyncPlayer-” + mTag);        }         public void run() {            while (true) {                Command cmd = null;                 synchronized (mCmdQueue) {                    if (mDebug) Log.d(mTag, “RemoveFirst”);                    cmd = mCmdQueue.removeFirst();                }                 switch (cmd.code) {                case PLAY:                    if (mDebug) Log.d(mTag, “PLAY”);                    startSound(cmd);                    break;                case STOP:                    if (mDebug) Log.d(mTag, “STOP”);                    if (mPlayer != null) {                        long delay = SystemClock.uptimeMillis() - cmd.requestTime;                        if (delay > 1000) {                            Log.w(mTag, "Notification stop delayed by " + delay + “msecs”);                        }                        mPlayer.stop();                        mPlayer.release();                        mPlayer = null;                    } else {                        Log.w(mTag, “STOP command without a player”);                    }                    break;                }}  【Android细节每日学习】第七期:警惕TimeTick广播爽约 【说明】  在手机中显示时间,或者产生周期性的心跳,我们往往第一时间想到的是监听TimeTick广播,比如锁屏时间,SystemUI的状态条时间,Launcher中的时钟Widget等等基本都是监听TimeTick广播,但是很不幸,在手机中极低概率下会出现收不到TimeTick广播了,对于时间显示的应用来说,这非常致命。  【解决方法】如果你的应用对时间不是特别敏感,不需要做特殊处理,一般稍后都能恢复,如果你的程序对时间特别敏感,就应该注意了,TimeTick广播爽约时有发生,不光是华为手机有这个问题。 此时我们可以充分利用系统的Hander对象,在收到TimeTick广播时,获取当前系统时间,计算出下一次刷星时间的的时间点,然后使用Handler发一个延时消息。如果TimeTick如约而至,我们可以正常刷新时间,在时间刷新的同时,取消上一次的Handler消息,然后再按照前述方法发送下一次的Handler消息。 如果TimeTick真的爽约,由于使用了比较准时的Handler消息,在时间需要刷新时,可以有Handler驱动,确保TimeTick爽约时,不至于时间停止不动。如下:注册广播接收:同时监听屏幕亮和TimeTick消息,如果是屏幕亮消息,检查是否有上一次的延时61秒检查消息,有的话,不做任何处理,没有的话,说明TimeTick中断了,立即刷新一次TimeTick消息。IntentFilter filter = new IntentFilter();        filter.addAction(Intent.ACTION_TIME_TICK);        filter.addAction(Intent.ACTION_SCREEN_ON);        mContext.registerReceiver(mTimeReceiver, filter); public class TimeReceiver extends BroadcastReceiver {        public void onReceive(Context context, Intent intent) {            if (DEBUG) Log.d(TAG, “TimeReceiver onReceive”);            if(Intent.ACTION_SCREEN_ON.equals(intent.getAction())){             if(mHandler.hasMessages(MSG_TIME_TICK)){             return;             }            }            mHandler.sendEmptyMessage(MSG_TIME_TICK);               }    }  Handler处理时,61秒后检查TimeTick是否达到:如果是正常的TimeTick消息,需要立即取消61秒延时的TimeTick。同时检查当前屏幕是否点亮,如果是亮屏,需要延时61秒再次检查TimeTick。如果是灭屏状态,为了减少功耗,不能再61秒后检查TimeTick。public Handler mHandler = new Handler() {        @Override        public void handleMessage(Message msg) {            if (MSG_TIME_TICK == msg.what) {              mHandler.removeMessages(MSG_TIME_TICK);                PowerManager pm = (PowerManager)getContext().getSystemServer(Context.POWER_SERVICE);                if(!pm.isScreenOn()){                    mHandler.removeMessages(MSG_TIME_TICK);                           long millis = System.currentTimeMillis()%60000;                    millis = 61000 - millis;                    mHandler.sendEmptyMessageDelayed(MSG_TIME_TICK,millis);                   }            } 类似现象已经在华为的手机上出现多次。当系统不能100%准确时,我们一定要准备好备份方案。 发散思维:对于系统中异常重要的流程,如果我们不能保证100%成功,一定要想清楚万一失败了怎么办?是否有临时方案补救。 Signal 11——SIGSEGV最为常见的出错类型,通常是访问了错误的内存地址造成,如访问野指针、空指针、已释放的内存地址、数组越界访问等等,signal之后,都会跟上fault addr 0xXXXXXXXX字样,对应访问出错的地址,如果是0x0或者很小的数字,基本可以确定就是访问空指针。如上图所示的的微信,就是典型的访问了空指针。对于fault addr很小的情况,则多为使用空结构指针去访问成员变量所致。Signal 11——SIGSEGV最为常见的出错类型,通常是访问了错误的内存地址造成,如访问野指针、空指针、已释放的内存地址、数组越界访问等等,signal之后,都会跟上fault addr 0xXXXXXXXX字样,对应访问出错的地址,如果是0x0或者很小的数字,基本可以确定就是访问空指针。如上图所示的的微信,就是典型的访问了空指针。对于fault addr很小的情况,则多为使用空结构指针去访问成员变量所致。  Signal 6——SIGABRT第二常见的类型,是主动报错然后结束自身进程,通常用于程序运行过程中检测到了不应出现的异常时,在framework中通常是因为CHECK宏导致,不满足条件则触发fatal。该类型也比较好定位,因为是主动报错所以会打印出代码中的上下文信息,如上图所示,对应的代码文件路径、出错函数、CHECK条件等都已经打印出来。   Signal 9——SIGKILL进程被Kill,可能是来源于进程自身,也可能是来源于其它进程或者系统,比如低内存情况下清理系统后台应用时,还有应用发生ANR然后用户选择结束进程等,相对少见。 Signal 4——SIGILL非法指令,可能原因是程序的二进制文件损坏或者内存跳变等原因,在Seattle平台早期上出现过不少次,目前在产线已经有了拦截方案,所以几乎没有见到了。   addr2line -a 地址 -e 含有符号表的库文件 对应上图所示日志,以DAVINCE为例完整命令行如下:addr2line -a 00093e7b -e ./out/target/product/DAVINCE/symbols/system/lib/libstagefright.so  onAttachedToWindow 在onResume()之后 自定义view postDelayed(Runnable action,long delayMillis)public boolean removeCallbacks(Runnable action)在自定义view中长按屏幕操作postDelayed 一个Runnable  线程,一段时间执行。再检测是否performLongClick()  来证明是否长按操作。在UP事件中加removeCallbacks 移除线程

发布了46 篇原创文章 · 获赞 0 · 访问量 629

猜你喜欢

转载自blog.csdn.net/qq_42894864/article/details/102671811