Android全局异常捕获并弹窗提示

Android 难免有崩溃的时候,但是崩溃了该如何处理呢?虽然那天有位同仁说 “既然崩溃了,用户体验就差了,心里会想这是毛APP,下次也不想用了” ,所以检查BUG以防崩溃是必须的,但是也需要一个后备方案,崩溃了能友好些,我们也能收集一些崩溃的信息。
说到全局捕获异常的UncaughtExceptionHandler,就不得不说期间遇到的各种坑:
1. 初始化肯定在Application,网上说的Activity启各种不认同。但在Application启就存在不能弹AlertDialog的问题(目前不确定,不知道是自己哪里没处理好还是的确是这个问题,有时间再验证一下)
2. 崩溃不一定是单次,在多层Activity中,崩溃一个顶层的Activity可能导致下层的Activity连续崩溃,所以uncaughtException可能会捕获到多次崩溃信息(具体影响后面会说到)
先来张崩溃后的效果图:
背景是另一个APP,当前的APP已崩溃并弹出该提示

%title插图%num
实现流程:
写个类继承于UncaughtExceptionHandler,实现方法
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处
mDefaultHandler.uncaughtException(thread, ex);
} else {
// 跳转到崩溃提示Activity
Intent intent = new Intent(mContext, CrashDialog.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
System.exit(0);// 关闭已奔溃的app进程
}
}

然后转handleException方法处理异常:
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}

// 收集错误信息
getCrashInfo(ex);

return true;
}

上面的代码很清楚了,如果异常被捕获到并且异常信息不会NULL,处理完则跳转到CrashDialog。为什么跳Activity用Dialog样式,而不直接弹AlertDialog,是因为的确弹不出来。
收集错误信息:
private void getCrashInfo(Throwable ex) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String errorMessage = writer.toString();
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String mFilePath = Environment.getExternalStorageDirectory() + “/” + App.ERROR_FILENAME;
FileTxt.WirteTxt(mFilePath, FileTxt.ReadTxt(mFilePath) + ‘\n’ + errorMessage);
} else {
Log.i(App.TAG, “哦豁,说好的SD呢…”);
}
}

是的,我把错误信息写到了存储并在刚才的CrashDialog中读取。为什么不直接传值呢?因为刚说到的坑第2条,多次崩溃的情况下,将导致直接传值只会传*后一次崩溃信息,而*后一次崩溃信息并不是主要引发崩溃的点,收集上来的错误信息可读性不大。那为什么我不写个全局变量来存储呢?因为尝试过,不知道是机型问题(Huawei Mate7 – API 23)还是全部问题,变量压根就不记录数据,*后只有将信息依次写到存储。
主要代码
App.java
package cn.qson.androidcrash;

/**
* @author x024
*/

import android.app.Application;

public class App extends Application {

public final static String TAG = “x024”;
public final static String ERROR_FILENAME = “x024_error.log”;

@Override
public void onCreate() {
super.onCreate();

CrashHanlder.getInstance().init(this);

}
}

CrashHanlder.java
package cn.qson.androidcrash;

/**
* @author x024
*/

import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;

import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import android.util.Log;

/**
* 收集错误报告并上传到服务器
*
* @author x024
*
*/
public class CrashHanlder implements UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefaultHandler;
// CrashHandler实例
private static CrashHanlder INSTANCE = new CrashHanlder();
// 程序的Context对象
private Context mContext;

private CrashHanlder() {
}

public static CrashHanlder getInstance() {
return INSTANCE;
}

/**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context;
// 获取系统默认的UncaughtException处理
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置该CrashHandler为程序的默认处理
Thread.setDefaultUncaughtExceptionHandler(this);
}

/**
* 异常捕获
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处
mDefaultHandler.uncaughtException(thread, ex);
} else {
// 跳转到崩溃提示Activity
Intent intent = new Intent(mContext, CrashDialog.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
System.exit(0);// 关闭已奔溃的app进程
}
}

/**
* 自定义错误捕获
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false.
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}

// 收集错误信息
getCrashInfo(ex);

return true;
}

/**
* 收集错误信息
*
* @param ex
*/
private void getCrashInfo(Throwable ex) {
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String errorMessage = writer.toString();
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String mFilePath = Environment.getExternalStorageDirectory() + “/” + App.ERROR_FILENAME;
FileTxt.WirteTxt(mFilePath, FileTxt.ReadTxt(mFilePath) + ‘\n’ + errorMessage);
} else {
Log.i(App.TAG, “哦豁,说好的SD呢…”);
}

}

}

CrashDialog.java
package cn.qson.androidcrash;

/**
* @author x024
*/

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class CrashDialog extends Activity {

private String mFilePath;
private Button btnExit, btnRestart;
private Boolean StorageState = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crash);
CrashDialog.this.setFinishOnTouchOutside(false);
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
mFilePath = Environment.getExternalStorageDirectory() + “/” + App.ERROR_FILENAME;
StorageState = true;
} else {
Log.i(App.TAG, “哦豁,说好的SD呢…”);
}

new Thread(upLog).start();
initView();
}

private void initView() {
btnExit = (Button) findViewById(R.id.cash_exit);
btnRestart = (Button) findViewById(R.id.cash_restart);

btnExit.setOnClickListener(mOnClick);
btnRestart.setOnClickListener(mOnClick);

}

OnClickListener mOnClick = new OnClickListener() {

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cash_exit:
exit();
break;
case R.id.cash_restart:
restart();
break;
default:
break;
}
}
};

// 上传错误信息
Runnable upLog = new Runnable() {
@Override
public void run() {
try {

String Mobile = Build.MODEL;
String maxMemory = “” + getmem_TOLAL() / 1024 + “m”;
String nowMemory = “” + getmem_UNUSED(CrashDialog.this) / 1024 + “m”;
String eMessage = “未获取到错误信息”;
if (StorageState) {
eMessage = FileTxt.ReadTxt(mFilePath).replace(“‘”, “”);
}
Log.i(App.TAG, “Mobile:” + Mobile + ” | maxMemory:” + maxMemory + ” |nowMemory:” + nowMemory
+ ” |eMessage:” + eMessage);

/**
* 可以在这调你自己的接口上传信息
*/
} catch (Exception e) {
e.printStackTrace();
}
}
};

private void exit() {
FileTxt.deleteFile(mFilePath);
System.exit(0);
android.os.Process.killProcess(android.os.Process.myPid());
}

