Android interview must ask performance optimization

For Android developers, knowing basic application development skills is often not enough, because whether it is a job or an interview, developers need to know a lot of performance optimization, which is very important to improve the application experience. For Android development, performance optimization mainly focuses on the following aspects: startup optimization, rendering optimization, memory optimization, network optimization, stuck detection and optimization, power consumption optimization, installation package volume optimization, security issues, etc.

1. Start optimization

The startup speed of an application can directly affect the user experience. If it starts slowly, it may cause the user to uninstall and abandon the application.

1.1 Optimization of cold start, hot start and warm start

1.1.1 Concept

For Android applications, according to the startup method, it can be divided into three types: cold startup, hot startup and warm startup.

  • Cold start: Starting an App when there is no App process in the system (such as when the APP is started for the first time or the APP is completely killed) is called a cold start.
  • Warm start: The process of starting the app again when the Home button is pressed or the app is switched to the background in other circumstances.
  • Warm start: Warm start includes some operations of cold start, but the App process still exists, which means that it has more overhead than hot start.

It can be seen that hot start is the fastest startup, while warm start is a startup method between cold start and hot start. Cold start is the slowest because it involves the creation of many processes. The following is the task flow related to cold start:

1.1.2 Visual optimization

In cold start mode, the system will start three tasks:

  • Load and start the application.
  • A blank startup window for the application is displayed immediately upon launch.
  • Create application process.

Once the system creates the application process, the application process will enter the next stage and complete some things as follows.

  • Create app object
  • Start the main thread (main thread)
  • Create an Activity object for application entry
  • Populate loading layout View
  • Execute the drawing process of View on the screen. measure -> layout -> draw

After the application process completes its first draw, the system process swaps the currently displayed background window, replacing it with the main activity. At this point, the user can start using the application. Because the creation process of the App application process is determined by the software and hardware of the mobile phone, we can only perform some visual optimization during this creation process.

1.1.3 Start theme optimization

During a cold start, after the application process is created, you need to set the theme of the startup window. At present, most applications will first enter a splash screen page (LaunchActivity) to display application information during the startup meeting. If other third-party services are initialized in the Application, a white screen problem will occur at startup.

In order to connect our splash screen page more smoothly and seamlessly, you can set the splash screen page image in the Theme of the startup Activity, so that the image of the startup window will be the splash screen page image instead of the white screen.

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@drawable/lunch</item>  //闪屏页图片
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
    </style>

1.2 Code optimization

The method of setting themes can only be used in scenarios that are not very demanding, and this kind of optimization treats the symptoms but not the root cause. The key lies in the optimization of the code. In order to optimize, we need to have some basic data.

1.2.1 Cold start time-consuming statistics

ADB command method: Enter the following command in the Terminal of Android Studio to view the startup time of the page. The command is as follows:

adb shell am start  -W packagename/[packagename].首屏Activity

After the execution is completed, the following information will be output on the console:

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.optimize.performance/.MainActivity }
Status: ok
Activity: com.optimize.performance/.MainActivity
ThisTime: 563
TotalTime: 563
WaitTime: 575
Complete

There are three field information in the above log, namely ThisTime, TotalTime and WaitTime.

  • ThisTime : The time it takes to start the last Activity
  • TotalTime : the time taken to start all activities
  • WaitTime : The total time taken by AMS to start Activity

Log method: Buried time method is another way to count online time. This method records the start time and end time, and then takes the difference between the two. First, you need to define a tool class for counting time:

class LaunchRecord {

    companion object {

        private var sStart: Long = 0

        fun startRecord() {
            sStart = System.currentTimeMillis()
        }

        fun endRecord() {
            endRecord("")
        }

        fun endRecord(postion: String) {
            val cost = System.currentTimeMillis() - sStart
            println("===$postion===$cost")
        }
    }
}

When starting up, we directly manage it in the attachBaseContext of Application. So where should the start and end be done? The suggestion to end burying the point is to bury the point when the page data is displayed. You can use the following methods:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mTextView.viewTreeObserver.addOnDrawListener {
            LaunchRecord.endRecord("onDraw")
        }

    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        LaunchRecord.endRecord("onWindowFocusChanged")
    }
}

1.2.2 Optimizing detection tools

When doing startup optimization, we can use third-party tools to help us clarify the methods or threads at each stage, the execution time of the CPU, etc. Here we mainly introduce the following two tools: TraceView and SysTrace.

TraceView

TraceView displays execution time, call stack and other information in a graphical form. The information is relatively comprehensive and includes all threads, as shown in the figure below.

The results generated using TraceView detection will be placed in the Andrid/data/packagename/files path. Because the information collected by Traceview is relatively comprehensive, it will cause serious operating overhead and the overall APP will run slower. Therefore, we cannot distinguish whether Traceview affects our startup time.

SysTrace Systrace combines Android kernel data to generate an HTML report. From the report, we can see the execution time of each thread, method time consumption, CPU execution time, etc.

