读《阿里巴巴Android开发手册》笔记

Posted by Don on March 1, 2018

自从个人域名博客关闭以后就转战至了github,但是账号注册了一年多了一篇正经的博文没写过,今日阅读了阿里巴巴Android开发手册后突然又有了写博文的冲动, 因此在此做些笔记,以规范自己。

一、基本组件

1、【强制】Activity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过 resolveActivity 检查, 避免找不到合适的调用组件,造成 ActivityNotFoundException 的异常。

public void viewUrl(String url, String mimeType) {
	Intent intent = new Intent(Intent.ACTION_VIEW);
	intent.setDataAndType(Uri.parse(url), mimeType);
	if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ ONLY) != null) {
		startActivity(intent);
	} else {
		// 找不到指定的 Activity
	}
}

PS:什么是显试Intent、隐试Intent?
显示Intent:是在已知包名和类名的情况下常用的跳转方法,用于跳转到当前应用的某个Activity.

Intent intent = new Intent(mContext, XXActivity.class);  
startActivity(intent); 

隐试Intent:即不是像显式的那样直接指定需要调用的Activity,而是设置Action、Data、Category, 让系统来筛选出合适的Activity。筛选是根据所有的来筛选. **a.隐式跳转之Action跳转** 假设有一个Activity在清单中是这样的声明的:

<activity android:name=".ActionActivity";   
     <intent-filter  
         action android:name="action_activity"  
     </intent-filter>  
 </activity>  

那么我们就可以使用以下代码进行跳转到上面这个Activity中:

//创建一个隐式的Intent对象
Intent intent=new Intent();
//设置Intent的Action为清单中指定的action
intent.setAction("action_activity");
startActivity(intent);

b、隐式跳转之Category跳转
假设有一个Activity在清单中是这样声明的:

<activity android:name=".CategoryActivity" >  
    <intent-filter>  
        <action android:name="action_activity" />  
        <category android:name="category_activity" />  
    </intent-filter>  
</activity>  

那我们就可以使用如下代码倒转到以上Activity中:

Intent intent=new Intent();
intent.setAction("action_activity");
//添加Category为清单中指定的category
intent.addCategory("category_activity");
startActivity(intent);

c、隐式跳转之Data跳转
假设有一个Activity是这样定义的:

< activity android:name=".DataActivity">  
    < intent-filter>  
        < category android:name="android.intent.category.DEFAULT" />  
        < data  
            android:scheme="content"  
            android:host="com.example.intentdemo"  
            android:port="8080"  
            android:pathPattern=".*pdf"  
            android:mimeType="text/plain"/>  
    < /intent-filter>  
< /activity>

那我们可以使用如下代码跳转到以上的Activity中:

Intent intent = new Intent();  
Uri uri = Uri.parse("content://com.example.intentdemo:8080/abc.pdf");  
//注:setData、setDataAndType、setType 这三种方法只能单独使用,不可共用                 
//单独以 setData 方法设置 URI  
//intent.setData(uri);  
//单独以 seType 方法设置 Type  
//intent.setType("text/plain");  
//上面分步骤设置是错误的,要么以 setDataAndType 方法设置 URI 及 mime type  
intent.setDataAndType(uri, "text/plain");  
startActivity(intent);  

d、隐式跳转之调用系统应用(调用短信程序(无需发送短信权限,用户手动点击发送按钮))

Uri uri = Uri.parse("smsto:12345678900");//指定接收者  
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);  
intent.putExtra("sms_body", "短信测试");  //短信内容
startActivity(intent); 

附Intent七大属性:
component(组件):目的组件
  action(动作):用来表现意图的行动
  category(类别):用来表现动作的类别
  data(数据):表示与动作要操纵的数据
  type(数据类型):对于data范例的描写
  extras(扩展信息):扩展信息
  Flags(标志位):期望这个意图的运行模式

2、【强制】避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作, 应该创建 IntentService 完成, 而不应该在 BroadcastReceiver 内创建子线程去做。
说明: 由于该方法是在主线程执行,如果执行耗时操作会导致 UI 不流畅。可以使用IntentService 、 创 建 HandlerThread 或 者 调 用 Context#registerReceiver (BroadcastReceiver, IntentFilter, String, Handler)方法等方式, 在其他 Wroker 线程执行 onReceive 方法。BroadcastReceiver#onReceive()方法耗时超过 10 秒钟,可能会被系统杀死。
正例

IntentFilter filter = new IntentFilter(); 
filter.addAction(LOGIN_SUCCESS); 
this.registerReceiver(mBroadcastReceiver, filter); 
mBroadcastReceiver = new BroadcastReceiver() { 

  @Override 
  public void onReceive(Context context, Intent intent) { 
    Intent userHomeIntent = new Intent(); 
    userHomeIntent.setClass(this, UserHomeService.class); 
    this.startService(userHomeIntent); 

  } 
};

