0%

布局就是页面的直接表现, 加载过程为 measure - layout - draw.

以此来:

  1. 能不先加载就不先加载 - 懒加载.
  2. 减少嵌套.
  3. 减少个数.
  • 使用 ViewStub 做懒加载.
  • TextView setCompoundDrawables 减少 View 的个数.
  • 根布局 FrameLayout 使用 merge 代替.
  • 集成 ViewGroup 时使用 merge.
  • 使用 ConstraintLayout 减少嵌套.
  • space 控件占位.
  • clipToPadding 和 ClipChildren 可以使 View Draw 到布局外面, 也是减少嵌套的方法.

以下内容提取于Android进程的内存管理分析Android内存泄漏分析及调试.

内存空间是一定的,所以在对象无用时就要回收一些对象来留出空间。当Java Garbage Collection开始运行时,它会从他了解还存活的对象作为内存遍历的根节点(GC Root),遍历heap内存空间,没有直接或间接引用到GC Root的对象便会被回收。

而Android内存泄漏便是指进程中的对象,虽然没有使用价值了,但它仍然有直接或间接的引用到GC Root,那么该对象便不会被GC回收,导致内存持续被占用,使可用内存变小。

常见的内存泄漏

  1. 查询数据库没有关闭Cursor。

  2. 使用BaseAdapter作为适配器时没有复用convertView。可以参考ListView与BaseAdapter优化.

  3. bitmap没有回收,可以参考Bitmap相关:管理Bitmap内存.

  4. 注册对象后没有反注册,比如Broadcast Receiver等。

  5. handler问题,如果handler是非静态的,会导致Activity或者Service不被回收,所以应当注册为静态内部类,同时在onDestroy时停止线程:mThread.getLooper().quit();

  6. Activity被静态引用,特别是缓存bitmap时,解决方法可以考虑使用Application的context代替Activity的context。

  7. View在callback中被引用,可能回调还没有结束,但是view处于引用状态,无法回收

     public void leak(final View view) {
         api.callback(new callBack() {
             onCallBack() {
                 ...
             }
         )
     }
  8. WebView的泄露问题:在魅族上面发现webView打开再关闭就会内存泄露..目前使用的解决方法是在webview外面嵌套一层layout作为container.在Activity的onDestroy中调用container.removeAllViews()方法.

  9. Dialog导致Window泄露,如果需要在dialog依附的Activity销毁前没有调用dialog.dismiss().会导致Activity泄露

JNI 中的内存泄露

除了 native code 的内存泄露, 即 C/C++ 中 new 出来的空间需要手动 free 之外, JNI 还需要特别考虑 LocalReference 和 GlobalReference 的泄露.

在 Java to native 的上下文切换期间, JVM 会分配一块区域用来存放 Local Ref, 每次 new 的对象都会存放在这个区域中, 这个区域只有在 native 执行完毕切回 Java 的时候才会清空, 如果这块区域满了, 就会报 OOM 错误.

而 Global Ref 则需要手动维护, 调用 DeleteGlobalRef 删除.

检测内存泄露的方法

  1. Android Studio 自带的 Memory Monitor,手动触发GC可以看得比较直观,但是dump出来的文件需要处理才能用MAT打开,利用 sdk/platforms-tool/ 下的 hprof-conv 文件,命令为:

     /hprof-conv source output
  2. MAT这篇文章分析得比较透彻

  3. Leakcanary,检测一些容易忽略的内存泄露很好用.

  4. adb shell dumpsys meminfo [包名]可以看到内存使用情况.

一些值得注意的地方

  1. 理论上来说,当你退出一个 Activity 后,强制GC一次,内存值应该回到跟进入 Activity 差不多的状态,否则就有可能是内存泄露了.但是有可能是图片加载库中对新 Activity 中加载的图片做了内存缓存,然而这部分内存 GC 可能暂时不会回收.

    比如在应用中我使用了 Fresco 去加载本地图库中的图片, Fresco 对这些图片做了内存缓存,然而这部分的图片其实是不需要在内存中缓存的.

Android经常会需要拍照、裁剪及图库中选择图片,其实都是通过intent调用系统相机或者系统图册,然后在onActivityResult中捕捉返回即可。

#####正常拍照选择图片的代码:

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

在onActivityResult中通过返回的intent.getExtras().get("data")便可以获取图片,但Android默认可用的应用内存大约为16M.所以Android为了不超出内存限制,在拍照返回时通过intent返回的bitmap会被压缩,这样导致一直都是获取的小图。所以在拍照时如果想返回大图需要通过Uri返回。

#####拍照选择大图代码(tampUri为路径,开发者文档提供的代码):

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 指定调用相机拍照后照片的储存路径
intent.putExtra(MediaStore.EXTRA_OUTPUT, tempUri);

在onActivityResult中通过tempUri进行下一步操作。

说明:

  1. 存储图片的路径建议使用 Activity.this.getExternalCacheDir()返回的/storage/sdcard0/Android/data/<package name>/cache路径.

  2. 通过路径获取 Uri 的方法,需要对不同版本兼容处理:

    1. 7.0 以下系统: 使用 Uri.fromFile(new File(path)) 等即可.

       String fileName = "temp.jpg";
       File tmpFile = new File(this.getExternalCacheDir(),fileName);
       Uri tmpUri = Uri.fromFile(tmpFile);
    2. 7.0 系统上,因为不允许传递通过 intent 传递 file:// 路径, 如果设置 targetSDK 为 24(7.0), 系统相机则无法获得 Uri.需要使用以下方法来获取 Uri.

      1. AndroidManifest.xml 中设置 provider:

         <provider
             android:authorities="${applicationId}"
             android:name="android.support.v4.content.FileProvider"
             android:exported="false"
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
                  android:resource="@xml/provider_paths"/>
        </provider>
      2. res 目录下新建 xml 目录, 新建 provider_paths.xml 文件:

        这里我使用了外部存储 external-path, 具体标签参考 FileProvider .

         <?xml version="1.0" encoding="utf-8"?>
         <paths xmlns:android="http://schemas.android.com/apk/res/android">
             <external-path name="image_file" path="."/>
         </paths>
      3. 判断系统版本大于7.0时,采用 FileProvider 获取 Uri:

         tring fileName = "temp.jpg";
         File tmpFile = new File(this.getExternalCacheDir(),fileName);
         tmpUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, tmpFile)

