fix: app memory leak; feat: app foreground service

This commit is contained in:
hyb1996
2018-09-19 14:40:05 +08:00
parent 44fe0079c3
commit 62aafaf06b
22 changed files with 167 additions and 305 deletions

Binary file not shown.

View File

@@ -8,8 +8,8 @@ android {
applicationId "org.autojs.autojs"
minSdkVersion 17
targetSdkVersion 23
versionCode 420
versionName "4.0.3 Alpha"
versionCode 421
versionName "4.0.3 Alpha2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
ndk {
@@ -24,7 +24,7 @@ android {
}
release {
shrinkResources false
minifyEnabled false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@@ -83,10 +83,10 @@ dependencies {
})
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
// Android supports
compile 'com.android.support:appcompat-v7:25.4.0'
compile 'com.android.support:cardview-v7:25.4.0'
compile 'com.android.support:design:25.4.0'
compile 'com.android.support:multidex:1.0.2'
compile 'com.android.support:appcompat-v7:27.1.1'
compile 'com.android.support:cardview-v7:27.1.1'
compile 'com.android.support:design:27.1.1'
compile 'com.android.support:multidex:1.0.3'
// Personal libraries
compile 'com.github.hyb1996:MutableTheme:0.2.2'
// Material Dialogs
@@ -146,6 +146,10 @@ dependencies {
compile('com.afollestad.material-dialogs:commons:0.9.2.3', {
exclude group: 'com.android.support'
})
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
// Optional, if you use support library fragments:
debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
compile project(':automator')
compile project(':common')
compile project(':autojs')

View File

@@ -116,6 +116,7 @@
android:theme="@style/IssueReporterTheme"/>
<service android:name=".external.foreground.ForegroundService"/>
<service android:name=".external.ScriptExecutionIntentService"/>
<activity android:name=".external.tasker.TaskPrefEditActivity_"/>

View File

@@ -1,8 +1,11 @@
package org.autojs.autojs;
import android.app.Application;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.multidex.MultiDex;
import android.support.multidex.MultiDexApplication;
import android.view.View;
import android.widget.ImageView;
@@ -14,6 +17,7 @@ import com.raizlabs.android.dbflow.config.DatabaseConfig;
import com.raizlabs.android.dbflow.config.FlowConfig;
import com.raizlabs.android.dbflow.config.FlowManager;
import com.raizlabs.android.dbflow.runtime.DirectModelNotifier;
import com.squareup.leakcanary.LeakCanary;
import com.stardust.app.GlobalAppContext;
import com.stardust.autojs.core.ui.inflater.ImageLoader;
import com.stardust.autojs.core.ui.inflater.util.Drawables;
@@ -67,6 +71,11 @@ public class App extends MultiDexApplication {
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
}
private void setUpDebugEnvironment() {
CrashHandler crashHandler = new CrashHandler(ErrorReportActivity.class);
@@ -77,6 +86,12 @@ public class App extends MultiDexApplication {
crashHandler.setBuglyHandler(Thread.getDefaultUncaughtExceptionHandler());
Thread.setDefaultUncaughtExceptionHandler(crashHandler);
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
private void init() {

View File

@@ -201,4 +201,8 @@ public class Pref {
getString(R.string.default_value_script_dir_path));
return new File(Environment.getExternalStorageDirectory(), dir).getPath();
}
public static boolean isForegroundServiceEnabled() {
return def().getBoolean(getString(R.string.key_foreground_servie), false);
}
}

View File

@@ -0,0 +1,58 @@
package org.autojs.autojs.external.foreground;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import org.autojs.autojs.R;
public class ForegroundService extends Service {
private static final int NOTIFICATION_ID = 117;
private static final String CHANEL_ID = "foreground";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground();
return START_STICKY;
}
private void startForeground(){
startForeground(NOTIFICATION_ID, buildNotification());
}
private Notification buildNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel();
}
return new NotificationCompat.Builder(this, CHANEL_ID)
.setContentTitle(getString(R.string.foreground_notification_title))
.setContentText(getString(R.string.foreground_notification_text))
.setSmallIcon(R.drawable.autojs_material)
.build();
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createNotificationChannel(){
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert manager != null;
CharSequence name = getString(R.string.foreground_notification_channel_name);
String description = getString(R.string.foreground_notification_channel_name);
NotificationChannel channel = new NotificationChannel(CHANEL_ID, name, NotificationManager.IMPORTANCE_HIGH);
channel.setDescription(description);
manager.createNotificationChannel(channel);
}
}

View File

@@ -63,4 +63,9 @@ public class TaskerScriptEditActivity extends BaseActivity {
TaskerScriptEditActivity.super.finish();
}
@Override
protected void onDestroy() {
mEditorView.destroy();
super.onDestroy();
}
}

View File

@@ -161,4 +161,7 @@ public class AutoCompletion {
}
public void shutdown(){
mExecutorService.shutdownNow();
}
}