反例

mBroadcastReceiver = new BroadcastReceiver() { 

@Override 
public void onReceive(Context context, Intent intent) { 
  MyDatabaseHelper myDB = new MyDatabaseHelper(context); 
  myDB.initData(); 
  // have more database operation here 

  } 
}; 

3、【 推 荐 】 添 加 Fragment 时 , 确 保 FragmentTransaction#commit() 在 Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。 不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替, 任何 commitAllowingStateLoss()的使用必须经过 code review,确保无负面影响。
说明:
Activity 可能因为各种原因被销毁,Android 支持页面被销毁前通过Activity#onSaveInstanceState() 保 存 自 己 的 状 态 。 但 如 果FragmentTransaction.commit()发生在 Activity 状态保存之后,就会导致 Activity 重 建、恢复状态时无法还原页面状态, 从而可能出错。为了避免给用户造成不好的体验,系统会抛出 IllegalStateExceptionStateLoss 异常。 推荐的做法是在 Activity 的onPostResume() 或 onResumeFragments() ( 对 FragmentActivity ) 里 执 行 FragmentTransaction.commit(), 如有必要也可在 onCreate()里执行。不要随意改用FragmentTransaction.commitAllowingStateLoss() 或 者 直 接 使 用 try-catch 避 免 crash, 这不是问题的根本解决之道,当且仅当你确认 Activity 重建、恢复状态时,本次 commit 丢失不会造成影响时才可这么做。
正例

public class MainActivity extends FragmentActivity { 
  FragmentManager fragmentManager; 
  @Override 
  protected void onCreate(Bundle savedInstanceState) { 

    super.onCreate(savedInstanceState); 
    setContentView(R.layout.activity_main2); 

    fragmentManager = getSupportFragmentManager(); 
    FragmentTransaction ft = fragmentManager.beginTransaction(); 
    MyFragment fragment = new MyFragment(); 
    ft.replace(R.id.fragment_container, fragment); 
    ft.commit(); 

  } 
}

反例

public class MainActivity extends FragmentActivity { 
  FragmentManager fragmentManager; 
  @Override 
  public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) 
  { 
    super.onSaveInstanceState(outState, outPersistentState); 
    fragmentManager = getSupportFragmentManager(); 
    FragmentTransaction ft = fragmentManager.beginTransaction(); 
    MyFragment fragment = new MyFragment(); 
    ft.replace(R.id.fragment_container, fragment); 
    ft.commit(); 

  } 
} 

4、【推荐】总是使用显式Intent启动或者绑定 Service,且不要为服务声明 Intent Filter,保证应用的安全性。如果确实需要使用隐式调用,则可为 Service提供 Intent Filter并从 Intent中排除相应的组件名称,但必须搭配使用 Intent#setPackage()方法设置Intent的指定包名, 这样可以充分消除目标服务的不确定性。
5、【推荐】Service需要以多线程来并发处理多个启动请求,建议使用IntentService,可避免各种复杂的设置。
说明:
Service组件一般运行主线程,应当避免耗时操作,如果有耗时操作应该在 Worker线程执行。可以使用 IntentService执行后台任务。
正例

public class SingleIntentService extends IntentService { 
  public SingleIntentService() { 
    super("single-service thread"); 
  } 

  @Override 
  protected void onHandleIntent(Intent intent) { 
    try { 
    ...... 
    } catch (InterruptedException e) { 
    e.printStackTrace(); 
    } 

  } 
}

反例

public class HelloService extends Service { 
  ... 
  @Override 
  public int onStartCommand(Intent intent, int flags, int startId) { 

    Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show(); 

    new Thread(new Runnable() { 
      @Override 
      public void run() { 
        //操作语句 
      } 
      }).start(); 
      ... 
  } 
} 

6、【推荐】不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的 销毁和停止,因为 onDestroy()执行的时机可能较晚。 可根据实际需要,在 Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。(请结合第7条一起查看)
7、【强制】 Activity或者Fragment中动态注册BroadCastReceiver时,registerReceiver()和 unregisterReceiver()要成对出现。
说明:
如果 registerReceiver()和 unregisterReceiver()不成对出现,则可能导致已经注册的 receiver没有在合适的时机注销,导致内存泄漏,占用内存空间, 加重 SystemService负担。
部分华为的机型会对 receiver进行资源管控,单个应用注册过多 receiver会触发管控模块抛出异常,应用直接崩溃。
正例

public class MainActivity extends AppCompatActivity { 
  private static MyReceiver myReceiver = new MyReceiver(); 
  ... 
  @Override 
  protected void onResume() { 
    super.onResume(); 
    IntentFilter filter = new IntentFilter("com.example.myservice"); 
    registerReceiver(myReceiver, filter); 
  } 

  @Override 
  protected void onPause() { 
    super.onPause(); 
    unregisterReceiver(myReceiver); 
  } 
  ... 
}

