角色权限分配(没有比这讲的更细的了)
Posted fantongxue
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了角色权限分配(没有比这讲的更细的了)相关的知识,希望对你有一定的参考价值。
第一大块 数据库表设计
最基本的实现权限树分配的话是需要三张表就可以了,企业级项目的实现权限树需要设计五张六张的也有,那种逻辑关系也就相当复杂了,可是要实现的话,三张足矣!
- emp表
- tb_dept_power部门权限关联表
- tb_power权限表
查询权限的关键sql,表示查询登录的员工在权限表中所对应的权限信息
select id,name,parentid,state,iconcls,url from tb_power where id in (select powerid from tb_dept_power where deptno=(select deptno from emp where empno=?))
首先是emp
员工表,后台系统是员工进行登录,在登录之后记下员工的部门编号(每个员工),然后根据部门编号在tb_dept_power
部门权限关联表中查询该部门拥有的权限powerId
,再根据权限powerId
在tb_power
权限表中查询所对应的节点信息。
emp表
CREATE TABLE `NewTable` (
`empid` int(10) NOT NULL AUTO_INCREMENT COMMENT '员工序号' ,
`empno` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '员工编号' ,
`empname` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '员工姓名' ,
`empage` int(10) NULL DEFAULT NULL COMMENT '员工年龄' ,
`empsex` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '员工性别' ,
`deptno` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门号' ,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`empid`),
INDEX `fk_deptid_dept_id` (`deptno`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=45
ROW_FORMAT=DYNAMIC
;
tb_dept_power表
CREATE TABLE `NewTable` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'id' ,
`deptno` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门编号' ,
`powerid` int(15) NULL DEFAULT NULL COMMENT '权限id' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=638
ROW_FORMAT=DYNAMIC
;
tb_power表
CREATE TABLE `NewTable` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限菜单名称' ,
`parentid` int(11) NULL DEFAULT NULL COMMENT '父节点ID' ,
`state` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态(打开或者关闭)' ,
`iconcls` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '节点图标' ,
`url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址' ,
`addtime` date NULL DEFAULT NULL COMMENT '添加时间' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=85
ROW_FORMAT=DYNAMIC
;
第二大块 权限分配(用户身份不同登录展示菜单不同,并动态分配权限)
先写权限表的实体类TreeNode
public class TreeNode {
private int id;
private String text;
private int parentId;
private String state;
private String iconCls;
private String url;
private String checked;
/**
* 带参构造
* @param id
* @param text
* @param parentId
* @param state
* @param iconCls
* @param url
*/
public TreeNode(int id, String text, int parentId, String state,
String iconCls, String url) {
super();
this.id = id;
this.text = text;
this.parentId = parentId;
this.state = state;
this.iconCls = iconCls;
this.url = url;
}
public TreeNode(int id, String text, int parentId, String state,
String iconCls, String url, String checked) {
super();
this.id = id;
this.text = text;
this.parentId = parentId;
this.state = state;
this.iconCls = iconCls;
this.url = url;
this.checked = checked;
}
public String getChecked() {
return checked;
}
public void setChecked(String checked) {
this.checked = checked;
}
public TreeNode() {
super();
// TODO Auto-generated constructor stub
}
private List<TreeNode> children;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getParentId() {
return parentId;
}
public void setParentId(int parentId) {
this.parentId = parentId;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getIconCls() {
return iconCls;
}
public void setIconCls(String iconCls) {
this.iconCls = iconCls;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<TreeNode> getChildren() {
return children;
}
public void setChildren(List<TreeNode> children) {
this.children = children;
}
前台的easyUI树,路径是power/tree.do
<div class="easyui-panel" style="padding:5px">
<ul id="tree" class="easyui-tree" data-options="url:'power/tree.do',method:'get',animate:true"></ul>
</div>
controller去后台拿权限树的结点信息,在权限表里面
/**
*权限树json数据@return
*(根据登陆不同,展示权限菜单不同)
*/
@ResponseBody
@RequestMapping("power/tree")
public Object powerTree(){
return powerService.getPowerList();
}
powerService.getPowerList方法
public List<TreeNode> getPowerList() {
// TODO Auto-generated method stub
List<TreeNode> powerAllList = powerDao.getPowerList();
//临时的powerList
List<TreeNode> powerTempList = new ArrayList<TreeNode>();
//判断是否为空
if(powerAllList!=null&&powerAllList.size()>0){
for(TreeNode ptreeNode:powerAllList){
if(ptreeNode.getParentId()==0){//如果等于0,说明是一级节点
powerTempList.add(ptreeNode);
//递归绑定子节点
bindChirldren(ptreeNode,powerAllList);
}
}
}
return powerTempList;
}
递归绑定子节点的方法
/**
* 递归绑定所有子节点
* @param parentTreeNode
* @param powerAllList
*/
private void bindChirldren(TreeNode parentTreeNode,List<TreeNode> powerAllList ){
for(TreeNode chirldrenTreeNode:powerAllList){
if(parentTreeNode.getId()==chirldrenTreeNode.getParentId()){
//获取当前节点的所有子节点集合
List<TreeNode> children = parentTreeNode.getChildren();
if(children==null){//如果孩子节点为空,
List<TreeNode> childrenTempList = new ArrayList<TreeNode>();//实例化孩子集合
childrenTempList.add(chirldrenTreeNode);//添加子节点到集合里面
parentTreeNode.setChildren(childrenTempList);//设置孩子集合
}else{//不空,说明设置过
children.add(chirldrenTreeNode);//添加子节点到集合里面
}
//自己调用自己,找孩子
bindChirldren(chirldrenTreeNode,powerAllList);
}
}
}
powerDao.getPowerList方法
public List<TreeNode> getPowerList() {
// TODO Auto-generated method stub
//拿到登录人的手机号
Map map = (Map)session.getAttribute("emptel");
Object empno =null;
if(map!=null&&!map.isEmpty()) {
//拿到员工号
empno = map.get("empno");
}
//查询数据
List<Map<String, Object>> powerMapList = BaseDao.selectMap("select id,name,parentid,state,iconcls,url from tb_power where id in (select powerid from tb_dept_power where deptno=(select deptno from emp where empno=?))",
new Object[]{empno});
//定义返回列表,一个空的集合
List<TreeNode> powerList = new ArrayList<TreeNode>();
//判断是否为空
if(powerMapList!=null&&powerMapList.size()>0){
TreeNode treeNode = null;
//循环遍历,构造TreeNode对象,加入powerList
for(Map powerMap:powerMapList){
treeNode = new TreeNode(Integer.valueOf(powerMap.get("id")+""), powerMap.get("name")+"",
Integer.valueOf(powerMap.get("parentid")+""), powerMap.get("state")+""
, powerMap.get("iconcls")+"", powerMap.get("url")+"");
powerList.add(treeNode);
}
}
//返回权限树信息
return powerList;
}
在部门表中对部门进行授权操作
选中某一个部门的一行,然后进行添加权限
<a class="easyui-linkbutton" iconCls="icon-add" plain="true" onclick="givePower()">权限添加</a>
点击添加权限按钮弹出的表单框(部门名称、权限选择)
<div id="dlga" class="easyui-dialog" style="width:400px;height:680px;padding:10px 20px;top:50px;"
closed="true" buttons="#dlg-buttonsa">
<form id="fma" method="post">
<div class="fitem">
<label>部门名称:</label>
<input name="deptname" class="easyui-validatebox" required="true">
<input name="deptno" type="hidden">
<!--这个是隐藏的该部门对应的powerId,这里不是一个powerId,而是很多个powerId以逗号分隔符分割的字符串放到了这个input框里,例如1,13,14,15-->
<input id="powerIds" name="powerIds" type="hidden">
</div>
<div class="fitem">
<label>权限选择:</label>
<!--权限选择也是一个tree树-->
<div class="easyui-panel" style="padding:5px">
<ul id="ttree" class="easyui-tree" data-options="method:'get',animate:true,checkbox:true"></ul>
</div>
</div>
</form>
</div>
点击添加权限按钮的onclick的方法
function givePower(){
//easyUI框架的用法,可以获取到选中的哪一行,row.deptno可以获取到deptno部门编号
var row = $('#tt').datagrid('getSelected');
if (row){
//动态给树添加url
$("#ttree").tree({
url:'power/checkedTree.do?deptno='+row.deptno
});
//清空form表单
//fma是权限添加的form表单的id
$('#fma').form('clear');
//弹框
$('#dlga').dialog('open').dialog('setTitle','授权');
$('#fma').form('load',row);
}else{
$.messager.show({//easyui封装的浏览器右下角弹框
title: '信息提示',//弹框标题
msg: '请至少选中一行!!!'//提示信息
});
}
}
进入power/checkedTree的controller方法,根据部门编号给树添加url
/**
* 权限树json数据(选择)
* @return
*/
@ResponseBody
@RequestMapping("/checkedTree")
public Object checkedTree(String deptno){
return powerService.getCheckedList(deptno);
}
service层的getCheckedList方法
public List<TreeNode> getCheckedList(String roleId) {
List<TreeNode> powerAllList = powerDao.getCheckedList(roleId);
//临时的powerList
List<TreeNode> powerTempList = new ArrayList<TreeNode>();
//判断是否为空
if(powerAllList!=null&&powerAllList.size()>0){
for(TreeNode ptreeNode:powerAllList){
if(ptreeNode.getParentId()==0){//如果等于0,说明是一级节点
powerTempList.add(ptreeNode);
//递归绑定子节点
bindChirldren(ptreeNode,powerAllList);
}
}
}
return powerTempList;
}
dao层的getCheckedList方法
public List<TreeNode> getCheckedList(String deptno) {
//查询数据
List<Map<String, Object>> powerMapList = BaseDao.selectMap("select id,name,parentid,state,iconcls,url from tb_power", null);
//查询该角色拥有的所有权限
List<Map<String, Object>> powersByRoleId = BaseDao.selectMap("select powerid from tb_dept_power where deptno=?", new Object[]{deptno});
//定义返回列表,空的集合
List<TreeNode> powerList = new ArrayList<TreeNode>();
//判断是否为空
if(powerMapList!=null&&powerMapList.size()>0){
TreeNode treeNode = null;
//循环遍历,构造TreeNode对象,加入powerList
for(Map powerMap:powerMapList){
//判断是否为空
if(powersByRoleId!=null&&powersByRoleId.size()>0){
for(Map powers:powersByRoleId){
//如果用户该全选,让节点checked=true
if(powers.get("powerid").toString().equals(powerMap.get("id").toString())){
powerMap.put("checked",true);
break;
}
}
}
// treeNode = new TreeNode(id, text, parentId, state, iconCls, url);
treeNode = new TreeNode(Integer.valueOf(powerMap.get("id")+""), powerMap.get("name")+"",
Integer.valueOf(powerMap.get("parentid")+""), "open"
, powerMap.get("iconcls")+"", powerMap.get("url")+"",powerMap.get("checked")==null?null:powerMap.get("checked")+"");
powerList.add(treeNode);
}
}
return powerList;
}
保存对部门的授权
<div id="dlg-buttonsa">
<a class="easyui-linkbutton" iconCls="icon-ok" onclick="saveRoleAndPowers()"> 保存</a>
<a class="easyui-linkbutton" iconCls="icon-cancel" onclick="javascript:$('#dlga').dialog('close')">取消</a>
</div>
//保存授权
function saveRoleAndPowers(){
//获取选中的所有节点
var nodes = $('#ttree').tree('getChecked');
var s = '';
//循环每一个节点
for(var i=0; i<nodes.length; i++){
if (s != '') s += ',';
s += nodes[i].id;
}
//赋值,目的向服务器传送数据
$("#powerIds").val(s);
//提交form表单
$('#fma').form('submit',{//给form表单绑定submit事件
url: "power/saveRoleAndPower.do",//请求的url地址
onSubmit: function(){//对form表单校验
return $(this).form('validate');
},
success: function(result){//数据保存后的回调函数
var result = eval('('+result+')');//eval把json字符串转换为json对象 {'errorMsg':'添加失败'}
//alert(result.errorMsg);
//if (result.errorMsg){//判断,如果错误信息不为空 js非null 非0 即true
$.messager.show({//easyui封装的浏览器右下角弹框
title: '信息提示',//弹框标题
msg: result.showInfo//提示信息
});
//} else {
if(result.suc=='true'){
$('#dlga').dialog('close'); // 关闭弹出框
$('#tt').datagrid('reload'); // 刷新父窗口
}
//}
}
});
}
power/saveRoleAndPower保存授权的controller方法
/**
* 保存角色权限关联
* @param paramMap
* @return
*/
@ResponseBody
@RequestMapping("/saveRoleAndPower")
public Object saveRoleAndPower(@RequestParam Map paramMap){
//保存
int rs = powerService.saveRoleAndPower(paramMap);
Map map = new HashMap();
if(rs==0){
map.put("showInfo", "添加失败");
}else{
map.put("suc", "true");
map.put("showInfo", "添加成功");
}
return map;
}
saveRoleAndPower保存权限的service方法
public int saveRoleAndPower(Map map) {
String deptno = map.get("deptno")+"";//1
String powerIds = map.get("powerIds")+"";//1,2,3,11
//添加之前,删除所有原来该角色关联的所有权限ID ,达到更新目的
powerDao.deletePowersByRoleId(deptno);
//分割字符串,循环添加
String[] powerArr = powerIds.split(",");//[1,2,3,11]
boolean isSuc=true;
for(String powerId:powerArr){
int rs = powerDao.addRoleAndPowers(deptno, powerId);
if(rs==0){
isSuc=false;
}
}
if(isSuc)
return 1;
else
return 0;
}
powerDao.deletePowersByRoleId(deptno),删除所有原来该角色关联的所有权限ID,达到更新目的
public int deletePowersByRoleId(String deptno) {
return BaseDao.insertAndUpdateAndDelete("delete from tb_dept_power where deptno=?",
new Object[]{deptno});
}
powerDao.addRoleAndPowers(deptno, powerId)分割字符串,循环添加权限
public int addRoleAndPowers(String deptno, String powerId) {
return BaseDao.insertAndUpdateAndDelete("insert into tb_dept_power(deptno,powerid) values(?,?)",
new Object[]{deptno,powerId});
}
第三大块 Tree节点添加(对树节点进行操作)
添加树节点的页面
在父节点的input处为什么能拿到权限树节点对应的id
<form action="power/add.do" method="post">
<table border="1" width="800px" align="center" style='font-family:楷体;font-size:20px' class="table table-hover">
<tr align="center" class="warning">
<td >名称</td>
<td>
<input type="hidden" name="id">
<input type="text" name="name">
</td>
</tr>
<tr align="center" class="success">
<td>父节点</td>
<td><input class="easyui-combotree" name="parentid" data-options="url:'power/tree.do',method:'get',required:true" style="width:210px"></td>
</tr>
<tr align="center" class="danger">
<td>状态</td>
<td><input type="radio" name="state" value="open" checked="checked">打开
<input type="radio" name="state" value="closed">关闭
</td>
</tr>
<tr align="center" class="warning">
<td>图标</td>
<td><input type="text" name="iconcls"></td>
</tr>
<tr align="center" class="active">
<td>链接</td>
<td><input type="text" name="url"></td>
</tr>
<tr align="center" class="success">
<td colspan="2"><input type="submit" class="btn btn-success" value="添加"></td>
</tr>
</table>
</form>
父节点的input框去加载tree
路径还是权限树的数据
/**
* 权限树json数据(根据登陆不同,展示权限菜单不同)
* @return
*/
@ResponseBody //返回json格式数据
@RequestMapping("/powerTree")
public Object powerListTree(){
return powerService.getPowerList();
}
dao和service上面已经说过了!
然后写好节点信息之后提交表单,路径power/add
add添加节点的controller方法
/**
* 添加权限
* @param map
* @param response
* @throws IOException
*/
@RequestMapping("/add")
public void add(@RequestParam Map map,HttpServletResponse response) throws IOException{
int add = powerService.add(map);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html");
if(add==0){
response.getWriter().print("添加失败");
}else{
response.getWriter().print("<script> window.parent.parent.location.href=window.parent.parent.location.href </script>");
}
}
add添加节点的service方法
public int add(Map map) {
return powerDao.add(map);
}
add添加节点的dao方法
public int add(Map map) {
return BaseDao.insertAndUpdateAndDelete("insert into tb_power (name,parentid,state,iconcls,url) values(?,?,?,?,?)",
new Object[]{map.get("name"),"".equals(map.get("parentid"))?0:map.get("parentid"),map.get("state"),map.get("iconcls"),map.get("url")});
}
第四大块 Tree节点信息修改(同样也是对树节点操作)
修改树节点的页面
<script type="text/javascript">
//动态添加页签,先加载一棵树
$(function(){
$('#tr').tree({
//easyUI用法,点击树节点时触发此方法,node参数是被点击的节点的所有信息
onClick:function(node){
$(this).tree(node.state === 'closed' ? 'expand' : 'collapse', node.target);
node.state = node.state === 'closed' ? 'open' : 'closed';
//当选中一个节点的时候,下面的操作的iframe跳转到修改页面并把树的信息带过去
//如果不选择节点,那iframe就默认指向添加节点的页面
$("#ifm").attr("src","power/toUpdate.do?id="+node.id);
}
});
});
</script>
</head>
<body class="easyui-layout">
<div data-options="region:'west',split:true,title:'功能菜单'" style="width:220px;padding:10px;">
<div class="easyui-panel" style="padding:5px" >
<ul id="tr" class="easyui-tree" data-options="url:'power/tree.do',method:'post',animate:true"></ul>
</div>
</div>
<div data-options="region:'center',title:'显示区'">
<div id="ta" class="easyui-tabs" style="width:100%;height:100%;">
<div title="操作" >
<iframe id="ifm" scrolling="auto" frameborder="0" src="power/addp.do" style="width:100%;height:100%;"></iframe>
</div>
</div>
</div>
</body>
toUpdate修改节点的controller方法
/**
* 去更新
* @param id
* @param model
* @return
*/
@RequestMapping("/toUpdate")
public String toUpdate(Integer id,Model model){
List<Map<String, Object>> list = powerService.getListById(id);
if(list!=null&&list.size()>0){
model.addAttribute("map", list.get(0));
//跳转到去更新的controller方法,该方法跳转到更新的页面
return "power/update";
}
return null;
}
service的powerService.getListById(id)方法
public List<Map<String, Object>> getListById(int id) {
return powerDao.getListById(id);
}
dao的powerService.getListById(id)方法
得到被点击的节点的信息
public List<Map<String, Object>> getListById(int id) {
return BaseDao.selectMap("select id,name,parentid,state,iconcls,url from tb_power where id=?", new Object[]{id});
}
跳转到更新节点信息的页面,把信息带过去
<form action="power/update.do" method="post">
<table border="1" width="400px" align="center" class="table table-hover" style='font-family:楷体;font-size:20px'>
<tr align="center">
<td>名称</td>
<td>
<input type="hidden" name="id" value="${map.id}">
<input type="text" name="name" value="${map.name}">
</td>
</tr>
<tr align="center">
<td>父节点</td>
<td><input class="easyui-combotree" name="parentid" value="${map.parentid}" data-options="url:'power/tree.do',method:'get',required:true" style="width"></td>
</tr>
<tr align="center">
<td>状态</td>
<td><input type="radio" name="state" ${map.state=='open'?'checked=checked':'' } value="open">打开
<input type="radio" name="state" ${map.state=='closed'?'checked=checked':'' } value="closed">关闭
</td>
</tr>
<tr align="center">
<td>图标</td>
<td><input type="text" name="iconcls" vlaue="${map.iconcls}"></td>
</tr>
<tr align="center">
<td>链接</td>
<td><input type="text" name="url" value="${map.url}"></td>
</tr>
<tr align="center">
<td colspan="2"><input type="submit" value="修改" class='btn btn-success'>
<input type="button" onclick="del('${map.id}','${map.url}')" value="删除" class="btn btn-danger"></td>
</tr>
</table>
</form>
js判断该节点下面是否有子节点
如果选中的该节点的url是空的话就是有子节点,这里是通过这样子判断的,具体判断由权限表的设计决定。
<script type="text/javascript">
function del(id,url){
if(url==null||url==''||url=='null'){
$.messager.show({
title: '错误信息',
msg: '当前节点有子节点,请删除子节点后重试!'
});
}else{
$.messager.confirm('Confirm','确认删除吗 ?',function(r){
if (r){
location.href='power/del.do?id='+id;
}
});
}
}
</script>
确认修改节点信息,提交表单,路径power/update
/**
* 更新权限 并刷新
* @param map
* @param response
* @throws IOException
*/
@RequestMapping("/update")
public void update(@RequestParam Map map,HttpServletResponse response) throws IOException{
int update = powerService.update(map);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html");
if(update==0){
response.getWriter().print("更新失败");
}else{
response.getWriter().print("<script> window.parent.parent.location.href=window.parent.parent.location.href </script>");
}
}
service的update方法
public int update(Map map) {
return powerDao.update(map);
}
dao的update方法
public int update(Map map) {
return BaseDao.insertAndUpdateAndDelete("update tb_power set name=?,parentid=?,state=?,iconcls=?,url=? where id=?",
new Object[]{map.get("name"),map.get("parentid"),map.get("state"),map.get("iconcls"),map.get("url"),map.get("id")});
}
第五大块 删除节点
在上面js判断在删除时该节点下面是否有子节点的时候已经说明了删除了路径location.href=‘power/del.do?id=‘+id;
删除节点的controller方法
/**
* 删除权限树节点
* @param id
* @param response
* @throws IOException
*/
@RequestMapping("/del")
public void del(Integer id,HttpServletResponse response) throws IOException{
int del = powerService.del(id);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html");
if(del==0){
response.getWriter().print("删除失败");
}else{
response.getWriter().print("<script> window.parent.parent.location.href=window.parent.parent.location.href </script>");
}
}
删除节点的service方法
public int del(int id) {
powerDao.del2(id);
return powerDao.del(id);
}
删除节点的dao方法
public int del(int id) {
return BaseDao.insertAndUpdateAndDelete("delete from tb_power where id=?", new Object[]{id});
}
总结
权限树的实现,还是需要靠一些Tree的组件才行,比如z-tree
,easyUI
和layUI
等这些前台框架也都有自己的Tree
树的组件,建议在学习权限树分配之前先熟悉一款Tree的组件并学习它的使用,诸如节点操作的js
方法!
这里我用的是easyUI
的组件。
以上是关于角色权限分配(没有比这讲的更细的了)的主要内容,如果未能解决你的问题,请参考以下文章
首发:深入探索 Flutter 性能优化,再也没有比这还全的了