为了防止Uri路径不对导致不能保存,使用前必须确保文件路径无误,不然会导致“无法保存剪裁的图片”的错误。或者无提示但不能点击确定保存图片。正确获取的Uri最终以file://开头。

常见问题:

  1. 设置通过Uri返回后,onActivityResult中返回的intent为null。

  2. 开发者文档中只说了简单调用的方法,其实可以添加一些其他属性。

     intent.putExtra("return-data",false);
     intent.putExtra("outputFormat",Bitmap.CompressFormat.JPEG.toString());
     intent.putExtra("noFaceDetection", true);
  3. 不能添加intent.setType("image/*");,会因为找不到intent导致ActivityNotFoundException。

  4. intent.putExtra("android.intent.extras.CAMERA_FACING",1);,系统默认开启的是后置摄像头,如果希望选择前置摄像头可以加这句。

目前从图库选择图片可以有两种方法:

以下结果属于vivo Xplay,Android Version 4.4.4测试结果,如果有其他机型测试我会补充
1.Intent.ACTION_PICK:

Intent intent = new Intent(Intent.ACTION_PICK,
            android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, PHOTO_PICK);

onActivityResult返回的Intent中:

  • intent.getData()会返回所选图片的Uri,格式为content://media/external/images/media/919
  • intent.getExtras()返回的bundle中包含一个boolean变量,通过data.getExtras().get("isFinishGallery")获取,应该是指是否获取完毕,查询文档没有官方结果。
  • intent.putExtra("return-data",false)对返回值没有影响。

