1.图片的刷新问题
刷新太慢:
控制图片的大小、质量
BitmapFactory.decodeFile(filePath, null);
BitmapFactory.decodeStream(inputStream);
BitmapFactory.decodeResource(getResources(),R.drawable.haha);
BitmapFactory.Options options = new BitmapFactory.Options();
首先options.inJustDecodeBounds = true;
再调用BitmapFactory.decodeFile(path,options);得到图片的基本信息
再利用图像的宽/高,得到inSampleSize值,
再将options.inJustDecodeBounds = false;
再将options.inDither = false;
再将options.inPreferredConfig = Bitmap.Config.ARGB_8888;
再调用BitmapFactory.decodeFile(path,options);
2.图片的加载问题
Gallery是监听媒体数据库的变化,及时作出回调加载
查询数据库->封装数据->加载bitmap->生成纹理->界面刷新
每一个界面均存在Loader、Media、View、Window、Render、
3.是否出现OOM,怎么避免?
Android的虚拟机是基于寄存器的Dalvik,它的最大堆大小一般是16M,有的机器为24M。我们平常看到的OutOfMemory的错误,通常 是堆内存溢出。移动开发和web开发的最大的区别是设备资源受限,对一般手机应用,这个资源是相当有限的,堆内存的上限值只有16M。Android的缺 省值是16M(某些机型是24M),而对于普通应用这是不能改的,当应用程序处理大资源的资源,如图片或视频等媒体资源时 ,数量一多,时间一长,这个16M是很容易耗尽的,OOM是很容易出现的。
虽然JAVA有垃圾回收机制,但也存在内存泄露。如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收器就无法回收它,当然 该对象占用的内存就无法被使用,这就造成了内存泄露。如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了。当然java的,内存 泄漏和C/C++是不一样的。如果java程序完全结束后,它所有的对象就都不可达了,系统就可以对他们进行垃圾回收,它的内存泄露仅仅限于它本身,而不 会影响整个系统的。C/C++的内存泄露就比较糟糕了,它的内存泄露是系统级,即使该C/C++程序退出,它的泄露的内存也无法被系统回收,永远不可用 了,除非重启机器。
Android的一个应用程序的内存泄露对别的应用程序影响不大。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都 会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。 Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视 为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会 引起系统重启),这是,我们的应用程序就会崩溃,我们就会看到OOM。
一般而言,android中常见的原因主要有以下几个:
- 1.数据库的cursor没有关闭。
- 2.构造adapter没有使用缓存contentview。
- 3.调用registerReceiver()后未调用unregisterReceiver().
- 4.未关闭InputStream/OutputStream。
- 5.Bitmap使用后未调用recycle()。
- 6.Context泄漏。
- 7.static关键字等。
避免static关键字溢出:
- 第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。
- 第二、Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。
- 第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef;
避免Context溢出
- 第一、将线程的内部类,改为静态内部类。并且注意第二条。
- 第二、在线程内部采用弱引用保存Context引用。
InputStream/OutputStream
这个就不多说了,我们操作完输入输出流都要关闭流
避免bitmap溢出
- 1.Bitmap及时回收
- 2.Bitmap控制大小、质量,BitmapFactory.Options
- 3.使用缓存机制,比如软引用、弱引用
数据库的cursor没有关闭
cursor.close();及时关闭
调用registerReceiver()后未调用unregisterReceiver().
当应用销毁的时候,记得把Broadcast及时关闭。
构造adapter没有使用缓存contentview
当一个listview的子项有成千上万个时,如果我们没有采用一定的策略来重用这些资源,那应用的那点对内存,是远远不够使用的。
在继承BaseAdapter时会让我们重写getView(int position, View convertView, ViewGroup parent)方法,
第二个参数convertView就是我们要用到的重用的对象。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
holder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_gv_picture, null);
holder.iv_Picture = (ImageView) convertView
.findViewById(R.id.iv_Picture);
convertView.setTag(holder);
}
holder = (ViewHolder) convertView.getTag();
holder.iv_Picture.setImageBitmap(bitmapEntities.get(position).getBmp());
holder.iv_Picture.setScaleType(ScaleType.FIT_XY);
return convertView;
}
/*控件缓存类*/
class ViewHolder {
ImageView iv_Picture;
}
package com.example.viewholderutils;
import android.util.SparseArray;
import android.view.View;
/**
* ViewHolder的工具类
*
*/
public class ViewHolder {
private ViewHolder() {
}
/**
* 用来缓存控件,优化加载
* @param view itemView的布局
* @param id itemView布局中需要缓存控件的id
* @return 缓存后的控件(textView、imageView...等控件)
*/
@SuppressWarnings("unchecked")
public static View get(View view, int id) {
// 获取itemView的ViewHolder对象,并将其转型为SparseArray<View>
SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
if (viewHolder == null) {
// 如果viewHolder为空,就新建一个
viewHolder = new SparseArray<View>();
// 给view设置tag标签
view.setTag(viewHolder);
}
// 根据控件的id获取itemView布局的控件
View childView = viewHolder.get(id);
if(childView == null){
// 如果还没有缓存该控件,那么就根据itemView找到该控件
childView = view.findViewById(id);
// 缓存该控件
viewHolder.put(id, childView);
}
// 返回缓存好的控件
return childView;
}
}
4.是否出现ANR,怎么解决?
IO耗时、底层数据死循环无法进行正确返回数据、死锁、Crash问题、
----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp // 最新的ANR发生的进程(包名)
...
DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
| sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
| state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
| stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应.
at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
- locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166) // 产生ANR的那个函数调用
- locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点
at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5254)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
文件最上的即为最新产生的ANR的trace信息.
前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.
CPU满负荷:
Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...
100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait
内存原因
// 以下trace信息来自网络, 用来做个示例
Cmdline: android.process.acore
DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)
...
MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732
ANR的处理
针对三种不同的情况, 一般的处理情况如下
主线程阻塞的
开辟单独的子线程来处理耗时阻塞事务.
CPU满负荷, I/O阻塞的
I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.
内存不够用的
增大VM内存, 使用largeHeap属性, 排查内存泄露(这个在内存优化那篇细说吧)等.
哪些地方是执行在主线程的?
- Activity的所有生命周期回调都是执行在主线程的.
- Service默认是执行在主线程的.
- BroadcastReceiver的onReceive回调是执行在主线程的.
- 没有使用子线程的looper的Handler的handleMessage, post(Runnable)是执行在主线程的.
- AsyncTask的回调中除了doInBackground, 其他都是执行在主线程的.
- View的post(Runnable)是执行在主线程的.
使用子线程的方式有哪些:
1.启Thread方式
继承Thread
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeThread p = new PrimeThread(143);
p.start();
实现Runnable接口
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
2.使用AsyncTask
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
// Do the long-running work in here
// 执行在子线程
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
// This is called each time you call publishProgress()
// 执行在主线程
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
// This is called when doInBackground() is finished
// 执行在主线程
protected void onPostExecute(Long result) {
showNotification("Downloaded " + result + " bytes");
}
}
// 启动方式
new DownloadFilesTask().execute(url1, url2, url3);
3.HandlerThread
// 启动一个名为new_thread的子线程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();
// 取new_thread赋值给ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
// 此时handleMessage是运行在new_thread这个子线程中了.
}
}
4.IntentService
Service是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.
5.Loader
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
// 使用LoaderManager来初始化Loader
getLoaderManager().initLoader(0, null, this);
//如果 ID 指定的加载器已存在,则将重复使用上次创建的加载器。
//如果 ID 指定的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以实现代码以实例化并返回新加载器
// 创建一个Loader
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ Contacts.DISPLAY_NAME + " != '' ))";
return new CursorLoader(getActivity(), baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}
// 加载完成
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.
5.是否有过性能优化
1).600图片删除
1.删除Uri
2.删除图片文件
删除时间接近不存在,删除不完全的时候
有ProgressBar显示
private boolean isDeleteAction(int mOperation) {
switch (mOperation) {
case R.id.action_delete:
return true;
default:
return false;
}
}
private void handlePhotosOrVideosAction(final ArrayList<Path> mItems) {
final StringBuilder imageStringBuidler = new StringBuilder();
final StringBuilder videoStringBuilder = new StringBuilder();
final ContentResolver contentResolver = mActivity.getContentResolver();
for (Path path : mItems) {
String pathId = path.getSuffix();
if (isImage(path)) {
imageStringBuidler.append(pathId);
imageStringBuidler.append(",");
} else {
videoStringBuilder.append(pathId);
videoStringBuilder.append(",");
}
}
//Start the thread to delete Uris
mHandler.post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "handlePhotosOrVideosAction delete uri begin ");
int deleteImageCounts = 0;
int deleteVideoCounts = 0;
if (imageStringBuidler.length() > 0) {
Uri imageBaseUri = Images.Media.EXTERNAL_CONTENT_URI;
String imageWhereClause = "_id in(" + imageStringBuidler.substring(0, imageStringBuidler.length() - 1) + ")";
deleteImageCounts = contentResolver.delete(imageBaseUri, imageWhereClause, null);
}
if (videoStringBuilder.length() > 0) {
Uri videoBaseUri = Video.Media.EXTERNAL_CONTENT_URI;
String videoWhereClause = "_id in(" + videoStringBuilder.substring(0, videoStringBuilder.length() - 1) + ")";
deleteVideoCounts = contentResolver.delete(videoBaseUri, videoWhereClause, null);
}
Log.d(TAG, "handlePhotosOrVideosAction delete uri end, deleteImageCounts = " + deleteImageCounts + ", deleteVideoCounts = " + deleteVideoCounts);
if (mDialog != null) mDialog.dismiss();
mSelectionManager.leaveSelectionMode();
}
});
//Start the thread to delete Files
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "handlePhotosOrVideosAction delete file begin ");
for (Path path : mItems) {
String pathId = path.getSuffix();
Uri uri = null;
if (imageStringBuidler.length() > 0) {
uri = Uri.parse("content://media/external/images/media/" + pathId);
}
if (videoStringBuilder.length() > 0) {
uri = Uri.parse("content://media/external/video/media/" + pathId);
}
SaveImage.deleteAuxFiles(contentResolver, uri);
}
Log.d(TAG, "handlePhotosOrVideosAction delete file end, files count = " + mItems.size());
}
}).start();
}
private boolean isImage(Path path) {
String mPath = path.toString();
if (mPath.contains("image")) {
return true;
} else {
return false;
}
}
2).图片、文件夹打开速度
1.去除不必要耗时
ClusterAlbumSet存在不必要加载
3).旋转大图片出现ANR
decode width、height耗时太多,而且重复多次调用,缩略图、大图、完整显示。
记录第一次获取的width、height,之后调用不要再次获取
6.图库的缓存机制
二级缓存机制:
ImageCache、二进制文件
读取速率:
内存>缓存>硬盘读取
7. 新增移动文件的功能:
File Api file.renamTo ()
新增创建新的相册的功能:首先会在指定的目录下去创建文件夹。
//移动文件
public boolean moveFile(String srcFileName, String destDirName) {
File srcFile = new File(srcFileName);
if(!srcFile.exists() || !srcFile.isFile())
return false;
File destDir = new File(destDirName);
if (!destDir.exists())
destDir.mkdirs();
return srcFile.renameTo(new File(destDirName + File.separator + srcFile.getName()));
}
//移动文件夹
public boolean moveDirectory(String srcDirName, String destDirName) {
File srcDir = new File(srcDirName);
if(!srcDir.exists() || !srcDir.isDirectory())
return false;
File destDir = new File(destDirName);
if(!destDir.exists())
destDir.mkdirs();
/**
* 如果是文件则移动,否则递归移动文件夹。删除最终的空源文件夹
* 注意移动文件夹时保持文件夹的树状结构
*/
File[] sourceFiles = srcDir.listFiles();
for (File sourceFile : sourceFiles) {
if (sourceFile.isFile())
moveFile(sourceFile.getAbsolutePath(), destDir.getAbsolutePath());
else if (sourceFile.isDirectory())
moveDirectory(sourceFile.getAbsolutePath(),
destDir.getAbsolutePath() + File.separator + sourceFile.getName());
else
;
}
return srcDir.delete();
}
8.新增标题栏置顶
首先TimeLineTimeBarView extends GLView
TimeLinePage界面:layout 具体的位置
时间线界面的标题是高通添加的,设计逻辑:标题、图片都是一个item,只不过标题item包含标题的内容,
滑动界面,更新标题
9.后台运行王者荣耀,打开Gallery变慢的问题
问题分析:
- 1).王者荣耀属于大型网络游戏,必定要占用大量的内存,那么系统就不会留给Gallery太多的内存使用,那么也就无法及时创建对象,处理数据。
- 2).Gallery中也存在大量耗时操作,只有本地存在很多的文件夹的时候,才会显示。因为Gallery中有一个隐藏、显示相册功能,进入Gallery的时候,LocalAlbumSet会查询哪些相册应该显示,还是隐藏,但是查询的方式是遍历整个机器内的所有子文件夹,这样子文件夹越多,那么查询的时间就越多。
解决方案: - 1).机器内相册的数量和名称是已知的,所以对已知的相册进行保存。
- 2).对保存的相册,添加,增删改查的功能,管理相册。
10.Gallery相册界面,快速选中图片,删除图片,相册界面全部黑
问题分析:
- 1).ThreadPool中的线程没有执行,因为有线程核心数,目前默认的数值是4个。
- 2).当核心线程数量达到饱和的时候,再次添加到线程池里,是不会继续执行,线程没有及时被释放掉。
- 3).ResourceCounter mCpuCounter = new ResourceCounter(2); 默认是2,现在更改为
- 4).增加释放资源的数量
11.Gallery中打开80兆图片左右旋转出现ANR
- 1).查看log发现,decode bitmap花费大量的时间,而且decode 会被执行三次。
- 2).第一次decode,是因为刚开始获取图片的相关信息会得到宽和高,第二次是获取缩略图,第三次是获取整个大的图片,
- 3).解决方案是:将第一次获取的宽和高的信息保存下来,之后第二次、第三次直接使用。