private void restart() {
Intent intent = getBaseContext().getPackageManager()
.getLaunchIntentForPackage(getBaseContext().getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
exit();
}

@Override
public void onBackPressed() {
super.onBackPressed();
exit();
}

// 获取可用内存
public static long getmem_UNUSED(Context mContext) {
long MEM_UNUSED;
ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
am.getMemoryInfo(mi);

MEM_UNUSED = mi.availMem / 1024;
return MEM_UNUSED;
}

// 获取剩余内存
public static long getmem_TOLAL() {
long mTotal;
String path = “/proc/meminfo”;
String content = null;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path), 8);
String line;
if ((line = br.readLine()) != null) {
content = line;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
int begin = content.indexOf(‘:’);
int end = content.indexOf(‘k’);

content = content.substring(begin + 1, end).trim();
mTotal = Integer.parseInt(content);
return mTotal;
}

}

完整代码:http://download.csdn.net/detail/hx7013/9710757

Android共享元素转场动画Part2——Fragment to Fragment

继续Part1部分–Activity to Activity,这个部分我们简单介绍下Fragment to Fragment的共享动画实现;

Fragment to Fragment

首先我们需要创建一个Activity容器来加载Fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Fragment fragment = getSupportFragmentManager().findFragmentByTag(FragmentA.class.getName());
if (fragment == null) {
fragment = FragmentA.newInstance();
getSupportFragmentManager().beginTransaction().add(R.id.activity_main,
fragment,
FragmentA.class.getName())
.commit();
}
}
}

按照代码所示,我们现在Activity中加载FragmentA 然后由FragmentA跳转到FragmentB,并且实现共享动画。

下面实现FragmentA:

FragmentA中的xml代码实现,一个ImageView 和一个Button ,其中ImageView为FragmentA的共享元素,并且为他设置属性android:transitionName="simple transition name"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:tools= “http://schemas.android.com/tools”
android:id= “@+id/activity_main”
android:layout_width= “match_parent”
android:layout_height= “match_parent”
tools:context= “io.github.hexiangyuan.sharedelementtransitionsdemo.MainActivity”>
<ImageView
android:id= “@+id/imageView”
android:layout_width= “64dp”
android:layout_height= “64dp”
android:scaleType= “centerCrop”
android:transitionName= “simple transition name”
android:src= “@drawable/image” />
<Button
android:id= “@+id/btn_click”
android:layout_width= “wrap_content”
android:layout_height= “wrap_content”
android:text= “Click Me”
android:textSize= “16sp”
android:layout_below= “@+id/imageView” />
</RelativeLayout>

FragmentA的java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FragmentA extends Fragment {
public static final String TAG = FragmentA.class.getSimpleName();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_a, container, false);
}
public static FragmentA newInstance() {
return new FragmentA();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final ImageView imageView = (ImageView) getView().findViewById(R.id.imageView);
getActivity().findViewById(R.id.btn_click).setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
Fragment fragmentB = getFragmentManager().findFragmentByTag(TAG);
if (fragmentB == null) fragmentB = FragmentB.newInstance();
getFragmentManager()
.beginTransaction()
.addSharedElement(imageView,
ViewCompat.getTransitionName(imageView))
.addToBackStack(TAG)
.replace(R.id.activity_main, fragmentB)
.commit();
}
});
}
}

值得注意的是在addShareElement()这个方法以及addToBackStack()这个方法;

  • addShareElement:设置了作为共享元素的控件以及transitionName;
  • addToBackStack:为了让fragment回栈,如果不设置这个回栈,当跳转到fragmentB的时候,BackClick就会直接退出Activity;

那么在FragmentB就比较容易实现了:

FragmentB:

1
2
3
4
5
6
7
<ImageView
android:id= “@+id/imageView”
android:layout_width= “match_parent”
android:layout_height= “184dp”
android:scaleType= “centerCrop”
android:transitionName= “simple transition name”
android:src= “@drawable/image” />
1
2
3
4
5
6
7
8
9
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setSharedElementEnterTransition(
TransitionInflater.from(getContext())
.inflateTransition(android.R.transition.move));
}
}

只需要在FragmentB的

  • xml 的共享控件里面设置android:transitionName="simple transition name"
  • onCreate里面设置动画为android.R.transition.move

加载网络图片的元素共享(以Picasso为例)

一般在App中,我们的ImageView都是在网络URL获取的资源,那么网络加载的ImageView也是可以实现共享元素转换的,下面我们就以Picasso为例:

添加依赖

1
compile ‘com.squareup.picasso:picasso:2.5.2’

FragmentA中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Picasso.with(getActivity())
.load( “https://s3-us-west-1.amazonaws.com/powr/defaults/image-slider2.jpg”)
.fit()
.centerCrop()
.into(imageView);
getActivity().findViewById(R.id.btn_click).setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
Fragment fragmentB = getFragmentManager().findFragmentByTag(TAG);
if (fragmentB == null) fragmentB = FragmentB.newInstance();
getFragmentManager()
.beginTransaction()
.addSharedElement(imageView,
ViewCompat.getTransitionName(imageView))
.addToBackStack(TAG)
.replace(R.id.activity_main, fragmentB)
.commit();
}
});

FragmentB中添加加载图片的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ImageView imageView = (ImageView) getView().findViewById(R.id.imageView);
Picasso.with(getContext())
.load( “https://s3-us-west-1.amazonaws.com/powr/defaults/image-slider2.jpg”)
.fit()
.centerCrop()
.noFade()
.into(imageView, new Callback() {
@Override
public void onSuccess() {
startPostponedEnterTransition();
}
@Override
public void onError() {
}
});
}
  • 添加noFade禁用渐隐的效果促使动画更加的流畅。
  • 在CallBack的onSuccess()中设置startPostponedEnterTransition()

github banch

Blog

非常感谢,你能耐心读完;

将Activity打成jar包供第三方调用

将Activity打成jar包供第三方调用(解决资源文件不能打包的问题)

*近有一个需要,我们公司做了一个apk客户端,然后其他的公司可以根据自己的需要来替换里面的资源图片,文字等一些资源文件问题,我本来想这个简单,用两个工程直接替换里面的资源文件就行,老大说,这样子不好,如果要改需要改两个客户端,而且还麻烦,叫我将所有的Activity打成Jar包的形式,这样子我们改了里面的内容就直接发布Jar包出去,其他公司直接下载Jar来使用,这样子他们自己公司也能更好的维护。

