导航号,我的单页导航
文章目录

前言

下载了《阿里Android手册》,看了一下,很多规定还是很合理的。尤其是一些命名的规定,虽然繁琐,但对于后续查阅代码还是很爽的。

今天有空,记录一些觉得很不错的内容,方便自己查阅。

正文

layout 文件的命名

Activity 的 layout 以 module_activity 开头
Fragment 的 layout 以 module_fragment 开头
Dialog 的 layout 以 module_dialog 开头
include 的 layout 以 module_include 开头
ListView 的行 layout 以 module_list_item 开头
RecyclerView 的 item layout 以 module_recycle_item 开头
GridView 的行 layout 以 module_grid_item 开头

这个建议不错,我只做到了一半,后面加强学习一下。

drawable 资源名称

drawable 资源名称以小写单词+下划线的方式命名

模块名业务功能描述控件描述控件状态限定词

module_login_btn_pressed
module_tabs_icon_home_normal

不过推荐简写,比如pressed = p, normal = n

module_login_btn_p
module_tabs_icon_home_n

anim 资源名

资源名称以小写单词+下划线的方式命名,采用以下规则:

模块名_逻辑名称_[方向|序号]
tween 动 画 资 源

尽可能以通用的动画名称命名,如module_fade_in,module_fade_out,module_push_down_in(动画+方向);

frame 动画资源

尽可能以模 块+功能命名+序号。

如: module_loading_grey_001

这个命名不错,但多数人不会这么干

Id资源命名

Id资源原则上以驼峰法命名,View组件的资源id需要以View的缩写作为前缀 。

LinearLayout        ll
RelativeLayout      rl
ConstraintLayout    cl
ListView            lv
ScollView           sv
TextView            tv
Button              btn
ImageView           iv
CheckBox            cb
RadioButton         rb
EditText            et

其它控件的缩写推荐使用小写字母并用下划线进行分割 ,比如:

ProgressBar 对应的缩写为 progress_bar
DatePicker 对应的缩写为 date_picker

Activity间的数据通信

对于数据量比较大的,避免使用 Intent + Parcelable的方式,可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException。

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) {
        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            e.printStackTrace();
        }
    }
}

避免在onReceive()中执行耗时操作

如果有耗时工作,应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。

由于该方法是在主线程执行,如果执行耗时操作会导致 UI 不流畅。可以使用IntentService 、 创 建 HandlerThread 或 者 调 用 registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)方法等方式。 BroadcastReceiver#onReceive()方法耗时超过 10 秒钟,可能会被系统杀死。

避免使用隐式广播

使用隐式广播会可能被拦截,如果是发送的有序广播,拦截接收后是不会被需要的接收。

如果是应用内,可以考虑本地广播,LocalBroadcastManager#sendBroadcast() 。

应用内推荐本地广播

对于只用于应用内的广播,优先使用 LocalBroadcastManager 来进行注册和发送, LocalBroadcastManager 安全性更好,同时拥有更高的运行效率。

不要在onDestroy()内执行释放资源的工作

例如一些工作线程的销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在onPause()/onStop()中结合 isFinishing()的判断来执行。

不建议onPause做耗时较长的工作

当前Activity的onPause方法执行结束后才会执行下一个Activity的onCreate 方法,所以在 onPause 方法中不适合做耗时较长的工作,这会影响到页面之间的跳 转效率。

不要在Application 对象中缓存数据

不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享请使用 Intent 等机制,也可使用 SharedPreferences 等数据持久化机制。

使用Toast 时,建议定义一个全局的 Toast 对象

使用Toast 时,建议定义一个全局的Toast 对象,这样可以避免连续显示Toast 时不能取消上一次 Toast 消息的情况。

如果你有连续弹出 Toast 的情况,避免使用 Toast.makeText。

多层嵌套,推荐用RelativeLayout

布局中不得不使用 ViewGroup 多重嵌套时,不要使用 LinearLayout 嵌套,改用 RelativeLayout,可以有效降低嵌套数。

推荐多使用约束布局ConstraintLayout。