View File

@@ -318,6 +318,7 @@ public class ScriptOperations {
String fileName = DownloadManager.parseFileNameLocally(url);
return new FileChooserDialogBuilder(mContext)
.title(R.string.text_select_save_path)
.dir(Pref.getScriptDirPath())
.chooseDir()
.singleChoice()
.map(saveDir -> new File(saveDir, fileName).getPath())

View File

@@ -180,6 +180,7 @@ EditActivity extends BaseActivity implements OnActivityResultDelegate.DelegateHo
@Override
protected void onDestroy() {
mEditorView.destroy();
super.onDestroy();
}

View File

@@ -392,7 +392,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
Snackbar.make(this, R.string.text_start_running, Snackbar.LENGTH_SHORT).show();
}
ScriptExecution execution = Scripts.runWithBroadcastSender(mFile);
if(execution == null){
if (execution == null) {
return null;
}
mScriptExecutionId = execution.getId();
@@ -671,4 +671,9 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC
super.onRestoreInstanceState(superData);
setMenuItemStatus(R.id.run, mScriptExecutionId == ScriptExecution.NO_ID);
}
public void destroy() {
mEditor.destroy();
mAutoCompletion.shutdown();
}
}

View File

@@ -373,6 +373,11 @@ public class CodeEditor extends HVScrollView {
mCodeEditText.removeAllBreakpoints();
}
public void destroy(){
mJavaScriptHighlighter.shutdown();
mJsBeautifier.shutdown();
}
@Override
protected void onDraw(Canvas canvas) {
int codeWidth = getWidth() - getPaddingLeft() - getPaddingRight();

View File

@@ -2,10 +2,12 @@ package org.autojs.autojs.ui.edit.editor;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.TimingLogger;
import com.stardust.autojs.rhino.TokenStream;
import com.stardust.pio.UncheckedIOException;
import org.autojs.autojs.ui.edit.theme.Theme;
import org.autojs.autojs.ui.widget.SimpleTextWatcher;
@@ -46,7 +48,7 @@ public class JavaScriptHighlighter implements SimpleTextWatcher.AfterTextChanged
@Override
public String toString() {
return super.toString() + "{count = " + mCount + ", length = " + mText.length() + "}";
return super.toString() + "{count = " + mCount + ", length = " + mText.length() + "}";
}
public int getCharCount() {
@@ -60,16 +62,18 @@ public class JavaScriptHighlighter implements SimpleTextWatcher.AfterTextChanged
private Theme mTheme;
private CodeEditText mCodeEditText;
private ThreadPoolExecutor mExecutorService = new ThreadPoolExecutor(3, 6,
private ThreadPoolExecutor mExecutorService = new ThreadPoolExecutor(3, 6,
2L, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
private AtomicInteger mRunningHighlighterId = new AtomicInteger();
private TimingLogger mLogger = new TimingLogger(CodeEditText.LOG_TAG, "highlight");
private final TextWatcher mTextWatcher;
public JavaScriptHighlighter(Theme theme, CodeEditText codeEditText) {
mExecutorService.allowCoreThreadTimeOut(true);
mTheme = theme;
mCodeEditText = codeEditText;
codeEditText.addTextChangedListener(new SimpleTextWatcher(this));
mTextWatcher = new SimpleTextWatcher(this);
codeEditText.addTextChangedListener(mTextWatcher);
}
@Override
@@ -112,5 +116,9 @@ public class JavaScriptHighlighter implements SimpleTextWatcher.AfterTextChanged
mCodeEditText.updateHighlightTokens(highlightTokens);
}
public void shutdown() {
mCodeEditText.removeTextChangedListener(mTextWatcher);
mExecutorService.shutdownNow();
}
}

View File

@@ -1,290 +0,0 @@
package org.autojs.autojs.ui.edit.editor;
/*
* Copyright 2016. SHENQINCI(沈钦赐)<946736079@qq.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
import java.util.Stack;
/**
* 撤销和恢复撤销
* Created by 沈钦赐 on 16/6/23.
*/
public class TextViewRedoUndoer {
//操作序号(一次编辑可能对应多个操作,如替换文字,就是删除+插入)
int index;
//撤销栈
Stack<Action> history = new Stack<>();
//恢复栈
Stack<Action> historyBack = new Stack<>();
private Editable editable;
private EditText editText;
//自动操作标志,防止重复回调,导致无限撤销
private boolean flag = false;
private int mInitialHistoryStackSize;
private boolean mEnabled = true;
public TextViewRedoUndoer(@NonNull EditText editText) {
this.editable = editText.getText();
this.editText = editText;
editText.addTextChangedListener(new Watcher());
}
protected void onEditableChanged(Editable s) {
}
protected void onTextChanged(Editable s) {
if (history.size() < mInitialHistoryStackSize) {
mInitialHistoryStackSize = 0;
}
}
/**
* 清理记录
* Clear history.
*/
public final void clearHistory() {
history.clear();
historyBack.clear();
mInitialHistoryStackSize = 0;
}
public boolean canUndo() {
return !history.empty();
}
/**
* 撤销
* Undo.
*/
public final void undo() {
if (history.empty()) return;
//锁定操作
flag = true;
Action action = history.pop();
historyBack.push(action);
if (action.isAdd) {
//撤销添加
editable.delete(action.startCursor, action.startCursor + action.actionTarget.length());
editText.setSelection(action.startCursor, action.startCursor);
} else {
//插销删除
editable.insert(action.startCursor, action.actionTarget);
if (action.endCursor == action.startCursor) {
editText.setSelection(action.startCursor + action.actionTarget.length());
} else {
editText.setSelection(action.startCursor, action.endCursor);
}
}
//释放操作
flag = false;
//判断是否是下一个动作是否和本动作是同一个操作,直到不同为止
if (!history.empty() && history.peek().index == action.index) {
undo();
}
}
public boolean canRedo() {
return !historyBack.empty();
}
/**
* 恢复
* Redo.
*/
public final void redo() {
if (historyBack.empty()) return;
flag = true;
Action action = historyBack.pop();
history.push(action);
if (action.isAdd) {
//恢复添加
editable.insert(action.startCursor, action.actionTarget);
if (action.endCursor == action.startCursor) {
editText.setSelection(action.startCursor + action.actionTarget.length());
} else {
editText.setSelection(action.startCursor, action.endCursor);
}
} else {
//恢复删除
editable.delete(action.startCursor, action.startCursor + action.actionTarget.length());
editText.setSelection(action.startCursor, action.startCursor);
}
flag = false;
//判断是否是下一个动作是否和本动作是同一个操作
if (!historyBack.empty() && historyBack.peek().index == action.index)
redo();
}
/**
* 首次设置文本
* Set default text.
*/
public final void setDefaultText(CharSequence text) {
clearHistory();
flag = true;
editable.replace(0, editable.length(), text);
flag = false;
}
public boolean isTextChanged() {
return history.size() != mInitialHistoryStackSize;
}
public void markTextAsUnchanged() {
mInitialHistoryStackSize = history.size();
}
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
public boolean isEnabled() {
return mEnabled;
}
private class Watcher implements TextWatcher {
/**
* Before text changed.
*
* @param s the s
* @param start the start 起始光标
* @param count the endCursor 选择数量
* @param after the after 替换增加的文字数
*/
@Override
public final void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (!editText.isEnabled() || !mEnabled) {
return;
}
if (flag) return;
int end = start + count;
if (end > start && end <= s.length()) {
CharSequence charSequence = s.subSequence(start, end);
//删除了文字
if (charSequence.length() > 0) {
Action action = new Action(charSequence, start, false);
if (count > 1) {
//如果一次超过一个字符,说名用户选择了,然后替换或者删除操作
action.setSelectCount(count);
} else if (count == 1 && count == after) {
//一个字符替换
action.setSelectCount(count);
}
//还有一种情况:选择一个字符,然后删除(暂时没有考虑这种情况)
history.push(action);
historyBack.clear();
action.setIndex(++index);
}
}
}
/**
* On text changed.
*
* @param s the s
* @param start the start 起始光标
* @param before the before 选择数量
* @param count the endCursor 添加的数量
*/
@Override
public final void onTextChanged(CharSequence s, int start, int before, int count) {
if (!editText.isEnabled() || !mEnabled) {
return;
}
if (flag) return;
int end = start + count;
if (end > start) {
CharSequence charSequence = s.subSequence(start, end);
//添加文字
if (charSequence.length() > 0) {
Action action = new Action(charSequence, start, true);
history.push(action);
historyBack.clear();
if (before > 0) {
//文字替换(先删除再增加),删除和增加是同一个操作,所以不需要增加序号
action.setIndex(index);
} else {
action.setIndex(++index);
}
}
}
}
@Override
public final void afterTextChanged(Editable s) {
if (!editText.isEnabled() || !mEnabled) {
return;
}
if (flag) return;
if (s != editable) {
editable = s;
onEditableChanged(s);
}
TextViewRedoUndoer.this.onTextChanged(s);
}
}
private class Action {
/**
* 改变字符.
*/
CharSequence actionTarget;
/**
* 光标位置.
*/
int startCursor;
int endCursor;
/**
* 标志增加操作.
*/
boolean isAdd;
/**
* 操作序号.
*/
int index;
public Action(CharSequence actionTag, int startCursor, boolean add) {
this.actionTarget = actionTag;
this.startCursor = startCursor;
this.endCursor = startCursor;
this.isAdd = add;
}
public void setSelectCount(int count) {
this.endCursor = endCursor + count;
}
public void setIndex(int index) {
this.index = index;
}
}
}

View File

@@ -9,6 +9,7 @@ import java.util.LinkedList;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Handler;
import android.text.Editable;
import android.text.Selection;
import android.text.TextWatcher;
@@ -46,6 +47,10 @@ public class TextViewUndoRedo {
private int mInitialHistoryStackSize;
private Handler mHandler = new Handler();
private int mTextChangeId = 0;
private boolean mTextChanging = false;
// =================================================================== //
/**
@@ -404,7 +409,6 @@ public class TextViewUndoRedo {
if (mIsUndoOrRedo || !mEnabled) {
return;
}
mBeforeChange = s.subSequence(start, start + count);
}
@@ -415,7 +419,10 @@ public class TextViewUndoRedo {
}
mAfterChange = s.subSequence(start, start + count);
mTextChangeId++;
mEditHistory.add(new EditItem(start, mBeforeChange, mAfterChange));
int textChangeId = mTextChangeId;
//TODO 增加连续输入文字当成一次撤销的功能
}
public void afterTextChanged(Editable s) {

View File

@@ -1,5 +1,6 @@
package org.autojs.autojs.ui.main.drawer;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
@@ -22,6 +23,7 @@ import com.stardust.notification.NotificationListenerService;
import org.autojs.autojs.Pref;
import org.autojs.autojs.R;
import org.autojs.autojs.external.foreground.ForegroundService;
import org.autojs.autojs.network.GlideApp;
import org.autojs.autojs.network.UserService;
import org.autojs.autojs.tool.EmptyObservers;
@@ -107,6 +109,8 @@ public class DrawerFragment extends android.support.v4.app.Fragment {
};
private DrawerMenuItem mNotificationPermissionItem = new DrawerMenuItem(R.drawable.ic_ali_notification, R.string.text_notification_permission, 0, this::goToNotificationServiceSettings);
private DrawerMenuItem mForegroundServiceItem = new DrawerMenuItem(R.drawable.ic_service_green, R.string.text_foreground_service, R.string.key_foreground_servie, this::toggleForegroundService);
private DrawerMenuItem mFloatingWindowItem = new DrawerMenuItem(R.drawable.ic_robot_64, R.string.text_floating_window, 0, this::showOrDismissFloatingWindow);
private DrawerMenuItem mCheckForUpdatesItem = new DrawerMenuItem(R.drawable.ic_check_for_updates, R.string.text_check_for_updates, this::checkForUpdates);
@@ -142,6 +146,10 @@ public class DrawerFragment extends android.support.v4.app.Fragment {
setChecked(mFloatingWindowItem, true);
}
setChecked(mConnectionItem, DevPluginService.getInstance().isConnected());
if(Pref.isForegroundServiceEnabled()){
GlobalAppContext.get().startService(new Intent(getContext(), ForegroundService.class));
setChecked(mForegroundServiceItem, true);
}
}
private void initMenuItems() {
@@ -150,6 +158,7 @@ public class DrawerFragment extends android.support.v4.app.Fragment {
mAccessibilityServiceItem,
mStableModeItem,
mNotificationPermissionItem,
mForegroundServiceItem,
new DrawerMenuGroup(R.string.text_script_record),
mFloatingWindowItem,
@@ -165,6 +174,7 @@ public class DrawerFragment extends android.support.v4.app.Fragment {
}
@SuppressLint("CheckResult")
@Click(R.id.avatar)
void loginOrShowUserInfo() {
UserService.getInstance()
@@ -251,6 +261,17 @@ public class DrawerFragment extends android.support.v4.app.Fragment {
}
}
private void toggleForegroundService(DrawerMenuItemViewHolder holder) {
boolean checked = holder.getSwitchCompat().isChecked();
if(checked){
GlobalAppContext.get().startService(new Intent(getContext(), ForegroundService.class));
}else {
GlobalAppContext.get().stopService(new Intent(getContext(), ForegroundService.class));
}
}
private void inputRemoteHost() {
String host = Pref.getServerAddressOrDefault(WifiTool.getRouterIp(getActivity()));
new MaterialDialog.Builder(getActivity())

View File

@@ -398,4 +398,9 @@
<string name="text_new_project">新建项目</string>
<string name="text_js_file">js文件</string>
<string name="text_invalid_project">无效项目</string>
<string name="text_foreground_service">前台服务</string>
<string name="key_foreground_servie">key_foreground_service</string>
<string name="foreground_notification_channel_name">前台服务通知</string>
<string name="foreground_notification_title">Auto.js保持运行中</string>
<string name="foreground_notification_text">点击进入主界面</string>
</resources>

View File

@@ -6,6 +6,7 @@ import com.stardust.autojs.engine.ScriptEngine;
import com.stardust.autojs.engine.ScriptEngineManager;
import com.stardust.autojs.runtime.exception.ScriptInterruptedException;
import com.stardust.autojs.script.ScriptSource;
import com.stardust.lang.ThreadCompat;
/**
* Created by Stardust on 2017/5/1.
@@ -24,6 +25,7 @@ public class RunnableScriptExecution extends ScriptExecution.AbstractScriptExecu
@Override
public void run() {
ThreadCompat.currentThread().setName("ScriptThread-" + getId() + "[" + getSource() + "]");
execute();
}

View File

@@ -3,6 +3,7 @@ package com.stardust.autojs.rhino;
import android.util.Log;
import com.android.dx.command.dexer.Main;
import com.android.dx.dex.file.DexFile;
import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;

View File

@@ -46,7 +46,7 @@ public class Threads {
TimerThread thread = createThread(runnable);
synchronized (mThreads) {
mThreads.add(thread);
thread.setName(thread.getName() + " (Spawn-" + mSpawnCount + ")");
thread.setName(mMainThread.getName() + " (Spawn-" + mSpawnCount + ")");
mSpawnCount++;
}
thread.start();

View File

@@ -19,6 +19,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
@@ -35,7 +36,7 @@ public class JsBeautifier {
void onException(Exception e);
}
private Executor mExecutor = Executors.newSingleThreadExecutor();
private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private Context mContext;
private Function mJsBeautifyFunction;
private org.mozilla.javascript.Context mScriptContext;
@@ -122,4 +123,9 @@ public class JsBeautifier {
}
}
public void shutdown(){
mExecutor.shutdownNow();
mView = null;
}
}

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":416},"path":"commonRelease-4.0.2 Alpha11.apk","properties":{"packageId":"org.autojs.autojs","split":"","minSdkVersion":"17"}}]
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":421},"path":"commonRelease-4.0.3 Alpha2.apk","properties":{"packageId":"org.autojs.autojs","split":"","minSdkVersion":"17"}}]