所以我就想直接将Activity打成Jar包,可是在使用的过程中发现这样子根本行不通,因为如果Activity引用了布局文件的话,比如R.layout.XXX或者R.string.XXX,我们使用的时候会报资源ID未找到的异常,在官网上看到可以将另一个工程当做Libraryhttp://developer.android.com/tools/projects/projects-eclipse.html,可是这样子需要将源码给到人家,不能直接发布Jar包,貌似不是我要的那种情况,今天我教大家如果将Activity打成Jar包的形式

1.我们新建一个Android工程,取名为ActivityLibrary,这个就是等下我们需要打包成Jar的工程

%title插图%num

注:MResource这个类很重要,主要是它的作用,利用反射根据资源名字获取资源ID(其实系统也自带了根据资源名字获取资源ID的方法getResources().getIdentifier(“main_activity”, “layout”, getPackageName());*个参数是资源的名字,第二个参数是资源的类型,例如layout, string等,第三个是包名字)

[java]  view plain copy

  1. package com.example.activitylibrary;
  2. import android.content.Context;
  3. /**
  4.  * 根据资源的名字获取其ID值
  5.  * @author mining
  6.  *
  7.  */
  8. public class MResource {
  9.     public static int getIdByName(Context context, String className, String name) {
  10.         String packageName = context.getPackageName();
  11.         Class r = null;
  12.         int id = 0;
  13.         try {
  14.             r = Class.forName(packageName + “.R”);
  15.             Class[] classes = r.getClasses();
  16.             Class desireClass = null;
  17.             for (int i = 0; i < classes.length; ++i) {
  18.                 if (classes[i].getName().split(“\\$”)[1].equals(className)) {
  19.                     desireClass = classes[i];
  20.                     break;
  21.                 }
  22.             }
  23.             if (desireClass != null)
  24.                 id = desireClass.getField(name).getInt(desireClass);
  25.         } catch (ClassNotFoundException e) {
  26.             e.printStackTrace();
  27.         } catch (IllegalArgumentException e) {
  28.             e.printStackTrace();
  29.         } catch (SecurityException e) {
  30.             e.printStackTrace();
  31.         } catch (IllegalAccessException e) {
  32.             e.printStackTrace();
  33.         } catch (NoSuchFieldException e) {
  34.             e.printStackTrace();
  35.         }
  36.         return id;
  37.     }
  38. }

当我们的资源Id是一个数组的时候,我们要用下面的方法

[java]  view plain copy

  1. public static int[] getIdsByName(Context context, String className, String name) {
  2.     String packageName = context.getPackageName();
  3.     Class r = null;
  4.     int[] ids = null;
  5.     try {
  6.       r = Class.forName(packageName + “.R”);
  7.       Class[] classes = r.getClasses();
  8.       Class desireClass = null;
  9.       for (int i = 0; i < classes.length; ++i) {
  10.         if (classes[i].getName().split(“\\$”)[1].equals(className)) {
  11.           desireClass = classes[i];
  12.           break;
  13.         }
  14.       }
  15.       if ((desireClass != null) && (desireClass.getField(name).get(desireClass) != null) && (desireClass.getField(name).get(desireClass).getClass().isArray()))
  16.         ids = (int[])desireClass.getField(name).get(desireClass);
  17.     }
  18.     catch (ClassNotFoundException e) {
  19.       e.printStackTrace();
  20.     } catch (IllegalArgumentException e) {
  21.       e.printStackTrace();
  22.     } catch (SecurityException e) {
  23.       e.printStackTrace();
  24.     } catch (IllegalAccessException e) {
  25.       e.printStackTrace();
  26.     } catch (NoSuchFieldException e) {
  27.       e.printStackTrace();
  28.     }
  29.     return ids;
  30.   }

 

LibraryActivity这里面比较简单,一个Button,一个TextView,一个ImageView

[java]  view plain copy

  1. package com.example.activitylibrary;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.view.View;
  5. import android.view.View.OnClickListener;
  6. import android.widget.Button;
  7. import android.widget.TextView;
  8. import android.widget.Toast;
  9. public class LibraryActivity extends Activity {
  10.     String msg = “我是来自Jar中的Activity”;
  11.     @Override
  12.     protected void onCreate(Bundle savedInstanceState) {
  13.         super.onCreate(savedInstanceState);
  14.         setContentView(MResource.getIdByName(getApplication(), “layout”“activity_main”));
  15.         TextView mTextView = (TextView) findViewById(MResource.getIdByName(getApplication(), “id”“textView1”));
  16.         mTextView.setText(msg);
  17.         Button mButton = (Button) findViewById(MResource.getIdByName(getApplication(), “id”“button1”));
  18.         mButton.setText(msg);
  19.         mButton.setOnClickListener(new OnClickListener() {
  20.             @Override
  21.             public void onClick(View v) {
  22.                 Toast.makeText(getApplication(), msg, Toast.LENGTH_SHORT).show();
  23.             }
  24.         });
  25.     }
  26. }

Activity的布局

[html]  view plain copy

  1. <RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  2.     xmlns:tools=“http://schemas.android.com/tools”
  3.     android:layout_width=“match_parent”
  4.     android:layout_height=“match_parent”
  5.     tools:context=“.MainActivity” >
  6.     <Button
  7.         android:id=“@+id/button1”
  8.         android:layout_width=“wrap_content”
  9.         android:layout_height=“wrap_content”
  10.         android:layout_alignParentLeft=“true”
  11.         android:layout_alignParentRight=“true”
  12.         android:layout_alignParentTop=“true” />
  13.     <TextView
  14.         android:id=“@+id/textView1”
  15.         android:layout_width=“wrap_content”
  16.         android:layout_height=“wrap_content”
  17.         android:layout_alignParentLeft=“true”
  18.         android:layout_alignParentRight=“true”
  19.         android:layout_below=“@+id/button1” />
  20.     <ImageView
  21.         android:id=“@+id/imageView1”
  22.         android:layout_width=“wrap_content”
  23.         android:layout_height=“wrap_content”
  24.         android:layout_alignParentBottom=“true”
  25.         android:layout_alignParentLeft=“true”
  26.         android:layout_alignParentRight=“true”
  27.         android:layout_below=“@+id/textView1”
  28.         android:layout_marginTop=“28dp”
  29.         android:src=“@drawable/ic_launcher” />
  30. </RelativeLayout>