反例

public class MainActivity extends AppCompatActivity { 
  private static MyReceiver myReceiver; 
  @Override 
  protected void onResume() { 
    super.onResume(); 
    myReceiver = new MyReceiver(); 
    IntentFilter filter = new IntentFilter("com.example.myservice"); 
    registerReceiver(myReceiver, filter); 
  } 

  @Override 
  protected void onDestroy() { 
    super.onDestroy(); 
    unregisterReceiver(myReceiver); 
  } 
} 

Activity的生命周期不对应,可能出现多次 onResume造成 receiver注册多个,但最终只注销一个,其余 receiver产生内存泄漏。

二、UI与布局

1、【推荐】在Activity中显示对话框或弹出浮层时,尽量使用DialogFragment,而非Dialog/AlertDialog,这样便于随Activity生命周期管理 对话框 /弹出浮层的生命周期。
正例:

public void showPromptDialog(String text) { 

  DialogFragment promptDialog = new DialogFragment() { 
    @Override 
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle 
    savedInstanceState) { 
      getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); 
      View view = inflater.inflate(R.layout.fragment_prompt, container); 
      return view; 
    } 
  }; 
  promptDialog.show(getFragmentManager(), text); 

}

2、【推荐】文本大小使用单位 dp,View大小使用单位 dp。对于 TextView,如果在文字大小确定的情况下推荐使用 wrap_content布局, 以避免出现文字显示不全的适配问题。
说明:
之所以文本大小也推荐使用dp而非 sp,因为 sp是 Android早期推荐使用的,但其实sp不仅和 dp一样受屏幕密度的影响,还受到系统设置里字体大小的影响, 所以使用 dp对于应用开发会更加保证 UI的一致性和还原度。
3、【推荐】在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局layout刷新:
1)设置固定的View大小的宽高,如倒计时组件等;
2)调用 View的 layout方法修改位置,如弹幕组件等;
3)通过修改Canvas位置并且调用invalidate(int l, int t, int r, int b)等方式限定刷新区域;
4)通过设置一个是否允许requestLayout的变量,然后重写控件的requestlayout、onSizeChanged方法,判断控件的大小没有改变的情况下,当进入 requestLayout的时候,直接返回而不调用super的requestLayout方法。
3、【推荐】使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示 Toast 时不能取消上一次 Toast 消息的情况。 即使需要连续弹出 Toast,也应避免直 接调用 Toast#makeText。

三、进程、线程与消息通讯

1、【强制】不要通过 Intent在Android基础组件之间传递大数据(binder transaction缓存为1MB),可能导致 OOM。
2、【强制】新建线程时,必须通过线程池提供(AsyncTask或者ThreadPoolExecutor或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。
说明:
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致 消耗完内存或者 “过度切换”的问题。另外创建匿名线程不便于后续的资源使用分析,对性能分析等会造成困扰。
正例:

int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); 
int KEEP_ALIVE_TIME = 1; 
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; 
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>(); 
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, 
NUMBER_OF_CORES*2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, taskQueue, 
new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler()); 
//执行任务 
executorService.execute(new Runnnable() { 
  ... 
});

反例:

new Thread(new Runnable() { 
  @Override 
  public void run() { 
    //操作语句 
    ... 
  } 
}).start(); 

3、【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:
Executors返回的线程池对象的弊端如下:
1) FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
2) CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
正例:

int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); 
int KEEP_ALIVE_TIME = 1; 
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; 
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>(); 
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, 
NUMBER_OF_CORES*2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, 
taskQueue, new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler());

反例:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); 

四、Bitmap

  1. 【推荐】应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,直接显示原图,并不会增加视觉上效果,但是却会耗费大量宝贵的内存。
    正例:
     public static Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
         //1.获得图片的尺寸
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inJustDecodeBounds = true;//为true可以避免将图片加载到内存中
         BitmapFactory.decodeResource(res, resId, options);
    
         //2.根据图片的分辨率以及我们实际需要展示的大小,计算压缩率
         options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
         //设置压缩率,并解码
         options.inJustDecodeBounds = false;
    
         return BitmapFactory.decodeResource(res, resId, options);
     }
        
     //计算图片的压缩比
     public static int calculateInSampleSize(BitmapFactory.Options options,
                                             int reqWidth, int reqHeight) {
         final int height = options.outHeight;
         final int width = options.outWidth;
         int inSampleSize = 1;
    
         if (height > reqHeight || width > reqWidth) {
             //四舍五入计算高度压缩比
             final int heightRatio = Math.round((float) height
                     / (float) reqHeight);
             final int widthRatio = Math.round((float) width / (float) reqWidth);
             //去两者中的较大值
             inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
         }
         return inSampleSize;
     }
    

    反例:
    不压缩显示原图

更多可参考 《阿里巴巴android开发手册》