0
点赞
收藏
分享

微信扫一扫

【Android春招每日一练】(十五) 剑指4题+Android进阶

伽马星系 2022-01-30 阅读 53

文章目录

概览

剑指offer:数组中数字出现的次数Ⅰ、数组中数字出现的次数 II、和为s的两个数字、和为s的连续正数序列
Android进阶:Parcelable和Serializable的区别、App启动流程、Android性能优化、Android 内存泄漏总结

剑指offer

1.57 数组中数字出现的次数Ⅰ

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VPOFLMtD-1643379789951)(D:\Typora\img\1614836837-oygHyk-Picture2.png)]

/*
1.先对所有数字进行一次异或,得到两个出现一次的数字的异或值。
2.在异或结果中找到任意为 1 的位。
3.根据这一位对所有的数字进行分组。
4.在每个组内进行异或操作,得到两个数字。
*/
class Solution {
    public int[] singleNumbers(int[] nums) {
        int x = 0, y = 0, n = 0, m = 1;
        for(int num : nums)               // 1. 遍历异或
            n ^= num;
        while((n & m) == 0)               // 2. 循环左移,计算 m
            m <<= 1;
        for(int num: nums) {              // 3. 遍历 nums 分组
            if((num & m) != 0) x ^= num;  // 4. 当 num & m != 0
            else y ^= num;                // 4. 当 num & m == 0
        }
        return new int[] {x, y};          // 5. 返回出现一次的数字
    }
}

1.58 数组中数字出现的次数 II

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4
//位运算
class Solution {
    public int singleNumber(int[] nums) {
        int[] counts = new int[32];
        for(int num : nums) {				//记录所有数字的各二进制位的1的出现次数
            for(int j = 0; j < 32; j++) {
                counts[j] += num & 1;
                num >>>= 1;
            }
        }
        int res = 0, m = 3;
        for(int i = 0; i < 32; i++) {		
            counts[31 - i] %= m;			//将各元素对3求余,则结果为 “只出现一次的数字” 的各二进制位。
            res <<= 1;						//移位恢复数字
            res |= counts[31 - i] ;
        }
        return res;
    }
}

1.59 和为s的两个数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
//双指针
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int i = 0,j = nums.length - 1,sum;
        while(i < j){
            sum = nums[i] + nums[j];
            if(sum < target) i++;
            else if(sum > target) j--;
            else return new int[] {nums[i],nums[j]};
        }
        return new int[0];
    }
}

1.60 和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3sRoyEp-1643379789953)(D:\Typora\img\1611495306-LsrxgS-Picture2.png)]

//滑动窗口
class Solution {
    public int[][] findContinuousSequence(int target) {
        int i = 1, j = 2, s = 3;
        List<int[]> res = new ArrayList<>();
        while(i < j) {
            if(s == target) {
                int[] ans = new int[j - i + 1];
                for(int k = i; k <= j; k++)
                    ans[k - i] = k;
                res.add(ans);
            }
            if(s >= target) {
                s -= i;
                i++;
            } else {
                j++;
                s += j;
            }
        }
        return res.toArray(new int[0][]);
    }
}

Android进阶

Parcelable和Serializable的区别

作用

Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间**(AIDL)**高效的传输数据而设计,这些数据仅在内存中存在, Parcelable是通过IBinder通信的消息的载体。

效率及选择

Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化。

编程实现

对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可。而Parcelable则需要实现writeToParcel、describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。

//Serializable
public class Person implements Serializable{
	private static final long serialVersionUID = -7060210544600464481L;
 	private String name;
	 private int age;
  
 public String getName(){
 	 return name;
 }
  
 public void setName(String name){
	  this.name = name;
 }
  
 public int getAge(){
	  return age;
 }
  
 public void setAge(int age){
 	 this.age = age;
 }
}
//Parcelable
public class MyParcelable implements Parcelable { 
	private int mData; 
	private String mStr; 
    
	public int describeContents() { 
		return 0; 
	}

	// 写数据进行保存 
	public void writeToParcel(Parcel out, int flags) { 
		out.writeInt(mData); 
		out.writeString(mStr); 
	}

	// 用来创建自定义的Parcelable的对象 
	public static final Parcelable.Creator<MyParcelable> CREATOR = new Parcelable.Creator<MyParcelable>() { 
		public MyParcelable createFromParcel(Parcel in) { 
			return new MyParcelable(in); 
	}

	public MyParcelable[] newArray(int size) { 
		return new MyParcelable[size]; 
	}};

	// 读数据进行恢复 
	private MyParcelable(Parcel in) { 
		mData = in.readInt(); 
		mStr = in.readString(); 
	} 
}

Serializable序列化不保存静态变量,可以使用Transient关键字对部分字段不进行序列化,也可以覆盖writeObject、readObject方法以实现序列化过程自定义。