2.我们将ActivityLibrary工程打成Jar包。右键工程—>Export—->Java—>JAR file—->Next如下图

%title插图%num%title插图%num

只勾选src目录,其他的都不勾选,如图

%title插图%num

通过上面这几步我们就将Android工程打包好了

3.我们来使用刚刚打包好的Activity,我们还需要刚刚那个工程的资源文件,因为我们刚刚只打包了src,资源文件不能打包,因此我们需要自己拿出来,我们需要吧Library.jar加入到libs里面去,然后用到的资源文件,如果layout,string之类的拷贝到对应工程的地方去

%title插图%num

这个工程一个MainActivity,里面一个按钮,点击按钮跳转到Library中的Activity中,比较简单我直接把代码贴上

[java]  view plain copy

  1. package com.example.androidlibraryinvoke;
  2. import android.app.Activity;
  3. import android.content.Intent;
  4. import android.os.Bundle;
  5. import android.view.View;
  6. import android.view.View.OnClickListener;
  7. import android.widget.Button;
  8. public class MainActivity extends Activity {
  9.     @Override
  10.     protected void onCreate(Bundle savedInstanceState) {
  11.         super.onCreate(savedInstanceState);
  12.         setContentView(R.layout.main);
  13.         Button mButton = (Button) findViewById(R.id.button1);
  14.         mButton.setOnClickListener(new OnClickListener() {
  15.             @Override
  16.             public void onClick(View v) {
  17.                 Intent intent = new Intent();
  18.                 intent.setClassName(getApplication(), “com.example.activitylibrary.LibraryActivity”);
  19.                 startActivity(intent);
  20.             }
  21.         });
  22.     }
  23. }

我们需要在AndroidManifest.xml注册LibraryActivity,否则报Activity找不到异常,总体来说就是这样子,这样子我们将Activity打成的Jar包和资源文件一起发出去,人家就可以调用可,如果你觉得我写的对你有帮助的话你就顶一下,谢谢!

【Android】Service学习之本地服务

 Service是在一段不定的时间运行在后台,不和用户交互应用组件。每个Service必须在manifest中 通过<service>来声明。可以通过contect.startservice和contect.bindserverice来启动。
    Service和其他的应用组件一样,运行在进程的主线程中。这就是说如果service需要很多耗时或者阻塞的操作,需要在其子线程中实现。
    service的两种模式(startService()/bindService()不是完全分离的):
  • 本地服务 Local Service 用于应用程序内部。
    它可以启动并运行,直至有人停止了它或它自己停止。在这种方式下,它以调用Context.startService()启动,而以调用Context.stopService()结束。它可以调用Service.stopSelf() 或 Service.stopSelfResult()来自己停止。不论调用了多少次startService()方法,你只需要调用一次stopService()来停止服务。
    用于实现应用程序自己的一些耗时任务,比如查询升级信息,并不占用应用程序比如Activity所属线程,而是单开线程后台执行,这样用户体验比较好。
  • 远程服务 Remote Service 用于android系统内部的应用程序之间。
    它可以通过自己定义并暴露出来的接口进行程序操作。客户端建立一个到服务对象的连接,并通过那个连接来调用服务。连接以调用Context.bindService()方法建立,以调用 Context.unbindService()关闭。多个客户端可以绑定至同一个服务。如果服务此时还没有加载,bindService()会先加载它。
    可被其他应用程序复用,比如天气预报服务,其他应用程序不需要再写这样的服务,调用已有的即可。
生命周期
    Service的生命周期并不像Activity那么复杂,它只继承了onCreate(),onStart(),onDestroy()三个方法,当我们*次启动Service时,先后调用了onCreate(),onStart()这两个方法,当停止Service时,则执行onDestroy()方法,这里需要注意的是,如果Service已经启动了,当我们再次启动Service时,不会在执行onCreate()方法,而是直接执行onStart()方法。
    而启动service,根据onStartCommand的返回值不同,有两个附加的模式:
    1. START_STICKY 用于显示启动和停止service。
    2. START_NOT_STICKY或START_REDELIVER_INTENT用于有命令需要处理时才运行的模式。
     服务不能自己运行,需要通过调用Context.startService()或Context.bindService()方法启动服务。这两个方法都可以启动Service,但是它们的使用场合有所不同。
1. 使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。
如果打算采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onStart()方法。
如果调用startService()方法前服务已经被创建,多次调用startService()方法并不会导致多次创建服务,但会导致多次调用onStart()方法。
采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用onDestroy()方法。
2. 使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。
onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。
采用Context.bindService()方法启动服务时只能调用onUnbind()方法解除调用者与服务解除,服务结束时会调用onDestroy()方法。
    官方文档告诉我们,一个service可以同时start并且bind,在这样的情况,系统会一直保持service的运行状态如果service已经start了或者BIND_AUTO_CREATE标志被设置。如果没有一个条件满足,那么系统将会调用onDestory方法来终止service.所有的清理工作(终止线程,反注册接收器)都在onDestory中完成。
拥有service的进程具有较高的优先级
    官方文档告诉我们,Android系统会尽量保持拥有service的进程运行,只要在该service已经被启动(start)或者客户端连接(bindService)到它。当内存不足时,需要保持,拥有service的进程具有较高的优先级。
1. 如果service正在调用onCreate,onStartCommand或者onDestory方法,那么用于当前service的进程则变为前台进程以避免被killed。

2. 如果当前service已经被启动(start),拥有它的进程则比那些用户可见的进程优先级低一些,但是比那些不可见的进程更重要,这就意味着service一般不会被killed.

3. 如果客户端已经连接到service (bindService),那么拥有Service的进程则拥有*高的优先级,可以认为service是可见的。

4. 如果service可以使用startForeground(int, Notification)方法来将service设置为前台状态,那么系统就认为是对用户可见的,并不会在内存不足时killed。

如果有其他的应用组件作为Service,Activity等运行在相同的进程中,那么将会增加该进程的重要性。
本地service
1.不需和Activity交互的本地服务
  1. public class LocalService extends Service {
  2. private static final String TAG = “LocalService”;
  3. @Override
  4. public IBinder onBind(Intent intent) {
  5. Log.i(TAG, “onBind”);
  6. return null;
  7. }
  8. @Override
  9. public void onCreate() {
  10. Log.i(TAG, “onCreate”);
  11. super.onCreate();
  12. }
  13. @Override
  14. public void onDestroy() {
  15. Log.i(TAG, “onDestroy”);
  16. super.onDestroy();
  17. }
  18. @Override
  19. public void onStart(Intent intent, int startId) {
  20. Log.i(TAG, “onStart”);
  21. super.onStart(intent, startId);
  22. }
  23. }

