前言
前几天读完了 Android 开发艺术探索,这篇文章主要是记录下其中每个章节的重点,便于以后复习。
Activity 的生命周期与启动模式
Activity A 启动 Activity B,生命周期调用顺序为 A.onPause -> B.onCreate -> B.onStart -> B.onResume -> A.onStop 因此不能在 onPause 里面做耗时任务,这会导致新 Activity 延迟显示。
onStart 调用完后 Activity 就可见了,onResume 调用完后 Activity 就可以与用户交互了,但是注意由于 View 的三大流程是在 onResume 回调完毕后才进行的,因此 onResume 调用完了,View 也是不会显示的,此时获取宽高都是 0。
Activity 被意外杀死或者当配置发生改变后,系统会调用 onSaveInstanceState(API 28 后在 onStop 后调用,API 28以下在 onStop 前调用),这里可以保存下信息,然后在 onRestoreInstance 中恢复。
Activity 的 onCreate 方法参数 bundle 可能为 null ,但是 onRestoreInstance 方法参数 bundle 一定不为空。
Activity A 启动 Activity B (standard 模式),会将 Activity B 放入 Activity A 所在的栈中,因此使用 Application.startActivity 需要添加 flag ,因为系统不知道将其放入哪个栈中。
Luncher 应用程序在开启一个应用时,会对 Intent 设置 Intent.ACTIVITY_NEW_TASK ,所以其它应用程序不会和 Luncher 运行在一个任务栈中。
SingleTask 启动模式,表示整个系统只能有该 Activity 的一个实例,并且启动它的时候附带清除 Top 的效果,比如当前栈中存在 ABCD 四个 Activity,这时候启动 B,栈中就变成了 AB,CD被清除了,并且还会调用 B.onNewIntent ,该启动模式的 Activity 始终运行在 taskAffinity 所指定的栈中。
SingleInstance 启动模式,表示整个系统只能有该 Activity 的一个实例,启动它的时候会新开一个任务栈,该栈中只会存在它这么一个 Activity,再次启动它如果该栈没有销毁会调用其 onNewIntent 方法。
隐式启动的 Activity 必须加上默认的 category,因为当调用 startActivity、startActivityForResult 时系统会验证是否含有默认的 category,如果不加 Activity 就无法启动了(也就是说至少要有一个默认的 category, 入口 Activity 是特例不需要默认 category )
可以通过以下 adb 指令查看当前所有的任务栈:
adb shell dumpsys activity activities
IPC 机制
每个 Android 进程都含有一个虚拟机实例,而每个虚拟机加载同一个类时都会新建一个 Class 实例,因此进程一修改了 A 类中静态值,但是进程二获取的还是原来的值。
开启多个进程会导致 Application 被创建多个,由于每次启动组件时,系统都会判断 Application 对象是否存在,而显然非主进程的 Application 对象不存在,因此会创建。
Android 进程间通信的方式主要有以下几种:
- 使用 Intent 传递数据。
- 使用共享文件和 SharedPreference。
- 使用 AIDL 、Messenger、ContentProvider,基于 Binder。
- 使用 Socket 。
将对象序列化保存到文件中,要求该类必须实现 serializable 接口,然后使用 ObjectInputSteam 写入,使用 ObjectOutputSteam 读取。
实现了 serializable 接口的类最好指定 serialVersionUID,若不指定编译器就会根据当前类自动加上该字段,当类字段发生变化时就会反序列化失败,因为序列化时的 UID 与当前类的UID不同,手动指定就不会导致该问题.当然类结构比如变量类型发生变化反序列化也会失败。
序列化的时候不会把静态变量和 transient (作用就是不参与序列化)标记的成员变量序列化,因为静态变量属于类而不属于对象。
实现了 parcelable 接口的类 writeToParcel 和 createFromParcel 的读写顺序必须要一致,不然会出错,parcelable 原理是使用共享内存,当使用内存序列化时使用 Parcelable,其他情况使用 Serialable,毕竟 Serialable 使用简单,但是开销大。
Binder 对于 framework 来说是用于连接各种 Manger(ActivityManager、WindowManager) 和相应的 ServiceManager(AMS、WMS) 的桥梁,其跨进程传递的数据只能是 Parcelable 以及基本数据类型。
Intent 能传输的数据类型包括 Serialable、Parcelable、CharSequence 以及基本数据类型。
API 21 及以上要求 bindService、startService 需要调用 intent.setPackage(目标 Service 包名),因为 ContextImpl 中做了判断:
private void validateServiceIntent(Intent service) {
if (service.getComponent() == null && service.getPackage() == null) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
IllegalArgumentException ex = new IllegalArgumentException(
"Service Intent must be explicit: " + service);
throw ex;
} else {
Log.w(TAG, "Implicit intents with startService are not safe: " + service
+ " " + Debug.getCallers(2, 3));
}
}
}AS 使用 aidl 遇到的坑 https://www.cnblogs.com/rookiechen/p/5352053.html,要使用自定义的类必须让该类实现 Parcelable 接口并且创建同名 .aidl 文件声明自己,最后在主 aidl 中引用。除此之外除基本数据类型,其他类型类型都得标上方向: in、out、或者 inout,并且 aidl 里面不能声明静态变量。
AIDL 服务端程序验证客户端程序,可以通过自定义权限或者调用 getCallingUid 获取包名。
AIDL 跨进程注册监听,需要了查看 https://blog.csdn.net/HJXASLZYY/article/details/52204379。
AIDL 编译后生成的类如下:
public interface IBookManager extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements com.hfw.androidtest.IBookManager {
private static final java.lang.String DESCRIPTOR = "com.hfw.androidtest.IBookManager";
// 1
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
public static com.hfw.androidtest.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
// 2
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.hfw.androidtest.IBookManager))) {
return ((com.hfw.androidtest.IBookManager) iin);
}
// 3
return new com.hfw.androidtest.IBookManager.Stub.Proxy(obj);
}
public android.os.IBinder asBinder() {
return this;
}
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList: {
data.enforceInterface(DESCRIPTOR);
java.util.List<com.hfw.androidtest.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
// 4
data.enforceInterface(DESCRIPTOR);
com.hfw.androidtest.Book _arg0;
if ((0 != data.readInt())) {
_arg0 = com.hfw.androidtest.Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.hfw.androidtest.IBookManager {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
public java.util.List<com.hfw.androidtest.Book> getBookList() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.hfw.androidtest.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.hfw.androidtest.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
public void addBook(com.hfw.androidtest.Book book) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book != null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public java.util.List<com.hfw.androidtest.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.hfw.androidtest.Book book) throws android.os.RemoteException;
}主要调用流程为:
- 服务端先调用 Binder binder = new IBookManager.Stub() 实现 getBookList、addBook,接着在 onBind 方法中返回 binder 。
- 客户端在 onServiceConnected 回调中拿到 IBinder 对象,调用 IBookManager.Stub.asInterface,拿到 IBookManager 实例。内部原理是,如果是同一个进程那么直接把服务端返回的 binder 返回,这点可以根据代码 1、2 看出,如果不是同一个进程那么根据代码 3 新建一个 Proxy 对象并返回。
- 客户端调用 binder.addBook,如果是同一个进程相当于普通的方法调用,如果不是同一个进程则执行到 Proxy.addBook,首先获取两个 Parcel 对象 _data、_reply 其中前者用来传递参数,后者用于接收参数,接着写入 1,表示一个参数,再将 book 实例写入 _data,然后调用 mRemote.transact 传入调用的方法索引,以及两个 Parcel 对象,注意这个的 mRemote 为 onServiceConnected 返回的 BinderProxy 对象。
- 调用 transact 方法后会挂起当前线程,接着会在服务端调用 binder.onTransact 方法,根据传入的方法索引,执行代码 4,从 Parcel 对象 data 中取出参数 Book 实例,执行服务端实现的 addBook 方法,由于这个方法无返回值所以不需要写入返回值到 reply 对象,接着客户端线程停止挂起继续执行。
View 的事件体系
当两次滑动距离小于该值时系统不认为滑动,可以通过以下代码获取:
ViewConfiguration.get(getContext()).getScaledTouchSlop();
VelocityTracker 用于获取水平或垂直的速度典型代码如下所示:
public boolean onTouchEvent(MotionEvent event) {
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
Log.d(TAG, "onTouchEvent: x="+velocityTracker.getXVelocity()+";y="+velocityTracker.getYVelocity());
return super.onTouchEvent(event);
}实现 View 移动可以使用以下几种方法:
- scrollTo、scrollBy。
- 修改 View 的 layoutParams。
- 修改 View 的 transactionX、transactionY。
- offsetLeftAndRight、offsetTopAndBottom。
scrollTo 与scrollBy 这两个方法都是移动 View 内的内容比如调用 Linearlayout.scrollTo 是将 Linearlayout 内部的内容移动,当调用 TextView.scrollTo 时是将其上的文字移动 TextView 本身不移动,同时参数 x 为正时向左移动,y 为正时向上移动。两者的区别时前者是相对于初始位置,连续调用同样参数无效,后者相对于当前位置可以重复执行(内部也是调用前者只是 x, y 每次都改变)。
使用 View 动画不会改变 View 的真实位置,只是用户看起来改变了而已,对于系统来说 View 的位置是没有改变的,并且如果不设置 setFillAfter(true) 在动画结束后又会回到原位置,因此 button 在动画结束的位置触发不了单击事件,而在初始位置可以触发。
当改变了一个 View 的 layoutParams 时有两种方法可以生效,一种是再设置回去 View.setLayoutParams ,另一种是调用 View.requestLayout。
实现 View 弹性滑动的方式有 Scroller、ObjectAnimator、延迟策略(滑动以下睡眠一下)。
Scroller 原理是 startScroll 以后会调用 invalidate ,然后会再调用 View.onDraw ,onDraw 里面会再调用computeScroll 该方法在 View 内是空方法。
View 的 clickable 或 longclickable 有一个为 true 就会消耗点击事件,setClickListener 会把 clickable 设置为true,setLongClickListener 同理。
View 的工作原理
ViewRoot 的作用主要是与 WMS 通信绘制 GUI 和给 DecorView 分发用户事件。
invalidate 会调用onDraw,requestLayout 会调用 onMeasure 、onLayout() 不调用 onDraw。
MeasureSpec 分为以下三种:
- UNSPECIFIED 容器不对 View 的大小做任何限制,子 View 要多大给多大,一般用于系统内部,比如ScrollView 对子 View 的高度。
- EXACTLY 确定值,父容器已经检测出当前 View 所需的大小,对应于 LayoutParams 里面的 match_parent 以及确定值。
- AT_MOST 父容器指定了一个大小,子 View 不能超过这个大小对应于 layoutParams 里面的wrap_content。
onMeasure 执行完后 getMeasuredWidth、getMeasureHeight 才能取到,其值大部分情况下等于getWidth、getHeight、最好是在 onLayout 后去获取最终高度。
View 真正进行 measure layout draw 是在 Activity 的 onResume 执行完后,因此 onResume 时布局并不可见。
View 的宽高在 onCreate、onStart、onResume 里面都无法获取到,可以通过以下几种方式获得:
- 在 Activty、View 的 onWindowFocusChanged 里面获取。
- 添加 ViewTreeObserver。
- View.post。
- View.measure。
直接继承自 View 的自定义 View 需要注意解决 wrap_content 情况因为如果不做处理 wrap_content 将于match_parent 效果一致,详见 View.getDefaultSize,此外还必须处理 padding (不需要处理 margin,margin 由父控件处理)处理方法就是在 onDraw 里面考虑内边界。
View 被从 Window 中移除时会调用 View.onDetachedFromWindow 该方法内部可以做一些资源回收,停止线程的操作。
RemoteViews 用于 Notification 中的自定义 View 以及 Widget,而其归根到底其实都是在 SystemServer 进程里面加载的,所以是跨进程的。
创建 Widget 步骤:
新建一个布局文件比如 a.xml。
在res里面新建文件夹 xml,再在该文件夹下面新建一个布局比如 b.xml 里面代码如下所示:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/a"
android:minHeight="84dp" android:minWidth="84dp"
android:updatePeriodMillis="5000000"><!--自动更新时间-->
</appwidget-provider>新建一个类继承自 AppWidgetProvider,小组件的点击事件在 onUpdate 里面进行设置,该方法会在每个小组件添加时就自动调用一次。
在Manifast文件中进行注册如下所示:
<receiver android:name=".MyAppWidget">
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/app"/>
<intent-filter>
<action android:name="anniu" /> <!--下行必须加不然小组件将无法显示-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
RemoteViews 里面只能使用一些系统内置的控件,其子控件及自定义控件都无法使用。
RemoteViews 的原理,当我们调用 RemoteViews.setXXX 时系统都会创建一个 Action,并将其放入一个ArrayList 中进行保存,只有调用对应 manager 中的更新方法时才会调用 RemoteViews 里面的 apply 方法,该方法里面再一一执行 action 的 apply 方法,这样就能避免多次 IPC 请求。
获取本应用中的资源文件可以通过 getResource().getIdentifer(“布局文件名字”, “layout”, getPackageName()) 获取。
Android Drawable
- selector 文件系统是根据从上到下的顺序来匹配 drawable 的所以必须把默认图案的 drawable 放到最下面,不然将一直显示默认图片。
Android 动画深入分析
Android的动画分为三种:View动画、帧动画、属性动画。View 动画既可以是单种动画,也可以是四种动画组合。组合动画所对应的类是 AnimationSet类,推荐在xml里面定义动画。
帧动画在 res/drawable 目录下创建一个 xml 文件根节点为 animation-list 然后在代码中View.setBackgroundResource(),然后再View.getBackground().start()。
LayoutAnimation 的使用先在 anim 目录下新建一个以 layoutAnimation 为根节点的 xml 文件,其中有个属性animation 需要依赖一个其他的 animation,然后在布局文件中为 ViewGroup 设置 layoutAnimation 就行了(案例是给 ListView 设置,会出现刚刚载入布局时每个 item 有序的从右到左从完全透明到完全不透明的动画)。
Activity 的切换动画 使用 overridePendingTransition 指定开启动画以及暂停动画,该方法必须紧挨startActivity 或 finish 后面否则将不起作用,其中 exitAnim 动画是给需要消失的 Activity 使用的。
ObjectAnimator 继承自 ValueAnimator,AnimatorSet 代表动画集合。ObjectAnimator 用于改变一个对象的属性并且赋予动画,ValueAnimator需要自己设置监听自己更改,可以通过 xxxAnimator.ofXXX 获取, AnimatorSet 使用 playTogether(ValueAnimator…) 或者 playSequentially(),如果要对 Color 使用属性动画要设置:
setEvaluator(new ArgbEvaluator());
ObjectAnimator 要求对象必须要有 setXXX 的方法,因为原理就是根据时间来生成一个值然后一个个通过反射调用 setXXX 方法设置,并且如果 value 可变参数只指定一个那么该对象就必须要有 getXXX 方法因为当属性动画开始时系统会通过反射调用该方法,如果找不到该方法,程序会直接崩溃。
ValueAnimator 设置 AnimatorListener 时如果不需要实现所有的回调应该使用 AnimatorListenerAdapter。
当属性动画显示出的效果不如预期,得先看看是否该对象的 set 和 get 方法在操作同一个属性,如果不是可以通过包装类进行实现。
理解 Window 和 WindowManager
Activity 内可以直接调用 getWindowManager 获取 WindowManager 实例。
Activity 其实也是通过调用 WindowManager.addView 添加的(在 ActivityThread.handleResumeActivity 里面。
通过 WindowManager 显示一个全局的悬浮窗代码如下:
if (Build.VERSION.SDK_INT >= 23) {
if (Settings.canDrawOverlays(this)) {
showWindow();
} else {
//打开设置去授权悬浮框权限
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivityForResult(intent,1);
}
} else {
showWindow();
}
private void showWindow() {
Button bt_window = new Button(this);
bt_window.setText("我是悬浮");
WindowManager wm = (WindowManager) getSystemService(Service.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(-2,-2,0,0, PixelFormat.TRANSPARENT);
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |//表示自己不需要获取焦点,事件向下传递,同时拥有下面的flag特性
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |//表示范围内的事件接受范围外不接受
WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING;//已经过时
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;//type必须设置,并且该type需要加上权限 system_alert_window
if (wm != null) {
wm.addView(bt_window,params);
}
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 1){
if(Build.VERSION.SDK_INT >= 23){
if(Settings.canDrawOverlays(this)){
showWindow();
}
}
}
}
WindowManager 主要有三个重要方法 addView、removeView、updateViewLayout 其中 addView 通过IPC调用 WMS.addView ,removeView 分为两个方法 removeView、removeImmediate 前者只是发了一个消息就返回了,不一定立马执行,后者直接执行,建议使用前者。
PhoneWindow 里面维护了一个 DecorView 对象,可以设置 Callback (比如 Activity.attach 就设置了,回调中包括触摸事件等)。
Toast 对 NMS发出请求,NMS 在能弹出的时候通过 IPC 回调 Tn.show,方法运行在 binder 线程池中需要通过 Handler 转到原来的线程,因为 Handler 需要 Looper,所以 Toast 无法在没 Looper 的线程中使用。
四大组件的工作过程
- 当程序中使用 startActivity、startService、bindService 时会通过 IPC 调用 AMS 里面的对应的方法后经过一系列流程通过调用 ApplicationThread 里面的方法,最后调到 H 类里面对应的方法。
- Service 的 start 过程,ComtextImpl.startService -> 验证Intent内是否包含了包名 -> AMS.startService -> 经过一系列操作后发送一个 message 让目标应用程序的 ActivityThread 创建 Service 对象,并且调用 Service.onCreate() -> 目标应用程序告诉 AMS 已经执行完毕 -> AMS 再给 ActivityThread 发送消息调用 Service.onStartCommand()。
- Service 的 bind 过程,ComtextImpl.bindService -> 验证Intent内是否包含了包名 -> 将 ServiceConnection对象进行包装成 Binder对象 -> AMS.bindService -> 经过一系列操作后发送一个 message 让目标应用程序的 ActivityThread 创建 Service 对象 并且调用 Service.onCreate -> 目标应用程序告诉 AMS 已经执行完毕 -> AMS 给 client 发送消息调用 Service.onBind -> client 告诉 AMS 执行完毕并且把 onBind 返回的对象传递给 AMS -> AMS通过IPC调用 ServiceConnection.connected。
Android 的消息机制
- Handler 作用就是把当前线程切换到 Handler 里面的 Looper 所在的线程,比如在主线程创建 Handler,在子线程获取数据再通过 Handler 切换到主线程执行,要使用 Handler 当前线程必须要先含有 Looper。
- ThreadLocal.get 可以取出当前线程中所 set 的值,在 android 消息机制中用于保存每个线程的 Looper。
- Looper.loop 死循环唯一跳出这个循环的条件是 messageQuene.next 返回null (只有调用了 Looper.quit 方法才能退出循环),当取出消息后,会进行 dispatch 然后再调用 meassageQuene.next 再阻塞。
- 使用 Handler 时如果不想创建其子类的实例可以使用 handler.post(Runnable),或者在创建 Handler 对象时传入一个 Callback 对象。
Android 的线程和线程池
IntentService 处理逻辑一般在 onHandleIntent 中处理,该方法是在子线程中运行的,当该方法运行完并且没有再次 startService 会自动销毁该 service,多次 startService 会多次调用 onHandleIntent 。
AsyncTask 的使用方式
class MyUpdateTask extends AsyncTask<Void,Integer,Void>{
protected void onPreExecute() {
super.onPreExecute();//主线程
}
protected Void doInBackground(Void... voids) {
for(int i = 1; i < 100; i++){
SystemClock.sleep(100);// 该方法运行于子线程
publishProgress(i);// 该方法会进行进程切换调用主线程的 onProgressUpdate()
if(isCanceled()){
break; //取消了把线程结束
}
}
return null;//返回值作为onPostExecute()的参数
}
protected void onPostExecute(Void result) {
super.onPostExecute(result)//主线程
}
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
pb.setProgress(values[0]);
}
}
- AsyncTask 的注意点
- AsuncTask 对象必须在主线程中创建,API 21 以下如果不在主线程创建会崩溃。
- AsyncTask.execute 也必须在主线程中调用。
- AsyncTask.execute 只能调用一次,再次调用会报异常,因为会在调用 execute 时检查当前的状态。
- AsyncTask.execute()是串行执行的当一个任务执行完,才会执行下一个任务,如果需要并行执行可以调用AsyncTask.executeOnExecutor。
- 不要在程序中显示的调用上面 4 个方法。
Bitmap 的加载和缓存
Bitmap 加载前需要进行缩放,因为一般 ImageView 的大小都比图片的小,可以使用 BitmapFactory.options里面的 inSampleSize,当该参数 <= 1 时不缩放,为2时宽高都为原来的 1/2 ,占用空间变为原来的 1/4。
Bitmap 压缩加载防止 OOM 的步骤如下:
BitmapFactory.Options.inJustDecodeBounds 设置为 true 去加载图片,该参数作用是不把图片加载进内存,但可以获取其宽高。
BitmapFactory.Options 中取出图片的原始宽高信息使用 outWidth、outHeight.
根据 ImageView 大小以及原图大小计算出 inSampleSize。
将 inJustDecodeBounds 设置为 false,然后重新加载图片。
public static Bitmap decodeSampledBitmapFromResource(Resources resources,int resId, int reqWidth, int reqHieght){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options); //为了获取原始图片的宽高
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHieght);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, resId, options);
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHieght) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if(height > reqHieght || width > reqWidth){
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while((halfHeight / inSampleSize) >= reqHieght && (halfWidth / inSampleSize) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
综合技术
捕获全局异常可以通过创建一个实现 UncaughtExceptionHandler 的类,然后调用Thread.setDefaultUncaughtException 来捕获全局崩溃信息。
一个 dex 文件的方法数量最多为 65536 个,当超过后需要进行如下设置。
- defaultConfig 中加入 multiDexEnabled = true。
- dependencies 中加入依赖 compile ‘com.android.support:multidex:1.0.0’。
- 应用的 Application 使用 MultiDexApplication 或其子类。