App启动流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTwy3mW8-1643379869780)(D:\Typora\img\image-20220127142150419.png)]

启动流程:

①点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;

②system_server进程接收到请求后,向zygote进程发送创建进程的请求;

③Zygote进程fork出新的子进程,即App进程;

④App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;

⑤system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;

⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;

⑦主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

zygote

zygote意为“受精卵“。Android是基于Linux系统的,而在Linux中,所有的进程都是由init进程直接或者是间接fork出来的,zygote进程也不例外。

我们都知道,每一个App其实都是

● 一个单独的dalvik虚拟机

● 一个单独的进程

所以当系统里面的第一个zygote进程运行之后,在这之后再开启App,就相当于开启一个新的进程。而为了实现资源共用和更快的启动速度,Android系统开启新进程的方式,是通过fork第一个zygote进程实现的。所以说,除了第一个zygote进程,其他应用所在的进程都是zygote的子进程。

system_server

SystemServer也是一个进程,而且是由zygote进程fork出来的。

知道了SystemServer的本质,我们对它就不算太陌生了,这个进程是Android Framework里面两大非常重要的进程之一——另外一个进程就是上面的zygote进程。

为什么说SystemServer非常重要呢?因为系统里面重要的服务都是在这个进程里面开启的,比如 ActivityManagerService、PackageManagerService、WindowManagerService等等。

ActivityManagerService

ActivityManagerService,简称AMS,服务端对象,负责系统中所有Activity的生命周期。

ActivityManagerService进行初始化的时机很明确,就是在SystemServer进程开启的时候,就会初始化ActivityManagerService。

如果想打开一个App的话,需要AMS去通知zygote进程,除此之外,其实所有的Activity的开启、暂停、关闭都需要AMS来控制,所以我们说,AMS负责系统中所有Activity的生命周期。

在Android系统中,任何一个Activity的启动都是由AMS和应用程序进程(主要是ActivityThread)相互配合来完成的。AMS服务统一调度系统中所有进程的Activity启动,而每个Activity的启动过程则由其所属的进程具体来完成。

Launcher

Launcher本质上也是一个应用程序,和我们的App一样,也是继承自Activity 。Launcher实现了点击、长按等回调接口,来接收用户的输入。我们点击图标的时候,捕捉图标点击事件,然后startActivity()发送对应的Intent请求。

InstrumentationActivityThread

每个Activity都持有Instrumentation对象的一个引用,但是整个进程只会存在一个Instrumentation对象。 Instrumentation这个类里面的方法大多数和Application和Activity有关,这个类就是完成对ApplicationActivity初始化和生命周期的工具类。Instrumentation这个类很重要,对Activity生命周期方法的调用根本就离不开他,他可以说是一个大管家。

ActivityThread,依赖于UI线程。App和AMS是通过Binder传递信息的,那么ActivityThread就是专门与AMS的外交工作的。

ApplicationThread

前面我们已经知道了App的启动以及Activity的显示都需要AMS的控制,那么我们便需要和服务端的沟通,而这个沟通是双向的。

客户端**–>**服务端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KCDGE3ZS-1643379869781)(D:\Typora\img\image-20220127144919975.png)]

而且由于继承了同样的公共接口类,ActivityManagerProxy提供了与ActivityManagerService一样的函数原型,使用户感觉不出Server是运行在本地还是远端,从而可以更加方便的调用这些重要的系统服务。

服务端**–>**客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8yGFP6V-1643379869782)(D:\Typora\img\image-20220127144930982.png)]

还是通过Binder通信,不过是换了另外一对,换成了ApplicationThread和ApplicationThreadProxy。 他们也都实现了相同的接口IApplicationThread

总结:创建进程—》绑定Application—》显示Activity界面

Android性能优化

布局优化

关于布局优化的思想很简单,就是尽量减少布局文件的层级。这个道理很浅显,布局中的层级少了,就意味着Android绘制时的工作量少了,那么程序的性能自然就提高了。

删除布局中无用的控件和层次,其次有选择地使用性能比较低的ViewGroup

例如:如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多的CPU时间。FrameLayout和LinearLayout一样都是一种简单高效的ViewGroup,因此可以考虑使用它们,但是很多时候单纯通过一个LinearLayout或者FrameLayout无法实现产品效果,需要通过嵌套的方式来完成。这种情况下还是建议采用RelativeLayout,因为ViewGroup的嵌套就相当于增加了布局的层级,同样会降低程序的性能。

采用标签**,标签,ViewStub**。

标签主要用于布局重用。

标签一般和配合使用,可以降低减少布局的层级。

ViewStub提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内存,提高了程序初始化效率。

避免多度绘制

过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。

绘制优化

绘制优化是指ViewonDraw方法要避免执行大量的操作,这主要体现在两个方面:

onDraw中不要创建新的局部对象。

因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。

onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。