Activity:

  1. public class ServiceActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.servicedemo);
  6. ((Button) findViewById(R.id.startLocalService)).setOnClickListener(
  7. new View.OnClickListener(){
  8. @Override
  9. public void onClick(View view) {
  10. // TODO Auto-generated method stub
  11. startService(new Intent(“com.demo.SERVICE_DEMO”));
  12. }
  13. });
  14. ((Button) findViewById(R.id.stopLocalService)).setOnClickListener(
  15. new View.OnClickListener(){
  16. @Override
  17. public void onClick(View view) {
  18. // TODO Auto-generated method stub
  19. stopService(new Intent(“com.demo.SERVICE_DEMO”));
  20. }
  21. });
  22. }
  23. }

在AndroidManifest.xml添加:

  1. <service android:name=“.LocalService”>
  2. <intent-filter>
  3. <action android:name=“com.demo.SERVICE_DEMO” />
  4. <category android:name=“android.intent.category.default” />
  5. </intent-filter>
  6. </service>
否则启动服务时会提示new Intent找不到”com.demo.SERVICE_DEMO”。
    对于这类不需和Activity交互的本地服务,是使用startService/stopService的*好例子。
    运行时可以发现*次startService时,会调用onCreate和onStart,在没有stopService前,无论点击多少次startService,都只会调用onStart。而stopService时调用onDestroy。再次点击stopService,会发现不会进入service的生命周期的,即不会再调用onCreate,onStart和onDestroy。
    而onBind在startService/stopService中没有调用。
2.本地服务和Activity交互
    对于这种case,官方的sample(APIDemo\app.LocalService)是*好的例子:
  1. /**
  2. * This is an example of implementing an application service that runs locally
  3. * in the same process as the application. The {@link LocalServiceController}
  4. * and {@link LocalServiceBinding} classes show how to interact with the
  5. * service.
  6. *
  7. * <p>Notice the use of the {@link NotificationManager} when interesting things
  8. * happen in the service. This is generally how background services should
  9. * interact with the user, rather than doing something more disruptive such as
  10. * calling startActivity().
  11. */
  12. public class LocalService extends Service {
  13. private NotificationManager mNM;
  14. /**
  15. * Class for clients to access. Because we know this service always
  16. * runs in the same process as its clients, we don’t need to deal with
  17. * IPC.
  18. */
  19. public class LocalBinder extends Binder {
  20. LocalService getService() {
  21. return LocalService.this;
  22. }
  23. }
  24. @Override
  25. public void onCreate() {
  26. mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
  27. // Display a notification about us starting. We put an icon in the status bar.
  28. showNotification();
  29. }
  30. @Override
  31. public int onStartCommand(Intent intent, int flags, int startId) {
  32. Log.i(“LocalService”, “Received start id “ + startId + “: “ + intent);
  33. // We want this service to continue running until it is explicitly
  34. // stopped, so return sticky.
  35. return START_STICKY;
  36. }
  37. @Override
  38. public void onDestroy() {
  39. // Cancel the persistent notification.
  40. mNM.cancel(R.string.local_service_started);
  41. // Tell the user we stopped.
  42. Toast.makeText(this, R.string.local_service_stopped, Toast.LENGTH_SHORT).show();
  43. }
  44. @Override
  45. public IBinder onBind(Intent intent) {
  46. return mBinder;
  47. }
  48. // This is the object that receives interactions from clients. See
  49. // RemoteService for a more complete example.
  50. private final IBinder mBinder = new LocalBinder();
  51. /**
  52. * Show a notification while this service is running.
  53. */
  54. private void showNotification() {
  55. // In this sample, we’ll use the same text for the ticker and the expanded notification
  56. CharSequence text = getText(R.string.local_service_started);
  57. // Set the icon, scrolling text and timestamp
  58. Notification notification = new Notification(R.drawable.stat_sample, text,
  59. System.currentTimeMillis());
  60. // The PendingIntent to launch our activity if the user selects this notification
  61. PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
  62. new Intent(this, LocalServiceController.class), 0);
  63. // Set the info for the views that show in the notification panel.
  64. notification.setLatestEventInfo(this, getText(R.string.local_service_label),
  65. text, contentIntent);
  66. // Send the notification.
  67. // We use a layout id because it is a unique number. We use it later to cancel.
  68. mNM.notify(R.string.local_service_started, notification);
  69. }
  70. }
   这里可以发现onBind需要返回一个IBinder对象。也就是说和上一例子LocalService不同的是,
1. 添加了一个public内部类继承Binder,并添加getService方法来返回当前的Service对象;
2. 新建一个IBinder对象——new那个Binder内部类;
3. onBind方法返还那个IBinder对象。
Activity:
  1. /**
  2. * <p>Example of binding and unbinding to the {@link LocalService}.
  3. * This demonstrates the implementation of a service which the client will
  4. * bind to, receiving an object through which it can communicate with the service.</p>
  5. */
  6. public class LocalServiceBinding extends Activity {
  7. private boolean mIsBound;
  8. private LocalService mBoundService;
  9. @Override
  10. protected void onCreate(Bundle savedInstanceState) {
  11. super.onCreate(savedInstanceState);
  12. setContentView(R.layout.local_service_binding);
  13. // Watch for button clicks.
  14. Button button = (Button)findViewById(R.id.bind);
  15. button.setOnClickListener(mBindListener);
  16. button = (Button)findViewById(R.id.unbind);
  17. button.setOnClickListener(mUnbindListener);
  18. }
  19. private ServiceConnection mConnection = new ServiceConnection() {
  20. public void onServiceConnected(ComponentName className, IBinder service) {
  21. // This is called when the connection with the service has been
  22. // established, giving us the service object we can use to
  23. // interact with the service. Because we have bound to a explicit
  24. // service that we know is running in our own process, we can
  25. // cast its IBinder to a concrete class and directly access it.
  26. mBoundService = ((LocalService.LocalBinder)service).getService();
  27. // Tell the user about this for our demo.
  28. Toast.makeText(LocalServiceBinding.this, R.string.local_service_connected,
  29. Toast.LENGTH_SHORT).show();
  30. }
  31. public void onServiceDisconnected(ComponentName className) {
  32. // This is called when the connection with the service has been
  33. // unexpectedly disconnected — that is, its process crashed.
  34. // Because it is running in our same process, we should never
  35. // see this happen.
  36. mBoundService = null;
  37. Toast.makeText(LocalServiceBinding.this, R.string.local_service_disconnected,
  38. Toast.LENGTH_SHORT).show();
  39. }
  40. };
  41. private OnClickListener mBindListener = new OnClickListener() {
  42. public void onClick(View v) {
  43. // Establish a connection with the service. We use an explicit
  44. // class name because we want a specific service implementation that
  45. // we know will be running in our own process (and thus won’t be
  46. // supporting component replacement by other applications).
  47. bindService(new Intent(LocalServiceBinding.this,
  48. LocalService.class), mConnection, Context.BIND_AUTO_CREATE);
  49. mIsBound = true;
  50. }
  51. };
  52. private OnClickListener mUnbindListener = new OnClickListener() {
  53. public void onClick(View v) {
  54. if (mIsBound) {
  55. // Detach our existing connection.
  56. unbindService(mConnection);
  57. mIsBound = false;
  58. }
  59. }
  60. };
  61. }
    明显看出这里面添加了一个名为ServiceConnection类,并实现了onServiceConnected(从IBinder获取Service对象)和onServiceDisconnected(set Service to null)。
    而bindService和unbindService方法都是操作这个ServiceConnection对象的。