在 Activity 中显示对话框或弹出浮层时,尽量使用 DialogFragment,

在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);
}

灵活使用布局

灵活使用布局,推荐 Merge、 ViewStub 来优化布局,尽可能多的减少 UI布局层级,推荐使用 FrameLayout, LinearLayout、 RelativeLayout 次之。

避免引发全局 layout刷新

在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局 layout刷新

  1. 设置固定的 view 大小的高宽,如倒计时组件等;

  2. 调用 view 的 layout 方式修改位置,如弹幕组件等;

  3. 通过修改 canvas 位置并且调用 invalidate(int l, int t, int r, int b)等方式限定刷新 区域;

  4. 通过设置一个是否允许 requestLayout 的变量,然后重写控件的 requestlayout、 onSizeChanged 方 法 , 判 断 控 件 的 大 小 没 有 改 变 的 情 况 下 , 当 进 入 requestLayout 的时候,直接返回而不调用 super 的 requestLayout 方法。

不能在 Activity 没有完全显示时显示 PopupWindow 和 Dialog

尽量不使用AnimationDrawable

尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,并且还不能释放,释放之后下次进入再次加载时会报错。

Android 的帧动画可以使用 AnimationDrawable 实现,但是如果你的帧动画中如果包含过多帧图片,一次性加载所有帧图片所导致的内存消耗会使低端机发生 OOM异常。

帧动画所使用的图片要注意降低内存消耗,当图片比较大时,容易出现 OOM。

图片数量较少的 AnimationDrawable 还是可以接受的 。

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot
="true">
<item android:duration="500" android:drawable="@drawable/ic_heart_100"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_75"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_50"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_25"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_0"/>
</animation-list>

推荐看《Android 帧动画OOM问题优化

不能使用ScrollView 包裹 ListView/GridView/ExpandableListVIew

因为这样会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图面。

ScrollView 中嵌套 List 或 RecyclerView 的做法官方明确禁止。除了开发过程中遇到的各种视觉和交互问题,这种做法对性能也有较大损耗。 ListView 等 UI 组件自身有垂直滚动功能,也没有必要在嵌套一层 ScrollView。目前为了较好的 UI 体验,更贴近 Material Design 的设计,推荐使用 NestedScrollView。

不要通过Intent在 Android 基础组件之间传递大数据

不要通过Intent在 Android 基础组件之间传递大数据(binder transaction缓存为 1MB),可能导致 OOM

不允许在应用中自行显式创建线程

新建线程时,必须通过线程池提供(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() {
    ...

线程池不允许使用 Executors 去创建

线程池不允许使用 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());

不要在非 UI 线程中初始化 ViewStub,否则会返回 null

新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查

public class MyThread extends Thread {
public MyThread(){
	super.setName("ThreadName"); … 
	}
}

ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放

谨慎使用 Android 的多进程

多进程虽然能够降低主进程的内存压力,但会遇到如下问题 :

  1. 不能实现完全退出所有 Activity 的功能;

  2. 首次进入新启动进程的页面时会有延时的现象( 有可能黑屏、白屏几秒,是白 屏还是黑屏和新 Activity 的主题有关);

  3. 应用内多进程时, Application 实例化多次,需要考虑各个模块是否都需要在所 有进程中初始化;

  4. 多进程间通过 SharedPreferences 共享数据时不稳定。

SharedPreference 中只能存储简单数据类型

SharedPreference 中只能存储简单数据类型(int,boolean,String等),复杂数据类型建议使用文件、数据库等其他方式存储 。

SharedPreference提交数据时,尽量使用Editor#apply()

SharedPreference 提 交 数 据 时,尽 量 使 用 Editor#apply() ,而 非Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用Editor#commit()。

SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入磁盘,commit 方法是直接写入磁盘。如果频繁操作的话 apply 的性能会优于 commit,apply 会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit。

多线程操作写入数据库时,需要使用事务,以免出现同步问题。

Android 的通过 SQLiteOpenHelper 获取数据库 SQLiteDatabase 实例,Helper 中会自动缓存已经打开的 SQLiteDatabase 实例,单个 App 中应使用SQLiteOpenHelper的单例模式确保数据库连接唯一。

由于SQLite自身是数据库级锁,单个数据库操作是保证线程安全的(不能同时写入), transaction 时一次原子操作,因此处于事务中的操作是线程安全的。

若同时打开多个数据库连接,并通过多线程写入数据库,会导致数据库异常,提示数据库已被锁住。

public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
    ContentValues cv = new ContentValues();
    cv.put("userId", userId);
    cv.put("content", content);
    db.beginTransaction();
    try {
        db.insert(TUserPhoto, null, cv);
        // 其他操作
        db.setTransactionSuccessful();
    } catch (Exception e) {
    // TODO
    } finally {
        db.endTransaction();
    }
}