For API versions 18 and above, you can directly use TraceCompat to capture data, because this is a compatible API.

开始:TraceCompat.beginSection("tag ")
结束:TraceCompat.endSection()

Then, execute the following script.

python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app

Here you can popularize the meaning of each prefix:

  • b: size of collected data
  • t: time
  • a: monitored application package name
  • o: The name of the generated file

Systrace has low overhead, is a lightweight tool, and can intuitively reflect CPU utilization.

2. UI rendering optimization

The Android system redraws the Activity every 16ms. Therefore, our application must complete all logical operations of screen refresh within 16ms, and each frame can only stay for 16ms, otherwise frame drops will occur. Whether Android applications are stuck is directly related to UI rendering.

2.1CPU、GPU

The screen refresh frequency of most mobile phones is 60hz, that is, if the task of this frame is not completed within 1000/60=16.67ms, frame loss will occur. Frame loss is the direct cause of interface lag. , rendering operations usually rely on two core components: CPU and GPU. The CPU is responsible for calculation operations including Measure, Layout, etc., and the GPU is responsible for Rasterization operations.

The so-called rasterization is the process of converting vector graphics into bitmaps. The display on the mobile phone is displayed pixel by pixel. For example, a Button, TextView and other components are split into pixels and displayed on the mobile phone screen. The purpose of UI rendering optimization is to reduce the pressure on the CPU and GPU, remove unnecessary operations, and ensure that all CPU and GPU calculations, drawing, rendering, etc. operations are processed within 16ms per frame, so that the UI can be displayed smoothly and smoothly. come out.

2.2 Overdrawing

The first step in UI rendering optimization is to find Overdraw, which describes a pixel on the screen being drawn multiple times in the same frame. In an overlapping UI layout, if the invisible UI is also performing drawing operations or the latter control blocks the previous control, some pixel areas will be drawn multiple times, thereby increasing the pressure on the CPU and GPU.

So how to find out where the Overdraw is in the layout? It's very simple, just open the developer options on the phone, and then turn on the switch to debug GPU overdrawing, and then you can see whether the layout of the application is Overdrawn, as shown in the figure below.

Blue, light green, light red, and dark red represent four different degrees of Overdraw situations. 1x, 2x, 3x, and 4x respectively indicate that the same pixel has been drawn multiple times in the same frame, and 1x indicates once (the last Ideally), 4x means 4 times (worst case), and what we need to eliminate is 3x and 4x.

2.3 Solve the OverDraw of custom View

We know that when customizing a View, the onDraw method is sometimes overridden, but the Android system cannot detect the specific operations that will be performed in onDraw, so the system cannot do some optimizations for us. This places high demands on programmers. If the View has a large amount of overlap, it will cause a waste of CPU and GPU resources. At this time, we can use canvas.clipRect() to help the system identify those visible areas.

This method can specify a rectangular area. Only this area will be drawn, and other areas will be ignored. Below we further illustrate the use of OverDraw through a small Demo provided by Google.

In the code below, the DroidCard class encapsulates card information. The code is as follows.

public class DroidCard {

public int x;//左侧绘制起点
public int width;
public int height;
public Bitmap bitmap;

public DroidCard(Resources res,int resId,int x){
this.bitmap = BitmapFactory.decodeResource(res,resId);
this.x = x;
this.width = this.bitmap.getWidth();
this.height = this.bitmap.getHeight();
 }
}

The code for custom View is as follows:

public class DroidCardsView extends View {
//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;

private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

private Paint paint = new Paint();

public DroidCardsView(Context context) {
super(context);
initCards();
}

public DroidCardsView(Context context, AttributeSet attrs) {
super(context, attrs);
initCards();
}
/**
* 初始化卡片集合
*/
protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (DroidCard c : mDroidCards){
drawDroidCard(canvas, c);
}
invalidate();
}

/**
* 绘制DroidCard
*/
private void drawDroidCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}
}

Then, we run the code and turn on the overdraw switch of the phone. The effect is as follows:

It can be seen that the light red area has obviously been drawn three times, which is caused by the overlap of the pictures. So how to solve this problem? In fact, analysis can find that only one-third of the bottom picture needs to be drawn, and only one-third of the bottom two pictures need to be drawn to ensure that the top picture is completely drawn. The optimized code is as follows:

public class DroidCardsView extends View {

//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;

private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

private Paint paint = new Paint();

public DroidCardsView(Context context) {
super(context);
initCards();
}

public DroidCardsView(Context context, AttributeSet attrs) {
super(context, attrs);
initCards();
}
/**
* 初始化卡片集合
*/
protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res, R.drawable.alex,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.claire,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.kathryn,mCardLeft));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mDroidCards.size() - 1; i++){
drawDroidCard(canvas, mDroidCards,i);
}
drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1));
invalidate();
}