AndroidManifest.xml里添加:
  1. <service android:name=“.app.LocalService” />
  2. <activity android:name=“.app.LocalServiceBinding” android:label=“@string/activity_local_service_binding”>
  3. <intent-filter>
  4. <action android:name=“android.intent.action.MAIN” />
  5. <category android:name=“android.intent.category.SAMPLE_CODE” />
  6. </intent-filter>
  7. </activity>
这里没什么特别的,因为service没有需要什么特别的action,所以只是声明service而已,而activity和普通的没差别。
运行时,发现调用次序是这样的:
bindService:
1.LocalService : onCreate
2.LocalService : onBind
3.Activity: onServiceConnected

unbindService: 只是调用onDestroy
可见,onStart是不会被调用的,而onServiceDisconnected没有调用的原因在上面代码的注释有说明。
介绍onStartCommand()需要用到的几个常量 (引自官方文档)
START_NOT_STICKY

If the system kills the service after onStartCommand() returns,  do not recreate the service, unless there are pending intents to deliver. This is the safest option to avoid running your service when not necessary and when your application can simply restart any unfinished jobs.

START_STICKY

If the system kills the service after onStartCommand() returns, recreate the service and call onStartCommand(), but  do not redeliver the last intent. Instead, the system calls onStartCommand() with a null intent, unless there were pending intents to start the service, in which case, those intents are delivered. This is suitable for media players (or similar services) that are not executing commands, but running indefinitely and waiting for a job.

START_REDELIVER_INTENT

If the system kills the service after onStartCommand() returns, recreate the service and call onStartCommand() with the last intent that was delivered to the service. Any pending intents are delivered in turn. This is suitable for services that are actively performing a job that should be immediately resumed, such as do wnloading a file.

 

Running a Service in the Foreground
    具体内容查看官方文档,主要是使用 startForeground() 和 stopForeground()方法。

Activity 为什么要有 onStart 生命周期?

Activity 为什么要有 onStart 生命周期? 直接用 onResume 不行吗?

两个 Activity 切换时, 它们的生命周期顺序是

… A#onPause B#onCreate B#onStart B#onResume A#onStop …

这样安排顺序我能理解, 在 A 显示的时候让 B 显示, B 完全展示后, A 走 onStop, 这样用户界面不会黑屏.

但有个疑问, B Activity 展示的回调中为什么要多一个 onStart, 直接通知正在展示的 Activity onRESUME 不行吗

8 条回复    2021-01-15 14:13:56 +08:00
also24
    1

also24   92 天前

我觉得谷歌是希望能严格区分 onPause() 和 onStop() 的。

所以对应着搞了 onResume() 和 onStart() 作为对应的恢复状态。

实际上 Google 确实专门讲了 onPause() 和 onStop() 的差异:
https://developer.android.com/guide/components/activities/activity-lifecycle

janus77
    2

janus77   92 天前 via iPhone

onresume 是 ui 渲染了,有些特殊的需求是希望在渲染之前执行,或者在进入后台(不渲染)的时候扔会执行。所以就需要 onstart 和 onstop
RikkaW
    3

RikkaW   92 天前   ❤️ 5

分屏、画中画等等屏幕上不止一个 Activity (窗口)的情况下就是只有处于焦点的那个是 resumed 状态,其他的都是 started 状态。
zagfai
    4

zagfai   92 天前   ❤️ 1

Android 给开发者制定的架构就是一坨翔
rosu
    5

rosu   92 天前 via Android

start 是可见,resume 是焦点。
narutow
    6

narutow   92 天前

@RikkaW 感谢, 理解了, onStart 和 onRESUME 就是界面可见 和 获得焦点可以同用户交互 的两个回调. 其他比如多窗口可以看到, 但不能交互的情况, Activity 就处于 onPause
kraits
    7

kraits   92 天前 via Android   ❤️ 1

楼主有没有考虑到 Activity 横竖屏切换的生命周期:

onSaveInstanceState()→onPause()→onStop()→onDestroy()→onCreate()→onStart()→onRestoreInstanceState()→onResume()

fromzero
    8

fromzero   85 天前

onStart onRestart 都是满有用的。

关于 kotlin 协程 lifecycleScope 用法和内存泄漏的问题

在随便一个 Activity 上启动此 Activity,然后迅速关闭,leakcanary 就会报内存泄漏。引用链包括了传入的第二个参数 lambda 对象和 Okhttpclient,这里泄漏的原因是什么呢? 一般 retrofit 或者 okhttpclient 对象全局只需要一个就行了吧, 如果还是需要传参和传回调的方式访问网络,该如何正确修改下面的代码呢?