2.Intent.ACTION_GET_CONTENT

Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");//不加会导致crash

onActivityResult返回的Intent中:

  • intent.getData()返回所选图片的Uri:

关于返回Uri格式的说明:

  • 系统版本大于等于19(KitKat):content://com.android.providers.media.documents/document/image%3A919

  • 系统版本低于19:content://media/external/images/media/919

可以通过以下函数获取真实路径:

    private String getRealPathFromURI(Context context,Uri contentUri) {
        String[] proj = { MediaStore.Images.Media.DATA };
        Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null);
        if (cursor != null) {
            cursor.moveToFirst();
            int columnIndex = cursor.getColumnIndex(proj[0]);
            String path = cursor.getString(columnIndex);
            cursor.close();
            return path;
        }
        return contentUri.getPath();
       }
  1. Uri获取bitmap的方法可以参考Bitmap相关:读取大图片.

  1. 所需工具:

    1. JD-GUI,主要反编译Java源代码。

    2. apktool,主要反编译xml等资源文件。需要下载apktool及apktool-install-macosx.

    3. dex2jar,主要将apk中的dex文件转为jar。

  2. 部署:

    1. JD-GUI直接解压就是app了。

    2. apktool 网页有说明.

    3. 解压即可。

  3. 获取xml(使用apktool):

    1. 使用apktool,命令如下: apktool d “apk的路径”。

      eg(我已经cd到apk路径):

       apktool d test.apk        
    2. 解压后的res路径就是你想要的了。

  4. 获取java(使用dex2jar和JD-GUI):

    2.1 之后的版本, 直接使用 apk 就可以

         ./d2j-dex2jar.sh app-release.apk

    然后在该目录下会产生一个”classes-dex2jar.jar”的jar文件。

    1. 利用JD-GUI打开该jar文件,所有的代码变一目了然了。可以使用File-Save All Source保存所有的源文件。

使用Intent:

注意点:

  1. 非官方提供的代码,该方法并不通用,不是所有机器都支持这个intent,不推荐
  2. 裁剪时putExtra设置crop为true即可。
  3. Uri必须以file://开头,不然会导致“无法保存剪裁的图片”的错误。

可以封装这样一段代码:

private void cropImageUri(Uri uri, int outputX, int outputY, int requestCode,Uri outputUri){
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 2);
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", outputX);
    intent.putExtra("outputY", outputY);
    intent.putExtra("scale", true);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
    intent.putExtra("return-data", false);
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    intent.putExtra("noFaceDetection", true); // no face detection
    startActivityForResult(intent, requestCode);
}

另外关于putExtra的信号:

附加选项 数据类型 描述
crop String 发送裁剪信号
aspectX int X方向上的比例
aspectY int Y方向上的比例
outputX int 裁剪区的宽
outputY int 裁剪区的高
scale boolean 是否保留比例
return-data boolean 是否将数据保留在Bitmap中返回
data Parcelable 相应的Bitmap数据
circleCrop String 圆形裁剪区域?
MediaStore.EXTRA_OUTPUT (“output”) URI 将URI指向相应的file:///…