/**
* 绘制最后一个DroidCard
* @param canvas
* @param c
*/
private void drawLastDroidCard(Canvas canvas,DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}

/**
* 绘制DroidCard
* @param canvas
* @param mDroidCards
* @param i
*/
private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) {
DroidCard c = mDroidCards.get(i);
canvas.save();
canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
canvas.restore();
 }
}

In the above code, we use the clipRect method of Canvas to clip out an area before drawing, so that when drawing, we will only draw within this area, and the excess will not be drawn. Rerun the above code, the effect is as shown below.

2.4 Hierarchy Viewer

Hierarchy Viewer is a tool built into Android Device Monitor that allows developers to measure the layout speed of each view in the layout hierarchy and helps developers find performance bottlenecks caused by the view hierarchy. Hierarchy Viewer can distinguish the relative performance of Measure, Layout, and Executive of the layout through three different colors: red, yellow, and green.

Open

  1. Connect the device to the computer. If a dialog box appears on the device prompting you to allow USB debugging? , click OK.
  2. Open your project in Android Studio, build and run the project on your device.
  3. Start Android Device Monitor. Android Studio may display the Disable adb integration dialog box because only one process can connect to the device through adb at a time, and Android Device Monitor is requesting the connection. Therefore, please click Yes.
  4. In the menu bar, select Window > Open Perspective and click Hierarchy View.
  5. Double-click the application's package name in the Windows tab on the left. This populates the relevant pane with the app's view hierarchy.

The key to improving layout performance is to keep the layout hierarchy as flat as possible and avoid repeated nested layouts. If we write a layout with a deep level, it will seriously increase the burden on the CPU and cause serious performance lag. For information about the use of Hierarchy Viewer, please refer to: Use Hierarchy Viewer to analyze layout .

2.5 Memory Jitter

After we optimize the tree structure and overdraw of the view, you may still feel that your app has problems such as freezing, frame dropping, or slow sliding. We need to check whether there is memory jitter. The so-called memory jitter refers to the phenomenon that the UI thread is frequently blocked due to frequent memory creation and GC.

Android has a mechanism to automatically manage memory, but improper use of memory can still easily cause serious performance problems. Creating too many objects in the same frame is something that requires special attention. Creating a large number of objects in the same frame may cause continuous GC operations. When performing GC operations, any operation of all threads will need to be suspended until GC The operation is completed. A large number of non-stop GC operations will significantly occupy the frame interval time. If too many GC operations are performed during the frame interval, the page will freeze.

In Android development, there are two main reasons for frequent GC operations:

  • Memory jitter. The so-called memory jitter means that a large number of objects are generated in a short period of time and then released immediately in a short period of time.
  • If a large number of objects are generated in a short period of time and exceed the threshold, and there is insufficient memory, GC operations will also be triggered.

Android memory jitter can be detected using Android Studio's Profiler.

Then, click record to record the memory information and find the location where memory jitter occurs. Of course, you can also directly locate the code location through Jump to Source.

In order to avoid memory jitter, we need to avoid allocating objects in the for loop and occupying memory. We need to try to move the creation of objects outside the loop body. The onDraw method in the custom View also needs attention. Each time drawing and animation occur on the screen, During the execution process, the onDraw method will be called to avoid performing complex operations in the onDraw method and avoiding creating objects. For those situations where it is unavoidable to create objects, we can consider the object pool model to solve the problem of frequent creation and destruction through object pools. However, it should be noted here that after the use is completed, the objects in the object pool need to be manually released.

3. Memory optimization

3.1 Memory management

In the previous Java basics section, we also gave a basic introduction to Java's memory management model. Reference link: Java basics that must be asked in Android high-frequency interviews

3.1.1 Memory area

In Java's memory model, the memory area is divided into five areas: method area, heap, program counter, local method stack, and virtual machine stack, as shown in the figure below.

method area

  • The thread shared area is used to store class information, static variables, constants, and code data compiled by the just-in-time compiler.
  • OOM occurs when memory allocation requirements cannot be met.

heap

  • The thread shared area is the largest piece of memory managed by the JAVA virtual machine and is created when the virtual machine starts.
  • Store object instances. Almost all object instances are allocated on the heap, which is the main area managed by GC.

Virtual machine stack

  • In the thread private area, each Java method will create a stack frame to store information such as local variable tables, operand stacks, dynamic links, method exits, etc. The process from the beginning to the end of method execution is the process of pushing and popping stack frames into the virtual machine stack.
  • The local variable table stores basic data types, object references, and returnAddress types that are known at compile time. The required memory space will be allocated during compilation. When entering a method, the space of the local variable table in the frame is completely determined and does not need to be changed at runtime.
  • If the stack depth requested by the thread is greater than the maximum depth allowed by the virtual machine, a SatckOverFlowError error will be thrown.
  • When a virtual machine is dynamically expanded, if it cannot apply for enough memory, an OutOfMemoryError will be thrown.