内存泄漏优化

内存泄漏是开发过程中的一个需要重视的问题,但是由于内存泄露问题对开发人员的经验和开发意识有较高的要求,因此也是开发人员最容易犯的错误之一。内存泄露的优化分为两个方面:

在开发过程中避免写出有内存泄漏的代码

通过一些分析工具比如MAT来找出潜在的内存泄露,然后解决。

响应速度优化

响应速度优化的核心思想就是避免在主线程中做耗时操作。

如果有耗时操作,可以开启子线程执行,即采用异步的方式来执行耗时操作。

如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至ANR。

Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver如果10秒钟之内还未执行完操作也会出现ANR

当一个进程发生了ANR之后,系统会在/data/anr目录下创建一个文件traces.txt,通过分析这个文件就能定位出ANR的原因。

ListView/RecycleView及Bitmap优化

ListView/RecycleView的优化思想主要从以下几个方面入手:

①使用ViewHolder模式来提高效率

②异步加载:耗时的操作放在异步线程中

③ListView/RecycleView的滑动时停止加载和分页加载

Bitmap优化

主要是对加载图片进行压缩,避免加载图片多大导致OOM出现。

线程优化

线程优化的思想就是采用线程池,避免程序中存在大量的Thread。线程池可以重用内部的线程,从而避免了线程的创建和销毁锁带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而导致阻塞现象的发生。因此在实际开发中,尽量采用线程池,而不是每次都要创建一个Thread对象。

其他性能优化建议

①避免过度的创建对象

②不要过度使用枚举,枚举占用的内存空间要比整型大

③常量请使用static final来修饰

④使用一些Android特有的数据结构,比如SparseArray和Pair等

⑤适当采用软引用和弱引用

⑥采用内存缓存和磁盘缓存

⑦尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄漏。

Android 内存泄漏总结

内存泄漏,简单通俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收 。

Java 内存分配策略

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

  • 栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储于堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的

Java内存泄漏

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsG5GEmt-1643379869783)(D:\Typora\img\image-20220127151132783.png)]

Android中常见的内存泄漏

  • 集合类泄漏

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。

  • 单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。

public class AppManager { 
	private static AppManager instance; 
	private Context context; 
	private AppManager(Context context) { 
		this.context = context; //this.context = context.getApplicationContext();
	}
    
	public static AppManager getInstance(Context context) { 
		if (instance == null) { 
		instance = new AppManager(context); 
		}
		return instance; 
	} 
} 

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

  • 匿名内部类/非静态内部类和异步线程

    • 非静态内部类创建静态实例造成的内存泄漏

    非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为: 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例。

    • 匿名内部类

    android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露。

public class MainActivity extends Activity { 
... 
	Runnable ref1 = new MyRunable(); 
	Runnable ref2 = new Runnable() {  
		@Override 
		public void run() { 
            
		} 
	};
... 
} 

ref1没什么特别的。 但ref2这个匿名类的实现对象里面多了一个引用:this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

  • Handler 造成的内存泄漏

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

public class SampleActivity extends Activity { 
	private final Handler mLeakyHandler = new Handler() { 
		@Override 
		public void handleMessage(Message msg) { 
		// ... 
		} 
	}

	@Override 
	protected void onCreate(Bundle savedInstanceState) { 
		super.onCreate(savedInstanceState); 
		// Post a message and delay its execution for 10 minutes. 
		mLeakyHandler.postDelayed(new Runnable() { 
			@Override 
			public void run() { /* ... */ } 
		}, 1000 * 60 * 10); 
		// Go back to the previous Activity. 
		finish(); 
	} 
} 

在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指SampleActivity)

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入Activity,避免直接将 Activity 作为 context 传进去

private static class MyHandler extends Handler { 
    private final WeakReference<SampleActivity> mActivity;
    public MyHandler(SampleActivity activity) {
        mActivity = new WeakReference<SampleActivity>(activity); 
    }
    @Override
    public void handleMessage(Message msg) { 
        SampleActivity activity = mActivity.get(); 
        if (activity != null) { // ... 
        } 
    }
}

即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

  • 尽量避免使用 static 成员变量

如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,因为如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了 电量、流量,你的app不得不被用户卸载或者静默。

修复方法是: 不要在类初始时初始化静态成员。可以考虑lazy初始化。 架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来

  • 避免 override finalize()

1、finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是:

虚拟机调用GC的时间不确定

Finalize daemon线程被调度到的时间不确定

2、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize方法,再次被 GC 时也不会再执行了,原因是:

含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。

3、含有Finalize方法的object需要至少经过两轮GC才有可能被释放。

  • 资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

**工具分析 **

MAT(Memory AnalysisTools) 和 YourKit

总结

1.内容较多,每个知识点都需要仔细理解,能够用自己的语言讲述原理

举报

相关推荐

0 条评论