class TestActivity:AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        requestBylifecycleCoroutine("https://www.baidu1.com/"){
            Log.w("TestActivityTAG","result:"+it)
        }
    }

    val client = OkHttpClient.Builder().build()
    fun requestBylifecycleCoroutine(url: String, callBack: (s: String) -> Unit) {
        lifecycleScope.launch {
            val result = withContext(Dispatchers.IO) {
                suspendCoroutine<String> { continuation ->
                    val request = Request.Builder().url(url).build()
                    val newCall = client.newCall(request)
                    newCall.enqueue(object : Callback {
                        override fun onFailure(call: Call, e: IOException) {
                            continuation.resume("fail")
                        }
                        override fun onResponse(call: Call, response: Response) {
                            continuation.resume("success")
                        }
                    })
                }
            }
            callBack(result)
        }
    }
}

21 条回复    2021-01-13 10:25:26 +08:00
lianyue13
    1

lianyue13   92 天前

协程取消的时候,请求没取消吧。用 suspendCancellableCoroutine,在 cancel 里把请求取消试试
m30102
    2

m30102   92 天前

@lianyue13 我换了 suspendCancellableCoroutine, 也在 invokeOnCancellation 添加了 call.cancel(). 和之前一样,leakcanary 还是会有 x retained objects,tap to dump heap,不过很快通知就变成了 All retained objects were garbage collected . 这样是为什呢,我还需要担心吗?
k10ndike
    3

k10ndike   92 天前

@m30102 OKHttpClient 对象是 Activity 持有的么?
hlayk
    4

hlayk   92 天前

如果在 viewmodel 中使用对应的 viewModelScope 那么携程 launch 返回的 job 会由 viewmodel 的 onCleared 统一自动 cancel
你在 activity 中这样使用 lifecycleScope 可以在 onDestory() 将 lifecycleScope.launch {} 返回的对象 job 主动取消下

[Easy Coroutines in Android: viewModelScope]( https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471)

m30102
    5

m30102   92 天前

@k10ndike OKHttpClient 对象就算写在其他类中,同样是单例的话, 也会间接持有到 activity
m30102
    6

m30102   92 天前

@hlayk 如果还需要考虑 onDestory,那么 lifecycleScope 就不用叫 lifefcycleScope 了。实际上 lifefcycleScope 也确实自动 cancel 了,*终的 callBack 没有执行。但是网络请求不一定能成功 cancel,而且回调时间较长,leakcanary 不知为什么显示引用到了 activity 。 如果替换为 viewModelScope 也有效,有时候一个页面就一个网络请求懒得再写一个类直接在 activity 中请求网络,这样貌似 无解?
k10ndike
    7

k10ndike   92 天前

@m30102 呃,Activity 怎么间接持有到的?有测试过吗,创建一个类里面就一个静态的 OkHttpClient 实例
vanxy
    8

vanxy   92 天前 via iPad

你这个等于用了一半的协程,用错了
应该同步地在协程里调用 okhttp,而不是通过回调 callback 的方式

直接 newCall.execute() 拿到 response

m30102
    9

m30102   92 天前

@vanxy 同步的我试了,还是一样。同步的话只是自动取消协程,但是 call.execute()方法开始执行后并不会立即取消。
m30102
    10

m30102   92 天前

@k10ndike 测试过, static 的 OkHttpClient *终会通过 activity 中传入的 lambda 回调,引用到 activity
vanxy
    11

vanxy   91 天前 via iPad

@m30102 你需要在 execute 外面包一个 withContext,这样当 activity finish 了,就不会执行接下来的代码了
sankemao
    12

sankemao   91 天前

我觉得应该是 callback 持有了 activity 的引用,可以试试把结果传给 livedata
m30102
    13

m30102   91 天前

@vanxy 是的,我有用 withContext. 无论是 execute 还是 enqueue ,activity finish 后传的 callback 不会执行,但是 okhttp 的 call 还是会执行的。
m30102
    14

m30102   91 天前

@sankemao liveData 一般配合 viewmodel 用吧,难道非得 mvp 或者 mvvm 把 activity 完全隔开才行吗。。。。
sankemao
    15

sankemao   91 天前 via iPhone

@m30102 你可以在 ac 里面用 livedata 验证下是否还有泄漏。你也可以用其他办法
vanxy
    16

vanxy   91 天前

@m30102 #13 那是自然地呀,call 是一整段的阻塞式调用。 不止是协程, 实际上没有任何方式可以取消它(除非强制停止线程),协程只是可以帮你在取消 withContext 之后的所有代码执行。

因为不执行接下来的 callback 了, 所以 activity 也不会内存泄露了。 也不需要关心 execute 是否执行完成

m30102
    17

m30102   91 天前

@vanxy 我反编译了半天对比了下找到原因了, 是 okhttpclient 的原因,activity 虽然执行 destory 了,但是 okhttpclient 还在执行 call,所以延长了 activity 生命,报泄漏。如果把 okhttpclient 写在其他类中声明 static, 那么 activity 中调用协程方法传的 callBack 必须不能引用 activity 任何成员变量或者 view 等,不然还是会被延长生命,一般传回调就是为了改变 view 等,所以这个是无解的!
vanxy
    18

vanxy   91 天前

@m30102 #17
哦哦, 因为 kotlin 的 lambda 持有了 activity 的引用。

所以要把执行的放到 viewmodel 或者 presenter 里,activity 解除与 viewmodel 或者 presenter 之间的引用之后, 就不会造成泄露了

k10ndike
    19

k10ndike   91 天前 via Android

@m30102 你本地的代码应该有其他操作吧,帖子里那样打 Log 应该不会泄露。可以在请求 callback 里调用 livedata,就不用处理生命周期了
xhpan10
    20

xhpan10   88 天前

协程的代码没有 rxjava 的好看
DiDiz
    21

DiDiz   87 天前

@xhpan10 也要封装的,封装好了协程代码就和普通同步代码一样。相比之下 rxjava 就显得很啰嗦了

单个 Activity 控件过多,对手机影响大么?

单个 Activity 控件过多,对手机影响大么?
以小米 8 为例

现在有个 Activity 用于显示设备的某些信息,设备有好几项属性需要显示,所以我在一个 Activity 里面使用 ViewPager 来显示设备信息,用户左右滑动就能查看设备的信息。

但随着需求越来越多,导致 ViewPager 所在的 Activity 控件越来越多,比如*个页面时设备基本信息,大概有近 20 个信息,每个信息都至少需要一个 TextView 和 TextEdit,有些条目还要 Spinner,多个 RadioButton 等。这样 ViewPager 的一个页面就有 50 多个各种控件

第二个页面是设备连接其他设备的列表,大概 60 多个页面