使用Github lib,
以下是一些star比较高的lib:

  1. ListView使用BaseAdapter作适配器的时候,在初始化获取View或者滚动获取View时,都会调用getView方法返回View添加到ListView中,这也是ListView每一项的item。

     public View getView(int position, View convertView, ViewGroup parent)

    所传递的三个参数中,

    position表示所添加的view的位置,parent一般为listview。

    而convertView参数:

    1. 如果listView的layout_height设置为wrap_content时,除了position=0时convertView为null,其他时候convertView都不为空。(因为这样会疯狂调用getView方法,不推荐)

    2. 如果listView的layout_height设置为fill_parent或者指定高度时,当listView没有填充到所需高度时,每一个convertView都为null,后面都不为null。

      因为listView所需的全部view不可能全部加载到内存中,所以不需要显示的view就需要回收,回收的view即为convertView。
      以listview的高度为fill_parent为例:

    3. 当convertView为null时,每个item的View需要通过LayoutInflater实例化返回。

    4. 当convertView不为null时,如果整个listView的item使用的是一样的布局,那我们可以直接使用这个view,只需更新convertView中的数据即可。

  2. ViewHolder

    经常在文章中看到ViewHolder来优化ListView,但其实ViewHolder不是库函数,而是需要自己定义的类。(注意viewHolder里面item方法重绘:如invalidate,setVisiblity,requestLayout后,会调用adapter的getView方法

    使用ViewHolder的原因是findViewById方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间。通过setTag,getTag直接获取View。

     class  ViewHolder{
         ImageView img;
         TextView name;
     }
    
     public View getView(int position, View convertView, ViewGroup parent) {
         ViewHolder holder = null;
         if(convertView==null){
             convertView = inflater.inflate(R.layout.list_item, parent, false);
             holder.img = (ImageView) convertView.findViewById(R.id.img);
             holder.name = (TextView) convertView.findViewById(R.id.name);
             holder = new ViewHolder();
             convertView.setTag(holder);
         }else{
             holder = (ViewHolder) convertView.getTag();
         }
         //设置holder
         holder.img.setImageResource(R.drawable.ic_launcher);
         holder.name.setText(list.get(position).partname);
         return convertView;
     }
  3. OnScrollListener

    ListView经常需要展示图片,如果在滑动时对滑动过的每张图片都要加载,会比较占内存。推荐的优化方法是设置OnScrollListener,在滑动完成后再下载当前页面的图片。

     listView.setOnScrollListener(new AbsListView.OnScrollListener() {
         @Override
         public void onScrollStateChanged(AbsListView view, int scrollState) {
             switch (scrollState){
                 // 用户手指滑动中
                 case SCROLL_STATE_TOUCH_SCROLL:
                 // 用户手指离开,但滑动动画进行中
                 case SCROLL_STATE_FLING:
                     break;
                 // 滑动结束
                 case SCROLL_STATE_IDLE:
                     int start = listView.getFirstVisiblePosition();
                     int end = listView.getLastVisiblePosition();
                     if(end >= listView.getCount()){
                         end = listView.getCount() - 1;
                     }
                     //展示start-end之间的图片
                     break;
             }
         }
    
         @Override
         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    
         }
     });
  4. onClickListener,当ListView的item中有比如button这些子view时,需要对其设置onclickListener,通常的写法是在getView方法中一个个设置,比如

     holder.img.setonClickListener(new onClickListenr)...

    但是这种写法每次调用getView时都设置了一个新的onClick事件,效率很低。高效的写法可以直接在ViewHolder中设置一个position,然后viewHolder implements OnClickListenr:

         class  ViewHolder implements OnClickListener{
             int position;
             TextView name;
    
             public void setPosition(int position){
                 this.position = position;
             }
    
             @Override
             public void onClick(View v) {
                 switch (v.getId()){
                     //XXXX
                 }
             }
         }
    
         public View getView(int position, View convertView, ViewGroup parent) {
             ViewHolder holder = null;
             if (convertView == null) {
                 convertView = inflater.inflate(R.layout.list_item, parent, false);
                 holder = new ViewHolder();
                 holder.name = (TextView) convertView.findViewById(R.id.name);
                 holder.name.setOnClickListener(holder);
                 convertView.setTag(holder);
             } else {
                 holder = (ViewHolder) convertView.getTag();
             }
             //设置holder
             holder.name.setText(list.get(position).partname);
             //设置position
             holder.setPosition(position);
             return convertView;
         }

  1. 光标设置

    如果EditText设置的高度比较大,光标默认会显示在居中的坐标,需要设置android:gravity=”top”。
    如果光标太贴近边框,只需设置android:paddingLeft即可。

  2. 多行输入

    在多行输入后光标会水平往右滚动,为了保持换行输入的习惯,还要设置

     android:inputType="textMultiLine"
     android:singleLine="false"
     android:scrollHorizontally="false"    

    为了直观显示,可以设置

     android:scrollbars="vertical"
     android:scrollbarStyle="insideInset"

    但输入框满了后,如果继续输入,EditText会不断增加高度,这样会隐藏上面的控件,最好设置 android:minLines和 android:maxLines确认最大最小行数。

  3. 软键盘遮挡
    可以在AndroidManifext.xml中设置Activity的软键盘:

     android:windowSoftInputMode="adjustPan"

    最后为了软键盘不挡住下方控件,最好还是全部添加一个scrollView。

    为了防止ScorllView和EditText(TextView)的滚动相冲突:
    除了在xml中设置editText:

     android:scrollbars="vertical"

    还需要在代码中设置:

     textViewExtras.setOnTouchListener(new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
             textViewExtras.getParent().requestDisallowInterceptTouchEvent(true);
             return false;
         }
     });
     textViewExtras.setMovementMethod(ScrollingMovementMethod.getInstance());
    1. 中划线 : textview.getPaint().setFlags(Paint. STRIKE_THRU_TEXT_FLAG );
    2. 底部横线: textview.getPaint().setFlags(Paint. UNDERLINE_TEXT_FLAG )
  4. 设置android:imeOptions属性时,有可能会失效.只需要再设置android:singleLine为true或者设置android:inputType为text,就可以修复~

  5. 设置setOnEditorActionListener()在键盘控制模拟器时会失效(检测不到回车键), 所以还是建议使用setOnKeyListener().

  1. EditText支持长按复制,TextView也可以通过android:textIsSelectable="true"来设置.

  1. ####设置壁纸最常规的想法就是通过默认的WallpaperManager,方法如下:

     WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
                            wallpaperManager.suggestDesiredDimensions(bitmap.getWidth(),bitmap.getHeight());
     wallpaperManager.setBitmap(bitmap);