大数据写入数据库时,请使用事务或其他能够提高 I/O 效率的机制,保证执行速度。

public void insertBulk(SQLiteDatabase db, ArrayList<UserInfo> users) {
    db.beginTransaction();
    try {
        for (int i = 0; i < users.size; i++) {
            ContentValues cv = new ContentValues();
            cv.put("userId", users[i].userId);
            cv.put("content", users[i].content);
            db.insert(TUserPhoto, null, cv);
        }
		// 其他操作
        db.setTransactionSuccessful();
    } catch (Exception e) {
        // TODO
    } finally {
        db.endTransaction();
    }
}

加载大图片或者一次性加载多张图片,应该在异步线程中进行

加载大图片或者一次性加载多张图片,应该在异步线程中进行。图片的加载,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡顿。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
    // 在后台进行图片解码
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = BitmapFactory.decodeFile("some path");
        return bitmap;
    }
...
}

应根据实际展示需要,压缩图片

应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,直接显示原图,并不会增加视觉上的收益,但是却会耗费大量宝贵的内存。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                     int reqWidth, int reqHeight) {
    // 首先通过 inJustDecodeBounds=true 获得图片的尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 然后根据图片分辨率以及我们实际需要展示的大小,计算压缩率
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 设置压缩率,并解码
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

onPause()或 onStop()回调中,关闭当前 activity 正在执行的的动画。

使用 inBitmap 重复利用内存空间,避免重复开辟新内存

    public static Bitmap decodeSampledBitmapFromFile(String filename,
                                                     int reqWidth, int reqHeight, ImageCache cache) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        ...
        BitmapFactory.decodeFile(filename, options);
        ...
        // 如果在 Honeycomb 或更新版本系统中运行,尝试使用 inBitmap
        if (Utils.hasHoneycomb()) {
            addInBitmapOptions(options, cache);
        }
        ...
        return BitmapFactory.decodeFile(filename, options);
    }
    
    private static void addInBitmapOptions(BitmapFactory.Options options,
                                           ImageCache cache) {
		// inBitmap 只处理可变的位图, 所以强制返回可变的位图
        options.inMutable = true;
        if (cache != null) {
            Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
            if (inBitmap != null) {
                options.inBitmap = inBitmap;
            }
        }
    }

使用 ARGB_565 代替 ARGB_888

在不怎么降低视觉效果的前提下,减少内存占用。

android.graphics.Bitmap.Config 类中关于图片颜色的存储方式定义

  1. ALPHA_8 代表 8 位 Alpha 位图

  2. ARGB_4444 代表 16 位 ARGB 位图

  3. ARGB_8888 代表 32 位 ARGB 位图

  4. RGB_565 代表 8 位 RGB 位图

位图位数越高,存储的颜色信息越多,图像也就越逼真。大多数场景使用的是ARGB_8888 和 RGB_565, RGB_565 能够在保证图片质量的情况下大大减少内存的开销,是解决 oom 的一种方法。

但是一定要注意 RGB_565 是没有透明度的,如果图片本身需要保留透明度,那么就不能使用 RGB_565

Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8888 :
Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);

参考文章

  1. 阿里Android手册

© 版权声明
导航号,我的单页导航

暂无评论

暂无评论...