第三个页面是设备的当前支持的一些协议信息,控件 100+。。。。

然后因为小公司,也没有架构师,所以导致需求不断增加,ViewPager 的页面不断增加,而 ViewPager 所在的 Activity 里的控件非常多,目前各种 TextView,TextEdit,Spinner,Radiobutton,Button 等已经超过 700 个了

我向问下以小米 8 为例,这样一个包含这么多控件的 Activity,对手机的硬件压力大吗?

• 2021-03-04 09:27:17 +08:00
SwiftFrank 1
SwiftFrank 24 天前
建议了解下 Android UI 的渲染相关知识,简单点讲效率跟你单个 Layout(XML)的嵌套层级有关系,推荐使用 ConstrainLayout,页面尽量解耦,方便维护和测试。另外建议看看 Jetpack Compose 的 Navigation 组件,应用单 Activity 的设计。
ssynhtn 2
ssynhtn 24 天前 via Android
太长的页面可以换成 recyclerview
QBugHunter 3
QBugHunter 24 天前
@ssynhtn
每个页面的显示设备某个方面的信息,每个页面之间的信息没有太多的关联以及共同点,用 recyclerview 这种滚动页面用户体验不太好
QBugHunter 4
QBugHunter 24 天前
@SwiftFrank
ViewPager 的每个页面嵌套非常简单,每个页面都是诺干行,每行一个都是 TextView+TextEdit/RadioButton/Spinner 等
QBugHunter 5
QBugHunter 24 天前
@SwiftFrank
每个页面布局文件就 2-3 层嵌套,就是控件的数目比较大。。。
coolesting 6
coolesting 24 天前 via Android
viewpager 里面放的是 fragment,每次翻页都 replace 掉,不会有太大的性能消耗。

如果你数据是一直加载却一直用 add 的方式来添加内容,那估计肯定消耗内存的。
KNOX 7
KNOX 24 天前 via Android
考虑下 ViewPager2? 因为是基于 RecyclerView 实现的,可以利用复用来减轻实时渲染压力,而且不应该只看一个设备。
NexTooo 8
NexTooo 24 天前
@QBugHunter #3 纯好奇,单个页面如果要全塞下几十个 View 显示信息的话,那字体应该不能大到哪儿去,这种情况下用户体验好么……
QBugHunter 9
QBugHunter 24 天前
@NexTooo
没有,比如设备状态,需要 TextView+2 个 RadioButton(开和关)+RadioGroup,总计 4 个控件
然后一个设备信息页面,有十几条这样的信息,一个屏幕可以显示 10 条+,用一个 ScrollView 滚动*多半个屏幕就可以显示全部信息了,对于用户来说不算糟糕,但这样一个页面十几条信息,每条 3-5 个控件,总计就 40+了

一个页面向下滚动*长的都没有超过 1 个屏幕,但那个页面控件 100+。。。

然后这个 ViewPager 有 6 个 view,现在还要加。。。。。

因为设计一改再改,所以 ViewPager 所在的 Activity 的控件数量就非常多了(加上刚提的需求,可能会超过 800 )
mcluyu 10
mcluyu 24 天前
不是 Android 开发,有类似性能调试工具的直接看下渲染,滑动时性能消耗什么样?总的来说你这些东西看起来都不如随便拉出个游戏消耗的 1/10 多。。

Arthur5 11
Arthur5 24 天前
@QBugHunter 一屏就 40 个控件还好吧。不显示的控件又不会绘制到屏幕上,上下滑动的页面用 RecyclerView 复用,左右滑动的 ViewPager 默认只保留当前和左一右一的 3 个页面。
StrorageBox 12
StrorageBox 24 天前
看你怎么写的了,如果 google 建议写法,正确添加的 fragment,没有任何问题。
不过看你所说“一个页面向下滚动*长的都没有超过 1 个屏幕,但那个页面控件 100+。。”,就知道不容乐观了,会有问题的,建议先学习一下 Android 绘制相关知识还有 viewpager1 的机制及 Android 界面的内存使用这三方面知识,然后再着手进行优化。
QBugHunter 13
QBugHunter 24 天前
@Arthur5
一个页面 40-100 个控件,然后这个 ViewPage 目前 6 个页面,现在还要再加 3 个。。。。
NexTooo 14
NexTooo 23 天前
@QBugHunter 我是觉得你可能需要先确定目前的绘制性能消耗如何,有必要了再优化吧,比如*限开到 9 个界面之后的情况。因为感觉如你这么说,要么不动,动起来是个蛮大的工程了。但我感觉若是基本的绘制优化做到位,应该不会有太大的问题,毕竟都是很简单的 View 而不是一些带复杂动画、大内存控件。
你可以看看绘制层级、View 嵌套层级这两块的优化模式,以及 ViewPager1 的机制进一步优化 Fragment 的回收与重建。细节的话我不清楚具体的情况就想不到别的,能想到的就是多个格式不同的文本可以通过富文本拼接的方式合并成一个 TextView
lwlizhe 15
lwlizhe 23 天前
我感觉控件数量跟渲染没啥关系吧

如果卡的话应该是控件本身的问题,或者层级结构什么的,或者说一个控件里面有个 xml 需要解析这种,总之应该是 xml 的锅;
要是直接 textView 那帮没啥 xml 内容的东西应该不影响渲染吧;无非就是 canvas 本身的操作,这块应该没啥问题吧;

我觉的现在你们这更大的问题反而是维护成本问题……假如来个人接手代码,会不会看一眼直接爆炸~
QBugHunter 16
QBugHunter 23 天前
@lwlizhe
这是我接手别人的代码。。。。。
QBugHunter 17
QBugHunter 23 天前
@lwlizhe
想换成碎片,但项目太赶了,我还有别的事情要处理,实在没时间把这个 ViewPager 里的 View 全部换成碎片了,所以只能暂时再加几个 View
pekki 18
pekki 23 天前
屏幕总共就那点大,怎么可能放下几十个控件,详细了解一下 android 屏幕绘制的机制,只不过是不断的重复绘制罢了,性能上影响不大,你看视频每秒都在重绘几十次也不卡啊。
lwlizhe 19
lwlizhe 23 天前
@QBugHunter 略表同情……感觉除了这块,还有其他地方的坑在等着你……这应该是个大坑项目~
hongch 20
hongch 21 天前
如果几十个控件 tv 、et 都会影响到性能的话,你让那些做游戏的怎么办?