注意这里的

wallpaperManager.suggestDesiredDimensions(bitmap.getWidth(),bitmap.getHeight());

是表示设置目前的像素是图片的像素,最好先获取手机屏幕像素大小,然后传递宽度和高度作为参数。

WallpaperManager wallpaperManager = WallpaperManager.getInstance(DisplayPicture.this);
        wallpaperManager.suggestDesiredDimensions(Utils.getDisplayWidth(),Utils.getDisplayHeight());

因为某些手机是可以设置壁纸跟桌面一起滑动的,这样所需的壁纸长宽跟手机屏幕的长宽是不一样的,所以需要这样设置。

###但是问题来了,这样加载的bitmap需要裁减,并且加载后非常容易out of memory。所以尝试以下的解决方法:

  1. ####通过系统intent来设置,即传递一个image对象,系统luancher选择相应的程序来处理,这种方法还可以设置锁屏壁纸~~

传递的sendUri为图片的Uri,代码如下:

Intent setAs = new Intent(Intent.ACTION_ATTACH_DATA);
        setAs.setDataAndType(sendUri, "image/jpg");
        setAs.putExtra("mimeType","image/jpg");
        startActivity(Intent.createChooser(setAs, "set As"));

其中

setAs.putExtra("mineType","image/jpg")

非常重要,通过这句告诉intent的Filter当前intent的data传递的是什么内容,不加这一句会导致某些机型的相册程序崩溃。如vivo。。