native method stack

  • Serving the Native methods in the virtual machine, there are no mandatory regulations on the language, data structure, and usage methods used in the local method stack, and the virtual machine can implement it by itself.
  • The size of the occupied memory area is not fixed and can be dynamically expanded as needed.

program counter

  • A small memory space, private to the thread, that stores the bytecode line number indicator executed by the current thread.
  • The bytecode interpreter changes the value of this counter to select the next bytecode instruction to be executed: branch, loop, jump, etc.
  • Each thread has an independent program counter
  • The only area in the Java virtual machine that does not OOM

3.1.2 Garbage collection

Mark and clear algorithm The mark and clear algorithm is mainly divided into two stages. First, the objects that need to be recycled are marked, and then all marked objects are recycled uniformly after the marking is completed; Disadvantages:

  • Efficiency issues: Both marking and clearing processes are inefficient.
  • Space problem: Marking and clearing will lead to a lot of discontinuous memory fragments, which will lead to the problem that when large objects need to be allocated, they cannot find enough continuous space and have to trigger GC.

The copy algorithm divides the available memory into two small blocks of the same size according to space, and only uses one of them at a time. When this block of memory is used up, the surviving objects are copied to another block of memory, and then the entire memory area object is Clear it out. Recycling memory for the entire half area every time will not cause fragmentation problems, and is simple and efficient to implement. Disadvantages: The memory needs to be reduced to half of the original size, and the space cost is too high.

Marking and Collating Algorithm Marking and Collating Algorithm The marking process is the same as the mark and clear algorithm, but the clearing process does not clean recyclable objects directly, but moves all surviving objects to one end, and then cleans the memory outside the end boundary in a centralized manner.

Generational collection algorithm Contemporary virtual machine garbage collection algorithms all use generational collection algorithms to collect. The memory is divided into the new generation and the old generation according to the different life cycles of objects, and then the most appropriate recycling algorithm is used according to the characteristics of each generation.

  • There are fewer surviving objects in the new generation, and a large number of objects die during each garbage collection. Generally, a copy algorithm is used, and garbage collection can be achieved by only paying the cost of copying a small number of surviving objects;
  • There are many surviving objects in the old generation and there is no extra space for allocation guarantee, so the mark clearing algorithm and mark sorting algorithm must be used for recycling;

3.2 Memory leak

The so-called memory leak refers to objects that are useless and cannot be recycled in the memory. The phenomenon is that it will cause memory jitter and reduced available memory, which will lead to frequent GC, freezes, and OOM.

Here is a piece of code that simulates a memory leak:

/**
 * 模拟内存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);

        // 添加静态类引用
        CallBackManager.addCallBack(this);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
//        CallBackManager.removeCallBack(this);
    }
    @Override
    public void dpOperate() {
        // do sth
    }

When we use the Memory Profiler tool to view the memory curve, we find that the memory is constantly rising, as shown in the figure below.

If we want to analyze and locate the specific location of memory leaks, we can use the MAT tool. First, use the MAT tool to generate an hprof file, click dump to convert the current memory information into an hprof file, and convert the generated file into a MAT readable file. Execute the conversion command to complete the conversion. The generated file is located in the Android/sdk/platorm-tools path.

hprof-conv 刚刚生成的hprof文件 memory-mat.hprof

Use mat to open the hprof file you just converted, and then use Android Studio to open the hprof file, as shown in the figure below.

Then click [Historygram] on the panel and search for MemoryLeakActivity to view relevant information about the corresponding leaked file.

Then, view all reference objects and get the relevant reference chains, as shown below.

You can see that GC Roots is CallBackManager

Therefore, we can remove the CallBackManager reference when the Activity is destroyed.

@Override
protected void onDestroy() {
    super.onDestroy();
    CallBackManager.removeCallBack(this);
}

Of course, the above is just an example of using the MAT analysis tool. Other memory leaks can be solved with the help of the MAT analysis tool.

3.3 Large image memory optimization

In Android development, we often encounter the problem of memory leaks caused by loading large images. For this scenario, there is a general solution, which is to use ARTHook to detect unreasonable images. We know that there are two main ways to obtain the memory occupied by Bitmap:

  • Through the getByteCount method, but it needs to be obtained at runtime
  • width *height *memory occupied by one pixel *compression ratio of the resource directory where the image is located

The ARTHook method can elegantly obtain unreasonable images and is less intrusive. However, it is generally used offline due to compatibility issues. To use ARTHook, you need to install the following dependencies:

implementation 'me.weishu:epic:0.3.6'

Then customize the Hook method as shown below.

public class CheckBitmapHook extends XC_MethodHook {
    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        ImageView imageView = (ImageView)param.thisObject;
        checkBitmap(imageView,imageView.getDrawable());
    }
    private static void checkBitmap(Object o,Drawable drawable) {
        if(drawable instanceof BitmapDrawable && o instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if(bitmap != null) {
                final View view = (View)o;
                int width = view.getWidth();
                int height = view.getHeight();
                if(width > 0 && height > 0) {
                    if(bitmap.getWidth() > (width <<1) && bitmap.getHeight() > (height << 1)) {
                        warn(bitmap.getWidth(),bitmap.getHeight(),width,height,
                                new RuntimeException("Bitmap size is too large"));
                    }
                } else {
                    final Throwable stacktrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(
                            new ViewTreeObserver.OnPreDrawListener() {
                                @Override public boolean onPreDraw() {
                                    int w = view.getWidth();
                                    int h = view.getHeight();
                                    if(w > 0 && h > 0) {
                                        if (bitmap.getWidth() >= (w << 1)
                                                && bitmap.getHeight() >= (h << 1)) {
                                            warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stacktrace);
                                        }
                                        view.getViewTreeObserver().removeOnPreDrawListener(this);
                                    }
                                    return true;
                                }
                            });
                }
            }
        }
    }
    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();
        LogUtils.i(warnInfo);

Finally, inject the Hook when the Application is initialized.

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap", Bitmap.class,
                new CheckBitmapHook());
    }
});

3.4 Online monitoring

3.4.1 Conventional scheme

Option 1: Obtain the currently occupied memory size in a specific scenario. If the current memory size exceeds 80% of the system's maximum memory, perform a Dump (Debug.dumpHprofData()) on the current memory, select a suitable time to upload the hprof file, and then use the MAT tool Analyze the file manually.

shortcoming:

  • Dump files are relatively large and are directly related to user usage time and object tree.
  • Larger files lead to higher upload failure rates and difficulty in analysis.

Option 2 : Bring LeakCannary online, add preset suspicion points, perform memory leak monitoring on the suspicion points, and report memory leaks back to the server if found.

shortcoming:

  • It has low versatility and requires preset suspicion points. It cannot monitor places without preset suspicion points.
  • LeakCanary analysis is time-consuming and memory-consuming, and OOM may occur.
3.4.2 LeakCannary transformation

The transformation mainly involves the following points:

  • Change the need to preset suspicion points to automatically search for suspicion points, and automatically set suspicion points in object classes that occupy a large amount of memory.
  • LeakCanary is slow to analyze leaked links and has been modified to only analyze objects with large Retain size.
  • The analysis process will OOM because LeakCannary will load all the analysis objects into the memory during analysis. We can record the number and occupied size of the analysis objects, cut the analysis objects, and not load them all into the memory.

The completed transformation steps are as follows:

  1. Monitor general indicators: standby memory, memory occupied by key modules, OOM rate
  2. Monitor the number of GCs, GC time, etc. within the life cycle of the APP and the life cycle of key module interfaces.
  3. Bring customized LeakCanary online to automatically analyze online memory leaks

4. Network optimization

4.1 Impact of network optimization

The network connection of the App has a lot of impact on the user, and in most cases it is very intuitive, which directly affects the user’s experience of using the App. The most important points are: Traffic: The traffic consumption of the App is relatively sensitive to the user . Yes, after all, traffic costs money. Nowadays, most people have traffic monitoring tool Apps installed on their mobile phones to monitor the traffic usage of the App. If our App does not control this aspect well, it will cause problems for users. Use experience. Power : The power is not that obvious to users. The average user may not pay much attention to it. But as mentioned in power optimization, network connection (radio) is a factor that has a great impact on power. So we should also pay attention to it. . User waiting : that is, user experience. A good user experience is the first step for us to retain users. If the App request wait time is long, it will give the user the feeling of network lag and slow application response. If there is a comparison, there are alternatives. , our App is likely to be ruthlessly abandoned by users.

4.2 Network analysis tools

Tools that can be used for network analysis include Monitor, proxy tools, etc.

4.2.1 Network Monitor

The built-in Monitor tool in Android Studio provides a Network Monitor that can help developers perform network analysis. The following is a typical Network Monitor diagram.

  • Rx — R(ecive) represents downstream traffic, that is, download reception.
  • Tx — T(ransmit) represents upstream traffic, that is, upload and send.

Network Monitor tracks the data requests of selected applications in real time. We can connect to the mobile phone, select the debug application process, and then operate the page request we need to analyze on the App.

4.2.2 Agent tools

The network proxy tool has two functions. One is to intercept network request response packets and analyze network requests; the other is to set up the proxy network. In mobile app development, it is generally used to test different network environments, such as Wifi/4G/3G/weak network, etc. .

Nowadays, there are many proxy tools available, such as Wireshark, Fiddler, Charles, etc.

4.3 Network optimization plan

For network optimization, optimization is mainly carried out from two aspects:

  1. Reduce active time : Reduce the frequency of network data acquisition, thereby reducing radio power consumption and controlling power usage.
  2. Compressed data packet size : Compressed data packets can reduce traffic consumption and make each request faster.

Based on the above solution, the following common solutions can be obtained:

4.3.1 Interface design

1. API design The API design between the App and the server should consider the frequency of network requests, the status of resources, etc. So that the App can complete business requirements and interface display with fewer requests.

For example, registration and login. Normally there are two APIs, registration and login, but when designing the API, we should include an implicit login for the registration interface. This prevents the App from having to request a login interface after registration.

2. Use Gzip compression

Use Gzip to compress request and response to reduce the amount of data transmitted, thereby reducing traffic consumption. When using network request frameworks such as Retrofit to make network requests, Gzip compression is performed by default.

3. Before using Protocol Buffer, we used XML to transmit data. Later, we used JSON instead of XML, largely for the sake of readability and reducing the amount of data. In game development, in order to ensure the accuracy and timeliness of data, Google launched the Protocol Buffer data exchange format.

4. Obtain images of different resolutions according to network conditions . When we use Taobao or JD.com, we will see that the application will acquire images of different resolutions according to network conditions to avoid waste of traffic and improve user experience.

4.3.2 Proper use of network cache

Appropriate use of cache can not only make our application look faster, but also avoid unnecessary traffic consumption and bring a better user experience.

1. Packaging network requests

When the interface design cannot meet our business needs. For example, maybe one interface needs to request multiple interfaces, or the network is good and we want to get more data when it is in Wifi state, etc. At this time, you can package some network requests, such as requesting a list and obtaining detailed data of items with higher header click rates.

2. Monitor the device status . In order to improve the user experience, we can monitor the device usage status, and then combine it with the JobScheduler to execute network requests. For example, for Splash advertising images, we can download and cache them locally when connected to Wifi; news apps can be cached offline while charging and on Wifi.

4.3.3 Weak network testing & optimization

1. Weak network test There are several ways to simulate a weak network for testing:

Android Emulator Normally, we create and start the Android emulator to set the network speed and delay, as shown in the figure below.

Then, the emulator command we use at startup is as follows.

$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22

2. Network proxy tool You can also simulate network conditions by using a network proxy tool. Taking Charles as an example, keep the mobile phone and PC in the same LAN, set the proxy mode to manual in the advanced settings of the mobile wifi settings, fill in the PC IP address for the proxy ip, and the default port number is 8888.

5. Power consumption optimization

In fact, if our application needs to play videos, obtain GPS information, or is a game application, the power consumption will be serious. How to judge which power consumption can be avoided or needs to be optimized? We can open the power consumption ranking list that comes with the phone and find that "Honor of Kings" has been used for more than 7 hours. At this time, users have expectations for the power consumption of "Honor of Kings".

5.1 Optimization direction

Suppose at this time that he finds an application that he has not used much at all, but consumes a lot of power, it will be mercilessly killed by the system. Therefore, the first direction of power consumption optimization is to optimize the background power consumption of applications.

Knowing how the system calculates power consumption, we can also know what applications should not do in the background, such as long-term acquisition of WakeLock, WiFi and Bluetooth scanning, etc., as well as background services. Why is it said that the first direction of power consumption optimization is to optimize the background power consumption of applications? This is because the most stringent requirements for most manufacturers' pre-installed projects are the background standby power consumption of applications.

Of course, we will not completely ignore the power consumption at the front desk, but the standards will be relaxed a lot. Let’s look at the picture below. If the system pops up this dialog box for your application, users may be able to tolerate it for WeChat, but for most other applications, many users may directly add you to the background. on the restricted list.

The second direction of power consumption optimization is to comply with the rules of the system and make the system think that your power consumption is normal.

Android P and above versions monitor background power consumption through Android Vitals, so we need to comply with the rules of Android Vitals. Currently, its specific rules are as follows.

It can be seen that the Android system is currently more concerned about background Alarm wake-up, background network, background WiFi scanning and some long-term WakeLock to prevent the system from sleeping in the background, because these may cause power consumption problems.

5.2 Power consumption monitoring

5.2.1 Android Vitals

Android Vitals has several power monitoring solutions and rules that can help us monitor power consumption.

After using it for a while, I found that it wasn't that easy to use. Taking Alarm wakeup as an example, Vitals uses more than 10 times per hour as a rule. Since this rule cannot be modified, many times we may want to make more detailed distinctions between different system versions. Secondly, like the Battery Historian, we can only get the components marked by wakeup, but we cannot get the applied stack, nor can we get information such as whether the phone is charging at the time or the remaining battery capacity. The picture below is the information obtained by wakeup.

The same goes for networking, WiFi scans, and WakeLock. Although Vitals helped us narrow down the scope of the investigation, we still couldn't confirm the specific cause of the problem.

5.3 How to monitor power consumption

As mentioned before, Android Vitals is not that easy to use, and it is actually not usable at all for domestic applications. So what should our power consumption monitoring system monitor, and how should it be done? First, let’s take a look at how to specifically monitor power consumption?

  • Monitoring information : To put it simply, we monitor what the system cares about, and the main focus should be on background power consumption monitoring. Things like Alarm wakeup, WakeLock, WiFi scans, and Network are all necessary, and others can be based on the actual situation of the application. If it is a map application, it is allowed to obtain GPS in the background; if it is a pedometer application, there is no big problem in obtaining Sensor in the background.
  • On-site information : The monitoring system hopes to obtain complete stack information, such as which line of code initiated WiFi scans, which line of code applied for WakeLock, etc. There are also some information such as whether the phone is charging at the time, the power level of the phone, the application foreground and background time, and the CPU status, which can also help us troubleshoot certain problems.
  • Refining rules : Finally, we need to abstract the monitored content into rules. Of course, the items or parameters monitored by different applications are different. Since the specifics of each application are different, here are some simple rules to follow.

5.3.2 Hook solution

After clarifying what we need to monitor and the specific rules, let's look at the technical solution for power monitoring. Let’s first look at the Hook solution. The advantage of the Hook solution is that user access is very simple, there is no need to modify the code, and the cost of access is relatively low. Below I will take several commonly used rules as examples to see how to use Java Hook to achieve monitoring purposes.

1. WakeLock WakeLock is used to prevent the CPU, screen and even the keyboard from sleeping. Similar to Alarm and JobService, they will also apply for WakeLock to complete background CPU operations. The core control code of WakeLock is in PowerManagerService, and the implementation method is very simple, as shown below.

// 代理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);

@Override
public void beforeInvoke(Method method, Object[] args) {
    // 申请 Wakelock
    if (method.getName().equals("acquireWakeLock")) {
        if (isAppBackground()) {
            // 应用后台逻辑,获取应用堆栈等等     
         } else {
            // 应用前台逻辑,获取应用堆栈等等
         }
    // 释放 Wakelock
    } else if (method.getName().equals("releaseWakeLock")) {
       // 释放的逻辑    
    }
}

2. Alarm Alarm is used to perform some scheduled repetitive tasks. It has four types in total, among which the ELAPSED_REALTIME_WAKEUP and RTC_WAKEUP types will wake up the device. Similarly, the core control logic of Alarm is in AlarmManagerService, which is implemented as follows.

// 代理 AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);

public void beforeInvoke(Method method, Object[] args) {
    // 设置 Alarm
    if (method.getName().equals("set")) {
        // 不同版本参数类型的适配,获取应用堆栈等等
    // 清除 Alarm
    } else if (method.getName().equals("remove")) {
        // 清除的逻辑
    }
}

In addition to WakeLock and Alarm, for the background CPU, we can use methods related to lag monitoring; for the background network, we can also use network monitoring related methods; for GPS monitoring, we can use Hook to proxy LOCATION_SERVICE; for Sensor, we can use "mSensorListeners" in Hook SENSOR_SERVICE can get some information.

Finally, we save the stack information for applying for resources. When we trigger a rule to report a problem, we can upload the collected stack information, whether the battery is charged, CPU information, application front and back time and other auxiliary information to the background.

5.3.3 Instrumentation method

Although using Hook is simple, it may not be easy to find suitable Hook points for certain rules, and after Android P, many Hook points are no longer supported. For compatibility reasons, the first thing I thought of was instrumentation. Take WakeLock as an example:

public class WakelockMetrics {
    // Wakelock 申请
    public void acquire(PowerManager.WakeLock wakelock) {
        wakeLock.acquire();
        // 在这里增加 Wakelock 申请监控逻辑
    }
    // Wakelock 释放
    public void release(PowerManager.WakeLock wakelock, int flags) {
        wakelock.release();
        // 在这里增加 Wakelock 释放监控逻辑
    }
}

If you have researched on power consumption, you must be aware of Facebook's open source library for power consumption monitoring, Battery-Metrics. It monitors very comprehensive data, including Alarm, WakeLock, Camera, CPU, Network, etc., and also collects power charging status, power level information. Unfortunately, Battery-Metrics only provides a series of basic classes, and developers still need to modify a large amount of source code for actual use.

6. Installation package optimization

Apps on the market now range from tens of megabytes in size to hundreds of megabytes in size. The smaller the installation package, the less data traffic during downloading, the better user experience, faster downloading, and faster installation. So what aspects can we start to optimize for the installation package?

6.1 Commonly used optimization strategies

1. Clean up useless resources. During the Android packaging process, if the code has references to resources and codes, it will be packaged into the App. In order to prevent these abandoned codes and resources from being packaged into the App, we need to clean up these useless resources in a timely manner. code and resources to reduce the size of the App. The method of cleaning is to click [Refactor]->[Remove unused Resource] of android Studio in sequence, as shown in the figure below.

2. Use Lint tool

The Lint tool is still very useful. It gives us the points that need to be optimized:

  • Detect unused layouts and delete them
  • Delete unused resources
  • It is recommended that some unused characters in String.xml be deleted as well.

3. Turn on shrinkResources to remove useless resources. Configure shrinkResources true in build.gradle. Useless resources will be automatically cleared when packaging. However, after experiments, it was found that the package will not, but will use smaller resources for some useless resources. Replace things. Note that "useless" here means that all parent functions that call the image are ultimately discarded code, and shrinkResources true can only remove the situation where there is no parent function call.

    android {
        buildTypes {
            release {
                shrinkResources true
            }
        }
    }

In addition, most applications do not actually need international support for dozens of languages, and language support files can also be deleted.

6.2 Resource compression

In Android development, there are many built-in pictures, and these pictures take up a lot of volume. Therefore, in order to reduce the size of the package, we can compress the resources. Commonly used methods are:

  1. Use compressed images: Using compressed images can effectively reduce the size of the App.
  2. Only use one set of pictures: For most apps, only one set of design drawings is enough.
  3. Use jpg images without alpha value: For large non-transparent images, jpg will have a significant advantage over png in size. Although not absolute, it will usually be reduced to more than half.
  4. Use tinypng lossy compression: It supports uploading PNG images to the official website for compression, then downloading and saving. The compression of PNG can be within 1/3 while maintaining the alpha channel, and the compression loss is basically invisible to the naked eye.
  5. Use webp format: webp supports transparency, compression ratio, and takes up a smaller size than JPG images. It is natively supported starting from Android 4.0+, but does not support transparency. It is not until Android 4.2.1+ that the display of webp with transparency is supported. Pay special attention when using it.
  6. Use svg: Vector graphics are composed of points and lines. Unlike bitmaps, they can maintain clarity even if they are enlarged. Moreover, using vector graphics can save 30 to 40% of space compared to bitmap designs.
  7. Compress the packaged images: Use 7zip compression method to compress the images. You can directly use WeChat’s open source AndResGuard compression scheme.
    apply plugin: 'AndResGuard'
    buildscript {
        dependencies {
            classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.1.7'
        }
    }
    andResGuard {
        mappingFile = null
        use7zip = true
        useSign = true
        keepRoot = false
        // add <your_application_id>.R.drawable.icon into whitelist.
        // because the launcher will get thgge icon with his name
        def packageName = <your_application_id>
                whiteList = [
        //for your icon
        packageName + ".R.drawable.icon",
                //for fabric
                packageName + ".R.string.com.crashlytics.*",
                //for umeng update
                packageName + ".R.string.umeng*",
                packageName + ".R.string.UM*",
                packageName + ".R.string.tb_*",
                packageName + ".R.layout.umeng*",
                packageName + ".R.layout.tb_*",
                packageName + ".R.drawable.umeng*",
                packageName + ".R.drawable.tb_*",
                packageName + ".R.anim.umeng*",
                packageName + ".R.color.umeng*",
                packageName + ".R.color.tb_*",
                packageName + ".R.style.*UM*",
                packageName + ".R.style.umeng*",
                packageName + ".R.id.umeng*"
        ]
        compressFilePattern = [
        "*.png",
                "*.jpg",
                "*.jpeg",
                "*.gif",
                "resources.arsc"
        ]
        sevenzip {
            artifact = 'com.tencent.mm:SevenZip:1.1.7'
            //path = "/usr/local/bin/7za"
        }
    }

6.3 Dynamic loading of resources

In front-end development, dynamically loading resources can effectively reduce the size of apk. In addition, it only provides support for mainstream architectures, such as arm, and may consider not supporting mips and x86 architectures, which can greatly reduce the size of the APK.

Of course, in addition to the optimization scenarios mentioned above, the optimization of Android App also includes storage optimization, multi-thread optimization, and crash handling.

In order to help everyone understand performance optimization more comprehensively and clearly, we have prepared relevant core notes (also the underlying logic):https://qr18.cn/FVlo89

Core notes on performance optimization:https://qr18.cn/FVlo89

Startup optimization

, memory optimization,

UI optimization,

network optimization,

Bitmap optimization and image compression optimization : multi-thread concurrency optimization and data transmission efficiency optimization, volume package optimizationhttps://qr18.cn/FVlo89




"Android Performance Monitoring Framework":https://qr18.cn/FVlo89

"Android Framework Learning Manual":https://qr18.cn/AQpN4J

  1. Boot Init process
  2. Start the Zygote process on boot
  3. Start the SystemServer process on boot
  4. Binder driver
  5. AMS startup process
  6. PMS startup process
  7. Launcher startup process
  8. Android four major components
  9. Android system service-Input event distribution process
  10. Android underlying rendering - screen refresh mechanism source code analysis
  11. Android source code analysis in practice

Guess you like

Origin blog.csdn.net/maniuT/article/details/132700957