另外关于mineType的其他可以参考这里

  1. ####将bitmap传入系统裁减intent,裁减至手机屏幕大小,然后在onActivitiyResult中捕捉bitmap并设置壁纸(强行避免OOM)

     Intent intent = new Intent("com.android.camera.action.CROP");
         intent.setDataAndType(sendUri, "image/*");
         intent.putExtra("crop", "true");
         //裁剪框的比例,1:1
         DisplayMetrics dm = new DisplayMetrics();
         dm = getResources().getDisplayMetrics();
         // float scale = (float)dm.heightPixels / (float)dm.widthPixels;
         intent.putExtra("aspectX", dm.widthPixels);
         intent.putExtra("aspectY", dm.heightPixels);
         intent.putExtra("scale",true);
         //裁剪后输出图片的尺寸大小
         intent.putExtra("outputX", dm.widthPixels);
         intent.putExtra("outputY", dm.heightPixels);
         Uri uri = Uri.fromFile(file);
         intent.putExtra(MediaStore.EXTRA_OUTPUT,uri);
         //图片格式
         intent.putExtra("outputFormat", "JPEG");
         intent.putExtra("noFaceDetection", true);
         intent.putExtra("return-data",false);
         startActivityForResult(intent, 1);

返回这里(file是通过sendUri new出来的):

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if(requestCode == 1){
        Uri uri = Uri.fromFile(file);
         Bitmap bitmap = null;
        if(uri != null){
            try {
                bitmap= imgLoader.getBitmapByPath(uri.getPath());
                WallpaperManager wallpaperManager = WallpaperManager.getInstance(DisplayPicture.this);
                wallpaperManager.suggestDesiredDimensions(bitmap.getWidth(),bitmap.getHeight());
                wallpaperManager.setBitmap(bitmap);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    super.onActivityResult(requestCode, resultCode, data);
}

先插一句比较小白的话。。
点动成线,其实我们看到的屏幕是一个个像素点显示的,密集的点显示成所看到的图像。

###常见的单位:in、px、dpi(ppi、density)、dp(dip)、sp

in:inch,英寸,每英寸大约是2.54厘米,一般手机所说的屏幕多少寸都是指对角线的长度。

px:Pixel,像素,最小单位,指每一个像素点。通常所说的分辨率1920*1080就是指各个方向上的像素点的数量。

dpi(ppi、density):
dpi为Dots Per Inch的缩写,表示每英寸点的个数,一般用于打印分辨率。
ppi为Pixels Per Inch的缩写,表示每英寸像素点的个数,一般用于显示分辨率。
density,密度,表示每Inch点的个数。
计算方法为px/in即可。

安卓的资源列表文件夹与其对应:

drawable-ldpi         (dpi=120 , density = 0.75)
drawable-mdpi         (dpi=160 , density = 1,这也是默认的layout文件夹)
drawable-hdpi         (dpi=240 , density = 1.5)
drawable-xhdpi         (dpi=320 , density = 2)
drawable-xhdpi         (dpi=480 , density = 3)
drawable-xxxhdpi     (dpi=640 , density = 4)

dp(dip):密度无关像素, Density Independent Pixels的缩写,dp * density = px。不会因为像素而导致显示效果不同。

sp:Scaled Pixel, 主要用于调整字体大小,转换方法与dp一样,但是会随着系统设置而变化字体大小.

默认的icon大小为48dp*48dp

默认的通知栏icon大小为24dp*24dp

适配时建议根据设计图计算密度,如iphone5的设备分辨率为640X1164, 屏幕尺寸为4英寸,那么由勾股定理对角线平方为640*640+1164*1164=1764496,开根号得出对角线分辨率为1328,1328/4=332,这就是iphone5的”dpi”,所以可以把iphone5的对应设计图放在xhdpi文件夹中。

加载资源时,Android会先根据屏幕的dpi在dpi相同的资源文件夹中找资源,然后从最大的资源文件夹开始找。
所以推荐在dpi最高的文件夹放置一套资源文件即可,对资源的长高设定wrap_content,系统会自动缩放保持显示效果。如100px*100px的图放在xhdpi中,那么mdpi的设备显示时,1001/2 = 50px,那么该图就会以50px\50px的大小显示。

LayoutParams实例化时传的参数为px,需要将布局的dp转为px才能使效果一样。
LayoutParams初始化时需要初始化为父布局的类型
dp转px的方法为:

int dip = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,60,getResources().getDisplayMetrics());